-
Notifications
You must be signed in to change notification settings - Fork 0
/
cab.go
331 lines (288 loc) · 9.21 KB
/
cab.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
package cab
import (
"encoding/binary"
"errors"
"io"
"strings"
"time"
)
type Cabinet struct {
Files []*File
ReservedHeaderBlock []byte
MultiCabinetInfo
}
type MultiCabinetInfo struct {
PreviousFile string
PreviousDisk string
NextFile string
NextDisk string
SetId uint16 // ID of this multi-cabinet set; should be the same in all files in the set
SetIndex uint16 // Index of this cabinet in the multi-cabinet set
}
// Helper flag for fuzzing:
// cabextract assumes that CFFILE structs are immediately after CFFOLDER, which is usually the case, but not necessary according to the specification.
// With this flag set, we return an error if this assumption is incorrect.
var requireDirectCfFileFollow = false
func Open(reader io.ReaderAt, size int64) (*Cabinet, error) {
fullReader := io.NewSectionReader(reader, 0, size)
var cab Cabinet
var cfHeader cabinetFileHeader
if err := binary.Read(fullReader, binary.LittleEndian, &cfHeader); err != nil {
return nil, err
}
if cfHeader.Signature != [4]byte{0x4D, 0x53, 0x43, 0x46} {
return nil, errors.New("CAB signature did not match")
}
if cfHeader.VersionMajor != 1 {
return nil, errors.New("unsupported major version")
}
if cfHeader.VersionMinor > 3 {
return nil, errors.New("unsupported minor version")
}
cab.SetIndex = cfHeader.SetIndex
cab.SetId = cfHeader.SetId
cabinetReserve := cfHeader.Flags&cabinetReserveExists != 0
var reservedSizes cabinetFileReservedSizes
if cabinetReserve {
if err := binary.Read(fullReader, binary.LittleEndian, &reservedSizes); err != nil {
return nil, err
}
}
var reservedHeaderBlock = make([]byte, reservedSizes.ReservedHeaderSize)
if _, err := io.ReadFull(fullReader, reservedHeaderBlock); err != nil {
return nil, err
}
cab.ReservedHeaderBlock = reservedHeaderBlock
previousCabinet := cfHeader.Flags&previousCabinetExists != 0
if previousCabinet {
var err error
if cab.PreviousFile, err = readZeroTerminatedString(fullReader); err != nil {
return nil, err
}
if cab.PreviousDisk, err = readZeroTerminatedString(fullReader); err != nil {
return nil, err
}
}
nextCabinet := cfHeader.Flags&nextCabinetExists != 0
if nextCabinet {
var err error
if cab.NextFile, err = readZeroTerminatedString(fullReader); err != nil {
return nil, err
}
if cab.NextDisk, err = readZeroTerminatedString(fullReader); err != nil {
return nil, err
}
}
folders, err := readFolderEntries(fullReader, cfHeader.FolderCount, reservedSizes.ReservedFolderSize)
if err != nil {
return nil, err
}
postFolderOffset, _ := fullReader.Seek(0, io.SeekCurrent)
// Look up data entries for each folder
for i := range folders {
folder := &folders[i]
if _, err := fullReader.Seek(int64(folder.CoffCabStart), io.SeekStart); err != nil {
return nil, err
}
dataEntries, err := readDataEntries(fullReader, folder.CfDataCount, reservedSizes.ReservedDatablockSize)
if err != nil {
return nil, err
}
folder.dataEntries = dataEntries
}
if requireDirectCfFileFollow {
if int64(cfHeader.FirstFileEntryOffset) != postFolderOffset {
return nil, errors.New("offset between CFFOLDER and CFFILE")
}
}
_, err = fullReader.Seek(int64(cfHeader.FirstFileEntryOffset), io.SeekStart)
if err != nil {
return nil, err
}
fileEntries, err := readFileEntries(fullReader, cfHeader.FileCount)
if err != nil {
return nil, err
}
for _, fileEntry := range fileEntries {
if int(fileEntry.FolderIndex) >= len(folders) {
return nil, errors.New("invalid folder reference")
}
folder := &folders[fileEntry.FolderIndex]
cab.Files = append(cab.Files, &File{
Name: fileEntry.fileName,
Modified: parseCabTimestamp(fileEntry.Date, fileEntry.Time),
Attributes: fileEntry.Attributes,
folder: folder,
header: fileEntry.cabinetFileEntryHeader,
})
}
return &cab, nil
}
const (
previousCabinetExists = 0x0001
nextCabinetExists = 0x0002
cabinetReserveExists = 0x0004
)
// Cabinet file header according to https://docs.microsoft.com/en-us/previous-versions//bb267310(v=vs.85)?redirectedfrom=MSDN#cfheader
type cabinetFileHeader struct {
Signature [4]byte
_ uint32
Filesize uint32
_ uint32
FirstFileEntryOffset uint32
_ uint32
VersionMinor byte
VersionMajor byte
FolderCount uint16
FileCount uint16
Flags uint16
SetId uint16
SetIndex uint16
// Optional: cabinetFileReservedSizes, if cabinetReserveExists is set
// Optional: Cabinet reserved area, if cabinetReserveExists is set
// Optional: Name of previous cabinet file
// Optional: Name of previous disk
// Optional: Name of next cabinet file
// Optional: Name of next disk
}
type cabinetFileReservedSizes struct {
ReservedHeaderSize uint16
ReservedFolderSize uint8
ReservedDatablockSize uint8
}
type cabinetFileFolderHeader struct {
CoffCabStart uint32
CfDataCount uint16
CompressionType uint16
// Optional: Per-Folder reserved area
}
type cabinetFileFolder struct {
cabinetFileFolderHeader
reservedData []byte
dataEntries []cabinetFileData
}
type cabinetFileEntryHeader struct {
UncompressedFileSize uint32
UncompressedOffsetInFolder uint32
FolderIndex uint16
Date uint16
Time uint16
Attributes uint16
// Followed by fileName, which is a zero-terminated string
}
type cabinetFileEntry struct {
cabinetFileEntryHeader
fileName string
}
type cabinetFileDataHeader struct {
Checksum uint32
CompressedBytes uint16
UncompressedBytes uint16
// Optional: Per-Datablock reserved area
// Followed by compressed data bytes
}
type cabinetFileData struct {
cabinetFileDataHeader
reservedData []byte
compressedData *io.SectionReader
}
func readZeroTerminatedString(reader *io.SectionReader) (string, error) {
stringStartOffset, _ := reader.Seek(0, io.SeekCurrent)
var currentBufferSize = 10
var stringBuffer strings.Builder
for {
var buffer = make([]byte, currentBufferSize)
n, err := io.ReadFull(reader, buffer)
foundZeroByte := false
for i := range buffer {
if buffer[i] == 0 {
n = i
foundZeroByte = true
break
}
}
stringBuffer.Write(buffer[:n])
if foundZeroByte {
break
}
if err != nil {
return "", err
}
currentBufferSize *= 2
}
// Adjust reader offset to the position after the string and terminating zero
reader.Seek(stringStartOffset+int64(stringBuffer.Len())+1, io.SeekStart)
return stringBuffer.String(), nil
}
func readFolderEntries(reader *io.SectionReader, folderCount uint16, reservedAreaSize uint8) ([]cabinetFileFolder, error) {
var folders []cabinetFileFolder
for i := 0; i < int(folderCount); i++ {
var folder cabinetFileFolder
var folderHeader cabinetFileFolderHeader
if err := binary.Read(reader, binary.LittleEndian, &folderHeader); err != nil {
return nil, err
}
folder.cabinetFileFolderHeader = folderHeader
if reservedAreaSize != 0 {
folder.reservedData = make([]byte, reservedAreaSize)
if _, err := io.ReadFull(reader, folder.reservedData); err != nil {
return nil, err
}
}
folders = append(folders, folder)
}
return folders, nil
}
func readFileEntries(reader *io.SectionReader, fileCount uint16) ([]cabinetFileEntry, error) {
var files []cabinetFileEntry
for i := 0; i < int(fileCount); i++ {
var file cabinetFileEntry
var fileHeader cabinetFileEntryHeader
if err := binary.Read(reader, binary.LittleEndian, &fileHeader); err != nil {
return nil, err
}
file.cabinetFileEntryHeader = fileHeader
filename, err := readZeroTerminatedString(reader)
if err != nil {
return nil, err
}
file.fileName = filename
files = append(files, file)
}
return files, nil
}
func readDataEntries(reader *io.SectionReader, dataCount uint16, reservedAreaSize uint8) ([]cabinetFileData, error) {
var dataEntries []cabinetFileData
for i := 0; i < int(dataCount); i++ {
var dataEntry cabinetFileData
var dataEntryHeader cabinetFileDataHeader
if err := binary.Read(reader, binary.LittleEndian, &dataEntryHeader); err != nil {
return nil, err
}
dataEntry.cabinetFileDataHeader = dataEntryHeader
if reservedAreaSize != 0 {
dataEntry.reservedData = make([]byte, reservedAreaSize)
if _, err := io.ReadFull(reader, dataEntry.reservedData); err != nil {
return nil, err
}
}
// Store the compressed data as a reader and skip it
currentOffset, _ := reader.Seek(0, io.SeekCurrent)
dataEntry.compressedData = io.NewSectionReader(reader, currentOffset, int64(dataEntry.CompressedBytes))
reader.Seek(int64(dataEntry.CompressedBytes), io.SeekCurrent)
dataEntries = append(dataEntries, dataEntry)
}
return dataEntries, nil
}
func parseCabTimestamp(cabDate uint16, cabTime uint16) time.Time {
// See https://docs.microsoft.com/en-us/previous-versions//bb267310(v=vs.85)#cffile
// cabDate is ((year–1980) << 9)+(month << 5)+(day)
// cabTime is (hour << 11)+(minute << 5)+(seconds/2)
year := int(cabDate>>9) + 1980
month := int(cabDate>>5) & 0b1111
day := int(cabDate) & 0b11111
hour := int(cabTime >> 11)
minute := int(cabTime>>5) & 0b111111
seconds := int(cabTime&0b11111) * 2
return time.Date(year, time.Month(month), day, hour, minute, seconds, 0, time.Local)
}