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