fingerprintd/tests/Store/main.cpp
Jorijn van der Graaf f2a696bbbe Drop the gid 60 compatibility path
gid 60 was never a decision -- it was the ENROLL token's timeout field being
read as a gid, then made self-consistent. Keeping it readable alongside gid =
uid would be a second code path, a second group to reason about, and a
migration to eventually retire, all to avoid re-enrolling one finger on one dev
phone. Re-enrol it instead.
2026-09-02 17:28:47 +02:00

160 lines
6.8 KiB
C++

// SPDX-License-Identifier: GPL-3.0-only
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
// lint-disable-file fixed-width-types
/*
Fingerprintd:Store unit tests.
The map is small, but two of its properties are load-bearing. A corrupt or
partly-unreadable map must degrade to "fewer names known" rather than to a
daemon that refuses to start, because that daemon is what unlocks the phone.
And fid 0 must never be storable or matchable: 0 is what the trustlet writes
into the fid field when authentication FAILS, so a stored 0 would turn every
rejection into a match.
*/
import std;
import Fingerprintd;
using namespace fingerprintd::store;
namespace {
int Failures = 0;
void Check(bool cond, std::string_view msg) {
if (!cond) {
std::println(std::cerr, "FAIL: {}", msg);
++Failures;
}
}
}
int main() {
// ---- fid 0 is the failure sentinel, never an identity
{
Map m;
Check(!m.Add(Finger::RightIndex, 0), "fid 0 is refused");
Check(m.Size() == 0, "and nothing was stored");
Check(!m.Lookup(0).has_value(), "a zero fid never resolves to a finger");
m.Add(Finger::RightIndex, 1296911490);
Check(!m.Lookup(0).has_value(), "still not, with entries present");
// Lookup's own zero guard is unreachable by construction: Add is the
// only way an entry is created and it refuses 0, so the guard is
// defence in depth against a future writer, not a live branch. What
// IS testable is the invariant that makes it unreachable -- assert
// that, including through the decoder, which is the other way entries
// arrive.
Map decoded = Map::Decode("left-thumb 0\nright-thumb 5\nleft-index-finger 0\n");
bool anyZero = false;
for (const Entry& e : decoded.Entries())
if (e.fid == 0) anyZero = true;
Check(!anyZero, "no entry ever holds fid 0, however it was created");
Check(decoded.Size() == 1, "the zero-fid lines were dropped by the decoder");
}
// ---- Names round-trip through fprintd's vocabulary
{
for (auto& [f, name] : FingerNames) {
Check(NameOf(f) == name, std::format("name of {}", name));
auto back = FingerFromName(name);
Check(back && *back == f, std::format("{} round-trips", name));
}
Check(FingerNames.size() == 10, "ten fingers");
Check(!FingerFromName(AnyFinger).has_value(),
"'any' is a verify argument, not a storable finger");
Check(!FingerFromName("").has_value(), "empty name");
Check(!FingerFromName("nose").has_value(), "unknown name");
}
// ---- Add, replace, remove
{
Map m;
Check(m.Add(Finger::RightIndex, 1296911490), "add");
Check(m.Size() == 1 && m.Has(Finger::RightIndex), "stored");
auto f = m.Lookup(1296911490);
Check(f && *f == Finger::RightIndex, "a match resolves to the finger");
// Re-enrolling replaces: the trustlet allocated a new template and the
// old id is not what a match will report any more.
Check(m.Add(Finger::RightIndex, 42), "re-enrol");
Check(m.Size() == 1, "no duplicate entry");
Check(!m.Lookup(1296911490).has_value(), "the stale id no longer resolves");
auto g = m.Lookup(42);
Check(g && *g == Finger::RightIndex, "the new id does");
Check(m.Remove(Finger::RightIndex), "remove");
Check(m.Size() == 0 && !m.Has(Finger::RightIndex), "removed");
Check(!m.Remove(Finger::RightIndex), "removing twice is not an error the second time");
}
// ---- The group limit
{
Map m;
Finger fingers[] = { Finger::LeftThumb, Finger::LeftIndex, Finger::LeftMiddle,
Finger::LeftRing, Finger::LeftLittle };
for (std::size_t i = 0; i < 5; i++)
Check(m.Add(fingers[i], static_cast<std::uint32_t>(100 + i)), "fill the group");
Check(m.Full() && m.Size() == MaxFingersPerGroup, "five is the limit");
Check(!m.Add(Finger::RightThumb, 200), "a sixth finger is refused");
// But replacing one already present still works when full.
Check(m.Add(Finger::LeftThumb, 999), "replacing while full is allowed");
Check(m.Size() == 5, "still five");
}
// ---- Serialisation round-trip
{
Map m;
m.Add(Finger::RightIndex, 1296911490);
m.Add(Finger::LeftThumb, 7);
std::string text = m.Encode();
Check(text.contains("right-index-finger 1296911490"), "encodes the name and id");
Check(text.contains("left-thumb 7"), "encodes the second entry");
Map back = Map::Decode(text);
Check(back.Size() == 2, "decodes both");
auto f = back.Lookup(1296911490);
Check(f && *f == Finger::RightIndex, "round-trips the mapping");
Check(back.Encode() == text, "re-encoding is stable");
}
// ---- A damaged map degrades, it does not fail
{
std::string damaged =
"right-index-finger 1296911490\n" // good
"\n" // blank
"left-thumb\n" // no id
"nose 5\n" // unknown finger
"left-index-finger notanumber\n" // unparsable id
"left-middle-finger 12x\n" // trailing junk
"left-ring-finger 0\n" // the failure sentinel
"right-thumb 8\n"; // good
Map m = Map::Decode(damaged);
Check(m.Size() == 2, "only the two good lines survive");
auto a = m.Lookup(1296911490);
auto b = m.Lookup(8);
Check(a && *a == Finger::RightIndex, "first good entry kept");
Check(b && *b == Finger::RightThumb, "last good entry kept");
Check(!m.Has(Finger::LeftRing), "the zero-fid line was dropped");
Check(!m.Has(Finger::LeftMiddle), "trailing junk rejected, not truncated to 12");
// Total garbage yields an empty map, not a throw.
Map junk = Map::Decode("\0\0\xff nonsense \n\n\n");
Check(junk.Size() == 0, "garbage decodes to empty");
Map empty = Map::Decode("");
Check(empty.Size() == 0, "empty input decodes to empty");
// A final line without a newline is still read.
Map noNl = Map::Decode("right-thumb 3");
Check(noNl.Size() == 1, "a missing trailing newline is not a lost entry");
}
// ---- Groups
{
Check(GidForUid(1000) == 1000, "gid is the uid");
Check(GidForUid(0) == 0, "root maps too");
Check(GidForUid(4294967295u) == 4294967295u, "the whole uid range maps");
Check(PathForUid("/var/lib/fingerprintd", 1000)
== "/var/lib/fingerprintd/fingers-1000.map", "state path");
}
if (Failures == 0) std::println("Store: all tests passed");
return Failures;
}