Skip to main content

wolfpack/ipk/
repository.rs

1use fs_err::create_dir_all;
2use fs_err::File;
3use std::collections::HashMap;
4use std::collections::HashSet;
5use std::ffi::OsStr;
6use std::fmt::Display;
7use std::fmt::Formatter;
8use std::io::ErrorKind;
9use std::io::Read;
10use std::io::Write;
11use std::path::Path;
12use std::path::PathBuf;
13
14use ksign::IO;
15use walkdir::WalkDir;
16
17use crate::hash::Sha256Hash;
18use crate::hash::Sha256Reader;
19use crate::ipk::Arch;
20use crate::ipk::Error;
21use crate::ipk::Package;
22use crate::ipk::PackageSigner;
23use crate::ipk::PackageVerifier;
24
25pub struct Repository {
26    packages: HashMap<Arch, PerArchPackages>,
27}
28
29impl Repository {
30    pub fn new<I, P, P2>(
31        output_dir: P2,
32        paths: I,
33        verifier: &PackageVerifier,
34    ) -> Result<Self, Error>
35    where
36        I: IntoIterator<Item = P>,
37        P: AsRef<Path>,
38        P2: AsRef<Path>,
39    {
40        let mut packages: HashMap<Arch, PerArchPackages> = HashMap::new();
41        let mut push_package = |path: &Path| -> Result<(), Error> {
42            let mut reader = Sha256Reader::new(File::open(path)?);
43            let control = Package::read_control(reader.by_ref(), path, verifier)?;
44            let (hash, size) = reader.digest()?;
45            let mut filename = PathBuf::new();
46            filename.push("data");
47            filename.push(hash.to_string());
48            create_dir_all(output_dir.as_ref().join(&filename))?;
49            filename.push(path.file_name().ok_or(ErrorKind::InvalidData)?);
50            let new_path = output_dir.as_ref().join(&filename);
51            fs_err::rename(path, new_path)?;
52            let file_name = path.file_name().expect("File name exists");
53            let file_name_sig = {
54                let mut name = file_name.to_os_string();
55                name.push(".sig");
56                name
57            };
58            fs_err::rename(
59                path.parent().expect("Parent exists").join(&file_name_sig),
60                output_dir.as_ref().join(&file_name_sig),
61            )?;
62            let control = ExtendedPackage {
63                control,
64                size,
65                hash,
66                filename,
67            };
68            packages
69                .entry(control.control.arch)
70                .or_insert_with(|| PerArchPackages {
71                    packages: Vec::new(),
72                })
73                .packages
74                .push(control);
75            Ok(())
76        };
77        for path in paths.into_iter() {
78            let path = path.as_ref();
79            if path.is_dir() {
80                for entry in WalkDir::new(path).into_iter() {
81                    let entry = entry?;
82                    if entry.file_type().is_dir()
83                        || entry.path().extension() != Some(OsStr::new("ipk"))
84                    {
85                        continue;
86                    }
87                    push_package(entry.path())?
88                }
89            } else {
90                push_package(path)?
91            }
92        }
93        Ok(Self { packages })
94    }
95
96    pub fn write<P: AsRef<Path>>(
97        &self,
98        output_dir: P,
99        signer: &PackageSigner,
100    ) -> Result<(), Error> {
101        let output_dir = output_dir.as_ref();
102        create_dir_all(output_dir)?;
103        let packages_string = self.to_string();
104        for (format, extension) in [(deko::Format::Verbatim, ""), (deko::Format::Gz, ".gz")] {
105            let filename = format!("Packages{}", extension);
106            let mut writer = deko::AnyEncoder::new(
107                File::create(output_dir.join(filename))?,
108                format,
109                deko::write::Compression::Best,
110            )?;
111            writer.write_all(packages_string.as_bytes())?;
112            writer.finish()?;
113        }
114        let signature = signer.sign(packages_string.as_bytes());
115        signature
116            .write_to_file(output_dir.join("Packages.sig"))
117            .map_err(|e| Error::other(e.to_string()))?;
118        Ok(())
119    }
120
121    pub fn iter(&self) -> impl Iterator<Item = (&Arch, &PerArchPackages)> {
122        self.packages.iter()
123    }
124
125    pub fn architectures(&self) -> HashSet<Arch> {
126        self.packages.keys().cloned().collect()
127    }
128}
129
130impl Display for Repository {
131    fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
132        for (_, per_arch_packages) in self.packages.iter() {
133            Display::fmt(per_arch_packages, f)?;
134        }
135        Ok(())
136    }
137}
138
139pub struct PerArchPackages {
140    packages: Vec<ExtendedPackage>,
141}
142
143impl Display for PerArchPackages {
144    fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
145        for control in self.packages.iter() {
146            writeln!(f, "{}", control)?;
147        }
148        Ok(())
149    }
150}
151
152pub struct ExtendedPackage {
153    pub control: Package,
154    hash: Sha256Hash,
155    filename: PathBuf,
156    size: u64,
157}
158
159impl Display for ExtendedPackage {
160    fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
161        write!(f, "{}", self.control)?;
162        writeln!(f, "Filename: {}", self.filename.display())?;
163        writeln!(f, "Size: {}", self.size)?;
164        writeln!(f, "SHA256sum: {}", self.hash)?;
165        Ok(())
166    }
167}
168
169#[cfg(test)]
170mod tests {
171
172    use fs_err::remove_dir_all;
173    use std::process::Command;
174
175    use arbtest::arbtest;
176    use command_error::CommandExt;
177    use tempfile::TempDir;
178
179    use super::*;
180    use crate::ipk::SigningKey;
181    use crate::test::prevent_concurrency;
182    use crate::test::DirectoryOfFiles;
183
184    #[ignore = "Needs `opkg`"]
185    #[test]
186    fn opkg_installs_from_repo() {
187        let _guard = prevent_concurrency("opkg");
188        let workdir = TempDir::new().unwrap();
189        let repo_dir = workdir.path().join("repo");
190        let signing_key = SigningKey::generate(Some("wolfpack".into()));
191        let verifying_key = signing_key.to_verifying_key();
192        // speed up opkg update
193        fs_err::remove_file("/etc/opkg/distfeeds.conf").unwrap();
194        arbtest(|u| {
195            let mut package: Package = u.arbitrary()?;
196            package.arch = "all".parse().unwrap();
197            package.depends.clear();
198            package.installed_size = Some(100);
199            let directory: DirectoryOfFiles = u.arbitrary()?;
200            let package_path = workdir.path().join("test.ipk");
201            package
202                .write(package_path.as_path(), directory.path(), &signing_key)
203                .unwrap();
204            let _ = remove_dir_all(&repo_dir);
205            Repository::new(&repo_dir, [&package_path], &verifying_key)
206                .unwrap()
207                .write(&repo_dir, &signing_key)
208                .unwrap();
209            Command::new("find")
210                .arg(workdir.path())
211                .status_checked()
212                .unwrap();
213            fs_err::write(
214                "/etc/opkg/test.conf",
215                format!("src/gz test file://{}\n", repo_dir.display()),
216            )
217            .unwrap();
218            verifying_key
219                .write_to_file(format!("/etc/opkg/keys/{}", verifying_key.fingerprint()))
220                .unwrap();
221            Command::new("cat")
222                .arg("/etc/opkg/test.conf")
223                .status_checked()
224                .unwrap();
225            Command::new("sh")
226                .arg("-c")
227                .arg("cat /etc/opkg/keys/*")
228                .status_checked()
229                .unwrap();
230            assert!(
231                Command::new("opkg")
232                    .arg("update")
233                    .arg(package_path.as_path())
234                    .status_checked()
235                    .unwrap()
236                    .success(),
237                "package:\n========{}========",
238                package
239            );
240            assert!(
241                Command::new("opkg")
242                    .arg("install")
243                    .arg(package.name.to_string())
244                    .status_checked()
245                    .unwrap()
246                    .success(),
247                "package:\n========{}========",
248                package
249            );
250            assert!(
251                Command::new("opkg")
252                    .arg("remove")
253                    .arg(package.name.to_string())
254                    .status_checked()
255                    .unwrap()
256                    .success(),
257                "package:\n========{}========",
258                package
259            );
260            Ok(())
261        });
262    }
263}