Skip to main content

forest/db/car/
any.rs

1// Copyright 2019-2026 ChainSafe Systems
2// SPDX-License-Identifier: Apache-2.0, MIT
3
4//! There are three different CAR formats: `.car`, `.car.zst` and
5//! `.forest.car.zst`. [`AnyCar`] identifies the format by inspecting the CAR
6//! header and the first key-value block, and picks the appropriate block store
7//! (either [`super::ForestCar`] or [`super::PlainCar`]).
8//!
9//! CARv2 is not supported yet.
10
11use super::{CacheKey, RandomAccessFileReader, ZstdFrameCache};
12use crate::blocks::{Tipset, TipsetKey};
13use crate::chain::FilecoinSnapshotMetadata;
14use crate::prelude::*;
15use crate::utils::db::car_stream::CarV1Header;
16use crate::utils::io::EitherMmapOrRandomAccessFile;
17use itertools::Either;
18use positioned_io::ReadAt;
19use std::{
20    borrow::Cow,
21    io::{Error, ErrorKind, Read, Result},
22    path::{Path, PathBuf},
23};
24
25#[derive(derive_more::From)]
26pub enum AnyCar<ReaderT> {
27    Plain(super::PlainCar<ReaderT>),
28    Forest(super::ForestCar<ReaderT>),
29    #[from(skip)]
30    Memory(super::PlainCar<Vec<u8>>),
31}
32
33impl<ReaderT: RandomAccessFileReader> AnyCar<ReaderT> {
34    /// Open an archive. May be formatted as `.car`, `.car.zst` or
35    /// `.forest.car.zst`. This call may block for an indeterminate amount of
36    /// time while data is decoded and indexed.
37    pub fn new(reader: ReaderT) -> Result<Self> {
38        if let Ok(validation_result) = super::ForestCar::validate_car(&reader) {
39            return Ok(
40                super::ForestCar::new_from_validation_result(reader, validation_result)?.into(),
41            );
42        }
43
44        // Maybe use a tempfile for this in the future.
45        if let Ok(decompressed) = zstd::stream::decode_all(positioned_io::Cursor::new(&reader))
46            && let Ok(mem_car) = super::PlainCar::new(decompressed)
47        {
48            return Ok(AnyCar::Memory(mem_car));
49        }
50
51        if let Ok(plain_car) = super::PlainCar::new(reader) {
52            return Ok(plain_car.into());
53        }
54        Err(Error::new(
55            ErrorKind::InvalidData,
56            "input not recognized as any kind of CAR data (.car, .car.zst, .forest.car)",
57        ))
58    }
59
60    pub fn header_v1(&self) -> &CarV1Header {
61        match self {
62            AnyCar::Forest(forest) => forest.header_v1(),
63            AnyCar::Plain(plain) => plain.header_v1(),
64            AnyCar::Memory(mem) => mem.header_v1(),
65        }
66    }
67
68    pub fn metadata(&self) -> Option<&FilecoinSnapshotMetadata> {
69        match self {
70            AnyCar::Forest(forest) => forest.metadata(),
71            AnyCar::Plain(plain) => plain.metadata(),
72            AnyCar::Memory(mem) => mem.metadata(),
73        }
74    }
75
76    pub fn heaviest_tipset_key(&self) -> TipsetKey {
77        match self {
78            AnyCar::Forest(forest) => forest.heaviest_tipset_key(),
79            AnyCar::Plain(plain) => plain.heaviest_tipset_key(),
80            AnyCar::Memory(mem) => mem.heaviest_tipset_key(),
81        }
82    }
83
84    /// Filecoin archives are tagged with the heaviest tipset. This call may
85    /// fail if the archive is corrupt or if it is not a Filecoin archive.
86    pub fn heaviest_tipset(&self) -> anyhow::Result<Tipset> {
87        match self {
88            AnyCar::Forest(forest) => forest.heaviest_tipset(),
89            AnyCar::Plain(plain) => plain.heaviest_tipset(),
90            AnyCar::Memory(mem) => mem.heaviest_tipset(),
91        }
92    }
93
94    /// Return the identified CAR format variant. There are three variants:
95    /// `CARv1`, `CARv2`, `CARv1.zst`, `CARv2.zst` and `ForestCARv1.zst`.
96    pub fn variant(&self) -> Cow<'static, str> {
97        match self {
98            AnyCar::Forest(_) => "ForestCARv1.zst".into(),
99            AnyCar::Plain(car) => format!("CARv{}", car.version()).into(),
100            AnyCar::Memory(car) => format!("CARv{}.zst", car.version()).into(),
101        }
102    }
103
104    /// Discard reader type and replace with dynamic trait object.
105    pub fn into_dyn(self) -> AnyCar<Box<dyn super::RandomAccessFileReader>> {
106        match self {
107            AnyCar::Forest(f) => AnyCar::Forest(f.into_dyn()),
108            AnyCar::Plain(p) => AnyCar::Plain(p.into_dyn()),
109            AnyCar::Memory(m) => AnyCar::Memory(m),
110        }
111    }
112
113    /// Set the z-frame cache of the inner CAR reader.
114    pub fn with_cache(self, cache: ZstdFrameCache, key: CacheKey) -> Self {
115        match self {
116            AnyCar::Forest(f) => AnyCar::Forest(f.with_cache(cache, key)),
117            AnyCar::Plain(p) => AnyCar::Plain(p),
118            AnyCar::Memory(m) => AnyCar::Memory(m),
119        }
120    }
121
122    /// Get the index size in bytes
123    pub fn index_size_bytes(&self) -> Option<u64> {
124        match self {
125            Self::Forest(car) => Some(car.index_size_bytes()),
126            _ => None,
127        }
128    }
129
130    /// Gets a reader of the block data by its `Cid`
131    pub fn get_reader(&self, k: Cid) -> anyhow::Result<Option<impl Read>> {
132        match self {
133            Self::Forest(car) => Ok(car.get_reader(k)?.map(Either::Left)),
134            Self::Plain(car) => Ok(car.get_reader(k).map(|r| Either::Right(Either::Left(r)))),
135            Self::Memory(car) => Ok(car.get_reader(k).map(|r| Either::Right(Either::Right(r)))),
136        }
137    }
138}
139
140impl TryFrom<&'static [u8]> for AnyCar<&'static [u8]> {
141    type Error = std::io::Error;
142    fn try_from(bytes: &'static [u8]) -> std::io::Result<Self> {
143        Ok(AnyCar::Plain(super::PlainCar::new(bytes)?))
144    }
145}
146
147impl TryFrom<&Path> for AnyCar<EitherMmapOrRandomAccessFile> {
148    type Error = std::io::Error;
149    fn try_from(path: &Path) -> std::io::Result<Self> {
150        AnyCar::new(EitherMmapOrRandomAccessFile::open(path)?)
151    }
152}
153
154impl TryFrom<&PathBuf> for AnyCar<EitherMmapOrRandomAccessFile> {
155    type Error = std::io::Error;
156    fn try_from(path: &PathBuf) -> std::io::Result<Self> {
157        Self::try_from(path.as_path())
158    }
159}
160
161impl<ReaderT> Blockstore for AnyCar<ReaderT>
162where
163    ReaderT: ReadAt,
164{
165    fn get(&self, k: &Cid) -> anyhow::Result<Option<Vec<u8>>> {
166        match self {
167            AnyCar::Forest(forest) => forest.get(k),
168            AnyCar::Plain(plain) => plain.get(k),
169            AnyCar::Memory(mem) => mem.get(k),
170        }
171    }
172
173    fn put_keyed(&self, k: &Cid, block: &[u8]) -> anyhow::Result<()> {
174        match self {
175            AnyCar::Forest(forest) => forest.put_keyed(k, block),
176            AnyCar::Plain(plain) => plain.put_keyed(k, block),
177            AnyCar::Memory(mem) => mem.put_keyed(k, block),
178        }
179    }
180}
181
182#[cfg(test)]
183mod tests {
184    use super::*;
185    use crate::networks::{calibnet, mainnet};
186
187    #[test]
188    fn forest_any_load_calibnet() {
189        let forest_car = AnyCar::new(calibnet::DEFAULT_GENESIS).unwrap();
190        assert!(forest_car.has(&calibnet::GENESIS_CID).unwrap());
191    }
192
193    #[test]
194    fn forest_any_load_calibnet_zstd() {
195        let data = zstd::encode_all(calibnet::DEFAULT_GENESIS, 3).unwrap();
196        let forest_car = AnyCar::new(data).unwrap();
197        assert!(forest_car.has(&calibnet::GENESIS_CID).unwrap());
198    }
199
200    #[test]
201    fn forest_any_load_mainnet() {
202        let forest_car = AnyCar::new(mainnet::DEFAULT_GENESIS).unwrap();
203        assert!(forest_car.has(&mainnet::GENESIS_CID).unwrap());
204    }
205
206    #[test]
207    fn forest_any_load_mainnet_zstd() {
208        let data = zstd::encode_all(mainnet::DEFAULT_GENESIS, 3).unwrap();
209        let forest_car = AnyCar::new(data).unwrap();
210        assert!(forest_car.has(&mainnet::GENESIS_CID).unwrap());
211    }
212}