diff --git a/scripts/prebuild.js b/scripts/prebuild.js index 988a6f8..c0f401d 100644 --- a/scripts/prebuild.js +++ b/scripts/prebuild.js @@ -22,7 +22,7 @@ async function bundleAssets() { STYLES: await readPkgFile('assets/styles.css'), }; const minify = (input = '') => input.replace(/^\s+/gm, '').trim(); - const escape = (input = '') => input.replace(/\`/g, '\\`'); + const escape = (input = '') => input.replace(/`/g, '\\`'); const out = Object.entries(assets).map(([key, contents]) => { return `export const ${key} = \`${escape(minify(contents))}\`;`; diff --git a/src/args.ts b/src/args.ts index 2bc2468..e18905c 100644 --- a/src/args.ts +++ b/src/args.ts @@ -52,8 +52,8 @@ export class CLIArgs { */ #cleanArgs(args: string[]): string[] { const clean: string[] = []; - const shortEqual = /^\-[a-z]\=/i; - const shortCombo = /^\-[a-z0-9]{2,}/i; + const shortEqual = /^-[a-z]=/i; + const shortCombo = /^-[a-z\d]{2,}/i; for (const arg of args) { if (arg.startsWith('-')) { if (shortEqual.test(arg)) { diff --git a/src/content-type.ts b/src/content-type.ts index 00210bb..40fe2de 100644 --- a/src/content-type.ts +++ b/src/content-type.ts @@ -207,7 +207,7 @@ async function typeForFile(handle: FileHandle, charset?: string): Promise { try { const stats = await lstat(filePath); return statsKind(stats); - } catch (err) { + } catch { return null; } } diff --git a/src/options.ts b/src/options.ts index f286ee1..2e538d1 100644 --- a/src/options.ts +++ b/src/options.ts @@ -1,3 +1,4 @@ +import { isIP } from 'node:net'; import { isAbsolute, resolve } from 'node:path'; import { DEFAULT_OPTIONS, PORTS_CONFIG } from './constants.ts'; @@ -140,11 +141,11 @@ function isStringArray(input: unknown): input is string[] { export function isValidExt(input: string): boolean { if (typeof input !== 'string' || !input) return false; - return /^\.[\w\-]+(\.[\w\-]+){0,4}$/.test(input); + return /^\.[\w-]+(\.[\w-]+){0,4}$/.test(input); } export function isValidHeader(name: string): boolean { - return typeof name === 'string' && /^[a-z\d\-\_]+$/i.test(name); + return typeof name === 'string' && /^[a-z\d-_]+$/i.test(name); } /** @type {(value: any) => value is HttpHeaderRule} */ @@ -171,15 +172,14 @@ export function isValidHeaderRule(value: unknown): value is HttpHeaderRule { // as a usability nicety to catch obvious errors export function isValidHost(input: string): boolean { if (typeof input !== 'string' || !input.length) return false; - const domainLike = /^([a-z\d\-]+)(\.[a-z\d\-]+)*$/i; - const ipLike = /^([\d\.]+|[a-f\d\:]+)$/i; - return domainLike.test(input) || ipLike.test(input); + if (isIP(input) >= 4) return true; + return /^([a-z\d-]+)(\.[a-z\d-]+)*$/i.test(input); } export function isValidPattern(value: string): boolean { if (typeof value !== 'string') return false; if (value.length < (value.startsWith('!') ? 2 : 1)) return false; - return !/[\\\/\:]/.test(value); + return !/[/\\:]/.test(value); } export function isValidPort(num: number): boolean { diff --git a/src/utils.ts b/src/utils.ts index 1365eb0..670dc6a 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -37,7 +37,7 @@ export class PathMatcher { if (input.includes('/') || input.includes('\\')) { return null; } else if (input.includes('*')) { - const toEscape = /([\[\]\(\)\|\^\$\.\+\?])/g; + const toEscape = /([\]|[)(^$.+?])/g; const re = input.replace(toEscape, '\\$1').replace(/\*/g, '[^/]*'); return new RegExp(re); } @@ -175,8 +175,8 @@ export function trimSlash( input: string = '', config: { start?: boolean; end?: boolean } = { start: true, end: true }, ) { - if (config.start === true) input = input.replace(/^[\/\\]/, ''); - if (config.end === true) input = input.replace(/[\/\\]$/, ''); + if (config.start === true) input = input.replace(/^[/\\]/, ''); + if (config.end === true) input = input.replace(/[/\\]$/, ''); return input; } diff --git a/test/logger.test.ts b/test/logger.test.ts index 2122fc3..dfbc189 100644 --- a/test/logger.test.ts +++ b/test/logger.test.ts @@ -127,7 +127,7 @@ Info 2 const { err, logger } = getLogger(); await logger.error(new Error('Whoops')); expect(err.contents).toMatch(`Error: Whoops`); - expect(err.contents).toMatch(/[\\\/]test[\\\/]logger\.test\.ts:\d+:\d+/); + expect(err.contents).toMatch(/[/\\]test[/\\]logger\.test\.ts:\d+:\d+/); }); }); diff --git a/test/options.test.ts b/test/options.test.ts index 73c08c7..59fa605 100644 --- a/test/options.test.ts +++ b/test/options.test.ts @@ -123,6 +123,8 @@ suite('isValidHost', () => { valid('127.0.0.1'); valid('192.168.0.99'); valid('255.255.255.255'); + // bug in node's net.isIP, or actually valid? + valid('9999.9999.9999.9999.9999'); }); test('accepts ipv6 addresses', () => { @@ -137,21 +139,16 @@ suite('isValidHost', () => { invalid(false); invalid(null); invalid(''); + invalid('.'); + invalid(':'); invalid('____'); invalid('with spaces'); invalid('piƱa-colada.dev'); invalid('1.1::1.1'); invalid('9999.9999:9999::::9999.9999'); invalid('2001:0zb9::7334'); - }); - - // not ideal, but don't want to make it stricter and possibly buggier - test('accepts bad strings that only use ip characters', () => { - valid(':'); - valid('.'); - valid('123...4567890...'); - valid('9999.9999.9999.9999.9999'); - valid('1::::1::::1::::1'); + invalid('123...4567890...'); + invalid('1::::1::::1::::1'); }); }); @@ -303,8 +300,9 @@ suite('OptionsValidator', () => { suite('serverOptions', () => { test('returns default options with empty input', () => { const onError = errorList(); - const { root, ...result } = serverOptions({ root: cwd() }, onError); - expect(result).toEqual(DEFAULT_OPTIONS); + const root = cwd(); + const result = serverOptions({ root }, onError); + expect(result).toEqual({ root, ...DEFAULT_OPTIONS }); expect(onError.list).toEqual([]); });