All Downloads are FREE. Search and download functionalities are using the official Maven repository.

net.java.truevfs.kernel.impl.ArchiveFileSystem Maven / Gradle / Ivy

/*
 * Copyright © 2005 - 2021 Schlichtherle IT Services.
 * All rights reserved. Use is subject to license terms.
 */
package net.java.truevfs.kernel.impl;

import lombok.Value;
import lombok.val;
import net.java.truecommons.cio.Container;
import net.java.truecommons.cio.Entry;
import net.java.truecommons.shed.BitField;
import net.java.truecommons.shed.PathNormalizer;
import net.java.truecommons.shed.PathSplitter;
import net.java.truevfs.kernel.spec.FsAccessOption;
import net.java.truevfs.kernel.spec.FsArchiveEntry;
import net.java.truevfs.kernel.spec.FsCovariantNode;
import net.java.truevfs.kernel.spec.FsNodeName;

import javax.annotation.Nonnull;
import javax.annotation.concurrent.NotThreadSafe;
import java.io.CharConversionException;
import java.io.IOException;
import java.net.URI;
import java.nio.file.*;
import java.util.*;
import java.util.function.Supplier;

import static java.util.Objects.requireNonNull;
import static net.java.truecommons.cio.Entry.*;
import static net.java.truecommons.cio.Entry.Access.WRITE;
import static net.java.truecommons.cio.Entry.Type.DIRECTORY;
import static net.java.truecommons.cio.Entry.Type.FILE;
import static net.java.truecommons.shed.HashMaps.OVERHEAD_SIZE;
import static net.java.truecommons.shed.HashMaps.initialCapacity;
import static net.java.truecommons.shed.Paths.*;
import static net.java.truevfs.kernel.spec.FsAccessOption.CREATE_PARENTS;
import static net.java.truevfs.kernel.spec.FsAccessOption.EXCLUSIVE;
import static net.java.truevfs.kernel.spec.FsAccessOptions.NONE;
import static net.java.truevfs.kernel.spec.FsNodeName.*;

/**
 * A read/write virtual file system for archive entries.
 *
 * @param  the type of the archive entries.
 * @author Christian Schlichtherle
 */
@NotThreadSafe
class ArchiveFileSystem
        extends AbstractCollection>
        implements ArchiveModelAspect {

    private static final String RootPath = ROOT.getPath();

    private final ArchiveModel model;
    private final EntryTable master;

    private final Splitter splitter = new Splitter();

    /**
     * Returns a new empty archive file system and ensures its integrity.
     * Only the root directory is created with its last modification time set to the system's current time.
     * The file system is set to be modifiable.
     *
     * @param model the archive model to use.
     */
    static  ArchiveFileSystem apply(ArchiveModel model) {
        return new ArchiveFileSystem<>(model);
    }

    private ArchiveFileSystem(final ArchiveModel model) {
        this(model, new EntryTable<>(OVERHEAD_SIZE));
        val root = newEntry(RootPath, DIRECTORY, Optional.empty());
        val time = System.currentTimeMillis();
        ALL_ACCESS.forEach(access -> root.setTime(access, time));
        master.add(RootPath, root);
    }

    /**
     * Returns a new archive file system which populates its entries from the given {@code archive} and ensures its
     * integrity.
     * 

* First, the entries from the archive are loaded into the file system. *

* Second, a root directory with the given last modification time is created and linked into the filesystem (so it's * never loaded from the archive). *

* Finally, the file system integrity is checked and fixed: * Any missing parent directories are created using the system's current time as their last modification time - * existing directories are not replaced. *

* Note that the entries in the file system are shared with the given {@code archive}. * * @param model the archive model to use. * @param archive the archive entry container to read the entries for * the population of the archive file system. * @param rootTemplate the optional template to use for the root entry of * the returned archive file system. * @param readOnly if not empty, any subsequent * modifying operation on the file system will result in a * {@link net.java.truevfs.kernel.spec.FsReadOnlyFileSystemException} with the contained * {@link java.lang.Throwable} as its cause. * @param the type of the archive entries. * @return A new archive file system. */ static ArchiveFileSystem apply( ArchiveModel model, Container archive, Entry rootTemplate, Optional> readOnly ) { return readOnly .>map(cause -> new ReadOnlyArchiveFileSystem<>(model, archive, rootTemplate, cause)) .orElseGet(() -> new ArchiveFileSystem<>(model, archive, rootTemplate)); } ArchiveFileSystem(final ArchiveModel model, final Container archive, final Entry rootTemplate) { // Allocate some extra capacity for creating missing parent directories. this(model, new EntryTable<>(archive.size() + OVERHEAD_SIZE)); // Load entries from source archive: val paths = new LinkedList(); val normalizer = new PathNormalizer(SEPARATOR_CHAR); archive.forEach(ae -> { val path = cutTrailingSeparators( // Fix invalid Windoze file name separators: normalizer.normalize(ae.getName().replace('\\', SEPARATOR_CHAR)), SEPARATOR_CHAR ); master.add(path, ae); if (isValidEntryName(path)) { paths.add(path); } }); // Setup root file system entry, potentially replacing its previous mapping from the source archive: master.add(RootPath, newEntry(RootPath, DIRECTORY, Optional.of(rootTemplate))); // Now perform a file system check to create missing parent directories and populate directories with their // members - this must be done separately! paths.forEach(this::fix); } /** * Called from a constructor in order to fix the parent directories of the file system entry identified by `name`, * ensuring that all parent directories of the file system entry exist and that they contain the respective member * entry. * If a parent directory does not exist, it is created using an unknown time as the last modification time - this is * defined to be a ghost directory. * If a parent directory does exist, the respective member entry is added. * * @param name the entry name. */ private void fix(String name) { while (!isRoot(name)) { splitter.split(name); val pp = splitter.getParentPath(); val mn = splitter.getMemberName(); val pcn = master .get(pp) .filter(x -> x.isType(DIRECTORY)) .orElseGet(() -> master.add(pp, newEntry(pp, DIRECTORY, Optional.empty()))); pcn.add(mn); name = pp; } } private ArchiveFileSystem(final ArchiveModel model, final EntryTable master) { this.model = model; this.master = master; } private static String typeName(final FsCovariantNode entry) { val types = entry.getTypes(); if (1 == types.cardinality()) { return typeName(types.iterator().next()); } else { return types.toString().toLowerCase(Locale.ROOT); } } private static String typeName(Entry.Type type) { return type.toString().toLowerCase(Locale.ROOT); } private static boolean isValidEntryName(String path) { return !isAbsolute(path, SEPARATOR_CHAR) && !(".." + SEPARATOR).startsWith(path.substring(0, Math.min(3, path.length()))); } @Override public ArchiveModel getModel() { return model; } @Override public int size() { return master.size(); } @Override public Iterator> iterator() { return master.iterator(); } private String fullPath(FsNodeName name) { return path(name).toString(); } /** * Possibly returns the covariant file system node for the given name. * Modifying the returned object graph is either not supported (i.e. throws an * {@link java.lang.UnsupportedOperationException} or does not have any visible side effect on this file system. * * @param name the name of the file system entry to look up. * @return A covariant file system node or {@link Optional#empty()} if no file system node exists for the given * name. */ Optional> node(final BitField options, final FsNodeName name) { return master.get(name.getPath()).map(e -> e.clone(getDriver())); } void checkAccess( final BitField options, final FsNodeName name, final BitField types ) throws IOException { if (!master.get(name.getPath()).isPresent()) { throw new NoSuchFileException(fullPath(name)); } } void setReadOnly(final BitField options, final FsNodeName name) throws IOException { throw new FileSystemException(fullPath(name), null, "Cannot set read-only state!"); } boolean setTime( final BitField options, final FsNodeName name, final Map times ) throws IOException { val cn = master.get(name.getPath()).orElseThrow(() -> new NoSuchFileException(fullPath(name))); // HC SVNT DRACONES! touch(options); val ae = cn.getEntry(); boolean ok = true; for (val time : times.entrySet()) { val access = time.getKey(); val value = time.getValue(); ok &= 0 <= value && ae.setTime(access, value); } return ok; } boolean setTime( final BitField options, final FsNodeName name, final BitField types, final long value ) throws IOException { if (0 > value) { throw new IllegalArgumentException(fullPath(name) + " (negative access time)"); } val cn = master.get(name.getPath()).orElseThrow(() -> new NoSuchFileException(fullPath(name))); // HC SVNT DRACONES! touch(options); val ae = cn.getEntry(); boolean ok = true; for (val type : types) { ok &= ae.setTime(type, value); } return ok; } /** * Begins a transaction to create or replace and finally link a chain of one or more archive entries for * the given {@code name} into this archive file system. *

* To commit the transaction, you need to call {@link Make#commit()} on the returned object, which will mark this * archive file system as touched and set the last modification time of the created and linked archive file system * entries to the system's current time at the moment of the call to this method. * * @param name the archive file system entry name. * @param type the type of the archive file system entry to create. * @param options if `CREATE_PARENTS` is set, any missing parent * directories will be created and linked into this file * system with its last modification time set to the system's * current time. * @param template if not `None`, then the archive file system entry * at the end of the chain shall inherit as much properties from * this entry as possible - with the exception of its name and type. * @return A new archive file system operation on a chain of one or more archive file system entries for the given * path name which will be linked into this archive file system upon a call to the {@link Make#commit()} method of * the returned object. * @throws IOException on any I/O error. */ Make make( final BitField options, final FsNodeName name, final Entry.Type type, final Optional template ) throws IOException { requireNonNull(type); // TODO: Add support for other entry types: if (FILE != type && DIRECTORY != type) { throw new FileSystemException(fullPath(name), null, "Can only create file or directory entries, but not a " + typeName(type) + " entry!"); } val np = name.getPath(); val ocn = master.get(np); if (ocn.isPresent()) { val cn = ocn.get(); if (!cn.isType(FILE)) { throw new FileAlreadyExistsException(fullPath(name), null, "Cannot replace a " + typeName(cn) + " entry!"); } if (FILE != type) { throw new FileAlreadyExistsException(fullPath(name), null, "Can only replace a file entry with a file entry, but not a " + typeName(type) + " entry!"); } if (options.get(EXCLUSIVE)) { throw new FileAlreadyExistsException(fullPath(name)); } } val t = template.map(e -> e instanceof FsCovariantNode ? ((FsCovariantNode) e).get(type) : e); return new Make(options, np, type, t); } /** * Represents a `make` transaction. * The transaction gets committed by calling `commit`. * The state of the archive file system will not change until this method gets called. * The head of the chain of covariant file system entries to commit can get obtained by calling `head`. *

* TODO: * The current implementation yields a potential issue: * The state of the file system may get altered between the construction of this transaction and the call to its * `commit` method. * However, the change may render this operation illegal and so the file system may get corrupted upon a call to * `commit`. * To avoid this, the caller must not allow concurrent changes to this archive file system. */ final class Make { private final BitField options; private final LinkedList> segments; private long time = UNKNOWN; Make( final BitField options, final String path, final Entry.Type type, final Optional template ) throws IOException { this.options = options; this.segments = newSegments(path, type, template); } private String fullPath(String path) { return ArchiveFileSystem.this.fullPath(FsNodeName.create(URI.create(path))); } private LinkedList> newSegments( final String path, final Entry.Type type, final Optional template ) throws IOException { splitter.split(path); val pp = splitter.getParentPath(); // may equal ROOT_PATH val mn = splitter.getMemberName(); // Lookup parent entry, creating it if necessary and allowed: val opcn = master.get(pp); if (opcn.isPresent()) { val pcn = opcn.get(); if (!pcn.isType(DIRECTORY)) { throw new NotDirectoryException(fullPath(path)); } val segments = new LinkedList>(); segments.push(new Segment<>(Optional.empty(), pcn)); val mcn = new FsCovariantNode(path); mcn.put(type, newEntry(options, path, type, template)); segments.push(new Segment<>(Optional.of(mn), mcn)); return segments; } else { if (options.get(CREATE_PARENTS)) { LinkedList> segments = newSegments(pp, DIRECTORY, Optional.empty()); val mcn = new FsCovariantNode(path); mcn.put(type, newEntry(options, path, type, template)); segments.push(new Segment<>(Optional.of(mn), mcn)); return segments; } else { throw new NoSuchFileException(fullPath(path), null, "Missing parent directory entry!"); } } } void commit() throws IOException { touch(options); val size = commit(segments); assert 2 <= size; val mae = segments.getFirst().entry.getEntry(); if (UNKNOWN == mae.getTime(WRITE)) { mae.setTime(WRITE, getTimeMillis()); } } private int commit(final List> segments) { if (0 < segments.size()) { val segment = segments.get(0); val mn = segment.getName(); val mcn = segment.getEntry(); val parentSegments = segments.subList(1, segments.size()); val parentSize = commit(parentSegments); if (0 < parentSize) { val pcn = parentSegments.get(0).entry; val pae = pcn.get(DIRECTORY); val mae = mcn.getEntry(); master.add(mcn.getName(), mae); // Never touch ghost directories: if (master.get(pcn.getName()).get().add(mn.get()) && UNKNOWN != pae.getTime(WRITE)) { pae.setTime(WRITE, getTimeMillis()); } } return 1 + parentSize; } else { return 0; } } private long getTimeMillis() { if (UNKNOWN == time) { time = System.currentTimeMillis(); } return time; } FsCovariantNode head() { return segments.getFirst().entry; } } /** * Tests the named file system entry and then - unless its the file system root - notifies the listener and deletes * the entry. * For the file system root, only the tests are performed but the listener does not get notified and the entry does * not get deleted. * For the tests to succeed, the named file system entry must exist and directory entries (including the file system * root) must be empty. * * @param name the archive file system entry name. * @throws IOException on any I/O error. */ void unlink(final BitField options, final FsNodeName name) throws IOException { // Test: val np = name.getPath(); val mcn = master.get(np).orElseThrow(() -> new NoSuchFileException(fullPath(name))); if (mcn.isType(DIRECTORY)) { if (0 != mcn.getMembers().size()) { throw new DirectoryNotEmptyException(fullPath(name)); } } if (name.isRoot()) { // Removing the root entry MUST get silently ignored in order to make the controller logic work. return; } // Notify listener and modify: touch(options); master.remove(np); { // See http://java.net/jira/browse/TRUEZIP-144 : // This is used to signal to the driver that the entry should not be included in the central directory even // if the entry is already physically present in the archive file (ZIP). // This signal will be ignored by drivers which do no support a central directory, e.g. for the TAR file // format. val mae = mcn.getEntry(); for (val type : ALL_SIZES) { mae.setSize(type, UNKNOWN); } for (val type : ALL_ACCESS) { mae.setTime(type, UNKNOWN); } } splitter.split(np); val pp = splitter.getParentPath(); val pcn = master.get(pp).get(); val ok = pcn.remove(splitter.getMemberName()); assert ok : "The parent directory of \"" + fullPath(name) + "\" does not contain this entry - archive file system is corrupted!"; val pae = pcn.get(DIRECTORY); if (UNKNOWN != pae.getTime(WRITE)) { // never touch ghost directories! pae.setTime(WRITE, System.currentTimeMillis()); } } /** * Returns a new archive entry. * Note that this is just a factory method and the returned file system entry is not (yet) linked into this * (virtual) archive file system. * * @param name the entry name. * @param type the entry type. * @param template if present, then the new entry shall inherit as many properties from this entry as possible, * with the exception of its name and type. * @return A new entry for the given name. */ private E newEntry(final String name, final Entry.Type type, final Optional template) { assert !isRoot(name) || DIRECTORY == type; return getDriver().newEntry(NONE, name, type, template.orElse(null)); } /** * Returns a new archive entry. * Note that this is just a factory method and the returned file system entry is not (yet) linked into this * (virtual) archive file system. *

* This version checks that the given entry name can get encoded by the driver's character set. * * @param name the entry name. * @param options a bit field of access options. * @param type the entry type. * @param template if not `None`, then the new entry shall inherit * as much properties from this entry as possible - with the * exception of its name and type. * @return A new entry for the given name. * @throws CharConversionException If the entry name contains characters * which cannot get encoded. * @see #make */ private E newEntry( final BitField options, final String name, final Entry.Type type, final Optional template ) throws CharConversionException { assert !isRoot(name); val driver = this.getDriver(); driver.checkEncodable(name); return driver.newEntry(options, name, type, template.orElse(null)); } /** * The master archive entry table. * * @param The type of the archive entries. */ private static final class EntryTable extends AbstractCollection> { /** * The map of covariant file system entries. *

* Note that the archive entries in the covariant file system entries in this map are shared with the * constructor parameter {@code archive} of the archive file system object. */ private final Map> map; EntryTable(final int initialSize) { this.map = new LinkedHashMap<>(initialCapacity(initialSize)); } @Override public Iterator> iterator() { return map.values().iterator(); } @Override public int size() { return map.size(); } FsCovariantNode add(String name, E ae) { val cn = map.computeIfAbsent(name, FsCovariantNode::new); cn.put(ae.getType(), ae); return cn; } Optional> get(String name) { return Optional.ofNullable(map.get(name)); } Optional> remove(String name) { return Optional.ofNullable(map.remove(name)); } } private static final class Splitter extends PathSplitter { Splitter() { super(SEPARATOR_CHAR, false); } @Nonnull @Override public String getParentPath() { val path = super.getParentPath(); return null != path ? path : RootPath; } } /** * A case class which represents a path segment for use by {@link ArchiveFileSystem.Make}. */ @Value private static class Segment { /** * The optional member name for the covariant file system entry. */ Optional name; /** * The covariant file system entry for the nullable member name. */ FsCovariantNode entry; } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy