This commit is contained in:
64
launcher/Cargo.lock
generated
Normal file
64
launcher/Cargo.lock
generated
Normal file
@@ -0,0 +1,64 @@
|
||||
# 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 = "cfg-if"
|
||||
version = "1.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600"
|
||||
|
||||
[[package]]
|
||||
name = "crc32fast"
|
||||
version = "1.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "01a7799fd6b852db0e61728dde9a204c423b44d689dbd432522543614b490e78"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "csr-intake-launcher"
|
||||
version = "2.6.0"
|
||||
dependencies = [
|
||||
"flate2",
|
||||
]
|
||||
|
||||
[[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 = "miniz_oxide"
|
||||
version = "0.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b63fbc4a50860e98e7b2aa7804ded1db5cbc3aff9193adaff57a6931bf7c4b4c"
|
||||
dependencies = [
|
||||
"adler2",
|
||||
"simd-adler32",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "simd-adler32"
|
||||
version = "0.3.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea"
|
||||
|
||||
[[package]]
|
||||
name = "zlib-rs"
|
||||
version = "0.6.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b268e58e7c693d7c271f93ffc4ba3b380412554231c85bf61ca7af91042a4112"
|
||||
13
launcher/Cargo.toml
Normal file
13
launcher/Cargo.toml
Normal file
@@ -0,0 +1,13 @@
|
||||
[package]
|
||||
name = "csr-intake-launcher"
|
||||
version = "2.6.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
flate2 = "1"
|
||||
|
||||
[profile.release]
|
||||
lto = true
|
||||
codegen-units = 1
|
||||
strip = true
|
||||
panic = "abort"
|
||||
330
launcher/src/main.rs
Normal file
330
launcher/src/main.rs
Normal file
@@ -0,0 +1,330 @@
|
||||
use flate2::read::GzDecoder;
|
||||
use std::env;
|
||||
use std::fs::{self, File};
|
||||
use std::io::{self, BufReader, Read, Write};
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
use std::os::unix::process::CommandExt;
|
||||
use std::path::{Component, Path, PathBuf};
|
||||
use std::process::{self, Command};
|
||||
|
||||
const BUILD_ID: &str = env!("CSR_BUILD_ID");
|
||||
const VERSION: &str = env!("CSR_VERSION");
|
||||
const PAYLOAD: &[u8] = include_bytes!(env!("CSR_PAYLOAD_FILE"));
|
||||
const MANIFEST: &str = include_str!(env!("CSR_PAYLOAD_MANIFEST"));
|
||||
const MAGIC: &[u8; 8] = b"CSRPKG3\0";
|
||||
|
||||
fn cache_dir() -> PathBuf {
|
||||
env::var_os("XDG_CACHE_HOME")
|
||||
.filter(|v| !v.is_empty())
|
||||
.map(PathBuf::from)
|
||||
.or_else(|| env::var_os("HOME").map(|h| PathBuf::from(h).join(".cache")))
|
||||
.unwrap_or_else(|| env::temp_dir().join(format!("csr-intake-{}", process::id())))
|
||||
.join("csr-intake")
|
||||
.join(BUILD_ID)
|
||||
}
|
||||
|
||||
fn default_data_dir() -> PathBuf {
|
||||
env::var_os("XDG_DATA_HOME")
|
||||
.filter(|v| !v.is_empty())
|
||||
.map(PathBuf::from)
|
||||
.or_else(|| env::var_os("HOME").map(|h| PathBuf::from(h).join(".local/share")))
|
||||
.unwrap_or_else(|| PathBuf::from("."))
|
||||
.join("csr-intake")
|
||||
}
|
||||
|
||||
fn fnv(path: &Path) -> io::Result<u64> {
|
||||
let mut input = BufReader::new(File::open(path)?);
|
||||
let mut hash = 0xcbf29ce484222325u64;
|
||||
let mut buf = [0u8; 65536];
|
||||
loop {
|
||||
let count = input.read(&mut buf)?;
|
||||
if count == 0 {
|
||||
break;
|
||||
}
|
||||
for byte in &buf[..count] {
|
||||
hash = (hash ^ u64::from(*byte)).wrapping_mul(0x100000001b3);
|
||||
}
|
||||
}
|
||||
Ok(hash)
|
||||
}
|
||||
|
||||
fn valid(root: &Path) -> bool {
|
||||
MANIFEST.lines().all(|line| {
|
||||
let mut fields = line.splitn(4, '\t');
|
||||
let expected_hash = fields.next().and_then(|v| u64::from_str_radix(v, 16).ok());
|
||||
let expected_mode = fields.next().and_then(|v| u32::from_str_radix(v, 8).ok());
|
||||
let expected_size = fields.next().and_then(|v| v.parse::<u64>().ok());
|
||||
let name = fields.next();
|
||||
let (Some(hash), Some(mode), Some(size), Some(name)) =
|
||||
(expected_hash, expected_mode, expected_size, name)
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
let path = root.join(name);
|
||||
let Ok(metadata) = fs::metadata(&path) else {
|
||||
return false;
|
||||
};
|
||||
metadata.is_file()
|
||||
&& metadata.len() == size
|
||||
&& metadata.permissions().mode() & 0o777 == mode
|
||||
&& fnv(&path).ok() == Some(hash)
|
||||
})
|
||||
}
|
||||
|
||||
fn read_u32(reader: &mut impl Read) -> io::Result<u32> {
|
||||
let mut bytes = [0u8; 4];
|
||||
reader.read_exact(&mut bytes)?;
|
||||
Ok(u32::from_le_bytes(bytes))
|
||||
}
|
||||
|
||||
fn read_u64(reader: &mut impl Read) -> io::Result<u64> {
|
||||
let mut bytes = [0u8; 8];
|
||||
reader.read_exact(&mut bytes)?;
|
||||
Ok(u64::from_le_bytes(bytes))
|
||||
}
|
||||
|
||||
fn safe_relative(value: &str) -> io::Result<&Path> {
|
||||
let path = Path::new(value);
|
||||
if value.is_empty()
|
||||
|| path
|
||||
.components()
|
||||
.any(|c| !matches!(c, Component::Normal(_)))
|
||||
{
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
"unsafe bundle path",
|
||||
));
|
||||
}
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
fn unpack(destination: &Path) -> io::Result<()> {
|
||||
let mut reader = GzDecoder::new(PAYLOAD);
|
||||
let mut magic = [0u8; 8];
|
||||
reader.read_exact(&mut magic)?;
|
||||
if &magic != MAGIC {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
"invalid bundle header",
|
||||
));
|
||||
}
|
||||
loop {
|
||||
let name_len = read_u32(&mut reader)?;
|
||||
if name_len == 0 {
|
||||
break;
|
||||
}
|
||||
if name_len > 4096 {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
"bundle path too long",
|
||||
));
|
||||
}
|
||||
let mode = read_u32(&mut reader)?;
|
||||
let size = read_u64(&mut reader)?;
|
||||
let mut name = vec![0u8; name_len as usize];
|
||||
reader.read_exact(&mut name)?;
|
||||
let name = std::str::from_utf8(&name)
|
||||
.map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "non-UTF-8 bundle path"))?;
|
||||
let path = destination.join(safe_relative(name)?);
|
||||
if let Some(parent) = path.parent() {
|
||||
fs::create_dir_all(parent)?;
|
||||
}
|
||||
let mut output = File::create(&path)?;
|
||||
io::copy(&mut reader.by_ref().take(size), &mut output)?;
|
||||
output.flush()?;
|
||||
fs::set_permissions(&path, fs::Permissions::from_mode(mode & 0o777))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn ensure_bundle() -> io::Result<PathBuf> {
|
||||
let app = cache_dir();
|
||||
if !valid(&app) {
|
||||
if app.exists() {
|
||||
fs::remove_dir_all(&app)?;
|
||||
}
|
||||
let parent = app.parent().unwrap();
|
||||
fs::create_dir_all(parent)?;
|
||||
let temp = parent.join(format!(".extract-{BUILD_ID}-{}", process::id()));
|
||||
if temp.exists() {
|
||||
fs::remove_dir_all(&temp)?;
|
||||
}
|
||||
fs::create_dir(&temp)?;
|
||||
if let Err(error) = unpack(&temp).and_then(|_| {
|
||||
if valid(&temp) {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
"bundle checksum mismatch",
|
||||
))
|
||||
}
|
||||
}) {
|
||||
let _ = fs::remove_dir_all(&temp);
|
||||
return Err(error);
|
||||
}
|
||||
if let Err(error) = fs::rename(&temp, &app) {
|
||||
if !valid(&app) {
|
||||
return Err(error);
|
||||
}
|
||||
let _ = fs::remove_dir_all(&temp);
|
||||
}
|
||||
}
|
||||
let runtime = app.join("runtime-launcher");
|
||||
let current = env::current_exe()?;
|
||||
let replace = fs::metadata(&runtime).map(|m| m.len()).ok()
|
||||
!= fs::metadata(¤t).map(|m| m.len()).ok();
|
||||
if replace {
|
||||
let _ = fs::remove_file(&runtime);
|
||||
if fs::hard_link(¤t, &runtime).is_err() {
|
||||
fs::copy(¤t, &runtime)?;
|
||||
}
|
||||
fs::set_permissions(&runtime, fs::Permissions::from_mode(0o700))?;
|
||||
}
|
||||
Ok(app)
|
||||
}
|
||||
|
||||
fn loader_command(app: &Path, program: &Path) -> Command {
|
||||
let libraries = app.join("lib");
|
||||
let mut command = Command::new(app.join("lib/ld-linux-x86-64.so.2"));
|
||||
command.arg("--library-path").arg(&libraries).arg(program);
|
||||
command.env("LD_LIBRARY_PATH", &libraries);
|
||||
command.env(
|
||||
"PATH",
|
||||
format!(
|
||||
"{}:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
|
||||
app.join("bin").display()
|
||||
),
|
||||
);
|
||||
let certificates = app.join("etc/ca-certificates.crt");
|
||||
if certificates.exists() {
|
||||
command.env("SSL_CERT_FILE", &certificates);
|
||||
command.env("CURL_CA_BUNDLE", &certificates);
|
||||
}
|
||||
command
|
||||
}
|
||||
|
||||
fn internal(app: &Path, kind: &str, args: impl Iterator<Item = String>) -> ! {
|
||||
let mut command = if kind == "python" {
|
||||
let mut command = loader_command(app, &app.join("python/bin/python3"));
|
||||
command.env("PYTHONHOME", app.join("python"));
|
||||
command.env(
|
||||
"PYTHONPATH",
|
||||
format!(
|
||||
"{}:{}",
|
||||
app.join("python/lib/python.zip").display(),
|
||||
app.join("python/lib/python/lib-dynload").display()
|
||||
),
|
||||
);
|
||||
command
|
||||
} else {
|
||||
loader_command(app, &app.join("pc-intake"))
|
||||
};
|
||||
command.args(args);
|
||||
let error = command.exec();
|
||||
eprintln!("Could not start bundled {kind}: {error}");
|
||||
process::exit(126)
|
||||
}
|
||||
|
||||
fn seed_config(app: &Path, data: &Path) -> io::Result<()> {
|
||||
let source = app.join("default-config");
|
||||
if !source.is_dir() {
|
||||
return Ok(());
|
||||
}
|
||||
let target = data.join("config");
|
||||
fs::create_dir_all(&target)?;
|
||||
fs::set_permissions(&target, fs::Permissions::from_mode(0o700))?;
|
||||
for name in [
|
||||
"inventory.json",
|
||||
"inventory-api-key",
|
||||
"inventory-server.crt",
|
||||
] {
|
||||
let destination = target.join(name);
|
||||
if !destination.exists() {
|
||||
fs::copy(source.join(name), &destination)?;
|
||||
fs::set_permissions(destination, fs::Permissions::from_mode(0o600))?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn keyboard_layout(data: &Path) -> Option<String> {
|
||||
let text = fs::read_to_string(data.join("config/workshop.json")).ok()?;
|
||||
let rest = text
|
||||
.split_once("\"keyboard_layout\"")?
|
||||
.1
|
||||
.split_once(':')?
|
||||
.1
|
||||
.trim_start();
|
||||
let value = rest.strip_prefix('"')?.split_once('"')?.0;
|
||||
matches!(value, "de" | "us").then(|| value.to_owned())
|
||||
}
|
||||
|
||||
fn fail(context: &str, error: impl std::fmt::Display) -> ! {
|
||||
eprintln!("CS-R Intake: {context}: {error}");
|
||||
process::exit(2)
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let mut args = env::args().skip(1);
|
||||
let first = args.next();
|
||||
if first.as_deref() == Some("--version") {
|
||||
println!("CS-R Intake {VERSION} · {BUILD_ID}");
|
||||
return;
|
||||
}
|
||||
if matches!(
|
||||
first.as_deref(),
|
||||
Some("--internal-python" | "--internal-scanner")
|
||||
) {
|
||||
let app = env::current_exe()
|
||||
.ok()
|
||||
.and_then(|p| p.parent().map(Path::to_owned))
|
||||
.unwrap_or_else(|| fail("runtime location", "unavailable"));
|
||||
internal(
|
||||
&app,
|
||||
if first.as_deref() == Some("--internal-python") {
|
||||
"python"
|
||||
} else {
|
||||
"scanner"
|
||||
},
|
||||
args,
|
||||
);
|
||||
}
|
||||
let app = ensure_bundle().unwrap_or_else(|e| fail("bundle extraction failed", e));
|
||||
if first.as_deref() == Some("--check-bundle") {
|
||||
println!("Standalone ELF verified: {BUILD_ID}");
|
||||
return;
|
||||
}
|
||||
let mut gui_args = Vec::new();
|
||||
if let Some(value) = first {
|
||||
gui_args.push(value);
|
||||
}
|
||||
gui_args.extend(args);
|
||||
let mut data = default_data_dir();
|
||||
let mut has_data = false;
|
||||
let mut index = 0;
|
||||
while index < gui_args.len() {
|
||||
if gui_args[index] == "--data" {
|
||||
has_data = true;
|
||||
data = gui_args
|
||||
.get(index + 1)
|
||||
.map(PathBuf::from)
|
||||
.unwrap_or_else(|| fail("argument error", "missing directory after --data"));
|
||||
index += 1;
|
||||
}
|
||||
index += 1;
|
||||
}
|
||||
seed_config(&app, &data).unwrap_or_else(|e| fail("could not install configuration", e));
|
||||
if !has_data {
|
||||
gui_args.extend(["--data".into(), data.to_string_lossy().into_owned()]);
|
||||
}
|
||||
if let Some(layout) = keyboard_layout(&data) {
|
||||
env::set_var("XKB_DEFAULT_LAYOUT", layout);
|
||||
}
|
||||
env::set_var("CSR_RUNTIME_LAUNCHER", app.join("runtime-launcher"));
|
||||
env::set_var("CSR_SCANNER", app.join("pc-intake"));
|
||||
let mut command = loader_command(&app, &app.join("intake-gui"));
|
||||
command.args(gui_args);
|
||||
let error = command.exec();
|
||||
fail("could not start GUI", error)
|
||||
}
|
||||
Reference in New Issue
Block a user