libzip
 
Easy-to-use library for creating, reading out or modifying .zip archives.

Website: https://libzip.org
Github: https://github.com/nih-at/libzip
Platforms supported: Windows (32-bit and 64-bit), Linux, FreeBSD, Haiku
Headers to include: zip.bi
Header version: 1.0.1
Examples: examples/manual/libraries/zip2.bas and examples/compression/libzip.bas

The packing and extraction examples were run on FreeBSD with libzip 1.11.4.
Both archive entries were written, extracted into a new directory and
byte-compared with the source text.

Haiku R1 beta 6 amd64: `examples/manual/libraries/zip2.bas` and
`examples/compression/libzip.bas` compiled and ran with FreeBASIC 1.20.4-3
against HaikuPorts libzip 1.11.4. Both archive entries were extracted and
byte-compared with their source text.

The extraction examples accept an archive filename and a new destination directory (defaults: test.zip and test-unpacked). They validate relative path components, reject parent/root/drive/device paths and duplicate files, and refuse an existing destination. Directory entries are created as directories. The demonstration limits extraction to 10,000 entries and one GiB of decoded bytes before writing data. It assumes that the fresh destination is not concurrently modified; it does not create archive symlinks, restore attributes or provide a transactional rollback of all prior entries.

Each entry checks read, CRC/close, byte-count and output errors, removes its own failed output file, and returns a failing process status. Archive indices and decoded sizes retain the libzip 64-bit types. Existing destination files are preserved.

The packing example accepts an optional archive filename (default 26.zip). zip_source_buffer borrows the FreeBASIC text until zip_close completes because freep is zero. A successful zip_file_add transfers source ownership to libzip; a failed add requires zip_source_free. A failed zip_close retains the archive handle and requires zip_discard. The two named text entries are deliberately overwritten when updating an existing archive.

See the upstream buffer source, file add, and archive close contracts.

Example

'' FreeBASIC libzip examples
'' File: zip.bas
'' Purpose: Extract regular ZIP entries into a fresh destination.
'' Responsibilities: Validate relative paths and sizes, check reads/writes, release handles.
'' This file contains no link creation, file attributes or concurrent extraction.

#include Once "zip.bi"
#include Once "file.bi"
#include Once "dir.bi"

'' The example bounds disk use before extracting any data. These are demo
'' limits, not libzip limits: 10,000 entries and one GiB of decoded bytes.
Const MAX_ENTRIES As zip_int64_t = 10000
Const MAX_TOTAL_SIZE As zip_uint64_t = 1024ull * 1024 * 1024
Const BUFFER_SIZE = 1024 * 512

'' Archive separators are normalized before component validation. A fresh output
'' directory prevents following preexisting links or overwriting unrelated files.
'' The caller must not concurrently modify that directory during extraction.
Private Function checked_name(ByRef filename As String) As Boolean
    If Len(filename) = 0 Then Return False
    For position As Integer = 0 To Len(filename)-1
        If filename[position] = Asc("\") Then filename[position] = Asc("/")
        If filename[position] < 32 Or filename[position] = Asc(":") Or _
           filename[position] = Asc("*") Or filename[position] = Asc("?") Then Return False
    Next
    If Left(filename, 1) = "/" Then Return False
    Dim first As Integer = 1
    Do While first <= Len(filename)
        Dim separator As Integer = InStr(first, filename, "/")
        Dim component As String
        If separator = 0 Then
            component = Mid(filename, first)
        Else
            component = Mid(filename, first, separator-first)
        End If
        If component = "" Or component = "." Or component = ".." Then Return False
        #ifdef __FB_WIN32__
            '' Win32 normalizes trailing dots/spaces and interprets device_name names.
            If Right(component, 1) = "." Or Right(component, 1) = " " Then Return False
            Dim device_name As String = UCase(component)
            Dim dot As Integer = InStr(device_name, ".")
            If dot <> 0 Then device_name = Left(device_name, dot-1)
            device_name = RTrim(device_name)
            If device_name = "CON" Or device_name = "PRN" Or device_name = "AUX" Or device_name = "NUL" Or _
               device_name = "CONIN$" Or device_name = "CONOUT$" Then Return False
            If Left(device_name, 3) = "COM" Or Left(device_name, 3) = "LPT" Then
                Dim suffix As String = Mid(device_name, 4)
                If Len(suffix) = 1 AndAlso suffix[0] >= Asc("1") And suffix[0] <= Asc("9") Then Return False
                '' Win32 also recognizes the superscript 1, 2 and 3 aliases.
                If suffix = Chr(&hc2, &hb9) Or suffix = Chr(&hc2, &hb2) Or suffix = Chr(&hc2, &hb3) Then Return False
            End If
        #endif
        If separator = 0 Then Exit Do
        first = separator+1
    Loop
    Return True
End Function

Private Function create_directory(ByRef directory As String) As Boolean
    If MkDir(directory) = 0 Then Return True
    '' DIR's output mask distinguishes a directory from an existing file.
    '' Use the built-in API supported by the installed 32-bit runtime too.
    Dim attributes As Integer
    Dim item As String = Dir(directory, fbDirectory Or fbArchive Or fbReadOnly Or fbHidden Or fbSystem, attributes)
    Return Len(item) <> 0 AndAlso (attributes And fbDirectory) <> 0
End Function

Private Function create_parent_dirs(ByRef filename As String) As Boolean
    Dim separator As Integer = InStr(filename, "/")
    Do While separator <> 0
        Dim directory As String = Left(filename, separator-1)
        '' A drive prefix or root separator is part of the caller's destination.
        If directory <> "" And Right(directory, 1) <> ":" Then
            If Not create_directory(directory) Then Return False
        End If
        separator = InStr(separator+1, filename, "/")
    Loop
    Return True
End Function

Private Function unpack_zip_file(ByVal archive As zip_t Ptr, ByVal index As zip_uint64_t, _
                                 ByRef destination As String) As Boolean
    Dim archive_name As Const ZString Ptr = zip_get_name(archive, index, 0)
    If archive_name = NULL Then Return False
    Dim filename As String = *archive_name
    If Not checked_name(filename) Then Return False
    Dim directory_entry As Boolean = Right(filename, 1) = "/"
    Dim output_name As String = destination & "/" & filename
    If directory_entry Then
        output_name = Left(output_name, Len(output_name)-1)
        Return create_parent_dirs(output_name) AndAlso create_directory(output_name)
    End If
    If Not create_parent_dirs(output_name) Then Return False
    If FileExists(output_name) Then
        Print "refusing existing output: "; output_name
        Return False
    End If

    Dim stat As zip_stat_t
    zip_stat_init(@stat)
    If zip_stat_index(archive, index, 0, @stat) <> 0 Then Return False
    If (stat.valid And ZIP_STAT_SIZE) = 0 Then Return False
    Dim input_file As zip_file_t Ptr = zip_fopen_index(archive, index, 0)
    If input_file = NULL Then Return False
    Dim output_file As Integer = FreeFile()
    If Open(output_name, For Binary, Access Write, As #output_file) <> 0 Then
        zip_fclose(input_file)
        Return False
    End If

    '' One extraction call owns this buffer and both open files.
    Static chunk(0 To BUFFER_SIZE-1) As UByte
    Dim total As zip_uint64_t = 0
    Dim succeeded As Boolean = True
    Do
        Dim bytes As zip_int64_t = zip_fread(input_file, @chunk(0), BUFFER_SIZE)
        If bytes < 0 Or bytes > BUFFER_SIZE Then
            succeeded = False
            Exit Do
        End If
        If bytes = 0 Then Exit Do
        If total > stat.size OrElse bytes > stat.size-total Then
            succeeded = False
            Exit Do
        End If
        If Put(#output_file, , chunk(0), CInt(bytes)) <> 0 Then
            succeeded = False
            Exit Do
        End If
        total += bytes
    Loop
    If zip_fclose(input_file) <> 0 Then succeeded = False
    If Close(#output_file) <> 0 Then succeeded = False
    If total <> stat.size Then succeeded = False
    If Not succeeded Then
        '' This name was created by this call in the fresh destination only.
        Kill output_name
        Return False
    End If
    Print "file: "; filename; ", "; total; " bytes"
    Return True
End Function

Function unpack_zip(ByRef archive_name As String, ByRef destination As String) As Integer
    Dim archive As zip_t Ptr = zip_open(archive_name, ZIP_CHECKCONS, NULL)
    If archive = NULL Then
        Print "could not open input file "; archive_name
        Return 1
    End If
    Dim entries As zip_int64_t = zip_get_num_entries(archive, 0)
    Dim total_size As zip_uint64_t = 0
    Dim succeeded As Boolean = entries >= 0 And entries <= MAX_ENTRIES
    '' Validate the complete name/size list before creating output directories.
    If succeeded Then
        For index As zip_int64_t = 0 To entries-1
            Dim entry_name As Const ZString Ptr = zip_get_name(archive, index, 0)
            If entry_name = NULL Then
                succeeded = False
                Exit For
            End If
            Dim filename As String = *entry_name
            If Not checked_name(filename) Then
                Print "invalid archive path: "; filename
                succeeded = False
                Exit For
            End If
            Dim stat As zip_stat_t
            zip_stat_init(@stat)
            If zip_stat_index(archive, index, 0, @stat) <> 0 Then
                succeeded = False
                Exit For
            End If
            If (stat.valid And ZIP_STAT_SIZE) = 0 OrElse stat.size > MAX_TOTAL_SIZE-total_size Then
                Print "extraction size limit exceeded"
                succeeded = False
                Exit For
            End If
            If Right(filename, 1) = "/" And stat.size <> 0 Then
                Print "directory entry contains file data"
                succeeded = False
                Exit For
            End If
            total_size += stat.size
        Next
    End If
    If Not succeeded Then
        Print "archive validation failed"
        zip_discard(archive)
        Return 1
    End If

    '' Never reuse an existing destination, including a directory link.
    If Len(destination) = 0 OrElse MkDir(destination) <> 0 Then
        Print "destination must be a new directory: "; destination
        zip_discard(archive)
        Return 1
    End If
    For index As zip_int64_t = 0 To entries-1
        If Not unpack_zip_file(archive, index, destination) Then
            Print "entry extraction failed"
            succeeded = False
            Exit For
        End If
    Next
    If zip_close(archive) <> 0 Then
        '' A failed close retains the archive; discard releases it.
        zip_discard(archive)
        succeeded = False
    End If
    If Not succeeded Then Return 1
    Return 0
End Function

Dim archive_name As String = Command(1)
Dim destination As String = Command(2)
If archive_name = "" Then archive_name = "test.zip"
If destination = "" Then destination = "test-unpacked"
End unpack_zip(archive_name, destination)

'' end of zip.bas

'' FreeBASIC libzip examples
'' File: zip2.bas
'' Purpose: Add text strings to a ZIP archive.
'' Responsibilities: Check commit errors and transfer ownership of each zip source.
'' This file contains no binary-buffer input or archive extraction.

#include Once "zip.bi"

Function zipAddFileFromString(ByVal filename As ZString Ptr, ByVal entry As ZString Ptr, _
                              ByVal text As ZString Ptr) As Boolean
    If filename = NULL Or entry = NULL Or text = NULL Then Return False
    Dim archive As zip_t Ptr = zip_open(filename, ZIP_CREATE, NULL)
    If archive = NULL Then
        Print "could not open output archive"
        Return False
    End If
    '' freep=0 borrows this FreeBASIC-owned text until zip_close completes.
    '' libzip must not release it with its own allocator.
    Dim source As zip_source_t Ptr = zip_source_buffer(archive, text, Len(*text), 0)
    If source = NULL Then
        zip_discard(archive)
        Return False
    End If
    If zip_file_add(archive, entry, source, ZIP_FL_OVERWRITE) < 0 Then
        '' Failed insertion retains caller ownership; successful insertion transfers it.
        zip_source_free(source)
        zip_discard(archive)
        Return False
    End If
    If zip_close(archive) <> 0 Then
        Print "could not commit output archive"
        zip_discard(archive)
        Return False
    End If
    Print "OK"
    Return True
End Function

Dim text As String = "<My text           >......6...3..2"
Dim archive_name As String = Command(1)
If archive_name = "" Then archive_name = "26.zip"
If Not zipAddFileFromString(archive_name, "file3.txt", text) Then End 1
If Not zipAddFileFromString(archive_name, "BIMBO/file3.txt", text) Then End 1
Sleep

'' end of zip2.bas