You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
DateKeys/cmd/datekeys/extract.go

111 lines
2.6 KiB

Format 3, step 5: the CLI datekeys encrypt writes format 3 and datekeys decrypt writes its files to a new folder, with the presentation of spec 29.7. - encrypt: -in is repeatable and takes files and folders; a folder gives its name as the first segment, as a browser does, and is walked with Lstat, following no link, taking regular files only. .DS_Store, Thumbs.db, desktop.ini, ._* and __MACOSX are left out of folders, and each one left out is reported (62.1 rule 15). New -comment, -author and -no-mtime; the mtimes are kept by default (rule 16). A capsule may hold a comment alone. The copy of a pipe to a temporary file goes, as only regular files are taken. - decrypt: the prelude decides. Format 3 claims -out with os.Mkdir, only when there are files, stages the tree in -out/.datekeys-* through an os.Root with O_EXCL and mode 0600, sets the mtimes, and moves each entry of the first level into place at step 18; any failure removes the folder (spec 56). Formats 1 and 2 still write a file. - The presentation goes to stdout: the verdicts, the declared author and the comment box with their labels, the paths, and the verdicts again. Every line of the creator goes in pieces of at most W - 3 columns behind the prefix, counting 2 for anything but printable ASCII, with its TABs expanded to multiples of 8; W is the width of the terminal, asked with syscall on Unix and Windows, or 80. Risky names get a warning: shortcuts, desktop.ini, .git, programs and a leading dash, compared by their key of R7. - Encrypt no longer runs in the CLI: only the test data generators set TestVectors. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 week ago
package main
import (
"crypto/rand"
"encoding/hex"
"errors"
"io"
"os"
"path"
"path/filepath"
"strings"
"time"
"g.activething.com/go/DateKeys/capsule"
)
// dirSink writes the files of a format 3 capsule to a new directory (spec
// §56). Begin claims dir with os.Mkdir, which fails if it exists, and stages
// the tree in dir/.datekeys-*, through an os.Root, which follows no link out
// of dir, with O_EXCL and mode 0600; R10 keeps every entry of the head off
// that name. Commit sets the mtimes and moves each entry of the first level
// into dir. Abort removes dir. A capsule without files creates nothing.
type dirSink struct {
dir string
head *capsule.Head
root *os.Root
stage string
created bool
}
func (s *dirSink) Begin(h *capsule.Head) error {
s.head = h
if len(h.Files) == 0 {
return nil
}
if err := os.Mkdir(s.dir, 0o700); err != nil {
if errors.Is(err, os.ErrExist) {
return errors.New(s.dir + " already exists; outputs are never overwritten")
}
return err
}
s.created = true
root, err := os.OpenRoot(s.dir)
if err != nil {
return err
}
s.root = root
var id [8]byte
_, _ = rand.Read(id[:]) // never fails since Go 1.24
s.stage = ".datekeys-" + hex.EncodeToString(id[:])
return root.Mkdir(s.stage, 0o700)
}
// staged is the name, within the root, of file i while it is staged.
func (s *dirSink) staged(i int) string {
return filepath.FromSlash(s.stage + "/" + s.head.Files[i].Path)
}
func (s *dirSink) Create(i int) (io.WriteCloser, error) {
name := s.staged(i)
if dir := filepath.Dir(name); dir != s.stage {
if err := s.root.MkdirAll(dir, 0o700); err != nil {
return nil, err
}
}
return s.root.OpenFile(name, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o600)
}
// Commit runs at step 18, once every check of step 17 has passed.
func (s *dirSink) Commit() error {
if s.root == nil {
return nil
}
for i, f := range s.head.Files {
if f.HasMTime {
t := time.Unix(int64(f.MTime), 0)
if err := s.root.Chtimes(s.staged(i), t, t); err != nil {
return err
}
}
}
moved := map[string]bool{}
for _, f := range s.head.Files {
first, _, _ := strings.Cut(f.Path, "/")
if moved[first] {
continue
}
if err := s.root.Rename(filepath.FromSlash(path.Join(s.stage, first)), first); err != nil {
return err
}
moved[first] = true
}
if err := s.root.Remove(s.stage); err != nil {
return err
}
// The files are in place: closing the root changes nothing of them.
s.root.Close()
s.root = nil
return nil
}
// Abort removes everything the sink created: dir itself.
func (s *dirSink) Abort() {
if s.root != nil {
s.root.Close()
}
if s.created {
os.RemoveAll(s.dir)
}
}

Powered by TurnKey Linux.