rsword_chirho 0.3.0

Core SWORD module library in pure Rust
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
// For God so loved the world that he gave his only begotten Son,
// that whoever believes in him should not perish but have eternal life.
// John 3:16

//! SWORD module installation manager.
//!
//! This module is only available on native platforms (requires filesystem
//! and network access for downloading and extracting module archives).

#![cfg(feature = "native")]

use std::fs::File;
use std::io::{BufReader, Read};
use std::path::{Path, PathBuf};

use flate2::read::GzDecoder;
use tar::Archive;

use crate::config_chirho::{InstallSourceChirho, InstallSourcesConfigChirho};
use crate::error_chirho::{ErrorChirho, ResultChirho};
use crate::transport_chirho::{SystemTransportChirho, TransportChirho};

/// SWORD module installation manager.
///
/// Handles downloading and installing modules from remote sources.
pub struct InstallMgrChirho {
    /// Configuration directory.
    config_dir_chirho: PathBuf,
    /// Installation sources.
    sources_chirho: InstallSourcesConfigChirho,
    /// Target installation path.
    install_path_chirho: PathBuf,
}

impl InstallMgrChirho {
    /// Create a new install manager.
    pub fn new_chirho<P: AsRef<Path>>(config_dir_chirho: P) -> ResultChirho<Self> {
        let config_dir_chirho = config_dir_chirho.as_ref().to_path_buf();

        // Try to load existing sources
        let sources_path_chirho = config_dir_chirho.join("InstallMgr.conf");
        let sources_chirho = if sources_path_chirho.exists() {
            InstallSourcesConfigChirho::from_file_chirho(&sources_path_chirho)?
        } else {
            InstallSourcesConfigChirho::default()
        };

        // Default install path
        let install_path_chirho = config_dir_chirho.clone();

        Ok(Self {
            config_dir_chirho,
            sources_chirho,
            install_path_chirho,
        })
    }

    /// Initialize the install manager configuration.
    pub fn init_chirho(&mut self) -> ResultChirho<()> {
        // Create config directory
        std::fs::create_dir_all(&self.config_dir_chirho)?;

        // Add default CrossWire source if no sources exist
        if self.sources_chirho.all_sources_chirho().is_empty() {
            self.sources_chirho.add_source_chirho(crate::config_chirho::default_crosswire_source_chirho());
        }

        // Save sources
        self.save_sources_chirho()?;

        // Create modules directory
        let mods_dir_chirho = self.install_path_chirho.join("mods.d");
        std::fs::create_dir_all(&mods_dir_chirho)?;

        Ok(())
    }

    /// Save the sources configuration.
    pub fn save_sources_chirho(&self) -> ResultChirho<()> {
        let sources_path_chirho = self.config_dir_chirho.join("InstallMgr.conf");
        let mut file_chirho = std::fs::File::create(sources_path_chirho)?;
        self.sources_chirho.write_to_chirho(&mut file_chirho)?;
        Ok(())
    }

    /// Get all install sources.
    pub fn get_sources_chirho(&self) -> Vec<&InstallSourceChirho> {
        self.sources_chirho.all_sources_chirho()
    }

    /// Add an install source.
    pub fn add_source_chirho(&mut self, source_chirho: InstallSourceChirho) {
        self.sources_chirho.add_source_chirho(source_chirho);
    }

    /// Remove an install source by caption.
    /// Returns true if a source was removed, false if not found.
    pub fn remove_source_chirho(&mut self, caption_chirho: &str) -> bool {
        self.sources_chirho.remove_source_chirho(caption_chirho)
    }

    /// Find a source by name.
    pub fn find_source_chirho(&self, name_chirho: &str) -> Option<&InstallSourceChirho> {
        self.sources_chirho.find_by_caption_chirho(name_chirho)
    }

    /// Set the installation path.
    pub fn set_install_path_chirho<P: AsRef<Path>>(&mut self, path_chirho: P) {
        self.install_path_chirho = path_chirho.as_ref().to_path_buf();
    }

    /// Get the installation path.
    pub fn get_install_path_chirho(&self) -> &Path {
        &self.install_path_chirho
    }

    /// Sync with remote catalog (download module list).
    pub fn refresh_source_chirho(&mut self, source_name_chirho: &str) -> ResultChirho<Vec<String>> {
        let source_chirho = self.find_source_chirho(source_name_chirho)
            .ok_or_else(|| ErrorChirho::generic_chirho(format!("Source not found: {}", source_name_chirho)))?;

        let url_chirho = format!("{}/mods.d.tar.gz", source_chirho.url_chirho());

        // Download and extract the module list
        let transport_chirho = SystemTransportChirho::new_chirho();
        let mods_d_tar_chirho = self.config_dir_chirho.join("mods.d.tar.gz");

        transport_chirho.download_chirho(&url_chirho, &mods_d_tar_chirho)?;

        // Extract the tarball to get module configs using native Rust
        let cache_dir_chirho = self.config_dir_chirho.join("remote_mods.d").join(source_name_chirho);
        std::fs::create_dir_all(&cache_dir_chirho)?;

        extract_tar_gz_chirho(&mods_d_tar_chirho, &cache_dir_chirho)?;

        // Clean up tarball
        let _ = std::fs::remove_file(&mods_d_tar_chirho);

        // List modules from cache
        self.list_remote_modules_chirho(source_name_chirho)
    }

    /// List available modules from a source.
    pub fn list_remote_modules_chirho(&self, source_name_chirho: &str) -> ResultChirho<Vec<String>> {
        let cache_dir_chirho = self.config_dir_chirho.join("remote_mods.d").join(source_name_chirho);

        // Try mods.d subdirectory first (common extraction pattern)
        let mods_d_chirho = cache_dir_chirho.join("mods.d");
        let search_dir_chirho = if mods_d_chirho.exists() { mods_d_chirho } else { cache_dir_chirho };

        if !search_dir_chirho.exists() {
            return Ok(Vec::new());
        }

        let mut modules_chirho = Vec::new();

        for entry_chirho in std::fs::read_dir(&search_dir_chirho)? {
            let entry_chirho = entry_chirho?;
            let path_chirho = entry_chirho.path();

            if path_chirho.extension().map(|e| e == "conf").unwrap_or(false) {
                if let Some(stem_chirho) = path_chirho.file_stem() {
                    modules_chirho.push(stem_chirho.to_string_lossy().to_uppercase());
                }
            }
        }

        modules_chirho.sort();
        Ok(modules_chirho)
    }

    /// Install a module from a source.
    pub fn install_module_chirho(
        &self,
        source_name_chirho: &str,
        module_name_chirho: &str,
    ) -> ResultChirho<()> {
        let source_chirho = self.find_source_chirho(source_name_chirho)
            .ok_or_else(|| ErrorChirho::generic_chirho(format!("Source not found: {}", source_name_chirho)))?;

        // First check if the module exists in the remote module list
        let remote_modules_chirho = self.list_remote_modules_chirho(source_name_chirho)?;
        let module_exists_chirho = remote_modules_chirho.iter()
            .any(|m_chirho| m_chirho.eq_ignore_ascii_case(module_name_chirho));

        if !module_exists_chirho {
            return Err(ErrorChirho::generic_chirho(format!(
                "Module '{}' not found in {} repository.\n\
                 This may be a licensed/commercial module not available for public download.\n\
                 Some Bible translations (ESV, NIV, NASB, etc.) require purchasing from \
                 the copyright holder.\n\
                 Use 'refresh' to update the module list, or 'list' to see available modules.",
                module_name_chirho, source_name_chirho
            )));
        }

        // Build the download URL
        // CrossWire mirrors use: https://crosswire.org/ftpmirror/pub/sword/packages/rawzip/ModuleName.zip
        // The source URL is https://crosswire.org/ftpmirror/pub/sword/raw
        // So we need to go up to /pub/sword/ and then to /packages/rawzip/
        let base_url_chirho = source_chirho.url_chirho();
        let zip_url_chirho = if base_url_chirho.contains("/raw") {
            // Standard CrossWire layout
            format!("{}/packages/rawzip/{}.zip",
                base_url_chirho.replace("/raw", ""), module_name_chirho)
        } else {
            format!("{}/packages/rawzip/{}.zip", base_url_chirho, module_name_chirho)
        };

        let transport_chirho = SystemTransportChirho::new_chirho();

        // Download the module zip
        let temp_zip_chirho = self.config_dir_chirho.join(format!("{}.zip", module_name_chirho));
        eprintln!("Downloading from {}...", zip_url_chirho);
        transport_chirho.download_chirho(&zip_url_chirho, &temp_zip_chirho)?;

        // Extract the zip file using native Rust
        eprintln!("Extracting...");
        if let Err(e_chirho) = extract_zip_chirho(&temp_zip_chirho, &self.install_path_chirho) {
            // Clean up temp file on error
            let _ = std::fs::remove_file(&temp_zip_chirho);
            return Err(e_chirho);
        }

        // Clean up temp zip
        let _ = std::fs::remove_file(&temp_zip_chirho);

        eprintln!("Module {} installed successfully.", module_name_chirho);
        Ok(())
    }

    /// Uninstall a module.
    pub fn uninstall_module_chirho(&self, module_name_chirho: &str) -> ResultChirho<()> {
        let conf_path_chirho = self.install_path_chirho
            .join("mods.d")
            .join(format!("{}.conf", module_name_chirho.to_lowercase()));

        if !conf_path_chirho.exists() {
            return Err(ErrorChirho::module_not_found_chirho(module_name_chirho));
        }

        // Read config to find data path
        let config_chirho = crate::config_chirho::ModuleConfigChirho::from_file_chirho(&conf_path_chirho)?;

        // Remove data directory
        if let Some(data_path_chirho) = config_chirho.data_path_chirho() {
            let full_data_path_chirho = self.install_path_chirho.join(data_path_chirho);
            if full_data_path_chirho.exists() {
                std::fs::remove_dir_all(&full_data_path_chirho)?;
            }
        }

        // Remove config file
        std::fs::remove_file(&conf_path_chirho)?;

        Ok(())
    }

    /// List installed modules.
    pub fn list_installed_chirho(&self) -> ResultChirho<Vec<String>> {
        let mods_d_chirho = self.install_path_chirho.join("mods.d");

        if !mods_d_chirho.exists() {
            return Ok(Vec::new());
        }

        let mut modules_chirho = Vec::new();

        for entry_chirho in std::fs::read_dir(&mods_d_chirho)? {
            let entry_chirho = entry_chirho?;
            let path_chirho = entry_chirho.path();

            if path_chirho.extension().map(|e| e == "conf").unwrap_or(false) {
                if let Ok(config_chirho) = crate::config_chirho::ModuleConfigChirho::from_file_chirho(&path_chirho) {
                    modules_chirho.push(config_chirho.name_chirho);
                }
            }
        }

        Ok(modules_chirho)
    }

    /// Get module info including version from a source.
    pub fn get_remote_module_info_chirho(
        &self,
        source_name_chirho: &str,
        module_name_chirho: &str,
    ) -> ResultChirho<ModuleInfoChirho> {
        let cache_dir_chirho = self.config_dir_chirho.join("remote_mods.d").join(source_name_chirho);
        let mods_d_chirho = cache_dir_chirho.join("mods.d");
        let search_dir_chirho = if mods_d_chirho.exists() { mods_d_chirho } else { cache_dir_chirho };

        let conf_path_chirho = search_dir_chirho.join(format!("{}.conf", module_name_chirho.to_lowercase()));

        if !conf_path_chirho.exists() {
            return Err(ErrorChirho::module_not_found_chirho(module_name_chirho));
        }

        let config_chirho = crate::config_chirho::ModuleConfigChirho::from_file_chirho(&conf_path_chirho)?;

        Ok(ModuleInfoChirho {
            name_chirho: config_chirho.name_chirho.clone(),
            version_chirho: config_chirho.get_chirho("Version").unwrap_or("1.0").to_string(),
            description_chirho: config_chirho.get_chirho("Description").unwrap_or("").to_string(),
            language_chirho: config_chirho.get_chirho("Lang").unwrap_or("en").to_string(),
            module_type_chirho: config_chirho.get_chirho("ModDrv").unwrap_or("Unknown").to_string(),
        })
    }

    /// Get installed module info.
    pub fn get_installed_module_info_chirho(&self, module_name_chirho: &str) -> ResultChirho<ModuleInfoChirho> {
        let conf_path_chirho = self.install_path_chirho
            .join("mods.d")
            .join(format!("{}.conf", module_name_chirho.to_lowercase()));

        if !conf_path_chirho.exists() {
            return Err(ErrorChirho::module_not_found_chirho(module_name_chirho));
        }

        let config_chirho = crate::config_chirho::ModuleConfigChirho::from_file_chirho(&conf_path_chirho)?;

        Ok(ModuleInfoChirho {
            name_chirho: config_chirho.name_chirho.clone(),
            version_chirho: config_chirho.get_chirho("Version").unwrap_or("1.0").to_string(),
            description_chirho: config_chirho.get_chirho("Description").unwrap_or("").to_string(),
            language_chirho: config_chirho.get_chirho("Lang").unwrap_or("en").to_string(),
            module_type_chirho: config_chirho.get_chirho("ModDrv").unwrap_or("Unknown").to_string(),
        })
    }

    /// Compare versions and find modules needing upgrade.
    pub fn check_updates_chirho(&self, source_name_chirho: &str) -> ResultChirho<Vec<ModuleUpdateChirho>> {
        let installed_chirho = self.list_installed_chirho()?;
        let remote_chirho = self.list_remote_modules_chirho(source_name_chirho)?;

        let mut updates_chirho = Vec::new();

        for mod_name_chirho in &installed_chirho {
            if remote_chirho.iter().any(|r_chirho| r_chirho.eq_ignore_ascii_case(mod_name_chirho)) {
                let local_info_chirho = self.get_installed_module_info_chirho(mod_name_chirho)?;
                let remote_info_chirho = self.get_remote_module_info_chirho(source_name_chirho, mod_name_chirho)?;

                if compare_versions_chirho(&local_info_chirho.version_chirho, &remote_info_chirho.version_chirho) < 0 {
                    updates_chirho.push(ModuleUpdateChirho {
                        name_chirho: mod_name_chirho.clone(),
                        current_version_chirho: local_info_chirho.version_chirho,
                        available_version_chirho: remote_info_chirho.version_chirho,
                    });
                }
            }
        }

        Ok(updates_chirho)
    }

    /// Upgrade all modules that have updates available.
    pub fn upgrade_all_chirho(&self, source_name_chirho: &str) -> ResultChirho<Vec<String>> {
        let updates_chirho = self.check_updates_chirho(source_name_chirho)?;
        let mut upgraded_chirho = Vec::new();

        for update_chirho in &updates_chirho {
            eprintln!("Upgrading {} from {} to {}...",
                update_chirho.name_chirho,
                update_chirho.current_version_chirho,
                update_chirho.available_version_chirho
            );
            self.install_module_chirho(source_name_chirho, &update_chirho.name_chirho)?;
            upgraded_chirho.push(update_chirho.name_chirho.clone());
        }

        Ok(upgraded_chirho)
    }

    /// Search for modules by name/description across all cached sources.
    pub fn search_modules_chirho(&self, query_chirho: &str) -> ResultChirho<Vec<SearchResultChirho>> {
        let query_lower_chirho = query_chirho.to_lowercase();
        let mut results_chirho = Vec::new();

        for source_chirho in self.get_sources_chirho() {
            let source_name_chirho = &source_chirho.caption_chirho;
            let modules_chirho = self.list_remote_modules_chirho(source_name_chirho).unwrap_or_default();

            for mod_name_chirho in modules_chirho {
                if mod_name_chirho.to_lowercase().contains(&query_lower_chirho) {
                    results_chirho.push(SearchResultChirho {
                        source_name_chirho: source_name_chirho.clone(),
                        module_name_chirho: mod_name_chirho,
                    });
                } else if let Ok(info_chirho) = self.get_remote_module_info_chirho(source_name_chirho, &mod_name_chirho) {
                    if info_chirho.description_chirho.to_lowercase().contains(&query_lower_chirho) {
                        results_chirho.push(SearchResultChirho {
                            source_name_chirho: source_name_chirho.clone(),
                            module_name_chirho: mod_name_chirho,
                        });
                    }
                }
            }
        }

        Ok(results_chirho)
    }

    /// Check if a module is available in a source.
    ///
    /// This checks the cached module list. Call `refresh_source_chirho` first
    /// to ensure the list is up to date.
    pub fn is_module_available_chirho(&self, source_name_chirho: &str, module_name_chirho: &str) -> bool {
        self.list_remote_modules_chirho(source_name_chirho)
            .map(|modules_chirho| modules_chirho.iter()
                .any(|m_chirho| m_chirho.eq_ignore_ascii_case(module_name_chirho)))
            .unwrap_or(false)
    }

    /// Install a module from a URL (for licensed modules or custom sources).
    ///
    /// Use this when installing modules that aren't in the standard repositories,
    /// such as licensed translations that must be obtained separately.
    pub fn install_from_url_chirho(&self, url_chirho: &str) -> ResultChirho<()> {
        let transport_chirho = SystemTransportChirho::new_chirho();

        // Extract filename from URL
        let filename_chirho = url_chirho
            .rsplit('/')
            .next()
            .unwrap_or("module.zip");

        let temp_zip_chirho = self.config_dir_chirho.join(filename_chirho);
        eprintln!("Downloading from {}...", url_chirho);
        transport_chirho.download_chirho(url_chirho, &temp_zip_chirho)?;

        // Extract the zip file
        eprintln!("Extracting...");
        if let Err(e_chirho) = extract_zip_chirho(&temp_zip_chirho, &self.install_path_chirho) {
            let _ = std::fs::remove_file(&temp_zip_chirho);
            return Err(e_chirho);
        }

        // Clean up
        let _ = std::fs::remove_file(&temp_zip_chirho);

        eprintln!("Module installed successfully from URL.");
        Ok(())
    }

    /// Refresh all sources.
    pub fn refresh_all_sources_chirho(&mut self) -> ResultChirho<()> {
        let sources_chirho: Vec<String> = self.get_sources_chirho()
            .iter()
            .map(|s_chirho| s_chirho.caption_chirho.clone())
            .collect();

        for source_name_chirho in sources_chirho {
            eprintln!("Refreshing {}...", source_name_chirho);
            let _ = self.refresh_source_chirho(&source_name_chirho);
        }

        Ok(())
    }
}

/// Module information.
#[derive(Debug, Clone)]
pub struct ModuleInfoChirho {
    /// Module name.
    pub name_chirho: String,
    /// Module version.
    pub version_chirho: String,
    /// Module description.
    pub description_chirho: String,
    /// Module language.
    pub language_chirho: String,
    /// Module driver type.
    pub module_type_chirho: String,
}

/// Module update information.
#[derive(Debug, Clone)]
pub struct ModuleUpdateChirho {
    /// Module name.
    pub name_chirho: String,
    /// Currently installed version.
    pub current_version_chirho: String,
    /// Available version.
    pub available_version_chirho: String,
}

/// Search result.
#[derive(Debug, Clone)]
pub struct SearchResultChirho {
    /// Source name where module was found.
    pub source_name_chirho: String,
    /// Module name.
    pub module_name_chirho: String,
}

/// Compare version strings (e.g., "1.0" vs "1.1").
/// Returns -1 if v1 < v2, 0 if equal, 1 if v1 > v2.
fn compare_versions_chirho(v1_chirho: &str, v2_chirho: &str) -> i32 {
    let parts1_chirho: Vec<u32> = v1_chirho.split('.')
        .filter_map(|p_chirho| p_chirho.parse().ok())
        .collect();
    let parts2_chirho: Vec<u32> = v2_chirho.split('.')
        .filter_map(|p_chirho| p_chirho.parse().ok())
        .collect();

    let max_len_chirho = parts1_chirho.len().max(parts2_chirho.len());

    for i_chirho in 0..max_len_chirho {
        let p1_chirho = parts1_chirho.get(i_chirho).copied().unwrap_or(0);
        let p2_chirho = parts2_chirho.get(i_chirho).copied().unwrap_or(0);

        if p1_chirho < p2_chirho {
            return -1;
        } else if p1_chirho > p2_chirho {
            return 1;
        }
    }

    0
}

/// Extract a .tar.gz archive to a destination directory.
fn extract_tar_gz_chirho(archive_path_chirho: &Path, dest_dir_chirho: &Path) -> ResultChirho<()> {
    let file_chirho = File::open(archive_path_chirho)?;
    let buf_reader_chirho = BufReader::new(file_chirho);
    let gz_decoder_chirho = GzDecoder::new(buf_reader_chirho);
    let mut archive_chirho = Archive::new(gz_decoder_chirho);

    archive_chirho.unpack(dest_dir_chirho)
        .map_err(|e_chirho| ErrorChirho::generic_chirho(format!("Failed to extract tar.gz: {}", e_chirho)))?;

    Ok(())
}

/// Extract a .zip archive to a destination directory.
fn extract_zip_chirho(archive_path_chirho: &Path, dest_dir_chirho: &Path) -> ResultChirho<()> {
    use std::io::Write;

    let file_chirho = File::open(archive_path_chirho)?;
    let buf_reader_chirho = BufReader::new(file_chirho);
    let mut archive_chirho = zip::ZipArchive::new(buf_reader_chirho)
        .map_err(|e_chirho| ErrorChirho::generic_chirho(format!("Failed to open zip: {}", e_chirho)))?;

    for i_chirho in 0..archive_chirho.len() {
        let mut file_chirho = archive_chirho.by_index(i_chirho)
            .map_err(|e_chirho| ErrorChirho::generic_chirho(format!("Failed to read zip entry: {}", e_chirho)))?;

        let outpath_chirho = match file_chirho.enclosed_name() {
            Some(path_chirho) => dest_dir_chirho.join(path_chirho),
            None => continue, // Skip entries with invalid paths
        };

        if file_chirho.is_dir() {
            std::fs::create_dir_all(&outpath_chirho)?;
        } else {
            // Create parent directories if needed
            if let Some(parent_chirho) = outpath_chirho.parent() {
                if !parent_chirho.exists() {
                    std::fs::create_dir_all(parent_chirho)?;
                }
            }

            let mut outfile_chirho = File::create(&outpath_chirho)?;
            let mut buffer_chirho = Vec::new();
            file_chirho.read_to_end(&mut buffer_chirho)?;
            outfile_chirho.write_all(&buffer_chirho)?;
        }

        // Set permissions on Unix
        #[cfg(unix)]
        {
            use std::os::unix::fs::PermissionsExt;
            if let Some(mode_chirho) = file_chirho.unix_mode() {
                std::fs::set_permissions(&outpath_chirho, std::fs::Permissions::from_mode(mode_chirho))?;
            }
        }
    }

    Ok(())
}

#[cfg(test)]
mod tests_chirho {
    use super::*;
    use tempfile::TempDir;

    #[test]
    fn test_new_install_mgr_chirho() {
        let temp_dir_chirho = TempDir::new().unwrap();
        let mgr_chirho = InstallMgrChirho::new_chirho(temp_dir_chirho.path()).unwrap();
        assert!(mgr_chirho.get_sources_chirho().is_empty() || !mgr_chirho.get_sources_chirho().is_empty());
    }

    #[test]
    fn test_init_chirho() {
        let temp_dir_chirho = TempDir::new().unwrap();
        let mut mgr_chirho = InstallMgrChirho::new_chirho(temp_dir_chirho.path()).unwrap();
        mgr_chirho.init_chirho().unwrap();

        // Check that config was saved
        let conf_path_chirho = temp_dir_chirho.path().join("InstallMgr.conf");
        assert!(conf_path_chirho.exists());
    }
}