2024-11-25 15:01:02 +00:00
// Package scs contains a reader for SCS# archive files
2019-10-23 18:13:12 +00:00
package scs
import (
"bytes"
"compress/flate"
2024-11-25 15:01:02 +00:00
"compress/zlib"
2019-10-23 18:13:12 +00:00
"encoding/binary"
2024-11-25 15:01:02 +00:00
"errors"
"fmt"
2019-10-23 18:13:12 +00:00
"io"
"path"
2024-11-25 15:01:02 +00:00
"sort"
2019-10-23 18:13:12 +00:00
"strings"
2019-12-26 00:17:18 +00:00
"github.com/Luzifer/scs-extract/b0rkhash"
2019-10-23 18:13:12 +00:00
)
2024-11-25 15:01:02 +00:00
const (
flagIsDirectory = 0x10
supportedVersion = 0x2
zipHeaderSize = 0x2
)
type (
// File represents a file inside the SCS# archive
File struct {
Name string
CompressedSize uint32
Hash uint64
IsCompressed bool
IsDirectory bool
Size uint32
archiveReader io . ReaderAt
offset uint64
}
// Reader contains a parser for the archive and after creation will
// hold a list of files ready to be opened from the archive
Reader struct {
Files [ ] * File
header fileHeader
entryTable [ ] catalogEntry
metadataTable map [ uint32 ] catalogMetaEntry
archiveReader io . ReaderAt
}
fileHeader struct {
Magic [ 4 ] byte
Version uint16
Salt uint16
HashMethod [ 4 ] byte
EntryCount uint32
EntryTableLength uint32
MetadataEntriesCount uint32
MetadataTableLength uint32
EntryTableStart uint64
MetadataTableStart uint64
SecurityDescriptorOffset uint32
Platform byte
}
catalogEntry struct {
Hash uint64
MetadataIndex uint32
MetadataCount uint16
Flags uint16
}
catalogMetaEntry struct {
Index uint32
Offset uint64
CompressedSize uint32
Size uint32
Flags byte
IsDirectory bool
IsCompressed bool
}
catalogMetaEntryType byte
)
const (
metaEntryTypeImage catalogMetaEntryType = 1
metaEntryTypeSample catalogMetaEntryType = 2
metaEntryTypeMipProxy catalogMetaEntryType = 3
metaEntryTypeInlineDirectory catalogMetaEntryType = 4
metaEntryTypePlain catalogMetaEntryType = 128
metaEntryTypeDirectory catalogMetaEntryType = 129
metaEntryTypeMip0 catalogMetaEntryType = 130
metaEntryTypeMip1 catalogMetaEntryType = 131
metaEntryTypeMipTail catalogMetaEntryType = 132
)
2019-12-26 00:17:18 +00:00
var (
2024-11-25 15:01:02 +00:00
scsMagic = [ ] byte ( "SCS#" )
scsHashMethod = [ ] byte ( "CITY" )
2019-12-26 00:17:18 +00:00
localeRootPathHash = b0rkhash . CityHash64 ( [ ] byte ( "locale" ) )
rootPathHash = b0rkhash . CityHash64 ( [ ] byte ( "" ) )
)
2019-10-23 18:13:12 +00:00
2024-11-25 15:01:02 +00:00
// NewReader opens the archive from the given io.ReaderAt and parses
// the header information
func NewReader ( r io . ReaderAt ) ( out * Reader , err error ) {
// Read the header
var header fileHeader
if err = binary . Read (
io . NewSectionReader ( r , 0 , int64 ( binary . Size ( fileHeader { } ) ) ) ,
binary . LittleEndian ,
& header ,
) ; err != nil {
return nil , fmt . Errorf ( "reading header: %w" , err )
}
// Sanity checks
if ! bytes . Equal ( header . Magic [ : ] , scsMagic ) {
return nil , fmt . Errorf ( "unexpected magic header" )
}
2019-10-23 18:13:12 +00:00
2024-11-25 15:01:02 +00:00
if ! bytes . Equal ( header . HashMethod [ : ] , scsHashMethod ) {
return nil , fmt . Errorf ( "unexpected hash method" )
}
2019-10-23 18:13:12 +00:00
2024-11-25 15:01:02 +00:00
if header . Version != supportedVersion {
return nil , fmt . Errorf ( "unsupported archive version: %d" , header . Version )
}
// Do the real parsing
out = & Reader {
archiveReader : r ,
header : header ,
}
if err = out . parseEntryTable ( ) ; err != nil {
return nil , fmt . Errorf ( "parsing entry table: %w" , err )
}
2019-10-23 18:13:12 +00:00
2024-11-25 15:01:02 +00:00
if err = out . parseMetadataTable ( ) ; err != nil {
return nil , fmt . Errorf ( "parsing metadata table: %w" , err )
}
2019-10-23 18:13:12 +00:00
2024-11-25 15:01:02 +00:00
for _ , e := range out . entryTable {
meta := out . metadataTable [ e . MetadataIndex + uint32 ( e . MetadataCount ) ]
f := File {
CompressedSize : meta . CompressedSize ,
Hash : e . Hash ,
IsCompressed : meta . IsCompressed || ( meta . Flags & flagIsDirectory ) != 0 ,
IsDirectory : meta . IsDirectory ,
Size : meta . Size ,
archiveReader : r ,
offset : meta . Offset ,
}
2019-10-23 18:13:12 +00:00
2024-11-25 15:01:02 +00:00
out . Files = append ( out . Files , & f )
}
return out , out . populateFileNames ( )
2019-10-23 18:13:12 +00:00
}
2024-11-25 15:01:02 +00:00
// Open opens the file for reading
2019-10-23 18:13:12 +00:00
func ( f * File ) Open ( ) ( io . ReadCloser , error ) {
var rc io . ReadCloser
2024-11-25 15:01:02 +00:00
if f . IsCompressed {
r := io . NewSectionReader ( f . archiveReader , int64 ( f . offset + zipHeaderSize ) , int64 ( f . CompressedSize ) ) //#nosec:G115 // int64 wraps at 9EB - We don't have to care for a LONG time
2019-10-23 18:13:12 +00:00
rc = flate . NewReader ( r )
2024-11-25 15:01:02 +00:00
} else {
r := io . NewSectionReader ( f . archiveReader , int64 ( f . offset ) , int64 ( f . Size ) ) //#nosec:G115 // int64 wraps at 9EB - We don't have to care for a LONG time
rc = io . NopCloser ( r )
2019-10-23 18:13:12 +00:00
}
2024-11-25 15:01:02 +00:00
return rc , nil
2019-10-23 18:13:12 +00:00
}
2024-11-25 15:01:02 +00:00
func ( r * Reader ) parseEntryTable ( ) error {
etReader , err := zlib . NewReader ( io . NewSectionReader (
r . archiveReader ,
int64 ( r . header . EntryTableStart ) , //#nosec:G115 // int64 wraps at 9EB - We don't have to care for a LONG time
int64 ( r . header . EntryTableLength ) ,
) )
if err != nil {
return fmt . Errorf ( "opening entry-table reader: %w" , err )
2019-10-23 18:13:12 +00:00
}
2024-11-25 15:01:02 +00:00
defer etReader . Close ( ) //nolint:errcheck
2019-10-23 18:13:12 +00:00
2024-11-25 15:01:02 +00:00
for i := uint32 ( 0 ) ; i < r . header . EntryCount ; i ++ {
var e catalogEntry
if err = binary . Read ( etReader , binary . LittleEndian , & e ) ; err != nil {
return fmt . Errorf ( "reading entry: %w" , err )
}
r . entryTable = append ( r . entryTable , e )
2019-10-23 18:13:12 +00:00
}
2024-11-25 15:01:02 +00:00
sort . Slice ( r . entryTable , func ( i , j int ) bool {
return r . entryTable [ i ] . MetadataIndex < r . entryTable [ j ] . MetadataIndex
} )
2019-10-23 18:13:12 +00:00
2024-11-25 15:01:02 +00:00
return nil
}
2019-10-23 18:13:12 +00:00
2024-11-25 15:01:02 +00:00
func ( r * Reader ) parseMetadataTable ( ) error {
r . metadataTable = make ( map [ uint32 ] catalogMetaEntry )
2019-10-23 18:13:12 +00:00
2024-11-25 15:01:02 +00:00
mtReader , err := zlib . NewReader ( io . NewSectionReader (
r . archiveReader ,
int64 ( r . header . MetadataTableStart ) , //#nosec:G115 // int64 wraps at 9EB - We don't have to care for a LONG time
int64 ( r . header . MetadataTableLength ) ,
) )
if err != nil {
return fmt . Errorf ( "opening metadata-table reader: %w" , err )
}
defer mtReader . Close ( ) //nolint:errcheck
for {
var metaType metaEntryType
if err = binary . Read ( mtReader , binary . LittleEndian , & metaType ) ; err != nil {
if errors . Is ( err , io . EOF ) {
return nil
}
return fmt . Errorf ( "reading meta-type-header: %w" , err )
2019-10-23 18:13:12 +00:00
}
2024-11-25 15:01:02 +00:00
var payload iMetaEntry
switch metaType . Type {
case metaEntryTypeDirectory :
var p metaEntryDir
if err = binary . Read ( mtReader , binary . LittleEndian , & p ) ; err != nil {
return fmt . Errorf ( "reading dir definition: %w" , err )
}
payload = metaEntry { t : metaType , p : p }
case metaEntryTypePlain :
var p metaEntryFile
if err = binary . Read ( mtReader , binary . LittleEndian , & p ) ; err != nil {
return fmt . Errorf ( "reading file definition: %w" , err )
}
payload = metaEntry { t : metaType , p : p }
case metaEntryTypeImage :
var p metaEntryImage
if err = binary . Read ( mtReader , binary . LittleEndian , & p ) ; err != nil {
return fmt . Errorf ( "reading image definition: %w" , err )
}
payload = metaEntry { t : metaType , p : p }
default :
return fmt . Errorf ( "unhandled file type: %v" , metaType . Type )
2019-10-23 18:13:12 +00:00
}
2024-11-25 15:01:02 +00:00
var e catalogMetaEntry
payload . Fill ( & e )
2019-10-23 18:13:12 +00:00
2024-11-25 15:01:02 +00:00
r . metadataTable [ e . Index ] = e
}
2019-10-23 18:13:12 +00:00
}
2024-11-25 15:01:02 +00:00
func ( r * Reader ) populateFileNames ( ) ( err error ) {
2019-10-23 18:13:12 +00:00
// first seek root entry, without the archive is not usable for us
var entry * File
2024-11-25 15:01:02 +00:00
for _ , f := range r . Files {
if f . Hash == rootPathHash {
entry = f
2019-12-26 00:17:18 +00:00
entry . Name = ""
break
2024-11-25 15:01:02 +00:00
} else if f . Hash == localeRootPathHash {
entry = f
2019-12-26 00:17:18 +00:00
entry . Name = "locale"
2019-10-23 18:13:12 +00:00
break
}
}
2024-11-25 15:01:02 +00:00
if entry == nil {
// We found no suitable entrypoint
return fmt . Errorf ( "no root entry found" )
2019-10-23 18:13:12 +00:00
}
2024-11-25 15:01:02 +00:00
if err = r . setFilenamesFromDir ( entry ) ; err != nil {
return fmt . Errorf ( "setting filenames: %w" , err )
}
return nil
2019-10-23 18:13:12 +00:00
}
2024-11-25 15:01:02 +00:00
func ( r * Reader ) setFilenamesFromDir ( node * File ) error {
2019-10-23 18:13:12 +00:00
f , err := node . Open ( )
if err != nil {
2024-11-25 15:01:02 +00:00
return fmt . Errorf ( "opening file: %w" , err )
2019-10-23 18:13:12 +00:00
}
2024-11-25 15:01:02 +00:00
defer f . Close ( ) //nolint:errcheck
2019-10-23 18:13:12 +00:00
2024-11-25 15:01:02 +00:00
var entryCount uint32
if err = binary . Read ( f , binary . LittleEndian , & entryCount ) ; err != nil {
return fmt . Errorf ( "reading entry count: %w" , err )
}
2019-10-23 18:13:12 +00:00
2024-11-25 15:01:02 +00:00
if entryCount == 0 {
// Listing without any files
return fmt . Errorf ( "no entries in directory listing" )
2019-10-23 18:13:12 +00:00
}
2024-11-25 15:01:02 +00:00
stringLengths := make ( [ ] byte , entryCount )
if err = binary . Read ( f , binary . LittleEndian , & stringLengths ) ; err != nil {
return fmt . Errorf ( "reading string lengths: %w" , err )
2019-10-23 18:13:12 +00:00
}
2024-11-25 15:01:02 +00:00
for i := uint32 ( 0 ) ; i < entryCount ; i ++ {
2019-10-23 18:13:12 +00:00
var (
hash uint64
2024-11-25 15:01:02 +00:00
name = make ( [ ] byte , stringLengths [ i ] )
2019-10-23 18:13:12 +00:00
recurse bool
)
2024-11-25 15:01:02 +00:00
if err = binary . Read ( f , binary . LittleEndian , & name ) ; err != nil {
return fmt . Errorf ( "reading name: %w" , err )
}
if name [ 0 ] == '/' {
// Directory entry
2019-10-23 18:13:12 +00:00
recurse = true
2024-11-25 15:01:02 +00:00
name = name [ 1 : ]
2019-10-23 18:13:12 +00:00
}
2024-11-25 15:01:02 +00:00
hash = b0rkhash . CityHash64 ( [ ] byte ( strings . TrimPrefix ( path . Join ( node . Name , string ( name ) ) , "/" ) ) )
2019-10-23 18:13:12 +00:00
var next * File
for _ , rf := range r . Files {
2024-11-25 15:01:02 +00:00
if rf . Hash == hash {
2019-10-23 18:13:12 +00:00
next = rf
break
}
}
if next == nil {
2024-11-25 15:01:02 +00:00
return fmt . Errorf ( "reference to void: %s" , path . Join ( node . Name , string ( name ) ) )
2019-10-23 18:13:12 +00:00
}
2024-11-25 15:01:02 +00:00
next . Name = strings . TrimPrefix ( path . Join ( node . Name , string ( name ) ) , "/" )
2019-10-23 18:13:12 +00:00
if recurse {
2024-11-25 15:01:02 +00:00
if err = r . setFilenamesFromDir ( next ) ; err != nil {
2019-10-23 18:13:12 +00:00
return err
}
}
}
return nil
}