use std::collections::HashMap;
use ant_core::data::XorName;
use bytes::Bytes;
use hex::FromHex;
use log::{debug, error};
use serde::{Deserialize, Serialize};
use utoipa::ToSchema;
use crate::model::path_detail::{PathDetail, PathDetailType};
#[derive(Debug, Serialize, Deserialize, Clone, Copy, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum ArchiveType {
Public,
Tarchive,
}
#[derive(Clone,Serialize,Deserialize)]
pub struct Archive {
data_address_offsets_map: HashMap<String, DataAddressOffset>,
data_address_offsets_vec: Vec<DataAddressOffset>,
pub archive_type: ArchiveType,
}
#[derive(Clone,Debug,Serialize,Deserialize)]
pub struct DataAddressOffset {
pub data_address: XorName,
pub path: String,
pub offset: u64,
pub size: u64,
pub modified: u64,
pub signature: Option<String>,
}
impl Archive {
pub fn new(data_address_offsets_map: HashMap<String, DataAddressOffset>, data_address_offsets_vec: Vec<DataAddressOffset>, archive_type: ArchiveType) -> Self {
Archive { data_address_offsets_map, data_address_offsets_vec, archive_type }
}
pub fn build_from_tar(tar_data_addr: &XorName, data: Bytes) -> Self {
let mut data_address_offsets_map = HashMap::new();
let mut data_address_offsets_vec = Vec::new();
match String::from_utf8(data.to_vec()) {
Ok(tar_index) => {
let mut entry_counter = 1;
for entry in tar_index.split('\n') {
if entry.is_empty() {
continue;
}
let entry_str = entry.to_string();
let parts = entry_str.split(' ').collect::<Vec<&str>>();
debug!("parts: [{:?}]", parts);
if parts.len() < 3 {
continue;
}
let path_string = Self::sanitise_path(parts.get(0).expect("path missing from tar"));
let offset = parts.get(1).expect("offset missing from tar").parse::<u64>().unwrap_or_else(|_| 0);
let size = parts.get(2).expect("size missing from tar").parse::<u64>().unwrap_or_else(|_| 0);
let xorname_hex = parts.get(3);
let signature = parts.get(4);
let data_address = if let Some(hex) = xorname_hex {
XorName::from_hex(hex).unwrap_or(*tar_data_addr)
} else {
*tar_data_addr
};
let data_address_offset = DataAddressOffset {
data_address,
path: path_string.clone(),
offset,
size,
modified: entry_counter, signature: signature.map(|s| s.to_string()),
};
debug!("insert into archive: path_string [{}], data address offset: [{:?}]", path_string, data_address_offset);
data_address_offsets_map.insert(
path_string.clone(),
data_address_offset.clone()
);
data_address_offsets_vec.push(data_address_offset);
entry_counter += 1;
}
},
Err(err) => {
error!("Failed to parse public data for tar index [{}]", err);
}
}
debug!("data_address_offsets size [{}]", data_address_offsets_map.len());
Archive::new(data_address_offsets_map, data_address_offsets_vec, ArchiveType::Tarchive)
}
pub fn sanitise_path(path: &str) -> String {
path.replace("\\", "/")
.trim_start_matches("./")
.trim_start_matches("/")
.to_string()
}
pub fn find_file(&self, search_key: &String) -> Option<&DataAddressOffset> {
let search_key = Archive::sanitise_path(&search_key);
self.data_address_offsets_map.get(&search_key)
}
pub fn list_dir(&self, search_key: String) -> Vec<PathDetail> {
debug!("list_dir - search_key: {}", &search_key);
let search_key = Archive::sanitise_path(&search_key);
let search_key_sanitised = if search_key.len() > 0 && search_key[search_key.len()-1..].to_string() != "/" {
&format!("{}/", &search_key)
} else {
&search_key
};
let search_key_parts = search_key_sanitised.split("/").collect::<Vec<&str>>();
debug!("list_dir - search_key_sanitised: {}", &search_key_sanitised);
let mut vec = vec![];
let mut map = HashMap::new();
for data_address_offset in &self.data_address_offsets_vec {
let path_parts = &data_address_offset.path.split("/").collect::<Vec<&str>>();
debug!("search_key_parts.len(): {}, path_parts.len(): {}", search_key_parts.len(), path_parts.len());
let mut i = 0;
while i < search_key_parts.len() {
if i > path_parts.len() - 1 {
break;
}
debug!("search_key_parts[i]: {}, path_parts[i]: {}, path: {}", search_key_parts[i], path_parts[i], data_address_offset.path);
if search_key_parts[i] != "" && search_key_parts[i] != path_parts[i] {
break;
}
if i == path_parts.len() - 1 && data_address_offset.size != 0 {
debug!("adding file: {}", path_parts[i]);
let path_detail = PathDetail {
path: data_address_offset.path.clone(),
display: path_parts[i].to_string(),
modified: data_address_offset.modified,
size: data_address_offset.size,
path_type: PathDetailType::FILE,
};
vec.push(path_detail);
} else if i == search_key_parts.len() - 1 && !map.contains_key(&path_parts[i].to_string()) {
let dir_display = format!("{}/", path_parts[i]);
let mut dir_path = path_parts[..=i].join("/");
if !dir_path.ends_with('/') {
dir_path.push('/');
}
debug!("adding dir: {}", path_parts[i]);
let path_detail = PathDetail {
path: dir_path,
display: dir_display,
modified: data_address_offset.modified,
size: 0,
path_type: PathDetailType::DIRECTORY,
};
vec.push(path_detail.clone());
map.insert(path_parts[i].to_string(), path_detail);
} else if search_key_parts.len() > 1 && !map.contains_key(&"../".to_string()) {
let dir = "../".to_string();
let path_detail = PathDetail {
path: dir.clone(),
display: dir.clone(),
modified: data_address_offset.modified,
size: 0,
path_type: PathDetailType::DIRECTORY,
};
vec.push(path_detail.clone());
map.insert("../".to_string(), path_detail);
}
i += 1;
}
}
vec
}
pub fn map(&self) -> &HashMap<String, DataAddressOffset> {
&self.data_address_offsets_map
}
pub fn vec(&self) -> &Vec<DataAddressOffset> {
&self.data_address_offsets_vec
}
}
#[cfg(test)]
mod tests {
use super::*;
fn create_test_data_address() -> XorName {
XorName::default()
}
#[test]
fn test_sanitise_path() {
assert_eq!(Archive::sanitise_path("folder\\file.txt"), "folder/file.txt");
assert_eq!(Archive::sanitise_path("./file.txt"), "file.txt");
assert_eq!(Archive::sanitise_path("/file.txt"), "file.txt");
assert_eq!(Archive::sanitise_path("file.txt"), "file.txt");
}
#[test]
fn test_build_from_tar() {
let tar_content = "file1.txt 100 50\nfolder/file2.txt 200 60\n";
let data = Bytes::from(tar_content);
let addr = create_test_data_address();
let archive = Archive::build_from_tar(&addr, data);
assert_eq!(archive.map().len(), 2);
assert!(matches!(archive.archive_type, ArchiveType::Tarchive));
assert!(archive.find_file(&"file1.txt".to_string()).is_some());
assert!(archive.find_file(&"folder/file2.txt".to_string()).is_some());
let file1 = archive.find_file(&"file1.txt".to_string()).unwrap();
assert_eq!(file1.offset, 100);
assert_eq!(file1.size, 50);
}
#[test]
fn test_find_file() {
let tar_content = "file1.txt 100 50\n";
let data = Bytes::from(tar_content);
let addr = create_test_data_address();
let archive = Archive::build_from_tar(&addr, data);
assert!(archive.find_file(&"file1.txt".to_string()).is_some());
assert!(archive.find_file(&"nonexistent.txt".to_string()).is_none());
}
#[test]
fn test_list_dir_root() {
let tar_content = "file1.txt 100 50\nfolder/file2.txt 200 60\n";
let data = Bytes::from(tar_content);
let addr = create_test_data_address();
let archive = Archive::build_from_tar(&addr, data);
let list = archive.list_dir("".to_string());
assert_eq!(list.len(), 2);
let has_file1 = list.iter().any(|p| p.path == "file1.txt" && p.display == "file1.txt" && p.path_type == PathDetailType::FILE);
let has_folder = list.iter().any(|p| p.path == "folder/" && p.display == "folder/" && p.path_type == PathDetailType::DIRECTORY);
assert!(has_file1);
assert!(has_folder);
}
#[test]
fn test_list_dir_sub() {
let tar_content = "folder/file2.txt 200 60\nfolder/sub/file3.txt 300 70\n";
let data = Bytes::from(tar_content);
let addr = create_test_data_address();
let archive = Archive::build_from_tar(&addr, data);
let list = archive.list_dir("folder".to_string());
assert_eq!(list.len(), 3);
let has_file2 = list.iter().any(|p| p.path == "folder/file2.txt" && p.display == "file2.txt");
let has_sub = list.iter().any(|p| p.path == "folder/sub/" && p.display == "sub/");
let has_parent = list.iter().any(|p| p.path == "../");
assert!(has_file2, "file2.txt missing or incorrect: {:?}", list);
assert!(has_sub, "sub/ missing or incorrect: {:?}", list);
assert!(has_parent, "../ missing or incorrect: {:?}", list);
}
#[test]
fn test_list_dir_leading_slash() {
let tar_content = "folder/file2.txt 200 60\n";
let data = Bytes::from(tar_content);
let addr = create_test_data_address();
let archive = Archive::build_from_tar(&addr, data);
let list1 = archive.list_dir("folder".to_string());
let list2 = archive.list_dir("/folder".to_string());
assert_eq!(list1.len(), list2.len());
assert_eq!(list1[0].path, list2[0].path);
}
}