The c_rename() function

Updated: April 19, 2023

The functionality to perform a rename can be done in one of two ways. You can simply return ENOSYS, which tells the client's rename() that you don't support renaming, or you can handle it. If you do return ENOSYS, an end user might not notice it right away, because the command-line utility mv deals with that and copies the file to the new location and then deletes the original. For a RAM disk, with small files, the time it takes to do the copy and unlink is imperceptible. However, simply changing the name of a directory that has lots of large files will take a long time, even though all you're doing is changing the name of the directory!

In order to properly implement rename functionality, there are two interesting issues:

The rename logic is further complicated by the fact that we are dealing with two paths instead of just one. In the c_link() case, one of the pathnames was implied by either an OCB (hard link) or actually given (symlink)—for the symlink we viewed the second “pathname” as a text string, without doing any particular checking on it.

You'll notice this “two path” impact when we look at the code:

int
cfs_c_rename (resmgr_context_t *ctp, io_rename_t *msg,
              RESMGR_HANDLE_T *handle, io_rename_extra_t *extra)
{
  // source and destination parents and targets
  des_t   sparent, starget, dparent, dtarget;
  des_t   components [_POSIX_PATH_MAX];
  int     ncomponents;
  int     sts;
  char    *p;
  int     i;
  struct  _client_info *cinfo;

  // 1) check for "initial subset" (mv x x/a) case
  i = strlen (extra -> path);
  if (!strncmp (extra -> path, msg -> connect.path, i)) {
    // source could be a subset, check character after
    // end of subset in destination
    if (msg -> connect.path [i] == 0
    || msg -> connect.path [i] == '/') {
      // source is identical to destination, or is a subset
      return (EINVAL);
    }
  }

  // get client info
  if ((sts = iofunc_client_info_ext (ctp, 0, &cinfo,
                IOFUNC_CLIENTINFO_GETGROUPS)) != EOK) {
    return (sts);
  }

  // 2) do destination resolution first in case we need to
  //    do a redirect or otherwise fail the request.
  if (connect_msg_to_attr (ctp, &msg -> connect, handle,
                           &dparent, &dtarget, &sts, cinfo)) {
    (void)iofunc_client_info_ext_free (&cinfo);
    return (sts);
  }

  // 3) if the destination exists, kill it and continue.
  if (sts != ENOENT) {
    if (sts == EOK) {
      if ((sts = cfs_rmnod (&dparent, dtarget.name,
                            dtarget.attr)) != EOK) {
        (void)iofunc_client_info_ext_free (&cinfo);
        return (sts);
      }
    } else {
      (void)iofunc_client_info_ext_free (&cinfo);
      return (sts);
    }
  }

  // 4) use our friend pathwalk() for source resolution.
  ncomponents = _POSIX_PATH_MAX;
  sts = pathwalk (ctp, extra -> path, handle, 0, components,
                  &ncomponents, cinfo);
  (void)iofunc_client_info_ext_free (&cinfo);

  // 5) missing directory component
  if (sts == ENOTDIR) {
    return (sts);
  }

  // 6) missing non-final component
  if (components [ncomponents].name != NULL && sts == ENOENT) {
    return (sts);
  }

  // 7) an annoying bug
  if (ncomponents < 2) {
    // can't move the root directory of the filesystem
    return (EBUSY);
  }

  starget = components [ncomponents - 1];
  sparent = components [ncomponents - 2];

  p = strdup (dtarget.name);
  if (p == NULL) {
    return (ENOMEM);
  }

  // 8) create new...
  if ((sts = add_new_dirent (dparent.attr, starget.attr, p)) != EOK) {
    free (p);
    return (sts);
  }
  starget.attr -> attr.nlink++;

  // 9) delete old
  return (cfs_rmnod (&sparent, starget.name, starget.attr));
}

The walkthrough is as follows:

  1. The first thing we check for is that the destination is not a child of the source as described in the comments above. This is accomplished primarily with a strncmp(). Then we need to check that there's something other than nothing or a / after the string (that's because mv x xa is perfectly legal, even though it would be picked up by the strncmp()).
  2. We do the “usual” destination resolution by calling connect_msg_to_attr(). Note that we use the dparent and dtarget (“d” for “destination”) variables.
  3. The destination better not exist. If it does, we attempt to remove it, and if that fails, we return whatever error cfs_rmnod() returned. If it doesn't exist, or we were able to remove it, we continue on. If there was any problem (other than the file originally existing or not existing, e.g. a permission problem), we return the status we got from connect_msg_to_attr().
  4. This is the only time you see pathwalk() called apart from the call in c_open(). That's because this is the only connect function that takes two pathnames as arguments.
  5. Catch missing intermediate directory components in the source.
  6. Catch missing nonfinal components.
  7. This was a nice bug, triggered by trying to rename . or the mount point. By simply ensuring that we're not trying to move the root directory of the filesystem, we fixed it. Next, we set up our “source” parent/target (sparent and starget).
  8. This is where we perform the “link to new, unlink old” logic. We call add_new_dirent() to create a new directory entry in the destination parent, then bump its link count (there are now two links to the object we're moving).
  9. Finally, we call cfs_rmnod() (see code below in discussion of c_unlink()) to remove the old. The removal logic decrements the link count.