use std::ffi::CStr;
use std::path::Path;
use crate::errors::*;
use log::{debug, error, info, warn};
use rustix::fs;
#[cfg(feature = "builder")]
use crate::context_node::PropertyAreaMutGuard;
use crate::context_node::{ContextNode, PropertyAreaGuard};
use crate::property_area::{PropertyArea, PropertyAreaMap};
use crate::property_info_parser::{PropertyInfoArea, PropertyInfoAreaFile};
const RESERVED_FILENAMES: &[&str] = &[".writer_lock", "properties_serial", "property_info"];
fn try_build_context_node(
area: &PropertyInfoArea<'_>,
dirname: &Path,
writable: bool,
i: usize,
seen_names: &mut std::collections::HashSet<String>,
) -> Result<ContextNode> {
let context_offset = area.context_offset(i)?;
let context_cstr = area.cstr(context_offset)?;
if context_cstr.is_empty() {
return Err(Error::FileValidation(format!(
"context entry {i}: empty context name at offset {context_offset}"
)));
}
let context_name = context_cstr.to_str()?;
if !context_name.is_ascii() {
return Err(Error::FileValidation(format!(
"context entry {i}: context name {context_name:?} contains non-ASCII characters"
)));
}
{
use std::path::Component;
let mut components = Path::new(context_name).components();
if context_name.contains('/')
|| !matches!(
(components.next(), components.next()),
(Some(Component::Normal(_)), None)
)
{
return Err(Error::FileValidation(format!(
"context entry {i}: context name {context_name:?} is not a plain filename"
)));
}
}
let folded_name = context_name.to_ascii_lowercase();
if RESERVED_FILENAMES.contains(&folded_name.as_str()) {
return Err(Error::FileValidation(format!(
"context entry {i}: context name {context_name:?} collides with a reserved filename"
)));
}
if !seen_names.insert(folded_name) {
return Err(Error::FileValidation(format!(
"context entry {i}: duplicate context name {context_name:?}"
)));
}
let context = writable.then(|| context_cstr.to_owned());
Ok(ContextNode::new(
writable,
context,
dirname.join(context_name),
))
}
const PROPERTIES_SERIAL_CONTEXT: &CStr = c"u:object_r:properties_serial:s0";
pub(crate) struct ContextsSerialized {
property_info_area_file: PropertyInfoAreaFile,
context_nodes: Vec<Option<ContextNode>>,
serial_property_area_map: PropertyAreaMap,
_writer_lock: Option<std::fs::File>,
}
impl ContextsSerialized {
pub(crate) fn new(writable: bool, dirname: &Path, load_default_path: bool) -> Result<Self> {
let tree_filename = dirname.join("property_info");
let serial_filename = dirname.join("properties_serial");
let property_info_area_file = if load_default_path {
PropertyInfoAreaFile::load_default_path()
} else {
PropertyInfoAreaFile::load_path(tree_filename.as_path())
}?;
let property_info_area = property_info_area_file.property_info_area();
let num_context_nodes = property_info_area.num_contexts();
const MAX_CONTEXTS: usize = 65_536;
if num_context_nodes > MAX_CONTEXTS {
return Err(Error::FileValidation(format!(
"context table declares {num_context_nodes} entries (max {MAX_CONTEXTS})"
)));
}
if num_context_nodes > 0 {
property_info_area
.context_offset(num_context_nodes - 1)
.map_err(|e| {
Error::FileValidation(format!(
"context table ({num_context_nodes} entries) exceeds property_info bounds: {e}"
))
})?;
}
let mut context_nodes: Vec<Option<ContextNode>> = Vec::with_capacity(num_context_nodes);
let mut seen_names = std::collections::HashSet::new();
for i in 0..num_context_nodes {
match try_build_context_node(&property_info_area, dirname, writable, i, &mut seen_names)
{
Ok(n) => context_nodes.push(Some(n)),
Err(e) => {
warn!("context entry {i} skipped: {e}");
context_nodes.push(None);
}
}
}
let (writer_lock, serial_property_area_map) = if writable {
if !dirname.is_dir() {
info!("Creating directory: {dirname:?}");
match fs::mkdir(dirname, fs::Mode::RWXU | fs::Mode::XGRP | fs::Mode::XOTH) {
Ok(()) => {}
Err(rustix::io::Errno::EXIST) if dirname.is_dir() => {}
Err(e) => return Err(Error::from(e)),
}
}
let lock = Self::acquire_writer_lock(dirname)?;
for node in context_nodes.iter().flatten() {
node.open()?;
}
(
Some(lock),
Self::map_serial_property_area(serial_filename.as_path(), true)?,
)
} else {
(
None,
Self::map_serial_property_area(serial_filename.as_path(), false)?,
)
};
Ok(Self {
property_info_area_file,
context_nodes,
serial_property_area_map,
_writer_lock: writer_lock,
})
}
fn acquire_writer_lock(dirname: &Path) -> Result<std::fs::File> {
use std::os::unix::fs::OpenOptionsExt;
let lock_path = dirname.join(".writer_lock");
let lock_file = std::fs::OpenOptions::new()
.write(true)
.create(true)
.truncate(false)
.custom_flags(fs::OFlags::NOFOLLOW.bits() as _)
.mode(0o600)
.open(&lock_path)
.context_with_location(format!("Failed to open writer lock {lock_path:?}"))?;
fs::fchmod(&lock_file, fs::Mode::RUSR | fs::Mode::WUSR)
.context_with_location(format!("Failed to restrict mode of {lock_path:?}"))?;
fs::flock(&lock_file, fs::FlockOperation::NonBlockingLockExclusive).map_err(|e| {
error!("Another writer holds the property area lock {lock_path:?}: {e}");
Error::Lock(format!(
"Writable property area already owned by another instance ({lock_path:?}): {e}"
))
})?;
Ok(lock_file)
}
fn map_serial_property_area(
serial_filename: &Path,
access_rw: bool,
) -> Result<PropertyAreaMap> {
let result = if access_rw {
PropertyAreaMap::new_rw(serial_filename, Some(PROPERTIES_SERIAL_CONTEXT))
} else {
PropertyAreaMap::new_ro(serial_filename)
};
result
.inspect_err(|e| error!("Failed to map serial property area {serial_filename:?}: {e}"))
}
fn context_node_at(
&self,
index: u32,
what: &dyn std::fmt::Display,
miss_is_expected: bool,
) -> Result<&ContextNode> {
match self.context_nodes.get(index as usize) {
Some(Some(node)) => Ok(node),
Some(None) => {
debug!("Context entry {index} for {what} was skipped during init");
Err(Error::FileValidation(format!(
"context entry {index} for {what} unavailable (corrupt at init)"
)))
}
None => {
if miss_is_expected {
debug!(
"No context for {what}: index={index}, max_contexts={}",
self.context_nodes.len()
);
} else {
warn!(
"Context index out of range for {what}: index={index}, max_contexts={}",
self.context_nodes.len()
);
}
Err(Error::NotFound(format!("no context for {what}")))
}
}
}
pub(crate) fn prop_area_for_name(&self, name: &str) -> Result<(PropertyAreaGuard<'_>, u32)> {
let (index, _) = self
.property_info_area_file
.property_info_area()
.get_property_info_indexes(name);
let node = self.context_node_at(index, &format_args!("property {name}"), true)?;
let area = node
.property_area()
.inspect_err(|e| error!("Failed to get property area for {name}: {e}"))?;
Ok((area, index))
}
#[cfg(feature = "builder")]
pub(crate) fn prop_area_mut_for_name(
&self,
name: &str,
) -> Result<(PropertyAreaMutGuard<'_>, u32)> {
let (index, _) = self
.property_info_area_file
.property_info_area()
.get_property_info_indexes(name);
let node = self.context_node_at(index, &format_args!("property {name}"), true)?;
let area = node
.property_area_mut()
.inspect_err(|e| error!("Failed to get mutable property area for {name}: {e}"))?;
Ok((area, index))
}
pub(crate) fn serial_prop_area(&self) -> &PropertyArea {
self.serial_property_area_map.property_area()
}
pub(crate) fn prop_area_with_index(&self, context_index: u32) -> Result<PropertyAreaGuard<'_>> {
self.context_node_at(
context_index,
&format_args!("context index {context_index}"),
false,
)?
.property_area()
.inspect_err(|e| {
error!("Failed to get property area for context index {context_index}: {e}")
})
}
#[cfg(feature = "builder")]
pub(crate) fn prop_area_mut_with_index(
&self,
context_index: u32,
) -> Result<PropertyAreaMutGuard<'_>> {
self.context_node_at(
context_index,
&format_args!("context index {context_index}"),
false,
)?
.property_area_mut()
.inspect_err(|e| {
error!("Failed to get mutable property area for context index {context_index}: {e}")
})
}
}