Repository: dotenv-linter/dotenv-linter (Rust)

Issue:
Long recursion possible with cyclic links in dir tree
Create a single file and a symbolic link to its containing dir:
```
$ mkdir -p /tmp/project
$ echo 'ohno' > /tmp/project/.env
$ ln -s /tmp/project/ /tmp/project/back_to_root
$ tree -a /tmp/project
/tmp/project
├── back_to_root -> /tmp/project/
└── .env
```

Run linter with recursive search:
```
$ cargo run -- -r /tmp/project/
    Finished dev [unoptimized + debuginfo] target(s) in 0.01s
     Running `target/debug/dotenv-linter -r /tmp/project/`
../../../../tmp/project/.env:1 KeyWithoutValue: The ohno key should be with a value or have an equal sign
../../../../tmp/project/back_to_root/.env:1 KeyWithoutValue: The ohno key should be with a value or have an equal sign
../../../../tmp/project/back_to_root/back_to_root/.env:1 KeyWithoutValue: The ohno key should be with a value or have an equal sign
[snipped]

Found 41 problems
```
It follows the link and rechecks the same file again and again.

Possible solutions:
1. Don't follow links
2. Keep track of already visited dirs in canonicalized form



Discussion:
@ametalon Thank you! I like more the first option.

@dotenv-linter/core What do you think?
> @ametalon Thank you! I like more the first option.
> 
> @dotenv-linter/core What do you think?

I agree, the first option sounds good.
Hi, I would like to fix this issue if it is available.
Don't follow links sound sufficient I think



Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2244aab7b..90e9fdefd 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 - Add default exclude list [#324](https://github.com/dotenv-linter/dotenv-linter/pull/324) ([@ametalon](https://github.com/ametalon))
 
 ### 🔧 Changed
+- Fix bug where symlinks would cycle in recursive mode [#328](https://github.com/dotenv-linter/dotenv-linter/pull/328) ([@sonro](https://github.com/sonro))
 - Fix linter rechecking files if they were listed more than once [#327](https://github.com/dotenv-linter/dotenv-linter/pull/327) ([@Aashu23](https://github.com/Aashu23))
 - Added docker build step to the CI pipeline [#322](https://github.com/dotenv-linter/dotenv-linter/pull/322) ([@JoeAmedeo](https://github.com/JoeAmedeo))
 - Change soon deprecated `set-env` action [#320](https://github.com/dotenv-linter/dotenv-linter/pull/320) ([@marcodenisi](https://github.com/marcodenisi))
diff --git a/src/lib.rs b/src/lib.rs
index f60779fe7..1870c4abf 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -86,7 +86,10 @@ fn get_file_paths(
             read_dir
                 .filter_map(|e| e.ok())
                 .map(|e| e.path())
-                .filter(|path| FileEntry::is_env_file(path) || (is_recursive && path.is_dir()))
+                .filter(|path| {
+                    FileEntry::is_env_file(path)
+                        || (is_recursive && path.is_dir() && path.read_link().is_err())
+                })
                 .collect()
         })
         .flat_map(|dir_entries| get_file_paths(dir_entries, excludes, is_recursive))


Test patch:
diff --git a/tests/common/mod.rs b/tests/common/mod.rs
index 8abf23f86..7dca2dc5d 100644
--- a/tests/common/mod.rs
+++ b/tests/common/mod.rs
@@ -1,6 +1,7 @@
 mod output;
 mod test_dir;
 mod test_file;
+mod test_link;
 
 pub use output::*;
 pub use test_dir::TestDir;
diff --git a/tests/common/test_dir.rs b/tests/common/test_dir.rs
index 1e58d3754..daac9eaf0 100644
--- a/tests/common/test_dir.rs
+++ b/tests/common/test_dir.rs
@@ -6,6 +6,7 @@ use tempfile::{tempdir, tempdir_in, TempDir};
 use dunce::canonicalize;
 
 use crate::common::test_file::TestFile;
+use crate::common::test_link::create_test_symlink;
 #[cfg(not(windows))]
 use std::fs::canonicalize;
 
@@ -50,6 +51,12 @@ impl TestDir {
         TestFile::new(&self.current_dir, name, contents)
     }
 
+    /// Create a new TestLink to a TestDir within the TestDir
+    pub fn create_symlink(&self, source_test_dir: &Self, name: &str) {
+        let dest = &self.current_dir.path().join(name);
+        create_test_symlink(&source_test_dir.current_dir, dest);
+    }
+
     /// Get full path of TestDir as a &str
     pub fn as_str(&self) -> &str {
         self.current_dir
diff --git a/tests/common/test_link.rs b/tests/common/test_link.rs
new file mode 100644
index 000000000..aed8ccf7b
--- /dev/null
+++ b/tests/common/test_link.rs
@@ -0,0 +1,13 @@
+use std::os;
+use std::path::Path;
+use tempfile::TempDir;
+
+/// Create a new symlink within a TempDir
+#[cfg(not(windows))]
+pub fn create_test_symlink(source_test_dir: &TempDir, dest: &Path) {
+    os::unix::fs::symlink(source_test_dir, dest).expect("create symlink");
+}
+#[cfg(windows)]
+pub fn create_test_symlink(source_test_dir: &TempDir, dest: &Path) {
+    os::windows::fs::symlink_dir(source_test_dir, dest).expect("create symlink");
+}
diff --git a/tests/flags/recursive.rs b/tests/flags/recursive.rs
index f30ea5a64..0004a0031 100644
--- a/tests/flags/recursive.rs
+++ b/tests/flags/recursive.rs
@@ -80,3 +80,23 @@ fn checks_recursive_with_exclude_subdir() {
 
     test_dir.test_command_fail_with_args(args, expected_output);
 }
+
+#[test]
+fn checks_nofollow_subdir_symlinks() {
+    let test_dir = TestDir::new();
+    let test_subdir = test_dir.subdir();
+    let testfile = test_subdir.create_testfile(".incorrect.env", "1BAR=\n");
+    // create a symbolic link to its containing directory
+    test_subdir.create_symlink(&test_subdir, "symlink");
+
+    let args = &["-r"];
+    let expected_output = format!(
+        "{}:1 LeadingCharacter: Invalid leading character detected\n\nFound 1 problem\n",
+        Path::new(&test_dir.relative_path(&test_subdir))
+            .join(testfile.shortname_as_str())
+            .to_str()
+            .expect("multi-platform path to test .env file")
+    );
+
+    test_dir.test_command_fail_with_args(args, expected_output);
+}