zkteco 1.0.0

A pure-Rust client for ZKTeco biometric attendance / access-control terminals (TCP/UDP protocol). A faithful port of the Python `pyzk` library.
Documentation
//! The [`Finger`] fingerprint-template record.

use crate::protocol::wr;
use std::fmt;

/// A fingerprint template belonging to a user.
///
/// Mirrors `pyzk`'s `Finger`. The `template` is the opaque vendor blob; this crate
/// transports it verbatim and never tries to interpret it.
#[derive(Clone, PartialEq, Eq)]
pub struct Finger {
    /// Internal device user index this template belongs to.
    pub uid: u16,
    /// Finger index, 0–9.
    pub fid: u8,
    /// Validity flag (0 = invalid, 1 = valid, 3 = duress, per firmware).
    pub valid: u8,
    /// The opaque template payload.
    pub template: Vec<u8>,
}

impl Finger {
    /// Construct a template record.
    pub fn new(uid: u16, fid: u8, valid: u8, template: Vec<u8>) -> Self {
        Finger {
            uid,
            fid,
            valid,
            template,
        }
    }

    /// Length of the template payload in bytes.
    pub fn size(&self) -> usize {
        self.template.len()
    }

    /// Pack the template in the "full" record form (the same layout templates
    /// have inside a bulk dump), useful for persisting a template to disk.
    ///
    /// Layout (`pyzk` `repack`, `<H H b b {n}s`): size+6, uid, fid, valid, template.
    /// The leading length includes the 6-byte header.
    pub fn repack(&self) -> Vec<u8> {
        wr(|w| {
            w.u16((self.size() + 6) as u16)
                .u16(self.uid)
                .u8(self.fid)
                .u8(self.valid)
                .bytes(&self.template);
        })
    }

    /// Pack only the template body prefixed by its length (`pyzk` `repack_only`,
    /// `<H {n}s`). Used when uploading templates in bulk.
    pub(crate) fn repack_only(&self) -> Vec<u8> {
        wr(|w| {
            w.u16(self.size() as u16).bytes(&self.template);
        })
    }

    /// A short hex "fingerprint" of the template (first/last 8 bytes), handy for
    /// logging without dumping the whole blob.
    fn mark(&self) -> String {
        let head = &self.template[..self.template.len().min(8)];
        let tail = if self.template.len() >= 8 {
            &self.template[self.template.len() - 8..]
        } else {
            &[][..]
        };
        format!("{}...{}", hex(head), hex(tail))
    }
}

impl fmt::Debug for Finger {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(
            f,
            "<Finger> [uid:{:>3}, fid:{}, size:{:>4} v:{} t:{}]",
            self.uid,
            self.fid,
            self.size(),
            self.valid,
            self.mark()
        )
    }
}

impl fmt::Display for Finger {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        fmt::Debug::fmt(self, f)
    }
}

/// Lowercase hex encoding of a byte slice (no external `hex` crate dependency).
fn hex(bytes: &[u8]) -> String {
    let mut s = String::with_capacity(bytes.len() * 2);
    for b in bytes {
        s.push_str(&format!("{b:02x}"));
    }
    s
}