diff --git a/README.md b/README.md index ed9997b..751103f 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ A unified command-line compression utility written in Rust, providing a consiste ## Features -- **Multi-Format Support**: GZIP, BZIP2, XZ, TAR, and compound formats (TGZ, TBZ2, TXZ) +- **Multi-Format Support**: GZIP, BZIP2, XZ, ZIP, TAR, and compound formats (TGZ, TBZ2, TXZ) - **Parallel Processing**: Concurrent compression/decompression of multiple files using Rayon - **Timestamp Options**: Add timestamps to output filenames (date, datetime, or nanoseconds) - **File Collection**: Combine multiple files into single archives @@ -32,6 +32,9 @@ jcz -c gzip file.txt # Compress with BZIP2 at level 9 jcz -c bzip2 -l 9 file.txt +# Compress with ZIP +jcz -c zip file.txt + # Create TAR archive jcz -c tar directory/ ``` @@ -95,6 +98,7 @@ jcz -c tgz -A myarchive file1.txt file2.txt - `gzip` - GZIP compression (.gz) - `bzip2` - BZIP2 compression (.bz2) - `xz` - XZ compression (.xz) +- `zip` - ZIP compression (.zip) - `tar` - TAR archive (.tar) - `tgz` - TAR + GZIP (.tar.gz) - `tbz2` - TAR + BZIP2 (.tar.bz2) @@ -117,7 +121,7 @@ JCDBG=debug jcz -c gzip file.txt The implementation follows a modular design: - **Core Module**: Trait definitions, error types, configuration structures -- **Compressor Modules**: Individual implementations for GZIP, BZIP2, XZ, TAR +- **Compressor Modules**: Individual implementations for GZIP, BZIP2, XZ, ZIP, TAR - **Operations Module**: High-level operations (compress, decompress, compound, collection) - **Utils Module**: File system utilities, logging, validation, timestamp generation - **CLI Module**: Command-line argument parsing and command execution @@ -140,7 +144,7 @@ The implementation follows a modular design: ## System Requirements - Rust 2021 edition or later -- System utilities: `gzip`, `bzip2`, `xz`, `tar`, `mv`, `cp`, `readlink` +- System utilities: `gzip`, `bzip2`, `xz`, `zip`, `unzip`, `tar`, `mv`, `cp`, `readlink` ## Documentation diff --git a/docs/jcz_sdd.md b/docs/jcz_sdd.md index 9d0553e..e0aa481 100644 --- a/docs/jcz_sdd.md +++ b/docs/jcz_sdd.md @@ -1,8 +1,8 @@ # Software Design Document (SDD) ## JCZ - Just Compress Zip Utility (Rust Implementation) -**Version:** 1.0 -**Date:** 2025-11-01 +**Version:** 1.1 +**Date:** 2025-11-30 **Document Status:** Final **Implementation Language:** Rust @@ -69,14 +69,14 @@ This document covers: │ (Trait Definitions, Common Interfaces, Type Dispatch) │ └──────────────────────┬──────────────────────────────────────┘ │ - ┌─────────────┼─────────────┬────────────┐ - ▼ ▼ ▼ ▼ - ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐ - │ GZIP │ │ BZIP2 │ │ XZ │ │ TAR │ - │ Module │ │ Module │ │ Module │ │ Module │ - └────────┘ └────────┘ └────────┘ └────────┘ - │ │ │ │ - └─────────────┴─────────────┴────────────┘ + ┌─────────────┼─────────────┬────────────┬────────────┐ + ▼ ▼ ▼ ▼ ▼ + ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐ + │ GZIP │ │ BZIP2 │ │ XZ │ │ ZIP │ │ TAR │ + │ Module │ │ Module │ │ Module │ │ Module │ │ Module │ + └────────┘ └────────┘ └────────┘ └────────┘ └────────┘ + │ │ │ │ │ + └─────────────┴─────────────┴────────────┴────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────┐ @@ -113,6 +113,7 @@ jcz/ │ │ ├── gzip.rs # GZIP implementation │ │ ├── bzip2.rs # BZIP2 implementation │ │ ├── xz.rs # XZ implementation +│ │ ├── zip.rs # ZIP implementation │ │ └── tar.rs # TAR implementation │ ├── operations/ │ │ ├── mod.rs # Operations module root @@ -465,6 +466,7 @@ pub enum CompressionFormat { Gzip, Bzip2, Xz, + Zip, Tar, } @@ -475,6 +477,7 @@ impl CompressionFormat { CompressionFormat::Gzip => "gz", CompressionFormat::Bzip2 => "bz2", CompressionFormat::Xz => "xz", + CompressionFormat::Zip => "zip", CompressionFormat::Tar => "tar", } } @@ -485,6 +488,7 @@ impl CompressionFormat { "gz" => Some(CompressionFormat::Gzip), "bz2" => Some(CompressionFormat::Bzip2), "xz" => Some(CompressionFormat::Xz), + "zip" => Some(CompressionFormat::Zip), "tar" => Some(CompressionFormat::Tar), _ => None, } @@ -496,6 +500,7 @@ impl CompressionFormat { CompressionFormat::Gzip => "gzip", CompressionFormat::Bzip2 => "bzip2", CompressionFormat::Xz => "xz", + CompressionFormat::Zip => "zip", CompressionFormat::Tar => "tar", } } @@ -954,12 +959,37 @@ impl MultiFileCompressor for TarCompressor { - **Path manipulation**: Handle parent/basename splitting - **No compression level**: Returns true for any level (no-op) -#### 3.2.3 Compressor Factory (`src/compressors/mod.rs`) +#### 3.2.3 ZIP Implementation (`src/compressors/zip.rs`) + +**Purpose**: ZIP compression and decompression using system `zip` and `unzip` commands. + +**Key Features**: +- Supports both files and directories +- Archive format (can contain multiple files) +- Compression levels 0-9 +- Cross-platform compatibility + +**Design Rationale**: +- **Archive support**: Unlike gzip/bzip2/xz, ZIP is both a compression algorithm and archive format +- **Directory compression**: Recursive flag (`-r`) enables directory compression +- **Quiet mode**: Suppress verbose output for cleaner logs +- **Flexible extraction**: Handles single files, directories, and multiple loose files + +**Implementation Notes**: +- Uses `zip` command for compression with `-q` (quiet) and optional `-r` (recursive) +- Uses `unzip` command for decompression with `-o` (overwrite) +- Compression levels: 0 (store only) to 9 (maximum compression) +- Default level: 6 +- Supports timestamp options for output naming +- `decompress_in_dir` method detects extraction result (single file, directory, or multiple files) + +#### 3.2.4 Compressor Factory (`src/compressors/mod.rs`) ```rust pub mod gzip; pub mod bzip2; pub mod xz; +pub mod zip; pub mod tar; use crate::core::compressor::Compressor; @@ -972,6 +1002,7 @@ pub fn create_compressor(format: CompressionFormat) -> Box { CompressionFormat::Gzip => Box::new(gzip::GzipCompressor::new()), CompressionFormat::Bzip2 => Box::new(bzip2::Bzip2Compressor::new()), CompressionFormat::Xz => Box::new(xz::XzCompressor::new()), + CompressionFormat::Zip => Box::new(zip::ZipCompressor::new()), CompressionFormat::Tar => Box::new(tar::TarCompressor::new()), } } @@ -1929,7 +1960,7 @@ impl CliArgs { } // Validate compression command - let valid_commands = ["gzip", "bzip2", "xz", "tar", "tgz", "tbz2", "txz"]; + let valid_commands = ["gzip", "bzip2", "xz", "zip", "tar", "tgz", "tbz2", "txz"]; if !valid_commands.contains(&self.command.as_str()) { return Err(format!("Invalid compression command: {}", self.command)); } @@ -2049,7 +2080,7 @@ fn handle_compress( Ok(()) } } else { - // Simple format (gzip, bzip2, xz, tar) + // Simple format (gzip, bzip2, xz, zip, tar) let format = CompressionFormat::from_extension(command) .ok_or_else(|| JcError::InvalidCommand(command.to_string()))?; @@ -2336,7 +2367,7 @@ jobs: - name: Install compression tools run: | sudo apt-get update - sudo apt-get install -y gzip bzip2 xz-utils tar + sudo apt-get install -y gzip bzip2 xz-utils zip unzip tar - name: Check formatting run: cargo fmt -- --check diff --git a/docs/jcz_srs.md b/docs/jcz_srs.md index f2f9394..ec631a6 100644 --- a/docs/jcz_srs.md +++ b/docs/jcz_srs.md @@ -1,8 +1,8 @@ # Software Requirements Specification (SRS) ## JCZ - Just Compress Zip Utility -**Version:** 1.0 -**Date:** 2025-11-01 +**Version:** 1.1 +**Date:** 2025-11-30 **Document Status:** Final --- @@ -13,7 +13,7 @@ This document specifies the functional and non-functional requirements for JCZ (Just Compress Zip), a command-line compression utility that provides a unified interface for multiple compression formats. ### 1.2 Scope -JCZ is a command-line tool that simplifies file and directory compression/decompression operations. It supports multiple compression algorithms (GZIP, BZIP2, XZ) and archive formats (TAR), including compound formats (TAR+GZIP, TAR+BZIP2, TAR+XZ). +JCZ is a command-line tool that simplifies file and directory compression/decompression operations. It supports multiple compression algorithms (GZIP, BZIP2, XZ, ZIP) and archive formats (TAR), including compound formats (TAR+GZIP, TAR+BZIP2, TAR+XZ). ### 1.3 Intended Audience - Software developers implementing the tool @@ -27,6 +27,7 @@ JCZ is a command-line tool that simplifies file and directory compression/decomp - **GZIP**: GNU Zip compression algorithm - **BZIP2**: Burrows-Wheeler compression algorithm - **XZ**: LZMA/LZMA2 compression algorithm +- **ZIP**: ZIP compression and archive format - **TAR**: Tape Archive format - **TGZ**: TAR + GZIP compound format (.tar.gz) - **TBZ2**: TAR + BZIP2 compound format (.tar.bz2) @@ -37,10 +38,10 @@ JCZ is a command-line tool that simplifies file and directory compression/decomp ## 2. Overall Description ### 2.1 Product Perspective -JCZ is a standalone command-line utility that wraps system compression tools (gzip, bzip2, xz, tar) to provide a consistent, simplified interface. It acts as a compression abstraction layer, allowing users to compress/decompress files without memorizing different command syntaxes for each compression tool. +JCZ is a standalone command-line utility that wraps system compression tools (gzip, bzip2, xz, zip, unzip, tar) to provide a consistent, simplified interface. It acts as a compression abstraction layer, allowing users to compress/decompress files without memorizing different command syntaxes for each compression tool. ### 2.2 Product Features -1. **Multi-Format Compression**: Support for GZIP, BZIP2, XZ, TAR, TGZ, TBZ2, TXZ +1. **Multi-Format Compression**: Support for GZIP, BZIP2, XZ, ZIP, TAR, TGZ, TBZ2, TXZ 2. **Multi-Format Decompression**: Automatic format detection and sequential decompression 3. **Isolated Decompression**: Temporary directory isolation prevents file conflicts 4. **Force Overwrite**: Skip interactive prompts with --force/-f flag @@ -62,12 +63,12 @@ JCZ is a standalone command-line utility that wraps system compression tools (gz ### 2.4 Operating Environment - **Operating Systems**: Linux, Unix-like systems -- **Dependencies**: System utilities (gzip, bzip2, xz, tar, mv, cp, readlink) +- **Dependencies**: System utilities (gzip, bzip2, xz, zip, unzip, tar, mv, cp, readlink) - **Interface**: Command-line terminal - **File Systems**: Any POSIX-compatible filesystem ### 2.5 Design and Implementation Constraints -1. Must use external system compression tools (gzip, bzip2, xz, tar) +1. Must use external system compression tools (gzip, bzip2, xz, zip, unzip, tar) 2. Must preserve original files during compression operations 3. Must handle file paths up to system limits 4. Must operate within terminal environment constraints @@ -82,11 +83,11 @@ JCZ is a standalone command-line utility that wraps system compression tools (gz #### 3.1.1 Compression Operations ##### FR-COMP-001: Single File Compression -**Description**: The system shall compress individual files using GZIP, BZIP2, or XZ algorithms. +**Description**: The system shall compress individual files using GZIP, BZIP2, XZ, or ZIP algorithms. **Inputs**: -- Input file path (file or directory for TAR) -- Compression algorithm (gzip, bzip2, xz, tar) +- Input file path (file or directory for TAR/ZIP) +- Compression algorithm (gzip, bzip2, xz, zip, tar) - Optional: Compression level (1-9) - Optional: Timestamp option (0-3) - Optional: Destination directory @@ -435,6 +436,7 @@ JCZ is a standalone command-line utility that wraps system compression tools (gz - `gzip` - `bzip2` - `xz` +- `zip` - `tar` - `tgz` - `tbz2` @@ -451,6 +453,7 @@ JCZ is a standalone command-line utility that wraps system compression tools (gz - `.gz` - `.bz2` - `.xz` +- `.zip` - `.tar` - `.tar.gz` - `.tar.bz2` @@ -541,6 +544,8 @@ jcz [Options] [File|Dir]... - `gzip`: For GZIP compression/decompression - `bzip2`: For BZIP2 compression/decompression - `xz`: For XZ compression/decompression +- `zip`: For ZIP compression +- `unzip`: For ZIP decompression - `tar`: For TAR archive creation/extraction - `mv`: For moving files - `cp`: For copying files @@ -912,7 +917,48 @@ jcz [Options] [File|Dir]... - Cannot compress directories (must error) - Only compresses single files -#### 4.5.4 TAR Archiving +#### 4.5.4 ZIP Compression + +**Algorithm**: DEFLATE (similar to GZIP) with archive support + +**Extension**: `.zip` + +**Compression Tool**: `zip` (compression), `unzip` (decompression) + +**Tool Arguments**: +- Compression: `zip - -q [-r] ` +- Decompression: `unzip -o -d ` + +**Compression Levels**: 0-9 +- 0: Store only (no compression) +- 1: Fastest compression +- 9: Best compression +- Default: 6 + +**Default Behavior**: +- Keep original file (zip doesn't modify source) +- Recursive flag (`-r`) for directories +- Quiet mode (`-q`) to suppress output +- Overwrite without prompting (`-o`) for decompression + +**File Naming**: +- Without timestamp: `.zip` +- With timestamp: `_.zip` + +**Decompression**: +- Extract to parent directory or specified destination +- Remove `.zip` extension to determine output name +- Supports single files, directories, and multiple files + +**Special Features**: +- Can compress both files and directories +- Archive format (can contain multiple files) +- Cross-platform compatibility + +**Restrictions**: +- None (supports both files and directories) + +#### 4.5.5 TAR Archiving **Format**: POSIX ustar format diff --git a/src/cli/args.rs b/src/cli/args.rs index 0a714c6..e254e0d 100644 --- a/src/cli/args.rs +++ b/src/cli/args.rs @@ -53,7 +53,7 @@ impl CliArgs { } // Validate compression command - let valid_commands = ["gzip", "bzip2", "xz", "tar", "tgz", "tbz2", "txz"]; + let valid_commands = ["gzip", "bzip2", "xz", "tar", "zip", "tgz", "tbz2", "txz"]; if !valid_commands.contains(&self.command.as_str()) { return Err(format!("Invalid compression command: {}", self.command)); } diff --git a/src/compressors/mod.rs b/src/compressors/mod.rs index 4ac25b5..27c2b1f 100644 --- a/src/compressors/mod.rs +++ b/src/compressors/mod.rs @@ -2,6 +2,7 @@ pub mod bzip2; pub mod gzip; pub mod tar; pub mod xz; +pub mod zip; use std::path::Path; @@ -12,6 +13,7 @@ pub use bzip2::Bzip2Compressor; pub use gzip::GzipCompressor; pub use tar::TarCompressor; pub use xz::XzCompressor; +pub use zip::ZipCompressor; /// Create a compressor instance for the given format pub fn create_compressor(format: CompressionFormat) -> Box { @@ -20,6 +22,7 @@ pub fn create_compressor(format: CompressionFormat) -> Box { CompressionFormat::Bzip2 => Box::new(bzip2::Bzip2Compressor::new()), CompressionFormat::Xz => Box::new(xz::XzCompressor::new()), CompressionFormat::Tar => Box::new(tar::TarCompressor::new()), + CompressionFormat::Zip => Box::new(zip::ZipCompressor::new()), } } diff --git a/src/compressors/zip.rs b/src/compressors/zip.rs new file mode 100644 index 0000000..d182e53 --- /dev/null +++ b/src/compressors/zip.rs @@ -0,0 +1,232 @@ +use std::path::{Path, PathBuf}; +use std::process::Command; + +use crate::core::compressor::Compressor; +use crate::core::config::CompressionConfig; +use crate::core::error::{JcError, JcResult}; +use crate::utils::{copy_to_dir, debug, generate_output_filename, info, move_file_if_needed}; + +/// ZIP compressor/decompressor implementation +#[derive(Debug, Clone)] +pub struct ZipCompressor; + +impl ZipCompressor { + pub fn new() -> Self { + Self + } + + /// Validate that input exists + fn validate_input(&self, path: &Path) -> JcResult<()> { + if !path.exists() { + return Err(JcError::FileNotFound(path.to_path_buf())); + } + Ok(()) + } +} + +impl Compressor for ZipCompressor { + fn name(&self) -> &'static str { + "zip" + } + + fn extension(&self) -> &'static str { + "zip" + } + + fn compress(&self, input: &Path, config: &CompressionConfig) -> JcResult { + self.validate_input(input)?; + + let output_path = generate_output_filename(input, "zip", config.timestamp)?; + info!( + "Compressing {} to {} with zip", + input.display(), + output_path.display() + ); + debug!("Compression level: {}", config.level); + + // Build zip command + let mut cmd = Command::new("zip"); + + // Add compression level (0-9) + cmd.arg(format!("-{}", config.level)); + + // Recursive flag for directories + if input.is_dir() { + cmd.arg("-r"); + } + + // Quiet mode + cmd.arg("-q"); + + // Output file and input + cmd.arg(&output_path).arg(input); + + debug!("Executing: {:?}", cmd); + + let output = cmd + .output() + .map_err(|e| JcError::Other(format!("Failed to execute zip: {}", e)))?; + + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + return Err(JcError::CompressionFailed { + tool: "zip".to_string(), + stderr: stderr.to_string(), + }); + } + + // Move to destination if specified + let final_path = move_file_if_needed(&output_path, &config.move_to)?; + + info!("Compressed file: {}", final_path.display()); + Ok(final_path) + } + + fn decompress(&self, input: &Path, _config: &CompressionConfig) -> JcResult { + // Validate extension + if !input.to_string_lossy().ends_with(".zip") { + return Err(JcError::InvalidExtension( + input.to_path_buf(), + "zip".to_string(), + )); + } + + debug!("Decompressing {} with unzip", input.display()); + + let parent = input.parent().unwrap_or_else(|| Path::new(".")); + + // Execute unzip command + let mut cmd = Command::new("unzip"); + cmd.arg("-o") // overwrite without prompting + .arg(input) + .arg("-d") + .arg(parent); + + let output = cmd + .output() + .map_err(|e| JcError::Other(format!("Failed to execute unzip: {}", e)))?; + + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + return Err(JcError::DecompressionFailed { + tool: "unzip".to_string(), + stderr: stderr.to_string(), + }); + } + + // Output is the filename without .zip extension + let output_path = input.with_extension(""); + + info!("Decompressed ZIP archive: {}", output_path.display()); + Ok(output_path) + } + + fn supports_levels(&self) -> bool { + true + } + + fn validate_level(&self, level: u8) -> bool { + level <= 9 // ZIP supports 0-9 + } + + fn default_level(&self) -> u8 { + 6 + } +} + +impl ZipCompressor { + /// Decompress in a specific working directory + pub fn decompress_in_dir( + &self, + input: &Path, + working_dir: &Path, + _config: &CompressionConfig, + ) -> JcResult { + // Validate extension + if !input.to_string_lossy().ends_with(".zip") { + return Err(JcError::InvalidExtension( + input.to_path_buf(), + "zip".to_string(), + )); + } + + debug!( + "Decompressing {} with unzip in working dir {}", + input.display(), + working_dir.display() + ); + + // Copy input file to working directory + let work_input = copy_to_dir(input, working_dir)?; + + // Execute unzip command in working directory + let mut cmd = Command::new("unzip"); + cmd.arg("-o") // overwrite without prompting + .arg(&work_input) + .arg("-d") + .arg(working_dir); + + let output = cmd + .output() + .map_err(|e| JcError::Other(format!("Failed to execute unzip: {}", e)))?; + + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + return Err(JcError::DecompressionFailed { + tool: "unzip".to_string(), + stderr: stderr.to_string(), + }); + } + + // Find what was extracted (similar to TAR behavior) + use std::fs; + let entries: Vec<_> = fs::read_dir(working_dir) + .map_err(|e| JcError::Io(e))? + .filter_map(|e| e.ok()) + .filter(|e| e.path() != work_input) // Exclude the zip file itself + .collect(); + + // Remove the copied zip file from working directory + let _ = fs::remove_file(&work_input); + + // If we found exactly one entry, use that + if entries.len() == 1 { + let extracted_path = entries[0].path(); + debug!("Extracted to: {}", extracted_path.display()); + return Ok(extracted_path); + } + + // Check if there's a directory with the zip's base name + let zip_base_name = work_input + .file_stem() + .and_then(|s| s.to_str()) + .unwrap_or(""); + + for entry in &entries { + let path = entry.path(); + if path.is_dir() { + if let Some(dir_name) = path.file_name().and_then(|s| s.to_str()) { + if dir_name == zip_base_name { + debug!("Extracted to directory: {}", path.display()); + return Ok(path); + } + } + } + } + + // Multiple files extracted - return the working directory + if !entries.is_empty() { + debug!( + "Extracted {} files to: {}", + entries.len(), + working_dir.display() + ); + return Ok(working_dir.to_path_buf()); + } + + // Fallback: assume filename without .zip extension + let output_path = work_input.with_extension(""); + debug!("Extracted to (fallback): {}", output_path.display()); + Ok(output_path) + } +} diff --git a/src/core/types.rs b/src/core/types.rs index 937bf59..75095d9 100644 --- a/src/core/types.rs +++ b/src/core/types.rs @@ -7,6 +7,7 @@ pub enum CompressionFormat { Bzip2, Xz, Tar, + Zip, } impl CompressionFormat { @@ -18,6 +19,7 @@ impl CompressionFormat { CompressionFormat::Bzip2 => "bz2", CompressionFormat::Xz => "xz", CompressionFormat::Tar => "tar", + CompressionFormat::Zip => "zip", } } @@ -28,6 +30,7 @@ impl CompressionFormat { "bz2" => Some(CompressionFormat::Bzip2), "xz" => Some(CompressionFormat::Xz), "tar" => Some(CompressionFormat::Tar), + "zip" => Some(CompressionFormat::Zip), _ => None, } } @@ -39,6 +42,7 @@ impl CompressionFormat { CompressionFormat::Bzip2 => "bzip2", CompressionFormat::Xz => "xz", CompressionFormat::Tar => "tar", + CompressionFormat::Zip => "zip", } } @@ -49,6 +53,7 @@ impl CompressionFormat { "bzip2" => Some(CompressionFormat::Bzip2), "xz" => Some(CompressionFormat::Xz), "tar" => Some(CompressionFormat::Tar), + "zip" => Some(CompressionFormat::Zip), _ => None, } } diff --git a/src/operations/decompress.rs b/src/operations/decompress.rs index ce1c768..73d8dbb 100644 --- a/src/operations/decompress.rs +++ b/src/operations/decompress.rs @@ -3,7 +3,7 @@ use std::fs; use std::path::PathBuf; use crate::compressors::{ - detect_format, Bzip2Compressor, GzipCompressor, TarCompressor, XzCompressor, + detect_format, Bzip2Compressor, GzipCompressor, TarCompressor, XzCompressor, ZipCompressor, }; use crate::core::config::CompressionConfig; use crate::core::error::{JcError, JcResult}; @@ -35,6 +35,10 @@ fn decompress_in_working_dir( let compressor = TarCompressor::new(); compressor.decompress_in_dir(input, working_dir, config) } + CompressionFormat::Zip => { + let compressor = ZipCompressor::new(); + compressor.decompress_in_dir(input, working_dir, config) + } } }