use alloc::string::{String, ToString};
use alloc::vec::Vec;
use crate::error::{Error, Result};
use super::Pointer;
use super::json::{self, Json};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Operation {
Download,
Upload,
}
impl Operation {
fn as_str(self) -> &'static str {
match self {
Operation::Download => "download",
Operation::Upload => "upload",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Action {
pub href: String,
pub headers: Vec<(String, String)>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ObjectResult {
pub oid: String,
pub size: u64,
pub download: Option<Action>,
pub upload: Option<Action>,
pub error: Option<String>,
}
pub fn build_request(operation: Operation, pointers: &[Pointer]) -> String {
let mut out = String::new();
out.push_str("{\"operation\":");
out.push_str(&json::escape(operation.as_str()));
out.push_str(",\"transfers\":[\"basic\"],\"objects\":[");
for (i, p) in pointers.iter().enumerate() {
if i > 0 {
out.push(',');
}
out.push_str("{\"oid\":");
out.push_str(&json::escape(&p.oid));
out.push_str(",\"size\":");
out.push_str(&p.size.to_string());
out.push('}');
}
out.push_str("]}");
out
}
pub fn parse_request(body: &str) -> Result<(Operation, Vec<Pointer>)> {
let root = Json::parse(body)?;
let operation = match root.get("operation").and_then(Json::as_str) {
Some("download") => Operation::Download,
Some("upload") => Operation::Upload,
_ => {
return Err(Error::Protocol(
"lfs batch: missing/unknown operation".into(),
));
}
};
let mut pointers = Vec::new();
if let Some(objects) = root.get("objects").and_then(Json::as_array) {
for obj in objects {
let oid = obj
.get("oid")
.and_then(Json::as_str)
.ok_or_else(|| Error::Protocol("lfs batch: object missing oid".into()))?;
let size = obj.get("size").and_then(Json::as_u64).unwrap_or(0);
pointers.push(Pointer {
oid: oid.to_string(),
size,
});
}
}
Ok((operation, pointers))
}
pub fn build_response(results: &[ObjectResult]) -> String {
let mut out = String::new();
out.push_str("{\"transfer\":\"basic\",\"objects\":[");
for (i, r) in results.iter().enumerate() {
if i > 0 {
out.push(',');
}
out.push_str("{\"oid\":");
out.push_str(&json::escape(&r.oid));
out.push_str(",\"size\":");
out.push_str(&r.size.to_string());
if let Some(msg) = &r.error {
out.push_str(",\"error\":{\"code\":404,\"message\":");
out.push_str(&json::escape(msg));
out.push('}');
} else {
let mut actions = String::new();
if let Some(a) = &r.download {
actions.push_str("\"download\":");
actions.push_str(&action_json(a));
}
if let Some(a) = &r.upload {
if !actions.is_empty() {
actions.push(',');
}
actions.push_str("\"upload\":");
actions.push_str(&action_json(a));
}
if !actions.is_empty() {
out.push_str(",\"actions\":{");
out.push_str(&actions);
out.push('}');
}
}
out.push('}');
}
out.push_str("]}");
out
}
fn action_json(action: &Action) -> String {
let mut s = String::new();
s.push_str("{\"href\":");
s.push_str(&json::escape(&action.href));
if !action.headers.is_empty() {
s.push_str(",\"header\":{");
for (i, (k, v)) in action.headers.iter().enumerate() {
if i > 0 {
s.push(',');
}
s.push_str(&json::escape(k));
s.push(':');
s.push_str(&json::escape(v));
}
s.push('}');
}
s.push('}');
s
}
pub fn parse_response(body: &str) -> Result<Vec<ObjectResult>> {
let root = Json::parse(body)?;
let objects = root
.get("objects")
.and_then(Json::as_array)
.ok_or_else(|| Error::Protocol("lfs batch: no objects array".into()))?;
let mut results = Vec::with_capacity(objects.len());
for obj in objects {
let oid = obj
.get("oid")
.and_then(Json::as_str)
.ok_or_else(|| Error::Protocol("lfs batch: object missing oid".into()))?
.to_string();
let size = obj.get("size").and_then(Json::as_u64).unwrap_or(0);
let error = obj
.get("error")
.and_then(|e| e.get("message"))
.and_then(Json::as_str)
.map(String::from);
let actions = obj.get("actions");
let download = actions
.and_then(|a| a.get("download"))
.and_then(parse_action);
let upload = actions.and_then(|a| a.get("upload")).and_then(parse_action);
results.push(ObjectResult {
oid,
size,
download,
upload,
error,
});
}
Ok(results)
}
fn parse_action(action: &Json) -> Option<Action> {
let href = action.get("href").and_then(Json::as_str)?.to_string();
let mut headers = Vec::new();
if let Some(Json::Obj(pairs)) = action.get("header") {
for (k, v) in pairs {
if let Some(v) = v.as_str() {
headers.push((k.clone(), v.to_string()));
}
}
}
Some(Action { href, headers })
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn builds_download_request() {
let p = Pointer {
oid: "abc".into(),
size: 42,
};
let req = build_request(Operation::Download, &[p]);
assert!(req.contains("\"operation\":\"download\""));
assert!(req.contains("\"transfers\":[\"basic\"]"));
assert!(req.contains("\"oid\":\"abc\""));
assert!(req.contains("\"size\":42"));
}
#[test]
fn parses_download_and_error() {
let body = r#"{
"objects": [
{"oid":"a1","size":10,"actions":{"download":{"href":"https://x/a1","header":{"Authorization":"Bearer t"}}}},
{"oid":"b2","size":20,"error":{"code":404,"message":"not found"}}
]
}"#;
let results = parse_response(body).unwrap();
assert_eq!(results.len(), 2);
let a = &results[0];
assert_eq!(a.oid, "a1");
assert_eq!(a.size, 10);
let d = a.download.as_ref().unwrap();
assert_eq!(d.href, "https://x/a1");
assert_eq!(
d.headers,
alloc::vec![("Authorization".to_string(), "Bearer t".to_string())]
);
let b = &results[1];
assert_eq!(b.error.as_deref(), Some("not found"));
assert!(b.download.is_none());
}
#[test]
fn request_and_response_roundtrip() {
let ps = alloc::vec![
Pointer {
oid: "a1".into(),
size: 10
},
Pointer {
oid: "b2".into(),
size: 20
},
];
let (op, back) = parse_request(&build_request(Operation::Upload, &ps)).unwrap();
assert_eq!(op, Operation::Upload);
assert_eq!(back, ps);
let results = alloc::vec![
ObjectResult {
oid: "a1".into(),
size: 10,
download: Some(Action {
href: "https://x/a1".into(),
headers: alloc::vec![("Authorization".into(), "Bearer t".into())],
}),
upload: None,
error: None,
},
ObjectResult {
oid: "b2".into(),
size: 20,
download: None,
upload: None,
error: Some("nope".into()),
},
];
let parsed = parse_response(&build_response(&results)).unwrap();
assert_eq!(parsed, results);
}
}