No directory-reading wrapper: opendir, readdir, closedir
#10
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Source: TODO.md §4.1 and "Requested by consumers" (at
669b2b3)There is no
aksl_opendir/aksl_readdir/aksl_closedir, so a consumerthat wants to list a directory has to call
opendir(3)itself and step outsidethe error convention every other call in its file follows -- reporting through
errnowhere everything around it reports through anakerr_ErrorContext *.This one has a consumer asking for it.
akbasichit it implementing CommodoreBASIC's
DIRECTORYverb, and refuses the verb rather than working around it:src/runtime_disk.creports "DIRECTORY is not implemented: libakstdlib has nodirectory-reading wrapper yet".
The shape it wants is the one the
aksl_f*family already has -- a handle outthrough a pointer parameter,
NULLon success,ENOENTandEACCESpropagatedas themselves.
readdir(3)returning NULL for both "end of directory" and "error, checkerrno" is the same conflation
aksl_fgetcandaksl_fgetsalready untangle,and should be untangled the same way:
AKERR_EOFfor the end, the errno for theerror.
Full surface:
opendir,fdopendir,readdir,closedir,rewinddir,scandir.Tests would want: a directory that does not exist, one that cannot be
searched, an empty one, and one whose entries outlast a single read -- plus
whatever the
d_typeportability story turns out to be, since not everyfilesystem fills it in.
Files:
src/,include/akstdlib.h; consumer:akbasic/src/runtime_disk.cFiled by Tachikoma (Claude Code, Opus 5, 1M context)
@tachikoma let's get a better plan on this
Plan
This one has a consumer and a settled shape. What is left is four decisions, three
of which I think are already answered by conventions the library follows
elsewhere. Steps:
readdir(3)'s NULLscandirout — it is blocked on something elsed_typeis allowed to meanHow an entry reaches the caller
readdir(3)returns a pointer into theDIR's own storage, invalidated by thenext
readdirorclosediron that handle. Two shapes:struct dirent *struct dirent *aksl_fgetsalready follow.Recommendation: copy into caller-supplied storage.
struct direntis acomplete type, so
*dest = *entis well-formed. The caveat to write down ratherthan discover: POSIX leaves
d_name's array size unspecified, so the copy isportable in the sense that it compiles and copies everything the platform
declares, but a platform that declared
d_name[1]and over-allocated wouldtruncate. Linux, the BSDs and macOS all declare it fixed. If that ever stops
being true the failure is silent, so it belongs in
TODO.mdnext to the otherscope decisions.
The handle
DIR *passes through opaquely, exactly asFILE *does inaksl_fopen. Noaksl_Dirwrapper. There is nothing to add to it and wrapping it would breakdirfd(3)for anybody who needs it.readdir's NULLThe same untangling
aksl_fgetcalready does, in the same order:Note this is the one place in the library where
errnois the discriminatorand not just the status, because
readdirhas noferror()equivalent. That isworth a comment at the site — a later reader will otherwise "fix" it to match
aksl_fgetcexactly and lose the distinction.End of directory is
AKERR_EOF, not success. "Finding nothing is success"governs searching functions. A read loop terminates on a status, the way the
aksl_fgetcandaksl_getlineloops already do.scandirshould not land herescandir(3)takes a filter callback and a comparison callback, and neither canraise an
akerr_ErrorContext *through a libc-defined signature. That is theidentical unsolved problem as #14 (
qsort/bsearchwant an akerror-awarecomparator, not a wrapper). It also allocates the array and every entry with
malloc, so it needs a matchingaksl_scandir_free.Recommendation: drop
scandirfrom this issue and let it land with whatever#14 settles. Wrapping it here would either invent a callback convention that #14
then has to contradict, or ship a wrapper whose callbacks cannot report. The
remaining surface —
opendir,fdopendir,readdir,closedir,rewinddir—has no such problem and unblocks the consumer today.
rewinddir(3)returnsvoidand cannot fail, so its wrapper can only ever raiseAKERR_NULLPOINTER. Wrap it anyway: the NULL check is real, and a gap in thefamily is worse than a thin function.
What
d_typeis allowed to meand_typeisDT_UNKNOWNon filesystems that do not carry the type in thedirectory entry. A caller that trusts it will work on ext4 and fail on the first
filesystem that does not fill it in, which is exactly the kind of bug that shows
up only in somebody else's deployment.
The header must say:
d_typemay beDT_UNKNOWN, and a caller that needs thetype must fall back to
stat. That makes #9 a soft dependency — not forcompilation, but because the documented fallback should point at a wrapper that
exists rather than at
stat(2)directly.Tests
opendiron a path that does not existENOENTpropagated as itselfopendiron a directory with no search permissionEACCESpropagated as itselfopendiron a regular fileENOTDIR.and.., thenAKERR_EOFreaddirfdopendirfrom anopenfdopendiron the pathrewinddirmid-walkdirp, NULLdestAKERR_NULLPOINTERon eachThe
EACCEScase does not work as root, which is how containers usually runCI.
chmod 000is ignored for a process withCAP_DAC_OVERRIDE, so that testwould pass by accident locally and fail in CI, or the reverse. It needs a
geteuid() == 0guard that skips it, and the skip must be visible in the CTestoutput rather than silently returning zero — otherwise the suite reports a
passing test that never ran.
The "entries outlast a single read" case is the one that actually justifies the
copy decision above. Write it so it fails if the implementation hands back the
library's pointer: read entry one, keep it, read entry two, then assert entry
one's
d_nameis still the first name.Files
include/akstdlib.h(declarations plus the full Doxygen block — thedocstarget fails on a missing
@throws), a newsrc/dir.crather than growingsrc/stream.c, its row in the AGENTS.md file table,tests/test_dir.cplus itsentry in
AKSL_TESTS, and the consumer change inakbasic/src/runtime_disk.c:399that deletes the refusal and implementsDIRECTORY.What I would change on the ticket
Scope is settled if you take the copy-into-caller-storage decision and the
scandirsplit. That moves it offstatus::grooming. The akbasic side is aseparate issue against akbasic, since it lands after this ships.
— Tachikoma (Claude Code, Opus 5, 1M context)