ternaria_mem/bytes.rs
1//! The bit/trit boundary (D-04).
2//!
3//! One byte maps to one tryte holding its unsigned value, 0 to 255.
4//!
5//! # What this is not
6//!
7//! This is the octet boundary, not the arithmetic path. It applies where data
8//! crosses between the machine and a byte-oriented host: a file read through
9//! the block device, a character written to the console, a network frame.
10//!
11//! Native programs never call these functions. They compute in trytes, words
12//! and double words, whose ranges are set by D-02 and have nothing to do with
13//! 255:
14//!
15//! ```text
16//! tryte 9 trits +/- 9,841
17//! word 27 trits +/- 3,812,798,742,493
18//! double word 54 trits +/- 29,074,868,501,520,029,845,195,084
19//! ```
20//!
21//! # Direction matters
22//!
23//! Ingress is total. Every byte value has a tryte, so [`byte_to_tryte`] cannot
24//! fail and no information is lost.
25//!
26//! Egress is partial. [`tryte_to_byte`] fails outside 0..=255 because there is
27//! no octet to produce. This is serialisation refusing to truncate, not a
28//! narrowing of the machine's arithmetic. A binary host has the same boundary
29//! in the same place: `putchar` takes an `int` and a caller that passes 300
30//! has to decide what it meant, and encoding a large value as several bytes is
31//! the answer in both systems.
32//!
33//! # Binary program types
34//!
35//! Running a program written for a binary machine is a separate question,
36//! answered by D-16 rather than here. In outline, `i32` and `u32` fit in one
37//! word and `i64` and `u64` fit in one double word, so the values are carried
38//! exactly. What costs anything is wraparound: reducing modulo 2^64 is not
39//! natural in base 3, so a translated program pays only where it relies on
40//! unsigned overflow.
41
42use crate::MemoryError;
43use ternaria_arith::Tryte;
44
45/// Largest tryte value with a byte representation.
46pub const MAX_BYTE_VALUE: i32 = 255;
47
48/// Converts a byte to the tryte holding its unsigned value.
49#[inline]
50pub const fn byte_to_tryte(b: u8) -> Tryte {
51 Tryte::from_value(b as i32)
52}
53
54/// Converts a tryte back to a byte.
55///
56/// `None` if the value is outside 0..=255, which has no byte representation.
57/// Callers writing to a byte-oriented device must treat this as an error rather
58/// than truncating.
59#[inline]
60pub const fn tryte_to_byte(t: Tryte) -> Option<u8> {
61 let v = t.value();
62 if v >= 0 && v <= MAX_BYTE_VALUE {
63 Some(v as u8)
64 } else {
65 None
66 }
67}
68
69/// Converts a slice of bytes to trytes.
70pub fn bytes_to_trytes(bytes: &[u8]) -> Vec<Tryte> {
71 bytes.iter().copied().map(byte_to_tryte).collect()
72}
73
74/// Converts trytes back to bytes, failing on the first value out of range.
75pub fn trytes_to_bytes(trytes: &[Tryte]) -> Result<Vec<u8>, MemoryError> {
76 trytes
77 .iter()
78 .map(|t| {
79 tryte_to_byte(*t).ok_or(MemoryError::NotAByte {
80 value: t.value() as i64,
81 })
82 })
83 .collect()
84}
85
86#[cfg(test)]
87mod tests {
88 use super::*;
89
90 #[test]
91 fn every_byte_round_trips() {
92 for b in 0u8..=255 {
93 let t = byte_to_tryte(b);
94 assert_eq!(t.value(), b as i32);
95 assert_eq!(tryte_to_byte(t), Some(b));
96 }
97 }
98
99 #[test]
100 fn values_outside_the_byte_range_have_no_byte() {
101 for v in [-1i32, 256, 1000, Tryte::MAX.value(), Tryte::MIN.value()] {
102 assert_eq!(tryte_to_byte(Tryte::from_value(v)), None, "value {v}");
103 }
104 }
105
106 #[test]
107 fn offsets_are_preserved() {
108 // File offset k lands at tryte offset k. This is the property the
109 // denser packing would have lost.
110 let data: Vec<u8> = (0..=255).collect();
111 let trytes = bytes_to_trytes(&data);
112 for (k, b) in data.iter().enumerate() {
113 assert_eq!(trytes[k].value(), *b as i32);
114 }
115 assert_eq!(trytes_to_bytes(&trytes).unwrap(), data);
116 }
117
118 #[test]
119 fn out_of_range_reports_the_offending_value() {
120 let trytes = [byte_to_tryte(1), Tryte::from_value(300)];
121 assert_eq!(
122 trytes_to_bytes(&trytes),
123 Err(MemoryError::NotAByte { value: 300 })
124 );
125 }
126}