1 /////////////////////////////////////////////////////////////////////////////// 2 // 3 /// \file list.c 4 /// \brief Listing information about .xz files 5 // 6 // Author: Lasse Collin 7 // 8 // This file has been put into the public domain. 9 // You can do whatever you want with this file. 10 // 11 /////////////////////////////////////////////////////////////////////////////// 12 13 #include "private.h" 14 #include "tuklib_integer.h" 15 16 17 /// Totals that are displayed if there was more than one file. 18 /// The "files" counter is also used in print_info_adv() to show 19 /// the file number. 20 static struct { 21 uint64_t files; 22 uint64_t streams; 23 uint64_t blocks; 24 uint64_t compressed_size; 25 uint64_t uncompressed_size; 26 uint32_t checks; 27 } totals = { 0, 0, 0, 0, 0, 0 }; 28 29 30 /// \brief Parse the Index(es) from the given .xz file 31 /// 32 /// \param idx If decoding is successful, *idx will be set to point 33 /// to lzma_index containing the decoded information. 34 /// On error, *idx is not modified. 35 /// \param pair Input file 36 /// 37 /// \return On success, false is returned. On error, true is returned. 38 /// 39 // TODO: This function is pretty big. liblzma should have a function that 40 // takes a callback function to parse the Index(es) from a .xz file to make 41 // it easy for applications. 42 static bool 43 parse_indexes(lzma_index **idx, file_pair *pair) 44 { 45 if (pair->src_st.st_size <= 0) { 46 message_error(_("%s: File is empty"), pair->src_name); 47 return true; 48 } 49 50 if (pair->src_st.st_size < 2 * LZMA_STREAM_HEADER_SIZE) { 51 message_error(_("%s: Too small to be a valid .xz file"), 52 pair->src_name); 53 return true; 54 } 55 56 io_buf buf; 57 lzma_stream_flags header_flags; 58 lzma_stream_flags footer_flags; 59 lzma_ret ret; 60 61 // lzma_stream for the Index decoder 62 lzma_stream strm = LZMA_STREAM_INIT; 63 64 // All Indexes decoded so far 65 lzma_index *combined_index = NULL; 66 67 // The Index currently being decoded 68 lzma_index *this_index = NULL; 69 70 // Current position in the file. We parse the file backwards so 71 // initialize it to point to the end of the file. 72 off_t pos = pair->src_st.st_size; 73 74 // Each loop iteration decodes one Index. 75 do { 76 // Check that there is enough data left to contain at least 77 // the Stream Header and Stream Footer. This check cannot 78 // fail in the first pass of this loop. 79 if (pos < 2 * LZMA_STREAM_HEADER_SIZE) { 80 message_error("%s: %s", pair->src_name, 81 message_strm(LZMA_DATA_ERROR)); 82 goto error; 83 } 84 85 pos -= LZMA_STREAM_HEADER_SIZE; 86 lzma_vli stream_padding = 0; 87 88 // Locate the Stream Footer. There may be Stream Padding which 89 // we must skip when reading backwards. 90 while (true) { 91 if (pos < LZMA_STREAM_HEADER_SIZE) { 92 message_error("%s: %s", pair->src_name, 93 message_strm( 94 LZMA_DATA_ERROR)); 95 goto error; 96 } 97 98 if (io_pread(pair, &buf, 99 LZMA_STREAM_HEADER_SIZE, pos)) 100 goto error; 101 102 // Stream Padding is always a multiple of four bytes. 103 int i = 2; 104 if (buf.u32[i] != 0) 105 break; 106 107 // To avoid calling io_pread() for every four bytes 108 // of Stream Padding, take advantage that we read 109 // 12 bytes (LZMA_STREAM_HEADER_SIZE) already and 110 // check them too before calling io_pread() again. 111 do { 112 stream_padding += 4; 113 pos -= 4; 114 --i; 115 } while (i >= 0 && buf.u32[i] == 0); 116 } 117 118 // Decode the Stream Footer. 119 ret = lzma_stream_footer_decode(&footer_flags, buf.u8); 120 if (ret != LZMA_OK) { 121 message_error("%s: %s", pair->src_name, 122 message_strm(ret)); 123 goto error; 124 } 125 126 // Check that the size of the Index field looks sane. 127 lzma_vli index_size = footer_flags.backward_size; 128 if ((lzma_vli)(pos) < index_size + LZMA_STREAM_HEADER_SIZE) { 129 message_error("%s: %s", pair->src_name, 130 message_strm(LZMA_DATA_ERROR)); 131 goto error; 132 } 133 134 // Set pos to the beginning of the Index. 135 pos -= index_size; 136 137 // See how much memory we can use for decoding this Index. 138 uint64_t memlimit = hardware_memlimit_get(); 139 uint64_t memused = 0; 140 if (combined_index != NULL) { 141 memused = lzma_index_memused(combined_index); 142 if (memused > memlimit) 143 message_bug(); 144 145 memlimit -= memused; 146 } 147 148 // Decode the Index. 149 ret = lzma_index_decoder(&strm, &this_index, memlimit); 150 if (ret != LZMA_OK) { 151 message_error("%s: %s", pair->src_name, 152 message_strm(ret)); 153 goto error; 154 } 155 156 do { 157 // Don't give the decoder more input than the 158 // Index size. 159 strm.avail_in = MIN(IO_BUFFER_SIZE, index_size); 160 if (io_pread(pair, &buf, strm.avail_in, pos)) 161 goto error; 162 163 pos += strm.avail_in; 164 index_size -= strm.avail_in; 165 166 strm.next_in = buf.u8; 167 ret = lzma_code(&strm, LZMA_RUN); 168 169 } while (ret == LZMA_OK); 170 171 // If the decoding seems to be successful, check also that 172 // the Index decoder consumed as much input as indicated 173 // by the Backward Size field. 174 if (ret == LZMA_STREAM_END) 175 if (index_size != 0 || strm.avail_in != 0) 176 ret = LZMA_DATA_ERROR; 177 178 if (ret != LZMA_STREAM_END) { 179 // LZMA_BUFFER_ERROR means that the Index decoder 180 // would have liked more input than what the Index 181 // size should be according to Stream Footer. 182 // The message for LZMA_DATA_ERROR makes more 183 // sense in that case. 184 if (ret == LZMA_BUF_ERROR) 185 ret = LZMA_DATA_ERROR; 186 187 message_error("%s: %s", pair->src_name, 188 message_strm(ret)); 189 190 // If the error was too low memory usage limit, 191 // show also how much memory would have been needed. 192 if (ret == LZMA_MEMLIMIT_ERROR) { 193 uint64_t needed = lzma_memusage(&strm); 194 if (UINT64_MAX - needed < memused) 195 needed = UINT64_MAX; 196 else 197 needed += memused; 198 199 message_mem_needed(V_ERROR, needed); 200 } 201 202 goto error; 203 } 204 205 // Decode the Stream Header and check that its Stream Flags 206 // match the Stream Footer. 207 pos -= footer_flags.backward_size + LZMA_STREAM_HEADER_SIZE; 208 if ((lzma_vli)(pos) < lzma_index_total_size(this_index)) { 209 message_error("%s: %s", pair->src_name, 210 message_strm(LZMA_DATA_ERROR)); 211 goto error; 212 } 213 214 pos -= lzma_index_total_size(this_index); 215 if (io_pread(pair, &buf, LZMA_STREAM_HEADER_SIZE, pos)) 216 goto error; 217 218 ret = lzma_stream_header_decode(&header_flags, buf.u8); 219 if (ret != LZMA_OK) { 220 message_error("%s: %s", pair->src_name, 221 message_strm(ret)); 222 goto error; 223 } 224 225 ret = lzma_stream_flags_compare(&header_flags, &footer_flags); 226 if (ret != LZMA_OK) { 227 message_error("%s: %s", pair->src_name, 228 message_strm(ret)); 229 goto error; 230 } 231 232 // Store the decoded Stream Flags into this_index. This is 233 // needed so that we can print which Check is used in each 234 // Stream. 235 ret = lzma_index_stream_flags(this_index, &footer_flags); 236 if (ret != LZMA_OK) 237 message_bug(); 238 239 // Store also the size of the Stream Padding field. It is 240 // needed to show the offsets of the Streams correctly. 241 ret = lzma_index_stream_padding(this_index, stream_padding); 242 if (ret != LZMA_OK) 243 message_bug(); 244 245 if (combined_index != NULL) { 246 // Append the earlier decoded Indexes 247 // after this_index. 248 ret = lzma_index_cat( 249 this_index, combined_index, NULL); 250 if (ret != LZMA_OK) { 251 message_error("%s: %s", pair->src_name, 252 message_strm(ret)); 253 goto error; 254 } 255 } 256 257 combined_index = this_index; 258 this_index = NULL; 259 260 } while (pos > 0); 261 262 lzma_end(&strm); 263 264 // All OK. Make combined_index available to the caller. 265 *idx = combined_index; 266 return false; 267 268 error: 269 // Something went wrong, free the allocated memory. 270 lzma_end(&strm); 271 lzma_index_end(combined_index, NULL); 272 lzma_index_end(this_index, NULL); 273 return true; 274 } 275 276 277 /// \brief Get the compression ratio 278 /// 279 /// This has slightly different format than that is used by in message.c. 280 static const char * 281 get_ratio(uint64_t compressed_size, uint64_t uncompressed_size) 282 { 283 if (uncompressed_size == 0) 284 return "---"; 285 286 const double ratio = (double)(compressed_size) 287 / (double)(uncompressed_size); 288 if (ratio > 9.999) 289 return "---"; 290 291 static char buf[6]; 292 snprintf(buf, sizeof(buf), "%.3f", ratio); 293 return buf; 294 } 295 296 297 static const char check_names[LZMA_CHECK_ID_MAX + 1][12] = { 298 "None", 299 "CRC32", 300 "Unknown-2", 301 "Unknown-3", 302 "CRC64", 303 "Unknown-5", 304 "Unknown-6", 305 "Unknown-7", 306 "Unknown-8", 307 "Unknown-9", 308 "SHA-256", 309 "Unknown-11", 310 "Unknown-12", 311 "Unknown-13", 312 "Unknown-14", 313 "Unknown-15", 314 }; 315 316 317 /// \brief Get a comma-separated list of Check names 318 /// 319 /// \param checks Bit mask of Checks to print 320 /// \param space_after_comma 321 /// It's better to not use spaces in table-like listings, 322 /// but in more verbose formats a space after a comma 323 /// is good for readability. 324 static const char * 325 get_check_names(uint32_t checks, bool space_after_comma) 326 { 327 assert(checks != 0); 328 329 static char buf[sizeof(check_names)]; 330 char *pos = buf; 331 size_t left = sizeof(buf); 332 333 const char *sep = space_after_comma ? ", " : ","; 334 bool comma = false; 335 336 for (size_t i = 0; i <= LZMA_CHECK_ID_MAX; ++i) { 337 if (checks & (UINT32_C(1) << i)) { 338 my_snprintf(&pos, &left, "%s%s", 339 comma ? sep : "", check_names[i]); 340 comma = true; 341 } 342 } 343 344 return buf; 345 } 346 347 348 /// \brief Read the Check value from the .xz file and print it 349 /// 350 /// Since this requires a seek, listing all Check values for all Blocks can 351 /// be slow. 352 /// 353 /// \param pair Input file 354 /// \param iter Location of the Block whose Check value should 355 /// be printed. 356 /// 357 /// \return False on success, true on I/O error. 358 static bool 359 print_check_value(file_pair *pair, const lzma_index_iter *iter) 360 { 361 // Don't read anything from the file if there is no integrity Check. 362 if (iter->stream.flags->check == LZMA_CHECK_NONE) { 363 printf("---"); 364 return false; 365 } 366 367 // Locate and read the Check field. 368 const uint32_t size = lzma_check_size(iter->stream.flags->check); 369 const off_t offset = iter->block.compressed_file_offset 370 + iter->block.total_size - size; 371 io_buf buf; 372 if (io_pread(pair, &buf, size, offset)) 373 return true; 374 375 // CRC32 and CRC64 are in little endian. Guess that all the future 376 // 32-bit and 64-bit Check values are little endian too. It shouldn't 377 // be a too big problem if this guess is wrong. 378 if (size == 4) { 379 printf("%08" PRIx32, conv32le(buf.u32[0])); 380 } else if (size == 8) { 381 printf("%016" PRIx64, conv64le(buf.u64[0])); 382 } else { 383 for (size_t i = 0; i < size; ++i) 384 printf("%02x", buf.u8[i]); 385 } 386 387 return false; 388 } 389 390 391 static void 392 print_info_basic(const lzma_index *idx, file_pair *pair) 393 { 394 static bool headings_displayed = false; 395 if (!headings_displayed) { 396 headings_displayed = true; 397 // TRANSLATORS: These are column titles. From Strms (Streams) 398 // to Ratio, the columns are right aligned. Check and Filename 399 // are left aligned. If you need longer words, it's OK to 400 // use two lines here. Test with xz --list. 401 puts(_("Strms Blocks Compressed Uncompressed Ratio " 402 "Check Filename")); 403 } 404 405 printf("%5s %7s %11s %11s %5s %-7s %s\n", 406 uint64_to_str(lzma_index_stream_count(idx), 0), 407 uint64_to_str(lzma_index_block_count(idx), 1), 408 uint64_to_nicestr(lzma_index_file_size(idx), 409 NICESTR_B, NICESTR_TIB, false, 2), 410 uint64_to_nicestr(lzma_index_uncompressed_size(idx), 411 NICESTR_B, NICESTR_TIB, false, 3), 412 get_ratio(lzma_index_file_size(idx), 413 lzma_index_uncompressed_size(idx)), 414 get_check_names(lzma_index_checks(idx), false), 415 pair->src_name); 416 417 return; 418 } 419 420 421 static void 422 print_adv_helper(uint64_t stream_count, uint64_t block_count, 423 uint64_t compressed_size, uint64_t uncompressed_size, 424 uint32_t checks) 425 { 426 printf(_(" Stream count: %s\n"), 427 uint64_to_str(stream_count, 0)); 428 printf(_(" Block count: %s\n"), 429 uint64_to_str(block_count, 0)); 430 printf(_(" Compressed size: %s\n"), 431 uint64_to_nicestr(compressed_size, 432 NICESTR_B, NICESTR_TIB, true, 0)); 433 printf(_(" Uncompressed size: %s\n"), 434 uint64_to_nicestr(uncompressed_size, 435 NICESTR_B, NICESTR_TIB, true, 0)); 436 printf(_(" Ratio: %s\n"), 437 get_ratio(compressed_size, uncompressed_size)); 438 printf(_(" Check: %s\n"), 439 get_check_names(checks, true)); 440 return; 441 } 442 443 444 static void 445 print_info_adv(const lzma_index *idx, file_pair *pair) 446 { 447 // Print the overall information. 448 print_adv_helper(lzma_index_stream_count(idx), 449 lzma_index_block_count(idx), 450 lzma_index_file_size(idx), 451 lzma_index_uncompressed_size(idx), 452 lzma_index_checks(idx)); 453 454 // TODO: The rest of this function needs some work. Currently 455 // the offsets are not printed, which could be useful even when 456 // printed in a less accurate format. On the other hand, maybe 457 // this should print the information with exact byte values, 458 // or maybe there should be at least an option to do that. 459 // 460 // We could also display some other info. E.g. it could be useful 461 // to quickly see how big is the biggest Block (uncompressed size) 462 // and if all Blocks have Compressed Size and Uncompressed Size 463 // fields present, which can be used e.g. for multithreaded 464 // decompression. 465 466 // Avoid printing Stream and Block lists when they wouldn't be useful. 467 bool show_blocks = false; 468 if (lzma_index_stream_count(idx) > 1) { 469 puts(_(" Streams:")); 470 puts(_(" Number Blocks Compressed " 471 "Uncompressed Ratio Check")); 472 473 lzma_index_iter iter; 474 lzma_index_iter_init(&iter, idx); 475 while (!lzma_index_iter_next(&iter, LZMA_INDEX_ITER_STREAM)) { 476 if (iter.stream.block_count > 1) 477 show_blocks = true; 478 479 printf(" %8s %10s %11s %11s %5s %s\n", 480 uint64_to_str(iter.stream.number, 0), 481 uint64_to_str(iter.stream.block_count, 1), 482 uint64_to_nicestr( 483 iter.stream.compressed_size, 484 NICESTR_B, NICESTR_TIB, false, 2), 485 uint64_to_nicestr( 486 iter.stream.uncompressed_size, 487 NICESTR_B, NICESTR_TIB, false, 3), 488 get_ratio(iter.stream.compressed_size, 489 iter.stream.uncompressed_size), 490 check_names[iter.stream.flags->check]); 491 } 492 } 493 494 if (show_blocks || lzma_index_block_count(idx) 495 > lzma_index_stream_count(idx) 496 || message_verbosity_get() >= V_DEBUG) { 497 puts(_(" Blocks:")); 498 // FIXME: Number in Stream/file, which one is better? 499 puts(_(" Stream Number Compressed " 500 "Uncompressed Ratio Check")); 501 502 lzma_index_iter iter; 503 lzma_index_iter_init(&iter, idx); 504 while (!lzma_index_iter_next(&iter, LZMA_INDEX_ITER_BLOCK)) { 505 printf(" %8s %10s %11s %11s %5s %-7s", 506 uint64_to_str(iter.stream.number, 0), 507 uint64_to_str(iter.block.number_in_stream, 1), 508 uint64_to_nicestr(iter.block.total_size, 509 NICESTR_B, NICESTR_TIB, false, 2), 510 uint64_to_nicestr( 511 iter.block.uncompressed_size, 512 NICESTR_B, NICESTR_TIB, false, 3), 513 get_ratio(iter.block.total_size, 514 iter.block.uncompressed_size), 515 check_names[iter.stream.flags->check]); 516 517 if (message_verbosity_get() >= V_DEBUG) 518 if (print_check_value(pair, &iter)) 519 return; 520 521 putchar('\n'); 522 } 523 } 524 } 525 526 527 static void 528 print_info_robot(const lzma_index *idx, file_pair *pair) 529 { 530 printf("file\t%" PRIu64 "\t%" PRIu64 "\t%" PRIu64 "\t%" PRIu64 531 "\t%s\t%s\t%s\n", 532 lzma_index_stream_count(idx), 533 lzma_index_block_count(idx), 534 lzma_index_file_size(idx), 535 lzma_index_uncompressed_size(idx), 536 get_ratio(lzma_index_file_size(idx), 537 lzma_index_uncompressed_size(idx)), 538 get_check_names(lzma_index_checks(idx), false), 539 pair->src_name); 540 541 if (message_verbosity_get() >= V_VERBOSE) { 542 lzma_index_iter iter; 543 lzma_index_iter_init(&iter, idx); 544 545 while (!lzma_index_iter_next(&iter, LZMA_INDEX_ITER_STREAM)) 546 printf("stream\t%" PRIu64 "\t%" PRIu64 "\t%" PRIu64 547 "\t%" PRIu64 "\t%" PRIu64 548 "\t%s\t%" PRIu64 "\t%s\n", 549 iter.stream.number, 550 iter.stream.compressed_offset, 551 iter.stream.uncompressed_offset, 552 iter.stream.compressed_size, 553 iter.stream.uncompressed_size, 554 get_ratio(iter.stream.compressed_size, 555 iter.stream.uncompressed_size), 556 iter.stream.padding, 557 check_names[iter.stream.flags->check]); 558 559 lzma_index_iter_rewind(&iter); 560 while (!lzma_index_iter_next(&iter, LZMA_INDEX_ITER_BLOCK)) { 561 printf("block\t%" PRIu64 "\t%" PRIu64 "\t%" PRIu64 562 "\t%" PRIu64 "\t%" PRIu64 563 "\t%" PRIu64 "\t%" PRIu64 "\t%s\t%s", 564 iter.stream.number, 565 iter.block.number_in_stream, 566 iter.block.number_in_file, 567 iter.block.compressed_file_offset, 568 iter.block.uncompressed_file_offset, 569 iter.block.total_size, 570 iter.block.uncompressed_size, 571 get_ratio(iter.block.total_size, 572 iter.block.uncompressed_size), 573 check_names[iter.stream.flags->check]); 574 575 if (message_verbosity_get() >= V_DEBUG) { 576 putchar('\t'); 577 if (print_check_value(pair, &iter)) 578 return; 579 } 580 581 putchar('\n'); 582 } 583 } 584 585 return; 586 } 587 588 589 static void 590 update_totals(const lzma_index *idx) 591 { 592 // TODO: Integer overflow checks 593 ++totals.files; 594 totals.streams += lzma_index_stream_count(idx); 595 totals.blocks += lzma_index_block_count(idx); 596 totals.compressed_size += lzma_index_file_size(idx); 597 totals.uncompressed_size += lzma_index_uncompressed_size(idx); 598 totals.checks |= lzma_index_checks(idx); 599 return; 600 } 601 602 603 static void 604 print_totals_basic(void) 605 { 606 // Print a separator line. 607 char line[80]; 608 memset(line, '-', sizeof(line)); 609 line[sizeof(line) - 1] = '\0'; 610 puts(line); 611 612 // Print the totals except the file count, which needs 613 // special handling. 614 printf("%5s %7s %11s %11s %5s %-7s ", 615 uint64_to_str(totals.streams, 0), 616 uint64_to_str(totals.blocks, 1), 617 uint64_to_nicestr(totals.compressed_size, 618 NICESTR_B, NICESTR_TIB, false, 2), 619 uint64_to_nicestr(totals.uncompressed_size, 620 NICESTR_B, NICESTR_TIB, false, 3), 621 get_ratio(totals.compressed_size, 622 totals.uncompressed_size), 623 get_check_names(totals.checks, false)); 624 625 // Since we print totals only when there are at least two files, 626 // the English message will always use "%s files". But some other 627 // languages need different forms for different plurals so we 628 // have to translate this string still. 629 // 630 // TRANSLATORS: This simply indicates the number of files shown 631 // by --list even though the format string uses %s. 632 printf(N_("%s file", "%s files\n", 633 totals.files <= ULONG_MAX ? totals.files 634 : (totals.files % 1000000) + 1000000), 635 uint64_to_str(totals.files, 0)); 636 637 return; 638 } 639 640 641 static void 642 print_totals_adv(void) 643 { 644 putchar('\n'); 645 puts(_("Totals:")); 646 printf(_(" Number of files: %s\n"), 647 uint64_to_str(totals.files, 0)); 648 print_adv_helper(totals.streams, totals.blocks, 649 totals.compressed_size, totals.uncompressed_size, 650 totals.checks); 651 652 return; 653 } 654 655 656 static void 657 print_totals_robot(void) 658 { 659 printf("totals\t%" PRIu64 "\t%" PRIu64 "\t%" PRIu64 "\t%" PRIu64 660 "\t%s\t%s\t%" PRIu64 "\n", 661 totals.streams, 662 totals.blocks, 663 totals.compressed_size, 664 totals.uncompressed_size, 665 get_ratio(totals.compressed_size, 666 totals.uncompressed_size), 667 get_check_names(totals.checks, false), 668 totals.files); 669 670 return; 671 } 672 673 674 extern void 675 list_totals(void) 676 { 677 if (opt_robot) { 678 // Always print totals in --robot mode. It can be convenient 679 // in some cases and doesn't complicate usage of the 680 // single-file case much. 681 print_totals_robot(); 682 683 } else if (totals.files > 1) { 684 // For non-robot mode, totals are printed only if there 685 // is more than one file. 686 if (message_verbosity_get() <= V_WARNING) 687 print_totals_basic(); 688 else 689 print_totals_adv(); 690 } 691 692 return; 693 } 694 695 696 extern void 697 list_file(const char *filename) 698 { 699 if (opt_format != FORMAT_XZ && opt_format != FORMAT_AUTO) 700 message_fatal(_("--list works only on .xz files " 701 "(--format=xz or --format=auto)")); 702 703 message_filename(filename); 704 705 if (filename == stdin_filename) { 706 message_error(_("--list does not support reading from " 707 "standard input")); 708 return; 709 } 710 711 // Unset opt_stdout so that io_open_src() won't accept special files. 712 // Set opt_force so that io_open_src() will follow symlinks. 713 opt_stdout = false; 714 opt_force = true; 715 file_pair *pair = io_open_src(filename); 716 if (pair == NULL) 717 return; 718 719 lzma_index *idx; 720 if (!parse_indexes(&idx, pair)) { 721 // Update the totals that are displayed after all 722 // the individual files have been listed. 723 update_totals(idx); 724 725 // We have three main modes: 726 // - --robot, which has submodes if --verbose is specified 727 // once or twice 728 // - Normal --list without --verbose 729 // - --list with one or two --verbose 730 if (opt_robot) 731 print_info_robot(idx, pair); 732 else if (message_verbosity_get() <= V_WARNING) 733 print_info_basic(idx, pair); 734 else 735 print_info_adv(idx, pair); 736 737 lzma_index_end(idx, NULL); 738 } 739 740 io_close(pair, false); 741 return; 742 } 743