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""#,

Reply via email to