Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ como fallaría Linux sin el servicio y dice en su nombre qué falta.
| `mlock` del secreto | `mlock`; `MADV_DONTDUMP` es solo de Linux. |
| Cerrojos (Firefox, carpeta de paso) | Los de Unix, sin cambios. |
| Llavero `oo7` (ADR-0034) | `PendingMacosKeychain`: responde que no hay llavero. |
| Manejadores de `afirma://` por GIO | `PendingMacosLaunchServices`: la lista no está disponible y elegir falla. |
| Diálogo GTK de fallo de arranque | El texto sale por la salida de error. |

La firma trifásica (ADR-0001) funciona de extremo a extremo con una clave en memoria: PAdES,
Expand Down
4 changes: 2 additions & 2 deletions rfirma-app/src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -228,8 +228,8 @@ openssl = { version = "0.10", features = ["vendored"] }
tokio-rustls = { version = "0.26", default-features = false, features = ["ring", "tls12"] }
block2 = "0.6"
objc2 = "0.6"
objc2-app-kit = { version = "0.3", default-features = false, features = ["std", "block2", "NSAlert", "NSApplication", "NSButton", "NSControl", "NSResponder", "NSSecureTextField", "NSTextField", "NSView", "NSWindow"] }
objc2-foundation = { version = "0.3", default-features = false, features = ["std", "NSGeometry", "NSString"] }
objc2-app-kit = { version = "0.3", default-features = false, features = ["std", "block2", "NSAlert", "NSApplication", "NSButton", "NSControl", "NSResponder", "NSSecureTextField", "NSTextField", "NSView", "NSWindow", "NSWorkspace"] }
objc2-foundation = { version = "0.3", default-features = false, features = ["std", "NSArray", "NSError", "NSFileManager", "NSGeometry", "NSString", "NSURL"] }
# El Almacén de macOS: las identidades del llavero por Security.framework (ADR-0040).
security-framework = "3.7"
security-framework-sys = "2.17"
Expand Down
43 changes: 7 additions & 36 deletions rfirma-app/src-tauri/src/desktop/adapters/registry.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
//! `DesktopRegistry`, el registro de manejadores detrás del puerto `HandlerRegistry`: canal, GIO y `mimeapps.list` en Linux, el registro en Windows y Launch Services, pendiente, en macOS.
//! `DesktopRegistry`, el registro de manejadores detrás del puerto `HandlerRegistry`: canal, GIO y `mimeapps.list` en Linux, el registro en Windows y LaunchServices en macOS.

use std::path::PathBuf;

#[cfg(target_os = "macos")]
pub mod launch_services;
#[cfg(windows)]
pub mod windows_classes;

Expand All @@ -12,7 +14,7 @@ use crate::desktop::adapters::choice::{
choose_handler_for_scheme, current_default_for_scheme, remove_handler_for_scheme,
};
use crate::desktop::domain::error::DesktopError;
#[cfg(not(windows))]
#[cfg(target_os = "linux")]
use crate::desktop::domain::error::Situation;
use crate::desktop::domain::handlers::UrlHandler;
use crate::desktop::ports::HandlerRegistry;
Expand Down Expand Up @@ -88,45 +90,14 @@ pub fn this_desktop_to_write() -> Result<Box<dyn HandlerRegistry>, DesktopError>
Ok(this_desktop())
}

/// Quién abre `afirma://` en este escritorio, para leerlo: Launch Services, aún pendiente.
/// Quién abre `afirma://` en este escritorio, para leerlo: LaunchServices.
#[cfg(target_os = "macos")]
pub fn this_desktop() -> Box<dyn HandlerRegistry> {
Box::new(PendingMacosLaunchServices)
Box::new(launch_services::of_this_app())
}

/// Quién abre `afirma://` en este escritorio, para escribirlo: Launch Services, aún pendiente.
/// Quién abre `afirma://` en este escritorio, para escribirlo: LaunchServices.
#[cfg(target_os = "macos")]
pub fn this_desktop_to_write() -> Result<Box<dyn HandlerRegistry>, DesktopError> {
Ok(this_desktop())
}

/// El registro de manejadores sobre Launch Services de macOS, que aún no existe: ni se consulta ni se escribe.
#[cfg(target_os = "macos")]
pub struct PendingMacosLaunchServices;

#[cfg(target_os = "macos")]
impl HandlerRegistry for PendingMacosLaunchServices {
fn registered_for(&self, _scheme: &str) -> Option<Vec<UrlHandler>> {
None
}

fn current_default_for(&self, _scheme: &str) -> Option<String> {
None
}

fn choose_for(&self, _scheme: &str, _handler: &str) -> Result<(), DesktopError> {
Err(pending_macos_launch_services())
}

fn remove_for(&self, _scheme: &str) -> Result<(), DesktopError> {
Err(pending_macos_launch_services())
}
}

#[cfg(target_os = "macos")]
fn pending_macos_launch_services() -> DesktopError {
DesktopError::new(
Situation::TheListIsNotWritable,
"el registro de manejadores de macOS aún no está disponible",
)
}
145 changes: 145 additions & 0 deletions rfirma-app/src-tauri/src/desktop/adapters/registry/launch_services.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
//! Quién abre un esquema de URL en macOS, según LaunchServices: lo lee siempre y solo lo escribe cuando la persona elige (ADR-0040).

pub mod ns_workspace;

use std::path::{Path, PathBuf};

use crate::desktop::domain::error::{DesktopError, Situation};
use crate::desktop::domain::handlers::{UrlHandler, OUR_DESKTOP_FILE};
use crate::desktop::ports::HandlerRegistry;

/// Una aplicación que declara el esquema.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct InstalledApp {
pub path: PathBuf,
pub name: String,
}

/// Lo que LaunchServices sabe de un esquema y cómo se le cambia la aplicación.
pub trait Workspace {
/// Las aplicaciones que declaran el esquema.
fn applications_for(&self, scheme: &str) -> Vec<InstalledApp>;

/// La aplicación que lo abre hoy.
fn default_for(&self, scheme: &str) -> Option<PathBuf>;

/// Deja esa aplicación como la que lo abre.
fn set_default_for(&self, scheme: &str, app: &Path) -> Result<(), String>;
}

/// El registro de manejadores sobre LaunchServices, con el `.app` que es rFirma.
pub struct LaunchServices<W> {
workspace: W,
ours: PathBuf,
}

impl<W: Workspace> LaunchServices<W> {
/// El registro sobre `workspace`, con `ours` como rFirma.
pub fn new(workspace: W, ours: PathBuf) -> Self {
Self { workspace, ours }
}

fn is_ours(&self, app: &Path) -> bool {
same_app(app, &self.ours)
}

fn id_of(&self, app: &Path) -> String {
if self.is_ours(app) {
return OUR_DESKTOP_FILE.to_owned();
}
app.display().to_string()
}

fn app_of(&self, scheme: &str, handler: &str) -> Option<PathBuf> {
if handler == OUR_DESKTOP_FILE {
return Some(self.ours.clone());
}
self.workspace
.applications_for(scheme)
.into_iter()
.map(|app| app.path)
.find(|path| self.id_of(path) == handler)
}

fn set_default(&self, scheme: &str, app: &Path) -> Result<(), DesktopError> {
self.workspace
.set_default_for(scheme, app)
.map_err(|reason| DesktopError::new(Situation::TheListIsNotWritable, reason))
}
}

impl<W: Workspace> HandlerRegistry for LaunchServices<W> {
fn registered_for(&self, scheme: &str) -> Option<Vec<UrlHandler>> {
let mut handlers: Vec<UrlHandler> = Vec::new();
for app in self.workspace.applications_for(scheme) {
let id = self.id_of(&app.path);
if !handlers.iter().any(|already| already.id == id) {
handlers.push(UrlHandler { id, name: app.name });
}
}
Some(handlers)
}

fn current_default_for(&self, scheme: &str) -> Option<String> {
self.workspace
.default_for(scheme)
.map(|app| self.id_of(&app))
}

fn choose_for(&self, scheme: &str, handler: &str) -> Result<(), DesktopError> {
let app = self.app_of(scheme, handler).ok_or_else(|| {
DesktopError::new(
Situation::TheListIsNotWritable,
format!("«{handler}» no declara {scheme}://"),
)
})?;
self.set_default(scheme, &app)
}

/// LaunchServices no deja un esquema sin aplicación: si lo abre rFirma, pasa a la siguiente que lo declare.
fn remove_for(&self, scheme: &str) -> Result<(), DesktopError> {
if !self
.workspace
.default_for(scheme)
.is_some_and(|app| self.is_ours(&app))
{
return Ok(());
}
match self
.workspace
.applications_for(scheme)
.into_iter()
.find(|app| !self.is_ours(&app.path))
{
Some(other) => self.set_default(scheme, &other.path),
None => Ok(()),
}
}
}

fn same_app(app: &Path, ours: &Path) -> bool {
let canonical = |path: &Path| path.canonicalize().unwrap_or_else(|_| path.to_path_buf());
!ours.as_os_str().is_empty() && canonical(app) == canonical(ours)
}

/// El LaunchServices de la sesión, con el `.app` que está corriendo como rFirma.
pub fn of_this_app() -> LaunchServices<ns_workspace::SystemWorkspace> {
let executable = std::env::current_exe().unwrap_or_default();
LaunchServices::new(ns_workspace::SystemWorkspace, bundle_of(&executable))
}

/// El `.app` que contiene el ejecutable o, fuera de uno, el ejecutable.
pub fn bundle_of(executable: &Path) -> PathBuf {
executable
.ancestors()
.find(|folder| {
folder
.extension()
.is_some_and(|extension| extension == "app")
})
.unwrap_or(executable)
.to_path_buf()
}

#[cfg(test)]
mod tests;
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
//! `SystemWorkspace`, el `NSWorkspace` de AppKit detrás de `Workspace`: toca LaunchServices de verdad y se prueba a mano.

use std::path::{Path, PathBuf};
use std::sync::mpsc;
use std::time::Duration;

use block2::RcBlock;
use objc2::rc::{autoreleasepool, Retained};
use objc2_app_kit::NSWorkspace;
use objc2_foundation::{NSError, NSFileManager, NSString, NSURL};

use super::{InstalledApp, Workspace};

const THE_PERSON_CONFIRMS_WITHIN: Duration = Duration::from_secs(120);

/// El LaunchServices de la sesión de la persona.
pub struct SystemWorkspace;

impl Workspace for SystemWorkspace {
fn applications_for(&self, scheme: &str) -> Vec<InstalledApp> {
autoreleasepool(|_| {
let Some(url) = url_of(scheme) else {
return Vec::new();
};
let files = NSFileManager::defaultManager();
NSWorkspace::sharedWorkspace()
.URLsForApplicationsToOpenURL(&url)
.iter()
.filter_map(|app| {
let path = app.path()?;
Some(InstalledApp {
name: files.displayNameAtPath(&path).to_string(),
path: PathBuf::from(path.to_string()),
})
})
.collect()
})
}

fn default_for(&self, scheme: &str) -> Option<PathBuf> {
autoreleasepool(|_| {
let url = url_of(scheme)?;
let app = NSWorkspace::sharedWorkspace().URLForApplicationToOpenURL(&url)?;
Some(PathBuf::from(app.path()?.to_string()))
})
}

fn set_default_for(&self, scheme: &str, app: &Path) -> Result<(), String> {
let (sender, answer) = mpsc::channel();
let done = RcBlock::new(move |error: *mut NSError| {
// SAFETY: AppKit pasa un `NSError` vivo mientras dura la llamada, o nulo si fue bien.
let outcome = unsafe { error.as_ref() }.map_or(Ok(()), |error| {
Err(error.localizedDescription().to_string())
});
let _ = sender.send(outcome);
});
autoreleasepool(|_| {
let app = NSURL::fileURLWithPath(&NSString::from_str(&app.to_string_lossy()));
NSWorkspace::sharedWorkspace()
.setDefaultApplicationAtURL_toOpenURLsWithScheme_completionHandler(
&app,
&NSString::from_str(scheme),
Some(&done),
);
});
answer
.recv_timeout(THE_PERSON_CONFIRMS_WITHIN)
.map_err(|_| format!("macOS no ha confirmado quién abre {scheme}://"))?
}
}

fn url_of(scheme: &str) -> Option<Retained<NSURL>> {
NSURL::URLWithString(&NSString::from_str(&format!("{scheme}://")))
}
Loading
Loading