b38f7f643b
Parse filanfc-dump/v1 zip/JSON, Bambu 1K images, and merge stickers by tray UID. dump and mifare packages stay importable for a later common library.
161 lines
4.0 KiB
Go
161 lines
4.0 KiB
Go
// Package dump reads brand-blind filanfc-dump/v1 JSON and zip archives.
|
|
package dump
|
|
|
|
import (
|
|
"archive/zip"
|
|
"bytes"
|
|
"encoding/base64"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"strings"
|
|
"time"
|
|
|
|
"git.s1d3sw1ped.com/s1d3sw1ped/bambu-nfc/mifare"
|
|
)
|
|
|
|
var (
|
|
ErrInvalidDump = errors.New("invalid dump")
|
|
ErrImageSize = errors.New("image size")
|
|
)
|
|
|
|
const FormatV1 = "filanfc-dump/v1"
|
|
|
|
// Dump is one sticker: chip UID plus a MIFARE Classic 1K image.
|
|
type Dump struct {
|
|
Format string
|
|
ScannedAt time.Time
|
|
ChipUID string
|
|
SAK string
|
|
ATQA string
|
|
Tech []string
|
|
SectorsOK []int
|
|
SectorsFail []int
|
|
Image []byte
|
|
}
|
|
|
|
type fileDump struct {
|
|
Format string `json:"format"`
|
|
ScannedAt string `json:"scanned_at"`
|
|
ChipUID string `json:"chip_uid"`
|
|
SAK string `json:"sak"`
|
|
ATQA string `json:"atqa"`
|
|
Tech []string `json:"tech"`
|
|
SectorsOK []int `json:"sectors_ok"`
|
|
SectorsFail []int `json:"sectors_fail"`
|
|
DumpHex string `json:"dump_hex"`
|
|
DumpB64 string `json:"dump_b64"`
|
|
}
|
|
|
|
// ParseJSON reads one filanfc-dump/v1 object.
|
|
func ParseJSON(r io.Reader) (Dump, error) {
|
|
var raw fileDump
|
|
dec := json.NewDecoder(r)
|
|
if err := dec.Decode(&raw); err != nil {
|
|
return Dump{}, fmt.Errorf("%w: %v", ErrInvalidDump, err)
|
|
}
|
|
return parseRaw(raw)
|
|
}
|
|
|
|
func parseRaw(raw fileDump) (Dump, error) {
|
|
if raw.Format != FormatV1 {
|
|
return Dump{}, fmt.Errorf("%w: format %q", ErrInvalidDump, raw.Format)
|
|
}
|
|
uid := strings.ToLower(strings.TrimSpace(raw.ChipUID))
|
|
if uid == "" || len(uid)%2 != 0 {
|
|
return Dump{}, fmt.Errorf("%w: chip_uid %q", ErrInvalidDump, raw.ChipUID)
|
|
}
|
|
if _, err := hex.DecodeString(uid); err != nil {
|
|
return Dump{}, fmt.Errorf("%w: chip_uid: %v", ErrInvalidDump, err)
|
|
}
|
|
|
|
var imgHex, imgB64 []byte
|
|
var err error
|
|
if raw.DumpHex != "" {
|
|
imgHex, err = hex.DecodeString(raw.DumpHex)
|
|
if err != nil {
|
|
return Dump{}, fmt.Errorf("%w: dump_hex: %v", ErrInvalidDump, err)
|
|
}
|
|
}
|
|
if raw.DumpB64 != "" {
|
|
imgB64, err = base64.StdEncoding.DecodeString(raw.DumpB64)
|
|
if err != nil {
|
|
return Dump{}, fmt.Errorf("%w: dump_b64: %v", ErrInvalidDump, err)
|
|
}
|
|
}
|
|
var image []byte
|
|
switch {
|
|
case len(imgHex) != 0 && len(imgB64) != 0:
|
|
if !bytes.Equal(imgHex, imgB64) {
|
|
return Dump{}, fmt.Errorf("%w: dump_hex and dump_b64 disagree", ErrInvalidDump)
|
|
}
|
|
image = imgHex
|
|
case len(imgHex) != 0:
|
|
image = imgHex
|
|
case len(imgB64) != 0:
|
|
image = imgB64
|
|
default:
|
|
return Dump{}, fmt.Errorf("%w: missing image", ErrInvalidDump)
|
|
}
|
|
if len(image) != mifare.Size1K {
|
|
return Dump{}, fmt.Errorf("%w: want %d got %d", ErrImageSize, mifare.Size1K, len(image))
|
|
}
|
|
|
|
var scanned time.Time
|
|
if raw.ScannedAt != "" {
|
|
scanned, err = time.Parse(time.RFC3339Nano, raw.ScannedAt)
|
|
if err != nil {
|
|
scanned, err = time.Parse(time.RFC3339, raw.ScannedAt)
|
|
if err != nil {
|
|
return Dump{}, fmt.Errorf("%w: scanned_at: %v", ErrInvalidDump, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
return Dump{
|
|
Format: raw.Format,
|
|
ScannedAt: scanned,
|
|
ChipUID: uid,
|
|
SAK: raw.SAK,
|
|
ATQA: raw.ATQA,
|
|
Tech: raw.Tech,
|
|
SectorsOK: raw.SectorsOK,
|
|
SectorsFail: raw.SectorsFail,
|
|
Image: image,
|
|
}, nil
|
|
}
|
|
|
|
// ParseZip reads every {chip_uid}.json entry in a dump zip.
|
|
func ParseZip(r io.Reader) ([]Dump, error) {
|
|
body, err := io.ReadAll(r)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%w: read zip: %v", ErrInvalidDump, err)
|
|
}
|
|
zr, err := zip.NewReader(bytes.NewReader(body), int64(len(body)))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%w: zip: %v", ErrInvalidDump, err)
|
|
}
|
|
var dumps []Dump
|
|
for _, f := range zr.File {
|
|
if !strings.HasSuffix(strings.ToLower(f.Name), ".json") {
|
|
continue
|
|
}
|
|
rc, err := f.Open()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%w: open %s: %v", ErrInvalidDump, f.Name, err)
|
|
}
|
|
d, err := ParseJSON(rc)
|
|
rc.Close()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%w: %s: %v", ErrInvalidDump, f.Name, err)
|
|
}
|
|
dumps = append(dumps, d)
|
|
}
|
|
if len(dumps) == 0 {
|
|
return nil, fmt.Errorf("%w: zip has no json entries", ErrInvalidDump)
|
|
}
|
|
return dumps, nil
|
|
}
|