nx_png_chunks.nx source
↩ module page · 123 lines · 5795 B
1// nx_png_chunks.nx -- THE PNG CHUNK WALK (one reader, shared): signature, then [length(4, big-endian)][type(4)][data][crc(4)]
2// until IEND. First consumer is companionchat CC2 (a Tavern V2 character card is base64 JSON inside a tEXt chunk whose
3// keyword is `chara`); gen G15 (workflow ingest from a rival's PNG metadata) names the same walk. EXTRACTED here so there is
4// exactly one chunk walker in the estate -- go_png_dim in the orchestrator reads only the IHDR dimensions and stays as is.
5// DECLARED IMPRECISION: CRCs are not verified. This walker EXTRACTS metadata; it does not validate an image, and a wrong CRC
6// on a text chunk cannot change the bytes it returns. A chunk whose declared length overruns the buffer is MALFORMED and the
7// walk stops there rather than reading past the end. license_tier: ORIGINAL
8import "nx_syscalls.nx"
9
10const PNG_SIG_LEN: i64 = 8
11const PNG_CHUNK_HDR_LEN: i64 = 8
12const PNG_CRC_LEN: i64 = 4
13const PNG_TYPE_LEN: i64 = 4
14const PNG_BYTE_MASK: i64 = 255
15const PNG_BITS_PER_BYTE: i64 = 8
16const PNG_CH_NUL: i64 = 0
17// return codes of png_find_text
18const PNG_FOUND: i64 = 1
19const PNG_ABSENT: i64 = 0
20const PNG_NOT_PNG: i64 = 0 - 1
21const PNG_MALFORMED: i64 = 0 - 2
22
23func png_len(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } return n }
24func png_be32(b: *u8, p: i64) -> i64 {
25 var v: i64 = 0
26 var i: i64 = 0
27 while i < PNG_TYPE_LEN { v = (v << PNG_BITS_PER_BYTE) | ((b[p + i] as i64) & PNG_BYTE_MASK); i = i + 1 }
28 return v
29}
30// 1 when buf[0..n) starts with the 8-byte PNG signature
31func png_is(buf: *u8, n: i64) -> i64 {
32 if n < PNG_SIG_LEN { return 0 }
33 if (buf[0] as i64) != 137 { return 0 }
34 if (buf[1] as i64) != 80 { return 0 }
35 if (buf[2] as i64) != 78 { return 0 }
36 if (buf[3] as i64) != 71 { return 0 }
37 if (buf[4] as i64) != 13 { return 0 }
38 if (buf[5] as i64) != 10 { return 0 }
39 if (buf[6] as i64) != 26 { return 0 }
40 if (buf[7] as i64) != 10 { return 0 }
41 return 1
42}
43// does the 4-byte type at buf[p..p+4) spell `t`?
44func png_type_is(buf: *u8, p: i64, t: *u8) -> i64 {
45 var i: i64 = 0
46 while i < PNG_TYPE_LEN { if buf[p + i] != t[i] { return 0 } i = i + 1 }
47 return 1
48}
49// walk every chunk; returns the chunk count up to and including IEND, or PNG_NOT_PNG / PNG_MALFORMED.
50func png_chunk_count(buf: *u8, n: i64) -> i64 {
51 if png_is(buf, n) == 0 { return PNG_NOT_PNG }
52 var p: i64 = PNG_SIG_LEN
53 var count: i64 = 0
54 var walk: i64 = 1
55 var rc: i64 = 0
56 while walk == 1 {
57 if p + PNG_CHUNK_HDR_LEN > n { rc = PNG_MALFORMED; walk = 0 } else {
58 let ln: i64 = png_be32(buf, p)
59 if p + PNG_CHUNK_HDR_LEN + ln + PNG_CRC_LEN > n { rc = PNG_MALFORMED; walk = 0 } else {
60 count = count + 1
61 if png_type_is(buf, p + PNG_TYPE_LEN, "IEND" as *u8) == 1 { rc = count; walk = 0 } else { p = p + PNG_CHUNK_HDR_LEN + ln + PNG_CRC_LEN }
62 }
63 }
64 }
65 return rc
66}
67// find the tEXt chunk whose keyword is `keyword`; on PNG_FOUND out[0] = offset of the text (just past the keyword's NUL),
68// out[1] = its length. PNG_ABSENT when no such chunk precedes IEND; PNG_NOT_PNG / PNG_MALFORMED as named.
69func png_find_text(buf: *u8, n: i64, keyword: *u8, out: *i64) -> i64 {
70 out[0] = 0
71 out[1] = 0
72 if png_is(buf, n) == 0 { return PNG_NOT_PNG }
73 let kl: i64 = png_len(keyword)
74 var p: i64 = PNG_SIG_LEN
75 var walk: i64 = 1
76 var rc: i64 = PNG_ABSENT
77 while walk == 1 {
78 if p + PNG_CHUNK_HDR_LEN > n { rc = PNG_MALFORMED; walk = 0 } else {
79 let ln: i64 = png_be32(buf, p)
80 let dp: i64 = p + PNG_CHUNK_HDR_LEN
81 if dp + ln + PNG_CRC_LEN > n { rc = PNG_MALFORMED; walk = 0 } else {
82 if png_type_is(buf, p + PNG_TYPE_LEN, "IEND" as *u8) == 1 { walk = 0 } else {
83 if png_type_is(buf, p + PNG_TYPE_LEN, "tEXt" as *u8) == 1 {
84 if ln > kl {
85 if (buf[dp + kl] as i64) == PNG_CH_NUL {
86 var same: i64 = 1
87 var i: i64 = 0
88 while i < kl { if buf[dp + i] != keyword[i] { same = 0; i = kl } else { i = i + 1 } }
89 if same == 1 { out[0] = dp + kl + 1; out[1] = ln - kl - 1; rc = PNG_FOUND; walk = 0 }
90 }
91 }
92 }
93 if walk == 1 { p = dp + ln + PNG_CRC_LEN }
94 }
95 }
96 }
97 }
98 return rc
99}
100// WRITER for fixtures and exporters: append one chunk (type + data) at buf[off], returns the new offset. The CRC field is
101// written as zero -- see the header: this reader never checks it, and a fixture that claims a CRC it did not compute would be
102// a second copy of a number nobody verifies. A consumer that needs a browser-valid PNG must compute the CRC itself.
103func png_put_chunk(buf: *u8, off: i64, t: *u8, data: *u8, dlen: i64) -> i64 {
104 var o: i64 = off
105 buf[o] = ((dlen >> 24) & PNG_BYTE_MASK) as u8; buf[o + 1] = ((dlen >> 16) & PNG_BYTE_MASK) as u8
106 buf[o + 2] = ((dlen >> 8) & PNG_BYTE_MASK) as u8; buf[o + 3] = (dlen & PNG_BYTE_MASK) as u8
107 o = o + PNG_TYPE_LEN
108 var i: i64 = 0
109 while i < PNG_TYPE_LEN { buf[o + i] = t[i]; i = i + 1 }
110 o = o + PNG_TYPE_LEN
111 i = 0
112 while i < dlen { buf[o + i] = data[i]; i = i + 1 }
113 o = o + dlen
114 i = 0
115 while i < PNG_CRC_LEN { buf[o + i] = 0 as u8; i = i + 1 }
116 return o + PNG_CRC_LEN
117}
118// the 8-byte signature at buf[0..8); returns 8
119func png_put_sig(buf: *u8) -> i64 {
120 buf[0] = 137 as u8; buf[1] = 80 as u8; buf[2] = 78 as u8; buf[3] = 71 as u8
121 buf[4] = 13 as u8; buf[5] = 10 as u8; buf[6] = 26 as u8; buf[7] = 10 as u8
122 return PNG_SIG_LEN
123}