From 412d490536e77a25cb5532e74128f1bbfce5ec47 Mon Sep 17 00:00:00 2001
From: Benjamin Nguyen <benjamin.van.nguyen@gmail.com>
Date: Sun, 5 Mar 2023 18:26:28 -0800
Subject: [PATCH] don't double count hardlinks if an inode is already taken
 into account

---
 src/fs/erdtree/node.rs     | 21 +++++++++++-
 src/fs/erdtree/tree/mod.rs | 10 +++++-
 src/fs/inode.rs            | 68 ++++++++++++++++++++++++++++++++++++++
 src/fs/mod.rs              |  3 ++
 4 files changed, 100 insertions(+), 2 deletions(-)
 create mode 100644 src/fs/inode.rs

diff --git a/src/fs/erdtree/node.rs b/src/fs/erdtree/node.rs
index daf7fbb3..eb447432 100644
--- a/src/fs/erdtree/node.rs
+++ b/src/fs/erdtree/node.rs
@@ -1,4 +1,8 @@
-use super::{disk_usage::DiskUsage, get_ls_colors};
+use super::{
+    super::inode::Inode,
+    disk_usage::DiskUsage,
+    get_ls_colors,
+};
 use crate::{
     fs::file_size::FileSize,
     icons::{self, icon_from_ext, icon_from_file_name, icon_from_file_type},
@@ -31,6 +35,7 @@ pub struct Node {
     children: Option<Vec<Node>>,
     file_name: OsString,
     file_type: Option<FileType>,
+    inode: Option<Inode>,
     path: PathBuf,
     show_icon: bool,
     style: Style,
@@ -45,6 +50,7 @@ impl Node {
         children: Option<Vec<Node>>,
         file_name: OsString,
         file_type: Option<FileType>,
+        inode: Option<Inode>,
         path: PathBuf,
         show_icon: bool,
         style: Style,
@@ -56,6 +62,7 @@ impl Node {
             file_name,
             file_size,
             file_type,
+            inode,
             path,
             show_icon,
             style,
@@ -146,6 +153,11 @@ impl Node {
         &self.style
     }
 
+    /// Returns reference to underlying [Inode] if any.
+    pub fn inode(&self) -> Option<&Inode> {
+        self.inode.as_ref()
+    }
+
     /// Gets stylized icon for node if enabled. Icons without extensions are styled based on the
     /// [`LS_COLORS`] foreground configuration of the associated file name.
     ///
@@ -263,12 +275,19 @@ impl From<NodePrecursor<'_>> for Node {
             }
         };
 
+        let inode = metadata
+            .map(Inode::try_from)
+            .transpose()
+            .ok()
+            .flatten();
+
         Self::new(
             depth,
             file_size,
             children,
             file_name,
             file_type,
+            inode,
             path.into(),
             show_icon,
             style,
diff --git a/src/fs/erdtree/tree/mod.rs b/src/fs/erdtree/tree/mod.rs
index aca13610..e1b6f33c 100644
--- a/src/fs/erdtree/tree/mod.rs
+++ b/src/fs/erdtree/tree/mod.rs
@@ -8,7 +8,7 @@ use crate::cli::Clargs;
 use crossbeam::channel::{self, Sender};
 use ignore::{WalkParallel, WalkState};
 use std::{
-    collections::HashMap,
+    collections::{HashMap, HashSet},
     convert::TryFrom,
     fmt::{self, Display, Formatter},
     path::PathBuf,
@@ -79,6 +79,7 @@ impl Tree {
         // components needed to assemble a `Tree`.
         let tree_components = thread::spawn(move || -> TreeResult<TreeComponents> {
             let mut branches: Branches = HashMap::new();
+            let mut inodes = HashSet::new();
             let mut root = None;
 
             while let Ok(node) = rx.recv() {
@@ -93,6 +94,13 @@ impl Tree {
                         root = Some(node);
                         continue;
                     }
+                } else {
+                    // If a hard-link is already accounted for skip the subsequent one.
+                    if let Some(inode) = node.inode() {
+                        if !inodes.insert(inode.properties()) {
+                            continue
+                        }
+                    }
                 }
 
                 let parent = node.parent_path_buf().ok_or(Error::ExpectedParent)?;
diff --git a/src/fs/inode.rs b/src/fs/inode.rs
new file mode 100644
index 00000000..040d5580
--- /dev/null
+++ b/src/fs/inode.rs
@@ -0,0 +1,68 @@
+use std::{
+    convert::TryFrom,
+    error::Error as StdError,
+    fmt::{self, Display},
+    fs::Metadata,
+};
+
+/// Represents a file's underlying inode.
+#[derive(Debug)]
+pub struct Inode {
+    ino: u64,
+    dev: u64,
+    nlink: u64
+}
+
+impl Inode {
+    /// Initializer for an inode given all the properties that make it unique.
+    pub fn new(ino: u64, dev: u64, nlink: u64) -> Self {
+        Self { ino, dev, nlink }
+    }
+
+    /// Returns a tuple of all the fields of the [Inode].
+    pub fn properties(&self) -> (u64, u64, u64) {
+        (self.ino, self.dev, self.nlink)
+    }
+}
+
+#[derive(Debug)]
+pub struct Error;
+
+impl Display for Error {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> std::fmt::Result {
+        write!(f, "Insufficient information to compute inode")
+    }
+}
+
+impl StdError for Error {}
+
+impl TryFrom<Metadata> for Inode {
+    type Error = Error;
+
+    #[cfg(unix)]
+    fn try_from(md: Metadata) -> Result<Self, Self::Error> {
+        use std::os::unix::fs::MetadataExt;
+
+        Ok(Self::new(md.ino(), md.dev(), md.nlink()))
+    }
+
+    #[cfg(windows)]
+    fn try_from(md: Metadata) -> Result<Self, Self::Error> {
+        use std::os::windows::fs::MetadataExt;
+
+        if let (Some(ino), Some(dev), Some(nlinks)) = (
+            metadata.file_index(),
+            metadata.volume_serial_number(),
+            metadata.number_of_links(),
+        ) {
+            return Ok(Self::new(md, dev, nlink));
+        }
+
+        Err(Error {})
+    }
+
+    #[cfg(not(any(unix, windows)))]
+    fn try_from(md: Metadata) -> Result<Self, Self::Error> {
+        Err(Error {})
+    }
+}
diff --git a/src/fs/mod.rs b/src/fs/mod.rs
index 1ebed201..d2bd8a27 100644
--- a/src/fs/mod.rs
+++ b/src/fs/mod.rs
@@ -1,6 +1,9 @@
 /// Errors related to filesystem traversal.
 pub mod error;
 
+/// Operations pertaining to Inodes.
+pub mod inode;
+
 /// Operations to present disk usage in human-readable format.
 pub mod file_size;