bug-hurd
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

Re: mtab translator (post August Hurd update version)


From: David Walter
Subject: Re: mtab translator (post August Hurd update version)
Date: Sat, 17 Aug 2002 18:48:43 -0400
User-agent: Gnus/5.090007 (Oort Gnus v0.07) XEmacs/21.4 (Honest Recruiter, i386-unknown-gnu0.2)

David Walter<dwalter@syr.edu> writes:

> Well here is a translator to act as mtab.   It accepts the mount point
> as a path,  then requests the information  from the filesystem and the
> active translator for options.
>
> df works with this as does cat /var/run/mtab

As luck,  and timing would  have it, I  installed the latest Hurd last
evening just  after posting this  and found  that  the interfaces  had
changed.

Here are changes to support the current Hurd.

2002-08-17  David Walter  <dwalter@syr.edu>

        * mtab.c (trivfs_S_io_write):
        struct trivfs_protid*cred --> trivfs_protid_t cred
        off_t --> loff_t
        vm_address_t data --> data_t data
        (trivfs_modify_stat): 
        struct trivfs_protid*cred --> trivfs_protid_t cred
        (trivfs_S_io_readable): 
        struct trivfs_protid*cred --> trivfs_protid_t cred
        mach_msg_type_number_t* amount -> vm_size_t* amount
        (trivfs_S_file_set_size): 
        struct trivfs_protid*cred --> trivfs_protid_t cred
        additional arguments, mach_port_t reply
                              mach_msg_type_name_t
        off_t size --> loff_t size
        (trivfs_S_io_read): 
        struct trivfs_protid*cred --> trivfs_protid_t cred
        off_t --> loff_t
        vm_address_t data --> data_t data
        (trivfs_S_io_seek): 
        struct trivfs_protid*cred --> trivfs_protid_t cred
        off_t --> loff_t
        (trivfs_S_io_select): 
        struct trivfs_protid*cred --> trivfs_protid_t cred
        int*type --> mach_msg_type_name_t type
        (trivfs_S_io_get_openmodes): 
        (trivfs_S_io_set_openmodes): 
        (trivfs_S_io_set_all_openmodes): 
        (trivfs_S_io_set_some_openmodes): 
        (trivfs_S_file_get_translator): 
        struct trivfs_protid*cred --> trivfs_protid_t cred

        * first submission was in parallel with the change to
        begin support long files. updated to include data type
        changes.

        * introduced first submitted version of mtab translator.

/* mtab: dynamic mount table information. add path, read mount info.

Copyright (C) 2002 Free Software Foundation, Inc.

This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License as
published by the Free Software Foundation; either version 2, or (at
your option) any later version.

This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. */

/* 
   Thanks to everyone who contributed ideas for this translator.
*/

#define _GNU_SOURCE 1

#include <hurd/trivfs.h>
#include <stdlib.h>                               /* exit () */
#include <error.h>                                /* Error numers */
#include <fcntl.h>                                /* O_READ etc. */
#include <sys/mman.h>                             /* MAP_ANON etc. */
#include <mntent.h>                               /* mount entry table 
manipulation */
#include <store.h>
#include <stdio.h>
#include <string.h>
#include <assert.h>
#include <unistd.h>                               
#include <argz.h>
#include <version.h>
#include <argp.h>
// const char *argp_program_version = STANDARD_HURD_VERSION (mtab);
static struct argp_option options[] =
  {
    {0, 0}
  };
static char *args_doc = "";
static char *doc = "mount table translator mtab. "
"behaves as the /etc/mtab file, "
"insert the path to a mounted filesystem and it"
"stores returns the information whenever requested."
"Automatically inserts the root '/' directory information.";
////////////////////////////////////////////////////////////////////////
// Data definitions
////////////////////////////////////////////////////////////////////////
typedef struct mtab{
  char* path;
  char* data;
  size_t length;
  struct mtab*next;
} mtab;
struct mutex mtab_update_lock;
mtab* mount_table=0;            /* list of paths of registered active
                                   mounted filesystems and their
                                   data */
mtab* tail=0;

////////////////////////////////////////////////////////////////////////
// Prototypes
////////////////////////////////////////////////////////////////////////
error_t mtab_write_table_to_buffer(mtab *mt, char **buffer, int *buffer_size);
void mtab_free(mtab *mt);
void mtab_free_table(mtab *mt);
error_t mtab_filesystem_info(const char *path, mtab **entry);
mtab *mtab_create(void);
mtab *mtab_remove_helper(mtab *mt, const char *path);
mtab *mtab_remove(mtab **mt, const char *path);
error_t mtab_insert(mtab **mount_table, mtab *entry);
int mtab_entry_buffer_size(mtab *mt);
size_t mtab_mount_table_buffer_size(mtab *mt);
void mtab_manage_info(void);

// allocates memory and returns size in buffer size, and buffer points
// to allocated memory.
error_t
mtab_write_table_to_buffer(mtab*mt, char**buffer, int*buffer_size){
  assert(buffer);
  if(*buffer) free(*buffer);
  *buffer             = 0;
  *buffer_size        = 0;
  *buffer_size       += mtab_mount_table_buffer_size(mt);
  *buffer             = malloc(*buffer_size);
  if(!*buffer)
    return ENOMEM;
  // copy all items from mount_table mt to the buffer.
  size_t offset = 0;

  mutex_lock(&mtab_update_lock);
  while(mt){
    sprintf((*buffer)+offset, "%s", mt->data);
    /*       mtab_entry_buffer_copy(mt, ); */
    int length = mtab_entry_buffer_size(mt);
    offset += length;
    mt      = mt->next;
  }
  mutex_unlock(&mtab_update_lock);
  return 0;
}

void    
mtab_free (mtab*mt){
  assert(mt);
  free(mt->path);
  free(mt->data);
  free(mt);
}

void    
mtab_free_table(mtab*mt){
  mutex_lock(&mtab_update_lock);
  mtab*t;
  while(mt){
    t = mt;
    mt = mt->next;
    mtab_free(t);
  }
  mutex_unlock(&mtab_update_lock);
}

/*
  create an mtab like entry in memory: see /include/mntent.h for
  structure

  devicename path translator translatorargs 0 0
*/
error_t
mtab_filesystem_info(const char* path, mtab**entry)
{
  char *transinfo = NULL;
  char *toptransinfo = NULL;
  file_t trans_node;
  int transinfolen = 0;
  struct store *storeinfo = NULL;
  mtab* mt = NULL;
   
  if(!(mt = mtab_create()))
    return ENOMEM;
  
  errno = 0;
  // step 1. is there an active translator on this path? 

  //  file_t file = file_name_lookup_carefully (path, 0, 0);
  ////////////////////////////////////////////////////////////////////////
  // determine if this is an (actively) translated path or not.
  ////////////////////////////////////////////////////////////////////////
  if(strcmp(path, "/") != 0) {  // skip if root, special case for root.
    // get the file w/o the translator 
    file_t file = file_name_lookup(path, O_NOTRANS, 0);
    fsys_t fsys;
    // get the active translator if there is one. 
    error_t active_trans_rc = file_get_translator_cntl(file, &fsys); 
    mach_port_deallocate(mach_task_self(), file);  
    if(active_trans_rc)
      return EINVAL;
  }
  ////////////////////////////////////////////////////////////////////////
  // The root partition (in particular)  needs to use the storeio info
  ////////////////////////////////////////////////////////////////////////
  // Others (other translated nodes) require only the information from
  // the translator.
  ////////////////////////////////////////////////////////////////////////
  ////////////////////////////////////////////////////////////////////////
  // it is actively translated, get the information re: the translation
  ////////////////////////////////////////////////////////////////////////
  
  trans_node = file_name_lookup(path, 0, 0666); // get the underlying node
  int s_rc;
  
  s_rc = store_create(trans_node,
                      STORE_INACTIVE|STORE_NO_FILEIO, 0, &storeinfo);
  int m_rc;
  int f_rc;
  
  m_rc = mach_port_deallocate(mach_task_self(), trans_node);
  // get the file w/o the translator 
  trans_node = file_name_lookup(path, 0, 0666); 
  f_rc = file_get_fs_options(trans_node, &toptransinfo, &transinfolen);
  m_rc = mach_port_deallocate(mach_task_self(), trans_node); 

  if (trans_node == MACH_PORT_NULL || f_rc != 0 ){ // || m_rc != KERN_SUCCESS){
    if (toptransinfo != NULL)       munmap(toptransinfo,transinfolen);
    return EINVAL;
  }

  char* options_start = toptransinfo + strlen(toptransinfo) + 1;
  argz_stringify(toptransinfo + strlen(toptransinfo)+1,
                 transinfolen-(strlen(toptransinfo)+1), ',');
  transinfo = strrchr(toptransinfo, '/')+1;
  toptransinfo[strlen(toptransinfo)] = ' '; 
  char*fsname=0;
  if(storeinfo && storeinfo->name) //  no store information (not
                                   //  active mount point?)
    fsname = strdup(storeinfo->name);
  else
    fsname = strdup(strrchr(strchr(toptransinfo, ',')+1, '/')+1);

  if(!fsname)
    return ENOMEM;

  mt->path = strdup(path);
  if (!mt->path)
    return ENOMEM;

  char* type = malloc(transinfolen);
  memset(type, 0, transinfolen);
  char* options = malloc(transinfolen);
  memset(options, 0, transinfolen);
  sscanf(options_start, "%s %s", type, options);

  if(!type || !options){
    if(type)
      free(type);
    if(options)
      free(options);
    return ENOMEM;
  }

  mt->length = asprintf(&mt->data, "%s %s %s %s %i %i\n",
                        fsname,
                        path,
                        type,
                        options,
                        0,
                        0);
  if (storeinfo)
    store_free(storeinfo);

  if (toptransinfo)
    munmap(toptransinfo,transinfolen);

  if(type)
    free(type);

  if(options)
    free(options);

  *entry=mt;
  return 0;
}

mtab*
mtab_create (){
  mtab* mt = malloc (sizeof (struct mtab));
  if (mt){
    mt->next=0;
    return mt;
  }
  return 0;
}

mtab*                                             
mtab_remove_helper(mtab* mt, const char*path)
{
  if (!mt)              // end of data case 
    return 0;
  if (!mt->next)
    tail = mt;
  // found case 
  if(strcmp(path, mt->path) == 0) {
    mtab* next = mt->next;
    mtab_free(mt);
    return next = mtab_remove_helper(next, path);
  }
  mt->next = mtab_remove_helper(mt->next, path);
  return mt;
}

/*
  mtab_remove: remove item when path matches, free memory of object.
*/
mtab*
mtab_remove(mtab**mt, const char*path){
  return *mt = mtab_remove_helper(*mt, path);
}

error_t
mtab_insert(mtab**mount_table, mtab*entry){
  assert(mount_table);
  mutex_lock(&mtab_update_lock);
  if(!*mount_table){              // empty table
    tail=*mount_table=entry;
  }
  else{
    tail->next= entry;
    tail      = tail->next;
  }
  mutex_unlock(&mtab_update_lock);
  return 0;
}

int
mtab_entry_buffer_size(mtab* mt){ // struct mntent*entry){
  return mt->length;
}

size_t
mtab_mount_table_buffer_size(mtab*mt){
  int size=0;
  while(mt){
    size+=mtab_entry_buffer_size(mt);
    mt=mt->next;
  }
  size++;
  return size;
}

void mtab_manage_info(void){
  static boolean_t started = FALSE;
  if(!started){                                  // always start with
    // a root
    // filesystem.
    started = TRUE;
    char* path = "/";
      
    mtab *mt=0;
    error_t rc = mtab_filesystem_info(path, &mt);

    ////////////////////////////////////////////////////////////////////////
    // something bad happened, if we can't instantiate the root filesystem.
    ////////////////////////////////////////////////////////////////////////
    switch(rc){

    case ENOMEM:
      if(mt)
        mtab_free(mt);
      error(1,KERN_NO_SPACE, "mtab_filesystem_info");
      break;
         
    case EINVAL:
      if(mt)
        mtab_free(mt);
      error(1,KERN_NO_SPACE, "mtab_filesystem_info");
      break;
    default:
      mt->next = 0;
      mtab_insert(&mount_table, mt);
      break;
    }
  }
}
// Port bucket we service requests on. 
struct port_bucket *port_bucket;
struct port_class *trivfs_protid_portclasses[1];
struct port_class *trivfs_cntl_portclasses[1];
int trivfs_protid_nportclasses = 1;
int trivfs_cntl_nportclasses = 1;

static int
mtab_demuxer (mach_msg_header_t *inp, mach_msg_header_t *outp)
{
  extern int mtab_server (mach_msg_header_t *inp, mach_msg_header_t *outp);
  return trivfs_demuxer (inp, outp);
}

char**arguments=NULL;
char*mtab_argz_string = 0;
int mtab_argz_length  = 0;

int
main(int argc, char**argv){

  error_t parse_opt (int key, char *arg, struct argp_state *state){
    switch (key)
      {
      case 'v': error(1,0, argp_program_version);     break;
      default: break;
      }
    return 0;
  }

  struct argp argp = {options, parse_opt, args_doc, doc};

  argp_parse (&argp, argc, argv, 0, 0, 0);

  error_t err;
  mach_port_t bootstrap;
  struct trivfs_control *fsys;
  // ************************************************************************
  // create a translator from here.
  // ************************************************************************
     
  task_get_bootstrap_port (mach_task_self (), &bootstrap);
  if (bootstrap == MACH_PORT_NULL)
    error (1, 0, "Must be started as a translator");

  arguments = argv;
  if(mtab_argz_string==0){
    if(argz_create(arguments, &mtab_argz_string, &mtab_argz_length) == ENOMEM){
      error(0, ENOMEM, "argz creation failed");
    }
  }
   
  //  Reply to our parent 
  port_bucket = ports_create_bucket ();
  trivfs_cntl_portclasses[0] = ports_create_class (trivfs_clean_cntl, 0);
  trivfs_protid_portclasses[0] = ports_create_class (trivfs_clean_protid, 0);
  // Reply to our parent. 
  err = trivfs_startup (bootstrap, 0,
                        trivfs_cntl_portclasses[0], port_bucket,
                        trivfs_protid_portclasses[0], port_bucket,
                        &fsys);
  if (err)
    error (1, err, "trivfs_startup failed");
     
  //  Launch. 
  ports_manage_port_operations_multithread (port_bucket, // fsys->pi.bucket,  
                                            mtab_demuxer,
                                            0,
                                            0,
                                            mtab_manage_info); 
  return 0;
}


/* Trivfs hooks. */
int trivfs_fstype = FSTYPE_MISC;  /* Generic trivfs server */
int trivfs_fsid = 0;              /* Should always be 0 on startup */
     
int trivfs_allow_open = O_READ | O_WRITE;
     
/* Actual supported modes: */
int trivfs_support_read  = 1;
int trivfs_support_write = 1;
int trivfs_support_exec  = 0;
     
/* A hook for us to keep track of the file descriptor state. */
struct open
{
  off_t offs;
  char*contents;
  size_t contents_length;
};

static error_t
open_hook (struct trivfs_peropen *peropen)
{
  struct open *op = malloc (sizeof (struct open));
  if (op == NULL)
    return ENOMEM;

  /* Initialize the offset. */
  op->offs            = 0;
  op->contents        = 0;
  op->contents_length = 0;
  peropen->hook = op;

  error_t rc;
  const char*path;

  mtab*new_table=0;
  mtab*mt=mount_table;
  mtab*entry;
  while(mt){
    path = mt->path;
    if(!(rc = mtab_filesystem_info(path, &entry)))
      mtab_insert(&new_table, entry);
    mt = mt->next;
  }
  mtab_free_table(mount_table);
  mount_table = new_table;
  return mtab_write_table_to_buffer(mount_table,
                                    &op->contents,
                                    &op->contents_length);
}


static void
close_hook (struct trivfs_peropen *peropen)
{
  struct open *op = peropen->hook;
  if(op && op->contents)
    free(op->contents);
  free (peropen->hook);
}

struct open*get_hook_from_cred(trivfs_protid_t cred){
  struct open *op;
  op = cred->po->hook;
  return op;
}

void
trivfs_modify_stat (trivfs_protid_t cred, struct stat *st)
{
  /* Get the offset. */

  st->st_mode &= ~S_IFMT;                        /*  */
  st->st_mode |= S_IFREG;       /* Appear as a regular file. */
  st->st_mode |= S_IATRANS;     /* Say that this has an active
                                   translator running. */
  if (!cred)
      return;

  mutex_lock(&mtab_update_lock);
  st->st_size = get_hook_from_cred(cred)->contents_length;
  mutex_unlock(&mtab_update_lock);
}
     
error_t
trivfs_goaway (struct trivfs_control *cntl, int flags)
{
  exit (EXIT_SUCCESS);
}

/*
  If this variable is set, it is called every time a new peropen
  structure is created and initialized.
*/
error_t (*trivfs_peropen_create_hook)(struct trivfs_peropen *) = open_hook;

/*
  If this variable is set, it is called every time a peropen structure
  is about to be destroyed.
*/
void (*trivfs_peropen_destroy_hook) (struct trivfs_peropen *) = close_hook;
     
kern_return_t
trivfs_S_io_write (trivfs_protid_t cred,
                   mach_port_t reply, mach_msg_type_name_t replytype,
                   data_t data, mach_msg_type_number_t datalen,
                   loff_t offs, mach_msg_type_number_t *amount)
{
  if (!cred)
    return EOPNOTSUPP;
  else if (!(cred->po->openmodes & O_WRITE))
    return EBADF;

  boolean_t space(char c){                       /* helper for clean input */
    return c == ' ' || c == '\n' || c == '\t' || c == '\f';
  }

  /*    fprintf(stderr, "%s\n", (char*)data); */

  mtab *mt=0;
  char* p = (char*)data+datalen-1;

  while(space(*p))
    *p-- = '\0';

  p = strdup((char*)data);
  error_t rc = mtab_filesystem_info(p, &mt);

  switch(rc){
  case ENOMEM:
    if(mt)
      mtab_free(mt);
    return KERN_NO_SPACE;
  case EINVAL:
    if(mt)
      mtab_free(mt);
    return KERN_INVALID_ARGUMENT;
  default:
    ////////////////////////////////////////////////////////////////////////
    // as mtab_remove is a recursive call, it must lock prior to the
    // call, unlock on return.
    ////////////////////////////////////////////////////////////////////////
    mutex_lock(&mtab_update_lock);
    mount_table = mtab_remove(&mount_table, p);
    mutex_unlock(&mtab_update_lock);
      
    mtab_insert(&mount_table, mt);
    break;
  }
  *amount = datalen;
  return KERN_SUCCESS;
}
     
/* 
   Tell how much data can be read from the object without blocking for
   a "long time" (this should be the same meaning of "long time" used
   by the nonblocking flag.
*/
kern_return_t
trivfs_S_io_readable (trivfs_protid_t cred,
                      mach_port_t reply, mach_msg_type_name_t replytype,
                      vm_size_t *amount)
{
  if (!cred)
    return EOPNOTSUPP;
  else if (!(cred->po->openmodes & O_READ))
    return EINVAL;
  else
    {
      mutex_lock(&mtab_update_lock);
      *amount = get_hook_from_cred(cred)->contents_length;
      mutex_unlock(&mtab_update_lock);
    }
  return 0;
}
     
/* Truncate file.  */
kern_return_t
trivfs_S_file_set_size (trivfs_protid_t cred,
                        mach_port_t reply,
                        mach_msg_type_name_t replyPoly,
                        loff_t size)
{
  if (!cred)
    return EOPNOTSUPP;
  else
    return 0;
}

/* Read data from an IO object.  If offset is -1, read from the object
   maintained file pointer.  If the object is not seekable, offset is
   ignored.  The amount desired to be read is in AMOUNT.  */
error_t
trivfs_S_io_read (trivfs_protid_t cred,
                  mach_port_t reply, mach_msg_type_name_t reply_type,
                  data_t *data, mach_msg_type_number_t *data_len,
                  loff_t offs, vm_size_t amount)
{

  struct open *op;

  /* Deny access if they have bad credentials. */
  if (! cred)
    return EOPNOTSUPP;
  else if (! (cred->po->openmodes & O_READ))
    return EBADF;
  /* Get the offset. */
  op = cred->po->hook;

  if(!op)
    return EINVAL;

  if (offs == -1)
    offs = op->offs;

  mutex_lock(&mtab_update_lock);

  /* Prune the amount they want to read. */
  if(offs == 0){
    // *FIXME* build the table here.
  }
  if (offs > op->contents_length)
    offs = op->contents_length;
  if (offs + amount > op->contents_length)
    amount = op->contents_length - offs;

  if (amount > 0)
    {
      /* Possibly allocate a new buffer. */
      if (*data_len < amount)
        *data = (data_t) mmap (0, amount, PROT_READ|PROT_WRITE,
                                     MAP_ANON, 0, 0);

      /* Copy the constant data into the buffer. */
      memcpy ((char *) *data, op->contents + offs, amount);

      /* Update the saved offset. */
      op->offs += amount;
    }
  *data_len = amount;

  mutex_unlock(&mtab_update_lock);
  return 0;
}


/* Change current read/write offset */
error_t
trivfs_S_io_seek (trivfs_protid_t cred,
                  mach_port_t reply, mach_msg_type_name_t reply_type,
                  loff_t offs, int whence, loff_t *new_offs)
{
  struct open *op;
  error_t err = 0;
  if (! cred)
    return EOPNOTSUPP;

  op = cred->po->hook;

  mutex_lock(&mtab_update_lock);
  switch (whence)
    {
    case SEEK_SET:
      op->offs = offs; break;
    case SEEK_CUR:
      op->offs += offs; break;
    case SEEK_END:
      op->offs = get_hook_from_cred(cred)->contents_length - offs; break;
    default:
      err = EINVAL;
    }

  mutex_unlock(&mtab_update_lock);
  
  if (! err)
    *new_offs = op->offs;

  return err;
}
     
/*
  SELECT_TYPE is the bitwise OR of SELECT_READ, SELECT_WRITE, and
  SELECT_URG. Block until one of the indicated types of i/o can be
  done "quickly", and return the types that are then available.
  TAG is returned as passed; it is just for the convenience of the
  user in matching up reply messages with specific requests sent.
*/
kern_return_t
trivfs_S_io_select (trivfs_protid_t cred,
                    mach_port_t reply,
                    mach_msg_type_name_t type,
                    int *tag)
{
  if (!cred)
    return EOPNOTSUPP;
  else
    if (((type & SELECT_READ) && !(cred->po->openmodes & O_READ))
        || ((type & SELECT_WRITE) && !(cred->po->openmodes & O_WRITE)))
      return EBADF;
    else
      type &= ~SELECT_URG;
  return 0;
}
     
/* Well, we have to define these four functions, so here we go: */
kern_return_t
trivfs_S_io_get_openmodes (trivfs_protid_t cred, mach_port_t reply,
                           mach_msg_type_name_t replytype, int *bits)
{
  if (!cred)
    return EOPNOTSUPP;
  else
    {
      *bits = cred->po->openmodes;
      return 0;
    }
}
     
error_t
trivfs_S_io_set_all_openmodes (trivfs_protid_t cred,
                               mach_port_t reply,
                               mach_msg_type_name_t replytype,
                               int mode)
{
  if (!cred)
    return EOPNOTSUPP;
  else
    return 0;
}
     
kern_return_t
trivfs_S_io_set_some_openmodes (trivfs_protid_t cred,
                                mach_port_t reply,
                                mach_msg_type_name_t replytype,
                                int bits)
{
  if (!cred)
    return EOPNOTSUPP;
  else
    return 0;
}
     
kern_return_t
trivfs_S_io_clear_some_openmodes (trivfs_protid_t cred,
                                  mach_port_t reply,
                                  mach_msg_type_name_t replytype,
                                  int bits)
{
  if (!cred)
    return EOPNOTSUPP;
  else
    return 0;
}

/* return the translators command line options. */
kern_return_t
trivfs_S_file_get_translator (trivfs_protid_t cred,
                              mach_port_t reply,
                              mach_msg_type_name_t reply_type,
                              char **trans, size_t *translen)
{
  
  if (!cred)
    return EOPNOTSUPP;

  *trans = mmap (0, mtab_argz_length, PROT_READ|PROT_WRITE, MAP_ANON, 0, 0);
  bcopy (mtab_argz_string, *trans, *translen=mtab_argz_length);
  return 0;
}

-- 
/^\
\ /     ASCII RIBBON CAMPAIGN
 X        AGAINST HTML MAIL
/ \




reply via email to

[Prev in Thread] Current Thread [Next in Thread]