Skip to content

Commit 06cfade

Browse files
authored
add safe byte codecs for the typed protocol payloads (#51)
1 parent 383e14e commit 06cfade

4 files changed

Lines changed: 165 additions & 2 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,12 @@ Releases up through `v0.4.0` were workspace-wide and tagged `vX.Y.Z`. From `0.4.
44

55
## [Unreleased]
66

7+
## [tinyboot-protocol 0.5.1] - 2026-10-20
8+
9+
### Added
10+
11+
- `InfoData`, `EraseData` and `VerifyData` gain `from_bytes` and `to_bytes`, so a host reads and writes them from plain byte slices without going through the `Data` union. `EraseData` and `VerifyData` gain `SIZE` like `InfoData`.
12+
713
## [tinyboot-core 0.6.1] - 2026-10-20
814

915
### Added

‎cli/Cargo.lock‎

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎lib/protocol/Cargo.toml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[package]
22
name = "tinyboot-protocol"
33
description = "Protocol implementation for tinyboot bootloader"
4-
version = "0.5.0"
4+
version = "0.5.1"
55
edition.workspace = true
66
repository.workspace = true
77
license.workspace = true

‎lib/protocol/src/frame.rs‎

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,38 @@ pub struct InfoData {
4040
impl InfoData {
4141
/// Encoded size in bytes.
4242
pub const SIZE: u16 = core::mem::size_of::<Self>() as u16;
43+
44+
/// Decode from the start of `bytes` (little-endian fields in declaration
45+
/// order); `None` when `bytes` is shorter than [`SIZE`](Self::SIZE).
46+
/// Bytes past `SIZE` are ignored, so a longer reply from a newer device
47+
/// still decodes.
48+
pub fn from_bytes(bytes: &[u8]) -> Option<Self> {
49+
let b = bytes.get(..Self::SIZE as usize)?;
50+
Some(Self {
51+
capacity: u32::from_le_bytes([b[0], b[1], b[2], b[3]]),
52+
erase_size: u16::from_le_bytes([b[4], b[5]]),
53+
boot_version: u16::from_le_bytes([b[6], b[7]]),
54+
app_version: u16::from_le_bytes([b[8], b[9]]),
55+
mode: b[10],
56+
boot_baud: b[11],
57+
compat: u32::from_le_bytes([b[12], b[13], b[14], b[15]]),
58+
max_payload: u16::from_le_bytes([b[16], b[17]]),
59+
})
60+
}
61+
62+
/// Encode as little-endian fields in declaration order.
63+
pub fn to_bytes(&self) -> [u8; Self::SIZE as usize] {
64+
let mut b = [0; Self::SIZE as usize];
65+
b[0..4].copy_from_slice(&{ self.capacity }.to_le_bytes());
66+
b[4..6].copy_from_slice(&{ self.erase_size }.to_le_bytes());
67+
b[6..8].copy_from_slice(&{ self.boot_version }.to_le_bytes());
68+
b[8..10].copy_from_slice(&{ self.app_version }.to_le_bytes());
69+
b[10] = self.mode;
70+
b[11] = self.boot_baud;
71+
b[12..16].copy_from_slice(&{ self.compat }.to_le_bytes());
72+
b[16..18].copy_from_slice(&{ self.max_payload }.to_le_bytes());
73+
b
74+
}
4375
}
4476

4577
/// Typed Erase request data.
@@ -50,6 +82,27 @@ pub struct EraseData {
5082
pub byte_count: u16,
5183
}
5284

85+
impl EraseData {
86+
/// Encoded size in bytes.
87+
pub const SIZE: u16 = core::mem::size_of::<Self>() as u16;
88+
89+
/// Decode from the start of `bytes` (little-endian fields in declaration
90+
/// order); `None` when `bytes` is shorter than [`SIZE`](Self::SIZE).
91+
/// Bytes past `SIZE` are ignored, so a longer reply from a newer device
92+
/// still decodes.
93+
pub fn from_bytes(bytes: &[u8]) -> Option<Self> {
94+
let b = bytes.get(..Self::SIZE as usize)?;
95+
Some(Self {
96+
byte_count: u16::from_le_bytes([b[0], b[1]]),
97+
})
98+
}
99+
100+
/// Encode as little-endian fields in declaration order.
101+
pub fn to_bytes(&self) -> [u8; Self::SIZE as usize] {
102+
self.byte_count.to_le_bytes()
103+
}
104+
}
105+
53106
/// Typed Verify response data.
54107
#[repr(C)]
55108
#[derive(Clone, Copy)]
@@ -58,6 +111,27 @@ pub struct VerifyData {
58111
pub crc: u16,
59112
}
60113

114+
impl VerifyData {
115+
/// Encoded size in bytes.
116+
pub const SIZE: u16 = core::mem::size_of::<Self>() as u16;
117+
118+
/// Decode from the start of `bytes` (little-endian fields in declaration
119+
/// order); `None` when `bytes` is shorter than [`SIZE`](Self::SIZE).
120+
/// Bytes past `SIZE` are ignored, so a longer reply from a newer device
121+
/// still decodes.
122+
pub fn from_bytes(bytes: &[u8]) -> Option<Self> {
123+
let b = bytes.get(..Self::SIZE as usize)?;
124+
Some(Self {
125+
crc: u16::from_le_bytes([b[0], b[1]]),
126+
})
127+
}
128+
129+
/// Encode as little-endian fields in declaration order.
130+
pub fn to_bytes(&self) -> [u8; Self::SIZE as usize] {
131+
self.crc.to_le_bytes()
132+
}
133+
}
134+
61135
/// Per-command flags occupying addr byte 3.
62136
///
63137
/// Each command can define its own bitflags type as a union variant.
@@ -448,6 +522,89 @@ mod tests {
448522
assert_eq!(InfoData::SIZE, 18);
449523
}
450524

525+
fn union_bytes<const L: usize>(d: &Data) -> [u8; L] {
526+
let mut b = [0; L];
527+
b.copy_from_slice(unsafe { &d.raw[..L] });
528+
b
529+
}
530+
531+
fn info() -> InfoData {
532+
InfoData {
533+
capacity: 0x0403_0201,
534+
erase_size: 0x0605,
535+
boot_version: 0x0807,
536+
app_version: 0x0A09,
537+
mode: 0x0B,
538+
boot_baud: 0x0C,
539+
compat: 0x100F_0E0D,
540+
max_payload: 0x1211,
541+
}
542+
}
543+
544+
fn assert_info_eq(a: &InfoData, b: &InfoData) {
545+
assert_eq!({ a.capacity }, { b.capacity });
546+
assert_eq!({ a.erase_size }, { b.erase_size });
547+
assert_eq!({ a.boot_version }, { b.boot_version });
548+
assert_eq!({ a.app_version }, { b.app_version });
549+
assert_eq!(a.mode, b.mode);
550+
assert_eq!(a.boot_baud, b.boot_baud);
551+
assert_eq!({ a.compat }, { b.compat });
552+
assert_eq!({ a.max_payload }, { b.max_payload });
553+
}
554+
555+
#[test]
556+
fn info_bytes_match_union() {
557+
let d = Data { info: info() };
558+
let raw = union_bytes::<{ InfoData::SIZE as usize }>(&d);
559+
assert_info_eq(&InfoData::from_bytes(&raw).unwrap(), &info());
560+
assert_eq!(info().to_bytes(), raw);
561+
}
562+
563+
#[test]
564+
fn info_from_bytes_length() {
565+
let raw = info().to_bytes();
566+
assert!(InfoData::from_bytes(&raw[..raw.len() - 1]).is_none());
567+
let mut long = [0xEE; InfoData::SIZE as usize + 3];
568+
long[..raw.len()].copy_from_slice(&raw);
569+
assert_info_eq(&InfoData::from_bytes(&long).unwrap(), &info());
570+
}
571+
572+
#[test]
573+
fn erase_bytes_match_union() {
574+
let d = Data {
575+
erase: EraseData { byte_count: 0x0201 },
576+
};
577+
let raw = union_bytes::<{ EraseData::SIZE as usize }>(&d);
578+
assert_eq!(EraseData::from_bytes(&raw).unwrap().byte_count, 0x0201);
579+
assert_eq!(EraseData { byte_count: 0x0201 }.to_bytes(), raw);
580+
}
581+
582+
#[test]
583+
fn erase_from_bytes_length() {
584+
assert_eq!(EraseData::SIZE, 2);
585+
assert!(EraseData::from_bytes(&[0x01]).is_none());
586+
let long = [0x01, 0x02, 0xEE, 0xEE, 0xEE];
587+
assert_eq!(EraseData::from_bytes(&long).unwrap().byte_count, 0x0201);
588+
}
589+
590+
#[test]
591+
fn verify_bytes_match_union() {
592+
let d = Data {
593+
verify: VerifyData { crc: 0x0201 },
594+
};
595+
let raw = union_bytes::<{ VerifyData::SIZE as usize }>(&d);
596+
assert_eq!(VerifyData::from_bytes(&raw).unwrap().crc, 0x0201);
597+
assert_eq!(VerifyData { crc: 0x0201 }.to_bytes(), raw);
598+
}
599+
600+
#[test]
601+
fn verify_from_bytes_length() {
602+
assert_eq!(VerifyData::SIZE, 2);
603+
assert!(VerifyData::from_bytes(&[0x01]).is_none());
604+
let long = [0x01, 0x02, 0xEE, 0xEE, 0xEE];
605+
assert_eq!(VerifyData::from_bytes(&long).unwrap().crc, 0x0201);
606+
}
607+
451608
#[test]
452609
fn large_payload_round_trip() {
453610
let data = [0x3C; 240];

0 commit comments

Comments
 (0)