diff --git a/diceware/wordlist.py b/diceware/wordlist.py index 246b5fb4ed11877c030cfd86374f22ded4de9d8e..787cfc4d614d0c51c1b5e896c9bc2677cd1ac121 100644 --- a/diceware/wordlist.py +++ b/diceware/wordlist.py @@ -39,6 +39,37 @@ RE_VALID_WORDLIST_FILENAME = re.compile( r'^wordlist_([\w-]+)\.[\w][\w\.]+[\w]+$') +def get_wordlist_dirs(): + """Get the directories in which wordlists can be stored. + + We look into the following dirs (in that order): + (1) Local `wordlists` dir (part of install) + (2a) ${XDG_DATA_HOME}/diceware/ (if $XDG_DATA_HOME is defined) + (2b) ${HOME}/.local/share/diceware/ (else) + (3a) `<DIR>/diceware/` for each <DIR> in ${XDG_DATA_DIRS} + (if ${XDG_DATA_DIRS} is defined) + (3b) /usr/local/share/diceware/, /usr/share/diceware/ + (else) + """ + xdg_data_dirs = os.getenv("XDG_DATA_DIRS") + if not xdg_data_dirs: # unset or empty string + xdg_data_dirs = "/usr/local/share:/usr/share" + user_home = os.path.expanduser("~") + xdg_data_home = os.getenv("XDG_DATA_HOME", "") + if (xdg_data_home == "") and (user_home != "~"): + xdg_data_home = os.path.join(user_home, ".local", "share") + if xdg_data_home: + xdg_data_dirs = "%s:%s" % (xdg_data_home, xdg_data_dirs) + local_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "wordlists")) + result = [local_dir] + list( + [ + os.path.join(os.path.abspath(path), "diceware") + for path in xdg_data_dirs.split(":") + ] + ) + return result + + def get_wordlists_dir(): """Get the directory in which word lists are stored. """ @@ -50,15 +81,18 @@ def get_wordlist_names(): """Get a all names of wordlists stored locally. """ result = [] - wordlists_dir = get_wordlists_dir() - filenames = os.listdir(wordlists_dir) - for filename in filenames: - if not os.path.isfile(os.path.join(wordlists_dir, filename)): - continue - match = RE_VALID_WORDLIST_FILENAME.match(filename) - if not match: + # wordlists_dir = get_wordlists_dir() + for wordlists_dir in get_wordlist_dirs(): + if not os.path.isdir(wordlists_dir): continue - result.append(match.groups()[0]) + filenames = os.listdir(wordlists_dir) + for filename in filenames: + if not os.path.isfile(os.path.join(wordlists_dir, filename)): + continue + match = RE_VALID_WORDLIST_FILENAME.match(filename) + if not match: + continue + result.append(match.groups()[0]) return sorted(result) @@ -74,13 +108,15 @@ def get_wordlist_path(name): """ if not RE_WORDLIST_NAME.match(name): raise ValueError("Not a valid wordlist name: %s" % name) - wordlists_dir = get_wordlists_dir() - for filename in os.listdir(wordlists_dir): - if not os.path.isfile(os.path.join(wordlists_dir, filename)): + for wordlists_dir in get_wordlist_dirs(): + if not os.path.isdir(wordlists_dir): continue - match = RE_VALID_WORDLIST_FILENAME.match(filename) - if match and match.groups()[0] == name: - return os.path.join(wordlists_dir, filename) + for filename in os.listdir(wordlists_dir): + if not os.path.isfile(os.path.join(wordlists_dir, filename)): + continue + match = RE_VALID_WORDLIST_FILENAME.match(filename) + if match and match.groups()[0] == name: + return os.path.join(wordlists_dir, filename) class WordList(object): diff --git a/tests/conftest.py b/tests/conftest.py index d9c7fb60608668c8f18b94aca9d1f74d25ccb5d5..7df11236902ca2eb7a2ddf65ff95d603c4422222 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -56,7 +56,7 @@ def wordlists_dir(request, monkeypatch, tmpdir): """This fixture provides a temporary wordlist dir. """ monkeypatch.setattr( - "diceware.wordlist.get_wordlists_dir", lambda: str(tmpdir)) + "diceware.wordlist.get_wordlist_dirs", lambda: [str(tmpdir)]) return tmpdir @@ -85,6 +85,8 @@ def change_home(monkeypatch, tmpdir): monkeypatch.setenv("HOME", str(tmpdir)) monkeypatch.delenv("XDG_CONFIG_DIRS", raising=False) monkeypatch.delenv("XDG_CONFIG_HOME", raising=False) + monkeypatch.delenv("XDG_DATA_DIRS", raising=False) + monkeypatch.delenv("XDG_DATA_HOME", raising=False) return tmpdir diff --git a/tests/test_wordlist.py b/tests/test_wordlist.py index 6dd5578b706b6a823cc2624c43bf1b8d1019bca6..14c23573ccdac98f213991a3a8d39077b4bc3222 100644 --- a/tests/test_wordlist.py +++ b/tests/test_wordlist.py @@ -4,9 +4,9 @@ import pytest import sys from io import StringIO from diceware.wordlist import ( - get_wordlists_dir, RE_WORDLIST_NAME, RE_NUMBERED_WORDLIST_ENTRY, - RE_VALID_WORDLIST_FILENAME, get_wordlist_path, get_wordlist_names, - WordList, + get_wordlist_dirs, get_wordlists_dir, RE_WORDLIST_NAME, + RE_NUMBERED_WORDLIST_ENTRY, RE_VALID_WORDLIST_FILENAME, get_wordlist_path, + get_wordlist_names, WordList, ) @@ -22,6 +22,32 @@ def wordlist(request, tmpdir): class TestWordlistModule(object): + def test_get_wordlist_dirs(self, home_dir): + # We can get a list of valid wordlist dirs even w/o home + mydir = os.path.abspath(os.path.dirname(__file__)) + local_wlist_dir = os.path.join(os.path.dirname(mydir), "diceware", "wordlists") + assert get_wordlist_dirs() == [ + local_wlist_dir, + str(home_dir / ".local" / "share" / "diceware"), + "/usr/local/share/diceware", + "/usr/share/diceware", + ] + + def test_get_wordlist_dirs_considers_xdg_data_home(self, home_dir, monkeypatch): + # We consider $XDG_DATA_HOME when determining dirs + monkeypatch.setenv("XDG_DATA_HOME", str(home_dir)) + wlists = get_wordlist_dirs() + assert str(home_dir / "diceware") in wlists + assert str(home_dir / ".local" / "share" / "diceware") not in wlists + + def test_get_wordlist_dirs_considers_xdg_data_dirs(self, home_dir, monkeypatch): + # We consider $XDG_DATA_DIRS when determining dirs + monkeypatch.setenv("XDG_DATA_DIRS", "/foo:/bar") + wlists = get_wordlist_dirs() + assert "/usr/share/diceware" not in wlists + assert "/foo/diceware" == wlists[-2] + assert "/bar/diceware" == wlists[-1] + def test_re_wordlist_name(self): # RE_WORDLIST_NAME really works # valid stuff @@ -129,6 +155,15 @@ class TestWordlistModule(object): assert exc_info.value.args[0].startswith( 'Not a valid wordlist name') + def test_get_wordlist_path_copes_w_nonexistant_dirs(self, wordlists_dir, monkeypatch): + path1 = wordlists_dir.join("wordlist_foo.txt") + path1.write("foo\n") + assert get_wordlist_path("foo") == path1 + # now we remove the wordlist and its path + path1.remove() + wordlists_dir.remove() + assert get_wordlist_path("foo") is None + def test_get_wordlist_names(self, wordlists_dir): # we can get wordlist names also if directory is empty. wlist_path = wordlists_dir.join('wordlist_my_en.txt')