-
-
Notifications
You must be signed in to change notification settings - Fork 353
/
ZipArchive.class.st
390 lines (323 loc) · 12.6 KB
/
ZipArchive.class.st
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
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
"
A ZipArchive represents an archive that is read and/or written using the PKZIP file format.
ZipArchive instances know how to read and write such archives; their members are subinstances of ZipArchiveMember.
"
Class {
#name : #ZipArchive,
#superclass : #Archive,
#instVars : [
'centralDirectorySize',
'centralDirectoryOffsetWRTStartingDiskNumber',
'zipFileComment',
'writeCentralDirectoryOffset',
'writeEOCDOffset'
],
#pools : [
'ZipFileConstants'
],
#category : #'Compression-Archives'
}
{ #category : #constants }
ZipArchive class >> compressionDeflated [
^CompressionDeflated
]
{ #category : #constants }
ZipArchive class >> compressionLevelDefault [
^CompressionLevelDefault
]
{ #category : #constants }
ZipArchive class >> compressionLevelNone [
^CompressionLevelNone
]
{ #category : #constants }
ZipArchive class >> compressionStored [
^CompressionStored
]
{ #category : #'fileIn/Out' }
ZipArchive class >> extractAllIn: aFileName [
"Service method to extract all contents of a zip."
| directory |
directory := (UIManager default chooseDirectoryFrom: aFileName asFileReference) ifNil: [^ self].
^ (self new)
readFrom: aFileName;
extractAllTo: directory.
]
{ #category : #constants }
ZipArchive class >> findEndOfCentralDirectoryFrom: stream [
"Seek in the given stream to the end, then read backwards until we find the
signature of the central directory record. Leave the file positioned right
before the signature.
Answers the file position of the EOCD, or 0 if not found."
| data fileLength seekOffset pos maxOffset |
stream setToEnd.
fileLength := stream position.
"If the file length is less than 18 for the EOCD length plus 4 for the signature, we have a problem"
fileLength < 22 ifTrue: [^ self error: 'file is too short'].
seekOffset := 0.
pos := 0.
data := ByteArray new: 4100.
maxOffset := 40960 min: fileLength. "limit search range to 40K"
[
seekOffset := (seekOffset + 4096) min: fileLength.
stream position: fileLength - seekOffset.
data := stream next: (4100 min: seekOffset) into: data startingAt: 1.
pos := data lastIndexOfPKSignature: EndOfCentralDirectorySignature.
pos = 0 and: [seekOffset < maxOffset]
] whileTrue.
^ pos > 0
ifTrue: [ | newPos | stream position: (newPos := (stream position + pos - seekOffset - 1)). newPos]
ifFalse: [0]
]
{ #category : #'file format' }
ZipArchive class >> isZipArchive: file [
"Answer whether the given file represents a valid zip file. The argument file can be a String, FileReference or an open binary read stream.
See ZipArchiveTest>>testisZipArchive for examples."
| stream eocdPosition |
stream := file isStream
ifTrue: [ file ]
ifFalse: [ file asFileReference binaryReadStream ].
stream size < 22 ifTrue: [ ^ false ].
eocdPosition := self findEndOfCentralDirectoryFrom: stream.
stream ~= file ifTrue: [ stream close ].
^ eocdPosition > 0
]
{ #category : #constants }
ZipArchive class >> validSignatures [
"Return the valid signatures for a zip file"
^Array
with: LocalFileHeaderSignature
with: CentralDirectoryFileHeaderSignature
with: EndOfCentralDirectorySignature
]
{ #category : #'archive operations' }
ZipArchive >> addDeflateString: aString as: aFileName [
"Add a verbatim string under the given file name"
| mbr |
mbr := self addString: aString as: aFileName.
mbr desiredCompressionMethod: CompressionDeflated.
^ mbr
]
{ #category : #initialization }
ZipArchive >> close [
self members do: [:m | m close ]
]
{ #category : #'archive operations' }
ZipArchive >> extractAllTo: aDirectory [
"Extract all elements to the given directory; notifying user via UI on progress and if existing files exist"
UIManager default
informUserDuring: [:bar |
bar max: self numberOfMembers.
self extractAllTo: aDirectory informing: bar ]
]
{ #category : #'archive operations' }
ZipArchive >> extractAllTo: aDirectory informing: bar [
"Extract all elements to the given directory; notifying user via UI on progress and if existing files exist"
^ self extractAllTo: aDirectory informing: bar overwrite: false
]
{ #category : #'archive operations' }
ZipArchive >> extractAllTo: aDirectory informing: aBar overwrite: allOverwrite [
"Extract all elements to the given directory. Informs user when a file exists and set to overwrite"
| bar overwriteAll barValue |
bar := aBar ifNil: [ DummySystemProgressItem new ].
overwriteAll := allOverwrite.
barValue := 0.
self members
select: #isDirectory
thenDo: [ :entry |
| dir shouldUpdateInfos lastUpdate |
lastUpdate := 0.
(shouldUpdateInfos := (Time millisecondsSince: lastUpdate) >= 100)
ifTrue: [ lastUpdate := Time millisecondClockValue.
bar label: 'Creating ' , entry fileName ].
dir := (entry fileName findTokens: '/')
inject: aDirectory
into: [ :base :part | base / part ].
dir ensureCreateDirectory.
barValue := barValue + 1.
shouldUpdateInfos
ifTrue: [ bar value: barValue ] ].
self members
reject: #isDirectory
thenDo: [ :entry |
| shouldUpdateInfos lastUpdate |
lastUpdate := 0.
(shouldUpdateInfos := (Time millisecondsSince: lastUpdate) >= 100)
ifTrue: [ lastUpdate := Time millisecondClockValue.
bar label: 'Extracting ' , entry fileName ].
entry
extractInDirectory: aDirectory
informingOverwrite: overwriteAll
onSuccess: [ ]
onRetryWithOverwrite: [ overwriteAll := true ]
onFailedOverwrite: [ (self confirm: 'Failed to extract ' , entry fileName , '. Proceed?')
ifFalse: [ ^ self ] ]
onAbortOverwrite: [ ^ self ].
barValue := barValue + 1.
shouldUpdateInfos
ifTrue: [ bar value: barValue.
lastUpdate := Time millisecondClockValue ] ]
]
{ #category : #'archive operations' }
ZipArchive >> extractAllTo: aDirectory overwrite: overwriteAll [
"Extracts all elements to the given directory, overwriting existing entries if necessary. Does not use the UI for confirmation"
self members do: [ :entry | entry extractInDirectory: aDirectory withoutInformingOverwrite: overwriteAll ]
]
{ #category : #accessing }
ZipArchive >> hasMemberSuchThat: aBlock [
"Answer whether we have a member satisfying the given condition"
^self members anySatisfy: aBlock
]
{ #category : #initialization }
ZipArchive >> initialize [
super initialize.
writeEOCDOffset := writeCentralDirectoryOffset := 0.
zipFileComment := ''
]
{ #category : #private }
ZipArchive >> memberClass [
^ZipArchiveMember
]
{ #category : #accessing }
ZipArchive >> prependedDataSize [
"Answer the size of whatever data exists before my first member.
Assumes that I was read from a file or stream (i.e. the first member is a ZipFileMember)"
^members isEmpty
ifFalse: [ members first localHeaderRelativeOffset ]
ifTrue: [ centralDirectoryOffsetWRTStartingDiskNumber ]
]
{ #category : #private }
ZipArchive >> readEndOfCentralDirectoryFrom: aStream [
"Read EOCD, starting from position before signature."
| signature zipFileCommentLength endianStream |
signature := self readSignatureFrom: aStream.
signature = EndOfCentralDirectorySignature ifFalse: [ ^self error: 'bad signature at ', aStream position printString ].
endianStream := ZnEndianessReadWriteStream on: aStream.
endianStream nextLittleEndianNumber: 2. "# of this disk"
endianStream nextLittleEndianNumber: 2. "# of disk with central dir start"
endianStream nextLittleEndianNumber: 2. "# of entries in central dir on this disk"
endianStream nextLittleEndianNumber: 2. "total # of entries in central dir"
centralDirectorySize := endianStream nextLittleEndianNumber: 4. "size of central directory"
centralDirectoryOffsetWRTStartingDiskNumber := endianStream nextLittleEndianNumber: 4. "offset of start of central directory"
zipFileCommentLength := endianStream nextLittleEndianNumber: 2. "zip file comment"
zipFileComment := aStream next: zipFileCommentLength
]
{ #category : #reading }
ZipArchive >> readFrom: aStreamOrFileName [
| stream name eocdPosition |
stream := aStreamOrFileName isStream
ifTrue: [ name := aStreamOrFileName printString. aStreamOrFileName ]
ifFalse: [ File openForReadFileNamed: (name := aStreamOrFileName asFileReference fullName) ].
eocdPosition := self class findEndOfCentralDirectoryFrom: stream.
eocdPosition <= 0 ifTrue: [ ZipArchiveError signal: 'can''t find EOCD position' ].
self readEndOfCentralDirectoryFrom: stream.
stream position: eocdPosition - centralDirectorySize.
self readMembersFrom: stream named: name
]
{ #category : #private }
ZipArchive >> readMembersFrom: stream named: fileName [
[ | signature newMember |
newMember := self memberClass newFromZipFile: stream named: fileName.
signature := self readSignatureFrom: stream.
signature = EndOfCentralDirectorySignature ifTrue: [ ^self ].
signature = CentralDirectoryFileHeaderSignature
ifFalse: [ ZipArchiveError signal: 'bad CD signature at ', (stream position - 4) printStringHex ].
newMember readFrom: stream.
newMember looksLikeDirectory ifTrue: [ newMember := newMember asDirectory ].
self addMember: newMember.
] repeat
]
{ #category : #private }
ZipArchive >> readSignatureFrom: stream [
"Returns next signature from given stream, leaves stream positioned afterwards."
| signatureData |
signatureData := ByteArray new: 4.
stream next: 4 into: signatureData.
({ CentralDirectoryFileHeaderSignature . LocalFileHeaderSignature . EndOfCentralDirectorySignature }
includes: signatureData)
ifFalse: [ ^ ZipArchiveError signal: 'bad signature ', signatureData asString asHex, ' at position ', (stream position - 4) asString ].
^signatureData
]
{ #category : #private }
ZipArchive >> writeCentralDirectoryTo: aStream [
| offset |
offset := writeCentralDirectoryOffset.
members do: [ :member |
member writeCentralDirectoryFileHeaderTo: aStream.
offset := offset + member centralDirectoryHeaderSize.
].
writeEOCDOffset := offset.
self writeEndOfCentralDirectoryTo: aStream
]
{ #category : #private }
ZipArchive >> writeEndOfCentralDirectoryTo: aStream [
| endianStream |
aStream nextPutAll: EndOfCentralDirectorySignature.
endianStream := ZnEndianessReadWriteStream on: aStream.
endianStream nextLittleEndianNumber: 2 put: 0. "diskNumber"
endianStream nextLittleEndianNumber: 2 put: 0. "diskNumberWithStartOfCentralDirectory"
endianStream nextLittleEndianNumber: 2 put: members size. "numberOfCentralDirectoriesOnThisDisk"
endianStream nextLittleEndianNumber: 2 put: members size. "numberOfCentralDirectories"
endianStream nextLittleEndianNumber: 4 put: writeEOCDOffset - writeCentralDirectoryOffset. "size of central dir"
endianStream nextLittleEndianNumber: 4 put: writeCentralDirectoryOffset. "offset of central dir"
endianStream nextLittleEndianNumber: 2 put: zipFileComment size. "zip file comment"
zipFileComment isEmpty ifFalse: [ aStream nextPutAll: zipFileComment ]
]
{ #category : #writing }
ZipArchive >> writeTo: stream [
members do: [ :member |
member writeTo: stream.
member endRead.
].
writeCentralDirectoryOffset := stream position.
self writeCentralDirectoryTo: stream
]
{ #category : #writing }
ZipArchive >> writeTo: stream prepending: aString [
stream nextPutAll: aString.
members do: [ :member |
member writeTo: stream.
member endRead.
].
writeCentralDirectoryOffset := stream position.
self writeCentralDirectoryTo: stream
]
{ #category : #writing }
ZipArchive >> writeTo: stream prependingFileNamed: aFileName [
| prepended buffer |
prepended := File openForReadFileNamed: aFileName.
buffer := ByteArray new: (prepended size min: 32768).
[ prepended atEnd ] whileFalse: [ | bytesRead |
bytesRead := prepended readInto: buffer startingAt: 1 count: buffer size.
stream next: bytesRead putAll: buffer startingAt: 1
].
members do: [ :member |
member writeTo: stream.
member endRead.
].
writeCentralDirectoryOffset := stream position.
self writeCentralDirectoryTo: stream
]
{ #category : #writing }
ZipArchive >> writeToFileNamed: aFileName prepending: aString [
"Catch attempts to overwrite existing zip file"
(self canWriteToFileNamed: aFileName)
ifFalse: [ ^ ZipArchiveError signal: (aFileName, ' is needed by one or more members in this archive') ].
(File named: aFileName)
writeStreamDo: [ :stream | self writeTo: stream prepending: aString ]
]
{ #category : #writing }
ZipArchive >> writeToFileNamed: aFileName prependingFileNamed: anotherFileName [
"Catch attempts to overwrite existing zip file"
(self canWriteToFileNamed: aFileName)
ifFalse: [ ^ZipArchiveError signal: (aFileName, ' is needed by one or more members in this archive') ].
(File named: aFileName)
writeStreamDo: [ :stream | self writeTo: stream prependingFileNamed: anotherFileName ]
]
{ #category : #accessing }
ZipArchive >> zipFileComment [
^zipFileComment asString
]
{ #category : #accessing }
ZipArchive >> zipFileComment: aString [
zipFileComment := aString
]