use jiff::tz::{Dst, TimeZone};
use jiff::{Timestamp, Zoned};
use luau_common::ByteSlice;
use web_time::{SystemTime, UNIX_EPOCH};
use crate::VmResult;
use crate::native::{NativeCallContext, NativeCallResult, NativeFunction};
use crate::thread::{LUA_TNONE, LuaStringBuilder, LuaStringBuilderStorage, Thread};
use crate::types::{LUA_TNIL, LUA_TTABLE};
const LUA_STRFTIME_OPTIONS: &[u8] = b"aAbBcdHIjmMpSUwWxXyYzZ%";
const WEEKDAY_ABBREVIATED_NAMES: [&[u8]; 7] =
[b"Sun", b"Mon", b"Tue", b"Wed", b"Thu", b"Fri", b"Sat"];
const WEEKDAY_FULL_NAMES: [&[u8]; 7] = [
b"Sunday",
b"Monday",
b"Tuesday",
b"Wednesday",
b"Thursday",
b"Friday",
b"Saturday",
];
const MONTH_ABBREVIATED_NAMES: [&[u8]; 12] = [
b"Jan", b"Feb", b"Mar", b"Apr", b"May", b"Jun", b"Jul", b"Aug", b"Sep", b"Oct", b"Nov", b"Dec",
];
const MONTH_FULL_NAMES: [&[u8]; 12] = [
b"January",
b"February",
b"March",
b"April",
b"May",
b"June",
b"July",
b"August",
b"September",
b"October",
b"November",
b"December",
];
static OS_LIB: [NativeFunction; 4] = [
NativeFunction {
name: "clock",
function: os_clock,
},
NativeFunction {
name: "date",
function: os_date,
},
NativeFunction {
name: "difftime",
function: os_difftime,
},
NativeFunction {
name: "time",
function: os_time,
},
];
#[derive(Clone, Copy)]
struct DateInput {
sec: i32,
min: i32,
hour: i32,
day: i32,
month_zero_based: i32,
year: i32,
}
#[derive(Clone, Copy)]
struct DateParts {
sec: i32,
min: i32,
hour: i32,
day: i32,
month: i32,
year: i32,
wday: i32,
yday: i32,
isdst: i32,
utc_offset: i32,
}
impl DateParts {
fn from_zoned(zoned: &Zoned, utc_offset: i32, isdst: i32) -> Self {
Self {
sec: zoned.second() as i32,
min: zoned.minute() as i32,
hour: zoned.hour() as i32,
day: zoned.day() as i32,
month: zoned.month() as i32,
year: zoned.year() as i32,
wday: zoned.weekday().to_sunday_zero_offset() as i32,
yday: zoned.day_of_year() as i32,
isdst,
utc_offset,
}
}
}
fn truncate_at_nul(bytes: &[u8]) -> &[u8] {
let len = bytes
.iter()
.position(|&byte| byte == b'\0')
.unwrap_or(bytes.len());
&bytes[..len]
}
fn current_time_seconds() -> Option<i64> {
let duration = SystemTime::now().duration_since(UNIX_EPOCH).ok()?;
i64::try_from(duration.as_secs()).ok()
}
fn os_timegm(time: DateInput) -> Option<i64> {
let day = time.day as i64;
let month = time.month_zero_based as i64 + 1;
let year = time.year as i64;
let mut a = if time.month_zero_based % 12 < 2 { 1 } else { 0 };
a -= time.month_zero_based / 12;
let y = year + 4800 - a as i64;
let m = month + 12 * a as i64 - 3;
let julian_day = day + ((153 * m + 2) / 5) + 365 * y + (y / 4) - (y / 100) + (y / 400) - 32045;
const UTC_START_AS_JULIAN_DAY: i64 = 2_440_588;
const UTC_START_AS_JULIAN_SECOND: i64 = UTC_START_AS_JULIAN_DAY * 86_400;
if julian_day < UTC_START_AS_JULIAN_DAY {
return None;
}
let day_second = time.hour as i64 * 3600 + time.min as i64 * 60 + time.sec as i64;
let julian_seconds = julian_day * 86_400 + day_second;
if julian_seconds < UTC_START_AS_JULIAN_SECOND {
return None;
}
Some(julian_seconds - UTC_START_AS_JULIAN_SECOND)
}
fn set_field(thread: &Thread, key: &[u8], value: i32) -> VmResult {
unsafe {
thread.push_integer(value)?;
thread.raw_set_field(-2, key)?;
}
Ok(())
}
fn set_bool_field(thread: &Thread, key: &[u8], value: i32) -> VmResult {
if value < 0 {
return Ok(());
}
unsafe {
thread.push_boolean(value)?;
thread.raw_set_field(-2, key)?;
}
Ok(())
}
fn get_bool_field(thread: &Thread, key: &[u8]) -> VmResult<i32> {
let value = unsafe {
if thread.raw_get_field(-1, key)? == LUA_TNIL {
-1
} else {
thread.to_boolean(-1)
}
};
unsafe { thread.pop(1) };
Ok(value)
}
fn get_field(thread: &Thread, key: &[u8], default: i32) -> VmResult<i32> {
let value = unsafe {
thread.raw_get_field(-1, key)?;
if thread.is_number(-1) != 0 {
thread.to_integer(-1).unwrap_or(0)
} else {
if default < 0 {
return crate::error!(thread, "field '%s' missing in date table", key)
.map_err(Into::into);
}
default
}
};
unsafe { thread.pop(1) };
Ok(value)
}
unsafe fn push_unsigned(
buffer: &mut LuaStringBuilder<'_, '_>,
mut value: u32,
width: usize,
pad: u8,
) -> VmResult {
let mut storage = [0u8; 10];
let mut index = storage.len();
loop {
index -= 1;
storage[index] = b'0' + (value % 10) as u8;
value /= 10;
if value == 0 {
break;
}
}
for _ in storage[index..].len()..width {
unsafe { buffer.push_byte(pad)? };
}
unsafe { buffer.push_bytes(&storage[index..])? };
Ok(())
}
unsafe fn push_signed(buffer: &mut LuaStringBuilder<'_, '_>, value: i32) -> VmResult {
if value < 0 {
unsafe { buffer.push_byte(b'-')? };
unsafe { push_unsigned(buffer, (-(value as i64)) as u32, 0, b'0') }
} else {
unsafe { push_unsigned(buffer, value as u32, 0, b'0') }
}
}
unsafe fn push_year(buffer: &mut LuaStringBuilder<'_, '_>, year: i32) -> VmResult {
if (0..=9999).contains(&year) {
unsafe { push_unsigned(buffer, year as u32, 4, b'0') }
} else {
unsafe { push_signed(buffer, year) }
}
}
unsafe fn push_time(buffer: &mut LuaStringBuilder<'_, '_>, date: &DateParts) -> VmResult {
unsafe {
push_unsigned(buffer, date.hour as u32, 2, b'0')?;
buffer.push_byte(b':')?;
push_unsigned(buffer, date.min as u32, 2, b'0')?;
buffer.push_byte(b':')?;
push_unsigned(buffer, date.sec as u32, 2, b'0')
}
}
unsafe fn push_posix_date(buffer: &mut LuaStringBuilder<'_, '_>, date: &DateParts) -> VmResult {
unsafe {
push_unsigned(buffer, date.month as u32, 2, b'0')?;
buffer.push_byte(b'/')?;
push_unsigned(buffer, date.day as u32, 2, b'0')?;
buffer.push_byte(b'/')?;
push_unsigned(buffer, date.year.rem_euclid(100) as u32, 2, b'0')
}
}
unsafe fn push_posix_date_time(
buffer: &mut LuaStringBuilder<'_, '_>,
date: &DateParts,
) -> VmResult {
unsafe {
buffer.push_bytes(WEEKDAY_ABBREVIATED_NAMES[date.wday as usize])?;
buffer.push_byte(b' ')?;
buffer.push_bytes(MONTH_ABBREVIATED_NAMES[(date.month - 1) as usize])?;
buffer.push_byte(b' ')?;
push_unsigned(buffer, date.day as u32, 2, b' ')?;
buffer.push_byte(b' ')?;
push_time(buffer, date)?;
buffer.push_byte(b' ')?;
push_year(buffer, date.year)
}
}
unsafe fn push_utc_offset(buffer: &mut LuaStringBuilder<'_, '_>, offset: i32) -> VmResult {
let sign = if offset < 0 { b'-' } else { b'+' };
let minutes = offset.abs() / 60;
unsafe {
buffer.push_byte(sign)?;
push_unsigned(buffer, (minutes / 60) as u32, 2, b'0')?;
push_unsigned(buffer, (minutes % 60) as u32, 2, b'0')
}
}
unsafe fn push_date_spec(
buffer: &mut LuaStringBuilder<'_, '_>,
date: &DateParts,
zone_name: &[u8],
spec: u8,
) -> VmResult {
unsafe {
match spec {
b'a' => buffer.push_bytes(WEEKDAY_ABBREVIATED_NAMES[date.wday as usize])?,
b'A' => buffer.push_bytes(WEEKDAY_FULL_NAMES[date.wday as usize])?,
b'b' => buffer.push_bytes(MONTH_ABBREVIATED_NAMES[(date.month - 1) as usize])?,
b'B' => buffer.push_bytes(MONTH_FULL_NAMES[(date.month - 1) as usize])?,
b'c' => push_posix_date_time(buffer, date)?,
b'd' => push_unsigned(buffer, date.day as u32, 2, b'0')?,
b'H' => push_unsigned(buffer, date.hour as u32, 2, b'0')?,
b'I' => {
let hour = match date.hour % 12 {
0 => 12,
hour => hour,
};
push_unsigned(buffer, hour as u32, 2, b'0')?;
}
b'j' => push_unsigned(buffer, date.yday as u32, 3, b'0')?,
b'm' => push_unsigned(buffer, date.month as u32, 2, b'0')?,
b'M' => push_unsigned(buffer, date.min as u32, 2, b'0')?,
b'p' if date.hour < 12 => buffer.push_bytes(b"AM")?,
b'p' => buffer.push_bytes(b"PM")?,
b'S' => push_unsigned(buffer, date.sec as u32, 2, b'0')?,
b'U' => push_unsigned(buffer, ((date.yday + 6 - date.wday) / 7) as u32, 2, b'0')?,
b'W' => push_unsigned(
buffer,
((date.yday + 6 - ((date.wday + 6) % 7)) / 7) as u32,
2,
b'0',
)?,
b'w' => push_unsigned(buffer, date.wday as u32, 1, b'0')?,
b'x' => push_posix_date(buffer, date)?,
b'X' => push_time(buffer, date)?,
b'y' => push_unsigned(buffer, date.year.rem_euclid(100) as u32, 2, b'0')?,
b'Y' => push_year(buffer, date.year)?,
b'z' => push_utc_offset(buffer, date.utc_offset)?,
b'Z' => buffer.push_bytes(zone_name)?,
b'%' => buffer.push_byte(b'%')?,
_ => unreachable!("validated strftime specifier"),
}
}
Ok(())
}
unsafe fn push_date_result(
thread: &Thread,
format: &[u8],
date: &DateParts,
zone_name: &[u8],
) -> VmResult {
unsafe {
if format == b"*t" {
thread.create_table(0, 9)?;
set_field(thread, b"sec", date.sec)?;
set_field(thread, b"min", date.min)?;
set_field(thread, b"hour", date.hour)?;
set_field(thread, b"day", date.day)?;
set_field(thread, b"month", date.month)?;
set_field(thread, b"year", date.year)?;
set_field(thread, b"wday", date.wday + 1)?;
set_field(thread, b"yday", date.yday)?;
set_bool_field(thread, b"isdst", date.isdst)?;
return Ok(());
}
let mut buffer_storage = LuaStringBuilderStorage::uninit();
let mut buffer = LuaStringBuilder::new(thread, &mut buffer_storage);
let mut index = 0;
while index < format.len() {
let byte = format[index];
if byte != b'%' || index + 1 == format.len() {
buffer.push_byte(byte)?;
index += 1;
continue;
}
let spec = format[index + 1];
if !LUA_STRFTIME_OPTIONS.contains(&spec) {
return thread
.lua_arg_error(1, "invalid conversion specifier")
.map_err(Into::into);
}
push_date_spec(&mut buffer, date, zone_name, spec)?;
index += 2;
}
buffer.finish()?;
}
Ok(())
}
fn os_clock(ctx: NativeCallContext) -> NativeCallResult {
ctx.push_number(crate::clock())?;
Ok(1)
}
fn os_date(ctx: NativeCallContext) -> NativeCallResult {
let thread = ctx.raw_thread();
unsafe {
let (mut format, time) = {
let format = truncate_at_nul(thread.opt_string(1)?.unwrap_or(b"%c".as_bstr()));
let time = if matches!(thread.type_of(2), LUA_TNONE | LUA_TNIL) {
current_time_seconds()
} else {
Some(thread.check_number(2)? as i64)
};
(format, time)
};
let Some(time) = time else {
thread.push_nil()?;
return Ok(1);
};
let utc = if matches!(format.first(), Some(b'!')) {
format = &format[1..];
true
} else {
false
};
let Some(timestamp) = Timestamp::from_second(time).ok() else {
thread.push_nil()?;
return Ok(1);
};
if utc {
let zoned = timestamp.to_zoned(TimeZone::UTC);
let date = DateParts::from_zoned(&zoned, 0, 0);
push_date_result(thread, format, &date, b"UTC")?;
} else {
if time < 0 {
thread.push_nil()?;
return Ok(1);
}
let timezone = TimeZone::system();
let info = timezone.to_offset_info(timestamp);
let zoned = timestamp.to_zoned(timezone.clone());
let isdst = i32::from(matches!(info.dst(), Dst::Yes));
let date = DateParts::from_zoned(&zoned, info.offset().seconds(), isdst);
push_date_result(thread, format, &date, info.abbreviation().as_bytes())?;
}
}
Ok(1)
}
fn os_time(ctx: NativeCallContext) -> NativeCallResult {
let thread = ctx.raw_thread();
unsafe {
let time = if matches!(thread.type_of(1), LUA_TNONE | LUA_TNIL) {
current_time_seconds()
} else {
thread.check_type(1, LUA_TTABLE)?;
thread.set_top(1)?;
let time = DateInput {
sec: get_field(thread, b"sec", 0)?,
min: get_field(thread, b"min", 0)?,
hour: get_field(thread, b"hour", 12)?,
day: get_field(thread, b"day", -1)?,
month_zero_based: get_field(thread, b"month", -1)? - 1,
year: get_field(thread, b"year", -1)?,
};
let _isdst = get_bool_field(thread, b"isdst")?;
os_timegm(time)
};
match time {
Some(time) => thread.push_number(time as f64)?,
None => thread.push_nil()?,
}
Ok(1)
}
}
fn os_difftime(ctx: NativeCallContext) -> NativeCallResult {
let thread = ctx.raw_thread();
let left = ctx.arg(1).number()?;
let right = unsafe {
if matches!(thread.type_of(2), LUA_TNONE | LUA_TNIL) {
0.0
} else {
ctx.arg(2).number()?
}
};
ctx.push_number(left - right)?;
Ok(1)
}
impl Thread {
pub unsafe fn open_os(&self) -> NativeCallResult {
unsafe { self.register(Some(super::LUA_OSLIB_NAME), &OS_LIB[..])? };
Ok(1)
}
}