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:
glm-5.2 committed 2026-08-11 08:41:15 +00:00
1 parent ee6e773123
commit a975befdd1
5 files changed
+187 -15

No files matched your search

+70 -6
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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,