use nord_format::cbin::{Cbin, RawBody};
use crate::envelope;
use crate::error::{Error, Result};
use crate::session::ReadWrite;
use crate::session::{Session, WRITE_LIMIT};
use crate::transport::Transport;
use crate::wire::{
cmd, read_u32, ui, AllocationUnit, Bank, Dependency, Location, Message, ObjectClass, Partition,
ProgramInfo, Service, Status,
};
pub async fn status<T: Transport, C>(session: &mut Session<'_, T, C>) -> Result<Status> {
let class = session.class();
let resp = session
.request(
Service::Program,
10,
cmd::STATUS,
&class.to_raw().to_be_bytes(),
)
.await?;
Status::decode(class, &resp)
}
pub async fn inventory<T: Transport>(transport: &mut T) -> Result<Vec<Status>> {
let mut out = Vec::new();
for class in ObjectClass::INVENTORY {
let mut session = match Session::open(transport, class).await {
Ok(s) => s,
Err(Error::ClassRefused { .. }) => continue,
Err(e) => return Err(e),
};
let result = status(&mut session).await;
session.commit().await?;
match result {
Ok(s) => out.push(s),
Err(Error::DeviceStatus(_)) => {}
Err(e) => return Err(e),
}
}
Ok(out)
}
pub async fn info<T: Transport, C>(
session: &mut Session<'_, T, C>,
at: Location,
) -> Result<ProgramInfo> {
let mut args = Vec::new();
at.write_to(&mut args);
let resp = session
.request(Service::Program, 10, cmd::INFO, &args)
.await?;
let info = ProgramInfo::decode(&resp)?;
if info.location != at {
return Err(Error::UnexpectedLocation {
requested: at,
reported: info.location,
});
}
Ok(info)
}
pub async fn read_program<T: Transport, C>(
session: &mut Session<'_, T, C>,
at: Location,
) -> Result<Vec<u8>> {
let (meta, body) = transfer_out(session, at).await?;
let file = envelope::wrap(&meta.format, at, meta.version, &body)?;
if let Some(expected) = meta.crc32 {
let actual = envelope::crc32(&body);
if expected != actual {
return Err(Error::Envelope(format!(
"body checksum mismatch: device reported {expected:08x}, received {actual:08x}"
)));
}
}
Ok(file)
}
pub async fn read_body<T: Transport, C>(
session: &mut Session<'_, T, C>,
at: Location,
) -> Result<Vec<u8>> {
Ok(transfer_out(session, at).await?.1)
}
const READ_CHUNK: u32 = 32720;
const WRITE_CHUNK: usize = 32720;
#[cfg(any(feature = "fault-injection", test))]
fn parse_chunk(name: &str, value: Option<&str>, default: u64) -> Result<u64> {
let Some(value) = value else {
return Ok(default);
};
value
.parse()
.ok()
.filter(|&size| size > 0)
.ok_or_else(|| Error::InvalidArgument(format!("{name} must be a positive integer")))
}
#[cfg(feature = "fault-injection")]
fn chunk_override(name: &str, default: u64) -> Result<u64> {
match std::env::var(name) {
Ok(value) => parse_chunk(name, Some(&value), default),
Err(std::env::VarError::NotPresent) => Ok(default),
Err(std::env::VarError::NotUnicode(_)) => {
Err(Error::InvalidArgument(format!("{name} must be UTF-8")))
}
}
}
#[cfg(feature = "fault-injection")]
fn read_chunk() -> Result<u32> {
let size = chunk_override("NORD_READ_CHUNK", READ_CHUNK.into())?;
u32::try_from(size).map_err(|_| Error::InvalidArgument("NORD_READ_CHUNK exceeds u32".into()))
}
#[cfg(not(feature = "fault-injection"))]
fn read_chunk() -> Result<u32> {
Ok(READ_CHUNK)
}
#[cfg(feature = "fault-injection")]
fn write_chunk() -> Result<usize> {
let size = chunk_override("NORD_WRITE_CHUNK", WRITE_CHUNK as u64)?;
usize::try_from(size)
.map_err(|_| Error::InvalidArgument("NORD_WRITE_CHUNK exceeds usize".into()))
}
#[cfg(not(feature = "fault-injection"))]
fn write_chunk() -> Result<usize> {
Ok(WRITE_CHUNK)
}
async fn transfer_out<T: Transport, C>(
session: &mut Session<'_, T, C>,
at: Location,
) -> Result<(ProgramInfo, Vec<u8>)> {
let chunk_size = read_chunk()?;
let meta = info(session, at).await?;
session.notify(&ui::label("Uploading...")?).await?;
let mut args = Vec::new();
at.write_to(&mut args);
session
.request(Service::Program, 10, cmd::BEGIN_READ, &args)
.await?;
let mut body = Vec::with_capacity((meta.body_len as usize).min(1 << 20));
let mut painted = None;
while (body.len() as u32) < meta.body_len {
let offset = body.len() as u32;
let want = chunk_size.min(meta.body_len - offset);
let mut req = args.clone();
req.extend_from_slice(&offset.to_be_bytes());
req.extend_from_slice(&want.to_be_bytes());
let resp = session
.request(Service::Program, 10, cmd::READ, &req)
.await?;
let chunk = read_payload(resp.payload(), at, offset, want)?;
body.extend_from_slice(chunk);
let pct = (body.len() as u64 * 100 / (meta.body_len.max(1)) as u64) as u16;
if painted != Some(pct) {
session.notify(&ui::percent(pct)).await?;
painted = Some(pct);
}
}
if painted != Some(100) {
session.notify(&ui::percent(100)).await?;
}
session
.request(Service::Program, 10, cmd::END_TRANSFER, &args)
.await?;
Ok((meta, body))
}
fn read_payload(payload: &[u8], at: Location, offset: u32, length: u32) -> Result<&[u8]> {
let echoed = (
read_u32(payload, 0)?,
read_u32(payload, 4)?,
read_u32(payload, 8)?,
read_u32(payload, 12)?,
);
let expected = (at.bank, at.slot, offset, length);
if echoed != expected {
return Err(Error::Transport(format!(
"READ response echoed {echoed:?}, expected {expected:?}"
)));
}
let body = &payload[16..];
if body.len() != length as usize {
return Err(Error::Transport(format!(
"asked for {length} bytes at offset {offset} but the device sent {}",
body.len()
)));
}
Ok(body)
}
const CLEANING_POLLS: u32 = 120;
const CLEANING_POLL_SPACING: std::time::Duration = std::time::Duration::from_millis(250);
async fn clean_library<T: Transport>(
session: &mut Session<'_, T, ReadWrite>,
blocks: u32,
) -> Result<()> {
session.notify(&ui::label("Cleaning...")?).await?;
session.notify(&ui::percent(0)).await?;
session
.request(
Service::Program,
10,
cmd::WRITE_PREPARE,
&blocks.to_be_bytes(),
)
.await?;
let mut painted = Some(0);
for polls in 0..CLEANING_POLLS {
if polls > 0 {
crate::sleep::sleep(CLEANING_POLL_SPACING).await;
}
let resp = session
.request(Service::Program, 10, cmd::WRITE_PREPARE_2, &[])
.await?;
let (requested, done, running) = cleaning_progress(resp.payload())?;
if running == 0 {
if painted != Some(100) {
session.notify(&ui::percent(100)).await?;
}
return Ok(());
}
let pct = (done as u64 * 100 / requested.max(1) as u64).min(99) as u16;
if painted != Some(pct) {
session.notify(&ui::percent(pct)).await?;
painted = Some(pct);
}
}
Err(Error::Transport(format!(
"the library's cleaning pass did not report ready within {} polls",
CLEANING_POLLS
)))
}
fn cleaning_progress(payload: &[u8]) -> Result<(u32, u32, u32)> {
Ok((
read_u32(payload, 0)?,
read_u32(payload, 4)?,
read_u32(payload, 8)?,
))
}
pub async fn reserve<T: Transport>(
session: &mut Session<'_, T, ReadWrite>,
blocks: u32,
) -> Result<()> {
let free = status(session).await?.free;
if blocks > free {
clean_library(session, blocks - free).await?;
}
Ok(())
}
pub async fn write<T: Transport>(
session: &mut Session<'_, T, ReadWrite>,
unit: AllocationUnit,
at: Location,
file: &[u8],
name: &str,
timestamp: u32,
) -> Result<()> {
if !unit.belongs_to(session.class().to_raw()) {
return Err(Error::InvalidArgument(format!(
"the allocation unit belongs to another partition, not {}",
session.class().label()
)));
}
let file = envelope::unwrap(file)?;
if !unit.is_bytes() {
reserve(session, unit.blocks_for(file.body.0.len())?).await?;
}
transfer_in(session, at, &file, name, timestamp).await
}
pub fn begin_write_args(
at: Location,
body_len: usize,
tag: &[u8; 4],
timestamp: u32,
name: &str,
) -> Result<Vec<u8>> {
let body_len = u32::try_from(body_len)
.map_err(|_| Error::InvalidArgument("the body is larger than the wire format".into()))?;
let name_len = u32::try_from(name.len())
.map_err(|_| Error::InvalidArgument("the name is larger than the wire format".into()))?;
let mut args = Vec::new();
at.write_to(&mut args);
args.extend_from_slice(&body_len.to_be_bytes());
args.extend_from_slice(tag);
args.extend_from_slice(×tamp.to_be_bytes());
args.extend_from_slice(&u32::MAX.to_be_bytes());
args.extend_from_slice(&name_len.to_be_bytes());
args.extend_from_slice(name.as_bytes());
Ok(args)
}
pub fn write_data_args(at: Location, offset: usize, chunk: &[u8]) -> Result<Vec<u8>> {
let offset = u32::try_from(offset)
.map_err(|_| Error::InvalidArgument("the offset is larger than the wire format".into()))?;
let len = u32::try_from(chunk.len())
.map_err(|_| Error::InvalidArgument("the chunk is larger than the wire format".into()))?;
let mut args = Vec::new();
at.write_to(&mut args);
args.extend_from_slice(&offset.to_be_bytes());
args.extend_from_slice(&len.to_be_bytes());
args.extend_from_slice(chunk);
Ok(args)
}
async fn transfer_in<T: Transport>(
session: &mut Session<'_, T, ReadWrite>,
at: Location,
file: &Cbin<RawBody>,
name: &str,
timestamp: u32,
) -> Result<()> {
let body = &file.body.0;
let chunk_size = write_chunk()?;
session.notify(&ui::label("Downloading...")?).await?;
let begin = begin_write_args(at, body.len(), &file.header.tag, timestamp, name)?;
session
.request(Service::Program, 10, cmd::BEGIN_WRITE, &begin)
.await?;
let mut offset = 0usize;
let mut painted = None;
while offset < body.len() {
let end = offset.saturating_add(chunk_size).min(body.len());
let data = write_data_args(at, offset, &body[offset..end])?;
if end == body.len() {
session
.request(Service::Program, 10, cmd::WRITE_DATA, &data)
.await?;
} else {
let msg = Message::new(Service::Program, 10, cmd::WRITE_DATA, data);
session.notify(&msg).await?;
}
offset = end;
let pct = (offset as u64 * 100 / (body.len().max(1)) as u64) as u16;
if painted != Some(pct) {
session.notify(&ui::percent(pct)).await?;
painted = Some(pct);
}
}
if painted != Some(100) {
session.notify(&ui::percent(100)).await?;
}
let mut args = Vec::new();
at.write_to(&mut args);
session
.request(Service::Program, 10, cmd::END_TRANSFER, &args)
.await?;
Ok(())
}
pub async fn select<T: Transport, C>(session: &mut Session<'_, T, C>, at: Location) -> Result<()> {
let mut args = Vec::new();
at.write_to(&mut args);
session
.request(Service::Program, 10, cmd::SELECT, &args)
.await?;
Ok(())
}
async fn drain<T: Transport>(transport: &mut T) -> Result<()> {
for _ in 0..RECOVER_DRAIN_CAP {
match transport
.read_timeout(crate::transport::READ_BUFFER, RECOVER_DRAIN_LIMIT)
.await?
{
Some(_) => continue,
None => break,
}
}
Ok(())
}
const RECOVER_DRAIN_LIMIT: std::time::Duration = std::time::Duration::from_millis(300);
const RECOVER_DRAIN_CAP: usize = 16;
async fn send_recovery<T: Transport>(transport: &mut T, msg: &Message, what: &str) -> Result<()> {
if transport.write_timeout(&msg.encode(), WRITE_LIMIT).await? {
return Ok(());
}
Err(Error::Transport(format!(
"the device did not accept {what} within {}s: bulk OUT endpoint {:#04x} is \
stalled, and only a power cycle clears it",
WRITE_LIMIT.as_secs(),
crate::transport::EP_OUT
)))
}
pub async fn recover<T: Transport>(transport: &mut T) -> Result<()> {
drain(transport).await?;
let goodbye = Message::new(Service::Ui, ui::SUBSYSTEM, ui::GOODBYE, Vec::new());
send_recovery(transport, &goodbye, "GOODBYE").await?;
let _ = transport
.read_timeout(crate::transport::READ_BUFFER, RECOVER_DRAIN_LIMIT)
.await?;
let close = Message::new(Service::Program, 10, cmd::SESSION_CLOSE, Vec::new());
send_recovery(transport, &close, "SESSION_CLOSE").await?;
let _ = transport
.read_timeout(crate::transport::READ_BUFFER, RECOVER_DRAIN_LIMIT)
.await?;
Ok(())
}
pub async fn partitions<T: Transport, C>(
session: &mut Session<'_, T, C>,
) -> Result<Vec<Partition>> {
let resp = session
.request(Service::Program, 10, cmd::PARTITIONS, &[])
.await?;
Partition::decode_all(&resp)
}
pub async fn banks<T: Transport, C>(
session: &mut Session<'_, T, C>,
partition: u32,
) -> Result<Vec<Bank>> {
let resp = session
.request(Service::Program, 10, cmd::BANKS, &partition.to_be_bytes())
.await?;
let payload = resp.payload();
if payload.len() < 4 {
return Err(Error::Truncated {
got: payload.len(),
need: 4,
});
}
let reported = u32::from_be_bytes(payload[..4].try_into().unwrap());
if reported != partition {
return Err(Error::UnexpectedPartition {
requested: partition,
reported,
});
}
Bank::decode_all(&resp)
}
pub async fn check_address<T: Transport, C>(
session: &mut Session<'_, T, C>,
at: Location,
) -> Result<Option<String>> {
let banks = banks(session, session.class().to_raw()).await?;
Ok(address_refusal(&banks, at))
}
pub(crate) fn address_refusal(banks: &[Bank], at: Location) -> Option<String> {
let Some(bank) = banks.get(at.bank as usize) else {
let names: Vec<&str> = banks.iter().map(|b| b.name.as_str()).collect();
return Some(format!(
"bank {} does not exist; this class has {} ({})",
at.user_bank(),
banks.len(),
names.join(", ")
));
};
(bank.is_bounded() && at.slot >= bank.slots).then(|| {
format!(
"\"{}\" holds {} slots, so slot {} is out of range",
bank.name,
bank.slots,
at.user_slot()
)
})
}
pub async fn focus<T: Transport, C>(session: &mut Session<'_, T, C>) -> Result<Location> {
let resp = session
.request(Service::Program, 10, cmd::FOCUS, &[])
.await?;
let p = resp.payload();
if p.len() < 8 {
return Err(Error::Truncated {
got: p.len(),
need: 8,
});
}
Ok(Location {
bank: u32::from_be_bytes(p[0..4].try_into().unwrap()),
slot: u32::from_be_bytes(p[4..8].try_into().unwrap()),
})
}
pub const ENUMERATION_DISABLED: u32 = 0x11;
pub const SLOT_BOUNDARY: u32 = 0xffff_ffff;
pub const ENUMERATION_LIMIT: usize = 4096;
pub async fn next_occupied<T: Transport, C>(
session: &mut Session<'_, T, C>,
at: Location,
) -> Result<Option<Location>> {
let mut args = Vec::new();
at.write_to(&mut args);
args.extend_from_slice(&0u32.to_be_bytes());
match session
.request(Service::Program, 10, cmd::NEXT_SLOT, &args)
.await
{
Ok(resp) => {
let p = resp.payload();
if p.len() < 8 {
return Err(Error::Truncated {
got: p.len(),
need: 8,
});
}
Ok(Some(Location {
bank: u32::from_be_bytes(p[0..4].try_into().unwrap()),
slot: u32::from_be_bytes(p[4..8].try_into().unwrap()),
}))
}
Err(Error::DeviceStatus(1)) => Ok(None),
Err(e) => Err(e),
}
}
pub async fn occupied_slots<T: Transport, C>(
session: &mut Session<'_, T, C>,
banks: &[Bank],
) -> Result<Vec<Location>> {
let mut found: Vec<Location> = Vec::new();
for bank in banks {
let mut at = Location {
bank: bank.index,
slot: SLOT_BOUNDARY,
};
let mut previous = None;
let limit = match bank.is_bounded() {
true => bank.slots,
false => Bank::UNBOUNDED,
};
while let Some(next) = next_occupied(session, at).await? {
let advanced = next.bank == bank.index
&& next.slot < limit
&& previous.is_none_or(|slot| next.slot > slot);
if !advanced {
return Err(Error::Enumeration {
bank: bank.index,
answered: next,
slots: bank.slots,
});
}
if found.len() >= ENUMERATION_LIMIT {
return Err(Error::ScanLimit {
bank: bank.index,
limit: ENUMERATION_LIMIT as u32,
});
}
found.push(next);
at = next;
previous = Some(next.slot);
}
}
Ok(found)
}
pub async fn dependencies<T: Transport, C>(
session: &mut Session<'_, T, C>,
at: Location,
) -> Result<Vec<Dependency>> {
let mut args = Vec::new();
at.write_to(&mut args);
let resp = session
.request(Service::Program, 10, cmd::DEPENDENCIES, &args)
.await?;
let dependencies = Dependency::decode_all(&resp)?;
let p = resp.payload();
let reported = Location {
bank: u32::from_be_bytes(p[0..4].try_into().unwrap()),
slot: u32::from_be_bytes(p[4..8].try_into().unwrap()),
};
if reported != at {
return Err(Error::UnexpectedLocation {
requested: at,
reported,
});
}
Ok(dependencies)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Referrer {
pub at: Location,
pub name: String,
pub version: u32,
pub programs: Vec<Location>,
}
pub async fn set_lists_referencing<T: Transport, C>(
session: &mut Session<'_, T, C>,
banks: &[Bank],
targets: &[Location],
) -> Result<Vec<Referrer>> {
if session.class() != ObjectClass::SetList {
return Err(Error::InvalidArgument(
"set-list referrers require a set-list session".into(),
));
}
let mut out = Vec::new();
if targets.is_empty() {
return Ok(out);
}
for at in occupied_slots(session, banks).await? {
let mut programs: Vec<Location> = Vec::new();
for l in dependencies(session, at)
.await?
.into_iter()
.filter(|d| d.class == ObjectClass::Program && d.is_required())
.filter_map(|d| d.location)
{
if targets.contains(&l) && !programs.contains(&l) {
programs.push(l);
}
}
if programs.is_empty() {
continue;
}
let meta = info(session, at).await?;
out.push(Referrer {
at,
name: meta.name,
version: meta.version,
programs,
});
}
Ok(out)
}
pub async fn move_object<T: Transport>(
session: &mut Session<'_, T, ReadWrite>,
from: Location,
to: Location,
) -> Result<()> {
let mut args = Vec::new();
from.write_to(&mut args);
to.write_to(&mut args);
session
.request(Service::Program, 10, cmd::MOVE, &args)
.await?;
Ok(())
}
pub async fn delete<T: Transport>(
session: &mut Session<'_, T, ReadWrite>,
at: Location,
) -> Result<()> {
session.notify(&ui::label("Deleting...")?).await?;
let mut args = Vec::new();
at.write_to(&mut args);
session
.request(Service::Program, 10, cmd::DELETE, &args)
.await?;
Ok(())
}
pub async fn rename<T: Transport>(
session: &mut Session<'_, T, ReadWrite>,
at: Location,
name: &str,
) -> Result<()> {
let mut args = Vec::new();
at.write_to(&mut args);
let name_len = u32::try_from(name.len())
.map_err(|_| Error::InvalidArgument("the name is larger than the wire format".into()))?;
args.extend_from_slice(&name_len.to_be_bytes());
args.extend_from_slice(name.as_bytes());
session
.request(Service::Program, 10, cmd::RENAME, &args)
.await?;
Ok(())
}
pub async fn duplicate<T: Transport>(
session: &mut Session<'_, T, ReadWrite>,
from: Location,
to: Location,
) -> Result<()> {
let mut args = Vec::new();
from.write_to(&mut args);
to.write_to(&mut args);
session
.request(Service::Program, 10, cmd::COPY, &args)
.await?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_read_chunk_must_echo_its_request() {
let at = Location { bank: 2, slot: 3 };
let mut payload = Vec::new();
for word in [at.bank, at.slot, 40, 3] {
payload.extend_from_slice(&word.to_be_bytes());
}
payload.extend_from_slice(&[1, 2, 3]);
assert_eq!(read_payload(&payload, at, 40, 3).unwrap(), [1, 2, 3]);
payload[3] ^= 1;
assert!(read_payload(&payload, at, 40, 3).is_err());
}
#[test]
fn invalid_chunk_overrides_are_refused() {
assert!(parse_chunk("NORD_READ_CHUNK", Some("0"), READ_CHUNK.into()).is_err());
assert!(parse_chunk("NORD_WRITE_CHUNK", Some("bad"), WRITE_CHUNK as u64).is_err());
assert_eq!(
parse_chunk("NORD_READ_CHUNK", None, READ_CHUNK.into()).unwrap(),
READ_CHUNK.into()
);
}
#[test]
fn cleaning_progress_requires_all_three_words() {
let err = cleaning_progress(&[0; 11]).expect_err("a partial cleaning reply");
assert!(matches!(err, Error::Truncated { got: 11, need: 12 }));
}
struct Stalled;
impl Transport for Stalled {
async fn write(&mut self, _buf: &[u8]) -> Result<()> {
panic!("recovery frames must carry a deadline");
}
async fn read(&mut self, _max: usize) -> Result<Vec<u8>> {
panic!("recovery reads must carry a deadline");
}
async fn write_timeout(
&mut self,
_buf: &[u8],
_limit: std::time::Duration,
) -> Result<bool> {
Ok(false)
}
async fn read_timeout(
&mut self,
_max: usize,
_limit: std::time::Duration,
) -> Result<Option<Vec<u8>>> {
Ok(None)
}
}
#[test]
fn recover_names_the_stalled_endpoint_instead_of_waiting_forever() {
let err = pollster::block_on(recover(&mut Stalled)).expect_err("the write is refused");
let message = err.to_string();
assert!(message.contains("GOODBYE"), "{message}");
assert!(message.contains("0x03"), "{message}");
}
}