/*
 * Copyright (c) 2023 Thomas Lindner <tom@dl6tom.de>
 *
 * Permission to use, copy, modify, and distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 */

#include <sys/param.h>

#include <sys/disklabel.h>
#include <sys/dkio.h>
#include <sys/ioctl.h>
#include <sys/stat.h>
#include <ufs/ffs/fs.h>
#include <ufs/ufs/dinode.h>
#include <ufs/ufs/dir.h>

#include <err.h>
#include <fcntl.h>
#include <inttypes.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <util.h>

int diskfd;
int filefd;
struct fs *sblock;

union dinode {
  struct ufs1_dinode dp1;
  struct ufs2_dinode dp2;
};
#define DIP(dp, field)                                                         \
  ((sblock->fs_magic == FS_UFS1_MAGIC) ? (dp)->dp1.field : (dp)->dp2.field)

void usage(void);
void dump_inode(ino_t inum, union dinode *dp);
void dump_directory(union dinode *dp);
void dump_file(union dinode *dp);

void *xmalloc(size_t size);
void xpread(int fd, void *buf, size_t nbytes, off_t offset);
void xwrite(int fd, void *buf, size_t nbytes);
void xpwrite(int fd, void *buf, size_t nbytes, off_t offset);

int main(int argc, char **argv) {
#define MODE_ALL 1
#define MODE_INODE 2
#define MODE_DIRECTORY 4
#define MODE_FILE 8
  unsigned mode = 0;
  ino_t inode;

  int opt;
  const char *errstr;
  while ((opt = getopt(argc, argv, "adi:o:")) != -1) {
    switch (opt) {
    case 'a':
      if (mode & MODE_INODE) {
        usage();
      }
      mode |= MODE_ALL;
      break;
    case 'i':
      if (mode & MODE_ALL) {
        usage();
      }
      mode |= MODE_INODE;
      inode = strtonum(optarg, 2, UINT32_MAX, &errstr);
      if (errstr) {
        errx(1, "Invalid inode %s: %s", optarg, errstr);
      }
      break;
    case 'd':
      if (mode & MODE_FILE) {
        usage();
      }
      mode |= MODE_DIRECTORY;
      break;
    case 'o':
      if (mode & (MODE_ALL | MODE_DIRECTORY)) {
        usage();
      }
      mode |= MODE_FILE;
      if (!strcmp(optarg, "-")) {
        filefd = STDOUT_FILENO;
        break;
      }
      filefd = open(optarg, O_WRONLY | O_CREAT | O_EXCL,
                    S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
      if (filefd == -1) {
        err(1, "Cannot open %s", optarg);
      }
      break;
    default:
      usage();
    }
  }
  if (!mode || optind + 1 != argc) {
    usage();
  }

  // open disk
  char *realpath;
  diskfd = opendev(argv[optind], O_RDONLY | O_NOFOLLOW, 0, &realpath);
  if (diskfd == -1) {
    err(1, "Cannot open %s (%s)", argv[optind], realpath);
  }

  // find superblock
  sblock = xmalloc(SBLOCKSIZE);
  int sblock_try[] = SBLOCKSEARCH;
  int i;
  for (i = 0; sblock_try[i] != -1; i++) {
    ssize_t n = pread(diskfd, sblock, SBLOCKSIZE, (off_t)sblock_try[i]);
    if (n == SBLOCKSIZE &&
        (sblock->fs_magic == FS_UFS1_MAGIC ||
         (sblock->fs_magic == FS_UFS2_MAGIC &&
          sblock->fs_sblockloc == sblock_try[i])) &&
        sblock->fs_bsize <= MAXBSIZE && sblock->fs_bsize >= sizeof(struct fs))
      break;
  }
  if (sblock_try[i] == -1) {
    errx(1, "Cannot find filesystem superblock");
  }

  void *inoblock = xmalloc(sblock->fs_bsize);
  if (mode & MODE_ALL) {
    // iterate over all inodes
    ino_t maxino = (ino_t)sblock->fs_ipg * sblock->fs_ncg;
    for (ino_t inum = 0; inum < maxino; inum++) {
      // read file system block
      if (!ino_to_fsbo(sblock, inum)) {
        daddr_t blkno = fsbtodb(sblock, ino_to_fsba(sblock, inum));
        xpread(diskfd, inoblock, sblock->fs_bsize, blkno * DEV_BSIZE);
      }
      // inode 0 is used as placeholder and 1 was used for bad blocks
      if (inum < 2) {
        continue;
      }
      union dinode *dp;
      if (sblock->fs_magic == FS_UFS1_MAGIC) {
        dp = (union dinode *)&(
            (struct ufs1_dinode *)inoblock)[ino_to_fsbo(sblock, inum)];
      } else {
        dp = (union dinode *)&(
            (struct ufs2_dinode *)inoblock)[ino_to_fsbo(sblock, inum)];
      }
      if (mode & MODE_DIRECTORY) {
        dump_directory(dp);
      } else {
        dump_inode(inum, dp);
      }
    }
  } else {
    daddr_t blkno = fsbtodb(sblock, ino_to_fsba(sblock, inode));
    xpread(diskfd, inoblock, sblock->fs_bsize, blkno * DEV_BSIZE);
    union dinode *dp;
    if (sblock->fs_magic == FS_UFS1_MAGIC) {
      dp = (union dinode *)&(
          (struct ufs1_dinode *)inoblock)[ino_to_fsbo(sblock, inode)];
    } else {
      dp = (union dinode *)&(
          (struct ufs2_dinode *)inoblock)[ino_to_fsbo(sblock, inode)];
    }
    if (mode & MODE_FILE) {
      dump_file(dp);
    } else if (mode & MODE_DIRECTORY) {
      if ((DIP(dp, di_mode) & IFMT) != IFDIR) {
        errx(1, "Not a directory");
      }
      dump_directory(dp);
    } else {
      dump_inode(inode, dp);
    }
  }

  return 0;
}

void usage(void) {
  fprintf(stderr, "Usage: %s (-a|-i inode) [-d|-o outputfile] disk\n",
          getprogname());
  exit(1);
}

void dump_inode(ino_t inum, union dinode *dp) {
  char type;
  switch (DIP(dp, di_mode) & IFMT) {
  case IFIFO:
    type = 'p';
    break;
  case IFCHR:
    type = 'c';
    break;
  case IFDIR:
    type = 'd';
    break;
  case IFBLK:
    type = 'b';
    break;
  case IFREG:
    type = 'f';
    break;
  case IFLNK:
    type = 'l';
    break;
  case IFSOCK:
    type = 's';
    break;
  case IFWHT:
    type = 'w';
    break;
  default:
    type = 'u';
  }
  printf("inode:%" PRIu32 " type:%c mode:%" PRIo16 " nlink:%" PRId16
         " uid:%" PRIu32 " gid:%" PRIu32 " size:%" PRIu64 " atime:%" PRId64
         " mtime:%" PRId64 " ctime:%" PRId64 "\n",
         (uint32_t)inum, type, DIP(dp, di_mode) & ~IFMT, DIP(dp, di_nlink),
         DIP(dp, di_uid), DIP(dp, di_gid), DIP(dp, di_size), DIP(dp, di_atime),
         DIP(dp, di_mtime), DIP(dp, di_ctime));
}

void dump_dirblk(void *dirblk, size_t size) {
  struct direct *d = dirblk;
  errx(1, "Not implemented");
}

void dump_directory(union dinode *dp) {
  if ((DIP(dp, di_mode) & IFMT) != IFDIR) {
    return;
  }
  void *datablock = xmalloc(sblock->fs_bsize);
  off_t filesize = DIP(dp, di_size);
  for (int i = 0; filesize > 0 && i < NDADDR; i++) {
    filesize -= sblock->fs_bsize;
    if (!DIP(dp, di_db[i])) {
      continue;
    }
    size_t blksize = sblksize(sblock, DIP(dp, di_size), i);
    daddr_t blkno = fsbtodb(sblock, DIP(dp, di_db[i]));
    xpread(diskfd, datablock, blksize, blkno * DEV_BSIZE);
    dump_dirblk(datablock, blksize);
  }
  if (filesize > 0) {
    errx(1, "Indirect blocks not implemented");
  }
  free(datablock);
}

void dump_file_indirect(union dinode *dp, void *indirblock, int level,
                        off_t *filesize) {
  void *datablock = xmalloc(sblock->fs_bsize);
  if (!level) {
    for (int i = 0; *filesize > 0 && i < NINDIR(sblock);
         *filesize -= sblock->fs_bsize, i++) {
      daddr_t fsblkno;
      if (sblock->fs_magic == FS_UFS1_MAGIC) {
        fsblkno = ((uint32_t *)indirblock)[i];
      } else {
        fsblkno = ((uint64_t *)indirblock)[i];
      }
      if (!fsblkno) {
        if (filefd == STDOUT_FILENO) {
          memset(datablock, 0, sblock->fs_bsize);
          xwrite(filefd, datablock, sblock->fs_bsize);
        }
        continue;
      }
      size_t blksize = sblock->fs_bsize;
      daddr_t blkno = fsbtodb(sblock, fsblkno);
      xpread(diskfd, datablock, blksize, blkno * DEV_BSIZE);
      if (*filesize < blksize) {
        blksize = *filesize;
      }
      if (filefd == STDOUT_FILENO) {
        xwrite(filefd, datablock, blksize);
      } else {
        xpwrite(filefd, datablock, blksize, DIP(dp, di_size) - *filesize);
      }
    }
  } else {
    for (int i = 0; *filesize > 0 && i < NINDIR(sblock); i++) {
      daddr_t fsblkno;
      if (sblock->fs_magic == FS_UFS1_MAGIC) {
        fsblkno = ((uint32_t *)indirblock)[i];
      } else {
        fsblkno = ((uint64_t *)indirblock)[i];
      }
      if (!fsblkno) {
        size_t gapsize = NINDIR(sblock) * sblock->fs_bsize;
        for (int j = 0; j < i; j++) {
          gapsize *= NINDIR(sblock);
        }
        *filesize -= gapsize;
        if (filefd == STDOUT_FILENO) {
          memset(datablock, 0, sblock->fs_bsize);
          for (int j = 0; j < gapsize / sblock->fs_bsize; j++) {
            xwrite(filefd, datablock, sblock->fs_bsize);
          }
        }
        continue;
      }
      daddr_t blkno = fsbtodb(sblock, fsblkno);
      xpread(diskfd, datablock, sblock->fs_bsize, blkno * DEV_BSIZE);
      dump_file_indirect(dp, datablock, level - 1, filesize);
    }
  }
  free(datablock);
  fprintf(stderr, "\r%" PRIu64 "/%" PRIu64, DIP(dp, di_size) - *filesize,
          DIP(dp, di_size));
}

void dump_file(union dinode *dp) {
  off_t filesize = DIP(dp, di_size);
  if (filefd != STDOUT_FILENO && ftruncate(filefd, filesize) == -1) {
    err(1, "ftruncate() failed");
  }
  void *datablock = xmalloc(sblock->fs_bsize);
  int i;
  for (i = 0; filesize > 0 && i < NDADDR; filesize -= sblock->fs_bsize, i++) {
    if (!DIP(dp, di_db[i])) {
      if (filefd == STDOUT_FILENO) {
        memset(datablock, 0, sblock->fs_bsize);
        xwrite(filefd, datablock, sblksize(sblock, DIP(dp, di_size), i));
      }
      continue;
    }
    size_t blksize = sblksize(sblock, DIP(dp, di_size), i);
    daddr_t blkno = fsbtodb(sblock, DIP(dp, di_db[i]));
    xpread(diskfd, datablock, blksize, blkno * DEV_BSIZE);
    if (filesize < blksize) {
      blksize = filesize;
    }
    if (filefd == STDOUT_FILENO) {
      xwrite(filefd, datablock, blksize);
    } else {
      xpwrite(filefd, datablock, blksize, DIP(dp, di_size) - filesize);
    }
  }
  for (i = 0; filesize > 0 && i < NIADDR; i++) {
    if (!DIP(dp, di_ib[i])) {
      size_t gapsize = NINDIR(sblock) * sblock->fs_bsize;
      for (int j = 0; j < i; j++) {
        gapsize *= NINDIR(sblock);
      }
      filesize -= gapsize;
      if (filefd == STDOUT_FILENO) {
        memset(datablock, 0, sblock->fs_bsize);
        for (int j = 0; j < gapsize / sblock->fs_bsize; j++) {
          xwrite(filefd, datablock, sblock->fs_bsize);
        }
      }
      continue;
    }
    daddr_t blkno = fsbtodb(sblock, DIP(dp, di_ib[i]));
    xpread(diskfd, datablock, sblock->fs_bsize, blkno * DEV_BSIZE);
    dump_file_indirect(dp, datablock, i, &filesize);
  }
  if (i) {
    fputc('\n', stderr);
  }
  free(datablock);
}

void *xmalloc(size_t size) {
  void *p = malloc(size);
  if (!p) {
    err(1, "malloc() failed");
  }
  return p;
}

void xpread(int fd, void *buf, size_t nbytes, off_t offset) {
  ssize_t n = pread(fd, buf, nbytes, offset);
  if (n == -1) {
    err(1, "pread() failed");
  }
  if (n != nbytes) {
    errx(1, "Incomplete read");
  }
}

void xwrite(int fd, void *buf, size_t nbytes) {
  ssize_t n = write(fd, buf, nbytes);
  if (n == -1) {
    err(1, "write() failed");
  }
  if (n != nbytes) {
    errx(1, "Incomplete write");
  }
}

void xpwrite(int fd, void *buf, size_t nbytes, off_t offset) {
  ssize_t n = pwrite(fd, buf, nbytes, offset);
  if (n == -1) {
    err(1, "pwrite() failed");
  }
  if (n != nbytes) {
    errx(1, "Incomplete write");
  }
}