diff --git a/Cargo.toml b/Cargo.toml index 86fe4e3..39e303e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,7 @@ members = [ "userland", "specfile", "ports", - "crates/*", + "pkg6", "xtask", ] resolver = "2" diff --git a/libips/src/image/mod.rs b/libips/src/image/mod.rs index 623243c..d218b47 100644 --- a/libips/src/image/mod.rs +++ b/libips/src/image/mod.rs @@ -1,10 +1,12 @@ mod properties; +#[cfg(test)] +mod tests; use miette::Diagnostic; use properties::*; use serde::{Deserialize, Serialize}; use std::collections::HashMap; -use std::fs::File; +use std::fs::{self, File}; use std::path::{Path, PathBuf}; use thiserror::Error; @@ -23,23 +25,49 @@ pub enum ImageError { help("Check the JSON format and try again") )] Json(#[from] serde_json::Error), + + #[error("Invalid image path: {0}")] + #[diagnostic( + code(ips::image_error::invalid_path), + help("Provide a valid path for the image") + )] + InvalidPath(String), } pub type Result = std::result::Result; +/// Type of image, either Full (base path of "/") or Partial (attached to a full image) +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)] +pub enum ImageType { + /// Full image with base path of "/" + Full, + /// Partial image attached to a full image + Partial, +} + +/// Represents an IPS image, which can be either a Full image or a Partial image #[derive(Clone, Debug, Deserialize, Serialize)] pub struct Image { + /// Path to the image path: PathBuf, + /// Type of image (Full or Partial) + image_type: ImageType, + /// Image properties props: Vec, + /// Image version version: i32, + /// Variants variants: HashMap, + /// Mediators mediators: HashMap, } impl Image { - pub fn new>(path: P) -> Image { + /// Creates a new Full image at the specified path + pub fn new_full>(path: P) -> Image { Image { path: path.into(), + image_type: ImageType::Full, version: 5, variants: HashMap::new(), mediators: HashMap::new(), @@ -47,21 +75,95 @@ impl Image { } } - pub fn open>(path: P) -> Result { - let path = path.as_ref(); - - //TODO: Parse the old INI format of pkg5 - //TODO once root images are implemented, look for metadata under sub directory var/pkg - let props_path = path.join("pkg6.image.json"); - let mut f = File::open(props_path)?; - Ok(serde_json::from_reader(&mut f)?) - } - - pub fn open_default>(path: P) -> Image { - if let Ok(img) = Image::open(path.as_ref()) { - img - } else { - Image::new(path.as_ref()) + /// Creates a new Partial image at the specified path + pub fn new_partial>(path: P) -> Image { + Image { + path: path.into(), + image_type: ImageType::Partial, + version: 5, + variants: HashMap::new(), + mediators: HashMap::new(), + props: vec![], } } + + /// Returns the path to the image + pub fn path(&self) -> &Path { + &self.path + } + + /// Returns the type of the image + pub fn image_type(&self) -> &ImageType { + &self.image_type + } + + /// Returns the path to the metadata directory for this image + pub fn metadata_dir(&self) -> PathBuf { + match self.image_type { + ImageType::Full => self.path.join("var/pkg"), + ImageType::Partial => self.path.join(".pkg"), + } + } + + /// Returns the path to the image JSON file + pub fn image_json_path(&self) -> PathBuf { + self.metadata_dir().join("pkg6.image.json") + } + + /// Creates the metadata directory if it doesn't exist + pub fn create_metadata_dir(&self) -> Result<()> { + let metadata_dir = self.metadata_dir(); + fs::create_dir_all(&metadata_dir).map_err(|e| { + ImageError::IO(std::io::Error::new( + std::io::ErrorKind::Other, + format!("Failed to create metadata directory at {:?}: {}", metadata_dir, e), + )) + }) + } + + /// Saves the image data to the metadata directory + pub fn save(&self) -> Result<()> { + self.create_metadata_dir()?; + let json_path = self.image_json_path(); + let file = File::create(&json_path).map_err(|e| { + ImageError::IO(std::io::Error::new( + std::io::ErrorKind::Other, + format!("Failed to create image JSON file at {:?}: {}", json_path, e), + )) + })?; + serde_json::to_writer_pretty(file, self).map_err(ImageError::Json) + } + + /// Loads an image from the specified path + pub fn load>(path: P) -> Result { + let path = path.as_ref(); + + // Check for both full and partial image JSON files + let full_image = Image::new_full(path); + let partial_image = Image::new_partial(path); + + let full_json_path = full_image.image_json_path(); + let partial_json_path = partial_image.image_json_path(); + + // Determine which JSON file exists + let json_path = if full_json_path.exists() { + full_json_path + } else if partial_json_path.exists() { + partial_json_path + } else { + return Err(ImageError::InvalidPath(format!( + "Image JSON file not found at either {:?} or {:?}", + full_json_path, partial_json_path + ))); + }; + + let file = File::open(&json_path).map_err(|e| { + ImageError::IO(std::io::Error::new( + std::io::ErrorKind::Other, + format!("Failed to open image JSON file at {:?}: {}", json_path, e), + )) + })?; + + serde_json::from_reader(file).map_err(ImageError::Json) + } } diff --git a/libips/src/image/tests.rs b/libips/src/image/tests.rs new file mode 100644 index 0000000..57e0aae --- /dev/null +++ b/libips/src/image/tests.rs @@ -0,0 +1,114 @@ +use super::*; +use std::path::Path; +use tempfile::tempdir; + +#[test] +fn test_image_types() { + let full_image = Image::new_full("/"); + let partial_image = Image::new_partial("/tmp/partial"); + + assert_eq!(*full_image.image_type(), ImageType::Full); + assert_eq!(*partial_image.image_type(), ImageType::Partial); +} + +#[test] +fn test_metadata_paths() { + let full_image = Image::new_full("/"); + let partial_image = Image::new_partial("/tmp/partial"); + + assert_eq!(full_image.metadata_dir(), Path::new("/var/pkg")); + assert_eq!(partial_image.metadata_dir(), Path::new("/tmp/partial/.pkg")); + + assert_eq!( + full_image.image_json_path(), + Path::new("/var/pkg/pkg6.image.json") + ); + assert_eq!( + partial_image.image_json_path(), + Path::new("/tmp/partial/.pkg/pkg6.image.json") + ); +} + +#[test] +fn test_save_and_load() -> Result<()> { + // Create a temporary directory for testing + let temp_dir = tempdir().expect("Failed to create temp directory"); + let temp_path = temp_dir.path(); + + // Create a full image + let mut full_image = Image::new_full(temp_path); + + // Add some test data + full_image.props.push(ImageProperty::String("test_prop".to_string())); + + // Save the image + full_image.save()?; + + // Check that the metadata directory and JSON file were created + let metadata_dir = temp_path.join("var/pkg"); + let json_path = metadata_dir.join("pkg6.image.json"); + + assert!(metadata_dir.exists()); + assert!(json_path.exists()); + + // Load the image + let loaded_image = Image::load(temp_path)?; + + // Check that the loaded image matches the original + assert_eq!(*loaded_image.image_type(), ImageType::Full); + assert_eq!(loaded_image.path, full_image.path); + assert_eq!(loaded_image.props.len(), 1); + + // Clean up + temp_dir.close().expect("Failed to clean up temp directory"); + + Ok(()) +} + +#[test] +fn test_partial_image() -> Result<()> { + // Create a temporary directory for testing + let temp_dir = tempdir().expect("Failed to create temp directory"); + let temp_path = temp_dir.path(); + + // Create a partial image + let mut partial_image = Image::new_partial(temp_path); + + // Add some test data + partial_image.props.push(ImageProperty::Boolean(true)); + + // Save the image + partial_image.save()?; + + // Check that the metadata directory and JSON file were created + let metadata_dir = temp_path.join(".pkg"); + let json_path = metadata_dir.join("pkg6.image.json"); + + assert!(metadata_dir.exists()); + assert!(json_path.exists()); + + // Load the image + let loaded_image = Image::load(temp_path)?; + + // Check that the loaded image matches the original + assert_eq!(*loaded_image.image_type(), ImageType::Partial); + assert_eq!(loaded_image.path, partial_image.path); + assert_eq!(loaded_image.props.len(), 1); + + // Clean up + temp_dir.close().expect("Failed to clean up temp directory"); + + Ok(()) +} + +#[test] +fn test_invalid_path() { + let result = Image::load("/nonexistent/path"); + assert!(result.is_err()); + + if let Err(ImageError::InvalidPath(_)) = result { + // Expected error + } else { + panic!("Expected InvalidPath error, got {:?}", result); + } +} \ No newline at end of file diff --git a/crates/pkg6/Cargo.toml b/pkg6/Cargo.toml similarity index 85% rename from crates/pkg6/Cargo.toml rename to pkg6/Cargo.toml index 449949f..c91041e 100644 --- a/crates/pkg6/Cargo.toml +++ b/pkg6/Cargo.toml @@ -9,6 +9,6 @@ readme.workspace = true keywords.workspace = true [dependencies] -libips = { version = "*", path = "../../libips" } +libips = { version = "*", path = "../libips" } diff-struct = "0.5.3" serde = { version = "1.0.207", features = ["derive"] } diff --git a/crates/pkg6/src/main.rs b/pkg6/src/main.rs similarity index 100% rename from crates/pkg6/src/main.rs rename to pkg6/src/main.rs