update docs
This commit is contained in:
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,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;
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user