Skip to main content

include_zstd/
lib.rs

1extern crate self as include_zstd;
2
3pub use include_zstd_derive::{bytes, file_bytes, file_str, include_zstd, r#str};
4use std::sync::OnceLock;
5use std::time::SystemTime;
6
7/// 文件元数据信息(从原始文件提取)
8pub struct ZstdMetadata {
9    pub len: u64,
10    pub modified: Option<SystemTime>,
11    pub accessed: Option<SystemTime>,
12    pub created: Option<SystemTime>,
13    pub is_file: bool,
14    pub is_dir: bool,
15}
16
17/// 编译期压缩的 zstd 资源
18pub struct ZstdAsset {
19    metadata: ZstdMetadata,
20    compressed: &'static [u8],
21    cache: OnceLock<Box<[u8]>>,
22}
23
24impl ZstdAsset {
25    /// 返回原始文件的元数据信息
26    pub fn metadata(&self) -> &ZstdMetadata {
27        &self.metadata
28    }
29
30    /// 返回 zstd 解压后的文件内容(惰性解压,首次调用时解压并缓存)
31    pub fn bytes(&self) -> &[u8] {
32        self.cache
33            .get_or_init(|| __private::decompress_bytes(self.compressed))
34            .as_ref()
35    }
36}
37
38#[doc(hidden)]
39pub mod __private {
40    pub fn decode_utf8(bytes: &'static [u8]) -> &'static str {
41        std::str::from_utf8(bytes).unwrap_or_else(|err| {
42            panic!("include_zstd::str!/file_str! decoded data is not UTF-8: {err}")
43        })
44    }
45
46    pub fn decompress_bytes(compressed: &[u8]) -> Box<[u8]> {
47        zstd::stream::decode_all(compressed)
48            .unwrap_or_else(|err| panic!("include_zstd decode failed: {err}"))
49            .into_boxed_slice()
50    }
51
52    pub fn create_zstd_asset(
53        metadata: crate::ZstdMetadata,
54        compressed: &'static [u8],
55    ) -> crate::ZstdAsset {
56        crate::ZstdAsset {
57            metadata,
58            compressed,
59            cache: std::sync::OnceLock::new(),
60        }
61    }
62}
63
64#[cfg(test)]
65mod tests {
66    use crate::ZstdAsset;
67
68    fn include_str_fixture() -> &'static str {
69        crate::str!("hello include-zstd")
70    }
71
72    fn include_bytes_fixture() -> &'static [u8] {
73        crate::bytes!(b"\x00\x01\x02\x03")
74    }
75
76    fn include_file_str_fixture() -> &'static str {
77        crate::file_str!("../Cargo.toml")
78    }
79
80    fn include_file_fixture() -> &'static [u8] {
81        crate::file_bytes!("../Cargo.toml")
82    }
83
84    fn include_zstd_fixture() -> ZstdAsset {
85        // 使用相对于 src/lib.rs 的路径,与宏的解析逻辑一致
86        crate::include_zstd!("../Cargo.toml")
87    }
88
89    #[test]
90    fn str_matches_original_text() {
91        let expected: &'static str = "hello include-zstd";
92        let actual: &'static str = include_str_fixture();
93
94        assert_eq!(actual, expected);
95    }
96
97    #[test]
98    fn binary_matches_original_bytes() {
99        let expected: &'static [u8] = b"\x00\x01\x02\x03";
100        let actual: &'static [u8] = include_bytes_fixture();
101
102        assert_eq!(actual, expected);
103    }
104
105    #[test]
106    fn file_str_matches_include_str() {
107        let expected: &'static str = include_str!("../Cargo.toml");
108        let actual: &'static str = include_file_str_fixture();
109
110        assert_eq!(actual, expected);
111    }
112
113    #[test]
114    fn file_matches_include_bytes() {
115        let expected: &'static [u8] = include_bytes!("../Cargo.toml");
116        let actual: &'static [u8] = include_file_fixture();
117
118        assert_eq!(actual, expected);
119    }
120
121    #[test]
122    fn macros_use_once_lock_for_each_callsite() {
123        let first = include_str_fixture().as_ptr();
124        let second = include_str_fixture().as_ptr();
125        assert_eq!(first, second);
126
127        let first = include_bytes_fixture().as_ptr();
128        let second = include_bytes_fixture().as_ptr();
129        assert_eq!(first, second);
130
131        let first = include_file_str_fixture().as_ptr();
132        let second = include_file_str_fixture().as_ptr();
133        assert_eq!(first, second);
134
135        let first = include_file_fixture().as_ptr();
136        let second = include_file_fixture().as_ptr();
137        assert_eq!(first, second);
138    }
139
140    #[test]
141    fn zstd_asset_bytes_matches_original() {
142        let asset = include_zstd_fixture();
143        let expected = include_bytes!("../Cargo.toml");
144        assert_eq!(asset.bytes(), expected);
145    }
146
147    #[test]
148    fn zstd_asset_metadata_len_matches() {
149        let asset = include_zstd_fixture();
150        // 宏在编译期相对于 src/lib.rs 解析路径,所以 ../Cargo.toml 指向 include-zstd/Cargo.toml
151        let expected_len =
152            std::fs::metadata(std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("Cargo.toml"))
153                .unwrap()
154                .len();
155        assert_eq!(asset.metadata().len, expected_len);
156    }
157
158    #[test]
159    fn zstd_asset_bytes_cached() {
160        let asset = include_zstd_fixture();
161        let first = asset.bytes().as_ptr();
162        let second = asset.bytes().as_ptr();
163        assert_eq!(first, second);
164    }
165
166    #[test]
167    fn zstd_asset_metadata_is_file() {
168        let asset = include_zstd_fixture();
169        assert!(asset.metadata().is_file);
170        assert!(!asset.metadata().is_dir);
171    }
172}