From e9b4340a25dfb7a5f8cec5b05f2fac6362eb7a2c Mon Sep 17 00:00:00 2001 From: jcfp Date: Wed, 3 Jun 2020 15:57:52 +0200 Subject: [PATCH] extend filesystem tests (#1481) * extend filesystem tests * fix test failure when no explicit umask was set * have black uglify the code * require case-sensitive fs for test_capitalization_linux * run black with -l120 instead * make windows-compatible, fix some minor issues * mark xfail rather than comment out part of trim_win_path --- sabnzbd/filesystem.py | 2 +- tests/requirements.txt | 3 +- tests/test_filesystem.py | 768 ++++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 767 insertions(+), 6 deletions(-) diff --git a/sabnzbd/filesystem.py b/sabnzbd/filesystem.py index 2f26ee8..9fbdf6f 100644 --- a/sabnzbd/filesystem.py +++ b/sabnzbd/filesystem.py @@ -252,7 +252,7 @@ def is_obfuscated_filename(filename): """ Check if this file has an extension, if not, it's probably obfuscated and we don't use it """ - return os.path.splitext(filename)[1] == "" + return len(get_ext(filename)) < 2 def real_path(loc, path): diff --git a/tests/requirements.txt b/tests/requirements.txt index 73688da..2e26c65 100644 --- a/tests/requirements.txt +++ b/tests/requirements.txt @@ -2,6 +2,7 @@ selenium requests pytest +pyfakefs # Only works on Python 3.6+ -black; python_version >= "3.6" \ No newline at end of file +black; python_version >= "3.6" diff --git a/tests/test_filesystem.py b/tests/test_filesystem.py index 5205215..f0dd060 100644 --- a/tests/test_filesystem.py +++ b/tests/test_filesystem.py @@ -16,14 +16,24 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. """ -tests.test_misc - Testing functions in filesystem.py +tests.test_filesystem - Testing functions in filesystem.py """ +import os +import stat +import pyfakefs.fake_filesystem_unittest as ffs import sabnzbd.filesystem as filesystem +import sabnzbd.cfg from tests.testhelper import * +# Set the global uid for fake filesystems to a non-root user; +# by default this depends on the user running pytest. +global_uid = 1000 +ffs.set_uid(global_uid) + + class TestFileFolderNameSanitizer: def test_empty(self): assert filesystem.sanitize_filename(None) is None @@ -32,17 +42,30 @@ class TestFileFolderNameSanitizer: @set_platform("win32") def test_colon_handling_windows(self): assert filesystem.sanitize_filename("test:aftertest") == "test-aftertest" + assert filesystem.sanitize_filename(":") == "-" + assert filesystem.sanitize_filename("test:") == "test-" + assert filesystem.sanitize_filename("test: ") == "test-" # They should act the same assert filesystem.sanitize_filename("test:aftertest") == filesystem.sanitize_foldername("test:aftertest") - # TODO: Add a lot more tests here! @set_platform("darwin") def test_colon_handling_darwin(self): assert filesystem.sanitize_filename("test:aftertest") == "aftertest" + assert filesystem.sanitize_filename(":aftertest") == "aftertest" + assert filesystem.sanitize_filename("::aftertest") == "aftertest" + assert filesystem.sanitize_filename(":after:test") == "test" + # Empty after sanitising with darwin colon handling + assert filesystem.sanitize_filename(":") == "unknown" + assert filesystem.sanitize_filename("test:") == "unknown" + assert filesystem.sanitize_filename("test: ") == "unknown" @set_platform("linux") + @set_config({"sanitize_safe": False}) def test_colon_handling_other(self): assert filesystem.sanitize_filename("test:aftertest") == "test:aftertest" + assert filesystem.sanitize_filename(":") == ":" + assert filesystem.sanitize_filename("test:") == "test:" + assert filesystem.sanitize_filename("test: ") == "test:" @set_platform("win32") def test_win_devices_on_win(self): @@ -53,6 +76,7 @@ class TestFileFolderNameSanitizer: assert filesystem.sanitize_filename("a$mft") == "a$mft" @set_platform("linux") + @set_config({"sanitize_safe": False}) def test_win_devices_not_win(self): # Linux and Darwin are the same for this assert filesystem.sanitize_filename(None) is None @@ -61,21 +85,140 @@ class TestFileFolderNameSanitizer: assert filesystem.sanitize_filename("$mft") == "$mft" assert filesystem.sanitize_filename("a$mft") == "a$mft" + @set_platform("linux") + @set_config({"sanitize_safe": False}) + def test_file_illegal_chars_linux(self): + assert filesystem.sanitize_filename("test/aftertest") == "test+aftertest" + assert filesystem.sanitize_filename("/test") == "+test" + assert filesystem.sanitize_filename("test/") == "test+" + assert filesystem.sanitize_filename(r"/test\/aftertest/") == r"+test\+aftertest+" + assert filesystem.sanitize_filename("/") == "+" + assert filesystem.sanitize_filename("///") == "+++" + assert filesystem.sanitize_filename("../") == "..+" + assert filesystem.sanitize_filename("../test") == "..+test" + + @set_platform("linux") + @set_config({"sanitize_safe": False}) + def test_folder_illegal_chars_linux(self): + assert filesystem.sanitize_foldername('test"aftertest') == "test'aftertest" + assert filesystem.sanitize_foldername("test:") == "test-" + assert filesystem.sanitize_foldername("test<>?*|aftertest") == "test<>?*|aftertest" + + def test_char_collections(self): + assert len(filesystem.CH_ILLEGAL) == len(filesystem.CH_LEGAL) + assert len(filesystem.CH_ILLEGAL_WIN) == len(filesystem.CH_LEGAL_WIN) + + @set_platform("linux") + @set_config({"sanitize_safe": False}) + def test_legal_chars_linux(self): + # Illegal on Windows but not on Linux, unless sanitize_safe is active. + # Don't bother with '/' which is illegal in filenames on all platforms. + char_ill = filesystem.CH_ILLEGAL_WIN.replace("/", "") + assert filesystem.sanitize_filename("test" + char_ill + "aftertest") == ("test" + char_ill + "aftertest") + for char in char_ill: + # Try at start, middle, and end of a filename. + assert filesystem.sanitize_filename("test" + char * 2 + "aftertest") == ("test" + char * 2 + "aftertest") + assert filesystem.sanitize_filename("test" + char * 2) == ("test" + char * 2).strip() + assert filesystem.sanitize_filename(char * 2 + "test") == (char * 2 + "test").strip() + + @set_platform("linux") + @set_config({"sanitize_safe": True}) + def test_sanitize_safe_linux(self): + # Set sanitize_safe to on, simulating Windows-style restrictions. + assert filesystem.sanitize_filename("test" + filesystem.CH_ILLEGAL_WIN + "aftertest") == ( + "test" + filesystem.CH_LEGAL_WIN + "aftertest" + ) + for index in range(0, len(filesystem.CH_ILLEGAL_WIN)): + char_leg = filesystem.CH_LEGAL_WIN[index] + char_ill = filesystem.CH_ILLEGAL_WIN[index] + assert filesystem.sanitize_filename("test" + char_ill * 2 + "aftertest") == ( + "test" + char_leg * 2 + "aftertest" + ) + # Illegal chars that also get caught by strip() never make it far + # enough to be replaced by their legal equivalents if they appear + # on either end of the filename. + if char_ill.strip(): + assert filesystem.sanitize_filename("test" + char_ill * 2) == ("test" + char_leg * 2) + assert filesystem.sanitize_filename(char_ill * 2 + "test") == (char_leg * 2 + "test") + + def test_filename_dot(self): + # All dots should survive in filenames + assert filesystem.sanitize_filename(".test") == ".test" + assert filesystem.sanitize_filename("..test") == "..test" + assert filesystem.sanitize_filename("test.") == "test." + assert filesystem.sanitize_filename("test..") == "test.." + assert filesystem.sanitize_filename("test.aftertest") == "test.aftertest" + assert filesystem.sanitize_filename("test..aftertest") == "test..aftertest" + assert filesystem.sanitize_filename("test.aftertest.") == "test.aftertest." + assert filesystem.sanitize_filename("test.aftertest..") == "test.aftertest.." + + def test_foldername_dot(self): + # Dot should be removed from the end of directory names only + assert filesystem.sanitize_foldername(".test") == ".test" + assert filesystem.sanitize_foldername("..test") == "..test" + assert filesystem.sanitize_foldername("test.") == "test" + assert filesystem.sanitize_foldername("test..") == "test" + assert filesystem.sanitize_foldername("test.aftertest") == "test.aftertest" + assert filesystem.sanitize_foldername("test..aftertest") == "test..aftertest" + assert filesystem.sanitize_foldername("test.aftertest.") == "test.aftertest" + assert filesystem.sanitize_foldername("test.aftertest..") == "test.aftertest" + assert filesystem.sanitize_foldername("/test/this.") == "+test+this" + assert filesystem.sanitize_foldername("/test./this.") == "+test.+this" + + def test_filename_empty_result(self): + # Nothing remains after sanitizing the filename + assert filesystem.sanitize_filename("\n") == "unknown" + assert filesystem.sanitize_filename("\r\n") == "unknown" + assert filesystem.sanitize_filename("\n\r") == "unknown" + assert filesystem.sanitize_filename("\t\t\t") == "unknown" + assert filesystem.sanitize_filename(" ") == "unknown" + assert filesystem.sanitize_filename(" ") == "unknown" + + def test_foldername_empty_result(self): + # Nothing remains after sanitizing the foldername + assert filesystem.sanitize_foldername("\n") == "unknown" + assert filesystem.sanitize_foldername("\r\n") == "unknown" + assert filesystem.sanitize_foldername("\n\r") == "unknown" + assert filesystem.sanitize_foldername("\t\t\t") == "unknown" + assert filesystem.sanitize_foldername(" ") == "unknown" + assert filesystem.sanitize_foldername(" ") == "unknown" + class TestSameFile: - def test_nothing_in_common(self): + def test_nothing_in_common_win_paths(self): assert 0 == filesystem.same_file("C:\\", "D:\\") assert 0 == filesystem.same_file("C:\\", "/home/test") + + def test_nothing_in_common_unix_paths(self): assert 0 == filesystem.same_file("/home/", "/data/test") assert 0 == filesystem.same_file("/test/home/test", "/home/") + assert 0 == filesystem.same_file("/test/../home", "/test") + assert 0 == filesystem.same_file("/test/./test", "/test") + + @pytest.mark.skipif(sys.platform.startswith("win"), reason="Not for Windows") + @set_platform("linux") + def test_posix_fun(self): + assert 1 == filesystem.same_file("/test", "/test") + # IEEE 1003.1-2017 par. 4.13 for details + assert 0 == filesystem.same_file("/test", "//test") + assert 1 == filesystem.same_file("/test", "///test") + assert 1 == filesystem.same_file("/test", "/test/") + assert 1 == filesystem.same_file("/test", "/test//") + assert 1 == filesystem.same_file("/test", "/test///") def test_same(self): assert 1 == filesystem.same_file("/home/123", "/home/123") assert 1 == filesystem.same_file("D:\\", "D:\\") + assert 1 == filesystem.same_file("/test/../test", "/test") + assert 1 == filesystem.same_file("test/../test", "test") + assert 1 == filesystem.same_file("/test/./test", "/test/test") + assert 1 == filesystem.same_file("./test", "test") def test_subfolder(self): assert 2 == filesystem.same_file("\\\\?\\C:\\", "\\\\?\\C:\\Users\\") assert 2 == filesystem.same_file("/home/test123", "/home/test123/sub") + assert 2 == filesystem.same_file("/test", "/test/./test") + assert 2 == filesystem.same_file("/home/../test", "/test/./test") @set_platform("win32") def test_capitalization(self): @@ -83,4 +226,621 @@ class TestSameFile: assert 1 == filesystem.same_file("/HOME/123", "/home/123") assert 1 == filesystem.same_file("D:\\", "d:\\") assert 2 == filesystem.same_file("\\\\?\\c:\\", "\\\\?\\C:\\Users\\") - assert 2 == filesystem.same_file("/HOME/test123", "/home/test123/sub") + + @pytest.mark.skipif(sys.platform.startswith(("win", "darwin")), reason="Requires a case-sensitive filesystem") + @set_platform("linux") + def test_capitalization_linux(self): + assert 2 == filesystem.same_file("/home/test123", "/home/test123/sub") + assert 0 == filesystem.same_file("/test", "/Test") + assert 0 == filesystem.same_file("tesT", "Test") + assert 0 == filesystem.same_file("/test/../Home", "/home") + + +class TestIsObfuscatedFilename: + def test_obfuscated(self): + # Files are considered obfuscated if they lack an extension + assert filesystem.is_obfuscated_filename(".") is True + assert filesystem.is_obfuscated_filename("..") is True + assert filesystem.is_obfuscated_filename(".test") is True + assert filesystem.is_obfuscated_filename("test.") is True + assert filesystem.is_obfuscated_filename("test.ext.") is True + assert filesystem.is_obfuscated_filename("t.....") is True + assert filesystem.is_obfuscated_filename("a_" + ("test" * 666)) is True + + def test_not_obfuscated(self): + assert filesystem.is_obfuscated_filename("test.ext") is False + assert filesystem.is_obfuscated_filename(".test.ext") is False + assert filesystem.is_obfuscated_filename("test..ext") is False + assert filesystem.is_obfuscated_filename("test.ext") is False + assert filesystem.is_obfuscated_filename("test .ext") is False + assert filesystem.is_obfuscated_filename("test. ext") is False + assert filesystem.is_obfuscated_filename("test . ext") is False + assert filesystem.is_obfuscated_filename("a." + ("test" * 666)) is False + + +class TestClipLongPath: + def test_empty(self): + assert filesystem.clip_path(None) is None + assert filesystem.long_path(None) is None + + @set_platform("win32") + def test_clip_path_win(self): + assert filesystem.clip_path(r"\\?\UNC\test") == r"\\test" + assert filesystem.clip_path(r"\\?\F:\test") == r"F:\test" + + @set_platform("win32") + def test_nothing_to_clip_win(self): + assert filesystem.clip_path(r"\\test") == r"\\test" + assert filesystem.clip_path(r"F:\test") == r"F:\test" + assert filesystem.clip_path("/test/dir") == "/test/dir" + + @set_platform("linux") + def test_clip_path_non_win(self): + # Shouldn't have any effect on platforms other than Windows + assert filesystem.clip_path(r"\\?\UNC\test") == r"\\?\UNC\test" + assert filesystem.clip_path(r"\\?\F:\test") == r"\\?\F:\test" + assert filesystem.clip_path(r"\\test") == r"\\test" + assert filesystem.clip_path(r"F:\test") == r"F:\test" + assert filesystem.clip_path("/test/dir") == "/test/dir" + + @set_platform("win32") + def test_long_path_win(self): + assert filesystem.long_path(r"\\test") == r"\\?\UNC\test" + assert filesystem.long_path(r"F:\test") == r"\\?\F:\test" + + @set_platform("win32") + def test_nothing_to_lenghten_win(self): + assert filesystem.long_path(r"\\?\UNC\test") == r"\\?\UNC\test" + assert filesystem.long_path(r"\\?\F:\test") == r"\\?\F:\test" + + @set_platform("linux") + def test_long_path_non_win(self): + # Shouldn't have any effect on platforms other than Windows + assert filesystem.long_path(r"\\?\UNC\test") == r"\\?\UNC\test" + assert filesystem.long_path(r"\\?\F:\test") == r"\\?\F:\test" + assert filesystem.long_path(r"\\test") == r"\\test" + assert filesystem.long_path(r"F:\test") == r"F:\test" + assert filesystem.long_path("/test/dir") == "/test/dir" + + +@pytest.mark.skipif(sys.platform.startswith("win"), reason="Broken on Windows") +class TestCheckMountLinux(ffs.TestCase): + # Our collection of fake directories + test_dirs = ["/media/test/dir", "/mnt/TEST/DIR"] + + def setUp(self): + self.setUpPyfakefs() + self.fs.path_separator = "/" + self.fs.is_case_sensitive = True + for dir in self.test_dirs: + self.fs.create_dir(dir, perm_bits=755) + # Sanity check the fake filesystem + assert os.path.exists(dir) is True + + @set_platform("linux") + def test_bare_mountpoint_linux(self): + assert filesystem.check_mount("/media") is True + assert filesystem.check_mount("/media/") is True + assert filesystem.check_mount("/mnt") is True + assert filesystem.check_mount("/mnt/") is True + + @set_platform("linux") + def test_existing_dir_linux(self): + assert filesystem.check_mount("/media/test") is True + assert filesystem.check_mount("/media/test/dir/") is True + assert filesystem.check_mount("/media/test/DIR/") is True + assert filesystem.check_mount("/mnt/TEST") is True + assert filesystem.check_mount("/mnt/TEST/dir/") is True + assert filesystem.check_mount("/mnt/TEST/DIR/") is True + + @set_platform("linux") + # Cut down a bit on the waiting time + @set_config({"wait_ext_drive": 1}) + def test_dir_nonexistent_linux(self): + # Filesystem is case-sensitive on this platform + assert filesystem.check_mount("/media/TEST") is False # Issue #1457 + assert filesystem.check_mount("/media/TesT/") is False + assert filesystem.check_mount("/mnt/TeSt/DIR") is False + assert filesystem.check_mount("/mnt/test/DiR/") is False + + @set_platform("linux") + def test_dir_outsider_linux(self): + # Outside of /media and /mnt + assert filesystem.check_mount("/test/that/") is True + # Root directory + assert filesystem.check_mount("/") is True + + +@pytest.mark.skipif(sys.platform.startswith("win"), reason="Broken on Windows") +class TestCheckMountDarwin(ffs.TestCase): + # Our faked macos directory + test_dir = "/Volumes/test/dir" + + def setUp(self): + self.setUpPyfakefs() + self.fs.is_macos = True + self.fs.is_case_sensitive = False + self.fs.path_separator = "/" + self.fs.create_dir(self.test_dir, perm_bits=755) + # Verify the fake filesystem does its thing + assert os.path.exists(self.test_dir) is True + + @set_platform("darwin") + def test_bare_mountpoint_darwin(self): + assert filesystem.check_mount("/Volumes") is True + assert filesystem.check_mount("/Volumes/") is True + + @set_platform("darwin") + def test_existing_dir_darwin(self): + assert filesystem.check_mount("/Volumes/test") is True + assert filesystem.check_mount("/Volumes/test/dir/") is True + # Filesystem is set case-insensitive for this platform + assert filesystem.check_mount("/VOLUMES/test") is True + assert filesystem.check_mount("/volumes/Test/dir/") is True + + @set_platform("darwin") + # Cut down a bit on the waiting time + @set_config({"wait_ext_drive": 1}) + def test_dir_nonexistent_darwin(self): + # Within /Volumes + assert filesystem.check_mount("/Volumes/nosuchdir") is False # Issue #1457 + assert filesystem.check_mount("/Volumes/noSuchDir/") is False + assert filesystem.check_mount("/Volumes/nosuchDIR/subdir") is False + assert filesystem.check_mount("/Volumes/NOsuchdir/subdir/") is False + + @set_platform("darwin") + def test_dir_outsider_darwin(self): + # Outside of /Volumes + assert filesystem.check_mount("/test/that/") is True + # Root directory + assert filesystem.check_mount("/") is True + + +class TestCheckMountWin(ffs.TestCase): + # Our faked windows directory + test_dir = r"F:\test\dir" + + def setUp(self): + self.setUpPyfakefs() + self.fs.is_windows_fs = True + self.fs.is_case_sensitive = False + self.fs.path_separator = "\\" + self.fs.create_dir(self.test_dir) + # Sanity check the fake filesystem + assert os.path.exists(self.test_dir) is True + + @set_platform("win32") + def test_existing_dir_win(self): + assert filesystem.check_mount("F:\\test") is True + assert filesystem.check_mount("F:\\test\\dir\\") is True + # Filesystem and drive letters are case-insensitive on this platform + assert filesystem.check_mount("f:\\Test") is True + assert filesystem.check_mount("f:\\test\\DIR\\") is True + + @set_platform("win32") + def test_bare_mountpoint_win(self): + assert filesystem.check_mount("F:\\") is True + assert filesystem.check_mount("Z:\\") is False + + @set_platform("win32") + def test_dir_nonexistent_win(self): + # The existance of the drive letter is what really matters + assert filesystem.check_mount("F:\\NoSuchDir") is True + assert filesystem.check_mount("F:\\NoSuchDir\\") is True + assert filesystem.check_mount("F:\\NOsuchdir\\subdir") is True + assert filesystem.check_mount("F:\\nosuchDIR\\subdir\\") is True + + @set_platform("win32") + # Cut down a bit on the waiting time + @set_config({"wait_ext_drive": 1}) + def test_dir_on_nonexistent_drive_win(self): + # Non-existent drive-letter + assert filesystem.check_mount("H:\\NoSuchDir") is False + assert filesystem.check_mount("E:\\NoSuchDir\\") is False + assert filesystem.check_mount("L:\\NOsuchdir\\subdir") is False + assert filesystem.check_mount("L:\\nosuchDIR\\subdir\\") is False + + @set_platform("win32") + def test_dir_outsider_win(self): + # Outside the local filesystem + assert filesystem.check_mount("//test/that/") is True + + +class TestTrimWinPath: + @set_platform("win32") + def test_short_path(self): + assert filesystem.trim_win_path(r"C:\short\path") == r"C:\short\path" + + @pytest.mark.xfail(sys.platform == "win32", reason="Bug in trim_win_path") + @set_platform("win32") + def test_long_path_short_segments(self): + test_path = "C:\\" + "A" * 20 + "\\" + "B" * 20 + "\\" + "C" * 20 # Strlen 65 + # Current code causes the path to end up with strlen 70 rather than 69 on Windows + assert filesystem.trim_win_path(test_path + "\\" + ("D" * 20)) == test_path + "\\" + "D" * 3 + + +@pytest.mark.skipif(sys.platform.startswith("win"), reason="Broken on Windows") +class TestRecursiveListdir(ffs.TestCase): + # Basic fake filesystem setup stanza + def setUp(self): + self.setUpPyfakefs() + self.fs.path_separator = "/" + self.fs.is_case_sensitive = True + + def test_nonexistent_dir(self): + assert filesystem.recursive_listdir("/foo/bar") == [] + + def test_no_exceptions(self): + test_files = ( + "/test/dir/file1.ext", + "/test/dir/file2", + "/test/dir/sub/sub/sub/dir/file3.ext", + ) + for file in test_files: + self.fs.create_file(file) + assert os.path.exists(file) is True + # List our fake directory structure + results_subdir = filesystem.recursive_listdir("/test/dir") + assert len(results_subdir) == 3 + for entry in test_files: + assert (entry in results_subdir) is True + + # List the same directory again, this time using its parent as the function argument. + # Results should be identical, since there's nothing in /test but that one subdirectory + results_parent = filesystem.recursive_listdir("/test") + # Don't make assumptions about the sorting of the lists of results + results_parent.sort() + results_subdir.sort() + assert results_parent == results_subdir + + # List that subsubsub-directory; no sorting required for a single result + assert filesystem.recursive_listdir("/test/dir/sub/sub") == ["/test/dir/sub/sub/sub/dir/file3.ext"] + + def test_exception_appledouble(self): + # Anything below a .AppleDouble directory should be omitted + test_file = "/foo/bar/.AppleDouble/Oooooo.ps" + self.fs.create_file(test_file) + assert os.path.exists(test_file) is True + assert filesystem.recursive_listdir("/foo") == [] + assert filesystem.recursive_listdir("/foo/bar") == [] + assert filesystem.recursive_listdir("/foo/bar/.AppleDouble") == [] + + def test_exception_dsstore(self): + # Anything below a .DS_Store directory should be omitted + for file in ( + "/some/FILE", + "/some/.DS_Store/oh.NO", + "/some/.DS_Store/subdir/The.End", + ): + self.fs.create_file(file) + assert os.path.exists(file) is True + assert filesystem.recursive_listdir("/some") == ["/some/FILE"] + assert filesystem.recursive_listdir("/some/.DS_Store/") == [] + assert filesystem.recursive_listdir("/some/.DS_Store/subdir") == [] + + def test_invalid_file_argument(self): + # This is obviously not intended use; the function expects a directory + # as its argument, not a file. Test anyway. + test_file = "/dev/sleepy" + self.fs.create_file(test_file) + assert os.path.exists(test_file) is True + assert filesystem.recursive_listdir(test_file) == [] + + +class TestRecursiveListdirWin(ffs.TestCase): + # Basic fake filesystem setup stanza + @set_platform("win32") + def setUp(self): + self.setUpPyfakefs() + self.fs.is_windows_fs = True + self.fs.path_separator = "\\" + self.fs.is_case_sensitive = False + + def test_nonexistent_dir(self): + assert filesystem.recursive_listdir(r"F:\foo\bar") == [] + + def test_no_exceptions(self): + test_files = ( + r"f:\test\dir\file1.ext", + r"f:\test\dir\file2", + r"f:\test\dir\sub\sub\sub\dir\file3.ext", + ) + for file in test_files: + self.fs.create_file(file) + assert os.path.exists(file) is True + # List our fake directory structure + results_subdir = filesystem.recursive_listdir(r"f:\test\dir") + assert len(results_subdir) == 3 + for entry in test_files: + assert (entry in results_subdir) is True + + # List the same directory again, this time using its parent as the function argument. + # Results should be identical, since there's nothing in /test but that one subdirectory + results_parent = filesystem.recursive_listdir(r"f:\test") + # Don't make assumptions about the sorting of the lists of results + results_parent.sort() + results_subdir.sort() + assert results_parent == results_subdir + + # List that subsubsub-directory; no sorting required for a single result + assert ( + filesystem.recursive_listdir(r"F:\test\dir\SUB\sub")[0].lower() == r"f:\test\dir\sub\sub\sub\dir\file3.ext" + ) + + def test_exception_appledouble(self): + # Anything below a .AppleDouble directory should be omitted + test_file = r"f:\foo\bar\.AppleDouble\Oooooo.ps" + self.fs.create_file(test_file) + assert os.path.exists(test_file) is True + assert filesystem.recursive_listdir(r"f:\foo") == [] + assert filesystem.recursive_listdir(r"f:\foo\bar") == [] + assert filesystem.recursive_listdir(r"F:\foo\bar\.AppleDouble") == [] + + def test_exception_dsstore(self): + # Anything below a .DS_Store directory should be omitted + for file in ( + r"f:\some\FILE", + r"f:\some\.DS_Store\oh.NO", + r"f:\some\.DS_Store\subdir\The.End", + ): + self.fs.create_file(file) + assert os.path.exists(file) is True + assert filesystem.recursive_listdir(r"f:\some") == [r"f:\some\FILE"] + assert filesystem.recursive_listdir(r"f:\some\.DS_Store") == [] + assert filesystem.recursive_listdir(r"f:\some\.DS_Store\subdir") == [] + + def test_invalid_file_argument(self): + # This is obviously not intended use; the function expects a directory + # as its argument, not a file. Test anyway. + test_file = r"f:\dev\sleepy" + self.fs.create_file(test_file) + assert os.path.exists(test_file) is True + assert filesystem.recursive_listdir(test_file) == [] + + +@pytest.mark.skipif(sys.platform.startswith("win"), reason="Broken on Windows") +class TestGetUniquePathFilename(ffs.TestCase): + # Basic fake filesystem setup stanza + def setUp(self): + self.setUpPyfakefs() + self.fs.path_separator = "/" + self.fs.is_case_sensitive = True + + # Reduce the waiting time when the function calls check_mount() + @set_config({"wait_ext_drive": 1}) + def test_nonexistent_dir(self): + # Absolute path + assert filesystem.get_unique_path("/foo/bar", n=0, create_dir=False) == "/foo/bar" + # Absolute path in a location that matters to check_mount + assert filesystem.get_unique_path("/mnt/foo/bar", n=0, create_dir=False) == "/mnt/foo/bar" + # Relative path + if self.fs.cwd != "/": + os.chdir("/") + assert filesystem.get_unique_path("foo/bar", n=0, create_dir=False) == "foo/bar" + + def test_creating_dir(self): + # First call also creates the directory for us + assert filesystem.get_unique_path("/foo/bar", n=0, create_dir=True) == "/foo/bar" + # Verify creation of the path + assert os.path.exists("/foo/bar") is True + # Directories from previous loops get in the way + for dir_n in range(1, 11): # Go high enough for double digits + assert filesystem.get_unique_path("/foo/bar", n=0, create_dir=True) == "/foo/bar." + str(dir_n) + assert os.path.exists("/foo/bar." + str(dir_n)) is True + # Explicitly set parameter n + assert filesystem.get_unique_path("/foo/bar", n=666, create_dir=True) == "/foo/bar.666" + assert os.path.exists("/foo/bar.666") is True + + def test_nonexistent_file(self): + assert filesystem.get_unique_filename("/dir/file.name") == "/dir/file.name" + # Relative path + assert filesystem.get_unique_filename("dir/file.name") == "dir/file.name" + + def test_existing_file(self): + test_file = "/dir/file.name" + max_obstruct = 11 # High enough for double digits + self.fs.create_file(test_file) + assert os.path.exists(test_file) + # Create obstructions + for n in range(1, max_obstruct): + file_n = "/dir/file." + str(n) + ".name" + self.fs.create_file(file_n) + assert os.path.exists(file_n) + assert filesystem.get_unique_filename(test_file) == "/dir/file." + str(max_obstruct) + ".name" + + def test_existing_file_without_extension(self): + test_file = "/some/filename" + # Create obstructions + self.fs.create_file(test_file) + assert os.path.exists(test_file) + assert filesystem.get_unique_filename(test_file) == "/some/filename.1" + + +class TestGetUniquePathFilenameWin(ffs.TestCase): + # Basic fake filesystem setup stanza + @set_platform("win32") + def setUp(self): + self.setUpPyfakefs() + self.fs.is_windows_fs = True + self.fs.path_separator = "\\" + self.fs.is_case_sensitive = False + + # Reduce the waiting time when the function calls check_mount() + @set_config({"wait_ext_drive": 1}) + def test_nonexistent_dir(self): + # Absolute path + assert filesystem.get_unique_path(r"C:\No\Such\Dir", n=0, create_dir=False).lower() == r"c:\no\such\dir" + # Relative path + assert filesystem.get_unique_path(r"foo\bar", n=0, create_dir=False).lower() == r"foo\bar" + + def test_creating_dir(self): + # First call also creates the directory for us + assert filesystem.get_unique_path(r"C:\foo\BAR", n=0, create_dir=True).lower() == r"c:\foo\bar" + # Verify creation of the path + assert os.path.exists(r"c:\foo\bar") is True + # Directories from previous loops get in the way + for dir_n in range(1, 11): # Go high enough for double digits + assert filesystem.get_unique_path(r"c:\foo\bar", n=0, create_dir=True) == r"c:\foo\bar." + str(dir_n) + assert os.path.exists(r"c:\foo\bar." + str(dir_n)) is True + # Explicitly set parameter n + assert filesystem.get_unique_path(r"c:\Foo\Bar", n=666, create_dir=True).lower() == r"c:\foo\bar.666" + assert os.path.exists(r"c:\foo\bar.666") is True + + def test_nonexistent_file(self): + assert filesystem.get_unique_filename(r"C:\DIR\file.name").lower() == r"c:\dir\file.name" + # Relative path + assert filesystem.get_unique_filename(r"DIR\file.name").lower() == r"dir\file.name" + + def test_existing_file(self): + test_file = r"C:\dir\file.name" + max_obstruct = 11 # High enough for double digits + self.fs.create_file(test_file) + assert os.path.exists(test_file) + # Create obstructions + for n in range(1, max_obstruct): + file_n = r"C:\dir\file." + str(n) + ".name" + self.fs.create_file(file_n) + assert os.path.exists(file_n) + assert filesystem.get_unique_filename(test_file).lower() == r"c:\dir\file." + str(max_obstruct) + ".name" + + def test_existing_file_without_extension(self): + test_file = r"c:\some\filename" + # Create obstructions + self.fs.create_file(test_file) + assert os.path.exists(test_file) + assert filesystem.get_unique_filename(test_file).lower() == r"c:\some\filename.1" + + +class TestSetPermissionsWin(ffs.TestCase): + @set_platform("win32") + def test_win32(self): + # Should not do or return anything on Windows + assert filesystem.set_permissions(r"F:\who\cares", recursive=False) is None + + +@pytest.mark.skipif(sys.platform.startswith("win"), reason="Broken on Windows") +class TestSetPermissions(ffs.TestCase): + # Basic fake filesystem setup stanza + def setUp(self): + self.setUpPyfakefs() + self.fs.path_separator = "/" + self.fs.is_case_sensitive = True + self.fs.umask = int("0755", 8) # rwxr-xr-x + + def _runner(self, perms_test, perms_after): + """ + Generic test runner for permissions testing. The umask is set per test + via the relevant sab config option; the fileystem parameter in setUp(). + Note that the umask set in the environment before starting the program + also affects the results if sabnzbd.cfg.umask isn't set. + + Arguments: + str perms_test: permissions for test objects, chmod style "0755". + str perms_after: expected permissions after completion of the test. + """ + perms_test = int(perms_test, 8) + if sabnzbd.cfg.umask(): + perms_after = int(perms_after, 8) + else: + perms_after = int("0777", 8) & (sabnzbd.ORG_UMASK ^ int("0777", 8)) + + # Setup and verify fake dir + test_dir = "/test" + try: + self.fs.create_dir(test_dir, perms_test) + except PermissionError: + ffs.set_uid(0) + self.fs.create_dir(test_dir, perms_test) + assert os.path.exists(test_dir) is True + assert stat.filemode(os.stat(test_dir).st_mode) == "d" + stat.filemode(perms_test)[1:] + + # Setup and verify fake files + for file in ( + "foobar", + "file.ext", + "sub/dir/.nzb", + "another/sub/dir/WithSome.File", + ): + file = os.path.join(test_dir, file) + try: + self.fs.create_file(file, perms_test) + except PermissionError: + try: + ffs.set_uid(0) + self.fs.create_file(file, perms_test) + except Exception: + # Skip creating files, if not even using root gets the job done. + break + assert os.path.exists(file) is True + assert stat.filemode(os.stat(file).st_mode)[1:] == stat.filemode(perms_test)[1:] + + # Set permissions, recursive by default + filesystem.set_permissions(test_dir) + + # Check the results + for root, dirs, files in os.walk(test_dir): + for dir in [os.path.join(root, d) for d in dirs]: + # Permissions on directories should now match perms_after + assert stat.filemode(os.stat(dir).st_mode) == "d" + stat.filemode(perms_after)[1:] + for file in [os.path.join(root, f) for f in files]: + # Files also shouldn't have any executable or special bits set + assert ( + stat.filemode(os.stat(file).st_mode)[1:] + == stat.filemode( + perms_after & ~(stat.S_ISUID | stat.S_ISGID | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH) + )[1:] + ) + + # Cleanup + ffs.set_uid(0) + self.fs.remove_object(test_dir) + assert os.path.exists(test_dir) is False + ffs.set_uid(global_uid) + + @set_platform("linux") + @set_config({"umask": ""}) + def test_dir0777_empty_umask_setting(self): + # World writable directory + self._runner("0777", "0700") + + @set_platform("linux") + @set_config({"umask": ""}) + def test_dir0450_empty_umask_setting(self): + # Insufficient access + self._runner("0450", "0700") + + @set_platform("linux") + @set_config({"umask": ""}) + def test_dir0000_empty_umask_setting(self): + # Weird directory permissions + self._runner("0000", "0700") + + @set_platform("linux") + @set_config({"umask": "0760"}) + def test_dir0777_umask0760_setting(self): + # World-writable directory, umask 760 + self._runner("0777", "0760") + + @set_platform("linux") + @set_config({"umask": "0617"}) + def test_dir0450_umask0617_setting(self): + # Insufficient access, weird umask + self._runner("0450", "0717") + + @set_platform("linux") + @set_config({"umask": "0000"}) + def test_dir0405_umask0000_setting(self): + # Insufficient access on all fronts, weird umask + self._runner("0405", "0700") + + @set_platform("linux") + @set_config({"umask": "2455"}) + def test_dir0444_umask2455_setting(self): + # Insufficient access, weird umask with setgid + self._runner("0444", "2755") + + @set_platform("linux") + @set_config({"umask": "4755"}) + def test_dir1755_umask4755_setting(self): + # Sticky bit on directory, umask with setuid + self._runner("1755", "4755")