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 }