1 // Copyright 2016 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.
4 
5 #include "chrome/installer/util/delete_old_versions.h"
6 
7 #include <map>
8 #include <memory>
9 #include <set>
10 #include <vector>
11 
12 #include "base/file_version_info.h"
13 #include "base/files/file.h"
14 #include "base/files/file_enumerator.h"
15 #include "base/files/file_path.h"
16 #include "base/files/file_util.h"
17 #include "base/logging.h"
18 #include "base/stl_util.h"
19 #include "base/version.h"
20 #include "chrome/installer/util/util_constants.h"
21 
22 namespace installer {
23 
24 namespace {
25 
26 using PathVector = std::vector<base::FilePath>;
27 using DirectorySet = std::set<base::FilePath>;
28 using ExecutableMap = std::map<base::FilePath, PathVector>;
29 
30 // Returns the name of the version directory for executable |exe_path|.
GetExecutableVersionDirName(const base::FilePath & exe_path)31 base::FilePath GetExecutableVersionDirName(const base::FilePath& exe_path) {
32   std::unique_ptr<FileVersionInfo> file_version_info(
33       FileVersionInfo::CreateFileVersionInfo(exe_path));
34   if (!file_version_info.get())
35     return base::FilePath();
36   return base::FilePath(file_version_info->file_version());
37 }
38 
39 // Returns the names of the old version directories found in |install_dir|. The
40 // directories named after the version of chrome.exe or new_chrome.exe are
41 // excluded.
GetOldVersionDirectories(const base::FilePath & install_dir)42 DirectorySet GetOldVersionDirectories(const base::FilePath& install_dir) {
43   const base::FilePath new_chrome_exe_version_dir_name =
44       GetExecutableVersionDirName(install_dir.Append(kChromeNewExe));
45   const base::FilePath chrome_exe_version_dir_name =
46       GetExecutableVersionDirName(install_dir.Append(kChromeExe));
47 
48   DirectorySet directories;
49   base::FileEnumerator enum_directories(install_dir, false,
50                                         base::FileEnumerator::DIRECTORIES);
51   for (base::FilePath directory_path = enum_directories.Next();
52        !directory_path.empty(); directory_path = enum_directories.Next()) {
53     const base::FilePath directory_name = directory_path.BaseName();
54     const base::Version version(directory_name.AsUTF8Unsafe());
55     const size_t kNumChromeVersionComponents = 4;
56     if (version.IsValid() &&
57         version.components().size() == kNumChromeVersionComponents &&
58         directory_name != new_chrome_exe_version_dir_name &&
59         directory_name != chrome_exe_version_dir_name) {
60       directories.insert(directory_name);
61     }
62   }
63   return directories;
64 }
65 
66 // Returns a map where the keys are version directory names and values are paths
67 // of old_chrome*.exe executables found in |install_dir|.
GetOldExecutables(const base::FilePath & install_dir)68 ExecutableMap GetOldExecutables(const base::FilePath& install_dir) {
69   ExecutableMap executables;
70   base::FileEnumerator enum_executables(install_dir, false,
71                                         base::FileEnumerator::FILES,
72                                         FILE_PATH_LITERAL("old_chrome*.exe"));
73   for (base::FilePath exe_path = enum_executables.Next(); !exe_path.empty();
74        exe_path = enum_executables.Next()) {
75     executables[GetExecutableVersionDirName(exe_path)].push_back(exe_path);
76   }
77   return executables;
78 }
79 
80 // Deletes directories that are in |directories| and don't have a matching
81 // executable in |executables|. Returns false if any such directories could not
82 // be deleted.
DeleteDirectoriesWithoutMatchingExecutable(const DirectorySet & directories,const ExecutableMap & executables,const base::FilePath & install_dir)83 bool DeleteDirectoriesWithoutMatchingExecutable(
84     const DirectorySet& directories,
85     const ExecutableMap& executables,
86     const base::FilePath& install_dir) {
87   bool success = true;
88   for (const base::FilePath& directory_name : directories) {
89     // Delete the directory if it doesn't have a matching executable.
90     if (!base::Contains(executables, directory_name)) {
91       const base::FilePath directory_path = install_dir.Append(directory_name);
92       LOG(WARNING) << "Attempting to delete stray directory "
93                    << directory_path.value();
94       if (!base::DeletePathRecursively(directory_path)) {
95         PLOG(ERROR) << "Failed to delete stray directory "
96                     << directory_path.value();
97         success = false;
98       }
99     }
100   }
101   return success;
102 }
103 
104 // Deletes executables that are in |executables| and don't have a matching
105 // directory in |directories|. Returns false if any such files could not be
106 // deleted.
DeleteExecutablesWithoutMatchingDirectory(const DirectorySet & directories,const ExecutableMap & executables)107 bool DeleteExecutablesWithoutMatchingDirectory(
108     const DirectorySet& directories,
109     const ExecutableMap& executables) {
110   bool success = true;
111   for (const auto& version_and_executables : executables) {
112     const auto& version_dir_name = version_and_executables.first;
113     const auto& executables_for_version = version_and_executables.second;
114 
115     // Don't delete the executables if they have a matching directory.
116     if (base::Contains(directories, version_dir_name))
117       continue;
118 
119     // Delete executables for version |version_dir_name|.
120     for (const auto& executable_path : executables_for_version) {
121       const base::FilePath executable_name = executable_path.BaseName();
122       LOG(WARNING) << "Attempting to delete stray executable "
123                    << executable_path.value();
124       if (!base::DeleteFile(executable_path)) {
125         PLOG(ERROR) << "Failed to delete stray executable "
126                     << executable_path.value();
127         success = false;
128       }
129     }
130   }
131   return success;
132 }
133 
134 // Opens |path| with options that prevent the file from being read or written
135 // via another handle. As long as the returned object is alive, it is guaranteed
136 // that |path| isn't in use. It can however be deleted.
GetFileLock(const base::FilePath & path)137 base::File GetFileLock(const base::FilePath& path) {
138   return base::File(path, base::File::FLAG_OPEN | base::File::FLAG_READ |
139                               base::File::FLAG_EXCLUSIVE_READ |
140                               base::File::FLAG_EXCLUSIVE_WRITE |
141                               base::File::FLAG_SHARE_DELETE);
142 }
143 
144 // Deletes |version_directory| and all executables in |version_executables| if
145 // no .exe or .dll file for the version is in use. Returns false if any file
146 // or directory for the version could not be deleted.
DeleteVersion(const base::FilePath & version_directory,const PathVector & version_executables)147 bool DeleteVersion(const base::FilePath& version_directory,
148                    const PathVector& version_executables) {
149   std::vector<base::File> locks;
150   PathVector locked_file_paths;
151 
152   // Lock .exe/.dll files in |version_directory|.
153   base::FileEnumerator enum_version_directory(version_directory, true,
154                                               base::FileEnumerator::FILES);
155   for (base::FilePath path = enum_version_directory.Next(); !path.empty();
156        path = enum_version_directory.Next()) {
157     if (!path.MatchesExtension(FILE_PATH_LITERAL(".exe")) &&
158         !path.MatchesExtension(FILE_PATH_LITERAL(".dll"))) {
159       continue;
160     }
161     locks.push_back(GetFileLock(path));
162     if (!locks.back().IsValid()) {
163       LOG(WARNING) << "Failed to delete old version "
164                    << version_directory.value() << " because " << path.value()
165                    << " is in use.";
166       return false;
167     }
168     locked_file_paths.push_back(path);
169   }
170 
171   // Lock executables in |version_executables|.
172   for (const base::FilePath& executable_path : version_executables) {
173     locks.push_back(GetFileLock(executable_path));
174     if (!locks.back().IsValid()) {
175       LOG(WARNING) << "Failed to delete old version "
176                    << version_directory.value() << " because "
177                    << executable_path.value() << " is in use.";
178       return false;
179     }
180     locked_file_paths.push_back(executable_path);
181   }
182 
183   bool success = true;
184 
185   // Delete locked files. The files won't actually be deleted until the locks
186   // are released.
187   for (const base::FilePath& locked_file_path : locked_file_paths) {
188     if (!base::DeleteFile(locked_file_path)) {
189       PLOG(ERROR) << "Failed to delete locked file "
190                   << locked_file_path.value();
191       success = false;
192     }
193   }
194 
195   // Release the locks, causing the locked files to actually be deleted. The
196   // version directory can't be deleted before this is done.
197   locks.clear();
198 
199   // Delete the version directory.
200   if (!base::DeletePathRecursively(version_directory)) {
201     PLOG(ERROR) << "Failed to delete version directory "
202                 << version_directory.value();
203     success = false;
204   }
205 
206   return success;
207 }
208 
209 // For each executable in |executables| that has a matching directory in
210 // |directories|, tries to delete the executable and the matching directory. No
211 // deletion occurs for a given version if a .exe or .dll file for that version
212 // is in use. Returns false if any directory/executables pair could not be
213 // deleted.
DeleteMatchingExecutablesAndDirectories(const DirectorySet & directories,const ExecutableMap & executables,const base::FilePath & install_dir)214 bool DeleteMatchingExecutablesAndDirectories(
215     const DirectorySet& directories,
216     const ExecutableMap& executables,
217     const base::FilePath& install_dir) {
218   bool success = true;
219   for (const auto& directory_name : directories) {
220     // Don't delete the version unless the directory has at least one matching
221     // executable.
222     auto version_executables_it = executables.find(directory_name);
223     if (version_executables_it == executables.end())
224       continue;
225 
226     // Try to delete all files for the version.
227     success &= DeleteVersion(install_dir.Append(directory_name),
228                              version_executables_it->second);
229   }
230   return success;
231 }
232 
233 }  // namespace
234 
DeleteOldVersions(const base::FilePath & install_dir)235 bool DeleteOldVersions(const base::FilePath& install_dir) {
236   const DirectorySet old_directories = GetOldVersionDirectories(install_dir);
237   const ExecutableMap old_executables = GetOldExecutables(install_dir);
238 
239   bool success = true;
240   success &= DeleteDirectoriesWithoutMatchingExecutable(
241       old_directories, old_executables, install_dir);
242   success &= DeleteExecutablesWithoutMatchingDirectory(old_directories,
243                                                        old_executables);
244   success &= DeleteMatchingExecutablesAndDirectories(
245       old_directories, old_executables, install_dir);
246 
247   return success;
248 }
249 
250 }  // namespace installer
251