1 // Copyright (c) 2011 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 // The file contains the implementation of the mini_installer re-versioner.
6 // The main function (GenerateNextVersion) does the following in a temp dir:
7 // - Extracts and unpacks setup.exe and the Chrome-bin folder from
9 // - Inspects setup.exe to determine the current version.
10 // - Runs through all .dll and .exe files:
11 // - Replacing all occurrences of the Unicode version string in the files'
12 // resources with the updated string.
13 // - For all resources in which the string substitution is made, the binary
14 // form of the version is also replaced.
15 // - Re-packs setup.exe and Chrome-bin.
16 // - Inserts them into the target mini_installer.exe.
18 // This code assumes that the host program 1) initializes the process-wide
19 // CommandLine instance, and 2) resides in the output directory of a build
20 // tree. When #2 is not the case, the --7za_path command-line switch may be
21 // used to provide the (relative or absolute) path to the directory containing
24 #include "chrome/installer/test/alternate_version_generator.h"
34 #include "base/basictypes.h"
35 #include "base/command_line.h"
36 #include "base/files/file.h"
37 #include "base/files/file_enumerator.h"
38 #include "base/files/file_path.h"
39 #include "base/files/file_util.h"
40 #include "base/logging.h"
41 #include "base/path_service.h"
42 #include "base/process/launch.h"
43 #include "base/process/process_handle.h"
44 #include "base/strings/string_util.h"
45 #include "base/strings/utf_string_conversions.h"
46 #include "base/version.h"
47 #include "base/win/pe_image.h"
48 #include "base/win/scoped_handle.h"
49 #include "chrome/installer/test/pe_image_resources.h"
50 #include "chrome/installer/test/resource_loader.h"
51 #include "chrome/installer/test/resource_updater.h"
52 #include "chrome/installer/util/lzma_util.h"
56 const wchar_t k7zaExe
[] = L
"7za.exe";
57 const wchar_t k7zaPathRelative
[] = L
"..\\..\\third_party\\lzma_sdk\\Executable";
58 const wchar_t kB7
[] = L
"B7";
59 const wchar_t kBl
[] = L
"BL";
60 const wchar_t kChromeBin
[] = L
"Chrome-bin";
61 const wchar_t kChromePacked7z
[] = L
"chrome.packed.7z";
62 const wchar_t kExe
[] = L
"exe";
63 const wchar_t kExpandExe
[] = L
"expand.exe";
64 const wchar_t kExtDll
[] = L
".dll";
65 const wchar_t kExtExe
[] = L
".exe";
66 const wchar_t kMakeCab
[] = L
"makecab.exe";
67 const wchar_t kSetupEx_
[] = L
"setup.ex_";
68 const wchar_t kSetupExe
[] = L
"setup.exe";
69 const char kSwitch7zaPath
[] = "7za_path";
70 const wchar_t kTempDirPrefix
[] = L
"mini_installer_test_temp";
72 // A helper class for creating and cleaning a temporary directory. A temporary
73 // directory is created in Initialize and destroyed (along with all of its
74 // contents) when the guard instance is destroyed.
75 class ScopedTempDirectory
{
77 ScopedTempDirectory() { }
78 ~ScopedTempDirectory() {
79 if (!directory_
.empty() && !base::DeleteFile(directory_
, true)) {
80 LOG(DFATAL
) << "Failed deleting temporary directory \""
81 << directory_
.value() << "\"";
84 // Creates a temporary directory.
86 DCHECK(directory_
.empty());
87 if (!base::CreateNewTempDirectory(&kTempDirPrefix
[0], &directory_
)) {
88 LOG(DFATAL
) << "Failed creating temporary directory.";
93 const base::FilePath
& directory() const {
94 DCHECK(!directory_
.empty());
99 base::FilePath directory_
;
100 DISALLOW_COPY_AND_ASSIGN(ScopedTempDirectory
);
101 }; // class ScopedTempDirectory
103 // A helper class for manipulating a Chrome product version.
104 class ChromeVersion
{
106 static ChromeVersion
FromHighLow(DWORD high
, DWORD low
) {
107 return ChromeVersion(static_cast<ULONGLONG
>(high
) << 32 |
108 static_cast<ULONGLONG
>(low
));
110 static ChromeVersion
FromString(const std::string
& version_string
) {
111 Version
version(version_string
);
112 DCHECK(version
.IsValid());
113 const std::vector
<uint32_t>& c(version
.components());
114 return ChromeVersion(static_cast<ULONGLONG
>(c
[0]) << 48 |
115 static_cast<ULONGLONG
>(c
[1]) << 32 |
116 static_cast<ULONGLONG
>(c
[2]) << 16 |
117 static_cast<ULONGLONG
>(c
[3]));
121 explicit ChromeVersion(ULONGLONG value
) : version_(value
) { }
122 WORD
major() const { return static_cast<WORD
>(version_
>> 48); }
123 WORD
minor() const { return static_cast<WORD
>(version_
>> 32); }
124 WORD
build() const { return static_cast<WORD
>(version_
>> 16); }
125 WORD
patch() const { return static_cast<WORD
>(version_
); }
126 DWORD
high() const { return static_cast<DWORD
>(version_
>> 32); }
127 DWORD
low() const { return static_cast<DWORD
>(version_
); }
128 ULONGLONG
value() const { return version_
; }
129 void set_value(ULONGLONG value
) { version_
= value
; }
130 std::wstring
ToString() const;
133 }; // class ChromeVersion
135 std::wstring
ChromeVersion::ToString() const {
138 swprintf_s(&buffer
[0], arraysize(buffer
), L
"%hu.%hu.%hu.%hu",
139 major(), minor(), build(), patch());
140 DCHECK_NE(-1, string_len
);
141 DCHECK_GT(static_cast<int>(arraysize(buffer
)), string_len
);
142 return std::wstring(&buffer
[0], string_len
);
146 // A read/write mapping of a file.
147 // Note: base::MemoryMappedFile is not used because it doesn't support
148 // read/write mappings. Adding such support across all platforms for this
149 // Windows-only test code seems like overkill.
152 MappedFile() : size_(), mapping_(), view_() { }
154 bool Initialize(base::File file
);
155 void* data() const { return view_
; }
156 size_t size() const { return size_
; }
163 DISALLOW_COPY_AND_ASSIGN(MappedFile
);
164 }; // class MappedFile
166 MappedFile::~MappedFile() {
168 if (UnmapViewOfFile(view_
) == 0) {
169 PLOG(DFATAL
) << "MappedFile failed to unmap view.";
172 if (mapping_
!= NULL
) {
173 if (CloseHandle(mapping_
) == 0) {
174 PLOG(DFATAL
) << "Could not close file mapping handle.";
179 bool MappedFile::Initialize(base::File file
) {
180 DCHECK(mapping_
== NULL
);
182 base::File::Info file_info
;
184 if (file
.GetInfo(&file_info
)) {
185 if (file_info
.size
<=
186 static_cast<int64
>(std::numeric_limits
<DWORD
>::max())) {
187 mapping_
= CreateFileMapping(file
.GetPlatformFile(), NULL
, PAGE_READWRITE
,
188 0, static_cast<DWORD
>(file_info
.size
), NULL
);
189 if (mapping_
!= NULL
) {
190 view_
= MapViewOfFile(mapping_
, FILE_MAP_WRITE
, 0, 0,
191 static_cast<size_t>(file_info
.size
));
195 PLOG(DFATAL
) << "MapViewOfFile failed";
198 PLOG(DFATAL
) << "CreateFileMapping failed";
201 LOG(DFATAL
) << "Files larger than " << std::numeric_limits
<DWORD
>::max()
202 << " are not supported.";
205 PLOG(DFATAL
) << "file.GetInfo failed";
211 // Calls CreateProcess with good default parameters and waits for the process
212 // to terminate returning the process exit code.
213 bool RunProcessAndWait(const wchar_t* exe_path
, const std::wstring
& cmdline
,
216 base::LaunchOptions options
;
218 options
.start_hidden
= true;
219 base::Process process
= base::LaunchProcess(cmdline
, options
);
220 if (process
.IsValid()) {
222 if (!GetExitCodeProcess(process
.Handle(),
223 reinterpret_cast<DWORD
*>(exit_code
))) {
224 PLOG(DFATAL
) << "Failed getting the exit code for \""
228 DCHECK_NE(*exit_code
, static_cast<int>(STILL_ACTIVE
));
238 // Retrieves the version number of |pe_file| from its version
239 // resource, placing the value in |version|. Returns true on success.
240 bool GetFileVersion(const base::FilePath
& pe_file
, ChromeVersion
* version
) {
243 upgrade_test::ResourceLoader pe_file_loader
;
244 std::pair
<const uint8
*, DWORD
> version_info_data
;
246 if (pe_file_loader
.Initialize(pe_file
) &&
249 static_cast<WORD
>(reinterpret_cast<uintptr_t>(RT_VERSION
)),
250 &version_info_data
)) {
251 const VS_FIXEDFILEINFO
* fixed_file_info
;
253 if (VerQueryValue(version_info_data
.first
, L
"\\",
254 reinterpret_cast<void**>(
255 const_cast<VS_FIXEDFILEINFO
**>(&fixed_file_info
)),
256 &ver_info_len
) != 0) {
257 DCHECK_EQ(sizeof(VS_FIXEDFILEINFO
), static_cast<size_t>(ver_info_len
));
258 *version
= ChromeVersion::FromHighLow(fixed_file_info
->dwFileVersionMS
,
259 fixed_file_info
->dwFileVersionLS
);
262 LOG(DFATAL
) << "VerQueryValue failed to retrieve VS_FIXEDFILEINFO";
269 // Retrieves the version number of setup.exe in |work_dir| from its version
270 // resource, placing the value in |version|. Returns true on success.
271 bool GetSetupExeVersion(const base::FilePath
& work_dir
,
272 ChromeVersion
* version
) {
273 return GetFileVersion(work_dir
.Append(&kSetupExe
[0]), version
);
277 // Replace all occurrences in the sequence [|dest_first|, |dest_last) that
278 // equals [|src_first|, |src_last) with the sequence at |replacement_first| of
279 // the same length. Returns true on success. If non-NULL, |replacements_made|
280 // is set to true/false accordingly.
281 bool ReplaceAll(uint8
* dest_first
, uint8
* dest_last
,
282 const uint8
* src_first
, const uint8
* src_last
,
283 const uint8
* replacement_first
, bool* replacements_made
) {
285 bool changed
= false;
287 dest_first
= std::search(dest_first
, dest_last
, src_first
, src_last
);
288 if (dest_first
== dest_last
) {
292 if (memcpy_s(dest_first
, dest_last
- dest_first
,
293 replacement_first
, src_last
- src_first
) != 0) {
297 dest_first
+= (src_last
- src_first
);
300 if (replacements_made
!= NULL
) {
301 *replacements_made
= changed
;
307 // A context structure in support of our EnumResource_Fn callback.
308 struct VisitResourceContext
{
309 ChromeVersion current_version
;
310 std::wstring current_version_str
;
311 ChromeVersion new_version
;
312 std::wstring new_version_str
;
313 }; // struct VisitResourceContext
315 // Replaces the old version with the new in a resource. A first pass is made to
316 // replace the string form (e.g., "9.0.584.0"). If any replacements are made, a
317 // second pass is made to replace the binary form (e.g., 0x0000024800000009).
318 void VisitResource(const upgrade_test::EntryPath
& path
,
319 uint8
* data
, DWORD size
, DWORD code_page
,
321 VisitResourceContext
& ctx
= *reinterpret_cast<VisitResourceContext
*>(context
);
323 // Replace all occurrences of current_version_str with new_version_str
324 bool changing_version
= false;
328 reinterpret_cast<const uint8
*>(ctx
.current_version_str
.c_str()),
329 reinterpret_cast<const uint8
*>(ctx
.current_version_str
.c_str() +
330 ctx
.current_version_str
.size() + 1),
331 reinterpret_cast<const uint8
*>(ctx
.new_version_str
.c_str()),
332 &changing_version
) &&
334 // Replace all occurrences of current_version with new_version
339 VersionPair cur_ver
= {
340 ctx
.current_version
.high(), ctx
.current_version
.low()
342 VersionPair new_ver
= {
343 ctx
.new_version
.high(), ctx
.new_version
.low()
345 ReplaceAll(data
, data
+ size
, reinterpret_cast<const uint8
*>(&cur_ver
),
346 reinterpret_cast<const uint8
*>(&cur_ver
) + sizeof(cur_ver
),
347 reinterpret_cast<const uint8
*>(&new_ver
), NULL
);
351 // Updates the version strings and numbers in all of |image_file|'s resources.
352 bool UpdateVersionIfMatch(const base::FilePath
& image_file
,
353 VisitResourceContext
* context
) {
355 context
->current_version_str
.size() < context
->new_version_str
.size()) {
360 uint32 flags
= base::File::FLAG_OPEN
| base::File::FLAG_READ
|
361 base::File::FLAG_WRITE
| base::File::FLAG_EXCLUSIVE_READ
|
362 base::File::FLAG_EXCLUSIVE_WRITE
;
363 base::File
file(image_file
, flags
);
364 // It turns out that the underlying CreateFile can fail due to unhelpful
365 // security software locking the newly created DLL. So add a few brief
366 // retries to help tests that use this pass on machines thusly encumbered.
368 while (!file
.IsValid() && retries
-- > 0) {
369 LOG(WARNING
) << "Failed to open \"" << image_file
.value() << "\"."
370 << " Retrying " << retries
<< " more times.";
372 file
.Initialize(image_file
, flags
);
375 if (file
.IsValid()) {
376 MappedFile image_mapping
;
377 if (image_mapping
.Initialize(file
.Pass())) {
378 base::win::PEImageAsData
image(
379 reinterpret_cast<HMODULE
>(image_mapping
.data()));
380 // PEImage class does not support other-architecture images.
381 if (image
.GetNTHeaders()->OptionalHeader
.Magic
==
382 IMAGE_NT_OPTIONAL_HDR_MAGIC
) {
383 result
= upgrade_test::EnumResources(
384 image
, &VisitResource
, reinterpret_cast<uintptr_t>(context
));
390 PLOG(DFATAL
) << "Failed to open \"" << image_file
.value() << "\"";
395 bool IncrementNewVersion(upgrade_test::Direction direction
,
396 VisitResourceContext
* ctx
) {
399 // Figure out a past or future version with the same string length as this one
400 // by decrementing or incrementing each component.
401 LONGLONG incrementer
= (direction
== upgrade_test::PREVIOUS_VERSION
? -1 : 1);
404 if (incrementer
== 0) {
405 LOG(DFATAL
) << "Improbable version at the cusp of complete rollover";
408 ctx
->new_version
.set_value(ctx
->current_version
.value() + incrementer
);
409 ctx
->new_version_str
= ctx
->new_version
.ToString();
411 } while (ctx
->new_version_str
.size() != ctx
->current_version_str
.size());
416 // Raises or lowers the version of all .exe and .dll files in |work_dir| as well
417 // as the |work-dir|\Chrome-bin\w.x.y.z directory. |original_version| and
418 // |new_version|, when non-NULL, are given the original and new version numbers
420 bool ApplyAlternateVersion(const base::FilePath
& work_dir
,
421 upgrade_test::Direction direction
,
422 std::wstring
* original_version
,
423 std::wstring
* new_version
) {
424 VisitResourceContext ctx
;
425 if (!GetSetupExeVersion(work_dir
, &ctx
.current_version
)) {
428 ctx
.current_version_str
= ctx
.current_version
.ToString();
430 if (!IncrementNewVersion(direction
, &ctx
)) {
434 // Modify all .dll and .exe files with the current version.
435 bool doing_great
= true;
436 base::FileEnumerator
all_files(work_dir
, true, base::FileEnumerator::FILES
);
438 base::FilePath file
= all_files
.Next();
442 std::wstring extension
= file
.Extension();
443 if (extension
== &kExtExe
[0] || extension
== &kExtDll
[0]) {
444 doing_great
= UpdateVersionIfMatch(file
, &ctx
);
446 } while (doing_great
);
448 // Change the versioned directory.
449 base::FilePath chrome_bin
= work_dir
.Append(&kChromeBin
[0]);
450 doing_great
= base::Move(chrome_bin
.Append(ctx
.current_version_str
),
451 chrome_bin
.Append(ctx
.new_version_str
));
454 // Report the version numbers if requested.
455 if (original_version
!= NULL
)
456 original_version
->assign(ctx
.current_version_str
);
457 if (new_version
!= NULL
)
458 new_version
->assign(ctx
.new_version_str
);
464 // Returns the path to the directory holding the 7za executable. By default, it
465 // is assumed that the test resides in the tree's output directory, so the
466 // relative path "..\..\third_party\lzma_sdk\Executable" is applied to the host
467 // executable's directory. This can be overridden with the --7za_path
468 // command-line switch.
469 base::FilePath
Get7zaPath() {
470 base::FilePath l7za_path
=
471 base::CommandLine::ForCurrentProcess()->GetSwitchValuePath(
473 if (l7za_path
.empty()) {
474 base::FilePath dir_exe
;
475 if (!PathService::Get(base::DIR_EXE
, &dir_exe
))
476 LOG(DFATAL
) << "Failed getting directory of host executable";
477 l7za_path
= dir_exe
.Append(&k7zaPathRelative
[0]);
482 bool CreateArchive(const base::FilePath
& output_file
,
483 const base::FilePath
& input_path
,
484 int compression_level
) {
485 DCHECK(compression_level
== 0 ||
486 compression_level
>= 1 && compression_level
<= 9 &&
487 (compression_level
& 0x01) != 0);
489 std::wstring
command_line(1, L
'"');
491 .append(Get7zaPath().Append(&k7zaExe
[0]).value())
492 .append(L
"\" a -bd -t7z \"")
493 .append(output_file
.value())
495 .append(input_path
.value())
497 .append(1, L
'0' + compression_level
);
499 if (!RunProcessAndWait(NULL
, command_line
, &exit_code
))
501 if (exit_code
!= 0) {
502 LOG(DFATAL
) << Get7zaPath().Append(&k7zaExe
[0]).value()
503 << " exited with code " << exit_code
504 << " while creating " << output_file
.value();
512 namespace upgrade_test
{
514 bool GenerateAlternateVersion(const base::FilePath
& original_installer_path
,
515 const base::FilePath
& target_path
,
517 std::wstring
* original_version
,
518 std::wstring
* new_version
) {
519 // Create a temporary directory in which we'll do our work.
520 ScopedTempDirectory work_dir
;
521 if (!work_dir
.Initialize())
524 // Copy the original mini_installer.
525 base::FilePath mini_installer
=
526 work_dir
.directory().Append(original_installer_path
.BaseName());
527 if (!base::CopyFile(original_installer_path
, mini_installer
)) {
528 LOG(DFATAL
) << "Failed copying \"" << original_installer_path
.value()
529 << "\" to \"" << mini_installer
.value() << "\"";
533 base::FilePath setup_ex_
= work_dir
.directory().Append(&kSetupEx_
[0]);
534 base::FilePath chrome_packed_7z
=
535 work_dir
.directory().Append(&kChromePacked7z
[0]);
536 // Load the original file and extract setup.ex_ and chrome.packed.7z
538 ResourceLoader resource_loader
;
539 std::pair
<const uint8
*, DWORD
> resource_data
;
541 if (!resource_loader
.Initialize(mini_installer
))
544 // Write out setup.ex_
545 if (!resource_loader
.Load(&kSetupEx_
[0], &kBl
[0], &resource_data
))
548 base::WriteFile(setup_ex_
,
549 reinterpret_cast<const char*>(resource_data
.first
),
550 static_cast<int>(resource_data
.second
));
551 if (written
!= static_cast<int>(resource_data
.second
)) {
552 LOG(DFATAL
) << "Failed writing \"" << setup_ex_
.value() << "\"";
556 // Write out chrome.packed.7z
557 if (!resource_loader
.Load(&kChromePacked7z
[0], &kB7
[0], &resource_data
))
560 base::WriteFile(chrome_packed_7z
,
561 reinterpret_cast<const char*>(resource_data
.first
),
562 static_cast<int>(resource_data
.second
));
563 if (written
!= static_cast<int>(resource_data
.second
)) {
564 LOG(DFATAL
) << "Failed writing \"" << chrome_packed_7z
.value() << "\"";
570 base::FilePath setup_exe
= setup_ex_
.ReplaceExtension(&kExe
[0]);
571 std::wstring command_line
;
572 command_line
.append(1, L
'"')
573 .append(&kExpandExe
[0])
575 .append(setup_ex_
.value())
577 .append(setup_exe
.value())
580 if (!RunProcessAndWait(NULL
, command_line
, &exit_code
))
582 if (exit_code
!= 0) {
583 LOG(DFATAL
) << &kExpandExe
[0] << " exited with code " << exit_code
;
587 // Unpack chrome.packed.7z
588 std::wstring chrome_7z_name
;
589 if (LzmaUtil::UnPackArchive(chrome_packed_7z
.value(),
590 work_dir
.directory().value(),
591 &chrome_7z_name
) != NO_ERROR
) {
592 LOG(DFATAL
) << "Failed unpacking \"" << chrome_packed_7z
.value() << "\"";
597 if (LzmaUtil::UnPackArchive(chrome_7z_name
, work_dir
.directory().value(),
599 LOG(DFATAL
) << "Failed unpacking \"" << chrome_7z_name
<< "\"";
603 // Get rid of intermediate files
604 base::FilePath
chrome_7z(chrome_7z_name
);
605 if (!base::DeleteFile(chrome_7z
, false) ||
606 !base::DeleteFile(chrome_packed_7z
, false) ||
607 !base::DeleteFile(setup_ex_
, false)) {
608 LOG(DFATAL
) << "Failed deleting intermediate files";
612 // Increment the version in all files.
613 ApplyAlternateVersion(work_dir
.directory(), direction
, original_version
,
616 // Pack up files into chrome.7z
617 if (!CreateArchive(chrome_7z
, work_dir
.directory().Append(&kChromeBin
[0]), 0))
620 // Compress chrome.7z into chrome.packed.7z
621 if (!CreateArchive(chrome_packed_7z
, chrome_7z
, 9))
624 // Compress setup.exe into setup.ex_
625 command_line
.assign(1, L
'"')
626 .append(&kMakeCab
[0])
627 .append(L
"\" /D CompressionType=LZX /L \"")
628 .append(work_dir
.directory().value())
630 .append(setup_exe
.value());
631 if (!RunProcessAndWait(NULL
, command_line
, &exit_code
))
633 if (exit_code
!= 0) {
634 LOG(DFATAL
) << &kMakeCab
[0] << " exited with code " << exit_code
;
638 // Replace the mini_installer's setup.ex_ and chrome.packed.7z resources.
639 ResourceUpdater updater
;
640 if (!updater
.Initialize(mini_installer
) ||
641 !updater
.Update(&kSetupEx_
[0], &kBl
[0],
642 MAKELANGID(LANG_ENGLISH
, SUBLANG_ENGLISH_US
),
644 !updater
.Update(&kChromePacked7z
[0], &kB7
[0],
645 MAKELANGID(LANG_ENGLISH
, SUBLANG_ENGLISH_US
),
651 // Finally, move the updated mini_installer into place.
652 return base::Move(mini_installer
, target_path
);
655 bool GenerateAlternatePEFileVersion(const base::FilePath
& original_file
,
656 const base::FilePath
& target_file
,
657 Direction direction
) {
658 VisitResourceContext ctx
;
659 if (!GetFileVersion(original_file
, &ctx
.current_version
)) {
660 LOG(DFATAL
) << "Failed reading version from \"" << original_file
.value()
664 ctx
.current_version_str
= ctx
.current_version
.ToString();
666 if (!IncrementNewVersion(direction
, &ctx
)) {
667 LOG(DFATAL
) << "Failed to increment version from \""
668 << original_file
.value() << "\"";
672 Version
new_version(base::UTF16ToASCII(ctx
.new_version_str
));
673 GenerateSpecificPEFileVersion(original_file
, target_file
, new_version
);
678 bool GenerateSpecificPEFileVersion(const base::FilePath
& original_file
,
679 const base::FilePath
& target_file
,
680 const Version
& version
) {
681 // First copy original_file to target_file.
682 if (!base::CopyFile(original_file
, target_file
)) {
683 LOG(DFATAL
) << "Failed copying \"" << original_file
.value()
684 << "\" to \"" << target_file
.value() << "\"";
688 VisitResourceContext ctx
;
689 if (!GetFileVersion(target_file
, &ctx
.current_version
)) {
690 LOG(DFATAL
) << "Failed reading version from \"" << target_file
.value()
694 ctx
.current_version_str
= ctx
.current_version
.ToString();
695 ctx
.new_version
= ChromeVersion::FromString(version
.GetString());
696 ctx
.new_version_str
= ctx
.new_version
.ToString();
698 return UpdateVersionIfMatch(target_file
, &ctx
);
701 } // namespace upgrade_test