| /* | |
| * Copyright (c) 2020 Microsoft Corporation. All rights reserved. | |
| * Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved. | |
| * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. | |
| * | |
| * This code is free software; you can redistribute it and/or modify it | |
| * under the terms of the GNU General Public License version 2 only, as | |
| * published by the Free Software Foundation. | |
| * | |
| * This code is distributed in the hope that it will be useful, but WITHOUT | |
| * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or | |
| * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License | |
| * version 2 for more details (a copy is included in the LICENSE file that | |
| * accompanied this code). | |
| * | |
| * You should have received a copy of the GNU General Public License version | |
| * 2 along with this work; if not, write to the Free Software Foundation, | |
| * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. | |
| * | |
| * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA | |
| * or visit www.oracle.com if you need additional information or have any | |
| * questions. | |
| * | |
| */ | |
| import java.util.Map; | |
| import java.util.Optional; | |
| import java.util.stream.Stream; | |
| import java.io.IOException; | |
| import java.io.UnsupportedEncodingException; | |
| import java.io.ByteArrayOutputStream; | |
| import java.io.PrintStream; | |
| import java.nio.charset.StandardCharsets; | |
| import java.nio.file.*; | |
| import static org.testng.Assert.*; | |
| import org.testng.annotations.BeforeClass; | |
| import org.testng.annotations.AfterClass; | |
| import org.testng.annotations.AfterMethod; | |
| import org.testng.annotations.Test; | |
| import org.testng.SkipException; | |
| import jdk.test.lib.process.ProcessTools; | |
| import jdk.test.lib.process.OutputAnalyzer; | |
| /* @test | |
| * @summary Test Files' public APIs with drives created using the subst command on Windows. | |
| * @requires (os.family == "windows") | |
| * @library /test/lib .. | |
| * @build SubstDrive | |
| * @run testng SubstDrive | |
| */ | |
| public class SubstDrive { | |
| private static Path SUBST_DRIVE; | |
| private static Path TEST_TEMP_DIRECTORY; | |
| /** | |
| * Setup for the test: | |
| * + Create a temporary directory where all subsequently created temp | |
| * directories will be in. This directory and all of its contents will be | |
| * deleted when the test finishes. | |
| * + Find a drive that is available for use with subst. | |
| */ | |
| @BeforeClass | |
| public void setup() throws IOException { | |
| TEST_TEMP_DIRECTORY = Files.createTempDirectory("tmp"); | |
| System.out.printf("Test directory is at %s\n", TEST_TEMP_DIRECTORY); | |
| Optional<Path> substDrive = findAvailableDrive(TEST_TEMP_DIRECTORY); | |
| if (!substDrive.isPresent()) { | |
| throw new SkipException( | |
| "Could not find any available drive to use with subst, skipping the tests"); | |
| } | |
| SUBST_DRIVE = substDrive.get(); | |
| System.out.printf("Using drive %s\n with subst", SUBST_DRIVE); | |
| } | |
| /** | |
| * Delete the root temporary directory together with all of its contents | |
| * when all tests finish. | |
| */ | |
| @AfterClass | |
| public void removeRootTempDirectory() throws IOException { | |
| TestUtil.removeAll(TEST_TEMP_DIRECTORY); | |
| } | |
| /** | |
| * Each test method maps drive `SUBST_DRIVE` to a temporary directory, | |
| * unmap the drive after every test so that subsequent ones can reuse | |
| * the drive. | |
| */ | |
| @AfterMethod | |
| public void deleteSubstDrive() throws IOException { | |
| Stream<String> substitutedDrives = substFindMappedDrives(); | |
| // Only delete `SUBST_DRIVE` if it is currently being substituted | |
| if (substitutedDrives.anyMatch(e -> e.contains(SUBST_DRIVE.toString()))) { | |
| substDelete(SUBST_DRIVE); | |
| } | |
| } | |
| /** | |
| * Test whether files can be created in the substituted drive. | |
| */ | |
| @Test | |
| public void testCreateAndDeleteFile() throws IOException { | |
| Path tempDirectory = Files.createTempDirectory(TEST_TEMP_DIRECTORY, "tmp"); | |
| substCreate(SUBST_DRIVE, tempDirectory); | |
| String fileContents = "Hello world!"; | |
| Path p = Path.of(SUBST_DRIVE.toString(), "testFile.txt"); | |
| Files.createFile(p); | |
| assertTrue(Files.exists(p)); | |
| Files.writeString(p, fileContents); | |
| assertEquals(Files.readString(p), fileContents); | |
| } | |
| /** | |
| * Test if we can delete the substituted drive (essentially just a directory). | |
| */ | |
| @Test | |
| public void testDeleteSubstitutedDrive() throws IOException { | |
| Path tempDirectory = Files.createTempDirectory(TEST_TEMP_DIRECTORY, "tmp"); | |
| substCreate(SUBST_DRIVE, tempDirectory); | |
| assertTrue(Files.exists(tempDirectory)); | |
| Files.delete(SUBST_DRIVE); | |
| assertTrue(Files.notExists(tempDirectory)); | |
| } | |
| /** | |
| * Test if the attributes returned by the Files' APIs are consistent when | |
| * using the actual path and the substituted path. | |
| */ | |
| @Test | |
| public void testAttributes() throws IOException { | |
| Path tempDirectory = Files.createTempDirectory(TEST_TEMP_DIRECTORY, "tmp"); | |
| substCreate(SUBST_DRIVE, tempDirectory); | |
| assertTrue(Files.isSameFile(tempDirectory, SUBST_DRIVE)); | |
| assertEquals( | |
| Files.isExecutable(tempDirectory), | |
| Files.isExecutable(SUBST_DRIVE)); | |
| assertEquals( | |
| Files.isReadable(tempDirectory), | |
| Files.isReadable(SUBST_DRIVE)); | |
| assertEquals( | |
| Files.isDirectory(tempDirectory), | |
| Files.isDirectory(SUBST_DRIVE)); | |
| assertEquals( | |
| Files.isHidden(tempDirectory), | |
| Files.isHidden(SUBST_DRIVE)); | |
| assertEquals( | |
| Files.isRegularFile(tempDirectory), | |
| Files.isRegularFile(SUBST_DRIVE)); | |
| assertEquals( | |
| Files.isSymbolicLink(tempDirectory), | |
| Files.isSymbolicLink(SUBST_DRIVE)); | |
| assertEquals( | |
| Files.getOwner(tempDirectory), | |
| Files.getOwner(SUBST_DRIVE)); | |
| assertEquals( | |
| Files.isWritable(tempDirectory), | |
| Files.isWritable(SUBST_DRIVE)); | |
| } | |
| /** | |
| * Test if setting attributes for a substituted path works the same way | |
| * as it would for a real path. | |
| */ | |
| @Test | |
| public void testGetSetAttributes() throws IOException { | |
| Path tempDirectory = Files.createTempDirectory(TEST_TEMP_DIRECTORY, "tmp"); | |
| substCreate(SUBST_DRIVE, tempDirectory); | |
| Files.setAttribute(SUBST_DRIVE, "dos:hidden", true); | |
| assertTrue(Files.isHidden(SUBST_DRIVE)); | |
| assertTrue(Files.isHidden(tempDirectory)); | |
| Files.setAttribute(tempDirectory, "dos:hidden", false); | |
| assertFalse(Files.isHidden(SUBST_DRIVE)); | |
| assertFalse(Files.isHidden(tempDirectory)); | |
| Map<String, Object> attr1 = Files.readAttributes(SUBST_DRIVE, "*"); | |
| Map<String, Object> attr2 = Files.readAttributes(tempDirectory, "*"); | |
| assertEquals(attr1, attr2); | |
| } | |
| /** | |
| * Test if the FileStores returned from using substituted path and real path | |
| * are the same. | |
| */ | |
| @Test | |
| public void testFileStore() throws IOException { | |
| Path tempDirectory = Files.createTempDirectory(TEST_TEMP_DIRECTORY, "tmp"); | |
| substCreate(SUBST_DRIVE, tempDirectory); | |
| FileStore fileStore1 = Files.getFileStore(tempDirectory); | |
| FileStore fileStore2 = Files.getFileStore(SUBST_DRIVE); | |
| assertEquals( | |
| fileStore1.getTotalSpace(), | |
| fileStore2.getTotalSpace()); | |
| assertEquals( | |
| fileStore1.getBlockSize(), | |
| fileStore2.getBlockSize()); | |
| assertEquals( | |
| fileStore1.name(), | |
| fileStore2.name()); | |
| assertEquals( | |
| fileStore1.type(), | |
| fileStore2.type()); | |
| assertEquals( | |
| SUBST_DRIVE.getFileSystem().getRootDirectories(), | |
| tempDirectory.getFileSystem().getRootDirectories()); | |
| } | |
| /** | |
| * Test if Files.copy works correctly on a substituted drive, and that | |
| * all of the attributes are the same. | |
| */ | |
| @Test | |
| public void testMoveAndCopySubstDrive() throws IOException { | |
| Path tempDirectory = Files.createTempDirectory(TEST_TEMP_DIRECTORY, "tmp"); | |
| Path tempDirectoryCopy = Path.of(tempDirectory.toString() + "_copy"); | |
| substCreate(SUBST_DRIVE, tempDirectory); | |
| Files.copy(SUBST_DRIVE, tempDirectoryCopy); | |
| assertEquals( | |
| Files.isExecutable(SUBST_DRIVE), | |
| Files.isExecutable(tempDirectoryCopy)); | |
| assertEquals( | |
| Files.isReadable(SUBST_DRIVE), | |
| Files.isReadable(tempDirectoryCopy)); | |
| assertEquals( | |
| Files.isDirectory(SUBST_DRIVE), | |
| Files.isDirectory(tempDirectoryCopy)); | |
| assertEquals( | |
| Files.isHidden(SUBST_DRIVE), | |
| Files.isHidden(tempDirectoryCopy)); | |
| assertEquals( | |
| Files.isRegularFile(SUBST_DRIVE), | |
| Files.isRegularFile(tempDirectoryCopy)); | |
| assertEquals( | |
| Files.isWritable(SUBST_DRIVE), | |
| Files.isWritable(tempDirectoryCopy)); | |
| assertEquals( | |
| Files.getOwner(SUBST_DRIVE), | |
| Files.getOwner(tempDirectoryCopy)); | |
| } | |
| /** | |
| * Test if the attributes of a resolved symlink are the same as its target's | |
| * Note: requires administrator privileges. | |
| */ | |
| @Test | |
| public void testGetResolvedSymlinkAttribute() throws IOException { | |
| if (!TestUtil.supportsSymbolicLinks(TEST_TEMP_DIRECTORY)) { | |
| return; | |
| } | |
| Path tempDirectory = Files.createTempDirectory(TEST_TEMP_DIRECTORY, "tmp"); | |
| substCreate(SUBST_DRIVE, tempDirectory); | |
| Path tempFile = Path.of(SUBST_DRIVE.toString(), "test.txt"); | |
| String contents = "Hello world!"; | |
| Files.writeString(tempFile, contents); | |
| assertEquals(Files.readString(tempFile), contents); | |
| Path link = Path.of(SUBST_DRIVE.toString(), "link"); | |
| Files.createSymbolicLink(link, tempFile); | |
| assertEquals(Files.readString(link), contents); | |
| assertEquals(Files.isExecutable(link), Files.isExecutable(tempFile)); | |
| assertEquals(Files.isReadable(link), Files.isReadable(tempFile)); | |
| assertEquals(Files.isDirectory(link), Files.isDirectory(tempFile)); | |
| assertEquals(Files.isHidden(link), Files.isHidden(tempFile)); | |
| assertEquals(Files.isRegularFile(link), Files.isRegularFile(tempFile)); | |
| assertEquals(Files.isWritable(link), Files.isWritable(tempFile)); | |
| assertEquals(Files.getOwner(link), Files.getOwner(tempFile)); | |
| } | |
| /** | |
| * Test if files and directories can be created, moved, and cut when the | |
| * substituted drive is a symlink. | |
| * Note: requires administrator privileges. | |
| */ | |
| @Test | |
| public void testSubstWithSymlinkedDirectory() throws IOException { | |
| if (!TestUtil.supportsSymbolicLinks(TEST_TEMP_DIRECTORY)) { | |
| return; | |
| } | |
| Path tempDirectory = Files.createTempDirectory(TEST_TEMP_DIRECTORY, "tmp"); | |
| Path tempLink = Path.of(tempDirectory.toString() + "_link"); | |
| Files.createSymbolicLink(tempLink, tempDirectory); | |
| substCreate(SUBST_DRIVE, tempLink); | |
| assertEquals( | |
| Files.readAttributes(SUBST_DRIVE, "*"), | |
| Files.readAttributes(tempDirectory, "*")); | |
| assertTrue(Files.isWritable(SUBST_DRIVE)); | |
| Path tempFile = Files.createTempFile(SUBST_DRIVE, "prefix", "suffix"); | |
| String contents = "Hello world!"; | |
| Files.writeString(tempFile, contents); | |
| assertEquals(Files.readString(tempFile), contents); | |
| Path tempDirectory2 = Files.createTempDirectory(TEST_TEMP_DIRECTORY, "tmp"); | |
| Path copy = Path.of(tempDirectory2.toString(), "copied"); | |
| Files.copy(tempFile, copy); | |
| assertTrue(Files.exists(copy)); | |
| assertEquals(Files.readString(copy), contents); | |
| Path cut = Path.of(tempDirectory2.toString(), "cut"); | |
| Files.move(tempFile, cut); | |
| assertTrue(Files.notExists(tempFile)); | |
| assertTrue(Files.exists(cut)); | |
| assertEquals(Files.readString(cut), contents); | |
| } | |
| /** | |
| * When the substituted drive is a symlink, test if it has the same | |
| * attributes as its target. | |
| * Note: requires administrator privileges. | |
| */ | |
| @Test | |
| public void testMoveAndCopyFilesToSymlinkedDrive() throws IOException { | |
| if (!TestUtil.supportsSymbolicLinks(TEST_TEMP_DIRECTORY)) { | |
| return; | |
| } | |
| Path tempDirectory = Files.createTempDirectory(TEST_TEMP_DIRECTORY, "tmp"); | |
| Path tempLink = Path.of(tempDirectory.toString() + "_link"); | |
| Files.createSymbolicLink(tempLink, tempDirectory); | |
| substCreate(SUBST_DRIVE, tempLink); | |
| assertEquals( | |
| Files.readAttributes(SUBST_DRIVE, "*"), | |
| Files.readAttributes(tempDirectory, "*")); | |
| assertTrue(Files.isWritable(SUBST_DRIVE)); | |
| } | |
| /** | |
| * Run a command and optionally prints stdout contents to | |
| * `customOutputStream`. | |
| */ | |
| private void runCmd(ProcessBuilder pb, PrintStream customOutputStream) { | |
| try { | |
| PrintStream ps = customOutputStream != null ? | |
| customOutputStream : | |
| System.out; | |
| OutputAnalyzer outputAnalyzer = ProcessTools.executeCommand(pb) | |
| .outputTo(ps) | |
| .errorTo(System.err); | |
| int exitCode = outputAnalyzer.getExitValue(); | |
| assertEquals( | |
| exitCode /* actual value */, | |
| 0 /* expected value */, | |
| String.format( | |
| "Command `%s` failed with exit code %d", | |
| pb.command(), | |
| exitCode | |
| ) | |
| ); | |
| } catch (Throwable t) { | |
| throw new RuntimeException(t); | |
| } | |
| } | |
| /** | |
| * Helper to map a path to a drive letter using subst. | |
| * For reference, see: | |
| * https://docs.microsoft.com/en-us/windows-server/administration/windows-commands/subst | |
| */ | |
| private void substCreate(Path drive, Path path) { | |
| runCmd( | |
| new ProcessBuilder( | |
| "cmd", "/c", "subst", drive.toString(), path.toString()), | |
| null /* customOutputStream */); | |
| } | |
| /** | |
| * Delete a drive mapping using subst. | |
| * For reference, see: | |
| * https://docs.microsoft.com/en-us/windows-server/administration/windows-commands/subst | |
| */ | |
| private void substDelete(Path drive) throws IOException { | |
| runCmd( | |
| new ProcessBuilder( | |
| "cmd", "/c", "subst", drive.toString(), "/D"), | |
| null /* customOutputStream */); | |
| } | |
| /** | |
| * Return a list of strings that represents all the currently mapped drives. | |
| * For instance, with the following output of subst: | |
| * A:\: => path1 | |
| * B:\: => path2 | |
| * T:\: => path3 | |
| * X:\: => path4 | |
| * The function returns: ["A:\", "B:\", "T:\", "X:\"] | |
| * For reference, see: | |
| * https://docs.microsoft.com/en-us/windows-server/administration/windows-commands/subst | |
| */ | |
| private Stream<String> substFindMappedDrives() throws UnsupportedEncodingException { | |
| ByteArrayOutputStream baos = new ByteArrayOutputStream(); | |
| String utf8 = StandardCharsets.UTF_8.name(); | |
| try (PrintStream ps = new PrintStream(baos, true, utf8)) { | |
| // subst without any arguments returns a list of drives that | |
| // are being substituted | |
| runCmd(new ProcessBuilder("cmd", "/c", "subst"), ps); | |
| String stdout = baos.toString(utf8); | |
| return stdout | |
| // split lines | |
| .lines() | |
| // only examine lines with "=>" | |
| .filter(line -> line.contains("=>")) | |
| // split each line into 2 components and take the first one | |
| .map(line -> line.split("=>")[0].trim()); | |
| } | |
| } | |
| /** | |
| * subst can fail if the drive to be mapped already exists. The method returns | |
| * a drive that is available. | |
| */ | |
| private Optional<Path> findAvailableDrive(Path tempDirectory) { | |
| for (char letter = 'Z'; letter >= 'A'; letter--) { | |
| try { | |
| Path p = Path.of(letter + ":"); | |
| substCreate(p, tempDirectory); | |
| substDelete(p); | |
| return Optional.of(p); | |
| } catch (Throwable t) { | |
| // fall through | |
| } | |
| } | |
| return Optional.empty(); | |
| } | |
| } |