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    /// Checks if this resource represents a network stream.
161    ///
162    /// Returns true if the resource is a file descriptor pointing to a stream,
163    /// false for files, processes, or null resources.
164    pub fn is_stream(&self) -> bool {
165        matches!(self, Resource::Fd(Fd::Stream(_)))
166    }
167
168    /// Converts this resource into a localized resource given the specified localization.
169    ///
170    /// It attempts to convert the resource into a localized stream resource, which infers
171    /// the peer node from the stream's peer socket.
172    /// Otherwise, it creates a localized resource with the specified localization..
173    pub fn into_localized(self, localization: String) -> LocalizedResource {
174        if let Some(localized_stream) = self.try_into_localized_peer_stream() {
175            localized_stream
176        } else {
177            LocalizedResource::new(localization, self)
178        }
179    }
180
181    /// Returns the peer stream resource if this is a stream resource.
182    ///
183    /// For stream resources, returns a new resource with the local and peer
184    /// socket addresses swapped. This is useful for tracking bidirectional
185    /// flows. Returns None for non-stream resources.
186    pub fn try_into_localized_peer_stream(&self) -> Option<LocalizedResource> {
187        if let Resource::Fd(Fd::Stream(stream)) = self
188            && let Ok(peer_socket) = stream.peer_socket.parse::<SocketAddr>()
189        {
190            Some(LocalizedResource::new(
191                peer_socket.ip().to_string(),
192                Resource::new_stream(stream.peer_socket.to_owned(), stream.local_socket.to_owned()),
193            ))
194        } else {
195            None
196        }
197    }
198
199    /// Checks if this resource represents a system process.
200    ///
201    /// Returns true if the resource is a process, false for file descriptors
202    /// or null resources.
203    pub fn is_process(&self) -> bool {
204        matches!(self, Resource::Process(_))
205    }
206}
207
208impl Display for Resource {
209    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
210        match self {
211            Resource::Fd(Fd::File(file)) => write!(f, "file:://{}", file.path),
212            Resource::Fd(Fd::Stream(stream)) => {
213                write!(f, "stream:://{}::{}", stream.local_socket, stream.peer_socket)
214            }
215            Resource::Process(process) => {
216                write!(f, "process:://{}::{}::{}", process.pid, process.starttime, process.exe_path)
217            }
218            Resource::None => write!(f, "None"),
219        }
220    }
221}
222
223/// Unified resource identifier for all trackable entities in the system.
224///
225/// Localized resources are resources that are associated with a specific node in a distributed system.
226/// They are used to identify resources that are local to a specific node and are used to track
227/// resources that are local to a specific node.
228#[derive(Debug, Clone, Eq, PartialEq, Hash, Default)]
229pub struct LocalizedResource {
230    /// Node identifier for this localized resource
231    node_id: String,
232    /// Resource for this localized resource
233    resource: Resource,
234}
235
236impl LocalizedResource {
237    /// Creates a new localized resource with the specified node identifier and resource.
238    pub fn new(node_id: String, resource: Resource) -> Self {
239        Self { node_id, resource }
240    }
241
242    /// Returns the node identifier for this localized resource.
243    pub fn node_id(&self) -> &String {
244        &self.node_id
245    }
246
247    /// Returns the resource for this localized resource.
248    pub fn resource(&self) -> &Resource {
249        &self.resource
250    }
251}
252
253impl TryFrom<&str> for Resource {
254    type Error = TraceabilityError;
255
256    /// Parse a resource string (without node_id) into a Resource.
257    ///
258    /// Supports the following formats:
259    /// - `file:///path` - File resource
260    /// - `stream://local::peer` - Stream resource
261    fn try_from(s: &str) -> Result<Self, Self::Error> {
262        if let Some(path) = s.strip_prefix("file://") {
263            Ok(Resource::new_file(path.to_string()))
264        } else if let Some(stream_part) = s.strip_prefix("stream://") {
265            let sockets: Vec<&str> = stream_part.split("::").collect();
266            if sockets.len() != 2 {
267                return Err(TraceabilityError::InvalidResourceFormat(
268                    "Invalid stream format: expected 'stream://local::peer'".to_string(),
269                ));
270            }
271            Ok(Resource::new_stream(sockets[0].to_string(), sockets[1].to_string()))
272        } else {
273            Err(TraceabilityError::InvalidResourceFormat(
274                "Resource must start with 'file://' or 'stream://'".to_string(),
275            ))
276        }
277    }
278}
279
280impl TryFrom<String> for Resource {
281    type Error = TraceabilityError;
282
283    fn try_from(s: String) -> Result<Self, Self::Error> {
284        Resource::try_from(s.as_str())
285    }
286}
287
288impl TryFrom<&str> for LocalizedResource {
289    type Error = TraceabilityError;
290
291    /// Parse a localized resource string into a LocalizedResource.
292    ///
293    /// Supports the format: `resource@node_id`
294    /// where resource is either:
295    /// - `file:///path` - File resource
296    /// - `stream://local::peer` - Stream resource
297    fn try_from(s: &str) -> Result<Self, Self::Error> {
298        // Split by '@' to separate resource and node_id
299        let parts: Vec<&str> = s.rsplitn(2, '@').collect();
300        if parts.len() != 2 {
301            return Err(TraceabilityError::InvalidResourceFormat(
302                "Missing '@node_id' separator".to_string(),
303            ));
304        }
305
306        let (node_id, resource_part) = (parts[0], parts[1]);
307        let resource = Resource::try_from(resource_part)?;
308
309        Ok(LocalizedResource::new(node_id.to_string(), resource))
310    }
311}
312
313impl TryFrom<String> for LocalizedResource {
314    type Error = TraceabilityError;
315
316    fn try_from(s: String) -> Result<Self, Self::Error> {
317        LocalizedResource::try_from(s.as_str())
318    }
319}
320
321impl Display for LocalizedResource {
322    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
323        write!(f, "{}@{}", self.resource, self.node_id)
324    }
325}
326
327/// Wrapper type for Display implementation of Option<Resource>
328///
329/// Uses references to avoid cloning data when displaying.
330#[derive(Debug)]
331pub enum DisplayableResource<'a, T> {
332    Option(&'a Option<T>),
333    HashSet(&'a HashSet<T>),
334    Slice(&'a [T]),
335}
336
337impl<'a> Display for DisplayableResource<'a, Resource> {
338    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
339        match self {
340            DisplayableResource::Option(Some(resource)) => write!(f, "{}", resource),
341            DisplayableResource::Option(None) => write!(f, "None"),
342            DisplayableResource::HashSet(resources) => write!(
343                f,
344                "[{}]",
345                resources.iter().map(|r| r.to_string()).collect::<Vec<String>>().join(", ")
346            ),
347            DisplayableResource::Slice(resources) => write!(
348                f,
349                "[{}]",
350                resources.iter().map(|r| r.to_string()).collect::<Vec<String>>().join(", ")
351            ),
352        }
353    }
354}
355
356impl<'a> Display for DisplayableResource<'a, LocalizedResource> {
357    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
358        match self {
359            DisplayableResource::Option(Some(resource)) => write!(f, "{}", resource),
360            DisplayableResource::Option(None) => write!(f, "None"),
361            DisplayableResource::HashSet(resources) => write!(
362                f,
363                "[{}]",
364                resources.iter().map(|r| r.to_string()).collect::<Vec<String>>().join(", ")
365            ),
366            DisplayableResource::Slice(resources) => write!(
367                f,
368                "[{}]",
369                resources.iter().map(|r| r.to_string()).collect::<Vec<String>>().join(", ")
370            ),
371        }
372    }
373}
374
375impl<'a> From<&'a Option<Resource>> for DisplayableResource<'a, Resource> {
376    fn from(t: &'a Option<Resource>) -> Self {
377        DisplayableResource::Option(t)
378    }
379}
380
381impl<'a> From<&'a HashSet<Resource>> for DisplayableResource<'a, Resource> {
382    fn from(t: &'a HashSet<Resource>) -> DisplayableResource<'a, Resource> {
383        DisplayableResource::HashSet(t)
384    }
385}
386
387impl<'a> From<&'a [Resource]> for DisplayableResource<'a, Resource> {
388    fn from(t: &'a [Resource]) -> DisplayableResource<'a, Resource> {
389        DisplayableResource::Slice(t)
390    }
391}
392
393impl<'a> From<&'a Option<LocalizedResource>> for DisplayableResource<'a, LocalizedResource> {
394    fn from(t: &'a Option<LocalizedResource>) -> Self {
395        DisplayableResource::Option(t)
396    }
397}
398
399impl<'a> From<&'a HashSet<LocalizedResource>> for DisplayableResource<'a, LocalizedResource> {
400    fn from(t: &'a HashSet<LocalizedResource>) -> Self {
401        DisplayableResource::HashSet(t)
402    }
403}
404
405impl<'a> From<&'a [LocalizedResource]> for DisplayableResource<'a, LocalizedResource> {
406    fn from(t: &'a [LocalizedResource]) -> Self {
407        DisplayableResource::Slice(t)
408    }
409}
410
411/// Trait for services that have a node identifier in distributed systems.
412///
413/// This trait is implemented by services that participate in distributed
414/// traceability operations and need to identify themselves to remote peers.
415/// The node ID is typically used in provenance records and logging to
416/// track which middleware instance processed specific operations.
417pub trait NodeId {
418    /// Returns the unique identifier for this node in the distributed system.
419    ///
420    /// Node IDs should be unique across the distributed deployment and
421    /// persistent across service restarts to maintain consistent provenance records.
422    fn node_id(&self) -> String;
423}
424
425#[cfg(test)]
426mod tests {
427    use super::*;
428
429    #[test]
430    fn test_resource_display() {
431        let file = LocalizedResource::new(
432            "127.0.0.1".to_string(),
433            Resource::new_file("/tmp/test.txt".to_string()),
434        );
435        let stream = LocalizedResource::new(
436            "127.0.0.1".to_string(),
437            Resource::new_stream("127.0.0.1:8080".to_string(), "127.0.0.1:8081".to_string()),
438        );
439        let process_mock =
440            LocalizedResource::new("127.0.0.1".to_string(), Resource::new_process_mock(1234));
441        let none = LocalizedResource::new(Default::default(), Resource::None);
442
443        assert_eq!(file.to_string(), "file::///tmp/test.txt@127.0.0.1");
444        assert_eq!(stream.to_string(), "stream:://127.0.0.1:8080::127.0.0.1:8081@127.0.0.1");
445        assert_eq!(process_mock.to_string(), "process:://1234::0::@127.0.0.1");
446        assert_eq!(none.to_string(), "None@");
447    }
448
449    #[test]
450    fn test_displayable_resource() {
451        let file = LocalizedResource::new(
452            "127.0.0.1".to_string(),
453            Resource::new_file("/tmp/test.txt".to_string()),
454        );
455        let stream = LocalizedResource::new(
456            "127.0.0.1".to_string(),
457            Resource::new_stream("127.0.0.1:8080".to_string(), "127.0.0.1:8081".to_string()),
458        );
459        let process_mock =
460            LocalizedResource::new("127.0.0.1".to_string(), Resource::new_process_mock(1234));
461        let none = LocalizedResource::new(Default::default(), Resource::None);
462
463        // localized resources - test direct resource display
464        assert_eq!(file.to_string(), "file::///tmp/test.txt@127.0.0.1");
465        assert_eq!(process_mock.to_string(), "process:://1234::0::@127.0.0.1");
466        assert_eq!(stream.to_string(), "stream:://127.0.0.1:8080::127.0.0.1:8081@127.0.0.1");
467        assert_eq!(none.to_string(), "None@");
468
469        // test DisplayableResource with vector of cloned resources for display
470        // In production code, we pass references to avoid clones (as shown in other files)
471        assert_eq!(
472            DisplayableResource::from(vec![file, process_mock, stream, none].as_slice())
473                .to_string(),
474            "[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@]"
475        );
476    }
477
478    #[test]
479    fn test_resource_try_from_file() {
480        let resource = Resource::try_from("file:///tmp/test.txt").unwrap();
481        assert!(resource.is_file());
482    }
483
484    #[test]
485    fn test_resource_try_from_stream() {
486        let resource = Resource::try_from("stream://127.0.0.1:8080::192.168.1.1:9000").unwrap();
487        assert!(resource.is_stream());
488    }
489
490    #[test]
491    fn test_resource_try_from_string() {
492        let resource = Resource::try_from("file:///tmp/test.txt".to_string()).unwrap();
493        assert!(resource.is_file());
494    }
495
496    #[test]
497    fn test_localized_resource_try_from_file() {
498        let localized = LocalizedResource::try_from("file:///tmp/test.txt@127.0.0.1").unwrap();
499        assert_eq!(localized.node_id(), "127.0.0.1");
500        assert!(localized.resource().is_file());
501    }
502
503    #[test]
504    fn test_localized_resource_try_from_stream() {
505        let localized =
506            LocalizedResource::try_from("stream://127.0.0.1:8080::192.168.1.1:9000@10.0.0.1")
507                .unwrap();
508        assert_eq!(localized.node_id(), "10.0.0.1");
509        assert!(localized.resource().is_stream());
510    }
511
512    #[test]
513    fn test_localized_resource_try_from_string() {
514        let localized =
515            LocalizedResource::try_from("file:///tmp/test.txt@127.0.0.1".to_string()).unwrap();
516        assert_eq!(localized.node_id(), "127.0.0.1");
517        assert!(localized.resource().is_file());
518    }
519
520    #[test]
521    fn test_resource_try_from_invalid() {
522        let result = Resource::try_from("invalid_resource");
523        assert!(result.is_err());
524        assert!(matches!(result, Err(TraceabilityError::InvalidResourceFormat(_))));
525
526        let result = Resource::try_from("stream://no_peer");
527        assert!(result.is_err());
528        assert!(matches!(result, Err(TraceabilityError::InvalidResourceFormat(_))));
529    }
530
531    #[test]
532    fn test_localized_resource_try_from_invalid() {
533        let result = LocalizedResource::try_from("file:///tmp/test.txt");
534        assert!(result.is_err());
535        assert!(matches!(result, Err(TraceabilityError::InvalidResourceFormat(_))));
536
537        let result = LocalizedResource::try_from("stream://127.0.0.1:8080::192.168.1.1:9000");
538        assert!(result.is_err());
539        assert!(matches!(result, Err(TraceabilityError::InvalidResourceFormat(_))));
540    }
541}