// dart:io for Dart programs on Domainless Code (domainless.fun/code), version 1. // // Programs there are compiled to JavaScript by dart2js and run in the browser's terminal, where the SDK's own dart:io // can only throw ("Unsupported operation"). The compile server points every `import 'dart:io'` (and export) at this // library, which keeps dart:io's names and behaviour for what a browser page can do: the console (stdin, stdout, // stderr), exit and exitCode, sleep, the project's files (File, Directory, FileSystemEntity) and Platform. Processes, // sockets and HTTP aren't here: a program that uses them doesn't compile, and the terminal says why. // // Every call goes to one JavaScript object the terminal provides, __dlio (runDart in terminal/worker.mjs). Typed input // works as the terminal's other languages do: a program that asks for a line that hasn't been typed yet stops there, // and runs again from the start once it has, with everything typed so far. library; import 'dart:async'; import 'dart:convert'; import 'dart:js_interop'; import 'dart:typed_data'; @JS('__dlio') external _Host get _host; extension type _Host._(JSObject _) implements JSObject { external void write(int fd, String text); external void writeBytes(int fd, JSUint8Array bytes); // the next typed line (null at the end of input); one not typed yet stops the program here (see above) external String? readLine(bool keepNewline); external void exit(int code); external int get exitCode; external set exitCode(int code); external void sleep(int ms); // files: paths as the program wrote them; the terminal resolves them against the project's root external JSUint8Array? readFile(String path); // a copy; null when there's no such file external bool writeFile(String path, JSUint8Array bytes, bool append); external int kind(String path); // 0 nothing there, 1 a file, 2 a folder external int size(String path); // -1 when there's no such file external JSArray list(String path, bool recursive); // names under it; a folder's ends with "/" external bool makeDir(String path, bool recursive); external bool remove(String path, bool recursive); external bool rename(String from, String to); external String resolve(String path); // the absolute path ("/data.txt") external int get processors; external String get locale; external int get columns; external int get rows; external String get script; } // ── errors ── abstract interface class IOException implements Exception { @override String toString() => 'IOException'; } class OSError implements Exception { static const int noErrorCode = -1; final String message; final int errorCode; const OSError([this.message = '', this.errorCode = noErrorCode]); @override String toString() { final sb = StringBuffer('OS Error'); if (message.isNotEmpty) { sb..write(': ')..write(message); if (errorCode != noErrorCode) sb..write(', errno = ')..write(errorCode.toString()); } else if (errorCode != noErrorCode) { sb..write(': errno = ')..write(errorCode.toString()); } return sb.toString(); } } class FileSystemException implements IOException { final String message; final String? path; final OSError? osError; const FileSystemException([this.message = '', this.path = '', this.osError]); String get _name => 'FileSystemException'; @override String toString() { final sb = StringBuffer(_name); if (message.isNotEmpty) { sb..write(': ')..write(message); if (path != null) sb..write(", path = '")..write(path)..write("'"); if (osError != null) sb..write(' (')..write(osError.toString())..write(')'); } else if (osError != null) { sb..write(': ')..write(osError.toString()); if (path != null) sb..write(", path = '")..write(path)..write("'"); } else if (path != null) { sb..write(': ')..write(path); } return sb.toString(); } } class PathNotFoundException extends FileSystemException { const PathNotFoundException(String path, OSError osError, [String message = '']) : super(message, path, osError); @override String get _name => 'PathNotFoundException'; } class PathExistsException extends FileSystemException { const PathExistsException(String path, OSError osError, [String message = '']) : super(message, path, osError); @override String get _name => 'PathExistsException'; } class StdinException implements IOException { final String message; final OSError? osError; const StdinException(this.message, [this.osError]); @override String toString() => 'StdinException: $message${osError == null ? '' : ', $osError'}'; } class StdoutException implements IOException { final String message; final OSError? osError; const StdoutException(this.message, [this.osError]); @override String toString() => 'StdoutException: $message${osError == null ? '' : ', $osError'}'; } PathNotFoundException _notFound(String message, String path) => PathNotFoundException(path, const OSError('No such file or directory', 2), message); FileSystemException _isDir(String message, String path) => FileSystemException(message, path, const OSError('Is a directory', 21)); // ── the console ── class SystemEncoding extends Encoding { const SystemEncoding(); @override String get name => 'system'; @override Converter> get encoder => utf8.encoder; @override Converter, String> get decoder => utf8.decoder; } const SystemEncoding systemEncoding = SystemEncoding(); abstract interface class IOSink implements StreamSink>, StringSink { Encoding get encoding; set encoding(Encoding value); @override void add(List data); @override void write(Object? object); @override void writeAll(Iterable objects, [String separator = '']); @override void writeln([Object? object = '']); @override void writeCharCode(int charCode); @override void addError(Object error, [StackTrace? stackTrace]); @override Future addStream(Stream> stream); Future flush(); @override Future close(); @override Future get done; } Uint8List _bytes(List data) => data is Uint8List ? data : Uint8List.fromList(data); /// stdout and stderr: what's written shows in the terminal at once (nothing waits in a buffer). class Stdout implements IOSink { final int _fd; String _eol = '\n'; Stdout._(this._fd); @override Encoding encoding = systemEncoding; bool get hasTerminal => true; int get terminalColumns => _host.columns; int get terminalLines => _host.rows; bool get supportsAnsiEscapes => true; IOSink get nonBlocking => this; String get lineTerminator => _eol; set lineTerminator(String value) { if (value != '\n' && value != '\r\n') { throw ArgumentError.value(value, 'lineTerminator', r'invalid line terminator, must be one of "\n" or "\r\n"'); } _eol = value; } String _lines(String s) => _eol == '\n' ? s : s.replaceAll('\n', _eol); @override void write(Object? object) => _host.write(_fd, _lines('$object')); @override void writeln([Object? object = '']) => _host.write(_fd, _lines('$object\n')); @override void writeAll(Iterable objects, [String separator = '']) => _host.write(_fd, _lines(objects.join(separator))); @override void writeCharCode(int charCode) => _host.write(_fd, _lines(String.fromCharCode(charCode))); @override void add(List data) => _host.writeBytes(_fd, _bytes(data).toJS); @override void addError(Object error, [StackTrace? stackTrace]) => Zone.current.handleUncaughtError(error, stackTrace ?? StackTrace.current); @override Future addStream(Stream> stream) => stream.forEach(add); @override Future flush() => Future.value(); @override Future close() => done; @override Future get done => Future.value(); } /// stdin: the lines typed into the terminal. readLineSync() for a line at a time; or listen to it, as a stream of /// bytes, a line per event. class Stdin extends Stream> { Stdin._(); /// The terminal always shows what's typed, line by line: these are kept but change nothing. bool echoMode = true; bool echoNewlineMode = true; bool lineMode = true; bool get hasTerminal => true; bool get supportsAnsiEscapes => true; Uint8List? _rest; // what readByteSync() hasn't handed out yet of the current line int _at = 0; Stream>? _stream; String? readLineSync({Encoding encoding = systemEncoding, bool retainNewlines = false}) { final rest = _rest; if (rest != null && _at < rest.length) { var s = utf8.decode(rest.sublist(_at), allowMalformed: true); _rest = null; if (!retainNewlines && s.endsWith('\n')) s = s.substring(0, s.length - 1); return s; } return _host.readLine(retainNewlines); } /// The next byte typed (a line's bytes, then its newline, 10); -1 at the end of input. int readByteSync() { var rest = _rest; if (rest == null || _at >= rest.length) { final line = _host.readLine(true); if (line == null) return -1; _rest = rest = utf8.encode(line); _at = 0; if (rest.isEmpty) return -1; } return rest[_at++]; } @override StreamSubscription> listen(void Function(List event)? onData, {Function? onError, void Function()? onDone, bool? cancelOnError}) { return (_stream ??= _lines()).listen(onData, onError: onError, onDone: onDone, cancelOnError: cancelOnError); } // one line per turn of the event loop, so whatever else the program does runs in between, as with a real terminal Stream> _lines() { late final StreamController> c; var queued = false; void pump() { queued = false; if (c.isClosed || c.isPaused || !c.hasListener) return; final line = _host.readLine(true); if (line == null) { c.close(); return; } c.add(utf8.encode(line)); if (!queued) { queued = true; Timer.run(pump); } } void start() { if (!queued) { queued = true; Timer.run(pump); } } c = StreamController>(onListen: start, onResume: start); return c.stream; } } final Stdin stdin = Stdin._(); final Stdout stdout = Stdout._(1); final Stdout stderr = Stdout._(2); class StdioType { final String name; const StdioType._(this.name); static const StdioType terminal = StdioType._('terminal'); static const StdioType pipe = StdioType._('pipe'); static const StdioType file = StdioType._('file'); static const StdioType other = StdioType._('other'); @override String toString() => 'StdioType: $name'; } StdioType stdioType(object) => StdioType.terminal; /// Ends the program now, with this exit code (the terminal shows a code that isn't 0). Never exit(int code) { _host.exit(code); throw StateError('exit($code)'); // not reached: the terminal has stopped the program } /// The code the program ends with when it ends by itself (0 unless set). int get exitCode => _host.exitCode; set exitCode(int code) { _host.exitCode = code; } /// Waits this long (the whole program waits, as in dart:io). void sleep(Duration duration) { final ms = duration.inMilliseconds; if (ms < 0) throw ArgumentError('sleep: duration cannot be negative'); _host.sleep(ms); } int get pid => 1; // ── the project's files ── class FileSystemEntityType { final int _type; const FileSystemEntityType._(this._type); static const FileSystemEntityType file = FileSystemEntityType._(0); static const FileSystemEntityType directory = FileSystemEntityType._(1); static const FileSystemEntityType link = FileSystemEntityType._(2); static const FileSystemEntityType unixDomainSock = FileSystemEntityType._(3); static const FileSystemEntityType pipe = FileSystemEntityType._(4); static const FileSystemEntityType notFound = FileSystemEntityType._(5); @override String toString() => const ['file', 'directory', 'link', 'unixDomainSock', 'pipe', 'notFound'][_type]; } class FileMode { final int _mode; const FileMode._internal(this._mode); static const FileMode read = FileMode._internal(0); static const FileMode write = FileMode._internal(1); static const FileMode append = FileMode._internal(2); static const FileMode writeOnly = FileMode._internal(3); static const FileMode writeOnlyAppend = FileMode._internal(4); bool get _appends => _mode == 2 || _mode == 4; } String _parentOf(String path) { var p = path; while (p.length > 1 && p.endsWith('/')) { p = p.substring(0, p.length - 1); } final i = p.lastIndexOf('/'); if (i < 0) return '.'; if (i == 0) return '/'; return p.substring(0, i); } abstract class FileSystemEntity { String get path; Uri get uri => Uri.file(path); bool get isAbsolute => path.startsWith('/'); FileSystemEntity get absolute; Directory get parent => Directory(_parentOf(path)); bool existsSync(); Future exists() async => existsSync(); void deleteSync({bool recursive = false}); Future delete({bool recursive = false}) async { deleteSync(recursive: recursive); return this; } FileSystemEntity renameSync(String newPath); Future rename(String newPath) async => renameSync(newPath); String resolveSymbolicLinksSync() { if (_host.kind(path) == 0) throw _notFound('Cannot resolve symbolic links', path); return _host.resolve(path); } Future resolveSymbolicLinks() async => resolveSymbolicLinksSync(); static bool isFileSync(String path) => _host.kind(path) == 1; static bool isDirectorySync(String path) => _host.kind(path) == 2; static bool isLinkSync(String path) => false; static Future isFile(String path) async => isFileSync(path); static Future isDirectory(String path) async => isDirectorySync(path); static Future isLink(String path) async => false; static FileSystemEntityType typeSync(String path, {bool followLinks = true}) => switch (_host.kind(path)) { 1 => FileSystemEntityType.file, 2 => FileSystemEntityType.directory, _ => FileSystemEntityType.notFound, }; static Future type(String path, {bool followLinks = true}) async => typeSync(path); static bool identicalSync(String path1, String path2) => _host.resolve(path1) == _host.resolve(path2); static Future identical(String path1, String path2) async => identicalSync(path1, path2); static String parentOf(String path) => _parentOf(path); } class File extends FileSystemEntity { @override final String path; File(this.path); factory File.fromUri(Uri uri) => File(uri.toFilePath()); @override File get absolute => File(_host.resolve(path)); @override bool existsSync() => _host.kind(path) == 1; @override Future exists() async => existsSync(); int lengthSync() { final n = _host.size(path); if (n < 0) throw _notFound('Cannot retrieve length of file', path); return n; } Future length() async => lengthSync(); Uint8List readAsBytesSync() { final b = _host.readFile(path); if (b == null) { if (_host.kind(path) == 2) throw _isDir('Cannot open file', path); throw _notFound('Cannot open file', path); } return b.toDart; } Future readAsBytes() async => readAsBytesSync(); String readAsStringSync({Encoding encoding = utf8}) { final bytes = readAsBytesSync(); try { return encoding.decode(bytes); } on FormatException { throw FileSystemException('Failed to decode data using encoding \'${encoding.name}\'', path); } } Future readAsString({Encoding encoding = utf8}) async => readAsStringSync(encoding: encoding); List readAsLinesSync({Encoding encoding = utf8}) => const LineSplitter().convert(readAsStringSync(encoding: encoding)); Future> readAsLines({Encoding encoding = utf8}) async => readAsLinesSync(encoding: encoding); void writeAsBytesSync(List bytes, {FileMode mode = FileMode.write, bool flush = false}) { if (mode == FileMode.read) throw FileSystemException('Cannot write to a file opened for reading', path); if (_host.kind(path) == 2) throw _isDir('Cannot open file', path); if (!_host.writeFile(path, _bytes(bytes).toJS, mode._appends)) throw _notFound('Cannot open file', path); } Future writeAsBytes(List bytes, {FileMode mode = FileMode.write, bool flush = false}) async { writeAsBytesSync(bytes, mode: mode, flush: flush); return this; } void writeAsStringSync(String contents, {FileMode mode = FileMode.write, Encoding encoding = utf8, bool flush = false}) => writeAsBytesSync(encoding.encode(contents), mode: mode, flush: flush); Future writeAsString(String contents, {FileMode mode = FileMode.write, Encoding encoding = utf8, bool flush = false}) async { writeAsStringSync(contents, mode: mode, encoding: encoding, flush: flush); return this; } void createSync({bool recursive = false, bool exclusive = false}) { final k = _host.kind(path); if (k == 2) throw _isDir('Cannot create file', path); if (k == 1) { if (exclusive) throw PathExistsException(path, const OSError('File exists', 17), 'Cannot create file'); return; } if (recursive) _host.makeDir(_parentOf(path), true); if (!_host.writeFile(path, Uint8List(0).toJS, false)) throw _notFound('Cannot create file', path); } Future create({bool recursive = false, bool exclusive = false}) async { createSync(recursive: recursive, exclusive: exclusive); return this; } @override void deleteSync({bool recursive = false}) { if (_host.kind(path) != 1) throw _notFound('Cannot delete file', path); _host.remove(path, false); } @override Future delete({bool recursive = false}) async { deleteSync(recursive: recursive); return this; } @override File renameSync(String newPath) { if (_host.kind(path) != 1 || !_host.rename(path, newPath)) { throw _notFound("Cannot rename file to '$newPath'", path); } return File(newPath); } @override Future rename(String newPath) async => renameSync(newPath); File copySync(String newPath) { final to = File(newPath); to.writeAsBytesSync(readAsBytesSync()); return to; } Future copy(String newPath) async => copySync(newPath); DateTime lastModifiedSync() { if (!existsSync()) throw _notFound('Cannot retrieve modification time', path); return DateTime.now(); } Future lastModified() async => lastModifiedSync(); /// The file's bytes as a stream (in one piece: the file is already here). Stream> openRead([int? start, int? end]) { return Stream>.fromFuture(Future>(() { final b = readAsBytesSync(); final s = start ?? 0, e = end ?? b.length; return b.sublist(s.clamp(0, b.length), e.clamp(s.clamp(0, b.length), b.length)); })); } /// Writes go to the file as they're made (FileMode.write empties it first). IOSink openWrite({FileMode mode = FileMode.write, Encoding encoding = utf8}) { if (mode == FileMode.read) throw ArgumentError.value(mode, 'mode', 'Must be FileMode.write, FileMode.append, FileMode.writeOnly or FileMode.writeOnlyAppend'); if (!mode._appends) writeAsBytesSync(const []); return _FileSink(this, encoding); } @override String toString() => "File: '$path'"; } class _FileSink implements IOSink { final File _file; @override Encoding encoding; bool _closed = false; _FileSink(this._file, this.encoding); void _put(List bytes) { if (_closed) throw StateError('StreamSink is closed'); _file.writeAsBytesSync(bytes, mode: FileMode.append); } @override void add(List data) => _put(data); @override void write(Object? object) => _put(encoding.encode('$object')); @override void writeln([Object? object = '']) => _put(encoding.encode('$object\n')); @override void writeAll(Iterable objects, [String separator = '']) => _put(encoding.encode(objects.join(separator))); @override void writeCharCode(int charCode) => _put(encoding.encode(String.fromCharCode(charCode))); @override void addError(Object error, [StackTrace? stackTrace]) => Zone.current.handleUncaughtError(error, stackTrace ?? StackTrace.current); @override Future addStream(Stream> stream) => stream.forEach(add); @override Future flush() => Future.value(); @override Future close() { _closed = true; return done; } @override Future get done => Future.value(); } class Directory extends FileSystemEntity { @override final String path; Directory(this.path); factory Directory.fromUri(Uri uri) => Directory(uri.toFilePath()); /// The project's root: relative paths start there. static Directory get current => Directory('/'); static set current(dynamic path) { throw UnsupportedError('A program here stays in the project\'s folder: paths start there.'); } static Directory get systemTemp => Directory('/tmp'); @override Directory get absolute => Directory(_host.resolve(path)); @override bool existsSync() => _host.kind(path) == 2; @override Future exists() async => existsSync(); void createSync({bool recursive = false}) { final k = _host.kind(path); if (k == 2) return; if (k == 1) throw FileSystemException('Creation failed', path, const OSError('File exists', 17)); if (!_host.makeDir(path, recursive)) throw _notFound('Creation failed', path); } Future create({bool recursive = false}) async { createSync(recursive: recursive); return this; } Directory createTempSync([String prefix = '']) { for (var i = 0;; i++) { final d = Directory('${path.endsWith('/') ? path : '$path/'}${prefix}tmp$i'); if (_host.kind(d.path) == 0) { d.createSync(recursive: true); return d; } } } Future createTemp([String prefix = '']) async => createTempSync(prefix); List listSync({bool recursive = false, bool followLinks = true}) { if (_host.kind(path) != 2) throw _notFound('Directory listing failed', path); final base = path.endsWith('/') ? path : '$path/'; final out = []; for (final n in _host.list(path, recursive).toDart) { final name = n.toDart; if (name.endsWith('/')) { out.add(Directory(base + name.substring(0, name.length - 1))); } else { out.add(File(base + name)); } } return out; } Stream list({bool recursive = false, bool followLinks = true}) => Stream>.fromFuture(Future(() => listSync(recursive: recursive))) .expand((l) => l); @override void deleteSync({bool recursive = false}) { if (_host.kind(path) != 2) throw _notFound('Deletion failed', path); if (!_host.remove(path, recursive)) { throw FileSystemException('Deletion failed', path, const OSError('Directory not empty', 39)); } } @override Future delete({bool recursive = false}) async { deleteSync(recursive: recursive); return this; } @override Directory renameSync(String newPath) { if (_host.kind(path) != 2 || !_host.rename(path, newPath)) throw _notFound('Rename failed', path); return Directory(newPath); } @override Future rename(String newPath) async => renameSync(newPath); @override String toString() => "Directory: '$path'"; } // ── the platform ── /// Where the program runs: a browser's terminal on Domainless Code, compiled by dart2js. abstract final class Platform { static int get numberOfProcessors => _host.processors; static String get pathSeparator => '/'; static String get localeName => _host.locale; static String get operatingSystem => 'browser'; static String get operatingSystemVersion => ''; static String get localHostname => 'localhost'; static String get version => '3.13.5 (stable) on "browser_js"'; static Map get environment => const {}; static String get executable => 'dart'; static String get resolvedExecutable => 'dart'; static Uri get script => Uri.file('/${_host.script}'); static List get executableArguments => const []; static String? get packageConfig => null; static String get lineTerminator => '\n'; static bool get isLinux => false; static bool get isMacOS => false; static bool get isWindows => false; static bool get isAndroid => false; static bool get isIOS => false; static bool get isFuchsia => false; }