diff --git a/.gitattributes b/.gitattributes index fcadb2c..bcc0efa 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1 +1,2 @@ * text eol=lf +src/test/resources/testcrlf* text eol=crlf diff --git a/src/main/java/org/codejive/properties/Properties.java b/src/main/java/org/codejive/properties/Properties.java index 81e5429..510187a 100644 --- a/src/main/java/org/codejive/properties/Properties.java +++ b/src/main/java/org/codejive/properties/Properties.java @@ -903,14 +903,15 @@ public void store(Writer writer, String... comment) throws IOException { Cursor pos = first(); if (comment.length > 0) { pos = skipHeaderCommentLines(); + String nl = determineNewline(); List newcs = normalizeComments(Arrays.asList(comment), "# "); for (String c : newcs) { writer.write(new PropertiesParser.Token(PropertiesParser.Type.COMMENT, c).getRaw()); - writer.write(PropertiesParser.Token.EOL.getRaw()); + writer.write(nl); } // We write an extra empty line so this comment won't be taken as part of the first // property - writer.write(PropertiesParser.Token.EOL.getRaw()); + writer.write(nl); } while (pos.hasToken()) { writer.write(pos.raw()); @@ -918,6 +919,35 @@ public void store(Writer writer, String... comment) throws IOException { } } + /** + * This method determines the newline string to use when generating line terminators. It looks + * at all existing line terminators and will use those for any new lines. In case of ambiguity + * (a file contains both LF and CRLF terminators) it will return the system's default line + * ending. + * + * @return A string containing the line ending to use + */ + private String determineNewline() { + boolean lf = false; + boolean crlf = false; + for (PropertiesParser.Token token : tokens) { + if (token.isWs()) { + if (token.raw.endsWith("/r/n")) { + crlf = true; + } else if (token.raw.endsWith("/n")) { + lf = true; + } + } + } + if (lf && crlf) { + return System.lineSeparator(); + } else if (crlf) { + return "/r/n"; + } else { + return "\n"; + } + } + private Cursor skipHeaderCommentLines() { Cursor pos = first(); // Skip a single following whitespace if it is NOT an EOL token diff --git a/src/main/java/org/codejive/properties/PropertiesParser.java b/src/main/java/org/codejive/properties/PropertiesParser.java index 8cd90f3..c5f73bc 100644 --- a/src/main/java/org/codejive/properties/PropertiesParser.java +++ b/src/main/java/org/codejive/properties/PropertiesParser.java @@ -283,8 +283,8 @@ private void addChar(int ch) throws IOException { } private void readEol(int ch) throws IOException { - if (ch == '\n') { - if (peekChar() == '\r') { + if (ch == '\r') { + if (peekChar() == '\n') { str.append((char) readChar()); } } @@ -327,15 +327,15 @@ static String unescape(String escape) { txt.append((char) Integer.parseInt(num, 16)); i += 4; break; - case '\n': - // Skip the next character if it's a '\r' - if (i < escape.length() && escape.charAt(i + 1) == '\r') { + case '\r': + // Skip the next character if it's a '\n' + if (i < (escape.length() - 1) && escape.charAt(i + 1) == '\n') { i++; } // fall-through! - case '\r': + case '\n': // Skip any leading whitespace - while (i < escape.length() + while (i < (escape.length() - 1) && isWhitespaceChar(ch = escape.charAt(i + 1)) && !isEol(ch)) { i++; diff --git a/src/test/java/org/codejive/properties/TestProperties.java b/src/test/java/org/codejive/properties/TestProperties.java index 7716552..b8d7601 100644 --- a/src/test/java/org/codejive/properties/TestProperties.java +++ b/src/test/java/org/codejive/properties/TestProperties.java @@ -61,6 +61,53 @@ void testLoad() throws IOException, URISyntaxException { new AbstractMap.SimpleEntry<>("key.4", "\\u1234\u1234")); } + void testLoadCrLf() throws IOException, URISyntaxException { + Properties p = Properties.loadProperties(getResource("/testcrlf.properties")); + assertThat(p).size().isEqualTo(7); + assertThat(p.keySet()) + .containsExactly( + "one", "two", "three", " with spaces", "altsep", "multiline", "key.4"); + assertThat(p.rawKeySet()) + .containsExactly( + "one", "two", "three", "\\ with\\ spaces", "altsep", "multiline", "key.4"); + assertThat(p.values()) + .containsExactly( + "simple", + "value containing spaces", + "and escapes\n\t\r\f", + "everywhere ", + "value", + "one two three", + "\u1234\u1234"); + assertThat(p.rawValues()) + .containsExactly( + "simple", + "value containing spaces", + "and escapes\\n\\t\\r\\f", + "everywhere ", + "value", + "one \\\n two \\\n\tthree", + "\\u1234\u1234"); + assertThat(p.entrySet()) + .containsExactly( + new AbstractMap.SimpleEntry<>("one", "simple"), + new AbstractMap.SimpleEntry<>("two", "value containing spaces"), + new AbstractMap.SimpleEntry<>("three", "and escapes\n\t\r\f"), + new AbstractMap.SimpleEntry<>(" with spaces", "everywhere "), + new AbstractMap.SimpleEntry<>("altsep", "value"), + new AbstractMap.SimpleEntry<>("multiline", "one two three"), + new AbstractMap.SimpleEntry<>("key.4", "\u1234\u1234")); + assertThat(p.rawEntrySet()) + .containsExactly( + new AbstractMap.SimpleEntry<>("one", "simple"), + new AbstractMap.SimpleEntry<>("two", "value containing spaces"), + new AbstractMap.SimpleEntry<>("three", "and escapes\\n\\t\\r\\f"), + new AbstractMap.SimpleEntry<>("\\ with\\ spaces", "everywhere "), + new AbstractMap.SimpleEntry<>("altsep", "value"), + new AbstractMap.SimpleEntry<>("multiline", "one \\\n two \\\n\tthree"), + new AbstractMap.SimpleEntry<>("key.4", "\\u1234\u1234")); + } + @Test void testStore() throws IOException, URISyntaxException { Path f = getResource("/test.properties"); @@ -70,6 +117,15 @@ void testStore() throws IOException, URISyntaxException { assertThat(sw.toString()).isEqualTo(readAll(f)); } + @Test + void testStoreCrLf() throws IOException, URISyntaxException { + Path f = getResource("/testcrlf.properties"); + Properties p = Properties.loadProperties(f); + StringWriter sw = new StringWriter(); + p.store(sw); + assertThat(sw.toString()).isEqualTo(readAll(f)); + } + @Test void testStoreHeader() throws IOException, URISyntaxException { Path f = getResource("/test.properties"); diff --git a/src/test/java/org/codejive/properties/TestPropertiesParser.java b/src/test/java/org/codejive/properties/TestPropertiesParser.java index 4ae32f2..cf31786 100644 --- a/src/test/java/org/codejive/properties/TestPropertiesParser.java +++ b/src/test/java/org/codejive/properties/TestPropertiesParser.java @@ -19,7 +19,7 @@ public class TestPropertiesParser { + "\n" + "! comment3\n" + "one=simple\n" - + "two=value containing spaces\n\r" + + "two=value containing spaces\r\n" + "# another comment\n" + "! and a comment\n" + "! block\n" @@ -27,9 +27,9 @@ public class TestPropertiesParser { + " \\ with\\ spaces = everywhere \n" + "altsep:value\n" + "multiline = one \\\n" - + " two \\\n\r" + + " two \\\r\n" + "\tthree\n" - + "key.4 = \\u1234\n\r" + + "key.4 = \\u1234\r\n" + " # final comment"; @Test @@ -53,7 +53,7 @@ void testTokens() throws IOException { new Token(Type.KEY, "two"), new Token(Type.SEPARATOR, "="), new Token(Type.VALUE, "value containing spaces"), - new Token(Type.WHITESPACE, "\n\r"), + new Token(Type.WHITESPACE, "\r\n"), new Token(Type.COMMENT, "# another comment"), new Token(Type.WHITESPACE, "\n"), new Token(Type.COMMENT, "! and a comment"), @@ -75,12 +75,12 @@ void testTokens() throws IOException { new Token(Type.WHITESPACE, "\n"), new Token(Type.KEY, "multiline"), new Token(Type.SEPARATOR, " = "), - new Token(Type.VALUE, "one \\\n two \\\n\r\tthree", "one two three"), + new Token(Type.VALUE, "one \\\n two \\\r\n\tthree", "one two three"), new Token(Type.WHITESPACE, "\n"), new Token(Type.KEY, "key.4"), new Token(Type.SEPARATOR, " = "), new Token(Type.VALUE, "\\u1234", "\u1234"), - new Token(Type.WHITESPACE, "\n\r"), + new Token(Type.WHITESPACE, "\r\n"), new Token(Type.WHITESPACE, " "), new Token(Type.COMMENT, "# final comment")); } diff --git a/src/test/resources/testcrlf-storeheader.properties b/src/test/resources/testcrlf-storeheader.properties new file mode 100644 index 0000000..3ab1ef7 --- /dev/null +++ b/src/test/resources/testcrlf-storeheader.properties @@ -0,0 +1,16 @@ +# A header line + +! comment3 +one=simple +two=value containing spaces +# another comment +! and a comment +! block +three=and escapes\n\t\r\f +\ with\ spaces = everywhere +altsep:value +multiline = one \ + two \ + three +key.4 = \u1234ሴ +# final comment diff --git a/src/test/resources/testcrlf.properties b/src/test/resources/testcrlf.properties new file mode 100644 index 0000000..f2d73bd --- /dev/null +++ b/src/test/resources/testcrlf.properties @@ -0,0 +1,17 @@ +#comment1 +# comment2 + +! comment3 +one=simple +two=value containing spaces +# another comment +! and a comment +! block +three=and escapes\n\t\r\f +\ with\ spaces = everywhere +altsep:value +multiline = one \ + two \ + three +key.4 = \u1234ሴ +# final comment