fix: raise .MANIFEST cap and fail closed (coral#1) #1
Merged
Chris Tusa wants to merge manifest-cap-coral1 into @
Fixes coral#1.
coral build of an OS-sized package (hammerhead-basesystem ~42k members) succeeded but wrote a silently truncated .MANIFEST at 8192 entries. Install is manifest-driven, so the host would omit kernel, libc, and Limine.
This is a Coral array bound, not a Reef compiler limit. 0.5.0 shipped without it.
Change
- Shared cap
types.max_package_files()= 131072 (above ~42k) - Generate walks one past the cap and returns empty on overflow;
coral buildfails closed instead of packaging a prefix parse_manifestreturns -1 if leftover real lines remain after max- Heapsort replaces insertion sort (O(n²) is too slow at 42k)
- install / remove / upgrade / verify / files / owner / db file lists use the same cap
Test
test/mtree_cap_test.reef: leftover parse fails closed, 20 files with max=10 yields empty, 8500-file tree inventories all 8500 entries, paths sorted.
Not a 0.5.1 tag — merge first, then bump/tag separately.
Changes · 12 files · +373 −62
| diff -r f6ecb7b9c2c8 -r 90ce6b0b95f1 src/commands/build.reef | ||
| --- a/src/commands/build.reef Wed Aug 26 15:29:04 2026 -0500 | ||
| +++ b/src/commands/build.reef Wed Aug 26 19:32:24 2026 -0500 | ||
| @@ -1166,6 +1166,7 @@ | ||
| 1166 | 1166 | // uid/gid resolution now uses zero-allocation stat.uid_name()/gid_name() |
| 1167 | 1167 | let content = mtree.generate_manifest(ctx.pkg_dir) |
| 1168 | 1168 | if str.length(content) == 0 |
| 1169 | + color.print_error("Failed to generate .MANIFEST (empty inventory or package exceeds " + int_to_str(types.max_package_files()) + " files)") |
|
| 1169 | 1170 | return false |
| 1170 | 1171 | end if |
| 1171 | 1172 |
| Old | New | ||
|---|---|---|---|
| diff -r f6ecb7b9c2c8 -r 90ce6b0b95f1 src/commands/build.reef | diff -r f6ecb7b9c2c8 -r 90ce6b0b95f1 src/commands/build.reef | ||
| --- a/src/commands/build.reef Wed Aug 26 15:29:04 2026 -0500 | --- a/src/commands/build.reef Wed Aug 26 15:29:04 2026 -0500 | ||
| +++ b/src/commands/build.reef Wed Aug 26 19:32:24 2026 -0500 | +++ b/src/commands/build.reef Wed Aug 26 19:32:24 2026 -0500 | ||
| @@ -1166,6 +1166,7 @@ | @@ -1166,6 +1166,7 @@ | ||
| 1166 | // uid/gid resolution now uses zero-allocation stat.uid_name()/gid_name() |
1166 | // uid/gid resolution now uses zero-allocation stat.uid_name()/gid_name() |
| 1167 | let content = mtree.generate_manifest(ctx.pkg_dir) |
1167 | let content = mtree.generate_manifest(ctx.pkg_dir) |
| 1168 | if str.length(content) == 0 |
1168 | if str.length(content) == 0 |
| 1169 | color.print_error("Failed to generate .MANIFEST (empty inventory or package exceeds " + int_to_str(types.max_package_files()) + " files)") |
||
| 1169 | return false |
1170 | return false |
| 1170 | end if |
1171 | end if |
| 1171 | 1172 |
| diff -r f6ecb7b9c2c8 -r 90ce6b0b95f1 src/commands/files.reef | ||
| --- a/src/commands/files.reef Wed Aug 26 15:29:04 2026 -0500 | ||
| +++ b/src/commands/files.reef Wed Aug 26 19:32:24 2026 -0500 | ||
| @@ -54,8 +54,9 @@ | ||
| 54 | 54 | end if |
| 55 | 55 | |
| 56 | 56 | // Get files (use rooted version if root is set) |
| 57 | - mut files: [string] = new [string](4096) |
|
| 58 | - let file_count = get_files_list(pkg_name, files, 4096, root_path) |
|
| 57 | + let file_cap = types.max_package_files() |
|
| 58 | + mut files: [string] = new [string](file_cap) |
|
| 59 | + let file_count = get_files_list(pkg_name, files, file_cap, root_path) |
|
| 59 | 60 | |
| 60 | 61 | if file_count == 0 |
| 61 | 62 | color.print_info("No files recorded for " + pkg_name) |
| Old | New | ||
|---|---|---|---|
| diff -r f6ecb7b9c2c8 -r 90ce6b0b95f1 src/commands/files.reef | diff -r f6ecb7b9c2c8 -r 90ce6b0b95f1 src/commands/files.reef | ||
| --- a/src/commands/files.reef Wed Aug 26 15:29:04 2026 -0500 | --- a/src/commands/files.reef Wed Aug 26 15:29:04 2026 -0500 | ||
| +++ b/src/commands/files.reef Wed Aug 26 19:32:24 2026 -0500 | +++ b/src/commands/files.reef Wed Aug 26 19:32:24 2026 -0500 | ||
| @@ -54,8 +54,9 @@ | @@ -54,8 +54,9 @@ | ||
| 54 | end if |
54 | end if |
| 55 | 55 | ||
| 56 | // Get files (use rooted version if root is set) |
56 | // Get files (use rooted version if root is set) |
| 57 | mut files: [string] = new [string](4096) |
57 | let file_cap = types.max_package_files() |
| 58 | let file_count = get_files_list(pkg_name, files, 4096, root_path) |
58 | mut files: [string] = new [string](file_cap) |
| 59 | let file_count = get_files_list(pkg_name, files, file_cap, root_path) |
||
| 59 | 60 | ||
| 60 | if file_count == 0 |
61 | if file_count == 0 |
| 61 | color.print_info("No files recorded for " + pkg_name) |
62 | color.print_info("No files recorded for " + pkg_name) |
| diff -r f6ecb7b9c2c8 -r 90ce6b0b95f1 src/commands/install.reef | ||
| --- a/src/commands/install.reef Wed Aug 26 15:29:04 2026 -0500 | ||
| +++ b/src/commands/install.reef Wed Aug 26 19:32:24 2026 -0500 | ||
| @@ -433,15 +433,21 @@ | ||
| 433 | 433 | |
| 434 | 434 | // Remove old files |
| 435 | 435 | color.print_info("Removing old version files...") |
| 436 | - mut old_files: [string] = new [string](4096) |
|
| 437 | - let old_file_count = get_old_files(info.name, old_files, 4096, root_path) |
|
| 436 | + let file_cap = types.max_package_files() |
|
| 437 | + mut old_files: [string] = new [string](file_cap) |
|
| 438 | + let old_file_count = get_old_files(info.name, old_files, file_cap, root_path) |
|
| 438 | 439 | if old_file_count > 0 |
| 439 | 440 | package.remove_files(old_files, old_file_count, install_root) |
| 440 | 441 | end if |
| 441 | 442 | |
| 442 | 443 | // Load old manifest for config file checksum protection |
| 443 | - mut old_manifest_entries: [mtree.ManifestEntry] = new [mtree.ManifestEntry](8192) |
|
| 444 | - let old_manifest_count = load_old_manifest(info.name, root_path, old_manifest_entries, 8192) |
|
| 444 | + mut old_manifest_entries: [mtree.ManifestEntry] = new [mtree.ManifestEntry](file_cap) |
|
| 445 | + let old_manifest_count = load_old_manifest(info.name, root_path, old_manifest_entries, file_cap) |
|
| 446 | + if old_manifest_count < 0 |
|
| 447 | + color.print_error("Old manifest exceeds maximum of " + int_to_str(file_cap) + " files") |
|
| 448 | + cleanup_extract_dir(extract_dir) |
|
| 449 | + return false |
|
| 450 | + end if |
|
| 445 | 451 | |
| 446 | 452 | // Install new files (with upgrade config protection) |
| 447 | 453 | color.print_info("Installing files...") |
| Old | New | ||
|---|---|---|---|
| diff -r f6ecb7b9c2c8 -r 90ce6b0b95f1 src/commands/install.reef | diff -r f6ecb7b9c2c8 -r 90ce6b0b95f1 src/commands/install.reef | ||
| --- a/src/commands/install.reef Wed Aug 26 15:29:04 2026 -0500 | --- a/src/commands/install.reef Wed Aug 26 15:29:04 2026 -0500 | ||
| +++ b/src/commands/install.reef Wed Aug 26 19:32:24 2026 -0500 | +++ b/src/commands/install.reef Wed Aug 26 19:32:24 2026 -0500 | ||
| @@ -433,15 +433,21 @@ | @@ -433,15 +433,21 @@ | ||
| 433 | 433 | ||
| 434 | // Remove old files |
434 | // Remove old files |
| 435 | color.print_info("Removing old version files...") |
435 | color.print_info("Removing old version files...") |
| 436 | mut old_files: [string] = new [string](4096) |
436 | let file_cap = types.max_package_files() |
| 437 | let old_file_count = get_old_files(info.name, old_files, 4096, root_path) |
437 | mut old_files: [string] = new [string](file_cap) |
| 438 | let old_file_count = get_old_files(info.name, old_files, file_cap, root_path) |
||
| 438 | if old_file_count > 0 |
439 | if old_file_count > 0 |
| 439 | package.remove_files(old_files, old_file_count, install_root) |
440 | package.remove_files(old_files, old_file_count, install_root) |
| 440 | end if |
441 | end if |
| 441 | 442 | ||
| 442 | // Load old manifest for config file checksum protection |
443 | // Load old manifest for config file checksum protection |
| 443 | mut old_manifest_entries: [mtree.ManifestEntry] = new [mtree.ManifestEntry](8192) |
444 | mut old_manifest_entries: [mtree.ManifestEntry] = new [mtree.ManifestEntry](file_cap) |
| 444 | let old_manifest_count = load_old_manifest(info.name, root_path, old_manifest_entries, 8192) |
445 | let old_manifest_count = load_old_manifest(info.name, root_path, old_manifest_entries, file_cap) |
| 446 | if old_manifest_count < 0 |
||
| 447 | color.print_error("Old manifest exceeds maximum of " + int_to_str(file_cap) + " files") |
||
| 448 | cleanup_extract_dir(extract_dir) |
||
| 449 | return false |
||
| 450 | end if |
||
| 445 | 451 | ||
| 446 | // Install new files (with upgrade config protection) |
452 | // Install new files (with upgrade config protection) |
| 447 | color.print_info("Installing files...") |
453 | color.print_info("Installing files...") |
| diff -r f6ecb7b9c2c8 -r 90ce6b0b95f1 src/commands/remove.reef | ||
| --- a/src/commands/remove.reef Wed Aug 26 15:29:04 2026 -0500 | ||
| +++ b/src/commands/remove.reef Wed Aug 26 19:32:24 2026 -0500 | ||
| @@ -195,8 +195,9 @@ | ||
| 195 | 195 | color.print_info("Package: " + pkg.info.name + " " + pkg.info.version) |
| 196 | 196 | |
| 197 | 197 | // Get file list from database (use rooted version if root is set) |
| 198 | - mut files: [string] = new [string](4096) |
|
| 199 | - let file_count = get_files_list(pkg_name, files, 4096, root_path) |
|
| 198 | + let file_cap = types.max_package_files() |
|
| 199 | + mut files: [string] = new [string](file_cap) |
|
| 200 | + let file_count = get_files_list(pkg_name, files, file_cap, root_path) |
|
| 200 | 201 | |
| 201 | 202 | if file_count == 0 |
| 202 | 203 | color.print_warning("No files recorded for package") |
| @@ -218,13 +219,17 @@ | ||
| 218 | 219 | mut config_files: [string] = new [string](256) |
| 219 | 220 | let config_count = database.get_config_files(pkg_name, config_files, 256) |
| 220 | 221 | |
| 221 | - mut manifest_entries: [mtree.ManifestEntry] = new [mtree.ManifestEntry](8192) |
|
| 222 | + mut manifest_entries: [mtree.ManifestEntry] = new [mtree.ManifestEntry](file_cap) |
|
| 222 | 223 | mut manifest_count = 0 |
| 223 | 224 | let manifest_path = database.get_manifest_path(pkg_name) |
| 224 | 225 | if file.fileExists(manifest_path) |
| 225 | 226 | let manifest_content = res.unwrap_or(file.readFile(manifest_path), "") |
| 226 | 227 | if str.length(manifest_content) > 0 |
| 227 | - manifest_count = mtree.parse_manifest(manifest_content, manifest_entries, 8192) |
|
| 228 | + manifest_count = mtree.parse_manifest(manifest_content, manifest_entries, file_cap) |
|
| 229 | + if manifest_count < 0 |
|
| 230 | + color.print_error("Manifest for " + pkg_name + " exceeds maximum of " + int_to_str(file_cap) + " files") |
|
| 231 | + return false |
|
| 232 | + end if |
|
| 228 | 233 | end if |
| 229 | 234 | end if |
| 230 | 235 |
| Old | New | ||
|---|---|---|---|
| diff -r f6ecb7b9c2c8 -r 90ce6b0b95f1 src/commands/remove.reef | diff -r f6ecb7b9c2c8 -r 90ce6b0b95f1 src/commands/remove.reef | ||
| --- a/src/commands/remove.reef Wed Aug 26 15:29:04 2026 -0500 | --- a/src/commands/remove.reef Wed Aug 26 15:29:04 2026 -0500 | ||
| +++ b/src/commands/remove.reef Wed Aug 26 19:32:24 2026 -0500 | +++ b/src/commands/remove.reef Wed Aug 26 19:32:24 2026 -0500 | ||
| @@ -195,8 +195,9 @@ | @@ -195,8 +195,9 @@ | ||
| 195 | color.print_info("Package: " + pkg.info.name + " " + pkg.info.version) |
195 | color.print_info("Package: " + pkg.info.name + " " + pkg.info.version) |
| 196 | 196 | ||
| 197 | // Get file list from database (use rooted version if root is set) |
197 | // Get file list from database (use rooted version if root is set) |
| 198 | mut files: [string] = new [string](4096) |
198 | let file_cap = types.max_package_files() |
| 199 | let file_count = get_files_list(pkg_name, files, 4096, root_path) |
199 | mut files: [string] = new [string](file_cap) |
| 200 | let file_count = get_files_list(pkg_name, files, file_cap, root_path) |
||
| 200 | 201 | ||
| 201 | if file_count == 0 |
202 | if file_count == 0 |
| 202 | color.print_warning("No files recorded for package") |
203 | color.print_warning("No files recorded for package") |
| @@ -218,13 +219,17 @@ | @@ -218,13 +219,17 @@ | ||
| 218 | mut config_files: [string] = new [string](256) |
219 | mut config_files: [string] = new [string](256) |
| 219 | let config_count = database.get_config_files(pkg_name, config_files, 256) |
220 | let config_count = database.get_config_files(pkg_name, config_files, 256) |
| 220 | 221 | ||
| 221 | mut manifest_entries: [mtree.ManifestEntry] = new [mtree.ManifestEntry](8192) |
222 | mut manifest_entries: [mtree.ManifestEntry] = new [mtree.ManifestEntry](file_cap) |
| 222 | mut manifest_count = 0 |
223 | mut manifest_count = 0 |
| 223 | let manifest_path = database.get_manifest_path(pkg_name) |
224 | let manifest_path = database.get_manifest_path(pkg_name) |
| 224 | if file.fileExists(manifest_path) |
225 | if file.fileExists(manifest_path) |
| 225 | let manifest_content = res.unwrap_or(file.readFile(manifest_path), "") |
226 | let manifest_content = res.unwrap_or(file.readFile(manifest_path), "") |
| 226 | if str.length(manifest_content) > 0 |
227 | if str.length(manifest_content) > 0 |
| 227 | manifest_count = mtree.parse_manifest(manifest_content, manifest_entries, 8192) |
228 | manifest_count = mtree.parse_manifest(manifest_content, manifest_entries, file_cap) |
| 229 | if manifest_count < 0 |
||
| 230 | color.print_error("Manifest for " + pkg_name + " exceeds maximum of " + int_to_str(file_cap) + " files") |
||
| 231 | return false |
||
| 232 | end if |
||
| 228 | end if |
233 | end if |
| 229 | end if |
234 | end if |
| 230 | 235 |
| diff -r f6ecb7b9c2c8 -r 90ce6b0b95f1 src/commands/upgrade.reef | ||
| --- a/src/commands/upgrade.reef Wed Aug 26 15:29:04 2026 -0500 | ||
| +++ b/src/commands/upgrade.reef Wed Aug 26 19:32:24 2026 -0500 | ||
| @@ -301,8 +301,9 @@ | ||
| 301 | 301 | end if |
| 302 | 302 | |
| 303 | 303 | // Remove old version (keep database entry for now) |
| 304 | - mut files: [string] = new [string](4096) |
|
| 305 | - let file_count = get_files_list(pkg_name, files, 4096, root_path) |
|
| 304 | + let file_cap = types.max_package_files() |
|
| 305 | + mut files: [string] = new [string](file_cap) |
|
| 306 | + let file_count = get_files_list(pkg_name, files, file_cap, root_path) |
|
| 306 | 307 | |
| 307 | 308 | color.print_info("Removing old files...") |
| 308 | 309 | let actual_root = get_actual_root(root_path) |
| @@ -327,8 +328,8 @@ | ||
| 327 | 328 | end if |
| 328 | 329 | |
| 329 | 330 | // Read new file list |
| 330 | - mut new_files: [string] = new [string](4096) |
|
| 331 | - let new_file_count = package.read_footprint(extract_dir, new_files, 4096) |
|
| 331 | + mut new_files: [string] = new [string](file_cap) |
|
| 332 | + let new_file_count = package.read_footprint(extract_dir, new_files, file_cap) |
|
| 332 | 333 | |
| 333 | 334 | // Install files to proper root |
| 334 | 335 | if not package.install_files(extract_dir, actual_root) |
| Old | New | ||
|---|---|---|---|
| diff -r f6ecb7b9c2c8 -r 90ce6b0b95f1 src/commands/upgrade.reef | diff -r f6ecb7b9c2c8 -r 90ce6b0b95f1 src/commands/upgrade.reef | ||
| --- a/src/commands/upgrade.reef Wed Aug 26 15:29:04 2026 -0500 | --- a/src/commands/upgrade.reef Wed Aug 26 15:29:04 2026 -0500 | ||
| +++ b/src/commands/upgrade.reef Wed Aug 26 19:32:24 2026 -0500 | +++ b/src/commands/upgrade.reef Wed Aug 26 19:32:24 2026 -0500 | ||
| @@ -301,8 +301,9 @@ | @@ -301,8 +301,9 @@ | ||
| 301 | end if |
301 | end if |
| 302 | 302 | ||
| 303 | // Remove old version (keep database entry for now) |
303 | // Remove old version (keep database entry for now) |
| 304 | mut files: [string] = new [string](4096) |
304 | let file_cap = types.max_package_files() |
| 305 | let file_count = get_files_list(pkg_name, files, 4096, root_path) |
305 | mut files: [string] = new [string](file_cap) |
| 306 | let file_count = get_files_list(pkg_name, files, file_cap, root_path) |
||
| 306 | 307 | ||
| 307 | color.print_info("Removing old files...") |
308 | color.print_info("Removing old files...") |
| 308 | let actual_root = get_actual_root(root_path) |
309 | let actual_root = get_actual_root(root_path) |
| @@ -327,8 +328,8 @@ | @@ -327,8 +328,8 @@ | ||
| 327 | end if |
328 | end if |
| 328 | 329 | ||
| 329 | // Read new file list |
330 | // Read new file list |
| 330 | mut new_files: [string] = new [string](4096) |
331 | mut new_files: [string] = new [string](file_cap) |
| 331 | let new_file_count = package.read_footprint(extract_dir, new_files, 4096) |
332 | let new_file_count = package.read_footprint(extract_dir, new_files, file_cap) |
| 332 | 333 | ||
| 333 | // Install files to proper root |
334 | // Install files to proper root |
| 334 | if not package.install_files(extract_dir, actual_root) |
335 | if not package.install_files(extract_dir, actual_root) |
| diff -r f6ecb7b9c2c8 -r 90ce6b0b95f1 src/commands/verify.reef | ||
| --- a/src/commands/verify.reef Wed Aug 26 15:29:04 2026 -0500 | ||
| +++ b/src/commands/verify.reef Wed Aug 26 19:32:24 2026 -0500 | ||
| @@ -75,10 +75,11 @@ | ||
| 75 | 75 | return ec.EXIT_PKG_INVALID() |
| 76 | 76 | end if |
| 77 | 77 | |
| 78 | - mut entries: [mtree.ManifestEntry] = new [mtree.ManifestEntry](8192) |
|
| 79 | - let entry_count = mtree.parse_manifest(manifest_content, entries, 8192) |
|
| 78 | + let cap = types.max_package_files() |
|
| 79 | + mut entries: [mtree.ManifestEntry] = new [mtree.ManifestEntry](cap) |
|
| 80 | + let entry_count = mtree.parse_manifest(manifest_content, entries, cap) |
|
| 80 | 81 | |
| 81 | - if entry_count == 0 |
|
| 82 | + if entry_count <= 0 |
|
| 82 | 83 | color.print_error("Failed to parse manifest for " + pkg_name) |
| 83 | 84 | return ec.EXIT_PKG_INVALID() |
| 84 | 85 | end if |
| Old | New | ||
|---|---|---|---|
| diff -r f6ecb7b9c2c8 -r 90ce6b0b95f1 src/commands/verify.reef | diff -r f6ecb7b9c2c8 -r 90ce6b0b95f1 src/commands/verify.reef | ||
| --- a/src/commands/verify.reef Wed Aug 26 15:29:04 2026 -0500 | --- a/src/commands/verify.reef Wed Aug 26 15:29:04 2026 -0500 | ||
| +++ b/src/commands/verify.reef Wed Aug 26 19:32:24 2026 -0500 | +++ b/src/commands/verify.reef Wed Aug 26 19:32:24 2026 -0500 | ||
| @@ -75,10 +75,11 @@ | @@ -75,10 +75,11 @@ | ||
| 75 | return ec.EXIT_PKG_INVALID() |
75 | return ec.EXIT_PKG_INVALID() |
| 76 | end if |
76 | end if |
| 77 | 77 | ||
| 78 | mut entries: [mtree.ManifestEntry] = new [mtree.ManifestEntry](8192) |
78 | let cap = types.max_package_files() |
| 79 | let entry_count = mtree.parse_manifest(manifest_content, entries, 8192) |
79 | mut entries: [mtree.ManifestEntry] = new [mtree.ManifestEntry](cap) |
| 80 | let entry_count = mtree.parse_manifest(manifest_content, entries, cap) |
||
| 80 | 81 | ||
| 81 | if entry_count == 0 |
82 | if entry_count <= 0 |
| 82 | color.print_error("Failed to parse manifest for " + pkg_name) |
83 | color.print_error("Failed to parse manifest for " + pkg_name) |
| 83 | return ec.EXIT_PKG_INVALID() |
84 | return ec.EXIT_PKG_INVALID() |
| 84 | end if |
85 | end if |
| diff -r f6ecb7b9c2c8 -r 90ce6b0b95f1 src/core/database.reef | ||
| --- a/src/core/database.reef Wed Aug 26 15:29:04 2026 -0500 | ||
| +++ b/src/core/database.reef Wed Aug 26 19:32:24 2026 -0500 | ||
| @@ -383,10 +383,14 @@ | ||
| 383 | 383 | // Parses manifest entries and returns a newline-joined list of relative paths |
| 384 | 384 | // (files and symlinks only, no directories, no metadata) |
| 385 | 385 | fn extract_paths_from_manifest(content: string): string |
| 386 | - mut entries: [mtree.ManifestEntry] = new [mtree.ManifestEntry](8192) |
|
| 387 | - let count = mtree.parse_manifest(content, entries, 8192) |
|
| 386 | + let cap = types.max_package_files() |
|
| 387 | + mut entries: [mtree.ManifestEntry] = new [mtree.ManifestEntry](cap) |
|
| 388 | + let count = mtree.parse_manifest(content, entries, cap) |
|
| 389 | + if count < 0 |
|
| 390 | + return "" |
|
| 391 | + end if |
|
| 388 | 392 | |
| 389 | - mut result_lines: [string] = new [string](8192) |
|
| 393 | + mut result_lines: [string] = new [string](cap) |
|
| 390 | 394 | mut result_count = 0 |
| 391 | 395 | |
| 392 | 396 | mut i = 0 |
| @@ -426,6 +430,11 @@ | ||
| 426 | 430 | // Store the full manifest for future use (verify, manifest-based remove) |
| 427 | 431 | let manifest_store = path.join_path(pkg_db_dir(pkg.info.name), "manifest.mtree") |
| 428 | 432 | let manifest_content = res.unwrap_or(file.readFile(manifest_path), "") |
| 433 | + let cap = types.max_package_files() |
|
| 434 | + mut probe: [mtree.ManifestEntry] = new [mtree.ManifestEntry](cap) |
|
| 435 | + if mtree.parse_manifest(manifest_content, probe, cap) < 0 |
|
| 436 | + return false |
|
| 437 | + end if |
|
| 429 | 438 | file.writeFile(manifest_store, manifest_content) |
| 430 | 439 | |
| 431 | 440 | // Also generate files.txt for backward compatibility |
| @@ -559,13 +568,14 @@ | ||
| 559 | 568 | mut packages: [string] = new [string](256) |
| 560 | 569 | let pkg_count = list_installed(packages, 256) |
| 561 | 570 | |
| 571 | + let file_cap = types.max_package_files() |
|
| 572 | + mut files: [string] = new [string](file_cap) |
|
| 562 | 573 | mut i = 0 |
| 563 | 574 | while i < pkg_count |
| 564 | 575 | let pkg_name = packages[i] |
| 565 | 576 | |
| 566 | 577 | // Get files for this package |
| 567 | - mut files: [string] = new [string](4096) |
|
| 568 | - let file_count = get_files(pkg_name, files, 4096) |
|
| 578 | + let file_count = get_files(pkg_name, files, file_cap) |
|
| 569 | 579 | |
| 570 | 580 | // Search for matching file |
| 571 | 581 | mut j = 0 |
| @@ -1032,13 +1042,14 @@ | ||
| 1032 | 1042 | mut packages: [string] = new [string](256) |
| 1033 | 1043 | let pkg_count = list_installed_rooted(packages, 256, root) |
| 1034 | 1044 | |
| 1045 | + let file_cap = types.max_package_files() |
|
| 1046 | + mut files: [string] = new [string](file_cap) |
|
| 1035 | 1047 | mut i = 0 |
| 1036 | 1048 | while i < pkg_count |
| 1037 | 1049 | let pkg_name = packages[i] |
| 1038 | 1050 | |
| 1039 | 1051 | // Get files for this package |
| 1040 | - mut files: [string] = new [string](4096) |
|
| 1041 | - let file_count = get_files_rooted(pkg_name, files, 4096, root) |
|
| 1052 | + let file_count = get_files_rooted(pkg_name, files, file_cap, root) |
|
| 1042 | 1053 | |
| 1043 | 1054 | // Search for matching file |
| 1044 | 1055 | mut j = 0 |
| Old | New | ||
|---|---|---|---|
| diff -r f6ecb7b9c2c8 -r 90ce6b0b95f1 src/core/database.reef | diff -r f6ecb7b9c2c8 -r 90ce6b0b95f1 src/core/database.reef | ||
| --- a/src/core/database.reef Wed Aug 26 15:29:04 2026 -0500 | --- a/src/core/database.reef Wed Aug 26 15:29:04 2026 -0500 | ||
| +++ b/src/core/database.reef Wed Aug 26 19:32:24 2026 -0500 | +++ b/src/core/database.reef Wed Aug 26 19:32:24 2026 -0500 | ||
| @@ -383,10 +383,14 @@ | @@ -383,10 +383,14 @@ | ||
| 383 | // Parses manifest entries and returns a newline-joined list of relative paths |
383 | // Parses manifest entries and returns a newline-joined list of relative paths |
| 384 | // (files and symlinks only, no directories, no metadata) |
384 | // (files and symlinks only, no directories, no metadata) |
| 385 | fn extract_paths_from_manifest(content: string): string |
385 | fn extract_paths_from_manifest(content: string): string |
| 386 | mut entries: [mtree.ManifestEntry] = new [mtree.ManifestEntry](8192) |
386 | let cap = types.max_package_files() |
| 387 | let count = mtree.parse_manifest(content, entries, 8192) |
387 | mut entries: [mtree.ManifestEntry] = new [mtree.ManifestEntry](cap) |
| 388 | let count = mtree.parse_manifest(content, entries, cap) |
||
| 389 | if count < 0 |
||
| 390 | return "" |
||
| 391 | end if |
||
| 388 | 392 | ||
| 389 | mut result_lines: [string] = new [string](8192) |
393 | mut result_lines: [string] = new [string](cap) |
| 390 | mut result_count = 0 |
394 | mut result_count = 0 |
| 391 | 395 | ||
| 392 | mut i = 0 |
396 | mut i = 0 |
| @@ -426,6 +430,11 @@ | @@ -426,6 +430,11 @@ | ||
| 426 | // Store the full manifest for future use (verify, manifest-based remove) |
430 | // Store the full manifest for future use (verify, manifest-based remove) |
| 427 | let manifest_store = path.join_path(pkg_db_dir(pkg.info.name), "manifest.mtree") |
431 | let manifest_store = path.join_path(pkg_db_dir(pkg.info.name), "manifest.mtree") |
| 428 | let manifest_content = res.unwrap_or(file.readFile(manifest_path), "") |
432 | let manifest_content = res.unwrap_or(file.readFile(manifest_path), "") |
| 433 | let cap = types.max_package_files() |
||
| 434 | mut probe: [mtree.ManifestEntry] = new [mtree.ManifestEntry](cap) |
||
| 435 | if mtree.parse_manifest(manifest_content, probe, cap) < 0 |
||
| 436 | return false |
||
| 437 | end if |
||
| 429 | file.writeFile(manifest_store, manifest_content) |
438 | file.writeFile(manifest_store, manifest_content) |
| 430 | 439 | ||
| 431 | // Also generate files.txt for backward compatibility |
440 | // Also generate files.txt for backward compatibility |
| @@ -559,13 +568,14 @@ | @@ -559,13 +568,14 @@ | ||
| 559 | mut packages: [string] = new [string](256) |
568 | mut packages: [string] = new [string](256) |
| 560 | let pkg_count = list_installed(packages, 256) |
569 | let pkg_count = list_installed(packages, 256) |
| 561 | 570 | ||
| 571 | let file_cap = types.max_package_files() |
||
| 572 | mut files: [string] = new [string](file_cap) |
||
| 562 | mut i = 0 |
573 | mut i = 0 |
| 563 | while i < pkg_count |
574 | while i < pkg_count |
| 564 | let pkg_name = packages[i] |
575 | let pkg_name = packages[i] |
| 565 | 576 | ||
| 566 | // Get files for this package |
577 | // Get files for this package |
| 567 | mut files: [string] = new [string](4096) |
578 | let file_count = get_files(pkg_name, files, file_cap) |
| 568 | let file_count = get_files(pkg_name, files, 4096) |
||
| 569 | 579 | ||
| 570 | // Search for matching file |
580 | // Search for matching file |
| 571 | mut j = 0 |
581 | mut j = 0 |
| @@ -1032,13 +1042,14 @@ | @@ -1032,13 +1042,14 @@ | ||
| 1032 | mut packages: [string] = new [string](256) |
1042 | mut packages: [string] = new [string](256) |
| 1033 | let pkg_count = list_installed_rooted(packages, 256, root) |
1043 | let pkg_count = list_installed_rooted(packages, 256, root) |
| 1034 | 1044 | ||
| 1045 | let file_cap = types.max_package_files() |
||
| 1046 | mut files: [string] = new [string](file_cap) |
||
| 1035 | mut i = 0 |
1047 | mut i = 0 |
| 1036 | while i < pkg_count |
1048 | while i < pkg_count |
| 1037 | let pkg_name = packages[i] |
1049 | let pkg_name = packages[i] |
| 1038 | 1050 | ||
| 1039 | // Get files for this package |
1051 | // Get files for this package |
| 1040 | mut files: [string] = new [string](4096) |
1052 | let file_count = get_files_rooted(pkg_name, files, file_cap, root) |
| 1041 | let file_count = get_files_rooted(pkg_name, files, 4096, root) |
||
| 1042 | 1053 | ||
| 1043 | // Search for matching file |
1054 | // Search for matching file |
| 1044 | mut j = 0 |
1055 | mut j = 0 |
| diff -r f6ecb7b9c2c8 -r 90ce6b0b95f1 src/core/package.reef | ||
| --- a/src/core/package.reef Wed Aug 26 15:29:04 2026 -0500 | ||
| +++ b/src/core/package.reef Wed Aug 26 19:32:24 2026 -0500 | ||
| @@ -190,6 +190,9 @@ | ||
| 190 | 190 | if str.length(content) > 0 |
| 191 | 191 | mut entries: [mtree.ManifestEntry] = new [mtree.ManifestEntry](max_count) |
| 192 | 192 | let entry_count = mtree.parse_manifest(content, entries, max_count) |
| 193 | + if entry_count < 0 |
|
| 194 | + return -1 |
|
| 195 | + end if |
|
| 193 | 196 | |
| 194 | 197 | mut count = 0 |
| 195 | 198 | mut i = 0 |
| @@ -286,10 +289,11 @@ | ||
| 286 | 289 | let config_count = read_config_files(src_dir, config_files, 256) |
| 287 | 290 | |
| 288 | 291 | // Parse .MANIFEST |
| 289 | - mut entries: [mtree.ManifestEntry] = new [mtree.ManifestEntry](8192) |
|
| 290 | - let entry_count = read_manifest(src_dir, entries, 8192) |
|
| 292 | + let cap = types.max_package_files() |
|
| 293 | + mut entries: [mtree.ManifestEntry] = new [mtree.ManifestEntry](cap) |
|
| 294 | + let entry_count = read_manifest(src_dir, entries, cap) |
|
| 291 | 295 | |
| 292 | - if entry_count == 0 |
|
| 296 | + if entry_count <= 0 |
|
| 293 | 297 | return false |
| 294 | 298 | end if |
| 295 | 299 | |
| @@ -902,10 +906,11 @@ | ||
| 902 | 906 | mut config_files: [string] = new [string](256) |
| 903 | 907 | let config_count = read_config_files(src_dir, config_files, 256) |
| 904 | 908 | |
| 905 | - mut entries: [mtree.ManifestEntry] = new [mtree.ManifestEntry](8192) |
|
| 906 | - let entry_count = read_manifest(src_dir, entries, 8192) |
|
| 909 | + let cap = types.max_package_files() |
|
| 910 | + mut entries: [mtree.ManifestEntry] = new [mtree.ManifestEntry](cap) |
|
| 911 | + let entry_count = read_manifest(src_dir, entries, cap) |
|
| 907 | 912 | |
| 908 | - if entry_count == 0 |
|
| 913 | + if entry_count <= 0 |
|
| 909 | 914 | return false |
| 910 | 915 | end if |
| 911 | 916 |
| Old | New | ||
|---|---|---|---|
| diff -r f6ecb7b9c2c8 -r 90ce6b0b95f1 src/core/package.reef | diff -r f6ecb7b9c2c8 -r 90ce6b0b95f1 src/core/package.reef | ||
| --- a/src/core/package.reef Wed Aug 26 15:29:04 2026 -0500 | --- a/src/core/package.reef Wed Aug 26 15:29:04 2026 -0500 | ||
| +++ b/src/core/package.reef Wed Aug 26 19:32:24 2026 -0500 | +++ b/src/core/package.reef Wed Aug 26 19:32:24 2026 -0500 | ||
| @@ -190,6 +190,9 @@ | @@ -190,6 +190,9 @@ | ||
| 190 | if str.length(content) > 0 |
190 | if str.length(content) > 0 |
| 191 | mut entries: [mtree.ManifestEntry] = new [mtree.ManifestEntry](max_count) |
191 | mut entries: [mtree.ManifestEntry] = new [mtree.ManifestEntry](max_count) |
| 192 | let entry_count = mtree.parse_manifest(content, entries, max_count) |
192 | let entry_count = mtree.parse_manifest(content, entries, max_count) |
| 193 | if entry_count < 0 |
||
| 194 | return -1 |
||
| 195 | end if |
||
| 193 | 196 | ||
| 194 | mut count = 0 |
197 | mut count = 0 |
| 195 | mut i = 0 |
198 | mut i = 0 |
| @@ -286,10 +289,11 @@ | @@ -286,10 +289,11 @@ | ||
| 286 | let config_count = read_config_files(src_dir, config_files, 256) |
289 | let config_count = read_config_files(src_dir, config_files, 256) |
| 287 | 290 | ||
| 288 | // Parse .MANIFEST |
291 | // Parse .MANIFEST |
| 289 | mut entries: [mtree.ManifestEntry] = new [mtree.ManifestEntry](8192) |
292 | let cap = types.max_package_files() |
| 290 | let entry_count = read_manifest(src_dir, entries, 8192) |
293 | mut entries: [mtree.ManifestEntry] = new [mtree.ManifestEntry](cap) |
| 294 | let entry_count = read_manifest(src_dir, entries, cap) |
||
| 291 | 295 | ||
| 292 | if entry_count == 0 |
296 | if entry_count <= 0 |
| 293 | return false |
297 | return false |
| 294 | end if |
298 | end if |
| 295 | 299 | ||
| @@ -902,10 +906,11 @@ | @@ -902,10 +906,11 @@ | ||
| 902 | mut config_files: [string] = new [string](256) |
906 | mut config_files: [string] = new [string](256) |
| 903 | let config_count = read_config_files(src_dir, config_files, 256) |
907 | let config_count = read_config_files(src_dir, config_files, 256) |
| 904 | 908 | ||
| 905 | mut entries: [mtree.ManifestEntry] = new [mtree.ManifestEntry](8192) |
909 | let cap = types.max_package_files() |
| 906 | let entry_count = read_manifest(src_dir, entries, 8192) |
910 | mut entries: [mtree.ManifestEntry] = new [mtree.ManifestEntry](cap) |
| 911 | let entry_count = read_manifest(src_dir, entries, cap) |
||
| 907 | 912 | ||
| 908 | if entry_count == 0 |
913 | if entry_count <= 0 |
| 909 | return false |
914 | return false |
| 910 | end if |
915 | end if |
| 911 | 916 |
| diff -r f6ecb7b9c2c8 -r 90ce6b0b95f1 src/core/pkgdb.reef | ||
| --- a/src/core/pkgdb.reef Wed Aug 26 15:29:04 2026 -0500 | ||
| +++ b/src/core/pkgdb.reef Wed Aug 26 19:32:24 2026 -0500 | ||
| @@ -741,11 +741,12 @@ | ||
| 741 | 741 | mut pkg_names: [string] = new [string](MAX_PACKAGES) |
| 742 | 742 | let pkg_count = database.list_installed_rooted(pkg_names, MAX_PACKAGES, root) |
| 743 | 743 | |
| 744 | + let file_cap = types.max_package_files() |
|
| 745 | + mut pkg_files: [string] = new [string](file_cap) |
|
| 744 | 746 | mut total = 0 |
| 745 | 747 | mut i = 0 |
| 746 | 748 | while i < pkg_count |
| 747 | - mut pkg_files: [string] = new [string](8192) |
|
| 748 | - let file_count = database.get_files_rooted(pkg_names[i], pkg_files, 8192, root) |
|
| 749 | + let file_count = database.get_files_rooted(pkg_names[i], pkg_files, file_cap, root) |
|
| 749 | 750 | total = total + file_count |
| 750 | 751 | i = i + 1 |
| 751 | 752 | end while |
| @@ -765,10 +766,11 @@ | ||
| 765 | 766 | mut all_owners: [string] = new [string](alloc_size) |
| 766 | 767 | mut total_files = 0 |
| 767 | 768 | |
| 769 | + let file_cap = types.max_package_files() |
|
| 770 | + mut pkg_files: [string] = new [string](file_cap) |
|
| 768 | 771 | mut i = 0 |
| 769 | 772 | while i < pkg_count and total_files < alloc_size - 256 |
| 770 | - mut pkg_files: [string] = new [string](8192) |
|
| 771 | - let file_count = database.get_files_rooted(pkg_names[i], pkg_files, 8192, root) |
|
| 773 | + let file_count = database.get_files_rooted(pkg_names[i], pkg_files, file_cap, root) |
|
| 772 | 774 | |
| 773 | 775 | mut j = 0 |
| 774 | 776 | while j < file_count and total_files < alloc_size |
| @@ -905,10 +907,11 @@ | ||
| 905 | 907 | stats.package_count = database.list_installed(names, 1024) |
| 906 | 908 | |
| 907 | 909 | // Count files across all packages |
| 910 | + let file_cap = types.max_package_files() |
|
| 911 | + mut files: [string] = new [string](file_cap) |
|
| 908 | 912 | mut i = 0 |
| 909 | 913 | while i < stats.package_count |
| 910 | - mut files: [string] = new [string](4096) |
|
| 911 | - let file_count = database.get_files(names[i], files, 4096) |
|
| 914 | + let file_count = database.get_files(names[i], files, file_cap) |
|
| 912 | 915 | stats.file_count = stats.file_count + file_count |
| 913 | 916 | i = i + 1 |
| 914 | 917 | end while |
| @@ -997,10 +1000,11 @@ | ||
| 997 | 1000 | stats.package_count = database.list_installed_rooted(names, 1024, root) |
| 998 | 1001 | |
| 999 | 1002 | // Count files across all packages |
| 1003 | + let file_cap = types.max_package_files() |
|
| 1004 | + mut files: [string] = new [string](file_cap) |
|
| 1000 | 1005 | mut i = 0 |
| 1001 | 1006 | while i < stats.package_count |
| 1002 | - mut files: [string] = new [string](4096) |
|
| 1003 | - let file_count = database.get_files_rooted(names[i], files, 4096, root) |
|
| 1007 | + let file_count = database.get_files_rooted(names[i], files, file_cap, root) |
|
| 1004 | 1008 | stats.file_count = stats.file_count + file_count |
| 1005 | 1009 | i = i + 1 |
| 1006 | 1010 | end while |
| Old | New | ||
|---|---|---|---|
| diff -r f6ecb7b9c2c8 -r 90ce6b0b95f1 src/core/pkgdb.reef | diff -r f6ecb7b9c2c8 -r 90ce6b0b95f1 src/core/pkgdb.reef | ||
| --- a/src/core/pkgdb.reef Wed Aug 26 15:29:04 2026 -0500 | --- a/src/core/pkgdb.reef Wed Aug 26 15:29:04 2026 -0500 | ||
| +++ b/src/core/pkgdb.reef Wed Aug 26 19:32:24 2026 -0500 | +++ b/src/core/pkgdb.reef Wed Aug 26 19:32:24 2026 -0500 | ||
| @@ -741,11 +741,12 @@ | @@ -741,11 +741,12 @@ | ||
| 741 | mut pkg_names: [string] = new [string](MAX_PACKAGES) |
741 | mut pkg_names: [string] = new [string](MAX_PACKAGES) |
| 742 | let pkg_count = database.list_installed_rooted(pkg_names, MAX_PACKAGES, root) |
742 | let pkg_count = database.list_installed_rooted(pkg_names, MAX_PACKAGES, root) |
| 743 | 743 | ||
| 744 | let file_cap = types.max_package_files() |
||
| 745 | mut pkg_files: [string] = new [string](file_cap) |
||
| 744 | mut total = 0 |
746 | mut total = 0 |
| 745 | mut i = 0 |
747 | mut i = 0 |
| 746 | while i < pkg_count |
748 | while i < pkg_count |
| 747 | mut pkg_files: [string] = new [string](8192) |
749 | let file_count = database.get_files_rooted(pkg_names[i], pkg_files, file_cap, root) |
| 748 | let file_count = database.get_files_rooted(pkg_names[i], pkg_files, 8192, root) |
||
| 749 | total = total + file_count |
750 | total = total + file_count |
| 750 | i = i + 1 |
751 | i = i + 1 |
| 751 | end while |
752 | end while |
| @@ -765,10 +766,11 @@ | @@ -765,10 +766,11 @@ | ||
| 765 | mut all_owners: [string] = new [string](alloc_size) |
766 | mut all_owners: [string] = new [string](alloc_size) |
| 766 | mut total_files = 0 |
767 | mut total_files = 0 |
| 767 | 768 | ||
| 769 | let file_cap = types.max_package_files() |
||
| 770 | mut pkg_files: [string] = new [string](file_cap) |
||
| 768 | mut i = 0 |
771 | mut i = 0 |
| 769 | while i < pkg_count and total_files < alloc_size - 256 |
772 | while i < pkg_count and total_files < alloc_size - 256 |
| 770 | mut pkg_files: [string] = new [string](8192) |
773 | let file_count = database.get_files_rooted(pkg_names[i], pkg_files, file_cap, root) |
| 771 | let file_count = database.get_files_rooted(pkg_names[i], pkg_files, 8192, root) |
||
| 772 | 774 | ||
| 773 | mut j = 0 |
775 | mut j = 0 |
| 774 | while j < file_count and total_files < alloc_size |
776 | while j < file_count and total_files < alloc_size |
| @@ -905,10 +907,11 @@ | @@ -905,10 +907,11 @@ | ||
| 905 | stats.package_count = database.list_installed(names, 1024) |
907 | stats.package_count = database.list_installed(names, 1024) |
| 906 | 908 | ||
| 907 | // Count files across all packages |
909 | // Count files across all packages |
| 910 | let file_cap = types.max_package_files() |
||
| 911 | mut files: [string] = new [string](file_cap) |
||
| 908 | mut i = 0 |
912 | mut i = 0 |
| 909 | while i < stats.package_count |
913 | while i < stats.package_count |
| 910 | mut files: [string] = new [string](4096) |
914 | let file_count = database.get_files(names[i], files, file_cap) |
| 911 | let file_count = database.get_files(names[i], files, 4096) |
||
| 912 | stats.file_count = stats.file_count + file_count |
915 | stats.file_count = stats.file_count + file_count |
| 913 | i = i + 1 |
916 | i = i + 1 |
| 914 | end while |
917 | end while |
| @@ -997,10 +1000,11 @@ | @@ -997,10 +1000,11 @@ | ||
| 997 | stats.package_count = database.list_installed_rooted(names, 1024, root) |
1000 | stats.package_count = database.list_installed_rooted(names, 1024, root) |
| 998 | 1001 | ||
| 999 | // Count files across all packages |
1002 | // Count files across all packages |
| 1003 | let file_cap = types.max_package_files() |
||
| 1004 | mut files: [string] = new [string](file_cap) |
||
| 1000 | mut i = 0 |
1005 | mut i = 0 |
| 1001 | while i < stats.package_count |
1006 | while i < stats.package_count |
| 1002 | mut files: [string] = new [string](4096) |
1007 | let file_count = database.get_files_rooted(names[i], files, file_cap, root) |
| 1003 | let file_count = database.get_files_rooted(names[i], files, 4096, root) |
||
| 1004 | stats.file_count = stats.file_count + file_count |
1008 | stats.file_count = stats.file_count + file_count |
| 1005 | i = i + 1 |
1009 | i = i + 1 |
| 1006 | end while |
1010 | end while |
| diff -r f6ecb7b9c2c8 -r 90ce6b0b95f1 src/types.reef | ||
| --- a/src/types.reef Wed Aug 26 15:29:04 2026 -0500 | ||
| +++ b/src/types.reef Wed Aug 26 19:32:24 2026 -0500 | ||
| @@ -64,6 +64,9 @@ | ||
| 64 | 64 | fn new_package_group(): PackageGroup |
| 65 | 65 | fn positional(opts: GlobalOptions, i: int): string |
| 66 | 66 | fn positional_count(opts: GlobalOptions): int |
| 67 | + | |
| 68 | + // Maximum files/dirs/symlinks in one package inventory (.MANIFEST, files.txt) |
|
| 69 | + fn max_package_files(): int |
|
| 67 | 70 | end export |
| 68 | 71 | |
| 69 | 72 | // Zygaena/Coral filesystem paths |
| @@ -368,6 +371,13 @@ | ||
| 368 | 371 | } |
| 369 | 372 | end new_port_result |
| 370 | 373 | |
| 374 | +// Per-package inventory bound. Callers allocate file/manifest arrays from this |
|
| 375 | +// rather than scattering 4096/8192 literals. 131072 is well above the |
|
| 376 | +// ~42308 members in hammerhead-basesystem (coral#1). |
|
| 377 | +fn max_package_files(): int |
|
| 378 | + return 131072 |
|
| 379 | +end max_package_files |
|
| 380 | + | |
| 371 | 381 | // Path getter functions |
| 372 | 382 | fn get_ports_dir(): string |
| 373 | 383 | return PORTS_DIR |
| Old | New | ||
|---|---|---|---|
| diff -r f6ecb7b9c2c8 -r 90ce6b0b95f1 src/types.reef | diff -r f6ecb7b9c2c8 -r 90ce6b0b95f1 src/types.reef | ||
| --- a/src/types.reef Wed Aug 26 15:29:04 2026 -0500 | --- a/src/types.reef Wed Aug 26 15:29:04 2026 -0500 | ||
| +++ b/src/types.reef Wed Aug 26 19:32:24 2026 -0500 | +++ b/src/types.reef Wed Aug 26 19:32:24 2026 -0500 | ||
| @@ -64,6 +64,9 @@ | @@ -64,6 +64,9 @@ | ||
| 64 | fn new_package_group(): PackageGroup |
64 | fn new_package_group(): PackageGroup |
| 65 | fn positional(opts: GlobalOptions, i: int): string |
65 | fn positional(opts: GlobalOptions, i: int): string |
| 66 | fn positional_count(opts: GlobalOptions): int |
66 | fn positional_count(opts: GlobalOptions): int |
| 67 | |||
| 68 | // Maximum files/dirs/symlinks in one package inventory (.MANIFEST, files.txt) |
||
| 69 | fn max_package_files(): int |
||
| 67 | end export |
70 | end export |
| 68 | 71 | ||
| 69 | // Zygaena/Coral filesystem paths |
72 | // Zygaena/Coral filesystem paths |
| @@ -368,6 +371,13 @@ | @@ -368,6 +371,13 @@ | ||
| 368 | } |
371 | } |
| 369 | end new_port_result |
372 | end new_port_result |
| 370 | 373 | ||
| 374 | // Per-package inventory bound. Callers allocate file/manifest arrays from this |
||
| 375 | // rather than scattering 4096/8192 literals. 131072 is well above the |
||
| 376 | // ~42308 members in hammerhead-basesystem (coral#1). |
||
| 377 | fn max_package_files(): int |
||
| 378 | return 131072 |
||
| 379 | end max_package_files |
||
| 380 | |||
| 371 | // Path getter functions |
381 | // Path getter functions |
| 372 | fn get_ports_dir(): string |
382 | fn get_ports_dir(): string |
| 373 | return PORTS_DIR |
383 | return PORTS_DIR |
| diff -r f6ecb7b9c2c8 -r 90ce6b0b95f1 src/util/mtree.reef | ||
| --- a/src/util/mtree.reef Wed Aug 26 15:29:04 2026 -0500 | ||
| +++ b/src/util/mtree.reef Wed Aug 26 19:32:24 2026 -0500 | ||
| @@ -17,6 +17,7 @@ | ||
| 17 | 17 | |
| 18 | 18 | module util.mtree |
| 19 | 19 | |
| 20 | +import types |
|
| 20 | 21 | import io.file |
| 21 | 22 | import io.dir |
| 22 | 23 | import io.path |
| @@ -34,12 +35,16 @@ | ||
| 34 | 35 | type ManifestEntry |
| 35 | 36 | |
| 36 | 37 | // Parse an mtree manifest string into an array of entries |
| 37 | - // Returns the number of entries parsed |
|
| 38 | + // Returns the number of entries parsed, or -1 if leftover real lines |
|
| 39 | + // remain after max_entries (fail closed; coral#1) |
|
| 38 | 40 | fn parse_manifest(content: string, entries: [ManifestEntry], max_entries: int): int |
| 39 | 41 | |
| 40 | 42 | // Generate an mtree manifest string from a package staging directory |
| 41 | 43 | fn generate_manifest(pkg_dir: string): string |
| 42 | 44 | |
| 45 | + // Same as generate_manifest, with an explicit entry cap (tests + fail-closed) |
|
| 46 | + fn generate_manifest_limited(pkg_dir: string, max: int): string |
|
| 47 | + | |
| 43 | 48 | // Accessors |
| 44 | 49 | fn entry_path(entry: ManifestEntry): string |
| 45 | 50 | fn entry_type(entry: ManifestEntry): string |
| @@ -117,9 +122,10 @@ | ||
| 117 | 122 | return 0 |
| 118 | 123 | end if |
| 119 | 124 | |
| 120 | - // Split into lines |
|
| 121 | - mut lines: [string] = new [string](max_entries + 64) |
|
| 122 | - let line_count = str.split(content, '\n', lines, max_entries + 64) |
|
| 125 | + // Split into lines. Extra slots cover the #mtree header and blank lines. |
|
| 126 | + let split_max = max_entries + 64 |
|
| 127 | + mut lines: [string] = new [string](split_max) |
|
| 128 | + let line_count = str.split(content, '\n', lines, split_max) |
|
| 123 | 129 | |
| 124 | 130 | mut count = 0 |
| 125 | 131 | mut i = 0 |
| @@ -147,6 +153,22 @@ | ||
| 147 | 153 | i = i + 1 |
| 148 | 154 | end while |
| 149 | 155 | |
| 156 | + // Fail closed: more real entries than max_entries, or the split itself |
|
| 157 | + // hit its cap so later lines were never seen. |
|
| 158 | + if count == max_entries |
|
| 159 | + mut j = i |
|
| 160 | + while j < line_count |
|
| 161 | + let rest = str.trim_ws(lines[j]) |
|
| 162 | + if str.length(rest) > 0 and rest[0] != '#' |
|
| 163 | + return -1 |
|
| 164 | + end if |
|
| 165 | + j = j + 1 |
|
| 166 | + end while |
|
| 167 | + if line_count == split_max |
|
| 168 | + return -1 |
|
| 169 | + end if |
|
| 170 | + end if |
|
| 171 | + | |
| 150 | 172 | return count |
| 151 | 173 | end parse_manifest |
| 152 | 174 | |
| @@ -209,19 +231,29 @@ | ||
| 209 | 231 | // Walks the directory tree, collects entries, sorts them, and returns the |
| 210 | 232 | // formatted manifest string. |
| 211 | 233 | fn generate_manifest(pkg_dir: string): string |
| 212 | - // Collect entries by walking the directory tree |
|
| 213 | - mut entries: [ManifestEntry] = new [ManifestEntry](8192) |
|
| 214 | - let count = walk_directory(pkg_dir, pkg_dir, entries, 0, 8192) |
|
| 234 | + return generate_manifest_limited(pkg_dir, types.max_package_files()) |
|
| 235 | +end generate_manifest |
|
| 236 | + | |
| 237 | +fn generate_manifest_limited(pkg_dir: string, max: int): string |
|
| 238 | + if max <= 0 |
|
| 239 | + return "" |
|
| 240 | + end if |
|
| 241 | + | |
| 242 | + // Walk one past the cap so a full tree is distinguishable from truncation. |
|
| 243 | + mut entries: [ManifestEntry] = new [ManifestEntry](max + 1) |
|
| 244 | + let count = walk_directory(pkg_dir, pkg_dir, entries, 0, max + 1) |
|
| 215 | 245 | |
| 216 | 246 | if count == 0 |
| 217 | 247 | return "" |
| 218 | 248 | end if |
| 249 | + if count > max |
|
| 250 | + return "" |
|
| 251 | + end if |
|
| 219 | 252 | |
| 220 | - // Sort entries by path (simple insertion sort — fine for package file counts) |
|
| 221 | 253 | sort_entries(entries, count) |
| 222 | 254 | |
| 223 | - // Build the output string using StringBuilder to avoid O(n²) heap |
|
| 224 | - // fragmentation from repeated string concatenation (Reef BUG-027) |
|
| 255 | + // StringBuilder avoids O(n²) heap fragmentation from repeated concat |
|
| 256 | + // (Reef BUG-027) |
|
| 225 | 257 | let builder = sb.sb_new() |
| 226 | 258 | sb.sb_append(builder, "#mtree\n") |
| 227 | 259 | mut i = 0 |
| @@ -232,7 +264,7 @@ | ||
| 232 | 264 | end while |
| 233 | 265 | |
| 234 | 266 | return sb.sb_build(builder) |
| 235 | -end generate_manifest |
|
| 267 | +end generate_manifest_limited |
|
| 236 | 268 | |
| 237 | 269 | // Recursively walk a directory, collecting ManifestEntry records. |
| 238 | 270 | // base_dir is the package root (for computing relative paths). |
| @@ -390,20 +422,58 @@ | ||
| 390 | 422 | // Sorting |
| 391 | 423 | // ============================================================================ |
| 392 | 424 | |
| 393 | -// Insertion sort entries by path (lexicographic). |
|
| 394 | -// Package file counts are typically <1000, so insertion sort is adequate. |
|
| 425 | +// Heapsort by path (lexicographic). Insertion sort is O(n²) and too slow |
|
| 426 | +// for OS-sized inventories (~42k members). |
|
| 427 | +proc swap_entries(entries: [ManifestEntry], i: int, j: int) |
|
| 428 | + let tmp = entries[i] |
|
| 429 | + entries[i] = entries[j] |
|
| 430 | + entries[j] = tmp |
|
| 431 | +end swap_entries |
|
| 432 | + | |
| 433 | +proc sift_down(entries: [ManifestEntry], start: int, heap_size: int) |
|
| 434 | + mut root = start |
|
| 435 | + mut more = true |
|
| 436 | + while more |
|
| 437 | + let left = root * 2 + 1 |
|
| 438 | + if left >= heap_size |
|
| 439 | + more = false |
|
| 440 | + else |
|
| 441 | + mut cand = root |
|
| 442 | + if str.compare(entries[cand].epath, entries[left].epath) < 0 |
|
| 443 | + cand = left |
|
| 444 | + end if |
|
| 445 | + let right = left + 1 |
|
| 446 | + if right < heap_size |
|
| 447 | + if str.compare(entries[cand].epath, entries[right].epath) < 0 |
|
| 448 | + cand = right |
|
| 449 | + end if |
|
| 450 | + end if |
|
| 451 | + if cand == root |
|
| 452 | + more = false |
|
| 453 | + else |
|
| 454 | + swap_entries(entries, root, cand) |
|
| 455 | + root = cand |
|
| 456 | + end if |
|
| 457 | + end if |
|
| 458 | + end while |
|
| 459 | +end sift_down |
|
| 460 | + | |
| 395 | 461 | proc sort_entries(entries: [ManifestEntry], count: int) |
| 396 | - mut i = 1 |
|
| 397 | - while i < count |
|
| 398 | - let key_entry = entries[i] |
|
| 399 | - let key_path = entries[i].epath |
|
| 400 | - mut j = i - 1 |
|
| 401 | - while j >= 0 and str.compare(entries[j].epath, key_path) > 0 |
|
| 402 | - entries[j + 1] = entries[j] |
|
| 403 | - j = j - 1 |
|
| 404 | - end while |
|
| 405 | - entries[j + 1] = key_entry |
|
| 406 | - i = i + 1 |
|
| 462 | + if count <= 1 |
|
| 463 | + return |
|
| 464 | + end if |
|
| 465 | + | |
| 466 | + mut i = count / 2 - 1 |
|
| 467 | + while i >= 0 |
|
| 468 | + sift_down(entries, i, count) |
|
| 469 | + i = i - 1 |
|
| 470 | + end while |
|
| 471 | + | |
| 472 | + mut end_idx = count - 1 |
|
| 473 | + while end_idx > 0 |
|
| 474 | + swap_entries(entries, 0, end_idx) |
|
| 475 | + sift_down(entries, 0, end_idx) |
|
| 476 | + end_idx = end_idx - 1 |
|
| 407 | 477 | end while |
| 408 | 478 | end sort_entries |
| 409 | 479 |
| Old | New | ||
|---|---|---|---|
| diff -r f6ecb7b9c2c8 -r 90ce6b0b95f1 src/util/mtree.reef | diff -r f6ecb7b9c2c8 -r 90ce6b0b95f1 src/util/mtree.reef | ||
| --- a/src/util/mtree.reef Wed Aug 26 15:29:04 2026 -0500 | --- a/src/util/mtree.reef Wed Aug 26 15:29:04 2026 -0500 | ||
| +++ b/src/util/mtree.reef Wed Aug 26 19:32:24 2026 -0500 | +++ b/src/util/mtree.reef Wed Aug 26 19:32:24 2026 -0500 | ||
| @@ -17,6 +17,7 @@ | @@ -17,6 +17,7 @@ | ||
| 17 | 17 | ||
| 18 | module util.mtree |
18 | module util.mtree |
| 19 | 19 | ||
| 20 | import types |
||
| 20 | import io.file |
21 | import io.file |
| 21 | import io.dir |
22 | import io.dir |
| 22 | import io.path |
23 | import io.path |
| @@ -34,12 +35,16 @@ | @@ -34,12 +35,16 @@ | ||
| 34 | type ManifestEntry |
35 | type ManifestEntry |
| 35 | 36 | ||
| 36 | // Parse an mtree manifest string into an array of entries |
37 | // Parse an mtree manifest string into an array of entries |
| 37 | // Returns the number of entries parsed |
38 | // Returns the number of entries parsed, or -1 if leftover real lines |
| 39 | // remain after max_entries (fail closed; coral#1) |
||
| 38 | fn parse_manifest(content: string, entries: [ManifestEntry], max_entries: int): int |
40 | fn parse_manifest(content: string, entries: [ManifestEntry], max_entries: int): int |
| 39 | 41 | ||
| 40 | // Generate an mtree manifest string from a package staging directory |
42 | // Generate an mtree manifest string from a package staging directory |
| 41 | fn generate_manifest(pkg_dir: string): string |
43 | fn generate_manifest(pkg_dir: string): string |
| 42 | 44 | ||
| 45 | // Same as generate_manifest, with an explicit entry cap (tests + fail-closed) |
||
| 46 | fn generate_manifest_limited(pkg_dir: string, max: int): string |
||
| 47 | |||
| 43 | // Accessors |
48 | // Accessors |
| 44 | fn entry_path(entry: ManifestEntry): string |
49 | fn entry_path(entry: ManifestEntry): string |
| 45 | fn entry_type(entry: ManifestEntry): string |
50 | fn entry_type(entry: ManifestEntry): string |
| @@ -117,9 +122,10 @@ | @@ -117,9 +122,10 @@ | ||
| 117 | return 0 |
122 | return 0 |
| 118 | end if |
123 | end if |
| 119 | 124 | ||
| 120 | // Split into lines |
125 | // Split into lines. Extra slots cover the #mtree header and blank lines. |
| 121 | mut lines: [string] = new [string](max_entries + 64) |
126 | let split_max = max_entries + 64 |
| 122 | let line_count = str.split(content, '\n', lines, max_entries + 64) |
127 | mut lines: [string] = new [string](split_max) |
| 128 | let line_count = str.split(content, '\n', lines, split_max) |
||
| 123 | 129 | ||
| 124 | mut count = 0 |
130 | mut count = 0 |
| 125 | mut i = 0 |
131 | mut i = 0 |
| @@ -147,6 +153,22 @@ | @@ -147,6 +153,22 @@ | ||
| 147 | i = i + 1 |
153 | i = i + 1 |
| 148 | end while |
154 | end while |
| 149 | 155 | ||
| 156 | // Fail closed: more real entries than max_entries, or the split itself |
||
| 157 | // hit its cap so later lines were never seen. |
||
| 158 | if count == max_entries |
||
| 159 | mut j = i |
||
| 160 | while j < line_count |
||
| 161 | let rest = str.trim_ws(lines[j]) |
||
| 162 | if str.length(rest) > 0 and rest[0] != '#' |
||
| 163 | return -1 |
||
| 164 | end if |
||
| 165 | j = j + 1 |
||
| 166 | end while |
||
| 167 | if line_count == split_max |
||
| 168 | return -1 |
||
| 169 | end if |
||
| 170 | end if |
||
| 171 | |||
| 150 | return count |
172 | return count |
| 151 | end parse_manifest |
173 | end parse_manifest |
| 152 | 174 | ||
| @@ -209,19 +231,29 @@ | @@ -209,19 +231,29 @@ | ||
| 209 | // Walks the directory tree, collects entries, sorts them, and returns the |
231 | // Walks the directory tree, collects entries, sorts them, and returns the |
| 210 | // formatted manifest string. |
232 | // formatted manifest string. |
| 211 | fn generate_manifest(pkg_dir: string): string |
233 | fn generate_manifest(pkg_dir: string): string |
| 212 | // Collect entries by walking the directory tree |
234 | return generate_manifest_limited(pkg_dir, types.max_package_files()) |
| 213 | mut entries: [ManifestEntry] = new [ManifestEntry](8192) |
235 | end generate_manifest |
| 214 | let count = walk_directory(pkg_dir, pkg_dir, entries, 0, 8192) |
236 | |
| 237 | fn generate_manifest_limited(pkg_dir: string, max: int): string |
||
| 238 | if max <= 0 |
||
| 239 | return "" |
||
| 240 | end if |
||
| 241 | |||
| 242 | // Walk one past the cap so a full tree is distinguishable from truncation. |
||
| 243 | mut entries: [ManifestEntry] = new [ManifestEntry](max + 1) |
||
| 244 | let count = walk_directory(pkg_dir, pkg_dir, entries, 0, max + 1) |
||
| 215 | 245 | ||
| 216 | if count == 0 |
246 | if count == 0 |
| 217 | return "" |
247 | return "" |
| 218 | end if |
248 | end if |
| 249 | if count > max |
||
| 250 | return "" |
||
| 251 | end if |
||
| 219 | 252 | ||
| 220 | // Sort entries by path (simple insertion sort — fine for package file counts) |
||
| 221 | sort_entries(entries, count) |
253 | sort_entries(entries, count) |
| 222 | 254 | ||
| 223 | // Build the output string using StringBuilder to avoid O(n²) heap |
255 | // StringBuilder avoids O(n²) heap fragmentation from repeated concat |
| 224 | // fragmentation from repeated string concatenation (Reef BUG-027) |
256 | // (Reef BUG-027) |
| 225 | let builder = sb.sb_new() |
257 | let builder = sb.sb_new() |
| 226 | sb.sb_append(builder, "#mtree\n") |
258 | sb.sb_append(builder, "#mtree\n") |
| 227 | mut i = 0 |
259 | mut i = 0 |
| @@ -232,7 +264,7 @@ | @@ -232,7 +264,7 @@ | ||
| 232 | end while |
264 | end while |
| 233 | 265 | ||
| 234 | return sb.sb_build(builder) |
266 | return sb.sb_build(builder) |
| 235 | end generate_manifest |
267 | end generate_manifest_limited |
| 236 | 268 | ||
| 237 | // Recursively walk a directory, collecting ManifestEntry records. |
269 | // Recursively walk a directory, collecting ManifestEntry records. |
| 238 | // base_dir is the package root (for computing relative paths). |
270 | // base_dir is the package root (for computing relative paths). |
| @@ -390,20 +422,58 @@ | @@ -390,20 +422,58 @@ | ||
| 390 | // Sorting |
422 | // Sorting |
| 391 | // ============================================================================ |
423 | // ============================================================================ |
| 392 | 424 | ||
| 393 | // Insertion sort entries by path (lexicographic). |
425 | // Heapsort by path (lexicographic). Insertion sort is O(n²) and too slow |
| 394 | // Package file counts are typically <1000, so insertion sort is adequate. |
426 | // for OS-sized inventories (~42k members). |
| 427 | proc swap_entries(entries: [ManifestEntry], i: int, j: int) |
||
| 428 | let tmp = entries[i] |
||
| 429 | entries[i] = entries[j] |
||
| 430 | entries[j] = tmp |
||
| 431 | end swap_entries |
||
| 432 | |||
| 433 | proc sift_down(entries: [ManifestEntry], start: int, heap_size: int) |
||
| 434 | mut root = start |
||
| 435 | mut more = true |
||
| 436 | while more |
||
| 437 | let left = root * 2 + 1 |
||
| 438 | if left >= heap_size |
||
| 439 | more = false |
||
| 440 | else |
||
| 441 | mut cand = root |
||
| 442 | if str.compare(entries[cand].epath, entries[left].epath) < 0 |
||
| 443 | cand = left |
||
| 444 | end if |
||
| 445 | let right = left + 1 |
||
| 446 | if right < heap_size |
||
| 447 | if str.compare(entries[cand].epath, entries[right].epath) < 0 |
||
| 448 | cand = right |
||
| 449 | end if |
||
| 450 | end if |
||
| 451 | if cand == root |
||
| 452 | more = false |
||
| 453 | else |
||
| 454 | swap_entries(entries, root, cand) |
||
| 455 | root = cand |
||
| 456 | end if |
||
| 457 | end if |
||
| 458 | end while |
||
| 459 | end sift_down |
||
| 460 | |||
| 395 | proc sort_entries(entries: [ManifestEntry], count: int) |
461 | proc sort_entries(entries: [ManifestEntry], count: int) |
| 396 | mut i = 1 |
462 | if count <= 1 |
| 397 | while i < count |
463 | return |
| 398 | let key_entry = entries[i] |
464 | end if |
| 399 | let key_path = entries[i].epath |
465 | |
| 400 | mut j = i - 1 |
466 | mut i = count / 2 - 1 |
| 401 | while j >= 0 and str.compare(entries[j].epath, key_path) > 0 |
467 | while i >= 0 |
| 402 | entries[j + 1] = entries[j] |
468 | sift_down(entries, i, count) |
| 403 | j = j - 1 |
469 | i = i - 1 |
| 404 | end while |
470 | end while |
| 405 | entries[j + 1] = key_entry |
471 | |
| 406 | i = i + 1 |
472 | mut end_idx = count - 1 |
| 473 | while end_idx > 0 |
||
| 474 | swap_entries(entries, 0, end_idx) |
||
| 475 | sift_down(entries, 0, end_idx) |
||
| 476 | end_idx = end_idx - 1 |
||
| 407 | end while |
477 | end while |
| 408 | end sort_entries |
478 | end sort_entries |
| 409 | 479 |
| diff -r f6ecb7b9c2c8 -r 90ce6b0b95f1 test/mtree_cap_test.reef | ||
| --- /dev/null Thu Jan 01 00:00:00 1970 +0000 | ||
| +++ b/test/mtree_cap_test.reef Wed Aug 26 19:32:24 2026 -0500 | ||
| @@ -0,0 +1,196 @@ | ||
| 1 | +/****************************************************************************** |
|
| 2 | + __ ____ __ |
|
| 3 | + / / ___ ____ _/ __/_____________ _/ /__ |
|
| 4 | + / / / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \ |
|
| 5 | + / /___/ __/ /_/ / __(__ ) /__/ /_/ / / __/ |
|
| 6 | + /_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/ |
|
| 7 | + | |
| 8 | + (C)opyright 2025, Leafscale, LLC - https://www.leafscale.com |
|
| 9 | + | |
| 10 | + Project: Zygaena |
|
| 11 | + Filename: mtree_cap_test.reef |
|
| 12 | + Authors: Chris Tusa <chris.tusa@leafscale.com> |
|
| 13 | + License: <see LICENSE file included with this source code> |
|
| 14 | +Description: Regression test for coral#1 (.MANIFEST 8192-entry silent cap) |
|
| 15 | + | |
| 16 | + generate_manifest allocated [ManifestEntry](8192) and walk_directory |
|
| 17 | + stopped at that bound without error. Install is manifest-driven, so a |
|
| 18 | + truncated inventory omitted the rest of the tree (kernel, libc, Limine |
|
| 19 | + on hammerhead-basesystem ~42308 members). |
|
| 20 | + | |
| 21 | + This test pins: |
|
| 22 | + - parse leftover real lines fail closed (return -1) |
|
| 23 | + - generate inventories more than 8192 files |
|
| 24 | + - generate fail-closed when the walk would exceed an explicit max |
|
| 25 | + | |
| 26 | +******************************************************************************/ |
|
| 27 | + | |
| 28 | +import core.str |
|
| 29 | +import core.convert as cv |
|
| 30 | +import core.result as res |
|
| 31 | +import io.file |
|
| 32 | +import io.dir |
|
| 33 | +import io.path |
|
| 34 | +import util.mtree |
|
| 35 | +import types |
|
| 36 | +import sys.process |
|
| 37 | + | |
| 38 | +let failures: [int] = new [int](1) |
|
| 39 | + | |
| 40 | +proc check(label: string, actual: int, expected: int) |
|
| 41 | + if actual == expected |
|
| 42 | + println(" ok " + label + " = " + cv.to_string(actual)) |
|
| 43 | + else |
|
| 44 | + println(" FAIL " + label + " = " + cv.to_string(actual) + " (expected " + cv.to_string(expected) + ")") |
|
| 45 | + failures[0] = failures[0] + 1 |
|
| 46 | + end if |
|
| 47 | +end check |
|
| 48 | + | |
| 49 | +proc check_true(label: string, actual: bool) |
|
| 50 | + if actual |
|
| 51 | + println(" ok " + label) |
|
| 52 | + else |
|
| 53 | + println(" FAIL " + label) |
|
| 54 | + failures[0] = failures[0] + 1 |
|
| 55 | + end if |
|
| 56 | +end check_true |
|
| 57 | + | |
| 58 | +// coral#1: leftover inventory after max_entries must not look like success. |
|
| 59 | +proc test_parse_overflow() |
|
| 60 | + println("parse_manifest leftover lines fail closed:") |
|
| 61 | + | |
| 62 | + let three = "#mtree\n./a type=file mode=0644 uname=root gname=root\n./b type=file mode=0644 uname=root gname=root\n./c type=file mode=0644 uname=root gname=root\n" |
|
| 63 | + mut tight: [mtree.ManifestEntry] = new [mtree.ManifestEntry](2) |
|
| 64 | + check("3 real lines with max=2", mtree.parse_manifest(three, tight, 2), -1) |
|
| 65 | + | |
| 66 | + mut room: [mtree.ManifestEntry] = new [mtree.ManifestEntry](8) |
|
| 67 | + check("3 real lines with max=8", mtree.parse_manifest(three, room, 8), 3) |
|
| 68 | + | |
| 69 | + let two = "#mtree\n./a type=file mode=0644 uname=root gname=root\n./b type=file mode=0644 uname=root gname=root\n" |
|
| 70 | + mut exact: [mtree.ManifestEntry] = new [mtree.ManifestEntry](2) |
|
| 71 | + check("exactly 2 real lines with max=2", mtree.parse_manifest(two, exact, 2), 2) |
|
| 72 | +end test_parse_overflow |
|
| 73 | + | |
| 74 | +fn count_real_lines(content: string, max_lines: int): int |
|
| 75 | + mut lines: [string] = new [string](max_lines) |
|
| 76 | + let n = str.split(content, '\n', lines, max_lines) |
|
| 77 | + mut count = 0 |
|
| 78 | + mut i = 0 |
|
| 79 | + while i < n |
|
| 80 | + let line = str.trim_ws(lines[i]) |
|
| 81 | + if str.length(line) > 0 and line[0] != '#' |
|
| 82 | + count = count + 1 |
|
| 83 | + end if |
|
| 84 | + i = i + 1 |
|
| 85 | + end while |
|
| 86 | + return count |
|
| 87 | +end count_real_lines |
|
| 88 | + | |
| 89 | +fn fixture_dir(): string |
|
| 90 | + return "/tmp/coral-mtree-cap-test" |
|
| 91 | +end fixture_dir |
|
| 92 | + | |
| 93 | +proc wipe_fixture() |
|
| 94 | + let pid = process.process_spawn_shell("/bin/rm -rf " + fixture_dir()) |
|
| 95 | + process.process_wait(pid) |
|
| 96 | +end wipe_fixture |
|
| 97 | + | |
| 98 | +// coral#1: more than 8192 staged files must all appear in the manifest. |
|
| 99 | +proc test_generate_above_old_cap() |
|
| 100 | + println("generate_manifest inventories >8192 files:") |
|
| 101 | + | |
| 102 | + wipe_fixture() |
|
| 103 | + if not res.is_ok(dir.create_dir_all(fixture_dir())) |
|
| 104 | + println(" FAIL could not create " + fixture_dir()) |
|
| 105 | + failures[0] = failures[0] + 1 |
|
| 106 | + return |
|
| 107 | + end if |
|
| 108 | + | |
| 109 | + let want = 8500 |
|
| 110 | + mut i = 0 |
|
| 111 | + while i < want |
|
| 112 | + let p = path.join_path(fixture_dir(), "f" + cv.to_string(i) + ".txt") |
|
| 113 | + if not res.is_ok(file.writeFile(p, "x")) |
|
| 114 | + println(" FAIL write " + p) |
|
| 115 | + failures[0] = failures[0] + 1 |
|
| 116 | + wipe_fixture() |
|
| 117 | + return |
|
| 118 | + end if |
|
| 119 | + i = i + 1 |
|
| 120 | + end while |
|
| 121 | + | |
| 122 | + let content = mtree.generate_manifest(fixture_dir()) |
|
| 123 | + let got = count_real_lines(content, 20000) |
|
| 124 | + check("8500-file tree entry count", got, want) |
|
| 125 | + | |
| 126 | + // A high index must be present; the old 8192 walk stopped on an unsorted |
|
| 127 | + // directory listing and dropped the rest. |
|
| 128 | + check_true("contains f8499.txt", str.contains(content, "f8499.txt")) |
|
| 129 | + | |
| 130 | + wipe_fixture() |
|
| 131 | +end test_generate_above_old_cap |
|
| 132 | + | |
| 133 | +// Fail closed: a walk that would exceed max must not return a prefix. |
|
| 134 | +proc test_generate_overflow() |
|
| 135 | + println("generate_manifest_limited fail-closed on overflow:") |
|
| 136 | + | |
| 137 | + wipe_fixture() |
|
| 138 | + if not res.is_ok(dir.create_dir_all(fixture_dir())) |
|
| 139 | + println(" FAIL could not create " + fixture_dir()) |
|
| 140 | + failures[0] = failures[0] + 1 |
|
| 141 | + return |
|
| 142 | + end if |
|
| 143 | + | |
| 144 | + mut i = 0 |
|
| 145 | + while i < 20 |
|
| 146 | + let p = path.join_path(fixture_dir(), "n" + cv.to_string(i)) |
|
| 147 | + file.writeFile(p, "x") |
|
| 148 | + i = i + 1 |
|
| 149 | + end while |
|
| 150 | + | |
| 151 | + let overflowed = mtree.generate_manifest_limited(fixture_dir(), 10) |
|
| 152 | + check("overflow returns empty", str.length(overflowed), 0) |
|
| 153 | + | |
| 154 | + let ok = mtree.generate_manifest_limited(fixture_dir(), 64) |
|
| 155 | + check("20 files with max=64", count_real_lines(ok, 128), 20) |
|
| 156 | + | |
| 157 | + mut ents: [mtree.ManifestEntry] = new [mtree.ManifestEntry](64) |
|
| 158 | + let n = mtree.parse_manifest(ok, ents, 64) |
|
| 159 | + mut sorted = true |
|
| 160 | + mut k = 1 |
|
| 161 | + while k < n |
|
| 162 | + if str.compare(mtree.entry_path(ents[k - 1]), mtree.entry_path(ents[k])) > 0 |
|
| 163 | + sorted = false |
|
| 164 | + end if |
|
| 165 | + k = k + 1 |
|
| 166 | + end while |
|
| 167 | + check_true("limited generate is sorted by path", sorted) |
|
| 168 | + | |
| 169 | + wipe_fixture() |
|
| 170 | +end test_generate_overflow |
|
| 171 | + | |
| 172 | +proc test_shared_cap() |
|
| 173 | + println("shared package-file cap:") |
|
| 174 | + check_true("max_package_files > 42308", types.max_package_files() > 42308) |
|
| 175 | + check_true("max_package_files >= 131072", types.max_package_files() >= 131072) |
|
| 176 | +end test_shared_cap |
|
| 177 | + | |
| 178 | +proc main() |
|
| 179 | + println("=== Coral mtree cap test (coral#1) ===") |
|
| 180 | + println("") |
|
| 181 | + | |
| 182 | + test_parse_overflow() |
|
| 183 | + println("") |
|
| 184 | + test_shared_cap() |
|
| 185 | + println("") |
|
| 186 | + test_generate_overflow() |
|
| 187 | + println("") |
|
| 188 | + test_generate_above_old_cap() |
|
| 189 | + println("") |
|
| 190 | + | |
| 191 | + if failures[0] == 0 |
|
| 192 | + println("PASS: all checks passed") |
|
| 193 | + else |
|
| 194 | + println("FAIL: " + cv.to_string(failures[0]) + " check(s) failed") |
|
| 195 | + end if |
|
| 196 | +end main |
|
| 197 |
| Old | New | ||
|---|---|---|---|
| diff -r f6ecb7b9c2c8 -r 90ce6b0b95f1 test/mtree_cap_test.reef | diff -r f6ecb7b9c2c8 -r 90ce6b0b95f1 test/mtree_cap_test.reef | ||
| --- /dev/null Thu Jan 01 00:00:00 1970 +0000 | --- /dev/null Thu Jan 01 00:00:00 1970 +0000 | ||
| +++ b/test/mtree_cap_test.reef Wed Aug 26 19:32:24 2026 -0500 | +++ b/test/mtree_cap_test.reef Wed Aug 26 19:32:24 2026 -0500 | ||
| @@ -0,0 +1,196 @@ | @@ -0,0 +1,196 @@ | ||
| 1 | /****************************************************************************** |
||
| 2 | __ ____ __ |
||
| 3 | / / ___ ____ _/ __/_____________ _/ /__ |
||
| 4 | / / / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \ |
||
| 5 | / /___/ __/ /_/ / __(__ ) /__/ /_/ / / __/ |
||
| 6 | /_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/ |
||
| 7 | |||
| 8 | (C)opyright 2025, Leafscale, LLC - https://www.leafscale.com |
||
| 9 | |||
| 10 | Project: Zygaena |
||
| 11 | Filename: mtree_cap_test.reef |
||
| 12 | Authors: Chris Tusa <chris.tusa@leafscale.com> |
||
| 13 | License: <see LICENSE file included with this source code> |
||
| 14 | Description: Regression test for coral#1 (.MANIFEST 8192-entry silent cap) |
||
| 15 | |||
| 16 | generate_manifest allocated [ManifestEntry](8192) and walk_directory |
||
| 17 | stopped at that bound without error. Install is manifest-driven, so a |
||
| 18 | truncated inventory omitted the rest of the tree (kernel, libc, Limine |
||
| 19 | on hammerhead-basesystem ~42308 members). |
||
| 20 | |||
| 21 | This test pins: |
||
| 22 | - parse leftover real lines fail closed (return -1) |
||
| 23 | - generate inventories more than 8192 files |
||
| 24 | - generate fail-closed when the walk would exceed an explicit max |
||
| 25 | |||
| 26 | ******************************************************************************/ |
||
| 27 | |||
| 28 | import core.str |
||
| 29 | import core.convert as cv |
||
| 30 | import core.result as res |
||
| 31 | import io.file |
||
| 32 | import io.dir |
||
| 33 | import io.path |
||
| 34 | import util.mtree |
||
| 35 | import types |
||
| 36 | import sys.process |
||
| 37 | |||
| 38 | let failures: [int] = new [int](1) |
||
| 39 | |||
| 40 | proc check(label: string, actual: int, expected: int) |
||
| 41 | if actual == expected |
||
| 42 | println(" ok " + label + " = " + cv.to_string(actual)) |
||
| 43 | else |
||
| 44 | println(" FAIL " + label + " = " + cv.to_string(actual) + " (expected " + cv.to_string(expected) + ")") |
||
| 45 | failures[0] = failures[0] + 1 |
||
| 46 | end if |
||
| 47 | end check |
||
| 48 | |||
| 49 | proc check_true(label: string, actual: bool) |
||
| 50 | if actual |
||
| 51 | println(" ok " + label) |
||
| 52 | else |
||
| 53 | println(" FAIL " + label) |
||
| 54 | failures[0] = failures[0] + 1 |
||
| 55 | end if |
||
| 56 | end check_true |
||
| 57 | |||
| 58 | // coral#1: leftover inventory after max_entries must not look like success. |
||
| 59 | proc test_parse_overflow() |
||
| 60 | println("parse_manifest leftover lines fail closed:") |
||
| 61 | |||
| 62 | let three = "#mtree\n./a type=file mode=0644 uname=root gname=root\n./b type=file mode=0644 uname=root gname=root\n./c type=file mode=0644 uname=root gname=root\n" |
||
| 63 | mut tight: [mtree.ManifestEntry] = new [mtree.ManifestEntry](2) |
||
| 64 | check("3 real lines with max=2", mtree.parse_manifest(three, tight, 2), -1) |
||
| 65 | |||
| 66 | mut room: [mtree.ManifestEntry] = new [mtree.ManifestEntry](8) |
||
| 67 | check("3 real lines with max=8", mtree.parse_manifest(three, room, 8), 3) |
||
| 68 | |||
| 69 | let two = "#mtree\n./a type=file mode=0644 uname=root gname=root\n./b type=file mode=0644 uname=root gname=root\n" |
||
| 70 | mut exact: [mtree.ManifestEntry] = new [mtree.ManifestEntry](2) |
||
| 71 | check("exactly 2 real lines with max=2", mtree.parse_manifest(two, exact, 2), 2) |
||
| 72 | end test_parse_overflow |
||
| 73 | |||
| 74 | fn count_real_lines(content: string, max_lines: int): int |
||
| 75 | mut lines: [string] = new [string](max_lines) |
||
| 76 | let n = str.split(content, '\n', lines, max_lines) |
||
| 77 | mut count = 0 |
||
| 78 | mut i = 0 |
||
| 79 | while i < n |
||
| 80 | let line = str.trim_ws(lines[i]) |
||
| 81 | if str.length(line) > 0 and line[0] != '#' |
||
| 82 | count = count + 1 |
||
| 83 | end if |
||
| 84 | i = i + 1 |
||
| 85 | end while |
||
| 86 | return count |
||
| 87 | end count_real_lines |
||
| 88 | |||
| 89 | fn fixture_dir(): string |
||
| 90 | return "/tmp/coral-mtree-cap-test" |
||
| 91 | end fixture_dir |
||
| 92 | |||
| 93 | proc wipe_fixture() |
||
| 94 | let pid = process.process_spawn_shell("/bin/rm -rf " + fixture_dir()) |
||
| 95 | process.process_wait(pid) |
||
| 96 | end wipe_fixture |
||
| 97 | |||
| 98 | // coral#1: more than 8192 staged files must all appear in the manifest. |
||
| 99 | proc test_generate_above_old_cap() |
||
| 100 | println("generate_manifest inventories >8192 files:") |
||
| 101 | |||
| 102 | wipe_fixture() |
||
| 103 | if not res.is_ok(dir.create_dir_all(fixture_dir())) |
||
| 104 | println(" FAIL could not create " + fixture_dir()) |
||
| 105 | failures[0] = failures[0] + 1 |
||
| 106 | return |
||
| 107 | end if |
||
| 108 | |||
| 109 | let want = 8500 |
||
| 110 | mut i = 0 |
||
| 111 | while i < want |
||
| 112 | let p = path.join_path(fixture_dir(), "f" + cv.to_string(i) + ".txt") |
||
| 113 | if not res.is_ok(file.writeFile(p, "x")) |
||
| 114 | println(" FAIL write " + p) |
||
| 115 | failures[0] = failures[0] + 1 |
||
| 116 | wipe_fixture() |
||
| 117 | return |
||
| 118 | end if |
||
| 119 | i = i + 1 |
||
| 120 | end while |
||
| 121 | |||
| 122 | let content = mtree.generate_manifest(fixture_dir()) |
||
| 123 | let got = count_real_lines(content, 20000) |
||
| 124 | check("8500-file tree entry count", got, want) |
||
| 125 | |||
| 126 | // A high index must be present; the old 8192 walk stopped on an unsorted |
||
| 127 | // directory listing and dropped the rest. |
||
| 128 | check_true("contains f8499.txt", str.contains(content, "f8499.txt")) |
||
| 129 | |||
| 130 | wipe_fixture() |
||
| 131 | end test_generate_above_old_cap |
||
| 132 | |||
| 133 | // Fail closed: a walk that would exceed max must not return a prefix. |
||
| 134 | proc test_generate_overflow() |
||
| 135 | println("generate_manifest_limited fail-closed on overflow:") |
||
| 136 | |||
| 137 | wipe_fixture() |
||
| 138 | if not res.is_ok(dir.create_dir_all(fixture_dir())) |
||
| 139 | println(" FAIL could not create " + fixture_dir()) |
||
| 140 | failures[0] = failures[0] + 1 |
||
| 141 | return |
||
| 142 | end if |
||
| 143 | |||
| 144 | mut i = 0 |
||
| 145 | while i < 20 |
||
| 146 | let p = path.join_path(fixture_dir(), "n" + cv.to_string(i)) |
||
| 147 | file.writeFile(p, "x") |
||
| 148 | i = i + 1 |
||
| 149 | end while |
||
| 150 | |||
| 151 | let overflowed = mtree.generate_manifest_limited(fixture_dir(), 10) |
||
| 152 | check("overflow returns empty", str.length(overflowed), 0) |
||
| 153 | |||
| 154 | let ok = mtree.generate_manifest_limited(fixture_dir(), 64) |
||
| 155 | check("20 files with max=64", count_real_lines(ok, 128), 20) |
||
| 156 | |||
| 157 | mut ents: [mtree.ManifestEntry] = new [mtree.ManifestEntry](64) |
||
| 158 | let n = mtree.parse_manifest(ok, ents, 64) |
||
| 159 | mut sorted = true |
||
| 160 | mut k = 1 |
||
| 161 | while k < n |
||
| 162 | if str.compare(mtree.entry_path(ents[k - 1]), mtree.entry_path(ents[k])) > 0 |
||
| 163 | sorted = false |
||
| 164 | end if |
||
| 165 | k = k + 1 |
||
| 166 | end while |
||
| 167 | check_true("limited generate is sorted by path", sorted) |
||
| 168 | |||
| 169 | wipe_fixture() |
||
| 170 | end test_generate_overflow |
||
| 171 | |||
| 172 | proc test_shared_cap() |
||
| 173 | println("shared package-file cap:") |
||
| 174 | check_true("max_package_files > 42308", types.max_package_files() > 42308) |
||
| 175 | check_true("max_package_files >= 131072", types.max_package_files() >= 131072) |
||
| 176 | end test_shared_cap |
||
| 177 | |||
| 178 | proc main() |
||
| 179 | println("=== Coral mtree cap test (coral#1) ===") |
||
| 180 | println("") |
||
| 181 | |||
| 182 | test_parse_overflow() |
||
| 183 | println("") |
||
| 184 | test_shared_cap() |
||
| 185 | println("") |
||
| 186 | test_generate_overflow() |
||
| 187 | println("") |
||
| 188 | test_generate_above_old_cap() |
||
| 189 | println("") |
||
| 190 | |||
| 191 | if failures[0] == 0 |
||
| 192 | println("PASS: all checks passed") |
||
| 193 | else |
||
| 194 | println("FAIL: " + cv.to_string(failures[0]) + " check(s) failed") |
||
| 195 | end if |
||
| 196 | end main |
||
| 197 |
Merged by Chris Tusa
13 days ago
· merge changeset 90ce6b0b95f1