blob: 0baec9b8c4a9ef678254cdff1264fa783d7248a1 [file] [log] [blame]
// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
part of file.src.backends.chroot;
const String _thisDir = '.';
const String _parentDir = '..';
/// File system that provides a view into _another_ [FileSystem] via a path.
///
/// This is similar in concept to the `chroot` operation in Linux operating
/// systems. Such a modified file system cannot name or access files outside of
/// the designated directory tree.
///
/// ## Example use:
/// ```dart
/// // Create a "file system" where the root directory is /tmp/some-dir.
/// var fs = ChrootFileSystem(existingFileSystem, '/tmp/some-dir');
/// ```
///
/// **Notes on usage**:
///
/// * This file system maintains its _own_ [currentDirectory], distinct from
/// that of the underlying file system, and new instances automatically start
/// at the root (i.e. `/`).
///
/// * This file system does _not_ leverage any underlying OS system calls (such
/// as `chroot` itself), so the developer needs to take care to not assume any
/// more of a secure environment than is actually provided. For instance, the
/// underlying system is available via the [delegate] - which underscores this
/// file system is intended to be a convenient abstraction, not a security
/// measure.
///
/// * This file system _necessarily_ carries certain performance overhead due
/// to the fact that symbolic links are resolved manually (not delegated).
class ChrootFileSystem extends FileSystem {
/// Creates a new file system backed by [root] path in [delegate] file system.
///
/// **NOTE**: [root] must be a _canonicalized_ path; see [p.canonicalize].
ChrootFileSystem(this.delegate, this.root) {
if (root != delegate.path.canonicalize(root)) {
throw ArgumentError.value(root, 'root', 'Must be canonical path');
}
_cwd = _localRoot;
}
/// Underlying file system.
final FileSystem delegate;
/// Directory in [delegate] file system that is treated as the root here.
final String root;
String _systemTemp;
/// Path to the synthetic current working directory in this file system.
String _cwd;
/// Gets the root path, as seen by entities in this file system.
String get _localRoot => delegate.path.rootPrefix(root);
@override
Directory directory(dynamic path) => _ChrootDirectory(this, getPath(path));
@override
File file(dynamic path) => _ChrootFile(this, getPath(path));
@override
Link link(dynamic path) => _ChrootLink(this, getPath(path));
@override
p.Context get path => p.Context(style: delegate.path.style, current: _cwd);
/// Gets the system temp directory. This directory will be created on-demand
/// in the local root of the file system. Once created, its location is fixed
/// for the life of the process.
@override
Directory get systemTempDirectory {
_systemTemp ??= directory(_localRoot).createTempSync('.tmp_').path;
return directory(_systemTemp)..createSync();
}
/// Creates a directory object pointing to the current working directory.
///
/// **NOTE** This does _not_ proxy to the underlying file system's current
/// directory in any way; the state of this file system's current directory
/// is local to this file system.
@override
Directory get currentDirectory => directory(_cwd);
/// Sets the current working directory to the specified [path].
///
/// **NOTE** This does _not_ proxy to the underlying file system's current
/// directory in any way; the state of this file system's current directory
/// is local to this file system.
/// Gets the path context for this file system given the current working dir.
@override
set currentDirectory(dynamic path) {
String value;
if (path is io.Directory) {
value = path.path;
} else if (path is String) {
value = path;
} else {
throw ArgumentError('Invalid type for "path": ${path?.runtimeType}');
}
value = _resolve(value, notFound: _NotFoundBehavior.throwError);
String realPath = _real(value, resolve: false);
switch (delegate.typeSync(realPath, followLinks: false)) {
case FileSystemEntityType.directory:
break;
case FileSystemEntityType.notFound:
throw common.noSuchFileOrDirectory(path);
default:
throw common.notADirectory(path);
}
assert(() {
p.Context ctx = delegate.path;
return ctx.isAbsolute(value) && value == ctx.canonicalize(value);
}());
_cwd = value;
}
@override
Future<FileStat> stat(String path) {
try {
path = _resolve(path);
} on FileSystemException {
return Future<FileStat>.value(const _NotFoundFileStat());
}
return delegate.stat(_real(path, resolve: false));
}
@override
FileStat statSync(String path) {
try {
path = _resolve(path);
} on FileSystemException {
return const _NotFoundFileStat();
}
return delegate.statSync(_real(path, resolve: false));
}
@override
Future<bool> identical(String path1, String path2) => delegate.identical(
_real(_resolve(path1, followLinks: false)),
_real(_resolve(path2, followLinks: false)),
);
@override
bool identicalSync(String path1, String path2) => delegate.identicalSync(
_real(_resolve(path1, followLinks: false)),
_real(_resolve(path2, followLinks: false)),
);
@override
bool get isWatchSupported => false;
@override
Future<FileSystemEntityType> type(String path, {bool followLinks = true}) {
String realPath;
try {
realPath = _real(path, followLinks: followLinks);
} on FileSystemException {
return Future<FileSystemEntityType>.value(FileSystemEntityType.notFound);
}
return delegate.type(realPath, followLinks: false);
}
@override
FileSystemEntityType typeSync(String path, {bool followLinks = true}) {
String realPath;
try {
realPath = _real(path, followLinks: followLinks);
} on FileSystemException {
return FileSystemEntityType.notFound;
}
return delegate.typeSync(realPath, followLinks: false);
}
/// Converts a [realPath] in the underlying file system to a local path here.
///
/// If [relative] is set to `true`, then the resulting path will be relative
/// to [currentDirectory], otherwise the resulting path will be absolute.
///
/// An exception is thrown if the path is outside of this file system's root
/// directory unless [keepInJail] is true, in which case this will instead
/// return the path of the root of this file system.
String _local(
String realPath, {
bool relative = false,
bool keepInJail = false,
}) {
assert(path.isAbsolute(realPath));
if (!realPath.startsWith(root)) {
if (keepInJail) {
return _localRoot;
}
throw _ChrootJailException();
}
// TODO(tvolkert): See if _context.relative() works here
String result = realPath.substring(root.length);
if (result.isEmpty) {
result = _localRoot;
}
if (relative) {
assert(result.startsWith(_cwd));
result = path.relative(result, from: _cwd);
}
return result;
}
/// Converts [localPath] in this file system to the real path in the delegate.
///
/// The returned path will always be absolute.
///
/// If [resolve] is true, symbolic links will be resolved in the local file
/// system _before_ converting the path to the delegate file system's
/// namespace, and if the tail element of the path is a symbolic link, it will
/// only be resolved if [followLinks] is true (where-as symbolic links found
/// in the middle of the path will always be resolved).
String _real(
String localPath, {
bool resolve = true,
bool followLinks = false,
}) {
if (resolve) {
localPath = _resolve(localPath, followLinks: followLinks);
} else {
assert(path.isAbsolute(localPath));
}
return '$root$localPath';
}
/// Resolves symbolic links on [path] and returns the resulting resolved path.
///
/// The return value will always be an absolute path; if [path] is relative
/// it will be interpreted relative to [from] (or [currentDirectory] if
/// `null`).
///
/// If the tail element is a symbolic link, then the link will be resolved
/// only if [followLinks] is `true`. Symbolic links found in the middle of
/// the path will always be resolved.
///
/// If the path cannot be resolved, and [notFound] is:
/// - [_NotFoundBehavior.throwError]: a [FileSystemException] is thrown.
/// - [_NotFoundBehavior.mkdir]: the path will be created as needed.
/// - [_NotFoundBehavior.allowAtTail]: a [FileSystemException] is thrown,
/// unless only the *tail* path element cannot be resolved, in which case
/// the resolution will halt at the tail element, and the partially
/// resolved path will be returned.
/// - [_NotFoundBehavior.allow] (the default), the resolution will halt and
/// the partially resolved path will be returned.
String _resolve(
String path, {
String from,
bool followLinks = true,
_NotFoundBehavior notFound = _NotFoundBehavior.allow,
}) {
if (path.isEmpty) {
throw common.noSuchFileOrDirectory(path);
}
p.Context ctx = this.path;
String root = _localRoot;
List<String> parts, ledger;
if (ctx.isAbsolute(path)) {
parts = ctx.split(path).sublist(1);
ledger = <String>[];
} else {
from ??= _cwd;
assert(ctx.isAbsolute(from));
parts = ctx.split(path);
ledger = ctx.split(from).sublist(1);
}
String getCurrentPath() => root + ctx.joinAll(ledger);
Set<String> breadcrumbs = Set<String>();
while (parts.isNotEmpty) {
String segment = parts.removeAt(0);
if (segment == _thisDir) {
continue;
} else if (segment == _parentDir) {
if (ledger.isNotEmpty) {
ledger.removeLast();
}
continue;
}
ledger.add(segment);
String currentPath = getCurrentPath();
String realPath = _real(currentPath, resolve: false);
switch (delegate.typeSync(realPath, followLinks: false)) {
case FileSystemEntityType.directory:
breadcrumbs.clear();
break;
case FileSystemEntityType.file:
breadcrumbs.clear();
if (parts.isNotEmpty) {
throw common.notADirectory(currentPath);
}
break;
case FileSystemEntityType.notFound:
String returnEarly() {
ledger.addAll(parts);
return getCurrentPath();
}
switch (notFound) {
case _NotFoundBehavior.mkdir:
if (parts.isNotEmpty) {
delegate.directory(realPath).createSync();
}
break;
case _NotFoundBehavior.allow:
return returnEarly();
case _NotFoundBehavior.allowAtTail:
if (parts.isEmpty) {
return returnEarly();
}
throw common.noSuchFileOrDirectory(path);
case _NotFoundBehavior.throwError:
throw common.noSuchFileOrDirectory(path);
}
break;
case FileSystemEntityType.link:
if (parts.isEmpty && !followLinks) {
break;
}
if (!breadcrumbs.add(currentPath)) {
throw common.tooManyLevelsOfSymbolicLinks(path);
}
String target = delegate.link(realPath).targetSync();
if (ctx.isAbsolute(target)) {
ledger.clear();
parts.insertAll(0, ctx.split(target).sublist(1));
} else {
ledger.removeLast();
parts.insertAll(0, ctx.split(target));
}
break;
default:
throw AssertionError();
}
}
return getCurrentPath();
}
}
/// Thrown when a path is encountered that exists outside of the root path.
class _ChrootJailException implements IOException {}
/// What to do when `NOT_FOUND` paths are encountered while resolving.
enum _NotFoundBehavior {
allow,
allowAtTail,
throwError,
mkdir,
}
/// A [FileStat] representing a `NOT_FOUND` entity.
class _NotFoundFileStat implements FileStat {
const _NotFoundFileStat();
@override
DateTime get changed => null;
@override
DateTime get modified => null;
@override
DateTime get accessed => null;
@override
FileSystemEntityType get type => FileSystemEntityType.notFound;
@override
int get mode => 0;
@override
int get size => -1;
@override
String modeString() => '---------';
}