Skip to content

Commit

Permalink
feat: validate pattern declarations (#298)
Browse files Browse the repository at this point in the history
* feat: validate pattern declarations

* refactor: change import

* chore: add comment

* fix: add missing call to `serializePattern`

* refactor: wrap call to `parsePattern`

* refactor: update error message

* fix: look for `excludedPattern`
  • Loading branch information
eduardoboucas committed Feb 10, 2023
1 parent ed5b8e6 commit d8c44a3
Show file tree
Hide file tree
Showing 5 changed files with 77 additions and 9 deletions.
33 changes: 33 additions & 0 deletions node/declaration.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import regexpAST from 'regexp-tree'

import { FunctionConfig } from './config.js'
import type { DeployConfig } from './deploy_config.js'

Expand Down Expand Up @@ -75,4 +77,35 @@ export const getDeclarationsFromConfig = (
return declarations
}

// Validates and normalizes a pattern so that it's a valid regular expression
// in Go, which is the engine used by our edge nodes.
export const parsePattern = (pattern: string) => {
// Escaping forward slashes with back slashes.
const normalizedPattern = pattern.replace(/\//g, '\\/')
const regex = regexpAST.transform(`/${normalizedPattern}/`, {
Assertion(path) {
// Lookaheads are not supported. If we find one, throw an error.
if (path.node.kind === 'Lookahead') {
throw new Error('Regular expressions with lookaheads are not supported')
}
},

Group(path) {
// Named captured groups in JavaScript use a different syntax than in Go.
// If we find one, convert it to an unnamed capture group, which is valid
// in both engines.
if ('name' in path.node && path.node.name !== undefined) {
path.replace({
...path.node,
name: undefined,
nameRaw: undefined,
})
}
},
})

// Strip leading and forward slashes.
return regex.toString().slice(1, -1)
}

export { Declaration, DeclarationWithPath, DeclarationWithPattern }
17 changes: 17 additions & 0 deletions node/manifest.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -191,3 +191,20 @@ test('Generates a manifest with layers', () => {
expect(manifest2.routes).toEqual(expectedRoutes)
expect(manifest2.layers).toEqual(layers)
})

test('Throws an error if the regular expression contains a negative lookahead', () => {
const functions = [{ name: 'func-1', path: '/path/to/func-1.ts' }]
const declarations = [{ function: 'func-1', pattern: '^/\\w+(?=\\d)$' }]

expect(() => generateManifest({ bundles: [], declarations, functions })).toThrowError(
/^Could not parse path declaration of function 'func-1': Regular expressions with lookaheads are not supported$/,
)
})

test('Converts named capture groups to unnamed capture groups in regular expressions', () => {
const functions = [{ name: 'func-1', path: '/path/to/func-1.ts' }]
const declarations = [{ function: 'func-1', pattern: '^/(?<name>\\w+)$' }]
const manifest = generateManifest({ bundles: [], declarations, functions })

expect(manifest.routes).toEqual([{ function: 'func-1', pattern: '^/(\\w+)$' }])
})
30 changes: 24 additions & 6 deletions node/manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import globToRegExp from 'glob-to-regexp'

import type { Bundle } from './bundle.js'
import { Cache, FunctionConfig } from './config.js'
import type { Declaration } from './declaration.js'
import { Declaration, parsePattern } from './declaration.js'
import { EdgeFunction } from './edge_function.js'
import { Layer } from './layer.js'
import { getPackageVersion } from './package_json.js'
Expand Down Expand Up @@ -47,7 +47,11 @@ interface Route {
pattern: string
}

const serializePattern = (regex: RegExp) => regex.source.replace(/\\\//g, '/')
// JavaScript regular expressions are converted to strings with leading and
// trailing slashes, so any slashes inside the expression itself are escaped
// as `//`. This function deserializes that back into a single slash, which
// is the format we want to use in the manifest.
const serializePattern = (pattern: string) => pattern.replace(/\\\//g, '/')

const sanitizeEdgeFunctionConfig = (config: Record<string, EdgeFunctionConfig>): Record<string, EdgeFunctionConfig> => {
const newConfig: Record<string, EdgeFunctionConfig> = {}
Expand Down Expand Up @@ -79,6 +83,7 @@ const generateManifest = ({
if (excludedPath) {
const paths = Array.isArray(excludedPath) ? excludedPath : [excludedPath]
const excludedPatterns = paths.map(pathToRegularExpression).map(serializePattern)

manifestFunctionConfig[name].excluded_patterns.push(...excludedPatterns)
}
}
Expand All @@ -97,6 +102,7 @@ const generateManifest = ({
pattern: serializePattern(pattern),
}
const excludedPattern = getExcludedRegularExpression(declaration)

if (excludedPattern) {
manifestFunctionConfig[func.name].excluded_patterns.push(serializePattern(excludedPattern))
}
Expand Down Expand Up @@ -134,20 +140,32 @@ const pathToRegularExpression = (path: string) => {
// for both `/foo` and `/foo/`.
const normalizedSource = `^${regularExpression.source}\\/?$`

return new RegExp(normalizedSource)
return normalizedSource
}

const getRegularExpression = (declaration: Declaration) => {
if ('pattern' in declaration) {
return new RegExp(declaration.pattern)
try {
return parsePattern(declaration.pattern)
} catch (error: unknown) {
throw new Error(
`Could not parse path declaration of function '${declaration.function}': ${(error as Error).message}`,
)
}
}

return pathToRegularExpression(declaration.path)
}

const getExcludedRegularExpression = (declaration: Declaration) => {
if ('pattern' in declaration && declaration.excludedPattern) {
return new RegExp(declaration.excludedPattern)
if ('excludedPattern' in declaration && declaration.excludedPattern) {
try {
return parsePattern(declaration.excludedPattern)
} catch (error: unknown) {
throw new Error(
`Could not parse path declaration of function '${declaration.function}': ${(error as Error).message}`,
)
}
}

if ('path' in declaration && declaration.excludedPath) {
Expand Down
5 changes: 2 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@
"p-retry": "^5.1.1",
"p-wait-for": "^4.1.0",
"path-key": "^4.0.0",
"regexp-tree": "^0.1.24",
"semver": "^7.3.5",
"tmp-promise": "^3.0.3",
"uuid": "^9.0.0"
Expand Down

0 comments on commit d8c44a3

Please sign in to comment.