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 is_stream(&self) -> bool {
165 matches!(self, Resource::Fd(Fd::Stream(_)))
166 }
167
168 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 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 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#[derive(Debug, Clone, Eq, PartialEq, Hash, Default)]
229pub struct LocalizedResource {
230 node_id: String,
232 resource: Resource,
234}
235
236impl LocalizedResource {
237 pub fn new(node_id: String, resource: Resource) -> Self {
239 Self { node_id, resource }
240 }
241
242 pub fn node_id(&self) -> &String {
244 &self.node_id
245 }
246
247 pub fn resource(&self) -> &Resource {
249 &self.resource
250 }
251}
252
253impl TryFrom<&str> for Resource {
254 type Error = TraceabilityError;
255
256 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 fn try_from(s: &str) -> Result<Self, Self::Error> {
298 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#[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
411pub trait NodeId {
418 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 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 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}