pinapod 0.4.3

Zero-copy pod types with derive macros. Alignment-1 representations for zero-overhead data access.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
#![allow(
	unsafe_code,
	unused_qualifications,
	missing_docs,
	reason = "the derive macro emits audited zero-copy code, upstream tests preserve explicit \
	          trait paths, and these test fixtures are not a published surface"
)]

use pinapod::PinaPod;
use pinapod::PinaPodCompact;
use pinapod::pod::PodBool;

#[allow(dead_code)]
#[derive(PinaPod)]
struct Validatable {
	pub score: u64,
	pub active: bool,
	pub maybe: Option<u64>,
	pub name: pinapod::String<8>,
	pub items: pinapod::Vec<u8, 4>,
}

// Layout:
//   score:  offset 0,  size 8  (PodU64)
//   active: offset 8,  size 1  (PodBool)
//   maybe:  offset 9,  size 9  (PodOption<PodU64>: tag 1 + value 8)
//   name:   offset 18, size 9  (PodString<8,1>: len 1 + data 8)
//   items:  offset 27, size 6  (PodVec<u8,4,2>: len 2 + data 4)
//   total: 33

#[test]
fn validate_correct_size() {
	assert_eq!(Validatable::SIZE, 33);
}

// --- `validate_layout`'s default is the full walk ---

/// A hand-written compact schema, the case the additive method must not break.
///
/// `validate_layout` is a *provided* method on `PinaPodCompact`, so this impl
/// compiles without it — and must then inherit the full `validate` rather than
/// an empty or weaker check. A custom impl that silently skipped the semantic
/// walk would be a soundness hole in every safe reader that used it.
#[repr(C)]
#[derive(Clone, Copy)]
struct HandWrittenHeader {
	value: pinapod::pod::PodU64,
}

impl pinapod::ZcValidate for HandWrittenHeader {
	fn validate_ref(value: &Self) -> Result<(), pinapod::PinaPodError> {
		if value.value.get() > 100 {
			return Err(pinapod::PinaPodError::InvalidLength);
		}
		Ok(())
	}
}

// SAFETY: `HandWrittenHeader` is `#[repr(C)]` over one alignment-one `PodU64`.
unsafe impl pinapod::ZcElem for HandWrittenHeader {}

struct HandWritten;

impl PinaPod for HandWritten {}

// SAFETY: `Header` is the complete representation, `HEADER_SIZE` is its size,
// and `validate` checks the schema's one restricted-domain field. This impl
// deliberately does not override `validate_layout`, so it exercises the default.
unsafe impl PinaPodCompact for HandWritten {
	type Header = HandWrittenHeader;

	const HEADER_SIZE: usize = core::mem::size_of::<HandWrittenHeader>();
	const MAX_SIZE: usize = core::mem::size_of::<HandWrittenHeader>();
	const MIN_SIZE: usize = core::mem::size_of::<HandWrittenHeader>();
	const TAIL_ALIGNMENT: usize = 1;

	fn validate(data: &[u8]) -> Result<(), pinapod::PinaPodError> {
		Self::validate_storage_len(data.len())?;
		// SAFETY: `MIN_SIZE` equals `HEADER_SIZE`, so the length check above
		// proves this cast is in bounds; the header is an alignment-one ZcElem.
		let header = unsafe { &*data.as_ptr().cast::<HandWrittenHeader>() };
		<HandWrittenHeader as pinapod::ZcValidate>::validate_ref(header)
	}
}

#[test]
fn validate_layout_defaults_to_the_full_walk_for_hand_written_impls() {
	let valid = 7u64.to_le_bytes();
	assert_eq!(HandWritten::validate(&valid), Ok(()));
	assert_eq!(
		HandWritten::validate_layout(&valid),
		Ok(()),
		"a valid representation passes both depths"
	);

	// The schema's own semantic rule. A default that skipped `validate` would
	// accept this buffer, so this assertion is what pins the safe default.
	let invalid = 101u64.to_le_bytes();
	assert_eq!(
		HandWritten::validate(&invalid),
		Err(pinapod::PinaPodError::InvalidLength)
	);
	assert_eq!(
		HandWritten::validate_layout(&invalid),
		Err(pinapod::PinaPodError::InvalidLength),
		"the default must not weaken a hand-written impl's semantic checks"
	);

	// And the allocation contract is still enforced through the shared prefix.
	let short: [u8; 4] = [0; 4];
	assert_eq!(
		HandWritten::validate_layout(&short),
		Err(pinapod::PinaPodError::InvalidLength)
	);
}

#[test]
fn validate_zeroed_buffer_ok() {
	let buf = [0u8; 33];
	assert!(Validatable::read_exact(&buf).is_ok());
}

#[test]
fn validate_bad_bool() {
	let mut buf = [0u8; 33];
	buf[8] = 2; // active field: invalid bool value
	assert!(Validatable::read_exact(&buf).is_err());
}

#[test]
fn validate_truncated_buffer() {
	let buf = [0u8; 20]; // too small (need 33)
	assert!(Validatable::read_exact(&buf).is_err());
}

#[test]
fn validate_bad_option_tag() {
	let mut buf = [0u8; 33];
	buf[9] = 3; // maybe field tag: invalid (must be 0 or 1)
	assert!(Validatable::read_exact(&buf).is_err());
}

#[test]
fn validate_overlength_string() {
	let mut buf = [0u8; 33];
	buf[18] = 9; // name len prefix: 9 > max capacity 8
	assert!(Validatable::read_exact(&buf).is_err());
}

#[test]
fn validate_overlength_vec() {
	let mut buf = [0u8; 33];
	buf[27] = 5; // items len prefix (LE u16 low byte): 5 > max capacity 4
	buf[28] = 0; // items len prefix (LE u16 high byte)
	assert!(Validatable::read_exact(&buf).is_err());
}

// --- ZcValidate: invalid UTF-8 in fixed PodString ---

#[test]
fn validate_rejects_invalid_utf8_in_string() {
	let mut buf = [0u8; 33];
	// name field: offset 18, PodString<8,1>
	// Set len prefix to 2
	buf[18] = 2;
	// Write invalid UTF-8 bytes in the data portion (offset 19)
	buf[19] = 0xFF;
	buf[20] = 0xFE;
	assert!(Validatable::read_exact(&buf).is_err());
}

// --- ZcValidate: Option<bool> inner validation ---

#[allow(dead_code)]
#[derive(PinaPod)]
struct WithOptionBool {
	pub flag: Option<bool>,
}

// Layout: PodOption<PodBool>: tag(1) + PodBool(1) = 2

#[test]
fn validate_option_bool_none_ok() {
	let buf = [0u8; 2]; // tag=0, None
	assert!(WithOptionBool::read_exact(&buf).is_ok());
}

#[test]
fn validate_option_bool_some_valid() {
	let buf = [1u8, 1]; // tag=1, inner=1 (true)
	assert!(WithOptionBool::read_exact(&buf).is_ok());
}

#[test]
fn validate_option_bool_some_invalid_inner() {
	let buf = [1u8, 5]; // tag=1 (Some), inner byte=5 (invalid bool)
	assert!(WithOptionBool::read_exact(&buf).is_err());
}

// --- ZcValidate: Option<Enum> inner validation ---

#[derive(PinaPod, Debug, PartialEq)]
#[repr(u8)]
enum Color {
	Red = 0,
	Green = 1,
	Blue = 2,
}

#[allow(dead_code)]
#[derive(PinaPod)]
struct WithOptionEnum {
	pub color: Option<Color>,
}

// Layout: PodOption<ColorZc>: tag(1) + ColorZc(1) = 2

#[test]
fn validate_option_enum_none_ok() {
	let buf = [0u8; 2]; // tag=0, None
	assert!(WithOptionEnum::read_exact(&buf).is_ok());
}

#[test]
fn validate_option_enum_some_valid() {
	let buf = [1u8, 2]; // tag=1, inner=2 (Blue)
	assert!(WithOptionEnum::read_exact(&buf).is_ok());
}

#[test]
fn validate_option_enum_some_invalid_inner() {
	let buf = [1u8, 99]; // tag=1 (Some), inner=99 (invalid discriminant)
	assert!(WithOptionEnum::read_exact(&buf).is_err());
}

// --- Compact validation ---

#[allow(dead_code)]
#[derive(PinaPod)]
#[pinapod(compact)]
struct CompactVal {
	pub authority: [u8; 32],
	pub bio: pinapod::String<16>,
}

// Compact header: authority(32) + bio_len(1, PFX=1) = 33

#[test]
fn compact_validate_overlength_tail_string() {
	let mut buf = vec![0u8; 100];
	buf[32] = 17; // bio_len = 17 > max 16
	assert!(CompactVal::validate(&buf).is_err());
}

#[test]
fn compact_validate_tail_exceeds_buffer() {
	let mut buf = vec![0u8; 40]; // header(33) + only 7 bytes of tail
	buf[32] = 10; // bio_len = 10, needs 33 + 10 = 43 bytes
	assert!(CompactVal::validate(&buf).is_err());
}

#[test]
fn compact_validate_rejects_invalid_utf8_in_tail_string() {
	let mut buf = vec![0u8; 100];
	// bio_len at offset 32, PFX=1
	buf[32] = 3; // bio_len = 3
	// bio data starts at offset 33 (header size = 33)
	buf[33] = 0xFF; // invalid UTF-8
	buf[34] = 0xFE;
	buf[35] = 0xFD;
	assert!(CompactVal::validate(&buf).is_err());
}

// --- Compact: inline bool validation via ZcValidate ---

#[allow(dead_code)]
#[derive(PinaPod)]
#[pinapod(compact)]
struct CompactWithBool {
	pub active: bool,
	pub bio: pinapod::String<8>,
}

// Header: PodBool(1) + bio_len(1) = 2

#[test]
fn compact_validate_inline_bad_bool() {
	let mut buf = vec![0u8; 20];
	buf[0] = 5; // active field: invalid bool
	assert!(CompactWithBool::validate(&buf).is_err());
}

// --- ZcValidate: PodVec element validation ---
//
// Tests validate that PodVec<PodBool, N> correctly validates each element.
// We test at the storage level directly (no derive) since the derive lowers
// bool → PodBool automatically, and PodVec<bool> is intentionally not valid
// (bool is not ZcElem because &bool from arbitrary bytes is UB).

#[test]
fn validate_vec_bool_all_valid() {
	// Layout: PodVec<PodBool, 5, 2>: len(2) + data(5) = 7
	let mut buf = [0u8; 7];
	// len = 3 (LE u16)
	buf[0] = 3;
	buf[1] = 0;
	// elements: 0, 1, 0 (all valid bool)
	buf[2] = 0;
	buf[3] = 1;
	buf[4] = 0;
	let v = unsafe { &*(buf.as_ptr().cast::<pinapod::pod::PodVec<PodBool, 5>>()) };
	assert!(pinapod::ZcValidate::validate_ref(v).is_ok());
}

#[test]
fn validate_vec_bool_rejects_invalid_element() {
	let mut buf = [0u8; 7];
	// len = 3 (LE u16)
	buf[0] = 3;
	buf[1] = 0;
	// elements: 0, 1, 5 — third element is invalid bool
	buf[2] = 0;
	buf[3] = 1;
	buf[4] = 5;
	let v = unsafe { &*(buf.as_ptr().cast::<pinapod::pod::PodVec<PodBool, 5>>()) };
	assert!(pinapod::ZcValidate::validate_ref(v).is_err());
}

#[cfg(target_pointer_width = "32")]
#[test]
fn validate_rejects_eight_byte_lengths_that_do_not_fit_usize() {
	use pinapod::PodString;
	use pinapod::ZcValidate;
	use pinapod::pod::PodVec;

	// 2^32 encoded as a little-endian u64. This cannot be represented by a
	// 32-bit usize, even for a zero-capacity container.
	let bytes: [u8; 8] = [0, 0, 0, 0, 1, 0, 0, 0];
	let string = unsafe { &*(bytes.as_ptr() as *const PodString<0, 8>) };
	let vector = unsafe { &*(bytes.as_ptr() as *const PodVec<u8, 0, 8>) };

	assert!(ZcValidate::validate_ref(string).is_err());
	assert!(ZcValidate::validate_ref(vector).is_err());
}

// --- ZcValidate: PodVec element validation works at the pod level ---
// Vec<Enum, N> in schema doesn't work directly because the type alias
// expands to PodVec<Enum, N> and Enum isn't Copy. This is a known v1
// limitation. For enum vectors, use PodBool as a proxy test since
// the ZcValidate recursion works the same way for any validated element type.

// --- PodString truncate char boundary ---

#[test]
fn podstring_truncate_snaps_to_char_boundary() {
	use pinapod::pod::PodString;
	let mut s = PodString::<32>::default();
	s.try_set("h\u{00e9}llo").unwrap(); // 'e\u{0301}' — actually \u{00e9} is 2 bytes: [0xC3, 0xA9]
	// String bytes: h(1) + \u{00e9}(2) + l(1) + l(1) + o(1) = 6 bytes
	assert_eq!(s.len(), 6);

	// Truncate at byte 2 — mid-codepoint (inside the 2-byte \u{00e9})
	s.truncate(2);
	// Should snap back to byte 1 (after 'h')
	assert_eq!(s.len(), 1);
	assert_eq!(s.as_str(), "h");
	// Verify it's valid UTF-8
	assert!(core::str::from_utf8(s.as_bytes()).is_ok());
}

#[test]
fn podstring_truncate_at_boundary_is_exact() {
	use pinapod::pod::PodString;
	let mut s = PodString::<32>::default();
	s.try_set("h\u{00e9}llo").unwrap();
	// Truncate at byte 3 — exactly after \u{00e9} (valid boundary)
	s.truncate(3);
	assert_eq!(s.len(), 3);
	assert_eq!(s.as_str(), "h\u{00e9}");
}

// --- Error variant specificity tests ---

#[test]
fn error_invalid_bool_variant() {
	let buf = [2u8]; // bad bool byte
	let val = unsafe { &*(buf.as_ptr().cast::<pinapod::pod::PodBool>()) };
	let err = <pinapod::pod::PodBool as pinapod::ZcValidate>::validate_ref(val);
	assert_eq!(err, Err(pinapod::PinaPodError::InvalidBool));
}

#[test]
fn error_invalid_tag_variant() {
	let buf = [5u8, 0u8]; // bad option tag
	let val = unsafe { &*(buf.as_ptr().cast::<pinapod::pod::PodOption<u8>>()) };
	let err = <pinapod::pod::PodOption<u8> as pinapod::ZcValidate>::validate_ref(val);
	assert_eq!(err, Err(pinapod::PinaPodError::InvalidTag));
}

// --- PodOption: is_some/is_none on invalid tag ---

#[test]
fn pod_option_invalid_tag_is_not_some() {
	// Construct a PodOption with raw tag = 0xFF (invalid).
	let buf = [0xFFu8, 42u8]; // PodOption<u8>: tag(1) + value(1)
	let opt = unsafe { &*(buf.as_ptr().cast::<pinapod::pod::PodOption<u8>>()) };

	// is_some() must NOT return true for invalid tags.
	assert!(
		!opt.is_some(),
		"invalid tag 0xFF must not be treated as Some"
	);
	assert!(opt.is_none(), "invalid tag 0xFF must be treated as None");
	// get() must return None for invalid tags.
	assert_eq!(opt.get(), None);
}

// --- Wincode: PodOption inner validation ---

#[cfg(feature = "wincode")]
mod wincode_option_validation {
	use pinapod::pod::PodBool;
	use pinapod::pod::PodOption;

	#[test]
	fn wincode_read_rejects_option_with_invalid_inner() {
		// Construct raw bytes: tag=1 (Some), inner byte=5 (invalid PodBool).
		let bytes: [u8; 2] = [1, 5];
		let result = wincode::deserialize::<PodOption<PodBool>>(&bytes);
		assert!(
			result.is_err(),
			"wincode SchemaRead must reject PodOption<PodBool> with invalid inner byte"
		);
	}

	#[test]
	fn wincode_read_accepts_valid_option_some() {
		let bytes: [u8; 2] = [1, 1]; // tag=1, inner=1 (true)
		let result = wincode::deserialize::<PodOption<PodBool>>(&bytes);
		assert!(result.is_ok());
	}

	#[test]
	fn wincode_read_accepts_valid_option_none() {
		let bytes: [u8; 2] = [0, 0]; // tag=0
		let result = wincode::deserialize::<PodOption<PodBool>>(&bytes);
		assert!(result.is_ok());
	}
}

// --- Compact: tail Vec element validation ---

#[allow(dead_code)]
#[derive(PinaPod)]
#[pinapod(compact)]
struct CompactWithVecBool {
	pub score: u64,
	pub flags: pinapod::Vec<PodBool, 4>,
}

// Compact header: score(PodU64=8) + flags_len([u8;2]=2) = 10
// On-chain: [header(10)][tail: flags data]

#[test]
fn compact_validate_rejects_invalid_vec_bool_element() {
	let mut buf = vec![0u8; 13]; // 10 header + 3 tail
	// flags_len at offset 8, PFX=2 (LE u16)
	buf[8] = 3; // count = 3
	buf[9] = 0;
	// Tail data at offset 10
	buf[10] = 0; // valid PodBool
	buf[11] = 1; // valid PodBool
	buf[12] = 5; // INVALID PodBool (byte > 1)

	assert!(
		CompactWithVecBool::validate(&buf).is_err(),
		"compact validate must reject Vec tail with invalid PodBool element"
	);
}

#[test]
fn compact_validate_accepts_valid_vec_bool_elements() {
	let mut buf = vec![0u8; 12]; // 10 header + 2 tail
	buf[8] = 2; // count = 2
	buf[9] = 0;
	buf[10] = 0; // valid
	buf[11] = 1; // valid

	assert!(CompactWithVecBool::validate(&buf).is_ok());
}