Skip to main content

thaiidcard/
lib.rs

1//! `libthai-idcard` — A Rust library for reading Thai National ID smart cards
2//! via PC/SC card readers. Supports personal information, NHSO insurance data,
3//! and the laser-engraved card serial number.
4//!
5//! # Basic usage
6//!
7//! ```no_run
8//! use thaiidcard::{SmartCard, Options};
9//!
10//! let card = SmartCard::new();
11//! let data = card.read(None, &Options::default()).unwrap();
12//! println!("Name: {}", data.personal.unwrap().name.full_name);
13//! ```
14#![allow(clippy::large_enum_variant)]
15
16pub mod apdu;
17pub mod ffi;
18pub mod model;
19pub mod options;
20pub mod reader;
21
22mod laser;
23mod nhso;
24mod personal;
25
26pub use options::Options;
27
28use std::sync::mpsc;
29use std::thread;
30use std::time::Duration;
31
32use pcsc::*;
33
34use crate::reader::CardError;
35
36/// Events emitted by [`SmartCard::start_daemon`].
37#[derive(Debug)]
38pub enum Event {
39    CardInserted { reader: String },
40    CardData(model::CardData),
41    CardRemoved { reader: String },
42    Error(String),
43}
44
45/// Main handle for reading Thai National ID smart cards.
46pub struct SmartCard;
47
48impl SmartCard {
49    /// Create a new `SmartCard` instance.
50    pub fn new() -> Self {
51        Self
52    }
53
54    /// Return all available PC/SC smart card reader names.
55    pub fn list_readers() -> Result<Vec<String>, CardError> {
56        let ctx = reader::establish_context()?;
57        reader::list_readers(&ctx)
58    }
59
60    /// Perform a single card read. If `reader_name` is `None`, the first
61    /// available reader with a card present is used.
62    pub fn read(
63        &self,
64        reader_name: Option<&str>,
65        opts: &Options,
66    ) -> Result<model::CardData, CardError> {
67        let ctx = reader::establish_context()?;
68
69        let readers = match reader_name {
70            Some(name) => vec![name.to_string()],
71            None => reader::list_readers(&ctx)?,
72        };
73
74        let idx = reader::wait_for_card(&ctx, &readers)?;
75        let reader = &readers[idx];
76
77        self.read_card(&ctx, reader, opts)
78    }
79
80    /// Monitor readers continuously and emit events on the returned channel.
81    /// The daemon runs until the receiver is dropped.
82    pub fn start_daemon(opts: Options) -> mpsc::Receiver<Event> {
83        let (tx, rx) = mpsc::channel();
84        let card = Self;
85
86        thread::spawn(move || loop {
87            let ctx = match reader::establish_context() {
88                Ok(c) => c,
89                Err(e) => {
90                    let _ = tx.send(Event::Error(e.to_string()));
91                    thread::sleep(Duration::from_secs(2));
92                    continue;
93                }
94            };
95
96            let readers = match reader::list_readers(&ctx) {
97                Ok(r) => r,
98                Err(_) => {
99                    thread::sleep(Duration::from_secs(2));
100                    continue;
101                }
102            };
103
104            let idx = match reader::wait_for_card(&ctx, &readers) {
105                Ok(i) => i,
106                Err(e) => {
107                    let _ = tx.send(Event::Error(e.to_string()));
108                    continue;
109                }
110            };
111
112            let reader_name = readers[idx].clone();
113            let _ = tx.send(Event::CardInserted {
114                reader: reader_name.clone(),
115            });
116
117            match card.read_card(&ctx, &reader_name, &opts) {
118                Ok(data) => {
119                    let _ = tx.send(Event::CardData(data));
120                }
121                Err(e) => {
122                    let _ = tx.send(Event::Error(e.to_string()));
123                }
124            }
125
126            let _ = reader::wait_for_card_removal(&ctx, idx, &readers);
127            let _ = tx.send(Event::CardRemoved {
128                reader: reader_name,
129            });
130        });
131
132        rx
133    }
134
135    fn read_card(
136        &self,
137        ctx: &Context,
138        reader: &str,
139        opts: &Options,
140    ) -> Result<model::CardData, CardError> {
141        let card = reader::connect_card(ctx, reader)?;
142
143        let status = card
144            .status2_owned()
145            .map_err(|e| CardError::Context(e.to_string()))?;
146        let resp_cmd = reader::get_response_command(status.atr());
147
148        let personal = personal::read_personal(&card, &resp_cmd, opts.show_face_image);
149
150        let card_info = if opts.show_laser_data {
151            Some(laser::read_laser_id(&card, &resp_cmd))
152        } else {
153            None
154        };
155
156        let nhso_data = if opts.show_nhso_data {
157            Some(nhso::read_nhso(&card, &resp_cmd))
158        } else {
159            None
160        };
161
162        drop(card);
163
164        Ok(model::CardData {
165            personal: Some(personal),
166            card: card_info,
167            nhso: nhso_data,
168        })
169    }
170}
171
172impl Default for SmartCard {
173    fn default() -> Self {
174        Self::new()
175    }
176}