Skip to content

Commit

Permalink
feat(validation): Validate script and module names
Browse files Browse the repository at this point in the history
  • Loading branch information
Blckbrry-Pi committed Mar 7, 2024
1 parent 9950b74 commit 639e5b4
Show file tree
Hide file tree
Showing 7 changed files with 147 additions and 1 deletion.
19 changes: 19 additions & 0 deletions src/cli/commands/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,37 @@ import { GlobalOpts, initProject } from "../common.ts";
import { templateScript } from "../../template/script.ts";
import { templateModule } from "../../template/module.ts";

import { validateIdentifier } from "../../types/identifiers/mod.ts";
import { IdentType } from "../../types/identifiers/defs.ts";

const handleNames = (name: string, type: string) => {
const moduleNameError = validateIdentifier(
name,
IdentType.ModuleScripts,
);
if (moduleNameError) {
console.error(moduleNameError.toString(type));
Deno.exit(1);
}
};

export const createCommand = new Command<GlobalOpts>();

createCommand.action(() => createCommand.showHelp());

createCommand.command("module").arguments("<module>").action(
async (opts, module) => {
handleNames(module, "module");

await templateModule(await initProject(opts), module);
},
);

createCommand.command("script").arguments("<module> <script>").action(
async (opts, module, script) => {
handleNames(module, "module");
handleNames(script, "script");

await templateScript(await initProject(opts), module, script);
},
);
9 changes: 8 additions & 1 deletion src/project/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { ModuleConfig } from "../config/module.ts";
import { Script } from "./script.ts";
import { Project } from "./project.ts";
import { Registry } from "./registry.ts";
import { validateIdentifier } from "../types/identifiers/mod.ts";
import { IdentType } from "../types/identifiers/defs.ts";

export interface Module {
path: string;
Expand Down Expand Up @@ -34,8 +36,13 @@ export async function loadModule(
);

// Read scripts
const scripts = new Map();
const scripts = new Map<string, Script>();
for (const scriptName in config.scripts) {
const scriptNameIssue = validateIdentifier(scriptName, IdentType.ModuleScripts);
if (scriptNameIssue) {
throw new Error(scriptNameIssue.toString("script"));
}

// Load script
const scriptPath = join(
scriptsPath,
Expand Down
7 changes: 7 additions & 0 deletions src/project/project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { ProjectConfig } from "../config/project.ts";
import { loadModule, Module } from "./module.ts";
import { loadRegistry, Registry } from "./registry.ts";
import { ProjectModuleConfig } from "../config/project.ts";
import { validateIdentifier } from "../types/identifiers/mod.ts";
import { IdentType } from "../types/identifiers/defs.ts";

export interface Project {
path: string;
Expand Down Expand Up @@ -104,6 +106,11 @@ async function fetchAndResolveModule(
registries: Map<string, Registry>,
moduleName: string,
): Promise<{ path: string; registry: Registry }> {
const moduleNameIssue = validateIdentifier(moduleName, IdentType.ModuleScripts);
if (moduleNameIssue) {
throw new Error(moduleNameIssue.toString("module"));
}

// Lookup module
const module = projectConfig.modules[moduleName];
if (!module) throw new Error(`Module not found ${moduleName}`);
Expand Down
23 changes: 23 additions & 0 deletions src/types/identifiers/defs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
export enum IdentType {
ModuleScripts = "snake_case",
Errors = "SCREAMING_SNAKE_CASE",
}

/**
* A record of regular expressions for each identifier type.
*
* `regexes[identType].test(ident)` will return whether `ident` is a valid
* `identType`.
*/
export const regexes: Record<IdentType, RegExp> = {
[IdentType.ModuleScripts]: /^[a-z]+(_[a-z0-9]+)*$/,
[IdentType.Errors]: /^[A-Z]+(_[A-Z0-9]+)*$/,
};

/**
* A regular expression that matches a string of only printable ASCII
* characters.
*
* Printable ASCII characters range from 0x20 to 0x7e, or space to tilde.
*/
export const printableAsciiRegex = /^[\x20-\x7E]+$/;
1 change: 1 addition & 0 deletions src/types/identifiers/deps.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { brightRed, bold } from "https://deno.land/std@0.208.0/fmt/colors.ts";
67 changes: 67 additions & 0 deletions src/types/identifiers/errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { printableAsciiRegex } from "./defs.ts";
import { brightRed, bold } from "./deps.ts";

export class IdentError extends Error {
public constructor(private issue: string, private identifier: string) {
super(`"${debugIdentifier(identifier)}": ${issue}`);

this.name = "IdentifierError";
}

public toString(identifierType = "identifier") {
const highlightedIdentifier = brightRed(bold(debugIdentifier(this.identifier)));
return `invalid ${identifierType} ${highlightedIdentifier}: ${this.issue}`;

}
}

/**
* Converts an identifier into a string that can be printed to the console.
*
* This function should be used when working with unverified
* module/script/error names, as it is guaranteed it won't mess up the
* terminal or obscure logging with control characters.
*
* Examples:
* ```ts
* IdentError.debugIdentifier("hello") // "hello"
* IdentError.debugIdentifier("hello\x20world") // "hello world"
* IdentError.debugIdentifier("hello\x7fworld") // "hello\7fworld"
* IdentError.debugIdentifier("hello_wòrld") // "hello_w\xf2rld"
*
* @param ident The identifier to be converted into a safe debugged string
* @returns A string containing all string-safe printable ascii characters, with
* non-printable characters escaped as hex or unicode.
*/
function debugIdentifier(ident: string): string {
const lenLimited = ident.length > 32 ? ident.slice(0, 32) + "..." : ident;
const characters = lenLimited.split("");

let output = "";
for (const char of characters) {
// If the character is printable without any special meaning, just
// add it.
if (printableAsciiRegex.test(char) && char !== "\\" && char !== '"') {
output += char;
continue;
}

// Escape `\` and `"` characters to reduce ambiguity
if (char === "\\") {
output += "\\\\";
continue;
} else if (char === '"') {
output += '\\"';
continue;
}

// Escape non-printable characters
const charCode = char.charCodeAt(0);
if (charCode > 255) { // 16-bit unicode escape
output += `\\u${charCode.toString(16).padStart(4, "0")}`;
} else { // 8-bit ascii/extended-ascii escape
output += `\\x${charCode.toString(16).padStart(2, "0")}`;
}
}
return `"${output}"`;
}
22 changes: 22 additions & 0 deletions src/types/identifiers/mod.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { IdentType, printableAsciiRegex, regexes } from "./defs.ts";
import { IdentError } from "./errors.ts";

export function validateIdentifier(
ident: string,
identType: IdentType,
): IdentError | null {
if (ident.length < 1 || ident.length > 32) {
return new IdentError("must be between 1 and 32 characters", ident);
}

if (!printableAsciiRegex.test(ident)) {
return new IdentError("must contain only printable ASCII characters", ident);
}

const regex = regexes[identType];
if (!regex.test(ident)) {
return new IdentError(`must be ${identType} (match the pattern ${regex})`, ident);
}

return null;
};

0 comments on commit 639e5b4

Please sign in to comment.