#!/usr/bin/perl
#
# Samba Printing Packages - Construction Utility
#
# Copyright (C) 2000 VA Linux Systems, Inc. All rights reserved.
#
# This utility takes a vendor's printer driver and turns it into a nice form
# for installation into a Samba server.

use strict;

use FindBin;
use lib "$FindBin::RealBin/../lib/perl5";

use Carp;
use Getopt::Long;
use File::Basename;
use File::Copy;
use File::Path;
use POSIX qw(tmpnam);

use INF qw(read_inf inf_get_key inf_get_keys inf_has_key);
use inf_nt qw(parse_inf_nt get_models);
use MSExpand;

sub usage
{
   print "usage: $0 <options> SPEC ...\n";
   print "       --name=NAME         Name of the package\n";
   print "       --version=VERSION   Version of the package\n";
   print "       --display-name=DN   Pretty name for GUI tools, etc.\n";
   print "       SPEC                Either a work directory or archive.\n";
   print "       --arch=ARCH         Override architecture: win95, win98, nt, W32X86,\n";
   print "                           nt-mips, mips, W32mips, nt-alpha, alpha, W32alpha,\n";
   print "                           nt-ppc, ppc, W32ppc\n";
   print "       --inf=INF_FILENAME  Override name of INF file\n";
   print "       -m MANUFACTURER     Override manufacturer name in INF\n";
   print "       --model=MODEL       Override model name in devices section\n";
   print "       --debug             Display debugging messages.\n";
   print "\n";
   print "hypothetical example:\n";
   print "$0 --name=\"hp4050\" --version=0.01\n";
   print "  --display-name=\"HP LaserJet 4050\" /tmp/pkg-work-dir\n";
   exit(1);
}

# Convert an architecture name to a subdirectory name.
sub arch2subdir
{
   croak("arch2subdir(ARCH)") if @_ != 1;
   my($arch) = @_;
   $arch =~ tr/A-Z/a-z/;

   if ($arch eq 'nt' or $arch eq 'w32x86') {
       return "W32X86";
   } elsif ($arch eq 'win95' or $arch eq 'win98'or $arch eq 'win40') {
       return "WIN40";
   } elsif ($arch eq 'nt-mips' or $arch eq 'mips' or $arch eq 'w32mips') {
       return "W32mips";
   } elsif ($arch eq 'nt-alpha' or $arch eq 'alpha' or $arch eq 'w32alpha') {
       return "W32alpha";
   } elsif ($arch eq 'nt-ppc' or $arch eq 'ppc' or $arch eq 'w32ppc') {
       return "W32ppc";
   } else {
       return undef;
   }
}

# Convert a short architecture name to a long name.
sub arch2longname
{
   croak("arch2longname(ARCH)") if @_ != 1;
   my($arch) = @_;
   $arch =~ tr/A-Z/a-z/;

   if ($arch eq 'w32x86') {
       return "Windows NT X86";
   } elsif ($arch eq 'win40') {
       return "Windows 95/98";
   } elsif ($arch eq 'w32alpha') {
       return "Windows NT Alpha";
   } elsif ($arch eq 'w32mips') {
       return "Windows NT MIPS";
   } elsif ($arch eq 'w32ppc') {
       return "Windows NT PowerPC";
   } else {
       return undef;
   }
}

# Return the contents of a directory as an array.
sub get_files_in_dir
{
   croak("get_files_in_dir(DIR)") if @_ != 1;
   my($dir) = @_;
   my @files = ();
   local(*DIR);

   if (opendir(DIR, $dir)) {
       my $fname;

       while (defined($fname = readdir(DIR))) {
           next if $fname eq '.' or $fname eq '..';
           push(@files, $fname);
       }
       closedir(DIR);
   }

   return @files;
}

# Scan an array of filenames for a filename matching the passed filename
# while ignoring case. (Solves problem with case-sensitivity on UNIX.)
sub get_real_filename
{
   croak("get_real_filename(FILENAME,FILE1,FILE2,...)") if @_ <= 1;
   my $lookup_fname = shift @_;

   # Make the filename we are searching for be in lower-case.
   $lookup_fname =~ tr/A-Z/a-z/;

   foreach my $fname (@_) {
       my $lc_fname = $fname;
       $lc_fname =~ tr/A-Z/a-z/;

       return $fname if $lookup_fname eq $lc_fname;
   }

   return undef;
}

# Create a temporary directory and extract all files in the provided ZIP
# file into the directory. The subdirectory structure of the ZIP file is
# not replicated. Return the temporary directory.
sub unpack_zip
{
   croak("unpack_zip(ZIP_FILENAME)") if @_ != 1;
   my($zip_fname) = @_;

   # Create the temporary directory.
   my $tempdir = tmpnam();
   if (! mkdir($tempdir, 0700)) {
       print STDERR "$0: unable to make temporary subdirectory: $!\n";
       return undef;
   }

   # Use the unzip utility to do all the hard work. Hope it's in the PATH ...
   system("unzip -qq -o -j $zip_fname -d $tempdir");

   return $tempdir;
}

# Return the passed filename with any extension replaced with a underscore.
sub add_trailing_underscore
{
   croak("add_trailing_underscore(FILENAME)") if @_ != 1;
   my($fname) = @_;

   my($name, $path, $suffix) = fileparse($fname, '\..*');
   substr($suffix, length($suffix) - 1, 1) = "_";
   return $name . $suffix;
}

my($pkg_name, $pkg_version, $display_name, $default_arch, $default_inf_fname,
  $default_manufacturer, $default_model, @SPECS, $debug);

# All command line arguments that did not fit into
# getopt (that is normally the driver.exe, driver.zip or
# the driver directory are the args of this function
sub process
{
   my($arg) = @_;
   my $hash = {
       'location' => $arg,
   };

       push(@SPECS, $hash);
}

Getopt::Long::Configure("bundling");
Getopt::Long::Configure("permute");
GetOptions("name|n=s" => \$pkg_name,
          "version|v=s" => \$pkg_version,
          "display-name|N=s" => \$display_name,
          "inf-fname|inf|i=s" => \$default_inf_fname,
          "architecture|arch|a=s" => \$default_arch,
          "manufacturer|m=s" => \$default_manufacturer,
          "model|d=s" => \$default_model,
          "debug" => \$debug,
          "<>" => \&process);

usage() unless $pkg_name ne '' and $pkg_version ne '' and $display_name ne '';

# Some debugging
print "With the command line options you have configured this:\n";
print "pkg_name               = \"$pkg_name\"\n";
print "pkg_version            = \"$pkg_version\"\n";
print "display_name           = \"$display_name\"\n";
print "default_inf_fname      = \"$default_inf_fname\"\n";
print "default_arch           = \"$default_arch\"\n";
print "default_manufacturer   = \"$default_manufacturer\"\n";
print "default_model          = \"$default_model\"\n";
print "debug                  = \"$debug\"\n\n";


# Complain if no driver spec arguments were given.
if (scalar(@SPECS) == 0) {
   print STDERR "$0: no driver specifications found\n";
   usage();
}

# insert the default parameter here
foreach my $hash (@SPECS) {
if ($default_inf_fname ne '') {
       $hash->{'inf_fname'} = $default_inf_fname;
       $default_inf_fname = "";
   }

if ($default_arch ne '') {
       $hash->{'arch'} = $default_arch;
       $default_arch = "";
   }

if ($default_manufacturer ne '') {
       $hash->{'manufacturer'} = $default_manufacturer;
       $default_manufacturer = "";
   }

if ($default_model ne '') {
       $hash->{'model'} = $default_model;
       $default_model = "";
   }
}

# Some Debugging
foreach my $schluessel (@SPECS) {
 print "$schluessel  =  \%SPECS{$schluessel}\n";
}

# Create and move into a subdirectory to use to build the package.
my $pkgdir = "/tmp/printpkg-$$-${pkg_name}-$pkg_version";
if (! mkdir($pkgdir, 0755)) {
   print STDERR "$0: unable to make temporary subdirectory: $!\n";
   exit(1);
}

# Install an exit handler to remove the package directory when done.
my @byebye_dirs = ($pkgdir);
END { foreach my $path (@byebye_dirs) { rmtree($path, 0, 0); } }

# Build the first part of control file as just text in a variable for now.
my $control_text = "[common]\n";
$control_text .= "name=${pkg_name}\n";
$control_text .= "version=${pkg_version}\n";
$control_text .= "display_name=\"${display_name}\"\n";

# Build the list of files for the package.
my @pkg_files = ("control");

# Announce the package that is being built.
print "Package: ${pkg_name}-${pkg_version}.tar.gz\n";
print "Display Name: ${display_name}\n";

# Loop for each driver spec provided by the user.
foreach my $spec (@SPECS) {
   my($workdir, $section, $inf_fname, $real_inf_fname, $manufacturer, $model);

   # Print a blank line to seperate from header and other archs.
   print "\n";

   # The first argument for a driver should either be a work directory
   # with all of the files unpacked or an archive file that we should
   # unpack.
   if (-d $spec->{location}) {
       # User has given us a directory. Use the value as is.
       $workdir = $spec->{location};
       print "Location: $workdir\n";
   } elsif (-f $spec->{location}) {
       # User has given us a file. Check to see what type of archive it is.
       if ($spec->{location} =~ /\.zip$/i or $spec->{location} =~ /\.exe$/i) {
           $workdir = unpack_zip($spec->{location});
           if (not defined $workdir) {
               print "ERROR: Unable to unpack archive, skipping ...\n";
               next;
           }

           # Add this directory to list of directories to be removed.
           push(@byebye_dirs, $workdir);

           print "Using archive $spec->{location} for files.\n";
       } else {
           print "ERROR: Unsupported extension for archive, skipping...\n";
           next;
       }
   } else {
       print "ERROR: You must specify a work directory or archive.\n";
       exit(1);
   }

   # Scan the work directory for a list of all the files.
   my @workdir_files = get_files_in_dir($workdir);

   # Now attempt to infer the name of the .INF file.
   if ($spec->{inf_fname} ne '') {
         $inf_fname = $spec->{inf_fname};
         $real_inf_fname = get_real_filename($inf_fname, @workdir_files);
   } else {
       my @inf_files = grep { /\.inf/i } @workdir_files;
       if (scalar(@inf_files) == 1) {
           $inf_fname = $inf_files[0];
           $real_inf_fname = $inf_files[0];
           print "Using file $real_inf_fname for INF information.\n";
       } elsif (scalar(@inf_files) == 0) {
           print "ERROR: No INF file found in the provided files.\n";
           exit(1);
       } else {
           print "ERROR: Unable to infer which INF file to use.\n";
           print "ERROR: Use the -i option with one of the following files:\n";
           foreach my $x (@inf_files) {
               print "  $x\n";
           }
           exit(1);
       }
   }

   # Parse the .INF file provided.
   my $inf = read_inf("$workdir/$real_inf_fname");
   if (not defined $inf) {
       print "ERROR: Unable to read INF file `$real_inf_fname', skipping ...\n";
       next;
   }

   # Infer the target architecture from the INF signature or use the
   # one provided on the command-line.
   if ($spec->{arch} ne '') {
       $section = arch2subdir($spec->{arch});
       if (not defined $section) {
           print "ERROR: Unknown architecture name `$spec->{arch}'.\n\n";
           usage();
       }
   } elsif (inf_get_keys($inf, "Version", "Signature") =~ /^\$Windows NT\$$/i) {
       $section = 'W32X86';
   } elsif (inf_get_keys($inf, "Version", "Signature") =~ /^\$CHICAGO\$$/i)  {
       $section = 'WIN40';
   } else {
       print "ERROR: Unable to infer target architecture, use --arch option to specify one.\n";
       exit(1);
   }

   # Announce the architecture being used.
   print "Architecture: ", arch2longname($section);
   print " (detected from INF)" if $spec->{arch} eq '';
   print "\n";

   # Check to see if the INF file's signature matches the architecture.
   if ($section eq 'W32X86' or $section eq 'W32alpha'
       or $section eq 'W32mips' or $section eq 'W32ppc') {
       if (inf_get_keys($inf, "Version", "Signature") !~ /^\$Windows NT\$$/i) {
           print "WARNING: INF signature is not a Windows NT signature.\n";
       }
   } elsif ($section eq 'WIN40') {
       if (inf_get_keys($inf, "Version", "Signature") !~ /^\$CHICAGO\$$/i) {
           print "WARNING: INF signature is not a Windows 95/98 signature.\n";
       }
   }

   # Determine the manufacturer string to use.
   if ($spec->{manufacturer} ne '') {
       $manufacturer = $spec->{manufacturer};
   } else {
       if (inf_has_key($inf, "Manufacturer")) {
           my @keys = keys %{inf_get_key($inf, "Manufacturer")};
           if (scalar(@keys) == 1) {
               $manufacturer = $keys[0];
           } elsif (scalar(@keys) == 0) {
               print "ERROR: INF file had no manufacturers listed.\n";
               exit(1);
           } else {
               print "ERROR: Unable to infer which manufacturer to use.\n";
               print "ERROR: Use the -m option to specify one of the following:\n";
               foreach my $x (@keys) {
                   print "  \"$x\"\n";
               }
               exit(1);
           }
       } else {
           print "ERROR: INF file does not have a [Manufacturer] section.\n";
           exit(1);
       }
   }

   # Announce the manufacturer being used.
   print "Manufacturer: $manufacturer";
   print " (detected from INF)" if $spec->{manufacturer} eq '';
   print "\n";

   # Determine the model string to use.
   if ($spec->{model} ne '') {
       $model = $spec->{model};
   } else {
       my @models = get_models($inf, $manufacturer);
       if (scalar(@models) == 1) {
           $model = $models[0];
       } elsif (scalar(@models) == 0) {
           print "ERROR: INF file had no models listed.\n";
           exit(1);
       } else {
           print "ERROR: Unable to infer which model to use.\n";
           print "ERROR: Use the -d option to specify one of the following:.\n";
           foreach my $x (@models) {
               print "  \"$x\"\n";
           }
           exit(1);
       }
   }

   # Announce the model being used.
   print "Model: $model";
   print " (detected from INF)" if $spec->{model} eq '';
   print "\n";

   # Parse the driver .INF file into magical Perl data structure.
   my %info = parse_inf_nt($inf, $manufacturer, $model, $section);
   exit(1) unless defined %info;

   # Create a subdirectory in the package toplevel for this architecture.
   if (! mkdir("$pkgdir/$section", 0755)) {
       print "ERROR: Unable to create subdirectory in package ($!).\n";
       exit(1);
   }

   # Copy the .INF file to the architecture subdirectory.
   if (! copy("$workdir/$real_inf_fname", "$pkgdir/$section/")) {
       print "ERROR: Unable to copy $workdir/$real_inf_fname to package ($!)\n";
       exit(1);
   }

   # Copy each of the destination files to the architecture subdirectory.
   foreach my $filehash (@{$info{CopyFiles}}) {
       my $dst_fname = $filehash->{DstFilename};
       my $src_fname = $filehash->{SrcFilename};
       my $real_dst_fname = get_real_filename($dst_fname, @workdir_files);
       my $real_src_fname = get_real_filename($src_fname, @workdir_files);
       my $file_action = 0; # 0 - copy, 1 - expand
       my $action_src = ""; # source file to expand or copy from
       my $action_dst = ""; # destination directory for copy or expanded file

       # Output a status message for the user.
       print ">>> file - `$src_fname' -> `$dst_fname'\n" if $debug;

       # Test for the existence of the destination filename in
       # the work directory.
       if (-f "$workdir/$real_dst_fname") {
           # Copy the destination file to the architecture subdirectory.
           $file_action = 0;
           $action_src = "$workdir/$real_dst_fname";
           $action_dst = "$pkgdir/$section/";
       } elsif (-f "$workdir/$real_src_fname") {
           # Check to see if the source file is compressed.
           if ($real_src_fname =~ /_$/) {
               $file_action = 1;
               $action_src = "$workdir/$real_src_fname";
               $action_dst = "$pkgdir/$section/$dst_fname";
           } else {
               $file_action = 0;
               $action_src = "$workdir/$real_src_fname";
               $action_dst = "$pkgdir/$section/$dst_fname";
               print "WARNING: Copying source file `$src_fname' to `$dst_fname'.\n";
           }
       } else {
           # Check to see if there is a source filename with a trailing _
           # which signifes compression. Some INF files have the source
           # filename specified without the _ in it. Icky spoo-tang.
           my $u_src_name = get_real_filename(add_trailing_underscore($src_fname), @workdir_files);
           my $u_dst_name = get_real_filename(add_trailing_underscore($dst_fname), @workdir_files);
           if (-f "$workdir/$u_dst_name") {
               $file_action = 1;
               $action_src = "$workdir/$u_dst_name";
               $action_dst = "$pkgdir/$section/$dst_fname";
           } elsif (-f "$workdir/$u_src_name") {
               $file_action = 1;
               $action_src = "$workdir/$u_src_name";
               $action_dst = "$pkgdir/$section/$dst_fname";
           } else {
               print "ERROR: Unable to determine what to do with this file:\n";
               print "ERROR: Source filename is `$src_fname'.\n";
               print "ERROR: Destination filename is `$dst_fname'.\n";
               exit(1);
           }
       }

       # Implement whatever action identified above.
       if ($file_action == 0) {
           # Copy file to package.
           print ">>> Copying `$action_src' to `$action_dst'.\n" if $debug;
           if (! copy($action_src, $action_dst)) {
               print "ERROR: Unable to copy `$action_src' to package.\n";
               exit(1);
           }
       } elsif ($file_action == 1) {
           # Expand the source file into the package.
           print ">>> Expanding `$action_src' to `$action_dst'.\n" if $debug;
           if (! ms_expand($action_src, $action_dst)) {
               print "ERROR: Unable to expand `$action_src' to package.\n";
               exit(1);
           }
       } else {
           print "INTERNAL ERROR: Unknown file action given.\n";
           exit(1);
       }
   }

   # Add a section to the control file for this architecture.
   $control_text .= "\n";
   $control_text .= "[$section]\n";
   $control_text .= "inf_fname=${inf_fname}\n";
   $control_text .= "manufacturer=\"$manufacturer\"\n";
   $control_text .= "model=\"$model\"\n";

   # Add list of files for package.
   push(@pkg_files, "$section/*");
}

# Write the package's control file.
if (! open(CONTROL, ">$pkgdir/control")) {
   print "ERROR: Unable to create control file in package ($!).\n";
   exit(1);
}
print CONTROL $control_text;
close(CONTROL);

# Create the the package file.
system("( cd $pkgdir/ ; tar cf - " . join(' ', @pkg_files) . " ) | gzip -9 > ${pkg_name}-${pkg_version}.tar.gz");

print "\nPackage created successfully as ${pkg_name}-${pkg_version}.tar.gz\n";