Skip to content

Commit

Permalink
Fix #84. Allow custom dirs to store wordlists.
Browse files Browse the repository at this point in the history
We now also look into `$XDG_DATA_HOME/diceware/` and other dirs for
wordlists. You can put your own lists for instance into
`~/.local/share/diceware/` and if its name does not overlap with default
lists, it can be used as input file.
  • Loading branch information
ulif committed Aug 15, 2024
1 parent 2e6a72f commit 05565e7
Show file tree
Hide file tree
Showing 3 changed files with 91 additions and 18 deletions.
64 changes: 50 additions & 14 deletions diceware/wordlist.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,37 @@
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.
"""
Expand All @@ -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)


Expand All @@ -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):
Expand Down
4 changes: 3 additions & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -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


Expand Down
41 changes: 38 additions & 3 deletions tests/test_wordlist.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@
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,
)


Expand All @@ -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
Expand Down Expand Up @@ -129,6 +155,15 @@ def test_get_wordlist_path_requires_ascii(self):
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')
Expand Down

0 comments on commit 05565e7

Please sign in to comment.