Skip to content
This repository has been archived by the owner on Apr 13, 2024. It is now read-only.

Commit

Permalink
Merge pull request #59 from AtkinsSJ/errno-builtin
Browse files Browse the repository at this point in the history
Add `errno` utility, which prints out error codes
  • Loading branch information
KernelDeimos authored Mar 12, 2024
2 parents 4076f7f + aa05200 commit dd88f33
Show file tree
Hide file tree
Showing 7 changed files with 335 additions and 40 deletions.
106 changes: 70 additions & 36 deletions src/platform/PosixError.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
4 changes: 2 additions & 2 deletions src/platform/node/filesystem.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
Expand All @@ -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);
Expand Down
4 changes: 2 additions & 2 deletions src/platform/puter/filesystem.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
Expand All @@ -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 });
Expand Down
2 changes: 2 additions & 0 deletions src/puter-shell/coreutils/__exports__.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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,
Expand Down
113 changes: 113 additions & 0 deletions src/puter-shell/coreutils/errno.js
Original file line number Diff line number Diff line change
@@ -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 <https://www.gnu.org/licenses/>.
*/
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);
}
}
};
2 changes: 2 additions & 0 deletions test/coreutils.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -34,6 +35,7 @@ describe('coreutils', function () {
runDirnameTests();
runEchoTests();
runEnvTests();
runErrnoTests();
runFalseTests();
runHeadTests();
runPrintfTests();
Expand Down
Loading

0 comments on commit dd88f33

Please sign in to comment.