Skip to main content

hxy_core/
geometry.rs

1use std::fmt;
2use std::num::NonZeroU16;
3use std::ops::Range;
4
5use serde::Deserialize;
6use serde::Serialize;
7
8use crate::error::Error;
9
10/// Absolute byte offset within a [`HexSource`](crate::HexSource).
11#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
12#[repr(transparent)]
13pub struct ByteOffset(pub u64);
14
15impl ByteOffset {
16    pub const ZERO: Self = Self(0);
17
18    pub fn new(offset: u64) -> Self {
19        Self(offset)
20    }
21
22    pub fn get(self) -> u64 {
23        self.0
24    }
25
26    pub fn checked_add_len(self, len: ByteLen) -> Option<Self> {
27        self.0.checked_add(len.0).map(Self)
28    }
29
30    pub fn saturating_add_len(self, len: ByteLen) -> Self {
31        Self(self.0.saturating_add(len.0))
32    }
33}
34
35impl fmt::Display for ByteOffset {
36    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
37        write!(f, "0x{:X}", self.0)
38    }
39}
40
41impl From<u64> for ByteOffset {
42    fn from(value: u64) -> Self {
43        Self(value)
44    }
45}
46
47/// Length of a byte range (distinct from [`ByteOffset`] so they can't be
48/// confused at call sites).
49#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
50#[repr(transparent)]
51pub struct ByteLen(pub u64);
52
53impl ByteLen {
54    pub const ZERO: Self = Self(0);
55
56    pub fn new(len: u64) -> Self {
57        Self(len)
58    }
59
60    pub fn get(self) -> u64 {
61        self.0
62    }
63
64    pub fn is_zero(self) -> bool {
65        self.0 == 0
66    }
67}
68
69impl fmt::Display for ByteLen {
70    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
71        write!(f, "{} bytes", self.0)
72    }
73}
74
75impl From<u64> for ByteLen {
76    fn from(value: u64) -> Self {
77        Self(value)
78    }
79}
80
81/// Half-open byte range `[start, end)`.
82///
83/// Validated on construction: `start <= end` is an invariant.
84#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
85pub struct ByteRange {
86    start: ByteOffset,
87    end: ByteOffset,
88}
89
90impl ByteRange {
91    pub fn new(start: ByteOffset, end: ByteOffset) -> Result<Self, Error> {
92        if start > end {
93            return Err(Error::InvalidRange { start, end });
94        }
95        Ok(Self { start, end })
96    }
97
98    pub fn from_offset_and_len(start: ByteOffset, len: ByteLen) -> Result<Self, Error> {
99        let end = start.checked_add_len(len).ok_or(Error::InvalidRange { start, end: ByteOffset(u64::MAX) })?;
100        Ok(Self { start, end })
101    }
102
103    pub fn start(self) -> ByteOffset {
104        self.start
105    }
106
107    pub fn end(self) -> ByteOffset {
108        self.end
109    }
110
111    pub fn len(self) -> ByteLen {
112        ByteLen(self.end.0 - self.start.0)
113    }
114
115    pub fn is_empty(self) -> bool {
116        self.start == self.end
117    }
118
119    pub fn contains(self, offset: ByteOffset) -> bool {
120        self.start <= offset && offset < self.end
121    }
122
123    pub fn as_u64_range(self) -> Range<u64> {
124        self.start.0..self.end.0
125    }
126}
127
128impl fmt::Display for ByteRange {
129    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
130        write!(f, "{}..{}", self.start, self.end)
131    }
132}
133
134/// Number of hex columns rendered per row. Non-zero.
135///
136/// hxy defaults to `16` columns. Exposed as a newtype so callers can't
137/// accidentally pass a row-index where a column-count is expected.
138#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
139#[repr(transparent)]
140pub struct ColumnCount(NonZeroU16);
141
142impl ColumnCount {
143    pub const DEFAULT: Self = match NonZeroU16::new(16) {
144        Some(n) => Self(n),
145        None => unreachable!(),
146    };
147
148    pub fn new(cols: u16) -> Result<Self, Error> {
149        NonZeroU16::new(cols).map(Self).ok_or(Error::ZeroColumns)
150    }
151
152    pub fn get(self) -> u16 {
153        self.0.get()
154    }
155
156    pub fn as_u64(self) -> u64 {
157        u64::from(self.0.get())
158    }
159}
160
161impl Default for ColumnCount {
162    fn default() -> Self {
163        Self::DEFAULT
164    }
165}
166
167/// Row index in a hex view. Row `n` starts at byte offset `n * columns`.
168#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
169#[repr(transparent)]
170pub struct RowIndex(pub u64);
171
172impl RowIndex {
173    pub fn new(row: u64) -> Self {
174        Self(row)
175    }
176
177    pub fn get(self) -> u64 {
178        self.0
179    }
180
181    /// Byte offset of the first cell in this row, given the column count.
182    pub fn start_offset(self, columns: ColumnCount) -> ByteOffset {
183        ByteOffset(self.0.saturating_mul(columns.as_u64()))
184    }
185}
186
187impl fmt::Display for RowIndex {
188    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
189        write!(f, "{}", self.0)
190    }
191}
192
193#[cfg(test)]
194mod tests {
195    use super::*;
196
197    #[test]
198    fn range_invariant() {
199        assert!(ByteRange::new(ByteOffset(5), ByteOffset(3)).is_err());
200        let r = ByteRange::new(ByteOffset(0), ByteOffset(10)).unwrap();
201        assert_eq!(r.len().get(), 10);
202        assert!(!r.is_empty());
203        assert!(r.contains(ByteOffset(0)));
204        assert!(r.contains(ByteOffset(9)));
205        assert!(!r.contains(ByteOffset(10)));
206    }
207
208    #[test]
209    fn row_start_offset_with_default_columns() {
210        let row = RowIndex::new(3);
211        assert_eq!(row.start_offset(ColumnCount::DEFAULT), ByteOffset(48));
212    }
213
214    #[test]
215    fn column_count_rejects_zero() {
216        assert!(ColumnCount::new(0).is_err());
217        assert_eq!(ColumnCount::new(32).unwrap().get(), 32);
218    }
219}