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