/*
 * Copyright (c) 1990,1993 Regents of The University of Michigan.
 * All Rights Reserved.  See COPYRIGHT.
 */

#ifdef HAVE_CONFIG_H
#include "config.h"
#endif /* HAVE_CONFIG_H */

#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/param.h>
#include <utime.h>

#include <atalk/adouble.h>
#include <atalk/afp.h>
#include <atalk/cnid.h>
#include <atalk/dsi.h>
#include <atalk/fce_api.h>
#include <atalk/globals.h>
#include <atalk/logger.h>
#include <atalk/netatalk_conf.h>

#ifdef WITH_SPOTLIGHT
#include <atalk/spotlight.h>
#endif

#include <atalk/unix.h>
#include <atalk/util.h>
#include <atalk/vfs.h>

#include "desktop.h"
#include "dircache.h"
#include "directory.h"
#include "file.h"
#include "filedir.h"
#include "fork.h"
#include "unix.h"
#include "volume.h"

/*!
 * @code
 * the format for the finderinfo fields (from IM: Toolbox Essentials):
 * field         bytes        subfield    bytes
 *
 * files:
 * ioFlFndrInfo  16      ->       type    4  type field
 *                             creator    4  creator field
 *                               flags    2  finder flags:
 *					     alias, bundle, etc.
 *                            location    4  location in window
 *                              folder    2  window that contains file
 *
 * ioFlXFndrInfo 16      ->     iconID    2  icon id
 *                              unused    6  reserved
 *                              script    1  script system
 *                              xflags    1  reserved
 *                           commentID    2  comment id
 *                           putawayID    4  home directory id
 * @endcode
 */

const uint8_t ufinderi[ADEDLEN_FINDERI] = {
    0, 0, 0, 0, 0, 0, 0, 0,
    1, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0
};

static const uint8_t old_ufinderi[] = {
    'T', 'E', 'X', 'T', 'U', 'N', 'I', 'X'
};

static int has_dotdot_component(const char *path)
{
    const char *p = path;

    while (*p) {
        size_t len;
        const char *slash = strchr(p, '/');
        len = slash ? (size_t)(slash - p) : strlen(p);

        if (len == 2 && p[0] == '.' && p[1] == '.') {
            return 1;
        }

        if (!slash) {
            break;
        }

        p = slash + 1;
    }

    return 0;
}

static int path_is_inside_volume(const struct vol *vol, const char *path)
{
    size_t volpath_len = strlen(vol->v_path);

    if (strncmp(path, vol->v_path, volpath_len) != 0) {
        return 0;
    }

    return path[volpath_len] == '\0' || path[volpath_len] == '/';
}

static int symlink_target_safe(const struct vol *vol,
                               const char *link_path,
                               const char *target)
{
    char target_path[MAXPATHLEN + 1];
    char link_dir[MAXPATHLEN + 1];
    char *slash;
    char *resolved_target = NULL;
    int safe = 0;

    if (target[0] == '/' || has_dotdot_component(target)) {
        return 0;
    }

    if (strlcpy(link_dir, link_path, sizeof(link_dir)) >= sizeof(link_dir)) {
        return 0;
    }

    slash = strrchr(link_dir, '/');

    if (slash == link_dir) {
        slash[1] = '\0';
    } else if (slash) {
        *slash = '\0';
    } else {
        strlcpy(link_dir, ".", sizeof(link_dir));
    }

    if (snprintf(target_path, sizeof(target_path), "%s/%s", link_dir, target)
            >= sizeof(target_path)) {
        return 0;
    }

    resolved_target = realpath_safe(target_path);

    if (resolved_target) {
        safe = path_is_inside_volume(vol, resolved_target);
        free(resolved_target);
    }

    return safe;
}

/* ----------------------
*/
static int default_type(void *finder)
{
    if (!memcmp(finder, ufinderi, 8) || !memcmp(finder, old_ufinderi, 8)) {
        return 1;
    }

    return 0;
}

/* FIXME path : unix or mac name ? (for now it's unix name ) */
void *get_finderinfo(const struct vol *vol, const char *upath,
                     struct adouble *adp, void *data, int islink)
{
    struct extmap       *em;
    void                *ad_finder = NULL;
    int                 chk_ext = 0;

    if (adp) {
        ad_finder = ad_entry(adp, ADEID_FINDERI);
    }

    if (ad_finder) {
        memcpy(data, ad_finder, ADEDLEN_FINDERI);

        /* default type ? */
        if (default_type(ad_finder)) {
            chk_ext = 1;
        }
    } else {
        memcpy(data, ufinderi, ADEDLEN_FINDERI);
        chk_ext = 1;

        if (upath && vol_inv_dots(vol) && *upath == '.') { /* make it invisible */
            uint16_t ashort;
            ashort = htons(FINDERINFO_INVISIBLE);
            memcpy((char *)data + FINDERINFO_FRFLAGOFF, &ashort, sizeof(ashort));
        }
    }

#ifdef __APPLE__

    /* If no adouble finder information was found, check for native macOS info */
    if (chk_ext && (vol->v_adouble == AD_VERSION_EA)) {
        char NativeFinderInfo[ADEDLEN_FINDERI] = {'\0'};

        if (ad_open_native_finderinfo(upath, NativeFinderInfo)) {
            memcpy(data, NativeFinderInfo, ADEDLEN_FINDERI);

            /* default type ? */
            if (!(default_type(data))) {
                chk_ext = 0;
            }
        }
    }

#endif

    if (islink && !vol_syml_opt(vol)) {
        uint16_t linkflag;
        memcpy(&linkflag, (char *)data + FINDERINFO_FRFLAGOFF, 2);
        linkflag |= htons(FINDERINFO_ISALIAS);
        memcpy((char *)data + FINDERINFO_FRFLAGOFF, &linkflag, 2);
        memcpy((char *)data + FINDERINFO_FRTYPEOFF, "slnk", 4);
        memcpy((char *)data + FINDERINFO_FRCREATOFF, "rhap", 4);
    }

    /* Only enter if no appledouble information and no finder information found. */
    if (chk_ext && (em = getextmap(upath))) {
        memcpy(data, em->em_type, sizeof(em->em_type));
        memcpy((char *)data + 4, em->em_creator, sizeof(em->em_creator));
    }

    return data;
}

/* ---------------------
*/
char *set_name(const struct vol *vol, char *data, cnid_t pid, char *name,
               cnid_t id, uint32_t utf8)
{
    uint32_t   aint;
    char        *tp = NULL;
    char        *src = name;
    aint = strlen(name);

    if (!utf8) {
        /* want mac name */
        if (utf8_encoding(vol->v_obj)) {
            /* but name is an utf8 mac name */
            char *u, *m;
            /* global static variable... */
            tp = strdup(name);

            if (!(u = mtoupath(vol, name, pid, 1)) || !(m = utompath(vol, u, id, 0))) {
                aint = 0;
            } else {
                aint = strlen(m);
                src = m;
            }
        }

        if (aint > MACFILELEN) {
            aint = MACFILELEN;
        }

        *data++ = aint;
    } else {
        uint16_t temp;

        /* FIXME safeguard, anyway if no ascii char it's game over */
        if (aint > UTF8FILELEN_EARLY) {
            aint = UTF8FILELEN_EARLY;
        }

        utf8 = (uint32_t) vol->v_kTextEncoding;
        memcpy(data, &utf8, sizeof(utf8));
        data += sizeof(utf8);
        temp = htons(aint);
        memcpy(data, &temp, sizeof(temp));
        data += sizeof(temp);
    }

    memcpy(data, src, aint);
    data += aint;

    if (tp) {
        strcpy(name, tp);
        free(tp);
    }

    return data;
}

/*
 * FIXME: PDINFO is UTF8 and doesn't need adp
*/
#define PARAM_NEED_ADP(b) ((b) & ((1 << FILPBIT_ATTR)  |\
				  (1 << FILPBIT_CDATE) |\
				  (1 << FILPBIT_MDATE) |\
				  (1 << FILPBIT_BDATE) |\
				  (1 << FILPBIT_FINFO) |\
				  (1 << FILPBIT_RFLEN) |\
				  (1 << FILPBIT_EXTRFLEN) |\
				  (1 << FILPBIT_PDINFO) |\
				  (1 << FILPBIT_FNUM) |\
				  (1 << FILPBIT_UNIXPR)))

/*!
 * @brief Get CNID for did/upath args both from database and adouble file
 *
 * 1. Get the objects CNID as stored in its adouble file
 * 2. Get the objects CNID from the database
 * 3. Store resource fork data
 *
 * @param[in,out] vol    volume
 * @param[in,out] adp    adouble struct of object upath, might be NULL
 * @param[in] st         stat of upath, must NOT be NULL
 * @param[in] did        parent CNID of upath
 * @param[in] upath      name of object
 * @param[in] len        strlen of upath
 */
uint32_t get_id(struct vol *vol,
                struct adouble *adp,
                const struct stat *st,
                const cnid_t did,
                const char *upath,
                const int len)
{
    uint32_t adcnid;
    uint32_t dbcnid = CNID_INVALID;

    if (vol->v_cdb != NULL) {
        /* prime aint with what we think is the cnid, set did to zero for
           catching moved files */
        adcnid = ad_getid(adp, st->st_dev, st->st_ino, 0, vol->v_stamp); /* (1) */
        AFP_CNID_START("cnid_add");
        dbcnid = cnid_add(vol->v_cdb, st, did, upath, len, adcnid); /* (2) */
        AFP_CNID_DONE();

        /* Throw errors if cnid_add fails. */
        if (dbcnid == CNID_INVALID) {
            switch (errno) {
            case CNID_ERR_CLOSE: /* the db is closed */
                break;

            case CNID_ERR_DB:
                LOG(log_error, logtype_afpd,
                    "get_id: Connection to the CNID backend DB failed. "
                    "This is now treated as a fatal error. "
                    "Is the cnid_metad process running? "
                    "Please escalate to the upstream project "
                    "if you suspect this is a bug");
                exit(EXITERR_SYS);

            case CNID_ERR_PARAM:
                LOG(log_error, logtype_afpd, "get_id: Incorrect parameters passed to cnid_add");
                afp_errno = AFPERR_PARAM;
                goto exit;

            case CNID_ERR_PATH:
                afp_errno = AFPERR_PARAM;
                goto exit;

            default:
                afp_errno = AFPERR_MISC;
                goto exit;
            }
        } else if (adp && adcnid && (adcnid != dbcnid)) { /* (3) */
            /* Update the resource fork. For a folder adp is always null */
            LOG(log_debug, logtype_afpd,
                "get_id(%s/%s): calling ad_setid(old: %u, new: %u)",
                getcwdpath(), upath, htonl(adcnid), htonl(dbcnid));

            if (ad_setid(adp, st->st_dev, st->st_ino, dbcnid, did, vol->v_stamp)) {
                if (ad_flush(adp) != 0) {
                    LOG(log_error, logtype_afpd, "get_id(\"%s\"): can't flush",
                        fullpathname(upath));
                }
            }
        }
    }

exit:
    return dbcnid;
}

/* -------------------------- */
int getmetadata(const AFPObj *obj,
                struct vol *vol,
                uint16_t bitmap,
                struct path *path, struct dir *dir,
                char *buf, size_t *buflen, struct adouble *adp)
{
    char		*data, *l_nameoff = NULL, *upath;
    char                *utf_nameoff = NULL;
    char		*ade = NULL;
    int			bit = 0;
    uint32_t		aint;
    cnid_t              id = 0;
    uint16_t		ashort;
    uint8_t              achar, fdType[4];
    uint32_t           utf8 = 0;
    struct stat         *st;
    struct maccess	ma;
    LOG(log_debug, logtype_afpd, "getmetadata(\"%s\")", path->u_name);
    upath = path->u_name;
    st = &path->st;
    data = buf;

    if (((bitmap & ((1 << FILPBIT_FINFO) | (1 << FILPBIT_LNAME) |
                    (1 << FILPBIT_PDINFO))) && !path->m_name)
            || (bitmap & (1 << FILPBIT_LNAME)
                && utf8_encoding(obj)) /* FIXME should be m_name utf8 filename */
            || (bitmap & (1 << FILPBIT_FNUM))) {
        if (!path->id) {
            bstring fullpath;
            struct dir *cachedfile;
            int len = upath ? (int)strnlen(upath, CNID_MAX_PATH_LEN) : 0;

            if ((cachedfile = dircache_search_by_name(vol, dir, upath, len)) != NULL) {
                /* Compare fresh stat inode matches dircache entry */
                if (cachedfile->dcache_ino == st->st_ino) {
                    id = cachedfile->d_did;
                } else {
                    /* File replaced externally - invalidate dircache */
                    LOG(log_debug, logtype_afpd,
                        "getmetadata: inode mismatch (cached:%llu vs current:%llu), file replaced, invalidating cache",
                        (unsigned long long)cachedfile->dcache_ino, (unsigned long long)st->st_ino);
                    dir_remove(vol, cachedfile);
                    cachedfile = NULL;
                }
            }

            if (cachedfile == NULL) {
                id = get_id(vol, adp, st, dir->d_did, upath, len);
                /* Add it to the cache */
                LOG(log_debug, logtype_afpd, "getmetadata: caching: did:%u, \"%s\", cnid:%u",
                    ntohl(dir->d_did), upath, ntohl(id));

                /* Get macname from unixname first */
                if (path->m_name == NULL) {
                    if ((path->m_name = utompath(vol, upath, id, utf8_encoding(obj))) == NULL) {
                        LOG(log_error, logtype_afpd, "getmetadata: utompath error");
                        return AFPERR_MISC;
                    }
                }

                /* Build fullpath */
                if (((fullpath = bstrcpy(dir->d_fullpath)) == NULL)
                        || (bconchar(fullpath, '/') != BSTR_OK)
                        || (bcatcstr(fullpath, upath)) != BSTR_OK) {
                    LOG(log_error, logtype_afpd, "getmetadata: fullpath: %s", strerror(errno));
                    bdestroy(fullpath);
                    return AFPERR_MISC;
                }

                if ((cachedfile = dir_new(path->m_name, upath, vol, dir->d_did, id, fullpath,
                                          st)) == NULL) {
                    LOG(log_error, logtype_afpd, "getmetadata: error from dir_new");
                    return AFPERR_MISC;
                }

                if ((dircache_add(vol, cachedfile)) != 0) {
                    LOG(log_error, logtype_afpd, "getmetadata: fatal dircache error");
                    return AFPERR_MISC;
                }
            }
        } else {
            id = path->id;
        }

        if (id == CNID_INVALID) {
            return afp_errno;
        }

        if (!path->m_name) {
            path->m_name = utompath(vol, upath, id, utf8_encoding(vol->v_obj));
        }
    }

    while (bitmap != 0) {
        while ((bitmap & 1) == 0) {
            bitmap = bitmap >> 1;
            bit++;
        }

        switch (bit) {
        case FILPBIT_ATTR :
            if (adp) {
                ad_getattr(adp, &ashort);
            } else if (upath && vol_inv_dots(vol) && *upath == '.') {
                ashort = htons(ATTRBIT_INVISIBLE);
            } else {
                ashort = 0;
            }

            ashort &= ~htons(vol->v_ignattr);
#if 0
            /* FIXME do we want a visual clue if the file is read only
             */
            struct maccess	ma;
            accessmode(vol, ".", &ma, dir, NULL);

            if ((ma.ma_user & AR_UWRITE)) {
                accessmode(vol, upath, &ma, dir, st);

                if (!(ma.ma_user & AR_UWRITE)) {
                    ashort |= htons(ATTRBIT_NOWRITE);
                }
            }

#endif
            memcpy(data, &ashort, sizeof(ashort));
            data += sizeof(ashort);
            LOG(log_debug, logtype_afpd, "metadata('%s'): AFP Attributes: %04x",
                path->u_name, ntohs(ashort));
            break;

        case FILPBIT_PDID :
            memcpy(data, &dir->d_did, sizeof(uint32_t));
            data += sizeof(uint32_t);
            LOG(log_debug, logtype_afpd, "metadata('%s'):     Parent DID: %u",
                path->u_name, ntohl(dir->d_did));
            break;

        case FILPBIT_CDATE :
            if (!adp || (ad_getdate(adp, AD_DATE_CREATE, &aint) < 0)) {
                aint = AD_DATE_FROM_UNIX(st->st_mtime);
            }

            memcpy(data, &aint, sizeof(aint));
            data += sizeof(aint);
            break;

        case FILPBIT_MDATE :
            if (adp && (ad_getdate(adp, AD_DATE_MODIFY, &aint) == 0)) {
                if (st->st_mtime > AD_DATE_TO_UNIX(aint)) {
                    aint = AD_DATE_FROM_UNIX(st->st_mtime);
                }
            } else {
                aint = AD_DATE_FROM_UNIX(st->st_mtime);
            }

            memcpy(data, &aint, sizeof(int));
            data += sizeof(int);
            break;

        case FILPBIT_BDATE :
            if (!adp || (ad_getdate(adp, AD_DATE_BACKUP, &aint) < 0)) {
                aint = AD_DATE_START;
            }

            memcpy(data, &aint, sizeof(int));
            data += sizeof(int);
            break;

        case FILPBIT_FINFO :
            get_finderinfo(vol, upath, adp, (char *)data, S_ISLNK(st->st_mode));
            data += ADEDLEN_FINDERI;
            break;

        case FILPBIT_LNAME :
            l_nameoff = data;
            data += sizeof(uint16_t);
            break;

        case FILPBIT_SNAME :
            memset(data, 0, sizeof(uint16_t));
            data += sizeof(uint16_t);
            break;

        case FILPBIT_FNUM :
            memcpy(data, &id, sizeof(id));
            data += sizeof(id);
            LOG(log_debug, logtype_afpd, "metadata('%s'):           CNID: %u",
                path->u_name, ntohl(id));
            break;

        case FILPBIT_DFLEN :
            if (st->st_size > 0xffffffff) {
                aint = 0xffffffff;
            } else {
                aint = htonl(st->st_size);
            }

            memcpy(data, &aint, sizeof(aint));
            data += sizeof(aint);
            break;

        case FILPBIT_RFLEN: {
            off_t rlen;

            if (adp) {
                if (adp->ad_rlen > 0xffffffff) {
                    aint = 0xffffffff;
                } else {
                    aint = htonl(adp->ad_rlen);
                }
            } else {
                rlen = ad_reso_size(path->u_name, 0, NULL);

                if (rlen > 0xffffffff) {
                    rlen = 0xffffffff;
                }

                aint = htonl(rlen);
            }

            memcpy(data, &aint, sizeof(aint));
            data += sizeof(aint);
            break;
        }

        /* Current client needs ProDOS info block for this file.
           Use simple heuristic and let the Mac "type" string tell
           us what the PD file code should be.  Everything gets a
           subtype of 0x0000 unless the original value was hashed
           to "pXYZ" when we created it.  See IA, Ver 2.
           <shirsch@adelphia.net> */
        case FILPBIT_PDINFO :
            if (obj->afp_version >= 30) { /* UTF8 name */
                utf8 = kTextEncodingUTF8;
                utf_nameoff = data;
                data += sizeof(uint16_t);
                aint = 0;
                memcpy(data, &aint, sizeof(aint));
                data += sizeof(aint);
            } else {
                if (adp && (ade = ad_entry(adp, ADEID_FINDERI)) != NULL) {
                    memcpy(fdType, ade, 4);

                    if (memcmp(fdType, "TEXT", 4) == 0) {
                        achar = '\x04';
                        ashort = 0x0000;
                    } else if (memcmp(fdType, "PSYS", 4) == 0) {
                        achar = '\xff';
                        ashort = 0x0000;
                    } else if (memcmp(fdType, "PS16", 4) == 0) {
                        achar = '\xb3';
                        ashort = 0x0000;
                    } else if (memcmp(fdType, "BINA", 4) == 0) {
                        achar = '\x00';
                        ashort = 0x0000;
                    } else if (fdType[0] == 'p') {
                        achar = fdType[1];
                        ashort = (fdType[2] * 256) + fdType[3];
                    } else {
                        achar = '\x00';
                        ashort = 0x0000;
                    }
                } else {
                    achar = '\x00';
                    ashort = 0x0000;
                }

                *data++ = achar;
                *data++ = 0;
                memcpy(data, &ashort, sizeof(ashort));
                data += sizeof(ashort);
                memset(data, 0, sizeof(ashort));
                data += sizeof(ashort);
            }

            break;

        case FILPBIT_EXTDFLEN:
            aint = htonl((uint64_t)st->st_size >> 32);
            memcpy(data, &aint, sizeof(aint));
            data += sizeof(aint);
            aint = htonl(st->st_size);
            memcpy(data, &aint, sizeof(aint));
            data += sizeof(aint);
            break;

        case FILPBIT_EXTRFLEN:
            if (adp) {
                aint = htonl((uint64_t)adp->ad_rlen >> 32);
                memcpy(data, &aint, sizeof(aint));
                data += sizeof(aint);
                aint = htonl(adp->ad_rlen);
                memcpy(data, &aint, sizeof(aint));
                data += sizeof(aint);
            } else {
                int64_t rlen = hton64(ad_reso_size(path->u_name, 0, NULL));
                memcpy(data, &rlen, sizeof(rlen));
                data += sizeof(rlen);
            }

            break;

        case FILPBIT_UNIXPR :
            /* accessmode may change st_mode with ACLs */
            accessmode(obj, vol, upath, &ma, dir, st);
            aint = htonl(st->st_uid);
            memcpy(data, &aint, sizeof(aint));
            data += sizeof(aint);
            aint = htonl(st->st_gid);
            memcpy(data, &aint, sizeof(aint));
            data += sizeof(aint);
            /* FIXME: ugly hack
                   type == slnk indicates an OSX style symlink,
                   we have to add S_IFLNK to the mode, otherwise
                   10.3 clients freak out. */
            aint = st->st_mode;

            /*
             * ad_open() does not initialize adouble header
             * for symlinks. Hence this should be skipped to
             * avoid AFP_ASSERT here. Decision was made to
             * not alter ad_open() behavior so that
             * improper ops on symlink adoubles will be
             * more visible (assert).
             */
            if (adp && (ad_meta_fileno(adp) != AD_SYMLINK)
                    && (ade = ad_entry(adp, ADEID_FINDERI)) != NULL) {
                memcpy(fdType, ade, 4);

                if (memcmp(fdType, "slnk", 4) == 0) {
                    aint |= S_IFLNK;
                }
            }

            aint = htonl(aint);
            memcpy(data, &aint, sizeof(aint));
            data += sizeof(aint);
            *data++ = ma.ma_user;
            *data++ = ma.ma_world;
            *data++ = ma.ma_group;
            *data++ = ma.ma_owner;
            break;

        default :
            return AFPERR_BITMAP;
        }

        bitmap = bitmap >> 1;
        bit++;
    }

    if (l_nameoff) {
        ashort = htons(data - buf);
        memcpy(l_nameoff, &ashort, sizeof(ashort));
        data = set_name(vol, data, dir->d_did, path->m_name, id, 0);
    }

    if (utf_nameoff) {
        ashort = htons(data - buf);
        memcpy(utf_nameoff, &ashort, sizeof(ashort));
        data = set_name(vol, data, dir->d_did, path->m_name, id, utf8);
    }

    *buflen = data - buf;
    return AFP_OK;
}

/* ----------------------- */
int getfilparams(const AFPObj *obj, struct vol *vol, uint16_t bitmap,
                 struct path *path,
                 struct dir *dir, char *buf, size_t *buflen, int in_enumerate)
{
    struct adouble	ad, *adp;
    int                 opened = 0;
    int rc = AFP_OK;
    int flags = 0;
    LOG(log_debug, logtype_afpd, "getfilparams(\"%s\")", path->u_name);
    opened = PARAM_NEED_ADP(bitmap);
    adp = NULL;

    if (opened) {
        char *upath;
        /*
         * Dont check for and resturn open fork attributes when enumerating
         * This saves a lot of syscalls, the client will hopefully only use the result
         * in FPGetFileParms where we return the correct value
         */
        flags = (!in_enumerate
                 && (bitmap & (1 << FILPBIT_ATTR))) ? ADFLAGS_CHECK_OF : 0;
        adp = of_ad(vol, path, &ad);
        upath = path->u_name;

        if (ad_metadata(upath, flags, adp) < 0) {
            switch (errno) {
            case EACCES:
                LOG(log_error, logtype_afpd,
                    "getfilparams(%s): %s: check resource fork permission?",
                    upath, strerror(errno));
                return AFPERR_ACCESS;

            case EIO:
                LOG(log_error, logtype_afpd, "getfilparams(%s): bad resource fork", upath);

            /* fall through */
            case ENOENT:
            default:
                adp = NULL;
                break;
            }
        }
    }

    rc = getmetadata(obj, vol, bitmap, path, dir, buf, buflen, adp);

    if (opened) {
        ad_close(adp, ADFLAGS_HF | flags);
    }

    return rc;
}

/* ----------------------------- */
int afp_createfile(AFPObj *obj, char *ibuf, size_t ibuflen _U_, char *rbuf _U_,
                   size_t *rbuflen)
{
    struct adouble	ad;
    struct vol		*vol;
    struct dir		*dir;
    struct ofork        *of = NULL;
    char		*path, *upath;
    int			creatf, did, openf, retvalue = AFP_OK;
    uint16_t		vid;
    struct path		*s_path;
    *rbuflen = 0;
    ibuf++;
    creatf = (unsigned char) * ibuf++;
    memcpy(&vid, ibuf, sizeof(vid));
    ibuf += sizeof(vid);

    if (NULL == (vol = getvolbyvid(vid))) {
        return AFPERR_PARAM;
    }

    if (vol->v_flags & AFPVOL_RO) {
        return AFPERR_VLOCK;
    }

    memcpy(&did, ibuf, sizeof(did));
    ibuf += sizeof(did);

    if (NULL == (dir = dirlookup(vol, did))) {
        LOG(log_error, logtype_afpd,
            "afp_createfile: dirlookup failed for did:%u, returning afp_errno=%d",
            ntohl(did), afp_errno);
        return afp_errno;
    }

    if (NULL == (s_path = cname(vol, dir, &ibuf))) {
        LOG(log_error, logtype_afpd,
            "afp_createfile: cname failed, returning error");
        return get_afp_errno(AFPERR_PARAM);
    }

    if (*s_path->m_name == '\0') {
        return AFPERR_BADTYPE;
    }

    upath = s_path->u_name;
    ad_init(&ad, vol);

    /* if upath is deleted we already in trouble anyway */
    if ((of = of_findname(vol, s_path))) {
        if (creatf) {
            return AFPERR_BUSY;
        } else {
            return AFPERR_EXIST;
        }
    }

    if (creatf) {
        openf = ADFLAGS_RDWR | ADFLAGS_CREATE | ADFLAGS_TRUNC;
    } else
        /* on a soft create, if the file is open then ad_open won't fail
           because open syscall is not called */
    {
        openf = ADFLAGS_RDWR | ADFLAGS_CREATE | ADFLAGS_EXCL;
    }

    if (ad_open(&ad, upath, ADFLAGS_DF | ADFLAGS_HF | ADFLAGS_NOHF | openf,
                0666) < 0) {
        switch (errno) {
        case EROFS:
            return AFPERR_VLOCK;

        case ENOENT : /* we were already in 'did folder' so chdir() didn't fail */
            return AFPERR_NOOBJ;

        case EEXIST :
            return AFPERR_EXIST;

        case EACCES :
            return AFPERR_ACCESS;

        case EDQUOT:
        case ENOSPC :
            LOG(log_info, logtype_afpd, "afp_createfile: DISK FULL");
            return AFPERR_DFULL;

        default :
            return AFPERR_PARAM;
        }
    }

    if (ad_meta_fileno(&ad) == -1) {     /* Hard META / HF */
        /* FIXME with hard create on an existing file, we already
         * corrupted the data file.
         */
        netatalk_unlink(upath);
        ad_close(&ad, ADFLAGS_DF);
        return AFPERR_ACCESS;
    }

    path = s_path->m_name;
    ad_setname(&ad, path);
    struct stat st;

    if (lstat(upath, &st) != 0) {
        LOG(log_error, logtype_afpd, "afp_createfile(\"%s\"): stat: %s",
            upath, strerror(errno));
        ad_close(&ad, ADFLAGS_DF | ADFLAGS_HF);
        return AFPERR_MISC;
    }

    cnid_t id;
    size_t upath_len = upath ? strnlen(upath, CNID_MAX_PATH_LEN) : 0;

    if ((id = get_id(vol, &ad, &st, dir->d_did, upath,
                     (int)upath_len)) == CNID_INVALID) {
        LOG(log_error, logtype_afpd, "afp_createfile(\"%s\"): CNID error", upath);
        goto createfile_iderr;
    }

    (void)ad_setid(&ad, st.st_dev, st.st_ino, id, dir->d_did, vol->v_stamp);
createfile_iderr:
    ad_flush(&ad);
    ad_close(&ad, ADFLAGS_DF | ADFLAGS_HF);
    fce_register(obj, FCE_FILE_CREATE, fullpathname(upath), NULL);

    /* Defensive check: curdir may be corrupted by race conditions or cname() failures */
    if (curdir == NULL) {
        LOG(log_error, logtype_afpd,
            "afp_createfile: curdir is NULL after operations, likely due to concurrent deletion or cname() failure");
        return AFPERR_MISC;
    }

    curdir->d_offcnt++;
    setvoltime(obj, vol);
    return retvalue;
}

int afp_setfilparams(AFPObj *obj, char *ibuf, size_t ibuflen _U_,
                     char *rbuf _U_, size_t *rbuflen)
{
    struct vol	*vol;
    struct dir	*dir;
    struct path *s_path;
    int		did, rc;
    uint16_t	vid, bitmap;
    *rbuflen = 0;
    ibuf += 2;
    memcpy(&vid, ibuf, sizeof(vid));
    ibuf += sizeof(vid);

    if (NULL == (vol = getvolbyvid(vid))) {
        return AFPERR_PARAM;
    }

    if (vol->v_flags & AFPVOL_RO) {
        return AFPERR_VLOCK;
    }

    memcpy(&did, ibuf, sizeof(did));
    ibuf += sizeof(did);

    /* Use strict lookup for parent directory validation (like afp_delete) */
    if (NULL == (dir = dirlookup_strict(vol, did))) {
        return afp_errno;
    }

    memcpy(&bitmap, ibuf, sizeof(bitmap));
    bitmap = ntohs(bitmap);
    ibuf += sizeof(bitmap);

    if (NULL == (s_path = cname(vol, dir, &ibuf))) {
        return get_afp_errno(AFPERR_PARAM);
    }

    if (path_isadir(s_path)) {
        /* it's a directory */
        return AFPERR_BADTYPE;
    }

    if (s_path->st_errno != 0) {
        return AFPERR_NOOBJ;
    }

    /* Validate target FILE inode to detect external replacement (like afp_delete) */
    struct dir *cachedfile = NULL;

    if (s_path->u_name != NULL) {
        size_t uname_len = strnlen(s_path->u_name, CNID_MAX_PATH_LEN);
        cachedfile = dircache_search_by_name(vol, curdir, s_path->u_name, uname_len);

        if (cachedfile != NULL && cachedfile->dcache_ino != s_path->st.st_ino) {
            /* File was replaced externally - inode mismatch */
            LOG(log_warning, logtype_afpd,
                "afp_setfilparams: inode mismatch for \"%s\" (cached:%llu vs current:%llu), file replaced externally",
                s_path->u_name, (unsigned long long)cachedfile->dcache_ino,
                (unsigned long long)s_path->st.st_ino);
            /* Invalidate stale cache entry */
            dir_remove(vol, cachedfile);
            /* Return error - the file the client knew about no longer exists */
            return AFPERR_NOOBJ;
        }
    }

    if ((intptr_t)ibuf & 1) {
        ibuf++;
    }

    if (AFP_OK == (rc = setfilparams(obj, vol, s_path, bitmap, ibuf))) {
        setvoltime(obj, vol);
    }

    return rc;
}

/*
 * cf AFP3.0.pdf page 252 for change_mdate and change_parent_mdate logic
 *
*/
extern struct path Cur_Path;

int setfilparams(const AFPObj *obj, struct vol *vol,
                 struct path *path, uint16_t f_bitmap, char *buf)
{
    struct adouble	ad, *adp;
    struct extmap	*em;
    int			bit, isad = 1, err = AFP_OK;
    char                *upath;
    char		*ade = NULL;
    uint8_t              achar, xyy[4];
    const uint8_t        *fdType = NULL;
    uint16_t		ashort = 0;
    uint16_t		bshort;
    uint16_t		oshort;
    uint32_t		aint;
    uint32_t		upriv = 0;
    uint16_t           upriv_bit = 0;
    struct utimbuf	ut;
    int                 change_mdate = 0;
    int                 change_parent_mdate = 0;
    int                 newdate = 0;
    struct timeval      tv;
    uid_t		f_uid;
    gid_t		f_gid;
    uint16_t           bitmap = f_bitmap;
    uint32_t           cdate, bdate;
    uint8_t              finder_buf[32];
    int symlinked = S_ISLNK(path->st.st_mode);
    int fp;
    ssize_t len;
    char symbuf[MAXPATHLEN + 1];
    LOG(log_debug9, logtype_afpd, "begin setfilparams:");
    adp = of_ad(vol, path, &ad);
    upath = path->u_name;

    if (!vol_unix_priv(vol) && check_access(obj, vol, upath, OPENACC_WR) < 0) {
        return AFPERR_ACCESS;
    }

    /* with unix priv maybe we have to change adouble file priv first */
    bit = 0;

    while (bitmap != 0) {
        while ((bitmap & 1) == 0) {
            bitmap = bitmap >> 1;
            bit++;
        }

        switch (bit) {
        case FILPBIT_ATTR :

            /* Preserve file dates during a GS/OS folder copy */
            if (obj->afp_version >= 30) {
                change_mdate = 1;
            }

            memcpy(&ashort, buf, sizeof(ashort));
            buf += sizeof(ashort);
            break;

        case FILPBIT_CDATE :
            change_mdate = 1;
            memcpy(&cdate, buf, sizeof(cdate));
            buf += sizeof(cdate);
            break;

        case FILPBIT_MDATE :
            memcpy(&newdate, buf, sizeof(newdate));
            buf += sizeof(newdate);
            break;

        case FILPBIT_BDATE :
            change_mdate = 1;
            memcpy(&bdate, buf, sizeof(bdate));
            buf += sizeof(bdate);
            break;

        case FILPBIT_FINFO :
            change_mdate = 1;

            if (memcmp(buf, "slnkrhap", 8) == 0
                    && !(S_ISLNK(path->st.st_mode))
                    && !(vol->v_flags & AFPVOL_FOLLOWSYM)) {
                /* request to turn this into a symlink */
                if ((fp = open(path->u_name, O_RDONLY)) == -1) {
                    err = AFPERR_MISC;
                    goto setfilparam_done;
                }

                len = read(fp, symbuf, MAXPATHLEN);
                close(fp);

                if (!(len > 0)) {
                    err = AFPERR_MISC;
                    goto setfilparam_done;
                }

                if (unlink(path->u_name) != 0) {
                    err = AFPERR_MISC;
                    goto setfilparam_done;
                }

                symbuf[len] = 0;

                if (!symlink_target_safe(vol, path->u_name, symbuf)) {
                    err = AFPERR_ACCESS;
                    goto setfilparam_done;
                }

                if (symlink(symbuf, path->u_name) != 0) {
                    err = AFPERR_MISC;
                    goto setfilparam_done;
                }

                of_stat(vol, path);
                symlinked = 1;
            }

            memcpy(finder_buf, buf, 32);
            buf += 32;
            break;

        case FILPBIT_UNIXPR :
            if (!vol_unix_priv(vol)) {
                /* this volume doesn't use unix priv */
                err = AFPERR_BITMAP;
                bitmap = 0;
                break;
            }

            change_mdate = 1;
            change_parent_mdate = 1;
            memcpy(&aint, buf, sizeof(aint));
            f_uid = ntohl(aint);
            buf += sizeof(aint);
            memcpy(&aint, buf, sizeof(aint));
            f_gid = ntohl(aint);
            buf += sizeof(aint);
            setfilowner(vol, f_uid, f_gid, path);
            memcpy(&upriv, buf, sizeof(upriv));
            buf += sizeof(upriv);
            upriv = ntohl(upriv);

            if (upriv & S_IWUSR) {
                setfilunixmode(vol, path, upriv);
            } else {
                /* do it later */
                upriv_bit = 1;
            }

            break;

        case FILPBIT_PDINFO :
            if (obj->afp_version < 30) { /* else it's UTF8 name */
                achar = *buf;
                buf += 2;

                /* Keep special case to support crlf translations */
                if ((unsigned int) achar == 0x04) {
                    fdType = (uint8_t *)"TEXT";
                    buf += 2;
                } else {
                    xyy[0] = (uint8_t) 'p';
                    xyy[1] = achar;
                    xyy[3] = *buf++;
                    xyy[2] = *buf++;
                    fdType = xyy;
                }

                break;
            }

        /* fallthrough */
        default :
            err = AFPERR_BITMAP;
            /* break while loop */
            bitmap = 0;
            break;
        }

        bitmap = bitmap >> 1;
        bit++;
    }

    /* second try with adouble open
    */
    if (ad_open(adp, upath, ADFLAGS_HF | ADFLAGS_RDWR | ADFLAGS_CREATE, 0666) < 0) {
        LOG(log_debug, logtype_afpd, "setfilparams: ad_open_metadata error: %s",
            strerror(errno));

        /*
         * For some things, we don't need an adouble header:
         * - change of modification date
         * - UNIX privs (Bug-ID #2863424)
         */
        if (!symlinked && f_bitmap & ~(1 << FILPBIT_MDATE | 1 << FILPBIT_UNIXPR)) {
            LOG(log_debug, logtype_afpd, "setfilparams: need adouble access");
            return AFPERR_ACCESS;
        }

        LOG(log_debug, logtype_afpd,
            "setfilparams: no adouble perms, but only FILPBIT_MDATE and/or FILPBIT_UNIXPR");
        isad = 0;
    } else if (ad_get_MD_flags(adp) & O_CREAT) {
        ad_setname(adp, path->m_name);
        cnid_t id;
        size_t upath_len = upath ? strnlen(upath, CNID_MAX_PATH_LEN) : 0;

        if ((id = get_id(vol, adp, &path->st, curdir->d_did, upath,
                         (int)upath_len)) == CNID_INVALID) {
            LOG(log_error, logtype_afpd, "afp_createfile(\"%s\"): CNID error", upath);
            return AFPERR_MISC;
        }

        (void)ad_setid(adp, path->st.st_dev, path->st.st_ino, id, curdir->d_did,
                       vol->v_stamp);
    }

    bit = 0;
    bitmap = f_bitmap;

    while (bitmap != 0) {
        while ((bitmap & 1) == 0) {
            bitmap = bitmap >> 1;
            bit++;
        }

        switch (bit) {
        case FILPBIT_ATTR :
            if (isad == 0) {
                break;
            }

            ad_getattr(adp, &bshort);
            oshort = bshort;

            if (ntohs(ashort) & ATTRBIT_SETCLR) {
                ashort &= ~htons(vol->v_ignattr);
                bshort |= htons(ntohs(ashort) & ~ATTRBIT_SETCLR);
            } else {
                bshort &= ~ashort;
            }

            if ((bshort & htons(ATTRBIT_INVISIBLE)) != (oshort & htons(
                        ATTRBIT_INVISIBLE))) {
                change_parent_mdate = 1;
            }

            ad_setattr(adp, bshort);
            break;

        case FILPBIT_CDATE :
            if (isad == 0) {
                break;
            }

            ad_setdate(adp, AD_DATE_CREATE, cdate);
            break;

        case FILPBIT_MDATE :
            break;

        case FILPBIT_BDATE :
            if (isad == 0) {
                break;
            }

            ad_setdate(adp, AD_DATE_BACKUP, bdate);
            break;

        case FILPBIT_FINFO :
            if (isad == 0) {
                break;
            }

            ade = ad_entry(adp, ADEID_FINDERI);

            if (ade == NULL) {
                LOG(log_debug, logtype_afpd, "setfilparams(\"%s\"): invalid FinderInfo",
                    path->u_name);
                break;
            }

            if (default_type(ade)
                    && (
                        ((em = getextmap(path->m_name)) &&
                         !memcmp(finder_buf, em->em_type, sizeof(em->em_type)) &&
                         !memcmp(finder_buf + 4, em->em_creator, sizeof(em->em_creator)))
                        || ((em = getdefextmap()) &&
                            !memcmp(finder_buf, em->em_type, sizeof(em->em_type)) &&
                            !memcmp(finder_buf + 4, em->em_creator, sizeof(em->em_creator)))
                    )) {
                memcpy(finder_buf, ufinderi, 8);
            }

            memcpy(ade, finder_buf, 32);
            break;

        case FILPBIT_UNIXPR :
            if (upriv_bit) {
                setfilunixmode(vol, path, upriv);
            }

            break;

        case FILPBIT_PDINFO :
            if (isad == 0) {
                break;
            }

            if (obj->afp_version < 30) { /* else it's UTF8 name */
                if ((ade = ad_entry(adp, ADEID_FINDERI)) != NULL) {
                    memcpy(ade, fdType, 4);
                    memcpy(ade + 4, "pdos", 4);
                }

                break;
            }

        /* fallthrough */
        default :
            err = AFPERR_BITMAP;
            goto setfilparam_done;
        }

        bitmap = bitmap >> 1;
        bit++;
    }

setfilparam_done:

    if (change_mdate && newdate == 0 && gettimeofday(&tv, NULL) == 0) {
        newdate = AD_DATE_FROM_UNIX(tv.tv_sec);
    }

    if (newdate) {
        if (isad) {
            ad_setdate(adp, AD_DATE_MODIFY, newdate);
        }

        if (upath) {
            ut.actime = ut.modtime = AD_DATE_TO_UNIX(newdate);
            utime(upath, &ut);
        }
    }

    if (isad) {
        ad_flush(adp);
        ad_close(adp, ADFLAGS_HF);
    }

    if (change_parent_mdate && gettimeofday(&tv, NULL) == 0) {
        newdate = AD_DATE_FROM_UNIX(tv.tv_sec);
        bitmap = 1 << FILPBIT_MDATE;
        setdirparams(vol, &Cur_Path, bitmap, (char *)&newdate);
    }

    LOG(log_debug9, logtype_afpd, "end setfilparams:");
    return err;
}

/*!
 * @brief Rename a file, including its resource fork and mac name.
 *
 * renamefile and copyfile take the old and new unix pathnames
 * and the new mac name.
 *
 * @param vol         volume structure
 * @param ddir        dest dir structure
 * @param sdir_fd     source dir fd to which src path is relative (for openat et al semantics)
 *                    passing -1 means this is not used, src path is a full path
 * @param src         the source path
 * @param dst         the dest filename in current dir
 * @param newname     the dest mac name
 * @param adp         adouble struct of src file, if open, or & zeroed one
 *
 */
int renamefile(struct vol *vol, struct dir *ddir, int sdir_fd, char *src,
               char *dst, char *newname, struct adouble *adp)
{
    int		rc;
    LOG(log_debug, logtype_afpd,
        "renamefile: src[%d, \"%s\"] -> dst[\"%s\"]", sdir_fd, src, dst);
    /* Clean BOTH source and destination cache entries before rename to prevent races
     * and maintain cache consistency.
     * - Source: Will have stale path after rename
     * - Destination: May have stale collision that needs cleanup
     * - Rename only happens if destination stat does not exist (no clobber)
     */
    /* Look up and clean SOURCE dircache entry being renamed */
    struct dir *source_entry = NULL;

    if (src != NULL) {
        size_t src_len = strnlen(src, CNID_MAX_PATH_LEN);
        source_entry = dircache_search_by_name(vol, ddir, src, src_len);
    }

    if (source_entry) {
        LOG(log_debug, logtype_afpd,
            "renamefile: cleaning source dircache \"%s\" before rename (will be stale)",
            src);
        dir_remove(vol, source_entry);
    }

    /* Look up and clean DESTINATION dircache entry for stale detection */
    struct dir *dest_entry = NULL;

    if (dst != NULL) {
        size_t dst_len = strnlen(dst, CNID_MAX_PATH_LEN);
        dest_entry = dircache_search_by_name(vol, ddir, dst, dst_len);
    }

    if (dest_entry) {
        LOG(log_debug, logtype_afpd,
            "renamefile: cleaning stale destination dircache \"%s\" before rename", dst);
        dir_remove(vol, dest_entry);
    }

    if (unix_rename(sdir_fd, src, -1, dst) < 0) {
        switch (errno) {
        case ENOENT :
            return AFPERR_NOOBJ;

        case EPERM:
        case EACCES :
            return AFPERR_ACCESS;

        case EROFS:
            return AFPERR_VLOCK;

        case EXDEV :			/* Cross device move -- try copy */

            /* NOTE: with open file it's an error because after the copy we will
             * get two files, it's fixable for our process (eg reopen the new file, get the
             * locks, and so on. But it doesn't solve the case with a second process
             */
            if (adp->ad_open_forks) {
                /* FIXME  warning in syslog so admin'd know there's a conflict ?*/
                return AFPERR_OLOCK; /* little lie */
            }

            if (AFP_OK != (rc = copyfile(vol, vol, ddir, sdir_fd, src, dst, newname,
                                         NULL))) {
                /* on error copyfile delete dest */
                return rc;
            }

            return deletefile(vol, sdir_fd, src, 0);

        default :
            return AFPERR_PARAM;
        }
    }

    if (vol->vfs->vfs_renamefile(vol, sdir_fd, src, dst) < 0) {
        int err;
        err = errno;

        /* try to undo the data fork rename,
         * we know we are on the same device
         */
        if (err) {
            unix_rename(-1, dst, sdir_fd, src);

            /* return the first error */
            switch (err) {
            case ENOENT :
                return AFPERR_NOOBJ;

            case EPERM:
            case EACCES :
                return AFPERR_ACCESS ;

            case EROFS:
                return AFPERR_VLOCK;

            default :
                return AFPERR_PARAM ;
            }
        }
    }

    /* don't care if we can't open the newly renamed resource fork */
    if (ad_open(adp, dst, ADFLAGS_HF | ADFLAGS_RDWR) == 0) {
        ad_setname(adp, newname);
        ad_flush(adp);
        ad_close(adp, ADFLAGS_HF);
    }

    return AFP_OK;
}

/*!
   convert a Mac long name to an utf8 name
*/
size_t mtoUTF8(const struct vol *vol, const char *src, size_t srclen,
               char *dest, size_t destlen)
{
    size_t    outlen;

    if ((size_t) -1 == (outlen = convert_string(vol->v_maccharset, CH_UTF8_MAC, src,
                                 srclen, dest, destlen))) {
        return -1;
    }

    return outlen;
}

/* ---------------- */
int copy_path_name(const struct vol *vol, char *newname, char *ibuf)
{
    char        type = *ibuf;
    size_t      plen = 0;
    uint16_t   len16;
    uint32_t   hint;

    if (type != 2 && !(vol->v_obj->afp_version >= 30 && type == 3)) {
        return -1;
    }

    ibuf++;

    switch (type) {
    case 2:
        if ((plen = (unsigned char) * ibuf++) != 0) {
            if (vol->v_obj->afp_version >= 30) {
                /* convert it to UTF8
                */
                if ((plen = mtoUTF8(vol, ibuf, plen, newname, AFPOBJ_TMPSIZ)) == (size_t) -1) {
                    return -1;
                }
            } else {
                strncpy(newname, ibuf, plen);
                newname[plen] = '\0';
            }

            if (strlen(newname) != plen) {
                /* there's \0 in newname, e.g. it's a pathname not
                 * only a filename.
                */
                return -1;
            }
        }

        break;

    case 3:
        memcpy(&hint, ibuf, sizeof(hint));
        ibuf += sizeof(hint);
        memcpy(&len16, ibuf, sizeof(len16));
        ibuf += sizeof(len16);
        plen = ntohs(len16);

        if (plen) {
            if (plen > AFPOBJ_TMPSIZ) {
                return -1;
            }

            strncpy(newname, ibuf, plen);
            newname[plen] = '\0';

            if (strlen(newname) != plen) {
                return -1;
            }
        }

        break;
    }

    return plen;
}

/* -----------------------------------
*/
int afp_copyfile(AFPObj *obj, char *ibuf, size_t ibuflen _U_, char *rbuf _U_,
                 size_t *rbuflen)
{
    struct vol	*s_vol, *d_vol;
    struct dir	*dir;
    char	*newname;
    char	*p;
    char	*upath = NULL;
    struct path *s_path;
    uint32_t	sdid, ddid;
    int         err = AFP_OK;
    int         retvalue = AFP_OK;
    uint16_t	svid, dvid;
    struct adouble ad, *adp;
    int denyreadset;
    *rbuflen = 0;
    ibuf += 2;
    memcpy(&svid, ibuf, sizeof(svid));
    ibuf += sizeof(svid);

    if (NULL == (s_vol = getvolbyvid(svid))) {
        return AFPERR_PARAM;
    }

    memcpy(&sdid, ibuf, sizeof(sdid));
    ibuf += sizeof(sdid);

    if (NULL == (dir = dirlookup(s_vol, sdid))) {
        return afp_errno;
    }

    memcpy(&dvid, ibuf, sizeof(dvid));
    ibuf += sizeof(dvid);
    memcpy(&ddid, ibuf, sizeof(ddid));
    ibuf += sizeof(ddid);

    if (NULL == (s_path = cname(s_vol, dir, &ibuf))) {
        return get_afp_errno(AFPERR_PARAM);
    }

    if (path_isadir(s_path)) {
        return AFPERR_BADTYPE;
    }

    /* don't allow copies when the file is open.
     * XXX: the spec only calls for read/deny write access.
     *      however, copyfile doesn't have any of that info,
     *      and locks need to stay coherent. as a result,
     *      we just balk if the file is opened already. */
    adp = of_ad(s_vol, s_path, &ad);

    if (ad_open(adp, s_path->u_name,
                ADFLAGS_DF | ADFLAGS_HF | ADFLAGS_NOHF | ADFLAGS_RDONLY | ADFLAGS_SETSHRMD) <
            0) {
        return AFPERR_DENYCONF;
    }

#ifdef HAVE_FSHARE_T
    fshare_t shmd;
    shmd.f_access = F_RDACC;
    shmd.f_deny = F_NODNY;

    if (fcntl(ad_data_fileno(adp), F_SHARE, &shmd) != 0) {
        retvalue = AFPERR_DENYCONF;
        goto copy_exit;
    }

    if (AD_RSRC_OPEN(adp) && fcntl(ad_reso_fileno(adp), F_SHARE, &shmd) != 0) {
        retvalue = AFPERR_DENYCONF;
        goto copy_exit;
    }

#endif
    denyreadset = (ad_testlock(adp, ADEID_DFORK, AD_FILELOCK_DENY_RD) != 0 ||
                   ad_testlock(adp, ADEID_RFORK, AD_FILELOCK_DENY_RD) != 0);

    if (denyreadset) {
        retvalue = AFPERR_DENYCONF;
        goto copy_exit;
    }

    newname = obj->newtmp;
    strcpy(newname, s_path->m_name);
    p = ctoupath(s_vol, curdir, newname);

    if (!p) {
        retvalue = AFPERR_PARAM;
        goto copy_exit;
    }

    if (NULL == (d_vol = getvolbyvid(dvid))) {
        retvalue = AFPERR_PARAM;
        goto copy_exit;
    }

    if (d_vol->v_flags & AFPVOL_RO) {
        retvalue = AFPERR_VLOCK;
        goto copy_exit;
    }

    /* Strict validate dest parent to ensure copy to correct location */
    if (NULL == (dir = dirlookup_strict(d_vol, ddid))) {
        retvalue = afp_errno;
        goto copy_exit;
    }

    if ((s_path = cname(d_vol, dir, &ibuf)) == NULL) {
        retvalue = get_afp_errno(AFPERR_NOOBJ);
        goto copy_exit;
    }

    if (*s_path->m_name != '\0') {
        retvalue = path_error(s_path, AFPERR_NOOBJ);
        goto copy_exit;
    }

    /* one of the handful of places that knows about the path type */
    if (copy_path_name(d_vol, newname, ibuf) < 0) {
        retvalue = AFPERR_PARAM;
        goto copy_exit;
    }

    /* Defensive check: curdir may be corrupted by race conditions or cname() failures */
    if (curdir == NULL) {
        LOG(log_error, logtype_afpd,
            "afp_copyfile: curdir is NULL after cname(), likely due to concurrent deletion");
        retvalue = AFPERR_MISC;
        goto copy_exit;
    }

    /* newname is always only a filename so curdir *is* its
     * parent folder
     */
    if (NULL == (upath = mtoupath(d_vol, newname, curdir->d_did,
                                  utf8_encoding(d_vol->v_obj)))) {
        retvalue = AFPERR_PARAM;
        goto copy_exit;
    }

    if ((err = copyfile(s_vol, d_vol, curdir, -1, p, upath, newname, adp)) < 0) {
        retvalue = err;
        goto copy_exit;
    }

    curdir->d_offcnt++;
    setvoltime(obj, d_vol);
copy_exit:

    if (upath && err == AFP_OK) {
        fce_register(obj, FCE_FILE_CREATE, fullpathname(upath), NULL);
    }

    ad_close(adp, ADFLAGS_DF | ADFLAGS_HF | ADFLAGS_SETSHRMD);
    return retvalue;
}

/*!
 * @note if newname is NULL (from directory.c) we don't want to copy the resource fork.
 * because we are doing it elsewhere.
 * currently if newname is NULL then adp is NULL.
 */
int copyfile(struct vol *s_vol,
             struct vol *d_vol,
             struct dir *d_dir,
             int sfd,
             char *src,
             char *dst,
             char *newname,
             struct adouble *adp)
{
    struct adouble	ads, add;
    int			err = 0;
    int                 adflags;
    int                 stat_result;
    struct stat         st;
    LOG(log_debug, logtype_afpd, "copyfile(sfd:%d,s:'%s',d:'%s',n:'%s')",
        sfd, src, dst, newname);

    if (adp == NULL) {
        ad_init(&ads, s_vol);
        adp = &ads;
    }

    adflags = ADFLAGS_DF | ADFLAGS_HF | ADFLAGS_NOHF | ADFLAGS_RF | ADFLAGS_NORF;

    if (ad_openat(adp, sfd, src, adflags | ADFLAGS_RDONLY) < 0) {
        err = errno;
        goto done;
    }

    if (!AD_META_OPEN(adp))
        /* no resource fork, don't create one for dst file */
    {
        adflags &= ~ADFLAGS_HF;
    }

    if (!AD_RSRC_OPEN(adp))
        /* no resource fork, don't create one for dst file */
    {
        adflags &= ~ADFLAGS_RF;
    }

    /* saving stat exit code, thus saving us on one more stat later on */
    stat_result = fstat(ad_data_fileno(adp), &st);

    if (stat_result < 0) {
        /* unlikely but if fstat fails, the default file mode will be 0666. */
        st.st_mode = S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH;
    }

    ad_init(&add, d_vol);

    if (ad_open(&add, dst, adflags | ADFLAGS_RDWR | ADFLAGS_CREATE | ADFLAGS_EXCL,
                st.st_mode | S_IRUSR | S_IWUSR) < 0) {
        err = errno;
        ad_close(adp, adflags);

        if (EEXIST != err) {
            deletefile(d_vol, -1, dst, 0);
            goto done;
        }

        return AFPERR_EXIST;
    }

    size_t copybuf_len = 0;
    uint8_t *copybuf = NULL;

    if (s_vol->v_obj->proto == AFPPROTO_DSI) {
        copybuf = s_vol->v_obj->dsi->commands;
        copybuf_len = s_vol->v_obj->dsi->server_quantum;
    }

    if (copy_fork(ADEID_DFORK, &add, adp,
                  copybuf,
                  copybuf_len) != 0) {
        err = errno;
        LOG(log_error, logtype_afpd, "copyfile('%s'): copy_fork: %s", src,
            strerror(errno));
    } else if (d_vol->vfs->vfs_copyfile(d_vol, sfd, src, dst) != AFP_OK) {
        err = errno;
        LOG(log_error, logtype_afpd, "copyfile('%s'): vfs_copyfile: %s", src,
            strerror(errno));
    }

    if (AD_META_OPEN(&add)) {
        if (AD_META_OPEN(adp)) {
            ad_copy_header(&add, adp);
        }

        ad_setname(&add, dst);
        cnid_t id;
        struct stat stdest;

        if (fstat(ad_meta_fileno(&add), &stdest) != 0) {
            err = errno;
            goto error;
        }

        if ((id = get_id(d_vol, &add, &stdest, d_dir->d_did, dst,
                         strlen(dst))) == CNID_INVALID) {
            err = EINVAL;
            goto error;
        }

        (void)ad_setid(&add, stdest.st_dev, stdest.st_ino, id, d_dir->d_did,
                       d_vol->v_stamp);
        ad_flush(&add);
    }

error:
    ad_close(adp, adflags);

    if (ad_close(&add, adflags) < 0) {
        err = errno;
    }

    if (err) {
        deletefile(d_vol, -1, dst, 0);
    } else if (stat_result == 0) {
        /* set dest modification date to src date */
        struct utimbuf	ut;
        ut.actime = ut.modtime = st.st_mtime;
        utime(dst, &ut);
        /* FIXME netatalk doesn't use resource fork file date
         * but maybe we should set its modtime too.
        */
    }

done:

    switch (err) {
    case 0:
        return AFP_OK;

    case EDQUOT:
    case EFBIG:
    case ENOSPC:
        LOG(log_info, logtype_afpd, "copyfile: DISK FULL");
        return AFPERR_DFULL;

    case ENOENT:
        return AFPERR_NOOBJ;

    case EACCES:
        return AFPERR_ACCESS;

    case EROFS:
        return AFPERR_VLOCK;
    }

    return AFPERR_PARAM;
}


/* -----------------------------------
   vol: not NULL delete cnid entry. then we are in curdir and file is a only filename
   checkAttrib:   1 check kFPDeleteInhibitBit (deletfile called by afp_delete)

   when deletefile is called we don't have lock on it, file is closed (for us)
   untrue if called by renamefile

   ad_open always try to open file RDWR first and ad_lock takes care of
   WRITE lock on read only file.
*/

static int check_attrib(const struct vol *vol, struct adouble *adp)
{
    uint16_t   bshort = 0;
    ad_getattr(adp, &bshort);

    /*
     * Does kFPDeleteInhibitBit (bit 8) set?
     */
    if (!(vol->v_ignattr & ATTRBIT_NODELETE)
            && (bshort & htons(ATTRBIT_NODELETE))) {
        return AFPERR_OLOCK;
    }

    if (bshort & htons(ATTRBIT_DOPEN | ATTRBIT_ROPEN)) {
        return AFPERR_BUSY;
    }

    return 0;
}

/*!
 * @note dirfd can be used for unlinkat semantics
 */
int deletefile(const struct vol *vol, int dirfd, char *file, int checkAttrib)
{
    struct adouble	ad;
    struct adouble      *adp = NULL;
    int			adflags, err = AFP_OK;
    int			meta = 0;

    if (file == NULL) {
        LOG(log_error, logtype_afpd, "deletefile: file parameter is NULL");
        return AFPERR_PARAM;
    }

    LOG(log_debug, logtype_afpd, "deletefile('%s')", file);
    ad_init(&ad, vol);

    if (checkAttrib) {
        /* was EACCESS error try to get only metadata */
        /* we never want to create a resource fork here, we are going to delete it
         * moreover sometimes deletefile is called with a no existent file and
         * ad_open would create a 0 byte resource fork
        */
        if (ad_metadataat(dirfd, file, ADFLAGS_CHECK_OF, &ad) == 0) {
            if ((err = check_attrib(vol, &ad))) {
                ad_close(&ad, ADFLAGS_HF | ADFLAGS_CHECK_OF);
                return err;
            }

            meta = 1;
        }
    }

    /* try to open both forks at once */
    adflags = ADFLAGS_DF;

    if (ad_openat(&ad, dirfd, file,
                  adflags | ADFLAGS_RF | ADFLAGS_NORF | ADFLAGS_RDONLY) < 0) {
        switch (errno) {
        case ENOENT:
            err = AFPERR_NOOBJ;
            goto end;

        case EACCES: /* maybe it's a file with no write mode for us */
            break;   /* was return AFPERR_ACCESS;*/

        case EROFS:
            err = AFPERR_VLOCK;
            goto end;

        default:
            err = AFPERR_PARAM;
            goto end;
        }
    } else {
        adp = &ad;
    }

    if (adp && AD_RSRC_OPEN(adp)) {   /* there's a resource fork */
        adflags |= ADFLAGS_RF;

        /* FIXME we have a pb here because we want to know if a file is open
         * there's a 'priority inversion' if you can't open the resource fork RW
         * you can delete it if it's open because you can't get a write lock.
         *
         * ADLOCK_FILELOCK means the whole resource fork, not only after the
         * metadatas
         *
         * FIXME it doesn't work for RFORK open read only and fork open without deny mode
         */
        if (ad_tmplock(&ad, ADEID_RFORK, ADLOCK_WR | ADLOCK_FILELOCK, 0, 0, 0) < 0) {
            err = AFPERR_BUSY;
            goto end;
        }
    }

    if (adp && ad_tmplock(&ad, ADEID_DFORK, ADLOCK_WR, 0, 0, 0) < 0) {
        LOG(log_error, logtype_afpd, "deletefile('%s'): ad_tmplock error: %s", file,
            strerror(errno));
        err = AFPERR_BUSY;
    } else if (!(err = vol->vfs->vfs_deletefile(vol, dirfd, file))
               && !(err = netatalk_unlinkat(dirfd, file))) {
        cnid_t id;
        struct dir *cachedfile;

        if (checkAttrib) {
            /* Delete CNID */
            AFP_CNID_START("cnid_get");
            id = cnid_get(vol->v_cdb, curdir->d_did, file, strlen(file));
            AFP_CNID_DONE();

            if (id) {
                AFP_CNID_START("cnid_delete");
                cnid_delete(vol->v_cdb, id);
                AFP_CNID_DONE();
            }

            /* Remove file from dircache */
            /* file guaranteed non-NULL (checked at function entry) */
            AFP_ASSERT(file != NULL);
            cachedfile = dircache_search_by_name(vol, curdir, file,
                                                 strlen(file)); // NOSONAR:

            if (cachedfile) {
                dir_remove(vol, cachedfile);
            }
        }
    }

end:

    if (meta) {
        ad_close(&ad, ADFLAGS_HF | ADFLAGS_CHECK_OF);
    }

    if (adp) {
        /* ad_close removes locks if any */
        ad_close(&ad, adflags);
    }

    return err;
}

/*! @returns a file id */
int afp_createid(AFPObj *obj _U_, char *ibuf, size_t ibuflen _U_, char *rbuf,
                 size_t *rbuflen)
{
    struct stat         *st;
    struct vol		*vol;
    struct dir		*dir;
    char		*upath;
    int                 len;
    cnid_t		did, id;
    u_short		vid;
    struct path         *s_path;
    *rbuflen = 0;
    ibuf += 2;
    memcpy(&vid, ibuf, sizeof(vid));
    ibuf += sizeof(vid);

    if (NULL == (vol = getvolbyvid(vid))) {
        return AFPERR_PARAM;
    }

    if (vol->v_cdb == NULL || !(vol->v_cdb->cnid_db_flags & CNID_FLAG_PERSISTENT)) {
        return AFPERR_NOOP;
    }

    if (vol->v_flags & AFPVOL_RO) {
        return AFPERR_VLOCK;
    }

    memcpy(&did, ibuf, sizeof(did));
    ibuf += sizeof(did);

    if (NULL == (dir = dirlookup(vol, did))) {
        return afp_errno;
    }

    if (NULL == (s_path = cname(vol, dir, &ibuf))) {
        return get_afp_errno(AFPERR_NOOBJ);
    }

    if (path_isadir(s_path)) {
        return AFPERR_BADTYPE;
    }

    upath = s_path->u_name;

    switch (s_path->st_errno) {
    case 0:
        /* success */
        break;

    case EPERM:
    case EACCES:
        return AFPERR_ACCESS;

    case ENOENT:
        return AFPERR_NOOBJ;

    default:
        return AFPERR_PARAM;
    }

    st = &s_path->st;
    len = upath ? (int)strnlen(upath, CNID_MAX_PATH_LEN) : 0;
    AFP_CNID_START("cnid_lookup");
    id = cnid_lookup(vol->v_cdb, st, did, upath, len);
    AFP_CNID_DONE();

    if (id) {
        memcpy(rbuf, &id, sizeof(id));
        *rbuflen = sizeof(id);
        return AFPERR_EXISTID;
    }

    if ((id = get_id(vol, NULL, st, did, upath, len)) != CNID_INVALID) {
        memcpy(rbuf, &id, sizeof(id));
        *rbuflen = sizeof(id);
        return AFP_OK;
    }

    return afp_errno;
}

/* ------------------------------- */
struct reenum {
    struct vol *vol;
    cnid_t     did;
};

static int reenumerate_loop(struct dirent *de, char *mname _U_, void *data)
{
    struct path   path;
    struct reenum *param = data;
    struct vol    *vol = param->vol;
    cnid_t        did  = param->did;
    cnid_t	  aint;

    if (ostat(de->d_name, &path.st, vol_syml_opt(vol)) < 0) {
        return 0;
    }

    /* update or add to cnid */
    AFP_CNID_START("cnid_add");
    /* ignore errors */
    aint = cnid_add(vol->v_cdb, &path.st, did, de->d_name, strlen(de->d_name), 0);
    AFP_CNID_DONE();
    return 0;
}

/* --------------------
 * Ok the db is out of synch with the dir.
 * but if it's a deleted file we don't want to do it again and again.
 */
static int
reenumerate_id(struct vol *vol, char *name, struct dir *dir)
{
    int             ret;
    struct reenum   data;
    struct stat     st;

    if (vol->v_cdb == NULL) {
        return -1;
    }

    /* FIXME use of_statdir ? */
    if (ostat(name, &st, vol_syml_opt(vol))) {
        return -1;
    }

    if (dirreenumerate(dir, &st)) {
        /* we already did it once and the dir haven't been modified */
        return dir->d_offcnt;
    }

    data.vol = vol;
    data.did = dir->d_did;

    if ((ret = for_each_dirent(vol, name, reenumerate_loop, (void *)&data)) >= 0) {
        setdiroffcnt(curdir, &st,  ret);
        dir->d_flags |= DIRF_CNID;
    }

    return ret;
}

/*!
   resolve a file id */
int afp_resolveid(AFPObj *obj, char *ibuf, size_t ibuflen _U_, char *rbuf,
                  size_t *rbuflen)
{
    struct vol		*vol;
    struct dir		*dir;
    char		*upath;
    struct path         path;
    int                 err, retry = 0;
    size_t		buflen;
    cnid_t		id, cnid;
    uint16_t		vid, bitmap;
    static char buffer[12 + MAXPATHLEN + 1];
    int len = 12 + MAXPATHLEN + 1;
    *rbuflen = 0;
    ibuf += 2;
    memcpy(&vid, ibuf, sizeof(vid));
    ibuf += sizeof(vid);

    if (NULL == (vol = getvolbyvid(vid))) {
        return AFPERR_PARAM;
    }

    if (vol->v_cdb == NULL || !(vol->v_cdb->cnid_db_flags & CNID_FLAG_PERSISTENT)) {
        return AFPERR_NOOP;
    }

    memcpy(&id, ibuf, sizeof(id));
    ibuf += sizeof(id);
    cnid = id;

    if (!id) {
        /* some MacOS versions after a catsearch do a *lot* of afp_resolveid with 0 */
        return AFPERR_NOID;
    }

retry:
    AFP_CNID_START("cnid_resolve");
    upath = cnid_resolve(vol->v_cdb, &id, buffer, len);
    AFP_CNID_DONE();

    if (upath == NULL) {
        /* was AFPERR_BADID, but help older Macs */
        return AFPERR_NOID;
    }

    if (NULL == (dir = dirlookup(vol, id))) {
        /* idem AFPERR_PARAM */
        return AFPERR_NOID;
    }

    if (movecwd(vol, dir) < 0) {
        switch (errno) {
        case EACCES:
        case EPERM:
            return AFPERR_ACCESS;

        case ENOENT:
            return AFPERR_NOID;

        default:
            return AFPERR_PARAM;
        }
    }

    memset(&path, 0, sizeof(path));
    path.u_name = upath;

    if (of_stat(vol, &path) < 0) {
#ifdef ESTALE

        /* with nfs and our working directory is deleted */
        if (errno == ESTALE) {
            errno = ENOENT;
        }

#endif

        if (errno == ENOENT && !retry) {
            /* cnid db is out of sync, reenumerate the directory and update ids */
            reenumerate_id(vol, ".", dir);
            id = cnid;
            retry = 1;
            goto retry;
        }

        switch (errno) {
        case EACCES:
        case EPERM:
            return AFPERR_ACCESS;

        case ENOENT:
            return AFPERR_NOID;

        default:
            return AFPERR_PARAM;
        }
    }

    /* directories are bad */
    if (S_ISDIR(path.st.st_mode)) {
        /* OS9 and OSX don't return the same error code  */
        return (obj->afp_version >= 30) ? AFPERR_NOID : AFPERR_BADTYPE;
    }

    memcpy(&bitmap, ibuf, sizeof(bitmap));
    bitmap = ntohs(bitmap);

    if (NULL == (path.m_name = utompath(vol, upath, cnid, utf8_encoding(obj)))) {
        return AFPERR_NOID;
    }

    path.id = cnid;

    if (AFP_OK != (err = getfilparams(obj, vol, bitmap, &path, curdir,
                                      rbuf + sizeof(bitmap), &buflen, 0))) {
        return err;
    }

    *rbuflen = buflen + sizeof(bitmap);
    memcpy(rbuf, ibuf, sizeof(bitmap));
    return AFP_OK;
}

/* ------------------------------ */
int afp_deleteid(AFPObj *obj _U_, char *ibuf, size_t ibuflen _U_,
                 char *rbuf _U_, size_t *rbuflen)
{
    struct stat         st;
    struct vol		*vol;
    struct dir		*dir;
    char                *upath;
    int                 err;
    cnid_t		id;
    cnid_t		fileid;
    u_short		vid;
    static char buffer[12 + MAXPATHLEN + 1];
    int len = 12 + MAXPATHLEN + 1;
    *rbuflen = 0;
    ibuf += 2;
    memcpy(&vid, ibuf, sizeof(vid));
    ibuf += sizeof(vid);

    if (NULL == (vol = getvolbyvid(vid))) {
        return AFPERR_PARAM;
    }

    if (vol->v_cdb == NULL || !(vol->v_cdb->cnid_db_flags & CNID_FLAG_PERSISTENT)) {
        return AFPERR_NOOP;
    }

    if (vol->v_flags & AFPVOL_RO) {
        return AFPERR_VLOCK;
    }

    memcpy(&id, ibuf, sizeof(id));
    ibuf += sizeof(id);
    fileid = id;
    AFP_CNID_START("cnid_resolve");
    upath = cnid_resolve(vol->v_cdb, &id, buffer, len);
    AFP_CNID_DONE();

    if (upath == NULL) {
        return AFPERR_NOID;
    }

    if (NULL == (dir = dirlookup(vol, id))) {
        if (afp_errno == AFPERR_NOOBJ) {
            err = AFPERR_NOOBJ;
            goto delete;
        }

        return AFPERR_PARAM;
    }

    err = AFP_OK;

    if ((movecwd(vol, dir) < 0) || (ostat(upath, &st, vol_syml_opt(vol)) < 0)) {
        switch (errno) {
        case EACCES:
        case EPERM:
            return AFPERR_ACCESS;
#ifdef ESTALE

        case ESTALE:
#endif
        case ENOENT:
            /* still try to delete the id */
            err = AFPERR_NOOBJ;
            break;

        default:
            return AFPERR_PARAM;
        }
    } else if (S_ISDIR(st.st_mode)) { /* directories are bad */
        return AFPERR_BADTYPE;
    }

delete:
    AFP_CNID_START("cnid_delete");

    if (cnid_delete(vol->v_cdb, fileid)) {
        AFP_CNID_DONE();

        switch (errno) {
        case EROFS:
            return AFPERR_VLOCK;

        case EPERM:
        case EACCES:
            return AFPERR_ACCESS;

        default:
            return AFPERR_PARAM;
        }
    }

    AFP_CNID_DONE();
    return err;
}

/* ------------------------------ */
static struct adouble *find_adouble(const AFPObj *obj, struct vol *vol,
                                    struct path *path, struct ofork **of, struct adouble *adp)
{
    int             ret;

    if (path->st_errno) {
        switch (path->st_errno) {
        case ENOENT:
            afp_errno = AFPERR_NOID;
            break;

        case EPERM:
        case EACCES:
            afp_errno = AFPERR_ACCESS;
            break;

        default:
            afp_errno = AFPERR_PARAM;
            break;
        }

        return NULL;
    }

    /* we use file_access both for legacy Mac perm and
     * for unix privilege, rename will take care of folder perms
    */
    if (file_access(obj, vol, path, OPENACC_WR) < 0) {
        afp_errno = AFPERR_ACCESS;
        return NULL;
    }

    if ((*of = of_findname(vol, path))) {
        /* reuse struct adouble so it won't break locks */
        adp = (*of)->of_ad;
    } else {
        ret = ad_open(adp, path->u_name, ADFLAGS_HF | ADFLAGS_RDWR);

        /* META and HF */
        if (!ret && ad_reso_fileno(adp) != -1
                && !(adp->ad_resource_fork.adf_flags & (O_RDWR | O_WRONLY))) {
            /* from AFP spec.
             * The user must have the Read & Write privilege for both files in order to use this command.
             */
            ad_close(adp, ADFLAGS_HF);
            afp_errno = AFPERR_ACCESS;
            return NULL;
        }
    }

    return adp;
}

#define APPLETEMP ".AppleTempXXXXXX"

int afp_exchangefiles(AFPObj *obj, char *ibuf, size_t ibuflen _U_,
                      char *rbuf _U_, size_t *rbuflen)
{
    struct stat         srcst, destst;
    struct vol		*vol;
    struct dir		*dir, *sdir;
    char		*spath, temp[17], *p;
    char                *supath, *upath;
    struct path         *path;
    int                 err;
    int                 fd;
    struct adouble	ads;
    struct adouble	add;
    struct adouble	*adsp = NULL;
    struct adouble	*addp = NULL;
    struct ofork	*s_of = NULL;
    struct ofork	*d_of = NULL;
    int                 crossdev;
    unsigned int        slen;
    unsigned int        dlen;
    uint32_t		sid, did;
    uint16_t		vid;
    *rbuflen = 0;
    ibuf += 2;
    memcpy(&vid, ibuf, sizeof(vid));
    ibuf += sizeof(vid);

    if (NULL == (vol = getvolbyvid(vid))) {
        return AFPERR_PARAM;
    }

    if (vol->v_flags & AFPVOL_RO) {
        return AFPERR_VLOCK;
    }

    /* source and destination dids */
    memcpy(&sid, ibuf, sizeof(sid));
    ibuf += sizeof(sid);
    memcpy(&did, ibuf, sizeof(did));
    ibuf += sizeof(did);

    /* source file - use strict validation for parent directory */
    if (NULL == (dir = dirlookup_strict(vol, sid))) {
        return afp_errno;
    }

    if (NULL == (path = cname(vol, dir, &ibuf))) {
        return get_afp_errno(AFPERR_NOOBJ);
    }

    if (path_isadir(path)) {
        /* it's a dir */
        return AFPERR_BADTYPE;
    }

    /* save some stuff */
    srcst = path->st;
    sdir = curdir;
    spath = obj->oldtmp;
    supath = obj->newtmp;
    strlcpy(spath, path->m_name, AFPOBJ_TMPSIZ + 1);
    /* this is for the cnid changing */
    strlcpy(supath, path->u_name, AFPOBJ_TMPSIZ + 1);
    p = absupath(vol, sdir, supath);

    if (!p) {
        /* pathname too long */
        return AFPERR_PARAM ;
    }

    ad_init(&ads, vol);

    if (!(adsp = find_adouble(obj, vol, path, &s_of, &ads))) {
        return afp_errno;
    }

    /* Validate source file inode against cache to detect external replacement */
    struct dir *cached_src = NULL;

    if (supath != NULL) {
        size_t supath_len = strnlen(supath, CNID_MAX_PATH_LEN);
        cached_src = dircache_search_by_name(vol, sdir, supath, supath_len);
    }

    if (cached_src && cached_src->dcache_ino != srcst.st_ino) {
        LOG(log_warning, logtype_afpd,
            "afp_exchangefiles: source file inode mismatch (cached:%llu vs current:%llu), file replaced externally",
            (unsigned long long)cached_src->dcache_ino,
            (unsigned long long)srcst.st_ino);
        /* Invalidate stale cache entry */
        dir_remove(vol, cached_src);

        /* Clean up source adouble */
        if (!s_of && adsp && ad_meta_fileno(adsp) != -1) {
            ad_close(adsp, ADFLAGS_HF);
        }

        return AFPERR_NOOBJ;
    }

    /* ***** from here we may have resource fork open **** */
    /* look for the source cnid. if it doesn't exist, don't worry about
     * it. */
    slen = supath ? (unsigned int)strnlen(supath, CNID_MAX_PATH_LEN) : 0;
    AFP_CNID_START("cnid_lookup");
    sid = cnid_lookup(vol->v_cdb, &srcst, sdir->d_did, supath, slen);
    AFP_CNID_DONE();

    /* destination file - use strict validation for parent directory */
    if (NULL == (dir = dirlookup_strict(vol, did))) {
        err = afp_errno;
        goto err_exchangefile;
    }

    if (NULL == (path = cname(vol, dir, &ibuf))) {
        err = get_afp_errno(AFPERR_NOOBJ);
        goto err_exchangefile;
    }

    if (path_isadir(path)) {
        err = AFPERR_BADTYPE;
        goto err_exchangefile;
    }

    /* FPExchangeFiles is the only call that can return the SameObj
     * error */
    if ((curdir == sdir) && strcmp(spath, path->m_name) == 0) {
        err = AFPERR_SAMEOBJ;
        goto err_exchangefile;
    }

    ad_init(&add, vol);

    if (!(addp = find_adouble(obj, vol, path, &d_of, &add))) {
        err = afp_errno;
        goto err_exchangefile;
    }

    destst = path->st;
    /* Validate dest file inode against cache to detect external replacement */
    struct dir *cached_dest = NULL;

    if (path->u_name != NULL) {
        size_t dest_uname_len = strnlen(path->u_name, CNID_MAX_PATH_LEN);
        cached_dest = dircache_search_by_name(vol, curdir, path->u_name,
                                              dest_uname_len);
    }

    if (cached_dest && cached_dest->dcache_ino != destst.st_ino) {
        LOG(log_warning, logtype_afpd,
            "afp_exchangefiles: dest file inode mismatch (cached:%llu vs current:%llu), file replaced externally",
            (unsigned long long)cached_dest->dcache_ino,
            (unsigned long long)destst.st_ino);
        /* Invalidate stale cache entry */
        dir_remove(vol, cached_dest);

        /* Clean up both adoubles before returning */
        if (!s_of && adsp && ad_meta_fileno(adsp) != -1) {
            ad_close(adsp, ADFLAGS_HF);
        }

        if (!d_of && addp && ad_meta_fileno(addp) != -1) {
            ad_close(addp, ADFLAGS_HF);
        }

        err = AFPERR_NOOBJ;
        goto err_exchangefile;
    }

    /* they are not on the same device and at least one is open
     * FIXME broken for for crossdev and adouble v2
     * return an error
    */
    crossdev = (srcst.st_dev != destst.st_dev);

    if (/* (d_of || s_of)  && */ crossdev) {
        err = AFPERR_MISC;
        goto err_exchangefile;
    }

    /* look for destination id. */
    upath = path->u_name;

    if (!upath) {
        err = AFPERR_MISC;
        goto err_exchangefile;
    }

    dlen = (unsigned int)strnlen(upath, CNID_MAX_PATH_LEN);
    AFP_CNID_START("cnid_lookup");
    did = cnid_lookup(vol->v_cdb, &destst, curdir->d_did, upath, dlen);
    AFP_CNID_DONE();
    /* construct a temp name.
     * NOTE: the temp file will be in the dest file's directory. it
     * will also be inaccessible from AFP. */
    memcpy(temp, APPLETEMP, sizeof(APPLETEMP));

    if ((fd = mkstemp(temp)) == -1) {
        err = AFPERR_MISC;
        goto err_exchangefile;
    }

    close(fd);

    if (crossdev) {
        /* FIXME we need to close fork for copy, both s_of and d_of are null */
        ad_close(adsp, ADFLAGS_HF);
        ad_close(addp, ADFLAGS_HF);
    }

    /* now, quickly rename the file. we error if we can't. */
    if ((err = renamefile(vol, curdir, -1, p, temp, temp, adsp)) != AFP_OK) {
        goto err_exchangefile;
    }

    of_rename(vol, s_of, sdir, spath, curdir, temp);

    /* rename destination to source */
    if ((err = renamefile(vol, curdir, -1, upath, p, spath, addp)) != AFP_OK) {
        goto err_src_to_tmp;
    }

    of_rename(vol, d_of, curdir, path->m_name, sdir, spath);

    /* rename temp to destination */
    if ((err = renamefile(vol, curdir, -1, temp, upath, path->m_name,
                          adsp)) != AFP_OK) {
        goto err_dest_to_src;
    }

    of_rename(vol, s_of, curdir, temp, curdir, path->m_name);

    /*
     * id's need switching. src -> dest and dest -> src.
     * we need to re-stat() if it was a cross device copy.
     */
    if (sid) {
        AFP_CNID_START("cnid_delete");
        cnid_delete(vol->v_cdb, sid);
        AFP_CNID_DONE();
    }

    if (did) {
        AFP_CNID_START("cnid_delete");
        cnid_delete(vol->v_cdb, did);
        AFP_CNID_DONE();
    }

    if ((did && ((crossdev && ostat(upath, &srcst, vol_syml_opt(vol)) < 0) ||
                 cnid_update(vol->v_cdb, did, &srcst, curdir->d_did, upath, dlen) < 0))
            ||
            (sid && ((crossdev && ostat(p, &destst, vol_syml_opt(vol)) < 0) ||
                     cnid_update(vol->v_cdb, sid, &destst, sdir->d_did, supath, slen) < 0))
       ) {
        switch (errno) {
        case EPERM:
        case EACCES:
            err = AFPERR_ACCESS;
            break;

        default:
            err = AFPERR_PARAM;
        }

        goto err_temp_to_dest;
    }

    if (AD_META_OPEN(adsp) || AD_META_OPEN(addp)) {
        struct adouble adtmp;
        bool opened_ads = false;
        bool opened_add = false;
        ad_init(&adtmp, vol);
        ad_init_offsets(&adtmp);

        if (!AD_META_OPEN(adsp)) {
            if (ad_open(adsp, p, ADFLAGS_HF) != 0) {
                return -1;
            }

            opened_ads = true;
        }

        if (!AD_META_OPEN(addp)) {
            if (ad_open(addp, upath, ADFLAGS_HF) != 0) {
                return -1;
            }

            opened_add = true;
        }

        if (ad_copy_header(&adtmp, adsp) != 0) {
            goto err_temp_to_dest;
        }

        if (ad_copy_header(adsp, addp) != 0) {
            goto err_temp_to_dest;
        }

        if (ad_copy_header(addp, &adtmp) != 0) {
            goto err_temp_to_dest;
        }

        ad_flush(adsp);
        ad_flush(addp);

        if (opened_ads) {
            ad_close(adsp, ADFLAGS_HF);
        }

        if (opened_add) {
            ad_close(addp, ADFLAGS_HF);
        }
    }

    /* FIXME: we should switch resource fork too */

    /* here we need to reopen if crossdev */
    if (sid && ad_setid(addp, destst.st_dev, destst.st_ino,  sid, sdir->d_did,
                        vol->v_stamp)) {
        ad_flush(addp);
    }

    if (did && ad_setid(adsp, srcst.st_dev, srcst.st_ino,  did, curdir->d_did,
                        vol->v_stamp)) {
        ad_flush(adsp);
    }

    /* change perms, src gets dest perm and vice versa */
    become_root();
    /*
     * we need to exchange ACL entries as well
     */
#if 0
    exchange_acls(vol, p, upath);
#endif
    path->st = srcst;
    path->st_valid = 1;
    path->st_errno = 0;
    path->m_name = NULL;
    path->u_name = upath;
    setfilunixmode(vol, path, destst.st_mode);
    setfilowner(vol, destst.st_uid, destst.st_gid, path);
    path->st = destst;
    path->st_valid = 1;
    path->st_errno = 0;
    path->u_name = p;
    setfilunixmode(vol, path, srcst.st_mode);
    setfilowner(vol, srcst.st_uid, srcst.st_gid, path);
    unbecome_root();
    err = AFP_OK;
    goto err_exchangefile;
    /* all this stuff is so that we can unwind a failed operation
     * properly. */
err_temp_to_dest:
    /* rename dest to temp */
    renamefile(vol, curdir, -1, upath, temp, temp, adsp);
    of_rename(vol, s_of, curdir, upath, curdir, temp);
err_dest_to_src:
    /* rename source back to dest */
    renamefile(vol, curdir, -1, p, upath, path->m_name, addp);
    of_rename(vol, d_of, sdir, spath, curdir, path->m_name);
err_src_to_tmp:
    /* rename temp back to source */
    renamefile(vol, curdir, -1, temp, p, spath, adsp);
    of_rename(vol, s_of, curdir, temp, sdir, spath);
err_exchangefile:

    if (!s_of && adsp && ad_meta_fileno(adsp) != -1) {   /* META */
        ad_close(adsp, ADFLAGS_HF);
    }

    if (!d_of && addp && ad_meta_fileno(addp) != -1) {  /* META */
        ad_close(addp, ADFLAGS_HF);
    }

    struct dir *cached;

    if ((cached = dircache_search_by_did(vol, sid)) != NULL) {
        (void)dir_remove(vol, cached);
    }

    if ((cached = dircache_search_by_did(vol, did)) != NULL) {
        (void)dir_remove(vol, cached);
    }

    return err;
}
