Skip to content

Commit 11e74d7

Browse files
authored
Merge changes in version 2.25.4 back to 2.x (#4085)
1 parent d375a66 commit 11e74d7

38 files changed

Lines changed: 1919 additions & 107 deletions

File tree

.github/workflows/build.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ on:
2121
push:
2222
branches:
2323
- "2.x"
24+
- "2.25.x"
2425
- "release/2*"
2526
pull_request:
2627

log4j-1.2-api/src/test/java/org/apache/log4j/layout/Log4j1XmlLayoutTest.java

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,8 @@ void testWithPropertiesAndLocationInfo() {
7272
final String expected = "<log4j:event logger=\"a.B\" timestamp=\"" + event.getTimeMillis()
7373
+ "\" level=\"INFO\" thread=\"main\">\r\n"
7474
+ "<log4j:message><![CDATA[Hello, World]]></log4j:message>\r\n"
75-
+ "<log4j:locationInfo class=\"pack.MyClass\" method=\"myMethod\" file=\"MyClass.java\" line=\"17\"/>\r\n"
75+
+ "<log4j:locationInfo class=\"pack.MyClass\" method=\"myMethod\" file=\"MyClass.java\""
76+
+ " line=\"17\"/>\r\n"
7677
+ "<log4j:properties>\r\n"
7778
+ "<log4j:data name=\"key1\" value=\"value1\"/>\r\n"
7879
+ "<log4j:data name=\"key2\" value=\"value2\"/>\r\n"
@@ -81,4 +82,43 @@ void testWithPropertiesAndLocationInfo() {
8182

8283
assertEquals(expected, result);
8384
}
85+
86+
@Test
87+
void testWithInvalidXmlCharacters() {
88+
final Log4j1XmlLayout layout = Log4j1XmlLayout.createLayout(true, true);
89+
90+
final String message = "<>'\"&A\uD800B\uDE00C\u0000\u0001\u0002\u0003\uFFFE\uFFFF";
91+
final String expectedMessage = "<>'\"&A\uFFFDB\uFFFDC\uFFFD\uFFFD\uFFFD\uFFFD\uFFFD\uFFFD";
92+
final String expectedEscapedMessage =
93+
"&lt;&gt;&#39;&quot;&amp;A\uFFFDB\uFFFDC\uFFFD\uFFFD\uFFFD\uFFFD\uFFFD\uFFFD";
94+
95+
final StringMap contextMap = ContextDataFactory.createContextData(1);
96+
contextMap.putValue(message, message);
97+
final Log4jLogEvent event = Log4jLogEvent.newBuilder()
98+
.setLoggerName(message)
99+
.setLevel(Level.forName(message, 100))
100+
.setMessage(new SimpleMessage(message))
101+
.setTimeMillis(System.currentTimeMillis() + 17)
102+
.setIncludeLocation(true)
103+
.setSource(new StackTraceElement(message, message, message, 17))
104+
.setContextData(contextMap)
105+
.build();
106+
107+
final String result = layout.toSerializable(event);
108+
109+
final String expected =
110+
"<log4j:event logger=\"" + expectedEscapedMessage + "\" timestamp=\"" + event.getTimeMillis()
111+
+ "\" level=\"" + expectedEscapedMessage + "\" thread=\"main\">\r\n"
112+
+ "<log4j:message><![CDATA[" + expectedMessage + "]]></log4j:message>\r\n"
113+
+ "<log4j:locationInfo class=\"" + expectedEscapedMessage
114+
+ "\" method=\"" + expectedEscapedMessage
115+
+ "\" file=\"" + expectedEscapedMessage + "\" line=\"17\"/>\r\n"
116+
+ "<log4j:properties>\r\n"
117+
+ "<log4j:data name=\"" + expectedEscapedMessage + "\" value=\"" + expectedEscapedMessage
118+
+ "\"/>\r\n"
119+
+ "</log4j:properties>\r\n"
120+
+ "</log4j:event>\r\n\r\n";
121+
122+
assertEquals(expected, result);
123+
}
84124
}

log4j-api-test/src/test/java/org/apache/logging/log4j/message/MapMessageTest.java

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
*/
1717
package org.apache.logging.log4j.message;
1818

19+
import static org.assertj.core.api.Assertions.assertThat;
1920
import static org.junit.jupiter.api.Assertions.assertEquals;
2021
import static org.junit.jupiter.api.Assertions.assertThrows;
2122

@@ -72,12 +73,26 @@ void testXML() {
7273

7374
@Test
7475
void testXMLEscape() {
75-
final String testMsg = "Test message <foo>";
76+
final String notBmp = new String(Character.toChars(0x10000));
77+
final String invalid = "A\uD800B\uDE00C\0\1\2\3";
78+
final String expectedInvalid = "A\uFFFDB\uFFFDC\uFFFD\uFFFD\uFFFD\uFFFD";
79+
final String key = "k<e&y> '\"\t\r\n" + notBmp + invalid;
80+
final String value = "v>al<u& '\"\t\r\n" + notBmp + invalid;
7681
final StringMapMessage msg = new StringMapMessage();
77-
msg.put("message", testMsg);
82+
msg.put(key, value);
7883
final String result = msg.getFormattedMessage(new String[] {"XML"});
79-
final String expected = "<Map>\n <Entry key=\"message\">Test message &lt;foo&gt;</Entry>\n" + "</Map>";
80-
assertEquals(expected, result);
84+
85+
assertThat(result)
86+
.isEqualTo(
87+
"<Map>\n" //
88+
+ " <Entry key=\"k&lt;e&amp;y&gt; &apos;&quot;\t\r\n"
89+
+ notBmp
90+
+ expectedInvalid
91+
+ "\">v&gt;al&lt;u&amp; &apos;&quot;\t\r\n"
92+
+ notBmp
93+
+ expectedInvalid
94+
+ "</Entry>\n" //
95+
+ "</Map>");
8196
}
8297

8398
@Test

log4j-api-test/src/test/java/org/apache/logging/log4j/util/StringBuildersTest.java

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,15 @@
1616
*/
1717
package org.apache.logging.log4j.util;
1818

19+
import static org.assertj.core.api.Assertions.assertThat;
1920
import static org.junit.jupiter.api.Assertions.assertEquals;
2021
import static org.junit.jupiter.api.Assertions.assertTrue;
2122

23+
import java.util.stream.Stream;
2224
import org.junit.jupiter.api.Test;
25+
import org.junit.jupiter.params.ParameterizedTest;
26+
import org.junit.jupiter.params.provider.Arguments;
27+
import org.junit.jupiter.params.provider.MethodSource;
2328

2429
/**
2530
* Tests the StringBuilders class.
@@ -79,15 +84,37 @@ void escapeJsonCharactersISOControl() {
7984
assertEquals(jsonValueEscaped, sb.toString());
8085
}
8186

82-
@Test
83-
void escapeXMLCharactersCorrectly() {
84-
final String xmlValueNotEscaped = "<\"Salt&Peppa'\">";
85-
final String xmlValueEscaped = "&lt;&quot;Salt&amp;Peppa&apos;&quot;&gt;";
87+
static Stream<Arguments> escapeXmlCharactersCorrectly() {
88+
final char replacement = '\uFFFD';
89+
return Stream.of(
90+
// Empty
91+
Arguments.of("", ""),
92+
// characters that need to be escaped
93+
Arguments.of("<\"Salt&Peppa'\">", "&lt;&quot;Salt&amp;Peppa&apos;&quot;&gt;"),
94+
// control character replaced with U+FFFD
95+
Arguments.of("A" + (char) 0x01 + "B", "A" + replacement + "B"),
96+
// standalone low surrogate replaced with U+FFFD
97+
Arguments.of("low" + Character.MIN_LOW_SURROGATE + "surrogate", "low" + replacement + "surrogate"),
98+
Arguments.of(Character.MIN_LOW_SURROGATE + "low", replacement + "low"),
99+
// standalone high surrogate replaced with U+FFFD
100+
Arguments.of("high" + Character.MIN_HIGH_SURROGATE + "surrogate", "high" + replacement + "surrogate"),
101+
Arguments.of(Character.MIN_HIGH_SURROGATE + "high", replacement + "high"),
102+
// FFFE and FFFF
103+
Arguments.of("invalid\uFFFEchars", "invalid" + replacement + "chars"),
104+
Arguments.of("invalid\uFFFFchars", "invalid" + replacement + "chars"),
105+
// whitespace characters are preserved
106+
Arguments.of("tab\tnewline\ncr\r", "tab\tnewline\ncr\r"),
107+
// character beyond BMP (emoji) preserved as surrogate pair
108+
Arguments.of("emoji " + "\uD83D\uDE00" + " end", "emoji " + "\uD83D\uDE00" + " end"));
109+
}
86110

111+
@ParameterizedTest
112+
@MethodSource
113+
void escapeXmlCharactersCorrectly(final String input, final String expected) {
87114
final StringBuilder sb = new StringBuilder();
88-
sb.append(xmlValueNotEscaped);
89-
assertEquals(xmlValueNotEscaped, sb.toString());
115+
sb.append(input);
116+
assertThat(sb.toString()).isEqualTo(input);
90117
StringBuilders.escapeXml(sb, 0);
91-
assertEquals(xmlValueEscaped, sb.toString());
118+
assertThat(sb.toString()).isEqualTo(expected);
92119
}
93120
}

log4j-api/src/main/java/org/apache/logging/log4j/message/MapMessage.java

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -358,10 +358,14 @@ private StringBuilder format(final MapFormat format, final StringBuilder sb) {
358358
public void asXml(final StringBuilder sb) {
359359
sb.append("<Map>\n");
360360
for (int i = 0; i < data.size(); i++) {
361-
sb.append(" <Entry key=\"").append(data.getKeyAt(i)).append("\">");
362-
final int size = sb.length();
361+
sb.append(" <Entry key=\"");
362+
int start = sb.length();
363+
sb.append(data.getKeyAt(i));
364+
StringBuilders.escapeXml(sb, start);
365+
sb.append("\">");
366+
start = sb.length();
363367
ParameterFormatter.recursiveDeepToString(data.getValueAt(i), sb);
364-
StringBuilders.escapeXml(sb, size);
368+
StringBuilders.escapeXml(sb, start);
365369
sb.append("</Entry>\n");
366370
}
367371
sb.append("</Map>");

log4j-api/src/main/java/org/apache/logging/log4j/util/StringBuilders.java

Lines changed: 51 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@
2727
@InternalApi
2828
public final class StringBuilders {
2929

30+
private static final char REPLACEMENT_CHAR = '\uFFFD';
31+
3032
private static final Class<?> timeClass;
3133
private static final Class<?> dateClass;
3234

@@ -310,8 +312,8 @@ private static int escapeAndDecrement(final StringBuilder toAppendTo, int lastPo
310312
*/
311313
public static void escapeXml(final StringBuilder toAppendTo, final int start) {
312314
int escapeCount = 0;
313-
for (int i = start; i < toAppendTo.length(); i++) {
314-
final char c = toAppendTo.charAt(i);
315+
for (int i = start; i < toAppendTo.length(); ) {
316+
final int c = toAppendTo.codePointAt(i);
315317
switch (c) {
316318
case '&':
317319
escapeCount += 4;
@@ -323,15 +325,36 @@ public static void escapeXml(final StringBuilder toAppendTo, final int start) {
323325
case '"':
324326
case '\'':
325327
escapeCount += 5;
328+
break;
329+
default:
330+
// All invalid XML 1.0 characters have the same length as the replacement character
331+
// Therefore no additional adjustment is needed
326332
}
333+
i += Character.charCount(c);
327334
}
328335

329336
final int lastChar = toAppendTo.length() - 1;
337+
if (lastChar < 0) {
338+
return;
339+
}
330340
toAppendTo.setLength(toAppendTo.length() + escapeCount);
331341
int lastPos = toAppendTo.length() - 1;
332342

333-
for (int i = lastChar; lastPos > i; i--) {
343+
for (int i = lastChar; lastPos >= start; i--) {
334344
final char c = toAppendTo.charAt(i);
345+
// Handle surrogate pairs and invalid low surrogates
346+
if (i > 0 && Character.isLowSurrogate(c)) {
347+
final char previous = toAppendTo.charAt(i - 1);
348+
// Invalid low surrogate
349+
if (!Character.isHighSurrogate(previous)) {
350+
toAppendTo.setCharAt(lastPos--, REPLACEMENT_CHAR);
351+
} else {
352+
toAppendTo.setCharAt(lastPos--, c);
353+
toAppendTo.setCharAt(lastPos--, previous);
354+
i--;
355+
}
356+
continue;
357+
}
335358
switch (c) {
336359
case '&':
337360
toAppendTo.setCharAt(lastPos--, ';');
@@ -369,8 +392,32 @@ public static void escapeXml(final StringBuilder toAppendTo, final int start) {
369392
toAppendTo.setCharAt(lastPos--, '&');
370393
break;
371394
default:
372-
toAppendTo.setCharAt(lastPos--, c);
395+
toAppendTo.setCharAt(lastPos--, isValidXml10(c) ? c : REPLACEMENT_CHAR);
373396
}
374397
}
375398
}
399+
400+
/**
401+
* Checks if a BMP {@code char} is a valid XML 1.0 character.
402+
*
403+
* <p>This method is restricted to characters in the BMP, i.e. represented by one UTF-16 code unit.</p>
404+
*
405+
* @param ch a BMP {@code char} to validate
406+
* @return {@code true} if it is a valid XML 1.0 character
407+
*/
408+
private static boolean isValidXml10(final char ch) {
409+
// XML 1.0 valid characters (Fifth Edition):
410+
// #x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD] | [#x10000-#x10FFFF]
411+
412+
// [#x20–#xD7FF] (placed early as a fast path for the most common case)
413+
return (ch >= ' ' && ch < Character.MIN_SURROGATE)
414+
// #x9
415+
|| ch == '\t'
416+
// #xA
417+
|| ch == '\n'
418+
// #xD
419+
|| ch == '\r'
420+
// [#xE000-#xFFFD]
421+
|| (ch > Character.MAX_SURROGATE && ch <= 0xFFFD);
422+
}
376423
}

log4j-core-test/pom.xml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,19 @@
6464
<!-- Additional version of LMAX Disruptor to test -->
6565
<disruptor4.version>4.0.0</disruptor4.version>
6666
<json-unit.version>2.40.1</json-unit.version>
67+
<bouncycastle.version>1.83</bouncycastle.version>
6768
</properties>
6869

70+
<dependencyManagement>
71+
<dependencies>
72+
<dependency>
73+
<groupId>org.bouncycastle</groupId>
74+
<artifactId>bcpkix-jdk18on</artifactId>
75+
<version>${bouncycastle.version}</version>
76+
</dependency>
77+
</dependencies>
78+
</dependencyManagement>
79+
6980
<dependencies>
7081

7182
<dependency>
@@ -149,6 +160,12 @@
149160
<scope>test</scope>
150161
</dependency>
151162

163+
<dependency>
164+
<groupId>org.bouncycastle</groupId>
165+
<artifactId>bcpkix-jdk18on</artifactId>
166+
<scope>test</scope>
167+
</dependency>
168+
152169
<!-- Other -->
153170
<dependency>
154171
<groupId>commons-codec</groupId>

log4j-core-test/src/main/java/org/apache/logging/log4j/core/test/package-info.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
* limitations under the license.
1616
*/
1717
@Export
18-
@Version("2.26.0")
18+
@Version("2.25.3")
1919
@BaselineIgnore("2.25.0")
2020
package org.apache.logging.log4j.core.test;
2121

log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/LineReadingTcpServer.java

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@ final class LineReadingTcpServer implements AutoCloseable {
5858

5959
private volatile boolean running;
6060

61+
private InetAddress bindAddress = InetAddress.getLoopbackAddress();
62+
6163
private ServerSocket serverSocket;
6264

6365
private Socket clientSocket;
@@ -74,6 +76,11 @@ final class LineReadingTcpServer implements AutoCloseable {
7476
this.serverSocketFactory = serverSocketFactory;
7577
}
7678

79+
// For testing purposes
80+
void setBindAddress(final InetAddress bindAddress) {
81+
this.bindAddress = bindAddress;
82+
}
83+
7784
synchronized void start(final String name, final int port) throws IOException {
7885
if (!running) {
7986
running = true;
@@ -83,8 +90,7 @@ synchronized void start(final String name, final int port) throws IOException {
8390
}
8491

8592
private ServerSocket createServerSocket(final int port) throws IOException {
86-
final ServerSocket serverSocket =
87-
serverSocketFactory.createServerSocket(port, 1, InetAddress.getLoopbackAddress());
93+
final ServerSocket serverSocket = serverSocketFactory.createServerSocket(port, 1, bindAddress);
8894
serverSocket.setReuseAddress(true);
8995
serverSocket.setSoTimeout(0); // Zero indicates `accept()` will block indefinitely
9096
await("server socket binding")
@@ -104,12 +110,12 @@ private Thread createReaderThread(final String name) {
104110
}
105111

106112
private void acceptClients() {
107-
try {
108-
while (running) {
113+
while (running) {
114+
try {
109115
acceptClient();
116+
} catch (final Exception error) {
117+
LOGGER.error("failed accepting client connections", error);
110118
}
111-
} catch (final Exception error) {
112-
LOGGER.error("failed accepting client connections", error);
113119
}
114120
}
115121

0 commit comments

Comments
 (0)