use crate::error::{Error, Result};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum Capability {
ImageJpeg,
ImageRaw,
VideoMjpeg,
VideoH264,
AudioPcmIn,
AudioPcmOut,
AudioOpus,
AudioFlac,
WifiStation,
WifiSoftAp,
WifiCsi,
RadarPresence,
RadarMmWave,
BleGatt,
EspNow,
LoraP2p,
LoraWan,
IrohLanDirect,
IrohRelay,
MidDevice,
MidAdopted,
Gpio,
Telemetry,
Ota,
}
impl Capability {
pub const ALL: &'static [Capability] = &[
Capability::ImageJpeg,
Capability::ImageRaw,
Capability::VideoMjpeg,
Capability::VideoH264,
Capability::AudioPcmIn,
Capability::AudioPcmOut,
Capability::AudioOpus,
Capability::AudioFlac,
Capability::WifiStation,
Capability::WifiSoftAp,
Capability::WifiCsi,
Capability::RadarPresence,
Capability::RadarMmWave,
Capability::BleGatt,
Capability::EspNow,
Capability::LoraP2p,
Capability::LoraWan,
Capability::IrohLanDirect,
Capability::IrohRelay,
Capability::MidDevice,
Capability::MidAdopted,
Capability::Gpio,
Capability::Telemetry,
Capability::Ota,
];
#[must_use]
pub const fn tag(self) -> &'static str {
match self {
Capability::ImageJpeg => "image.jpeg",
Capability::ImageRaw => "image.raw",
Capability::VideoMjpeg => "video.mjpeg",
Capability::VideoH264 => "video.h264",
Capability::AudioPcmIn => "audio.pcm.in",
Capability::AudioPcmOut => "audio.pcm.out",
Capability::AudioOpus => "audio.opus",
Capability::AudioFlac => "audio.flac",
Capability::WifiStation => "wifi.sta",
Capability::WifiSoftAp => "wifi.ap",
Capability::WifiCsi => "wifi.csi",
Capability::RadarPresence => "radar.presence",
Capability::RadarMmWave => "radar.mmwave",
Capability::BleGatt => "ble.gatt",
Capability::EspNow => "espnow",
Capability::LoraP2p => "lora.p2p",
Capability::LoraWan => "lora.wan",
Capability::IrohLanDirect => "iroh.lan",
Capability::IrohRelay => "iroh.relay",
Capability::MidDevice => "mid.device",
Capability::MidAdopted => "mid.adopted",
Capability::Gpio => "gpio",
Capability::Telemetry => "telemetry",
Capability::Ota => "ota",
}
}
#[must_use]
pub fn parse(tag: &str) -> Option<Capability> {
Capability::ALL.iter().copied().find(|c| c.tag() == tag)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Status {
Available,
Preview,
Planned,
}
impl Status {
#[must_use]
pub const fn tag(self) -> &'static str {
match self {
Status::Available => "available",
Status::Preview => "preview",
Status::Planned => "planned",
}
}
#[must_use]
pub fn parse(tag: &str) -> Option<Status> {
match tag {
"available" => Some(Status::Available),
"preview" => Some(Status::Preview),
"planned" => Some(Status::Planned),
_ => None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Declared {
pub capability: Capability,
pub status: Status,
pub backing: &'static str,
}
impl Declared {
#[must_use]
pub const fn available(capability: Capability, backing: &'static str) -> Self {
Declared {
capability,
status: Status::Available,
backing,
}
}
#[must_use]
pub const fn preview(capability: Capability, backing: &'static str) -> Self {
Declared {
capability,
status: Status::Preview,
backing,
}
}
#[must_use]
pub const fn planned(capability: Capability) -> Self {
Declared {
capability,
status: Status::Planned,
backing: "",
}
}
pub fn validate(&self) -> Result<()> {
let ok = match self.status {
Status::Available | Status::Preview => !self.backing.is_empty(),
Status::Planned => self.backing.is_empty(),
};
let clean = self
.backing
.bytes()
.all(|b| b.is_ascii_alphanumeric() || b == b'_' || b == b'-');
if ok && clean {
Ok(())
} else {
Err(Error::InvalidFormat)
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum Chip {
Esp32,
Esp32S2,
Esp32S3,
Esp32C3,
Esp32C5,
Esp32C6,
Esp32C61,
Esp32H2,
Esp32P4,
}
impl Chip {
#[must_use]
pub const fn tag(self) -> &'static str {
match self {
Chip::Esp32 => "esp32",
Chip::Esp32S2 => "esp32s2",
Chip::Esp32S3 => "esp32s3",
Chip::Esp32C3 => "esp32c3",
Chip::Esp32C5 => "esp32c5",
Chip::Esp32C6 => "esp32c6",
Chip::Esp32C61 => "esp32c61",
Chip::Esp32H2 => "esp32h2",
Chip::Esp32P4 => "esp32p4",
}
}
#[must_use]
pub const fn has_wifi(self) -> bool {
!matches!(self, Chip::Esp32H2 | Chip::Esp32P4)
}
#[must_use]
pub const fn has_pie(self) -> bool {
matches!(self, Chip::Esp32S3 | Chip::Esp32P4)
}
pub const ALL: &'static [Chip] = &[
Chip::Esp32,
Chip::Esp32S2,
Chip::Esp32S3,
Chip::Esp32C3,
Chip::Esp32C5,
Chip::Esp32C6,
Chip::Esp32C61,
Chip::Esp32H2,
Chip::Esp32P4,
];
#[must_use]
pub fn parse(tag: &str) -> Option<Chip> {
Chip::ALL.iter().copied().find(|c| c.tag() == tag)
}
}
pub const MAX_FIELD_LEN: usize = 64;
#[derive(Debug, Clone, Copy)]
pub struct Manifest<'a> {
pub model: &'a str,
pub firmware: &'a str,
pub chip: Chip,
pub declared: &'a [Declared],
}
impl<'a> Manifest<'a> {
#[must_use]
pub fn has(&self, capability: Capability) -> bool {
self.declared
.iter()
.any(|d| d.capability == capability && d.status == Status::Available)
}
pub fn validate(&self) -> Result<()> {
check_field(self.model)?;
check_field(self.firmware)?;
for (i, d) in self.declared.iter().enumerate() {
d.validate()?;
if self.declared[..i]
.iter()
.any(|e| e.capability == d.capability)
{
return Err(Error::InvalidFormat);
}
}
Ok(())
}
pub fn encode(&self, out: &mut [u8]) -> Result<usize> {
self.validate()?;
encode_lines(out, self.model, self.firmware, self.chip, |cap| {
self.declared
.iter()
.find(|d| d.capability == cap)
.map(|d| (d.status, d.backing))
})
}
#[cfg(feature = "alloc")]
pub fn parse(bytes: &[u8]) -> Result<ParsedManifest> {
ParsedManifest::parse(bytes)
}
#[must_use]
pub fn encoded_len(&self) -> usize {
let mut n = "janus/1\n".len()
+ "model=\n".len()
+ self.model.len()
+ "fw=\n".len()
+ self.firmware.len()
+ "chip=\n".len()
+ self.chip.tag().len();
for d in self.declared {
n += "cap=::\n".len()
+ d.capability.tag().len()
+ d.status.tag().len()
+ d.backing.len();
}
n
}
}
fn check_field(field: &str) -> Result<()> {
if field.is_empty()
|| field.len() > MAX_FIELD_LEN
|| field.bytes().any(|b| b == b'\n' || b == b'=' || b < 0x20)
{
return Err(Error::InvalidFormat);
}
Ok(())
}
fn encode_lines<'s>(
out: &mut [u8],
model: &str,
firmware: &str,
chip: Chip,
mut find: impl FnMut(Capability) -> Option<(Status, &'s str)>,
) -> Result<usize> {
let mut w = Cursor::new(out);
w.str("janus/")?;
w.byte(b'0' + crate::FORMAT_VERSION)?;
w.byte(b'\n')?;
w.str("model=")?;
w.str(model)?;
w.byte(b'\n')?;
w.str("fw=")?;
w.str(firmware)?;
w.byte(b'\n')?;
w.str("chip=")?;
w.str(chip.tag())?;
w.byte(b'\n')?;
for cap in Capability::ALL {
if let Some((status, backing)) = find(*cap) {
w.str("cap=")?;
w.str(cap.tag())?;
w.byte(b':')?;
w.str(status.tag())?;
w.byte(b':')?;
w.str(backing)?;
w.byte(b'\n')?;
}
}
Ok(w.len())
}
#[cfg(feature = "alloc")]
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct ParsedDeclared {
pub capability: Capability,
pub status: Status,
pub backing: alloc::string::String,
}
#[cfg(feature = "alloc")]
impl ParsedDeclared {
pub fn validate(&self) -> Result<()> {
let ok = match self.status {
Status::Available | Status::Preview => !self.backing.is_empty(),
Status::Planned => self.backing.is_empty(),
};
let clean = self
.backing
.bytes()
.all(|b| b.is_ascii_alphanumeric() || b == b'_' || b == b'-');
if ok && clean {
Ok(())
} else {
Err(Error::InvalidFormat)
}
}
}
#[cfg(feature = "alloc")]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ParsedManifest {
pub model: alloc::string::String,
pub firmware: alloc::string::String,
pub chip: Chip,
pub declared: alloc::vec::Vec<ParsedDeclared>,
}
#[cfg(feature = "alloc")]
impl ParsedManifest {
pub fn parse(bytes: &[u8]) -> Result<Self> {
use alloc::string::ToString;
let text = core::str::from_utf8(bytes).map_err(|_| Error::InvalidFormat)?;
let body = text.strip_suffix('\n').ok_or(Error::InvalidFormat)?;
let mut lines = body.split('\n');
match lines.next() {
Some("janus/1") => {}
Some(v) if v.starts_with("janus/") => return Err(Error::Unsupported),
_ => return Err(Error::InvalidFormat),
}
let model = lines
.next()
.and_then(|l| l.strip_prefix("model="))
.ok_or(Error::InvalidFormat)?;
let firmware = lines
.next()
.and_then(|l| l.strip_prefix("fw="))
.ok_or(Error::InvalidFormat)?;
let chip_tag = lines
.next()
.and_then(|l| l.strip_prefix("chip="))
.ok_or(Error::InvalidFormat)?;
check_field(model)?;
check_field(firmware)?;
let chip = Chip::parse(chip_tag).ok_or(Error::Unsupported)?;
let mut declared = alloc::vec::Vec::new();
for line in lines {
let rest = line.strip_prefix("cap=").ok_or(Error::InvalidFormat)?;
let mut parts = rest.splitn(3, ':');
let (tag, status, backing) = match (parts.next(), parts.next(), parts.next()) {
(Some(t), Some(s), Some(b)) => (t, s, b),
_ => return Err(Error::InvalidFormat),
};
let capability = Capability::parse(tag).ok_or(Error::Unsupported)?;
let status = Status::parse(status).ok_or(Error::InvalidFormat)?;
let d = ParsedDeclared {
capability,
status,
backing: backing.to_string(),
};
d.validate()?;
if declared
.iter()
.any(|e: &ParsedDeclared| e.capability == capability)
{
return Err(Error::InvalidFormat);
}
declared.push(d);
}
Ok(ParsedManifest {
model: model.to_string(),
firmware: firmware.to_string(),
chip,
declared,
})
}
#[must_use]
pub fn has(&self, capability: Capability) -> bool {
self.declared
.iter()
.any(|d| d.capability == capability && d.status == Status::Available)
}
pub fn validate(&self) -> Result<()> {
check_field(&self.model)?;
check_field(&self.firmware)?;
for (i, d) in self.declared.iter().enumerate() {
d.validate()?;
if self.declared[..i]
.iter()
.any(|e| e.capability == d.capability)
{
return Err(Error::InvalidFormat);
}
}
Ok(())
}
pub fn encode(&self, out: &mut [u8]) -> Result<usize> {
self.validate()?;
encode_lines(out, &self.model, &self.firmware, self.chip, |cap| {
self.declared
.iter()
.find(|d| d.capability == cap)
.map(|d| (d.status, d.backing.as_str()))
})
}
#[must_use]
pub fn encoded_len(&self) -> usize {
let mut n = "janus/1\n".len()
+ "model=\n".len()
+ self.model.len()
+ "fw=\n".len()
+ self.firmware.len()
+ "chip=\n".len()
+ self.chip.tag().len();
for d in &self.declared {
n += "cap=::\n".len()
+ d.capability.tag().len()
+ d.status.tag().len()
+ d.backing.len();
}
n
}
pub fn to_bytes(&self) -> Result<alloc::vec::Vec<u8>> {
let mut out = alloc::vec![0u8; self.encoded_len()];
let n = self.encode(&mut out)?;
out.truncate(n);
Ok(out)
}
}
struct Cursor<'a> {
out: &'a mut [u8],
pos: usize,
}
impl<'a> Cursor<'a> {
fn new(out: &'a mut [u8]) -> Self {
Cursor { out, pos: 0 }
}
fn len(&self) -> usize {
self.pos
}
fn byte(&mut self, b: u8) -> Result<()> {
match self.out.get_mut(self.pos) {
Some(slot) => {
*slot = b;
self.pos += 1;
Ok(())
}
None => Err(Error::BufferTooSmall {
needed: self.pos + 1,
}),
}
}
fn str(&mut self, s: &str) -> Result<()> {
let end = self.pos + s.len();
match self.out.get_mut(self.pos..end) {
Some(slot) => {
slot.copy_from_slice(s.as_bytes());
self.pos = end;
Ok(())
}
None => Err(Error::BufferTooSmall { needed: end }),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
const DECLARED: &[Declared] = &[
Declared::planned(Capability::IrohRelay),
Declared::available(Capability::ImageJpeg, "rusty_esp_image"),
Declared::preview(Capability::MidDevice, "rusty_esp_mid"),
];
fn manifest() -> Manifest<'static> {
Manifest {
model: "acme/doorbell-2",
firmware: "1.4.0",
chip: Chip::Esp32S3,
declared: DECLARED,
}
}
#[test]
fn tags_round_trip() {
for c in Capability::ALL {
assert_eq!(Capability::parse(c.tag()), Some(*c));
}
assert_eq!(Capability::parse("nope"), None);
for s in [Status::Available, Status::Preview, Status::Planned] {
assert_eq!(Status::parse(s.tag()), Some(s));
}
}
#[test]
fn honesty_rule() {
assert!(
Declared::available(Capability::Gpio, "x")
.validate()
.is_ok()
);
assert!(
Declared::available(Capability::Gpio, "")
.validate()
.is_err()
);
assert!(Declared::planned(Capability::Gpio).validate().is_ok());
assert!(
Declared {
capability: Capability::Gpio,
status: Status::Planned,
backing: "x",
}
.validate()
.is_err()
);
assert!(
Declared::available(Capability::Gpio, "bad crate")
.validate()
.is_err()
);
}
#[test]
fn encoding_is_canonical_and_sized() {
let m = manifest();
let mut buf = [0u8; 256];
let n = m.encode(&mut buf).unwrap();
assert_eq!(n, m.encoded_len());
let text = core::str::from_utf8(&buf[..n]).unwrap();
assert_eq!(
text,
"janus/1\nmodel=acme/doorbell-2\nfw=1.4.0\nchip=esp32s3\ncap=image.jpeg:available:rusty_esp_image\ncap=iroh.relay:planned:\ncap=mid.device:preview:rusty_esp_mid\n"
);
assert!(m.has(Capability::ImageJpeg));
assert!(!m.has(Capability::MidDevice));
let mut small = [0u8; 16];
assert!(matches!(
m.encode(&mut small),
Err(Error::BufferTooSmall { .. })
));
}
#[test]
fn rejects_duplicates_and_bad_fields() {
let dup = [
Declared::planned(Capability::Gpio),
Declared::planned(Capability::Gpio),
];
let m = Manifest {
declared: &dup,
..manifest()
};
assert_eq!(m.validate(), Err(Error::InvalidFormat));
let m = Manifest {
model: "bad=model",
..manifest()
};
assert_eq!(m.validate(), Err(Error::InvalidFormat));
}
#[test]
fn chip_tags_parse_back() {
for c in Chip::ALL {
assert_eq!(Chip::parse(c.tag()), Some(*c));
}
assert_eq!(Chip::parse("esp8266"), None);
assert_eq!(Chip::ALL.len(), 9);
}
#[cfg(feature = "alloc")]
struct Lcg(u64);
impl Lcg {
fn next(&mut self) -> u64 {
self.0 = self
.0
.wrapping_mul(6_364_136_223_846_793_005)
.wrapping_add(1_442_695_040_888_963_407);
self.0 >> 33
}
fn below(&mut self, n: u64) -> u64 {
self.next() % n
}
fn field(&mut self, out: &mut alloc::string::String, max: usize) {
const ALPHABET: &[u8] =
b"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789/-_. :+";
let len = 1 + self.below(max as u64) as usize;
out.clear();
for _ in 0..len {
out.push(ALPHABET[self.below(ALPHABET.len() as u64) as usize] as char);
}
}
fn backing(&mut self, out: &mut alloc::string::String) {
const ALPHABET: &[u8] = b"abcdefghijklmnopqrstuvwxyz0123456789_-";
let len = 1 + self.below(24) as usize;
out.clear();
for _ in 0..len {
out.push(ALPHABET[self.below(ALPHABET.len() as u64) as usize] as char);
}
}
}
#[cfg(feature = "alloc")]
#[test]
fn parse_round_trips_a_corpus_byte_for_byte() {
let mut rng = Lcg(0x4A414E5553);
let mut model = alloc::string::String::new();
let mut firmware = alloc::string::String::new();
let mut backing = alloc::string::String::new();
let mut buf = [0u8; 4096];
let mut again = [0u8; 4096];
for _ in 0..400 {
rng.field(&mut model, MAX_FIELD_LEN);
rng.field(&mut firmware, MAX_FIELD_LEN);
let chip = Chip::ALL[rng.below(Chip::ALL.len() as u64) as usize];
let mut declared = alloc::vec::Vec::new();
for cap in Capability::ALL {
match rng.below(4) {
0 => {}
1 => declared.push(ParsedDeclared {
capability: *cap,
status: Status::Planned,
backing: alloc::string::String::new(),
}),
k => {
rng.backing(&mut backing);
declared.push(ParsedDeclared {
capability: *cap,
status: if k == 2 {
Status::Available
} else {
Status::Preview
},
backing: backing.clone(),
});
}
}
}
for i in (1..declared.len()).rev() {
let j = rng.below(i as u64 + 1) as usize;
declared.swap(i, j);
}
let m = ParsedManifest {
model: model.clone(),
firmware: firmware.clone(),
chip,
declared,
};
let n = m.encode(&mut buf).unwrap();
assert_eq!(n, m.encoded_len());
let back = ParsedManifest::parse(&buf[..n]).unwrap();
assert_eq!(back.model, m.model);
assert_eq!(back.firmware, m.firmware);
assert_eq!(back.chip, m.chip);
assert_eq!(back.declared.len(), m.declared.len());
let k = back.encode(&mut again).unwrap();
assert_eq!(&again[..k], &buf[..n], "re-encoding is byte-identical");
assert_eq!(back.to_bytes().unwrap(), &buf[..n]);
for d in &m.declared {
assert_eq!(back.has(d.capability), d.status == Status::Available);
}
}
}
#[cfg(feature = "alloc")]
#[test]
fn parse_agrees_with_the_borrowed_form_and_refuses_the_rest() {
let m = manifest();
let mut buf = [0u8; 512];
let n = m.encode(&mut buf).unwrap();
let p = Manifest::parse(&buf[..n]).unwrap();
assert_eq!(p.model, m.model);
assert_eq!(p.chip, m.chip);
assert_eq!(p.declared.len(), m.declared.len());
assert_eq!(p.to_bytes().unwrap(), &buf[..n]);
let mut rng = Lcg(7);
for _ in 0..2000 {
let mut bytes = buf[..n].to_vec();
match rng.below(3) {
0 => {
let i = rng.below(bytes.len() as u64) as usize;
bytes[i] = rng.below(256) as u8;
}
1 => {
let cut = rng.below(bytes.len() as u64) as usize;
bytes.truncate(cut);
}
_ => {
let i = rng.below(bytes.len() as u64) as usize;
bytes.insert(i, b'\n');
}
}
if let Ok(again) = ParsedManifest::parse(&bytes) {
assert_eq!(again.to_bytes().unwrap(), bytes);
}
}
let cases: [(&[u8], Error); 6] = [
(b"janus/2\nmodel=a\nfw=1\nchip=esp32\n", Error::Unsupported),
(
b"janus/1\nmodel=a\nfw=1\nchip=esp8266\n",
Error::Unsupported,
),
(
b"janus/1\nmodel=a\nfw=1\nchip=esp32\ncap=warp.drive:available:x\n",
Error::Unsupported,
),
(
b"janus/1\nfw=1\nmodel=a\nchip=esp32\n",
Error::InvalidFormat,
),
(b"janus/1\nmodel=a\nfw=1\nchip=esp32", Error::InvalidFormat),
(
b"janus/1\nmodel=a\nfw=1\nchip=esp32\ncap=gpio:available:x\ncap=gpio:planned:\n",
Error::InvalidFormat,
),
];
for (bytes, err) in cases {
assert_eq!(
ParsedManifest::parse(bytes).err(),
Some(err),
"{:?}",
core::str::from_utf8(bytes)
);
}
}
#[test]
fn chip_facts() {
assert!(!Chip::Esp32P4.has_wifi());
assert!(Chip::Esp32P4.has_pie());
assert!(Chip::Esp32C6.has_wifi());
assert!(!Chip::Esp32C6.has_pie());
}
}