Files
erofs-freebsd-out-tree/dir.c
T
2026-08-13 10:48:49 +02:00

347 lines
9.1 KiB
C

// SPDX-License-Identifier: GPL-2.0-only
/*
* Copyright (C) 2017-2018 HUAWEI, Inc.
* https://www.huawei.com/
* Copyright (C) 2022, Alibaba Cloud
*/
#include <sys/param.h>
#include <sys/systm.h>
#include <sys/dirent.h>
#include <sys/kernel.h>
#include <sys/libkern.h>
#include <sys/limits.h>
#include <sys/malloc.h>
#include <sys/mount.h>
#include <sys/vnode.h>
#include "internal.h"
#include "erofs_defs.h"
/* Map EROFS directory entry file type to FreeBSD dirent.d_type. */
static unsigned char
erofs_ftype_to_dtype(uint8_t ftype)
{
switch (ftype) {
case EROFS_FT_REG_FILE:
return (DT_REG);
case EROFS_FT_DIR:
return (DT_DIR);
case EROFS_FT_CHRDEV:
return (DT_CHR);
case EROFS_FT_BLKDEV:
return (DT_BLK);
case EROFS_FT_FIFO:
return (DT_FIFO);
case EROFS_FT_SOCK:
return (DT_SOCK);
case EROFS_FT_SYMLINK:
return (DT_LNK);
default:
return (DT_UNKNOWN);
}
}
/* Validate one name slot and return its Linux-visible length. */
int
erofs_dirent_namelen(const char *blk, uint32_t nameoff, uint32_t endoff,
bool trailing, size_t *namelenp)
{
size_t namelen, span;
if (endoff <= nameoff)
return (EINTEGRITY);
span = endoff - nameoff;
if (trailing) {
namelen = strnlen(blk + nameoff, span);
} else {
namelen = span;
if (memchr(blk + nameoff, '\0', span) != NULL)
return (EINTEGRITY);
}
if (namelen == 0 || namelen > EROFS_NAME_LEN)
return (EINTEGRITY);
for (size_t i = 0; i < namelen; i++) {
if (blk[nameoff + i] == '/')
return (EINTEGRITY);
}
*namelenp = namelen;
return (0);
}
/* Validate the dirent array, name offsets, and names in a block. */
int
erofs_validate_dirblock(const char *blk, uint32_t blksz, uint32_t maxsize,
uint32_t *ndirentsp)
{
const struct erofs_dirent *de;
uint32_t endoff, first_nameoff, idx, nameoff, ndirents, prev_nameoff;
size_t namelen;
int error;
if (blksz < EROFS_DIRENT_SIZE || maxsize < EROFS_DIRENT_SIZE ||
maxsize > blksz)
return (EINTEGRITY);
de = (const struct erofs_dirent *)blk;
first_nameoff = le16toh(de[0].nameoff);
if (first_nameoff < EROFS_DIRENT_SIZE || first_nameoff >= maxsize ||
(first_nameoff % EROFS_DIRENT_SIZE) != 0)
return (EINTEGRITY);
ndirents = first_nameoff / EROFS_DIRENT_SIZE;
prev_nameoff = 0;
for (idx = 0; idx < ndirents; idx++) {
nameoff = le16toh(de[idx].nameoff);
if ((idx == 0 && nameoff != first_nameoff) ||
(idx != 0 && nameoff <= prev_nameoff) ||
nameoff < first_nameoff || nameoff >= maxsize)
return (EINTEGRITY);
endoff = idx + 1 < ndirents ?
le16toh(de[idx + 1].nameoff) : maxsize;
if (endoff <= nameoff || endoff > maxsize)
return (EINTEGRITY);
error = erofs_dirent_namelen(blk, nameoff, endoff,
idx + 1 == ndirents, &namelen);
if (error != 0)
return (error);
prev_nameoff = nameoff;
}
*ndirentsp = ndirents;
return (0);
}
/* Extract name, nid, and type of the idx'th directory entry from a block. */
static int
erofs_dirent_name(const char *blk, uint32_t maxsize,
uint32_t idx, uint32_t ndirents, char *name, size_t namesz, uint64_t *nid,
uint8_t *ftype, size_t *namelenp)
{
const struct erofs_dirent *de;
uint32_t nameoff, endoff;
size_t namelen;
int error;
de = (const struct erofs_dirent *)blk;
nameoff = le16toh(de[idx].nameoff);
if (idx + 1 < ndirents)
endoff = le16toh(de[idx + 1].nameoff);
else
endoff = maxsize;
error = erofs_dirent_namelen(blk, nameoff, endoff,
idx + 1 == ndirents, &namelen);
if (error != 0)
return (error);
if (namelen >= namesz)
return (EINTEGRITY);
memcpy(name, blk + nameoff, namelen);
name[namelen] = '\0';
*nid = le64toh(de[idx].nid);
*ftype = de[idx].file_type;
*namelenp = namelen;
return (0);
}
/* Per-call state for readdir dirent/cookie output. */
struct erofs_uiodir {
struct dirent *dirent;
uint64_t *cookies;
uint64_t last_cookie;
int ncookies;
int acookies;
int eofflag;
};
enum erofs_uiodir_result {
EROFS_UIODIR_BUFFER_FULL = -1,
EROFS_UIODIR_OK = 0,
};
/* Push a dirent and its cookie to the caller, modelled after UDF. */
static int
erofs_uiodir(struct erofs_uiodir *uiodir, int de_size, struct uio *uio,
uint64_t cookie)
{
int error;
if (cookie <= uiodir->last_cookie)
return (EINTEGRITY);
if (uio->uio_resid < de_size ||
(uiodir->cookies != NULL &&
uiodir->acookies >= uiodir->ncookies)) {
return (EROFS_UIODIR_BUFFER_FULL);
}
error = uiomove(uiodir->dirent, de_size, uio);
if (error != 0)
return (error);
uiodir->last_cookie = cookie;
if (uiodir->cookies != NULL)
uiodir->cookies[uiodir->acookies++] = cookie;
return (EROFS_UIODIR_OK);
}
/*
* Process directory entries within a single block and output them to uio.
* (Linux equivalent: erofs_fill_dentries in Linux's dir.c)
*
* Returns 0 on success (all entries consumed), -1 if uio is full, or a
* positive error code on corruption.
*/
static int
erofs_fill_dentries(struct erofs_uiodir *uiodir, struct uio *uio,
struct dirent *d, const char *blk, uint32_t maxsize,
uint32_t start_idx, uint32_t ndirents, uint64_t block_off,
uint64_t *logical_offp)
{
char name[EROFS_NAME_LEN + 1];
uint32_t idx;
uint64_t curpos, nextoff, nid;
size_t namelen;
uint8_t ftype;
int error;
for (idx = start_idx; idx < ndirents; idx++) {
curpos = block_off + idx * EROFS_DIRENT_SIZE;
nextoff = (idx + 1 < ndirents) ?
(curpos + EROFS_DIRENT_SIZE) :
(block_off + maxsize);
error = erofs_dirent_name(blk, maxsize, idx, ndirents, name,
sizeof(name), &nid, &ftype, &namelen);
if (error != 0)
return (error);
bzero(d, sizeof(*d));
d->d_fileno = nid;
d->d_type = erofs_ftype_to_dtype(ftype);
d->d_namlen = namelen;
d->d_reclen = GENERIC_DIRSIZ(d);
d->d_off = nextoff;
strlcpy(d->d_name, name, sizeof(d->d_name));
error = erofs_uiodir(uiodir, d->d_reclen, uio, d->d_off);
if (error != 0)
return (error);
*logical_offp = nextoff;
uio->uio_offset = *logical_offp;
}
return (0);
}
/*
* Read directory contents and output a FreeBSD dirent stream to uio.
*
* Key points:
* - On-disk entries use their logical file offsets as cookies;
* - A dot_omitted directory appends a synthetic "." at i_size, matching
* Linux, so existing on-disk cookies are not shifted;
* - The dirent array occupies only the front portion of a block, so after
* scanning all entries offset must jump to maxsize (the block end),
* otherwise the loop would get stuck on the same block;
* - Supports a_ncookies / a_cookies for NFS and other callers that need
* resumable iteration.
*/
int
erofs_readdir_block(struct vnode *vp, struct uio *uio, int *eofflag,
int *ncookies, uint64_t **cookies)
{
struct erofs_node *dir;
struct erofs_mount *em;
struct erofs_uiodir uiodir;
struct dirent d;
uint64_t *cookiebuf;
char *blk;
uint64_t block_off, logical_off;
uint32_t block_pos, blksz, ndirents, start_idx, maxsize;
int error;
dir = VTOE(vp);
em = MTOE(vp->v_mount);
blksz = em->block_size;
error = 0;
cookiebuf = NULL;
uiodir.eofflag = 0;
uiodir.acookies = 0;
uiodir.dirent = &d;
uiodir.cookies = NULL;
uiodir.ncookies = 0;
if (cookies != NULL && ncookies != NULL) {
*cookies = NULL;
*ncookies = 0;
uiodir.ncookies = MAX(1, uio->uio_resid / 8);
cookiebuf = malloc(sizeof(*uiodir.cookies) * uiodir.ncookies,
M_TEMP, M_WAITOK);
uiodir.cookies = cookiebuf;
}
if (uio->uio_offset < 0) {
error = EINVAL;
goto out;
}
if (dir->dot_omitted && dir->size == (uint64_t)OFF_MAX) {
error = EINTEGRITY;
goto out;
}
logical_off = uio->uio_offset;
uiodir.last_cookie = logical_off;
uio->uio_offset = logical_off;
while (logical_off < dir->size) {
block_off = rounddown2(logical_off, (uint64_t)blksz);
maxsize = MIN((uint64_t)blksz, dir->size - block_off);
block_pos = logical_off - block_off;
if ((block_pos % EROFS_DIRENT_SIZE) != 0) {
block_pos = roundup(block_pos, EROFS_DIRENT_SIZE);
logical_off = block_off + block_pos;
uio->uio_offset = logical_off;
}
error = erofs_read_data(em, dir, block_off, maxsize,
(void **)&blk);
if (error != 0)
goto out;
error = erofs_validate_dirblock(blk, blksz, maxsize, &ndirents);
if (error != 0) {
erofs_brelse(blk);
goto out;
}
start_idx = block_pos / EROFS_DIRENT_SIZE;
if (start_idx >= ndirents) {
logical_off = block_off + maxsize;
uio->uio_offset = logical_off;
erofs_brelse(blk);
continue;
}
error = erofs_fill_dentries(&uiodir, uio, &d, blk, maxsize,
start_idx, ndirents, block_off, &logical_off);
erofs_brelse(blk);
if (error != 0)
goto out;
}
if (dir->dot_omitted && logical_off == dir->size) {
bzero(&d, sizeof(d));
d.d_fileno = dir->nid;
d.d_type = DT_DIR;
d.d_namlen = 1;
d.d_reclen = GENERIC_DIRSIZ(&d);
d.d_off = dir->size + 1;
d.d_name[0] = '.';
d.d_name[1] = '\0';
error = erofs_uiodir(&uiodir, d.d_reclen, uio, d.d_off);
if (error != 0)
goto out;
logical_off++;
uio->uio_offset = logical_off;
}
uiodir.eofflag = 1;
out:
if (error == EROFS_UIODIR_BUFFER_FULL)
error = 0;
if (eofflag != NULL && error == 0)
*eofflag = uiodir.eofflag;
if (cookies != NULL && ncookies != NULL) {
if (error != 0) {
free(cookiebuf, M_TEMP);
} else {
*ncookies = uiodir.acookies;
*cookies = cookiebuf;
}
}
return (error);
}