Fix M1, M2, L1, L3 from code review #002
Four of seven review findings resolved. 5 new tests (391 -> 396 crate tests; 438 -> 443 total). cargo test, clippy, wasm32 all green. M2 (data_access.rs): write_bytes now validates data_len fits in u32 before the length-prefix cast. A >4GiB blob returns Access error instead of silently writing a truncated length prefix (silent data corruption on read-back). M1 (builder.rs): Definitions::merge_into rewritten to access self.defs directly instead of round-tripping through self.build() with a double- cloned/unwrap_or_default chain that could silently drop definitions on a shape mismatch. 4 new tests: insert-when-absent, merge-into- existing, overwrite-duplicate-keys, no-op-on-non-object-top. L1 (materialize.rs): byte-offset discriminator arm of materialize_union_packed now uses checked_add for offset+disc_offset and disc_abs_offset+disc_size, returning Access error on overflow. Mirrors the existing sequential_reader.rs::read_union_value pattern. L3 (error.rs): AlkTypeError::source() now returns Some(inner) for the Validation variant (jsonschema::ValidationError implements std::error::Error). Existing source_returns_none_for_all_variants test split into source_returns_none_for_schema_offset_access and source_returns_some_for_validation_variant. Deferred: L2 (unreachable! -> Err, defense-in-depth), N1 (non-strict RFC 3339 validator, docs-only), N2 (FieldValue::Bytes for Record, API asymmetry). Review doc updated with resolution section.
This commit is contained in:
1 parent
ee6e773123
commit
a975befdd1
5 files changed
+187
-15
No files matched your search
+70
-6
@@ -489,14 +489,15 @@ impl Definitions {
|
||||
|
||||
/// Merge the `$defs` into a top-level schema `Value`. Convenience
|
||||
/// for the common case of building a schema that references its
|
||||
/// definitions.
|
||||
/// definitions. If `top` already has a `$defs` object, the definitions
|
||||
/// are merged into it (existing keys with the same name are
|
||||
/// overwritten); otherwise a `$defs` key is inserted.
|
||||
pub fn merge_into(self, top: &mut Value) {
|
||||
if let Some(obj) = top.as_object_mut() {
|
||||
if let Some(defs) = self.build().as_object() {
|
||||
if let Some(existing) = obj.get_mut("$defs").and_then(Value::as_object_mut) {
|
||||
existing.extend(defs.get("$defs").cloned().unwrap_or_default().as_object().cloned().unwrap_or_default());
|
||||
} else {
|
||||
obj.insert("$defs".to_string(), Value::Object(defs.get("$defs").cloned().unwrap_or_default().as_object().cloned().unwrap_or_default()));
|
||||
match obj.get_mut("$defs").and_then(Value::as_object_mut) {
|
||||
Some(existing) => existing.extend(self.defs),
|
||||
None => {
|
||||
obj.insert("$defs".to_string(), Value::Object(self.defs));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -649,6 +650,69 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn builder_merge_into_inserts_defs_when_absent() {
|
||||
let mut defs = Definitions::new();
|
||||
defs.define("Read", Schema::struct_().field("id", Schema::uint32()));
|
||||
let mut top = Schema::struct_().field("payload", Schema::ref_def("Read")).build();
|
||||
defs.merge_into(&mut top);
|
||||
assert!(top["$defs"].is_object(), "$defs should be inserted");
|
||||
assert_eq!(
|
||||
top["$defs"]["Read"],
|
||||
json!({"AlkType:Struct": true, "properties": {"id": {"AlkType:Uint32": true}}})
|
||||
);
|
||||
// The top-level schema's own keys are preserved.
|
||||
assert_eq!(top["AlkType:Struct"], json!(true));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn builder_merge_into_merges_into_existing_defs() {
|
||||
// Top already has a $defs with "Write"; merging in "Read" should
|
||||
// add "Read" and keep "Write".
|
||||
let mut top = Schema::struct_()
|
||||
.field("payload", Schema::ref_def("Read"))
|
||||
.build();
|
||||
top["$defs"] = json!({
|
||||
"Write": {"AlkType:Struct": true, "properties": {"n": {"AlkType:Uint16": true}}}
|
||||
});
|
||||
let mut defs = Definitions::new();
|
||||
defs.define("Read", Schema::struct_().field("id", Schema::uint32()));
|
||||
defs.merge_into(&mut top);
|
||||
assert_eq!(
|
||||
top["$defs"]["Write"],
|
||||
json!({"AlkType:Struct": true, "properties": {"n": {"AlkType:Uint16": true}}}),
|
||||
"existing Write def should be preserved"
|
||||
);
|
||||
assert_eq!(
|
||||
top["$defs"]["Read"],
|
||||
json!({"AlkType:Struct": true, "properties": {"id": {"AlkType:Uint32": true}}}),
|
||||
"new Read def should be merged in"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn builder_merge_into_overwrites_duplicate_keys() {
|
||||
// If both top and defs define the same key, the merged value wins.
|
||||
let mut top = json!({
|
||||
"AlkType:Struct": true,
|
||||
"$defs": {"X": {"old": true}}
|
||||
});
|
||||
let mut defs = Definitions::new();
|
||||
defs.define_value("X", json!({"new": true}));
|
||||
defs.merge_into(&mut top);
|
||||
assert_eq!(top["$defs"]["X"], json!({"new": true}));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn builder_merge_into_is_noop_for_non_object_top() {
|
||||
// If top is not an object, merge_into silently does nothing.
|
||||
let mut top = json!("not an object");
|
||||
let mut defs = Definitions::new();
|
||||
defs.define("Read", Schema::uint32());
|
||||
defs.merge_into(&mut top);
|
||||
assert_eq!(top, json!("not an object"), "non-object top should be untouched");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn builder_ref_def_produces_json_pointer() {
|
||||
assert_eq!(
|
||||
|
||||
+7
-1
@@ -262,6 +262,12 @@ pub fn write_bytes(
|
||||
endian: Endian,
|
||||
) -> Result<usize, AlkTypeError> {
|
||||
let data_len = value.len();
|
||||
let data_len_u32 = u32::try_from(data_len).map_err(|_| {
|
||||
access_err(
|
||||
field_path,
|
||||
format!("data length {data_len} exceeds u32::MAX (length prefix width)"),
|
||||
)
|
||||
})?;
|
||||
let total = U32_SIZE.checked_add(data_len).ok_or_else(|| {
|
||||
access_err(
|
||||
field_path,
|
||||
@@ -275,7 +281,7 @@ pub fn write_bytes(
|
||||
)
|
||||
})?;
|
||||
check_bounds(buffer.len(), offset, end, field_path)?;
|
||||
write_array(buffer, offset, u32_to(data_len as u32, endian), field_path)?;
|
||||
write_array(buffer, offset, u32_to(data_len_u32, endian), field_path)?;
|
||||
let data_start = offset + U32_SIZE;
|
||||
let dest = buffer.get_mut(data_start..end).ok_or_else(|| {
|
||||
access_err(
|
||||
|
||||
+22
-2
@@ -42,7 +42,14 @@ impl fmt::Display for AlkTypeError {
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for AlkTypeError {}
|
||||
impl std::error::Error for AlkTypeError {
|
||||
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
||||
match self {
|
||||
AlkTypeError::Validation(inner) => Some(inner),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
@@ -93,7 +100,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn source_returns_none_for_all_variants() {
|
||||
fn source_returns_none_for_schema_offset_access() {
|
||||
let schema_err = AlkTypeError::Schema("x".to_string());
|
||||
assert!(std::error::Error::source(&schema_err).is_none());
|
||||
let offset_err = AlkTypeError::Offset {
|
||||
@@ -107,4 +114,17 @@ mod tests {
|
||||
};
|
||||
assert!(std::error::Error::source(&access_err).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn source_returns_some_for_validation_variant() {
|
||||
// The Validation variant wraps a jsonschema::ValidationError<'static>
|
||||
// which implements std::error::Error. source() should return Some.
|
||||
let schema = serde_json::json!({"type": "integer"});
|
||||
let validator = jsonschema::validator_for(&schema).expect("validator");
|
||||
let instance = serde_json::json!("not-an-integer");
|
||||
let js_err = validator.validate(&instance).expect_err("invalid");
|
||||
let owned = js_err.to_owned();
|
||||
let err = AlkTypeError::Validation(owned);
|
||||
assert!(std::error::Error::source(&err).is_some(), "Validation variant should expose its source");
|
||||
}
|
||||
}
|
||||
+20
-4
@@ -311,15 +311,23 @@ fn materialize_union_packed(
|
||||
offset: disc_offset,
|
||||
disc_type,
|
||||
} => {
|
||||
let disc_abs_offset = offset.checked_add(disc_offset).ok_or_else(|| {
|
||||
AlkTypeError::Access {
|
||||
field_path: field_path.to_string(),
|
||||
reason: format!(
|
||||
"discriminator offset {offset} + {disc_offset} overflows usize"
|
||||
),
|
||||
}
|
||||
})?;
|
||||
let disc_value = match disc_type {
|
||||
AlkTypeKind::Uint8 => {
|
||||
data_access::read_u8(buffer, offset + disc_offset, field_path)? as u32
|
||||
data_access::read_u8(buffer, disc_abs_offset, field_path)? as u32
|
||||
}
|
||||
AlkTypeKind::Uint16 => {
|
||||
data_access::read_u16(buffer, offset + disc_offset, field_path, endian)? as u32
|
||||
data_access::read_u16(buffer, disc_abs_offset, field_path, endian)? as u32
|
||||
}
|
||||
AlkTypeKind::Uint32 => {
|
||||
data_access::read_u32(buffer, offset + disc_offset, field_path, endian)?
|
||||
data_access::read_u32(buffer, disc_abs_offset, field_path, endian)?
|
||||
}
|
||||
_ => unreachable!("disc_type restricted by parse_discriminator"),
|
||||
};
|
||||
@@ -333,7 +341,15 @@ fn materialize_union_packed(
|
||||
field_path: field_path.to_string(),
|
||||
reason: format!("union discriminator value {key} not in mapping"),
|
||||
})?;
|
||||
let variant_offset = offset + disc_offset + disc_type.type_size().unwrap_or(1);
|
||||
let disc_size = disc_type.type_size().unwrap_or(1);
|
||||
let variant_offset = disc_abs_offset.checked_add(disc_size).ok_or_else(|| {
|
||||
AlkTypeError::Access {
|
||||
field_path: field_path.to_string(),
|
||||
reason: format!(
|
||||
"variant offset {disc_abs_offset} + disc size {disc_size} overflows usize"
|
||||
),
|
||||
}
|
||||
})?;
|
||||
let (variant_value, new_offset) = materialize_field_packed(
|
||||
buffer,
|
||||
root,
|
||||
|
||||
Reference in new issue
Block a user