zstd_complete/encoding/
file_profile.rs1#[cfg(feature = "std")]
4use crate::io::Read;
5#[cfg(feature = "std")]
6use alloc::vec::Vec;
7#[cfg(feature = "std")]
8use std::path::Path;
9
10#[cfg(feature = "std")]
11use super::util::{likely_composer_lockfile_text, likely_incompressible, likely_lockfile_text};
12
13#[cfg(feature = "std")]
14const FILE_TYPE_SAMPLE_BYTES: usize = 32 * 1024;
15
16#[derive(Copy, Clone, Debug, Eq, PartialEq)]
17pub(crate) enum CompressionFileProfile {
18 None,
19 CargoLock,
20 ComposerLock,
21 SmallTextLockfile,
22 DependencyJsonLockfile,
23}
24
25impl CompressionFileProfile {
26 pub(crate) const fn internal_hint_code(self) -> u8 {
27 match self {
28 Self::None => 0,
29 Self::CargoLock => 1,
30 Self::ComposerLock => 2,
31 Self::SmallTextLockfile => 3,
32 Self::DependencyJsonLockfile => 4,
33 }
34 }
35
36 pub(crate) const fn from_internal_hint_code(code: u8) -> Self {
37 match code {
38 1 => Self::CargoLock,
39 2 => Self::ComposerLock,
40 3 => Self::SmallTextLockfile,
41 4 => Self::DependencyJsonLockfile,
42 _ => Self::None,
43 }
44 }
45}
46
47#[cfg(feature = "std")]
48mod known;
49
50#[cfg(feature = "std")]
51use known::{classify_file_name, file_name_matches_exact_or_prefixed};
52
53#[cfg(feature = "std")]
54fn likely_json_text_sample(data: &[u8]) -> bool {
55 if !sample_looks_text(data) {
56 return false;
57 }
58
59 let trimmed = data
60 .iter()
61 .skip_while(|&&byte| matches!(byte, b' ' | b'\t' | b'\n' | b'\r'))
62 .copied()
63 .collect::<Vec<u8>>();
64 let trimmed = trimmed.as_slice();
65 if !matches!(trimmed.first(), Some(b'{') | Some(b'[')) {
66 return false;
67 }
68
69 let window = &trimmed[..trimmed.len().min(2048)];
70 window.contains(&b':') && window.iter().filter(|&&byte| byte == b'"').count() >= 4
71}
72
73#[cfg(feature = "std")]
74fn sample_looks_text(data: &[u8]) -> bool {
75 if data.len() < 16 {
76 return false;
77 }
78
79 let step = (data.len() / 128).max(1);
80 let mut printable = 0usize;
81 let mut total = 0usize;
82 for idx in (0..data.len()).step_by(step).take(128) {
83 total += 1;
84 let byte = data[idx];
85 if byte == b'\n'
86 || byte == b'\r'
87 || byte == b'\t'
88 || byte == b' '
89 || byte.is_ascii_graphic()
90 {
91 printable += 1;
92 }
93 }
94
95 printable * 100 >= total * 85
96}
97
98#[cfg(feature = "std")]
99fn likely_dictionary_text_sample(data: &[u8]) -> bool {
100 let mut package_markers = 0usize;
101 let mut name_markers = 0usize;
102 let mut version_markers = 0usize;
103 let mut checksum_markers = 0usize;
104 let mut composer_packages = false;
105 let mut composer_require = 0usize;
106 let mut composer_reference = 0usize;
107
108 for line in data.split(|&byte| byte == b'\n').take(512) {
109 let trimmed = line
110 .iter()
111 .skip_while(|&&byte| matches!(byte, b' ' | b'\t'))
112 .copied()
113 .collect::<Vec<u8>>();
114 let trimmed = trimmed.as_slice();
115
116 if trimmed.starts_with(b"[[package]]") {
117 package_markers += 1;
118 } else if trimmed.starts_with(b"name = \"") || trimmed.starts_with(br#""name": ""#) {
119 name_markers += 1;
120 } else if trimmed.starts_with(b"version = \"") || trimmed.starts_with(br#""version": ""#) {
121 version_markers += 1;
122 } else if trimmed.starts_with(b"checksum = \"") {
123 checksum_markers += 1;
124 } else if trimmed.starts_with(br#""packages": ["#) {
125 composer_packages = true;
126 } else if trimmed.starts_with(br#""require": {"#) {
127 composer_require += 1;
128 } else if trimmed.starts_with(br#""reference": ""#) {
129 composer_reference += 1;
130 }
131
132 if package_markers >= 2
133 && name_markers >= 2
134 && version_markers >= 2
135 && checksum_markers >= 2
136 {
137 return true;
138 }
139 if composer_packages
140 && name_markers >= 2
141 && version_markers >= 2
142 && composer_require >= 2
143 && composer_reference >= 2
144 {
145 return true;
146 }
147 }
148
149 false
150}
151
152#[cfg(feature = "std")]
153fn likely_config_text_sample(data: &[u8]) -> bool {
154 if !sample_looks_text(data) {
155 return false;
156 }
157
158 let mut score = 0usize;
159 for line in data.split(|&byte| byte == b'\n').take(256) {
160 let line = line
161 .iter()
162 .skip_while(|&&byte| matches!(byte, b' ' | b'\t'))
163 .copied()
164 .collect::<Vec<u8>>();
165 let line = line.as_slice();
166 if line.is_empty() {
167 continue;
168 }
169
170 if (line.starts_with(b"[") && line.ends_with(b"]"))
171 || (line.starts_with(b"<") && line.contains(&b'>'))
172 {
173 score += 2;
174 } else if line.starts_with(b"- ") || line.starts_with(b"* ") {
175 score += 1;
176 } else if line.starts_with(b"FROM ")
177 || line.starts_with(b"RUN ")
178 || line.starts_with(b"ENV ")
179 || line.starts_with(b"COPY ")
180 || line.starts_with(b"WORKDIR ")
181 || line.starts_with(b"ENTRYPOINT ")
182 || line.starts_with(b"CMD ")
183 {
184 score += 2;
185 } else if (line.windows(2).any(|pair| pair == b": ")
186 && !line.windows(3).any(|pair| pair == b"://"))
187 || line.contains(&b'=')
188 {
189 score += 1;
190 }
191
192 if score >= 4 {
193 return true;
194 }
195 }
196
197 false
198}
199
200#[cfg(feature = "std")]
201fn likely_code_text_sample(data: &[u8]) -> bool {
202 if !sample_looks_text(data) {
203 return false;
204 }
205
206 let mut score = 0usize;
207 for line in data.split(|&byte| byte == b'\n').take(256) {
208 let line = line
209 .iter()
210 .skip_while(|&&byte| matches!(byte, b' ' | b'\t'))
211 .copied()
212 .collect::<Vec<u8>>();
213 let line = line.as_slice();
214 if line.is_empty() {
215 continue;
216 }
217
218 if line.ends_with(b";") || line.ends_with(b"{") || line.ends_with(b"}") {
219 score += 1;
220 }
221 if line
222 .windows(2)
223 .any(|pair| matches!(pair, b"::" | b"->" | b"=>" | b"//"))
224 {
225 score += 1;
226 }
227 if line.starts_with(b"fn ")
228 || line.starts_with(b"pub ")
229 || line.starts_with(b"let ")
230 || line.starts_with(b"const ")
231 || line.starts_with(b"var ")
232 || line.starts_with(b"def ")
233 || line.starts_with(b"class ")
234 || line.starts_with(b"import ")
235 || line.starts_with(b"package ")
236 || line.starts_with(b"use ")
237 || line.starts_with(b"#include")
238 || line.starts_with(b"<?php")
239 {
240 score += 2;
241 }
242
243 if score >= 4 {
244 return true;
245 }
246 }
247
248 false
249}
250
251#[cfg(feature = "std")]
252fn has_archive_signature(data: &[u8]) -> bool {
253 data.starts_with(b"PK\x03\x04")
254 || data.starts_with(b"PK\x05\x06")
255 || data.starts_with(b"PK\x07\x08")
256 || data.starts_with(&[0x1F, 0x8B])
257 || data.starts_with(b"BZh")
258 || data.starts_with(&[0xFD, b'7', b'z', b'X', b'Z', 0x00])
259 || data.starts_with(&[0x28, 0xB5, 0x2F, 0xFD])
260 || data.starts_with(&[b'7', b'z', 0xBC, 0xAF, 0x27, 0x1C])
261 || data.starts_with(b"Rar!\x1A\x07\x00")
262 || data.starts_with(b"Rar!\x1A\x07\x01\x00")
263 || (data.len() > 262 && &data[257..262] == b"ustar")
264}
265
266#[cfg(feature = "std")]
267fn has_binary_signature(data: &[u8]) -> bool {
268 data.starts_with(b"\x7FELF")
269 || data.starts_with(b"MZ")
270 || data.starts_with(b"\0asm")
271 || data.starts_with(&[0xCA, 0xFE, 0xBA, 0xBE])
272 || data.starts_with(&[0xFE, 0xED, 0xFA, 0xCE])
273 || data.starts_with(&[0xFE, 0xED, 0xFA, 0xCF])
274 || data.starts_with(&[0xCE, 0xFA, 0xED, 0xFE])
275 || data.starts_with(&[0xCF, 0xFA, 0xED, 0xFE])
276 || data.starts_with(b"SQLite format 3\0")
277 || data.starts_with(b"%PDF-")
278}
279
280#[cfg(feature = "std")]
281fn compression_file_type_for_sample(sample: &[u8]) -> CompressionFileType {
282 if sample.is_empty() {
283 return CompressionFileType::Unknown;
284 }
285 if has_archive_signature(sample) {
286 return CompressionFileType::ArchiveLike;
287 }
288 if has_binary_signature(sample) {
289 return CompressionFileType::BinaryLike;
290 }
291 if likely_lockfile_text(sample)
292 || likely_composer_lockfile_text(sample)
293 || likely_dictionary_text_sample(sample)
294 {
295 return CompressionFileType::DictionaryText;
296 }
297 if likely_json_text_sample(sample) {
298 return CompressionFileType::JsonText;
299 }
300 if likely_config_text_sample(sample) {
301 return CompressionFileType::ConfigText;
302 }
303 if likely_code_text_sample(sample) {
304 return CompressionFileType::CodeText;
305 }
306 if likely_incompressible(sample) {
307 return CompressionFileType::BinaryLike;
308 }
309 CompressionFileType::Unknown
310}
311
312#[cfg(feature = "std")]
313pub(crate) fn read_file_type_sample<R: Read>(source: &mut R) -> Vec<u8> {
314 let mut sample = Vec::with_capacity(FILE_TYPE_SAMPLE_BYTES);
315 let mut buffer = [0u8; 4096];
316 while sample.len() < FILE_TYPE_SAMPLE_BYTES {
317 let wanted = (FILE_TYPE_SAMPLE_BYTES - sample.len()).min(buffer.len());
318 let read = source.read(&mut buffer[..wanted]).unwrap();
319 if read == 0 {
320 break;
321 }
322 sample.extend_from_slice(&buffer[..read]);
323 }
324 sample
325}
326
327#[derive(Copy, Clone, Debug, Eq, PartialEq)]
331pub enum CompressionFileType {
332 Unknown,
333 ArchiveLike,
334 BinaryLike,
335 CodeText,
336 ConfigText,
337 JsonText,
338 DictionaryText,
339}
340
341#[cfg(feature = "std")]
342pub fn compression_file_type_for_path(path: &Path) -> CompressionFileType {
343 let file_name = path
344 .file_name()
345 .and_then(|name| name.to_str())
346 .unwrap_or_default();
347 classify_file_name(&file_name.to_ascii_lowercase())
348}
349
350#[cfg(feature = "std")]
352pub fn compression_file_type_for_path_and_data(path: &Path, sample: &[u8]) -> CompressionFileType {
353 let by_path = compression_file_type_for_path(path);
354 if by_path != CompressionFileType::Unknown {
355 return by_path;
356 }
357 compression_file_type_for_sample(sample)
358}
359
360#[cfg(feature = "std")]
361pub(crate) fn compression_file_profile_for_path_and_data(
362 path: &Path,
363 sample: &[u8],
364) -> CompressionFileProfile {
365 let file_name = path
366 .file_name()
367 .and_then(|name| name.to_str())
368 .unwrap_or_default()
369 .to_ascii_lowercase();
370 if file_name_matches_exact_or_prefixed(&file_name, "cargo.lock") {
371 CompressionFileProfile::CargoLock
372 } else if file_name_matches_exact_or_prefixed(&file_name, "composer.lock") {
373 CompressionFileProfile::ComposerLock
374 } else if file_name_matches_exact_or_prefixed(&file_name, "poetry.lock")
375 || file_name_matches_exact_or_prefixed(&file_name, "pubspec.lock")
376 {
377 CompressionFileProfile::SmallTextLockfile
378 } else if file_name_matches_exact_or_prefixed(&file_name, "package-lock.json")
379 || file_name_matches_exact_or_prefixed(&file_name, "pipfile.lock")
380 || super::util::likely_dependency_json_lockfile_text(sample)
381 {
382 CompressionFileProfile::DependencyJsonLockfile
383 } else {
384 CompressionFileProfile::None
385 }
386}
387
388#[cfg(all(test, feature = "std"))]
389mod tests;