11package jfif
22
33import (
4+ "bytes"
45 "errors"
56 "io"
7+ "math"
68 "os"
9+
10+ xio "neilpa.me/go-x/io"
711)
812
913var (
@@ -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.
2079type 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...
3498func 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}
0 commit comments