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) const INLINE_DOCKERFILE_NAME: &str = ".dockerfile-inline";
pub(super) fn stream_build_context<W: std::io::Write>(
writer: W,
context: &Path,
dockerfile: &str,
extra_files: &[(String, Vec<u8>)],
) -> Result<()> {
let (ignore_name, ignore_patterns) = ignore_file(context);
let encoder = GzEncoder::new(writer, 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,
&[ignore_name],
&[dockerfile],
)?;
let synthesized: Vec<&str> = extra_files.iter().map(|(n, _)| n.as_str()).collect();
append_ignore_file(
&mut tar,
ignore_name,
&synthesized_ignore_file(context, ignore_name, &synthesized),
)?;
}
append_extra_files(&mut tar, extra_files)?;
finish_tar(tar)
}
pub(super) fn stream_build_context_with_inline<W: std::io::Write>(
writer: W,
context: &Path,
inline: &str,
extra_files: &[(String, Vec<u8>)],
) -> Result<()> {
let (ignore_name, ignore_patterns) = ignore_file(context);
let encoder = GzEncoder::new(writer, 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_DOCKERFILE_NAME, inline.as_bytes())
.map_err(|e| ComposeError::Build(e.to_string()))?;
append_context(&mut tar, context, &ignore_patterns, &[ignore_name], &[])?;
let mut synthesized: Vec<&str> = vec![INLINE_DOCKERFILE_NAME];
synthesized.extend(extra_files.iter().map(|(n, _)| n.as_str()));
append_ignore_file(
&mut tar,
ignore_name,
&synthesized_ignore_file(context, ignore_name, &synthesized),
)?;
append_extra_files(&mut tar, extra_files)?;
finish_tar(tar)
}
fn finish_tar<W: std::io::Write>(tar: tar::Builder<GzEncoder<W>>) -> Result<()> {
let gz = tar
.into_inner()
.map_err(|e| ComposeError::Build(e.to_string()))?;
let mut writer = gz
.finish()
.map_err(|e| ComposeError::Build(e.to_string()))?;
writer
.flush()
.map_err(|e| ComposeError::Build(e.to_string()))?;
Ok(())
}
#[cfg(test)]
pub(super) fn build_context_tar_with_inline(
context: &Path,
inline: &str,
extra_files: &[(String, Vec<u8>)],
) -> Result<(Vec<u8>, String)> {
let mut buf = Vec::new();
stream_build_context_with_inline(&mut buf, context, inline, extra_files)?;
Ok((buf, INLINE_DOCKERFILE_NAME.to_string()))
}
#[cfg(test)]
pub(crate) fn build_context_tar(
context: &Path,
dockerfile: &str,
extra_files: &[(String, Vec<u8>)],
) -> Result<Vec<u8>> {
let mut buf = Vec::new();
stream_build_context(&mut buf, context, dockerfile, extra_files)?;
Ok(buf)
}
fn append_ignore_file<W: std::io::Write>(
tar: &mut tar::Builder<W>,
name: &str,
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, name, content.as_bytes())
.map_err(|e| ComposeError::Build(e.to_string()))
}
fn synthesized_ignore_file(context: &Path, name: &str, names: &[&str]) -> String {
let existing = crate::filesystem::read_to_string_capped(context.join(name)).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 ignore_file(context: &Path) -> (&'static str, Vec<String>) {
for name in [".containerignore", ".dockerignore"] {
let Ok(content) = crate::filesystem::read_to_string_capped(context.join(name)) else {
continue;
};
let patterns = content
.lines()
.map(|l| l.trim().to_string())
.filter(|l| !l.is_empty() && !l.starts_with('#'))
.collect();
return (name, patterns);
}
(".containerignore", Vec::new())
}
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;