Skip to main content

ternaria_dev/
block.rs

1//! A block device backed by host bytes.
2//!
3//! | Offset | Register | Behaviour |
4//! |--------|----------|-----------|
5//! | 0..3 | `SECTOR` | Which sector the next load transfers. Word-sized. |
6//! | 3 | `CONTROL` | Write 1 to load the selected sector, 2 to store it. |
7//! | 4 | `STATUS` | 0 after a successful transfer, -1 if the sector is out of range. |
8//! | 5.. | window | The staging buffer, one byte per tryte (D-04). Readable and writable. |
9//!
10//! The backing bytes come from a host file or an in-memory image. A load past
11//! the end of the image zeroes the window and sets `STATUS` to -1.
12//!
13//! # Writing
14//!
15//! A store copies the window back to the selected sector. The window holds
16//! trytes and the image holds bytes, so every tryte in the window must be in
17//! 0..=255; one that is not fails the store with `NotAByte` and leaves the
18//! image untouched. The check runs over the whole window before anything is
19//! written, so a store either happens completely or not at all.
20//!
21//! A store to the sector one past the end appends, growing the image by one
22//! sector. Anything further out is an error. This is enough for a filesystem
23//! to extend a file without needing a separate resize operation, and it stops
24//! a stray sector number from inflating the image by a wild amount.
25
26use crate::{Device, SECTOR_SIZE, unmapped};
27use ternaria_arith::{Tryte, Word};
28use ternaria_mem::{
29    MemoryError,
30    bytes::{byte_to_tryte, tryte_to_byte},
31};
32
33/// Offset of the first tryte of the sector register.
34pub const SECTOR: i64 = 0;
35/// Offset of the control register.
36pub const CONTROL: i64 = 3;
37/// Control value that loads the selected sector into the window.
38pub const CONTROL_LOAD: i32 = 1;
39/// Control value that stores the window to the selected sector.
40pub const CONTROL_STORE: i32 = 2;
41/// Offset of the status register.
42pub const STATUS: i64 = 4;
43/// Offset of the first tryte of the sector window.
44pub const WINDOW: i64 = 5;
45
46/// A sector-addressed block device.
47pub struct BlockDevice {
48    image: Vec<u8>,
49    sector: i64,
50    status: i32,
51    window: Vec<Tryte>,
52}
53
54impl BlockDevice {
55    /// A device backed by the given bytes.
56    pub fn new(image: impl Into<Vec<u8>>) -> BlockDevice {
57        BlockDevice {
58            image: image.into(),
59            sector: 0,
60            status: 0,
61            window: vec![Tryte::ZERO; SECTOR_SIZE as usize],
62        }
63    }
64
65    /// A device backed by the contents of a host file.
66    pub fn from_file(path: impl AsRef<std::path::Path>) -> std::io::Result<BlockDevice> {
67        Ok(BlockDevice::new(std::fs::read(path)?))
68    }
69
70    /// How many whole or partial sectors the image contains.
71    pub fn sector_count(&self) -> i64 {
72        self.image.len().div_ceil(SECTOR_SIZE as usize) as i64
73    }
74
75    /// Copies the selected sector into the window.
76    ///
77    /// A short final sector is zero-padded. A sector past the end of the image
78    /// leaves the window zeroed and sets `STATUS` to -1.
79    fn load(&mut self) {
80        self.window.fill(Tryte::ZERO);
81        if self.sector < 0 || self.sector >= self.sector_count() {
82            self.status = -1;
83            return;
84        }
85        let start = self.sector as usize * SECTOR_SIZE as usize;
86        let end = (start + SECTOR_SIZE as usize).min(self.image.len());
87        for (i, b) in self.image[start..end].iter().enumerate() {
88            self.window[i] = byte_to_tryte(*b);
89        }
90        self.status = 0;
91    }
92
93    /// Copies the window back to the selected sector.
94    ///
95    /// Every tryte must be a byte. The check runs first, over the whole
96    /// window, so a window holding one bad value leaves the image unchanged
97    /// rather than half written.
98    fn store(&mut self) -> Result<(), MemoryError> {
99        if self.sector < 0 || self.sector > self.sector_count() {
100            self.status = -1;
101            return Ok(());
102        }
103        let mut bytes = [0u8; SECTOR_SIZE as usize];
104        for (slot, t) in bytes.iter_mut().zip(self.window.iter()) {
105            *slot = tryte_to_byte(*t).ok_or(MemoryError::NotAByte {
106                value: t.value() as i64,
107            })?;
108        }
109        let start = self.sector as usize * SECTOR_SIZE as usize;
110        let end = start + SECTOR_SIZE as usize;
111        if end > self.image.len() {
112            self.image.resize(end, 0);
113        }
114        self.image[start..end].copy_from_slice(&bytes);
115        self.status = 0;
116        Ok(())
117    }
118
119    /// The backing bytes, for a host writing an image back to a file.
120    pub fn image(&self) -> &[u8] {
121        &self.image
122    }
123
124    /// Replaces one tryte of the sector register, leaving the others alone.
125    ///
126    /// The register is word-sized, so a program may set it with one `sw` or
127    /// one tryte at a time.
128    fn set_sector_tryte(&mut self, index: usize, value: Tryte) {
129        let current = Word::from_value(self.sector);
130        let mut trytes = [Tryte::ZERO; Word::TRYTES];
131        for (i, slot) in trytes.iter_mut().enumerate() {
132            *slot = current.tryte(i);
133        }
134        trytes[index] = value;
135        self.sector = Word::from_trytes(trytes).value();
136    }
137}
138
139impl Device for BlockDevice {
140    fn name(&self) -> &'static str {
141        "block"
142    }
143
144    fn read(&mut self, offset: i64) -> Result<Tryte, MemoryError> {
145        match offset {
146            0..=2 => Ok(Word::from_value(self.sector).tryte(offset as usize)),
147            CONTROL => Ok(Tryte::ZERO),
148            STATUS => Ok(Tryte::from_value(self.status)),
149            _ if (WINDOW..WINDOW + SECTOR_SIZE).contains(&offset) => {
150                Ok(self.window[(offset - WINDOW) as usize])
151            }
152            _ => Err(unmapped(offset)),
153        }
154    }
155
156    fn write(&mut self, offset: i64, value: Tryte) -> Result<(), MemoryError> {
157        match offset {
158            0..=2 => {
159                self.set_sector_tryte(offset as usize, value);
160                Ok(())
161            }
162            CONTROL => match value.value() {
163                CONTROL_LOAD => {
164                    self.load();
165                    Ok(())
166                }
167                CONTROL_STORE => self.store(),
168                // Any other value is a no-op rather than an error, so that a
169                // zero written to clear the register does nothing.
170                _ => Ok(()),
171            },
172            STATUS => Err(MemoryError::Device {
173                addr: offset,
174                reason: "block status is read-only",
175            }),
176            _ if (WINDOW..WINDOW + SECTOR_SIZE).contains(&offset) => {
177                self.window[(offset - WINDOW) as usize] = value;
178                Ok(())
179            }
180            _ => Err(unmapped(offset)),
181        }
182    }
183}