/* $Id: recovery.c,v 1.2 2001/05/30 15:47:04 harbourn Exp $
 * File recovery module for fatback
 */
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <string.h>
#include <errno.h>
#include "recovery.h"
#include "dirtree.h"
#include "output.h"
#include "fat.h"
#include "input.h"
#include "util.h"
#include "output.h"

static int carve_file(clust_t *, unsigned long, unsigned long, unsigned long, int);
static int fname_is_avail(char *);

/* Error messages for the audit log */
char *log_notowner = "\"%s\": file may contain data that does not belong to it.\n";
char *log_clustoccupied = "\"%s\": cluster number %lu of file is occupied by an active file\n";
char *log_clustinvalid = "\"%s\": invalid cluster reported, contents may be innaccurate\n";
char *log_sizeinconsis = "\"%s\": File size inconsistancy, contents may be inaccurate\n";
char *log_nametaken = "\"%s\": name already taken, outputting cluster chain %lu to \"%s\" instead\n";
char *log_extracting = "Extracting cluster chain %lu to file %s\n";
char *log_carve = "\"%s\": Unable to recover file entirely,  carving instead.\n";
char *log_nocarve = "\"%s\": Unable to recover file entirely\n";
char *error_naming = "Error: Can't generate unique filename for \"%s\"\n";
char *error_reading = "Error: Unable to read cluster from input stream at location: %lu\n";
char *error_writing = "Error: Unable to write to file \"%s\"\n";

/*
 * Extract a file from the input stream, and place it in the file
 * specified by fname.  If size is specified as 0, then the entire
 * cluster chain is written out.
 */
int extract_file(
     dirent_t *ent,
     clust_t *clusts, 
     unsigned long bytes_per_clust,
     unsigned long size,
     unsigned long cluster,
     unsigned long max_cluster,
     char *filename)
{
     int file;
     char *fname;
     unsigned long clust = 0, left;
     void *buffer;
     unsigned long chainlen, reqd_clusts;

     assert(filename && clusts && bytes_per_clust);

     /* replace '~' with the users home directory */
     if (filename[0] == '~')
          filename = replace_tilde(filename);

     /* Find a filename for output */
     if (!(fname = unused_fname(filename))) {
          display(VERBOSE, error_naming, fname);
          free(fname);
          return -1;
     } else if (strcmp(fname, filename) != 0)
          display(VERBOSE, log_nametaken, filename, cluster, fname);

     if ((file = open(fname, O_WRONLY|O_CREAT|O_EXCL, S_IRUSR|S_IRGRP|S_IROTH|S_IWUSR)) == NULL) {
          perror("Error");
          free(fname);
          return -1;
     }
     /* If it is a directory entry being extracted, 
      * whose size is zero, just close the file */
     if (ent && !size) {
          close(file);
          free(fname);
          return 0;
     }
     
     display(VERBOSE, log_extracting, cluster,
           fname);

     chainlen = chain_length(clusts, cluster);
     reqd_clusts = size / bytes_per_clust;
     reqd_clusts += !!(size % bytes_per_clust);
     if (chainlen < reqd_clusts) {
          display(VERBOSE, log_carve, fname);
          carve_file(clusts, cluster, size, bytes_per_clust, file);
          return 0;
     }

     /* for efficiency we are going to use a single buffer of 
      * cluster size, and reuse it for each write */
     buffer = emalloc(bytes_per_clust);

     /* loop through the clusters, writing out the data */
     /* if a size of 0 was passed in, we are to recover the cluster chain */
     left = size ? size : chainlen * bytes_per_clust;
     do {
          size_t amount = left < bytes_per_clust? left : bytes_per_clust;
     
          clust = clust ? clusts[clust].fat_entry : cluster;
          if (!amount && size != 0) {
               display(VERBOSE, log_sizeinconsis, fname);
               close(file);
               free(buffer);
               free(fname);
               return -1;
          }
          if ((clust > max_cluster) || (clust < 2)) {
               display(VERBOSE, log_clustinvalid, fname);
               close(file);
               free(buffer);
               free(fname);
               return -1;
          }
          if (ent && ent != clusts[clust].owner) {
               if (clusts[clust].flags & CLUST_ACTIVE) {
                    display(VERBOSE, log_clustoccupied, fname, clust);
                    close(file);
                    free(buffer);
                    free(fname);
                    return -1;
               } else if (ent != NULL) {
                    /* keep track of which file this log message was last
                     * reported for to keep from having multiple log entries
                     * for the same file */
                    static dirent_t *last_notowner;
                    if (last_notowner != ent) {
                         display(VERBOSE, log_notowner, fname);
                         last_notowner = ent;
                    }
               }
          }
          errno = 0;
          read_data(buffer, clusts[clust].loc, bytes_per_clust);
          if (errno) {
               display(NORMAL, error_reading, clusts[clust].loc);
               close(file);
               free(buffer);
               free(fname);
               return -1;
          }
          if (amount != write(file, buffer, amount)) {
               display(NORMAL, error_writing, fname);
               close(file);
               free(buffer);
               free(fname);
               return -1;
          }
          /* mark the cluster as recovered */
          clusts[clust].flags |= CLUST_RECOVERED;
          left -= amount;
     } while (left && 
              !clust_is_end(&clusts[clust]) && 
              !clust_is_bad(&clusts[clust]) &&
              clusts[clust].fat_entry);

     close(file);
     free(buffer);
     free(fname);
     return 0;
}

/*
 * Extract clusters to a file.  This is a physical technique
 * because it does not check for other file's ownerships or
 * whether or not the file has been recovered. 
 */
static int carve_file(clust_t *clusts,
                      unsigned long cluster,
                      unsigned long size,
                      unsigned long bytes_per_clust,
                      int file)
{
     void *buffer;
     unsigned long left, ci;
     size_t amount;

     assert(bytes_per_clust && file && clusts);
     buffer = emalloc(bytes_per_clust);
     
     left = size;
     for (left = size, ci = 0; left != 0; left -= amount, ci++) {
          amount = left <  bytes_per_clust ? left : bytes_per_clust;
          read_data(buffer, clusts[cluster + ci].loc, bytes_per_clust);
          if (bytes_per_clust != write(file, buffer, amount))
               return -1;
     }
     return 0;
}
     
/*
 * Determine if a file in the real filesystem is a directory
 */
int stat_is_dir(char *arg)
{
     struct stat buf;
     
     if (0 > stat(arg, &buf)) {
          return -1;
     }
     return S_ISDIR(buf.st_mode);
}

/*
 * Concatenate a filename by adding a filename to the
 * end of a directory name.  Also append a '/' if necissary
 */
char *fn_cat(char *dir, char *file)
{
     int dir_len, file_len;
     char *retval;

     assert(dir && file);
     dir_len = strlen(dir);
     file_len = strlen(file);
     
     /* if the dir name already ends in '/'
      * remove it, for simplicity later */
     if (dir[dir_len - 1] == '/')
          dir[--dir_len] = '\0';

     /* add the directry name */
     retval = emalloc(dir_len + file_len + 2);
     strncpy(retval, dir, dir_len);
     retval[dir_len] = '/';
     /* now the file name */
     strncpy((retval + dir_len + 1), file, file_len);
     retval[dir_len + 1 + file_len] = '\0';
     return retval;
}

/*
 * Make a directory with a specified name
 */
int make_dir(char *dirname)
{
     assert(dirname);

     //dname = unused_fname(dirname);
     if (mkdir(dirname, S_IRWXU | S_IRWXG | S_IROTH | S_IXOTH) != 0) {
          perror("Error creating directory:");
          return -1;
     }
     return 1;
}


/*
 * Return a filename that is a unique permutation of
 * the fname passed in 
 */
char *unused_fname(char *fname)
{
     /* MAX_NUMBER_LEN represents the maximum length that
      * a printed representation of a number can be.  This
      * is arbitrarily picked.  Change this if there is a 
      * need for more than 10^MAX_NUMBER_LEN duplicate files.
      * The value of 100 should work. */
     const size_t MAX_NUMBER_LEN = 100;
     char *new_fname = fname;
     struct stat stat_buf;
     unsigned i;
     int test;

     assert(fname);

     /* the loop counter i will be the suffix whenever 
      * a unique name is found */
     for (i = 2; new_fname && !fname_is_avail(new_fname); i++) {
          size_t new_buflen = strlen(fname) + MAX_NUMBER_LEN + 1;
          if (new_fname != fname)
               free(new_fname);
          new_fname = emalloc(new_buflen);
          snprintf(new_fname, new_buflen, "%s.%u", fname, i);
     }

     return new_fname == fname ? strdup(fname) : new_fname;
}

/*
 * Determine if a filename is in use
 */
static int fname_is_avail(char *fname)
{
     struct stat stat_buf;

     if ((stat(fname, &stat_buf) < 0) && errno == ENOENT)
          return 1;
     else 
          return 0;
}

/*
 * Replace '~' with the users home directory in
 * a filename
 */
char *replace_tilde(char *fname)
{
     char *retval;
     char *homedir = getenv("HOME");

     retval = emalloc(strlen(homedir) + strlen(fname));

     strcpy(retval, homedir);
     strcat(retval, ++fname);
     
     return retval;
}
     


syntax highlighted by Code2HTML, v. 0.9.1