use std::time::Instant;
use bytes::{Bytes, BytesMut};
pub const MAX_UPLOAD_SESSION_BYTES: u64 = 4 * 1024 * 1024 * 1024;
#[derive(Debug, Clone)]
pub struct UploadState {
pub name: String,
pub uuid: String,
pub buffer: BytesMut,
pub last_activity: Instant,
}
impl UploadState {
#[must_use]
pub fn new(name: impl Into<String>, uuid: impl Into<String>) -> Self {
Self {
name: name.into(),
uuid: uuid.into(),
buffer: BytesMut::new(),
last_activity: Instant::now(),
}
}
#[must_use]
pub fn offset(&self) -> u64 {
self.buffer.len() as u64
}
pub fn append(&mut self, chunk: &Bytes) -> u64 {
self.buffer.extend_from_slice(chunk);
self.last_activity = Instant::now();
self.offset()
}
#[must_use]
pub fn is_idle_for(&self, now: Instant, ttl: std::time::Duration) -> bool {
now.saturating_duration_since(self.last_activity) >= ttl
}
pub fn take_bytes(&mut self) -> Bytes {
std::mem::take(&mut self.buffer).freeze()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
pub enum ContentRangeParseError {
#[error("malformed Content-Range")]
Malformed,
#[error("reversed range (start > end)")]
Reversed,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ContentRange {
pub start: u64,
pub end: u64,
}
impl ContentRange {
pub fn parse(value: &str) -> Result<Self, ContentRangeParseError> {
let value = value.trim();
let payload = value.strip_prefix("bytes ").unwrap_or(value);
let payload = payload.split('/').next().unwrap_or(payload);
let (start, end) = payload
.split_once('-')
.ok_or(ContentRangeParseError::Malformed)?;
let start: u64 = start
.trim()
.parse()
.map_err(|_| ContentRangeParseError::Malformed)?;
let end: u64 = end
.trim()
.parse()
.map_err(|_| ContentRangeParseError::Malformed)?;
if start > end {
return Err(ContentRangeParseError::Reversed);
}
Ok(Self { start, end })
}
#[must_use]
pub const fn checked_length(self) -> Option<u64> {
match self.end.checked_sub(self.start) {
Some(span) => span.checked_add(1),
None => None,
}
}
#[must_use]
pub const fn length(self) -> u64 {
match self.checked_length() {
Some(len) => len,
None => u64::MAX,
}
}
}
#[cfg(test)]
mod tests {
use super::{ContentRange, UploadState};
use bytes::Bytes;
#[test]
fn append_updates_offset() {
let mut s = UploadState::new("lib/alpine", "abc");
assert_eq!(s.offset(), 0);
let n1 = s.append(&Bytes::from_static(b"hello"));
assert_eq!(n1, 5);
let n2 = s.append(&Bytes::from_static(b"!"));
assert_eq!(n2, 6);
}
#[test]
fn take_bytes_returns_everything_and_resets() {
let mut s = UploadState::new("lib/alpine", "abc");
s.append(&Bytes::from_static(b"hello"));
let out = s.take_bytes();
assert_eq!(&out[..], b"hello");
assert_eq!(s.offset(), 0);
}
#[test]
fn content_range_parse_bare_form() {
let r = ContentRange::parse("0-1023").expect("parse");
assert_eq!(
r,
ContentRange {
start: 0,
end: 1023
}
);
assert_eq!(r.length(), 1024);
}
#[test]
fn content_range_parse_bytes_prefix() {
let r = ContentRange::parse("bytes 100-199").expect("parse");
assert_eq!(
r,
ContentRange {
start: 100,
end: 199
}
);
}
#[test]
fn content_range_parse_with_total() {
let r = ContentRange::parse("bytes 0-9/100").expect("parse");
assert_eq!(r, ContentRange { start: 0, end: 9 });
}
#[test]
fn content_range_rejects_reversed() {
assert!(ContentRange::parse("10-5").is_err());
}
#[test]
fn content_range_rejects_garbage() {
assert!(ContentRange::parse("not-a-range").is_err());
assert!(ContentRange::parse("").is_err());
}
#[test]
fn checked_length_handles_full_u64_range_without_overflow() {
let r = ContentRange::parse(&format!("0-{}", u64::MAX)).expect("parse full range");
assert_eq!(r.checked_length(), None, "full-u64 span has no exact length");
assert_eq!(r.length(), u64::MAX, "length() saturates, never wraps to 0");
}
#[test]
fn checked_length_normal_range_is_exact() {
let r = ContentRange::parse("0-1023").expect("parse");
assert_eq!(r.checked_length(), Some(1024));
}
#[test]
fn equal_start_end_is_a_valid_single_byte_range() {
let r = ContentRange::parse("5-5").expect("equal bounds is one byte");
assert_eq!(r, ContentRange { start: 5, end: 5 });
assert_eq!(r.length(), 1, "inclusive length of N-N is 1");
assert!(
ContentRange::parse("6-5").is_err(),
"a genuinely reversed range stays rejected"
);
}
}