stuffr-cli 0.25.1

The stuffr command line interface
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
//! `unix/unix.c`'s names and paths: `mapname()` (an entry's name to the
//! path it is extracted to) and `checkdir()` (the extraction root, the path
//! being built, the directories made on the way), with Apple's `mkdir_qtn()`.
//!
//! The path is built as the C builds it, into [`Checkdir::buildpath`] and
//! then `G.filename` (`uz.filename`): under Apple's tree it starts with the
//! root's real absolute path (`G.rootpath`, no trailing `/`), under
//! upstream's (Ubuntu) with the `-d` operand and a `/`, or nothing. Every
//! file-system operation takes the part after the root ([`target`]) and
//! goes through the beneath walk (`beneath.rs`, deviation 3).

use std::io;

use super::super::fileio::{self, c_str};
use super::super::messages as m;
use super::super::{PK_WARN, Uz, flavour};
use super::beneath::{self, Root, Stat};
use crate::compat::host;

/// unzpriv.h's `mapname()`/`checkdir()` codes.
pub(crate) const MPN_OK: i32 = 0;
pub(crate) const MPN_INF_TRUNC: i32 = 1 << 8;
pub(crate) const MPN_INF_SKIP: i32 = 2 << 8;
pub(crate) const MPN_ERR_SKIP: i32 = 3 << 8;
pub(crate) const MPN_ERR_TOOLONG: i32 = 4 << 8;
pub(crate) const MPN_CREATED_DIR: i32 = 16 << 8;
pub(crate) const MPN_VOL_LABEL: i32 = 17 << 8;
pub(crate) const MPN_MASK: i32 = 0x7f00;

/// unzpriv.h's host number `UNIX_`.
const UNIX_: u8 = 3;
/// `S_ISGID`.
#[cfg(unix)]
const S_ISGID: u32 = 0o2000;

/// `checkdir()`'s state (`SYSTEM_SPECIFIC_GLOBALS`: `rootpath`, `rootlen`,
/// `rootdir`, `buildpath`, `end`, `created_dir`, `renamed_fullpath`, and
/// `G.create_dirs`).
#[derive(Default)]
pub(crate) struct Checkdir {
    /// `G.rootpath` (Apple: the real absolute path, no trailing `/`;
    /// upstream: the operand and a `/`), `G.rootlen` its length (0: none).
    pub(crate) rootpath: Vec<u8>,
    /// apple: unix.c:1169, `G.rootdir`: the root's descriptor. For the
    /// Ubuntu flavour the `-d` root's (upstream keeps only the path).
    pub(crate) rootdir: Option<Root>,
    /// The Ubuntu flavour's paths without a root (no `-d`, or a name
    /// renamed to an absolute one): walked beneath the current directory
    /// (deviation 3).
    pub(crate) cwd: Option<Root>,
    /// `G.buildpath`, the path being built (`None`: freed).
    pub(crate) buildpath: Option<Vec<u8>>,
    /// `G.created_dir`: this entry's path made a directory.
    pub(crate) created_dir: bool,
    /// `G.renamed_fullpath`: renamed to an absolute name (no root prefix).
    pub(crate) renamed_fullpath: bool,
    /// `G.create_dirs`: directories may be made (not `-f`, or renamed).
    pub(crate) create_dirs: bool,
}

impl Checkdir {
    /// `G.rootlen`.
    pub(crate) fn rootlen(&self) -> usize {
        self.rootpath.len()
    }
}

/// Where the path `full` (a `G.filename` or `G.buildpath` this module
/// built) lives: the root it is walked from and the part after the root.
/// ruling: T10-3. Apple's tree takes `full + G.rootlen + 1` on faith; for a
/// name renamed to an absolute one (no root prefix) that points past the
/// name into stale memory (undefined behaviour; the binary failed with
/// "No such file or directory"). Here such a path is "No such file or
/// directory" for every operation. Upstream builds it relative (the
/// leading `/` is dropped) and walks it from the current directory, as the
/// Ubuntu flavour does here.
pub(crate) fn target<'a>(uz: &'a Uz, full: &'a [u8]) -> io::Result<(&'a Root, &'a [u8])> {
    let ck = &uz.ck;
    let rootlen = ck.rootlen();
    if flavour::apple_root(uz.flavour) {
        let root = ck.rootdir.as_ref().ok_or_else(enoent)?;
        if rootlen > 0
            && full.len() > rootlen
            && full.starts_with(&ck.rootpath)
            && full[rootlen] == b'/'
        {
            return Ok((root, &full[rootlen + 1..]));
        }
        Err(enoent())
    } else if rootlen > 0 && !ck.renamed_fullpath && full.starts_with(&ck.rootpath) {
        let root = ck.rootdir.as_ref().ok_or_else(enoent)?;
        Ok((root, &full[rootlen..]))
    } else {
        let root = ck.cwd.as_ref().ok_or_else(enoent)?;
        Ok((root, full))
    }
}

/// `ENOENT`.
fn enoent() -> io::Error {
    #[cfg(unix)]
    {
        io::Error::from_raw_os_error(libc::ENOENT)
    }
    #[cfg(not(unix))]
    {
        io::Error::from(io::ErrorKind::NotFound)
    }
}

/// How the extraction shows a path (`full`, a `G.filename`): apple:
/// unzpriv.h:2738 `FnFilterP` (`uO.exdir`, a `/`, then the name from
/// `full + G.rootlen + 1`), upstream's `FnFilter1(G.filename)`. Without a
/// root (`-c`, `-p`) Apple's skips the name's first byte, as its binary
/// does (probe: `unzip -c a.zip` of `dir/f.txt` shows `extracting:
/// ir/f.txt`; with `-d x`, `x/ir/f.txt`); past the name's end it reads what
/// `G.filename` holds there, as the C reads its array.
pub(crate) fn shown(uz: &mut Uz, full: &[u8]) -> Vec<u8> {
    if flavour::apple_root(uz.flavour) {
        let off = uz.ck.rootlen() + 1;
        let raw = if off <= full.len() {
            full[off..].to_vec()
        } else {
            c_str(uz.filename.get(off..).unwrap_or(&[])).to_vec()
        };
        let pfx = uz.o.exdir.clone();
        return fileio::fnfilterp(uz, pfx.as_deref(), &raw);
    }
    fileio::fnfilter1(uz, full)
}

/// The text `strerror()` gives for `e`: an escape is `EACCES`, as Apple's
/// `open_outfile()` converts `ENOTCAPABLE` (apple: fileio.c:314), and as
/// the Ubuntu flavour answers for what it cannot create (deviation 3).
pub(crate) fn strerror(_uz: &Uz, e: &io::Error) -> Vec<u8> {
    if beneath::is_escape(e) {
        return host::strerror(&beneath::eacces());
    }
    host::strerror(e)
}

/// [`strerror`] where Apple prints `ENOTCAPABLE` as it is: `checkdir()`'s
/// "cannot create" (probe: `unzip -d dst c.zip` of `link/sub/file.txt`
/// with `dst/link -> ../bar`: `Capabilities insufficient`).
fn strerror_unconverted(uz: &Uz, e: &io::Error) -> Vec<u8> {
    if flavour::apple_root(uz.flavour) {
        return host::strerror(e);
    }
    strerror(uz, e)
}

/// `stat()` or `fstatat(..., AT_RESOLVE_BENEATH | AT_SYMLINK_NOFOLLOW)` of a
/// path this module built.
pub(crate) fn lstat(uz: &Uz, full: &[u8]) -> io::Result<Stat> {
    let (root, rel) = target(uz, full)?;
    root.lstat(rel)
}

/// `stat()` beneath the root (the last component followed beneath it).
pub(crate) fn stat(uz: &Uz, full: &[u8]) -> io::Result<Stat> {
    let (root, rel) = target(uz, full)?;
    root.stat(rel)
}

// port: unix.c:mkdir_qtn
/// `mkdir_qtn(zipfd, path, mode)` within the root: the parent opened
/// beneath the root, `mkdirat()` there, and the archive's quarantine given
/// to the directory (apple: unix.c:757; both flavours on macOS, deviation
/// 7). `EEXIST` is success in Apple's wrapper; upstream calls `mkdir()`
/// itself, for which it is a failure.
fn mkdir_qtn(uz: &Uz, path: &[u8], mode: u32) -> io::Result<()> {
    let (root, rel) = target(uz, path)?;
    let (dir, name) = root.parent(rel)?;
    match dir.mkdir(&name, mode) {
        Ok(()) => {}
        Err(e) if e.kind() == io::ErrorKind::AlreadyExists && flavour::apple_root(uz.flavour) => {}
        Err(e) => return Err(e),
    }
    if uz.qf.is_some()
        && let Ok(d) = root.dir(rel)
        && let Err(e) = super::attrs::quarantine_dir(uz, &d)
    {
        // apple: unix.c:809 (the new directory is removed again; the error
        // is the quarantine's)
        let _ = dir.unlink(&name, true);
        return Err(e);
    }
    Ok(())
}

/// `mkdir_qtn()` of the extraction root itself (`G.rootpath` still NULL:
/// the operand, made in the current directory), with its quarantine
/// (apple: unix.c:781).
fn mkdir_root(uz: &Uz, tmproot: &[u8], mode: u32) -> io::Result<()> {
    let p = uz.host.path(tmproot);
    let made = {
        #[cfg(unix)]
        {
            use std::os::unix::fs::DirBuilderExt;
            std::fs::DirBuilder::new().mode(mode).create(&p)
        }
        #[cfg(not(unix))]
        {
            let _ = mode;
            std::fs::create_dir(&p)
        }
    };
    match made {
        Ok(()) => {}
        // apple: unix.c:787 (Apple's wrapper; upstream's own `mkdir()`
        // fails)
        Err(e) if e.kind() == io::ErrorKind::AlreadyExists && flavour::apple_root(uz.flavour) => {}
        Err(e) => return Err(e),
    }
    if uz.qf.is_some()
        && let Ok(r) = Root::open(&p)
        && let Ok(d) = r.top()
        && let Err(e) = super::attrs::quarantine_dir(uz, &d)
    {
        let _ = std::fs::remove_dir(&p);
        return Err(e);
    }
    Ok(())
}

// port: unix.c:checkdir (ROOT)
/// `checkdir(exdir, ROOT)`: the extraction root, made when missing (one
/// level, as the C makes it) unless freshening. Apple's tree always has one
/// (`.` without `-d`) and keeps its real path; upstream's only for `-d`.
/// Returns an `MPN_*` code.
pub(crate) fn checkdir_root(uz: &mut Uz, pathcomp: &[u8]) -> i32 {
    if uz.ck.rootlen() > 0 {
        // rootpath was already set, nothing to do
        return MPN_OK;
    }
    if pathcomp.is_empty() {
        return MPN_OK;
    }
    let mut tmproot = pathcomp.to_vec();
    if tmproot.last() == Some(&b'/') {
        tmproot.pop();
    }
    if !tmproot.is_empty() {
        let p = uz.host.path(&tmproot);
        let is_dir = std::fs::metadata(&p).is_ok_and(|md| md.is_dir());
        if !is_dir {
            // path does not exist
            if !uz.ck.create_dirs {
                // skip (or treat as stored file)
                return MPN_INF_SKIP;
            }
            // create the directory (could add loop here scanning tmproot
            // to create more than one level, but why really necessary?)
            if let Err(e) = mkdir_root(uz, &tmproot, 0o777) {
                let shown = fileio::fnfilter1(uz, &tmproot);
                let err = host::strerror(&e);
                uz.info_fmt(1, m::CHECKDIR_CANNOT_CREATE_ROOT, &[&shown, &err]);
                // path didn't exist, tried to create, and failed: file
                // exists, or 2+ subdir levels required
                return MPN_ERR_SKIP;
            }
        }
    }
    if flavour::apple_root(uz.flavour) {
        // apple: unix.c:1162
        let Ok(real) = std::fs::canonicalize(uz.host.path(&tmproot)) else {
            return MPN_ERR_SKIP;
        };
        let Ok(root) = Root::open(&real) else {
            return MPN_ERR_SKIP;
        };
        uz.ck.rootpath = real.as_os_str().as_encoded_bytes().to_vec();
        uz.ck.rootdir = Some(root);
    } else {
        tmproot.push(b'/');
        match Root::open(&uz.host.path(&tmproot)) {
            Ok(root) => uz.ck.rootdir = Some(root),
            Err(_) => return MPN_ERR_SKIP,
        }
        uz.ck.rootpath = tmproot;
    }
    MPN_OK
}

/// The Ubuntu flavour's root for paths without one (deviation 3): the
/// current directory, opened once.
pub(crate) fn open_cwd(uz: &mut Uz) {
    if uz.ck.cwd.is_none() {
        uz.ck.cwd = Root::open(&uz.host.path(b".")).ok();
    }
}

// port: unix.c:checkdir (INIT)
/// `checkdir(NULL, INIT)`: start the path at the root (unless renamed to an
/// absolute name).
fn checkdir_init(uz: &mut Uz) {
    let mut b = Vec::new();
    if uz.ck.rootlen() > 0 && !uz.ck.renamed_fullpath {
        b.extend_from_slice(&uz.ck.rootpath);
    }
    uz.ck.buildpath = Some(b);
}

// port: unix.c:checkdir (APPEND_DIR)
/// `checkdir(pathcomp, APPEND_DIR)`: append a directory and make it when it
/// does not exist (unless freshening). Under Apple's tree a symlink there
/// is left for the walk (apple: unix.c:1002). Returns an `MPN_*` code.
fn checkdir_append_dir(uz: &mut Uz, pathcomp: &[u8]) -> i32 {
    let apple = flavour::apple_root(uz.flavour);
    let filnamsiz = uz.filename.len();
    let mut b = uz.ck.buildpath.take().unwrap_or_default();
    if apple {
        b.push(b'/');
    }
    b.extend_from_slice(pathcomp);
    // next check: need to append '/', at least one-char name, '\0'
    let too_long = b.len() > filnamsiz - 3;
    let st = if apple {
        lstat(uz, &b)
    } else {
        // ruling: deviation 3. Upstream's `stat()` follows a symlink out of
        // the root and sees what is there; here such a path is taken for
        // the directory Ubuntu would usually find, so the entry goes on to
        // its create, which is refused ("cannot create ... Permission
        // denied"), and nothing outside is looked at to decide.
        match stat(uz, &b) {
            Err(e) if beneath::is_escape(&e) => Ok(Stat {
                mode: 0o040755,
                dev: 0,
                ino: 0,
                mtime: 0,
            }),
            st => st,
        }
    };
    match st {
        Err(_) => {
            // path doesn't exist
            if !uz.ck.create_dirs {
                // told not to create (freshening)
                return MPN_INF_SKIP;
            }
            if too_long {
                let shown = fileio::fnfilter1(uz, &b);
                uz.info_fmt(1, m::CHECKDIR_TOO_LONG, &[&shown]);
                // no room for filenames:  fatal
                return MPN_ERR_TOOLONG;
            }
            if let Err(e) = mkdir_qtn(uz, &b, 0o777) {
                let err = strerror_unconverted(uz, &e);
                let shown2 = fileio::fnfilter2(uz, &b);
                let name = c_str(&uz.filename).to_vec();
                let shown1 = fileio::fnfilter1(uz, &name);
                uz.info_fmt(1, m::CHECKDIR_CANNOT_CREATE, &[&shown2, &err, &shown1]);
                // path didn't exist, tried to create, failed
                return MPN_ERR_SKIP;
            }
            uz.ck.created_dir = true;
        }
        // apple: unix.c:1002 (dead, or pointing outside the root: dealt
        // with later)
        Ok(st) if apple && st.is_symlink() => {}
        Ok(st) if !st.is_dir() => {
            let shown2 = fileio::fnfilter2(uz, &b);
            let name = c_str(&uz.filename).to_vec();
            let shown1 = fileio::fnfilter1(uz, &name);
            uz.info_fmt(1, m::CHECKDIR_NOT_DIR, &[&shown2, &shown1]);
            // path existed but wasn't dir
            return MPN_ERR_SKIP;
        }
        Ok(_) => {}
    }
    if too_long {
        let shown = fileio::fnfilter1(uz, &b);
        uz.info_fmt(1, m::CHECKDIR_TOO_LONG, &[&shown]);
        // no room for filenames:  fatal
        return MPN_ERR_TOOLONG;
    }
    if !apple {
        b.push(b'/');
    }
    uz.ck.buildpath = Some(b);
    MPN_OK
}

// port: unix.c:checkdir (APPEND_NAME)
/// `checkdir(pathcomp, APPEND_NAME)`: append the name, truncated (with a
/// warning) at `FILNAMSIZ`.
fn checkdir_append_name(uz: &mut Uz, pathcomp: &[u8]) -> i32 {
    let filnamsiz = uz.filename.len();
    let mut b = uz.ck.buildpath.take().unwrap_or_default();
    if flavour::apple_root(uz.flavour) {
        b.push(b'/');
    }
    for &c in pathcomp {
        b.push(c);
        if b.len() >= filnamsiz {
            b.pop();
            let name = c_str(&uz.filename).to_vec();
            let shown1 = fileio::fnfilter1(uz, &name);
            let shown2 = fileio::fnfilter2(uz, &b);
            uz.info_fmt(0x201, m::CHECKDIR_TRUNCATING, &[&shown1, &shown2]);
            uz.ck.buildpath = Some(b);
            // filename truncated
            return MPN_INF_TRUNC;
        }
    }
    uz.ck.buildpath = Some(b);
    MPN_OK
}

// port: unix.c:checkdir (GETPATH)
/// `checkdir(G.filename, GETPATH)`: the built path into `G.filename`.
fn checkdir_getpath(uz: &mut Uz) {
    let b = uz.ck.buildpath.take().unwrap_or_default();
    set_filename(uz, &b);
}

/// `strcpy(G.filename, s)` (the path never passes `FILNAMSIZ - 1` bytes:
/// `checkdir()` holds it there).
pub(crate) fn set_filename(uz: &mut Uz, s: &[u8]) {
    let n = s.len().min(uz.filename.len() - 1);
    uz.filename[..n].copy_from_slice(&s[..n]);
    uz.filename[n] = 0;
}

/// unix.c:filtattr: the set-user-ID, set-group-ID and sticky bits dropped
/// unless `-K`.
pub(crate) fn filtattr(uz: &Uz, perms: u32) -> u32 {
    let mut perms = perms;
    // keep setuid/setgid/tacky perms?
    if !uz.o.k_flag {
        perms &= !0o7000;
    }
    0xffff & perms
}

// port: unix.c:mapname
/// `mapname(renamed)`: the entry's name (`G.filename`) mapped to the path it
/// is extracted to, directories made on the way, `G.filename` set to it.
/// `../` components are dropped with a warning (unless `-:`), control
/// bytes dropped (unless `-^`), a VMS version removed (unless `-V`), and
/// `.` and `..` as a last component become `_` and `__`. A directory entry
/// is made here and answers `MPN_CREATED_DIR` (or `MPN_INF_SKIP` when it
/// existed). `pinfo` is the entry's `G.pInfo` (its `file_attr` may change),
/// `extra_field` its local extra field (`G.extra_field`), which the Ubuntu
/// flavour's `-F` reads.
pub(crate) fn mapname(
    uz: &mut Uz,
    pinfo: &mut super::super::extract::Info,
    extra_field: Option<&[u8]>,
    renamed: bool,
) -> i32 {
    let mut killed_ddot = false; // is set when skipping "../" pathcomp
    let mut error = MPN_OK;

    if pinfo.pinfo.vollabel {
        return MPN_VOL_LABEL; // can't set disk volume labels in Unix
    }

    // can create path as long as not just freshening, or if user told us
    uz.ck.create_dirs = !uz.o.fflag || renamed;
    uz.ck.created_dir = false; // not yet

    let filename = c_str(&uz.filename).to_vec();
    // user gave full pathname:  don't prepend rootpath
    uz.ck.renamed_fullpath = renamed && filename.first() == Some(&b'/');

    checkdir_init(uz); // initialize path buffer

    let mut pathcomp: Vec<u8> = Vec::new(); // initialize translation buffer
    let mut lastsemi: Option<usize> = None;
    // ubuntu: -DACORN_FTYPE_NFS: `lastcomma` points into the C's
    // `pathcomp[]` and is not reset between components, so the filetype
    // check can read what an earlier component left there; `image` is that
    // array as the C writes it (every byte put, every terminator).
    let acorn = flavour::acorn_ftype_nfs(uz.flavour);
    let mut lastcomma: Option<usize> = None;
    let mut image: Vec<u8> = Vec::new();
    let cp = if uz.o.jflag {
        // junking directories
        match filename.iter().rposition(|&b| b == b'/') {
            Some(i) => &filename[i + 1..],
            None => &filename[..],
        }
    } else {
        &filename[..]
    };

    for &workch in cp {
        match workch {
            b'/' => {
                // can assume -j flag not given
                poke(&mut image, pathcomp.len(), 0);
                if pathcomp == b"." {
                    // don't bother appending "./" to the path
                    pathcomp.clear();
                    poke(&mut image, 0, 0);
                } else if uz.o.ddotflag == 0 && pathcomp == b".." {
                    // "../" dir traversal detected, skip over it
                    pathcomp.clear();
                    poke(&mut image, 0, 0);
                    killed_ddot = true; // set "show message" flag
                }
                // when path component is not empty, append it now
                if !pathcomp.is_empty() {
                    error = checkdir_append_dir(uz, &pathcomp);
                    if error & MPN_MASK > MPN_INF_TRUNC {
                        return error;
                    }
                }
                pathcomp.clear(); // reset conversion buffer for next piece
                lastsemi = None; // leave direct. semi-colons alone
            }
            b';' => {
                // VMS version (or DEC-20 attrib?)
                lastsemi = Some(pathcomp.len());
                // keep for now; remove VMS ";##"
                put(&mut pathcomp, &mut image, b';');
            }
            // ubuntu: -DACORN_FTYPE_NFS
            b',' if acorn => {
                // NFS filetype extension
                lastcomma = Some(pathcomp.len());
                // keep for now; may need to remove later, if requested
                put(&mut pathcomp, &mut image, b',');
            }
            _ => {
                // disable control character filter when requested, else
                // allow 8-bit characters (e.g. UTF-8) in filenames:
                if uz.o.cflxflag != 0
                    || fileio::is_print(uz, workch)
                    || (128..=254).contains(&workch)
                {
                    put(&mut pathcomp, &mut image, workch);
                }
            }
        }
    }

    // Show warning when stripping insecure "parent dir" path components
    if killed_ddot && uz.o.qflag == 0 {
        let shown = fileio::fnfilter1(uz, &filename);
        uz.info_fmt(0, m::SKIPPED_DDOT, &[&shown]);
        if error & !MPN_MASK == 0 {
            error = (error & MPN_MASK) | PK_WARN;
        }
    }

    // Report if directory was created (and no file to create:  filename
    // ended in '/'), check name to be sure it exists, and combine path and
    // name before exiting.
    // ruling: T10-4. An empty name makes the C read `G.filename[-1]` (before
    // the array: undefined behaviour); here an empty name does not end in
    // '/', so it fails conversion below.
    if filename.last() == Some(&b'/') {
        checkdir_getpath(uz);
        if uz.ck.created_dir {
            let full = c_str(&uz.filename).to_vec();
            if uz.o.qflag == 0 {
                let shown = shown(uz, &full);
                uz.info_fmt(0, m::CREATING, &[&shown]);
            }
            // Filter out security-relevant attributes bits.
            pinfo.file_attr = filtattr(uz, pinfo.file_attr);
            // When extracting non-UNIX directories or when extracting
            // without UID/GID restoration or SGID preservation, any SGID
            // flag inherited from the parent directory should be
            // maintained to allow files extracted into this new folder to
            // inherit the GID setting from the parent directory.
            if pinfo.pinfo.hostnum != UNIX_ || !(uz.o.x_flag != 0 || uz.o.k_flag) {
                // preserve SGID bit when inherited from parent dir (a
                // `stat()`: read only, as the C's)
                match std::fs::metadata(uz.host.path(&full)) {
                    #[cfg(unix)]
                    Ok(st) => {
                        use std::os::unix::fs::PermissionsExt;
                        pinfo.file_attr |= st.permissions().mode() & S_ISGID;
                    }
                    #[cfg(not(unix))]
                    Ok(_) => {}
                    Err(e) => perror(uz, m::PERROR_DIR_ATTRS, &e),
                }
            }
            // set approx. dir perms (make sure can still read/write in dir)
            if let Err(e) = super::attrs::chmod_dir(uz, &full, pinfo.file_attr | 0o700) {
                perror(uz, m::PERROR_CHMOD_DIR, &e);
            }
            // set dir time (note trailing '/')
            return (error & !MPN_MASK) | MPN_CREATED_DIR;
        }
        // dir existed already; don't look for data to extract
        return (error & !MPN_MASK) | MPN_INF_SKIP;
    }

    poke(&mut image, pathcomp.len(), 0); // done with pathcomp:  terminate it

    // if not saving them, remove VMS version numbers (appended ";###")
    if !uz.o.v_flag
        && let Some(semi) = lastsemi
        && pathcomp[semi + 1..].iter().all(u8::is_ascii_digit)
    {
        // only digits between ';' and end:  nuke
        pathcomp.truncate(semi);
        poke(&mut image, semi, 0);
    }

    // On UNIX (and compatible systems), "." and ".." are reserved for
    // directory navigation and cannot be used as regular file names. These
    // reserved one-dot and two-dot names are mapped to "_" and "__".
    if pathcomp == b"." {
        pathcomp = b"_".to_vec();
        poke(&mut image, 0, b'_');
    } else if pathcomp == b".." {
        pathcomp = b"__".to_vec();
        for (i, &c) in b"__\0".iter().enumerate() {
            poke(&mut image, i, c);
        }
    }

    // ubuntu: -DACORN_FTYPE_NFS
    // translate Acorn filetype information if asked to do so
    if acorn
        && uz.o.acorn_nfs_ext
        && let Some(ef_spark) = extra_field.and_then(get_riscos_exfield)
    {
        // file *must* have a RISC OS extra field
        let ft = fileio::makelong(&ef_spark[8..]);
        if let Some(comma) = lastcomma {
            // `isxdigit()` from the comma on, in the C's array
            let at = |i: usize| image.get(i).copied().unwrap_or(0);
            let mut pp = comma + 1;
            while at(pp).is_ascii_hexdigit() {
                pp += 1;
            }
            if pp == comma + 4 && at(pp) == 0 {
                // nuke
                pathcomp.truncate(comma);
                poke(&mut image, comma, 0);
            }
        }
        // ruling: T13-4. `ft & 1<<31` shifts into the sign bit of an `int`
        // (undefined in C); gcc's answer, the sign bit, is taken: bit 31 of
        // the load address.
        let ft = if ft & (1 << 31) == 0 { 0x000F_FD00 } else { ft };
        pathcomp.extend_from_slice(format!(",{:03x}", (ft >> 8) & 0xFFF).as_bytes());
    }

    if pathcomp.is_empty() {
        let shown = fileio::fnfilter1(uz, &filename);
        uz.info_fmt(1, m::MAPNAME_FAILED, &[&shown]);
        return (error & !MPN_MASK) | MPN_ERR_SKIP;
    }

    checkdir_append_name(uz, &pathcomp); // returns 1 if truncated: care?
    checkdir_getpath(uz);
    error
}

/// `*pp++ = c`: `c` put at the end of `pathcomp`, and into the C array
/// [`mapname`] keeps the image of.
fn put(pathcomp: &mut Vec<u8>, image: &mut Vec<u8>, c: u8) {
    poke(image, pathcomp.len(), c);
    pathcomp.push(c);
}

/// `pathcomp[i] = c` in the image of the C's array.
fn poke(image: &mut Vec<u8>, i: usize, c: u8) {
    if image.len() <= i {
        image.resize(i + 1, 0);
    }
    image[i] = c;
}

/// process.c's `SPARKID_2`: `"ARC0"`.
const SPARKID_2: u32 = 0x3043_5241;
/// unzpriv.h's `EF_SPARK`: David Pilling's Acorn/SparkFS block (`"AC"`).
const EF_SPARK: u16 = 0x4341;

// port: process.c:getRISCOSexfield
/// ubuntu: -DACORN_FTYPE_NFS. `getRISCOSexfield()`: the extra field's
/// first SparkFS block of 20 or 24 bytes whose data starts `ARC0`, from its
/// header on (`RO_extra_block`: the load address at offset 8), else `None`.
fn get_riscos_exfield(ef: &[u8]) -> Option<&[u8]> {
    let mut ef = ef;
    while ef.len() >= 4 {
        let eb_id = fileio::makeword(ef);
        let eb_len = usize::from(fileio::makeword(&ef[2..]));
        if eb_len > ef.len() - 4 {
            // discovered some extra field inconsistency!
            break;
        }
        if eb_id == EF_SPARK
            && (eb_len == 24 || eb_len == 20)
            && fileio::makelong(&ef[4..]) == SPARKID_2
        {
            // a pointer to the valid SPARK filetype ef block
            return Some(&ef[..4 + eb_len]);
        }
        // Skip this extra field block
        ef = &ef[4 + eb_len..];
    }
    None
}

/// `perror(s)`: `s: <strerror>` and a newline on standard error, straight
/// to the stream (not through `Info()`: `G.sol` is not touched).
pub(crate) fn perror(uz: &mut Uz, s: &str, e: &io::Error) {
    let mut v = s.as_bytes().to_vec();
    v.extend_from_slice(b": ");
    v.extend_from_slice(&strerror(uz, e));
    v.push(b'\n');
    let _ = uz.err.write_all(&v);
    let _ = uz.err.flush();
}