InstallationSet.pm (c9b362f6) | InstallationSet.pm (9f91b7e3) |
---|---|
1#************************************************************** 2# 3# Licensed to the Apache Software Foundation (ASF) under one 4# or more contributor license agreements. See the NOTICE file 5# distributed with this work for additional information 6# regarding copyright ownership. The ASF licenses this file 7# to you under the Apache License, Version 2.0 (the 8# "License"); you may not use this file except in compliance --- 11 unchanged lines hidden (view full) --- 20#************************************************************** 21 22package installer::patch::InstallationSet; 23 24use installer::patch::Tools; 25use installer::patch::Version; 26use installer::logger; 27 | 1#************************************************************** 2# 3# Licensed to the Apache Software Foundation (ASF) under one 4# or more contributor license agreements. See the NOTICE file 5# distributed with this work for additional information 6# regarding copyright ownership. The ASF licenses this file 7# to you under the Apache License, Version 2.0 (the 8# "License"); you may not use this file except in compliance --- 11 unchanged lines hidden (view full) --- 20#************************************************************** 21 22package installer::patch::InstallationSet; 23 24use installer::patch::Tools; 25use installer::patch::Version; 26use installer::logger; 27 |
28use strict; |
|
28 | 29 |
30# TODO: Detect the location of 7z.exe |
|
29my $Unpacker = "/c/Program\\ Files/7-Zip/7z.exe"; 30 | 31my $Unpacker = "/c/Program\\ Files/7-Zip/7z.exe"; 32 |
33 34 35# TODO: Is there a touch in a standard library? 36sub touch ($) 37{ 38 my ($filename) = @_; 39 40 open my $out, ">", $filename; 41 close $out; 42} 43 44 45 46 |
|
31=head1 NAME 32 33 package installer::patch::InstallationSet - Functions for handling installation sets 34 35=head1 DESCRIPTION 36 37 This package contains functions for unpacking the .exe files that 38 are created by the NSIS installer creator and the .cab files in --- 4 unchanged lines hidden (view full) --- 43sub UnpackExe ($$) 44{ 45 my ($filename, $destination_path) = @_; 46 47 $installer::logger::Info->printf("unpacking installation set to '%s'\n", $destination_path); 48 49 # Unpack to a temporary path and change its name to the destination path 50 # only when the unpacking has completed successfully. | 47=head1 NAME 48 49 package installer::patch::InstallationSet - Functions for handling installation sets 50 51=head1 DESCRIPTION 52 53 This package contains functions for unpacking the .exe files that 54 are created by the NSIS installer creator and the .cab files in --- 4 unchanged lines hidden (view full) --- 59sub UnpackExe ($$) 60{ 61 my ($filename, $destination_path) = @_; 62 63 $installer::logger::Info->printf("unpacking installation set to '%s'\n", $destination_path); 64 65 # Unpack to a temporary path and change its name to the destination path 66 # only when the unpacking has completed successfully. |
51 my $temporary_destination_path = $destination_path . ".tmp"; 52 File::Path::make_path($temporary_destination_path); | 67 File::Path::make_path($destination_path); |
53 | 68 |
54 my $windows_filename = installer::patch::Tools::CygpathToWindows($filename); 55 my $windows_destination_path = installer::patch::Tools::CygpathToWindows($temporary_destination_path); | 69 my $windows_filename = installer::patch::Tools::ToEscapedWindowsPath($filename); 70 my $windows_destination_path = installer::patch::Tools::ToEscapedWindowsPath($destination_path); |
56 my $command = join(" ", 57 $Unpacker, | 71 my $command = join(" ", 72 $Unpacker, |
58 "x", "-o".$windows_destination_path, | 73 "x", 74 "-y", 75 "-o".$windows_destination_path, |
59 $windows_filename); 60 my $result = qx($command); 61 62 # Check the existence of the .cab files. | 76 $windows_filename); 77 my $result = qx($command); 78 79 # Check the existence of the .cab files. |
63 my $cab_filename = File::Spec->catfile($temporary_destination_path, "openoffice1.cab"); | 80 my $cab_filename = File::Spec->catfile($destination_path, "openoffice1.cab"); |
64 if ( ! -f $cab_filename) 65 { 66 installer::logger::PrintError("cab file '%s' was not extracted from installation set\n", $cab_filename); 67 return 0; 68 } | 81 if ( ! -f $cab_filename) 82 { 83 installer::logger::PrintError("cab file '%s' was not extracted from installation set\n", $cab_filename); 84 return 0; 85 } |
69 if (rename($temporary_destination_path, $destination_path) == 0) 70 { 71 installer::logger::PrintError("can not rename temporary extraction directory\n"); 72 return 0; 73 } | |
74 return 1; 75} 76 77 78 79 80=head2 UnpackCab($cab_filename, $destination_path) 81 --- 12 unchanged lines hidden (view full) --- 94sub UnpackCab ($$$) 95{ 96 my ($cab_filename, $msi, $destination_path) = @_; 97 98 # Step 1 99 # Extract the directory structure from the 'File' and 'Directory' tables in the given msi. 100 $installer::logger::Info->printf("setting up directory tree\n"); 101 my $file_table = $msi->GetTable("File"); | 86 return 1; 87} 88 89 90 91 92=head2 UnpackCab($cab_filename, $destination_path) 93 --- 12 unchanged lines hidden (view full) --- 106sub UnpackCab ($$$) 107{ 108 my ($cab_filename, $msi, $destination_path) = @_; 109 110 # Step 1 111 # Extract the directory structure from the 'File' and 'Directory' tables in the given msi. 112 $installer::logger::Info->printf("setting up directory tree\n"); 113 my $file_table = $msi->GetTable("File"); |
102 my $file_to_directory_map = $msi->GetFileToDirectoryMap(); | 114 my $file_map = $msi->GetFileMap(); |
103 104 # Step 2 105 # Unpack the .cab file to a temporary path. 106 my $temporary_destination_path = $destination_path . ".tmp"; 107 if ( -d $temporary_destination_path) 108 { 109 # Temporary directory already exists => cab file has already been unpacked (flat), nothing to do. 110 $installer::logger::Info->printf("cab file has already been unpacked to flat structure\n"); --- 6 unchanged lines hidden (view full) --- 117 # Step 3 118 # Move the files to their destinations. 119 File::Path::make_path($destination_path); 120 $installer::logger::Info->printf("moving files to their directories\n"); 121 my $count = 0; 122 foreach my $file_row (@{$file_table->GetAllRows()}) 123 { 124 my $unique_name = $file_row->GetValue('File'); | 115 116 # Step 2 117 # Unpack the .cab file to a temporary path. 118 my $temporary_destination_path = $destination_path . ".tmp"; 119 if ( -d $temporary_destination_path) 120 { 121 # Temporary directory already exists => cab file has already been unpacked (flat), nothing to do. 122 $installer::logger::Info->printf("cab file has already been unpacked to flat structure\n"); --- 6 unchanged lines hidden (view full) --- 129 # Step 3 130 # Move the files to their destinations. 131 File::Path::make_path($destination_path); 132 $installer::logger::Info->printf("moving files to their directories\n"); 133 my $count = 0; 134 foreach my $file_row (@{$file_table->GetAllRows()}) 135 { 136 my $unique_name = $file_row->GetValue('File'); |
125 my $directory_full_names = $file_to_directory_map->{$unique_name}; 126 my ($source_full_name, $target_full_name) = @$directory_full_names; | 137 my $directory_item = $file_map->{$unique_name}->{'directory'}; 138 my $source_full_name = $directory_item->{'full_source_long_name'}; |
127 128 my $flat_filename = File::Spec->catfile($temporary_destination_path, $unique_name); 129 my $dir_path = File::Spec->catfile($destination_path, $source_full_name); 130 my $dir_filename = File::Spec->catfile($dir_path, $unique_name); 131 | 139 140 my $flat_filename = File::Spec->catfile($temporary_destination_path, $unique_name); 141 my $dir_path = File::Spec->catfile($destination_path, $source_full_name); 142 my $dir_filename = File::Spec->catfile($dir_path, $unique_name); 143 |
132 printf("%d: making path %s and copying %s to %s\n", 133 $count, 134 $dir_path, 135 $unique_name, 136 $dir_filename); 137 File::Path::make_path($dir_path); | 144 if ( ! -d $dir_path) 145 { 146 File::Path::make_path($dir_path); 147 } |
138 File::Copy::move($flat_filename, $dir_filename); 139 140 ++$count; 141 } 142 143 # Cleanup. Remove the temporary directory. It should be empty by now. 144 rmdir($temporary_destination_path); 145} --- 15 unchanged lines hidden (view full) --- 161 my ($cab_filename, $destination_path, $file_table) = @_; 162 163 # Unpack the .cab file to a temporary path (note that 164 # $destination_path may alreay bee a temporary path). Using a 165 # second one prevents the lengthy flat unpacking to be repeated 166 # when another step fails. 167 168 $installer::logger::Info->printf("unpacking cab file\n"); | 148 File::Copy::move($flat_filename, $dir_filename); 149 150 ++$count; 151 } 152 153 # Cleanup. Remove the temporary directory. It should be empty by now. 154 rmdir($temporary_destination_path); 155} --- 15 unchanged lines hidden (view full) --- 171 my ($cab_filename, $destination_path, $file_table) = @_; 172 173 # Unpack the .cab file to a temporary path (note that 174 # $destination_path may alreay bee a temporary path). Using a 175 # second one prevents the lengthy flat unpacking to be repeated 176 # when another step fails. 177 178 $installer::logger::Info->printf("unpacking cab file\n"); |
169 my $temporary_destination_path = $destination_path . ".tmp"; 170 File::Path::make_path($temporary_destination_path); 171 my $windows_cab_filename = installer::patch::Tools::CygpathToWindows($cab_filename); 172 my $windows_destination_path = installer::patch::Tools::CygpathToWindows($temporary_destination_path); | 179 File::Path::make_path($destination_path); 180 my $windows_cab_filename = installer::patch::Tools::ToEscapedWindowsPath($cab_filename); 181 my $windows_destination_path = installer::patch::Tools::ToEscapedWindowsPath($destination_path); |
173 my $command = join(" ", 174 $Unpacker, 175 "x", "-o".$windows_destination_path, 176 $windows_cab_filename, 177 "-y"); | 182 my $command = join(" ", 183 $Unpacker, 184 "x", "-o".$windows_destination_path, 185 $windows_cab_filename, 186 "-y"); |
178 printf("running command '%s'\n", $command); | |
179 open my $cmd, $command."|"; 180 my $extraction_count = 0; 181 my $file_count = $file_table->GetRowCount(); 182 while (<$cmd>) 183 { 184 my $message = $_; 185 chomp($message); 186 ++$extraction_count; 187 printf("%4d/%4d %3.2f%% \r", 188 $extraction_count, 189 $file_count, 190 $extraction_count*100/$file_count); 191 } 192 close $cmd; | 187 open my $cmd, $command."|"; 188 my $extraction_count = 0; 189 my $file_count = $file_table->GetRowCount(); 190 while (<$cmd>) 191 { 192 my $message = $_; 193 chomp($message); 194 ++$extraction_count; 195 printf("%4d/%4d %3.2f%% \r", 196 $extraction_count, 197 $file_count, 198 $extraction_count*100/$file_count); 199 } 200 close $cmd; |
193 printf("extraction done \n"); 194 195 rename($temporary_destination_path, $destination_path) 196 || installer::logger::PrintError( 197 "can not rename the temporary directory '%s' to '%s'\n", 198 $temporary_destination_path, 199 $destination_path); | |
200} 201 202 203 204 | 201} 202 203 204 205 |
205=head GetUnpackedMsiPath ($version, $language, $package_format, $product) | 206=head GetUnpackedExePath ($version, $is_current_version, $language, $package_format, $product) |
206 207 Convenience function that returns where a downloadable installation set is extracted to. 208 209=cut | 207 208 Convenience function that returns where a downloadable installation set is extracted to. 209 210=cut |
210sub GetUnpackedMsiPath ($$$$) | 211sub GetUnpackedExePath ($$$$$) |
211{ | 212{ |
212 my ($version, $language, $package_format, $product) = @_; | 213 my ($version, $is_current_version, $language, $package_format, $product) = @_; |
213 | 214 |
214 return File::Spec->catfile( 215 GetUnpackedPath($version, $language, $package_format, $product), 216 "unpacked_msi"); | 215 my $path = GetUnpackedPath($version, $is_current_version, $language, $package_format, $product); 216 return File::Spec->catfile($path, "unpacked"); |
217} 218 219 220 221 | 217} 218 219 220 221 |
222=head GetUnpackedCabPath ($version, $language, $package_format, $product) | 222=head GetUnpackedCabPath ($version, $is_current_version, $language, $package_format, $product) |
223 224 Convenience function that returns where a cab file is extracted 225 (with injected directory structure from the msi file) to. 226 227=cut | 223 224 Convenience function that returns where a cab file is extracted 225 (with injected directory structure from the msi file) to. 226 227=cut |
228sub GetUnpackedCabPath ($$$$) | 228sub GetUnpackedCabPath ($$$$$) |
229{ | 229{ |
230 my ($version, $language, $package_format, $product) = @_; | 230 my ($version, $is_current_version, $language, $package_format, $product) = @_; |
231 | 231 |
232 return File::Spec->catfile( 233 GetUnpackedPath($version, $language, $package_format, $product), 234 "unpacked_cab"); | 232 my $path = GetUnpackedPath($version, $is_current_version, $language, $package_format, $product); 233 return File::Spec->catfile($path, "unpacked"); |
235} 236 237 238 239 | 234} 235 236 237 238 |
240=head2 GetUnpackedPath($version, $language, $package_format, $product) | 239=head2 GetUnpackedPath($version, $is_current_version, $language, $package_format, $product) |
241 242 Internal function for creating paths to where archives are unpacked. 243 244=cut | 240 241 Internal function for creating paths to where archives are unpacked. 242 243=cut |
245sub GetUnpackedPath ($$$$) | 244sub GetUnpackedPath ($$$$$) |
246{ | 245{ |
247 my ($version, $language, $package_format, $product) = @_; | 246 my ($version, $is_current_version, $language, $package_format, $product) = @_; |
248 249 return File::Spec->catfile( 250 $ENV{'SRC_ROOT'}, 251 "instsetoo_native", 252 $ENV{'INPATH'}, 253 $product, 254 $package_format, | 247 248 return File::Spec->catfile( 249 $ENV{'SRC_ROOT'}, 250 "instsetoo_native", 251 $ENV{'INPATH'}, 252 $product, 253 $package_format, |
255 installer::patch::Version::ArrayToDirectoryName(installer::patch::Version::StringToNumberArray($version)), 256 $language); | 254 installer::patch::Version::ArrayToDirectoryName( 255 installer::patch::Version::StringToNumberArray($version)), 256 $language); |
257} 258 259 260 261 | 257} 258 259 260 261 |
262sub GetMsiFilename ($$) 263{ 264 my ($path, $version) = @_; 265 266 my $no_dot_version = installer::patch::Version::ArrayToNoDotName( 267 installer::patch::Version::StringToNumberArray( 268 $version)); 269 return File::Spec->catfile( 270 $path, 271 "openoffice" . $no_dot_version . ".msi"); 272} 273 274 275 276 277sub GetCabFilename ($$) 278{ 279 my ($path, $version) = @_; 280 281 return File::Spec->catfile( 282 $path, 283 "openoffice1.cab"); 284} 285 286 287 288 |
|
262=head2 Download($language, $release_data, $filename) 263 264 Download an installation set to $filename. The URL for the 265 download is taken from $release_data, a snippet from the 266 instsetoo_native/data/releases.xml file. 267 268=cut 269sub Download ($$$) --- 189 unchanged lines hidden (view full) --- 459 $installer::logger::Info->printf("download set could not be downloaded\n"); 460 return 0; 461 } 462 } 463 464 return $ext_sources_filename; 465} 466 | 289=head2 Download($language, $release_data, $filename) 290 291 Download an installation set to $filename. The URL for the 292 download is taken from $release_data, a snippet from the 293 instsetoo_native/data/releases.xml file. 294 295=cut 296sub Download ($$$) --- 189 unchanged lines hidden (view full) --- 486 $installer::logger::Info->printf("download set could not be downloaded\n"); 487 return 0; 488 } 489 } 490 491 return $ext_sources_filename; 492} 493 |
494 495 496 497sub ProvideUnpackedExe ($$$$$) 498{ 499 my ($version, $is_current_version, $language, $package_format, $product_name) = @_; 500 501 # Check if the exe has already been unpacked. 502 my $unpacked_exe_path = installer::patch::InstallationSet::GetUnpackedExePath( 503 $version, 504 $is_current_version, 505 $language, 506 $package_format, 507 $product_name); 508 my $unpacked_exe_flag_filename = File::Spec->catfile($unpacked_exe_path, "__exe_is_unpacked"); 509 my $exe_is_unpacked = -f $unpacked_exe_flag_filename; 510 511 if ($exe_is_unpacked) 512 { 513 # Yes, exe has already been unpacked. There is nothing more to do. 514 $installer::logger::Info->printf("downloadable installation set has already been unpacked to\n"); 515 $installer::logger::Info->printf(" %s\n", $unpacked_exe_path); 516 return 1; 517 } 518 elsif ($is_current_version) 519 { 520 # For the current version the exe is created from the unpacked 521 # content and both are expected to be already present. 522 523 # In order to have the .cab and its unpacked content in one 524 # directory and don't interfere with the creation of regular 525 # installation sets, we copy the unpacked .exe into a separate 526 # directory. 527 528 my $original_path = File::Spec->catfile( 529 $ENV{'SRC_ROOT'}, 530 "instsetoo_native", 531 $ENV{'INPATH'}, 532 $product_name, 533 $package_format, 534 "install", 535 $language); 536 $installer::logger::Info->printf("creating a copy\n"); 537 $installer::logger::Info->printf(" of %s\n", $original_path); 538 $installer::logger::Info->printf(" at %s\n", $unpacked_exe_path); 539 File::Path::make_path($unpacked_exe_path) unless -d $unpacked_exe_path; 540 my ($file_count,$directory_count) = CopyRecursive($original_path, $unpacked_exe_path); 541 return 0 if ( ! defined $file_count); 542 $installer::logger::Info->printf(" copied %d files in %d directories\n", 543 $file_count, 544 $directory_count); 545 546 touch($unpacked_exe_flag_filename); 547 548 return 1; 549 } 550 else 551 { 552 # No, we have to unpack the exe. 553 554 # Provide the exe. 555 my $filename = installer::patch::InstallationSet::ProvideDownloadSet( 556 $version, 557 $language, 558 $package_format); 559 560 # Unpack it. 561 if (defined $filename) 562 { 563 if (installer::patch::InstallationSet::UnpackExe($filename, $unpacked_exe_path)) 564 { 565 $installer::logger::Info->printf("downloadable installation set has been unpacked to\n"); 566 $installer::logger::Info->printf(" %s\n", $unpacked_exe_path); 567 568 touch($unpacked_exe_flag_filename); 569 570 return 1; 571 } 572 } 573 else 574 { 575 installer::logger::PrintError("could not provide .exe installation set at '%s'\n", $filename); 576 } 577 } 578 579 return 0; 580} 581 582 583 584 585sub CopyRecursive ($$) 586{ 587 my ($source_path, $destination_path) = @_; 588 589 return (undef,undef) unless -d $source_path; 590 591 my @todo = ([$source_path, $destination_path]); 592 my $file_count = 0; 593 my $directory_count = 0; 594 while (scalar @todo > 0) 595 { 596 my ($source,$destination) = @{shift @todo}; 597 598 next if ! -d $source; 599 File::Path::make_path($destination); 600 ++$directory_count; 601 602 # Read list of files in the current source directory. 603 opendir( my $dir, $source); 604 my @files = readdir $dir; 605 closedir $dir; 606 607 # Copy all files and push all directories to @todo. 608 foreach my $file (@files) 609 { 610 next if $file =~ /^\.+$/; 611 612 my $source_file = File::Spec->catfile($source, $file); 613 my $destination_file = File::Spec->catfile($destination, $file); 614 if ( -f $source_file) 615 { 616 File::Copy::copy($source_file, $destination_file); 617 ++$file_count; 618 } 619 elsif ( -d $source_file) 620 { 621 push @todo, [$source_file, $destination_file]; 622 } 623 } 624 } 625 626 return ($file_count, $directory_count); 627} 628 629 630 631 632sub CheckLocalCopy ($$$$) 633{ 634 my ($version, $language, $package_format, $product_name) = @_; 635 636 # Compare creation times of the original .msi and its copy. 637 638 my $original_path = File::Spec->catfile( 639 $ENV{'SRC_ROOT'}, 640 "instsetoo_native", 641 $ENV{'INPATH'}, 642 $product_name, 643 $package_format, 644 "install", 645 $language); 646 647 my $copy_path = installer::patch::InstallationSet::GetUnpackedExePath( 648 $version, 649 1, 650 $language, 651 $package_format, 652 $product_name); 653 654 my $msi_basename = "openoffice" 655 . installer::patch::Version::ArrayToNoDotName( 656 installer::patch::Version::StringToNumberArray($version)) 657 . ".msi"; 658 659 my $original_msi_filename = File::Spec->catfile($original_path, $msi_basename); 660 my $copied_msi_filename = File::Spec->catfile($copy_path, $msi_basename); 661 662 my @original_msi_stats = stat($original_msi_filename); 663 my @copied_msi_stats = stat($copied_msi_filename); 664 my $original_msi_mtime = $original_msi_stats[9]; 665 my $copied_msi_mtime = $copied_msi_stats[9]; 666 667 if (defined $original_msi_mtime 668 && defined $copied_msi_mtime 669 && $original_msi_mtime > $copied_msi_mtime) 670 { 671 # The installation set is newer than its copy. 672 # Remove the copy. 673 $installer::logger::Info->printf( 674 "removing copy of installation set (version %s) because it is out of date\n", 675 $version); 676 File::Path::remove_tree($copy_path); 677 } 678} 679 680 681 682 683=head2 ProvideUnpackedCab 684 685 1a. Make sure that a downloadable installation set is present. 686 1b. or that a freshly built installation set (packed and unpacked is present) 687 2. Unpack the downloadable installation set 688 3. Unpack the .cab file. 689 690 The 'Provide' in the function name means that any step that has 691 already been made is not made again. 692 693=cut 694sub ProvideUnpackedCab ($$$$$) 695{ 696 my ($version, $is_current_version, $language, $package_format, $product_name) = @_; 697 698 if ($is_current_version) 699 { 700 # For creating patches we maintain a copy of the unpacked .exe. Make sure that that is updated when 701 # a new installation set has been built. 702 CheckLocalCopy($version, $language, $package_format, $product_name); 703 } 704 705 # Check if the cab file has already been unpacked. 706 my $unpacked_cab_path = installer::patch::InstallationSet::GetUnpackedCabPath( 707 $version, 708 $is_current_version, 709 $language, 710 $package_format, 711 $product_name); 712 my $unpacked_cab_flag_filename = File::Spec->catfile($unpacked_cab_path, "__cab_is_unpacked"); 713 my $cab_is_unpacked = -f $unpacked_cab_flag_filename; 714 715 if ($cab_is_unpacked) 716 { 717 # Yes. Cab was already unpacked. There is nothing more to do. 718 $installer::logger::Info->printf("cab has already been unpacked to\n"); 719 $installer::logger::Info->printf(" %s\n", $unpacked_cab_path); 720 721 return 1; 722 } 723 else 724 { 725 # Make sure that the exe is unpacked and the cab file exists. 726 ProvideUnpackedExe($version, $is_current_version, $language, $package_format, $product_name); 727 728 # Unpack the cab file. 729 my $unpacked_exe_path = installer::patch::InstallationSet::GetUnpackedExePath( 730 $version, 731 $is_current_version, 732 $language, 733 $package_format, 734 $product_name); 735 my $msi = new installer::patch::Msi( 736 installer::patch::InstallationSet::GetMsiFilename($unpacked_exe_path, $version), 737 $version, 738 $is_current_version, 739 $language, 740 $product_name); 741 742 my $cab_filename = installer::patch::InstallationSet::GetCabFilename( 743 $unpacked_exe_path, 744 $version); 745 if ( ! -f $cab_filename) 746 { 747 # Cab file does not exist. 748 installer::logger::PrintError( 749 "could not find .cab file at '%s'. Extraction of .exe seems to have failed.\n", 750 $cab_filename); 751 return 0; 752 } 753 754 if (installer::patch::InstallationSet::UnpackCab( 755 $cab_filename, 756 $msi, 757 $unpacked_cab_path)) 758 { 759 $installer::logger::Info->printf("unpacked cab file '%s'\n", $cab_filename); 760 $installer::logger::Info->printf(" to '%s'\n", $unpacked_cab_path); 761 762 touch($unpacked_cab_flag_filename); 763 764 return 1; 765 } 766 else 767 { 768 return 0; 769 } 770 } 771} |
|
4671; | 7721; |