Phase 6: Native Executables implementieren und Change archivieren

This commit is contained in:
2026-09-07 14:16:32 +02:00
parent 993c3e7638
commit 60d37ec79d
28 changed files with 1951 additions and 215 deletions

View 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
View 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;

View 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);
}