wolfpack/ipk/
repository.rs1use 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 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}