diff --git a/.github/workflows/mpkg-action-check.yml b/.github/workflows/mpkg-action-check.yml new file mode 100644 index 0000000..ffddacc --- /dev/null +++ b/.github/workflows/mpkg-action-check.yml @@ -0,0 +1,68 @@ +name: Check public MPKG Action +on: + push: + branches: [staging, main, master] + paths: + - actions/build-mpkg/** + - crates/mpack/** + - .github/workflows/mpkg-action-check.yml + pull_request: + paths: + - actions/build-mpkg/** + - crates/mpack/** + - .github/workflows/mpkg-action-check.yml + workflow_dispatch: +permissions: + contents: read +jobs: + check: + runs-on: ubuntu-24.04 + timeout-minutes: 20 + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 + with: + persist-credentials: false + - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 + with: + python-version: '3.12' + - name: Build bundled mpack without the OS or AppCore workspace + shell: bash + run: | + python3 - <<'PY' + import importlib.util, os + from pathlib import Path + action = Path('actions/build-mpkg').resolve() + spec = importlib.util.spec_from_file_location('action', action / 'package.py') + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + scratch = Path(os.environ['RUNNER_TEMP']) / 'mpkg-tool-test' + scratch.mkdir() + tool = module.bundled_tool(action, scratch) + with open(os.environ['GITHUB_ENV'], 'a') as output: + output.write('MPKG_TEST_MPACK=' + str(tool) + '\n') + PY + - name: Test package boundaries and the real container format + run: python3 -m unittest discover -s actions/build-mpkg/tests -v + - name: Prepare an independent publisher's payload + run: | + mkdir -p test-input/payload/bundle + printf 'format = 1\n[package]\nid = "net.example.independent"\nkind = "resource"\n' > test-input/manifest.toml + printf 'Independent publisher\n' > test-input/payload/bundle/example.txt + - name: Exercise the composite Action + id: mpkg + uses: ./actions/build-mpkg + with: + manifest: test-input/manifest.toml + payload: test-input/payload + output: test-output/Example.mpkg + mpack: ${{ env.MPKG_TEST_MPACK }} + - name: Verify the returned checksum + env: + PACKAGE: ${{ steps.mpkg.outputs.package }} + EXPECTED: ${{ steps.mpkg.outputs.sha256 }} + run: | + python3 - <<'PY' + import hashlib, os + from pathlib import Path + assert hashlib.sha256(Path(os.environ['PACKAGE']).read_bytes()).hexdigest() == os.environ['EXPECTED'] + PY diff --git a/Cargo.lock b/Cargo.lock index d2c95f4..2cfef0e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1851,6 +1851,7 @@ dependencies = [ "mochios-posix-client", "mochios-posix-protocol", "mochios-workspace-protocol", + "serde_json", "toml 1.1.6+spec-1.1.0", "viewkit", "zeroize", @@ -3923,9 +3924,9 @@ checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" [[package]] name = "ui_layout" -version = "0.9.8" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe00a83f195b147e7ea8a2b9abc08b9b3ca043e7fcca996bd7a980174438fb1b" +checksum = "afbdfab90b0052416a13fa78be0f151f5bb6480242601b97e76207f58e8761b0" [[package]] name = "unicode-bidi" diff --git a/actions/build-mpkg/README.md b/actions/build-mpkg/README.md new file mode 100644 index 0000000..64b909f --- /dev/null +++ b/actions/build-mpkg/README.md @@ -0,0 +1,82 @@ +# Build an MPKG in GitHub Actions + +This public composite Action creates an **unsigned MPKG v1** from an existing +manifest and prebuilt payload. It accepts packages from any publisher: native +applications, command-line tools, libraries, and resources supported by MPKG. +It requires no mochiOS organization membership, GitHub token, signing identity, +AppStore account, or particular repository layout. + +Compilation remains a previous workflow step, using the language and SDK chosen +by the developer. The Action does not invoke the mochiOS image build, generate +publisher identities, alter capabilities, sign packages, or publish releases. + +```yaml +permissions: + contents: read + +jobs: + package: + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 + with: + persist-credentials: false + # Build your software and prepare manifest.toml and payload/ here. + - name: Create the unsigned package + id: mpkg + uses: mochiOS/devkit/actions/build-mpkg@staging + with: + manifest: build/package/manifest.toml + payload: build/package/payload + output: dist/Example.mpkg + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 + with: + name: unsigned-mpkg + path: | + ${{ steps.mpkg.outputs.package }} + ${{ steps.mpkg.outputs.checksum-file }} + if-no-files-found: error +``` + +`staging` above is the development example. For production, pin the Action to a +reviewed full commit SHA. No stable Action version is published by this change. + +## Inputs and outputs + +| Input | Meaning | +| --- | --- | +| `manifest` | Required, completed UTF-8 MPKG manifest, `format = 1`. | +| `payload` | Required, directory with the exact prebuilt payload. | +| `output` | New MPKG path, default `dist/package.mpkg`. | +| `mpack` | Optional trusted executable path; otherwise build the bundled mpack. | + +The output directory must be fresh: an existing package or `SHA256SUMS` is never +overwritten. Use a separate output directory for each package. Symlink and +hard-linked payload files are rejected by mpack. Payload contents follow the ordinary MPKG layout: `bundle/` for application +files and `root/` for filesystem-root payloads. Manifest file bindings, resource +paths and capabilities are preserved exactly; this Action does not derive or +repair missing bindings. It performs packaging and container checks, not signer, +installation-policy or runtime validation. + +Outputs are `package` (absolute MPKG path), `sha256` (whole-file SHA-256), and +`checksum-file` (absolute `SHA256SUMS` path). A checksum is not a code signature. +Artifact upload, signing, release approval and publication belong to the caller's +workflow. Official mochiOS apps use the same Action as third-party developers. + +## Runner requirements + +The initial supported runner is `ubuntu-24.04` (Python 3.12+, Bash). Either provide +`mpack`, or have Cargo/Rust and network access to fetch locked crates. The bundled +tool is built as a standalone host project from this Action's own source and +Cargo.lock; the OS/AppCore workspace is not required. Native mochiOS programs are +never executed on the host by this Action. + +Local invocation, useful for debugging the same operation: + +```sh +MPKG_MANIFEST=build/package/manifest.toml \ +MPKG_PAYLOAD=build/package/payload \ +MPKG_OUTPUT=dist/Example.mpkg \ +MPKG_TOOL=/path/to/mpack \ +python3 /path/to/devkit/actions/build-mpkg/package.py +``` diff --git a/actions/build-mpkg/action.yml b/actions/build-mpkg/action.yml new file mode 100644 index 0000000..6376fc9 --- /dev/null +++ b/actions/build-mpkg/action.yml @@ -0,0 +1,42 @@ +name: Build mochiOS package +description: Create an unsigned MPKG from a manifest and prebuilt payload, for any publisher or package kind. +author: mochiOS +branding: + icon: package + color: orange +inputs: + manifest: + description: Path to the completed MPKG manifest.toml. + required: true + payload: + description: Directory containing the already-built MPKG payload. + required: true + output: + description: Destination MPKG path; must not already exist or be inside the payload. + required: false + default: dist/package.mpkg + mpack: + description: Optional path to an existing trusted mpack executable. Otherwise build the version bundled with this Action. + required: false +outputs: + package: + description: Absolute path of the generated unsigned MPKG. + value: ${{ steps.package.outputs.package }} + sha256: + description: SHA-256 of the complete unsigned MPKG file. + value: ${{ steps.package.outputs.sha256 }} + checksum-file: + description: Absolute path of the generated SHA256SUMS file. + value: ${{ steps.package.outputs.checksum-file }} +runs: + using: composite + steps: + - id: package + shell: bash + env: + MPKG_ACTION_PATH: ${{ github.action_path }} + MPKG_MANIFEST: ${{ inputs.manifest }} + MPKG_PAYLOAD: ${{ inputs.payload }} + MPKG_OUTPUT: ${{ inputs.output }} + MPKG_TOOL: ${{ inputs.mpack }} + run: python3 "$MPKG_ACTION_PATH/package.py" diff --git a/actions/build-mpkg/package.py b/actions/build-mpkg/package.py new file mode 100644 index 0000000..e0ea770 --- /dev/null +++ b/actions/build-mpkg/package.py @@ -0,0 +1,121 @@ +#!/usr/bin/env python3 +"""Public MPKG Action: no publisher policy, credentials, signing, or publication.""" +import hashlib +import os +from pathlib import Path +import shutil +import subprocess +import tempfile +import tomllib + + +def digest(path): + """Hash a package without loading it into memory.""" + result = hashlib.sha256() + with path.open('rb') as stream: + for chunk in iter(lambda: stream.read(1024 * 1024), b''): + result.update(chunk) + return result.hexdigest() + + +def checked_path(value, label): + """Reject multiline Action outputs and ambiguous filesystem names.""" + if not value or any(c in value for c in ('\r', '\n', '\0')): + raise ValueError('Invalid ' + label + ' path') + return Path(value).absolute() + + +def validate(manifest, payload, output): + """Check input boundaries without inventing, rewriting, or trusting package metadata.""" + if not manifest.is_file() or not payload.is_dir(): + raise ValueError('A regular manifest file and payload directory are required') + data = tomllib.loads(manifest.read_text(encoding='utf-8')) + if type(data.get('format')) is not int or data['format'] != 1 or not isinstance(data.get('package'), dict): + raise ValueError('Expected a completed format-1 MPKG manifest') + if output.exists() or output.is_symlink(): + raise ValueError('Refusing to overwrite an existing output') + if output.resolve().is_relative_to(payload.resolve()) or output.resolve() == manifest.resolve(): + raise ValueError('Output must be outside the payload and must not alias the manifest') + checksum = output.parent / 'SHA256SUMS' + if checksum.exists() or checksum.is_symlink(): + raise ValueError('Refusing to overwrite an existing SHA256SUMS file; use a fresh output directory') + return checksum + + +def bundled_tool(action, temporary): + """Build only mpack from this Action revision, independently of SDK/OS workspaces.""" + devkit = action.parents[1] + project = temporary / 'mpack' + project.mkdir() + shutil.copyfile(devkit / 'crates/mpack/Cargo.toml', project / 'Cargo.toml') + shutil.copyfile(action / 'tool.Cargo.lock', project / 'Cargo.lock') + shutil.copytree(devkit / 'crates/mpack/src', project / 'src') + # Cargo resolves a standalone tool, never the AppCore/compiler workspace. + with (project / 'Cargo.toml').open('a') as stream: + stream.write('\n[workspace]\n') + target = temporary / 'target' + env = os.environ.copy() + for key in ('RUSTFLAGS', 'CARGO_ENCODED_RUSTFLAGS', 'RUSTC', 'RUSTDOC', 'CARGO_BUILD_TARGET'): + env.pop(key, None) + subprocess.run(['cargo', 'build', '--locked', '--release', '--manifest-path', str(project / 'Cargo.toml'), + '--target-dir', str(target)], check=True, cwd=temporary, env=env) + return target / 'release' / ('mpack.exe' if os.name == 'nt' else 'mpack') + + +def create(manifest, payload, output, tool): + """Run the ordinary packager and attest file integrity, never signature validity.""" + checksum = validate(manifest, payload, output) + output.parent.mkdir(parents=True, exist_ok=True) + # Publish completed outputs only. Failed mpack cannot leave a final artifact. + with tempfile.TemporaryDirectory(prefix='.mpkg-', dir=output.parent) as folder: + staged = Path(folder) / 'package.mpkg' + subprocess.run([str(tool), 'create', '--manifest', str(manifest), '--payload', str(payload), + '--output', str(staged)], check=True) + with staged.open('rb') as stream: + header = stream.read(32) + if (len(header) != 32 or header[:12] != b'MPKG\x01\0\0\0\x20\0\0\0' + or header[20:] != bytes(12) or int.from_bytes(header[12:20], 'little') != staged.stat().st_size - 32): + raise ValueError('mpack did not generate an unsigned MPKG v1') + sha256 = digest(staged) + staged_checksum = Path(folder) / 'SHA256SUMS' + staged_checksum.write_text(sha256 + ' ' + output.name + '\n', encoding='utf-8') + # Same-filesystem links publish complete files, exclusively and atomically. + # The temporary names disappear on exit, leaving ordinary single-link files. + published = False + try: + os.link(staged, output) + published = True + os.link(staged_checksum, checksum) + except FileExistsError: + if published and output.exists() and os.path.samefile(staged, output): + output.unlink() + raise ValueError('Output appeared while packaging; refusing to replace it') + except OSError: + if published and output.exists() and os.path.samefile(staged, output): + output.unlink() + raise + return {'package': str(output), 'sha256': sha256, 'checksum-file': str(checksum)} + + +def main(): + """Read untrusted Action inputs as data, without shell interpolation.""" + manifest = checked_path(os.environ.get('MPKG_MANIFEST', ''), 'manifest') + payload = checked_path(os.environ.get('MPKG_PAYLOAD', ''), 'payload') + output = checked_path(os.environ.get('MPKG_OUTPUT', 'dist/package.mpkg'), 'output') + validate(manifest, payload, output) + action = Path(__file__).resolve().parent + scratch = os.environ.get('RUNNER_TEMP') + with tempfile.TemporaryDirectory(prefix='mpkg-action-', dir=scratch) as folder: + configured = os.environ.get('MPKG_TOOL', '') + tool = checked_path(configured, 'mpack') if configured else bundled_tool(action, Path(folder)) + results = create(manifest, payload, output, tool) + if os.environ.get('GITHUB_OUTPUT'): + with open(os.environ['GITHUB_OUTPUT'], 'a', encoding='utf-8') as stream: + for key, value in results.items(): + stream.write(key + '=' + value + '\n') + print('Created unsigned MPKG: ' + results['package']) + print('SHA-256: ' + results['sha256']) + + +if __name__ == '__main__': + main() diff --git a/actions/build-mpkg/tests/test_package.py b/actions/build-mpkg/tests/test_package.py new file mode 100644 index 0000000..5070225 --- /dev/null +++ b/actions/build-mpkg/tests/test_package.py @@ -0,0 +1,115 @@ +import importlib.util +import os +from pathlib import Path +import subprocess +import tempfile +import unittest +from unittest.mock import patch + +ACTION = Path(__file__).resolve().parents[1] +spec = importlib.util.spec_from_file_location('mpkg_action', ACTION / 'package.py') +package = importlib.util.module_from_spec(spec) +spec.loader.exec_module(package) + + +class Packaging(unittest.TestCase): + def setUp(self): + self.folder = tempfile.TemporaryDirectory() + self.root = Path(self.folder.name) + self.addCleanup(self.folder.cleanup) + self.payload = self.root / 'payload' + self.payload.mkdir() + self.manifest = self.root / 'manifest.toml' + self.manifest.write_text('format = 1\n[package]\nid = "net.example.independent"\nkind = "resource"\n') + (self.payload / 'bundle').mkdir() + (self.payload / 'bundle/sample').write_bytes(b'Independent publisher payload\n') + self.output = self.root / 'dist/Example.mpkg' + + def test_no_publisher_or_package_kind_restriction(self): + for kind in ('application', 'library', 'resource', 'command-line-tool'): + self.manifest.write_text(f'format = 1\n[package]\nid = "net.thirdparty.example"\nkind = "{kind}"\n') + package.validate(self.manifest, self.payload, self.output) + + def test_cannot_overwrite_inputs_or_existing_outputs(self): + for output in (self.manifest, self.payload / 'package.mpkg'): + with self.assertRaises(ValueError): + package.validate(self.manifest, self.payload, output) + self.output.parent.mkdir() + self.output.write_text('existing') + with self.assertRaises(ValueError): + package.validate(self.manifest, self.payload, self.output) + self.assertEqual(self.output.read_text(), 'existing') + + def test_cannot_overwrite_checksum_or_follow_dangling_output_link(self): + self.output.parent.mkdir() + self.output.symlink_to(self.root / 'not-created') + with self.assertRaises(ValueError): + package.validate(self.manifest, self.payload, self.output) + self.output.unlink() + (self.output.parent / 'SHA256SUMS').write_text('existing') + with self.assertRaises(ValueError): + package.validate(self.manifest, self.payload, self.output) + + def test_invalid_and_multiline_inputs(self): + for value in ('', 'a\nb', 'a\rb', 'a\0b'): + with self.assertRaises(ValueError): + package.checked_path(value, 'test') + for text in ('format = true\n[package]\n', 'format = 2\n[package]\n', 'format = 1\n'): + self.manifest.write_text(text) + with self.assertRaises(ValueError): + package.validate(self.manifest, self.payload, self.output) + + def test_packager_failure_never_creates_final_outputs(self): + with patch.object(package.subprocess, 'run', side_effect=subprocess.CalledProcessError(1, 'mpack')): + with self.assertRaises(subprocess.CalledProcessError): + package.create(self.manifest, self.payload, self.output, Path('/unavailable')) + self.assertFalse(self.output.exists()) + self.assertFalse((self.output.parent / 'SHA256SUMS').exists()) + + def test_bad_container_cannot_be_reported_as_success(self): + def fake(args, **kwargs): + Path(args[-1]).write_bytes(b'not an mpkg') + with patch.object(package.subprocess, 'run', side_effect=fake): + with self.assertRaises(ValueError): + package.create(self.manifest, self.payload, self.output, Path('/fake')) + self.assertFalse(self.output.exists()) + + def test_bundled_tool_uses_standalone_locked_sources(self): + temporary = self.root / 'tool' + temporary.mkdir() + with patch.object(package.subprocess, 'run') as call: + package.bundled_tool(ACTION, temporary) + self.assertIn('--locked', call.call_args.args[0]) + self.assertIn('[workspace]', (temporary / 'mpack/Cargo.toml').read_text()) + self.assertEqual((temporary / 'mpack/Cargo.lock').read_bytes(), (ACTION / 'tool.Cargo.lock').read_bytes()) + self.assertTrue((temporary / 'mpack/src/main.rs').is_file()) + + @unittest.skipUnless(os.environ.get('MPKG_TEST_MPACK'), 'Set MPKG_TEST_MPACK to test the actual packager') + def test_real_mpack_and_literal_action_inputs(self): + # Shell syntax is a literal filename, never evaluated by this Action. + self.output = self.root / 'out; literal/Example $(touch INJECTION).mpkg' + original = self.manifest.read_bytes() + result = package.create(self.manifest, self.payload, self.output, Path(os.environ['MPKG_TEST_MPACK'])) + self.assertEqual(result['sha256'], package.digest(self.output)) + self.assertEqual(self.manifest.read_bytes(), original) + self.assertEqual(self.output.stat().st_nlink, 1) + self.assertFalse((self.root / 'INJECTION').exists()) + self.assertEqual((self.output.parent / 'SHA256SUMS').read_text(), result['sha256'] + ' ' + self.output.name + '\n') + environment = dict(os.environ, MPKG_MANIFEST=str(self.manifest), MPKG_PAYLOAD=str(self.payload), + MPKG_OUTPUT=str(self.root / 'second/Example.mpkg'), MPKG_TOOL=os.environ['MPKG_TEST_MPACK'], + GITHUB_OUTPUT=str(self.root / 'outputs')) + subprocess.run(['python3', str(ACTION / 'package.py')], env=environment, check=True) + outputs = (self.root / 'outputs').read_text() + self.assertIn('package=' + str(self.root / 'second/Example.mpkg'), outputs) + self.assertIn('sha256=', outputs) + + @unittest.skipUnless(os.environ.get('MPKG_TEST_MPACK'), 'Set MPKG_TEST_MPACK to test the actual packager') + def test_real_mpack_rejects_symlink_payload(self): + (self.payload / 'bundle/external-link').symlink_to(self.manifest) + with self.assertRaises(subprocess.CalledProcessError): + package.create(self.manifest, self.payload, self.output, Path(os.environ['MPKG_TEST_MPACK'])) + self.assertFalse(self.output.exists()) + + +if __name__ == '__main__': + unittest.main() diff --git a/actions/build-mpkg/tool.Cargo.lock b/actions/build-mpkg/tool.Cargo.lock new file mode 100644 index 0000000..bf2d8f9 --- /dev/null +++ b/actions/build-mpkg/tool.Cargo.lock @@ -0,0 +1,544 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys", +] + +[[package]] +name = "anyhow" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + +[[package]] +name = "bitflags" +version = "2.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "cfg-if" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" + +[[package]] +name = "clap" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa8876b300ab35ba921adea3dfd70157a46249b33f95c9084ae5709785478946" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0797fb7aeb1406c84efac526901f7ec3ead2124f946b494e72879d4b54704d" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9c751b79415d4e559e3d1fcf128e09e720eb673a06d26cf6f392d37d75b66e0" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c133bc6a41be0d194c306b5506d15e6feeea7b1d6604bd3f8310dfb2ca96486" + +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01a7799fd6b852db0e61728dde9a204c423b44d689dbd432522543614b490e78" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "fastrand" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" + +[[package]] +name = "filetime" +version = "0.2.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759" +dependencies = [ + "cfg-if", + "libc", +] + +[[package]] +name = "flate2" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e634e2e0ebac1ee034020da1ca582e17ffe4e0f5e985823721e168928136dcb" +dependencies = [ + "crc32fast", + "miniz_oxide", + "zlib-rs", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "libc", + "r-efi", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "indexmap" +version = "2.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "libc" +version = "0.2.191" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65cd87c9a6d60536580b316ad985726104730a5151c99845f2490428ef91240a" + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "miniz_oxide" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b63fbc4a50860e98e7b2aa7804ded1db5cbc3aff9193adaff57a6931bf7c4b4c" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mpack" +version = "0.1.0" +dependencies = [ + "anyhow", + "clap", + "flate2", + "serde", + "sha2", + "tar", + "tempfile", + "toml", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rustix" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891efababe418670775f199f0d233d84843c227a0949a883ce15b37c78d6629d" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_spanned" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7523beb55eece201a2356bee0bbca0d1ab466c14c07703b2e0ee6d42cb0c2c" +dependencies = [ + "serde_core", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "simd-adler32" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "syn" +version = "3.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ae9ccbea3a7b0b09443036490c8f34a8b1b760fd345e5c0dae0aac718faea79" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tar" +version = "0.4.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f6221d9a6003c78398e3b239969f352578258df48c8eb051caadae0015bc840" +dependencies = [ + "filetime", + "libc", + "xattr", +] + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom", + "once_cell", + "rustix", + "windows-sys", +] + +[[package]] +name = "toml" +version = "1.1.8+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20489e00e4d8741d6be680764cc12e270655e375a20d1011e844a9c3379e678d" +dependencies = [ + "indexmap", + "serde_core", + "serde_spanned", + "toml_datetime", + "toml_parser", + "toml_writer", + "winnow", +] + +[[package]] +name = "toml_datetime" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b86d767906c6c42421dcba507eb9d203e779497710a47782a224bb871653053" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_parser" +version = "1.1.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baa693a8032d7e1cada7d0041e96126df243179ff061456783ac7f12bda4744c" +dependencies = [ + "winnow", +] + +[[package]] +name = "toml_writer" +version = "1.2.0+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "009dbb7700f11c55ef83aebbe4c0e9cf1f1ac0844bd8d6f00dc38f302026e13a" + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "unicode-ident" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "winnow" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" + +[[package]] +name = "xattr" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" +dependencies = [ + "libc", + "rustix", +] + +[[package]] +name = "zlib-rs" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b268e58e7c693d7c271f93ffc4ba3b380412554231c85bf61ca7af91042a4112"