Skip to main content

luau_vm/libs/
os.rs

1use jiff::tz::{Dst, TimeZone};
2use jiff::{Timestamp, Zoned};
3use luau_common::ByteSlice;
4use web_time::{SystemTime, UNIX_EPOCH};
5
6use crate::VmResult;
7use crate::native::{NativeCallContext, NativeCallResult, NativeFunction};
8use crate::thread::{LUA_TNONE, LuaStringBuilder, LuaStringBuilderStorage, Thread};
9use crate::types::{LUA_TNIL, LUA_TTABLE};
10
11const LUA_STRFTIME_OPTIONS: &[u8] = b"aAbBcdHIjmMpSUwWxXyYzZ%";
12const WEEKDAY_ABBREVIATED_NAMES: [&[u8]; 7] =
13    [b"Sun", b"Mon", b"Tue", b"Wed", b"Thu", b"Fri", b"Sat"];
14const WEEKDAY_FULL_NAMES: [&[u8]; 7] = [
15    b"Sunday",
16    b"Monday",
17    b"Tuesday",
18    b"Wednesday",
19    b"Thursday",
20    b"Friday",
21    b"Saturday",
22];
23const MONTH_ABBREVIATED_NAMES: [&[u8]; 12] = [
24    b"Jan", b"Feb", b"Mar", b"Apr", b"May", b"Jun", b"Jul", b"Aug", b"Sep", b"Oct", b"Nov", b"Dec",
25];
26const MONTH_FULL_NAMES: [&[u8]; 12] = [
27    b"January",
28    b"February",
29    b"March",
30    b"April",
31    b"May",
32    b"June",
33    b"July",
34    b"August",
35    b"September",
36    b"October",
37    b"November",
38    b"December",
39];
40
41static OS_LIB: [NativeFunction; 4] = [
42    NativeFunction {
43        name: "clock",
44        function: os_clock,
45    },
46    NativeFunction {
47        name: "date",
48        function: os_date,
49    },
50    NativeFunction {
51        name: "difftime",
52        function: os_difftime,
53    },
54    NativeFunction {
55        name: "time",
56        function: os_time,
57    },
58];
59
60#[derive(Clone, Copy)]
61struct DateInput {
62    sec: i32,
63    min: i32,
64    hour: i32,
65    day: i32,
66    month_zero_based: i32,
67    year: i32,
68}
69
70#[derive(Clone, Copy)]
71struct DateParts {
72    sec: i32,
73    min: i32,
74    hour: i32,
75    day: i32,
76    month: i32,
77    year: i32,
78    wday: i32,
79    yday: i32,
80    isdst: i32,
81    utc_offset: i32,
82}
83
84impl DateParts {
85    fn from_zoned(zoned: &Zoned, utc_offset: i32, isdst: i32) -> Self {
86        Self {
87            sec: zoned.second() as i32,
88            min: zoned.minute() as i32,
89            hour: zoned.hour() as i32,
90            day: zoned.day() as i32,
91            month: zoned.month() as i32,
92            year: zoned.year() as i32,
93            wday: zoned.weekday().to_sunday_zero_offset() as i32,
94            yday: zoned.day_of_year() as i32,
95            isdst,
96            utc_offset,
97        }
98    }
99}
100
101fn truncate_at_nul(bytes: &[u8]) -> &[u8] {
102    let len = bytes
103        .iter()
104        .position(|&byte| byte == b'\0')
105        .unwrap_or(bytes.len());
106    &bytes[..len]
107}
108
109fn current_time_seconds() -> Option<i64> {
110    let duration = SystemTime::now().duration_since(UNIX_EPOCH).ok()?;
111    i64::try_from(duration.as_secs()).ok()
112}
113
114/// `os_timegm`
115fn os_timegm(time: DateInput) -> Option<i64> {
116    let day = time.day as i64;
117    let month = time.month_zero_based as i64 + 1;
118    let year = time.year as i64;
119
120    let mut a = if time.month_zero_based % 12 < 2 { 1 } else { 0 };
121    a -= time.month_zero_based / 12;
122
123    let y = year + 4800 - a as i64;
124    let m = month + 12 * a as i64 - 3;
125
126    let julian_day = day + ((153 * m + 2) / 5) + 365 * y + (y / 4) - (y / 100) + (y / 400) - 32045;
127
128    const UTC_START_AS_JULIAN_DAY: i64 = 2_440_588;
129    const UTC_START_AS_JULIAN_SECOND: i64 = UTC_START_AS_JULIAN_DAY * 86_400;
130
131    if julian_day < UTC_START_AS_JULIAN_DAY {
132        return None;
133    }
134
135    let day_second = time.hour as i64 * 3600 + time.min as i64 * 60 + time.sec as i64;
136    let julian_seconds = julian_day * 86_400 + day_second;
137
138    if julian_seconds < UTC_START_AS_JULIAN_SECOND {
139        return None;
140    }
141
142    Some(julian_seconds - UTC_START_AS_JULIAN_SECOND)
143}
144
145/// `setfield`
146fn set_field(thread: &Thread, key: &[u8], value: i32) -> VmResult {
147    unsafe {
148        thread.push_integer(value)?;
149        thread.raw_set_field(-2, key)?;
150    }
151    Ok(())
152}
153
154/// `setboolfield`
155fn set_bool_field(thread: &Thread, key: &[u8], value: i32) -> VmResult {
156    if value < 0 {
157        return Ok(());
158    }
159
160    unsafe {
161        thread.push_boolean(value)?;
162        thread.raw_set_field(-2, key)?;
163    }
164    Ok(())
165}
166
167/// `getboolfield`
168fn get_bool_field(thread: &Thread, key: &[u8]) -> VmResult<i32> {
169    let value = unsafe {
170        if thread.raw_get_field(-1, key)? == LUA_TNIL {
171            -1
172        } else {
173            thread.to_boolean(-1)
174        }
175    };
176    unsafe { thread.pop(1) };
177    Ok(value)
178}
179
180/// `getfield`
181fn get_field(thread: &Thread, key: &[u8], default: i32) -> VmResult<i32> {
182    let value = unsafe {
183        thread.raw_get_field(-1, key)?;
184        if thread.is_number(-1) != 0 {
185            thread.to_integer(-1).unwrap_or(0)
186        } else {
187            if default < 0 {
188                return crate::error!(thread, "field '%s' missing in date table", key)
189                    .map_err(Into::into);
190            }
191            default
192        }
193    };
194    unsafe { thread.pop(1) };
195    Ok(value)
196}
197
198unsafe fn push_unsigned(
199    buffer: &mut LuaStringBuilder<'_, '_>,
200    mut value: u32,
201    width: usize,
202    pad: u8,
203) -> VmResult {
204    let mut storage = [0u8; 10];
205    let mut index = storage.len();
206
207    loop {
208        index -= 1;
209        storage[index] = b'0' + (value % 10) as u8;
210        value /= 10;
211        if value == 0 {
212            break;
213        }
214    }
215
216    for _ in storage[index..].len()..width {
217        unsafe { buffer.push_byte(pad)? };
218    }
219    unsafe { buffer.push_bytes(&storage[index..])? };
220    Ok(())
221}
222
223unsafe fn push_signed(buffer: &mut LuaStringBuilder<'_, '_>, value: i32) -> VmResult {
224    if value < 0 {
225        unsafe { buffer.push_byte(b'-')? };
226        unsafe { push_unsigned(buffer, (-(value as i64)) as u32, 0, b'0') }
227    } else {
228        unsafe { push_unsigned(buffer, value as u32, 0, b'0') }
229    }
230}
231
232unsafe fn push_year(buffer: &mut LuaStringBuilder<'_, '_>, year: i32) -> VmResult {
233    if (0..=9999).contains(&year) {
234        unsafe { push_unsigned(buffer, year as u32, 4, b'0') }
235    } else {
236        unsafe { push_signed(buffer, year) }
237    }
238}
239
240unsafe fn push_time(buffer: &mut LuaStringBuilder<'_, '_>, date: &DateParts) -> VmResult {
241    unsafe {
242        push_unsigned(buffer, date.hour as u32, 2, b'0')?;
243        buffer.push_byte(b':')?;
244        push_unsigned(buffer, date.min as u32, 2, b'0')?;
245        buffer.push_byte(b':')?;
246        push_unsigned(buffer, date.sec as u32, 2, b'0')
247    }
248}
249
250unsafe fn push_posix_date(buffer: &mut LuaStringBuilder<'_, '_>, date: &DateParts) -> VmResult {
251    unsafe {
252        push_unsigned(buffer, date.month as u32, 2, b'0')?;
253        buffer.push_byte(b'/')?;
254        push_unsigned(buffer, date.day as u32, 2, b'0')?;
255        buffer.push_byte(b'/')?;
256        push_unsigned(buffer, date.year.rem_euclid(100) as u32, 2, b'0')
257    }
258}
259
260unsafe fn push_posix_date_time(
261    buffer: &mut LuaStringBuilder<'_, '_>,
262    date: &DateParts,
263) -> VmResult {
264    unsafe {
265        buffer.push_bytes(WEEKDAY_ABBREVIATED_NAMES[date.wday as usize])?;
266        buffer.push_byte(b' ')?;
267        buffer.push_bytes(MONTH_ABBREVIATED_NAMES[(date.month - 1) as usize])?;
268        buffer.push_byte(b' ')?;
269        push_unsigned(buffer, date.day as u32, 2, b' ')?;
270        buffer.push_byte(b' ')?;
271        push_time(buffer, date)?;
272        buffer.push_byte(b' ')?;
273        push_year(buffer, date.year)
274    }
275}
276
277unsafe fn push_utc_offset(buffer: &mut LuaStringBuilder<'_, '_>, offset: i32) -> VmResult {
278    let sign = if offset < 0 { b'-' } else { b'+' };
279    let minutes = offset.abs() / 60;
280    unsafe {
281        buffer.push_byte(sign)?;
282        push_unsigned(buffer, (minutes / 60) as u32, 2, b'0')?;
283        push_unsigned(buffer, (minutes % 60) as u32, 2, b'0')
284    }
285}
286
287unsafe fn push_date_spec(
288    buffer: &mut LuaStringBuilder<'_, '_>,
289    date: &DateParts,
290    zone_name: &[u8],
291    spec: u8,
292) -> VmResult {
293    unsafe {
294        match spec {
295            b'a' => buffer.push_bytes(WEEKDAY_ABBREVIATED_NAMES[date.wday as usize])?,
296            b'A' => buffer.push_bytes(WEEKDAY_FULL_NAMES[date.wday as usize])?,
297            b'b' => buffer.push_bytes(MONTH_ABBREVIATED_NAMES[(date.month - 1) as usize])?,
298            b'B' => buffer.push_bytes(MONTH_FULL_NAMES[(date.month - 1) as usize])?,
299            b'c' => push_posix_date_time(buffer, date)?,
300            b'd' => push_unsigned(buffer, date.day as u32, 2, b'0')?,
301            b'H' => push_unsigned(buffer, date.hour as u32, 2, b'0')?,
302            b'I' => {
303                let hour = match date.hour % 12 {
304                    0 => 12,
305                    hour => hour,
306                };
307                push_unsigned(buffer, hour as u32, 2, b'0')?;
308            }
309            b'j' => push_unsigned(buffer, date.yday as u32, 3, b'0')?,
310            b'm' => push_unsigned(buffer, date.month as u32, 2, b'0')?,
311            b'M' => push_unsigned(buffer, date.min as u32, 2, b'0')?,
312            b'p' if date.hour < 12 => buffer.push_bytes(b"AM")?,
313            b'p' => buffer.push_bytes(b"PM")?,
314            b'S' => push_unsigned(buffer, date.sec as u32, 2, b'0')?,
315            b'U' => push_unsigned(buffer, ((date.yday + 6 - date.wday) / 7) as u32, 2, b'0')?,
316            b'W' => push_unsigned(
317                buffer,
318                ((date.yday + 6 - ((date.wday + 6) % 7)) / 7) as u32,
319                2,
320                b'0',
321            )?,
322            b'w' => push_unsigned(buffer, date.wday as u32, 1, b'0')?,
323            b'x' => push_posix_date(buffer, date)?,
324            b'X' => push_time(buffer, date)?,
325            b'y' => push_unsigned(buffer, date.year.rem_euclid(100) as u32, 2, b'0')?,
326            b'Y' => push_year(buffer, date.year)?,
327            b'z' => push_utc_offset(buffer, date.utc_offset)?,
328            b'Z' => buffer.push_bytes(zone_name)?,
329            b'%' => buffer.push_byte(b'%')?,
330            _ => unreachable!("validated strftime specifier"),
331        }
332    }
333    Ok(())
334}
335
336unsafe fn push_date_result(
337    thread: &Thread,
338    format: &[u8],
339    date: &DateParts,
340    zone_name: &[u8],
341) -> VmResult {
342    unsafe {
343        if format == b"*t" {
344            thread.create_table(0, 9)?;
345            set_field(thread, b"sec", date.sec)?;
346            set_field(thread, b"min", date.min)?;
347            set_field(thread, b"hour", date.hour)?;
348            set_field(thread, b"day", date.day)?;
349            set_field(thread, b"month", date.month)?;
350            set_field(thread, b"year", date.year)?;
351            set_field(thread, b"wday", date.wday + 1)?;
352            set_field(thread, b"yday", date.yday)?;
353            set_bool_field(thread, b"isdst", date.isdst)?;
354            return Ok(());
355        }
356
357        let mut buffer_storage = LuaStringBuilderStorage::uninit();
358        let mut buffer = LuaStringBuilder::new(thread, &mut buffer_storage);
359
360        let mut index = 0;
361        while index < format.len() {
362            let byte = format[index];
363            if byte != b'%' || index + 1 == format.len() {
364                buffer.push_byte(byte)?;
365                index += 1;
366                continue;
367            }
368
369            let spec = format[index + 1];
370            if !LUA_STRFTIME_OPTIONS.contains(&spec) {
371                return thread
372                    .lua_arg_error(1, "invalid conversion specifier")
373                    .map_err(Into::into);
374            }
375
376            push_date_spec(&mut buffer, date, zone_name, spec)?;
377            index += 2;
378        }
379
380        buffer.finish()?;
381    }
382    Ok(())
383}
384
385/// `os_clock`
386fn os_clock(ctx: NativeCallContext) -> NativeCallResult {
387    ctx.push_number(crate::clock())?;
388    Ok(1)
389}
390
391/// `os_date`
392fn os_date(ctx: NativeCallContext) -> NativeCallResult {
393    let thread = ctx.raw_thread();
394    unsafe {
395        let (mut format, time) = {
396            let format = truncate_at_nul(thread.opt_string(1)?.unwrap_or(b"%c".as_bstr()));
397            let time = if matches!(thread.type_of(2), LUA_TNONE | LUA_TNIL) {
398                current_time_seconds()
399            } else {
400                Some(thread.check_number(2)? as i64)
401            };
402
403            (format, time)
404        };
405
406        let Some(time) = time else {
407            thread.push_nil()?;
408            return Ok(1);
409        };
410
411        let utc = if matches!(format.first(), Some(b'!')) {
412            format = &format[1..];
413            true
414        } else {
415            false
416        };
417
418        let Some(timestamp) = Timestamp::from_second(time).ok() else {
419            thread.push_nil()?;
420            return Ok(1);
421        };
422
423        if utc {
424            let zoned = timestamp.to_zoned(TimeZone::UTC);
425            let date = DateParts::from_zoned(&zoned, 0, 0);
426            push_date_result(thread, format, &date, b"UTC")?;
427        } else {
428            if time < 0 {
429                thread.push_nil()?;
430                return Ok(1);
431            }
432
433            let timezone = TimeZone::system();
434            let info = timezone.to_offset_info(timestamp);
435            let zoned = timestamp.to_zoned(timezone.clone());
436            let isdst = i32::from(matches!(info.dst(), Dst::Yes));
437            let date = DateParts::from_zoned(&zoned, info.offset().seconds(), isdst);
438            push_date_result(thread, format, &date, info.abbreviation().as_bytes())?;
439        }
440    }
441
442    Ok(1)
443}
444
445/// `os_time`
446fn os_time(ctx: NativeCallContext) -> NativeCallResult {
447    let thread = ctx.raw_thread();
448    unsafe {
449        let time = if matches!(thread.type_of(1), LUA_TNONE | LUA_TNIL) {
450            current_time_seconds()
451        } else {
452            thread.check_type(1, LUA_TTABLE)?;
453            thread.set_top(1)?;
454
455            let time = DateInput {
456                sec: get_field(thread, b"sec", 0)?,
457                min: get_field(thread, b"min", 0)?,
458                hour: get_field(thread, b"hour", 12)?,
459                day: get_field(thread, b"day", -1)?,
460                month_zero_based: get_field(thread, b"month", -1)? - 1,
461                year: get_field(thread, b"year", -1)?,
462            };
463            let _isdst = get_bool_field(thread, b"isdst")?;
464
465            os_timegm(time)
466        };
467
468        match time {
469            Some(time) => thread.push_number(time as f64)?,
470            None => thread.push_nil()?,
471        }
472        Ok(1)
473    }
474}
475
476/// `os_difftime`
477fn os_difftime(ctx: NativeCallContext) -> NativeCallResult {
478    let thread = ctx.raw_thread();
479    let left = ctx.arg(1).number()?;
480    let right = unsafe {
481        if matches!(thread.type_of(2), LUA_TNONE | LUA_TNIL) {
482            0.0
483        } else {
484            ctx.arg(2).number()?
485        }
486    };
487    ctx.push_number(left - right)?;
488    Ok(1)
489}
490
491impl Thread {
492    /// `luaopen_os`
493    pub unsafe fn open_os(&self) -> NativeCallResult {
494        unsafe { self.register(Some(super::LUA_OSLIB_NAME), &OS_LIB[..])? };
495        Ok(1)
496    }
497}