github.com/dolthub/dolt/go@v0.40.5-0.20240520175717-68db7794bea6/store/nbs/archive.go (about)

     1  // Copyright 2024 Dolthub, Inc.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package nbs
    16  
    17  import (
    18  	"crypto/sha512"
    19  	"errors"
    20  )
    21  
    22  /*
    23  A Dolt Archive is a file format for storing a collection of Chunks in a single file. The archive is essentially many
    24  byte spans concatenated together, with an index at the end of the file. Chunk Addresses are used to lookup and retrieve
    25  Chunks from the Archive.
    26  
    27  There are byte spans with in the archive which are not addressable by a Chunk Address. These are used as internal data
    28  to aid in the compression of the Chunks.
    29  
    30  A Dolt Archive file follows the following format:
    31     +------------+------------+-----+------------+-------+----------+--------+
    32     | ByteSpan 1 | ByteSpan 2 | ... | ByteSpan N | Index | Metadata | Footer |
    33     +------------+------------+-----+------------+-------+----------+--------+
    34  
    35  In reverse order, since that's how we read it
    36  
    37  Footer:
    38     +----------------------+-------------------------+----------------------+--------------------------+-----------------+------------------------+--------------------+
    39     | (Uint32) IndexLength | (Uint32) ByteSpan Count | (Uint32) Chunk Count | (Uint32) Metadata Length | (192) CheckSums | (Uint8) Format Version | (7) File Signature |
    40     +----------------------+-------------------------+----------------------+--------------------------+-----------------+------------------------+--------------------+
    41     - Index Length: The length of the Index in bytes.
    42     - ByteSpan Count: (N) The number of ByteSpans in the Archive. (does not include the null ByteSpan)
    43     - Chunk Count: (M) The number of Chunk Records in the Archive.
    44        * These 3 values are all required to properly parse the Index. Note that the NBS Index has a deterministic size
    45          based on the Chunk Count. This is not the case with a Dolt Archive.
    46     - Metadata Length: The length of the Metadata in bytes.
    47     - CheckSums: See Below.
    48     - Format Version: Sequence starting at 1.
    49     - File Signature: Some would call this a magic number. Not on my watch. Dolt Archives have a 7 byte signature: "DOLTARC"
    50  
    51     CheckSums:
    52     +----------------------------+-------------------+----------------------+
    53     | (64) Sha512 ByteSpan 1 - N | (64) Sha512 Index | (64) Sha512 Metadata |
    54     +----------------------------+-------------------+----------------------+
    55     - The Sha512 checksums of the ByteSpans, Index, and Metadata. Currently unused, but may be used in the future. Leaves
    56       the opening to verify integrity manually at least, but could be used in the future to allow to break the file into
    57       parts, and ensure we can verify the integrity of each part.
    58  
    59  Index:
    60     +--------------+------------+-----------------+----------+
    61     | ByteSpan Map | Prefix Map | ChunkReferences | Suffixes |
    62     +--------------+------------+-----------------+----------+
    63     - The Index is a concatenation of 4 sections, the first three of which are compressed as one stream. The Suffixes are
    64       are not compressed because they won't compress well. For this reason there are two methods on the footer to get the
    65       the two spans individually.
    66  
    67     ByteSpan Map:
    68         +------------------+------------------+-----+------------------+
    69         | ByteSpanLength 1 | ByteSpanLength 2 | ... | ByteSpanLength N |
    70         +------------------+------------------+-----+------------------+
    71         - The Length of each ByteSpan is recorded as a varuint, and as we read them we will calculate the offset of each.
    72  
    73         The ByteSpan Map contains N ByteSpan Records. The index in the map is considered the ByteSpan's ID, and
    74         is used to reference the ByteSpan in the ChunkRefs. Note that the ByteSpan ID is 1-based, as 0 is reserved to indicate
    75         an empty ByteSpan.
    76  
    77     Prefix Map:
    78         +-------------------+-------------------+-----+---------------------------+
    79         | (Uint64) Prefix 0 | (Uint64) Prefix 1 | ... | (Uint64) Prefix Tuple M-1 |
    80         +-------------------+-------------------+-----+---------------------------+
    81         - The Prefix Map contains M Prefixes - one for each Chunk Record in the Table.
    82         - The Prefix Tuples are sorted, allowing for a binary search.
    83         - NB: THE SAME PREFIX MAY APPEAR MULTIPLE TIMES, as distinct Hashes (referring to distinct Chunks) may share the same Prefix.
    84         - The index into this map is the Ordinal of the Chunk Record.
    85  
    86     ChunkReferences:
    87         +------------+------------+-----+--------------+
    88         | ChunkRef 0 | ChunkRef 1 | ... | ChunkRef M-1 |
    89         +------------+------------+-----+--------------+
    90         ChunkRef:
    91             +-------------------------------+--------------------------+
    92             | (uvarint) Dictionary ByteSpan | (uvarint) Chunk ByteSpan |
    93             +-------------------------------+--------------------------+
    94          - Dictionary: ID for a ByteSpan to be used as zstd dictionary. 0 refers to the empty ByteSpan, which indicates no dictionary.
    95          - Chunk: ID for the ByteSpan containing the Chunk data. Never 0.
    96          - Dictionary and Chunk ByteSpans are constrained to be uint32, which is plenty. Varints can exceed this value, but we constrain them.
    97  
    98     Suffixes:
    99         +--------------------+--------------------+-----+----------------------+
   100         | (12) Hash Suffix 0 | (12) Hash Suffix 1 | ... | (12) Hash Suffix M-1 |
   101         +--------------------+--------------------+-----+----------------------+
   102  
   103       - Each Hash Suffix is the last 12 bytes of a Chunk in this Table.
   104       - Hash Suffix M must correspond to Prefix M and Chunk Record M
   105  
   106  Metadata:
   107     The Metadata section is intended to be used for additional information about the Archive. This may include the version
   108     of Dolt that created the archive, possibly references to other archives, or other information. For Format version 1,
   109     We use a simple JSON object. The Metadata Length is the length of the JSON object in bytes. Could be a Flatbuffer in
   110     the future, which would mandate a format version bump.
   111  
   112  ByteSpan:
   113     +----------------+
   114     | Data as []byte |
   115     +----------------+
   116       - Self Explanatory.
   117       - zStd automatically applies and checks CRC.
   118  
   119  Chunk Retrieval (phase 1 is similar to NBS):
   120  
   121    Phase one: Chunk Presence
   122    - Slice off the first 8 bytes of your Hash to create a Prefix
   123    - Since the Prefix Tuples in the Prefix Map are in lexicographic order, binary search the Prefix Map for the desired
   124      Prefix. To not mix terms with Index, we'll call this the Chunk Id, which is the 0-based index into the Prefix Map.
   125    - Using the Chunk Id found with a binary search, search locally for additional matching Prefixes. The matching indexes
   126      are all potential matches for the chunk you are looking for.
   127      - For each Chunk Id found, grab the corresponding Suffix, and compare to the Suffix of the Hash you are looking for.
   128      - If they match, your chunk is in this file in the Chunk Id which matched.
   129      - If they don't match, continue to the next matching Chunk Id.
   130    - If not found, your chunk is not in this Table.
   131    - If found, the given Chunk Id is the same index into the ChunkRef Map for the desired chunk.
   132  
   133    Phase two: Loading Chunk data
   134    - Take the Chunk Id discovered in Phase one, and use it to grab that index from the ChunkRefs Map.
   135    - Retrieve the ByteSpan Id for the Chunk data. Verify integrity with CRC.
   136    - If Dictionary is 0:
   137      - Decompress the Chunk data using zstd (no dictionary)
   138    - Otherwise:
   139      - Retrieve the ByteSpan ID for the Dictionary data.
   140      - Decompress the Chunk data using zstd with the Dictionary data.
   141  */
   142  
   143  const (
   144  	archiveFormatVersion = uint8(1)
   145  	archiveFileSignature = "DOLTARC"
   146  	archiveFileSigSize   = uint64(len(archiveFileSignature))
   147  	archiveCheckSumSize  = sha512.Size * 3 // sha512 3 times.
   148  	archiveFooterSize    = uint32Size +    // index length
   149  		uint32Size + // byte span count
   150  		uint32Size + // chunk count
   151  		uint32Size + // metadataSpan length
   152  		archiveCheckSumSize +
   153  		1 + // version byte
   154  		archiveFileSigSize
   155  )
   156  
   157  /*
   158  +----------------------+-------------------------+----------------------+--------------------------+-----------------+------------------------+--------------------+
   159  | (Uint32) IndexLength | (Uint32) ByteSpan Count | (Uint32) Chunk Count | (Uint32) Metadata Length | (192) CheckSums | (Uint8) Format Version | (7) File Signature |
   160  +----------------------+-------------------------+----------------------+--------------------------+-----------------+------------------------+--------------------+
   161  */
   162  const ( // afr = Archive FooteR
   163  	afrIndexLenOffset    = 0
   164  	afrByteSpanOffset    = afrIndexLenOffset + uint32Size
   165  	afrChunkCountOffset  = afrByteSpanOffset + uint32Size
   166  	afrMetaLenOffset     = afrChunkCountOffset + uint32Size
   167  	afrDataChkSumOffset  = afrMetaLenOffset + uint32Size
   168  	afrIndexChkSumOffset = afrDataChkSumOffset + sha512.Size
   169  	afrMetaChkSumOffset  = afrIndexChkSumOffset + sha512.Size
   170  	afrVersionOffset     = afrMetaChkSumOffset + sha512.Size
   171  	afrSigOffset         = afrVersionOffset + 1
   172  )
   173  
   174  var ErrInvalidChunkRange = errors.New("invalid chunk range")
   175  var ErrInvalidDictionaryRange = errors.New("invalid dictionary range")
   176  var ErrInvalidFileSignature = errors.New("invalid file signature")
   177  var ErrInvalidFormatVersion = errors.New("invalid format version")
   178  
   179  type sha512Sum [sha512.Size]byte
   180  
   181  type byteSpan struct {
   182  	offset uint64
   183  	length uint64
   184  }