Skip to main content

fxfs/
blob_metadata.rs

1// Copyright 2026 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5use crate::errors::FxfsError;
6use crate::lsm_tree::Query;
7use crate::lsm_tree::types::{ItemRef, LayerIterator};
8use crate::object_handle::ObjectHandle;
9use crate::object_store::object_record::{AttributeKey, ObjectKey, ObjectKeyData, ObjectValue};
10use crate::object_store::{AttributeId, DataObjectHandle, HandleOwner, StoreObjectHandle};
11use crate::serialized_types::{Versioned, VersionedLatest};
12use anyhow::{Context, Error};
13use fuchsia_merkle::{Hash, LeafHashCollector, MerkleVerifier};
14
15pub use blob_metadata::{
16    BlobFormat, BlobFormatV53, BlobMetadata, BlobMetadataUnversioned, BlobMetadataV53, MerkleLeaves,
17};
18
19pub trait FxfsBlobMetadataExt: Sized {
20    /// Reads the blob metadata from an attribute on `blob_object`. If the attribute doesn't exist
21    /// then it's assumed to be `BlobMetadata::empty()`.
22    fn read_from<S: HandleOwner>(
23        blob_object: &StoreObjectHandle<S>,
24    ) -> impl std::future::Future<Output = Result<Self, Error>> + Send;
25
26    /// Writes the metadata to the `AttributeId::BLOB_METADATA` attribute on `blob_object`. If the
27    /// metadata is equal to `BlobMetadata::empty()` then the attribute isn't written.
28    fn write_to<S: HandleOwner>(
29        &self,
30        blob_object: &DataObjectHandle<S>,
31    ) -> impl std::future::Future<Output = Result<(), Error>> + Send;
32
33    /// Returns the size of the serialized metadata. If the metadata is equal to
34    /// `BlobMetadata::empty()` then the metadata won't get written, so 0 is returned.
35    fn serialized_size(&self) -> Result<usize, Error>;
36
37    /// Consumes the metadata and turns it into a `MerkleVerifier`.
38    fn into_merkle_verifier(self, root: Hash) -> Result<MerkleVerifier, Error>;
39}
40
41impl FxfsBlobMetadataExt for BlobMetadata {
42    fn read_from<S: HandleOwner>(
43        blob_object: &StoreObjectHandle<S>,
44    ) -> impl std::future::Future<Output = Result<Self, Error>> + Send {
45        async move {
46            let store = blob_object.store();
47            let layer_set = store.tree().layer_set();
48            let mut merger = layer_set.merger();
49            // A blob should never have both attributes and also should never have the fs-verity
50            // attribute which is ordered between them. Querying for `AttributeId::BLOB_MERKLE` will
51            // have the iterator point to that attribute if it exists. If it doesn't exist then the
52            // iterator will point the next item which will be the `AttributeId::BLOB_METADATA`
53            // attribute if it exists.
54            static_assertions::const_assert!(
55                AttributeId::BLOB_MERKLE.raw() < AttributeId::BLOB_METADATA.raw()
56            );
57            let key = ObjectKey::attribute(
58                blob_object.object_id(),
59                AttributeId::BLOB_MERKLE,
60                AttributeKey::Attribute,
61            );
62            let iter = merger.query(Query::FullRange(&key)).await?;
63            match iter.get() {
64                Some(ItemRef {
65                    key:
66                        ObjectKey {
67                            object_id,
68                            data:
69                                ObjectKeyData::Attribute(
70                                    AttributeId::BLOB_MERKLE,
71                                    AttributeKey::Attribute,
72                                ),
73                        },
74                    value,
75                    ..
76                }) if *object_id == blob_object.object_id() => match value {
77                    ObjectValue::Attribute { .. } => {
78                        let serialized_metadata = blob_object.read_attr_from_iter(iter).await?;
79                        let old_metadata: BlobMetadataUnversioned =
80                            bincode::deserialize_from(&*serialized_metadata)?;
81                        Ok(Self::from(old_metadata))
82                    }
83                    _ => Err(FxfsError::Inconsistent.into()),
84                },
85                Some(ItemRef {
86                    key:
87                        ObjectKey {
88                            object_id,
89                            data:
90                                ObjectKeyData::Attribute(
91                                    AttributeId::BLOB_METADATA,
92                                    AttributeKey::Attribute,
93                                ),
94                        },
95                    value,
96                    ..
97                }) if *object_id == blob_object.object_id() => match value {
98                    ObjectValue::Attribute { .. } => {
99                        let serialized_metadata = blob_object.read_attr_from_iter(iter).await?;
100                        Ok(Self::deserialize_with_version(&mut &*serialized_metadata)?.0)
101                    }
102                    _ => Err(FxfsError::Inconsistent.into()),
103                },
104                Some(ItemRef {
105                    key:
106                        ObjectKey {
107                            object_id,
108                            data:
109                                ObjectKeyData::Attribute(
110                                    AttributeId::FSVERITY_MERKLE,
111                                    AttributeKey::Attribute,
112                                ),
113                        },
114                    ..
115                }) if *object_id == blob_object.object_id() => {
116                    // Blobs should not have the fs-verity attribute. This is explicitly checked
117                    // because the fs-verity attribute is ordered between the 2 blob metadata
118                    // attributes.  `AttributeId::BLOB_MERKLE` was queried for with the expectation
119                    // of finding either blob attribute. Finding the fs-verity attribute could be
120                    // hiding the `AttributeId::BLOB_METADATA` attribute.
121                    Err(FxfsError::Inconsistent.into())
122                }
123                // Neither attribute exists.
124                _ => Ok(Self::empty()),
125            }
126        }
127    }
128
129    fn write_to<S: HandleOwner>(
130        &self,
131        blob_object: &DataObjectHandle<S>,
132    ) -> impl std::future::Future<Output = Result<(), Error>> + Send {
133        async move {
134            // Don't write the attribute when there's no metadata.
135            if self.is_empty() {
136                return Ok(());
137            }
138            let mut buf = Vec::new();
139            self.serialize_with_version(&mut buf)?;
140            blob_object
141                .write_attr(AttributeId::BLOB_METADATA, &buf)
142                .await
143                .context("Failed to write blob metadata attribute.")
144        }
145    }
146
147    fn serialized_size(&self) -> Result<usize, Error> {
148        if self.is_empty() {
149            return Ok(0);
150        }
151        struct CountingWriter(usize);
152        impl std::io::Write for CountingWriter {
153            fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
154                self.0 += buf.len();
155                Ok(buf.len())
156            }
157            fn flush(&mut self) -> std::io::Result<()> {
158                Ok(())
159            }
160        }
161        let mut writer = CountingWriter(0);
162        self.serialize_with_version(&mut writer)?;
163        Ok(writer.0)
164    }
165
166    fn into_merkle_verifier(self, root: Hash) -> Result<MerkleVerifier, Error> {
167        Ok(BlobMetadataV53::into_merkle_verifier(self, root)?)
168    }
169}
170
171impl Versioned for BlobMetadataV53 {
172    fn max_serialized_size() -> Option<u64> {
173        // There's no restriction on the size of the blob metadata.
174        None
175    }
176}
177
178#[derive(Default)]
179pub struct BlobMetadataLeafHashCollector(MerkleLeaves);
180
181impl BlobMetadataLeafHashCollector {
182    pub fn new() -> Self {
183        Self(Vec::new())
184    }
185}
186
187impl LeafHashCollector for BlobMetadataLeafHashCollector {
188    type Output = (Hash, MerkleLeaves);
189
190    fn add_leaf_hash(&mut self, hash: Hash) {
191        self.0.push(hash.into())
192    }
193
194    fn complete(mut self, root: Hash) -> Self::Output {
195        // If the there's only 1 hash then it's the root and doesn't get stored in the metadata.
196        if self.0.len() == 1 {
197            debug_assert!(*root == self.0[0]);
198            self.0 = Vec::new();
199        }
200        (root, self.0)
201    }
202}
203
204#[cfg(test)]
205mod tests {
206    use super::BlobMetadata;
207    use crate::blob_metadata::{
208        BlobFormat, BlobMetadataLeafHashCollector, BlobMetadataUnversioned, FxfsBlobMetadataExt,
209    };
210    use crate::filesystem::{FxFilesystem, OpenFxFilesystem};
211    use crate::object_store::transaction::{LockKey, Options, lock_keys};
212    use crate::object_store::{
213        AttributeId, DataObjectHandle, Directory, HandleOptions, ObjectStore,
214    };
215    use assert_matches::assert_matches;
216    use fuchsia_merkle::MerkleRootBuilder;
217    use storage_device::DeviceHolder;
218    use storage_device::fake_device::FakeDevice;
219
220    const TEST_DEVICE_BLOCK_SIZE: u32 = 512;
221    const TEST_DEVICE_BLOCK_COUNT: u64 = 16 * 1024;
222    const TEST_OBJECT_NAME: &str = "foo";
223
224    async fn test_filesystem() -> OpenFxFilesystem {
225        let device =
226            DeviceHolder::new(FakeDevice::new(TEST_DEVICE_BLOCK_COUNT, TEST_DEVICE_BLOCK_SIZE));
227        FxFilesystem::new_empty(device).await.expect("new_empty failed")
228    }
229
230    async fn test_filesystem_and_empty_object() -> (OpenFxFilesystem, DataObjectHandle<ObjectStore>)
231    {
232        let fs = test_filesystem().await;
233        let store = fs.root_store();
234
235        let mut transaction = fs
236            .root_store()
237            .new_transaction(
238                lock_keys![LockKey::object(
239                    store.store_object_id(),
240                    store.root_directory_object_id()
241                )],
242                Options::default(),
243            )
244            .await
245            .expect("new_transaction failed");
246
247        let object =
248            ObjectStore::create_object(&store, &mut transaction, HandleOptions::default(), None)
249                .await
250                .expect("create_object failed");
251
252        let root_directory =
253            Directory::open(&store, store.root_directory_object_id()).await.expect("open failed");
254        root_directory
255            .add_child_file(&mut transaction, TEST_OBJECT_NAME, &object)
256            .await
257            .expect("add_child_file failed");
258
259        transaction.commit().await.expect("commit failed");
260
261        (fs, object)
262    }
263
264    #[fuchsia::test(threads = 3)]
265    async fn test_write_read_zstd() {
266        let (fs, object) = test_filesystem_and_empty_object().await;
267
268        let metadata = BlobMetadata {
269            merkle_leaves: vec![[1; 32], [2; 32], [3; 32], [4; 32]],
270            format: BlobFormat::ChunkedZstd {
271                uncompressed_size: 128 * 1024,
272                chunk_size: 32 * 1024,
273                compressed_offsets: vec![0, 100, 200, 400],
274            },
275        };
276        metadata.write_to(&object).await.expect("failed to write attribute");
277        let read_metadata =
278            BlobMetadata::read_from(&object).await.expect("failed to read attribute");
279        assert_eq!(read_metadata, metadata);
280
281        fs.close().await.expect("close failed");
282    }
283
284    #[fuchsia::test(threads = 3)]
285    async fn test_write_read_lz4() {
286        let (fs, object) = test_filesystem_and_empty_object().await;
287
288        let metadata = BlobMetadata {
289            merkle_leaves: vec![[1; 32], [2; 32], [3; 32], [4; 32]],
290            format: BlobFormat::ChunkedLz4 {
291                uncompressed_size: 128 * 1024,
292                chunk_size: 32 * 1024,
293                compressed_offsets: vec![0, 100, 200, 400],
294            },
295        };
296        metadata.write_to(&object).await.expect("failed to write attribute");
297        let read_metadata =
298            BlobMetadata::read_from(&object).await.expect("failed to read attribute");
299        assert_eq!(read_metadata, metadata);
300
301        fs.close().await.expect("close failed");
302    }
303
304    #[fuchsia::test(threads = 3)]
305    async fn test_empty_attribute_is_not_written() {
306        let (fs, object) = test_filesystem_and_empty_object().await;
307
308        BlobMetadata::empty().write_to(&object).await.expect("failed to write attribute");
309        let result = object
310            .read_attr(AttributeId::BLOB_METADATA)
311            .await
312            .expect("reading the attribute failed");
313        assert_eq!(result, None);
314
315        fs.close().await.expect("close failed");
316    }
317
318    #[fuchsia::test(threads = 3)]
319    async fn test_read_corrupt_attribute_fails() {
320        let (fs, object) = test_filesystem_and_empty_object().await;
321
322        object
323            .write_attr(AttributeId::BLOB_METADATA, b"garbage")
324            .await
325            .expect("failed to write attribute");
326        BlobMetadata::read_from(&object).await.expect_err("reading the metadata should fail");
327
328        fs.close().await.expect("close failed");
329    }
330
331    #[fuchsia::test(threads = 3)]
332    async fn test_read_unversioned_attribute() {
333        let (fs, object) = test_filesystem_and_empty_object().await;
334
335        let unversioned_metadata = BlobMetadataUnversioned {
336            hashes: vec![[1; 32], [2; 32]],
337            chunk_size: 32 * 1024,
338            compressed_offsets: vec![0],
339            uncompressed_size: 15 * 1024,
340        };
341        let mut buf = Vec::new();
342        bincode::serialize_into(&mut buf, &unversioned_metadata)
343            .expect("failed to serialize metadata");
344        object.write_attr(AttributeId::BLOB_MERKLE, &buf).await.expect("failed to write attribute");
345        let metadata = BlobMetadata::read_from(&object).await.expect("failed to read attribute");
346        assert_eq!(metadata, BlobMetadata::from(unversioned_metadata));
347
348        fs.close().await.expect("close failed");
349    }
350
351    #[fuchsia::test(threads = 3)]
352    async fn test_read_corrupt_unversioned_attribute_fails() {
353        let (fs, object) = test_filesystem_and_empty_object().await;
354
355        object
356            .write_attr(AttributeId::BLOB_MERKLE, b"garbage")
357            .await
358            .expect("failed to write attribute");
359        BlobMetadata::read_from(&object).await.expect_err("reading the metadata should fail");
360
361        fs.close().await.expect("close failed");
362    }
363
364    #[fuchsia::test(threads = 3)]
365    async fn test_fs_verity_hides_blob_metadata() {
366        let (fs, object) = test_filesystem_and_empty_object().await;
367
368        let metadata = BlobMetadata {
369            merkle_leaves: vec![[1; 32], [2; 32]],
370            format: BlobFormat::Uncompressed,
371        };
372        metadata.write_to(&object).await.expect("failed to write attribute");
373        object
374            .write_attr(AttributeId::FSVERITY_MERKLE, b"fs-verify")
375            .await
376            .expect("failed to write fs-verity attribute");
377        BlobMetadata::read_from(&object).await.expect_err("fs-verity should have been found");
378
379        fs.close().await.expect("close failed");
380    }
381
382    #[fuchsia::test]
383    async fn test_serialized_size() {
384        assert_matches!(BlobMetadata::empty().serialized_size(), Ok(0));
385        assert_matches!(
386            BlobMetadata {
387                merkle_leaves: vec![[54; 32], [55; 32]],
388                format: BlobFormat::Uncompressed,
389            }
390            .serialized_size(),
391            // 4 bytes for the version.
392            // 1 byte for the count of merkle leaves.
393            // 64 bytes of merkle leaves.
394            // 1 byte discriminant for the format.
395            Ok(70)
396        );
397        assert_matches!(
398            BlobMetadata {
399                merkle_leaves: vec![[54; 32], [55; 32]],
400                format: BlobFormat::ChunkedZstd {
401                    uncompressed_size: 128 * 1024,
402                    chunk_size: 32 * 1024,
403                    compressed_offsets: vec![0, 100, 200, 400],
404                },
405            }
406            .serialized_size(),
407            // 4 bytes for the version.
408            // 1 byte for the count of merkle leaves.
409            // 64 bytes of merkle leaves.
410            // 1 byte discriminant for the format.
411            // 5 bytes for the uncompressed size.
412            // 3 bytes for the chunk size.
413            // 1 byte for the count of compressed offsets.
414            // 6 bytes of compressed offsets.
415            Ok(85)
416        );
417    }
418
419    #[fuchsia::test]
420    fn test_leaf_hash_collector_with_only_root() {
421        let data = vec![3; 4096];
422        let (_root, leaves) =
423            MerkleRootBuilder::new(BlobMetadataLeafHashCollector::new()).complete(&data);
424        assert!(leaves.is_empty());
425    }
426
427    #[fuchsia::test]
428    fn test_leaf_hash_collector_with_leaves() {
429        let data = vec![3; 12 * 1024];
430        let (_root, leaves) =
431            MerkleRootBuilder::new(BlobMetadataLeafHashCollector::new()).complete(&data);
432        assert_eq!(leaves.len(), 2);
433    }
434
435    #[fuchsia::test]
436    fn test_into_merkle_verifier_with_only_root() {
437        let data = vec![3; 4096];
438        let (root, leaves) =
439            MerkleRootBuilder::new(BlobMetadataLeafHashCollector::new()).complete(&data);
440        let metadata = BlobMetadata { merkle_leaves: leaves, format: BlobFormat::Uncompressed };
441        let verifier =
442            metadata.into_merkle_verifier(root).expect("failed to create merkle verifier");
443        verifier.verify(0, &data).expect("failed to verify data");
444    }
445
446    #[fuchsia::test]
447    fn test_into_merkle_verifier_with_leaves() {
448        let data = vec![3; 12 * 1024];
449        let (root, leaves) =
450            MerkleRootBuilder::new(BlobMetadataLeafHashCollector::new()).complete(&data);
451        let metadata = BlobMetadata { merkle_leaves: leaves, format: BlobFormat::Uncompressed };
452        let verifier =
453            metadata.into_merkle_verifier(root).expect("failed to create merkle verifier");
454        verifier.verify(0, &data).expect("failed to verify data");
455    }
456
457    #[fuchsia::test]
458    fn test_convert_unversioned_to_versioned() {
459        assert_eq!(
460            BlobMetadata::from(BlobMetadataUnversioned {
461                hashes: vec![[1; 32], [2; 32]],
462                chunk_size: 0,
463                compressed_offsets: vec![],
464                uncompressed_size: 15 * 1024,
465            }),
466            BlobMetadata {
467                merkle_leaves: vec![[1; 32], [2; 32]],
468                format: BlobFormat::Uncompressed,
469            }
470        );
471
472        assert_eq!(
473            BlobMetadata::from(BlobMetadataUnversioned {
474                hashes: vec![[1; 32], [2; 32], [3; 32], [4; 32]],
475                chunk_size: 32 * 1024,
476                compressed_offsets: vec![0, 100],
477                uncompressed_size: 33 * 1024,
478            }),
479            BlobMetadata {
480                merkle_leaves: vec![[1; 32], [2; 32], [3; 32], [4; 32]],
481                format: BlobFormat::ChunkedZstd {
482                    uncompressed_size: 33 * 1024,
483                    chunk_size: 32 * 1024,
484                    compressed_offsets: vec![0, 100]
485                },
486            }
487        );
488    }
489
490    #[fuchsia::test]
491    fn test_merkle_serialization() {}
492}