use std::collections::HashMap;
use std::io::Write;
use futures::StreamExt;
use serde_json::{Value, json};
use crate::api::ApiClient;
use crate::base64::{base64url_decode, base64url_encode};
use crate::crypto::aes::{aes128_cbc_decrypt, aes128_ctr_decrypt, aes128_ecb_decrypt};
use crate::error::{MegaError, Result};
use crate::fs::node::{Node, NodeType};
#[derive(Debug, Clone)]
pub struct PublicFile {
pub name: String,
pub size: u64,
pub handle: String,
key: [u8; 32],
download_url: String,
}
impl PublicFile {
pub fn get_key(&self) -> String {
base64url_encode(&self.key)
}
pub fn get_link(&self) -> String {
format!("https://mega.nz/file/{}#{}", self.handle, self.get_key())
}
}
pub fn parse_mega_link(url: &str) -> Result<(String, String)> {
if url.contains("/file/") {
if let Some(pos) = url.find("/file/") {
let rest = &url[pos + 6..];
if let Some(hash_pos) = rest.find('#') {
let handle = rest[..hash_pos].to_string();
let key = rest[hash_pos + 1..].to_string();
return Ok((handle, key));
}
}
}
if url.contains("#!") {
if let Some(pos) = url.find("#!") {
let rest = &url[pos + 2..];
if let Some(bang_pos) = rest.find('!') {
let handle = rest[..bang_pos].to_string();
let key = rest[bang_pos + 1..].to_string();
return Ok((handle, key));
}
}
}
Err(MegaError::Custom(format!(
"Invalid MEGA link format: {}",
url
)))
}
pub async fn get_public_file_info(url: &str) -> Result<PublicFile> {
let (handle, key_b64) = parse_mega_link(url)?;
let key_bytes = base64url_decode(&key_b64)?;
if key_bytes.len() != 32 {
return Err(MegaError::Custom(format!(
"Invalid key length: expected 32, got {}",
key_bytes.len()
)));
}
let mut key = [0u8; 32];
key.copy_from_slice(&key_bytes);
let mut api = ApiClient::new();
let response = api
.request(json!({
"a": "g",
"g": 1,
"p": handle
}))
.await?;
let size = response
.get("s")
.and_then(|v| v.as_u64())
.ok_or_else(|| MegaError::Custom("Missing file size".to_string()))?;
let download_url = response
.get("g")
.and_then(|v| v.as_str())
.ok_or_else(|| MegaError::Custom("Missing download URL".to_string()))?
.to_string();
let attrs_b64 = response
.get("at")
.and_then(|v| v.as_str())
.ok_or_else(|| MegaError::Custom("Missing attributes".to_string()))?;
let name = decrypt_public_attrs(attrs_b64, &key)?;
Ok(PublicFile {
name,
size,
handle,
key,
download_url,
})
}
pub async fn download_public_file<W: Write>(url: &str, writer: &mut W) -> Result<PublicFile> {
let info = get_public_file_info(url).await?;
download_public_file_data(&info, writer).await?;
Ok(info)
}
pub async fn download_public_file_data<W: Write>(info: &PublicFile, writer: &mut W) -> Result<()> {
let mut aes_key = [0u8; 16];
let mut nonce = [0u8; 8];
for i in 0..16 {
aes_key[i] = info.key[i] ^ info.key[i + 16];
}
nonce.copy_from_slice(&info.key[16..24]);
let client = reqwest::Client::new();
let response = client
.get(&info.download_url)
.send()
.await
.map_err(MegaError::RequestError)?;
if !response.status().is_success() {
return Err(MegaError::Custom(format!(
"Download failed with status: {}",
response.status()
)));
}
let mut offset = 0u64;
let mut stream = response.bytes_stream();
while let Some(chunk_result) = stream.next().await {
let chunk = chunk_result.map_err(MegaError::RequestError)?;
let decrypted = aes128_ctr_decrypt(&chunk, &aes_key, &nonce, offset);
writer
.write_all(&decrypted)
.map_err(|e| MegaError::Custom(format!("Write error: {}", e)))?;
offset += chunk.len() as u64;
}
Ok(())
}
fn decrypt_public_attrs(attrs_b64: &str, key: &[u8; 32]) -> Result<String> {
let encrypted = base64url_decode(attrs_b64)?;
let mut aes_key = [0u8; 16];
for i in 0..16 {
aes_key[i] = key[i] ^ key[i + 16];
}
let decrypted = aes128_cbc_decrypt(&encrypted, &aes_key);
let text = String::from_utf8_lossy(&decrypted);
if !text.starts_with("MEGA") {
return Err(MegaError::Custom("Invalid decryption key".to_string()));
}
let json_str = text.trim_start_matches("MEGA").trim_end_matches('\0');
let attrs: Value = serde_json::from_str(json_str)
.map_err(|_| MegaError::Custom("Failed to parse attributes".to_string()))?;
attrs
.get("n")
.and_then(|v| v.as_str())
.map(|s| s.to_string())
.ok_or_else(|| MegaError::Custom("Missing file name".to_string()))
}
#[derive(Debug)]
pub struct PublicFolder {
pub handle: String,
nodes: Vec<Node>,
}
impl PublicFolder {
pub fn nodes(&self) -> &[Node] {
&self.nodes
}
pub fn list(&self, path: &str, recursive: bool) -> Vec<&Node> {
let normalized = normalize_folder_path(path);
let search_prefix = if normalized == "/" {
"/".to_string()
} else {
format!("{}/", normalized)
};
let mut results = Vec::new();
for node in &self.nodes {
if let Some(node_path) = &node.path {
if recursive {
if node_path.starts_with(&search_prefix) && node_path != &normalized {
results.push(node);
}
} else {
if let Some(stripped) = node_path.strip_prefix(&search_prefix) {
if !stripped.contains('/') && !stripped.is_empty() {
results.push(node);
}
}
}
}
}
results
}
pub fn stat(&self, path: &str) -> Option<&Node> {
let normalized = normalize_folder_path(path);
self.nodes
.iter()
.find(|n| n.path.as_deref() == Some(&normalized))
}
pub async fn download<W: Write>(&self, node: &Node, writer: &mut W) -> Result<()> {
if node.node_type != NodeType::File {
return Err(MegaError::Custom("Node is not a file".to_string()));
}
let mut api = ApiClient::new();
let response = api
.request(json!({
"a": "g",
"g": 1,
"n": node.handle
}))
.await?;
let url = response
.get("g")
.and_then(|v| v.as_str())
.ok_or_else(|| MegaError::Custom("Missing download URL".to_string()))?;
let k = &node.key;
if k.len() < 32 {
return Err(MegaError::Custom("Invalid node key".to_string()));
}
let mut aes_key = [0u8; 16];
let mut nonce = [0u8; 8];
for i in 0..16 {
aes_key[i] = k[i] ^ k[i + 16];
}
nonce.copy_from_slice(&k[16..24]);
let client = reqwest::Client::new();
let response = client
.get(url)
.send()
.await
.map_err(MegaError::RequestError)?;
let mut offset = 0u64;
let mut stream = response.bytes_stream();
while let Some(chunk_result) = stream.next().await {
let chunk = chunk_result.map_err(MegaError::RequestError)?;
let decrypted = aes128_ctr_decrypt(&chunk, &aes_key, &nonce, offset);
writer
.write_all(&decrypted)
.map_err(|e| MegaError::Custom(format!("Write error: {}", e)))?;
offset += chunk.len() as u64;
}
Ok(())
}
}
pub fn parse_folder_link(url: &str) -> Result<(String, String)> {
if url.contains("/folder/") {
if let Some(pos) = url.find("/folder/") {
let rest = &url[pos + 8..];
if let Some(hash_pos) = rest.find('#') {
let handle = rest[..hash_pos].to_string();
let key = rest[hash_pos + 1..].to_string();
return Ok((handle, key));
}
}
}
if url.contains("#F!") {
if let Some(pos) = url.find("#F!") {
let rest = &url[pos + 3..];
if let Some(bang_pos) = rest.find('!') {
let handle = rest[..bang_pos].to_string();
let key = rest[bang_pos + 1..].to_string();
return Ok((handle, key));
}
}
}
Err(MegaError::Custom(format!(
"Invalid folder link format: {}",
url
)))
}
pub async fn open_folder(url: &str) -> Result<PublicFolder> {
let (handle, key_b64) = parse_folder_link(url)?;
let key_bytes = base64url_decode(&key_b64)?;
if key_bytes.len() != 16 {
return Err(MegaError::Custom(format!(
"Invalid folder key length: expected 16, got {}",
key_bytes.len()
)));
}
let mut folder_key = [0u8; 16];
folder_key.copy_from_slice(&key_bytes);
let url = format!(
"https://g.api.mega.co.nz/cs?id={}&n={}",
rand::random::<u32>(),
handle
);
let body = serde_json::to_string(&vec![json!({"a": "f", "c": 1, "r": 1})])?;
let client = reqwest::Client::new();
let response = client
.post(&url)
.header("Content-Type", "application/json")
.body(body)
.send()
.await
.map_err(MegaError::RequestError)?;
let response_text = response.text().await.map_err(MegaError::RequestError)?;
let response: Value = serde_json::from_str(&response_text)?;
let response = response
.as_array()
.and_then(|arr| arr.first().cloned())
.ok_or(MegaError::InvalidResponse)?;
let nodes_array = response
.get("f")
.and_then(|v| v.as_array())
.ok_or(MegaError::InvalidResponse)?;
let mut share_keys: HashMap<String, [u8; 16]> = HashMap::new();
let mut nodes = Vec::new();
for (idx, node_json) in nodes_array.iter().enumerate() {
if idx == 0 {
if let Some(h) = node_json.get("h").and_then(|v| v.as_str()) {
share_keys.insert(h.to_string(), folder_key);
}
}
if let Some(node) = parse_public_node(node_json, &folder_key, &share_keys, idx == 0) {
nodes.push(node);
}
}
build_public_node_paths(&mut nodes);
Ok(PublicFolder { handle, nodes })
}
fn parse_public_node(
json: &Value,
folder_key: &[u8; 16],
share_keys: &HashMap<String, [u8; 16]>,
is_root: bool,
) -> Option<Node> {
let handle = json.get("h")?.as_str()?.to_string();
let parent_handle = if is_root {
None
} else {
json.get("p")
.and_then(|v| v.as_str())
.map(|s| s.to_string())
};
let node_type_int = json.get("t")?.as_i64()?;
let node_type = NodeType::from_i64(node_type_int)?;
let size = json.get("s").and_then(|v| v.as_u64()).unwrap_or(0);
let timestamp = json.get("ts").and_then(|v| v.as_i64()).unwrap_or(0);
let (node_key, name) = if is_root {
let key = folder_key.to_vec();
let attrs_b64 = json.get("a").and_then(|v| v.as_str());
let name = attrs_b64
.and_then(|a| decrypt_public_node_attrs(a, &key))
.unwrap_or_else(|| "Shared Folder".to_string());
(key, name)
} else {
let key_str = json.get("k").and_then(|v| v.as_str()).unwrap_or("");
let node_key = if key_str.is_empty() {
folder_key.to_vec()
} else {
decrypt_public_node_key(key_str, folder_key, share_keys)?
};
let attrs_b64 = json.get("a")?.as_str()?;
let name = decrypt_public_node_attrs(attrs_b64, &node_key)?;
(node_key, name)
};
Some(Node {
name,
handle,
parent_handle,
node_type,
size,
timestamp,
key: node_key,
path: None,
link: None,
})
}
fn decrypt_public_node_key(
key_str: &str,
folder_key: &[u8; 16],
share_keys: &HashMap<String, [u8; 16]>,
) -> Option<Vec<u8>> {
for part in key_str.split('/') {
if let Some((key_handle, encrypted_key)) = part.split_once(':') {
let decrypt_key = if share_keys.contains_key(key_handle) {
share_keys.get(key_handle)?
} else {
folder_key
};
if let Ok(encrypted) = base64url_decode(encrypted_key) {
return Some(aes128_ecb_decrypt(&encrypted, decrypt_key));
}
}
}
None
}
fn decrypt_public_node_attrs(attrs_b64: &str, node_key: &[u8]) -> Option<String> {
let encrypted = base64url_decode(attrs_b64).ok()?;
let aes_key: [u8; 16] = if node_key.len() >= 32 {
let mut key = [0u8; 16];
for i in 0..16 {
key[i] = node_key[i] ^ node_key[i + 16];
}
key
} else if node_key.len() >= 16 {
node_key[..16].try_into().ok()?
} else {
return None;
};
let decrypted = aes128_cbc_decrypt(&encrypted, &aes_key);
let text = String::from_utf8_lossy(&decrypted);
if !text.starts_with("MEGA") {
return None;
}
let json_str = text.trim_start_matches("MEGA").trim_end_matches('\0');
let attrs: Value = serde_json::from_str(json_str).ok()?;
attrs
.get("n")
.and_then(|v| v.as_str())
.map(|s| s.to_string())
}
fn build_public_node_paths(nodes: &mut [Node]) {
let handle_map: HashMap<&str, usize> = nodes
.iter()
.enumerate()
.map(|(i, n)| (n.handle.as_str(), i))
.collect();
let paths: Vec<String> = (0..nodes.len())
.map(|i| build_public_path(nodes, i, &handle_map, 0))
.collect();
for (i, path) in paths.into_iter().enumerate() {
nodes[i].path = Some(path);
}
}
fn build_public_path(
nodes: &[Node],
idx: usize,
handle_map: &HashMap<&str, usize>,
depth: usize,
) -> String {
if depth > 100 {
return format!("/{}", nodes[idx].name);
}
let node = &nodes[idx];
if node.parent_handle.is_none() {
return format!("/{}", node.name);
}
if let Some(parent_handle) = &node.parent_handle {
if let Some(&parent_idx) = handle_map.get(parent_handle.as_str()) {
let parent_path = build_public_path(nodes, parent_idx, handle_map, depth + 1);
return format!("{}/{}", parent_path.trim_end_matches('/'), node.name);
}
}
format!("/{}", node.name)
}
fn normalize_folder_path(path: &str) -> String {
let mut result = path.replace("//", "/");
while result.ends_with('/') && result.len() > 1 {
result.pop();
}
if !result.starts_with('/') {
result = format!("/{}", result);
}
result
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_mega_link_new_format() {
let (handle, key) = parse_mega_link("https://mega.nz/file/ABC123#keydata123").unwrap();
assert_eq!(handle, "ABC123");
assert_eq!(key, "keydata123");
}
#[test]
fn test_parse_mega_link_legacy_format() {
let (handle, key) = parse_mega_link("https://mega.nz/#!ABC123!keydata123").unwrap();
assert_eq!(handle, "ABC123");
assert_eq!(key, "keydata123");
}
#[test]
fn test_parse_mega_link_invalid() {
assert!(parse_mega_link("https://example.com/file").is_err());
}
#[test]
fn test_parse_folder_link_new_format() {
let (handle, key) =
parse_folder_link("https://mega.nz/folder/XYZ789#folderkey123").unwrap();
assert_eq!(handle, "XYZ789");
assert_eq!(key, "folderkey123");
}
#[test]
fn test_parse_folder_link_legacy_format() {
let (handle, key) = parse_folder_link("https://mega.nz/#F!XYZ789!folderkey123").unwrap();
assert_eq!(handle, "XYZ789");
assert_eq!(key, "folderkey123");
}
}