use crate::constants;
#[derive(Default)]
pub(crate) struct Writer {
buf: Vec<u8>,
}
impl Writer {
pub(crate) fn new() -> Self {
Writer { buf: Vec::new() }
}
pub(crate) fn u8(&mut self, v: u8) -> &mut Self {
self.buf.push(v);
self
}
pub(crate) fn u16(&mut self, v: u16) -> &mut Self {
self.buf.extend_from_slice(&v.to_le_bytes());
self
}
pub(crate) fn u32(&mut self, v: u32) -> &mut Self {
self.buf.extend_from_slice(&v.to_le_bytes());
self
}
pub(crate) fn pad(&mut self, n: usize) -> &mut Self {
self.buf.resize(self.buf.len() + n, 0);
self
}
pub(crate) fn bytes(&mut self, b: &[u8]) -> &mut Self {
self.buf.extend_from_slice(b);
self
}
pub(crate) fn fixed(&mut self, src: &[u8], width: usize) -> &mut Self {
let take = src.len().min(width);
self.buf.extend_from_slice(&src[..take]);
self.buf.resize(self.buf.len() + (width - take), 0);
self
}
pub(crate) fn finish(self) -> Vec<u8> {
self.buf
}
}
pub(crate) fn wr(f: impl FnOnce(&mut Writer)) -> Vec<u8> {
let mut w = Writer::new();
f(&mut w);
w.finish()
}
pub(crate) fn u16_le(b: &[u8]) -> u16 {
u16::from_le_bytes([b[0], b[1]])
}
pub(crate) fn u32_le(b: &[u8]) -> u32 {
u32::from_le_bytes([b[0], b[1], b[2], b[3]])
}
pub(crate) fn checksum(payload: &[u8]) -> u16 {
let mut sum: u32 = 0;
let mut i = 0;
let n = payload.len();
while i + 1 < n {
sum += u16::from_le_bytes([payload[i], payload[i + 1]]) as u32;
if sum > constants::USHRT_MAX as u32 {
sum -= constants::USHRT_MAX as u32;
}
i += 2;
}
if i < n {
sum += payload[n - 1] as u32;
}
while sum > constants::USHRT_MAX as u32 {
sum -= constants::USHRT_MAX as u32;
}
let mut chk: i64 = !(sum as i64);
while chk < 0 {
chk += constants::USHRT_MAX as i64;
}
chk as u16
}
pub(crate) fn create_header(
command: u16,
command_string: &[u8],
session_id: u16,
reply_id: u16,
) -> Vec<u8> {
let zeroed = wr(|w| {
w.u16(command)
.u16(0) .u16(session_id)
.u16(reply_id)
.bytes(command_string);
});
let chk = checksum(&zeroed);
let rid = next_reply_id(reply_id);
wr(|w| {
w.u16(command)
.u16(chk)
.u16(session_id)
.u16(rid)
.bytes(command_string);
})
}
pub(crate) fn next_reply_id(reply_id: u16) -> u16 {
let mut next = reply_id as u32 + 1;
if next >= constants::USHRT_MAX as u32 {
next -= constants::USHRT_MAX as u32;
}
next as u16
}
pub(crate) fn create_tcp_top(packet: &[u8]) -> Vec<u8> {
wr(|w| {
w.u16(constants::MACHINE_PREPARE_DATA_1)
.u16(constants::MACHINE_PREPARE_DATA_2)
.u32(packet.len() as u32)
.bytes(packet);
})
}
pub(crate) fn test_tcp_top(packet: &[u8]) -> u32 {
if packet.len() <= 8 {
return 0;
}
let m1 = u16_le(&packet[0..2]);
let m2 = u16_le(&packet[2..4]);
if m1 == constants::MACHINE_PREPARE_DATA_1 && m2 == constants::MACHINE_PREPARE_DATA_2 {
u32_le(&packet[4..8])
} else {
0
}
}
pub(crate) fn make_commkey(key: u32, session_id: u32, ticks: u8) -> [u8; 4] {
let mut k: u32 = 0;
for i in 0..32 {
if key & (1 << i) != 0 {
k = (k << 1) | 1;
} else {
k <<= 1;
}
}
k = k.wrapping_add(session_id);
let mut b = k.to_le_bytes();
b[0] ^= b'Z';
b[1] ^= b'K';
b[2] ^= b'S';
b[3] ^= b'O';
let w0 = u16::from_le_bytes([b[0], b[1]]);
let w1 = u16::from_le_bytes([b[2], b[3]]);
let swapped = wr(|w| {
w.u16(w1).u16(w0);
});
let mut b = [swapped[0], swapped[1], swapped[2], swapped[3]];
let t = ticks;
b[0] ^= t;
b[1] ^= t;
b[2] = t; b[3] ^= t;
b
}
pub(crate) fn decode_time(packed: u32) -> chrono::NaiveDateTime {
let mut t = packed;
let second = t % 60;
t /= 60;
let minute = t % 60;
t /= 60;
let hour = t % 24;
t /= 24;
let day = t % 31 + 1;
t /= 31;
let month = t % 12 + 1;
t /= 12;
let year = t + 2000;
naive(year as i32, month, day, hour, minute, second)
}
pub(crate) fn decode_timehex(b: &[u8; 6]) -> chrono::NaiveDateTime {
naive(
2000 + b[0] as i32,
b[1] as u32,
b[2] as u32,
b[3] as u32,
b[4] as u32,
b[5] as u32,
)
}
pub(crate) fn encode_time(dt: &chrono::NaiveDateTime) -> u32 {
use chrono::{Datelike, Timelike};
let year = (dt.year() % 100) as u32;
let month = dt.month();
let day = dt.day();
let hour = dt.hour();
let minute = dt.minute();
let second = dt.second();
((year * 12 * 31 + (month - 1) * 31 + day - 1) * (24 * 60 * 60))
+ (hour * 60 + minute) * 60
+ second
}
fn naive(
year: i32,
month: u32,
day: u32,
hour: u32,
minute: u32,
second: u32,
) -> chrono::NaiveDateTime {
use chrono::NaiveDate;
let date = NaiveDate::from_ymd_opt(year, month.clamp(1, 12), day.clamp(1, 31))
.or_else(|| {
(1..=31)
.rev()
.filter_map(|d| NaiveDate::from_ymd_opt(year, month.clamp(1, 12), d))
.next()
})
.unwrap_or_else(|| NaiveDate::from_ymd_opt(2000, 1, 1).expect("constant date is valid"));
date.and_hms_opt(hour.min(23), minute.min(59), second.min(59))
.unwrap_or_else(|| date.and_hms_opt(0, 0, 0).expect("midnight is valid"))
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::NaiveDate;
#[test]
fn commkey_matches_pyzk() {
assert_eq!(make_commkey(0, 0, 50), [97, 125, 50, 121]);
assert_eq!(make_commkey(123456, 57, 50), [38, 127, 50, 249]);
assert_eq!(make_commkey(0, 1234, 99), [48, 44, 99, 44]);
}
#[test]
fn checksum_matches_pyzk() {
assert_eq!(checksum(&[0xe8, 0x03, 0, 0, 0, 0, 0xfe, 0xff]), 64535);
assert_eq!(checksum(&[1, 2, 3, 4, 5]), 63989);
}
#[test]
fn time_encode_matches_pyzk_and_roundtrips() {
let when = NaiveDate::from_ymd_opt(2024, 5, 6)
.unwrap()
.and_hms_opt(7, 8, 9)
.unwrap();
let encoded = encode_time(&when);
assert_eq!(encoded, 782_550_489);
assert_eq!(decode_time(encoded), when);
}
#[test]
fn timehex_decodes() {
let when = decode_timehex(&[24, 5, 6, 7, 8, 9]);
assert_eq!(
when,
NaiveDate::from_ymd_opt(2024, 5, 6)
.unwrap()
.and_hms_opt(7, 8, 9)
.unwrap()
);
}
#[test]
fn reply_id_wraps_at_ushrt_max() {
assert_eq!(next_reply_id(constants::USHRT_MAX - 1), 0);
assert_eq!(next_reply_id(0), 1);
}
#[test]
fn tcp_top_roundtrips() {
let payload = [9u8, 8, 7, 6];
let framed = create_tcp_top(&payload);
assert_eq!(framed.len(), 12);
assert_eq!(test_tcp_top(&framed), payload.len() as u32);
}
#[test]
fn header_increments_sent_reply_id_but_checksums_original() {
let pkt = create_header(constants::CMD_CONNECT, &[], 0, constants::USHRT_MAX - 1);
assert_eq!(u16_le(&pkt[6..8]), 0);
assert_eq!(u16_le(&pkt[0..2]), constants::CMD_CONNECT);
}
}