feat: recipe durability guard — prevents build system from deleting local recipes

Add guard-recipes.sh with four modes:
- --verify: check all local/recipes have correct symlinks into recipes/
- --fix: repair broken symlinks (run before builds)
- --save-all: snapshot all recipe.toml into local/recipes/
- --restore: recreate all symlinks from local/recipes/ (run after sync-upstream)

Wired into apply-patches.sh (post-patch) and sync-upstream.sh (post-sync).
This prevents the build system from deleting recipe files during
cargo cook, make distclean, or upstream source refresh.
This commit is contained in:
2026-04-30 18:47:03 +01:00
parent 34360e1e4f
commit 7c7399e0a6
126 changed files with 13145 additions and 178 deletions
@@ -0,0 +1,141 @@
use alloc::boxed::Box;
use alloc::vec::Vec;
/// DMA buffer for USB transfers.
///
/// This crate intentionally avoids Redox-specific allocation APIs, so `allocate`
/// produces an owned staging buffer and leaves `physical_addr` unset (`0`).
/// Controller-specific code must install a real physical address before using
/// the buffer for hardware DMA.
pub struct DmaBuffer {
pub virtual_addr: usize,
pub physical_addr: u64,
pub size: usize,
data: Box<[u8]>,
mapped: bool,
}
impl DmaBuffer {
/// Allocate an owned staging buffer for a future DMA mapping.
///
/// The returned buffer is not DMA-mapped yet. `physical_addr` remains `0`
/// until the caller supplies a real mapping with `set_physical_addr`.
pub fn allocate(size: usize) -> Result<Self, DmaError> {
if size > isize::MAX as usize {
return Err(DmaError::TooLarge);
}
if size == 0 {
return Ok(Self {
virtual_addr: 0,
physical_addr: 0,
size: 0,
data: Vec::new().into_boxed_slice(),
mapped: true,
});
}
let mut data = Vec::new();
if data.try_reserve_exact(size).is_err() {
return Err(DmaError::NoMemory);
}
data.resize(size, 0);
let mut data = data.into_boxed_slice();
let virtual_addr = data.as_mut_ptr() as usize;
Ok(Self {
virtual_addr,
physical_addr: 0,
size,
data,
mapped: false,
})
}
pub fn as_slice(&self) -> &[u8] {
&self.data
}
pub fn as_mut_slice(&mut self) -> &mut [u8] {
&mut self.data
}
/// Returns `true` once the controller-specific layer has installed a
/// hardware-usable physical address for this buffer.
pub fn is_dma_mapped(&self) -> bool {
self.mapped
}
/// Attach a controller/OS-specific physical address to the staging buffer.
pub fn set_physical_addr(&mut self, physical_addr: u64) -> Result<(), DmaError> {
if self.size != 0 && physical_addr == 0 {
return Err(DmaError::AllocFailed);
}
self.physical_addr = physical_addr;
self.mapped = true;
Ok(())
}
}
#[derive(Debug)]
pub enum DmaError {
AllocFailed,
TooLarge,
NoMemory,
}
#[cfg(test)]
mod tests {
use super::{DmaBuffer, DmaError};
#[test]
fn allocated_buffers_start_unmapped() {
let buffer = match DmaBuffer::allocate(16) {
Ok(buffer) => buffer,
Err(error) => panic!("expected allocation to succeed: {error:?}"),
};
assert_eq!(buffer.physical_addr, 0);
assert!(!buffer.is_dma_mapped());
assert_eq!(buffer.as_slice().len(), 16);
}
#[test]
fn attaching_a_physical_address_marks_the_buffer_mapped() {
let mut buffer = match DmaBuffer::allocate(16) {
Ok(buffer) => buffer,
Err(error) => panic!("expected allocation to succeed: {error:?}"),
};
let result = buffer.set_physical_addr(0x1000);
assert!(result.is_ok());
assert_eq!(buffer.physical_addr, 0x1000);
assert!(buffer.is_dma_mapped());
}
#[test]
fn nonempty_buffers_reject_a_zero_physical_address() {
let mut buffer = match DmaBuffer::allocate(8) {
Ok(buffer) => buffer,
Err(error) => panic!("expected allocation to succeed: {error:?}"),
};
let result = buffer.set_physical_addr(0);
assert!(matches!(result, Err(DmaError::AllocFailed)));
}
#[test]
fn direct_field_mutation_does_not_forge_mapping_state() {
let mut buffer = match DmaBuffer::allocate(8) {
Ok(buffer) => buffer,
Err(error) => panic!("expected allocation to succeed: {error:?}"),
};
buffer.physical_addr = 0x2000;
assert_eq!(buffer.physical_addr, 0x2000);
assert!(!buffer.is_dma_mapped());
}
}
@@ -0,0 +1,21 @@
#![cfg_attr(not(feature = "std"), no_std)]
#![doc = "Shared USB types and primitives for Red Bear host controller drivers."]
extern crate alloc;
pub mod dma;
pub mod scheme;
pub mod spawn;
pub mod transfer;
pub mod types;
pub use dma::{DmaBuffer, DmaError};
pub use scheme::{UsbError, UsbHostController};
pub use spawn::spawn_usb_driver;
pub use transfer::{
control_transfer, parse_config_descriptor, parse_device_descriptor, parse_endpoint_descriptor,
};
pub use types::{
ConfigDescriptor, DeviceDescriptor, EndpointDescriptor, PortStatus, SetupPacket,
TransferDirection, TransferType, Urb, UrbStatus,
};
@@ -0,0 +1,57 @@
use crate::types::{PortStatus, SetupPacket, TransferDirection};
/// Trait that all USB host controller drivers must implement.
/// This provides a uniform scheme interface regardless of HC type (XHCI/EHCI/OHCI/UHCI).
pub trait UsbHostController {
/// Get the number of ports on this controller
fn port_count(&self) -> usize;
/// Get status of a specific port
fn port_status(&self, port: usize) -> Option<PortStatus>;
/// Reset a port (USB enumeration step 1)
fn port_reset(&mut self, port: usize) -> bool;
/// Submit a control transfer to endpoint 0
fn control_transfer(
&mut self,
device_address: u8,
setup: &SetupPacket,
data: &mut [u8],
) -> Result<usize, UsbError>;
/// Submit a bulk transfer
fn bulk_transfer(
&mut self,
device_address: u8,
endpoint: u8,
data: &mut [u8],
direction: TransferDirection,
) -> Result<usize, UsbError>;
/// Submit an interrupt transfer
fn interrupt_transfer(
&mut self,
device_address: u8,
endpoint: u8,
data: &mut [u8],
) -> Result<usize, UsbError>;
/// Set device address (after reset, before config)
fn set_address(&mut self, device_address: u8) -> bool;
/// Get the controller name for logging
fn name(&self) -> &str;
}
#[derive(Debug)]
pub enum UsbError {
Timeout,
Stall,
DataError,
Babble,
NoDevice,
NotConfigured,
IoError,
Unsupported,
}
@@ -0,0 +1,52 @@
/// Spawn a child USB class driver (hub, HID, storage).
/// On Redox, this forks and execs the driver binary with the USB device path.
#[cfg(feature = "std")]
pub fn spawn_usb_driver(driver_binary: &str, device_path: &str) {
if driver_binary.is_empty()
|| device_path.is_empty()
|| !driver_binary.starts_with('/')
|| !device_path.starts_with('/')
|| !is_trusted_usb_driver(driver_binary)
{
return;
}
let mut command = std::process::Command::new(driver_binary);
command.env_clear();
command.stdin(std::process::Stdio::null());
command.arg(device_path);
let _ = command.spawn();
}
#[cfg(feature = "std")]
fn is_trusted_usb_driver(driver_binary: &str) -> bool {
matches!(
driver_binary,
"/usr/bin/usbhubd" | "/usr/bin/usbhidd" | "/usr/bin/usbscsid"
)
}
/// Spawn a child USB class driver (hub, HID, storage).
/// On no_std builds, class-driver spawning is not available.
#[cfg(not(feature = "std"))]
pub fn spawn_usb_driver(_driver_binary: &str, _device_path: &str) {}
#[cfg(all(test, feature = "std"))]
mod tests {
use super::is_trusted_usb_driver;
#[test]
fn trusted_driver_whitelist_allows_expected_binaries() {
assert!(is_trusted_usb_driver("/usr/bin/usbhubd"));
assert!(is_trusted_usb_driver("/usr/bin/usbhidd"));
assert!(is_trusted_usb_driver("/usr/bin/usbscsid"));
}
#[test]
fn trusted_driver_whitelist_rejects_other_binaries() {
assert!(!is_trusted_usb_driver("/usr/bin/sh"));
assert!(!is_trusted_usb_driver("/tmp/usbhubd"));
assert!(!is_trusted_usb_driver("relative/usbhubd"));
assert!(!is_trusted_usb_driver(""));
}
}
@@ -0,0 +1,181 @@
use alloc::vec::Vec;
use crate::types::{
ConfigDescriptor, DeviceDescriptor, EndpointDescriptor, SetupPacket, TransferDirection,
TransferType, Urb, UrbStatus,
};
/// Build a standard control transfer setup packet + data stage
pub fn control_transfer(
device_address: u8,
request_type: u8,
request: u8,
value: u16,
index: u16,
data: &[u8],
direction: TransferDirection,
) -> Urb {
let data = if data.len() > u16::MAX as usize {
&data[..u16::MAX as usize]
} else {
data
};
let setup = SetupPacket {
request_type,
request,
value,
index,
length: data.len() as u16,
};
let mut buffer = Vec::with_capacity(data.len().saturating_add(8));
buffer.extend_from_slice(&setup_packet_bytes(&setup));
buffer.extend_from_slice(data);
Urb {
device_address,
endpoint: 0,
transfer_type: TransferType::Control,
direction,
buffer,
actual_length: 0,
status: UrbStatus::Pending,
timeout_ms: 1000,
}
}
/// Parse a Device Descriptor from raw bytes
pub fn parse_device_descriptor(data: &[u8]) -> Option<DeviceDescriptor> {
if data.len() < 18 || data[0] != 18 || data[1] != 0x01 {
return None;
}
Some(DeviceDescriptor {
length: data[0],
descriptor_type: data[1],
usb_version: u16::from_le_bytes([data[2], data[3]]),
device_class: data[4],
device_subclass: data[5],
device_protocol: data[6],
max_packet_size0: data[7],
vendor_id: u16::from_le_bytes([data[8], data[9]]),
product_id: u16::from_le_bytes([data[10], data[11]]),
device_version: u16::from_le_bytes([data[12], data[13]]),
manufacturer_idx: data[14],
product_idx: data[15],
serial_idx: data[16],
num_configurations: data[17],
})
}
/// Parse a Configuration Descriptor from raw bytes
pub fn parse_config_descriptor(data: &[u8]) -> Option<ConfigDescriptor> {
if data.len() < 9 || data[0] != 9 || data[1] != 0x02 {
return None;
}
Some(ConfigDescriptor {
length: data[0],
descriptor_type: data[1],
total_length: u16::from_le_bytes([data[2], data[3]]),
num_interfaces: data[4],
config_value: data[5],
config_idx: data[6],
attributes: data[7],
max_power: data[8],
})
}
/// Parse an Endpoint Descriptor from raw bytes
pub fn parse_endpoint_descriptor(data: &[u8]) -> Option<EndpointDescriptor> {
if data.len() < 7 || data[0] != 7 || data[1] != 0x05 {
return None;
}
Some(EndpointDescriptor {
length: data[0],
descriptor_type: data[1],
endpoint_address: data[2],
attributes: data[3],
max_packet_size: u16::from_le_bytes([data[4], data[5]]),
interval: data[6],
})
}
fn setup_packet_bytes(setup: &SetupPacket) -> [u8; 8] {
let value = setup.value.to_le_bytes();
let index = setup.index.to_le_bytes();
let length = setup.length.to_le_bytes();
[
setup.request_type,
setup.request,
value[0],
value[1],
index[0],
index[1],
length[0],
length[1],
]
}
#[cfg(test)]
mod tests {
use super::{
parse_config_descriptor, parse_device_descriptor, parse_endpoint_descriptor,
};
#[test]
fn parses_valid_device_descriptor() {
let raw = [
18, 0x01, 0x00, 0x02, 0xff, 0x00, 0x00, 64, 0x34, 0x12, 0x78, 0x56, 0x00, 0x01,
1, 2, 3, 1,
];
match parse_device_descriptor(&raw) {
Some(descriptor) => {
assert_eq!(descriptor.length, 18);
assert_eq!(descriptor.descriptor_type, 0x01);
assert_eq!(descriptor.usb_version, 0x0200);
assert_eq!(descriptor.vendor_id, 0x1234);
assert_eq!(descriptor.product_id, 0x5678);
assert_eq!(descriptor.num_configurations, 1);
}
None => panic!("valid device descriptor should parse"),
}
}
#[test]
fn parses_valid_config_descriptor() {
let raw = [9, 0x02, 32, 0, 1, 1, 0, 0x80, 50];
match parse_config_descriptor(&raw) {
Some(descriptor) => {
assert_eq!(descriptor.length, 9);
assert_eq!(descriptor.descriptor_type, 0x02);
assert_eq!(descriptor.total_length, 32);
assert_eq!(descriptor.num_interfaces, 1);
assert_eq!(descriptor.max_power, 50);
}
None => panic!("valid config descriptor should parse"),
}
}
#[test]
fn parses_valid_endpoint_descriptor() {
let raw = [7, 0x05, 0x81, 0x03, 8, 0, 10];
match parse_endpoint_descriptor(&raw) {
Some(descriptor) => {
assert_eq!(descriptor.length, 7);
assert_eq!(descriptor.descriptor_type, 0x05);
assert_eq!(descriptor.endpoint_address, 0x81);
assert_eq!(descriptor.attributes, 0x03);
assert_eq!(descriptor.max_packet_size, 8);
assert_eq!(descriptor.interval, 10);
}
None => panic!("valid endpoint descriptor should parse"),
}
}
}
@@ -0,0 +1,151 @@
use alloc::vec::Vec;
// USB Standard Device Descriptor (USB 2.0 §9.6.1)
#[derive(Clone, Debug)]
pub struct DeviceDescriptor {
pub length: u8,
pub descriptor_type: u8,
pub usb_version: u16,
pub device_class: u8,
pub device_subclass: u8,
pub device_protocol: u8,
pub max_packet_size0: u8,
pub vendor_id: u16,
pub product_id: u16,
pub device_version: u16,
pub manufacturer_idx: u8,
pub product_idx: u8,
pub serial_idx: u8,
pub num_configurations: u8,
}
// USB Configuration Descriptor (USB 2.0 §9.6.3)
#[derive(Clone, Debug)]
pub struct ConfigDescriptor {
pub length: u8,
pub descriptor_type: u8,
pub total_length: u16,
pub num_interfaces: u8,
pub config_value: u8,
pub config_idx: u8,
pub attributes: u8,
pub max_power: u8,
}
// USB Endpoint Descriptor (USB 2.0 §9.6.6)
#[derive(Clone, Debug)]
pub struct EndpointDescriptor {
pub length: u8,
pub descriptor_type: u8,
pub endpoint_address: u8,
pub attributes: u8,
pub max_packet_size: u16,
pub interval: u8,
}
// Standard Setup Packet (USB 2.0 §9.3)
#[derive(Clone, Debug)]
pub struct SetupPacket {
pub request_type: u8,
pub request: u8,
pub value: u16,
pub index: u16,
pub length: u16,
}
// USB Request Block — a transfer to/from a device
#[derive(Clone, Debug)]
pub struct Urb {
pub device_address: u8,
pub endpoint: u8,
pub transfer_type: TransferType,
pub direction: TransferDirection,
pub buffer: Vec<u8>,
pub actual_length: usize,
pub status: UrbStatus,
pub timeout_ms: u32,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum TransferType {
Control,
Bulk,
Interrupt,
Isochronous,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum TransferDirection {
Out,
In,
Setup,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum UrbStatus {
Pending,
Complete,
Error,
Timeout,
}
// USB port status
#[derive(Clone, Debug)]
pub struct PortStatus {
pub connected: bool,
pub enabled: bool,
pub suspended: bool,
pub over_current: bool,
pub reset: bool,
pub power: bool,
pub low_speed: bool,
pub high_speed: bool,
pub test_mode: bool,
pub indicator: bool,
}
#[cfg(test)]
mod tests {
use alloc::vec::Vec;
use super::{SetupPacket, TransferDirection, TransferType, Urb, UrbStatus};
#[test]
fn setup_packet_fields_are_accessible() {
let packet = SetupPacket {
request_type: 0x80,
request: 0x06,
value: 0x0100,
index: 0x0000,
length: 18,
};
assert_eq!(packet.request_type, 0x80);
assert_eq!(packet.request, 0x06);
assert_eq!(packet.value, 0x0100);
assert_eq!(packet.index, 0x0000);
assert_eq!(packet.length, 18);
}
#[test]
fn urb_status_can_transition_between_states() {
let mut urb = Urb {
device_address: 1,
endpoint: 1,
transfer_type: TransferType::Bulk,
direction: TransferDirection::In,
buffer: Vec::new(),
actual_length: 0,
status: UrbStatus::Pending,
timeout_ms: 1000,
};
assert_eq!(urb.status, UrbStatus::Pending);
urb.status = UrbStatus::Complete;
assert_eq!(urb.status, UrbStatus::Complete);
urb.status = UrbStatus::Timeout;
assert_eq!(urb.status, UrbStatus::Timeout);
}
}