diff --git a/Cargo.toml b/Cargo.toml index 2f51bf5d5ff5f83e023cbc17f4365d99ab837ce0..f3ae0bc5e1c27894369fc387d87d45318ff89b98 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,3 +8,4 @@ rust-version = "1.49.0" crate-type = ["staticlib"] [dependencies] +libz-sys = "1.1.23" diff --git a/nilla.nix b/nilla.nix new file mode 100644 index 0000000000000000000000000000000000000000..2e4d7f604e3eee3de57fc2999396a65ef472b322 --- /dev/null +++ b/nilla.nix @@ -0,0 +1,43 @@ +# SPDX-FileCopyrightText: 2025 FreshlyBakedCake +# +# SPDX-License-Identifier: MIT + +let + pins = import ./npins; + + nilla = import pins.nilla; +in +nilla.create ( + { config, lib }: + { + config = { + inputs.nixpkgs = { + src = pins.nixpkgs; + settings.overlays = [ config.inputs.fenix.result.overlays.default ]; + }; + + inputs.fenix.src = pins.fenix; + + shells.default = { + systems = [ "x86_64-linux" ]; + + shell = + { mkShell, git, fenix, bacon }: + mkShell { + inputsFrom = [ git ]; + packages = [ + (fenix.complete.withComponents [ + "cargo" + "clippy" + "rust-src" + "rustc" + "rustfmt" + "rust-analyzer" + ]) + bacon + ]; + }; + }; + }; + } +) diff --git a/npins/default.nix b/npins/default.nix new file mode 100644 index 0000000000000000000000000000000000000000..b844b313647b7da9740f535e4a3d552f67fbf6ee --- /dev/null +++ b/npins/default.nix @@ -0,0 +1,247 @@ +/* + This file is provided under the MIT licence: + + Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ +# Generated by npins. Do not modify; will be overwritten regularly +let + # Backwards-compatibly make something that previously didn't take any arguments take some + # The function must return an attrset, and will unfortunately be eagerly evaluated + # Same thing, but it catches eval errors on the default argument so that one may still call it with other arguments + mkFunctor = + fn: + let + e = builtins.tryEval (fn { }); + in + (if e.success then e.value else { error = fn { }; }) // { __functor = _self: fn; }; + + # https://github.com/NixOS/nixpkgs/blob/0258808f5744ca980b9a1f24fe0b1e6f0fecee9c/lib/lists.nix#L295 + range = + first: last: if first > last then [ ] else builtins.genList (n: first + n) (last - first + 1); + + # https://github.com/NixOS/nixpkgs/blob/0258808f5744ca980b9a1f24fe0b1e6f0fecee9c/lib/strings.nix#L257 + stringToCharacters = s: map (p: builtins.substring p 1 s) (range 0 (builtins.stringLength s - 1)); + + # https://github.com/NixOS/nixpkgs/blob/0258808f5744ca980b9a1f24fe0b1e6f0fecee9c/lib/strings.nix#L269 + stringAsChars = f: s: concatStrings (map f (stringToCharacters s)); + concatStrings = builtins.concatStringsSep ""; + + # If the environment variable NPINS_OVERRIDE_${name} is set, then use + # the path directly as opposed to the fetched source. + # (Taken from Niv for compatibility) + mayOverride = + name: path: + let + envVarName = "NPINS_OVERRIDE_${saneName}"; + saneName = stringAsChars (c: if (builtins.match "[a-zA-Z0-9]" c) == null then "_" else c) name; + ersatz = builtins.getEnv envVarName; + in + if ersatz == "" then + path + else + # this turns the string into an actual Nix path (for both absolute and + # relative paths) + builtins.trace "Overriding path of \"${name}\" with \"${ersatz}\" due to set \"${envVarName}\"" ( + if builtins.substring 0 1 ersatz == "/" then + /. + ersatz + else + /. + builtins.getEnv "PWD" + "/${ersatz}" + ); + + mkSource = + name: spec: + { + pkgs ? null, + }: + assert spec ? type; + let + # Unify across builtin and pkgs fetchers. + # `fetchGit` requires a wrapper because of slight API differences. + fetchers = + if pkgs == null then + { + inherit (builtins) fetchTarball fetchurl; + # For some fucking reason, fetchGit has a different signature than the other builtin fetchers … + fetchGit = args: (builtins.fetchGit args).outPath; + } + else + { + fetchTarball = + { + url, + sha256, + }: + pkgs.fetchzip { + inherit url sha256; + extension = "tar"; + }; + inherit (pkgs) fetchurl; + fetchGit = + { + url, + submodules, + rev, + name, + narHash, + }: + pkgs.fetchgit { + inherit url rev name; + fetchSubmodules = submodules; + hash = narHash; + }; + }; + + # Dispatch to the correct code path based on the type + path = + if spec.type == "Git" then + mkGitSource fetchers spec + else if spec.type == "GitRelease" then + mkGitSource fetchers spec + else if spec.type == "PyPi" then + mkPyPiSource fetchers spec + else if spec.type == "Channel" then + mkChannelSource fetchers spec + else if spec.type == "Tarball" then + mkTarballSource fetchers spec + else if spec.type == "Container" then + mkContainerSource pkgs spec + else + builtins.throw "Unknown source type ${spec.type}"; + in + spec // { outPath = mayOverride name path; }; + + mkGitSource = + { + fetchTarball, + fetchGit, + ... + }: + { + repository, + revision, + url ? null, + submodules, + hash, + ... + }: + assert repository ? type; + # At the moment, either it is a plain git repository (which has an url), or it is a GitHub/GitLab repository + # In the latter case, there we will always be an url to the tarball + if url != null && !submodules then + fetchTarball { + inherit url; + sha256 = hash; + } + else + let + url = + if repository.type == "Git" then + repository.url + else if repository.type == "GitHub" then + "https://github.com/${repository.owner}/${repository.repo}.git" + else if repository.type == "GitLab" then + "${repository.server}/${repository.repo_path}.git" + else + throw "Unrecognized repository type ${repository.type}"; + urlToName = + url: rev: + let + matched = builtins.match "^.*/([^/]*)(\\.git)?$" url; + + short = builtins.substring 0 7 rev; + + appendShort = if (builtins.match "[a-f0-9]*" rev) != null then "-${short}" else ""; + in + "${if matched == null then "source" else builtins.head matched}${appendShort}"; + name = urlToName url revision; + in + fetchGit { + rev = revision; + narHash = hash; + + inherit name submodules url; + }; + + mkPyPiSource = + { fetchurl, ... }: + { + url, + hash, + ... + }: + fetchurl { + inherit url; + sha256 = hash; + }; + + mkChannelSource = + { fetchTarball, ... }: + { + url, + hash, + ... + }: + fetchTarball { + inherit url; + sha256 = hash; + }; + + mkTarballSource = + { fetchTarball, ... }: + { + url, + locked_url ? url, + hash, + ... + }: + fetchTarball { + url = locked_url; + sha256 = hash; + }; + + mkContainerSource = + pkgs: + { + image_name, + image_tag, + image_digest, + ... + }: + if pkgs == null then + builtins.throw "container sources require passing in a Nixpkgs value: https://github.com/andir/npins/blob/master/README.md#using-the-nixpkgs-fetchers" + else + pkgs.dockerTools.pullImage { + imageName = image_name; + imageDigest = image_digest; + finalImageTag = image_tag; + }; +in +mkFunctor ( + { + input ? ./sources.json, + }: + let + data = + if builtins.isPath input then + # while `readFile` will throw an error anyways if the path doesn't exist, + # we still need to check beforehand because *our* error can be caught but not the one from the builtin + # *piegames sighs* + if builtins.pathExists input then + builtins.fromJSON (builtins.readFile input) + else + throw "Input path ${toString input} does not exist" + else if builtins.isAttrs input then + input + else + throw "Unsupported input type ${builtins.typeOf input}, must be a path or an attrset"; + version = data.version; + in + if version == 7 then + builtins.mapAttrs (name: spec: mkFunctor (mkSource name spec)) data.pins + else + throw "Unsupported format version ${toString version} in sources.json. Try running `npins upgrade`" +) diff --git a/npins/sources.json b/npins/sources.json new file mode 100644 index 0000000000000000000000000000000000000000..f559efda2c72e5c99cd30f0831dba950f4b7d713 --- /dev/null +++ b/npins/sources.json @@ -0,0 +1,40 @@ +{ + "pins": { + "fenix": { + "type": "Git", + "repository": { + "type": "GitHub", + "owner": "nix-community", + "repo": "fenix" + }, + "branch": "main", + "submodules": false, + "revision": "a563f057979806c59da53070297502eb7af22f62", + "url": "https://github.com/nix-community/fenix/archive/a563f057979806c59da53070297502eb7af22f62.tar.gz", + "hash": "sha256-WfItZn6duisxCxyltbu7Hs7kxzNeylgZGOwCYwHe26g=" + }, + "nilla": { + "type": "GitRelease", + "repository": { + "type": "GitHub", + "owner": "nilla-nix", + "repo": "nilla" + }, + "pre_releases": true, + "version_upper_bound": null, + "release_prefix": null, + "submodules": false, + "version": "v0.0.0-alpha.14", + "revision": "2e98ae315a592ad6b6de44670514c048dcc88dc7", + "url": "https://api.github.com/repos/nilla-nix/nilla/tarball/refs/tags/v0.0.0-alpha.14", + "hash": "sha256-15lwhWcMonJH6UholMMHDc+p2BoSpGA4AYGrsXQA9Do=" + }, + "nixpkgs": { + "type": "Channel", + "name": "nixpkgs-unstable", + "url": "https://releases.nixos.org/nixpkgs/nixpkgs-25.11pre887438.a7fc11be66bd/nixexprs.tar.xz", + "hash": "sha256-LDT9wuUZtjPfmviCcVWif5+7j4kBI2mWaZwjNNeg4eg=" + } + }, + "version": 7 +} diff --git a/src/lib.rs b/src/lib.rs index 9da70d8b57d5f67e41b8f26e729b8875663a72ae..168c39e2bffa30becfe4606d25b7963929b4ddc9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1 +1,2 @@ +pub mod reftable; pub mod varint; diff --git a/src/reftable/basics.rs b/src/reftable/basics.rs new file mode 100644 index 0000000000000000000000000000000000000000..cfca69d5b58e45b05b97d054636873de6aafc2b2 --- /dev/null +++ b/src/reftable/basics.rs @@ -0,0 +1,64 @@ +#[derive(Debug, Clone)] +pub struct ReftableBuf { + pub data: Vec, + pub len: usize, +} + +impl ReftableBuf { + pub fn new() -> Self { + Self { + data: Vec::new(), + len: 0, + } + } + + pub fn with_capacity(capacity: usize) -> Self { + Self { + data: Vec::with_capacity(capacity), + len: 0, + } + } + + pub fn clear(&mut self) { + self.data.clear(); + self.len = 0; + } + + pub fn as_slice(&self) -> &[u8] { + &self.data[..self.len] + } +} + +impl Default for ReftableBuf { + fn default() -> Self { + Self::new() + } +} + +pub fn binsearch(sz: usize, mut f: F) -> usize +where + F: FnMut(usize) -> Result, +{ + todo!() +} + +pub fn names_length(names: &[&str]) -> usize { + todo!() +} + +pub fn parse_names(buf: &str) -> Result, String> { + todo!() +} + +pub fn common_prefix_size(a: &ReftableBuf, b: &ReftableBuf) -> usize { + todo!() +} + +pub enum HashAlgorithm { + Sha1 = 89, + Sha256 = 247, +} + +pub fn hash_size(id: HashAlgorithm) -> u32 { + todo!() +} diff --git a/src/reftable/block.rs b/src/reftable/block.rs new file mode 100644 index 0000000000000000000000000000000000000000..45d7148b5068199e96a104e0c378c6e0989d5678 --- /dev/null +++ b/src/reftable/block.rs @@ -0,0 +1,59 @@ +use crate::reftable::{basics::ReftableBuf, blocksource::ReftableBlockData}; +use libz_sys::z_stream; + +pub struct BlockWriter { + pub zstream: Box, + pub compressed: Vec, + + pub block: Vec, + pub block_size: u32, + + pub header_off: u32, + + pub restart_interval: u16, + pub hash_size: u32, + + pub next: u32, + pub restarts: Vec, + pub restart_len: u32, + pub restart_cap: u32, + + pub last_key: ReftableBuf, + + pub scratch: ReftableBuf, + pub entries: i32, +} + +pub struct BlockIter { + pub next_off: u32, + pub block: Vec, + + pub last_key: ReftableBuf, + pub scratch: ReftableBuf, +} + +pub struct ReftableBlock { + pub header_off: u32, + + pub block_data: ReftableBlockData, + pub hash_size: usize, + + pub zstream: Box, + pub uncompressed_data: Vec, + + pub restart_count: u16, + pub restart_off: u32, + + pub full_block_size: u32, + pub block_type: u8, +} + +pub struct BlockReader { + pub zstream: Box, + pub uncompressed: Vec, + pub uncompressed_len: usize, + + pub block: ReftableBlock, + + pub pos: usize, +} diff --git a/src/reftable/blocksource.rs b/src/reftable/blocksource.rs new file mode 100644 index 0000000000000000000000000000000000000000..02f4ca617a74d01c308633e02528d70d60eda299 --- /dev/null +++ b/src/reftable/blocksource.rs @@ -0,0 +1,21 @@ +pub struct ReftableBlockData { + data: Vec, + source: ReftableBlockSource, +} + +impl AsRef<[u8]> for ReftableBlockData { + fn as_ref(&self) -> &[u8] { + &self.data + } +} + +pub trait ReftableBlockSourceOps { + fn size(&self) -> u64; + fn read_block(&self, offset: u64, size: u32) -> Result, String>; + fn release_block(&self, data: &ReftableBlockData) -> Result<(), String>; + fn close(&mut self) -> Result<(), String>; +} + +pub struct ReftableBlockSource { + ops: Box, +} diff --git a/src/reftable/error.rs b/src/reftable/error.rs new file mode 100644 index 0000000000000000000000000000000000000000..515fdb7eb97eb53e4ef25f298a4bec390c44acae --- /dev/null +++ b/src/reftable/error.rs @@ -0,0 +1,34 @@ +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ReftableError { + General(String), + Io, + Format, + NotExist, + Lock, + Api, + Zlib, + EmptyTable, + Refname, + EntryTooBig, + Outdated, + OutOfMemory, +} + +impl std::fmt::Display for ReftableError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ReftableError::General(msg) => write!(f, "general error: {}", msg), + ReftableError::Io => write!(f, "I/O error"), + ReftableError::Format => write!(f, "corrupt reftable file"), + ReftableError::NotExist => write!(f, "file does not exist"), + ReftableError::Lock => write!(f, "data is locked"), + ReftableError::Api => write!(f, "misuse of the reftable API"), + ReftableError::Zlib => write!(f, "zlib failure"), + ReftableError::EmptyTable => write!(f, "wrote empty table"), + ReftableError::Refname => write!(f, "invalid refname"), + ReftableError::EntryTooBig => write!(f, "entry too large"), + ReftableError::Outdated => write!(f, "data concurrently modified"), + ReftableError::OutOfMemory => write!(f, "out of memory"), + } + } +} diff --git a/src/reftable/fsck.rs b/src/reftable/fsck.rs new file mode 100644 index 0000000000000000000000000000000000000000..c8645c318bce5fddec67a906cec57221765e7eda --- /dev/null +++ b/src/reftable/fsck.rs @@ -0,0 +1,28 @@ +use crate::reftable::reftable_table::ReftableTable; + +pub enum ReftableFsckError { + TableName, + MaxValue, +} + +pub struct ReftableFsckInfo { + pub msg: String, + pub path: String, + pub error: ReftableFsckError, +} + +pub fn table_has_valid_name(name: &str) -> bool { + todo!() +} + +pub fn table_check_name(table: &ReftableTable) -> Result<(), String> { + todo!() +} + +pub fn table_checks(table: &ReftableTable) -> Result<(), String> { + todo!() +} + +pub fn reftable_fsck_check(stack: &ReftableStack) -> Result<(), String> { + todo!() +} diff --git a/src/reftable/iter.rs b/src/reftable/iter.rs new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/src/reftable/merged.rs b/src/reftable/merged.rs new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/src/reftable/mod.rs b/src/reftable/mod.rs new file mode 100644 index 0000000000000000000000000000000000000000..b1968abe14c1628ae680d577f0f96ca4e0b78577 --- /dev/null +++ b/src/reftable/mod.rs @@ -0,0 +1,15 @@ +pub mod basics; +pub mod block; +pub mod blocksource; +pub mod error; +pub mod fsck; +pub mod iter; +pub mod merged; +pub mod pq; +pub mod record; +pub mod reftable_table; +pub mod stack; +pub mod system; +pub mod table; +pub mod tree; +pub mod write; diff --git a/src/reftable/pq.rs b/src/reftable/pq.rs new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/src/reftable/record.rs b/src/reftable/record.rs new file mode 100644 index 0000000000000000000000000000000000000000..a4684d38c79ec5eba77fb21da02c88aa8e7c5347 --- /dev/null +++ b/src/reftable/record.rs @@ -0,0 +1,148 @@ +use crate::reftable::basics::ReftableBuf; + +pub const REFTABLE_HASH_SIZE_MAX: usize = 32; +pub const REFTABLE_HASH_SIZE_SHA1: usize = 20; +pub const REFTABLE_HASH_SIZE_SHA256: usize = 32; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum RefValueType { + Deletion = 0x0, + Val1 = 0x1, + Val2 = 0x2, + SymRef = 0x3, +} + +impl std::convert::TryFrom for RefValueType { + type Error = String; + + fn try_from(value: u8) -> Result { + match value { + 0x0 => Ok(RefValueType::Deletion), + 0x1 => Ok(RefValueType::Val1), + 0x2 => Ok(RefValueType::Val2), + 0x3 => Ok(RefValueType::SymRef), + _ => Err(format!("Unknown ref value type: {value}")), + } + } +} + +#[derive(Debug, Clone)] +pub enum RefValue { + Val1([u8; REFTABLE_HASH_SIZE_MAX]), + Val2 { + value: [u8; REFTABLE_HASH_SIZE_MAX], + target_value: [u8; REFTABLE_HASH_SIZE_MAX], + }, + SymRef(String), +} + +#[derive(Debug, Clone)] +pub struct ReftableRefRecord { + pub refname: String, + pub update_index: u64, + pub value: Option, +} + +impl ReftableRefRecord { + pub fn equals(&self, other: &ReftableRefRecord, hash_size: u32) -> bool { + todo!() + } +} + +impl PartialEq for ReftableRefRecord { + fn eq(&self, other: &ReftableRefRecord) -> bool { + self.equals(other, REFTABLE_HASH_SIZE_MAX as u32) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum LogValueType { + Deletion = 0x0, + Update = 0x1, +} + +impl std::convert::TryFrom for LogValueType { + type Error = String; + + fn try_from(value: u8) -> Result { + match value { + 0x0 => Ok(LogValueType::Deletion), + 0x1 => Ok(LogValueType::Update), + _ => Err(format!("Unknown log value type: {value}")), + } + } +} + +#[derive(Debug, Clone)] +pub struct LogUpdate { + pub new_hash: [u8; REFTABLE_HASH_SIZE_MAX], + pub old_hash: [u8; REFTABLE_HASH_SIZE_MAX], + pub name: String, + pub email: String, + pub time: u64, + pub tz_offset: i16, + pub message: String, +} + +#[derive(Debug, Clone)] +pub struct ReftableLogRecord { + pub refname: String, + pub update_index: u64, + pub value: Option, +} + +impl ReftableLogRecord { + pub fn equals(&self, other: &ReftableLogRecord, hash_size: u32) -> bool { + todo!() + } +} + +impl PartialEq for ReftableLogRecord { + fn eq(&self, other: &ReftableLogRecord) -> bool { + self.equals(other, REFTABLE_HASH_SIZE_MAX as u32) + } +} + +#[derive(Debug, Clone)] +pub struct ReftableObjRecord { + pub hash_prefix: Vec, + pub offsets: Vec, +} + +impl PartialEq for ReftableObjRecord { + fn eq(&self, other: &Self) -> bool { + self.hash_prefix == other.hash_prefix && self.offsets == other.offsets + } +} + +#[derive(Debug, Clone)] +pub struct ReftableIndexRecord { + pub offset: u64, + pub last_key: ReftableBuf, +} + +impl PartialEq for ReftableIndexRecord { + fn eq(&self, other: &Self) -> bool { + self.offset == other.offset && self.last_key.as_slice() == other.last_key.as_slice() + } +} + +#[derive(Debug, Clone)] +pub enum ReftableRecord { + Ref(ReftableRefRecord), + Log(ReftableLogRecord), + Obj(ReftableObjRecord), + Index(ReftableIndexRecord), +} + +impl PartialEq for ReftableRecord { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (ReftableRecord::Ref(a), ReftableRecord::Ref(b)) => a == b, + (ReftableRecord::Log(a), ReftableRecord::Log(b)) => a == b, + (ReftableRecord::Obj(a), ReftableRecord::Obj(b)) => a == b, + (ReftableRecord::Index(a), ReftableRecord::Index(b)) => a == b, + _ => false, + } + } +} diff --git a/src/reftable/reftable_table.rs b/src/reftable/reftable_table.rs new file mode 100644 index 0000000000000000000000000000000000000000..9c795df7adf62e3cf0413309f55283ec08520349 --- /dev/null +++ b/src/reftable/reftable_table.rs @@ -0,0 +1,14 @@ +pub struct ReftableTable { + pub name: String, + pub source: ReftableBlockSource, + pub size: u64, + pub hash_id: ReftableHash, + pub block_size: u32, + pub min_update_index: u64, + pub max_update_index: u64, + pub object_id_len: i32, // TODO: is this the right integer size? (originally was int) + pub version: i32, // TODO: is this the right integer size? (originally was int) + pub ref_offsets: ReftableTableOffsets, + pub obj_offsets: ReftableTableOffsets, + pub log_offsets: ReftableTableOffsets, +} diff --git a/src/reftable/stack.rs b/src/reftable/stack.rs new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/src/reftable/system.rs b/src/reftable/system.rs new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/src/reftable/table.rs b/src/reftable/table.rs new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/src/reftable/tree.rs b/src/reftable/tree.rs new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/src/reftable/write.rs b/src/reftable/write.rs new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391