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
3 changes: 3 additions & 0 deletions .release-notes/protect-ignore-file-size.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
## Protect pony-lint against oversize ignore files

pony-lint now rejects `.gitignore` and `.ignore` files larger than 64 KB. With hierarchical ignore loading, each directory in a project can have its own ignore files — an unexpectedly large file could cause excessive memory consumption. Ignore files that exceed the limit or that cannot be opened produce a `lint/ignore-error` diagnostic with exit code 2.
40 changes: 38 additions & 2 deletions tools/pony-lint/ignore_matcher.pony
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ class ref IgnoreMatcher
let _root: String val
let _in_git_repo: Bool
let _rules: Array[(IgnorePattern val, String val)]
let _errors: Array[(String val, String val)]

new ref create(file_auth: FileAuth, root: (String val | None)) =>
"""
Expand All @@ -63,6 +64,7 @@ class ref IgnoreMatcher
_in_git_repo = false
end
_rules = Array[(IgnorePattern val, String val)]
_errors = Array[(String val, String val)]

fun ref load_directory(dir_path: String val) =>
"""
Expand All @@ -75,15 +77,47 @@ class ref IgnoreMatcher
end
_load_file(Path.join(dir_path, ".ignore"), dir_path)

fun errors(): this->Array[(String val, String val)] =>
"""
Return errors accumulated during `load_directory` calls. Each entry is
a `(message, file_path)` tuple. Call `clear_errors` after draining.
"""
_errors

fun ref clear_errors() =>
"""
Remove all accumulated errors. Call after draining `errors()`.
"""
_errors.clear()

fun ref _load_file(file_path: String val, base_dir: String val) =>
"""
Parse all lines from an ignore file and append the resulting rules.

Rejects files that cannot be opened or are larger than 64 KB to prevent
unexpected memory consumption, especially with hierarchical ignore
loading where each directory can have its own `.gitignore` and `.ignore`
files.
"""
let fp = FilePath(_file_auth, file_path)
if not fp.exists() then return end
let file = File.open(fp)
if not file.valid() then return end
let content: String val = file.read_string(file.size())
if not file.valid() then
_errors.push((
"could not open ignore file: " + file_path,
file_path))
return
end
let size = file.size()
if size > _max_ignore_file_size() then
file.dispose()
_errors.push((
"ignore file too large (" + size.string() + " bytes, max "
+ _max_ignore_file_size().string() + "): " + file_path,
file_path))
return
end
let content: String val = file.read_string(size)
file.dispose()
// Normalize line endings and parse
let clean_content: String val =
Expand Down Expand Up @@ -168,3 +202,5 @@ class ref IgnoreMatcher
else
false
end

fun _max_ignore_file_size(): USize => 65_536
22 changes: 22 additions & 0 deletions tools/pony-lint/linter.pony
Original file line number Diff line number Diff line change
Expand Up @@ -480,6 +480,17 @@ class val Linter
end
end

// Surface errors from intermediate ignore file loading
for (msg, path) in matcher.errors().values() do
config_errors.push(Diagnostic(
"lint/ignore-error",
msg,
try Path.rel(_cwd, path)? else path end,
0,
0))
end
matcher.clear_errors()

// Pre-load subdirectory configs from hierarchy root through intermediate
// directories to each target. Uses _root_dir (config hierarchy root),
// not the git root.
Expand Down Expand Up @@ -674,6 +685,17 @@ class ref _FileCollector is WalkHandler
// Load ignore files for this directory
_matcher.load_directory(dir_path.path)

// Surface errors from ignore file loading
for (msg, path) in _matcher.errors().values() do
_config_errors.push(Diagnostic(
"lint/ignore-error",
msg,
try Path.rel(_cwd, path)? else path end,
0,
0))
end
_matcher.clear_errors()

// Load subdirectory config if present
_load_config(dir_path.path)

Expand Down
45 changes: 45 additions & 0 deletions tools/pony-lint/test/_test_ignore.pony
Original file line number Diff line number Diff line change
Expand Up @@ -424,6 +424,51 @@ class \nodoc\ _TestIgnoreMatcherHierarchical is UnitTest
h.fail("could not create temp directory")
end

class \nodoc\ _TestIgnoreMatcherOversizedFile is UnitTest
"""Oversized ignore file produces an error and loads no rules."""
fun name(): String => "IgnoreMatcher: oversized file -> error"

fun apply(h: TestHelper) =>
let auth = h.env.root
try
let tmp = FilePath.mkdtemp(FileAuth(auth), "ignore-test")?
let git_dir =
FilePath(FileAuth(auth), Path.join(tmp.path, ".git"))
git_dir.mkdir()
// Write a .gitignore that exceeds the 64 KB limit
let gitignore_path =
Path.join(tmp.path, ".gitignore")
let gitignore =
File(FilePath(FileAuth(auth), gitignore_path))
let content = recover val String(65_537) .> append("x" * 65_537) end
gitignore.print(content)
gitignore.dispose()

let matcher = lint.IgnoreMatcher(FileAuth(auth), tmp.path)
matcher.load_directory(tmp.path)

// Should not have loaded any rules
h.assert_false(
matcher.is_ignored(
Path.join(tmp.path, "anything"), "anything", false))

// Should have recorded an error
let errs = matcher.errors()
h.assert_eq[USize](1, errs.size())
try
(let msg, _) = errs(0)?
h.assert_true(msg.contains("too large"))
else
h.fail("could not read error")
end

FilePath(FileAuth(auth), gitignore_path).remove()
git_dir.remove()
tmp.remove()
else
h.fail("could not create temp directory")
end

class \nodoc\ _TestIgnoreMatcherAnchoredPattern is UnitTest
"""Anchored patterns match against the relative path from base_dir."""
fun name(): String => "IgnoreMatcher: anchored pattern"
Expand Down
1 change: 1 addition & 0 deletions tools/pony-lint/test/main.pony
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@ actor \nodoc\ Main is TestList
test(_TestIgnoreMatcherNonGitIgnoresGitignore)
test(_TestIgnoreMatcherHierarchical)
test(_TestIgnoreMatcherAnchoredPattern)
test(_TestIgnoreMatcherOversizedFile)

// Linter tests
test(_TestLinterSingleFile)
Expand Down
Loading