nuts_tool/
archive.rs

1// MIT License
2//
3// Copyright (c) 2023,2024 Robin Doer
4//
5// Permission is hereby granted, free of charge, to any person obtaining a copy
6// of this software and associated documentation files (the "Software"), to
7// deal in the Software without restriction, including without limitation the
8// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
9// sell copies of the Software, and to permit persons to whom the Software is
10// furnished to do so, subject to the following conditions:
11//
12// The above copyright notice and this permission notice shall be included in
13// all copies or substantial portions of the Software.
14//
15// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
20// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
21// IN THE SOFTWARE.
22
23use anyhow::Result;
24use chrono::{DateTime, Utc};
25use log::{debug, error, trace, warn};
26use nuts_archive::{Archive, Group};
27use std::fs::{self, File, Metadata};
28use std::io::Read;
29use std::path::Path;
30
31#[cfg(unix)]
32use std::os::unix::fs::{MetadataExt, PermissionsExt};
33
34use crate::backend::PluginBackend;
35use crate::{say, say_err};
36
37#[cfg(unix)]
38mod unix {
39    pub const S_IRUSR: u32 = 0o0000400; // Read permission, owner.
40    pub const S_IWUSR: u32 = 0o0000200; // Write permission, owner.
41    pub const S_IXUSR: u32 = 0o0000100; // Execute/search permission, owner.
42    pub const S_IRGRP: u32 = 0o0000040; // Read permission, group.
43    pub const S_IWGRP: u32 = 0o0000020; // Write permission, group.
44    pub const S_IXGRP: u32 = 0o0000010; // Execute/search permission, group.
45    pub const S_IROTH: u32 = 0o0000004; // Read permission, others.
46    pub const S_IWOTH: u32 = 0o0000002; // Write permission, others.
47    pub const S_IXOTH: u32 = 0o0000001; // Execute/search permission, others.
48}
49
50macro_rules! into_utc {
51    ($metadata:ident . $type:ident ()) => {
52        if let Ok(time) = $metadata.$type() {
53            time.into()
54        } else {
55            warn!(
56                "the {} time is not available on your platform",
57                stringify!($type)
58            );
59            Utc::now()
60        }
61    };
62}
63
64fn changed(metadata: &Metadata) -> DateTime<Utc> {
65    if cfg!(unix) {
66        DateTime::from_timestamp(metadata.ctime(), 0).unwrap_or_else(|| {
67            warn!(
68                "could not convert epoch ctime {} into naive datetime",
69                metadata.ctime()
70            );
71
72            Utc::now()
73        })
74    } else {
75        panic!("platform currently not supported");
76    }
77}
78
79fn can_read(metadata: &Metadata, group: Group) -> bool {
80    if cfg!(unix) {
81        let mask = match group {
82            Group::User => unix::S_IRUSR,
83            Group::Group => unix::S_IRGRP,
84            Group::Other => unix::S_IROTH,
85        };
86
87        metadata.permissions().mode() & mask > 0
88    } else {
89        match group {
90            Group::User => true,
91            Group::Group => false,
92            Group::Other => false,
93        }
94    }
95}
96
97fn can_write(metadata: &Metadata, group: Group) -> bool {
98    if cfg!(unix) {
99        let mask = match group {
100            Group::User => unix::S_IWUSR,
101            Group::Group => unix::S_IWGRP,
102            Group::Other => unix::S_IWOTH,
103        };
104
105        metadata.permissions().mode() & mask > 0
106    } else {
107        match group {
108            Group::User => !metadata.permissions().readonly(),
109            Group::Group => false,
110            Group::Other => false,
111        }
112    }
113}
114
115fn can_execute(metadata: &Metadata, group: Group) -> bool {
116    if cfg!(unix) {
117        let mask = match group {
118            Group::User => unix::S_IXUSR,
119            Group::Group => unix::S_IXGRP,
120            Group::Other => unix::S_IXOTH,
121        };
122
123        metadata.permissions().mode() & mask > 0
124    } else {
125        false
126    }
127}
128
129pub fn append_recursive(archive: &mut Archive<PluginBackend>, path: &Path) -> Result<()> {
130    debug!("append {}", path.display());
131
132    let metadata = match fs::symlink_metadata(path) {
133        Ok(md) => md,
134        Err(err) => {
135            error!("{}", err);
136            say_err!("! {}", path.display());
137            return Ok(());
138        }
139    };
140
141    if metadata.is_file() {
142        let block_size = archive.as_ref().block_size() as usize;
143
144        let mut fh = File::open(path)?;
145        let mut buf = vec![0; block_size];
146
147        let mut builder = archive.append_file(path.to_string_lossy());
148
149        builder.set_created(into_utc!(metadata.created()));
150        builder.set_changed(changed(&metadata));
151        builder.set_modified(into_utc!(metadata.modified()));
152
153        for group in [Group::User, Group::Group, Group::Other] {
154            builder.set_readable(group, can_read(&metadata, group));
155            builder.set_writable(group, can_write(&metadata, group));
156            builder.set_executable(group, can_execute(&metadata, group));
157        }
158
159        let mut entry = builder.build()?;
160
161        loop {
162            let n = fh.read(&mut buf)?;
163            trace!("{} bytes read from {}", n, path.display());
164
165            if n > 0 {
166                entry.write_all(&buf[..n])?;
167            } else {
168                break;
169            }
170        }
171    } else if metadata.is_dir() {
172        let mut builder = archive.append_directory(path.to_string_lossy());
173
174        builder.set_created(into_utc!(metadata.created()));
175        builder.set_changed(changed(&metadata));
176        builder.set_modified(into_utc!(metadata.modified()));
177
178        for group in [Group::User, Group::Group, Group::Other] {
179            builder.set_readable(group, can_read(&metadata, group));
180            builder.set_writable(group, can_write(&metadata, group));
181            builder.set_executable(group, can_execute(&metadata, group));
182        }
183
184        builder.build()?;
185    } else if metadata.is_symlink() {
186        let target = path.read_link()?;
187
188        let mut builder = archive.append_symlink(path.to_string_lossy(), target.to_string_lossy());
189
190        builder.set_created(into_utc!(metadata.created()));
191        builder.set_changed(changed(&metadata));
192        builder.set_modified(into_utc!(metadata.modified()));
193
194        for group in [Group::User, Group::Group, Group::Other] {
195            builder.set_readable(group, can_read(&metadata, group));
196            builder.set_writable(group, can_write(&metadata, group));
197            builder.set_executable(group, can_execute(&metadata, group));
198        }
199
200        builder.build()?;
201    }
202
203    say!("a {}", path.display());
204
205    if path.is_dir() {
206        for entry in path.read_dir()? {
207            let child = entry?.path();
208
209            append_recursive(archive, &child)?;
210        }
211    }
212
213    Ok(())
214}