1 /* ----------------------------------------------------------------------- *
2 *
3 * Copyright 2003-2009 H. Peter Anvin - All Rights Reserved
4 * Copyright 2009-2010 Intel Corporation; author: H. Peter Anvin
5 * Copyright 2010 Shao Miller
6 * Copyright 2010-2012 Michal Soltys
7 *
8 * Permission is hereby granted, free of charge, to any person
9 * obtaining a copy of this software and associated documentation
10 * files (the "Software"), to deal in the Software without
11 * restriction, including without limitation the rights to use,
12 * copy, modify, merge, publish, distribute, sublicense, and/or
13 * sell copies of the Software, and to permit persons to whom
14 * the Software is furnished to do so, subject to the following
15 * conditions:
16 *
17 * The above copyright notice and this permission notice shall
18 * be included in all copies or substantial portions of the Software.
19 *
20 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
21 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
22 * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
23 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
24 * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
25 * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
26 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
27 * OTHER DEALINGS IN THE SOFTWARE.
28 *
29 * ----------------------------------------------------------------------- */
30
31 #include <com32.h>
32 #include <stdlib.h>
33 #include <stdio.h>
34 #include <string.h>
35 #include <stdint.h>
36 #include <dprintf.h>
37 #include <syslinux/config.h>
38 #include "chain.h"
39 #include "options.h"
40 #include "utility.h"
41 #include "partiter.h"
42 #include "mangle.h"
43
44 static const char cmldr_signature[8] = "cmdcons";
45
46 /* Create boot info table: needed when you want to chainload
47 * another version of ISOLINUX (or another bootlaoder that needs
48 * the -boot-info-table switch of mkisofs)
49 * (will only work when run from ISOLINUX)
50 */
manglef_isolinux(struct data_area * data)51 int manglef_isolinux(struct data_area *data)
52 {
53 const union syslinux_derivative_info *sdi;
54 unsigned char *isolinux_bin;
55 uint32_t *checksum, *chkhead, *chktail;
56 uint32_t file_lba = 0;
57
58 if (!(opt.file && opt.isolinux))
59 return 0;
60
61 sdi = syslinux_derivative_info();
62
63 if (sdi->c.filesystem != SYSLINUX_FS_ISOLINUX) {
64 error("The isolinux= option is only valid when run from ISOLINUX.");
65 goto bail;
66 }
67
68 /* Boot info table info (integers in little endian format)
69
70 Offset Name Size Meaning
71 8 bi_pvd 4 bytes LBA of primary volume descriptor
72 12 bi_file 4 bytes LBA of boot file
73 16 bi_length 4 bytes Boot file length in bytes
74 20 bi_csum 4 bytes 32-bit checksum
75 24 bi_reserved 40 bytes Reserved
76
77 The 32-bit checksum is the sum of all the 32-bit words in the
78 boot file starting at byte offset 64. All linear block
79 addresses (LBAs) are given in CD sectors (normally 2048 bytes).
80
81 LBA of primary volume descriptor should already be set to 16.
82 */
83
84 isolinux_bin = (unsigned char *)data->data;
85
86 /* Get LBA address of bootfile */
87 file_lba = get_file_lba(opt.file);
88
89 if (file_lba == 0) {
90 error("Failed to find LBA offset of the boot file.");
91 goto bail;
92 }
93 /* Set it */
94 *((uint32_t *) & isolinux_bin[12]) = file_lba;
95
96 /* Set boot file length */
97 *((uint32_t *) & isolinux_bin[16]) = data->size;
98
99 /* Calculate checksum */
100 checksum = (uint32_t *) & isolinux_bin[20];
101 chkhead = (uint32_t *) & isolinux_bin[64];
102 chktail = (uint32_t *) & isolinux_bin[data->size & ~3u];
103 *checksum = 0;
104 while (chkhead < chktail)
105 *checksum += *chkhead++;
106
107 /*
108 * Deal with possible fractional dword at the end;
109 * this *should* never happen...
110 */
111 if (data->size & 3) {
112 uint32_t xword = 0;
113 memcpy(&xword, chkhead, data->size & 3);
114 *checksum += xword;
115 }
116 return 0;
117 bail:
118 return -1;
119 }
120
121 /*
122 * Legacy grub's stage2 chainloading
123 */
manglef_grub(const struct part_iter * iter,struct data_area * data)124 int manglef_grub(const struct part_iter *iter, struct data_area *data)
125 {
126 /* Layout of stage2 file (from byte 0x0 to 0x270) */
127 struct grub_stage2_patch_area {
128 /* 0x0 to 0x205 */
129 char unknown[0x206];
130 /* 0x206: compatibility version number major */
131 uint8_t compat_version_major;
132 /* 0x207: compatibility version number minor */
133 uint8_t compat_version_minor;
134
135 /* 0x208: install_partition variable */
136 struct {
137 /* 0x208: sub-partition in sub-partition part2 */
138 uint8_t part3;
139 /* 0x209: sub-partition in top-level partition */
140 uint8_t part2;
141 /* 0x20a: top-level partiton number */
142 uint8_t part1;
143 /* 0x20b: BIOS drive number (must be 0) */
144 uint8_t drive;
145 } __attribute__ ((packed)) install_partition;
146
147 /* 0x20c: deprecated (historical reason only) */
148 uint32_t saved_entryno;
149 /* 0x210: stage2_ID: will always be STAGE2_ID_STAGE2 = 0 in stage2 */
150 uint8_t stage2_id;
151 /* 0x211: force LBA */
152 uint8_t force_lba;
153 /* 0x212: version string (will probably be 0.97) */
154 char version_string[5];
155 /* 0x217: config filename */
156 char config_file[89];
157 /* 0x270: start of code (after jump from 0x200) */
158 char codestart[1];
159 } __attribute__ ((packed)) *stage2;
160
161 if (!(opt.file && opt.grub))
162 return 0;
163
164 if (data->size < sizeof *stage2) {
165 error("The file specified by grub=<loader> is too small to be stage2 of GRUB Legacy.");
166 goto bail;
167 }
168 stage2 = data->data;
169
170 /*
171 * Check the compatibility version number to see if we loaded a real
172 * stage2 file or a stage2 file that we support.
173 */
174 if (stage2->compat_version_major != 3
175 || stage2->compat_version_minor != 2) {
176 error("The file specified by grub=<loader> is not a supported stage2 GRUB Legacy binary.");
177 goto bail;
178 }
179
180 /*
181 * GRUB Legacy wants the partition number in the install_partition
182 * variable, located at offset 0x208 of stage2.
183 * When GRUB Legacy is loaded, it is located at memory address 0x8208.
184 *
185 * It looks very similar to the "boot information format" of the
186 * Multiboot specification:
187 * http://www.gnu.org/software/grub/manual/multiboot/multiboot.html#Boot-information-format
188 *
189 * 0x208 = part3: sub-partition in sub-partition part2
190 * 0x209 = part2: sub-partition in top-level partition
191 * 0x20a = part1: top-level partition number
192 * 0x20b = drive: BIOS drive number (must be 0)
193 *
194 * GRUB Legacy doesn't store the BIOS drive number at 0x20b, but at
195 * another location.
196 *
197 * Partition numbers always start from zero.
198 * Unused partition bytes must be set to 0xFF.
199 *
200 * We only care about top-level partition, so we only need to change
201 * "part1" to the appropriate value:
202 * -1: whole drive (default) (-1 = 0xFF)
203 * 0-3: primary partitions
204 * 4-*: logical partitions
205 */
206 stage2->install_partition.part1 = iter->index - 1;
207
208 /*
209 * Grub Legacy reserves 89 bytes (from 0x8217 to 0x826f) for the
210 * config filename. The filename passed via grubcfg= will overwrite
211 * the default config filename "/boot/grub/menu.lst".
212 */
213 if (opt.grubcfg) {
214 if (strlen(opt.grubcfg) >= sizeof stage2->config_file) {
215 error("The config filename length can't exceed 88 characters.");
216 goto bail;
217 }
218
219 strcpy((char *)stage2->config_file, opt.grubcfg);
220 }
221
222 return 0;
223 bail:
224 return -1;
225 }
226 #if 0
227 /*
228 * Dell's DRMK chainloading.
229 */
230 int manglef_drmk(struct data_area *data)
231 {
232 /*
233 * DRMK entry is different than MS-DOS/PC-DOS
234 * A new size, aligned to 16 bytes to ease use of ds:[bp+28].
235 * We only really need 4 new, usable bytes at the end.
236 */
237
238 if (!(opt.file && opt.drmk))
239 return 0;
240
241 uint32_t tsize = (data->size + 19) & 0xfffffff0;
242 const union syslinux_derivative_info *sdi;
243 uint64_t fs_lba;
244
245 sdi = syslinux_derivative_info();
246 /* We should lookup the Syslinux partition offset and use it */
247 fs_lba = *sdi->disk.partoffset;
248
249 /*
250 * fs_lba should be verified against the disk as some DRMK
251 * variants will check and fail if it does not match
252 */
253 dprintf(" fs_lba offset is %d\n", fs_lba);
254 /* DRMK only uses a DWORD */
255 if (fs_lba > 0xffffffff) {
256 error("LBA very large; Only using lower 32 bits; DRMK will probably fail.");
257 }
258 opt.regs.ss = opt.regs.fs = opt.regs.gs = 0; /* Used before initialized */
259 if (!realloc(data->data, tsize)) {
260 error("Failed to realloc for DRMK.");
261 goto bail;
262 }
263 data->size = tsize;
264 /* ds:bp is assumed by DRMK to be the boot sector */
265 /* offset 28 is the FAT HiddenSectors value */
266 opt.regs.ds = (tsize >> 4) + (opt.fseg - 2);
267 /* "Patch" into tail of the new space */
268 *(uint32_t *)((char*)data->data + tsize - 4) = fs_lba;
269
270 return 0;
271 bail:
272 return -1;
273 }
274 #endif
275 /* Adjust BPB common function */
mangle_bpb(const struct part_iter * iter,struct data_area * data,const char * tag)276 static int mangle_bpb(const struct part_iter *iter, struct data_area *data, const char *tag)
277 {
278 int type = bpb_detect(data->data, tag);
279 int off = drvoff_detect(type);
280
281 /* BPB: hidden sectors 64bit - exFAT only for now */
282 if (type == bpbEXF)
283 *(uint64_t *) ((char *)data->data + 0x40) = iter->abs_lba;
284 /* BPB: hidden sectors 32bit*/
285 else if (bpbV34 <= type && type <= bpbV70) {
286 if (iter->abs_lba < ~0u)
287 *(uint32_t *) ((char *)data->data + 0x1c) = iter->abs_lba;
288 else
289 /* won't really help much, but ... */
290 *(uint32_t *) ((char *)data->data + 0x1c) = ~0u;
291 /* BPB: hidden sectors 16bit*/
292 } else if (bpbV30 <= type && type <= bpbV32) {
293 if (iter->abs_lba < 0xFFFF)
294 *(uint16_t *) ((char *)data->data + 0x1c) = iter->abs_lba;
295 else
296 /* won't really help much, but ... */
297 *(uint16_t *) ((char *)data->data + 0x1c) = (uint16_t)~0u;
298 }
299
300 /* BPB: legacy geometry */
301 if (bpbV30 <= type && type <= bpbV70) {
302 if (iter->di.cbios)
303 *(uint32_t *)((char *)data->data + 0x18) = (iter->di.head << 16) | iter->di.spt;
304 else {
305 if (iter->di.disk & 0x80)
306 *(uint32_t *)((char *)data->data + 0x18) = 0x00FF003F;
307 else
308 *(uint32_t *)((char *)data->data + 0x18) = 0x00020012;
309 }
310 }
311 /* BPB: drive */
312 if (off >= 0) {
313 *(uint8_t *)((char *)data->data + off) = (opt.swap ? iter->di.disk & 0x80 : iter->di.disk);
314 }
315
316 return 0;
317 }
318
319 /*
320 * Adjust BPB of a BPB-compatible file
321 */
manglef_bpb(const struct part_iter * iter,struct data_area * data)322 int manglef_bpb(const struct part_iter *iter, struct data_area *data)
323 {
324 if (!(opt.file && opt.filebpb))
325 return 0;
326
327 return mangle_bpb(iter, data, "file");
328 }
329
330 /*
331 * Adjust BPB of a sector
332 */
mangles_bpb(const struct part_iter * iter,struct data_area * data)333 int mangles_bpb(const struct part_iter *iter, struct data_area *data)
334 {
335 if (!(opt.sect && opt.setbpb))
336 return 0;
337
338 return mangle_bpb(iter, data, "sect");
339 }
340
341 /*
342 * This function performs full BPB patching, analogously to syslinux's
343 * native BSS.
344 */
manglesf_bss(struct data_area * sec,struct data_area * fil)345 int manglesf_bss(struct data_area *sec, struct data_area *fil)
346 {
347 int type1, type2;
348 size_t cnt = 0;
349
350 if (!(opt.sect && opt.file && opt.bss))
351 return 0;
352
353 type1 = bpb_detect(fil->data, "bss/file");
354 type2 = bpb_detect(sec->data, "bss/sect");
355
356 if (!type1 || !type2) {
357 error("Couldn't determine the BPB type for option 'bss'.");
358 goto bail;
359 }
360 if (type1 != type2) {
361 error("Option 'bss' can't be used,\n"
362 "when a sector and a file have incompatible BPBs.");
363 goto bail;
364 }
365
366 /* Copy common 2.0 data */
367 memcpy((char *)fil->data + 0x0B, (char *)sec->data + 0x0B, 0x0D);
368
369 /* Copy 3.0+ data */
370 if (type1 <= bpbV30) {
371 cnt = 0x06;
372 } else if (type1 <= bpbV32) {
373 cnt = 0x08;
374 } else if (type1 <= bpbV34) {
375 cnt = 0x0C;
376 } else if (type1 <= bpbV40) {
377 cnt = 0x2E;
378 } else if (type1 <= bpbVNT) {
379 cnt = 0x3C;
380 } else if (type1 <= bpbV70) {
381 cnt = 0x42;
382 } else if (type1 <= bpbEXF) {
383 cnt = 0x60;
384 }
385 memcpy((char *)fil->data + 0x18, (char *)sec->data + 0x18, cnt);
386
387 return 0;
388 bail:
389 return -1;
390 }
391
392 /*
393 * Save sector.
394 */
mangles_save(const struct part_iter * iter,const struct data_area * data,void * org)395 int mangles_save(const struct part_iter *iter, const struct data_area *data, void *org)
396 {
397 if (!(opt.sect && opt.save))
398 return 0;
399
400 if (memcmp(org, data->data, data->size)) {
401 if (disk_write_sectors(&iter->di, iter->abs_lba, data->data, 1)) {
402 error("Cannot write the updated sector.");
403 goto bail;
404 }
405 /* function can be called again */
406 memcpy(org, data->data, data->size);
407 }
408
409 return 0;
410 bail:
411 return -1;
412 }
413
414 /*
415 * To boot the Recovery Console of Windows NT/2K/XP we need to write
416 * the string "cmdcons\0" to memory location 0000:7C03.
417 * Memory location 0000:7C00 contains the bootsector of the partition.
418 */
mangles_cmldr(struct data_area * data)419 int mangles_cmldr(struct data_area *data)
420 {
421 if (!(opt.sect && opt.cmldr))
422 return 0;
423
424 memcpy((char *)data->data + 3, cmldr_signature, sizeof cmldr_signature);
425 return 0;
426 }
427
428 /* Set common registers */
mangler_init(const struct part_iter * iter)429 int mangler_init(const struct part_iter *iter)
430 {
431 /* Set initial registry values */
432 if (opt.file) {
433 opt.regs.cs = opt.regs.ds = opt.regs.ss = opt.fseg;
434 opt.regs.ip = opt.fip;
435 } else {
436 opt.regs.cs = opt.regs.ds = opt.regs.ss = opt.sseg;
437 opt.regs.ip = opt.sip;
438 }
439
440 if (opt.regs.ip == 0x7C00 && !opt.regs.cs)
441 opt.regs.esp.l = 0x7C00;
442
443 /* DOS kernels want the drive number in BL instead of DL. Indulge them. */
444 opt.regs.ebx.b[0] = opt.regs.edx.b[0] = iter->di.disk;
445
446 return 0;
447 }
448
449 /* ds:si & ds:bp */
mangler_handover(const struct part_iter * iter,const struct data_area * data)450 int mangler_handover(const struct part_iter *iter, const struct data_area *data)
451 {
452 if (opt.file && opt.maps && !opt.hptr) {
453 opt.regs.esi.l = opt.regs.ebp.l = opt.soff;
454 opt.regs.ds = opt.sseg;
455 opt.regs.eax.l = 0;
456 } else if (opt.hand) {
457 /* base is really 0x7be */
458 opt.regs.esi.l = opt.regs.ebp.l = data->base;
459 opt.regs.ds = 0;
460 if (iter->index && iter->type == typegpt) /* must be iterated and GPT */
461 opt.regs.eax.l = 0x54504721; /* '!GPT' */
462 else
463 opt.regs.eax.l = 0;
464 }
465
466 return 0;
467 }
468
469 /*
470 * GRLDR of GRUB4DOS wants the partition number in DH:
471 * -1: whole drive (default)
472 * 0-3: primary partitions
473 * 4-*: logical partitions
474 */
mangler_grldr(const struct part_iter * iter)475 int mangler_grldr(const struct part_iter *iter)
476 {
477 if (opt.grldr)
478 opt.regs.edx.b[1] = iter->index - 1;
479
480 return 0;
481 }
482
483 /*
484 * try to copy values from temporary iterator, if positions match
485 */
mbrcpy(struct part_iter * diter,struct part_iter * siter)486 static void mbrcpy(struct part_iter *diter, struct part_iter *siter)
487 {
488 if (diter->dos.cebr_lba == siter->dos.cebr_lba &&
489 diter->di.disk == siter->di.disk) {
490 memcpy(diter->data, siter->data, sizeof(struct disk_dos_mbr));
491 }
492 }
493
fliphide(struct part_iter * iter,struct part_iter * miter)494 static int fliphide(struct part_iter *iter, struct part_iter *miter)
495 {
496 struct disk_dos_part_entry *dp;
497 static const uint16_t mask =
498 (1 << 0x01) | (1 << 0x04) | (1 << 0x06) |
499 (1 << 0x07) | (1 << 0x0b) | (1 << 0x0c) | (1 << 0x0e);
500 uint8_t t;
501
502 dp = (struct disk_dos_part_entry *)iter->record;
503 t = dp->ostype;
504
505 if ((t <= 0x1f) && ((mask >> (t & ~0x10u)) & 1)) {
506 /* It's a hideable partition type */
507 if (miter->index == iter->index || opt.hide & HIDE_REV)
508 t &= ~0x10u; /* unhide */
509 else
510 t |= 0x10u; /* hide */
511 }
512 if (dp->ostype != t) {
513 dp->ostype = t;
514 return -1;
515 }
516 return 0;
517 }
518
519 /*
520 * miter - iterator we match against
521 * hide bits meaning:
522 * ..| - enable (1) / disable (0)
523 * .|. - all (1) / pri (0)
524 * |.. - unhide (1) / hide (0)
525 */
manglepe_hide(struct part_iter * miter)526 int manglepe_hide(struct part_iter *miter)
527 {
528 int wb = 0, werr = 0;
529 struct part_iter *iter = NULL;
530 int ridx;
531
532 if (!(opt.hide & HIDE_ON))
533 return 0;
534
535 if (miter->type != typedos) {
536 error("Option '[un]hide[all]' works only for legacy (DOS) partition scheme.");
537 return -1;
538 }
539
540 if (miter->index > 4 && !(opt.hide & HIDE_EXT))
541 warn("Specified partition is logical, so it can't be unhidden without 'unhideall'.");
542
543 if (!(iter = pi_begin(&miter->di, PIF_STEPALL | opt.piflags)))
544 return -1;
545
546 while (!pi_next(iter) && !werr) {
547 ridx = iter->index0;
548 if (!(opt.hide & HIDE_EXT) && ridx > 3)
549 break; /* skip when we're constrained to pri only */
550
551 if (iter->index != -1)
552 wb |= fliphide(iter, miter);
553
554 /*
555 * we have to update mbr and each extended partition, but only if
556 * changes (wb) were detected and there was no prior write error (werr)
557 */
558 if (ridx >= 3 && wb && !werr) {
559 mbrcpy(miter, iter);
560 werr |= disk_write_sectors(&iter->di, iter->dos.cebr_lba, iter->data, 1);
561 wb = 0;
562 }
563 }
564
565 if (iter->status < 0)
566 goto bail;
567
568 /* last update */
569 if (wb && !werr) {
570 mbrcpy(miter, iter);
571 werr |= disk_write_sectors(&iter->di, iter->dos.cebr_lba, iter->data, 1);
572 }
573 if (werr)
574 warn("Failed to write E/MBR during '[un]hide[all]'.");
575
576 bail:
577 pi_del(&iter);
578 return 0;
579 }
580
updchs(struct part_iter * iter,int ext)581 static int updchs(struct part_iter *iter, int ext)
582 {
583 struct disk_dos_part_entry *dp;
584 uint32_t ochs1, ochs2, lba;
585
586 dp = (struct disk_dos_part_entry *)iter->record;
587 if (!ext) {
588 /* primary or logical */
589 lba = (uint32_t)iter->abs_lba;
590 } else {
591 /* extended */
592 dp += 1;
593 lba = iter->dos.nebr_lba;
594 }
595 ochs1 = *(uint32_t *)dp->start;
596 ochs2 = *(uint32_t *)dp->end;
597
598 /*
599 * We have to be a bit more careful here in case of 0 start and/or length;
600 * start = 0 would be converted to the beginning of the disk (C/H/S =
601 * 0/0/1) or the [B]EBR, length = 0 would actually set the end CHS to be
602 * lower than the start CHS.
603 *
604 * Both are harmless in case of a hole (and in non-hole case will make
605 * partiter complain about corrupt layout if PIF_STRICT is set), but it
606 * makes everything look silly and not really correct.
607 *
608 * Thus the approach as seen below.
609 */
610
611 if (dp->start_lba || iter->index != -1) {
612 lba2chs(&dp->start, &iter->di, lba, L2C_CADD);
613 } else {
614 memset(&dp->start, 0, sizeof dp->start);
615 }
616
617 if ((dp->start_lba || iter->index != -1) && dp->length) {
618 lba2chs(&dp->end, &iter->di, lba + dp->length - 1, L2C_CADD);
619 } else {
620 memset(&dp->end, 0, sizeof dp->end);
621 }
622
623 return
624 *(uint32_t *)dp->start != ochs1 ||
625 *(uint32_t *)dp->end != ochs2;
626 }
627
628 /*
629 * miter - iterator we match against
630 */
manglepe_fixchs(struct part_iter * miter)631 int manglepe_fixchs(struct part_iter *miter)
632 {
633 int wb = 0, werr = 0;
634 struct part_iter *iter = NULL;
635 int ridx;
636
637 if (!opt.fixchs)
638 return 0;
639
640 if (miter->type != typedos) {
641 error("Option 'fixchs' works only for legacy (DOS) partition scheme.");
642 return -1;
643 }
644
645 if (!(iter = pi_begin(&miter->di, PIF_STEPALL | opt.piflags)))
646 return -1;
647
648 while (!pi_next(iter) && !werr) {
649 ridx = iter->index0;
650
651 wb |= updchs(iter, 0);
652 if (ridx > 3)
653 wb |= updchs(iter, 1);
654
655 /*
656 * we have to update mbr and each extended partition, but only if
657 * changes (wb) were detected and there was no prior write error (werr)
658 */
659 if (ridx >= 3 && wb && !werr) {
660 mbrcpy(miter, iter);
661 werr |= disk_write_sectors(&iter->di, iter->dos.cebr_lba, iter->data, 1);
662 wb = 0;
663 }
664 }
665
666 if (iter->status < 0)
667 goto bail;
668
669 /* last update */
670 if (wb && !werr) {
671 mbrcpy(miter, iter);
672 werr |= disk_write_sectors(&iter->di, iter->dos.cebr_lba, iter->data, 1);
673 }
674 if (werr)
675 warn("Failed to write E/MBR during 'fixchs'.");
676
677 bail:
678 pi_del(&iter);
679 return 0;
680 }
681
682 /* vim: set ts=8 sts=4 sw=4 noet: */
683