Skip to content

Commit 2fe4920

Browse files
committed
fix: Fixed handling of files containin CRLF lines endings
Fixes #15
1 parent b41c1b9 commit 2fe4920

File tree

7 files changed

+135
-15
lines changed

7 files changed

+135
-15
lines changed

.gitattributes

+1
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
* text eol=lf
2+
src/test/resources/testcrlf* text eol=crlf

src/main/java/org/codejive/properties/Properties.java

+32-2
Original file line numberDiff line numberDiff line change
@@ -903,21 +903,51 @@ public void store(Writer writer, String... comment) throws IOException {
903903
Cursor pos = first();
904904
if (comment.length > 0) {
905905
pos = skipHeaderCommentLines();
906+
String nl = determineNewline();
906907
List<String> newcs = normalizeComments(Arrays.asList(comment), "# ");
907908
for (String c : newcs) {
908909
writer.write(new PropertiesParser.Token(PropertiesParser.Type.COMMENT, c).getRaw());
909-
writer.write(PropertiesParser.Token.EOL.getRaw());
910+
writer.write(nl);
910911
}
911912
// We write an extra empty line so this comment won't be taken as part of the first
912913
// property
913-
writer.write(PropertiesParser.Token.EOL.getRaw());
914+
writer.write(nl);
914915
}
915916
while (pos.hasToken()) {
916917
writer.write(pos.raw());
917918
pos.next();
918919
}
919920
}
920921

922+
/**
923+
* This method determines the newline string to use when generating line terminators. It looks
924+
* at all existing line terminators and will use those for any new lines. In case of ambiguity
925+
* (a file contains both LF and CRLF terminators) it will return the system's default line
926+
* ending.
927+
*
928+
* @return A string containing the line ending to use
929+
*/
930+
private String determineNewline() {
931+
boolean lf = false;
932+
boolean crlf = false;
933+
for (PropertiesParser.Token token : tokens) {
934+
if (token.isWs()) {
935+
if (token.raw.endsWith("/r/n")) {
936+
crlf = true;
937+
} else if (token.raw.endsWith("/n")) {
938+
lf = true;
939+
}
940+
}
941+
}
942+
if (lf && crlf) {
943+
return System.lineSeparator();
944+
} else if (crlf) {
945+
return "/r/n";
946+
} else {
947+
return "\n";
948+
}
949+
}
950+
921951
private Cursor skipHeaderCommentLines() {
922952
Cursor pos = first();
923953
// Skip a single following whitespace if it is NOT an EOL token

src/main/java/org/codejive/properties/PropertiesParser.java

+7-7
Original file line numberDiff line numberDiff line change
@@ -283,8 +283,8 @@ private void addChar(int ch) throws IOException {
283283
}
284284

285285
private void readEol(int ch) throws IOException {
286-
if (ch == '\n') {
287-
if (peekChar() == '\r') {
286+
if (ch == '\r') {
287+
if (peekChar() == '\n') {
288288
str.append((char) readChar());
289289
}
290290
}
@@ -327,15 +327,15 @@ static String unescape(String escape) {
327327
txt.append((char) Integer.parseInt(num, 16));
328328
i += 4;
329329
break;
330-
case '\n':
331-
// Skip the next character if it's a '\r'
332-
if (i < escape.length() && escape.charAt(i + 1) == '\r') {
330+
case '\r':
331+
// Skip the next character if it's a '\n'
332+
if (i < (escape.length() - 1) && escape.charAt(i + 1) == '\n') {
333333
i++;
334334
}
335335
// fall-through!
336-
case '\r':
336+
case '\n':
337337
// Skip any leading whitespace
338-
while (i < escape.length()
338+
while (i < (escape.length() - 1)
339339
&& isWhitespaceChar(ch = escape.charAt(i + 1))
340340
&& !isEol(ch)) {
341341
i++;

src/test/java/org/codejive/properties/TestProperties.java

+56
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,53 @@ void testLoad() throws IOException, URISyntaxException {
6161
new AbstractMap.SimpleEntry<>("key.4", "\\u1234\u1234"));
6262
}
6363

64+
void testLoadCrLf() throws IOException, URISyntaxException {
65+
Properties p = Properties.loadProperties(getResource("/testcrlf.properties"));
66+
assertThat(p).size().isEqualTo(7);
67+
assertThat(p.keySet())
68+
.containsExactly(
69+
"one", "two", "three", " with spaces", "altsep", "multiline", "key.4");
70+
assertThat(p.rawKeySet())
71+
.containsExactly(
72+
"one", "two", "three", "\\ with\\ spaces", "altsep", "multiline", "key.4");
73+
assertThat(p.values())
74+
.containsExactly(
75+
"simple",
76+
"value containing spaces",
77+
"and escapes\n\t\r\f",
78+
"everywhere ",
79+
"value",
80+
"one two three",
81+
"\u1234\u1234");
82+
assertThat(p.rawValues())
83+
.containsExactly(
84+
"simple",
85+
"value containing spaces",
86+
"and escapes\\n\\t\\r\\f",
87+
"everywhere ",
88+
"value",
89+
"one \\\n two \\\n\tthree",
90+
"\\u1234\u1234");
91+
assertThat(p.entrySet())
92+
.containsExactly(
93+
new AbstractMap.SimpleEntry<>("one", "simple"),
94+
new AbstractMap.SimpleEntry<>("two", "value containing spaces"),
95+
new AbstractMap.SimpleEntry<>("three", "and escapes\n\t\r\f"),
96+
new AbstractMap.SimpleEntry<>(" with spaces", "everywhere "),
97+
new AbstractMap.SimpleEntry<>("altsep", "value"),
98+
new AbstractMap.SimpleEntry<>("multiline", "one two three"),
99+
new AbstractMap.SimpleEntry<>("key.4", "\u1234\u1234"));
100+
assertThat(p.rawEntrySet())
101+
.containsExactly(
102+
new AbstractMap.SimpleEntry<>("one", "simple"),
103+
new AbstractMap.SimpleEntry<>("two", "value containing spaces"),
104+
new AbstractMap.SimpleEntry<>("three", "and escapes\\n\\t\\r\\f"),
105+
new AbstractMap.SimpleEntry<>("\\ with\\ spaces", "everywhere "),
106+
new AbstractMap.SimpleEntry<>("altsep", "value"),
107+
new AbstractMap.SimpleEntry<>("multiline", "one \\\n two \\\n\tthree"),
108+
new AbstractMap.SimpleEntry<>("key.4", "\\u1234\u1234"));
109+
}
110+
64111
@Test
65112
void testStore() throws IOException, URISyntaxException {
66113
Path f = getResource("/test.properties");
@@ -70,6 +117,15 @@ void testStore() throws IOException, URISyntaxException {
70117
assertThat(sw.toString()).isEqualTo(readAll(f));
71118
}
72119

120+
@Test
121+
void testStoreCrLf() throws IOException, URISyntaxException {
122+
Path f = getResource("/testcrlf.properties");
123+
Properties p = Properties.loadProperties(f);
124+
StringWriter sw = new StringWriter();
125+
p.store(sw);
126+
assertThat(sw.toString()).isEqualTo(readAll(f));
127+
}
128+
73129
@Test
74130
void testStoreHeader() throws IOException, URISyntaxException {
75131
Path f = getResource("/test.properties");

src/test/java/org/codejive/properties/TestPropertiesParser.java

+6-6
Original file line numberDiff line numberDiff line change
@@ -19,17 +19,17 @@ public class TestPropertiesParser {
1919
+ "\n"
2020
+ "! comment3\n"
2121
+ "one=simple\n"
22-
+ "two=value containing spaces\n\r"
22+
+ "two=value containing spaces\r\n"
2323
+ "# another comment\n"
2424
+ "! and a comment\n"
2525
+ "! block\n"
2626
+ "three=and escapes\\n\\t\\r\\f\n"
2727
+ " \\ with\\ spaces = everywhere \n"
2828
+ "altsep:value\n"
2929
+ "multiline = one \\\n"
30-
+ " two \\\n\r"
30+
+ " two \\\r\n"
3131
+ "\tthree\n"
32-
+ "key.4 = \\u1234\n\r"
32+
+ "key.4 = \\u1234\r\n"
3333
+ " # final comment";
3434

3535
@Test
@@ -53,7 +53,7 @@ void testTokens() throws IOException {
5353
new Token(Type.KEY, "two"),
5454
new Token(Type.SEPARATOR, "="),
5555
new Token(Type.VALUE, "value containing spaces"),
56-
new Token(Type.WHITESPACE, "\n\r"),
56+
new Token(Type.WHITESPACE, "\r\n"),
5757
new Token(Type.COMMENT, "# another comment"),
5858
new Token(Type.WHITESPACE, "\n"),
5959
new Token(Type.COMMENT, "! and a comment"),
@@ -75,12 +75,12 @@ void testTokens() throws IOException {
7575
new Token(Type.WHITESPACE, "\n"),
7676
new Token(Type.KEY, "multiline"),
7777
new Token(Type.SEPARATOR, " = "),
78-
new Token(Type.VALUE, "one \\\n two \\\n\r\tthree", "one two three"),
78+
new Token(Type.VALUE, "one \\\n two \\\r\n\tthree", "one two three"),
7979
new Token(Type.WHITESPACE, "\n"),
8080
new Token(Type.KEY, "key.4"),
8181
new Token(Type.SEPARATOR, " = "),
8282
new Token(Type.VALUE, "\\u1234", "\u1234"),
83-
new Token(Type.WHITESPACE, "\n\r"),
83+
new Token(Type.WHITESPACE, "\r\n"),
8484
new Token(Type.WHITESPACE, " "),
8585
new Token(Type.COMMENT, "# final comment"));
8686
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# A header line
2+
3+
! comment3
4+
one=simple
5+
two=value containing spaces
6+
# another comment
7+
! and a comment
8+
! block
9+
three=and escapes\n\t\r\f
10+
\ with\ spaces = everywhere
11+
altsep:value
12+
multiline = one \
13+
two \
14+
three
15+
key.4 = \u1234
16+
# final comment
+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
#comment1
2+
# comment2
3+
4+
! comment3
5+
one=simple
6+
two=value containing spaces
7+
# another comment
8+
! and a comment
9+
! block
10+
three=and escapes\n\t\r\f
11+
\ with\ spaces = everywhere
12+
altsep:value
13+
multiline = one \
14+
two \
15+
three
16+
key.4 = \u1234
17+
# final comment

0 commit comments

Comments
 (0)