use std::collections::HashMap;
use std::io::{self, Write};
use std::path::{Path, PathBuf};
use std::time::{Duration, SystemTime};
use super::{Manifest, Version, VersionReq};
#[derive(Debug, Clone)]
pub struct RegistryConfig {
pub url: String,
pub token: Option<String>,
pub cache_dir: PathBuf,
pub timeout: Duration,
pub max_concurrent: usize,
}
impl Default for RegistryConfig {
fn default() -> Self {
Self {
url: "https://registry.quantalang.org".to_string(),
token: None,
cache_dir: dirs_cache().join("quanta").join("registry"),
timeout: Duration::from_secs(30),
max_concurrent: 4,
}
}
}
fn dirs_cache() -> PathBuf {
#[cfg(target_os = "windows")]
{
std::env::var("LOCALAPPDATA")
.map(PathBuf::from)
.unwrap_or_else(|_| PathBuf::from("C:\\Users\\Default\\AppData\\Local"))
}
#[cfg(not(target_os = "windows"))]
{
std::env::var("XDG_CACHE_HOME")
.map(PathBuf::from)
.unwrap_or_else(|_| {
std::env::var("HOME")
.map(|h| PathBuf::from(h).join(".cache"))
.unwrap_or_else(|_| PathBuf::from("/tmp"))
})
}
}
#[derive(Debug)]
pub enum RegistryError {
Network(String),
NotFound(String),
VersionNotFound(String, String),
AuthRequired,
AuthFailed,
RateLimited(Duration),
InvalidResponse(String),
CacheError(String),
Io(io::Error),
ChecksumMismatch { expected: String, actual: String },
Yanked(String, Version),
}
impl std::fmt::Display for RegistryError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Network(msg) => write!(f, "network error: {}", msg),
Self::NotFound(name) => write!(f, "package '{}' not found", name),
Self::VersionNotFound(name, ver) => {
write!(f, "version {} of package '{}' not found", ver, name)
}
Self::AuthRequired => write!(f, "authentication required"),
Self::AuthFailed => write!(f, "authentication failed"),
Self::RateLimited(dur) => write!(f, "rate limited, retry after {:?}", dur),
Self::InvalidResponse(msg) => write!(f, "invalid response: {}", msg),
Self::CacheError(msg) => write!(f, "cache error: {}", msg),
Self::Io(e) => write!(f, "IO error: {}", e),
Self::ChecksumMismatch { expected, actual } => {
write!(
f,
"checksum mismatch: expected {}, got {}",
expected, actual
)
}
Self::Yanked(name, ver) => write!(f, "package {}@{} has been yanked", name, ver),
}
}
}
impl std::error::Error for RegistryError {}
impl From<io::Error> for RegistryError {
fn from(e: io::Error) -> Self {
Self::Io(e)
}
}
#[derive(Debug, Clone)]
pub struct PackageMetadata {
pub name: String,
pub description: Option<String>,
pub repository: Option<String>,
pub documentation: Option<String>,
pub homepage: Option<String>,
pub keywords: Vec<String>,
pub categories: Vec<String>,
pub license: Option<String>,
pub versions: Vec<VersionInfo>,
pub downloads: u64,
pub created_at: Option<SystemTime>,
pub updated_at: Option<SystemTime>,
}
#[derive(Debug, Clone)]
pub struct VersionInfo {
pub version: Version,
pub dependencies: HashMap<String, VersionReq>,
pub dev_dependencies: HashMap<String, VersionReq>,
pub features: HashMap<String, Vec<String>>,
pub checksum: String,
pub size: u64,
pub yanked: bool,
pub published_at: Option<SystemTime>,
pub quanta_version: Option<VersionReq>,
}
#[derive(Debug)]
pub struct DownloadedPackage {
pub name: String,
pub version: Version,
pub path: PathBuf,
pub manifest: Manifest,
}
pub struct Registry {
config: RegistryConfig,
cache: PackageCache,
}
impl Registry {
pub fn new(config: RegistryConfig) -> Self {
let cache = PackageCache::new(config.cache_dir.clone());
Self { config, cache }
}
pub fn default_registry() -> Self {
Self::new(RegistryConfig::default())
}
pub fn search(&self, query: &str, limit: usize) -> Result<Vec<PackageMetadata>, RegistryError> {
let url = format!(
"{}/api/v1/search?q={}&limit={}",
self.config.url,
urlencoding::encode(query),
limit
);
let response = self.http_get(&url)?;
self.parse_search_response(&response)
}
pub fn get_package(&self, name: &str) -> Result<PackageMetadata, RegistryError> {
if let Some(meta) = self.cache.get_metadata(name) {
return Ok(meta);
}
let url = format!("{}/api/v1/packages/{}", self.config.url, name);
let response = self.http_get(&url)?;
let meta = self.parse_package_response(&response)?;
self.cache.store_metadata(name, &meta)?;
Ok(meta)
}
pub fn get_version(&self, name: &str, version: &Version) -> Result<VersionInfo, RegistryError> {
let meta = self.get_package(name)?;
meta.versions
.into_iter()
.find(|v| &v.version == version)
.ok_or_else(|| RegistryError::VersionNotFound(name.to_string(), version.to_string()))
}
pub fn find_version(&self, name: &str, req: &VersionReq) -> Result<VersionInfo, RegistryError> {
let meta = self.get_package(name)?;
let mut matching: Vec<_> = meta
.versions
.into_iter()
.filter(|v| !v.yanked && req.matches(&v.version))
.collect();
matching.sort_by(|a, b| b.version.cmp(&a.version));
matching
.into_iter()
.next()
.ok_or_else(|| RegistryError::VersionNotFound(name.to_string(), req.to_string()))
}
pub fn download(
&self,
name: &str,
version: &Version,
) -> Result<DownloadedPackage, RegistryError> {
if let Some(path) = self.cache.get_package(name, version) {
let manifest = self.load_manifest(&path)?;
return Ok(DownloadedPackage {
name: name.to_string(),
version: version.clone(),
path,
manifest,
});
}
let info = self.get_version(name, version)?;
if info.yanked {
return Err(RegistryError::Yanked(name.to_string(), version.clone()));
}
let url = format!(
"{}/api/v1/packages/{}/{}/download",
self.config.url, name, version
);
let data = self.http_get_binary(&url)?;
let checksum = sha256_hex(&data);
if checksum != info.checksum {
return Err(RegistryError::ChecksumMismatch {
expected: info.checksum,
actual: checksum,
});
}
let path = self.cache.store_package(name, version, &data)?;
let manifest = self.load_manifest(&path)?;
Ok(DownloadedPackage {
name: name.to_string(),
version: version.clone(),
path,
manifest,
})
}
pub fn publish(&self, tarball: &[u8], manifest: &Manifest) -> Result<(), RegistryError> {
let token = self
.config
.token
.as_ref()
.ok_or(RegistryError::AuthRequired)?;
let url = format!("{}/api/v1/packages/new", self.config.url);
let boundary = "----QuantaPublishBoundary";
let mut body = Vec::new();
write!(body, "--{}\r\n", boundary)?;
write!(
body,
"Content-Disposition: form-data; name=\"manifest\"\r\n"
)?;
write!(body, "Content-Type: application/toml\r\n\r\n")?;
body.extend_from_slice(manifest.to_toml().as_bytes());
write!(body, "\r\n")?;
write!(body, "--{}\r\n", boundary)?;
write!(
body,
"Content-Disposition: form-data; name=\"tarball\"; filename=\"package.tar.gz\"\r\n"
)?;
write!(body, "Content-Type: application/gzip\r\n\r\n")?;
body.extend_from_slice(tarball);
write!(body, "\r\n--{}--\r\n", boundary)?;
let _response = self.http_post(&url, &body, token, boundary)?;
Ok(())
}
pub fn yank(&self, name: &str, version: &Version) -> Result<(), RegistryError> {
let token = self
.config
.token
.as_ref()
.ok_or(RegistryError::AuthRequired)?;
let url = format!(
"{}/api/v1/packages/{}/{}/yank",
self.config.url, name, version
);
self.http_delete(&url, token)?;
self.cache.invalidate(name);
Ok(())
}
pub fn unyank(&self, name: &str, version: &Version) -> Result<(), RegistryError> {
let token = self
.config
.token
.as_ref()
.ok_or(RegistryError::AuthRequired)?;
let url = format!(
"{}/api/v1/packages/{}/{}/unyank",
self.config.url, name, version
);
self.http_put(&url, &[], token)?;
self.cache.invalidate(name);
Ok(())
}
pub fn get_owners(&self, name: &str) -> Result<Vec<Owner>, RegistryError> {
let url = format!("{}/api/v1/packages/{}/owners", self.config.url, name);
let response = self.http_get(&url)?;
self.parse_owners_response(&response)
}
pub fn add_owner(&self, name: &str, user: &str) -> Result<(), RegistryError> {
let token = self
.config
.token
.as_ref()
.ok_or(RegistryError::AuthRequired)?;
let url = format!("{}/api/v1/packages/{}/owners", self.config.url, name);
let body = format!(r#"{{"login":"{}"}}"#, user);
self.http_put(&url, body.as_bytes(), token)?;
Ok(())
}
pub fn remove_owner(&self, name: &str, user: &str) -> Result<(), RegistryError> {
let token = self
.config
.token
.as_ref()
.ok_or(RegistryError::AuthRequired)?;
let url = format!(
"{}/api/v1/packages/{}/owners/{}",
self.config.url, name, user
);
self.http_delete(&url, token)?;
Ok(())
}
fn http_get(&self, url: &str) -> Result<String, RegistryError> {
let _ = url;
let _ = self.config.timeout;
Err(RegistryError::Network(
"HTTP client not implemented".to_string(),
))
}
fn http_get_binary(&self, url: &str) -> Result<Vec<u8>, RegistryError> {
let _ = url;
Err(RegistryError::Network(
"HTTP client not implemented".to_string(),
))
}
fn http_post(
&self,
url: &str,
body: &[u8],
token: &str,
boundary: &str,
) -> Result<String, RegistryError> {
let _ = (url, body, token, boundary);
Err(RegistryError::Network(
"HTTP client not implemented".to_string(),
))
}
fn http_put(&self, url: &str, body: &[u8], token: &str) -> Result<String, RegistryError> {
let _ = (url, body, token);
Err(RegistryError::Network(
"HTTP client not implemented".to_string(),
))
}
fn http_delete(&self, url: &str, token: &str) -> Result<(), RegistryError> {
let _ = (url, token);
Err(RegistryError::Network(
"HTTP client not implemented".to_string(),
))
}
fn parse_search_response(
&self,
_response: &str,
) -> Result<Vec<PackageMetadata>, RegistryError> {
Ok(Vec::new())
}
fn parse_package_response(&self, _response: &str) -> Result<PackageMetadata, RegistryError> {
Err(RegistryError::InvalidResponse(
"JSON parsing not implemented".to_string(),
))
}
fn parse_owners_response(&self, _response: &str) -> Result<Vec<Owner>, RegistryError> {
Ok(Vec::new())
}
fn load_manifest(&self, path: &Path) -> Result<Manifest, RegistryError> {
let manifest_path = path.join("Quanta.toml");
let content = std::fs::read_to_string(&manifest_path)?;
Manifest::from_str(&content)
.map_err(|e| RegistryError::InvalidResponse(format!("invalid manifest: {}", e)))
}
}
#[derive(Debug, Clone)]
pub struct Owner {
pub login: String,
pub name: Option<String>,
pub email: Option<String>,
}
pub struct PackageCache {
root: PathBuf,
}
impl PackageCache {
pub fn new(root: PathBuf) -> Self {
Self { root }
}
pub fn get_metadata(&self, name: &str) -> Option<PackageMetadata> {
let path = self.metadata_path(name);
if !path.exists() {
return None;
}
if let Ok(meta) = std::fs::metadata(&path) {
if let Ok(modified) = meta.modified() {
if let Ok(elapsed) = SystemTime::now().duration_since(modified) {
if elapsed > Duration::from_secs(3600) {
return None;
}
}
}
}
let content = std::fs::read_to_string(&path).ok()?;
self.parse_cached_metadata(&content)
}
pub fn store_metadata(&self, name: &str, _meta: &PackageMetadata) -> Result<(), RegistryError> {
let path = self.metadata_path(name);
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
let content = "{}"; std::fs::write(&path, content)?;
Ok(())
}
pub fn get_package(&self, name: &str, version: &Version) -> Option<PathBuf> {
let path = self.package_path(name, version);
if path.exists() && path.join("Quanta.toml").exists() {
Some(path)
} else {
None
}
}
pub fn store_package(
&self,
name: &str,
version: &Version,
data: &[u8],
) -> Result<PathBuf, RegistryError> {
let path = self.package_path(name, version);
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
self.extract_tarball(data, &path)?;
Ok(path)
}
pub fn invalidate(&self, name: &str) {
let path = self.metadata_path(name);
let _ = std::fs::remove_file(path);
}
pub fn clear(&self) -> Result<(), RegistryError> {
if self.root.exists() {
std::fs::remove_dir_all(&self.root)?;
}
Ok(())
}
fn metadata_path(&self, name: &str) -> PathBuf {
self.root.join("metadata").join(format!("{}.json", name))
}
fn package_path(&self, name: &str, version: &Version) -> PathBuf {
self.root
.join("packages")
.join(name)
.join(version.to_string())
}
fn parse_cached_metadata(&self, _content: &str) -> Option<PackageMetadata> {
None
}
fn extract_tarball(&self, _data: &[u8], _dest: &Path) -> Result<(), RegistryError> {
Ok(())
}
}
mod urlencoding {
pub fn encode(s: &str) -> String {
let mut result = String::with_capacity(s.len() * 3);
for c in s.chars() {
match c {
'A'..='Z' | 'a'..='z' | '0'..='9' | '-' | '_' | '.' | '~' => {
result.push(c);
}
_ => {
for b in c.to_string().as_bytes() {
result.push_str(&format!("%{:02X}", b));
}
}
}
}
result
}
}
fn sha256_hex(data: &[u8]) -> String {
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
let mut hasher = DefaultHasher::new();
data.hash(&mut hasher);
format!(
"{:016x}{:016x}{:016x}{:016x}",
hasher.finish(),
hasher.finish(),
hasher.finish(),
hasher.finish()
)
}
#[derive(Debug, Clone)]
pub struct GitSource {
pub url: String,
pub branch: Option<String>,
pub tag: Option<String>,
pub rev: Option<String>,
}
impl GitSource {
pub fn new(url: impl Into<String>) -> Self {
Self {
url: url.into(),
branch: None,
tag: None,
rev: None,
}
}
pub fn with_branch(mut self, branch: impl Into<String>) -> Self {
self.branch = Some(branch.into());
self
}
pub fn with_tag(mut self, tag: impl Into<String>) -> Self {
self.tag = Some(tag.into());
self
}
pub fn with_rev(mut self, rev: impl Into<String>) -> Self {
self.rev = Some(rev.into());
self
}
pub fn fetch(&self, dest: &Path) -> Result<(), RegistryError> {
let _ = dest;
Err(RegistryError::Network(
"Git support not implemented".to_string(),
))
}
}
#[derive(Debug, Clone)]
pub struct PathSource {
pub path: PathBuf,
}
impl PathSource {
pub fn new(path: impl Into<PathBuf>) -> Self {
Self { path: path.into() }
}
pub fn resolve(&self, base: &Path) -> PathBuf {
if self.path.is_absolute() {
self.path.clone()
} else {
base.join(&self.path)
}
}
pub fn load_manifest(&self) -> Result<Manifest, RegistryError> {
let manifest_path = self.path.join("Quanta.toml");
let content = std::fs::read_to_string(&manifest_path)?;
Manifest::from_str(&content)
.map_err(|e| RegistryError::InvalidResponse(format!("invalid manifest: {}", e)))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_url_encoding() {
assert_eq!(urlencoding::encode("hello"), "hello");
assert_eq!(urlencoding::encode("hello world"), "hello%20world");
assert_eq!(urlencoding::encode("a+b=c"), "a%2Bb%3Dc");
}
#[test]
fn test_cache_paths() {
let cache = PackageCache::new(PathBuf::from("/cache"));
let meta_path = cache.metadata_path("my-package");
assert!(meta_path.to_str().unwrap().contains("my-package.json"));
let pkg_path = cache.package_path("my-package", &Version::new(1, 2, 3));
assert!(pkg_path.to_str().unwrap().contains("1.2.3"));
}
#[test]
fn test_git_source() {
let source = GitSource::new("https://github.com/user/repo")
.with_branch("main")
.with_tag("v1.0.0");
assert_eq!(source.url, "https://github.com/user/repo");
assert_eq!(source.branch, Some("main".to_string()));
assert_eq!(source.tag, Some("v1.0.0".to_string()));
}
#[test]
fn test_path_source() {
let source = PathSource::new("../other-package");
let resolved = source.resolve(Path::new("/home/user/project"));
assert!(resolved.to_str().unwrap().contains("other-package"));
}
}