std/archive/extract

std/archive/src/extract.trb

The entries of an archive written out below a directory: List.extract, which refuses every entry that would land outside of it. A module of its own because it reaches the file system - std/archive itself stays pure, and a package that imports this module reaches the capability "files" (docs/design/RELEASE.md section 7.8). It is a member of the list of entries, written here so that use List.extract from "std/archive/extract" finds it.

Examples

use Buffer.untarred from "std/archive"
use List.extract from "std/archive/extract"
use Buffer.gunzipped from "std/compression"

fn unpack(archive: String, directory: String): Result<Void, String> {
  const compressed = File.readBytes(archive).mapError({ _.show() })?
  const tarball = compressed.gunzipped().mapError({ _.show() })?
  const entries = tarball.untarred().mapError({ _.show() })?
  entries.extract(into: directory, stripComponents: 1).mapError({ _.show() })
}

extend ExtractError with From<IoError>

extend ExtractError with From<IoError>

A failure of the file system, as the failure of the extraction it stopped.

fn from

static fn from(value: IoError): Self

extend List<TarEntry>

extend List<TarEntry>

The write-out of a list of entries, written where it is imported from (see the top of this file).

fn extract

fn extract(into: String, stripComponents: Int = 0): Result<Void, ExtractError>

Writes these entries out below the directory into, creating it: every directory, every file with its bytes and its permission bits, every hard link as a copy of the file it names, and the symbolic links last. stripComponents drops that many leading directories from every path and from the target of every hard link, the way tar --strip-components does; an entry left with no path is skipped. A later entry of the same path replaces an earlier one, as tar does.

Safe by default. Every entry is checked before the first byte is written, and one that fails stops the whole extraction: a path that is absolute, names a drive (any :) or climbs with .. - \ counts as a separator as well, as Windows reads it - and a path that leads through a symbolic link, one of the archive or one already below into. A symbolic link is refused where its target is absolute, climbs higher than the link's own directory, or climbs with a .. after a name: that .. would follow whatever the name is, and a name may be another link - under another spelling too, on a system that ignores case - so only a target that climbs first and then descends is sure to stay inside. A hard link is refused where it does not name a file of the archive written before it. A link already where an entry goes is replaced, not followed. The permission bits lose the set-id and sticky bits and the write bits of group and others - what the usual umask 022 does - and a directory gets its own last, so a read-only one is filled first.

Errors

  • ExtractError.Refused for an entry that fails a check, ExtractError.Failed for the file system. What was written before a failure of the file system stays written.

Pitfalls

  • Windows lets a program create a symbolic link only in developer mode or with the privilege to; there an archive with one fails with Operation not permitted, after everything else is written.
  • On Windows the mode sets nothing but the read-only attribute, where the owner's write bit is clear.