package net.i2p.addressbook; import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.io.StringReader; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Properties; import net.i2p.client.naming.HostTxtEntry; import net.i2p.data.DataHelper; import net.i2p.util.SecureFile; import net.i2p.util.SecureFileOutputStream; import net.i2p.util.SystemVersion; /** * Utility class providing methods to parse and write files in a hosts.txt file * format, and subscription file format. * * @since 0.9.26 modified from ConfigParser */ class HostTxtParser { private static final boolean isWindows = SystemVersion.isWindows(); /** * Return a Map using the contents of BufferedReader input. input must have * a single key, value pair on each line, in the format: key=value. Lines * starting with '#' or ';' are considered comments, and ignored. Lines that * are obviously not in the format key=value are also ignored. * The key is converted to lower case. * * Returned map will not contain null ("remove") entries. * * @param input * A BufferedReader with lines in key=value format to parse into * a Map. * @return A Map containing the key, value pairs from input. * @throws IOException * if the BufferedReader cannot be read. * */ private static Map<String, HostTxtEntry> parse(BufferedReader input) throws IOException { try { Map<String, HostTxtEntry> result = new HashMap<String, HostTxtEntry>(); String inputLine; while ((inputLine = input.readLine()) != null) { HostTxtEntry he = parse(inputLine, false); if (he == null) continue; result.put(he.getName(), he); } return result; } finally { try { input.close(); } catch (IOException ioe) {} } } /** * Return a HostTxtEntry from the contents of the inputLine. * * @param inputLine key=value[#!k1=v1#k2=v2...] * @param allowCommandOnly if true, a line starting with #! will return * a HostTxtEntry with a null name and dest and non-null props. * If false, these lines will return null. * @return null if no entry found or on error */ public static HostTxtEntry parse(String inputLine, boolean allowCommandOnly) { if (inputLine.startsWith(";")) return null; int comment = inputLine.indexOf('#'); String kv; String sprops; if (comment >= 0) { int shebang = inputLine.indexOf(HostTxtEntry.PROPS_SEPARATOR); if (shebang == comment && shebang + 2 < inputLine.length()) { if (comment == 0 && !allowCommandOnly) return null; sprops = inputLine.substring(shebang + 2); } else { if (comment == 0) return null; sprops = null; } kv = inputLine.substring(0, comment); } else { sprops = null; kv = inputLine; } String name, dest; if (comment != 0) { // we have a name=dest String[] splitLine = DataHelper.split(kv, "=", 2); if (splitLine.length < 2) return null; name = splitLine[0].trim().toLowerCase(Locale.US); dest = splitLine[1].trim(); if (name.length() == 0 || dest.length() == 0) return null; } else { // line starts with #!, rv will contain props only name = null; dest = null; } HostTxtEntry he; if (sprops != null) { try { he = new HostTxtEntry(name, dest, sprops); } catch (IllegalArgumentException iae) { return null; } } else { he = new HostTxtEntry(name, dest); } return he; } /** * Return a Map using the contents of the File file. See parse(BufferedReader) * for details of the input format. * * Returned map will not contain null ("remove") entries. * * @param file * A File to parse. * @return A Map containing the key, value pairs from file. * @throws IOException * if file cannot be read. */ public static Map<String, HostTxtEntry> parse(File file) throws IOException { FileInputStream fileStream = null; try { fileStream = new FileInputStream(file); BufferedReader input = new BufferedReader(new InputStreamReader( fileStream, "UTF-8")); Map<String, HostTxtEntry> rv = parse(input); return rv; } finally { if (fileStream != null) { try { fileStream.close(); } catch (IOException ioe) {} } } } /** * Return a Map using the contents of the File file. If file cannot be read, * use map instead, and write the result to where file should have been. * * Returned map will not contain null ("remove") entries. * * @param file * A File to attempt to parse. * @param map * A Map containing values to use as defaults. * @return A Map containing the key, value pairs from file, or if file * cannot be read, map. */ public static Map<String, HostTxtEntry> parse(File file, Map<String, HostTxtEntry> map) { Map<String, HostTxtEntry> result; try { result = parse(file); for (Map.Entry<String, HostTxtEntry> entry : map.entrySet()) { if (!result.containsKey(entry.getKey())) result.put(entry.getKey(), entry.getValue()); } } catch (IOException exp) { result = map; try { write(result, file); } catch (IOException exp2) { } } return result; } /** * Write contents of Map map to BufferedWriter output. Output is written * with one key, value pair on each line, in the format: key=value. * * @param map * A Map to write to output. * @param output * A BufferedWriter to write the Map to. * @throws IOException * if the BufferedWriter cannot be written to. */ private static void write(Map<String, HostTxtEntry> map, BufferedWriter output) throws IOException { try { for (Map.Entry<String, HostTxtEntry> entry : map.entrySet()) { entry.getValue().write(output); } } finally { try { output.close(); } catch (IOException ioe) {} } } /** * Write contents of Map map to the File file. Output is written * with one key, value pair on each line, in the format: key=value. * Write to a temp file in the same directory and then rename, to not corrupt * simultaneous accesses by the router. Except on Windows where renameTo() * will fail if the target exists. * * @param map * A Map to write to file. * @param file * A File to write the Map to. * @throws IOException * if file cannot be written to. */ public static void write(Map<String, HostTxtEntry> map, File file) throws IOException { boolean success = false; if (!isWindows) { File tmp = SecureFile.createTempFile("temp-", ".tmp", file.getAbsoluteFile().getParentFile()); write(map, new BufferedWriter(new OutputStreamWriter(new SecureFileOutputStream(tmp), "UTF-8"))); success = tmp.renameTo(file); if (!success) { tmp.delete(); //System.out.println("Warning: addressbook rename fail from " + tmp + " to " + file); } } if (!success) { // hmm, that didn't work, try it the old way write(map, new BufferedWriter(new OutputStreamWriter(new SecureFileOutputStream(file), "UTF-8"))); } } /** * Usage: HostTxtParser [-q] validate example.i2p=b64dest[#!key1=val1#key2=val2] */ public static void main(String[] args) throws Exception { boolean quiet = false; if (args.length > 0 && args[0].equals("-q")) { quiet = true; args = java.util.Arrays.copyOfRange(args, 1, args.length); } if (args.length != 2 || !args[0].equals("validate")) { System.err.println("Usage: HostTxtParser validate example.i2p=b64dest[#!key1=val1#key2=val2]"); System.exit(1); } HostTxtEntry e = parse(args[1].trim(), false); if (e == null) { if (!quiet) System.err.println("Bad format"); System.exit(2); } if (!e.hasValidSig()) { if (!quiet) System.err.println("Bad signature"); System.exit(3); } Properties p = e.getProps(); if (p != null) { if (p.containsKey(HostTxtEntry.PROP_ACTION) || p.containsKey(HostTxtEntry.PROP_OLDDEST) || p.containsKey(HostTxtEntry.PROP_OLDNAME) || p.containsKey(HostTxtEntry.PROP_OLDSIG)) { if (!e.hasValidSig()) { if (!quiet) System.err.println("Bad inner signature"); System.exit(4); } } } if (!quiet) System.err.println("Good signature for " + e.getName()); System.exit(0); } /**** public static void test(String[] args) throws Exception { File f = new File("tmp-hosts.txt"); Map<String, HostTxtEntry> map = parse(f); for (HostTxtEntry e : map.values()) { System.out.println("Host: " + e.getName() + "\nDest: " + e.getDest() + "\nAction: " + (e.getProps() != null ? e.getProps().getProperty("action") : "(none)") + "\nValid Inner? " + e.hasValidInnerSig() + "\nValid? " + e.hasValidSig() + '\n'); } } ****/ }