Skip to content

Commit 9dbc9cf

Browse files
committed
file: Appending new segments
Adds the ability to append new segments right before the SOS segment. Uses xio.Splice to handle that actual file manipulation which works on a temp copy of the file to avoid corrupting the original before moving it back.
1 parent db4c6b5 commit 9dbc9cf

File tree

6 files changed

+278
-37
lines changed

6 files changed

+278
-37
lines changed

file.go

Lines changed: 134 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
package jfif
22

33
import (
4+
"bytes"
45
"errors"
56
"io"
7+
"math"
68
"os"
9+
10+
xio "neilpa.me/go-x/io"
711
)
812

913
var (
@@ -13,24 +17,84 @@ var (
1317
// ErrOversizePayload means there's not enough space to update
1418
// the segment data in-place.
1519
ErrOversizePayload = errors.New("Oversize payload")
20+
21+
// ErrOversizeSegment means segment data was to large to append.
22+
ErrOversizeSegment = errors.New("Oversize segment")
1623
)
1724

25+
// Patch is used to insert new JFIF segments just before the SOS segment.
26+
type Patch struct { // TODO better name or use segments directly ignoring offset
27+
// Marker is the type of segment
28+
Marker Marker
29+
// Data are the segment bytes that will be appended. Max size is 0xFFFF-2
30+
Data []byte
31+
}
32+
33+
// Append new JFIF segments to the file at path.
34+
//
35+
// Notes:
36+
// - Under the hood this creates a temp-copy of the original file so
37+
// that it can safely insert the new segments in the middle of the
38+
// file. This avoids potential for corrupting data if an error is
39+
// hit in the middle of the update. At the end the original path
40+
// is replaced with a single os.Rename operation.
41+
//
42+
// TODO: Higher-level version of this that could be smarter for XMP data
43+
// TODO: Return the updated pointer data?
44+
func Append(path string, patches ...Patch) error {
45+
// Prep the buffer for writing
46+
var buf bytes.Buffer
47+
for _, p := range patches {
48+
seg := Segment{}
49+
seg.Marker = p.Marker
50+
seg.Offset = -1
51+
52+
l := len(seg.Data) + 2
53+
if l > math.MaxUint16 {
54+
return ErrOversizeSegment
55+
}
56+
seg.Length = uint16(l) // TODO not right for oversize segments
57+
// TODO: what about an embedded Data where the first two bytes are the length
58+
seg.Data = p.Data
59+
60+
// TODO Would be nice to avoid yet-another-copy of data and plumb
61+
// through a custom reader that calculated the size
62+
if err := EncodeSegment(&buf, seg); err != nil {
63+
return err
64+
}
65+
}
66+
67+
f, err := os.Open(path)
68+
ptrs, err := ScanSegments(f)
69+
if err != nil {
70+
return err
71+
}
72+
last := ptrs[len(ptrs)-1]
73+
74+
return xio.SpliceFile(f, buf.Bytes(), last.Offset)
75+
}
76+
1877
// File is used to perform in-place updates to JFIF segments to a backing
1978
// file on-disk.
2079
type File struct {
2180
// f is the underlying file on disk.
2281
f *os.File
2382

24-
// refs are the intially scanned segment pointers.
25-
refs []SegmentP
83+
// refs are the minimally scanned segment pointers.
84+
refs []Pointer
2685
}
2786

2887
// Edit opens and scans segments from a JFIF file. This should be
2988
// used to replace segments in-place without having to re-write the
3089
// full file. Note that this will fail on attempts to write segments
31-
// that would expend beyond the current bounds. Otherise, "short-segments"
32-
// retain the desired size but there are 0xFF fill bytes used for padding
33-
// until the next segment.
90+
// that would expand beyond the current bounds.
91+
//
92+
// TODO: Otherise, "short-segments" retain the desired size but there
93+
// are 0xFF fill bytes used for padding until the next segment.
94+
//
95+
// TODO: This may not be all that valuable verse doing a proper splice.
96+
// in a copied version of the file and replacing over top of it. This
97+
// can lead to file corruption if not careful...
3498
func Edit(path string) (*File, error) {
3599
f, err := os.OpenFile(path, os.O_RDWR, 0)
36100
if err != nil {
@@ -57,8 +121,8 @@ func (f *File) Close() (err error) {
57121
}
58122

59123
// Query finds existing segments that matches the given marker
60-
func (f *File) Query(m Marker) ([]SegmentP, error) {
61-
refs := make([]SegmentP, 0)
124+
func (f *File) Query(m Marker) ([]Pointer, error) {
125+
refs := make([]Pointer, 0)
62126
for _, r := range f.refs {
63127
if r.Marker == m {
64128
refs = append(refs, r)
@@ -67,10 +131,68 @@ func (f *File) Query(m Marker) ([]SegmentP, error) {
67131
return refs, nil
68132
}
69133

134+
// Add new segments at the end of the JFIF header, before
135+
// the first SOI marker.
136+
//
137+
// Notes:
138+
// * This is an expensive operation. It requires shifting all of
139+
// image bytes on disk to make space.
140+
// * Offset and Length are ignored on the incoming segments. They
141+
// are simply calcluted from the provided Data in order.
142+
//
143+
// TODO What about a multi-segment that could have splitting/chunking behavior?
144+
//
145+
// TODO: This interface doesn't quite work since we need to close the file
146+
// descriptor and do the move...
147+
func (f *File) Add(segs ...Segment) error {
148+
// TODO Probably want an xio primitive to insert-in-middle operation
149+
if len(f.refs) < 3 {
150+
return errors.New("todo: Not enough file segments to start")
151+
}
152+
153+
last := f.refs[len(f.refs)-1]
154+
155+
// Calculate how much extra space we need.
156+
size := int64(0)
157+
insert := last.Offset
158+
for i, s := range segs {
159+
s.Offset = insert + size
160+
if len(s.Data) > 0 {
161+
l := len(s.Data) + 2
162+
if l > 0xffff { // TODO double-check actual max segment size
163+
return ErrOversizeSegment
164+
}
165+
s.Length = uint16(l)
166+
} else {
167+
s.Length = 0
168+
}
169+
size += s.DiskSize()
170+
segs[i] = s
171+
}
172+
173+
// TODO Apply bookkeeping for the `last` SOS marker offset
174+
175+
return nil
176+
}
177+
178+
// Sync writes any updated contents of the segment back to disk
179+
//
180+
// TODO: Note that at some point this may "do the right thing" when
181+
// further downstream allocations need ot happen.
182+
func (f *File) Sync(s Segment) error {
183+
return f.Update(s.Pointer, s.Data)
184+
}
185+
70186
// Update replaces the payload for the given segment ref. Returns an
71187
// error if it's too large or doesn't match a known segment in this
72188
// file.
73-
func (f *File) Update(r SegmentP, buf []byte) error {
189+
//
190+
// Note:
191+
// - This updates the file in-place so all of the general warnings
192+
// apply w.r.t. potential file corruption. This should be limited
193+
// to files that have already been copied and are intended to
194+
// be edited directly.
195+
func (f *File) Update(r Pointer, buf []byte) error {
74196
var i int
75197
for ; i < len(f.refs); i++ {
76198
if f.refs[i] == r {
@@ -92,14 +214,15 @@ func (f *File) Update(r SegmentP, buf []byte) error {
92214
}
93215

94216
// Encode the updated segment to disk
95-
// TODO Need to make sure to update our SegmentP copy
217+
// TODO Need to make sure to update our Pointer copy
96218
_, err := f.f.Seek(r.Offset, io.SeekStart)
97219
if err != nil {
98220
return err
99221
}
100222

101223
seg := Segment{ // TODO Can I avoid all the "+/- 2's" everywhere
102-
SegmentP{r.Offset, r.Marker, uint16(len(buf) + 2)}, buf,
224+
Pointer{r.Offset, r.Marker, uint16(len(buf) + 2)},
225+
buf,
103226
}
104227
err = EncodeSegment(f.f, seg)
105228
if err != nil {
@@ -110,6 +233,6 @@ func (f *File) Update(r SegmentP, buf []byte) error {
110233
}
111234

112235
// Update our in-memory location
113-
f.refs[i] = seg.SegmentP
236+
f.refs[i] = seg.Pointer
114237
return nil
115238
}

file_test.go

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package jfif
22

33
import (
44
"bytes"
5+
"fmt"
56
"io"
67
"io/ioutil"
78
"os"
@@ -15,14 +16,14 @@ func TestFileQuery(t *testing.T) { // TODO
1516
func TestFileUpdate(t *testing.T) {
1617
var tests = []struct {
1718
name string
18-
ref SegmentP
19+
ref Pointer
1920
buf []byte
2021

2122
golden string
2223
}{
2324
{
2425
"min",
25-
SegmentP{Offset: 2, Marker: DQT, Length: 67},
26+
Pointer{Offset: 2, Marker: DQT, Length: 67},
2627
[]byte{0, // Pq and Tq bytes
2728
// Arbitrary DQT table for testing
2829
16, 11, 10, 16, 24, 40, 51, 61,
@@ -86,3 +87,44 @@ func TestFileUpdate(t *testing.T) {
8687
})
8788
}
8889
}
90+
91+
func TestFileAdd(t *testing.T) {
92+
93+
var tests = []struct {
94+
name string
95+
ref Pointer
96+
buf []byte
97+
98+
golden string
99+
}{
100+
{
101+
"min",
102+
Pointer{Offset: 2, Marker: DQT, Length: 67},
103+
[]byte{0, // Pq and Tq bytes
104+
// Arbitrary DQT table for testing
105+
16, 11, 10, 16, 24, 40, 51, 61,
106+
12, 12, 14, 19, 26, 58, 60, 55,
107+
14, 13, 16, 24, 40, 57, 69, 56,
108+
14, 17, 22, 29, 51, 87, 80, 62,
109+
18, 22, 37, 56, 68, 109, 103, 77,
110+
24, 35, 55, 64, 81, 104, 113, 92,
111+
49, 64, 78, 87, 103, 121, 120, 101,
112+
72, 92, 95, 98, 112, 100, 103, 99,
113+
},
114+
"min.dqt.jpg",
115+
},
116+
}
117+
for _, tt := range tests {
118+
t.Run(tt.name, func(t *testing.T) {
119+
temp, err := ioutil.TempFile(os.TempDir(), "jfif-test-add-"+tt.name)
120+
if err != nil {
121+
t.Fatal(err)
122+
}
123+
path := temp.Name()
124+
//defer os.Remove(path)
125+
defer temp.Close()
126+
127+
fmt.Println("TODO: TestFileAdd:", path)
128+
})
129+
}
130+
}

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@ module neilpa.me/go-jfif
22

33
go 1.14
44

5-
require neilpa.me/go-x v0.2.0
5+
require neilpa.me/go-x v0.2.1-0.20200507232743-5243b9624d5e

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,4 @@
11
neilpa.me/go-x v0.2.0 h1:GbLRDtAZ9MgVrwrIe3jWnHF2W40LCFA9Ng/aDbd9GVs=
22
neilpa.me/go-x v0.2.0/go.mod h1:aIemU+pQYLLV3dygXotHKF7SantXe5HzZR6VIjzY/4g=
3+
neilpa.me/go-x v0.2.1-0.20200507232743-5243b9624d5e h1:+O171A50t9HKAT8lpmTeTVscpTNYUvwDlPvOAOpgq/E=
4+
neilpa.me/go-x v0.2.1-0.20200507232743-5243b9624d5e/go.mod h1:aIemU+pQYLLV3dygXotHKF7SantXe5HzZR6VIjzY/4g=

0 commit comments

Comments
 (0)