Skip to main content

thaiidcard/
reader.rs

1//! Low-level PC/SC card operations and APDU transport.
2
3use pcsc::*;
4use std::error::Error;
5use std::ffi::CString;
6use std::fmt;
7use std::time::Duration;
8
9/// Errors returned by card operations.
10#[derive(Debug)]
11pub enum CardError {
12    NoReaders,
13    CardNotFound,
14    Transmit(String),
15    Context(String),
16    Connect(String),
17}
18
19impl fmt::Display for CardError {
20    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
21        match self {
22            Self::NoReaders => write!(f, "no smart card readers available"),
23            Self::CardNotFound => write!(f, "no card present"),
24            Self::Transmit(msg) => write!(f, "transmit: {msg}"),
25            Self::Context(msg) => write!(f, "context: {msg}"),
26            Self::Connect(msg) => write!(f, "connect: {msg}"),
27        }
28    }
29}
30
31impl Error for CardError {}
32
33/// Establish a PC/SC context.
34pub fn establish_context() -> Result<Context, CardError> {
35    Context::establish(Scope::User).map_err(|e| CardError::Context(e.to_string()))
36}
37
38/// List available card reader names.
39pub fn list_readers(ctx: &Context) -> Result<Vec<String>, CardError> {
40    let mut buf = [0u8; 2048];
41    let readers = ctx
42        .list_readers(&mut buf)
43        .map_err(|e| CardError::Context(e.to_string()))?;
44    let result: Vec<String> = readers
45        .filter_map(|r| r.to_str().ok())
46        .map(|s| s.to_string())
47        .collect();
48    if result.is_empty() {
49        return Err(CardError::NoReaders);
50    }
51    Ok(result)
52}
53
54/// Helper to create a CString from a string slice for pcsc APIs.
55fn to_c_str(s: &str) -> CString {
56    CString::new(s).unwrap_or_else(|_| CString::new("").unwrap())
57}
58
59/// Wait until a card is inserted into any of the given readers.
60pub fn wait_for_card(ctx: &Context, readers: &[String]) -> Result<usize, CardError> {
61    let mut states: Vec<ReaderState> = readers
62        .iter()
63        .map(|r| ReaderState::new(to_c_str(r), State::UNAWARE))
64        .collect();
65
66    loop {
67        ctx.get_status_change(Duration::from_secs(u64::MAX), &mut states)
68            .map_err(|e| CardError::Context(e.to_string()))?;
69
70        for (i, state) in states.iter().enumerate() {
71            if state.event_state().intersects(State::PRESENT) {
72                return Ok(i);
73            }
74        }
75        for state in &mut states {
76            state.sync_current_state();
77        }
78    }
79}
80
81/// Wait until the card is removed from its reader.
82pub fn wait_for_card_removal(
83    ctx: &Context,
84    reader_idx: usize,
85    readers: &[String],
86) -> Result<(), CardError> {
87    let mut states = vec![ReaderState::new(
88        to_c_str(&readers[reader_idx]),
89        State::UNAWARE,
90    )];
91
92    loop {
93        ctx.get_status_change(Duration::from_secs(u64::MAX), &mut states)
94            .map_err(|e| CardError::Context(e.to_string()))?;
95
96        if states[0].event_state().intersects(State::EMPTY) {
97            return Ok(());
98        }
99        states[0].sync_current_state();
100    }
101}
102
103/// Connect to the card in the specified reader (exclusive, any protocol).
104pub fn connect_card(ctx: &Context, reader: &str) -> Result<Card, CardError> {
105    let c_reader = to_c_str(reader);
106    ctx.connect(c_reader.as_c_str(), ShareMode::Exclusive, Protocols::ANY)
107        .map_err(|e| CardError::Connect(e.to_string()))
108}
109
110/// Get the GET RESPONSE command prefix based on the card ATR.
111pub fn get_response_command(atr: &[u8]) -> Vec<u8> {
112    if atr.len() >= 2 && atr[0] == 0x3B && atr[1] == 0x67 {
113        vec![0x00, 0xC0, 0x00, 0x01]
114    } else {
115        vec![0x00, 0xC0, 0x00, 0x00]
116    }
117}
118
119/// Maximum APDU buffer size (short APDU max is 264 for 255-byte payload + status).
120const BUF_SIZE: usize = 264;
121
122/// Send a READ BINARY APDU followed by GET RESPONSE.
123/// If `is_tis620`, the response is decoded from Windows-874 (TIS-620) to UTF-8.
124/// The trailing 2-byte status word is trimmed.
125pub fn transmit_read(
126    card: &Card,
127    cmd: &[u8],
128    get_resp_prefix: &[u8],
129    is_tis620: bool,
130) -> Result<String, CardError> {
131    let _ = transmit_raw(card, cmd)?;
132
133    let mut get_resp = get_resp_prefix.to_vec();
134    get_resp.push(cmd[cmd.len() - 1]);
135    let rsp = transmit_raw(card, &get_resp)?;
136
137    let mut payload = strip_status(&rsp);
138    if is_tis620 {
139        let (decoded, _, _) = encoding_rs::WINDOWS_874.decode(&payload);
140        payload = decoded.as_bytes().to_vec();
141    }
142    Ok(String::from_utf8_lossy(&payload).trim().to_string())
143}
144
145/// Read the laser ID using a fixed 0x10 GET RESPONSE length.
146pub fn transmit_read_laser_id(
147    card: &Card,
148    cmd: &[u8],
149    get_resp_prefix: &[u8],
150) -> Result<String, CardError> {
151    let _ = transmit_raw(card, cmd)?;
152
153    let mut get_resp = get_resp_prefix.to_vec();
154    get_resp.push(0x10);
155    let rsp = transmit_raw(card, &get_resp)?;
156
157    let payload: Vec<u8> = strip_status(&rsp)
158        .into_iter()
159        .filter(|&b| b != 0x00)
160        .collect();
161    Ok(String::from_utf8_lossy(&payload).trim().to_string())
162}
163
164/// Like `transmit_read` but returns raw bytes without UTF-8 conversion.
165/// Used for binary data such as face images.
166pub fn transmit_read_bytes(
167    card: &Card,
168    cmd: &[u8],
169    get_resp_prefix: &[u8],
170) -> Result<Vec<u8>, CardError> {
171    let _ = transmit_raw(card, cmd)?;
172    let mut get_resp = get_resp_prefix.to_vec();
173    get_resp.push(cmd[cmd.len() - 1]);
174    let rsp = transmit_raw(card, &get_resp)?;
175    Ok(strip_status(&rsp))
176}
177
178/// Send a SELECT APDU to choose an applet on the card.
179pub fn transmit_select(card: &Card, cmd: &[u8]) -> Result<(), CardError> {
180    transmit_raw(card, cmd).map(|_| ())
181}
182
183fn transmit_raw(card: &Card, cmd: &[u8]) -> Result<Vec<u8>, CardError> {
184    let mut buf = [0u8; BUF_SIZE];
185    let rsp = card
186        .transmit(cmd, &mut buf)
187        .map_err(|e| CardError::Transmit(e.to_string()))?;
188    Ok(rsp.to_vec())
189}
190
191fn strip_status(data: &[u8]) -> Vec<u8> {
192    if data.len() < 2 {
193        Vec::new()
194    } else {
195        data[..data.len() - 2].to_vec()
196    }
197}