aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/shirabe/src/package/archiver/zip_archiver.rs
blob: 41201b564a19c769b6be4af10c94c665a024a0c3 (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
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
//! ref: composer/src/Composer/Package/Archiver/ZipArchiver.php

use crate::package::archiver::ArchivableFilesFinder;
use crate::package::archiver::ArchiverInterface;
use crate::util::Filesystem;
use crate::util::Platform;
use indexmap::IndexMap;
use shirabe_php_shim::{
    PhpMixed, RuntimeException, ZipArchive, class_exists, fileperms, method_exists, pack, realpath,
};
use std::path::PathBuf;

#[derive(Debug)]
pub struct ZipArchiver;

impl Default for ZipArchiver {
    fn default() -> Self {
        Self::new()
    }
}

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;
                let mut relative_path = filepath
                    .strip_prefix(&sources)
                    .unwrap_or(filepath.as_path())
                    .to_path_buf();

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

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

                // setExternalAttributesName() is only available with libzip 0.11.2 or above
                if method_exists(&PhpMixed::Null, "setExternalAttributesName") {
                    let perms = fileperms(&filepath.to_string_lossy());
                    zip.set_external_attributes_name(
                        &relative_path.to_string_lossy(),
                        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()
    }

    fn as_any(&self) -> &dyn std::any::Any {
        self
    }
}