
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <mntent.h>

#include <sys/mount.h>

#define MTAB    "/etc/mtab"
#define TMPMTAB "/tmp/mtab.detach.tmp"

#define DEBUG   0

typedef struct {
  char dev[256], mp[1024], flags[1024];
}
fs;

int searchfstab(const char *tabfname, const char *fname, int dev, fs *result);
int detachdev(const char *dev);


int main(int argc, char **argv)
{
  fs getdev, filesys, fstab;
  char *blkdev, *edit;
  int argind, mtaberr;

  if( argc < 2 || !strcmp(argv[1], "-h") || !strcmp(argv[1], "--help") ) {
    fprintf(stderr,  "Usage: detach <device or path> ...\n"
"Unmounts all file systems on a device and removes it from the system.  The\n"
"device can be specified as the device file (starting with /dev/) or by the\n"
"absolute path of a file on one of its file systems.  This program is intended\n"
"for use with removable media and will refuse to work on devices which are in\n"
"/etc/fstab but are not user mountable.\n");
    exit(0);
  }
  
  for( argind= 1; argind< argc; ++argind )
  {
#if DEBUG
    printf("arg %s\n", argv[argind]);
#endif
    if( !strncmp(argv[argind], "/dev/", 5) ) {
      blkdev= argv[argind];
    }
    else {
      if( !searchfstab("/proc/mounts", argv[argind], 0, &getdev) ) {
        fprintf(stderr, "Error searching /proc/mounts: %s not found.\n", argv[argind]);
        continue;
      }
      blkdev= getdev.dev;
#if DEBUG
      fprintf(stderr, "Found %s:\n%s  \t%s  \t%s\n", argv[argind], 
                  getdev.dev, getdev.mp, getdev.flags);
#endif
    }
    for( edit= blkdev + strlen(blkdev) - 1;
         edit >= blkdev && *edit >= '0' && *edit <= '9'; --edit )
      *edit= 0;
    while( searchfstab("/proc/mounts", blkdev, 1, &filesys) ) {
      if( searchfstab("/etc/fstab", filesys.mp, 0, &fstab) &&
          !strcmp(filesys.mp, fstab.mp) &&
          (strstr(fstab.flags, "nouser") || !strstr(fstab.flags, "user")) ) {
        fprintf(stderr, "Error: %s is not user-unmountable.\n", filesys.mp);
        goto continue_argind;
      }
      printf("Unmounting %s from %s...\n", filesys.dev, filesys.mp);
      errno= 0;
      if( umount(filesys.mp) || errno ) {
        perror("Error unmounting");
        goto continue_argind;
      }
    }
    mtaberr= !grepmtab(blkdev);
    for( edit= blkdev + strlen(blkdev) - 1; edit > blkdev && *edit != '/'; --edit );
    if( *edit == '/' )  blkdev= edit+1;
    else                blkdev= edit;
    if( strlen(blkdev) != 3 ) {
      fprintf(stderr, "Error: device %s not of length 3.\n", blkdev);
      continue;
    }
    printf("Detaching device %s...\n", blkdev);
    if( detachdev(blkdev) ) {
      fprintf(stderr, "Error detaching device\n");
      continue;
    }
    printf("Removing %s from /etc/mtab...\n", blkdev);
    if( mtaberr || rename(TMPMTAB, MTAB) )
      fprintf(stderr, "Could not update /etc/mtab\n");
continue_argind:
    ;
  }
  return 0;
}


/*
  searchfstab   search fstab or similar file for filesystem on which a file
                name resides.  The root file system is never returned (as this
                is not desirable for this application).  If the dev argument is
                true, the first filesystem on a subdevice of the one given is
                returned.

  ->  tabfname  file name of fstab
      fname     file name to search for
      dev       if non-zero, fname is a block device to search for
      result    storage for fs data; will be used as buffer even if unsuccessful
  <-  1 success, 0 error or not found
*/
int searchfstab(const char *tabfname, const char *fname, int dev, fs *result)
{
  FILE *in;
  struct mntent *entry;
  int size, len, currlen= 0;

  in= setmntent(tabfname, "r");
  if( !in )
    return 0;
  while( entry = getmntent(in) ) {
    if( !dev )
      if( (len= strlen(entry->mnt_dir)) > currlen
           && !strncmp(entry->mnt_dir, fname, len)
           && (!fname[len] || fname[len] == '/') ) {
        strncpy(result->dev, entry->mnt_fsname, 256);
        strncpy(result->mp, entry->mnt_dir, 1024);
        strncpy(result->flags, entry->mnt_opts, 1024);
        currlen= len;
      }
      else;
    else if( !strncmp(entry->mnt_fsname, fname, strlen(fname)) ) {
      strncpy(result->dev, entry->mnt_fsname, 256);
      strncpy(result->mp, entry->mnt_dir, 1024);
      strncpy(result->flags, entry->mnt_opts, 1024);
      endmntent(in);
      return 1;
    }
  }
  endmntent(in);
  return !!currlen;
}


/*
  detachdev     detach disk device by writing "1" to
                /sys/block/<device>/device/delete
  ->  dev       block device, without /dev/ or subdevice number
  <-  0 success, 1 write error
*/
int detachdev(const char *dev)
{
  FILE *w;
  char delpath[80];
  int err;

  snprintf(delpath, 80, "/sys/block/%s/device/delete", dev);
  w= fopen(delpath, "w");
  if( !w )
    return 0;
  errno= 0;
  err= fwrite("1\n", 1, 2, w) != 2;
  err= err || ferror(w) || errno;
  fclose(w);
  return err;
}


/*
  grepmtab    Remove lines referring to a specific block device from
                /etc/mtab and save the result in a temporary file TMPMTAB

  ->  dev   Name of the block device
  <-  1 success, 0 error
*/
int grepmtab(const char *dev)
{
  FILE *in, *copy;
  struct mntent *entry;
  int size, len, currlen= 0;

  in= setmntent(MTAB, "r");
  copy= setmntent(TMPMTAB, "w");
  if( !in || !copy )
    return 0;
  while( entry = getmntent(in) ) {
    if( strncmp(entry->mnt_fsname, dev, strlen(dev)) &&
        addmntent(copy, entry) ) {
      endmntent(in);
      endmntent(copy);
      return 0;
    }
  }
  endmntent(in);
  endmntent(copy);
  return 1;
}

