// Copyright (c) 2025, Christian Lee Seibold // All rights reserved. // // Redistribution and use in source and binary forms, with or without // modification, are permitted provided that the following conditions are met: // // 1. Redistributions of source code must retain the above copyright notice, this // list of conditions and the following disclaimer. // // 2. Redistributions in binary form must reproduce the above copyright notice, // this list of conditions and the following disclaimer in the documentation // and/or other materials provided with the distribution. // // 3. Neither the name of the copyright holder nor the names of its // contributors may be used to endorse or promote products derived from // this software without specific prior written permission. // // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. package main import ( "errors" "io" "os" "path/filepath" "strconv" "strings" "github.com/klauspost/compress/zstd" ) type ObjectType uint const ( ObjectType_Unknown ObjectType = iota ObjectType_Commit ObjectType_Tree ObjectType_Blob ) type ObjectStore struct { vcsDirectory string } func (s *ObjectStore) GetBranches() ([]string, error) { entries, err := os.ReadDir(filepath.Join(s.vcsDirectory + "/refs/branches/")) if err != nil { return nil, err } var branchNames []string for _, entry := range entries { branchNames = append(branchNames, entry.Name()) } return branchNames, nil } // Gets an ObjectReader to read the object uncompressed. Object must be closed after using. func (s *ObjectStore) GetObject(hash Hash) (*ObjectReader, error) { path := filepath.Join(s.vcsDirectory, "objects", hash.HexString()) file, err := os.Open(path) if err != nil { return nil, err } decoder, err := zstd.NewReader(file) if err != nil { return nil, err } // Read header header := strings.Builder{} var b = [1]byte{} for { _, err = decoder.Read(b[:]) if err != nil && err != io.EOF { break } if b[0] == byte('\000') { break } header.WriteByte(b[0]) } typeString, sizeString, _ := strings.Cut(header.String(), " ") objType := ObjectType_Unknown switch typeString { case "blob": objType = ObjectType_Blob case "tree": objType = ObjectType_Tree case "commit": objType = ObjectType_Commit } size, _ := strconv.ParseUint(sizeString, 10, 64) return &ObjectReader{Type: objType, Size: size, f: file, Decoder: decoder}, nil } // Generic object to be read from type ObjectReader struct { Type ObjectType Size uint64 f *os.File *zstd.Decoder } // Closes the compression decoder and the file. func (o ObjectReader) Close() error { o.IOReadCloser().Close() return o.f.Close() } var ErrObjectExists = errors.New("Object already exists.") // Write an object to the objects store from the reader, automatically handling the compression. // Returns ErrObjectExists if an object with the given hash already exists. func (s *ObjectStore) WriteObject(hash Hash, reader io.Reader) error { path := filepath.Join(s.vcsDirectory, "objects", hash.HexString()) // Return error if object already exists. if _, err := os.Stat(path); err == nil { return ErrObjectExists } file, err := os.OpenFile(path, os.O_CREATE|os.O_RDWR|os.O_EXCL, 0644) if err != nil { return err } zw, err := zstd.NewWriter(file, zstd.WithEncoderLevel(zstd.SpeedBestCompression)) if err != nil { return err } _, err = io.Copy(zw, reader) if err != nil { return err } file.Sync() zw.Close() file.Close() return nil }