1mod error;
2mod file_tree;
3#[cfg(test)]
4mod tests;
5
6pub use error::DockerArchiveError;
7pub use file_tree::FileTree;
8use flate2::read::GzDecoder;
9use path_clean::clean;
10use serde::{Deserialize, Serialize};
11use std::cell::RefCell;
12use std::collections::HashMap;
13use std::fs::OpenOptions;
14use std::io::{BufReader, Read};
15use std::path::{Path, PathBuf};
16use std::process::{Command, Stdio};
17use std::rc::Rc;
18use tar::{Archive, EntryType};
19use which::which;
20
21pub type Result<T> = std::result::Result<T, DockerArchiveError>;
22
23#[derive(Default, Debug, Serialize)]
24pub struct ImageArchive {
25 pub manifest: Manifest,
26 pub config: Config,
27 pub layer_map: HashMap<String, Rc<RefCell<FileTree>>>,
29}
30
31#[derive(Default, Debug, Deserialize, Serialize)]
32pub struct Manifest {
33 #[serde(rename = "Config")]
34 pub config_path: String,
35 #[serde(rename = "RepoTags")]
36 pub repo_tags: Option<Vec<String>>,
37 #[serde(rename = "Layers")]
39 pub layer_tar_paths: Vec<String>,
40}
41
42#[derive(Default, Debug, Deserialize, Serialize)]
43pub struct Config {
44 pub history: Vec<HistoryEntry>,
45 pub rootfs: RootFS,
46}
47
48#[derive(Default, Debug, Deserialize, Serialize)]
49pub struct RootFS {
50 #[serde(rename = "type")]
51 pub typ: String,
52 pub diff_ids: Vec<String>,
53}
54
55#[derive(Default, Debug, Deserialize, Serialize)]
56pub struct HistoryEntry {
57 #[serde(default)]
58 pub id: String,
59 #[serde(default)]
60 pub size: u64,
61 #[serde(default)]
63 pub created: String,
64 #[serde(default)]
65 pub author: String,
66 #[serde(default)]
67 pub created_by: String,
68 #[serde(default)]
69 pub empty_layer: bool,
70}
71
72impl ImageArchive {
73 pub fn new_from_url(img_url: &str) -> Result<Self> {
74 let executable = which("docker").map_or_else(|_| which("podman"), |i| Ok(i))?;
75 let mut child = Command::new(executable)
76 .arg("image")
77 .arg("save")
78 .arg(img_url)
79 .stdout(Stdio::piped())
80 .spawn()?;
81 let stdout = child
82 .stdout
83 .as_mut()
84 .ok_or(DockerArchiveError::StdoutError)?;
85 Self::new_from_reader(BufReader::new(stdout))
86 }
87
88 pub fn new_from_file<P: AsRef<Path>>(tar_file: P) -> Result<Self> {
89 let file = OpenOptions::new().read(true).open(tar_file)?;
90 Self::new_from_reader(BufReader::new(file))
91 }
92
93 pub fn new_from_reader<R: Read>(obj: R) -> Result<Self> {
94 let mut img = Self::default();
95 let mut ar = Archive::new(obj);
96 let mut json_files: HashMap<String, Vec<u8>> = HashMap::new();
98 let mut layer_tar_links: HashMap<String, String> = HashMap::new();
100 for entry in ar.entries()? {
101 let mut e = entry?;
102 match e.header().entry_type() {
104 EntryType::Symlink | EntryType::Regular => {
105 if let Some(name) = e.path()?.to_str().map(|s| s.to_string()) {
106 if name.ends_with(".tar") {
107 if e.header().entry_type().eq(&EntryType::Symlink) {
108 if let Ok(Some(p)) = e.header().link_name() {
109 let tmp = PathBuf::from(&name);
110 if let Some(tmp_parent) = tmp.parent() {
111 layer_tar_links.insert(
112 name,
113 clean(&tmp_parent.join(p).to_string_lossy()),
114 );
115 } else {
116 layer_tar_links.insert(name, clean(&p.to_string_lossy()));
117 }
118 }
119 } else {
120 let layer_tree = FileTree::build_from_layer_tar(Archive::new(e))?;
121 layer_tree.borrow_mut().name = name.clone();
122 img.layer_map.insert(name, layer_tree);
123 }
124 } else if name.ends_with(".tar.gz") || name.ends_with("tgz") {
125 if e.header().entry_type().eq(&EntryType::Symlink) {
126 if let Ok(Some(p)) = e.header().link_name() {
127 let tmp = PathBuf::from(&name);
128 if let Some(tmp_parent) = tmp.parent() {
129 layer_tar_links.insert(
130 name,
131 clean(&tmp_parent.join(p).to_string_lossy()),
132 );
133 } else {
134 layer_tar_links.insert(name, clean(&p.to_string_lossy()));
135 }
136 }
137 } else {
138 let layer_tree = FileTree::build_from_layer_tar(Archive::new(
139 GzDecoder::new(e),
140 ))?;
141 layer_tree.borrow_mut().name = name.clone();
142 img.layer_map.insert(name, layer_tree);
143 }
144 } else if name.ends_with(".json") || name.starts_with("sha256:") {
145 let mut content = vec![];
146 e.read_to_end(&mut content)?;
147 json_files.insert(name, content);
148 }
149 }
150 }
151 _ => (),
152 }
153 }
154 for (k, v) in layer_tar_links.iter() {
155 if let Some(tree) = img.layer_map.get(v).map(Rc::clone) {
157 img.layer_map.insert(k.clone(), tree);
158 }
159 }
160 if let Some(data) = json_files.get("manifest.json") {
161 let manifests: Vec<Manifest> = serde_json::from_slice(data)?;
162 img.manifest =
163 manifests
164 .into_iter()
165 .nth(0)
166 .ok_or(DockerArchiveError::InvalidArchive(
167 "image manifest is empty",
168 ))?;
169 } else {
170 return Err(DockerArchiveError::InvalidArchive(
171 "could not find image manifest",
172 ));
173 }
174 if let Some(data) = json_files.get(&img.manifest.config_path) {
175 let mut config: Config = serde_json::from_slice(data)?;
176 let mut layer_idx = 0;
177 for h in config.history.iter_mut() {
178 if h.empty_layer {
179 h.id = "<missing>".to_string();
180 } else if let Some(s) = config.rootfs.diff_ids.get(layer_idx) {
181 h.id = s.clone();
182 layer_idx += 1;
183 }
184 }
185 img.config = config;
186 } else {
187 return Err(DockerArchiveError::InvalidArchive(
188 "could not find image config",
189 ));
190 }
191 Ok(img)
192 }
193
194 pub fn merged_tree(&self) -> Rc<RefCell<FileTree>> {
195 FileTree::stack_trees(
196 self.manifest
197 .layer_tar_paths
198 .iter()
199 .map(|path| self.layer_map.get(path))
200 .filter(|i| i.is_some())
201 .map(|i| Rc::clone(i.unwrap()))
202 .collect(),
203 )
204 }
205}