use crate::errors::{SageError, SageResult};
use std::collections::BTreeSet;
use winreg::enums::*;
use winreg::RegKey;
use winreg::HKEY;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum RegistryView {
Native,
Registry32,
Registry64,
}
impl RegistryView {
fn sam_flags(self) -> u32 {
match self {
RegistryView::Native => KEY_READ,
RegistryView::Registry32 => KEY_READ | KEY_WOW64_32KEY,
RegistryView::Registry64 => KEY_READ | KEY_WOW64_64KEY,
}
}
fn display_name(self) -> &'static str {
match self {
RegistryView::Native => "vue native",
RegistryView::Registry32 => "vue 32 bits",
RegistryView::Registry64 => "vue 64 bits",
}
}
}
#[derive(Debug, Clone, Copy)]
struct RegistryLocation {
hive: HKEY,
sub_path: &'static str,
view: RegistryView,
display_name: &'static str,
}
const REGISTRY_LOCATIONS: [RegistryLocation; 7] = [
RegistryLocation {
hive: HKEY_CLASSES_ROOT,
sub_path: "",
view: RegistryView::Native,
display_name: "HKEY_CLASSES_ROOT",
},
RegistryLocation {
hive: HKEY_CURRENT_USER,
sub_path: "Software\\Classes",
view: RegistryView::Native,
display_name: "HKEY_CURRENT_USER\\Software\\Classes",
},
RegistryLocation {
hive: HKEY_CURRENT_USER,
sub_path: "Software\\Classes",
view: RegistryView::Registry32,
display_name: "HKEY_CURRENT_USER\\Software\\Classes",
},
RegistryLocation {
hive: HKEY_CURRENT_USER,
sub_path: "Software\\Classes",
view: RegistryView::Registry64,
display_name: "HKEY_CURRENT_USER\\Software\\Classes",
},
RegistryLocation {
hive: HKEY_LOCAL_MACHINE,
sub_path: "SOFTWARE\\Classes",
view: RegistryView::Native,
display_name: "HKEY_LOCAL_MACHINE\\SOFTWARE\\Classes",
},
RegistryLocation {
hive: HKEY_LOCAL_MACHINE,
sub_path: "SOFTWARE\\Classes",
view: RegistryView::Registry32,
display_name: "HKEY_LOCAL_MACHINE\\SOFTWARE\\Classes",
},
RegistryLocation {
hive: HKEY_LOCAL_MACHINE,
sub_path: "SOFTWARE\\Classes",
view: RegistryView::Registry64,
display_name: "HKEY_LOCAL_MACHINE\\SOFTWARE\\Classes",
},
];
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SageModule {
Comptabilite,
Commercial,
}
impl SageModule {
fn legacy_prog_id(&self) -> &'static str {
match self {
SageModule::Comptabilite => "Objets100c.Cpta.Stream",
SageModule::Commercial => "Objets100c.Cial.Stream",
}
}
fn legacy_version_prefix(&self) -> &'static str {
match self {
SageModule::Comptabilite => "Objets100c.Cpta.Stream.",
SageModule::Commercial => "Objets100c.Cial.Stream.",
}
}
fn modern_prog_id_prefix(&self) -> &'static str {
match self {
SageModule::Comptabilite => "Sage.BSCpta.Application.",
SageModule::Commercial => "Sage.BSCial.Application.",
}
}
fn display_name(&self) -> &'static str {
match self {
SageModule::Comptabilite => "Sage Comptabilité",
SageModule::Commercial => "Sage Commercial",
}
}
fn matches_prog_id(&self, prog_id: &str) -> bool {
prog_id == self.legacy_prog_id()
|| prog_id.starts_with(self.legacy_version_prefix())
|| prog_id.starts_with(self.modern_prog_id_prefix())
}
}
fn open_registry_root(location: RegistryLocation) -> Option<RegKey> {
let root = RegKey::predef(location.hive);
if location.sub_path.is_empty() {
return Some(root);
}
root.open_subkey_with_flags(location.sub_path, location.view.sam_flags())
.ok()
}
fn registry_locations_description() -> String {
REGISTRY_LOCATIONS
.iter()
.map(|location| {
format!(
"{} ({})",
location.display_name,
location.view.display_name()
)
})
.collect::<Vec<_>>()
.join(", ")
}
fn lookup_subkey_value(subkey_path: &str) -> Option<String> {
for location in REGISTRY_LOCATIONS {
let Some(root) = open_registry_root(location) else {
continue;
};
let Ok(key) = root.open_subkey_with_flags(subkey_path, KEY_READ) else {
continue;
};
if let Ok(value) = key.get_value::<String, _>("") {
return Some(value);
}
}
None
}
fn lookup_prog_id_clsid(prog_id: &str) -> Option<String> {
let clsid_key_path = format!("{}\\CLSID", prog_id);
lookup_subkey_value(&clsid_key_path).map(|clsid| normalize_clsid(&clsid))
}
fn lookup_prog_id_curver(prog_id: &str) -> Option<String> {
let curver_key_path = format!("{}\\CurVer", prog_id);
lookup_subkey_value(&curver_key_path)
.map(|value| value.trim().to_string())
.filter(|value| !value.is_empty())
}
fn normalize_clsid(clsid: &str) -> String {
clsid.trim_matches(|c| c == '{' || c == '}').to_string()
}
fn resolve_prog_id_with_lookup<F, G>(
prog_id: &str,
lookup_clsid: &F,
lookup_curver: &G,
) -> Option<String>
where
F: Fn(&str) -> Option<String>,
G: Fn(&str) -> Option<String>,
{
if let Some(clsid) = lookup_clsid(prog_id) {
return Some(clsid);
}
if let Some(curver) = lookup_curver(prog_id) {
if curver != prog_id {
if let Some(clsid) = resolve_prog_id_with_lookup(&curver, lookup_clsid, lookup_curver) {
return Some(clsid);
}
}
}
if !prog_id.ends_with(".1") {
let versioned_prog_id = format!("{}.1", prog_id);
if let Some(clsid) = lookup_clsid(&versioned_prog_id) {
return Some(clsid);
}
}
None
}
fn prog_id_rank(module: SageModule, prog_id: &str) -> (u8, i32, String) {
if prog_id.starts_with(module.modern_prog_id_prefix()) {
let version = prog_id.trim_start_matches(module.modern_prog_id_prefix());
return (0, parse_version_number(version), version.to_string());
}
if prog_id == module.legacy_prog_id() {
return (1, i32::MAX, String::new());
}
if prog_id.starts_with(module.legacy_version_prefix()) {
let version = prog_id.trim_start_matches(module.legacy_version_prefix());
return (2, parse_version_number(version), version.to_string());
}
(3, -1, prog_id.to_string())
}
fn parse_version_number(version: &str) -> i32 {
let numeric_part: String = version
.chars()
.take_while(|character| character.is_ascii_digit())
.collect();
numeric_part.parse::<i32>().unwrap_or(-1)
}
pub fn resolve_prog_id(prog_id: &str) -> SageResult<String> {
resolve_prog_id_with_lookup(prog_id, &lookup_prog_id_clsid, &lookup_prog_id_curver).ok_or_else(
|| {
SageError::RegistryError(format!(
"ProgID '{}' introuvable dans le registre (recherché dans {}).",
prog_id,
registry_locations_description()
))
},
)
}
pub fn is_clsid_registered(clsid: &str) -> SageResult<bool> {
let clsid_formatted = format!("{{{}}}", normalize_clsid(clsid));
let clsid_key_path = format!("CLSID\\{}", clsid_formatted);
let mut clsid_found = false;
for location in REGISTRY_LOCATIONS {
let Some(root) = open_registry_root(location) else {
continue;
};
let Ok(clsid_key) = root.open_subkey_with_flags(&clsid_key_path, KEY_READ) else {
continue;
};
clsid_found = true;
let has_inproc = clsid_key.open_subkey("InprocServer32").is_ok();
let has_local = clsid_key.open_subkey("LocalServer32").is_ok();
if has_inproc || has_local {
return Ok(true);
}
}
if clsid_found {
Ok(false)
} else {
Err(SageError::RegistryError(format!(
"CLSID '{}' non trouvé dans le registre (recherché dans {}).",
clsid,
registry_locations_description()
)))
}
}
fn find_module_versions(module: SageModule) -> SageResult<Vec<(String, String)>> {
let mut prog_ids = BTreeSet::new();
let mut versions = Vec::new();
prog_ids.insert(module.legacy_prog_id().to_string());
if let Some(curver) = lookup_prog_id_curver(module.legacy_prog_id()) {
prog_ids.insert(curver);
}
for location in REGISTRY_LOCATIONS {
let Some(root) = open_registry_root(location) else {
continue;
};
for key_name in root.enum_keys().flatten() {
if module.matches_prog_id(&key_name) {
prog_ids.insert(key_name);
}
}
}
for prog_id in prog_ids {
if let Ok(clsid) = resolve_prog_id(&prog_id) {
if matches!(is_clsid_registered(&clsid), Ok(true)) {
versions.push((prog_id, clsid));
}
}
}
if versions.is_empty() {
return Err(SageError::RegistryError(format!(
"Aucune version de {} avec serveur COM enregistré trouvée dans le registre. \
Vérifiez que Sage 100c est installé et enregistré (regsvr32 objets100c.dll).",
module.display_name()
)));
}
versions.sort_by(|a, b| {
let rank_a = prog_id_rank(module, &a.0);
let rank_b = prog_id_rank(module, &b.0);
rank_a
.0
.cmp(&rank_b.0)
.then_with(|| rank_b.1.cmp(&rank_a.1))
.then_with(|| rank_b.2.cmp(&rank_a.2))
});
Ok(versions)
}
fn find_latest_module(module: SageModule) -> SageResult<(String, String)> {
let versions = find_module_versions(module)?;
versions.into_iter().next().ok_or_else(|| {
SageError::RegistryError(format!("Aucune version {} trouvée", module.display_name()))
})
}
pub fn find_bscpta_versions() -> SageResult<Vec<(String, String)>> {
find_module_versions(SageModule::Comptabilite)
}
pub fn find_latest_bscpta() -> SageResult<(String, String)> {
find_latest_module(SageModule::Comptabilite)
}
pub fn find_bscial_versions() -> SageResult<Vec<(String, String)>> {
find_module_versions(SageModule::Commercial)
}
pub fn find_latest_bscial() -> SageResult<(String, String)> {
find_latest_module(SageModule::Commercial)
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashMap;
#[test]
fn test_matches_legacy_and_versioned_prog_ids() {
assert!(SageModule::Comptabilite.matches_prog_id("Objets100c.Cpta.Stream"));
assert!(SageModule::Comptabilite.matches_prog_id("Objets100c.Cpta.Stream.1"));
assert!(SageModule::Comptabilite.matches_prog_id("Sage.BSCpta.Application.100c"));
assert!(!SageModule::Comptabilite.matches_prog_id("Objets100c.Cial.Stream"));
}
#[test]
fn test_resolve_prog_id_uses_curver_and_version_suffix() {
let clsids = HashMap::from([(
"Objets100c.Cpta.Stream.1".to_string(),
"{309DE0FB-9FB8-4F4E-8295-CC60C60DAA33}".to_string(),
)]);
let curvers = HashMap::from([(
"Objets100c.Cpta.Stream".to_string(),
"Objets100c.Cpta.Stream.1".to_string(),
)]);
let resolved = resolve_prog_id_with_lookup(
"Objets100c.Cpta.Stream",
&|prog_id| {
clsids
.get(prog_id)
.cloned()
.map(|value| normalize_clsid(&value))
},
&|prog_id| curvers.get(prog_id).cloned(),
);
assert_eq!(
resolved.as_deref(),
Some("309DE0FB-9FB8-4F4E-8295-CC60C60DAA33")
);
}
#[test]
fn test_prog_id_rank_prefers_modern_then_legacy_base_then_legacy_versioned() {
let modern = prog_id_rank(SageModule::Comptabilite, "Sage.BSCpta.Application.140");
let legacy_base = prog_id_rank(SageModule::Comptabilite, "Objets100c.Cpta.Stream");
let legacy_versioned = prog_id_rank(SageModule::Comptabilite, "Objets100c.Cpta.Stream.1");
assert!(modern < legacy_base);
assert!(legacy_base < legacy_versioned);
}
#[test]
fn test_find_bscpta_versions() {
match find_bscpta_versions() {
Ok(versions) => {
println!("✅ Versions BSCpta trouvées:");
for (prog_id, clsid) in versions {
println!(" - {} -> {}", prog_id, clsid);
}
}
Err(e) => println!("⚠️ Aucune version BSCpta trouvée: {}", e),
}
}
#[test]
fn test_find_latest_bscpta() {
match find_latest_bscpta() {
Ok((prog_id, clsid)) => {
println!("✅ Dernière version BSCpta: {} ({})", prog_id, clsid);
}
Err(e) => println!("⚠️ Erreur: {}", e),
}
}
#[test]
fn test_find_bscial_versions() {
match find_bscial_versions() {
Ok(versions) => {
println!("✅ Versions BSCial trouvées:");
for (prog_id, clsid) in versions {
println!(" - {} -> {}", prog_id, clsid);
}
}
Err(e) => println!("⚠️ Aucune version BSCial trouvée: {}", e),
}
}
#[test]
fn test_find_latest_bscial() {
match find_latest_bscial() {
Ok((prog_id, clsid)) => {
println!("✅ Dernière version BSCial: {} ({})", prog_id, clsid);
}
Err(e) => println!("⚠️ Erreur: {}", e),
}
}
#[test]
fn test_resolve_prog_id_bscpta() {
match resolve_prog_id("Sage.BSCpta.Application.100c") {
Ok(clsid) => {
println!("✅ CLSID BSCpta résolu: {}", clsid);
assert_eq!(clsid, "309DE0FB-9FB8-4F4E-8295-CC60C60DAA33");
}
Err(e) => println!(
"⚠️ ProgID BSCpta non trouvé (normal si Sage non installé): {}",
e
),
}
}
#[test]
fn test_resolve_prog_id_bscial() {
match resolve_prog_id("Sage.BSCial.Application.100c") {
Ok(clsid) => {
println!("✅ CLSID BSCial résolu: {}", clsid);
}
Err(e) => println!(
"⚠️ ProgID BSCial non trouvé (normal si Sage non installé): {}",
e
),
}
}
}