reth_era/common/
file_ops.rs

1//! Era file format traits and I/O operations.
2
3use crate::e2s::{error::E2sError, types::Version};
4use std::{
5    fs::File,
6    io::{Read, Seek, Write},
7    path::Path,
8};
9
10/// Represents era file with generic content and identifier types
11pub trait EraFileFormat: Sized {
12    /// Content group type
13    type EraGroup;
14
15    /// The identifier type
16    type Id: EraFileId;
17
18    /// Get the version
19    fn version(&self) -> &Version;
20
21    /// Get the content group
22    fn group(&self) -> &Self::EraGroup;
23
24    /// Get the file identifier
25    fn id(&self) -> &Self::Id;
26
27    /// Create a new instance
28    fn new(group: Self::EraGroup, id: Self::Id) -> Self;
29}
30
31/// Era file identifiers
32pub trait EraFileId: Clone {
33    /// File type for this identifier
34    const FILE_TYPE: EraFileType;
35
36    /// Number of items, slots for `era`, blocks for `era1`, per era
37    const ITEMS_PER_ERA: u64;
38
39    /// Get the network name
40    fn network_name(&self) -> &str;
41
42    /// Get the starting number (block or slot)
43    fn start_number(&self) -> u64;
44
45    /// Get the count of items
46    fn count(&self) -> u32;
47
48    /// Get the optional hash identifier
49    fn hash(&self) -> Option<[u8; 4]>;
50
51    /// Whether to include era count in filename
52    fn include_era_count(&self) -> bool;
53
54    /// Calculate era number
55    fn era_number(&self) -> u64 {
56        self.start_number() / Self::ITEMS_PER_ERA
57    }
58
59    /// Calculate the number of eras spanned per file.
60    ///
61    /// If the user can decide how many slots/blocks per era file there are, we need to calculate
62    /// it. Most of the time it should be 1, but it can never be more than 2 eras per file
63    /// as there is a maximum of 8192 slots/blocks per era file.
64    fn era_count(&self) -> u64 {
65        if self.count() == 0 {
66            return 0;
67        }
68        let first_era = self.era_number();
69        let last_number = self.start_number() + self.count() as u64 - 1;
70        let last_era = last_number / Self::ITEMS_PER_ERA;
71        last_era - first_era + 1
72    }
73
74    /// Convert to standardized file name.
75    fn to_file_name(&self) -> String {
76        Self::FILE_TYPE.format_filename(
77            self.network_name(),
78            self.era_number(),
79            self.hash(),
80            self.include_era_count(),
81            self.era_count(),
82        )
83    }
84}
85
86/// [`StreamReader`] for reading era-format files
87pub trait StreamReader<R: Read + Seek>: Sized {
88    /// The file type the reader produces
89    type File: EraFileFormat;
90
91    /// The iterator type for streaming data
92    type Iterator;
93
94    /// Create a new reader
95    fn new(reader: R) -> Self;
96
97    /// Read and parse the complete file
98    fn read(self, network_name: String) -> Result<Self::File, E2sError>;
99
100    /// Get an iterator for streaming processing
101    fn iter(self) -> Self::Iterator;
102}
103
104/// [`FileReader`] provides reading era file operations for era files
105pub trait FileReader: StreamReader<File> {
106    /// Opens and reads an era file from the given path
107    fn open<P: AsRef<Path>>(
108        path: P,
109        network_name: impl Into<String>,
110    ) -> Result<Self::File, E2sError> {
111        let file = File::open(path).map_err(E2sError::Io)?;
112        let reader = Self::new(file);
113        reader.read(network_name.into())
114    }
115}
116
117/// [`StreamWriter`] for writing era-format files
118pub trait StreamWriter<W: Write>: Sized {
119    /// The file type this writer handles
120    type File: EraFileFormat;
121
122    /// Create a new writer
123    fn new(writer: W) -> Self;
124
125    /// Writer version
126    fn write_version(&mut self) -> Result<(), E2sError>;
127
128    /// Write a complete era file
129    fn write_file(&mut self, file: &Self::File) -> Result<(), E2sError>;
130
131    /// Flush any buffered data
132    fn flush(&mut self) -> Result<(), E2sError>;
133}
134
135/// [`StreamWriter`] provides writing file operations for era files
136pub trait FileWriter {
137    /// Era file type the writer handles
138    type File: EraFileFormat<Id: EraFileId>;
139
140    /// Creates a new file at the specified path and writes the era file to it
141    fn create<P: AsRef<Path>>(path: P, file: &Self::File) -> Result<(), E2sError>;
142
143    /// Creates a file in the directory using standardized era naming
144    fn create_with_id<P: AsRef<Path>>(directory: P, file: &Self::File) -> Result<(), E2sError>;
145}
146
147impl<T: StreamWriter<File>> FileWriter for T {
148    type File = T::File;
149
150    /// Creates a new file at the specified path and writes the era file to it
151    fn create<P: AsRef<Path>>(path: P, file: &Self::File) -> Result<(), E2sError> {
152        let file_handle = File::create(path).map_err(E2sError::Io)?;
153        let mut writer = Self::new(file_handle);
154        writer.write_file(file)?;
155        Ok(())
156    }
157
158    /// Creates a file in the directory using standardized era naming
159    fn create_with_id<P: AsRef<Path>>(directory: P, file: &Self::File) -> Result<(), E2sError> {
160        let filename = file.id().to_file_name();
161        let path = directory.as_ref().join(filename);
162        Self::create(path, file)
163    }
164}
165
166/// Era file type identifier
167#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
168pub enum EraFileType {
169    /// Consensus layer ERA file, `.era`
170    /// Contains beacon blocks and states
171    Era,
172    /// Execution layer ERA1 file, `.era1`
173    /// Contains execution blocks pre-merge
174    Era1,
175}
176
177impl EraFileType {
178    /// Get the file extension for this type, dot included
179    pub const fn extension(&self) -> &'static str {
180        match self {
181            Self::Era => ".era",
182            Self::Era1 => ".era1",
183        }
184    }
185
186    /// Detect file type from a filename
187    pub fn from_filename(filename: &str) -> Option<Self> {
188        if filename.ends_with(".era") {
189            Some(Self::Era)
190        } else if filename.ends_with(".era1") {
191            Some(Self::Era1)
192        } else {
193            None
194        }
195    }
196
197    /// Generate era file name.
198    ///
199    /// Standard format: `<config-name>-<era-number>-<short-historical-root>.<ext>`
200    /// See also <https://github.com/eth-clients/e2store-format-specs/blob/main/formats/era.md#file-name>
201    ///
202    /// With era count (for custom exports):
203    /// `<config-name>-<era-number>-<era-count>-<short-historical-root>.<ext>`
204    pub fn format_filename(
205        &self,
206        network_name: &str,
207        era_number: u64,
208        hash: Option<[u8; 4]>,
209        include_era_count: bool,
210        era_count: u64,
211    ) -> String {
212        let hash = format_hash(hash);
213
214        if include_era_count {
215            format!(
216                "{}-{:05}-{:05}-{}{}",
217                network_name,
218                era_number,
219                era_count,
220                hash,
221                self.extension()
222            )
223        } else {
224            format!("{}-{:05}-{}{}", network_name, era_number, hash, self.extension())
225        }
226    }
227
228    /// Detect file type from URL
229    /// By default, it assumes `Era` type
230    pub fn from_url(url: &str) -> Self {
231        if url.contains("era1") {
232            Self::Era1
233        } else {
234            Self::Era
235        }
236    }
237}
238
239/// Format hash as hex string, or placeholder if none
240pub fn format_hash(hash: Option<[u8; 4]>) -> String {
241    match hash {
242        Some(h) => format!("{:02x}{:02x}{:02x}{:02x}", h[0], h[1], h[2], h[3]),
243        None => "00000000".to_string(),
244    }
245}