//
// Uncramfs
// A program to unpack a cramfs image
//
// Copyright Andrew Stitcher, 2001
//
// Licensed according to the GNU GPL v2
//

// C things
#include <stdio.h>
//#include <stdlib.h>
#include <string.h>
#include <assert.h>

// Unix things
#include <unistd.h>
#include <errno.h>
//#include <dirent.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/mman.h>
#include <sys/fcntl.h>

// Application libraries
#include <zlib.h>

// Needed by cramfs
typedef unsigned char u8;
typedef unsigned short u16;
typedef unsigned int u32;

// Cramfs definitions
#include "cramfs.h"

#define PAGE_CACHE_SIZE (4096)

/* The kernel assumes PAGE_CACHE_SIZE as block size. */
static unsigned int blksize = PAGE_CACHE_SIZE;

static const char* progname = "uncramfs";

static char *opt_devfile = NULL;
static char *opt_idsfile = NULL;

// Get version number from external file
static const char*
#include "VERSION"
;

void do_file_entry(const u8* base,
		   const char* dir,
		   const char* path,
		   const char* name, int namelen,
		   const struct cramfs_inode* inode);

void do_dir_entry(const u8* base,
		  const char* dir,
		  const char* path,
		  const char* name, int namelen,
		  const struct cramfs_inode* inode);

///////////////////////////////////////////////////////////////////////////////

static void usage(void)
{
   printf(
     "%s v%s by Andrew Stitcher\n"
     "Usage: '%s [-d devfilename] [-m modefilename] dirname infile'\n"
     " where <dirname> is the root for the\n"
     " uncompressed (output) filesystem.\n", progname, VERSION, progname);
   exit(1);
}

///////////////////////////////////////////////////////////////////////////////

u32 compressed_size(const u8* base, const u8* data, u32 size)
{
   const u32* buffs=(const u32*)(data);
   int nblocks=(size-1)/blksize+1;
   const u8* buffend=base+*(buffs+nblocks-1);
   
   if (size == 0)
     return 0;
   else
     return buffend-data;
}

void uncompress_data(const u8* base, const u8* data, u32 size, u8* dstdata)
{
   const u32* buffs=(const u32*)(data);
   int nblocks=(size-1)/blksize+1;
   const u8* buff=(const u8*)(buffs+nblocks);
   const u8* nbuff;
   int block=0;
   uLongf len=size;
   
   if (size == 0) {
     return;
   }
   
   for (;
	block < nblocks;
	++block, buff=nbuff, dstdata+=blksize, len-=blksize
	) {
      uLongf tran=(len < blksize) ? len : blksize;
      nbuff=base+*(buffs+block);
      if (uncompress(dstdata, &tran, buff, nbuff-buff) != Z_OK) {
	 fprintf(stderr,"Uncompression failed");
	 return;
      }
   }
}

///////////////////////////////////////////////////////////////////////////////

int stats_totalsize;
int stats_totalcsize;
int stats_count;
int stats_compresses;
int stats_expands;

void clearstats()
{
   stats_totalsize=0;
   stats_totalcsize=0;
   stats_count=0;
   stats_compresses=0;
   stats_expands=0;
}

void updatestats(int size, int csize)
{
   ++stats_count;
   stats_totalsize+=size;
   stats_totalcsize+=csize;
   
   if (size>=csize) {
      stats_compresses++;
   } else {
      stats_expands++;
   }
}

void printstats()
{
   printf("\n[Summary:]\n");
   printf("[Total uncompressed size:    %9d]\n", stats_totalsize);
   printf("[Total compressed size:      %9d]\n", stats_totalcsize);
   printf("[Number of entries:          %9d]\n", stats_count);
   printf("[Number of files compressed: %9d]\n", stats_compresses);
   printf("[Number of files expanded:   %9d]\n", stats_expands);
   printf("\n");
}

///////////////////////////////////////////////////////////////////////////////

void printmode(const struct cramfs_inode* inode)
{
   u16 mode=inode->mode;
   
   // Deal with file type bitsetc
   if (S_ISDIR(mode)) printf("d");
   else if(S_ISLNK(mode)) printf("l");
   else if(S_ISBLK(mode)) printf("b");
   else if(S_ISCHR(mode)) printf("c");
   else if(S_ISFIFO(mode)) printf("p");
   else if(S_ISSOCK(mode)) printf("s");
   else printf("-");
   
   // Deal with mode bits
   if (mode & S_IRUSR) printf("r"); else printf("-");
   if (mode & S_IWUSR) printf("w"); else printf("-");
   if (mode & S_IXUSR)
     if (mode & S_ISUID) printf("s"); else printf("x");
   else
     if (mode & S_ISUID) printf("S"); else printf("-");
   if (mode & S_IRGRP) printf("r"); else printf("-");
   if (mode & S_IWGRP) printf("w"); else printf("-");
   if (mode & S_IXGRP)
     if (mode & S_ISGID) printf("s"); else printf("x");
   else
     if (mode & S_ISGID) printf("S"); else printf("-");
   if (mode & S_IROTH) printf("r"); else printf("-");
   if (mode & S_IWOTH) printf("w"); else printf("-");
   if (mode & S_IXOTH)
     if (mode & S_ISVTX) printf("t"); else printf("x");
   else
     if (mode & S_ISVTX) printf("T"); else printf("-");
}

void printuidgid(const struct cramfs_inode* inode)
{
   char res[14];
   
   snprintf(res, 14, "%d/%d", inode->uid, inode->gid);
   printf(" %-14s", res);
   
}

void printsize(int size, int csize)
{
   char s[17];

   // As a side effect update the size stats
   updatestats(size, csize);
   
   snprintf(s, 17, "%7d(%d)", size, csize);
   printf("%-16s ", s);
}

///////////////////////////////////////////////////////////////////////////////

void do_file(const u8* base, u32 offset, u32 size,
	     const char* path, const char* name, int mode)
{
   int fd;
   u8* file_data;
   const u8* srcdata;
   
   // Allow for uncompressed XIP executable
   if (mode & S_ISVTX) {
      // It seems that the offset may not necessarily be page
      // aligned. This is silly because mkcramfs wastes
      // the alignment space, whereas it might be used if it wasn't
      // bogusly in our file extent.
      //
      // blksize must be a power of 2 for the following to work, but it seems
      // quite likely.
 
      srcdata=(const u8*)(((u32)(base+offset)+blksize-1) & ~(blksize-1));
      printsize(size, srcdata+size-(base+offset));
      printf("%s", name);
   } else {
      printsize(size, compressed_size(base, base+offset, size) );
      printf("%s", name);
   }

   // Check if we are actually unpacking
   if (path[0]=='-') {
      return;
   }

   // Make local copy
   fd=open(path, O_CREAT|O_TRUNC|O_RDWR, mode);
   if (fd == -1) {
      perror("create");
      return;
   };

   if (ftruncate(fd, size) == -1) {
      perror("ftruncate");
      close(fd);
      return;
   }
   
   file_data = mmap(0, size, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
   if (file_data == MAP_FAILED) {
      perror("mmap");
      close(fd);
      return;
   }

   // Allow for uncompressed XIP executable
   if (mode & S_ISVTX) {
      memcpy(file_data, srcdata, size);
   } else {
      uncompress_data(base, base+offset, size, file_data);
   }
   
   munmap(file_data, size);
   close(fd);

}

void do_directory(const u8* base, u32 offset, u32 size,
		  const char* path, const char* name, int mode)
{
   printsize(size, size);
   printf("%s", name);
   
   // Check if we are actually unpacking
   if (path[0]=='-') {
      return;
   }

   // Make the local directory
   if (mkdir(path, mode) == -1) {
      perror(path);
      return;
   }
}

void do_symlink(const u8* base, u32 offset, u32 size,
		const char* path, const char* name, int mode)
{
   // Allocate the uncompressed string
   u8 link_contents[size+1];
   
   // do uncompression
   uncompress_data(base, base+offset, size, link_contents);
   link_contents[size]=0;
   
   printsize(size, compressed_size(base, base+offset, size) );
   printf("%s -> %s", name, link_contents );

   // Check if we are actually unpacking
   if (path[0]=='-') {
      return;
   }

   // Make local copy
   if (symlink(link_contents, path) == -1) {
      perror(path);
      exit(1);
   }
}

void do_chrdev(const u8* base, u32 offset, u32 size,
	       const char* path, const char* name, int mode, int uid, int gid)
{
   {
      char s[17];
      snprintf(s, 17, "%3d, %3d", major(size), minor(size) );
      printf("%-16s %s", s, name);
   }

   // Check if we are actually unpacking
   if (path[0]=='-') {
      return;
   }

   // Make local copy
   if (geteuid() == 0) {
      if (mknod(path, S_IFCHR | mode, size) == -1)
         perror(path);
   } else if (opt_devfile) {
      char dfp[1024];
      char *p;
      FILE *f;

      strcpy(dfp,path);
      p = strrchr(dfp,'/');
      if (!p) {
         fprintf(stderr,"Could not find path in '%s'\n",path);
         return;
      }
      strcpy(p+1,opt_devfile);
      f = fopen(dfp,"at");
      if (!f) {
   	    perror(dfp);
   	    return;
      }
      fprintf(f,"%s,%08x,%u,%u,%08x\n",name,mode,uid,gid,size);
      fclose(f);
   }
}

void do_blkdev(const u8* base, u32 offset, u32 size,
	       const char* path, const char* name, int mode, int uid, int gid)
{
   {
      char s[17];
      snprintf(s, 17, "%3d, %3d", major(size), minor(size) );
      printf("%-16s %s", s, name);
   }

   // Check if we are actually unpacking
   if (path[0]=='-') {
      return;
   }

   // Make local copy
   if (geteuid() == 0) {
      if (mknod(path, S_IFBLK | mode, size) == -1)
         perror(path);
   } else if (opt_devfile) {
      char dfp[1024];
      char *p;
      FILE *f;

      strcpy(dfp,path);
      p = strrchr(dfp,'/');
      if (!p) {
         fprintf(stderr,"Could not find path in '%s'\n",path);
         return;
      }
      strcpy(p+1,opt_devfile);
      f = fopen(dfp,"at");
      if (!f) {
   	    perror(dfp);
   	    return;
      }
      fprintf(f,"%s,%08x,%u,%u,%08x\n",name,mode,uid,gid,size);
      fclose(f);
   }
}

void do_fifo(const u8* base, u32 offset, u32 size,
	     const char* path, const char* name, int mode, int gid, int uid)
{
   printf("                 %s", name);
   
   // Check if we are actually unpacking
   if (path[0]=='-') {
      return;
   }

   // Make local copy
   if (geteuid() == 0) {
      if (mknod(path, S_IFIFO | mode, 0) == -1)
         perror(path);
   } else if (opt_devfile) {
      char dfp[1024];
      char *p;
      FILE *f;

      strcpy(dfp,path);
      p = strrchr(dfp,'/');
      if (!p) {
         fprintf(stderr,"Could not find path in '%s'\n",path);
         return;
      }
      strcpy(p+1,opt_devfile);
      f = fopen(dfp,"at");
      if (!f) {
   	    perror(dfp);
   	    return;
      }
      fprintf(f,"%s,%08x,%u,%u,%08x\n",name,mode,uid,gid,size);
      fclose(f);
   }
}

void do_socket(const u8* base, u32 offset, u32 size,
	       const char* path, const char* name, int mode)
{
   printf("<UNIMPLEMENTED>  %s", name);

   // Check if we are actually unpacking
   if (path[0]=='-') {
      return;
   }
}

void do_unknown(const u8* base, u32 offset, u32 size,
	        const char* path, const char* name, int mode)
{
   printf("<UNKNOWN TYPE>   %s", name);
}

void process_directory(const u8* base, const char* dir, u32 offset, u32 size,
		  const char* path)
{
   struct cramfs_inode* de;
   char* name;
   int namelen;
   u32 current=offset;
   u32 dirend=offset+size;
   
   // Do files
   while (current < dirend) {
      u32 nextoffset;
      
      de=(struct cramfs_inode*)(base+current);
      namelen=de->namelen<<2;
      nextoffset=current+sizeof(struct cramfs_inode)+namelen;
      
      name=(char*)(de+1);
      
      while (1) {
	 assert(namelen!=0);
	 
	 if (name[namelen-1])
	   break;
	 namelen--;
      }

      do_file_entry(base, dir, path, name, namelen, de);
      
      current=nextoffset;
   }

   // Recurse into directories
   current=offset;
   while (current < dirend) {
      u32 nextoffset;
      
      de=(struct cramfs_inode*)(base+current);
      namelen=de->namelen<<2;
      nextoffset=current+sizeof(struct cramfs_inode)+namelen;
      
      name=(char*)(de+1);
      
      while (1) {
	 assert(namelen!=0);
	 
	 if (name[namelen-1])
	   break;
	 namelen--;
      }

      do_dir_entry(base, dir, path, name, namelen, de);
      
      current=nextoffset;
   }
}

///////////////////////////////////////////////////////////////////////////////

void do_file_entry(const u8* base,
		   const char* dir,
	           const char* path,
		   const char* name, int namelen, 
		   const struct cramfs_inode* inode)
{
   int dirlen=strlen(dir);
   int pathlen=strlen(path);
   char pname[dirlen+pathlen+namelen+3];
   const char* basename;
   
   if (dirlen) {
      strncpy(pname, dir, dirlen);
   }
   
   if (pathlen) {
      if (dirlen) {
	 pname[dirlen]='/';
	 ++dirlen;
      }
      strncpy(pname+dirlen, path, pathlen);
   }
   
   if (namelen) {
      if (pathlen+dirlen) {
	 pname[dirlen+pathlen]='/';
	 ++pathlen;
      }
      strncpy(pname+dirlen+pathlen, name, namelen);
   }

   pname[pathlen+dirlen+namelen]=0;
   basename=namelen ? pname+dirlen+pathlen : "/";
   
   // Create things here
   printmode(inode);
   printuidgid(inode);
   
   if (S_ISREG(inode->mode)) {
      do_file(base, inode->offset<<2, inode->size, pname, basename, inode->mode);
   } else  if (S_ISDIR(inode->mode)) {
      do_directory(base, inode->offset<<2, inode->size, pname, basename, inode->mode);
   } else  if (S_ISLNK(inode->mode)) {
      do_symlink(base, inode->offset<<2, inode->size, pname, basename, inode->mode);
   } else  if (S_ISFIFO(inode->mode)) {
      do_fifo(base, inode->offset<<2, inode->size, pname, basename, inode->mode, inode->uid, inode->gid);
   } else  if (S_ISSOCK(inode->mode)) {
      do_socket(base, inode->offset<<2, inode->size, pname, basename, inode->mode);
   } else  if (S_ISCHR(inode->mode)) {
      do_chrdev(base, inode->offset<<2, inode->size, pname, basename, inode->mode, inode->uid, inode->gid);
   } else  if (S_ISBLK(inode->mode)) {
      do_blkdev(base, inode->offset<<2, inode->size, pname, basename, inode->mode, inode->uid, inode->gid);
   } else {
      do_unknown(base, inode->offset<<2, inode->size, pname, basename, inode->mode);
   }
   
   if (geteuid() == 0) {
      if (lchown(pname, inode->uid, inode->gid) == -1)
         perror("cannot change owner or group");
   } else if(opt_idsfile && path && path[0]) {
      char dfp[1024];
      char *p;
      FILE *f;

      strcpy(dfp,pname);
      p = strrchr(dfp,'/');
      if (!p) {
         fprintf(stderr,"could not find path in '%s'\n",pname);
         return;
      }
      strcpy(p+1,opt_idsfile);
      f = fopen(dfp,"at");
      if (!f) {
   	    perror(dfp);
   	    return;
      }
	  fprintf(f,"%s,%u,%u,%08x\n",basename,inode->uid,inode->gid,inode->mode);
      fclose(f);
   }

   if (geteuid() == 0 || !opt_idsfile) {
      if (inode->mode & (S_ISGID|S_ISUID|S_ISVTX)) {
        if (0 != chmod(pname, inode->mode)){
          perror("chmod");
          return;
        }
      }
   }


   
   printf("\n");
}

void do_dir_entry(const u8* base,
		  const char* dir,
		  const char* path,
		  const char* name, int namelen, 
		  const struct cramfs_inode* inode)
{
   int pathlen=strlen(path);
   char pname[pathlen+namelen+2];
   
   if (pathlen) {
      strncpy(pname, path, pathlen);
   }
   
   if (namelen) {
      if (pathlen) {
	 pname[pathlen]='/';
	 ++pathlen;
      }
      strncpy(pname+pathlen, name, namelen);
   }

   pname[pathlen+namelen]=0;

   // Only process directories here   
   if (S_ISDIR(inode->mode)) {
      printf("\n/%s:\n", pname);
      process_directory(base, dir, inode->offset<<2, inode->size, pname);
   }
}

///////////////////////////////////////////////////////////////////////////////

/*
 * Usage:
 *
 *      uncramfs directory-name outfile
 *
 * where "directory-name" is simply the root of the directory
 * tree that we want to expand the compressed filesystem into.
 */
int main(int argc, char **argv)
{
   char const* dirname;
   char const* imagefile;
   struct stat st;
   int fd;
   size_t fslen_ub;
   u8 const* rom_image;
   struct cramfs_super const* sb;
   int i;

   // Check the program usage
   if (argc)
     progname = argv[0];
   if (argc != 3 && argc != 5 && argc != 7)
     usage();

   if (argc == 3) {
      dirname=argv[1];
      imagefile=argv[2];
   } else if (argc == 5) {
      if (strcmp(argv[1],"-d")==0) {
         opt_devfile = argv[2];
      } else if (strcmp(argv[1],"-m")==0) {
         opt_idsfile = argv[2];
      } else
         usage();
      dirname=argv[3];
      imagefile=argv[4];
   } else {
      if (strcmp(argv[1],"-d")==0) {
         opt_devfile = argv[2];
      } else if (strcmp(argv[1],"-m")==0) {
         opt_idsfile = argv[2];
      } else
         usage();

      if (strcmp(argv[3],"-d")==0 && !opt_devfile) {
         opt_devfile = argv[4];
      } else if (strcmp(argv[3],"-m")==0 && !opt_idsfile) {
         opt_idsfile = argv[4];
      } else
         usage();
      dirname=argv[5];
      imagefile=argv[6];
   }
   
   // Check the directory
   if (access(dirname, W_OK) == -1) {
      if (errno != ENOENT ) {
	 perror(dirname);
	 exit(1);
      }
   }
   
   // Check the image file
   if (stat(imagefile, &st) == -1) {
      perror(imagefile);
      exit(1);
   }

   // Map the cramfs image
   fd = open(imagefile, O_RDONLY);
   fslen_ub = st.st_size;
   rom_image = mmap(0, fslen_ub, PROT_READ, MAP_SHARED, fd, 0);
   if (rom_image == MAP_FAILED) {
      perror("Mapping cramfs file");
      exit(1);
   }
   
   sb=(struct cramfs_super const*)(rom_image);
   // Check cramfs magic number and signature
   if (CRAMFS_MAGIC != sb->magic ||
       0 != memcmp(sb->signature, CRAMFS_SIGNATURE, sizeof(sb->signature))) {
      fprintf(stderr,"The image file doesn't have cramfs signatures\n");
      exit(1);
   }

   // Set umask to 0 to let the image modes shine through
   umask(0);
   
   // Print some reassuring blurb
   printf("[Volume size: 0x%x]\n", fslen_ub);
   printf("[Volume serial: ");
   for (i=0; i< sizeof(sb->fsid); ++i)
     printf("%02x", sb->fsid[i]);
   printf("]\n");
   printf("[Volume name: %s]\n", sb->name);
   printf("\n");

   clearstats();
   
   // Start doing...
   do_file_entry(rom_image, dirname, "", "", 0, &sb->root);
   do_dir_entry(rom_image, dirname, "", "", 0, &sb->root);
   
   //process_directory(rom_image, dirname, sb->root.offset<<2, sb->root.size, ".");
   
   printstats();
   return 0;
}
