diff --git a/libips/src/repository/catalog_writer.rs b/libips/src/repository/catalog_writer.rs index 9ecc149..e1d6151 100644 --- a/libips/src/repository/catalog_writer.rs +++ b/libips/src/repository/catalog_writer.rs @@ -18,7 +18,11 @@ use super::{RepositoryError, Result}; struct PythonFormatter; impl Formatter for PythonFormatter { - fn begin_object_key(&mut self, writer: &mut W, first: bool) -> std::io::Result<()> { + fn begin_object_key( + &mut self, + writer: &mut W, + first: bool, + ) -> std::io::Result<()> { if !first { writer.write_all(b",")?; } @@ -29,7 +33,11 @@ impl Formatter for PythonFormatter { writer.write_all(b":") } - fn begin_array_value(&mut self, writer: &mut W, first: bool) -> std::io::Result<()> { + fn begin_array_value( + &mut self, + writer: &mut W, + first: bool, + ) -> std::io::Result<()> { if !first { writer.write_all(b",")?; } diff --git a/libips/src/repository/file_backend.rs b/libips/src/repository/file_backend.rs index 635b382..210fef9 100644 --- a/libips/src/repository/file_backend.rs +++ b/libips/src/repository/file_backend.rs @@ -10,7 +10,7 @@ use lz4::EncoderBuilder; use regex::Regex; use serde::{Deserialize, Serialize}; use sha2::{Digest as Sha2Digest, Sha256}; -use std::collections::{HashMap, HashSet, BTreeMap}; +use std::collections::{BTreeMap, HashMap, HashSet}; use std::fs; use std::fs::File; use std::io::{Read, Write}; @@ -23,7 +23,7 @@ use walkdir::WalkDir; use crate::actions::{File as FileAction, Manifest}; use crate::digest::Digest; use crate::fmri::Fmri; -use crate::payload::{Payload, PayloadCompressionAlgorithm, PayloadArchitecture, PayloadBits}; +use crate::payload::{Payload, PayloadArchitecture, PayloadBits, PayloadCompressionAlgorithm}; use super::catalog_writer; use super::{ @@ -74,17 +74,48 @@ struct SearchQuery { fn parse_query(query: &str) -> SearchQuery { if !query.contains(':') { - return SearchQuery { pkg: None, action: None, index: None, token: query.to_string() }; + return SearchQuery { + pkg: None, + action: None, + index: None, + token: query.to_string(), + }; } - + let parts: Vec<&str> = query.split(':').collect(); - let get_opt = |s: &str| if s.is_empty() { None } else { Some(s.to_string()) }; + let get_opt = |s: &str| { + if s.is_empty() { + None + } else { + Some(s.to_string()) + } + }; match parts.len() { - 2 => SearchQuery { pkg: None, action: None, index: get_opt(parts[0]), token: parts[1].to_string() }, - 3 => SearchQuery { pkg: None, action: get_opt(parts[0]), index: get_opt(parts[1]), token: parts[2].to_string() }, - 4 => SearchQuery { pkg: get_opt(parts[0]), action: get_opt(parts[1]), index: get_opt(parts[2]), token: parts[3].to_string() }, - _ => SearchQuery { pkg: None, action: None, index: None, token: query.to_string() }, + 2 => SearchQuery { + pkg: None, + action: None, + index: get_opt(parts[0]), + token: parts[1].to_string(), + }, + 3 => SearchQuery { + pkg: None, + action: get_opt(parts[0]), + index: get_opt(parts[1]), + token: parts[2].to_string(), + }, + 4 => SearchQuery { + pkg: get_opt(parts[0]), + action: get_opt(parts[1]), + index: get_opt(parts[2]), + token: parts[3].to_string(), + }, + _ => SearchQuery { + pkg: None, + action: None, + index: None, + token: query.to_string(), + }, } } @@ -130,7 +161,15 @@ impl SearchIndex { } /// Add a term to the index for a package - fn add_term(&mut self, term: &str, fmri: &str, action_type: &str, index_type: &str, value: &str, attributes: Option>) { + fn add_term( + &mut self, + term: &str, + fmri: &str, + action_type: &str, + index_type: &str, + value: &str, + attributes: Option>, + ) { let token = term.to_string(); // Convert term to lowercase for case-insensitive search let term_lower = term.to_lowercase(); @@ -165,7 +204,7 @@ impl SearchIndex { // Actually man page says `pkg_name` is implicit. // Let's index it as: action="pkg", index="name", value=stem self.add_term(stem, &fmri, "pkg", "name", stem, None); - + // Also index parts of the stem if it contains '/'? // Legacy behavior might index tokens. for part in stem.split('/') { @@ -250,7 +289,7 @@ impl SearchIndex { let parsed = parse_query(term_str); let token_has_wildcard = parsed.token.contains('*') || parsed.token.contains('?'); let token_lower = parsed.token.to_lowercase(); - + let mut term_entries: Vec<&IndexEntry> = Vec::new(); if token_has_wildcard { @@ -267,47 +306,62 @@ impl SearchIndex { term_entries.extend(entries); } } - + // Filter entries based on structured query and case sensitivity - let filtered: Vec<&IndexEntry> = term_entries.into_iter().filter(|e| { - // Check Index Type - if let Some(idx) = &parsed.index { - if &e.index_type != idx { return false; } - } - // Check Action Type - if let Some(act) = &parsed.action { - if &e.action_type != act { return false; } - } - // Check Package Name (FMRI) - if let Some(pkg) = &parsed.pkg { - let pkg_has_wildcard = pkg.contains('*') || pkg.contains('?'); - if pkg_has_wildcard { - let re_str = glob_to_regex(&pkg.to_lowercase()); - if let Ok(re) = Regex::new(&re_str) { - // FMRIs are usually lowercase, but let's compare lowercase to be safe/consistent - if !re.is_match(&e.fmri.to_lowercase()) { return false; } + let filtered: Vec<&IndexEntry> = term_entries + .into_iter() + .filter(|e| { + // Check Index Type + if let Some(idx) = &parsed.index { + if &e.index_type != idx { + return false; + } + } + // Check Action Type + if let Some(act) = &parsed.action { + if &e.action_type != act { + return false; + } + } + // Check Package Name (FMRI) + if let Some(pkg) = &parsed.pkg { + let pkg_has_wildcard = pkg.contains('*') || pkg.contains('?'); + if pkg_has_wildcard { + let re_str = glob_to_regex(&pkg.to_lowercase()); + if let Ok(re) = Regex::new(&re_str) { + // FMRIs are usually lowercase, but let's compare lowercase to be safe/consistent + if !re.is_match(&e.fmri.to_lowercase()) { + return false; + } + } + } else { + if !e.fmri.contains(pkg) { + return false; + } } - } else { - if !e.fmri.contains(pkg) { return false; } } - } - // Check Case Sensitivity on VALUE - if case_sensitive { - if token_has_wildcard { - let re_str = glob_to_regex(&parsed.token); // Original token - if let Ok(re) = Regex::new(&re_str) { - if !re.is_match(&e.token) { return false; } - } - } else { - if e.token != parsed.token { return false; } - } - } - - true - }).collect(); + // Check Case Sensitivity on VALUE + if case_sensitive { + if token_has_wildcard { + let re_str = glob_to_regex(&parsed.token); // Original token + if let Ok(re) = Regex::new(&re_str) { + if !re.is_match(&e.token) { + return false; + } + } + } else { + if e.token != parsed.token { + return false; + } + } + } - if filtered.is_empty() { + true + }) + .collect(); + + if filtered.is_empty() { return Vec::new(); // Term found no matches } @@ -334,7 +388,8 @@ impl SearchIndex { } results.sort_by(|a, b| { - a.fmri.cmp(&b.fmri) + a.fmri + .cmp(&b.fmri) .then(a.action_type.cmp(&b.action_type)) .then(a.index_type.cmp(&b.index_type)) .then(a.value.cmp(&b.value)) @@ -388,7 +443,6 @@ pub struct FileBackend { Option>, } - /// Transaction for publishing packages pub struct Transaction { /// Unique ID for the transaction @@ -1666,7 +1720,7 @@ impl ReadableRepository for FileBackend { if added_fmris.contains(&entry.fmri) { continue; } - + if let Ok(fmri) = Fmri::parse(&entry.fmri) { debug!("Adding package to results: {}", fmri); results.push(PackageInfo { fmri }); @@ -2444,7 +2498,11 @@ impl FileBackend { info!("Rebuilding catalog (batched) for publisher: {}", publisher); let quote_action_value = |s: &str| -> String { - if s.is_empty() || s.contains(char::is_whitespace) || s.contains('"') || s.contains('\'') { + if s.is_empty() + || s.contains(char::is_whitespace) + || s.contains('"') + || s.contains('\'') + { format!("\"{}\"", s.replace("\"", "\\\"")) } else { s.to_string() @@ -2541,7 +2599,12 @@ impl FileBackend { // Extract variant and facet actions for attr in &manifest.attributes { if attr.key.starts_with("variant.") || attr.key.starts_with("facet.") { - let values_str = attr.values.iter().map(|s| quote_action_value(s)).collect::>().join(" value="); + let values_str = attr + .values + .iter() + .map(|s| quote_action_value(s)) + .collect::>() + .join(" value="); dependency_actions.push(format!("set name={} value={}", attr.key, values_str)); } } @@ -2562,7 +2625,12 @@ impl FileBackend { && !attr.key.starts_with("facet.") && attr.key != "pkg.fmri" { - let values_str = attr.values.iter().map(|s| quote_action_value(s)).collect::>().join(" value="); + let values_str = attr + .values + .iter() + .map(|s| quote_action_value(s)) + .collect::>() + .join(" value="); summary_actions.push(format!("set name={} value={}", attr.key, values_str)); } } @@ -2734,7 +2802,8 @@ impl FileBackend { ); } - let update_log_sig = catalog_writer::write_update_log(&update_log_path, &mut update_log)?; + let update_log_sig = + catalog_writer::write_update_log(&update_log_path, &mut update_log)?; debug!("Wrote update log file"); // Add an update log to catalog.attrs @@ -2932,15 +3001,19 @@ impl FileBackend { match Manifest::parse_file(&path) { Ok(manifest) => { // Look for the pkg.fmri attribute - let fmri_opt = manifest.attributes.iter().find(|a| a.key == "pkg.fmri").and_then(|a| a.values.first()); - + let fmri_opt = manifest + .attributes + .iter() + .find(|a| a.key == "pkg.fmri") + .and_then(|a| a.values.first()); + if let Some(fmri_str) = fmri_opt { // Parse the FMRI using our Fmri type match Fmri::parse(fmri_str) { Ok(parsed_fmri) => { let fmri = parsed_fmri.to_string(); let stem = parsed_fmri.stem().to_string(); - + // Add package mapping index.packages.insert(fmri.clone(), stem.clone()); @@ -2948,19 +3021,30 @@ impl FileBackend { index.add_term(&stem, &fmri, "pkg", "name", &stem, None); for part in stem.split('/') { if part != stem { - index.add_term(part, &fmri, "pkg", "name", &stem, None); + index.add_term( + part, &fmri, "pkg", "name", &stem, None, + ); } } - + // 2. Index Publisher if let Some(publ) = &parsed_fmri.publisher { - index.add_term(publ, &fmri, "pkg", "publisher", publ, None); + index.add_term( + publ, + &fmri, + "pkg", + "publisher", + publ, + None, + ); } - + // 3. Index Version let version = parsed_fmri.version(); if !version.is_empty() { - index.add_term(&version, &fmri, "pkg", "version", &version, None); + index.add_term( + &version, &fmri, "pkg", "version", &version, None, + ); } // 4. Index Files with attributes @@ -2970,42 +3054,68 @@ impl FileBackend { attrs.insert("owner".to_string(), file.owner.clone()); attrs.insert("group".to_string(), file.group.clone()); attrs.insert("mode".to_string(), file.mode.clone()); - + if let Some(payload) = &file.payload { - let arch_str = match payload.architecture { + let arch_str = match payload.architecture { PayloadArchitecture::I386 => Some("i386"), PayloadArchitecture::SPARC => Some("sparc"), - _ => None - }; - if let Some(a) = arch_str { - attrs.insert("elfarch".to_string(), a.to_string()); - } - - let bits_str = match payload.bitness { - PayloadBits::Bits64 => Some("64"), - PayloadBits::Bits32 => Some("32"), - _ => None - }; - if let Some(b) = bits_str { - attrs.insert("elfbits".to_string(), b.to_string()); - } - - attrs.insert("pkg.content-hash".to_string(), payload.primary_identifier.to_string()); + _ => None, + }; + if let Some(a) = arch_str { + attrs.insert( + "elfarch".to_string(), + a.to_string(), + ); + } + + let bits_str = match payload.bitness { + PayloadBits::Bits64 => Some("64"), + PayloadBits::Bits32 => Some("32"), + _ => None, + }; + if let Some(b) = bits_str { + attrs.insert( + "elfbits".to_string(), + b.to_string(), + ); + } + + attrs.insert( + "pkg.content-hash".to_string(), + payload.primary_identifier.to_string(), + ); } - + for prop in file.properties { attrs.insert(prop.key, prop.value); } - + // index=path - index.add_term(&file.path, &fmri, "file", "path", &file.path, Some(attrs.clone())); - + index.add_term( + &file.path, + &fmri, + "file", + "path", + &file.path, + Some(attrs.clone()), + ); + // index=basename - if let Some(basename) = Path::new(&file.path).file_name().and_then(|s| s.to_str()) { - index.add_term(basename, &fmri, "file", "basename", &file.path, Some(attrs)); + if let Some(basename) = Path::new(&file.path) + .file_name() + .and_then(|s| s.to_str()) + { + index.add_term( + basename, + &fmri, + "file", + "basename", + &file.path, + Some(attrs), + ); } } - + // 5. Index Directories for dir in manifest.directories { let mut attrs = BTreeMap::new(); @@ -3013,32 +3123,66 @@ impl FileBackend { attrs.insert("owner".to_string(), dir.owner.clone()); attrs.insert("group".to_string(), dir.group.clone()); attrs.insert("mode".to_string(), dir.mode.clone()); - + // index=path - index.add_term(&dir.path, &fmri, "dir", "path", &dir.path, Some(attrs.clone())); - + index.add_term( + &dir.path, + &fmri, + "dir", + "path", + &dir.path, + Some(attrs.clone()), + ); + // index=basename - if let Some(basename) = Path::new(&dir.path).file_name().and_then(|s| s.to_str()) { - index.add_term(basename, &fmri, "dir", "basename", &dir.path, Some(attrs)); + if let Some(basename) = Path::new(&dir.path) + .file_name() + .and_then(|s| s.to_str()) + { + index.add_term( + basename, + &fmri, + "dir", + "basename", + &dir.path, + Some(attrs), + ); } } - + // 6. Index Dependencies for dep in manifest.dependencies { if let Some(dep_fmri) = &dep.fmri { let dep_fmri_str = dep_fmri.to_string(); let mut attrs = BTreeMap::new(); - + if !dep.dependency_type.is_empty() { - attrs.insert("type".to_string(), dep.dependency_type.clone()); + attrs.insert( + "type".to_string(), + dep.dependency_type.clone(), + ); } - + for prop in dep.optional { attrs.insert(prop.key, prop.value); } - - index.add_term(&dep_fmri_str, &fmri, "depend", "fmri", &dep_fmri_str, Some(attrs.clone())); - index.add_term(dep_fmri.stem(), &fmri, "depend", "fmri", &dep_fmri_str, Some(attrs)); + + index.add_term( + &dep_fmri_str, + &fmri, + "depend", + "fmri", + &dep_fmri_str, + Some(attrs.clone()), + ); + index.add_term( + dep_fmri.stem(), + &fmri, + "depend", + "fmri", + &dep_fmri_str, + Some(attrs), + ); } } } @@ -3101,10 +3245,13 @@ impl FileBackend { let entries = index.search(query, case_sensitive, limit); results.extend(entries); } else { - debug!("No search index found for publisher: {}, falling back to simple listing", pub_name); + debug!( + "No search index found for publisher: {}, falling back to simple listing", + pub_name + ); // Fallback: list packages and convert to basic IndexEntries - let all_packages = self.list_packages(Some(&pub_name), None)?; - let matching_packages: Vec = all_packages + let all_packages = self.list_packages(Some(&pub_name), None)?; + let matching_packages: Vec = all_packages .into_iter() .filter(|pkg| pkg.fmri.stem().contains(query)) .map(|pkg| { diff --git a/pkg6depotd/src/http/handlers/mod.rs b/pkg6depotd/src/http/handlers/mod.rs index 87df9a0..ff5c198 100644 --- a/pkg6depotd/src/http/handlers/mod.rs +++ b/pkg6depotd/src/http/handlers/mod.rs @@ -3,5 +3,5 @@ pub mod file; pub mod info; pub mod manifest; pub mod publisher; -pub mod versions; pub mod search; +pub mod versions; diff --git a/pkg6depotd/src/http/handlers/search.rs b/pkg6depotd/src/http/handlers/search.rs index b8b0bef..98acd16 100644 --- a/pkg6depotd/src/http/handlers/search.rs +++ b/pkg6depotd/src/http/handlers/search.rs @@ -8,13 +8,13 @@ pub async fn get_search_v0( State(repo): State>, Path((publisher, token)): Path<(String, String)>, ) -> Result { - // Decode the token (it might be URL encoded in the path, but axum usually decodes path params? + // Decode the token (it might be URL encoded in the path, but axum usually decodes path params? // Actually, axum decodes percent-encoded path segments automatically if typed as String? // Let's assume yes or use it as is. // However, typical search tokens might contain chars that need decoding. // If standard axum decoding is not enough, we might need manual decoding. // But let's start with standard. - + // Call search let results = repo.search(Some(&publisher), &token, false)?; @@ -27,11 +27,7 @@ pub async fn get_search_v0( )); } - Ok(( - [(axum::http::header::CONTENT_TYPE, "text/plain")], - body, - ) - .into_response()) + Ok(([(axum::http::header::CONTENT_TYPE, "text/plain")], body).into_response()) } pub async fn get_search_v1( @@ -81,23 +77,28 @@ pub async fn get_search_v1( fn split_v1_token(token: &str) -> Option<(&str, &str)> { // Try to find the 4th underscore let mut parts = token.splitn(5, '_'); - if let (Some(_), Some(_), Some(_), Some(_), Some(_)) = (parts.next(), parts.next(), parts.next(), parts.next(), parts.next()) { - // We found 4 parts and a remainder. + if let (Some(_), Some(_), Some(_), Some(_), Some(_)) = ( + parts.next(), + parts.next(), + parts.next(), + parts.next(), + parts.next(), + ) { + // We found 4 parts and a remainder. // We need to reconstruct where the split happened to return slices // Actually, splitn(5) returns 5 parts. The last part is the remainder. // But we want to be careful about the length of the prefix. - + // Let's iterate chars to find 4th underscore let mut underscore_count = 0; for (i, c) in token.chars().enumerate() { if c == '_' { underscore_count += 1; if underscore_count == 4 { - return Some((&token[..i], &token[i+1..])); + return Some((&token[..i], &token[i + 1..])); } } } } None } - diff --git a/pkg6depotd/src/repo.rs b/pkg6depotd/src/repo.rs index f660193..d844d6b 100644 --- a/pkg6depotd/src/repo.rs +++ b/pkg6depotd/src/repo.rs @@ -1,7 +1,7 @@ use crate::config::Config; use crate::errors::{DepotError, Result}; use libips::fmri::Fmri; -use libips::repository::{FileBackend, ReadableRepository, IndexEntry}; +use libips::repository::{FileBackend, IndexEntry, ReadableRepository}; use std::path::PathBuf; use std::sync::Mutex; @@ -23,7 +23,12 @@ impl DepotRepo { }) } - pub fn search(&self, publisher: Option<&str>, query: &str, case_sensitive: bool) -> Result> { + pub fn search( + &self, + publisher: Option<&str>, + query: &str, + case_sensitive: bool, + ) -> Result> { let backend = self .backend .lock() diff --git a/pkg6depotd/tests/integration_tests.rs b/pkg6depotd/tests/integration_tests.rs index efca949..e534889 100644 --- a/pkg6depotd/tests/integration_tests.rs +++ b/pkg6depotd/tests/integration_tests.rs @@ -375,9 +375,13 @@ async fn test_file_url_without_algo() { // Expected format: /{publisher}/file/1/{hash} let url = format!("{}/test/file/1/{}", base_url, hash); println!("Requesting: {}", url); - + let resp = client.get(&url).send().await.unwrap(); - - assert_eq!(resp.status(), 200, "Should handle file URL without algorithm"); + + assert_eq!( + resp.status(), + 200, + "Should handle file URL without algorithm" + ); let _content = resp.text().await.unwrap(); }