root/src/system/boot/loader/package_support.cpp
/*
 * Copyright 2014, Ingo Weinhold, ingo_weinhold@gmx.de.
 * Distributed under the terms of the MIT License.
 */


#include "package_support.h"

#include <errno.h>
#include <stdio.h>
#include <string.h>

#include <AutoDeleter.h>
#include <boot/vfs.h>
#include <package/PackagesDirectoryDefs.h>


#define TRACE_PACKAGE_SUPPORT
#ifdef TRACE_PACKAGE_SUPPORT
#       define TRACE(...) dprintf(__VA_ARGS__)
#else
#       define TRACE(...) do {} while (false)
#endif

static const char* const kAdministrativeDirectory
        = PACKAGES_DIRECTORY_ADMIN_DIRECTORY;
static const char* const kActivatedPackagesFile
        = PACKAGES_DIRECTORY_ACTIVATION_FILE;


static inline bool
is_system_package(const char* name)
{
        // The name must end with ".hpkg".
        size_t nameLength = strlen(name);
        if (nameLength < 6 || strcmp(name + nameLength - 5, ".hpkg") != 0)
                return false;

        // The name must either be "haiku.hpkg" or start with "haiku-".
        return strcmp(name, "haiku.hpkg") == 0 || strncmp(name, "haiku-", 6) == 0;
}


// #pragma mark - PackageVolumeState


PackageVolumeState::PackageVolumeState()
        :
        fName(NULL),
        fDisplayName(NULL),
        fSystemPackage(NULL)
{
}


PackageVolumeState::~PackageVolumeState()
{
        Unset();
}


status_t
PackageVolumeState::SetTo(const char* stateName)
{
        Unset();

        if (stateName != NULL) {
                fName = strdup(stateName);
                if (fName == NULL)
                        return B_NO_MEMORY;

                // Derive the display name from the directory name: Chop off the leading
                // "state_" and replace underscores by spaces.
                fDisplayName = strncmp(stateName, "state_", 6) == 0
                        ? strdup(stateName + 6) : strdup(stateName);
                if (fDisplayName == NULL)
                        return B_NO_MEMORY;

                char* remainder = fDisplayName;
                while (char* underscore = strchr(remainder, '_')) {
                        *underscore = ' ';
                        remainder = underscore + 1;
                }
        }

        return B_OK;
}


void
PackageVolumeState::Unset()
{
        free(fName);
        fName = NULL;

        free(fDisplayName);
        fDisplayName = NULL;

        free(fSystemPackage);
        fSystemPackage = NULL;
}


const char*
PackageVolumeState::DisplayName() const
{
        return fDisplayName;
}


status_t
PackageVolumeState::SetSystemPackage(const char* package)
{
        if (fSystemPackage != NULL)
                free(fSystemPackage);

        fSystemPackage = strdup(package);
        if (fSystemPackage == NULL)
                return B_NO_MEMORY;

        if (fName == NULL) {
                free(fDisplayName);
                fDisplayName = NULL;

                const char* packageVersion = strchr(package, '-');
                if (packageVersion == NULL) {
                        fDisplayName = strdup("Latest state");
                } else {
                        ArrayDeleter<char> newDisplayName(new(std::nothrow) char[B_FILE_NAME_LENGTH]);
                        if (!newDisplayName.IsSet())
                                return B_NO_MEMORY;

                        packageVersion++;
                        const char* packageVersionEnd = strchr(packageVersion, '-');
                        int length = -1;
                        if (packageVersionEnd != NULL)
                                length = packageVersionEnd - packageVersion;

                        snprintf(newDisplayName.Get(), B_FILE_NAME_LENGTH,
                                "Latest state (%.*s)", length, packageVersion);
                        fDisplayName = newDisplayName.Detach();
                }
        }

        return B_OK;
}


void
PackageVolumeState::GetPackagePath(const char* name, char* path,
        size_t pathSize)
{
        if (fName == NULL) {
                // the current state -- packages are directly in the packages directory
                strlcpy(path, name, pathSize);
        } else {
                // an old state
                snprintf(path, pathSize, "%s/%s/%s", kAdministrativeDirectory, fName,
                        name);
        }
}


/*static*/ bool
PackageVolumeState::IsNewer(const PackageVolumeState* a,
        const PackageVolumeState* b)
{
        if (b->fName == NULL)
                return false;
        if (a->fName == NULL)
                return true;
        return strcmp(a->fName, b->fName) > 0;
}


// #pragma mark - PackageVolumeInfo


PackageVolumeInfo::PackageVolumeInfo()
        :
        BReferenceable(),
        fStates(),
        fPackagesDir(NULL)
{
}


PackageVolumeInfo::~PackageVolumeInfo()
{
        while (PackageVolumeState* state = fStates.RemoveHead())
                delete state;

        if (fPackagesDir != NULL)
                closedir(fPackagesDir);
}


status_t
PackageVolumeInfo::SetTo(Directory* baseDirectory, const char* packagesPath)
{
        TRACE("PackageVolumeInfo::SetTo()\n");

        if (fPackagesDir != NULL)
                closedir(fPackagesDir);

        // get the packages directory
        fPackagesDir = open_directory(baseDirectory, packagesPath);
        if (fPackagesDir == NULL) {
                TRACE("PackageVolumeInfo::SetTo(): failed to open packages directory: "
                        "%s\n", strerror(errno));
                return errno;
        }

        Directory* packagesDirectory = directory_from(fPackagesDir);
        packagesDirectory->Acquire();

        // add the current state
        PackageVolumeState* state = _AddState(NULL);
        if (state == NULL)
                return B_NO_MEMORY;
        status_t error = _InitState(packagesDirectory, fPackagesDir, state);
        if (error != B_OK) {
                TRACE("PackageVolumeInfo::SetTo(): failed to init current state: "
                        "%s\n", strerror(error));
                return error;
        }

        return B_OK;
}


status_t
PackageVolumeInfo::LoadOldStates()
{
        if (fPackagesDir == NULL) {
                TRACE("PackageVolumeInfo::LoadOldStates(): package directory is NULL");
                return B_ERROR;
        }

        Directory* packagesDirectory = directory_from(fPackagesDir);
        packagesDirectory->Acquire();

        if (DIR* administrativeDir = open_directory(packagesDirectory,
                        kAdministrativeDirectory)) {
                while (dirent* entry = readdir(administrativeDir)) {
                        if (strncmp(entry->d_name, "state_", 6) == 0) {
                                TRACE("  old state directory \"%s\"\n", entry->d_name);
                                _AddState(entry->d_name);
                        }
                }

                closedir(administrativeDir);

                fStates.Sort(&PackageVolumeState::IsNewer);

                // initialize the old states
                PackageVolumeState* state = fStates.Head();
                status_t error;
                for (state = fStates.GetNext(state); state != NULL;) {
                        PackageVolumeState* nextState = fStates.GetNext(state);
                        if (state->Name() != NULL) {
                                error = _InitState(packagesDirectory, fPackagesDir, state);
                                if (error != B_OK) {
                                        TRACE("PackageVolumeInfo::LoadOldStates(): failed to "
                                                "init state \"%s\": %s\n", state->Name(),
                                                strerror(error));
                                        fStates.Remove(state);
                                        delete state;
                                }
                        }
                        state = nextState;
                }
        } else {
                TRACE("PackageVolumeInfo::LoadOldStates(): failed to open "
                        "administrative directory: %s\n", strerror(errno));
        }

        return B_OK;
}


PackageVolumeState*
PackageVolumeInfo::_AddState(const char* stateName)
{
        ObjectDeleter<PackageVolumeState> state(new(std::nothrow) PackageVolumeState);
        if (!state.IsSet())
                return NULL;

        if (state->SetTo(stateName) != B_OK) {
                return NULL;
        }

        fStates.Add(state.Get());
        return state.Detach();
}


status_t
PackageVolumeInfo::_InitState(Directory* packagesDirectory, DIR* dir,
        PackageVolumeState* state)
{
        // find the system package
        ArrayDeleter<char> systemPackageName(new(std::nothrow) char[B_FILE_NAME_LENGTH]);
        if (!systemPackageName.IsSet())
                return B_NO_MEMORY;
        ArrayDeleter<char> packagePath(new(std::nothrow) char[B_PATH_NAME_LENGTH]);
        if (!packagePath.IsSet())
                return B_NO_MEMORY;

        status_t error = _ParseActivatedPackagesFile(packagesDirectory, state,
                systemPackageName.Get(), B_FILE_NAME_LENGTH);
        if (error == B_OK) {
                // check, if package exists
                for (PackageVolumeState* otherState = state; otherState != NULL;
                                otherState = fStates.GetPrevious(otherState)) {
                        otherState->GetPackagePath(systemPackageName.Get(), packagePath.Get(),
                                B_PATH_NAME_LENGTH);
                        struct stat st;
                        if (get_stat(packagesDirectory, packagePath.Get(), st) == B_OK
                                        && S_ISREG(st.st_mode)) {
                                state->SetSystemPackage(packagePath.Get());
                                break;
                        }
                }
        } else {
                TRACE("PackageVolumeInfo::_InitState(): failed to parse "
                        "activated-packages: %s\n", strerror(error));

                // No or invalid activated-packages file. That is OK for the current
                // state. We'll iterate through the packages directory to find the
                // system package. We don't do that for old states, though.
                if (state->Name() != NULL)
                        return B_ENTRY_NOT_FOUND;

                while (dirent* entry = readdir(dir)) {
                        // The name must end with ".hpkg".
                        if (is_system_package(entry->d_name)) {
                                state->SetSystemPackage(entry->d_name);
                                break;
                        }
                }
        }

        if (state->SystemPackage() == NULL)
                return B_ENTRY_NOT_FOUND;

        return B_OK;
}


status_t
PackageVolumeInfo::_ParseActivatedPackagesFile(Directory* packagesDirectory,
        PackageVolumeState* state, char* packageName, size_t packageNameSize)
{
        // open the activated-packages file
        static const size_t kBufferSize = 3 * B_FILE_NAME_LENGTH + 2;
        ArrayDeleter<char> path(new(std::nothrow) char[kBufferSize]);
        if (!path.IsSet())
                return B_NO_MEMORY;
        snprintf(path.Get(), kBufferSize, "%s/%s/%s",
                kAdministrativeDirectory, state->Name() != NULL ? state->Name() : "",
                kActivatedPackagesFile);
        FileDescriptorCloser fd(open_from(packagesDirectory, path.Get(), O_RDONLY));
        if (!fd.IsSet())
                return fd.Get();

        struct stat st;
        if (fstat(fd.Get(), &st) != 0)
                return errno;
        if (!S_ISREG(st.st_mode))
                return B_ENTRY_NOT_FOUND;

        // read the file until we find the system package line
        size_t remainingBytes = 0;
        for (;;) {
                ssize_t bytesRead = read(fd.Get(), path.Get() + remainingBytes,
                        kBufferSize - remainingBytes - 1);
                if (bytesRead <= 0)
                        return B_ENTRY_NOT_FOUND;

                remainingBytes += bytesRead;
                path[remainingBytes] = '\0';

                char* line = path.Get();
                while (char* lineEnd = strchr(line, '\n')) {
                        *lineEnd = '\0';
                        if (is_system_package(line)) {
                                status_t result = strlcpy(packageName, line, packageNameSize)
                                                < packageNameSize
                                        ?  B_OK : B_NAME_TOO_LONG;
                                return result;
                        }

                        line = lineEnd + 1;
                }

                // move the remainder to the start of the buffer
                if (line < path.Get() + remainingBytes) {
                        size_t left = path.Get() + remainingBytes - line;
                        memmove(path.Get(), line, left);
                        remainingBytes = left;
                } else
                        remainingBytes = 0;
        }

        return B_ENTRY_NOT_FOUND;
}