From 9d3f46f54d018dc8d952e8f106f2c9b622ffb3f0 Mon Sep 17 00:00:00 2001 From: tas0dev <156896971+tas0dev@users.noreply.github.com> Date: Mon, 9 Nov 2026 18:47:41 +0900 Subject: [PATCH 1/3] Add shared local crash windows and report history --- Cargo.lock | 5 +- crates/appcore/Cargo.toml | 1 + crates/appcore/src/diagnostics.rs | 234 ++++++++++++++++++++++++++++++ crates/appcore/src/lib.rs | 1 + 4 files changed, 239 insertions(+), 2 deletions(-) create mode 100644 crates/appcore/src/diagnostics.rs 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/crates/appcore/Cargo.toml b/crates/appcore/Cargo.toml index 6162fea..8ca8b73 100644 --- a/crates/appcore/Cargo.toml +++ b/crates/appcore/Cargo.toml @@ -20,6 +20,7 @@ mochios-filesystem-protocol = { path = "../../../../user/crates/filesystem-proto mochios-keychain-protocol = { path = "../../../../user/crates/keychain-protocol" } zeroize = { version = "1.8", features = ["alloc"] } toml = "1.1.2" +serde_json = "1" viewkit = { version = "2.0.0", git = "https://github.com/mochiOS/viewkit", rev = "768d9f903f9ae4bb7b6c861cd2d1cd3e8fbc4d95", optional = true } mochios-workspace-protocol = { path = "../../../../user/crates/workspace-protocol" } diff --git a/crates/appcore/src/diagnostics.rs b/crates/appcore/src/diagnostics.rs new file mode 100644 index 0000000..3f963c9 --- /dev/null +++ b/crates/appcore/src/diagnostics.rs @@ -0,0 +1,234 @@ +//! Local crash history and the shared crash presentation. Detailed reports +//! remain local; neither reading nor exporting a report grants upload consent. +use crate::{Error, Result}; +use serde_json::Value; + +#[derive(Clone, Debug)] +pub struct CrashSummary { + pub id: String, + pub package_id: Option, + pub name: String, + pub version: String, + pub utc_seconds: Option, +} + +impl CrashSummary { + fn decode(value: &Value) -> Result { + let id = value["id"] + .as_str() + .ok_or(Error::InvalidArgument)? + .to_owned(); + if !valid_report_id(&id) { + return Err(Error::InvalidArgument); + } + Ok(Self { + id, + package_id: value["package_id"].as_str().map(str::to_owned), + name: value["package_name"] + .as_str() + .filter(|name| !name.is_empty()) + .unwrap_or("Application") + .to_owned(), + version: value["package_version"] + .as_str() + .unwrap_or("Unknown") + .to_owned(), + utc_seconds: value["collected_utc_seconds"].as_u64(), + }) + } +} + +#[derive(Clone, Debug)] +pub struct CrashReport { + pub summary: CrashSummary, + pub details: String, + bytes: Vec, +} + +pub fn valid_report_id(id: &str) -> bool { + let Some(body) = id + .strip_prefix("crash-") + .and_then(|id| id.strip_suffix(".json")) + else { + return false; + }; + let bytes = body.as_bytes(); + bytes.len() == 37 + && bytes[16] == b'-' + && bytes[..16] + .iter() + .all(|b| b.is_ascii_hexdigit() && !b.is_ascii_uppercase()) + && bytes[17..].iter().all(u8::is_ascii_digit) +} + +pub fn history() -> Result> { + #[cfg(target_os = "mochios")] + { + let bytes = mochi_user_platform::diagnostic::reports::list()?; + let value: Value = serde_json::from_slice(&bytes).map_err(|_| Error::InvalidArgument)?; + if value["schema_version"] != 1 { + return Err(Error::InvalidArgument); + } + let reports = value["reports"] + .as_array() + .filter(|v| v.len() <= 128) + .ok_or(Error::InvalidArgument)?; + reports.iter().rev().map(CrashSummary::decode).collect() + } + #[cfg(not(target_os = "mochios"))] + { + Err(Error::UnsupportedPlatform) + } +} + +pub fn report(id: &str) -> Result { + if !valid_report_id(id) { + return Err(Error::InvalidArgument); + } + #[cfg(target_os = "mochios")] + { + let bytes = mochi_user_platform::diagnostic::reports::read(id)?; + let value: Value = serde_json::from_slice(&bytes).map_err(|_| Error::InvalidArgument)?; + if value["schema_version"] != 1 { + return Err(Error::InvalidArgument); + } + let image = &value["image"]; + let summary = CrashSummary::decode(&serde_json::json!({ + "id":id, "package_id":image["package_id"], "package_name":image["package_name"], + "package_version":image["package_version"], "collected_utc_seconds":value["collected_utc_seconds"], + }))?; + let mut details = format!( + "Application: {}\nVersion: {}\nPackage: {}\nProcess: {} / Thread: {}\nImage revision: {}\nArchitecture: {}\nException vector: {} / cause: {}\nInstruction: {}\nStack pointer: {}\nFault address: {}\nImage SHA-256: {}\n\n{}\n", + summary.name, + summary.version, + summary.package_id.as_deref().unwrap_or("Unavailable"), + value["process_id"], + value["thread_id"], + value["image_revision"], + value["architecture"], + value["exception"]["vector"], + value["exception"]["cause"], + value["exception"]["instruction"] + .as_str() + .unwrap_or("Unavailable"), + value["exception"]["stack_pointer"] + .as_str() + .unwrap_or("Unavailable"), + value["exception"]["address"] + .as_str() + .unwrap_or("Unavailable"), + value["image_sha256"].as_str().unwrap_or("Unavailable"), + value["os_version_toml"] + .as_str() + .unwrap_or("OS version unavailable") + ); + if let Some(modules) = image["modules"].as_array() { + details.push_str("Loaded images at launch:\n"); + for module in modules.iter().take(128) { + details.push_str(&format!( + "{}\n SHA-256: {}\n Load bias: {}\n", + module["path"].as_str().unwrap_or("Unavailable"), + module["source_sha256"].as_str().unwrap_or("Unavailable"), + module["load_bias"] + )); + } + } + details.push_str("\nLater dynamic loads and full stack traces are not captured.\nThis report has not been sent to mochiOS."); + Ok(CrashReport { + summary, + details, + bytes, + }) + } + #[cfg(not(target_os = "mochios"))] + { + Err(Error::UnsupportedPlatform) + } +} + +pub fn reopen(report: &CrashReport) -> Result<()> { + let package = report + .summary + .package_id + .as_deref() + .ok_or(Error::InvalidArgument)?; + #[cfg(target_os = "mochios")] + { + mochi_user_platform::workspace::launch_application(package)?; + Ok(()) + } + #[cfg(not(target_os = "mochios"))] + { + let _ = package; + Err(Error::UnsupportedPlatform) + } +} + +impl CrashReport { + /// Export only to the destination explicitly selected by the user. + pub fn export(&self, destination: &std::path::Path) -> std::io::Result<()> { + use std::io::Write; + let mut options = std::fs::OpenOptions::new(); + options.write(true).create(true).truncate(true); + #[cfg(unix)] + { + use std::os::unix::fs::OpenOptionsExt; + options.mode(0o600); + } + let mut file = options.open(destination)?; + file.write_all(&self.bytes)?; + file.sync_all() + } +} + +/// Standard ViewKit crash prompt shared by system diagnostic readers. +#[cfg(feature = "ui")] +pub fn crash_dialog( + report: &CrashReport, + count: usize, + reopen: impl FnMut() + 'static, + details: impl FnMut() + 'static, + close: impl FnMut() + 'static, +) -> Box { + use viewkit::prelude::*; + let message = if count > 1 { + format!( + "{count} applications stopped unexpectedly. The latest report is available in Details." + ) + } else { + "A local diagnostic report was saved. You can reopen the application or view its details." + .into() + }; + Box::new( + Dialog::new() + .accessibility_label("Application stopped unexpectedly") + .content( + VStack::new() + .gap(StackGap::Large) + .child( + HStack::new().child(Icon::new(SymbolName::Warning)).child( + Text::new(format!("{} quit unexpectedly", report.summary.name)) + .style(TextRole::TitleSmall), + ), + ) + .child(Text::new(message)) + .child( + Text::new("This report has not been sent to mochiOS.") + .tone(TextTone::Secondary), + ) + .child(Spacer::new()) + .child( + HStack::new() + .distribution(StackDistribution::End) + .child(Button::new("Close").on_click(close)) + .child(Button::new("Details").on_click(details)) + .child( + Button::new("Reopen") + .style(ButtonStyle::Primary) + .enabled(report.summary.package_id.is_some()) + .on_click(reopen), + ), + ), + ), + ) +} diff --git a/crates/appcore/src/lib.rs b/crates/appcore/src/lib.rs index b4e2a8e..7157256 100644 --- a/crates/appcore/src/lib.rs +++ b/crates/appcore/src/lib.rs @@ -14,6 +14,7 @@ pub mod content_type; pub mod control_center; pub mod status_item; pub mod document; +pub mod diagnostics; #[cfg(any(target_os = "linux", target_os = "mochios"))] pub mod worker; #[cfg(feature = "ui")] From db4024b0fab483c0f8658ae95a6f82a991a94413 Mon Sep 17 00:00:00 2001 From: tas0dev <156896971+tas0dev@users.noreply.github.com> Date: Wed, 11 Nov 2026 14:40:15 +0900 Subject: [PATCH 2/3] Add a publisher-neutral MPKG packaging Action --- .github/workflows/mpkg-action-check.yml | 68 +++ actions/build-mpkg/README.md | 82 ++++ actions/build-mpkg/action.yml | 42 ++ actions/build-mpkg/package.py | 121 +++++ actions/build-mpkg/tests/test_package.py | 115 +++++ actions/build-mpkg/tool.Cargo.lock | 544 +++++++++++++++++++++++ 6 files changed, 972 insertions(+) create mode 100644 .github/workflows/mpkg-action-check.yml create mode 100644 actions/build-mpkg/README.md create mode 100644 actions/build-mpkg/action.yml create mode 100644 actions/build-mpkg/package.py create mode 100644 actions/build-mpkg/tests/test_package.py create mode 100644 actions/build-mpkg/tool.Cargo.lock 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/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" From e8564ed59b40d864abb9f8698581207c38bc6698 Mon Sep 17 00:00:00 2001 From: tas0dev <156896971+tas0dev@users.noreply.github.com> Date: Wed, 11 Nov 2026 14:42:02 +0900 Subject: [PATCH 3/3] Exclude existing AppCore diagnostics work from the packaging Action branch --- crates/appcore/Cargo.toml | 1 - crates/appcore/src/diagnostics.rs | 234 ------------------------------ crates/appcore/src/lib.rs | 1 - 3 files changed, 236 deletions(-) delete mode 100644 crates/appcore/src/diagnostics.rs diff --git a/crates/appcore/Cargo.toml b/crates/appcore/Cargo.toml index 8ca8b73..6162fea 100644 --- a/crates/appcore/Cargo.toml +++ b/crates/appcore/Cargo.toml @@ -20,7 +20,6 @@ mochios-filesystem-protocol = { path = "../../../../user/crates/filesystem-proto mochios-keychain-protocol = { path = "../../../../user/crates/keychain-protocol" } zeroize = { version = "1.8", features = ["alloc"] } toml = "1.1.2" -serde_json = "1" viewkit = { version = "2.0.0", git = "https://github.com/mochiOS/viewkit", rev = "768d9f903f9ae4bb7b6c861cd2d1cd3e8fbc4d95", optional = true } mochios-workspace-protocol = { path = "../../../../user/crates/workspace-protocol" } diff --git a/crates/appcore/src/diagnostics.rs b/crates/appcore/src/diagnostics.rs deleted file mode 100644 index 3f963c9..0000000 --- a/crates/appcore/src/diagnostics.rs +++ /dev/null @@ -1,234 +0,0 @@ -//! Local crash history and the shared crash presentation. Detailed reports -//! remain local; neither reading nor exporting a report grants upload consent. -use crate::{Error, Result}; -use serde_json::Value; - -#[derive(Clone, Debug)] -pub struct CrashSummary { - pub id: String, - pub package_id: Option, - pub name: String, - pub version: String, - pub utc_seconds: Option, -} - -impl CrashSummary { - fn decode(value: &Value) -> Result { - let id = value["id"] - .as_str() - .ok_or(Error::InvalidArgument)? - .to_owned(); - if !valid_report_id(&id) { - return Err(Error::InvalidArgument); - } - Ok(Self { - id, - package_id: value["package_id"].as_str().map(str::to_owned), - name: value["package_name"] - .as_str() - .filter(|name| !name.is_empty()) - .unwrap_or("Application") - .to_owned(), - version: value["package_version"] - .as_str() - .unwrap_or("Unknown") - .to_owned(), - utc_seconds: value["collected_utc_seconds"].as_u64(), - }) - } -} - -#[derive(Clone, Debug)] -pub struct CrashReport { - pub summary: CrashSummary, - pub details: String, - bytes: Vec, -} - -pub fn valid_report_id(id: &str) -> bool { - let Some(body) = id - .strip_prefix("crash-") - .and_then(|id| id.strip_suffix(".json")) - else { - return false; - }; - let bytes = body.as_bytes(); - bytes.len() == 37 - && bytes[16] == b'-' - && bytes[..16] - .iter() - .all(|b| b.is_ascii_hexdigit() && !b.is_ascii_uppercase()) - && bytes[17..].iter().all(u8::is_ascii_digit) -} - -pub fn history() -> Result> { - #[cfg(target_os = "mochios")] - { - let bytes = mochi_user_platform::diagnostic::reports::list()?; - let value: Value = serde_json::from_slice(&bytes).map_err(|_| Error::InvalidArgument)?; - if value["schema_version"] != 1 { - return Err(Error::InvalidArgument); - } - let reports = value["reports"] - .as_array() - .filter(|v| v.len() <= 128) - .ok_or(Error::InvalidArgument)?; - reports.iter().rev().map(CrashSummary::decode).collect() - } - #[cfg(not(target_os = "mochios"))] - { - Err(Error::UnsupportedPlatform) - } -} - -pub fn report(id: &str) -> Result { - if !valid_report_id(id) { - return Err(Error::InvalidArgument); - } - #[cfg(target_os = "mochios")] - { - let bytes = mochi_user_platform::diagnostic::reports::read(id)?; - let value: Value = serde_json::from_slice(&bytes).map_err(|_| Error::InvalidArgument)?; - if value["schema_version"] != 1 { - return Err(Error::InvalidArgument); - } - let image = &value["image"]; - let summary = CrashSummary::decode(&serde_json::json!({ - "id":id, "package_id":image["package_id"], "package_name":image["package_name"], - "package_version":image["package_version"], "collected_utc_seconds":value["collected_utc_seconds"], - }))?; - let mut details = format!( - "Application: {}\nVersion: {}\nPackage: {}\nProcess: {} / Thread: {}\nImage revision: {}\nArchitecture: {}\nException vector: {} / cause: {}\nInstruction: {}\nStack pointer: {}\nFault address: {}\nImage SHA-256: {}\n\n{}\n", - summary.name, - summary.version, - summary.package_id.as_deref().unwrap_or("Unavailable"), - value["process_id"], - value["thread_id"], - value["image_revision"], - value["architecture"], - value["exception"]["vector"], - value["exception"]["cause"], - value["exception"]["instruction"] - .as_str() - .unwrap_or("Unavailable"), - value["exception"]["stack_pointer"] - .as_str() - .unwrap_or("Unavailable"), - value["exception"]["address"] - .as_str() - .unwrap_or("Unavailable"), - value["image_sha256"].as_str().unwrap_or("Unavailable"), - value["os_version_toml"] - .as_str() - .unwrap_or("OS version unavailable") - ); - if let Some(modules) = image["modules"].as_array() { - details.push_str("Loaded images at launch:\n"); - for module in modules.iter().take(128) { - details.push_str(&format!( - "{}\n SHA-256: {}\n Load bias: {}\n", - module["path"].as_str().unwrap_or("Unavailable"), - module["source_sha256"].as_str().unwrap_or("Unavailable"), - module["load_bias"] - )); - } - } - details.push_str("\nLater dynamic loads and full stack traces are not captured.\nThis report has not been sent to mochiOS."); - Ok(CrashReport { - summary, - details, - bytes, - }) - } - #[cfg(not(target_os = "mochios"))] - { - Err(Error::UnsupportedPlatform) - } -} - -pub fn reopen(report: &CrashReport) -> Result<()> { - let package = report - .summary - .package_id - .as_deref() - .ok_or(Error::InvalidArgument)?; - #[cfg(target_os = "mochios")] - { - mochi_user_platform::workspace::launch_application(package)?; - Ok(()) - } - #[cfg(not(target_os = "mochios"))] - { - let _ = package; - Err(Error::UnsupportedPlatform) - } -} - -impl CrashReport { - /// Export only to the destination explicitly selected by the user. - pub fn export(&self, destination: &std::path::Path) -> std::io::Result<()> { - use std::io::Write; - let mut options = std::fs::OpenOptions::new(); - options.write(true).create(true).truncate(true); - #[cfg(unix)] - { - use std::os::unix::fs::OpenOptionsExt; - options.mode(0o600); - } - let mut file = options.open(destination)?; - file.write_all(&self.bytes)?; - file.sync_all() - } -} - -/// Standard ViewKit crash prompt shared by system diagnostic readers. -#[cfg(feature = "ui")] -pub fn crash_dialog( - report: &CrashReport, - count: usize, - reopen: impl FnMut() + 'static, - details: impl FnMut() + 'static, - close: impl FnMut() + 'static, -) -> Box { - use viewkit::prelude::*; - let message = if count > 1 { - format!( - "{count} applications stopped unexpectedly. The latest report is available in Details." - ) - } else { - "A local diagnostic report was saved. You can reopen the application or view its details." - .into() - }; - Box::new( - Dialog::new() - .accessibility_label("Application stopped unexpectedly") - .content( - VStack::new() - .gap(StackGap::Large) - .child( - HStack::new().child(Icon::new(SymbolName::Warning)).child( - Text::new(format!("{} quit unexpectedly", report.summary.name)) - .style(TextRole::TitleSmall), - ), - ) - .child(Text::new(message)) - .child( - Text::new("This report has not been sent to mochiOS.") - .tone(TextTone::Secondary), - ) - .child(Spacer::new()) - .child( - HStack::new() - .distribution(StackDistribution::End) - .child(Button::new("Close").on_click(close)) - .child(Button::new("Details").on_click(details)) - .child( - Button::new("Reopen") - .style(ButtonStyle::Primary) - .enabled(report.summary.package_id.is_some()) - .on_click(reopen), - ), - ), - ), - ) -} diff --git a/crates/appcore/src/lib.rs b/crates/appcore/src/lib.rs index 7157256..b4e2a8e 100644 --- a/crates/appcore/src/lib.rs +++ b/crates/appcore/src/lib.rs @@ -14,7 +14,6 @@ pub mod content_type; pub mod control_center; pub mod status_item; pub mod document; -pub mod diagnostics; #[cfg(any(target_os = "linux", target_os = "mochios"))] pub mod worker; #[cfg(feature = "ui")]