1use crate::config::PackageConfig;
2use anyhow::{Context, Result};
3use std::os::unix::fs::MetadataExt;
4use std::path::Path;
5
6#[derive(Debug, Clone)]
8pub struct ResolvedMetadata {
9 pub owner: Option<String>,
10 pub group: Option<String>,
11 pub mode: Option<String>,
12}
13
14pub fn resolve_metadata(pkg_config: &PackageConfig, rel_path: &str) -> ResolvedMetadata {
20 let preserve_fields: Vec<&str> = pkg_config
21 .preserve
22 .get(rel_path)
23 .map(|v| v.iter().map(|s| s.as_str()).collect())
24 .unwrap_or_default();
25
26 let owner = if preserve_fields.contains(&"owner") {
27 None
28 } else if let Some(ownership) = pkg_config.ownership.get(rel_path) {
29 ownership.split(':').next().map(|s| s.to_string())
30 } else {
31 pkg_config.owner.clone()
32 };
33
34 let group = if preserve_fields.contains(&"group") {
35 None
36 } else if let Some(ownership) = pkg_config.ownership.get(rel_path) {
37 ownership.split(':').nth(1).map(|s| s.to_string())
38 } else {
39 pkg_config.group.clone()
40 };
41
42 let mode = if preserve_fields.contains(&"mode") {
43 None
44 } else {
45 pkg_config.permissions.get(rel_path).cloned()
46 };
47
48 ResolvedMetadata { owner, group, mode }
49}
50
51pub fn read_file_metadata(path: &Path) -> Result<(String, String, String)> {
53 let meta = std::fs::metadata(path)
54 .with_context(|| format!("failed to read metadata for {}", path.display()))?;
55
56 let uid = meta.uid();
57 let gid = meta.gid();
58
59 let owner = nix::unistd::User::from_uid(nix::unistd::Uid::from_raw(uid))
60 .ok()
61 .flatten()
62 .map(|u| u.name)
63 .unwrap_or_else(|| uid.to_string());
64
65 let group = nix::unistd::Group::from_gid(nix::unistd::Gid::from_raw(gid))
66 .ok()
67 .flatten()
68 .map(|g| g.name)
69 .unwrap_or_else(|| gid.to_string());
70
71 let mode = format!("{:o}", meta.mode() & 0o7777);
72
73 Ok((owner, group, mode))
74}
75
76pub fn apply_ownership(path: &Path, owner: Option<&str>, group: Option<&str>) -> Result<()> {
78 let uid = match owner {
79 Some(name) => {
80 let user = nix::unistd::User::from_name(name)
81 .with_context(|| format!("failed to look up user '{name}'"))?
82 .with_context(|| format!("user '{name}' not found"))?;
83 Some(user.uid)
84 }
85 None => None,
86 };
87
88 let gid = match group {
89 Some(name) => {
90 let grp = nix::unistd::Group::from_name(name)
91 .with_context(|| format!("failed to look up group '{name}'"))?
92 .with_context(|| format!("group '{name}' not found"))?;
93 Some(grp.gid)
94 }
95 None => None,
96 };
97
98 nix::unistd::chown(path, uid, gid)
99 .with_context(|| format!("failed to chown {}", path.display()))?;
100
101 Ok(())
102}
103
104#[cfg(test)]
105mod tests {
106 use super::*;
107 use crate::config::PackageConfig;
108
109 fn make_pkg_config() -> PackageConfig {
110 PackageConfig {
111 target: Some("/etc/foo".into()),
112 strategy: Some(crate::config::DeployStrategy::Copy),
113 system: true,
114 owner: Some("root".into()),
115 group: Some("root".into()),
116 ..Default::default()
117 }
118 }
119
120 #[test]
121 fn resolve_uses_package_level_defaults() {
122 let pkg = make_pkg_config();
123 let meta = resolve_metadata(&pkg, "some/file.conf");
124 assert_eq!(meta.owner.as_deref(), Some("root"));
125 assert_eq!(meta.group.as_deref(), Some("root"));
126 assert!(meta.mode.is_none());
127 }
128
129 #[test]
130 fn resolve_per_file_ownership_overrides_package() {
131 let mut pkg = make_pkg_config();
132 pkg.ownership
133 .insert("file.conf".into(), "www:webgroup".into());
134 let meta = resolve_metadata(&pkg, "file.conf");
135 assert_eq!(meta.owner.as_deref(), Some("www"));
136 assert_eq!(meta.group.as_deref(), Some("webgroup"));
137 }
138
139 #[test]
140 fn resolve_preserve_overrides_package_level() {
141 let mut pkg = make_pkg_config();
142 pkg.preserve
143 .insert("file.conf".into(), vec!["owner".into()]);
144 let meta = resolve_metadata(&pkg, "file.conf");
145 assert!(meta.owner.is_none());
146 assert_eq!(meta.group.as_deref(), Some("root"));
147 }
148
149 #[test]
150 fn resolve_preserve_mode_blocks_permission_override() {
151 let mut pkg = make_pkg_config();
152 pkg.permissions.insert("file.conf".into(), "640".into());
153 pkg.preserve
154 .insert("file.conf".into(), vec!["mode".into()]);
155 let meta = resolve_metadata(&pkg, "file.conf");
156 assert!(meta.mode.is_none());
157 }
158
159 #[test]
160 fn resolve_no_config_preserves_everything() {
161 let mut pkg = make_pkg_config();
162 pkg.owner = None;
163 pkg.group = None;
164 let meta = resolve_metadata(&pkg, "file.conf");
165 assert!(meta.owner.is_none());
166 assert!(meta.group.is_none());
167 assert!(meta.mode.is_none());
168 }
169
170 #[test]
171 fn resolve_permissions_from_config() {
172 let mut pkg = make_pkg_config();
173 pkg.permissions.insert("file.conf".into(), "755".into());
174 let meta = resolve_metadata(&pkg, "file.conf");
175 assert_eq!(meta.mode.as_deref(), Some("755"));
176 }
177}