⛏️ index : haiku.git

/*
 * 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;
}