RASCSI/src/raspberrypi/rascsi_image.cpp
Uwe Seimet ddeede2beb
SASI code removal, error handling update, bug fixes, code cleanup (#806)
Summary ov most important changes triggered by the SASI code removal:

- Removed the SASI controller code
- New controller management. There is a new controller base class AbstractController and a class ControllerManager managing the controller lifecycle. The lifecycle management was removed from rasci.cpp and is covered by unit tests.
- New device management. The DeviceFactory manages the device lifecycle instead of rascsi.cpp. The new code is covered by unit tests.
- The lifecycle managment uses C++ collections with variable size instead of arrays with hard-coded sizes.
- The ScsiController method contains most of what was previously contained in scsidev_ctrl.cpp plus the code from sasidev_ctrl.cpp that was relevant for SCSI.
- scsi_command_util contains helper methods used for identical SCSI command implementations of more than one device
- Devices know their controllers, so that the controller instance does not need to be passed to each SCSI command. This change helps to decouple the devices from the controller. The phase_handler interface is also part of this decoupling.
- Use scsi_command_exception for propagating SCSI command execution errors, This resolves issues with the previous error handling, which was based on return values and often on magic numbers.
- Removed legacy SCSI error codes, all errors are now encoded by sense_key::, asc:: and status::.
- Fixed various warnings reported with -Wextra, -Weffc++ and -Wpedantic.
- Use constructor member initialization lists (recommended for ISO C++)
- Consistently use new/delete instead of malloc/free (recommended for ISO C++), resulting in better type safety and error handling
- Replaced variable sized arrays on the stack (violates ISO C++ and can cause a stack overflow)
- Replaced NULL by nullptr (recommended for C++), resulting in better type safety
- Use more const member functions in order to avoid side effects
- The format device page can now also be changed for hard disk drives (Fujitsu M2624S supports this, for instance), not just for MOs.
- Better encapsulation, updated access specifiers in many places
- Removed unused methods and method arguments
- Fixed a number of TODOs
- Added/updated unit tests for a lot of non-legacy classes
- Makefile support for creating HTML coverage reports with lcov/genhtml
2022-09-03 16:53:53 +02:00

405 lines
12 KiB
C++

//---------------------------------------------------------------------------
//
// SCSI Target Emulator RaSCSI Reloaded
// for Raspberry Pi
//
// Copyright (C) 2021 Uwe Seimet
//
//---------------------------------------------------------------------------
#include <unistd.h>
#include <sys/sendfile.h>
#include "os.h"
#include "log.h"
#include "filepath.h"
#include "spdlog/spdlog.h"
#include "devices/file_support.h"
#include "protobuf_util.h"
#include "rascsi_image.h"
#include <string>
#include <filesystem>
using namespace std;
using namespace spdlog;
using namespace rascsi_interface;
using namespace protobuf_util;
#define FPRT(fp, ...) fprintf(fp, __VA_ARGS__ )
RascsiImage::RascsiImage()
{
// ~/images is the default folder for device image files, for the root user it is /home/pi/images
int uid = getuid();
const char *sudo_user = getenv("SUDO_UID");
if (sudo_user) {
uid = stoi(sudo_user);
}
const passwd *passwd = getpwuid(uid);
if (uid && passwd) {
default_image_folder = passwd->pw_dir;
default_image_folder += "/images";
}
else {
default_image_folder = "/home/pi/images";
}
}
bool RascsiImage::CheckDepth(const string& filename)
{
return count(filename.begin(), filename.end(), '/') <= depth;
}
bool RascsiImage::CreateImageFolder(const CommandContext& context, const string& filename)
{
size_t filename_start = filename.rfind('/');
if (filename_start != string::npos) {
string folder = filename.substr(0, filename_start);
// Checking for existence first prevents an error if the top-level folder is a softlink
struct stat st;
if (stat(folder.c_str(), &st)) {
std::error_code error;
filesystem::create_directories(folder, error);
if (error) {
ReturnStatus(context, false, "Can't create image folder '" + folder + "': " + strerror(errno));
return false;
}
}
}
return true;
}
string RascsiImage::SetDefaultImageFolder(const string& f)
{
if (f.empty()) {
return "Can't set default image folder: Missing folder name";
}
string folder = f;
// If a relative path is specified the path is assumed to be relative to the user's home directory
if (folder[0] != '/') {
int uid = getuid();
const char *sudo_user = getenv("SUDO_UID");
if (sudo_user) {
uid = stoi(sudo_user);
}
const passwd *passwd = getpwuid(uid);
if (passwd) {
folder = passwd->pw_dir;
folder += "/";
folder += f;
}
}
else {
if (folder.find("/home/") != 0) {
return "Default image folder must be located in '/home/'";
}
}
struct stat info;
stat(folder.c_str(), &info);
if (!S_ISDIR(info.st_mode) || access(folder.c_str(), F_OK) == -1) {
return "Folder '" + f + "' does not exist or is not accessible";
}
default_image_folder = folder;
LOGINFO("Default image folder set to '%s'", default_image_folder.c_str());
return "";
}
bool RascsiImage::IsValidSrcFilename(const string& filename)
{
// Source file must exist and must be a regular file or a symlink
struct stat st;
return !stat(filename.c_str(), &st) && (S_ISREG(st.st_mode) || S_ISLNK(st.st_mode));
}
bool RascsiImage::IsValidDstFilename(const string& filename)
{
// Destination file must not yet exist
struct stat st;
return stat(filename.c_str(), &st);
}
bool RascsiImage::CreateImage(const CommandContext& context, const PbCommand& command)
{
string filename = GetParam(command, "file");
if (filename.empty()) {
return ReturnStatus(context, false, "Can't create image file: Missing image filename");
}
if (!CheckDepth(filename)) {
return ReturnStatus(context, false, ("Invalid folder hierarchy depth '" + filename + "'").c_str());
}
string full_filename = default_image_folder + "/" + filename;
if (!IsValidDstFilename(full_filename)) {
return ReturnStatus(context, false, "Can't create image file: '" + full_filename + "': File already exists");
}
const string size = GetParam(command, "size");
if (size.empty()) {
return ReturnStatus(context, false, "Can't create image file '" + full_filename + "': Missing image size");
}
off_t len;
try {
len = stoull(size);
}
catch(const invalid_argument& e) {
return ReturnStatus(context, false, "Can't create image file '" + full_filename + "': Invalid file size " + size);
}
catch(const out_of_range& e) {
return ReturnStatus(context, false, "Can't create image file '" + full_filename + "': Invalid file size " + size);
}
if (len < 512 || (len & 0x1ff)) {
return ReturnStatus(context, false, "Invalid image file size " + to_string(len) + " (not a multiple of 512)");
}
if (!CreateImageFolder(context, full_filename)) {
return false;
}
string permission = GetParam(command, "read_only");
// Since rascsi is running as root ensure that others can access the file
int permissions = !strcasecmp(permission.c_str(), "true") ?
S_IRUSR | S_IRGRP | S_IROTH : S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH;
int image_fd = open(full_filename.c_str(), O_CREAT|O_WRONLY, permissions);
if (image_fd == -1) {
return ReturnStatus(context, false, "Can't create image file '" + full_filename + "': " + string(strerror(errno)));
}
if (fallocate(image_fd, 0, 0, len)) {
close(image_fd);
unlink(full_filename.c_str());
return ReturnStatus(context, false, "Can't allocate space for image file '" + full_filename + "': " + string(strerror(errno)));
}
close(image_fd);
LOGINFO("%s", string("Created " + string(permissions & S_IWUSR ? "": "read-only ") + "image file '" + full_filename +
"' with a size of " + to_string(len) + " bytes").c_str());
return ReturnStatus(context);
}
bool RascsiImage::DeleteImage(const CommandContext& context, const PbCommand& command)
{
string filename = GetParam(command, "file");
if (filename.empty()) {
return ReturnStatus(context, false, "Missing image filename");
}
if (!CheckDepth(filename)) {
return ReturnStatus(context, false, ("Invalid folder hierarchy depth '" + filename + "'").c_str());
}
string full_filename = default_image_folder + "/" + filename;
int id;
int unit;
Filepath filepath;
filepath.SetPath(full_filename.c_str());
if (FileSupport::GetIdsForReservedFile(filepath, id, unit)) {
return ReturnStatus(context, false, "Can't delete image file '" + full_filename +
"', it is currently being used by device ID " + to_string(id) + ", unit " + to_string(unit));
}
if (remove(full_filename.c_str())) {
return ReturnStatus(context, false, "Can't delete image file '" + full_filename + "': " + string(strerror(errno)));
}
// Delete empty subfolders
size_t last_slash = filename.rfind('/');
while (last_slash != string::npos) {
string folder = filename.substr(0, last_slash);
string full_folder = default_image_folder + "/" + folder;
std::error_code error;
if (!filesystem::is_empty(full_folder, error) || error) {
break;
}
if (remove(full_folder.c_str())) {
return ReturnStatus(context, false, "Can't delete empty image folder '" + full_folder + "'");
}
last_slash = folder.rfind('/');
}
LOGINFO("Deleted image file '%s'", full_filename.c_str());
return ReturnStatus(context);
}
bool RascsiImage::RenameImage(const CommandContext& context, const PbCommand& command)
{
string from = GetParam(command, "from");
if (from.empty()) {
return ReturnStatus(context, false, "Can't rename/move image file: Missing source filename");
}
if (!CheckDepth(from)) {
return ReturnStatus(context, false, ("Invalid folder hierarchy depth '" + from + "'").c_str());
}
from = default_image_folder + "/" + from;
if (!IsValidSrcFilename(from)) {
return ReturnStatus(context, false, "Can't rename/move image file: '" + from + "': Invalid name or type");
}
string to = GetParam(command, "to");
if (to.empty()) {
return ReturnStatus(context, false, "Can't rename/move image file '" + from + "': Missing destination filename");
}
if (!CheckDepth(to)) {
return ReturnStatus(context, false, ("Invalid folder hierarchy depth '" + to + "'").c_str());
}
to = default_image_folder + "/" + to;
if (!IsValidDstFilename(to)) {
return ReturnStatus(context, false, "Can't rename/move image file '" + from + "' to '" + to + "': File already exists");
}
if (!CreateImageFolder(context, to)) {
return false;
}
if (rename(from.c_str(), to.c_str())) {
return ReturnStatus(context, false, "Can't rename/move image file '" + from + "' to '" + to + "': " + string(strerror(errno)));
}
LOGINFO("Renamed/Moved image file '%s' to '%s'", from.c_str(), to.c_str());
return ReturnStatus(context);
}
bool RascsiImage::CopyImage(const CommandContext& context, const PbCommand& command)
{
string from = GetParam(command, "from");
if (from.empty()) {
return ReturnStatus(context, false, "Can't copy image file: Missing source filename");
}
if (!CheckDepth(from)) {
return ReturnStatus(context, false, ("Invalid folder hierarchy depth '" + from + "'").c_str());
}
from = default_image_folder + "/" + from;
if (!IsValidSrcFilename(from)) {
return ReturnStatus(context, false, "Can't copy image file: '" + from + "': Invalid name or type");
}
string to = GetParam(command, "to");
if (to.empty()) {
return ReturnStatus(context, false, "Can't copy image file '" + from + "': Missing destination filename");
}
if (!CheckDepth(to)) {
return ReturnStatus(context, false, ("Invalid folder hierarchy depth '" + to + "'").c_str());
}
to = default_image_folder + "/" + to;
if (!IsValidDstFilename(to)) {
return ReturnStatus(context, false, "Can't copy image file '" + from + "' to '" + to + "': File already exists");
}
struct stat st;
if (lstat(from.c_str(), &st)) {
return ReturnStatus(context, false, "Can't access source image file '" + from + "': " + string(strerror(errno)));
}
if (!CreateImageFolder(context, to)) {
return false;
}
// Symbolic links need a special handling
if ((st.st_mode & S_IFMT) == S_IFLNK) {
if (symlink(filesystem::read_symlink(from).c_str(), to.c_str())) {
return ReturnStatus(context, false, "Can't copy symlink '" + from + "': " + string(strerror(errno)));
}
LOGINFO("Copied symlink '%s' to '%s'", from.c_str(), to.c_str());
return ReturnStatus(context);
}
int fd_src = open(from.c_str(), O_RDONLY, 0);
if (fd_src == -1) {
return ReturnStatus(context, false, "Can't open source image file '" + from + "': " + string(strerror(errno)));
}
string permission = GetParam(command, "read_only");
// Since rascsi is running as root ensure that others can access the file
int permissions = !strcasecmp(permission.c_str(), "true") ?
S_IRUSR | S_IRGRP | S_IROTH : S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH;
int fd_dst = open(to.c_str(), O_WRONLY | O_CREAT, permissions);
if (fd_dst == -1) {
close(fd_src);
return ReturnStatus(context, false, "Can't open destination image file '" + to + "': " + string(strerror(errno)));
}
if (sendfile(fd_dst, fd_src, 0, st.st_size) == -1) {
close(fd_dst);
close(fd_src);
unlink(to.c_str());
return ReturnStatus(context, false, "Can't copy image file '" + from + "' to '" + to + "': " + string(strerror(errno)));
}
close(fd_dst);
close(fd_src);
LOGINFO("Copied image file '%s' to '%s'", from.c_str(), to.c_str());
return ReturnStatus(context);
}
bool RascsiImage::SetImagePermissions(const CommandContext& context, const PbCommand& command)
{
string filename = GetParam(command, "file");
if (filename.empty()) {
return ReturnStatus(context, false, "Missing image filename");
}
if (!CheckDepth(filename)) {
return ReturnStatus(context, false, ("Invalid folder hierarchy depth '" + filename + "'").c_str());
}
filename = default_image_folder + "/" + filename;
if (!IsValidSrcFilename(filename)) {
return ReturnStatus(context, false, "Can't modify image file '" + filename + "': Invalid name or type");
}
bool protect = command.operation() == PROTECT_IMAGE;
int permissions = protect ? S_IRUSR | S_IRGRP | S_IROTH : S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH;
if (chmod(filename.c_str(), permissions) == -1) {
return ReturnStatus(context, false, "Can't " + string(protect ? "protect" : "unprotect") + " image file '" + filename + "': " +
strerror(errno));
}
if (protect) {
LOGINFO("Protected image file '%s'", filename.c_str());
}
else {
LOGINFO("Unprotected image file '%s'", filename.c_str());
}
return ReturnStatus(context);
}