duckstation

duckstation, but archived from the revision just before upstream changed it to a proprietary software project, this version is the libre one
git clone https://git.neptards.moe/u3shit/duckstation.git
Log | Files | Refs | README | LICENSE

updater.cpp (13596B)


      1 // SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin <stenzek@gmail.com>
      2 // SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0)
      3 
      4 #include "updater.h"
      5 
      6 #include "common/error.h"
      7 #include "common/file_system.h"
      8 #include "common/log.h"
      9 #include "common/minizip_helpers.h"
     10 #include "common/path.h"
     11 #include "common/progress_callback.h"
     12 #include "common/string_util.h"
     13 
     14 #include <algorithm>
     15 #include <cstdio>
     16 #include <cstring>
     17 #include <memory>
     18 #include <set>
     19 #include <string>
     20 #include <vector>
     21 
     22 #ifdef _WIN32
     23 #include "common/windows_headers.h"
     24 #include <Shobjidl.h>
     25 #include <shellapi.h>
     26 #include <wrl/client.h>
     27 #else
     28 #include <sys/stat.h>
     29 #endif
     30 
     31 #ifdef __APPLE__
     32 #include "common/cocoa_tools.h"
     33 #endif
     34 
     35 Updater::Updater(ProgressCallback* progress) : m_progress(progress)
     36 {
     37   progress->SetTitle("DuckStation Update Installer");
     38 }
     39 
     40 Updater::~Updater()
     41 {
     42   CloseUpdateZip();
     43 }
     44 
     45 bool Updater::Initialize(std::string staging_directory, std::string destination_directory)
     46 {
     47   m_staging_directory = std::move(staging_directory);
     48   m_destination_directory = std::move(destination_directory);
     49   m_progress->FormatInformation("Destination directory: '{}'", m_destination_directory);
     50   m_progress->FormatInformation("Staging directory: '{}'", m_staging_directory);
     51   return true;
     52 }
     53 
     54 bool Updater::OpenUpdateZip(const char* path)
     55 {
     56   m_zf = MinizipHelpers::OpenUnzFile(path);
     57   if (!m_zf)
     58     return false;
     59 
     60   m_zip_path = path;
     61 
     62   m_progress->SetStatusText("Parsing update zip...");
     63   return ParseZip();
     64 }
     65 
     66 void Updater::CloseUpdateZip()
     67 {
     68   if (m_zf)
     69   {
     70     unzClose(m_zf);
     71     m_zf = nullptr;
     72   }
     73 }
     74 
     75 void Updater::RemoveUpdateZip()
     76 {
     77   if (m_zip_path.empty())
     78     return;
     79 
     80   CloseUpdateZip();
     81 
     82   if (!FileSystem::DeleteFile(m_zip_path.c_str()))
     83     m_progress->FormatError("Failed to remove update zip '{}'", m_zip_path);
     84 }
     85 
     86 bool Updater::RecursiveDeleteDirectory(const char* path, bool remove_dir)
     87 {
     88 #ifdef _WIN32
     89   if (!remove_dir)
     90     return false;
     91 
     92   Microsoft::WRL::ComPtr<IFileOperation> fo;
     93   HRESULT hr = CoCreateInstance(CLSID_FileOperation, NULL, CLSCTX_ALL, IID_PPV_ARGS(fo.ReleaseAndGetAddressOf()));
     94   if (FAILED(hr))
     95   {
     96     m_progress->FormatError("CoCreateInstance() for IFileOperation failed: {}",
     97                             Error::CreateHResult(hr).GetDescription());
     98     return false;
     99   }
    100 
    101   Microsoft::WRL::ComPtr<IShellItem> item;
    102   hr = SHCreateItemFromParsingName(StringUtil::UTF8StringToWideString(path).c_str(), NULL,
    103                                    IID_PPV_ARGS(item.ReleaseAndGetAddressOf()));
    104   if (FAILED(hr))
    105   {
    106     m_progress->FormatError("SHCreateItemFromParsingName() for delete failed: {}",
    107                             Error::CreateHResult(hr).GetDescription());
    108     return false;
    109   }
    110 
    111   hr = fo->SetOperationFlags(FOF_NOCONFIRMATION | FOF_SILENT);
    112   if (FAILED(hr))
    113   {
    114     m_progress->FormatWarning("IFileOperation::SetOperationFlags() failed: {}",
    115                               Error::CreateHResult(hr).GetDescription());
    116   }
    117 
    118   hr = fo->DeleteItem(item.Get(), nullptr);
    119   if (FAILED(hr))
    120   {
    121     m_progress->FormatError("IFileOperation::DeleteItem() failed: {}", Error::CreateHResult(hr).GetDescription());
    122     return false;
    123   }
    124 
    125   item.Reset();
    126   hr = fo->PerformOperations();
    127   if (FAILED(hr))
    128   {
    129     m_progress->FormatError("IFileOperation::PerformOperations() failed: {}",
    130                             Error::CreateHResult(hr).GetDescription());
    131     return false;
    132   }
    133 
    134   return true;
    135 #else
    136   FileSystem::FindResultsArray results;
    137   if (FileSystem::FindFiles(path, "*", FILESYSTEM_FIND_FILES | FILESYSTEM_FIND_FOLDERS | FILESYSTEM_FIND_HIDDEN_FILES,
    138                             &results))
    139   {
    140     for (const FILESYSTEM_FIND_DATA& fd : results)
    141     {
    142       if (fd.Attributes & FILESYSTEM_FILE_ATTRIBUTE_DIRECTORY)
    143       {
    144         if (!RecursiveDeleteDirectory(fd.FileName.c_str(), true))
    145           return false;
    146       }
    147       else
    148       {
    149         m_progress->FormatInformation("Removing directory '{}'.", fd.FileName);
    150         if (!FileSystem::DeleteFile(fd.FileName.c_str()))
    151           return false;
    152       }
    153     }
    154   }
    155 
    156   if (!remove_dir)
    157     return true;
    158 
    159   m_progress->FormatInformation("Removing directory '{}'.", path);
    160   return FileSystem::DeleteDirectory(path);
    161 #endif
    162 }
    163 
    164 bool Updater::ParseZip()
    165 {
    166   if (unzGoToFirstFile(m_zf) != UNZ_OK)
    167   {
    168     m_progress->ModalError("unzGoToFirstFile() failed");
    169     return {};
    170   }
    171 
    172   for (;;)
    173   {
    174     char zip_filename_buffer[256];
    175     unz_file_info64 file_info;
    176     if (unzGetCurrentFileInfo64(m_zf, &file_info, zip_filename_buffer, sizeof(zip_filename_buffer), nullptr, 0, nullptr,
    177                                 0) != UNZ_OK)
    178     {
    179       m_progress->ModalError("unzGetCurrentFileInfo64() failed");
    180       return false;
    181     }
    182 
    183     FileToUpdate entry;
    184     entry.original_zip_filename = zip_filename_buffer;
    185 
    186     // replace forward slashes with backslashes
    187     size_t len = std::strlen(zip_filename_buffer);
    188     for (size_t i = 0; i < len; i++)
    189     {
    190       if (zip_filename_buffer[i] == '/' || zip_filename_buffer[i] == '\\')
    191         zip_filename_buffer[i] = FS_OSPATH_SEPARATOR_CHARACTER;
    192     }
    193 
    194     // should never have a leading slash. just in case.
    195     while (zip_filename_buffer[0] == FS_OSPATH_SEPARATOR_CHARACTER)
    196       std::memmove(&zip_filename_buffer[1], &zip_filename_buffer[0], --len);
    197 
    198 #ifdef _WIN32
    199     entry.file_mode = 0;
    200 #else
    201     // Preserve permissions on Unix.
    202     static constexpr u32 PERMISSION_MASK = (S_IRWXO | S_IRWXG | S_IRWXU);
    203     entry.file_mode =
    204       ((file_info.external_fa >> 16) & 0x01FFu) & PERMISSION_MASK; // https://stackoverflow.com/a/28753385
    205 #endif
    206 
    207     // skip directories (we sort them out later)
    208     if (len > 0 && zip_filename_buffer[len - 1] != FS_OSPATH_SEPARATOR_CHARACTER)
    209     {
    210       bool process_file = true;
    211       const char* filename_to_add = zip_filename_buffer;
    212 #ifdef _WIN32
    213       // skip updater itself, since it was already pre-extracted.
    214       process_file = process_file && (StringUtil::Strcasecmp(zip_filename_buffer, "updater.exe") != 0);
    215 #elif defined(__APPLE__)
    216       // on MacOS, we want to remove the DuckStation.app prefix.
    217       static constexpr const char* PREFIX_PATH = "DuckStation.app/";
    218       const size_t prefix_length = std::strlen(PREFIX_PATH);
    219       process_file = process_file && (std::strncmp(zip_filename_buffer, PREFIX_PATH, prefix_length) == 0);
    220       filename_to_add += prefix_length;
    221 #endif
    222       if (process_file)
    223       {
    224         entry.destination_filename = filename_to_add;
    225         m_progress->FormatInformation("Found file in zip: '{}'", entry.destination_filename);
    226         m_update_paths.push_back(std::move(entry));
    227       }
    228     }
    229 
    230     int res = unzGoToNextFile(m_zf);
    231     if (res == UNZ_END_OF_LIST_OF_FILE)
    232       break;
    233     if (res != UNZ_OK)
    234     {
    235       m_progress->ModalError("unzGoToNextFile() failed");
    236       return false;
    237     }
    238   }
    239 
    240   if (m_update_paths.empty())
    241   {
    242     m_progress->ModalError("No files found in update zip.");
    243     return false;
    244   }
    245 
    246   for (const FileToUpdate& ftu : m_update_paths)
    247   {
    248     const size_t len = ftu.destination_filename.length();
    249     for (size_t i = 0; i < len; i++)
    250     {
    251       if (ftu.destination_filename[i] == FS_OSPATH_SEPARATOR_CHARACTER)
    252       {
    253         std::string dir(ftu.destination_filename.begin(), ftu.destination_filename.begin() + i);
    254         while (!dir.empty() && dir[dir.length() - 1] == FS_OSPATH_SEPARATOR_CHARACTER)
    255           dir.erase(dir.length() - 1);
    256 
    257         if (std::find(m_update_directories.begin(), m_update_directories.end(), dir) == m_update_directories.end())
    258           m_update_directories.push_back(std::move(dir));
    259       }
    260     }
    261   }
    262 
    263   std::sort(m_update_directories.begin(), m_update_directories.end());
    264   for (const std::string& dir : m_update_directories)
    265     m_progress->FormatDebugMessage("Directory: {}", dir);
    266 
    267   return true;
    268 }
    269 
    270 bool Updater::PrepareStagingDirectory()
    271 {
    272   if (FileSystem::DirectoryExists(m_staging_directory.c_str()))
    273   {
    274     m_progress->DisplayWarning("Update staging directory already exists, removing");
    275     if (!RecursiveDeleteDirectory(m_staging_directory.c_str(), true) ||
    276         FileSystem::DirectoryExists(m_staging_directory.c_str()))
    277     {
    278       m_progress->ModalError("Failed to remove old staging directory");
    279       return false;
    280     }
    281   }
    282   if (!FileSystem::CreateDirectory(m_staging_directory.c_str(), false))
    283   {
    284     m_progress->FormatModalError("Failed to create staging directory {}", m_staging_directory);
    285     return false;
    286   }
    287 
    288   // create subdirectories in staging directory
    289   for (const std::string& subdir : m_update_directories)
    290   {
    291     m_progress->FormatInformation("Creating subdirectory in staging: {}", subdir);
    292 
    293     const std::string staging_subdir = Path::Combine(m_staging_directory, subdir);
    294     if (!FileSystem::CreateDirectory(staging_subdir.c_str(), false))
    295     {
    296       m_progress->FormatModalError("Failed to create staging subdirectory {}", staging_subdir);
    297       return false;
    298     }
    299   }
    300 
    301   return true;
    302 }
    303 
    304 bool Updater::StageUpdate()
    305 {
    306   m_progress->SetProgressRange(static_cast<u32>(m_update_paths.size()));
    307   m_progress->SetProgressValue(0);
    308 
    309   for (const FileToUpdate& ftu : m_update_paths)
    310   {
    311     m_progress->FormatStatusText("Extracting '{}' (mode {:o})...", ftu.original_zip_filename, ftu.file_mode);
    312 
    313     if (unzLocateFile(m_zf, ftu.original_zip_filename.c_str(), 0) != UNZ_OK)
    314     {
    315       m_progress->FormatModalError("Unable to locate file '{}' in zip", ftu.original_zip_filename);
    316       return false;
    317     }
    318     else if (unzOpenCurrentFile(m_zf) != UNZ_OK)
    319     {
    320       m_progress->FormatModalError("Failed to open file '{}' in zip", ftu.original_zip_filename);
    321       return false;
    322     }
    323 
    324     m_progress->FormatInformation("Extracting '{}'...", ftu.destination_filename);
    325 
    326     const std::string destination_file = Path::Combine(m_staging_directory, ftu.destination_filename);
    327     std::FILE* fp = FileSystem::OpenCFile(destination_file.c_str(), "wb");
    328     if (!fp)
    329     {
    330       m_progress->FormatModalError("Failed to open staging output file '{}'", destination_file);
    331       unzCloseCurrentFile(m_zf);
    332       return false;
    333     }
    334 
    335     static constexpr u32 CHUNK_SIZE = 4096;
    336     u8 buffer[CHUNK_SIZE];
    337     for (;;)
    338     {
    339       int byte_count = unzReadCurrentFile(m_zf, buffer, CHUNK_SIZE);
    340       if (byte_count < 0)
    341       {
    342         m_progress->FormatModalError("Failed to read file '{}' from zip", ftu.original_zip_filename);
    343         std::fclose(fp);
    344         FileSystem::DeleteFile(destination_file.c_str());
    345         unzCloseCurrentFile(m_zf);
    346         return false;
    347       }
    348       else if (byte_count == 0)
    349       {
    350         // end of file
    351         break;
    352       }
    353 
    354       if (std::fwrite(buffer, static_cast<size_t>(byte_count), 1, fp) != 1)
    355       {
    356         m_progress->FormatModalError("Failed to write to file '{}'", destination_file);
    357         std::fclose(fp);
    358         FileSystem::DeleteFile(destination_file.c_str());
    359         unzCloseCurrentFile(m_zf);
    360         return false;
    361       }
    362     }
    363 
    364 #ifndef _WIN32
    365     if (ftu.file_mode != 0)
    366     {
    367       const int fd = fileno(fp);
    368       const int res = (fd >= 0) ? fchmod(fd, ftu.file_mode) : -1;
    369       if (res < 0)
    370       {
    371         m_progress->FormatModalError("Failed to set mode for file '{}' (fd {}) to {:o}: errno {}", destination_file, fd,
    372                                      res, errno);
    373         std::fclose(fp);
    374         FileSystem::DeleteFile(destination_file.c_str());
    375         unzCloseCurrentFile(m_zf);
    376         return false;
    377       }
    378     }
    379 #endif
    380 
    381     std::fclose(fp);
    382     unzCloseCurrentFile(m_zf);
    383     m_progress->IncrementProgressValue();
    384   }
    385 
    386   return true;
    387 }
    388 
    389 bool Updater::CommitUpdate()
    390 {
    391   m_progress->SetStatusText("Committing update...");
    392 
    393   // create directories in target
    394   for (const std::string& subdir : m_update_directories)
    395   {
    396     const std::string dest_subdir = Path::Combine(m_destination_directory, subdir);
    397     if (!FileSystem::DirectoryExists(dest_subdir.c_str()) && !FileSystem::CreateDirectory(dest_subdir.c_str(), false))
    398     {
    399       m_progress->FormatModalError("Failed to create target directory '{}'", dest_subdir);
    400       return false;
    401     }
    402   }
    403 
    404   // move files to target
    405   for (const FileToUpdate& ftu : m_update_paths)
    406   {
    407     const std::string staging_file_name = Path::Combine(m_staging_directory, ftu.destination_filename);
    408     const std::string dest_file_name = Path::Combine(m_destination_directory, ftu.destination_filename);
    409     m_progress->FormatInformation("Moving '{}' to '{}'", staging_file_name, dest_file_name);
    410 
    411     Error error;
    412 #ifdef _WIN32
    413     const bool result = MoveFileExW(FileSystem::GetWin32Path(staging_file_name).c_str(),
    414                                     FileSystem::GetWin32Path(dest_file_name).c_str(), MOVEFILE_REPLACE_EXISTING);
    415     if (!result)
    416       error.SetWin32(GetLastError());
    417 #elif defined(__APPLE__)
    418     const bool result = CocoaTools::MoveFile(staging_file_name.c_str(), dest_file_name.c_str(), &error);
    419 #else
    420     const bool result = (rename(staging_file_name.c_str(), dest_file_name.c_str()) == 0);
    421     if (!result)
    422       error.SetErrno(errno);
    423 #endif
    424     if (!result)
    425     {
    426       m_progress->FormatModalError("Failed to rename '{}' to '{}': {}", staging_file_name, dest_file_name,
    427                                    error.GetDescription());
    428       return false;
    429     }
    430   }
    431 
    432   return true;
    433 }
    434 
    435 void Updater::CleanupStagingDirectory()
    436 {
    437   // remove staging directory itself
    438   if (!RecursiveDeleteDirectory(m_staging_directory.c_str(), true))
    439     m_progress->FormatError("Failed to remove staging directory '{}'", m_staging_directory);
    440 }
    441 
    442 bool Updater::ClearDestinationDirectory()
    443 {
    444   return RecursiveDeleteDirectory(m_destination_directory.c_str(), false);
    445 }