Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/build-linux.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,5 @@ jobs:
cmake -S . -B build
export MAKEFLAGS=-j$(nproc)
cmake --build build --verbose
- name: Test
run: ctest --test-dir build --output-on-failure
26 changes: 26 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ add_executable(${PROJECT_NAME}
main.cpp
Sources/audioMenu.cpp
Sources/config.cpp
Sources/directoryReader.cpp
Sources/fileBrowser.cpp
Sources/fileBrowserMenu.cpp
Sources/fileBrowserModel.cpp
Sources/font.cpp
Sources/ftpConnection.cpp
Sources/ftpServer.cpp
Expand All @@ -35,12 +39,15 @@ add_executable(${PROJECT_NAME}
Sources/sntpClient.cpp
Sources/subAppRouter.cpp
Sources/subsystems.cpp
Sources/textLayout.cpp
Sources/timing.cpp
Sources/timeMenu.cpp
Sources/videoMenu.cpp
Sources/wipeCache.cpp
Sources/xbeLauncher.cpp
Sources/xbeLaunchPath.cpp
Sources/xbeScanner.cpp
Sources/xbeValidator.cpp
3rdparty/SDL_FontCache/SDL_FontCache.c
)

Expand All @@ -58,5 +65,24 @@ else()
target_compile_options(${PROJECT_NAME} PRIVATE -Wall -Wextra -pedantic)
endif()

include(CTest)

if(BUILD_TESTING)
add_executable(FileBrowserModelTests
Tests/fileBrowserModelTests.cpp
Sources/directoryReader.cpp
Sources/fileBrowserModel.cpp
Sources/textLayout.cpp
Sources/xbeLaunchPath.cpp
Sources/xbeScanner.cpp
Sources/xbeValidator.cpp)
target_include_directories(FileBrowserModelTests PRIVATE
${CMAKE_SOURCE_DIR}
${PROJECT_SOURCE_DIR}/Includes)
set_property(TARGET FileBrowserModelTests PROPERTY CXX_STANDARD 11)
set_property(TARGET FileBrowserModelTests PROPERTY CXX_STANDARD_REQUIRED ON)
add_test(NAME FileBrowserModelTests COMMAND FileBrowserModelTests)
endif()

#set (CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS_DEBUG} -fno-omit-frame-pointer -fsanitize=address")
#set (CMAKE_LINKER_FLAGS "${CMAKE_LINKER_FLAGS_DEBUG} -fno-omit-frame-pointer -fsanitize=address")
51 changes: 51 additions & 0 deletions Includes/directoryReader.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
#ifndef NEVOLUTIONX_INCLUDES_DIRECTORYREADER_HPP_
#define NEVOLUTIONX_INCLUDES_DIRECTORYREADER_HPP_

#include <cstddef>
#include <cstdint>
#include <string>
#include <utility>
#include <vector>

enum class DirectoryEntryKind
{
DIRECTORY,
FILE,
OTHER
};

struct DirectoryEntry {
DirectoryEntry() = default;
DirectoryEntry(std::string entryName,
DirectoryEntryKind entryKind,
uint64_t entrySize = 0) :
name(std::move(entryName)), kind(entryKind), size(entrySize) {}

std::string name;
DirectoryEntryKind kind{ DirectoryEntryKind::OTHER };
uint64_t size{ 0 };
};

struct DirectoryReadResult {
bool succeeded{ false };
std::vector<DirectoryEntry> entries;
std::string error;
};

class DirectoryReader {
public:
virtual ~DirectoryReader() = default;

virtual DirectoryReadResult read(const std::string& path) const = 0;
virtual char separator() const = 0;
virtual size_t maxPathLength() const = 0;
};

class PlatformDirectoryReader : public DirectoryReader {
public:
DirectoryReadResult read(const std::string& path) const override;
char separator() const override;
size_t maxPathLength() const override;
};

#endif // NEVOLUTIONX_INCLUDES_DIRECTORYREADER_HPP_
41 changes: 41 additions & 0 deletions Includes/fileBrowser.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
#ifndef NEVOLUTIONX_INCLUDES_FILEBROWSER_HPP_
#define NEVOLUTIONX_INCLUDES_FILEBROWSER_HPP_

#include <memory>
#include <string>
#include <vector>
#include "fileBrowserModel.hpp"
#include "renderer.hpp"
#include "subApp.hpp"

class FileBrowser : public SubApp {
public:
FileBrowser(Renderer& renderer, const std::vector<std::string>& roots);

void render(Font& font) override;

void onUpPressed() override;
void onDownPressed() override;
void onLeftPressed() override;
void onRightPressed() override;
void onAPressed() override;
void onBPressed() override;
void onBackPressed() override;
void onXPressed() override;

void onLeftStickDigitalUpPressed() override { onUpPressed(); }
void onLeftStickDigitalDownPressed() override { onDownPressed(); }
void onLeftStickDigitalLeftPressed() override { onLeftPressed(); }
void onLeftStickDigitalRightPressed() override { onRightPressed(); }

private:
void navigateBack();
std::string displayLabel(const DirectoryEntry& entry) const;

Renderer& renderer;
std::shared_ptr<DirectoryReader> reader;
FileBrowserModel model;
size_t visibleRows{ 1 };
};

#endif // NEVOLUTIONX_INCLUDES_FILEBROWSER_HPP_
20 changes: 20 additions & 0 deletions Includes/fileBrowserMenu.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
#ifndef NEVOLUTIONX_INCLUDES_FILEBROWSERMENU_HPP_
#define NEVOLUTIONX_INCLUDES_FILEBROWSERMENU_HPP_

#include <string>
#include <vector>
#include "menu.hpp"

class FileBrowserMenu : public MenuItem {
public:
FileBrowserMenu(MenuNode* parent,
const std::string& label,
std::vector<std::string> roots);

void execute(Menu* menu) override;

private:
std::vector<std::string> roots;
};

#endif // NEVOLUTIONX_INCLUDES_FILEBROWSERMENU_HPP_
76 changes: 76 additions & 0 deletions Includes/fileBrowserModel.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
#ifndef NEVOLUTIONX_INCLUDES_FILEBROWSERMODEL_HPP_
#define NEVOLUTIONX_INCLUDES_FILEBROWSERMODEL_HPP_

#include <memory>
#include <string>
#include <vector>
#include "directoryReader.hpp"

class FileBrowserModel {
public:
enum class ActivateResult
{
NONE,
ENTERED_DIRECTORY,
FILE_SELECTED,
XBE_SELECTED,
FAILED
};

FileBrowserModel(std::vector<std::string> configuredRoots,
std::shared_ptr<DirectoryReader> reader);

const std::vector<DirectoryEntry>& getEntries() const { return entries; }
size_t getSelected() const { return selected; }
const std::string& getStatus() const { return status; }
std::string getLocationLabel() const;
bool isAtVirtualRoot() const { return atVirtualRoot; }

void moveSelection(int delta, bool allowWrap = true);
void pageSelection(int delta);
ActivateResult activateSelected(std::string* xbePath = nullptr);
bool navigateBack();
bool refresh();
void setStatus(const std::string& newStatus) { status = newStatus; }

static bool isSafeChildName(const std::string& name);
static bool entryLess(const DirectoryEntry& lhs, const DirectoryEntry& rhs);

private:
struct SelectionIdentity {
bool valid{ false };
std::string name;
DirectoryEntryKind kind{ DirectoryEntryKind::OTHER };
};

struct HistoryFrame {
bool virtualRoot{ true };
size_t rootIndex{ 0 };
std::vector<std::string> components;
SelectionIdentity selection;
};

static std::string trim(const std::string& value);
static bool equalsIgnoreCase(const std::string& lhs, const std::string& rhs);
std::vector<std::string> normalizeRoots(const std::vector<std::string>& roots) const;
bool normalizeRoot(const std::string& input, std::string& output) const;
static bool hasXbeExtension(const std::string& name);
std::string buildCurrentPath() const;
bool buildChildPath(const std::string& name, std::string& path) const;
bool loadCurrentDirectory(bool preserveOldListing);
void showVirtualRoot();
SelectionIdentity selectedIdentity() const;
void restoreSelection(const SelectionIdentity& identity);

std::shared_ptr<DirectoryReader> reader;
std::vector<std::string> roots;
std::vector<DirectoryEntry> entries;
std::vector<std::string> components;
std::vector<HistoryFrame> history;
size_t rootIndex{ 0 };
size_t selected{ 0 };
bool atVirtualRoot{ true };
std::string status;
};

#endif // NEVOLUTIONX_INCLUDES_FILEBROWSERMODEL_HPP_
13 changes: 13 additions & 0 deletions Includes/textLayout.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#ifndef NEVOLUTIONX_INCLUDES_TEXTLAYOUT_HPP_
#define NEVOLUTIONX_INCLUDES_TEXTLAYOUT_HPP_

#include <functional>
#include <string>

std::string sanitizeUtf8ForDisplay(const std::string& text);

std::string ellipsizeText(const std::string& text,
float maximumWidth,
const std::function<float(const std::string&)>& measure);

#endif // NEVOLUTIONX_INCLUDES_TEXTLAYOUT_HPP_
13 changes: 13 additions & 0 deletions Includes/xbeLaunchPath.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#ifndef NEVOLUTIONX_INCLUDES_XBELAUNCHPATH_HPP_
#define NEVOLUTIONX_INCLUDES_XBELAUNCHPATH_HPP_

#include <string>

struct XBEPreparedLaunchPath {
bool valid{ false };
std::string path;
};

XBEPreparedLaunchPath prepareXbeLaunchPath(const std::string& input);

#endif // NEVOLUTIONX_INCLUDES_XBELAUNCHPATH_HPP_
5 changes: 3 additions & 2 deletions Includes/xbeLauncher.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,9 @@ class XBELauncher {
// Returns to the dashboard. Should not return.
static void exitToDashboard();

// Launches the XBE at the given path. Should not return.
static void launch(std::string const& xbePath);
// Launches the XBE at the given path. Returns false if launch was refused or failed.
// A successful launch does not return.
static bool launch(std::string const& xbePath);

private:
// TODO(#113): Add support for a pre-launch image.
Expand Down
11 changes: 6 additions & 5 deletions Includes/xbeScanner.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
#include <thread>
#include <utility>
#include <vector>
#include "xbeValidator.hpp"

#ifdef NXDK
#include <windows.h>
Expand Down Expand Up @@ -41,6 +42,11 @@ class XBEScanner {
// XBEInfo instances for any XBEs that were discovered.
static void scanPath(std::string const& path, Callback&& callback);

// Produces display-safe scanner text, using the directory name when an XBE
// certificate has no title.
static std::string displayNameFor(const XBEValidationResult& validation,
const std::string& fallback);

private:
class QueueItem {
public:
Expand All @@ -52,8 +58,6 @@ class XBEScanner {
std::chrono::steady_clock::time_point scanStart;
long long scanDuration{ 0 };

const static int XBE_NAME_SIZE = 40;

std::string path;
Callback callback;
std::list<XBEInfo> results;
Expand All @@ -65,9 +69,6 @@ class XBEScanner {
HANDLE dirHandle{ INVALID_HANDLE_VALUE };
WIN32_FIND_DATAA findData{};
#endif

char xbeName[XBE_NAME_SIZE + 1]{ 0 };
std::vector<char> xbeData;
};

#ifdef SCANNER_THREADED
Expand Down
38 changes: 38 additions & 0 deletions Includes/xbeValidator.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
#ifndef NEVOLUTIONX_INCLUDES_XBEVALIDATOR_HPP_
#define NEVOLUTIONX_INCLUDES_XBEVALIDATOR_HPP_

#include <cstdint>
#include <string>
#include <vector>

enum class XBEValidationError
{
NONE,
OPEN_FAILED,
READ_FAILED,
FILE_TOO_SMALL,
BAD_MAGIC,
BAD_IMAGE_BASE,
INVALID_HEADER_SIZE,
HEADER_TOO_LARGE,
TRUNCATED_HEADER,
INVALID_CERTIFICATE
};

struct XBEValidationResult {
bool valid{ false };
XBEValidationError error{ XBEValidationError::NONE };
std::string title;

const char* message() const;
};

class XBEValidator {
public:
static XBEValidationResult validateFile(const std::string& path);
static XBEValidationResult validateBytes(const std::vector<uint8_t>& data);

static const size_t MAX_HEADER_SIZE = 4 * 1024 * 1024;
};

#endif // NEVOLUTIONX_INCLUDES_XBEVALIDATOR_HPP_
7 changes: 7 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ SRCS += \
$(SRCDIR)/audioMenu.cpp \
$(SRCDIR)/config.cpp \
$(SRCDIR)/font.cpp \
$(SRCDIR)/directoryReader.cpp \
$(SRCDIR)/fileBrowser.cpp \
$(SRCDIR)/fileBrowserMenu.cpp \
$(SRCDIR)/fileBrowserModel.cpp \
$(SRCDIR)/ftpConnection.cpp \
$(SRCDIR)/ftpServer.cpp \
$(SRCDIR)/infoLog.cpp \
Expand All @@ -24,10 +28,13 @@ SRCS += \
$(SRCDIR)/subsystems.cpp \
$(SRCDIR)/timeMenu.cpp \
$(SRCDIR)/timing.cpp \
$(SRCDIR)/textLayout.cpp \
$(SRCDIR)/videoMenu.cpp \
$(SRCDIR)/wipeCache.cpp \
$(SRCDIR)/xbeLauncher.cpp \
$(SRCDIR)/xbeLaunchPath.cpp \
$(SRCDIR)/xbeScanner.cpp \
$(SRCDIR)/xbeValidator.cpp \
$(CURDIR)/3rdparty/SDL_FontCache/SDL_FontCache.c

NXDK_DIR ?= $(CURDIR)/../nxdk
Expand Down
Loading