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
// http://preserve.mactech.com/articles/mactech/Vol.21/21.08/PDBFile/index.html reference data

//! Item category record helpers

use core::{fmt::Debug, str};
use std::io::{self, Cursor, Read, Write};

use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt};

use crate::{header::DatabaseHeader, info::ExtraInfoRecord};

/// Representation of an item category
#[derive(Debug, Copy, Clone, PartialEq)]
pub struct ExtraInfoCategory {
	/// Category ID
	///
	/// Category IDs are in the range `0..15`, where category ID `0` (zero) is reserved for the
	/// "Unfiled" category. This gives a maximum of 15 user-definable categories.
	pub category_id: u8,

	/// Category name
	///
	/// 16-character null-padded string - the same format used by the [`DatabaseHeader`] `name`
	/// field.
	pub name: [u8; 16],

	/// Has the category been renamed?
	pub renamed: bool,
}

impl ExtraInfoCategory {
	/// Attempt to convert the category name to a [`str`][core::str]
	pub fn name_try_str<'x>(&'x self) -> Result<&'x str, str::Utf8Error> {
		let mut idx = 0;
		while idx < self.name.len() {
			if self.name[idx] == 0u8 {
				break;
			}

			idx += 1;
		}

		str::from_utf8(&self.name[0..idx])
	}
}

// struct // from reference at top of page
// {
//     unsigned short renamedCategories;
//     unsigned char  categoryLabels[16][16];
//     unsigned char  categoryUniqueIDs[16];
//     unsigned char  lastUniqueID;
//     unsigned char  RSVD;
// } 	AppInfoType;

#[derive(Debug, Clone, PartialEq, Default)]
pub struct AppInfoCategories {
	/// The number of categories renamed by the user
	pub renamed_categories: u16,
	pub categories: Vec<ExtraInfoCategory>,
	category_unique_ids: [u8; 16],
	last_unique_id: u8,
	rsvd: u8,

	/// check if it's a data header and shouldn't have categories
	is_data: bool,
}

impl AppInfoCategories {
	pub fn from_bytes(hdr: &DatabaseHeader, rdr: &mut Cursor<&[u8]>) -> Result<Self, io::Error> {
		// Do a quick check by type/creator codes for whether we should actually have categories
		if &hdr.type_code[..] != b"DATA" {
			return Ok(Default::default());
		}

		let mut categories = Vec::new();
		let renamed_flags = rdr.read_u16::<BigEndian>()?;
		for category_id in 0..16 {
			let name = {
				let mut buf = [0u8; 16];
				rdr.read_exact(&mut buf)?;
				buf
			};

			if name == [0u8; 16] {
				continue;
			}

			categories.push(ExtraInfoCategory {
				category_id,
				name,
				renamed: (renamed_flags & (1 << category_id)) != 0,
			});
		}

		let mut category_unique_ids = [0_u8; 16];
		for idx in 0..16 {
			category_unique_ids[idx] = rdr.read_u8()?;
		}

		let last_unique_id = rdr.read_u8()?;
		let rsvd = rdr.read_u8()?;

		Ok(Self {
			renamed_categories: renamed_flags,
			categories,
			category_unique_ids,
			last_unique_id,
			rsvd,
			is_data: true,
		})
	}
}

impl ExtraInfoRecord for AppInfoCategories {
	const SIZE: usize = 2 + 16 * 16 + 16 + 1 + 1;

	fn from_bytes(hdr: &DatabaseHeader, data: &mut Cursor<&[u8]>) -> Result<Self, io::Error> {
		Self::from_bytes(hdr, data)
	}

	fn to_bytes(&self) -> Result<Vec<u8>, io::Error> {
		if !self.is_data {
			// this is not a data-type record and should not have categories
			return Ok(vec![]);
		}

		let mut cursor = Cursor::new(Vec::new());

		cursor.write_u16::<BigEndian>(self.renamed_categories)?;

		for cat in self.categories.iter() {
			cursor.write(&cat.name)?;
		}
		for _ in self.categories.len()..16 {
			cursor.write(&[0_u8; 16])?;
		}
		cursor.write(&self.category_unique_ids)?;
		cursor.write_u8(self.last_unique_id)?;
		cursor.write_u8(self.rsvd)?;

		Ok(cursor.into_inner())
	}

	fn data_empty(&self) -> bool {
		false
	}

	fn data_item_categories(&self) -> Option<Vec<ExtraInfoCategory>> {
		Some(self.categories.clone())
	}
}