9 Commits

85 changed files with 1207 additions and 5439 deletions

View File

@ -24,10 +24,6 @@ body:
label: Ce problème est il relatif à un ou des modules en particulier ? label: Ce problème est il relatif à un ou des modules en particulier ?
multiple: true multiple: true
options: options:
- Interface utilisateur⋅ice (crates/app) - Clego
- Encapsulation Tauri (crates/desktop) - Tauri
- Moteur SESAM-Vitale (crates/sesam-vitale) - Axum
- Librairie utilitaire (crates/utils)
- Documentation (docs)
- Scripts (scripts)
- Autre

2
.gitignore vendored
View File

@ -21,5 +21,3 @@ target/
*.sln *.sln
*.sw? *.sw?
# Ignore .env files
.env

View File

@ -1,5 +0,0 @@
# Ignorer les fichiers dont ne dépent pas la compilation
*.md
tailwind.config.js
*.example
scripts

2011
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,9 +1,7 @@
[workspace] [workspace]
resolver = "2" resolver = "2"
members = [ members = [
"crates/backend", "crates/app",
"crates/desktop",
"crates/sesam-vitale", "crates/sesam-vitale",
"crates/services-sesam-vitale-sys", "crates/desktop"
"crates/utils",
] ]

View File

@ -2,75 +2,37 @@
Logiciel de Pharmacie libre et open-source. Logiciel de Pharmacie libre et open-source.
## Modules applicatifs ## Crates
- `crates`: Dossier racine des modules Rust - `app`: Interface du logiciel, servie par un serveur web propulsé par Axum. Utilisable en mode endpoint ou encapsulé dans le client `desktop`
- `crates/backend`: Serveur backend propulsé par Axum, exposant une API REST - `desktop`: Client desktop propulsé par Tauri, encapsulant le serveur web `app`
- `crates/desktop`: Client desktop propulsé par Tauri, exposant le `frontend` - `sesam-vitale`: Bibliothèque de gestion des services SESAM-Vitale (Lecture des cartes CPS et Vitale, téléservices ...)
- `crates/sesam-vitale`: Bibliothèque de gestion des services SESAM-Vitale (Lecture des cartes CPS et Vitale, téléservices ...)
- `crates/utils`: Bibliothèque de fonctions utilitaires
- `frontend`: Interface web du logiciel, propulsée par Nuxt.js
## Installation
### Fichiers de configuration
Certaines librairies nécessitent de définir certaines paramètres de configuration pour fonctionner correctement, en particulier le moteur SESAM-Vitale.
Ces paramètres sont définis dans un fichier de configuration `.env` situé dans un des dossiers suivant (par ordre de priorité) :
- dans le dossier courant (`./.env`)
- dans le dossier du manifeste (par exemple `crates/sesam-vitale/.env`)
- dans le dossier de configuration standard de l'OS (par exemple, sur linux, `~/.config/krys4lide/.env` - [plus d'info](https://github.com/dirs-dev/directories-rs?tab=readme-ov-file#projectdirs))
Des exemples de fichiers de configuration sont disponibles à la racine du projet : `.env.linux.example` et `.env.win.example`.
## Development ## Development
### Pré-requis ### Pré-requis
#### Frontend (Nuxt + Typescript)
Le frontend est propulsé par Nuxt.js, un framework TypeScript pour Vue.js. Pour le développement, il est nécessaire d'installer les dépendances suivantes :
- [Bun](https://bun.sh/docs/installation), un gestionnaire de paquets, équivalent à `npm` en plus performant
#### Tauri CLI
TODO: Tauri CLI, réellement nécessaire ?
La CLI Tauri est nécessaire au lancement du client `desktop`. Elle peut être installée via Cargo : La CLI Tauri est nécessaire au lancement du client `desktop`. Elle peut être installée via Cargo :
```bash ```bash
cargo install tauri-cli --version "^2.0.0-rc" cargo install tauri-cli
``` ```
#### SESAM-Vitale ### Exécution de l'application cliente desktop
La crate `sesam-vitale` nécessite la présence des librairies dynamiques fournies par le package FSV et la CryptolibCPS. Les instructions d'installation sont disponibles dans le [README](crates/sesam-vitale/README.md) de la crate `sesam-vitale`.
#### Backend Hot-reload
Voir le [README](crates/backend/README.md) de la crate `backend` pour les prérequis de développement du serveur backend.
### Lancement
Pour lancer l'application en mode développement, il est nécessaire d'exécuter plusieurs composants simultanément :
```bash ```bash
# Lancement du serveur backend
systemfd --no-pid -s http::3030 -- cargo watch -x 'run --bin backend'
```
```bash
# Lancement de l'interface utilisateur (frontend ou desktop)
# - frontend (serveur web, accessible via navigateur)
bun run --cwd frontend/ dev
# - desktop (client desktop, basé sur Tauri)
cargo tauri dev cargo tauri dev
``` ```
### Exécution du serveur web `app` en mode endpoint
```bash
cargo run --bin app
```
## Build ## 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 : Packager le client desktop
```bash ```bash
cargo tauri build cargo tauri build

View File

@ -1,4 +1 @@
/target /target
# Tailwind CSS CLI
tailwindcss

View File

@ -7,15 +7,6 @@ edition = "2021"
askama = "0.12.1" askama = "0.12.1"
askama_axum = "0.4.0" askama_axum = "0.4.0"
axum = "0.7.5" axum = "0.7.5"
axum-htmx = { version = "0.6", features = ["auto-vary"] }
listenfd = "1.0.1"
notify = "6.1.1"
serde = { version = "1.0.204", features = ["derive"] }
thiserror = "1.0.63"
tokio = { version = "1.39.1", features = ["macros", "rt-multi-thread"] } tokio = { version = "1.39.1", features = ["macros", "rt-multi-thread"] }
tower-http = { version = "0.5.2", features = ["fs"] } tower-http = { version = "0.5.2", features = ["fs"] }
tower-livereload = "0.9.3"
[dev-dependencies]
cargo-watch = "8.5.1"
systemfd = "0.4.0"

View File

@ -1,35 +0,0 @@
## Pré-requis
- Récupérer le binaire TailwindCSS : https://tailwindcss.com/blog/standalone-cli
## Exécution
- Lancer tailwindcss en mode watch dans un terminal :
```bash
./tailwindcss -i css/input.css -o assets/css/style.css --watch
```
- Lancer le serveur web dans un autre terminal :
```bash
cargo run --bin app
```
## Rechargement automatique (_auto-reload_)
Pour le projet `app`, nous utilisons en plus de `cargo-watch` ses librairies :
- [`systemfd`](https://github.com/mitsuhiko/systemfd) permet de redémarrer un serveur sans interrompre les connexions en cours, il transmet le descripteur de fichier du socket à une nouvelle instance du serveur (exemple: `cargo watch -x run` --> `systemfd --no-pid -s http::3000 -- cargo watch -x run`). Si le port est déjà pris il en prendra un autre.
- [`listenfd`](https://github.com/mitsuhiko/listenfd) permet, côté _Rust_, de démarrer un serveur en utilisant des connexions déjà ouvertes.
Pour notre application voici la commande à lancer :
```bash
systemfd --no-pid -s http::3000 -- cargo watch -x 'run --bin app'
```
## Chargement à chaud (_livereload_)
Pour que notre navigateur rafraîchisse automatique notre page lorsque le serveur a été recompilé, nous utilisons la librairie [`tower-livereload`](https://github.com/leotaku/tower-livereload).
A chaque changement, que ça soit sur du code en _Rust_, _HTML_, _CSS_ ou _JS_ alors le navigateur va recharger entièrement la page.
En Rust, il n'existe pas encore d'outil de _Hot Reload_ complet et intégré comme on en trouve dans d'autres environnements de développement web, comme pour _Node.js_.

View File

@ -1,6 +0,0 @@
[general]
# Directories to search for templates, relative to the crate root.
dirs = [
"src/pages",
"src/components",
]

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,3 +0,0 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

View File

@ -1,23 +0,0 @@
{% if hx_request %}
<title>{% block title %}{{ title }}{% endblock %}</title>
{% block body %}{% endblock %}
{% else %}
<!doctype html>
<html lang="fr" class="h-full">
<head>
<title>{% block title %}{{ title }}{% endblock %}</title>
<script src="/assets/js/htmx@2.0.1.min.js"></script>
<script src="/assets/js/alpinejs@3.14.1.min.js" defer></script>
<script src="/assets/js/flowbite@2.5.1.min.js"></script>
<link href="/assets/css/style.css" rel="stylesheet">
{% block head %}{% endblock %}
</head>
<body class="h-full">
<div class="min-h-full">
{% block body %}{% endblock %}
</div>
</body>
</html>
{% endif %}

View File

@ -1,18 +0,0 @@
{% set selected = item.id == current %}
<li>
<a
href="{{ item.href }}"
{% if selected -%}
class="block py-2 px-3 text-white bg-blue-700 rounded md:bg-transparent md:text-blue-700 md:p-0 md:dark:text-blue-500"
aria-current="page"
{% else -%}
class="block py-2 px-3 text-gray-900 rounded hover:bg-gray-100 md:hover:bg-transparent md:hover:text-blue-700 md:p-0 dark:text-white md:dark:hover:text-blue-500 dark:hover:bg-gray-700 dark:hover:text-white md:dark:hover:bg-transparent dark:border-gray-700"
{% endif -%}
hx-get="{{ item.href }}"
hx-push-url="true"
hx-swap="outerHTML"
hx-select-oob="#menu-items,#page-header,#page-main"
>
{{ item.label }}
</a>
</li>

View File

@ -1,50 +0,0 @@
{% macro navbar(current) %}
{% let items=crate::menu::get_menu_items() %}
<nav class="bg-white border-gray-200 dark:bg-gray-900">
<div class="max-w-screen-xl flex flex-wrap items-center justify-between mx-auto p-4">
<a href="/" class="flex items-center space-x-3 rtl:space-x-reverse">
<img src="https://flowbite.com/docs/images/logo.svg" class="h-8" alt="Flowbite Logo" />
<span class="self-center text-2xl font-semibold whitespace-nowrap dark:text-white">Krys4lide</span>
</a>
<div class="flex items-center md:order-2 space-x-3 md:space-x-0 rtl:space-x-reverse">
<button type="button" class="flex text-sm bg-gray-800 rounded-full md:me-0 focus:ring-4 focus:ring-gray-300 dark:focus:ring-gray-600" id="user-menu-button" aria-expanded="false" data-dropdown-toggle="user-dropdown" data-dropdown-placement="bottom">
<span class="sr-only">Ouvrir le menu de profil</span>
<img class="w-8 h-8 rounded-full" src="https://flowbite.com/docs/images/people/profile-picture-3.jpg" alt="user photo">
</button>
<!-- Dropdown menu -->
<div class="z-50 hidden my-4 text-base list-none bg-white divide-y divide-gray-100 rounded-lg shadow dark:bg-gray-700 dark:divide-gray-600" id="user-dropdown">
<div class="px-4 py-3">
<span class="block text-sm text-gray-900 dark:text-white">Bonnie Green</span>
<span class="block text-sm text-gray-500 truncate dark:text-gray-400">name@flowbite.com</span>
</div>
<ul class="py-2" aria-labelledby="user-menu-button">
<li>
<a href="#" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-gray-200 dark:hover:text-white">Profile</a>
</li>
<li>
<a href="#" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-gray-200 dark:hover:text-white">Settings</a>
</li>
<li>
<a href="#" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-gray-200 dark:hover:text-white">Sign out</a>
</li>
</ul>
</div>
<button data-collapse-toggle="navbar-user" type="button" class="inline-flex items-center p-2 w-10 h-10 justify-center text-sm text-gray-500 rounded-lg md:hidden hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-gray-200 dark:text-gray-400 dark:hover:bg-gray-700 dark:focus:ring-gray-600" aria-controls="navbar-user" aria-expanded="false">
<span class="sr-only">Ouvrir le menu de navigation</span>
<svg class="w-5 h-5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 17 14">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M1 1h15M1 7h15M1 13h15"/>
</svg>
</button>
</div>
<div class="items-center justify-between hidden w-full md:flex md:w-auto md:order-1" id="navbar-user">
<ul id="menu-items" class="flex flex-col font-medium p-4 md:p-0 mt-4 border border-gray-100 rounded-lg bg-gray-50 md:space-x-8 rtl:space-x-reverse md:flex-row md:mt-0 md:border-0 md:bg-white dark:bg-gray-800 md:dark:bg-gray-900 dark:border-gray-700">
{% for item in items %}
{% include "navbar/menu-item.html" %}
{% endfor %}
</ul>
</div>
</div>
</nav>
{% endmacro %}

View File

@ -1,22 +0,0 @@
<div role="status" class="animate-pulse max-w-sm p-4 border border-gray-200 rounded shadow md:p-6 dark:border-gray-700">
<div class="flex items-center justify-center h-48 mb-4 bg-gray-300 rounded dark:bg-gray-700">
<svg class="w-10 h-10 text-gray-200 dark:text-gray-600" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 16 20">
<path d="M14.066 0H7v5a2 2 0 0 1-2 2H0v11a1.97 1.97 0 0 0 1.934 2h12.132A1.97 1.97 0 0 0 16 18V2a1.97 1.97 0 0 0-1.934-2ZM10.5 6a1.5 1.5 0 1 1 0 2.999A1.5 1.5 0 0 1 10.5 6Zm2.221 10.515a1 1 0 0 1-.858.485h-8a1 1 0 0 1-.9-1.43L5.6 10.039a.978.978 0 0 1 .936-.57 1 1 0 0 1 .9.632l1.181 2.981.541-1a.945.945 0 0 1 .883-.522 1 1 0 0 1 .879.529l1.832 3.438a1 1 0 0 1-.031.988Z"/>
<path d="M5 5V.13a2.96 2.96 0 0 0-1.293.749L.879 3.707A2.98 2.98 0 0 0 .13 5H5Z"/>
</svg>
</div>
<div class="h-2.5 bg-gray-200 rounded-full dark:bg-gray-700 w-48 mb-4"></div>
<div class="h-2 bg-gray-200 rounded-full dark:bg-gray-700 mb-2.5"></div>
<div class="h-2 bg-gray-200 rounded-full dark:bg-gray-700 mb-2.5"></div>
<div class="h-2 bg-gray-200 rounded-full dark:bg-gray-700"></div>
<div class="flex items-center mt-4">
<svg class="w-10 h-10 me-3 text-gray-200 dark:text-gray-700" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 20 20">
<path d="M10 0a10 10 0 1 0 10 10A10.011 10.011 0 0 0 10 0Zm0 5a3 3 0 1 1 0 6 3 3 0 0 1 0-6Zm0 13a8.949 8.949 0 0 1-4.951-1.488A3.987 3.987 0 0 1 9 13h2a3.987 3.987 0 0 1 3.951 3.512A8.949 8.949 0 0 1 10 18Z"/>
</svg>
<div>
<div class="h-2.5 bg-gray-200 rounded-full dark:bg-gray-700 w-32 mb-2"></div>
<div class="w-48 h-2 bg-gray-200 rounded-full dark:bg-gray-700"></div>
</div>
</div>
<span class="sr-only">Loading...</span>
</div>

View File

@ -1,4 +0,0 @@
<div role="status" class="animate-pulse flex items-center justify-center h-full">
<div class="w-32 h-4 bg-gray-200 rounded-full dark:bg-gray-700 me-3"></div>
<div class="w-32 h-4 bg-gray-200 rounded-full dark:bg-gray-700"></div>
</div>

View File

@ -1 +0,0 @@
<div role="status" class="animate-pulse h-7 bg-gray-200 rounded-full dark:bg-gray-700 w-48 mt-3"></div>

View File

@ -1,21 +1,27 @@
use std::path::PathBuf; mod templates;
use axum::http::{StatusCode, Uri}; use std::path::Path;
use axum_htmx::AutoVaryLayer;
use askama_axum::IntoResponse;
use templates::{hello::HelloResponse, index::GetIndexResponse};
use tower_http::services::ServeDir; use tower_http::services::ServeDir;
mod menu; async fn root() -> impl IntoResponse {
mod pages; return GetIndexResponse {}.into_response();
async fn fallback(uri: Uri) -> (StatusCode, String) {
(StatusCode::NOT_FOUND, format!("No route for {uri}"))
} }
pub async fn get_router(assets_path: PathBuf) -> axum::Router<()> { async fn hello() -> impl IntoResponse {
axum::Router::new() return HelloResponse {
name: "Theo".to_string(),
}
.into_response();
}
pub fn get_router(assets_path: &Path) -> axum::Router {
let router = axum::Router::new()
.nest_service("/assets", ServeDir::new(assets_path)) .nest_service("/assets", ServeDir::new(assets_path))
.merge(pages::get_routes()) .route("/", axum::routing::get(root))
.fallback(fallback) .route("/hello", axum::routing::get(hello));
// The AutoVaryLayer is used to avoid cache issues with htmx (cf: https://github.com/robertwayne/axum-htmx?tab=readme-ov-file#auto-caching-management)
.layer(AutoVaryLayer) router
} }

View File

@ -1,84 +1,14 @@
use std::path::{Path, PathBuf};
use std::{env, io};
use axum::body::Body;
use axum::http::Request;
use listenfd::ListenFd;
use notify::Watcher;
use thiserror::Error;
use tokio::net::TcpListener;
use tower_livereload::predicate::Predicate;
use tower_livereload::LiveReloadLayer;
use ::app::get_router; use ::app::get_router;
use std::env;
#[derive(Error, Debug)] use std::path::Path;
pub enum AppError {
#[error("Unable to bind to TCP listener")]
TCPListener(#[from] std::io::Error),
#[error("Error with the notify watcher")]
NotifyWatcher(#[from] notify::Error),
#[error("Missing environment variable {var}")]
MissingEnvVar { var: &'static str },
}
/// Nous filtrons les requêtes de `htmx` pour ne pas inclure le script _JS_ qui gère le rechargement
/// Voir https://github.com/leotaku/tower-livereload/pull/3
#[derive(Copy, Clone)]
struct NotHtmxPredicate;
impl<T> Predicate<Request<T>> for NotHtmxPredicate {
fn check(&mut self, req: &Request<T>) -> bool {
!(req.headers().contains_key("hx-request"))
}
}
const DEFAULT_LISTENER: &str = "localhost:3000";
async fn get_tcp_listener() -> Result<TcpListener, io::Error> {
let mut listenfd = ListenFd::from_env();
match listenfd.take_tcp_listener(0)? {
// if we are given a tcp listener on listen fd 0, we use that one
Some(listener) => {
listener.set_nonblocking(true)?;
Ok(TcpListener::from_std(listener)?)
}
// otherwise fall back to local listening
None => Ok(TcpListener::bind(DEFAULT_LISTENER).await?),
}
}
fn get_livereload_layer(
templates_paths: Vec<PathBuf>,
) -> Result<LiveReloadLayer<NotHtmxPredicate>, notify::Error> {
let livereload = LiveReloadLayer::new();
let reloader = livereload.reloader();
let mut watcher = notify::recommended_watcher(move |_| reloader.reload())?;
for templates_path in templates_paths {
watcher.watch(templates_path.as_path(), notify::RecursiveMode::Recursive)?;
}
Ok(livereload.request_predicate::<Body, NotHtmxPredicate>(NotHtmxPredicate))
}
#[tokio::main] #[tokio::main]
async fn main() -> Result<(), AppError> { async fn main() {
let manifest_dir = env::var("CARGO_MANIFEST_DIR").map_err(|_| AppError::MissingEnvVar { let manifest_dir = env::var("CARGO_MANIFEST_DIR").unwrap();
var: "CARGO_MANIFEST_DIR",
})?;
let assets_path = Path::new(&manifest_dir).join("assets"); let assets_path = Path::new(&manifest_dir).join("assets");
let templates_paths = vec![ let router = get_router(assets_path.as_path());
Path::new(&manifest_dir).join("src/pages"),
Path::new(&manifest_dir).join("src/components"),
];
let livereload_layer = // TODO: select port based on available port (or ask in CLI)
get_livereload_layer(templates_paths).map_err(AppError::NotifyWatcher)?; let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
let router = get_router(assets_path).await.layer(livereload_layer); axum::serve(listener, router).await.unwrap();
let listener: TcpListener = get_tcp_listener().await.map_err(AppError::TCPListener)?;
let local_addr = listener.local_addr().map_err(AppError::TCPListener)?;
println!("Listening on: http://{}", local_addr);
// Run the server with the router
axum::serve(listener, router.into_make_service()).await?;
Ok(())
} }

View File

@ -1,23 +0,0 @@
pub struct MenuItem {
pub id: String,
pub label: String,
pub href: String,
}
/// Get the menu items
/// This function is the central place to define the menu items
/// It can be used directly in templates, for example in the `navbar` component to render the menu
pub fn get_menu_items() -> Vec<MenuItem> {
vec![
MenuItem {
id: "home".to_string(),
label: "Accueil".to_string(),
href: "/".to_string(),
},
MenuItem {
id: "cps".to_string(),
label: "CPS".to_string(),
href: "/cps".to_string(),
},
]
}

View File

@ -1,43 +0,0 @@
{% extends "base.html" %}
{% import "navbar/navbar.html" as navbar -%}
{% block title %}Pharma Libre - CPS{% endblock %}
{% block body %}
{% call navbar::navbar(current="cps") %}
<div class="py-10">
<header id="page-header">
<div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<h1
id="page-title"
class="text-3xl font-bold leading-tight tracking-tight text-gray-900"
>
CPS
</h1>
</div>
</header>
<main id="page-main">
<div
class="mx-auto max-w-7xl px-4 py-8 sm:px-6 lg:px-8"
>
<div
class="border-2 border-dashed rounded-lg border-gray-300 dark:border-gray-600 h-96 mb-4"
>A</div>
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 mb-4">
<div
class="border-2 border-dashed border-gray-300 rounded-lg dark:border-gray-600 h-32 md:h-64"
>B</div>
<div
class="border-2 border-dashed rounded-lg border-gray-300 dark:border-gray-600 h-32 md:h-64"
>C</div>
<div
class="border-2 border-dashed rounded-lg border-gray-300 dark:border-gray-600 h-32 md:h-64"
>D</div>
<div
class="border-2 border-dashed rounded-lg border-gray-300 dark:border-gray-600 h-32 md:h-64"
>E</div>
</div>
</div>
</main>
</div>
{% endblock %}

View File

@ -1,12 +0,0 @@
use askama_axum::Template;
use axum_htmx::HxRequest;
#[derive(Template)]
#[template(path = "cps.html")]
pub struct CpsTemplate {
hx_request: bool,
}
pub async fn cps(HxRequest(hx_request): HxRequest) -> CpsTemplate {
CpsTemplate { hx_request }
}

View File

@ -1,43 +0,0 @@
{% extends "base.html" %}
{% import "navbar/navbar.html" as navbar -%}
{% block title %}Pharma Libre - Accueil{% endblock %}
{% block body %}
{% call navbar::navbar(current="home") %}
<div class="py-10">
<header id="page-header">
<div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<h1
id="page-title"
class="text-3xl font-bold leading-tight tracking-tight text-gray-900"
>
Accueil
</h1>
</div>
</header>
<main id="page-main">
<div
class="mx-auto max-w-7xl px-4 py-8 sm:px-6 lg:px-8"
>
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 mb-4">
<div
class="border-2 border-dashed border-gray-300 rounded-lg dark:border-gray-600 h-32 md:h-64"
>A</div>
<div
class="border-2 border-dashed rounded-lg border-gray-300 dark:border-gray-600 h-32 md:h-64"
>B</div>
<div
class="border-2 border-dashed rounded-lg border-gray-300 dark:border-gray-600 h-32 md:h-64"
>C</div>
<div
class="border-2 border-dashed rounded-lg border-gray-300 dark:border-gray-600 h-32 md:h-64"
>D</div>
</div>
<div
class="border-2 border-dashed rounded-lg border-gray-300 dark:border-gray-600 h-96 mb-4"
>E</div>
</div>
</main>
</div>
{% endblock %}

View File

@ -1,12 +0,0 @@
use askama_axum::Template;
use axum_htmx::HxRequest;
#[derive(Template)]
#[template(path = "home.html")]
pub struct GetHomeTemplate {
hx_request: bool,
}
pub async fn home(HxRequest(hx_request): HxRequest) -> GetHomeTemplate {
GetHomeTemplate { hx_request }
}

View File

@ -1,10 +0,0 @@
use axum::{routing, Router};
mod cps;
mod home;
pub fn get_routes() -> Router {
Router::new()
.route("/", routing::get(home::home))
.route("/cps", routing::get(cps::cps))
}

View File

@ -0,0 +1,7 @@
use askama::Template;
#[derive(Template)]
#[template(path = "hello.html")]
pub struct HelloResponse {
pub name: String,
}

View File

@ -0,0 +1,5 @@
use askama::Template;
#[derive(Template)]
#[template(path = "index.html")]
pub struct GetIndexResponse;

View File

@ -0,0 +1,2 @@
pub mod hello;
pub mod index;

View File

@ -1,12 +0,0 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
'./src/**/*.html',
'./css/**/*.css',
],
theme: {
extend: {},
},
plugins: [],
}

View File

@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<title>{% block title %}{{ title }}{% endblock %}</title>
<script src="/assets/js/htmx.min.js"></script>
{% block head %}{% endblock %}
</head>
<body>
{% block body %}{% endblock %}
</body>
</html>

View File

@ -0,0 +1 @@
<div>Hello {{name}}!</div>

View File

@ -0,0 +1,23 @@
{% extends "base.html" %}
{% block title %}Pharma Libre{% endblock %}
{% block body %}
<div>
<header>
<h1>Pharma Libre</h1>
</header>
<main>
<div
id="hello"
hx-get="/hello"
hx-target="this"
hx-trigger="load"
hx-swap="outerHTML"
>
Loading...
</div>
</main>
</div>
{% endblock %}

View File

@ -1,14 +0,0 @@
[package]
name = "backend"
version = "0.1.0"
edition = "2021"
[dependencies]
anyhow = "1.0.89"
axum = "0.7.6"
listenfd = "1.0.1"
tokio = { version = "1.40.0", features = ["macros", "rt-multi-thread"] }
[dev-dependencies]
cargo-watch = "8.5.2"
systemfd = "0.4.3"

View File

@ -1,19 +0,0 @@
# Backend
Ceci est un serveur backend, basé sur axum, et permettant d'offrir une gestion centralisée des accès aux données.
## Prérequis
En développement, le mécanisme de hot-reload nécessite de disposer de `cargo-watch` et `systemfd`. Pour les installer, exécutez la commande suivante :
```bash
cargo install cargo-watch systemfd
```
## Développement
Pour lancer le serveur en mode développement, exécutez la commande suivante :
```bash
systemfd --no-pid -s http::3030 -- cargo watch -x 'run --bin backend'
```

View File

@ -1,37 +0,0 @@
use anyhow::Error as AnyError;
use axum::http::{StatusCode, Uri};
use axum::response::{IntoResponse, Response};
use axum::{routing::get, Router};
pub fn get_router() -> Router {
Router::new()
.route("/", get(|| async { "Hello, world!" }))
.fallback(fallback)
}
async fn fallback(uri: Uri) -> (StatusCode, String) {
(StatusCode::NOT_FOUND, format!("No route for {uri}"))
}
struct AppError(AnyError);
// To automatically convert `AppError` into a response
impl IntoResponse for AppError {
fn into_response(self) -> Response {
(
StatusCode::INTERNAL_SERVER_ERROR,
format!("Internal Server Error: {}", self.0),
)
.into_response()
}
}
// To automatically convert `AnyError` into `AppError`
impl<E> From<E> for AppError
where
E: Into<AnyError>,
{
fn from(err: E) -> Self {
Self(err.into())
}
}

View File

@ -1,24 +0,0 @@
use listenfd::ListenFd;
use tokio::net::TcpListener;
use backend::get_router;
#[tokio::main]
async fn main() {
let app = get_router();
let mut listenfd = ListenFd::from_env();
let listener = match listenfd.take_tcp_listener(0).unwrap() {
// if we are given a tcp listener on listen fd 0, we use that one
Some(listener) => {
listener.set_nonblocking(true).unwrap();
TcpListener::from_std(listener).unwrap()
}
// otherwise fall back to local listening
None => TcpListener::bind("0.0.0.0:8080").await.unwrap(),
};
println!("Listening on {}", listener.local_addr().unwrap());
axum::serve(listener, app).await.unwrap();
}

View File

@ -10,11 +10,15 @@ name = "desktop_lib"
crate-type = ["lib", "cdylib", "staticlib"] crate-type = ["lib", "cdylib", "staticlib"]
[build-dependencies] [build-dependencies]
tauri-build = { version = "2.0.0-rc", features = [] } tauri-build = { version = "2.0.0-beta", features = [] }
[dependencies] [dependencies]
tauri = { version = "2.0.0-rc", features = [] } axum = "0.7.5"
tauri-plugin-shell = "2.0.0-rc" tauri = { version = "2.0.0-beta", features = [] }
serde = { version = "1", features = ["derive"] } tower = "0.4.13"
serde_json = "1" tokio = "1.39.1"
app = { path = "../app" }
http = "1.1.0"
bytes = "1.6.1"

View File

@ -1,10 +0,0 @@
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default",
"description": "Capability for the main window",
"windows": ["main"],
"permissions": [
"core:default",
"shell:allow-open"
]
}

View File

@ -1,14 +1,69 @@
// Learn more about Tauri commands at https://tauri.app/v1/guides/features/command use bytes::Bytes;
#[tauri::command] use http::{request, response, Request, Response};
fn greet(name: &str) -> String { use std::path::PathBuf;
format!("Hello, {}! You've been greeted from Rust!", name) use std::sync::Arc;
use axum::body::{to_bytes, Body};
use axum::Router;
use tauri::path::BaseDirectory;
use tauri::Manager;
use tokio::sync::{Mutex, MutexGuard};
use tower::{Service, ServiceExt};
async fn process_tauri_request(
tauri_request: Request<Vec<u8>>,
mut router: MutexGuard<'_, Router>,
) -> Response<Vec<u8>> {
let (parts, body): (request::Parts, Vec<u8>) = tauri_request.into_parts();
let axum_request: Request<Body> = Request::from_parts(parts, body.into());
let axum_response: Response<Body> = router
.as_service()
.ready()
.await
.expect("Failed to get ready service from router")
.call(axum_request)
.await
.expect("Could not get response from router");
let (parts, body): (response::Parts, Body) = axum_response.into_parts();
let body: Bytes = to_bytes(body, usize::MAX).await.unwrap_or_default();
let tauri_response: Response<Vec<u8>> = Response::from_parts(parts, body.into());
tauri_response
} }
#[cfg_attr(mobile, tauri::mobile_entry_point)] #[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() { pub fn run() {
tauri::Builder::default() tauri::Builder::default()
.plugin(tauri_plugin_shell::init()) .setup(|app| {
.invoke_handler(tauri::generate_handler![greet]) let assets_path: PathBuf = app
.path()
.resolve("assets", BaseDirectory::Resource)
.expect("Path should be resolvable");
// Adds Axum router to application state
// This makes it so we can retrieve it from any app instance (see bellow)
let router = Arc::new(Mutex::new(app::get_router(&assets_path)));
app.manage(router);
Ok(())
})
.register_asynchronous_uri_scheme_protocol("axum", move |app, request, responder| {
// Retrieve the router from the application state and clone it for the async block
let router = Arc::clone(&app.state::<Arc<Mutex<axum::Router>>>());
// Spawn a new async task to process the request
tauri::async_runtime::spawn(async move {
let router = router.lock().await;
let response = process_tauri_request(request, router).await;
responder.respond(response);
});
})
.run(tauri::generate_context!()) .run(tauri::generate_context!())
.expect("error while running tauri application"); .expect("error while running tauri application");
} }

View File

@ -1,24 +1,20 @@
{ {
"$schema": "https://schema.tauri.app/config/2.0.0-rc", "productName": "Logiciel Pharma",
"productName": "Chrys4lide LGO",
"version": "0.0.1", "version": "0.0.1",
"identifier": "org.p4pillon.chrys4lide.lgo", "identifier": "org.p4pillon.pharma.desktop",
"build": { "build": {
"beforeDevCommand": { "beforeDevCommand": {
"cwd": "../../frontend", "cwd": "../app",
"script": "bun run dev" "script": "cargo run"
}, },
"devUrl": "http://localhost:1420", "devUrl": "http://localhost:3000",
"beforeBuildCommand": { "frontendDist": "axum://place.holder/"
"cwd": "../../frontend",
"script": "bun run generate"
},
"frontendDist": "../../frontend/dist"
}, },
"app": { "app": {
"withGlobalTauri": true,
"windows": [ "windows": [
{ {
"title": "Chrys4lide | LG0", "title": "Logiciel Pharma",
"width": 800, "width": 800,
"height": 600 "height": 600
} }
@ -29,6 +25,9 @@
}, },
"bundle": { "bundle": {
"active": true, "active": true,
"resources": {
"../app/assets/": "./assets/"
},
"targets": "all", "targets": "all",
"icon": [ "icon": [
"icons/32x32.png", "icons/32x32.png",
@ -38,4 +37,5 @@
"icons/icon.ico" "icons/icon.ico"
] ]
} }
} }

View File

@ -1,12 +0,0 @@
[package]
name = "services-sesam-vitale-sys"
version = "0.1.0"
edition = "2021"
#links= "ssvlux64"
[dependencies]
bitvec = "1.0.1"
deku = "0.17.0"
libc = "0.2.155"
num_enum = { version = "0.7.3", features = ["complex-expressions"] }
thiserror = "1.0.63"

View File

@ -1,104 +0,0 @@
use deku::{deku_derive, DekuContainerRead, DekuError, DekuReader};
use std::{ffi::CString, fmt, path::Path, ptr};
use thiserror::Error;
use crate::{
bindings::{SSV_InitLIB2, SSV_LireConfig, SSV_TermLIB},
types::{common::read_from_buffer, configuration::Configuration},
};
use num_enum::FromPrimitive;
#[derive(Error, Debug)]
pub struct SesamVitaleError {
code: u16,
}
#[derive(Debug, Eq, PartialEq, FromPrimitive)]
#[repr(u16)]
enum SSVIntError {
CPSNotInserted = 61441,
#[num_enum(catch_all)]
NotImplemented(u16),
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_sesam_vitale_error() {
let int_error = SSVIntError::from(61441);
assert_eq!(int_error, SSVIntError::CPSNotInserted);
let int_error = SSVIntError::from(123);
assert_eq!(int_error, SSVIntError::NotImplemented(123));
println!("{:?}", int_error);
}
}
#[derive(Error, Debug)]
enum SSVError {
#[error("Erreur standard de la librairie SSV")]
SSVStandard,
// #[error("Erreur de parsing")]
// Parsing(#[from] ParsingError),
#[error("Erreur inattendue de la librairie SSV (TMP)")]
SSVUnknownTmp,
}
impl fmt::Display for SesamVitaleError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Got error code {} from SSV_LireConfig", self.code)
}
}
pub fn init_library(sesam_ini_path: &Path) -> Result<(), SesamVitaleError> {
// TODO: better error handling
let path_str = sesam_ini_path.to_str().unwrap();
let path_ptr = CString::new(path_str).expect("failed to create cstring");
let exit_code: u16 = unsafe { SSV_InitLIB2(path_ptr.as_ptr()) };
if exit_code != 0 {
let error = SesamVitaleError { code: exit_code };
return Err(error);
};
Ok(())
}
pub fn close_library() -> Result<(), SesamVitaleError> {
let exit_code: u16 = unsafe { SSV_TermLIB() };
if exit_code != 0 {
let error = SesamVitaleError { code: exit_code };
return Err(error);
};
Ok(())
}
pub fn read_config() -> Result<Configuration, SesamVitaleError> {
let mut buffer_ptr: *mut libc::c_void = ptr::null_mut();
let mut size: libc::size_t = 0;
let buffer_ptr_ptr: *mut *mut libc::c_void = &mut buffer_ptr;
let size_ptr: *mut libc::size_t = &mut size;
// Need to add proper error handling -> return a result with error code pointing to an error
// enum
let exit_code: u16 = unsafe { SSV_LireConfig(buffer_ptr_ptr, size_ptr) };
if exit_code != 0 {
let error = SesamVitaleError { code: exit_code };
return Err(error);
};
let buffer: &[u8] = unsafe { std::slice::from_raw_parts(buffer_ptr as *const u8, size) };
// TODO: Improve error handling
let configuration: Configuration = read_from_buffer(buffer).unwrap();
// TODO: Call library function for memory delocating
unsafe { libc::free(buffer_ptr) };
Ok(configuration)
}

View File

@ -1,288 +0,0 @@
#![allow(non_upper_case_globals)]
#![allow(non_camel_case_types)]
#![allow(non_snake_case)]
#![allow(dead_code)]
// Generated using bindgen
extern "C" {
// Fonctions de gestion des données
pub fn SSV_LireCartePS(
NomRessourcePS: *const ::std::os::raw::c_char,
NomRessourceLecteur: *const ::std::os::raw::c_char,
CodePorteurPS: *const ::std::os::raw::c_char,
pZDataOut: *mut *mut ::std::os::raw::c_void,
pTailleZone: *mut usize,
) -> ::std::os::raw::c_ushort;
pub fn SSV_LireDroitsVitale(
NomRessourcePS: *const ::std::os::raw::c_char,
NomRessourceLecteur: *const ::std::os::raw::c_char,
CodePorteurPS: *const ::std::os::raw::c_char,
DateConsultation: *const ::std::os::raw::c_char,
pZDataOut: *mut *mut ::std::os::raw::c_void,
pTailleZone: *mut usize,
) -> ::std::os::raw::c_ushort;
pub fn SSV_FormaterFactures(
cFactureACreer: ::std::os::raw::c_char,
cModeSecur: ::std::os::raw::c_char,
cTypeFlux: ::std::os::raw::c_char,
pZDataIn: *mut ::std::os::raw::c_void,
TailleDataIn: usize,
pZDataOut: *mut *mut ::std::os::raw::c_void,
pTailleZone: *mut usize,
) -> ::std::os::raw::c_ushort;
pub fn SSV_ChiffrerFacture(
pZDataIn: *mut ::std::os::raw::c_void,
TailleDataIn: usize,
pZDataOut: *mut *mut ::std::os::raw::c_void,
pTailleZone: *mut usize,
) -> ::std::os::raw::c_ushort;
pub fn SSV_SignerFactureVitale(
pcNomRessourceVitale: *const ::std::os::raw::c_char,
pZDataIn: *mut ::std::os::raw::c_void,
szTailleDataIn: usize,
pZDataOut: *mut *mut ::std::os::raw::c_void,
pszTailleZone: *mut usize,
) -> ::std::os::raw::c_ushort;
pub fn SSV_CalculerHashFactureAssure(
pcNumSerie: *const ::std::os::raw::c_char,
pZDataIn: *mut ::std::os::raw::c_void,
szTailleDataIn: usize,
pZDataOut: *mut *mut ::std::os::raw::c_void,
pszTailleZone: *mut usize,
) -> ::std::os::raw::c_ushort;
pub fn SSV_AjouterSignatureAssureDansFacture(
pZDataIn: *mut ::std::os::raw::c_void,
szTailleDataIn: usize,
pZDataOut: *mut *mut ::std::os::raw::c_void,
pszTailleZone: *mut usize,
) -> ::std::os::raw::c_ushort;
pub fn SSV_SignerFactureCPS(
pcNomRessourcePS: *const ::std::os::raw::c_char,
pcNomRessourceLecteur: *const ::std::os::raw::c_char,
pcCodePorteurPS: *const ::std::os::raw::c_char,
cNologSituation: ::std::os::raw::c_char,
pZDataIn: *mut ::std::os::raw::c_void,
szTailleDataIn: usize,
pZDataOut: *mut *mut ::std::os::raw::c_void,
pszTailleZone: *mut usize,
) -> ::std::os::raw::c_ushort;
pub fn SSV_FormaterLot(
NBZDataIn: ::std::os::raw::c_short,
TZDataIn: *mut *mut ::std::os::raw::c_void,
TTailleZoneIn: *mut usize,
pNbZDataOut: *mut ::std::os::raw::c_short,
TZDataOut: *mut *mut ::std::os::raw::c_void,
TTailleZoneOut: *mut usize,
) -> ::std::os::raw::c_ushort;
pub fn SSV_SignerLotCPS(
pcNomRessourcePS: *const ::std::os::raw::c_char,
pcNomRessourceLecteur: *const ::std::os::raw::c_char,
pcCodePorteurPS: *const ::std::os::raw::c_char,
cNologSituation: ::std::os::raw::c_char,
pZDataIn: *mut ::std::os::raw::c_void,
szTailleDataIn: usize,
pZDataOut: *mut *mut ::std::os::raw::c_void,
pszTailleZone: *mut usize,
) -> ::std::os::raw::c_ushort;
pub fn SSV_FormaterFichier(
pZDataIn: *mut ::std::os::raw::c_void,
TailleDataIn: usize,
pZDataOut: *mut *mut ::std::os::raw::c_void,
pTailleZone: *mut usize,
) -> ::std::os::raw::c_ushort;
pub fn SSV_TraduireARL(
NbZDonneesEntree: ::std::os::raw::c_short,
TZDataIn: *mut *mut ::std::os::raw::c_void,
TTailleZoneIn: *mut usize,
pZDataOut: *mut *mut ::std::os::raw::c_void,
pTailleZoneOut: *mut usize,
) -> ::std::os::raw::c_ushort;
pub fn SSV_LireNumSerieCarteVitale(
pcNomRessource: *mut ::std::os::raw::c_char,
numeroSerie: *mut ::std::os::raw::c_uchar,
) -> ::std::os::raw::c_ushort;
pub fn SSV_CalculerHashFacturePS(
pcNumSerieCPS: *const ::std::os::raw::c_char,
pZDataIn: *mut ::std::os::raw::c_void,
usTailleDataIn: usize,
pZDataOut: *mut *mut ::std::os::raw::c_void,
pusTailleZone: *mut usize,
) -> ::std::os::raw::c_ushort;
pub fn SSV_AjouterSignaturePSFacture(
pZDataIn: *mut ::std::os::raw::c_void,
szTailleDataIn: usize,
pZDataOut: *mut *mut ::std::os::raw::c_void,
pszTailleZone: *mut usize,
) -> ::std::os::raw::c_ushort;
pub fn SSV_DechargerFacturesPdT(
NomRessourcePS: *const ::std::os::raw::c_char,
NomRessourceLecteur: *const ::std::os::raw::c_char,
CodePorteurPS: *const ::std::os::raw::c_char,
pcNumFact: *const ::std::os::raw::c_char,
sNbZDataIn: ::std::os::raw::c_short,
pvTZDataIn: *mut *mut ::std::os::raw::c_void,
psTTailleDataIn: *mut usize,
pNbZDataOut: *mut ::std::os::raw::c_short,
TZDataOut: *mut *mut ::std::os::raw::c_void,
TTailleZoneOut: *mut usize,
) -> ::std::os::raw::c_ushort;
pub fn SSV_TraduireFSE(
pZDataIn: *mut ::std::os::raw::c_void,
TailleDataIn: usize,
pZDataOut: *mut *mut ::std::os::raw::c_void,
pTailleZone: *mut usize,
) -> ::std::os::raw::c_ushort;
// Fonctions TLA
// TLA (Terminal Lecteur Applicatif) -> lecteur autre que PC-SC, on ne prend pas en compte cela
pub fn SSV_IdentifierTLA(
pcNomRessourceLecteur: *const ::std::os::raw::c_char,
NumVersionCDC: *const ::std::os::raw::c_char,
pZDataOut: *mut *mut ::std::os::raw::c_void,
tailleDataOut: *mut usize,
) -> ::std::os::raw::c_ushort;
pub fn SSV_ChargerDonneesTLA(
pcNomRessourceLecteur: *const ::std::os::raw::c_char,
sNbZDataIn: ::std::os::raw::c_short,
pvTZDataIn: *mut *mut ::std::os::raw::c_void,
psTTailleDataIn: *mut usize,
) -> ::std::os::raw::c_ushort;
pub fn SSV_ChargerFacturesPdT(
pcNomRessourceLecteur: *const ::std::os::raw::c_char,
pcNumFacturation: *const ::std::os::raw::c_char,
sNbZDataIn: ::std::os::raw::c_short,
pvTZDataIn: *mut *mut ::std::os::raw::c_void,
psTTailleDataIn: *mut usize,
pNbZDataOut: *mut ::std::os::raw::c_short,
TZDataOut: *mut *mut ::std::os::raw::c_void,
TTailleZoneOut: *mut usize,
) -> ::std::os::raw::c_ushort;
pub fn SSV_DechargerFSETLA(
NomRessourcePS: *const ::std::os::raw::c_char,
NomRessourceLecteur: *const ::std::os::raw::c_char,
CodePorteurPS: *const ::std::os::raw::c_char,
pcNumFact: *const ::std::os::raw::c_char,
pNbZDataOut: *mut ::std::os::raw::c_short,
TZDataOut: *mut *mut ::std::os::raw::c_void,
TTailleZoneOut: *mut usize,
) -> ::std::os::raw::c_ushort;
pub fn SSV_DechargerFSETLANC(
NomRessourcePS: *const ::std::os::raw::c_char,
NomRessourceLecteur: *const ::std::os::raw::c_char,
CodePorteurPS: *const ::std::os::raw::c_char,
pcNumFact: *const ::std::os::raw::c_char,
pNbZDataOut: *mut ::std::os::raw::c_short,
TZDataOut: *mut *mut ::std::os::raw::c_void,
TTailleZoneOut: *mut usize,
) -> ::std::os::raw::c_ushort;
pub fn SSV_DechargerBeneficiaires(
NomRessourcePS: *const ::std::os::raw::c_char,
NomRessourceLecteur: *const ::std::os::raw::c_char,
CodePorteurPS: *const ::std::os::raw::c_char,
cNumFacturation: *const ::std::os::raw::c_char,
sNbZDataOut: *mut ::std::os::raw::c_short,
pTZDataOut: *mut *mut ::std::os::raw::c_void,
sTTailleDataOut: *mut usize,
) -> ::std::os::raw::c_ushort;
pub fn SSV_EffacerTLA(
NomRessourcePS: *const ::std::os::raw::c_char,
NomRessourceLecteur: *const ::std::os::raw::c_char,
CodePorteurPS: *const ::std::os::raw::c_char,
cNumFacturation: *const ::std::os::raw::c_char,
cTypeDonnee: *const ::std::os::raw::c_char,
) -> ::std::os::raw::c_ushort;
pub fn SSV_SecuriserFacture(
pcNomRessourcePS: *const ::std::os::raw::c_char,
pcNomRessourceLecteur: *const ::std::os::raw::c_char,
pcCodePorteurPS: *const ::std::os::raw::c_char,
cNologSituation: ::std::os::raw::c_char,
pcNumFact: *const ::std::os::raw::c_char,
pvDataIn: *mut ::std::os::raw::c_void,
szTailleDataIn: usize,
pvDataOut: *mut *mut ::std::os::raw::c_void,
pszTailleDataOut: *mut usize,
) -> ::std::os::raw::c_ushort;
// Fonctions de gestion de configuration (GALSS)
pub fn SSV_LireConfig(
pZDataOut: *mut *mut ::std::os::raw::c_void,
psTailleDataOut: *mut usize,
) -> ::std::os::raw::c_ushort;
pub fn SSV_LireDateLecteur(
pcNomRessourceLecteur: *const ::std::os::raw::c_char,
pcDateHeure: *mut ::std::os::raw::c_char,
) -> ::std::os::raw::c_ushort;
pub fn SSV_MajDateLecteur(
pcNomRessourceLecteur: *const ::std::os::raw::c_char,
pcDateHeure: *const ::std::os::raw::c_char,
) -> ::std::os::raw::c_ushort;
pub fn SSV_ChargerAppli(
pcNomRessourceLecteur: *const ::std::os::raw::c_char,
sNbZDataIn: ::std::os::raw::c_short,
pvTZDataIn: *mut *mut ::std::os::raw::c_void,
psTTailleDataIn: *mut usize,
) -> ::std::os::raw::c_ushort;
// Fonctions techniques
// La fonction Initialiser Librairie a pour objet de charger et dinitialiser dans la mémoire du système :
// - dans le cas où le GALSS est installé sur le poste :
// - la bibliothèque du Gestionnaire dAccès au Lecteur Santé Social (GALSS),
// - qui charge la bibliothèque du Protocole Santé Social (PSS),
// - la configuration du poste de travail à laide du fichier galssinf,
// - les variables globales communes aux différents Services SESAM-Vitale,
// - les fichiers de tables et scripts des répertoires par défaut.
// Cette fonction accède au référentiel électronique en utilisant le chemin complet indiqué dans le fichier sesam.ini.
pub fn SSV_InitLIB2(pcFichierSesam: *const ::std::os::raw::c_char) -> ::std::os::raw::c_ushort;
// La fonction Terminer a pour objet de décharger de la mémoire du système les éléments
// chargés par la fonction Initialiser Librairie, qui ne sont plus utiles.
pub fn SSV_TermLIB() -> ::std::os::raw::c_ushort;
/// Fonctions de Tracage
//La fonction Allouer Zone Mémoire a un rôle purement technique : elle permet dallouer, autrement dit de réserver une zone ou partie de la mémoire du poste de travail pour y écrire les données à passer en entrée dun Service SESAM-Vitale.
// Cette fonction doit être utilisée pour allouer toutes les zones de mémoire requises en entrée des Services SESAM-Vitale de manière à permettre un diagnostic fiable par le « mode trace » en cas de dysfonctionnement. En effet, son mode dexécution est susceptible de fournir des informations utiles au « mode trace » lorsquil est activé.
pub fn SSV_AllouerZoneMem(
pZDataIn: *mut *mut ::std::os::raw::c_void,
taille: usize,
) -> ::std::os::raw::c_ushort;
// La fonction Libérer Zone Mémoire a un rôle purement technique : elle permet de libérer une zone de mémoire du poste de travail précédemment allouée après exploitation des données quelle contient.
// Cette fonction doit être utilisée pour libérer toutes les zones de mémoire :
// - celles qui ont été allouées par le progiciel de santé pour fournir les données nécessaires à lentrée des Services SESAM-Vitale, avant leur appel, celles qui ont été allouées par les Services SESAM-Vitale pour fournir en sortie les données utiles au progiciel de santé qui a fait appel à ces services,
// - de façon à permettre un diagnostic fiable par le mode trace en cas de dysfonctionnement
//En effet, son exécution est susceptible de fournir des informations utiles au « mode trace » lorsquil est activé.
pub fn SSV_LibererZoneMem(pZone: *mut ::std::os::raw::c_void);
// La fonction Initialiser Trace a pour objet de permettre lactivation du « mode trace ».
// Ce mode de fonctionnement est prévu pour permettre à lassistance technique du GIE
// SESAM-Vitale danalyser les problèmes de mise en œuvre des Services SESAM-Vitale,
// notamment lorsque une fonction retourne un code derreur de valeur hexadécimale supérieure à FF00.
pub fn SSV_InitTrace(
pathConf: *mut ::std::os::raw::c_char,
ModeOuvertureFicherLog: *mut ::std::os::raw::c_char,
ModuleLog: ::std::os::raw::c_ushort,
NiveauLog: ::std::os::raw::c_uchar,
) -> ::std::os::raw::c_ushort;
}

View File

@ -1,6 +0,0 @@
pub mod api;
mod bindings;
pub mod types;
#[cfg(test)]
mod tests {}

View File

@ -1,144 +0,0 @@
use crate::types::configuration::{
ConfigurationHeader, PCSCReader, ReaderConfiguration, SESAMVitaleComponent,
};
use std::{error::Error, str::FromStr};
use bitvec::index::BitIdx;
use deku::{
bitvec::{BitStore, Msb0},
ctx::ByteSize,
deku_derive,
reader::{Reader, ReaderRet},
DekuContainerRead, DekuError, DekuReader,
};
#[deku_derive(DekuRead)]
#[derive(Debug, Clone, PartialEq)]
pub(crate) struct NumericString(#[deku(map = "convert_from_data_field")] String);
#[deku_derive(DekuRead)]
#[derive(Debug, Clone, PartialEq)]
pub(crate) struct AlphaNumericString(#[deku(map = "convert_from_data_field")] String);
#[deku_derive(DekuRead)]
#[derive(Debug, Clone, PartialEq)]
pub(crate) struct BinaryData(#[deku(map = "extract_from_data_field")] Vec<u8>);
#[deku_derive(DekuRead)]
#[derive(Debug, Clone, Copy, PartialEq)]
#[deku(endian = "big")]
pub(crate) struct GroupId(u16);
trait MapToDekuParseError<T> {
fn map_to_deku_parse_error(self) -> Result<T, DekuError>;
}
impl<T, E: Error> MapToDekuParseError<T> for Result<T, E> {
fn map_to_deku_parse_error(self) -> Result<T, DekuError> {
self.map_err(|e| DekuError::Parse(e.to_string().into()))
}
}
fn read_size<R: std::io::Read>(reader: &mut Reader<R>) -> Result<ByteSize, DekuError> {
let first_byte: u8 = u8::from_reader_with_ctx(reader, ())?;
let is_length_expanded = first_byte.get_bit::<Msb0>(BitIdx::new(0).map_to_deku_parse_error()?);
match is_length_expanded {
true => {
let size_of_data_size: ByteSize = ByteSize((first_byte & 0b0111_1111) as usize);
if size_of_data_size.0 > 4 {
return Err(DekuError::Parse("Size of the length encoding is > 4, this is not normal. Probable parsing error".to_string().into()));
};
// maximum size of the buffer is 4, we use the offset to read values less than 4 bytes
let buffer: &mut [u8; 4] = &mut [0; 4];
let write_offset = 4 - size_of_data_size.0;
match reader.read_bytes(size_of_data_size.0, &mut buffer[write_offset..])? {
ReaderRet::Bits(_bit_vec) => Err(DekuError::Parse("Got bits when trying to read bytes -> reader is unaligned, this is not normal.".to_string().into())),
ReaderRet::Bytes => Ok(ByteSize(u32::from_be_bytes(*buffer) as usize)),
}
}
false => Ok(ByteSize(first_byte as usize)),
}
}
// Using this as the map function asks deku to parse a datafield
// We then use the datafield and convert it to the corresponding value
pub(super) fn convert_from_data_field<T>(data_field: DataField) -> Result<T, DekuError>
where
T: FromStr,
T::Err: Error,
{
let text = String::from_utf8(data_field.data).map_to_deku_parse_error()?;
T::from_str(&text).map_to_deku_parse_error()
}
pub(crate) fn extract_from_data_field(data_field: DataField) -> Result<Vec<u8>, DekuError> {
Ok(data_field.data)
}
#[deku_derive(DekuRead)]
#[derive(Debug, PartialEq)]
pub(crate) struct DataField {
#[deku(reader = "read_size(deku::reader)")]
pub(crate) data_size: ByteSize,
#[deku(bytes_read = "data_size.0")]
pub(crate) data: Vec<u8>,
}
#[deku_derive(DekuRead)]
#[derive(Debug, PartialEq)]
pub(crate) struct BlockHeader {
pub(crate) group_id: GroupId,
#[deku(reader = "read_size(deku::reader)")]
pub(crate) data_size: ByteSize,
}
#[deku_derive(DekuRead)]
#[derive(Debug, PartialEq)]
pub(crate) struct DataBlock {
pub(crate) header: BlockHeader,
#[deku(ctx = "header.group_id")]
pub(crate) inner: DataGroup,
}
#[deku_derive(DekuRead)]
#[derive(Debug, PartialEq)]
#[deku(ctx = "group_id: GroupId", id = "group_id.0")]
pub enum DataGroup {
#[deku(id = 60)]
ConfigurationHeader(ConfigurationHeader),
#[deku(id = 61)]
ReaderConfiguration(ReaderConfiguration),
#[deku(id = 64)]
SESAMVitaleComponent(SESAMVitaleComponent),
#[deku(id = 67)]
PCSCReader(PCSCReader),
}
pub(crate) fn read_from_buffer<T>(buffer: &[u8]) -> Result<T, T::Error>
where
T: TryFrom<Vec<DataBlock>>,
{
let mut data_blocks: Vec<DataBlock> = Vec::new();
let mut offset = 0;
let mut remaining_buffer = buffer;
while !remaining_buffer.is_empty() {
// TODO: properly handle errors
let (rest, data_block) = DataBlock::from_bytes((remaining_buffer, offset)).unwrap();
data_blocks.push(data_block);
(remaining_buffer, offset) = rest;
}
T::try_from(data_blocks)
}

View File

@ -1,137 +0,0 @@
use crate::types::common::DataBlock;
use std::{error::Error, fmt, vec::Vec};
use crate::types::common::convert_from_data_field;
use deku::{deku_derive, DekuReader};
use super::common::{AlphaNumericString, DataGroup};
#[deku_derive(DekuRead)]
#[derive(Debug, PartialEq)]
pub struct SSVVersionNumber(#[deku(map = "convert_from_data_field")] u16);
#[deku_derive(DekuRead)]
#[derive(Debug, PartialEq)]
pub struct GALSSVersionNumber(#[deku(map = "convert_from_data_field")] u16);
#[deku_derive(DekuRead)]
#[derive(Debug, PartialEq)]
pub struct PSSVersionNumber(#[deku(map = "convert_from_data_field")] u16);
#[deku_derive(DekuRead)]
#[derive(Debug, PartialEq)]
pub struct ConfigurationHeader {
pub ssv_version: SSVVersionNumber,
pub galss_version: GALSSVersionNumber,
pub pss_version: PSSVersionNumber,
}
#[deku_derive(DekuRead)]
#[derive(Debug, PartialEq)]
pub struct PCSCReaderName(AlphaNumericString);
#[deku_derive(DekuRead)]
#[derive(Debug, PartialEq)]
pub struct CardType(#[deku(map = "convert_from_data_field")] u8);
#[deku_derive(DekuRead)]
#[derive(Debug, PartialEq)]
pub struct PCSCReader {
pub name: PCSCReaderName,
pub card_type: CardType,
}
#[deku_derive(DekuRead)]
#[derive(Debug, PartialEq)]
pub struct SESAMVitaleComponentID(#[deku(map = "convert_from_data_field")] u16);
#[deku_derive(DekuRead)]
#[derive(Debug, PartialEq)]
pub struct SESAMVitaleComponentDescription(AlphaNumericString);
#[deku_derive(DekuRead)]
#[derive(Debug, PartialEq)]
pub struct SESAMVitaleComponentVersion(AlphaNumericString);
#[deku_derive(DekuRead)]
#[derive(Debug, PartialEq)]
pub struct SESAMVitaleComponent {
pub id: SESAMVitaleComponentID,
pub description: SESAMVitaleComponentDescription,
pub version: SESAMVitaleComponentVersion,
}
#[deku_derive(DekuRead)]
#[derive(Debug, PartialEq)]
pub struct ReaderConfiguration {}
#[derive(Debug)]
pub enum ConfigurationError {
MultipleConfigurationHeaders,
MissingConfigurationHeader,
}
impl fmt::Display for ConfigurationError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ConfigurationError::MultipleConfigurationHeaders => {
write!(f, "Multiple ConfigurationHeader blocks found")
}
ConfigurationError::MissingConfigurationHeader => {
write!(f, "Missing ConfigurationHeader block")
}
}
}
}
impl Error for ConfigurationError {}
#[derive(Debug)]
pub struct Configuration {
pub configuration_header: ConfigurationHeader,
pub reader_configurations: Vec<ReaderConfiguration>,
pub sesam_vitale_components: Vec<SESAMVitaleComponent>,
pub pcsc_readers: Vec<PCSCReader>,
}
impl TryFrom<Vec<DataBlock>> for Configuration {
type Error = ConfigurationError;
fn try_from(data_blocks: Vec<DataBlock>) -> Result<Self, Self::Error> {
let mut configuration_header: Option<ConfigurationHeader> = None;
let mut reader_configurations: Vec<ReaderConfiguration> = Vec::new();
let mut sesam_vitale_components: Vec<SESAMVitaleComponent> = Vec::new();
let mut pcsc_readers: Vec<PCSCReader> = Vec::new();
for block in data_blocks {
match block.inner {
DataGroup::ConfigurationHeader(header) => {
if configuration_header.is_some() {
return Err(ConfigurationError::MultipleConfigurationHeaders);
}
configuration_header = Some(header);
}
DataGroup::ReaderConfiguration(configuration) => {
reader_configurations.push(configuration)
}
DataGroup::SESAMVitaleComponent(component) => {
sesam_vitale_components.push(component);
}
DataGroup::PCSCReader(reader) => {
pcsc_readers.push(reader);
}
}
}
let configuration_header = match configuration_header {
Some(header) => header,
None => return Err(ConfigurationError::MissingConfigurationHeader),
};
Ok(Self {
configuration_header,
reader_configurations,
sesam_vitale_components,
pcsc_readers,
})
}
}

View File

@ -1,3 +0,0 @@
pub mod common;
pub mod configuration;
// pub mod droits_vitale;

View File

@ -4,10 +4,8 @@ version = "0.1.0"
edition = "2021" edition = "2021"
[dependencies] [dependencies]
anyhow = "1.0" dotenv = "0.15"
libc = "0.2" libc = "0.2"
thiserror = "1.0"
utils = { path = "../utils" }
[build-dependencies] [build-dependencies]
dotenv = "0.15" dotenv = "0.15"

View File

@ -1,13 +1,13 @@
extern crate dotenv;
use std::env; use std::env;
use std::path::PathBuf; use std::path::PathBuf;
use dotenv::from_path;
fn main() { fn main() {
// Load the .env.build file for build-time environment variables // Load the .env.build file for build-time environment variables
let manifest_dir = env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR must be set"); let manifest_dir = env::var("CARGO_MANIFEST_DIR").unwrap();
let manifest_path = PathBuf::from(manifest_dir); let manifest_path = PathBuf::from(manifest_dir);
from_path(manifest_path.join(".env.build")).ok(); dotenv::from_path(manifest_path.join(".env.build")).ok();
println!("cargo::rerun-if-env-changed=SESAM_FSV_LIB_PATH"); println!("cargo::rerun-if-env-changed=SESAM_FSV_LIB_PATH");
println!("cargo::rerun-if-env-changed=SESAM_FSV_SSVLIB"); println!("cargo::rerun-if-env-changed=SESAM_FSV_SSVLIB");
@ -16,28 +16,21 @@ fn main() {
// Add local lib directory to the linker search path (for def files and static libs) // Add local lib directory to the linker search path (for def files and static libs)
let static_lib_path = manifest_path.join("lib"); let static_lib_path = manifest_path.join("lib");
println!( println!("cargo::rustc-link-search=native={}", static_lib_path.display());
"cargo::rustc-link-search=native={}",
static_lib_path.display()
);
// Add the SESAM_FSV_LIB_PATH to the linker search path // Add the SESAM_FSV_LIB_PATH to the linker search path
let fsv_lib_path = let fsv_lib_path = PathBuf::from(env::var("SESAM_FSV_LIB_PATH").unwrap());
PathBuf::from(env::var("SESAM_FSV_LIB_PATH").expect("SESAM_FSV_LIB_PATH must be set"));
println!("cargo::rustc-link-search=native={}", fsv_lib_path.display()); println!("cargo::rustc-link-search=native={}", fsv_lib_path.display());
// Add the SESAM_FSV_LIB_PATH to the PATH environment variable // Add the SESAM_FSV_LIB_PATH to the PATH environment variable
if cfg!(target_os = "windows") { if cfg!(target_os = "windows") {
let path = env::var("PATH").unwrap_or_default(); let path = env::var("PATH").unwrap_or(String::new());
println!("cargo:rustc-env=PATH={};{}", fsv_lib_path.display(), path); println!("cargo:rustc-env=PATH={};{}", fsv_lib_path.display(), path);
} else if cfg!(target_os = "linux") { } else if cfg!(target_os = "linux") {
println!("cargo:rustc-env=LD_LIBRARY_PATH={}", fsv_lib_path.display()); println!("cargo:rustc-env=LD_LIBRARY_PATH={}", fsv_lib_path.display());
} }
// Link the SESAM_FSV_SSVLIB dynamic library // Link the SESAM_FSV_SSVLIB dynamic library
println!( println!("cargo::rustc-link-lib=dylib={}", env::var("SESAM_FSV_SSVLIB").unwrap());
"cargo::rustc-link-lib=dylib={}",
env::var("SESAM_FSV_SSVLIB").expect("SESAM_FSV_SSVLIB must be set")
);
// TODO : try `raw-dylib` instead of `dylib` on Windows to avoid the need of the `lib` headers compiled from the `def` // TODO : try `raw-dylib` instead of `dylib` on Windows to avoid the need of the `lib` headers compiled from the `def`
} }

View File

@ -1,400 +0,0 @@
use libc::{c_void, size_t};
use std::ffi::CString;
use std::ptr;
use thiserror::Error;
use crate::libssv::{self, SSV_LireCartePS};
use crate::ssv_memory::{decode_ssv_memory, Block, SSVMemoryError};
#[derive(Error, Debug)]
pub enum CartePSError {
#[error("Unknown (group, field) pair: ({group}, {field})")]
UnknownGroupFieldPair { group: u16, field: u16 },
#[error("CString creation error: {0}")]
CString(#[from] std::ffi::NulError),
#[error("Unable to get the last situation while parsing a CartePS")]
InvalidLastSituation,
#[error(transparent)]
SSVMemory(#[from] SSVMemoryError),
#[error(transparent)]
SSVLibErrorCode(#[from] libssv::LibSSVError),
}
#[derive(Debug, Default)]
pub struct CartePS {
titulaire: TitulairePS,
situations: Vec<SituationPS>,
}
// 1. CB = Caractères Binaires »
// 2. CE = Caractères « Etendus » (ISO 8859-1)
// 3. CA = Caractères Alphanumériques (ASCII?)
// 4. CN = Caractères Numériques
#[derive(Debug, Default)]
struct TitulairePS {
type_de_carte_ps: String, // CN
type_d_identification_nationale: String, // CN
numero_d_identification_nationale: String, // CE - 8 -> 30
cle_du_numero_d_identification_nationale: String, // CN
code_civilite: String, // CN
nom_du_ps: String, // CE - 27
prenom_du_ps: String, // CE - 27
categorie_carte: char, // CA
}
#[derive(Debug, Default)]
struct SituationPS {
numero_logique_de_la_situation_de_facturation_du_ps: u8,
mode_d_exercice: String,
statut_d_exercice: String,
secteur_d_activite: String,
type_d_identification_structure: String,
numero_d_identification_structure: String,
cle_du_numero_d_identification_structure: String,
raison_sociale_structure: String,
numero_d_identification_de_facturation_du_ps: String,
cle_du_numero_d_identification_de_facturation_du_ps: String,
numero_d_identification_du_ps_remplaçant: String,
cle_du_numero_d_identification_du_ps_remplaçant: String,
code_conventionnel: String,
code_specialite: String,
code_zone_tarifaire: String,
code_zone_ik: String,
code_agrement_1: String,
code_agrement_2: String,
code_agrement_3: String,
habilitation_à_signer_une_facture: String,
habilitation_à_signer_un_lot: String,
}
pub fn lire_carte(code_pin: &str, lecteur: &str) -> Result<CartePS, CartePSError> {
let resource_ps = CString::new(lecteur)?;
let resource_reader = CString::new("")?;
let card_number = CString::new(code_pin)?;
let mut buffer: *mut c_void = ptr::null_mut();
let mut size: size_t = 0;
let mut hex_values: &[u8] = &[];
unsafe {
let result = SSV_LireCartePS(
resource_ps.as_ptr(),
resource_reader.as_ptr(),
card_number.as_ptr(),
&mut buffer,
&mut size,
);
println!("SSV_LireCartePS result: {}", result);
if result != 0 {
return Err(libssv::LibSSVError::StandardErrorCode {
code: result,
function: "SSV_LireCartePS",
}
.into());
}
if !buffer.is_null() {
hex_values = std::slice::from_raw_parts(buffer as *const u8, size);
libc::free(buffer);
}
}
let groups =
decode_ssv_memory(hex_values, hex_values.len()).map_err(CartePSError::SSVMemory)?;
decode_carte_ps(groups)
}
fn get_last_mut_situation(carte_ps: &mut CartePS) -> Result<&mut SituationPS, CartePSError> {
carte_ps
.situations
.last_mut()
.ok_or(CartePSError::InvalidLastSituation)
}
fn decode_carte_ps(groups: Vec<Block>) -> Result<CartePS, CartePSError> {
let mut carte_ps = CartePS::default();
for group in groups {
for field in group.content {
match (group.id, field.id) {
(1, 1) => {
carte_ps.titulaire.type_de_carte_ps =
String::from_utf8_lossy(field.content).to_string();
}
(1, 2) => {
carte_ps.titulaire.type_d_identification_nationale =
String::from_utf8_lossy(field.content).to_string();
}
(1, 3) => {
carte_ps.titulaire.numero_d_identification_nationale =
String::from_utf8_lossy(field.content).to_string();
}
(1, 4) => {
carte_ps.titulaire.cle_du_numero_d_identification_nationale =
String::from_utf8_lossy(field.content).to_string();
}
(1, 5) => {
carte_ps.titulaire.code_civilite =
String::from_utf8_lossy(field.content).to_string();
}
(1, 6) => {
carte_ps.titulaire.nom_du_ps =
String::from_utf8_lossy(field.content).to_string();
}
(1, 7) => {
carte_ps.titulaire.prenom_du_ps =
String::from_utf8_lossy(field.content).to_string();
}
(1, 8) => {
let byte = field.content[0];
carte_ps.titulaire.categorie_carte = byte as char;
}
(2..=16, 1) => {
carte_ps.situations.push(SituationPS::default());
get_last_mut_situation(&mut carte_ps)?
.numero_logique_de_la_situation_de_facturation_du_ps = field.content[0];
}
(2..=16, 2) => {
get_last_mut_situation(&mut carte_ps)?.mode_d_exercice =
String::from_utf8_lossy(field.content).to_string();
}
(2..=16, 3) => {
get_last_mut_situation(&mut carte_ps)?.statut_d_exercice =
String::from_utf8_lossy(field.content).to_string();
}
(2..=16, 4) => {
get_last_mut_situation(&mut carte_ps)?.secteur_d_activite =
String::from_utf8_lossy(field.content).to_string();
}
(2..=16, 5) => {
get_last_mut_situation(&mut carte_ps)?.type_d_identification_structure =
String::from_utf8_lossy(field.content).to_string();
}
(2..=16, 6) => {
get_last_mut_situation(&mut carte_ps)?.numero_d_identification_structure =
String::from_utf8_lossy(field.content).to_string();
}
(2..=16, 7) => {
get_last_mut_situation(&mut carte_ps)?
.cle_du_numero_d_identification_structure =
String::from_utf8_lossy(field.content).to_string();
}
(2..=16, 8) => {
get_last_mut_situation(&mut carte_ps)?.raison_sociale_structure =
String::from_utf8_lossy(field.content).to_string();
}
(2..=16, 9) => {
get_last_mut_situation(&mut carte_ps)?
.numero_d_identification_de_facturation_du_ps =
String::from_utf8_lossy(field.content).to_string();
}
(2..=16, 10) => {
get_last_mut_situation(&mut carte_ps)?
.cle_du_numero_d_identification_de_facturation_du_ps =
String::from_utf8_lossy(field.content).to_string();
}
(2..=16, 11) => {
get_last_mut_situation(&mut carte_ps)?
.numero_d_identification_du_ps_remplaçant =
String::from_utf8_lossy(field.content).to_string();
}
(2..=16, 12) => {
get_last_mut_situation(&mut carte_ps)?
.cle_du_numero_d_identification_du_ps_remplaçant =
String::from_utf8_lossy(field.content).to_string();
}
(2..=16, 13) => {
get_last_mut_situation(&mut carte_ps)?.code_conventionnel =
String::from_utf8_lossy(field.content).to_string();
}
(2..=16, 14) => {
get_last_mut_situation(&mut carte_ps)?.code_specialite =
String::from_utf8_lossy(field.content).to_string();
}
(2..=16, 15) => {
get_last_mut_situation(&mut carte_ps)?.code_zone_tarifaire =
String::from_utf8_lossy(field.content).to_string();
}
(2..=16, 16) => {
get_last_mut_situation(&mut carte_ps)?.code_zone_ik =
String::from_utf8_lossy(field.content).to_string();
}
(2..=16, 17) => {
get_last_mut_situation(&mut carte_ps)?.code_agrement_1 =
String::from_utf8_lossy(field.content).to_string();
}
(2..=16, 18) => {
get_last_mut_situation(&mut carte_ps)?.code_agrement_2 =
String::from_utf8_lossy(field.content).to_string();
}
(2..=16, 19) => {
get_last_mut_situation(&mut carte_ps)?.code_agrement_3 =
String::from_utf8_lossy(field.content).to_string();
}
(2..=16, 20) => {
get_last_mut_situation(&mut carte_ps)?.habilitation_à_signer_une_facture =
String::from_utf8_lossy(field.content).to_string();
}
(2..=16, 21) => {
get_last_mut_situation(&mut carte_ps)?.habilitation_à_signer_un_lot =
String::from_utf8_lossy(field.content).to_string();
}
_ => {
return Err(CartePSError::UnknownGroupFieldPair {
group: group.id,
field: field.id,
});
}
}
}
}
Ok(carte_ps)
}
#[cfg(test)]
mod test_decode_carte_ps {
use super::*;
#[test]
fn test_francoise_pharmacien0052419() {
let bytes: &[u8] = &[
0, 1, 51, // Block 01, Content size 51
1, 48, // Field 01, Content size 1
1, 56, // Field 02, Content size 1
11, 57, 57, 55, 48, 48, 53, 50, 52, 49, 57, 52, // Field 03, Content size 11
1, 52, // Field 04, Content size 1
2, 50, 50, // Field 05, Content size 2
17, 80, 72, 65, 82, 77, 65, 67, 73, 69, 78, 48, 48, 53, 50, 52, 49,
57, // Field 06, Content size 17
9, 70, 82, 65, 78, 67, 79, 73, 83, 69, // Field 07, Content size 9
1, 84, // Field 08, Content size 1
0, 2, 83, // Block 02, Content size 83
1, 1, 1, 48, 1, 49, 2, 56, 54, 1, 49, 9, 48, 66, 48, 50, 50, 49, 57, 53, 56, 1, 56, 24,
80, 72, 65, 82, 77, 65, 67, 73, 69, 32, 68, 85, 32, 67, 69, 78, 84, 82, 69, 50, 50, 49,
57, 53, 8, 48, 48, 50, 48, 50, 52, 49, 57, 1, 56, 0, 1, 48, 1, 49, 2, 53, 48, 2, 49,
48, 2, 48, 48, 1, 48, 1, 48, 1, 48, 1, 49, 1, 49,
];
let blocks = decode_ssv_memory(bytes, bytes.len()).unwrap();
let carte_ps = decode_carte_ps(blocks).unwrap();
assert_eq!(carte_ps.titulaire.type_de_carte_ps, "0");
assert_eq!(carte_ps.titulaire.type_d_identification_nationale, "8");
assert_eq!(
carte_ps.titulaire.numero_d_identification_nationale,
"99700524194"
);
assert_eq!(
carte_ps.titulaire.cle_du_numero_d_identification_nationale,
"4"
);
assert_eq!(carte_ps.titulaire.code_civilite, "22");
assert_eq!(carte_ps.titulaire.nom_du_ps, "PHARMACIEN0052419");
assert_eq!(carte_ps.titulaire.prenom_du_ps, "FRANCOISE");
assert_eq!(carte_ps.titulaire.categorie_carte, 'T');
assert_eq!(carte_ps.situations.len(), 1);
assert_eq!(
carte_ps.situations[0].numero_logique_de_la_situation_de_facturation_du_ps,
1
);
assert_eq!(carte_ps.situations[0].mode_d_exercice, "0");
assert_eq!(carte_ps.situations[0].statut_d_exercice, "1");
assert_eq!(carte_ps.situations[0].secteur_d_activite, "86");
assert_eq!(carte_ps.situations[0].type_d_identification_structure, "1");
assert_eq!(
carte_ps.situations[0].numero_d_identification_structure,
"0B0221958"
);
assert_eq!(
carte_ps.situations[0].cle_du_numero_d_identification_structure,
"8"
);
assert_eq!(
carte_ps.situations[0].raison_sociale_structure,
"PHARMACIE DU CENTRE22195"
);
assert_eq!(
carte_ps.situations[0].numero_d_identification_de_facturation_du_ps,
"00202419"
);
assert_eq!(
carte_ps.situations[0].cle_du_numero_d_identification_de_facturation_du_ps,
"8"
);
assert_eq!(
carte_ps.situations[0].numero_d_identification_du_ps_remplaçant,
""
);
assert_eq!(
carte_ps.situations[0].cle_du_numero_d_identification_du_ps_remplaçant,
"0"
);
assert_eq!(carte_ps.situations[0].code_conventionnel, "1");
assert_eq!(carte_ps.situations[0].code_specialite, "50");
assert_eq!(carte_ps.situations[0].code_zone_tarifaire, "10");
assert_eq!(carte_ps.situations[0].code_zone_ik, "00");
assert_eq!(carte_ps.situations[0].code_agrement_1, "0");
assert_eq!(carte_ps.situations[0].code_agrement_2, "0");
assert_eq!(carte_ps.situations[0].code_agrement_3, "0");
assert_eq!(
carte_ps.situations[0].habilitation_à_signer_une_facture,
"1"
);
assert_eq!(carte_ps.situations[0].habilitation_à_signer_un_lot, "1");
}
#[test]
fn test_multiple_situations() {
let bytes: &[u8] = &[
0, 1, 51, // Block 01, Content size 51
1, 48, 1, 56, 11, 57, 57, 55, 48, 48, 53, 50, 52, 49, 57, 52, 1, 52, 2, 50, 50, 17, 80,
72, 65, 82, 77, 65, 67, 73, 69, 78, 48, 48, 53, 50, 52, 49, 57, 9, 70, 82, 65, 78, 67,
79, 73, 83, 69, 1, 84, 0, 2, 83, // Block 02, Content size 83
1, 1, 1, 48, 1, 49, 2, 56, 54, 1, 49, 9, 48, 66, 48, 50, 50, 49, 57, 53, 56, 1, 56, 24,
80, 72, 65, 82, 77, 65, 67, 73, 69, 32, 68, 85, 32, 67, 69, 78, 84, 82, 69, 50, 50, 49,
57, 53, 8, 48, 48, 50, 48, 50, 52, 49, 57, 1, 56, 0, 1, 48, 1, 49, 2, 53, 48, 2, 49,
48, 2, 48, 48, 1, 48, 1, 48, 1, 48, 1, 49, 1, 49, 0, 3,
83, // Block 03, Content size 83
1, 1, 1, 48, 1, 49, 2, 56, 54, 1, 49, 9, 48, 66, 48, 50, 50, 49, 57, 53, 56, 1, 56, 24,
80, 72, 65, 82, 77, 65, 67, 73, 69, 32, 68, 85, 32, 67, 69, 78, 84, 82, 69, 50, 50, 49,
57, 53, 8, 48, 48, 50, 48, 50, 52, 49, 57, 1, 56, 0, 1, 48, 1, 49, 2, 53, 48, 2, 49,
48, 2, 48, 48, 1, 48, 1, 48, 1, 48, 1, 49, 1, 49, 0, 4,
83, // Block 04, Content size 83
1, 1, 1, 48, 1, 49, 2, 56, 54, 1, 49, 9, 48, 66, 48, 50, 50, 49, 57, 53, 56, 1, 56, 24,
80, 72, 65, 82, 77, 65, 67, 73, 69, 32, 68, 85, 32, 67, 69, 78, 84, 82, 69, 50, 50, 49,
57, 53, 8, 48, 48, 50, 48, 50, 52, 49, 57, 1, 56, 0, 1, 48, 1, 49, 2, 53, 48, 2, 49,
48, 2, 48, 48, 1, 48, 1, 48, 1, 48, 1, 49, 1, 49,
];
let blocks = decode_ssv_memory(bytes, bytes.len()).unwrap();
let carte_ps = decode_carte_ps(blocks).unwrap();
assert_eq!(carte_ps.situations.len(), 3);
assert_eq!(
carte_ps.situations[0].raison_sociale_structure,
"PHARMACIE DU CENTRE22195"
);
assert_eq!(
carte_ps.situations[1].raison_sociale_structure,
"PHARMACIE DU CENTRE22195"
);
assert_eq!(
carte_ps.situations[2].raison_sociale_structure,
"PHARMACIE DU CENTRE22195"
);
}
#[test]
#[should_panic]
fn test_missing_field() {
todo!();
}
#[test]
#[should_panic]
fn test_unknown_group_field_pair() {
todo!();
}
#[test]
#[should_panic]
fn test_invalid_field_format() {
todo!();
}
}

View File

@ -1,8 +1,3 @@
pub mod cps;
pub mod libssv;
pub mod ssv_memory;
pub mod ssvlib_demo;
pub fn add(left: usize, right: usize) -> usize { pub fn add(left: usize, right: usize) -> usize {
left + right left + right
} }

View File

@ -1,30 +1,17 @@
/// libssv.rs /**
/// * libssv.rs
/// Low level bindings to the SSVLIB dynamic library. *
// TODO : look for creating a dedicated *-sys crate : https://kornel.ski/rust-sys-crate * Low level bindings to the SSVLIB dynamic library.
use libc::{c_char, c_ushort, c_void, size_t}; * TODO : look for creating a dedicated *-sys crate : https://kornel.ski/rust-sys-crate
use thiserror::Error; */
#[derive(Debug, Error)] use libc::{ c_char, c_void, c_ushort, size_t };
pub enum LibSSVError {
#[error("SSV library error in {function}: {code}")]
StandardErrorCode { code: u16, function: &'static str },
}
#[cfg_attr(target_os = "linux", link(name = "ssvlux64"))] #[cfg_attr(target_os = "linux", link(name = "ssvlux64"))]
#[cfg_attr(target_os = "windows", link(name = "ssvw64"))] #[cfg_attr(target_os = "windows", link(name = "ssvw64"))]
extern "C" { extern "C" {
pub fn SSV_InitLIB2(pcRepSesamIni: *const c_char) -> c_ushort; pub fn SSV_InitLIB2(pcRepSesamIni: *const c_char) -> c_ushort;
pub fn SSV_LireCartePS( pub fn SSV_LireCartePS(NomRessourcePS: *const c_char, NomRessourceLecteur: *const c_char, CodePorteurPS: *const c_char, ZDonneesSortie: *mut *mut c_void, TTailleDonneesSortie: *mut size_t) -> c_ushort;
NomRessourcePS: *const c_char, pub fn SSV_LireConfig(ZDonneesSortie: *mut *mut c_void, TTailleDonneesSortie: *mut size_t) -> c_ushort;
NomRessourceLecteur: *const c_char,
CodePorteurPS: *const c_char,
ZDonneesSortie: *mut *mut c_void,
TTailleDonneesSortie: *mut size_t,
) -> c_ushort;
pub fn SSV_LireConfig(
ZDonneesSortie: *mut *mut c_void,
TTailleDonneesSortie: *mut size_t,
) -> c_ushort;
} }
// TODO : replace void* by Rust struct : https://doc.rust-lang.org/nomicon/ffi.html#representing-opaque-structs /* TODO : replace void* by Rust struct : https://doc.rust-lang.org/nomicon/ffi.html#representing-opaque-structs */

View File

@ -1,10 +1,8 @@
mod cps; mod ssvlib_demo;
mod libssv; mod libssv;
mod ssv_memory; mod ssv_memory;
mod ssvlib_demo;
use anyhow::{Context, Result}; fn main() {
ssvlib_demo::demo();
fn main() -> Result<()> { // XXX
ssvlib_demo::demo().context("Error while running the SSV library demo")
} }

View File

@ -1,33 +1,10 @@
/// # SSV Memory /**
/// Provide functions to manipulate raw memory from SSV library. * Provide functions to manipulate raw memory from SSV library.
*/
// TODO : Est-ce qu'on pourrait/devrait définir un type custom pour représenter les tableaux de bytes ?
use std::convert::TryFrom; use std::convert::TryFrom;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum BytesReadingError {
#[error("Empty bytes input")]
EmptyBytes,
#[error("Invalid memory: not enough bytes ({actual}) to read the expected size ({expected})")]
InvalidSize { expected: usize, actual: usize },
#[error("Invalid memory: size ({actual}) is expected to be less than {expected} bytes")]
SizeTooBig { expected: usize, actual: usize },
#[error("Invalid memory: not enough bytes to read the block id")]
InvalidBlockId(#[from] std::array::TryFromSliceError),
#[error("Error while reading field at offset {offset}")]
InvalidField {
source: Box<BytesReadingError>,
offset: usize,
},
}
#[derive(Debug, Error)]
pub enum SSVMemoryError {
#[error("Error while parsing block at offset {offset}")]
BlockParsing {
source: BytesReadingError,
offset: usize,
},
}
#[derive(PartialEq, Debug)] #[derive(PartialEq, Debug)]
struct ElementSize { struct ElementSize {
@ -35,19 +12,19 @@ struct ElementSize {
pub pad: usize, pub pad: usize,
} }
// TODO : Est-ce qu'on pourrait/devrait définir un type custom pour représenter les tableaux de bytes ?
impl TryFrom<&[u8]> for ElementSize { impl TryFrom<&[u8]> for ElementSize {
type Error = BytesReadingError; type Error = &'static str;
fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> { fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> {
if bytes.is_empty() { /* Longueur:
return Err(BytesReadingError::EmptyBytes); * - si le bit de poids fort du premier octet est à 0, la longueur est codée sur un octet
* - si le bit de poids fort du premier octet est à 1, les 7 bits de poids faible codent le nombre d'octets utilisés pour coder la longueur
*/
if bytes.len() == 0 {
return Err("Empty bytes input");
} }
let mut element_size = ElementSize { size: 0, pad: 1 }; let mut element_size = ElementSize { size: 0, pad: 1 };
// Longueur:
// - si le bit de poids fort du premier octet est à 0, la longueur est codée sur un octet
// - si le bit de poids fort du premier octet est à 1, les 7 bits de poids faible codent le nombre d'octets utilisés pour coder la longueur
if bytes[0] & 0b1000_0000 == 0 { if bytes[0] & 0b1000_0000 == 0 {
// Size coded on 1 byte // Size coded on 1 byte
element_size.size = bytes[0] as usize; element_size.size = bytes[0] as usize;
@ -56,21 +33,15 @@ impl TryFrom<&[u8]> for ElementSize {
// N are the 7 lower bits of the first byte // N are the 7 lower bits of the first byte
let size_bytes_len = (bytes[0] & 0b0111_1111) as usize; let size_bytes_len = (bytes[0] & 0b0111_1111) as usize;
if size_bytes_len > bytes.len() - 1 { if size_bytes_len > bytes.len() - 1 {
return Err(BytesReadingError::InvalidSize { return Err("Invalid memory: not enough bytes to read the size");
expected: size_bytes_len,
actual: bytes.len() - 1,
});
} else if size_bytes_len > 4 { } else if size_bytes_len > 4 {
return Err(BytesReadingError::SizeTooBig { return Err("Invalid memory: size is too big");
expected: 4,
actual: size_bytes_len,
});
} }
let size_bytes = &bytes[1..1 + size_bytes_len]; let size_bytes = &bytes[1..1 + size_bytes_len];
// u32::from_be_bytes() requires a 4 bytes array // u32::from_be_bytes() requires a 4 bytes array
let mut padded_bytes = [0u8; 4]; let mut padded_bytes = [0u8; 4];
padded_bytes[size_bytes_len..].copy_from_slice(size_bytes); padded_bytes[size_bytes_len..].copy_from_slice(&size_bytes);
element_size.size = u32::from_be_bytes(padded_bytes) as usize; element_size.size = u32::from_be_bytes(padded_bytes) as usize;
element_size.pad += size_bytes_len; element_size.pad += size_bytes_len;
@ -86,89 +57,60 @@ pub struct Block<'a> {
pub content: Vec<Field<'a>>, pub content: Vec<Field<'a>>,
} }
impl<'a> TryFrom<&'a [u8]> for Block<'a> { impl<'a> From<&'a [u8]> for Block<'a> {
type Error = BytesReadingError; fn from(bytes: &'a [u8]) -> Self {
fn try_from(bytes: &'a [u8]) -> Result<Self, Self::Error> {
let mut offset = 0; let mut offset = 0;
let id = u16::from_be_bytes( let id = u16::from_be_bytes(bytes[..2].try_into().unwrap());
bytes[..2]
.try_into()
.map_err(BytesReadingError::InvalidBlockId)?,
);
offset += 2; offset += 2;
let ElementSize { let ElementSize { size: block_size, pad } = bytes[2..].try_into().unwrap();
size: block_size,
pad,
} = bytes[2..].try_into()?;
offset += pad; offset += pad;
let raw_content = &bytes[offset..]; let raw_content = &bytes[offset..];
let mut field_offset = 0; let mut field_offset = 0;
// While there is still content to read, parse Fields // While there is still content to read, parse Fields
let mut content = Vec::new(); let mut content = Vec::new();
let mut field_id = 1;
while field_offset < block_size { while field_offset < block_size {
let mut field: Field<'a> = raw_content[field_offset..].try_into().map_err(|err| { let field: Field<'a> = raw_content[field_offset..].into();
BytesReadingError::InvalidField {
source: Box::new(err),
offset: field_offset,
}
})?;
field.id = field_id;
field_offset += field.size; field_offset += field.size;
field_id += 1;
content.push(field); content.push(field);
} }
Ok(Block { Block {
id, id,
size: offset + block_size, size: offset + block_size,
content, content,
}) }
} }
} }
#[derive(Debug)] #[derive(Debug)]
pub struct Field<'a> { pub struct Field<'a> {
pub id: u16,
pub size: usize, pub size: usize,
pub content: &'a [u8], pub content: &'a [u8],
} }
impl<'a> TryFrom<&'a [u8]> for Field<'a> { impl<'a> From<&'a [u8]> for Field<'a> {
type Error = BytesReadingError; fn from(bytes: &'a [u8]) -> Self {
let ElementSize { size, pad } = bytes.try_into().unwrap();
fn try_from(bytes: &'a [u8]) -> Result<Self, Self::Error> { let contenu = &bytes[pad..pad+size];
let ElementSize { size, pad } = bytes.try_into()?; Field {
let contenu = &bytes[pad..pad + size]; size: pad+size,
Ok(Field {
id: 0,
size: pad + size,
content: contenu, content: contenu,
}) }
} }
} }
pub fn decode_ssv_memory(bytes: &[u8], size: usize) -> Result<Vec<Block>, SSVMemoryError> { pub fn decode_ssv_memory(bytes: &[u8], size: usize) -> Vec<Block> {
let mut blocks: Vec<Block> = Vec::new(); let mut blocks: Vec<Block> = Vec::new();
let mut offset = 0; let mut offset = 0;
while offset < size { while offset < size {
let block: Block = let block: Block = bytes[offset..].into();
bytes[offset..]
.try_into()
.map_err(|err| SSVMemoryError::BlockParsing {
source: err,
offset,
})?;
offset += block.size; offset += block.size;
blocks.push(block); blocks.push(block);
} }
Ok(blocks) blocks
} }
#[cfg(test)] #[cfg(test)]
mod test_element_size { mod test_element_size {
use std::any::Any;
use super::*; use super::*;
#[test] #[test]
@ -195,50 +137,34 @@ mod test_element_size {
#[test] #[test]
fn null_size() { fn null_size() {
let bytes: &[u8] = &[]; let bytes: &[u8] = &[];
let result: Result<ElementSize, BytesReadingError> = bytes.try_into(); let result: Result<ElementSize, &str> = bytes.try_into();
assert!(result.is_err());
assert_eq!( assert_eq!(
result.unwrap_err().type_id(), result,
BytesReadingError::EmptyBytes.type_id() Err("Empty bytes input"),
); );
} }
#[test] #[test]
fn invalid_memory() { fn invalid_memory() {
let bytes: &[u8] = &[0b_1000_0001_u8]; let bytes: &[u8] = &[0b_1000_0001_u8];
let result: Result<ElementSize, BytesReadingError> = bytes.try_into(); let result: Result<ElementSize, &str> = bytes.try_into();
assert!(result.is_err());
assert_eq!( assert_eq!(
result.unwrap_err().to_string(), result,
BytesReadingError::InvalidSize { Err("Invalid memory: not enough bytes to read the size"),
expected: 1,
actual: 0
}
.to_string()
); );
let bytes: &[u8] = &[0b_1000_0010_u8, 1]; let bytes: &[u8] = &[0b_1000_0010_u8, 1];
let result: Result<ElementSize, BytesReadingError> = bytes.try_into(); let result: Result<ElementSize, &str> = bytes.try_into();
assert!(result.is_err());
assert_eq!( assert_eq!(
result.unwrap_err().to_string(), result,
BytesReadingError::InvalidSize { Err("Invalid memory: not enough bytes to read the size"),
expected: 2,
actual: 1
}
.to_string()
); );
let bytes: &[u8] = &[0b_1000_0101_u8, 1, 1, 1, 1, 1]; let bytes: &[u8] = &[0b_1000_0101_u8, 1, 1, 1, 1, 1];
let result: Result<ElementSize, BytesReadingError> = bytes.try_into(); let result: Result<ElementSize, &str> = bytes.try_into();
assert!(result.is_err());
assert_eq!( assert_eq!(
result.unwrap_err().to_string(), result,
BytesReadingError::SizeTooBig { Err("Invalid memory: size is too big"),
expected: 4,
actual: 5
}
.to_string()
); );
} }
} }
@ -249,10 +175,13 @@ mod test_field {
#[test] #[test]
fn short_size() { fn short_size() {
let bytes: &[u8] = &[ let bytes: &[u8] = &[51,
51, 1, 48, 1, 56, 11, 57, 57, 55, 48, 48, 53, 50, 52, 49, 57, 52, 1, 52, 2, 50, 50, 17, 1, 48, 1, 56, 11, 57, 57, 55, 48, 48,
80, 72, 65, 82, 77, 65, 67, 73, 69, 78, 48, 48, 53, 50, 52, 49, 57, 9, 70, 82, 65, 78, 53, 50, 52, 49, 57, 52, 1, 52, 2, 50,
67, 79, 73, 83, 69, 1, 84, 50, 17, 80, 72, 65, 82, 77, 65, 67, 73,
69, 78, 48, 48, 53, 50, 52, 49, 57, 9,
70, 82, 65, 78, 67, 79, 73, 83, 69, 1,
84,
]; ];
let element: Field = bytes.try_into().unwrap(); let element: Field = bytes.try_into().unwrap();
assert_eq!(element.size, 52); assert_eq!(element.size, 52);
@ -261,10 +190,8 @@ mod test_field {
#[test] #[test]
fn long_size() { fn long_size() {
let mut bytes_vec = vec![ let mut bytes_vec = vec![0b_1000_0010_u8,
0b_1000_0010_u8, 0b_0000_0001_u8, 0b_0000_0000_u8, // size = 256
0b_0000_0001_u8,
0b_0000_0000_u8, // size = 256
]; ];
// Add 256 bytes to the content // Add 256 bytes to the content
bytes_vec.append(&mut vec![1; 256]); bytes_vec.append(&mut vec![1; 256]);
@ -281,22 +208,23 @@ mod test_block {
#[test] #[test]
fn test_francoise_pharmacien0052419_partial_block_1() { fn test_francoise_pharmacien0052419_partial_block_1() {
let bytes: &[u8] = &[1, 48, 1, 56, 11, 57, 57, 55, 48, 48, 53, 50, 52, 49, 57, 52]; let bytes: &[u8] = &[
1, 48,
1, 56,
11, 57, 57, 55, 48, 48, 53, 50, 52, 49, 57, 52,
];
let field1: Field = bytes.try_into().unwrap(); let field1: Field = bytes.into();
assert_eq!(field1.size, 2); assert_eq!(field1.size, 2);
assert_eq!(field1.content, &[48]); assert_eq!(field1.content, &[48]);
let field2: Field = bytes[field1.size..].try_into().unwrap(); let field2: Field = bytes[field1.size..].into();
assert_eq!(field2.size, 2); assert_eq!(field2.size, 2);
assert_eq!(field2.content, &[56]); assert_eq!(field2.content, &[56]);
let field3: Field = bytes[field1.size + field2.size..].try_into().unwrap(); let field3: Field = bytes[field1.size + field2.size..].into();
assert_eq!(field3.size, 12); assert_eq!(field3.size, 12);
assert_eq!( assert_eq!(field3.content, &[57, 57, 55, 48, 48, 53, 50, 52, 49, 57, 52]);
field3.content,
&[57, 57, 55, 48, 48, 53, 50, 52, 49, 57, 52]
);
} }
#[test] #[test]
@ -312,18 +240,37 @@ mod test_block {
9, 70, 82, 65, 78, 67, 79, 73, 83, 69, // 10 9, 70, 82, 65, 78, 67, 79, 73, 83, 69, // 10
1, 84, // 2 1, 84, // 2
// total: 54 // total: 54
0, 2, 83, 1, 1, 1, 48, 1, 49, 2, 56, 54, 1, 49, 9, 48, 66, 48, 50, 50, 49, 57, 53, 56,
1, 56, 24, 80, 72, 65, 82, 77, 65, 67, 73, 69, 32, 68, 85, 32, 67, 69, 78, 84, 82, 69, 0, 2, 83,
50, 50, 49, 57, 53, 8, 48, 48, 50, 48, 50, 52, 49, 57, 1, 56, 0, 1, 48, 1, 49, 2, 53, 1, 1,
48, 2, 49, 48, 2, 48, 48, 1, 48, 1, 48, 1, 48, 1, 49, 1, 49, 1, 48,
1, 49,
2, 56, 54,
1, 49,
9, 48, 66, 48, 50, 50, 49, 57, 53, 56,
1, 56,
24, 80, 72, 65, 82, 77, 65, 67, 73, 69, 32, 68, 85, 32, 67, 69, 78, 84, 82, 69, 50, 50, 49, 57, 53,
8, 48, 48, 50, 48, 50, 52, 49, 57,
1, 56,
0,
1, 48,
1, 49,
2, 53, 48,
2, 49, 48,
2, 48, 48,
1, 48,
1, 48,
1, 48,
1, 49,
1, 49,
]; ];
let first_block: Block = bytes.try_into().unwrap(); let first_block: Block = bytes.into();
assert_eq!(first_block.id, 1); assert_eq!(first_block.id, 1);
assert_eq!(first_block.size, 54); assert_eq!(first_block.size, 54);
assert_eq!(first_block.content.len(), 8); assert_eq!(first_block.content.len(), 8);
let second_block: Block = bytes[first_block.size..].try_into().unwrap(); let second_block: Block = bytes[first_block.size..].into();
assert_eq!(second_block.id, 2); assert_eq!(second_block.id, 2);
assert_eq!(second_block.size, 86); assert_eq!(second_block.size, 86);
assert_eq!(second_block.content.len(), 21); assert_eq!(second_block.content.len(), 21);
@ -347,12 +294,32 @@ mod test_decode_ssv_memory {
9, 70, 82, 65, 78, 67, 79, 73, 83, 69, // 10 9, 70, 82, 65, 78, 67, 79, 73, 83, 69, // 10
1, 84, // 2 1, 84, // 2
// total: 54 // total: 54
0, 2, 83, 1, 1, 1, 48, 1, 49, 2, 56, 54, 1, 49, 9, 48, 66, 48, 50, 50, 49, 57, 53, 56,
1, 56, 24, 80, 72, 65, 82, 77, 65, 67, 73, 69, 32, 68, 85, 32, 67, 69, 78, 84, 82, 69, 0, 2, 83,
50, 50, 49, 57, 53, 8, 48, 48, 50, 48, 50, 52, 49, 57, 1, 56, 0, 1, 48, 1, 49, 2, 53, 1, 1,
48, 2, 49, 48, 2, 48, 48, 1, 48, 1, 48, 1, 48, 1, 49, 1, 49, 1, 48,
1, 49,
2, 56, 54,
1, 49,
9, 48, 66, 48, 50, 50, 49, 57, 53, 56,
1, 56,
24, 80, 72, 65, 82, 77, 65, 67, 73, 69, 32, 68, 85, 32, 67, 69, 78, 84, 82, 69, 50, 50, 49, 57, 53,
8, 48, 48, 50, 48, 50, 52, 49, 57,
1, 56,
0,
1, 48,
1, 49,
2, 53, 48,
2, 49, 48,
2, 48, 48,
1, 48,
1, 48,
1, 48,
1, 49,
1, 49,
]; ];
let blocks = decode_ssv_memory(bytes, bytes.len()).unwrap(); let blocks = decode_ssv_memory(&bytes, bytes.len());
println!("{:#?}", blocks);
assert_eq!(blocks.len(), 2); assert_eq!(blocks.len(), 2);
} }
} }

View File

@ -1,56 +1,423 @@
/// High level API for the SSV library, extern crate dotenv;
/// based on the low level bindings in libssv.rs. /**
* High level API for the SSV library,
* based on the low level bindings in libssv.rs.
*
*/
extern crate libc;
use libc::{c_void, size_t}; use libc::{c_void, size_t};
use std::env;
use std::ffi::CString; use std::ffi::CString;
use std::path::PathBuf;
use std::ptr; use std::ptr;
use thiserror::Error;
use crate::cps::lire_carte; use std::env;
use crate::libssv::{SSV_InitLIB2, SSV_LireConfig};
use ::utils::config::load_config; use crate::libssv::{SSV_InitLIB2, SSV_LireCartePS, SSV_LireConfig};
#[derive(Error, Debug)] fn ssv_init_lib_2() {
pub enum SSVDemoError {
#[error(transparent)]
CartePSReading(#[from] crate::cps::CartePSError),
#[error(transparent)]
SSVLibErrorCode(#[from] crate::libssv::LibSSVError),
#[error(transparent)]
Anyhow(#[from] anyhow::Error),
}
fn ssv_init_lib_2() -> Result<(), SSVDemoError> {
let ini_str = env::var("SESAM_INI_PATH").expect("SESAM_INI_PATH must be set"); let ini_str = env::var("SESAM_INI_PATH").expect("SESAM_INI_PATH must be set");
let ini = CString::new(ini_str).expect("CString::new failed"); let ini = CString::new(ini_str).expect("CString::new failed");
unsafe { unsafe {
let result = SSV_InitLIB2(ini.as_ptr()); let result = SSV_InitLIB2(ini.as_ptr());
println!("SSV_InitLIB2 result: {}", result); println!("SSV_InitLIB2 result: {}", result);
if result != 0 {
return Err(crate::libssv::LibSSVError::StandardErrorCode {
code: result,
function: "SSV_InitLIB2",
} }
.into());
}
}
Ok(())
} }
fn ssv_lire_config() -> Result<(), SSVDemoError> { /*
1. CB = Caractères Binaires »
2. CE = Caractères « Etendus » (ISO 8859-1)
3. CA = Caractères Alphanumériques (ASCII?)
4. CN = Caractères Numériques
*/
#[derive(Debug)]
struct CartePS {
titulaire: TitulairePS,
situations: Vec<SituationPS>,
}
#[derive(Debug)]
struct TitulairePS {
type_de_carte_ps: String, // CN
type_d_identification_nationale: String, // CN
numero_d_identification_nationale: String, // CE - 8 -> 30
cle_du_numero_d_identification_nationale: String, // CN
code_civilite: String, // CN
nom_du_ps: String, // CE - 27
prenom_du_ps: String, // CE - 27
categorie_carte: char, // CA
}
#[derive(Debug)]
struct SituationPS {
numero_logique_de_la_situation_de_facturation_du_ps: u8,
mode_d_exercice: String,
statut_d_exercice: String,
secteur_d_activite: String,
type_d_identification_structure: String,
numero_d_identification_structure: String,
cle_du_numero_d_identification_structure: String,
raison_sociale_structure: String,
numero_d_identification_de_facturation_du_ps: String,
cle_du_numero_d_identification_de_facturation_du_ps: String,
numero_d_identification_du_ps_remplaçant: String,
cle_du_numero_d_identification_du_ps_remplaçant: String,
code_conventionnel: String,
code_specialite: String,
code_zone_tarifaire: String,
code_zone_ik: String,
code_agrement_1: String,
code_agrement_2: String,
code_agrement_3: String,
habilitation_à_signer_une_facture: String,
habilitation_à_signer_un_lot: String,
}
impl Default for SituationPS {
fn default() -> Self {
Self {
numero_logique_de_la_situation_de_facturation_du_ps: 0,
mode_d_exercice: String::new(),
statut_d_exercice: String::new(),
secteur_d_activite: String::new(),
type_d_identification_structure: String::new(),
numero_d_identification_structure: String::new(),
cle_du_numero_d_identification_structure: String::new(),
raison_sociale_structure: String::new(),
numero_d_identification_de_facturation_du_ps: String::new(),
cle_du_numero_d_identification_de_facturation_du_ps: String::new(),
numero_d_identification_du_ps_remplaçant: String::new(),
cle_du_numero_d_identification_du_ps_remplaçant: String::new(),
code_conventionnel: String::new(),
code_specialite: String::new(),
code_zone_tarifaire: String::new(),
code_zone_ik: String::new(),
code_agrement_1: String::new(),
code_agrement_2: String::new(),
code_agrement_3: String::new(),
habilitation_à_signer_une_facture: String::new(),
habilitation_à_signer_un_lot: String::new(),
}
}
}
fn ssv_lire_carte_ps(code_pin :&str, lecteur :&str) {
let resource_ps =
CString::new(lecteur).expect("CString::new failed");
let resource_reader = CString::new("").expect("CString::new failed");
let card_number = CString::new(code_pin).expect("CString::new failed");
let mut buffer: *mut c_void = ptr::null_mut();
let mut size: size_t = 0;
let mut hex_values: &[u8] = &[];
unsafe {
let result = SSV_LireCartePS(
resource_ps.as_ptr(),
resource_reader.as_ptr(),
card_number.as_ptr(),
&mut buffer,
&mut size,
);
println!("SSV_LireCartePS result: {}", result);
if !buffer.is_null() {
hex_values = std::slice::from_raw_parts(buffer as *const u8, size);
libc::free(buffer);
}
}
decode_zone_memoire(hex_values);
/* IDÉE : implémenter decode_zone_memoire sous forme d'itérateur
for (group, group_rank) in decode_zone_memoire(...) {
for (field, field_rank) in group {
match group_rank {
GROUP_1: {
match field_rank {
1 => carte_ps.titulaire.type_de_carte_ps = field.into(),
2 => carte_ps.titulaire.type_d_identification_nationale = field.into(),
3 => carte_ps.titulaire.numero_d_identification_nationale = field.into(),
4 => carte_ps.titulaire.cle_du_numero_d_identification_nationale = field.into(),
5 => carte_ps.titulaire.code_civilite = field.into(),
6 => carte_ps.titulaire.nom_du_ps = field.into(),
7 => carte_ps.titulaire.prenom_du_ps = field.into(),
8 => carte_ps.titulaire.categorie_carte = field.into(),
_ => (),
}
}
GROUP_2: {
match field_rank {
...
}
}
}
}
}
*/
}
/**
* Options possibles :
* - Passer un objet de type variable (CartePS, CarteVitale...) et le remplir intelligemment
* - Retourner un itérateur et faire le remplissage en dehors de decode_zone_memoire
*/
pub fn decode_zone_memoire(bytes: &[u8]) {
// Maintenant, vous pouvez accéder aux octets individuels dans `donnees`
let mut current_pos_general = 0;
let mut current_groupe = 0;
let mut num_champ = 0;
let mut carte_ps = CartePS {
titulaire: TitulairePS {
type_de_carte_ps: String::new(),
type_d_identification_nationale: String::new(),
numero_d_identification_nationale: String::new(),
cle_du_numero_d_identification_nationale: String::new(),
code_civilite: String::new(),
nom_du_ps: String::new(),
prenom_du_ps: String::new(),
categorie_carte: ' ',
},
situations: Vec::new(),
};
println!("--- Decoding Zone Memoire ---");
println!("Taille de la zone mémoire: {}", bytes.len());
while current_pos_general < bytes.len() - 1 {
num_champ = 0;
current_groupe =
256 * bytes[current_pos_general] as i32 + bytes[current_pos_general + 1] as i32;
println!("-- Nouveau groupe : {}", current_groupe);
current_pos_general += 2;
let longueur_groupe: usize;
if bytes[current_pos_general] < 128 {
longueur_groupe = bytes[current_pos_general] as usize;
} else {
let nbre_octets_longueur = bytes[current_pos_general] - 128;
longueur_groupe = (0..nbre_octets_longueur).fold(0, |acc, i| {
current_pos_general += 1;
acc + (256_i32.pow(i as u32) * bytes[current_pos_general] as i32) as usize
});
}
println!("--- Longueur groupe: {}", longueur_groupe);
current_pos_general += 1;
let mut current_pos_groupe = 0;
while current_pos_groupe < longueur_groupe {
num_champ += 1;
println!("---- Nouveau champ : {}", num_champ);
let longueur_champs: usize;
let nbre_octets_longueur: usize;
if bytes[current_pos_general] < 128 {
longueur_champs = bytes[current_pos_general] as usize;
nbre_octets_longueur = 1;
} else {
nbre_octets_longueur = bytes[current_pos_general] as usize - 128;
longueur_champs = (0..nbre_octets_longueur).fold(0, |acc, i| {
current_pos_general += 1;
current_pos_groupe += 1;
acc + (256_i32.pow(i as u32) * bytes[current_pos_general] as i32) as usize
});
}
println!("----- Longueur champ: {}", longueur_champs);
let mut bytes_champ = vec![0; longueur_champs];
if longueur_champs > 0 {
bytes_champ.copy_from_slice(
&bytes[current_pos_general + nbre_octets_longueur
..current_pos_general + nbre_octets_longueur + longueur_champs],
);
println!("----- Bytes du champ: {:?}", bytes_champ);
match current_groupe {
1 => match num_champ {
1 => {
carte_ps.titulaire.type_de_carte_ps =
String::from_utf8_lossy(&bytes_champ).to_string();
}
2 => {
carte_ps.titulaire.type_d_identification_nationale =
String::from_utf8_lossy(&bytes_champ).to_string();
}
3 => {
carte_ps.titulaire.numero_d_identification_nationale =
String::from_utf8_lossy(&bytes_champ).to_string();
}
4 => {
carte_ps.titulaire.cle_du_numero_d_identification_nationale =
String::from_utf8_lossy(&bytes_champ).to_string();
}
5 => {
carte_ps.titulaire.code_civilite =
String::from_utf8_lossy(&bytes_champ).to_string();
}
6 => {
carte_ps.titulaire.nom_du_ps =
String::from_utf8_lossy(&bytes_champ).to_string();
}
7 => {
carte_ps.titulaire.prenom_du_ps =
String::from_utf8_lossy(&bytes_champ).to_string();
}
8 => {
carte_ps.titulaire.categorie_carte = bytes_champ[0] as char;
}
_ => (),
},
2 => match num_champ {
1 => {
carte_ps.situations.push(SituationPS::default());
carte_ps
.situations
.last_mut()
.unwrap()
.numero_logique_de_la_situation_de_facturation_du_ps =
bytes_champ[0];
}
2 => {
carte_ps.situations.last_mut().unwrap().mode_d_exercice =
String::from_utf8_lossy(&bytes_champ).to_string();
}
3 => {
carte_ps.situations.last_mut().unwrap().statut_d_exercice =
String::from_utf8_lossy(&bytes_champ).to_string();
}
4 => {
carte_ps.situations.last_mut().unwrap().secteur_d_activite =
String::from_utf8_lossy(&bytes_champ).to_string();
}
5 => {
carte_ps
.situations
.last_mut()
.unwrap()
.type_d_identification_structure =
String::from_utf8_lossy(&bytes_champ).to_string();
}
6 => {
carte_ps
.situations
.last_mut()
.unwrap()
.numero_d_identification_structure =
String::from_utf8_lossy(&bytes_champ).to_string();
}
7 => {
carte_ps
.situations
.last_mut()
.unwrap()
.cle_du_numero_d_identification_structure =
String::from_utf8_lossy(&bytes_champ).to_string();
}
8 => {
carte_ps
.situations
.last_mut()
.unwrap()
.raison_sociale_structure =
String::from_utf8_lossy(&bytes_champ).to_string();
}
9 => {
carte_ps
.situations
.last_mut()
.unwrap()
.numero_d_identification_de_facturation_du_ps =
String::from_utf8_lossy(&bytes_champ).to_string();
}
10 => {
carte_ps
.situations
.last_mut()
.unwrap()
.cle_du_numero_d_identification_de_facturation_du_ps =
String::from_utf8_lossy(&bytes_champ).to_string();
}
11 => {
carte_ps
.situations
.last_mut()
.unwrap()
.numero_d_identification_du_ps_remplaçant =
String::from_utf8_lossy(&bytes_champ).to_string();
}
12 => {
carte_ps
.situations
.last_mut()
.unwrap()
.cle_du_numero_d_identification_du_ps_remplaçant =
String::from_utf8_lossy(&bytes_champ).to_string();
}
13 => {
carte_ps.situations.last_mut().unwrap().code_conventionnel =
String::from_utf8_lossy(&bytes_champ).to_string();
}
14 => {
carte_ps.situations.last_mut().unwrap().code_specialite =
String::from_utf8_lossy(&bytes_champ).to_string();
}
15 => {
carte_ps.situations.last_mut().unwrap().code_zone_tarifaire =
String::from_utf8_lossy(&bytes_champ).to_string();
}
16 => {
carte_ps.situations.last_mut().unwrap().code_zone_ik =
String::from_utf8_lossy(&bytes_champ).to_string();
}
17 => {
carte_ps.situations.last_mut().unwrap().code_agrement_1 =
String::from_utf8_lossy(&bytes_champ).to_string();
}
18 => {
carte_ps.situations.last_mut().unwrap().code_agrement_2 =
String::from_utf8_lossy(&bytes_champ).to_string();
}
19 => {
carte_ps.situations.last_mut().unwrap().code_agrement_3 =
String::from_utf8_lossy(&bytes_champ).to_string();
}
20 => {
carte_ps
.situations
.last_mut()
.unwrap()
.habilitation_à_signer_une_facture =
String::from_utf8_lossy(&bytes_champ).to_string();
}
21 => {
carte_ps
.situations
.last_mut()
.unwrap()
.habilitation_à_signer_un_lot =
String::from_utf8_lossy(&bytes_champ).to_string();
}
_ => (),
},
_ => (),
}
} else {
println!("----- Pas de bytes pour ce champ");
}
current_pos_general += longueur_champs + nbre_octets_longueur;
current_pos_groupe += longueur_champs + nbre_octets_longueur;
}
}
println!("CartePS: {:#?}", carte_ps);
}
fn ssv_lire_config() {
let mut buffer: *mut c_void = ptr::null_mut(); let mut buffer: *mut c_void = ptr::null_mut();
let mut size: size_t = 0; let mut size: size_t = 0;
unsafe { unsafe {
let result = SSV_LireConfig(&mut buffer, &mut size); let result = SSV_LireConfig(&mut buffer, &mut size);
println!("SSV_LireConfig result: {}", result); println!("SSV_LireConfig result: {}", result);
if result != 0 {
return Err(crate::libssv::LibSSVError::StandardErrorCode {
code: result,
function: "SSV_LireConfig",
}
.into());
}
if !buffer.is_null() { if !buffer.is_null() {
let hex_values = std::slice::from_raw_parts(buffer as *const u8, size); let hex_values = std::slice::from_raw_parts(buffer as *const u8, size);
@ -62,26 +429,201 @@ fn ssv_lire_config() -> Result<(), SSVDemoError> {
libc::free(buffer); libc::free(buffer);
} }
} }
Ok(())
} }
pub fn demo() -> Result<(), SSVDemoError> { pub fn demo() {
// TODO : this is probably not working on release, because I'm not sure it exists a CARGO_MANIFEST_DIR and so it can find the `.env` /*
// Maybe we could use a system standard config path to store a config file * TODO : this is probably not working on release, because I'm not sure it exists a CARGO_MANIFEST_DIR and so it can find the `.env`
* Maybe we could use a system standard config path to store a config file
*/
let manifest_dir = env::var("CARGO_MANIFEST_DIR").unwrap();
let manifest_path = PathBuf::from(manifest_dir);
dotenv::from_path(manifest_path.join(".env")).ok();
println!("------- Demo for the SSV library --------"); println!("------- Demo for the SSV library 2 --------");
load_config()?;
ssv_init_lib_2()?;
ssv_init_lib_2();
let code_pin = "1234"; let code_pin = "1234";
let lecteur = "HID Global OMNIKEY 3x21 Smart Card Reader 0"; let lecteur = "HID Global OMNIKEY 3x21 Smart Card Reader 0";
let carte_ps = lire_carte(code_pin, lecteur)?; let carte_ps = ssv_lire_carte_ps2(code_pin, lecteur);
println!("CartePS: {:#?}", carte_ps); println!("CartePS: {:#?}", carte_ps);
// ssv_lire_config();
ssv_lire_config()?;
println!("-----------------------------------------"); println!("-----------------------------------------");
Ok(())
} }
struct field{
rank : i32,
value : Vec<u8>
}
impl Default for field {
fn default() -> Self {
Self {rank: 0,
value: Vec::new(),
}
}
}
pub struct group{
rank : i32,
fields : Vec<field>
}
impl Default for group {
fn default() -> Self {
Self {rank: 0,
fields: Vec::new(),
}
}
}
pub fn decode_zone_memoire2(bytes: &[u8]) -> Vec<group>{
let mut current_pos_general = 0;
let mut current_groupe = 0;
let mut num_champ = 0;
let mut longueur_groupe: usize;
let mut current_pos_groupe = 0;
let mut groups : Vec<group> = Vec::new();
while current_pos_general < (bytes.len() - 1) {
num_champ = 0;
current_groupe =
256 * bytes[current_pos_general] as i32 + bytes[current_pos_general + 1 ] as i32;
current_pos_general += 2;
groups.push(group::default());
groups.last_mut().unwrap().rank=current_groupe;
if bytes[current_pos_general] < 128 {
longueur_groupe = bytes[current_pos_general] as usize;
} else {
let nbre_octets_longueur = bytes[current_pos_general] - 128;
longueur_groupe = (0..nbre_octets_longueur).fold(0, |acc, i| {
current_pos_general += 1;
acc + (256_i32.pow(i as u32) * bytes[current_pos_general] as i32) as usize
});
}
current_pos_general += 1;
println!("------- 3--------");
current_pos_groupe = 0;
while current_pos_groupe < longueur_groupe {
num_champ += 1;
let longueur_champs: usize;
let nbre_octets_longueur: usize;
if bytes[current_pos_general] < 128 {
longueur_champs = bytes[current_pos_general] as usize;
nbre_octets_longueur = 1;
} else {
nbre_octets_longueur = bytes[current_pos_general] as usize - 128;
longueur_champs = (0..nbre_octets_longueur).fold(0, |acc, i| {
current_pos_general += 1;
current_pos_groupe += 1;
acc + (256_i32.pow(i as u32) * bytes[current_pos_general] as i32) as usize
});
}
groups.last_mut().unwrap().fields.push(field::default());
groups.last_mut().unwrap().fields.last_mut().unwrap().rank=num_champ;
if longueur_champs > 0 {
let mut bytes_champ = vec![0; longueur_champs];
bytes_champ.copy_from_slice(
&bytes[current_pos_general + nbre_octets_longueur
..current_pos_general + nbre_octets_longueur + longueur_champs],
);
//groups.last_mut().unwrap().fields.last_mut().unwrap().value= String::from_utf8_lossy(&bytes_champ).to_string();
groups.last_mut().unwrap().fields.last_mut().unwrap().value= bytes_champ;
}
current_pos_general += longueur_champs + nbre_octets_longueur;
current_pos_groupe += longueur_champs + nbre_octets_longueur;
}
}
groups
}
fn ssv_lire_carte_ps2(code_pin :&str, lecteur :&str) -> CartePS{
let resource_ps = CString::new(lecteur).expect("CString::new failed");
let resource_reader = CString::new("").expect("CString::new failed");
let card_number = CString::new(code_pin).expect("CString::new failed");
let mut buffer: *mut c_void = ptr::null_mut();
let mut size: size_t = 0;
let mut hex_values: &[u8] = &[];
unsafe {
let result = SSV_LireCartePS(
resource_ps.as_ptr(),
resource_reader.as_ptr(),
card_number.as_ptr(),
&mut buffer,
&mut size,
);
println!("SSV_LireCartePS result: {}", result);
if !buffer.is_null() {
hex_values = std::slice::from_raw_parts(buffer as *const u8, size);
libc::free(buffer);
}
}
let groups= decode_zone_memoire2(hex_values);
let mut carte_ps = CartePS {
titulaire: TitulairePS {
type_de_carte_ps: String::new(),
type_d_identification_nationale: String::new(),
numero_d_identification_nationale: String::new(),
cle_du_numero_d_identification_nationale: String::new(),
code_civilite: String::new(),
nom_du_ps: String::new(),
prenom_du_ps: String::new(),
categorie_carte: ' ',
},
situations: Vec::new(),
};
for item_group in groups.iter(){
for item_field in item_group.fields.iter(){
match (item_group.rank,item_field.rank) {
(1, 1) => {carte_ps.titulaire.type_de_carte_ps = String::from_utf8_lossy(&item_field.value).to_string();}
(1, 2) => {carte_ps.titulaire.type_d_identification_nationale = String::from_utf8_lossy(&item_field.value).to_string();}
(1, 3) => {carte_ps.titulaire.numero_d_identification_nationale = String::from_utf8_lossy(&item_field.value).to_string();}
(1, 4) => {carte_ps.titulaire.cle_du_numero_d_identification_nationale = String::from_utf8_lossy(&item_field.value).to_string();}
(1, 5) => {carte_ps.titulaire.code_civilite = String::from_utf8_lossy(&item_field.value).to_string();}
(1, 6) => {carte_ps.titulaire.nom_du_ps = String::from_utf8_lossy(&item_field.value).to_string();}
(1, 7) => {carte_ps.titulaire.prenom_du_ps = String::from_utf8_lossy(&item_field.value).to_string();}
(1, 8) => unsafe {carte_ps.titulaire.categorie_carte = char::from_u32_unchecked(item_field.value[0] as u32)}
(2, 1) => {carte_ps.situations.push(SituationPS::default());
carte_ps.situations.last_mut().unwrap().numero_logique_de_la_situation_de_facturation_du_ps = item_field.value[0];}
(2, 2) => {carte_ps.situations.last_mut().unwrap().mode_d_exercice = String::from_utf8_lossy(&item_field.value).to_string();}
(2, 3) => {carte_ps.situations.last_mut().unwrap().statut_d_exercice = String::from_utf8_lossy(&item_field.value).to_string();}
(2, 4) => {carte_ps.situations.last_mut().unwrap().secteur_d_activite = String::from_utf8_lossy(&item_field.value).to_string();}
(2, 5) => {carte_ps.situations.last_mut().unwrap().type_d_identification_structure = String::from_utf8_lossy(&item_field.value).to_string();}
(2, 6) => {carte_ps.situations.last_mut().unwrap().numero_d_identification_structure = String::from_utf8_lossy(&item_field.value).to_string();}
(2, 7) => {carte_ps.situations.last_mut().unwrap().cle_du_numero_d_identification_structure = String::from_utf8_lossy(&item_field.value).to_string();}
(2, 8) => {carte_ps.situations.last_mut().unwrap().raison_sociale_structure = String::from_utf8_lossy(&item_field.value).to_string();}
(2, 9) => {carte_ps.situations.last_mut().unwrap().numero_d_identification_de_facturation_du_ps = String::from_utf8_lossy(&item_field.value).to_string();}
(2, 10) => {carte_ps.situations.last_mut().unwrap().cle_du_numero_d_identification_de_facturation_du_ps = String::from_utf8_lossy(&item_field.value).to_string();}
(2, 11) => {carte_ps.situations.last_mut().unwrap().numero_d_identification_du_ps_remplaçant = String::from_utf8_lossy(&item_field.value).to_string();}
(2, 12) => {carte_ps.situations.last_mut().unwrap().cle_du_numero_d_identification_du_ps_remplaçant = String::from_utf8_lossy(&item_field.value).to_string();}
(2, 13) => {carte_ps.situations.last_mut().unwrap().code_conventionnel = String::from_utf8_lossy(&item_field.value).to_string();}
(2, 14) => {carte_ps.situations.last_mut().unwrap().code_specialite = String::from_utf8_lossy(&item_field.value).to_string();}
(2, 15) => {carte_ps.situations.last_mut().unwrap().code_zone_tarifaire = String::from_utf8_lossy(&item_field.value).to_string();}
(2, 16) => {carte_ps.situations.last_mut().unwrap().code_zone_ik = String::from_utf8_lossy(&item_field.value).to_string();}
(2, 17) => {carte_ps.situations.last_mut().unwrap().code_agrement_1 = String::from_utf8_lossy(&item_field.value).to_string();}
(2, 18) => {carte_ps.situations.last_mut().unwrap().code_agrement_2 = String::from_utf8_lossy(&item_field.value).to_string();}
(2, 19) => {carte_ps.situations.last_mut().unwrap().code_agrement_3 = String::from_utf8_lossy(&item_field.value).to_string();}
(2, 20) => {carte_ps.situations.last_mut().unwrap().habilitation_à_signer_une_facture = String::from_utf8_lossy(&item_field.value).to_string();}
(2, 21) => {carte_ps.situations.last_mut().unwrap().habilitation_à_signer_un_lot = String::from_utf8_lossy(&item_field.value).to_string();}
_ => (),
}
}
}
carte_ps
}

View File

@ -1,9 +0,0 @@
[package]
name = "utils"
version = "0.1.0"
edition = "2021"
[dependencies]
anyhow = "1.0"
directories = "5.0"
dotenv = "0.15"

View File

@ -1,48 +0,0 @@
use std::{env, path::PathBuf};
use anyhow::{bail, Context, Result};
use directories::ProjectDirs;
use dotenv::from_path;
const CONFIG_FILE_NAME: &str = ".env";
pub fn get_config_dirs() -> Vec<PathBuf> {
let mut config_dirs = vec![
PathBuf::from(""), // Current directory
];
if let Ok(manifest_dir) = env::var("CARGO_MANIFEST_DIR") {
config_dirs.push(PathBuf::from(manifest_dir));
}
if let Some(proj_dirs) = ProjectDirs::from("org", "P4pillon", "Krys4lide") {
config_dirs.push(proj_dirs.config_dir().to_path_buf());
}
config_dirs
}
pub fn get_config_files() -> Result<Vec<PathBuf>> {
let config_dirs = get_config_dirs();
let mut config_files = Vec::new();
for config_dir in config_dirs.iter() {
let config_file = config_dir.join(CONFIG_FILE_NAME);
if config_file.exists() {
config_files.push(config_file);
}
}
if config_files.is_empty() {
bail!(
"No config file {CONFIG_FILE_NAME} found in the following directories: {config_dirs:#?}"
);
}
Ok(config_files)
}
pub fn load_config() -> Result<()> {
let config_files = get_config_files()?;
// Load the first config file found
// TODO: add a verbose log to list all config files found
println!(
"DEBUG: Config files found (1st loaded): {:#?}",
config_files
);
from_path(config_files[0].as_path()).context("Failed to load config file")
}

View File

@ -1 +0,0 @@
pub mod config;

View File

@ -1,85 +0,0 @@
# Gestion des erreurs
Ce document décrit comment les erreurs sont gérées dans le projet.
## Gestion native
Par principe, en Rust, on évite au maximum la gestion par exception, ne la réservant qu'aux situations où un crash du programme est la meilleure solution.
En temps normal, on renvoie des `Result<Valeur, Erreur>` (pour les situations réussite/erreur) ou des `Option<Valeur>` (pour les situations valeur non-nulle/nulle).
Quand on fait face à une situation d'erreur, on cherchera à la gérer de manière explicite (voir [Récupération des erreurs](#récupération-des-erreurs)) ou à la remonter à un niveau supérieur, généralement à l'aide de l'opérateur `?`.
On évitera, par contre, au maximum de générer des exceptions (appelées "panics" en Rust), que ce soit par l'usage de `panic!` ou par des appels à des fonctions qui paniquent en cas d'erreur (comme `unwrap` ou `expect`).
De nombreux exemples des idiomes natifs de gestion des erreurs en Rust sont disponibles dans la documentation [Rust by example](https://doc.rust-lang.org/rust-by-example/error.html).
## Librairies de gestion des erreurs
Deux librairies sont utilisées pour gérer les erreurs dans le projet :
- [`anyhow`](https://docs.rs/anyhow/latest/anyhow/) : qui permet de renvoyer des erreurs faiblement typées, mais très facile à enrichir avec des messages d'explication. On l'utilise pour communiquer facilement des erreurs de haut niveau avec un utilisateur final.
```rust
use anyhow::{anyhow, Result};
fn get_cluster_info() -> Result<ClusterInfo> {
let data = fs::read_to_string("cluster.json")
.with_context(|| "failed to read cluster config")?;
let info: ClusterInfo = serde_json::from_str(&data)
.with_context(|| "failed to parse cluster config")?;
Ok(info)
}
```
- [`thiserror`](https://docs.rs/thiserror/latest/thiserror/) : qui fournit des macros pour définir des erreurs fortement typées. On l'utilise pour définir des erreurs spécifiques à une partie du code, contextualisées avec des données structurées plutôt que de simples messages d'erreurs, afin de favoriser la "récupération" face aux erreurs.
```rust
use thiserror::Error;
#[derive(Error, Debug)]
pub enum DataStoreError {
#[error("data store disconnected")]
Disconnect(#[from] io::Error),
#[error("the data for key `{0}` is not available")]
Redaction(String),
#[error("invalid header (expected {expected:?}, found {found:?})")]
InvalidHeader {
expected: String,
found: String,
},
#[error("unknown data store error")]
Unknown,
}
```
## Récupération des erreurs
Dans la mesure du possible, on essaie de privilégier la "récupération" face à une erreur plutôt que le "crash". Les stratégies de récupération sont :
- Réessayer l'opération, tel quel ou avec des paramètres différents
- Contourner l'opération, en la remplaçant par une autre
- À défaut, informer l'utilisateur de l'erreur et :
- arrêter / annuler l'opération en cours
- ignorer l'erreur et continuer l'exécution
Quand on ne peut pas récupérer une erreur, on la remonte à un niveau supérieur, si besoin en la convertissant dans un type d'erreur plus générique et approprié au niveau considéré.
## Conversion des erreurs
Quand on remonte une erreur à un niveau supérieur, on peut être amené à la convertir dans un type d'erreur plus générique et approprié au niveau considéré. Pour faciliter cette conversion, on implémente le trait `From`. Avec `thiserror`, on peut utiliser l'attribut `#[from]` ou le paramètre `source` pour automatiser l'implémentation de telles conversions.
On peut ensuite, lors de la gestion d'une erreur, on pourra :
- soit directement renvoyer l'erreur à l'aide de l'opérateur `?`, qui se chargera de la conversion ;
- soit convertir l'erreur explicitement, par exemple en utilisant la méthode `map_err` sur un `Result`, en particulier quand on veut enrichir l'erreur avec des informations supplémentaires.
## Usages exceptionnels de `unwrap` et `expect`
Provoquant des "panics" en cas d'erreur, les fonctions `unwrap` et `expect` ne doivent être utilisées que dans des cas exceptionnels :
- Dans les tests, pour signaler une erreur de test
- Au plus haut niveau de l'application, pour signaler une erreur fatale qui ne peut pas être récupérée
- Dans des situations où l'erreur ne peut pas se produire, par exemple après une vérification de préconditions
Dans l'idéal, on préférera l'usage de `expect` à `unwrap`, car il permet de donner un message d'erreur explicite.
## Ressources
- [The Rust Programming Language - Ch. 9: Error Handling](https://doc.rust-lang.org/book/ch09-00-error-handling.html)
- [The NRC Book - Error Handling in Rust](https://nrc.github.io/error-docs/intro.html)
- [The Error Design Patterns Book - by Project Error Handling WG](https://github.com/rust-lang/project-error-handling/blob/master/error-design-patterns-book/src/SUMMARY.md)
- [The Rust Cookbook - Error Handling](https://rust-lang-nursery.github.io/rust-cookbook/error-handling.html)
- [Le ticket initial de l'intégration de la gestion des erreurs dans le projet](https://forge.p4pillon.org/P4Pillon/Krys4lide/issues/34)

24
frontend/.gitignore vendored
View File

@ -1,24 +0,0 @@
# Nuxt dev/build outputs
.output
.data
.nuxt
.nitro
.cache
dist
# Node dependencies
node_modules
# Logs
logs
*.log
# Misc
.DS_Store
.fleet
.idea
# Local env files
.env
.env.*
!.env.example

View File

@ -1,77 +0,0 @@
# Nuxt 3 Minimal Starter
TODO : Faire un vrai README pour `frontend` (Nuxt 3)
Look at the [Nuxt 3 documentation](https://nuxt.com/docs/getting-started/introduction) to learn more.
## Setup
Make sure to install the dependencies:
```bash
# npm
npm install
# pnpm
pnpm install
# yarn
yarn install
# bun
bun install
```
## Development Server
Start the development server on `http://localhost:3000`:
```bash
# npm
npm run dev
# pnpm
pnpm run dev
# yarn
yarn dev
# bun
bun run dev
```
## Production
Build the application for production:
```bash
# npm
npm run build
# pnpm
pnpm run build
# yarn
yarn build
# bun
bun run build
```
Locally preview production build:
```bash
# npm
npm run preview
# pnpm
pnpm run preview
# yarn
yarn preview
# bun
bun run preview
```
Check out the [deployment documentation](https://nuxt.com/docs/getting-started/deployment) for more information.

View File

@ -1,7 +0,0 @@
<template>
<div>
<NuxtRouteAnnouncer />
<NavBar />
<NuxtPage />
</div>
</template>

View File

@ -1,43 +0,0 @@
<!--
This component is used to show a loading spinner when the SPA is loading.
Source: https://github.com/barelyhuman/snips/blob/dev/pages/css-loader.md
-->
<div class="loader"></div>
<style>
.loader {
display: block;
position: fixed;
z-index: 1031;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 18px;
height: 18px;
box-sizing: border-box;
border: solid 2px transparent;
border-top-color: #000;
border-left-color: #000;
border-bottom-color: #efefef;
border-right-color: #efefef;
border-radius: 50%;
-webkit-animation: loader 400ms linear infinite;
animation: loader 400ms linear infinite;
}
\@-webkit-keyframes loader {
0% {
-webkit-transform: translate(-50%, -50%) rotate(0deg);
}
100% {
-webkit-transform: translate(-50%, -50%) rotate(360deg);
}
}
\@keyframes loader {
0% {
transform: translate(-50%, -50%) rotate(0deg);
}
100% {
transform: translate(-50%, -50%) rotate(360deg);
}
}
</style>

Binary file not shown.

View File

@ -1,22 +0,0 @@
<template>
<div class="avatar">
<div class="rounded-full">
<img :src="getAvatarUrl(user)" />
</div>
</div>
</template>
<script setup lang="ts">
import type { User } from '~/types/user';
const props = defineProps<{
user: User,
}>();
const getAvatarUrl = (user: User) => {
if (user.avatar) {
return user.avatar;
}
return 'https://avatar.iran.liara.run/username?username=' + user.name;
};
</script>

View File

@ -1,71 +0,0 @@
<template>
<dialog
id="login_modal"
ref="login_modal"
@cancel.prevent=""
@keyup="handleKeyPress"
class="modal"
>
<div class="modal-box">
<h3 class="text-3xl text-center mb-6">Connexion</h3>
<div class="flex flex-wrap gap-5 justify-center">
<template v-for="(user, index) in users" :key="user.id">
<LoginModalAvatar
:user="user"
:rank="index+1"
@selectUser="login"
/>
</template>
</div>
</div>
<form method="dialog" class="modal-backdrop">
<button class="cursor-default">close</button>
</form>
</dialog>
</template>
<script setup lang="ts">
import type { User } from '~/types/user';
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: '' },
];
const loginModal = useTemplateRef('login_modal');
const current_user = useCurrentUser();
const login = (user: User) => {
console.log('login', user);
current_user.value = user;
loginModal.value?.close();
};
const handleKeyPress = (event: KeyboardEvent) => {
// Extract the rank from the event.code : Digit7 -> 7
const rank = event.code.match(/\d/);
if (!rank) {
console.debug('Not handled key event', { event });
return;
}
const user = getUserByRank(parseInt(rank[0]));
if (user) {
login(user);
} else {
console.debug('Not handled key event', { event });
}
};
const getUserByRank = (rank: number): User => {
return users[rank - 1];
};
</script>

View File

@ -1,17 +0,0 @@
<template>
<button class="relative" @click="$emit('selectUser', user)">
<Avatar class="w-24" :user="user" />
<div class="absolute w-fit mx-auto bottom-0 inset-x-0">
<kbd class="kbd kbd-sm">{{ rank }}</kbd>
</div>
</button>
</template>
<script setup lang="ts">
import type { User } from '~/types/user';
const props = defineProps<{
user: User,
rank: Number,
}>();
</script>

View File

@ -1,35 +0,0 @@
<template>
<div class="navbar">
<div class="navbar-start">
<a class="btn btn-ghost text-xl" href="/">Chrys4lide</a>
</div>
<nav class="navbar-center">
<NuxtLink to="/" class="btn btn-ghost">Accueil</NuxtLink>
<NuxtLink to="/CPS" class="btn btn-ghost">Carte CPS</NuxtLink>
</nav>
<div class="navbar-end">
<template v-if="!current_user">
<button class="btn btn-ghost" type="button" onclick="login_modal.showModal()">
Connexion
</button>
</template>
<template v-else>
<details class="dropdown dropdown-end">
<summary class="block"><Avatar :user="current_user" class="w-12" role="button" /></summary>
<ul class="menu dropdown-content bg-base-100 rounded-box z-[1] w-52 p-2 shadow">
<li><a @click="logout">Déconnexion</a></li>
</ul>
</details>
</template>
</div>
</div>
<LoginModal />
</template>
<script setup lang="ts">
const current_user = useCurrentUser();
const logout = () => {
current_user.value = null;
};
</script>

View File

@ -1,3 +0,0 @@
import type { User } from '@/types/user';
export const useCurrentUser = () => useState<User | null>('currentUser', () => null);

View File

@ -1,39 +0,0 @@
// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
compatibilityDate: '2024-04-03',
// Enables the development server to be discoverable by other devices for mobile development
devServer: { host: '0.0.0.0', port: 1420 },
devtools: { enabled: true },
modules: [
'@nuxtjs/tailwindcss',
'@nuxtjs/color-mode',
],
// Disable SSR for Tauri
ssr: false,
vite: {
// Better support for Tauri CLI output
clearScreen: false,
// Enable environment variables
// Additional environment variables can be found at
// https://v2.tauri.app/reference/environment-variables/
envPrefix: ['VITE_', 'TAURI_'],
server: {
// Tauri requires a consistent port
strictPort: true,
hmr: {
// Use websocket for mobile hot reloading
protocol: 'ws',
// Make sure it's available on the network
host: '0.0.0.0',
// Use a specific port for hmr
port: 5183,
},
},
},
colorMode: {
// Add `data-theme` attribute to the `html` tag, allowing DaisyUI to handle dark mode automatically
dataValue: 'theme',
// Remove the default `-mode` suffix from the class name, letting have `dark` and `light` as class names, for DaisyUI compatibility
classSuffix: '',
},
})

View File

@ -1,22 +0,0 @@
{
"name": "nuxt-app",
"private": true,
"type": "module",
"scripts": {
"build": "nuxi build",
"dev": "nuxi dev",
"generate": "nuxi generate",
"preview": "nuxi preview",
"postinstall": "nuxi prepare"
},
"dependencies": {
"@nuxtjs/color-mode": "^3.5.1",
"daisyui": "^4.12.10",
"nuxt": "^3.13.0",
"vue": "latest",
"vue-router": "latest"
},
"devDependencies": {
"@nuxtjs/tailwindcss": "^6.12.1"
}
}

View File

@ -1,8 +0,0 @@
<template>
<div>
<h1 class="text-xl">Carte CPS</h1>
</div>
</template>
<script setup>
</script>

View File

@ -1,17 +0,0 @@
<template>
<div>
<h1 class="text-xl">Welcome to your {{ appName }}!</h1>
<p v-if="current_user">Logged in as {{ current_user.name }}</p>
</div>
</template>
<script setup lang="ts">
const current_user = useCurrentUser();
const appName = 'Nuxt App';
</script>
<style scoped>
h1 {
color: #42b983;
}
</style>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

View File

@ -1 +0,0 @@

View File

@ -1,3 +0,0 @@
{
"extends": "../.nuxt/tsconfig.server.json"
}

View File

@ -1,7 +0,0 @@
import type { Config } from 'tailwindcss'
export default <Partial<Config>>{
plugins: [
require('daisyui'),
],
}

View File

@ -1,4 +0,0 @@
{
// https://nuxt.com/docs/guide/concepts/typescript
"extends": "./.nuxt/tsconfig.json"
}

View File

@ -1,6 +0,0 @@
export declare interface User {
id: number;
name: string;
email?: string;
avatar?: string;
}