1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18 package org.apache.commons.io;
19
20 import static java.nio.charset.StandardCharsets.ISO_8859_1;
21 import static java.nio.charset.StandardCharsets.US_ASCII;
22 import static java.nio.charset.StandardCharsets.UTF_8;
23 import static org.apache.commons.io.FileSystem.NameLengthStrategy.BYTES;
24 import static org.apache.commons.io.FileSystem.NameLengthStrategy.UTF16_CODE_UNITS;
25 import static org.apache.commons.lang3.StringUtils.repeat;
26 import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
27 import static org.junit.jupiter.api.Assertions.assertEquals;
28 import static org.junit.jupiter.api.Assertions.assertFalse;
29 import static org.junit.jupiter.api.Assertions.assertInstanceOf;
30 import static org.junit.jupiter.api.Assertions.assertNotNull;
31 import static org.junit.jupiter.api.Assertions.assertNotSame;
32 import static org.junit.jupiter.api.Assertions.assertThrows;
33 import static org.junit.jupiter.api.Assertions.assertTrue;
34 import static org.junit.jupiter.api.Assumptions.assumeTrue;
35
36 import java.io.BufferedReader;
37 import java.io.FileNotFoundException;
38 import java.io.IOException;
39 import java.io.StringReader;
40 import java.nio.charset.Charset;
41 import java.nio.charset.StandardCharsets;
42 import java.nio.file.Files;
43 import java.nio.file.Path;
44 import java.util.Objects;
45 import java.util.stream.Stream;
46
47 import javax.xml.parsers.DocumentBuilder;
48 import javax.xml.parsers.DocumentBuilderFactory;
49 import javax.xml.parsers.ParserConfigurationException;
50
51 import org.apache.commons.io.FileSystem.NameLengthStrategy;
52 import org.apache.commons.lang3.JavaVersion;
53 import org.apache.commons.lang3.StringUtils;
54 import org.apache.commons.lang3.SystemProperties;
55 import org.apache.commons.lang3.SystemUtils;
56 import org.junit.jupiter.api.Test;
57 import org.junit.jupiter.api.io.TempDir;
58 import org.junit.jupiter.params.ParameterizedTest;
59 import org.junit.jupiter.params.provider.Arguments;
60 import org.junit.jupiter.params.provider.EnumSource;
61 import org.junit.jupiter.params.provider.MethodSource;
62 import org.w3c.dom.Document;
63 import org.xml.sax.InputSource;
64 import org.xml.sax.SAXException;
65
66
67
68
69 class FileSystemTest {
70
71
72 private static final String CHAR_UTF8_1B = "a";
73
74
75 private static final String CHAR_UTF8_2B = "Γ©";
76
77
78 private static final String CHAR_UTF8_3B = "β
";
79
80
81 private static final String CHAR_UTF8_4B = "π";
82
83
84
85
86
87
88
89
90
91
92
93
94 private static final String CHAR_UTF8_69B =
95
96 "\uD83D\uDC69\uD83C\uDFFB\u200D\uD83E\uDDB0"
97
98 + "\u200D"
99
100 + "\uD83D\uDC68\uD83C\uDFFF\u200D\uD83E\uDDB2"
101
102 + "\u200D"
103
104 + "\uD83D\uDC67\uD83C\uDFFD\u200D\uD83E\uDDB1"
105
106 + "\u200D"
107
108 + "\uD83D\uDC66\uD83C\uDFFC\u200D\uD83E\uDDB3";
109
110
111 private static final String FILE_NAME_255_BYTES_UTF8_1B = repeat(CHAR_UTF8_1B, 255);
112
113
114 private static final String FILE_NAME_255_BYTES_UTF8_2B = repeat(CHAR_UTF8_2B, 127) + CHAR_UTF8_1B;
115
116
117 private static final String FILE_NAME_255_BYTES_UTF8_3B = repeat(CHAR_UTF8_3B, 85);
118
119
120 private static final String FILE_NAME_255_BYTES_UTF8_4B = repeat(CHAR_UTF8_4B, 63) + CHAR_UTF8_3B;
121
122
123 private static final String FILE_NAME_255_CHARS_UTF8_1B = FILE_NAME_255_BYTES_UTF8_1B;
124
125
126 private static final String FILE_NAME_255_CHARS_UTF8_2B = repeat(CHAR_UTF8_2B, 255);
127
128
129 private static final String FILE_NAME_255_CHARS_UTF8_3B = repeat(CHAR_UTF8_3B, 255);
130
131
132 private static final String FILE_NAME_255_CHARS_UTF8_4B = repeat(CHAR_UTF8_4B, 127) + CHAR_UTF8_3B;
133
134 private static void createAndDelete(final Path tempDir, final String fileName) throws IOException {
135 final Path filePath = tempDir.resolve(fileName);
136 Files.createFile(filePath);
137 try (Stream<Path> files = Files.list(tempDir)) {
138 final boolean found = files.anyMatch(filePath::equals);
139 if (!found) {
140 throw new FileNotFoundException(fileName + " not found in " + tempDir);
141 }
142 }
143 Files.delete(filePath);
144 }
145
146 static Stream<Arguments> testIsLegalName_Length() {
147 return Stream.of(
148 Arguments.of(FileSystem.GENERIC, repeat(FILE_NAME_255_BYTES_UTF8_1B, 4), UTF_8),
149 Arguments.of(FileSystem.GENERIC, repeat(FILE_NAME_255_BYTES_UTF8_2B, 4), UTF_8),
150 Arguments.of(FileSystem.GENERIC, repeat(FILE_NAME_255_BYTES_UTF8_3B, 4), UTF_8),
151 Arguments.of(FileSystem.GENERIC, repeat(FILE_NAME_255_BYTES_UTF8_4B, 4), UTF_8),
152 Arguments.of(FileSystem.LINUX, FILE_NAME_255_BYTES_UTF8_1B, UTF_8),
153 Arguments.of(FileSystem.LINUX, FILE_NAME_255_BYTES_UTF8_2B, UTF_8),
154 Arguments.of(FileSystem.LINUX, FILE_NAME_255_BYTES_UTF8_3B, UTF_8),
155 Arguments.of(FileSystem.LINUX, FILE_NAME_255_BYTES_UTF8_4B, UTF_8),
156 Arguments.of(FileSystem.MAC_OSX, FILE_NAME_255_BYTES_UTF8_1B, UTF_8),
157 Arguments.of(FileSystem.MAC_OSX, FILE_NAME_255_BYTES_UTF8_2B, UTF_8),
158 Arguments.of(FileSystem.MAC_OSX, FILE_NAME_255_BYTES_UTF8_3B, UTF_8),
159 Arguments.of(FileSystem.MAC_OSX, FILE_NAME_255_BYTES_UTF8_4B, UTF_8),
160 Arguments.of(FileSystem.WINDOWS, FILE_NAME_255_CHARS_UTF8_1B, UTF_8),
161 Arguments.of(FileSystem.WINDOWS, FILE_NAME_255_CHARS_UTF8_2B, UTF_8),
162 Arguments.of(FileSystem.WINDOWS, FILE_NAME_255_CHARS_UTF8_3B, UTF_8),
163 Arguments.of(FileSystem.WINDOWS, FILE_NAME_255_CHARS_UTF8_4B, UTF_8),
164
165 Arguments.of(FileSystem.GENERIC, repeat(FILE_NAME_255_BYTES_UTF8_1B, 4), US_ASCII),
166 Arguments.of(FileSystem.GENERIC, repeat(CHAR_UTF8_2B, 1020), ISO_8859_1),
167 Arguments.of(FileSystem.LINUX, FILE_NAME_255_BYTES_UTF8_1B, US_ASCII),
168 Arguments.of(FileSystem.LINUX, repeat(CHAR_UTF8_2B, 255), ISO_8859_1));
169 }
170
171 static Stream<Arguments> testNameLengthStrategyTruncate_Succeeds() {
172
173 final String woman;
174 final String redHeadWoman;
175 if (SystemUtils.isJavaVersionAtMost(JavaVersion.JAVA_19)) {
176 woman = CHAR_UTF8_69B.substring(0, 2);
177 redHeadWoman = CHAR_UTF8_69B.substring(0, 7);
178 } else {
179 woman = "";
180 redHeadWoman = "";
181 }
182 return Stream.of(
183
184
185
186
187 Arguments.of(BYTES, 0, "", ""),
188
189 Arguments.of(BYTES, 10, "simple.txt", "simple.txt"),
190
191 Arguments.of(BYTES, 10, "." + repeat(CHAR_UTF8_1B, 10), "." + repeat(CHAR_UTF8_1B, 9)),
192 Arguments.of(BYTES, 20, "." + repeat(CHAR_UTF8_2B, 10), "." + repeat(CHAR_UTF8_2B, 9)),
193 Arguments.of(BYTES, 30, "." + repeat(CHAR_UTF8_3B, 10), "." + repeat(CHAR_UTF8_3B, 9)),
194 Arguments.of(BYTES, 40, "." + repeat(CHAR_UTF8_4B, 10), "." + repeat(CHAR_UTF8_4B, 9)),
195
196 Arguments.of(BYTES, 13, repeat(CHAR_UTF8_1B, 10) + ".txt", repeat(CHAR_UTF8_1B, 9) + ".txt"),
197 Arguments.of(BYTES, 23, repeat(CHAR_UTF8_2B, 10) + ".txt", repeat(CHAR_UTF8_2B, 9) + ".txt"),
198 Arguments.of(BYTES, 33, repeat(CHAR_UTF8_3B, 10) + ".txt", repeat(CHAR_UTF8_3B, 9) + ".txt"),
199 Arguments.of(BYTES, 43, repeat(CHAR_UTF8_4B, 10) + ".txt", repeat(CHAR_UTF8_4B, 9) + ".txt"),
200
201 Arguments.of(BYTES, 1, CHAR_UTF8_1B, CHAR_UTF8_1B),
202 Arguments.of(BYTES, 2, CHAR_UTF8_2B, CHAR_UTF8_2B),
203 Arguments.of(BYTES, 3, CHAR_UTF8_3B, CHAR_UTF8_3B),
204 Arguments.of(BYTES, 4, CHAR_UTF8_4B, CHAR_UTF8_4B),
205 Arguments.of(BYTES, 9, repeat(CHAR_UTF8_1B, 10), repeat(CHAR_UTF8_1B, 9)),
206 Arguments.of(BYTES, 19, repeat(CHAR_UTF8_2B, 10), repeat(CHAR_UTF8_2B, 9)),
207 Arguments.of(BYTES, 29, repeat(CHAR_UTF8_3B, 10), repeat(CHAR_UTF8_3B, 9)),
208 Arguments.of(BYTES, 39, repeat(CHAR_UTF8_4B, 10), repeat(CHAR_UTF8_4B, 9)),
209
210 Arguments.of(BYTES, 69, CHAR_UTF8_69B, CHAR_UTF8_69B),
211
212 Arguments.of(BYTES, 69 + 4, repeat(CHAR_UTF8_69B, 2), CHAR_UTF8_69B + woman),
213 Arguments.of(BYTES, 69 + 15, repeat(CHAR_UTF8_69B, 2), CHAR_UTF8_69B + redHeadWoman),
214
215
216
217 Arguments.of(UTF16_CODE_UNITS, 0, "", ""),
218
219 Arguments.of(UTF16_CODE_UNITS, 10, "simple.txt", "simple.txt"),
220
221 Arguments.of(UTF16_CODE_UNITS, 10, "." + repeat(CHAR_UTF8_1B, 10), "." + repeat(CHAR_UTF8_1B, 9)),
222 Arguments.of(UTF16_CODE_UNITS, 10, "." + repeat(CHAR_UTF8_2B, 10), "." + repeat(CHAR_UTF8_2B, 9)),
223 Arguments.of(UTF16_CODE_UNITS, 10, "." + repeat(CHAR_UTF8_3B, 10), "." + repeat(CHAR_UTF8_3B, 9)),
224 Arguments.of(UTF16_CODE_UNITS, 20, "." + repeat(CHAR_UTF8_4B, 10), "." + repeat(CHAR_UTF8_4B, 9)),
225
226 Arguments.of(UTF16_CODE_UNITS, 13, repeat(CHAR_UTF8_1B, 10) + ".txt", repeat(CHAR_UTF8_1B, 9) + ".txt"),
227 Arguments.of(UTF16_CODE_UNITS, 13, repeat(CHAR_UTF8_2B, 10) + ".txt", repeat(CHAR_UTF8_2B, 9) + ".txt"),
228 Arguments.of(UTF16_CODE_UNITS, 13, repeat(CHAR_UTF8_3B, 10) + ".txt", repeat(CHAR_UTF8_3B, 9) + ".txt"),
229 Arguments.of(UTF16_CODE_UNITS, 23, repeat(CHAR_UTF8_4B, 10) + ".txt", repeat(CHAR_UTF8_4B, 9) + ".txt"),
230
231 Arguments.of(UTF16_CODE_UNITS, 1, CHAR_UTF8_1B, CHAR_UTF8_1B),
232 Arguments.of(UTF16_CODE_UNITS, 1, CHAR_UTF8_2B, CHAR_UTF8_2B),
233 Arguments.of(UTF16_CODE_UNITS, 1, CHAR_UTF8_3B, CHAR_UTF8_3B),
234 Arguments.of(UTF16_CODE_UNITS, 2, CHAR_UTF8_4B, CHAR_UTF8_4B),
235 Arguments.of(UTF16_CODE_UNITS, 9, repeat(CHAR_UTF8_1B, 10), repeat(CHAR_UTF8_1B, 9)),
236 Arguments.of(UTF16_CODE_UNITS, 9, repeat(CHAR_UTF8_2B, 10), repeat(CHAR_UTF8_2B, 9)),
237 Arguments.of(UTF16_CODE_UNITS, 9, repeat(CHAR_UTF8_3B, 10), repeat(CHAR_UTF8_3B, 9)),
238 Arguments.of(UTF16_CODE_UNITS, 19, repeat(CHAR_UTF8_4B, 10), repeat(CHAR_UTF8_4B, 9)),
239
240 Arguments.of(UTF16_CODE_UNITS, 31, CHAR_UTF8_69B, CHAR_UTF8_69B),
241
242 Arguments.of(UTF16_CODE_UNITS, 31 + 2, repeat(CHAR_UTF8_69B, 2), CHAR_UTF8_69B + woman),
243 Arguments.of(UTF16_CODE_UNITS, 31 + 7, repeat(CHAR_UTF8_69B, 2), CHAR_UTF8_69B + redHeadWoman));
244 }
245
246 static Stream<Arguments> testNameLengthStrategyTruncate_Throws() {
247 final Stream<Arguments> common = Stream.of(
248
249 Arguments.of(BYTES, 10, "cafΓ©", US_ASCII, "US-ASCII"),
250 Arguments.of(UTF16_CODE_UNITS, 10, "\uD800.txt", UTF_8, "UTF-16"),
251 Arguments.of(UTF16_CODE_UNITS, 10, "\uDC00.txt", UTF_8, "UTF-16"),
252
253 Arguments.of(BYTES, 4, "a.txt", UTF_8, "extension"),
254 Arguments.of(UTF16_CODE_UNITS, 4, "a.txt", UTF_8, "extension"),
255
256 Arguments.of(BYTES, 3, CHAR_UTF8_4B, UTF_8, "truncated to 1 character"),
257 Arguments.of(UTF16_CODE_UNITS, 1, CHAR_UTF8_4B, UTF_8, "truncated to 1 character"));
258 return SystemUtils.isJavaVersionAtMost(JavaVersion.JAVA_19)
259 ? common
260 : Stream.concat(
261 common,
262
263
264 Stream.of(
265 Arguments.of(BYTES, 68, CHAR_UTF8_69B, UTF_8, "truncated to 29 characters"),
266 Arguments.of(UTF16_CODE_UNITS, 30, CHAR_UTF8_69B, UTF_8, "truncated to 30 characters")));
267 }
268
269 private String parseXmlRootValue(final Path xmlPath, final Charset charset) throws SAXException, IOException, ParserConfigurationException {
270 final DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
271 try (BufferedReader reader = Files.newBufferedReader(xmlPath, charset)) {
272 final Document document = builder.parse(new InputSource(reader));
273 return document.getDocumentElement().getTextContent();
274 }
275 }
276
277 private String parseXmlRootValue(final String xmlString) throws SAXException, IOException, ParserConfigurationException {
278 final DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
279 final Document document = builder.parse(new InputSource(new StringReader(xmlString)));
280 return document.getDocumentElement().getTextContent();
281 }
282
283 @Test
284 void testGetBlockSize() {
285 assertTrue(FileSystem.getCurrent().getBlockSize() >= 0);
286 }
287
288 @Test
289 void testGetCurrent() {
290 if (SystemUtils.IS_OS_WINDOWS) {
291 assertEquals(FileSystem.WINDOWS, FileSystem.getCurrent());
292 }
293 if (SystemUtils.IS_OS_LINUX) {
294 assertEquals(FileSystem.LINUX, FileSystem.getCurrent());
295 }
296 if (SystemUtils.IS_OS_MAC_OSX) {
297 assertEquals(FileSystem.MAC_OSX, FileSystem.getCurrent());
298 }
299 }
300
301 @Test
302 void testGetIllegalFileNameChars() {
303 final FileSystem current = FileSystem.getCurrent();
304 assertNotSame(current.getIllegalFileNameChars(), current.getIllegalFileNameChars());
305 }
306
307 @Test
308 void testGetNameSeparator() {
309 final FileSystem current = FileSystem.getCurrent();
310 assertEquals(SystemProperties.getFileSeparator(), Character.toString(current.getNameSeparator()));
311 }
312
313 @ParameterizedTest
314 @EnumSource(FileSystem.class)
315 void testIsLegalName(final FileSystem fs) {
316 assertFalse(fs.isLegalFileName(""), fs.name());
317 assertFalse(fs.isLegalFileName(null), fs.name());
318 assertFalse(fs.isLegalFileName("\0"), fs.name());
319 assertTrue(fs.isLegalFileName("0"), fs.name());
320 for (final String candidate : fs.getReservedFileNames()) {
321
322 assertFalse(fs.isLegalFileName(candidate), candidate);
323 }
324 }
325
326 @Test
327 void testIsLegalName_Encoding() {
328 assertFalse(FileSystem.GENERIC.isLegalFileName(FILE_NAME_255_BYTES_UTF8_3B, US_ASCII), "US-ASCII cannot represent all chars");
329 assertTrue(FileSystem.GENERIC.isLegalFileName(FILE_NAME_255_BYTES_UTF8_3B, UTF_8), "UTF-8 can represent all chars");
330 }
331
332 @ParameterizedTest(name = "{index}: {0} with charset {2}")
333 @MethodSource
334 void testIsLegalName_Length(final FileSystem fs, final String nameAtLimit, final Charset charset) {
335 assertTrue(fs.isLegalFileName(nameAtLimit, charset), fs.name() + " length at limit");
336 final String nameOverLimit = nameAtLimit + "a";
337 assertFalse(fs.isLegalFileName(nameOverLimit, charset), fs.name() + " length over limit");
338 }
339
340 @Test
341 void testIsReservedFileName() {
342 for (final FileSystem fs : FileSystem.values()) {
343 for (final String candidate : fs.getReservedFileNames()) {
344 assertTrue(fs.isReservedFileName(candidate));
345 }
346 }
347 }
348
349 @Test
350 void testIsReservedFileNameOnWindows() {
351 final FileSystem fs = FileSystem.WINDOWS;
352 for (final String candidate : fs.getReservedFileNames()) {
353
354 assertTrue(fs.isReservedFileName(candidate));
355 assertTrue(fs.isReservedFileName(candidate + ".txt"), candidate);
356 }
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374 }
375
376 @Test
377 void testMaxNameLength_MatchesRealSystem(@TempDir final Path tempDir) {
378 final FileSystem fs = FileSystem.getCurrent();
379 final String[] validNames;
380 switch (fs) {
381 case MAC_OSX:
382 case LINUX:
383
384
385 validNames = new String[] {
386 FILE_NAME_255_BYTES_UTF8_1B,
387 FILE_NAME_255_BYTES_UTF8_2B,
388 FILE_NAME_255_BYTES_UTF8_3B,
389 FILE_NAME_255_BYTES_UTF8_4B
390 };
391
392 break;
393 case WINDOWS:
394
395
396 validNames = new String[] {
397 FILE_NAME_255_CHARS_UTF8_1B,
398 FILE_NAME_255_CHARS_UTF8_2B,
399 FILE_NAME_255_CHARS_UTF8_3B,
400 FILE_NAME_255_CHARS_UTF8_4B
401 };
402
403 break;
404 default:
405 throw new IllegalStateException("Unexpected value: " + fs);
406 }
407 int failures = 0;
408 for (final String fileName : validNames) {
409
410 assertDoesNotThrow(() -> createAndDelete(tempDir, fileName), "OS should accept max-length name: " + fileName);
411
412 assertTrue(fs.isLegalFileName(fileName, UTF_8), "Commons IO should accept max-length name: " + fileName);
413
414 final String tooLongName = fileName + "a";
415
416 assertFalse(fs.isLegalFileName(tooLongName, UTF_8), "Commons IO should reject too-long name: " + tooLongName);
417
418 try {
419 createAndDelete(tempDir, tooLongName);
420 } catch (final Throwable e) {
421 failures++;
422 assertInstanceOf(IOException.class, e, "OS rejects too-long name");
423 }
424 }
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439 if (SystemUtils.IS_OS_MAC_OSX) {
440 assertTrue(failures >= 1, "Expected at least one too-long name rejected, got " + failures);
441 } else {
442 assertEquals(4, failures, "All too-long names were rejected");
443 }
444 }
445
446 @ParameterizedTest(name = "{index}: {0} truncates {1} to {2}")
447 @MethodSource
448 void testNameLengthStrategyTruncate_Succeeds(final NameLengthStrategy strategy, final int limit, final String input, final String expected) {
449 final CharSequence out = strategy.truncate(input, limit, UTF_8);
450 assertEquals(expected, out.toString(), strategy.name() + " truncates to limit");
451 }
452
453 @ParameterizedTest(name = "{index}: {0} truncates {2} with limit {1} throws")
454 @MethodSource
455 void testNameLengthStrategyTruncate_Throws(final NameLengthStrategy strategy, final int limit, final String input, final Charset charset,
456 final String message) {
457 final IllegalArgumentException ex = assertThrows(IllegalArgumentException.class, () -> strategy.truncate(input, limit, charset));
458 final String exMessage = ex.getMessage();
459 assertTrue(exMessage.contains(message), "ex message contains " + message + ": " + exMessage);
460 }
461
462 @Test
463 void testReplacementWithNUL() {
464 for (final FileSystem fs : FileSystem.values()) {
465 try {
466 fs.toLegalFileName("Test", '\0');
467 } catch (final IllegalArgumentException iae) {
468 assertTrue(iae.getMessage().startsWith("The replacement character '\\0'"), iae.getMessage());
469 }
470 }
471 }
472
473 @Test
474 void testSorted() {
475 for (final FileSystem fs : FileSystem.values()) {
476 final char[] chars = fs.getIllegalFileNameChars();
477 for (int i = 0; i < chars.length - 1; i++) {
478 assertTrue(chars[i] < chars[i + 1], fs.name());
479 }
480 }
481 }
482
483 @Test
484 void testSupportsDriveLetter() {
485 assertTrue(FileSystem.WINDOWS.supportsDriveLetter());
486 assertFalse(FileSystem.GENERIC.supportsDriveLetter());
487 assertFalse(FileSystem.LINUX.supportsDriveLetter());
488 assertFalse(FileSystem.MAC_OSX.supportsDriveLetter());
489 }
490
491 @Test
492 void testToLegalFileNameWindows() {
493 final FileSystem fs = FileSystem.WINDOWS;
494 final char replacement = '-';
495 for (char i = 0; i < 32; i++) {
496 assertEquals(replacement, fs.toLegalFileName(String.valueOf(i), replacement).charAt(0));
497 }
498 final char[] illegal = { '<', '>', ':', '"', '/', '\\', '|', '?', '*' };
499 for (char i = 0; i < illegal.length; i++) {
500 assertEquals(replacement, fs.toLegalFileName(String.valueOf(i), replacement).charAt(0));
501 }
502 for (char i = 'a'; i < 'z'; i++) {
503 assertEquals(i, fs.toLegalFileName(String.valueOf(i), replacement).charAt(0));
504 }
505 for (char i = 'A'; i < 'Z'; i++) {
506 assertEquals(i, fs.toLegalFileName(String.valueOf(i), replacement).charAt(0));
507 }
508 for (char i = '0'; i < '9'; i++) {
509 assertEquals(i, fs.toLegalFileName(String.valueOf(i), replacement).charAt(0));
510 }
511
512 assertThrows(NullPointerException.class, () -> fs.toLegalFileName(null, '_'));
513 assertThrows(IllegalArgumentException.class, () -> fs.toLegalFileName("", '_'));
514
515 assertThrows(IllegalArgumentException.class, () -> fs.toLegalFileName("test", '\0'));
516 assertThrows(IllegalArgumentException.class, () -> fs.toLegalFileName("test", ':'));
517 }
518
519 @ParameterizedTest
520 @EnumSource(FileSystem.class)
521 void testXmlRoundtrip(final FileSystem fs, @TempDir final Path tempDir) throws Exception {
522 if (SystemUtils.IS_OS_WINDOWS) {
523
524
525 assumeTrue(SystemUtils.isJavaVersionAtLeast(JavaVersion.JAVA_21));
526 }
527 final Charset charset = StandardCharsets.UTF_8;
528 assertEquals("a", fs.toLegalFileName("a", '_', charset));
529 assertEquals("abcdefghijklmno", fs.toLegalFileName("abcdefghijklmno", '_', charset));
530 assertEquals("\u4F60\u597D\u55CE", fs.toLegalFileName("\u4F60\u597D\u55CE", '_', charset));
531 assertEquals("\u2713\u2714", fs.toLegalFileName("\u2713\u2714", '_', charset));
532 assertEquals("\uD83D\uDE80\u2728\uD83C\uDF89", fs.toLegalFileName("\uD83D\uDE80\u2728\uD83C\uDF89", '_', charset));
533 assertEquals("\uD83D\uDE03", fs.toLegalFileName("\uD83D\uDE03", '_', charset));
534 assertEquals("\uD83D\uDE03\uD83D\uDE03\uD83D\uDE03\uD83D\uDE03\uD83D\uDE03",
535 fs.toLegalFileName("\uD83D\uDE03\uD83D\uDE03\uD83D\uDE03\uD83D\uDE03\uD83D\uDE03", '_', charset));
536 for (int i = 1; i <= 10; i++) {
537 final String name1 = fs.toLegalFileName(StringUtils.repeat("π¦", i), '_', charset);
538 assertNotNull(name1);
539 final byte[] name1Bytes = name1.getBytes();
540 final String xmlString1 = toXmlString(name1, charset);
541 final Path path = tempDir.resolve(name1);
542 Files.write(path, xmlString1.getBytes(charset));
543 final String xmlFromPath = parseXmlRootValue(path, charset);
544 assertEquals(name1, xmlFromPath, "i = " + i);
545 final String name2 = new String(name1Bytes, charset);
546 assertEquals(name1, name2);
547 final String xmlString2 = toXmlString(name2, charset);
548 assertEquals(xmlString1, xmlString2);
549 final String parsedValue = Objects.toString(parseXmlRootValue(xmlString2), "");
550 assertEquals(name1, parsedValue, "i = " + i);
551 assertEquals(name2, parsedValue, "i = " + i);
552 }
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574 }
575
576 private String toXmlString(final String s, final Charset charset) {
577 return String.format("<?xml version=\"1.0\" encoding=\"%s\"?><data>%s</data>", charset.name(), s);
578 }
579
580 }