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

royaltm / rust-delharc / 29506494737

16 Jul 2026 02:24PM UTC coverage: 94.52% (-0.5%) from 95.043%
29506494737

push

github

royaltm
replaced static string errors with new LhaHeaderError and DecompressionError objects:
* improve error messages
* improve conversion of LhaError to io::Error to include the original enum capsule

64 of 111 new or added lines in 6 files covered. (57.66%)

3122 of 3303 relevant lines covered (94.52%)

25079920.21 hits per line

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

95.33
/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_MSDOS_TIME:  u8 = 0x41;
29
    /// The "File size" header with 64-bit file size information
30
    pub const EXT_HEADER_MSDOS_SIZE:  u8 = 0x42;
31
    /// The UNIX ["Permission"](super::Permissions) header
32
    pub const EXT_HEADER_UNIX_PERM:   u8 = 0x50;
33
    /// The UNIX "GID UID" header
34
    pub const EXT_HEADER_UNIX_UIDGID: u8 = 0x51;
35
    /// The UNIX "Group name" header
36
    pub const EXT_HEADER_UNIX_GROUP:  u8 = 0x52;
37
    /// The UNIX "User name" header
38
    pub const EXT_HEADER_UNIX_OWNER:  u8 = 0x53;
39
    /// The UNIX "Time stamp" header
40
    pub const EXT_HEADER_UNIX_TIME:   u8 = 0x54;
41
    /// The Mac "Capsule" header
42
    pub const EXT_HEADER_MAC_CAPSULE: u8 = 0x7D;
43
    /// The OS/2 "Extended attribute" header
44
    pub const EXT_HEADER_OS2_ATTR1:   u8 = 0x7E;
45
    /// Level 3 "Extended attribute" header
46
    pub const EXT_HEADER_EXT_ATTRS:   u8 = 0x7F;
47
}
48

49
use ext::*;
50
/// An iterator through extra headers, yielding the headers' raw content excluding
51
/// the next header length field.
52
pub struct ExtraHeaderIter<'a> {
53
    data: &'a [u8],
54
    header_length: u32,
55
    header_len32: bool
56
}
57

58
impl<'a> Iterator for ExtraHeaderIter<'a> {
59
    type Item = &'a [u8];
60

61
    fn next(&mut self) -> Option<Self::Item> {
43,090✔
62
        let header_length = self.header_length as usize;
43,090✔
63
        if header_length == 0 {
43,090✔
64
            return None
14,932✔
65
        }
28,158✔
66
        let (res, data) = self.data.split_at(header_length);
28,158✔
67
        let (res, len) = if self.header_len32 {
28,158✔
68
            res.split_last_chunk::<4>().map(|(dat, &len)|
1,812✔
69
                (dat, u32::from_le_bytes(len)))
1,812✔
70
        }
71
        else {
72
            res.split_last_chunk::<2>().map(|(dat, &len)|
26,346✔
73
                (dat, u16::from_le_bytes(len).into()))
26,346✔
74
        }.unwrap();
28,158✔
75
        self.header_length = len;
28,158✔
76
        self.data = data;
28,158✔
77
        Some(res)
28,158✔
78
    }
43,090✔
79
}
80

81
/// Allocate at once this number of bytes maximum when reading variable size fields
82
const ALLOCATE_LIMIT_MAX: usize = 8*1024;
83

84
/// The raw LHA header fragment with a rigid structure
85
#[derive(Clone, Copy, Debug, Default, NoUninit, AnyBitPattern)]
86
#[repr(C)]
87
#[repr(packed)]
88
struct LhaRawBaseHeader {
89
    compression: [u8;5],
90
    compressed_size: [u8;4],
91
    original_size: [u8;4],
92
    last_modified: [u8;4],
93
    msdos_attrs: u8,
94
    lha_level: u8
95
}
96

97
/// The internal header parser object
98
struct Parser<'a, R> {
99
    rd: &'a mut R,
100
    /// A collected header's CRC-16 checksum
101
    crc: Crc16,
102
    /// A collected header's wrapping sum checksum
103
    csum: Wrapping<u8>,
104
    /// The number of bytes parsed so far
105
    len: usize
106
}
107

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

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

225
        let mut raw_header = LhaRawBaseHeader::default();
372✔
226
        parser.read_exact(bytes_of_mut(&mut raw_header))?;
372✔
227
        if raw_header.lha_level > 3 {
372✔
NEW
228
            return Err(LhaError::HeaderParse(LhaHeaderError::UnknownLevel))
×
229
        }
372✔
230

231
        // read filename if level 0 or 1
232
        let filename = if raw_header.lha_level < 2 {
372✔
233
            let filename_len = parser.read_u8()? as usize;
283✔
234
            if (header_len as usize) < parser.len + filename_len {
283✔
NEW
235
                return Err(LhaError::HeaderParse(LhaHeaderError::SizeMismatch))
×
236
            }
283✔
237
            parser.read_limit(filename_len)?
283✔
238
        }
239
        else {
240
            Box::new([])
89✔
241
        };
242

243
        // file CRC-16
244
        let file_crc = parser.read_u16()?;
372✔
245

246
        // OS-TYPE
247
        let mut os_type = 0;
372✔
248
        if raw_header.lha_level > 0 {
372✔
249
            os_type = parser.read_u8()?;
227✔
250
        }
145✔
251

252
        // extended area, only 0 and 1 level
253
        let mut extended_area: Box<[u8]> = Box::new([]);
372✔
254
        if raw_header.lha_level < 2 {
372✔
255
            let mut min_len = parser.len;
283✔
256
            if raw_header.lha_level == 0 {
283✔
257
                min_len -= 2; // no extra headers
145✔
258
            }
182✔
259
            if (header_len as usize) < min_len {
283✔
NEW
260
                return Err(LhaError::HeaderParse(LhaHeaderError::SizeMismatch))
×
261
            }
283✔
262
            let mut extended_len = (header_len as usize) - min_len;
283✔
263
            if extended_len != 0 && raw_header.lha_level == 0  {
283✔
264
                // Get optional os_type from level 0 extended area
265
                extended_len -= 1;
34✔
266
                os_type = parser.read_u8()?;
34✔
267
            }
249✔
268
            if extended_len != 0 {
283✔
269
                extended_area = parser.read_limit(extended_len)?;
34✔
270
            }
249✔
271
        };
89✔
272

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

295
        // validate level 0 and 1 header checksum
296
        if raw_header.lha_level < 2 {
372✔
297
            if csum != parser.csum.0 {
283✔
NEW
298
                return Err(LhaError::HeaderParse(LhaHeaderError::WrappingSumMismatch))
×
299
            }
283✔
300
        }
301
        else if (long_header_len.saturating_sub(first_header_len) as usize) < parser.len {
89✔
302
            return Err(LhaError::HeaderParse(LhaHeaderError::LongSizeMismatch))
1✔
303
        }
88✔
304

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

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

382
        // validate headers CRC
383
        if let Some(crc) = header_crc && crc != parser.crc.sum16() {
370✔
NEW
384
            return Err(LhaError::HeaderParse(LhaHeaderError::Crc16Mismatch))
×
385
        }
370✔
386

387
        // adjust compressed size for level 1
388
        if raw_header.lha_level == 1 {
370✔
389
            compressed_size = compressed_size.checked_sub(extra_headers.len() as u64)
138✔
390
                .ok_or(LhaError::HeaderParse(LhaHeaderError::SkipSizeMismatch))?
138✔
391
        }
232✔
392

393
        let compression = raw_header.compression;
370✔
394
        let last_modified = u32::from_le_bytes(raw_header.last_modified);
370✔
395
        let extra_headers = extra_headers.into_boxed_slice();
370✔
396

397
        Ok(Some(LhaHeader {
370✔
398
            level: raw_header.lha_level,
370✔
399
            compression,
370✔
400
            compressed_size,
370✔
401
            original_size,
370✔
402
            filename,
370✔
403
            os_type,
370✔
404
            msdos_attrs,
370✔
405
            last_modified,
370✔
406
            file_crc,
370✔
407
            extended_area,
370✔
408
            first_header_len,
370✔
409
            extra_headers
370✔
410
        }))
370✔
411
    }
669✔
412

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

427
#[inline]
428
pub(super) fn read_u16(slice: &[u8]) -> Option<u16> {
2,357✔
429
    slice.as_array::<{size_of::<u16>()}>().copied().map(u16::from_le_bytes)
2,357✔
430
}
2,357✔
431

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

437
#[inline]
438
pub(super) fn read_u64(slice: &[u8]) -> Option<u64> {
223✔
439
    slice.as_array::<{size_of::<u64>()}>().copied().map(u64::from_le_bytes)
223✔
440
}
223✔
441

442
fn wrapping_csum(init: Wrapping<u8>, data: &[u8]) -> Wrapping<u8> {
28,337✔
443
    let sum: Wrapping<u8> = data.iter().copied().map(Wrapping).sum();
28,337✔
444
    sum + init
28,337✔
445
}
28,337✔
446

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

461
#[cfg(feature = "std")]
462
pub(super) fn parse_pathname(data: &[u8], path: &mut PathBuf) {
4,886✔
463
    path.reserve(data.len());
4,886✔
464
    // split by all possible path separators
465
    for part in data.split(|&c| matches!(c, 0xFF|b'/'|b'\\')) {
41,775✔
466
        match part {
7,744✔
467
            b"."|b".."|[] => {} // ignore malicious and empty paths
7,744✔
468
            name => path.push(parse_str_nilterm(name, false, false).as_ref())
5,703✔
469
        }
470
    }
471
}
4,886✔
472

473
pub(super) fn parse_pathname_to_str(data: &[u8], path: &mut String) {
4,336✔
474
    path.reserve(data.len());
4,336✔
475
    // split by all possible path separators
476
    for part in data.split(|&c| matches!(c, 0xFF|b'/'|b'\\')) {
37,653✔
477
        match part {
6,930✔
478
            b"."|b".."|[] => {} // ignore malicious and empty paths
6,930✔
479
            name => {
5,188✔
480
                if !path.is_empty() {
5,188✔
481
                    path.push('/');
1,721✔
482
                }
3,472✔
483
                path.push_str(parse_str_nilterm(name, false, false).as_ref())
5,188✔
484
            }
485
        }
486
    }
487
}
4,336✔
488

489
#[cfg(feature = "std")]
490
#[inline(always)]
491
fn is_separator(c: char) -> bool {
85,743✔
492
    std::path::is_separator(c)
85,743✔
493
}
85,743✔
494

495
#[cfg(not(feature = "std"))]
496
fn is_separator(c: char) -> bool {
5✔
497
    matches!(c, '/'|'\\')
5✔
498
}
5✔
499

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

543
#[cfg(feature = "std")]
544
#[cfg(test)]
545
mod tests {
546
    use super::*;
547
    use std::path::MAIN_SEPARATOR;
548

549
    fn parse_filename(data: &[u8]) -> Cow<'_, str> {
6✔
550
        parse_str_nilterm(data, false, false)
6✔
551
    }
6✔
552

553
   #[test]
554
    fn split_data_at_nil_or_end_works() {
1✔
555
        assert_eq!((&b"Foo"[..], None), split_data_at_nil_or_end(b"Foo"));
1✔
556
        assert_eq!((&b"Foo"[..], Some(&b"Bar"[..])), split_data_at_nil_or_end(b"Foo\x00Bar"));
1✔
557
        assert_eq!((&[][..], Some(&b"Bar"[..])), split_data_at_nil_or_end(b"\x00Bar"));
1✔
558
    }
1✔
559

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

634
    #[test]
635
    fn path_parser_to_str_works() {
1✔
636
        let mut path = String::new();
1✔
637
        parse_pathname_to_str(b"", &mut path);
1✔
638
        assert!(!path.starts_with('/'));
1✔
639
        assert_eq!("", &path);
1✔
640
        parse_pathname_to_str(b"/", &mut path);
1✔
641
        assert!(!path.starts_with('/'));
1✔
642
        assert_eq!("", &path);
1✔
643
        parse_pathname_to_str(br"\", &mut path);
1✔
644
        assert!(!path.starts_with('/'));
1✔
645
        assert_eq!("", &path);
1✔
646
        parse_pathname_to_str(br".", &mut path);
1✔
647
        assert!(!path.starts_with('/'));
1✔
648
        assert_eq!("", &path);
1✔
649
        parse_pathname_to_str(br"..", &mut path);
1✔
650
        assert!(!path.starts_with('/'));
1✔
651
        assert_eq!("", &path);
1✔
652
        parse_pathname_to_str(br"./..", &mut path);
1✔
653
        assert!(!path.starts_with('/'));
1✔
654
        assert_eq!("", &path);
1✔
655
        parse_pathname_to_str(br".\..", &mut path);
1✔
656
        assert!(!path.starts_with('/'));
1✔
657
        assert_eq!("", &path);
1✔
658
        parse_pathname_to_str(br"/..\./", &mut path);
1✔
659
        assert!(!path.starts_with('/'));
1✔
660
        assert_eq!("", &path);
1✔
661
        parse_pathname_to_str(br"\../.\", &mut path);
1✔
662
        assert!(!path.starts_with('/'));
1✔
663
        assert_eq!("", &path);
1✔
664
        parse_pathname_to_str(br"foo/bar\baz", &mut path);
1✔
665
        assert!(!path.starts_with('/'));
1✔
666
        let expect = "foo/bar/baz";
1✔
667
        assert_eq!(expect, &path);
1✔
668
        path.clear();
1✔
669
        parse_pathname_to_str(br"\foo/bar\baz/", &mut path);
1✔
670
        assert!(!path.starts_with('/'));
1✔
671
        let expect = "foo/bar/baz";
1✔
672
        assert_eq!(expect, &path);
1✔
673
        path.clear();
1✔
674
        parse_pathname_to_str(br"/foo\bar/baz\", &mut path);
1✔
675
        assert!(!path.starts_with('/'));
1✔
676
        let expect = "foo/bar/baz";
1✔
677
        assert_eq!(expect, &path);
1✔
678
        path.clear();
1✔
679
        parse_pathname_to_str(b"foo\xffbar\xffbaz", &mut path);
1✔
680
        assert!(!path.starts_with('/'));
1✔
681
        let expect = "foo/bar/baz";
1✔
682
        assert_eq!(expect, &path);
1✔
683
        path.clear();
1✔
684
        parse_pathname_to_str(b"\xfffoo\xffb\x91ar\xffbaz\xff", &mut path);
1✔
685
        assert!(!path.starts_with('/'));
1✔
686
        let expect = "foo/b%91ar/baz";
1✔
687
        assert_eq!(expect, &path);
1✔
688
        path.clear();
1✔
689
    }
1✔
690
}
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