Skip to main content

omy_core/
error.rs

1//! 错误类型。
2//!
3//! 设计要求(见 `docs/research/08-ui-ux-design.md` §7.2):错误必须是**结构化的错误码
4//! 加参数**,而不是拼装好的自然语言字符串。这样 GUI 层才能按用户语言渲染,CLI 层才能
5//! 映射到稳定的退出码。
6//!
7//! 因此本模块的 `Display` 实现只用于开发者调试与日志,**不应**直接展示给终端用户。
8
9use core::fmt;
10
11/// CLI 退出码。数值定义见 `docs/research/09-cli-design.md` §3.1,
12/// 脚本依赖这些数值区分「密码错」与「文件坏」,**不得随意改动**。
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14#[repr(u8)]
15pub enum ExitCode {
16    /// 成功。
17    Success = 0,
18    /// 一般错误。
19    General = 1,
20    /// 参数错误。
21    Usage = 2,
22    /// 密码错误或无匹配 slot。
23    WrongPassword = 3,
24    /// 文件损坏或被篡改(MAC / AEAD 校验失败)。
25    Corrupted = 4,
26    /// 格式版本不支持。
27    UnsupportedVersion = 5,
28    /// 缺少分片。
29    MissingShard = 6,
30    /// 权限不足。
31    Permission = 7,
32    /// 用户取消。
33    Cancelled = 8,
34}
35
36/// omy 核心库的错误类型。
37///
38/// 每个变体都携带定位问题所需的结构化字段,供上层渲染本地化消息。
39#[derive(Debug, thiserror::Error)]
40#[non_exhaustive]
41pub enum Error {
42    /// 文件头 magic 不匹配,不是 omy 文件。
43    #[error("not an omy file: magic mismatch (got {got:02x?})")]
44    BadMagic {
45        /// 实际读到的前 8 字节。
46        got: [u8; 8],
47    },
48
49    /// 主版本号高于本实现支持的上限,必须拒绝打开(规范 §9 步骤 2)。
50    #[error("unsupported format version {found_major}.{found_minor}, this build supports up to {supported_major}.x")]
51    UnsupportedVersion {
52        /// 文件声明的主版本。
53        found_major: u16,
54        /// 文件声明的次版本。
55        found_minor: u16,
56        /// 本实现支持的最高主版本。
57        supported_major: u16,
58    },
59
60    /// 输入数据长度不足,无法解析出完整结构。
61    #[error("truncated input while reading {context}: need {need} bytes, got {got}")]
62    Truncated {
63        /// 正在解析的结构名,便于定位。
64        context: &'static str,
65        /// 需要的字节数。
66        need: usize,
67        /// 实际可用的字节数。
68        got: usize,
69    },
70
71    /// 头部字段取值非法或自相矛盾。
72    #[error("malformed header: {reason}")]
73    MalformedHeader {
74        /// 具体原因。
75        reason: &'static str,
76    },
77
78    /// 没有任何候选密钥能解开任何一个 key slot。
79    ///
80    /// 注意:这**不代表**文件损坏,也可能只是该文件不属于当前解锁的密码。
81    /// 面向用户的文案必须同时表达这两种可能。
82    #[error("no key slot could be unwrapped with the provided credentials")]
83    NoMatchingSlot,
84
85    /// 头部 HMAC 校验失败:文件被篡改,或 FEK 不正确。
86    ///
87    /// 规范 §7 要求此时**必须终止**,不得继续读取载荷。
88    #[error("header MAC verification failed: file tampered or truncated")]
89    HeaderMacMismatch,
90
91    /// 载荷块的 AEAD 认证失败。
92    #[error("chunk {index} failed authentication: data corrupted or tampered")]
93    ChunkAuthFailed {
94        /// 出错的块序号。
95        index: u64,
96    },
97
98    /// 遇到未知且标记为 CRITICAL 的 TLV,按规范必须拒绝打开(§4.2)。
99    #[error("unknown critical TLV type 0x{tlv_type:04x}: cannot safely open this file")]
100    UnknownCriticalTlv {
101        /// 未知的 TLV 类型号。
102        tlv_type: u16,
103    },
104
105    /// 必需的 TLV 缺失。
106    #[error("required TLV type 0x{tlv_type:04x} is missing")]
107    MissingTlv {
108        /// 缺失的 TLV 类型号。
109        tlv_type: u16,
110    },
111
112    /// TLV 内容解析失败。
113    #[error("malformed TLV type 0x{tlv_type:04x}: {reason}")]
114    MalformedTlv {
115        /// 出错的 TLV 类型号。
116        tlv_type: u16,
117        /// 具体原因。
118        reason: &'static str,
119    },
120
121    /// 请求的块序号超出文件实际块数。
122    #[error("chunk index {index} out of range (file has {total} chunks)")]
123    ChunkOutOfRange {
124        /// 请求的块序号。
125        index: u64,
126        /// 实际总块数。
127        total: u64,
128    },
129
130    /// key slot 数量超过格式上限。
131    #[error("too many key slots: {got} exceeds maximum {max}")]
132    TooManySlots {
133        /// 请求的数量。
134        got: usize,
135        /// 格式允许的上限。
136        max: usize,
137    },
138
139    /// 分块大小超出允许范围。
140    #[error("chunk size {got} out of allowed range [{min}, {max}]")]
141    InvalidChunkSize {
142        /// 请求的分块大小。
143        got: u32,
144        /// 下限。
145        min: u32,
146        /// 上限。
147        max: u32,
148    },
149
150    /// 分片的 CRC-32 校验失败。
151    #[error("shard {index} CRC mismatch: expected {expected:08x}, computed {computed:08x}")]
152    ShardCrcMismatch {
153        /// 分片序号。
154        index: u32,
155        /// 头部记录的 CRC。
156        expected: u32,
157        /// 实际算出的 CRC。
158        computed: u32,
159    },
160
161    /// 分片集合中缺少某些序号。
162    #[error("missing shards: {missing:?} (expected {total} total)")]
163    MissingShards {
164        /// 缺失的分片序号列表。
165        missing: Vec<u32>,
166        /// 应有的总片数。
167        total: u32,
168    },
169
170    /// 分片的 `file_uuid` 与其它片不一致,属于不同文件。
171    #[error("shard {index} belongs to a different file")]
172    ShardUuidMismatch {
173        /// 不一致的分片序号。
174        index: u32,
175    },
176
177    /// 解密后内容哈希与 `TLV_CONTENT_HASH` 记录不符。
178    #[error("content hash mismatch after decryption")]
179    ContentHashMismatch,
180
181    /// 压缩或解压失败。
182    #[error("compression error: {reason}")]
183    Compression {
184        /// 具体原因。
185        reason: String,
186    },
187
188    /// 密钥派生失败。
189    #[error("key derivation failed: {reason}")]
190    KeyDerivation {
191        /// 具体原因。
192        reason: &'static str,
193    },
194
195    /// 底层 IO 错误。
196    #[error("io error: {0}")]
197    Io(#[from] std::io::Error),
198}
199
200impl Error {
201    /// 映射到 CLI 退出码。
202    ///
203    /// 见 `docs/research/09-cli-design.md` §3.1。
204    #[must_use]
205    pub const fn exit_code(&self) -> ExitCode {
206        match self {
207            Self::NoMatchingSlot => ExitCode::WrongPassword,
208            Self::BadMagic { .. }
209            | Self::HeaderMacMismatch
210            | Self::ChunkAuthFailed { .. }
211            | Self::ShardCrcMismatch { .. }
212            | Self::ContentHashMismatch
213            | Self::Truncated { .. }
214            | Self::MalformedHeader { .. }
215            | Self::MalformedTlv { .. } => ExitCode::Corrupted,
216            Self::UnsupportedVersion { .. } | Self::UnknownCriticalTlv { .. } => {
217                ExitCode::UnsupportedVersion
218            }
219            Self::MissingShards { .. } | Self::ShardUuidMismatch { .. } => ExitCode::MissingShard,
220            Self::InvalidChunkSize { .. } | Self::TooManySlots { .. } => ExitCode::Usage,
221            Self::Io(_) => ExitCode::Permission,
222            _ => ExitCode::General,
223        }
224    }
225
226    /// 稳定的机器可读错误码,供 `--json` 输出与前端 i18n 查表使用。
227    ///
228    /// 这些字符串是**接口契约**,脚本会匹配它们,因此不得随意更名。
229    #[must_use]
230    pub const fn code(&self) -> &'static str {
231        match self {
232            Self::BadMagic { .. } => "BAD_MAGIC",
233            Self::UnsupportedVersion { .. } => "UNSUPPORTED_VERSION",
234            Self::Truncated { .. } => "TRUNCATED",
235            Self::MalformedHeader { .. } => "MALFORMED_HEADER",
236            Self::NoMatchingSlot => "WRONG_PASSWORD",
237            Self::HeaderMacMismatch => "HEADER_MAC_MISMATCH",
238            Self::ChunkAuthFailed { .. } => "CHUNK_AUTH_FAILED",
239            Self::UnknownCriticalTlv { .. } => "UNKNOWN_CRITICAL_TLV",
240            Self::MissingTlv { .. } => "MISSING_TLV",
241            Self::MalformedTlv { .. } => "MALFORMED_TLV",
242            Self::ChunkOutOfRange { .. } => "CHUNK_OUT_OF_RANGE",
243            Self::TooManySlots { .. } => "TOO_MANY_SLOTS",
244            Self::InvalidChunkSize { .. } => "INVALID_CHUNK_SIZE",
245            Self::ShardCrcMismatch { .. } => "SHARD_CRC_MISMATCH",
246            Self::MissingShards { .. } => "MISSING_SHARDS",
247            Self::ShardUuidMismatch { .. } => "SHARD_UUID_MISMATCH",
248            Self::ContentHashMismatch => "CONTENT_HASH_MISMATCH",
249            Self::Compression { .. } => "COMPRESSION_ERROR",
250            Self::KeyDerivation { .. } => "KEY_DERIVATION_FAILED",
251            Self::Io(_) => "IO_ERROR",
252        }
253    }
254}
255
256impl fmt::Display for ExitCode {
257    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
258        write!(f, "{}", *self as u8)
259    }
260}
261
262/// 本库统一的 `Result` 别名。
263pub type Result<T> = core::result::Result<T, Error>;