diff --git a/src/platform/PosixError.js b/src/platform/PosixError.js
index 98e3ce0..0fc136a 100644
--- a/src/platform/PosixError.js
+++ b/src/platform/PosixError.js
@@ -36,74 +36,108 @@ export const ErrorCodes = {
ETIMEDOUT: Symbol.for('ETIMEDOUT'),
};
+// Codes taken from `errno` on Linux.
+export const ErrorMetadata = new Map([
+ [ErrorCodes.EPERM, { code: 1, description: 'Operation not permitted' }],
+ [ErrorCodes.ENOENT, { code: 2, description: 'File or directory not found' }],
+ [ErrorCodes.EIO, { code: 5, description: 'IO error' }],
+ [ErrorCodes.EACCES, { code: 13, description: 'Permission denied' }],
+ [ErrorCodes.EEXIST, { code: 17, description: 'File already exists' }],
+ [ErrorCodes.ENOTDIR, { code: 20, description: 'Is not a directory' }],
+ [ErrorCodes.EISDIR, { code: 21, description: 'Is a directory' }],
+ [ErrorCodes.EINVAL, { code: 22, description: 'Argument invalid' }],
+ [ErrorCodes.EMFILE, { code: 24, description: 'Too many open files' }],
+ [ErrorCodes.EFBIG, { code: 27, description: 'File too big' }],
+ [ErrorCodes.ENOSPC, { code: 28, description: 'Device out of space' }],
+ [ErrorCodes.EPIPE, { code: 32, description: 'Pipe broken' }],
+ [ErrorCodes.ENOTEMPTY, { code: 39, description: 'Directory is not empty' }],
+ [ErrorCodes.EADDRINUSE, { code: 98, description: 'Address already in use' }],
+ [ErrorCodes.ECONNRESET, { code: 104, description: 'Connection reset'}],
+ [ErrorCodes.ETIMEDOUT, { code: 110, description: 'Connection timed out' }],
+ [ErrorCodes.ECONNREFUSED, { code: 111, description: 'Connection refused' }],
+]);
+
+export const errorFromIntegerCode = (code) => {
+ for (const [errorCode, metadata] of ErrorMetadata) {
+ if (metadata.code === code) {
+ return errorCode;
+ }
+ }
+ return undefined;
+};
+
export class PosixError extends Error {
// posixErrorCode can be either a string, or one of the ErrorCodes above.
+ // If message is undefined, a default message will be used.
constructor(posixErrorCode, message) {
- super(message);
+ let posixCode;
if (typeof posixErrorCode === 'symbol') {
if (ErrorCodes[Symbol.keyFor(posixErrorCode)] !== posixErrorCode) {
throw new Error(`Unrecognized POSIX error code: '${posixErrorCode}'`);
}
- this.posixCode = posixErrorCode;
+ posixCode = posixErrorCode;
} else {
const code = ErrorCodes[posixErrorCode];
if (!code) throw new Error(`Unrecognized POSIX error code: '${posixErrorCode}'`);
- this.posixCode = code;
+ posixCode = code;
}
+
+ super(message ?? ErrorMetadata.get(posixCode).description);
+ this.posixCode = posixCode;
}
//
// Helpers for constructing a PosixError when you don't already have an error message.
//
- static AccessNotPermitted(path) {
- return new PosixError(ErrorCodes.EACCES, `Access not permitted to: '${path}'`);
+ static AccessNotPermitted({ message, path } = {}) {
+ return new PosixError(ErrorCodes.EACCES, message ?? (path ? `Access not permitted to: '${path}'` : undefined));
}
- static AddressInUse() {
- return new PosixError(ErrorCodes.EADDRINUSE, `Address already in use`);
+ static AddressInUse({ message, address } = {}) {
+ return new PosixError(ErrorCodes.EADDRINUSE, message ?? (address ? `Address '${address}' in use` : undefined));
}
- static ConnectionRefused() {
- return new PosixError(ErrorCodes.ECONNREFUSED, `Connection refused`);
+ static ConnectionRefused({ message } = {}) {
+ return new PosixError(ErrorCodes.ECONNREFUSED, message);
}
- static ConnectionReset() {
- return new PosixError(ErrorCodes.ECONNRESET, `Connection reset`);
+ static ConnectionReset({ message } = {}) {
+ return new PosixError(ErrorCodes.ECONNRESET, message);
}
- static PathAlreadyExists(path) {
- return new PosixError(ErrorCodes.EEXIST, `Path already exists: '${path}'`);
+ static PathAlreadyExists({ message, path } = {}) {
+ return new PosixError(ErrorCodes.EEXIST, message ?? (path ? `Path already exists: '${path}'` : undefined));
}
- static FileTooLarge() {
- return new PosixError(ErrorCodes.EFBIG, `File too large`);
+ static FileTooLarge({ message } = {}) {
+ return new PosixError(ErrorCodes.EFBIG, message);
}
- static InvalidArgument(message) {
+ static InvalidArgument({ message } = {}) {
return new PosixError(ErrorCodes.EINVAL, message);
}
- static IO() {
- return new PosixError(ErrorCodes.EIO, `IO error`);
+ static IO({ message } = {}) {
+ return new PosixError(ErrorCodes.EIO, message);
}
- static IsDirectory(path) {
- return new PosixError(ErrorCodes.EISDIR, `Path is directory: '${path}'`);
+ static IsDirectory({ message, path } = {}) {
+ return new PosixError(ErrorCodes.EISDIR, message ?? (path ? `Path is directory: '${path}'` : undefined));
}
- static TooManyOpenFiles() {
- return new PosixError(ErrorCodes.EMFILE, `Too many open files`);
+ static TooManyOpenFiles({ message } = {}) {
+ return new PosixError(ErrorCodes.EMFILE, message);
}
- static DoesNotExist(path) {
- return new PosixError(ErrorCodes.ENOENT, `Path not found: '${path}'`);
+ static DoesNotExist({ message, path } = {}) {
+ return new PosixError(ErrorCodes.ENOENT, message ?? (path ? `Path not found: '${path}'` : undefined));
}
- static NotEnoughSpace() {
- return new PosixError(ErrorCodes.ENOSPC, `Not enough space available`);
+ static NotEnoughSpace({ message } = {}) {
+ return new PosixError(ErrorCodes.ENOSPC, message);
}
- static IsNotDirectory(path) {
- return new PosixError(ErrorCodes.ENOTDIR, `Path is not a directory: '${path}'`);
+ static IsNotDirectory({ message, path } = {}) {
+ return new PosixError(ErrorCodes.ENOTDIR, message ?? (path ? `Path is not a directory: '${path}'` : undefined));
}
- static DirectoryIsNotEmpty(path) {
- return new PosixError(ErrorCodes.ENOTEMPTY, `Directory is not empty: '${path}'`);
+ static DirectoryIsNotEmpty({ message, path } = {}) {
+ return new PosixError(ErrorCodes.ENOTEMPTY, message ?? (path ?`Directory is not empty: '${path}'` : undefined));
}
- static OperationNotPermitted() {
- return new PosixError(ErrorCodes.EPERM, 'Operation not permitted');
+ static OperationNotPermitted({ message } = {}) {
+ return new PosixError(ErrorCodes.EPERM, message);
}
- static BrokenPipe() {
- return new PosixError(ErrorCodes.EPIPE, 'Broken pipe');
+ static BrokenPipe({ message } = {}) {
+ return new PosixError(ErrorCodes.EPIPE, message);
}
- static TimedOut() {
- return new PosixError(ErrorCodes.ETIMEDOUT, 'Connection timed out');
+ static TimedOut({ message } = {}) {
+ return new PosixError(ErrorCodes.ETIMEDOUT, message);
}
}
diff --git a/src/platform/node/filesystem.js b/src/platform/node/filesystem.js
index 90ba5d6..85b9e84 100644
--- a/src/platform/node/filesystem.js
+++ b/src/platform/node/filesystem.js
@@ -157,7 +157,7 @@ export const CreateFilesystemProvider = () => {
const stat = await fs.promises.stat(path);
if ( stat.isDirectory() && ! recursive ) {
- throw PosixError.IsDirectory(path);
+ throw PosixError.IsDirectory({ path });
}
return await fs.promises.rm(path, { recursive });
@@ -166,7 +166,7 @@ export const CreateFilesystemProvider = () => {
const stat = await fs.promises.stat(path);
if ( !stat.isDirectory() ) {
- throw PosixError.IsNotDirectory(path);
+ throw PosixError.IsNotDirectory({ path });
}
return await fs.promises.rmdir(path);
diff --git a/src/platform/puter/filesystem.js b/src/platform/puter/filesystem.js
index 6cc93c0..ca60e2f 100644
--- a/src/platform/puter/filesystem.js
+++ b/src/platform/puter/filesystem.js
@@ -157,7 +157,7 @@ export const CreateFilesystemProvider = ({
const stat = await puterSDK.fs.stat(path);
if ( stat.is_dir && ! recursive ) {
- throw PosixError.IsDirectory(path);
+ throw PosixError.IsDirectory({ path });
}
return await puterSDK.fs.delete(path, { recursive });
@@ -168,7 +168,7 @@ export const CreateFilesystemProvider = ({
const stat = await puterSDK.fs.stat(path);
if ( ! stat.is_dir ) {
- throw PosixError.IsNotDirectory(path);
+ throw PosixError.IsNotDirectory({ path });
}
return await puterSDK.fs.delete(path, { recursive: false });
diff --git a/src/puter-shell/coreutils/__exports__.js b/src/puter-shell/coreutils/__exports__.js
index 1ac42ec..0b0a95b 100644
--- a/src/puter-shell/coreutils/__exports__.js
+++ b/src/puter-shell/coreutils/__exports__.js
@@ -28,6 +28,7 @@ import module_dcall from './dcall.js'
import module_dirname from './dirname.js'
import module_echo from './echo.js'
import module_env from './env.js'
+import module_errno from './errno.js'
import module_false from './false.js'
import module_grep from './grep.js'
import module_head from './head.js'
@@ -67,6 +68,7 @@ export default {
"dirname": module_dirname,
"echo": module_echo,
"env": module_env,
+ "errno": module_errno,
"false": module_false,
"grep": module_grep,
"head": module_head,
diff --git a/src/puter-shell/coreutils/errno.js b/src/puter-shell/coreutils/errno.js
new file mode 100644
index 0000000..dd36d8d
--- /dev/null
+++ b/src/puter-shell/coreutils/errno.js
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2024 Puter Technologies Inc.
+ *
+ * This file is part of Phoenix Shell.
+ *
+ * Phoenix Shell is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+import { ErrorCodes, ErrorMetadata, errorFromIntegerCode } from '../../platform/PosixError.js';
+import { Exit } from './coreutil_lib/exit.js';
+
+const maxErrorNameLength = Object.keys(ErrorCodes)
+ .reduce((longest, name) => Math.max(longest, name.length), 0);
+const maxNumberLength = 3;
+
+async function printSingleErrno(errorCode, out) {
+ const metadata = ErrorMetadata.get(errorCode);
+ const paddedName = errorCode.description + ' '.repeat(maxErrorNameLength - errorCode.description.length);
+ const code = metadata.code.toString();
+ const paddedCode = ' '.repeat(maxNumberLength - code.length) + code;
+ await out.write(`${paddedName} ${paddedCode} ${metadata.description}\n`);
+}
+
+export default {
+ name: 'errno',
+ usage: 'errno [OPTIONS] [NAME-OR-CODE...]',
+ description: 'Look up and describe errno codes.',
+ args: {
+ $: 'simple-parser',
+ allowPositionals: true,
+ options: {
+ list: {
+ description: 'List all errno values',
+ type: 'boolean',
+ short: 'l'
+ },
+ search: {
+ description: 'Search for errors whose descriptions contain NAME-OR-CODEs, case-insensitively',
+ type: 'boolean',
+ short: 's'
+ }
+ }
+ },
+ execute: async ctx => {
+ const { err, out } = ctx.externs;
+ const { positionals, values } = ctx.locals;
+
+ if (values.search) {
+ for (const [errorCode, metadata] of ErrorMetadata) {
+ const description = metadata.description.toLowerCase();
+ let matches = true;
+ for (const nameOrCode of positionals) {
+ if (! description.includes(nameOrCode.toLowerCase())) {
+ matches = false;
+ break;
+ }
+ }
+ if (matches) {
+ await printSingleErrno(errorCode, out);
+ }
+ }
+ return;
+ }
+
+ if (values.list) {
+ for (const errorCode of ErrorMetadata.keys()) {
+ await printSingleErrno(errorCode, out);
+ }
+ return;
+ }
+
+ let failedToMatchSomething = false;
+ const fail = async (nameOrCode) => {
+ await err.write(`ERROR: Not understood: ${nameOrCode}\n`);
+ failedToMatchSomething = true;
+ };
+
+ for (const nameOrCode of positionals) {
+ let errorCode = ErrorCodes[nameOrCode.toUpperCase()];
+ if (errorCode) {
+ await printSingleErrno(errorCode, out);
+ continue;
+ }
+
+ const code = Number.parseInt(nameOrCode);
+ if (!isFinite(code)) {
+ await fail(nameOrCode);
+ continue;
+ }
+ errorCode = errorFromIntegerCode(code);
+ if (errorCode) {
+ await printSingleErrno(errorCode, out);
+ continue;
+ }
+
+ await fail(nameOrCode);
+ }
+
+ if (failedToMatchSomething) {
+ throw new Exit(1);
+ }
+ }
+};
diff --git a/test/coreutils.test.js b/test/coreutils.test.js
index db8c6e4..3d98f53 100644
--- a/test/coreutils.test.js
+++ b/test/coreutils.test.js
@@ -20,6 +20,7 @@ import { runBasenameTests } from "./coreutils/basename.js";
import { runDirnameTests } from "./coreutils/dirname.js";
import { runEchoTests } from "./coreutils/echo.js";
import { runEnvTests } from "./coreutils/env.js";
+import { runErrnoTests } from './coreutils/errno.js';
import { runFalseTests } from "./coreutils/false.js";
import { runHeadTests } from "./coreutils/head.js";
import { runPrintfTests } from './coreutils/printf.js';
@@ -34,6 +35,7 @@ describe('coreutils', function () {
runDirnameTests();
runEchoTests();
runEnvTests();
+ runErrnoTests();
runFalseTests();
runHeadTests();
runPrintfTests();
diff --git a/test/coreutils/errno.js b/test/coreutils/errno.js
new file mode 100644
index 0000000..fbb63b5
--- /dev/null
+++ b/test/coreutils/errno.js
@@ -0,0 +1,144 @@
+/*
+ * Copyright (C) 2024 Puter Technologies Inc.
+ *
+ * This file is part of Phoenix Shell.
+ *
+ * Phoenix Shell is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+import assert from 'assert';
+import { MakeTestContext } from './harness.js'
+import builtins from '../../src/puter-shell/coreutils/__exports__.js';
+import { ErrorCodes, ErrorMetadata } from '../../src/platform/PosixError.js';
+
+export const runErrnoTests = () => {
+ describe('errno', function () {
+
+ const testCases = [
+ {
+ description: 'exits normally if nothing is passed in',
+ input: [ ],
+ values: {},
+ expectedStdout: '',
+ expectedStderr: '',
+ expectedFail: false,
+ },
+ {
+ description: 'can search by number',
+ input: [ ErrorMetadata.get(ErrorCodes.EFBIG).code.toString() ],
+ values: {},
+ expectedStdout: 'EFBIG 27 File too big\n',
+ expectedStderr: '',
+ expectedFail: false,
+ },
+ {
+ description: 'can search by number',
+ input: [ ErrorCodes.EIO.description ],
+ values: {},
+ expectedStdout: 'EIO 5 IO error\n',
+ expectedStderr: '',
+ expectedFail: false,
+ },
+ {
+ description: 'prints an error message and returns a code > 0 if an error is not found',
+ input: [ 'NOT-A-REAL-ERROR' ],
+ values: {},
+ expectedStdout: '',
+ expectedStderr: 'ERROR: Not understood: NOT-A-REAL-ERROR\n',
+ expectedFail: true,
+ },
+ {
+ description: 'accepts multiple arguments and prints each',
+ input: [ ErrorMetadata.get(ErrorCodes.ENOENT).code.toString(), 'NOT-A-REAL-ERROR', ErrorCodes.EPIPE.description ],
+ values: {},
+ expectedStdout:
+ 'ENOENT 2 File or directory not found\n' +
+ 'EPIPE 32 Pipe broken\n',
+ expectedStderr: 'ERROR: Not understood: NOT-A-REAL-ERROR\n',
+ expectedFail: true,
+ },
+ {
+ description: 'searches descriptions if --search is provided',
+ input: [ 'directory' ],
+ values: { search: true },
+ expectedStdout:
+ 'ENOENT 2 File or directory not found\n' +
+ 'ENOTDIR 20 Is not a directory\n' +
+ 'EISDIR 21 Is a directory\n' +
+ 'ENOTEMPTY 39 Directory is not empty\n',
+ expectedStderr: '',
+ expectedFail: false,
+ },
+ {
+ description: 'lists all errors if --list is provided, ignoring parameters',
+ input: [ 'directory' ],
+ values: { list: true },
+ expectedStdout:
+ 'EPERM 1 Operation not permitted\n' +
+ 'ENOENT 2 File or directory not found\n' +
+ 'EIO 5 IO error\n' +
+ 'EACCES 13 Permission denied\n' +
+ 'EEXIST 17 File already exists\n' +
+ 'ENOTDIR 20 Is not a directory\n' +
+ 'EISDIR 21 Is a directory\n' +
+ 'EINVAL 22 Argument invalid\n' +
+ 'EMFILE 24 Too many open files\n' +
+ 'EFBIG 27 File too big\n' +
+ 'ENOSPC 28 Device out of space\n' +
+ 'EPIPE 32 Pipe broken\n' +
+ 'ENOTEMPTY 39 Directory is not empty\n' +
+ 'EADDRINUSE 98 Address already in use\n' +
+ 'ECONNRESET 104 Connection reset\n' +
+ 'ETIMEDOUT 110 Connection timed out\n' +
+ 'ECONNREFUSED 111 Connection refused\n',
+ expectedStderr: '',
+ expectedFail: false,
+ },
+ {
+ description: '--search overrides --list',
+ input: [ 'directory' ],
+ values: { list: true, search: true },
+ expectedStdout:
+ 'ENOENT 2 File or directory not found\n' +
+ 'ENOTDIR 20 Is not a directory\n' +
+ 'EISDIR 21 Is a directory\n' +
+ 'ENOTEMPTY 39 Directory is not empty\n',
+ expectedStderr: '',
+ expectedFail: false,
+ },
+ ];
+
+ for (const { description, input, values, expectedStdout, expectedStderr, expectedFail } of testCases) {
+ it(description, async () => {
+ let ctx = MakeTestContext(builtins.errno, { positionals: input, values });
+ let hadError = false;
+ try {
+ const result = await builtins.errno.execute(ctx);
+ if (!expectedFail) {
+ assert.equal(result, undefined, 'should exit successfully, returning nothing');
+ }
+ } catch (e) {
+ hadError = true;
+ if (!expectedFail) {
+ assert.fail(e);
+ }
+ }
+ if (expectedFail && !hadError) {
+ assert.fail('should have returned an error code');
+ }
+ assert.equal(ctx.externs.out.output, expectedStdout, 'wrong output written to stdout');
+ assert.equal(ctx.externs.err.output, expectedStderr, 'wrong output written to stderr');
+ });
+ }
+ });
+}
\ No newline at end of file