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

qubit-ltd / rs-codec / d1925943-91ae-4f1b-af26-abe80199e416

04 May 2026 11:45AM UTC coverage: 96.956% (-0.9%) from 97.844%
d1925943-91ae-4f1b-af26-abe80199e416

push

circleci

Haixing-Hu
docs: update HexCodec prefix documentation

Document the 0.2.0 HexCodec prefix API, byte prefix API, and prefix case matching option in English and Chinese READMEs.

414 of 427 relevant lines covered (96.96%)

12.16 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(CodecError::OddHexLength {
1✔
216
                digits: digits.len(),
1✔
217
            });
1✔
218
        }
11✔
219
        output.reserve(digits.len() / 2);
11✔
220
        for pair in digits.chunks_exact(2) {
25✔
221
            let mut pair = pair.iter();
25✔
222
            let Some(&(high_index, high_char)) = pair.next() else {
25✔
223
                continue;
×
224
            };
225
            let Some(&(low_index, low_char)) = pair.next() else {
25✔
226
                continue;
×
227
            };
228
            let high = hex_value(high_char).ok_or(CodecError::InvalidHexDigit {
25✔
229
                index: high_index,
25✔
230
                character: high_char,
25✔
231
            })?;
25✔
232
            let low = hex_value(low_char).ok_or(CodecError::InvalidHexDigit {
25✔
233
                index: low_index,
25✔
234
                character: low_char,
25✔
235
            })?;
25✔
236
            output.push((high << 4) | low);
25✔
237
        }
238
        Ok(())
11✔
239
    }
18✔
240

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

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

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

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

382
            let mut digit_count = 0;
8✔
383
            while digit_count < 2 && index < text.len() {
25✔
384
                let Some(rest) = text.get(index..) else {
18✔
385
                    break;
×
386
                };
387
                let Some(ch) = rest.chars().next() else {
18✔
388
                    break;
×
389
                };
390
                if self.ignore_ascii_whitespace && ch.is_ascii_whitespace() {
18✔
391
                    index += ch.len_utf8();
2✔
392
                    continue;
2✔
393
                }
16✔
394
                if hex_value(ch).is_some() {
16✔
395
                    digits.push((index, ch));
15✔
396
                    index += ch.len_utf8();
15✔
397
                    digit_count += 1;
15✔
398
                    continue;
15✔
399
                }
1✔
400
                return Err(CodecError::InvalidHexDigit {
1✔
401
                    index,
1✔
402
                    character: ch,
1✔
403
                });
1✔
404
            }
405
        }
406
        Ok(digits)
3✔
407
    }
5✔
408

409
    /// Skips configured separators and ignored ASCII whitespace.
410
    ///
411
    /// # Parameters
412
    /// - `text`: Text being decoded.
413
    /// - `index`: Current byte index.
414
    /// - `separator`: Optional configured separator.
415
    ///
416
    /// # Returns
417
    /// The next byte index that should be parsed.
418
    fn skip_ignored(&self, text: &str, mut index: usize, separator: Option<&str>) -> usize {
10✔
419
        loop {
420
            let Some(rest) = text.get(index..) else {
14✔
421
                return index;
×
422
            };
423
            if let Some(separator) = separator
14✔
424
                && rest.starts_with(separator)
7✔
425
            {
426
                index += separator.len();
3✔
427
                continue;
3✔
428
            }
11✔
429
            let Some(ch) = rest.chars().next() else {
11✔
430
                return index;
1✔
431
            };
432
            if self.ignore_ascii_whitespace && ch.is_ascii_whitespace() {
10✔
433
                index += ch.len_utf8();
1✔
434
                continue;
1✔
435
            }
9✔
436
            return index;
9✔
437
        }
438
    }
10✔
439

440
    /// Skips ignored leading ASCII whitespace.
441
    ///
442
    /// # Parameters
443
    /// - `text`: Text being decoded.
444
    /// - `index`: Current byte index.
445
    ///
446
    /// # Returns
447
    /// The next byte index after ignored ASCII whitespace.
448
    fn skip_ascii_whitespace(&self, text: &str, mut index: usize) -> usize {
8✔
449
        while self.ignore_ascii_whitespace && index < text.len() {
10✔
450
            let Some(rest) = text.get(index..) else {
3✔
451
                return index;
×
452
            };
453
            let Some(ch) = rest.chars().next() else {
3✔
454
                return index;
×
455
            };
456
            if !ch.is_ascii_whitespace() {
3✔
457
                return index;
1✔
458
            }
2✔
459
            index += ch.len_utf8();
2✔
460
        }
461
        index
7✔
462
    }
8✔
463

464
    /// Tests whether `text` starts with a configured prefix.
465
    ///
466
    /// # Parameters
467
    /// - `text`: Text slice to inspect.
468
    /// - `prefix`: Configured prefix.
469
    ///
470
    /// # Returns
471
    /// `true` when `text` starts with `prefix`, honoring the configured
472
    /// ASCII case sensitivity for decoding prefixes.
473
    fn starts_with_prefix(&self, text: &str, prefix: &str) -> bool {
17✔
474
        if !self.ignore_prefix_case {
17✔
475
            return text.starts_with(prefix);
13✔
476
        }
4✔
477
        let Some(candidate) = text.get(..prefix.len()) else {
4✔
478
            return false;
1✔
479
        };
480
        candidate.eq_ignore_ascii_case(prefix)
3✔
481
    }
17✔
482
}
483

484
impl Default for HexCodec {
485
    /// Creates a lowercase codec without prefix or separators.
486
    fn default() -> Self {
1✔
487
        Self::new()
1✔
488
    }
1✔
489
}
490

491
impl Encoder<[u8]> for HexCodec {
492
    type Error = CodecError;
493
    type Output = String;
494

495
    /// Encodes bytes into hexadecimal text.
496
    fn encode(&self, input: &[u8]) -> Result<Self::Output, Self::Error> {
1✔
497
        Ok(HexCodec::encode(self, input))
1✔
498
    }
1✔
499
}
500

501
impl Decoder<str> for HexCodec {
502
    type Error = CodecError;
503
    type Output = Vec<u8>;
504

505
    /// Decodes hexadecimal text into bytes.
506
    fn decode(&self, input: &str) -> Result<Self::Output, Self::Error> {
1✔
507
        HexCodec::decode(self, input)
1✔
508
    }
1✔
509
}
510

511
/// Converts one hex digit to its value.
512
///
513
/// # Parameters
514
/// - `ch`: Character to inspect.
515
///
516
/// # Returns
517
/// Nibble value, or `None` when `ch` is not a hex digit.
518
fn hex_value(ch: char) -> Option<u8> {
112✔
519
    match ch {
112✔
520
        '0'..='9' => Some(ch as u8 - b'0'),
112✔
521
        'a'..='f' => Some(ch as u8 - b'a' + 10),
45✔
522
        'A'..='F' => Some(ch as u8 - b'A' + 10),
11✔
523
        _ => None,
3✔
524
    }
525
}
112✔
526

527
/// Appends one encoded byte to `output`.
528
///
529
/// # Parameters
530
/// - `byte`: Byte to encode.
531
/// - `uppercase`: Whether to use uppercase digits.
532
/// - `output`: Destination string.
533
fn push_hex_byte(byte: u8, uppercase: bool, output: &mut String) {
43✔
534
    output.push(hex_digit(byte >> 4, uppercase));
43✔
535
    output.push(hex_digit(byte & 0x0f, uppercase));
43✔
536
}
43✔
537

538
/// Converts one nibble to a hexadecimal digit.
539
///
540
/// # Parameters
541
/// - `value`: Nibble value.
542
/// - `uppercase`: Whether to use uppercase digits.
543
///
544
/// # Returns
545
/// Hexadecimal digit. Values above `0x0f` are masked to their low nibble.
546
fn hex_digit(value: u8, uppercase: bool) -> char {
86✔
547
    match value & 0x0f {
86✔
548
        0x0 => '0',
9✔
549
        0x1 => '1',
8✔
550
        0x2 => '2',
4✔
551
        0x3 => '3',
4✔
552
        0x4 => '4',
3✔
553
        0x5 => '5',
3✔
554
        0x6 => '6',
6✔
555
        0x7 => '7',
3✔
556
        0x8 => '8',
7✔
557
        0x9 => '9',
3✔
558
        0x0a if uppercase => 'A',
2✔
559
        0x0b if uppercase => 'B',
4✔
560
        0x0c if uppercase => 'C',
2✔
561
        0x0d if uppercase => 'D',
2✔
562
        0x0e if uppercase => 'E',
2✔
563
        0x0f if uppercase => 'F',
8✔
564
        0x0a => 'a',
2✔
565
        0x0b => 'b',
4✔
566
        0x0c => 'c',
2✔
567
        0x0d => 'd',
2✔
568
        0x0e => 'e',
1✔
569
        _ => 'f',
5✔
570
    }
571
}
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