1 /* 2 * Licensed to the Apache Software Foundation (ASF) under one or more 3 * contributor license agreements. See the NOTICE file distributed with 4 * this work for additional information regarding copyright ownership. 5 * The ASF licenses this file to You under the Apache License, Version 2.0 6 * (the "License"); you may not use this file except in compliance with 7 * the License. You may obtain a copy of the License at 8 * 9 * http://www.apache.org/licenses/LICENSE-2.0 10 * 11 * Unless required by applicable law or agreed to in writing, software 12 * distributed under the License is distributed on an "AS IS" BASIS, 13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 * See the License for the specific language governing permissions and 15 * limitations under the License. 16 */ 17 18 package org.apache.commons.io.file; 19 20 import static org.junit.jupiter.api.Assertions.assertArrayEquals; 21 import static org.junit.jupiter.api.Assertions.assertEquals; 22 import static org.junit.jupiter.api.Assertions.assertFalse; 23 import static org.junit.jupiter.api.Assertions.assertNotEquals; 24 import static org.junit.jupiter.api.Assertions.assertNotNull; 25 import static org.junit.jupiter.api.Assertions.assertNull; 26 import static org.junit.jupiter.api.Assertions.assertThrows; 27 import static org.junit.jupiter.api.Assertions.assertThrowsExactly; 28 import static org.junit.jupiter.api.Assertions.assertTrue; 29 import static org.junit.jupiter.api.Assumptions.assumeFalse; 30 31 import java.io.File; 32 import java.io.IOException; 33 import java.io.OutputStream; 34 import java.net.URI; 35 import java.net.URISyntaxException; 36 import java.net.URL; 37 import java.nio.charset.StandardCharsets; 38 import java.nio.file.DirectoryStream; 39 import java.nio.file.FileAlreadyExistsException; 40 import java.nio.file.FileSystem; 41 import java.nio.file.FileSystems; 42 import java.nio.file.Files; 43 import java.nio.file.LinkOption; 44 import java.nio.file.Path; 45 import java.nio.file.Paths; 46 import java.nio.file.attribute.DosFileAttributeView; 47 import java.nio.file.attribute.FileTime; 48 import java.nio.file.attribute.PosixFileAttributes; 49 import java.util.GregorianCalendar; 50 import java.util.HashMap; 51 import java.util.Iterator; 52 import java.util.Map; 53 54 import org.apache.commons.io.FileUtils; 55 import org.apache.commons.io.FilenameUtils; 56 import org.apache.commons.io.filefilter.NameFileFilter; 57 import org.apache.commons.io.test.TestUtils; 58 import org.apache.commons.lang3.ArrayUtils; 59 import org.apache.commons.lang3.StringUtils; 60 import org.apache.commons.lang3.SystemUtils; 61 import org.junit.jupiter.api.Test; 62 63 /** 64 * Tests {@link PathUtils}. 65 */ 66 public class PathUtilsTest extends AbstractTempDirTest { 67 68 private static final String STRING_FIXTURE = "Hello World"; 69 70 private static final byte[] BYTE_ARRAY_FIXTURE = STRING_FIXTURE.getBytes(StandardCharsets.UTF_8); 71 72 private static final String TEST_JAR_NAME = "test.jar"; 73 74 private static final String TEST_JAR_PATH = "src/test/resources/org/apache/commons/io/test.jar"; 75 76 private static final String PATH_FIXTURE = "NOTICE.txt"; 77 78 /** 79 * Creates directory test fixtures. 80 * <ol> 81 * <li>tempDirPath/subdir</li> 82 * <li>tempDirPath/symlinked-dir -> tempDirPath/subdir</li> 83 * </ol> 84 * 85 * @return Path to tempDirPath/subdir 86 * @throws IOException if an I/O error occurs or the parent directory does not exist. 87 */ createTempSymlinkedRelativeDir()88 private Path createTempSymlinkedRelativeDir() throws IOException { 89 final Path targetDir = tempDirPath.resolve("subdir"); 90 final Path symlinkDir = tempDirPath.resolve("symlinked-dir"); 91 Files.createDirectory(targetDir); 92 Files.createSymbolicLink(symlinkDir, targetDir); 93 return symlinkDir; 94 } 95 current()96 private Path current() { 97 return PathUtils.current(); 98 } 99 getLastModifiedMillis(final Path file)100 private Long getLastModifiedMillis(final Path file) throws IOException { 101 return Files.getLastModifiedTime(file).toMillis(); 102 } 103 getNonExistantPath()104 private Path getNonExistantPath() { 105 return Paths.get("/does not exist/for/certain"); 106 } 107 openArchive(final Path p, final boolean createNew)108 private FileSystem openArchive(final Path p, final boolean createNew) throws IOException { 109 if (createNew) { 110 final Map<String, String> env = new HashMap<>(); 111 env.put("create", "true"); 112 final URI fileUri = p.toAbsolutePath().toUri(); 113 final URI uri = URI.create("jar:" + fileUri.toASCIIString()); 114 return FileSystems.newFileSystem(uri, env, null); 115 } 116 return FileSystems.newFileSystem(p, (ClassLoader) null); 117 } 118 setLastModifiedMillis(final Path file, final long millis)119 private void setLastModifiedMillis(final Path file, final long millis) throws IOException { 120 Files.setLastModifiedTime(file, FileTime.fromMillis(millis)); 121 } 122 123 @Test testCopyDirectoryForDifferentFilesystemsWithAbsolutePath()124 public void testCopyDirectoryForDifferentFilesystemsWithAbsolutePath() throws IOException { 125 final Path archivePath = Paths.get(TEST_JAR_PATH); 126 try (FileSystem archive = openArchive(archivePath, false)) { 127 // relative jar -> absolute dir 128 Path sourceDir = archive.getPath("dir1"); 129 PathUtils.copyDirectory(sourceDir, tempDirPath); 130 assertTrue(Files.exists(tempDirPath.resolve("f1"))); 131 132 // absolute jar -> absolute dir 133 sourceDir = archive.getPath("/next"); 134 PathUtils.copyDirectory(sourceDir, tempDirPath); 135 assertTrue(Files.exists(tempDirPath.resolve("dir"))); 136 } 137 } 138 139 @Test testCopyDirectoryForDifferentFilesystemsWithAbsolutePathReverse()140 public void testCopyDirectoryForDifferentFilesystemsWithAbsolutePathReverse() throws IOException { 141 try (FileSystem archive = openArchive(tempDirPath.resolve(TEST_JAR_NAME), true)) { 142 // absolute dir -> relative jar 143 Path targetDir = archive.getPath("target"); 144 Files.createDirectory(targetDir); 145 final Path sourceDir = Paths.get("src/test/resources/org/apache/commons/io/dirs-2-file-size-2").toAbsolutePath(); 146 PathUtils.copyDirectory(sourceDir, targetDir); 147 assertTrue(Files.exists(targetDir.resolve("dirs-a-file-size-1"))); 148 149 // absolute dir -> absolute jar 150 targetDir = archive.getPath("/"); 151 PathUtils.copyDirectory(sourceDir, targetDir); 152 assertTrue(Files.exists(targetDir.resolve("dirs-a-file-size-1"))); 153 } 154 } 155 156 @Test testCopyDirectoryForDifferentFilesystemsWithRelativePath()157 public void testCopyDirectoryForDifferentFilesystemsWithRelativePath() throws IOException { 158 final Path archivePath = Paths.get(TEST_JAR_PATH); 159 try (FileSystem archive = openArchive(archivePath, false); final FileSystem targetArchive = openArchive(tempDirPath.resolve(TEST_JAR_NAME), true)) { 160 final Path targetDir = targetArchive.getPath("targetDir"); 161 Files.createDirectory(targetDir); 162 // relative jar -> relative dir 163 Path sourceDir = archive.getPath("next"); 164 PathUtils.copyDirectory(sourceDir, targetDir); 165 assertTrue(Files.exists(targetDir.resolve("dir"))); 166 167 // absolute jar -> relative dir 168 sourceDir = archive.getPath("/dir1"); 169 PathUtils.copyDirectory(sourceDir, targetDir); 170 assertTrue(Files.exists(targetDir.resolve("f1"))); 171 } 172 } 173 174 @Test testCopyDirectoryForDifferentFilesystemsWithRelativePathReverse()175 public void testCopyDirectoryForDifferentFilesystemsWithRelativePathReverse() throws IOException { 176 try (FileSystem archive = openArchive(tempDirPath.resolve(TEST_JAR_NAME), true)) { 177 // relative dir -> relative jar 178 Path targetDir = archive.getPath("target"); 179 Files.createDirectory(targetDir); 180 final Path sourceDir = Paths.get("src/test/resources/org/apache/commons/io/dirs-2-file-size-2"); 181 PathUtils.copyDirectory(sourceDir, targetDir); 182 assertTrue(Files.exists(targetDir.resolve("dirs-a-file-size-1"))); 183 184 // relative dir -> absolute jar 185 targetDir = archive.getPath("/"); 186 PathUtils.copyDirectory(sourceDir, targetDir); 187 assertTrue(Files.exists(targetDir.resolve("dirs-a-file-size-1"))); 188 } 189 } 190 191 @Test testCopyFile()192 public void testCopyFile() throws IOException { 193 final Path sourceFile = Paths.get("src/test/resources/org/apache/commons/io/dirs-1-file-size-1/file-size-1.bin"); 194 final Path targetFile = PathUtils.copyFileToDirectory(sourceFile, tempDirPath); 195 assertTrue(Files.exists(targetFile)); 196 assertEquals(Files.size(sourceFile), Files.size(targetFile)); 197 } 198 199 @Test testCopyURL()200 public void testCopyURL() throws IOException { 201 final Path sourceFile = Paths.get("src/test/resources/org/apache/commons/io/dirs-1-file-size-1/file-size-1.bin"); 202 final URL url = new URL("file:///" + FilenameUtils.getPath(sourceFile.toAbsolutePath().toString()) + sourceFile.getFileName()); 203 final Path targetFile = PathUtils.copyFileToDirectory(url, tempDirPath); 204 assertTrue(Files.exists(targetFile)); 205 assertEquals(Files.size(sourceFile), Files.size(targetFile)); 206 } 207 208 @Test testCreateDirectoriesAlreadyExists()209 public void testCreateDirectoriesAlreadyExists() throws IOException { 210 assertEquals(tempDirPath.getParent(), PathUtils.createParentDirectories(tempDirPath)); 211 } 212 213 @SuppressWarnings("resource") // FileSystems.getDefault() is a singleton 214 @Test testCreateDirectoriesForRoots()215 public void testCreateDirectoriesForRoots() throws IOException { 216 for (final Path path : FileSystems.getDefault().getRootDirectories()) { 217 final Path parent = path.getParent(); 218 assertNull(parent); 219 assertEquals(parent, PathUtils.createParentDirectories(path)); 220 } 221 } 222 223 @Test testCreateDirectoriesForRootsLinkOptionNull()224 public void testCreateDirectoriesForRootsLinkOptionNull() throws IOException { 225 for (final File f : File.listRoots()) { 226 final Path path = f.toPath(); 227 assertEquals(path.getParent(), PathUtils.createParentDirectories(path, (LinkOption) null)); 228 } 229 } 230 231 @Test testCreateDirectoriesNew()232 public void testCreateDirectoriesNew() throws IOException { 233 assertEquals(tempDirPath, PathUtils.createParentDirectories(tempDirPath.resolve("child"))); 234 } 235 236 @Test testCreateDirectoriesSymlink()237 public void testCreateDirectoriesSymlink() throws IOException { 238 final Path symlinkedDir = createTempSymlinkedRelativeDir(); 239 final String leafDirName = "child"; 240 final Path newDirFollowed = PathUtils.createParentDirectories(symlinkedDir.resolve(leafDirName), PathUtils.NULL_LINK_OPTION); 241 assertEquals(Files.readSymbolicLink(symlinkedDir), newDirFollowed); 242 } 243 244 @Test testCreateDirectoriesSymlinkClashing()245 public void testCreateDirectoriesSymlinkClashing() throws IOException { 246 final Path symlinkedDir = createTempSymlinkedRelativeDir(); 247 assertThrowsExactly(FileAlreadyExistsException.class, () -> PathUtils.createParentDirectories(symlinkedDir.resolve("child"))); 248 } 249 250 @Test testGetLastModifiedFileTime_File_Present()251 public void testGetLastModifiedFileTime_File_Present() throws IOException { 252 assertNotNull(PathUtils.getLastModifiedFileTime(current().toFile())); 253 } 254 255 @Test testGetLastModifiedFileTime_Path_Absent()256 public void testGetLastModifiedFileTime_Path_Absent() throws IOException { 257 assertNull(PathUtils.getLastModifiedFileTime(getNonExistantPath())); 258 } 259 260 @Test testGetLastModifiedFileTime_Path_FileTime_Absent()261 public void testGetLastModifiedFileTime_Path_FileTime_Absent() throws IOException { 262 final FileTime fromMillis = FileTime.fromMillis(0); 263 assertEquals(fromMillis, PathUtils.getLastModifiedFileTime(getNonExistantPath(), fromMillis)); 264 } 265 266 @Test testGetLastModifiedFileTime_Path_Present()267 public void testGetLastModifiedFileTime_Path_Present() throws IOException { 268 assertNotNull(PathUtils.getLastModifiedFileTime(current())); 269 } 270 271 @Test testGetLastModifiedFileTime_URI_Present()272 public void testGetLastModifiedFileTime_URI_Present() throws IOException { 273 assertNotNull(PathUtils.getLastModifiedFileTime(current().toUri())); 274 } 275 276 @Test testGetLastModifiedFileTime_URL_Present()277 public void testGetLastModifiedFileTime_URL_Present() throws IOException, URISyntaxException { 278 assertNotNull(PathUtils.getLastModifiedFileTime(current().toUri().toURL())); 279 } 280 281 @Test testGetTempDirectory()282 public void testGetTempDirectory() { 283 final Path tempDirectory = Paths.get(System.getProperty("java.io.tmpdir")); 284 assertEquals(tempDirectory, PathUtils.getTempDirectory()); 285 } 286 287 @Test testIsDirectory()288 public void testIsDirectory() throws IOException { 289 assertFalse(PathUtils.isDirectory(null)); 290 291 assertTrue(PathUtils.isDirectory(tempDirPath)); 292 try (TempFile testFile1 = TempFile.create(tempDirPath, "prefix", null)) { 293 assertFalse(PathUtils.isDirectory(testFile1.get())); 294 295 Path ref = null; 296 try (TempDirectory tempDir = TempDirectory.create(getClass().getCanonicalName())) { 297 ref = tempDir.get(); 298 assertTrue(PathUtils.isDirectory(tempDir.get())); 299 } 300 assertFalse(PathUtils.isDirectory(ref)); 301 } 302 } 303 304 @Test testIsPosix()305 public void testIsPosix() throws IOException { 306 boolean isPosix; 307 try { 308 Files.getPosixFilePermissions(current()); 309 isPosix = true; 310 } catch (final UnsupportedOperationException e) { 311 isPosix = false; 312 } 313 assertEquals(isPosix, PathUtils.isPosix(current())); 314 } 315 316 @Test testIsRegularFile()317 public void testIsRegularFile() throws IOException { 318 assertFalse(PathUtils.isRegularFile(null)); 319 320 assertFalse(PathUtils.isRegularFile(tempDirPath)); 321 try (TempFile testFile1 = TempFile.create(tempDirPath, "prefix", null)) { 322 assertTrue(PathUtils.isRegularFile(testFile1.get())); 323 324 Files.delete(testFile1.get()); 325 assertFalse(PathUtils.isRegularFile(testFile1.get())); 326 } 327 } 328 329 @Test testNewDirectoryStream()330 public void testNewDirectoryStream() throws Exception { 331 final PathFilter pathFilter = new NameFileFilter(PATH_FIXTURE); 332 try (DirectoryStream<Path> stream = PathUtils.newDirectoryStream(current(), pathFilter)) { 333 final Iterator<Path> iterator = stream.iterator(); 334 final Path path = iterator.next(); 335 assertEquals(PATH_FIXTURE, path.getFileName().toString()); 336 assertFalse(iterator.hasNext()); 337 } 338 } 339 340 @Test testNewOutputStreamExistingFileAppendFalse()341 public void testNewOutputStreamExistingFileAppendFalse() throws IOException { 342 testNewOutputStreamNewFile(false); 343 testNewOutputStreamNewFile(false); 344 } 345 346 @Test testNewOutputStreamExistingFileAppendTrue()347 public void testNewOutputStreamExistingFileAppendTrue() throws IOException { 348 testNewOutputStreamNewFile(true); 349 final Path file = writeToNewOutputStream(true); 350 assertArrayEquals(ArrayUtils.addAll(BYTE_ARRAY_FIXTURE, BYTE_ARRAY_FIXTURE), Files.readAllBytes(file)); 351 } 352 testNewOutputStreamNewFile(final boolean append)353 public void testNewOutputStreamNewFile(final boolean append) throws IOException { 354 final Path file = writeToNewOutputStream(append); 355 assertArrayEquals(BYTE_ARRAY_FIXTURE, Files.readAllBytes(file)); 356 } 357 358 @Test testNewOutputStreamNewFileAppendFalse()359 public void testNewOutputStreamNewFileAppendFalse() throws IOException { 360 testNewOutputStreamNewFile(false); 361 } 362 363 @Test testNewOutputStreamNewFileAppendTrue()364 public void testNewOutputStreamNewFileAppendTrue() throws IOException { 365 testNewOutputStreamNewFile(true); 366 } 367 368 @Test testNewOutputStreamNewFileInsideExistingSymlinkedDir()369 public void testNewOutputStreamNewFileInsideExistingSymlinkedDir() throws IOException { 370 final Path symlinkDir = createTempSymlinkedRelativeDir(); 371 final Path file = symlinkDir.resolve("test.txt"); 372 try (OutputStream outputStream = PathUtils.newOutputStream(file, new LinkOption[] {})) { 373 // empty 374 } 375 try (OutputStream outputStream = PathUtils.newOutputStream(file, null)) { 376 // empty 377 } 378 try (OutputStream outputStream = PathUtils.newOutputStream(file, true)) { 379 // empty 380 } 381 try (OutputStream outputStream = PathUtils.newOutputStream(file, false)) { 382 // empty 383 } 384 } 385 386 @Test testReadAttributesPosix()387 public void testReadAttributesPosix() throws IOException { 388 boolean isPosix; 389 try { 390 Files.getPosixFilePermissions(current()); 391 isPosix = true; 392 } catch (final UnsupportedOperationException e) { 393 isPosix = false; 394 } 395 assertEquals(isPosix, PathUtils.readAttributes(current(), PosixFileAttributes.class) != null); 396 } 397 398 @Test testReadStringEmptyFile()399 public void testReadStringEmptyFile() throws IOException { 400 final Path path = Paths.get("src/test/resources/org/apache/commons/io/test-file-empty.bin"); 401 assertEquals(StringUtils.EMPTY, PathUtils.readString(path, StandardCharsets.UTF_8)); 402 assertEquals(StringUtils.EMPTY, PathUtils.readString(path, null)); 403 } 404 405 @Test testReadStringSimpleUtf8()406 public void testReadStringSimpleUtf8() throws IOException { 407 final Path path = Paths.get("src/test/resources/org/apache/commons/io/test-file-simple-utf8.bin"); 408 final String expected = "ABC\r\n"; 409 assertEquals(expected, PathUtils.readString(path, StandardCharsets.UTF_8)); 410 assertEquals(expected, PathUtils.readString(path, null)); 411 } 412 413 @Test testSetReadOnlyFile()414 public void testSetReadOnlyFile() throws IOException { 415 final Path resolved = tempDirPath.resolve("testSetReadOnlyFile.txt"); 416 // Ask now, as we are allowed before editing parent permissions. 417 final boolean isPosix = PathUtils.isPosix(tempDirPath); 418 419 // TEMP HACK 420 assumeFalse(SystemUtils.IS_OS_LINUX); 421 422 PathUtils.writeString(resolved, "test", StandardCharsets.UTF_8); 423 final boolean readable = Files.isReadable(resolved); 424 final boolean writable = Files.isWritable(resolved); 425 final boolean regularFile = Files.isRegularFile(resolved); 426 final boolean executable = Files.isExecutable(resolved); 427 final boolean hidden = Files.isHidden(resolved); 428 final boolean directory = Files.isDirectory(resolved); 429 final boolean symbolicLink = Files.isSymbolicLink(resolved); 430 // Sanity checks 431 assertTrue(readable); 432 assertTrue(writable); 433 // Test A 434 PathUtils.setReadOnly(resolved, false); 435 assertTrue(Files.isReadable(resolved), "isReadable"); 436 assertTrue(Files.isWritable(resolved), "isWritable"); 437 // Again, shouldn't blow up. 438 PathUtils.setReadOnly(resolved, false); 439 assertTrue(Files.isReadable(resolved), "isReadable"); 440 assertTrue(Files.isWritable(resolved), "isWritable"); 441 // 442 assertEquals(regularFile, Files.isReadable(resolved)); 443 assertEquals(executable, Files.isExecutable(resolved)); 444 assertEquals(hidden, Files.isHidden(resolved)); 445 assertEquals(directory, Files.isDirectory(resolved)); 446 assertEquals(symbolicLink, Files.isSymbolicLink(resolved)); 447 // Test B 448 PathUtils.setReadOnly(resolved, true); 449 if (isPosix) { 450 // On POSIX, now that the parent is not WX, the file is not readable. 451 assertFalse(Files.isReadable(resolved), "isReadable"); 452 } else { 453 assertTrue(Files.isReadable(resolved), "isReadable"); 454 } 455 assertFalse(Files.isWritable(resolved), "isWritable"); 456 final DosFileAttributeView dosFileAttributeView = PathUtils.getDosFileAttributeView(resolved); 457 if (dosFileAttributeView != null) { 458 assertTrue(dosFileAttributeView.readAttributes().isReadOnly()); 459 } 460 if (isPosix) { 461 assertFalse(Files.isReadable(resolved)); 462 } else { 463 assertEquals(regularFile, Files.isReadable(resolved)); 464 } 465 assertEquals(executable, Files.isExecutable(resolved)); 466 assertEquals(hidden, Files.isHidden(resolved)); 467 assertEquals(directory, Files.isDirectory(resolved)); 468 assertEquals(symbolicLink, Files.isSymbolicLink(resolved)); 469 // 470 PathUtils.setReadOnly(resolved, false); 471 PathUtils.deleteFile(resolved); 472 } 473 474 @Test testTouch()475 public void testTouch() throws IOException { 476 assertThrows(NullPointerException.class, () -> FileUtils.touch(null)); 477 478 final Path file = managedTempDirPath.resolve("touch.txt"); 479 Files.deleteIfExists(file); 480 assertFalse(Files.exists(file), "Bad test: test file still exists"); 481 PathUtils.touch(file); 482 assertTrue(Files.exists(file), "touch() created file"); 483 try (OutputStream out = Files.newOutputStream(file)) { 484 assertEquals(0, Files.size(file), "Created empty file."); 485 out.write(0); 486 } 487 assertEquals(1, Files.size(file), "Wrote one byte to file"); 488 final long y2k = new GregorianCalendar(2000, 0, 1).getTime().getTime(); 489 setLastModifiedMillis(file, y2k); // 0L fails on Win98 490 assertEquals(y2k, getLastModifiedMillis(file), "Bad test: set lastModified set incorrect value"); 491 final long nowMillis = System.currentTimeMillis(); 492 PathUtils.touch(file); 493 assertEquals(1, Files.size(file), "FileUtils.touch() didn't empty the file."); 494 assertNotEquals(y2k, getLastModifiedMillis(file), "FileUtils.touch() changed lastModified"); 495 final int delta = 3000; 496 assertTrue(getLastModifiedMillis(file) >= nowMillis - delta, "FileUtils.touch() changed lastModified to more than now-3s"); 497 assertTrue(getLastModifiedMillis(file) <= nowMillis + delta, "FileUtils.touch() changed lastModified to less than now+3s"); 498 } 499 500 @Test testWriteStringToFile1()501 public void testWriteStringToFile1() throws Exception { 502 final Path file = tempDirPath.resolve("write.txt"); 503 PathUtils.writeString(file, "Hello /u1234", StandardCharsets.UTF_8); 504 final byte[] text = "Hello /u1234".getBytes(StandardCharsets.UTF_8); 505 TestUtils.assertEqualContent(text, file); 506 } 507 508 /** 509 * Tests newOutputStream() here and don't use Files.write obviously. 510 */ writeToNewOutputStream(final boolean append)511 private Path writeToNewOutputStream(final boolean append) throws IOException { 512 final Path file = tempDirPath.resolve("test1.txt"); 513 try (OutputStream os = PathUtils.newOutputStream(file, append)) { 514 os.write(BYTE_ARRAY_FIXTURE); 515 } 516 return file; 517 } 518 519 } 520