aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/shirabe/src/package/archiver/zip_archiver.rs
blob: 471352fd71bd1055c05a06bf7f622b7c88bf3f9d (plain)
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
//! ref: composer/src/Composer/Package/Archiver/ZipArchiver.php

use crate::package::archiver::archivable_files_finder::ArchivableFilesFinder;
use crate::package::archiver::archiver_interface::ArchiverInterface;
use crate::util::filesystem::Filesystem;
use crate::util::platform::Platform;
use indexmap::IndexMap;
use shirabe_php_shim::{
    PhpMixed, RuntimeException, ZipArchive, class_exists, fileperms, method_exists, pack, realpath,
};

#[derive(Debug)]
pub struct ZipArchiver;

impl ZipArchiver {
    pub fn new() -> Self {
        Self
    }

    fn formats() -> IndexMap<String, bool> {
        let mut map = IndexMap::new();
        map.insert("zip".to_string(), true);
        map
    }

    fn compression_available(&self) -> bool {
        class_exists("ZipArchive")
    }
}

impl ArchiverInterface for ZipArchiver {
    fn archive(
        &self,
        sources: String,
        target: String,
        format: String,
        excludes: Vec<String>,
        ignore_filters: bool,
    ) -> anyhow::Result<String> {
        let fs = Filesystem::new(None);
        let sources_realpath = realpath(&sources);
        let sources = if let Some(p) = sources_realpath {
            p
        } else {
            sources
        };
        let sources = fs.normalize_path(&sources);

        let mut zip = ZipArchive::new();
        if zip.open(&target, ZipArchive::CREATE).is_ok() {
            let files = ArchivableFilesFinder::new(&sources, excludes, ignore_filters)?;
            for file in files {
                let filepath = file.get_pathname();
                let mut relative_path = file.get_relative_path_name();

                if Platform::is_windows() {
                    relative_path = shirabe_php_shim::strtr(&relative_path, "\\", "/");
                }

                if file.is_dir() {
                    zip.add_empty_dir(&relative_path);
                } else {
                    zip.add_file(&filepath, &relative_path);
                }

                // setExternalAttributesName() is only available with libzip 0.11.2 or above
                if method_exists(&PhpMixed::Null, "setExternalAttributesName") {
                    let perms = fileperms(&filepath);
                    zip.set_external_attributes_name(
                        &relative_path,
                        ZipArchive::OPSYS_UNIX,
                        perms << 16,
                    );
                }
            }
            if zip.close() {
                if !std::path::Path::new(&target).exists() {
                    // create minimal valid ZIP file (Empty Central Directory + End of Central Directory record)
                    let eocd = pack(
                        "VvvvvVVv",
                        &[
                            PhpMixed::Int(0x06054b50), // End of central directory signature
                            PhpMixed::Int(0),          // Number of this disk
                            PhpMixed::Int(0),          // Disk where central directory starts
                            PhpMixed::Int(0), // Number of central directory records on this disk
                            PhpMixed::Int(0), // Total number of central directory records
                            PhpMixed::Int(0), // Size of central directory (bytes)
                            PhpMixed::Int(0), // Offset of start of central directory
                            PhpMixed::Int(0), // Comment length
                        ],
                    );
                    std::fs::write(&target, &eocd)?;
                }

                return Ok(target);
            }
        }
        let message = format!(
            "Could not create archive '{}' from '{}': {}",
            target,
            sources,
            zip.get_status_string()
        );
        Err(RuntimeException { message, code: 0 }.into())
    }

    fn supports(&self, format: String, _source_type: Option<String>) -> bool {
        Self::formats().contains_key(&format) && self.compression_available()
    }
}