Skip to main content

stuffr_cli/compat/bzip2recover/
mod.rs

1//! `bzip2recover`: stuffr behaving as bzip2recover 1.0.8.
2//!
3//! Ported from bzip2 1.0.8's `bzip2recover.c`, copyright (C) 1996-2019
4//! Julian Seward, under the bzip2 licence reproduced in `LICENSE-bzip2`.
5//! `main` is followed line by line, quirks included:
6//!
7//! - two passes over the input: the first finds every 48-bit block and
8//!   end-of-stream magic and records the blocks between them; the second
9//!   copies each recorded block's bits into its own stream;
10//! - a "block" is whatever lies between two magics and is recorded when it
11//!   spans at least 130 bits, measured in unsigned 64-bit arithmetic, so a
12//!   magic that follows another within 49 bits wraps round and is recorded
13//!   with its end before its start (the second pass then writes nothing for
14//!   it);
15//! - a block the input ends inside is reported as `(incomplete)` but never
16//!   written;
17//! - each output is `BZh9`, the block magic, the block's bits, the
18//!   end-of-stream magic and, as the combined CRC, the block's own stored
19//!   CRC: nothing is decompressed, recompressed or checked;
20//! - outputs are `rec%05d` and the input's base name, split at the last
21//!   `/`, with `.bz2` appended unless the result already ends in it, opened
22//!   as `fopen(.., "wb")` does (created mode 0666 less the umask, or an
23//!   existing file truncated). So, as upstream does, an output is written
24//!   through a symlink already at its name and truncates a file already
25//!   there; Debian's `bzip2recover-race-open-output.diff` refuses both;
26//! - the limits: `BZ_MAX_FILENAME` (an operand of 1980 bytes or more is
27//!   refused) and `BZ_MAX_HANDLED_BLOCKS` (50000), with their messages; every
28//!   failure exits 1.
29//!
30//! Memory is bounded by the block cap: the input is streamed twice, and only
31//! the recorded blocks' bit positions (at most 50000 pairs) are kept.
32//!
33//! Known deviations from bzip2recover 1.0.8:
34//!
35//! - At exactly 50000 magics (for example 49999 blocks plus the
36//!   end-of-stream marker) with at least 40 bits after the last, 1.0.8
37//!   writes past its `bEnd` array and, on macOS, silently drops block 1;
38//!   stuffr writes it, as if the arrays were 50001 long. Debian's gcc build
39//!   of the same source instead stops at the 50000th magic ("more than
40//!   50000 blocks", exit 1); stuffr, like upstream, stops at the 50001st.
41//! - Like bzip2, a read error is reported with its `strerror` text; a stale
42//!   `errno` that the C library left nonzero at a clean end of file (which
43//!   would make 1.0.8 report a read error) is not reproduced.
44//! - `mallocFail` cannot happen: nothing is allocated per bit or per block
45//!   beyond the bounded position list.
46
47use std::ffi::{OsStr, OsString};
48use std::fs::File;
49use std::io::{self, Read, Write};
50use std::path::{Path, PathBuf};
51use std::process::ExitCode;
52
53mod bits;
54mod messages;
55
56use bits::{BitIn, BitOut};
57use messages as m;
58
59/// `BZ_MAX_FILENAME`.
60const MAX_FILENAME: usize = 2000;
61/// `BZ_MAX_HANDLED_BLOCKS`.
62const MAX_HANDLED_BLOCKS: i64 = 50_000;
63/// `BLOCK_HEADER_HI`/`_LO`: the block magic, `0x314159265359`.
64const BLOCK_MAGIC: u64 = 0x3141_5926_5359;
65/// `BLOCK_ENDMARK_HI`/`_LO`: the end-of-stream magic, `0x177245385090`.
66const END_MAGIC: u64 = 0x1772_4538_5090;
67/// `BZ_SPLIT_SYM`.
68const SPLIT: u8 = if cfg!(windows) { b'\\' } else { b'/' };
69
70/// Entry point: `argv0` is the program path as invoked (bzip2recover prints
71/// it in full), `args` the arguments after it.
72pub fn run(argv0: OsString, args: Vec<OsString>) -> ExitCode {
73    // bzip2recover dies of SIGPIPE when stderr is a closed pipe; Rust
74    // ignores the signal by default, so restore its default action.
75    super::host::reset_sigpipe();
76    let args: Vec<Vec<u8>> = args.iter().map(|a| bytes(a)).collect();
77    let code = recover(&bytes(&argv0), &args, &mut RealFs, &mut io::stderr());
78    ExitCode::from(code)
79}
80
81/// [`run`] without the process: names resolve against `dir`, and standard
82/// error is captured and returned with the exit code (bzip2recover writes
83/// nothing to standard output). `SIGPIPE` is left alone. For the fuzz
84/// target; not a stable interface.
85#[doc(hidden)]
86pub fn run_in(dir: &Path, argv0: &[u8], args: &[OsString]) -> (i32, Vec<u8>) {
87    let args: Vec<Vec<u8>> = args.iter().map(|a| bytes(a)).collect();
88    let mut err = Vec::new();
89    let mut fs = DirFs(dir.to_path_buf());
90    let code = recover(argv0, &args, &mut fs, &mut err);
91    (i32::from(code), err)
92}
93
94/// Where the input is read from and the outputs are written to, so the
95/// scan can be run (and its reads counted) without a file system. Public
96/// only for the fuzz target, which counts them; not a stable interface.
97#[doc(hidden)]
98pub trait Fs {
99    /// `fopen(name, "rb")`.
100    fn open(&mut self, name: &[u8]) -> io::Result<Box<dyn Read>>;
101    /// `fopen(name, "wb")`.
102    fn create(&mut self, name: &[u8]) -> io::Result<Box<dyn Write>>;
103}
104
105/// The real file system.
106pub(crate) struct RealFs;
107
108impl Fs for RealFs {
109    fn open(&mut self, name: &[u8]) -> io::Result<Box<dyn Read>> {
110        Ok(Box::new(File::open(path(name))?))
111    }
112    fn create(&mut self, name: &[u8]) -> io::Result<Box<dyn Write>> {
113        create(&path(name))
114    }
115}
116
117/// The real file system, with names resolved against a directory.
118struct DirFs(PathBuf);
119
120impl Fs for DirFs {
121    fn open(&mut self, name: &[u8]) -> io::Result<Box<dyn Read>> {
122        Ok(Box::new(File::open(self.0.join(path(name)))?))
123    }
124    fn create(&mut self, name: &[u8]) -> io::Result<Box<dyn Write>> {
125        create(&self.0.join(path(name)))
126    }
127}
128
129/// `fopen(.., "wb")`: O_WRONLY|O_CREAT|O_TRUNC, mode 0666 less umask.
130fn create(p: &Path) -> io::Result<Box<dyn Write>> {
131    let f = File::options()
132        .write(true)
133        .create(true)
134        .truncate(true)
135        .open(p)?;
136    Ok(Box::new(f))
137}
138
139fn bytes(s: &OsStr) -> Vec<u8> {
140    #[cfg(unix)]
141    {
142        use std::os::unix::ffi::OsStrExt;
143        s.as_bytes().to_vec()
144    }
145    #[cfg(not(unix))]
146    {
147        s.to_string_lossy().into_owned().into_bytes()
148    }
149}
150
151fn path(b: &[u8]) -> PathBuf {
152    #[cfg(unix)]
153    {
154        use std::os::unix::ffi::OsStrExt;
155        PathBuf::from(OsStr::from_bytes(b))
156    }
157    #[cfg(not(unix))]
158    {
159        PathBuf::from(String::from_utf8_lossy(b).into_owned())
160    }
161}
162
163/// C's `strerror`: the OS text without Rust's ` (os error N)` suffix.
164fn strerror(e: &io::Error) -> Vec<u8> {
165    let s = e.to_string();
166    match e.raw_os_error() {
167        Some(n) => {
168            let suffix = format!(" (os error {n})");
169            s.strip_suffix(&suffix).unwrap_or(&s).as_bytes().to_vec()
170        }
171        None => s.into_bytes(),
172    }
173}
174
175/// The run stopped with this exit code; its message is already printed.
176struct Exit(u8);
177
178/// One run of bzip2recover's state over a pair of streams.
179struct Recover<'a> {
180    prog: &'a [u8],
181    in_name: &'a [u8],
182    err: &'a mut dyn Write,
183}
184
185impl Recover<'_> {
186    fn say(&mut self, fmt: &str, args: &[&[u8]]) {
187        m::printf(self.err, fmt, args);
188    }
189
190    /// `readError` and `writeError`, which print the same text.
191    fn io_error(&mut self, e: &io::Error) -> Exit {
192        let (p, n) = (self.prog, self.in_name);
193        self.say(m::IO_ERROR, &[p, n]);
194        self.say(m::PERROR, &[p, &strerror(e)]);
195        self.say(m::MAY_BE_INCOMPLETE, &[p]);
196        Exit(1)
197    }
198
199    fn bit(&mut self, input: &mut BitIn) -> Result<Option<u32>, Exit> {
200        input.bit().map_err(|e| self.io_error(&e))
201    }
202}
203
204/// The blocks the first pass recorded (`rbStart`/`rbEnd`, inclusive bit
205/// positions) and its shift register, which the second pass continues.
206struct Found {
207    blocks: Vec<(u64, u64)>,
208    hi: u32,
209    lo: u32,
210}
211
212/// Shift `b` into the 64-bit window and report whether its low 48 bits
213/// are now a block or end-of-stream magic.
214fn shift(hi: &mut u32, lo: &mut u32, b: u32) -> bool {
215    *hi = (*hi << 1) | (*lo >> 31);
216    *lo = (*lo << 1) | (b & 1);
217    let w = (u64::from(*hi & 0xffff) << 32) | u64::from(*lo);
218    w == BLOCK_MAGIC || w == END_MAGIC
219}
220
221/// bzip2recover's `main`, from the argument checks to the end. `prog` is
222/// `argv[0]`, `args` the rest. Returns the exit code. Public only for the
223/// fuzz target; not a stable interface.
224#[doc(hidden)]
225pub fn recover(prog: &[u8], args: &[Vec<u8>], fs: &mut dyn Fs, err: &mut dyn Write) -> u8 {
226    // strncpy into progName[BZ_MAX_FILENAME], then terminated.
227    let prog = &prog[..prog.len().min(MAX_FILENAME - 1)];
228    m::printf(err, m::BANNER, &[]);
229    let [in_name] = args else {
230        m::printf(err, m::USAGE, &[prog, prog]);
231        m::printf(err, m::USAGE_SIZE, &[]);
232        return 1;
233    };
234    if in_name.len() >= MAX_FILENAME - 20 {
235        let n = in_name.len().to_string();
236        m::printf(err, m::NAME_TOO_LONG, &[prog, n.as_bytes()]);
237        return 1;
238    }
239    let mut r = Recover { prog, in_name, err };
240    match r.both_passes(fs) {
241        Ok(()) => 0,
242        Err(Exit(c)) => c,
243    }
244}
245
246impl Recover<'_> {
247    fn both_passes(&mut self, fs: &mut dyn Fs) -> Result<(), Exit> {
248        let (p, n) = (self.prog, self.in_name);
249        let Ok(input) = fs.open(n) else {
250            self.say(m::CANT_READ, &[p, n]);
251            return Err(Exit(1));
252        };
253        self.say(m::SEARCHING, &[p]);
254        let found = self.find_blocks(&mut BitIn::new(input))?;
255        if found.blocks.is_empty() {
256            self.say(m::NO_BLOCKS, &[p]);
257            return Err(Exit(1));
258        }
259        self.say(m::SPLITTING, &[p]);
260        let Ok(input) = fs.open(n) else {
261            self.say(m::CANT_OPEN, &[p, n]);
262            return Err(Exit(1));
263        };
264        self.split(&mut BitIn::new(input), found, fs)?;
265        self.say(m::FINISHED, &[p]);
266        Ok(())
267    }
268
269    /// The first pass: every magic ends the current block and starts the
270    /// next. `bitsRead` counts bits including the one just read, so a
271    /// block's start is the position of the bit after its magic and its end
272    /// the position of the bit before the next.
273    fn find_blocks(&mut self, input: &mut BitIn) -> Result<Found, Exit> {
274        let (mut hi, mut lo) = (0u32, 0u32);
275        let mut bits_read: u64 = 0;
276        let mut curr_block: i64 = 0;
277        let mut b_start: u64 = 0; // bStart[currBlock]
278        let mut blocks: Vec<(u64, u64)> = Vec::new();
279        loop {
280            let b = self.bit(input)?;
281            bits_read = bits_read.wrapping_add(1);
282            let Some(b) = b else {
283                if bits_read >= b_start && bits_read - b_start >= 40 {
284                    let b_end = bits_read - 1;
285                    if curr_block > 0 {
286                        let c = curr_block.to_string();
287                        self.say(
288                            m::BLOCK_INCOMPLETE,
289                            &[c.as_bytes(), num(b_start).as_bytes(), num(b_end).as_bytes()],
290                        );
291                    }
292                }
293                break;
294            };
295            if shift(&mut hi, &mut lo, b) {
296                let b_end = bits_read.saturating_sub(49);
297                if curr_block > 0 && b_end.wrapping_sub(b_start) >= 130 {
298                    let c = (blocks.len() + 1).to_string();
299                    self.say(
300                        m::BLOCK,
301                        &[c.as_bytes(), num(b_start).as_bytes(), num(b_end).as_bytes()],
302                    );
303                    blocks.push((b_start, b_end));
304                }
305                if curr_block >= MAX_HANDLED_BLOCKS {
306                    let (p, n) = (self.prog, self.in_name);
307                    let max = MAX_HANDLED_BLOCKS.to_string();
308                    self.say(m::TOO_MANY_BLOCKS, &[p, n, max.as_bytes(), p, p]);
309                    return Err(Exit(1));
310                }
311                curr_block += 1;
312                b_start = bits_read;
313            }
314        }
315        Ok(Found { blocks, hi, lo })
316    }
317
318    /// The second pass: copy each recorded block's bits into its own
319    /// stream. `bitsRead` here is the position of the bit just read, and is
320    /// advanced after it is handled. Past the last block the position list
321    /// reads as zero (`rbStart`/`rbEnd` are zero-initialised globals), and
322    /// the input is read to its end.
323    fn split(&mut self, input: &mut BitIn, found: Found, fs: &mut dyn Fs) -> Result<(), Exit> {
324        let Found {
325            blocks,
326            mut hi,
327            mut lo,
328        } = found;
329        let at = |i: usize| blocks.get(i).copied().unwrap_or((0, 0));
330        let mut bits_read: u64 = 0;
331        let mut wr_block: usize = 0;
332        let mut block_crc: u32 = 0;
333        let mut out: Option<BitOut> = None;
334        while let Some(b) = self.bit(input)? {
335            shift(&mut hi, &mut lo, b);
336            let (start, end) = at(wr_block);
337            if bits_read == start.wrapping_add(47) {
338                block_crc = (hi << 16) | (lo >> 16);
339            }
340            if let Some(w) = out.as_mut()
341                && bits_read >= start
342                && bits_read <= end
343            {
344                w.put_bit(b).map_err(|e| self.io_error(&e))?;
345            }
346            bits_read = bits_read.wrapping_add(1);
347            if bits_read == end.wrapping_add(1) {
348                if let Some(mut w) = out.take() {
349                    let r: io::Result<()> = (|| {
350                        for c in [0x17, 0x72, 0x45, 0x38, 0x50, 0x90] {
351                            w.put_u8(c)?;
352                        }
353                        w.put_u32(block_crc)?;
354                        w.close()
355                    })();
356                    r.map_err(|e| self.io_error(&e))?;
357                }
358                if wr_block >= blocks.len() {
359                    break;
360                }
361                wr_block += 1;
362            } else if bits_read == start {
363                let name = out_name(self.in_name, wr_block + 1);
364                let k = (wr_block + 1).to_string();
365                self.say(m::WRITING, &[k.as_bytes(), &name]);
366                let Ok(f) = fs.create(&name) else {
367                    let p = self.prog;
368                    self.say(m::CANT_WRITE, &[p, &name]);
369                    return Err(Exit(1));
370                };
371                let mut w = BitOut::new(f);
372                let r: io::Result<()> = (|| {
373                    for c in *b"BZh9" {
374                        w.put_u8(c)?;
375                    }
376                    for c in [0x31, 0x41, 0x59, 0x26, 0x53, 0x59] {
377                        w.put_u8(c)?;
378                    }
379                    Ok(())
380                })();
381                r.map_err(|e| self.io_error(&e))?;
382                out = Some(w);
383            }
384        }
385        Ok(())
386    }
387}
388
389/// `MaybeUInt64_FMT`.
390fn num(n: u64) -> String {
391    n.to_string()
392}
393
394/// The output name for block `k` (1-based): the input's directory part,
395/// `rec%5d` with its spaces made zeros, the input's base name, and `.bz2`
396/// unless that already ends the name (`endsInBz2`, which wants more than
397/// four bytes).
398fn out_name(in_name: &[u8], k: usize) -> Vec<u8> {
399    let ofs = in_name
400        .iter()
401        .rposition(|&c| c == SPLIT)
402        .map_or(0, |i| i + 1);
403    let mut v = in_name[..ofs].to_vec();
404    v.extend_from_slice(format!("rec{k:5}").replace(' ', "0").as_bytes());
405    v.extend_from_slice(&in_name[ofs..]);
406    if !(v.len() > 4 && v.ends_with(b".bz2")) {
407        v.extend_from_slice(b".bz2");
408    }
409    v
410}
411
412#[cfg(test)]
413mod tests {
414    use super::*;
415    use std::cell::{Cell, RefCell};
416    use std::collections::BTreeMap;
417    use std::rc::Rc;
418
419    /// An in-memory file system that counts every byte read from it.
420    #[derive(Default)]
421    struct MemFs {
422        files: BTreeMap<Vec<u8>, Rc<RefCell<Vec<u8>>>>,
423        read: Rc<Cell<u64>>,
424        opens: usize,
425    }
426
427    struct Counting {
428        data: Rc<RefCell<Vec<u8>>>,
429        pos: usize,
430        read: Rc<Cell<u64>>,
431    }
432    impl Read for Counting {
433        fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
434            let d = self.data.borrow();
435            let n = buf.len().min(d.len() - self.pos);
436            buf[..n].copy_from_slice(&d[self.pos..self.pos + n]);
437            self.pos += n;
438            self.read.set(self.read.get() + n as u64);
439            Ok(n)
440        }
441    }
442
443    struct Shared(Rc<RefCell<Vec<u8>>>);
444    impl Write for Shared {
445        fn write(&mut self, b: &[u8]) -> io::Result<usize> {
446            self.0.borrow_mut().extend_from_slice(b);
447            Ok(b.len())
448        }
449        fn flush(&mut self) -> io::Result<()> {
450            Ok(())
451        }
452    }
453
454    impl Fs for MemFs {
455        fn open(&mut self, name: &[u8]) -> io::Result<Box<dyn Read>> {
456            self.opens += 1;
457            let data = self
458                .files
459                .get(name)
460                .cloned()
461                .ok_or_else(|| io::Error::from(io::ErrorKind::NotFound))?;
462            Ok(Box::new(Counting {
463                data,
464                pos: 0,
465                read: self.read.clone(),
466            }))
467        }
468        fn create(&mut self, name: &[u8]) -> io::Result<Box<dyn Write>> {
469            let v = Rc::new(RefCell::new(Vec::new()));
470            self.files.insert(name.to_vec(), v.clone());
471            Ok(Box::new(Shared(v)))
472        }
473    }
474
475    /// `BZh9`, then `n` blocks of a magic, a CRC and some bytes, then the
476    /// end-of-stream magic and a combined CRC.
477    fn synthetic(n: usize, body: usize) -> Vec<u8> {
478        let mut v = b"BZh9".to_vec();
479        for i in 0..n {
480            v.extend_from_slice(&[0x31, 0x41, 0x59, 0x26, 0x53, 0x59]);
481            v.extend_from_slice(&(i as u32).wrapping_mul(0x9e37_79b9).to_be_bytes());
482            v.extend((0..body).map(|k| (k * 7 + i) as u8 | 0x80));
483        }
484        v.extend_from_slice(&[0x17, 0x72, 0x45, 0x38, 0x50, 0x90, 1, 2, 3, 4]);
485        v
486    }
487
488    fn run_mem(data: Vec<u8>) -> (u8, MemFs, String) {
489        let mut fs = MemFs::default();
490        fs.files
491            .insert(b"d.bz2".to_vec(), Rc::new(RefCell::new(data)));
492        let mut err = Vec::new();
493        let code = recover(b"bzip2recover", &[b"d.bz2".to_vec()], &mut fs, &mut err);
494        (code, fs, String::from_utf8(err).unwrap())
495    }
496
497    /// The read-amplification bound: the input is read exactly twice,
498    /// whatever its shape, and nothing else is read.
499    #[test]
500    fn the_input_is_read_at_most_twice() {
501        for (blocks, body) in [(1, 40), (7, 3000), (300, 25), (2000, 13)] {
502            let data = synthetic(blocks, body);
503            let len = data.len() as u64;
504            let (code, fs, err) = run_mem(data);
505            assert_eq!(code, 0, "{err}");
506            assert_eq!(fs.opens, 2);
507            assert_eq!(fs.read.get(), 2 * len, "{blocks} blocks of {body}");
508        }
509        // Inputs that stop after the first pass read once.
510        for data in [Vec::new(), b"not bzip2 at all".repeat(1000)] {
511            let len = data.len() as u64;
512            let (code, fs, _) = run_mem(data);
513            assert_eq!(code, 1);
514            assert_eq!(fs.read.get(), len);
515        }
516    }
517
518    #[test]
519    fn each_block_becomes_a_stream_with_its_crc_as_the_combined_crc() {
520        let data = synthetic(2, 30);
521        let (code, fs, err) = run_mem(data);
522        assert_eq!(code, 0, "{err}");
523        let rec1 = fs.files[&b"rec00001d.bz2".to_vec()].borrow().clone();
524        let mut want = b"BZh9".to_vec();
525        want.extend_from_slice(&[0x31, 0x41, 0x59, 0x26, 0x53, 0x59]);
526        let crc = 0u32.to_be_bytes();
527        want.extend_from_slice(&crc);
528        want.extend((0..30).map(|k| (k * 7) as u8 | 0x80));
529        want.extend_from_slice(&[0x17, 0x72, 0x45, 0x38, 0x50, 0x90]);
530        want.extend_from_slice(&crc);
531        // Byte-aligned, so bsClose's final byte is the CRC's last: no padding.
532        assert_eq!(rec1, want);
533        let mut line = Vec::new();
534        m::printf(&mut line, m::WRITING, &[b"2", b"rec00002d.bz2"]);
535        assert!(err.contains(std::str::from_utf8(&line).unwrap()));
536        assert!(err.ends_with("bzip2recover: finished\n"));
537    }
538
539    #[test]
540    fn output_names_split_at_the_last_slash_and_keep_one_bz2() {
541        let n = |s: &str, k| String::from_utf8(out_name(s.as_bytes(), k)).unwrap();
542        assert_eq!(n("d.bz2", 1), "rec00001d.bz2");
543        assert_eq!(n("a/b/d.bz2", 12), "a/b/rec00012d.bz2");
544        assert_eq!(n("d.dat", 50000), "rec50000d.dat.bz2");
545        assert_eq!(n("my file", 3), "rec00003my file.bz2");
546        assert_eq!(n(".bz2", 1), "rec00001.bz2");
547        assert_eq!(n("dir/", 1), "dir/rec00001.bz2");
548    }
549
550    #[test]
551    fn usage_and_length_limits() {
552        let mut fs = MemFs::default();
553        let mut err = Vec::new();
554        assert_eq!(recover(b"/x/bzip2recover", &[], &mut fs, &mut err), 1);
555        assert_eq!(
556            String::from_utf8(err).unwrap(),
557            "bzip2recover 1.0.8: extracts blocks from damaged .bz2 files.\n\
558             /x/bzip2recover: usage is `/x/bzip2recover damaged_file_name'.\n\
559             \trestrictions on size of recovered file: None\n"
560        );
561        for (len, refused) in [(1979, false), (1980, true)] {
562            let mut err = Vec::new();
563            assert_eq!(recover(b"p", &[vec![b'a'; len]], &mut fs, &mut err), 1);
564            let err = String::from_utf8(err).unwrap();
565            assert_eq!(
566                err.contains(&format!("suspiciously (>= {len} chars) long.  Bye!")),
567                refused,
568                "{err}"
569            );
570        }
571        // progName keeps at most BZ_MAX_FILENAME - 1 bytes.
572        let mut err = Vec::new();
573        recover(&[b'p'; 2500], &[], &mut fs, &mut err);
574        let first = err.split(|&c| c == b'\n').nth(1).unwrap();
575        assert!(first.starts_with(&[b'p'; 1999]) && first[1999] == b':');
576    }
577}