Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/dockerignore-ascii-digit-class.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@e2b/dockerfile-utils-python": patch
---

Keep bracket class escapes in Dockerignore patterns ASCII-only, matching Moby and the JavaScript utility. Non-ASCII decimal digits are no longer excluded by a pattern such as `[\d]`, and non-ASCII spaces by a pattern such as `[\s]`.
5 changes: 5 additions & 0 deletions .changeset/dockerignore-ascii-whitespace-class.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@e2b/dockerfile-utils": patch
---

Keep bracket whitespace classes in Dockerignore patterns ASCII-only, matching Moby and the Python utility. Non-ASCII spaces such as U+00A0 are no longer matched by a pattern such as `[\s]`.
19 changes: 17 additions & 2 deletions packages/dockerfile-utils-js/src/dockerignore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,23 @@ function escapeRegex(ch: string): string {
}

// Escapes that keep their regex meaning in a bracket expression, like in Python
const CLASS_ESCAPES = new Set('dDsSwWfnrtv')
const CLASS_ESCAPES = new Set('dDwWfnrtv')
// ASCII-only whitespace like Moby, as JS `\s` also matches Unicode spaces
const CLASS_WHITESPACE_ESCAPES: Record<string, string> = {
s: '\\t-\\r ',
S: '\\u{0}-\\u{8}\\u{e}-\\u{1f}\\u{21}-\\u{10ffff}',
}
const CLASS_CONTROL_ESCAPES: Record<string, string> = { a: '\\x07', b: '\\x08' }
const CLASS_HEX_ESCAPE_LENGTHS: Record<string, number> = { x: 2, u: 4, U: 8 }
// Characters that must stay escaped in a bracket expression with the `u` flag
const CLASS_SYNTAX_CHARS = new Set('^$\\.*+?()[]{}|/-')

// Make a bracket expression body valid with the regex `u` flag
function translateClass(body: string, backslashIsEscape: boolean): string {
function translateClass(
body: string,
backslashIsEscape: boolean,
unicodeWhitespace = false
): string {
let cls = ''
let i = 0
while (i < body.length) {
Expand All @@ -45,6 +54,8 @@ function translateClass(body: string, backslashIsEscape: boolean): string {
const next = body[i++]
if (CLASS_ESCAPES.has(next)) {
cls += '\\' + next
} else if (next in CLASS_WHITESPACE_ESCAPES) {
cls += unicodeWhitespace ? '\\' + next : CLASS_WHITESPACE_ESCAPES[next]
} else if (next in CLASS_CONTROL_ESCAPES) {
cls += CLASS_CONTROL_ESCAPES[next]
} else if (next in CLASS_HEX_ESCAPE_LENGTHS) {
Expand Down Expand Up @@ -78,6 +89,10 @@ function translateClass(body: string, backslashIsEscape: boolean): string {
}
}
}
if (!unicodeWhitespace) {
// Reject ranges such as `[\s-x]` that the expanded set would allow
new RegExp('[' + translateClass(body, backslashIsEscape, true) + ']', 'u')
}
return cls
}

Expand Down
17 changes: 17 additions & 0 deletions packages/dockerfile-utils-js/tests/dockerignore.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,3 +84,20 @@ test('rejects invalid patterns with a plain Error', () => {
"Invalid ignore pattern '[\\q]'"
)
})

test.each([
['[\\d]', '5', true],
['[\\d]', '\u0661', false],
['[\\d]', 'd', false],
['[\\s]', ' ', true],
['[\\s]', '\u00a0', false],
['[\\S]', '\u00a0', true],
['[\\S]', '\t', false],
])('class %s matches %j: %s', (pattern, path, excluded) => {
assert.strictEqual(new PatternMatcher([pattern]).matches(path), excluded)
})

test('rejects ranges with a whitespace class escape', () => {
assert.throws(() => new PatternMatcher(['[\\s-x]']), Error)
assert.throws(() => new PatternMatcher(['[a-\\S]']), Error)
})
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ def _compile(pattern: str, backslash_is_escape: bool) -> Callable[[str], bool]:
return lambda path: path.endswith(suffix) or (
suffix.startswith("/") and path == suffix[1:]
)
compiled = re.compile(regex + r"\Z")
compiled = re.compile(regex + r"\Z", flags=re.ASCII)
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
Resolved
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
Resolved
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
Resolved
return lambda path: compiled.match(path) is not None


Expand Down
23 changes: 23 additions & 0 deletions packages/dockerfile-utils-python/tests/test_dockerignore.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,3 +72,26 @@ def test_rejects_invalid_patterns_with_value_error():
PatternMatcher(["[abc"])
with pytest.raises(ValueError, match=r"Invalid ignore pattern '\[\\q\]'"):
PatternMatcher(["[\\q]"])


@pytest.mark.parametrize(
("pattern", "path", "excluded"),
[
(r"[\d]", "5", True),
(r"[\d]", "\u0661", False),
(r"[\d]", "d", False),
(r"[\s]", " ", True),
(r"[\s]", "\u00a0", False),
(r"[\S]", "\u00a0", True),
(r"[\S]", "\t", False),
],
)
def test_class_escapes_are_ascii_only(pattern, path, excluded):
assert PatternMatcher([pattern]).matches(path) is excluded


def test_rejects_ranges_with_a_whitespace_class_escape():
with pytest.raises(ValueError):
PatternMatcher([r"[\s-x]"])
with pytest.raises(ValueError):
PatternMatcher([r"[a-\S]"])
Loading