diff --git a/README.md b/README.md new file mode 100644 index 0000000..ccb47fb --- /dev/null +++ b/README.md @@ -0,0 +1,21 @@ +# bambu-nfc + +Go library for Bambu Lab filament NFC dumps produced by the Filament NFC dump Android app. + +```go +import bambunfc "git.s1d3sw1ped.com/s1d3sw1ped/bambu-nfc" + +tags, err := bambunfc.ParseZip(r) +spools, err := bambunfc.Merge(tags) +keys, err := bambunfc.KeysA(uid) +``` + +- `dump` — brand-blind `filanfc-dump/v1` JSON/zip +- `mifare` — Classic 1K layout helpers +- root package — Bambu Key-A HKDF, block parse, merge by `tray_uid` + +`Brand` is `"Bambu Lab"` on a successful parse. + +```bash +go test -race ./... +``` diff --git a/color.go b/color.go new file mode 100644 index 0000000..408d1ae --- /dev/null +++ b/color.go @@ -0,0 +1,13 @@ +package bambunfc + +import "fmt" + +// Color is an RGBA filament color from a Bambu tag. +type Color struct { + R, G, B, A uint8 +} + +// Hex returns "#RRGGBBAA" with uppercase hex digits. +func (c Color) Hex() string { + return fmt.Sprintf("#%02X%02X%02X%02X", c.R, c.G, c.B, c.A) +} diff --git a/doc.go b/doc.go new file mode 100644 index 0000000..cb15ae7 --- /dev/null +++ b/doc.go @@ -0,0 +1,2 @@ +// Package bambunfc parses Bambu Lab filament NFC dumps and derives MIFARE Key-A from a chip UID. +package bambunfc diff --git a/docs/superpowers/plans/2026-08-23-bambu-nfc-library.md b/docs/superpowers/plans/2026-08-23-bambu-nfc-library.md new file mode 100644 index 0000000..a64c51a --- /dev/null +++ b/docs/superpowers/plans/2026-08-23-bambu-nfc-library.md @@ -0,0 +1,23 @@ +# bambu-nfc Library Implementation Plan + +> **For agentic workers:** Use TDD. Spec: `docs/superpowers/specs/2026-08-23-bambu-nfc-library-design.md` + +**Goal:** Go module that parses filanfc-dump/v1 zips into Bambu tags/spools and derives MIFARE Key-A from a chip UID. + +**Architecture:** Public packages `dump` (JSON/zip), `mifare` (1K layout), root `bambunfc` (HKDF, block parse, Merge). + +**Tech Stack:** Go 1.25, stdlib `crypto/hkdf`, `encoding/json`, `archive/zip`. + +## Global Constraints + +- Module `git.s1d3sw1ped.com/s1d3sw1ped/bambu-nfc` +- Brand on success: `"Bambu Lab"` +- HKDF: IKM=UID, salt=`9a759cf2c4f7caff222cb9769b41bc96`, info=`RFID-A\0`, 16×6 Key-A +- Diameter float32 at block 5 offset 8; tray UID 16 raw bytes hex +- Required dump sectors for a Bambu parse: 0, 1, 2 (`sectors_fail` is sector indexes) +- `go test -race ./...` +- Fixtures from dump-app `docs/dumps/` → `testdata/` + +Tasks: (1) module + mifare (2) dump JSON/zip (3) KeysA (4) ParseImage/ParseDump/ParseZip (5) Merge (6) testdata + README. + +Executing in this session per user "do it". diff --git a/dump/dump.go b/dump/dump.go new file mode 100644 index 0000000..41db6af --- /dev/null +++ b/dump/dump.go @@ -0,0 +1,160 @@ +// 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 +} diff --git a/dump/dump_test.go b/dump/dump_test.go new file mode 100644 index 0000000..874191f --- /dev/null +++ b/dump/dump_test.go @@ -0,0 +1,58 @@ +package dump + +import ( + "errors" + "os" + "path/filepath" + "strings" + "testing" +) + +func TestParseJSONFixture(t *testing.T) { + t.Parallel() + f, err := os.Open(filepath.Join("..", "testdata", "dumps", "e27be276.json")) + if err != nil { + t.Fatal(err) + } + defer f.Close() + d, err := ParseJSON(f) + if err != nil { + t.Fatal(err) + } + if d.ChipUID != "e27be276" { + t.Errorf("ChipUID = %q", d.ChipUID) + } + if d.Format != FormatV1 { + t.Errorf("Format = %q", d.Format) + } + if len(d.Image) != 1024 { + t.Errorf("Image len = %d", len(d.Image)) + } + if len(d.SectorsFail) != 0 { + t.Errorf("SectorsFail = %v", d.SectorsFail) + } +} + +func TestParseZipFixture(t *testing.T) { + t.Parallel() + f, err := os.Open(filepath.Join("..", "testdata", "dumps.zip")) + if err != nil { + t.Fatal(err) + } + defer f.Close() + dumps, err := ParseZip(f) + if err != nil { + t.Fatal(err) + } + if len(dumps) != 6 { + t.Errorf("len = %d; want 6", len(dumps)) + } +} + +func TestParseJSONRejectsBadFormat(t *testing.T) { + t.Parallel() + _, err := ParseJSON(strings.NewReader(`{"format":"nope","chip_uid":"aa","dump_hex":"` + strings.Repeat("00", 1024) + `"}`)) + if !errors.Is(err, ErrInvalidDump) { + t.Fatalf("err = %v; want ErrInvalidDump", err) + } +} diff --git a/errors.go b/errors.go new file mode 100644 index 0000000..b36bda1 --- /dev/null +++ b/errors.go @@ -0,0 +1,12 @@ +package bambunfc + +import "errors" + +var ( + ErrInvalidDump = errors.New("invalid dump") + ErrImageSize = errors.New("image size") + ErrIncomplete = errors.New("incomplete tag") + ErrNotBambu = errors.New("not bambu") + ErrUID = errors.New("uid") + ErrConflict = errors.New("tag conflict") +) diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..47e55c0 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module git.s1d3sw1ped.com/s1d3sw1ped/bambu-nfc + +go 1.25 diff --git a/kdf.go b/kdf.go new file mode 100644 index 0000000..7ea79f2 --- /dev/null +++ b/kdf.go @@ -0,0 +1,30 @@ +package bambunfc + +import ( + "crypto/hkdf" + "crypto/sha256" + "fmt" +) + +var kdfSalt = []byte{ + 0x9a, 0x75, 0x9c, 0xf2, 0xc4, 0xf7, 0xca, 0xff, + 0x22, 0x2c, 0xb9, 0x76, 0x9b, 0x41, 0xbc, 0x96, +} + +const kdfInfo = "RFID-A\x00" + +// KeysA derives the 16 MIFARE Key-A values for a Bambu tag from its chip UID. +func KeysA(uid []byte) ([16][6]byte, error) { + var keys [16][6]byte + if len(uid) == 0 { + return keys, fmt.Errorf("%w: empty", ErrUID) + } + okm, err := hkdf.Key(sha256.New, uid, kdfSalt, kdfInfo, 16*6) + if err != nil { + return keys, fmt.Errorf("%w: hkdf: %v", ErrUID, err) + } + for i := range 16 { + copy(keys[i][:], okm[i*6:i*6+6]) + } + return keys, nil +} diff --git a/kdf_test.go b/kdf_test.go new file mode 100644 index 0000000..bb41c46 --- /dev/null +++ b/kdf_test.go @@ -0,0 +1,48 @@ +package bambunfc + +import ( + "encoding/hex" + "errors" + "testing" +) + +func TestKeysAGolden11223344(t *testing.T) { + t.Parallel() + uid, err := hex.DecodeString("11223344") + if err != nil { + t.Fatal(err) + } + want := []string{ + "0729F3B2D37A", "2027210D85E7", "D77B2E7C92BC", "29126A53A2EE", + "E49850F62778", "9A9128B882BD", "45F69B980786", "90DD67095D6B", + "EB994746CD69", "B4AEB23473E3", "445E850D699C", "5C9BD5BD9FD7", + "1062AFE9F5F5", "81EE5259CF6B", "0920CEEEE2BC", "BAF630CCBFD3", + } + got, err := KeysA(uid) + if err != nil { + t.Fatal(err) + } + for i, w := range want { + if h := hex.EncodeToString(got[i][:]); h != toLower(w) { + t.Errorf("key[%d] = %s; want %s", i, h, toLower(w)) + } + } +} + +func toLower(s string) string { + b := []byte(s) + for i, c := range b { + if c >= 'A' && c <= 'F' { + b[i] = c + ('a' - 'A') + } + } + return string(b) +} + +func TestKeysAEmpty(t *testing.T) { + t.Parallel() + _, err := KeysA(nil) + if !errors.Is(err, ErrUID) { + t.Errorf("KeysA(nil) err = %v; want ErrUID", err) + } +} diff --git a/merge.go b/merge.go new file mode 100644 index 0000000..6c58f18 --- /dev/null +++ b/merge.go @@ -0,0 +1,107 @@ +package bambunfc + +import ( + "fmt" + "slices" +) + +// Spool is one physical filament roll (both stickers merged). +type Spool struct { + Brand string + TrayUID string + ChipUIDs []string + Type string + Material string + MaterialID string + VariantID string + Color Color + WeightG int + DiameterMM float32 + LengthM int + MinHotendC int + MaxHotendC int + BedC int + DryC int + DryHours int + Produced string + SpoolWidthRaw uint16 +} + +func spoolKey(t Tag) string { + return t.Brand + "\x00" + t.TrayUID +} + +func sameFilament(a, b Tag) bool { + return a.Type == b.Type && + a.Material == b.Material && + a.Color == b.Color && + a.WeightG == b.WeightG +} + +func tagToSpool(t Tag) Spool { + return Spool{ + Brand: t.Brand, + TrayUID: t.TrayUID, + ChipUIDs: []string{t.ChipUID}, + Type: t.Type, + Material: t.Material, + MaterialID: t.MaterialID, + VariantID: t.VariantID, + Color: t.Color, + WeightG: t.WeightG, + DiameterMM: t.DiameterMM, + LengthM: t.LengthM, + MinHotendC: t.MinHotendC, + MaxHotendC: t.MaxHotendC, + BedC: t.BedC, + DryC: t.DryC, + DryHours: t.DryHours, + Produced: t.Produced, + SpoolWidthRaw: t.SpoolWidthRaw, + } +} + +// Merge groups tags by brand + tray UID into spools. +func Merge(tags []Tag) ([]Spool, error) { + order := make([]string, 0) + groups := make(map[string][]Tag) + for _, t := range tags { + if t.TrayUID == "" { + return nil, fmt.Errorf("%w: empty tray uid chip %s", ErrIncomplete, t.ChipUID) + } + k := spoolKey(t) + if _, ok := groups[k]; !ok { + order = append(order, k) + } + groups[k] = append(groups[k], t) + } + + spools := make([]Spool, 0, len(order)) + for _, k := range order { + g := groups[k] + s := tagToSpool(g[0]) + uids := map[string]struct{}{g[0].ChipUID: {}} + for _, t := range g[1:] { + if !sameFilament(g[0], t) { + return nil, fmt.Errorf("%w: tray %s", ErrConflict, t.TrayUID) + } + uids[t.ChipUID] = struct{}{} + } + s.ChipUIDs = make([]string, 0, len(uids)) + for u := range uids { + s.ChipUIDs = append(s.ChipUIDs, u) + } + slices.Sort(s.ChipUIDs) + spools = append(spools, s) + } + slices.SortFunc(spools, func(a, b Spool) int { + if a.TrayUID < b.TrayUID { + return -1 + } + if a.TrayUID > b.TrayUID { + return 1 + } + return 0 + }) + return spools, nil +} diff --git a/merge_test.go b/merge_test.go new file mode 100644 index 0000000..87a25d1 --- /dev/null +++ b/merge_test.go @@ -0,0 +1,87 @@ +package bambunfc + +import ( + "errors" + "os" + "slices" + "testing" +) + +func TestMergeFixtures(t *testing.T) { + t.Parallel() + f, err := os.Open("testdata/dumps.zip") + if err != nil { + t.Fatal(err) + } + defer f.Close() + tags, err := ParseZip(f) + if err != nil { + t.Fatal(err) + } + spools, err := Merge(tags) + if err != nil { + t.Fatal(err) + } + if len(spools) != 3 { + t.Fatalf("len(spools) = %d; want 3", len(spools)) + } + + byTray := map[string]Spool{} + for _, s := range spools { + byTray[s.TrayUID] = s + } + + a, ok := byTray["2fb8e0e972084e74bf737393b28e7b12"] + if !ok { + t.Fatal("missing spool A") + } + if !slices.Equal(a.ChipUIDs, []string{"52d60177", "e27be276"}) { + t.Errorf("spool A chips %v", a.ChipUIDs) + } + if a.Type != "PETG Translucent" || a.Brand != BrandBambuLab { + t.Errorf("spool A type/brand %s %s", a.Type, a.Brand) + } + + b, ok := byTray["aa42f322172c48e99c0ece895346ea9f"] + if !ok { + t.Fatal("missing spool B") + } + if !slices.Equal(b.ChipUIDs, []string{"92a80577", "b2d32177"}) { + t.Errorf("spool B chips %v", b.ChipUIDs) + } + + c, ok := byTray["3cb568b7af4f41819412fe60afc5c446"] + if !ok { + t.Fatal("missing spool C") + } + if !slices.Equal(c.ChipUIDs, []string{"92b6fb31", "f28c58ed"}) { + t.Errorf("spool C chips %v", c.ChipUIDs) + } + if c.Type != "PETG Basic" || c.Color.Hex() != "#000000FF" { + t.Errorf("spool C %s %s", c.Type, c.Color.Hex()) + } + + for i := 1; i < len(spools); i++ { + if spools[i-1].TrayUID > spools[i].TrayUID { + t.Fatalf("spools not sorted by TrayUID") + } + } +} + +func TestMergeConflict(t *testing.T) { + t.Parallel() + a := Tag{Brand: BrandBambuLab, TrayUID: "aa", ChipUID: "01", Type: "PLA", Material: "PLA", WeightG: 1000} + b := Tag{Brand: BrandBambuLab, TrayUID: "aa", ChipUID: "02", Type: "PETG", Material: "PETG", WeightG: 1000} + _, err := Merge([]Tag{a, b}) + if !errors.Is(err, ErrConflict) { + t.Errorf("err = %v; want ErrConflict", err) + } +} + +func TestMergeEmptyTray(t *testing.T) { + t.Parallel() + _, err := Merge([]Tag{{Brand: BrandBambuLab, ChipUID: "01", Type: "PLA"}}) + if !errors.Is(err, ErrIncomplete) { + t.Errorf("err = %v; want ErrIncomplete", err) + } +} diff --git a/mifare/mifare.go b/mifare/mifare.go new file mode 100644 index 0000000..feb2d25 --- /dev/null +++ b/mifare/mifare.go @@ -0,0 +1,36 @@ +// Package mifare holds MIFARE Classic 1K layout helpers with no brand-specific memory map. +package mifare + +import "fmt" + +const ( + Sectors = 16 + Blocks = 64 + BlockSize = 16 + SectorSize = 64 + Size1K = 1024 +) + +// SectorOf returns the sector index for a block number (0–63). +func SectorOf(block int) int { + return block / 4 +} + +// BlockRange returns the half-open block range [first, last) for a sector. +func BlockRange(sector int) (first, last int) { + first = sector * 4 + last = first + 4 + return first, last +} + +// SectorBytes returns the 64-byte slice for sector (0–15) from a 1K image. +func SectorBytes(image []byte, sector int) ([]byte, error) { + if len(image) != Size1K { + return nil, fmt.Errorf("image size: want %d got %d", Size1K, len(image)) + } + if sector < 0 || sector >= Sectors { + return nil, fmt.Errorf("sector %d out of range", sector) + } + off := sector * SectorSize + return image[off : off+SectorSize], nil +} diff --git a/mifare/mifare_test.go b/mifare/mifare_test.go new file mode 100644 index 0000000..222b3b0 --- /dev/null +++ b/mifare/mifare_test.go @@ -0,0 +1,30 @@ +package mifare + +import "testing" + +func TestSectorOfAndBlockRange(t *testing.T) { + t.Parallel() + if got := SectorOf(9); got != 2 { + t.Errorf("SectorOf(9) = %d; want 2", got) + } + first, last := BlockRange(2) + if first != 8 || last != 12 { + t.Errorf("BlockRange(2) = %d,%d; want 8,12", first, last) + } +} + +func TestSectorBytes(t *testing.T) { + t.Parallel() + img := make([]byte, Size1K) + img[2*SectorSize] = 0xAB + got, err := SectorBytes(img, 2) + if err != nil { + t.Fatal(err) + } + if got[0] != 0xAB || len(got) != SectorSize { + t.Errorf("SectorBytes sector 2: len=%d first=%02x", len(got), got[0]) + } + if _, err := SectorBytes(img[:1023], 0); err == nil { + t.Fatal("short image: want error") + } +} diff --git a/parse.go b/parse.go new file mode 100644 index 0000000..66bfef2 --- /dev/null +++ b/parse.go @@ -0,0 +1,156 @@ +package bambunfc + +import ( + "bytes" + "encoding/binary" + "encoding/hex" + "fmt" + "io" + "math" + "strings" + + "git.s1d3sw1ped.com/s1d3sw1ped/bambu-nfc/dump" + "git.s1d3sw1ped.com/s1d3sw1ped/bambu-nfc/mifare" +) + +const BrandBambuLab = "Bambu Lab" + +// requiredSectors must not appear in SectorsFail (blocks 0–2, 4–6, 8–9). +var requiredSectors = []int{0, 1, 2} + +// Tag is one Bambu sticker after parsing the 1K image. +type Tag struct { + Brand string + ChipUID string + TrayUID string + Type string + Material string + MaterialID string + VariantID string + Color Color + WeightG int + DiameterMM float32 + LengthM int + MinHotendC int + MaxHotendC int + BedC int + DryC int + DryHours int + Produced string + SpoolWidthRaw uint16 +} + +func failedSet(fail []int) map[int]bool { + s := make(map[int]bool, len(fail)) + for _, n := range fail { + s[n] = true + } + return s +} + +func block(image []byte, n int) []byte { + off := n * mifare.BlockSize + return image[off : off+mifare.BlockSize] +} + +func cstr(b []byte) string { + if i := bytes.IndexByte(b, 0); i >= 0 { + b = b[:i] + } + return string(b) +} + +func normalizeUID(uid string) (string, error) { + uid = strings.ToLower(strings.TrimSpace(uid)) + if uid == "" || len(uid)%2 != 0 { + return "", fmt.Errorf("%w: %q", ErrUID, uid) + } + if _, err := hex.DecodeString(uid); err != nil { + return "", fmt.Errorf("%w: %v", ErrUID, err) + } + return uid, nil +} + +// ParseImage parses a Bambu MIFARE Classic 1K image. +// fail is the dump's sectors_fail list (sector indexes 0–15). +func ParseImage(chipUID string, image []byte, fail []int) (Tag, error) { + var zero Tag + uid, err := normalizeUID(chipUID) + if err != nil { + return zero, err + } + if len(image) != mifare.Size1K { + return zero, fmt.Errorf("%w: want %d got %d", ErrImageSize, mifare.Size1K, len(image)) + } + failed := failedSet(fail) + for _, s := range requiredSectors { + if failed[s] { + return zero, fmt.Errorf("%w: sector %d", ErrIncomplete, s) + } + } + + b1 := block(image, 1) + b2 := block(image, 2) + b4 := block(image, 4) + b5 := block(image, 5) + b6 := block(image, 6) + b9 := block(image, 9) + + material := cstr(b2) + typ := cstr(b4) + if material == "" && typ == "" { + return zero, ErrNotBambu + } + + tag := Tag{ + Brand: BrandBambuLab, + ChipUID: uid, + VariantID: cstr(b1[0:8]), + MaterialID: cstr(b1[8:16]), + Material: material, + Type: typ, + Color: Color{ + R: b5[0], G: b5[1], B: b5[2], A: b5[3], + }, + WeightG: int(binary.LittleEndian.Uint16(b5[4:6])), + DiameterMM: math.Float32frombits(binary.LittleEndian.Uint32(b5[8:12])), + TrayUID: hex.EncodeToString(b9), + DryC: int(binary.LittleEndian.Uint16(b6[0:2])), + DryHours: int(binary.LittleEndian.Uint16(b6[2:4])), + BedC: int(binary.LittleEndian.Uint16(b6[6:8])), + MaxHotendC: int(binary.LittleEndian.Uint16(b6[8:10])), + MinHotendC: int(binary.LittleEndian.Uint16(b6[10:12])), + } + if !failed[2] { + b10 := block(image, 10) + tag.SpoolWidthRaw = binary.LittleEndian.Uint16(b10[4:6]) + } + if !failed[3] { + tag.Produced = cstr(block(image, 12)) + b14 := block(image, 14) + tag.LengthM = int(binary.LittleEndian.Uint16(b14[4:6])) + } + return tag, nil +} + +// ParseDump parses one dump JSON record as a Bambu tag. +func ParseDump(d dump.Dump) (Tag, error) { + return ParseImage(d.ChipUID, d.Image, d.SectorsFail) +} + +// ParseZip reads a filanfc-dump zip and parses every sticker as Bambu. +func ParseZip(r io.Reader) ([]Tag, error) { + dumps, err := dump.ParseZip(r) + if err != nil { + return nil, fmt.Errorf("%w: %v", ErrInvalidDump, err) + } + tags := make([]Tag, 0, len(dumps)) + for _, d := range dumps { + tag, err := ParseDump(d) + if err != nil { + return nil, fmt.Errorf("chip %s: %w", d.ChipUID, err) + } + tags = append(tags, tag) + } + return tags, nil +} diff --git a/parse_test.go b/parse_test.go new file mode 100644 index 0000000..1b7f8c4 --- /dev/null +++ b/parse_test.go @@ -0,0 +1,101 @@ +package bambunfc + +import ( + "bytes" + "errors" + "os" + "testing" + + "git.s1d3sw1ped.com/s1d3sw1ped/bambu-nfc/dump" +) + +func TestParseZipFixtures(t *testing.T) { + t.Parallel() + f, err := os.Open("testdata/dumps.zip") + if err != nil { + t.Fatal(err) + } + defer f.Close() + tags, err := ParseZip(f) + if err != nil { + t.Fatal(err) + } + if len(tags) != 6 { + t.Fatalf("len(tags) = %d; want 6", len(tags)) + } + var translucent, basic int + for _, tag := range tags { + if tag.Brand != BrandBambuLab { + t.Errorf("chip %s brand %q", tag.ChipUID, tag.Brand) + } + if tag.DiameterMM != 1.75 { + t.Errorf("chip %s diameter %v", tag.ChipUID, tag.DiameterMM) + } + switch tag.Type { + case "PETG Translucent": + translucent++ + if tag.Color.Hex() != "#61B0FF80" { + t.Errorf("translucent color %s", tag.Color.Hex()) + } + case "PETG Basic": + basic++ + if tag.Color.Hex() != "#000000FF" { + t.Errorf("basic color %s", tag.Color.Hex()) + } + default: + t.Errorf("unexpected type %q", tag.Type) + } + } + if translucent != 4 || basic != 2 { + t.Errorf("translucent=%d basic=%d", translucent, basic) + } +} + +func TestParseImageErrors(t *testing.T) { + t.Parallel() + raw, err := os.ReadFile("testdata/dumps/e27be276.bin") + if err != nil { + t.Fatal(err) + } + if _, err := ParseImage("e27be276", raw[:1023], nil); !errors.Is(err, ErrImageSize) { + t.Errorf("short image err = %v; want ErrImageSize", err) + } + if _, err := ParseImage("e27be276", raw, []int{2}); !errors.Is(err, ErrIncomplete) { + t.Errorf("fail sector 2 err = %v; want ErrIncomplete", err) + } + blank := make([]byte, 1024) + if _, err := ParseImage("e27be276", blank, nil); !errors.Is(err, ErrNotBambu) { + t.Errorf("blank image err = %v; want ErrNotBambu", err) + } +} + +func TestParseDumpJSON(t *testing.T) { + t.Parallel() + f, err := os.Open("testdata/dumps/e27be276.json") + if err != nil { + t.Fatal(err) + } + defer f.Close() + d, err := dump.ParseJSON(f) + if err != nil { + t.Fatal(err) + } + tag, err := ParseDump(d) + if err != nil { + t.Fatal(err) + } + if tag.TrayUID != "2fb8e0e972084e74bf737393b28e7b12" { + t.Errorf("TrayUID = %s", tag.TrayUID) + } + if tag.ChipUID != "e27be276" { + t.Errorf("ChipUID = %s", tag.ChipUID) + } +} + +func TestParseImageEmptyUID(t *testing.T) { + t.Parallel() + _, err := ParseImage("", bytes.Repeat([]byte{1}, 1024), nil) + if !errors.Is(err, ErrUID) { + t.Errorf("err = %v; want ErrUID", err) + } +} diff --git a/testdata/dumps.zip b/testdata/dumps.zip new file mode 100644 index 0000000..37634c7 Binary files /dev/null and b/testdata/dumps.zip differ diff --git a/testdata/dumps/52d60177.bin b/testdata/dumps/52d60177.bin new file mode 100644 index 0000000..4ddaf87 Binary files /dev/null and b/testdata/dumps/52d60177.bin differ diff --git a/testdata/dumps/52d60177.json b/testdata/dumps/52d60177.json new file mode 100644 index 0000000..0c63b49 --- /dev/null +++ b/testdata/dumps/52d60177.json @@ -0,0 +1 @@ +{"format":"filanfc-dump/v1","scanned_at":"2026-08-23T19:52:17.612080Z","chip_uid":"52d60177","sak":"08","atqa":"0004","tech":["android.nfc.tech.MifareClassic","android.nfc.tech.NfcA"],"sectors_ok":[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15],"sectors_fail":[],"dump_hex":"52d60177f2080400050f4bb2bb315e904730312d423000004746473031000000504554470000000000000000000000000000000000008787876900000000000050455447205472616e736c7563656e7461b0ff80e80300000000e03f0000000041000800000000000401e6000000000000000000000087878769000000000000342134218403e8033333333fcdcc4c3e2fb8e0e972084e74bf737393b28e7b1200000000c9000000000000000000000000000000000087878769000000000000323032365f30345f30325f31335f323132303236303430320000000000000000000000004a0100000000000000000000000000000000878787690000000000000200010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008787876900000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000087878769000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000878787690000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008787876900000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000087878769000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000878787690000000000000100000000000000000000000000000000000000000000000000000000000000156a277052e48d22d9c515522178c96700000000000087878769000000000000a092f47b7ddeee11ae5bd3209ea4eac7fcbc8c44b15f39c48031c63900c8c9fb970e215eb13b518763875c4f6de59c2b0000000000008787876900000000000084114ae56a384a765b1da4d0ea011fc768af8a9f5366b47fc5bfb15c3f4e97f350dfc8c51318e00eb9ca8f128a35667c000000000000878787690000000000008e579b88c536abca254e6161dfa3ecb373576204ab38589cc460d7018067bd43a89f15db64d8e5aed0a9839c458b6b3b000000000000878787690000000000003d5348fc8b55473868629b1e031d23cba4a682307f5a8a51c8416bac428b024e8775b8c071bc07e7e3df779e40b8eb5b00000000000087878769000000000000a07cc38936af652fb9c56e46129adf740d44b8fc902d588a367ef1efb5f88895bcd82eef67df0e2bbc215525a73c1d0000000000000087878769000000000000","dump_b64":"UtYBd/IIBAAFD0uyuzFekEcwMS1CMAAAR0ZHMDEAAABQRVRHAAAAAAAAAAAAAAAAAAAAAAAAh4eHaQAAAAAAAFBFVEcgVHJhbnNsdWNlbnRhsP+A6AMAAAAA4D8AAAAAQQAIAAAAAAAEAeYAAAAAAAAAAAAAAIeHh2kAAAAAAAA0ITQhhAPoAzMzMz/NzEw+L7jg6XIITnS/c3OTso57EgAAAADJAAAAAAAAAAAAAAAAAAAAAACHh4dpAAAAAAAAMjAyNl8wNF8wMl8xM18yMTIwMjYwNDAyAAAAAAAAAAAAAAAASgEAAAAAAAAAAAAAAAAAAAAAh4eHaQAAAAAAAAIAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIeHh2kAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACHh4dpAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAh4eHaQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIeHh2kAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACHh4dpAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAh4eHaQAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFWoncFLkjSLZxRVSIXjJZwAAAAAAAIeHh2kAAAAAAACgkvR7fd7uEa5b0yCepOrH/LyMRLFfOcSAMcY5AMjJ+5cOIV6xO1GHY4dcT23lnCsAAAAAAACHh4dpAAAAAAAAhBFK5Wo4SnZbHaTQ6gEfx2ivip9TZrR/xb+xXD9Ol/NQ38jFExjgDrnKjxKKNWZ8AAAAAAAAh4eHaQAAAAAAAI5Xm4jFNqvKJU5hYd+j7LNzV2IEqzhYnMRg1wGAZ71DqJ8V22TY5a7QqYOcRYtrOwAAAAAAAIeHh2kAAAAAAAA9U0j8i1VHOGhimx4DHSPLpKaCMH9ailHIQWusQosCTod1uMBxvAfn4993nkC461sAAAAAAACHh4dpAAAAAAAAoHzDiTavZS+5xW5GEprfdA1EuPyQLViKNn7x77X4iJW82C7vZ98OK7whVSWnPB0AAAAAAAAAh4eHaQAAAAAAAA=="} \ No newline at end of file diff --git a/testdata/dumps/92a80577.bin b/testdata/dumps/92a80577.bin new file mode 100644 index 0000000..be8f9fe Binary files /dev/null and b/testdata/dumps/92a80577.bin differ diff --git a/testdata/dumps/92a80577.json b/testdata/dumps/92a80577.json new file mode 100644 index 0000000..576716f --- /dev/null +++ b/testdata/dumps/92a80577.json @@ -0,0 +1 @@ +{"format":"filanfc-dump/v1","scanned_at":"2026-08-23T19:52:33.888563Z","chip_uid":"92a80577","sak":"08","atqa":"0004","tech":["android.nfc.tech.MifareClassic","android.nfc.tech.NfcA"],"sectors_ok":[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15],"sectors_fail":[],"dump_hex":"92a805774808040005bbb1ffee6a26904730312d423000004746473031000000504554470000000000000000000000000000000000008787876900000000000050455447205472616e736c7563656e7461b0ff80e80300000000e03f0000000041000800000000000401e6000000000000000000000087878769000000000000342134218403e8033333333fcdcc4c3eaa42f322172c48e99c0ece895346ea9f00000000c9000000000000000000000000000000000087878769000000000000323032365f30345f30325f31335f343632303236303430320000000000000000000000004a01000000000000000000000000000000008787876900000000000002000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000087878769000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000878787690000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008787876900000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000087878769000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000878787690000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008787876900000000000001000000000000000000000000000000000000000000000000000000000000008d3fbd5a0eb9037c318d989139fd582600000000000087878769000000000000d22c7b4757adbcc88e3ebbc5ca1145711c97f643ebb67068e6b1bfee54057edc63f7c36e05159147152c62b270758d0700000000000087878769000000000000c42bbba5082f31656c9956313a49609810219e5bbbf372c822f1967919b663de6f20ead8ba298762d000440febc0634a000000000000878787690000000000005dc0beafe4dfeee033dcce4d5d9e232cb2aed198420f7bb2c06367b80d92210b9087789eae480234ad1f519f376ff17a00000000000087878769000000000000e4fcf2274514b9464159e8c1efc4423cfc42a0e1d2259ba287f62db47e559a405fef21496905c2db3eaedf84646d7d5e00000000000087878769000000000000319c5908b98ee01ffa1cc1713e2f9c3e0b98f6b35875aeb0c6feefa218b5a402abd20a09b7ebfbfbd0b6192cbf62c67500000000000087878769000000000000","dump_b64":"kqgFd0gIBAAFu7H/7momkEcwMS1CMAAAR0ZHMDEAAABQRVRHAAAAAAAAAAAAAAAAAAAAAAAAh4eHaQAAAAAAAFBFVEcgVHJhbnNsdWNlbnRhsP+A6AMAAAAA4D8AAAAAQQAIAAAAAAAEAeYAAAAAAAAAAAAAAIeHh2kAAAAAAAA0ITQhhAPoAzMzMz/NzEw+qkLzIhcsSOmcDs6JU0bqnwAAAADJAAAAAAAAAAAAAAAAAAAAAACHh4dpAAAAAAAAMjAyNl8wNF8wMl8xM180NjIwMjYwNDAyAAAAAAAAAAAAAAAASgEAAAAAAAAAAAAAAAAAAAAAh4eHaQAAAAAAAAIAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIeHh2kAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACHh4dpAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAh4eHaQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIeHh2kAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACHh4dpAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAh4eHaQAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAjT+9Wg65A3wxjZiROf1YJgAAAAAAAIeHh2kAAAAAAADSLHtHV628yI4+u8XKEUVxHJf2Q+u2cGjmsb/uVAV+3GP3w24FFZFHFSxisnB1jQcAAAAAAACHh4dpAAAAAAAAxCu7pQgvMWVsmVYxOklgmBAhnlu783LIIvGWeRm2Y95vIOrYuimHYtAARA/rwGNKAAAAAAAAh4eHaQAAAAAAAF3Avq/k3+7gM9zOTV2eIyyyrtGYQg97ssBjZ7gNkiELkId4nq5IAjStH1GfN2/xegAAAAAAAIeHh2kAAAAAAADk/PInRRS5RkFZ6MHvxEI8/EKg4dIlm6KH9i20flWaQF/vIUlpBcLbPq7fhGRtfV4AAAAAAACHh4dpAAAAAAAAMZxZCLmO4B/6HMFxPi+cPguY9rNYda6wxv7vohi1pAKr0goJt+v7+9C2GSy/YsZ1AAAAAAAAh4eHaQAAAAAAAA=="} \ No newline at end of file diff --git a/testdata/dumps/92b6fb31.bin b/testdata/dumps/92b6fb31.bin new file mode 100644 index 0000000..f3b7fd7 Binary files /dev/null and b/testdata/dumps/92b6fb31.bin differ diff --git a/testdata/dumps/92b6fb31.json b/testdata/dumps/92b6fb31.json new file mode 100644 index 0000000..cb17e8e --- /dev/null +++ b/testdata/dumps/92b6fb31.json @@ -0,0 +1 @@ +{"format":"filanfc-dump/v1","scanned_at":"2026-08-23T19:53:36.914443Z","chip_uid":"92b6fb31","sak":"08","atqa":"0004","tech":["android.nfc.tech.MifareClassic","android.nfc.tech.NfcA"],"sectors_ok":[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15],"sectors_fail":[],"dump_hex":"92b6fb31ee0804000577a98edf9f09904730302d4b3030004746473030000000504554470000000000000000000000000000000000008787876900000000000050455447204261736963000000000000000000ffe80300000000e03f0000000041000800000000000401e6000000000000000000000087878769000000000000000000000000000000000000cdcc4c3e3cb568b7af4f41819412fe60afc5c44600000000ab0f0000000000000000000000000000000087878769000000000000323032365f30325f32355f31365f313932365f30325f32355f31360000000000000000004a0100000000000000000000000000000000878787690000000000000200010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008787876900000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000087878769000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000878787690000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008787876900000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000087878769000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000878787690000000000000100000000000000000000000000000000000000000000000000000000000000475be12293a7f927bf42fa7b1aa7944a000000000000878787690000000000007f71f47a229a5dd5057f33aa2fee1531172829d75bf33a6c9f3e41365a1e844ff4f8528313f48fefaa1e6d02765e8abf0000000000008787876900000000000085b72ba08ddb600e8ce8d003758deca39e3ac4a83ef98429fc9d73a0ad596427aed0c23621aee0f7c25712a8e518e20c00000000000087878769000000000000e328f11df058429ddbd7ad67596e04800262e81492c59f53888bd32e65764727ad23acca7ed7a61f2c565595d39e4e79000000000000878787690000000000002f11ee1918f03618fd3c428911fb7b61cfed49fb7ebd25478eefe2e06fa633d2305950d0ced79ba6ede7a933ecaf81f500000000000087878769000000000000349b83bce314d86f4f02ddb897eb43e714dbec97891357686165cb66c230958a77e45cf5c1eb71d7aa9337499c59cbbc00000000000087878769000000000000","dump_b64":"krb7Me4IBAAFd6mO358JkEcwMC1LMDAAR0ZHMDAAAABQRVRHAAAAAAAAAAAAAAAAAAAAAAAAh4eHaQAAAAAAAFBFVEcgQmFzaWMAAAAAAAAAAAD/6AMAAAAA4D8AAAAAQQAIAAAAAAAEAeYAAAAAAAAAAAAAAIeHh2kAAAAAAAAAAAAAAAAAAAAAAADNzEw+PLVot69PQYGUEv5gr8XERgAAAACrDwAAAAAAAAAAAAAAAAAAAACHh4dpAAAAAAAAMjAyNl8wMl8yNV8xNl8xOTI2XzAyXzI1XzE2AAAAAAAAAAAASgEAAAAAAAAAAAAAAAAAAAAAh4eHaQAAAAAAAAIAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIeHh2kAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACHh4dpAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAh4eHaQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIeHh2kAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACHh4dpAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAh4eHaQAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAR1vhIpOn+Se/Qvp7GqeUSgAAAAAAAIeHh2kAAAAAAAB/cfR6Ippd1QV/M6ov7hUxFygp11vzOmyfPkE2Wh6ET/T4UoMT9I/vqh5tAnZeir8AAAAAAACHh4dpAAAAAAAAhbcroI3bYA6M6NADdY3so546xKg++YQp/J1zoK1ZZCeu0MI2Ia7g98JXEqjlGOIMAAAAAAAAh4eHaQAAAAAAAOMo8R3wWEKd29etZ1luBIACYugUksWfU4iL0y5ldkcnrSOsyn7Xph8sVlWV055OeQAAAAAAAIeHh2kAAAAAAAAvEe4ZGPA2GP08QokR+3thz+1J+369JUeO7+Lgb6Yz0jBZUNDO15um7eepM+yvgfUAAAAAAACHh4dpAAAAAAAANJuDvOMU2G9PAt24l+tD5xTb7JeJE1doYWXLZsIwlYp35Fz1wetx16qTN0mcWcu8AAAAAAAAh4eHaQAAAAAAAA=="} \ No newline at end of file diff --git a/testdata/dumps/b2d32177.bin b/testdata/dumps/b2d32177.bin new file mode 100644 index 0000000..415be7f Binary files /dev/null and b/testdata/dumps/b2d32177.bin differ diff --git a/testdata/dumps/b2d32177.json b/testdata/dumps/b2d32177.json new file mode 100644 index 0000000..f8de2e3 --- /dev/null +++ b/testdata/dumps/b2d32177.json @@ -0,0 +1 @@ +{"format":"filanfc-dump/v1","scanned_at":"2026-08-23T19:52:52.668047Z","chip_uid":"b2d32177","sak":"08","atqa":"0004","tech":["android.nfc.tech.MifareClassic","android.nfc.tech.NfcA"],"sectors_ok":[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15],"sectors_fail":[],"dump_hex":"b2d321773708040005116ac96f99fd904730312d423000004746473031000000504554470000000000000000000000000000000000008787876900000000000050455447205472616e736c7563656e7461b0ff80e80300000000e03f0000000041000800000000000401e6000000000000000000000087878769000000000000342134218403e8033333333fcdcc4c3eaa42f322172c48e99c0ece895346ea9f00000000c9000000000000000000000000000000000087878769000000000000323032365f30345f30325f31335f343632303236303430320000000000000000000000004a01000000000000000000000000000000008787876900000000000002000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000087878769000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000878787690000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008787876900000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000087878769000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000878787690000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008787876900000000000001000000000000000000000000000000000000000000000000000000000000001bc1b3d3eb58970ec6aa04d904b7280e00000000000087878769000000000000f7d96a1c22d18e419694cf375bc072f9f0325fdf07301faf4b7f63aec72f962863d4e42761e1d59c30292e45b8088bc800000000000087878769000000000000fadc47e3d52f8722df5e7613c9445c4f9a0a7a3a87362a4f8da04fdefbf082a98897d99124e9f61021e29c5e28c5ffd500000000000087878769000000000000fa03e9bc64418cee3eac18ac24a08e8195ebdb985b3780a570b83ac5c2b3e4576e80a5c5b4f5a44d80695cc9a1ea328700000000000087878769000000000000a56fb5e43f60cf4414a458576596307b2028bdd57e449e69cdccbdc5cfb5b119ebc6f1788549fdfbfcf0478c0f9d386700000000000087878769000000000000416768587629189df719eb6b91851f6191241892547475cddc0ce27eb6ba4f5e8bba9736223405f48296d27b5a5b82b500000000000087878769000000000000","dump_b64":"stMhdzcIBAAFEWrJb5n9kEcwMS1CMAAAR0ZHMDEAAABQRVRHAAAAAAAAAAAAAAAAAAAAAAAAh4eHaQAAAAAAAFBFVEcgVHJhbnNsdWNlbnRhsP+A6AMAAAAA4D8AAAAAQQAIAAAAAAAEAeYAAAAAAAAAAAAAAIeHh2kAAAAAAAA0ITQhhAPoAzMzMz/NzEw+qkLzIhcsSOmcDs6JU0bqnwAAAADJAAAAAAAAAAAAAAAAAAAAAACHh4dpAAAAAAAAMjAyNl8wNF8wMl8xM180NjIwMjYwNDAyAAAAAAAAAAAAAAAASgEAAAAAAAAAAAAAAAAAAAAAh4eHaQAAAAAAAAIAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIeHh2kAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACHh4dpAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAh4eHaQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIeHh2kAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACHh4dpAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAh4eHaQAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAG8Gz0+tYlw7GqgTZBLcoDgAAAAAAAIeHh2kAAAAAAAD32WocItGOQZaUzzdbwHL58DJf3wcwH69Lf2Ouxy+WKGPU5Cdh4dWcMCkuRbgIi8gAAAAAAACHh4dpAAAAAAAA+txH49UvhyLfXnYTyURcT5oKejqHNipPjaBP3vvwgqmIl9mRJOn2ECHinF4oxf/VAAAAAAAAh4eHaQAAAAAAAPoD6bxkQYzuPqwYrCSgjoGV69uYWzeApXC4OsXCs+RXboClxbT1pE2AaVzJoeoyhwAAAAAAAIeHh2kAAAAAAAClb7XkP2DPRBSkWFdlljB7ICi91X5EnmnNzL3Fz7WxGevG8XiFSf37/PBHjA+dOGcAAAAAAACHh4dpAAAAAAAAQWdoWHYpGJ33GetrkYUfYZEkGJJUdHXN3Azifra6T16Lupc2IjQF9IKW0ntaW4K1AAAAAAAAh4eHaQAAAAAAAA=="} \ No newline at end of file diff --git a/testdata/dumps/e27be276.bin b/testdata/dumps/e27be276.bin new file mode 100644 index 0000000..e67d3da Binary files /dev/null and b/testdata/dumps/e27be276.bin differ diff --git a/testdata/dumps/e27be276.json b/testdata/dumps/e27be276.json new file mode 100644 index 0000000..8d0de9e --- /dev/null +++ b/testdata/dumps/e27be276.json @@ -0,0 +1 @@ +{"format":"filanfc-dump/v1","scanned_at":"2026-08-23T19:52:02.388908Z","chip_uid":"e27be276","sak":"08","atqa":"0004","tech":["android.nfc.tech.MifareClassic","android.nfc.tech.NfcA"],"sectors_ok":[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15],"sectors_fail":[],"dump_hex":"e27be2760d08040005919d6a17ddcf904730312d423000004746473031000000504554470000000000000000000000000000000000008787876900000000000050455447205472616e736c7563656e7461b0ff80e80300000000e03f0000000041000800000000000401e6000000000000000000000087878769000000000000342134218403e8033333333fcdcc4c3e2fb8e0e972084e74bf737393b28e7b1200000000c9000000000000000000000000000000000087878769000000000000323032365f30345f30325f31335f323132303236303430320000000000000000000000004a0100000000000000000000000000000000878787690000000000000200010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008787876900000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000087878769000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000878787690000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008787876900000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000087878769000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000878787690000000000000100000000000000000000000000000000000000000000000000000000000000630fe06e572f8f0f94e2ff3bfba403190000000000008787876900000000000074eaa3ac7501ae371180023e3e834388526896deb1450667081e07af5ad55257dabb43981c79bd7263072f7d4727292f000000000000878787690000000000004f52ee7faa3adf093b19814efac2123f66a0cd95e9da3abc88cb652f284bc98e6d1098712a24e49ae0d4c682783cb6b400000000000087878769000000000000433a59c30c8920059b3d004360d17bc2670314a1a927acf0527258297d89bd72301337347a597fd7135d9af0a436e437000000000000878787690000000000006fd6cc3b82cd5c326eb0ff87104db3440322b1e578cf30410f2fc66271da62bd627347fea583a136f72599a900ccf14e000000000000878787690000000000002e07c6b457d7672aa10ed15b889180dedeb1a26fceb509fe96da5740c5c234dae1a96fe45a819a4acfbdaa225931999800000000000087878769000000000000","dump_b64":"4nvidg0IBAAFkZ1qF93PkEcwMS1CMAAAR0ZHMDEAAABQRVRHAAAAAAAAAAAAAAAAAAAAAAAAh4eHaQAAAAAAAFBFVEcgVHJhbnNsdWNlbnRhsP+A6AMAAAAA4D8AAAAAQQAIAAAAAAAEAeYAAAAAAAAAAAAAAIeHh2kAAAAAAAA0ITQhhAPoAzMzMz/NzEw+L7jg6XIITnS/c3OTso57EgAAAADJAAAAAAAAAAAAAAAAAAAAAACHh4dpAAAAAAAAMjAyNl8wNF8wMl8xM18yMTIwMjYwNDAyAAAAAAAAAAAAAAAASgEAAAAAAAAAAAAAAAAAAAAAh4eHaQAAAAAAAAIAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIeHh2kAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACHh4dpAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAh4eHaQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIeHh2kAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACHh4dpAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAh4eHaQAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAYw/gblcvjw+U4v87+6QDGQAAAAAAAIeHh2kAAAAAAAB06qOsdQGuNxGAAj4+g0OIUmiW3rFFBmcIHgevWtVSV9q7Q5gceb1yYwcvfUcnKS8AAAAAAACHh4dpAAAAAAAAT1Luf6o63wk7GYFO+sISP2agzZXp2jq8iMtlLyhLyY5tEJhxKiTkmuDUxoJ4PLa0AAAAAAAAh4eHaQAAAAAAAEM6WcMMiSAFmz0AQ2DRe8JnAxShqSes8FJyWCl9ib1yMBM3NHpZf9cTXZrwpDbkNwAAAAAAAIeHh2kAAAAAAABv1sw7gs1cMm6w/4cQTbNEAyKx5XjPMEEPL8ZicdpivWJzR/6lg6E29yWZqQDM8U4AAAAAAACHh4dpAAAAAAAALgfGtFfXZyqhDtFbiJGA3t6xom/OtQn+ltpXQMXCNNrhqW/kWoGaSs+9qiJZMZmYAAAAAAAAh4eHaQAAAAAAAA=="} \ No newline at end of file diff --git a/testdata/dumps/f28c58ed.bin b/testdata/dumps/f28c58ed.bin new file mode 100644 index 0000000..55d9076 Binary files /dev/null and b/testdata/dumps/f28c58ed.bin differ diff --git a/testdata/dumps/f28c58ed.json b/testdata/dumps/f28c58ed.json new file mode 100644 index 0000000..4361666 --- /dev/null +++ b/testdata/dumps/f28c58ed.json @@ -0,0 +1 @@ +{"format":"filanfc-dump/v1","scanned_at":"2026-08-23T19:53:49.531896Z","chip_uid":"f28c58ed","sak":"08","atqa":"0004","tech":["android.nfc.tech.MifareClassic","android.nfc.tech.NfcA"],"sectors_ok":[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15],"sectors_fail":[],"dump_hex":"f28c58edcb08040005fbe0f72ba5ad904730302d4b3030004746473030000000504554470000000000000000000000000000000000008787876900000000000050455447204261736963000000000000000000ffe80300000000e03f0000000041000800000000000401e6000000000000000000000087878769000000000000000000000000000000000000cdcc4c3e3cb568b7af4f41819412fe60afc5c44600000000ab0f0000000000000000000000000000000087878769000000000000323032365f30325f32355f31365f313932365f30325f32355f31360000000000000000004a010000000000000000000000000000000087878769000000000000020001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000878787690000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008787876900000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000087878769000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000878787690000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008787876900000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000087878769000000000000010000000000000000000000000000000000000000000000000000000000000050ee165b9ac59477618c747996394944000000000000878787690000000000000821e5296e38edaf4e579c0a3e385a718d7cd7cae8bcf1885ab51da9d642d7b5d00bd7bf9222d1cdd573728a842d9328000000000000878787690000000000004b929502ea8bdcc645a7d415cfeb6c0449095bf56227d777936a3492e256583dbd494e976e67c421c7293696fea9380700000000000087878769000000000000b96ad7d6fcd727be230357d9e228a669819e5727dfb287e0649347da066ca2e1e6b043aea72f43b7aae856d3fd1cc30b00000000000087878769000000000000be81eea3831737d3e3fdeb81e0b360206d3db6f50864effbf3e784a8547f0a9a730e3af09b052b0de6f7fe87959cee4f00000000000087878769000000000000e3f7d07cc16a75a374b81b53a73256b34921a3fcf6e6688a9925c40e2acb6786bf4457280976268f5703988b49b73a3400000000000087878769000000000000","dump_b64":"8oxY7csIBAAF++D3K6WtkEcwMC1LMDAAR0ZHMDAAAABQRVRHAAAAAAAAAAAAAAAAAAAAAAAAh4eHaQAAAAAAAFBFVEcgQmFzaWMAAAAAAAAAAAD/6AMAAAAA4D8AAAAAQQAIAAAAAAAEAeYAAAAAAAAAAAAAAIeHh2kAAAAAAAAAAAAAAAAAAAAAAADNzEw+PLVot69PQYGUEv5gr8XERgAAAACrDwAAAAAAAAAAAAAAAAAAAACHh4dpAAAAAAAAMjAyNl8wMl8yNV8xNl8xOTI2XzAyXzI1XzE2AAAAAAAAAAAASgEAAAAAAAAAAAAAAAAAAAAAh4eHaQAAAAAAAAIAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIeHh2kAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACHh4dpAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAh4eHaQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIeHh2kAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACHh4dpAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAh4eHaQAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUO4WW5rFlHdhjHR5ljlJRAAAAAAAAIeHh2kAAAAAAAAIIeUpbjjtr05XnAo+OFpxjXzXyui88YhatR2p1kLXtdAL17+SItHN1XNyioQtkygAAAAAAACHh4dpAAAAAAAAS5KVAuqL3MZFp9QVz+tsBEkJW/ViJ9d3k2o0kuJWWD29SU6XbmfEIccpNpb+qTgHAAAAAAAAh4eHaQAAAAAAALlq19b81ye+IwNX2eIopmmBnlcn37KH4GSTR9oGbKLh5rBDrqcvQ7eq6FbT/RzDCwAAAAAAAIeHh2kAAAAAAAC+ge6jgxc30+P964Hgs2AgbT229Qhk7/vz54SoVH8KmnMOOvCbBSsN5vf+h5Wc7k8AAAAAAACHh4dpAAAAAAAA4/fQfMFqdaN0uBtTpzJWs0kho/z25miKmSXEDirLZ4a/RFcoCXYmj1cDmItJtzo0AAAAAAAAh4eHaQAAAAAAAA=="} \ No newline at end of file