package datapoint import ( "archive/zip" "bytes" "encoding/json" "fmt" "io" ) const ( MaxBatchBytes = 4 << 20 // Maximum ZIP payload size on the wire. MaxBatchJSONBytes = 16 << 20 // Maximum expanded JSON size. ) // ZipBatch packages GPS fixes into an in-memory ZIP containing points.json. func ZipBatch(points []DataPoint) ([]byte, error) { raw, err := json.Marshal(points) if err != nil { return nil, fmt.Errorf("encode points: %w", err) } if len(raw) > MaxBatchJSONBytes { return nil, fmt.Errorf("batch JSON exceeds %d bytes", MaxBatchJSONBytes) } var buf bytes.Buffer zw := zip.NewWriter(&buf) file, err := zw.Create("points.json") if err != nil { return nil, err } if _, err = file.Write(raw); err != nil { return nil, err } if err = zw.Close(); err != nil { return nil, err } if buf.Len() > MaxBatchBytes { return nil, fmt.Errorf("ZIP batch exceeds %d bytes", MaxBatchBytes) } return buf.Bytes(), nil } // UnzipBatch reads points in memory; archive entries are never written to disk. // Legacy JSON batches are accepted so the server can be upgraded first. func UnzipBatch(payload []byte) ([]DataPoint, error) { if len(payload) > MaxBatchBytes { return nil, fmt.Errorf("batch exceeds %d bytes", MaxBatchBytes) } raw := payload if !bytes.HasPrefix(bytes.TrimSpace(payload), []byte("[")) && !bytes.Equal(bytes.TrimSpace(payload), []byte("null")) { zr, err := zip.NewReader(bytes.NewReader(payload), int64(len(payload))) if err != nil { return nil, fmt.Errorf("open ZIP batch: %w", err) } if len(zr.File) != 1 || zr.File[0].Name != "points.json" { return nil, fmt.Errorf("ZIP batch must contain only points.json") } file := zr.File[0] if file.UncompressedSize64 > MaxBatchJSONBytes { return nil, fmt.Errorf("expanded batch exceeds %d bytes", MaxBatchJSONBytes) } r, err := file.Open() if err != nil { return nil, fmt.Errorf("open points.json: %w", err) } defer r.Close() raw, err = io.ReadAll(io.LimitReader(r, MaxBatchJSONBytes+1)) if err != nil { return nil, fmt.Errorf("read points.json: %w", err) } if len(raw) > MaxBatchJSONBytes { return nil, fmt.Errorf("expanded batch exceeds %d bytes", MaxBatchJSONBytes) } } var points []DataPoint if err := json.Unmarshal(raw, &points); err != nil { return nil, fmt.Errorf("decode points: %w", err) } return points, nil }