CC-Tweaked/src/main/java/dan200/computercraft/core/filesystem/FileSystem.java

900 lines
27 KiB
Java

/*
* This file is part of ComputerCraft - http://www.computercraft.info
* Copyright Daniel Ratcliffe, 2011-2019. Do not distribute without permission.
* Send enquiries to dratcliffe@gmail.com
*/
package dan200.computercraft.core.filesystem;
import com.google.common.io.ByteStreams;
import dan200.computercraft.ComputerCraft;
import dan200.computercraft.api.filesystem.FileOperationException;
import dan200.computercraft.api.filesystem.IFileSystem;
import dan200.computercraft.api.filesystem.IMount;
import dan200.computercraft.api.filesystem.IWritableMount;
import dan200.computercraft.shared.util.IoUtil;
import javax.annotation.Nonnull;
import java.io.Closeable;
import java.io.IOException;
import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.WeakReference;
import java.nio.channels.Channel;
import java.nio.channels.ReadableByteChannel;
import java.nio.channels.WritableByteChannel;
import java.nio.file.AccessDeniedException;
import java.util.*;
import java.util.function.Function;
import java.util.regex.Pattern;
public class FileSystem
{
private static class MountWrapper
{
private String m_label;
private String m_location;
private IMount m_mount;
private IWritableMount m_writableMount;
MountWrapper( String label, String location, IMount mount )
{
m_label = label;
m_location = location;
m_mount = mount;
m_writableMount = null;
}
MountWrapper( String label, String location, IWritableMount mount )
{
this( label, location, (IMount) mount );
m_writableMount = mount;
}
public String getLabel()
{
return m_label;
}
public String getLocation()
{
return m_location;
}
public long getFreeSpace()
{
if( m_writableMount == null )
{
return 0;
}
try
{
return m_writableMount.getRemainingSpace();
}
catch( IOException e )
{
return 0;
}
}
public boolean isReadOnly( String path )
{
return m_writableMount == null;
}
// IMount forwarders:
public boolean exists( String path ) throws FileSystemException
{
path = toLocal( path );
try
{
return m_mount.exists( path );
}
catch( IOException e )
{
throw new FileSystemException( e.getMessage() );
}
}
public boolean isDirectory( String path ) throws FileSystemException
{
path = toLocal( path );
try
{
return m_mount.exists( path ) && m_mount.isDirectory( path );
}
catch( IOException e )
{
throw localExceptionOf( e );
}
}
public void list( String path, List<String> contents ) throws FileSystemException
{
path = toLocal( path );
try
{
if( m_mount.exists( path ) && m_mount.isDirectory( path ) )
{
m_mount.list( path, contents );
}
else
{
throw localExceptionOf( path, "Not a directory" );
}
}
catch( IOException e )
{
throw localExceptionOf( e );
}
}
public long getSize( String path ) throws FileSystemException
{
path = toLocal( path );
try
{
if( m_mount.exists( path ) )
{
if( m_mount.isDirectory( path ) )
{
return 0;
}
else
{
return m_mount.getSize( path );
}
}
else
{
throw localExceptionOf( path, "No such file" );
}
}
catch( IOException e )
{
throw localExceptionOf( e );
}
}
public ReadableByteChannel openForRead( String path ) throws FileSystemException
{
path = toLocal( path );
try
{
if( m_mount.exists( path ) && !m_mount.isDirectory( path ) )
{
return m_mount.openChannelForRead( path );
}
else
{
throw localExceptionOf( path, "No such file" );
}
}
catch( IOException e )
{
throw localExceptionOf( e );
}
}
// IWritableMount forwarders:
public void makeDirectory( String path ) throws FileSystemException
{
if( m_writableMount == null ) throw exceptionOf( path, "Access denied" );
path = toLocal( path );
try
{
if( m_mount.exists( path ) )
{
if( !m_mount.isDirectory( path ) ) throw localExceptionOf( path, "File exists" );
}
else
{
m_writableMount.makeDirectory( path );
}
}
catch( IOException e )
{
throw localExceptionOf( e );
}
}
public void delete( String path ) throws FileSystemException
{
if( m_writableMount == null ) throw exceptionOf( path, "Access denied" );
try
{
path = toLocal( path );
if( m_mount.exists( path ) )
{
m_writableMount.delete( path );
}
}
catch( AccessDeniedException e )
{
throw new FileSystemException( "Access denied" );
}
catch( IOException e )
{
throw localExceptionOf( e );
}
}
public WritableByteChannel openForWrite( String path ) throws FileSystemException
{
if( m_writableMount == null ) throw exceptionOf( path, "Access denied" );
path = toLocal( path );
try
{
if( m_mount.exists( path ) && m_mount.isDirectory( path ) )
{
throw localExceptionOf( path, "Cannot write to directory" );
}
else
{
if( !path.isEmpty() )
{
String dir = getDirectory( path );
if( !dir.isEmpty() && !m_mount.exists( path ) )
{
m_writableMount.makeDirectory( dir );
}
}
return m_writableMount.openChannelForWrite( path );
}
}
catch( AccessDeniedException e )
{
throw new FileSystemException( "Access denied" );
}
catch( IOException e )
{
throw localExceptionOf( e );
}
}
public WritableByteChannel openForAppend( String path ) throws FileSystemException
{
if( m_writableMount == null ) throw exceptionOf( path, "Access denied" );
path = toLocal( path );
try
{
if( !m_mount.exists( path ) )
{
if( !path.isEmpty() )
{
String dir = getDirectory( path );
if( !dir.isEmpty() && !m_mount.exists( path ) )
{
m_writableMount.makeDirectory( dir );
}
}
return m_writableMount.openChannelForWrite( path );
}
else if( m_mount.isDirectory( path ) )
{
throw localExceptionOf( path, "Cannot write to directory" );
}
else
{
return m_writableMount.openChannelForAppend( path );
}
}
catch( AccessDeniedException e )
{
throw new FileSystemException( "Access denied" );
}
catch( IOException e )
{
throw localExceptionOf( e );
}
}
private String toLocal( String path )
{
return FileSystem.toLocal( path, m_location );
}
private FileSystemException localExceptionOf( IOException e )
{
if( !m_location.isEmpty() && e instanceof FileOperationException )
{
FileOperationException ex = (FileOperationException) e;
if( ex.getFilename() != null ) return localExceptionOf( ex.getFilename(), ex.getMessage() );
}
return new FileSystemException( e.getMessage() );
}
private FileSystemException localExceptionOf( String path, String message )
{
if( !m_location.isEmpty() ) path = path.isEmpty() ? m_location : m_location + "/" + path;
return exceptionOf( path, message );
}
private static FileSystemException exceptionOf( String path, String message )
{
return new FileSystemException( "/" + path + ": " + message );
}
}
private final FileSystemWrapperMount m_wrapper = new FileSystemWrapperMount( this );
private final Map<String, MountWrapper> m_mounts = new HashMap<>();
private final HashMap<WeakReference<FileSystemWrapper<?>>, ChannelWrapper<?>> m_openFiles = new HashMap<>();
private final ReferenceQueue<FileSystemWrapper<?>> m_openFileQueue = new ReferenceQueue<>();
public FileSystem( String rootLabel, IMount rootMount ) throws FileSystemException
{
mount( rootLabel, "", rootMount );
}
public FileSystem( String rootLabel, IWritableMount rootMount ) throws FileSystemException
{
mountWritable( rootLabel, "", rootMount );
}
public void close()
{
// Close all dangling open files
synchronized( m_openFiles )
{
for( Closeable file : m_openFiles.values() ) IoUtil.closeQuietly( file );
m_openFiles.clear();
while( m_openFileQueue.poll() != null ) ;
}
}
public synchronized void mount( String label, String location, IMount mount ) throws FileSystemException
{
if( mount == null ) throw new NullPointerException();
location = sanitizePath( location );
if( location.contains( ".." ) )
{
throw new FileSystemException( "Cannot mount below the root" );
}
mount( new MountWrapper( label, location, mount ) );
}
public synchronized void mountWritable( String label, String location, IWritableMount mount ) throws FileSystemException
{
if( mount == null )
{
throw new NullPointerException();
}
location = sanitizePath( location );
if( location.contains( ".." ) )
{
throw new FileSystemException( "Cannot mount below the root" );
}
mount( new MountWrapper( label, location, mount ) );
}
private synchronized void mount( MountWrapper wrapper )
{
String location = wrapper.getLocation();
m_mounts.remove( location );
m_mounts.put( location, wrapper );
}
public synchronized void unmount( String path )
{
path = sanitizePath( path );
m_mounts.remove( path );
}
public synchronized String combine( String path, String childPath )
{
path = sanitizePath( path, true );
childPath = sanitizePath( childPath, true );
if( path.isEmpty() )
{
return childPath;
}
else if( childPath.isEmpty() )
{
return path;
}
else
{
return sanitizePath( path + '/' + childPath, true );
}
}
public static String getDirectory( String path )
{
path = sanitizePath( path, true );
if( path.isEmpty() )
{
return "..";
}
int lastSlash = path.lastIndexOf( '/' );
if( lastSlash >= 0 )
{
return path.substring( 0, lastSlash );
}
else
{
return "";
}
}
public static String getName( String path )
{
path = sanitizePath( path, true );
if( path.isEmpty() )
{
return "root";
}
int lastSlash = path.lastIndexOf( '/' );
if( lastSlash >= 0 )
{
return path.substring( lastSlash + 1 );
}
else
{
return path;
}
}
public synchronized long getSize( String path ) throws FileSystemException
{
path = sanitizePath( path );
MountWrapper mount = getMount( path );
return mount.getSize( path );
}
public synchronized String[] list( String path ) throws FileSystemException
{
path = sanitizePath( path );
MountWrapper mount = getMount( path );
// Gets a list of the files in the mount
List<String> list = new ArrayList<>();
mount.list( path, list );
// Add any mounts that are mounted at this location
for( MountWrapper otherMount : m_mounts.values() )
{
if( getDirectory( otherMount.getLocation() ).equals( path ) )
{
list.add( getName( otherMount.getLocation() ) );
}
}
// Return list
String[] array = new String[list.size()];
list.toArray( array );
Arrays.sort( array );
return array;
}
private void findIn( String dir, List<String> matches, Pattern wildPattern ) throws FileSystemException
{
String[] list = list( dir );
for( String entry : list )
{
String entryPath = dir.isEmpty() ? entry : dir + "/" + entry;
if( wildPattern.matcher( entryPath ).matches() )
{
matches.add( entryPath );
}
if( isDir( entryPath ) )
{
findIn( entryPath, matches, wildPattern );
}
}
}
public synchronized String[] find( String wildPath ) throws FileSystemException
{
// Match all the files on the system
wildPath = sanitizePath( wildPath, true );
// If we don't have a wildcard at all just check the file exists
int starIndex = wildPath.indexOf( '*' );
if( starIndex == -1 )
{
return exists( wildPath ) ? new String[] { wildPath } : new String[0];
}
// Find the all non-wildcarded directories. For instance foo/bar/baz* -> foo/bar
int prevDir = wildPath.substring( 0, starIndex ).lastIndexOf( '/' );
String startDir = prevDir == -1 ? "" : wildPath.substring( 0, prevDir );
// If this isn't a directory then just abort
if( !isDir( startDir ) ) return new String[0];
// Scan as normal, starting from this directory
Pattern wildPattern = Pattern.compile( "^\\Q" + wildPath.replaceAll( "\\*", "\\\\E[^\\\\/]*\\\\Q" ) + "\\E$" );
List<String> matches = new ArrayList<>();
findIn( startDir, matches, wildPattern );
// Return matches
String[] array = new String[matches.size()];
matches.toArray( array );
return array;
}
public synchronized boolean exists( String path ) throws FileSystemException
{
path = sanitizePath( path );
MountWrapper mount = getMount( path );
return mount.exists( path );
}
public synchronized boolean isDir( String path ) throws FileSystemException
{
path = sanitizePath( path );
MountWrapper mount = getMount( path );
return mount.isDirectory( path );
}
public synchronized boolean isReadOnly( String path ) throws FileSystemException
{
path = sanitizePath( path );
MountWrapper mount = getMount( path );
return mount.isReadOnly( path );
}
public synchronized String getMountLabel( String path ) throws FileSystemException
{
path = sanitizePath( path );
MountWrapper mount = getMount( path );
return mount.getLabel();
}
public synchronized void makeDir( String path ) throws FileSystemException
{
path = sanitizePath( path );
MountWrapper mount = getMount( path );
mount.makeDirectory( path );
}
public synchronized void delete( String path ) throws FileSystemException
{
path = sanitizePath( path );
MountWrapper mount = getMount( path );
mount.delete( path );
}
public synchronized void move( String sourcePath, String destPath ) throws FileSystemException
{
sourcePath = sanitizePath( sourcePath );
destPath = sanitizePath( destPath );
if( isReadOnly( sourcePath ) || isReadOnly( destPath ) )
{
throw new FileSystemException( "Access denied" );
}
if( !exists( sourcePath ) )
{
throw new FileSystemException( "No such file" );
}
if( exists( destPath ) )
{
throw new FileSystemException( "File exists" );
}
if( contains( sourcePath, destPath ) )
{
throw new FileSystemException( "Can't move a directory inside itself" );
}
copy( sourcePath, destPath );
delete( sourcePath );
}
public synchronized void copy( String sourcePath, String destPath ) throws FileSystemException
{
sourcePath = sanitizePath( sourcePath );
destPath = sanitizePath( destPath );
if( isReadOnly( destPath ) )
{
throw new FileSystemException( "/" + destPath + ": Access denied" );
}
if( !exists( sourcePath ) )
{
throw new FileSystemException( "/" + sourcePath + ": No such file" );
}
if( exists( destPath ) )
{
throw new FileSystemException( "/" + destPath + ": File exists" );
}
if( contains( sourcePath, destPath ) )
{
throw new FileSystemException( "/" + sourcePath + ": Can't copy a directory inside itself" );
}
copyRecursive( sourcePath, getMount( sourcePath ), destPath, getMount( destPath ) );
}
private synchronized void copyRecursive( String sourcePath, MountWrapper sourceMount, String destinationPath, MountWrapper destinationMount ) throws FileSystemException
{
if( !sourceMount.exists( sourcePath ) )
{
return;
}
if( sourceMount.isDirectory( sourcePath ) )
{
// Copy a directory:
// Make the new directory
destinationMount.makeDirectory( destinationPath );
// Copy the source contents into it
List<String> sourceChildren = new ArrayList<>();
sourceMount.list( sourcePath, sourceChildren );
for( String child : sourceChildren )
{
copyRecursive(
combine( sourcePath, child ), sourceMount,
combine( destinationPath, child ), destinationMount
);
}
}
else
{
// Copy a file:
try( ReadableByteChannel source = sourceMount.openForRead( sourcePath );
WritableByteChannel destination = destinationMount.openForWrite( destinationPath ) )
{
// Copy bytes as fast as we can
ByteStreams.copy( source, destination );
}
catch( AccessDeniedException e )
{
throw new FileSystemException( "Access denied" );
}
catch( IOException e )
{
throw new FileSystemException( e.getMessage() );
}
}
}
private void cleanup()
{
synchronized( m_openFiles )
{
Reference<?> ref;
while( (ref = m_openFileQueue.poll()) != null )
{
Closeable file = m_openFiles.remove( ref );
if( file != null ) IoUtil.closeQuietly( file );
}
}
}
private synchronized <T extends Closeable> FileSystemWrapper<T> openFile( @Nonnull Channel channel, @Nonnull T file ) throws FileSystemException
{
synchronized( m_openFiles )
{
if( ComputerCraft.maximumFilesOpen > 0 &&
m_openFiles.size() >= ComputerCraft.maximumFilesOpen )
{
IoUtil.closeQuietly( file );
IoUtil.closeQuietly( channel );
throw new FileSystemException( "Too many files already open" );
}
ChannelWrapper<T> channelWrapper = new ChannelWrapper<>( file, channel );
FileSystemWrapper<T> fsWrapper = new FileSystemWrapper<>( this, channelWrapper, m_openFileQueue );
m_openFiles.put( fsWrapper.self, channelWrapper );
return fsWrapper;
}
}
synchronized void removeFile( FileSystemWrapper<?> handle )
{
synchronized( m_openFiles )
{
m_openFiles.remove( handle.self );
}
}
public synchronized <T extends Closeable> FileSystemWrapper<T> openForRead( String path, Function<ReadableByteChannel, T> open ) throws FileSystemException
{
cleanup();
path = sanitizePath( path );
MountWrapper mount = getMount( path );
ReadableByteChannel channel = mount.openForRead( path );
if( channel != null )
{
return openFile( channel, open.apply( channel ) );
}
return null;
}
public synchronized <T extends Closeable> FileSystemWrapper<T> openForWrite( String path, boolean append, Function<WritableByteChannel, T> open ) throws FileSystemException
{
cleanup();
path = sanitizePath( path );
MountWrapper mount = getMount( path );
WritableByteChannel channel = append ? mount.openForAppend( path ) : mount.openForWrite( path );
if( channel != null )
{
return openFile( channel, open.apply( channel ) );
}
return null;
}
public long getFreeSpace( String path ) throws FileSystemException
{
path = sanitizePath( path );
MountWrapper mount = getMount( path );
return mount.getFreeSpace();
}
private MountWrapper getMount( String path ) throws FileSystemException
{
// Return the deepest mount that contains a given path
Iterator<MountWrapper> it = m_mounts.values().iterator();
MountWrapper match = null;
int matchLength = 999;
while( it.hasNext() )
{
MountWrapper mount = it.next();
if( contains( mount.getLocation(), path ) )
{
int len = toLocal( path, mount.getLocation() ).length();
if( match == null || len < matchLength )
{
match = mount;
matchLength = len;
}
}
}
if( match == null )
{
throw new FileSystemException( "/" + path + ": Invalid Path" );
}
return match;
}
public IFileSystem getMountWrapper()
{
return m_wrapper;
}
private static String sanitizePath( String path )
{
return sanitizePath( path, false );
}
private static final Pattern threeDotsPattern = Pattern.compile( "^\\.{3,}$" );
private static String sanitizePath( String path, boolean allowWildcards )
{
// Allow windowsy slashes
path = path.replace( '\\', '/' );
// Clean the path or illegal characters.
final char[] specialChars = new char[] {
'"', ':', '<', '>', '?', '|', // Sorted by ascii value (important)
};
StringBuilder cleanName = new StringBuilder();
for( int i = 0; i < path.length(); i++ )
{
char c = path.charAt( i );
if( c >= 32 && Arrays.binarySearch( specialChars, c ) < 0 && (allowWildcards || c != '*') )
{
cleanName.append( c );
}
}
path = cleanName.toString();
// Collapse the string into its component parts, removing ..'s
String[] parts = path.split( "/" );
Stack<String> outputParts = new Stack<>();
for( String part : parts )
{
if( part.isEmpty() || part.equals( "." ) || threeDotsPattern.matcher( part ).matches() )
{
// . is redundant
// ... and more are treated as .
continue;
}
if( part.equals( ".." ) )
{
// .. can cancel out the last folder entered
if( !outputParts.empty() )
{
String top = outputParts.peek();
if( !top.equals( ".." ) )
{
outputParts.pop();
}
else
{
outputParts.push( ".." );
}
}
else
{
outputParts.push( ".." );
}
}
else if( part.length() >= 255 )
{
// If part length > 255 and it is the last part
outputParts.push( part.substring( 0, 255 ) );
}
else
{
// Anything else we add to the stack
outputParts.push( part );
}
}
// Recombine the output parts into a new string
StringBuilder result = new StringBuilder();
Iterator<String> it = outputParts.iterator();
while( it.hasNext() )
{
String part = it.next();
result.append( part );
if( it.hasNext() )
{
result.append( '/' );
}
}
return result.toString();
}
public static boolean contains( String pathA, String pathB )
{
pathA = sanitizePath( pathA );
pathB = sanitizePath( pathB );
if( pathB.equals( ".." ) )
{
return false;
}
else if( pathB.startsWith( "../" ) )
{
return false;
}
else if( pathB.equals( pathA ) )
{
return true;
}
else if( pathA.isEmpty() )
{
return true;
}
else
{
return pathB.startsWith( pathA + "/" );
}
}
public static String toLocal( String path, String location )
{
path = sanitizePath( path );
location = sanitizePath( location );
assert contains( location, path );
String local = path.substring( location.length() );
if( local.startsWith( "/" ) )
{
return local.substring( 1 );
}
else
{
return local;
}
}
}