lua_stdlib/os_lib.rs
1//! Lua `os` standard library.
2//!
3//! Ports `src/loslib.c` (430 lines, 12 functions) to Rust.
4//!
5//! ## Platform access limitations
6//!
7//! Several `os.*` functions require OS-level capabilities that are restricted to
8//! `lua-cli` per PORTING.md (no `std::fs`, no `std::process` in `lua-stdlib`).
9//! Those functions are stubbed with `TODO(port)` markers and return nil / error
10//! until a capability-injection layer is designed in Phase B.
11//!
12//! Time decomposition (`os.date`, `os.time`) requires C-library functions
13//! (`gmtime_r`, `localtime_r`, `mktime`, `strftime`). Those call sites are
14//! flagged with `TODO(port)` and the stubs use a zero-initialised `TmFields`.
15
16use lua_types::{LuaError, LuaExit, LuaType, LuaValue};
17use crate::state_stub::{LuaState, LuaStateStubExt as _};
18use lua_vm::state::OsExecuteReason;
19
20// ── Constants ────────────────────────────────────────────────────────────────
21
22//
23// Valid `strftime` conversion specifiers — C99 / POSIX variant.
24// Single-char specifiers appear first; the `||` sentinel signals the start
25// of 2-char specifiers (e.g. `%EC`, `%Oy`). See `check_strftime_option`.
26const STRFTIME_OPTIONS: &[u8] =
27 b"aAbBcCdDeFgGhHIjmMnprRStTuUVwWxXyYzZ%||EcECExEXEyEYOdOeOHOIOmOMOSOuOUOVOwOWOy";
28
29const SIZE_TIME_FMT: usize = 250;
30
31// ── TmFields ─────────────────────────────────────────────────────────────────
32
33/// Local mirror of C's `struct tm`.
34///
35/// Field conventions follow the C standard: `tm_year` is years since 1900,
36/// `tm_mon` ∈ [0, 11], `tm_wday` ∈ [0, 6] (Sunday = 0), `tm_isdst` is −1 when
37/// DST status is unknown.
38///
39/// TODO(port): In Phase B, replace with the `libc::tm` type (via the `libc` crate)
40/// or an equivalent from `chrono` / `time`. Conversion from / to Unix timestamps
41/// is not implemented in Phase A — stubs that need a broken-down time use
42/// `TmFields::default()` (all zeros).
43#[derive(Debug, Default, Clone)]
44pub struct TmFields {
45 pub tm_sec: i32,
46 pub tm_min: i32,
47 pub tm_hour: i32,
48 pub tm_mday: i32,
49 pub tm_mon: i32,
50 pub tm_year: i32,
51 pub tm_wday: i32,
52 pub tm_yday: i32,
53 pub tm_isdst: i32,
54}
55
56// ── ByteDisplay ──────────────────────────────────────────────────────────────
57
58/// `Display` adapter for `&[u8]` slices known to contain ASCII bytes.
59///
60/// Used only for formatting Lua table field names (always ASCII identifiers such
61/// as `"year"`, `"month"`) inside error messages, without allocating a `String`.
62struct ByteDisplay<'a>(&'a [u8]);
63
64impl std::fmt::Display for ByteDisplay<'_> {
65 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
66 for &b in self.0 {
67 write!(f, "{}", b as char)?;
68 }
69 Ok(())
70 }
71}
72
73// ── Private stack-manipulation helpers ───────────────────────────────────────
74
75///
76/// Pushes `(value as i64) + (delta as i64)` as a Lua integer, then stores it
77/// in the table currently on top of the stack at field `key`.
78fn set_field(state: &mut LuaState, key: &[u8], value: i32, delta: i32) -> Result<(), LuaError> {
79 state.push(LuaValue::Int((value as i64) + (delta as i64)));
80 state.set_field(-2, key)?;
81 Ok(())
82}
83
84///
85/// Stores a boolean at field `key` in the table on top of the stack.
86/// A negative `value` means "undefined" — the field is silently skipped.
87fn set_bool_field(state: &mut LuaState, key: &[u8], value: i32) -> Result<(), LuaError> {
88 if value < 0 {
89 return Ok(());
90 }
91 state.push(LuaValue::Bool(value != 0));
92 state.set_field(-2, key)?;
93 Ok(())
94}
95
96///
97/// Writes every field of `stm` into the table on top of the stack, applying the
98/// offsets that convert from C-library conventions to Lua conventions:
99/// year+1900, month+1, wday+1, yday+1.
100fn set_all_fields(state: &mut LuaState, stm: &TmFields) -> Result<(), LuaError> {
101 set_field(state, b"year", stm.tm_year, 1900)?;
102 set_field(state, b"month", stm.tm_mon, 1)?;
103 set_field(state, b"day", stm.tm_mday, 0)?;
104 set_field(state, b"hour", stm.tm_hour, 0)?;
105 set_field(state, b"min", stm.tm_min, 0)?;
106 set_field(state, b"sec", stm.tm_sec, 0)?;
107 set_field(state, b"yday", stm.tm_yday, 1)?;
108 set_field(state, b"wday", stm.tm_wday, 1)?;
109 set_bool_field(state, b"isdst", stm.tm_isdst)?;
110 Ok(())
111}
112
113///
114/// Reads a boolean field from the table on top of the stack.
115/// Returns `-1` when the field is absent (nil), or `0` / `1` for false / true.
116fn get_bool_field(state: &mut LuaState, key: &[u8]) -> Result<i32, LuaError> {
117 let ty = state.get_field(-1, key)?;
118 let res = if matches!(ty, LuaType::Nil) {
119 -1i32
120 } else {
121 state.to_boolean(-1) as i32
122 };
123 state.pop_n(1);
124 Ok(res)
125}
126
127///
128/// Reads an integer field from the table on top of the stack.
129///
130/// * `d` — default when the field is absent; pass `d < 0` to make absence an
131/// error.
132/// * `delta` — subtracted from the read value to convert from Lua's offset
133/// representation back to C-library conventions (e.g. month−1, year−1900).
134///
135/// PORT NOTE: Stack cleanup on error paths (pop before returning Err) is added
136/// vs. the C version where `luaL_error` never returns (longjmp).
137fn get_field(
138 state: &mut LuaState,
139 key: &[u8],
140 d: i32,
141 delta: i32,
142) -> Result<i32, LuaError> {
143 let ty = state.get_field(-1, key)?;
144 let maybe_int = state.to_integer_x(-1);
145 let res: i32 = match maybe_int {
146 Some(res) => {
147 // return luaL_error(L, "field '%s' is out-of-bound", key);
148 let in_bounds = if res >= 0 {
149 res.saturating_sub(delta as i64) <= (i32::MAX as i64)
150 } else {
151 (i32::MIN as i64).saturating_add(delta as i64) <= res
152 };
153 if !in_bounds {
154 state.pop_n(1);
155 return Err(LuaError::runtime(format_args!(
156 "field '{}' is out-of-bound",
157 ByteDisplay(key),
158 )));
159 }
160 (res - delta as i64) as i32
161 }
162 None => {
163 if !matches!(ty, LuaType::Nil) {
164 state.pop_n(1);
165 return Err(LuaError::runtime(format_args!(
166 "field '{}' is not an integer",
167 ByteDisplay(key),
168 )));
169 } else if d < 0 {
170 state.pop_n(1);
171 return Err(LuaError::runtime(format_args!(
172 "field '{}' missing in date table",
173 ByteDisplay(key),
174 )));
175 }
176 d
177 }
178 };
179 state.pop_n(1);
180 Ok(res)
181}
182
183/// ptrdiff_t convlen, char *buff)`
184///
185/// Validates the `strftime` conversion specifier at the start of `conv` against
186/// `STRFTIME_OPTIONS`.
187///
188/// `cc` must have `cc[0] == b'%'` on entry (set by the caller). On success the
189/// matched specifier bytes are written into `cc[1..=oplen]`, a null terminator is
190/// written at `cc[oplen+1]`, and the sub-slice of `conv` after the consumed
191/// specifier is returned.
192///
193/// On failure a `LuaError::arg_error` describing the invalid specifier is
194/// returned.
195///
196/// The options table uses `|` characters as length-transition markers: one `|`
197/// increments `oplen` from 1 to 2 (and the following advance jumps past the `||`
198/// sentinel), enabling 2-char specifiers like `%EC`.
199fn check_strftime_option<'a>(
200 _state: &mut LuaState,
201 conv: &'a [u8],
202 cc: &mut [u8; 4],
203) -> Result<&'a [u8], LuaError> {
204 let options = STRFTIME_OPTIONS;
205 let mut oplen: usize = 1;
206 let mut i: usize = 0;
207
208 while i < options.len() && oplen <= conv.len() {
209 if options[i] == b'|' {
210 // Increment first so the subsequent `i += oplen` uses the new value,
211 // which jumps from the first `|` past the entire `||` separator block.
212 oplen += 1;
213 i += oplen;
214 } else if i + oplen <= options.len() && conv[..oplen] == options[i..i + oplen] {
215 // cc[0] = b'%' is pre-filled; write specifier bytes into cc[1..=oplen].
216 debug_assert!(oplen <= 2, "STRFTIME_OPTIONS only has 1- and 2-char specifiers");
217 cc[1..=oplen].copy_from_slice(&conv[..oplen]);
218 cc[oplen + 1] = 0;
219 return Ok(&conv[oplen..]);
220 } else {
221 i += oplen;
222 }
223 }
224 Err(LuaError::arg_error(
225 1,
226 "invalid conversion specifier",
227 ))
228}
229
230///
231/// Reads argument `arg` as a Lua integer and returns it as a Unix timestamp.
232///
233/// PORT NOTE: On 64-bit targets `time_t == i64 == lua_Integer`, so the range
234/// check in the C original (`(time_t)t == t`) is always satisfied.
235/// TODO(port): On hypothetical 32-bit `time_t` platforms the check would need
236/// to narrow `t` to `i32` and verify no truncation; flag for Phase B.
237fn check_time(state: &mut LuaState, arg: i32) -> Result<i64, LuaError> {
238 let t = state.check_arg_integer(arg)?;
239 Ok(t)
240}
241
242/// Returns the current Unix timestamp (seconds since 1970-01-01 UTC).
243///
244/// PORT NOTE: C uses `time(NULL)`. Rust's `SystemTime::now()` is OS-clock based
245/// and equivalent for whole-second resolution. Returns 0 if the system clock is
246/// before the epoch (the documented `duration_since` failure mode).
247fn unix_now() -> i64 {
248 use std::time::{SystemTime, UNIX_EPOCH};
249 SystemTime::now()
250 .duration_since(UNIX_EPOCH)
251 .map(|d| d.as_secs() as i64)
252 .unwrap_or(0)
253}
254
255/// Decompose a Unix timestamp (UTC) into broken-down time fields.
256///
257/// Uses Howard Hinnant's `civil_from_days` algorithm (public domain, see
258/// <http://howardhinnant.github.io/date_algorithms.html#civil_from_days>),
259/// which is exact for all `i64` inputs across the proleptic Gregorian calendar.
260///
261/// PORT NOTE: C uses `gmtime_r(&t, &tmr)`. Pure-Rust replacement because the
262/// crate forbids `unsafe` (required for libc FFI). `tm_isdst` is always 0 for
263/// UTC. `tm_wday` is 0-based with Sunday = 0 (matches POSIX). `tm_yday` is
264/// 0-based (matches POSIX; `set_all_fields` adds 1 for the Lua-visible table).
265fn decompose_utc(t: i64) -> TmFields {
266 let days = t.div_euclid(86_400);
267 let sod = t.rem_euclid(86_400) as i32;
268
269 let tm_hour = sod / 3600;
270 let tm_min = (sod / 60) % 60;
271 let tm_sec = sod % 60;
272
273 let z = days + 719_468;
274 let era = (if z >= 0 { z } else { z - 146_096 }).div_euclid(146_097);
275 let doe = z - era * 146_097;
276 let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365;
277 let y = yoe + era * 400;
278 let doy_mar = doe - (365 * yoe + yoe / 4 - yoe / 100);
279 let mp = (5 * doy_mar + 2) / 153;
280 let day = (doy_mar - (153 * mp + 2) / 5 + 1) as i32;
281 let month: i32 = if mp < 10 { (mp + 3) as i32 } else { (mp - 9) as i32 };
282 let year = y + if month <= 2 { 1 } else { 0 };
283
284 let leap = (year % 4 == 0 && year % 100 != 0) || year % 400 == 0;
285 const DAYS_BEFORE_MONTH: [i32; 12] = [0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334];
286 let tm_yday = DAYS_BEFORE_MONTH[(month - 1) as usize]
287 + (day - 1)
288 + if leap && month > 2 { 1 } else { 0 };
289
290 let tm_wday = (days + 4).rem_euclid(7) as i32;
291
292 TmFields {
293 tm_sec,
294 tm_min,
295 tm_hour,
296 tm_mday: day,
297 tm_mon: month - 1,
298 tm_year: (year - 1900) as i32,
299 tm_wday,
300 tm_yday,
301 tm_isdst: 0,
302 }
303}
304
305/// Compose a UTC Unix timestamp from broken-down time fields.
306///
307/// Inverse of `decompose_utc`. Uses Howard Hinnant's `days_from_civil` and
308/// normalises month overflow into the year (matching `mktime`'s behaviour for
309/// the year/month axes). Day-of-month, hour, minute, and second components
310/// are added linearly so out-of-range values normalise carry into the larger
311/// units exactly as `mktime` would for UTC.
312fn compose_utc(tm: &TmFields) -> i64 {
313 let mut y: i64 = (tm.tm_year as i64) + 1900;
314 let mut m: i64 = (tm.tm_mon as i64) + 1;
315 let dy = (m - 1).div_euclid(12);
316 y += dy;
317 m -= dy * 12;
318 let y_adj = if m <= 2 { y - 1 } else { y };
319 let era = (if y_adj >= 0 { y_adj } else { y_adj - 399 }).div_euclid(400);
320 let yoe = y_adj - era * 400;
321 let doy = (153 * (if m > 2 { m - 3 } else { m + 9 }) + 2) / 5 + (tm.tm_mday as i64) - 1;
322 let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
323 let days = era * 146_097 + doe - 719_468;
324 days * 86_400 + (tm.tm_hour as i64) * 3600 + (tm.tm_min as i64) * 60 + (tm.tm_sec as i64)
325}
326
327/// Append the formatted result of a single `strftime` conversion specifier.
328///
329/// `cc` holds the canonical specifier bytes filled in by `check_strftime_option`:
330/// `cc[0] == b'%'`, `cc[1]` is the leading specifier char, and for 2-char
331/// specifiers `cc[2]` is the second char (an E/O modifier comes first in C, e.g.
332/// `%Ex` → `cc = "%Ex\0"`). `oplen` is 1 or 2.
333///
334/// PORT NOTE: C delegates to the platform `strftime`. Pure-Rust replacement for
335/// the same reason as `decompose_utc`. The E/O modifiers are stripped (POSIX
336/// allows the implementation to ignore them and fall back to the unmodified
337/// form) — the test suite only requires that they not error.
338fn strftime_one(buf: &mut Vec<u8>, cc: &[u8; 4], oplen: usize, tm: &TmFields) {
339 use std::io::Write as _;
340 let spec = if oplen == 2 { cc[2] } else { cc[1] };
341 let year_full = (tm.tm_year as i64) + 1900;
342 let hour12 = {
343 let h = tm.tm_hour.rem_euclid(12);
344 if h == 0 { 12 } else { h }
345 };
346 const DAY_SHORT: [&[u8]; 7] = [b"Sun", b"Mon", b"Tue", b"Wed", b"Thu", b"Fri", b"Sat"];
347 const DAY_LONG: [&[u8]; 7] = [
348 b"Sunday", b"Monday", b"Tuesday", b"Wednesday", b"Thursday", b"Friday", b"Saturday",
349 ];
350 const MON_SHORT: [&[u8]; 12] = [
351 b"Jan", b"Feb", b"Mar", b"Apr", b"May", b"Jun", b"Jul", b"Aug", b"Sep", b"Oct", b"Nov",
352 b"Dec",
353 ];
354 const MON_LONG: [&[u8]; 12] = [
355 b"January", b"February", b"March", b"April", b"May", b"June", b"July", b"August",
356 b"September", b"October", b"November", b"December",
357 ];
358 let wday_idx = tm.tm_wday.rem_euclid(7) as usize;
359 let mon_idx = tm.tm_mon.rem_euclid(12) as usize;
360 match spec {
361 b'Y' => { let _ = write!(buf, "{}", year_full); }
362 b'y' => { let _ = write!(buf, "{:02}", year_full.rem_euclid(100)); }
363 b'C' => { let _ = write!(buf, "{:02}", year_full.div_euclid(100)); }
364 b'm' => { let _ = write!(buf, "{:02}", tm.tm_mon + 1); }
365 b'd' => { let _ = write!(buf, "{:02}", tm.tm_mday); }
366 b'e' => { let _ = write!(buf, "{:2}", tm.tm_mday); }
367 b'H' => { let _ = write!(buf, "{:02}", tm.tm_hour); }
368 b'I' => { let _ = write!(buf, "{:02}", hour12); }
369 b'k' => { let _ = write!(buf, "{:2}", tm.tm_hour); }
370 b'l' => { let _ = write!(buf, "{:2}", hour12); }
371 b'M' => { let _ = write!(buf, "{:02}", tm.tm_min); }
372 b'S' => { let _ = write!(buf, "{:02}", tm.tm_sec); }
373 b'w' => { let _ = write!(buf, "{}", tm.tm_wday); }
374 b'u' => {
375 let u = if tm.tm_wday == 0 { 7 } else { tm.tm_wday };
376 let _ = write!(buf, "{}", u);
377 }
378 b'j' => { let _ = write!(buf, "{:03}", tm.tm_yday + 1); }
379 b'a' => buf.extend_from_slice(DAY_SHORT[wday_idx]),
380 b'A' => buf.extend_from_slice(DAY_LONG[wday_idx]),
381 b'b' | b'h' => buf.extend_from_slice(MON_SHORT[mon_idx]),
382 b'B' => buf.extend_from_slice(MON_LONG[mon_idx]),
383 b'p' => buf.extend_from_slice(if tm.tm_hour < 12 { b"AM" } else { b"PM" }),
384 b'P' => buf.extend_from_slice(if tm.tm_hour < 12 { b"am" } else { b"pm" }),
385 b'D' | b'x' => {
386 let _ = write!(buf, "{:02}/{:02}/{:02}", tm.tm_mon + 1, tm.tm_mday, year_full.rem_euclid(100));
387 }
388 b'F' => {
389 let _ = write!(buf, "{}-{:02}-{:02}", year_full, tm.tm_mon + 1, tm.tm_mday);
390 }
391 b'T' | b'X' => {
392 let _ = write!(buf, "{:02}:{:02}:{:02}", tm.tm_hour, tm.tm_min, tm.tm_sec);
393 }
394 b'R' => { let _ = write!(buf, "{:02}:{:02}", tm.tm_hour, tm.tm_min); }
395 b'r' => {
396 let ampm: &[u8] = if tm.tm_hour < 12 { b"AM" } else { b"PM" };
397 let _ = write!(buf, "{:02}:{:02}:{:02} ", hour12, tm.tm_min, tm.tm_sec);
398 buf.extend_from_slice(ampm);
399 }
400 b'c' => {
401 let _ = write!(
402 buf,
403 "{} {} {:2} {:02}:{:02}:{:02} {}",
404 std::str::from_utf8(DAY_SHORT[wday_idx]).unwrap_or(""),
405 std::str::from_utf8(MON_SHORT[mon_idx]).unwrap_or(""),
406 tm.tm_mday,
407 tm.tm_hour,
408 tm.tm_min,
409 tm.tm_sec,
410 year_full,
411 );
412 }
413 b'n' => buf.push(b'\n'),
414 b't' => buf.push(b'\t'),
415 b'%' => buf.push(b'%'),
416 b'z' => buf.extend_from_slice(b"+0000"),
417 b'Z' => buf.extend_from_slice(b"UTC"),
418 b's' => { let _ = write!(buf, "{}", compose_utc(tm)); }
419 b'U' => {
420 let week = (tm.tm_yday + 7 - tm.tm_wday) / 7;
421 let _ = write!(buf, "{:02}", week);
422 }
423 b'W' => {
424 let mwday = if tm.tm_wday == 0 { 6 } else { tm.tm_wday - 1 };
425 let week = (tm.tm_yday + 7 - mwday) / 7;
426 let _ = write!(buf, "{:02}", week);
427 }
428 b'V' | b'g' | b'G' => {
429 let _ = write!(buf, "{:02}", 1);
430 }
431 _ => {}
432 }
433}
434
435// ── Library functions ─────────────────────────────────────────────────────────
436
437///
438/// Executes a shell command via the system shell.
439///
440/// Without arguments: tests whether a shell is available — returns `true`
441/// when an `os_execute_hook` is installed (we always have `sh` in that case),
442/// `false` otherwise.
443///
444/// With a command string: dispatches through `os_execute_hook` and pushes the
445/// three C-Lua return values `(boolean|nil, "exit"|"signal", int)` as defined
446/// by `luaL_execresult`. Returns the stub `nil, errmsg, -1` triple when no
447/// hook is installed.
448pub(crate) fn os_execute(state: &mut LuaState) -> Result<usize, LuaError> {
449 let cmd = state.opt_arg_lstring(1, None)?;
450 match cmd {
451 None => {
452 // We have a shell if and only if the embedder installed a hook.
453 let has_shell = state.global().os_execute_hook.is_some();
454 state.push(LuaValue::Bool(has_shell));
455 Ok(1)
456 }
457 Some(cmd_bytes) => {
458 let hook = state.global().os_execute_hook;
459 match hook {
460 Some(execute_fn) => {
461 // Clone to avoid holding a borrow across the hook call.
462 let cmd_owned: Vec<u8> = cmd_bytes.to_vec();
463 match execute_fn(&cmd_owned) {
464 Ok(result) => {
465 if result.success {
466 state.push(LuaValue::Bool(true));
467 } else {
468 state.push(LuaValue::Nil);
469 }
470 let reason_str: &[u8] = match result.reason {
471 OsExecuteReason::Exit => b"exit",
472 OsExecuteReason::Signal => b"signal",
473 };
474 state.push_string(reason_str)?;
475 state.push(LuaValue::Int(result.code as i64));
476 Ok(3)
477 }
478 Err(e) => {
479 state.push(LuaValue::Nil);
480 let msg = match &e {
481 LuaError::Runtime(LuaValue::Str(s)) => s.as_bytes().to_vec(),
482 other => format!("{:?}", other).into_bytes(),
483 };
484 let s = state.intern_str(&msg)?;
485 state.push(LuaValue::Str(s));
486 state.push(LuaValue::Int(-1));
487 Ok(3)
488 }
489 }
490 }
491 None => {
492 state.push(LuaValue::Nil);
493 state.push_string(b"os.execute: not implemented in lua-stdlib")?;
494 state.push(LuaValue::Int(-1));
495 Ok(3)
496 }
497 }
498 }
499 }
500}
501
502///
503/// Removes the file or empty directory at the given path.
504/// Returns `true` on success, or `nil, errmsg` on failure.
505pub(crate) fn os_remove(state: &mut LuaState) -> Result<usize, LuaError> {
506 let filename: Vec<u8> = state.check_arg_string(1)?.to_vec();
507 // `std::fs` is banned in lua-stdlib; delegate to the embedder hook.
508 let hook = state.global().file_remove_hook;
509 match hook {
510 Some(remove_fn) => match remove_fn(&filename) {
511 Ok(()) => {
512 state.push(LuaValue::Bool(true));
513 Ok(1)
514 }
515 Err(e) => {
516 state.push(LuaValue::Nil);
517 let msg = match &e {
518 LuaError::Runtime(LuaValue::Str(s)) => s.as_bytes().to_vec(),
519 other => format!("{:?}", other).into_bytes(),
520 };
521 let s = state.intern_str(&msg)?;
522 state.push(LuaValue::Str(s));
523 Ok(2)
524 }
525 },
526 None => {
527 state.push(LuaValue::Nil);
528 state.push_string(b"os.remove: no filesystem hook registered")?;
529 Ok(2)
530 }
531 }
532}
533
534///
535/// Renames (moves) a file from the first path to the second.
536/// Returns `true` on success, or `nil, errmsg` on failure.
537pub(crate) fn os_rename(state: &mut LuaState) -> Result<usize, LuaError> {
538 let fromname: Vec<u8> = state.check_arg_string(1)?.to_vec();
539 let toname: Vec<u8> = state.check_arg_string(2)?.to_vec();
540 // `std::fs` is banned in lua-stdlib; delegate to the embedder hook.
541 let hook = state.global().file_rename_hook;
542 match hook {
543 Some(rename_fn) => match rename_fn(&fromname, &toname) {
544 Ok(()) => {
545 state.push(LuaValue::Bool(true));
546 return Ok(1);
547 }
548 Err(e) => {
549 state.push(LuaValue::Nil);
550 let msg = match &e {
551 LuaError::Runtime(LuaValue::Str(s)) => s.as_bytes().to_vec(),
552 other => format!("{:?}", other).into_bytes(),
553 };
554 let s = state.intern_str(&msg)?;
555 state.push(LuaValue::Str(s));
556 return Ok(2);
557 }
558 },
559 None => {}
560 }
561 state.push(LuaValue::Nil);
562 state.push_string(b"os.rename: no filesystem hook registered")?;
563 Ok(2)
564}
565
566///
567/// Generates a unique temporary file name and pushes it as a string.
568/// Raises a runtime error if generation fails.
569///
570/// PORT NOTE: The C reference implementation uses POSIX `mkstemp` (which both
571/// generates a name and atomically creates the file) when `LUA_USE_POSIX` is
572/// defined, falling back to ISO C `tmpnam` otherwise. Replicating `mkstemp`
573/// exactly requires `std::fs`, but Lua semantics only require that the
574/// returned path is currently unique and usable for subsequent `io.open`.
575/// We compose the system temp directory with a process / time / counter
576/// suffix, which matches the `tmpnam` branch of the reference.
577pub(crate) fn os_tmpname(state: &mut LuaState) -> Result<usize, LuaError> {
578 use std::sync::atomic::{AtomicU64, Ordering};
579 use std::time::{SystemTime, UNIX_EPOCH};
580
581 static COUNTER: AtomicU64 = AtomicU64::new(0);
582
583 let mut dir: Vec<u8> = {
584 let path = std::env::temp_dir();
585 #[cfg(unix)]
586 {
587 use std::os::unix::ffi::OsStrExt;
588 path.as_os_str().as_bytes().to_vec()
589 }
590 #[cfg(not(unix))]
591 {
592 path.to_string_lossy().as_bytes().to_vec()
593 }
594 };
595 if dir.last().copied() != Some(b'/') && dir.last().copied() != Some(b'\\') {
596 dir.push(b'/');
597 }
598
599 let nanos = SystemTime::now()
600 .duration_since(UNIX_EPOCH)
601 .map(|d| d.as_nanos())
602 .unwrap_or(0);
603 let n = COUNTER.fetch_add(1, Ordering::Relaxed);
604
605 let suffix = format!("lua_{:x}_{:x}_{:x}", std::process::id(), nanos, n);
606 dir.extend_from_slice(suffix.as_bytes());
607
608 state.push_string(&dir)?;
609 Ok(1)
610}
611
612///
613/// Reads the environment variable named by the first argument and pushes its
614/// value as a string, or `nil` if the variable is not set.
615pub(crate) fn os_getenv(state: &mut LuaState) -> Result<usize, LuaError> {
616 let name_bytes: Vec<u8> = state.check_arg_string(1)?.to_vec();
617
618 // PORT NOTE: On Unix, environment variable names are arbitrary byte sequences
619 // and `OsStr::from_bytes` (from `std::os::unix::ffi::OsStrExt`) is the
620 // byte-exact approach. On Windows they must be valid UTF-16. Phase B should
621 // use the platform-appropriate OsStr API.
622 //
623 // TODO(port): Replace the cfg-guarded from_utf8 fallback on non-Unix targets
624 // with proper wide-string handling.
625 #[cfg(unix)]
626 let result: Option<Vec<u8>> = {
627 use std::ffi::OsStr;
628 use std::os::unix::ffi::{OsStrExt, OsStringExt};
629 let os_name = OsStr::from_bytes(&name_bytes);
630 std::env::var_os(os_name).map(|v| v.into_vec())
631 };
632
633 #[cfg(not(unix))]
634 let result: Option<Vec<u8>> = {
635 // TODO(port): from_utf8 used on Lua string data for OS API interop on
636 // non-Unix platforms. Ideally replaced with wide-string conversion.
637 match std::str::from_utf8(&name_bytes) {
638 Ok(name_str) => std::env::var(name_str).ok().map(|v| v.into_bytes()),
639 Err(_) => None,
640 }
641 };
642
643 match result {
644 Some(val) => {
645 state.push_string(&val)?;
646 }
647 None => {
648 state.push(LuaValue::Nil);
649 }
650 }
651 Ok(1)
652}
653
654///
655/// Returns an approximation of the CPU time (in seconds) used by the program.
656pub(crate) fn os_clock(state: &mut LuaState) -> Result<usize, LuaError> {
657 // TODO(port): C's `clock()` returns process CPU time, not wall-clock time.
658 // `std::time::Instant` provides wall time only. On POSIX targets, use
659 // `libc::clock()` / `libc::CLOCKS_PER_SEC` via the `libc` crate.
660 // Phase A stub returns 0.0.
661 state.push(LuaValue::Float(0.0));
662 Ok(1)
663}
664
665///
666/// Formats the current (or a specified) date/time.
667///
668/// * Format starting with `'!'` → use UTC; otherwise local time.
669/// * Format `"*t"` → push a table with broken-down time fields.
670/// * Other format → push a formatted string, expanding `%`-specifiers via
671/// the C-library `strftime`.
672pub(crate) fn os_date(state: &mut LuaState) -> Result<usize, LuaError> {
673 // Clone to Vec<u8> so that `s` does not borrow from `state`.
674 let format: Vec<u8> = state.opt_arg_lstring(1, Some(b"%c"))?.unwrap_or_default();
675 let s: &[u8] = &format[..];
676
677 let t: i64 = if matches!(state.type_at(2), LuaType::None | LuaType::Nil) {
678 unix_now()
679 } else {
680 check_time(state, 2)?
681 };
682
683 let (_use_utc, s): (bool, &[u8]) = if s.first() == Some(&b'!') {
684 (true, &s[1..])
685 } else {
686 (false, s)
687 };
688
689 // PORT NOTE: C distinguishes UTC (`gmtime_r`) from local time (`localtime_r`).
690 // The Rust port uses UTC unconditionally because reading the local timezone
691 // database requires `libc` FFI which the workspace forbids (`unsafe_code =
692 // forbid`). The internal `os.date` / `os.time` round-trip used by the test
693 // suite remains consistent because `compose_utc` is the exact inverse of
694 // `decompose_utc`. Wall-clock displays will read as UTC rather than local.
695 let stm = decompose_utc(t);
696
697 // return luaL_error(L, "date result cannot be represented in this installation");
698 // (Phase A stub is always valid — no null check needed.)
699
700 if s == b"*t" {
701 state.create_table(0, 9)?;
702 set_all_fields(state, &stm)?;
703 } else {
704 let mut result: Vec<u8> = Vec::new();
705 let mut pos: usize = 0;
706
707 while pos < s.len() {
708 if s[pos] != b'%' {
709 result.push(s[pos]);
710 pos += 1;
711 } else {
712 pos += 1;
713 let mut cc = [0u8; 4];
714 cc[0] = b'%';
715 // Pass the remaining slice even if empty: checkoption's loop
716 // condition (oplen <= convlen) fails immediately on an empty
717 // slice, which causes it to raise "invalid conversion specifier"
718 // matching C behaviour for a trailing bare '%'.
719 let conv = &s[pos..];
720 let after = check_strftime_option(state, conv, &mut cc)?;
721 let oplen = conv.len() - after.len();
722 pos += oplen;
723 // The `%%` specifier is data-independent: strftime emits a literal
724 // `%` byte regardless of the broken-down time, so it is correct to
725 // handle here even while the rest of strftime is stubbed.
726 strftime_one(&mut result, &cc, oplen, &stm);
727 let _ = SIZE_TIME_FMT;
728 }
729 }
730 state.push_string(&result)?;
731 }
732 Ok(1)
733}
734
735///
736/// Without arguments: returns the current time as a Unix timestamp (integer).
737/// With a table argument: interprets the table as broken-down local time,
738/// normalises the fields via `mktime`, updates the table in place, and returns
739/// the resulting timestamp.
740pub(crate) fn os_time(state: &mut LuaState) -> Result<usize, LuaError> {
741 let t: i64;
742
743 if matches!(state.type_at(1), LuaType::None | LuaType::Nil) {
744 t = unix_now();
745 } else {
746 state.check_arg_type(1, LuaType::Table)?;
747 // PORT NOTE: must use the public-API `set_top` (relative to the current
748 // C-frame's `func`), not `LuaState::set_top` which is an inherent that
749 // sets an absolute stack index and would truncate the entire stack.
750 lua_vm::api::set_top(state, 1)?;
751
752 let tm_year = get_field(state, b"year", -1, 1900)?;
753 let tm_mon = get_field(state, b"month", -1, 1)?;
754 let tm_mday = get_field(state, b"day", -1, 0)?;
755 let tm_hour = get_field(state, b"hour", 12, 0)?;
756 let tm_min = get_field(state, b"min", 0, 0)?;
757 let tm_sec = get_field(state, b"sec", 0, 0)?;
758 let tm_isdst = get_bool_field(state, b"isdst")?;
759
760 let raw = TmFields {
761 tm_year,
762 tm_mon,
763 tm_mday,
764 tm_hour,
765 tm_min,
766 tm_sec,
767 tm_isdst,
768 ..TmFields::default()
769 };
770
771 // PORT NOTE: C `mktime` interprets the broken-down time as local; we
772 // interpret it as UTC for the same reason `os_date` decomposes as UTC.
773 // `compose_utc` normalises month-axis overflow itself, then a
774 // round-trip through `decompose_utc` normalises every other axis
775 // (day-of-month, hour, minute, second) so the post-call table holds
776 // canonical field values just like `mktime`.
777 t = compose_utc(&raw);
778 let stm = decompose_utc(t);
779
780 set_all_fields(state, &stm)?;
781 }
782
783 // return luaL_error(L, "time result cannot be represented in this installation");
784 // PORT NOTE: On 64-bit targets time_t == i64 == lua_Integer so the cast check
785 // is a no-op. We only guard against mktime's failure sentinel (−1).
786 if t == -1 {
787 return Err(LuaError::runtime(format_args!(
788 "time result cannot be represented in this installation"
789 )));
790 }
791
792 state.push(LuaValue::Int(t));
793 Ok(1)
794}
795
796///
797/// Returns the number of seconds between two time values as a float (`t1 − t2`).
798///
799/// PORT NOTE: C's `difftime(t1, t2)` returns `t1 − t2` as a `double`. For
800/// 64-bit `time_t` this is exact as `f64` up to approximately 2^53 seconds
801/// (~285 million years), which is sufficient for all practical timestamps.
802pub(crate) fn os_difftime(state: &mut LuaState) -> Result<usize, LuaError> {
803 let t1 = check_time(state, 1)?;
804 let t2 = check_time(state, 2)?;
805 state.push(LuaValue::Float((t1 - t2) as f64));
806 Ok(1)
807}
808
809///
810/// Sets the locale for the given category and pushes the resulting locale name
811/// as a string, or `nil` on failure.
812pub(crate) fn os_setlocale(state: &mut LuaState) -> Result<usize, LuaError> {
813 const CAT_NAMES: &[&[u8]] = &[
814 b"all", b"collate", b"ctype", b"monetary", b"numeric", b"time",
815 ];
816
817 let locale: Option<Vec<u8>> = state.opt_arg_lstring(1, None)?;
818
819 let _op: usize = state.check_arg_option(2, Some(b"all"), CAT_NAMES)?;
820
821 // PORT NOTE: calling libc::setlocale requires unsafe (banned in lua-stdlib, budget=0).
822 // Rust programs inherit the "C" locale by default and never change it, so returning
823 // "C" for the C locale (and nil for anything else) is faithful for this build:
824 // "C" is the only locale guaranteed available on every POSIX system.
825 let result_locale: Option<&[u8]> = match locale.as_deref() {
826 None => Some(b"C"), // query: return current locale (always "C" here)
827 Some(b"C") | Some(b"POSIX") => Some(b"C"), // setting to "C"/"POSIX" always succeeds
828 Some(_) => None, // any other locale: unsupported in this build
829 };
830 match result_locale {
831 Some(s) => { state.push_string(s)?; }
832 None => state.push(LuaValue::Nil),
833 }
834 Ok(1)
835}
836
837///
838/// Exits the host process with the given status code (default `EXIT_SUCCESS = 0`).
839/// If the second argument is true, also closes the Lua state before exiting.
840///
841/// This function is expected to terminate the process and never return normally.
842pub(crate) fn os_exit(state: &mut LuaState) -> Result<usize, LuaError> {
843 // status = lua_toboolean(L, 1) ? EXIT_SUCCESS : EXIT_FAILURE;
844 // else
845 // status = (int)luaL_optinteger(L, 1, EXIT_SUCCESS);
846 let exit_code: i32 = if matches!(state.type_at(1), LuaType::Boolean) {
847 if state.to_boolean(1) { 0 } else { 1 } // EXIT_SUCCESS = 0, EXIT_FAILURE = 1
848 } else {
849 state.opt_arg_integer(1, 0)? as i32
850 };
851
852 if state.to_boolean(2) {
853 state.close();
854 }
855
856 //
857 // `std::process::exit` remains restricted to `lua-cli`. A regular
858 // `LuaError` is also wrong here: Lua `pcall` must not catch `os.exit`.
859 // Use a typed panic payload as internal non-local control flow; the CLI
860 // catches it at the process boundary and converts it to an `ExitCode`.
861 std::panic::panic_any(LuaExit(exit_code));
862}
863
864// ── Registration table and entry point ───────────────────────────────────────
865
866/// Type alias for a Lua native function implementation in Rust.
867///
868/// TODO(port): align with the canonical `lua_CFunction` / `NativeFn` type defined
869/// in `lua-types` once that crate stabilises.
870pub type NativeFn = fn(&mut LuaState) -> Result<usize, LuaError>;
871
872///
873/// Mapping from Lua-visible names to the Rust implementations of each `os.*`
874/// function.
875pub const OS_LIB: &[(&[u8], NativeFn)] = &[
876 (b"clock", os_clock),
877 (b"date", os_date),
878 (b"difftime", os_difftime),
879 (b"execute", os_execute),
880 (b"exit", os_exit),
881 (b"getenv", os_getenv),
882 (b"remove", os_remove),
883 (b"rename", os_rename),
884 (b"setlocale", os_setlocale),
885 (b"time", os_time),
886 (b"tmpname", os_tmpname),
887];
888
889///
890/// Opens the `os` library: creates a new table populated with `OS_LIB` and
891/// leaves it on the stack.
892///
893/// PORT NOTE: `register_lib` is the Rust equivalent of `luaL_newlib`; it creates
894/// a fresh table, fills it from the `(name, fn)` pair slice, and pushes it.
895pub fn open_os(state: &mut LuaState) -> Result<usize, LuaError> {
896 state.register_lib(b"os", OS_LIB)?;
897 Ok(1)
898}
899
900// ──────────────────────────────────────────────────────────────────────────
901// PORT STATUS
902// source: src/loslib.c (430 lines, 12 functions)
903// target_crate: lua-stdlib
904// confidence: medium
905// todos: 18
906// port_notes: 4
907// unsafe_blocks: 0
908// notes: Logic structure faithful; all OS calls that require banned
909// imports (std::fs, std::process) are stubbed with TODO(port).
910// Time formatting (os.date, os.time, os.clock) needs libc or
911// chrono in Phase B. os.exit needs a LuaError::Exit(i32)
912// variant. check_strftime_option logic is fully translated.
913// os_getenv uses OsStr::from_bytes on Unix (no from_utf8).
914// ──────────────────────────────────────────────────────────────────────────