/*****************************************************************************
 * dvd_udf.c: udf filesystem tools.
 *****************************************************************************
 * Mainly used to find asolute logical block adress of *.ifo files. It only
 * contains the basic udf handling functions
 *****************************************************************************
 * Copyright (C) 1998-2001 VideoLAN
 *
 * Author: Stéphane Borel <stef@via.ecp.fr>
 *
 * based on:
 *  - dvdudf by Christian Wolff <scarabaeus@convergence.de>
 *  - fixes by Billy Biggs <vektor@dumbterm.net>
 *
 * 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., 59 Temple Place - Suite 330, Boston, MA  02111, USA.
 *****************************************************************************/


#include <stdio.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <strings.h>

#include <dvdcss/dvdcss.h>

#define DVD_LB_SIZE 2048

#include "dvd_udf.h"

#define UDFADshort      1
#define UDFADlong       2
#define UDFADext        4

typedef struct partition_s
{
    boolean_t     b_valid;
    u8            pi_volume_desc[128];
    u16           i_flags;
    u16           i_number;
    u8            pi_contents[32];
    u32           i_access_type;
    u32           i_start;
    u32           i_length;
    dvdcss_handle dvdhandle;
} partition_t;

typedef struct ad_s
{
    u32         i_location;
    u32         i_length;
    u8          i_flags;
    u16         i_partition;
} ad_t;

/* for direct data access, LSB first */
#define GETN1(p) ((u8)pi_data[p])
#define GETN2(p) ((u16)pi_data[p]|((u16)pi_data[(p)+1]<<8))
#define GETN4(p) ((u32)pi_data[p]|((u32)pi_data[(p)+1]<<8)|((u32)pi_data[(p)+2]<<16)|((u32)pi_data[(p)+3]<<24))
#define GETN(p,n,target) memcpy(target,&pi_data[p],n)


/*****************************************************************************
 * UDFReadLB: reads absolute Logical Block of the disc
 *****************************************************************************
 * Returns number of read bytes on success, 0 on error
 *****************************************************************************/
static int UDFReadLB( dvdcss_handle dvdhandle, off_t i_lba,
                      size_t i_block_count, u8 *pi_data )
{
    if( dvdcss_seek( dvdhandle, i_lba, DVDCSS_NOFLAGS ) < 0 )
    {
        fprintf(stderr, "UDF: Postion not found\n");
        return 0;
    }

    return dvdcss_read( dvdhandle, pi_data, i_block_count, DVDCSS_NOFLAGS );
}


/*****************************************************************************
 * UDFDecode: decode unicode encoded udf data
 *****************************************************************************/
static int UDFDecode( u8 * pi_data, int i_len, char * psz_target )
{
    int     p = 1;
    int     i = 0;

    if( !( pi_data[0] & 0x18 ) )
    {
        psz_target[0] = '\0';
        return 0;
    }

    if( pi_data[0] & 0x10 )
    {
        /* ignore MSB of unicode16 */
        p++;

        while( p < i_len )
        {
            psz_target[i++] = pi_data[p+=2];
        }
    }
    else
    {
        while( p < i_len )
        {
            psz_target[i++] = pi_data[p++];
        }
    }
    
    psz_target[i]='\0';

    return 0;
}

#if 0
/**
 *
 **/

int UDFEntity (u8 *data, u8 *Flags, char *Identifier)
{
    Flags[0] = data[0];
    strncpy (Identifier, &data[1], 5);

    return 0;
}
#endif


/*****************************************************************************
 * UDFDescriptor: gives a tag ID from your data to find out what it refers to
 *****************************************************************************/
static int UDFDescriptor( u8 * pi_data, u16 * pi_tag_id )
{
    pi_tag_id[0] = GETN2( 0 );
    /* TODO: check CRC 'n stuff */

    return 0;
}


/*****************************************************************************
 * UDFExtendAD: main volume information
 *****************************************************************************/
static int UDFExtentAD (u8 * pi_data, u32 * pi_length, u32 * pi_location)
{
    pi_length[0] = GETN4( 0 );
    pi_location[0] = GETN4( 4 );

    return 0;
}


/*****************************************************************************
 * UDFAD: file set information
 *****************************************************************************/
static int UDFAD( u8 * pi_data, struct ad_s * p_ad, u8 i_type,
                  struct partition_s partition )
{
    p_ad->i_length = GETN4( 0 );
    p_ad->i_flags = p_ad->i_length >> 30;
    p_ad->i_length &= 0x3FFFFFFF;

    switch( i_type )
    {
        case UDFADshort:
            p_ad->i_location = GETN4( 4 );
            /* use number of current partition */
            p_ad->i_partition = partition.i_number;
             break;

        case UDFADlong:
            p_ad->i_location = GETN4( 4 );
            p_ad->i_partition = GETN2( 8 );
            break;

        case UDFADext:
            p_ad->i_location = GETN4( 12 );
            p_ad->i_partition = GETN2( 16 );
            break;
    }

    return 0;
}


/*****************************************************************************
 * UDFICB: takes Information Control Block from pi_data
 *****************************************************************************/
static int UDFICB( u8 * pi_data, u8 * pi_file_type, u16 * pi_flags)
{
    pi_file_type[0] = GETN1( 11 );
    pi_flags[0] = GETN2( 18 );

    return 0;
}


/*****************************************************************************
 * UDFPartition: gets partition descriptor
 *****************************************************************************/
static int UDFPartition( u8 * pi_data, u16 * pi_flags, u16 * pi_nb,
                         char * ps_contents, u32 * pi_start, u32 * pi_length )
{
    pi_flags[0] = GETN2( 20 );
    pi_nb[0] = GETN2( 22 );
    GETN( 24, 32, ps_contents );
    pi_start[0] = GETN4( 188 );
    pi_length[0] = GETN4( 192 );

    return 0;
}


/*****************************************************************************
 * UDFLogVolume: reads the volume descriptor and checks the parameters
 *****************************************************************************
 * Returns 0 on OK, 1 on error
 *****************************************************************************/
static int UDFLogVolume(u8 * pi_data, char * p_volume_descriptor )
{
    u32 i_lb_size;
    u32 i_MT_L;
    u32 i_N_PM;

    UDFDecode( &pi_data[84], 128, p_volume_descriptor );

    i_lb_size = GETN4( 212 );        // should be 2048
    i_MT_L = GETN4( 264 );        // should be 6
    i_N_PM = GETN4( 268 );        // should be 1

    if( i_lb_size != DVD_LB_SIZE )
    {
        fprintf(stderr, "UDF: Non valid sector size (%d)\n", i_lb_size );
        return 1;
    }

    return 0;
}


/*****************************************************************************
 * UDFFileEntry: fills a ad_t struct with information at pi_data
 *****************************************************************************/
static int UDFFileEntry( u8 * pi_data, u8 * pi_file_type, struct ad_s * p_ad,
                         struct partition_s partition )
{
    u8      i_file_type;
    u16     i_flags;
    u32     i_L_EA;
    u32     i_L_AD;
    int     p;

    UDFICB( &pi_data[16], &i_file_type, &i_flags );

    pi_file_type[0] = i_file_type;
    i_L_EA = GETN4( 168 );
    i_L_AD = GETN4( 172 );
    p = 176 + i_L_EA;

    while( p < 176 + i_L_EA + i_L_AD )
    {
        switch( i_flags & 0x07 )
        {
        case 0:
            UDFAD( &pi_data[p], p_ad, UDFADshort, partition );
            p += 0x08;
            break;
        case 1:
            UDFAD( &pi_data[p], p_ad, UDFADlong, partition );
            p += 0x10;
            break;
        case 2:
            UDFAD( &pi_data[p], p_ad, UDFADext, partition );
            p += 0x14;
            break;
        case 3:
            switch( i_L_AD )
            {
            case 0x08:
                UDFAD( &pi_data[p], p_ad, UDFADshort, partition );
                break;
            case 0x10:
                UDFAD( &pi_data[p], p_ad, UDFADlong, partition );
                break;
            case 0x14:
                UDFAD( &pi_data[p], p_ad, UDFADext, partition );
                break;
            }
        default:
            p += i_L_AD;
            break;
        }
    }

    return 0;
}


/*****************************************************************************
 * UDFFileIdentifier: gives filename and characteristics of pi_data
 *****************************************************************************/
static int UDFFileIdentifier( u8 * pi_data, u8 * pi_file_characteristics,
                              char * psz_filename, struct ad_s * p_file_icb,
                              struct partition_s partition )
{
    u8      i_L_FI;
    u16     i_L_IU;
  
    pi_file_characteristics[0] = GETN1( 18 );
    i_L_FI = GETN1( 19 );
    UDFAD( &pi_data[20], p_file_icb, UDFADlong, partition );
    i_L_IU = GETN2( 36 );

    if( i_L_FI )
    {
        UDFDecode( &pi_data[38+i_L_IU], i_L_FI, psz_filename );
    }
    else
    {
        psz_filename[0]='\0';
    }

    return  4 * ( ( 38 + i_L_FI + i_L_IU + 3 ) / 4 );
}


/*****************************************************************************
 * UDFMapICB: Maps ICB to FileAD
 *****************************************************************************
 * ICB: Location of ICB of directory to scan
 * FileType: Type of the file
 * File: Location of file the ICB is pointing to
 * return 1 on success, 0 on error;
 *****************************************************************************/
static int UDFMapICB( struct ad_s icb, u8 * pi_file_type, struct ad_s * p_file,
                      struct partition_s partition )
{
    u8      pi_lb[DVD_LB_SIZE];
    u32     i_lba;
    u16     i_tag_id;

    i_lba = partition.i_start + icb.i_location;

    do
    {
        if( !UDFReadLB( partition.dvdhandle, i_lba++, 1, pi_lb ) )
        {
            i_tag_id = 0;
        }
        else
        {
            UDFDescriptor( pi_lb , &i_tag_id );
        }

        if( i_tag_id == 261 )
        {
            UDFFileEntry( pi_lb, pi_file_type, p_file, partition );
            return 1;
        }

    } while( ( i_lba <= partition.i_start + icb.i_location +
               ( icb.i_length - 1 ) / DVD_LB_SIZE ) && ( i_tag_id != 261 ) );

    return 0;
}

/*****************************************************************************
 * UDFScanDir: serach filename in dir
 *****************************************************************************
 * Dir: Location of directory to scan
 * FileName: Name of file to look for
 * FileICB: Location of ICB of the found file
 * return 1 on success, 0 on error;
 *****************************************************************************/
static int UDFScanDir( struct ad_s dir, char * psz_filename,
                       struct ad_s * p_file_icb, struct partition_s partition )
{
    u8      pi_lb[2*DVD_LB_SIZE];
    u32     i_lba;
    u16     i_tag_id;
    u8      i_file_char;
    char    psz_temp[DVD_LB_SIZE];
    int     p;
  
    /* Scan dir for ICB of file */
    i_lba = partition.i_start + dir.i_location;
#if 0
    do
    {
        if( !UDFReadLB( partition.dvdhandle, i_lba++, 1, pi_lb ) )
        {
            i_tag_id = 0;
        }
        else
        {
            p=0;
            while( p < DVD_LB_SIZE )
            {
                UDFDescriptor( &pi_lb[p], &i_tag_id );

                if( i_tag_id == 257 )
                {
                    p += UDFFileIdentifier( &pi_lb[p], &i_file_char,
                                            psz_temp, p_file_icb, partition );
                    if( !strcasecmp( psz_filename, psz_temp ) )
                    {
                        return 1;
                    }
                }
                else
                {
                    p = DVD_LB_SIZE;
                }
            }
        }

    } while( i_lba <=
      partition.i_start + dir.i_location + ( dir.i_length - 1 ) / DVD_LB_SIZE );

#else

    if( UDFReadLB( partition.dvdhandle, i_lba, 2, pi_lb ) <= 0 ) {
        return 0;
    }

    p = 0;
    while( p < dir.i_length )
    {
        if( p > DVD_LB_SIZE )
        {
            ++i_lba;
            p -= DVD_LB_SIZE;
            dir.i_length -= DVD_LB_SIZE;
            if( UDFReadLB( partition.dvdhandle, i_lba, 2, pi_lb ) <= 0 )
            {
                return 0;
            }
        }

        UDFDescriptor( &pi_lb[p], &i_tag_id );

        if( i_tag_id == 257 )
        {
            p += UDFFileIdentifier( &pi_lb[p], &i_file_char,
                                    psz_temp, p_file_icb, partition );
            if( !strcasecmp( psz_filename, psz_temp ) )
            {
                return 1;
            }
        }
        else
        {
            return 0;
        }
    }

#endif
    return 0;
}

/*****************************************************************************
 * UDFFindPartition: looks for a partition on the disc
 *****************************************************************************
 *   partnum: number of the partition, starting at 0
 *   part: structure to fill with the partition information
 *   return 1 if partition found, 0 on error;
 *****************************************************************************/
static int UDFFindPartition( int i_part_nb, struct partition_s *p_partition )
{
    u8          pi_lb[DVD_LB_SIZE];
    u8          pi_anchor[DVD_LB_SIZE];
    u16         i_tag_id;
    u32         i_lba;
    u32         i_MVDS_location;
    u32         i_MVDS_length;
    u32         i_last_sector;
    boolean_t   b_term;
    boolean_t   b_vol_valid;
    int         i;

    /* Find Anchor */
    i_last_sector = 0;
 
    /* try #1, prime anchor */
    i_lba = 256;    
    b_term = 0;

    /* Search anchor loop */
    while( 1 )
    {
        if( UDFReadLB( p_partition->dvdhandle, i_lba, 1, pi_anchor ) )
        {
            UDFDescriptor( pi_anchor, &i_tag_id );
        }
        else
        {
            i_tag_id = 0;
        }

        if( i_tag_id != 2 )
        {                
            /* not an anchor? */
            if( b_term )
            {
                /* final try failed */
                return 0;
            }

            if( i_last_sector )
            {
                /* we already found the last sector
                 * try #3, alternative backup anchor */
                i_lba = i_last_sector;    
            
                /* but that's just about enough, then! */
                b_term = 1;            
            }
            else
            {
                /* TODO: find last sector of the disc (this is optional) */
                if( i_last_sector )
                {
                    /* try #2, backup anchor */
                    i_lba = i_last_sector - 256;                
                }
                else
                {
                    /* unable to find last sector */
                    return 0;
                }
            }
        }
        else
        {
            /* it is an anchor! continue... */
            break;
        }
    }

    /* main volume descriptor */
    UDFExtentAD( &pi_anchor[16], &i_MVDS_length, &i_MVDS_location );
  
    p_partition->b_valid = 0;
    b_vol_valid = 0;
    p_partition->pi_volume_desc[0] = '\0';

    i = 1;
    /* Find Volume Descriptor */
    do
    {
        i_lba = i_MVDS_location;

        do
        {
            if( !UDFReadLB( p_partition->dvdhandle, i_lba++, 1, pi_lb ) )
            {
                i_tag_id = 0;
            }
            else
            {
                UDFDescriptor( pi_lb, &i_tag_id );
            }

            if( ( i_tag_id == 5 ) && ( !p_partition->b_valid ) )
            {
                /* Partition Descriptor */
                UDFPartition( pi_lb,
                              &p_partition->i_flags,
                              &p_partition->i_number,
                              p_partition->pi_contents,
                              &p_partition->i_start,
                              &p_partition->i_length );
                p_partition->b_valid = ( i_part_nb == p_partition->i_number );
            }
            else if( ( i_tag_id == 6 ) && ( !b_vol_valid) )
            {
                /* Logical Volume Descriptor */
                if( UDFLogVolume( pi_lb , p_partition->pi_volume_desc ) )
                {  
                /* TODO: sector size wrong! */
                }
                else
                {
                    b_vol_valid = 1;
                }
            }

        } while( ( i_lba <= i_MVDS_location +
                   ( i_MVDS_length - 1 ) / DVD_LB_SIZE )
                 && ( i_tag_id != 8 )
                 && ( ( !p_partition->b_valid ) || ( !b_vol_valid ) ) );
    
        if( ( !p_partition->b_valid ) || ( !b_vol_valid ) )
        {
            /* backup volume descriptor */
            UDFExtentAD( &pi_anchor[24], &i_MVDS_length, &i_MVDS_location );
        }
    } while( i-- && ( ( !p_partition->b_valid ) || ( !b_vol_valid ) ) );

    /* we only care for the partition, not the volume */
    return( p_partition->b_valid);
}


/*****************************************************************************
 * DVDUDFFindFile: looks for a file on the UDF disc/imagefile
 *****************************************************************************
 * Path has to be the absolute pathname on the UDF filesystem,
 * starting with '/'.
 * returns absolute LB number, or 0 on error
 *****************************************************************************/
u32 DVDUDFFindFile( dvdcss_handle dvdhandle, char * psz_path )
{
    struct partition_s  partition;
    struct ad_s         root_icb;
    struct ad_s         file;
    struct ad_s         icb;
    u32                 i_lba;
    u16                 i_tag_id;
    u8                  pi_lb[DVD_LB_SIZE];
    u8                  i_file_type;
    char                psz_tokenline[DVD_LB_SIZE] = "";
    char *              psz_token;
    int                 i_partition;
 
    strcat( psz_tokenline, psz_path );

    /* Init file descriptor of UDF filesystem (== DVD) */
    partition.dvdhandle = dvdhandle;

    /* Find partition 0, standard partition for DVD-Video */
    i_partition = 0;
    if( !UDFFindPartition( i_partition, &partition ) )
    {
        fprintf(stderr, "UDF: Partition 0 not found\n");
        return 0;
    }
  
    /* Find root dir ICB */
    i_lba = partition.i_start;

    do
    {
        if( !UDFReadLB( dvdhandle, i_lba++, 1, pi_lb ) )
        {
            i_tag_id = 0;
        }
        else
        {
            UDFDescriptor( pi_lb, &i_tag_id );
        }

        if( i_tag_id == 256 )
        {
            /* File Set Descriptor */
            UDFAD( &pi_lb[400], &root_icb, UDFADlong, partition );
        }

    } while( ( i_lba < partition.i_start + partition.i_length )
          && ( i_tag_id != 8) && ( i_tag_id != 256 ) );

    if( i_tag_id != 256 )
    {
        fprintf(stderr, "UDF: Bad descriptor\n");
        return 0;
    }
    if( root_icb.i_partition != i_partition )
    {
        fprintf(stderr, "UDF: Bad partition\n");
        return 0;
    }
  
    /* Find root dir */
    if( !UDFMapICB( root_icb, &i_file_type, &file, partition ) )
    {
        fprintf(stderr, "UDF: Can't find root dir\n");
        return 0;
    }

    /* root dir should be dir */
    if( i_file_type != 4 )
    {
        fprintf(stderr, "UDF: Root dir error\n");
        return 0;
    }

    /* Tokenize filepath */
    psz_token = strtok( psz_tokenline, "/" );
    while( psz_token )
    {
        if( !UDFScanDir( file, psz_token, &icb, partition ) )
        {
            fprintf(stderr, "UDF: Scan dir error\n");
            return 0;
        }

        if( !UDFMapICB ( icb, &i_file_type, &file, partition ) )
        {
            fprintf(stderr, "UDF: ICB error\n");
            return 0;
        }

        psz_token = strtok( NULL, "/" );
    }

    return partition.i_start + file.i_location;
}

