//--------------------------------------------------------------------------- // // SCSI Target Emulator PiSCSI // for Raspberry Pi // // Copyright (C) 2021-2023 Uwe Seimet // //--------------------------------------------------------------------------- #include "devices/disk.h" #include "piscsi_image.h" #include "shared/protobuf_util.h" #include #include #include #include #include using namespace std; using namespace filesystem; using namespace piscsi_interface; using namespace protobuf_util; PiscsiImage::PiscsiImage() { // ~/images is the default folder for device image files, for the root user it is /home/pi/images default_folder = GetHomeDir() + "/images"; } bool PiscsiImage::CheckDepth(string_view filename) const { return ranges::count(filename, '/') <= depth; } bool PiscsiImage::CreateImageFolder(const CommandContext& context, string_view filename) const { if (const auto folder = path(filename).parent_path(); !folder.string().empty()) { // Checking for existence first prevents an error if the top-level folder is a softlink if (error_code error; exists(folder, error)) { return true; } try { create_directories(folder); return ChangeOwner(context, folder, false); } catch(const filesystem_error& e) { return context.ReturnErrorStatus("Can't create image folder '" + folder.string() + "': " + e.what()); } } return true; } string PiscsiImage::SetDefaultFolder(string_view f) { if (f.empty()) { return "Can't set default image folder: Missing folder name"; } // If a relative path is specified, the path is assumed to be relative to the user's home directory path folder(f); if (folder.is_relative()) { folder = path(GetHomeDir() + "/" + folder.string()); } if (path home_root = path(GetHomeDir()).parent_path(); !folder.string().starts_with(home_root.string())) { return "Default image folder must be located in '" + home_root.string() + "'"; } // Resolve a potential symlink if (error_code error; is_symlink(folder, error)) { folder = read_symlink(folder); } if (error_code error; !is_directory(folder)) { return string("'") + folder.string() + "' is not a valid image folder"; } default_folder = folder.string(); spdlog::info("Default image folder set to '" + default_folder + "'"); return ""; } bool PiscsiImage::CreateImage(const CommandContext& context) const { const string filename = GetParam(context.GetCommand(), "file"); if (filename.empty()) { return context.ReturnErrorStatus("Missing image filename"); } if (!CheckDepth(filename)) { return context.ReturnErrorStatus(("Invalid folder hierarchy depth '" + filename + "'").c_str()); } const string full_filename = GetFullName(filename); if (!IsValidDstFilename(full_filename)) { return context.ReturnErrorStatus("Can't create image file: '" + full_filename + "': File already exists"); } const string size = GetParam(context.GetCommand(), "size"); if (size.empty()) { return context.ReturnErrorStatus("Can't create image file '" + full_filename + "': Missing file size"); } off_t len; try { len = stoull(size); } catch(const invalid_argument&) { return context.ReturnErrorStatus("Can't create image file '" + full_filename + "': Invalid file size " + size); } catch(const out_of_range&) { return context.ReturnErrorStatus("Can't create image file '" + full_filename + "': Invalid file size " + size); } if (len < 512 || (len & 0x1ff)) { return context.ReturnErrorStatus("Invalid image file size " + to_string(len) + " (not a multiple of 512)"); } if (!CreateImageFolder(context, full_filename)) { return false; } const bool read_only = GetParam(context.GetCommand(), "read_only") == "true"; error_code error; path file(full_filename); try { ofstream s(file); s.close(); if (!ChangeOwner(context, file, read_only)) { return false; } resize_file(file, len); } catch(const filesystem_error& e) { remove(file, error); return context.ReturnErrorStatus("Can't create image file '" + full_filename + "': " + e.what()); } spdlog::info("Created " + string(read_only ? "read-only " : "") + "image file '" + full_filename + "' with a size of " + to_string(len) + " bytes"); return context.ReturnSuccessStatus(); } bool PiscsiImage::DeleteImage(const CommandContext& context) const { const string filename = GetParam(context.GetCommand(), "file"); if (filename.empty()) { return context.ReturnErrorStatus("Missing image filename"); } if (!CheckDepth(filename)) { return context.ReturnErrorStatus("Invalid folder hierarchy depth '" + filename + "'"); } const auto full_filename = path(GetFullName(filename)); if (!exists(full_filename)) { return context.ReturnErrorStatus("Image file '" + full_filename.string() + "' does not exist"); } if (!IsReservedFile(context, full_filename, "delete")) { return false; } if (error_code error; !remove(full_filename, error)) { return context.ReturnErrorStatus("Can't delete image file '" + full_filename.string() + "'"); } // Delete empty subfolders size_t last_slash = filename.rfind('/'); while (last_slash != string::npos) { const string folder = filename.substr(0, last_slash); const auto full_folder = path(GetFullName(folder)); if (error_code error; !filesystem::is_empty(full_folder, error) || error) { break; } if (error_code error; !remove(full_folder)) { return context.ReturnErrorStatus("Can't delete empty image folder '" + full_folder.string() + "'"); } last_slash = folder.rfind('/'); } spdlog::info("Deleted image file '" + full_filename.string() + "'"); return context.ReturnSuccessStatus(); } bool PiscsiImage::RenameImage(const CommandContext& context) const { string from; string to; if (!ValidateParams(context, "rename/move", from, to)) { return false; } try { rename(path(from), path(to)); } catch(const filesystem_error& e) { return context.ReturnErrorStatus("Can't rename/move image file '" + from + "': " + e.what()); } spdlog::info("Renamed/Moved image file '" + from + "' to '" + to + "'"); return context.ReturnSuccessStatus(); } bool PiscsiImage::CopyImage(const CommandContext& context) const { string from; string to; if (!ValidateParams(context, "copy", from, to)) { return false; } path f(from); path t(to); // Symbolic links need a special handling if (error_code error; is_symlink(f, error)) { try { copy_symlink(f, t); } catch(const filesystem_error& e) { return context.ReturnErrorStatus("Can't copy image file symlink '" + from + "': " + e.what()); } spdlog::info("Copied image file symlink '" + from + "' to '" + to + "'"); return context.ReturnSuccessStatus(); } try { copy_file(f, t); permissions(t, GetParam(context.GetCommand(), "read_only") == "true" ? perms::owner_read | perms::group_read | perms::others_read : perms::owner_read | perms::group_read | perms::others_read | perms::owner_write | perms::group_write); } catch(const filesystem_error& e) { return context.ReturnErrorStatus("Can't copy image file '" + from + "': " + e.what()); } spdlog::info("Copied image file '" + from + "' to '" + to + "'"); return context.ReturnSuccessStatus(); } bool PiscsiImage::SetImagePermissions(const CommandContext& context) const { const string filename = GetParam(context.GetCommand(), "file"); if (filename.empty()) { return context.ReturnErrorStatus("Missing image filename"); } if (!CheckDepth(filename)) { return context.ReturnErrorStatus("Invalid folder hierarchy depth '" + filename + "'"); } const string full_filename = GetFullName(filename); if (!IsValidSrcFilename(full_filename)) { return context.ReturnErrorStatus("Can't modify image file '" + full_filename + "': Invalid name or type"); } const bool protect = context.GetCommand().operation() == PROTECT_IMAGE; if (protect && !IsReservedFile(context, full_filename, "protect")) { return false; } try { permissions(path(full_filename), protect ? perms::owner_read | perms::group_read | perms::others_read : perms::owner_read | perms::group_read | perms::others_read | perms::owner_write | perms::group_write); } catch(const filesystem_error& e) { return context.ReturnErrorStatus("Can't " + string(protect ? "protect" : "unprotect") + " image file '" + full_filename + "': " + e.what()); } spdlog::info((protect ? "Protected" : "Unprotected") + string(" image file '") + full_filename + "'"); return context.ReturnSuccessStatus(); } bool PiscsiImage::IsReservedFile(const CommandContext& context, const string& file, const string& op) { const auto [id, lun] = StorageDevice::GetIdsForReservedFile(file); if (id != -1) { return context.ReturnErrorStatus("Can't " + op + " image file '" + file + "', it is currently being used by device " + to_string(id) + ":" + to_string(lun)); } return true; } bool PiscsiImage::ValidateParams(const CommandContext& context, const string& op, string& from, string& to) const { from = GetParam(context.GetCommand(), "from"); if (from.empty()) { return context.ReturnErrorStatus("Can't " + op + " image file: Missing source filename"); } if (!CheckDepth(from)) { return context.ReturnErrorStatus("Invalid folder hierarchy depth '" + from + "'"); } to = GetParam(context.GetCommand(), "to"); if (to.empty()) { return context.ReturnErrorStatus("Can't " + op + " image file '" + from + "': Missing destination filename"); } if (!CheckDepth(to)) { return context.ReturnErrorStatus("Invalid folder hierarchy depth '" + to + "'"); } from = GetFullName(from); if (!IsValidSrcFilename(from)) { return context.ReturnErrorStatus("Can't " + op + " image file: '" + from + "': Invalid name or type"); } to = GetFullName(to); if (!IsValidDstFilename(to)) { return context.ReturnErrorStatus("Can't " + op + " image file '" + from + "' to '" + to + "': File already exists"); } if (!IsReservedFile(context, from, op)) { return false; } if (!CreateImageFolder(context, to)) { return false; } return true; } bool PiscsiImage::IsValidSrcFilename(string_view filename) { // Source file must exist and must be a regular file or a symlink path file(filename); error_code error; return is_regular_file(file, error) || is_symlink(file, error); } bool PiscsiImage::IsValidDstFilename(string_view filename) { // Destination file must not yet exist try { return !exists(path(filename)); } catch(const filesystem_error&) { return false; } } bool PiscsiImage::ChangeOwner(const CommandContext& context, const path& filename, bool read_only) { const auto [uid, gid] = GetUidAndGid(); if (chown(filename.c_str(), uid, gid)) { // Remember the current error before the next filesystem operation const int e = errno; error_code error; remove(filename, error); return context.ReturnErrorStatus("Can't change ownership of '" + filename.string() + "': " + strerror(e)); } permissions(filename, read_only ? perms::owner_read | perms::group_read | perms::others_read : perms::owner_read | perms::group_read | perms::others_read | perms::owner_write | perms::group_write); return true; } string PiscsiImage::GetHomeDir() { const auto [uid, gid] = GetUidAndGid(); passwd pwd = {}; passwd *p_pwd; array pwbuf; if (uid && !getpwuid_r(uid, &pwd, pwbuf.data(), pwbuf.size(), &p_pwd)) { return pwd.pw_dir; } else { return "/home/pi"; } } pair PiscsiImage::GetUidAndGid() { int uid = getuid(); if (const char *sudo_user = getenv("SUDO_UID"); sudo_user != nullptr) { uid = stoi(sudo_user); } passwd pwd = {}; passwd *p_pwd; array pwbuf; int gid = -1; if (!getpwuid_r(uid, &pwd, pwbuf.data(), pwbuf.size(), &p_pwd)) { gid = pwd.pw_gid; } return { uid, gid }; }