161 lines
6.9 KiB
C++
161 lines
6.9 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(LegacyGid == 60, "the dev phone's accidental group is remembered");
|
||
|
|
Check(GidForUid(1000) != LegacyGid, "and a real uid does not collide with it");
|
||
|
|
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;
|
||
|
|
}
|