Skip to main content

amiss_wire/
action.rs

1use crate::controls::{ConstraintPlatform, root};
2use crate::de::{self, Error, ErrorKind, Obj, fail};
3use crate::json::canonical;
4use crate::model::RepoPath;
5
6/// The root `action.yml`: JCS JSON plus LF, using JSON's YAML-compatible
7/// subset rather than a general YAML parser. Exactly `name`, `description`,
8/// and `runs`; `runs` is exactly `{"main": <RepoPath>, "using": "node20"}`.
9/// Anchors, aliases, tags, merge keys, `pre`, `post`, composite steps,
10/// containers, and any other field are unrepresentable.
11#[derive(Clone, Debug, PartialEq, Eq)]
12pub struct ActionMetadata {
13    pub name: String,
14    pub description: String,
15    pub main: RepoPath,
16}
17
18impl ActionMetadata {
19    /// Parses the metadata blob: the bytes must be exactly
20    /// `JCS(metadata) || LF`, so a reordered or reindented file is rejected
21    /// before its content is read.
22    ///
23    /// # Errors
24    ///
25    /// A noncanonical encoding, a missing trailing newline, or any shape
26    /// defect.
27    pub fn parse(bytes: &[u8]) -> Result<Self, Error> {
28        let trimmed = bytes
29            .strip_suffix(b"\n")
30            .ok_or_else(|| Error::new("$", ErrorKind::InvalidValue))?;
31        let value = root(trimmed)?;
32        if canonical(&value) != trimmed {
33            return fail("$", ErrorKind::InvalidValue);
34        }
35        let mut obj = Obj::new("$", value)?;
36        let name = de::string(&obj.field("name"), obj.take("name")?)?;
37        let description = de::string(&obj.field("description"), obj.take("description")?)?;
38        let runs_path = obj.field("runs");
39        let mut runs = Obj::new(&runs_path, obj.take("runs")?)?;
40        let main_path = runs.field("main");
41        let main = RepoPath::new(de::string(&main_path, runs.take("main")?)?)
42            .ok_or_else(|| Error::new(&main_path, ErrorKind::InvalidValue))?;
43        de::const_str(&runs.field("using"), runs.take("using")?, "node20")?;
44        runs.finish()?;
45        obj.finish()?;
46        if name.is_empty() || description.is_empty() {
47            return fail("$", ErrorKind::InvalidValue);
48        }
49        Ok(Self {
50            name,
51            description,
52            main,
53        })
54    }
55}
56
57/// The platform an executable's own header declares. The bootstrap derives
58/// the target this way, from the protected artifact bytes, never from an
59/// environment value or a candidate field, and requires equality with the
60/// constraint, the manifest row, and the sandbox verification.
61#[must_use]
62pub fn executable_platform(bytes: &[u8]) -> Option<ConstraintPlatform> {
63    elf_platform(bytes)
64        .or_else(|| mach_o_platform(bytes))
65        .or_else(|| pe_platform(bytes))
66}
67
68/// ELF: `7f 45 4c 46`, 64-bit little-endian only, with `e_machine` at offset
69/// 18 naming `x86-64` (0x3e) or `AArch64` (0xb7).
70fn elf_platform(bytes: &[u8]) -> Option<ConstraintPlatform> {
71    if bytes.get(..4) != Some(&[0x7f, b'E', b'L', b'F']) {
72        return None;
73    }
74    if bytes.get(4) != Some(&2) || bytes.get(5) != Some(&1) {
75        return None;
76    }
77    match bytes.get(18..20)? {
78        [0x3e, 0x00] => Some(ConstraintPlatform::LinuxX8664),
79        [0xb7, 0x00] => Some(ConstraintPlatform::LinuxAarch64),
80        _ => None,
81    }
82}
83
84/// Mach-O: the 64-bit little-endian magic `cf fa ed fe`, with `cputype` at
85/// offset 4 naming `x86-64` (0x01000007) or `ARM64` (0x0100000c).
86fn mach_o_platform(bytes: &[u8]) -> Option<ConstraintPlatform> {
87    if bytes.get(..4) != Some(&[0xcf, 0xfa, 0xed, 0xfe]) {
88        return None;
89    }
90    match bytes.get(4..8)? {
91        [0x07, 0x00, 0x00, 0x01] => Some(ConstraintPlatform::MacosX8664),
92        [0x0c, 0x00, 0x00, 0x01] => Some(ConstraintPlatform::MacosAarch64),
93        _ => None,
94    }
95}
96
97/// PE: `MZ`, the PE signature at the offset stored at `0x3c`, and the COFF
98/// machine field naming AMD64 (0x8664) or ARM64 (0xaa64).
99fn pe_platform(bytes: &[u8]) -> Option<ConstraintPlatform> {
100    if bytes.get(..2) != Some(b"MZ") {
101        return None;
102    }
103    let header = bytes.get(0x3c..0x40)?;
104    let [a, b, c, d] = header else { return None };
105    let offset = usize::try_from(u32::from_le_bytes([*a, *b, *c, *d])).ok()?;
106    if bytes.get(offset..offset.checked_add(4)?) != Some(b"PE\0\0") {
107        return None;
108    }
109    let machine = offset.checked_add(4)?;
110    match bytes.get(machine..machine.checked_add(2)?)? {
111        [0x64, 0x86] => Some(ConstraintPlatform::WindowsX8664),
112        [0x64, 0xaa] => Some(ConstraintPlatform::WindowsAarch64),
113        _ => None,
114    }
115}
116
117/// The platform of the running process, taken from the protected runtime's
118/// own build target rather than any ambient value. `None` where the closed
119/// six-value table has no row, which the bootstrap treats as unsupported.
120#[must_use]
121pub const fn host_platform() -> Option<ConstraintPlatform> {
122    match (
123        cfg!(target_os = "linux"),
124        cfg!(target_os = "macos"),
125        cfg!(target_os = "windows"),
126    ) {
127        (true, _, _) => arch(
128            ConstraintPlatform::LinuxX8664,
129            ConstraintPlatform::LinuxAarch64,
130        ),
131        (_, true, _) => arch(
132            ConstraintPlatform::MacosX8664,
133            ConstraintPlatform::MacosAarch64,
134        ),
135        (_, _, true) => arch(
136            ConstraintPlatform::WindowsX8664,
137            ConstraintPlatform::WindowsAarch64,
138        ),
139        _ => None,
140    }
141}
142
143const fn arch(
144    x86_64: ConstraintPlatform,
145    aarch64: ConstraintPlatform,
146) -> Option<ConstraintPlatform> {
147    if cfg!(target_arch = "x86_64") {
148        Some(x86_64)
149    } else if cfg!(target_arch = "aarch64") {
150        Some(aarch64)
151    } else {
152        None
153    }
154}