Skip to main content

tibba_util/
lib.rs

1// Copyright 2026 Tree xie.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use snafu::Snafu;
16use std::env;
17use std::sync::LazyLock;
18use tibba_error::Error as BaseError;
19
20// Error 变体按 feature 门控:Zstd / Lz4Decompress 只被 compression.rs 构造,
21// InvalidHeaderName / InvalidHeaderValue 只被 http.rs 构造,
22// 对应 feature 关闭时变体一并消失,避免 enum 携带无法到达的分支。
23#[derive(Snafu, Debug)]
24pub enum Error {
25    #[cfg(feature = "compression")]
26    #[snafu(display("{source}"))]
27    Zstd { source: std::io::Error },
28    #[cfg(feature = "compression")]
29    #[snafu(display("{source}"))]
30    Lz4Decompress {
31        source: lz4_flex::block::DecompressError,
32    },
33    /// 解压输出超过允许上限,见 `decompress_with_limit`。
34    #[cfg(feature = "compression")]
35    #[snafu(display("decompressed size {size} exceeds limit {limit}"))]
36    DecompressTooLarge { size: usize, limit: usize },
37    #[cfg(feature = "http")]
38    #[snafu(display("{source}"))]
39    InvalidHeaderName {
40        source: axum::http::header::InvalidHeaderName,
41    },
42    #[cfg(feature = "http")]
43    #[snafu(display("{source}"))]
44    InvalidHeaderValue {
45        source: axum::http::header::InvalidHeaderValue,
46    },
47    #[snafu(display("{message}"))]
48    Invalid { message: String },
49    #[snafu(display("{source}"))]
50    Deserialize { source: serde_urlencoded::de::Error },
51}
52
53impl From<Error> for BaseError {
54    fn from(val: Error) -> Self {
55        // 单次 match:从 source 构造 BaseError 并打 sub_category,
56        // 与项目其他 snafu 模块(tibba-config / tibba-sql 等)保持一致
57        let err = match val {
58            #[cfg(feature = "compression")]
59            Error::Zstd { source } => BaseError::new(source).with_sub_category("zstd"),
60            #[cfg(feature = "compression")]
61            Error::Lz4Decompress { source } => {
62                BaseError::new(source).with_sub_category("lz4_decompress")
63            }
64            // 超限多半意味着数据损坏或被篡改,值得告警
65            #[cfg(feature = "compression")]
66            Error::DecompressTooLarge { size, limit } => {
67                BaseError::new(format!("decompressed size {size} exceeds limit {limit}"))
68                    .with_sub_category("decompress_too_large")
69                    .with_exception(true)
70            }
71            #[cfg(feature = "http")]
72            Error::InvalidHeaderName { source } => {
73                BaseError::new(source).with_sub_category("invalid_header_name")
74            }
75            #[cfg(feature = "http")]
76            Error::InvalidHeaderValue { source } => {
77                BaseError::new(source).with_sub_category("invalid_header_value")
78            }
79            Error::Invalid { message } => BaseError::new(message).with_sub_category("invalid"),
80            Error::Deserialize { source } => {
81                BaseError::new(source).with_sub_category("deserialize")
82            }
83        };
84        err.with_category("util")
85    }
86}
87
88/// 未设置 `RUST_ENV` 时采用的运行环境。
89///
90/// # 必须是 production(fail closed)
91/// 此前默认是 `dev`,而 Dockerfile / entrypoint / 部署脚本都没有设置 `RUST_ENV`——
92/// 也就是说**默认构建出来的镜像以开发模式运行**,一口气打开了下面这些口子:
93///
94/// - 验证码可以用固定的 `1234` 绕过(`magic_code`)
95/// - 允许沿用脚手架自带的占位 `basic.secret`,session 签名密钥等于公开
96/// - 登录防重放可以用 `ts=0` 整个跳过
97/// - session / CSRF / 设备 cookie 不带 `Secure`
98/// - Swagger UI 对外暴露,CORS 的生产安全校验被跳过
99///
100/// 这些放宽都是为本地开发准备的,默认值理应站在最安全的一侧:忘了设环境变量
101/// 的后果应该是「本地开发多敲一行 `RUST_ENV=dev`」,而不是「线上门户大开」。
102pub const DEFAULT_RUST_ENV: &str = "production";
103
104static RUST_ENV: LazyLock<String> =
105    LazyLock::new(|| env::var("RUST_ENV").unwrap_or_else(|_| DEFAULT_RUST_ENV.to_string()));
106
107/// 原始的 `RUST_ENV` 值(未设置时为 [`DEFAULT_RUST_ENV`])。
108///
109/// 用于按环境选择配置文件(`<env>.toml`)等需要原值的场合;**判断安全相关
110/// 行为请用** [`is_development`] / [`is_test`] / [`is_production`]。
111pub fn get_env() -> &'static str {
112    &RUST_ENV
113}
114
115/// 是否为本地开发环境(`RUST_ENV=dev`,必须精确匹配)。
116pub fn is_development() -> bool {
117    get_env() == "dev"
118}
119
120/// 是否为测试环境(`RUST_ENV=test`,必须精确匹配)。
121pub fn is_test() -> bool {
122    get_env() == "test"
123}
124
125/// 是否按生产环境的安全要求运行:**既不是 dev 也不是 test 就是 production**。
126///
127/// 此前要求精确等于 `"production"`,于是 `staging`、`prod`、`Production` 这类
128/// 值三个判断全为 false——既拿不到开发便利,也躲过了所有 `if is_production()`
129/// 的生产校验(例如 CORS 安全断言),落在一个谁都没设计过的中间态里。
130/// 现在只有显式声明的 dev / test 才放宽,其余一律按生产对待。
131pub fn is_production() -> bool {
132    !is_development() && !is_test()
133}
134
135#[cfg(feature = "compression")]
136mod compression;
137mod datetime;
138#[cfg(feature = "http")]
139mod http;
140mod request;
141mod response;
142mod string;
143mod uri;
144mod validate;
145mod value;
146
147#[cfg(feature = "compression")]
148pub use compression::*;
149pub use datetime::*;
150#[cfg(feature = "http")]
151pub use http::*;
152pub use request::*;
153pub use response::*;
154pub use string::*;
155pub use uri::*;
156pub use validate::*;
157pub use value::*;