This is an automated email from the ASF dual-hosted git repository.
Kriskras99 pushed a commit to branch fix/decimal_zero_length_bytes
in repository https://gitbox.apache.org/repos/asf/avro-rs.git
The following commit(s) were added to refs/heads/fix/decimal_zero_length_bytes
by this push:
new 9ea014b fix!: `Decimal` should not be able to decode from a
zero-length bytes
9ea014b is described below
commit 9ea014b001fb8caf70693c9f8914b96cc08a23b1
Author: Kriskras99 <[email protected]>
AuthorDate: Wed Sep 16 10:09:44 2026 +0200
fix!: `Decimal` should not be able to decode from a zero-length bytes
This is a breaking change because it is not possible to do
`impl<T: AsRef<[u8]>> TryFrom<T> for Decimal` as we have a
`TryFrom<Decimal>`
implementation on `Vec<u8>` which causes auto-trait conflicts.
---
avro/src/decimal.rs | 40 +++++++++++++++++++-------------------
avro/src/decode.rs | 22 ++++++++++++++++-----
avro/src/error.rs | 3 +++
avro/src/schema_compatibility.rs | 2 +-
avro/src/serde/de.rs | 10 +++++-----
avro/src/serde/deser_schema/mod.rs | 4 ++--
avro/src/serde/ser.rs | 2 +-
avro/src/serde/ser_schema/mod.rs | 4 ++--
avro/src/types.rs | 22 ++++++++++-----------
avro/src/writer/datum.rs | 4 ++--
10 files changed, 64 insertions(+), 49 deletions(-)
diff --git a/avro/src/decimal.rs b/avro/src/decimal.rs
index c219b5d..0d31e16 100644
--- a/avro/src/decimal.rs
+++ b/avro/src/decimal.rs
@@ -18,11 +18,12 @@
use crate::{AvroResult, Error, error::Details};
use num_bigint::{BigInt, Sign};
use serde::{Deserialize, Serialize, Serializer, de::SeqAccess};
+use std::num::NonZero;
#[derive(Debug, Clone, Eq)]
pub struct Decimal {
value: BigInt,
- len: usize,
+ len: NonZero<usize>,
}
impl Serialize for Decimal {
@@ -53,18 +54,19 @@ impl<'de> Deserialize<'de> for Decimal {
where
E: serde::de::Error,
{
- Ok(Decimal::from(v))
+ Decimal::new(v).map_err(E::custom)
}
fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
where
A: SeqAccess<'de>,
{
+ use serde::de::Error;
let mut bytes = Vec::new();
while let Some(value) = seq.next_element::<u8>()? {
bytes.push(value);
}
- Ok(Decimal::from(bytes))
+ Decimal::new(bytes).map_err(A::Error::custom)
}
}
deserializer.deserialize_bytes(DecimalVisitor)
@@ -80,12 +82,20 @@ impl PartialEq for Decimal {
}
impl Decimal {
+ pub fn new(bytes: impl AsRef<[u8]>) -> AvroResult<Self> {
+ let bytes_ref = bytes.as_ref();
+ Ok(Self {
+ value: BigInt::from_signed_bytes_be(bytes_ref),
+ len:
NonZero::new(bytes_ref.len()).ok_or(Details::DecimalIsZeroLength)?,
+ })
+ }
+
pub(crate) fn len(&self) -> usize {
- self.len
+ self.len.get()
}
pub(crate) fn to_vec(&self) -> AvroResult<Vec<u8>> {
- self.to_sign_extended_bytes_with_len(self.len)
+ self.to_sign_extended_bytes_with_len(self.len.get())
}
pub(crate) fn to_sign_extended_bytes_with_len(&self, len: usize) ->
AvroResult<Vec<u8>> {
@@ -117,7 +127,7 @@ impl From<Decimal> for BigInt {
/// let decimal = Decimal::from(vec![1, 24]);
/// let maybe_bytes = <Vec<u8>>::try_from(&decimal);
/// ```
-impl std::convert::TryFrom<&Decimal> for Vec<u8> {
+impl TryFrom<&Decimal> for Vec<u8> {
type Error = Error;
fn try_from(decimal: &Decimal) -> Result<Self, Self::Error> {
@@ -134,7 +144,7 @@ impl std::convert::TryFrom<&Decimal> for Vec<u8> {
/// let decimal = Decimal::from(vec![1, 24]);
/// let maybe_bytes = <Vec<u8>>::try_from(decimal);
/// ```
-impl std::convert::TryFrom<Decimal> for Vec<u8> {
+impl TryFrom<Decimal> for Vec<u8> {
type Error = Error;
fn try_from(decimal: Decimal) -> Result<Self, Self::Error> {
@@ -142,16 +152,6 @@ impl std::convert::TryFrom<Decimal> for Vec<u8> {
}
}
-impl<T: AsRef<[u8]>> From<T> for Decimal {
- fn from(bytes: T) -> Self {
- let bytes_ref = bytes.as_ref();
- Self {
- value: BigInt::from_signed_bytes_be(bytes_ref),
- len: bytes_ref.len(),
- }
- }
-}
-
#[cfg(test)]
mod tests {
use super::*;
@@ -161,7 +161,7 @@ mod tests {
#[test]
fn test_decimal_from_bytes_from_ref_decimal() -> TestResult {
let input = vec![1, 24];
- let d = Decimal::from(&input);
+ let d = Decimal::new(&input)?;
let output = <Vec<u8>>::try_from(&d)?;
assert_eq!(output, input);
@@ -172,7 +172,7 @@ mod tests {
#[test]
fn test_decimal_from_bytes_from_owned_decimal() -> TestResult {
let input = vec![1, 24];
- let d = Decimal::from(&input);
+ let d = Decimal::new(&input)?;
let output = <Vec<u8>>::try_from(d)?;
assert_eq!(output, input);
@@ -182,7 +182,7 @@ mod tests {
#[test]
fn avro_3949_decimal_serde() -> TestResult {
- let decimal = Decimal::from(&[1, 2, 3]);
+ let decimal = Decimal::new([1, 2, 3])?;
let ser = serde_json::to_string(&decimal)?;
let de = serde_json::from_str(&ser)?;
diff --git a/avro/src/decode.rs b/avro/src/decode.rs
index 9c06eb9..4c62647 100644
--- a/avro/src/decode.rs
+++ b/avro/src/decode.rs
@@ -194,13 +194,13 @@ fn decode_internal_body<R: Read, S: Borrow<Schema>>(
reader,
ctx,
)? {
- Value::Fixed(_, bytes) =>
Ok(Value::Decimal(Decimal::from(bytes))),
+ Value::Fixed(_, bytes) =>
Ok(Value::Decimal(Decimal::new(bytes)?)),
value => Err(Details::FixedValue(value).into()),
}
}
InnerDecimalSchema::Bytes => {
match decode_internal(&Schema::Bytes, names,
enclosing_namespace, reader, ctx)? {
- Value::Bytes(bytes) =>
Ok(Value::Decimal(Decimal::from(bytes))),
+ Value::Bytes(bytes) =>
Ok(Value::Decimal(Decimal::new(bytes)?)),
value => Err(Details::BytesValue(value).into()),
}
}
@@ -697,7 +697,7 @@ mod tests {
scale: 2,
});
let bigint = (-423).to_bigint().unwrap();
- let value = Value::Decimal(Decimal::from(bigint.to_signed_bytes_be()));
+ let value = Value::Decimal(Decimal::new(bigint.to_signed_bytes_be())?);
let mut buffer = Vec::new();
encode(&value, &schema, &mut buffer).expect(&success(&value, &schema));
@@ -724,9 +724,9 @@ mod tests {
precision: NonZero::new(4).unwrap(),
scale: 2,
});
- let value = Value::Decimal(Decimal::from(
+ let value = Value::Decimal(Decimal::new(
((-423).to_bigint().unwrap()).to_signed_bytes_be(),
- ));
+ )?);
let mut buffer = Vec::<u8>::new();
encode(&value, &schema, &mut buffer).expect(&success(&value, &schema));
@@ -1182,4 +1182,16 @@ mod tests {
Ok(())
}
+
+ #[test]
+ fn avro_rs_680_decimal_bytes_zero_length() -> TestResult {
+ let schema =
+ Schema::parse_str(r#"{"type": "bytes", "logicalType": "decimal",
"precision": 4}"#)?;
+ // 0x00 is the varint encoding of length 0 (zero-length bytes)
+ let mut buf: &[u8] = &[0x00];
+ let error = decode(&schema, &mut buf).unwrap_err().into_details();
+ assert_eq!(error.to_string(), "Decimal bytes cannot be zero length");
+
+ Ok(())
+ }
}
diff --git a/avro/src/error.rs b/avro/src/error.rs
index 924e8f9..6811cfe 100644
--- a/avro/src/error.rs
+++ b/avro/src/error.rs
@@ -92,6 +92,9 @@ pub enum Details {
#[error("Not a fixed or bytes type, required for decimal schema, got:
{0:?}")]
ResolveDecimalSchema(SchemaKind),
+ #[error("Decimal bytes cannot be zero length")]
+ DecimalIsZeroLength,
+
#[error("Invalid utf-8 string")]
ConvertToUtf8(#[source] std::string::FromUtf8Error),
diff --git a/avro/src/schema_compatibility.rs b/avro/src/schema_compatibility.rs
index 13f07fc..32dd8a7 100644
--- a/avro/src/schema_compatibility.rs
+++ b/avro/src/schema_compatibility.rs
@@ -1712,7 +1712,7 @@ mod tests {
SchemaCompatibility::mutual_read(&bytes, &fixed)?
);
- let value = Value::Decimal(Decimal::from(vec![1; 10]));
+ let value = Value::Decimal(Decimal::new([1; 10])?);
let fixed_value = value.clone().resolve(&fixed)?;
let bytes_value = value.resolve(&bytes)?;
diff --git a/avro/src/serde/de.rs b/avro/src/serde/de.rs
index 20e3beb..52f9229 100644
--- a/avro/src/serde/de.rs
+++ b/avro/src/serde/de.rs
@@ -1150,12 +1150,12 @@ mod tests {
let test = Value::Record(vec![
("a".to_owned(), Value::Long(27)),
("b".to_owned(), Value::String("foo".to_owned())),
- ("c".to_owned(), Value::Decimal(Decimal::from(vec![1, 24]))),
+ ("c".to_owned(), Value::Decimal(Decimal::new([1, 24])?)),
]);
let expected = Test {
a: 27,
b: "foo".to_owned(),
- c: Decimal::from(vec![1, 24]),
+ c: Decimal::new([1, 24])?,
};
let final_value: Test = from_value(&test)?;
assert_eq!(final_value, expected);
@@ -1166,7 +1166,7 @@ mod tests {
Value::Record(vec![
("a".to_owned(), Value::Long(27)),
("b".to_owned(), Value::String("foo".to_owned())),
- ("c".to_owned(), Value::Decimal(Decimal::from(vec![1,
24]))),
+ ("c".to_owned(), Value::Decimal(Decimal::new([1, 24])?)),
]),
),
("b".to_owned(), Value::Int(35)),
@@ -1794,11 +1794,11 @@ mod tests {
#[test]
fn test_avro_3892_deserialize_bytes_from_decimal() -> TestResult {
let expected_bytes = BigInt::from(123456789).to_signed_bytes_be();
- let value = Value::Decimal(Decimal::from(&expected_bytes));
+ let value = Value::Decimal(Decimal::new(&expected_bytes)?);
let raw_bytes = from_value::<Bytes>(&value)?;
assert_eq!(raw_bytes.0, expected_bytes);
- let value = Value::Union(0,
Box::new(Value::Decimal(Decimal::from(&expected_bytes))));
+ let value = Value::Union(0,
Box::new(Value::Decimal(Decimal::new(&expected_bytes)?)));
let raw_bytes = from_value::<Option<Bytes>>(&value)?;
assert_eq!(raw_bytes.unwrap().0, expected_bytes);
Ok(())
diff --git a/avro/src/serde/deser_schema/mod.rs
b/avro/src/serde/deser_schema/mod.rs
index 0d9d3ad..7ef90ee 100644
--- a/avro/src/serde/deser_schema/mod.rs
+++ b/avro/src/serde/deser_schema/mod.rs
@@ -991,7 +991,7 @@ mod tests {
let test = Test {
a: 27,
b: "foo".to_string(),
- c: Decimal::from(vec![1, 24]),
+ c: Decimal::new([1, 24])?,
};
assert_roundtrip(test.clone(), &schemas[0], Vec::new())?;
@@ -1845,7 +1845,7 @@ mod tests {
)?;
let expected_bytes = BigInt::from(123456789).to_signed_bytes_be();
- let value = Decimal::from(&expected_bytes);
+ let value = Decimal::new(&expected_bytes)?;
assert_roundtrip(value.clone(), &schema, Vec::new())?;
assert_roundtrip(value.clone(), &Schema::union(vec![schema])?,
Vec::new())?;
diff --git a/avro/src/serde/ser.rs b/avro/src/serde/ser.rs
index efd6fe8..bd41a56 100644
--- a/avro/src/serde/ser.rs
+++ b/avro/src/serde/ser.rs
@@ -689,7 +689,7 @@ mod tests {
let test = Test {
a: 27,
b: "foo".to_owned(),
- decimal: Decimal::from(vec![1, 24]),
+ decimal: Decimal::new([1, 24])?,
};
let expected = Value::Record(vec![
("a".to_owned(), Value::Long(27)),
diff --git a/avro/src/serde/ser_schema/mod.rs b/avro/src/serde/ser_schema/mod.rs
index 4369235..f47beec 100644
--- a/avro/src/serde/ser_schema/mod.rs
+++ b/avro/src/serde/ser_schema/mod.rs
@@ -1447,7 +1447,7 @@ mod tests {
let names = HashMap::new();
- let val = Decimal::from(&[251, 155]);
+ let val = Decimal::new([251, 155])?;
assert_serialize(val, &schema, &names, &[4, 251, 155]);
assert_serialize_err(
(),
@@ -1474,7 +1474,7 @@ mod tests {
let names = HashMap::new();
- let val = Decimal::from(&[0, 0, 0, 0, 0, 0, 251, 155]);
+ let val = Decimal::new([0, 0, 0, 0, 0, 0, 251, 155])?;
assert_serialize(val, &schema, &names, &[0, 0, 0, 0, 0, 0, 251, 155]);
assert_serialize_err(
(),
diff --git a/avro/src/types.rs b/avro/src/types.rs
index ddae551..9d5819a 100644
--- a/avro/src/types.rs
+++ b/avro/src/types.rs
@@ -935,7 +935,7 @@ impl Value {
.into())
} else {
// precision and scale match, can we assume the underlying
type can hold the data?
- Ok(Value::Decimal(Decimal::from(bytes)))
+ Ok(Value::Decimal(Decimal::new(bytes)?))
}
}
@@ -956,7 +956,7 @@ impl Value {
}
})
.collect::<Result<Vec<u8>, Error>>()?;
- Ok(Value::Decimal(Decimal::from(bytes)))
+ Ok(Value::Decimal(Decimal::new(bytes)?))
}
other => Err(Details::ResolveDecimal(other).into()),
}
@@ -1894,7 +1894,7 @@ Field with name '"b"' is not a member of the map items"#,
#[test]
fn resolve_decimal_bytes() -> TestResult {
- let value = Value::Decimal(Decimal::from(vec![1, 2, 3, 4, 5]));
+ let value = Value::Decimal(Decimal::new([1, 2, 3, 4, 5])?);
value.clone().resolve(&Schema::Decimal(DecimalSchema {
precision: NonZero::new(10).unwrap(),
scale: 4,
@@ -1913,7 +1913,7 @@ Field with name '"b"' is not a member of the map items"#,
scale: 4,
inner: InnerDecimalSchema::Bytes,
}))?;
- assert_eq!(resolved, Value::Decimal(Decimal::from(vec![0u8])));
+ assert_eq!(resolved, Value::Decimal(Decimal::new([0u8])?));
let mut all_bytes_str = String::new();
for b in 0u8..=255u8 {
@@ -1926,7 +1926,7 @@ Field with name '"b"' is not a member of the map items"#,
}))?;
assert_eq!(
resolved,
- Value::Decimal(Decimal::from((0u8..=255u8).collect::<Vec<_>>()))
+ Value::Decimal(Decimal::new((0u8..=255u8).collect::<Vec<_>>())?)
);
let value = Value::String("\u{0100}".to_string());
@@ -1970,7 +1970,7 @@ Field with name '"b"' is not a member of the map items"#,
#[test]
fn resolve_decimal_invalid_scale() {
- let value = Value::Decimal(Decimal::from(vec![1, 2]));
+ let value = Value::Decimal(Decimal::new([1, 2]).unwrap());
assert!(
value
.resolve(&Schema::Decimal(DecimalSchema {
@@ -1984,7 +1984,7 @@ Field with name '"b"' is not a member of the map items"#,
#[test]
fn resolve_decimal_invalid_precision_for_length() {
- let value =
Value::Decimal(Decimal::from((1u8..=8u8).rev().collect::<Vec<_>>()));
+ let value =
Value::Decimal(Decimal::new((1u8..=8u8).rev().collect::<Vec<_>>()).unwrap());
assert!(
value
.resolve(&Schema::Decimal(DecimalSchema {
@@ -1998,7 +1998,7 @@ Field with name '"b"' is not a member of the map items"#,
#[test]
fn resolve_decimal_fixed() {
- let value = Value::Decimal(Decimal::from(vec![1, 2, 3, 4, 5]));
+ let value = Value::Decimal(Decimal::new([1, 2, 3, 4, 5]).unwrap());
assert!(
value
.clone()
@@ -2325,7 +2325,7 @@ Field with name '"b"' is not a member of the map items"#,
JsonValue::Number(1.into())
);
assert_eq!(
- JsonValue::try_from(Value::Decimal(vec![1, 2, 3].into()))?,
+ JsonValue::try_from(Value::Decimal(Decimal::new([1, 2, 3])?))?,
JsonValue::Array(vec![
JsonValue::Number(1.into()),
JsonValue::Number(2.into()),
@@ -3321,9 +3321,9 @@ Field with name '"b"' is not a member of the map items"#,
fn test_avro_3782_incorrect_decimal_resolving() -> TestResult {
let schema = r#"{"name": "decimalSchema", "logicalType": "decimal",
"type": "fixed", "precision": 8, "scale": 0, "size": 8}"#;
- let avro_value = Value::Decimal(Decimal::from(
+ let avro_value = Value::Decimal(Decimal::new(
BigInt::from(12345678u32).to_signed_bytes_be(),
- ));
+ )?);
let schema = Schema::parse_str(schema)?;
let resolve_result = avro_value.resolve(&schema);
assert!(
diff --git a/avro/src/writer/datum.rs b/avro/src/writer/datum.rs
index b021c80..8f2c264 100644
--- a/avro/src/writer/datum.rs
+++ b/avro/src/writer/datum.rs
@@ -461,7 +461,7 @@ mod tests {
scale: 5,
inner,
}),
- Value::Decimal(Decimal::from(value.clone())),
+ Value::Decimal(Decimal::new(&value)?),
&Schema::Fixed(fixed),
Value::Fixed(size, value),
)
@@ -477,7 +477,7 @@ mod tests {
scale: 3,
inner: InnerDecimalSchema::Bytes,
}),
- Value::Decimal(Decimal::from(value.clone())),
+ Value::Decimal(Decimal::new(&value)?),
&Schema::Bytes,
value,
)