use std::{borrow::Cow, fmt, ops::Deref, time::Duration};
use crate::{
args::ToArgs,
exceptions::{ExcType, MontyException},
file_mode::FileMode,
format::StringRepr,
graph::{MontyGraph, MontyNode, NodeId},
object::{CallArgs, MontyObject, MontyTimeZone},
scan::ScanArgs,
unstable::{self, PushValue},
virtual_path::normalize_virtual_path,
};
pub const IMPORT_CALL: &str = "import";
pub const IMPORT_STUBS_CALL: &str = "import_stubs";
pub const IMPORT_STUBS_CALL_ID: u32 = u32::MAX;
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, strum::IntoStaticStr)]
pub enum OsFunctionCall {
#[strum(serialize = "Path.exists")]
Exists(MontyPath),
#[strum(serialize = "Path.is_file")]
IsFile(MontyPath),
#[strum(serialize = "Path.is_dir")]
IsDir(MontyPath),
#[strum(serialize = "Path.is_symlink")]
IsSymlink(MontyPath),
#[strum(serialize = "Path.read_text")]
ReadText(MontyPath),
#[strum(serialize = "Path.read_bytes")]
ReadBytes(MontyPath),
#[strum(serialize = "Path.stat")]
Stat(MontyPath),
#[strum(serialize = "Path.iterdir")]
Iterdir(MontyPath),
#[strum(serialize = "Path.scan")]
Scan(ScanArgs),
#[strum(serialize = "Path.resolve")]
Resolve(MontyPath),
#[strum(serialize = "Path.absolute")]
Absolute(MontyPath),
#[strum(serialize = "Path.write_text")]
WriteText(PathStringDataArgs),
#[strum(serialize = "Path.append_text")]
AppendText(PathStringDataArgs),
#[strum(serialize = "Path.write_bytes")]
WriteBytes(PathBytesDataArgs),
#[strum(serialize = "Path.append_bytes")]
AppendBytes(PathBytesDataArgs),
#[strum(serialize = "open")]
Open(OpenCallArgs),
#[strum(serialize = "Path.mkdir")]
Mkdir(MkdirCallArgs),
#[strum(serialize = "Path.unlink")]
Unlink(MontyPath),
#[strum(serialize = "Path.rmdir")]
Rmdir(MontyPath),
#[strum(serialize = "Path.rename")]
Rename(RenameCallArgs),
#[strum(serialize = "os.getenv")]
Getenv(GetenvArgs),
#[strum(serialize = "os.environ")]
GetEnviron,
#[strum(serialize = "date.today")]
DateToday,
#[strum(serialize = "datetime.now")]
DateTimeNow(Option<MontyTimeZone>),
#[strum(serialize = "os.urandom")]
Urandom(UrandomArgs),
#[strum(serialize = "time.time")]
Time(TimeCaller),
#[strum(serialize = "time.sleep")]
Sleep(Duration),
#[strum(serialize = "system.sleep")]
SystemSleep(Duration),
#[strum(serialize = "asyncio.sleep")]
AsyncSleep(Duration),
#[strum(serialize = "system.async_sleep")]
AsyncSystemSleep(Duration),
#[strum(serialize = "import")] Import(String),
#[strum(serialize = "import_stubs")] ImportStubs(Vec<String>),
}
impl OsFunctionCall {
#[must_use]
pub fn accepts_future(name: &str) -> bool {
matches!(name, "asyncio.sleep" | "system.async_sleep")
}
#[must_use]
pub fn name(&self) -> &'static str {
self.into()
}
#[must_use]
pub fn to_args(mut self) -> CallArgs {
for path in self.fs_paths_mut() {
if !path.is_empty()
&& let Cow::Owned(normalized) = normalize_virtual_path(path)
{
*path = MontyPath::new(normalized);
}
}
match self {
Self::Exists(p)
| Self::IsFile(p)
| Self::IsDir(p)
| Self::IsSymlink(p)
| Self::ReadText(p)
| Self::ReadBytes(p)
| Self::Stat(p)
| Self::Iterdir(p)
| Self::Resolve(p)
| Self::Absolute(p)
| Self::Unlink(p)
| Self::Rmdir(p) => single_arg(p),
Self::WriteText(a) | Self::AppendText(a) => a.to_args(),
Self::WriteBytes(a) | Self::AppendBytes(a) => a.to_args(),
Self::Open(a) => a.to_args(),
Self::Mkdir(a) => a.to_args(),
Self::Rename(a) => a.to_args(),
Self::Scan(a) => a.to_args(),
Self::Getenv(a) => a.to_args(),
Self::Urandom(a) => a.to_args(),
Self::GetEnviron | Self::DateToday => CallArgs::new(),
Self::Time(caller) => single_arg(caller),
Self::DateTimeNow(tz) => single_arg(tz.map_or(MontyNode::None, MontyNode::TimeZone)),
Self::Sleep(delay) | Self::SystemSleep(delay) | Self::AsyncSleep(delay) | Self::AsyncSystemSleep(delay) => {
single_arg(MontyNode::Float(delay.as_secs_f64()))
}
Self::Import(name) => single_arg(MontyObject::string(name)),
Self::ImportStubs(names) => single_arg(MontyObject::list(names.into_iter().map(MontyObject::string))),
}
}
#[must_use]
pub fn is_write(&self) -> bool {
match self {
Self::WriteText(_)
| Self::WriteBytes(_)
| Self::AppendText(_)
| Self::AppendBytes(_)
| Self::Mkdir(_)
| Self::Unlink(_)
| Self::Rmdir(_)
| Self::Rename(_) => true,
Self::Open(args) => args.mode.create(),
_ => false,
}
}
#[must_use]
pub fn is_existence_check(&self) -> bool {
matches!(
self,
Self::Exists(_) | Self::IsFile(_) | Self::IsDir(_) | Self::IsSymlink(_)
)
}
pub fn check_path_null_bytes(&self) -> Result<(), &'static str> {
if self.fs_primary_path().is_some_and(|path| path.contains('\0')) {
Err(self.embedded_null_message(false))
} else if self.rename_destination().is_some_and(|path| path.contains('\0')) {
Err(self.embedded_null_message(true))
} else {
Ok(())
}
}
#[must_use]
pub fn embedded_null_message(&self, for_destination: bool) -> &'static str {
match self {
Self::Mkdir(_) => "mkdir: embedded null character in path",
Self::Unlink(_) => "unlink: embedded null character in path",
Self::Rmdir(_) => "rmdir: embedded null character in path",
Self::Stat(_) => "stat: embedded null character in path",
Self::Iterdir(_) | Self::Scan(_) => "scandir: embedded null character in path",
Self::Rename(_) if for_destination => "rename: embedded null character in dst",
Self::Rename(_) => "rename: embedded null character in src",
Self::Resolve(_) => "lstat: embedded null character in path",
_ => "embedded null byte",
}
}
#[must_use]
pub fn fs_primary_path(&self) -> Option<&str> {
match self {
Self::Exists(p)
| Self::IsFile(p)
| Self::IsDir(p)
| Self::IsSymlink(p)
| Self::ReadText(p)
| Self::ReadBytes(p)
| Self::Stat(p)
| Self::Iterdir(p)
| Self::Resolve(p)
| Self::Absolute(p)
| Self::Unlink(p)
| Self::Rmdir(p) => Some(p.as_str()),
Self::WriteText(a) | Self::AppendText(a) => Some(a.path.as_str()),
Self::WriteBytes(a) | Self::AppendBytes(a) => Some(a.path.as_str()),
Self::Open(a) => Some(a.path.as_str()),
Self::Mkdir(a) => Some(a.path.as_str()),
Self::Scan(a) => Some(a.path.as_str()),
Self::Rename(a) => Some(a.src.as_str()),
Self::Getenv(_)
| Self::GetEnviron
| Self::DateToday
| Self::DateTimeNow(_)
| Self::Urandom(_)
| Self::Time(_)
| Self::Sleep(_)
| Self::SystemSleep(_)
| Self::AsyncSleep(_)
| Self::AsyncSystemSleep(_)
| Self::Import(_)
| Self::ImportStubs(_) => None,
}
}
#[must_use]
pub fn rename_destination(&self) -> Option<&str> {
match self {
Self::Rename(a) => Some(a.dst.as_str()),
_ => None,
}
}
pub fn fs_paths_mut(&mut self) -> impl Iterator<Item = &mut MontyPath> {
let (primary, dst) = match self {
Self::Exists(p)
| Self::IsFile(p)
| Self::IsDir(p)
| Self::IsSymlink(p)
| Self::ReadText(p)
| Self::ReadBytes(p)
| Self::Stat(p)
| Self::Iterdir(p)
| Self::Resolve(p)
| Self::Absolute(p)
| Self::Unlink(p)
| Self::Rmdir(p) => (Some(p), None),
Self::WriteText(a) | Self::AppendText(a) => (Some(&mut a.path), None),
Self::WriteBytes(a) | Self::AppendBytes(a) => (Some(&mut a.path), None),
Self::Open(a) => (Some(&mut a.path), None),
Self::Mkdir(a) => (Some(&mut a.path), None),
Self::Scan(a) => (Some(&mut a.path), None),
Self::Rename(a) => (Some(&mut a.src), Some(&mut a.dst)),
Self::Getenv(_)
| Self::GetEnviron
| Self::DateToday
| Self::DateTimeNow(_)
| Self::Urandom(_)
| Self::Time(_)
| Self::Sleep(_)
| Self::SystemSleep(_)
| Self::AsyncSleep(_)
| Self::AsyncSystemSleep(_)
| Self::Import(_)
| Self::ImportStubs(_) => (None, None),
};
primary.into_iter().chain(dst)
}
#[must_use]
pub fn on_no_handler(&self) -> MontyException {
if let Self::Import(name) = self {
MontyException::new(
ExcType::ModuleNotFoundError,
Some(format!("No module named {}", StringRepr(name))),
)
} else if let Some(path) = self.fs_primary_path() {
let path = if path.is_empty() {
Cow::Borrowed(path)
} else {
normalize_virtual_path(path)
};
MontyException::new(
ExcType::PermissionError,
Some(format!("Permission denied: {}", StringRepr(&path))),
)
} else {
MontyException::new(
ExcType::RuntimeError,
Some(format!("'{}' is not supported in this environment", self.name())),
)
}
}
}
impl fmt::Display for OsFunctionCall {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.name())
}
}
#[derive(
Debug,
Clone,
Copy,
PartialEq,
Eq,
serde::Serialize,
serde::Deserialize,
strum::EnumIter,
strum::EnumString,
strum::IntoStaticStr,
)]
pub enum TimeCaller {
#[strum(serialize = "time.time")]
Time,
#[strum(serialize = "time.time_ns")]
TimeNs,
#[strum(serialize = "time.monotonic")]
Monotonic,
#[strum(serialize = "time.monotonic_ns")]
MonotonicNs,
#[strum(serialize = "time.perf_counter")]
PerfCounter,
#[strum(serialize = "time.perf_counter_ns")]
PerfCounterNs,
#[strum(serialize = "time.gmtime")]
Gmtime,
#[strum(serialize = "time.localtime")]
Localtime,
#[strum(serialize = "time.asctime")]
Asctime,
#[strum(serialize = "time.ctime")]
Ctime,
#[strum(serialize = "time.strftime")]
Strftime,
}
impl TimeCaller {
#[must_use]
pub fn as_str(self) -> &'static str {
self.into()
}
}
impl fmt::Display for TimeCaller {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
fn single_arg(value: impl PushValue) -> CallArgs {
let mut call = CallArgs::new();
unstable::push_arg(&mut call, value);
call
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, monty_macros::ToArgs)]
pub struct PathStringDataArgs {
pub path: MontyPath,
pub data: String,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, monty_macros::ToArgs)]
pub struct PathBytesDataArgs {
pub path: MontyPath,
#[serde(with = "serde_bytes")]
pub data: Vec<u8>,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, monty_macros::ToArgs)]
pub struct OpenCallArgs {
pub path: MontyPath,
pub mode: FileMode,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, monty_macros::ToArgs)]
pub struct MkdirCallArgs {
pub path: MontyPath,
#[from_args(kw_only)]
pub parents: bool,
#[from_args(kw_only)]
pub exist_ok: bool,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, monty_macros::ToArgs)]
pub struct RenameCallArgs {
pub src: MontyPath,
pub dst: MontyPath,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, monty_macros::ToArgs)]
pub struct GetenvArgs {
pub key: String,
pub default: MontyObject,
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize, monty_macros::ToArgs)]
pub struct UrandomArgs {
pub size: u64,
}
pub const MAX_SLEEP_SECONDS: f64 = 9_223_372_036.854_775;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SleepError {
NotANumber,
Negative,
TooLarge,
}
pub fn sleep_duration(seconds: f64) -> Result<Duration, SleepError> {
if seconds.is_nan() {
Err(SleepError::NotANumber)
} else if seconds.abs() > MAX_SLEEP_SECONDS {
Err(SleepError::TooLarge)
} else if seconds < 0.0 {
Err(SleepError::Negative)
} else {
Duration::try_from_secs_f64(seconds).map_err(|_| SleepError::TooLarge)
}
}
pub fn sleep_duration_saturating(seconds: f64) -> Result<Duration, SleepError> {
match sleep_duration(seconds) {
Ok(delay) => Ok(delay),
Err(SleepError::Negative) => Ok(Duration::ZERO),
Err(SleepError::TooLarge) if seconds < 0.0 => Ok(Duration::ZERO),
Err(SleepError::TooLarge) => Ok(Duration::from_secs_f64(MAX_SLEEP_SECONDS)),
Err(err @ SleepError::NotANumber) => Err(err),
}
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct MontyPath(String);
impl MontyPath {
#[must_use]
pub fn new(path: String) -> Self {
Self(path)
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
#[must_use]
pub fn into_string(self) -> String {
self.0
}
}
impl Deref for MontyPath {
type Target = str;
fn deref(&self) -> &str {
&self.0
}
}
impl From<String> for MontyPath {
fn from(s: String) -> Self {
Self(s)
}
}
impl From<&str> for MontyPath {
fn from(s: &str) -> Self {
Self(s.to_owned())
}
}
impl PushValue for MontyPath {
fn push_into(self, graph: &mut MontyGraph) -> NodeId {
graph.push(MontyNode::Path(self.0))
}
}
#[must_use]
pub fn file_stat(mode: i64, size: i64, mtime: f64) -> MontyObject {
let mode = if mode < 0o1000 { mode | 0o100_000 } else { mode };
stat_result(mode, 0, 0, 1, 0, 0, size, mtime, mtime, mtime)
}
#[must_use]
pub fn dir_stat(mode: i64, mtime: f64) -> MontyObject {
let mode = if mode < 0o1000 { mode | 0o040_000 } else { mode };
stat_result(mode, 0, 0, 2, 0, 0, 4096, mtime, mtime, mtime)
}
#[must_use]
pub fn symlink_stat(mode: i64, mtime: f64) -> MontyObject {
let mode = if mode < 0o1000 { mode | 0o120_000 } else { mode };
stat_result(mode, 0, 0, 1, 0, 0, 0, mtime, mtime, mtime)
}
#[must_use]
#[expect(clippy::too_many_arguments)]
pub fn stat_result(
st_mode: i64,
st_ino: i64,
st_dev: i64,
st_nlink: i64,
st_uid: i64,
st_gid: i64,
st_size: i64,
st_atime: f64,
st_mtime: f64,
st_ctime: f64,
) -> MontyObject {
MontyObject::named_tuple(
STAT_RESULT_TYPE_NAME,
STAT_RESULT_FIELDS.iter().copied(),
[
MontyObject::int(st_mode),
MontyObject::int(st_ino),
MontyObject::int(st_dev),
MontyObject::int(st_nlink),
MontyObject::int(st_uid),
MontyObject::int(st_gid),
MontyObject::int(st_size),
MontyObject::float(st_atime),
MontyObject::float(st_mtime),
MontyObject::float(st_ctime),
],
)
}
const STAT_RESULT_TYPE_NAME: &str = "StatResult";
const STAT_RESULT_FIELDS: &[&str] = &[
"st_mode", "st_ino", "st_dev", "st_nlink", "st_uid", "st_gid", "st_size", "st_atime", "st_mtime", "st_ctime",
];