#
# $Id: laola.pl,v 0.5.1.5 1997/07/01 00:06:42 schwartz Rel $
#
# laola.pl, LAOLA filesystem. 
#
# This perl 4 library gives raw access to "Ole/Com" documents. These are 
# documents like created by Microsoft Word 6.0+ or newer Star Divisions 
# Word by using so called "Structured Storage" technology. Write access 
# still is nearly not supported, but will be done one day. This library 
# is part of LAOLA, a distribution this file should have come along with. 
# It can be found at:
#
#    http://wwwwbs.cs.tu-berlin.de/~schwartz/pmh/index.html
# or
#    http://www.cs.tu-berlin.de/~schwartz/pmh/index.html
#
# Copyright (C) 1996, 1997 Martin Schwartz 
#
#    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, you should find it at:
#
#    http://wwwwbs.cs.tu-berlin.de/~schwartz/pmh/COPYING
#
# Diese Veröffentlichung erfolgt ohne Berücksichtigung eines eventuellen
# Patentschutzes. Warennamen werden ohne Gewährleistung einer freien 
# Verwendung benutzt. ;-)
#
# Contact: schwartz@cs.tu-berlin.de
#

#
# Really important topics still MISSING until now:
#
#   - human rights and civil rights where _you_live_
#   - Reformfraktion president for Technische Universität Berlin 
#
#   - creating documents 
#   - sensible error handling...
#   - many property set things: 
#     *  documentation of variable types
#     *  code page support
#   - opening multiple documents at a time 
#   - consistant name giving, checked against MS'
#
# Please refer to the Quick Reference at Laolas home page for further 
# explanations.
#

#
# Abbreviations
# 
#    bbd    Big Block Depot
#    pps    Property Storage
#    ppset  Property Set
#    ppss   Property Set Storage
#    sb     Start Block
#    sbd    Small Block Depot
#    tss    Time Stamp Seconds
#    tsd    Time Stamp Days
# 

##
## "public" 
##

sub laola_open_document        { &laola'laola_open_document; }
sub laola_close_document       { &laola'laola_close_document; }

sub laola_pps_get_name         { &laola'laola_pps_get_name; }
sub laola_pps_get_date         { &laola'laola_pps_get_date; }

sub laola_is_directory         { &laola'laola_is_directory; }
sub laola_get_directory        { &laola'laola_get_directory; }
sub laola_get_dirhandles       { &laola'laola_get_dirhandles; }

sub laola_is_file              { &laola'laola_is_file; }
sub laola_get_filesize         { &laola'laola_get_filesize; }
sub laola_get_file             { &laola'laola_get_file; }

sub laola_is_root              { &laola'laola_is_root; }

#
# writing 
#
sub laola_modify_file          { &laola'laola_modify_file; }

#
# property set handling
#
sub laola_is_file_ppset        { &laola'laola_is_file_ppset; }
sub laola_ppset_get_dictionary { &laola'laola_ppset_get_dictionary; }
sub laola_ppset_get_idset      { &laola'laola_ppset_get_idset; }
sub laola_ppset_get_property   { &laola'laola_ppset_get_property; }

#
# trash handling
#
sub laola_get_trashsize        { &laola'laola_get_trashsize; }
sub laola_get_trash            { &laola'laola_get_trash; }
sub laola_modify_trash         { &laola'laola_modify_trash; }


package laola;

$laola_date = "03/25/97";

changable_options: {
   $optional_do_iobuf=0;  # 0: don't cache  1: cache whole compound document 
   $optional_do_debug=0;  # 0: don't debug  1: print some debugging information
}

##
## File and directory handling
##

sub laola_open_document { ##
#
# "ok"||$error = laola_open_document($filename [,$openmode [,$streambuf]]);
#
# openmode bitmask (0 is default):
#
# Bit 0:  0 read only   1 read and write
# Bit 4:  0 file mode   1 buffer mode 
#
   local($status)="";
   open_doc1: {
      &init_vars();
      if ( ($status=&init_io(@_)) ne "ok") {
         last;
      }
      if ( ($status=&init_doc()) ne "ok") {
         &laola_close_document();
         last;
      }
      return "ok";
   }
   $status;
}

sub laola_close_document { ##
#
# "ok" = laola_close_document([streambuf])
#
   if ($openmode & 0x10) {
      if (defined $_[0]) {
         $_[0]=$iobuf;
      }
   } else {
      &flush_cache();
      &clean_file();
   }
   &init_vars();
   return "ok";
}

sub laola_is_directory { ##
#
# 1||0 = laola_is_directory($pps)
#
   local($pps)=shift;
   (!$pps || ($pps_type[$pps] == 1));
}

sub laola_is_file { ##
#
# 1||0 = laola_is_file($pps)
#
   ($pps_type[shift] == 2);
}

sub laola_is_root { ##
#
# 1||0 = laola_is_root($pps)
#
   ($pps_type[shift] == 5);
}

sub laola_get_dirhandles { ##
#
# @pps = laola_get_dirhandles($pps);
#
   local($start)=shift;

   local (@chain) = ();
   local (%chaincontrol) = ();

   (!$start || &laola_is_directory($start))
      && &get_ppss_chain($pps_dir[$start])
   ;

   @chain;
}

sub laola_get_directory { ##
#
# %pps_names = laola_get_directory($pps);
#
   local(%pps_namehandle)=();
   for (&laola_get_dirhandles) {
      $pps_namehandle{&laola_pps_get_name($_)} = $_;
   }
   %pps_namehandle;
}

sub laola_pps_get_name { ##
#
# $name_of_pps = laola_pps_get_name($pps);
#
   $pps_name[shift];
}

sub laola_pps_get_date { ##
#
# ($day,$month,$year,$hour,$min,$sec)||0 = laola_pps_get_date($pps)
# (1..31, 1..12, 1601..., 0..23, 0..59, 0.x .. 59.x)
#
   local($pps)=shift;
   &laola_is_directory($pps) 
      && &filetime_to_time($pps_ts2s[$pps], $pps_ts2d[$pps]);
}

sub laola_get_filesize { ##
#
# $filesize || 0 = laola_get_filesize($pps);
#
   local($pps)=shift;
   &laola_is_file($pps) && $pps_size[$pps];
}

sub laola_get_file { ##
#
# "ok"||$error = laola_get_file($pps, extern $buf [,$offset, $size]);
#
   &rw_file("r", @_);
}

sub laola_modify_file { ##
#
# "ok"||$error = laola_modify_file($pps,extern $buf, $offset, $size);
# 
   return "Laola: File is write protected!" if !io_writable;
   &rw_file("w", @_);
}


##
## Property set handling
##

sub laola_is_file_ppset { ##
#
# ppset_type || 0 = laola_is_file_ppset($pps)
# ppset_type e {1, 5}
#
   local($pps)=shift;
   (&laola_is_file($pps)) 
   && ( (&laola_pps_get_name($pps) =~ /^\05/) && 5
        || (&laola_pps_get_name($pps) =~ /^\01CompObj$/) && 1
   );
}

sub laola_ppset_get_dictionary { ##
#
# ("ok", %dictionary)||$error = laola_ppset_get_dictionary($pps)
#
   local($pps)=shift;
   local($status) = &load_propertyset($pps);
   if ($status ne "ok") {
      return $status;
   } else {
      return ("ok", %ppset_dictionary);
   }
}

sub laola_ppset_get_idset { ##
#
# ("ok", %ppset_idset) || $error = laola_ppset_get_idset($pps);
#
   local($pps)=shift;
   local($status) = &load_propertyset($pps);
   return $status if $status ne "ok";

   local(%ts)=();
   foreach $key (keys %ppset_fido) {
      $ts{$key} = $ppset_dictionary{$key}; 
   }
   ("ok", %ts);
}

sub laola_ppset_get_property { ##
#
# ($type,@mixed)||("error",$error)=laola_ppset_get_property($pps, $id)
#
   local($pps, $id)=@_;
   local($type, $l, $var, @var);
   local($o, $n);

   local($status)= &load_propertyset($pps);
   return ("error", $status) if $status ne "ok";

   return "" if !defined $ppset_fido{$id}; 
   $n = int($id / 0x1000);
   $o = $ppset_o[$n]+$ppset_fido{$id};

   if ($ppset_type == 5) {
      #return ("error", "Property Identifier is invalid.") if $id < 2;
      ($type, $l, @var) = &ppset_get_property($o);
      return ($type, @var);

   } elsif ($ppset_type == 1) {
      ($l, $var) = &ppset_get_var(0x1e, $o);
      return (0x1e, $var);
   }
}


##
## Trash handling
##

sub laola_get_trashsize { ##
#
# $sizeof_trash_section = laola_get_trashsize($type)
#
   &get_trash_size(@_);
}

sub laola_get_trash { ##
#
# "ok"||$error = laola_get_trash ($type, extern $buf [,$offset,$size]);
#
   &rw_trash("r", @_);
}

sub laola_modify_trash { ##
#
# "ok"||$error = laola_modify_trash ($type, extern $buf [,$offset,$size]);
# 
   return "Laola: File is write protected!" if !io_writable;
   &rw_trash("w", @_);
}


##
## "private" 
##

global_init: {
   &var_init();
   &filetime_init();
   &propertyset_type_init();
   $[=0;
}

#
# laola_open_document ->
#

sub init_vars {
   # laola_open_document->init_vars
   # laola_close_document->init_vars
   internal: {
      $infilename=undef;
      $filesize=undef;
      $openmode=undef;
      $io_writable=undef;

      $curfile=undef;
      @curfile_iolist = ();

      $iobuf=undef;
      @iobuf_modify_a=();
      @iobuf_modify_l=();
   }

   &init_propertyset(); 

   OLEstructure: {
      # unknown header things that matter:
      # ? $version=undef;       # word(1a)
      # ? $revision=undef;      # word(18)
      # ? $bigunknown=undef;    # byte(1e)

      # known header things that matter:
      $header_size=0x200;
      $big_block_size=undef;    # word(1e)
      $small_block_size=undef;  # word(20)
      $num_of_bbd_blocks=undef; # long(2c)
      $root_startblock=undef;   # long(30)
      $sbd_startblock=undef;    # long(3c)
      $ext_startblock=undef;    # long(44)
      $num_of_ext_blocks=undef; # long(48)

      # property storage things
      @pps_name=();		# 0 .. pps_sizeofname
      #pps_sizeofname=();	# word(40)
      @pps_type=();		# byte(42)
      @pps_uk0=();		# byte(43)
      @pps_prev=();		# long(44)
      @pps_next=();		# long(48)
      @pps_dir=();		# long(4c)
      @pps_ts1s=();		# long(64)
      @pps_ts1d=();		# long(68)
      @pps_ts2s=();		# long(6c)
      @pps_ts2d=();		# long(70)
      @pps_sb=();		# long(74)
      @pps_size=();		# long(78)
   }

   various: {
      $maxblock=undef;
      $maxsmallblock=undef;

      # block depot blocks
      # - these blocks are building the block depots 
      @bbd_list=();
      @sbd_list=();	  

      # block depot tables
      @bbd=();
      @sbd=();

      # contents blocks
      @root_list=();
      @sb_list=();

      blockusage: {
         @bb_usage=();       # big blocks usage  
         @sb_usage=();       # small blocks usage
         $usage_known=undef;
      }

      trash: {
         %trashsize=();
         @trash1_o=(); @trash1_l=();
         @trash2_o=(); @trash2_l=();
         @trash3_o=(); @trash3_l=();
         @trash4_o=(); @trash4_l=();
         $trash_known=undef;
      }
   }
}


sub init_io {
   ($infilename, $openmode) = @_;

   if ($openmode & 0x10) {
      return &init_stream;
   } else {
      return &init_file;
   }
}

sub init_stream {
   return "No stream data available!" if !defined $_[2];
   #$openmode &= 0xfffffffe; # clear writeable flag
   $optional_do_iobuf=1;
   $iobuf = $_[2];
   $filesize = length($iobuf);
   if ( (&read_long(0) != 0xe011cfd0) ||
        (&read_long(4) != 0xe11ab1a1) ) {
      return "\"$infilename\" is no Ole / Compound Document!\n";
   }
   "ok";
}

sub init_file {
   local($status);
   return "\"$infilename\" does not exist!"    if ! -e $infilename;
   return "\"$infilename\" is a directory!"    if -d $infilename;
   return "\"$infilename\" is no proper file!" if ! -f $infilename;
   return "Cannot read \"$infilename\"!"       if ! -r $infilename;
   if ($openmode & 1) {
      return "\"$infilename\" is write protected!" if ! -w $infilename;
      $io_writable = 1;
      $status = open(IO, '+<'.$infilename);
   } else {
      $io_writable = 0;
      $status = open(IO, $infilename);
   }
   return "Cannot open \"$infilename\"!" if !$status;

   binmode(IO); 
   if ($io_writable) {
      select(IO); $|=1; select(STDOUT);
   }

   if ( (&read_long(0) != 0xe011cfd0) ||
        (&read_long(4) != 0xe11ab1a1) ) {
      return "\"$infilename\" is no Ole / Compound Document!\n";
   }

   $filesize = -s $infilename;

   read_iobuf: {
      if ($optional_do_iobuf) {
         if (!&myread(0, $filesize, $iobuf, 0)) {
            undef $iobuf;
         } 
      }
   }

   "ok";
}

sub init_doc {
   # read bbd, 
   # get bbd -> root-chain,  get bbd -> sbd-chain
   local($i, $tmp)=(undef, undef);
   local(@tmp)=undef;

   header_information: {
      $big_block_size=1<<&read_word(0x1e);
      $small_block_size=1<<&read_word(0x20);
      $num_of_bbd_blocks=&read_long(0x2c);
      $root_startblock=&read_long(0x30);
      $sbd_startblock=&read_long(0x3c);
      $ext_startblock=&read_long(0x44);
      $num_of_ext_blocks=&read_long(0x48);
      $maxsmallblock= int ( 
        &read_long( $header_size + $root_startblock*$big_block_size + 0x78 )
           / $small_block_size 
        -1
      );
   }

   internal: {
      $maxblock = int ( ($filesize-$header_size) / $big_block_size -1);
      return "Document is corrupt - size is too small." if $maxblock < 1;
   }

   # read big block depot
   read_bbd: {
      $max_in_header = int ( ($header_size-0x4c)/4 );

      $todo = $num_of_bbd_blocks;
      $num = $todo;
      $num = $max_in_header if $num_of_bbd_blocks > $max_in_header;

      for ($i=0; $i<$num; $i++) {
         push (@bbd_list, &read_long(0x4c+4*$i));
      }
      $todo -= $num;
      $next = $ext_startblock;

      while ($todo > 0) {
         $num = $todo;
         $num = ($big_block_size-4)/4 if $todo>(($big_block_size-4)/4);
         $o = $header_size + $next*$big_block_size;
         for ($i=0; $i<$num; $i++) {
            push (@bbd_list, &read_long($o+4*$i));
         }
         $todo -= $num;
         $next = &read_long($o+4*$num);
      }

      $tmp="";
      &rw_iolist("r", $tmp, 
         &get_iolist(3, 0, 0xffffffff, 0, @bbd_list) 
      );
      @bbd = unpack ($vtype{"l"}.($maxblock+1), $tmp);
   }

   # read small block depot
   read_sbd: {
      $tmp="";
      @sbd_list=&get_list_from_depot($sbd_startblock, 1);
      &rw_iolist("r", $tmp, 
         &get_iolist(3, 0, 0xffffffff, 0, @sbd_list) 
      );
      @sbd = unpack ($vtype{"l"}.($maxsmallblock+1), $tmp);
   }

   root_and_sb_chains: {
      @root_list=&get_list_from_depot($root_startblock, 1);
      return "Document is corrupt - no root entry." if !@root_list;
      @sb_list=&get_list_from_depot (
         &read_long ( $header_size + $root_startblock*$big_block_size + 0x74 ),
         1
      );
   }

   read_PropertyStorages: {
      &read_ppss(0);

      #
      # If there are many property storages, they will be loaded 
      # dynamically. If there are few (I randomly chosed 50), they
      # all will be read (ditto for debugging).
      #
      last if $#root_list>50 || !$optional_do_debug;

      local($buf)=""; 
      local($i, $nl);
      &rw_iolist("r", $buf, 
         &get_iolist(3, 0, 0xffffffff, 0, @root_list) 
      );
      print "\n\n"
         ."---------------------------------------------\n"
         ."LAOLA INTERNAL start of debugging information\n\n"
         ." n   size    chain     typ name                    date\n"
      ;
      for ($i=0; $i<=($#root_list+1)*4; $i++) {
         &read_ppss_buf($i, $buf);
         &debug_report_pps($i) if $optional_do_debug;
      }
      print "\n"
         ."LAOLA INTERNAL end of debugging information\n"
         ."-------------------------------------------\n\n"
      ;
   }

   &report_blockuse_statistic() if $optional_do_debug;
   "ok";
}

##
## laola_close_document ->
##

sub clean_file {
   close(IO);
}


##
## -------------------------- File IO ------------------------------
## 

sub rw_file {
#
# "ok"||error = rw_file("r"||"w", $pps_handle, extern $buf [,$offset, $size])
#
   local($maxarg)=$#_;
   local($rw, $pps) = @_[0..1];
   return "Laola: pps is no file!" if !&laola_is_file($pps);
   return "Laola: no method \"$rw\"!" if !($rw =~ /^[rw]$/i);

   local($status, $offset, $size) = 
      &get_default_iosize($pps_size[$pps], $rw, @_[2..$maxarg]);
   return $status if $status ne "ok";

   return "Bad document structure!" if ! &get_curfile_iolist($pps);
   return "ok" if &rw_iolist($rw, $_[2], &get_iolist(4, $offset, $size));

   $rw =~ /^r$/i ? "Laola: read error!" : "Laola: write error!";
}

sub get_default_iosize {
#
# ("ok", $offset, $size) || $error =
#    get_default_iosize (defsize, "r"||"w", extern buf, offset, size)
#
   local($maxarg)=$#_;
   local($defsize, $rw) = @_[0..1];
   local($offset, $size) = @_[3..4];

   if (!$size) {
      if ($rw =~ /^r$/i) { 
         if ($maxarg < 4) {
            # read default: read trashsize
            $offset=0; $size=$defsize;
         } else {
            # read zero size: no problem 
            $_[2]="";
         }
      } else {
         if ($maxarg < 4) {
            # write default: not allowed!
            return "Laola: write error! Unknown size.";
         } else {
            # write zero size: no problem
         }
      }
   }
   ("ok", $offset, $size);
}

sub get_curfile_iolist {
#
# 1||0 = get_curfile_iolist($pps)
#
# Gets the iolist for the current file $pps
#
   if ($curfile) {
      return 1 if $curfile==$pps;
   }
   @curfile_iolist = &get_iolist(
      $pps_size[$pps]>=0x1000, 0, $pps_size[$pps], $pps_sb[$pps]
   );
   $curfile = $pps;
   1;
}

sub get_all_filehandles {
#
# &get_all_filehandles(starting directory)
#
# !recursive!
# Recurse over all files and directories, 
# return all file handles as @files.
#
   local($directory_pps)=shift;
   local(@dir)=&laola_get_dirhandles($directory_pps);
   local(@files)=();
   local(%filescontrol)=();

   foreach $entry (@dir) {
      if (!$filescontrol{$entry}) {
         $filescontrol{$entry} = 1;
         if (&laola_is_file($entry)) {
            push (@files, $entry)
         } elsif (&laola_is_directory($entry)) {
            push (@files, &get_all_filehandles($entry));
         }
      } else {
         print STDERR "This document is corrupt!\n";
      }
   } 
   @files;
}

##
## --------------------- Property Set Handling -------------------------
##

sub propertyset_type_init {
   %ppset_vtype = ( 
      0x00, "empty",
      0x01, "null",
      0x02, "i2",
      0x03, "i4",
      0x04, "r4",
      0x05, "r8",
      0x06, "cy",
      0x07, "date",
      0x08, "bstr",
      0x0a, "error",
      0x0b, "bool",
      0x0c, "variant",
      0x11, "ui1",
      0x12, "ui2",
      0x13, "ui4",
      0x14, "i8",
      0x15, "ui8",
      0x1e, "lpstr",
      0x1f, "lpwstr",
      0x40, "filetime",
      0x41, "blob",
      0x42, "stream",
      0x43, "storage",
      0x44, "streamed_object",
      0x45, "stored_object",
      0x46, "blobobject",
      0x48, "clsid",
      0x49, "cf",
      0xfff, "typemask",
   ); 
   local(@type) = keys %ppset_vtype;
   for (@type) {
      $ppset_vtype{$_+0x1000} = $ppset_vtype{$_}.'[]';
   }

   # \05
   %ppset_SummaryInformation = (
     2, "title", 3, "subject", 4, "authress", 5, "keywords", 
     6, "comments", 7, "template", 8, "lastauthress", 
     9, "revnumber", 10, "edittime", 11, "lastprinted", 
    12, "create_dtm_ro", 13, "lastsave_dtm", 14, "pagecount", 
    15, "wordcount", 16, "charcount", 17, "thumbnail", 
    18, "appname", 19, "security"
   );

   %ppset_DocumentSummaryInformation = (
    15, "organization"
   );

   # \01CompObj
   %ppset_CompObj = (
      0, "doc_long", 1, "doc_class", 2, "doc_spec"
   );
}


sub load_dictionary {
#
# "ok"||"done"||0 = load_dictionary($pps)
#
   local($pps)=shift;
   &load_dictionary_defaults($pps);

   local($i, $n, $o, $ps);
   local($did, $dname, $l);

   foreach $id (keys %ppset_fido_dict) {
      next if !$ppset_fido_dict{$id};

      $ps = int($id/0x1000);
      $o = $ppset_o[$ps]+$ppset_fido_dict{$id};
      $n = &get_long($o, $ppset_buf); $o+=4;

      for (; $n; $n--) {
         $did = &get_long($o, $ppset_buf); $o+=4;
         ($l, $dname) = &ppset_get_var(0x1e, $o); $o+=$l;
         $ppset_dictionary{$did+$ps*0x1000} = $dname;
      }
   }
   return "ok";
}

sub load_dictionary_defaults {
   local($name)=&laola_pps_get_name($pps);
   if ($name eq "\05SummaryInformation") {
      %ppset_dictionary = %ppset_SummaryInformation;
      return "ok";
   } elsif ($name eq "\05DocumentSummaryInformation") {
      %ppset_dictionary = %ppset_DocumentSummaryInformation;
      return "ok";
   } elsif ($name eq "\01CompObj") {
      %ppset_dictionary = %ppset_CompObj;
      return "ok";
   }
   return 0;
}

sub load_propertyset {
   local($pps)=shift;
   local($status)="";

   check_current: {
      if ($ppset_current && $pps && ($ppset_current == $pps)) {
         $status="ok"; last;
      }
      if (!&laola_is_file_ppset($pps)) {
         $status="This is not a property set handle."; last;
      }
      &init_propertyset();
      if (!&laola_get_file($pps, $ppset_buf)) {
         $status="Cannot load property set.";
      }
      $ppset_type = &laola_is_file_ppset($pps);
   }
   return $status if $status;

   if ($ppset_type == 5) {
      $status = &load_propertyset_05($pps);
      return $status if $status ne "ok";
   } elsif ($ppset_type == 1) {
      $status = &load_propertyset_01CompObj($pps);
      return $status if $status ne "ok";
   } else {
      return "Unknown property set!";
   }

   $status = &load_dictionary($pps);
   return $status;
}

sub init_propertyset {
   # !global! property set things

   $ppset_current=undef;      # current property storage handle
   $ppset_type=undef;         # \05, \01CompObj
   $ppset_buf=undef;          # buffer for whole property
   %ppset_fido=();            # $ppset_fido{Identifier}=Offset; 
                              #    Format Pairs of $ppset_current
   %ppset_fido_dict=();       # Dictionaries
   %ppset_fido_cp=();         # Code pages

   $ppset_codepage=undef;
   %ppset_dictionary=(); 

   structure_05: { # 05 ppsets
      # Header
      $ppset_byteorder=undef; # word (0)  {0xfffe}
      $ppset_format=undef;    # word (2)  {0}
      $ppset_osver=undef;     # word (4)  {lbyte=version  hbyte=revision}
      $ppset_os=undef;        # word (6)  {0=win16|1=mac|2=win32)
      @ppset_clsid=();        # class identifier (8) {e.g. @0}
      $ppset_reserved=undef;  # long (18) {>=1}
   
      # FormatIDOffset
      @ppset_fmtid=();        # format identifier (1c)
      @ppset_o=();            # ppset_o[0]: long (2c)

      # PropertySectionHeader
      @ppset_size=();         # word ($ppset_o[])
      @ppset_num=();          # long ($ppset_o[]+4)
   }

   #structure_01CompObj: {  
      #$ppset_uk1=undef;       # word (0)  {0x0001}
      #$ppset_byteorder=undef; # word (2)  {0xfffe}
      #$ppset_osver=undef;     # word (4)  {lbyte=version  hbyte=revision}
      #$ppset_os=undef;        # word (6)  {0=win16|1=mac|2=win32)
                               # { ff ff ff ff  00 09 02 00  00 00 00 00 
                               #   c0 00 00 00  00 00 00 46 }
      #@ppset_o=();            # 0x1c
   #}
}

sub load_propertyset_01CompObj {
   local($pps)=shift;
   set_current: {
      $ppset_current = $pps;
      get_structure: {
         $ppset_byteorder = &get_word(0x02, $ppset_buf);
         $ppset_osver =     &get_word(0x04, $ppset_buf);
         $ppset_os =        &get_word(0x06, $ppset_buf);
         @ppset_o =         (0x1c);
      }
      check_structure: {
         if ($ppset_byteorder !=0xfffe) {
            return "Cannot understand property set.";
         } 
      }
   }
   get_offsets: {
      local($i);
      local($offset, $length)=(0, 0);
      for ($i=0; $i<3; $i++) {
         $length = &get_long($ppset_o[0] + $offset, $ppset_buf);
         last if !$length;
         $ppset_fido{$i} = $offset;
         $offset = $offset + 4 + $length;
      }
   }
   "ok";
}

sub load_propertyset_05 {
   local($pps)=shift;
   set_current: {
      $ppset_current = $pps;
      get_structure: {
         ($ppset_byteorder, $ppset_format, $ppset_osver, $ppset_os) =
            &get_nword(4, 0, $ppset_buf)
         ;
         @ppset_clsid =     &get_uuid(0x08, $ppset_buf);
         $ppset_reserved =  &get_long(0x18, $ppset_buf);
         @ppset_fmtid =     &get_uuid(0x1c, $ppset_buf);
         $ppset_o[0] =      &get_word(0x2c, $ppset_buf);
         $ppset_size[0] =   &get_word($ppset_o[0], $ppset_buf);
         $ppset_num[0] =    &get_word($ppset_o[0]+4, $ppset_buf);
      }
      check_structure: {
         $status="Cannot understand property set.";
         last if $ppset_byteorder != 0xfffe;
         last if $ppset_format != 0;
         last if $ppset_reserved < 1;
         last if $ppset_o[0] < 0x30;
         $status="";
      }
   }
   return $status if $status;
 
   get_ids_and_offsets: {
      local($i, $id, $n, $num, $fido);
      local($o)=$ppset_o[0];
      for ($n=0; $n<$ppset_reserved; $n++) {

         # default dictionary and codepage
         $ppset_fido_dict{$n*0x1000+0} = 0;
         $ppset_fido_cp{$n*0x1000+1} = 0x4e4;

         $num=&get_word($o+4, $ppset_buf);
         for ($i=0; $i<$num; $i++) {
            $id = &get_long($o+8+$i*8, $ppset_buf);
            if ($n) {
               $id = $i if $id>1; # ! hacky !
            }
            $fido = &get_long($o+8+$i*8+4, $ppset_buf);
            if ($id>1) {
               $ppset_fido{$n*0x1000+$id} = $fido;
            } elsif ($id==1) {
               $ppset_fido_cp{$n*0x1000+1} = $fido;
            } elsif ($id==0) {
               $ppset_fido_dict{$n*0x1000} = $fido;
            }
         }
         $o+=&get_word($o, $ppset_buf);
         $ppset_o[$n+1]=$o;
      }
   }
   # todo: code page
   "ok";
}

sub ppset_get_property {
#
# ($type, $size, @mixed)||("error", $debuginfo) = ppset_get_property($offset)
#
   local($o_begin)=$_[0];
   local($o)=$o_begin;
   local($type) = &get_long($o, $ppset_buf);

   if (! ($type & 0x1000)) {
      return ($type, &ppset_get_var($type, $o+4));
   } else {
      local(@mixed)=();
      local($n)=&get_long($o+4, $ppset_buf); $o+=8;
      local($t, $l, @var);
      for (; $n; $n--) {
         @var=();
         ($l, @var) = &ppset_get_var($type^0x1000, $o);
         push (@mixed, 1+($#var+1), $type^0x1000, @var);
         $o+=$l; 
      }
      return ($type, $o-$o_begin, @mixed);
   }
}

sub ppset_get_var {
#
# ($size, @var) = &ppset_get_var($type, $offset);
#
   local($type, $o)=@_;
   if (!$type || $type == 0x01) { # empty, null
      return (0, ""); 
   } elsif ($type == 0x02) {  # i2
      local($tmp) = &get_word($o, $ppset_buf);
      $tmp = - (($tmp^0xffff) +1) if ($tmp & 0x8000);
      return (2, $tmp);
   } elsif ($type == 0x03) {  # i4
      local($tmp) = &get_long($o, $ppset_buf);
      $tmp = - (($tmp^0xffffffff) +1) if ($tmp & 0x80000000);
      return (4, $tmp);
   } elsif ($type == 0x04) {  # real
      return (4, unpack("f", substr($ppset_buf, $o, 4)) );
   } elsif ($type == 0x05) {  # double
      return (8, unpack("d", substr($ppset_buf, $o, 8)) );
   } elsif ($type == 0x0a) {  # error
      return (4, &get_word($o, $ppset_buf)); 
   } elsif ($type == 0x0b) {  # bool (0==false, -1==true)
      return (4, &get_long($o, $ppset_buf)); 
   } elsif ($type == 0x0c) {  # variant
      local($t, $l, @var);
      $t = &get_long($o, $ppset_buf);
      ($l, @var) = &ppset_get_var($t, $o+4);
      return (4+$l, $t, @var);
   } elsif ($type == 0x11) {  # ui1
      return (1, &get_byte($o, $ppset_buf));
   } elsif ($type == 0x12) {  # ui2
      return (2, &get_word($o, $ppset_buf));
   } elsif ($type == 0x13) {  # ui4
      return (4, &get_long($o, $ppset_buf));
   } elsif ($type == 0x1e) {  # lpstr
      local($l)=&get_long($o, $ppset_buf);
      if ($l) {
         return (4+$l, substr($ppset_buf, $o+4, $l-1));
      } else {
         return (4, "");
      }
   } elsif ($type==0x40) {    # filetime
      return (8, &filetime_to_time(&get_nlong(2, $o, $ppset_buf)) );
   } else {
      return (
         "error", 
         sprintf("(offset=%x, type=%x, buf[0]=%x)",
            $o, $type, &get_long($o+4, $ppset_buf)
         )
      );
   }
}

##
## Basic laola data types
##

sub var_init {
#
# At this work I still don't trust in signed integers, therefore I 
# prefer the unsigned 0xffffffff to -1 (don't beat me)
#
   $vtype{"c"}="C"; $vsize{"c"}=1;    # unsigned char
   $vtype{"w"}="v"; $vsize{"w"}=2;    # 0xfe21     == 21 fe
   $vtype{"l"}="V"; $vsize{"l"}=4;    # 0xfe21abde == de ab 21 fe
}

sub get_chars { 
#
# get_chars ($offset, $number, extern $sourcebuf);
#
   substr($_[2], $_[0], $_[1]); 
}

sub read_chars { 
#
# read_chars ($offset, $number);
#
   local($tmp)=""; 
   &myread($_[0], $_[1], $tmp) && $tmp; 
}

# get_thing ($offset, extern $buf);
sub get_byte { &get_var("c", @_); }
sub get_word { &get_var("w", @_); }
sub get_long { &get_var("l", @_); }
sub get_var { 
   unpack ($vtype{$_[0]}, substr($_[2], $_[1], $vsize{$_[0]}));
}

# get_nthing ($n, $offset, extern $buf);
sub get_nbyte { &get_nvar("c", @_); }
sub get_nword { &get_nvar("w", @_); }
sub get_nlong { &get_nvar("l", @_); }
sub get_nvar { 
   unpack ($vtype{$_[0]}.$_[1], substr($_[3], $_[2], $vsize{$_[0]}*$_[1]));
}

# read_thing ($offset);
sub read_byte { &read_var("c", @_); }
sub read_word { &read_var("w", @_); }
sub read_long { &read_var("l", @_); }
sub read_var {
   unpack ($vtype{$_[0]}, &read_chars($_[1], $vsize{$_[0]}));
}

# read_nthing ($n, $offset);
sub read_nbyte { &read_nvar("c", @_); }
sub read_nword { &read_nvar("w", @_); }
sub read_nlong { &read_nvar("l", @_); }
sub read_nvar {
   unpack ($vtype{$_[0]}.$_[1], &read_chars($_[2], $vsize{$_[0]}*$_[1]));
}

##
## --------------------------- IO handling ------------------------------
##

sub myio {
#
# 1||0= myio("r"||"w", $file_offset, $num_of_chars, $extern_var [,$var_offset])
#
   $_ = shift;
   /^r$/i ? &myread : /^w$/i ? &mywrite : 0;
}

sub myread {
#
# 1||0 = myread($file_offset, $num_of_chars, $extern_var [,$var_offset])
#
   local($varoffset)= $_[3] || 0;
   if ($optional_do_iobuf && $iobuf) {
      substr($_[2], $varoffset, $_[1])=substr($iobuf, $_[0], $_[1]);
      return 1;
   } else {
      seek(IO, $_[0], 0) && (read(IO,$_[2],$_[1],$varoffset) == $_[1]);
   }
}

sub mywrite {
#
# 1||0 = mywrite($file_offset, $num_of_chars, $extern_var [,$var_offset])
#
   return 0 if !$io_writable;

   local($varoffset)= $_[3] || 0;
   local($tmp) = substr($_[2], $varoffset, $_[1]);
         $tmp .= "\00" x ($_[1]-length($tmp));
   if ($optional_do_iobuf && $iobuf) {
      substr($iobuf, $_[0], $_[1]) = $tmp;
      push(@iobuf_modify_a, $_[0]);
      push(@iobuf_modify_l, $_[1]);
      return 1;
   } else {
      seek(IO, $_[0], 0) && print IO $tmp;
   }
}

sub flush_cache {
#
# void = flush_cache()
#
# flush io cache, if caching is turned on
#
   return if !($optional_do_iobuf && $iobuf);

   &rw_iolist("w", $iobuf, 
      &aggregate_iolist(2, @iobuf_modify_a, @iobuf_modify_l)
   );

   @iobuf_modify_a=(); @iobuf_modify_l=();
}

##
## The "logical" core of laola
##

sub get_ppss_chain {
#
# @blocks = get_ppss_chain($ppss)
#
# !recursive!
#
   local($ppss) = @_;
   return if $ppss == 0xffffffff;

   if ($chaincontrol{$ppss}) {
      # Recursive entry! 
      @chain = ();
      print STDERR "This document is corrupt!\n";
      return;
   } else {
      &read_ppss($ppss);
      $chaincontrol{$ppss}=1;
   }

   &get_ppss_chain ( $pps_prev[$ppss] );

   push(@chain, $ppss);

   &get_ppss_chain ( $pps_next[$ppss] );
}

sub read_ppss_buf {
#
# "ok" = read_ppss_buf ($i, extern $buf)
#
   local($i)=$_[0];
   local($nl);
   return "ok" if $pps_name[$i];
   return if ! ($nl = &get_word($i*0x80+0x40, $_[1]));

   $pps_name[$i] = &pps_name_to_string($i*0x80, $nl, $_[1]);

   ($pps_type[$i], $pps_uk0[$i], 
    $pps_prev[$i], $pps_next[$i], $pps_dir[$i]) =
      unpack($vtype{"c"}."2".$vtype{"l"}."3",
             substr($_[1], $i*0x80+0x42, $vsize{"c"}*2+$vsize{"l"}*3))
   ;

   ($pps_ts1s[$i], $pps_ts1d[$i], $pps_ts2s[$i], $pps_ts2d[$i],
    $pps_sb[$i], $pps_size[$i]) = 
      &get_nlong(6, $i*0x80+0x64, $_[1])
   ;

   "ok";
}

sub read_ppss {
#
# "ok" = read_ppss ($i)
#
   local($i)=shift;
   return "ok" if $pps_name[$i];

   local($buf)="";
   &rw_iolist("r", $buf, &get_iolist(3, $i*0x80, 0x80, 0, @root_list));

   local($nl);
   return if ! ($nl = &get_word(0x40, $buf));
   $pps_name[$i] = &pps_name_to_string(0, $nl, $buf);
   ($pps_type[$i], $pps_uk0[$i], $pps_prev[$i], $pps_next[$i], $pps_dir[$i])=
      unpack($vtype{"c"}."2".$vtype{"l"}."3",
             substr($buf, 0x42, $vsize{"c"}*2+$vsize{"l"}*3)
      )
   ;

   ($pps_ts1s[$i], $pps_ts1d[$i], $pps_ts2s[$i], $pps_ts2d[$i],
    $pps_sb[$i], $pps_size[$i]) = unpack(
      $vtype{"l"}."6", substr($buf, 0x64, $vsize{"l"}*6)
   );

   "ok";
}


sub get_list_from_depot {
#
# @blocks = get_list_from_depot ($start, depottype)
#
# Read a block chain starting with block $start out of a either
# depot @bbd (for $t) or depot @sbd (for !$t).
#
   local($start, $t)=@_;
   local(@chain)=();
   return @chain if $start == 0xfffffffe;

   push (@chain, $start);
   while ( ($start = $t?$bbd[$start]:$sbd[$start]) != 0xfffffffe ) {
      push(@chain, $start);
   }
   @chain;
}

sub get_iolist {
# 
# @iolist = get_iolist ($depottype, $offset, $size, $startblock [,@depot])
#
# This is the main IO logic. Returns the iolist for a data stream according 
# to depot type $t. The stream may start at offset $offset and can have a 
# size $size. If size is bigger than the total size of the stream according 
# to its depot, it will be cut correctly. (So if you want to read until the
# files end without knowing how many bytes that are, take 0xffffffff as size).
#
# depottype $t:
#    0 small block (for @sbd)                    small block depot
#    1 big block   (for @bbd)                    big block depot
#    2 small block (for @_[4..$#])               some small blocks
#    3 big block   (for @_[4..$#])               some big blocks
#    4 variable    (for @curfile_iolist)         iolist of current file
#    5 variable    (for @_[4..$#] == (@o, @l))   some iolist
#
   local($t, $offset, $size, $sb) = (shift||0, shift||0, shift||0, shift||0);
   local($di);
   local($bs, $max);

   local(@empty)=();
   return @empty if !$size;

   local($begin, $done, $len);
   local(@o)=(); local(@l)=();

   $bs = ($t==1 || $t==3) ? $big_block_size : $small_block_size;

   if ($t<2) {
      # To skip these offsets, stream chains would have to be resolved 
      # before. 
   } elsif ($t<4) {
      $max = $#_;
      # Skip whole blocks, when offset given
      $sb += int ($offset / $bs);
      $offset -= int ($offset / $bs) * $bs;
   } elsif ($t==4) {
      $max = ($#curfile_iolist-1)/2;
   } elsif ($t==5) {
      $max = ($#_-1)/2;
   } else {
      return @empty;
   }

   $done = 0;
   for ( $di=$sb; 
         ($t<2) ? ($di!=0xfffffffe): ($di<=$max);
         $di=&next_dl
   ) {
      last if ($done == $size);
      if ($t==4) {
         $bs = $curfile_iolist[$max+1+$di];
      } elsif ($t==5) {
         $bs = $_[$max+1+$di];
      }
      if ($offset) {
         if ($bs <= $offset) {
            $offset -= $bs;
            next;
         } else {
            $begin = &depot_offset + $offset;
            $len   = $bs - $offset;
            $offset = 0;
         }
      } else {
         $begin = &depot_offset;
         $len   = $bs;
      }
      if ( ($done+$len) > $size ) {
         $len = $size - $done;
      }
      if ( !@o || ($o[$#o]+$l[$#l])!=$begin ) {
         push(@o, $begin); 
         push(@l, $len);
      } else {
         $l[$#l]+=$len;
      }
      $done += $len;
   }
   (@o, @l);
}
sub next_dl { # get_iolist:next_dl
#
# index = depot ($di==index, $t==depothandle)
#
# Returns next chain link of depot @bbd ($t) or @sbd (!$t)
#
   return $sbd[$di] if !$t;
   return $bbd[$di] if $t==1;
   $di+1;
}
sub depot_offset { # get_iolist:depot_offset
#
# offset = depot_offset ($di==index, $t==depottype)
#
   return (($sb_list[$di/8]+1)*8 + ($di%8))*$small_block_size if $t==0;
   return $header_size + $di*$big_block_size if $t==1;
   return (($sb_list[$_[$di]/8]+1)*8 + ($_[$di]%8))*$small_block_size if $t==2;
   return $header_size + $_[$di]*$big_block_size if $t==3;
   return ($curfile_iolist[$di]) if $t==4;
   return ($_[$di]) if $t==5;
}


sub aggregate_iolist {
#
# (@offsets, @lengths)||() = aggregate_iolist(method,@offsets,@lengths)
#
# method:  
#    1  @offsets shall be sorted, no overlap allowed
#    2  @offsets shall be sorted, overlap is allowed
#    3  @offsets are sorted, no overlap allowed
#    4  @offsets are sorted, overlap is allowed
#
   local($method)=shift;
   local(@empty)=();
   return @empty if ($method<1)||($method>4); # Don't know method!

   local($max)=int(($#_+1)/2);

   local($i, $j);
   local(@o_in)=(); local(@l_in)=();
   local(%o_in)=();
   local(@o_out)=(); local(@l_out)=();
   local($offset, $len);

   #
   # Sort
   #
   if ( ($method==1) || ($method==2)) {
      # sort offsets
      for ($i=0; $i<$max; $i++) {
         next if !$_[$max+$i];
         if ($o_in{$_[$i]}) {
            return @empty if $method==1; # Data chunks overlap!
            $o_in{$_[$i]}=$i if $_[$max+$i]>$o_in{$_[$i]};
         } else {
            $o_in{$_[$i]}=$i;
         }
      } 
      foreach $key (sort {$a <=> $b} keys %o_in) {
         push(@o_in, $_[$o_in{$key}]);
         push(@l_in, $_[$max + $o_in{$key}]);
      }
   } else {
      @o_in=@_[0..($max-1)];
      @l_in=@_[$max..$#_];
   }

   #
   # Aggregate
   #
   $offset=$o_in[0];
   $len=$l_in[0];

   for ($i=1; $i<=($#o_in+1); $i++) {
      if ( ($i==($#o_in+1)) 
           || ($o_in[$i]<$offset)
           || ($o_in[$i]>($offset+$len)) 
         ) {
         push(@o_out, $offset);
         push(@l_out, $len);
         $offset=$o_in[$i];
         $len=$l_in[$i];
      } elsif ($o_in[$i]<($offset+$len)) {
         return @empty if ($type==1 || $type==3); # Data chunks overlap! 
         if ( ($o_in[$i]+$l_in[$i]) > ($offset+$len) ) {
            $len=$o_in[$i]+$l_in[$i]-$offset;
         }
      } else {
         $len += $l_in[$i];
      }
   }
   (@o_out, @l_out); 
}

sub rw_iolist {
   # 
   # 1||0 = rw_iolist("r"||"w", extern buf, @offsets, @lengths);
   # . read or write global chunklist
   #
   local($done, $i, $l);
   local($max) = int(($#_-2+1)/2);

   $done=0;
   for ($i=0; $i<$max; $i++) {
      next if ! ($l = $_[2+$i+$max]);
      if (&myio($_[0], $_[2+$i], $l, $_[1], $done)) {
         $done += $l;
      } else {
         # io error!
         return 0;
      }
   }
   1;
}

##
## ---------------------- Property Set Handling --------------------------
##

sub pps_name_to_string {
#
# $string = pps_name_to_string($offset, $pps_name_len, extern $buf)
#
   local($l)=$_[1]-2;
   local($i); 
   local($tmp)="";
   for ($i=0; $i<$l; $i+=2) {
      $tmp.=substr($_[2], $_[0]+$i, 1);
   }
   $tmp;
}

sub learn_guids {
    @guids = ("dsi", "si");
    $guid_dsi="\0x5DocumentSummaryInformation";
    @guid_dsi=( 0xd5cdd502, 0x2e9c, 0x101b, 
                "\0x93\0x97\0x08\0x00\0x2b\0x2c\0xf9\0xae" );
    $guid_si="\0x5SummaryInformation";
    @guid_si=( 0xf29f85e0, 0x4ff9, 0x1068, 
                "\0xab\0x91\0x08\0x00\0x2b\0x27\0xb3\0xd9" );
}

sub get_uuid {
   local($o)=$_[0];
   ( &get_long($o, $_[1]), 
     &get_word($o+4, $_[1]), 
     &get_word($o+6, $_[1]), 
     &get_chars($o+8, 8, $_[1])
   );
}

#
# This section refers to pps_ts2 and pps_ts1, the one ore two timestamps 
# used for each "Storage" Property Set. It seems, that the second timestamp 
# gets actualized, when changing the storage. The first stamp is sometimes 
# used, sometimes unused.
#
# The stamp is a 64 bit ulong. It counts every second 10 * 10 ^ 6, 
# starting at 01/01/1601. When the 64 bit int gets evaluated as
# two 32 bit integers, the faster running ("least significant long")
# can hold just 0x100000000 / 10000000.0 (about 429.5) seconds. So the 
# slower running ("most significant long") increments every 429.5 seconds.
#

sub filetime_init {
   @monsum  = ( 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334,
                0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335 );
   $a_minute = 60 * 10000000.0 / (0x10000000 * 16);
}

sub is_schaltjahr {
   local($year)=shift;
   !($year%4) && ($year%100 || !($year%400) ) && 1;
}

sub filetime_years_to_days {
   local($year)=shift;
   int($year-1600) * 365 
     + int( ($year-1600) / 4 )
     - int( ($year-1600) / 100 )
     + int( ($year-1600) / 400 )
     ;
}

sub filetime_to_time {
   local($ds, $dd)=@_;
   local($day, $month, $year, $hour, $min, $sec);
   local($i, $m, $d, $dsum, $tmpsec);

   $dsum = $dd + ($ds / (0x10000000 * 16.0));

   $d= int( $dsum/($a_minute*60*24) )+1;
   $m= $dsum - ($d-1)*$a_minute*60*24;

   $year  = int( $d/365.2425 ) + 1601;
   $d -= &filetime_years_to_days($year-1);

   for( $i=11; $i && ($d <= $monsum[$i+&is_schaltjahr($year)*12]); $i--) {}
   $month = $i+1; 
   $day   = $d - $monsum[$i+&is_schaltjahr($year)*12];

   $hour  = int( $m / ($a_minute*60) ); 
   $min   = int( $m/$a_minute - $hour*60 );
   $sec   =    ( ($m/$a_minute - $hour*60 - $min) * 60);

   ($day, $month, $year, $hour, $min, $sec);
}

sub time_to_filetime {
   local($day, $month, $year, $hour, $min, $sec)=@_;
   local($d, $tss, $tsd);

   $d = &filetime_years_to_days($year-1) 
   + $monsum[$month-1 + &is_schaltjahr($year)*12]
   + $day-1; 

   $tsd = (24*60*$d + 60*$hour +$min +$sec/60.0) * $a_minute;

   $tss = ($tsd-int($tsd)) * 0x10000000 * 16;

   ( int($tss), int($tsd) );
}


##
## ------------------------- Trash Handling ------------------------------
##

sub make_blockuse_statistic {
   #
   # block statistic: 
   #    0 == irregular free (block depot entry != -1) (== undef)
   #    1 == regular free (block depot entry == -1)
   #    2 == used for ole system
   #    3 == used for ole application
   #
   return 1 if $usage_known;
   local($i, @list);

   # default: all small and big blocks are undef

   #
   # regular system data 
   #

   # ole system blocks
   for (@bbd_list, @sbd_list, @root_list, @sb_list) { 
      $bb_usage[$_]=2; 
   }

   # free blocks according to block depots
   for (@bbd) { 
      $bb_usage[$_]=1 if $bbd[$_]==0xffffffff; 
   }
   for (@sbd) { 
      $sb_usage[$_]=1 if $sbd[$_]==0xffffffff; 
   }

   #
   # OLE application blocks
   #
   foreach $file (&get_all_filehandles(0)) {
      if ($pps_size[$file]>=0x1000) {
         for (&get_list_from_depot($pps_sb[$file], 1)) { $bb_usage[$_]=3; }
      } else {
         for (&get_list_from_depot($pps_sb[$file], 0)) { $sb_usage[$_]=3; }
      }
   }

   $usage_known=1;
}

sub get_trash_info {
#
# void get_trash_info();
#
# Trash types:
#
#    0 == all
#    1 == unused big blocks
#    2 == unused small blocks 
#    4 == unused file space, according to sizeof pps_size (incl. root_entry)
#    8 == unused system space (header, sb_table, bb_table)
#
   return 1 if $trash_known;
   &make_blockuse_statistic();

   local(@o, @l);
   local(@list);
   local($size, $m);
   local($i);
   local($begin, $len);
 
   unused_big_blocks: {
      $size=0; @list=();
      for ($i=0; $i<=$maxblock; $i++) {
         push(@list, $i) if $bb_usage[$i]<=1;
      }
      @trash1_o = &get_iolist(3, 0, 0xfffffff, 0, @list);
      @trash1_l = splice(@trash1_o, ($#trash1_o+1)/2); 
      $m=$#trash1_o; for ($i=0; $i<=$m; $i++) { $size+=$trash1_l[$i]; }
      $trashsize{1}=$size;
   }

   unused_small_blocks: {
      $size=0; @list=();
      for ($i=0; $i<=$maxsmallblock; $i++) {
         push(@list, $i) if $sb_usage[$i]<=1;
      }
      @trash2_o = &get_iolist(2, 0, 0xfffffff, 0, @list);
      @trash2_l = splice(@trash2_o, ($#trash2_o+1)/2); 
      $m=$#trash2_o; for ($i=0; $i<=$m; $i++) { $size+=$trash2_l[$i]; }
      $trashsize{2}=$size;
   }

   unused_file_space: {
      $size=0;

      # 3.1. normal files
      foreach $file (&get_all_filehandles(0)) {
         @o = &get_iolist(
            $pps_size[$file]>=0x1000 && 1, 
            $pps_size[$file], 0xffffffff, $pps_sb[$file]
         );
         push(@trash3_l, splice(@o, ($#o+1)/2)); 
         push(@trash3_o, @o);
      }
      $m=$#trash3_o; for ($i=0; $i<=$m; $i++) { $size+=$trash3_l[$i]; }

      # 3.2. system file of root_entry (small block file)
      @list = ();
      while (($#list+$#sbd+2) % 8) {
         push(@list, $#list+$#sbd+2);
      }
      @o = &get_iolist(2, 0, 0xfffffff, 0, @list);
      @l = splice(@o, ($#o+1)/2); 
      push(@trash3_o, @o); push(@trash3_l, @l);
      $m=$#o; for ($i=0; $i<=$m; $i++) { $size+=$l[$i]; }

      $trashsize{3}=$size;
   }

   unused_system_space: {
      $size=0;

      # 4.1. header block
      $begin = 0x4c + $num_of_bbd_blocks*4;
      $len = $header_size - $begin;
      push(@trash4_o, $begin); push(@trash4_l, $len);
      $size+=$len;
   
      # 4.2. big block depot
      @o = &get_iolist(3, ($maxblock+1)*4, 0xffffffff, 0, @bbd_list);
      @l = splice(@o, ($#o+1)/2); 
      push(@trash4_o, @o); push(@trash4_l, @l);
      $m=$#o; for ($i=0; $i<=$m; $i++) { $size+=$l[$i]; }
   
      # 4.3. small block depot
      @o = &get_iolist(3, ($maxsmallblock+1)*4, 0xffffffff, 0, @sbd_list);
      @l = splice(@o, ($#o+1)/2); 
      push(@trash4_o, @o); push(@trash4_l, @l);
      $m=$#o; for ($i=0; $i<=$m; $i++) { $size+=$l[$i]; }

      $trashsize{4}=$size;
   }

   $trash_known=1;
}

sub get_trash_size {
   local($type)=shift;
   $type = (1|2|4|8) if !$type;
   &get_trash_info();

   local($trashsize)=0;
   $trashsize += $trashsize{1} if $type & 1;
   $trashsize += $trashsize{2} if $type & 2;
   $trashsize += $trashsize{3} if $type & 4;
   $trashsize += $trashsize{4} if $type & 8;

   $trashsize;
}

sub rw_trash {
#
# "ok"||error = rw_trash("r"||"w", $type, extern $buf [,$offset,$size])
#
   local($maxarg)=$#_;
   &get_trash_info();

   local($rw, $type) = @_[0..1];
   $type = (1|2|4|8) if !$type;

   local($status, $offset, $size) = 
      &get_default_iosize(&laola_get_trashsize($type), $rw, @_[2..$maxarg]);
   return $status if $status ne "ok";

   local(@o)=(); local(@l)=();
   if ($type & 1) { push (@o, @trash1_o); push (@l, @trash1_l); }
   if ($type & 2) { push (@o, @trash2_o); push (@l, @trash2_l); }
   if ($type & 4) { push (@o, @trash3_o); push (@l, @trash3_l); }
   if ($type & 8) { push (@o, @trash4_o); push (@l, @trash4_l); }

   return "ok" if &rw_iolist(
      $rw, $_[2],
      &get_iolist(5, $offset, $size, 0, &aggregate_iolist(1, @o, @l))
   );

   "Laola: IO Error!";
}


##
## ----------------------------- Debugging -------------------------------
##

#
# Some debug information. Switch it on via $optional_do_debug=1
# Information will be shown directly after opening any document.
# 

sub debug_report_pps {
   local($i)=shift;
   local($out)="";
   local($tmp, $tmp2)="";

   return if !$pps_name[$i];

   $out = sprintf ("%2x", $i); 
   $out .= $pps_uk0[$i]==1 ? ":  " : sprintf ("#%-2x", $pps_uk0[$i]);

   if (&laola_is_directory($i)) {
      $out .= "-->    ";
   } elsif (&laola_is_file($i)) {
      $out .= sprintf ("%-5x  ", 
                       &laola_get_filesize($i));
   } else {
      $out .= "       ";
   }
   
   if ($pps_prev[$i]==0xffffffff) { $out .= "  .";
      } else { $out .= sprintf ("%3x", $pps_prev[$i]); }
   if ($pps_next[$i]==0xffffffff) { $out .= "  .";
      } else { $out .= sprintf ("%3x", $pps_next[$i]); }
   if ($pps_dir[$i]==0xffffffff) { $out .= "  .";
      } else { $out .= sprintf ("%3x", $pps_dir[$i]); }

   if (&laola_is_file_ppset($i)) {
      $out .= "  set";
   } else {
      $out .= "  pp "; 
   }

   ($tmp=$pps_name[$i]) =~ s/[^_a-zA-Z0-9]/ /g;
   $out .= sprintf (" \"%s\"",$tmp); 

   $out .= " " x (50 - length($out));

   if ($pps_ts2d[$i]) {
      $out .= sprintf (" %d.%d.%d %02d.%02d:%02d", 
        &filetime_to_time($pps_ts2s[$i], $pps_ts2d[$i])
      );
   }

   print "$out\n";
}

sub report_blockuse_statistic {
   return 1;
   print "--- LAOLA internal, begin block statistic ---\n\n";
   &make_blockuse_statistic();
   local($i, $j, $m);
   local(@o, @l);
   print "Big blocks:\n";
   for ($i=0; $i<4; $i++) {
      @o=(); @l=();
      $m=$#bb_usage; for ($j=0; $j<=$m; $j++) {
         next if $bb_usage[$j]!=$i;
         push(@o, $j); push(@l, 1);
      }
      &report_blockuse_list($i, &aggregate_iolist(1, @o, @l));
   }
   print "Small blocks:\n";
   for ($i=0; $i<4; $i++) {
      @o=(); @l=();
      $m=$#sb_usage; for ($j=0; $j<=$m; $j++) {
         next if $sb_usage[$j]!=$i;
         push(@o, $j); push(@l, 1); 
      }
      &report_blockuse_list($i, &aggregate_iolist(1, @o, @l));
   }
   print "\n--- LAOLA internal, end block statistic ---\n\n";
}

sub report_blockuse_list {
   local($type)=shift;
   return if !@_;
   local(%info)=(0, "Trash", 1, "Free", 2, "System", 3, "Application");
   local($max)=($#_+1)/2;
   local($i); local($o, $l);
   print "Type $type {$info{$type}} = (";
   for ($i=0; $i<$max; $i++) {
      $o=$_[$i]; $l=$_[$max+$i];
      if ($l==1) {
         printf (" %x ", $o);
      } else {
         printf (" %x-%x ", $o, $o+$l-1);
      }
   }
   print ")\n";
}

sub report_trash_statistic {
   return; 
   &get_trash_info();
   print "Trash statistic.\n";
   print "Free big block chunks: (\n";
   &report_trash_list($trashsize{1}, @trash1_o, @trash1_l);
   print "\nFree small block chunks: (\n";
   &report_trash_list($trashsize{2}, @trash2_o, @trash2_l);
   print "\nUnused file space: (\n";
   &report_trash_list($trashsize{3}, @trash3_o, @trash3_l);
   print "\nUnused system space: (\n";
   &report_trash_list($trashsize{4}, @trash4_o, @trash4_l);

   print "\nSummary: (\n";
   &report_trash_list(
      $trashsize{1}+$trashsize{2}+$trashsize{3}+$trashsize{4}, 
      &aggregate_iolist( 1, 
         @trash1_o, @trash2_o, @trash3_o, @trash4_o,
         @trash1_l, @trash2_l, @trash3_l, @trash4_l
      )
   );
}

sub report_trash_list {
   local($size)=shift;
   local(@o)=@_;
   local(@l)=splice(@o, ($#o+1)/2);
   local($i, $m);
   printf ("   %d elements, size=%x\n", $#o+1, $size);
   $m=$#o; for ($i=0; $i<=$m; $i++) {
      printf ("   offset %5x (len %x)\n", $o[$i], $l[$i]);
   }
   print ")\n";
}

sub print_iolist {
   local(@o)=@_;
   local(@l)=splice(@o, ($#o+1)/2); 
   local($i);
   $m=$#o; for ($i=0; $i<=$m; $i++) {
      printf("   o=%6x (%x)\n", $o[$i], $l[$i]);
   }
}

"Atomkraft? Nein, danke!"

