add framework for multi platform support

This commit is contained in:
Ryan Heywood 2025-06-17 19:56:38 -04:00
parent 8b5d4ce527
commit 25c06488e4
Signed by: ryan
GPG Key ID: 8E401478A3FBEF72
8 changed files with 395 additions and 1 deletions

12
Cargo.toml Normal file
View File

@ -0,0 +1,12 @@
[package]
name = "nit"
version = "0.1.0"
edition = "2024"
license = "MIT"
[features]
aws = []
default = ["aws"]
[dependencies]
libc = "0.2.172"

View File

@ -1,6 +1,6 @@
MIT License
Copyright (c) 2025 public
Copyright (c) 2025 Distrust
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

23
src/main.rs Normal file
View File

@ -0,0 +1,23 @@
mod result;
mod system;
mod platform;
use result::Result;
fn main() {
if let Err(e) = init() {
eprintln!("Error: {e}");
let mut opt = Some(&e as &dyn std::error::Error);
while let Some(current_source) = opt {
eprintln!("Caused by: {current_source}");
opt = current_source.source();
}
}
}
fn init() -> Result<()> {
platform::init()?;
println!("Hello, world!");
Ok(())
}

26
src/platform/aws.rs Normal file
View File

@ -0,0 +1,26 @@
use crate::result::{Result, Context};
pub struct Aws;
impl Aws {
fn init_heartbeat() -> Result<()> {
Ok(())
}
}
impl super::Platform for Aws {
fn is(&self) -> Result<bool> {
std::fs::exists("/dev/nsm").context(format_args!("could not check if /dev/nsm exists"))
}
fn get_modules(&self) -> Result<Vec<(String, String)>> {
Ok(vec![
("/nsm.ko".into(), String::new())
])
}
fn init(&self) -> Result<()> {
Self::init_heartbeat()?;
Ok(())
}
}

87
src/platform/mod.rs Normal file
View File

@ -0,0 +1,87 @@
use crate::{system::{self, Mount, MountType}, result::Result};
trait Platform {
/// Whether the current Platform is the `Self` platform.
///
/// This method should check for the existence of hardware devices that irrefutably defines
/// this platform as the given platform. For instance, the existence of `/dev/n`
fn is(&self) -> Result<bool>;
/// Return the names and parameters for any required kernel modules.
fn get_modules(&self) -> Result<Vec<(String, String)>> {
Ok(vec![])
}
/// The configuration for mounting filesystems for the platform.
///
/// This normally includes filesystems such as `/dev` and `/proc` that are not
/// backed by physical media.
fn get_mounts(&self) -> Result<Vec<Mount>> {
use libc::{MS_NODEV, MS_NOEXEC, MS_NOSUID};
use MountType::{DevPts, DevTmpFs, Proc, Shm, SysFs, TmpFs};
let no_se = MS_NOSUID | MS_NOEXEC;
let no_dse = no_se | MS_NODEV;
let m755 = Some("mode=0755");
Ok(vec![
Mount::new(DevTmpFs, "/dev", DevTmpFs, no_se, m755),
Mount::new(DevPts, "/dev/pts", DevPts, no_se, None),
Mount::new(Shm, "/dev/shm", TmpFs, no_dse, m755),
Mount::new(Proc, "/proc", Proc, no_dse, m755),
Mount::new(TmpFs, "/tmp", TmpFs, no_dse, None),
Mount::new(SysFs, "/sys", SysFs, no_dse, None),
Mount::new("cgroup_root", "/sys/fs/cgroup", TmpFs, no_dse, m755),
])
}
/// Initialize all necessary requirements for the platform.
fn init(&self) -> Result<()> {
Ok(())
}
}
fn init_filesystems(iter: impl IntoIterator<Item = Mount>) -> Result<()> {
for mount in iter {
mount.mount_self()?;
}
Ok(())
}
fn init_modules(iter: impl IntoIterator<Item = (String, String)>) -> Result<()> {
for (module, params) in iter {
system::insmod(&module, &params)?;
}
Ok(())
}
#[cfg(feature = "aws")]
mod aws;
fn get_current_platform() -> Result<Option<Box<dyn Platform>>> {
#[cfg(feature = "aws")]
if aws::Aws.is()? {
return Ok(Some(Box::new(aws::Aws)))
}
Ok(None)
}
pub fn init() -> Result<()> {
// Error handling strategy: If a platform is compiled in and loaded, if platform
// specific error handling doesn't work, fall back to generic.
let platform = get_current_platform()?;
if let Some(platform) = platform {
// NOTE: We need to make get_mounts _additional_ beyond a base set.
// We need `/dev/nsm` to exist so Aws.is() works.
platform.get_mounts().and_then(init_filesystems)?;
platform.get_modules().and_then(init_modules)?;
platform.init()?;
}
Ok(())
}

52
src/result.rs Normal file
View File

@ -0,0 +1,52 @@
//! Error handling niceties.
// Because who needs anyhow anyways.
/// Provide context for an existing error.
pub trait Context<T> {
fn context(self, fmt: std::fmt::Arguments<'_>) -> Result<T, CtxError>;
}
impl<T, E> Context<T> for std::result::Result<T, E>
where
E: std::error::Error + 'static,
{
fn context(self, fmt: std::fmt::Arguments<'_>) -> Result<T, CtxError> {
match self {
Ok(o) => Ok(o),
Err(e) => Err(CtxError {
context: fmt.to_string(),
inner: Box::new(e),
}),
}
}
}
/// An error with context attached.
///
/// The context is provided as format arguments, but will be constructed into a string if the value
/// is an error.
#[derive(Debug)]
pub struct CtxError {
context: String,
inner: Box<dyn std::error::Error>,
}
impl std::fmt::Display for CtxError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.context.as_str())
}
}
impl std::error::Error for CtxError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
Some(self.inner.as_ref())
}
}
pub type Result<T, E = CtxError> = std::result::Result<T, E>;
pub fn ctx_os_error(args: std::fmt::Arguments<'_>) -> Result<(), CtxError> {
Err(std::io::Error::last_os_error()).context(args)
}

85
src/system/mod.rs Normal file
View File

@ -0,0 +1,85 @@
use crate::result::{Result, Context};
use std::path::{PathBuf, Path};
pub mod syscall;
pub enum MountType {
DevTmpFs,
DevPts,
Shm,
Proc,
TmpFs,
SysFs,
}
impl From<MountType> for PathBuf {
fn from(value: MountType) -> Self {
Self::from(value.as_str())
}
}
impl MountType {
fn as_str(&self) -> &'static str {
match self {
MountType::DevTmpFs => "devtmpfs",
MountType::DevPts => "devpts",
MountType::Shm => "shm",
MountType::Proc => "proc",
MountType::TmpFs => "tmpfs",
MountType::SysFs => "sysfs",
}
}
}
pub struct Mount {
source: PathBuf,
target: PathBuf,
fstype: MountType,
flags: libc::c_ulong,
data: Option<String>,
}
impl Mount {
pub fn new<'a>(
source: impl Into<PathBuf>,
target: impl Into<PathBuf>,
fstype: MountType,
flags: libc::c_ulong,
data: impl Into<Option<&'a str>>,
) -> Self {
let source = source.into();
let target = target.into();
let data = data.into().map(Into::into);
Self {
source,
target,
fstype,
flags,
data,
}
}
pub fn mount_self(&self) -> Result<()> {
syscall::mount(
&self.source,
&self.target,
self.fstype.as_str(),
self.flags,
self.data.as_deref(),
)?;
Ok(())
}
}
pub fn insmod(path: impl AsRef<Path>, params: impl AsRef<str>) -> Result<()> {
let path = path.as_ref();
let params = params.as_ref();
let file = std::fs::File::open(path).context(format_args!(
"could not open: {path}",
path = path.display()
))?;
syscall::finit_module(file, params)?;
Ok(())
}

109
src/system/syscall.rs Normal file
View File

@ -0,0 +1,109 @@
//! Wrappers for libc system calls and other necessary functions.
//!
//! These functions, for one reason or another, require using the `libc` crate and can't
//! be implemented directly in the standard library.
// Some of these methods might not be called, because support for the features that
// invoke the functions were not enabled.
#![allow(dead_code)]
use crate::result::{ctx_os_error, Context, Result};
use libc::{self, c_ulong, c_void};
use std::{
ffi::CString,
os::fd::AsRawFd,
path::Path,
};
pub fn mount(
src: impl AsRef<Path>,
target: impl AsRef<Path>,
fstype: impl AsRef<str>,
flags: c_ulong,
data: Option<&str>,
) -> Result<()> {
let src = src.as_ref();
let target = target.as_ref();
let fstype = fstype.as_ref();
// it upsets me to no end that these two lines format differently.
#[rustfmt::ignore]
let src_cstr = CString::new(src.as_os_str().as_encoded_bytes())
.context(format_args!("bad src: {src}", src = src.display()))?;
#[rustfmt::ignore]
let target_cstr = CString::new(target.as_os_str().as_encoded_bytes()).context(format_args!(
"bad target: {target}",
target = target.display()
))?;
let fstype_cs = CString::new(fstype).context(format_args!("bad fstype: {fstype}"))?;
let data_ptr = if let Some(s) = data {
s.as_ptr()
} else {
std::ptr::null()
};
// SAFETY: mount() can accept nullptr for data. no other pointers are constructed manually.
match unsafe {
libc::mount(
src_cstr.as_ptr(),
target_cstr.as_ptr(),
fstype_cs.as_ptr(),
flags,
data_ptr.cast::<c_void>(),
)
} {
0 => Ok(()),
-1 => ctx_os_error(format_args!(
"error calling mount({src}, {target}, {fstype}, {flags}, {data:?})",
src = src.display(),
target = target.display()
)),
n => unreachable!("mount() syscall returned bad value: {n}"),
}
}
pub fn freopen(path: impl AsRef<Path>, mode: impl AsRef<str>, fd: &impl AsRawFd) -> Result<()> {
let path = path.as_ref();
let mode = mode.as_ref();
let fd = fd.as_raw_fd();
#[rustfmt::ignore]
let filename_cs = CString::new(path.as_os_str().as_encoded_bytes())
.context(format_args!("bad path: {path}", path = path.display()))?;
let mode_cs = CString::new(mode).context(format_args!("bad mode: {mode}"))?;
// SAFETY: no pointers are constructed manually.
let file = unsafe { libc::fdopen(fd, mode.as_ptr().cast::<i8>()) };
if file.is_null() {
return ctx_os_error(format_args!("bad fdopen({fd}, {mode})"));
}
// SAFETY: no pointers are constructed manually.
let file = unsafe { libc::freopen(filename_cs.as_ptr(), mode_cs.as_ptr(), file) };
if file.is_null() {
return ctx_os_error(format_args!(
"bad freopen({path}, {mode}, {file:?} as *FILE)",
path = path.display()
));
}
Ok(())
}
// NOTE: We want to consume the file, so we do not allow a ref to impl AsRawFd
#[allow(clippy::needless_pass_by_value)]
pub fn finit_module(fd: impl AsRawFd, params: impl AsRef<str>) -> Result<()> {
let fd = fd.as_raw_fd();
let params = params.as_ref();
let params_cs = CString::new(params).context(format_args!("bad params: {params}"))?;
// SAFETY: no pointers are constructed manually, and AsRawFd should be a proper
// file descriptor.
match unsafe { libc::syscall(libc::SYS_finit_module, fd, params_cs.as_ptr(), 0) } {
0 => Ok(()),
-1 => ctx_os_error(format_args!("error calling finit_module({fd}, {params})")),
n => unreachable!("syscall(SYS_finit_module, ...) returned bad value: {n}"),
}
}