- request() now reads exactly the expected response length with read_exact instead of a single non-guaranteed fill_buf, fixing a latent bug where a response split across multiple TCP packets could be mistaken for malformed data; the per-method length checks are now redundant and were removed - sequence_number wraps explicitly via wrapping_add instead of relying on implicit (and debug-vs-release inconsistent) overflow behavior after 65536 requests - get_name validates UTF-8 strictly instead of silently lossy-decoding invalid bytes; on invalid data it now returns the FromUtf8Error itself (byte-level \0 trimming moved after the UTF-8 check), letting the caller choose to fail or fall back to a lossy rendering - removed the unused Default derive from Clock and Record - removed npm from the CI image (no JS/Node in this repo), fixed a typo in a CI step name - fixed the Cargo.toml license field to the actual SPDX identifier (AGPL-3.0-or-later, per the notice at the end of LICENSE) instead of the free-text "GNU AGPLv3.0" Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -11,7 +11,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
rustup component add clippy rustfmt &&
|
rustup component add clippy rustfmt &&
|
||||||
apk update &&
|
apk update &&
|
||||||
apk add musl-dev git npm
|
apk add musl-dev git
|
||||||
|
|
||||||
- name: Checkout the code
|
- name: Checkout the code
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
@@ -27,5 +27,5 @@ jobs:
|
|||||||
- name: Check if the code is formatted correctly
|
- name: Check if the code is formatted correctly
|
||||||
run: cargo fmt --check
|
run: cargo fmt --check
|
||||||
|
|
||||||
- name: Check if Clippy has someting to say
|
- name: Check if Clippy has something to say
|
||||||
run: cargo clippy --all-targets
|
run: cargo clippy --all-targets
|
||||||
|
|||||||
+1
-1
@@ -2,7 +2,7 @@
|
|||||||
name = "r701"
|
name = "r701"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "GNU AGPLv3.0"
|
license = "AGPL-3.0-or-later"
|
||||||
repository = "https://git.nicolabelluti.me/nicolabelluti/r701"
|
repository = "https://git.nicolabelluti.me/nicolabelluti/r701"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
|||||||
@@ -45,11 +45,11 @@ If you want to read about this reverse engineering attempt, check out
|
|||||||
println!("No\tMchn\tEnNo\t\tName\t\tMode\tIOMd\tDateTime\t");
|
println!("No\tMchn\tEnNo\t\tName\t\tMode\tIOMd\tDateTime\t");
|
||||||
while let Some(record) = record_stream.next().await {
|
while let Some(record) = record_stream.next().await {
|
||||||
let record = record.unwrap();
|
let record = record.unwrap();
|
||||||
let name = r701
|
let name = match r701.get_name(record.employee_id).await.unwrap() {
|
||||||
.get_name(record.employee_id)
|
Some(Ok(name)) => name,
|
||||||
.await
|
Some(Err(error)) => String::from_utf8_lossy(error.as_bytes()).into_owned(),
|
||||||
.unwrap()
|
None => format!("user #{}", record.employee_id),
|
||||||
.unwrap_or(format!("user #{}", record.employee_id));
|
};
|
||||||
|
|
||||||
println!(
|
println!(
|
||||||
"{:0>6}\t{}\t{:0>9}\t{: <10}\t{}\t{}\t{}",
|
"{:0>6}\t{}\t{:0>9}\t{: <10}\t{}\t{}\t{}",
|
||||||
|
|||||||
+31
-34
@@ -1,6 +1,6 @@
|
|||||||
use crate::RecordIterator;
|
use crate::RecordIterator;
|
||||||
use std::io::{Error, ErrorKind::InvalidData, Result};
|
use std::io::{Error, ErrorKind::InvalidData, Result};
|
||||||
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
|
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||||
use tokio::net::{TcpStream, ToSocketAddrs};
|
use tokio::net::{TcpStream, ToSocketAddrs};
|
||||||
use tokio::sync::Mutex;
|
use tokio::sync::Mutex;
|
||||||
|
|
||||||
@@ -35,7 +35,7 @@ impl R701 {
|
|||||||
// The lock must stay held for the whole write+read round-trip, so that
|
// The lock must stay held for the whole write+read round-trip, so that
|
||||||
// concurrent callers can't interleave their requests on the wire
|
// concurrent callers can't interleave their requests on the wire
|
||||||
#[allow(clippy::significant_drop_tightening)]
|
#[allow(clippy::significant_drop_tightening)]
|
||||||
pub async fn request(&self, payload: &[u8; 12]) -> Result<Vec<u8>> {
|
pub async fn request(&self, payload: &[u8; 12], response_len: usize) -> Result<Vec<u8>> {
|
||||||
let mut state = self.state.lock().await;
|
let mut state = self.state.lock().await;
|
||||||
|
|
||||||
// Create a blank request
|
// Create a blank request
|
||||||
@@ -46,14 +46,16 @@ impl R701 {
|
|||||||
|
|
||||||
// Insert the sequence number
|
// Insert the sequence number
|
||||||
request[14..].clone_from_slice(&state.sequence_number.to_le_bytes());
|
request[14..].clone_from_slice(&state.sequence_number.to_le_bytes());
|
||||||
state.sequence_number += 1;
|
state.sequence_number = state.sequence_number.wrapping_add(1);
|
||||||
|
|
||||||
// Send the request
|
// Send the request
|
||||||
state.tcp_stream.write_all(&request).await?;
|
state.tcp_stream.write_all(&request).await?;
|
||||||
|
|
||||||
// Create a buffer and return the response
|
// Read exactly response_len bytes, looping over multiple reads if
|
||||||
let mut buffer = BufReader::new(&mut state.tcp_stream);
|
// the response arrives split across several TCP packets
|
||||||
Ok(buffer.fill_buf().await?.to_vec())
|
let mut buffer = vec![0u8; response_len];
|
||||||
|
state.tcp_stream.read_exact(&mut buffer).await?;
|
||||||
|
Ok(buffer)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::missing_errors_doc)]
|
#[allow(clippy::missing_errors_doc)]
|
||||||
@@ -61,7 +63,7 @@ impl R701 {
|
|||||||
// Create a request with a payload of `01 80 00 00 00 00 00 00 00 00 00
|
// Create a request with a payload of `01 80 00 00 00 00 00 00 00 00 00
|
||||||
// 00` and get the response
|
// 00` and get the response
|
||||||
let response = self
|
let response = self
|
||||||
.request(&[0x01, 0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0])
|
.request(&[0x01, 0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 10)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
// If the response is not `aa 55 01 01 00 00 00 00 00 00` then return an
|
// If the response is not `aa 55 01 01 00 00 00 00 00 00` then return an
|
||||||
@@ -74,39 +76,38 @@ impl R701 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::missing_errors_doc)]
|
#[allow(clippy::missing_errors_doc)]
|
||||||
pub async fn get_name(&self, id: u32) -> Result<Option<String>> {
|
pub async fn get_name(
|
||||||
|
&self,
|
||||||
|
id: u32,
|
||||||
|
) -> Result<Option<std::result::Result<String, std::string::FromUtf8Error>>> {
|
||||||
// Create a request with a payload of `01 c7 xx xx xx xx 00 00 00 00 14
|
// Create a request with a payload of `01 c7 xx xx xx xx 00 00 00 00 14
|
||||||
// 00`, where `xx xx xx xx` is the employee ID in little endian
|
// 00`, where `xx xx xx xx` is the employee ID in little endian
|
||||||
let mut request = [0x01, 0xc7, 0, 0, 0, 0, 0, 0, 0, 0, 0x14, 0];
|
let mut request = [0x01, 0xc7, 0, 0, 0, 0, 0, 0, 0, 0, 0x14, 0];
|
||||||
request[2..6].clone_from_slice(&id.to_le_bytes());
|
request[2..6].clone_from_slice(&id.to_le_bytes());
|
||||||
|
|
||||||
// Get the response
|
// Get the response
|
||||||
let response = self.request(&request).await?;
|
let response = self.request(&request, 34).await?;
|
||||||
|
|
||||||
// If the response length is right but the header is `01 00 00 00 00 00
|
// If the header is `01 00 00 00 00 00 00 00` then the request has
|
||||||
// 00 00` then the request is been succesful but the name was not found
|
// been succesful but the name was not found
|
||||||
if response.len() == 34
|
if response[..12] == [0xaa, 0x55, 0x01, 0, 0, 0, 0, 0, 0, 0, 0x55, 0xaa] {
|
||||||
&& response[..12] == [0xaa, 0x55, 0x01, 0, 0, 0, 0, 0, 0, 0, 0x55, 0xaa]
|
|
||||||
{
|
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
}
|
}
|
||||||
|
|
||||||
// If one between the response length or the response header is wrong
|
// If the response header is wrong return an error
|
||||||
// return an error
|
if response[..12] != [0xaa, 0x55, 0x01, 0x01, 0, 0, 0, 0, 0, 0, 0x55, 0xaa] {
|
||||||
if response.len() != 34
|
|
||||||
|| response[..12] != [0xaa, 0x55, 0x01, 0x01, 0, 0, 0, 0, 0, 0, 0x55, 0xaa]
|
|
||||||
{
|
|
||||||
return Err(Error::new(InvalidData, "Malformed response"));
|
return Err(Error::new(InvalidData, "Malformed response"));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the name as a UTF-8 string and delete the `\0` at the end
|
// Get the name as a UTF-8 string and delete the `\0` at the end
|
||||||
let name = String::from_utf8_lossy(&response[12..22])
|
let name = match String::from_utf8(response[12..22].to_vec()) {
|
||||||
.trim_end_matches('\0')
|
Ok(name) => name.trim_end_matches('\0').to_string(),
|
||||||
.to_string();
|
Err(error) => return Ok(Some(Err(error))),
|
||||||
|
};
|
||||||
|
|
||||||
// Return None if the name is empty, else return the name wrapped into a
|
// Return None if the name is empty, else return the name wrapped into a
|
||||||
// Some
|
// Some
|
||||||
Ok(Some(name).filter(|name| !name.is_empty()))
|
Ok(Some(name).filter(|name| !name.is_empty()).map(Ok))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::missing_errors_doc)]
|
#[allow(clippy::missing_errors_doc)]
|
||||||
@@ -114,15 +115,12 @@ impl R701 {
|
|||||||
// Create a request with a payload of `01 b4 08 00 00 00 00 00 ff ff 00
|
// Create a request with a payload of `01 b4 08 00 00 00 00 00 ff ff 00
|
||||||
// 00` and get the response
|
// 00` and get the response
|
||||||
let response = self
|
let response = self
|
||||||
.request(&[0x01, 0xb4, 0x08, 0, 0, 0, 0, 0, 0xff, 0xff, 0, 0])
|
.request(&[0x01, 0xb4, 0x08, 0, 0, 0, 0, 0, 0xff, 0xff, 0, 0], 10)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
// If one between the response length or the response header is wrong
|
// If one between the response header or the trailing bytes is wrong
|
||||||
// return an error
|
// return an error
|
||||||
if response.len() != 10
|
if response[..4] != [0xaa, 0x55, 0x01, 0x01] || response[6..] != [0u8; 4] {
|
||||||
|| response[..4] != [0xaa, 0x55, 0x01, 0x01]
|
|
||||||
|| response[6..] != [0u8; 4]
|
|
||||||
{
|
|
||||||
return Err(Error::new(InvalidData, "Malformed response"));
|
return Err(Error::new(InvalidData, "Malformed response"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -155,12 +153,11 @@ impl R701 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get the response
|
// Get the response
|
||||||
let response = self.request(&request).await?;
|
let response = self.request(&request, 1038).await?;
|
||||||
|
|
||||||
// If one between the response length, the response header or the last
|
// If one between the response header or the last two bits is wrong
|
||||||
// two bits is wrong return an error
|
// return an error
|
||||||
if response.len() != 1038
|
if response[..12] != [0xaa, 0x55, 0x01, 0x01, 0, 0, 0, 0, 0, 0, 0x55, 0xaa]
|
||||||
|| response[..12] != [0xaa, 0x55, 0x01, 0x01, 0, 0, 0, 0, 0, 0, 0x55, 0xaa]
|
|
||||||
|| response[1036..] != [0, 0]
|
|| response[1036..] != [0, 0]
|
||||||
{
|
{
|
||||||
return Err(Error::new(InvalidData, "Malformed response"));
|
return Err(Error::new(InvalidData, "Malformed response"));
|
||||||
|
|||||||
+2
-9
@@ -2,20 +2,16 @@ use chrono::{DateTime, Local, TimeZone};
|
|||||||
#[cfg(feature = "serde")]
|
#[cfg(feature = "serde")]
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, Default, Eq, PartialEq, Ord, PartialOrd, Hash)]
|
#[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash)]
|
||||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||||
pub enum Clock {
|
pub enum Clock {
|
||||||
#[default]
|
|
||||||
FirstIn,
|
FirstIn,
|
||||||
FirstOut,
|
FirstOut,
|
||||||
SecondIn,
|
SecondIn,
|
||||||
SecondOut,
|
SecondOut,
|
||||||
}
|
}
|
||||||
|
|
||||||
// unsafe impl Send for Clock {}
|
#[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash)]
|
||||||
// unsafe impl Sync for Clock {}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, Default, Eq, PartialEq, Ord, PartialOrd, Hash)]
|
|
||||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||||
pub struct Record {
|
pub struct Record {
|
||||||
pub employee_id: u32,
|
pub employee_id: u32,
|
||||||
@@ -23,9 +19,6 @@ pub struct Record {
|
|||||||
pub datetime: DateTime<Local>,
|
pub datetime: DateTime<Local>,
|
||||||
}
|
}
|
||||||
|
|
||||||
// unsafe impl Send for Record {}
|
|
||||||
// unsafe impl Sync for Record {}
|
|
||||||
|
|
||||||
impl TryFrom<&[u8]> for Record {
|
impl TryFrom<&[u8]> for Record {
|
||||||
type Error = &'static str;
|
type Error = &'static str;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user