Difference between revisions of "ZIM file format"

From openZIM
Jump to navigation Jump to search
(Specified geo index)
(→‎Split ZIM files: Give more precision of where and how the ZIM archives should be chunked.)
 
(36 intermediate revisions by 6 users not shown)
Line 1: Line 1:
[[Image:Schema File Format.png|500px|right]]
Beginning 2021, we change the way we handle namespaces in ZIM file format.
The '''ZIM file format''' is based on the old and deprecated [[Zeno File Format]]. See also a walk through example at [[ZIM File Example]].
It starts with a header, which is described here:


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 file starts with a header. This is offset 0.
A ZIM archive starts with a header :


Length in bytes, all types are little-endian.
{| 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                 
{|{{Prettytable}}
|-
! Field Name !! Type !!Offset!!Length!! Description                 
| magicNumber || integer || 0 || 4 || Magic number to recognise the file format, must be 72173914 (0x44D495A)
|-
|-
| magicNumber || integer || 0 || 4 || Magic number to recognise the file format, must be 72173914                   
| [[#Major_.26_Minor_versions|majorVersion]]
| integer
| 4
| 2
| Major version of the ZIM archive format. 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)
|-
|-
| version || integer || 4 || 4 || ZIM=5, bytes 1-2: major, bytes 3-4: minor version of the ZIM file format                      
| [[#Major_.26_Minor_versions|minorVersion]] || integer || 6 || 2 || Minor version of the ZIM archive format. 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)                     
|-
|-
| uuid || integer || 8 || 16 || unique id of this zim file                          
| uuid || integer || 8 || 16 || unique id of this zim archive                          
|-
|-
| articleCount || integer || 24 || 4 || total number of articles                  
| entryCount || integer || 24 || 4 || total number of entries                  
|-
|-
| clusterCount || integer || 28 || 4 || total number of clusters                   
| clusterCount || integer || 28 || 4 || total number of clusters                   
|-
|-
| urlPtrPos || integer || 32 || 8 || position of the directory pointerlist ordered by URL                      
| pathPtrPos || integer || 32 || 8 || position of the directory pointerlist ordered by Path                      
|-
|-
| titlePtrPos || integer || 40 || 8 || position of the directory pointerlist ordered by Title                  
| 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                 
| clusterPtrPos || integer || 48 || 8 || position of the cluster pointer list                 
Line 31: Line 37:
| mainPage || integer || 64 || 4 || main page or 0xffffffff if no main page                       
| 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                     
| 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.
|}
 
A ZIM archive may be embedded in another file at a specific offset. In the context of the ZIM format, the start of the ZIM header is the offset 0. Readers allowing to read an embedded archive must adapt offset accordingly.
 
=== Major & Minor versions ===
 
Versioning of the file format specification has not been done [https://semver.org/ rigorously] until version 5.
 
Before version 5, there was only one version number and no Major vs Minor distinction.
 
{| class="wikitable"
|+ ZIM format versions
|-
! Major !! Minor !! Backward compatible !! Description
!libzim version
|-
|-
| checksumPos || integer || 72 || 8 || pointer to the md5checksum of this file without the checksum itself. This points always 16 bytes before the end of the file.
| colspan="2" | 0 || no || ''This version features have not been tracked properly''
|Unknown
|-
|-
| geoIndexPos || integer || 80 || 8 || pointer to the geo index (optional). Present if mimeListPos is at least 80.
| colspan="2" | 1 || no || ''This version features have not been tracked properly''
|Unknown
|-
| colspan="2" | 2 || no || ''This version features have not been tracked properly''
|Unknown
|-
| colspan="2" | 3 || no || ''This version features have not been tracked properly''
|Unknown
|-
| colspan="2" | 4 || no ||''This version features have not been tracked properly''
|Unknown
|-
| colspan="2" | 5 || yes || Introduces:
- Path index (was only title indexed before)
 
- MimeList Pos
|From date 2009-11-29
Until 6.3.1 (included)
|-
|5
|0
|yes
|Introduces:
- Notion of Major and Minor version
|From 3.2.0
|-
| rowspan="3" |6|| 0 || no  || Introduces extended clusters
Still uses [[ZIM file format old namespace|"old" namespaces]]
|From 3.2.0
|-
|                    1 || yes  || Introduces [[#Namespaces|"new" namespaces]] scheme
|From 7.0.0
|-
|                    2 || yes || Explicitly allows alias entries (several entries pointing to the same cluster/blob)
|From 9.1.0
|}
|}


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


{|{{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}}
! Field Name !! Type !!Offset!!Length!! Description                 
! Field Name !! Type !!Offset!!Length!! Description                 
|-
|-
Line 55: Line 113:
|}
|}


== URL Pointer List (urlPtrPos) ==
== Path Pointer List (pathPtrPos) ==
The URL pointer list is a list of 8 byte offsets to the directory entries.
The Path pointer list is a list of 8 byte offsets to the directory entries.


The directory entries are always ordered by URL (including the namespace prefix). Ordering is simply done by comparing the URL strings in binary.
The directory entries are always ordered by "full" path (<code><namespace><path></code>). Ordering is simply done by comparing the path strings (utf8 encoded)


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


{|{{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}}
! Field Name !! Type !!Offset!!Length!! Description                 
! Field Name !! Type !!Offset!!Length!! Description                 
|-
|-
| <1st URL> || integer || 0 || 8 || pointer to the directory entry of <1st URL>                       
| <1st path> || integer || 0 || 8 || pointer to the directory entry of <1st path>                       
|-
|-
| <2nd URL> || integer || 8 || 8 || pointer to the directory entry of <2nd URL>                       
| <2nd path> || integer || 8 || 8 || pointer to the directory entry of <2nd path>                       
|-
|-
| <nth URL> || integer ||(n-1)*8|| 8 || pointer to the directory entry of <nth URL>                 
| <nth path> || integer ||(n-1)*8|| 8 || pointer to the directory entry of <nth path>                 
|-
|-
| ... || integer || ... || 8 || ...                           
| ... || integer || ... || 8 || ...                           
|}
|}


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


== Title Pointer List (titlePtrPos) ==
== Title Pointer List (titlePtrPos) ==
The title pointer list is a list of article indices ordered by title. The title pointer list actually points to entries
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 path pointer list.
in the URL pointer list. Note that the title pointers are only 4 bytes. They are not offsets in the file but article numbers.
To get the offset of an article from the title pointer list, you have to look it up in the URL pointer list.


{|{{Prettytable}}
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 path 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                 
! Field Name !! Type !!Offset!!Length!! Description                 
|-
|-
| <1st Title> || integer || 0 || 4 || pointer to the URL pointer of <1st Title>                     
| <1st Title> || integer || 0 || 4 || pointer to the path pointer of <1st Title>                     
|-
|-
| <2nd Title> || integer || 4 || 4 || pointer to the URL pointer of <2nd Title>                     
| <2nd Title> || integer || 4 || 4 || pointer to the path pointer of <2nd Title>                     
|-
|-
| <nth Title> || integer ||(n-1)*4|| 4 || pointer to the URL pointer of <nth Title>               
| <nth Title> || integer ||(n-1)*4|| 4 || pointer to the path pointer of <nth Title>               
|-
|-
| ... || integer || ... || 4 || ...                           
| ... || integer || ... || 4 || ...                           
|}
|}


The indirection from titles via URLs to directory entries has two reasons:
The indirection from titles via Paths to directory entries has two reasons:
* the pointer list is only half in size as 4 bytes are enough for each entry
* 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.
* accessing directory entries by title also makes use of cached directory entries which are referenced by the path pointers, as implemented in libzim.


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


There are many types of directory entries:
There are different types of directory entries:


=== Article Entry ===
=== Content Entry ===
{|{{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}}
! Field Name !! Type !!Offset!!Length!! Description                 
! Field Name !! Type !!Offset!!Length!! Description                 
|-
|-
| mimetype || integer || 0 || 2 || MIME type number as defined in the MIME type list                       
| 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                       
| 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                           
| namespace || char || 3 || 1 || defines to which namespace this directory entry belongs                           
|-
|-
| revision || integer || 4 || 4 || (optional) identifies a revision of the contents of this directory entry, needed to identify updates or revisions in the original history                       
| 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                 
| cluster number || integer || 8 || 4 || cluster number in which the data of this directory entry is stored                 
Line 118: Line 178:
| blob number || integer || 12 || 4 || blob number inside the compressed cluster where the contents are 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                         
| path || string || 16 ||zero terminated|| string with the path as referred in the path 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                       
| title || string || n/a ||zero terminated|| string with an title as referred in the Title pointer list or empty; in case it is empty, the path is used as title                       
|-
|-
| parameter || data || ||see parameter len|| (not used) extra parameters                         
| parameter || data || ||see parameter len|| (not used) extra parameters                         
Line 126: Line 186:


=== Redirect Entry ===
=== Redirect Entry ===
{|{{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}}
! Field Name !! Type !!Offset!!Length!! Description                 
! Field Name !! Type !!Offset!!Length!! Description                 
|-
|-
Line 135: Line 195:
| namespace || char || 3 || 1 || defines to which namespace this directory entry belongs                           
| namespace || char || 3 || 1 || defines to which namespace this directory entry belongs                           
|-
|-
| revision || integer || 4 || 4 || (optional) identifies a revision of the contents of this directory entry, needed to identify updates or revisions in the original history                       
| 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                 
| 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                           
| path || string || 12 ||zero terminated|| string with the path as referred in the path 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                       
| title || string || n/a ||zero terminated|| string with an title as referred in the Title pointer list or empty; in case it is empty, the path is used as title                       
|-
|-
| parameter || data || ||see parameter len|| (not used) extra parameters                         
| parameter || data || ||see parameter len|| (not used) extra parameters                         
|}
|}


=== Linktarget or deleted Entry ===
None of the strings should have control characters from U+0000 through U+001F.
{|{{Prettytable}}
 
! Field Name !! Type !!Offset!!Length!! Description               
=== 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).
| mimetype || integer || 0 || 2 || 0xfffe for linktarget, 0xfffd for deleted entry
 
|-
They have mimetype equal to 0xfffe or 0xfffd. Reader implementation may check for those value and ignore the whole dirent.
| 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 || (optional) identifies a revision of the contents of this directory entry, needed to identify updates or revisions in the original history                     
|-
| 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                       
|}


== Cluster Pointer List (clusterPtrPos) ==
== Cluster Pointer List (clusterPtrPos) ==
The cluster pointer list is a list of 8 byte offsets which point to all data clusters in a ZIM file.
The cluster pointer list is a list of 8 byte offsets which point to all data clusters in a ZIM archive.


{|{{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}}
! Field Name !! Type !!Offset!!Length!! Description                 
! Field Name !! Type !!Offset!!Length!! Description                 
|-
|-
Line 183: Line 231:
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 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 if it is compressed (4) or not (0). The default is uncompressed indicated by a value of 0 or 1 (obsoleted, inherited by Zeno) while compressed clusters are indicated by a value of 4 which indicates [[LZMA2 compression]] (or more precisely XZ, since there is a XZ header). There have been other compression algorithms used before (2: zlib, 3: bzip2) which have been removed. 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].
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 libzim uses [http://tukaani.org/xz/ xz-utils] as a C++ implementation of lzma2, for Java see [http://tukaani.org/xz/java.html 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.
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}}
! Field Name !! Type !!Offset!!Length!! Description                 
! Field Name !! Type !!Offset!!Length!! Description                 
|-
|-
| compression type || integer || 0 || 1 || 0: default (no compression), 1: none (inherited from Zeno), 4: LZMA2 compressed                
| 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)               
|-
|-
|colspan=5| The following data bytes have to be uncompressed!
| colspan="5" | The following data bytes have to be uncompressed!
|-
|-
| <1st Blob> || integer || 1 || 4 || offset to the <1st Blob>                     
| <1st Blob> || integer || 1 || OFFSET_SIZE || offset to the <1st Blob>                     
|-
|-
| <2nd Blob> || integer || 5 || 4 || offset to the <2nd Blob>                     
| <2nd Blob> || integer || 1+OFFSET_SIZE || OFFSET_SIZE || offset to the <2nd Blob>                     
|-
|-
| <nth Blob> || integer ||(n-1)*4+1|| 4 || offset to the <nth Blob>             
| <nth Blob> || integer ||(n-1)*OFFSET_SIZE+1|| OFFSET_SIZE || offset to the <nth Blob>             
|-
|-
| ... || integer || ... || 4 || ...                           
| ... || integer || ... || OFFSET_SIZE || ...                           
|-
|-
| <last blob / end> || integer || n/a || 4 || offset to the end of the cluster             
| <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>                     
| <1st Blob> || data || n/a || n/a || data of the <1st Blob>                     
Line 211: Line 273:
|}
|}


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 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.


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


The geo index contains a spatially indexed data structure so that it is possible to search for articles by their position on the globe. Coordinates are assumed to be WGS84 coordinates, but encoded into binary in the following way: Both latitudes and longitudes use the full span of 32 bit unsigned big endian integers, which means that the south pole has an encoded latitude of
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.
0 and the north pole an encoded latitude of 0xffffffff (actually 0x100000000). This means that the scaling between degree and encoded integers
is different for latitudes and longitudes. The coordinate of Berlin, which is approximately at 52° 31′ N, 13° 23′ E or
52.516667, 13.383333 is encoded as latitude (52.516667 + 90) / 180 * 0x100000000 = 3400580132 and longitude
(13.383333 + 180) / 360 * 0x100000000 = 2307153030.


The index actually consists of a list of indices assumed to be ordered by importance. The idea is that the indices
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.
are searched in order until a certain amount of search results is found, such that at low zoom levels, only the
important articles (countries, large cities) are displayed.


All offsets in the geo index are counted relative to the start of the geo index. This means that in order to get
Other implementation are free to explicit the namespace or not (but need to provide some fallback for all namespace usage)
absolute file offsets, you have to add the value of the geoIndexPos field in the file header.


At geoIndexPos, the geo index starts with the following table:
{| 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
{|{{Prettytable}}
! Field Name !! Type !!Offset!!Length!! Description                
|-
|-
| indicesCount || integer || 0 || 4 || number of geo indices (=n)
| C || User content entries - see [[Article Format]]       
|-
|-
| <1st offset> || integer || 4 || 4 || offset to the data area of the 1st geo index
| M || ZIM metadata - see [[Metadata]]       
|-
| <2nd offset> || integer || 8 || 4 || offset to the data area of the 2nd geo index
|-
| ... || integer || ... || 4 || ...                         
|-
|-
| <nth offset> || integer ||n*4|| 4 || offset to the data area of the nth geo index
| W || Well know entries (MainPage, Favicon) - see [[Well known entries]]       
|-
|-
| <end offset> || integer ||(n+1)*4|| 4 || offset to the end of the data area of the nth geo index
| X || search indexes - see [[Search indexes]]
|}
|}


Each geo index is a binary tree of coordinates alternating between latitudes and longitudes (starting with latitudes)
== Paths ==
ending in a list of coordinates and article indices as leaves. A non-leaf node is encoded as follows:


{|{{Prettytable}}
=== Path Encoding in the ZIM ===
! Field Name !! Type !!Offset!!Length!! Description               
The Path in the PathPointerlist are encoded in utf-8 and are '''not''' url encoded.
|-
| value || integer || ? || 4 || discriminator coordinate, cannot be zero (would indicate a leaf node)
|-
| greaterOffset || integer || ? + 4 || 4 || offset to the node containing coordinates with latitude / longitude greater than the discriminator
|-
| <smaller or equal coordinates> || ... || ... || ... || node leading to coordinates with latitude / longitude smaller or equal than the discriminator
|-
| <larger coordinates> || ... || greaterOffset || ... || node leading to coordinates with latitude / longitude greater than the discriminator
|}


Leaf nodes containing the actual coordinates and links to the articles are encoded as follows:
For instance, if you store in the ZIM an HTML document with a href pointing to `characters%20%C3%A9ncoding.html`, you have to store the corresponding ZIM entry at `characters éncoding.html` Path.


{|{{Prettytable}}
Or if you want to store a ZIM entry at `index.html?param=value`, the HTML document pointing to it will have to use the `index.html%3Fparam%3Dvalue` href.
! Field Name !! Type !!Offset!!Length!! Description               
|-
| 0 || integer || ? || 4 || value identifying leaf nodes
|-
| numCoordinates || integer || ? + 4 || 4 || number of coordinates that follow
|-
| <1st latitude> || integer || ? + 8 || 4 || 1st latitude
|-
| <1st longitude> || integer || ? + 12 || 4 || 1st longitude
|-
| <1st title index> || integer || ? + 16 || 4 || 1st title index
|-
| ... || ... || ... || ... || ...
|-
| <nth latitude> || integer || ? + n * 12 - 4 || 4 || nth latitude
|-
| <nth longitude> || integer || ? + n * 12 || 4 || nth longitude
|-
| <nth title index> || integer || ? + n * 12 + 4 || 4 || nth title index
|}


The title indices in the above table are article numbers just like in the title pointer list.
The reason behind it is that libzim is agnostic of which kind of content and which kind of readers will be used. Everything around URL encoding is purely linked to HTTP / HTML / Web standards.


When serving web content (which is usually the case), some readers process the requests and already do the url decoding internally, whereas most readers will handle the paths directly.


It can be assumed that the index is generated in the following way: All coordinates are sorted
The same applies to querystring which might be absorbed by some webservers and not passed to the libzim.
by latitude. The median is stored as discriminator value, the first half of the coordinates is
stored in a "lower node", the second half in an upper node. For each of the two nodes, the
coordinates are sorted by longitude and are split in much the same way until there are
less than a specific number of articles (e.g. 10) left at which point they are
stored as a list in a leaf node.


== Namespaces ==
In any case, the reader will have to do the HTTP URL decoding before passing the parameter to libzim.
Namespaces seperate different types of directory entries - which might have the same title - stored in the ZIM File Format.
 
They can be distinguished by prepending the article namespace before the article name in the URL path, eg. ''http://localhost/A/Articlename''.
 
{|{{Prettytable}}
! Namespace !! Description 
|-
| - || layout, eg. the LayoutPage, CSS, favicon.png (48x48), JavaScript and images not related to the articles       
|-
| A || articles - see [[Article Format]]       
|-
| B || article meta data - see [[Article Format]]       
|-
| I || images, files - see [[Image Handling]]       
|-
| J || images, text - see [[Image Handling]]       
|-
| M || ZIM metadata - see [[Metadata]]       
|-
| U || categories, text - see [[Category Handling]]       
|-
| V || categories, article list - see [[Category Handling]]       
|-
| W || categories per article, category list - see [[Category Handling]]       
|-
| X || fulltext index - see [[ZIM Index Format]]       
|}
 
== URLs ==
 
ZIM contents are addressed using URLs fitting the following pattern: <namespace>/<article_url>. The references in articles HTML code (''<a href=""></a>'', ''<img src="">'', etc.) are URL-encoded following the [http://www.ietf.org/rfc/rfc1738.txt RFC 1738] rules.
 
Absolute URLs, ie. with a leading slash (''/'') are forbidden, because this avoid including the ZIM contents in any HTTP sub-hierachy. ZIM contents URLs must consequently be relative. Be careful, <article_url> may itself contain slashes (for example "BMW_501/502").
 
The URLs in the UrlPointerlist are not encoded. Some readers process the requests that already do the 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 zimlib, but zimlib already provides a method to do so.


=== Local Anchors ===
=== Local Anchors ===
Many articles - especially when a table of contents is used - use local anchors to jump within an article.   
Many HTML href - especially when a table of contents is used - use local anchors to jump within a document.   


<pre>
<pre>
<a href="../A/foo#headline1">jump to article foo, headline 1</a>
<a href="foo#headline1">jump to article foo, headline 1</a>
</pre>
</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 "../A/foo". After the article has been loaded the browser will then search for the local anchor tag and jump to the right location.
When a web browser is used a reader, it handles these local anchors locally client-side. This is never sent to the webserver, and even less to libzim. The browser will determine by itself if another ZIM entry has to be loaded (local anchor inside another document than the currently shown) and will send a request only with the document URL without the local anchor - in our example "foo". After the document 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.
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.
Should you render the article contents by yourself you have to consider this and take care of it before you hand-out requests to libzim.


== Encodings ==
== Encodings ==
=== Character Encoding ===
=== Character Encoding ===
The standard encoding for ZIM file content is UTF-8. So both article data and URLs should be handled accordingly.
The standard encoding for ZIM archive content is UTF-8. So both article data and Path should be handled accordingly.


Old Zeno files used a mixture of Latin1 and UTF-8 so there is still some "auto detection" code left in the ''zimlib'', a workaround for this bug. This will be removed in future versions. Zeno files are not supported anymore.
=== Integer Encoding ===
All types are little-endian.


=== Integer Encoding ===
All integers are unsigned integers (uint_16, uint_32, uint_64).
For integer encoding the same algorithm as UTF-8 encoding is used. This encoding is also known as "integer compression". It safes some bytes by using variable lengths of integer fields, depending on the actual value of the number.
 
All lengths are bytes.


See also http://en.wikipedia.org/wiki/UTF-8#Design.
== Split ZIM archives in chunks ==
ZIM archives can be split in multiple chunk files. This is necessary to be able to store big (over 4GB for example) ZIM archives in file systems with single file size restrictions (like FAT32). The size of each ZIM file chunk can be "choosen" but the ZIM archives can't be cut anywhere. ZIM archives have to be cut between [[#Clusters|cluster]]s. In addition, the naming of ZIM archive chunks is really important: they should be named like following (the file name extensions matter): ''foobar.zimaa, foobar.zimab, foobar.zimac''. To perform this splitting operation easily, you can rely on the ''zimsplit'' command line tool (part of the [https://github.com/openzim/zim-tools ZIM tools]).


Old Zeno files used the QUnicode library instead. By switching to UTF-8 the new format is more standard-adherent and easier to understand.
== Other ==
<code>pathPtrPos</code> was initially called <code>urlPtrPos</code> (and <code>path</code> in dirent structure was called <code>url</code>). In April 2024, we changed the wording from <code>url</code> to <code>path</code> as it better conveys the fact that we are storing a path for the entry and not a full url (with scheme, host...). Note that this is just a wording change and the semantic has not changed at all. Implementations do not need to change anything (except a potential renaming of variables to better follow the spec).


== See also ==
== See also ==

Latest revision as of 08:35, 24 August 2024

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. 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)
minorVersion integer 6 2 Minor version of the ZIM archive format. 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)
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
pathPtrPos integer 32 8 position of the directory pointerlist ordered by Path
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.

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

Major & Minor versions

Versioning of the file format specification has not been done rigorously until version 5.

Before version 5, there was only one version number and no Major vs Minor distinction.

ZIM format versions
Major Minor Backward compatible Description libzim version
0 no This version features have not been tracked properly Unknown
1 no This version features have not been tracked properly Unknown
2 no This version features have not been tracked properly Unknown
3 no This version features have not been tracked properly Unknown
4 no This version features have not been tracked properly Unknown
5 yes Introduces:

- Path index (was only title indexed before)

- MimeList Pos

From date 2009-11-29

Until 6.3.1 (included)

5 0 yes Introduces:

- Notion of Major and Minor version

From 3.2.0
6 0 no Introduces extended clusters

Still uses "old" namespaces

From 3.2.0
1 yes Introduces "new" namespaces scheme From 7.0.0
2 yes Explicitly allows alias entries (several entries pointing to the same cluster/blob) From 9.1.0

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

Path Pointer List (pathPtrPos)

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

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

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

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

Libzim caches directory entries and references the cached entries via the path 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 path 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 path pointer list.

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

The indirection from titles via Paths 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 path pointers, as implemented in libzim.

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
path string 16 zero terminated string with the path as referred in the path pointer list
title string n/a zero terminated string with an title as referred in the Title pointer list or empty; in case it is empty, the path 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
path string 12 zero terminated string with the path as referred in the path pointer list
title string n/a zero terminated string with an title as referred in the Title pointer list or empty; in case it is empty, the path is used as title
parameter data see parameter len (not used) extra parameters

None of the strings should have control characters from U+0000 through U+001F.

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 libzim 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 path - 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

Paths

Path Encoding in the ZIM

The Path in the PathPointerlist are encoded in utf-8 and are not url encoded.

For instance, if you store in the ZIM an HTML document with a href pointing to `characters%20%C3%A9ncoding.html`, you have to store the corresponding ZIM entry at `characters éncoding.html` Path.

Or if you want to store a ZIM entry at `index.html?param=value`, the HTML document pointing to it will have to use the `index.html%3Fparam%3Dvalue` href.

The reason behind it is that libzim is agnostic of which kind of content and which kind of readers will be used. Everything around URL encoding is purely linked to HTTP / HTML / Web standards.

When serving web content (which is usually the case), some readers process the requests and already do the url decoding internally, whereas most readers will handle the paths directly.

The same applies to querystring which might be absorbed by some webservers and not passed to the libzim.

In any case, the reader will have to do the HTTP URL decoding before passing the parameter to libzim.

Local Anchors

Many HTML href - especially when a table of contents is used - use local anchors to jump within a document.

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

When a web browser is used a reader, it handles these local anchors locally client-side. This is never sent to the webserver, and even less to libzim. The browser will determine by itself if another ZIM entry has to be loaded (local anchor inside another document than the currently shown) and will send a request only with the document URL without the local anchor - in our example "foo". After the document 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-out requests to libzim.

Encodings

Character Encoding

The standard encoding for ZIM archive content is UTF-8. So both article data and Path 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 archives in chunks

ZIM archives can be split in multiple chunk files. This is necessary to be able to store big (over 4GB for example) ZIM archives in file systems with single file size restrictions (like FAT32). The size of each ZIM file chunk can be "choosen" but the ZIM archives can't be cut anywhere. ZIM archives have to be cut between clusters. In addition, the naming of ZIM archive chunks is really important: they should be named like following (the file name extensions matter): foobar.zimaa, foobar.zimab, foobar.zimac. To perform this splitting operation easily, you can rely on the zimsplit command line tool (part of the ZIM tools).

Other

pathPtrPos was initially called urlPtrPos (and path in dirent structure was called url). In April 2024, we changed the wording from url to path as it better conveys the fact that we are storing a path for the entry and not a full url (with scheme, host...). Note that this is just a wording change and the semantic has not changed at all. Implementations do not need to change anything (except a potential renaming of variables to better follow the spec).

See also