takanawa-http 0.3.1

Tokio and reqwest HTTP range downloader for Takanawa
Documentation
use takanawa_core::{Result, TakanawaError};

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ContentRange {
    pub start: u64,
    pub end: u64,
    pub total: u64,
}

pub fn parse_content_range(value: &str) -> Result<ContentRange> {
    let value = value.trim();
    let Some(rest) = value.strip_prefix("bytes ") else {
        return Err(TakanawaError::HttpProtocol(format!(
            "Content-Range does not use bytes unit: {value}"
        )));
    };
    if rest.starts_with("*/") {
        return Err(TakanawaError::HttpProtocol(format!(
            "unsatisfied Content-Range is not a byte range: {value}"
        )));
    }

    let (range, total) = rest
        .split_once('/')
        .ok_or_else(|| TakanawaError::HttpProtocol(format!("invalid Content-Range: {value}")))?;
    if total == "*" {
        return Err(TakanawaError::HttpProtocol(
            "Content-Range total length is unknown".to_owned(),
        ));
    }
    let total = total
        .parse::<u64>()
        .map_err(|err| TakanawaError::HttpProtocol(format!("invalid total length: {err}")))?;
    let (start, end) = range
        .split_once('-')
        .ok_or_else(|| TakanawaError::HttpProtocol(format!("invalid range: {value}")))?;
    let start = start
        .parse::<u64>()
        .map_err(|err| TakanawaError::HttpProtocol(format!("invalid range start: {err}")))?;
    let end = end
        .parse::<u64>()
        .map_err(|err| TakanawaError::HttpProtocol(format!("invalid range end: {err}")))?;
    if start > end || end >= total {
        return Err(TakanawaError::HttpProtocol(format!(
            "invalid Content-Range bounds: {value}"
        )));
    }

    Ok(ContentRange { start, end, total })
}

pub fn parse_unsatisfied_total(value: &str) -> Result<u64> {
    let value = value.trim();
    let Some(rest) = value.strip_prefix("bytes */") else {
        return Err(TakanawaError::HttpProtocol(format!(
            "invalid unsatisfied Content-Range: {value}"
        )));
    };
    rest.parse::<u64>()
        .map_err(|err| TakanawaError::HttpProtocol(format!("invalid unsatisfied total: {err}")))
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn parses_content_range() {
        let parsed = parse_content_range("bytes 4-7/10").unwrap();

        assert_eq!(
            parsed,
            ContentRange {
                start: 4,
                end: 7,
                total: 10,
            }
        );
    }

    #[test]
    fn rejects_unknown_total() {
        assert!(parse_content_range("bytes 0-1/*").is_err());
    }

    #[test]
    fn parses_unsatisfied_total() {
        assert_eq!(parse_unsatisfied_total("bytes */0").unwrap(), 0);
    }
}