1use std::io;
18use std::num::NonZeroU64;
19use std::ops::Range;
20
21#[derive(Debug, Clone, Copy)]
23pub struct PageLayout {
24 page_size: NonZeroU64,
25 file_size: u64,
26}
27
28impl PageLayout {
29 pub fn new(page_size: NonZeroU64, file_size: u64) -> Self {
31 Self {
32 page_size,
33 file_size,
34 }
35 }
36
37 pub fn page_ids(&self, request: Range<u64>) -> io::Result<Range<u64>> {
43 validate_range(&request)?;
44 let start = request.start.min(self.file_size);
45 let end = request.end.min(self.file_size);
46 let first = start / self.page_size.get();
47 if start == end {
48 return Ok(first..first);
49 }
50 let last = (end - 1) / self.page_size.get();
53 Ok(first..last + 1)
54 }
55
56 pub fn page_range(&self, page_id: u64) -> Option<Range<u64>> {
58 let start = page_id.checked_mul(self.page_size.get())?;
59 if start >= self.file_size {
60 return None;
61 }
62 let len = self.page_size.get().min(self.file_size - start);
63 Some(start..start + len)
64 }
65
66 pub fn request_slice(
72 &self,
73 page_id: u64,
74 request: Range<u64>,
75 ) -> io::Result<Option<Range<usize>>> {
76 validate_range(&request)?;
77 let Some(page) = self.page_range(page_id) else {
78 return Ok(None);
79 };
80 let start = request.start.max(page.start);
81 let end = request.end.min(page.end);
82 if start >= end {
83 return Ok(None);
84 }
85 Ok(Some(
86 to_usize(start - page.start)?..to_usize(end - page.start)?,
87 ))
88 }
89}
90
91fn validate_range(range: &Range<u64>) -> io::Result<()> {
92 if range.start > range.end {
93 return Err(invalid_input("Byte range start exceeds end"));
94 }
95 Ok(())
96}
97
98fn to_usize(value: u64) -> io::Result<usize> {
99 usize::try_from(value).map_err(|_| invalid_input("Byte offset exceeds addressable memory"))
100}
101
102fn invalid_input(message: &'static str) -> io::Error {
103 io::Error::new(io::ErrorKind::InvalidInput, message)
104}
105
106#[cfg(test)]
107mod tests {
108 use super::*;
109
110 fn nonzero(value: u64) -> NonZeroU64 {
111 NonZeroU64::new(value).unwrap()
112 }
113
114 #[test]
115 fn test_page_alignment_and_eof() {
116 let layout = PageLayout::new(nonzero(64), 150);
117 for (request, expected) in [
118 (0..0, 0..0),
119 (0..64, 0..1),
120 (32..96, 0..2),
121 (64..128, 1..2),
122 (128..200, 2..3),
123 (150..200, 2..2),
124 ] {
125 assert_eq!(layout.page_ids(request).unwrap(), expected);
126 }
127 assert_eq!(layout.page_range(2), Some(128..150));
128 assert_eq!(layout.page_range(3), None);
129 assert_eq!(layout.request_slice(0, 32..96).unwrap(), Some(32..64));
130 assert_eq!(layout.request_slice(1, 32..96).unwrap(), Some(0..32));
131 assert_eq!(layout.request_slice(2, 128..200).unwrap(), Some(0..22));
132 assert_eq!(layout.request_slice(1, 0..64).unwrap(), None);
133 assert_eq!(PageLayout::new(nonzero(64), 0).page_range(0), None);
134 assert!(layout.page_ids(Range { start: 2, end: 1 }).is_err());
135 assert!(layout.request_slice(0, Range { start: 2, end: 1 }).is_err());
136 }
137
138 #[test]
139 fn test_large_file_boundaries() {
140 let layout = PageLayout::new(nonzero(1), u64::MAX);
141 assert_eq!(layout.page_ids(0..u64::MAX).unwrap(), 0..u64::MAX);
142 assert_eq!(
143 layout.page_range(u64::MAX - 1),
144 Some(u64::MAX - 1..u64::MAX)
145 );
146 assert_eq!(layout.page_range(u64::MAX), None);
147 let layout = PageLayout::new(nonzero(64), u64::MAX);
148 assert_eq!(layout.page_range(u64::MAX), None);
149 let last = u64::MAX / 64;
150 assert_eq!(layout.page_range(last), Some(last * 64..u64::MAX));
151 assert_eq!(
152 layout.page_ids(u64::MAX - 1..u64::MAX).unwrap(),
153 last..last + 1
154 );
155 }
156}