This is an automated email from the ASF dual-hosted git repository.
Kriskras99 pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/avro-rs.git
The following commit(s) were added to refs/heads/main by this push:
new 0117bf0 fix: Check length and codepoints when resolving String to
Fixed (#700)
0117bf0 is described below
commit 0117bf015cc3bbf3a092329f60965f4107f341ba
Author: Mike Beaumont <[email protected]>
AuthorDate: Tue Oct 13 07:55:39 2026 +0200
fix: Check length and codepoints when resolving String to Fixed (#700)
* fix: Check length and codepoints when resolving String to Fixed
* test: prefix
---
avro/src/error.rs | 3 ++
avro/src/schema/mod.rs | 2 +-
avro/src/types.rs | 97 +++++++++++++++++++++++++++++++++++++++++++++++++-
avro/tests/io.rs | 12 +++----
4 files changed, 106 insertions(+), 8 deletions(-)
diff --git a/avro/src/error.rs b/avro/src/error.rs
index 88a2ecb..21859f7 100644
--- a/avro/src/error.rs
+++ b/avro/src/error.rs
@@ -289,6 +289,9 @@ pub enum Details {
#[error("String expected for fixed, got: {0:?}")]
GetStringForFixed(Value),
+ #[error("String codepoints must be in the range 0-255 to resolve to Fixed,
got: {0:?}")]
+ ResolveFixed(Value),
+
#[error("Enum default {symbol:?} is not among allowed symbols
{symbols:?}")]
GetEnumDefault {
symbol: String,
diff --git a/avro/src/schema/mod.rs b/avro/src/schema/mod.rs
index bd2a871..cf97bd5 100644
--- a/avro/src/schema/mod.rs
+++ b/avro/src/schema/mod.rs
@@ -1788,7 +1788,7 @@ mod tests {
"name": "EmployeeId",
"size": 16
},
- "default": "female"
+ "default": "0123456789abcdef"
}
]
},
diff --git a/avro/src/types.rs b/avro/src/types.rs
index 78289c7..3cb6292 100644
--- a/avro/src/types.rs
+++ b/avro/src/types.rs
@@ -1388,7 +1388,28 @@ impl Value {
Err(Details::CompareFixedSizes { size, n }.into())
}
}
- Value::String(s) => Ok(Value::Fixed(s.len(), s.into_bytes())),
+ // The spec says a fixed value can be encoded as a JSON string
+ // whose codepoints (0-255) map directly to byte values. This
applies
+ // to defaults and any other String to Fixed resolution.
+ Value::String(s) => {
+ let bytes = s
+ .chars()
+ .map(|c| {
+ let cp = c as u32;
+ if cp > 0xFF {
+
Err(Details::ResolveFixed(Value::String(s.clone())).into())
+ } else {
+ Ok(cp as u8)
+ }
+ })
+ .collect::<Result<Vec<u8>, Error>>()?;
+ if bytes.len() == size {
+ Ok(Value::Fixed(size, bytes))
+ } else {
+ let n = bytes.len();
+ Err(Details::CompareFixedSizes { size, n }.into())
+ }
+ }
Value::Bytes(s) => {
if s.len() == size {
Ok(Value::Fixed(size, s))
@@ -3664,6 +3685,80 @@ mod tests {
Ok(())
}
+ #[test]
+ fn avro_rs_700_resolve_fixed_from_string_checks_size() -> TestResult {
+ let fixed = |size| {
+ Ok::<_, Error>(Schema::Fixed(FixedSchema {
+ name: "test".try_into()?,
+ aliases: None,
+ doc: None,
+ size,
+ attributes: Default::default(),
+ }))
+ };
+
+ assert_eq!(
+ Value::String("abc".into()).resolve(&fixed(3)?)?,
+ Value::Fixed(3, vec![97, 98, 99])
+ );
+
+ assert_eq!(
+ Value::String("ab".into())
+ .resolve(&fixed(3)?)
+ .unwrap_err()
+ .to_string(),
+ "Fixed size mismatch, expected: 3, got: 2"
+ );
+
+ assert_eq!(
+ Value::String("abcd".into())
+ .resolve(&fixed(3)?)
+ .unwrap_err()
+ .to_string(),
+ "Fixed size mismatch, expected: 3, got: 4"
+ );
+
+ // Codepoints 0-255 map to one byte each, they are not UTF-8 encoded.
+ assert_eq!(
+ Value::String("\u{ff}\u{ff}\u{ff}".into()).resolve(&fixed(3)?)?,
+ Value::Fixed(3, vec![0xFF, 0xFF, 0xFF])
+ );
+
+ assert_eq!(
+ Value::String("\u{100}".into())
+ .resolve(&fixed(1)?)
+ .unwrap_err()
+ .to_string(),
+ "String codepoints must be in the range 0-255 to resolve to Fixed,
got: String(\"Ā\")"
+ );
+
+ Ok(())
+ }
+
+ #[test]
+ fn avro_rs_700_resolve_fixed_default_from_schema_checks_size() ->
TestResult {
+ let schema = |default: &str| {
+ Schema::parse_str(&format!(
+ r#"{{"type": "record", "name": "r", "fields": [
+ {{"name": "a", "type": {{"type": "fixed", "name": "f",
"size": 4}},
+ "default": "{default}"}}
+ ]}}"#
+ ))
+ };
+
+ // A default whose size does not match the fixed size is not a valid
schema.
+ assert!(schema("abcdefghij").is_err());
+ assert!(schema("ab").is_err());
+
+ let schema = schema("abcd")?;
+ assert_eq!(
+ Value::Record(vec![]).resolve(&schema)?,
+ Value::Record(vec![("a".into(), Value::Fixed(4, vec![97, 98, 99,
100]))])
+ );
+
+ Ok(())
+ }
+
#[test]
fn avro_3928_from_serde_value_to_types_value() -> TestResult {
assert_eq!(Value::try_from(serde_json::Value::Null)?, Value::Null);
diff --git a/avro/tests/io.rs b/avro/tests/io.rs
index 9fcf60b..a59b16b 100644
--- a/avro/tests/io.rs
+++ b/avro/tests/io.rs
@@ -126,14 +126,14 @@ fn default_value_examples() -> &'static Vec<(&'static
str, &'static str, Value)>
(r#""double""#, r#""NaN""#, Value::Double(f64::NAN)),
(
r#"{"type": "fixed", "name": "F", "size": 2}"#,
- r#""a""#,
- Value::Fixed(1, vec![97]),
- ), // ASCII 'a' => one byte
+ r#""ab""#,
+ Value::Fixed(2, vec![97, 98]),
+ ), // Each codepoint maps to one byte, and the default must fill
the fixed size
(
r#"{"type": "fixed", "name": "F", "size": 2}"#,
- r#""\u00FF""#,
- Value::Fixed(2, vec![195, 191]),
- ), // The value is between U+0080 and U+07FF => two bytes
+ r#""\u00FF\u0000""#,
+ Value::Fixed(2, vec![255, 0]),
+ ), // Codepoints 0-255 map directly to byte values, they are not
UTF-8 encoded
(
r#"{"type": "enum", "name": "F", "symbols": ["FOO", "BAR"]}"#,
r#""FOO""#,