Skip to main content

alopex_core/storage/format/
header.rs

1//! ファイルヘッダー定義とシリアライズ/デシリアライズ処理。
2
3use std::convert::TryInto;
4use std::mem;
5
6#[cfg(not(target_arch = "wasm32"))]
7use std::time::{SystemTime, UNIX_EPOCH};
8
9use crate::storage::checksum::ChecksumAlgorithm;
10use crate::storage::compression::CompressionAlgorithm;
11use crate::storage::format::{FileVersion, FormatError, HEADER_SIZE, MAGIC};
12
13/// ファイルフラグ(ビットフィールド)。
14#[repr(C)]
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
16pub struct FileFlags(pub u32);
17
18impl FileFlags {
19    /// 暗号化フラグ。
20    pub const ENCRYPTED: u32 = 1 << 1;
21    /// 値分離フラグ。
22    pub const VALUE_SEPARATED: u32 = 1 << 2;
23
24    /// フラグ値を取得。
25    pub fn bits(&self) -> u32 {
26        self.0
27    }
28
29    /// 暗号化フラグが有効か。
30    pub fn is_encrypted(&self) -> bool {
31        self.0 & Self::ENCRYPTED != 0
32    }
33
34    /// 値分離フラグが有効か。
35    pub fn is_value_separated(&self) -> bool {
36        self.0 & Self::VALUE_SEPARATED != 0
37    }
38}
39
40/// ファイルヘッダー(64バイト固定)。
41///
42/// フィールド配置(リトルエンディアン):
43/// - 0..4:   magic
44/// - 4..6:   version.major
45/// - 6..8:   version.minor
46/// - 8..10:  version.patch
47/// - 10:     checksum_algorithm
48/// - 11:     compression_algorithm
49/// - 12..16: flags
50/// - 16..24: created_at (µs)
51/// - 24..32: modified_at (µs)
52/// - 32..40: schema_version
53/// - 40..64: reserved (ゼロ埋め)
54#[repr(C)]
55#[derive(Debug, Clone, Copy, PartialEq, Eq)]
56pub struct FileHeader {
57    /// マジックナンバー。
58    pub magic: [u8; 4],
59    /// ファイルバージョン。
60    pub version: FileVersion,
61    /// チェックサムアルゴリズム。
62    pub checksum_algorithm: ChecksumAlgorithm,
63    /// 圧縮アルゴリズム(デフォルト)。
64    pub compression_algorithm: CompressionAlgorithm,
65    /// グローバルフラグ。
66    pub flags: FileFlags,
67    /// 作成タイムスタンプ(µs)。
68    pub created_at: u64,
69    /// 最終更新タイムスタンプ(µs)。
70    pub modified_at: u64,
71    /// スキーマバージョン(アプリケーション依存)。
72    pub schema_version: u64,
73    /// 将来拡張のための予約領域。
74    pub reserved: [u8; 24],
75}
76
77const _: () = assert!(mem::size_of::<FileHeader>() == HEADER_SIZE);
78
79impl FileHeader {
80    /// ヘッダーサイズ(バイト)。
81    pub const SIZE: usize = HEADER_SIZE;
82
83    /// 新規ヘッダーを生成する。
84    ///
85    /// タイムスタンプは現在時刻のマイクロ秒、圧縮アルゴリズムはSnappy、
86    /// チェックサムはCRC32をデフォルトとする。
87    pub fn new(version: FileVersion, flags: FileFlags) -> Self {
88        let now = now_micros();
89        Self {
90            magic: MAGIC,
91            version,
92            checksum_algorithm: ChecksumAlgorithm::Crc32,
93            compression_algorithm: CompressionAlgorithm::Snappy,
94            flags,
95            created_at: now,
96            modified_at: now,
97            schema_version: 0,
98            reserved: [0u8; 24],
99        }
100    }
101
102    /// バイト列からヘッダーを復元する。
103    pub fn from_bytes(bytes: &[u8; HEADER_SIZE]) -> Result<Self, FormatError> {
104        let mut magic = [0u8; 4];
105        magic.copy_from_slice(&bytes[0..4]);
106
107        let version = FileVersion {
108            major: u16::from_le_bytes([bytes[4], bytes[5]]),
109            minor: u16::from_le_bytes([bytes[6], bytes[7]]),
110            patch: u16::from_le_bytes([bytes[8], bytes[9]]),
111        };
112
113        let checksum_algorithm = match bytes[10] {
114            0 => ChecksumAlgorithm::Crc32,
115            1 => ChecksumAlgorithm::Xxh64,
116            other => return Err(FormatError::UnsupportedChecksum { algorithm: other }),
117        };
118
119        let compression_algorithm = match bytes[11] {
120            0 => CompressionAlgorithm::None,
121            1 => CompressionAlgorithm::Snappy,
122            2 => CompressionAlgorithm::Zstd,
123            3 => CompressionAlgorithm::Lz4,
124            other => return Err(FormatError::UnsupportedCompression { algorithm: other }),
125        };
126
127        let flags = FileFlags(u32::from_le_bytes(
128            bytes[12..16].try_into().expect("fixed slice length"),
129        ));
130
131        let created_at = u64::from_le_bytes(bytes[16..24].try_into().expect("fixed slice length"));
132        let modified_at = u64::from_le_bytes(bytes[24..32].try_into().expect("fixed slice length"));
133        let schema_version =
134            u64::from_le_bytes(bytes[32..40].try_into().expect("fixed slice length"));
135
136        let mut reserved = [0u8; 24];
137        reserved.copy_from_slice(&bytes[40..64]);
138
139        let header = Self {
140            magic,
141            version,
142            checksum_algorithm,
143            compression_algorithm,
144            flags,
145            created_at,
146            modified_at,
147            schema_version,
148            reserved,
149        };
150
151        header.validate_magic()?;
152        Ok(header)
153    }
154
155    /// ヘッダーをバイト列にシリアライズする。
156    pub fn to_bytes(&self) -> [u8; HEADER_SIZE] {
157        let mut bytes = [0u8; HEADER_SIZE];
158        bytes[0..4].copy_from_slice(&self.magic);
159        bytes[4..6].copy_from_slice(&self.version.major.to_le_bytes());
160        bytes[6..8].copy_from_slice(&self.version.minor.to_le_bytes());
161        bytes[8..10].copy_from_slice(&self.version.patch.to_le_bytes());
162        bytes[10] = self.checksum_algorithm as u8;
163        bytes[11] = self.compression_algorithm as u8;
164        bytes[12..16].copy_from_slice(&self.flags.bits().to_le_bytes());
165        bytes[16..24].copy_from_slice(&self.created_at.to_le_bytes());
166        bytes[24..32].copy_from_slice(&self.modified_at.to_le_bytes());
167        bytes[32..40].copy_from_slice(&self.schema_version.to_le_bytes());
168        bytes[40..64].copy_from_slice(&self.reserved);
169        bytes
170    }
171
172    /// マジックナンバーを検証する。
173    pub fn validate_magic(&self) -> Result<(), FormatError> {
174        if self.magic == MAGIC {
175            Ok(())
176        } else {
177            Err(FormatError::InvalidMagic { found: self.magic })
178        }
179    }
180
181    /// リーダーバージョンとの互換性を検証する。
182    ///
183    /// ファイルのバージョンがリーダーより新しい場合に [`FormatError::IncompatibleVersion`] を返す。
184    pub fn check_compatibility(&self, reader_version: &FileVersion) -> Result<(), FormatError> {
185        if self.version > *reader_version {
186            Err(FormatError::IncompatibleVersion {
187                file: self.version,
188                reader: *reader_version,
189            })
190        } else {
191            Ok(())
192        }
193    }
194}
195
196#[cfg(not(target_arch = "wasm32"))]
197fn now_micros() -> u64 {
198    SystemTime::now()
199        .duration_since(UNIX_EPOCH)
200        .unwrap_or_default()
201        .as_micros() as u64
202}
203
204#[cfg(target_arch = "wasm32")]
205fn now_micros() -> u64 {
206    #[cfg(feature = "wasm-indexeddb")]
207    {
208        (js_sys::Date::now() * 1000.0) as u64
209    }
210    #[cfg(not(feature = "wasm-indexeddb"))]
211    {
212        0
213    }
214}