Skip to content
Open
6 changes: 4 additions & 2 deletions ordvec-manifest/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,8 @@ Stable limit codes are part of the contract:
size on create; the flat cap is an opt-in ceiling, unbounded by default
(`auxiliary_artifact_file_too_large`);
- primary index artifact bytes: bounded by the manifest-declared
`file_size_bytes` on verify (`artifact_file_too_large`);
`file_size_bytes` on verify; the flat cap is an opt-in ceiling, unbounded
by default (`artifact_file_too_large`);
- calibration profile artifact bytes: bounded by the declared
`file_size_bytes`; flat cap opt-in, unbounded by default
(`calibration_profile_too_large`);
Expand All @@ -174,7 +175,7 @@ The CLI exposes matching override flags on `inspect`, `verify`, `create`,
`sqlite verify`, and `sqlite activate`: `--max-manifest-bytes`,
`--max-row-map-line-bytes`, `--max-row-map-rows`,
`--max-row-map-tracked-id-bytes`, `--max-auxiliary-artifacts`,
`--max-auxiliary-artifact-bytes`,
`--max-auxiliary-artifact-bytes`, `--max-index-artifact-bytes`,
`--max-calibration-profile-bytes`,
`--max-encoder-distortion-profile-bytes`, `--max-report-issues`, and
`--max-cached-report-bytes`. Library callers can override the same ceilings
Expand All @@ -190,6 +191,7 @@ Stable limit codes:
| row-identity duplicate-tracking `db_id` bytes | `row_identity_duplicate_tracking_limit_exceeded` | `row_identity_duplicate_tracking_limit_exceeded` |
| auxiliary artifact declarations | `auxiliary_artifact_count_limit_exceeded` | n/a |
| auxiliary artifact bytes per declared file | `auxiliary_artifact_file_too_large` | n/a |
| primary index artifact bytes | `artifact_file_too_large` | n/a |
| calibration profile artifact bytes | `calibration_profile_too_large` | n/a |
| encoder distortion profile artifact bytes | `encoder_distortion_profile_too_large` | n/a |
| collected verification report issues | `verification_report_issue_limit_exceeded` | n/a |
Expand Down
11 changes: 11 additions & 0 deletions ordvec-manifest/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -571,6 +571,17 @@ fn validate_auxiliary_artifact_shape(
),
);
}
// Optional artifacts may legitimately be declared absent with a
// zero-size placeholder (see `AuxiliaryArtifactState::OptionalAbsent`);
// only required declarations must carry a real size.
if artifact.required && artifact.file_size_bytes == 0 {
report.error(
"auxiliary_artifact_file_size_zero",
format!(
"required auxiliary artifact {name:?} file_size_bytes must be greater than zero"
),
);
}
}
}

Expand Down
44 changes: 43 additions & 1 deletion ordvec-manifest/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,8 @@ fn parse_auxiliary_artifact_arg(value: &str) -> Result<AuxiliaryArtifactArg, Str

#[cfg(test)]
mod tests {
use super::parse_auxiliary_artifact_arg;
use super::{parse_auxiliary_artifact_arg, Cli, Commands, LimitArgs};
use clap::Parser;
use std::path::PathBuf;

#[test]
Expand All @@ -112,6 +113,42 @@ mod tests {
assert_eq!(parsed.name, "app.ids");
assert_eq!(parsed.path, PathBuf::from("ids.bin"));
}

#[test]
fn limit_args_wire_index_artifact_ceiling() {
let args = LimitArgs {
max_index_artifact_bytes: Some(42),
..LimitArgs::default()
};
assert_eq!(args.resource_limits().max_index_artifact_bytes, 42);
// Unset flag leaves the library default (unbounded) untouched.
assert_eq!(
LimitArgs::default()
.resource_limits()
.max_index_artifact_bytes,
ordvec_manifest::ResourceLimits::default().max_index_artifact_bytes
);
}

#[test]
fn verify_accepts_max_index_artifact_bytes_flag() {
let cli = Cli::try_parse_from([
"ordvec-manifest",
"verify",
"--manifest",
"manifest.json",
"--max-index-artifact-bytes",
"8",
])
.expect("flag must parse");
match cli.command {
Commands::Verify { limits, .. } => {
assert_eq!(limits.max_index_artifact_bytes, Some(8));
assert_eq!(limits.resource_limits().max_index_artifact_bytes, 8);
}
_ => panic!("expected verify command"),
}
}
}

#[cfg(feature = "sqlite")]
Expand Down Expand Up @@ -174,6 +211,8 @@ struct LimitArgs {
#[arg(long)]
max_auxiliary_artifact_bytes: Option<u64>,
#[arg(long)]
max_index_artifact_bytes: Option<u64>,
#[arg(long)]
max_calibration_profile_bytes: Option<u64>,
#[arg(long)]
max_encoder_distortion_profile_bytes: Option<u64>,
Expand Down Expand Up @@ -204,6 +243,9 @@ impl LimitArgs {
if let Some(value) = self.max_auxiliary_artifact_bytes {
limits.max_auxiliary_artifact_bytes = value;
}
if let Some(value) = self.max_index_artifact_bytes {
limits.max_index_artifact_bytes = value;
}
Comment thread
qodo-code-review[bot] marked this conversation as resolved.
if let Some(value) = self.max_calibration_profile_bytes {
limits.max_calibration_profile_bytes = value;
}
Expand Down
43 changes: 43 additions & 0 deletions ordvec-manifest/tests/manifest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2639,6 +2639,49 @@ fn auxiliary_artifacts_fail_closed_on_tamper_missing_and_path_escape() {
.ends_with("missing.bin"));
}

#[test]
fn manifest_shape_rejects_zero_declared_file_sizes_for_required_artifacts() {
let root = tempfile::tempdir().unwrap();
let (temp, mut manifest, _manifest_path) = identity_manifest(root.path());
fs::write(temp.path().join("extra.bin"), b"extra").unwrap();
let extra_hash = sha256_file(temp.path().join("extra.bin")).unwrap();

manifest.artifact.file_size_bytes = 0;
manifest.auxiliary_artifacts = vec![AuxiliaryArtifact {
name: "extra".to_string(),
path: "extra.bin".to_string(),
sha256: extra_hash.sha256,
file_size_bytes: 0,
required: true,
}];

let report = verify_manifest_with_base(manifest, temp.path(), VerifyOptions::default());
assert!(!report.ok);
let codes = error_codes(&report);
assert!(codes.contains(&"artifact_file_size_zero"), "{codes:?}");
assert!(
codes.contains(&"auxiliary_artifact_file_size_zero"),
"{codes:?}"
);
}

#[test]
fn optional_absent_zero_size_placeholder_is_not_flagged_zero_size() {
let root = tempfile::tempdir().unwrap();
let (temp, mut manifest, _manifest_path) = identity_manifest(root.path());
manifest.auxiliary_artifacts = vec![AuxiliaryArtifact {
name: "optional-model".to_string(),
path: "missing-model.json".to_string(),
sha256: "0".repeat(64),
file_size_bytes: 0,
required: false,
}];

let report = verify_manifest_with_base(manifest, temp.path(), VerifyOptions::default());
assert!(report.ok, "{:?}", report.errors);
assert!(!error_codes(&report).contains(&"auxiliary_artifact_file_size_zero"));
}

#[test]
fn auxiliary_artifact_schema_rejects_unknown_fields_and_duplicate_names() {
let root = tempfile::tempdir().unwrap();
Expand Down
Loading