Difference between revisions of "ZIM file format"

From openZIM
Jump to: navigation, search
(Header)
(Clusters: Clarification around the cluster compression types)
 
(89 intermediate revisions by 10 users not shown)
Line 1: Line 1:
The ZIM file format is based on the [[Zeno File Format]]. It starts with a header, which is described here:
+
Beginning 2021, we change the way we handle namespaces in ZIM file format.
  
 +
This is major change in the way we handle entries in libzim and the semantics around but it is not a break in the binary zim format. Old library/readers are compatible with new zim files.
 +
 +
This page describe the new format. The old format can be found here : [[ZIM file format old namespace]].[[Image:Schema File Format.png|500px|right]]
 
== Header ==
 
== Header ==
 +
A ZIM archive starts with a header :
 +
 +
{| class="sortable" style="border-width:1px; border-style:solid; border-color:#888888; background-color:#eeeeee; border-collapse:collapse; empty-cells:show" cellspacing="0" cellpadding="4" {{Prettytable}}
 +
! Field Name !! Type !! Offset !! Length !! Description               
 +
|-
 +
| magicNumber || integer || 0 || 4 || Magic number to recognise the file format, must be 72173914 (0x44D495A)
 +
|-
 +
|majorVersion
 +
|integer
 +
|4
 +
|2
 +
|Major version of the ZIM archive format (6)
 +
|-
 +
| minorVersion || integer || 6 || 2 || Minor version of the ZIM archive format (1 for new namespace usage, 0 for old namespace usage)                       
 +
|-
 +
| uuid || integer || 8 || 16 || unique id of this zim archive                         
 +
|-
 +
| entryCount || integer || 24 || 4 || total number of entries                 
 +
|-
 +
| clusterCount || integer || 28 || 4 || total number of clusters                 
 +
|-
 +
| urlPtrPos || integer || 32 || 8 || position of the directory pointerlist ordered by URL                   
 +
|-
 +
| titlePtrPos || integer || 40 || 8 || position of the directory pointerlist ordered by Title
 +
This is considered as obsolete, readers should use <code>[[Search indexes#Title index v0|X/listing/titleordered/v0]]</code> instead and fallback to <code>titlePtrPos</code> if entry is not present.
 +
|-
 +
| clusterPtrPos || integer || 48 || 8 || position of the cluster pointer list               
 +
|-
 +
| mimeListPos || integer || 56 || 8 || position of the MIME type list (also header size)                 
 +
|-
 +
| mainPage || integer || 64 || 4 || main page or 0xffffffff if no main page                     
 +
|-
 +
| layoutPage || integer || 68 || 4 || [[Layout Page|layout page]] or 0xffffffffff if no layout page (deprecated, always 0xffffffffff)                   
 +
|-
 +
| checksumPos || integer || 72 || 8 || pointer to the md5checksum of this archive without the checksum itself. This points always 16 bytes before the end of the archive.
 +
|}
 +
 +
Major version is updated when an incompatible change is integrated in the format (a lib made for a version N will probably not be able to read a version N+1)
 +
 +
Minor version is updated when an compatible change is integrated (a lib made for a minor version n will be able to read a version n+1)
  
A ZIM file starts with a header. This starts always at offset 0.
+
The current major version is 6. You may found old zim archives with major version 5. They are the same than 6 less extended cluster, so you can read a 5 major version as if it was a 6.
  
Length in byte, all types are littlendian
+
The minor version can be :
 +
* 0 : We use the old namespace usage (see [[ZIM file format old namespace]])
 +
* 1 : We use the new namespace usage (describe here).
 +
A zim archive may be embedded in another file at a specific offset. In the context of zim format, the start of the zim header is the offset 0. Readers allowing to read an embedded archive must adapt offset accordingly.
  
{|{{Prettytable}}
+
== MIME Type List (mimeListPos) ==
! Field Name !! Type !! Offset !! Length !! Description
+
The MIME type list always follows directly after the header, so the ''mimeListPos'' also defines the end and size of the ZIM file header.
 +
 
 +
The MIME types in this list are zero terminated strings. An empty string marks the end of the MIME type list.
 +
 
 +
{| class="sortable" style="border-width:1px; border-style:solid; border-color:#888888; background-color:#eeeeee; border-collapse:collapse; empty-cells:show" cellspacing="0" cellpadding="4" {{Prettytable}}
 +
! Field Name !! Type !!Offset!!Length!! Description                
 
|-
 
|-
| magicNumber            || integer || 0 || 4 || Magic number to recognise the file format, must be 72173914
+
| <1st MIME Type> || string || 0 ||zero terminated|| declaration of the <1st MIME Type>             
 
|-
 
|-
| version                || integer || 4 || 4 || ZIM=4, version of the file format
+
| <2nd MIME Type> || string || n/a ||zero terminated|| declaration of the <2nd MIME Type>           
 
|-
 
|-
| uuid                  || integer || 8 || 16 || unique id of this zim file
+
| ... || string || ... ||zero terminated|| ...                       
 
|-
 
|-
| count                  || integer || 24 || 4 || total number of articles
+
| <last entry / end> || string || n/a ||zero terminated|| empty string - end of MIME type list       
 +
|}
 +
 
 +
== URL Pointer List (urlPtrPos) ==
 +
The URL pointer list is a list of 8 byte offsets to the directory entries.
 +
 
 +
The directory entries are always ordered by "full" URL (<code><namespace><path></code>). Ordering is simply done by comparing the URL strings.
 +
 
 +
Since directory entries have variable sizes this is needed for random access.
 +
 
 +
{| class="sortable" style="border-width:1px; border-style:solid; border-color:#888888; background-color:#eeeeee; border-collapse:collapse; empty-cells:show" cellspacing="0" cellpadding="4" {{Prettytable}}
 +
! Field Name !! Type !!Offset!!Length!! Description               
 
|-
 
|-
| indexPtrPos            || integer || 28 || 8 || position of the directory pointerlist
+
| <1st URL> || integer || 0 || 8 || pointer to the directory entry of <1st URL>                     
 
|-
 
|-
| clusterCount          || integer || 36 || 4 || number of data clusters
+
| <2nd URL> || integer || 8 || 8 || pointer to the directory entry of <2nd URL>                     
 
|-
 
|-
| clusterPtrPos          || integer || 40 || 8 || position of the cluster pointer list
+
| <nth URL> || integer ||(n-1)*8|| 8 || pointer to the directory entry of <nth URL>               
 
|-
 
|-
| mainPage              || integer || 48 || 4 || article index of main page or 0xffffffff if no main page
+
| ... || integer || ... || 8 || ...                         
|-
 
| layoutPage            || integer || 52 ||  4 || article index of layout page or 0xffffffffff if no layout page
 
 
|}
 
|}
  
Each article in the zim file has a directory entry. Since the directory entry has a variable size we have an index pointerlist which is a list of 4-byte offsets. The pointers points to the directory entries.
+
Zimlib caches directory entries and references the cached entries via the URL pointers.
  
== Index pointer list ==
+
== Title Pointer List (titlePtrPos) ==
 +
The title pointer list is a list of entry indices ordered by title (<code><namespace><title></code>). The title pointer list actually points to entries in the URL pointer list.
  
The index pointer list is a list of 8 byte offsets to the directory entries. Since directory entries have variable sizes this is needed for random access.
+
Note that the title pointers are only 4 bytes. They are not offsets in the file but entry numbers.
  
The directory entries are always sorted by title. The title is encoded as [[QUnicode]], which is a custom utf-variant, which supports fast ordering.
+
To get the offset of an entry from the title pointer list, you have to look it up in the URL pointer list.
  
== Cluster pointer list ==
+
{| class="sortable" style="border-width:1px; border-style:solid; border-color:#888888; background-color:#eeeeee; border-collapse:collapse; empty-cells:show" cellspacing="0" cellpadding="4" {{Prettytable}}
 
+
! Field Name !! Type !!Offset!!Length!! Description               
The cluster pointer list is a list of 8 byte offsets which point to the data clusters.
+
|-
 +
| <1st Title> || integer || 0 || 4 || pointer to the URL pointer of <1st Title>                   
 +
|-
 +
| <2nd Title> || integer || 4 || 4 || pointer to the URL pointer of <2nd Title>                   
 +
|-
 +
| <nth Title> || integer ||(n-1)*4|| 4 || pointer to the URL pointer of <nth Title>             
 +
|-
 +
| ... || integer || ... || 4 || ...                         
 +
|}
  
== Directory entries ==
+
The indirection from titles via URLs to directory entries has two reasons:
 +
* the pointer list is only half in size as 4 bytes are enough for each entry
 +
* accessing directory entries by title also makes use of cached directory entries which are referenced by the URL pointers, as implemented in zimlib.
  
length in byte, all types are littlendian
+
== Directory Entries ==
 +
Directory entries hold the meta information about all entries, images and other objects in a ZIM archive.
  
=== article entry ===
+
There are different types of directory entries:
  
{|{{Prettytable}}
+
=== Content Entry ===
! Field Name !! Type !! Offset !! Length !! Description
+
{| class="sortable" style="border-width:1px; border-style:solid; border-color:#888888; background-color:#eeeeee; border-collapse:collapse; empty-cells:show" cellspacing="0" cellpadding="4" {{Prettytable}}
 +
! Field Name !! Type !!Offset!!Length!! Description                
 
|-
 
|-
| redirectFlag || boolean || 0 || 1 || 0 for article
+
| mimetype || integer || 0 || 2 || MIME type number as defined in the MIME type list                     
 
|-
 
|-
| mime || integer || 1 || 1 || mime type code
+
| parameter len || byte || 2 || 1 || (not used) length of extra paramters (must be 0)                     
 
|-
 
|-
| empty || || 2 || 1 || was compression flag, this is now in the cluster header
+
| namespace || char || 3 || 1 || defines to which namespace this directory entry belongs                         
 
|-
 
|-
| namespace || char || 3 || 1 ||
+
| revision || integer || 4 || 4 || (not used) identifies a revision of the contents of this directory entry, needed to identify updates or revisions in the original history (must be 0)                     
 
|-
 
|-
| cluster number || integer || 4 || 4 ||
+
| cluster number || integer || 8 || 4 || cluster number in which the data of this directory entry is stored               
 
|-
 
|-
| blob number || integer || 8 || 4 ||
+
| blob number || integer || 12 || 4 || blob number inside the compressed cluster where the contents are stored                 
 
|-
 
|-
| extraLen          || integer || 12 || 2 || length of extra bytes (title and parameter)
+
| url || string || 16 ||zero terminated|| string with the URL as refered in the URL pointer list                       
 
|-
 
|-
| title + parameter separated by 0-byte || [[QUnicode]] (title) + custom (parameter, not used in articles) || 14 || specified by extraLen || actual title of article; when parameter is empty, the 0-byte is omitted
+
| title || string || n/a ||zero terminated|| string with an title as refered in the Title pointer list or empty; in case it is empty, the URL is used as title                     
 
|-
 
|-
 +
| parameter || data || ||see parameter len|| (not used) extra parameters                       
 
|}
 
|}
  
=== redirect entry ===
+
=== Redirect Entry ===
 
+
{| class="sortable" style="border-width:1px; border-style:solid; border-color:#888888; background-color:#eeeeee; border-collapse:collapse; empty-cells:show" cellspacing="0" cellpadding="4" {{Prettytable}}
{|{{Prettytable}}
+
! Field Name !! Type !!Offset!!Length!! Description                
! Field Name !! Type !! Offset !! Length !! Description
+
|-
 +
| mimetype || integer || 0 || 2 || 0xffff for redirect                     
 +
|-
 +
| parameter len || byte || 2 || 1 || (not used) length of extra paramters                     
 
|-
 
|-
| redirectFlag || boolean || 0 || 1 || 1 for redirect
+
| namespace || char || 3 || 1 || defines to which namespace this directory entry belongs                         
 
|-
 
|-
| mime || integer || 1 || 1 || unused for redirects
+
| revision || integer || 4 || 4 || (not used) identifies a revision of the contents of this directory entry, needed to identify updates or revisions in the original history (must be 0)                     
 
|-
 
|-
| empty || || 2 || 1 || was compression flag, this is now in the cluster header
+
| redirect index || integer || 8 || 4 || pointer to the directory entry of the redirect target               
 
|-
 
|-
| namespace || char || 3 || 1 ||
+
| url || string || 12 ||zero terminated|| string with the URL as refered in the URL pointer list                         
 
|-
 
|-
| redirect index || integer || 4 || 4 ||
+
| title || string || n/a ||zero terminated|| string with an title as refered in the Title pointer list or empty; in case it is empty, the URL is used as title                     
 
|-
 
|-
| extraLen          || integer || 8 || 2 || length of extra bytes (title and parameter)
+
| parameter || data || ||see parameter len|| (not used) extra parameters                       
 +
|}
 +
 
 +
=== Linktarget or deleted Entry (DEPRECATED) ===
 +
There is two kinds of deprecated entry that could be found in pretty old zim files (I, main develloper of libzim, never saw it).
 +
 
 +
They have mimetype equal to 0xfffe or 0xfffd. Reader implementation may check for those value and ignore the whole dirent.
 +
 
 +
== Cluster Pointer List (clusterPtrPos) ==
 +
The cluster pointer list is a list of 8 byte offsets which point to all data clusters in a ZIM archive.
 +
 
 +
{| class="sortable" style="border-width:1px; border-style:solid; border-color:#888888; background-color:#eeeeee; border-collapse:collapse; empty-cells:show" cellspacing="0" cellpadding="4" {{Prettytable}}
 +
! Field Name !! Type !!Offset!!Length!! Description               
 +
|-
 +
| <1st Cluster> || integer || 0 || 8 || pointer to the <1st Cluster>                 
 +
|-
 +
| <1st Cluster> || integer || 8 || 8 || pointer to the <2nd Cluster>                 
 
|-
 
|-
| title + parameter separated by 0-byte || [[QUnicode]] (title) + custom (parameter, not used in articles) || 10 || specified by extraLen || actual title of article; when parameter is empty, the 0-byte is omitted
+
| <nth Cluster> || integer ||(n-1)*8|| 8 || pointer to the <nth Cluster>           
 
|-
 
|-
 +
| ... || integer || ... || 8 || ...                         
 
|}
 
|}
  
 
== Clusters ==
 
== Clusters ==
 +
The clusters contain the actual data of the directory entries. Clusters can be compressed or uncompressed. The purpose of the clusters are that data of more than one directory entry can be compressed inside one cluster, making the compression much more efficient. Typically clusters have a size of about 1 MB.
  
The clusters contain the actual article data. This file section contain a list of clusters, which contain a list of blobs each. The blob is the data of one specific article. So this blob is adressed by the cluster number and the blob number in this cluster. The cluster number is used to look up the file offset in the cluster pointer list.
+
The first byte of the cluster identifies some information about the cluster.
  
The cluster has a starting byte, which indicated, which compresion is used. After this byte, all other data is compressed.
+
The first fourth low bits identifies if the cluster compression type:
 +
* No compression is indicated by a value of 1
 +
* Compressed clusters are indicated by a value of 4 ([[LZMA2 compression]] (or more precisely XZ, since there is a XZ header)) or 5 (Zstandard compression).
 +
* There have been other compression algorithms used before  which have been removed: 2 for zlib and 3 for bzip2.
 +
* 0 is an obselete code for no compression (inhereted from the Zeno)
  
The data area has a list of 4 byte offsets to the blobs counting from the first offset. The offset addresses uncompressed data. The last pointer points to the end of the data area. So there is always one more offset than blobs. Since the first offset points to the start of the first data, the number of offsets can be determined by dividing this offset by 4. The size of one blob is calculated by the difference of two consecutive offsets.
+
The fifth bit identifies the cluster is extended or not :
 +
* By default (5th bit == 0) the cluster is not extended. It means that the offsets are stored in a 4 bytes length integer. Thus contents stored in the cluster cannot exceed 4Go.
 +
* If the cluster is extended (5th bit == 1), the offsets are stored in 8 bytes length integer. Thus contents stored in the cluster can exceed 4Go.
 +
A cluster can be extended only if the zim major version is 6. Else (major version == 5) cluster will always be not extended.
  
== Mime types ==
+
The zimlib uses [http://tukaani.org/xz/ xz-utils] as a C++ implementation of lzma2, for Java see [http://tukaani.org/xz/java.html XZ-Java].
  
Currently these mime types are assigned:
+
To find the data of a specific directory entry within a cluster the uncompressed cluster has a list of pointers to blobs within the uncompressed cluster after the first byte.
  
{|{{Prettytable}}
+
{| class="sortable" style="border-width:1px; border-style:solid; border-color:#888888; background-color:#eeeeee; border-collapse:collapse; empty-cells:show" cellspacing="0" cellpadding="4" {{Prettytable}}
! number !! mime-type
+
! Field Name !! Type !!Offset!!Length!! Description               
 
|-
 
|-
| 0 || text/html
+
| cluster information || integer || 0 || 1 || Fourth low bits : 1: no compression, 4: LZMA2 compressed, 5: zstd compressed
 +
Firth bits : 0: normal (OFFSET_SIZE=4) 1: extended (OFFSET_SIZE=8)             
 
|-
 
|-
| 1 || text/plain
+
| colspan="5" | The following data bytes have to be uncompressed!
 
|-
 
|-
| 2 || image/jpeg
+
| <1st Blob> || integer || 1 || OFFSET_SIZE || offset to the <1st Blob>                   
 
|-
 
|-
| 3 || image/png
+
| <2nd Blob> || integer || 1+OFFSET_SIZE || OFFSET_SIZE || offset to the <2nd Blob>                   
 
|-
 
|-
| 4 || image/tiff
+
| <nth Blob> || integer ||(n-1)*OFFSET_SIZE+1|| OFFSET_SIZE || offset to the <nth Blob>           
 
|-
 
|-
| 5 || text/css
+
| ... || integer || ... || OFFSET_SIZE || ...                         
 
|-
 
|-
| 6 || image/gif
+
| <last blob / end> || integer || n/a || OFFSET_SIZE || offset to the end of the cluster           
 
|-
 
|-
| 7 || index page
+
| <1st Blob> || data || n/a || n/a || data of the <1st Blob>                   
 
|-
 
|-
| 8 || application/x-javascript
+
| <2nd Blob> || data || n/a || n/a || data of the <2nd Blob>                   
|-
 
| 9 || image/x-icon
 
|-
 
| 10 || text/xml
 
 
|-
 
|-
 +
| ... || data || ... || n/a || ...                           
 
|}
 
|}
 +
 +
The offset addresses uncompressed data. The last pointer points to the end of the data area. So there is always one more offset than blobs. Since the first offset points to the start of the first data, the number of offsets can be determined by dividing this offset by OFFSET_SIZE. The size of one blob is calculated by the difference of two consecutive offsets.
  
 
== Namespaces ==
 
== Namespaces ==
Namespaces seperate different types of data stored in the ZIM File Format.
+
Namespaces separate different types of directory entries - which might have the same title or url - stored in the ZIM archive Format.
 +
 
 +
The new namespace usage put a strong semantics on the namespaces. The libzim uses this semantics and provide different kind of API to access the different kind of entries.
 +
 
 +
The libzim hide the namespace to the user, so a entry `foo.html` in namespace `C` will be accessible as `foo.html`. libzim provides specific API to access metadata.
  
They can be distinguished by prepending the article namespace before the article name in the URL path, eg. ''http://localhost/A/Articlename''.
+
Other implementation are free to explicit the namespace or not (but need to provide some fallback for all namespace usage)
  
{|{{Prettytable}}
+
{| class="sortable" style="border-width:1px; border-style:solid; border-color:#888888; background-color:#eeeeee; border-collapse:collapse; empty-cells:show" cellspacing="0" cellpadding="4" {{Prettytable}}
! Namespace !! Description  
+
! Namespace !! Description
|-
 
| - || template data
 
|-
 
| A || articles
 
|-
 
| I || images, files - see [[Image Handling]]
 
 
|-
 
|-
| J || images, text - see [[Image Handling]]
+
| C || User content entries - see [[Article Format]]        
 
|-
 
|-
| U || categories, text - see [[Category Handling]]
+
| M || ZIM metadata - see [[Metadata]]        
 
|-
 
|-
| V || categories, article list - see [[Category Handling]]
+
| W || Well know entries (MainPage, Favicon) - see [[Well known entries]]        
 
|-
 
|-
| W || ''deprecated'' - see [[Zeno File Format]]
+
| X || search indexes - see [[Search indexes]]
|-
 
| X || fulltext index - see [[Zim Index Format]]
 
|-
 
| Y || ''deprecated'' - see [[Zeno File Format]]
 
|-
 
| Z || ''deprecated'' - see [[Zeno File Format]]
 
 
|}
 
|}
 +
 +
== URLs ==
 +
 +
=== URL Encoding ===
 +
The URLs in the UrlPointerlist are utf-8 and are not url encoded (https://www.ietf.org/rfc/rfc1738.txt)
 +
 +
Some readers process the requests that already do the url decoding internally whereas most readers will handle the URLs directly. In this case you have to do the decoding before you pass the parameter to libzim.
 +
 +
=== Local Anchors ===
 +
Many articles - especially when a table of contents is used - use local anchors to jump within an article. 
 +
 +
<pre>
 +
<a href="foo#headline1">jump to article foo, headline 1</a>
 +
</pre>
 +
 +
The browser handles these local anchors by itself. It will determine if another article has to be loaded (local anchor inside another article than the currently shown) and will send a request only with the article URL without the local anchor - in our example "foo". After the article has been loaded the browser will then search for the local anchor tag and jump to the right location.
 +
 +
If you use a common rendering engine or HTML widget you don't have to care for this cases, you can just use the requests as they are submitted by the engine / widget.
 +
 +
Should you render the article contents by yourself you have to consider this and take care of it before you hand requests to zimlib.
 +
 +
== Encodings ==
 +
=== Character Encoding ===
 +
The standard encoding for ZIM archive content is UTF-8. So both article data and URLs should be handled accordingly.
 +
 +
=== Integer Encoding ===
 +
All types are little-endian.
 +
 +
All integers are unsigned integers (uint_16, uint_32, uint_64).
 +
 +
All lengths are bytes.
 +
 +
== Split ZIM files ==
 +
ZIM archives can be split in multiple files. This is necessary to be able to store big (over 4GB for example) ZIM archives to limited file systems (like FAT32). That said, the files can be of any size, but the naming is really important. The ZIM files should be named like following (the file name extensions matter): ''foobar.zimaa, foobar.zimab, foobar.zimac''...
 +
 +
== See also ==
 +
* [[Zeno file format]] (deprecated)
 +
* [[ZIM File Format/4]] (deprecated)
 +
* [[ZIM File Example]]

Latest revision as of 07:44, 23 April 2021

Beginning 2021, we change the way we handle namespaces in ZIM file format.

This is major change in the way we handle entries in libzim and the semantics around but it is not a break in the binary zim format. Old library/readers are compatible with new zim files.

This page describe the new format. The old format can be found here : ZIM file format old namespace.

Schema File Format.png

Header

A ZIM archive starts with a header :

Field Name Type Offset Length Description
magicNumber integer 0 4 Magic number to recognise the file format, must be 72173914 (0x44D495A)
majorVersion integer 4 2 Major version of the ZIM archive format (6)
minorVersion integer 6 2 Minor version of the ZIM archive format (1 for new namespace usage, 0 for old namespace usage)
uuid integer 8 16 unique id of this zim archive
entryCount integer 24 4 total number of entries
clusterCount integer 28 4 total number of clusters
urlPtrPos integer 32 8 position of the directory pointerlist ordered by URL
titlePtrPos integer 40 8 position of the directory pointerlist ordered by Title

This is considered as obsolete, readers should use X/listing/titleordered/v0 instead and fallback to titlePtrPos if entry is not present.

clusterPtrPos integer 48 8 position of the cluster pointer list
mimeListPos integer 56 8 position of the MIME type list (also header size)
mainPage integer 64 4 main page or 0xffffffff if no main page
layoutPage integer 68 4 layout page or 0xffffffffff if no layout page (deprecated, always 0xffffffffff)
checksumPos integer 72 8 pointer to the md5checksum of this archive without the checksum itself. This points always 16 bytes before the end of the archive.

Major version is updated when an incompatible change is integrated in the format (a lib made for a version N will probably not be able to read a version N+1)

Minor version is updated when an compatible change is integrated (a lib made for a minor version n will be able to read a version n+1)

The current major version is 6. You may found old zim archives with major version 5. They are the same than 6 less extended cluster, so you can read a 5 major version as if it was a 6.

The minor version can be :

A zim archive may be embedded in another file at a specific offset. In the context of zim format, the start of the zim header is the offset 0. Readers allowing to read an embedded archive must adapt offset accordingly.

MIME Type List (mimeListPos)

The MIME type list always follows directly after the header, so the mimeListPos also defines the end and size of the ZIM file header.

The MIME types in this list are zero terminated strings. An empty string marks the end of the MIME type list.

Field Name Type Offset Length Description
<1st MIME Type> string 0 zero terminated declaration of the <1st MIME Type>
<2nd MIME Type> string n/a zero terminated declaration of the <2nd MIME Type>
... string ... zero terminated ...
<last entry / end> string n/a zero terminated empty string - end of MIME type list

URL Pointer List (urlPtrPos)

The URL pointer list is a list of 8 byte offsets to the directory entries.

The directory entries are always ordered by "full" URL (<namespace><path>). Ordering is simply done by comparing the URL strings.

Since directory entries have variable sizes this is needed for random access.

Field Name Type Offset Length Description
<1st URL> integer 0 8 pointer to the directory entry of <1st URL>
<2nd URL> integer 8 8 pointer to the directory entry of <2nd URL>
<nth URL> integer (n-1)*8 8 pointer to the directory entry of <nth URL>
... integer ... 8 ...

Zimlib caches directory entries and references the cached entries via the URL pointers.

Title Pointer List (titlePtrPos)

The title pointer list is a list of entry indices ordered by title (<namespace><title>). The title pointer list actually points to entries in the URL pointer list.

Note that the title pointers are only 4 bytes. They are not offsets in the file but entry numbers.

To get the offset of an entry from the title pointer list, you have to look it up in the URL pointer list.

Field Name Type Offset Length Description
<1st Title> integer 0 4 pointer to the URL pointer of <1st Title>
<2nd Title> integer 4 4 pointer to the URL pointer of <2nd Title>
<nth Title> integer (n-1)*4 4 pointer to the URL pointer of <nth Title>
... integer ... 4 ...

The indirection from titles via URLs to directory entries has two reasons:

  • the pointer list is only half in size as 4 bytes are enough for each entry
  • accessing directory entries by title also makes use of cached directory entries which are referenced by the URL pointers, as implemented in zimlib.

Directory Entries

Directory entries hold the meta information about all entries, images and other objects in a ZIM archive.

There are different types of directory entries:

Content Entry

Field Name Type Offset Length Description
mimetype integer 0 2 MIME type number as defined in the MIME type list
parameter len byte 2 1 (not used) length of extra paramters (must be 0)
namespace char 3 1 defines to which namespace this directory entry belongs
revision integer 4 4 (not used) identifies a revision of the contents of this directory entry, needed to identify updates or revisions in the original history (must be 0)
cluster number integer 8 4 cluster number in which the data of this directory entry is stored
blob number integer 12 4 blob number inside the compressed cluster where the contents are stored
url string 16 zero terminated string with the URL as refered in the URL pointer list
title string n/a zero terminated string with an title as refered in the Title pointer list or empty; in case it is empty, the URL is used as title
parameter data see parameter len (not used) extra parameters

Redirect Entry

Field Name Type Offset Length Description
mimetype integer 0 2 0xffff for redirect
parameter len byte 2 1 (not used) length of extra paramters
namespace char 3 1 defines to which namespace this directory entry belongs
revision integer 4 4 (not used) identifies a revision of the contents of this directory entry, needed to identify updates or revisions in the original history (must be 0)
redirect index integer 8 4 pointer to the directory entry of the redirect target
url string 12 zero terminated string with the URL as refered in the URL pointer list
title string n/a zero terminated string with an title as refered in the Title pointer list or empty; in case it is empty, the URL is used as title
parameter data see parameter len (not used) extra parameters

Linktarget or deleted Entry (DEPRECATED)

There is two kinds of deprecated entry that could be found in pretty old zim files (I, main develloper of libzim, never saw it).

They have mimetype equal to 0xfffe or 0xfffd. Reader implementation may check for those value and ignore the whole dirent.

Cluster Pointer List (clusterPtrPos)

The cluster pointer list is a list of 8 byte offsets which point to all data clusters in a ZIM archive.

Field Name Type Offset Length Description
<1st Cluster> integer 0 8 pointer to the <1st Cluster>
<1st Cluster> integer 8 8 pointer to the <2nd Cluster>
<nth Cluster> integer (n-1)*8 8 pointer to the <nth Cluster>
... integer ... 8 ...

Clusters

The clusters contain the actual data of the directory entries. Clusters can be compressed or uncompressed. The purpose of the clusters are that data of more than one directory entry can be compressed inside one cluster, making the compression much more efficient. Typically clusters have a size of about 1 MB.

The first byte of the cluster identifies some information about the cluster.

The first fourth low bits identifies if the cluster compression type:

  • No compression is indicated by a value of 1
  • Compressed clusters are indicated by a value of 4 (LZMA2 compression (or more precisely XZ, since there is a XZ header)) or 5 (Zstandard compression).
  • There have been other compression algorithms used before which have been removed: 2 for zlib and 3 for bzip2.
  • 0 is an obselete code for no compression (inhereted from the Zeno)

The fifth bit identifies the cluster is extended or not :

  • By default (5th bit == 0) the cluster is not extended. It means that the offsets are stored in a 4 bytes length integer. Thus contents stored in the cluster cannot exceed 4Go.
  • If the cluster is extended (5th bit == 1), the offsets are stored in 8 bytes length integer. Thus contents stored in the cluster can exceed 4Go.

A cluster can be extended only if the zim major version is 6. Else (major version == 5) cluster will always be not extended.

The zimlib uses xz-utils as a C++ implementation of lzma2, for Java see XZ-Java.

To find the data of a specific directory entry within a cluster the uncompressed cluster has a list of pointers to blobs within the uncompressed cluster after the first byte.

Field Name Type Offset Length Description
cluster information integer 0 1 Fourth low bits : 1: no compression, 4: LZMA2 compressed, 5: zstd compressed

Firth bits : 0: normal (OFFSET_SIZE=4) 1: extended (OFFSET_SIZE=8)

The following data bytes have to be uncompressed!
<1st Blob> integer 1 OFFSET_SIZE offset to the <1st Blob>
<2nd Blob> integer 1+OFFSET_SIZE OFFSET_SIZE offset to the <2nd Blob>
<nth Blob> integer (n-1)*OFFSET_SIZE+1 OFFSET_SIZE offset to the <nth Blob>
... integer ... OFFSET_SIZE ...
<last blob / end> integer n/a OFFSET_SIZE offset to the end of the cluster
<1st Blob> data n/a n/a data of the <1st Blob>
<2nd Blob> data n/a n/a data of the <2nd Blob>
... data ... n/a ...

The offset addresses uncompressed data. The last pointer points to the end of the data area. So there is always one more offset than blobs. Since the first offset points to the start of the first data, the number of offsets can be determined by dividing this offset by OFFSET_SIZE. The size of one blob is calculated by the difference of two consecutive offsets.

Namespaces

Namespaces separate different types of directory entries - which might have the same title or url - stored in the ZIM archive Format.

The new namespace usage put a strong semantics on the namespaces. The libzim uses this semantics and provide different kind of API to access the different kind of entries.

The libzim hide the namespace to the user, so a entry `foo.html` in namespace `C` will be accessible as `foo.html`. libzim provides specific API to access metadata.

Other implementation are free to explicit the namespace or not (but need to provide some fallback for all namespace usage)

Namespace Description
C User content entries - see Article Format
M ZIM metadata - see Metadata
W Well know entries (MainPage, Favicon) - see Well known entries
X search indexes - see Search indexes

URLs

URL Encoding

The URLs in the UrlPointerlist are utf-8 and are not url encoded (https://www.ietf.org/rfc/rfc1738.txt)

Some readers process the requests that already do the url decoding internally whereas most readers will handle the URLs directly. In this case you have to do the decoding before you pass the parameter to libzim.

Local Anchors

Many articles - especially when a table of contents is used - use local anchors to jump within an article.

<a href="foo#headline1">jump to article foo, headline 1</a>

The browser handles these local anchors by itself. It will determine if another article has to be loaded (local anchor inside another article than the currently shown) and will send a request only with the article URL without the local anchor - in our example "foo". After the article has been loaded the browser will then search for the local anchor tag and jump to the right location.

If you use a common rendering engine or HTML widget you don't have to care for this cases, you can just use the requests as they are submitted by the engine / widget.

Should you render the article contents by yourself you have to consider this and take care of it before you hand requests to zimlib.

Encodings

Character Encoding

The standard encoding for ZIM archive content is UTF-8. So both article data and URLs should be handled accordingly.

Integer Encoding

All types are little-endian.

All integers are unsigned integers (uint_16, uint_32, uint_64).

All lengths are bytes.

Split ZIM files

ZIM archives can be split in multiple files. This is necessary to be able to store big (over 4GB for example) ZIM archives to limited file systems (like FAT32). That said, the files can be of any size, but the naming is really important. The ZIM files should be named like following (the file name extensions matter): foobar.zimaa, foobar.zimab, foobar.zimac...

See also