Skip to content

fix: CLI directory parsing, glob separator and case matching, MFT extension records - #9

Merged
lperezmo merged 2 commits into
masterfrom
fix/cli-dir-glob-semantics-mft-ext-records
Sep 28, 2026
Merged

lperezmo merged 2 commits into
masterfrom
fix/cli-dir-glob-semantics-mft-ext-records

Conversation

@lperezmo

@lperezmo lperezmo commented Sep 28, 2026 •

Copy link
Copy Markdown
Owner

Summary

Three confirmed bugs and one MFT correctness fix.

1. CLI: directory after --ext / --names was silently ignored

--ext and --names are nargs="+", so pyofiles find --ext py D:\docs read D:\docs as a second extension and searched . instead, with no error.

  • A trailing value that contains a path separator (/, or also \ on Windows) is now used as the directory when none was given. File names cannot contain a separator, so these values could never match anything. ., .. and colons are not treated as paths: they are legitimate --names substrings (and on POSIX backslashes are legal in names).
  • A value with a separator anywhere else in those lists is now an argparse error (exit 2), including when a directory was already given.
  • If the last of two or more values is a bare name that happens to be an existing directory (--ext py src), it stays a filter and the CLI prints a note with the corrected command. A single value is left alone (--ext js next to a js/ folder is a normal command).
  • New cli.parse_args(argv) wraps build_parser() plus the resolution step; main() uses it.

2. glob: * crossed directories

globset's default literal_separator(false) meant "*.py" matched every .py recursively and "src/*.py" matched src/deep/b.py. Now *, ? and [...] stay within one component and only ** recurses, matching glob.glob. This changes behavior for anyone relying on * recursing; **/ is the fix on their side.

3. glob: case-sensitive on Windows

glob(d, "*.pdf") missed REPORT.PDF while find(extensions=["pdf"]) found it. Matching is now case-insensitive on Windows and case-sensitive elsewhere.

4. MFT: possible duplicate entries from extension records

When a file's $FILE_NAME attributes spill into an extension record (many hard links), the base record already reports them through $ATTRIBUTE_LIST, and parse_record then emitted them again from the extension record with size 0 and no timestamps. Only base records carry $STANDARD_INFORMATION, so records without it are now skipped. The ntfs crate does not expose base_file_record, hence this check. It can't be exercised locally without elevation, so a new elevated Windows CI step creates a file with 30 long-named hard links (forcing $FILE_NAME into extension records) and asserts every link comes back exactly once with its real size.

Tests

  • New pytest coverage: glob separator semantics, platform glob case, CLI directory recovery (6 subcommand shapes), default ., explicit directory untouched, path-like rejection, and the ambiguous-name note.
  • Windows glob now resolves literal prefix components to their on-disk casing, so SRC/*.py returns src\... paths.
  • Local (Windows, non-elevated): cargo test --lib 9 passed; pytest 112 passed, 3 skipped (MFT/elevation-gated).
  • README documents glob semantics and CLI argument order.

Second commit addresses an adversarial review pass: ./.. --names regression, over-eager stderr note, glob prefix casing, and a CI MFT check that could not fail.

…ension records

- cli: a directory typed after --ext/--names was swallowed as a filter
  value and the search silently ran on ".". A trailing path-like value
  (contains a separator, or is ".", "..", "D:") is now used as the
  directory; path-like filter values anywhere else are an error; a bare
  existing directory name keeps its filter meaning but prints a note.
- glob: `*`, `?` and `[...]` no longer cross path separators (only `**`
  does), matching glob.glob. Matching is case-insensitive on Windows.
- mft: skip records without $STANDARD_INFORMATION. Extension records can
  hold spilled-over $FILE_NAME attributes that the base record already
  reports through its $ATTRIBUTE_LIST, which produced duplicate entries
  with zero size and no timestamps. CI now asserts no duplicate paths.
- cli: only a platform path separator marks a filter value as a path.
  "." and ".." are legitimate --names substrings again (they were
  rejected, a regression), and on POSIX backslashes and colons are legal
  file name characters.
- cli: the "put it first" note needs at least two values, so a single
  filter that happens to match a folder (`--ext js` next to js/) is quiet,
  and the suggested command now keeps the remaining filter values.
- glob: on Windows, resolve literal prefix components to their on-disk
  spelling so a pattern typed as "SRC/*.py" returns src\... paths.
- ci: the old duplicate check could not fail on System32\drivers. Create
  a file with 30 long-named hard links, which forces $FILE_NAME into
  extension records, and assert every link appears once with its size.
@lperezmo
lperezmo merged commit 46c068b into master Sep 28, 2026
13 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant