+static int ct_opendirat(int parent_fd, const char *name, DIR **pdir)
+{
+ DIR *dir = NULL;
+ int fd = -1;
+ int rc;
+
+ fd = openat(parent_fd, name, O_RDONLY|O_DIRECTORY);
+ if (fd < 0)
+ return -errno;
+
+ dir = fdopendir(fd);
+ if (dir == NULL) {
+ rc = -errno;
+ goto out;
+ }
+
+ *pdir = dir;
+ fd = -1;
+ rc = 0;
+out:
+ if (!(fd < 0))
+ close(fd);
+
+ return rc;
+}
+
+static int ct_archive_upgrade_reg(int arc_fd, enum ct_archive_format ctaf,
+ int old_fd, const char *name)
+{
+ char new_path[PATH_MAX];
+ struct lu_fid fid;
+ char *split;
+ int scan_count;
+ int suffix_offset = -1;
+ int rc;
+
+ /* Formatted fix with optional suffix. We do not inspect
+ * suffixes. */
+ scan_count = sscanf(name, SFID"%n", RFID(&fid), &suffix_offset);
+ if (scan_count != 3 || suffix_offset < 0) {
+ rc = 0;
+ CT_TRACE("ignoring unexpected file '%s' in archive", name);
+ goto out;
+ }
+
+ ct_path_archive_v(new_path, sizeof(new_path),
+ ctaf, ".", &fid, name + suffix_offset);
+
+ rc = renameat(old_fd, name, arc_fd, new_path);
+ if (rc == 0)
+ goto out;
+
+ if (errno != ENOENT) {
+ rc = -errno;
+ goto out;
+ }
+
+ /* Create parent directory and try again. */
+ split = strrchr(new_path, '/');
+
+ *split = '\0';
+ rc = ct_mkdirat_p(arc_fd, new_path, DIR_PERM);
+ *split = '/';
+ if (rc < 0)
+ goto out;
+
+ rc = renameat(old_fd, name, arc_fd, new_path);
+ if (rc < 0)
+ rc = -errno;
+out:
+ if (rc < 0)
+ CT_ERROR(rc, "cannot rename '%s' to '%s'", name, new_path);
+ else
+ CT_TRACE("renamed '%s' to '%s'", name, new_path);
+
+ return rc;
+}
+
+static const char *d_type_name(unsigned int type)
+{
+ static const char *name[] = {
+ [DT_UNKNOWN] = "unknown",
+ [DT_FIFO] = "fifo",
+ [DT_CHR] = "chr",
+ [DT_DIR] = "dir",
+ [DT_BLK] = "blk",
+ [DT_REG] = "reg",
+ [DT_LNK] = "lnk",
+ [DT_SOCK] = "sock",
+ [DT_WHT] = "wht",
+ };
+
+ if (type < ARRAY_SIZE(name) && name[type] != NULL)
+ return name[type];
+
+ return name[DT_UNKNOWN];
+}
+
+static int ct_archive_upgrade_dir(int arc_fd, enum ct_archive_format ctaf,
+ int parent_fd, const char *dir_name)
+{
+ DIR *dir = NULL;
+ struct dirent *d;
+ int rc = 0;
+ int rc2;
+
+ rc = ct_opendirat(parent_fd, dir_name, &dir);
+ if (rc < 0) {
+ CT_ERROR(rc, "cannot open archive dir '%s'", dir_name);
+ goto out;
+ }
+
+ while ((d = readdir(dir)) != NULL) {
+ CT_TRACE("archive upgrade found %s '%s' (%ld)\n",
+ d_type_name(d->d_type), d->d_name, d->d_ino);
+
+ switch (d->d_type) {
+ case DT_DIR:
+ if (strcmp(d->d_name, ".") == 0 ||
+ strcmp(d->d_name, "..") == 0)
+ continue;
+
+ if (strlen(d->d_name) != 4 ||
+ strspn(d->d_name, "0123456789abcdef") != 4)
+ goto ignore;
+
+ rc2 = ct_archive_upgrade_dir(arc_fd, ctaf,
+ dirfd(dir), d->d_name);
+ if (rc2 < 0) {
+ rc = rc2;
+ CT_ERROR(rc, "cannot upgrade dir '%s' (%ld)",
+ d->d_name, d->d_ino);
+ }
+
+ rc2 = unlinkat(dirfd(dir), d->d_name, AT_REMOVEDIR);
+ CT_TRACE("unlink dir '%s' (%ld): %s",
+ d->d_name, d->d_ino, strerror(rc2 < 0 ? errno: 0));
+ if (rc2 < 0 && errno != ENOTEMPTY)
+ rc = -errno;
+
+ break;
+ case DT_REG:
+ rc2 = ct_archive_upgrade_reg(arc_fd, ctaf,
+ dirfd(dir), d->d_name);
+ if (rc2 < 0)
+ rc = rc2;
+
+ break;
+ default:
+ignore:
+ CT_TRACE("ignoring unexpected %s '%s' (%ld) in archive",
+ d_type_name(d->d_type), d->d_name, d->d_ino);
+ break;
+ }
+ }
+out:
+ if (dir != NULL)
+ closedir(dir);
+
+ return rc;
+}
+
+/* Recursive inplace upgrade (or downgrade) of archive to format
+ * ctaf. Prunes empty archive subdirectories. Idempotent. */
+static int ct_archive_upgrade(int arc_fd, enum ct_archive_format ctaf)
+{
+ /* FIXME Handle shadow tree. */
+ CT_TRACE("upgrade archive to format %s", ct_archive_format_to_str(ctaf));
+
+ return ct_archive_upgrade_dir(arc_fd, ctaf, arc_fd, ".");
+}
+