-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathsource.go
More file actions
142 lines (131 loc) · 4.84 KB
/
Copy pathsource.go
File metadata and controls
142 lines (131 loc) · 4.84 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
package waxlabel
import (
"context"
"fmt"
"io"
"os"
"github.com/colespringer/waxlabel/internal/core"
"github.com/colespringer/waxlabel/waxerr"
)
// fileSource adapts an open file to ReaderAtSized for the duration of a parse
// or a write. It is never retained by a Document.
type fileSource struct {
f *os.File
size int64
}
func openFileSource(path string) (*fileSource, error) {
// Stat before Open: opening the read end of a FIFO (or certain device files)
// blocks until a writer appears, so a non-regular path must be rejected before
// os.Open or the parse hangs before any guard can run. This stat-first check is
// the library backstop that stops the hang for every caller (the CLI adds a
// friendlier exit-2 layer on top). os.Stat follows symlinks, so a symlink to a
// regular file is still accepted; the check also folds in the former directory
// guard, keeping its specific message.
info, err := os.Stat(path)
if err != nil {
return nil, err
}
if !info.Mode().IsRegular() {
if info.IsDir() {
return nil, fmt.Errorf("%w: %s is a directory, not a file", waxerr.ErrInvalidData, path)
}
return nil, fmt.Errorf("%w: %s is not a regular file", waxerr.ErrInvalidData, path)
}
f, err := os.Open(path)
if err != nil {
return nil, err
}
// Take the size from the open descriptor, not the pre-open stat: that stat only
// gated the file kind, and a concurrent truncate/extend in the stat-then-open
// window could have made its size stale. A regular file's f.Stat never blocks
// (the FIFO hazard is os.Open, already past), so this is safe and authoritative.
fi, err := f.Stat()
if err != nil {
f.Close()
return nil, err
}
return &fileSource{f: f, size: fi.Size()}, nil
}
func (s *fileSource) ReadAt(p []byte, off int64) (int, error) { return s.f.ReadAt(p, off) }
func (s *fileSource) Size() int64 { return s.size }
func (s *fileSource) Close() error { return s.f.Close() }
// fileIdentity captures a file's strong identity: size, mtime, and (where the
// OS exposes them) inode and device. These detect a same-source save against a
// file that changed underneath us.
func fileIdentity(path string) (core.Identity, error) {
info, err := os.Stat(path)
if err != nil {
return core.Identity{}, err
}
id := core.Identity{
Path: path,
Size: info.Size(),
ModTimeUnixNano: info.ModTime().UnixNano(),
}
id.INode, id.Device = sysInodeDevice(info)
return id, nil
}
// resolveSource selects the bytes to read for a write or a hash: an explicit
// source, else the document's in-memory source (from OpenSource), else its file
// reopened (from ParseFile). The returned closer must always be called. Both
// the write path and the hashing path share this.
func (d *Document) resolveSource(explicit core.ReaderAtSized) (core.ReaderAtSized, func(), error) {
noop := func() {}
if explicit != nil {
return explicit, noop, nil
}
if d.src != nil {
return d.src, noop, nil
}
if d.path != "" {
fs, err := openFileSource(d.path)
if err != nil {
return nil, noop, err
}
return fs, func() { fs.Close() }, nil
}
return nil, noop, fmt.Errorf("%w: no source available; supply one via WriteTo or WithHashSource", waxerr.ErrInvalidData)
}
// Source retains the complete bytes of a non-seekable stream that was parsed
// for editing. Unlike a Document (which is detached and holds nothing), a
// Source is closable: it owns the teed buffer. Edit the [Source.Document] and
// save it; the Source supplies the original bytes the rewrite copies.
type Source struct {
doc *Document
data []byte
}
// OpenSource parses a non-seekable stream, teeing the complete stream into
// memory as it reads (you cannot spool bytes after they have passed). The
// returned Source is closable and its Document can be edited and saved.
func OpenSource(ctx context.Context, r io.Reader, opts ...ParseOption) (*Source, error) {
if err := checkContext(ctx); err != nil {
return nil, err
}
// io.ReadAll(nil) panics; reject a nil reader with a clean error first, mirroring
// the context guard above and Parse's nil-source guard.
if r == nil {
return nil, fmt.Errorf("%w: nil reader", waxerr.ErrInvalidData)
}
data, err := io.ReadAll(r)
if err != nil {
return nil, err
}
doc, err := parseSource(ctx, core.BytesSource(data), "", resolveParseOptions(opts))
if err != nil {
return nil, err
}
doc.src = core.BytesSource(data)
return &Source{doc: doc, data: data}, nil
}
// Document returns the parsed document. It remains valid after Close (it is
// detached); only the Source's role as a write source ends at Close.
func (s *Source) Document() *Document { return s.doc }
// Close releases the retained buffer. After Close the Document can still be
// read, but saving it requires supplying a source explicitly.
func (s *Source) Close() error {
s.data = nil
if s.doc != nil {
s.doc.src = nil
}
return nil
}