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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 27 additions & 8 deletions crates/ethos-cli/src/cmd/verify.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ pub(crate) fn verify(args: VerifyArgs) -> Result<(), Failure> {
let source = NativeCropSource { document: &doc };
let mut report =
ethos_verify::verify_claims(&source, citations, &config, config_sha256);
assign_logical_crop_refs(&mut report);
assign_logical_crop_refs(&mut report)?;
if let Some(crop_dir) = args.crop_dir.as_deref() {
write_crop_artifacts(crop_dir, &report, crop_source_pdf.as_ref())?;
}
Expand Down Expand Up @@ -547,9 +547,9 @@ fn logical_crop_ref_for(
Ok(format!("crop-{hash}.json"))
}

fn assign_logical_crop_refs(report: &mut VerificationReport) {
fn assign_logical_crop_refs(report: &mut VerificationReport) -> Result<(), Failure> {
let Some(document_fingerprint) = report.document_fingerprint.as_deref() else {
return;
return Ok(());
};
for check in &mut report.checks {
let Some(evidence) = check.evidence.as_mut() else {
Expand All @@ -561,10 +561,10 @@ fn assign_logical_crop_refs(report: &mut VerificationReport) {
let Some(page) = evidence.page.as_deref() else {
continue;
};
if let Ok(crop_ref) = logical_crop_ref_for(document_fingerprint, &check.id, page) {
evidence.crop_ref = Some(crop_ref);
}
let crop_ref = logical_crop_ref_for(document_fingerprint, &check.id, page)?;
evidence.crop_ref = Some(crop_ref);
}
Ok(())
}

#[derive(Debug)]
Expand Down Expand Up @@ -862,7 +862,8 @@ fn zlib_store(data: &[u8]) -> Result<Vec<u8>, Failure> {
let len = remaining.len().min(u16::MAX as usize);
let final_block = len == remaining.len();
out.push(if final_block { 0x01 } else { 0x00 });
let len_u16 = u16::try_from(len).expect("block length is capped at u16::MAX");
let len_u16 = u16::try_from(len)
.map_err(|_| Failure::Ethos(EthosError::internal("PNG zlib block length overflow")))?;
out.extend_from_slice(&len_u16.to_le_bytes());
out.extend_from_slice(&(!len_u16).to_le_bytes());
out.extend_from_slice(&remaining[..len]);
Expand Down Expand Up @@ -1072,6 +1073,23 @@ mod tests {
}
}

#[test]
fn zlib_store_splits_blocks_larger_than_u16_max() {
let data = vec![0x41; u16::MAX as usize + 1];
let zlib = zlib_store(&data).unwrap_or_else(|_| panic!("zlib store should encode"));

assert_eq!(&zlib[..2], &[0x78, 0x01]);
assert_eq!(zlib[2], 0x00);
assert_eq!(u16::from_le_bytes([zlib[3], zlib[4]]), u16::MAX);

let second_block = 2 + 1 + 2 + 2 + u16::MAX as usize;
assert_eq!(zlib[second_block], 0x01);
assert_eq!(
u16::from_le_bytes([zlib[second_block + 1], zlib[second_block + 2]]),
1
);
}

#[test]
fn logical_crop_ref_uses_check_identity_not_bbox() {
let first = logical_crop_ref_for(
Expand Down Expand Up @@ -1185,7 +1203,8 @@ mod tests {
warnings: Vec::new(),
};

assign_logical_crop_refs(&mut report);
assign_logical_crop_refs(&mut report)
.unwrap_or_else(|_| panic!("logical crop refs should be assigned"));

let expected = logical_crop_ref_for(
report.document_fingerprint.as_deref().unwrap(),
Expand Down
4 changes: 4 additions & 0 deletions crates/ethos-core/src/grounding.rs
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,10 @@ pub trait GroundingSource {
None
}
/// Element lookup by id. Default: linear scan over [`Self::elements`].
///
/// Adapters may override this as a convenience API for direct callers. The
/// verifier builds its own deterministic per-run index from [`Self::elements`]
/// so duplicate handling and traversal order stay tied to the evidence list.
fn element_by_id(&self, id: &str) -> Option<GroundingElement> {
self.elements().into_iter().find(|e| e.id == id)
}
Expand Down
4 changes: 3 additions & 1 deletion crates/ethos-core/src/verify_types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,9 @@ pub struct Check {
/// Method used.
pub match_method: MatchMethod,
/// True when grounding would require semantic judgment beyond the declared
/// method. Such checks can never make `all_evidence_grounded` true.
/// method. Alpha literal checkers keep this false because unsupported or
/// non-literal claims fail closed instead of being marked semantically checked.
/// Such checks can never make `all_evidence_grounded` true.
pub semantic_unverified: bool,
/// Echoed evidence, when configured.
#[serde(skip_serializing_if = "Option::is_none")]
Expand Down
Loading