update docs

This commit is contained in:
2026-09-22 13:23:34 +08:00
parent 99bc3d15c5
commit 8a4788fca8
126 changed files with 7198 additions and 2425 deletions
+18
View File
@@ -47,6 +47,14 @@ pub enum Request {
bay: Bay,
arguments: Vec<String>,
},
/// Foreground PATH launcher; stdio is passed to an ordinary managed child.
Program {
name: String,
arguments: Vec<String>,
terminal: bool,
working: String,
term: String,
},
MediaPrepare {
bay: Bay,
image: String,
@@ -105,6 +113,11 @@ impl MediaJob {
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PublishedCommand {
pub selector: String,
pub alias: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BayState {
pub bay: Bay,
pub state: String,
@@ -114,6 +127,8 @@ pub struct BayState {
pub manifest: Option<Manifest>,
pub mount: Option<String>,
pub consumers: usize,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub commands: Vec<PublishedCommand>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub software: Option<crate::software::Catalogue>,
}
@@ -168,6 +183,8 @@ pub struct Response {
pub power: Option<PowerState>,
#[serde(default)]
pub recovery: Option<RecoveryReport>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub exit_status: Option<i32>,
}
impl Response {
pub fn failure(error: impl ToString) -> Self {
@@ -182,6 +199,7 @@ impl Response {
disk: None,
power: None,
recovery: None,
exit_status: None,
}
}
}
+171
View File
@@ -0,0 +1,171 @@
//! Foreground launch transfers ordinary stdio to a managed child and receives its pidfd.
use crate::{
Error, Result,
control::{LIMIT, Request, Response, SOCKET},
};
use serde::{Deserialize, Serialize};
use std::{
io::{Read, Write},
os::{
fd::{AsRawFd, FromRawFd, OwnedFd},
unix::net::UnixStream,
},
time::Duration,
};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Program {
pub arguments: Vec<String>,
pub environment: Vec<(String, String)>,
}
pub fn send_fds(socket: &UnixStream, descriptors: &[i32]) -> Result<()> {
if descriptors.is_empty() || descriptors.len() > 3 {
return Err(Error("Invalid descriptor count".into()));
}
let mut marker = b'F';
let mut vector = libc::iovec {
iov_base: (&mut marker as *mut u8).cast(),
iov_len: 1,
};
let mut control = [0usize; 8];
let mut message: libc::msghdr = unsafe { std::mem::zeroed() };
message.msg_iov = &mut vector;
message.msg_iovlen = 1;
message.msg_control = control.as_mut_ptr().cast();
message.msg_controllen =
unsafe { libc::CMSG_SPACE(std::mem::size_of_val(descriptors) as _) } as _;
unsafe {
let header = libc::CMSG_FIRSTHDR(&message);
(*header).cmsg_level = libc::SOL_SOCKET;
(*header).cmsg_type = libc::SCM_RIGHTS;
(*header).cmsg_len = libc::CMSG_LEN(std::mem::size_of_val(descriptors) as _) as _;
for (index, descriptor) in descriptors.iter().enumerate() {
std::ptr::write_unaligned(
libc::CMSG_DATA(header).cast::<i32>().add(index),
*descriptor,
);
}
if libc::sendmsg(socket.as_raw_fd(), &message, libc::MSG_NOSIGNAL) != 1 {
return Err(std::io::Error::last_os_error().into());
}
}
Ok(())
}
pub fn receive_fds(socket: &mut UnixStream, expected: usize) -> Result<Vec<OwnedFd>> {
let mut marker = 0u8;
let mut vector = libc::iovec {
iov_base: (&mut marker as *mut u8).cast(),
iov_len: 1,
};
let mut control = [0usize; 8];
let mut message: libc::msghdr = unsafe { std::mem::zeroed() };
message.msg_iov = &mut vector;
message.msg_iovlen = 1;
message.msg_control = control.as_mut_ptr().cast();
message.msg_controllen = std::mem::size_of_val(&control) as _;
let n = unsafe { libc::recvmsg(socket.as_raw_fd(), &mut message, libc::MSG_CMSG_CLOEXEC) };
if n < 0 {
return Err(std::io::Error::last_os_error().into());
}
let mut received = Vec::new();
unsafe {
let mut header = libc::CMSG_FIRSTHDR(&message);
while !header.is_null() {
if (*header).cmsg_level == libc::SOL_SOCKET && (*header).cmsg_type == libc::SCM_RIGHTS {
let count = ((*header).cmsg_len as usize - libc::CMSG_LEN(0) as usize)
/ std::mem::size_of::<i32>();
for index in 0..count {
let fd =
std::ptr::read_unaligned(libc::CMSG_DATA(header).cast::<i32>().add(index));
received.push(OwnedFd::from_raw_fd(fd));
}
}
header = libc::CMSG_NXTHDR(&message, header);
}
}
if n == 1
&& marker == b'F'
&& message.msg_flags & libc::MSG_CTRUNC == 0
&& received.len() == expected
{
return Ok(received);
}
if n == 1 && marker == b'{' && received.is_empty() {
let mut bytes = vec![marker];
socket.take(LIMIT as u64).read_to_end(&mut bytes)?;
if bytes.len() <= LIMIT {
if let Ok(reply) = serde_json::from_slice::<Response>(&bytes) {
if let Some(error) = reply.error {
return Err(Error(error));
}
}
}
}
Err(Error("Invalid managed program handshake".into()))
}
pub fn start(
name: &str,
arguments: Vec<String>,
terminal: bool,
working: String,
term: String,
descriptors: &[i32],
) -> Result<(UnixStream, OwnedFd)> {
let mut socket = UnixStream::connect(SOCKET)?;
socket.set_read_timeout(Some(Duration::from_secs(120)))?;
socket.set_write_timeout(Some(Duration::from_secs(5)))?;
let mut request = serde_json::to_vec(&Request::Program {
name: name.into(),
arguments,
terminal,
working,
term,
})
.map_err(|e| Error(e.to_string()))?;
if request.len() >= LIMIT {
return Err(Error("Program request is too long".into()));
}
request.push(b'\n');
socket.write_all(&request)?;
let mut ready = [0u8];
socket.read_exact(&mut ready)?;
if ready != [b'R'] {
let mut bytes = ready.to_vec();
(&mut socket).take(LIMIT as u64).read_to_end(&mut bytes)?;
let reply: Response = serde_json::from_slice(&bytes)
.map_err(|_| Error("Invalid program handshake".into()))?;
return Err(Error(
reply
.error
.unwrap_or_else(|| "Program launch failed".into()),
));
}
send_fds(&socket, descriptors)?;
let mut received = receive_fds(&mut socket, 1)?;
socket.set_read_timeout(None)?;
socket.set_nonblocking(true)?;
Ok((socket, received.pop().unwrap()))
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs::File;
#[test]
fn descriptor_transfer_is_cloexec_and_owned() {
let (sender, mut receiver) = UnixStream::pair().unwrap();
let original = File::open("/dev/null").unwrap();
send_fds(&sender, &[original.as_raw_fd()]).unwrap();
let received = receive_fds(&mut receiver, 1).unwrap().pop().unwrap();
assert_ne!(received.as_raw_fd(), original.as_raw_fd());
assert_ne!(
unsafe { libc::fcntl(received.as_raw_fd(), libc::F_GETFD) } & libc::FD_CLOEXEC,
0
);
drop(original);
assert!(File::from(received).metadata().is_ok());
}
}
+1
View File
@@ -1,6 +1,7 @@
//! Shared data contracts. Cartridge contents are data, never startup commands.
pub mod boot;
pub mod control;
pub mod launch;
pub mod machine;
pub mod manifest;
pub mod software;
+60 -4
View File
@@ -1,4 +1,4 @@
//! Software metadata is descriptive. Bundles never contain privileged build hooks.
//! Software metadata describes immutable installed trees or legacy archives.
use crate::{Error, Result, manifest::identifier};
use serde::{Deserialize, Serialize};
@@ -21,6 +21,13 @@ pub struct Software {
pub architecture: String,
/// GPT partition number, starting at 2 after FDS_METADATA.
pub partition: u8,
/// New media executes installed Void package files directly from EROFS.
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub installed: bool,
/// Exact XBPS package versions included in this software tree.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub packages: Vec<String>,
#[serde(default, skip_serializing_if = "is_zero")]
pub archive_bytes: u64,
pub unpacked_bytes: u64,
pub entries: u32,
@@ -29,6 +36,9 @@ pub struct Software {
pub commands: BTreeMap<String, String>,
}
impl Software {
pub fn root_path(&self) -> String {
format!("programs/{}", self.id)
}
pub fn archive_path(&self) -> String {
format!("bundles/{}.tar.xz", self.id)
}
@@ -41,7 +51,14 @@ impl Software {
|| !display(&self.version, 32)
|| !matches!(self.architecture.as_str(), "aarch64" | "any")
|| !(2..=33).contains(&self.partition)
|| !(1..=MAX_ARCHIVE).contains(&self.archive_bytes)
|| if self.installed {
self.archive_bytes != 0
|| self.packages.is_empty()
|| self.packages.len() > 512
|| self.packages.iter().any(|p| !xbps_identifier(p))
} else {
!(1..=MAX_ARCHIVE).contains(&self.archive_bytes) || !self.packages.is_empty()
}
|| self.unpacked_bytes > MAX_UNPACKED
|| !(1..=MAX_ENTRIES).contains(&self.entries)
|| self.sha256.len() != 64
@@ -81,15 +98,20 @@ impl Catalogue {
Ok(value)
}
pub fn validate(&self) -> Result<()> {
if self.format != 1 || self.software.is_empty() || self.software.len() > 128 {
if !matches!(self.format, 1 | 2) || self.software.is_empty() || self.software.len() > 128 {
return Err(Error(
"Software catalogue requires format 1 and 1..128 software entries".into(),
"Software catalogue requires format 1 or 2 and 1..128 software entries".into(),
));
}
let mut ids = BTreeSet::new();
let mut partitions = BTreeSet::new();
for software in &self.software {
software.validate()?;
if software.installed != (self.format == 2) {
return Err(Error(
"Catalogue format disagrees with software storage layout".into(),
));
}
if !ids.insert(&software.id) {
return Err(Error("Duplicate software id".into()));
}
@@ -118,6 +140,18 @@ impl Catalogue {
Ok(result)
}
}
fn is_zero(value: &u64) -> bool {
*value == 0
}
pub fn xbps_identifier(value: &str) -> bool {
!value.is_empty()
&& value.len() <= 256
&& value.as_bytes()[0].is_ascii_alphanumeric()
&& value
.bytes()
.all(|c| c.is_ascii_alphanumeric() || b"._-+~".contains(&c))
&& !value.contains("..")
}
pub fn relative(value: &str) -> bool {
!value.is_empty()
&& value.len() <= 1024
@@ -141,6 +175,8 @@ mod tests {
version: "1".into(),
architecture: "aarch64".into(),
partition,
installed: false,
packages: Vec::new(),
archive_bytes: 100,
unpacked_bytes: 200,
entries: 1,
@@ -181,4 +217,24 @@ mod tests {
}
assert!(relative("share/document with spaces.txt"));
}
#[test]
fn installed_trees_use_format_two_and_record_xbps_versions() {
let mut entry = software("one", 2);
entry.installed = true;
entry.archive_bytes = 0;
entry.packages = vec!["WindowMaker-0.96.0_1".into(), "libstdc++-14.2.1_1".into()];
let mut catalogue = Catalogue {
format: 2,
software: vec![entry],
};
assert_eq!(
Catalogue::parse(&catalogue.to_toml().unwrap()).unwrap(),
catalogue
);
catalogue.format = 1;
assert!(catalogue.validate().is_err());
catalogue.format = 2;
catalogue.software[0].packages.clear();
assert!(catalogue.validate().is_err());
}
}