1use 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#[derive(Debug, Clone, Eq, PartialEq, Hash)]
42pub struct File {
43 pub path: String,
45}
46
47#[derive(Debug, Clone, Eq, PartialEq, Hash)]
54pub struct Stream {
55 pub local_socket: String,
57 pub peer_socket: String,
59}
60
61#[derive(Debug, Clone, Eq, PartialEq, Hash)]
67pub enum Fd {
68 File(File),
70 Stream(Stream),
72}
73
74#[derive(Debug, Clone, Eq, PartialEq, Hash)]
80pub struct Process {
81 pub pid: i32,
83 pub starttime: u64,
85 pub exe_path: String,
87}
88
89#[derive(Debug, Clone, Eq, PartialEq, Hash, Default)]
95pub enum Resource {
96 Fd(Fd),
98 Process(Process),
100 #[default]
102 None,
103}
104
105impl Resource {
106 pub fn new_file(path: String) -> Self {
111 Self::Fd(Fd::File(File { path }))
112 }
113
114 pub fn new_stream(local_socket: String, peer_socket: String) -> Self {
119 Self::Fd(Fd::Stream(Stream { local_socket, peer_socket }))
120 }
121
122 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 pub fn new_process_mock(pid: i32) -> Self {
149 Self::Process(Process { pid, starttime: 0, exe_path: String::new() })
150 }
151
152 pub fn is_file(&self) -> bool {
157 matches!(self, Resource::Fd(Fd::File(_)))
158 }
159
160 pub fn path(&self) -> Option<&str> {
162 if let Resource::Fd(Fd::File(f)) = self { Some(&f.path) } else { None }
163 }
164
165 pub fn is_stream(&self) -> bool {
170 matches!(self, Resource::Fd(Fd::Stream(_)))
171 }
172
173 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 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 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 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 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#[derive(Debug, Clone, Eq, PartialEq, Hash, Default)]
246pub struct LocalizedResource {
247 node_id: String,
249 resource: Resource,
251}
252
253impl LocalizedResource {
254 pub fn new(node_id: String, resource: Resource) -> Self {
256 Self { node_id, resource }
257 }
258
259 pub fn node_id(&self) -> &String {
261 &self.node_id
262 }
263
264 pub fn resource(&self) -> &Resource {
266 &self.resource
267 }
268}
269
270impl TryFrom<&str> for Resource {
271 type Error = TraceabilityError;
272
273 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 fn try_from(s: &str) -> Result<Self, Self::Error> {
315 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#[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
428pub trait NodeId {
435 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 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 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}