The key was sha256+version only, so interpreting the same bytes under a
different --format (sc32 vs sc64) could serve the other variant's
stack/tight/decoded results. The CLI already disables caching for
non-auto formats, but API callers passing cache_dir were unprotected.
Format is now part of the key, so variants never collide.
load() probed the cache entry with path.is_file() unguarded, but on
Python 3.8+ PermissionError from stat() through a locked cache directory
is not swallowed (only ENOENT-like errnos are), so it crashed the
analysis instead of treating the entry as a miss.
FLOSS_CACHE_ENABLE=False or FLOSS_CACHE_REFRESH=True were evaluated
verbatim, so the uppercase variants silently did nothing. Normalize the
value with .lower() so 0/false/no/n and 1/true/yes/y work regardless of
capitalization.
materialize() filtered the document to the requested -n but left
metadata.min_length at the cached extraction threshold, so --json on a
hit (e.g. -n 6 against a -n 4 entry) advertised min_length: 4. Set it to
the requested value, matching what load() does for user-supplied
documents and what a fresh run reports.
cache_dir.mkdir() in store() and tempfile.mkstemp() in _write_atomic()
ran outside the OSError guard: an unwritable cache directory or a full
disk raised straight through and aborted the analysis. Both now log a
warning and skip caching.
Deleting a cache entry manually is the only forced-refresh path today, and
FLOSS_CACHE_ENABLE=0 disables reads and writes entirely. FLOSS_CACHE_REFRESH=1
bypasses the cache on the current run and overwrites the entry with the
fresh document, documented in --help alongside the other envars.
the new materialize tag tests asserted layout.strings without proving the
layout is present, failing the CI mypy check. assert layout is not None
first so the union narrows.
covers() treated surplus tag data as a miss: if not wanted.enable_tags and
cached.analysis.enable_tags was a miss, forcing a full re-analysis just
because the cache holds more than requested. Tag false-positive cleanup
(remove_false_positive_lib_strings) only redacts tags, never strings, so
the cached data is a faithful superset.
covers() now accepts it, and materialize() redacts the tags across the
static, language, and layout-tree strings when tags are not wanted.
load() removed stale entries with path.unlink() and store() replaced the
entry with os.replace(), neither guarded. On Windows a cache file held
open by another process or an antivirus scanner raises PermissionError,
which propagated and aborted the analysis.
Writes and removals are now best-effort: _write_atomic() logs and skips
on any OSError during mkstemp/write/replace and cleans up the temporary
file, and _drop_cache_entry() logs and continues when an entry cannot be
removed.
A cache hit for a document that holds more string types than the user
requested previously left those arrays populated: covers() treats a
superset as a hit, but materialize() only flipped the analysis flags.
With --json the output then contained data a fresh run would omit.
materialize() now mirrors a fresh run: disabled types are cleared from
the document (static also drops the layout that holds them) and the
enable_layout/enable_tags flags are synced to the requested analysis.
covers() no longer treats a cached layout as a miss for a no-layout
request: materialize() now drops the layout when it is not wanted. Tags
remain a miss because a tags-enabled document has already had
false-positive strings removed. Move the --analyze-functions comment
inline with the caching guard it documents.
msvcrt.locking cannot lock a byte range past EOF, so the lock on an
empty lock file always failed on Windows and store() never wrote a cache
entry. Ensure the lock file has at least one byte before locking.
test_get_cache_dir_default asserted the default path ends with 'floss',
but the Windows default is %LOCALAPPDATA%\floss\Cache. Assert it
equals platformdirs' user_cache_dir('floss') instead.