diff --git a/vortex-array/src/extension/datetime/date.rs b/vortex-array/src/extension/datetime/date.rs index 8f3ae954535..8985c7fe8c5 100644 --- a/vortex-array/src/extension/datetime/date.rs +++ b/vortex-array/src/extension/datetime/date.rs @@ -87,6 +87,7 @@ impl ExtVTable for Date { } fn deserialize_metadata(&self, metadata: &[u8]) -> VortexResult { + vortex_ensure!(!metadata.is_empty(), "Date metadata must not be empty"); let tag = metadata[0]; TimeUnit::try_from(tag) } @@ -209,4 +210,21 @@ mod tests { assert!(ms.can_coerce_from(&days)); assert!(!days.can_coerce_from(&ms)); } + + #[test] + fn deserialize_empty_metadata_returns_error() { + use crate::dtype::extension::ExtVTable; + + let vtable = Date; + assert!(vtable.deserialize_metadata(&[]).is_err()); + } + + #[test] + fn deserialize_invalid_tag_returns_error() { + use crate::dtype::extension::ExtVTable; + + let vtable = Date; + // 0xFF is not a valid TimeUnit tag. + assert!(vtable.deserialize_metadata(&[0xFF]).is_err()); + } } diff --git a/vortex-array/src/extension/datetime/time.rs b/vortex-array/src/extension/datetime/time.rs index d3a62b5ef65..b8fe92b2eeb 100644 --- a/vortex-array/src/extension/datetime/time.rs +++ b/vortex-array/src/extension/datetime/time.rs @@ -88,6 +88,7 @@ impl ExtVTable for Time { } fn deserialize_metadata(&self, data: &[u8]) -> VortexResult { + vortex_ensure!(!data.is_empty(), "Time metadata must not be empty"); let tag = data[0]; TimeUnit::try_from(tag) } @@ -219,4 +220,21 @@ mod tests { assert_eq!(secs.least_supertype(&ns).unwrap(), expected); assert_eq!(ns.least_supertype(&secs).unwrap(), expected); } + + #[test] + fn deserialize_empty_metadata_returns_error() { + use crate::dtype::extension::ExtVTable; + + let vtable = Time; + assert!(vtable.deserialize_metadata(&[]).is_err()); + } + + #[test] + fn deserialize_invalid_tag_returns_error() { + use crate::dtype::extension::ExtVTable; + + let vtable = Time; + // 0xFF is not a valid TimeUnit tag. + assert!(vtable.deserialize_metadata(&[0xFF]).is_err()); + } } diff --git a/vortex-array/src/extension/datetime/timestamp.rs b/vortex-array/src/extension/datetime/timestamp.rs index ab65ce8b84c..135545468a4 100644 --- a/vortex-array/src/extension/datetime/timestamp.rs +++ b/vortex-array/src/extension/datetime/timestamp.rs @@ -141,7 +141,11 @@ impl ExtVTable for Timestamp { } fn deserialize_metadata(&self, data: &[u8]) -> VortexResult { - vortex_ensure!(data.len() >= 3); + vortex_ensure!( + data.len() >= 3, + "Timestamp metadata must have at least 3 bytes, got {}", + data.len() + ); let tag = data[0]; let time_unit = TimeUnit::try_from(tag)?; @@ -158,7 +162,13 @@ impl ExtVTable for Timestamp { } // Attempt to load from len-prefixed bytes - let tz_bytes = &data[3..][..tz_len]; + vortex_ensure!( + data.len() >= 3 + tz_len, + "Timestamp metadata is truncated: declared timezone length {} but only {} bytes available", + tz_len, + data.len() - 3 + ); + let tz_bytes = &data[3..3 + tz_len]; let tz: Arc = str::from_utf8(tz_bytes) .map_err(|e| vortex_err!("timezone is not valid utf8 string: {e}"))? .to_string() @@ -365,4 +375,32 @@ mod tests { assert!(utc.can_coerce_from(&utc_s)); assert!(!utc.can_coerce_from(&none)); } + + #[test] + fn deserialize_empty_metadata_returns_error() { + use crate::dtype::extension::ExtVTable; + + let vtable = Timestamp; + assert!(vtable.deserialize_metadata(&[]).is_err()); + } + + #[test] + fn deserialize_too_short_metadata_returns_error() { + use crate::dtype::extension::ExtVTable; + + let vtable = Timestamp; + // Only 2 bytes - too short for the required 3-byte header. + assert!(vtable.deserialize_metadata(&[0x00, 0x01]).is_err()); + } + + #[test] + fn deserialize_truncated_timezone_returns_error() { + use crate::dtype::extension::ExtVTable; + + let vtable = Timestamp; + // Valid tag (0x00 = Nanoseconds), tz_len = 10 (little-endian: [0x0A, 0x00]), + // but only 3 bytes of timezone data instead of the declared 10. + let data = [0x00u8, 0x0A, 0x00, b'U', b'T', b'C']; + assert!(vtable.deserialize_metadata(&data).is_err()); + } }