616 lines
21 KiB
Plaintext
616 lines
21 KiB
Plaintext
{
|
|
.sob linker
|
|
2009
|
|
April
|
|
10 Successfully linked two test programs
|
|
15 SOB1 format:
|
|
#args in exported pubs now just one byte
|
|
added byte to imports for possible future use with object "pointers"
|
|
rearranged fields in SOB file header so that longs are long-aligned in memory
|
|
June
|
|
9 Sphinxified
|
|
timestamp comparison
|
|
12 Removed /s and /t options
|
|
18 Don't stop on first out-of-date error.
|
|
|
|
To do:
|
|
removal of duplicate sobs
|
|
|
|
|
|
Usage: link sobname [options]
|
|
sobname is name of .sob file, with or without .sob suffix.
|
|
Options start with / or - and are case-insensitive:
|
|
/v <n> -- Sets verbosity level. Higher values of n => more verbose. Default is 0.
|
|
|
|
SOB file format
|
|
|
|
0 4 bytes: SOB file format version #
|
|
4 4 bytes: OBJ timestamp or version
|
|
8 4 bytes: hash of OBJ's binary code.
|
|
12 2 bytes: NUMEXPORTS
|
|
14 2 bytes: EXPORTSIZE
|
|
16 2 bytes: NUMIMPORTS
|
|
18 2 bytes: IMPORTSIZE
|
|
20 2 bytes: OBJBINSIZE (this is always a multiple of 4)
|
|
22 1 byte: checksum
|
|
23 1 byte: reserved
|
|
24 2 bytes: size of OBJ's VAR space.
|
|
26 EXPORTSIZE bytes: exported symbols: CONs, PUBs, maybe PRIs.
|
|
IMPORTSIZE bytes: OBJ's sub-OBJs.
|
|
OBJBINSIZE bytes: the compiled OBJ: header followed by methods.
|
|
|
|
Export record format
|
|
|
|
name + null + 0 + 4-byte int CON int
|
|
name + null + 1 + 4-byte float CON float
|
|
name + null + 2 + index + #args PUB
|
|
name + null + 3 + index + #args PRI (not exported)
|
|
|
|
Import record format
|
|
|
|
name + null + 2-byte count + reserved
|
|
|
|
Export and import names are uppercase. Import name must not include ".SOB" or other suffix.
|
|
|
|
}
|
|
SIZEOFSOBHEADER = 26
|
|
|
|
obj
|
|
term: "isxtv"
|
|
fs[2]: "sxfile"
|
|
str: "stringx"
|
|
|
|
pub Main | err
|
|
err := \Try
|
|
if err
|
|
if err > 0
|
|
term.str( err )
|
|
else
|
|
term.str( string("Error ") )
|
|
term.dec( err )
|
|
term.out( 13 )
|
|
|
|
fs[0].Close
|
|
fs[0].Open( string("sphinx.bin"), "R" )
|
|
fs[0].Execute( 0 )
|
|
|
|
dat
|
|
STACKSPACE long 500 ' in bytes; must be multiple of 4
|
|
TABLESPACE long 1000 ' in bytes; must be multiple of 4
|
|
WORKSPACE long 0
|
|
verbosity byte 0
|
|
outOfDate byte 0
|
|
ignoreOutOfDate byte 0
|
|
|
|
pri Try | nArgs, pTable, pWork, l, p, totalVarSize
|
|
|
|
fs.Open( string("args.d8a"), "R" )
|
|
nArgs := fs.ReadByte
|
|
ifnot nArgs--
|
|
abort string("usage: link sobname [options]")
|
|
fs.ReadStringUpperCase( @sobName, MAXFILENAMELENGTH )
|
|
|
|
l := strsize( @sobName )
|
|
if sobName[l-4] == "." and sobName[l-3] == "S" and sobName[l-2] == "O" and sobName[l-1] == "B"
|
|
sobName[l-4]~
|
|
if strsize( @sobName ) > 8
|
|
term.str( @sobName )
|
|
abort string(" -- sobname too long")
|
|
str.Copy( @outputName, @sobName )
|
|
|
|
' Process command line arguments
|
|
|
|
repeat while nArgs--
|
|
fs.ReadStringUpperCase( @stringBuffer, 20 )
|
|
if stringBuffer[0] == "-"
|
|
stringBuffer[0] := "/"
|
|
if strcomp( @stringBuffer, string("/V") )
|
|
ifnot nArgs--
|
|
abort string("/V must be followed by a number")
|
|
verbosity := fs.ReadNumber
|
|
elseif strcomp( @stringBuffer, string("/I") )
|
|
ignoreOutOfDate~~
|
|
{
|
|
elseif strcomp( @stringBuffer, string("/S") )
|
|
ifnot nArgs--
|
|
abort string("/S must be followed by a number")
|
|
STACKSPACE := fs.ReadNumber
|
|
if STACKSPACE & 3
|
|
abort string("/S argument must be a multiple of 4")
|
|
elseif strcomp( @stringBuffer, string("/T") )
|
|
ifnot nArgs--
|
|
abort string("/T must be followed by a number")
|
|
TABLESPACE := fs.ReadNumber
|
|
if TABLESPACE & 3
|
|
abort string("/T argument must be a multiple of 4")
|
|
}
|
|
'else
|
|
'ignore
|
|
|
|
fs.Close
|
|
|
|
if verbosity => 1
|
|
term.str( string("link 090627", 13) )
|
|
|
|
pTable := word[$000a] + STACKSPACE
|
|
pWork := pTable + TABLESPACE
|
|
WORKSPACE := 32768-pWork
|
|
|
|
if verbosity => 2
|
|
term.dec( WORKSPACE )
|
|
term.str( string(" bytes of work space", 13) )
|
|
|
|
if WORKSPACE =< 0
|
|
abort string("No work space")
|
|
|
|
SobInit( pTable, TABLESPACE )
|
|
AddSob( @sobName )
|
|
ProcessSob( pTable, true )
|
|
checksum~
|
|
totalVarSize := ComputeAddressAndTotalVarSize( pTable, $0010 )
|
|
|
|
word[@header][3] := $0010
|
|
word[@header[$08]] := objBinEndAddress
|
|
word[@header[$0a]] := objBinEndAddress + totalVarSize + 8
|
|
word[@header[$0c]] := firstPubOffset + $0010
|
|
word[@header[$0e]] := word[@header[$0a]] + firstPubLocalsSize + (firstPubNumArgs + 1) << 2
|
|
|
|
AddToChecksum( @header, $10 )
|
|
AddToChecksum( @footer, 8 )
|
|
header[5] := -checksum
|
|
|
|
if verbosity => 3
|
|
p := pTable
|
|
repeat while p
|
|
term.str( word[p +_pName] )
|
|
term.out( " " )
|
|
term.hex( word[p +_startAddress], 4 )
|
|
term.out( " " )
|
|
term.dec( word[p +_totalVarSize] )
|
|
term.out( " " )
|
|
term.hex( byte[p +_checksum], 2 )
|
|
term.out( 13 )
|
|
p := word[p +_pNextSorted]
|
|
|
|
if not outOfDate or ignoreOutOfDate
|
|
str.Append( @outputName, string(".BIN") )
|
|
if verbosity => 2
|
|
term.str( string("Writing ") )
|
|
term.str( @outputName )
|
|
term.out( 13 )
|
|
WriteBinaryFile( @outputName, pTable, pWork, WORKSPACE )
|
|
else
|
|
term.str( string("No .bin written", 13) )
|
|
|
|
pri WriteBinaryFile( pFilename, pSob, pBuff, buffsize ) | n, p, pImports, pCounts, interObjectOffset, varOffset
|
|
{{
|
|
Go down the priority-sorted list of sobs assigning them hub addresses such that they follow one
|
|
another in memory. On the way back up the list, compute each sob's total VAR size (the sob's
|
|
own VARs plus its imported sobs' VARs.)
|
|
Also updates checksum.
|
|
Return value is the current sob's total VAR size. Only the top object's return value is looked at.
|
|
}}
|
|
fs[0].Open( pFilename, "W" )
|
|
fs[0].Write( @header, 16 )
|
|
|
|
repeat while pSob
|
|
str.Copy( @stringBuffer, word[pSob +_pName] )
|
|
str.Append( @stringBuffer, string(".SOB") )
|
|
if verbosity => 2
|
|
term.str( @stringBuffer )
|
|
term.str( string(" -- copying", 13) )
|
|
|
|
n := word[pSob +_objBinSize]
|
|
if n > buffsize
|
|
abort string("work area too small")
|
|
|
|
fs[1].Open( @stringBuffer, "R" )
|
|
fs[1].SkipBytes( SIZEOFSOBHEADER + word[pSob +_exportImportSize] )
|
|
fs[1].Read( pBuff, n )
|
|
fs[1].Close
|
|
|
|
p := pBuff + byte[pBuff][2] << 2 ' byte[2] is index of 1st obj entry; multiply by 4 bytes/entry
|
|
varOffset := word[pSob +_varSize]
|
|
pImports := word[pSob +_pImports]
|
|
pCounts := word[pSob +_pCounts]
|
|
repeat word[pSob +_nImports]
|
|
interObjectOffset := word[ word[pImports] +_startAddress] - word[pSob +_startAddress]
|
|
repeat word[pCounts]
|
|
word[p] := interObjectOffset
|
|
p += 2
|
|
word[p] := varOffset
|
|
p += 2
|
|
varOffset += word[ word[pImports] +_totalVarSize]
|
|
|
|
pImports += 2
|
|
pCounts += 2
|
|
|
|
|
|
fs[0].Write( pBuff, n )
|
|
pSob := word[pSob +_pNextSorted]
|
|
fs[0].Close
|
|
|
|
{{
|
|
objBinEndAddress := address ' end address will point just beyond the last obj in hub memory.
|
|
' Here we're just overwriting as we go and keeping the last one.
|
|
|
|
ComputeAddressAndTotalVarSize( word[p +_pNextSorted], address )
|
|
|
|
checksum += byte[p +_checksum] ' this is the partial checksum of the obj (doesn't count its sub-object table because
|
|
' sub-object links are not known until link time (i.e., now))
|
|
totalVarSize := word[p +_varSize]
|
|
pImports := word[p +_pImports]
|
|
pCounts := word[p +_pCounts]
|
|
repeat word[p +_nImports] ' for each import, add the import's VAR size multiplied by its count
|
|
interObjectOffset := word[ word[pImports] +_startAddress] - word[p +_startAddress]
|
|
repeat word[pCounts]
|
|
AddToChecksum( @totalVarSize, 2 ) ' checksum needs to include this half of an object table entry
|
|
AddToChecksum( @interObjectOffset, 2 ) ' and this other half of an object table entry
|
|
|
|
totalVarSize += word[ word[pImports] +_totalVarSize]
|
|
|
|
pImports += 2
|
|
pCounts += 2
|
|
|
|
word[p +_totalVarSize] := totalVarSize
|
|
}}
|
|
|
|
var
|
|
word firstPubLocalsSize
|
|
word firstPubOffset
|
|
word firstPubNumArgs
|
|
word objBinEndAddress
|
|
byte checksum
|
|
long clk_freq
|
|
long xin_freq
|
|
long clk_mode
|
|
long free
|
|
long stack
|
|
|
|
pri ReadExports( numExports ) | firstPub, type, val, index, nArgs, i, p, f, frequency
|
|
f~
|
|
firstPub~~
|
|
stack := 16
|
|
frequency := 12_000_000
|
|
|
|
repeat numExports
|
|
fs.ReadStringUpperCase( @stringBuffer, MAXEXPORTLENGTH )
|
|
case type := fs.ReadByte
|
|
0, 1: ' int or float CON
|
|
val := fs.ReadLong
|
|
2, 3: ' PUB or PRI
|
|
index := fs.ReadByte
|
|
nArgs := fs.ReadByte
|
|
if firstPub~
|
|
firstPubNumArgs := nArgs
|
|
|
|
repeat i from 0 to 4
|
|
if strcomp( @stringBuffer, @@ptrs[i] )
|
|
if type
|
|
term.str( @stringBuffer )
|
|
abort string(" -- not an int CON")
|
|
clk_freq[i] := val
|
|
f |= |< i
|
|
|
|
f &= 7
|
|
if clk_mode & 3
|
|
f |= 8
|
|
case f ' four bits: rc|clkmode|xinfreq|clkfreq
|
|
%0000: ' none of clkmode/xinfreq/clkfreq specified
|
|
%0001..%0011: abort string("_CLKMODE must be specified")
|
|
%0100: abort string("_CLKFREQ or _XINFREQ must be specified")
|
|
%0101: frequency := clk_freq
|
|
%0110: frequency := xin_freq * ((clk_mode >> 6) #> 1)
|
|
%0111: if clk_freq <> xin_freq * ((clk_mode >> 6) #> 1)
|
|
abort string("conflicting _CLKFREQ and _XINFREQ")
|
|
%1000..%1011: ' these cases shouldn't happen
|
|
%1100: ' this case is OK
|
|
%1101..%1111: abort string("RCFAST/SLOW incompatible with _CLKFREQ/_XINFREQ")
|
|
|
|
long[@header] := frequency
|
|
|
|
header[4] := ComputeClkmodeByte( clk_mode )
|
|
|
|
pri ComputeClkmodeByte( mode ) : m | b1, b2, i
|
|
{
|
|
rcfast $001 exactly one 1 in 0000_0000_00xx incompatible with clkfreq/xinfreq
|
|
rcslow $002
|
|
or
|
|
xinput $004 exactly one 1 in 0000_00xx_xx00 and requires clkfreq/xinfreq
|
|
xtal1 $008 up to one 1 in 0xxx_xx00_0000
|
|
xtal2 $010
|
|
xtal3 $020
|
|
pll1x $040
|
|
pll2x $080
|
|
pll4x $100
|
|
pll8x $200
|
|
pll16x $400
|
|
}
|
|
b1 := -1 ' b1 is the position of the single 1 in mode[5..0].
|
|
repeat i from 0 to 5
|
|
if mode & |< i
|
|
if b1 <> -1
|
|
abort string("invalid _CLKMODE") ' only one 1 allowed
|
|
b1 := i
|
|
m := lookupz( b1: $00, $01, $22, $2a, $32, $3a )
|
|
|
|
b2 := -1 ' b2 is the position of single 1 in mode[10..6] (-1 if no 1 bit)
|
|
repeat i from 6 to 10
|
|
if mode & |< i
|
|
if b2 <> -1
|
|
abort string("invalid _CLKMODE (multiple PLL)") ' only one 1 allowed
|
|
b2 := i
|
|
|
|
if b1 < 2 ' RCFAST/RCSLOW?
|
|
if b2 <> -1 ' b2 better not be set
|
|
abort string("invalid _CLKMODE (RC+PLL)")
|
|
else ' one of the X-modes
|
|
if b2 <> -1
|
|
m |= $40 ' PLLENA
|
|
m += b2 - 5
|
|
|
|
|
|
dat
|
|
ptrs word @s0, @s1, @s2, @s3, @s4
|
|
s0 byte "_CLKFREQ", 0
|
|
s1 byte "_XINFREQ", 0
|
|
s2 byte "_CLKMODE", 0
|
|
s3 byte "_FREE", 0
|
|
s4 byte "_STACK", 0
|
|
|
|
|
|
dat
|
|
long
|
|
header byte 0[16]
|
|
footer byte $ff, $ff, $f9, $ff
|
|
byte $ff, $ff, $f9, $ff
|
|
|
|
con
|
|
MAXFILENAMELENGTH = 8 + 1 + 3 ' 8.3
|
|
MAXEXPORTLENGTH = 32
|
|
|
|
var
|
|
byte stringBuffer[MAXEXPORTLENGTH+1]
|
|
byte sobName[MAXFILENAMELENGTH+1]
|
|
byte outputName[MAXFILENAMELENGTH+1]
|
|
|
|
con
|
|
#0
|
|
_pNext[2]
|
|
_pNextSorted[2]
|
|
_pName[2]
|
|
_nImports[2]
|
|
_pImports[2]
|
|
_pCounts[2]
|
|
_startAddress[2]
|
|
_objBinSize[2]
|
|
_totalVarSize[2]
|
|
_varSize[2]
|
|
_timestamp[4]
|
|
_exportImportSize[2]
|
|
_checksum[1]
|
|
_[1] ' for alignment: _SIZEOFSOBINFO must be a multiple of 4
|
|
_SIZEOFSOBINFO
|
|
|
|
SOBFILEFORMATVERSION = "S" + "O" << 8 + "B" << 16 + "1" << 24 ' "SOB1"
|
|
|
|
dat
|
|
pSobSpace word 0
|
|
SOBSSIZE word 0
|
|
pDataSpace word 0
|
|
pFirst word 0
|
|
pLast word 0
|
|
pLastSorted word 0
|
|
|
|
pri SobInit( p, size )
|
|
pFirst := pSobSpace := p
|
|
SOBSSIZE := size
|
|
pDataSpace := pSobSpace + SOBSSIZE
|
|
|
|
pLastSorted := pLast := pFirst
|
|
|
|
word[pFirst +_pNext]~
|
|
word[pFirst +_pNextSorted]~
|
|
|
|
pri ComputeAddressAndTotalVarSize( p, address ) : totalVarSize | pImports, pCounts, interObjectOffset
|
|
{{
|
|
Go down the priority-sorted list of sobs assigning them hub addresses such that they follow one
|
|
another in memory. On the way back up the list, compute each sob's total VAR size (the sob's
|
|
own VARs plus its imported sobs' VARs.)
|
|
Also updates checksum.
|
|
Return value is the current sob's total VAR size. Only the top object's return value is looked at.
|
|
}}
|
|
ifnot p
|
|
return
|
|
|
|
word[p +_startAddress] := address
|
|
address += word[p +_objBinSize]
|
|
|
|
objBinEndAddress := address ' end address will point just beyond the last obj in hub memory.
|
|
' Here we're just overwriting as we go and keeping the last one.
|
|
|
|
ComputeAddressAndTotalVarSize( word[p +_pNextSorted], address )
|
|
|
|
checksum += byte[p +_checksum] ' this is the partial checksum of the obj (doesn't count its sub-object table because
|
|
' sub-object links are not known until link time (i.e., now))
|
|
totalVarSize := word[p +_varSize]
|
|
pImports := word[p +_pImports]
|
|
pCounts := word[p +_pCounts]
|
|
repeat word[p +_nImports] ' for each import, add the import's VAR size multiplied by its count
|
|
interObjectOffset := word[ word[pImports] +_startAddress] - word[p +_startAddress]
|
|
repeat word[pCounts]
|
|
AddToChecksum( @totalVarSize, 2 ) ' checksum needs to include this half of an object table entry
|
|
AddToChecksum( @interObjectOffset, 2 ) ' and this other half of an object table entry
|
|
|
|
totalVarSize += word[ word[pImports] +_totalVarSize]
|
|
|
|
pImports += 2
|
|
pCounts += 2
|
|
|
|
word[p +_totalVarSize] := totalVarSize
|
|
|
|
pri AddToChecksum( p, n )
|
|
repeat n
|
|
checksum += byte[p++]
|
|
|
|
pri AddSob( pName ) : p | n
|
|
{{
|
|
Copies name to data area, appends name to the sobs list, returns pointer to new sob entry.
|
|
}}
|
|
if verbosity => 2
|
|
term.str(string("adding "))
|
|
term.str(pName)
|
|
term.out(13)
|
|
|
|
p := pSobSpace
|
|
pSobSpace += _SIZEOFSOBINFO
|
|
|
|
n := strsize(pName) + 1
|
|
bytemove( Alloc(n), pName, n )
|
|
|
|
word[pLast +_pNext] := p
|
|
word[pLastSorted +_pNextSorted] := p
|
|
word[p +_pName] := pDataSpace
|
|
word[p +_pNext]~
|
|
word[p +_pNextSorted]~
|
|
pLast := p
|
|
pLastSorted := p
|
|
|
|
pri Alloc( n )
|
|
pDataSpace := (pDataSpace - n) & $fffffffc ' long-aligned
|
|
if pDataSpace < pSobSpace
|
|
abort string("Insufficient sob table space")
|
|
return pDataSpace
|
|
|
|
pri FindSob( pName ) : p | pPrev
|
|
{{
|
|
Search the sob list in priority order. If a sob in the list matches name,
|
|
update priority-order links to put the sob at the end and return a pointer to it.
|
|
Otherwise, return 0.
|
|
}}
|
|
p := pPrev := pFirst
|
|
repeat while p
|
|
if strcomp( word[p +_pName], pName )
|
|
if p <> pLastSorted
|
|
word[pPrev +_pNextSorted] := word[p +_pNextSorted]
|
|
word[pLastSorted +_pNextSorted] := p
|
|
word[p +_pNextSorted]~
|
|
pLastSorted := p
|
|
return
|
|
pPrev := p
|
|
p := word[p +_pNextSorted]
|
|
return 0
|
|
|
|
pri ProcessSob( p, top ) | len, numExports, exportSize, numImports, importSize, objBinSize, hash, pStart, temp, pImports, pCounts, ts0, ts1
|
|
{{
|
|
Reads the sob file identified by p, appends the sob's imports to the sob list,
|
|
then recursively processes the imported sobs.
|
|
top is true for the top sob, false for all other sobs.
|
|
}}
|
|
Str.Copy( @stringBuffer, word[p +_pName] )
|
|
Str.Append( @stringBuffer, string(".SOB") )
|
|
|
|
if verbosity => 2
|
|
term.str( string("Reading [") )
|
|
term.str( @stringBuffer )
|
|
term.out( "]" )
|
|
term.out( 13 )
|
|
|
|
fs.Open( @stringBuffer, "R" )
|
|
|
|
if fs.Readlong <> SOBFILEFORMATVERSION ' SOB file format version
|
|
abort string("Unrecognized SOB file format")
|
|
long[p +_timestamp] := fs.Readlong ' timestamp
|
|
hash := fs.ReadLong ' hash
|
|
numExports := fs.ReadWord ' number of exports
|
|
exportSize := fs.ReadWord ' size of exports segment
|
|
numImports := fs.ReadWord ' number of imports
|
|
importSize := fs.ReadWord ' size of imports segment
|
|
word[p +_objBinSize] := fs.ReadWord ' size of bytecode segment
|
|
byte[p +_checksum] := fs.ReadByte ' checksum
|
|
fs.ReadByte ' padding
|
|
word[p +_varSize] := fs.ReadWord ' size of sob's VAR space
|
|
|
|
ts0 := long[p +_timestamp]
|
|
|
|
if top
|
|
ReadExports( numExports )
|
|
else
|
|
fs.SkipBytes( exportSize )
|
|
|
|
word[p +_exportImportSize] := exportSize + importSize
|
|
|
|
word[p +_nImports] := numImports
|
|
word[p +_pImports] := pImports := Alloc( numImports << 1 ) ' points to an array of sob pointers
|
|
word[p +_pCounts] := pCounts := Alloc( numImports << 1 ) ' points to an array of sob counts
|
|
|
|
pStart := pLast
|
|
repeat numImports
|
|
fs.ReadStringUpperCase( @stringBuffer, 8 )
|
|
ts1 := GetTimestamp( @stringBuffer )
|
|
if ts0 and ts1 ' Only compare non-zero timestamps
|
|
if ts0 =< ts1
|
|
term.str( word[p+_pName] )
|
|
term.str( string(".SOB is older than ") )
|
|
term.str( @stringBuffer )
|
|
term.str( string(".SOB", 13) )
|
|
outOfDate~~
|
|
ifnot temp := FindSob( @stringBuffer )
|
|
temp := AddSob( @stringBuffer )
|
|
word[pImports] := temp
|
|
word[pCounts] := fs.ReadWord
|
|
fs.ReadByte ' reserved byte
|
|
if verbosity => 3
|
|
term.str( word[ word[pImports] +_pName] )
|
|
term.out( "*" )
|
|
term.dec( word[pCounts] )
|
|
term.out( 13 )
|
|
pImports += 2
|
|
pCounts += 2
|
|
|
|
if top
|
|
fs.SkipBytes( $4 ) ' look into the top sob's object header
|
|
firstPubOffset := fs.ReadWord ' and get offset to first pub
|
|
firstPubLocalsSize := fs.ReadWord ' and size of first pub's locals
|
|
|
|
fs.Close
|
|
|
|
' Process the imported sobs
|
|
|
|
pStart := word[pStart +_pNext]
|
|
repeat while pStart
|
|
ProcessSob( pStart, false )
|
|
pStart := word[pStart +_pNext]
|
|
|
|
if verbosity => 2
|
|
term.str( string("done with ") )
|
|
term.str( word[p +_pName] )
|
|
term.out( 13 )
|
|
|
|
pri GetTimestamp( pFilename )
|
|
str.Append( pFilename, string(".SOB") )
|
|
fs[1].Open( pFilename, "R" )
|
|
fs[1].ReadLong
|
|
result := fs[1].ReadLong
|
|
fs[1].Close
|
|
byte[pFilename][ strsize(pFilename) - 4 ]~ ' remove .SOB tail
|
|
|
|
{{
|
|
Copyright (c) 2009 Michael Park
|
|
+------------------------------------------------------------------------------------------------------------------------------+
|
|
| TERMS OF USE: MIT License |
|
|
+------------------------------------------------------------------------------------------------------------------------------+
|
|
|Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation |
|
|
|files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, |
|
|
|modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software|
|
|
|is furnished to do so, subject to the following conditions: |
|
|
| |
|
|
|The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.|
|
|
| |
|
|
|THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE |
|
|
|WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR |
|
|
|COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, |
|
|
|ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. |
|
|
+------------------------------------------------------------------------------------------------------------------------------+
|
|
}}
|
|
|