use std::fs;
use std::os::unix::fs::MetadataExt;
use std::path::Path;
use nix::unistd::{chown, Gid, Uid};
use crate::error::{Error, IoResultExt, Result};
use crate::namespace::{
current_gid_map, current_uid_map, inside_to_outside, mappings_equal, outside_to_inside,
NsConfig,
};
use crate::Repo;
#[derive(Debug, Clone, Default)]
pub struct MapOptions {
pub force: bool,
pub dry_run: bool,
}
#[derive(Debug, Clone, Default)]
pub struct MapStats {
pub remapped: u64,
pub skipped_unmapped_source: u64,
pub skipped_unmapped_target: u64,
pub total: u64,
}
pub fn map(repo: &mut Repo, options: &MapOptions) -> Result<MapStats> {
let source_ns = repo.config().namespace.clone();
let current_ns = NsConfig {
uid_map: current_uid_map()?,
gid_map: current_gid_map()?,
};
if mappings_equal(&source_ns, ¤t_ns) {
return Ok(MapStats::default());
}
let _lock = repo.lock()?;
let stats = remap_blobs(repo.blobs_path(), &source_ns, ¤t_ns, options)?;
if !options.dry_run && stats.remapped > 0 {
repo.config_mut().namespace = current_ns;
repo.save_config()?;
let config_file = fs::File::open(repo.config_path()).with_path(repo.config_path())?;
config_file.sync_all().with_path(repo.config_path())?;
}
Ok(stats)
}
fn remap_blobs(
blobs_path: impl AsRef<Path>,
source_ns: &NsConfig,
current_ns: &NsConfig,
options: &MapOptions,
) -> Result<MapStats> {
let blobs_path = blobs_path.as_ref();
let mut stats = MapStats::default();
for prefix_entry in fs::read_dir(blobs_path).with_path(blobs_path)? {
let prefix_entry = prefix_entry.with_path(blobs_path)?;
let prefix_path = prefix_entry.path();
if !prefix_path.is_dir() {
continue;
}
for blob_entry in fs::read_dir(&prefix_path).with_path(&prefix_path)? {
let blob_entry = blob_entry.with_path(&prefix_path)?;
let blob_path = blob_entry.path();
if !blob_path.is_file() {
continue;
}
stats.total += 1;
match remap_single_blob(&blob_path, source_ns, current_ns, options)? {
RemapResult::Remapped => stats.remapped += 1,
RemapResult::NoChange => {}
RemapResult::SkippedUnmappedSource => stats.skipped_unmapped_source += 1,
RemapResult::SkippedUnmappedTarget => stats.skipped_unmapped_target += 1,
}
}
}
Ok(stats)
}
enum RemapResult {
Remapped,
NoChange,
SkippedUnmappedSource,
SkippedUnmappedTarget,
}
fn remap_single_blob(
path: &Path,
source_ns: &NsConfig,
current_ns: &NsConfig,
options: &MapOptions,
) -> Result<RemapResult> {
let meta = fs::metadata(path).with_path(path)?;
let old_outside_uid = meta.uid();
let old_outside_gid = meta.gid();
let old_inside_uid = match outside_to_inside(old_outside_uid, &source_ns.uid_map) {
Some(uid) => uid,
None => {
return Ok(RemapResult::SkippedUnmappedSource);
}
};
let old_inside_gid = match outside_to_inside(old_outside_gid, &source_ns.gid_map) {
Some(gid) => gid,
None => {
return Ok(RemapResult::SkippedUnmappedSource);
}
};
let new_outside_uid = match inside_to_outside(old_inside_uid, ¤t_ns.uid_map) {
Some(uid) => uid,
None => {
if options.force {
return Ok(RemapResult::SkippedUnmappedTarget);
}
return Err(Error::UnmappedUid(old_inside_uid));
}
};
let new_outside_gid = match inside_to_outside(old_inside_gid, ¤t_ns.gid_map) {
Some(gid) => gid,
None => {
if options.force {
return Ok(RemapResult::SkippedUnmappedTarget);
}
return Err(Error::UnmappedGid(old_inside_gid));
}
};
if old_outside_uid == new_outside_uid && old_outside_gid == new_outside_gid {
return Ok(RemapResult::NoChange);
}
if !options.dry_run {
chown(
path,
Some(Uid::from_raw(new_outside_uid)),
Some(Gid::from_raw(new_outside_gid)),
)
.map_err(|e| Error::Io {
path: path.to_path_buf(),
source: std::io::Error::from_raw_os_error(e as i32),
})?;
}
Ok(RemapResult::Remapped)
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::tempdir;
#[test]
fn test_mappings_match_returns_early() {
let dir = tempdir().unwrap();
let repo_path = dir.path().join("repo");
let mut repo = Repo::init(&repo_path).unwrap();
let result = map(&mut repo, &MapOptions::default()).unwrap();
assert_eq!(result.total, 0);
assert_eq!(result.remapped, 0);
}
#[test]
fn test_remap_result_enum() {
let _r1 = RemapResult::Remapped;
let _r2 = RemapResult::NoChange;
let _r3 = RemapResult::SkippedUnmappedSource;
let _r4 = RemapResult::SkippedUnmappedTarget;
}
#[test]
fn test_map_stats_default() {
let stats = MapStats::default();
assert_eq!(stats.remapped, 0);
assert_eq!(stats.total, 0);
assert_eq!(stats.skipped_unmapped_source, 0);
assert_eq!(stats.skipped_unmapped_target, 0);
}
}