trace2e_core/traceability/infrastructure/
naming.rs

1//! Resource naming and identification system.
2//!
3//! This module defines the core resource identification and naming conventions used throughout
4//! the trace2e traceability system. It provides a unified way to represent and identify
5//! different types of computational resources including files, network streams, and processes.
6//!
7//! ## Resource Hierarchy
8//!
9//! **File Descriptors (Fd)**: Represent operating system file descriptors that can point to
10//! either files in the filesystem or network streams (sockets).
11//!
12//! **Files**: Filesystem resources identified by their path, supporting both absolute and
13//! relative path specifications.
14//!
15//! **Streams**: Network communication channels defined by local and peer socket addresses,
16//! supporting TCP connections, Unix domain sockets, and other network protocols.
17//!
18//! **Processes**: Running system processes identified by PID with additional metadata including
19//! start time and executable path for precise identification across process reuse.
20//!
21//! ## Resource Construction
22//!
23//! Resources can be constructed using dedicated factory methods that handle system queries
24//! and validation, or using mock variants for testing purposes.
25
26use std::{
27    collections::HashSet,
28    fmt::{Debug, Display},
29    net::SocketAddr,
30};
31
32use sysinfo::{Pid, System};
33
34use crate::traceability::error::TraceabilityError;
35
36/// Represents a file resource in the filesystem.
37///
38/// Files are identified by their path, which can be absolute or relative.
39/// The path is stored as provided without normalization to preserve
40/// the original specification for audit and debugging purposes.
41#[derive(Debug, Clone, Eq, PartialEq, Hash)]
42pub struct File {
43    /// Filesystem path to the file, as specified by the application
44    pub path: String,
45}
46
47/// Represents a network stream or socket connection.
48///
49/// Streams are bidirectional communication channels between two endpoints,
50/// typically used for TCP connections, Unix domain sockets, or other
51/// network protocols. Both endpoints must be specified to enable proper
52/// flow tracking and policy enforcement.
53#[derive(Debug, Clone, Eq, PartialEq, Hash)]
54pub struct Stream {
55    /// Local socket address (e.g., "127.0.0.1:8080")
56    pub local_socket: String,
57    /// Remote peer socket address (e.g., "192.168.1.100:9000")
58    pub peer_socket: String,
59}
60
61/// Represents a file descriptor that can point to either a file or a stream.
62///
63/// File descriptors are the operating system's handle for I/O operations.
64/// This enum distinguishes between filesystem-based I/O (files) and
65/// network-based I/O (streams) while maintaining a unified interface.
66#[derive(Debug, Clone, Eq, PartialEq, Hash)]
67pub enum Fd {
68    /// File descriptor pointing to a filesystem file
69    File(File),
70    /// File descriptor pointing to a network stream or socket
71    Stream(Stream),
72}
73
74/// Represents a running system process with identifying metadata.
75///
76/// Processes are identified not only by their PID (which can be reused)
77/// but also by their start time and executable path to ensure precise
78/// identification across the system lifecycle.
79#[derive(Debug, Clone, Eq, PartialEq, Hash)]
80pub struct Process {
81    /// Process identifier assigned by the operating system
82    pub pid: i32,
83    /// Process start time in seconds since epoch for uniqueness
84    pub starttime: u64,
85    /// Path to the executable that created this process
86    pub exe_path: String,
87}
88
89/// Unified resource identifier for all trackable entities in the system.
90///
91/// Resources represent any entity that can participate in data flows within
92/// the traceability system. This includes file descriptors (which may point
93/// to files or streams), processes, or null resources for uninitialized states.
94#[derive(Debug, Clone, Eq, PartialEq, Hash, Default)]
95pub enum Resource {
96    /// File descriptor resource (file or stream)
97    Fd(Fd),
98    /// Process resource  
99    Process(Process),
100    /// Null or uninitialized resource
101    #[default]
102    None,
103}
104
105impl Resource {
106    /// Creates a new file resource with the specified filesystem path.
107    ///
108    /// The path is stored as provided without validation or normalization.
109    /// Applications should provide valid paths to ensure proper resource tracking.
110    pub fn new_file(path: String) -> Self {
111        Self::Fd(Fd::File(File { path }))
112    }
113
114    /// Creates a new stream resource with the specified socket addresses.
115    ///
116    /// Both local and peer socket addresses should be valid network addresses
117    /// in the format appropriate for the protocol (e.g., "IP:port" for TCP).
118    pub fn new_stream(local_socket: String, peer_socket: String) -> Self {
119        Self::Fd(Fd::Stream(Stream { local_socket, peer_socket }))
120    }
121
122    /// Creates a new process resource by querying the system for process information.
123    ///
124    /// Attempts to retrieve the process start time and executable path from the
125    /// system process table. If the process is not found, creates a process
126    /// resource with default values for the metadata fields.
127    pub fn new_process(pid: i32) -> Self {
128        let mut system = System::new();
129        system.refresh_all();
130        if let Some(process) = system.process(Pid::from(pid as usize)) {
131            let starttime = process.start_time();
132            let exe_path = if let Some(exe) = process.exe() {
133                exe.to_string_lossy().to_string()
134            } else {
135                String::new()
136            };
137            Self::Process(Process { pid, starttime, exe_path })
138        } else {
139            Self::Process(Process { pid, starttime: 0, exe_path: String::new() })
140        }
141    }
142
143    /// Creates a mock process resource for testing purposes.
144    ///
145    /// Creates a process resource with the specified PID but default values
146    /// for start time and executable path. Should only be used in test
147    /// environments where system process queries are not needed.
148    pub fn new_process_mock(pid: i32) -> Self {
149        Self::Process(Process { pid, starttime: 0, exe_path: String::new() })
150    }
151
152    /// Checks if this resource represents a filesystem file.
153    ///
154    /// Returns true if the resource is a file descriptor pointing to a file,
155    /// false for streams, processes, or null resources.
156    pub fn is_file(&self) -> bool {
157        matches!(self, Resource::Fd(Fd::File(_)))
158    }
159
160    /// Returns the path for File variant, or None otherwise
161    pub fn path(&self) -> Option<&str> {
162        if let Resource::Fd(Fd::File(f)) = self { Some(&f.path) } else { None }
163    }
164
165    /// Checks if this resource represents a network stream.
166    ///
167    /// Returns true if the resource is a file descriptor pointing to a stream,
168    /// false for files, processes, or null resources.
169    pub fn is_stream(&self) -> bool {
170        matches!(self, Resource::Fd(Fd::Stream(_)))
171    }
172
173    /// Returns the local socket for Stream variant, or None otherwise
174    pub fn local_socket(&self) -> Option<&str> {
175        if let Resource::Fd(Fd::Stream(s)) = self { Some(&s.local_socket) } else { None }
176    }
177
178    /// Returns the local socket for Stream variant, or None otherwise
179    pub fn peer_socket(&self) -> Option<&str> {
180        if let Resource::Fd(Fd::Stream(s)) = self { Some(&s.peer_socket) } else { None }
181    }
182
183    /// Converts this resource into a localized resource given the specified localization.
184    ///
185    /// It attempts to convert the resource into a localized stream resource, which infers
186    /// the peer node from the stream's peer socket.
187    /// Otherwise, it creates a localized resource with the specified localization..
188    pub fn into_localized(self, localization: String) -> LocalizedResource {
189        if let Some(localized_stream) = self.try_into_localized_peer_stream() {
190            localized_stream
191        } else {
192            LocalizedResource::new(localization, self)
193        }
194    }
195
196    /// Returns the peer stream resource if this is a stream resource.
197    ///
198    /// For stream resources, returns a new resource with the local and peer
199    /// socket addresses swapped. This is useful for tracking bidirectional
200    /// flows. Returns None for non-stream resources.
201    pub fn try_into_localized_peer_stream(&self) -> Option<LocalizedResource> {
202        let Resource::Fd(Fd::Stream(stream)) = self else {
203            return None;
204        };
205
206        let peer_socket = stream.peer_socket.parse::<SocketAddr>().ok()?;
207        let ip = peer_socket.ip();
208        let node_id = if ip.is_ipv6() { format!("[{}]", ip) } else { ip.to_string() };
209
210        Some(LocalizedResource::new(
211            node_id,
212            Resource::new_stream(stream.peer_socket.clone(), stream.local_socket.clone()),
213        ))
214    }
215
216    /// Checks if this resource represents a system process.
217    ///
218    /// Returns true if the resource is a process, false for file descriptors
219    /// or null resources.
220    pub fn is_process(&self) -> bool {
221        matches!(self, Resource::Process(_))
222    }
223}
224
225impl Display for Resource {
226    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
227        match self {
228            Resource::Fd(Fd::File(file)) => write!(f, "file://{}", file.path),
229            Resource::Fd(Fd::Stream(stream)) => {
230                write!(f, "stream://{}::::{}", stream.local_socket, stream.peer_socket)
231            }
232            Resource::Process(process) => {
233                write!(f, "process://{}::{}::{}", process.pid, process.starttime, process.exe_path)
234            }
235            Resource::None => write!(f, "None"),
236        }
237    }
238}
239
240/// Unified resource identifier for all trackable entities in the system.
241///
242/// Localized resources are resources that are associated with a specific node in a distributed system.
243/// They are used to identify resources that are local to a specific node and are used to track
244/// resources that are local to a specific node.
245#[derive(Debug, Clone, Eq, PartialEq, Hash, Default)]
246pub struct LocalizedResource {
247    /// Node identifier for this localized resource
248    node_id: String,
249    /// Resource for this localized resource
250    resource: Resource,
251}
252
253impl LocalizedResource {
254    /// Creates a new localized resource with the specified node identifier and resource.
255    pub fn new(node_id: String, resource: Resource) -> Self {
256        Self { node_id, resource }
257    }
258
259    /// Returns the node identifier for this localized resource.
260    pub fn node_id(&self) -> &String {
261        &self.node_id
262    }
263
264    /// Returns the resource for this localized resource.
265    pub fn resource(&self) -> &Resource {
266        &self.resource
267    }
268}
269
270impl TryFrom<&str> for Resource {
271    type Error = TraceabilityError;
272
273    /// Parse a resource string (without node_id) into a Resource.
274    ///
275    /// Supports the following formats:
276    /// - `file:///path` - File resource
277    /// - `stream://local::::peer` - Stream resource
278    fn try_from(s: &str) -> Result<Self, Self::Error> {
279        if let Some(path) = s.strip_prefix("file://") {
280            Ok(Resource::new_file(path.to_string()))
281        } else if let Some(stream_part) = s.strip_prefix("stream://") {
282            let sockets: Vec<&str> = stream_part.split("::::").collect();
283            if sockets.len() != 2 {
284                return Err(TraceabilityError::InvalidResourceFormat(
285                    "Invalid stream format: expected 'stream://local::::peer'".to_string(),
286                ));
287            }
288            Ok(Resource::new_stream(sockets[0].to_string(), sockets[1].to_string()))
289        } else {
290            Err(TraceabilityError::InvalidResourceFormat(
291                "Resource must start with 'file://' or 'stream://'".to_string(),
292            ))
293        }
294    }
295}
296
297impl TryFrom<String> for Resource {
298    type Error = TraceabilityError;
299
300    fn try_from(s: String) -> Result<Self, Self::Error> {
301        Resource::try_from(s.as_str())
302    }
303}
304
305impl TryFrom<&str> for LocalizedResource {
306    type Error = TraceabilityError;
307
308    /// Parse a localized resource string into a LocalizedResource.
309    ///
310    /// Supports the format: `resource@node_id`
311    /// where resource is either:
312    /// - `file:///path` - File resource
313    /// - `stream://local::::peer` - Stream resource
314    fn try_from(s: &str) -> Result<Self, Self::Error> {
315        // Split by '@' to separate resource and node_id
316        let parts: Vec<&str> = s.rsplitn(2, '@').collect();
317        if parts.len() != 2 {
318            return Err(TraceabilityError::InvalidResourceFormat(
319                "Missing '@node_id' separator".to_string(),
320            ));
321        }
322
323        let (node_id, resource_part) = (parts[0], parts[1]);
324        let resource = Resource::try_from(resource_part)?;
325
326        Ok(LocalizedResource::new(node_id.to_string(), resource))
327    }
328}
329
330impl TryFrom<String> for LocalizedResource {
331    type Error = TraceabilityError;
332
333    fn try_from(s: String) -> Result<Self, Self::Error> {
334        LocalizedResource::try_from(s.as_str())
335    }
336}
337
338impl Display for LocalizedResource {
339    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
340        write!(f, "{}@{}", self.resource, self.node_id)
341    }
342}
343
344/// Wrapper type for Display implementation of Option<Resource>
345///
346/// Uses references to avoid cloning data when displaying.
347#[derive(Debug)]
348pub enum DisplayableResource<'a, T> {
349    Option(&'a Option<T>),
350    HashSet(&'a HashSet<T>),
351    Slice(&'a [T]),
352}
353
354impl<'a> Display for DisplayableResource<'a, Resource> {
355    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
356        match self {
357            DisplayableResource::Option(Some(resource)) => write!(f, "{}", resource),
358            DisplayableResource::Option(None) => write!(f, "None"),
359            DisplayableResource::HashSet(resources) => write!(
360                f,
361                "[{}]",
362                resources.iter().map(|r| r.to_string()).collect::<Vec<String>>().join(", ")
363            ),
364            DisplayableResource::Slice(resources) => write!(
365                f,
366                "[{}]",
367                resources.iter().map(|r| r.to_string()).collect::<Vec<String>>().join(", ")
368            ),
369        }
370    }
371}
372
373impl<'a> Display for DisplayableResource<'a, LocalizedResource> {
374    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
375        match self {
376            DisplayableResource::Option(Some(resource)) => write!(f, "{}", resource),
377            DisplayableResource::Option(None) => write!(f, "None"),
378            DisplayableResource::HashSet(resources) => write!(
379                f,
380                "[{}]",
381                resources.iter().map(|r| r.to_string()).collect::<Vec<String>>().join(", ")
382            ),
383            DisplayableResource::Slice(resources) => write!(
384                f,
385                "[{}]",
386                resources.iter().map(|r| r.to_string()).collect::<Vec<String>>().join(", ")
387            ),
388        }
389    }
390}
391
392impl<'a> From<&'a Option<Resource>> for DisplayableResource<'a, Resource> {
393    fn from(t: &'a Option<Resource>) -> Self {
394        DisplayableResource::Option(t)
395    }
396}
397
398impl<'a> From<&'a HashSet<Resource>> for DisplayableResource<'a, Resource> {
399    fn from(t: &'a HashSet<Resource>) -> DisplayableResource<'a, Resource> {
400        DisplayableResource::HashSet(t)
401    }
402}
403
404impl<'a> From<&'a [Resource]> for DisplayableResource<'a, Resource> {
405    fn from(t: &'a [Resource]) -> DisplayableResource<'a, Resource> {
406        DisplayableResource::Slice(t)
407    }
408}
409
410impl<'a> From<&'a Option<LocalizedResource>> for DisplayableResource<'a, LocalizedResource> {
411    fn from(t: &'a Option<LocalizedResource>) -> Self {
412        DisplayableResource::Option(t)
413    }
414}
415
416impl<'a> From<&'a HashSet<LocalizedResource>> for DisplayableResource<'a, LocalizedResource> {
417    fn from(t: &'a HashSet<LocalizedResource>) -> Self {
418        DisplayableResource::HashSet(t)
419    }
420}
421
422impl<'a> From<&'a [LocalizedResource]> for DisplayableResource<'a, LocalizedResource> {
423    fn from(t: &'a [LocalizedResource]) -> Self {
424        DisplayableResource::Slice(t)
425    }
426}
427
428/// Trait for services that have a node identifier in distributed systems.
429///
430/// This trait is implemented by services that participate in distributed
431/// traceability operations and need to identify themselves to remote peers.
432/// The node ID is typically used in provenance records and logging to
433/// track which middleware instance processed specific operations.
434pub trait NodeId {
435    /// Returns the unique identifier for this node in the distributed system.
436    ///
437    /// Node IDs should be unique across the distributed deployment and
438    /// persistent across service restarts to maintain consistent provenance records.
439    fn node_id(&self) -> String;
440}
441
442#[cfg(test)]
443mod tests {
444    use super::*;
445
446    #[test]
447    fn test_resource_display() {
448        let file = LocalizedResource::new(
449            "127.0.0.1".to_string(),
450            Resource::new_file("/tmp/test.txt".to_string()),
451        );
452        let stream = LocalizedResource::new(
453            "127.0.0.1".to_string(),
454            Resource::new_stream("127.0.0.1:8080".to_string(), "127.0.0.1:8081".to_string()),
455        );
456        let process_mock =
457            LocalizedResource::new("127.0.0.1".to_string(), Resource::new_process_mock(1234));
458        let none = LocalizedResource::new(Default::default(), Resource::None);
459
460        assert_eq!(file.to_string(), "file:///tmp/test.txt@127.0.0.1");
461        assert_eq!(stream.to_string(), "stream://127.0.0.1:8080::::127.0.0.1:8081@127.0.0.1");
462        assert_eq!(process_mock.to_string(), "process://1234::0::@127.0.0.1");
463        assert_eq!(none.to_string(), "None@");
464    }
465
466    #[test]
467    fn test_displayable_resource() {
468        let file = LocalizedResource::new(
469            "127.0.0.1".to_string(),
470            Resource::new_file("/tmp/test.txt".to_string()),
471        );
472        let stream = LocalizedResource::new(
473            "127.0.0.1".to_string(),
474            Resource::new_stream("127.0.0.1:8080".to_string(), "127.0.0.1:8081".to_string()),
475        );
476        let process_mock =
477            LocalizedResource::new("127.0.0.1".to_string(), Resource::new_process_mock(1234));
478        let none = LocalizedResource::new(Default::default(), Resource::None);
479
480        // localized resources - test direct resource display
481        assert_eq!(file.to_string(), "file:///tmp/test.txt@127.0.0.1");
482        assert_eq!(process_mock.to_string(), "process://1234::0::@127.0.0.1");
483        assert_eq!(stream.to_string(), "stream://127.0.0.1:8080::::127.0.0.1:8081@127.0.0.1");
484        assert_eq!(none.to_string(), "None@");
485
486        // test DisplayableResource with vector of cloned resources for display
487        // In production code, we pass references to avoid clones (as shown in other files)
488        assert_eq!(
489            DisplayableResource::from(vec![file, process_mock, stream, none].as_slice())
490                .to_string(),
491            "[file:///tmp/test.txt@127.0.0.1, process://1234::0::@127.0.0.1, stream://127.0.0.1:8080::::127.0.0.1:8081@127.0.0.1, None@]"
492        );
493    }
494
495    #[test]
496    fn test_resource_try_from_file() {
497        let resource = Resource::try_from("file:///tmp/test.txt").unwrap();
498        assert!(resource.is_file());
499    }
500
501    #[test]
502    fn test_resource_try_from_stream() {
503        let resource = Resource::try_from("stream://127.0.0.1:8080::::192.168.1.1:9000").unwrap();
504        assert!(resource.is_stream());
505    }
506
507    #[test]
508    fn test_resource_try_from_string() {
509        let resource = Resource::try_from("file:///tmp/test.txt".to_string()).unwrap();
510        assert!(resource.is_file());
511    }
512
513    #[test]
514    fn test_localized_resource_try_from_file() {
515        let localized = LocalizedResource::try_from("file:///tmp/test.txt@127.0.0.1").unwrap();
516        assert_eq!(localized.node_id(), "127.0.0.1");
517        assert!(localized.resource().is_file());
518    }
519
520    #[test]
521    fn test_localized_resource_try_from_stream() {
522        let localized =
523            LocalizedResource::try_from("stream://127.0.0.1:8080::::192.168.1.1:9000@10.0.0.1")
524                .unwrap();
525        assert_eq!(localized.node_id(), "10.0.0.1");
526        assert!(localized.resource().is_stream());
527    }
528
529    #[test]
530    fn test_localized_resource_try_from_string() {
531        let localized =
532            LocalizedResource::try_from("file:///tmp/test.txt@127.0.0.1".to_string()).unwrap();
533        assert_eq!(localized.node_id(), "127.0.0.1");
534        assert!(localized.resource().is_file());
535    }
536
537    #[test]
538    fn test_resource_try_from_invalid() {
539        let result = Resource::try_from("invalid_resource");
540        assert!(result.is_err());
541        assert!(matches!(result, Err(TraceabilityError::InvalidResourceFormat(_))));
542
543        let result = Resource::try_from("stream://no_peer");
544        assert!(result.is_err());
545        assert!(matches!(result, Err(TraceabilityError::InvalidResourceFormat(_))));
546    }
547
548    #[test]
549    fn test_localized_resource_try_from_invalid() {
550        let result = LocalizedResource::try_from("file:///tmp/test.txt");
551        assert!(result.is_err());
552        assert!(matches!(result, Err(TraceabilityError::InvalidResourceFormat(_))));
553
554        let result = LocalizedResource::try_from("stream://127.0.0.1:8080::::192.168.1.1:9000");
555        assert!(result.is_err());
556        assert!(matches!(result, Err(TraceabilityError::InvalidResourceFormat(_))));
557    }
558}