//// Facade for the `packkit` package. This module wires the codec, //// archive, and recipe primitives so callers can read, write, pack, //// and unpack data without selecting the underlying engine by hand. import gleam/bit_array import gleam/list import gleam/option.{type Option, None, Some} import gleam/result import packkit/ar import packkit/archive import packkit/brotli import packkit/bzip2 import packkit/codec import packkit/cpio import packkit/deflate import packkit/detect import packkit/error import packkit/gzip import packkit/level import packkit/limit import packkit/lz4 import packkit/lzw import packkit/recipe import packkit/seven_z import packkit/snappy import packkit/tar import packkit/xz import packkit/zip as zip_archive import packkit/zlib import packkit/zstd pub type Archive = archive.Archive pub type ArchiveFormat = archive.ArchiveFormat pub type Codec = codec.Codec pub type CodecError = error.CodecError pub type ArchiveError = error.ArchiveError pub type DetectError = error.DetectError pub type Detected = detect.Detected pub type Recipe = recipe.Recipe pub type Limits = limit.Limits /// The package version. pub fn package_version() -> String { "0.1.0" } /// Compress `bytes` with `codec`. The codec's optional level and /// preset dictionary are honoured where the family supports them; if /// the family cannot honour an option a typed `CodecOptionUnsupported` /// error is returned instead of silently dropping the request. pub fn compress( bytes bytes: BitArray, with codec_value: Codec, ) -> Result(BitArray, error.CodecError) { case codec.kind(codec_value) { codec.Identity -> { use _ <- result.try(reject_dictionary(codec_value)) use _ <- result.try(reject_non_default_level(codec_value, "identity")) Ok(bytes) } codec.Deflate -> compress_deflate(bytes, codec_value) codec.Zlib -> compress_zlib(bytes, codec_value) codec.Gzip -> compress_gzip(bytes, codec_value) codec.Lz4 -> compress_levelless(bytes, codec_value, "lz4", lz4.encode) codec.Snappy -> compress_levelless(bytes, codec_value, "snappy", snappy.encode) codec.Bzip2 -> compress_bzip2(bytes, codec_value) codec.Lzw -> compress_levelless(bytes, codec_value, "lzw", lzw.encode) codec.Xz -> compress_fixed_level(bytes, codec_value, "xz", xz.encode) codec.Zstd -> compress_fixed_level(bytes, codec_value, "zstd", zstd.encode) codec.Brotli -> compress_fixed_level(bytes, codec_value, "brotli", brotli.encode) } } /// Decompress `bytes` with `codec` using the default limits. Honours /// the codec's optional preset dictionary (currently only zlib) and /// otherwise rejects dictionary use with a typed error. pub fn decompress( bytes bytes: BitArray, with codec_value: Codec, ) -> Result(BitArray, error.CodecError) { decompress_with_limits( bytes: bytes, with: codec_value, limits: limit.default(), ) } /// Decompress `bytes` with `codec`, threading the supplied `Limits` /// value through to the underlying codec's `decode_with_limits` /// entrypoint. Codecs without an explicit limits hook receive their /// own family-specific defaults; today every byte-to-byte codec we /// support honours `Limits`. pub fn decompress_with_limits( bytes bytes: BitArray, with codec_value: Codec, limits limits: Limits, ) -> Result(BitArray, error.CodecError) { case codec.kind(codec_value) { codec.Identity -> { use _ <- result.try(reject_dictionary(codec_value)) use _ <- result.try(reject_non_default_level(codec_value, "identity")) enforce_input_limit(bytes, limits) |> result.map(fn(_) { bytes }) } codec.Deflate -> { use _ <- result.try(reject_dictionary(codec_value)) deflate.decode_with_limits(bytes: bytes, limits: limits) } codec.Zlib -> decompress_zlib(bytes, codec_value, limits) codec.Gzip -> { use _ <- result.try(reject_dictionary(codec_value)) gzip.decode_with_limits(bytes: bytes, limits: limits) |> result.map(fn(decoded) { decoded.payload }) } codec.Lz4 -> { use _ <- result.try(reject_dictionary(codec_value)) lz4.decode_with_limits(bytes: bytes, limits: limits) } codec.Snappy -> { use _ <- result.try(reject_dictionary(codec_value)) snappy.decode_with_limits(bytes: bytes, limits: limits) } codec.Bzip2 -> { use _ <- result.try(reject_dictionary(codec_value)) bzip2.decode_with_limits(bytes: bytes, limits: limits) } codec.Lzw -> { use _ <- result.try(reject_dictionary(codec_value)) lzw.decode_with_limits(bytes: bytes, limits: limits) } codec.Xz -> { use _ <- result.try(reject_dictionary(codec_value)) xz.decode_with_limits(bytes: bytes, limits: limits) } codec.Zstd -> { use _ <- result.try(reject_dictionary(codec_value)) zstd.decode_with_limits(bytes: bytes, limits: limits) } codec.Brotli -> { use _ <- result.try(reject_dictionary(codec_value)) brotli.decode_with_limits(bytes: bytes, limits: limits) } } } fn enforce_input_limit( bytes: BitArray, limits: Limits, ) -> Result(Nil, error.CodecError) { let size = bit_array.byte_size(bytes) case size > limit.max_input_bytes(limits) { True -> Error(error.CodecLimitExceeded(limit: "max_input_bytes", actual: size)) False -> Ok(Nil) } } fn compress_deflate( bytes: BitArray, codec_value: Codec, ) -> Result(BitArray, error.CodecError) { use _ <- result.try(reject_dictionary(codec_value)) // DEFLATE honours exactly two settings today: level 0 (`store`) and // the implicit default (fixed-Huffman LZ77). Anything else would be // silently coerced, so reject it. case effective_level(codec_value) { None -> deflate.encode(bytes: bytes) Some(0) -> deflate.encode_stored_only(bytes: bytes) Some(n) -> case n == default_level_value() { True -> deflate.encode(bytes: bytes) False -> Error(error.CodecOptionUnsupported( option: "level", codec_name: "deflate", )) } } } fn compress_zlib( bytes: BitArray, codec_value: Codec, ) -> Result(BitArray, error.CodecError) { // zlib.encode delegates to the fixed-Huffman DEFLATE encoder, which // has no level knob today. Accept only the implicit default level // so callers can't pass `with_level(level.best())` and silently get // the same bytes as `with_level(level.fast())`. use _ <- result.try(reject_non_default_level(codec_value, "zlib")) case codec.dictionary_of(codec_value) { None -> zlib.encode(bytes: bytes) Some(dict) -> zlib.encode_with_dictionary( bytes: bytes, dictionary: codec.dictionary_bytes(dict), ) } } fn decompress_zlib( bytes: BitArray, codec_value: Codec, limits: Limits, ) -> Result(BitArray, error.CodecError) { case codec.dictionary_of(codec_value) { None -> zlib.decode_with_limits(bytes: bytes, limits: limits) Some(dict) -> zlib.decode_with_dictionary_and_limits( bytes: bytes, dictionary: codec.dictionary_bytes(dict), limits: limits, ) } } fn compress_gzip( bytes: BitArray, codec_value: Codec, ) -> Result(BitArray, error.CodecError) { use _ <- result.try(reject_dictionary(codec_value)) // gzip.encode shares the zlib code path's lack of a level knob, so // we accept only the implicit default level for the same reason. use _ <- result.try(reject_non_default_level(codec_value, "gzip")) gzip.encode(bytes: bytes) } fn compress_bzip2( bytes: BitArray, codec_value: Codec, ) -> Result(BitArray, error.CodecError) { use _ <- result.try(reject_dictionary(codec_value)) let level_value = case effective_level(codec_value) { // Treat "store" (0) as the smallest valid bzip2 block size (1) // so callers can ask for the fastest setting without colliding // with the bzip2-specific 1..9 range. Some(0) -> 1 Some(n) -> int_clamp(n, 1, 9) None -> 9 } bzip2.encode_with_level(bytes: bytes, level: level_value) } /// Codecs whose encoders genuinely cannot consume a level knob — any /// non-`None` level (other than the implicit default) is reported as /// `CodecOptionUnsupported` so callers see the mismatch instead of /// the codec silently doing the same thing for every level. fn compress_levelless( bytes: BitArray, codec_value: Codec, codec_name: String, run: fn(BitArray) -> Result(BitArray, error.CodecError), ) -> Result(BitArray, error.CodecError) { use _ <- result.try(reject_dictionary(codec_value)) use _ <- result.try(reject_level(codec_value, codec_name)) run(bytes) } /// Codecs whose encoders accept a level conceptually but currently /// always emit the simplest representation (xz LZMA2 uncompressed, /// zstd raw frames, brotli uncompressed metablocks). We accept the /// implicit default level (so the smart constructor still works) but /// reject any caller-supplied non-default level so it's never /// silently dropped. Dictionaries are still rejected. fn compress_fixed_level( bytes: BitArray, codec_value: Codec, codec_name: String, run: fn(BitArray) -> Result(BitArray, error.CodecError), ) -> Result(BitArray, error.CodecError) { use _ <- result.try(reject_dictionary(codec_value)) use _ <- result.try(reject_non_default_level(codec_value, codec_name)) run(bytes) } fn reject_dictionary(codec_value: Codec) -> Result(Nil, error.CodecError) { case codec.dictionary_of(codec_value) { None -> Ok(Nil) Some(_) -> case codec.name(codec_value) { "zlib" -> Ok(Nil) name -> Error(error.CodecOptionUnsupported( option: "dictionary", codec_name: name, )) } } } fn reject_level( codec_value: Codec, codec_name: String, ) -> Result(Nil, error.CodecError) { case codec.level(codec_value) { None -> Ok(Nil) Some(_) -> Error(error.CodecOptionUnsupported( option: "level", codec_name: codec_name, )) } } /// Accepts the implicit default level, rejects anything else with /// `CodecOptionUnsupported`. Used by codecs whose encoders share a /// single fixed strategy, so non-default levels would be silently /// dropped if accepted. fn reject_non_default_level( codec_value: Codec, codec_name: String, ) -> Result(Nil, error.CodecError) { case codec.level(codec_value) { None -> Ok(Nil) Some(l) -> case level.value(l) == default_level_value() { True -> Ok(Nil) False -> Error(error.CodecOptionUnsupported( option: "level", codec_name: codec_name, )) } } } fn default_level_value() -> Int { level.value(level.default()) } fn effective_level(codec_value: Codec) -> Option(Int) { case codec.level(codec_value) { Some(l) -> Some(level.value(l)) None -> None } } fn int_clamp(value: Int, low: Int, high: Int) -> Int { case value < low, value > high { True, _ -> low _, True -> high _, _ -> value } } /// Read an archive from `bytes` interpreted as `format` using the /// default resource limits. pub fn read( bytes bytes: BitArray, format format: ArchiveFormat, ) -> Result(Archive, error.ArchiveError) { read_with_limits(bytes: bytes, format: format, limits: limit.default()) } /// Read an archive while threading the supplied `Limits` through to /// the underlying archive decoder. Each family enforces the subset of /// limits that applies to it (input size, member count, name length, /// entry depth). pub fn read_with_limits( bytes bytes: BitArray, format format: ArchiveFormat, limits limits: Limits, ) -> Result(Archive, error.ArchiveError) { case archive.kind(format) { archive.Tar -> tar.decode_with_limits(bytes: bytes, limits: limits) archive.Zip -> zip_archive.decode_with_limits(bytes: bytes, limits: limits) archive.CpioNewc -> cpio.decode_with_limits(bytes: bytes, limits: limits) archive.Ar -> ar.decode_with_limits(bytes: bytes, limits: limits) archive.SevenZ -> seven_z.decode_with_limits(bytes: bytes, limits: limits) } } /// Serialise an archive to bytes using `format`. The supplied /// `format` must match the format tag the `Archive` was constructed /// with — `Archive` is bound to one format at construction time, and /// pretending it is a different format would silently corrupt the /// output. Mismatches surface as `ArchiveFormatMismatch`. pub fn write( archive_value archive_value: Archive, format format: ArchiveFormat, ) -> Result(BitArray, error.ArchiveError) { use _ <- result.try(ensure_archive_format_matches(archive_value, format)) case archive.kind(format) { archive.Tar -> tar.encode(archive: archive_value) archive.Zip -> zip_archive.encode(archive: archive_value) archive.CpioNewc -> cpio.encode(archive: archive_value) archive.Ar -> ar.encode(archive: archive_value) archive.SevenZ -> seven_z.encode(archive: archive_value) } } fn ensure_archive_format_matches( archive_value: Archive, requested: ArchiveFormat, ) -> Result(Nil, error.ArchiveError) { let archive_kind = archive.kind(archive.format(archive_value)) let requested_kind = archive.kind(requested) case archive_kind == requested_kind { True -> Ok(Nil) False -> Error(error.ArchiveFormatMismatch( archive: archive.name(archive.format(archive_value)), requested: archive.name(requested), )) } } /// Pack an archive with the codec chain described by `recipe`. pub fn pack( archive_value archive_value: Archive, using recipe_value: Recipe, ) -> Result(BitArray, error.ArchiveError) { use archive_bytes <- result.try(write( archive_value: archive_value, format: recipe.archive_format(recipe_value), )) apply_codec_chain_forward(archive_bytes, recipe.codecs(recipe_value)) |> codec_to_archive_error(step: "encode") } /// Unpack a byte stream produced by `recipe` using the default /// resource limits. pub fn unpack( bytes bytes: BitArray, using recipe_value: Recipe, ) -> Result(Archive, error.ArchiveError) { unpack_with_limits(bytes: bytes, using: recipe_value, limits: limit.default()) } /// Unpack a byte stream produced by `recipe`, threading the supplied /// `Limits` through both the codec chain and the underlying archive /// decoder. pub fn unpack_with_limits( bytes bytes: BitArray, using recipe_value: Recipe, limits limits: Limits, ) -> Result(Archive, error.ArchiveError) { use raw_bytes <- result.try( apply_codec_chain_reverse( bytes, list.reverse(recipe.codecs(recipe_value)), limits, ) |> codec_to_archive_error(step: "decode"), ) read_with_limits( bytes: raw_bytes, format: recipe.archive_format(recipe_value), limits: limits, ) } /// Detect from a filename or path suffix. pub fn detect_filename(path: String) -> Result(Detected, error.DetectError) { detect.from_filename(path) } /// Detect from byte signatures. pub fn detect_bytes(bytes: BitArray) -> Result(Detected, error.DetectError) { detect.from_bytes(bytes) } /// Detect via filename first, then fall back to magic-byte detection /// on the supplied content. Re-exports /// `packkit/detect.from_path_or_bytes` from the top-level facade so /// most CLI integrations only need to import `packkit`. pub fn detect_path_or_bytes( path path: String, bytes bytes: BitArray, ) -> Result(Detected, error.DetectError) { detect.from_path_or_bytes(path: path, bytes: bytes) } fn apply_codec_chain_forward( bytes: BitArray, codecs: List(Codec), ) -> Result(BitArray, error.CodecError) { case codecs { [] -> Ok(bytes) [head, ..rest] -> { use compressed <- result.try(compress(bytes: bytes, with: head)) apply_codec_chain_forward(compressed, rest) } } } fn apply_codec_chain_reverse( bytes: BitArray, codecs: List(Codec), limits: Limits, ) -> Result(BitArray, error.CodecError) { case codecs { [] -> Ok(bytes) [head, ..rest] -> { use plain <- result.try(decompress_with_limits( bytes: bytes, with: head, limits: limits, )) apply_codec_chain_reverse(plain, rest, limits) } } } fn codec_to_archive_error( value: Result(a, error.CodecError), step step: String, ) -> Result(a, error.ArchiveError) { case value { Ok(v) -> Ok(v) Error(err) -> Error(error.ArchiveCodecFailed(step: step, cause: err)) } }