• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

royaltm / rust-delharc / 29785215315

20 Jul 2026 10:49PM UTC coverage: 93.666% (-0.6%) from 94.242%
29785215315

push

github

royaltm
fix header: check for known dir separators instead of system dependent ones

5 of 6 new or added lines in 1 file covered. (83.33%)

193 existing lines in 13 files now uncovered.

3978 of 4247 relevant lines covered (93.67%)

43937550.69 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

96.67
/src/header/parser.rs
1
#[cfg(not(feature = "std"))]
2
use alloc::vec::Vec;
3
use core::{fmt::Write, num::Wrapping, slice};
4
use bytemuck::{NoUninit, AnyBitPattern, bytes_of_mut};
5
use crate::{
6
    error::{LhaError, LhaResult, LhaHeaderError},
7
    stub_io::Read,
8
    crc::Crc16,
9
};
10
use super::*;
11

12
/// Raw identifiers of extra headers.
13
pub mod ext {
14
    /// The "Common" header's CRC-16 field will always be reset to 0 in the parsed header data.
15
    /// This is the necessary condition to verify header's checksum.
16
    pub const EXT_HEADER_COMMON:       u8 = 0x00;
17
    /// The "File name" header may contain the entry's file name.
18
    pub const EXT_HEADER_FILENAME:     u8 = 0x01;
19
    /// The "Directory name" header may contain the directory of the entry.
20
    pub const EXT_HEADER_PATH:         u8 = 0x02;
21
    /// The "Multi-disc" header
22
    pub const EXT_HEADER_MULTI_DISC:   u8 = 0x39;
23
    /// The "Comment" header
24
    pub const EXT_HEADER_COMMENT:      u8 = 0x3F;
25
    /// The MS-DOS ["Attributes"](super::MsDosAttrs) header
26
    pub const EXT_HEADER_MSDOS_ATTRS:  u8 = 0x40;
27
    /// The "Windows time stamp" header
28
    pub const EXT_HEADER_WINDOWS_TIME: u8 = 0x41;
29
    /// An alias of [`EXT_HEADER_WINDOWS_TIME`]
30
    #[deprecated(note="please use `EXT_HEADER_WINDOWS_TIME` instead")]
31
    pub const EXT_HEADER_MSDOS_TIME:   u8 = EXT_HEADER_WINDOWS_TIME;
32
    /// The "File size" header with 64-bit file size information
33
    pub const EXT_HEADER_FILE_SIZES:   u8 = 0x42;
34
    /// An alias of [`EXT_HEADER_FILE_SIZES`]
35
    #[deprecated(note="please use `EXT_HEADER_FILE_SIZES` instead")]
36
    pub const EXT_HEADER_MSDOS_SIZE:   u8 = EXT_HEADER_FILE_SIZES;
37
    /// The UNIX ["Permission"](super::Permissions) header
38
    pub const EXT_HEADER_UNIX_PERM:    u8 = 0x50;
39
    /// The UNIX "GID UID" header
40
    pub const EXT_HEADER_UNIX_UIDGID:  u8 = 0x51;
41
    /// The UNIX "Group name" header
42
    pub const EXT_HEADER_UNIX_GROUP:   u8 = 0x52;
43
    /// The UNIX "User name" header
44
    pub const EXT_HEADER_UNIX_OWNER:   u8 = 0x53;
45
    /// The UNIX "Time stamp" header
46
    pub const EXT_HEADER_UNIX_TIME:    u8 = 0x54;
47
    /// The Mac "Capsule" header
48
    pub const EXT_HEADER_MAC_CAPSULE:  u8 = 0x7D;
49
    /// The OS/2 extended attributes header
50
    pub const EXT_HEADER_OS2_ATTR1:    u8 = 0x7E;
51
    /// Level 3 extended attributes header
52
    pub const EXT_HEADER_EXT_ATTRS:    u8 = 0x7F;
53
    /// The OS/9 extended attributes header
54
    pub const EXT_HEADER_OS9:          u8 = 0xCC;
55
    /// The metadata header, currently used by MorphOS to store file comments
56
    pub const EXT_HEADER_METADATA:     u8 = 0x71;
57
}
58

59
use ext::*;
60
/// An iterator through extra headers, yielding the headers' raw content excluding
61
/// the next header length field.
62
pub struct ExtraHeaderIter<'a> {
63
    data: &'a [u8],
64
    header_length: u32,
65
    header_len32: bool
66
}
67

68
impl<'a> Iterator for ExtraHeaderIter<'a> {
69
    type Item = &'a [u8];
70

71
    fn next(&mut self) -> Option<Self::Item> {
110,632✔
72
        let header_length = self.header_length as usize;
110,632✔
73
        if header_length == 0 {
110,632✔
74
            return None
43,559✔
75
        }
67,073✔
76
        let (res, data) = self.data.split_at(header_length);
67,073✔
77
        let (res, len) = if self.header_len32 {
67,073✔
78
            res.split_last_chunk::<4>().map(|(dat, &len)|
4,515✔
79
                (dat, u32::from_le_bytes(len)))
4,515✔
80
        }
81
        else {
82
            res.split_last_chunk::<2>().map(|(dat, &len)|
62,558✔
83
                (dat, u16::from_le_bytes(len).into()))
62,558✔
84
        }.unwrap();
67,073✔
85
        self.header_length = len;
67,073✔
86
        self.data = data;
67,073✔
87
        Some(res)
67,073✔
88
    }
110,632✔
89
}
90

91
/// Allocate at once this number of bytes maximum when reading variable size fields
92
const ALLOCATE_LIMIT_MAX: usize = 8*1024;
93

94
/// The raw LHA header fragment with a rigid structure
95
#[derive(Clone, Copy, Debug, Default, NoUninit, AnyBitPattern)]
96
#[repr(C)]
97
#[repr(packed)]
98
struct LhaRawBaseHeader {
99
    compression: [u8;5],
100
    compressed_size: [u8;4],
101
    original_size: [u8;4],
102
    last_modified: [u8;4],
103
    msdos_attrs: u8,
104
    lha_level: u8
105
}
106

107
/// The internal header parser object
108
struct Parser<'a, R> {
109
    rd: &'a mut R,
110
    /// A collected header's CRC-16 checksum
111
    crc: Crc16,
112
    /// A collected header's wrapping sum checksum
113
    csum: Wrapping<u8>,
114
    /// The number of bytes parsed so far
115
    len: usize
116
}
117

118
impl<R: Read> Parser<'_, R> {
119
    /// Read a next byte if there is one more in the stream increasing
120
    /// the parsed counter and updating the header CRC-16 checksum.
121
    ///
122
    /// NOTE: this function does not update the wrapping sum.
123
    fn read_u8_or_none(&mut self) -> LhaResult<Option<u8>, R> {
992✔
124
        let mut byte = 0u8;
992✔
125
        if 0 == self.rd.read_all(slice::from_mut(&mut byte)).map_err(LhaError::Io)? {
992✔
126
            return Ok(None)
11✔
127
        }
981✔
128
        self.update_checksums_no_wrapping_sum(slice::from_ref(&byte));
981✔
129
        Ok(Some(byte))
981✔
130
    }
992✔
131
    /// Read the next byte, increase the parsed counter and update all checksums
132
    fn read_u8(&mut self) -> LhaResult<u8, R> {
1,323✔
133
        let mut byte: u8 = 0;
1,323✔
134
        self.read_exact(slice::from_mut(&mut byte))?;
1,323✔
135
        Ok(byte)
1,323✔
136
    }
1,323✔
137
    /// Read the next 2 bytes, increase the parsed counter and update all checksums.
138
    /// Return an LE 16-bit value.
139
    fn read_u16(&mut self) -> LhaResult<u16, R> {
874✔
140
        let mut buf = [0u8;2];
874✔
141
        self.read_exact(&mut buf)?;
874✔
142
        Ok(u16::from_le_bytes(buf))
874✔
143
    }
874✔
144
    /// Read the next 4 bytes, increase the parsed counter and update all checksums.
145
    /// Return an LE 32-bit value.
146
    fn read_u32(&mut self) -> LhaResult<u32, R> {
28✔
147
        let mut buf = [0u8;4];
28✔
148
        self.read_exact(&mut buf)?;
28✔
149
        Ok(u32::from_le_bytes(buf))
28✔
150
    }
28✔
151
    /// Read the exact number of bytes, increase the parsed counter and update all
152
    /// checksums.
153
    fn read_exact(&mut self, buf: &mut [u8]) -> LhaResult<(), R> {
2,790✔
154
        self.rd.read_exact(buf).map_err(LhaError::Io)?;
2,790✔
155
        self.update_checksums(buf);
2,790✔
156
        Ok(())
2,790✔
157
    }
2,790✔
158
    /// Read the `limit` bytes into an newly allocated boxed slice, increase the
159
    /// parsed counter and update all checksums.
160
    fn read_limit(&mut self, limit: usize) -> LhaResult<Box<[u8]>, R> {
464✔
161
        let mut buf = Vec::new();
464✔
162
        self.read_limit_no_checksums(limit, &mut buf)?;
464✔
163
        self.update_checksums(&buf);
464✔
164
        Ok(buf.into_boxed_slice())
464✔
165
    }
464✔
166
    /// Increase the parser counter and update all header checksums from data
167
    fn update_checksums(&mut self, data: &[u8]) {
3,254✔
168
        self.update_checksums_no_wrapping_sum(data);
3,254✔
169
        self.csum = wrapping_csum(self.csum, data);
3,254✔
170
    }
3,254✔
171
    /// Increase the parser counter and update only the CRC-16 header checksum
172
    fn update_checksums_no_wrapping_sum(&mut self, data: &[u8]) {
5,068✔
173
        self.len += data.len();
5,068✔
174
        self.crc.digest(data);
5,068✔
175
    }
5,068✔
176
    /// Read the `limit` bytes into a vector.
177
    ///
178
    /// This function does not increase the parsed counter, nor updates any checksums.
179
    ///
180
    /// Take care not to allocate too much memory when doing so, until more data
181
    /// is read from the stream.
182
    fn read_limit_no_checksums(&mut self, mut limit: usize, buf: &mut Vec<u8>) -> LhaResult<(), R> {
1,298✔
183
        while limit != 0 {
2,556✔
184
            let chunk_size = limit.min(ALLOCATE_LIMIT_MAX);
1,259✔
185
            buf.try_reserve_exact(chunk_size).map_err(|err| LhaError::HeaderParse(err.into()))?;
1,259✔
186
            // FIXME: use BorrowedBuf once stabilized
187
            let spare_uninit = &mut buf.spare_capacity_mut()[..chunk_size];
1,259✔
188
            // SAFETY: assume read_exact is write-only
189
            let spare = unsafe { spare_uninit.assume_init_mut() };
1,259✔
190
            self.rd.read_exact(spare).map_err(LhaError::Io)?;
1,259✔
191
            // SAFETY: assume chunk_size was read into buf
192
            // this can't overflow because buf.len() + chunk_size <= buf.capacity()
193
            unsafe { buf.set_len(buf.len() + chunk_size); }
1,258✔
194
            limit -= chunk_size;
1,258✔
195
        }
196
        Ok(())
1,297✔
197
    }
1,298✔
198
}
199

200
impl LhaHeader {
201
    /// Attempt to parse the LHA header. Return `Ok(Some(LhaHeader))` on success. Return `Ok(None)`
202
    /// if the end of archive marker (a `0` byte) was encountered.
203
    ///
204
    /// The method validates all length and checksum fields of the header, but does not parse extra
205
    /// headers except:
206
    ///
207
    /// * The ["Common"][EXT_HEADER_COMMON] header for validating the header's CRC-16 checksum.
208
    /// * The ["MS-DOS Attributes"][EXT_HEADER_MSDOS_ATTRS] header for reading MS-DOS attributes.
209
    /// * The ["File size"][EXT_HEADER_FILE_SIZES] header for reading 64-bit file size.
210
    ///
211
    /// All extra header data is available as raw bytes and raw extra headers can be easily iterated
212
    /// with the [`LhaHeader::iter_extra`] function.
213
    ///
214
    /// [`LhaHeader`] methods can be further called on the returned object to attempt to parse the
215
    /// additional properties of an archive entry.
216
    ///
217
    /// # Errors
218
    /// Returns an error from the underlying reading operations or because a malformed header was
219
    /// encountered.
220
    pub fn read<R: Read>(rd: &mut R) -> LhaResult<Option<LhaHeader>, R> {
992✔
221
        let mut parser = Parser {
992✔
222
            rd, 
992✔
223
            crc: Crc16::default(),
992✔
224
            csum: Wrapping(0),
992✔
225
            len: 0
992✔
226
        };
992✔
227
        let header_len = match parser.read_u8_or_none()? {
992✔
228
            Some(0)|None => return Ok(None),
427✔
229
            Some(len) => len
565✔
230
        };
231
        let csum = parser.read_u8()?;
565✔
232
        // reset wrapping checksum which should not include the first 2 bytes
233
        parser.csum = Wrapping(0);
565✔
234

235
        let mut raw_header = LhaRawBaseHeader::default();
565✔
236
        parser.read_exact(bytes_of_mut(&mut raw_header))?;
565✔
237
        if raw_header.lha_level > 3 {
565✔
UNCOV
238
            return Err(LhaError::HeaderParse(LhaHeaderError::UnknownLevel))
×
239
        }
565✔
240

241
        // read filename if level 0 or 1
242
        let filename = if raw_header.lha_level < 2 {
565✔
243
            let filename_len = parser.read_u8()? as usize;
429✔
244
            if (header_len as usize) < parser.len + filename_len {
429✔
UNCOV
245
                return Err(LhaError::HeaderParse(LhaHeaderError::SizeMismatch))
×
246
            }
429✔
247
            parser.read_limit(filename_len)?
429✔
248
        }
249
        else {
250
            Box::new([])
136✔
251
        };
252

253
        // file CRC-16
254
        let file_crc = parser.read_u16()?;
565✔
255

256
        // OS-TYPE
257
        let mut os_type = 0;
565✔
258
        if raw_header.lha_level > 0 {
565✔
259
            os_type = parser.read_u8()?;
323✔
260
        }
242✔
261

262
        // extended area, only 0 and 1 level
263
        let mut extended_area: Box<[u8]> = Box::new([]);
565✔
264
        if raw_header.lha_level < 2 {
565✔
265
            let mut min_len = parser.len;
429✔
266
            if raw_header.lha_level == 0 {
429✔
267
                min_len -= 2; // no extra headers
242✔
268
            }
273✔
269
            let extended_len = (header_len as usize).checked_sub(min_len)
429✔
270
                              .ok_or(LhaHeaderError::SizeMismatch)?;
429✔
271
            if extended_len != 0 {
429✔
272
                extended_area = parser.read_limit(extended_len)?;
35✔
273
            }
394✔
274
        };
136✔
275

276
        // extra headers
277
        let mut long_header_len: u32 = 0; // a long header length found in level >= 2
565✔
278
        let mut first_header_len: u32 = 0;
565✔
279
        // establish the first extra header length and the long header length
280
        match raw_header.lha_level {
565✔
281
            1 => {
282
                first_header_len = parser.read_u16()? as u32;
187✔
283
            }
284
            2 => {
285
                long_header_len = u16::from_le_bytes([header_len, csum]) as u32;
122✔
286
                first_header_len = parser.read_u16()? as u32;
122✔
287
            }
288
            3 => {
289
                long_header_len = parser.read_u32()?;
14✔
290
                first_header_len = parser.read_u32()?;
14✔
291
                if header_len != 4 || csum != 0 {
14✔
UNCOV
292
                    return Err(LhaError::HeaderParse(LhaHeaderError::Level3Signature))
×
293
                }
14✔
294
            }
295
            _ => {}
242✔
296
        }
297

298
        // validate level 0 and 1 header checksum
299
        if raw_header.lha_level < 2 {
565✔
300
            if csum != parser.csum.0 {
429✔
UNCOV
301
                return Err(LhaError::HeaderParse(LhaHeaderError::WrappingSumMismatch))
×
302
            }
429✔
303
        }
304
        else if (long_header_len.saturating_sub(first_header_len) as usize) < parser.len {
136✔
305
            return Err(LhaError::HeaderParse(LhaHeaderError::LongSizeMismatch))
1✔
306
        }
135✔
307

308
        let mut extra_headers = Vec::new();
564✔
309
        let mut msdos_attrs = MsDosAttrs::from_bits_retain(raw_header.msdos_attrs as u16);
564✔
310
        let mut original_size = u32::from_le_bytes(raw_header.original_size) as u64;
564✔
311
        let mut compressed_size = u32::from_le_bytes(raw_header.compressed_size) as u64;
564✔
312
        let mut header_crc: Option<u16> = None;
564✔
313
        // read extra headers
314
        let min_header_len = if raw_header.lha_level == 3 { 5 } else { 3 };
564✔
315
        let mut extra_header_len = first_header_len as usize;
564✔
316
        while extra_header_len != 0 {
1,397✔
317
            if extra_header_len < min_header_len {
834✔
UNCOV
318
                return Err(LhaError::HeaderParse(LhaHeaderError::ExtendedHeaderSize))
×
319
            }
834✔
320
            // check long header length (level 2, 3)
321
            if long_header_len != 0 {
834✔
322
                if (long_header_len as usize).saturating_sub(extra_header_len - 2) < parser.len {
464✔
UNCOV
323
                    return Err(LhaError::HeaderParse(LhaHeaderError::LongSizeMismatch))
×
324
                }
464✔
325
            }
326
            else if compressed_size < (extra_headers.len() as u64) + extra_header_len as u64  {
370✔
327
                // otherwise check skip size (level 1)
UNCOV
328
                return Err(LhaError::HeaderParse(LhaHeaderError::SkipSizeMismatch))
×
329
            }
370✔
330
            parser.read_limit_no_checksums(extra_header_len, &mut extra_headers)?;
834✔
331
            let start = extra_headers.len() - extra_header_len;
833✔
332
            let header = &mut extra_headers[start..];
833✔
333
            match header {
25✔
334
                // we need to extract the CRC-16 from header and clear it in order to calculate checksum
335
                [EXT_HEADER_COMMON, data @ ..] => {
174✔
336
                    if header_crc.is_some() {
174✔
UNCOV
337
                        return Err(LhaError::HeaderParse(LhaHeaderError::CommonHeader))
×
338
                    }
174✔
339
                    if let Some(crc) = data.get_mut(0..2) {
174✔
340
                        header_crc = read_u16(crc);
174✔
341
                        for p in crc.iter_mut() {
348✔
342
                            *p = 0;
348✔
343
                        }
348✔
UNCOV
344
                    }
×
345
                }
346
                [EXT_HEADER_MSDOS_ATTRS, data @ ..]|
25✔
347
                [EXT_HEADER_EXT_ATTRS,   data @ ..] if data.len() >= 2 => {
29✔
348
                    if let Some(attrs) = read_u16(&data[0..2]) {
37✔
349
                        msdos_attrs = MsDosAttrs::from_bits_retain(attrs);
37✔
350
                    }
37✔
351
                }
352
                [EXT_HEADER_FILE_SIZES, data @ ..] if raw_header.lha_level >= 2 && data.len() >= 16 => {
2✔
353
                    if let (Some(compr), Some(orig)) = (read_u64(&data[0..8]), read_u64(&data[8..16])) {
2✔
354
                        compressed_size = compr;
2✔
355
                        original_size = orig;
2✔
356
                    }
2✔
357
                }
358
                _ => {}
620✔
359
            }
360
            parser.update_checksums_no_wrapping_sum(header);
833✔
361
            extra_header_len = if raw_header.lha_level == 3 {
833✔
362
                u32::from_le_bytes(*header.last_chunk::<4>().unwrap()) as usize
42✔
363
            }
364
            else {
365
                u16::from_le_bytes(*header.last_chunk::<2>().unwrap()) as usize
791✔
366
            }
367
        }
368

369
        // validate long header length
370
        if long_header_len != 0 &&
563✔
371
           long_header_len as usize != parser.len
134✔
372
        {
373
            if raw_header.lha_level == 2 && (long_header_len as usize) - 1 == parser.len
18✔
374
            {
375
                // read padding byte
376
                parser.read_u8()?;
6✔
377
            }
378
            else if raw_header.lha_level != 2 || long_header_len as usize != parser.len - 2
12✔
379
            {
380
                // some packers (Osk) don't include self in the header length
UNCOV
381
                return Err(LhaError::HeaderParse(LhaHeaderError::LongSizeMismatch))
×
382
            }
12✔
383
        }
545✔
384

385
        // validate headers CRC
386
        if let Some(crc) = header_crc && crc != parser.crc.sum16() {
563✔
UNCOV
387
            return Err(LhaError::HeaderParse(LhaHeaderError::Crc16Mismatch))
×
388
        }
563✔
389

390
        // adjust compressed size for level 1
391
        if raw_header.lha_level == 1 {
563✔
392
            compressed_size = compressed_size.checked_sub(extra_headers.len() as u64)
187✔
393
                .ok_or(LhaError::HeaderParse(LhaHeaderError::SkipSizeMismatch))?
187✔
394
        }
376✔
395

396
        let compression = raw_header.compression;
563✔
397
        let last_modified = u32::from_le_bytes(raw_header.last_modified);
563✔
398
        let extra_headers = extra_headers.into_boxed_slice();
563✔
399

400
        Ok(Some(LhaHeader {
563✔
401
            level: raw_header.lha_level,
563✔
402
            compression,
563✔
403
            compressed_size,
563✔
404
            original_size,
563✔
405
            filename,
563✔
406
            os_type,
563✔
407
            msdos_attrs,
563✔
408
            last_modified,
563✔
409
            file_crc,
563✔
410
            extended_area,
563✔
411
            first_header_len,
563✔
412
            extra_headers
563✔
413
        }))
563✔
414
    }
992✔
415

416
    /// Return an iterator that will iterate through extra headers, yielding the headers' raw
417
    /// data, excluding the next header length field.
418
    ///
419
    /// # Note
420
    /// Each iterated slice will have at least the size of 1 byte containing the header identifier.
421
    pub fn iter_extra(&self) -> ExtraHeaderIter<'_> {
49,665✔
422
        ExtraHeaderIter {
49,665✔
423
            data: &self.extra_headers,
49,665✔
424
            header_length: self.first_header_len,
49,665✔
425
            header_len32: self.level == 3
49,665✔
426
        }
49,665✔
427
    }
49,665✔
428
}
429

430
#[inline]
431
pub(super) fn read_u16(slice: &[u8]) -> Option<u16> {
7,481✔
432
    slice.as_array::<{size_of::<u16>()}>().copied().map(u16::from_le_bytes)
7,481✔
433
}
7,481✔
434

435
#[inline]
436
pub(super) fn read_u32(slice: &[u8]) -> Option<u32> {
1,519✔
437
    slice.as_array::<{size_of::<u32>()}>().copied().map(u32::from_le_bytes)
1,519✔
438
}
1,519✔
439

440
#[inline]
441
pub(super) fn read_u64(slice: &[u8]) -> Option<u64> {
560✔
442
    slice.as_array::<{size_of::<u64>()}>().copied().map(u64::from_le_bytes)
560✔
443
}
560✔
444

445
fn wrapping_csum(init: Wrapping<u8>, data: &[u8]) -> Wrapping<u8> {
53,801✔
446
    let sum: Wrapping<u8> = data.iter().copied().map(Wrapping).sum();
53,801✔
447
    sum + init
53,801✔
448
}
53,801✔
449

450
pub(super) fn split_data_at_nil_or_end(data: &[u8]) -> (&[u8], Option<&[u8]>) {
10,272✔
451
    match memchr::memchr(0, data) {
10,272✔
452
        Some(index) => {
248✔
453
            #[cfg(all(not(feature = "no-unsafe-assertions"), not(debug_assertions)))]
454
            unsafe {
455
                // SAFETY: memchr guarantee asserted condition
456
                core::hint::assert_unchecked(index < data.len());
457
            }
458
            (&data[0..index], Some(&data[index + 1..]))
248✔
459
        }
460
        None => (data, None)
10,024✔
461
    }
462
}
10,272✔
463

464
#[cfg(feature = "std")]
465
pub(super) fn parse_pathname(data: &[u8], path: &mut PathBuf) {
8,624✔
466
    path.reserve(data.len());
8,624✔
467
    // split by all possible path separators
468
    for part in data.split(|&c| matches!(c, 0xFF|b'/'|b'\\')) {
88,707✔
469
        match part {
13,810✔
470
            b"."|b".."|[] => {} // ignore malicious and empty paths
13,810✔
471
            name => path.push(parse_str_nilterm(name, false, false).as_ref())
11,235✔
472
        }
473
    }
474
}
8,624✔
475

476
pub(super) fn parse_pathname_to_str(data: &[u8], path: &mut String) {
7,876✔
477
    path.reserve(data.len());
7,876✔
478
    // split by all possible path separators
479
    for part in data.split(|&c| matches!(c, 0xFF|b'/'|b'\\')) {
80,486✔
480
        match part {
12,522✔
481
            b"."|b".."|[] => {} // ignore malicious and empty paths
12,522✔
482
            name => {
10,310✔
483
                if !path.is_empty() {
10,310✔
484
                    path.push('/');
3,072✔
485
                }
7,243✔
486
                path.push_str(parse_str_nilterm(name, false, false).as_ref())
10,310✔
487
            }
488
        }
489
    }
490
}
7,876✔
491

492
#[inline(always)]
493
pub(super) fn is_separator(c: char) -> bool {
182,606✔
494
    #[cfg(feature = "std")]
495
    {
496
        std::path::is_separator(c)
182,601✔
497
    }
498
    #[cfg(not(feature = "std"))]
499
    {
500
        matches!(c, '/'|'\\')
5✔
501
    }
502
}
182,606✔
503

504
pub(super) fn parse_str_nilterm(
25,617✔
505
        data: &[u8], nilterm: bool, ignore_sep: bool
25,617✔
506
    ) -> Cow<'_, str>
25,617✔
507
{
508
    if let Some(index) = data.iter().position(|&c|
25,617✔
509
            !(0x20..0x7f).contains(&c) ||
187,184✔
510
            (!ignore_sep && is_separator(c as char))
187,146✔
511
        )
512
    {
513
        let mut out = String::with_capacity(data.len()*3);
41✔
514
        let (head, rest) = data.split_at(index);
41✔
515
        // SAFETY: head was validated to contain ASCII-only characters
516
        out.push_str(unsafe {
41✔
517
            core::str::from_utf8_unchecked(head)
41✔
518
        });
519
        for byte in rest.iter() {
525✔
520
            match byte {
5✔
521
                0 if nilterm => break,
3✔
522
                0x00..=0x1f|
522✔
523
                0x7f..=0xff => {
103✔
524
                    write!(out, "%{:02x}", byte).unwrap();
103✔
525
                }
103✔
526
                &ch => {
419✔
527
                    let c = ch as char;
419✔
528
                    if !ignore_sep && is_separator(c) {
419✔
529
                        out.push('_');
5✔
530
                    }
5✔
531
                    else {
414✔
532
                        out.push(c);
414✔
533
                    }
414✔
534
                }
535
            }
536
        }
537
        Cow::Owned(out)
41✔
538
    }
539
    else {
540
        // SAFETY: data was validated to contain ASCII-only characters
541
        unsafe {
542
            Cow::Borrowed(core::str::from_utf8_unchecked(data))
25,576✔
543
        }
544
    }
545
}
25,617✔
546

547
#[cfg(feature = "std")]
548
#[cfg(test)]
549
mod tests {
550
    use super::*;
551
    use std::path::MAIN_SEPARATOR;
552

553
    fn parse_filename(data: &[u8]) -> Cow<'_, str> {
6✔
554
        parse_str_nilterm(data, false, false)
6✔
555
    }
6✔
556

557
   #[test]
558
    fn split_data_at_nil_or_end_works() {
1✔
559
        assert_eq!((&b"Foo"[..], None), split_data_at_nil_or_end(b"Foo"));
1✔
560
        assert_eq!((&b"Foo"[..], Some(&b"Bar"[..])), split_data_at_nil_or_end(b"Foo\x00Bar"));
1✔
561
        assert_eq!((&[][..], Some(&b"Bar"[..])), split_data_at_nil_or_end(b"\x00Bar"));
1✔
562
    }
1✔
563

564
   #[test]
565
    fn path_parser_works() {
1✔
566
        assert_eq!("", parse_filename(b""));
1✔
567
        assert_eq!("Hello World!", parse_filename(b"Hello World!"));
1✔
568
        if std::path::is_separator('/') {
1✔
569
            assert_eq!("_Hello_World_", parse_filename(b"/Hello/World/"));
1✔
UNCOV
570
        }
×
571
        if std::path::is_separator('\\') {
1✔
UNCOV
572
            assert_eq!("_Hello_World_", parse_filename(br"\Hello\World\"));
×
573
        }
1✔
574
        assert_eq!("Hello%00World%7f", parse_filename(b"Hello\x00World\x7f"));
1✔
575
        assert_eq!("Hello%01World%ff", parse_filename(b"Hello\x01World\xff"));
1✔
576
        assert_eq!("Hello", parse_str_nilterm(b"Hello\x00World\xff", true, false));
1✔
577
        if std::path::is_separator('/') {
1✔
578
            assert_eq!("He_llo", parse_str_nilterm(b"He/llo\x00World\xff", true, false));
1✔
579
            assert_eq!("He/llo", parse_str_nilterm(b"He/llo\x00World\xff", true, true));
1✔
580
            assert_eq!("He/llo%00World%ff", parse_str_nilterm(b"He/llo\x00World\xff", false, true));
1✔
581
            assert_eq!("_Hello%1fWorld%80", parse_filename(b"/Hello\x1fWorld\x80"));
1✔
UNCOV
582
        }
×
583
        let mut path = PathBuf::new();
1✔
584
        parse_pathname(b"", &mut path);
1✔
585
        assert!(path.is_relative());
1✔
586
        assert_eq!("", path.to_str().unwrap());
1✔
587
        parse_pathname(b"/", &mut path);
1✔
588
        assert!(path.is_relative());
1✔
589
        assert_eq!("", path.to_str().unwrap());
1✔
590
        parse_pathname(br"\", &mut path);
1✔
591
        assert!(path.is_relative());
1✔
592
        assert_eq!("", path.to_str().unwrap());
1✔
593
        parse_pathname(br".", &mut path);
1✔
594
        assert!(path.is_relative());
1✔
595
        assert_eq!("", path.to_str().unwrap());
1✔
596
        parse_pathname(br"..", &mut path);
1✔
597
        assert!(path.is_relative());
1✔
598
        assert_eq!("", path.to_str().unwrap());
1✔
599
        parse_pathname(br"./..", &mut path);
1✔
600
        assert!(path.is_relative());
1✔
601
        assert_eq!("", path.to_str().unwrap());
1✔
602
        parse_pathname(br".\..", &mut path);
1✔
603
        assert!(path.is_relative());
1✔
604
        assert_eq!("", path.to_str().unwrap());
1✔
605
        parse_pathname(br"/..\./", &mut path);
1✔
606
        assert!(path.is_relative());
1✔
607
        assert_eq!("", path.to_str().unwrap());
1✔
608
        parse_pathname(br"\../.\", &mut path);
1✔
609
        assert!(path.is_relative());
1✔
610
        assert_eq!("", path.to_str().unwrap());
1✔
611
        parse_pathname(br"foo/bar\baz", &mut path);
1✔
612
        assert!(path.is_relative());
1✔
613
        let expect = format!("foo{}bar{}baz", MAIN_SEPARATOR, MAIN_SEPARATOR);
1✔
614
        assert_eq!(expect, path.to_str().unwrap());
1✔
615
        path.clear();
1✔
616
        parse_pathname(br"\foo/bar\baz/", &mut path);
1✔
617
        assert!(path.is_relative());
1✔
618
        let expect = format!("foo{}bar{}baz", MAIN_SEPARATOR, MAIN_SEPARATOR);
1✔
619
        assert_eq!(expect, path.to_str().unwrap());
1✔
620
        path.clear();
1✔
621
        parse_pathname(br"/foo\bar/baz\", &mut path);
1✔
622
        assert!(path.is_relative());
1✔
623
        let expect = format!("foo{}bar{}baz", MAIN_SEPARATOR, MAIN_SEPARATOR);
1✔
624
        assert_eq!(expect, path.to_str().unwrap());
1✔
625
        path.clear();
1✔
626
        parse_pathname(b"foo\xffbar\xffbaz", &mut path);
1✔
627
        assert!(path.is_relative());
1✔
628
        let expect = format!("foo{}bar{}baz", MAIN_SEPARATOR, MAIN_SEPARATOR);
1✔
629
        assert_eq!(expect, path.to_str().unwrap());
1✔
630
        path.clear();
1✔
631
        parse_pathname(b"\xfffoo\xffb\x91ar\xffbaz\xff", &mut path);
1✔
632
        assert!(path.is_relative());
1✔
633
        let expect = format!("foo{}b%91ar{}baz", MAIN_SEPARATOR, MAIN_SEPARATOR);
1✔
634
        assert_eq!(expect, path.to_str().unwrap());
1✔
635
        path.clear();
1✔
636
    }
1✔
637

638
    #[test]
639
    fn path_parser_to_str_works() {
1✔
640
        let mut path = String::new();
1✔
641
        parse_pathname_to_str(b"", &mut path);
1✔
642
        assert!(!path.starts_with('/'));
1✔
643
        assert_eq!("", &path);
1✔
644
        parse_pathname_to_str(b"/", &mut path);
1✔
645
        assert!(!path.starts_with('/'));
1✔
646
        assert_eq!("", &path);
1✔
647
        parse_pathname_to_str(br"\", &mut path);
1✔
648
        assert!(!path.starts_with('/'));
1✔
649
        assert_eq!("", &path);
1✔
650
        parse_pathname_to_str(br".", &mut path);
1✔
651
        assert!(!path.starts_with('/'));
1✔
652
        assert_eq!("", &path);
1✔
653
        parse_pathname_to_str(br"..", &mut path);
1✔
654
        assert!(!path.starts_with('/'));
1✔
655
        assert_eq!("", &path);
1✔
656
        parse_pathname_to_str(br"./..", &mut path);
1✔
657
        assert!(!path.starts_with('/'));
1✔
658
        assert_eq!("", &path);
1✔
659
        parse_pathname_to_str(br".\..", &mut path);
1✔
660
        assert!(!path.starts_with('/'));
1✔
661
        assert_eq!("", &path);
1✔
662
        parse_pathname_to_str(br"/..\./", &mut path);
1✔
663
        assert!(!path.starts_with('/'));
1✔
664
        assert_eq!("", &path);
1✔
665
        parse_pathname_to_str(br"\../.\", &mut path);
1✔
666
        assert!(!path.starts_with('/'));
1✔
667
        assert_eq!("", &path);
1✔
668
        parse_pathname_to_str(br"foo/bar\baz", &mut path);
1✔
669
        assert!(!path.starts_with('/'));
1✔
670
        let expect = "foo/bar/baz";
1✔
671
        assert_eq!(expect, &path);
1✔
672
        path.clear();
1✔
673
        parse_pathname_to_str(br"\foo/bar\baz/", &mut path);
1✔
674
        assert!(!path.starts_with('/'));
1✔
675
        let expect = "foo/bar/baz";
1✔
676
        assert_eq!(expect, &path);
1✔
677
        path.clear();
1✔
678
        parse_pathname_to_str(br"/foo\bar/baz\", &mut path);
1✔
679
        assert!(!path.starts_with('/'));
1✔
680
        let expect = "foo/bar/baz";
1✔
681
        assert_eq!(expect, &path);
1✔
682
        path.clear();
1✔
683
        parse_pathname_to_str(b"foo\xffbar\xffbaz", &mut path);
1✔
684
        assert!(!path.starts_with('/'));
1✔
685
        let expect = "foo/bar/baz";
1✔
686
        assert_eq!(expect, &path);
1✔
687
        path.clear();
1✔
688
        parse_pathname_to_str(b"\xfffoo\xffb\x91ar\xffbaz\xff", &mut path);
1✔
689
        assert!(!path.starts_with('/'));
1✔
690
        let expect = "foo/b%91ar/baz";
1✔
691
        assert_eq!(expect, &path);
1✔
692
        path.clear();
1✔
693
    }
1✔
694
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc