| /* |
| * Copyright (c) 2010, 2018 Oracle and/or its affiliates. All rights reserved. |
| * Copyright (c) 2021 Contributors to the Eclipse Foundation |
| * |
| * This program and the accompanying materials are made available under the |
| * terms of the Eclipse Public License v. 2.0, which is available at |
| * http://www.eclipse.org/legal/epl-2.0. |
| * |
| * This Source Code may also be made available under the following Secondary |
| * Licenses when the conditions for such availability set forth in the |
| * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, |
| * version 2 with the GNU Classpath Exception, which is available at |
| * https://www.gnu.org/software/classpath/license.html. |
| * |
| * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 |
| */ |
| |
| package com.sun.enterprise.deploy.shared; |
| |
| import com.sun.enterprise.module.ModulesRegistry; |
| import com.sun.enterprise.module.single.StaticModulesRegistry; |
| |
| import java.io.File; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.io.OutputStream; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Enumeration; |
| import java.util.HashSet; |
| import java.util.List; |
| import java.util.Set; |
| import java.util.Vector; |
| import java.util.logging.Handler; |
| import java.util.logging.LogRecord; |
| import java.util.logging.Logger; |
| |
| import org.glassfish.api.deployment.archive.ReadableArchive; |
| import org.glassfish.api.deployment.archive.WritableArchive; |
| import org.glassfish.hk2.api.ServiceLocator; |
| import org.junit.jupiter.api.AfterAll; |
| import org.junit.jupiter.api.AfterEach; |
| import org.junit.jupiter.api.BeforeAll; |
| import org.junit.jupiter.api.BeforeEach; |
| import org.junit.jupiter.api.Test; |
| |
| import static org.glassfish.deployment.common.DeploymentContextImpl.deplLogger; |
| import static org.hamcrest.MatcherAssert.assertThat; |
| import static org.hamcrest.Matchers.emptyIterable; |
| import static org.hamcrest.Matchers.not; |
| import static org.junit.jupiter.api.Assertions.assertEquals; |
| import static org.junit.jupiter.api.Assertions.assertNull; |
| import static org.junit.jupiter.api.Assertions.assertTrue; |
| import static org.junit.jupiter.api.Assertions.fail; |
| |
| /** |
| * @author Tim Quinn |
| */ |
| public class FileArchiveTest { |
| |
| private static final String EXPECTED_LOG_KEY = "NCLS-DEPLOYMENT-00022"; |
| private static final String LINE_SEP = System.getProperty("line.separator"); |
| private static final String STALE_ENTRY = "oldLower/oldFile.txt"; |
| private static final String SUBARCHIVE_NAME = "subarch"; |
| |
| private File archiveDir; |
| private final Set<String> usualEntryNames = |
| new HashSet<>(Arrays.asList(new String[] {"sample.txt", "lower/other.txt"})); |
| |
| private final Set<String> usualExpectedEntryNames = initUsualExpectedEntryNames(); |
| private final Set<String> usualExpectedEntryNamesWithOverwrittenStaleEntry = |
| initUsualExpectedEntryNamesWithOverwrittenStaleEntry(); |
| |
| private final Set<String> usualSubarchiveEntryNames = |
| new HashSet<>(Arrays.asList(new String[] {"a.txt", "under/b.txt"})); |
| |
| private final Set<String> usualExpectedSubarchiveEntryNames = initUsualExpectedSubarchiveEntryNames(); |
| |
| private static ServiceLocator locator; |
| private static ModulesRegistry registry; |
| private static ArchiveFactory archiveFactory; |
| private static RecordingHandler handler; |
| |
| @BeforeAll |
| public static void setUpClass() throws Exception { |
| registry = new StaticModulesRegistry(FileArchiveTest.class.getClassLoader()); |
| locator = registry.createServiceLocator("default"); |
| archiveFactory = locator.getService(ArchiveFactory.class); |
| handler = new RecordingHandler(); |
| deplLogger.addHandler(handler); |
| } |
| |
| @BeforeEach |
| public void setUp() throws IOException { |
| archiveDir = tempDir(); |
| } |
| |
| @AfterEach |
| public void tearDown() { |
| if (archiveDir != null) { |
| clean(archiveDir); |
| } |
| archiveDir = null; |
| } |
| |
| @AfterAll |
| public static void shutdownLocator() { |
| if (locator != null) { |
| locator.shutdown(); |
| } |
| if (registry != null) { |
| registry.shutdown(); |
| } |
| } |
| |
| |
| private Set<String> initUsualExpectedEntryNames() { |
| final Set<String> expectedEntryNames = new HashSet<>(usualEntryNames); |
| expectedEntryNames.add("lower"); |
| return expectedEntryNames; |
| } |
| |
| private Set<String> initUsualExpectedEntryNamesWithOverwrittenStaleEntry() { |
| final Set<String> result = initUsualExpectedEntryNames(); |
| result.add(STALE_ENTRY); |
| result.add("oldLower"); |
| return result; |
| } |
| |
| private Set<String> initUsualExpectedSubarchiveEntryNames() { |
| final Set<String> result = new HashSet<>(usualSubarchiveEntryNames); |
| result.add("under"); |
| return result; |
| } |
| |
| private File tempDir() throws IOException { |
| final File f = File.createTempFile("FileArch", ""); |
| f.delete(); |
| f.mkdir(); |
| return f; |
| } |
| |
| private void clean(final File dir) { |
| for (File f : dir.listFiles()) { |
| if (f.isDirectory()) { |
| clean(f); |
| } |
| if ( ! f.delete()) { |
| f.deleteOnExit(); |
| } |
| } |
| if ( ! dir.delete()) { |
| dir.deleteOnExit(); |
| } |
| } |
| |
| |
| private ReadableArchive createAndPopulateArchive(final Set<String> entryNames) throws Exception { |
| WritableArchive instance = archiveFactory.createArchive(archiveDir.toURI()); |
| instance.create(archiveDir.toURI()); |
| |
| // Add some entries. |
| for (String entryName : entryNames) { |
| instance.putNextEntry(entryName); |
| instance.closeEntry(); |
| } |
| instance.close(); |
| return archiveFactory.openArchive(archiveDir); |
| } |
| |
| private ReadableArchive createAndPopulateSubarchive( |
| final WritableArchive parent, |
| final String subarchiveName, |
| final Set<String> entryNames) throws Exception { |
| final WritableArchive result = parent.createSubArchive(subarchiveName); |
| for (String entryName : entryNames) { |
| result.putNextEntry(entryName); |
| result.closeEntry(); |
| } |
| result.close(); |
| |
| final ReadableArchive readableParent = archiveFactory.openArchive(parent.getURI()); |
| return readableParent.getSubArchive(subarchiveName); |
| } |
| |
| |
| private void createAndPopulateAndCheckArchive(final Set<String> entryNames) throws Exception { |
| final ReadableArchive instance = createAndPopulateArchive(entryNames); |
| |
| checkArchive(instance, usualExpectedEntryNames); |
| |
| } |
| |
| |
| private void checkArchive(final ReadableArchive instance, final Set<String> expectedEntryNames) { |
| final Set<String> foundEntryNames = new HashSet<>(); |
| for (Enumeration<String> e = instance.entries(); e.hasMoreElements();) { |
| foundEntryNames.add(e.nextElement()); |
| } |
| assertEquals(expectedEntryNames, foundEntryNames, "Missing or unexpected entry names reported"); |
| } |
| |
| |
| private void getListOfFiles(final FileArchive instance, final Set<String> expectedEntryNames, final Logger logger) { |
| final List<String> foundEntryNames = new ArrayList<>(); |
| instance.getListOfFiles(archiveDir, foundEntryNames, null, logger); |
| assertEquals(expectedEntryNames, new HashSet<>(foundEntryNames), "Missing or unexpected entry names reported"); |
| } |
| |
| private void getListOfFilesCheckForLogRecord(FileArchive instance, final Set<String> expectedEntryNames) throws IOException { |
| handler.flush(); |
| getListOfFiles(instance, expectedEntryNames, deplLogger); |
| if (handler.logRecords().size() != 1) { |
| final StringBuilder sb = new StringBuilder(); |
| for (LogRecord record : handler.logRecords()) { |
| sb.append(record.getLevel().getLocalizedName()) |
| .append(": ") |
| .append(record.getMessage()) |
| .append(LINE_SEP); |
| } |
| fail("Expected 1 log message but received " + handler.logRecords().size() + " as follows:" + LINE_SEP + sb); |
| } |
| |
| // We have a stale file under a stale directory. Make sure a direct |
| // request for the stale file fails. (We know already from above that |
| // getting the entries list triggers a warning about the skipped stale file.) |
| final InputStream is = instance.getEntry(STALE_ENTRY); |
| assertNull(is, "Incorrectly located stale FileArchive entry " + STALE_ENTRY); |
| } |
| |
| /** |
| * Computes the expected entry names for an archive which contains a subarchive. |
| * <p> |
| * The archive's entries method will report all the entries in the main |
| * archive, plus the subarchive name, plus the entries in the subarchive. |
| * @param expectedFromArchive entries from the main archive |
| * @param subarchiveName name of the subarchive |
| * @param expectedFromSubarchive entries in the subarchive |
| * @return entry names that should be returned from the main archive's entries() method |
| */ |
| private Set<String> expectedEntryNames(Set<String> expectedFromArchive, final String subarchiveName, Set<String>expectedFromSubarchive) { |
| final Set<String> result = new HashSet<>(expectedFromArchive); |
| result.add(subarchiveName); |
| for (String expectedSubarchEntryName : expectedFromSubarchive) { |
| final StringBuilder path = new StringBuilder(); |
| path.append(subarchiveName).append("/"); |
| final String[] segments = expectedSubarchEntryName.split("/"); |
| for (int i = 0; i < segments.length; i++) { |
| path.append(segments[i]); |
| result.add(path.toString()); |
| if (i < segments.length) { |
| path.append("/"); |
| } |
| } |
| } |
| return result; |
| } |
| |
| @Test |
| public void testSubarchive() throws Exception { |
| final ArchiveAndSubarchive archives = createAndPopulateArchiveAndSubarchive(); |
| checkArchive(archives.parent, archives.fullExpectedEntryNames); |
| checkArchive(archives.subarchive, usualExpectedSubarchiveEntryNames); |
| } |
| |
| @Test |
| public void testSubArchiveCreateWithStaleEntry() throws Exception { |
| // Subarchives are a little tricky. The marker file lives only at |
| // the top level (because that's where undeployment puts it). So |
| // when a subarchive tests to see if an entry is valid it needs to |
| // consult the marker file (if any) in the top-level owning archive. |
| // |
| // This test creates a directory structure containing a stale file |
| // in a lower-level directory, creates the top-level marker file |
| // as undeployment would, then creates an archive for the top level |
| // and a subarchive for the lower-level directory (as the next |
| // deployment would). The archive and subarchive need to skip the |
| // stale file. |
| |
| // Create a file in the directory before creating the archive. |
| final File oldDir = new File(archiveDir, SUBARCHIVE_NAME); |
| final File oldFile = new File(oldDir, STALE_ENTRY); |
| oldFile.getParentFile().mkdirs(); |
| oldFile.createNewFile(); |
| |
| // Mimic what undeployment does by creating a marker file for the |
| // archive recording the pre-existing file. |
| FileArchive.StaleFileManager.Util.markDeletedArchive(archiveDir); |
| |
| // Now create the archive and subarchive on top of the directories |
| // which already exist and contain the stale file and directory. |
| final ArchiveAndSubarchive archives = createAndPopulateArchiveAndSubarchive(); |
| |
| checkArchive(archives.parent, archives.fullExpectedEntryNames); |
| checkArchive(archives.subarchive, usualExpectedSubarchiveEntryNames); |
| |
| getListOfFilesCheckForLogRecord((FileArchive) archives.parent, archives.fullExpectedEntryNames); |
| |
| } |
| |
| private static class ArchiveAndSubarchive { |
| ReadableArchive parent; |
| ReadableArchive subarchive; |
| Set<String> fullExpectedEntryNames; |
| } |
| |
| private ArchiveAndSubarchive createAndPopulateArchiveAndSubarchive() throws Exception { |
| final ArchiveAndSubarchive result = new ArchiveAndSubarchive(); |
| result.parent = createAndPopulateArchive(usualEntryNames); |
| result.subarchive = createAndPopulateSubarchive( |
| (FileArchive) result.parent, |
| SUBARCHIVE_NAME, |
| usualSubarchiveEntryNames); |
| result.fullExpectedEntryNames = expectedEntryNames( |
| usualExpectedEntryNames, SUBARCHIVE_NAME, usualSubarchiveEntryNames); |
| |
| return result; |
| } |
| |
| /** |
| * Test of open method, of class FileArchive. |
| */ |
| @Test |
| public void testNormalCreate() throws Exception { |
| createAndPopulateAndCheckArchive(usualEntryNames); |
| } |
| |
| @Test |
| public void testCreateWithOlderLeftoverEntry() throws Exception { |
| final ReadableArchive instance = createWithOlderLeftoverEntry(usualEntryNames); |
| getListOfFilesCheckForLogRecord((FileArchive) instance, usualExpectedEntryNames); |
| } |
| |
| @Test |
| public void testCreateWithOlderLeftoverEntryWhichIsCreatedAgain() throws Exception { |
| final FileArchive instance = (FileArchive) createWithOlderLeftoverEntry(usualEntryNames); |
| // Now add the stale entry explicitly which should make it valid. |
| try (OutputStream os = instance.putNextEntry(STALE_ENTRY)) { |
| os.write("No longer stale!".getBytes()); |
| } |
| checkArchive(instance, usualExpectedEntryNamesWithOverwrittenStaleEntry); |
| } |
| |
| private ReadableArchive createWithOlderLeftoverEntry(final Set<String> entryNames) throws Exception { |
| // Create a file in the directory before creating the archive. |
| final File oldFile = new File(archiveDir, STALE_ENTRY); |
| oldFile.getParentFile().mkdirs(); |
| oldFile.createNewFile(); |
| |
| // Mimic what undeployment does by creating a marker file for the |
| // archive recording the pre-existing file. |
| FileArchive.StaleFileManager.Util.markDeletedArchive(archiveDir); |
| |
| // Now create the archive. The archive should not see the old file. |
| return createAndPopulateArchive(entryNames); |
| } |
| |
| @Test |
| public void testCreateWithOlderLeftoverEntryAndThenOpen() throws Exception { |
| createWithOlderLeftoverEntry(usualEntryNames); |
| final FileArchive openedArchive = new FileArchive(); |
| openedArchive.open(archiveDir.toURI()); |
| System.err.println("A WARNING should appear next"); |
| checkArchive(openedArchive, usualExpectedEntryNames); |
| } |
| |
| @Test |
| public void testOpenWithPreexistingDir() throws Exception { |
| createPreexistingDir(); |
| final FileArchive openedArchive = new FileArchive(); |
| openedArchive.open(archiveDir.toURI()); |
| checkArchive(openedArchive, usualExpectedEntryNames); |
| } |
| |
| private void createPreexistingDir() throws IOException { |
| for (String entryName : usualEntryNames) { |
| final File f = fileForPath(archiveDir, entryName); |
| final File parentDir = f.getParentFile(); |
| if(parentDir != null) { |
| parentDir.mkdirs(); |
| } |
| try { |
| f.createNewFile(); |
| } catch (Exception ex) { |
| throw new IOException(f.getAbsolutePath(), ex); |
| } |
| } |
| } |
| |
| private File fileForPath(File anchor, final String path) { |
| final String[] interveningDirNames = path.split("/"); |
| File interveningDir = anchor; |
| for (int i = 0; i < interveningDirNames.length - 1; i++) { |
| String name = interveningDirNames[i]; |
| interveningDir = new File(interveningDir, name + "/"); |
| } |
| return new File(interveningDir,interveningDirNames[interveningDirNames.length - 1]); |
| } |
| |
| @Test |
| public void testInaccessibleDirectoryInFileArchive() throws Exception { |
| final FileArchive archive = (FileArchive) createAndPopulateArchive(usualEntryNames); |
| |
| // Now make the lower-level directory impossible to execute - therefore |
| // the attempt to list the files should fail. |
| final File lower = new File(archiveDir, "lower"); |
| lower.setExecutable(false, false); |
| assertTrue(lower.setReadable(false, false)); |
| |
| // Try to list the files. This should fail with our logger getting one record. |
| final Vector<String> fileList = new Vector<>(); |
| handler.flush(); |
| archive.getListOfFiles(lower, fileList, null /* embeddedArchives */, deplLogger); |
| |
| List<LogRecord> logRecords = handler.logRecords(); |
| assertThat("FileArchive logged no message about being unable to list files; expected " + EXPECTED_LOG_KEY, |
| logRecords, not(emptyIterable())); |
| assertEquals(EXPECTED_LOG_KEY, logRecords.get(0).getMessage(), |
| "FileArchive did not log expected message (re: being unable to list files)"); |
| // Change the protection back. |
| lower.setExecutable(true, false); |
| lower.setReadable(true, false); |
| handler.flush(); |
| |
| archive.getListOfFiles(lower, fileList, null, deplLogger); |
| assertTrue(logRecords.isEmpty(), |
| "FileArchive was incorrectly unable to list files; error key in log record:" + logRecords); |
| } |
| |
| private static class RecordingHandler extends Handler { |
| private final List<LogRecord> records = new ArrayList<>(); |
| |
| @Override |
| public void close() { |
| records.clear(); |
| } |
| |
| @Override |
| public void flush() { |
| records.clear(); |
| } |
| |
| @Override |
| public void publish(LogRecord record) { |
| records.add(record); |
| } |
| |
| List<LogRecord> logRecords() { |
| return records; |
| } |
| } |
| } |
| |