use std::path::Path;
use flate2::write::GzEncoder;
use flate2::Compression;
use crate::error::{ComposeError, Result};
fn append_context<W: std::io::Write>(
tar: &mut tar::Builder<W>,
context: &Path,
ignore_patterns: &[String],
skip_names: &[&str],
force_include: &[&str],
) -> Result<()> {
tar.follow_symlinks(false);
for abs in super::super::walk_dir(context).map_err(ComposeError::Io)? {
let rel = abs
.strip_prefix(context)
.map_err(|_| ComposeError::Build("path strip error".into()))?;
let rel_str = rel.to_string_lossy();
if skip_names.iter().any(|n| rel_str == *n) {
continue;
}
let forced = force_include.iter().any(|n| rel_str == *n);
if !forced && is_ignored(&rel_str, ignore_patterns) {
continue;
}
let is_dir = abs.symlink_metadata().map(|m| m.is_dir()).unwrap_or(false);
if is_dir {
tar.append_dir(rel, &abs)
.map_err(|e| ComposeError::Build(e.to_string()))?;
} else {
tar.append_path_with_name(&abs, rel)
.map_err(|e| ComposeError::Build(e.to_string()))?;
}
}
Ok(())
}
fn append_extra_files<W: std::io::Write>(
tar: &mut tar::Builder<W>,
extra_files: &[(String, Vec<u8>)],
) -> Result<()> {
for (name, bytes) in extra_files {
let mut header = tar::Header::new_gnu();
header.set_size(bytes.len() as u64);
header.set_mode(0o600);
header.set_cksum();
tar.append_data(&mut header, name, bytes.as_slice())
.map_err(|e| ComposeError::Build(e.to_string()))?;
}
Ok(())
}
pub(super) fn build_context_tar_with_inline(
context: &Path,
inline: &str,
extra_files: &[(String, Vec<u8>)],
) -> Result<(Vec<u8>, String)> {
let inline_name = ".dockerfile-inline";
let ignore_patterns = read_dockerignore(context);
let encoder = GzEncoder::new(Vec::new(), Compression::default());
let mut tar = tar::Builder::new(encoder);
let mut header = tar::Header::new_gnu();
header.set_size(inline.len() as u64);
header.set_mode(0o644);
header.set_cksum();
tar.append_data(&mut header, inline_name, inline.as_bytes())
.map_err(|e| ComposeError::Build(e.to_string()))?;
append_context(&mut tar, context, &ignore_patterns, &[".dockerignore"], &[])?;
let mut synthesized: Vec<&str> = vec![inline_name];
synthesized.extend(extra_files.iter().map(|(n, _)| n.as_str()));
append_dockerignore(&mut tar, &synthesized_dockerignore(context, &synthesized))?;
append_extra_files(&mut tar, extra_files)?;
let gz = tar
.into_inner()
.map_err(|e| ComposeError::Build(e.to_string()))?;
let bytes = gz
.finish()
.map_err(|e| ComposeError::Build(e.to_string()))?;
Ok((bytes, inline_name.to_string()))
}
pub(crate) fn build_context_tar(
context: &Path,
dockerfile: &str,
extra_files: &[(String, Vec<u8>)],
) -> Result<Vec<u8>> {
let ignore_patterns = read_dockerignore(context);
let encoder = GzEncoder::new(Vec::new(), Compression::default());
let mut tar = tar::Builder::new(encoder);
if extra_files.is_empty() {
append_context(&mut tar, context, &ignore_patterns, &[], &[dockerfile])?;
} else {
append_context(
&mut tar,
context,
&ignore_patterns,
&[".dockerignore"],
&[dockerfile],
)?;
let synthesized: Vec<&str> = extra_files.iter().map(|(n, _)| n.as_str()).collect();
append_dockerignore(&mut tar, &synthesized_dockerignore(context, &synthesized))?;
}
append_extra_files(&mut tar, extra_files)?;
let gz = tar
.into_inner()
.map_err(|e| ComposeError::Build(e.to_string()))?;
let bytes = gz
.finish()
.map_err(|e| ComposeError::Build(e.to_string()))?;
Ok(bytes)
}
fn append_dockerignore<W: std::io::Write>(tar: &mut tar::Builder<W>, content: &str) -> Result<()> {
let mut header = tar::Header::new_gnu();
header.set_size(content.len() as u64);
header.set_mode(0o644);
header.set_cksum();
tar.append_data(&mut header, ".dockerignore", content.as_bytes())
.map_err(|e| ComposeError::Build(e.to_string()))
}
fn synthesized_dockerignore(context: &Path, names: &[&str]) -> String {
let existing =
crate::filesystem::read_to_string_capped(context.join(".dockerignore")).unwrap_or_default();
let mut out = existing.trim_end_matches(['\n', '\r']).to_string();
for name in names {
if !out.is_empty() {
out.push('\n');
}
out.push_str(name);
}
out.push('\n');
out
}
pub(super) fn map_additional_context(base_dir: &Path, value: &str) -> String {
if let Some(img) = value.strip_prefix("docker-image://") {
format!("image:{img}")
} else if value.starts_with("http://")
|| value.starts_with("https://")
|| value.starts_with("git://")
{
format!("url:{value}")
} else {
format!("localpath:{}", base_dir.join(value).display())
}
}
fn read_dockerignore(context: &Path) -> Vec<String> {
let path = context.join(".dockerignore");
let Ok(content) = crate::filesystem::read_to_string_capped(path) else {
return Vec::new();
};
content
.lines()
.map(|l| l.trim().to_string())
.filter(|l| !l.is_empty() && !l.starts_with('#'))
.collect()
}
fn is_ignored(path: &str, patterns: &[String]) -> bool {
let mut ignored = false;
for pattern in patterns {
let (negated, pat) = match pattern.strip_prefix('!') {
Some(rest) => (true, rest),
None => (false, pattern.as_str()),
};
if pattern_matches(pat, path) {
ignored = !negated;
}
}
ignored
}
fn pattern_matches(pattern: &str, path: &str) -> bool {
if pattern.is_empty() {
return false;
}
if let Some(dir) = pattern.strip_suffix('/') {
return path == dir || path.starts_with(&format!("{dir}/"));
}
if pattern.contains('*') || pattern.contains('?') {
return glob_match(pattern, path);
}
path == pattern
|| (path.starts_with(pattern) && path.as_bytes().get(pattern.len()) == Some(&b'/'))
}
fn glob_match(pattern: &str, path: &str) -> bool {
if !pattern.contains('/') && !pattern.contains("**") {
let filename = path.rsplit('/').next().unwrap_or(path);
return glob_rec(pattern.as_bytes(), filename.as_bytes());
}
glob_rec(pattern.as_bytes(), path.as_bytes())
}
fn glob_rec(pat: &[u8], s: &[u8]) -> bool {
if pat.is_empty() {
return s.is_empty();
}
if pat.starts_with(b"**") {
let mut rest = &pat[2..];
if rest.first() == Some(&b'/') && glob_rec(&rest[1..], s) {
return true;
}
if rest.is_empty() {
rest = b"";
}
for i in 0..=s.len() {
if glob_rec(rest, &s[i..]) {
return true;
}
}
return false;
}
match pat[0] {
b'*' => {
let mut i = 0;
loop {
if glob_rec(&pat[1..], &s[i..]) {
return true;
}
if i >= s.len() || s[i] == b'/' {
return false;
}
i += 1;
}
}
b'?' => !s.is_empty() && s[0] != b'/' && glob_rec(&pat[1..], &s[1..]),
c => !s.is_empty() && s[0] == c && glob_rec(&pat[1..], &s[1..]),
}
}
#[cfg(test)]
mod tests;