util-zip.lua /size: 32 Kb    last modification: 2023-12-21 09:44
1if not modules then modules = { } end modules ['util-zip'] = {
2    version   = 1.001,
3    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
4    copyright = "PRAGMA ADE / ConTeXt Development Team",
5    license   = "see context related readme files"
6}
7
8-- This module is mostly meant for relative simple zip and unzip tasks. We can read
9-- and write zip files but with limitations. Performance is quite good and it makes
10-- us independent of zip tools, which (for some reason) are not always installed.
11--
12-- This is an lmtx module and at some point will be lmtx only but for a while we
13-- keep some hybrid functionality.
14
15local type, tostring, tonumber = type, tostring, tonumber
16local sort, concat = table.sort, table.concat
17
18local find, format, sub, gsub = string.find, string.format, string.sub, string.gsub
19local osdate, ostime, osclock = os.date, os.time, os.clock
20local ioopen = io.open
21local loaddata, savedata = io.loaddata, io.savedata
22local filejoin, isdir, dirname, mkdirs = file.join, lfs.isdir, file.dirname, dir.mkdirs
23local suffix, suffixes = file.suffix, file.suffixes
24local openfile = io.open
25
26gzip = gzip or { } -- so in luatex we keep the old ones too
27
28if not zlib then
29    zlib = xzip    -- in luametatex we shadow the old one
30elseif not xzip then
31    xzip = zlib
32end
33
34local files         = utilities.files
35local openfile      = files.open
36local closefile     = files.close
37local getsize       = files.size
38local readstring    = files.readstring
39local readcardinal2 = files.readcardinal2le
40local readcardinal4 = files.readcardinal4le
41local setposition   = files.setposition
42local getposition   = files.getposition
43local skipbytes     = files.skip
44
45local band          = bit32.band
46local rshift        = bit32.rshift
47local lshift        = bit32.lshift
48
49local zlibdecompress     = zlib.decompress
50local zlibdecompresssize = zlib.decompresssize
51local zlibchecksum       = zlib.crc32
52
53if not CONTEXTLMTXMODE or CONTEXTLMTXMODE == 0 then
54    local cs = zlibchecksum
55    zlibchecksum = function(str,n) return cs(n or 0, str) end
56end
57
58local decompress     = function(source)            return zlibdecompress    (source,-15)            end -- auto
59local decompresssize = function(source,targetsize) return zlibdecompresssize(source,targetsize,-15) end -- auto
60local calculatecrc   = function(buffer,initial)    return zlibchecksum      (initial or 0,buffer)   end
61
62local zipfiles      = { }
63utilities.zipfiles  = zipfiles
64
65local openzipfile, closezipfile, unzipfile, foundzipfile, getziphash, getziplist  do
66
67    function openzipfile(name)
68        return {
69            name   = name,
70            handle = openfile(name,0),
71        }
72    end
73
74 -- https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT
75
76--     local function collect(z)
77--         if not z.list then
78--             local list     = { }
79--             local hash     = { }
80--             local position = 0
81--             local index    = 0
82--             local handle   = z.handle
83--             while true do
84--                 setposition(handle,position)
85--                 local signature = readstring(handle,4)
86--                 if signature == "PK\3\4" then
87--                     -- [local file header 1]
88--                     -- [encryption header 1]
89--                     -- [file data 1]
90--                     -- [data descriptor 1]
91--                     local version      = readcardinal2(handle)
92--                     local flag         = readcardinal2(handle)
93--                     local method       = readcardinal2(handle)
94--                     local filetime     = readcardinal2(handle)
95--                     local filedate     = readcardinal2(handle)
96--                     local crc32        = readcardinal4(handle)
97--                     local compressed   = readcardinal4(handle)
98--                     local uncompressed = readcardinal4(handle)
99--                     local namelength   = readcardinal2(handle)
100--                     local extralength  = readcardinal2(handle)
101--                     local filename     = readstring(handle,namelength)
102--                     local descriptor   = band(flag,8) ~= 0
103--                     local encrypted    = band(flag,1) ~= 0
104--                     local acceptable   = method == 0 or method == 8
105--                     -- 30 bytes of header including the signature
106--                     local skipped      = 0
107--                     local size         = 0
108--                     if encrypted then
109--                         size = readcardinal2(handle)
110--                         skipbytes(handle,size)
111--                         skipped = skipped + size + 2
112--                         skipbytes(8)
113--                         skipped = skipped + 8
114--                         size = readcardinal2(handle)
115--                         skipbytes(handle,size)
116--                         skipped = skipped + size + 2
117--                         size = readcardinal4(handle)
118--                         skipbytes(handle,size)
119--                         skipped = skipped + size + 4
120--                         size = readcardinal2(handle)
121--                         skipbytes(handle,size)
122--                         skipped = skipped + size + 2
123--                     end
124--                     position = position + 30 + namelength + extralength + skipped
125--                  -- if descriptor then
126--                  --     -- where is this one located
127--                  --     setposition(handle,position + compressed)
128--                  --     crc32        = readcardinal4(handle)
129--                  --     compressed   = readcardinal4(handle)
130--                  --     uncompressed = readcardinal4(handle)
131--                  -- end
132--                     if acceptable then
133--                         index = index + 1
134--                         local data = {
135--                             filename     = filename,
136--                             index        = index,
137--                             position     = position,
138--                             method       = method,
139--                             compressed   = compressed,
140--                             uncompressed = uncompressed,
141--                             crc32        = crc32,
142--                             encrypted    = encrypted,
143--                         }
144--                         hash[filename] = data
145--                         list[index]    = data
146--                     else
147--                         -- maybe a warning when encrypted
148--                     end
149--                     position = position + compressed
150--                 else
151--                     break
152--                 end
153--                 z.list = list
154--                 z.hash = hash
155--             end
156--         end
157--     end
158-- end
159
160--         end
161--     end
162
163    local function update(handle,data)
164        position = data.offset
165        setposition(handle,position)
166        local signature = readstring(handle,4)
167        if signature == "PK\3\4" then -- 0x04034B50
168            -- [local file header 1]
169            -- [encryption header 1]
170            -- [file data 1]
171            -- [data descriptor 1]
172            local version      = readcardinal2(handle)
173            local flag         = readcardinal2(handle)
174            local method       = readcardinal2(handle)
175                                  skipbytes(handle,4)
176            ----- filetime     = readcardinal2(handle)
177            ----- filedate     = readcardinal2(handle)
178            local crc32        = readcardinal4(handle)
179            local compressed   = readcardinal4(handle)
180            local uncompressed = readcardinal4(handle)
181            local namelength   = readcardinal2(handle)
182            local extralength  = readcardinal2(handle)
183            local filename     = readstring(handle,namelength)
184            local descriptor   = band(flag,8) ~= 0
185            local encrypted    = band(flag,1) ~= 0
186            local acceptable   = method == 0 or method == 8
187            -- 30 bytes of header including the signature
188            local skipped      = 0
189            local size         = 0
190            if encrypted then
191                size = readcardinal2(handle)
192                skipbytes(handle,size)
193                skipped = skipped + size + 2
194                skipbytes(8)
195                skipped = skipped + 8
196                size = readcardinal2(handle)
197                skipbytes(handle,size)
198                skipped = skipped + size + 2
199                size = readcardinal4(handle)
200                skipbytes(handle,size)
201                skipped = skipped + size + 4
202                size = readcardinal2(handle)
203                skipbytes(handle,size)
204                skipped = skipped + size + 2
205            end
206            if acceptable then
207                    if                       filename     ~= data.filename     then
208             -- elseif                       method       ~= data.method       then
209             -- elseif                       encrypted    ~= data.encrypted    then
210             -- elseif crc32        ~= 0 and crc32        ~= data.crc32        then
211             -- elseif uncompressed ~= 0 and uncompressed ~= data.uncompressed then
212             -- elseif compressed   ~= 0 and compressed   ~= data.compressed   then
213                else
214                    position = position + 30 + namelength + extralength + skipped
215                    data.position = position
216                    return position
217                end
218            else
219                -- maybe a warning when encrypted
220            end
221        end
222        data.position = false
223        return false
224    end
225
226    local function collect(z)
227        if not z.list then
228            local list     = { }
229            local hash     = { }
230            local position = 0
231            local index    = 0
232            local handle   = z.handle
233            local size     = getsize(handle)
234            --
235            -- Not all files have the compressed into set so we need to get the directory
236            -- first. We only handle single disk zip files.
237            --
238            for i=size-4,size-64*1024,-1 do
239                setposition(handle,i)
240                local enddirsignature = readcardinal4(handle)
241                if enddirsignature == 0x06054B50 then
242                    local thisdisknumber    = readcardinal2(handle)
243                    local centraldisknumber = readcardinal2(handle)
244                    local thisnofentries    = readcardinal2(handle)
245                    local totalnofentries   = readcardinal2(handle)
246                    local centralsize       = readcardinal4(handle)
247                    local centraloffset     = readcardinal4(handle)
248                    local commentlength     = readcardinal2(handle)
249                    local comment           = readstring(handle,length)
250                    if size - i >= 22 then
251                        if thisdisknumber == centraldisknumber then
252                            setposition(handle,centraloffset)
253                            while true do
254                                if readcardinal4(handle) == 0x02014B50 then
255                                                          skipbytes(handle,4)
256                                    ----- versionmadeby = readcardinal2(handle)
257                                    ----- versionneeded = readcardinal2(handle)
258                                    local flag          = readcardinal2(handle)
259                                    local method        = readcardinal2(handle)
260                                                          skipbytes(handle,4)
261                                    ----- filetime      = readcardinal2(handle)
262                                    ----- filedate      = readcardinal2(handle)
263                                    local crc32         = readcardinal4(handle)
264                                    local compressed    = readcardinal4(handle)
265                                    local uncompressed  = readcardinal4(handle)
266                                    local namelength    = readcardinal2(handle)
267                                    local extralength   = readcardinal2(handle)
268                                    local commentlength = readcardinal2(handle)
269                                                          skipbytes(handle,8)
270                                    ----- disknumber    = readcardinal2(handle)
271                                    ----- intattributes = readcardinal2(handle)
272                                    ----- extattributes = readcardinal4(handle)
273                                    local headeroffset  = readcardinal4(handle)
274                                    local filename      = readstring(handle,namelength)
275                                                          skipbytes(handle,extralength+commentlength)
276                                    ----- extradata     = readstring(handle,extralength)
277                                    ----- comment       = readstring(handle,commentlength)
278                                    --
279                                    local descriptor   = band(flag,8) ~= 0
280                                    local encrypted    = band(flag,1) ~= 0
281                                    local acceptable   = method == 0 or method == 8
282                                    if acceptable then
283                                        index = index + 1
284                                        local data = {
285                                            filename     = filename,
286                                            index        = index,
287                                            position     = nil,
288                                            method       = method,
289                                            compressed   = compressed,
290                                            uncompressed = uncompressed,
291                                            crc32        = crc32,
292                                            encrypted    = encrypted,
293                                            offset       = headeroffset,
294                                        }
295                                        hash[filename] = data
296                                        list[index]    = data
297                                    end
298                                else
299                                    break
300                                end
301                            end
302                        end
303                        break
304                    end
305                end
306            end
307         -- for i=1,index do -- delayed
308         --     local data = list[i]
309         --     if not data.position then
310         --         update(handle,list[i])
311         --     end
312         -- end
313            z.list = list
314            z.hash = hash
315        end
316    end
317
318    function getziplist(z)
319        local list = z.list
320        if not list then
321            collect(z)
322        end
323     -- inspect(z.list)
324        return z.list
325    end
326
327    function getziphash(z)
328        local hash = z.hash
329        if not hash then
330            collect(z)
331        end
332        return z.hash
333    end
334
335    function foundzipfile(z,name)
336        return getziphash(z)[name]
337    end
338
339    function closezipfile(z)
340        local f = z.handle
341        if f then
342            closefile(f)
343            z.handle = nil
344        end
345    end
346
347    function unzipfile(z,filename,check)
348        local hash = z.hash
349        if not hash then
350            hash = zipfiles.hash(z)
351        end
352        local data = hash[filename] -- normalize
353        if not data then
354            -- lower and cleanup
355            -- only name
356        end
357        if data then
358            local handle     = z.handle
359            local position   = data.position
360            local compressed = data.compressed
361            if position == nil then
362                position = update(handle,data)
363            end
364            if position and compressed > 0 then
365                setposition(handle,position)
366                local result = readstring(handle,compressed)
367                if data.method == 8 then
368                    if decompresssize then
369                        result = decompresssize(result,data.uncompressed)
370                    else
371                        result = decompress(result)
372                    end
373                end
374                if check and data.crc32 ~= calculatecrc(result) then
375                    print("checksum mismatch")
376                    return ""
377                end
378                return result
379            else
380                return ""
381            end
382        end
383    end
384
385    zipfiles.open  = openzipfile
386    zipfiles.close = closezipfile
387    zipfiles.unzip = unzipfile
388    zipfiles.hash  = getziphash
389    zipfiles.list  = getziplist
390    zipfiles.found = foundzipfile
391
392end
393
394if xzip then -- flate then do
395
396    local writecardinal1 = files.writebyte
397    local writecardinal2 = files.writecardinal2le
398    local writecardinal4 = files.writecardinal4le
399
400    local logwriter      = logs.writer
401
402    local globpattern    = dir.globpattern
403--     local compress       = flate.flate_compress
404--     local checksum       = flate.update_crc32
405    local compress       = xzip.compress
406    local checksum       = xzip.crc32
407
408 -- local function fromdostime(dostime,dosdate)
409 --     return ostime {
410 --         year  = (dosdate >>  9) + 1980, -- 25 .. 31
411 --         month = (dosdate >>  5) & 0x0F, -- 21 .. 24
412 --         day   = (dosdate      ) & 0x1F, -- 16 .. 20
413 --         hour  = (dostime >> 11)       , -- 11 .. 15
414 --         min   = (dostime >>  5) & 0x3F, --  5 .. 10
415 --         sec   = (dostime      ) & 0x1F, --  0 ..  4
416 --     }
417 -- end
418 --
419 -- local function todostime(time)
420 --     local t = osdate("*t",time)
421 --     return
422 --         ((t.year - 1980) <<  9) + (t.month << 5) +  t.day,
423 --          (t.hour         << 11) + (t.min   << 5) + (t.sec >> 1)
424 -- end
425
426    local function fromdostime(dostime,dosdate)
427        return ostime {
428            year  =      rshift(dosdate, 9) + 1980,  -- 25 .. 31
429            month = band(rshift(dosdate, 5),  0x0F), -- 21 .. 24
430            day   = band(      (dosdate   ),  0x1F), -- 16 .. 20
431            hour  = band(rshift(dostime,11)       ), -- 11 .. 15
432            min   = band(rshift(dostime, 5),  0x3F), --  5 .. 10
433            sec   = band(      (dostime   ),  0x1F), --  0 ..  4
434        }
435    end
436
437    local function todostime(time)
438        local t = osdate("*t",time)
439        return
440            lshift(t.year - 1980, 9) + lshift(t.month,5) +        t.day,
441            lshift(t.hour       ,11) + lshift(t.min  ,5) + rshift(t.sec,1)
442    end
443
444    local function openzip(filename,level,comment,verbose)
445        local f = ioopen(filename,"wb")
446        if f then
447            return {
448                filename     = filename,
449                handle       = f,
450                list         = { },
451                level        = tonumber(level) or 3,
452                comment      = tostring(comment),
453                verbose      = verbose,
454                uncompressed = 0,
455                compressed   = 0,
456            }
457        end
458    end
459
460    local function writezip(z,name,data,level,time)
461        local f        = z.handle
462        local list     = z.list
463        local level    = tonumber(level) or z.level or 3
464        local method   = 8
465        local zipped   = compress(data,level)
466        local checksum = checksum(data)
467        local verbose  = z.verbose
468        --
469        if not zipped then
470            method = 0
471            zipped = data
472        end
473        --
474        local start        = f:seek()
475        local compressed   = #zipped
476        local uncompressed = #data
477        --
478        z.compressed   = z.compressed   + compressed
479        z.uncompressed = z.uncompressed + uncompressed
480        --
481        if verbose then
482            local pct = 100 * compressed/uncompressed
483            if pct >= 100 then
484                logwriter(format("%10i        %s",uncompressed,name))
485            else
486                logwriter(format("%10i  %02.1f  %s",uncompressed,pct,name))
487            end
488        end
489        --
490        f:write("\x50\x4b\x03\x04") -- PK..  0x04034b50
491        --
492        writecardinal2(f,0)            -- minimum version
493        writecardinal2(f,0)            -- flag
494        writecardinal2(f,method)       -- method
495        writecardinal2(f,0)            -- time
496        writecardinal2(f,0)            -- date
497        writecardinal4(f,checksum)     -- crc32
498        writecardinal4(f,compressed)   -- compressed
499        writecardinal4(f,uncompressed) -- uncompressed
500        writecardinal2(f,#name)        -- namelength
501        writecardinal2(f,0)            -- extralength
502        --
503        f:write(name)                  -- name
504        f:write(zipped)
505        --
506        list[#list+1] = { #zipped, #data, name, checksum, start, time or 0 }
507    end
508
509    local function closezip(z)
510        local f       = z.handle
511        local list    = z.list
512        local comment = z.comment
513        local verbose = z.verbose
514        local count   = #list
515        local start   = f:seek()
516        --
517        for i=1,count do
518            local l = list[i]
519            local compressed   = l[1]
520            local uncompressed = l[2]
521            local name         = l[3]
522            local checksum     = l[4]
523            local start        = l[5]
524            local time         = l[6]
525            local date, time   = todostime(time)
526            f:write('\x50\x4b\x01\x02')
527            writecardinal2(f,0)            -- version made by
528            writecardinal2(f,0)            -- version needed to extract
529            writecardinal2(f,0)            -- flags
530            writecardinal2(f,8)            -- method
531            writecardinal2(f,time)         -- time
532            writecardinal2(f,date)         -- date
533            writecardinal4(f,checksum)     -- crc32
534            writecardinal4(f,compressed)   -- compressed
535            writecardinal4(f,uncompressed) -- uncompressed
536            writecardinal2(f,#name)        -- namelength
537            writecardinal2(f,0)            -- extralength
538            writecardinal2(f,0)            -- commentlength
539            writecardinal2(f,0)            -- nofdisks -- ?
540            writecardinal2(f,0)            -- internal attr (type)
541            writecardinal4(f,0)            -- external attr (mode)
542            writecardinal4(f,start)        -- local offset
543            f:write(name)                  -- name
544        end
545        --
546        local stop = f:seek()
547        local size = stop - start
548        --
549        f:write('\x50\x4b\x05\x06')
550        writecardinal2(f,0)            -- disk
551        writecardinal2(f,0)            -- disks
552        writecardinal2(f,count)        -- entries
553        writecardinal2(f,count)        -- entries
554        writecardinal4(f,size)         -- dir size
555        writecardinal4(f,start)        -- dir offset
556        if type(comment) == "string" and comment ~= "" then
557            writecardinal2(f,#comment) -- comment length
558            f:write(comment)           -- comemnt
559        else
560            writecardinal2(f,0)
561        end
562        --
563        if verbose then
564            local compressed   = z.compressed
565            local uncompressed = z.uncompressed
566            local filename     = z.filename
567            --
568            local pct = 100 * compressed/uncompressed
569            logwriter("")
570            if pct >= 100 then
571                logwriter(format("%10i        %s",uncompressed,filename))
572            else
573                logwriter(format("%10i  %02.1f  %s",uncompressed,pct,filename))
574            end
575        end
576        --
577        f:close()
578    end
579
580    local function zipdir(zipname,path,level,verbose)
581        if type(zipname) == "table" then
582            verbose = zipname.verbose
583            level   = zipname.level
584            path    = zipname.path
585            zipname = zipname.zipname
586        end
587        if not zipname or zipname == "" then
588            return
589        end
590        if not path or path == "" then
591            path = "."
592        end
593        if not isdir(path) then
594            return
595        end
596        path = gsub(path,"\\+","/")
597        path = gsub(path,"/+","/")
598        local list  = { }
599        local count = 0
600        globpattern(path,"",true,function(name,size,time)
601            count = count + 1
602            list[count] = { name, time }
603        end)
604        sort(list,function(a,b)
605            return a[1] < b[1]
606        end)
607        local zipf = openzip(zipname,level,comment,verbose)
608        if zipf then
609            local p = #path + 2
610            for i=1,count do
611                local li   = list[i]
612                local name = li[1]
613                local time = li[2]
614                local data = loaddata(name)
615                local name = sub(name,p,#name)
616                writezip(zipf,name,data,level,time,verbose)
617            end
618            closezip(zipf)
619        end
620    end
621
622    local function unzipdir(zipname,path,verbose,collect,validate)
623        if type(zipname) == "table" then
624            validate = zipname.validate
625            collect  = zipname.collect
626            verbose  = zipname.verbose
627            path     = zipname.path
628            zipname  = zipname.zipname
629        end
630        if not zipname or zipname == "" then
631            return
632        end
633        if not path or path == "" then
634            path = "."
635        end
636        local z = openzipfile(zipname)
637        if z then
638            local list = getziplist(z)
639            if list then
640                local total = 0
641                local count = #list
642                local step  = number.idiv(count,10)
643                local done  = 0
644                local steps = verbose == "steps"
645                local time  = steps and osclock()
646             -- local skip  = 0
647                if collect then
648                    collect = { }
649                else
650                    collect = false
651                end
652                for i=1,count do
653                    local l = list[i]
654                    local n = l.filename
655                    if not validate or validate(n) then
656                        local d = unzipfile(z,n) -- true for check
657                        if d then
658                            local p = filejoin(path,n)
659                            if mkdirs(dirname(p)) then
660                                if steps then
661                                    total = total + #d
662                                    done = done + 1
663                                    if done >= step then
664                                        done = 0
665                                        logwriter(format("%4i files of %4i done, %10i bytes, %0.3f seconds",i,count,total,osclock()-time))
666                                    end
667                                elseif verbose then
668                                    logwriter(n)
669                                end
670                                savedata(p,d)
671                                if collect then
672                                    collect[#collect+1] = p
673                                end
674                            end
675                        else
676                            logwriter(format("problem with file %s",n))
677                        end
678                    else
679                     -- skip = skip + 1
680                    end
681                end
682                if steps then
683                    logwriter(format("%4i files of %4i done, %10i bytes, %0.3f seconds",count,count,total,osclock()-time))
684                end
685                closezipfile(z)
686                if collect then
687                    return collect
688                end
689            else
690                closezipfile(z)
691            end
692        end
693    end
694
695    zipfiles.zipdir   = zipdir
696    zipfiles.unzipdir = unzipdir
697
698end
699
700-- todo: compress/decompress that work with offset in string
701
702-- We only have a few official methods here:
703--
704--   local decompressed = gzip.load       (filename)
705--   local resultsize   = gzip.save       (filename,compresslevel)
706--   local compressed   = gzip.compress   (str,compresslevel)
707--   local decompressed = gzip.decompress (str)
708--   local iscompressed = gzip.compressed (str)
709--   local suffix, okay = gzip.suffix     (filename)
710--
711-- In LuaMetaTeX we have only xzip which implements a very few methods:
712--
713--   compress   (str,level,method,window,memory,strategy)
714--   decompress (str,window)
715--   adler32    (str,checksum)
716--   crc32      (str,checksum)
717
718local pattern   = "^\x1F\x8B\x08"
719local gziplevel = 3
720
721function gzip.suffix(filename)
722    local suffix, extra = suffixes(filename)
723    local gzipped = extra == "gz"
724    return suffix, gzipped
725end
726
727function gzip.compressed(s)
728    return s and find(s,pattern)
729end
730
731local getdecompressed
732local putcompressed
733
734if gzip.compress then
735
736    local gzipwindow = 15 + 16 -- +16: gzip, +32: gzip|zlib
737
738    local compress   = zlib.compress
739    local decompress = zlib.decompress
740
741    getdecompressed = function(str)
742        return decompress(str,gzipwindow) -- pass offset
743    end
744
745    putcompressed = function(str,level)
746        return compress(str,level or gziplevel,nil,gzipwindow)
747    end
748
749else
750
751    -- Special window values are: flate: -15, zlib: 15, gzip : -15
752
753    local gzipwindow = -15 -- miniz needs this
754    local identifier = "\x1F\x8B"
755
756    local compress      = zlib.compress
757    local decompress    = zlib.decompress
758    local zlibchecksum  = zlib.crc32
759
760    if not CONTEXTLMTXMODE or CONTEXTLMTXMODE == 0 then
761        local cs = zlibchecksum
762        zlibchecksum = function(str,n) return cs(n or 0, str) end
763    end
764
765    local streams       = utilities.streams
766    local openstream    = streams.openstring
767    local closestream   = streams.close
768    local getposition   = streams.getposition
769    local readbyte      = streams.readbyte
770    local readcardinal4 = streams.readcardinal4le
771    local readcardinal2 = streams.readcardinal2le
772    local readstring    = streams.readstring
773    local readcstring   = streams.readcstring
774    local skipbytes     = streams.skip
775
776    local tocardinal1   = streams.tocardinal1
777    local tocardinal4   = streams.tocardinal4le
778
779    getdecompressed = function(str)
780        local s = openstream(str)
781        local identifier  = readstring(s,2)
782        local method      = readbyte(s,1)
783        local flags       = readbyte(s,1)
784        local timestamp   = readcardinal4(s)
785        local compression = readbyte(s,1)
786        local operating   = readbyte(s,1)
787     -- local isjusttext  = (flags & 0x01 ~= 0) and true             or false
788     -- local extrasize   = (flags & 0x04 ~= 0) and readcardinal2(s) or 0
789     -- local filename    = (flags & 0x08 ~= 0) and readcstring(s)   or ""
790     -- local comment     = (flags & 0x10 ~= 0) and readcstring(s)   or ""
791     -- local checksum    = (flags & 0x02 ~= 0) and readcardinal2(s) or 0
792        local isjusttext  = band(flags,0x01) ~= 0 and true             or false
793        local extrasize   = band(flags,0x04) ~= 0 and readcardinal2(s) or 0
794        local filename    = band(flags,0x08) ~= 0 and readcstring(s)   or ""
795        local comment     = band(flags,0x10) ~= 0 and readcstring(s)   or ""
796        local checksum    = band(flags,0x02) ~= 0 and readcardinal2(s) or 0
797        local compressed  = readstring(s,#str)
798        local data = decompress(compressed,gzipwindow) -- pass offset
799        return data
800    end
801
802    putcompressed = function(str,level,originalname)
803        return concat {
804            identifier, -- 2 identifier
805            tocardinal1(0x08), -- 1 method
806            tocardinal1(0x08), -- 1 flags
807            tocardinal4(os.time()), -- 4 mtime
808            tocardinal1(0x02), -- 1 compression (2 or 4)
809            tocardinal1(0xFF), -- 1 operating
810            (originalname or "unknownname") .. "\0",
811            compress(str,level,nil,gzipwindow),
812            tocardinal4(zlibchecksum(str)), -- 4
813            tocardinal4(#str), -- 4
814        }
815    end
816
817end
818
819function gzip.load(filename)
820    local f = openfile(filename,"rb")
821    if not f then
822        -- invalid file
823    else
824        local data = f:read("*all")
825        f:close()
826        if data and data ~= "" then
827            if suffix(filename) == "gz" then
828                data = getdecompressed(data)
829            end
830            return data
831        end
832    end
833end
834
835function gzip.save(filename,data,level,originalname)
836    if suffix(filename) ~= "gz" then
837        filename = filename .. ".gz"
838    end
839    local f = openfile(filename,"wb")
840    if f then
841        data = putcompressed(data or "",level or gziplevel,originalname)
842        f:write(data)
843        f:close()
844        return #data
845    end
846end
847
848function gzip.compress(s,level)
849    if s and not find(s,pattern) then
850        if not level then
851            level = gziplevel
852        elseif level <= 0 then
853            return s
854        elseif level > 9 then
855            level = 9
856        end
857        return putcompressed(s,level or gziplevel) or s
858    end
859end
860
861function gzip.decompress(s)
862    if s and find(s,pattern) then
863        return getdecompressed(s)
864    else
865        return s
866    end
867end
868
869return zipfiles
870