Phase 6: Native Executables implementieren und Change archivieren
This commit is contained in:
9
crates/tb-export/Cargo.toml
Normal file
9
crates/tb-export/Cargo.toml
Normal file
@@ -0,0 +1,9 @@
|
||||
[package]
|
||||
name = "tb-export"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
[dependencies]
|
||||
tb-vm.workspace = true
|
||||
anyhow.workspace = true
|
||||
12
crates/tb-export/src/bin/tb-template.rs
Normal file
12
crates/tb-export/src/bin/tb-template.rs
Normal file
@@ -0,0 +1,12 @@
|
||||
//! Builder-Hilfe: geprüfte Metadaten für eine frisch gebaute Runtime schreiben.
|
||||
fn main() -> anyhow::Result<()> {
|
||||
let args: Vec<_> = std::env::args().skip(1).collect();
|
||||
anyhow::ensure!(
|
||||
args.len() == 2,
|
||||
"Aufruf: tb-template <tbrt-Pfad> <Target-Triple>"
|
||||
);
|
||||
tb_export::prepare_template(
|
||||
std::path::Path::new(&args[0]),
|
||||
tb_export::Target::parse(&args[1])?,
|
||||
)
|
||||
}
|
||||
514
crates/tb-export/src/lib.rs
Normal file
514
crates/tb-export/src/lib.rs
Normal file
@@ -0,0 +1,514 @@
|
||||
//! Geprüfte Runtime-Vorlagen, Nutzlastcontainer und geschützte Veröffentlichung.
|
||||
use anyhow::{bail, ensure, Context, Result};
|
||||
use std::{
|
||||
fs,
|
||||
path::{Path, PathBuf},
|
||||
sync::atomic::{AtomicU64, Ordering},
|
||||
};
|
||||
use tb_vm::bytecode::{CompiledModule, TBC_VERSION};
|
||||
|
||||
pub const RUNTIME_VERSION: u32 = 1;
|
||||
const CONTAINER_VERSION: u32 = 1;
|
||||
const MAGIC: &[u8; 8] = b"TBPCODE!";
|
||||
const FOOTER: usize = 48;
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
#[repr(u32)]
|
||||
pub enum Target {
|
||||
WindowsAmd64 = 1,
|
||||
MacosArm64 = 2,
|
||||
LinuxAmd64 = 3,
|
||||
LinuxArm64 = 4,
|
||||
}
|
||||
impl Target {
|
||||
pub const ALL: [Self; 4] = [
|
||||
Self::WindowsAmd64,
|
||||
Self::MacosArm64,
|
||||
Self::LinuxAmd64,
|
||||
Self::LinuxArm64,
|
||||
];
|
||||
pub fn triple(self) -> &'static str {
|
||||
match self {
|
||||
Self::WindowsAmd64 => "x86_64-pc-windows-msvc",
|
||||
Self::MacosArm64 => "aarch64-apple-darwin",
|
||||
Self::LinuxAmd64 => "x86_64-unknown-linux-gnu",
|
||||
Self::LinuxArm64 => "aarch64-unknown-linux-gnu",
|
||||
}
|
||||
}
|
||||
pub fn parse(s: &str) -> Result<Self> {
|
||||
Self::ALL
|
||||
.into_iter()
|
||||
.find(|t| t.triple() == s)
|
||||
.with_context(|| format!("Nicht unterstütztes Target: {s}"))
|
||||
}
|
||||
pub fn host() -> Result<Self> {
|
||||
match (std::env::consts::OS, std::env::consts::ARCH) {
|
||||
("windows", "x86_64") => Ok(Self::WindowsAmd64),
|
||||
("macos", "aarch64") => Ok(Self::MacosArm64),
|
||||
("linux", "x86_64") => Ok(Self::LinuxAmd64),
|
||||
("linux", "aarch64") => Ok(Self::LinuxArm64),
|
||||
other => bail!("Kein unterstütztes Host-Target: {other:?}"),
|
||||
}
|
||||
}
|
||||
pub fn runtime_name(self) -> &'static str {
|
||||
if self == Self::WindowsAmd64 {
|
||||
"tbrt.exe"
|
||||
} else {
|
||||
"tbrt"
|
||||
}
|
||||
}
|
||||
}
|
||||
fn part(b: &[u8], at: usize, len: usize) -> Result<&[u8]> {
|
||||
b.get(at..at.checked_add(len).context("Längenüberlauf")?)
|
||||
.context("Abgeschnittenes natives Format/Nutzlast")
|
||||
}
|
||||
fn u16_at(b: &[u8], at: usize) -> Result<u16> {
|
||||
Ok(u16::from_le_bytes(part(b, at, 2)?.try_into()?))
|
||||
}
|
||||
fn u32_at(b: &[u8], at: usize) -> Result<u32> {
|
||||
Ok(u32::from_le_bytes(part(b, at, 4)?.try_into()?))
|
||||
}
|
||||
fn u64_at(b: &[u8], at: usize) -> Result<u64> {
|
||||
Ok(u64::from_le_bytes(part(b, at, 8)?.try_into()?))
|
||||
}
|
||||
fn size_at(b: &[u8], at: usize) -> Result<usize> {
|
||||
usize::try_from(u64_at(b, at)?).context("Nutzlastlänge nicht darstellbar")
|
||||
}
|
||||
fn put64(b: &mut [u8], at: usize, n: usize) {
|
||||
b[at..at + 8].copy_from_slice(&(n as u64).to_le_bytes());
|
||||
}
|
||||
|
||||
/// FNV-1a-64 gegen Übertragungs-/Dateibeschädigung; keine Herkunftsauthentisierung.
|
||||
pub fn checksum(b: &[u8]) -> u64 {
|
||||
b.iter().fold(0xcbf29ce484222325, |h, v| {
|
||||
(h ^ u64::from(*v)).wrapping_mul(0x100000001b3)
|
||||
})
|
||||
}
|
||||
|
||||
fn macho_commands(b: &[u8]) -> Result<Vec<(u32, usize)>> {
|
||||
let end = 32usize
|
||||
.checked_add(u32_at(b, 20)? as usize)
|
||||
.context("Mach-O-Headerlänge")?;
|
||||
part(b, 0, end)?;
|
||||
let mut at = 32;
|
||||
let mut commands = Vec::new();
|
||||
for _ in 0..u32_at(b, 16)? {
|
||||
ensure!(at + 8 <= end, "Ungültige Mach-O-Ladekommandos");
|
||||
let cmd = u32_at(b, at)?;
|
||||
let len = u32_at(b, at + 4)? as usize;
|
||||
ensure!(
|
||||
len >= 8 && len.is_multiple_of(8) && len <= end - at,
|
||||
"Ungültige Mach-O-Kommandolänge"
|
||||
);
|
||||
commands.push((cmd, at));
|
||||
at += len;
|
||||
}
|
||||
ensure!(at == end, "Mach-O-Kommandotabelle unvollständig");
|
||||
Ok(commands)
|
||||
}
|
||||
|
||||
/// Format-/Architekturprüfung; synthetische Header sind kein Zielausführungsnachweis.
|
||||
pub fn native_target(b: &[u8]) -> Result<Target> {
|
||||
part(b, 0, 64)?;
|
||||
if b.starts_with(b"MZ") {
|
||||
let pe = u32_at(b, 0x3c)? as usize;
|
||||
ensure!(part(b, pe, 4)? == b"PE\0\0", "Ungültiges PE-Format");
|
||||
ensure!(
|
||||
u16_at(b, pe + 4)? == 0x8664,
|
||||
"PE-Architektur muss amd64 sein"
|
||||
);
|
||||
ensure!(
|
||||
u16_at(b, pe + 24)? == 0x20b && u16_at(b, pe + 92)? == 3,
|
||||
"PE muss PE32+ Terminalanwendung sein"
|
||||
);
|
||||
ensure!(
|
||||
u16_at(b, pe + 22)? & 0x2002 == 2,
|
||||
"PE muss Executable statt DLL sein"
|
||||
);
|
||||
return Ok(Target::WindowsAmd64);
|
||||
}
|
||||
if b.starts_with(b"\x7fELF") {
|
||||
ensure!(
|
||||
matches!(b[7], 0 | 3),
|
||||
"ELF-System-ABI muss System V/Linux sein"
|
||||
);
|
||||
ensure!(
|
||||
part(b, 4, 3)? == [2, 1, 1],
|
||||
"ELF muss 64-Bit Little Endian sein"
|
||||
);
|
||||
ensure!(matches!(u16_at(b, 16)?, 2 | 3), "ELF muss ausführbar sein");
|
||||
part(b, 0, 64)?;
|
||||
return match u16_at(b, 18)? {
|
||||
62 => Ok(Target::LinuxAmd64),
|
||||
183 => Ok(Target::LinuxArm64),
|
||||
_ => bail!("Nicht unterstützte ELF-Architektur"),
|
||||
};
|
||||
}
|
||||
if b.starts_with(&[0xcf, 0xfa, 0xed, 0xfe]) {
|
||||
ensure!(
|
||||
u32_at(b, 4)? == 0x0100000c,
|
||||
"Mach-O-Architektur muss arm64 sein"
|
||||
);
|
||||
ensure!(u32_at(b, 12)? == 2, "Mach-O muss Executable sein");
|
||||
macho_commands(b)?;
|
||||
return Ok(Target::MacosArm64);
|
||||
}
|
||||
bail!("Unbekanntes natives Zielformat (PE/ELF/Mach-O erwartet)")
|
||||
}
|
||||
|
||||
/// Mach-O-Signaturen folgen der Nutzlast. Ihr Ladekommando ist die stabile Grenze.
|
||||
fn content_end(b: &[u8], target: Target) -> Result<usize> {
|
||||
if target == Target::MacosArm64 {
|
||||
let signatures: Vec<_> = macho_commands(b)?
|
||||
.into_iter()
|
||||
.filter(|(cmd, _)| *cmd == 0x1d)
|
||||
.collect();
|
||||
ensure!(signatures.len() <= 1, "Doppelte Mach-O-Signatur");
|
||||
if let Some((_, at)) = signatures.first() {
|
||||
ensure!(u32_at(b, at + 4)? == 16, "Ungültiges Signaturkommando");
|
||||
let off = u32_at(b, at + 8)? as usize;
|
||||
let len = u32_at(b, at + 12)? as usize;
|
||||
part(b, off, len)?;
|
||||
ensure!(off + len == b.len(), "Daten hinter Mach-O-Signatur");
|
||||
return Ok(off);
|
||||
}
|
||||
}
|
||||
Ok(b.len())
|
||||
}
|
||||
|
||||
pub fn embedded(b: &[u8], expected: Target) -> Result<CompiledModule> {
|
||||
ensure!(
|
||||
native_target(b)? == expected,
|
||||
"Executable-Target passt nicht zum Host"
|
||||
);
|
||||
let end = content_end(b, expected)?;
|
||||
// codesign richtet die Signatur auf 16 Bytes aus; ausschließlich Nullpadding zulassen.
|
||||
let footer = (0..=15)
|
||||
.find_map(|padding| {
|
||||
let at = end.checked_sub(FOOTER + padding)?;
|
||||
(b.get(at..at + 8) == Some(MAGIC) && b[at + FOOTER..end].iter().all(|v| *v == 0))
|
||||
.then_some(at)
|
||||
})
|
||||
.context("Fehlende oder beschädigte eingebettete Nutzlast (leere tbrt-Vorlage)")?;
|
||||
ensure!(
|
||||
u32_at(b, footer + 8)? == CONTAINER_VERSION,
|
||||
"Inkompatible Container-Version"
|
||||
);
|
||||
ensure!(
|
||||
u32_at(b, footer + 12)? == RUNTIME_VERSION,
|
||||
"Inkompatible Runtime-Version"
|
||||
);
|
||||
ensure!(
|
||||
u32_at(b, footer + 16)? == u32::from(TBC_VERSION),
|
||||
"Inkompatible TBC-Version"
|
||||
);
|
||||
ensure!(
|
||||
u32_at(b, footer + 20)? == expected as u32,
|
||||
"Nutzlast-Target passt nicht zum Executable"
|
||||
);
|
||||
let offset = size_at(b, footer + 24)?;
|
||||
let len = size_at(b, footer + 32)?;
|
||||
ensure!(
|
||||
offset >= 64 && offset <= footer && len == footer - offset,
|
||||
"Ungültige Nutzlastgrenzen"
|
||||
);
|
||||
let payload = part(b, offset, len)?;
|
||||
ensure!(
|
||||
checksum(payload) == u64_at(b, footer + 40)?,
|
||||
"Nutzlast-Prüfsumme stimmt nicht"
|
||||
);
|
||||
CompiledModule::from_tbc(payload)
|
||||
.map_err(|e| anyhow::anyhow!("Eingebettetes Programm nicht ladbar: {e}"))
|
||||
}
|
||||
|
||||
/// Im nativen Runner vorhanden; verhindert Verwechslung mit beliebigen Executables.
|
||||
pub const fn runtime_marker(target: Target) -> [u8; 32] {
|
||||
let mut marker = [0; 32];
|
||||
let magic = *b"TBRT-RUNTIME-v1!";
|
||||
let mut i = 0;
|
||||
while i < 16 {
|
||||
marker[i] = magic[i];
|
||||
i += 1;
|
||||
}
|
||||
let values = [
|
||||
CONTAINER_VERSION,
|
||||
RUNTIME_VERSION,
|
||||
TBC_VERSION as u32,
|
||||
target as u32,
|
||||
];
|
||||
i = 0;
|
||||
while i < 4 {
|
||||
let bytes = values[i].to_le_bytes();
|
||||
let mut j = 0;
|
||||
while j < 4 {
|
||||
marker[16 + i * 4 + j] = bytes[j];
|
||||
j += 1;
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
marker
|
||||
}
|
||||
fn check_runner(b: &[u8], target: Target) -> Result<()> {
|
||||
ensure!(
|
||||
b.windows(32).any(|w| w == runtime_marker(target)),
|
||||
"Keine kompatible tbrt-Runtime-Vorlage (Runtime-/Format-/Target-Markierung fehlt)"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn manifest(b: &[u8], target: Target) -> String {
|
||||
format!("TBRT-TEMPLATE 1\nruntime={RUNTIME_VERSION}\npackage={}\ntbc={TBC_VERSION}\ntarget={}\nchecksum={:016x}\n",env!("CARGO_PKG_VERSION"),target.triple(),checksum(b))
|
||||
}
|
||||
pub fn manifest_path(path: &Path) -> PathBuf {
|
||||
let mut p = path.as_os_str().to_owned();
|
||||
p.push(".meta");
|
||||
PathBuf::from(p)
|
||||
}
|
||||
/// Für den Runtime-Builder; kein impliziter Build beim Export.
|
||||
pub fn prepare_template(path: &Path, target: Target) -> Result<()> {
|
||||
let b = fs::read(path)?;
|
||||
check_runner(&b, target)?;
|
||||
ensure!(
|
||||
native_target(&b)? == target,
|
||||
"Vorlagen-Zielformat/Architektur widerspricht {}",
|
||||
target.triple()
|
||||
);
|
||||
ensure!(
|
||||
embedded(&b, target).is_err(),
|
||||
"Bereits eingebettetes Programm ist keine leere Vorlage"
|
||||
);
|
||||
publish(
|
||||
&manifest_path(path),
|
||||
false,
|
||||
&[path.to_path_buf()],
|
||||
&|| false,
|
||||
|temp| {
|
||||
fs::write(temp, manifest(&b, target))?;
|
||||
Ok(())
|
||||
},
|
||||
)
|
||||
}
|
||||
fn template(path: &Path, target: Target) -> Result<Vec<u8>> {
|
||||
let b = fs::read(path).with_context(|| format!("Runtime-Vorlage fehlt: {}", path.display()))?;
|
||||
ensure!(
|
||||
native_target(&b)? == target,
|
||||
"Vorlagen-Zielformat/Architektur passt nicht zu {}",
|
||||
target.triple()
|
||||
);
|
||||
let found = fs::read_to_string(manifest_path(path)).context("Vorlagenmetadaten fehlen")?;
|
||||
check_runner(&b, target)?;
|
||||
let want = manifest(&b, target);
|
||||
for (index, (a, z)) in found.lines().zip(want.lines()).enumerate() {
|
||||
ensure!(
|
||||
a == z,
|
||||
"Inkompatible Vorlagenmetadaten, Zeile {}: Soll {z}, Ist {a}",
|
||||
index + 1
|
||||
);
|
||||
}
|
||||
ensure!(
|
||||
found == want,
|
||||
"Unvollständige Vorlagenmetadaten oder Integritätsdaten"
|
||||
);
|
||||
Ok(b)
|
||||
}
|
||||
|
||||
static NEXT: AtomicU64 = AtomicU64::new(0);
|
||||
struct Temporary(PathBuf);
|
||||
impl Drop for Temporary {
|
||||
fn drop(&mut self) {
|
||||
let _ = fs::remove_file(&self.0);
|
||||
}
|
||||
}
|
||||
fn destination(path: &Path, protected: &[PathBuf]) -> Result<Option<Vec<u8>>> {
|
||||
let key = tb_vm::project_io::identity(path)?;
|
||||
for source in protected {
|
||||
ensure!(
|
||||
tb_vm::project_io::identity(source)? != key,
|
||||
"Ausgabe würde Projektdaten/Vorlage überschreiben: {}",
|
||||
path.display()
|
||||
);
|
||||
}
|
||||
match fs::symlink_metadata(path) {
|
||||
Ok(meta) => {
|
||||
ensure!(
|
||||
meta.is_file() && !meta.permissions().readonly(),
|
||||
"Ziel ist keine beschreibbare reguläre Datei: {}",
|
||||
path.display()
|
||||
);
|
||||
Ok(Some(fs::read(path)?))
|
||||
}
|
||||
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
|
||||
Err(e) => Err(e.into()),
|
||||
}
|
||||
}
|
||||
/// No-clobber per hard_link; Ersetzen nur nach expliziter Freigabe und unverändertem Ziel.
|
||||
/// prepare darf finalisieren; cancellation wird vor Arbeit und Veröffentlichung geprüft.
|
||||
pub fn publish(
|
||||
path: &Path,
|
||||
overwrite: bool,
|
||||
protected: &[PathBuf],
|
||||
cancelled: &dyn Fn() -> bool,
|
||||
prepare: impl FnOnce(&Path) -> Result<()>,
|
||||
) -> Result<()> {
|
||||
ensure!(!cancelled(), "Export abgebrochen");
|
||||
let initial = destination(path, protected)?;
|
||||
ensure!(
|
||||
initial.is_none() || overwrite,
|
||||
"Ziel existiert; --force zum Ersetzen: {}",
|
||||
path.display()
|
||||
);
|
||||
let parent = path
|
||||
.parent()
|
||||
.filter(|p| !p.as_os_str().is_empty())
|
||||
.unwrap_or(Path::new("."));
|
||||
let temporary = loop {
|
||||
let p = parent.join(format!(
|
||||
".tb-export-{}-{}.tmp",
|
||||
std::process::id(),
|
||||
NEXT.fetch_add(1, Ordering::Relaxed)
|
||||
));
|
||||
match fs::OpenOptions::new().write(true).create_new(true).open(&p) {
|
||||
Ok(_) => break Temporary(p),
|
||||
Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => continue,
|
||||
Err(e) => return Err(e.into()),
|
||||
}
|
||||
};
|
||||
prepare(&temporary.0)?;
|
||||
fs::File::open(&temporary.0)?.sync_all()?;
|
||||
ensure!(!cancelled(), "Export abgebrochen");
|
||||
ensure!(
|
||||
destination(path, protected)? == initial,
|
||||
"Ziel wurde während des Exports verändert"
|
||||
);
|
||||
if initial.is_some() {
|
||||
fs::rename(&temporary.0, path)?;
|
||||
} else {
|
||||
fs::hard_link(&temporary.0, path)
|
||||
.context("Ziel inzwischen belegt oder Veröffentlichung nicht möglich")?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn codesign(path: &Path, args: &[&str]) -> Result<()> {
|
||||
ensure!(
|
||||
cfg!(target_os = "macos"),
|
||||
"macOS-Finalisierung benötigt macOS mit /usr/bin/codesign"
|
||||
);
|
||||
let result = std::process::Command::new("/usr/bin/codesign")
|
||||
.args(args)
|
||||
.arg(path)
|
||||
.output()
|
||||
.context("Finalisierung: /usr/bin/codesign fehlt oder ist nicht ausführbar")?;
|
||||
ensure!(
|
||||
result.status.success(),
|
||||
"Finalisierung fehlgeschlagen: {}",
|
||||
String::from_utf8_lossy(&result.stderr)
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
fn append_payload(b: &mut Vec<u8>, module: &CompiledModule, target: Target) -> Result<()> {
|
||||
module.validate().map_err(|e| anyhow::anyhow!("{e}"))?;
|
||||
if target == Target::WindowsAmd64 {
|
||||
let pe = u32_at(b, 0x3c)? as usize;
|
||||
// Eine bestehende Authenticode-Signatur wäre nach dem Einbetten ungültig.
|
||||
if u32_at(b, pe + 132)? > 4 {
|
||||
ensure!(
|
||||
part(b, pe + 168, 8)?.iter().all(|v| *v == 0),
|
||||
"Signierte PE-Vorlagen werden nicht unterstützt; unsigned tbrt benötigt"
|
||||
);
|
||||
}
|
||||
// Für Benutzerprogramme optional; keine veraltete PE-Prüfsumme übernehmen.
|
||||
b[pe + 88..pe + 92].fill(0);
|
||||
}
|
||||
let payload = module.to_tbc();
|
||||
let offset = b.len();
|
||||
b.extend_from_slice(&payload);
|
||||
b.extend_from_slice(MAGIC);
|
||||
for n in [
|
||||
CONTAINER_VERSION,
|
||||
RUNTIME_VERSION,
|
||||
u32::from(TBC_VERSION),
|
||||
target as u32,
|
||||
] {
|
||||
b.extend_from_slice(&n.to_le_bytes());
|
||||
}
|
||||
for n in [offset as u64, payload.len() as u64, checksum(&payload)] {
|
||||
b.extend_from_slice(&n.to_le_bytes());
|
||||
}
|
||||
if target == Target::MacosArm64 {
|
||||
let link = macho_commands(b)?
|
||||
.into_iter()
|
||||
.find(|(cmd, at)| {
|
||||
*cmd == 0x19 && b.get(at + 8..at + 24) == Some(b"__LINKEDIT\0\0\0\0\0\0")
|
||||
})
|
||||
.context("Mach-O __LINKEDIT fehlt")?
|
||||
.1;
|
||||
ensure!(
|
||||
u32_at(b, link + 4)? as usize >= 72,
|
||||
"Mach-O-Segment zu kurz"
|
||||
);
|
||||
let start = size_at(b, link + 40)?;
|
||||
ensure!(start <= offset, "Ungültige __LINKEDIT-Grenze");
|
||||
let len = b.len() - start;
|
||||
put64(b, link + 48, len);
|
||||
put64(
|
||||
b,
|
||||
link + 32,
|
||||
len.checked_add(16383).context("Mach-O-Länge")? & !16383,
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub struct Export<'a> {
|
||||
pub module: &'a CompiledModule,
|
||||
pub target: Target,
|
||||
pub template: &'a Path,
|
||||
pub output: &'a Path,
|
||||
pub overwrite: bool,
|
||||
pub protected: &'a [PathBuf],
|
||||
}
|
||||
pub fn export(request: Export<'_>, cancelled: &dyn Fn() -> bool) -> Result<()> {
|
||||
let mut b = template(request.template, request.target)?;
|
||||
let mut protected = request.protected.to_vec();
|
||||
protected.extend([
|
||||
request.template.to_path_buf(),
|
||||
manifest_path(request.template),
|
||||
]);
|
||||
protected.extend(
|
||||
request
|
||||
.module
|
||||
.sources
|
||||
.iter()
|
||||
.map(|s| PathBuf::from(&s.path)),
|
||||
);
|
||||
publish(
|
||||
request.output,
|
||||
request.overwrite,
|
||||
&protected,
|
||||
cancelled,
|
||||
|temp| {
|
||||
if request.target == Target::MacosArm64 {
|
||||
fs::write(temp, &b)?;
|
||||
codesign(temp, &["--remove-signature"])?;
|
||||
b = fs::read(temp)?;
|
||||
}
|
||||
append_payload(&mut b, request.module, request.target)?;
|
||||
fs::write(temp, &b)?;
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
fs::set_permissions(temp, fs::Permissions::from_mode(0o755))?;
|
||||
}
|
||||
if request.target == Target::MacosArm64 {
|
||||
codesign(temp, &["--force", "--sign", "-", "--timestamp=none"])?;
|
||||
codesign(temp, &["--verify", "--strict"])?;
|
||||
}
|
||||
embedded(&fs::read(temp)?, request.target)?;
|
||||
Ok(())
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
226
crates/tb-export/src/tests.rs
Normal file
226
crates/tb-export/src/tests.rs
Normal file
@@ -0,0 +1,226 @@
|
||||
use super::*;
|
||||
use std::cell::Cell;
|
||||
|
||||
fn header(t: Target) -> Vec<u8> {
|
||||
let mut b = vec![0; 256];
|
||||
match t {
|
||||
Target::WindowsAmd64 => {
|
||||
b[..2].copy_from_slice(b"MZ");
|
||||
b[0x3c..0x40].copy_from_slice(&64u32.to_le_bytes());
|
||||
b[64..68].copy_from_slice(b"PE\0\0");
|
||||
b[68..70].copy_from_slice(&0x8664u16.to_le_bytes());
|
||||
b[86..88].copy_from_slice(&2u16.to_le_bytes());
|
||||
b[88..90].copy_from_slice(&0x20bu16.to_le_bytes());
|
||||
b[156..158].copy_from_slice(&3u16.to_le_bytes());
|
||||
}
|
||||
Target::MacosArm64 => {
|
||||
b[..4].copy_from_slice(&[0xcf, 0xfa, 0xed, 0xfe]);
|
||||
b[4..8].copy_from_slice(&0x0100000cu32.to_le_bytes());
|
||||
b[12..16].copy_from_slice(&2u32.to_le_bytes());
|
||||
}
|
||||
Target::LinuxAmd64 | Target::LinuxArm64 => {
|
||||
b[..7].copy_from_slice(b"\x7fELF\x02\x01\x01");
|
||||
b[16..18].copy_from_slice(&2u16.to_le_bytes());
|
||||
let cpu: u16 = if t == Target::LinuxAmd64 { 62 } else { 183 };
|
||||
b[18..20].copy_from_slice(&cpu.to_le_bytes());
|
||||
}
|
||||
}
|
||||
b.extend_from_slice(&runtime_marker(t));
|
||||
b
|
||||
}
|
||||
struct Dir(PathBuf);
|
||||
impl Dir {
|
||||
fn new() -> Self {
|
||||
let p = std::env::temp_dir().join(format!(
|
||||
"tb-export-test-{}-{}",
|
||||
std::process::id(),
|
||||
NEXT.fetch_add(1, Ordering::Relaxed)
|
||||
));
|
||||
fs::create_dir(&p).unwrap();
|
||||
Self(p)
|
||||
}
|
||||
}
|
||||
impl Drop for Dir {
|
||||
fn drop(&mut self) {
|
||||
fs::remove_dir_all(&self.0).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn targets_versions_integrity_and_payload_bounds_are_enforced() {
|
||||
let d = Dir::new();
|
||||
for t in Target::ALL {
|
||||
let b = header(t);
|
||||
assert_eq!(native_target(&b).unwrap(), t);
|
||||
let path = d.0.join(t.runtime_name());
|
||||
fs::write(&path, &b).unwrap();
|
||||
prepare_template(&path, t).unwrap();
|
||||
assert_eq!(template(&path, t).unwrap(), b);
|
||||
let other = if t == Target::LinuxAmd64 {
|
||||
Target::LinuxArm64
|
||||
} else {
|
||||
Target::LinuxAmd64
|
||||
};
|
||||
assert!(template(&path, other)
|
||||
.unwrap_err()
|
||||
.to_string()
|
||||
.contains("Architektur"));
|
||||
let meta = manifest(&b, t);
|
||||
for wrong in [
|
||||
meta.replace("runtime=1", "runtime=2"),
|
||||
meta.replace("package=0.1.0", "package=9"),
|
||||
meta.replace("tbc=4", "tbc=99"),
|
||||
meta.replace("checksum=", "checksum=0"),
|
||||
] {
|
||||
fs::write(manifest_path(&path), wrong).unwrap();
|
||||
assert!(template(&path, t).is_err());
|
||||
}
|
||||
fs::remove_file(manifest_path(&path)).unwrap();
|
||||
assert!(template(&path, t).is_err());
|
||||
fs::remove_file(&path).unwrap();
|
||||
assert!(template(&path, t)
|
||||
.unwrap_err()
|
||||
.to_string()
|
||||
.contains("fehlt"));
|
||||
for len in 0..64 {
|
||||
assert!(native_target(&b[..len]).is_err());
|
||||
}
|
||||
}
|
||||
assert!(Target::parse("aarch64-pc-windows-msvc").is_err());
|
||||
assert!(Target::parse("x86_64-apple-darwin").is_err());
|
||||
let mut foreign_abi = header(Target::LinuxAmd64);
|
||||
foreign_abi[7] = 9;
|
||||
assert!(native_target(&foreign_abi).is_err());
|
||||
let module = tb_vm::compile_source("TEST", "PRINT 42\nEND\n").unwrap();
|
||||
for t in [Target::WindowsAmd64, Target::LinuxAmd64, Target::LinuxArm64] {
|
||||
let mut b = header(t);
|
||||
if t == Target::WindowsAmd64 {
|
||||
let mut signed = b.clone();
|
||||
signed[196..200].copy_from_slice(&5u32.to_le_bytes());
|
||||
signed[232] = 1;
|
||||
assert!(append_payload(&mut signed, &module, t)
|
||||
.unwrap_err()
|
||||
.to_string()
|
||||
.contains("Signierte PE"));
|
||||
b[152..156].copy_from_slice(&123u32.to_le_bytes());
|
||||
}
|
||||
append_payload(&mut b, &module, t).unwrap();
|
||||
if t == Target::WindowsAmd64 {
|
||||
assert_eq!(&b[152..156], &[0; 4]);
|
||||
}
|
||||
assert_eq!(embedded(&b, t).unwrap().to_tbc(), module.to_tbc());
|
||||
let at = b.len() - FOOTER;
|
||||
for field in [8, 12, 16, 20, 24, 32, 40] {
|
||||
let mut bad = b.clone();
|
||||
bad[at + field] ^= 0xff;
|
||||
assert!(embedded(&bad, t).is_err(), "{field}");
|
||||
}
|
||||
for cut in 1..=FOOTER + 1 {
|
||||
assert!(embedded(&b[..b.len() - cut], t).is_err());
|
||||
}
|
||||
let mut bad = b.clone();
|
||||
bad[300] ^= 1;
|
||||
assert!(embedded(&bad, t).is_err());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn publication_preserves_originals_on_conflict_failure_race_and_cancel() {
|
||||
let d = Dir::new();
|
||||
let out = d.0.join("result");
|
||||
let source = d.0.join("source.bas");
|
||||
fs::write(&source, b"source").unwrap();
|
||||
fs::write(&out, b"old").unwrap();
|
||||
assert!(publish(&out, false, &[], &|| false, |_| panic!(
|
||||
"darf nicht schreiben"
|
||||
))
|
||||
.is_err());
|
||||
assert!(publish(
|
||||
&source,
|
||||
true,
|
||||
std::slice::from_ref(&source),
|
||||
&|| false,
|
||||
|_| panic!("Projektschutz")
|
||||
)
|
||||
.is_err());
|
||||
assert!(publish(&out, true, &[], &|| false, |p| {
|
||||
fs::write(p, b"new")?;
|
||||
bail!("Finalisierungsfehler")
|
||||
})
|
||||
.is_err());
|
||||
assert_eq!(fs::read(&out).unwrap(), b"old");
|
||||
let cancel = Cell::new(false);
|
||||
assert!(publish(&out, true, &[], &|| cancel.get(), |p| {
|
||||
fs::write(p, b"new")?;
|
||||
cancel.set(true);
|
||||
Ok(())
|
||||
})
|
||||
.is_err());
|
||||
assert_eq!(fs::read(&out).unwrap(), b"old");
|
||||
assert!(publish(&out, true, &[], &|| true, |_| panic!("abgebrochen")).is_err());
|
||||
assert!(publish(&out, true, &[], &|| false, |p| {
|
||||
fs::write(p, b"new")?;
|
||||
fs::write(&out, b"external")?;
|
||||
Ok(())
|
||||
})
|
||||
.is_err());
|
||||
assert_eq!(fs::read(&out).unwrap(), b"external");
|
||||
fs::remove_file(&out).unwrap();
|
||||
assert!(publish(&out, true, &[], &|| false, |p| {
|
||||
fs::write(p, b"new")?;
|
||||
fs::write(&out, b"racer")?;
|
||||
Ok(())
|
||||
})
|
||||
.is_err());
|
||||
assert_eq!(fs::read(&out).unwrap(), b"racer");
|
||||
assert!(
|
||||
publish(&d.0.join("missing/result"), false, &[], &|| false, |_| Ok(
|
||||
()
|
||||
))
|
||||
.is_err()
|
||||
);
|
||||
publish(&out, true, &[], &|| false, |p| {
|
||||
fs::write(p, b"approved")?;
|
||||
Ok(())
|
||||
})
|
||||
.unwrap();
|
||||
assert_eq!(fs::read(&out).unwrap(), b"approved");
|
||||
assert_eq!(fs::read(&source).unwrap(), b"source");
|
||||
assert_eq!(fs::read_dir(&d.0).unwrap().count(), 2);
|
||||
#[cfg(unix)]
|
||||
{
|
||||
std::os::unix::fs::symlink(&source, d.0.join("alias")).unwrap();
|
||||
assert!(
|
||||
publish(&d.0.join("alias"), true, &[], &|| false, |_| panic!(
|
||||
"Symlink"
|
||||
))
|
||||
.is_err()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fehlgeschlagene_native_finalisierung_erhaelt_das_ziel() {
|
||||
let dir = Dir::new();
|
||||
let runtime = dir.0.join("tbrt");
|
||||
let out = dir.0.join("program");
|
||||
fs::write(&runtime, header(Target::MacosArm64)).unwrap();
|
||||
prepare_template(&runtime, Target::MacosArm64).unwrap();
|
||||
fs::write(&out, b"original").unwrap();
|
||||
let module = tb_vm::compile_source("TEST", "END\n").unwrap();
|
||||
let error = export(
|
||||
Export {
|
||||
module: &module,
|
||||
target: Target::MacosArm64,
|
||||
template: &runtime,
|
||||
output: &out,
|
||||
overwrite: true,
|
||||
protected: &[],
|
||||
},
|
||||
&|| false,
|
||||
)
|
||||
.unwrap_err();
|
||||
assert!(error.to_string().contains("Finalisierung"), "{error:#}");
|
||||
assert_eq!(fs::read(&out).unwrap(), b"original");
|
||||
assert_eq!(fs::read_dir(&dir.0).unwrap().count(), 3);
|
||||
}
|
||||
Reference in New Issue
Block a user