|
1 /* |
|
2 * (C) Copyright David Gibson <dwg@au1.ibm.com>, IBM Corporation. 2007. |
|
3 * |
|
4 * |
|
5 * This program is free software; you can redistribute it and/or |
|
6 * modify it under the terms of the GNU General Public License as |
|
7 * published by the Free Software Foundation; either version 2 of the |
|
8 * License, or (at your option) any later version. |
|
9 * |
|
10 * This program is distributed in the hope that it will be useful, |
|
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
|
13 * General Public License for more details. |
|
14 * |
|
15 * You should have received a copy of the GNU General Public License |
|
16 * along with this program; if not, write to the Free Software |
|
17 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 |
|
18 * USA |
|
19 */ |
|
20 |
|
21 #include "dtc.h" |
|
22 |
|
23 #ifdef TRACE_CHECKS |
|
24 #define TRACE(c, ...) \ |
|
25 do { \ |
|
26 fprintf(stderr, "=== %s: ", (c)->name); \ |
|
27 fprintf(stderr, __VA_ARGS__); \ |
|
28 fprintf(stderr, "\n"); \ |
|
29 } while (0) |
|
30 #else |
|
31 #define TRACE(c, fmt, ...) do { } while (0) |
|
32 #endif |
|
33 |
|
34 enum checklevel { |
|
35 IGNORE = 0, |
|
36 WARN = 1, |
|
37 ERROR = 2, |
|
38 }; |
|
39 |
|
40 enum checkstatus { |
|
41 UNCHECKED = 0, |
|
42 PREREQ, |
|
43 PASSED, |
|
44 FAILED, |
|
45 }; |
|
46 |
|
47 struct check; |
|
48 |
|
49 typedef void (*tree_check_fn)(struct check *c, struct node *dt); |
|
50 typedef void (*node_check_fn)(struct check *c, struct node *dt, struct node *node); |
|
51 typedef void (*prop_check_fn)(struct check *c, struct node *dt, |
|
52 struct node *node, struct property *prop); |
|
53 |
|
54 struct check { |
|
55 const char *name; |
|
56 tree_check_fn tree_fn; |
|
57 node_check_fn node_fn; |
|
58 prop_check_fn prop_fn; |
|
59 void *data; |
|
60 enum checklevel level; |
|
61 enum checkstatus status; |
|
62 int inprogress; |
|
63 int num_prereqs; |
|
64 struct check **prereq; |
|
65 }; |
|
66 |
|
67 #define CHECK(nm, tfn, nfn, pfn, d, lvl, ...) \ |
|
68 static struct check *nm##_prereqs[] = { __VA_ARGS__ }; \ |
|
69 static struct check nm = { \ |
|
70 .name = #nm, \ |
|
71 .tree_fn = (tfn), \ |
|
72 .node_fn = (nfn), \ |
|
73 .prop_fn = (pfn), \ |
|
74 .data = (d), \ |
|
75 .level = (lvl), \ |
|
76 .status = UNCHECKED, \ |
|
77 .num_prereqs = ARRAY_SIZE(nm##_prereqs), \ |
|
78 .prereq = nm##_prereqs, \ |
|
79 }; |
|
80 |
|
81 #define TREE_CHECK(nm, d, lvl, ...) \ |
|
82 CHECK(nm, check_##nm, NULL, NULL, d, lvl, __VA_ARGS__) |
|
83 #define NODE_CHECK(nm, d, lvl, ...) \ |
|
84 CHECK(nm, NULL, check_##nm, NULL, d, lvl, __VA_ARGS__) |
|
85 #define PROP_CHECK(nm, d, lvl, ...) \ |
|
86 CHECK(nm, NULL, NULL, check_##nm, d, lvl, __VA_ARGS__) |
|
87 #define BATCH_CHECK(nm, lvl, ...) \ |
|
88 CHECK(nm, NULL, NULL, NULL, NULL, lvl, __VA_ARGS__) |
|
89 |
|
90 #ifdef __GNUC__ |
|
91 static inline void check_msg(struct check *c, const char *fmt, ...) __attribute__((format (printf, 2, 3))); |
|
92 #endif |
|
93 static inline void check_msg(struct check *c, const char *fmt, ...) |
|
94 { |
|
95 va_list ap; |
|
96 va_start(ap, fmt); |
|
97 |
|
98 if ((c->level < WARN) || (c->level <= quiet)) |
|
99 return; /* Suppress message */ |
|
100 |
|
101 fprintf(stderr, "%s (%s): ", |
|
102 (c->level == ERROR) ? "ERROR" : "Warning", c->name); |
|
103 vfprintf(stderr, fmt, ap); |
|
104 fprintf(stderr, "\n"); |
|
105 } |
|
106 |
|
107 #define FAIL(c, ...) \ |
|
108 do { \ |
|
109 TRACE((c), "\t\tFAILED at %s:%d", __FILE__, __LINE__); \ |
|
110 (c)->status = FAILED; \ |
|
111 check_msg((c), __VA_ARGS__); \ |
|
112 } while (0) |
|
113 |
|
114 static void check_nodes_props(struct check *c, struct node *dt, struct node *node) |
|
115 { |
|
116 struct node *child; |
|
117 struct property *prop; |
|
118 |
|
119 TRACE(c, "%s", node->fullpath); |
|
120 if (c->node_fn) |
|
121 c->node_fn(c, dt, node); |
|
122 |
|
123 if (c->prop_fn) |
|
124 for_each_property(node, prop) { |
|
125 TRACE(c, "%s\t'%s'", node->fullpath, prop->name); |
|
126 c->prop_fn(c, dt, node, prop); |
|
127 } |
|
128 |
|
129 for_each_child(node, child) |
|
130 check_nodes_props(c, dt, child); |
|
131 } |
|
132 |
|
133 static int run_check(struct check *c, struct node *dt) |
|
134 { |
|
135 int error = 0; |
|
136 int i; |
|
137 |
|
138 assert(!c->inprogress); |
|
139 |
|
140 if (c->status != UNCHECKED) |
|
141 goto out; |
|
142 |
|
143 c->inprogress = 1; |
|
144 |
|
145 for (i = 0; i < c->num_prereqs; i++) { |
|
146 struct check *prq = c->prereq[i]; |
|
147 error |= run_check(prq, dt); |
|
148 if (prq->status != PASSED) { |
|
149 c->status = PREREQ; |
|
150 check_msg(c, "Failed prerequisite '%s'", |
|
151 c->prereq[i]->name); |
|
152 } |
|
153 } |
|
154 |
|
155 if (c->status != UNCHECKED) |
|
156 goto out; |
|
157 |
|
158 if (c->node_fn || c->prop_fn) |
|
159 check_nodes_props(c, dt, dt); |
|
160 |
|
161 if (c->tree_fn) |
|
162 c->tree_fn(c, dt); |
|
163 if (c->status == UNCHECKED) |
|
164 c->status = PASSED; |
|
165 |
|
166 TRACE(c, "\tCompleted, status %d", c->status); |
|
167 |
|
168 out: |
|
169 c->inprogress = 0; |
|
170 if ((c->status != PASSED) && (c->level == ERROR)) |
|
171 error = 1; |
|
172 return error; |
|
173 } |
|
174 |
|
175 /* |
|
176 * Utility check functions |
|
177 */ |
|
178 |
|
179 static void check_is_string(struct check *c, struct node *root, |
|
180 struct node *node) |
|
181 { |
|
182 struct property *prop; |
|
183 char *propname = c->data; |
|
184 |
|
185 prop = get_property(node, propname); |
|
186 if (!prop) |
|
187 return; /* Not present, assumed ok */ |
|
188 |
|
189 if (!data_is_one_string(prop->val)) |
|
190 FAIL(c, "\"%s\" property in %s is not a string", |
|
191 propname, node->fullpath); |
|
192 } |
|
193 #define CHECK_IS_STRING(nm, propname, lvl) \ |
|
194 CHECK(nm, NULL, check_is_string, NULL, (propname), (lvl)) |
|
195 |
|
196 static void check_is_cell(struct check *c, struct node *root, |
|
197 struct node *node) |
|
198 { |
|
199 struct property *prop; |
|
200 char *propname = c->data; |
|
201 |
|
202 prop = get_property(node, propname); |
|
203 if (!prop) |
|
204 return; /* Not present, assumed ok */ |
|
205 |
|
206 if (prop->val.len != sizeof(cell_t)) |
|
207 FAIL(c, "\"%s\" property in %s is not a single cell", |
|
208 propname, node->fullpath); |
|
209 } |
|
210 #define CHECK_IS_CELL(nm, propname, lvl) \ |
|
211 CHECK(nm, NULL, check_is_cell, NULL, (propname), (lvl)) |
|
212 |
|
213 /* |
|
214 * Structural check functions |
|
215 */ |
|
216 |
|
217 static void check_duplicate_node_names(struct check *c, struct node *dt, |
|
218 struct node *node) |
|
219 { |
|
220 struct node *child, *child2; |
|
221 |
|
222 for_each_child(node, child) |
|
223 for (child2 = child->next_sibling; |
|
224 child2; |
|
225 child2 = child2->next_sibling) |
|
226 if (streq(child->name, child2->name)) |
|
227 FAIL(c, "Duplicate node name %s", |
|
228 child->fullpath); |
|
229 } |
|
230 NODE_CHECK(duplicate_node_names, NULL, ERROR); |
|
231 |
|
232 static void check_duplicate_property_names(struct check *c, struct node *dt, |
|
233 struct node *node) |
|
234 { |
|
235 struct property *prop, *prop2; |
|
236 |
|
237 for_each_property(node, prop) |
|
238 for (prop2 = prop->next; prop2; prop2 = prop2->next) |
|
239 if (streq(prop->name, prop2->name)) |
|
240 FAIL(c, "Duplicate property name %s in %s", |
|
241 prop->name, node->fullpath); |
|
242 } |
|
243 NODE_CHECK(duplicate_property_names, NULL, ERROR); |
|
244 |
|
245 #define LOWERCASE "abcdefghijklmnopqrstuvwxyz" |
|
246 #define UPPERCASE "ABCDEFGHIJKLMNOPQRSTUVWXYZ" |
|
247 #define DIGITS "0123456789" |
|
248 #define PROPNODECHARS LOWERCASE UPPERCASE DIGITS ",._+*#?-" |
|
249 |
|
250 static void check_node_name_chars(struct check *c, struct node *dt, |
|
251 struct node *node) |
|
252 { |
|
253 int n = strspn(node->name, c->data); |
|
254 |
|
255 if (n < strlen(node->name)) |
|
256 FAIL(c, "Bad character '%c' in node %s", |
|
257 node->name[n], node->fullpath); |
|
258 } |
|
259 NODE_CHECK(node_name_chars, PROPNODECHARS "@", ERROR); |
|
260 |
|
261 static void check_node_name_format(struct check *c, struct node *dt, |
|
262 struct node *node) |
|
263 { |
|
264 if (strchr(get_unitname(node), '@')) |
|
265 FAIL(c, "Node %s has multiple '@' characters in name", |
|
266 node->fullpath); |
|
267 } |
|
268 NODE_CHECK(node_name_format, NULL, ERROR, &node_name_chars); |
|
269 |
|
270 static void check_property_name_chars(struct check *c, struct node *dt, |
|
271 struct node *node, struct property *prop) |
|
272 { |
|
273 int n = strspn(prop->name, c->data); |
|
274 |
|
275 if (n < strlen(prop->name)) |
|
276 FAIL(c, "Bad character '%c' in property name \"%s\", node %s", |
|
277 prop->name[n], prop->name, node->fullpath); |
|
278 } |
|
279 PROP_CHECK(property_name_chars, PROPNODECHARS, ERROR); |
|
280 |
|
281 static void check_explicit_phandles(struct check *c, struct node *root, |
|
282 struct node *node) |
|
283 { |
|
284 struct property *prop; |
|
285 struct node *other; |
|
286 cell_t phandle; |
|
287 |
|
288 prop = get_property(node, "linux,phandle"); |
|
289 if (! prop) |
|
290 return; /* No phandle, that's fine */ |
|
291 |
|
292 if (prop->val.len != sizeof(cell_t)) { |
|
293 FAIL(c, "%s has bad length (%d) linux,phandle property", |
|
294 node->fullpath, prop->val.len); |
|
295 return; |
|
296 } |
|
297 |
|
298 phandle = propval_cell(prop); |
|
299 if ((phandle == 0) || (phandle == -1)) { |
|
300 FAIL(c, "%s has invalid linux,phandle value 0x%x", |
|
301 node->fullpath, phandle); |
|
302 return; |
|
303 } |
|
304 |
|
305 other = get_node_by_phandle(root, phandle); |
|
306 if (other) { |
|
307 FAIL(c, "%s has duplicated phandle 0x%x (seen before at %s)", |
|
308 node->fullpath, phandle, other->fullpath); |
|
309 return; |
|
310 } |
|
311 |
|
312 node->phandle = phandle; |
|
313 } |
|
314 NODE_CHECK(explicit_phandles, NULL, ERROR); |
|
315 |
|
316 static void check_name_properties(struct check *c, struct node *root, |
|
317 struct node *node) |
|
318 { |
|
319 struct property **pp, *prop = NULL; |
|
320 |
|
321 for (pp = &node->proplist; *pp; pp = &((*pp)->next)) |
|
322 if (streq((*pp)->name, "name")) { |
|
323 prop = *pp; |
|
324 break; |
|
325 } |
|
326 |
|
327 if (!prop) |
|
328 return; /* No name property, that's fine */ |
|
329 |
|
330 if ((prop->val.len != node->basenamelen+1) |
|
331 || (memcmp(prop->val.val, node->name, node->basenamelen) != 0)) { |
|
332 FAIL(c, "\"name\" property in %s is incorrect (\"%s\" instead" |
|
333 " of base node name)", node->fullpath, prop->val.val); |
|
334 } else { |
|
335 /* The name property is correct, and therefore redundant. |
|
336 * Delete it */ |
|
337 *pp = prop->next; |
|
338 free(prop->name); |
|
339 data_free(prop->val); |
|
340 free(prop); |
|
341 } |
|
342 } |
|
343 CHECK_IS_STRING(name_is_string, "name", ERROR); |
|
344 NODE_CHECK(name_properties, NULL, ERROR, &name_is_string); |
|
345 |
|
346 /* |
|
347 * Reference fixup functions |
|
348 */ |
|
349 |
|
350 static void fixup_phandle_references(struct check *c, struct node *dt, |
|
351 struct node *node, struct property *prop) |
|
352 { |
|
353 struct marker *m = prop->val.markers; |
|
354 struct node *refnode; |
|
355 cell_t phandle; |
|
356 |
|
357 for_each_marker_of_type(m, REF_PHANDLE) { |
|
358 assert(m->offset + sizeof(cell_t) <= prop->val.len); |
|
359 |
|
360 refnode = get_node_by_ref(dt, m->ref); |
|
361 if (! refnode) { |
|
362 FAIL(c, "Reference to non-existent node or label \"%s\"\n", |
|
363 m->ref); |
|
364 continue; |
|
365 } |
|
366 |
|
367 phandle = get_node_phandle(dt, refnode); |
|
368 *((cell_t *)(prop->val.val + m->offset)) = cpu_to_fdt32(phandle); |
|
369 } |
|
370 } |
|
371 CHECK(phandle_references, NULL, NULL, fixup_phandle_references, NULL, ERROR, |
|
372 &duplicate_node_names, &explicit_phandles); |
|
373 |
|
374 static void fixup_path_references(struct check *c, struct node *dt, |
|
375 struct node *node, struct property *prop) |
|
376 { |
|
377 struct marker *m = prop->val.markers; |
|
378 struct node *refnode; |
|
379 char *path; |
|
380 |
|
381 for_each_marker_of_type(m, REF_PATH) { |
|
382 assert(m->offset <= prop->val.len); |
|
383 |
|
384 refnode = get_node_by_ref(dt, m->ref); |
|
385 if (!refnode) { |
|
386 FAIL(c, "Reference to non-existent node or label \"%s\"\n", |
|
387 m->ref); |
|
388 continue; |
|
389 } |
|
390 |
|
391 path = refnode->fullpath; |
|
392 prop->val = data_insert_at_marker(prop->val, m, path, |
|
393 strlen(path) + 1); |
|
394 } |
|
395 } |
|
396 CHECK(path_references, NULL, NULL, fixup_path_references, NULL, ERROR, |
|
397 &duplicate_node_names); |
|
398 |
|
399 /* |
|
400 * Semantic checks |
|
401 */ |
|
402 CHECK_IS_CELL(address_cells_is_cell, "#address-cells", WARN); |
|
403 CHECK_IS_CELL(size_cells_is_cell, "#size-cells", WARN); |
|
404 CHECK_IS_CELL(interrupt_cells_is_cell, "#interrupt-cells", WARN); |
|
405 |
|
406 CHECK_IS_STRING(device_type_is_string, "device_type", WARN); |
|
407 CHECK_IS_STRING(model_is_string, "model", WARN); |
|
408 CHECK_IS_STRING(status_is_string, "status", WARN); |
|
409 |
|
410 static void fixup_addr_size_cells(struct check *c, struct node *dt, |
|
411 struct node *node) |
|
412 { |
|
413 struct property *prop; |
|
414 |
|
415 node->addr_cells = -1; |
|
416 node->size_cells = -1; |
|
417 |
|
418 prop = get_property(node, "#address-cells"); |
|
419 if (prop) |
|
420 node->addr_cells = propval_cell(prop); |
|
421 |
|
422 prop = get_property(node, "#size-cells"); |
|
423 if (prop) |
|
424 node->size_cells = propval_cell(prop); |
|
425 } |
|
426 CHECK(addr_size_cells, NULL, fixup_addr_size_cells, NULL, NULL, WARN, |
|
427 &address_cells_is_cell, &size_cells_is_cell); |
|
428 |
|
429 #define node_addr_cells(n) \ |
|
430 (((n)->addr_cells == -1) ? 2 : (n)->addr_cells) |
|
431 #define node_size_cells(n) \ |
|
432 (((n)->size_cells == -1) ? 1 : (n)->size_cells) |
|
433 |
|
434 static void check_reg_format(struct check *c, struct node *dt, |
|
435 struct node *node) |
|
436 { |
|
437 struct property *prop; |
|
438 int addr_cells, size_cells, entrylen; |
|
439 |
|
440 prop = get_property(node, "reg"); |
|
441 if (!prop) |
|
442 return; /* No "reg", that's fine */ |
|
443 |
|
444 if (!node->parent) { |
|
445 FAIL(c, "Root node has a \"reg\" property"); |
|
446 return; |
|
447 } |
|
448 |
|
449 if (prop->val.len == 0) |
|
450 FAIL(c, "\"reg\" property in %s is empty", node->fullpath); |
|
451 |
|
452 addr_cells = node_addr_cells(node->parent); |
|
453 size_cells = node_size_cells(node->parent); |
|
454 entrylen = (addr_cells + size_cells) * sizeof(cell_t); |
|
455 |
|
456 if ((prop->val.len % entrylen) != 0) |
|
457 FAIL(c, "\"reg\" property in %s has invalid length (%d bytes) " |
|
458 "(#address-cells == %d, #size-cells == %d)", |
|
459 node->fullpath, prop->val.len, addr_cells, size_cells); |
|
460 } |
|
461 NODE_CHECK(reg_format, NULL, WARN, &addr_size_cells); |
|
462 |
|
463 static void check_ranges_format(struct check *c, struct node *dt, |
|
464 struct node *node) |
|
465 { |
|
466 struct property *prop; |
|
467 int c_addr_cells, p_addr_cells, c_size_cells, p_size_cells, entrylen; |
|
468 |
|
469 prop = get_property(node, "ranges"); |
|
470 if (!prop) |
|
471 return; |
|
472 |
|
473 if (!node->parent) { |
|
474 FAIL(c, "Root node has a \"ranges\" property"); |
|
475 return; |
|
476 } |
|
477 |
|
478 p_addr_cells = node_addr_cells(node->parent); |
|
479 p_size_cells = node_size_cells(node->parent); |
|
480 c_addr_cells = node_addr_cells(node); |
|
481 c_size_cells = node_size_cells(node); |
|
482 entrylen = (p_addr_cells + c_addr_cells + c_size_cells) * sizeof(cell_t); |
|
483 |
|
484 if (prop->val.len == 0) { |
|
485 if (p_addr_cells != c_addr_cells) |
|
486 FAIL(c, "%s has empty \"ranges\" property but its " |
|
487 "#address-cells (%d) differs from %s (%d)", |
|
488 node->fullpath, c_addr_cells, node->parent->fullpath, |
|
489 p_addr_cells); |
|
490 if (p_size_cells != c_size_cells) |
|
491 FAIL(c, "%s has empty \"ranges\" property but its " |
|
492 "#size-cells (%d) differs from %s (%d)", |
|
493 node->fullpath, c_size_cells, node->parent->fullpath, |
|
494 p_size_cells); |
|
495 } else if ((prop->val.len % entrylen) != 0) { |
|
496 FAIL(c, "\"ranges\" property in %s has invalid length (%d bytes) " |
|
497 "(parent #address-cells == %d, child #address-cells == %d, " |
|
498 "#size-cells == %d)", node->fullpath, prop->val.len, |
|
499 p_addr_cells, c_addr_cells, c_size_cells); |
|
500 } |
|
501 } |
|
502 NODE_CHECK(ranges_format, NULL, WARN, &addr_size_cells); |
|
503 |
|
504 /* |
|
505 * Style checks |
|
506 */ |
|
507 static void check_avoid_default_addr_size(struct check *c, struct node *dt, |
|
508 struct node *node) |
|
509 { |
|
510 struct property *reg, *ranges; |
|
511 |
|
512 if (!node->parent) |
|
513 return; /* Ignore root node */ |
|
514 |
|
515 reg = get_property(node, "reg"); |
|
516 ranges = get_property(node, "ranges"); |
|
517 |
|
518 if (!reg && !ranges) |
|
519 return; |
|
520 |
|
521 if ((node->parent->addr_cells == -1)) |
|
522 FAIL(c, "Relying on default #address-cells value for %s", |
|
523 node->fullpath); |
|
524 |
|
525 if ((node->parent->size_cells == -1)) |
|
526 FAIL(c, "Relying on default #size-cells value for %s", |
|
527 node->fullpath); |
|
528 } |
|
529 NODE_CHECK(avoid_default_addr_size, NULL, WARN, &addr_size_cells); |
|
530 |
|
531 static void check_obsolete_chosen_interrupt_controller(struct check *c, |
|
532 struct node *dt) |
|
533 { |
|
534 struct node *chosen; |
|
535 struct property *prop; |
|
536 |
|
537 chosen = get_node_by_path(dt, "/chosen"); |
|
538 if (!chosen) |
|
539 return; |
|
540 |
|
541 prop = get_property(chosen, "interrupt-controller"); |
|
542 if (prop) |
|
543 FAIL(c, "/chosen has obsolete \"interrupt-controller\" " |
|
544 "property"); |
|
545 } |
|
546 TREE_CHECK(obsolete_chosen_interrupt_controller, NULL, WARN); |
|
547 |
|
548 static struct check *check_table[] = { |
|
549 &duplicate_node_names, &duplicate_property_names, |
|
550 &node_name_chars, &node_name_format, &property_name_chars, |
|
551 &name_is_string, &name_properties, |
|
552 &explicit_phandles, |
|
553 &phandle_references, &path_references, |
|
554 |
|
555 &address_cells_is_cell, &size_cells_is_cell, &interrupt_cells_is_cell, |
|
556 &device_type_is_string, &model_is_string, &status_is_string, |
|
557 |
|
558 &addr_size_cells, ®_format, &ranges_format, |
|
559 |
|
560 &avoid_default_addr_size, |
|
561 &obsolete_chosen_interrupt_controller, |
|
562 }; |
|
563 |
|
564 void process_checks(int force, struct boot_info *bi) |
|
565 { |
|
566 struct node *dt = bi->dt; |
|
567 int i; |
|
568 int error = 0; |
|
569 |
|
570 for (i = 0; i < ARRAY_SIZE(check_table); i++) { |
|
571 struct check *c = check_table[i]; |
|
572 |
|
573 if (c->level != IGNORE) |
|
574 error = error || run_check(c, dt); |
|
575 } |
|
576 |
|
577 if (error) { |
|
578 if (!force) { |
|
579 fprintf(stderr, "ERROR: Input tree has errors, aborting " |
|
580 "(use -f to force output)\n"); |
|
581 exit(2); |
|
582 } else if (quiet < 3) { |
|
583 fprintf(stderr, "Warning: Input tree has errors, " |
|
584 "output forced\n"); |
|
585 } |
|
586 } |
|
587 } |