ferro_oci_server/
upload.rs1use bytes::{Bytes, BytesMut};
21
22#[derive(Debug, Clone)]
28pub struct UploadState {
29 pub name: String,
31 pub uuid: String,
33 pub buffer: BytesMut,
35}
36
37impl UploadState {
38 #[must_use]
40 pub fn new(name: impl Into<String>, uuid: impl Into<String>) -> Self {
41 Self {
42 name: name.into(),
43 uuid: uuid.into(),
44 buffer: BytesMut::new(),
45 }
46 }
47
48 #[must_use]
50 pub fn offset(&self) -> u64 {
51 self.buffer.len() as u64
52 }
53
54 pub fn append(&mut self, chunk: &Bytes) -> u64 {
56 self.buffer.extend_from_slice(chunk);
57 self.offset()
58 }
59
60 pub fn take_bytes(&mut self) -> Bytes {
62 std::mem::take(&mut self.buffer).freeze()
63 }
64}
65
66#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
68pub enum ContentRangeParseError {
69 #[error("malformed Content-Range")]
71 Malformed,
72 #[error("reversed range (start > end)")]
74 Reversed,
75}
76
77#[derive(Debug, Clone, Copy, PartialEq, Eq)]
79pub struct ContentRange {
80 pub start: u64,
82 pub end: u64,
84}
85
86impl ContentRange {
87 pub fn parse(value: &str) -> Result<Self, ContentRangeParseError> {
96 let value = value.trim();
97 let payload = value.strip_prefix("bytes ").unwrap_or(value);
100 let payload = payload.split('/').next().unwrap_or(payload);
101 let (start, end) = payload
102 .split_once('-')
103 .ok_or(ContentRangeParseError::Malformed)?;
104 let start: u64 = start
105 .trim()
106 .parse()
107 .map_err(|_| ContentRangeParseError::Malformed)?;
108 let end: u64 = end
109 .trim()
110 .parse()
111 .map_err(|_| ContentRangeParseError::Malformed)?;
112 if start > end {
113 return Err(ContentRangeParseError::Reversed);
114 }
115 Ok(Self { start, end })
116 }
117
118 #[must_use]
120 pub const fn length(self) -> u64 {
121 self.end - self.start + 1
122 }
123}
124
125#[cfg(test)]
126mod tests {
127 use super::{ContentRange, UploadState};
128 use bytes::Bytes;
129
130 #[test]
131 fn append_updates_offset() {
132 let mut s = UploadState::new("lib/alpine", "abc");
133 assert_eq!(s.offset(), 0);
134 let n1 = s.append(&Bytes::from_static(b"hello"));
135 assert_eq!(n1, 5);
136 let n2 = s.append(&Bytes::from_static(b"!"));
137 assert_eq!(n2, 6);
138 }
139
140 #[test]
141 fn take_bytes_returns_everything_and_resets() {
142 let mut s = UploadState::new("lib/alpine", "abc");
143 s.append(&Bytes::from_static(b"hello"));
144 let out = s.take_bytes();
145 assert_eq!(&out[..], b"hello");
146 assert_eq!(s.offset(), 0);
147 }
148
149 #[test]
150 fn content_range_parse_bare_form() {
151 let r = ContentRange::parse("0-1023").expect("parse");
152 assert_eq!(
153 r,
154 ContentRange {
155 start: 0,
156 end: 1023
157 }
158 );
159 assert_eq!(r.length(), 1024);
160 }
161
162 #[test]
163 fn content_range_parse_bytes_prefix() {
164 let r = ContentRange::parse("bytes 100-199").expect("parse");
165 assert_eq!(
166 r,
167 ContentRange {
168 start: 100,
169 end: 199
170 }
171 );
172 }
173
174 #[test]
175 fn content_range_parse_with_total() {
176 let r = ContentRange::parse("bytes 0-9/100").expect("parse");
177 assert_eq!(r, ContentRange { start: 0, end: 9 });
178 }
179
180 #[test]
181 fn content_range_rejects_reversed() {
182 assert!(ContentRange::parse("10-5").is_err());
183 }
184
185 #[test]
186 fn content_range_rejects_garbage() {
187 assert!(ContentRange::parse("not-a-range").is_err());
188 assert!(ContentRange::parse("").is_err());
189 }
190}