11package backup
22
33import (
4+ "bytes"
45 "encoding/json"
56 "io"
67 "time"
@@ -210,8 +211,38 @@ func WriteManifest(w io.Writer, m Manifest) error {
210211// error is wrapped as ErrUnsupportedFormatVersion or ErrInvalidManifest so
211212// callers can branch on errors.Is.
212213func ReadManifest (r io.Reader ) (Manifest , error ) {
214+ // Read the entire payload once so we can pre-decode just the
215+ // format_version before strict struct decoding. Without this
216+ // two-phase approach, a manifest produced by a newer major version
217+ // that also changed the JSON type of a known field (e.g. `phase`
218+ // switched from string to int) would surface as
219+ // ErrInvalidManifest instead of ErrUnsupportedFormatVersion,
220+ // breaking the documented version-branching contract for callers
221+ // that key off errors.Is(err, ErrUnsupportedFormatVersion). See
222+ // Codex P2, round 5.
223+ payload , err := io .ReadAll (r )
224+ if err != nil {
225+ return Manifest {}, errors .Wrap (ErrInvalidManifest , err .Error ())
226+ }
227+ // Phase 1: probe format_version with a relaxed shape that tolerates
228+ // arbitrary types on every other field.
229+ var probe struct {
230+ FormatVersion uint32 `json:"format_version"`
231+ }
232+ if err := json .Unmarshal (payload , & probe ); err != nil {
233+ return Manifest {}, errors .Wrap (ErrInvalidManifest , err .Error ())
234+ }
235+ if probe .FormatVersion == 0 {
236+ return Manifest {}, errors .Wrapf (ErrUnsupportedFormatVersion ,
237+ "format_version is zero" )
238+ }
239+ if probe .FormatVersion > CurrentFormatVersion {
240+ return Manifest {}, errors .Wrapf (ErrUnsupportedFormatVersion ,
241+ "format_version %d > current %d (newer producer)" , probe .FormatVersion , CurrentFormatVersion )
242+ }
243+ // Phase 2: strict struct decode on a known-supported version.
213244 var m Manifest
214- dec := json .NewDecoder (r )
245+ dec := json .NewDecoder (bytes . NewReader ( payload ) )
215246 // We intentionally do NOT call DisallowUnknownFields here.
216247 // The format-version contract (Codex P1, follow-up) is:
217248 // - format_version > CurrentFormatVersion -> hard refuse
@@ -237,14 +268,6 @@ func ReadManifest(r io.Reader) (Manifest, error) {
237268 return Manifest {}, errors .Wrap (ErrInvalidManifest ,
238269 "trailing bytes after manifest JSON object" )
239270 }
240- if m .FormatVersion == 0 {
241- return Manifest {}, errors .Wrapf (ErrUnsupportedFormatVersion ,
242- "format_version is zero" )
243- }
244- if m .FormatVersion > CurrentFormatVersion {
245- return Manifest {}, errors .Wrapf (ErrUnsupportedFormatVersion ,
246- "format_version %d > current %d (newer producer)" , m .FormatVersion , CurrentFormatVersion )
247- }
248271 if err := m .validate (); err != nil {
249272 return Manifest {}, err
250273 }
0 commit comments