Compare commits

...

11 Commits

Author SHA1 Message Date
2ad6dba535
feat: implement high level get_vitale_card_rights over SSV_LireDroitsVitale - without output data parsing 2024-10-24 21:59:04 +02:00
db5eedd45a
feat: Ajout de la fonction SSV_LireDroitsVitale à la crate fsv-sys et refactoring des tests unitaires associés 2024-10-24 21:59:04 +02:00
e01b28f66f
feat: add integration tests with snapshot for some CPS cards in kits 2024-10-24 21:59:03 +02:00
51bed2b4f4
feat: implement group2, CPS situation for SSV_LireCartePS 2024-10-24 21:59:03 +02:00
76d9e4e4f0
feat: Une enums for deserializable fields in CPS Holder group (01) 2024-10-24 21:59:03 +02:00
0b5b4978a6
feat: Take the size into account for Block reading, to handle missing fields 2024-10-24 21:59:02 +02:00
8fe733fc30
feat: use an enum instead of raw ID for CPS type 2024-10-24 21:59:02 +02:00
71f9d0c5f8
feat: raw (non-deserialized) implementation of the 01 group of LireCartePS 2024-10-24 21:59:02 +02:00
0ef8ede764
feat: implement a full SSV_LireConfig output parsing, using deku for a declarative bytes parsing
Co-authored-by: theo <t.lettermann@criteo.com>
2024-10-24 21:59:01 +02:00
ae68de8a3c Merge pull request 'feat: Update avatar URLs in Avatar and LoginModal components' (#68) from fix/change_default_avatar_urls into main
Le service que j'avais utilisé pour générer des avatars aléatoires pour cette démo a cessé de fonctionner aujourd'hui ... XD

Je le remplace donc par un autre service ^^

Reviewed-on: #68
Reviewed-by: kosssi <simon@p4pillon.org>
2024-10-14 17:11:13 +02:00
e24b9c7859 feat: Update avatar URLs in Avatar and LoginModal components 2024-10-14 17:10:46 +02:00
22 changed files with 2647 additions and 94 deletions

View File

@ -722,7 +722,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf4918709cc4dd777ad2b6303ed03cb37f3ca0ccede8c1b0d28ac6db8f4710e0"
dependencies = [
"once_cell",
"proc-macro-crate 2.0.2",
"proc-macro-crate 2.0.0",
"proc-macro2",
"quote",
"syn 2.0.77",
@ -1384,6 +1384,32 @@ dependencies = [
"syn 2.0.77",
]
[[package]]
name = "deku"
version = "0.18.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9711031e209dc1306d66985363b4397d4c7b911597580340b93c9729b55f6eb"
dependencies = [
"bitvec",
"deku_derive",
"log",
"no_std_io2",
"rustversion",
]
[[package]]
name = "deku_derive"
version = "0.18.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "58cb0719583cbe4e81fb40434ace2f0d22ccc3e39a74bb3796c22b451b4f139d"
dependencies = [
"darling 0.20.10",
"proc-macro-crate 3.2.0",
"proc-macro2",
"quote",
"syn 2.0.77",
]
[[package]]
name = "der"
version = "0.7.9"
@ -1747,6 +1773,29 @@ dependencies = [
"syn 2.0.77",
]
[[package]]
name = "env_filter"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4f2c92ceda6ceec50f43169f9ee8424fe2db276791afde7b2cd8bc084cb376ab"
dependencies = [
"log",
"regex",
]
[[package]]
name = "env_logger"
version = "0.11.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e13fa619b91fb2381732789fc5de83b45675e882f66623b7d8cb4f643017018d"
dependencies = [
"anstream",
"anstyle",
"env_filter",
"humantime",
"log",
]
[[package]]
name = "equivalent"
version = "1.0.1"
@ -1968,9 +2017,15 @@ name = "fsv"
version = "0.1.0"
dependencies = [
"anyhow",
"chrono",
"deku",
"env_logger",
"fsv-sys",
"insta",
"libc",
"log",
"num_enum",
"serde",
"thiserror",
"utils",
]
@ -1980,6 +2035,7 @@ name = "fsv-sys"
version = "0.1.0"
dependencies = [
"bindgen",
"chrono",
"libc",
"libloading 0.8.5",
"thiserror",
@ -2365,7 +2421,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0bb0228f477c0900c880fd78c8759b95c7636dbd7842707f49e132378aa2acdc"
dependencies = [
"heck 0.4.1",
"proc-macro-crate 2.0.2",
"proc-macro-crate 2.0.0",
"proc-macro-error",
"proc-macro2",
"quote",
@ -2633,6 +2689,12 @@ version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
[[package]]
name = "humantime"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
[[package]]
name = "hyper"
version = "1.4.1"
@ -2784,6 +2846,18 @@ dependencies = [
"libc",
]
[[package]]
name = "insta"
version = "1.40.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6593a41c7a73841868772495db7dc1e8ecab43bb5c0b6da2059246c4b506ab60"
dependencies = [
"console",
"lazy_static",
"linked-hash-map",
"similar",
]
[[package]]
name = "instant"
version = "0.1.13"
@ -3078,6 +3152,12 @@ dependencies = [
"vcpkg",
]
[[package]]
name = "linked-hash-map"
version = "0.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f"
[[package]]
name = "linux-raw-sys"
version = "0.3.8"
@ -3467,6 +3547,15 @@ dependencies = [
"memoffset 0.9.1",
]
[[package]]
name = "no_std_io2"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f038b95e66372ec5f4adabd615fc9a46a1fe42bcfe549863921c0e44667b605"
dependencies = [
"memchr",
]
[[package]]
name = "nodrop"
version = "0.1.14"
@ -3612,7 +3701,7 @@ version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af1844ef2428cc3e1cb900be36181049ef3d3193c63e43026cfe202983b27a56"
dependencies = [
"proc-macro-crate 2.0.2",
"proc-macro-crate 3.2.0",
"proc-macro2",
"quote",
"syn 2.0.77",
@ -4286,14 +4375,22 @@ dependencies = [
[[package]]
name = "proc-macro-crate"
version = "2.0.2"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b00f26d3400549137f92511a46ac1cd8ce37cb5598a96d382381458b992a5d24"
checksum = "7e8366a6159044a37876a2b9817124296703c586a5c92e2c53751fa06d8d43e8"
dependencies = [
"toml_datetime",
"toml_edit 0.20.2",
]
[[package]]
name = "proc-macro-crate"
version = "3.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ecf48c7ca261d60b74ab1a7b20da18bede46776b2e55535cb958eb595c5fa7b"
dependencies = [
"toml_edit 0.22.22",
]
[[package]]
name = "proc-macro-error"
version = "1.0.4"
@ -5308,6 +5405,12 @@ version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e"
[[package]]
name = "similar"
version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1de1d4f81173b03af4c0cbed3c898f6bff5b870e4a7f5d6f4057d62a7a4b686e"
[[package]]
name = "siphasher"
version = "0.3.11"
@ -6377,9 +6480,9 @@ dependencies = [
[[package]]
name = "toml_datetime"
version = "0.6.3"
version = "0.6.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b"
checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41"
dependencies = [
"serde",
]
@ -6394,7 +6497,7 @@ dependencies = [
"serde",
"serde_spanned",
"toml_datetime",
"winnow",
"winnow 0.5.40",
]
[[package]]
@ -6407,7 +6510,18 @@ dependencies = [
"serde",
"serde_spanned",
"toml_datetime",
"winnow",
"winnow 0.5.40",
]
[[package]]
name = "toml_edit"
version = "0.22.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5"
dependencies = [
"indexmap 2.5.0",
"toml_datetime",
"winnow 0.6.20",
]
[[package]]
@ -7485,6 +7599,15 @@ dependencies = [
"memchr",
]
[[package]]
name = "winnow"
version = "0.6.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b"
dependencies = [
"memchr",
]
[[package]]
name = "winreg"
version = "0.52.0"

View File

@ -91,6 +91,20 @@ cargo tauri dev --no-watch
> - le `backend` n'est rechargé que si des modifications sont détectées dans le dossier précisé par `-w crates/backend`
> - le rechargement du `desktop` est désactivé par l'option `--no-watch` ; en effet, le rechargement du `frontend` est déjà pris en charge par `bun` et ne nécessite pas de rechargement du `desktop`
## Tests
Les tests unitaires et d'intégration utilisants des cartes CPS ou Vitale réelles sont désactivés par défaut et doivent être
[explicitement activés](https://doc.rust-lang.org/book/ch11-02-running-tests.html#ignoring-some-tests-unless-specifically-requested) pour s'exécuter.
### Snapshots
Pour certains modules, des tests d'intégration s'appuient sur la crate [`insta`](https://insta.rs) pour générer et comparer des snapshots.
Pour faciliter la gestion de ces tests, il est recommandé d'installer `cargo-insta` :
```bash
cargo install cargo-insta
```
## Build
Pour packager le client `desktop`, il est nécessaire de faire appel à la CLI Tauri, qui se charge de gérer le build du `frontend` et son intégration au bundle :

View File

@ -16,3 +16,6 @@ thiserror.workspace = true
[build-dependencies]
bindgen = "0.70.1"
[dev-dependencies]
chrono = "0.4.38"

View File

@ -14,13 +14,13 @@
| SSV_InitLIB2 |
| SSV_LireConfig |
| SSV_LireCartePS |
| SSV_LireDroitsVitale |
## SGD
| Fonctions implémentées |
|------------------------|
## SRT
| Fonctions implémentées |

View File

@ -109,6 +109,15 @@ impl SSVLibrary<V1_40_14> {
pZDataOut: *mut *mut libc::c_void,
pTailleZone: *mut usize
});
ssv_function!(BINDINGS_V1_40_14::SSV_LireDroitsVitale, ssv_lire_droits_vitale, {
NomRessourcePS: *const i8,
NomRessourceLecteur: *const i8,
CodePorteurPS: *const i8,
DateConsultation: *const i8,
pZDataOut: *mut *mut libc::c_void,
pTailleZone: *mut usize
});
}
impl SSVLibrary<V1_40_13> {
@ -128,6 +137,15 @@ impl SSVLibrary<V1_40_13> {
pZDataOut: *mut *mut libc::c_void,
pTailleZone: *mut usize
});
ssv_function!(BINDINGS_V1_40_13::SSV_LireDroitsVitale, ssv_lire_droits_vitale, {
NomRessourcePS: *const i8,
NomRessourceLecteur: *const i8,
CodePorteurPS: *const i8,
DateConsultation: *const i8,
pZDataOut: *mut *mut libc::c_void,
pTailleZone: *mut usize
});
}
pub fn get_library_path(version: &SupportedFsvVersion) -> String {
@ -162,51 +180,103 @@ mod test {
use super::*;
mod common {
use super::*;
const CPS_READER_NAME: &str = "Gemalto PC Twin Reader (645D94C3) 00 00";
const CPS_PIN: &str = "1234";
const VITALE_READER_NAME: &str = "XXXXXX";
pub fn init_library() -> SSVLibrary<V1_40_13> {
let lib_path = &get_library_path(&SupportedFsvVersion::V1_40_13);
let ssv_library = SSVLibrary::<V1_40_13>::new(lib_path).expect("SSVLibrary::new failed");
let sesam_ini_str =
CString::new(sesam_ini_path(&SupportedFsvVersion::V1_40_13)).expect("CString::new failed");
let result = unsafe { ssv_library.ssv_init_lib2(sesam_ini_str.as_ptr()) }.unwrap();
assert_eq!(result, 0);
ssv_library
}
pub fn lire_config(ssv_library: &SSVLibrary<V1_40_13>) {
let mut buffer_ptr: *mut libc::c_void = ptr::null_mut();
let mut size: libc::size_t = 0;
let result = unsafe { ssv_library.ssv_lire_config(&mut buffer_ptr, &mut size) }.unwrap();
assert_eq!(result, 0);
unsafe { libc::free(buffer_ptr) };
}
pub fn lire_carte_ps(ssv_library: &SSVLibrary<V1_40_13>) {
let nom_ressource_ps =
CString::new(CPS_READER_NAME).expect("CString::new failed");
let nom_ressource_lecteur =
CString::new("useless parameter").expect("CString::new failed");
let code_porteur_ps = CString::new(CPS_PIN).expect("CString::new failed");
let mut buffer_ptr: *mut libc::c_void = ptr::null_mut();
let mut size: libc::size_t = 0;
let result = unsafe {
ssv_library.ssv_lire_carte_ps(
nom_ressource_ps.as_ptr(),
nom_ressource_lecteur.as_ptr(),
code_porteur_ps.as_ptr(),
&mut buffer_ptr,
&mut size,
)
}.unwrap();
assert_eq!(result, 0);
unsafe { libc::free(buffer_ptr) };
}
pub fn lire_droits_vitale(ssv_library: &SSVLibrary<V1_40_13>) {
let nom_ressource_ps =
CString::new(CPS_READER_NAME).expect("CString::new failed");
let nom_ressource_lecteur =
CString::new(VITALE_READER_NAME).expect("CString::new failed");
let code_porteur_ps = CString::new(CPS_PIN).expect("CString::new failed");
// Today's date, in the format YYYYMMDD
let today = chrono::Local::now().format("%Y%m%d").to_string();
let date_consultation = CString::new(today).expect("CString::new failed");
let mut buffer_ptr: *mut libc::c_void = ptr::null_mut();
let mut size: libc::size_t = 0;
let result = unsafe {
ssv_library.ssv_lire_droits_vitale(
nom_ressource_ps.as_ptr(),
nom_ressource_lecteur.as_ptr(),
code_porteur_ps.as_ptr(),
date_consultation.as_ptr(),
&mut buffer_ptr,
&mut size,
)
}.unwrap();
assert_eq!(result, 0);
unsafe { libc::free(buffer_ptr) };
}
}
#[test]
fn test_initlib2() {
let lib_path = &get_library_path(&SupportedFsvVersion::V1_40_13);
let ssv_library = SSVLibrary::<V1_40_13>::new(lib_path).expect("SSVLibrary::new failed");
let sesam_ini_str =
CString::new(sesam_ini_path(&SupportedFsvVersion::V1_40_13)).expect("CString::new failed");
let result = unsafe { ssv_library.ssv_init_lib2(sesam_ini_str.as_ptr()) }.unwrap();
assert_eq!(result, 0);
let _ssv_library = common::init_library();
}
#[test]
fn test_lire_config_and_carte_ps() {
let lib_path = &get_library_path(&SupportedFsvVersion::V1_40_13);
let ssv_library = SSVLibrary::<V1_40_13>::new(lib_path).expect("SSVLibrary::new failed");
fn test_lire_config() {
let ssv_library = common::init_library();
common::lire_config(&ssv_library);
}
let sesam_ini_str =
CString::new(sesam_ini_path(&SupportedFsvVersion::V1_40_13)).expect("CString::new failed");
let result = unsafe { ssv_library.ssv_init_lib2(sesam_ini_str.as_ptr()) }.unwrap();
assert_eq!(result, 0);
#[test]
#[ignore="This test requires a CPS card to be inserted in the reader"]
fn test_lire_carte_ps() {
let ssv_library = common::init_library();
common::lire_carte_ps(&ssv_library);
}
let mut buffer_ptr: *mut libc::c_void = ptr::null_mut();
let mut size: libc::size_t = 0;
let result = unsafe { ssv_library.ssv_lire_config(&mut buffer_ptr, &mut size) }.unwrap();
assert_eq!(result, 0);
unsafe { libc::free(buffer_ptr) };
let nom_ressource_ps =
CString::new("Gemalto PC Twin Reader (645D94C3) 00 00").expect("CString::new failed");
let nom_ressource_lecteur =
CString::new("Gemalto PC Twin Reader (645D94C3) 00 00").expect("CString::new failed");
let code_porteur_ps = CString::new("1234").expect("CString::new failed");
let mut buffer_ptr: *mut libc::c_void = ptr::null_mut();
let mut size: libc::size_t = 0;
let result = unsafe {
ssv_library.ssv_lire_carte_ps(
nom_ressource_ps.as_ptr(),
nom_ressource_lecteur.as_ptr(),
code_porteur_ps.as_ptr(),
&mut buffer_ptr,
&mut size,
)
}
.unwrap();
assert_eq!(result, 0);
unsafe { libc::free(buffer_ptr) };
#[test]
#[ignore="This test requires a CPS card and a Vitale card to be inserted in the readers"]
fn test_lire_droits_vitale() {
let ssv_library = common::init_library();
common::lire_droits_vitale(&ssv_library);
}
}

View File

@ -7,7 +7,23 @@ edition = "2021"
anyhow = "1.0.89"
libc = "0.2.159"
num_enum = { version = "0.7.3", features = ["complex-expressions"] }
thiserror = "1.0.64"
deku = { version = "0.18.1", features = ["logging"] }
thiserror.workspace = true
serde.workspace = true
fsv-sys = { path = "../fsv-sys" }
utils = { path = "../utils" }
#[dev-dependencies]
log = "0.4.22"
env_logger = "0.11.5"
chrono = "0.4.38"
[dev-dependencies]
insta = "1.40.0"
[profile.dev.package]
# Optimize insta (snapshot testing library) for faster compile times
insta.opt-level = 3
similar.opt-level = 3

View File

@ -0,0 +1,238 @@
use deku::deku_derive;
use super::{ groups, size_read, read_with_size };
#[derive(Debug, PartialEq)]
#[deku_derive(DekuRead)]
/// # Data: FSV data structure
/// This structure is the core structure to read FSV raw data
/// It handles directly the raw data returned by the FSV library
/// A `Data` structure is composed of multiple `DataBlock` structures
pub struct Data {
#[deku(read_all)]
pub blocks: Vec<DataBlock>,
}
#[derive(Debug, PartialEq)]
#[deku_derive(DekuRead)]
/// # Data block structure
/// The `DataBlock` are the main structures inside a `Data` struct
pub struct DataBlock {
pub header: BlockHeader,
#[deku(ctx = "header.group_id.0, header.data_size")]
pub content: DataGroup,
}
#[derive(Debug, PartialEq)]
#[deku_derive(DekuRead)]
/// # Block header structure
/// The `BlockHeader` structure is the header of a `DataBlock`
/// It contains the group ID and the size of the `DataBlock` contained data (`inner` field)
pub struct BlockHeader {
pub group_id: GroupId,
#[deku(reader = "size_read(deku::reader)")]
pub data_size: u64,
}
#[derive(Debug, PartialEq)]
#[deku_derive(DekuRead)]
/// # Group ID
/// Allow to identify the type of data contained in a `DataBlock`
/// It is use as matching ID in the `DataGroup` enum. All the
/// IDs are documented on the SSV documentation, pages 23-28
pub struct GroupId(
#[deku(endian="big", bytes= 2)]
pub u16,
);
/// # Data group enum
/// This enum is used to match a `DataBlock` content with the
/// correct data structure, able to parse the data contained in
#[derive(Debug, PartialEq)]
#[deku_derive(DekuRead)]
#[deku(ctx = "group_id: u16, data_size: u64", id = "group_id")]
#[allow(non_camel_case_types)]
pub enum DataGroup {
#[deku(id = 1)]
LireCartePS_Group1_Holder(
#[deku(reader = "read_with_size(deku::reader, data_size as usize)")]
groups::ssv_lire_carte_ps::group_1_holder::Holder),
#[deku(id = 2)]
LireCartePS_Group2_Situation(
#[deku(reader = "read_with_size(deku::reader, data_size as usize)")]
groups::ssv_lire_carte_ps::group_2_situation::Situation),
#[deku(id = 60)]
LireConfig_Group60_ConfigHeader(
#[deku(reader = "read_with_size(deku::reader, data_size as usize)")]
groups::ssv_lire_config::group_60_header_config::ConfigHeader),
#[deku(id = 61)]
LireConfig_Group61_ReaderConfig(
#[deku(reader = "read_with_size(deku::reader, data_size as usize)")]
groups::ssv_lire_config::group_61_reader_config::ReaderConfig),
#[deku(id = 64)]
LireConfig_Group64_SVComponentsConfig(
#[deku(reader = "read_with_size(deku::reader, data_size as usize)")]
groups::ssv_lire_config::group_64_sv_config::SVComponentsConfig),
#[deku(id = 67)]
LireConfig_Group67_PCSCReaderConfig(
#[deku(reader = "read_with_size(deku::reader, data_size as usize)")]
groups::ssv_lire_config::group_67_pcsc_config::PCSCReaderConfig),
}
#[cfg(test)]
mod tests {
use deku::DekuContainerRead as _;
use super::*;
mod deku_testing {
use super::*;
#[derive(Debug, PartialEq)]
#[deku_derive(DekuRead)]
#[deku(endian = "big")]
pub struct DekuTest {
#[deku(bits = 4)]
pub a: u8,
#[deku(bits = 4)]
pub b: u8,
pub c: u16,
}
#[derive(Debug, PartialEq)]
#[deku_derive(DekuRead)]
#[deku(endian = "big")]
pub struct DekuTestWithSizeReader {
#[deku(bytes = 2)]
pub id: u16,
#[deku(reader = "size_read(deku::reader)")]
pub size: u64,
}
#[derive(Debug, PartialEq)]
#[deku_derive(DekuRead)]
pub struct DekuTestWithGroupId {
pub group_id: GroupId,
}
}
#[test]
fn test_deserialize_deku_test() {
let buffer: &[u8] = &[0b0110_1001, 0xBE, 0xEF];
let offset: usize = 0;
let ((rest, offset), val) = deku_testing::DekuTest::from_bytes((buffer, offset)).unwrap();
assert_eq!(val.a, 0b0110);
assert_eq!(val.b, 0b1001);
assert_eq!(val.c, 0xBEEF);
assert_eq!(offset, 0);
assert_eq!(rest, &[]);
}
#[test]
fn test_deserialize_deku_test_with_offset() {
let buffer: &[u8] = &[0b0000_1111, 0b0110_1001, 0xBE, 0xEF];
let offset: usize = 8;
let ((rest, offset), val) = deku_testing::DekuTest::from_bytes((buffer, offset)).unwrap();
assert_eq!(val.a, 0b0110);
assert_eq!(val.b, 0b1001);
assert_eq!(val.c, 0xBEEF);
assert_eq!(offset, 0);
assert_eq!(rest, &[]);
}
#[test]
fn test_serialize_deku_test_with_rest() {
let buffer: &[u8] = &[0b0110_1001, 0xBE, 0xEF, 0x1F, 0x2F];
let offset: usize = 0;
let ((rest, offset), val) = deku_testing::DekuTest::from_bytes((buffer, offset)).unwrap();
assert_eq!(val.a, 0b0110);
assert_eq!(val.b, 0b1001);
assert_eq!(val.c, 0xBEEF);
assert_eq!(offset, 0);
assert_eq!(rest, &[0x1F, 0x2F]);
}
#[test]
fn test_size_read() {
let buffer: &[u8] = &[
0, 60, // ID (60)
0b0100_0000, // Size type bit (0) + Size (64)
3, 4, 5, 6, 7, 8, 9, 10, 11, 12 // Extra data (10 bytes ; should be 64)
];
let ((rest, _offset), val) = deku_testing::DekuTestWithSizeReader::from_bytes((buffer, 0)).unwrap();
assert_eq!(val.id, 60, "EX1: ID");
assert_eq!(val.size, 64, "EX1: Size");
assert_eq!(rest.len(), 10, "EX1: Rest");
let buffer: &[u8] = &[
0, 60, // ID (60)
0b1000_0010, // Size type bit (1) + Size block length (2)
0b0000_0001, 0b0100_0000, // Size (320)
3, 4, 5, 6, 7, 8, 9, 10, 11, 12 // Extra data (10 bytes ; should be 320)
];
let ((rest, _offset), val) = deku_testing::DekuTestWithSizeReader::from_bytes((buffer, 0)).unwrap();
assert_eq!(val.id, 60, "EX2: ID");
assert_eq!(val.size, 320, "EX2: Size");
println!("{:?}", rest);
// assert_eq!(val.size, 320, "EX2: Size");
}
#[test]
fn test_endianness() {
#[derive(Debug, PartialEq)]
#[deku_derive(DekuRead)]
struct DekuTest {
#[deku(endian = "big")]
field_be: u16,
#[deku(endian = "little")]
field_le: u16,
field_default: u16,
}
let buffer: &[u8] = &[
0xAB, 0xCD,
0xAB, 0xCD,
0xAB, 0xCD,
];
let (_rest, result) = DekuTest::from_bytes((buffer, 0)).unwrap();
assert_eq!(result.field_be, 0xABCD, "0xAB,0xCD - Big Endian");
assert_eq!(result.field_le, 0xCDAB, "0xAB,0xCD - Little Endian");
assert_eq!(deku::ctx::Endian::default(), deku::ctx::Endian::Little, "Default Endian");
assert_eq!(result.field_default, 0xCDAB, "0xAB,0xCD - Default Endian");
let buffer: &[u8] = &[
0, 64,
0, 64,
0, 64,
];
let (_rest, result) = DekuTest::from_bytes((buffer, 0)).unwrap();
assert_eq!(result.field_be, 64, "0,64 - Big Endian");
assert_eq!(result.field_le, 16384, "0,64 - Little Endian");
assert_eq!(deku::ctx::Endian::default(), deku::ctx::Endian::Little);
assert_eq!(result.field_default, 16384, "0,64 - Default Endian");
}
#[test]
fn test_group_id() {
// env_logger::init(); // Uncomment and run with RUST_LOG=trace for deku debugging
let buffer: &[u8] = &[
0, 60, // ID (60)
];
let (_rest, val) = deku_testing::DekuTestWithGroupId::from_bytes((buffer, 0)).unwrap();
assert_eq!(val.group_id.0, 60, "EX1: ID");
let buffer: &[u8] = &[
7, 118, // ID (1910)
];
let (_rest, val) = deku_testing::DekuTestWithGroupId::from_bytes((buffer, 0)).unwrap();
assert_eq!(val.group_id.0, 1910, "EX2: ID");
}
}

View File

@ -0,0 +1,153 @@
use std::{fmt, str::FromStr};
use deku::{deku_derive, DekuError};
use super::{ size_read, map_bytes_to_lossy_string };
pub mod ssv_lire_carte_ps;
pub mod ssv_lire_config;
/// # Convert a DataField to a specific type
/// Using this as deku map function to fill a field value from
/// a DataField
fn map_from_data_field<T>(data_field: DataField) -> Result<T, DekuError>
where
T: FromStr,
T::Err: std::fmt::Display,
{
let text = String::from_utf8(data_field.data)
.map_err(|e| DekuError::Parse(e.to_string().into()))?;
text.parse::<T>()
.map_err(|e| DekuError::Parse(e.to_string().into()))
}
/// # Extract raw bytes from a DataField, through a Vec<u8>
/// This function is used as deku map function to extract raw bytes
/// from a DataField
fn map_raw_from_data_field<T>(data_field: DataField) -> Result<T, DekuError>
where
T: From<Vec<u8>>,
{
Ok(data_field.data.into())
}
/// # Extract an enum id from a string
/// Deku enums only supports numbers as id, and we usually extract strings
/// from data fields. This function is used as a context function to convert
/// a string, such as obtained with NumericString, to an enum id
pub fn extract_enum_id_from_str<T>(id_string: &str, default: T) -> T
where T: FromStr {
id_string.parse::<T>().unwrap_or(default)
}
// ------------------- DATA FIELD TYPES -------------------
/// # Data field structure
/// This structure is the core structure to read data fields
/// It is usually used by other structures implementing the
/// `#[deku(map = "map_from_data_field")]` attribute
#[deku_derive(DekuRead)]
#[derive(Debug, PartialEq)]
struct DataField {
#[deku(temp, reader = "size_read(deku::reader)")]
pub data_size: u64,
#[deku(bytes_read = "data_size")]
pub data: Vec<u8>,
}
#[deku_derive(DekuRead)]
#[derive(Debug, Clone, PartialEq)]
/// # Data field: Numeric string (x CN)
/// TODO: check if all the characters are numeric
pub struct NumericString(
#[deku(map = "map_from_data_field")]
pub String
);
impl From<&str> for NumericString {
fn from(s: &str) -> Self {
NumericString(s.to_string())
}
}
#[deku_derive(DekuRead)]
#[derive(Debug, Clone, PartialEq)]
/// # Data field: Alphanumeric string (x CA/CE)
pub struct AlphaNumericString(
#[deku(map = "map_from_data_field")]
pub String
);
impl From<&str> for AlphaNumericString {
fn from(s: &str) -> Self {
AlphaNumericString(s.to_string())
}
}
#[deku_derive(DekuRead)]
#[derive(Debug, Clone, PartialEq)]
/// # Data field: Raw bytes (x CB)
pub struct RawBytes(
#[deku(map = "map_raw_from_data_field")]
pub Vec<u8>
);
#[deku_derive(DekuRead)]
#[derive(Debug, Clone, PartialEq)]
/// # Data field: Raw byte (1 CB)
pub struct RawByte(
#[deku(map = "|x: DataField| -> Result<u8, DekuError> { Ok(x.data[0]) }")]
pub u8
);
#[deku_derive(DekuRead)]
#[derive(Debug, Clone, PartialEq)]
#[deku(endian = "big")]
/// # Software version
/// An almost standard software version structure in FSV
/// It is composed of a version and a revision, encoded on 2 bytes each
pub struct SoftwareVersion {
#[deku(temp, reader = "size_read(deku::reader)", assert_eq = "4")]
data_size: u64,
#[deku(bytes= 2, map = "|x: [u8; 2]| map_bytes_to_lossy_string(&x)")]
pub version: String,
#[deku(bytes= 2, map = "|x: [u8; 2]| map_bytes_to_lossy_string(&x)")]
pub revision: String,
}
impl fmt::Display for SoftwareVersion {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}.{}", self.version, self.revision)
}
}
#[cfg(test)]
mod test {
use deku::DekuContainerRead as _;
use super::*;
#[test]
fn test() {
let version_bytes: [u8; 2] = [48, 55];
let version = map_bytes_to_lossy_string(&version_bytes).unwrap();
assert_eq!(version, "07");
}
#[test]
fn test_software_version() {
// env_logger::init(); // Uncomment and run with RUST_LOG=trace for deku debugging
let data: [u8; 5] = [4, 48, 55, 50, 48];
let (_rest, software_version) = SoftwareVersion::from_bytes((&data, 0)).unwrap();
// assert_eq!(software_version.data_size, 4);
assert_eq!(software_version.version, "07");
assert_eq!(software_version.revision, "20");
}
#[test]
fn test_map_from_data_field() {
let data_field = DataField {
data: vec![48, 55],
};
let id: u8 = map_from_data_field(data_field).unwrap();
assert_eq!(id, 7);
}
}

View File

@ -0,0 +1,733 @@
//! # Structures de parsing des données de la fonction SSV_LireCartePS
#![allow(clippy::explicit_auto_deref)] // False positive on ctx attributes when using extract_enum_id_from_str
use deku::deku_derive;
use crate::fsv_parsing::groups::{ extract_enum_id_from_str, AlphaNumericString, NumericString, RawByte };
/// # Titulaire
/// 1 occurence
pub mod group_1_holder {
use super::*;
/// Groupe 1 - Titulaire
#[deku_derive(DekuRead)]
#[derive(Debug, PartialEq)]
pub struct Holder {
#[deku(temp)]
card_type_raw: NumericString, // Champ 1 : Type de carte PS (2 CN)
#[deku(ctx = "extract_enum_id_from_str::<u8>(&*card_type_raw.0, 255)")]
pub card_type: CardPSType,
#[deku(temp)]
national_id_type_raw: NumericString, // Champ 2 : Type didentification nationale (1 CN)
#[deku(ctx = "extract_enum_id_from_str::<u8>(&*national_id_type_raw.0, 255)")]
pub national_id_type: NationalIDType,
// TODO: handle national_id depending on national_id_type
pub national_id: AlphaNumericString, // /!\ CE and not CA - Champ 3 : N° didentification nationale (8-30 CE)
pub national_id_key: AlphaNumericString, // Champ 4 : Clé du N° didentification nationale (1 CN)
#[deku(temp)]
civility_code_raw: NumericString, // Champ 5 : Code civilité (2 CN)
#[deku(ctx = "extract_enum_id_from_str::<u8>(&*civility_code_raw.0, 255)")]
pub civility_code: CivilityCode,
pub holder_lastname: AlphaNumericString, // /!\ CE and not CA - Champ 6 : Nom du PS (27 CE)
pub holder_firstname: AlphaNumericString, // /!\ CE and not CA - Champ 7 : Prénom du PS (27 CE)
#[deku(temp)]
category_card_size: u8, // Champ 8 : Catégorie Carte (1 CA)
pub category_card: CategoryCard,
}
// Fields
#[deku_derive(DekuRead)]
#[derive(Debug, PartialEq)]
#[deku(ctx = "id: u8", id = "id")]
pub enum CardPSType {
#[deku(id = 0)]
CPS, // Carte de Professionnel de Santé (CPS)
#[deku(id = 1)]
CPF, // Carte de Professionnel de Santé en Formation (CPF)
#[deku(id = 2)]
CPE, // Carte de Personnel / Directeur⋅ice d'Établissement de Santé (CDE/CPE)
#[deku(id = 3)]
CPA, // Carte de Personnel / Directeur⋅ice Autorisé⋅e (CDA/CPA)
#[deku(id = 4)]
CPM, // Carte de Personne Morale
}
#[deku_derive(DekuRead)]
#[derive(Debug, PartialEq)]
#[deku(ctx = "id: u8", id = "id")]
pub enum NationalIDType {
#[deku(id = 0)]
ADELI, // N° ADELI
#[deku(id = 1)]
ADELICabinet, // Id Cabinet ADELI + N° employé
#[deku(id = 2)]
DRASS, // N° DRASS
#[deku(id = 3)]
FINESS, // N° FINESS + N° employé
#[deku(id = 4)]
SIREN, // N° SIREN + N° employé
#[deku(id = 5)]
SIRET, // N° SIRET + N° employé
#[deku(id = 6)]
RPPSCabinet, // Id Cabinet RPPS + N° employé
#[deku(id = 8)]
RPPS, // N° RPPS
#[deku(id = 9)]
ADELIEtudiantMedecin, // N° Etudiant Médecin type ADELI sur 9 caractères (information transmise par lANS)
}
#[deku_derive(DekuRead)]
#[derive(Debug, PartialEq)]
#[deku(ctx = "id: u8", id = "id")]
pub enum CivilityCode {
#[deku(id = 1)]
Adjudant,
#[deku(id = 2)]
Amiral,
#[deku(id = 3)]
Aspirant,
#[deku(id = 4)]
Aumonier,
#[deku(id = 5)]
Capitaine,
#[deku(id = 6)]
Cardinal,
#[deku(id = 7)]
Chanoine,
#[deku(id = 8)]
Colonel,
#[deku(id = 9)]
Commandant,
#[deku(id = 10)]
Commissaire,
#[deku(id = 11)]
Conseiller,
#[deku(id = 12)]
Directeur,
#[deku(id = 13)]
Docteur,
#[deku(id = 14)]
Douanier,
#[deku(id = 15)]
Epouxse,
#[deku(id = 16)]
Eveque,
#[deku(id = 17)]
General,
#[deku(id = 18)]
Gouverneur,
#[deku(id = 19)]
Ingenieur,
#[deku(id = 20)]
Inspecteur,
#[deku(id = 21)]
Lieutenant,
#[deku(id = 22)]
Madame,
#[deku(id = 23)]
Mademoiselle,
#[deku(id = 24)]
Maitre,
#[deku(id = 25)]
Marechal,
#[deku(id = 26)]
Medecin,
#[deku(id = 27)]
Mesdames,
#[deku(id = 28)]
Mesdemoiselles,
#[deku(id = 29)]
Messieurs,
#[deku(id = 30)]
Monseigneur,
#[deku(id = 31)]
Monsieur,
#[deku(id = 32)]
NotreDame,
#[deku(id = 33)]
Pasteur,
#[deku(id = 34)]
Prefet,
#[deku(id = 35)]
President,
#[deku(id = 36)]
Professeur,
#[deku(id = 37)]
Recteur,
#[deku(id = 38)]
Sergent,
#[deku(id = 39)]
SousPrefet,
#[deku(id = 40)]
Technicien,
#[deku(id = 41)]
Veuve,
}
#[deku_derive(DekuRead)]
#[derive(Debug, PartialEq)]
#[deku(id_type="u8")]
pub enum CategoryCard {
#[deku(id = 74)] // T
Test,
#[deku(id = 72)] // R
Reelle,
#[deku(id_pat = "_")]
Unknown,
}
}
/// # Situation
/// 1-16 occurences
pub mod group_2_situation {
use super::*;
/// Groupe 2 - Situation du PS
#[deku_derive(DekuRead)]
#[derive(Debug, PartialEq)]
pub struct Situation {
pub id: RawByte, // Champ 1 : N° logique de la situation de facturation du PS (1 CB)
#[deku(temp)]
practice_mode_raw: NumericString, // Champ 2 : Mode dexercice (2 CN)
#[deku(ctx = "extract_enum_id_from_str::<u8>(&*practice_mode_raw.0, 255)")]
pub practice_mode: PracticeMode,
// #[deku(temp)]
pub practice_status_raw: NumericString, // Champ 3 : Statut dexercice (3 CN)
// #[deku(ctx = "extract_enum_id_from_str::<u8>(&*practice_status_raw.0, 255)")]
// pub practice_status: PracticeStatus,
#[deku(temp)]
activity_sector_raw: NumericString, // Champ 4 : Secteur dactivité (3 CN)
#[deku(ctx = "extract_enum_id_from_str::<u8>(&*activity_sector_raw.0, 255)")]
pub activity_sector: ActivitySector,
#[deku(temp)]
structure_id_type_raw: NumericString, // Champ 5 : Type didentification structure (1 CN)
#[deku(ctx = "extract_enum_id_from_str::<u8>(&*structure_id_type_raw.0, 255)")]
pub structure_id_type: StructureIDType,
pub structure_id: AlphaNumericString, // Champ 6 : N° didentification structure (14 CA)
pub structure_id_key: NumericString, // Champ 7 : Clé du n° didentification structure (1 CN)
pub structure_name: AlphaNumericString, // Champ 8 : Raison sociale structure (40 CE)
pub ps_billing_number: NumericString, // Champ 9 : N° didentification de facturation du PS (8 CN)
pub ps_billing_number_key: NumericString, // Champ 10 : Clé du n° didentification de facturation du PS (1 CN)
pub ps_replacement_number: AlphaNumericString, // Champ 11 : N° didentification du PS remplaçant (30 CA) -- TODO OPTIONNEL
pub ps_replacement_number_key: NumericString, // Champ 12 : Clé du n° didentification du PS remplaçant (1 CN) -- TODO OPTIONNEL
#[deku(temp)]
convention_code_raw: NumericString, // Champ 13 : Code conventionnel (1 CN)
#[deku(ctx = "extract_enum_id_from_str::<u8>(&*convention_code_raw.0, 255)")]
pub convention_code: ConventionCode,
#[deku(temp)]
specialty_code_raw: NumericString, // Champ 14 : Code spécialité (2 CN)
#[deku(ctx = "extract_enum_id_from_str::<u8>(&*specialty_code_raw.0, 255)")]
pub specialty_code: SpecialtyCode,
// #[deku(temp)]
pub rate_zone_code_raw: NumericString, // Champ 15 : Code zone tarifaire (2 CN)
// #[deku(ctx = "extract_enum_id_from_str::<u8>(&*rate_zone_code_raw.0, 255)")]
// pub rate_zone_code: RateZoneCode, // CF p53-55 - Attribution complexe, dépendant du practice_status, du specialty_code et du convention_code
#[deku(temp)]
ik_zone_code_raw: NumericString, // Champ 16 : Code zone IK - Indemnité kilométrique (2 CN)
#[deku(ctx = "extract_enum_id_from_str::<u8>(&*ik_zone_code_raw.0, 255)")]
pub ik_zone_code: IkZoneCode,
#[deku(temp)]
approval_code_1_raw: NumericString, // Champ 17 : Code agrément 1 (1 CN)
#[deku(ctx = "extract_enum_id_from_str::<u8>(&*approval_code_1_raw.0, 255)")]
pub approval_code_1: ApprovalCode,
pub approval_code_2_raw: NumericString, // Champ 18 : Code agrément 2 (1 CN) - Non utilisé pour le moment
pub approval_code_3_raw: NumericString, // Champ 19 : Code agrément 3 (1 CN) - Non utilisé pour le moment
pub invoice_signature_permission: NumericString, // Champ 20 : Habilitation à signer une facture (1 CN)
pub lot_signature_permission: NumericString, // Champ 21 : Habilitation à signer un lot (1 CN)
}
// Fields
#[deku_derive(DekuRead)]
#[derive(Debug, PartialEq)]
#[deku(ctx = "id: u8", id = "id")]
/// Mode d'exercice
pub enum PracticeMode {
#[deku(id = 0)]
Liberal, // Libéral, exploitant, commerçant
#[deku(id = 1)]
Salarie,
#[deku(id = 4)]
Remplacant,
#[deku(id = 7)]
Benevole,
}
//
// #[deku_derive(DekuRead)]
// #[derive(Debug, PartialEq)]
// #[deku(ctx = "id: u8", id = "id")]
// /// Statut d'exercice
// pub enum PracticeStatus {
// // Cf. TAB-Statuts géré par lANS
// }
#[deku_derive(DekuRead)]
#[derive(Debug, PartialEq)]
#[deku(ctx = "id: u8", id = "id")]
/// Secteur d'activité
pub enum ActivitySector {
#[deku(id = 10)]
EtablissementPublicDeSante, // Etablissement Public de santé
#[deku(id = 11)]
HopitauxMilitaires, // Hôpitaux Militaires
#[deku(id = 16)]
EtablissementPrivePSPH, // Etablissement Privé PSPH
#[deku(id = 17)]
EtablissementPriveNonPSPH, // Etablissement Privé Non PSPH
#[deku(id = 25)]
DispensaireDeSoins, // Dispensaire de soins
#[deku(id = 26)]
AutresStructuresDeSoinsArmee, // Autres structures de soins relevant du Service de santé des armées
#[deku(id = 31)]
CabinetIndividuel, // Cabinet individuel
#[deku(id = 32)]
CabinetDeGroupe, // Cabinet de Groupe
#[deku(id = 33)]
ExerciceEnSociete, // Exercice en Société
#[deku(id = 34)]
SecteurPrivePHTempsPlein, // Secteur privé PH temps plein
#[deku(id = 35)]
TransportSanitaire, // Transport sanitaire
#[deku(id = 37)]
EntrepriseDInterim, // Entreprise d'intérim
#[deku(id = 41)]
EtablissementDeSoinsEtPrevention, // Etablissement de Soins et Prévention
#[deku(id = 42)]
PreventionEtSoinsEnEntreprise, // Prévention. Et Soins en Entreprise
#[deku(id = 43)]
SanteScolaireEtUniversitaire, // Santé scolaire & universitaire
#[deku(id = 44)]
RecrutementEtGestionRH, // Recrutement & gestion RH
#[deku(id = 45)]
PMIPlanificationFamiliale, // P.M.I. Planification familiale
#[deku(id = 51)]
EtablissementPourHandicapes, // Etablissement pour Handicapés
#[deku(id = 52)]
ComMarketingConsultingMedia, // Com/Marketing/Consulting/Media
#[deku(id = 53)]
EtablissementPersonnesAgees, // Etablissement Personnes Agées
#[deku(id = 54)]
EtablissementAideALaFamille, // Etablissement Aide à la famille
#[deku(id = 55)]
EtablissementDEnseignement, // Etablissement d'enseignement
#[deku(id = 56)]
EtablissementsDeProtectionDeLEnfance, // Etablissements de protection de l'enfance
#[deku(id = 57)]
EtablissementsDHebergementEtDeReadaptation, // Etablissements d'hébergement et de réadaptation
#[deku(id = 58)]
Recherche, // Recherche
#[deku(id = 61)]
AssurancePrivee, // Assurance Privée
#[deku(id = 62)]
OrganismeDeSecuriteSociale, // Organisme de Sécurité Sociale
#[deku(id = 65)]
MinistereEtServicesDeconcentres, // Ministère & Serv. Déconcentrés
#[deku(id = 66)]
CollectivitesTerritoriales, // Collectivités Territoriales
#[deku(id = 68)]
AssoEtOrgaHumanitaire, // Asso et orga humanitaire
#[deku(id = 71)]
LABM, // LABM
#[deku(id = 75)]
AutreEtablissementSanitaire, // Autre établissement Sanitaire
#[deku(id = 81)]
ProdEtComGrosBienMed, // Prod. & Com. Gros Bien Med.
#[deku(id = 85)]
CommDetailDeBiensMedicaux, // Comm. Détail de biens médicaux
#[deku(id = 86)]
PharmacieDOfficine, // Pharmacie d'officine
#[deku(id = 87)]
CentreDeDialyse, // Centre de dialyse
#[deku(id = 88)]
ParaPharmacie, // Para-pharmacie
#[deku(id = 91)]
AutreSecteurDActivite, // Autre secteur d'activité
#[deku(id = 92)]
SecteurNonDefini, // Secteur non défini
#[deku(id = 93)]
CentreAntiCancer, // Centre anti-cancer
#[deku(id = 94)]
CentreDeTransfusionSanguine, // Centre de transfusion sanguine
#[deku(id = 95)]
ChaineDuMedicament, // Répart. Distrib. Fab. Exploit. Import Médicaments
#[deku(id = 96)]
IncendiesEtSecours, // Incendies et secours
#[deku(id = 97)]
EntreprisesIndustriellesNonPharma, // Entreprises industrielles et tertiaires hors industries pharmaceutiques
#[deku(id = 98)]
EntiteDUnTOM, // Entité d'un TOM
#[deku(id = 99)]
ChaineDuDispositifMedical, // Fab. Exploit. Import. Médicaments et Dispositifs Médicaux
}
#[deku_derive(DekuRead)]
#[derive(Debug, PartialEq)]
#[deku(ctx = "id: u8", id = "id")]
/// Type d'identification structure
pub enum StructureIDType {
#[deku(id = 0)]
ADELICabinet, // Id Cabinet ADELI
#[deku(id = 1)]
FINESS, // N° FINESS
#[deku(id = 2)]
SIREN, // N° SIREN
#[deku(id = 3)]
SIRET, // N° SIRET
#[deku(id = 4)]
RPPSCabinet, // Id Cabinet RPPS
}
#[deku_derive(DekuRead)]
#[derive(Debug, PartialEq)]
#[deku(ctx = "id: u8", id = "id")]
/// Code conventionnel
/// Dictionnaire des données FSV, p37
pub enum ConventionCode {
#[deku(id = 0)]
NonConventionne,
#[deku(id = 1)]
Conventionne,
#[deku(id = 2)]
ConventionneAvecDepassement,
#[deku(id = 3)]
ConventionneAvecHonorairesLibres,
}
#[deku_derive(DekuRead)]
#[derive(Debug, PartialEq)]
#[deku(ctx = "id: u8", id = "id")]
/// Code spécialité
/// Dictionnaire des données FSV, p43
pub enum SpecialtyCode {
#[deku(id = 1)]
MedecineGenerale, // Médecine générale
#[deku(id = 2)]
AnesthesieReanimation, // Anesthésie-Réanimation
#[deku(id = 3)]
Cardiologie, // Cardiologie
#[deku(id = 4)]
ChirurgieGenerale, // Chirurgie Générale
#[deku(id = 5)]
DermatoVenerologie, // Dermatologie et Vénérologie
#[deku(id = 6)]
Radiologie, // Radiologie
#[deku(id = 7)]
GynecologieObstetrique, // Gynécologie obstétrique
#[deku(id = 8)]
GastroEnterologieHepatologie, // Gastro-Entérologie et Hépatologie
#[deku(id = 9)]
MedecineInterne, // Médecine interne
#[deku(id = 10)]
NeuroChirurgie, // Neuro-Chirurgie
#[deku(id = 11)]
OtoRhinoLaryngologie, // Oto-Rhino-Laryngologie
#[deku(id = 12)]
Pediatrie, // Pédiatrie
#[deku(id = 13)]
Pneumologie, // Pneumologie
#[deku(id = 14)]
Rhumatologie, // Rhumatologie
#[deku(id = 15)]
Ophtalmologie, // Ophtalmologie
#[deku(id = 16)]
ChirurgieUrologique, // Chirurgie urologique
#[deku(id = 17)]
NeuroPsychiatrie, // Neuro-Psychiatrie
#[deku(id = 18)]
Stomatologie, // Stomatologie
#[deku(id = 19)]
ChirurgienDentiste, // Chirurgien dentiste
#[deku(id = 20)]
ReanimationMedicale, // Réanimation médicale
#[deku(id = 21)]
SageFemme, // Sage-femme
#[deku(id = 22)]
SpécialisteEnMGDiplome, // Spécialiste en médecine générale avec diplôme
#[deku(id = 23)]
SpécialisteEnMGReconnu, // Spécialiste en médecine générale reconnu par lOrdre
#[deku(id = 24)]
Infirmier, // Infirmier
#[deku(id = 25)]
Psychologue, // Psychologue
#[deku(id = 26)]
MasseurKinesitherapeute, // Masseur Kinésithérapeute
#[deku(id = 27)]
PedicurePodologue, // Pédicure Podologue
#[deku(id = 28)]
Orthophoniste, // Orthophoniste
#[deku(id = 29)]
Orthoptiste, // Orthoptiste
#[deku(id = 30)]
LaboAnalysesMedicales, // Laboratoire d'analyses médicales
#[deku(id = 31)]
ReeducationReadaptationFonctionnelle, // Rééducation Réadaptation fonctionnelle
#[deku(id = 32)]
Neurologie, // Neurologie
#[deku(id = 33)]
Psychiatrie, // Psychiatrie
#[deku(id = 34)]
Geriatrie, // Gériatrie
#[deku(id = 35)]
Nephrologie, // Néphrologie
#[deku(id = 36)]
ChirurgieDentaireSpecialiteODF, // Chirurgie Dentaire spécialité O.D.F
#[deku(id = 37)]
AnatomoCytoPathologie, // Anatomo-Cyto-Pathologie
#[deku(id = 38)]
MedecinBiologiste, // Médecin biologiste
#[deku(id = 39)]
LaboPolyvalent, // Laboratoire polyvalent
#[deku(id = 40)]
LaboAnatomoCytoPathologique, // Laboratoire danatomo-cyto-pathologique
#[deku(id = 41)]
ChirurgieOrthopediqueTraumatologie, // Chirurgie Orthopédique et Traumatologie
#[deku(id = 42)]
EndocrinologieMetabolisme, // Endocrinologie et Métabolisme
#[deku(id = 43)]
ChirurgieInfantile, // Chirurgie infantile
#[deku(id = 44)]
ChirurgieMaxilloFaciale, // Chirurgie maxillo-faciale
#[deku(id = 45)]
ChirurgieMaxilloFacialeStomatologie, // Chirurgie maxillo-faciale et stomatologie
#[deku(id = 46)]
ChirurgiePlastiqueReconstructriceEsthetique, // Chirurgie plastique reconstructrice et esthétique
#[deku(id = 47)]
ChirurgieThoraciqueCardioVasculaire, // Chirurgie thoracique et cardio-vasculaire
#[deku(id = 48)]
ChirurgieVasculaire, // Chirurgie vasculaire
#[deku(id = 49)]
ChirurgieVisceraleDigestive, // Chirurgie viscérale et digestive
#[deku(id = 50)]
PharmacieDOfficine, // Pharmacie dofficine
#[deku(id = 51)]
PharmacieMutualiste, // Pharmacie Mutualiste
#[deku(id = 53)]
ChirurgienDentisteSpecialiteCO, // Chirurgien dentiste spécialité C.O.
#[deku(id = 54)]
ChirurgienDentisteSpecialiteMBD, // Chirurgien dentiste spécialité M.B.D.
#[deku(id = 60)]
PrestataireDeTypeSociete, // Prestataire de type société
#[deku(id = 61)]
PrestataireArtisan, // Prestataire artisan
#[deku(id = 62)]
PrestataireDeTypeAssociation, // Prestataire de type association
#[deku(id = 63)]
Orthesiste, // Orthésiste
#[deku(id = 64)]
Opticien, // Opticien
#[deku(id = 65)]
Audioprothesiste, // Audioprothésiste
#[deku(id = 66)]
EpithesisteOculariste, // Épithésiste Oculariste
#[deku(id = 67)]
PodoOrthesiste, // Podo-orthésiste
#[deku(id = 68)]
Orthoprothesiste, // Orthoprothésiste
#[deku(id = 69)]
ChirurgieOrale, // Chirurgie orale
#[deku(id = 70)]
GynecologieMedicale, // Gynécologie médicale
#[deku(id = 71)]
Hematologie, // Hématologie
#[deku(id = 72)]
MedecineNucleaire, // Médecine nucléaire
#[deku(id = 73)]
OncologieMedicale, // Oncologie médicale
#[deku(id = 74)]
OncologieRadiotherapique, // Oncologie radiothérapique
#[deku(id = 75)]
PsychiatrieEnfantAdolescent, // Psychiatrie de lenfant et de ladolescent
#[deku(id = 76)]
Radiotherapie, // Radiothérapie
#[deku(id = 77)]
Obstetrique, // Obstétrique
#[deku(id = 78)]
GenetiqueMedicale, // Génétique médicale
#[deku(id = 79)]
ObstetriqueGynecologieMedicale, // Obstétrique et Gynécologie médicale
#[deku(id = 80)]
SantePubliqueMedecineSociale, // Santé publique et médecine sociale
#[deku(id = 81)]
MaladiesInfectieusesTropicales, // Médecine des Maladies infectieuses et tropicales
#[deku(id = 82)]
MedecineLegaleExpertisesMedicales, // Médecine légale et expertises médicales
#[deku(id = 83)]
MedecineDurgence, // Médecine durgence
#[deku(id = 84)]
MedecineVasculaire, // Médecine vasculaire
#[deku(id = 85)]
Allergologie, // Allergologie
#[deku(id = 86)]
IPA, // Infirmier exerçant en Pratiques Avancées (IPA)
}
#[deku_derive(DekuRead)]
#[derive(Debug, PartialEq)]
#[deku(ctx = "id: u8", id = "id")]
/// Code zone IK - Indemnité kilométrique
/// Dictionnaire des données FSV, p44
pub enum IkZoneCode {
#[deku(id = 0)]
PasDIndemniteKilometrique, // Pas d'indemnité kilométrique
#[deku(id = 1)]
IndemniteKilometriquePlaine, // Indemnité kilométrique plaine
#[deku(id = 2)]
IndemniteKilometriqueMontagne, // Indemnité kilométrique montagne
}
#[deku_derive(DekuRead)]
#[derive(Debug, PartialEq)]
#[deku(ctx = "id: u8", id = "id")]
/// Code agrément
/// Dictionnaire des données FSV, p44
pub enum ApprovalCode {
#[deku(id = 0)]
PasAgrementRadio, // Pas d'agrément radio
#[deku(id = 1)]
AgrementDOuDDASS, // Agrément D ou DDASS
#[deku(id = 2)]
AgrementABCEF, // Agrément A, B, C, E, F
#[deku(id = 3)]
AgrementGHJ, // Agrément G, H, J
#[deku(id = 4)]
AgrementK, // Agrément K
#[deku(id = 5)]
AgrementL, // Agrément L
#[deku(id = 6)]
AgrementM, // Agrément M
}
}
#[cfg(test)]
mod tests {
use deku::DekuContainerRead as _;
use group_1_holder::{CardPSType, CategoryCard, CivilityCode, NationalIDType};
use group_2_situation::{ActivitySector, ApprovalCode, ConventionCode, IkZoneCode, PracticeMode, SpecialtyCode, StructureIDType};
use crate::fsv_parsing::blocks::BlockHeader;
use super::*;
mod data {
pub const BUFFER: &[u8] = &[
0, 1, // Block ID
53, // Block size
1, // Type de carte PS, 2 CN
48, // 0
1, // Type d'identification nationale, 1 CN
56, // 8
11, // N° d'identification nationale, 8-30 CE
57, 57, 55, 48, 48, 53, 57, 51, 54, 56, 54,
1, // Clé du N° d'identification nationale, 1 CN
54, // 6
2, // Code civilité, 2 CN
51, 49, // 31
23, // Nom du PS, 27 CE
80, 72, 65, 82, 77, 79, 70, 70, 73, 67,
69, 32, 82, 80, 80, 83, 48, 48, 53, 57,
51, 54, 56,
7, // Prénom du PS, 27 CE
71, 73, 76, 66, 69, 82, 84,
// ??? Missing ??? Catégorie Carte, 1 CA
0, 2, // Block ID
93, // Block size
1, // N° logique de la situation de facturation du PS, 1 CB
1,
1, // Mode d'exercice, 2 CN
48,
1, // Statut d'exercice, 3 CN
49,
2, // Secteur d'activité, 3 CN
56, 54,
1, // Type d'identification structure, 1 CN
49,
9, // N° d'identification structure, 14 CA
48, 66, 48, 50, 52, 54, 50, 56, 54, // 0B0246286
1, // Clé du N° d'identification structure, 1 CN
54,
34, // Raison sociale structure, 40 CE
80, 72, 65, 82, 77, 65, 67, 73, 69, 32,
68, 69, 32, 76, 65, 32, 71, 65, 82, 69,
32, 82, 79, 85, 84, 73, 69, 82, 69, 50,
52, 54, 50, 56,
8, // N° d'identification de facturation du PS, 8 CN
48, 48, 50, 48, 57, 51, 54, 56, // 00209368
1, // Clé du N° d'identification de facturation du PS, 1 CN
48,
0, // N° d'identification du PS remplaçant, 30 CA
1, // Clé du N° d'identification du PS remplaçant, 1 CN
48,
1, // Code conventionnel, 1 CN
49,
2, // Code spécialité, 2 CN
53, 48,
2, // Code zone tarifaire, 2 CN
49, 48,
2, // Code zone IK
48, 48,
1, // Code agrément 1, 1 CN
48,
1, // Code agrément 2, 1 CN
48,
1, // Code agrément 3, 1 CN
48,
1, // Habilitation à signer une Facture // 1 CN
49,
1, // Habilitation à signer un lot // 1 CN
49,
];
}
#[test]
fn test_lire_carte_ps_first_header() {
// env_logger::init(); // Uncomment and run with RUST_LOG=trace for deku debugging
let ((_rest, _offset), block_header) = BlockHeader::from_bytes((data::BUFFER, 0)).unwrap();
assert_eq!(block_header.group_id.0, 1, "Header ID");
assert_eq!(block_header.data_size, 53, "Header Size");
}
#[test]
fn test_group_1_holder() {
// env_logger::init(); // Uncomment and run with RUST_LOG=trace for deku debugging
let offset = 3*8;
let (_rest, holder) = group_1_holder::Holder::from_bytes((data::BUFFER, offset)).unwrap();
assert_eq!(holder.card_type, CardPSType::CPS, "Card type");
assert_eq!(holder.national_id_type, NationalIDType::RPPS, "National ID type");
assert_eq!(holder.national_id.0, "99700593686", "National Id");
assert_eq!(holder.national_id_key.0, "6", "National ID Key");
assert_eq!(holder.civility_code, CivilityCode::Monsieur, "Civility Code");
assert_eq!(holder.holder_lastname.0, "PHARMOFFICE RPPS0059368", "Holder Lastname");
assert_eq!(holder.holder_firstname.0, "GILBERT", "Holder Firstname");
assert_eq!(holder.category_card, CategoryCard::Unknown, "Category card");
}
#[test]
fn test_group_2_situation() {
// env_logger::init(); // Uncomment and run with RUST_LOG=trace for deku debugging
let offset = 3*8 + 53*8 + 3*8;
let (_rest, situation) = group_2_situation::Situation::from_bytes((data::BUFFER, offset)).unwrap();
assert_eq!(situation.id.0, 1, "ID");
assert_eq!(situation.practice_mode, PracticeMode::Liberal, "Practice mode");
assert_eq!(situation.practice_status_raw.0, "1", "Practice status raw");
assert_eq!(situation.activity_sector, ActivitySector::PharmacieDOfficine, "Activity sector");
assert_eq!(situation.structure_id_type, StructureIDType::FINESS, "Structure ID type");
assert_eq!(situation.structure_id.0, "0B0246286", "Structure ID");
assert_eq!(situation.structure_id_key.0, "6", "Structure ID key");
assert_eq!(situation.structure_name.0, "PHARMACIE DE LA GARE ROUTIERE24628", "Structure name");
assert_eq!(situation.ps_billing_number.0, "00209368", "PS billing number");
assert_eq!(situation.ps_billing_number_key.0, "0", "PS billing number key");
assert_eq!(situation.ps_replacement_number.0, "", "PS replacement number");
assert_eq!(situation.ps_replacement_number_key.0, "0", "PS replacement number key");
assert_eq!(situation.convention_code, ConventionCode::Conventionne, "Convention code");
assert_eq!(situation.specialty_code, SpecialtyCode::PharmacieDOfficine, "Specialty code");
assert_eq!(situation.rate_zone_code_raw.0, "10", "Rate zone code raw");
assert_eq!(situation.ik_zone_code, IkZoneCode::PasDIndemniteKilometrique, "IK zone code");
assert_eq!(situation.approval_code_1, ApprovalCode::PasAgrementRadio, "Approval code 1");
assert_eq!(situation.approval_code_2_raw.0, "0", "Approval code 2 raw");
assert_eq!(situation.approval_code_3_raw.0, "0", "Approval code 3 raw");
assert_eq!(situation.invoice_signature_permission.0, "1", "Invoice signature permission");
assert_eq!(situation.lot_signature_permission.0, "1", "Lot signature permission");
}
}

View File

@ -0,0 +1,296 @@
//! # Structures de parsing des données de la fonction SSV_LireConfig
//! Le groupe `ReaderConfig61` décrit ci-dessous est renseigné en cas dutilisation dun
//! lecteur homologué sesam-vitale uniquement et non en cas
//! dutilisation de lecteur(s) PC/SC. dans le cas dun TL ou TLA
//! configuré en mode PC/SC, un groupe `ReaderConfig61` est restitué pour chaque
//! lecteur exposé par le gestionnaire de ressources PC/SC. les
//! informations sont alors dupliquées dans chacun des groupes `ReaderConfig61`.
//! les informations sur les lecteurs PC/SC sont disponibles
//! dans les groupes `PCSCReaderConfig67`.
use deku::deku_derive;
use super::{AlphaNumericString, NumericString, SoftwareVersion};
/// # En-tête de configuration
/// 1 occurence
pub mod group_60_header_config {
use super::*;
/// Groupe 60 - En-tête de configuration
#[deku_derive(DekuRead)]
#[derive(Debug, PartialEq)]
pub struct ConfigHeader {
pub ssv_version: SSVVersionNumber,
pub galss_version: GALSSVersionNumber,
pub pss_version: PSSVersionNumber,
}
// Fields
#[deku_derive(DekuRead)]
#[derive(Debug, PartialEq)]
pub struct SSVVersionNumber(pub SoftwareVersion);
#[deku_derive(DekuRead)]
#[derive(Debug, PartialEq)]
pub struct GALSSVersionNumber(pub SoftwareVersion);
#[deku_derive(DekuRead)]
#[derive(Debug, PartialEq)]
pub struct PSSVersionNumber(pub SoftwareVersion);
}
/// # Configuration du lecteur
/// 0 à 15 occurences
pub mod group_61_reader_config {
use super::*;
/// Groupe 61 - Configuration du lecteur
#[deku_derive(DekuRead)]
#[derive(Debug, PartialEq)]
pub struct ReaderConfig {
pub manufacturer_name: AlphaNumericString, // 15 CA
pub reader_type: AlphaNumericString, // 30 CA
pub serial_number: AlphaNumericString, // 20 CA
pub os: NumericString, // 2 CN
pub software_count: NumericString, // 2 CN
pub software_name: AlphaNumericString, // 30 CA
pub software_version: ReaderSoftwareVersion, // 4 CA
pub reader_datetime: ReaderSoftwareDate, // 12 CN
pub software_checksum: AlphaNumericString, // 4 CA
}
// Fields
#[deku_derive(DekuRead)]
#[derive(Debug, PartialEq)]
pub struct ReaderSoftwareVersion(pub SoftwareVersion);
#[deku_derive(DekuRead)]
#[derive(Debug, PartialEq)]
/// Format « AAAAMMJJhhmm »
/// TODO: Build a generic date-time structure
/// TODO: Implement a date parsing, like chrono crate
pub struct ReaderSoftwareDate(pub AlphaNumericString);
}
/// # Configuration SESAM-Vitale
/// N occurences
pub mod group_64_sv_config {
use super::*;
/// Groupe 64 - Configuration SESAM-Vitale
#[deku_derive(DekuRead)]
#[derive(Debug, PartialEq)]
pub struct SVComponentsConfig {
pub id: ComponentID,
pub description: ComponentDescription,
pub version: ComponentVersion,
}
// Fields
#[deku_derive(DekuRead)]
#[derive(Debug, PartialEq)]
pub struct ComponentID(pub NumericString);
#[deku_derive(DekuRead)]
#[derive(Debug, PartialEq)]
pub struct ComponentDescription(pub AlphaNumericString);
#[deku_derive(DekuRead)]
#[derive(Debug, PartialEq)]
pub struct ComponentVersion(pub AlphaNumericString);
}
/// # Configuration du lecteur PC/SC
/// N occurences
pub mod group_67_pcsc_config {
use super::*;
/// Groupe 67 - Configuration du lecteur PC/SC
#[deku_derive(DekuRead)]
#[derive(Debug, PartialEq)]
pub struct PCSCReaderConfig {
pub name: ReaderName,
pub card_type: CardType,
}
// Fields
#[deku_derive(DekuRead)]
#[derive(Debug, PartialEq)]
pub struct ReaderName(pub AlphaNumericString);
#[deku_derive(DekuRead)]
#[derive(Debug, PartialEq)]
pub struct CardType(pub NumericString);
}
#[cfg(test)]
mod tests {
use deku::DekuContainerRead as _;
use crate::fsv_parsing::blocks::{BlockHeader, Data, DataBlock, DataGroup};
mod data {
pub const BUFFER: &[u8] = &[
0, 60, // Block ID
15, // Block Size
4, // SSV Version
48, 55, 50, 48, // 0720
4, // GALSS Version
48, 48, 48, 48, // 0000
4, // PSS Version
48, 48, 48, 48, // 0000
0, 67, // Block ID
42, // Block Size
39, // PCSC Reader Name
71, 101, 109, 97, 108, 116, 111, 32, 80, 67,
32, 84, 119, 105, 110, 32, 82, 101, 97, 100,
101, 114, 32, 40, 54, 52, 53, 68, 57, 52,
67, 51, 41, 32, 48, 48, 32, 48, 48,
1, // Card type
50,
0, 64, // Block ID
44, // Block Size
2, // Component ID
49, 49,
35, // Component label
86, 69, 82, 83, 73, 79, 78, 32, 68, 69,
32, 76, 65, 32, 66, 73, 66, 76, 73, 79,
84, 72, 69, 81, 85, 69, 32, 68, 85, 32,
71, 65, 76, 83, 83,
4, // Component version
48, 48, 48, 48,
0, 64, // Block ID
69, // Block Size
3, // Component ID
49, 53, 49,
27, // Component label
73, 68, 69, 78, 84, 73, 70, 73, 65, 78,
84, 32, 85, 78, 73, 81, 85, 69, 32, 68,
85, 32, 80, 79, 83, 84, 69,
36, // Component version
50, 54, 57, 102, 99, 55, 101, 98, 45, 49,
100, 56, 53, 45, 52, 55, 57, 51, 45, 98,
55, 48, 101, 45, 51, 55, 49, 99, 51, 56,
102, 57, 49, 54, 51, 52,
0, 61, // Block ID
62, // Block Size
17, // Manufacturer Name
84, 69, 83, 84, 32, 77, 65, 78, 85, 70,
65, 67, 84, 85, 82, 69, 82,
4, // Reader Type
84, 69, 83, 84,
4, // Serial Number
84, 69, 83, 84,
2, // OS
79, 83,
2, // Software Count
48, 49,
4, // Software Name
84, 69, 83, 84,
4, // Software Version
48, 49, 53, 53,
12, // Reader Datetime
50, 48, 50, 52, // 2024
48, 54, 50, 53, // 06-25
49, 50, 52, 53, // 12:45
4, // Software Checksum
49, 50, 51, 52,
];
}
#[test]
fn test_lire_config_first_header() {
// env_logger::init(); // Uncomment and run with RUST_LOG=trace for deku debugging
let buffer = data::BUFFER;
let offset: usize = 0;
let ((_rest, _offset), block_header) = BlockHeader::from_bytes((buffer, offset)).unwrap();
assert_eq!(block_header.group_id.0, 60, "Header ID");
assert_eq!(block_header.data_size, 15, "Header Size");
}
#[test]
fn test_lire_config_first_block() {
// env_logger::init(); // Uncomment and run with RUST_LOG=trace for deku debugging
let buffer = data::BUFFER;
let offset: usize = 0;
let ((_rest, _offset), block) = DataBlock::from_bytes((buffer, offset)).unwrap();
let header = block.header;
let content = match block.content {
DataGroup::LireConfig_Group60_ConfigHeader(content) => content,
_ => panic!("Unexpected data block type"),
};
assert_eq!(header.group_id.0, 60, "Header ID");
assert_eq!(header.data_size, 15, "Header Size");
assert_eq!(content.ssv_version.0.version, "07", "SSV Version");
assert_eq!(content.ssv_version.0.revision, "20", "SSV Revision");
assert_eq!(content.galss_version.0.to_string(), "00.00", "GALSS Version");
assert_eq!(content.pss_version.0.to_string(), "00.00", "PSS Version");
}
#[test]
fn test_lire_config_all() {
// env_logger::init(); // Uncomment and run with RUST_LOG=trace for deku debugging
let buffer = data::BUFFER;
let offset: usize = 0;
let ((_rest, _offset), data) = Data::from_bytes((buffer, offset)).unwrap();
let blocks = data.blocks;
assert_eq!(blocks.len(), 5, "Number of blocks");
for block in blocks {
match block.content {
DataGroup::LireConfig_Group60_ConfigHeader(content) => {
assert_eq!(block.header.group_id.0, 60, "Header ID");
assert_eq!(block.header.data_size, 15, "Header Size");
assert_eq!(content.ssv_version.0.version, "07", "SSV Version");
assert_eq!(content.ssv_version.0.revision, "20", "SSV Revision");
assert_eq!(content.galss_version.0.to_string(), "00.00", "GALSS Version");
assert_eq!(content.pss_version.0.to_string(), "00.00", "PSS Version");
},
DataGroup::LireConfig_Group61_ReaderConfig(content) => {
assert_eq!(block.header.group_id.0, 61, "Header ID");
assert_eq!(block.header.data_size, 62, "Header Size");
assert_eq!(content.manufacturer_name.0, "TEST MANUFACTURER", "Manufacturer Name");
assert_eq!(content.reader_type.0, "TEST", "Reader Type");
assert_eq!(content.serial_number.0, "TEST", "Serial Number");
assert_eq!(content.os.0, "OS", "OS");
assert_eq!(content.software_count.0, "01", "Software Count");
assert_eq!(content.software_name.0, "TEST", "Software Name");
assert_eq!(content.software_version.0.version, "01", "Software Version");
assert_eq!(content.software_version.0.revision, "55", "Software Revision");
assert_eq!(content.reader_datetime.0.0, "202406251245", "Reader Datetime");
assert_eq!(content.software_checksum.0, "1234", "Software Checksum");
},
DataGroup::LireConfig_Group64_SVComponentsConfig(content) => {
assert_eq!(block.header.group_id.0, 64, "Header ID");
match content.id.0.0.as_str() {
"11" => {
assert_eq!(block.header.data_size, 44, "Header Size");
assert_eq!(content.id.0.0, "11", "G64 - 11 : Component ID");
assert_eq!(content.description.0.0, "VERSION DE LA BIBLIOTHEQUE DU GALSS", "G64 - 11 : Component Description");
assert_eq!(content.version.0.0, "0000", "G64 - 11 : Component Version");
},
"151" => {
assert_eq!(block.header.data_size, 69, "Header Size");
assert_eq!(content.id.0.0, "151", "G64 - 151 : Component ID");
assert_eq!(content.description.0.0, "IDENTIFIANT UNIQUE DU POSTE", "G64 - 151 : Component Description");
assert_eq!(content.version.0.0, "269fc7eb-1d85-4793-b70e-371c38f91634", "G64 - 151 : Component Version");
},
_ => panic!("Unexpected Component ID"),
}
},
DataGroup::LireConfig_Group67_PCSCReaderConfig(content) => {
assert_eq!(block.header.group_id.0, 67, "Header ID");
assert_eq!(block.header.data_size, 42, "Header Size");
assert_eq!(content.name.0.0, "Gemalto PC Twin Reader (645D94C3) 00 00", "Reader Name");
assert_eq!(content.card_type.0.0, "2", "Card Type");
},
_ => panic!("Unexpected data block type"),
}
}
}
}

View File

@ -0,0 +1,132 @@
use deku::ctx::BitSize;
use deku::prelude::*;
use deku::reader::ReaderRet;
use deku::{reader::Reader, DekuError};
pub mod blocks;
pub mod groups;
pub mod prelude;
pub use blocks::Data;
/// # Read the size of a FSV block / field
/// Documentation: SSV Documentation, page 29
fn size_read<R: std::io::Read + std::io::Seek>(reader: &mut Reader<R>) -> Result<u64, DekuError> {
let size_bytes = u8::from_reader_with_ctx(reader, BitSize(8))?;
let size: u64 = if size_bytes & 0b1000_0000 == 0 {
// If the Most Significant Bit is 0, the size is encoded on 7 bits
size_bytes.into()
} else {
// Else, the 7 following bits indicate the number of bytes of the block containing the size
let size_block_len: usize = (size_bytes & 0b0111_1111).into();
if size_block_len > 4 {
return Err(DekuError::Parse(format!("Unexpected size block length: {}", size_block_len).into()));
};
// The block containing the size is encoded on 1 to 4 bytes
let buffer: &mut [u8; 4] = &mut [0; 4];
let write_offset = 4 - size_block_len;
match reader.read_bytes(size_block_len, &mut buffer[write_offset..])? {
ReaderRet::Bits(_bit_vec) => return Err(DekuError::Parse("Unexpected result reading size bytes: got bits".into())),
ReaderRet::Bytes => u32::from_be_bytes(*buffer).into(),
}
};
Ok(size)
}
/// Deku Reader taking an expected size into account
/// This function limit the reading to the size given in input
fn read_with_size<T, R: std::io::Read + std::io::Seek>(
reader: &mut Reader<R>,
size: usize
) -> Result<T, DekuError>
where T: for<'a> DekuContainerRead<'a>
{
let max_size = core::mem::size_of::<T>();
let mut buf = vec![0; max_size];
let buf: &mut [u8] = &mut buf;
let ret = reader.read_bytes(size, buf)?;
let (_rest, block) = match ret {
ReaderRet::Bytes => {
T::from_bytes((buf, 0))?
},
_ => return Err(DekuError::Parse("Unexpected result reading size bytes: got bits".into())),
};
Ok(block)
}
/// # Map bytes to a lossy string
/// This function is used to map bytes to a string, ignoring invalid UTF-8 characters
/// Example: [0x41, 0x42] -> "AB"
/// Example: [48, 49, 50, 51] -> "0123"
fn map_bytes_to_lossy_string(data: &[u8]) -> Result<String, DekuError> {
// let data = data.to_vec();
let version: String = String::from_utf8_lossy(data).to_string();
Ok(version)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_read_with_size_reader() {
#[derive(Debug, PartialEq)]
#[deku_derive(DekuRead)]
struct Data {
#[deku(read_all)]
pub blocks: Vec<Block>,
}
#[derive(Debug, PartialEq)]
#[deku_derive(DekuRead)]
struct Block {
pub size: u8,
pub id: u8,
#[deku(ctx = "*id,*size-1")] // size-1 to remove the ID size
pub data: BlockType,
}
#[derive(Debug, PartialEq)]
#[deku_derive(DekuRead)]
#[deku(ctx = "id: u8, size: u8", id = "id")]
enum BlockType {
#[deku(id = 1)]
Block1(
#[deku(reader = "read_with_size(deku::reader, size as usize)")]
Block1
),
#[deku(id = 2)]
Block2(
#[deku(reader = "read_with_size(deku::reader, size as usize)")]
Block2
),
}
#[derive(Debug, PartialEq)]
#[deku_derive(DekuRead)]
struct Block1 {
pub field1_size: u8,
pub field1: u16,
pub field2_size: u8,
pub field2: u64,
}
#[derive(Debug, PartialEq)]
#[deku_derive(DekuRead)]
struct Block2;
let buffer = &[
// 1st Block, type 1
4, // Size: 4
1, // ID: 1
2, 0x12, 0x34, // Field 1, size 2
// No Field 2
// 2nd Block, type 1
6, // Size: Y
1, // ID: 2
1, 0x56, // Field 1, size 1 (casted into u16)
2, 0x78, 0x9A // Field 2, size 2
];
let (_rest, val) = Data::from_bytes((buffer, 0)).unwrap();
assert_eq!(val.blocks.len(), 2);
assert_eq!(val.blocks[0].size, 4);
assert_eq!(val.blocks[1].size, 6);
}
}

View File

@ -0,0 +1,6 @@
/*! Crate prelude
[What is a prelude?](std::prelude)
*/
pub use deku::DekuContainerRead as _;
pub use super::Data;

View File

@ -1 +1,2 @@
mod ssv;
pub mod fsv_parsing;
pub mod ssv;

View File

@ -13,7 +13,9 @@ use fsv_sys::{
};
mod errors_ssv;
use errors_ssv::SSVErrorCodes;
pub use errors_ssv::SSVErrorCodes;
use crate::fsv_parsing::prelude::*;
#[derive(Error, Debug)]
pub enum Error {
@ -24,7 +26,7 @@ pub enum Error {
}
/// Enum to hold the different versions of the SSV library
enum SsvLibraryVersion {
pub enum SsvLibraryVersion {
V1_40_13(SSVLibrary<V1_40_13>),
V1_40_14(SSVLibrary<V1_40_14>),
}
@ -35,7 +37,10 @@ pub struct SSV {
}
impl SSV {
fn new(version: SupportedFsvVersion) -> Result<Self, Error> {
const CPS_READER_NAME: &'static str = "Gemalto PC Twin Reader (645D94C3) 00 00";
const VITALE_READER_NAME: &'static str = "Gemalto PC Twin Reader (645D94C3) 00 00"; // TODO: Change this to the correct reader name
pub fn new(version: SupportedFsvVersion) -> Result<Self, Error> {
let library = match version {
SupportedFsvVersion::V1_40_13 => {
let lib_path = get_library_path(&version);
@ -74,19 +79,19 @@ impl SSV {
/// # Read the CPS card
/// Implement: SSV_LireCartePS
pub fn read_professional_card(&self, pin_code: &str) -> Result<(), Error> {
let pcsc_reader_name = "Gemalto PC Twin Reader (645D94C3) 00 00";
pub fn read_professional_card(&self, pin_code: &str) -> Result<Data, Error> {
let pin_code = CString::new(pin_code).expect("CString::new failed");
let pcsc_reader_name = CString::new(pcsc_reader_name).expect("CString::new failed");
let cps_pcsc_reader_name = CString::new(SSV::CPS_READER_NAME).expect("CString::new failed");
let vitale_pcsc_reader_name = CString::new(SSV::VITALE_READER_NAME).expect("CString::new failed");
let mut out_buffer_ptr: *mut libc::c_void = ptr::null_mut();
let mut out_buffer_size: libc::size_t = 0;
let result = match &self.library {
SsvLibraryVersion::V1_40_13(library) => {
unsafe { library.ssv_lire_carte_ps(
pcsc_reader_name.as_ptr(),
pcsc_reader_name.as_ptr(),
cps_pcsc_reader_name.as_ptr(),
vitale_pcsc_reader_name.as_ptr(),
pin_code.as_ptr(),
&mut out_buffer_ptr,
&mut out_buffer_size)
@ -94,8 +99,8 @@ impl SSV {
},
SsvLibraryVersion::V1_40_14(library) => {
unsafe { library.ssv_lire_carte_ps(
pcsc_reader_name.as_ptr(),
pcsc_reader_name.as_ptr(),
cps_pcsc_reader_name.as_ptr(),
vitale_pcsc_reader_name.as_ptr(),
pin_code.as_ptr(),
&mut out_buffer_ptr,
&mut out_buffer_size)
@ -109,42 +114,96 @@ impl SSV {
let error = SSVErrorCodes::from(result);
return Err(Error::SSVError(error));
}
// Print 10 bytes of the buffer
let buffer = unsafe { std::slice::from_raw_parts(out_buffer_ptr as *const u8, 10) };
println!("{:?}", buffer);
// Parse the buffer into a Data struct
let buffer = unsafe { std::slice::from_raw_parts(out_buffer_ptr as *const u8, out_buffer_size) };
let (_rest, cps_blocks) = Data::from_bytes((buffer, 0)).unwrap();
// Free memory
unsafe { libc::free(out_buffer_ptr) };
Ok(())
Ok(cps_blocks)
}
/// # Get the configuration of the SSV library
/// Implement: SSV_LireConfig
pub fn get_config(&self) -> Result<(), Error> {
let mut buffer_ptr: *mut libc::c_void = ptr::null_mut();
let mut size: libc::size_t = 0;
pub fn get_config(&self) -> Result<Data, Error> {
let mut out_buffer_ptr: *mut libc::c_void = ptr::null_mut();
let mut out_buffer_size: libc::size_t = 0;
let result = match &self.library {
SsvLibraryVersion::V1_40_13(library) => {
unsafe { library.ssv_lire_config(&mut buffer_ptr, &mut size) }?
unsafe { library.ssv_lire_config(&mut out_buffer_ptr, &mut out_buffer_size) }?
},
SsvLibraryVersion::V1_40_14(library) => {
unsafe { library.ssv_lire_config(&mut buffer_ptr, &mut size) }?
unsafe { library.ssv_lire_config(&mut out_buffer_ptr, &mut out_buffer_size) }?
},
};
if result != 0 {
// Free memory
unsafe { libc::free(buffer_ptr) };
unsafe { libc::free(out_buffer_ptr) };
let error = SSVErrorCodes::from(result);
return Err(Error::SSVError(error));
}
// Print 10 bytes of the buffer
let buffer = unsafe { std::slice::from_raw_parts(buffer_ptr as *const u8, 10) };
println!("{:?}", buffer);
// Parse the buffer into a Data struct
let buffer = unsafe { std::slice::from_raw_parts(out_buffer_ptr as *const u8, out_buffer_size) };
let (_rest, config_blocks) = Data::from_bytes((buffer, 0)).unwrap();
// Free memory
unsafe { libc::free(buffer_ptr) };
Ok(())
unsafe { libc::free(out_buffer_ptr) };
Ok(config_blocks)
}
pub fn get_vitale_card_rights(&self, pin_code: &str) -> Result<Data, Error> {
let pin_code = CString::new(pin_code).expect("CString::new failed");
let cps_pcsc_reader_name = CString::new(SSV::CPS_READER_NAME).expect("CString::new failed");
let vitale_pcsc_reader_name = CString::new(SSV::VITALE_READER_NAME).expect("CString::new failed");
// Today's date, in the format YYYYMMDD
let today = chrono::Local::now().format("%Y%m%d").to_string();
let date_consultation = CString::new(today).expect("CString::new failed");
let mut out_buffer_ptr: *mut libc::c_void = ptr::null_mut();
let mut out_buffer_size: libc::size_t = 0;
let result = match &self.library {
SsvLibraryVersion::V1_40_13(library) => {
unsafe { library.ssv_lire_droits_vitale(
cps_pcsc_reader_name.as_ptr(),
vitale_pcsc_reader_name.as_ptr(),
pin_code.as_ptr(),
date_consultation.as_ptr(),
&mut out_buffer_ptr,
&mut out_buffer_size)
}?
},
SsvLibraryVersion::V1_40_14(library) => {
unsafe { library.ssv_lire_droits_vitale(
cps_pcsc_reader_name.as_ptr(),
vitale_pcsc_reader_name.as_ptr(),
pin_code.as_ptr(),
date_consultation.as_ptr(),
&mut out_buffer_ptr,
&mut out_buffer_size)
}?
},
};
if result != 0 {
// Free memory
unsafe { libc::free(out_buffer_ptr) };
let error = SSVErrorCodes::from(result);
return Err(Error::SSVError(error));
}
// Parse the buffer into a Data struct
let buffer = unsafe { std::slice::from_raw_parts(out_buffer_ptr as *const u8, out_buffer_size) };
////////////////////////////
println!("{:?}", buffer);
////////////////////////////
let (_rest, vitale_blocks) = Data::from_bytes((buffer, 0)).unwrap();
// Free memory
unsafe { libc::free(out_buffer_ptr) };
Ok(vitale_blocks)
}
}
#[cfg(test)]
@ -152,7 +211,9 @@ mod tests {
use std::env;
use utils::config::load_config;
use anyhow::Result;
use anyhow::{bail, Result};
use crate::fsv_parsing::{blocks::DataGroup, groups::ssv_lire_carte_ps::{group_1_holder::CardPSType, group_2_situation::{PracticeMode, SpecialtyCode}}};
use super::*;
@ -160,7 +221,7 @@ mod tests {
use super::*;
pub fn init() -> Result<SSV> {
load_config().unwrap();
load_config(None)?;
let sesam_ini_path = env::var("SESAM_INI_PATH").expect("SESAM_INI_PATH must be set");
let lib = SSV::new(SupportedFsvVersion::V1_40_13)?;
lib.init_library(&sesam_ini_path)?;
@ -169,21 +230,46 @@ mod tests {
}
#[test]
#[ignore="Not working with other tests using SSV library in parallel - Need to fix"]
// We should implement a way to initialize the library only once
// Or implement them sequentially with [serial_test crate](https://docs.rs/serial_test/latest/serial_test)
fn test_init_library() -> Result<()> {
setup::init()?;
Ok(())
}
#[test]
#[ignore="
WARNING: Read the card with PIN 1234 - Risk of blocking the card if it's not the right card
WARNING: This test will only work with GILBERT's PHARMOFFICE card (titulaire kit pharmacie)
"]
fn test_read_professional_card_good_pin() -> Result<()> {
let lib = setup::init()?;
let pin_code = "1234";
lib.read_professional_card(pin_code)?;
let cps_blocks = lib.read_professional_card(pin_code)?;
// Check the first group is the holder group
let holder_group = cps_blocks.blocks.first().unwrap();
assert_eq!(holder_group.header.group_id.0, 1);
let holder_content = match &holder_group.content {
DataGroup::LireCartePS_Group1_Holder(content) => { content },
_ => bail!("Wrong group type"),
};
assert_eq!(holder_content.card_type, CardPSType::CPS, "Card type");
assert_eq!(holder_content.holder_firstname.0, "GILBERT", "Holder firstname");
// Check the second group is a situation group
let situation_group = cps_blocks.blocks.get(1).unwrap();
assert_eq!(situation_group.header.group_id.0, 2);
let situation_content = match &situation_group.content {
DataGroup::LireCartePS_Group2_Situation(content) => { content },
_ => bail!("Wrong group type"),
};
assert_eq!(situation_content.practice_mode, PracticeMode::Liberal, "Practice mode");
assert_eq!(situation_content.specialty_code, SpecialtyCode::PharmacieDOfficine, "Specialty code");
Ok(())
}
#[ignore]
#[test]
#[ignore="WARNING: Read the card with PIN 0000 - Risk of blocking the card if it's not the right card"]
fn test_read_professional_card_bad_pin() -> Result<()> {
let lib = setup::init()?;
let pin_code = "0000";
@ -194,15 +280,38 @@ mod tests {
Error::SSVError(err) => {
assert_eq!(err as SSVErrorCodes, SSVErrorCodes::CPSPinWrong);
},
_ => panic!("Error type is not SSVError"),
_ => bail!("Error type is not SSVError"),
}
Ok(())
}
#[test]
// #[ignore="Needs a valid FSV installation"]
fn test_get_config() -> Result<()> {
let lib = setup::init()?;
lib.get_config()?;
let data = lib.get_config()?;
// I don't know what to assert here ...
let header_group = data.blocks.first().unwrap();
assert_eq!(header_group.header.group_id.0, 60);
let header_content = match &header_group.content {
DataGroup::LireConfig_Group60_ConfigHeader(content) => { content },
_ => bail!("Wrong group type"),
};
assert_eq!(header_content.ssv_version.0.version, "07");
assert_eq!(header_content.ssv_version.0.revision, "20");
Ok(())
}
#[test]
#[ignore="
WARNING: Read the card with PIN 1234 - Risk of blocking the card if it's not the right card
WARNING: This test needs a CPS and a VITALE card available simultaneously
"]
fn test_get_vitale_card_rights() -> Result<()> {
let lib = setup::init()?;
let pin_code = "1234";
let _cps_blocks = lib.read_professional_card(pin_code)?;
let _vitale_blocks = lib.get_vitale_card_rights(pin_code)?;
Ok(())
}
}

View File

@ -0,0 +1,97 @@
---
source: crates/fsv/tests/test_ssv.rs
expression: cps_blocks
---
Data {
blocks: [
DataBlock {
header: BlockHeader {
group_id: GroupId(
1,
),
data_size: 36,
},
content: LireCartePS_Group1_Holder(
Holder {
card_type: CPS,
national_id_type: RPPS,
national_id: AlphaNumericString(
"99700619010",
),
national_id_key: AlphaNumericString(
"0",
),
civility_code: Monsieur,
holder_lastname: AlphaNumericString(
"DOC0061901",
),
holder_firstname: AlphaNumericString(
"KIT",
),
category_card: Unknown,
},
),
},
DataBlock {
header: BlockHeader {
group_id: GroupId(
2,
),
data_size: 84,
},
content: LireCartePS_Group2_Situation(
Situation {
id: RawByte(
2,
),
practice_mode: Liberal,
practice_status_raw: NumericString(
"1",
),
activity_sector: CabinetIndividuel,
structure_id_type: RPPSCabinet,
structure_id: AlphaNumericString(
"99700619010009",
),
structure_id_key: NumericString(
"0",
),
structure_name: AlphaNumericString(
"CABINET M DOC0061901",
),
ps_billing_number: NumericString(
"00102901",
),
ps_billing_number_key: NumericString(
"6",
),
ps_replacement_number: AlphaNumericString(
"",
),
ps_replacement_number_key: NumericString(
"0",
),
convention_code: Conventionne,
specialty_code: MedecineGenerale,
rate_zone_code_raw: NumericString(
"24",
),
ik_zone_code: IndemniteKilometriquePlaine,
approval_code_1: PasAgrementRadio,
approval_code_2_raw: NumericString(
"0",
),
approval_code_3_raw: NumericString(
"0",
),
invoice_signature_permission: NumericString(
"1",
),
lot_signature_permission: NumericString(
"1",
),
},
),
},
],
}

View File

@ -0,0 +1,97 @@
---
source: crates/fsv/tests/test_ssv.rs
expression: cps_blocks
---
Data {
blocks: [
DataBlock {
header: BlockHeader {
group_id: GroupId(
1,
),
data_size: 55,
},
content: LireCartePS_Group1_Holder(
Holder {
card_type: CPS,
national_id_type: RPPS,
national_id: AlphaNumericString(
"99700593694",
),
national_id_key: AlphaNumericString(
"4",
),
civility_code: Madame,
holder_lastname: AlphaNumericString(
"ADJOINTPHARM RPPS0059369",
),
holder_firstname: AlphaNumericString(
"PATRICIA",
),
category_card: Unknown,
},
),
},
DataBlock {
header: BlockHeader {
group_id: GroupId(
2,
),
data_size: 93,
},
content: LireCartePS_Group2_Situation(
Situation {
id: RawByte(
1,
),
practice_mode: Salarie,
practice_status_raw: NumericString(
"2",
),
activity_sector: PharmacieDOfficine,
structure_id_type: FINESS,
structure_id: AlphaNumericString(
"0B0246286",
),
structure_id_key: NumericString(
"6",
),
structure_name: AlphaNumericString(
"PHARMACIE DE LA GARE ROUTIERE24628",
),
ps_billing_number: NumericString(
"00209368",
),
ps_billing_number_key: NumericString(
"0",
),
ps_replacement_number: AlphaNumericString(
"",
),
ps_replacement_number_key: NumericString(
"0",
),
convention_code: Conventionne,
specialty_code: PharmacieDOfficine,
rate_zone_code_raw: NumericString(
"10",
),
ik_zone_code: PasDIndemniteKilometrique,
approval_code_1: PasAgrementRadio,
approval_code_2_raw: NumericString(
"0",
),
approval_code_3_raw: NumericString(
"0",
),
invoice_signature_permission: NumericString(
"1",
),
lot_signature_permission: NumericString(
"1",
),
},
),
},
],
}

View File

@ -0,0 +1,97 @@
---
source: crates/fsv/tests/test_ssv.rs
expression: cps_blocks
---
Data {
blocks: [
DataBlock {
header: BlockHeader {
group_id: GroupId(
1,
),
data_size: 49,
},
content: LireCartePS_Group1_Holder(
Holder {
card_type: CPE,
national_id_type: FINESS,
national_id: AlphaNumericString(
"0B0246286/CPET00001",
),
national_id_key: AlphaNumericString(
"6",
),
civility_code: Monsieur,
holder_lastname: AlphaNumericString(
"EMPLOYE246280001",
),
holder_firstname: AlphaNumericString(
"AA",
),
category_card: Unknown,
},
),
},
DataBlock {
header: BlockHeader {
group_id: GroupId(
2,
),
data_size: 93,
},
content: LireCartePS_Group2_Situation(
Situation {
id: RawByte(
1,
),
practice_mode: Salarie,
practice_status_raw: NumericString(
"0",
),
activity_sector: PharmacieDOfficine,
structure_id_type: FINESS,
structure_id: AlphaNumericString(
"0B0246286",
),
structure_id_key: NumericString(
"6",
),
structure_name: AlphaNumericString(
"PHARMACIE DE LA GARE ROUTIERE24628",
),
ps_billing_number: NumericString(
"00209368",
),
ps_billing_number_key: NumericString(
"0",
),
ps_replacement_number: AlphaNumericString(
"",
),
ps_replacement_number_key: NumericString(
"0",
),
convention_code: Conventionne,
specialty_code: PharmacieDOfficine,
rate_zone_code_raw: NumericString(
"10",
),
ik_zone_code: PasDIndemniteKilometrique,
approval_code_1: PasAgrementRadio,
approval_code_2_raw: NumericString(
"0",
),
approval_code_3_raw: NumericString(
"0",
),
invoice_signature_permission: NumericString(
"1",
),
lot_signature_permission: NumericString(
"0",
),
},
),
},
],
}

View File

@ -0,0 +1,97 @@
---
source: crates/fsv/tests/test_ssv.rs
expression: cps_blocks
---
Data {
blocks: [
DataBlock {
header: BlockHeader {
group_id: GroupId(
1,
),
data_size: 50,
},
content: LireCartePS_Group1_Holder(
Holder {
card_type: CPS,
national_id_type: RPPS,
national_id: AlphaNumericString(
"99700520499",
),
national_id_key: AlphaNumericString(
"9",
),
civility_code: Madame,
holder_lastname: AlphaNumericString(
"INFIRMIERE RPPS0052049",
),
holder_firstname: AlphaNumericString(
"ALINE",
),
category_card: Unknown,
},
),
},
DataBlock {
header: BlockHeader {
group_id: GroupId(
2,
),
data_size: 93,
},
content: LireCartePS_Group2_Situation(
Situation {
id: RawByte(
1,
),
practice_mode: Liberal,
practice_status_raw: NumericString(
"1",
),
activity_sector: CabinetIndividuel,
structure_id_type: RPPSCabinet,
structure_id: AlphaNumericString(
"99700520499002",
),
structure_id_key: NumericString(
"0",
),
structure_name: AlphaNumericString(
"CABINET MME INFIRMIERE0052049",
),
ps_billing_number: NumericString(
"00602049",
),
ps_billing_number_key: NumericString(
"9",
),
ps_replacement_number: AlphaNumericString(
"",
),
ps_replacement_number_key: NumericString(
"0",
),
convention_code: Conventionne,
specialty_code: Infirmier,
rate_zone_code_raw: NumericString(
"20",
),
ik_zone_code: IndemniteKilometriqueMontagne,
approval_code_1: PasAgrementRadio,
approval_code_2_raw: NumericString(
"0",
),
approval_code_3_raw: NumericString(
"0",
),
invoice_signature_permission: NumericString(
"1",
),
lot_signature_permission: NumericString(
"1",
),
},
),
},
],
}

View File

@ -0,0 +1,97 @@
---
source: crates/fsv/tests/test_ssv.rs
expression: cps_blocks
---
Data {
blocks: [
DataBlock {
header: BlockHeader {
group_id: GroupId(
1,
),
data_size: 53,
},
content: LireCartePS_Group1_Holder(
Holder {
card_type: CPS,
national_id_type: RPPS,
national_id: AlphaNumericString(
"99700593686",
),
national_id_key: AlphaNumericString(
"6",
),
civility_code: Monsieur,
holder_lastname: AlphaNumericString(
"PHARMOFFICE RPPS0059368",
),
holder_firstname: AlphaNumericString(
"GILBERT",
),
category_card: Unknown,
},
),
},
DataBlock {
header: BlockHeader {
group_id: GroupId(
2,
),
data_size: 93,
},
content: LireCartePS_Group2_Situation(
Situation {
id: RawByte(
1,
),
practice_mode: Liberal,
practice_status_raw: NumericString(
"1",
),
activity_sector: PharmacieDOfficine,
structure_id_type: FINESS,
structure_id: AlphaNumericString(
"0B0246286",
),
structure_id_key: NumericString(
"6",
),
structure_name: AlphaNumericString(
"PHARMACIE DE LA GARE ROUTIERE24628",
),
ps_billing_number: NumericString(
"00209368",
),
ps_billing_number_key: NumericString(
"0",
),
ps_replacement_number: AlphaNumericString(
"",
),
ps_replacement_number_key: NumericString(
"0",
),
convention_code: Conventionne,
specialty_code: PharmacieDOfficine,
rate_zone_code_raw: NumericString(
"10",
),
ik_zone_code: PasDIndemniteKilometrique,
approval_code_1: PasAgrementRadio,
approval_code_2_raw: NumericString(
"0",
),
approval_code_3_raw: NumericString(
"0",
),
invoice_signature_permission: NumericString(
"1",
),
lot_signature_permission: NumericString(
"1",
),
},
),
},
],
}

View File

@ -0,0 +1,174 @@
use anyhow::Result;
use common::read_cps;
use fsv::ssv::{Error, SSVErrorCodes, SSV};
use insta::assert_debug_snapshot;
mod common {
use super::*;
use std::env;
use fsv::fsv_parsing::Data;
use fsv_sys::SupportedFsvVersion;
use utils::config::load_config;
pub fn init() -> Result<SSV> {
load_config(None)?;
let sesam_ini_path = env::var("SESAM_INI_PATH").expect("SESAM_INI_PATH must be set");
let lib = SSV::new(SupportedFsvVersion::V1_40_13)?;
lib.init_library(&sesam_ini_path)?;
Ok(lib)
}
pub fn read_cps() -> Result<Data> {
let lib = init()?;
Ok(lib.read_professional_card("1234")?)
}
}
// ---------------- PHARMA KIT ----------------
#[test]
#[ignore]
fn test_read_cps_pharma_kit_titulaire() -> Result<()> {
let cps_blocks = read_cps()?;
assert_debug_snapshot!(cps_blocks);
Ok(())
}
#[test]
#[ignore]
fn test_read_cps_pharma_kit_adjointe() -> Result<()> {
let cps_blocks = read_cps()?;
assert_debug_snapshot!(cps_blocks);
Ok(())
}
#[test]
#[ignore]
fn test_read_cps_pharma_kit_employe_aa() -> Result<()> {
let cps_blocks = read_cps()?;
assert_debug_snapshot!(cps_blocks);
Ok(())
}
#[test]
#[ignore]
fn test_read_cps_pharma_kit_infirmiere() -> Result<()> {
let cps_blocks = read_cps()?;
assert_debug_snapshot!(cps_blocks);
Ok(())
}
// ---------------- DEFAULT KIT ----------------
#[test]
#[ignore]
fn test_read_cps_default_kit_doc() -> Result<()> {
let cps_blocks = read_cps()?;
assert_debug_snapshot!(cps_blocks);
Ok(())
}
#[test]
#[ignore]
fn test_read_cps_default_kit_directeur() -> Result<()> {
let error = read_cps().expect_err("This card should be opposed and not be readable");
match error.downcast_ref::<Error>() {
Some(Error::SSVError(e)) => {
match e {
SSVErrorCodes::CPSInvalid => { Ok(()) },
_ => panic!("Expected SSVErrorCodes::CPSInvalid, got {:?}", e),
}
},
_ => panic!("Expected Error::SSVError got {:?}", error),
}
}
#[test]
#[ignore]
fn test_read_cps_default_kit_employee_opposee() -> Result<()> {
let error = read_cps().expect_err("This card should be opposed and not be readable");
match error.downcast_ref::<Error>() {
Some(Error::SSVError(e)) => {
match e {
SSVErrorCodes::SSVInternalError => { Ok(()) },
_ => panic!("Expected SSVErrorCodes::SSVInternalError, got {:?}", e),
}
},
_ => panic!("Expected Error::SSVError got {:?}", error),
}
}
#[test]
#[ignore]
fn test_read_cps_default_kit_doc_maximaxima() -> Result<()> {
// TODO : debug this card (ConventionCode value is 9)
let cps_blocks = read_cps()?;
assert_debug_snapshot!(cps_blocks);
Ok(())
}
#[test]
#[ignore]
fn test_read_cps_default_kit_orthophoniste() -> Result<()> {
let error = read_cps().expect_err("This card should be opposed and not be readable");
match error.downcast_ref::<Error>() {
Some(Error::SSVError(e)) => {
match e {
SSVErrorCodes::SSVInternalError => { Ok(()) },
_ => panic!("Expected SSVErrorCodes::SSVInternalError, got {:?}", e),
}
},
_ => panic!("Expected Error::SSVError got {:?}", error),
}
}
// ---------------- CENTRE SANTÉ ----------------
#[test]
#[ignore]
fn test_read_cps_centre_sante_kit_directeur() -> Result<()> {
// TODO : debug this card (ConventionCode value is 9)
let cps_blocks = read_cps()?;
assert_debug_snapshot!(cps_blocks);
Ok(())
}
#[test]
#[ignore]
fn test_read_cps_centre_sante_kit_doc() -> Result<()> {
// TODO : debug this card (ConventionCode value is 9)
let cps_blocks = read_cps()?;
assert_debug_snapshot!(cps_blocks);
Ok(())
}
#[test]
#[ignore]
fn test_read_cps_centre_sante_kit_dentiste() -> Result<()> {
// TODO : debug this card (ConventionCode value is 9)
let cps_blocks = read_cps()?;
assert_debug_snapshot!(cps_blocks);
Ok(())
}
#[test]
#[ignore]
fn test_read_cps_centre_sante_kit_assist_vladimir() -> Result<()> {
// TODO : debug this card (ConventionCode value is 9)
let cps_blocks = read_cps()?;
assert_debug_snapshot!(cps_blocks);
Ok(())
}
#[test]
#[ignore]
fn test_read_cps_centre_sante_kit_assist_marie() -> Result<()> {
// TODO : debug this card (ConventionCode value is 9)
let cps_blocks = read_cps()?;
assert_debug_snapshot!(cps_blocks);
Ok(())
}

View File

@ -17,6 +17,6 @@
if (user.avatar) {
return user.avatar;
}
return 'https://avatar.iran.liara.run/username?username=' + user.name;
return 'https://i.pravatar.cc/150?u=' + user.name;
};
</script>

View File

@ -29,15 +29,15 @@
const users: User[] = [
{ id: 1, name: 'John Doe', avatar: 'https://img.daisyui.com/images/stock/photo-1534528741775-53994a69daeb.webp' },
{ id: 2, name: 'Jane Doe', avatar: 'https://avatar.iran.liara.run/public' },
{ id: 3, name: 'Michel Moulin', avatar: '' },
{ id: 4, name: 'Jean Paris', avatar: '' },
{ id: 5, name: 'Marie Dupont', avatar: '' },
{ id: 6, name: 'Émilie Fournier', avatar: '' },
{ id: 7, name: 'Pierre Lefevre', avatar: '' },
{ id: 8, name: 'Sophie Lemoine', avatar: '' },
{ id: 9, name: 'Lucie Simon', avatar: '' },
{ id: 10, name: 'Kevin Boucher', avatar: '' },
{ id: 2, name: 'Jane Doe', avatar: 'https://i.pravatar.cc/150?u=JANEDOE728' },
{ id: 3, name: 'Michel Moulin' },
{ id: 4, name: 'Jean Paris' },
{ id: 5, name: 'Marie Dupont' },
{ id: 6, name: 'Émilie Fournier' },
{ id: 7, name: 'Pierre Lefevre' },
{ id: 8, name: 'Sophie Lemoine' },
{ id: 9, name: 'Lucie Simon' },
{ id: 10, name: 'Kevin Boucher' },
];
const loginModal = useTemplateRef('login_modal');