|

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 build fails closed instead of packaging a prefix
  • parse_manifest returns -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

Comments (0)