root/src/tests/apps/partitioner/Partitioner.cpp
/*
 * Copyright 2007, Haiku, Inc. All Rights Reserved.
 * Distributed under the terms of the MIT License.
 *
 * Authors:
 *              Ingo Weinhold <bonefish@cs.tu-berlin.de>
 */

#include <ctype.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include <DiskDevice.h>
#include <DiskDeviceRoster.h>
#include <DiskDeviceVisitor.h>
#include <DiskSystem.h>
#include <PartitioningInfo.h>
#include <Path.h>
#include <String.h>

#include <ObjectList.h>


extern "C" const char* __progname;
static const char* kProgramName = __progname;

static const char* kUsage =
"Usage: %s <options> <device>\n"
"\n"
"Options:\n"
"  -h, --help   - print this help text\n"
;


static void
print_usage(bool error)
{
        fprintf(error ? stderr : stdout, kUsage, kProgramName);
}


static void
print_usage_and_exit(bool error)
{
        print_usage(error);
        exit(error ? 1 : 0);
}


static void
get_size_string(off_t _size, BString& string)
{
        const char* suffixes[] = {
                "", "K", "M", "G", "T", NULL
        };

        double size = _size;
        int index = 0;
        while (size > 1024 && suffixes[index + 1]) {
                size /= 1024;
                index++;
        }

        char buffer[128];
        snprintf(buffer, sizeof(buffer), "%.2f%s", size, suffixes[index]);

        string = buffer;
}


// PrintLongVisitor
class PrintLongVisitor : public BDiskDeviceVisitor {
public:
        virtual bool Visit(BDiskDevice *device)
        {
                BPath path;
                status_t error = device->GetPath(&path);
                const char *pathString = NULL;
                if (error == B_OK)
                        pathString = path.Path();
                else
                        pathString = strerror(error);
                printf("device %" B_PRId32 ": \"%s\"\n", device->ID(), pathString);
                printf("  has media:      %d\n", device->HasMedia());
                printf("  removable:      %d\n", device->IsRemovableMedia());
                printf("  read only:      %d\n", device->IsReadOnlyMedia());
                printf("  write once:     %d\n", device->IsWriteOnceMedia());
                printf("  ---\n");
                Visit(device, 0);
                return false;
        }
        
        virtual bool Visit(BPartition *partition, int32 level)
        {
                char prefix[128];
                sprintf(prefix, "%*s", 2 * (int)level, "");
                if (level > 0) {
                        BPath path;
                        status_t error = partition->GetPath(&path);
                        const char *pathString = NULL;
                        if (error == B_OK)
                                pathString = path.Path();
                        else
                                pathString = strerror(error);
                        printf("%spartition %" B_PRId32 ": \"%s\"\n", prefix, partition->ID(),
                                   pathString);
                }
                printf("%s  offset:         %" B_PRId64 "\n", prefix, partition->Offset());
                printf("%s  size:           %" B_PRId64 "\n", prefix, partition->Size());
                printf("%s  block size:     %" B_PRIu32 "\n", prefix, partition->BlockSize());
                printf("%s  index:          %" B_PRId32 "\n", prefix, partition->Index());
                printf("%s  status:         %" B_PRIu32 "\n", prefix, partition->Status());
                printf("%s  file system:    %d\n", prefix,
                           partition->ContainsFileSystem());
                printf("%s  part. system:   %d\n", prefix,
                           partition->ContainsPartitioningSystem());
                printf("%s  device:         %d\n", prefix, partition->IsDevice());
                printf("%s  read only:      %d\n", prefix, partition->IsReadOnly());
                printf("%s  mounted:        %d\n", prefix, partition->IsMounted());
                printf("%s  flags:          %" B_PRIx32 "\n", prefix, partition->Flags());
                printf("%s  name:           \"%s\"\n", prefix, partition->Name());
                printf("%s  content name:   \"%s\"\n", prefix,
                        partition->ContentName().String());
                printf("%s  type:           \"%s\"\n", prefix, partition->Type());
                printf("%s  content type:   \"%s\"\n", prefix,
                        partition->ContentType());
                printf("%s  params:         \"%s\"\n", prefix, partition->Parameters());
                printf("%s  content params: \"%s\"\n", prefix,
                        partition->ContentParameters());
                // volume, icon,...
                return false;
        }
};


static void
print_partition_table_header()
{
        printf("   Index     Start      Size                Content Type      "
                "Content Name\n");
        printf("--------------------------------------------------------------"
                "------------\n");
}


static void
print_partition(BPartition* partition, int level, int index)
{
        BString offset, size;
        get_size_string(partition->Offset(), offset);
        get_size_string(partition->Size(), size);

        printf("%*s%02d%*s  %8s  %8s  %26.26s  %16.16s\n", 2 * level, "",
                index,
                2 * max_c(3 - level, 0), "",
                offset.String(), size.String(),
                (partition->ContentType() ? partition->ContentType() : "-"),
                partition->ContentName().String());
}


// PrintShortVisitor
class PrintShortVisitor : public BDiskDeviceVisitor {
public:
        virtual bool Visit(BDiskDevice* device)
        {
                fIndex = 0;

                // get path
                BPath path;
                status_t error = device->GetPath(&path);
                const char *pathString = NULL;
                if (error == B_OK)
                        pathString = path.Path();
                else
                        pathString = strerror(error);

                // check media present; if so which read/write abilities
                const char* media;
                const char* readWrite;
                if (device->HasMedia()) {
                        if (device->IsRemovableMedia()) {
                                media = "removable media";
                        } else {
                                media = "fixed media";
                        }

                        if (device->IsReadOnlyMedia()) {
                                if (device->IsWriteOnceMedia())
                                        readWrite = ", write once";
                                else
                                        readWrite = ", read-only";
                        } else
                                readWrite = ", read/write";
                } else {
                        media = "no media";
                        readWrite = "";
                }

                printf("\ndevice %" B_PRId32 ": \"%s\": %s%s\n\n", device->ID(), pathString,
                        media, readWrite);
                print_partition_table_header();

                Visit(device, 0);
                return false;
        }
        
        virtual bool Visit(BPartition* partition, int32 level)
        {
                print_partition(partition, level, fIndex++);
                return false;
        }

private:
        int     fIndex;
};


// FindPartitionByIndexVisitor
class FindPartitionByIndexVisitor : public BDiskDeviceVisitor {
public:
        FindPartitionByIndexVisitor(int index)
                : fIndex(index)
        {
        }

        virtual bool Visit(BDiskDevice *device)
        {
                return Visit(device, 0);
        }
        
        virtual bool Visit(BPartition *partition, int32 level)
        {
                return (fIndex-- == 0);
        }

private:
        int     fIndex;
};


// Partitioner
class Partitioner {
public:
        Partitioner(BDiskDevice* device)
                : fDevice(device),
                  fPrepared(false)
        {
        }

        void Run()
        {
                // prepare device modifications
                if (fDevice->IsReadOnly()) {
                        printf("Device is read-only. Can't change anything.\n");
                } else {
                        status_t error = fDevice->PrepareModifications();
                        fPrepared = (error == B_OK);
                        if (error != B_OK) {
                                printf("Error: Failed to prepare device for modifications: "
                                        "%s\n", strerror(error));
                        }
                }

                // main input loop
                while (true) {
                        BString line;
                        if (!_ReadLine("party> ", line))
                                return;

                        if (line == "") {
                                // do nothing
                        } else if (line == "h" || line == "help") {
                                _PrintHelp();
                        } else if (line == "i") {
                                _InitializePartition();
                        } else if (line == "l") {
                                _PrintPartitionsShort();
                        } else if (line == "ll") {
                                _PrintPartitionsLong();
                        } else if (line == "n") {
                                _NewPartition();
                        } else if (line == "q" || line == "quit") {
                                return;
                        } else if (line == "w") {
                                _WriteChanges();
                        } else {
                                printf("Invalid command \"%s\", type \"help\" for help\n",
                                        line.String());
                        }
                }
        }

private:
        void _PrintHelp()
        {
                printf("Valid commands:\n"
                        "  h, help  - prints this help text\n"
                        "  i        - initialize a partition\n"
                        "  l        - lists the device's partitions recursively\n"
                        "  ll       - lists the device's partitions recursively, long "
                                "format\n"
                        "  l        - create a new child partition\n"
                        "  q, quit  - quits the program, changes are discarded\n"
                        "  w        - write changes to disk\n");
        }

        void _PrintPartitionsShort()
        {
                PrintShortVisitor visitor;
                fDevice->VisitEachDescendant(&visitor);
        }

        void _PrintPartitionsLong()
        {
                PrintLongVisitor visitor;
                fDevice->VisitEachDescendant(&visitor);
        }

        void _InitializePartition()
        {
                if (!fPrepared) {
                        if (fDevice->IsReadOnly())
                                printf("Device is read-only!\n");
                        else
                                printf("Sorry, not prepared for modifications!\n");
                        return;
                }

                // get the partition
                int32 partitionIndex;
                BPartition* partition = NULL;
                if (!_SelectPartition("partition index [-1 to abort]: ", partition,
                                partitionIndex)) {
                        return;
                }

                printf("\nselected partition:\n\n");
                print_partition_table_header();
                print_partition(partition, 0, partitionIndex);

                // get available disk systems
                BObjectList<BDiskSystem> diskSystems(20, true);
                BDiskDeviceRoster roster;
                {
                        BDiskSystem diskSystem;
                        while (roster.GetNextDiskSystem(&diskSystem) == B_OK) {
                                if (partition->CanInitialize(diskSystem.PrettyName()))
                                        diskSystems.AddItem(new BDiskSystem(diskSystem));
                        }
                }

                if (diskSystems.IsEmpty()) {
                        printf("There are no disk systems, that can initialize this "
                                "partition.\n");
                        return;
                }

                // print the available disk systems
                printf("\ndisk systems that can initialize the selected partition:\n");
                for (int32 i = 0; BDiskSystem* diskSystem = diskSystems.ItemAt(i); i++)
                        printf("%2" B_PRId32 "  %s\n", i, diskSystem->PrettyName());

                printf("\n");

                // get the disk system
                int64 diskSystemIndex;
                if (!_ReadNumber("disk system index [-1 to abort]: ", 0,
                                diskSystems.CountItems() - 1, -1, "invalid index",
                                diskSystemIndex)) {
                        return;
                }
                BDiskSystem* diskSystem = diskSystems.ItemAt(diskSystemIndex);

                bool supportsName = diskSystem->SupportsContentName();
                BString name;
                BString parameters;
                while (true) {
                        // let the user enter name and parameters
                        if ((supportsName && !_ReadLine("partition name: ", name))
                                || !_ReadLine("partition parameters: ", parameters)) {
                                return;
                        }

                        // validate parameters
                        BString validatedName(name);
                        if (partition->ValidateInitialize(diskSystem->PrettyName(),
                                        supportsName ? &validatedName : NULL, parameters.String())
                                        != B_OK) {
                                printf("Validation of the given values failed. Sorry, can't "
                                        "continue.\n");
                                return;
                        }

                        // did the disk system change the name?
                        if (!supportsName || name == validatedName) {
                                printf("Everything looks dandy.\n");
                        } else {
                                printf("The disk system adjusted the file name to \"%s\".\n",
                                        validatedName.String());
                                name = validatedName;
                        }

                        // let the user decide whether to continue, change parameters, or
                        // abort
                        bool changeParameters = false;
                        while (true) {
                                BString line;
                                _ReadLine("[c]ontinue, change [p]arameters, or [a]bort? ", line);
                                if (line == "a")
                                        return;
                                if (line == "p") {
                                        changeParameters = true;
                                        break;
                                }
                                if (line == "c")
                                        break;

                                printf("invalid input\n");
                        }

                        if (!changeParameters)
                                break;
                }

                // initialize
                status_t error = partition->Initialize(diskSystem->PrettyName(),
                        supportsName ? name.String() : NULL, parameters.String());
                if (error != B_OK)
                        printf("Initialization failed: %s\n", strerror(error));
        }

        void _NewPartition()
        {
                if (!fPrepared) {
                        if (fDevice->IsReadOnly())
                                printf("Device is read-only!\n");
                        else
                                printf("Sorry, not prepared for modifications!\n");
                        return;
                }

                // get the parent partition
                BPartition* partition = NULL;
                int32 partitionIndex;
                if (!_SelectPartition("parent partition index [-1 to abort]: ",
                                partition, partitionIndex)) {
                        return;
                }

                printf("\nselected partition:\n\n");
                print_partition_table_header();
                print_partition(partition, 0, partitionIndex);

                if (!partition->ContainsPartitioningSystem()) {
                        printf("The selected partition does not contain a partitioning "
                                "system.\n");
                        return;
                }

                // get supported types
                BObjectList<BString> supportedTypes(20, true);
                BString typeBuffer;
                int32 cookie = 0;
                while (partition->GetNextSupportedChildType(&cookie, &typeBuffer)
                                == B_OK) {
                        supportedTypes.AddItem(new BString(typeBuffer));
                }

                if (supportedTypes.IsEmpty()) {
                        printf("The partitioning system is not able to create any "
                                "child partition (no supported types).\n");
                        return;
                }

                // get partitioning info
                BPartitioningInfo partitioningInfo;
                status_t error = partition->GetPartitioningInfo(&partitioningInfo);
                if (error != B_OK) {
                        printf("Failed to get partitioning info for partition: %s\n",
                                strerror(error));
                        return;
                }

                int32 spacesCount = partitioningInfo.CountPartitionableSpaces();
                if (spacesCount == 0) {
                        printf("There's no space on the partition where a child partition "
                                "could be created\n");
                        return;
                }

                // let the user select the partition type, if there's more than one
                int64 typeIndex = 0;
                int32 supportedTypesCount = supportedTypes.CountItems();
                if (supportedTypesCount > 1) {
                        // list them
                        printf("Possible partition types:\n");
                        for (int32 i = 0; i < supportedTypesCount; i++)
                                printf("%2" B_PRId32 "  %s\n", i, supportedTypes.ItemAt(i)->String());

                        if (!_ReadNumber("supported type index [-1 to abort]: ", 0,
                                        supportedTypesCount - 1, -1, "invalid index", typeIndex)) {
                                return;
                        }
                }

                const char* type = supportedTypes.ItemAt(typeIndex)->String();

                // list partitionable spaces
                printf("Unused regions where the new partition could be created:\n");
                for (int32 i = 0; i < spacesCount; i++) {
                        off_t _offset;
                        off_t _size;
                        partitioningInfo.GetPartitionableSpaceAt(i, &_offset, &_size);
                        BString offset, size;
                        get_size_string(_offset, offset);
                        get_size_string(_size, size);
                        printf("%2" B_PRId32 "  start: %8s,  size:  %8s\n", i, offset.String(),
                                size.String());
                }

                // let the user select the partitionable space, if there's more than one
                int64 spaceIndex = 0;
                if (spacesCount > 1) {
                        if (!_ReadNumber("unused region index [-1 to abort]: ", 0,
                                        spacesCount - 1, -1, "invalid index", spaceIndex)) {
                                return;
                        }
                }

                off_t spaceOffset;
                off_t spaceSize;
                partitioningInfo.GetPartitionableSpaceAt(spaceIndex, &spaceOffset,
                        &spaceSize);

                off_t start;
                off_t size;
                BString parameters;
                while (true) {
                        // let the user enter start, size, and parameters

                        // start
                        while (true) {
                                BString spaceOffsetString;
                                get_size_string(spaceOffset, spaceOffsetString);
                                BString prompt("partition start [default: ");
                                prompt << spaceOffsetString << "]: ";
                                if (!_ReadSize(prompt.String(), spaceOffset, start))
                                        return;

                                if (start >= spaceOffset && start <= spaceOffset + spaceSize)
                                        break;

                                printf("invalid partition start\n");
                        }

                        // size
                        off_t maxSize = spaceOffset + spaceSize - start;
                        while (true) {
                                BString maxSizeString;
                                get_size_string(maxSize, maxSizeString);
                                BString prompt("partition size [default: ");
                                prompt << maxSizeString << "]: ";
                                if (!_ReadSize(prompt.String(), maxSize, size))
                                        return;

                                if (size >= 0 && start + size <= spaceOffset + spaceSize)
                                        break;

                                printf("invalid partition size\n");
                        }

                        // parameters
                        if (!_ReadLine("partition parameters: ", parameters))
                                return;

                        // validate parameters
                        off_t validatedStart = start;
                        off_t validatedSize = size;
// TODO: Support the name parameter!
                        if (partition->ValidateCreateChild(&start, &size, type, NULL,
                                        parameters.String()) != B_OK) {
                                printf("Validation of the given values failed. Sorry, can't "
                                        "continue.\n");
                                return;
                        }

                        // did the disk system change offset or size?
                        if (validatedStart == start && validatedSize == size) {
                                printf("Everything looks dandy.\n");
                        } else {
                                BString startString, sizeString;
                                get_size_string(validatedStart, startString);
                                get_size_string(validatedSize, sizeString);
                                printf("The disk system adjusted the partition start and "
                                        "size to %" B_PRIdOFF " (%s) and %" B_PRIdOFF " (%s).\n",
                                        validatedStart, startString.String(), validatedSize,
                                        sizeString.String());
                                start = validatedStart;
                                size = validatedSize;
                        }

                        // let the user decide whether to continue, change parameters, or
                        // abort
                        bool changeParameters = false;
                        while (true) {
                                BString line;
                                _ReadLine("[c]ontinue, change [p]arameters, or [a]bort? ", line);
                                if (line == "a")
                                        return;
                                if (line == "p") {
                                        changeParameters = true;
                                        break;
                                }
                                if (line == "c")
                                        break;

                                printf("invalid input\n");
                        }

                        if (!changeParameters)
                                break;
                }

                // create child
                error = partition->CreateChild(start, size, type, NULL,
                        parameters.String());
                if (error != B_OK)
                        printf("Creating the partition failed: %s\n", strerror(error));
        }

        void _WriteChanges()
        {
                if (!fPrepared || !fDevice->IsModified()) {
                        printf("No changes have been made!\n");
                        return;
                }

                printf("Writing changes to disk. This can take a while...\n");

                // commit
                status_t error = fDevice->CommitModifications();
                if (error == B_OK) {
                        printf("All changes have been written successfully!\n");
                } else {
                        printf("Failed to write all changes: %s\n", strerror(error));
                }

                // prepare again
                error = fDevice->PrepareModifications();
                fPrepared = (error == B_OK);
                if (error != B_OK) {
                        printf("Error: Failed to prepare device for modifications: "
                                "%s\n", strerror(error));
                }
        }

        bool _SelectPartition(const char* prompt, BPartition*& partition,
                int32& _partitionIndex)
        {
                // if the disk device has no children, we select it without asking
                if (fDevice->CountChildren() == 0) {
                        partition = fDevice;
                        _partitionIndex = 0;
                        return true;
                }

                // otherwise let the user select
                _PrintPartitionsShort();

                partition = NULL;
                int64 partitionIndex;
                while (true) {
                        if (!_ReadNumber(prompt, partitionIndex) || partitionIndex < 0)
                                return false;

                        FindPartitionByIndexVisitor visitor(partitionIndex);
                        partition = fDevice->VisitEachDescendant(&visitor);
                        if (partition) {
                                _partitionIndex = partitionIndex;
                                return true;
                        }

                        printf("invalid partition index\n");
                }
        }

        bool _ReadLine(const char* prompt, BString& _line)
        {
                // prompt
                printf(prompt);
                fflush(stdout);

                // read line
                char line[256];
                if (!fgets(line, sizeof(line), stdin))
                        return false;

                // remove trailing '\n'
                if (char* trailingNL = strchr(line, '\n'))
                        *trailingNL = '\0';

                _line = line;
                return true;
        }

        bool _ReadNumber(const char* prompt, int64& number)
        {
                while (true) {
                        BString line;
                        if (!_ReadLine(prompt, line))
                                return false;

                        char buffer[256];
                        if (sscanf(line.String(), "%" B_PRId64 "%s", &number, buffer) == 1)
                                return true;

                        printf("invalid input\n");
                }
        }

        bool _ReadNumber(const char* prompt, int64 minNumber, int64 maxNumber,
                int64 abortNumber, const char* invalidNumberMessage, int64& number)
        {
                while (true) {
                        BString line;
                        if (!_ReadLine(prompt, line))
                                return false;

                        char buffer[256];
                        if (sscanf(line.String(), "%" B_PRId64 "%s", &number, buffer) != 1) {
                                printf("invalid input\n");
                                continue;
                        }

                        if (number == abortNumber)
                                return false;

                        if (number >= minNumber && number <= maxNumber)
                                return true;

                        puts(invalidNumberMessage);
                }
        }

        bool _ReadSize(const char* prompt, off_t defaultValue, off_t& size)
        {
                while (true) {
                        BString _line;
                        if (!_ReadLine(prompt, _line))
                                return false;
                        const char* line = _line.String();

                        // skip whitespace
                        while (isspace(*line))
                                line++;

                        if (*line == '\0') {
                                size = defaultValue;
                                return true;
                        }

                        // get the number
                        int32 endIndex = 0;
                        while (isdigit(line[endIndex]))
                                endIndex++;

                        if (endIndex == 0) {
                                printf("invalid input\n");
                                continue;
                        }

                        size = atoll(BString(line, endIndex).String());

                        // skip whitespace
                        line += endIndex;
                        while (isspace(*line))
                                line++;

                        // get the size modifier
                        if (*line != '\0') {
                                switch (*line) {
                                        case 'K':
                                                size *= 1024;
                                                break;
                                        case 'M':
                                                size *= 1024 * 1024;
                                                break;
                                        case 'G':
                                                size *= 1024LL * 1024 * 1024;
                                                break;
                                        case 'T':
                                                size *= 1024LL * 1024 * 1024 * 1024;
                                                break;
                                        default:
                                                line--;
                                }
                
                                line++;
                        }

                        if (*line == 'B')
                                line++;

                        // skip whitespace
                        while (isspace(*line))
                                line++;

                        if (*line == '\0')
                                return true;

                        printf("invalid input\n");
                }
        }

private:
        BDiskDevice*    fDevice;
        bool                    fPrepared;
};


int
main(int argc, const char* const* argv)
{
        // parse arguments
        int argi = 1;
        for (; argi < argc; argi++) {
                const char* arg = argv[argi];
                if (arg[0] == '-') {
                        if (arg[1] == '-') {
                                // a double '-' option
                                if (strcmp(arg, "--help") == 0) {
                                        print_usage_and_exit(false);
                                } else
                                        print_usage_and_exit(true);
                        } else {
                                // single '-' options
                                for (int i = 1; arg[i] != '\0'; i++) {
                                        switch (arg[i]) {
                                                case 'h':
                                                        print_usage_and_exit(false);
                                                default:
                                                        print_usage_and_exit(true);
                                        }
                                }
                        }
                } else
                        break;
        }

        // only the device path should remain
        if (argi != argc - 1) 
                print_usage_and_exit(true);
        const char* devicePath = argv[argi];

        // get the disk device
        BDiskDeviceRoster roster;
        BDiskDevice device;
        status_t error = roster.GetDeviceForPath(devicePath, &device);
        if (error != B_OK) {
                fprintf(stderr, "Error: Failed to get disk device for path \"%s\": "
                        "%s\n", devicePath, strerror(error));
        }

        Partitioner partitioner(&device);
        partitioner.Run();

        return 0;
}