Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Error on {Array,Group}::[async_]open[_opt] with additional fields with "must_understand": true #149

Merged
merged 1 commit into from
Feb 21, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Bump `lru` to 0.13
- Use codec identifiers in the example for `experimental_codec_names` remapping

### Fixed
- Error on `{Array,Group}::[async_]open[_opt]` with additional fields with `"must_understand": true`

## [0.19.2] - 2025-02-13

### Changed
Expand Down
55 changes: 55 additions & 0 deletions zarrs/src/array.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ pub use chunk_cache::{
pub use array_sharded_ext::ArrayShardedExt;
#[cfg(feature = "sharding")]
pub use array_sync_sharded_readable_ext::{ArrayShardedReadableExt, ArrayShardedReadableExtCache};
use zarrs_metadata::v3::UnsupportedAdditionalFieldError;
// TODO: Add AsyncArrayShardedReadableExt and AsyncArrayShardedReadableExtCache

use crate::{
Expand Down Expand Up @@ -807,6 +808,22 @@ impl<TStorage: ?Sized> Array<TStorage> {
ArrayMetadata::V3(_) => Ok(self),
}
}

/// Reject the array if it contains additional fields with `"must_understand": true`.
fn validate_metadata(metadata: &ArrayMetadata) -> Result<(), ArrayCreateError> {
let additional_fields = match &metadata {
ArrayMetadata::V2(metadata) => &metadata.additional_fields,
ArrayMetadata::V3(metadata) => &metadata.additional_fields,
};
for (name, field) in additional_fields {
if field.must_understand() {
return Err(ArrayCreateError::UnsupportedAdditionalFieldError(
UnsupportedAdditionalFieldError::new(name.clone(), field.as_value().clone()),
));
}
}
Ok(())
}
}

#[cfg(feature = "ndarray")]
Expand Down Expand Up @@ -938,6 +955,7 @@ pub fn bytes_to_ndarray<T: bytemuck::Pod>(
mod tests {
use crate::storage::store::MemoryStore;
use zarrs_filesystem::FilesystemStore;
use zarrs_metadata::v3::AdditionalField;

use super::*;

Expand Down Expand Up @@ -1330,4 +1348,41 @@ mod tests {
// false,
// );
// }

#[test]
fn array_additional_fields() {
let store = Arc::new(MemoryStore::new());
let array_path = "/group/array";

for must_understand in [true, false] {
let additional_field = serde_json::Map::new();
let additional_field = AdditionalField::new(additional_field, must_understand);
let mut additional_fields = AdditionalFields::new();
additional_fields.insert("key".to_string(), additional_field);

// Permit array creation with manually added additional fields
let array = ArrayBuilder::new(
vec![8, 8], // array shape
DataType::Float32,
vec![4, 4].try_into().unwrap(),
FillValue::from(ZARR_NAN_F32),
)
.bytes_to_bytes_codecs(vec![
#[cfg(feature = "gzip")]
Arc::new(codec::GzipCodec::new(5).unwrap()),
])
.additional_fields(additional_fields)
.build(store.clone(), array_path)
.unwrap();
array.store_metadata().unwrap();

let array = Array::open(store.clone(), array_path);
if must_understand {
// Disallow array opening with unknown `"must_understand": true` additional fields
assert!(array.is_err());
} else {
assert!(array.is_ok());
}
}
}
}
14 changes: 12 additions & 2 deletions zarrs/src/array/array_async_readable.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,16 @@
path: &str,
version: &MetadataRetrieveVersion,
) -> Result<Array<TStorage>, ArrayCreateError> {
let metadata = Self::async_open_metadata(storage.clone(), path, version).await?;
Self::validate_metadata(&metadata)?;
Self::new_with_metadata(storage, path, metadata)
}

Check warning on line 48 in zarrs/src/array/array_async_readable.rs

View check run for this annotation

Codecov / codecov/patch

zarrs/src/array/array_async_readable.rs#L45-L48

Added lines #L45 - L48 were not covered by tests

async fn async_open_metadata(
storage: Arc<TStorage>,
path: &str,
version: &MetadataRetrieveVersion,
) -> Result<ArrayMetadata, ArrayCreateError> {

Check warning on line 54 in zarrs/src/array/array_async_readable.rs

View check run for this annotation

Codecov / codecov/patch

zarrs/src/array/array_async_readable.rs#L50-L54

Added lines #L50 - L54 were not covered by tests
let node_path = NodePath::new(path)?;

if let MetadataRetrieveVersion::Default | MetadataRetrieveVersion::V3 = version {
Expand All @@ -50,7 +60,7 @@
if let Some(metadata) = storage.get(&key_v3).await? {
let metadata: ArrayMetadataV3 = serde_json::from_slice(&metadata)
.map_err(|err| StorageError::InvalidMetadata(key_v3, err.to_string()))?;
return Self::new_with_metadata(storage, path, ArrayMetadata::V3(metadata));
return Ok(ArrayMetadata::V3(metadata));

Check warning on line 63 in zarrs/src/array/array_async_readable.rs

View check run for this annotation

Codecov / codecov/patch

zarrs/src/array/array_async_readable.rs#L63

Added line #L63 was not covered by tests
}
}

Expand All @@ -69,7 +79,7 @@
})?;
}

return Self::new_with_metadata(storage, path, ArrayMetadata::V2(metadata));
return Ok(ArrayMetadata::V2(metadata));

Check warning on line 82 in zarrs/src/array/array_async_readable.rs

View check run for this annotation

Codecov / codecov/patch

zarrs/src/array/array_async_readable.rs#L82

Added line #L82 was not covered by tests
}
}

Expand Down
14 changes: 12 additions & 2 deletions zarrs/src/array/array_sync_readable.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,16 @@ impl<TStorage: ?Sized + ReadableStorageTraits + 'static> Array<TStorage> {
path: &str,
version: &MetadataRetrieveVersion,
) -> Result<Self, ArrayCreateError> {
let metadata = Self::open_metadata(&storage, path, version)?;
Self::validate_metadata(&metadata)?;
Self::new_with_metadata(storage, path, metadata)
}

fn open_metadata(
storage: &Arc<TStorage>,
path: &str,
version: &MetadataRetrieveVersion,
) -> Result<ArrayMetadata, ArrayCreateError> {
let node_path = NodePath::new(path)?;

if let MetadataRetrieveVersion::Default | MetadataRetrieveVersion::V3 = version {
Expand All @@ -55,7 +65,7 @@ impl<TStorage: ?Sized + ReadableStorageTraits + 'static> Array<TStorage> {
if let Some(metadata) = storage.get(&key_v3)? {
let metadata: ArrayMetadataV3 = serde_json::from_slice(&metadata)
.map_err(|err| StorageError::InvalidMetadata(key_v3, err.to_string()))?;
return Self::new_with_metadata(storage, path, ArrayMetadata::V3(metadata));
return Ok(ArrayMetadata::V3(metadata));
}
}

Expand All @@ -74,7 +84,7 @@ impl<TStorage: ?Sized + ReadableStorageTraits + 'static> Array<TStorage> {
})?;
}

return Self::new_with_metadata(storage, path, ArrayMetadata::V2(metadata));
return Ok(ArrayMetadata::V2(metadata));
}
}

Expand Down
55 changes: 50 additions & 5 deletions zarrs/src/group.rs
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,22 @@
self
}
}

/// Reject the group if it contains additional fields with `"must_understand": true`.
fn validate_metadata(metadata: &GroupMetadata) -> Result<(), GroupCreateError> {
let additional_fields = match &metadata {
GroupMetadata::V2(metadata) => &metadata.additional_fields,

Check warning on line 209 in zarrs/src/group.rs

View check run for this annotation

Codecov / codecov/patch

zarrs/src/group.rs#L209

Added line #L209 was not covered by tests
GroupMetadata::V3(metadata) => &metadata.additional_fields,
};
for (name, field) in additional_fields {
if field.must_understand() {
return Err(GroupCreateError::UnsupportedAdditionalFieldError(
UnsupportedAdditionalFieldError::new(name.clone(), field.as_value().clone()),
));
}

Check warning on line 217 in zarrs/src/group.rs

View check run for this annotation

Codecov / codecov/patch

zarrs/src/group.rs#L217

Added line #L217 was not covered by tests
}
Ok(())
}
}

impl<TStorage: ?Sized + ReadableStorageTraits> Group<TStorage> {
Expand All @@ -224,15 +240,24 @@
path: &str,
version: &MetadataRetrieveVersion,
) -> Result<Self, GroupCreateError> {
let node_path = path.try_into()?;
let metadata = Self::open_metadata(&storage, path, version)?;
Self::validate_metadata(&metadata)?;
Self::new_with_metadata(storage, path, metadata)
}

fn open_metadata(
storage: &Arc<TStorage>,
path: &str,
version: &MetadataRetrieveVersion,
) -> Result<GroupMetadata, GroupCreateError> {
let node_path = path.try_into()?;
if let MetadataRetrieveVersion::Default | MetadataRetrieveVersion::V3 = version {
// Try Zarr V3
let key_v3 = meta_key_v3(&node_path);
if let Some(metadata) = storage.get(&key_v3)? {
let metadata: GroupMetadataV3 = serde_json::from_slice(&metadata)
.map_err(|err| StorageError::InvalidMetadata(key_v3, err.to_string()))?;
return Self::new_with_metadata(storage, path, GroupMetadata::V3(metadata));
return Ok(GroupMetadata::V3(metadata));
}
}

Expand All @@ -249,7 +274,7 @@
StorageError::InvalidMetadata(attributes_key, err.to_string())
})?;
}
return Self::new_with_metadata(storage, path, GroupMetadata::V2(metadata));
return Ok(GroupMetadata::V2(metadata));

Check warning on line 277 in zarrs/src/group.rs

View check run for this annotation

Codecov / codecov/patch

zarrs/src/group.rs#L277

Added line #L277 was not covered by tests
}
}

Expand Down Expand Up @@ -373,6 +398,16 @@
path: &str,
version: &MetadataRetrieveVersion,
) -> Result<Self, GroupCreateError> {
let metadata = Self::async_open_metadata(storage.clone(), path, version).await?;
Self::validate_metadata(&metadata)?;
Self::new_with_metadata(storage, path, metadata)
}

async fn async_open_metadata(
storage: Arc<TStorage>,
path: &str,
version: &MetadataRetrieveVersion,
) -> Result<GroupMetadata, GroupCreateError> {
let node_path = path.try_into()?;

if let MetadataRetrieveVersion::Default | MetadataRetrieveVersion::V3 = version {
Expand All @@ -381,7 +416,7 @@
if let Some(metadata) = storage.get(&key_v3).await? {
let metadata: GroupMetadataV3 = serde_json::from_slice(&metadata)
.map_err(|err| StorageError::InvalidMetadata(key_v3, err.to_string()))?;
return Self::new_with_metadata(storage, path, GroupMetadata::V3(metadata));
return Ok(GroupMetadata::V3(metadata));
}
}

Expand All @@ -398,7 +433,7 @@
StorageError::InvalidMetadata(attributes_key, err.to_string())
})?;
}
return Self::new_with_metadata(storage, path, GroupMetadata::V2(metadata));
return Ok(GroupMetadata::V2(metadata));

Check warning on line 436 in zarrs/src/group.rs

View check run for this annotation

Codecov / codecov/patch

zarrs/src/group.rs#L436

Added line #L436 was not covered by tests
}
}

Expand Down Expand Up @@ -773,6 +808,16 @@
.get("unknown")
.unwrap()
.must_understand());

// Permit manual creation of group with unsupported metadata
let storage = Arc::new(MemoryStore::new());
let group =
Group::new_with_metadata(storage.clone(), "/", group_metadata.clone().into()).unwrap();
group.store_metadata().unwrap();

// Group opening should fail with unsupported metadata
let group = Group::open(storage, "/");
assert!(group.is_err());
}

#[test]
Expand Down