/* * Copyright (c) 2020 Microsoft Corporation. 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 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 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 attr1 = Files.readAttributes(SUBST_DRIVE, "*"); Map 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.supportsLinks(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.supportsLinks(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.supportsLinks(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 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 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(); } }