Skip to main content

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}