blob: 09244181357e5a86266ffe3a32ba62e332262af6 [file] [log] [blame]
Serge Bazanski81981362021-03-06 13:08:00 +01001/*
2 * Platform Management FRU Information Storage Definition v1.0
3 * Document Revision 1.3, March 24, 2015
4 *
5 * From: https://www.intel.com/content/www/us/en/servers/ipmi/ipmi-platform-mgt-fru-infostorage-def-v1-0-rev-1-3-spec-update.html
6 */
7export class FRUParser {
8 constructor(data) {
9 this.data = data
10 }
11
12 parseCommon(data) {
13 // 8. Common Header Format
14 let version = data[0];
15 if ((version >> 4) !== 0) throw new Error("Invalid Common Header version");
16 if ((version & 0b1111) !== 1) throw new Error("Invalid Common Header version");
17
18 let res = {};
19 res.version = version;
20 res.internalUseStart = data[1] * 8;
21 res.chassisInfoStart = data[2] * 8;
22 res.boardInfoStart = data[3] * 8;
23 res.productInfoStart = data[4] * 8;
24 res.multiRecordInfoStart = data[5] * 8;
25
26 let sum = data.reduce((a, b) => a + b, 0) & 0xff;
27 if (sum !== 0) throw new Error("Common area checksum error");
28
29 return res;
30 }
31
32 parseBoardInfo(data) {
33 // 11. Board Info Area Format
34 let res = {};
35
36 let version = data[0];
37 if ((version >> 4) !== 0) throw new Error("Invalid Board Info version");
38 if ((version & 0b1111) !== 1) throw new Error("Invalid Board Info version");
39 res.version = version;
40
41 let areaLength = data[1] * 8;
42 if (areaLength > data.length) throw new Error("Invalid Board Info length");
43 data = data.slice(0, areaLength);
44
45 let sum = data.reduce((a, b) => a + b, 0) & 0xff;
46 if (sum !== 0) throw new Error("Board Info Area checksum error");
47
48 let r = new Reader(data);
49 r.skip(2);
50
51 res.language = r.readLanguageCode();
52 res.manufacturingDate = r.readDateTime();
53 res.manufacturerName = r.readTypeLength(res.language);
54 res.productName = r.readTypeLength(res.language);
55 res.serialNumber = r.readTypeLength(res.language);
56 res.partNumber = r.readTypeLength(res.language);
57 res.fruFileID = r.readTypeLength(res.language);
58 res.custom = r.readTypeLength(res.language);
59 // Not sure if this is up to standard - the standard seems to say that
60 // C1 must always appear, but the Dell storage cards I've looked at
61 // skip it. There's an earlier C1, but that's part of the FRU File ID.
62 if (res.length > 0) {
63 if (r.readByte() !== 0xc1) throw new Error("Custom area must end with C1");
64 }
65 return res;
66 }
67
68 parseInternalUseDell(data) {
69 let version = data[0];
70 if ((version >> 4) !== 0) throw new Error("Invalid Internal Use version");
71 if ((version & 0b1111) !== 1) throw new Error("Invalid Internal Use version");
72
73 if ((new TextDecoder().decode(data.slice(1,5))) !== "DELL") {
74 throw new Error("Invalid 'DELL' magic in internal area");
75 }
76
77 let sum = data.reduce((a, b) => a + b, 0) & 0xff;
78 if (sum !== 0) throw new Error("Dell Internal Area checksum error");
79
80 return {};
81 }
82
83 parse() {
84 this.common = this.parseCommon(this.data.slice(0, 8))
85
86 if (this.common.boardInfoStart !== 0) {
87 let data = this.data.slice(this.common.boardInfoStart, this.data.length);
88 this.boardInfo = this.parseBoardInfo(data);
89 } else {
90 this.boardInfo = {};
91 }
92 this.internalUse = {};
93 if (this.common.internalUseStart !== 0) {
94 let data = this.data.slice(this.common.internalUseStart, this.data.length);
95 this.internalUse.dell = this.parseInternalUseDell(data);
96 }
97 }
98
99 stringify() {
100 let res = [];
101 res.push(`Version: ${this.common.version}`)
102 res.push(`Board Info:`)
103 let bi = this.boardInfo;
104 res.push(` Language: ${bi.language}`)
105 if (bi.manufacturingDate !== undefined)
106 res.push(` Manufacturing Date: ${bi.manufacturingDate}`);
107 res.push(` Manufacturer Name: ${bi.manufacturerName}`)
108 res.push(` Product Name: ${bi.productName}`)
109 res.push(` Serial Number: ${bi.serialNumber}`)
110 res.push(` PartNumber: ${bi.partNumber}`)
111 res.push(` FRU File ID: ${bi.fruFileID}`)
112
113 if (this.internalUse.dell !== undefined) {
114 res.push("Internal Use: DELL-specific")
115 }
116 return res.join("\n");
117 }
118}
119
120class Reader {
121 constructor(data) {
122 this.data = data;
123 }
124 skip(n) {
125 this.data = this.data.slice(n);
126 }
127 readByte() {
128 let num = this.data[0];
129 this.data = this.data.slice(1);
130 return num;
131 }
132 readLanguageCode() {
133 let num = this.readByte();
134 let encoding = num >> 6;
135 let language = [
136 "en", "aa", "ab", "af", "am", "ar", "as", "ay", "az", "ba", "be",
137 "bg", "bh", "bi", "bn", "bo", "br", "ca", "co", "cs", "cy", "da",
138 "de", "dz", "el", "en", "eo", "es", "et", "eu", "fa", "fi", "fj",
139 "fo", "fr", "fy", "ga", "gd", "gl", "gn", "gu", "ha", "hi", "hr",
140 "hu", "hy", "ia", "ie", "ik", "in", "is", "it", "iw", "ja", "ji",
141 "jw", "ka", "kk", "kl", "km", "kn", "ko", "ks", "ku", "ky", "la",
142 "ln", "lo", "lt", "lv", "mg", "mi", "mk", "ml", "mn", "mo", "mr",
143 "ms", "mt", "my", "na", "ne", "nl", "no", "oc", "om", "or", "pa",
144 "pl", "ps", "pt", "qu", "rm", "rn", "ro", "ru", "rw", "sa", "sd",
145 "sg", "sh", "di", "sk", "dl", "sm", "sn", "so", "sq", "sr", "ss",
146 "st", "su", "sv", "sw", "ta", "te", "tg", "th", "ti", "tk", "tl",
147 "tn", "to", "tr", "ts", "tt", "tw", "uk", "ur", "uz", "vi", "vo",
148 "wo", "xh", "yo", "zh", "zu",
149 ][num & 0b111111];
150 return language;
151 }
152 readTypeLength(language) {
153 let tag = this.readByte();
154 let type = tag >> 6;
155 let len = tag & 0b111111;
156 switch (type) {
157 case 0:
158 return this.readTLBinary(len);
159 case 1:
160 return this.readTLBCDPlus(len);
161 case 2:
162 return this.readTL6BASCII(len);
163 case 3:
164 return this.readTLString(len, language);
165 }
166 }
167 readTLBinary(len) {
168 let data = this.data.slice(0, len);
169 this.data = this.data.slice(len);
170 return data;
171 }
172 readTLBCDPlus(len) {
173 let data = this.data.slice(0, len);
174 this.data = this.data.slice(len);
175 const lookup = "012345689 -.???";
176 let res = [];
177 for (const c of data) {
178 let upper = lookup[c >> 4];
179 let lower = lookup[c & 0b1111];
180 if ((upper === "?") || (lower === "?")) {
181 throw new Error("Invalid BCD Plus data");
182 }
183 res.push(upper);
184 res.push(lower);
185 }
186 return res.join("");
187 }
188 readTL6BASCII(len) {
189 let data = this.data.slice(0, len);
190 this.data = this.data.slice(len);
191 const lookup =
192 " !\"#$%&'()*+,-./" +
193 "0123456789:;<=>?" +
194 "@ABCDEFGHIJKLMNO" +
195 "PQRSTUVWXYZ[\\]^_";
196
197 let res = [];
198 let availbits = 0;
199 let bits = 0;
200 while ((data.length > 0) || (availbits >= 6)) {
201 if (availbits < 6) {
202 bits |= (data[0] << availbits);
203 availbits += 8;
204 data = data.slice(1);
205 }
206 let n = bits & 0b111111;
207 availbits -= 6;
208 bits >>= 6;
209 res.push(lookup[n]);
210 }
211 return res.join("");
212 }
213 readTLString(len, language) {
214 let data = this.data.slice(0, len);
215 this.data = this.data.slice(len);
216 // 13. Type/Length Byte Format
217 // Yikes, Intel.
218 if (language !== "en") {
219 throw new Error("Unicode unimplemented");
220 }
221 // This should be 'ASCII + Latin 1', but this is a good enough approximation.
222 return new TextDecoder().decode(data);
223 }
224 readDateTime() {
225 let minutes = this.readByte() | (this.readByte() << 8) | (this.readByte() << 16);
226 if (minutes !== 0) throw new Error("Datetime parsing not implemented");
227 return undefined;
228 }
229}
230
231export class HBJ11FRUAssembler {
232 constructor(serial) {
233 this.serial = serial;
234 }
235
236 assemble() {
237 // Strings can be longer in FRU spec, but let's keep it conservative.
238 if (this.serial.length > 8) {
239 throw new Error("Serial too long");
240 }
241 // Same layout as DELL FRUs, board specific after common, internal use after board specific.
242 let common = [0x01, 0x0a, 0x00, 0x01, 0x00, 0x00, 0x00, 0xf4];
243
244 // Similar layout to DELL FRUs, 72 bytes.
245 let board = [
246 0x01, // Version 1
247 0x09, // Length (9*8 == 72 bytes)
248 0x00, 0x00, 0x00, 0x00, // Manufacturing time (unspecified)
Serge Bazanskief3d7b82021-03-25 19:54:23 +0100249 // Manufacturer: HELL (we can't use anything longer, as the product
250 // name below needs to be in this exact byte offset in the ROM for
251 // the iDRAC to display the name correctly).
252 0x83, 0x68, 0xc9, 0xb2,
253
254 // Product name: bgpwtf SATA Repeater
255 0xDE, 98, 103, 112, 119, 116, 102, 32, 83, 65, 84, 65, 32, 82, 101, 112, 101, 97, 116, 101, 114,
256 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, // ... pad above to 30 chars.
Serge Bazanski81981362021-03-06 13:08:00 +0100257 ];
258 // Serial number tag/length.
259 board.push(0xC0 | (this.serial.length));
260 // Serial number.
261 for (const c of this.serial) {
262 board.push(c.charCodeAt());
263 }
264 board = board.concat([
265 0xC7, 72, 66, 74, 49, 49, 65, 48, // Part number: HBJ11A0
266 0xC1, 0x02, 0xC1, 0x00, // FRU File ID 2, one-byte custom area/end? Weird shit.
267 ]);
268 if (board.length > 71) {
269 throw new Error("Board Area too long!");
270 }
271 // Pad with zeroes.
272 board = board.concat(Array(71 - board.length).fill(0));
273 // Calculate checksum.
274 let sum = (0xff ^ (board.reduce((a, b) => a + b, 0) & 0xff));
275 board.push((sum + 1) & 0xff);
276
277 // Dell internal use.
278 let dell = [
279 0x01, 0x44, 0x45, 0x4c, 0x4c, 0xf7, 0x00, 0x00, 0x01, 0x02, 0x00, 0x00, 0x01, 0x13, 0x58, 0x01,
280 0x0f, 0x68, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0x0d, 0x00, 0xe0, 0x01, 0x00, 0x00, 0x01,
281 0x00, 0x01, 0x01, 0x00,
282 ];
283
284 let eeprom = common.concat(board).concat(dell);
285 // Pad to 256 bytes.
286 eeprom = eeprom.concat(Array(256 - eeprom.length).fill(0));
287 return new Uint8Array(eeprom);
288 }
289}