#!/usr/bin/env ruby
# $Id: iso9660.rb,v 1.12 2007/10/13 23:00:18 rocky Exp $
#
#    Copyright (C) 2006 Rocky Bernstein <rocky@gnu.org>
#
#    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 of the License, 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., 51 Franklin Street, Fifth Floor, Boston, MA
#    02110-1301 USA.
#
# Author::    Rocky Bernstein  (mailto:rocky@gnu.org)
#
# = iso9660
# Module for ISO 9660 handling
# == Version
# :include:VERSION
#
# ==SYNOPSIS
# 
# This encapsulates IS9660 filesystem handling. This library however
# needs to be used in conjunction with classes Device
# ISO9660::IFS and ISO9660::FS.
# 
#     require "iso9660"
#     name = ISO9660::name_translate('COPYING.;1')
#     bool = ISO9660::is_achar('A')
# 
# == DESCRIPTION
# 
# This is an Ruby interface to the GNU CD Input and
# Control library's ISO 9660 library, <tt>libiso9660</tt>. 
# 
# Encapsulation is done in two parts. The lower-level Ruby interface is
# called Rubyiso9660 and is generated by SWIG.
# 
# The more object-oriented package ISO9660 and uses
# Rubyiso9660. 
# 
# Although Rubyiso9660 is perfectly usable on its own, it is expected
# that these module and classes are what most people will use. As
# Rubyiso9660 more closely models the C interface, it is conceivable (if
# unlikely) that die-hard libiso9660 C users who are very familiar with
# that interface could prefer that.


require "cdio"
require "rubyiso9660"

# General device or driver exceptions
class DeviceException < Exception
end

class ISO9660

  # = ISO 9660 Filesystem image reading
  # == SYNOPSIS
  # 
  # This encapsulates ISO 9660 filesystem Image handling. The class is
  # often used in conjunction with ISO9660.
  # 
  #     require "cdio"
  #     require "iso9660"
  # 
  #     iso = ISO9660::IFS::new('copying.iso')
  #     id =  iso.get_application_id()
  #     file_stats = iso.readdir($path)
  #     for stat in file_stats
  #        filename = stat["filename"]
  #        lsn      = stat["lsn"]
  #        size     = stat["size"]
  #        sec_size = stat["secsize"]
  #        is_dir   = stat["type"] == 2 ? 'd' : '-'
  #        puts "%s [LSN %6d] %8d %s%s" % [is_dir, lsn, size, path,
  #                                        name_translate(filename)]
  #     end
  # 
  # == DESCRIPTION
  # 
  # This is an Ruby interface to the GNU CD Input and Control library
  # (libcdio) which is written in C. This class handles ISO 9660
  # aspects of an ISO 9600 image. An ISO 9660 image is distinct from a
  # CD or a CD iamge in that the latter contains other CD-like
  # information (e.g. tracks, information or assocated with the
  # CD). See also ISO9660::FS for working with a CD or CD image.


  class IFS

    # Create a new ISO 9660 object.  If source is given, open()
    # is called using that and the optional iso_mask parameter;
    # iso_mask is used only if source is specified.  If source is
    # given but opening fails, nil is returned.  If source is not
    # given, an object is always returned.
    def initialize(source=nil, iso_mask=Rubyiso9660::EXTENSION_NONE)
      @iso9660 = nil
      if source
        open(source, iso_mask)
      end
    end

    # Returns: bool
    # 
    # Close previously opened ISO 9660 image and free resources
    # associated with ISO9660.  Call this when done using using
    # an ISO 9660 image.
    def close()
      if @iso9660
        Rubyiso9660::close(@iso9660)
      else
        puts "***No object to close"
      end
      @iso9660 = nil
    end

    # Returns: [stat_href]
    # 
    # Find the filesystem entry that contains LSN and return
    # file stat information about it. nil is returned on
    # error.
    def find_lsn(lsn)

      if Rubycdio::VERSION_NUM <= 76
        puts "*** Routine available only in libcdio versions >= 0.76"
        return nil
      end
      
      return Rubyiso9660::ifs_find_lsn(@iso9660, lsn)
    end

    # Returns: String (id)
    #    
    # Get the application ID stored in the Primary Volume Descriptor.
    # nil is returned if there is some problem.
    def application_id()
      return Rubyiso9660::ifs_get_application_id(@iso9660)
    end

    # Returns: String (id)
    # 
    # Get the preparer ID stored in the Primary Volume Descriptor.
    # nil is returned if there is some problem.
    def preparer_id()
      return Rubyiso9660::ifs_get_preparer_id(@iso9660)
    end
    
    # Returns: String (id)
    #    
    # Get the publisher ID stored in the Primary Volume Descriptor.
    # nil is returned if there is some problem.
    def publisher_id()
      return Rubyiso9660::ifs_get_publisher_id(@iso9660)
    end
    
    # Returns: Fixnum (lsn)
    #    
    #    Get the Root LSN stored in the Primary Volume Descriptor.
    #    nil is returned if there is some problem.
    def root_lsn()
      return Rubyiso9660::ifs_get_root_lsn(@iso9660)
    end
    
    # Returns: String (id)
    #    
    # Get the Volume ID stored in the Primary Volume Descriptor.
    # nil is returned if there is some problem.
    #
    def system_id()
      return Rubyiso9660::ifs_get_system_id(@iso9660)
    end

    # Returns; String (id)
    #    
    # Get the Volume ID stored in the Primary Volume Descriptor.
    # nil is returned if there is some problem.
    def volume_id()
      return Rubyiso9660::ifs_get_volume_id(@iso9660)
    end
    
    # Returns: String (id)
    #    
    # Get the Volume ID stored in the Primary Volume Descriptor.
    # nil is returned if there is some problem.
    def volumeset_id()
      return Rubyiso9660::ifs_get_volumeset_id(@iso9660)
    end
    
    # Returns: bool
    #
    # Return true if we have an ISO9660 image open.
    #
    def open?()
      return @iso9660 != nil
    end

    # Open an ISO 9660 image for reading. Subsequent operations
    # will read from this ISO 9660 image.
    #    
    # This should be called before using any other routine
    # except possibly new. It is implicitly called when a new is
    # done specifying a source.
    #    
    # If device object was previously opened it is closed first.
    #    
    # See also open_fuzzy.
    def open(source, iso_mask=Rubyiso9660::EXTENSION_NONE)
      if @iso9660 != nil then close() end

      @iso9660 = Rubyiso9660::open_ext(source, iso_mask)
      return @iso9660 !=  nil
    end

    # Open an ISO 9660 image for reading. Subsequent operations
    # will read from this ISO 9660 image. Some tolerence allowed
    # for positioning the ISO9660 image. We scan for
    # Rubyiso9660::STANDARD_ID and use that to set the eventual
    # offset to adjust by (as long as that is <= fuzz).
    #    
    # This should be called before using any other routine
    # except possibly new (which must be called first. It is
    # implicitly called when a new is done specifying a source.
    #    
    # See also open.

    def open_fuzzy(source, iso_mask=Rubyiso9660::EXTENSION_NONE,
                   fuzz=20)
      if @iso9660 != nil then close()  end
      
      if fuzz.class  != Fixnum
        puts "*** Expecting fuzz to be an integer; got 'fuzz'"
        return false
      end
      
      @iso9660 = Rubyiso9660::open_fuzzy_ext(source, iso_mask, fuzz)
      return @iso9660
    end
    
    # Read the Super block of an ISO 9660 image but determine
    # framesize and datastart and a possible additional
    # offset. Generally here we are not reading an ISO 9660 image
    # but a CD-Image which contains an ISO 9660 filesystem.
    def read_fuzzy_superblock(iso_mask=Rubyiso9660::EXTENSION_NONE,
                              fuzz=20)
      if fuzz.class != Fixnum
        puts "*** Expecting fuzz to be an integer; got 'fuzz'"
        return false
      end
      
      return Rubyiso9660::ifs_fuzzy_read_superblock(@iso9660, 
                                                    iso_mask,
                                                    fuzz)
    end
    
    # Read path (a directory) and return a list of iso9660 stat
    # references
    #    
    # Each item of @iso_stat is a hash which contains
    #
    #    * lsn       - the Logical sector number (an integer)
    #    * size      - the total size of the file in bytes
    #    * secsize   - the number of sectors allocated
    #    * filename  - the file name of the statbuf entry
    def readdir(dirname)
      #---
      #  FIXME: If you look at iso9660.h you'll see more fields,
      #  such as for Rock-Ridge specific fields or XA specific
      #  fields. Eventually these will be added. Volunteers?
      #+++
      return Rubyiso9660::ifs_readdir(@iso9660, dirname)
    end


    # Returns: pvd
    # 
    # Read the Super block of an ISO 9660 image. This is the
    # Primary Volume Descriptor (PVD) and perhaps a Supplemental
    # Volume Descriptor if (Joliet) extensions are
    # acceptable.
    def read_pvd()
      return Rubyiso9660::ifs_read_pvd(@iso9660)
    end

    # Returns: bool
    # 
    # Read the Super block of an ISO 9660 image. This is the
    # Primary Volume Descriptor (PVD) and perhaps a Supplemental
    # Volume Descriptor if (Joliet) extensions are
    # acceptable.
    def read_superblock(iso_mask=Rubyiso9660::EXTENSION_NONE)
      
      return Rubyiso9660::ifs_read_superblock(@iso9660, iso_mask)
    end

    # Returns; [size, str]
    #
    # Seek to a position and then read n blocks. A block is
    # Rubycdio::ISO_BLOCKSIZE (2048) bytes. The Size in BYTES (not blocks)
    # is returned.
    def seek_read(start, size=1)
      size *= Rubyiso9660::ISO_BLOCKSIZE
      return Rubyiso9660::seek_read(@iso9660, start, size)
    end

    # Returns: {stat}
    #    
    # Return file status for path name psz_path. nil is returned on
    # error.  If translate is true, version numbers in the ISO 9660
    # name are dropped, i.e. ;1 is removed and if level 1 ISO-9660
    # names are lowercased.
    #    
    # Each item of the return is a hash reference which contains:
    #    
    # * lsn      - the Logical sector number (an integer)
    # * size     - the total size of the file in bytes
    # * sec_size - the number of sectors allocated
    # * filename - the file name of the statbuf entry
    def stat(path, translate=false)
      
      if translate
        values = Rubyiso9660::ifs_stat_translate(@iso9660, path)
      else
        values = Rubyiso9660::ifs_stat(@iso9660, path)
      end
      return values
    end
  end # IFS

  # = ISO 9660 Filesystem reading
  # == SYNOPSIS
  # 
  # This encapsulates ISO-9660 Filesystem aspects of CD Tracks. 
  # As such this is a This library
  # however needs to be used in conjunction with ISO9660.
  # 
  #     require "iso9660"
  #     cd = ISO9660::FS::new('/dev/cdrom')
  #     statbuf = cd.stat("filename")
  # 
  #     blocks = (statbuf['size'].to_f / Rubycdio::ISO_BLOCKSIZE).ceil()
  #     for i in 0.. block - 1
  #         lsn = statbuf['lsn'] + i
  #         size, buf = cd.read_data_blocks(lsn)
  #         puts buf  
  #     end
  # 
  # == DESCRIPTION
  # 
  # This is an Object-Oriented interface to the GNU CD Input and Control
  # library (libcdio) which is written in C. This class handles ISO
  # 9660 aspects of a tracks from a CD in a CD-ROM or as a track of a CD
  # image. A CD image is distinct from an ISO 9660 image in that a CD
  # image contains other CD-line information (e.g. tracks, information or
  # assocated with the CD). See also ISO9660::IFS for working with an 
  # ISO 9660 image.

  class FS < Device

    # find_lsn(lsn)->[stat_href]
    # 
    # Find the filesystem entry that contains LSN and return
    # file stat information about it. nil is returned on
    # error.
    def find_lsn(lsn)
      return Rubyiso9660::fs_find_lsn(@cd, lsn)
    end
    
    
    # Read path (a directory) and return a list of iso9660 stat
    # references
    #    
    #  Each item of a hash which contains
    #
    #  * lsn       - the Logical sector number (an integer)
    #  * size      - the total size of the file in bytes
    #  * sec_size  - the number of sectors allocated
    #  * filename  - the file name of the statbuf entry
    #  * is_dir    - 2 if a directory; 0 if a not;
    #    
    #  FIXME: If you look at iso9660.h you'll see more fields, such as for
    #  Rock-Ridge specific fields or XA specific fields. Eventually these
    #  will be added. Volunteers?
    
    def readdir(dirname)
      return Rubyiso9660::fs_readdir(@cd, dirname)
    end

    # Returns: pvd
    #    
    # Read the Super block of an ISO 9660 image. This is the
    # Primary Volume Descriptor (PVD) and perhaps a Supplemental
    # Volume Descriptor if (Joliet) extensions are
    # acceptable.
    def read_pvd()
      return Rubyiso9660::fs_read_pvd(@cd)
    end # read_pvd

    # read_superblock(iso_mask=Rubyiso9660::EXTENSION_NONE)->bool
    #    
    # Read the Super block of an ISO 9660 image. This is the
    # Primary Volume Descriptor (PVD) and perhaps a Supplemental
    # Volume Descriptor if (Joliet) extensions are
    # acceptable.
    def read_superblock(iso_mask=Rubyiso9660::EXTENSION_NONE)
      return Rubyiso9660::fs_read_superblock(@cd, iso_mask)
    end # read_superblock

    # Returns: {stat}
    #    
    # Return file status for path name psz_path. nil is returned on
    # error.  If translate is true, version numbers in the ISO 9660
    # name are dropped, i.e. ;1 is removed and if level 1 ISO-9660
    # names are lowercased.
    #    
    # Each item of the return is a hash reference which contains:
    #    
    #  * lsn      - the Logical sector number (an integer)
    #  * size     - the total size of the file in bytes
    #  * secsize  - the number of sectors allocated
    #  * filename - the file name of the statbuf entry
    #  * is_dir   - true if a directory; false if a not.
    def stat(path, translate=false)
      if translate
        value = Rubyiso9660::fs_stat_translate(@cd, path)
      else
        value = Rubyiso9660::fs_stat(@cd, path)
      end
      return value
    end # stat
  end # class FS
end # class ISO9660

def ISO9660.check_types()
  return {
    :nocheck   => Rubyiso9660::NOCHECK,
    :"7bit"    => Rubyiso9660::SEVEN_BIT,
    :achars    => Rubyiso9660::ACHARS,
    :dchars    => Rubyiso9660::DCHARS
  }
end

# Returns: bool
#
# Check that path is a valid ISO-9660 directory name.
#
# A valid directory name should not start out with a slash (/), 
# dot (.) or null byte, should be less than 37 characters long, 
# have no more than 8 characters in a directory component 
# which is separated by a /, and consist of only DCHARs. 
#
# true is returned if path is valid.
def ISO9660.dirname_valid?(path)
  return Rubyiso9660::dirname_valid?(path)
end

# Returns: bool
#
# Return 1 if achar is an ISO-9660 ACHAR. achar should either be a string of 
# length one or the ord() of a string of length 1.
# 
# These are the DCHAR's plus some ASCII symbols including the space 
# symbol.
def ISO9660.achar?(achar)
  if achar.class == Fixnum
    # Is an integer. Is it too large?
    if achar > 255 then return false end
  elsif achar.class == String and achar.length() == 1
    achar = achar[0]
  else
    return false
  end
  return Rubyiso9660::achar?(achar)
end

# Returns: bool
# 
# Return 1 if dchar is a ISO-9660 DCHAR - a character that can appear in an an
# ISO-9600 level 1 directory name. These are the ASCII capital
# letters A-Z, the digits 0-9 and an underscore.
# 
# dchar should either be a string of length one or the ord() of a string
# of length 1.
def ISO9660.dchar?(dchar)
  if dchar.class == Fixnum
    # Is an integer. Is it too large?
    if dchar > 255 then return false end
    # Not integer. Should be a string of length one then.
    # We'll turn it into an integer.
  elsif
    dchar = dchar[0]
  else
    return false
  end
  return Rubyiso9660::dchar?(dchar)
end

# Returns: bool
# 
# Check that path is a valid ISO-9660 pathname.  
# 
# A valid pathname contains a valid directory name, if one appears and
# the filename portion should be no more than 8 characters for the
# file prefix and 3 characters in the extension (or portion after a
# dot). There should be exactly one dot somewhere in the filename
# portion and the filename should be composed of only DCHARs.
#   
# true is returned if path is valid.
def ISO9660.pathname_valid?(path)
  return Rubyiso9660::pathname_valid?(path)
end

# Take path and a version number and turn that into a ISO-9660
# pathname.  (That's just the pathname followed by ';' and the version
# number. For example, mydir/file.ext -> MYDIR/FILE.EXT;1 for version 1.
# The resulting ISO-9660 pathname is returned.
def ISO9660.pathname_isofy(path, version=1)
  return Rubyiso9660::pathname_isofy(path, version)
end

# Returns: String
# 
# Convert an ISO-9660 file name of the kind that is that stored in a ISO
# 9660 directory entry into what's usually listed as the file name in a
# listing.  Lowercase name if no Joliet Extension interpretation. Remove
# trailing ;1's or .;1's and turn the other ;'s into version numbers.
# 
# If joliet_level is not given it is 0 which means use no Joliet
# Extensions. Otherwise use the specified the Joliet level. 
# 
# The translated string is returned and it will be larger than the input
# filename.
def ISO9660.name_translate(filename, joliet_level=0)
  return Rubyiso9660::name_translate_ext(filename, joliet_level)
end

#---
# FIXME: should be
# def stat_array_to_dict(*args):
# Probably have a SWIG error.
#+++

# Returns: String
#
# Pad string 'name' with spaces to size len and return this. If 'len' is
# less than the length of 'src', the return value will be truncated to
# the first len characters of 'name'.
# 
# 'name' can also be scanned to see if it contains only ACHARs, DCHARs,
# or 7-bit ASCII chars, and this is specified via the 'check' parameter. 
# If the I<check> parameter is given it must be one of the 'nocheck',
# '7bit', 'achars' or 'dchars'. Case is not significant.
def ISO9660.strncpy_pad(name, len, check)
  if not ISO9660.check_types().member?(check)
    puts "*** A CHECK parameter must be one of %s\n" % ISO9660.check_types.keys().join(',')
    return nil
  end
  return Rubyiso9660::strncpy_pad(name, len, ISO9660.check_types()[check])
end
