// SPDX-License-Identifier: GPL-2.0-only /* * Copyright (C) 2017-2018 HUAWEI, Inc. * https://www.huawei.com/ * Copyright (C) 2022, Alibaba Cloud */ #include #include #include #include #include #include #include #include #include #include #include "internal.h" /* * Compare two directory entry names using an already-matched prefix. * (Linux equivalent: erofs_dirnamecmp in Linux's namei.c) * * qn_name / qn_len: search key (not necessarily null-terminated). * qd_name / qd_end: on-disk name range (may not be null-terminated). * matched: in/out count of prefix characters already known to match. * * Returns 0 if equal, 1 if qn > qd, -1 if qn < qd. */ static int erofs_dirnamecmp(const char *qn_name, size_t qn_len, const char *qd_name, const char *qd_end, unsigned int *matched) { size_t dname_span; unsigned int i; dname_span = qd_end - qd_name; i = MIN(*matched, qn_len); i = MIN(i, dname_span); while (i < qn_len && i < dname_span && qd_name[i] != '\0') { if ((unsigned char)qn_name[i] != (unsigned char)qd_name[i]) { *matched = i; return ((unsigned char)qn_name[i] > (unsigned char)qd_name[i] ? 1 : -1); } ++i; } *matched = i; if (i == qn_len) return (i == dname_span || qd_name[i] == '\0' ? 0 : -1); return (1); } /* * Binary search within a directory block for the target name. * * Returns a pointer to the matching dirent, or NULL on miss. */ static struct erofs_dirent * find_target_dirent(const char *name, size_t namelen, char *data, uint32_t datasize, uint32_t ndirents) { uint32_t head, back; unsigned int startprfx, endprfx; struct erofs_dirent *const de = (struct erofs_dirent *)data; /* The 1st dirent has already been evaluated by the caller. */ head = 1; back = ndirents - 1; startprfx = endprfx = 0; while (head <= back) { const uint32_t mid = head + (back - head) / 2; const uint32_t nameoff = le16toh(de[mid].nameoff); unsigned int matched = MIN(startprfx, endprfx); const char *dname_start = data + nameoff; const char *dname_end; if (mid >= ndirents - 1) dname_end = data + datasize; else dname_end = data + le16toh(de[mid + 1].nameoff); /* String comparison without already matched prefix */ int ret = erofs_dirnamecmp(name, namelen, dname_start, dname_end, &matched); if (ret == 0) return (de + mid); else if (ret > 0) { head = mid + 1; startprfx = matched; } else { back = mid - 1; endprfx = matched; } } return (NULL); } /* * Find the directory block most likely to contain the target name. * * Uses two-level binary search: first across blocks, then within the * candidate block via find_target_dirent(). * * Returns the block buffer on success (caller must erofs_brelse), * or NULL on error. *_ndirents is set to the number of dirents in * the returned block (0 means the first entry is the match). * On error, *errorp is set to a positive errno. */ static char * erofs_find_target_block(struct erofs_mount *em, struct erofs_node *dir, const char *name, size_t namelen, uint32_t *_ndirents, uint32_t *_datasize, int *errorp) { uint32_t bsz = em->block_size; uint64_t head, back; unsigned int startprfx = 0, endprfx = 0; char *candidate = NULL; int error; *errorp = 0; *_ndirents = 0; *_datasize = 0; if (dir->size == 0) return (NULL); head = 0; back = (dir->size - 1) / bsz; while (head <= back) { const uint64_t mid = head + (back - head) / 2; uint64_t block_off; uint32_t maxsize; const struct erofs_dirent *de; char *blk; int diff; uint32_t ndirents; uint32_t nameoff; unsigned int matched; const char *dname_start, *dname_end; if (__builtin_mul_overflow(mid, (uint64_t)bsz, &block_off) || block_off >= dir->size) { *errorp = EINTEGRITY; goto out; } maxsize = MIN((uint64_t)bsz, dir->size - block_off); error = erofs_read_data(em, dir, block_off, maxsize, (void **)&blk); if (error != 0) { *errorp = error; goto out; } error = erofs_validate_dirblock(blk, bsz, maxsize, &ndirents); if (error != 0) { erofs_brelse(blk); *errorp = error; goto out; } de = (const struct erofs_dirent *)blk; nameoff = le16toh(de[0].nameoff); matched = MIN(startprfx, endprfx); dname_start = blk + nameoff; if (ndirents == 1) dname_end = blk + maxsize; else dname_end = blk + le16toh(de[1].nameoff); /* String comparison without already matched prefix */ diff = erofs_dirnamecmp(name, namelen, dname_start, dname_end, &matched); if (diff < 0) { erofs_brelse(blk); if (mid == 0) break; back = mid - 1; endprfx = matched; continue; } /* diff >= 0: this block is a candidate. */ if (candidate != NULL) erofs_brelse(candidate); candidate = blk; if (diff == 0) { *_ndirents = 0; *_datasize = maxsize; return (candidate); } head = mid + 1; startprfx = matched; *_ndirents = ndirents; *_datasize = maxsize; } return (candidate); out: if (candidate != NULL) erofs_brelse(candidate); return (NULL); } /* * Look up a name in a directory and return its nid and d_type. * (Linux equivalent: erofs_namei in Linux's namei.c) */ static int erofs_namei(struct erofs_mount *em, struct erofs_node *dir, const char *name, size_t namelen, uint64_t *nid, uint8_t *d_type) { int error; uint32_t ndirents; uint32_t datasize; char *blk; struct erofs_dirent *de; if (dir->size == 0) return (ENOENT); blk = erofs_find_target_block(em, dir, name, namelen, &ndirents, &datasize, &error); if (blk == NULL) return (error != 0 ? error : ENOENT); de = (struct erofs_dirent *)blk; if (ndirents > 0) de = find_target_dirent(name, namelen, blk, datasize, ndirents); if (de != NULL) { *nid = le64toh(de->nid); *d_type = de->file_type; } erofs_brelse(blk); return (de != NULL ? 0 : ENOENT); } /* * Directory name lookup (VOP_CACHEDLOOKUP entry point). * * FreeBSD-side API requirements: * - "." must be returned under the caller's requested lock mode; * - ".." must go through vn_vget_ino() to avoid holding a child lock while * acquiring the parent directory lock in reverse; * - Both hit and miss must correctly update the namecache. */ int erofs_lookup(struct vop_cachedlookup_args *ap) { struct vnode *dvp, *vp; struct erofs_node *dir; struct erofs_mount *em; struct componentname *cnp; uint64_t nid; uint8_t dtype; int error, ltype; dvp = ap->a_dvp; cnp = ap->a_cnp; *ap->a_vpp = NULL; if ((cnp->cn_flags & ISLASTCN) != 0 && (cnp->cn_nameiop == DELETE || cnp->cn_nameiop == RENAME)) return (EROFS); if (cnp->cn_namelen < 0) return (EINVAL); if (cnp->cn_namelen > EROFS_NAME_LEN) return (ENAMETOOLONG); if (cnp->cn_namelen == 1 && cnp->cn_nameptr[0] == '.') { vref(dvp); ltype = cnp->cn_lkflags & LK_TYPE_MASK; if (ltype != VOP_ISLOCKED(dvp)) { if (ltype == LK_EXCLUSIVE) vn_lock(dvp, LK_UPGRADE | LK_RETRY); else if (ltype == LK_SHARED) vn_lock(dvp, LK_DOWNGRADE | LK_RETRY); } *ap->a_vpp = dvp; return (0); } dir = VTOE(dvp); em = MTOE(dvp->v_mount); error = erofs_namei(em, dir, cnp->cn_nameptr, cnp->cn_namelen, &nid, &dtype); if (error != 0) { if (error == ENOENT && (cnp->cn_flags & MAKEENTRY) != 0) cache_enter(dvp, NULL, cnp); if (error == ENOENT && (cnp->cn_flags & ISLASTCN) != 0 && (cnp->cn_nameiop == CREATE || cnp->cn_nameiop == RENAME)) return (EROFS); return (error); } if ((cnp->cn_flags & ISDOTDOT) != 0) error = vn_vget_ino(dvp, nid, cnp->cn_lkflags, &vp); else error = erofs_vget(dvp->v_mount, nid, cnp->cn_lkflags, &vp); if (error != 0) return (error); *ap->a_vpp = vp; if ((cnp->cn_flags & MAKEENTRY) != 0) cache_enter(dvp, vp, cnp); return (0); }