/* * The MIT License * * Copyright (c) 2004-2010, Sun Microsystems, Inc., Kohsuke Kawaguchi, Alan Harder * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package hudson; import hudson.FilePath.TarCompression; import hudson.model.TaskListener; import hudson.os.PosixAPI; import hudson.os.WindowsUtil; import hudson.remoting.VirtualChannel; import hudson.util.NullStream; import hudson.util.StreamTaskListener; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.RandomAccessFile; import java.net.ConnectException; import java.net.HttpURLConnection; import java.net.URL; import java.net.URLConnection; import java.net.URLStreamHandler; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.attribute.FileTime; import java.nio.file.attribute.PosixFilePermission; import java.util.ArrayList; import java.util.Arrays; import java.util.EnumSet; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.concurrent.Callable; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; import org.apache.commons.io.FileUtils; import org.apache.commons.io.output.NullOutputStream; import org.apache.tools.ant.Project; import org.apache.tools.ant.taskdefs.Chmod; import static org.hamcrest.CoreMatchers.is; import static org.junit.Assert.*; import static org.junit.Assert.assertThat; import static org.junit.Assume.assumeThat; import static org.junit.Assume.assumeTrue; import static org.junit.Assume.assumeFalse; import org.junit.Ignore; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TemporaryFolder; import org.jvnet.hudson.test.Issue; import org.mockito.Mockito; import static org.mockito.Mockito.*; /** * @author Kohsuke Kawaguchi */ public class FilePathTest { @Rule public ChannelRule channels = new ChannelRule(); @Rule public TemporaryFolder temp = new TemporaryFolder(); @Test public void copyTo() throws Exception { File tmp = temp.newFile(); FilePath f = new FilePath(channels.french, tmp.getPath()); f.copyTo(new NullStream()); assertTrue("target does not exist", tmp.exists()); assertTrue("could not delete target " + tmp.getPath(), tmp.delete()); } /** * An attempt to reproduce the file descriptor leak. * If this operation leaks a file descriptor, 2500 should be enough, I think. */ // TODO: this test is much too slow to be a traditional unit test. Should be extracted into some stress test // which is no part of the default test harness? @Test public void noFileLeakInCopyTo() throws Exception { for (int j=0; j<2500; j++) { File tmp = temp.newFile(); FilePath f = new FilePath(tmp); File tmp2 = temp.newFile(); FilePath f2 = new FilePath(channels.british, tmp2.getPath()); f.copyTo(f2); f.delete(); f2.delete(); } } /** * As we moved the I/O handling to another thread, there's a race condition in * {@link FilePath#copyTo(OutputStream)} — this method can return before * all the writes are delivered to {@link OutputStream}. * *

* To reproduce that problem, we use a large number of threads, so that we can * maximize the chance of out-of-order execution, and make sure we are * seeing the right byte count at the end. * * Also see JENKINS-7897 */ @Issue("JENKINS-7871") @Test public void noRaceConditionInCopyTo() throws Exception { final File tmp = temp.newFile(); int fileSize = 90000; givenSomeContentInFile(tmp, fileSize); List> results = whenFileIsCopied100TimesConcurrently(tmp); // THEN copied count was always equal the expected size for (Future f : results) assertEquals(fileSize,f.get().intValue()); } private void givenSomeContentInFile(File file, int size) throws IOException { try (OutputStream os = Files.newOutputStream(file.toPath())) { byte[] buf = new byte[size]; for (int i = 0; i < buf.length; i++) buf[i] = (byte) (i % 256); os.write(buf); } } private List> whenFileIsCopied100TimesConcurrently(final File file) throws InterruptedException { List> r = new ArrayList>(); for (int i=0; i<100; i++) { r.add(new Callable() { public Integer call() throws Exception { class Sink extends OutputStream { private Exception closed; private volatile int count; private void checkNotClosed() throws IOException { if (closed != null) throw new IOException(closed); } @Override public void write(int b) throws IOException { count++; checkNotClosed(); } @Override public void write(byte[] b) throws IOException { count+=b.length; checkNotClosed(); } @Override public void write(byte[] b, int off, int len) throws IOException { count+=len; checkNotClosed(); } @Override public void close() throws IOException { closed = new Exception(); //if (size!=count) // fail(); } } FilePath f = new FilePath(channels.french, file.getPath()); Sink sink = new Sink(); f.copyTo(sink); return sink.count; } }); } ExecutorService es = Executors.newFixedThreadPool(100); try { return es.invokeAll(r); } finally { es.shutdown(); } } @Test public void repeatCopyRecursiveTo() throws Exception { // local->local copy used to return 0 if all files were "up to date" // should return number of files processed, whether or not they were copied or already current File src = temp.newFolder("src"); File dst = temp.newFolder("dst"); File.createTempFile("foo", ".tmp", src); FilePath fp = new FilePath(src); assertEquals(1, fp.copyRecursiveTo(new FilePath(dst))); // copy again should still report 1 assertEquals(1, fp.copyRecursiveTo(new FilePath(dst))); } @Issue("JENKINS-9540") @Test public void errorMessageInRemoteCopyRecursive() throws Exception { File src = temp.newFolder("src"); File dst = temp.newFolder("dst"); FilePath from = new FilePath(src); FilePath to = new FilePath(channels.british, dst.getAbsolutePath()); for (int i = 0; i < 10000; i++) { // TODO is there a simpler way to force the TarOutputStream to be flushed and the reader to start? // Have not found a way to make the failure guaranteed. OutputStream os = from.child("content" + i).write(); try { for (int j = 0; j < 1024; j++) { os.write('.'); } } finally { os.close(); } } FilePath toF = to.child("content0"); toF.write().close(); toF.chmod(0400); try { from.copyRecursiveTo(to); // on Windows this may just succeed; OK, test did not prove anything then } catch (IOException x) { if (Functions.printThrowable(x).contains("content0")) { // Fine, error message talks about permission denied. } else { throw x; } } finally { toF.chmod(0700); } } @Issue("JENKINS-4039") @Test public void archiveBug() throws Exception { FilePath d = new FilePath(channels.french, temp.getRoot().getPath()); d.child("test").touch(0); d.zip(new NullOutputStream()); d.zip(new NullOutputStream(),"**/*"); } @Test public void normalization() throws Exception { compare("abc/def\\ghi","abc/def\\ghi"); // allow mixed separators {// basic '.' trimming compare("./abc/def","abc/def"); compare("abc/./def","abc/def"); compare("abc/def/.","abc/def"); compare(".\\abc\\def","abc\\def"); compare("abc\\.\\def","abc\\def"); compare("abc\\def\\.","abc\\def"); } compare("abc/../def","def"); compare("abc/def/../../ghi","ghi"); compare("abc/./def/../././../ghi","ghi"); // interleaving . and .. compare("../abc/def","../abc/def"); // uncollapsible .. compare("abc/def/..","abc"); compare("c:\\abc\\..","c:\\"); // we want c:\\, not c: compare("c:\\abc\\def\\..","c:\\abc"); compare("/abc/../","/"); compare("abc/..","."); compare(".","."); // @Issue("JENKINS-5951") compare("C:\\Hudson\\jobs\\foo\\workspace/../../otherjob/workspace/build.xml", "C:\\Hudson\\jobs/otherjob/workspace/build.xml"); // Other cases that failed before compare("../../abc/def","../../abc/def"); compare("..\\..\\abc\\def","..\\..\\abc\\def"); compare("/abc//../def","/def"); compare("c:\\abc\\\\..\\def","c:\\def"); compare("/../abc/def","/abc/def"); compare("c:\\..\\abc\\def","c:\\abc\\def"); compare("abc/def/","abc/def"); compare("abc\\def\\","abc\\def"); // The new code can collapse extra separator chars compare("abc//def/\\//\\ghi","abc/def/ghi"); compare("\\\\host\\\\abc\\\\\\def","\\\\host\\abc\\def"); // don't collapse for \\ prefix compare("\\\\\\foo","\\\\foo"); compare("//foo","/foo"); // Other edge cases compare("abc/def/../../../ghi","../ghi"); compare("\\abc\\def\\..\\..\\..\\ghi\\","\\ghi"); } private void compare(String original, String answer) { assertEquals(answer,new FilePath((VirtualChannel)null,original).getRemote()); } @Issue("JENKINS-6494") @Test public void getParent() throws Exception { FilePath fp = new FilePath((VirtualChannel)null, "/abc/def"); assertEquals("/abc", (fp = fp.getParent()).getRemote()); assertEquals("/", (fp = fp.getParent()).getRemote()); assertNull(fp.getParent()); fp = new FilePath((VirtualChannel)null, "abc/def\\ghi"); assertEquals("abc/def", (fp = fp.getParent()).getRemote()); assertEquals("abc", (fp = fp.getParent()).getRemote()); assertNull(fp.getParent()); fp = new FilePath((VirtualChannel)null, "C:\\abc\\def"); assertEquals("C:\\abc", (fp = fp.getParent()).getRemote()); assertEquals("C:\\", (fp = fp.getParent()).getRemote()); assertNull(fp.getParent()); } private FilePath createFilePath(final File base, final String... path) throws IOException { File building = base; for (final String component : path) { building = new File(building, component); } FileUtils.touch(building); return new FilePath(building); } /** * Performs round-trip archiving for Tar handling methods. * @throws Exception test failure */ @Test public void compressTarUntarRoundTrip() throws Exception { checkTarUntarRoundTrip("compressTarUntarRoundTrip_zero", 0); checkTarUntarRoundTrip("compressTarUntarRoundTrip_small", 100); checkTarUntarRoundTrip("compressTarUntarRoundTrip_medium", 50000); } /** * Checks that big files (greater than 8GB) can be archived and then unpacked. * This test is disabled by default due the impact on RAM. * The actual file size limit is 8589934591 bytes. * @throws Exception test failure */ @Issue("JENKINS-10629") @Ignore @Test public void archiveBigFile() throws Exception { final long largeFileSize = 9000000000L; // >8589934591 bytes final String filePrefix = "JENKINS-10629"; checkTarUntarRoundTrip(filePrefix, largeFileSize); } private void checkTarUntarRoundTrip(String filePrefix, long fileSize) throws Exception { final File tmpDir = temp.newFolder(filePrefix); final File tempFile = new File(tmpDir, filePrefix + ".log"); RandomAccessFile file = new RandomAccessFile(tempFile, "rw"); final File tarFile = new File(tmpDir, filePrefix + ".tar"); file.setLength(fileSize); assumeTrue(fileSize == file.length()); file.close(); // Compress archive final FilePath tmpDirPath = new FilePath(tmpDir); int tar = tmpDirPath.tar(Files.newOutputStream(tarFile.toPath()), tempFile.getName()); assertEquals("One file should have been compressed", 1, tar); // Decompress FilePath outDir = new FilePath(temp.newFolder(filePrefix + "_out")); final FilePath outFile = outDir.child(tempFile.getName()); tmpDirPath.child(tarFile.getName()).untar(outDir, TarCompression.NONE); assertEquals("Result file after the roundtrip differs from the initial file", new FilePath(tempFile).digest(), outFile.digest()); } @Test public void list() throws Exception { File baseDir = temp.getRoot(); final Set expected = new HashSet(); expected.add(createFilePath(baseDir, "top", "sub", "app.log")); expected.add(createFilePath(baseDir, "top", "sub", "trace.log")); expected.add(createFilePath(baseDir, "top", "db", "db.log")); expected.add(createFilePath(baseDir, "top", "db", "trace.log")); final FilePath[] result = new FilePath(baseDir).list("**"); assertEquals(expected, new HashSet(Arrays.asList(result))); } @Test public void listWithExcludes() throws Exception { File baseDir = temp.getRoot(); final Set expected = new HashSet(); expected.add(createFilePath(baseDir, "top", "sub", "app.log")); createFilePath(baseDir, "top", "sub", "trace.log"); expected.add(createFilePath(baseDir, "top", "db", "db.log")); createFilePath(baseDir, "top", "db", "trace.log"); final FilePath[] result = new FilePath(baseDir).list("**", "**/trace.log"); assertEquals(expected, new HashSet(Arrays.asList(result))); } @Test public void listWithDefaultExcludes() throws Exception { File baseDir = temp.getRoot(); final Set expected = new HashSet(); expected.add(createFilePath(baseDir, "top", "sub", "backup~")); expected.add(createFilePath(baseDir, "top", "CVS", "somefile,v")); expected.add(createFilePath(baseDir, "top", ".git", "config")); // none of the files are included by default (default includes true) assertEquals(0, new FilePath(baseDir).list("**", "").length); final FilePath[] result = new FilePath(baseDir).list("**", "", false); assertEquals(expected, new HashSet(Arrays.asList(result))); } @Issue("JENKINS-11073") @Test public void isUnix() { VirtualChannel dummy = Mockito.mock(VirtualChannel.class); FilePath winPath = new FilePath(dummy, " c:\\app\\hudson\\workspace\\3.8-jelly-db\\jdk/jdk1.6.0_21/label/sqlserver/profile/sqlserver\\acceptance-tests\\distribution.zip"); assertFalse(winPath.isUnix()); FilePath base = new FilePath(dummy, "c:\\app\\hudson\\workspace\\3.8-jelly-db"); FilePath middle = new FilePath(base, "jdk/jdk1.6.0_21/label/sqlserver/profile/sqlserver"); FilePath full = new FilePath(middle, "acceptance-tests\\distribution.zip"); assertFalse(full.isUnix()); FilePath unixPath = new FilePath(dummy, "/home/test"); assertTrue(unixPath.isUnix()); } /** * Tests that permissions are kept when using {@link FilePath#copyToWithPermission(FilePath)}. * Also tries to check that a problem with setting the last-modified date on Windows doesn't fail the whole copy * - well at least when running this test on a Windows OS. See JENKINS-11073 */ @Test public void copyToWithPermission() throws IOException, InterruptedException { File tmp = temp.getRoot(); File child = new File(tmp,"child"); FilePath childP = new FilePath(child); childP.touch(4711); Chmod chmodTask = new Chmod(); chmodTask.setProject(new Project()); chmodTask.setFile(child); chmodTask.setPerm("0400"); chmodTask.execute(); FilePath copy = new FilePath(channels.british, tmp.getPath()).child("copy"); childP.copyToWithPermission(copy); assertEquals(childP.mode(),copy.mode()); if (!Functions.isWindows()) { assertEquals(childP.lastModified(),copy.lastModified()); } // JENKINS-11073: // Windows seems to have random failures when setting the timestamp on newly generated // files. So test that: for (int i=0; i<100; i++) { copy = new FilePath(channels.british, tmp.getPath()).child("copy"+i); childP.copyToWithPermission(copy); } } @Test public void copyToWithPermissionSpecialPermissions() throws IOException, InterruptedException { assumeFalse("Test uses POSIX-specific features", Functions.isWindows()); File tmp = temp.getRoot(); File original = new File(tmp,"original"); FilePath originalP = new FilePath(channels.french, original.getPath()); originalP.touch(0); PosixAPI.jnr().chmod(original.getAbsolutePath(), 02777); // Read/write/execute for everyone and setuid. File sameChannelCopy = new File(tmp,"sameChannelCopy"); FilePath sameChannelCopyP = new FilePath(channels.french, sameChannelCopy.getPath()); originalP.copyToWithPermission(sameChannelCopyP); assertEquals("Special permissions should be copied on the same machine", 02777, PosixAPI.jnr().stat(sameChannelCopy.getAbsolutePath()).mode() & 07777); File diffChannelCopy = new File(tmp,"diffChannelCopy"); FilePath diffChannelCopyP = new FilePath(channels.british, diffChannelCopy.getPath()); originalP.copyToWithPermission(diffChannelCopyP); assertEquals("Special permissions should not be copied across machines", 00777, PosixAPI.jnr().stat(diffChannelCopy.getAbsolutePath()).mode() & 07777); } @Test public void symlinkInTar() throws Exception { assumeFalse("can't test on Windows", Functions.isWindows()); FilePath tmp = new FilePath(temp.getRoot()); FilePath in = tmp.child("in"); in.mkdirs(); in.child("c").touch(0); in.child("b").symlinkTo("c", TaskListener.NULL); FilePath tar = tmp.child("test.tar"); in.tar(tar.write(), "**/*"); FilePath dst = in.child("dst"); tar.untar(dst, TarCompression.NONE); assertEquals("c",dst.child("b").readLink()); } @Issue("JENKINS-13649") @Test public void multiSegmentRelativePaths() throws Exception { VirtualChannel d = Mockito.mock(VirtualChannel.class); FilePath winPath = new FilePath(d, "c:\\app\\jenkins\\workspace"); FilePath nixPath = new FilePath(d, "/opt/jenkins/workspace"); assertEquals("c:\\app\\jenkins\\workspace\\foo\\bar\\manchu", new FilePath(winPath, "foo/bar/manchu").getRemote()); assertEquals("c:\\app\\jenkins\\workspace\\foo\\bar\\manchu", new FilePath(winPath, "foo\\bar/manchu").getRemote()); assertEquals("c:\\app\\jenkins\\workspace\\foo\\bar\\manchu", new FilePath(winPath, "foo\\bar\\manchu").getRemote()); assertEquals("/opt/jenkins/workspace/foo/bar/manchu", new FilePath(nixPath, "foo\\bar\\manchu").getRemote()); assertEquals("/opt/jenkins/workspace/foo/bar/manchu", new FilePath(nixPath, "foo/bar\\manchu").getRemote()); assertEquals("/opt/jenkins/workspace/foo/bar/manchu", new FilePath(nixPath, "foo/bar/manchu").getRemote()); } @Test public void validateAntFileMask() throws Exception { File tmp = temp.getRoot(); FilePath d = new FilePath(channels.french, tmp.getPath()); d.child("d1/d2/d3").mkdirs(); d.child("d1/d2/d3/f.txt").touch(0); d.child("d1/d2/d3/f.html").touch(0); d.child("d1/d2/f.txt").touch(0); assertValidateAntFileMask(null, d, "**/*.txt"); assertValidateAntFileMask(null, d, "d1/d2/d3/f.txt"); assertValidateAntFileMask(null, d, "**/*.html"); assertValidateAntFileMask(Messages.FilePath_validateAntFileMask_portionMatchButPreviousNotMatchAndSuggest("**/*.js", "**", "**/*.js"), d, "**/*.js"); assertValidateAntFileMask(Messages.FilePath_validateAntFileMask_doesntMatchAnything("index.htm"), d, "index.htm"); assertValidateAntFileMask(Messages.FilePath_validateAntFileMask_doesntMatchAndSuggest("f.html", "d1/d2/d3/f.html"), d, "f.html"); // TODO lots more to test, e.g. multiple patterns separated by commas; ought to have full code coverage for this method } @SuppressWarnings("deprecation") private static void assertValidateAntFileMask(String expected, FilePath d, String fileMasks) throws Exception { assertEquals(expected, d.validateAntFileMask(fileMasks)); } @Issue("JENKINS-7214") @SuppressWarnings("deprecation") @Test public void validateAntFileMaskBounded() throws Exception { File tmp = temp.getRoot(); FilePath d = new FilePath(channels.french, tmp.getPath()); FilePath d2 = d.child("d1/d2"); d2.mkdirs(); for (int i = 0; i < 100; i++) { FilePath d3 = d2.child("d" + i); d3.mkdirs(); d3.child("f.txt").touch(0); } assertEquals(null, d.validateAntFileMask("d1/d2/**/f.txt")); assertEquals(null, d.validateAntFileMask("d1/d2/**/f.txt", 10)); assertEquals(Messages.FilePath_validateAntFileMask_portionMatchButPreviousNotMatchAndSuggest("**/*.js", "**", "**/*.js"), d.validateAntFileMask("**/*.js", 1000)); try { d.validateAntFileMask("**/*.js", 10); fail(); } catch (InterruptedException x) { // good } } @Issue("JENKINS-5253") public void testValidateCaseSensitivity() throws Exception { File tmp = Util.createTempDir(); try { FilePath d = new FilePath(channels.french, tmp.getPath()); d.child("d1/d2/d3").mkdirs(); d.child("d1/d2/d3/f.txt").touch(0); d.child("d1/d2/d3/f.html").touch(0); d.child("d1/d2/f.txt").touch(0); assertEquals(null, d.validateAntFileMask("**/d1/**/f.*", FilePath.VALIDATE_ANT_FILE_MASK_BOUND, true)); assertEquals(null, d.validateAntFileMask("**/d1/**/f.*", FilePath.VALIDATE_ANT_FILE_MASK_BOUND, false)); assertEquals(Messages.FilePath_validateAntFileMask_matchWithCaseInsensitive("**/D1/**/F.*"), d.validateAntFileMask("**/D1/**/F.*", FilePath.VALIDATE_ANT_FILE_MASK_BOUND, true)); assertEquals(null, d.validateAntFileMask("**/D1/**/F.*", FilePath.VALIDATE_ANT_FILE_MASK_BOUND, false)); } finally { Util.deleteRecursive(tmp); } } @Issue("JENKINS-15418") @Test public void deleteLongPathOnWindows() throws Exception { File tmp = temp.getRoot(); FilePath d = new FilePath(channels.french, tmp.getPath()); // construct a very long path StringBuilder sb = new StringBuilder(); while(sb.length() + tmp.getPath().length() < 260 - "very/".length()) { sb.append("very/"); } sb.append("pivot/very/very/long/path"); FilePath longPath = d.child(sb.toString()); longPath.mkdirs(); FilePath childInLongPath = longPath.child("file.txt"); childInLongPath.touch(0); File firstDirectory = new File(tmp.getAbsolutePath() + "/very"); Util.deleteRecursive(firstDirectory); assertFalse("Could not delete directory!", firstDirectory.exists()); } @Issue("JENKINS-16215") @Test public void installIfNecessaryAvoidsExcessiveDownloadsByUsingIfModifiedSince() throws Exception { File tmp = temp.getRoot(); final FilePath d = new FilePath(tmp); d.child(".timestamp").touch(123000); final HttpURLConnection con = mock(HttpURLConnection.class); final URL url = someUrlToZipFile(con); when(con.getResponseCode()) .thenReturn(HttpURLConnection.HTTP_NOT_MODIFIED); assertFalse(d.installIfNecessaryFrom(url, null, "message if failed")); verify(con).setIfModifiedSince(123000); } @Issue("JENKINS-16215") @Test public void installIfNecessaryPerformsInstallation() throws Exception { File tmp = temp.getRoot(); final FilePath d = new FilePath(tmp); final HttpURLConnection con = mock(HttpURLConnection.class); final URL url = someUrlToZipFile(con); when(con.getResponseCode()) .thenReturn(HttpURLConnection.HTTP_OK); when(con.getInputStream()) .thenReturn(someZippedContent()); assertTrue(d.installIfNecessaryFrom(url, null, "message if failed")); } @Issue("JENKINS-26196") @Test public void installIfNecessarySkipsDownloadWhenErroneous() throws Exception { File tmp = temp.getRoot(); final FilePath d = new FilePath(tmp); d.child(".timestamp").touch(123000); final HttpURLConnection con = mock(HttpURLConnection.class); final URL url = someUrlToZipFile(con); when(con.getResponseCode()).thenReturn(HttpURLConnection.HTTP_GATEWAY_TIMEOUT); when(con.getResponseMessage()).thenReturn("Gateway Timeout"); when(con.getInputStream()).thenThrow(new ConnectException()); ByteArrayOutputStream baos = new ByteArrayOutputStream(); String message = "going ahead"; assertFalse(d.installIfNecessaryFrom(url, new StreamTaskListener(baos), message)); verify(con).setIfModifiedSince(123000); String log = baos.toString(); assertFalse(log, log.contains(message)); assertTrue(log, log.contains("504 Gateway Timeout")); } @Issue("JENKINS-23507") @Test public void installIfNecessaryFollowsRedirects() throws Exception{ File tmp = temp.getRoot(); final FilePath d = new FilePath(tmp); FilePath.UrlFactory urlFactory = mock(FilePath.UrlFactory.class); d.setUrlFactory(urlFactory); final HttpURLConnection con = mock(HttpURLConnection.class); final HttpURLConnection con2 = mock(HttpURLConnection.class); final URL url = someUrlToZipFile(con); when(con.getResponseCode()).thenReturn(HttpURLConnection.HTTP_MOVED_TEMP); URL url2 = someUrlToZipFile(con2); String someUrl = url2.toExternalForm(); when(con.getHeaderField("Location")).thenReturn(someUrl); when(urlFactory.newURL(someUrl)).thenReturn(url2); when(con2.getResponseCode()).thenReturn(HttpURLConnection.HTTP_OK); when(con2.getInputStream()).thenReturn(someZippedContent()); ByteArrayOutputStream baos = new ByteArrayOutputStream(); String message = "going ahead"; assertTrue(d.installIfNecessaryFrom(url, new StreamTaskListener(baos), message)); } private URL someUrlToZipFile(final URLConnection con) throws IOException { final URLStreamHandler urlHandler = new URLStreamHandler() { @Override protected URLConnection openConnection(URL u) throws IOException { return con; } }; return new URL("http", "some-host", 0, "/some-path.zip", urlHandler); } private InputStream someZippedContent() throws IOException { final ByteArrayOutputStream buf = new ByteArrayOutputStream(); final ZipOutputStream zip = new ZipOutputStream(buf); zip.putNextEntry(new ZipEntry("abc")); zip.write("abc".getBytes()); zip.close(); return new ByteArrayInputStream(buf.toByteArray()); } @Issue("JENKINS-16846") @Test public void moveAllChildrenTo() throws IOException, InterruptedException { File tmp = temp.getRoot(); final String dirname = "sub"; final File top = new File(tmp, "test"); final File sub = new File(top, dirname); final File subsub = new File(sub, dirname); subsub.mkdirs(); final File subFile1 = new File( sub.getAbsolutePath() + "/file1.txt" ); subFile1.createNewFile(); final File subFile2 = new File( subsub.getAbsolutePath() + "/file2.txt" ); subFile2.createNewFile(); final FilePath src = new FilePath(sub); final FilePath dst = new FilePath(top); // test conflict subdir src.moveAllChildrenTo(dst); } @Issue("JENKINS-10629") @Test public void testEOFbrokenFlush() throws IOException, InterruptedException { final File srcFolder = temp.newFolder("src"); // simulate magic structure with magic sizes: // |- dir/pom.xml (2049) // |- pom.xml (2049) // \- small.tar (1537) final File smallTar = new File(srcFolder, "small.tar"); givenSomeContentInFile(smallTar, 1537); final File dir = new File(srcFolder, "dir"); dir.mkdirs(); final File pomFile = new File(dir, "pom.xml"); givenSomeContentInFile(pomFile, 2049); FileUtils.copyFileToDirectory(pomFile, srcFolder); final File archive = temp.newFile("archive.tar"); // Compress archive final FilePath tmpDirPath = new FilePath(srcFolder); int tarred = tmpDirPath.tar(Files.newOutputStream(archive.toPath()), "**"); assertEquals("One file should have been compressed", 3, tarred); // Decompress final File dstFolder = temp.newFolder("dst"); dstFolder.mkdirs(); FilePath outDir = new FilePath(dstFolder); // and now fail when flush is bad! tmpDirPath.child("../" + archive.getName()).untar(outDir, TarCompression.NONE); } @Test public void chmod() throws Exception { assumeFalse(Functions.isWindows()); File f = temp.newFile("file"); FilePath fp = new FilePath(f); int prevMode = fp.mode(); assertEquals(0400, chmodAndMode(fp, 0400)); assertEquals(0412, chmodAndMode(fp, 0412)); assertEquals(0777, chmodAndMode(fp, 0777)); assertEquals(prevMode, chmodAndMode(fp, prevMode)); } @Test public void chmodInvalidPermissions() throws Exception { assumeFalse(Functions.isWindows()); File f = temp.newFolder("folder"); FilePath fp = new FilePath(f); int invalidMode = 01770; // Full permissions for owner and group plus sticky bit. try { chmodAndMode(fp, invalidMode); fail("Setting sticky bit should fail"); } catch (IOException e) { assertEquals("Invalid mode: " + invalidMode, e.getMessage()); } } private int chmodAndMode(FilePath path, int mode) throws Exception { path.chmod(mode); return path.mode(); } @Issue("JENKINS-48227") @Test public void testCreateTempDir() throws IOException, InterruptedException { final File srcFolder = temp.newFolder("src"); final FilePath filePath = new FilePath(srcFolder); FilePath x = filePath.createTempDir("jdk", "dmg"); FilePath y = filePath.createTempDir("jdk", "pkg"); FilePath z = filePath.createTempDir("jdk", null); assertNotNull("FilePath x should not be null", x); assertNotNull("FilePath y should not be null", y); assertNotNull("FilePath z should not be null", z); assertTrue(x.getName().contains("jdk.dmg")); assertTrue(y.getName().contains("jdk.pkg")); assertTrue(z.getName().contains("jdk.tmp")); } @Test public void deleteRecursiveOnUnix() throws Exception { assumeFalse("Uses Unix-specific features", Functions.isWindows()); Path targetDir = temp.newFolder("target").toPath(); Path targetContents = Files.createFile(targetDir.resolve("contents.txt")); Path toDelete = temp.newFolder("toDelete").toPath(); Util.createSymlink(toDelete.toFile(), "../targetDir", "link", TaskListener.NULL); Files.createFile(toDelete.resolve("foo")); Files.createFile(toDelete.resolve("bar")); FilePath f = new FilePath(toDelete.toFile()); f.deleteRecursive(); assertTrue("symlink target should not be deleted", Files.exists(targetDir)); assertTrue("symlink target contents should not be deleted", Files.exists(targetContents)); assertFalse("could not delete target", Files.exists(toDelete)); } @Test public void deleteRecursiveOnWindows() throws Exception { assumeTrue("Uses Windows-specific features", Functions.isWindows()); Path targetDir = temp.newFolder("targetDir").toPath(); Path targetContents = Files.createFile(targetDir.resolve("contents.txt")); Path toDelete = temp.newFolder("toDelete").toPath(); File junction = WindowsUtil.createJunction(toDelete.resolve("junction").toFile(), targetDir.toFile()); Files.createFile(toDelete.resolve("foo")); Files.createFile(toDelete.resolve("bar")); FilePath f = new FilePath(toDelete.toFile()); f.deleteRecursive(); assertTrue("junction target should not be deleted", Files.exists(targetDir)); assertTrue("junction target contents should not be deleted", Files.exists(targetContents)); assertFalse("could not delete junction", junction.exists()); assertFalse("could not delete target", Files.exists(toDelete)); } @Issue("JENKINS-13128") @Test public void copyRecursivePreservesPosixFilePermissions() throws Exception { assumeFalse("windows doesn't support posix file permissions", Functions.isWindows()); File src = temp.newFolder("src"); File dst = temp.newFolder("dst"); Path sourceFile = Files.createFile(src.toPath().resolve("test-file")); Set allRWX = EnumSet.allOf(PosixFilePermission.class); Files.setPosixFilePermissions(sourceFile, allRWX); FilePath f = new FilePath(src); f.copyRecursiveTo(new FilePath(dst)); Path destinationFile = dst.toPath().resolve("test-file"); assertTrue("file was not copied", Files.exists(destinationFile)); Set destinationPermissions = Files.getPosixFilePermissions(destinationFile); assertEquals("file permissions not copied", allRWX, destinationPermissions); } @Issue("JENKINS-13128") @Test public void copyRecursivePreservesLastModifiedTime() throws Exception { File src = temp.newFolder("src"); File dst = temp.newFolder("dst"); Path sourceFile = Files.createFile(src.toPath().resolve("test-file")); FileTime mtime = FileTime.from(42L, TimeUnit.SECONDS); Files.setLastModifiedTime(sourceFile, mtime); FilePath f = new FilePath(src); f.copyRecursiveTo(new FilePath(dst)); Path destinationFile = dst.toPath().resolve("test-file"); assertTrue("file was not copied", Files.exists(destinationFile)); assertEquals("file mtime was not preserved", mtime, Files.getLastModifiedTime(destinationFile)); } @Test @Issue("SECURITY-904") public void isDescendant_regularFiles() throws IOException, InterruptedException { // root // /workspace // /sub // sub-regular.txt // regular.txt // /protected // secret.txt FilePath rootFolder = new FilePath(temp.newFolder("root")); FilePath workspaceFolder = rootFolder.child("workspace"); FilePath subFolder = workspaceFolder.child("sub"); FilePath protectedFolder = rootFolder.child("protected"); FilePath regularFile = workspaceFolder.child("regular.txt"); regularFile.write("regular-file", StandardCharsets.UTF_8.name()); FilePath subRegularFile = subFolder.child("sub-regular.txt"); subRegularFile.write("sub-regular-file", StandardCharsets.UTF_8.name()); FilePath secretFile = protectedFolder.child("secret.txt"); secretFile.write("secrets", StandardCharsets.UTF_8.name()); assertTrue(workspaceFolder.isDescendant(".")); assertTrue(workspaceFolder.isDescendant("regular.txt")); assertTrue(workspaceFolder.isDescendant("./regular.txt")); assertTrue(workspaceFolder.isDescendant("sub/sub-regular.txt")); assertTrue(workspaceFolder.isDescendant("sub//sub-regular.txt")); assertTrue(workspaceFolder.isDescendant("sub/../sub/sub-regular.txt")); assertTrue(workspaceFolder.isDescendant("./sub/../sub/sub-regular.txt")); // nonexistent files assertTrue(workspaceFolder.isDescendant("nonexistent.txt")); assertTrue(workspaceFolder.isDescendant("sub/nonexistent.txt")); assertTrue(workspaceFolder.isDescendant("nonexistent/nonexistent.txt")); assertFalse(workspaceFolder.isDescendant("../protected/nonexistent.txt")); assertFalse(workspaceFolder.isDescendant("../nonexistent/nonexistent.txt")); // the intermediate path "./.." goes out of workspace and so is refused assertFalse(workspaceFolder.isDescendant("./../workspace")); assertFalse(workspaceFolder.isDescendant("./../workspace/")); assertFalse(workspaceFolder.isDescendant("./../workspace/regular.txt")); assertFalse(workspaceFolder.isDescendant("../workspace/regular.txt")); assertFalse(workspaceFolder.isDescendant("./../../root/workspace/regular.txt")); // attempt to reach other folder assertFalse(workspaceFolder.isDescendant("../protected/secret.txt")); assertFalse(workspaceFolder.isDescendant("./../protected/secret.txt")); } @Test @Issue("SECURITY-904") public void isDescendant_regularSymlinks() throws IOException, InterruptedException { // root // /workspace // /a // a.txt // /b // _a => symlink to ../a // _atxt => symlink to ../a/a.txt // regular.txt // _nonexistent => symlink to nonexistent (nonexistent folder) // _nonexistentUp => symlink to ../nonexistent (nonexistent folder + illegal) // _protected => symlink to ../protected (illegal) // _secrettxt => symlink to ../protected/secret.txt (illegal) // /protected // secret.txt FilePath rootFolder = new FilePath(temp.newFolder("root")); FilePath workspaceFolder = rootFolder.child("workspace"); FilePath aFolder = workspaceFolder.child("a"); FilePath bFolder = workspaceFolder.child("b"); FilePath protectedFolder = rootFolder.child("protected"); FilePath regularFile = workspaceFolder.child("regular.txt"); regularFile.write("regular-file", StandardCharsets.UTF_8.name()); FilePath aFile = aFolder.child("a.txt"); aFile.write("a-file", StandardCharsets.UTF_8.name()); FilePath bFile = bFolder.child("a.txt"); bFile.write("b-file", StandardCharsets.UTF_8.name()); bFolder.child("_a").symlinkTo("../a", null); bFolder.child("_atxt").symlinkTo("../a/a.txt", null); // illegal symlinks workspaceFolder.child("_protected").symlinkTo("../protected", null); workspaceFolder.child("_nonexistent").symlinkTo("nonexistent", null); workspaceFolder.child("_nonexistentUp").symlinkTo("../nonexistent", null); workspaceFolder.child("_secrettxt").symlinkTo("../protected/secret.txt", null); FilePath secretFile = protectedFolder.child("secret.txt"); secretFile.write("secrets", StandardCharsets.UTF_8.name()); assertTrue(workspaceFolder.isDescendant("regular.txt")); assertTrue(workspaceFolder.isDescendant("_nonexistent")); assertTrue(workspaceFolder.isDescendant("a")); assertTrue(workspaceFolder.isDescendant("a/a.txt")); assertTrue(workspaceFolder.isDescendant("a/../a/a.txt")); assertTrue(workspaceFolder.isDescendant("b/../a/a.txt")); assertTrue(workspaceFolder.isDescendant("b")); assertTrue(workspaceFolder.isDescendant("./b")); assertTrue(workspaceFolder.isDescendant("b/_a/a.txt")); assertTrue(workspaceFolder.isDescendant("b/_a/../a/a.txt")); assertTrue(workspaceFolder.isDescendant("b/_atxt")); // nonexistent but illegal assertFalse(workspaceFolder.isDescendant("_nonexistentUp")); // illegal symlinks assertFalse(workspaceFolder.isDescendant("_protected")); assertFalse(workspaceFolder.isDescendant("_protected/")); assertFalse(workspaceFolder.isDescendant("_protected/secret.txt")); assertFalse(workspaceFolder.isDescendant("./_protected/secret.txt")); assertFalse(workspaceFolder.isDescendant("_secrettxt")); assertFalse(workspaceFolder.isDescendant("./_secrettxt")); } @Test @Issue("SECURITY-904") public void isDescendant_windowsSpecificSymlinks() throws Exception { assumeTrue(Functions.isWindows()); // root // /workspace // /a // a.txt // /b // b.txt // _a => junction to ../a // regular.txt // _nonexistent => junction to nonexistent (nonexistent folder) // _nonexistentUp => junction to ../nonexistent (nonexistent and illegal) // _protected => junction to ../protected (illegal) // /protected // secret.txt File root = temp.newFolder("root"); FilePath rootFolder = new FilePath(root); FilePath workspaceFolder = rootFolder.child("workspace"); FilePath aFolder = workspaceFolder.child("a"); FilePath bFolder = workspaceFolder.child("b"); FilePath protectedFolder = rootFolder.child("protected"); FilePath regularFile = workspaceFolder.child("regular.txt"); regularFile.write("regular-file", StandardCharsets.UTF_8.name()); FilePath aFile = aFolder.child("a.txt"); aFile.write("a-file", StandardCharsets.UTF_8.name()); FilePath bFile = bFolder.child("a.txt"); bFile.write("b-file", StandardCharsets.UTF_8.name()); createJunction(new File(root, "/workspace/b/_a"), new File(root, "/workspace/a")); createJunction(new File(root, "/workspace/_nonexistent"), new File(root, "/workspace/nonexistent")); createJunction(new File(root, "/workspace/_nonexistentUp"), new File(root, "/nonexistent")); createJunction(new File(root, "/workspace/_protected"), new File(root, "/protected")); FilePath secretFile = protectedFolder.child("secret.txt"); secretFile.write("secrets", StandardCharsets.UTF_8.name()); assertTrue(workspaceFolder.isDescendant("b")); assertTrue(workspaceFolder.isDescendant("b/_a/a.txt")); assertTrue(workspaceFolder.isDescendant("b\\_a\\a.txt")); assertTrue(workspaceFolder.isDescendant("b\\_a\\../a/a.txt")); assertTrue(workspaceFolder.isDescendant("b\\_a\\..\\a\\a.txt")); assertTrue(workspaceFolder.isDescendant(".\\b\\_a\\..\\a\\a.txt")); assertTrue(workspaceFolder.isDescendant("b/_a/../a/a.txt")); assertTrue(workspaceFolder.isDescendant("./b/_a/../a/a.txt")); // nonexistent and not proven illegal, the junction links are not resolved // by Util.resolveSymlinkToFile / neither Path.toRealPath under Windows assertTrue(workspaceFolder.isDescendant("_nonexistent")); assertTrue(workspaceFolder.isDescendant("_nonexistent/")); assertTrue(workspaceFolder.isDescendant("_nonexistent/..")); assertTrue(workspaceFolder.isDescendant("_nonexistentUp")); // illegal symlinks assertFalse(workspaceFolder.isDescendant("_protected")); assertFalse(workspaceFolder.isDescendant("_protected/../a")); } private void createJunction(File from, File to) throws Exception { Process p = Runtime.getRuntime().exec(new String[]{"cmd", "/c", "mklink", "/J", from.getAbsolutePath(), to.getAbsolutePath()}); p.waitFor(2, TimeUnit.SECONDS); } @Issue("SECURITY-904") public void isDescendant_throwIfParentDoesNotExist_symlink() throws Exception { FilePath rootFolder = new FilePath(temp.newFolder("root")); FilePath aFolder = rootFolder.child("a"); aFolder.mkdirs(); FilePath linkToNonexistent = aFolder.child("linkToNonexistent"); linkToNonexistent.symlinkTo("__nonexistent__", null); assertThat(linkToNonexistent.isDescendant("."), is(false)); } @Issue("SECURITY-904") public void isDescendant_throwIfParentDoesNotExist_directNonexistent() throws Exception { FilePath rootFolder = new FilePath(temp.newFolder("root")); FilePath nonexistent = rootFolder.child("nonexistent"); assertThat(nonexistent.isDescendant("."), is(false)); } @Test(expected = IllegalArgumentException.class) @Issue("SECURITY-904") public void isDescendant_throwIfAbsolutePathGiven() throws Exception { FilePath rootFolder = new FilePath(temp.newFolder("root")); rootFolder.mkdirs(); rootFolder.isDescendant(temp.newFile().getAbsolutePath()); } @Test @Issue("SECURITY-904") public void isDescendant_worksEvenInSymbolicWorkspace() throws Exception { // root // /w // /_workspace => symlink to ../workspace // /workspace // /a // a.txt // /b // _a => symlink to ../a // _atxt => symlink to ../a/a.txt // regular.txt // _nonexistent => symlink to nonexistent (nonexistent folder) // _nonexistentUp => symlink to ../nonexistent (nonexistent folder + illegal) // _protected => symlink to ../protected (illegal) // _secrettxt => symlink to ../protected/secret.txt (illegal) // /protected // secret.txt FilePath rootFolder = new FilePath(temp.newFolder("root")); FilePath wFolder = rootFolder.child("w"); FilePath workspaceFolder = rootFolder.child("workspace"); FilePath aFolder = workspaceFolder.child("a"); FilePath bFolder = workspaceFolder.child("b"); FilePath protectedFolder = rootFolder.child("protected"); FilePath regularFile = workspaceFolder.child("regular.txt"); regularFile.write("regular-file", StandardCharsets.UTF_8.name()); FilePath aFile = aFolder.child("a.txt"); aFile.write("a-file", StandardCharsets.UTF_8.name()); FilePath bFile = bFolder.child("a.txt"); bFile.write("b-file", StandardCharsets.UTF_8.name()); bFolder.child("_a").symlinkTo("../a", null); bFolder.child("_atxt").symlinkTo("../a/a.txt", null); // illegal symlinks workspaceFolder.child("_protected").symlinkTo("../protected", null); workspaceFolder.child("_protected2").symlinkTo("../../protected", null); workspaceFolder.child("_nonexistent").symlinkTo("nonexistent", null); workspaceFolder.child("_nonexistentUp").symlinkTo("../nonexistent", null); workspaceFolder.child("_secrettxt").symlinkTo("../protected/secret.txt", null); workspaceFolder.child("_secrettxt2").symlinkTo("../../protected/secret.txt", null); wFolder.mkdirs(); FilePath symbolicWorkspace = wFolder.child("_w"); symbolicWorkspace.symlinkTo("../workspace", null); FilePath secretFile = protectedFolder.child("secret.txt"); secretFile.write("secrets", StandardCharsets.UTF_8.name()); assertTrue(symbolicWorkspace.isDescendant("regular.txt")); assertTrue(symbolicWorkspace.isDescendant("_nonexistent")); assertTrue(symbolicWorkspace.isDescendant("a")); assertTrue(symbolicWorkspace.isDescendant("a/a.txt")); assertTrue(symbolicWorkspace.isDescendant("b")); assertTrue(symbolicWorkspace.isDescendant("b/_a/a.txt")); assertTrue(symbolicWorkspace.isDescendant("b/_atxt")); // nonexistent but illegal assertFalse(symbolicWorkspace.isDescendant("_nonexistentUp")); // illegal symlinks assertFalse(symbolicWorkspace.isDescendant("_protected")); assertFalse(symbolicWorkspace.isDescendant("_protected/")); assertFalse(symbolicWorkspace.isDescendant("_protected/secret.txt")); assertFalse(symbolicWorkspace.isDescendant("./_protected/secret.txt")); assertFalse(symbolicWorkspace.isDescendant("_protected2")); assertFalse(symbolicWorkspace.isDescendant("_protected2/secret.txt")); assertFalse(symbolicWorkspace.isDescendant("_secrettxt")); assertFalse(symbolicWorkspace.isDescendant("./_secrettxt")); assertFalse(symbolicWorkspace.isDescendant("_secrettxt2")); } }