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

qubit-ltd / rs-codec / 22955caa-f634-4543-8f60-2b7e9c6f596f

04 May 2026 12:26PM UTC coverage: 98.042% (+1.1%) from 96.956%
22955caa-f634-4543-8f60-2b7e9c6f596f

push

circleci

Haixing-Hu
fix: support escaped spaces in C string literals

1 of 1 new or added line in 1 file covered. (100.0%)

13 existing lines in 1 file now uncovered.

651 of 664 relevant lines covered (98.04%)

16.1 hits per line

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

95.37
/src/hex_codec.rs
1
/*******************************************************************************
2
 *
3
 *    Copyright (c) 2026 Haixing Hu.
4
 *
5
 *    SPDX-License-Identifier: Apache-2.0
6
 *
7
 *    Licensed under the Apache License, Version 2.0.
8
 *
9
 ******************************************************************************/
10
//! Hexadecimal byte codec.
11

12
use crate::{
13
    CodecError,
14
    CodecResult,
15
    Decoder,
16
    Encoder,
17
};
18

19
/// Encodes and decodes hexadecimal byte strings.
20
#[derive(Debug, Clone, PartialEq, Eq)]
21
pub struct HexCodec {
22
    /// Whether to use uppercase hexadecimal digits.
23
    uppercase: bool,
24
    /// The prefix to use before the whole encoded string.
25
    prefix: Option<String>,
26
    /// The prefix to use before each encoded byte.
27
    byte_prefix: Option<String>,
28
    /// The separator to use between bytes in the encoded string.
29
    separator: Option<String>,
30
    /// Whether to ignore ASCII whitespace while decoding.
31
    ignore_ascii_whitespace: bool,
32
    /// Whether to ignore ASCII case when matching configured prefixes.
33
    ignore_prefix_case: bool,
34
}
35

36
impl HexCodec {
37
    /// Creates a lowercase codec without prefix or separators.
38
    ///
39
    /// # Returns
40
    /// A hexadecimal codec using lowercase digits.
41
    pub fn new() -> Self {
22✔
42
        Self {
22✔
43
            uppercase: false,
22✔
44
            prefix: None,
22✔
45
            byte_prefix: None,
22✔
46
            separator: None,
22✔
47
            ignore_ascii_whitespace: false,
22✔
48
            ignore_prefix_case: false,
22✔
49
        }
22✔
50
    }
22✔
51

52
    /// Creates an uppercase codec without prefix or separators.
53
    ///
54
    /// # Returns
55
    /// A hexadecimal codec using uppercase digits.
56
    pub fn upper() -> Self {
2✔
57
        Self::new().with_uppercase(true)
2✔
58
    }
2✔
59

60
    /// Sets whether encoded digits should be uppercase.
61
    ///
62
    /// # Parameters
63
    /// - `uppercase`: Whether to use uppercase hexadecimal digits.
64
    ///
65
    /// # Returns
66
    /// The updated codec.
67
    pub fn with_uppercase(mut self, uppercase: bool) -> Self {
3✔
68
        self.uppercase = uppercase;
3✔
69
        self
3✔
70
    }
3✔
71

72
    /// Sets a whole-output prefix.
73
    ///
74
    /// The prefix is written once before the encoded bytes and required once
75
    /// before decoded input. For example, using prefix `0x` encodes bytes as
76
    /// `0x1f8b`.
77
    ///
78
    /// # Parameters
79
    /// - `prefix`: Whole-output prefix text such as `0x`.
80
    ///
81
    /// # Returns
82
    /// The updated codec.
83
    pub fn with_prefix(mut self, prefix: impl Into<String>) -> Self {
10✔
84
        self.prefix = Some(prefix.into());
10✔
85
        self
10✔
86
    }
10✔
87

88
    /// Sets a per-byte prefix.
89
    ///
90
    /// The prefix is written before every encoded byte and required before
91
    /// every decoded byte. For example, using byte prefix `0x` and separator
92
    /// ` ` encodes bytes as `0x1f 0x8b`.
93
    ///
94
    /// # Parameters
95
    /// - `prefix`: Per-byte prefix text such as `0x`.
96
    ///
97
    /// # Returns
98
    /// The updated codec.
99
    pub fn with_byte_prefix(mut self, prefix: impl Into<String>) -> Self {
7✔
100
        self.byte_prefix = Some(prefix.into());
7✔
101
        self
7✔
102
    }
7✔
103

104
    /// Sets a separator written and accepted between encoded bytes.
105
    ///
106
    /// # Parameters
107
    /// - `separator`: Separator text.
108
    ///
109
    /// # Returns
110
    /// The updated codec.
111
    pub fn with_separator(mut self, separator: impl Into<String>) -> Self {
5✔
112
        self.separator = Some(separator.into());
5✔
113
        self
5✔
114
    }
5✔
115

116
    /// Sets whether ASCII whitespace is ignored while decoding.
117
    ///
118
    /// # Parameters
119
    /// - `ignore`: Whether to ignore ASCII whitespace.
120
    ///
121
    /// # Returns
122
    /// The updated codec.
123
    pub fn with_ignored_ascii_whitespace(mut self, ignore: bool) -> Self {
3✔
124
        self.ignore_ascii_whitespace = ignore;
3✔
125
        self
3✔
126
    }
3✔
127

128
    /// Sets whether ASCII case is ignored when decoding configured prefixes.
129
    ///
130
    /// This option affects whole-output prefixes and per-byte prefixes during
131
    /// decoding only. Encoding writes prefixes exactly as configured.
132
    ///
133
    /// # Parameters
134
    /// - `ignore`: Whether to ignore ASCII case while matching prefixes.
135
    ///
136
    /// # Returns
137
    /// The updated codec.
138
    pub fn with_ignore_prefix_case(mut self, ignore: bool) -> Self {
3✔
139
        self.ignore_prefix_case = ignore;
3✔
140
        self
3✔
141
    }
3✔
142

143
    /// Encodes bytes into a hexadecimal string.
144
    ///
145
    /// # Parameters
146
    /// - `bytes`: Bytes to encode.
147
    ///
148
    /// # Returns
149
    /// Hexadecimal text.
150
    pub fn encode(&self, bytes: &[u8]) -> String {
9✔
151
        let separator_len = self.separator.as_ref().map_or(0, String::len);
9✔
152
        let prefix_len = self.prefix.as_ref().map_or(0, String::len);
9✔
153
        let byte_prefix_len = self.byte_prefix.as_ref().map_or(0, String::len);
9✔
154
        let capacity = prefix_len.saturating_add(
9✔
155
            bytes
9✔
156
                .len()
9✔
157
                .saturating_mul(byte_prefix_len.saturating_add(2))
9✔
158
                .saturating_add(bytes.len().saturating_sub(1).saturating_mul(separator_len)),
9✔
159
        );
160
        let mut output = String::with_capacity(capacity);
9✔
161
        self.encode_into(bytes, &mut output);
9✔
162
        output
9✔
163
    }
9✔
164

165
    /// Encodes bytes into an existing string.
166
    ///
167
    /// # Parameters
168
    /// - `bytes`: Bytes to encode.
169
    /// - `output`: Destination string.
170
    pub fn encode_into(&self, bytes: &[u8], output: &mut String) {
10✔
171
        if let Some(prefix) = &self.prefix {
10✔
172
            output.push_str(prefix);
3✔
173
        }
7✔
174
        for (index, byte) in bytes.iter().enumerate() {
43✔
175
            if index > 0
43✔
176
                && let Some(separator) = &self.separator
34✔
177
            {
20✔
178
                output.push_str(separator);
20✔
179
            }
23✔
180
            if let Some(byte_prefix) = &self.byte_prefix {
43✔
181
                output.push_str(byte_prefix);
14✔
182
            }
29✔
183
            push_hex_byte(*byte, self.uppercase, output);
43✔
184
        }
185
    }
10✔
186

187
    /// Decodes hexadecimal text into bytes.
188
    ///
189
    /// # Parameters
190
    /// - `text`: Hexadecimal text.
191
    ///
192
    /// # Returns
193
    /// Decoded bytes.
194
    ///
195
    /// # Errors
196
    /// Returns [`CodecError`] when a configured whole or per-byte prefix is missing,
197
    /// when the normalized digit count is odd, or when a non-hex digit is found.
198
    pub fn decode(&self, text: &str) -> CodecResult<Vec<u8>> {
17✔
199
        let mut output = Vec::new();
17✔
200
        self.decode_into(text, &mut output)?;
17✔
201
        Ok(output)
10✔
202
    }
17✔
203

204
    /// Decodes hexadecimal text into an existing byte vector.
205
    ///
206
    /// # Parameters
207
    /// - `text`: Hexadecimal text.
208
    /// - `output`: Destination byte vector.
209
    ///
210
    /// # Errors
211
    /// Returns [`CodecError`] when the input is malformed.
212
    pub fn decode_into(&self, text: &str, output: &mut Vec<u8>) -> CodecResult<()> {
18✔
213
        let digits = self.normalized_digits(text)?;
18✔
214
        if digits.len() % 2 != 0 {
12✔
215
            return Err(invalid_hex_length(digits.len()));
1✔
216
        }
11✔
217
        output.reserve(digits.len() / 2);
11✔
218
        for pair in digits.chunks_exact(2) {
25✔
219
            let mut pair = pair.iter();
25✔
220
            let Some(&(high_index, high_char)) = pair.next() else {
25✔
UNCOV
221
                continue;
×
222
            };
223
            let Some(&(low_index, low_char)) = pair.next() else {
25✔
UNCOV
224
                continue;
×
225
            };
226
            let high = hex_value(high_char).ok_or(invalid_hex_digit(high_index, high_char))?;
25✔
227
            let low = hex_value(low_char).ok_or(invalid_hex_digit(low_index, low_char))?;
25✔
228
            output.push((high << 4) | low);
25✔
229
        }
230
        Ok(())
11✔
231
    }
18✔
232

233
    /// Normalizes accepted input characters into hex digits.
234
    ///
235
    /// # Parameters
236
    /// - `text`: Text to decode.
237
    ///
238
    /// # Returns
239
    /// Hex digits paired with their original character indexes.
240
    ///
241
    /// # Errors
242
    /// Returns [`CodecError::InvalidDigit`] for unsupported characters.
243
    fn normalized_digits(&self, text: &str) -> CodecResult<Vec<(usize, char)>> {
18✔
244
        let start_index = self.consume_prefix(text)?;
18✔
245
        if let Some(byte_prefix) = self
16✔
246
            .byte_prefix
16✔
247
            .as_deref()
16✔
248
            .filter(|prefix| !prefix.is_empty())
16✔
249
        {
250
            return self.normalized_byte_prefixed_digits(text, byte_prefix, start_index);
5✔
251
        }
11✔
252
        self.normalized_unprefixed_digits(text, start_index)
11✔
253
    }
18✔
254

255
    /// Consumes the configured whole-output prefix.
256
    ///
257
    /// # Parameters
258
    /// - `text`: Text to decode.
259
    ///
260
    /// # Returns
261
    /// Byte index where byte parsing should start.
262
    ///
263
    /// # Errors
264
    /// Returns [`CodecError::MissingPrefix`] when a non-empty whole-output
265
    /// prefix is configured but absent.
266
    fn consume_prefix(&self, text: &str) -> CodecResult<usize> {
18✔
267
        let Some(prefix) = self.prefix.as_deref().filter(|prefix| !prefix.is_empty()) else {
18✔
268
            return Ok(0);
10✔
269
        };
270
        let index = self.skip_ascii_whitespace(text, 0);
8✔
271
        let Some(rest) = text.get(index..) else {
8✔
UNCOV
272
            return Err(CodecError::MissingPrefix {
×
UNCOV
273
                prefix: prefix.to_owned(),
×
UNCOV
274
            });
×
275
        };
276
        if self.starts_with_prefix(rest, prefix) {
8✔
277
            Ok(index + prefix.len())
6✔
278
        } else {
279
            Err(CodecError::MissingPrefix {
2✔
280
                prefix: prefix.to_owned(),
2✔
281
            })
2✔
282
        }
283
    }
18✔
284

285
    /// Normalizes unprefixed input characters into hex digits.
286
    ///
287
    /// # Parameters
288
    /// - `text`: Text to decode.
289
    ///
290
    /// # Returns
291
    /// Hex digits paired with their original character indexes.
292
    ///
293
    /// # Errors
294
    /// Returns [`CodecError::InvalidDigit`] for unsupported characters.
295
    fn normalized_unprefixed_digits(
11✔
296
        &self,
11✔
297
        text: &str,
11✔
298
        mut index: usize,
11✔
299
    ) -> CodecResult<Vec<(usize, char)>> {
11✔
300
        let mut digits = Vec::with_capacity(text.len());
11✔
301
        let separator = self
11✔
302
            .separator
11✔
303
            .as_deref()
11✔
304
            .filter(|separator| !separator.is_empty());
11✔
305
        while index < text.len() {
60✔
306
            let Some(rest) = text.get(index..) else {
51✔
UNCOV
307
                break;
×
308
            };
309
            if let Some(separator) = separator
51✔
310
                && rest.starts_with(separator)
15✔
311
            {
312
                index += separator.len();
3✔
313
                continue;
3✔
314
            }
48✔
315
            let Some(ch) = rest.chars().next() else {
48✔
UNCOV
316
                break;
×
317
            };
318
            if self.ignore_ascii_whitespace && ch.is_ascii_whitespace() {
48✔
319
                index += ch.len_utf8();
2✔
320
                continue;
2✔
321
            }
46✔
322
            if hex_value(ch).is_some() {
46✔
323
                digits.push((index, ch));
44✔
324
                index += ch.len_utf8();
44✔
325
                continue;
44✔
326
            }
2✔
327
            return Err(invalid_hex_digit(index, ch));
2✔
328
        }
329
        Ok(digits)
9✔
330
    }
11✔
331

332
    /// Normalizes byte-prefixed input characters into hex digits.
333
    ///
334
    /// # Parameters
335
    /// - `text`: Text to decode.
336
    /// - `prefix`: Required prefix before each byte.
337
    /// - `index`: Byte index where parsing should start.
338
    ///
339
    /// # Returns
340
    /// Hex digits paired with their original character indexes.
341
    ///
342
    /// # Errors
343
    /// Returns [`CodecError::MissingPrefix`] when a byte prefix is missing, or
344
    /// [`CodecError::InvalidDigit`] for unsupported characters.
345
    fn normalized_byte_prefixed_digits(
5✔
346
        &self,
5✔
347
        text: &str,
5✔
348
        prefix: &str,
5✔
349
        mut index: usize,
5✔
350
    ) -> CodecResult<Vec<(usize, char)>> {
5✔
351
        let mut digits = Vec::with_capacity(text.len());
5✔
352
        let separator = self
5✔
353
            .separator
5✔
354
            .as_deref()
5✔
355
            .filter(|separator| !separator.is_empty());
5✔
356
        while index < text.len() {
12✔
357
            index = self.skip_ignored(text, index, separator);
10✔
358
            if index >= text.len() {
10✔
359
                break;
1✔
360
            }
9✔
361
            let Some(rest) = text.get(index..) else {
9✔
UNCOV
362
                break;
×
363
            };
364
            if !self.starts_with_prefix(rest, prefix) {
9✔
365
                return Err(CodecError::MissingPrefix {
1✔
366
                    prefix: prefix.to_owned(),
1✔
367
                });
1✔
368
            }
8✔
369
            index += prefix.len();
8✔
370

371
            let mut digit_count = 0;
8✔
372
            while digit_count < 2 && index < text.len() {
25✔
373
                let Some(rest) = text.get(index..) else {
18✔
UNCOV
374
                    break;
×
375
                };
376
                let Some(ch) = rest.chars().next() else {
18✔
UNCOV
377
                    break;
×
378
                };
379
                if self.ignore_ascii_whitespace && ch.is_ascii_whitespace() {
18✔
380
                    index += ch.len_utf8();
2✔
381
                    continue;
2✔
382
                }
16✔
383
                if hex_value(ch).is_some() {
16✔
384
                    digits.push((index, ch));
15✔
385
                    index += ch.len_utf8();
15✔
386
                    digit_count += 1;
15✔
387
                    continue;
15✔
388
                }
1✔
389
                return Err(invalid_hex_digit(index, ch));
1✔
390
            }
391
        }
392
        Ok(digits)
3✔
393
    }
5✔
394

395
    /// Skips configured separators and ignored ASCII whitespace.
396
    ///
397
    /// # Parameters
398
    /// - `text`: Text being decoded.
399
    /// - `index`: Current byte index.
400
    /// - `separator`: Optional configured separator.
401
    ///
402
    /// # Returns
403
    /// The next byte index that should be parsed.
404
    fn skip_ignored(&self, text: &str, mut index: usize, separator: Option<&str>) -> usize {
10✔
405
        loop {
406
            let Some(rest) = text.get(index..) else {
14✔
UNCOV
407
                return index;
×
408
            };
409
            if let Some(separator) = separator
14✔
410
                && rest.starts_with(separator)
7✔
411
            {
412
                index += separator.len();
3✔
413
                continue;
3✔
414
            }
11✔
415
            let Some(ch) = rest.chars().next() else {
11✔
416
                return index;
1✔
417
            };
418
            if self.ignore_ascii_whitespace && ch.is_ascii_whitespace() {
10✔
419
                index += ch.len_utf8();
1✔
420
                continue;
1✔
421
            }
9✔
422
            return index;
9✔
423
        }
424
    }
10✔
425

426
    /// Skips ignored leading ASCII whitespace.
427
    ///
428
    /// # Parameters
429
    /// - `text`: Text being decoded.
430
    /// - `index`: Current byte index.
431
    ///
432
    /// # Returns
433
    /// The next byte index after ignored ASCII whitespace.
434
    fn skip_ascii_whitespace(&self, text: &str, mut index: usize) -> usize {
8✔
435
        while self.ignore_ascii_whitespace && index < text.len() {
10✔
436
            let Some(rest) = text.get(index..) else {
3✔
UNCOV
437
                return index;
×
438
            };
439
            let Some(ch) = rest.chars().next() else {
3✔
UNCOV
440
                return index;
×
441
            };
442
            if !ch.is_ascii_whitespace() {
3✔
443
                return index;
1✔
444
            }
2✔
445
            index += ch.len_utf8();
2✔
446
        }
447
        index
7✔
448
    }
8✔
449

450
    /// Tests whether `text` starts with a configured prefix.
451
    ///
452
    /// # Parameters
453
    /// - `text`: Text slice to inspect.
454
    /// - `prefix`: Configured prefix.
455
    ///
456
    /// # Returns
457
    /// `true` when `text` starts with `prefix`, honoring the configured
458
    /// ASCII case sensitivity for decoding prefixes.
459
    fn starts_with_prefix(&self, text: &str, prefix: &str) -> bool {
17✔
460
        if !self.ignore_prefix_case {
17✔
461
            return text.starts_with(prefix);
13✔
462
        }
4✔
463
        let Some(candidate) = text.get(..prefix.len()) else {
4✔
464
            return false;
1✔
465
        };
466
        candidate.eq_ignore_ascii_case(prefix)
3✔
467
    }
17✔
468
}
469

470
impl Default for HexCodec {
471
    /// Creates a lowercase codec without prefix or separators.
472
    fn default() -> Self {
1✔
473
        Self::new()
1✔
474
    }
1✔
475
}
476

477
impl Encoder<[u8]> for HexCodec {
478
    type Error = CodecError;
479
    type Output = String;
480

481
    /// Encodes bytes into hexadecimal text.
482
    fn encode(&self, input: &[u8]) -> Result<Self::Output, Self::Error> {
1✔
483
        Ok(HexCodec::encode(self, input))
1✔
484
    }
1✔
485
}
486

487
impl Decoder<str> for HexCodec {
488
    type Error = CodecError;
489
    type Output = Vec<u8>;
490

491
    /// Decodes hexadecimal text into bytes.
492
    fn decode(&self, input: &str) -> Result<Self::Output, Self::Error> {
1✔
493
        HexCodec::decode(self, input)
1✔
494
    }
1✔
495
}
496

497
/// Converts one hex digit to its value.
498
///
499
/// # Parameters
500
/// - `ch`: Character to inspect.
501
///
502
/// # Returns
503
/// Nibble value, or `None` when `ch` is not a hex digit.
504
fn hex_value(ch: char) -> Option<u8> {
112✔
505
    match ch {
112✔
506
        '0'..='9' => Some(ch as u8 - b'0'),
112✔
507
        'a'..='f' => Some(ch as u8 - b'a' + 10),
45✔
508
        'A'..='F' => Some(ch as u8 - b'A' + 10),
11✔
509
        _ => None,
3✔
510
    }
511
}
112✔
512

513
/// Builds an invalid hexadecimal digit error.
514
///
515
/// # Parameters
516
/// - `index`: Byte index of the invalid character in the original input.
517
/// - `character`: Invalid character.
518
///
519
/// # Returns
520
/// A radix-16 digit error.
521
fn invalid_hex_digit(index: usize, character: char) -> CodecError {
53✔
522
    CodecError::InvalidDigit {
53✔
523
        radix: 16,
53✔
524
        index,
53✔
525
        character,
53✔
526
    }
53✔
527
}
53✔
528

529
/// Builds an invalid hexadecimal length error.
530
///
531
/// # Parameters
532
/// - `actual`: Number of normalized hexadecimal digits.
533
///
534
/// # Returns
535
/// An invalid length error describing the even-digit requirement.
536
fn invalid_hex_length(actual: usize) -> CodecError {
1✔
537
    CodecError::InvalidLength {
1✔
538
        context: "hex digits",
1✔
539
        expected: "an even number of digits".to_owned(),
1✔
540
        actual,
1✔
541
    }
1✔
542
}
1✔
543

544
/// Appends one encoded byte to `output`.
545
///
546
/// # Parameters
547
/// - `byte`: Byte to encode.
548
/// - `uppercase`: Whether to use uppercase digits.
549
/// - `output`: Destination string.
550
fn push_hex_byte(byte: u8, uppercase: bool, output: &mut String) {
43✔
551
    output.push(hex_digit(byte >> 4, uppercase));
43✔
552
    output.push(hex_digit(byte & 0x0f, uppercase));
43✔
553
}
43✔
554

555
/// Converts one nibble to a hexadecimal digit.
556
///
557
/// # Parameters
558
/// - `value`: Nibble value.
559
/// - `uppercase`: Whether to use uppercase digits.
560
///
561
/// # Returns
562
/// Hexadecimal digit. Values above `0x0f` are masked to their low nibble.
563
fn hex_digit(value: u8, uppercase: bool) -> char {
86✔
564
    match value & 0x0f {
86✔
565
        0x0 => '0',
9✔
566
        0x1 => '1',
8✔
567
        0x2 => '2',
4✔
568
        0x3 => '3',
4✔
569
        0x4 => '4',
3✔
570
        0x5 => '5',
3✔
571
        0x6 => '6',
6✔
572
        0x7 => '7',
3✔
573
        0x8 => '8',
7✔
574
        0x9 => '9',
3✔
575
        0x0a if uppercase => 'A',
2✔
576
        0x0b if uppercase => 'B',
4✔
577
        0x0c if uppercase => 'C',
2✔
578
        0x0d if uppercase => 'D',
2✔
579
        0x0e if uppercase => 'E',
2✔
580
        0x0f if uppercase => 'F',
8✔
581
        0x0a => 'a',
2✔
582
        0x0b => 'b',
4✔
583
        0x0c => 'c',
2✔
584
        0x0d => 'd',
2✔
585
        0x0e => 'e',
1✔
586
        _ => 'f',
5✔
587
    }
588
}
86✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc