Raise on non-KDL files in the contacts directory
Dotfiles are exempt so Finder dropping a .DS_Store into the directory
cannot take the address book down; kdl files with unfetchable ids are
skipped with a Sentry capture.
Assisted-by: GLM-5.3 via pi
diff --git a/lib/pro_tacts/contacts.rb b/lib/pro_tacts/contacts.rb
index 6f85def..1ac28d7 100644
--- a/lib/pro_tacts/contacts.rb
+++ b/lib/pro_tacts/contacts.rb
@@ -12,7 +12,8 @@ module ProTacts
Contact = Data.define(:id, :vcard)
# IDs end up in paths, and they arrive from client-supplied hrefs, so
- # anything outside this charset simply does not exist.
+ # anything outside this charset simply does not exist. Enforced at
+ # load too, so everything listed is fetchable by that id.
ID_FORMAT = /\A[\w-]+\z/
attr_reader :directory
@@ -24,7 +25,19 @@ module ProTacts
end
end
+ # An empty directory is a valid empty address book. Anything in it
+ # that is not a .kdl file means a misplaced file or a wrong
+ # PRO_TACTS_CONTACTS_DIR, so it raises rather than quietly serving a
+ # partial address book. Dotfiles are exempt: Finder drops .DS_Store
+ # into any directory it opens.
def all
+ unexpected = directory.children
+ .reject { it.basename.to_s.start_with?(".") }
+ .reject { it.extname == ".kdl" }
+ unless unexpected.empty?
+ raise ArgumentError, "unexpected non-KDL file in contacts directory: #{unexpected.first}"
+ end
+
directory.glob("*.kdl").map { load(it) }.compact
end
@@ -43,6 +56,10 @@ module ProTacts
# so tests need no DSN.
def load(path)
id = path.basename(".kdl").to_s
+ unless id.match?(ID_FORMAT)
+ raise ArgumentError, "invalid contact id: #{id}"
+ end
+
nodes = KDL.parse(path.read).nodes
unless nodes.length == 1 && nodes.first.name == "contact"
raise ArgumentError, "expected exactly one contact node"
diff --git a/test/pro_tacts/test_contacts.rb b/test/pro_tacts/test_contacts.rb
index 83b0923..d4f68f7 100644
--- a/test/pro_tacts/test_contacts.rb
+++ b/test/pro_tacts/test_contacts.rb
@@ -68,6 +68,36 @@ class ContactsTest < Minitest::Test
end
end
+ def test_non_kdl_files_raise
+ with_contacts({
+ "aiden.kdl" => "contact { name \"Aiden\" }",
+ "notes.txt" => "hello",
+ }) do |contacts|
+ error = assert_raises(ArgumentError) { contacts.all }
+
+ assert_match(/non-KDL file/, error.message)
+ assert_match(/notes\.txt/, error.message)
+ end
+ end
+
+ def test_dotfiles_are_ignored
+ with_contacts({
+ ".DS_Store" => "junk",
+ "aiden.kdl" => "contact { name \"Aiden\" }",
+ }) do |contacts|
+ assert_equal %w[aiden], contacts.all.map { it.id }
+ end
+ end
+
+ def test_files_whose_id_cannot_be_fetched_are_skipped
+ with_contacts({
+ "John Smith.kdl" => "contact { name \"John\" }",
+ "aiden.kdl" => "contact { name \"Aiden\" }",
+ }) do |contacts|
+ assert_equal %w[aiden], contacts.all.map { it.id }
+ end
+ end
+
def test_unparseable_files_are_skipped
with_contacts({
"broken.kdl" => "contact {",