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
114fn 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
145fn 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
154fn 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
167fn 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
180fn 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
385fn os_clock(ctx: NativeCallContext) -> NativeCallResult {
387 ctx.push_number(crate::clock())?;
388 Ok(1)
389}
390
391fn 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
445fn 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
476fn 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 pub unsafe fn open_os(&self) -> NativeCallResult {
494 unsafe { self.register(Some(super::LUA_OSLIB_NAME), &OS_LIB[..])? };
495 Ok(1)
496 }
497}