| // 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 = new 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 { |
| /// 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; |
| |
| /// 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 new ArgumentError.value(root, 'root', 'Must be canonical path'); |
| } |
| _cwd = _localRoot; |
| } |
| |
| /// Gets the root path, as seen by entities in this file system. |
| String get _localRoot => delegate.path.rootPrefix(root); |
| |
| @override |
| Directory directory(dynamic path) => |
| new _ChrootDirectory(this, common.getPath(path)); |
| |
| @override |
| File file(dynamic path) => new _ChrootFile(this, common.getPath(path)); |
| |
| @override |
| Link link(dynamic path) => new _ChrootLink(this, common.getPath(path)); |
| |
| @override |
| p.Context get path => |
| new 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 new 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.NOT_FOUND: |
| 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 new 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 new Future<FileSystemEntityType>.value( |
| FileSystemEntityType.NOT_FOUND); |
| } |
| 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.NOT_FOUND; |
| } |
| 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 new _ChrootJailException(); |
| } |
| // TODO: 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, |
| }) { |
| 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 = new 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.NOT_FOUND: |
| 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 new 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.NOT_FOUND; |
| |
| @override |
| int get mode => 0; |
| |
| @override |
| int get size => -1; |
| |
| @override |
| String modeString() => '---------'; |
| } |