ente-policy-provider, ente-tmpfiles-compat, journald export format
1//! ente-tmpfiles-compat: aplica directivas tmpfiles.d al boot.
2//!
3//! Lee, en orden, los conf files de:
4//! /usr/lib/tmpfiles.d/*.conf
5//! /etc/tmpfiles.d/*.conf (override del usuario, gana)
6//! /run/tmpfiles.d/*.conf (efímero)
7//!
8//! Aplica un subset de directivas — las suficientes para el boot:
9//! d — crear directorio (idempotente: no falla si existe)
10//! D — crear directorio + limpiar contenido si existe
11//! f — crear archivo (vacío, perms aplicados)
12//! L — crear symlink (overrideable con `+L` si existe)
13//! r — remove file (no falla si ausente)
14//! R — remove recursivamente
15//! e — adjust perms si existe
16//!
17//! Edad/cleanup (`age` field) y modos exotic (b, c, p, P) se ignoran.
18//! El proceso es OneShot: corre, aplica, sale con código 0 / 1.
19
20use std::collections::BTreeMap;
21use std::fs;
22use std::os::unix::fs::PermissionsExt;
23use std::path::{Path, PathBuf};
24use tracing::{debug, info, warn};
25use tracing_subscriber::EnvFilter;
26
27const SEARCH_DIRS: &[&str] = &[
28 "/usr/lib/tmpfiles.d",
29 "/etc/tmpfiles.d",
30 "/run/tmpfiles.d",
31];
32
33#[derive(Debug, Clone)]
34struct Directive {
35 typ: char, // d, D, f, L, r, R, e
36 path: PathBuf,
37 mode: Option<u32>,
38 user: Option<String>,
39 group: Option<String>,
40 arg: Option<String>, // symlink target o content
41}
42
43fn main() {
44 init_tracing();
45 info!("ente-tmpfiles-compat: aplicando directivas tmpfiles.d");
46 let directives = collect_directives();
47 info!(count = directives.len(), "directivas a aplicar");
48
49 let mut applied = 0;
50 let mut skipped = 0;
51 let mut errors = 0;
52 for d in directives {
53 match apply(&d) {
54 Ok(true) => applied += 1,
55 Ok(false) => skipped += 1,
56 Err(e) => {
57 warn!(?e, ?d.typ, path = %d.path.display(), "directiva falló");
58 errors += 1;
59 }
60 }
61 }
62 info!(applied, skipped, errors, "tmpfiles aplicado");
63 if errors > 0 { std::process::exit(1); }
64}
65
66fn collect_directives() -> Vec<Directive> {
67 // Last-wins por path: /etc supera /usr/lib, /run supera /etc.
68 let mut by_path: BTreeMap<(PathBuf, char), Directive> = BTreeMap::new();
69 for dir in SEARCH_DIRS {
70 if !Path::new(dir).exists() { continue; }
71 let mut entries: Vec<_> = match fs::read_dir(dir) {
72 Ok(rd) => rd.filter_map(|e| e.ok()).collect(),
73 Err(_) => continue,
74 };
75 entries.sort_by_key(|e| e.file_name());
76 for entry in entries {
77 let path = entry.path();
78 if path.extension().map(|e| e != "conf").unwrap_or(true) { continue; }
79 match fs::read_to_string(&path) {
80 Ok(content) => {
81 for (line_no, line) in content.lines().enumerate() {
82 if let Some(d) = parse_line(line) {
83 by_path.insert((d.path.clone(), d.typ), d);
84 } else if !line.trim().is_empty() && !line.trim().starts_with('#') {
85 debug!(file = %path.display(), line_no, line, "no parseable, skip");
86 }
87 }
88 }
89 Err(e) => warn!(?e, path = %path.display(), "read"),
90 }
91 }
92 }
93 // Orden de aplicación: removes (r/R) primero, luego creates (d/D/f/L),
94 // adjusts (e) al final.
95 let mut all: Vec<Directive> = by_path.into_values().collect();
96 all.sort_by_key(|d| match d.typ {
97 'r' | 'R' => 0,
98 'd' | 'D' | 'f' | 'L' => 1,
99 'e' => 2,
100 _ => 3,
101 });
102 all
103}
104
105fn parse_line(line: &str) -> Option<Directive> {
106 let line = line.trim();
107 if line.is_empty() || line.starts_with('#') { return None; }
108 // Formato: TYPE PATH MODE USER GROUP AGE ARGUMENT
109 // Strip leading '+' (override marker) y '!' (boot-only) — los soportamos
110 // implícitamente.
111 let typ_str = line.chars().next()?;
112 let typ = match typ_str {
113 '+' | '!' => line.chars().nth(1)?,
114 c => c,
115 };
116 if !"dDfLrRe".contains(typ) { return None; }
117 // tokenize tomando en cuenta '-' como "default"
118 let mut parts = line.splitn(7, char::is_whitespace).filter(|s| !s.is_empty());
119 let _t = parts.next()?;
120 let path = parts.next()?.to_string();
121 let mode = parts.next().and_then(parse_mode);
122 let user = parts.next().and_then(parse_default);
123 let group = parts.next().and_then(parse_default);
124 let _age = parts.next();
125 let arg = parts.next().and_then(parse_default);
126 Some(Directive {
127 typ,
128 path: PathBuf::from(path),
129 mode, user, group, arg,
130 })
131}
132
133fn parse_default(s: &str) -> Option<String> {
134 if s == "-" { None } else { Some(s.to_string()) }
135}
136
137fn parse_mode(s: &str) -> Option<u32> {
138 if s == "-" { return None; }
139 u32::from_str_radix(s.trim_start_matches('~'), 8).ok()
140}
141
142fn apply(d: &Directive) -> anyhow::Result<bool> {
143 match d.typ {
144 'd' | 'D' => apply_d(d),
145 'f' => apply_f(d),
146 'L' => apply_l(d),
147 'r' => apply_r(d, false),
148 'R' => apply_r(d, true),
149 'e' => apply_e(d),
150 _ => Ok(false),
151 }
152}
153
154fn apply_d(d: &Directive) -> anyhow::Result<bool> {
155 fs::create_dir_all(&d.path)
156 .map_err(|e| anyhow::anyhow!("mkdir {}: {e}", d.path.display()))?;
157 if let Some(mode) = d.mode {
158 fs::set_permissions(&d.path, fs::Permissions::from_mode(mode))?;
159 }
160 chown(&d.path, d.user.as_deref(), d.group.as_deref())?;
161 if d.typ == 'D' {
162 // Limpiar contenido (no recursivo).
163 if let Ok(rd) = fs::read_dir(&d.path) {
164 for entry in rd.flatten() {
165 let p = entry.path();
166 if p.is_dir() { let _ = fs::remove_dir_all(&p); }
167 else { let _ = fs::remove_file(&p); }
168 }
169 }
170 }
171 info!(path = %d.path.display(), mode = ?d.mode, "d/D aplicado");
172 Ok(true)
173}
174
175fn apply_f(d: &Directive) -> anyhow::Result<bool> {
176 if !d.path.exists() {
177 if let Some(parent) = d.path.parent() {
178 let _ = fs::create_dir_all(parent);
179 }
180 let content = d.arg.clone().unwrap_or_default();
181 fs::write(&d.path, content.as_bytes())?;
182 }
183 if let Some(mode) = d.mode {
184 fs::set_permissions(&d.path, fs::Permissions::from_mode(mode))?;
185 }
186 chown(&d.path, d.user.as_deref(), d.group.as_deref())?;
187 info!(path = %d.path.display(), mode = ?d.mode, "f aplicado");
188 Ok(true)
189}
190
191fn apply_l(d: &Directive) -> anyhow::Result<bool> {
192 let target = match &d.arg {
193 Some(t) => t,
194 None => anyhow::bail!("L sin target en {}", d.path.display()),
195 };
196 if d.path.exists() {
197 // No sobreescribimos symlinks/files existentes (modo no-`+`).
198 debug!(path = %d.path.display(), "L: existe, skip");
199 return Ok(false);
200 }
201 if let Some(parent) = d.path.parent() {
202 let _ = fs::create_dir_all(parent);
203 }
204 std::os::unix::fs::symlink(target, &d.path)?;
205 info!(path = %d.path.display(), %target, "L aplicado");
206 Ok(true)
207}
208
209fn apply_r(d: &Directive, recursive: bool) -> anyhow::Result<bool> {
210 if !d.path.exists() {
211 return Ok(false);
212 }
213 if recursive {
214 fs::remove_dir_all(&d.path)?;
215 } else if d.path.is_dir() {
216 fs::remove_dir(&d.path)?;
217 } else {
218 fs::remove_file(&d.path)?;
219 }
220 info!(path = %d.path.display(), recursive, "remove aplicado");
221 Ok(true)
222}
223
224fn apply_e(d: &Directive) -> anyhow::Result<bool> {
225 if !d.path.exists() {
226 return Ok(false);
227 }
228 if let Some(mode) = d.mode {
229 fs::set_permissions(&d.path, fs::Permissions::from_mode(mode))?;
230 }
231 chown(&d.path, d.user.as_deref(), d.group.as_deref())?;
232 info!(path = %d.path.display(), "e aplicado");
233 Ok(true)
234}
235
236fn chown(path: &Path, user: Option<&str>, group: Option<&str>) -> anyhow::Result<()> {
237 use std::ffi::CString;
238 let uid = match user {
239 Some(u) => Some(lookup_uid(u)?),
240 None => None,
241 };
242 let gid = match group {
243 Some(g) => Some(lookup_gid(g)?),
244 None => None,
245 };
246 let (uid, gid) = (uid.unwrap_or(u32::MAX), gid.unwrap_or(u32::MAX));
247 let cstr = CString::new(path.as_os_str().as_encoded_bytes())?;
248 let r = unsafe { libc::chown(cstr.as_ptr(), uid, gid) };
249 if r != 0 {
250 let e = std::io::Error::last_os_error();
251 // No-op si ya somos non-root y el chown falla con EPERM.
252 if e.raw_os_error() == Some(libc::EPERM) {
253 debug!(path = %path.display(), "chown EPERM (esperado sin root)");
254 return Ok(());
255 }
256 return Err(anyhow::anyhow!("chown: {e}"));
257 }
258 Ok(())
259}
260
261fn lookup_uid(name: &str) -> anyhow::Result<u32> {
262 if let Ok(n) = name.parse::<u32>() { return Ok(n); }
263 let cstr = std::ffi::CString::new(name)?;
264 let pw = unsafe { libc::getpwnam(cstr.as_ptr()) };
265 if pw.is_null() { anyhow::bail!("user '{name}' no encontrado"); }
266 Ok(unsafe { (*pw).pw_uid })
267}
268
269fn lookup_gid(name: &str) -> anyhow::Result<u32> {
270 if let Ok(n) = name.parse::<u32>() { return Ok(n); }
271 let cstr = std::ffi::CString::new(name)?;
272 let gr = unsafe { libc::getgrnam(cstr.as_ptr()) };
273 if gr.is_null() { anyhow::bail!("group '{name}' no encontrado"); }
274 Ok(unsafe { (*gr).gr_gid })
275}
276
277fn init_tracing() {
278 let filter = EnvFilter::try_from_default_env()
279 .unwrap_or_else(|_| EnvFilter::new("ente_tmpfiles_compat=info"));
280 tracing_subscriber::fmt().with_env_filter(filter).with_target(true).init();
281}