Skip to content

Commit ea02493

Browse files
Allow whitelist to be loaded from fat jar files
When a `META-INF/deserializable.types` file is located in a jar file nested inside another jar file (for example, a Spring Boot fat jar), it can be found as a resource but can't be read with `Files.readAllLines`. This commit changes the file reading to use an input stream that allows the file to be read in this scenario. Signed-off-by: Scott Frederick <scottyfred@gmail.com>
1 parent 3ce6b99 commit ea02493

2 files changed

Lines changed: 138 additions & 5 deletions

File tree

‎spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/config/JsonMessageConverter.java‎

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,12 @@
1616

1717
package org.springframework.cloud.function.context.config;
1818

19+
import java.io.BufferedReader;
20+
import java.io.InputStreamReader;
1921
import java.lang.reflect.ParameterizedType;
2022
import java.lang.reflect.Type;
21-
import java.net.URI;
2223
import java.net.URL;
2324
import java.nio.charset.StandardCharsets;
24-
import java.nio.file.Files;
25-
import java.nio.file.Path;
2625
import java.util.ArrayList;
2726
import java.util.Enumeration;
2827
import java.util.List;
@@ -68,8 +67,12 @@ public JsonMessageConverter(JsonMapper jsonMapper, MimeType... supportedMimeType
6867
try {
6968
Enumeration<URL> resources = ClassUtils.getDefaultClassLoader().getResources("META-INF/deserializable.types");
7069
while (resources.hasMoreElements()) {
71-
URI uri = resources.nextElement().toURI();
72-
List<String> lines = Files.readAllLines(Path.of(uri));
70+
URL url = resources.nextElement();
71+
List<String> lines;
72+
try (BufferedReader reader = new BufferedReader(
73+
new InputStreamReader(url.openStream(), StandardCharsets.UTF_8))) {
74+
lines = reader.lines().toList();
75+
}
7376
for (String line : lines) {
7477
// need to split in case if delimited
7578
String[] keys = line.split(",");
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
/*
2+
* Copyright 2026-present the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.cloud.function.context.config;
18+
19+
import java.io.IOException;
20+
import java.net.URL;
21+
import java.net.URLClassLoader;
22+
import java.nio.charset.StandardCharsets;
23+
import java.nio.file.Files;
24+
import java.nio.file.Path;
25+
import java.util.Enumeration;
26+
import java.util.jar.JarEntry;
27+
import java.util.jar.JarOutputStream;
28+
29+
import org.junit.jupiter.api.Test;
30+
import org.junit.jupiter.api.io.TempDir;
31+
import tools.jackson.databind.ObjectMapper;
32+
33+
import org.springframework.cloud.function.json.JacksonMapper;
34+
import org.springframework.messaging.Message;
35+
import org.springframework.messaging.MessageHeaders;
36+
import org.springframework.messaging.support.MessageBuilder;
37+
import org.springframework.util.MimeTypeUtils;
38+
39+
import static org.assertj.core.api.Assertions.assertThat;
40+
41+
public class JsonMessageConverterAllowlistPackagingTests {
42+
43+
private static final String ALLOWLIST = "META-INF/deserializable.types";
44+
45+
@Test
46+
public void readsAllowlistFromDirectoryOnClasspath(@TempDir Path temp) throws Exception {
47+
Path directory = temp.resolve("classes");
48+
Path metaInf = Files.createDirectories(directory.resolve("META-INF"));
49+
Files.writeString(metaInf.resolve("deserializable.types"), Person.class.getName());
50+
51+
assertThat(convertWithAllowlistAt(directory.toUri().toURL())).isInstanceOf(Person.class);
52+
}
53+
54+
@Test
55+
public void readsAllowlistNestedInsideJarOnClasspath(@TempDir Path temp) throws Exception {
56+
URL jar = jarContainingAllowlist(temp.resolve("allowlist.jar"), Person.class.getName());
57+
58+
assertThat(allowlistLoader(jar).getResource(ALLOWLIST))
59+
.hasToString("jar:" + jar + "!/META-INF/deserializable.types");
60+
61+
assertThat(convertWithAllowlistAt(jar))
62+
.as("an allowlist nested inside an archive must be read, as it is when an application "
63+
+ "is packaged as a Spring Boot fat jar")
64+
.isInstanceOf(Person.class);
65+
}
66+
67+
@Test
68+
public void ignoresTypeParameterWhenNestedAllowlistDoesNotNameIt(@TempDir Path temp) throws Exception {
69+
URL jar = jarContainingAllowlist(temp.resolve("allowlist.jar"), "com.example.SomethingElse");
70+
71+
assertThat(convertWithAllowlistAt(jar)).isNotInstanceOf(Person.class);
72+
}
73+
74+
private Object convertWithAllowlistAt(URL allowlist) throws IOException {
75+
ClassLoader original = Thread.currentThread().getContextClassLoader();
76+
try (URLClassLoader loader = allowlistLoader(allowlist)) {
77+
Thread.currentThread().setContextClassLoader(loader);
78+
JsonMessageConverter converter = new JsonMessageConverter(new JacksonMapper(new ObjectMapper()));
79+
Message<String> message = MessageBuilder.withPayload("{\"name\":\"bill\"}")
80+
.setHeader(MessageHeaders.CONTENT_TYPE,
81+
MimeTypeUtils.APPLICATION_JSON + ";type=" + Person.class.getName())
82+
.build();
83+
return converter.convertFromInternal(message, Object.class, null);
84+
}
85+
finally {
86+
Thread.currentThread().setContextClassLoader(original);
87+
}
88+
}
89+
90+
private URLClassLoader allowlistLoader(URL allowlist) {
91+
ClassLoader parent = getClass().getClassLoader();
92+
return new URLClassLoader(new URL[] { allowlist }, parent) {
93+
94+
@Override
95+
public URL getResource(String name) {
96+
return ALLOWLIST.equals(name) ? findResource(name) : super.getResource(name);
97+
}
98+
99+
@Override
100+
public Enumeration<URL> getResources(String name) throws IOException {
101+
return ALLOWLIST.equals(name) ? findResources(name) : super.getResources(name);
102+
}
103+
104+
};
105+
}
106+
107+
private URL jarContainingAllowlist(Path jar, String allowlistedType) throws IOException {
108+
try (JarOutputStream out = new JarOutputStream(Files.newOutputStream(jar))) {
109+
out.putNextEntry(new JarEntry(ALLOWLIST));
110+
out.write(allowlistedType.getBytes(StandardCharsets.UTF_8));
111+
out.closeEntry();
112+
}
113+
return jar.toUri().toURL();
114+
}
115+
116+
public static class Person {
117+
118+
private String name;
119+
120+
public String getName() {
121+
return this.name;
122+
}
123+
124+
public void setName(String name) {
125+
this.name = name;
126+
}
127+
128+
}
129+
130+
}

0 commit comments

Comments
 (0)