From 8c34fd307402f2e48a9d0197856d07a4d7b5175f Mon Sep 17 00:00:00 2001 From: Mokapi48 Date: Mon, 20 Jul 2026 22:31:23 +0200 Subject: [PATCH] Add massives changes and fixes bugs --- .../SimpleModManager/CMakeLists.txt | 3 +- .../include/SimpleModManager.h | 4 +- .../SimpleModManager/src/SimpleModManager.cpp | 422 ++++- .../src/SimpleModManagerConsole.cpp | 19 + src/ModManagerCore/include/ConfigHandler.h | 41 + src/ModManagerCore/include/GameBrowser.h | 6 + src/ModManagerCore/include/ModManager.h | 45 + src/ModManagerCore/include/Toolbox.h | 15 + src/ModManagerCore/src/ConfigHandler.cpp | 46 +- src/ModManagerCore/src/GameBrowser.cpp | 402 ++++- src/ModManagerCore/src/ModManager.cpp | 1093 +++++++++++- src/ModManagerCore/src/Toolbox.cpp | 456 ++++- .../CoreExtension/CMakeLists.txt | 1 + .../CoreExtension/include/GuiModManager.h | 36 +- .../include/SystemStatusOverlay.h | 15 + .../CoreExtension/src/GuiModManager.cpp | 1104 +++++++++++- .../CoreExtension/src/SystemStatusOverlay.cpp | 124 ++ .../FrameGameBrowser/CMakeLists.txt | 25 +- .../FrameGameBrowser/include/FrameRoot.h | 1 + .../FrameGameBrowser/include/ModsMtpServer.h | 19 + .../FrameGameBrowser/include/TabGames.h | 17 + .../FrameGameBrowser/include/TabImportMod.h | 34 + .../FrameGameBrowser/src/FrameRoot.cpp | 8 + .../FrameGameBrowser/src/ModsMtpServer.cpp | 756 ++++++++ .../FrameGameBrowser/src/TabGames.cpp | 190 ++- .../src/TabGeneralSettings.cpp | 72 +- .../FrameGameBrowser/src/TabImportMod.cpp | 144 ++ .../FrameModBrowser/include/FrameModBrowser.h | 9 + .../FrameModBrowser/include/TabModBrowser.h | 7 + .../FrameModBrowser/src/FrameModBrowser.cpp | 87 +- .../FrameModBrowser/src/TabModBrowser.cpp | 314 +++- .../mtp-server-nx/include/MtpDataPacket.h | 109 ++ .../mtp-server-nx/include/MtpDatabase.h | 131 ++ .../mtp-server-nx/include/MtpDebug.h | 34 + .../mtp-server-nx/include/MtpDeviceInfo.h | 54 + .../mtp-server-nx/include/MtpEventPacket.h | 41 + .../mtp-server-nx/include/MtpObjectInfo.h | 60 + .../mtp-server-nx/include/MtpPacket.h | 67 + .../mtp-server-nx/include/MtpProperty.h | 114 ++ .../mtp-server-nx/include/MtpRequestPacket.h | 41 + .../mtp-server-nx/include/MtpResponsePacket.h | 41 + .../mtp-server-nx/include/MtpServer.h | 169 ++ .../mtp-server-nx/include/MtpStorage.h | 61 + .../mtp-server-nx/include/MtpStorageInfo.h | 49 + .../mtp-server-nx/include/MtpStringBuffer.h | 57 + .../mtp-server-nx/include/MtpTypes.h | 88 + .../mtp-server-nx/include/MtpUtils.h | 29 + .../mtp-server-nx/include/SwitchMtpDatabase.h | 1365 +++++++++++++++ .../mtp-server-nx/include/USBMtpInterface.h | 71 + .../include/USBSerialInterface.h | 61 + src/ThirdParty/mtp-server-nx/include/log.h | 37 + src/ThirdParty/mtp-server-nx/include/mtp.h | 492 ++++++ src/ThirdParty/mtp-server-nx/include/nxlink.h | 27 + src/ThirdParty/mtp-server-nx/include/usb.h | 45 + .../mtp-server-nx/source/MtpDataPacket.cpp | 411 +++++ .../mtp-server-nx/source/MtpDebug.cpp | 402 +++++ .../mtp-server-nx/source/MtpDeviceInfo.cpp | 107 ++ .../mtp-server-nx/source/MtpEventPacket.cpp | 45 + .../mtp-server-nx/source/MtpObjectInfo.cpp | 120 ++ .../mtp-server-nx/source/MtpPacket.cpp | 151 ++ .../mtp-server-nx/source/MtpProperty.cpp | 555 ++++++ .../mtp-server-nx/source/MtpRequestPacket.cpp | 45 + .../source/MtpResponsePacket.cpp | 43 + .../mtp-server-nx/source/MtpServer.cpp | 1513 +++++++++++++++++ .../mtp-server-nx/source/MtpStorage.cpp | 106 ++ .../mtp-server-nx/source/MtpStorageInfo.cpp | 80 + .../mtp-server-nx/source/MtpStringBuffer.cpp | 171 ++ .../mtp-server-nx/source/MtpUtils.cpp | 82 + .../mtp-server-nx/source/USBMtpInterface.cpp | 71 + .../source/USBSerialInterface.cpp | 41 + src/ThirdParty/mtp-server-nx/source/log.cpp | 28 + src/ThirdParty/mtp-server-nx/source/main.cpp | 209 +++ .../mtp-server-nx/source/nxlink.cpp | 69 + src/ThirdParty/mtp-server-nx/source/usb.c | 433 +++++ submodules/borealis | 2 +- submodules/cpp-generic-toolbox | 2 +- submodules/libtesla | 2 +- submodules/simple-cpp-logger | 2 +- 78 files changed, 13209 insertions(+), 239 deletions(-) create mode 100644 src/ModManagerGui/CoreExtension/include/SystemStatusOverlay.h create mode 100644 src/ModManagerGui/CoreExtension/src/SystemStatusOverlay.cpp create mode 100644 src/ModManagerGui/FrameGameBrowser/include/ModsMtpServer.h create mode 100644 src/ModManagerGui/FrameGameBrowser/include/TabImportMod.h create mode 100644 src/ModManagerGui/FrameGameBrowser/src/ModsMtpServer.cpp create mode 100644 src/ModManagerGui/FrameGameBrowser/src/TabImportMod.cpp create mode 100644 src/ThirdParty/mtp-server-nx/include/MtpDataPacket.h create mode 100644 src/ThirdParty/mtp-server-nx/include/MtpDatabase.h create mode 100644 src/ThirdParty/mtp-server-nx/include/MtpDebug.h create mode 100644 src/ThirdParty/mtp-server-nx/include/MtpDeviceInfo.h create mode 100644 src/ThirdParty/mtp-server-nx/include/MtpEventPacket.h create mode 100644 src/ThirdParty/mtp-server-nx/include/MtpObjectInfo.h create mode 100644 src/ThirdParty/mtp-server-nx/include/MtpPacket.h create mode 100644 src/ThirdParty/mtp-server-nx/include/MtpProperty.h create mode 100644 src/ThirdParty/mtp-server-nx/include/MtpRequestPacket.h create mode 100644 src/ThirdParty/mtp-server-nx/include/MtpResponsePacket.h create mode 100644 src/ThirdParty/mtp-server-nx/include/MtpServer.h create mode 100644 src/ThirdParty/mtp-server-nx/include/MtpStorage.h create mode 100644 src/ThirdParty/mtp-server-nx/include/MtpStorageInfo.h create mode 100644 src/ThirdParty/mtp-server-nx/include/MtpStringBuffer.h create mode 100644 src/ThirdParty/mtp-server-nx/include/MtpTypes.h create mode 100644 src/ThirdParty/mtp-server-nx/include/MtpUtils.h create mode 100644 src/ThirdParty/mtp-server-nx/include/SwitchMtpDatabase.h create mode 100644 src/ThirdParty/mtp-server-nx/include/USBMtpInterface.h create mode 100644 src/ThirdParty/mtp-server-nx/include/USBSerialInterface.h create mode 100644 src/ThirdParty/mtp-server-nx/include/log.h create mode 100644 src/ThirdParty/mtp-server-nx/include/mtp.h create mode 100644 src/ThirdParty/mtp-server-nx/include/nxlink.h create mode 100644 src/ThirdParty/mtp-server-nx/include/usb.h create mode 100644 src/ThirdParty/mtp-server-nx/source/MtpDataPacket.cpp create mode 100644 src/ThirdParty/mtp-server-nx/source/MtpDebug.cpp create mode 100644 src/ThirdParty/mtp-server-nx/source/MtpDeviceInfo.cpp create mode 100644 src/ThirdParty/mtp-server-nx/source/MtpEventPacket.cpp create mode 100644 src/ThirdParty/mtp-server-nx/source/MtpObjectInfo.cpp create mode 100644 src/ThirdParty/mtp-server-nx/source/MtpPacket.cpp create mode 100644 src/ThirdParty/mtp-server-nx/source/MtpProperty.cpp create mode 100644 src/ThirdParty/mtp-server-nx/source/MtpRequestPacket.cpp create mode 100644 src/ThirdParty/mtp-server-nx/source/MtpResponsePacket.cpp create mode 100644 src/ThirdParty/mtp-server-nx/source/MtpServer.cpp create mode 100644 src/ThirdParty/mtp-server-nx/source/MtpStorage.cpp create mode 100644 src/ThirdParty/mtp-server-nx/source/MtpStorageInfo.cpp create mode 100644 src/ThirdParty/mtp-server-nx/source/MtpStringBuffer.cpp create mode 100644 src/ThirdParty/mtp-server-nx/source/MtpUtils.cpp create mode 100644 src/ThirdParty/mtp-server-nx/source/USBMtpInterface.cpp create mode 100644 src/ThirdParty/mtp-server-nx/source/USBSerialInterface.cpp create mode 100644 src/ThirdParty/mtp-server-nx/source/log.cpp create mode 100644 src/ThirdParty/mtp-server-nx/source/main.cpp create mode 100644 src/ThirdParty/mtp-server-nx/source/nxlink.cpp create mode 100644 src/ThirdParty/mtp-server-nx/source/usb.c diff --git a/src/Applications/SimpleModManager/CMakeLists.txt b/src/Applications/SimpleModManager/CMakeLists.txt index 1832970..929ef9f 100644 --- a/src/Applications/SimpleModManager/CMakeLists.txt +++ b/src/Applications/SimpleModManager/CMakeLists.txt @@ -30,7 +30,7 @@ target_link_libraries( -L/opt/devkitpro/libnx/lib ${ZLIB_LIBRARIES} ${FREETYPE_LIBRARIES} - -lglfw3 -lEGL -lglad -lglapi -ldrm_nouveau -lm -lnx + -lminizip -lz -lbz2 -lglfw3 -lEGL -lglad -lglapi -ldrm_nouveau -lm -lnx ) set_target_properties(${GUI_APP}.elf PROPERTIES @@ -61,4 +61,3 @@ build_switch_binaries( - diff --git a/src/Applications/SimpleModManager/include/SimpleModManager.h b/src/Applications/SimpleModManager/include/SimpleModManager.h index 819393d..c3b63cd 100644 --- a/src/Applications/SimpleModManager/include/SimpleModManager.h +++ b/src/Applications/SimpleModManager/include/SimpleModManager.h @@ -5,7 +5,9 @@ #ifndef SIMPLEMODMANAGER_SIMPLEMODMANAGER_H #define SIMPLEMODMANAGER_SIMPLEMODMANAGER_H -void runGui(); +#include + +void runGui(const std::string& modsRootFolder_); #endif //SIMPLEMODMANAGER_SIMPLEMODMANAGER_H diff --git a/src/Applications/SimpleModManager/src/SimpleModManager.cpp b/src/Applications/SimpleModManager/src/SimpleModManager.cpp index a8d8524..b7680ee 100644 --- a/src/Applications/SimpleModManager/src/SimpleModManager.cpp +++ b/src/Applications/SimpleModManager/src/SimpleModManager.cpp @@ -6,15 +6,25 @@ #include "SimpleModManager.h" #include +#include +#include +#include +#include +#include #include "ConsoleHandler.h" #include "ConfigHandler.h" +#include "Toolbox.h" #include "Logger.h" #include +#include +#include +#include #include +#include #include #include "iostream" @@ -25,10 +35,400 @@ LoggerInit([]{ Logger::setUserHeaderStr("[SimpleModManager.nro]"); }); +namespace { + +constexpr int kTouchScreenWidth = 1280; +constexpr int kTouchScreenHeight = 720; +constexpr int kTouchTapSlopPx = 24; +constexpr int kTouchNavigationStepPx = 56; +constexpr int kSettingsTouchNavigationStepPx = 104; + +struct TouchState { + bool touching{false}; + bool dragging{false}; + int startX{0}; + int startY{0}; + int lastX{0}; + int lastY{0}; + int dragAccumulatorY{0}; +}; + +enum class PendingTouchType { + None, + View, + DialogButton +}; + +struct PendingTouchSelection { + PendingTouchType type{PendingTouchType::None}; + brls::View* topView{nullptr}; + brls::View* view{nullptr}; + brls::Key key{brls::Key::A}; + int dialogButtonIndex{-1}; +}; + +PendingTouchSelection gPendingTouchSelection; + +void clearPendingTouchSelection() { + gPendingTouchSelection = PendingTouchSelection(); +} + +bool isSamePendingView(brls::View* topView_, brls::View* view_) { + return gPendingTouchSelection.type == PendingTouchType::View + && gPendingTouchSelection.topView == topView_ + && gPendingTouchSelection.view == view_; +} + +bool isSamePendingDialogButton(brls::View* topView_, int buttonIndex_) { + return gPendingTouchSelection.type == PendingTouchType::DialogButton + && gPendingTouchSelection.topView == topView_ + && gPendingTouchSelection.dialogButtonIndex == buttonIndex_; +} + +void dispatchTouchButton(char button_) { + brls::Application::onGamepadButtonPressed( button_, false ); +} + +void giveTouchFocus(brls::View* view_) { + brls::Application::giveFocus( view_ ); +} + +bool isPointInsideView(brls::View* view_, int x_, int y_) { + if( view_ == nullptr || view_->isHidden() || view_->isCollapsed() ){ + return false; + } + + return x_ >= view_->getX() + && y_ >= view_->getY() + && x_ < view_->getX() + static_cast(view_->getWidth()) + && y_ < view_->getY() + static_cast(view_->getHeight()); +} + +brls::View* findActiveTabView(brls::TabFrame* tabFrame_) { + if( tabFrame_ == nullptr || tabFrame_->sidebar == nullptr ){ + return nullptr; + } + + for( size_t i = 0; i < tabFrame_->sidebar->getViewsCount(); ++i ){ + auto* item = dynamic_cast(tabFrame_->sidebar->getChild(i)); + if( item != nullptr && item->isActive() ){ + return item->getAssociatedView(); + } + } + + return nullptr; +} + +bool shouldFocusTabContentOnTouch(brls::View* view_) { + return dynamic_cast(view_) != nullptr + || dynamic_cast(view_) != nullptr + || dynamic_cast(view_) != nullptr; +} + +bool isSettingsTabActive() { + auto* tabFrame = dynamic_cast(brls::Application::getTopStackView()); + return dynamic_cast(findActiveTabView(tabFrame)) != nullptr; +} + +brls::View* findTouchableView(brls::View* view_, int x_, int y_) { + if( !isPointInsideView(view_, x_, y_) ){ + return nullptr; + } + + if( auto* tabFrame = dynamic_cast(view_) ){ + if( auto* sidebarHit = findTouchableView(tabFrame->sidebar, x_, y_) ){ + return sidebarHit; + } + if( auto* activeView = findActiveTabView(tabFrame) ){ + if( auto* activeHit = findTouchableView(activeView, x_, y_) ){ + return activeHit; + } + } + } + + if( auto* scrollView = dynamic_cast(view_) ){ + if( auto* contentView = scrollView->getContentView() ){ + if( auto* contentHit = findTouchableView(contentView, x_, y_) ){ + return contentHit; + } + } + } + + if( auto* boxLayout = dynamic_cast(view_) ){ + for( size_t i = boxLayout->getViewsCount(); i > 0; --i ){ + if( auto* childHit = findTouchableView(boxLayout->getChild(i - 1), x_, y_) ){ + return childHit; + } + } + } + + return view_->getDefaultFocus() == view_ ? view_ : nullptr; +} + +bool touchActionsSortFunc(brls::Action a_, brls::Action b_) { + if( a_.key == brls::Key::PLUS ){ + return true; + } + if( b_.key == brls::Key::A ){ + return true; + } + if( b_.key == brls::Key::B && a_.key != brls::Key::A ){ + return true; + } + return false; +} + +std::vector collectVisibleActions() { + std::vector actions; + std::set addedKeys; + + brls::View* focusParent = brls::Application::getCurrentFocus(); + if( focusParent == nullptr ){ + focusParent = brls::Application::getTopStackView(); + } + + while( focusParent != nullptr ){ + for( const auto& action : focusParent->getActions() ){ + if( action.hidden || !action.available || action.hintText.empty() ){ + continue; + } + if( addedKeys.find(action.key) != addedKeys.end() ){ + continue; + } + + addedKeys.insert(action.key); + actions.emplace_back(action); + } + focusParent = focusParent->getParent(); + } + + std::stable_sort(actions.begin(), actions.end(), touchActionsSortFunc); + return actions; +} + +const char* getTouchActionIcon(brls::Key key_) { + switch( key_ ){ + case brls::Key::A: return "\uE0E0"; + case brls::Key::B: return "\uE0E1"; + case brls::Key::X: return "\uE0E2"; + case brls::Key::Y: return "\uE0E3"; + case brls::Key::L: return "\uE0E4"; + case brls::Key::R: return "\uE0E5"; + case brls::Key::PLUS: return "\uE0EF"; + case brls::Key::MINUS: return "\uE0F0"; + case brls::Key::DLEFT: return "\uE0ED"; + case brls::Key::DUP: return "\uE0EB"; + case brls::Key::DRIGHT: return "\uE0EF"; + case brls::Key::DDOWN: return "\uE0EC"; + default: return "\uE152"; + } +} + +float measureTouchHintWidth(const std::string& text_) { + auto* vg = brls::Application::getNVGContext(); + auto* style = brls::Application::getStyle(); + auto* stash = brls::Application::getFontStash(); + if( vg == nullptr || style == nullptr || stash == nullptr ){ + return static_cast(text_.size() * 12 + 36); + } + + float bounds[4]{}; + nvgFontSize(vg, style->Label.hintFontSize); + nvgFontFaceId(vg, stash->regular); + nvgTextAlign(vg, NVG_ALIGN_LEFT | NVG_ALIGN_MIDDLE); + nvgTextBounds(vg, 0, 0, text_.c_str(), nullptr, bounds); + return bounds[2] - bounds[0]; +} + +bool handleFooterTouch(brls::View* topView_, int x_, int y_) { + auto* style = brls::Application::getStyle(); + if( style == nullptr ){ + return false; + } + + const int footerTop = static_cast(brls::Application::contentHeight) - static_cast(style->AppletFrame.footerHeight); + if( y_ < footerTop ){ + return false; + } + + auto actions = collectVisibleActions(); + if( actions.empty() ){ + return false; + } + + float right = static_cast(brls::Application::contentWidth + - style->AppletFrame.separatorSpacing + - style->AppletFrame.footerTextSpacing); + const float spacing = static_cast(style->AppletFrame.footerTextSpacing); + + for( auto it = actions.rbegin(); it != actions.rend(); ++it ){ + const std::string label = std::string(getTouchActionIcon(it->key)) + " " + it->hintText; + const float width = measureTouchHintWidth(label) + 16.0f; + const float left = right - width; + + if( x_ >= left - 8.0f && x_ <= right + 8.0f ){ + clearPendingTouchSelection(); + dispatchTouchButton(static_cast(it->key)); + return true; + } + + right = left - spacing; + } + + return false; +} + +bool handleDialogTouch(brls::View* topView_, int x_, int y_) { + if( dynamic_cast(topView_) == nullptr ){ + return false; + } + + auto* style = brls::Application::getStyle(); + if( style == nullptr ){ + return false; + } + + const int buttonHeight = static_cast(style->Dialog.buttonHeight); + const int frameWidth = static_cast(style->Dialog.width); + const int frameHeight = static_cast(style->Dialog.height) + buttonHeight; + const int frameX = static_cast(topView_->getWidth()) / 2 - frameWidth / 2; + const int frameY = static_cast(topView_->getHeight()) / 2 - frameHeight / 2; + const int buttonTop = frameY + static_cast(style->Dialog.height); + const int buttonBottom = static_cast(topView_->getHeight()); + + if( x_ < frameX || x_ >= frameX + frameWidth || y_ < buttonTop || y_ >= buttonBottom ){ + return false; + } + + const int buttonIndex = x_ < frameX + frameWidth / 2 ? 0 : 1; + dispatchTouchButton(buttonIndex == 0 ? GLFW_GAMEPAD_BUTTON_DPAD_LEFT : GLFW_GAMEPAD_BUTTON_DPAD_RIGHT); + + if( !isSamePendingDialogButton(topView_, buttonIndex) ){ + gPendingTouchSelection.type = PendingTouchType::DialogButton; + gPendingTouchSelection.topView = topView_; + gPendingTouchSelection.dialogButtonIndex = buttonIndex; + return true; + } + + clearPendingTouchSelection(); + dispatchTouchButton(GLFW_GAMEPAD_BUTTON_A); + return true; +} + +bool handleContentTouch(int x_, int y_) { + auto* topView = brls::Application::getTopStackView(); + if( topView == nullptr || brls::Application::hasViewDisappearing() ){ + return false; + } + + if( handleDialogTouch(topView, x_, y_) ){ + return true; + } + if( handleFooterTouch(topView, x_, y_) ){ + return true; + } + + auto* touchedView = findTouchableView(topView, x_, y_); + if( touchedView == nullptr ){ + return false; + } + + auto* focusTarget = touchedView->getDefaultFocus(); + if( focusTarget == nullptr ){ + return false; + } + + if( dynamic_cast(focusTarget) != nullptr ){ + clearPendingTouchSelection(); + brls::Application::giveFocus(focusTarget); + auto* sidebarItem = dynamic_cast(focusTarget); + auto* associatedView = sidebarItem != nullptr ? sidebarItem->getAssociatedView() : nullptr; + auto* tabContentFocus = associatedView != nullptr ? associatedView->getDefaultFocus() : nullptr; + if( shouldFocusTabContentOnTouch(associatedView) && tabContentFocus != nullptr ){ + brls::Application::giveFocus(tabContentFocus); + } + return true; + } + + if( !isSamePendingView(topView, focusTarget) || brls::Application::getCurrentFocus() != focusTarget ){ + giveTouchFocus(focusTarget); + gPendingTouchSelection.type = PendingTouchType::View; + gPendingTouchSelection.topView = topView; + gPendingTouchSelection.view = focusTarget; + return true; + } + + clearPendingTouchSelection(); + dispatchTouchButton(GLFW_GAMEPAD_BUTTON_A); + return true; +} + +void processTouchInput() { + HidTouchScreenState touchScreenState{}; + if( hidGetTouchScreenStates(&touchScreenState, 1) == 0 ){ + return; + } + + static TouchState touchState; + const bool isTouching = touchScreenState.count > 0; + + if( isTouching ){ + const auto& touch = touchScreenState.touches[0]; + const int x = static_cast(std::lround( + static_cast(touch.x) * static_cast(brls::Application::contentWidth) / kTouchScreenWidth)); + const int y = static_cast(std::lround( + static_cast(touch.y) * static_cast(brls::Application::contentHeight) / kTouchScreenHeight)); + + if( !touchState.touching ){ + touchState.touching = true; + touchState.dragging = false; + touchState.startX = touchState.lastX = x; + touchState.startY = touchState.lastY = y; + touchState.dragAccumulatorY = 0; + return; + } + + const int totalDx = x - touchState.startX; + const int totalDy = y - touchState.startY; + if( !touchState.dragging && (std::abs(totalDx) > kTouchTapSlopPx || std::abs(totalDy) > kTouchTapSlopPx) ){ + clearPendingTouchSelection(); + touchState.dragging = true; + } + + if( touchState.dragging ){ + touchState.dragAccumulatorY += y - touchState.lastY; + const int navigationStep = isSettingsTabActive() ? kSettingsTouchNavigationStepPx : kTouchNavigationStepPx; + while( std::abs(touchState.dragAccumulatorY) >= navigationStep ){ + dispatchTouchButton(touchState.dragAccumulatorY < 0 ? GLFW_GAMEPAD_BUTTON_DPAD_DOWN : GLFW_GAMEPAD_BUTTON_DPAD_UP); + touchState.dragAccumulatorY += touchState.dragAccumulatorY < 0 ? navigationStep : -navigationStep; + } + } + + touchState.lastX = x; + touchState.lastY = y; + return; + } + + if( touchState.touching ){ + const bool wasTap = !touchState.dragging + && std::abs(touchState.lastX - touchState.startX) <= kTouchTapSlopPx + && std::abs(touchState.lastY - touchState.startY) <= kTouchTapSlopPx; + if( wasTap ){ + handleContentTouch(touchState.lastX, touchState.lastY); + } + } + + touchState = TouchState(); +} + +} + int main(int argc, char* argv[]){ LogInfo << "SimpleModManager is starting..." << std::endl; + Toolbox::ensureModsRootFolder(); + // https://github.com/jbeder/yaml-cpp/wiki/Tutorial // YAML::Node config = YAML::LoadFile("config.yaml"); // if (config["lastLogin"]) { @@ -38,11 +438,21 @@ int main(int argc, char* argv[]){ // const auto password = config["password"].as(); ConfigHandler c; - if( c.getConfig().useGui ){ runGui(); } + if( c.getConfig().useGui ){ runGui(c.getConfig().baseFolder); } else{ + const Result nsRc = nsInitialize(); + if( R_SUCCEEDED(nsRc) ) { + Toolbox::ensureInstalledGameModFolders(c.getConfig().baseFolder); + } + else { + LogError << "nsInitialize Failed: 0x" << std::hex << nsRc << std::dec << std::endl; + } consoleInit(nullptr); ConsoleHandler::run(); consoleExit(nullptr); + if( R_SUCCEEDED(nsRc) ) { + nsExit(); + } } // Exit @@ -50,14 +460,16 @@ int main(int argc, char* argv[]){ } -void runGui(){ +void runGui(const std::string& modsRootFolder_){ LogInfo << "Starting GUI..." << std::endl; LogThrowIf(R_FAILED(nsInitialize()), "nsInitialize Failed"); + Toolbox::ensureInstalledGameModFolders(modsRootFolder_); brls::Logger::setLogLevel(brls::LogLevel::ERROR); brls::i18n::loadTranslations("en-US"); LogThrowIf(not brls::Application::init("SimpleModManager"), "Unable to init Borealis application"); + hidInitializeTouchScreen(); LogInfo << "Creating root frame..." << std::endl; auto* mainFrame = new FrameRoot(); @@ -67,7 +479,11 @@ void runGui(){ mainFrame->registerAction( "", brls::Key::PLUS, []{return true;}, true ); mainFrame->updateActionHint( brls::Key::PLUS, "" ); // make the change visible - while( brls::Application::mainLoop() ){ } + while( brls::Application::mainLoop() ){ + processTouchInput(); + } + SystemStatusOverlay::shutdown(); + ModsMtpServer::shutdownForAppExit(); nsExit(); } diff --git a/src/Applications/SimpleModManagerConsole/src/SimpleModManagerConsole.cpp b/src/Applications/SimpleModManagerConsole/src/SimpleModManagerConsole.cpp index 0b4f784..1a59539 100644 --- a/src/Applications/SimpleModManagerConsole/src/SimpleModManagerConsole.cpp +++ b/src/Applications/SimpleModManagerConsole/src/SimpleModManagerConsole.cpp @@ -1,17 +1,36 @@ #include "ConsoleHandler.h" +#include "ConfigHandler.h" +#include "Toolbox.h" + +#include "Logger.h" #include #include "chrono" +#include "iostream" #include "thread" // MAIN int main( int argc, char **argv ){ + Toolbox::ensureModsRootFolder(); + ConfigHandler config; + + const Result nsRc = nsInitialize(); + if( R_SUCCEEDED(nsRc) ) { + Toolbox::ensureInstalledGameModFolders(config.getConfig().baseFolder); + } + else { + LogError << "nsInitialize Failed: 0x" << std::hex << nsRc << std::dec << std::endl; + } + consoleInit(nullptr); ConsoleHandler::run(); consoleExit(nullptr); + if( R_SUCCEEDED(nsRc) ) { + nsExit(); + } return EXIT_SUCCESS; } diff --git a/src/ModManagerCore/include/ConfigHandler.h b/src/ModManagerCore/include/ConfigHandler.h index 6f9f939..ad24f20 100644 --- a/src/ModManagerCore/include/ConfigHandler.h +++ b/src/ModManagerCore/include/ConfigHandler.h @@ -27,11 +27,24 @@ struct ConfigHolder{ #define ENUM_FIELDS \ ENUM_FIELD( NbMods, 0 ) \ ENUM_FIELD( Alphabetical ) \ + ENUM_FIELD( GameLaunched ) \ + ENUM_FIELD( ModAdded ) \ + ENUM_FIELD( PlayTime ) \ + ENUM_FIELD( LaunchCount ) \ ENUM_FIELD( NoSort ) #include "GenericToolbox.MakeEnum.h" +#define ENUM_NAME SortGameListDirection +#define ENUM_FIELDS \ + ENUM_FIELD( Ascending, 0 ) \ + ENUM_FIELD( Descending ) +#include "GenericToolbox.MakeEnum.h" + bool useGui{true}; + bool showDebugMtpFiles{false}; + bool offerOrphanInstalledModCleanup{true}; SortGameList sortGameList{SortGameList::NbMods}; + SortGameListDirection sortGameListDirection{SortGameListDirection::Ascending}; std::string baseFolder{"/mods"}; int selectedPresetIndex{0}; std::vector presetList{ @@ -48,11 +61,39 @@ struct ConfigHolder{ void setSelectedPreset(const std::string& preset_); [[nodiscard]] std::string getCurrentPresetName() const; [[nodiscard]] const PresetConfig& getCurrentPreset() const { return presetList[selectedPresetIndex]; } + [[nodiscard]] std::string getSortGameListDisplayName() const { + switch( sortGameList.value ) { + case SortGameList::Alphabetical: return "Alphabetical"; + case SortGameList::NbMods: return "Number of mods"; + case SortGameList::GameLaunched: return "Game launched"; + case SortGameList::ModAdded: return "Mod added"; + case SortGameList::PlayTime: return "Play time"; + case SortGameList::LaunchCount: return "Launch count"; + case SortGameList::NoSort: return "No sort"; + default: return sortGameList.toString(); + } + } + [[nodiscard]] std::string getSortGameListDirectionDisplayName() const { + switch( sortGameListDirection.value ) { + case SortGameListDirection::Ascending: return "Ascending"; + case SortGameListDirection::Descending: return "Descending"; + default: return sortGameListDirection.toString(); + } + } + [[nodiscard]] std::string getSortGameListSettingDisplayName() const { + if( sortGameList == SortGameList::NoSort ){ + return getSortGameListDisplayName(); + } + return getSortGameListDisplayName() + " (" + getSortGameListDirectionDisplayName() + ")"; + } [[nodiscard]] std::string getSummary() const { std::stringstream ss; ss << GET_VAR_NAME_VALUE(useGui) << std::endl; + ss << GET_VAR_NAME_VALUE(showDebugMtpFiles) << std::endl; + ss << GET_VAR_NAME_VALUE(offerOrphanInstalledModCleanup) << std::endl; ss << GET_VAR_NAME_VALUE(sortGameList.toString()) << std::endl; + ss << GET_VAR_NAME_VALUE(sortGameListDirection.toString()) << std::endl; ss << GET_VAR_NAME_VALUE(baseFolder) << std::endl; ss << GET_VAR_NAME_VALUE(selectedPresetIndex) << std::endl; ss << GET_VAR_NAME_VALUE(lastSmmVersion) << std::endl; diff --git a/src/ModManagerCore/include/GameBrowser.h b/src/ModManagerCore/include/GameBrowser.h index 1ccfa15..fd1b567 100644 --- a/src/ModManagerCore/include/GameBrowser.h +++ b/src/ModManagerCore/include/GameBrowser.h @@ -38,6 +38,8 @@ class GameBrowser{ void scanInputs(u64 kDown, u64 kHeld); void printTerminal(); void rebuildSelectorMenu(); + bool refreshGameList(bool force_ = false); + std::string refreshGameListTag(const std::string& gameName_); // utils -> move to gui lib?? uint8_t* getFolderIcon(const std::string& gameFolder_); @@ -46,7 +48,11 @@ class GameBrowser{ void init(); private: + [[nodiscard]] std::string buildGameListSignature() const; + bool _isGameSelected_{false}; + bool _gameListReady_{false}; + std::string _gameListSignature_{}; Selector _selector_; ModManager _modManager_{this}; diff --git a/src/ModManagerCore/include/ModManager.h b/src/ModManagerCore/include/ModManager.h index 9e41a8f..05785ba 100644 --- a/src/ModManagerCore/include/ModManager.h +++ b/src/ModManagerCore/include/ModManager.h @@ -15,9 +15,23 @@ #include #include +struct ModFileStatusCache{ + std::string state{"MISSING"}; + long long sourceSize{-1}; + long long sourceMtime{0}; + bool destinationExists{false}; + long long destinationSize{-1}; + long long destinationMtime{0}; +}; + struct ApplyCache{ std::string statusStr{"UNCHECKED"}; double applyFraction{0}; + size_t totalFiles{0}; + size_t matchingFiles{0}; + size_t differentFiles{0}; + size_t missingFiles{0}; + std::map fileStatusCache; }; struct ModEntry{ @@ -45,6 +59,20 @@ struct ModEntry{ } }; +struct ModStatusSummary{ + size_t totalMods{0}; + size_t activeMods{0}; + size_t partialMods{0}; + size_t inactiveMods{0}; + size_t noFileMods{0}; + size_t uncheckedMods{0}; +}; + +struct OrphanInstalledMod{ + std::string modName; + std::map applyCache; +}; + ENUM_EXPANDER( ResultModAction, 0, Success, @@ -72,6 +100,7 @@ class ModManager { const Selector &getSelector() const; [[nodiscard]] const std::vector & getIgnoredFileList() const; const std::vector &getModList() const; + [[nodiscard]] const std::vector& getOrphanInstalledModList() const; std::vector &getModList(); std::vector & getIgnoredFileList(); @@ -85,11 +114,18 @@ class ModManager { void dumpModStatusCache(); void reloadModStatusCache(); void resetAllModsCacheAndFile(); + void refreshAllModStatusCache(bool forceRecheck_ = false); + void refreshOrphanInstalledModList(); + void removeOrphanInstalledModCache(const std::string& modName_); + void claimOrphanInstalledFilesForMod(const std::string& modName_); + int getOrphanInstalledModIndex(const std::string& modName_) const; // mod management void resetModCache(int modIndex_); void resetModCache(const std::string &modName_); + ResultModAction refreshModStatus(int modIndex_, bool forceRecheck_ = false); + ResultModAction refreshModStatus(const std::string& modName_, bool forceRecheck_ = false); ResultModAction updateModStatus(int modIndex_); ResultModAction updateModStatus(const std::string& modName_); ResultModAction updateAllModStatus(); @@ -119,10 +155,18 @@ class ModManager { const std::string &getCurrentPresetName() const; + static ModStatusSummary readGameStatusSummary( + const std::string& gameFolderPath_, + const std::string& presetName_ + ); + static std::string formatGameStatusSummary(const ModStatusSummary& summary_); + protected: void displayConflictsWithOtherMods(size_t modIndex_); private: + ResultModAction updateModStatusInternal(int modIndex_, bool forceRecheck_, bool showTerminalProgress_, bool dumpCache_); + GameBrowser* _owner_{nullptr}; bool _ignoreCacheFiles_{true}; @@ -133,6 +177,7 @@ class ModManager { Selector _selector_; std::vector _modList_{}; + std::vector _orphanInstalledModList_{}; std::string _currentPresetName_{}; }; diff --git a/src/ModManagerCore/include/Toolbox.h b/src/ModManagerCore/include/Toolbox.h index 24cd960..7f2f2e2 100644 --- a/src/ModManagerCore/include/Toolbox.h +++ b/src/ModManagerCore/include/Toolbox.h @@ -15,6 +15,21 @@ namespace Toolbox{ //! External function std::string getAppVersion(); + + //! Ensures sdmc:/mods exists (SD root /mods). + void ensureModsRootFolder(); + + //! Creates one folder in the mods root for each installed game title. + void ensureInstalledGameModFolders(const std::string& modsRootFolder_ = "/mods"); + + //! Gets the title id attached to a game mods folder, using SMM metadata first. + std::string getGameFolderTitleId(const std::string& gameFolderPath_); + + //! Gets the Switch icon for a game mods folder. + uint8_t* getGameFolderIcon(const std::string& gameFolderPath_); + + //! Tells whether a game mods folder can resolve to a Switch icon. + bool hasGameFolderIcon(const std::string& gameFolderPath_); } #endif //SIMPLEMODMANAGER_TOOLBOX_H diff --git a/src/ModManagerCore/src/ConfigHandler.cpp b/src/ModManagerCore/src/ConfigHandler.cpp index edda735..75e6414 100644 --- a/src/ModManagerCore/src/ConfigHandler.cpp +++ b/src/ModManagerCore/src/ConfigHandler.cpp @@ -37,6 +37,7 @@ void ConfigHandler::loadConfig(const std::string &configFilePath_) { } std::string lastUsedPresetName{"default"}; + bool sortGameListDirectionWasRead{false}; if( not GenericToolbox::isFile(config.configFilePath) ){ // immediately dump the default config to the file @@ -65,8 +66,44 @@ void ConfigHandler::loadConfig(const std::string &configFilePath_) { if ( elements[0] == "use-gui" ){ config.useGui = GenericToolbox::toBool( elements[1] ); } + else if( elements[0] == "show-debug-mtp-files" ){ + config.showDebugMtpFiles = GenericToolbox::toBool( elements[1] ); + } + else if( elements[0] == "offer-orphan-installed-mod-cleanup" ){ + config.offerOrphanInstalledModCleanup = GenericToolbox::toBool( elements[1] ); + } else if( elements[0] == "sort-game-list-by" ){ - config.sortGameList = ConfigHolder::SortGameList::toEnum( elements[1] ); + if( elements[1] == "LastPlayed" ){ + config.sortGameList = ConfigHolder::SortGameList::GameLaunched; + if( not sortGameListDirectionWasRead ){ + config.sortGameListDirection = ConfigHolder::SortGameListDirection::Descending; + } + } + else if( elements[1] == "FirstPlayed" ){ + config.sortGameList = ConfigHolder::SortGameList::GameLaunched; + if( not sortGameListDirectionWasRead ){ + config.sortGameListDirection = ConfigHolder::SortGameListDirection::Ascending; + } + } + else if( elements[1] == "LastModAdded" ){ + config.sortGameList = ConfigHolder::SortGameList::ModAdded; + if( not sortGameListDirectionWasRead ){ + config.sortGameListDirection = ConfigHolder::SortGameListDirection::Descending; + } + } + else if( elements[1] == "FirstModAdded" ){ + config.sortGameList = ConfigHolder::SortGameList::ModAdded; + if( not sortGameListDirectionWasRead ){ + config.sortGameListDirection = ConfigHolder::SortGameListDirection::Ascending; + } + } + else { + config.sortGameList = ConfigHolder::SortGameList::toEnum( elements[1] ); + } + } + else if( elements[0] == "sort-game-list-direction" ){ + config.sortGameListDirection = ConfigHolder::SortGameListDirection::toEnum( elements[1] ); + sortGameListDirectionWasRead = true; } else if( elements[0] == "stored-mods-base-folder" ){ config.baseFolder = elements[1]; @@ -110,7 +147,10 @@ void ConfigHandler::dumpConfigToFile() const { ssConfig << "# folder where mods are stored" << std::endl; ssConfig << "stored-mods-base-folder = " << _config_.baseFolder << std::endl; ssConfig << "use-gui = " << _config_.useGui << std::endl; + ssConfig << "show-debug-mtp-files = " << _config_.showDebugMtpFiles << std::endl; + ssConfig << "offer-orphan-installed-mod-cleanup = " << _config_.offerOrphanInstalledModCleanup << std::endl; ssConfig << "sort-game-list-by = " << _config_.sortGameList.toString() << std::endl; + ssConfig << "sort-game-list-direction = " << _config_.sortGameListDirection.toString() << std::endl; ssConfig << "last-preset-used = " << _config_.getCurrentPresetName() << std::endl; ssConfig << std::endl; ssConfig << std::endl; @@ -152,7 +192,3 @@ void ConfigHandler::selectNextPreset(){ void ConfigHandler::selectPreviousPreset(){ _config_.setSelectedPresetIndex( _config_.selectedPresetIndex - 1 ); } - - - - diff --git a/src/ModManagerCore/src/GameBrowser.cpp b/src/ModManagerCore/src/GameBrowser.cpp index 70254b6..f8729c4 100644 --- a/src/ModManagerCore/src/GameBrowser.cpp +++ b/src/ModManagerCore/src/GameBrowser.cpp @@ -10,14 +10,279 @@ #include "GenericToolbox.Vector.h" #include +#include -#include #include -#include +#include #include +#include +#include +#include +#include +#include +#include + +#include + +namespace { + +struct GameSortEntry{ + std::string title{}; + std::string path{}; + size_t nMods{0}; + ModStatusSummary modStatusSummary{}; + + bool hasFirstModTimestamp{false}; + bool hasLastModTimestamp{false}; + u64 firstModTimestamp{0}; + u64 lastModTimestamp{0}; + + bool hasPlayStats{false}; + u64 firstPlayedTimestamp{0}; + u64 lastPlayedTimestamp{0}; + u64 playtimeNs{0}; + u64 launchCount{0}; +}; + +std::string toLowerAscii(std::string value){ + std::transform(value.begin(), value.end(), value.begin(), [](unsigned char c){ + return static_cast(std::tolower(c)); + }); + return value; +} + +bool sortNeedsPlayStats(const ConfigHolder::SortGameList& sortMode){ + switch( sortMode.value ){ + case ConfigHolder::SortGameList::GameLaunched: + case ConfigHolder::SortGameList::PlayTime: + case ConfigHolder::SortGameList::LaunchCount: + return true; + default: + return false; + } +} + +u64 parseTitleId(const std::string& titleId){ + if( titleId.empty() ){ + return 0; + } + + u64 out{0}; + std::stringstream ss; + ss << std::hex << titleId; + ss >> out; + if( ss.fail() ){ + return 0; + } + return out; +} + +void fillModTimestamps(GameSortEntry& entry){ + auto modList = GenericToolbox::lsDirs(entry.path); + entry.nMods = modList.size(); + + for( const auto& modName : modList ){ + const std::string modPath = GenericToolbox::joinPath(entry.path, modName); + struct stat result{}; + if( stat(modPath.c_str(), &result) != 0 ){ + continue; + } + + const u64 timestamp = static_cast(result.st_mtime); + if( timestamp == 0 ){ + continue; + } + + if( not entry.hasFirstModTimestamp or timestamp < entry.firstModTimestamp ){ + entry.firstModTimestamp = timestamp; + entry.hasFirstModTimestamp = true; + } + if( not entry.hasLastModTimestamp or timestamp > entry.lastModTimestamp ){ + entry.lastModTimestamp = timestamp; + entry.hasLastModTimestamp = true; + } + } +} + +void fillPlayStats(GameSortEntry& entry){ + const u64 titleId = parseTitleId(Toolbox::getGameFolderTitleId(entry.path)); + if( titleId == 0 ){ + return; + } + + PdmPlayStatistics stats{}; + if( R_FAILED(pdmqryQueryPlayStatisticsByApplicationId(titleId, false, &stats)) ){ + return; + } + + entry.firstPlayedTimestamp = stats.first_timestamp_user; + entry.lastPlayedTimestamp = stats.last_timestamp_user; + entry.playtimeNs = stats.playtime; + entry.launchCount = stats.total_launches; + entry.hasPlayStats = stats.first_timestamp_user != 0 + or stats.last_timestamp_user != 0 + or stats.playtime != 0 + or stats.total_launches != 0; +} + +bool hasSortValue(const GameSortEntry& entry, const ConfigHolder& config){ + const bool ascending = config.sortGameListDirection == ConfigHolder::SortGameListDirection::Ascending; + const auto sortMode = config.sortGameList; + + switch( sortMode.value ){ + case ConfigHolder::SortGameList::Alphabetical: + case ConfigHolder::SortGameList::NbMods: + case ConfigHolder::SortGameList::NoSort: + return true; + case ConfigHolder::SortGameList::GameLaunched: + return entry.hasPlayStats and (ascending ? entry.firstPlayedTimestamp : entry.lastPlayedTimestamp) != 0; + case ConfigHolder::SortGameList::ModAdded: + return ascending ? entry.hasFirstModTimestamp : entry.hasLastModTimestamp; + case ConfigHolder::SortGameList::PlayTime: + return entry.hasPlayStats; + case ConfigHolder::SortGameList::LaunchCount: + return entry.hasPlayStats; + default: + return true; + } +} + +u64 getSortValue(const GameSortEntry& entry, const ConfigHolder& config){ + const bool ascending = config.sortGameListDirection == ConfigHolder::SortGameListDirection::Ascending; + + switch( config.sortGameList.value ){ + case ConfigHolder::SortGameList::NbMods: return entry.nMods; + case ConfigHolder::SortGameList::GameLaunched: return ascending ? entry.firstPlayedTimestamp : entry.lastPlayedTimestamp; + case ConfigHolder::SortGameList::ModAdded: return ascending ? entry.firstModTimestamp : entry.lastModTimestamp; + case ConfigHolder::SortGameList::PlayTime: return entry.playtimeNs; + case ConfigHolder::SortGameList::LaunchCount: return entry.launchCount; + default: return 0; + } +} + +std::string formatTimestamp(u64 timestamp){ + if( timestamp == 0 ){ + return "Unknown"; + } + + std::time_t rawTime = static_cast(timestamp); + std::tm* timeInfo = std::localtime(&rawTime); + if( timeInfo == nullptr ){ + return std::to_string(timestamp); + } + + char buffer[32]{}; + if( std::strftime(buffer, sizeof(buffer), "%Y-%m-%d %H:%M", timeInfo) == 0 ){ + return std::to_string(timestamp); + } + return buffer; +} + +std::string formatPlaytime(u64 playtimeNs){ + constexpr u64 nsPerMinute = 60ULL * 1000ULL * 1000ULL * 1000ULL; + u64 totalMinutes = playtimeNs / nsPerMinute; + if( totalMinutes == 0 ){ + return "0 min"; + } + + const u64 days = totalMinutes / (24 * 60); + totalMinutes %= (24 * 60); + const u64 hours = totalMinutes / 60; + const u64 minutes = totalMinutes % 60; + + std::stringstream ss; + if( days > 0 ){ + ss << days << "d "; + } + if( hours > 0 or days > 0 ){ + ss << hours << "h "; + } + ss << minutes << "min"; + return ss.str(); +} + +const PresetConfig& resolvePresetForGame(const std::string& gameFolderPath, const ConfigHolder& config){ + static PresetConfig fallbackPreset{}; + std::string presetName; + const std::string configFilePath = GenericToolbox::joinPath(gameFolderPath, "this_folder_config.txt"); + if( GenericToolbox::isFile(configFilePath) ){ + presetName = GenericToolbox::dumpFileAsString(configFilePath); + GenericToolbox::trimInputString(presetName, " \n\r\t"); + } + + if( !presetName.empty() ){ + for( const auto& preset : config.presetList ){ + if( preset.name == presetName ){ + return preset; + } + } + } + + if( !config.presetList.empty() + && config.selectedPresetIndex >= 0 + && config.selectedPresetIndex < int(config.presetList.size()) ){ + return config.presetList[config.selectedPresetIndex]; + } + + fallbackPreset.name = "default"; + fallbackPreset.installBaseFolder = "/atmosphere"; + return fallbackPreset; +} +std::string buildSortTag(const GameSortEntry& entry, const ConfigHolder& config){ + const std::string statusSummary = ModManager::formatGameStatusSummary(entry.modStatusSummary); + switch( config.sortGameList.value ){ + case ConfigHolder::SortGameList::GameLaunched: + return statusSummary + " | " + (entry.hasPlayStats ? "Game launched: " + formatTimestamp(getSortValue(entry, config)) : "Never launched"); + case ConfigHolder::SortGameList::ModAdded: + return statusSummary + " | " + (hasSortValue(entry, config) ? "Mod added: " + formatTimestamp(getSortValue(entry, config)) : "No mods"); + case ConfigHolder::SortGameList::PlayTime: + return statusSummary + " | " + (entry.hasPlayStats ? "Play time: " + formatPlaytime(entry.playtimeNs) : "No play data"); + case ConfigHolder::SortGameList::LaunchCount: + return statusSummary + " | " + (entry.hasPlayStats ? "Launches: " + std::to_string(entry.launchCount) : "No launch data"); + default: + return statusSummary; + } +} -GameBrowser::GameBrowser(){ this->init(); } +void sortGameEntries(std::vector& entries, const ConfigHolder& config){ + if( config.sortGameList == ConfigHolder::SortGameList::NoSort ){ + return; + } + + const bool ascending = config.sortGameListDirection == ConfigHolder::SortGameListDirection::Ascending; + const auto sortMode = config.sortGameList; + + std::sort(entries.begin(), entries.end(), [&](const GameSortEntry& a, const GameSortEntry& b){ + const bool aHasValue = hasSortValue(a, config); + const bool bHasValue = hasSortValue(b, config); + if( aHasValue != bHasValue ){ + return aHasValue; + } + + if( sortMode == ConfigHolder::SortGameList::Alphabetical ){ + const auto aTitle = toLowerAscii(a.title); + const auto bTitle = toLowerAscii(b.title); + if( aTitle != bTitle ){ + return ascending ? aTitle < bTitle : aTitle > bTitle; + } + return a.title < b.title; + } + + const u64 aValue = getSortValue(a, config); + const u64 bValue = getSortValue(b, config); + if( aValue != bValue ){ + return ascending ? aValue < bValue : aValue > bValue; + } + + return toLowerAscii(a.title) < toLowerAscii(b.title); + }); +} + +} // namespace + + +GameBrowser::GameBrowser(){ this->refreshGameList(true); } void GameBrowser::setIsGameSelected(bool isGameSelected) { _isGameSelected_ = isGameSelected; @@ -144,37 +409,134 @@ void GameBrowser::rebuildSelectorMenu(){ _selector_.refillPageEntryCache(); } +bool GameBrowser::refreshGameList(bool force_){ + const std::string signature = this->buildGameListSignature(); + if( not force_ and _gameListReady_ and signature == _gameListSignature_ ){ + return false; + } + + _selector_ = Selector(); + this->init(); + _gameListSignature_ = signature; + _gameListReady_ = true; + return true; +} + +std::string GameBrowser::refreshGameListTag(const std::string& gameName_){ + if( gameName_.empty() ){ + return {}; + } + + GameSortEntry entry; + entry.title = gameName_; + entry.path = GenericToolbox::joinPath(_configHandler_.getConfig().baseFolder, gameName_); + fillModTimestamps(entry); + const auto& preset = resolvePresetForGame(entry.path, _configHandler_.getConfig()); + entry.modStatusSummary = ModManager::readGameStatusSummary(entry.path, preset.name); + + const bool needsPlayStats = sortNeedsPlayStats(_configHandler_.getConfig().sortGameList); + const bool playStatsReady = not needsPlayStats or R_SUCCEEDED(pdmqryInitialize()); + if( needsPlayStats and playStatsReady ){ + fillPlayStats(entry); + pdmqryExit(); + } + + const std::string tag = buildSortTag(entry, _configHandler_.getConfig()); + for( size_t iEntry = 0; iEntry < _selector_.getEntryList().size(); ++iEntry ){ + if( _selector_.getEntryList()[iEntry].title == gameName_ ){ + _selector_.setTag(iEntry, tag); + break; + } + } + + return tag; +} + uint8_t* GameBrowser::getFolderIcon(const std::string& gameFolder_){ if( _isGameSelected_ ){ return nullptr; } std::string game_folder_path = _configHandler_.getConfig().baseFolder + "/" + gameFolder_; - uint8_t* icon = GenericToolbox::Switch::Utils::getIconFromTitleId( - GenericToolbox::Switch::Utils::lookForTidInSubFolders(game_folder_path)); - return icon; + return Toolbox::getGameFolderIcon(game_folder_path); } // protected void GameBrowser::init(){ auto gameList = GenericToolbox::lsDirs( _configHandler_.getConfig().baseFolder ); - std::vector nGameMod; - nGameMod.reserve( gameList.size() ); + std::vector visibleGameList; + visibleGameList.reserve( gameList.size() ); + + const bool needsPlayStats = sortNeedsPlayStats(_configHandler_.getConfig().sortGameList); + const bool playStatsReady = not needsPlayStats or R_SUCCEEDED(pdmqryInitialize()); + for( auto& game : gameList ){ - nGameMod.emplace_back( - GenericToolbox::lsDirs( - _configHandler_.getConfig().baseFolder + "/" + game - ).size() - ); + const std::string gameFolderPath = _configHandler_.getConfig().baseFolder + "/" + game; + if( !Toolbox::hasGameFolderIcon(gameFolderPath) ){ + continue; + } + + visibleGameList.emplace_back(); + visibleGameList.back().title = game; + visibleGameList.back().path = gameFolderPath; + fillModTimestamps(visibleGameList.back()); + const auto& preset = resolvePresetForGame(gameFolderPath, _configHandler_.getConfig()); + visibleGameList.back().modStatusSummary = ModManager::readGameStatusSummary(gameFolderPath, preset.name); + if( needsPlayStats and playStatsReady ){ + fillPlayStats(visibleGameList.back()); + } + } + + if( needsPlayStats and playStatsReady ){ + pdmqryExit(); } - auto ordering = GenericToolbox::getSortPermutation(nGameMod, [](size_t a_, size_t b_){ return a_ > b_; }); - GenericToolbox::applyPermutation(gameList, ordering); - GenericToolbox::applyPermutation(nGameMod, ordering); + sortGameEntries(visibleGameList, _configHandler_.getConfig()); - _selector_.getEntryList().reserve( gameList.size() ); - for( size_t iGame = 0 ; iGame < gameList.size() ; iGame++ ){ + _selector_.getEntryList().reserve( visibleGameList.size() ); + for( const auto& game : visibleGameList ){ _selector_.getEntryList().emplace_back(); - _selector_.getEntryList().back().title = gameList[iGame]; - _selector_.getEntryList().back().tag = "(" + std::to_string(nGameMod[iGame]) + " mods)"; + _selector_.getEntryList().back().title = game.title; + _selector_.getEntryList().back().tag = buildSortTag(game, _configHandler_.getConfig()); } } +std::string GameBrowser::buildGameListSignature() const{ + std::stringstream ss; + ss << _configHandler_.getConfig().baseFolder << "|"; + ss << _configHandler_.getConfig().getCurrentPresetName() << "|"; + ss << _configHandler_.getConfig().sortGameList.toString() << "|"; + ss << _configHandler_.getConfig().sortGameListDirection.toString() << "|"; + + auto gameList = GenericToolbox::lsDirs( _configHandler_.getConfig().baseFolder ); + std::sort(gameList.begin(), gameList.end()); + + for( const auto& game : gameList ){ + const std::string gameFolderPath = _configHandler_.getConfig().baseFolder + "/" + game; + struct stat gameStat{}; + const auto gameMtime = stat(gameFolderPath.c_str(), &gameStat) == 0 ? gameStat.st_mtime : 0; + + ss << game << ":" << gameMtime << ":"; + const std::string cacheFilePath = GenericToolbox::joinPath(gameFolderPath, "mods_status_cache.txt"); + struct stat cacheStat{}; + if( stat(cacheFilePath.c_str(), &cacheStat) == 0 ){ + ss << "cache=" << cacheStat.st_mtime << ":" << cacheStat.st_size << ":"; + } + const std::string customPresetPath = GenericToolbox::joinPath(gameFolderPath, "this_folder_config.txt"); + struct stat customPresetStat{}; + if( stat(customPresetPath.c_str(), &customPresetStat) == 0 ){ + ss << "custom=" << customPresetStat.st_mtime << ":" << customPresetStat.st_size << ":"; + } + + auto modList = GenericToolbox::lsDirs(gameFolderPath); + std::sort(modList.begin(), modList.end()); + ss << modList.size() << "["; + for( const auto& mod : modList ){ + const std::string modPath = GenericToolbox::joinPath(gameFolderPath, mod); + struct stat modStat{}; + const auto modMtime = stat(modPath.c_str(), &modStat) == 0 ? modStat.st_mtime : 0; + ss << mod << ":" << modMtime << ";"; + } + ss << "]"; + } + + return ss.str(); +} diff --git a/src/ModManagerCore/src/ModManager.cpp b/src/ModManagerCore/src/ModManager.cpp index 78c7345..e9529a8 100644 --- a/src/ModManagerCore/src/ModManager.cpp +++ b/src/ModManagerCore/src/ModManager.cpp @@ -13,6 +13,485 @@ #include #include +#include +#include +#include +#include +#include + +namespace { + +constexpr const char* kModStatusCacheFileName = "mods_status_cache.txt"; +constexpr const char* kModStatusCacheVersion = "# SimpleModManager mod status cache v2"; +constexpr const char* kUnknownInstalledFilesName = "Unknown installed files"; + +struct FileStamp{ + bool exists{false}; + long long size{-1}; + long long mtime{0}; +}; + +std::string getStatusCachePath(const std::string& gameFolderPath){ + return GenericToolbox::joinPath(gameFolderPath, kModStatusCacheFileName); +} + +FileStamp getFileStamp(const std::string& path){ + struct stat st{}; + if( stat(path.c_str(), &st) != 0 ){ + return {}; + } + if( !S_ISREG(st.st_mode) ){ + return {}; + } + + FileStamp out; + out.exists = true; + out.size = static_cast(st.st_size); + out.mtime = static_cast(st.st_mtime); + return out; +} + +long long parseLongLong(const std::string& value, long long fallback = 0){ + try { return std::stoll(value); } + catch(...) { return fallback; } +} + +size_t parseSizeT(const std::string& value, size_t fallback = 0){ + try { return static_cast(std::stoull(value)); } + catch(...) { return fallback; } +} + +double parseDouble(const std::string& value, double fallback = 0){ + try { return std::stod(value); } + catch(...) { return fallback; } +} + +bool isManagedStatusFile(const std::string& relativePath){ + const std::string fileName = GenericToolbox::getFileName(relativePath); + return !fileName.empty() && fileName[0] == '.'; +} + +std::string normalizeStatusState(std::string state){ + std::transform(state.begin(), state.end(), state.begin(), [](unsigned char c){ + return static_cast(std::toupper(c)); + }); + if( state == "MATCH" || state == "MATCHING" || state == "ACTIVE" ){ + return "MATCHING"; + } + if( state == "DIFF" || state == "DIFFERENT" || state == "PARTIAL" ){ + return "DIFFERENT"; + } + if( state == "MISS" || state == "MISSING" || state == "INACTIVE" ){ + return "MISSING"; + } + return "MISSING"; +} + +void rebuildStatusString(ApplyCache& cache){ + if( cache.totalFiles == 0 ){ + cache.applyFraction = 0; + cache.statusStr = "NO FILE"; + return; + } + + cache.applyFraction = double(cache.matchingFiles) / double(cache.totalFiles); + + if( cache.matchingFiles == cache.totalFiles ){ + cache.statusStr = "ACTIVE"; + } + else if( cache.matchingFiles == 0 ){ + cache.statusStr = "INACTIVE"; + } + else{ + cache.statusStr = "PARTIAL (" + std::to_string(cache.matchingFiles) + + "/" + std::to_string(cache.totalFiles) + ")"; + } +} + +bool canReuseCachedFileStatus( + const ModFileStatusCache& previous, + const FileStamp& sourceStamp, + const FileStamp& destinationStamp ){ + if( !sourceStamp.exists ){ + return false; + } + if( previous.sourceSize != sourceStamp.size || previous.sourceMtime != sourceStamp.mtime ){ + return false; + } + if( previous.destinationExists != destinationStamp.exists ){ + return false; + } + if( destinationStamp.exists ){ + return previous.destinationSize == destinationStamp.size + && previous.destinationMtime == destinationStamp.mtime; + } + return true; +} + +std::string classifyFileStatus( + const std::string& sourcePath, + const std::string& destinationPath, + const FileStamp& sourceStamp, + const FileStamp& destinationStamp ){ + if( !sourceStamp.exists || !destinationStamp.exists ){ + return "MISSING"; + } + if( sourceStamp.size != destinationStamp.size ){ + return "DIFFERENT"; + } + try { + return GenericToolbox::Switch::IO::doFilesAreIdentical(destinationPath, sourcePath) ? "MATCHING" : "DIFFERENT"; + } + catch(...) { + return "DIFFERENT"; + } +} + +std::vector listVisibleModFiles(const std::string& modFolderPath){ + std::vector files; + try { + files = GenericToolbox::lsFilesRecursive(modFolderPath); + } + catch(...) { + files.clear(); + } + GenericToolbox::removeEntryIf(files, [](const std::string& file){ + return isManagedStatusFile(file); + }); + std::sort(files.begin(), files.end()); + return files; +} + +bool safeIsFile(const std::string& path){ + try { return GenericToolbox::isFile(path); } + catch(...) { return false; } +} + +bool isRelativePathSharedWithOtherMod( + const std::string& gameFolderPath, + const std::vector& modList, + const std::string& modName, + const std::string& relativePath ){ + for( const auto& otherMod : modList ){ + if( otherMod.modName == modName ){ + continue; + } + + const std::string otherPath = GenericToolbox::joinPath( + GenericToolbox::joinPath(gameFolderPath, otherMod.modName), + relativePath ); + if( safeIsFile(otherPath) ){ + return true; + } + } + return false; +} + +bool isRelativePathOwnedByActiveOtherMod( + const std::string& gameFolderPath, + const std::vector& modList, + const std::string& currentModName, + const std::string& presetName, + const std::string& installBaseFolder, + const std::string& relativePath ){ + const std::string dstPath = GenericToolbox::joinPath(installBaseFolder, relativePath); + if( !safeIsFile(dstPath) ){ + return false; + } + + for( const auto& otherMod : modList ){ + if( otherMod.modName == currentModName ){ + continue; + } + + auto cacheIt = otherMod.applyCache.find(presetName); + if( cacheIt == otherMod.applyCache.end() || cacheIt->second.statusStr != "ACTIVE" ){ + continue; + } + + const std::string otherPath = GenericToolbox::joinPath( + GenericToolbox::joinPath(gameFolderPath, otherMod.modName), + relativePath ); + if( !safeIsFile(otherPath) ){ + continue; + } + + try { + if( GenericToolbox::Switch::IO::doFilesAreIdentical(otherPath, dstPath) ){ + return true; + } + } + catch(...) {} + } + return false; +} + +bool isRelativePathOwnedByActiveMod( + const std::string& gameFolderPath, + const std::vector& modList, + const std::string& presetName, + const std::string& installBaseFolder, + const std::string& relativePath ){ + const std::string dstPath = GenericToolbox::joinPath(installBaseFolder, relativePath); + if( !safeIsFile(dstPath) ){ + return false; + } + + for( const auto& mod : modList ){ + auto cacheIt = mod.applyCache.find(presetName); + if( cacheIt == mod.applyCache.end() || cacheIt->second.statusStr != "ACTIVE" ){ + continue; + } + + const std::string modFilePath = GenericToolbox::joinPath( + GenericToolbox::joinPath(gameFolderPath, mod.modName), + relativePath ); + if( !safeIsFile(modFilePath) ){ + continue; + } + + try { + if( GenericToolbox::Switch::IO::doFilesAreIdentical(modFilePath, dstPath) ){ + return true; + } + } + catch(...) {} + } + return false; +} + +void countCacheState(ApplyCache& cache, const std::string& state){ + if( state == "MATCHING" ){ + cache.matchingFiles++; + } + else if( state == "DIFFERENT" ){ + cache.differentFiles++; + } + else{ + cache.missingFiles++; + } +} + +void rebuildCacheSummaryFromFiles(ApplyCache& cache){ + cache.totalFiles = cache.fileStatusCache.size(); + cache.matchingFiles = 0; + cache.differentFiles = 0; + cache.missingFiles = 0; + + for( auto& fileCache : cache.fileStatusCache ){ + fileCache.second.state = normalizeStatusState(fileCache.second.state); + countCacheState(cache, fileCache.second.state); + } + + rebuildStatusString(cache); +} + +void addSummaryLine( + std::stringstream& ss, + const std::string& preset, + const std::string& modName, + const ApplyCache& cache ){ + ss << "summary\t" << preset + << "\t" << modName + << "\t" << cache.statusStr + << "\t" << cache.applyFraction + << "\t" << cache.totalFiles + << "\t" << cache.matchingFiles + << "\t" << cache.differentFiles + << "\t" << cache.missingFiles + << std::endl; +} + +void addFileLine( + std::stringstream& ss, + const std::string& preset, + const std::string& modName, + const std::string& relativePath, + const ModFileStatusCache& fileCache ){ + ss << "file\t" << preset + << "\t" << modName + << "\t" << relativePath + << "\t" << fileCache.state + << "\t" << fileCache.sourceSize + << "\t" << fileCache.sourceMtime + << "\t" << (fileCache.destinationExists ? 1 : 0) + << "\t" << fileCache.destinationSize + << "\t" << fileCache.destinationMtime + << std::endl; +} + +void classifySummaryStatus(ModStatusSummary& summary, const std::string& status){ + if( status == "ACTIVE" ){ + summary.activeMods++; + } + else if( status == "INACTIVE" ){ + summary.inactiveMods++; + } + else if( GenericToolbox::startsWith(status, "PARTIAL") ){ + summary.partialMods++; + } + else if( status == "NO FILE" ){ + summary.noFileMods++; + } + else{ + summary.uncheckedMods++; + } +} + +const PresetConfig* findPresetConfig(const ConfigHolder& config, const std::string& presetName){ + for( const auto& preset : config.presetList ){ + if( preset.name == presetName ){ + return &preset; + } + } + return nullptr; +} + +std::string toLowerAscii(std::string value){ + std::transform(value.begin(), value.end(), value.begin(), [](unsigned char c){ + return static_cast(std::tolower(c)); + }); + return value; +} + +std::string findTitleContentsFolder(const std::string& installBaseFolder, const std::string& titleId){ + const std::string contentsFolder = GenericToolbox::joinPath(installBaseFolder, "contents"); + if( !GenericToolbox::isDir(contentsFolder) ){ + return {}; + } + + const std::string exactFolder = GenericToolbox::joinPath(contentsFolder, titleId); + if( GenericToolbox::isDir(exactFolder) ){ + return exactFolder; + } + + const std::string expectedTitleId = toLowerAscii(titleId); + std::vector contentDirs; + try { + contentDirs = GenericToolbox::lsDirs(contentsFolder); + } + catch(...) { + return {}; + } + + for( const auto& contentDir : contentDirs ){ + if( toLowerAscii(contentDir) == expectedTitleId ){ + return GenericToolbox::joinPath(contentsFolder, contentDir); + } + } + + return {}; +} + +std::string normalizeRelativeModPath(const std::string& relativePath){ + std::string out = relativePath; + std::replace(out.begin(), out.end(), '\\', '/'); + while( !out.empty() && out.front() == '/' ){ + out.erase(out.begin()); + } + return toLowerAscii(out); +} + +bool isRelativePathOwnedByOtherOrphanMod( + const std::vector& orphanModList, + const std::string& currentModName, + const std::string& presetName, + const std::string& relativePath ){ + const std::string normalizedPath = normalizeRelativeModPath(relativePath); + for( const auto& orphanMod : orphanModList ){ + if( orphanMod.modName == currentModName || orphanMod.modName == kUnknownInstalledFilesName ){ + continue; + } + + auto presetCacheIt = orphanMod.applyCache.find(presetName); + if( presetCacheIt == orphanMod.applyCache.end() ){ + continue; + } + + for( const auto& fileCache : presetCacheIt->second.fileStatusCache ){ + if( !fileCache.second.destinationExists ){ + continue; + } + if( normalizeRelativeModPath(fileCache.first) == normalizedPath ){ + return true; + } + } + } + return false; +} + +bool naturalStringLess(const std::string& left, const std::string& right){ + size_t iLeft{0}; + size_t iRight{0}; + + while( iLeft < left.size() && iRight < right.size() ){ + const auto cLeft = static_cast(left[iLeft]); + const auto cRight = static_cast(right[iRight]); + + if( std::isdigit(cLeft) && std::isdigit(cRight) ){ + size_t leftDigitsStart = iLeft; + size_t rightDigitsStart = iRight; + while( leftDigitsStart < left.size() && left[leftDigitsStart] == '0' ){ leftDigitsStart++; } + while( rightDigitsStart < right.size() && right[rightDigitsStart] == '0' ){ rightDigitsStart++; } + + size_t leftDigitsEnd = leftDigitsStart; + size_t rightDigitsEnd = rightDigitsStart; + while( leftDigitsEnd < left.size() && std::isdigit(static_cast(left[leftDigitsEnd])) ){ + leftDigitsEnd++; + } + while( rightDigitsEnd < right.size() && std::isdigit(static_cast(right[rightDigitsEnd])) ){ + rightDigitsEnd++; + } + + const size_t leftDigitsLength = leftDigitsEnd - leftDigitsStart; + const size_t rightDigitsLength = rightDigitsEnd - rightDigitsStart; + if( leftDigitsLength != rightDigitsLength ){ + return leftDigitsLength < rightDigitsLength; + } + + for( size_t offset = 0; offset < leftDigitsLength; ++offset ){ + if( left[leftDigitsStart + offset] != right[rightDigitsStart + offset] ){ + return left[leftDigitsStart + offset] < right[rightDigitsStart + offset]; + } + } + + const size_t leftTotalDigitsEnd = leftDigitsEnd; + const size_t rightTotalDigitsEnd = rightDigitsEnd; + if( leftDigitsStart == leftDigitsEnd ){ + while( leftDigitsEnd < left.size() && left[leftDigitsEnd] == '0' ){ leftDigitsEnd++; } + } + if( rightDigitsStart == rightDigitsEnd ){ + while( rightDigitsEnd < right.size() && right[rightDigitsEnd] == '0' ){ rightDigitsEnd++; } + } + + const size_t leftLeadingZeroes = leftDigitsStart - iLeft; + const size_t rightLeadingZeroes = rightDigitsStart - iRight; + if( leftLeadingZeroes != rightLeadingZeroes ){ + return leftLeadingZeroes < rightLeadingZeroes; + } + + iLeft = leftTotalDigitsEnd; + iRight = rightTotalDigitsEnd; + continue; + } + + const char lowerLeft = static_cast(std::tolower(cLeft)); + const char lowerRight = static_cast(std::tolower(cRight)); + if( lowerLeft != lowerRight ){ + return lowerLeft < lowerRight; + } + if( left[iLeft] != right[iRight] ){ + return left[iLeft] < right[iRight]; + } + + iLeft++; + iRight++; + } + + return left.size() < right.size(); +} + +} // namespace ModManager::ModManager(GameBrowser* owner_) : _owner_(owner_) {} @@ -49,6 +528,9 @@ const std::vector & ModManager::getIgnoredFileList() const { const std::vector &ModManager::getModList() const { return _modList_; } +const std::vector& ModManager::getOrphanInstalledModList() const { + return _orphanInstalledModList_; +} std::vector &ModManager::getModList() { return _modList_; @@ -66,14 +548,27 @@ ConfigHolder& ModManager::getConfig(){ void ModManager::updateModList() { // list folders - auto folderList = GenericToolbox::lsDirs(_gameFolderPath_); + std::vector folderList; + try { + if( GenericToolbox::isDir(_gameFolderPath_) ){ + folderList = GenericToolbox::lsDirs(_gameFolderPath_); + } + } + catch(...) { + folderList.clear(); + } GenericToolbox::removeEntryIf(folderList, [](const std::string& entry_){ return entry_ == ".plugins"; }); + std::sort(folderList.begin(), folderList.end(), naturalStringLess); _modList_.clear(); _modList_.reserve( folderList.size() ); for( auto& folder : folderList ){ _modList_.emplace_back( folder ); } + _orphanInstalledModList_.clear(); - // reload .txt cache + // reload and refresh the status cache. Valid entries are reused through file stats, + // stale entries are checked once and written back. this->reloadModStatusCache(); + this->refreshOrphanInstalledModList(); + this->refreshAllModStatusCache(false); // reset the selector _selector_ = Selector(); @@ -87,20 +582,30 @@ void ModManager::updateModList() { } void ModManager::dumpModStatusCache() { std::stringstream ss; + ss << kModStatusCacheVersion << std::endl; for( auto& mod : _modList_ ){ for( auto& presetCache : mod.applyCache ){ - ss << presetCache.first << ": " << mod.modName - << " = " << presetCache.second.statusStr - << " = " << presetCache.second.applyFraction << std::endl; + addSummaryLine(ss, presetCache.first, mod.modName, presetCache.second); + for( const auto& fileCache : presetCache.second.fileStatusCache ){ + addFileLine(ss, presetCache.first, mod.modName, fileCache.first, fileCache.second); + } + } + } + for( auto& orphanMod : _orphanInstalledModList_ ){ + for( auto& presetCache : orphanMod.applyCache ){ + addSummaryLine(ss, presetCache.first, orphanMod.modName, presetCache.second); + for( const auto& fileCache : presetCache.second.fileStatusCache ){ + addFileLine(ss, presetCache.first, orphanMod.modName, fileCache.first, fileCache.second); + } } } - std::string cacheFilePath = _gameFolderPath_ + "/mods_status_cache.txt"; + std::string cacheFilePath = getStatusCachePath(_gameFolderPath_); GenericToolbox::dumpStringInFile(cacheFilePath, ss.str()); } void ModManager::reloadModStatusCache(){ - std::string cacheFilePath = _gameFolderPath_ + "/mods_status_cache.txt"; + std::string cacheFilePath = getStatusCachePath(_gameFolderPath_); if( not GenericToolbox::isFile(cacheFilePath) ) return; auto lines = GenericToolbox::dumpFileAsVectorString( cacheFilePath ); @@ -109,6 +614,69 @@ void ModManager::reloadModStatusCache(){ if( GenericToolbox::startsWith(line, "#") ) continue; + auto tabElements = GenericToolbox::splitString(line, "\t"); + if( tabElements.size() >= 9 && tabElements[0] == "summary" ){ + const std::string& preset = tabElements[1]; + const std::string& modName = tabElements[2]; + + int modIndex = this->getModIndex( modName ); + if( modIndex == -1 ){ + int orphanIndex = this->getOrphanInstalledModIndex(modName); + if( orphanIndex == -1 ){ + _orphanInstalledModList_.emplace_back(); + _orphanInstalledModList_.back().modName = modName; + orphanIndex = int(_orphanInstalledModList_.size()) - 1; + } + + auto& cache = _orphanInstalledModList_[orphanIndex].applyCache[preset]; + cache.statusStr = tabElements[3]; + cache.applyFraction = parseDouble(tabElements[4]); + cache.totalFiles = parseSizeT(tabElements[5]); + cache.matchingFiles = parseSizeT(tabElements[6]); + cache.differentFiles = parseSizeT(tabElements[7]); + cache.missingFiles = parseSizeT(tabElements[8]); + continue; + } + + auto& cache = _modList_[modIndex].applyCache[preset]; + cache.statusStr = tabElements[3]; + cache.applyFraction = parseDouble(tabElements[4]); + cache.totalFiles = parseSizeT(tabElements[5]); + cache.matchingFiles = parseSizeT(tabElements[6]); + cache.differentFiles = parseSizeT(tabElements[7]); + cache.missingFiles = parseSizeT(tabElements[8]); + continue; + } + + if( tabElements.size() >= 10 && tabElements[0] == "file" ){ + const std::string& preset = tabElements[1]; + const std::string& modName = tabElements[2]; + const std::string& relativePath = tabElements[3]; + + int modIndex = this->getModIndex( modName ); + ModFileStatusCache fileCache; + fileCache.state = normalizeStatusState(tabElements[4]); + fileCache.sourceSize = parseLongLong(tabElements[5], -1); + fileCache.sourceMtime = parseLongLong(tabElements[6]); + fileCache.destinationExists = parseLongLong(tabElements[7]) != 0; + fileCache.destinationSize = parseLongLong(tabElements[8], -1); + fileCache.destinationMtime = parseLongLong(tabElements[9]); + + if( modIndex == -1 ){ + int orphanIndex = this->getOrphanInstalledModIndex(modName); + if( orphanIndex == -1 ){ + _orphanInstalledModList_.emplace_back(); + _orphanInstalledModList_.back().modName = modName; + orphanIndex = int(_orphanInstalledModList_.size()) - 1; + } + _orphanInstalledModList_[orphanIndex].applyCache[preset].fileStatusCache[relativePath] = fileCache; + continue; + } + + _modList_[modIndex].applyCache[preset].fileStatusCache[relativePath] = fileCache; + continue; + } + auto elements = GenericToolbox::splitString(line, "="); if( elements.size() < 2 ) continue; for( auto& element : elements ){ GenericToolbox::trimInputString(element, " "); } @@ -119,21 +687,230 @@ void ModManager::reloadModStatusCache(){ for( auto& element : presetModName ){ GenericToolbox::trimInputString(element, " "); } int modIndex = this->getModIndex( presetModName[1] ); - if( modIndex == -1 ) continue; + if( modIndex == -1 ){ + int orphanIndex = this->getOrphanInstalledModIndex(presetModName[1]); + if( orphanIndex == -1 ){ + _orphanInstalledModList_.emplace_back(); + _orphanInstalledModList_.back().modName = presetModName[1]; + orphanIndex = int(_orphanInstalledModList_.size()) - 1; + } + _orphanInstalledModList_[orphanIndex].applyCache[presetModName[0]].statusStr = elements[1]; + if( elements.size() >= 3 ){ + _orphanInstalledModList_[orphanIndex].applyCache[presetModName[0]].applyFraction = parseDouble( elements[2] ); + } + continue; + } auto* modEntryPtr = &_modList_[modIndex]; - modEntryPtr->applyCache[presetModName[0]].statusStr = elements[1]; + auto& cache = modEntryPtr->applyCache[presetModName[0]]; + cache.statusStr = elements[1]; // v < 1.5.0 if( elements.size() < 3 ){ continue; } // v >= 1.5.0 - modEntryPtr->applyCache[presetModName[0]].applyFraction = std::stod( elements[2] ); + cache.applyFraction = parseDouble( elements[2] ); } + + GenericToolbox::removeEntryIf(_orphanInstalledModList_, [this](OrphanInstalledMod& orphanMod){ + bool hasInstalledFiles{false}; + for( auto& presetCache : orphanMod.applyCache ){ + const auto* preset = findPresetConfig(this->getConfig(), presetCache.first); + if( preset == nullptr ){ + continue; + } + + for( auto& fileCache : presetCache.second.fileStatusCache ){ + const std::string dstPath = GenericToolbox::joinPath(preset->installBaseFolder, fileCache.first); + const FileStamp dstStamp = getFileStamp(dstPath); + fileCache.second.destinationExists = dstStamp.exists; + fileCache.second.destinationSize = dstStamp.size; + fileCache.second.destinationMtime = dstStamp.mtime; + if( dstStamp.exists ){ + hasInstalledFiles = true; + } + } + } + return !hasInstalledFiles; + }); } void ModManager::resetAllModsCacheAndFile(){ - GenericToolbox::rm(_gameFolderPath_ + "/mods_status_cache.txt"); - this->updateModList(); + GenericToolbox::rm(getStatusCachePath(_gameFolderPath_)); + for( auto& mod : _modList_ ){ + mod = ModEntry(mod.modName); + } + _selector_.clearTags(); +} + +void ModManager::removeOrphanInstalledModCache(const std::string& modName_){ + auto it = std::remove_if( + _orphanInstalledModList_.begin(), + _orphanInstalledModList_.end(), + [&](const OrphanInstalledMod& mod){ return mod.modName == modName_; }); + _orphanInstalledModList_.erase(it, _orphanInstalledModList_.end()); + this->dumpModStatusCache(); +} + +void ModManager::claimOrphanInstalledFilesForMod(const std::string& modName_){ + const int modIndex = this->getModIndex(modName_); + if( modIndex == -1 ){ + return; + } + + const auto& preset = this->fetchCurrentPreset(); + const std::string modFolderPath = GenericToolbox::joinPath(_gameFolderPath_, modName_); + + std::set claimedFiles; + for( const auto& relativePath : listVisibleModFiles(modFolderPath) ){ + const std::string srcPath = GenericToolbox::joinPath(modFolderPath, relativePath); + const std::string dstPath = GenericToolbox::joinPath(preset.installBaseFolder, relativePath); + if( !safeIsFile(srcPath) || !safeIsFile(dstPath) ){ + continue; + } + + try { + if( GenericToolbox::Switch::IO::doFilesAreIdentical(srcPath, dstPath) ){ + claimedFiles.insert(normalizeRelativeModPath(relativePath)); + } + } + catch(...) {} + } + + if( claimedFiles.empty() ){ + return; + } + + bool cacheChanged{false}; + GenericToolbox::removeEntryIf(_orphanInstalledModList_, [&](OrphanInstalledMod& orphanMod){ + if( orphanMod.modName == modName_ ){ + return false; + } + + for( auto presetCacheIt = orphanMod.applyCache.begin(); presetCacheIt != orphanMod.applyCache.end(); ){ + if( presetCacheIt->first != preset.name ){ + ++presetCacheIt; + continue; + } + + auto& cache = presetCacheIt->second; + for( auto fileCacheIt = cache.fileStatusCache.begin(); fileCacheIt != cache.fileStatusCache.end(); ){ + if( claimedFiles.count(normalizeRelativeModPath(fileCacheIt->first)) == 0 ){ + ++fileCacheIt; + continue; + } + + cacheChanged = true; + fileCacheIt = cache.fileStatusCache.erase(fileCacheIt); + } + + if( cache.fileStatusCache.empty() ){ + cacheChanged = true; + presetCacheIt = orphanMod.applyCache.erase(presetCacheIt); + continue; + } + + rebuildCacheSummaryFromFiles(cache); + ++presetCacheIt; + } + + return orphanMod.applyCache.empty(); + }); + + if( cacheChanged ){ + this->dumpModStatusCache(); + } +} + +int ModManager::getOrphanInstalledModIndex(const std::string& modName_) const{ + return GenericToolbox::findElementIndex(modName_, _orphanInstalledModList_, [](const OrphanInstalledMod& mod_){ return mod_.modName; }); +} + +void ModManager::refreshOrphanInstalledModList(){ + auto clearOrphanInstalledMods = [this](){ + if( !_orphanInstalledModList_.empty() ){ + _orphanInstalledModList_.clear(); + this->dumpModStatusCache(); + } + }; + + if( !this->getConfig().offerOrphanInstalledModCleanup ){ + clearOrphanInstalledMods(); + return; + } + + const std::string titleId = Toolbox::getGameFolderTitleId(_gameFolderPath_); + if( titleId.empty() ){ + clearOrphanInstalledMods(); + return; + } + + const auto& preset = this->fetchCurrentPreset(); + const std::string titleContentsFolder = findTitleContentsFolder(preset.installBaseFolder, titleId); + if( titleContentsFolder.empty() ){ + clearOrphanInstalledMods(); + return; + } + + bool cacheChanged{false}; + GenericToolbox::removeEntryIf(_orphanInstalledModList_, [this, &cacheChanged](OrphanInstalledMod& orphanMod){ + if( orphanMod.modName == kUnknownInstalledFilesName ){ + cacheChanged = true; + return true; + } + + bool hasInstalledFiles{false}; + for( auto presetCacheIt = orphanMod.applyCache.begin(); presetCacheIt != orphanMod.applyCache.end(); ){ + const auto* orphanPreset = findPresetConfig(this->getConfig(), presetCacheIt->first); + if( orphanPreset == nullptr ){ + cacheChanged = true; + presetCacheIt = orphanMod.applyCache.erase(presetCacheIt); + continue; + } + + auto& cache = presetCacheIt->second; + for( auto fileCacheIt = cache.fileStatusCache.begin(); fileCacheIt != cache.fileStatusCache.end(); ){ + const bool ownedByActiveCurrentSdMod = isRelativePathOwnedByActiveMod( + _gameFolderPath_, + _modList_, + orphanPreset->name, + orphanPreset->installBaseFolder, + fileCacheIt->first ); + const std::string dstPath = GenericToolbox::joinPath(orphanPreset->installBaseFolder, fileCacheIt->first); + const FileStamp dstStamp = getFileStamp(dstPath); + fileCacheIt->second.destinationExists = dstStamp.exists; + fileCacheIt->second.destinationSize = dstStamp.size; + fileCacheIt->second.destinationMtime = dstStamp.mtime; + + if( ownedByActiveCurrentSdMod || !dstStamp.exists ){ + cacheChanged = true; + fileCacheIt = cache.fileStatusCache.erase(fileCacheIt); + continue; + } + + hasInstalledFiles = true; + ++fileCacheIt; + } + + if( cache.fileStatusCache.empty() ){ + cacheChanged = true; + presetCacheIt = orphanMod.applyCache.erase(presetCacheIt); + continue; + } + + rebuildCacheSummaryFromFiles(cache); + ++presetCacheIt; + } + + if( !hasInstalledFiles ){ + cacheChanged = true; + return true; + } + return false; + }); + + if( cacheChanged ){ + this->dumpModStatusCache(); + } } // mod management @@ -149,23 +926,28 @@ void ModManager::resetModCache(const std::string &modName_){ this->resetModCache( this->getModIndex(modName_) ); } -ResultModAction ModManager::updateModStatus(int modIndex_){ +ResultModAction ModManager::updateModStatusInternal(int modIndex_, bool forceRecheck_, bool showTerminalProgress_, bool dumpCache_){ if( modIndex_ < 0 or modIndex_ >= int( _modList_.size() ) ) return Fail; auto* modPtr = &_modList_[modIndex_]; if( modPtr == nullptr ) return Fail; - GenericToolbox::Switch::Terminal::printLeft("Checking : " + modPtr->modName, GenericToolbox::ColorCodes::magentaBackground); - consoleUpdate(nullptr); + if( showTerminalProgress_ ){ + GenericToolbox::Switch::Terminal::printLeft("Checking : " + modPtr->modName, GenericToolbox::ColorCodes::magentaBackground); + consoleUpdate(nullptr); + } std::string modFolderPath = _gameFolderPath_ + "/" + modPtr->modName; - auto filesList = GenericToolbox::lsFilesRecursive( modFolderPath ); + auto filesList = listVisibleModFiles( modFolderPath ); PadState pad; padInitializeAny(&pad); - size_t nSameFiles{0}; - size_t nIgnoredFiles{0}; + const std::string presetName = this->fetchCurrentPreset().name; + ApplyCache previousCache = modPtr->applyCache[presetName]; + ApplyCache refreshedCache; + refreshedCache.totalFiles = filesList.size(); + size_t iFile{0}; for( auto& file : filesList ){ @@ -175,49 +957,104 @@ ResultModAction ModManager::updateModStatus(int modIndex_){ if( kDown & HidNpadButton_B ) return Abort; } - if( _ignoreCacheFiles_ and GenericToolbox::startsWith(GenericToolbox::getFileName(file), ".") ) { - nIgnoredFiles++; continue; - } - std::string installedPathCandidate{this->fetchCurrentPreset().installBaseFolder}; installedPathCandidate += "/" + file; std::string modFilePath{modFolderPath}; modFilePath += "/" + file; - std::stringstream ssPbar; - ssPbar << "Checking : (" << iFile+1 << "/" << filesList.size() << ") " << GenericToolbox::getFileName(file); - GenericToolbox::Switch::Terminal::displayProgressBar( iFile++, filesList.size(), ssPbar.str() ); + if( showTerminalProgress_ ){ + std::stringstream ssPbar; + ssPbar << "Checking : (" << iFile+1 << "/" << filesList.size() << ") " << GenericToolbox::getFileName(file); + GenericToolbox::Switch::Terminal::displayProgressBar( iFile, filesList.size(), ssPbar.str() ); + } + iFile++; + + const FileStamp sourceStamp = getFileStamp(modFilePath); + const FileStamp destinationStamp = getFileStamp(installedPathCandidate); + + ModFileStatusCache fileCache; + auto previousFileIt = previousCache.fileStatusCache.find(file); + if( !forceRecheck_ + && previousFileIt != previousCache.fileStatusCache.end() + && canReuseCachedFileStatus(previousFileIt->second, sourceStamp, destinationStamp) ){ + fileCache = previousFileIt->second; + } + else{ + fileCache.state = classifyFileStatus(modFilePath, installedPathCandidate, sourceStamp, destinationStamp); + fileCache.sourceSize = sourceStamp.size; + fileCache.sourceMtime = sourceStamp.mtime; + fileCache.destinationExists = destinationStamp.exists; + fileCache.destinationSize = destinationStamp.size; + fileCache.destinationMtime = destinationStamp.mtime; + } - if( GenericToolbox::Switch::IO::doFilesAreIdentical( installedPathCandidate, modFilePath ) ){ - nSameFiles++; + fileCache.state = normalizeStatusState(fileCache.state); + if( fileCache.state == "MATCHING" + && isRelativePathOwnedByOtherOrphanMod(_orphanInstalledModList_, modPtr->modName, presetName, file) ){ + fileCache.state = "MISSING"; } + refreshedCache.fileStatusCache[file] = fileCache; + countCacheState(refreshedCache, fileCache.state); } - // (XX/XX) Files Applied - // ACTIVE - // INACTIVE - modPtr->applyCache[this->fetchCurrentPreset().name].applyFraction = double(nSameFiles) / double(filesList.size() - nIgnoredFiles); - if ( filesList.empty() ) { modPtr->applyCache[this->fetchCurrentPreset().name].statusStr = "NO FILE"; } - else if( modPtr->applyCache[this->fetchCurrentPreset().name].applyFraction == 0 ){ modPtr->applyCache[this->fetchCurrentPreset().name].statusStr = "INACTIVE"; } - else if( modPtr->applyCache[this->fetchCurrentPreset().name].applyFraction == 1 ){ modPtr->applyCache[this->fetchCurrentPreset().name].statusStr = "ACTIVE"; } - else{ - std::stringstream ss; - ss << "PARTIAL (" << nSameFiles << "/" << filesList.size() - nIgnoredFiles << ")"; - modPtr->applyCache[this->fetchCurrentPreset().name].statusStr = ss.str(); + rebuildStatusString(refreshedCache); + if( refreshedCache.matchingFiles > 0 && refreshedCache.matchingFiles < refreshedCache.totalFiles ){ + size_t matchingFilesOwnedByThisMod{0}; + for( const auto& fileCache : refreshedCache.fileStatusCache ){ + if( fileCache.second.state != "MATCHING" ){ + continue; + } + if( !isRelativePathSharedWithOtherMod(_gameFolderPath_, _modList_, modPtr->modName, fileCache.first) ){ + matchingFilesOwnedByThisMod++; + } + } + + if( matchingFilesOwnedByThisMod == 0 ){ + refreshedCache.matchingFiles = 0; + refreshedCache.differentFiles = 0; + refreshedCache.missingFiles = refreshedCache.totalFiles; + refreshedCache.applyFraction = 0; + refreshedCache.statusStr = "INACTIVE"; + } } + modPtr->applyCache[presetName] = refreshedCache; // update selector - _selector_.setTag(modIndex_, modPtr->applyCache[this->fetchCurrentPreset().name].statusStr); + if( modIndex_ >= 0 and modIndex_ < int(_selector_.getEntryList().size()) ){ + _selector_.setTag(modIndex_, modPtr->applyCache[presetName].statusStr); + } - // immediately save - this->dumpModStatusCache(); + if( dumpCache_ ){ + this->dumpModStatusCache(); + } return Success; } + +ResultModAction ModManager::refreshModStatus(int modIndex_, bool forceRecheck_){ + return this->updateModStatusInternal(modIndex_, forceRecheck_, false, true); +} + +ResultModAction ModManager::refreshModStatus(const std::string& modName_, bool forceRecheck_){ + return this->refreshModStatus( this->getModIndex(modName_), forceRecheck_ ); +} + +ResultModAction ModManager::updateModStatus(int modIndex_){ + return this->updateModStatusInternal(modIndex_, true, true, true); +} ResultModAction ModManager::updateModStatus(const std::string& modName_){ return this->updateModStatus( this->getModIndex(modName_) ); } + +void ModManager::refreshAllModStatusCache(bool forceRecheck_){ + for(size_t iMod = 0 ; iMod < _modList_.size() ; iMod++ ){ + this->updateModStatusInternal( int(iMod), forceRecheck_, false, false ); + } + + this->dumpModStatusCache(); +} + ResultModAction ModManager::updateAllModStatus(){ _selector_.clearTags(); @@ -239,10 +1076,11 @@ ResultModAction ModManager::updateAllModStatus(){ ss << _selector_.getEntryList()[iMod].title << "..."; GenericToolbox::Switch::Terminal::printLeft(ss.str(), GenericToolbox::ColorCodes::magentaBackground); consoleUpdate(nullptr); - auto result = this->updateModStatus( _selector_.getEntryList()[iMod].title ); + auto result = this->updateModStatusInternal( int(iMod), true, true, false ); if( result == Abort ) return result; } + this->dumpModStatusCache(); return Success; } @@ -325,6 +1163,7 @@ ResultModAction ModManager::applyMod(int modIndex_, bool overrideConflicts_){ } + this->claimOrphanInstalledFilesForMod(modPtr->modName); return Success; } ResultModAction ModManager::applyMod(const std::string& modName_, bool overrideConflicts_) { @@ -388,19 +1227,74 @@ void ModManager::removeMod(int modIndex_) { ); // Check if the installed mod belongs to the selected mod - if( GenericToolbox::Switch::IO::doFilesAreIdentical( dstFilePath, srcFilePath ) ){ - - // Remove the mod file - GenericToolbox::rm( dstFilePath ); - - // Delete the folder if no other files is present - std::string emptyFolderCandidate = GenericToolbox::getFolderPath(dstFilePath ); - while( GenericToolbox::isDirEmpty( emptyFolderCandidate ) ) { - if( emptyFolderCandidate.empty() ) break; - GenericToolbox::rmDir( emptyFolderCandidate ); - emptyFolderCandidate = GenericToolbox::getFolderPath( emptyFolderCandidate ); + try { + if( GenericToolbox::Switch::IO::doFilesAreIdentical( dstFilePath, srcFilePath ) ){ + if( isRelativePathOwnedByActiveOtherMod( + _gameFolderPath_, + _modList_, + modPtr->modName, + this->fetchCurrentPreset().name, + this->fetchCurrentPreset().installBaseFolder, + file ) ){ + continue; + } + + // Remove the mod file with multiple retries + for( int retry = 0; retry < 5; ++retry ){ + try { + GenericToolbox::rm( dstFilePath ); + break; // Success, exit retry loop + } catch (...) { + // Retry with delay + if( retry < 4 ){ + svcSleepThread(50000000); // 50ms delay between retries + } + } + } + + // Delete the folder if no other files is present + std::string emptyFolderCandidate = GenericToolbox::getFolderPath(dstFilePath ); + int safetyCounter = 0; + while( GenericToolbox::isDirEmpty( emptyFolderCandidate ) ) { + if( emptyFolderCandidate.empty() ) break; + + // Safety check to prevent deleting system folders + std::string installBase = this->fetchCurrentPreset().installBaseFolder; + if( emptyFolderCandidate.find(installBase) != 0 ) break; + if( emptyFolderCandidate == installBase ) break; + + // Safety counter to prevent infinite loops + if( safetyCounter++ > 50 ) break; + + // Delete directory with retries + for( int retry = 0; retry < 3; ++retry ){ + try { + GenericToolbox::rmDir( emptyFolderCandidate ); + break; // Success, exit retry loop + } catch (...) { + // Retry with delay + if( retry < 2 ){ + svcSleepThread(100000000); // 100ms delay between retries + } else { + // Max retries reached, break the while loop + goto folder_cleanup_done; + } + } + } + + // Longer delay to prevent filesystem issues + svcSleepThread(20000000); // 20ms delay + + emptyFolderCandidate = GenericToolbox::getFolderPath( emptyFolderCandidate ); + } + folder_cleanup_done:; } + } catch (...) { + // Ignore all errors in the comparison and deletion process to prevent crashes } + + // Small delay between file deletions to prevent filesystem overload + svcSleepThread(10000000); // 10ms delay } this->resetModCache( modPtr->modName ); @@ -450,6 +1344,7 @@ void ModManager::scanInputs(u64 kDown, u64 kHeld){ auto subAnswer = Selector::askQuestion("Do you which to recheck all mods ?", {{"Yes"}, {"No"}}); if( subAnswer == "Yes"){ this->resetAllModsCacheAndFile(); + this->refreshOrphanInstalledModList(); for( auto& mod : _modList_ ){ this->updateModStatus( mod.modName ); } } } @@ -736,6 +1631,92 @@ int ModManager::getModIndex(const std::string& modName_){ return GenericToolbox::findElementIndex(modName_, _modList_, [](const ModEntry& mod_){ return mod_.modName; } ); } +ModStatusSummary ModManager::readGameStatusSummary( + const std::string& gameFolderPath_, + const std::string& presetName_ ){ + ModStatusSummary summary; + + std::vector modDirs; + try { + modDirs = GenericToolbox::lsDirs(gameFolderPath_); + } + catch(...) { + modDirs.clear(); + } + GenericToolbox::removeEntryIf(modDirs, [](const std::string& entry){ return entry == ".plugins"; }); + summary.totalMods = modDirs.size(); + + std::map cachedStatusByMod; + const std::string cacheFilePath = getStatusCachePath(gameFolderPath_); + if( GenericToolbox::isFile(cacheFilePath) ){ + auto lines = GenericToolbox::dumpFileAsVectorString(cacheFilePath); + for( auto& line : lines ){ + GenericToolbox::trimInputString(line, " "); + if( line.empty() || GenericToolbox::startsWith(line, "#") ){ + continue; + } + + auto tabElements = GenericToolbox::splitString(line, "\t"); + if( tabElements.size() >= 4 && tabElements[0] == "summary" ){ + if( tabElements[1] == presetName_ ){ + cachedStatusByMod[tabElements[2]] = tabElements[3]; + } + continue; + } + + auto elements = GenericToolbox::splitString(line, "="); + if( elements.size() < 2 ){ + continue; + } + for( auto& element : elements ){ GenericToolbox::trimInputString(element, " "); } + + auto presetModName = GenericToolbox::splitString(elements[0], ":"); + if( presetModName.size() != 2 ){ + continue; + } + for( auto& element : presetModName ){ GenericToolbox::trimInputString(element, " "); } + if( presetModName[0] == presetName_ ){ + cachedStatusByMod[presetModName[1]] = elements[1]; + } + } + } + + for( const auto& modName : modDirs ){ + auto it = cachedStatusByMod.find(modName); + if( it == cachedStatusByMod.end() ){ + summary.uncheckedMods++; + continue; + } + classifySummaryStatus(summary, it->second); + } + + return summary; +} + +std::string ModManager::formatGameStatusSummary(const ModStatusSummary& summary_){ + if( summary_.totalMods == 0 ){ + return "0 mods"; + } + + std::stringstream ss; + ss << summary_.totalMods << " mod" << (summary_.totalMods == 1 ? "" : "s"); + + const size_t knownMods = summary_.activeMods + summary_.partialMods + summary_.inactiveMods + summary_.noFileMods; + if( knownMods == 0 ){ + return ss.str(); + } + + ss << " | " << summary_.activeMods << " active" + << " / " << summary_.partialMods << " partial" + << " / " << summary_.inactiveMods << " inactive"; + + if( summary_.noFileMods > 0 ){ + ss << " / " << summary_.noFileMods << " empty"; + } + + return ss.str(); +} + void ModManager::reloadCustomPreset(){ std::string configFilePath = _gameFolderPath_ + "/this_folder_config.txt"; @@ -764,9 +1745,3 @@ const PresetConfig &ModManager::fetchCurrentPreset() const { const std::string &ModManager::getCurrentPresetName() const { return _currentPresetName_; } - - - - - - diff --git a/src/ModManagerCore/src/Toolbox.cpp b/src/ModManagerCore/src/Toolbox.cpp index 19adf7d..c5a7a1c 100644 --- a/src/ModManagerCore/src/Toolbox.cpp +++ b/src/ModManagerCore/src/Toolbox.cpp @@ -5,11 +5,277 @@ #include #include +#include "GenericToolbox.Fs.h" +#include "GenericToolbox.String.h" +#include "GenericToolbox.Switch.h" +#include "Logger.h" + +#include +#include +#include #include +#include +#include +#include #include +#include +#include +#include #include -#include "string" +#include +#include + +#include + + +LoggerInit( [] { + Logger::setUserHeaderStr( "[Toolbox]" ); +} ); + +namespace { + +constexpr const char* kTitleIdMetadataFile = ".smm_title_id"; +constexpr size_t kSwitchIconBufferSize = 0x20000; + +std::map> g_gameIconCache; +std::map g_gameIconAvailabilityCache; + +std::string trimFolderName(std::string name) { + while( !name.empty() && ( name.front() == ' ' || name.front() == '.' ) ) { + name.erase( name.begin() ); + } + while( !name.empty() && ( name.back() == ' ' || name.back() == '.' ) ) { + name.pop_back(); + } + return name; +} + +std::string toUpperAscii(std::string value) { + std::transform(value.begin(), value.end(), value.begin(), [](unsigned char c) { + return static_cast(std::toupper(c)); + }); + return value; +} + +bool isWindowsReservedName(const std::string& name) { + const std::string upper = toUpperAscii(name); + if( upper == "CON" || upper == "PRN" || upper == "AUX" || upper == "NUL" ) { + return true; + } + if( upper.size() == 4 && ( upper.rfind("COM", 0) == 0 || upper.rfind("LPT", 0) == 0 ) + && upper[3] >= '1' && upper[3] <= '9' ) { + return true; + } + return false; +} + +bool isLikelyRetailGameTitleId(u64 titleId) { + constexpr u64 kApplicationPrefixMask = 0xFF00000000000000ULL; + constexpr u64 kRetailApplicationPrefix = 0x0100000000000000ULL; + constexpr u64 kSystemApplicationMax = 0x010000000000FFFFULL; + + if( ( titleId & kApplicationPrefixMask ) != kRetailApplicationPrefix ) { + return false; + } + if( titleId <= kSystemApplicationMax ) { + return false; + } + + // Base game application IDs normally end in 000. Updates/DLC/system/homebrew + // applets/forwarders often land outside this shape and should not get mod folders. + return ( titleId & 0xFFFULL ) == 0; +} + +std::string formatTitleId(u64 titleId) { + char out[17]{}; + std::snprintf(out, sizeof(out), "%016llX", static_cast(titleId)); + return out; +} + +std::string sanitizeGameFolderName(const std::string& rawName, u64 titleId) { + std::string out; + out.reserve(rawName.size()); + + bool lastWasSpace = false; + for( unsigned char c : rawName ) { + const bool invalidPathChar = c < 0x20 + || c == '<' || c == '>' || c == ':' + || c == '"' || c == '/' || c == '\\' + || c == '|' || c == '?' || c == '*'; + char next = invalidPathChar ? ' ' : static_cast(c); + if( next == '\t' ) { + next = ' '; + } + if( next == ' ' ) { + if( lastWasSpace ) { + continue; + } + lastWasSpace = true; + } + else { + lastWasSpace = false; + } + out.push_back(next); + } + + out = trimFolderName(out); + if( out.empty() ) { + out = formatTitleId(titleId); + } + if( isWindowsReservedName(out) ) { + out += "_"; + } + return out; +} + +bool hasApplicationContentMeta(u64 titleId) { + s32 totalMeta = 0; + const Result countRc = nsCountApplicationContentMeta(titleId, &totalMeta); + if( R_FAILED(countRc) ) { + LogWarning << "Could not count content meta for title " << formatTitleId(titleId) + << ": 0x" << std::hex << countRc << std::dec << std::endl; + return true; + } + if( totalMeta <= 0 ) { + return false; + } + + constexpr s32 batchSize = 16; + std::vector statuses(batchSize); + for( s32 offset = 0; offset < totalMeta; ) { + s32 entryCount = 0; + const Result listRc = nsListApplicationContentMetaStatus( + titleId, + offset, + statuses.data(), + batchSize, + &entryCount + ); + if( R_FAILED(listRc) ) { + LogWarning << "Could not list content meta for title " << formatTitleId(titleId) + << ": 0x" << std::hex << listRc << std::dec << std::endl; + return true; + } + if( entryCount <= 0 ) { + break; + } + + for( s32 i = 0; i < entryCount; ++i ) { + if( statuses[i].meta_type == NcmContentMetaType_Application + && statuses[i].application_id == titleId ) { + return true; + } + } + + offset += entryCount; + } + + return false; +} + +std::string cleanTitleIdText(std::string titleId) { + titleId.erase( + std::remove_if(titleId.begin(), titleId.end(), [](unsigned char c) { + return std::isspace(c) != 0; + }), + titleId.end() + ); + titleId = toUpperAscii(titleId); + if( titleId.size() != 16 ) { + return {}; + } + for( char c : titleId ) { + if( !std::isxdigit(static_cast(c)) ) { + return {}; + } + } + return titleId; +} + +std::string getTitleIdMetadataPath(const std::string& gameFolderPath) { + return GenericToolbox::joinPath(gameFolderPath, kTitleIdMetadataFile); +} + +void writeGameFolderTitleIdMetadata(const std::string& gameFolderPath, u64 titleId) { + const std::string metadataPath = getTitleIdMetadataPath(gameFolderPath); + const std::string titleIdText = formatTitleId(titleId); + if( cleanTitleIdText(GenericToolbox::dumpFileAsString(metadataPath)) == titleIdText ) { + return; + } + GenericToolbox::dumpStringInFile(metadataPath, titleIdText + "\n"); +} + +uint8_t* copyCachedIcon(const std::vector& cachedIcon) { + if( cachedIcon.empty() ) { + return nullptr; + } + auto* out = new uint8_t[cachedIcon.size()]; + std::memcpy(out, cachedIcon.data(), cachedIcon.size()); + return out; +} + +std::string getApplicationName(NsApplicationControlData& controlData) { + NacpLanguageEntry* langEntry = nullptr; + if( R_SUCCEEDED(nsGetApplicationDesiredLanguage(&controlData.nacp, &langEntry)) + && langEntry != nullptr + && langEntry->name[0] != '\0' ) { + return langEntry->name; + } + + for( auto& entry : controlData.nacp.lang ) { + if( entry.name[0] != '\0' ) { + return entry.name; + } + } + return {}; +} + +std::string getApplicationAuthor(NsApplicationControlData& controlData) { + NacpLanguageEntry* langEntry = nullptr; + if( R_SUCCEEDED(nsGetApplicationDesiredLanguage(&controlData.nacp, &langEntry)) + && langEntry != nullptr + && langEntry->author[0] != '\0' ) { + return langEntry->author; + } + + for( auto& entry : controlData.nacp.lang ) { + if( entry.author[0] != '\0' ) { + return entry.author; + } + } + return {}; +} + +bool isKnownHomebrewTitle(const std::string& name, const std::string& author) { + const std::string haystack = GenericToolbox::toLowerCase(name + " " + author); + const std::vector patterns{ + "homebrew", + "hbmenu", + "switchbrew", + "devkitpro", + "simplemodmanager", + "tinfoil", + "goldleaf", + "dbi", + "awoo installer", + "tinwoo", + "checkpoint", + "edizon", + "nx-shell", + "ftpd", + "daybreak" + }; + + for( const auto& pattern : patterns ) { + if( haystack.find(pattern) != std::string::npos ) { + return true; + } + } + return false; +} + +} // namespace namespace Toolbox{ //! External function @@ -19,4 +285,190 @@ namespace Toolbox{ return ss.str(); } -} \ No newline at end of file + void ensureModsRootFolder() { + if( mkdir( "sdmc:/mods", 0777 ) != 0 and errno != EEXIST ) { + LogError << "Could not create sdmc:/mods: " << std::strerror( errno ) << std::endl; + } + } + + std::string getGameFolderTitleId(const std::string& gameFolderPath_) { + std::string titleId = cleanTitleIdText( + GenericToolbox::dumpFileAsString(getTitleIdMetadataPath(gameFolderPath_)) + ); + if( !titleId.empty() ) { + return titleId; + } + + titleId = cleanTitleIdText(GenericToolbox::Switch::Utils::lookForTidInSubFolders(gameFolderPath_)); + if( !titleId.empty() ) { + return titleId; + } + + return {}; + } + + uint8_t* getGameFolderIcon(const std::string& gameFolderPath_) { + const std::string titleId = getGameFolderTitleId(gameFolderPath_); + if( titleId.empty() ) { + return nullptr; + } + + auto cacheIt = g_gameIconCache.find(titleId); + if( cacheIt != g_gameIconCache.end() ) { + return copyCachedIcon(cacheIt->second); + } + + auto availabilityIt = g_gameIconAvailabilityCache.find(titleId); + if( availabilityIt != g_gameIconAvailabilityCache.end() and not availabilityIt->second ) { + return nullptr; + } + + auto* icon = GenericToolbox::Switch::Utils::getIconFromTitleId(titleId); + if( icon == nullptr ) { + g_gameIconAvailabilityCache[titleId] = false; + return nullptr; + } + + g_gameIconAvailabilityCache[titleId] = true; + g_gameIconCache[titleId] = std::vector(icon, icon + kSwitchIconBufferSize); + return icon; + } + + bool hasGameFolderIcon(const std::string& gameFolderPath_) { + const std::string titleId = getGameFolderTitleId(gameFolderPath_); + if( titleId.empty() ) { + return false; + } + + auto availabilityIt = g_gameIconAvailabilityCache.find(titleId); + if( availabilityIt != g_gameIconAvailabilityCache.end() ) { + return availabilityIt->second; + } + + auto* icon = getGameFolderIcon(gameFolderPath_); + if( icon == nullptr ) { + return false; + } + delete[] icon; + return true; + } + + void ensureInstalledGameModFolders(const std::string& modsRootFolder_) { + std::string modsRoot = modsRootFolder_.empty() ? "/mods" : modsRootFolder_; + GenericToolbox::mkdir(modsRoot); + + constexpr s32 batchSize = 128; + std::vector records(batchSize); + auto controlData = std::make_unique(); + std::set namesUsedThisRun; + + s32 offset = 0; + s32 detected = 0; + s32 created = 0; + s32 skipped = 0; + s32 failed = 0; + + while( true ) { + s32 entryCount = 0; + const Result listRc = nsListApplicationRecord(records.data(), batchSize, offset, &entryCount); + if( R_FAILED(listRc) ) { + LogError << "Could not list installed applications: 0x" << std::hex << listRc << std::dec << std::endl; + break; + } + if( entryCount <= 0 ) { + break; + } + + for( s32 i = 0; i < entryCount; ++i ) { + const u64 titleId = records[i].application_id; + if( titleId == 0 ) { + continue; + } + if( !isLikelyRetailGameTitleId(titleId) ) { + ++skipped; + LogDebug << "Skipped non-game title id " << formatTitleId(titleId) << std::endl; + continue; + } + if( !hasApplicationContentMeta(titleId) ) { + ++skipped; + LogDebug << "Skipped title without application content meta " << formatTitleId(titleId) << std::endl; + continue; + } + + std::memset(controlData.get(), 0, sizeof(NsApplicationControlData)); + u64 actualSize = 0; + Result controlRc = nsGetApplicationControlData( + NsApplicationControlSource_CacheOnly, + titleId, + controlData.get(), + sizeof(NsApplicationControlData), + &actualSize + ); + if( R_FAILED(controlRc) ) { + controlRc = nsGetApplicationControlData( + NsApplicationControlSource_Storage, + titleId, + controlData.get(), + sizeof(NsApplicationControlData), + &actualSize + ); + } + if( R_FAILED(controlRc) ) { + LogWarning << "Could not read control data for title " << formatTitleId(titleId) + << ": 0x" << std::hex << controlRc << std::dec << std::endl; + continue; + } + + const std::string applicationName = getApplicationName(*controlData); + const std::string applicationAuthor = getApplicationAuthor(*controlData); + if( isKnownHomebrewTitle(applicationName, applicationAuthor) ) { + ++skipped; + LogDebug << "Skipped known homebrew title " << applicationName + << " (" << formatTitleId(titleId) << ")" << std::endl; + continue; + } + + auto* icon = GenericToolbox::Switch::Utils::getIconFromTitleId(formatTitleId(titleId)); + if( icon == nullptr ) { + ++skipped; + LogDebug << "Skipped title without icon " << applicationName + << " (" << formatTitleId(titleId) << ")" << std::endl; + continue; + } + delete[] icon; + + std::string folderName = sanitizeGameFolderName(applicationName, titleId); + if( namesUsedThisRun.count(toUpperAscii(folderName)) != 0 ) { + folderName += " [" + formatTitleId(titleId) + "]"; + } + namesUsedThisRun.insert(toUpperAscii(folderName)); + + const std::string folderPath = GenericToolbox::joinPath(modsRoot, folderName); + const bool alreadyExists = GenericToolbox::isDir(folderPath); + if( GenericToolbox::mkdir(folderPath) ) { + writeGameFolderTitleIdMetadata(folderPath, titleId); + ++detected; + if( !alreadyExists ) { + ++created; + LogInfo << "Created mod folder for " << folderName << std::endl; + } + } + else { + ++failed; + LogWarning << "Could not create mod folder: " << folderPath << std::endl; + } + } + + offset += entryCount; + if( entryCount < batchSize ) { + break; + } + } + + LogInfo << "Installed game folder sync done: " << detected + << " detected, " << created + << " created, " << skipped + << " skipped, " << failed << " failed." << std::endl; + } + +} diff --git a/src/ModManagerGui/CoreExtension/CMakeLists.txt b/src/ModManagerGui/CoreExtension/CMakeLists.txt index 4c58927..33da318 100644 --- a/src/ModManagerGui/CoreExtension/CMakeLists.txt +++ b/src/ModManagerGui/CoreExtension/CMakeLists.txt @@ -4,6 +4,7 @@ set( SRC_FILES ${CMAKE_CURRENT_SOURCE_DIR}/src/GuiModManager.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/SystemStatusOverlay.cpp ) add_library( CoreExtension STATIC ${SRC_FILES} ) diff --git a/src/ModManagerGui/CoreExtension/include/GuiModManager.h b/src/ModManagerGui/CoreExtension/include/GuiModManager.h index b2f8cf4..d870d22 100644 --- a/src/ModManagerGui/CoreExtension/include/GuiModManager.h +++ b/src/ModManagerGui/CoreExtension/include/GuiModManager.h @@ -13,6 +13,7 @@ #include #include +#include class GuiModManager { @@ -22,21 +23,29 @@ class GuiModManager { // setters void setTriggerUpdateModsDisplayedStatus(bool triggerUpdateModsDisplayedStatus); + void setTriggerRebuildModBrowser(bool triggerRebuildModBrowser); // getters [[nodiscard]] bool isTriggerUpdateModsDisplayedStatus() const; + [[nodiscard]] bool isTriggerRebuildModBrowser() const; const GameBrowser &getGameBrowser() const; GameBrowser &getGameBrowser(); void startApplyModThread(const std::string& modName_); void startRemoveModThread(const std::string& modName_); + bool startDeleteModFolderThread(const std::string& modName_); + bool startDeleteOrphanInstalledModsThread(const std::vector& modNameList_); void startCheckAllModsThread(); void startRemoveAllModsThread(); void startApplyModPresetThread(const std::string &modPresetName_); - - void applyMod(const std::string &modName_); - void applyModsList(std::vector& modsList_); - void removeMod(const std::string &modName_); + bool isBackgroundTaskRunning() const; + bool canStartDeleteModFolderThread() const; + + bool applyMod(const std::string &modName_); + bool applyModsList(std::vector& modsList_); + bool removeMod(const std::string &modName_); + bool deleteModFolderFromSd(const std::string &modName_); + bool deleteOrphanInstalledMods(const std::vector& modNameList_); void removeAllMods(); void checkAllMods(bool useCache_ = false); void getModStatus(const std::string &modName_, bool useCache_ = false); @@ -45,10 +54,17 @@ class GuiModManager { bool applyModFunction(const std::string& modName_); bool applyModPresetFunction(const std::string& presetName_); bool removeModFunction(const std::string& modName_); + bool deleteModFolderFunction(const std::string& modName_); + bool deleteOrphanInstalledModsFunction(std::vector modNameList_); bool checkAllModsFunction(); bool removeAllModsFunction(); + bool removeModInstalledFiles(const std::string &modName_, bool forceUnknownInstalledFiles_); + bool cleanupInstalledFilesByRelativePaths(const std::vector& relativePathList_, const std::string& label_, bool allowCurrentSdOwnedFiles_); + bool deleteOrphanInstalledModsPass(const std::vector& modNameList_, bool forceDelete_); + void finalizeModFilesystemChanges(const std::string& title_); bool leaveModAction(bool isSuccess_); + void finishDeleteModFolderTask(); @@ -60,6 +76,9 @@ class GuiModManager { bool _triggeredOnCancel_{false}; bool _triggerUpdateModsDisplayedStatus_{false}; + bool _triggerRebuildModBrowser_{false}; + std::atomic _deleteModFolderRunning_{false}; + std::atomic _lastDeleteModFolderFinishedMs_{0}; // monitors @@ -93,6 +112,15 @@ class GuiModManager { std::string currentMod{}; }; ModRemoveAllMonitor modRemoveAllMonitor{}; + struct ModDeleteFolderMonitor{ + double progress{0}; + std::string currentEntry{}; + }; ModDeleteFolderMonitor modDeleteFolderMonitor{}; + + struct ModFinalizeMonitor{ + double progress{0}; + std::string currentStep{}; + }; ModFinalizeMonitor modFinalizeMonitor{}; }; diff --git a/src/ModManagerGui/CoreExtension/include/SystemStatusOverlay.h b/src/ModManagerGui/CoreExtension/include/SystemStatusOverlay.h new file mode 100644 index 0000000..895bf29 --- /dev/null +++ b/src/ModManagerGui/CoreExtension/include/SystemStatusOverlay.h @@ -0,0 +1,15 @@ +#ifndef SIMPLEMODMANAGER_SYSTEMSTATUSOVERLAY_H +#define SIMPLEMODMANAGER_SYSTEMSTATUSOVERLAY_H + +#include +#include +#include + +namespace SystemStatusOverlay { + +void draw(NVGcontext* vg, int x, int y, unsigned width, brls::Style* style, brls::FrameContext* ctx); +void shutdown(); + +} + +#endif //SIMPLEMODMANAGER_SYSTEMSTATUSOVERLAY_H diff --git a/src/ModManagerGui/CoreExtension/src/GuiModManager.cpp b/src/ModManagerGui/CoreExtension/src/GuiModManager.cpp index b87e843..e07c279 100644 --- a/src/ModManagerGui/CoreExtension/src/GuiModManager.cpp +++ b/src/ModManagerGui/CoreExtension/src/GuiModManager.cpp @@ -12,26 +12,468 @@ #include #include #include +#include +#include +#include +#include +#include +#include +#include +#include LoggerInit([]{ Logger::setUserHeaderStr("[GuiModManager]"); }); +namespace { + +constexpr long long kDeleteModFolderCooldownMs = 750; +constexpr long long kDeleteModFolderFsSettleNs = 1000000000; // 1s +constexpr long long kSdmcCommitSettleNs = 500000000; // 500ms +constexpr int kFinalSdmcCommitPasses = 6; +constexpr long long kFinalSdmcCommitSettleNs = 250000000; // 250ms + +long long monotonicMs() { + const auto now = std::chrono::steady_clock::now(); + return std::chrono::duration_cast(now.time_since_epoch()).count(); +} + +void settleSdmcWrites(long long settleNs_ = kSdmcCommitSettleNs) { + const Result rc = fsdevCommitDevice("sdmc"); + if( R_FAILED(rc) ){ + LogWarning << "fsdevCommitDevice(sdmc) failed: 0x" << std::hex << rc << std::dec << std::endl; + } + svcSleepThread(settleNs_); +} + +bool safeIsDir(const std::string& path_) { + try { return GenericToolbox::isDir(path_); } + catch(...) { return false; } +} + +bool safeIsFile(const std::string& path_) { + try { return GenericToolbox::isFile(path_); } + catch(...) { return false; } +} + +bool safeIsDirEmpty(const std::string& path_) { + try { return GenericToolbox::isDirEmpty(path_); } + catch(...) { return false; } +} + +bool safeRmFile(const std::string& path_) { + for( int attempt = 0; attempt < 10; ++attempt ) { + if( !safeIsFile(path_) ) { + return true; + } + try { + if( GenericToolbox::rm(path_) ) { + return true; + } + } + catch(...) {} + svcSleepThread(50000000); // 50ms + } + return !safeIsFile(path_); +} + +bool safeRmDir(const std::string& path_) { + for( int attempt = 0; attempt < 10; ++attempt ) { + if( !safeIsDir(path_) ) { + return true; + } + try { + if( GenericToolbox::rmDir(path_) ) { + return true; + } + } + catch(...) {} + svcSleepThread(60000000); // 60ms + } + return !safeIsDir(path_); +} + +ssize_t safeGetFileSize(const std::string& path_) { + try { return GenericToolbox::getFileSize(path_); } + catch(...) { return -1; } +} + +bool safeRenameFile(const std::string& srcPath_, const std::string& dstPath_) { + for( int attempt = 0; attempt < 6; ++attempt ) { + if( std::rename(srcPath_.c_str(), dstPath_.c_str()) == 0 ) { + return true; + } + svcSleepThread(50000000); // 50ms + } + LogError << "Could not rename " << srcPath_ << " -> " << dstPath_ + << ": " << std::strerror(errno) << std::endl; + return false; +} + +bool copyFileDurable(const std::string& srcPath_, const std::string& dstPath_) { + if( !safeIsFile(srcPath_) ){ + return false; + } + + const std::string dstFolder = GenericToolbox::getFolderPath(dstPath_); + GenericToolbox::mkdir(dstFolder); + + const std::string dstFileName = GenericToolbox::getFileName(dstPath_); + if( dstFileName.empty() ){ + return false; + } + + const std::string tmpPath = GenericToolbox::joinPath(dstFolder, ".smm_tmp_" + dstFileName); + if( safeIsFile(tmpPath) && !safeRmFile(tmpPath) ){ + return false; + } + + const ssize_t srcSize = safeGetFileSize(srcPath_); + if( srcSize < 0 ){ + return false; + } + + bool copyOk = true; + size_t copiedBytes = 0; + GenericToolbox::Switch::Utils::b.progressMap["copyFile"] = srcSize == 0 ? 1. : 0.; + { + std::ifstream in(srcPath_, std::ios::in | std::ios::binary); + std::ofstream out(tmpPath, std::ios::out | std::ios::binary | std::ios::trunc); + if( !in || !out ){ + copyOk = false; + } + + constexpr size_t bufferSize = 1024 * 512; + std::vector buffer(bufferSize, 0); + while( copyOk && in ){ + in.read(buffer.data(), static_cast(buffer.size())); + const std::streamsize readBytes = in.gcount(); + if( readBytes > 0 ){ + out.write(buffer.data(), readBytes); + if( !out ){ + copyOk = false; + } + else{ + copiedBytes += static_cast(readBytes); + GenericToolbox::Switch::Utils::b.progressMap["copyFile"] = + srcSize == 0 ? 1. : std::min(1., double(copiedBytes) / double(srcSize)); + } + } + } + + if( copyOk ){ + out.flush(); + if( !out ){ + copyOk = false; + } + } + } + + if( !copyOk ){ + safeRmFile(tmpPath); + return false; + } + + if( safeGetFileSize(tmpPath) != srcSize ){ + safeRmFile(tmpPath); + return false; + } + + if( safeIsFile(dstPath_) && !safeRmFile(dstPath_) ){ + safeRmFile(tmpPath); + return false; + } + + if( !safeRenameFile(tmpPath, dstPath_) ){ + safeRmFile(tmpPath); + return false; + } + + const bool success = safeIsFile(dstPath_) && safeGetFileSize(dstPath_) == srcSize; + GenericToolbox::Switch::Utils::b.progressMap["copyFile"] = success ? 1. : 0.; + return success; +} + +void cleanupEmptyParentDirs(const std::string& filePath_, const std::string& installBase_) { + std::string emptyFolderCandidate = GenericToolbox::getFolderPath(filePath_); + int safetyCounter = 0; + while( safeIsDirEmpty(emptyFolderCandidate) ) { + if( emptyFolderCandidate.empty() ) { + break; + } + if( emptyFolderCandidate.find(installBase_) != 0 ) { + break; + } + if( emptyFolderCandidate == installBase_ ) { + break; + } + if( safetyCounter++ > 50 ) { + break; + } + if( !safeRmDir(emptyFolderCandidate) ) { + break; + } + svcSleepThread(20000000); // 20ms + emptyFolderCandidate = GenericToolbox::getFolderPath(emptyFolderCandidate); + } +} + +bool isRelativePathOwnedByActiveCurrentSdMod(ModManager& modManager_, const std::string& relativePath_) { + const std::string currentPreset = modManager_.fetchCurrentPreset().name; + const std::string dstPath = GenericToolbox::joinPath(modManager_.fetchCurrentPreset().installBaseFolder, relativePath_); + if( !safeIsFile(dstPath) ){ + return false; + } + + for( const auto& mod : modManager_.getModList() ) { + auto cacheIt = mod.applyCache.find(currentPreset); + if( cacheIt == mod.applyCache.end() || cacheIt->second.statusStr != "ACTIVE" ){ + continue; + } + + const std::string modFilePath = GenericToolbox::joinPath( + GenericToolbox::joinPath(modManager_.getGameFolderPath(), mod.modName), + relativePath_); + if( !safeIsFile(modFilePath) ){ + continue; + } + + try { + if( GenericToolbox::Switch::IO::doFilesAreIdentical(modFilePath, dstPath) ){ + return true; + } + } + catch(...) {} + } + return false; +} + +bool isRelativePathOwnedByActiveOtherMod( + ModManager& modManager_, + const std::string& currentModName_, + const std::string& relativePath_ ){ + const std::string currentPreset = modManager_.fetchCurrentPreset().name; + const std::string dstPath = GenericToolbox::joinPath(modManager_.fetchCurrentPreset().installBaseFolder, relativePath_); + if( !safeIsFile(dstPath) ){ + return false; + } + + for( const auto& otherMod : modManager_.getModList() ){ + if( otherMod.modName == currentModName_ ){ + continue; + } + + auto cacheIt = otherMod.applyCache.find(currentPreset); + if( cacheIt == otherMod.applyCache.end() || cacheIt->second.statusStr != "ACTIVE" ){ + continue; + } + + const std::string otherModFilePath = GenericToolbox::joinPath( + GenericToolbox::joinPath(modManager_.getGameFolderPath(), otherMod.modName), + relativePath_ ); + if( !safeIsFile(otherModFilePath) ){ + continue; + } + + try { + if( GenericToolbox::Switch::IO::doFilesAreIdentical(otherModFilePath, dstPath) ){ + return true; + } + } + catch(...) {} + } + + return false; +} + +const PresetConfig* findPresetConfig(ModManager& modManager_, const std::string& presetName_) { + for( const auto& candidatePreset : modManager_.getConfig().presetList ) { + if( candidatePreset.name == presetName_ ) { + return &candidatePreset; + } + } + return nullptr; +} + +std::string getCurrentModStatus(ModManager& modManager_, const std::string& modName_) { + const int modIndex = modManager_.getModIndex(modName_); + if( modIndex < 0 || modIndex >= int(modManager_.getModList().size()) ){ + return {}; + } + + return modManager_.getModList()[modIndex].getStatus(modManager_.fetchCurrentPreset().name); +} + +bool isDisabledStatus(const std::string& status_) { + return status_ == "INACTIVE" || status_ == "NO FILE"; +} + +bool orphanModStillHasUnownedInstalledFiles(ModManager& modManager_, const OrphanInstalledMod& orphanMod_, bool) { + for( const auto& presetCache : orphanMod_.applyCache ) { + const PresetConfig* preset = findPresetConfig(modManager_, presetCache.first); + if( preset == nullptr ) { + continue; + } + + for( const auto& fileCache : presetCache.second.fileStatusCache ) { + if( fileCache.first.empty() || GenericToolbox::getFileName(fileCache.first).empty() ) { + continue; + } + if( isRelativePathOwnedByActiveCurrentSdMod(modManager_, fileCache.first) ) { + continue; + } + + const std::string dstPath = GenericToolbox::joinPath(preset->installBaseFolder, fileCache.first); + if( safeIsFile(dstPath) ) { + return true; + } + } + } + return false; +} + +std::vector listVisibleRelativeFiles(const std::string& folderPath_) { + std::vector fileList; + try { + fileList = GenericToolbox::lsFilesRecursive(folderPath_); + } + catch(...) { + fileList.clear(); + } + GenericToolbox::removeEntryIf(fileList, [](const std::string& file){ + const auto fileName = GenericToolbox::getFileName(file); + return fileName.empty() || fileName[0] == '.'; + }); + std::sort(fileList.begin(), fileList.end()); + return fileList; +} + +void bumpDeleteProgress(double& progress_, size_t processedEntries_) { + const double p = 1.0 - (1.0 / double(processedEntries_ + 3)); + progress_ = std::min(0.97, p); +} + +bool deleteDirectoryTreeForSd( + const std::string& rootPath_, + bool& cancelRequested_, + std::string& currentEntry_, + double& progress_ ) { + if( !safeIsDir(rootPath_) ) { + progress_ = 1; + return true; + } + + bool success = true; + size_t processedEntries = 0; + std::vector dirsToRemove; + std::vector dirsToVisit{ rootPath_ }; + + while( !dirsToVisit.empty() ) { + if( cancelRequested_ ) { + return false; + } + + const std::string dir = dirsToVisit.back(); + dirsToVisit.pop_back(); + dirsToRemove.push_back(dir); + + std::vector entries; + try { + entries = GenericToolbox::ls(dir); + } + catch(...) { + success = false; + continue; + } + + for( const auto& entry : entries ) { + if( cancelRequested_ ) { + return false; + } + if( entry == "." || entry == ".." ) { + continue; + } + + const std::string childPath = GenericToolbox::joinPath(dir, entry); + currentEntry_ = entry; + + if( safeIsDir(childPath) ) { + dirsToVisit.push_back(childPath); + } + else if( safeIsFile(childPath) ) { + if( !safeRmFile(childPath) ) { + success = false; + } + processedEntries++; + bumpDeleteProgress(progress_, processedEntries); + } + } + } + + for( auto it = dirsToRemove.rbegin(); it != dirsToRemove.rend(); ++it ) { + if( cancelRequested_ ) { + return false; + } + currentEntry_ = GenericToolbox::getFileName(*it); + if( !safeRmDir(*it) ) { + success = false; + } + processedEntries++; + bumpDeleteProgress(progress_, processedEntries); + } + + progress_ = success ? 1 : progress_; + return success && !safeIsDir(rootPath_); +} + +} // namespace + void GuiModManager::setTriggerUpdateModsDisplayedStatus(bool triggerUpdateModsDisplayedStatus) { _triggerUpdateModsDisplayedStatus_ = triggerUpdateModsDisplayedStatus; } +void GuiModManager::setTriggerRebuildModBrowser(bool triggerRebuildModBrowser) { + _triggerRebuildModBrowser_ = triggerRebuildModBrowser; +} + bool GuiModManager::isTriggerUpdateModsDisplayedStatus() const { return _triggerUpdateModsDisplayedStatus_; } +bool GuiModManager::isTriggerRebuildModBrowser() const { + return _triggerRebuildModBrowser_; +} +bool GuiModManager::isBackgroundTaskRunning() const { + if( not _asyncResponse_.valid() ){ + return false; + } + return _asyncResponse_.wait_for(std::chrono::seconds(0)) != std::future_status::ready; +} +bool GuiModManager::canStartDeleteModFolderThread() const { + if( _triggerRebuildModBrowser_ ){ + return false; + } + if( this->isBackgroundTaskRunning() ){ + return false; + } + if( _deleteModFolderRunning_.load() ){ + return false; + } + if( brls::Application::hasViewDisappearing() ){ + return false; + } + + const long long lastDeleteFinished = _lastDeleteModFolderFinishedMs_.load(); + return lastDeleteFinished <= 0 || monotonicMs() - lastDeleteFinished >= kDeleteModFolderCooldownMs; +} const GameBrowser &GuiModManager::getGameBrowser() const { return _gameBrowser_; } GameBrowser &GuiModManager::getGameBrowser(){ return _gameBrowser_; } -void GuiModManager::applyMod(const std::string &modName_) { +bool GuiModManager::applyMod(const std::string &modName_) { LogWarning << __METHOD_NAME__ << ": " << modName_ << std::endl; modApplyMonitor = ModApplyMonitor(); + bool success = true; std::string modPath = GenericToolbox::joinPath(_gameBrowser_.getModManager().getGameFolderPath(), modName_); LogInfo << "Installing files in: " << modPath << std::endl; @@ -48,7 +490,10 @@ void GuiModManager::applyMod(const std::string &modName_) { LogInfo << modFilesList.size() << " files to be installed." << std::endl; for(size_t iFile = 0 ; iFile < modFilesList.size() ; iFile++){ - LogReturnIf( _triggeredOnCancel_, "Cancel detected. Leaving " << __METHOD_NAME__ ); + if( _triggeredOnCancel_ ){ + LogWarning << "Cancel detected. Leaving " << __METHOD_NAME__ << std::endl; + return false; + } if( GenericToolbox::getFileName(modFilesList[iFile])[0] == '.' ){ // ignoring cached files @@ -62,112 +507,388 @@ void GuiModManager::applyMod(const std::string &modName_) { modApplyMonitor.progress = double(iFile + 1) / double(modFilesList.size()); std::string installPath = GenericToolbox::joinPath(_gameBrowser_.getModManager().fetchCurrentPreset().installBaseFolder, modFilesList[iFile] ); - GenericToolbox::Switch::IO::copyFile(filePath, installPath); + if( !copyFileDurable(filePath, installPath) ){ + LogError << "Could not durably copy mod file: " << filePath << " -> " << installPath << std::endl; + success = false; + } } + settleSdmcWrites(); + _gameBrowser_.getModManager().claimOrphanInstalledFilesForMod(modName_); _gameBrowser_.getModManager().resetModCache(modName_); + return success; } void GuiModManager::getModStatus(const std::string &modName_, bool useCache_) { LogWarning << __METHOD_NAME__ << ": " << modName_ << ", with cache? " << useCache_ << std::endl; modCheckMonitor = ModCheckMonitor(); - // (XX/XX) Files Applied - // ACTIVE - // INACTIVE - std::string result; - auto& modManager = _gameBrowser_.getModManager(); - int modIndex = modManager.getModIndex( modName_ ); + const bool forceRecheck = !useCache_; + modCheckMonitor.currentFile = forceRecheck ? "Checking changed files..." : "Refreshing status cache..."; + modCheckMonitor.progress = 0.5; - // entry valid? - if( modIndex == -1 ){ return; } + auto result = modManager.refreshModStatus(modName_, forceRecheck); + if( result == ResultModAction::Fail ){ + LogWarning << "Could not refresh mod status: " << modName_ << std::endl; + } - // cached? - std::string configPresetName{modManager.fetchCurrentPreset().name}; - if( useCache_ and GenericToolbox::isIn(configPresetName, modManager.getModList()[modIndex].applyCache ) ){ - LogDebug << configPresetName << ":" << modManager.getModList()[modIndex].modName << " CACHED: " << modManager.getModList()[modIndex].applyCache[configPresetName].statusStr << std::endl; - return; + int modIndex = modManager.getModIndex(modName_); + if( modIndex != -1 ){ + const std::string configPresetName{modManager.fetchCurrentPreset().name}; + LogInfo << modName_ << " -> " << modManager.getModList()[modIndex].getStatus(configPresetName) << std::endl; } + modCheckMonitor.progress = 1; +} +bool GuiModManager::removeMod(const std::string &modName_){ + return this->removeModInstalledFiles(modName_, false); +} +bool GuiModManager::removeModInstalledFiles(const std::string &modName_, bool forceUnknownInstalledFiles_){ + LogWarning << __METHOD_NAME__ << ": " << modName_ << std::endl; + modRemoveMonitor = ModRemoveMonitor(); + bool success = true; + + auto& modManager = _gameBrowser_.getModManager(); + std::string modPath = GenericToolbox::joinPath( modManager.getGameFolderPath(), modName_ ); + auto modFileList = GenericToolbox::lsFilesRecursive(modPath); - // recheck? - auto& cacheEntry = modManager.getModList()[modIndex].applyCache[configPresetName]; - std::string modPath = GenericToolbox::joinPath(modManager.getGameFolderPath(), modName_ ); + int iFile{0}; + for(auto &modFile : modFileList){ + if( _triggeredOnCancel_ ){ + LogWarning << "Cancel detected. Leaving " << __METHOD_NAME__ << std::endl; + return false; + } - int sameFileCount = 0; + const auto modFileName = GenericToolbox::getFileName(modFile); + if( modFileName.empty() || modFileName[0] == '.' ){ + continue; + } - modCheckMonitor.currentFile = "Listing mod files..."; - std::vector modFileList = GenericToolbox::lsFilesRecursive(modPath); + modRemoveMonitor.currentFile = modFileName; + modRemoveMonitor.currentFile += " (" + GenericToolbox::joinAsString("/", iFile+1, modFileList.size()) + ")"; + modRemoveMonitor.progress = (iFile++ + 1.) / double(modFileList.size()); - for( size_t iFile = 0 ; iFile < modFileList.size() ; iFile++ ){ - LogReturnIf( _triggeredOnCancel_, "Cancel detected. Leaving " << __METHOD_NAME__ ); + std::string srcFilePath = GenericToolbox::joinPath( modPath, modFile ); + std::string dstFilePath = GenericToolbox::joinPath(modManager.fetchCurrentPreset().installBaseFolder, modFile ); - if( GenericToolbox::getFileName(modFileList[iFile])[0] == '.' ){ - // ignoring cached files - continue; + // Check if the installed mod belongs to the selected mod + bool shouldRemoveInstalledFile = false; + try { + shouldRemoveInstalledFile = GenericToolbox::Switch::IO::doFilesAreIdentical(srcFilePath, dstFilePath); } + catch(...) {} - std::string srcFilePath = GenericToolbox::joinPath(modPath, modFileList[iFile] ); - std::string dstFilePath = GenericToolbox::joinPath(modManager.fetchCurrentPreset().installBaseFolder, modFileList[iFile] ); + if( !shouldRemoveInstalledFile && forceUnknownInstalledFiles_ && safeIsFile(dstFilePath) ){ + shouldRemoveInstalledFile = !isRelativePathOwnedByActiveOtherMod(modManager, modName_, modFile); + } + + if( shouldRemoveInstalledFile && isRelativePathOwnedByActiveOtherMod(modManager, modName_, modFile) ){ + shouldRemoveInstalledFile = false; + } - modCheckMonitor.currentFile = GenericToolbox::getFileName(modFileList[iFile]); - modCheckMonitor.progress = (double(iFile) + 1.) / double(modFileList.size()); + if( shouldRemoveInstalledFile ){ + + // Remove the mod file with multiple retries + if( !safeRmFile(dstFilePath) ){ + LogError << "Could not remove installed mod file: " << dstFilePath << std::endl; + success = false; + continue; + } + + // Delete the folder if no other files is present + std::string emptyFolderCandidate = GenericToolbox::getFolderPath( dstFilePath ); + int safetyCounter = 0; + while( safeIsDirEmpty( emptyFolderCandidate ) ) { - if(GenericToolbox::Switch::IO::doFilesAreIdentical( - modManager.fetchCurrentPreset().installBaseFolder + "/" + modFileList[iFile], - srcFilePath - )){ sameFileCount++; } + // Safety check to prevent deleting system folders + std::string installBase = modManager.fetchCurrentPreset().installBaseFolder; + if( emptyFolderCandidate.find(installBase) != 0 ) break; + if( emptyFolderCandidate == installBase ) break; + // Safety counter to prevent infinite loops + if( safetyCounter++ > 50 ) break; + + // Delete directory with retries + if( !safeRmDir(emptyFolderCandidate) ){ + goto folder_cleanup_done; + } + + // Longer delay to prevent filesystem issues + svcSleepThread(20000000); // 20ms delay + + emptyFolderCandidate = GenericToolbox::getFolderPath(emptyFolderCandidate); + } + folder_cleanup_done:; + } + + // Small delay between file deletions to prevent filesystem overload + svcSleepThread(10000000); // 10ms delay } - cacheEntry.applyFraction = double(sameFileCount) / double(modFileList.size()); + settleSdmcWrites(); + _gameBrowser_.getModManager().resetModCache(modName_); + + return success; +} +bool GuiModManager::cleanupInstalledFilesByRelativePaths( + const std::vector& relativePathList_, + const std::string& label_, + bool allowCurrentSdOwnedFiles_ ){ + auto& modManager = _gameBrowser_.getModManager(); + size_t totalFiles{0}; + for( const auto& relativePath : relativePathList_ ){ + if( relativePath.empty() || GenericToolbox::getFileName(relativePath).empty() ){ + continue; + } + if( !allowCurrentSdOwnedFiles_ && isRelativePathOwnedByActiveCurrentSdMod(modManager, relativePath) ){ + continue; + } + + const std::string dstPath = GenericToolbox::joinPath(modManager.fetchCurrentPreset().installBaseFolder, relativePath); + if( safeIsFile(dstPath) ){ + totalFiles++; + } + } + + if( totalFiles == 0 ){ + return true; + } + + bool success = true; + size_t processedFiles{0}; + for( const auto& relativePath : relativePathList_ ){ + if( _triggeredOnCancel_ ){ + return false; + } + if( relativePath.empty() || GenericToolbox::getFileName(relativePath).empty() ){ + continue; + } + if( !allowCurrentSdOwnedFiles_ && isRelativePathOwnedByActiveCurrentSdMod(modManager, relativePath) ){ + continue; + } + + const std::string dstPath = GenericToolbox::joinPath(modManager.fetchCurrentPreset().installBaseFolder, relativePath); + if( !safeIsFile(dstPath) ){ + continue; + } + + modDeleteFolderMonitor.currentEntry = label_ + ": " + GenericToolbox::getFileName(relativePath); + if( !safeRmFile(dstPath) ){ + success = false; + LogError << "Could not remove leftover installed file: " << dstPath << std::endl; + } + for( int attempt = 0; attempt < 5 && safeIsFile(dstPath); ++attempt ){ + svcSleepThread(50000000); // 50ms + } + if( safeIsFile(dstPath) ){ + success = false; + LogError << "Leftover installed file is still present after delete: " << dstPath << std::endl; + } + else{ + cleanupEmptyParentDirs(dstPath, modManager.fetchCurrentPreset().installBaseFolder); + } - if ( modFileList.empty() ) cacheEntry.statusStr = "NO FILE"; - else if( cacheEntry.applyFraction == 0 ) cacheEntry.statusStr = "INACTIVE"; - else if( cacheEntry.applyFraction == 1 ) cacheEntry.statusStr = "ACTIVE"; - else cacheEntry.statusStr = "PARTIAL (" + GenericToolbox::joinAsString("/", sameFileCount, modFileList.size()) + ")"; + processedFiles++; + modDeleteFolderMonitor.progress = double(processedFiles) / double(totalFiles); + svcSleepThread(10000000); // 10ms + } - LogInfo << modName_ << " -> " << cacheEntry.statusStr << std::endl; - modManager.dumpModStatusCache(); + settleSdmcWrites(); + return success; } -void GuiModManager::removeMod(const std::string &modName_){ +bool GuiModManager::deleteModFolderFromSd(const std::string &modName_) { LogWarning << __METHOD_NAME__ << ": " << modName_ << std::endl; - modRemoveMonitor = ModRemoveMonitor(); + modDeleteFolderMonitor = ModDeleteFolderMonitor(); + modDeleteFolderMonitor.currentEntry = "Preparing delete..."; - std::string modPath = GenericToolbox::joinPath( _gameBrowser_.getModManager().getGameFolderPath(), modName_ ); - auto modFileList = GenericToolbox::lsFilesRecursive(modPath); + auto& modManager = _gameBrowser_.getModManager(); + const std::string modFolderPath = GenericToolbox::joinPath(modManager.getGameFolderPath(), modName_); - int iFile{0}; - for(auto &modFile : modFileList){ - LogReturnIf( _triggeredOnCancel_, "Cancel detected. Leaving " << __METHOD_NAME__ ); + const bool deleted = deleteDirectoryTreeForSd( + modFolderPath, + _triggeredOnCancel_, + modDeleteFolderMonitor.currentEntry, + modDeleteFolderMonitor.progress ); - modRemoveMonitor.currentFile = GenericToolbox::getFileName(modFile); - modRemoveMonitor.currentFile += " (" + GenericToolbox::joinAsString("/", iFile+1, modFileList.size()) + ")"; - modRemoveMonitor.progress = (iFile++ + 1.) / double(modFileList.size()); + settleSdmcWrites(); - std::string srcFilePath = GenericToolbox::joinPath( modPath, modFile ); - std::string dstFilePath = GenericToolbox::joinPath(_gameBrowser_.getModManager().fetchCurrentPreset().installBaseFolder, modFile ); - // Check if the installed mod belongs to the selected mod - if( GenericToolbox::Switch::IO::doFilesAreIdentical(srcFilePath, dstFilePath ) ){ + bool stillPresentOnSd = true; + for( int attempt = 0; attempt < 10; ++attempt ) { + stillPresentOnSd = safeIsDir(modFolderPath); + if( !stillPresentOnSd ) { + break; + } + svcSleepThread(120000000); // 120ms + } - // Remove the mod file - GenericToolbox::rm( dstFilePath ); + if( deleted && !stillPresentOnSd ) { + modDeleteFolderMonitor.currentEntry = "Deleted."; + modDeleteFolderMonitor.progress = 1; + LogInfo << "Deleted mod folder: " << modFolderPath << std::endl; + return true; + } - // Delete the folder if no other files is present - std::string emptyFolderCandidate = GenericToolbox::getFolderPath( dstFilePath ); - while( GenericToolbox::isDirEmpty( emptyFolderCandidate ) ) { + modDeleteFolderMonitor.currentEntry = "Delete failed."; + LogError << "Failed to delete mod folder: " << modFolderPath << " -> " + << (deleted ? "folder still exists" : "recursive delete failed") << std::endl; + return false; +} +bool GuiModManager::deleteOrphanInstalledModsPass(const std::vector& modNameList_, bool forceDelete_) { + auto& modManager = _gameBrowser_.getModManager(); + size_t totalFiles{0}; + for( const auto& modName : modNameList_ ) { + const int orphanIndex = modManager.getOrphanInstalledModIndex(modName); + if( orphanIndex == -1 ) { + continue; + } + const auto& orphanMod = modManager.getOrphanInstalledModList()[orphanIndex]; + for( const auto& presetCache : orphanMod.applyCache ) { + const PresetConfig* preset = findPresetConfig(modManager, presetCache.first); + if( preset == nullptr ) { + continue; + } - GenericToolbox::rmDir( emptyFolderCandidate ); + for( const auto& fileCache : presetCache.second.fileStatusCache ) { + if( fileCache.first.empty() || GenericToolbox::getFileName(fileCache.first).empty() ) { + continue; + } + if( isRelativePathOwnedByActiveCurrentSdMod(modManager, fileCache.first) ) { + continue; + } + + const std::string dstPath = GenericToolbox::joinPath(preset->installBaseFolder, fileCache.first); + if( safeIsFile(dstPath) ) { + totalFiles++; + } + } + } + } - auto subFolderList = GenericToolbox::splitString(emptyFolderCandidate, "/"); - if( subFolderList.empty() ){ break; } - // decrement folder depth - emptyFolderCandidate = "/" + GenericToolbox::joinVectorString( subFolderList, "/", 0, int(subFolderList.size()) - 1 ); + bool success = true; + if( totalFiles == 0 ) { + for( const auto& modName : modNameList_ ) { + const int orphanIndex = modManager.getOrphanInstalledModIndex(modName); + if( orphanIndex == -1 ) { + continue; + } + const auto orphanMod = modManager.getOrphanInstalledModList()[orphanIndex]; + if( !orphanModStillHasUnownedInstalledFiles(modManager, orphanMod, forceDelete_) ) { + modManager.removeOrphanInstalledModCache(modName); } } + modDeleteFolderMonitor.currentEntry = "Nothing to delete."; + modDeleteFolderMonitor.progress = 1; } + else { + size_t processedFiles{0}; + for( const auto& modName : modNameList_ ) { + if( _triggeredOnCancel_ ) { + return false; + } - _gameBrowser_.getModManager().resetModCache(modName_); + const int orphanIndex = modManager.getOrphanInstalledModIndex(modName); + if( orphanIndex == -1 ) { + continue; + } + + const auto orphanMod = modManager.getOrphanInstalledModList()[orphanIndex]; + for( const auto& presetCache : orphanMod.applyCache ) { + const PresetConfig* preset = findPresetConfig(modManager, presetCache.first); + if( preset == nullptr ) { + continue; + } + + for( const auto& fileCache : presetCache.second.fileStatusCache ) { + if( _triggeredOnCancel_ ) { + return false; + } + if( fileCache.first.empty() || GenericToolbox::getFileName(fileCache.first).empty() ) { + continue; + } + if( isRelativePathOwnedByActiveCurrentSdMod(modManager, fileCache.first) ) { + continue; + } + + const std::string dstPath = GenericToolbox::joinPath(preset->installBaseFolder, fileCache.first); + if( !safeIsFile(dstPath) ) { + continue; + } + + modDeleteFolderMonitor.currentEntry = modName + ": " + GenericToolbox::getFileName(fileCache.first); + if( !safeRmFile(dstPath) ) { + success = false; + LogError << "Could not remove orphan installed file: " << dstPath << std::endl; + } + const int settleAttempts = forceDelete_ ? 10 : 5; + for( int attempt = 0; attempt < settleAttempts && safeIsFile(dstPath); ++attempt ) { + svcSleepThread(forceDelete_ ? 80000000 : 50000000); + if( forceDelete_ ){ + safeRmFile(dstPath); + } + } + if( safeIsFile(dstPath) ) { + success = false; + LogError << "Orphan installed file is still present after delete: " << dstPath << std::endl; + } + else { + cleanupEmptyParentDirs(dstPath, preset->installBaseFolder); + } + + processedFiles++; + modDeleteFolderMonitor.progress = double(processedFiles) / double(totalFiles); + svcSleepThread(10000000); // 10ms + } + } + + if( orphanModStillHasUnownedInstalledFiles(modManager, orphanMod, forceDelete_) ) { + success = false; + } + else { + modManager.removeOrphanInstalledModCache(modName); + } + } + } + + settleSdmcWrites(); + return success; +} + +bool GuiModManager::deleteOrphanInstalledMods(const std::vector& modNameList_) { + LogWarning << __METHOD_NAME__ << ": " << GenericToolbox::toString(modNameList_) << std::endl; + modDeleteFolderMonitor = ModDeleteFolderMonitor(); + modDeleteFolderMonitor.currentEntry = "Preparing cleanup..."; + + auto& modManager = _gameBrowser_.getModManager(); + bool success = this->deleteOrphanInstalledModsPass(modNameList_, false); + if( _triggeredOnCancel_ ){ + return false; + } + + modDeleteFolderMonitor.currentEntry = "Checking cleanup..."; + modDeleteFolderMonitor.progress = 0; + settleSdmcWrites(); + modManager.refreshOrphanInstalledModList(); + std::vector remainingOrphanList; + for( const auto& orphanMod : modManager.getOrphanInstalledModList() ){ + remainingOrphanList.emplace_back(orphanMod.modName); + } + + if( !remainingOrphanList.empty() ){ + modDeleteFolderMonitor.currentEntry = "Forcing remaining cleanup..."; + modDeleteFolderMonitor.progress = 0; + success = this->deleteOrphanInstalledModsPass(remainingOrphanList, true) && success; + if( !_triggeredOnCancel_ ){ + modDeleteFolderMonitor.currentEntry = "Checking cleanup..."; + modDeleteFolderMonitor.progress = 0; + settleSdmcWrites(); + modManager.refreshOrphanInstalledModList(); + success = modManager.getOrphanInstalledModList().empty() && success; + } + } + + modDeleteFolderMonitor.currentEntry = success ? "Cleanup done." : "Cleanup partially failed."; + modDeleteFolderMonitor.progress = 1; + return success; } void GuiModManager::removeAllMods() { LogWarning << __METHOD_NAME__ << std::endl; @@ -185,9 +906,10 @@ void GuiModManager::removeAllMods() { } } -void GuiModManager::applyModsList(std::vector& modsList_){ +bool GuiModManager::applyModsList(std::vector& modsList_){ LogWarning << __METHOD_NAME__ << ": " << GenericToolbox::toString(modsList_) << std::endl; modApplyListMonitor = ModApplyListMonitor(); + bool success = true; // checking for overwritten files in advance: @@ -215,18 +937,22 @@ void GuiModManager::applyModsList(std::vector& modsList_){ // applying mods with ignored files for( size_t iMod = 0 ; iMod < modsList_.size() ; iMod++ ){ - LogReturnIf( _triggeredOnCancel_, "Cancel detected. Leaving " << __METHOD_NAME__ ); + if( _triggeredOnCancel_ ){ + LogWarning << "Cancel detected. Leaving " << __METHOD_NAME__ << std::endl; + return false; + } modApplyListMonitor.currentMod = modsList_[iMod]; modApplyListMonitor.currentMod += " (" + std::to_string(iMod + 1) + "/" + std::to_string(modsList_.size()) + ")"; modApplyListMonitor.progress = (double(iMod) + 1.) / double(modsList_.size()); _gameBrowser_.getModManager().setIgnoredFileList(ignoredFileListPerMod[iMod]); - this->applyMod( modsList_[iMod] ); + success = this->applyMod( modsList_[iMod] ) && success; _gameBrowser_.getModManager().getIgnoredFileList().clear(); } + return success; } void GuiModManager::checkAllMods(bool useCache_) { LogWarning << __METHOD_NAME__ << ": with cache? " << useCache_ << std::endl; @@ -261,7 +987,73 @@ void GuiModManager::startRemoveModThread(const std::string& modName_){ // start the parallel thread _asyncResponse_ = std::async(&GuiModManager::removeModFunction, this, modName_); } +bool GuiModManager::startDeleteModFolderThread(const std::string& modName_){ + if( modName_.empty() ){ + LogWarning << "No mod name provided. Can't delete mod folder." << std::endl; + return false; + } + if( _triggerRebuildModBrowser_ ){ + brls::Application::notify("Refreshing mod list. Please wait."); + return false; + } + if( this->isBackgroundTaskRunning() ){ + brls::Application::notify("A mod task is already running."); + return false; + } + if( _deleteModFolderRunning_.load() ){ + brls::Application::notify("A mod delete is already finishing."); + return false; + } + if( brls::Application::hasViewDisappearing() ){ + brls::Application::notify("Please wait before deleting another mod."); + return false; + } + + const long long lastDeleteFinished = _lastDeleteModFolderFinishedMs_.load(); + if( lastDeleteFinished > 0 && monotonicMs() - lastDeleteFinished < kDeleteModFolderCooldownMs ){ + brls::Application::notify("Please wait before deleting another mod."); + return false; + } + + this->_triggeredOnCancel_ = false; + this->_triggerRebuildModBrowser_ = false; + _deleteModFolderRunning_.store(true); + + _asyncResponse_ = std::async(std::launch::async, &GuiModManager::deleteModFolderFunction, this, modName_); + return true; +} +bool GuiModManager::startDeleteOrphanInstalledModsThread(const std::vector& modNameList_){ + if( modNameList_.empty() ){ + return false; + } + if( _triggerRebuildModBrowser_ ){ + brls::Application::notify("Refreshing mod list. Please wait."); + return false; + } + if( this->isBackgroundTaskRunning() ){ + brls::Application::notify("A mod task is already running."); + return false; + } + if( _deleteModFolderRunning_.load() ){ + brls::Application::notify("A mod delete is already finishing."); + return false; + } + if( brls::Application::hasViewDisappearing() ){ + brls::Application::notify("Please wait before deleting installed files."); + return false; + } + + this->_triggeredOnCancel_ = false; + this->_triggerRebuildModBrowser_ = false; + _deleteModFolderRunning_.store(true); + + _asyncResponse_ = std::async(std::launch::async, &GuiModManager::deleteOrphanInstalledModsFunction, this, modNameList_); + return true; +} void GuiModManager::startCheckAllModsThread(){ + if( this->isBackgroundTaskRunning() ){ + return; + } this->_triggeredOnCancel_ = false; // start the parallel thread @@ -293,7 +1085,7 @@ bool GuiModManager::applyModFunction(const std::string& modName_){ _loadingPopup_.getMonitorView()->setSubTitlePtr( &modApplyMonitor.currentFile ); _loadingPopup_.getMonitorView()->setProgressFractionPtr( &modApplyMonitor.progress ); _loadingPopup_.getMonitorView()->setSubProgressFractionPtr( &GenericToolbox::Switch::Utils::b.progressMap["copyFile"] ); - this->applyMod( modName_ ); + bool success = this->applyMod( modName_ ); if( _triggeredOnCancel_ ){ return this->leaveModAction(false); } LogWarning << "Checking: " << modName_ << std::endl; @@ -305,8 +1097,15 @@ bool GuiModManager::applyModFunction(const std::string& modName_){ _loadingPopup_.getMonitorView()->setProgressFractionPtr( &modCheckMonitor.progress ); _loadingPopup_.getMonitorView()->setSubProgressFractionPtr(&GenericToolbox::Switch::Utils::b.progressMap["doFilesAreIdentical"]); this->getModStatus( modName_, false ); + _gameBrowser_.getModManager().refreshAllModStatusCache(false); + const std::string appliedStatus = getCurrentModStatus(_gameBrowser_.getModManager(), modName_); + if( appliedStatus != "ACTIVE" ){ + LogWarning << "Applied mod did not verify as ACTIVE: " << modName_ << " -> " << appliedStatus << std::endl; + success = false; + } + this->finalizeModFilesystemChanges("Finalizing install..."); - return this->leaveModAction(true); + return this->leaveModAction(success); } bool GuiModManager::applyModPresetFunction(const std::string& presetName_){ // push the progress bar to the view @@ -330,14 +1129,14 @@ bool GuiModManager::applyModPresetFunction(const std::string& presetName_){ _loadingPopup_.getMonitorView()->resetMonitorAddresses(); _loadingPopup_.getMonitorView()->setTitlePtr( &modApplyListMonitor.currentMod ); _loadingPopup_.getMonitorView()->setSubTitlePtr( &modApplyMonitor.currentFile ); - _loadingPopup_.getMonitorView()->setProgressFractionPtr( &modApplyMonitor.progress ); + _loadingPopup_.getMonitorView()->setProgressFractionPtr( &modApplyListMonitor.progress ); _loadingPopup_.getMonitorView()->setSubProgressFractionPtr(&GenericToolbox::Switch::Utils::b.progressMap["copyFile"]); std::vector modsList; for( auto& preset : _gameBrowser_.getModPresetHandler().getPresetList() ){ if( preset.name == presetName_ ){ modsList = preset.modList; break; } } - this->applyModsList(modsList); + bool success = this->applyModsList(modsList); if( _triggeredOnCancel_ ){ return this->leaveModAction(false); } LogInfo("Checking all mods status..."); @@ -347,12 +1146,13 @@ bool GuiModManager::applyModPresetFunction(const std::string& presetName_){ _loadingPopup_.getMonitorView()->resetMonitorAddresses(); _loadingPopup_.getMonitorView()->setTitlePtr( &modCheckAllMonitor.currentMod ); _loadingPopup_.getMonitorView()->setSubTitlePtr( &modCheckMonitor.currentFile ); - _loadingPopup_.getMonitorView()->setProgressFractionPtr( &modCheckMonitor.progress ); + _loadingPopup_.getMonitorView()->setProgressFractionPtr( &modCheckAllMonitor.progress ); _loadingPopup_.getMonitorView()->setSubProgressFractionPtr(&GenericToolbox::Switch::Utils::b.progressMap["doFilesAreIdentical"]); this->checkAllMods(); if( _triggeredOnCancel_ ){ return this->leaveModAction(false); } + this->finalizeModFilesystemChanges("Finalizing mod preset..."); - return this->leaveModAction(true); + return this->leaveModAction(success); } bool GuiModManager::removeModFunction(const std::string& modName_){ // push the progress bar to the view @@ -366,7 +1166,8 @@ bool GuiModManager::removeModFunction(const std::string& modName_){ _loadingPopup_.getMonitorView()->setTitlePtr(&modName_); _loadingPopup_.getMonitorView()->setSubTitlePtr( &modRemoveMonitor.currentFile ); _loadingPopup_.getMonitorView()->setProgressFractionPtr( &modRemoveMonitor.progress ); - this->removeMod( modName_ ); + _loadingPopup_.getMonitorView()->setSubProgressFractionPtr( &GenericToolbox::Switch::Utils::b.progressMap["doFilesAreIdentical"] ); + bool success = this->removeMod( modName_ ); if( _triggeredOnCancel_ ){ return this->leaveModAction(false); } LogWarning << "Checking: " << modName_ << std::endl; @@ -378,17 +1179,130 @@ bool GuiModManager::removeModFunction(const std::string& modName_){ _loadingPopup_.getMonitorView()->setProgressFractionPtr( &modCheckMonitor.progress ); _loadingPopup_.getMonitorView()->setSubProgressFractionPtr(&GenericToolbox::Switch::Utils::b.progressMap["doFilesAreIdentical"]); this->getModStatus(modName_); + std::string removedStatus = getCurrentModStatus(_gameBrowser_.getModManager(), modName_); + if( !isDisabledStatus(removedStatus) ){ + LogWarning << "Removed mod did not verify as disabled, forcing cleanup once: " + << modName_ << " -> " << removedStatus << std::endl; + success = this->removeModInstalledFiles(modName_, true) && success; + this->getModStatus(modName_, false); + removedStatus = getCurrentModStatus(_gameBrowser_.getModManager(), modName_); + } + if( !isDisabledStatus(removedStatus) ){ + LogWarning << "Removed mod is still not disabled after forced cleanup: " + << modName_ << " -> " << removedStatus << std::endl; + success = false; + } + _gameBrowser_.getModManager().refreshAllModStatusCache(false); + this->finalizeModFilesystemChanges("Finalizing removal..."); - return this->leaveModAction(true); + return this->leaveModAction(success); +} +bool GuiModManager::deleteModFolderFunction(const std::string& modName_){ + _loadingPopup_.pushView(); + _loadingPopup_.getMonitorView()->setExecOnDelete([this]{ this->_triggeredOnCancel_ = true; }); + + bool success = false; + try { + const std::string modFolderPath = GenericToolbox::joinPath( + _gameBrowser_.getModManager().getGameFolderPath(), + modName_ ); + const auto modFileListBeforeDelete = listVisibleRelativeFiles(modFolderPath); + + LogWarning << "Removing installed files before deleting mod folder: " << modName_ << std::endl; + _loadingPopup_.getMonitorView()->setHeaderTitle("Removing installed mod..."); + _loadingPopup_.getMonitorView()->setProgressColor(GenericToolbox::Borealis::redNvgColor); + _loadingPopup_.getMonitorView()->resetMonitorAddresses(); + _loadingPopup_.getMonitorView()->setTitlePtr(&modName_); + _loadingPopup_.getMonitorView()->setSubTitlePtr(&modRemoveMonitor.currentFile); + _loadingPopup_.getMonitorView()->setProgressFractionPtr(&modRemoveMonitor.progress); + _loadingPopup_.getMonitorView()->setSubProgressFractionPtr(&GenericToolbox::Switch::Utils::b.progressMap["doFilesAreIdentical"]); + + this->removeModInstalledFiles(modName_, true); + if( _triggeredOnCancel_ ){ + _triggerRebuildModBrowser_ = true; + this->finishDeleteModFolderTask(); + return this->leaveModAction(false); + } + + svcSleepThread(kDeleteModFolderFsSettleNs); + + LogWarning << "Deleting mod folder: " << modName_ << std::endl; + _loadingPopup_.getMonitorView()->setHeaderTitle("Deleting mod folder..."); + _loadingPopup_.getMonitorView()->resetMonitorAddresses(); + _loadingPopup_.getMonitorView()->setTitlePtr(&modName_); + _loadingPopup_.getMonitorView()->setSubTitlePtr(&modDeleteFolderMonitor.currentEntry); + _loadingPopup_.getMonitorView()->setProgressFractionPtr(&modDeleteFolderMonitor.progress); + + success = this->deleteModFolderFromSd(modName_); + if( success && !_triggeredOnCancel_ ){ + LogWarning << "Cleaning leftover installed files after deleting mod folder: " << modName_ << std::endl; + _loadingPopup_.getMonitorView()->setHeaderTitle("Cleaning leftover installed files..."); + _loadingPopup_.getMonitorView()->resetMonitorAddresses(); + _loadingPopup_.getMonitorView()->setTitlePtr(&modDeleteFolderMonitor.currentEntry); + _loadingPopup_.getMonitorView()->setProgressFractionPtr(&modDeleteFolderMonitor.progress); + success = this->cleanupInstalledFilesByRelativePaths(modFileListBeforeDelete, modName_, false) && success; + } + } + catch(...) { + LogError << "Unexpected exception while deleting mod folder: " << modName_ << std::endl; + modDeleteFolderMonitor.currentEntry = "Delete failed."; + modDeleteFolderMonitor.progress = 0; + success = false; + } + + this->finalizeModFilesystemChanges("Finalizing delete..."); + _triggerRebuildModBrowser_ = true; + this->finishDeleteModFolderTask(); + + return this->leaveModAction(success); } + +bool GuiModManager::deleteOrphanInstalledModsFunction(std::vector modNameList_){ + _loadingPopup_.pushView(); + _loadingPopup_.getMonitorView()->setExecOnDelete([this]{ this->_triggeredOnCancel_ = true; }); + + bool success = false; + try { + _loadingPopup_.getMonitorView()->setHeaderTitle("Deleting orphan installed mods..."); + _loadingPopup_.getMonitorView()->setProgressColor(GenericToolbox::Borealis::redNvgColor); + _loadingPopup_.getMonitorView()->resetMonitorAddresses(); + _loadingPopup_.getMonitorView()->setTitlePtr(&modDeleteFolderMonitor.currentEntry); + _loadingPopup_.getMonitorView()->setProgressFractionPtr(&modDeleteFolderMonitor.progress); + + success = this->deleteOrphanInstalledMods(modNameList_); + } + catch(...) { + LogError << "Unexpected exception while deleting orphan installed mods." << std::endl; + modDeleteFolderMonitor.currentEntry = "Cleanup failed."; + modDeleteFolderMonitor.progress = 0; + success = false; + } + + this->finalizeModFilesystemChanges("Finalizing cleanup..."); + _triggerRebuildModBrowser_ = true; + _triggerUpdateModsDisplayedStatus_ = false; + this->finishDeleteModFolderTask(); + + return this->leaveModAction(success); +} + +void GuiModManager::finishDeleteModFolderTask(){ + _lastDeleteModFolderFinishedMs_.store(monotonicMs()); + _deleteModFolderRunning_.store(false); +} + bool GuiModManager::checkAllModsFunction(){ // push the progress bar to the view _loadingPopup_.pushView(); _loadingPopup_.getMonitorView()->setExecOnDelete([this]{ this->_triggeredOnCancel_ = true; }); + LogInfo("Resetting mods cache before recheck..."); + _gameBrowser_.getModManager().resetAllModsCacheAndFile(); + _gameBrowser_.getModManager().refreshOrphanInstalledModList(); + LogInfo("Checking all mods status..."); _loadingPopup_.getMonitorView()->setProgressColor(GenericToolbox::Borealis::blueNvgColor); - _loadingPopup_.getMonitorView()->setHeaderTitle("Checking all mods status..."); + _loadingPopup_.getMonitorView()->setHeaderTitle("Rechecking all mods status..."); _loadingPopup_.getMonitorView()->resetMonitorAddresses(); _loadingPopup_.getMonitorView()->setTitlePtr( &modCheckAllMonitor.currentMod ); _loadingPopup_.getMonitorView()->setSubTitlePtr( &modCheckMonitor.currentFile ); @@ -422,14 +1336,43 @@ bool GuiModManager::removeAllModsFunction(){ _loadingPopup_.getMonitorView()->resetMonitorAddresses(); _loadingPopup_.getMonitorView()->setTitlePtr( &modCheckAllMonitor.currentMod ); _loadingPopup_.getMonitorView()->setSubTitlePtr( &modCheckMonitor.currentFile ); - _loadingPopup_.getMonitorView()->setProgressFractionPtr( &modCheckMonitor.progress ); + _loadingPopup_.getMonitorView()->setProgressFractionPtr( &modCheckAllMonitor.progress ); _loadingPopup_.getMonitorView()->setSubProgressFractionPtr( &GenericToolbox::Switch::Utils::b.progressMap["doFilesAreIdentical"] ); this->checkAllMods(); if( _triggeredOnCancel_ ){ return this->leaveModAction(false); } + this->finalizeModFilesystemChanges("Finalizing removal..."); return this->leaveModAction(true); } +void GuiModManager::finalizeModFilesystemChanges(const std::string& title_){ + LogInfo << "Finalizing SD filesystem writes: " << title_ << std::endl; + modFinalizeMonitor = ModFinalizeMonitor(); + modFinalizeMonitor.currentStep = "Synchronizing SD card..."; + + _loadingPopup_.getMonitorView()->setHeaderTitle(title_); + _loadingPopup_.getMonitorView()->setProgressColor(GenericToolbox::Borealis::blueNvgColor); + _loadingPopup_.getMonitorView()->resetMonitorAddresses(); + _loadingPopup_.getMonitorView()->setTitlePtr(&modFinalizeMonitor.currentStep); + _loadingPopup_.getMonitorView()->setProgressFractionPtr(&modFinalizeMonitor.progress); + + for( int pass = 0; pass < kFinalSdmcCommitPasses; ++pass ){ + modFinalizeMonitor.currentStep = "Synchronizing SD card... (" + + std::to_string(pass + 1) + "/" + std::to_string(kFinalSdmcCommitPasses) + ")"; + modFinalizeMonitor.progress = double(pass) / double(kFinalSdmcCommitPasses); + const Result rc = fsdevCommitDevice("sdmc"); + if( R_FAILED(rc) ){ + LogWarning << "fsdevCommitDevice(sdmc) failed during finalization: 0x" + << std::hex << rc << std::dec << std::endl; + } + svcSleepThread(kFinalSdmcCommitSettleNs); + } + + modFinalizeMonitor.currentStep = "Filesystem ready."; + modFinalizeMonitor.progress = 1; + (void) fsdevCommitDevice("sdmc"); + svcSleepThread(kFinalSdmcCommitSettleNs); +} bool GuiModManager::leaveModAction(bool isSuccess_){ _triggerUpdateModsDisplayedStatus_ = true; @@ -438,4 +1381,3 @@ bool GuiModManager::leaveModAction(bool isSuccess_){ LogInfo << "Leaving mod action with success? " << isSuccess_ << std::endl; return isSuccess_; } - diff --git a/src/ModManagerGui/CoreExtension/src/SystemStatusOverlay.cpp b/src/ModManagerGui/CoreExtension/src/SystemStatusOverlay.cpp new file mode 100644 index 0000000..05e6a74 --- /dev/null +++ b/src/ModManagerGui/CoreExtension/src/SystemStatusOverlay.cpp @@ -0,0 +1,124 @@ +#include "SystemStatusOverlay.h" + +#include +#include +#include +#include +#include + +#include + +namespace { + +constexpr int kStatusAreaWidth = 230; +constexpr int kStatusRightPadding = 8; +constexpr int kStatusFontSize = 22; + +bool g_psmInitAttempted = false; +bool g_psmReady = false; +std::string g_statusText = "--:--"; +std::chrono::steady_clock::time_point g_lastRefresh{}; + +void ensurePsmInitialized(){ + if( g_psmInitAttempted ){ + return; + } + + g_psmReady = R_SUCCEEDED(psmInitialize()); + g_psmInitAttempted = true; +} + +bool fetchCalendarTime(TimeCalendarTime& outCalendar_){ + u64 timestamp = 0; + if( R_SUCCEEDED(timeGetCurrentTime(TimeType_Default, ×tamp)) ){ + TimeCalendarAdditionalInfo additionalInfo{}; + if( R_SUCCEEDED(timeToCalendarTimeWithMyRule(timestamp, &outCalendar_, &additionalInfo)) ){ + return true; + } + } + + const std::time_t rawTime = std::time(nullptr); + const std::tm* localTime = std::localtime(&rawTime); + if( localTime == nullptr ){ + return false; + } + + outCalendar_.hour = static_cast(localTime->tm_hour); + outCalendar_.minute = static_cast(localTime->tm_min); + return true; +} + +void refreshStatusText(){ + const auto now = std::chrono::steady_clock::now(); + if( not g_statusText.empty() + and g_lastRefresh.time_since_epoch().count() != 0 + and now - g_lastRefresh < std::chrono::seconds(1) ){ + return; + } + g_lastRefresh = now; + + char timeBuffer[8] = "--:--"; + TimeCalendarTime calendarTime{}; + if( fetchCalendarTime(calendarTime) ){ + std::snprintf(timeBuffer, sizeof(timeBuffer), "%02u:%02u", + static_cast(calendarTime.hour), + static_cast(calendarTime.minute)); + } + + ensurePsmInitialized(); + + u32 batteryPercent = 0; + if( g_psmReady and R_SUCCEEDED(psmGetBatteryChargePercentage(&batteryPercent)) ){ + char statusBuffer[24]; + std::snprintf(statusBuffer, sizeof(statusBuffer), "%s %u%%", + timeBuffer, + static_cast(std::min(batteryPercent, 100))); + g_statusText = statusBuffer; + } + else{ + g_statusText = timeBuffer; + } +} + +} // namespace + +namespace SystemStatusOverlay { + +void draw(NVGcontext* vg, int x, int y, unsigned width, brls::Style* style, brls::FrameContext* ctx){ + refreshStatusText(); + if( g_statusText.empty() ){ + return; + } + + const int statusRight = x + static_cast(width) - static_cast(style->AppletFrame.separatorSpacing) - kStatusRightPadding; + const int statusLeft = statusRight - kStatusAreaWidth; + const int statusCenterY = y + static_cast(style->AppletFrame.headerHeightRegular) / 2 + + static_cast(style->AppletFrame.titleOffset); + + nvgSave(vg); + + nvgBeginPath(vg); + nvgFillColor(vg, ctx->theme->backgroundColorRGB); + nvgRect(vg, statusLeft, y, kStatusAreaWidth + kStatusRightPadding, style->AppletFrame.headerHeightRegular - 2); + nvgFill(vg); + + nvgFillColor(vg, ctx->theme->textColor); + nvgFontFaceId(vg, ctx->fontStash->regular); + nvgFontSize(vg, kStatusFontSize); + nvgTextAlign(vg, NVG_ALIGN_RIGHT | NVG_ALIGN_MIDDLE); + nvgBeginPath(vg); + nvgText(vg, statusRight, statusCenterY, g_statusText.c_str(), nullptr); + + nvgRestore(vg); +} + +void shutdown(){ + if( g_psmReady ){ + psmExit(); + } + + g_psmReady = false; + g_psmInitAttempted = false; +} + +} diff --git a/src/ModManagerGui/FrameGameBrowser/CMakeLists.txt b/src/ModManagerGui/FrameGameBrowser/CMakeLists.txt index c73924e..fa61ea0 100644 --- a/src/ModManagerGui/FrameGameBrowser/CMakeLists.txt +++ b/src/ModManagerGui/FrameGameBrowser/CMakeLists.txt @@ -7,7 +7,28 @@ set( SRC_FILES ${CMAKE_CURRENT_SOURCE_DIR}/src/FrameRoot.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/TabAbout.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/TabGames.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/ModsMtpServer.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/TabImportMod.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/TabGeneralSettings.cpp + ${PROJECT_SOURCE_DIR}/src/ThirdParty/mtp-server-nx/source/MtpDataPacket.cpp + ${PROJECT_SOURCE_DIR}/src/ThirdParty/mtp-server-nx/source/MtpDebug.cpp + ${PROJECT_SOURCE_DIR}/src/ThirdParty/mtp-server-nx/source/MtpDeviceInfo.cpp + ${PROJECT_SOURCE_DIR}/src/ThirdParty/mtp-server-nx/source/MtpEventPacket.cpp + ${PROJECT_SOURCE_DIR}/src/ThirdParty/mtp-server-nx/source/MtpObjectInfo.cpp + ${PROJECT_SOURCE_DIR}/src/ThirdParty/mtp-server-nx/source/MtpPacket.cpp + ${PROJECT_SOURCE_DIR}/src/ThirdParty/mtp-server-nx/source/MtpProperty.cpp + ${PROJECT_SOURCE_DIR}/src/ThirdParty/mtp-server-nx/source/MtpRequestPacket.cpp + ${PROJECT_SOURCE_DIR}/src/ThirdParty/mtp-server-nx/source/MtpResponsePacket.cpp + ${PROJECT_SOURCE_DIR}/src/ThirdParty/mtp-server-nx/source/MtpServer.cpp + ${PROJECT_SOURCE_DIR}/src/ThirdParty/mtp-server-nx/source/MtpStorage.cpp + ${PROJECT_SOURCE_DIR}/src/ThirdParty/mtp-server-nx/source/MtpStorageInfo.cpp + ${PROJECT_SOURCE_DIR}/src/ThirdParty/mtp-server-nx/source/MtpStringBuffer.cpp + ${PROJECT_SOURCE_DIR}/src/ThirdParty/mtp-server-nx/source/MtpUtils.cpp + ${PROJECT_SOURCE_DIR}/src/ThirdParty/mtp-server-nx/source/USBMtpInterface.cpp + ${PROJECT_SOURCE_DIR}/src/ThirdParty/mtp-server-nx/source/USBSerialInterface.cpp + ${PROJECT_SOURCE_DIR}/src/ThirdParty/mtp-server-nx/source/log.cpp + ${PROJECT_SOURCE_DIR}/src/ThirdParty/mtp-server-nx/source/nxlink.cpp + ${PROJECT_SOURCE_DIR}/src/ThirdParty/mtp-server-nx/source/usb.c ) @@ -18,6 +39,8 @@ install( TARGETS FrameGameBrowser DESTINATION lib ) target_include_directories( FrameGameBrowser PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include + ${PROJECT_SOURCE_DIR}/src/ThirdParty/mtp-server-nx/include + /opt/devkitpro/portlibs/switch/include ) target_link_libraries( FrameGameBrowser PUBLIC @@ -28,5 +51,5 @@ target_link_libraries( FrameGameBrowser PUBLIC -L/opt/devkitpro/libnx/lib ${ZLIB_LIBRARIES} ${FREETYPE_LIBRARIES} - -lglfw3 -lEGL -lglad -lglapi -ldrm_nouveau -lm -lnx + -lminizip -lz -lbz2 -lglfw3 -lEGL -lglad -lglapi -ldrm_nouveau -lm -lnx ) diff --git a/src/ModManagerGui/FrameGameBrowser/include/FrameRoot.h b/src/ModManagerGui/FrameGameBrowser/include/FrameRoot.h index 340d122..3c927ab 100644 --- a/src/ModManagerGui/FrameGameBrowser/include/FrameRoot.h +++ b/src/ModManagerGui/FrameGameBrowser/include/FrameRoot.h @@ -15,6 +15,7 @@ class FrameRoot : public brls::TabFrame { public: FrameRoot(); + void draw(NVGcontext* vg, int x, int y, unsigned width, unsigned height, brls::Style* style, brls::FrameContext* ctx) override; bool onCancel() override; const GuiModManager &getGuiModManager() const { return _guiModManager_; } diff --git a/src/ModManagerGui/FrameGameBrowser/include/ModsMtpServer.h b/src/ModManagerGui/FrameGameBrowser/include/ModsMtpServer.h new file mode 100644 index 0000000..2201d8a --- /dev/null +++ b/src/ModManagerGui/FrameGameBrowser/include/ModsMtpServer.h @@ -0,0 +1,19 @@ +// +// Embedded MTP responder (USB) for direct PC transfer into sdmc:/mods. +// + +#ifndef SIMPLEMODMANAGER_MODSMTPSERVER_H +#define SIMPLEMODMANAGER_MODSMTPSERVER_H + +#include + +class ModsMtpServer { +public: + static void start(); + static void stop(); + static void shutdownForAppExit( int timeoutMs = 2000 ); + static bool isRunning(); + static std::string getStatusLine(); +}; + +#endif diff --git a/src/ModManagerGui/FrameGameBrowser/include/TabGames.h b/src/ModManagerGui/FrameGameBrowser/include/TabGames.h index f544bfd..bd1e700 100644 --- a/src/ModManagerGui/FrameGameBrowser/include/TabGames.h +++ b/src/ModManagerGui/FrameGameBrowser/include/TabGames.h @@ -23,6 +23,11 @@ class TabGames : public brls::List { public: explicit TabGames(FrameRoot* owner_); + void rebuildLayout(bool force_ = false); + void willAppear(bool resetState = false) override; + void draw(NVGcontext* vg, int x, int y, unsigned width, unsigned height, brls::Style* style, brls::FrameContext* ctx) override; + brls::View* getDefaultFocus() override; + // non native getters [[nodiscard]] const GameBrowser& getGameBrowser() const; [[nodiscard]] const ConfigHolder& getConfig() const; @@ -31,8 +36,20 @@ class TabGames : public brls::List { ConfigHolder& getConfig(); private: + void resyncListItemFocusIndices(); + brls::ListItem* findGameItem(const std::string& gameTitle_) const; + void refreshDisplayedGameStatus(const std::string& gameTitle_); + void restoreFocusAfterRebuild(); + FrameRoot* _owner_{}; std::vector _gameList_; + std::string _focusGameNameAfterReturn_{}; + bool _restoreFocusAfterModBrowser_{false}; + bool _refreshOnNextDraw_{false}; + bool _refreshGameStatusOnNextDraw_{false}; + bool _restoreFocusOnNextDraw_{false}; + bool _hasAppearedOnce_{false}; + bool _layoutBuilt_{false}; }; diff --git a/src/ModManagerGui/FrameGameBrowser/include/TabImportMod.h b/src/ModManagerGui/FrameGameBrowser/include/TabImportMod.h new file mode 100644 index 0000000..07c4ebc --- /dev/null +++ b/src/ModManagerGui/FrameGameBrowser/include/TabImportMod.h @@ -0,0 +1,34 @@ +// +// Import mods from a PC into sdmc:/mods via the in-app HTTP upload server. +// + +#ifndef SIMPLEMODMANAGER_TABIMPORTMOD_H +#define SIMPLEMODMANAGER_TABIMPORTMOD_H + +#include + +#include + +class TabImportMod : public brls::List { + +public: + TabImportMod(); + ~TabImportMod() override = default; + + void draw( NVGcontext* vg, int x, int y, unsigned width, unsigned height, brls::Style* style, brls::FrameContext* ctx ) override; + void customSpacing(brls::View* current, brls::View* next, int* spacing) override; + + View* getDefaultFocus() override; + +private: + void refreshStatusLine(); + + brls::View* _bodyLabel_{ nullptr }; + brls::Label* _statusLabel_{ nullptr }; + brls::ListItem* _actionRow_{ nullptr }; + + std::string _lastStatus_{}; + int _statusTick_{ 0 }; +}; + +#endif diff --git a/src/ModManagerGui/FrameGameBrowser/src/FrameRoot.cpp b/src/ModManagerGui/FrameGameBrowser/src/FrameRoot.cpp index 3b35569..e046b1f 100644 --- a/src/ModManagerGui/FrameGameBrowser/src/FrameRoot.cpp +++ b/src/ModManagerGui/FrameGameBrowser/src/FrameRoot.cpp @@ -5,9 +5,11 @@ #include "FrameRoot.h" #include +#include #include #include +#include "SystemStatusOverlay.h" #include "Toolbox.h" @@ -25,6 +27,7 @@ FrameRoot::FrameRoot() { this->setFooterText( "v" + Toolbox::getAppVersion() ); this->setIcon("romfs:/images/icon_corner.png"); this->addTab( "Game Browser", new TabGames(this) ); + this->addTab( "Import Mod", new TabImportMod() ); this->addSeparator(); this->addTab( "Settings", new TabGeneralSettings(this) ); this->addTab( "About", new TabAbout() ); @@ -32,6 +35,11 @@ FrameRoot::FrameRoot() { LogInfo << "Root frame built." << std::endl; } +void FrameRoot::draw(NVGcontext* vg, int x, int y, unsigned width, unsigned height, brls::Style* style, brls::FrameContext* ctx) { + brls::TabFrame::draw(vg, x, y, width, height, style, ctx); + SystemStatusOverlay::draw(vg, x, y, width, style, ctx); +} + bool FrameRoot::onCancel() { // fetch the current focus auto* lastFocus = brls::Application::getCurrentFocus(); diff --git a/src/ModManagerGui/FrameGameBrowser/src/ModsMtpServer.cpp b/src/ModManagerGui/FrameGameBrowser/src/ModsMtpServer.cpp new file mode 100644 index 0000000..a51733a --- /dev/null +++ b/src/ModManagerGui/FrameGameBrowser/src/ModsMtpServer.cpp @@ -0,0 +1,756 @@ +// +// MTP responder wrapper using vendored mtp-server-nx core. +// + +#include "ModsMtpServer.h" + +#include "ConfigHandler.h" +#include "Logger.h" + +#include "MtpServer.h" +#include "MtpStorage.h" +#include "SwitchMtpDatabase.h" +#include "USBMtpInterface.h" +#include "usb.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +extern "C" { +#include +#include +#include +} + +LoggerInit( [] { + Logger::setUserHeaderStr( "[ModsMtpServer]" ); +} ); + +namespace { + +std::mutex g_mutex; +std::mutex g_statusMutex; +std::mutex g_serverMutex; +std::mutex g_powerMutex; +std::thread g_thread; +std::atomic g_workerActive{ false }; +std::atomic g_starting{ false }; +std::atomic g_running{ false }; +std::atomic g_stopping{ false }; +std::atomic g_stopRequested{ false }; +std::atomic g_appShuttingDown{ false }; +std::atomic g_usbInitialized{ false }; +std::atomic g_disconnectDetected{ false }; +std::atomic g_restartRequested{ false }; +std::string g_status = "MTP stopped."; +bool g_keepAwakeActive = false; +bool g_autoSleepWasDisabled = false; +const auto g_processStartTs = std::chrono::steady_clock::now(); +std::atomic g_lastTransitionMs{ 0 }; +std::atomic g_waitingForHostSinceMs{ 0 }; +std::atomic g_pendingStartDelayMs{ 0 }; +std::atomic g_waitingForHostRestartCount{ 0 }; + +constexpr long long kMtpStartGuardAfterBootMs = 3500; +constexpr long long kMtpTransitionCooldownMs = 2000; +constexpr long long kMtpStoppingTextMaxMs = 2500; +constexpr long long kMtpStopRetryMs = 1200; +constexpr long long kMtpStartingTextMaxMs = 4500; +constexpr long long kMtpHostWaitAutoRestartMs = 4500; +constexpr int kMtpHostWaitMaxAutoRestarts = 3; +constexpr size_t kZipReadBufferSize = 64 * 1024; +constexpr const char* kModsRoot = "sdmc:/mods"; + +android::MtpServer* g_server = nullptr; +android::MtpStorage* g_storage = nullptr; +android::MtpDatabase* g_database = nullptr; +USBMtpInterface* g_mtpInterface = nullptr; + +std::atomic g_lastUsbState{ static_cast( UsbState_Detached ) }; + +void setStatus( const std::string& s ) { + std::lock_guard lock( g_statusMutex ); + g_status = s; +} + +long long nowMs() { + const auto now = std::chrono::steady_clock::now(); + return std::chrono::duration_cast( now.time_since_epoch() ).count(); +} + +void setMtpKeepAwake( bool enabled ) { + std::lock_guard lock( g_powerMutex ); + if( enabled ) { + if( g_keepAwakeActive ) { + return; + } + bool wasDisabled = false; + if( R_FAILED( appletIsAutoSleepDisabled( &wasDisabled ) ) ) { + wasDisabled = false; + } + g_autoSleepWasDisabled = wasDisabled; + appletSetAutoSleepDisabled( true ); + appletSetMediaPlaybackState( true ); + g_keepAwakeActive = true; + return; + } + + if( !g_keepAwakeActive ) { + return; + } + appletSetMediaPlaybackState( false ); + if( !g_autoSleepWasDisabled ) { + appletSetAutoSleepDisabled( false ); + } + g_keepAwakeActive = false; +} + +long long sinceProcessStartMs() { + const auto now = std::chrono::steady_clock::now(); + return std::chrono::duration_cast( now - g_processStartTs ).count(); +} + +long long bootGuardRemainingMs() { + const long long elapsed = sinceProcessStartMs(); + if( elapsed >= kMtpStartGuardAfterBootMs ) return 0; + return kMtpStartGuardAfterBootMs - elapsed; +} + +long long transitionCooldownRemainingMs() { + const long long last = g_lastTransitionMs.load(); + if( last <= 0 ) return 0; + const long long elapsed = nowMs() - last; + if( elapsed >= kMtpTransitionCooldownMs ) return 0; + return kMtpTransitionCooldownMs - elapsed; +} + +void requestStop_NoLock( const char* statusMsg ) { + g_stopping.store( true ); + g_stopRequested.store( true ); + g_lastTransitionMs.store( nowMs() ); + setStatus( statusMsg ); + std::lock_guard lk( g_serverMutex ); + if( g_server != nullptr ) { + g_server->stop(); + } +} + +std::string getStatus() { + std::lock_guard lock( g_statusMutex ); + return g_status; +} + +bool hasNoActiveSession() { + return !g_workerActive.load() && !g_running.load() && !g_starting.load(); +} + +bool shouldRestartWhileWaitingForHost( UsbState state ) { + (void)state; + if( g_restartRequested.load() || g_stopRequested.load() || g_stopping.load() ) { + return false; + } + + const long long now = nowMs(); + const long long waitingSince = g_waitingForHostSinceMs.load(); + if( waitingSince <= 0 ) { + g_waitingForHostSinceMs.store( now ); + return false; + } + + if( now - waitingSince < kMtpHostWaitAutoRestartMs ) { + return false; + } + + if( g_waitingForHostRestartCount.load() >= kMtpHostWaitMaxAutoRestarts ) { + return false; + } + + g_waitingForHostRestartCount.fetch_add( 1 ); + g_waitingForHostSinceMs.store( 0 ); + g_restartRequested.store( true ); + return true; +} + +bool hasSuffixIgnoreCase( const std::string& value, const char* suffix ) { + const size_t suffixLen = std::strlen( suffix ); + if( value.size() < suffixLen ) { + return false; + } + const size_t offset = value.size() - suffixLen; + for( size_t i = 0; i < suffixLen; ++i ) { + const char a = static_cast( std::tolower( static_cast( value[offset + i] ) ) ); + const char b = static_cast( std::tolower( static_cast( suffix[i] ) ) ); + if( a != b ) { + return false; + } + } + return true; +} + +std::string sanitizeArchivePath( const std::string& raw ) { + std::string normalized; + normalized.reserve( raw.size() ); + for( char c : raw ) { + normalized += ( c == '\\' ) ? '/' : c; + } + + while( !normalized.empty() && normalized.front() == '/' ) { + normalized.erase( normalized.begin() ); + } + + std::string out; + size_t pos = 0; + while( pos < normalized.size() ) { + while( pos < normalized.size() && normalized[pos] == '/' ) { + ++pos; + } + size_t end = pos; + while( end < normalized.size() && normalized[end] != '/' ) { + ++end; + } + const std::string segment = normalized.substr( pos, end - pos ); + pos = end; + + if( segment.empty() || segment == "." ) { + continue; + } + if( segment == ".." || segment.find( ':' ) != std::string::npos ) { + return {}; + } + if( !out.empty() ) { + out += '/'; + } + out += segment; + } + return out; +} + +bool ensureDirectory( const std::string& dir ) { + if( dir.empty() ) { + return false; + } + if( mkdir( dir.c_str(), 0777 ) == 0 || errno == EEXIST ) { + return true; + } + return false; +} + +bool ensureParentDirectories( const std::string& fullPath ) { + const size_t slash = fullPath.rfind( '/' ); + if( slash == std::string::npos ) { + return false; + } + const std::string dir = fullPath.substr( 0, slash ); + if( dir.size() < std::strlen( kModsRoot ) || dir.compare( 0, std::strlen( kModsRoot ), kModsRoot ) != 0 ) { + return false; + } + if( dir.size() == std::strlen( kModsRoot ) ) { + return true; + } + + std::string current = kModsRoot; + size_t pos = std::strlen( kModsRoot ); + if( pos < dir.size() && dir[pos] == '/' ) { + ++pos; + } + while( pos <= dir.size() ) { + size_t next = dir.find( '/', pos ); + if( next == std::string::npos ) { + next = dir.size(); + } + if( next > pos ) { + current += '/'; + current += dir.substr( pos, next - pos ); + if( !ensureDirectory( current ) ) { + return false; + } + } + if( next == dir.size() ) { + break; + } + pos = next + 1; + } + return true; +} + +bool extractZipArchiveToMods( const std::string& zipPath ) { + unzFile zip = unzOpen64( zipPath.c_str() ); + if( zip == nullptr ) { + return false; + } + + bool ok = true; + if( unzGoToFirstFile( zip ) != UNZ_OK ) { + ok = false; + } + + std::vector buffer( kZipReadBufferSize ); + while( ok ) { + unz_file_info64 info{}; + char rawName[1024] = {}; + int rc = unzGetCurrentFileInfo64( zip, &info, rawName, sizeof( rawName ) - 1, nullptr, 0, nullptr, 0 ); + if( rc != UNZ_OK ) { + ok = false; + break; + } + if( info.size_filename >= sizeof( rawName ) ) { + ok = false; + break; + } + + std::string relativePath = sanitizeArchivePath( rawName ); + if( relativePath.empty() ) { + ok = false; + break; + } + + const bool isDir = relativePath.back() == '/' || rawName[std::strlen( rawName ) - 1] == '/'; + const std::string outPath = std::string( kModsRoot ) + "/" + relativePath; + if( isDir ) { + if( !ensureParentDirectories( outPath + "/.dir" ) ) { + ok = false; + break; + } + if( !ensureDirectory( outPath ) ) { + ok = false; + break; + } + } + else { + if( !ensureParentDirectories( outPath ) ) { + ok = false; + break; + } + if( outPath == zipPath ) { + ok = false; + break; + } + if( unzOpenCurrentFile( zip ) != UNZ_OK ) { + ok = false; + break; + } + + FILE* out = std::fopen( outPath.c_str(), "wb" ); + if( out == nullptr ) { + unzCloseCurrentFile( zip ); + ok = false; + break; + } + + while( true ) { + const int readBytes = unzReadCurrentFile( zip, buffer.data(), static_cast( buffer.size() ) ); + if( readBytes < 0 ) { + ok = false; + break; + } + if( readBytes == 0 ) { + break; + } + const size_t written = std::fwrite( buffer.data(), 1, static_cast( readBytes ), out ); + if( written != static_cast( readBytes ) ) { + ok = false; + break; + } + } + + std::fclose( out ); + if( unzCloseCurrentFile( zip ) != UNZ_OK ) { + ok = false; + } + if( !ok ) { + std::remove( outPath.c_str() ); + break; + } + } + + rc = unzGoToNextFile( zip ); + if( rc == UNZ_END_OF_LIST_OF_FILE ) { + break; + } + if( rc != UNZ_OK ) { + ok = false; + break; + } + } + + unzClose( zip ); + if( ok ) { + std::remove( zipPath.c_str() ); + } + return ok; +} + +int extractTopLevelZipArchives() { + DIR* dir = opendir( kModsRoot ); + if( dir == nullptr ) { + return 0; + } + + std::vector archives; + while( dirent* entry = readdir( dir ) ) { + if( entry->d_name[0] == '.' ) { + continue; + } + const std::string name = entry->d_name; + if( hasSuffixIgnoreCase( name, ".zip" ) ) { + archives.push_back( std::string( kModsRoot ) + "/" + name ); + } + } + closedir( dir ); + + int extracted = 0; + for( const std::string& archive : archives ) { + setStatus( "Extracting archive: " + archive.substr( std::strlen( kModsRoot ) + 1 ) ); + if( extractZipArchiveToMods( archive ) ) { + ++extracted; + } + } + return extracted; +} + +void resetToStoppedState( const std::string& statusMessage = "MTP stopped." ) { + setMtpKeepAwake( false ); + g_starting.store( false ); + g_running.store( false ); + g_stopping.store( false ); + g_stopRequested.store( false ); + g_disconnectDetected.store( false ); + g_lastUsbState.store( static_cast( UsbState_Detached ) ); + g_waitingForHostSinceMs.store( 0 ); + g_pendingStartDelayMs.store( 0 ); + if( !g_restartRequested.load() ) { + g_waitingForHostRestartCount.store( 0 ); + } + g_lastTransitionMs.store( nowMs() ); + setStatus( statusMessage ); +} + +void mtpWorker() { + g_workerActive.store( true ); + const long long startDelayMs = g_pendingStartDelayMs.exchange( 0 ); + if( startDelayMs > 0 ) { + const auto deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds( startDelayMs ); + while( std::chrono::steady_clock::now() < deadline ) { + if( g_appShuttingDown.load() || g_stopRequested.load() || g_stopping.load() ) { + resetToStoppedState( "MTP stopped." ); + g_workerActive.store( false ); + return; + } + std::this_thread::sleep_for( std::chrono::milliseconds( 20 ) ); + } + } + + if( g_appShuttingDown.load() || g_stopRequested.load() || g_stopping.load() ) { + resetToStoppedState( "MTP stopped." ); + g_workerActive.store( false ); + return; + } + + struct usb_device_descriptor deviceDescriptor = { + .bLength = USB_DT_DEVICE_SIZE, + .bDescriptorType = USB_DT_DEVICE, + .bcdUSB = 0x0110, + .bDeviceClass = 0x00, + .bDeviceSubClass = 0x00, + .bDeviceProtocol = 0x00, + .bMaxPacketSize0 = 0x40, + .idVendor = 0x057e, + .idProduct = 0x4000, + .bcdDevice = 0x0100, + .bNumConfigurations = 0x01, + }; + + UsbInterfaceDesc infos[1]; + g_mtpInterface = new USBMtpInterface( 0, &infos[0] ); + + setMtpKeepAwake( true ); + + Result rc = usbInitialize( &deviceDescriptor, 1, infos ); + if( R_FAILED( rc ) ) { + setStatus( "MTP failed to start (usb init error)." ); + setMtpKeepAwake( false ); + g_starting.store( false ); + g_running.store( false ); + g_stopping.store( false ); + g_stopRequested.store( false ); + g_disconnectDetected.store( false ); + g_workerActive.store( false ); + delete g_mtpInterface; + g_mtpInterface = nullptr; + return; + } + g_starting.store( false ); + g_usbInitialized.store( true ); + + android::MtpStorage* storage = new android::MtpStorage( + MTP_STORAGE_REMOVABLE_RAM, + "sdmc:/mods/", + "mods", + 1024U * 1024U, // 1 MiB reserved + false, + 0xffffffffULL ); + + ConfigHandler config; + android::MtpDatabase* database = new android::SwitchMtpDatabase( config.getConfig().showDebugMtpFiles ); + database->addStoragePath( "sdmc:/mods/", "mods", MTP_STORAGE_REMOVABLE_RAM, true ); + + android::MtpServer* server = new android::MtpServer( g_mtpInterface, database, false, 0, 0, 0 ); + server->addStorage( storage ); + { + std::lock_guard lk( g_serverMutex ); + g_server = server; + g_storage = storage; + g_database = database; + } + + // Important: if user requested stop (or we are stopping), never overwrite the UI + // with "waiting" again — it creates confusing stuck statuses after fast stop/unplug. + if( g_stopRequested.load() || g_stopping.load() ) { + server->stop(); + setStatus( "Stopping MTP..." ); + } + else { + setStatus( "MTP waiting for USB host..." ); + try { + server->run(); + } + catch( const std::exception& e ) { + LOG( ERROR ) << "MTP responder stopped after an error: " << e.what() << std::endl; + setStatus( "MTP stopped after filesystem error." ); + } + catch( ... ) { + LOG( ERROR ) << "MTP responder stopped after an unknown error." << std::endl; + setStatus( "MTP stopped after filesystem error." ); + } + } + + { + std::lock_guard lk( g_serverMutex ); + if( g_server == server ) { + g_server = nullptr; + g_storage = nullptr; + g_database = nullptr; + } + } + delete server; + delete storage; + delete database; + delete g_mtpInterface; + g_mtpInterface = nullptr; + if( g_usbInitialized.exchange( false ) ) { + usbExit(); + } + + if( !g_appShuttingDown.load() && !g_restartRequested.load() ) { + const int extractedArchives = extractTopLevelZipArchives(); + if( extractedArchives > 0 ) { + resetToStoppedState( "MTP stopped. Zip archive extracted." ); + } + else { + resetToStoppedState( "MTP stopped." ); + } + } + else { + resetToStoppedState( "MTP stopped." ); + } + g_workerActive.store( false ); +} + +} // namespace + +void ModsMtpServer::start() { + std::lock_guard lock( g_mutex ); + if( g_appShuttingDown.load() ) { + return; + } + if( g_stopping.load() ) { + if( g_disconnectDetected.load() && !g_workerActive.load() && !g_running.load() && !g_starting.load() ) { + resetToStoppedState( "MTP stopped." ); + } + else { + setStatus( g_disconnectDetected.load() ? "MTP stopped." : "MTP is still stopping..." ); + return; + } + } + if( g_thread.joinable() && !g_workerActive.load() ) { + g_thread.join(); + } + if( g_workerActive.load() || g_running.load() || g_starting.load() || g_stopping.load() ) { + if( g_disconnectDetected.load() ) { + setStatus( "Waiting for previous MTP session to stop..." ); + } + return; + } + const bool autoRestart = g_restartRequested.load(); + const long long startDelayMs = std::max( bootGuardRemainingMs(), transitionCooldownRemainingMs() ); + setStatus( autoRestart ? "Restarting MTP..." : "Starting MTP..." ); + g_stopping.store( false ); + g_stopRequested.store( false ); + g_disconnectDetected.store( false ); + g_restartRequested.store( false ); + g_waitingForHostSinceMs.store( 0 ); + g_pendingStartDelayMs.store( startDelayMs ); + if( !autoRestart ) { + g_waitingForHostRestartCount.store( 0 ); + } + g_starting.store( true ); + g_running.store( true ); + g_lastTransitionMs.store( nowMs() ); + g_thread = std::thread( [] { mtpWorker(); } ); +} + +void ModsMtpServer::stop() { + std::lock_guard lock( g_mutex ); + g_restartRequested.store( false ); + g_waitingForHostSinceMs.store( 0 ); + g_pendingStartDelayMs.store( 0 ); + g_waitingForHostRestartCount.store( 0 ); + if( ( !g_running.load() && !g_starting.load() && !g_workerActive.load() ) || g_stopping.load() ) { + return; + } + + if( g_starting.load() && !g_usbInitialized.load() ) { + // Start is in progress: request deferred stop and let worker tear down safely. + requestStop_NoLock( "Stopping MTP..." ); + return; + } + // Always request stop; worker thread performs the real teardown safely. + requestStop_NoLock( "Stopping MTP..." ); + // Non-blocking stop: worker thread handles USB teardown and final state reset. +} + +void ModsMtpServer::shutdownForAppExit( int timeoutMs ) { + { + std::lock_guard lock( g_mutex ); + g_appShuttingDown.store( true ); + } + + stop(); + + const auto startTs = std::chrono::steady_clock::now(); + while( g_workerActive.load() || g_running.load() || g_starting.load() || g_stopping.load() ) { + const auto now = std::chrono::steady_clock::now(); + const auto elapsedMs = std::chrono::duration_cast( now - startTs ).count(); + if( elapsedMs >= timeoutMs ) { + break; + } + std::this_thread::sleep_for( std::chrono::milliseconds( 10 ) ); + } + + setMtpKeepAwake( false ); + + std::lock_guard lock( g_mutex ); + if( g_thread.joinable() ) { + if( g_workerActive.load() || g_running.load() || g_starting.load() || g_stopping.load() ) { + // Never let std::thread stay joinable at process shutdown. + g_thread.detach(); + setStatus( "MTP shutdown pending in background..." ); + } + else { + g_thread.join(); + } + } +} + +bool ModsMtpServer::isRunning() { + return g_workerActive.load() || g_running.load() || g_starting.load() || g_stopping.load(); +} + +std::string ModsMtpServer::getStatusLine() { + // Self-heal stale transitional flags to avoid UI being stuck on + // "Starting..." / "Stopping..." after fast unplug, tab switches or app lifecycle edges. + const long long elapsedSinceTransition = nowMs() - g_lastTransitionMs.load(); + if( g_stopping.load() && hasNoActiveSession() ) { + g_stopping.store( false ); + if( getStatus().find( "Stopping MTP" ) != std::string::npos ) { + setStatus( "MTP stopped." ); + } + } + if( g_stopping.load() && elapsedSinceTransition > kMtpStoppingTextMaxMs && !g_workerActive.load() ) { + g_stopping.store( false ); + setStatus( "MTP stopped." ); + } + if( g_stopping.load() && elapsedSinceTransition > kMtpStopRetryMs && g_workerActive.load() ) { + requestStop_NoLock( g_restartRequested.load() ? "Restarting MTP..." : "Stopping MTP..." ); + } + if( g_starting.load() && elapsedSinceTransition > kMtpStartingTextMaxMs && !g_usbInitialized.load() ) { + g_starting.store( false ); + g_running.store( false ); + setStatus( "MTP start timeout. Please try again." ); + } + + // If there is no active session, never show "waiting for USB host". + if( hasNoActiveSession() ) { + const std::string s = getStatus(); + if( s.find( "MTP waiting for USB host" ) != std::string::npos ) { + setStatus( "MTP stopped." ); + } + } + + if( g_restartRequested.load() && hasNoActiveSession() && !g_appShuttingDown.load() ) { + ModsMtpServer::start(); + return getStatus(); + } + + if( g_starting.load() ) { + return "Starting MTP..."; + } + if( g_disconnectDetected.load() && ( g_stopping.load() || g_stopRequested.load() ) ) { + setStatus( "MTP stopped." ); + return "MTP stopped."; + } + if( g_stopping.load() || g_stopRequested.load() ) { + return g_restartRequested.load() ? "Restarting MTP..." : "Stopping MTP..."; + } + if( g_running.load() || g_workerActive.load() ) { + if( g_stopRequested.load() ) { + return g_restartRequested.load() ? "Restarting MTP..." : "Stopping MTP..."; + } + UsbState state = UsbState_Detached; + if( g_usbInitialized.load() && R_SUCCEEDED( usbDsGetState( &state ) ) ) { + const UsbState previousState = static_cast( g_lastUsbState.exchange( static_cast( state ) ) ); + + // If USB was connected and is now unplugged/not configured, request a clean stop. + if( state != UsbState_Configured && previousState == UsbState_Configured ) { + g_disconnectDetected.store( true ); + requestStop_NoLock( "MTP stopped." ); + return "MTP stopped."; + } + + if( state == UsbState_Configured ) { + g_waitingForHostSinceMs.store( 0 ); + g_waitingForHostRestartCount.store( 0 ); + return "MTP running."; + } + if( g_stopRequested.load() ) { + return g_restartRequested.load() ? "Restarting MTP..." : "Stopping MTP..."; + } + if( shouldRestartWhileWaitingForHost( state ) ) { + requestStop_NoLock( "Restarting MTP..." ); + return "Restarting MTP..."; + } + return "MTP waiting for USB host..."; + } + if( g_stopRequested.load() ) { + return g_restartRequested.load() ? "Restarting MTP..." : "Stopping MTP..."; + } + return "MTP waiting for USB host..."; + } + const std::string status = getStatus(); + if( status.find("Stopping MTP") != std::string::npos && hasNoActiveSession() ) { + return "MTP stopped."; + } + return status; +} diff --git a/src/ModManagerGui/FrameGameBrowser/src/TabGames.cpp b/src/ModManagerGui/FrameGameBrowser/src/TabGames.cpp index c35d50a..9c1fe04 100644 --- a/src/ModManagerGui/FrameGameBrowser/src/TabGames.cpp +++ b/src/ModManagerGui/FrameGameBrowser/src/TabGames.cpp @@ -5,11 +5,13 @@ #include "TabGames.h" #include "FrameModBrowser.h" #include "FrameRoot.h" +#include "Toolbox.h" #include "GenericToolbox.Switch.h" #include "GenericToolbox.Vector.h" #include "Logger.h" +#include #include LoggerInit([]{ @@ -18,10 +20,39 @@ LoggerInit([]{ TabGames::TabGames(FrameRoot* owner_) : _owner_(owner_) { LogWarning << "Building game tab..." << std::endl; + this->rebuildLayout(false); + LogInfo << "Game tab build." << std::endl; +} + +void TabGames::willAppear(bool resetState) { + brls::List::willAppear(resetState); + + if( _hasAppearedOnce_ ){ + if( _restoreFocusAfterModBrowser_ ){ + _refreshGameStatusOnNextDraw_ = true; + _restoreFocusOnNextDraw_ = true; + } + else{ + _refreshOnNextDraw_ = true; + } + _restoreFocusAfterModBrowser_ = false; + this->invalidate(); + } + _hasAppearedOnce_ = true; +} + +void TabGames::rebuildLayout(bool force_) { + if( _layoutBuilt_ and not getGameBrowser().refreshGameList(force_) ){ + this->resyncListItemFocusIndices(); + return; + } + + this->clear(true); + _gameList_.clear(); auto gameList = this->getGameBrowser().getSelector().getEntryList(); - if( gameList.empty() ){ + auto addNoGamesItem = [this]() { LogInfo << "No game found." << std::endl; std::stringstream ssTitle; @@ -38,6 +69,10 @@ TabGames::TabGames(FrameRoot* owner_) : _owner_(owner_) { _gameList_.emplace_back(); _gameList_.back().item = new brls::ListItem( ssTitle.str(), ssSubTitle.str() ); _gameList_.back().item->show([](){}, false); + }; + + if( gameList.empty() ){ + addNoGamesItem(); } else{ LogInfo << "Adding " << gameList.size() << " game folders..." << std::endl; @@ -45,20 +80,26 @@ TabGames::TabGames(FrameRoot* owner_) : _owner_(owner_) { _gameList_.reserve( gameList.size() ); for( auto& gameEntry : gameList ){ LogScopeIndent; - LogInfo << "Adding game folder: \"" << gameEntry.title << "\"" << std::endl; + LogDebug << "Adding game folder: \"" << gameEntry.title << "\"" << std::endl; std::string gamePath{GenericToolbox::joinPath(this->getConfig().baseFolder, gameEntry.title)}; - int nMods = int( GenericToolbox::lsDirs(gamePath).size() ); + + auto* icon = Toolbox::getGameFolderIcon(gamePath); + if( icon == nullptr ){ + LogInfo << "Skipping game folder without icon: \"" << gameEntry.title << "\"" << std::endl; + continue; + } // memory allocation - auto* item = new brls::ListItem(gameEntry.title, "", std::to_string(nMods) + " mod(s) available."); + const std::string tag = gameEntry.tag.empty() ? "" : gameEntry.tag; + auto* item = new brls::ListItem(gameEntry.title, "", tag); + item->setThumbnail(icon, 0x20000); + delete[] icon; - // looking for tid is quite slow... Slows down the boot up - std::string _titleId_{ GenericToolbox::Switch::Utils::lookForTidInSubFolders(gamePath) }; - auto* icon = GenericToolbox::Switch::Utils::getIconFromTitleId(_titleId_); - if(icon != nullptr){ item->setThumbnail(icon, 0x20000); } item->getClickEvent()->subscribe([&, gameEntry](View* view) { LogWarning << "Opening \"" << gameEntry.title << "\"" << std::endl; + _focusGameNameAfterReturn_ = gameEntry.title; + _restoreFocusAfterModBrowser_ = true; getGameBrowser().selectGame( gameEntry.title ); auto* modsBrowser = new FrameModBrowser( &_owner_->getGuiModManager() ); brls::Application::pushView(modsBrowser, brls::ViewAnimation::SLIDE_LEFT); @@ -71,45 +112,122 @@ TabGames::TabGames(FrameRoot* owner_) : _owner_(owner_) { _gameList_.emplace_back(); _gameList_.back().title = gameEntry.title; _gameList_.back().item = item; - _gameList_.back().nMods = nMods; } + + if( _gameList_.empty() ){ + addNoGamesItem(); + } } - switch( this->getConfig().sortGameList.value ){ - case ConfigHolder::SortGameList::Alphabetical: - { - LogInfo << "Sorting games wrt nb of mods..." << std::endl; - GenericToolbox::sortVector(_gameList_, [](const GameItem& a_, const GameItem& b_){ - return GenericToolbox::toLowerCase(a_.title) < GenericToolbox::toLowerCase(b_.title); // if true, then a_ goes first - }); - break; + // add to the view + for( auto& game : _gameList_ ){ this->addView( game.item ); } + this->resyncListItemFocusIndices(); + _layoutBuilt_ = true; + +} + +void TabGames::draw(NVGcontext* vg, int x, int y, unsigned width, unsigned height, brls::Style* style, brls::FrameContext* ctx) { + const bool viewTransitionRunning = brls::Application::hasViewDisappearing(); + + if( _refreshOnNextDraw_ ){ + if( not viewTransitionRunning ){ + _refreshOnNextDraw_ = false; + this->rebuildLayout(false); } - case ConfigHolder::SortGameList::NbMods: - { - // "nb-mods" or default - LogInfo << "Sorting games wrt nb of mods..." << std::endl; - GenericToolbox::sortVector(_gameList_, [](const GameItem& a_, const GameItem& b_){ - return a_.nMods > b_.nMods; // if true, then a_ goes first - }); - break; + } + + if( _refreshGameStatusOnNextDraw_ ){ + if( not viewTransitionRunning ){ + _refreshGameStatusOnNextDraw_ = false; + this->refreshDisplayedGameStatus(_focusGameNameAfterReturn_); } - case ConfigHolder::SortGameList::NoSort: - { - LogInfo << "No sort selected." << std::endl; - break; + } + + if( _restoreFocusOnNextDraw_ ){ + if( not viewTransitionRunning ){ + _restoreFocusOnNextDraw_ = false; + this->restoreFocusAfterRebuild(); } - default: - { - LogError << "Invalid sort preset: " << this->getConfig().sortGameList << " / " << this->getConfig().sortGameList.toString() << std::endl; + } + + brls::ScrollView::draw(vg, x, y, width, height, style, ctx); +} + +brls::View* TabGames::getDefaultFocus() { + if( _gameList_.empty() ){ + return nullptr; + } + return _gameList_.front().item; +} + +void TabGames::resyncListItemFocusIndices() { + // Borealis navigation uses parentUserData as the child index. After a full + // clear/rebuild the old focused item can be gone, so refresh every child index. + for( size_t i = 0; i < this->getViewsCount(); ++i ) { + auto* child = this->getChild( i ); + if( child == nullptr ) { + continue; } + auto* parent = child->getParent(); + if( parent == nullptr ) { + continue; + } + + auto* userdata = static_cast( malloc( sizeof(size_t) ) ); + *userdata = i; + child->setParent( parent, userdata ); } - LogDebug << "Sort done." << std::endl; +} - // add to the view - for( auto& game : _gameList_ ){ this->addView( game.item ); } +brls::ListItem* TabGames::findGameItem(const std::string& gameTitle_) const { + if( gameTitle_.empty() ){ + return nullptr; + } - LogInfo << "Game tab build." << std::endl; + for( const auto& game : _gameList_ ){ + if( game.item != nullptr && game.item->getLabel() == gameTitle_ ){ + return game.item; + } + } + return nullptr; +} + +void TabGames::refreshDisplayedGameStatus(const std::string& gameTitle_) { + auto* item = this->findGameItem(gameTitle_); + if( item == nullptr ){ + return; + } + + const std::string tag = this->getGameBrowser().refreshGameListTag(gameTitle_); + item->setSubLabel(tag); + item->invalidate(true); + this->invalidate(true); + if( this->getParent() != nullptr ){ + this->getParent()->invalidate(true); + } +} + +void TabGames::restoreFocusAfterRebuild() { + if( _gameList_.empty() ){ + return; + } + if( this->getParent() == nullptr and not _hasAppearedOnce_ ){ + return; + } + + auto* focusItem = this->findGameItem(_focusGameNameAfterReturn_); + if( focusItem == nullptr ){ + focusItem = _gameList_.front().item; + } + + if( focusItem != nullptr ){ + brls::Application::giveFocus(focusItem); + this->invalidate(true); + if( this->getParent() != nullptr ){ + this->getParent()->invalidate(true); + } + } } const GameBrowser& TabGames::getGameBrowser() const{ diff --git a/src/ModManagerGui/FrameGameBrowser/src/TabGeneralSettings.cpp b/src/ModManagerGui/FrameGameBrowser/src/TabGeneralSettings.cpp index eba98b5..12662d4 100644 --- a/src/ModManagerGui/FrameGameBrowser/src/TabGeneralSettings.cpp +++ b/src/ModManagerGui/FrameGameBrowser/src/TabGeneralSettings.cpp @@ -8,6 +8,15 @@ #include "Logger.h" +#include "GenericToolbox.Fs.h" +#include "GenericToolbox.String.h" +#include "GenericToolbox.Switch.h" + +#include +#include +#include +#include +#include LoggerInit([]{ Logger::setUserHeaderStr("[TabGeneralSettings]"); @@ -68,10 +77,23 @@ void TabGeneralSettings::rebuildLayout() { auto* itemSortGames = new brls::ListItem( "\uE255 Sort games by", - "Set which ordering of the games are displayed in the Game Browser list.\n", + "Set which ordering of the games are displayed in the Game Browser list. Press X to switch ascending/descending.\n", "" ); - itemSortGames->setValue( this->getConfig().sortGameList.toString() ); + itemSortGames->setValue( this->getConfig().getSortGameListSettingDisplayName() ); + + itemSortGames->registerAction("Order", brls::Key::X, [this, itemSortGames](){ + if( this->getConfig().sortGameListDirection == ConfigHolder::SortGameListDirection::Ascending ){ + this->getConfig().sortGameListDirection = ConfigHolder::SortGameListDirection::Descending; + } + else{ + this->getConfig().sortGameListDirection = ConfigHolder::SortGameListDirection::Ascending; + } + _owner_->getGuiModManager().getGameBrowser().getConfigHandler().dumpConfigToFile(); + itemSortGames->setValue( this->getConfig().getSortGameListSettingDisplayName() ); + return true; + }); + itemSortGames->updateActionHint(brls::Key::X, "Order"); // On click : show scrolling up menu @@ -81,14 +103,20 @@ void TabGeneralSettings::rebuildLayout() { // build the choice list + preselection int preSelection{0}; std::vector menuList; + std::vector sortValueList; menuList.reserve( ConfigHolder::SortGameList::getEnumSize() ); + sortValueList.reserve( ConfigHolder::SortGameList::getEnumSize() ); for( int iEnum = 0 ; iEnum < ConfigHolder::SortGameList::getEnumSize() ; iEnum++ ){ - menuList.emplace_back( ConfigHolder::SortGameList::toString(iEnum) ); - if( menuList.back() == this->getConfig().sortGameList.toString() ){ preSelection = iEnum; } + ConfigHolder::SortGameList sortValue{ConfigHolder::SortGameList::getEnumVal(iEnum)}; + sortValueList.emplace_back( sortValue ); + ConfigHolder tmpConfig{this->getConfig()}; + tmpConfig.sortGameList = sortValue; + menuList.emplace_back( tmpConfig.getSortGameListDisplayName() ); + if( sortValue == this->getConfig().sortGameList ){ preSelection = iEnum; } } // function that will set the config preset from the Dropdown menu selection (int result) - brls::ValueSelectedEvent::Callback valueCallback = [this, itemSortGames, menuList](int result) { + brls::ValueSelectedEvent::Callback valueCallback = [this, itemSortGames, menuList, sortValueList](int result) { if( result == -1 ){ LogDebug << "Not selected. Return." << std::endl; // auto pop view @@ -96,9 +124,9 @@ void TabGeneralSettings::rebuildLayout() { } LogInfo << "Selected: " << menuList[result] << std::endl; - this->getConfig().sortGameList = ConfigHolder::SortGameList::toEnum( menuList[result] ); + this->getConfig().sortGameList = sortValueList[result]; _owner_->getGuiModManager().getGameBrowser().getConfigHandler().dumpConfigToFile(); - itemSortGames->setValue( this->getConfig().sortGameList.toString() ); + itemSortGames->setValue( this->getConfig().getSortGameListSettingDisplayName() ); brls::Application::popView(); return; @@ -114,6 +142,36 @@ void TabGeneralSettings::rebuildLayout() { }); this->addView(itemSortGames); + auto* itemShowDebugMtpFiles = new brls::ListItem( + "\uE073 Debug MTP files:", + "Show SimpleModManager internal metadata files in MTP.", + "" + ); + itemShowDebugMtpFiles->setValue(this->getConfig().showDebugMtpFiles ? "Enabled" : "Disabled"); + itemShowDebugMtpFiles->registerAction("Toggle", brls::Key::A, [this, itemShowDebugMtpFiles](){ + this->getConfig().showDebugMtpFiles = !this->getConfig().showDebugMtpFiles; + _owner_->getGuiModManager().getGameBrowser().getConfigHandler().dumpConfigToFile(); + itemShowDebugMtpFiles->setValue(this->getConfig().showDebugMtpFiles ? "Enabled" : "Disabled"); + return true; + }); + itemShowDebugMtpFiles->updateActionHint(brls::Key::A, "Toggle"); + this->addView(itemShowDebugMtpFiles); + + auto* itemOfferOrphanInstalledModCleanup = new brls::ListItem( + "\uE0A6 Orphan installed mod cleanup:", + "Offer to delete installed mod files when their mod folder is no longer present on the SD card.", + "" + ); + itemOfferOrphanInstalledModCleanup->setValue(this->getConfig().offerOrphanInstalledModCleanup ? "Enabled" : "Disabled"); + itemOfferOrphanInstalledModCleanup->registerAction("Toggle", brls::Key::A, [this, itemOfferOrphanInstalledModCleanup](){ + this->getConfig().offerOrphanInstalledModCleanup = !this->getConfig().offerOrphanInstalledModCleanup; + _owner_->getGuiModManager().getGameBrowser().getConfigHandler().dumpConfigToFile(); + itemOfferOrphanInstalledModCleanup->setValue(this->getConfig().offerOrphanInstalledModCleanup ? "Enabled" : "Disabled"); + return true; + }); + itemOfferOrphanInstalledModCleanup->updateActionHint(brls::Key::A, "Toggle"); + this->addView(itemOfferOrphanInstalledModCleanup); + auto* itemUseUI = new brls::ListItem("\uE072 Disable the GUI", "If you want to go back on the old UI, select this option."); diff --git a/src/ModManagerGui/FrameGameBrowser/src/TabImportMod.cpp b/src/ModManagerGui/FrameGameBrowser/src/TabImportMod.cpp new file mode 100644 index 0000000..e578e04 --- /dev/null +++ b/src/ModManagerGui/FrameGameBrowser/src/TabImportMod.cpp @@ -0,0 +1,144 @@ +// +// In-app import: USB MTP responder -> sdmc:/mods +// + +#include "TabImportMod.h" + +#include "ModsMtpServer.h" + +#include "Logger.h" + +#include + +#include + +LoggerInit( [] { + Logger::setUserHeaderStr( "[TabImportMod]" ); +} ); + +namespace { + +const char* kInstructionsEn = + "This screen starts an in-app MTP USB responder. Windows File Explorer can copy files and folders directly " + "to the \"mods\" folder (sdmc:/mods) while this tab is active.\n\n" + "1. Connect the Switch to the PC with a data USB cable.\n" + "2. Wait until the device appears in Explorer.\n" + "3. Open the device, then copy files/folders into the mods storage.\n\n" + "Press A to start/stop MTP."; + +class CompactMultilineLabel : public brls::View { +public: + explicit CompactMultilineLabel(std::string text_) : _text_(std::move(text_)) {} + + void layout(NVGcontext* vg, brls::Style* style, brls::FontStash* stash) override { + float bounds[4]{}; + nvgSave(vg); + nvgReset(vg); + nvgFontSize(vg, style->Label.regularFontSize); + nvgFontFaceId(vg, stash->regular); + nvgTextLineHeight(vg, 1.42F); + nvgTextAlign(vg, NVG_ALIGN_LEFT | NVG_ALIGN_TOP); + nvgTextBoxBounds(vg, this->x, this->y, this->width, _text_.c_str(), nullptr, bounds); + this->height = static_cast(bounds[3] - bounds[1]); + nvgRestore(vg); + } + + void draw( + NVGcontext* vg, + int x, + int y, + unsigned width, + unsigned, + brls::Style* style, + brls::FrameContext* ctx ) override { + nvgFillColor(vg, this->a(ctx->theme->textColor)); + nvgFontSize(vg, style->Label.regularFontSize); + nvgFontFaceId(vg, ctx->fontStash->regular); + nvgTextLineHeight(vg, 1.42F); + nvgTextAlign(vg, NVG_ALIGN_LEFT | NVG_ALIGN_TOP); + nvgTextBox(vg, x, y, width, _text_.c_str(), nullptr); + } + +private: + std::string _text_{}; +}; + +} // namespace + +TabImportMod::TabImportMod() { + LogWarning << "Building Import Mod tab..." << std::endl; + + this->addView( new brls::Header( "Import mod from PC" ) ); + + _bodyLabel_ = new CompactMultilineLabel( kInstructionsEn ); + this->addView( _bodyLabel_ ); + + _statusLabel_ = new brls::Label( brls::LabelStyle::REGULAR, "", true ); + _statusLabel_->setHorizontalAlign( NVG_ALIGN_LEFT ); + this->addView( _statusLabel_ ); + + _actionRow_ = new brls::ListItem( "Toggle MTP responder", "Press A to start or stop USB MTP mode." ); + this->addView( _actionRow_ ); + + _actionRow_->registerAction( "Toggle", brls::Key::A, [this] { + if( ModsMtpServer::isRunning() ) { + ModsMtpServer::stop(); + } + else { + ModsMtpServer::start(); + } + refreshStatusLine(); + return true; + } ); + + refreshStatusLine(); + + LogInfo << "Import Mod tab built." << std::endl; +} + +brls::View* TabImportMod::getDefaultFocus() { + return _actionRow_; +} + +void TabImportMod::customSpacing(brls::View* current, brls::View* next, int* spacing) { + if( current == _bodyLabel_ && next == _statusLabel_ ){ + *spacing = 24; + } + else if( current == _statusLabel_ && next == _actionRow_ ){ + *spacing = 24; + } +} + +void TabImportMod::refreshStatusLine() { + if( _statusLabel_ == nullptr ) { + return; + } + const std::string s = ModsMtpServer::getStatusLine(); + _lastStatus_ = s; + _statusLabel_->setText( s ); + if( _actionRow_ != nullptr ) { + if( ModsMtpServer::isRunning() ) { + _actionRow_->setLabel( "Stop MTP responder" ); + _actionRow_->setDescription( "Press A to stop USB MTP mode." ); + } + else { + _actionRow_->setLabel( "Start MTP responder" ); + _actionRow_->setDescription( "Press A to expose mods folder over USB MTP." ); + } + } +} + +void TabImportMod::draw( + NVGcontext* vg, + int x, + int y, + unsigned width, + unsigned height, + brls::Style* style, + brls::FrameContext* ctx ) { + if( ++_statusTick_ >= 15 ) { + _statusTick_ = 0; + refreshStatusLine(); + } + this->brls::List::draw( vg, x, y, width, height, style, ctx ); +} diff --git a/src/ModManagerGui/FrameModBrowser/include/FrameModBrowser.h b/src/ModManagerGui/FrameModBrowser/include/FrameModBrowser.h index d367741..c4f4f8c 100644 --- a/src/ModManagerGui/FrameModBrowser/include/FrameModBrowser.h +++ b/src/ModManagerGui/FrameModBrowser/include/FrameModBrowser.h @@ -22,12 +22,17 @@ class FrameModBrowser : public brls::TabFrame { public: explicit FrameModBrowser(GuiModManager* guiModManagerPtr_); + void draw(NVGcontext* vg, int x, int y, unsigned width, unsigned height, brls::Style* style, brls::FrameContext* ctx) override; bool onCancel() override; uint8_t *getIcon(); std::string getTitleId(); TabModBrowser* getTabModBrowser(){ return _tabModBrowser_; } TabModPresets* getTabModPresets(){ return _tabModPresets_; } + void resetOrphanCleanupPrompt(){ + _orphanCleanupPromptShown_ = false; + _orphanCleanupScanDone_ = false; + } [[nodiscard]] const ConfigHolder& getConfig() const{ return _guiModManagerPtr_->getGameBrowser().getConfigHandler().getConfig(); } @@ -39,7 +44,11 @@ class FrameModBrowser : public brls::TabFrame { private: + void promptOrphanInstalledModsCleanup(); + GuiModManager* _guiModManagerPtr_{}; + bool _orphanCleanupPromptShown_{false}; + bool _orphanCleanupScanDone_{false}; // memory handled by brls TabModBrowser* _tabModBrowser_{nullptr}; diff --git a/src/ModManagerGui/FrameModBrowser/include/TabModBrowser.h b/src/ModManagerGui/FrameModBrowser/include/TabModBrowser.h index aa46347..b52e2f3 100644 --- a/src/ModManagerGui/FrameModBrowser/include/TabModBrowser.h +++ b/src/ModManagerGui/FrameModBrowser/include/TabModBrowser.h @@ -24,6 +24,9 @@ class TabModBrowser : public brls::List { explicit TabModBrowser(FrameModBrowser* owner_); void updateDisplayedModsStatus(); + void removeDisplayedMod(const std::string& modName_); + void removeDisplayedMod(brls::ListItem* item_); + void rebuildUiFromSd(const std::string& focusModName_ = ""); void draw(NVGcontext* vg, int x, int y, unsigned width, unsigned height, brls::Style* style, brls::FrameContext* ctx) override; @@ -32,6 +35,10 @@ class TabModBrowser : public brls::List { private: FrameModBrowser* _owner_{nullptr}; std::vector _modItemList_{}; + std::string _focusModNameAfterDelete_{}; + + [[nodiscard]] std::string getFocusTargetBeforeDelete(const std::string& modName_) const; + void resyncListItemFocusIndices(); }; diff --git a/src/ModManagerGui/FrameModBrowser/src/FrameModBrowser.cpp b/src/ModManagerGui/FrameModBrowser/src/FrameModBrowser.cpp index 5358d34..cf22722 100644 --- a/src/ModManagerGui/FrameModBrowser/src/FrameModBrowser.cpp +++ b/src/ModManagerGui/FrameModBrowser/src/FrameModBrowser.cpp @@ -9,9 +9,13 @@ #include #include +#include "SystemStatusOverlay.h" + #include "GenericToolbox.Switch.h" #include "Logger.h" +#include +#include LoggerInit([]{ Logger::setUserHeaderStr("[FrameModBrowser]"); @@ -54,16 +58,31 @@ FrameModBrowser::FrameModBrowser(GuiModManager* guiModManagerPtr_) : _guiModMana this->addTab("Options", _tabModOptions_); this->addTab("Plugins", _tabModPlugins_); + // Auto-recheck disabled to prevent freeze - use cache-based verification only + // User can manually trigger verification if needed + // if( not getGuiModManager().isBackgroundTaskRunning() ){ + // getGuiModManager().startCheckAllModsThread(); + // } + } else{ auto* list = new brls::List(); - LogError("Can't open: %s", gamePath.c_str()); - auto* item = new brls::ListItem("Error: Can't open " + gamePath , "", ""); + LogInfo("No mods found for: %s", gamePath.c_str()); + auto* item = new brls::ListItem( + "No mods for this game are on your SD card.", + "Put mods in: " + gamePath, + ""); list->addView(item); this->addTab("Mod Browser", list); } } +void FrameModBrowser::draw(NVGcontext* vg, int x, int y, unsigned width, unsigned height, brls::Style* style, brls::FrameContext* ctx) { + brls::TabFrame::draw(vg, x, y, width, height, style, ctx); + SystemStatusOverlay::draw(vg, x, y, width, style, ctx); + this->promptOrphanInstalledModsCleanup(); +} + bool FrameModBrowser::onCancel() { // Go back to sidebar @@ -85,3 +104,67 @@ uint8_t *FrameModBrowser::getIcon() { std::string FrameModBrowser::getTitleId() { return _titleId_; } + +void FrameModBrowser::promptOrphanInstalledModsCleanup() { + if( _orphanCleanupPromptShown_ ){ + return; + } + if( !this->getConfig().offerOrphanInstalledModCleanup ){ + return; + } + if( brls::Application::hasViewDisappearing() ){ + return; + } + if( brls::Application::getTopStackView() != this ){ + return; + } + if( this->getGuiModManager().isBackgroundTaskRunning() ){ + return; + } + + auto& modManager = this->getGameBrowser().getModManager(); + if( !_orphanCleanupScanDone_ ){ + modManager.refreshOrphanInstalledModList(); + _orphanCleanupScanDone_ = true; + } + + const auto& orphanMods = modManager.getOrphanInstalledModList(); + if( orphanMods.empty() ){ + return; + } + + _orphanCleanupPromptShown_ = true; + + std::vector modNameList; + modNameList.reserve(orphanMods.size()); + for( const auto& orphanMod : orphanMods ){ + modNameList.emplace_back(orphanMod.modName); + } + + std::stringstream ss; + if( modNameList.size() == 1 ){ + if( modNameList.front() == "Unknown installed files" ){ + ss << "Installed files were found for this game, but they do not match any mod currently on your SD card. Delete these installed files?"; + } + else{ + ss << "Installed files were found for \"" << modNameList.front() + << "\", but this mod is no longer on your SD card. Delete these installed files?"; + } + } + else{ + ss << "Installed files were found for " << modNameList.size() + << " cleanup entries that no longer match mods on your SD card. Delete these installed files?"; + } + + auto* dialog = new brls::Dialog(ss.str()); + dialog->addButton("Yes", [this, dialog, modNameList](brls::View* view) { + dialog->close([this, modNameList]{ + this->getGuiModManager().startDeleteOrphanInstalledModsThread(modNameList); + }); + }); + dialog->addButton("No", [dialog](brls::View* view) { + dialog->close(); + }); + dialog->setCancelable(true); + dialog->open(); +} diff --git a/src/ModManagerGui/FrameModBrowser/src/TabModBrowser.cpp b/src/ModManagerGui/FrameModBrowser/src/TabModBrowser.cpp index 883399d..293f115 100644 --- a/src/ModManagerGui/FrameModBrowser/src/TabModBrowser.cpp +++ b/src/ModManagerGui/FrameModBrowser/src/TabModBrowser.cpp @@ -8,17 +8,14 @@ #include "GenericToolbox.Macro.h" +#include "GenericToolbox.String.h" #include "Logger.h" -#include - - LoggerInit([]{ Logger::setUserHeaderStr("[TabModBrowser]"); }); - TabModBrowser::TabModBrowser(FrameModBrowser* owner_) : _owner_(owner_) { // Fetch the available mods @@ -29,8 +26,8 @@ TabModBrowser::TabModBrowser(FrameModBrowser* owner_) : _owner_(owner_) { _modItemList_.emplace_back(); _modItemList_.back().item = new brls::ListItem( - "No mods have been found in " + this->getModManager().getGameFolderPath(), - "There you need to put your mods such as: .//" + "No mods for this game are on your SD card.", + "Put mods in: " + this->getModManager().getGameFolderPath() ); _modItemList_.back().item->show([](){}, false ); } @@ -81,6 +78,38 @@ TabModBrowser::TabModBrowser(FrameModBrowser* owner_) : _owner_(owner_) { dialog->open(); return true; }); + item->updateActionHint(brls::Key::X, "Disable"); + + item->registerAction("Delete mod", brls::Key::Y, [&, mod]{ + if( !_owner_->getGuiModManager().canStartDeleteModFolderThread() ){ + brls::Application::notify("Please wait before deleting another mod."); + return true; + } + + const std::string modName = mod.modName; + auto* dialog = new brls::Dialog("Do you want to delete \"" + mod.modName + "\" from the SD card and remove its installed files?"); + + dialog->addButton("Yes", [this, dialog, modName](brls::View* view) { + if( !_owner_->getGuiModManager().canStartDeleteModFolderThread() ){ + brls::Application::notify("Please wait before deleting another mod."); + dialog->close(); + return; + } + + _focusModNameAfterDelete_ = this->getFocusTargetBeforeDelete(modName); + dialog->close([this, modName]{ + if( !_owner_->getGuiModManager().startDeleteModFolderThread(modName) ){ + _focusModNameAfterDelete_.clear(); + } + }); + }); + dialog->addButton("No", [dialog](brls::View* view) { dialog->close(); }); + + dialog->setCancelable(true); + dialog->open(); + return true; + }); + item->updateActionHint(brls::Key::Y, "Delete mod"); // create the holding struct _modItemList_.emplace_back(); @@ -98,9 +127,250 @@ TabModBrowser::TabModBrowser(FrameModBrowser* owner_) : _owner_(owner_) { } +void TabModBrowser::rebuildUiFromSd(const std::string& focusModName_) { + // Fully rebuild UI to guarantee it matches SD state. + brls::Application::giveFocus(nullptr); + _owner_->getGameBrowser().getModManager().updateModList(); + + this->clear( true ); + _modItemList_.clear(); + + auto modList = this->getModManager().getModList(); + brls::ListItem* focusItem = nullptr; + + if( modList.empty() ){ + _modItemList_.emplace_back(); + _modItemList_.back().item = new brls::ListItem( + "No mods for this game are on your SD card.", + "Put mods in: " + this->getModManager().getGameFolderPath() + ); + _modItemList_.back().item->show([](){}, false); + this->addView( _modItemList_.back().item ); + this->resyncListItemFocusIndices(); + brls::Application::giveFocus( _modItemList_.back().item ); + this->invalidate(true); + if( this->getParent() != nullptr ) { + this->getParent()->invalidate(true); + } + return; + } + + _modItemList_.reserve( modList.size() ); + for( auto& mod : modList ){ + // memory allocation + auto* item = new brls::ListItem(mod.modName, "", ""); + + // Click to install + item->getClickEvent()->subscribe([&, mod](View* view) { + auto* dialog = new brls::Dialog("Do you want to install \"" + mod.modName + "\" ?"); + + dialog->addButton("Yes", [&, mod, dialog](brls::View* view) { + dialog->close(); + _owner_->getGuiModManager().startApplyModThread( mod.modName ); + }); + dialog->addButton("No", [dialog](brls::View* view) { dialog->close(); }); + dialog->setCancelable(true); + dialog->open(); + return true; + }); + item->updateActionHint(brls::Key::A, "Apply"); + + // Disable + item->registerAction("Disable", brls::Key::X, [&, mod]{ + auto* dialog = new brls::Dialog("Do you want to disable \"" + mod.modName + "\" ?"); + dialog->addButton("Yes", [&, dialog, mod](brls::View* view) { + dialog->close(); + _owner_->getGuiModManager().startRemoveModThread( mod.modName ); + }); + dialog->addButton("No", [dialog](brls::View* view) { dialog->close(); }); + dialog->setCancelable(true); + dialog->open(); + return true; + }); + item->updateActionHint(brls::Key::X, "Disable"); + + // Delete folder (SD) + item->registerAction("Delete mod", brls::Key::Y, [&, mod]{ + if( !_owner_->getGuiModManager().canStartDeleteModFolderThread() ){ + brls::Application::notify("Please wait before deleting another mod."); + return true; + } + + const std::string modName = mod.modName; + auto* dialog = new brls::Dialog("Do you want to delete \"" + mod.modName + "\" from the SD card and remove its installed files?"); + dialog->addButton("Yes", [this, dialog, modName](brls::View* view) { + if( !_owner_->getGuiModManager().canStartDeleteModFolderThread() ){ + brls::Application::notify("Please wait before deleting another mod."); + dialog->close(); + return; + } + + _focusModNameAfterDelete_ = this->getFocusTargetBeforeDelete(modName); + dialog->close([this, modName]{ + if( !_owner_->getGuiModManager().startDeleteModFolderThread(modName) ){ + _focusModNameAfterDelete_.clear(); + } + }); + }); + dialog->addButton("No", [dialog](brls::View* view) { dialog->close(); }); + dialog->setCancelable(true); + dialog->open(); + return true; + }); + item->updateActionHint(brls::Key::Y, "Delete mod"); + + _modItemList_.emplace_back(); + _modItemList_.back().modIndex = int(_modItemList_.size()) - 1; + _modItemList_.back().item = item; + if( item->getLabel() == focusModName_ ) { + focusItem = item; + } + } + + this->updateDisplayedModsStatus(); + for( auto& modItem : _modItemList_ ){ + this->addView( modItem.item ); + } + this->resyncListItemFocusIndices(); + if( focusItem == nullptr && !_modItemList_.empty() ) { + focusItem = _modItemList_.front().item; + } + if( focusItem != nullptr ) { + brls::Application::giveFocus( focusItem ); + } + this->invalidate(true); + if( this->getParent() != nullptr ) { + this->getParent()->invalidate(true); + } +} + +std::string TabModBrowser::getFocusTargetBeforeDelete(const std::string& modName_) const { + for( size_t i = 0; i < _modItemList_.size(); ++i ) { + auto* item = _modItemList_[i].item; + if( item == nullptr || item->getLabel() != modName_ ) { + continue; + } + + if( i > 0 && _modItemList_[i - 1].item != nullptr ) { + return _modItemList_[i - 1].item->getLabel(); + } + if( i + 1 < _modItemList_.size() && _modItemList_[i + 1].item != nullptr ) { + return _modItemList_[i + 1].item->getLabel(); + } + return {}; + } + return {}; +} + +void TabModBrowser::removeDisplayedMod(const std::string& modName_) { + bool removed = false; + for( size_t i = 0; i < _modItemList_.size(); ++i ) { + auto& modItem = _modItemList_[i]; + if( modItem.item == nullptr ) { + continue; + } + if( modItem.item->getLabel() == modName_ ) { + this->removeView( static_cast( i ) ); + _modItemList_.erase( _modItemList_.begin() + static_cast( i ) ); + removed = true; + break; + } + } + + if( _modItemList_.empty() ) { + auto* emptyItem = new brls::ListItem( + "No mods for this game are on your SD card.", + "Put mods in: " + this->getModManager().getGameFolderPath() ); + emptyItem->show([](){}, false); + _modItemList_.push_back( ModItem{} ); + _modItemList_.back().item = emptyItem; + this->addView( emptyItem ); + } + + if( removed ) { + this->resyncListItemFocusIndices(); + this->invalidate(true); + if( this->getParent() != nullptr ) { + this->getParent()->invalidate(true); + } + if( not _modItemList_.empty() && _modItemList_.front().item != nullptr ) { + brls::Application::giveFocus( _modItemList_.front().item ); + } + } +} + +void TabModBrowser::removeDisplayedMod(brls::ListItem* item_) { + bool removed = false; + for( size_t i = 0; i < _modItemList_.size(); ++i ) { + auto& modItem = _modItemList_[i]; + if( modItem.item == nullptr ) { + continue; + } + if( modItem.item == item_ ) { + this->removeView( static_cast( i ) ); + _modItemList_.erase( _modItemList_.begin() + static_cast( i ) ); + removed = true; + break; + } + } + + if( _modItemList_.empty() ) { + auto* emptyItem = new brls::ListItem( + "No mods for this game are on your SD card.", + "Put mods in: " + this->getModManager().getGameFolderPath() ); + emptyItem->show([](){}, false); + _modItemList_.push_back( ModItem{} ); + _modItemList_.back().item = emptyItem; + this->addView( emptyItem ); + } + + if( removed ) { + this->resyncListItemFocusIndices(); + this->invalidate(true); + if( this->getParent() != nullptr ) { + this->getParent()->invalidate(true); + } + if( not _modItemList_.empty() && _modItemList_.front().item != nullptr ) { + brls::Application::giveFocus( _modItemList_.front().item ); + } + } +} + +void TabModBrowser::resyncListItemFocusIndices() { + // Borealis uses parentUserData (child index) to navigate in BoxLayout::getNextFocus(). + // After removeView(), the remaining indices can be stale and trap focus on one item. + for( size_t i = 0; i < this->getViewsCount(); ++i ) { + auto* child = this->getChild( i ); + if( child == nullptr ) { + continue; + } + auto* parent = child->getParent(); + if( parent == nullptr ) { + continue; + } + + auto* userdata = static_cast( malloc( sizeof(size_t) ) ); + *userdata = i; + child->setParent( parent, userdata ); + } +} + void TabModBrowser::draw(NVGcontext *vg, int x, int y, unsigned int width, unsigned int height, brls::Style *style, brls::FrameContext *ctx) { + if( _owner_->getGuiModManager().isTriggerRebuildModBrowser() ){ + const bool canRebuildNow = !brls::Application::hasViewDisappearing() + && brls::Application::getTopStackView() == _owner_; + if( canRebuildNow ){ + LogDebug << "Rebuilding mod browser from SD..." << std::endl; + _owner_->getGuiModManager().setTriggerRebuildModBrowser( false ); + _owner_->getGuiModManager().setTriggerUpdateModsDisplayedStatus( false ); + this->rebuildUiFromSd(_focusModNameAfterDelete_); + _owner_->resetOrphanCleanupPrompt(); + _focusModNameAfterDelete_.clear(); + } + } + ScrollView::draw(vg, x, y, width, height, style, ctx); if( _owner_->getGuiModManager().isTriggerUpdateModsDisplayedStatus() ){ @@ -119,16 +389,36 @@ void TabModBrowser::updateDisplayedModsStatus(){ auto currentPreset = this->getModManager().fetchCurrentPreset().name; LogInfo << "Will display mod status with install preset: " << currentPreset << std::endl; - for( size_t iMod = 0 ; iMod < modEntryList.size() ; iMod++ ){ + for( size_t iMod = 0 ; iMod < modEntryList.size() && iMod < _modItemList_.size() ; iMod++ ){ + if( _modItemList_[iMod].item == nullptr ){ + continue; + } + + // Use cached status only to avoid slow verification + std::string statusStr; + double frac = 0.0; + + // Check if status is cached + if( GenericToolbox::isIn(currentPreset, modEntryList[iMod].applyCache) ){ + statusStr = modEntryList[iMod].applyCache[currentPreset].statusStr; + frac = modEntryList[iMod].applyCache[currentPreset].applyFraction; + } else { + // Default to UNCHECKED if not cached + statusStr = "UNCHECKED"; + frac = 0.0; + } // processing tag - _modItemList_[iMod].item->setValue( modEntryList[iMod].getStatus(currentPreset ) ); - double frac = modEntryList[iMod].getStatusFraction(currentPreset); + _modItemList_[iMod].item->setValue( statusStr ); NVGcolor color; // processing color - if ( frac == 0 ){ - // inactive color + if( GenericToolbox::startsWith(statusStr, "PARTIAL") ){ + // partial or conflicting files + color = GenericToolbox::Borealis::orangeNvgColor; + } + else if( frac == 0 ){ + // inactive/unchecked color color = GenericToolbox::Borealis::grayNvgColor; } else if( frac == 1 ){ @@ -136,7 +426,7 @@ void TabModBrowser::updateDisplayedModsStatus(){ color = nvgRGB(88, 195, 169); } else{ - // partial color + // partial color fallback color = GenericToolbox::Borealis::orangeNvgColor; } _modItemList_[iMod].item->setValueActiveColor( color ); diff --git a/src/ThirdParty/mtp-server-nx/include/MtpDataPacket.h b/src/ThirdParty/mtp-server-nx/include/MtpDataPacket.h new file mode 100644 index 0000000..ab116c9 --- /dev/null +++ b/src/ThirdParty/mtp-server-nx/include/MtpDataPacket.h @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef _MTP_DATA_PACKET_H +#define _MTP_DATA_PACKET_H + +#include "MtpPacket.h" +#include "mtp.h" + +namespace android { + +class MtpStringBuffer; + +class MtpDataPacket : public MtpPacket { +private: + // current offset for get/put methods + int mOffset; + +public: + MtpDataPacket(); + virtual ~MtpDataPacket(); + + virtual void reset(); + + void setOperationCode(MtpOperationCode code); + void setTransactionID(MtpTransactionID id); + + inline const uint8_t* getData() const { return mBuffer + MTP_CONTAINER_HEADER_SIZE; } + inline uint8_t getUInt8() { return (uint8_t)mBuffer[mOffset++]; } + inline int8_t getInt8() { return (int8_t)mBuffer[mOffset++]; } + uint16_t getUInt16(); + inline int16_t getInt16() { return (int16_t)getUInt16(); } + uint32_t getUInt32(); + inline int32_t getInt32() { return (int32_t)getUInt32(); } + uint64_t getUInt64(); + inline int64_t getInt64() { return (int64_t)getUInt64(); } + void getUInt128(uint128_t& value); + inline void getInt128(int128_t& value) { getUInt128((uint128_t&)value); } + void getString(MtpStringBuffer& string); + + Int8List* getAInt8(); + UInt8List* getAUInt8(); + Int16List* getAInt16(); + UInt16List* getAUInt16(); + Int32List* getAInt32(); + UInt32List* getAUInt32(); + Int64List* getAInt64(); + UInt64List* getAUInt64(); + + void putInt8(int8_t value); + void putUInt8(uint8_t value); + void putInt16(int16_t value); + void putUInt16(uint16_t value); + void putInt32(int32_t value); + void putUInt32(uint32_t value); + void putInt64(int64_t value); + void putUInt64(uint64_t value); + void putInt128(const int128_t& value); + void putUInt128(const uint128_t& value); + void putInt128(int64_t value); + void putUInt128(uint64_t value); + + void putAInt8(const int8_t* values, int count); + void putAUInt8(const uint8_t* values, int count); + void putAInt16(const int16_t* values, int count); + void putAUInt16(const uint16_t* values, int count); + void putAUInt16(const UInt16List* values); + void putAInt32(const int32_t* values, int count); + void putAUInt32(const uint32_t* values, int count); + void putAUInt32(const UInt32List* list); + void putAInt64(const int64_t* values, int count); + void putAUInt64(const uint64_t* values, int count); + void putString(const MtpStringBuffer& string); + void putString(const char* string); + void putString(const uint16_t* string); + inline void putEmptyString() { putUInt8(0); } + inline void putEmptyArray() { putUInt32(0); } + + // fill our buffer with data from the given file descriptor + int read(USBMtpInterface* usb); + int read(USBMtpInterface* usb, uint32_t length); + int readWithTimeout(USBMtpInterface* usb, uint64_t timeout); + int readWithTimeout(USBMtpInterface* usb, uint32_t length, uint64_t timeout); + + // write our data to the given file descriptor + int write(USBMtpInterface* usb); + int writeData(USBMtpInterface* usb, void* data, uint32_t length); + + inline bool hasData() const { return mPacketSize > MTP_CONTAINER_HEADER_SIZE; } + inline uint32_t getContainerLength() const { return MtpPacket::getUInt32(MTP_CONTAINER_LENGTH_OFFSET); } + void* getData(int& outLength) const; +}; + +}; // namespace android + +#endif // _MTP_DATA_PACKET_H diff --git a/src/ThirdParty/mtp-server-nx/include/MtpDatabase.h b/src/ThirdParty/mtp-server-nx/include/MtpDatabase.h new file mode 100644 index 0000000..96d6d34 --- /dev/null +++ b/src/ThirdParty/mtp-server-nx/include/MtpDatabase.h @@ -0,0 +1,131 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef _MTP_DATABASE_H +#define _MTP_DATABASE_H + +#include "MtpTypes.h" +#include "MtpServer.h" + +namespace android { + +class MtpDataPacket; +class MtpProperty; +class MtpObjectInfo; + +class MtpDatabase { +public: + virtual ~MtpDatabase() {} + + virtual bool isHandleValid(MtpObjectHandle handle) = 0; + + // called to add a path to include in the database. + virtual void addStoragePath(const MtpString& path, + const MtpString& displayName, + MtpStorageID storage, + bool hidden) = 0; + + // Called to remove database entries for a storage. + virtual void removeStorage(MtpStorageID storage) = 0; + + // called from SendObjectInfo to reserve a database entry for the incoming file + virtual MtpObjectHandle beginSendObject(const MtpString& path, + MtpObjectFormat format, + MtpObjectHandle parent, + MtpStorageID storage, + uint64_t size, + time_t modified) = 0; + + virtual void updateObjectSize(MtpObjectHandle handle, + uint64_t size) = 0; + + // called to report success or failure of the SendObject file transfer + // success should signal a notification of the new object's creation, + // failure should remove the database entry created in beginSendObject + virtual void endSendObject(const MtpString& path, + MtpObjectHandle handle, + MtpObjectFormat format, + bool succeeded) = 0; + + virtual MtpObjectHandleList* getObjectList(MtpStorageID storageID, + MtpObjectFormat format, + MtpObjectHandle parent) = 0; + + virtual int getNumObjects(MtpStorageID storageID, + MtpObjectFormat format, + MtpObjectHandle parent) = 0; + + // callee should delete[] the results from these + // results can be NULL + virtual MtpObjectFormatList* getSupportedPlaybackFormats() = 0; + virtual MtpObjectFormatList* getSupportedCaptureFormats() = 0; + virtual MtpObjectPropertyList* getSupportedObjectProperties(MtpObjectFormat format) = 0; + virtual MtpDevicePropertyList* getSupportedDeviceProperties() = 0; + + virtual MtpResponseCode getObjectPropertyValue(MtpObjectHandle handle, + MtpObjectProperty property, + MtpDataPacket& packet) = 0; + + virtual MtpResponseCode setObjectPropertyValue(MtpObjectHandle handle, + MtpObjectProperty property, + MtpDataPacket& packet) = 0; + + virtual MtpResponseCode getDevicePropertyValue(MtpDeviceProperty property, + MtpDataPacket& packet) = 0; + + virtual MtpResponseCode setDevicePropertyValue(MtpDeviceProperty property, + MtpDataPacket& packet) = 0; + + virtual MtpResponseCode resetDeviceProperty(MtpDeviceProperty property) = 0; + + virtual MtpResponseCode getObjectPropertyList(MtpObjectHandle handle, + uint32_t format, uint32_t property, + int groupCode, int depth, + MtpDataPacket& packet) = 0; + + virtual MtpResponseCode getObjectInfo(MtpObjectHandle handle, + MtpObjectInfo& info) = 0; + + virtual void* getThumbnail(MtpObjectHandle handle, size_t& outThumbSize) = 0; + + virtual MtpResponseCode getObjectFilePath(MtpObjectHandle handle, + MtpString& outFilePath, + int64_t& outFileLength, + MtpObjectFormat& outFormat) = 0; + + virtual MtpResponseCode deleteFile(MtpObjectHandle handle) = 0; + + virtual MtpResponseCode moveFile(MtpObjectHandle handle, + MtpObjectHandle new_parent) = 0; + + virtual MtpObjectHandleList* getObjectReferences(MtpObjectHandle handle) = 0; + + virtual MtpResponseCode setObjectReferences(MtpObjectHandle handle, + MtpObjectHandleList* references) = 0; + + virtual MtpProperty* getObjectPropertyDesc(MtpObjectProperty property, + MtpObjectFormat format) = 0; + + virtual MtpProperty* getDevicePropertyDesc(MtpDeviceProperty property) = 0; + + virtual void sessionStarted(MtpServer* server) = 0; + + virtual void sessionEnded() = 0; +}; + +}; // namespace android + +#endif // _MTP_DATABASE_H diff --git a/src/ThirdParty/mtp-server-nx/include/MtpDebug.h b/src/ThirdParty/mtp-server-nx/include/MtpDebug.h new file mode 100644 index 0000000..0527cd4 --- /dev/null +++ b/src/ThirdParty/mtp-server-nx/include/MtpDebug.h @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef _MTP_DEBUG_H +#define _MTP_DEBUG_H + +#include "MtpTypes.h" + +namespace android { + +class MtpDebug { +public: + static const char* getOperationCodeName(MtpOperationCode code); + static const char* getFormatCodeName(MtpObjectFormat code); + static const char* getObjectPropCodeName(MtpPropertyCode code); + static const char* getDevicePropCodeName(MtpPropertyCode code); +}; + +}; // namespace android + +#endif // _MTP_DEBUG_H diff --git a/src/ThirdParty/mtp-server-nx/include/MtpDeviceInfo.h b/src/ThirdParty/mtp-server-nx/include/MtpDeviceInfo.h new file mode 100644 index 0000000..2abaa10 --- /dev/null +++ b/src/ThirdParty/mtp-server-nx/include/MtpDeviceInfo.h @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef _MTP_DEVICE_INFO_H +#define _MTP_DEVICE_INFO_H + +struct stat; + +namespace android { + +class MtpDataPacket; + +class MtpDeviceInfo { +public: + uint16_t mStandardVersion; + uint32_t mVendorExtensionID; + uint16_t mVendorExtensionVersion; + char* mVendorExtensionDesc; + uint16_t mFunctionalCode; + UInt16List* mOperations; + UInt16List* mEvents; + MtpDevicePropertyList* mDeviceProperties; + MtpObjectFormatList* mCaptureFormats; + MtpObjectFormatList* mPlaybackFormats; + char* mManufacturer; + char* mModel; + char* mVersion; + char* mSerial; + +public: + MtpDeviceInfo(); + virtual ~MtpDeviceInfo(); + + void read(MtpDataPacket& packet); + + void print(); +}; + +}; // namespace android + +#endif // _MTP_DEVICE_INFO_H diff --git a/src/ThirdParty/mtp-server-nx/include/MtpEventPacket.h b/src/ThirdParty/mtp-server-nx/include/MtpEventPacket.h new file mode 100644 index 0000000..7782815 --- /dev/null +++ b/src/ThirdParty/mtp-server-nx/include/MtpEventPacket.h @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef _MTP_EVENT_PACKET_H +#define _MTP_EVENT_PACKET_H + +#include "MtpPacket.h" +#include "mtp.h" + +namespace android { + +class MtpEventPacket : public MtpPacket { + +public: + MtpEventPacket(); + virtual ~MtpEventPacket(); + + // write our data to the given file descriptor + int write(USBMtpInterface* usb); + + inline MtpEventCode getEventCode() const { return getContainerCode(); } + inline void setEventCode(MtpEventCode code) + { return setContainerCode(code); } +}; + +}; // namespace android + +#endif // _MTP_EVENT_PACKET_H diff --git a/src/ThirdParty/mtp-server-nx/include/MtpObjectInfo.h b/src/ThirdParty/mtp-server-nx/include/MtpObjectInfo.h new file mode 100644 index 0000000..c7a449c --- /dev/null +++ b/src/ThirdParty/mtp-server-nx/include/MtpObjectInfo.h @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef _MTP_OBJECT_INFO_H +#define _MTP_OBJECT_INFO_H + +#include "MtpTypes.h" + +namespace android { + +class MtpDataPacket; + +class MtpObjectInfo { +public: + MtpObjectHandle mHandle; + MtpStorageID mStorageID; + MtpObjectFormat mFormat; + uint16_t mProtectionStatus; + uint32_t mCompressedSize; + MtpObjectFormat mThumbFormat; + uint32_t mThumbCompressedSize; + uint32_t mThumbPixWidth; + uint32_t mThumbPixHeight; + uint32_t mImagePixWidth; + uint32_t mImagePixHeight; + uint32_t mImagePixDepth; + MtpObjectHandle mParent; + uint16_t mAssociationType; + uint32_t mAssociationDesc; + uint32_t mSequenceNumber; + char* mName; + time_t mDateCreated; + time_t mDateModified; + char* mKeywords; + +public: + MtpObjectInfo(MtpObjectHandle handle); + virtual ~MtpObjectInfo(); + + void read(MtpDataPacket& packet); + + void print(); +}; + +}; // namespace android + +#endif // _MTP_OBJECT_INFO_H diff --git a/src/ThirdParty/mtp-server-nx/include/MtpPacket.h b/src/ThirdParty/mtp-server-nx/include/MtpPacket.h new file mode 100644 index 0000000..849ac25 --- /dev/null +++ b/src/ThirdParty/mtp-server-nx/include/MtpPacket.h @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef _MTP_PACKET_H +#define _MTP_PACKET_H + +#include "MtpTypes.h" +#include "USBMtpInterface.h" + +namespace android { + +class MtpPacket { + +protected: + uint8_t* mBuffer; + // current size of the buffer + int mBufferSize; + // number of bytes to add when resizing the buffer + int mAllocationIncrement; + // size of the data in the packet + int mPacketSize; + +public: + MtpPacket(int bufferSize); + virtual ~MtpPacket(); + + // sets packet size to the default container size and sets buffer to zero + virtual void reset(); + + void allocate(int length); + void dump(); + void copyFrom(const MtpPacket& src); + + uint16_t getContainerCode() const; + void setContainerCode(uint16_t code); + + uint16_t getContainerType() const; + + MtpTransactionID getTransactionID() const; + void setTransactionID(MtpTransactionID id); + + uint32_t getParameter(int index) const; + void setParameter(int index, uint32_t value); + +protected: + uint16_t getUInt16(int offset) const; + uint32_t getUInt32(int offset) const; + void putUInt16(int offset, uint16_t value); + void putUInt32(int offset, uint32_t value); +}; + +}; // namespace android + +#endif // _MTP_PACKET_H diff --git a/src/ThirdParty/mtp-server-nx/include/MtpProperty.h b/src/ThirdParty/mtp-server-nx/include/MtpProperty.h new file mode 100644 index 0000000..06ca56e --- /dev/null +++ b/src/ThirdParty/mtp-server-nx/include/MtpProperty.h @@ -0,0 +1,114 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef _MTP_PROPERTY_H +#define _MTP_PROPERTY_H + +#include "MtpTypes.h" + +namespace android { + +class MtpDataPacket; + +struct MtpPropertyValue { + union { + int8_t i8; + uint8_t u8; + int16_t i16; + uint16_t u16; + int32_t i32; + uint32_t u32; + int64_t i64; + uint64_t u64; + int128_t i128; + uint128_t u128; + } u; + // string in UTF8 format + char* str; +}; + +class MtpProperty { +public: + MtpPropertyCode mCode; + MtpDataType mType; + bool mWriteable; + MtpPropertyValue mDefaultValue; + MtpPropertyValue mCurrentValue; + + // for array types + int mDefaultArrayLength; + MtpPropertyValue* mDefaultArrayValues; + int mCurrentArrayLength; + MtpPropertyValue* mCurrentArrayValues; + + enum { + kFormNone = 0, + kFormRange = 1, + kFormEnum = 2, + kFormDateTime = 3, + }; + + uint32_t mGroupCode; + uint8_t mFormFlag; + + // for range form + MtpPropertyValue mMinimumValue; + MtpPropertyValue mMaximumValue; + MtpPropertyValue mStepSize; + + // for enum form + int mEnumLength; + MtpPropertyValue* mEnumValues; + +public: + MtpProperty(); + MtpProperty(MtpPropertyCode propCode, + MtpDataType type, + bool writeable = false, + int defaultValue = 0); + virtual ~MtpProperty(); + + inline MtpPropertyCode getPropertyCode() const { return mCode; } + + void read(MtpDataPacket& packet); + void write(MtpDataPacket& packet); + + void setDefaultValue(const uint16_t* string); + void setCurrentValue(const uint16_t* string); + + void setFormRange(int min, int max, int step); + void setFormEnum(const int* values, int count); + void setFormDateTime(); + + void print(); + void print(MtpPropertyValue& value, MtpString& buffer); + + inline bool isDeviceProperty() const { + return ( ((mCode & 0xF000) == 0x5000) + || ((mCode & 0xF800) == 0xD000)); + } + +private: + void readValue(MtpDataPacket& packet, MtpPropertyValue& value); + void writeValue(MtpDataPacket& packet, MtpPropertyValue& value); + MtpPropertyValue* readArrayValues(MtpDataPacket& packet, int& length); + void writeArrayValues(MtpDataPacket& packet, + MtpPropertyValue* values, int length); +}; + +}; // namespace android + +#endif // _MTP_PROPERTY_H diff --git a/src/ThirdParty/mtp-server-nx/include/MtpRequestPacket.h b/src/ThirdParty/mtp-server-nx/include/MtpRequestPacket.h new file mode 100644 index 0000000..205fac9 --- /dev/null +++ b/src/ThirdParty/mtp-server-nx/include/MtpRequestPacket.h @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef _MTP_REQUEST_PACKET_H +#define _MTP_REQUEST_PACKET_H + +#include "MtpPacket.h" +#include "mtp.h" + +namespace android { + +class MtpRequestPacket : public MtpPacket { + +public: + MtpRequestPacket(); + virtual ~MtpRequestPacket(); + + // fill our buffer with data from the given file descriptor + int read(USBMtpInterface* usb); + + inline MtpOperationCode getOperationCode() const { return getContainerCode(); } + inline void setOperationCode(MtpOperationCode code) + { return setContainerCode(code); } +}; + +}; // namespace android + +#endif // _MTP_REQUEST_PACKET_H diff --git a/src/ThirdParty/mtp-server-nx/include/MtpResponsePacket.h b/src/ThirdParty/mtp-server-nx/include/MtpResponsePacket.h new file mode 100644 index 0000000..fbf5131 --- /dev/null +++ b/src/ThirdParty/mtp-server-nx/include/MtpResponsePacket.h @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef _MTP_RESPONSE_PACKET_H +#define _MTP_RESPONSE_PACKET_H + +#include "MtpPacket.h" +#include "mtp.h" + +namespace android { + +class MtpResponsePacket : public MtpPacket { + +public: + MtpResponsePacket(); + virtual ~MtpResponsePacket(); + + // write our data to the given file descriptor + int write(USBMtpInterface* usb); + + inline MtpResponseCode getResponseCode() const { return getContainerCode(); } + inline void setResponseCode(MtpResponseCode code) + { return setContainerCode(code); } +}; + +}; // namespace android + +#endif // _MTP_RESPONSE_PACKET_H diff --git a/src/ThirdParty/mtp-server-nx/include/MtpServer.h b/src/ThirdParty/mtp-server-nx/include/MtpServer.h new file mode 100644 index 0000000..3827c41 --- /dev/null +++ b/src/ThirdParty/mtp-server-nx/include/MtpServer.h @@ -0,0 +1,169 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef _MTP_SERVER_H +#define _MTP_SERVER_H + +#include "MtpRequestPacket.h" +#include "MtpDataPacket.h" +#include "MtpResponsePacket.h" +#include "MtpEventPacket.h" +#include "mtp.h" +#include "MtpUtils.h" +#include "USBMtpInterface.h" + +#include +#include +#include + +namespace android { + +class MtpDatabase; +class MtpStorage; + +class MtpServer { + +private: + // USB interface + USBMtpInterface* mUSB; + + MtpDatabase* mDatabase; + + // keep state whether the server should be running + std::atomic mRunning; + std::atomic mStopRequested; + + // appear as a PTP device + bool mPtp; + + // group to own new files and folders + int mFileGroup; + // permissions for new files and directories + int mFilePermission; + int mDirectoryPermission; + + // current session ID + MtpSessionID mSessionID; + // true if we have an open session and mSessionID is valid + bool mSessionOpen; + + MtpRequestPacket mRequest; + MtpDataPacket mData; + MtpResponsePacket mResponse; + MtpEventPacket mEvent; + + MtpStorageList mStorages; + + // handle for new object, set by SendObjectInfo and used by SendObject + MtpObjectHandle mSendObjectHandle; + MtpObjectFormat mSendObjectFormat; + MtpString mSendObjectFilePath; + size_t mSendObjectFileSize; + + MtpMutex mMutex; + + // represents an MTP object that is being edited using the android extensions + // for direct editing (BeginEditObject, SendPartialObject, TruncateObject and EndEditObject) + class ObjectEdit { + public: + MtpObjectHandle mHandle; + MtpString mPath; + uint64_t mSize; + MtpObjectFormat mFormat; + int mFD; + + ObjectEdit(MtpObjectHandle handle, const MtpString& path, uint64_t size, + MtpObjectFormat format, int fd) + : mHandle(handle), mPath(path), mSize(size), mFormat(format), mFD(fd) { + } + + virtual ~ObjectEdit() { + close(mFD); + } + }; + Vector mObjectEditList; + +public: + MtpServer(USBMtpInterface* usb, MtpDatabase* database, bool ptp, + int fileGroup, int filePerm, int directoryPerm); + virtual ~MtpServer(); + + MtpStorage* getStorage(MtpStorageID id); + inline bool hasStorage() { return mStorages.size() > 0; } + bool hasStorage(MtpStorageID id); + void addStorage(MtpStorage* storage); + void removeStorage(MtpStorage* storage); + + void run(); + void stop(); + + void sendObjectAdded(MtpObjectHandle handle); + void sendObjectRemoved(MtpObjectHandle handle); + void sendObjectInfoChanged(MtpObjectHandle handle); + void sendObjectPropChanged(MtpObjectHandle handle, + MtpObjectProperty prop); + +private: + void sendStoreAdded(MtpStorageID id); + void sendStoreRemoved(MtpStorageID id); + void sendEvent(MtpEventCode code, + uint32_t param1, + uint32_t param2, + uint32_t param3); + + void addEditObject(MtpObjectHandle handle, MtpString& path, + uint64_t size, MtpObjectFormat format, int fd); + ObjectEdit* getEditObject(MtpObjectHandle handle); + void removeEditObject(MtpObjectHandle handle); + void commitEdit(ObjectEdit* edit); + + bool handleRequest(); + + MtpResponseCode doGetDeviceInfo(); + MtpResponseCode doOpenSession(); + MtpResponseCode doCloseSession(); + MtpResponseCode doGetStorageIDs(); + MtpResponseCode doGetStorageInfo(); + MtpResponseCode doGetObjectPropsSupported(); + MtpResponseCode doGetObjectHandles(); + MtpResponseCode doGetNumObjects(); + MtpResponseCode doGetObjectReferences(); + MtpResponseCode doSetObjectReferences(); + MtpResponseCode doGetObjectPropValue(); + MtpResponseCode doSetObjectPropValue(); + MtpResponseCode doGetDevicePropValue(); + MtpResponseCode doSetDevicePropValue(); + MtpResponseCode doResetDevicePropValue(); + MtpResponseCode doGetObjectPropList(); + MtpResponseCode doGetObjectInfo(); + MtpResponseCode doGetObject(); + MtpResponseCode doGetThumb(); + MtpResponseCode doGetPartialObject(MtpOperationCode operation); + MtpResponseCode doSendObjectInfo(); + MtpResponseCode doSendObject(); + MtpResponseCode doDeleteObject(); + MtpResponseCode doMoveObject(); + MtpResponseCode doGetObjectPropDesc(); + MtpResponseCode doGetDevicePropDesc(); + MtpResponseCode doSendPartialObject(); + MtpResponseCode doTruncateObject(); + MtpResponseCode doBeginEditObject(); + MtpResponseCode doEndEditObject(); +}; + +}; // namespace android + +#endif // _MTP_SERVER_H diff --git a/src/ThirdParty/mtp-server-nx/include/MtpStorage.h b/src/ThirdParty/mtp-server-nx/include/MtpStorage.h new file mode 100644 index 0000000..7b4de9b --- /dev/null +++ b/src/ThirdParty/mtp-server-nx/include/MtpStorage.h @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef _MTP_STORAGE_H +#define _MTP_STORAGE_H + +#include "MtpTypes.h" +#include "mtp.h" + +namespace android { + +class MtpDatabase; + +class MtpStorage { + +private: + MtpStorageID mStorageID; + MtpString mFilePath; + MtpString mDescription; + uint64_t mMaxCapacity; + uint64_t mCachedFreeSpace; + long long mCachedFreeSpaceMs; + uint64_t mMaxFileSize; + // amount of free space to leave unallocated + uint64_t mReserveSpace; + bool mRemovable; + +public: + MtpStorage(MtpStorageID id, const char* filePath, + const char* description, uint64_t reserveSpace, + bool removable, uint64_t maxFileSize); + virtual ~MtpStorage(); + + inline MtpStorageID getStorageID() const { return mStorageID; } + int getType() const; + int getFileSystemType() const; + int getAccessCapability() const; + uint64_t getMaxCapacity(); + uint64_t getFreeSpace(); + const char* getDescription() const; + inline const char* getPath() const { return mFilePath.c_str(); } + inline bool isRemovable() const { return mRemovable; } + inline uint64_t getMaxFileSize() const { return mMaxFileSize; } +}; + +}; // namespace android + +#endif // _MTP_STORAGE_H diff --git a/src/ThirdParty/mtp-server-nx/include/MtpStorageInfo.h b/src/ThirdParty/mtp-server-nx/include/MtpStorageInfo.h new file mode 100644 index 0000000..2cb626e --- /dev/null +++ b/src/ThirdParty/mtp-server-nx/include/MtpStorageInfo.h @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef _MTP_STORAGE_INFO_H +#define _MTP_STORAGE_INFO_H + +#include "MtpTypes.h" + +namespace android { + +class MtpDataPacket; + +class MtpStorageInfo { +public: + MtpStorageID mStorageID; + uint16_t mStorageType; + uint16_t mFileSystemType; + uint16_t mAccessCapability; + uint64_t mMaxCapacity; + uint64_t mFreeSpaceBytes; + uint32_t mFreeSpaceObjects; + char* mStorageDescription; + char* mVolumeIdentifier; + +public: + MtpStorageInfo(MtpStorageID id); + virtual ~MtpStorageInfo(); + + void read(MtpDataPacket& packet); + + void print(); +}; + +}; // namespace android + +#endif // _MTP_STORAGE_INFO_H diff --git a/src/ThirdParty/mtp-server-nx/include/MtpStringBuffer.h b/src/ThirdParty/mtp-server-nx/include/MtpStringBuffer.h new file mode 100644 index 0000000..cbc8307 --- /dev/null +++ b/src/ThirdParty/mtp-server-nx/include/MtpStringBuffer.h @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef _MTP_STRING_BUFFER_H +#define _MTP_STRING_BUFFER_H + +#include + +namespace android { + +class MtpDataPacket; + +// Represents a utf8 string, with a maximum of 255 characters +class MtpStringBuffer { + +private: + // mBuffer contains string in UTF8 format + // maximum 3 bytes/character, with 1 extra for zero termination + uint8_t mBuffer[255 * 3 + 1]; + int mCharCount; + int mByteCount; + +public: + MtpStringBuffer(); + MtpStringBuffer(const char* src); + MtpStringBuffer(const uint16_t* src); + MtpStringBuffer(const MtpStringBuffer& src); + virtual ~MtpStringBuffer(); + + void set(const char* src); + void set(const uint16_t* src); + + void readFromPacket(MtpDataPacket* packet); + void writeToPacket(MtpDataPacket* packet) const; + + inline int getCharCount() const { return mCharCount; } + inline int getByteCount() const { return mByteCount; } + + inline operator const char*() const { return (const char *)mBuffer; } +}; + +}; // namespace android + +#endif // _MTP_STRING_BUFFER_H diff --git a/src/ThirdParty/mtp-server-nx/include/MtpTypes.h b/src/ThirdParty/mtp-server-nx/include/MtpTypes.h new file mode 100644 index 0000000..abce3fa --- /dev/null +++ b/src/ThirdParty/mtp-server-nx/include/MtpTypes.h @@ -0,0 +1,88 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef _MTP_TYPES_H +#define _MTP_TYPES_H + +#include +#include + +#include +#include +#include +#include + +namespace android { + +typedef int32_t int128_t[4]; +typedef uint32_t uint128_t[4]; + +typedef uint16_t MtpOperationCode; +typedef uint16_t MtpResponseCode; +typedef uint16_t MtpEventCode; +typedef uint32_t MtpSessionID; +typedef uint32_t MtpStorageID; +typedef uint32_t MtpTransactionID; +typedef uint16_t MtpPropertyCode; +typedef uint16_t MtpDataType; +typedef uint16_t MtpObjectFormat; +typedef MtpPropertyCode MtpDeviceProperty; +typedef MtpPropertyCode MtpObjectProperty; + +// object handles are unique across all storage but only within a single session. +// object handles cannot be reused after an object is deleted. +// values 0x00000000 and 0xFFFFFFFF are reserved for special purposes. +typedef uint32_t MtpObjectHandle; + +// Special values +#define MTP_PARENT_ROOT 0xFFFFFFFF // parent is root of the storage +#define kInvalidObjectHandle 0xFFFFFFFF + +class MtpStorage; +class MtpDevice; +class MtpProperty; + +template +using Vector = std::vector; + +typedef std::vector MtpStorageList; +typedef std::vector MtpDeviceList; +typedef std::vector MtpPropertyList; + +typedef std::vector UInt8List; +typedef std::vector UInt16List; +typedef std::vector UInt32List; +typedef std::vector UInt64List; +typedef std::vector Int8List; +typedef std::vector Int16List; +typedef std::vector Int32List; +typedef std::vector Int64List; + +typedef UInt16List MtpObjectPropertyList; +typedef UInt16List MtpDevicePropertyList; +typedef UInt16List MtpObjectFormatList; +typedef UInt32List MtpObjectHandleList; +typedef UInt16List MtpObjectPropertyList; +typedef UInt32List MtpStorageIDList; + +typedef std::string MtpString; + +typedef std::mutex MtpMutex; +typedef std::lock_guard MtpAutolock; + +}; // namespace android + +#endif // _MTP_TYPES_H diff --git a/src/ThirdParty/mtp-server-nx/include/MtpUtils.h b/src/ThirdParty/mtp-server-nx/include/MtpUtils.h new file mode 100644 index 0000000..61f9055 --- /dev/null +++ b/src/ThirdParty/mtp-server-nx/include/MtpUtils.h @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef _MTP_UTILS_H +#define _MTP_UTILS_H + +#include + +namespace android { + +bool parseDateTime(const char* dateTime, time_t& outSeconds); +void formatDateTime(time_t seconds, char* buffer, int bufferLength); + +}; // namespace android + +#endif // _MTP_UTILS_H diff --git a/src/ThirdParty/mtp-server-nx/include/SwitchMtpDatabase.h b/src/ThirdParty/mtp-server-nx/include/SwitchMtpDatabase.h new file mode 100644 index 0000000..42b74ac --- /dev/null +++ b/src/ThirdParty/mtp-server-nx/include/SwitchMtpDatabase.h @@ -0,0 +1,1365 @@ +/* + * Copyright (C) 2013 Canonical Ltd. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3, as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef STUB_MTP_DATABASE_H_ +#define STUB_MTP_DATABASE_H_ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "mtp.h" +#include "MtpDatabase.h" +#include "MtpDataPacket.h" +#include "MtpStringBuffer.h" +#include "MtpObjectInfo.h" +#include "MtpProperty.h" +#include "MtpDebug.h" + +#include "log.h" + +#define ALL_PROPERTIES 0xffffffff + +using namespace std::filesystem; + +namespace android +{ +class SwitchMtpDatabase : public android::MtpDatabase { +private: + struct DbEntry + { + MtpStorageID storage_id; + MtpObjectFormat object_format; + MtpObjectHandle parent; + size_t object_size; + std::string display_name; + std::string path; + std::time_t last_modified; + bool scanned = false; + bool pending = false; + }; + + MtpServer* local_server; + bool show_debug_mtp_files; + uint32_t counter; + std::string root_path; + std::map db; + std::unordered_map> children_by_parent; + std::map formats = { + {".gif", MTP_FORMAT_GIF}, + {".png", MTP_FORMAT_PNG}, + {".jpeg", MTP_FORMAT_JFIF}, + {".tiff", MTP_FORMAT_TIFF}, + {".ogg", MTP_FORMAT_OGG}, + {".mp3", MTP_FORMAT_MP3}, + {".wav", MTP_FORMAT_WAV}, + {".wma", MTP_FORMAT_WMA}, + {".aac", MTP_FORMAT_AAC}, + {".flac", MTP_FORMAT_FLAC} + }; + + MtpObjectFormat guess_object_format(std::string extension) + { + std::map::iterator it; + + it = formats.find(extension); + if (it == formats.end()) { + std::transform(extension.begin(), extension.end(), extension.begin(), ::toupper); + it = formats.find(extension); + if (it == formats.end()) { + return MTP_FORMAT_UNDEFINED; + } + } + + return it->second; + } + + static void eraseChildRef(std::vector& vec, MtpObjectHandle handle) + { + vec.erase(std::remove(vec.begin(), vec.end(), handle), vec.end()); + } + + static bool isObjectPropertySupported(MtpObjectProperty property) + { + switch (property) { + case MTP_PROPERTY_STORAGE_ID: + case MTP_PROPERTY_PARENT_OBJECT: + case MTP_PROPERTY_OBJECT_FORMAT: + case MTP_PROPERTY_OBJECT_SIZE: + case MTP_PROPERTY_OBJECT_FILE_NAME: + case MTP_PROPERTY_DISPLAY_NAME: + case MTP_PROPERTY_PERSISTENT_UID: + case MTP_PROPERTY_ASSOCIATION_TYPE: + case MTP_PROPERTY_DATE_MODIFIED: + case MTP_PROPERTY_ASSOCIATION_DESC: + case MTP_PROPERTY_PROTECTION_STATUS: + case MTP_PROPERTY_DATE_CREATED: + case MTP_PROPERTY_HIDDEN: + case MTP_PROPERTY_NON_CONSUMABLE: + return true; + default: + return false; + } + } + + static bool isDebugOnlyMtpFile(const path& p) + { + const std::string name = p.filename().string(); + return name == ".smm_title_id" || name == "mods_status_cache.txt"; + } + + bool shouldExposePath(const path& p) const + { + return show_debug_mtp_files || !isDebugOnlyMtpFile(p); + } + + static bool isSafeObjectName(const std::string& name) + { + return !name.empty() + && name != "." + && name != ".." + && name.find('/') == std::string::npos + && name.find('\\') == std::string::npos + && name.find(':') == std::string::npos; + } + + static std::string normalizePathString(const path& p) + { + std::string value = p.string(); + while (value.size() > 1 && value.back() == '/') { + value.pop_back(); + } + return value; + } + + static bool pathExistsSafe(const path& p) + { + std::error_code ec; + const bool ok = exists(p, ec); + return !ec && ok; + } + + static bool isDirectorySafe(const path& p) + { + std::error_code ec; + const bool ok = is_directory(p, ec); + return !ec && ok; + } + + static size_t fileSizeSafe(const path& p) + { + std::error_code ec; + const auto size = file_size(p, ec); + if (ec) + return 0; + return static_cast(size); + } + + static std::time_t lastModifiedSafe(const path& p, std::time_t fallback = 0) + { + struct stat result {}; + if (stat(p.string().c_str(), &result) == 0) + return result.st_mtime; + return fallback; + } + + MtpObjectHandle findChildByName(MtpObjectHandle parent, const std::string& name) const + { + auto pit = children_by_parent.find(parent); + if (pit == children_by_parent.end()) + return kInvalidObjectHandle; + + for (auto handle : pit->second) { + auto dit = db.find(handle); + if (dit != db.end() && dit->second.display_name == name) + return handle; + } + return kInvalidObjectHandle; + } + + MtpObjectHandle findByPath(const std::string& value) const + { + for (const auto& item : db) { + if (normalizePathString(path(item.second.path)) == normalizePathString(path(value))) + return item.first; + } + return kInvalidObjectHandle; + } + + void insertEntry(MtpObjectHandle handle, const DbEntry& entry) + { + db[handle] = entry; + auto& children = children_by_parent[entry.parent]; + if (std::find(children.begin(), children.end(), handle) == children.end()) + children.push_back(handle); + } + + void moveEntryParent(MtpObjectHandle handle, MtpObjectHandle new_parent) + { + auto it = db.find(handle); + if (it == db.end()) + return; + MtpObjectHandle old_parent = it->second.parent; + if (old_parent == new_parent) + return; + auto vit = children_by_parent.find(old_parent); + if (vit != children_by_parent.end()) + eraseChildRef(vit->second, handle); + it->second.parent = new_parent; + children_by_parent[new_parent].push_back(handle); + } + + void eraseEntryRecursive(MtpObjectHandle handle) + { + auto it = db.find(handle); + if (it == db.end()) + return; + + auto childIt = children_by_parent.find(handle); + if (childIt != children_by_parent.end()) { + auto children = childIt->second; + for (auto child : children) { + eraseEntryRecursive(child); + } + children_by_parent.erase(handle); + } + + auto parentIt = children_by_parent.find(it->second.parent); + if (parentIt != children_by_parent.end()) { + eraseChildRef(parentIt->second, handle); + } + db.erase(it); + } + + void updateDescendantPaths(MtpObjectHandle handle, const path& oldRoot, const path& newRoot) + { + const std::string oldRootStr = normalizePathString(oldRoot); + const std::string newRootStr = normalizePathString(newRoot); + auto childIt = children_by_parent.find(handle); + if (childIt == children_by_parent.end()) + return; + + auto children = childIt->second; + for (auto child : children) { + auto dit = db.find(child); + if (dit == db.end()) + continue; + + std::string childPath = normalizePathString(path(dit->second.path)); + if (childPath == oldRootStr) { + dit->second.path = newRootStr; + } else if (childPath.size() > oldRootStr.size() + && childPath.compare(0, oldRootStr.size(), oldRootStr) == 0 + && childPath[oldRootStr.size()] == '/') { + dit->second.path = newRootStr + childPath.substr(oldRootStr.size()); + } + updateDescendantPaths(child, oldRoot, newRoot); + } + } + + MtpObjectHandle add_file_entry(path p, MtpObjectHandle parent, MtpStorageID storage) + { + if (!shouldExposePath(p) || !pathExistsSafe(p)) + return kInvalidObjectHandle; + + try { + const std::string displayName = p.filename().string(); + if (!isSafeObjectName(displayName)) + return kInvalidObjectHandle; + + MtpObjectHandle handle = findChildByName(parent, displayName); + if (handle == kInvalidObjectHandle) + handle = findByPath(p.string()); + + const bool isNewEntry = (handle == kInvalidObjectHandle); + DbEntry entry; + bool wasScanned = false; + bool wasPending = false; + if (!isNewEntry) { + entry = db.at(handle); + wasScanned = entry.scanned; + wasPending = entry.pending; + } else { + handle = counter++; + } + + entry.storage_id = storage; + entry.parent = parent; + entry.display_name = displayName; + entry.path = normalizePathString(p); + entry.pending = wasPending; + if (!wasPending) + entry.last_modified = lastModifiedSafe(p, entry.last_modified); + if (isDirectorySafe(p)) { + entry.object_format = MTP_FORMAT_ASSOCIATION; + entry.object_size = 0; + entry.scanned = wasScanned; + } else { + entry.object_format = MTP_FORMAT_UNDEFINED; + if (!wasPending) + entry.object_size = fileSizeSafe(p); + entry.scanned = false; + } + + if (!isNewEntry && db.at(handle).parent != parent) + moveEntryParent(handle, parent); + insertEntry(handle, entry); + VLOG(1) << "Adding \"" << p.string() << "\""; + return handle; + } catch (const filesystem_error& ex) { + LOG(ERROR) << ex.what(); + } catch (...) { + LOG(ERROR) << "Unexpected error while adding MTP file entry"; + } + return kInvalidObjectHandle; + } + + void parse_directory(path p, MtpObjectHandle parent, MtpStorageID storage) + { + std::vector seen; + bool scanComplete = true; + + if(!isDirectorySafe(p)) { + if (pathExistsSafe(p)) + add_file_entry(p, parent, storage); + else if (parent != 0 && db.find(parent) != db.end()) + eraseEntryRecursive(parent); + if (db.find(parent) != db.end()) + db.at(parent).scanned = true; + return; + } + + try { + std::error_code ec; + directory_iterator it(p, directory_options::skip_permission_denied, ec); + directory_iterator end; + while (!ec && it != end) { + const path current = it->path(); + if (shouldExposePath(current)) { + MtpObjectHandle child = add_file_entry(current, parent, storage); + if (child != kInvalidObjectHandle) + seen.push_back(child); + } + it.increment(ec); + } + if (ec) { + scanComplete = false; + LOG(WARNING) << "MTP directory scan interrupted for " << p.string(); + } + } catch (const filesystem_error& ex) { + scanComplete = false; + LOG(WARNING) << ex.what(); + } catch (...) { + scanComplete = false; + LOG(WARNING) << "Unexpected error while scanning MTP directory"; + } + + auto childIt = children_by_parent.find(parent); + if (scanComplete && childIt != children_by_parent.end()) { + auto children = childIt->second; + for (auto child : children) { + auto dit = db.find(child); + if (dit == db.end()) + continue; + if (dit->second.pending) + continue; + if (std::find(seen.begin(), seen.end(), child) == seen.end()) + eraseEntryRecursive(child); + } + } + + if (db.find(parent) != db.end()) + db.at(parent).scanned = true; + } + + void readFiles(const std::string& sourcedir, const std::string& display, MtpStorageID storage, bool hidden) + { + path p (sourcedir); + DbEntry entry; + MtpObjectHandle handle = counter++; + std::string display_name = std::string(p.filename().string()); + + if (!display.empty()) + display_name = display; + + try { + if (pathExistsSafe(p)) { + if (isDirectorySafe(p)) { + if (hidden) + root_path = normalizePathString(p); + entry.storage_id = storage; + entry.parent = hidden ? MTP_PARENT_ROOT : 0; + entry.display_name = display_name; + entry.path = normalizePathString(p); + entry.object_format = MTP_FORMAT_ASSOCIATION; + entry.object_size = 0; + entry.last_modified = lastModifiedSafe(p); + + insertEntry(handle, entry); + + parse_directory (p, hidden ? 0 : handle, storage); + } else + LOG(WARNING) << p << " is not a directory."; + } else { + if (storage == MTP_STORAGE_FIXED_RAM) + LOG(WARNING) << p << " does not exist."; + else { + entry.storage_id = storage; + entry.parent = -1; + entry.display_name = display_name; + entry.path = p.parent_path().string(); + entry.object_format = MTP_FORMAT_ASSOCIATION; + entry.object_size = 0; + entry.last_modified = 0; + } + } + } + catch (const filesystem_error& ex) { + LOG(ERROR) << ex.what(); + } + } + +public: + + explicit SwitchMtpDatabase(bool showDebugMtpFiles = false) : + show_debug_mtp_files(showDebugMtpFiles), + counter(1) + { + local_server = nullptr; + db = std::map(); + root_path = "sdmc:/mods"; + } + + virtual ~SwitchMtpDatabase() { + } + + virtual bool isHandleValid(MtpObjectHandle handle) { + return db.find(handle) != db.end(); + } + + virtual void addStoragePath(const MtpString& path, + const MtpString& displayName, + MtpStorageID storage, + bool hidden) + { + readFiles(path, displayName, storage, hidden); + } + + virtual void removeStorage(MtpStorageID storage) + { + // remove all database entries corresponding to said storage. + std::vector toErase; + for(std::map::iterator it = db.begin(); it != db.end(); ++it) { + if (it->second.storage_id == storage) + toErase.push_back(it->first); + } + for (auto handle : toErase) + eraseEntryRecursive(handle); + } + + // called from SendObjectInfo to reserve a database entry for the incoming file + virtual MtpObjectHandle beginSendObject( + const MtpString& path, + MtpObjectFormat format, + MtpObjectHandle parent, + MtpStorageID storage, + uint64_t size, + time_t modified) + { + DbEntry entry; + MtpObjectHandle handle = kInvalidObjectHandle; + + if (storage == MTP_STORAGE_FIXED_RAM && parent == 0) + return kInvalidObjectHandle; + if (!shouldExposePath(std::filesystem::path(path))) + return kInvalidObjectHandle; + if (parent != 0 && parent != MTP_PARENT_ROOT && db.find(parent) == db.end()) + return kInvalidObjectHandle; + + VLOG(1) << __PRETTY_FUNCTION__ << ": " << path << " - " << parent + << " format: " << std::hex << format << std::dec; + + const std::filesystem::path incomingPath(path); + const std::string displayName = incomingPath.filename().string(); + if (!isSafeObjectName(displayName)) + return kInvalidObjectHandle; + + if (parent == MTP_PARENT_ROOT) + parent = 0; + + handle = findChildByName(parent, displayName); + if (handle == kInvalidObjectHandle) + handle = findByPath(incomingPath.string()); + + const bool isNewEntry = (handle == kInvalidObjectHandle); + bool wasScanned = false; + if (!isNewEntry) { + entry = db.at(handle); + wasScanned = entry.scanned; + } else { + handle = counter++; + } + + entry.storage_id = storage; + entry.parent = parent; + entry.display_name = displayName; + entry.path = normalizePathString(incomingPath); + entry.object_format = (format == MTP_FORMAT_ASSOCIATION) ? format : MTP_FORMAT_UNDEFINED; + entry.object_size = size; + entry.last_modified = modified; + entry.pending = true; + entry.scanned = (format == MTP_FORMAT_ASSOCIATION) ? wasScanned : false; + + if (!isNewEntry && db.at(handle).parent != parent) + moveEntryParent(handle, parent); + insertEntry(handle, entry); + + return handle; + } + + virtual void updateObjectSize(MtpObjectHandle handle, uint64_t size) + { + auto it = db.find(handle); + if (it != db.end()) + it->second.object_size = static_cast(size); + } + + // called to report success or failure of the SendObject file transfer + // success should signal a notification of the new object's creation, + // failure should remove the database entry created in beginSendObject + virtual void endSendObject( + const MtpString& path, + MtpObjectHandle handle, + MtpObjectFormat format, + bool succeeded) + { + VLOG(1) << __PRETTY_FUNCTION__ << ": " << path; + + try + { + if (!succeeded) { + eraseEntryRecursive(handle); + } else { + std::filesystem::path p (path); + auto it = db.find(handle); + if (it == db.end()) + return; + + it->second.pending = false; + it->second.path = normalizePathString(p); + it->second.last_modified = lastModifiedSafe(p, it->second.last_modified); + if (format != MTP_FORMAT_ASSOCIATION) { + it->second.object_size = fileSizeSafe(p); + } else { + it->second.object_format = MTP_FORMAT_ASSOCIATION; + it->second.object_size = 0; + } + } + } catch(...) + { + LOG(ERROR) << __PRETTY_FUNCTION__ + << ": failed to complete object creation:" << path; + } + } + + virtual MtpObjectHandleList* getObjectList( + MtpStorageID storageID, + MtpObjectFormat format, + MtpObjectHandle parent) + { + VLOG(1) << __PRETTY_FUNCTION__ << ": " << storageID << ", " << format << ", " << parent; + MtpObjectHandleList* list = nullptr; + + if (parent == MTP_PARENT_ROOT) + parent = 0; + + if (parent == 0 && !root_path.empty()) + parse_directory(root_path, parent, storageID); + // Scan unscanned directories + else if (isHandleValid(parent) && !db.at(parent).scanned) + parse_directory (db.at(parent).path, parent, storageID); + + try + { + std::vector keys; + + auto pit = children_by_parent.find(parent); + if (pit != children_by_parent.end()) { + const auto& children = pit->second; + for (auto h : children) { + auto dit = db.find(h); + if (dit == db.end()) + continue; + if (dit->second.storage_id != storageID) + continue; + if (format == 0 || dit->second.object_format == format) + keys.push_back(h); + } + } + + list = new MtpObjectHandleList(keys); + } catch(...) + { + list = new MtpObjectHandleList(); + } + + return list; + } + + virtual int getNumObjects( + MtpStorageID storageID, + MtpObjectFormat format, + MtpObjectHandle parent) + { + VLOG(1) << __PRETTY_FUNCTION__ << ": " << storageID << ", " << format << ", " << parent; + + int result = 0; + + try + { + MtpObjectHandleList *list = getObjectList(storageID, format, parent); + result = list->size(); + delete list; + } catch(...) + { + } + + return result; + } + + // callee should delete[] the results from these + // results can be NULL + virtual MtpObjectFormatList* getSupportedPlaybackFormats() + { + VLOG(1) << __PRETTY_FUNCTION__; + static const MtpObjectFormatList list = { + MTP_FORMAT_UNDEFINED, + MTP_FORMAT_ASSOCIATION, // folders + }; + + return new MtpObjectFormatList{list}; + } + + virtual MtpObjectFormatList* getSupportedCaptureFormats() + { + VLOG(1) << __PRETTY_FUNCTION__; + static const MtpObjectFormatList list = {MTP_FORMAT_UNDEFINED, MTP_FORMAT_ASSOCIATION}; + return new MtpObjectFormatList{list}; + } + + virtual MtpObjectPropertyList* getSupportedObjectProperties(MtpObjectFormat format) + { + VLOG(1) << __PRETTY_FUNCTION__; + /* + if (format != MTP_FORMAT_PNG) + return nullptr; + */ + + static const MtpObjectPropertyList list = + { + MTP_PROPERTY_STORAGE_ID, + MTP_PROPERTY_PARENT_OBJECT, + MTP_PROPERTY_OBJECT_FORMAT, + MTP_PROPERTY_OBJECT_SIZE, + MTP_PROPERTY_OBJECT_FILE_NAME, + MTP_PROPERTY_DISPLAY_NAME, + MTP_PROPERTY_PERSISTENT_UID, + MTP_PROPERTY_ASSOCIATION_TYPE, + MTP_PROPERTY_DATE_MODIFIED, + + }; + + return new MtpObjectPropertyList{list}; + } + + virtual MtpDevicePropertyList* getSupportedDeviceProperties() + { + VLOG(1) << __PRETTY_FUNCTION__; + static const MtpDevicePropertyList list = { + MTP_DEVICE_PROPERTY_DEVICE_FRIENDLY_NAME, + MTP_DEVICE_PROPERTY_SYNCHRONIZATION_PARTNER, + }; + return new MtpDevicePropertyList{list}; + } + + virtual MtpResponseCode getObjectPropertyValue( + MtpObjectHandle handle, + MtpObjectProperty property, + MtpDataPacket& packet) + { + char date[20]; + + VLOG(1) << __PRETTY_FUNCTION__ + << " handle: " << handle + << " property: " << MtpDebug::getObjectPropCodeName(property); + + if (handle == MTP_PARENT_ROOT || handle == 0) + return MTP_RESPONSE_INVALID_OBJECT_HANDLE; + + if (!isObjectPropertySupported(property)) + return MTP_RESPONSE_OBJECT_PROP_NOT_SUPPORTED; + + try { + auto it = db.find(handle); + if (it == db.end()) + return MTP_RESPONSE_INVALID_OBJECT_HANDLE; + const DbEntry& entry = it->second; + + switch(property) + { + case MTP_PROPERTY_STORAGE_ID: packet.putUInt32(entry.storage_id); break; + case MTP_PROPERTY_PARENT_OBJECT: packet.putUInt32(entry.parent); break; + case MTP_PROPERTY_OBJECT_FORMAT: packet.putUInt16(entry.object_format); break; + case MTP_PROPERTY_OBJECT_SIZE: packet.putUInt32(entry.object_size); break; + case MTP_PROPERTY_DISPLAY_NAME: packet.putString(entry.display_name.c_str()); break; + case MTP_PROPERTY_OBJECT_FILE_NAME: packet.putString(entry.display_name.c_str()); break; + case MTP_PROPERTY_PERSISTENT_UID: packet.putUInt128(handle); break; + case MTP_PROPERTY_ASSOCIATION_TYPE: + if (entry.object_format == MTP_FORMAT_ASSOCIATION) + packet.putUInt16(MTP_ASSOCIATION_TYPE_GENERIC_FOLDER); + else + packet.putUInt16(0); + break; + case MTP_PROPERTY_ASSOCIATION_DESC: packet.putUInt32(0); break; + case MTP_PROPERTY_PROTECTION_STATUS: + packet.putUInt16(0x0000); // no files are read-only for now. + break; + case MTP_PROPERTY_DATE_CREATED: + formatDateTime(0, date, sizeof(date)); + packet.putString(date); + break; + case MTP_PROPERTY_DATE_MODIFIED: + formatDateTime(entry.last_modified, date, sizeof(date)); + packet.putString(date); + break; + case MTP_PROPERTY_HIDDEN: packet.putUInt16(0); break; + case MTP_PROPERTY_NON_CONSUMABLE: + if (entry.object_format == MTP_FORMAT_ASSOCIATION) + packet.putUInt16(0); // folders are non-consumable + else + packet.putUInt16(1); // files can usually be played. + break; + default: return MTP_RESPONSE_OBJECT_PROP_NOT_SUPPORTED; break; + } + + return MTP_RESPONSE_OK; + } + catch (...) { + LOG(ERROR) << __PRETTY_FUNCTION__ + << "Could not retrieve property: " + << MtpDebug::getObjectPropCodeName(property) + << " for handle: " << handle; + return MTP_RESPONSE_GENERAL_ERROR; + } + } + + virtual MtpResponseCode setObjectPropertyValue( + MtpObjectHandle handle, + MtpObjectProperty property, + MtpDataPacket& packet) + { + MtpStringBuffer buffer; + + VLOG(1) << __PRETTY_FUNCTION__ + << " handle: " << handle + << " property: " << MtpDebug::getObjectPropCodeName(property); + + if (handle == MTP_PARENT_ROOT || handle == 0) + return MTP_RESPONSE_INVALID_OBJECT_HANDLE; + + switch(property) + { + case MTP_PROPERTY_OBJECT_FILE_NAME: + try { + auto it = db.find(handle); + if (it == db.end()) + return MTP_RESPONSE_INVALID_OBJECT_HANDLE; + + packet.getString(buffer); + const std::string newname = static_cast(buffer); + if (!isSafeObjectName(newname)) + return MTP_RESPONSE_INVALID_OBJECT_PROP_VALUE; + + const path oldpath = it->second.path; + const path newpath = oldpath.parent_path() / newname; + if (normalizePathString(oldpath) == normalizePathString(newpath)) { + it->second.display_name = newname; + return MTP_RESPONSE_OK; + } + + MtpObjectHandle existing = findChildByName(it->second.parent, newname); + if (existing != kInvalidObjectHandle && existing != handle) + return MTP_RESPONSE_DEVICE_BUSY; + + std::error_code ec; + if (exists(newpath, ec) && existing != handle) + return MTP_RESPONSE_DEVICE_BUSY; + ec.clear(); + rename(oldpath, newpath, ec); + if (ec) { + LOG(ERROR) << "MTP rename failed: " << oldpath.string() + << " -> " << newpath.string() + << " (" << ec.message() << ")"; + return MTP_RESPONSE_DEVICE_BUSY; + } + + const bool isDir = it->second.object_format == MTP_FORMAT_ASSOCIATION; + it->second.display_name = newname; + it->second.path = normalizePathString(newpath); + it->second.last_modified = lastModifiedSafe(newpath, it->second.last_modified); + if (isDir) + updateDescendantPaths(handle, oldpath, newpath); + } catch (filesystem_error& fe) { + LOG(ERROR) << fe.what(); + return MTP_RESPONSE_DEVICE_BUSY; + } catch (std::exception& e) { + LOG(ERROR) << e.what(); + return MTP_RESPONSE_GENERAL_ERROR; + } catch (...) { + LOG(ERROR) << "An unexpected error has occurred"; + return MTP_RESPONSE_GENERAL_ERROR; + } + + break; + case MTP_PROPERTY_PARENT_OBJECT: + try { + MtpObjectHandle newParent = packet.getUInt32(); + return moveFile(handle, newParent); + } + catch (...) { + LOG(ERROR) << "Could not change parent object for handle " + << handle; + return MTP_RESPONSE_GENERAL_ERROR; + } + break; + default: return MTP_RESPONSE_OPERATION_NOT_SUPPORTED; break; + } + + return MTP_RESPONSE_OK; + } + + virtual MtpResponseCode getDevicePropertyValue( + MtpDeviceProperty property, + MtpDataPacket& packet) + { + VLOG(1) << __PRETTY_FUNCTION__; + switch(property) + { + case MTP_DEVICE_PROPERTY_SYNCHRONIZATION_PARTNER: + packet.putString(""); + break; + case MTP_DEVICE_PROPERTY_DEVICE_FRIENDLY_NAME: + packet.putString("Simple Mod Manager MTP"); + break; + default: return MTP_RESPONSE_OPERATION_NOT_SUPPORTED; break; + } + + return MTP_RESPONSE_OK; + } + + virtual MtpResponseCode setDevicePropertyValue( + MtpDeviceProperty property, + MtpDataPacket& packet) + { + VLOG(1) << __PRETTY_FUNCTION__; + return MTP_RESPONSE_DEVICE_PROP_NOT_SUPPORTED; + } + + virtual MtpResponseCode resetDeviceProperty( + MtpDeviceProperty property) + { + VLOG(1) << __PRETTY_FUNCTION__; + return MTP_RESPONSE_DEVICE_PROP_NOT_SUPPORTED; + } + + virtual MtpResponseCode getObjectPropertyList( + MtpObjectHandle handle, + uint32_t format, + uint32_t property, + int groupCode, + int depth, + MtpDataPacket& packet) + { + std::vector handles; + + VLOG(2) << __PRETTY_FUNCTION__; + + if (handle == kInvalidObjectHandle) + return MTP_RESPONSE_PARAMETER_NOT_SUPPORTED; + + if (property == 0 && groupCode == 0) + return MTP_RESPONSE_PARAMETER_NOT_SUPPORTED; + + if (groupCode != 0) + return MTP_RESPONSE_SPECIFICATION_BY_GROUP_UNSUPPORTED; + + if (depth > 1) + return MTP_RESPONSE_SPECIFICATION_BY_DEPTH_UNSUPPORTED; + + if (property != ALL_PROPERTIES + && !isObjectPropertySupported(static_cast(property))) { + return MTP_RESPONSE_OBJECT_PROP_NOT_SUPPORTED; + } + + if (depth == 0) { + /* For a depth search, a handle of 0 is valid (objects at the root) + * but it isn't when querying for the properties of a single object. + */ + if (db.find(handle) == db.end()) + return MTP_RESPONSE_INVALID_OBJECT_HANDLE; + + handles.push_back(handle); + } else { + auto pit = children_by_parent.find(handle); + if (pit != children_by_parent.end()) { + handles.reserve(pit->second.size()); + for (auto child : pit->second) { + auto dit = db.find(child); + if (dit == db.end()) + continue; + if (format == 0 || dit->second.object_format == format) + handles.push_back(child); + } + } + } + + /* + * getObjectPropList returns an ObjectPropList dataset table; + * built as such: + * + * 1- Number of elements (quadruples) + * a1- Element 1 Object Handle + * a2- Element 1 Property Code + * a3- Element 1 Data type + * a4- Element 1 Value + * b... rinse, repeat. + */ + + static constexpr uint32_t kAllPropertyCount = 9; + if (property == ALL_PROPERTIES) + packet.putUInt32(kAllPropertyCount * handles.size()); + else + packet.putUInt32(1 * handles.size()); + + for(std::vector::iterator it = handles.begin(); it != handles.end(); ++it) { + MtpObjectHandle i = *it; + const DbEntry& entry = db.at(i); + + // Persistent Unique Identifier. + if (property == ALL_PROPERTIES || property == MTP_PROPERTY_PERSISTENT_UID) { + packet.putUInt32(i); + packet.putUInt16(MTP_PROPERTY_PERSISTENT_UID); + packet.putUInt16(MTP_TYPE_UINT128); + packet.putUInt128(i); + } + + // Storage ID + if (property == ALL_PROPERTIES || property == MTP_PROPERTY_STORAGE_ID) { + packet.putUInt32(i); + packet.putUInt16(MTP_PROPERTY_STORAGE_ID); + packet.putUInt16(MTP_TYPE_UINT32); + packet.putUInt32(entry.storage_id); + } + + // Parent + if (property == ALL_PROPERTIES || property == MTP_PROPERTY_PARENT_OBJECT) { + packet.putUInt32(i); + packet.putUInt16(MTP_PROPERTY_PARENT_OBJECT); + packet.putUInt16(MTP_TYPE_UINT32); + packet.putUInt32(entry.parent); + } + + // Object Format + if (property == ALL_PROPERTIES || property == MTP_PROPERTY_OBJECT_FORMAT) { + packet.putUInt32(i); + packet.putUInt16(MTP_PROPERTY_OBJECT_FORMAT); + packet.putUInt16(MTP_TYPE_UINT16); + packet.putUInt16(entry.object_format); + } + + // Object Size + if (property == ALL_PROPERTIES || property == MTP_PROPERTY_OBJECT_SIZE) { + packet.putUInt32(i); + packet.putUInt16(MTP_PROPERTY_OBJECT_SIZE); + packet.putUInt16(MTP_TYPE_UINT32); + packet.putUInt32(entry.object_size); + } + + // Object File Name + if (property == ALL_PROPERTIES || property == MTP_PROPERTY_OBJECT_FILE_NAME) { + packet.putUInt32(i); + packet.putUInt16(MTP_PROPERTY_OBJECT_FILE_NAME); + packet.putUInt16(MTP_TYPE_STR); + packet.putString(entry.display_name.c_str()); + } + + // Display Name + if (property == ALL_PROPERTIES || property == MTP_PROPERTY_DISPLAY_NAME) { + packet.putUInt32(i); + packet.putUInt16(MTP_PROPERTY_DISPLAY_NAME); + packet.putUInt16(MTP_TYPE_STR); + packet.putString(entry.display_name.c_str()); + } + + // Association Type + if (property == ALL_PROPERTIES || property == MTP_PROPERTY_ASSOCIATION_TYPE) { + packet.putUInt32(i); + packet.putUInt16(MTP_PROPERTY_ASSOCIATION_TYPE); + packet.putUInt16(MTP_TYPE_UINT16); + if (entry.object_format == MTP_FORMAT_ASSOCIATION) + packet.putUInt16(MTP_ASSOCIATION_TYPE_GENERIC_FOLDER); + else + packet.putUInt16(0); + } + + // Association Description + if (property == MTP_PROPERTY_ASSOCIATION_DESC) { + packet.putUInt32(i); + packet.putUInt16(MTP_PROPERTY_ASSOCIATION_DESC); + packet.putUInt16(MTP_TYPE_UINT32); + packet.putUInt32(0); + } + + // Protection Status + if (property == MTP_PROPERTY_PROTECTION_STATUS) { + packet.putUInt32(i); + packet.putUInt16(MTP_PROPERTY_PROTECTION_STATUS); + packet.putUInt16(MTP_TYPE_UINT16); + packet.putUInt16(0x0000); //FIXME: all files are read-write for now + // packet.putUInt16(0x8001); + } + + // Date Created + if (property == MTP_PROPERTY_DATE_CREATED) { + char date[20]; + formatDateTime(0, date, sizeof(date)); + packet.putUInt32(i); + packet.putUInt16(MTP_PROPERTY_DATE_CREATED); + packet.putUInt16(MTP_TYPE_STR); + packet.putString(date); + } + + // Date Modified + if (property == ALL_PROPERTIES || property == MTP_PROPERTY_DATE_MODIFIED) { + char date[20]; + formatDateTime(entry.last_modified, date, sizeof(date)); + packet.putUInt32(i); + packet.putUInt16(MTP_PROPERTY_DATE_MODIFIED); + packet.putUInt16(MTP_TYPE_STR); + packet.putString(date); + } + + // Hidden + if (property == MTP_PROPERTY_HIDDEN) { + packet.putUInt32(i); + packet.putUInt16(MTP_PROPERTY_HIDDEN); + packet.putUInt16(MTP_TYPE_UINT16); + packet.putUInt16(0); + } + + // Non Consumable + if (property == MTP_PROPERTY_NON_CONSUMABLE) { + packet.putUInt32(i); + packet.putUInt16(MTP_PROPERTY_NON_CONSUMABLE); + packet.putUInt16(MTP_TYPE_UINT16); + if (entry.object_format == MTP_FORMAT_ASSOCIATION) + packet.putUInt16(0); // folders are non-consumable + else + packet.putUInt16(1); // files can usually be played. + } + + } + + return MTP_RESPONSE_OK; + } + + virtual MtpResponseCode getObjectInfo( + MtpObjectHandle handle, + MtpObjectInfo& info) + { + VLOG(2) << __PRETTY_FUNCTION__; + + if (handle == 0 || handle == MTP_PARENT_ROOT) + return MTP_RESPONSE_INVALID_OBJECT_HANDLE; + + try { + auto it = db.find(handle); + if (it == db.end()) + return MTP_RESPONSE_INVALID_OBJECT_HANDLE; + const DbEntry& entry = it->second; + + info.mHandle = handle; + info.mStorageID = entry.storage_id; + info.mFormat = entry.object_format; + info.mProtectionStatus = 0x0; + info.mCompressedSize = entry.object_size; + info.mImagePixWidth = 0; + info.mImagePixHeight = 0; + info.mImagePixDepth = 0; + info.mParent = entry.parent; + info.mAssociationType + = info.mFormat == MTP_FORMAT_ASSOCIATION + ? MTP_ASSOCIATION_TYPE_GENERIC_FOLDER : 0; + info.mAssociationDesc = 0; + info.mSequenceNumber = 0; + info.mName = ::strdup(entry.display_name.c_str()); + info.mDateCreated = 0; + info.mDateModified = entry.last_modified; + info.mKeywords = ::strdup("ubuntu,touch"); + + if (VLOG_IS_ON(2)) + info.print(); + + return MTP_RESPONSE_OK; + } + catch (...) { + return MTP_RESPONSE_GENERAL_ERROR; + } + } + + virtual void* getThumbnail(MtpObjectHandle handle, size_t& outThumbSize) + { + outThumbSize = 0; + return nullptr; + } + + virtual MtpResponseCode getObjectFilePath( + MtpObjectHandle handle, + MtpString& outFilePath, + int64_t& outFileLength, + MtpObjectFormat& outFormat) + { + VLOG(1) << __PRETTY_FUNCTION__ << " handle: " << handle; + + if (handle == 0 || handle == MTP_PARENT_ROOT) + return MTP_RESPONSE_INVALID_OBJECT_HANDLE; + + try { + auto it = db.find(handle); + if (it == db.end()) + return MTP_RESPONSE_INVALID_OBJECT_HANDLE; + const DbEntry& entry = it->second; + + VLOG(2) << __PRETTY_FUNCTION__ + << "handle: " << handle + << "path: " << entry.path + << "length: " << entry.object_size + << "format: " << entry.object_format; + + outFilePath = std::string(entry.path); + outFileLength = entry.object_size; + outFormat = entry.object_format; + + return MTP_RESPONSE_OK; + } + catch (...) { + return MTP_RESPONSE_GENERAL_ERROR; + } + } + + virtual MtpResponseCode deleteFile(MtpObjectHandle handle) + { + VLOG(2) << __PRETTY_FUNCTION__ << " handle: " << handle; + + if (handle == 0 || handle == MTP_PARENT_ROOT) + return MTP_RESPONSE_INVALID_OBJECT_HANDLE; + + try { + if (db.find(handle) != db.end()) { + eraseEntryRecursive(handle); + return MTP_RESPONSE_OK; + } + else + return MTP_RESPONSE_GENERAL_ERROR; + } + catch (...) { + return MTP_RESPONSE_GENERAL_ERROR; + } + } + + virtual MtpResponseCode moveFile(MtpObjectHandle handle, MtpObjectHandle new_parent) + { + VLOG(1) << __PRETTY_FUNCTION__ << " handle: " << handle + << " new parent: " << new_parent; + + if (handle == 0 || handle == MTP_PARENT_ROOT) + return MTP_RESPONSE_INVALID_OBJECT_HANDLE; + + try { + if (new_parent == MTP_PARENT_ROOT) + new_parent = 0; + + auto it = db.find(handle); + if (it == db.end()) + return MTP_RESPONSE_INVALID_OBJECT_HANDLE; + + path parentPath; + if (new_parent == 0) { + parentPath = root_path; + } else { + auto parentIt = db.find(new_parent); + if (parentIt == db.end() + || parentIt->second.object_format != MTP_FORMAT_ASSOCIATION) + return MTP_RESPONSE_INVALID_PARENT_OBJECT; + parentPath = parentIt->second.path; + } + + const path oldPath = it->second.path; + const path newPath = parentPath / it->second.display_name; + const bool isDir = it->second.object_format == MTP_FORMAT_ASSOCIATION; + + if (normalizePathString(oldPath) != normalizePathString(newPath)) { + std::error_code ec; + if (exists(newPath, ec)) + return MTP_RESPONSE_DEVICE_BUSY; + ec.clear(); + rename(oldPath, newPath, ec); + if (ec) { + LOG(ERROR) << "MTP move failed: " << oldPath.string() + << " -> " << newPath.string() + << " (" << ec.message() << ")"; + return MTP_RESPONSE_DEVICE_BUSY; + } + it->second.path = normalizePathString(newPath); + it->second.last_modified = lastModifiedSafe(newPath, it->second.last_modified); + if (isDir) + updateDescendantPaths(handle, oldPath, newPath); + } + + moveEntryParent(handle, new_parent); + } + catch (...) { + return MTP_RESPONSE_INVALID_OBJECT_HANDLE; + } + + return MTP_RESPONSE_OK; + } + + /* + virtual MtpResponseCode copyFile(MtpObjectHandle handle, MtpObjectHandle new_parent) + { + VLOG(2) << __PRETTY_FUNCTION__; + + // duplicate DbEntry + // change parent + + return MTP_RESPONSE_OK + } + */ + + virtual MtpObjectHandleList* getObjectReferences(MtpObjectHandle handle) + { + VLOG(1) << __PRETTY_FUNCTION__; + + if (handle == 0 || handle == MTP_PARENT_ROOT) + return nullptr; + + auto it = db.find(handle); + if (it == db.end()) + return new MtpObjectHandleList(); + + return getObjectList(it->second.storage_id, + it->second.object_format, + handle); + } + + virtual MtpResponseCode setObjectReferences( + MtpObjectHandle handle, + MtpObjectHandleList* references) + { + VLOG(1) << __PRETTY_FUNCTION__; + + // ignore, we don't keep the references in a list. + + return MTP_RESPONSE_OK; + } + + virtual MtpProperty* getObjectPropertyDesc( + MtpObjectProperty property, + MtpObjectFormat format) + { + VLOG(1) << __PRETTY_FUNCTION__ << MtpDebug::getObjectPropCodeName(property); + + MtpProperty* result = nullptr; + switch(property) + { + case MTP_PROPERTY_STORAGE_ID: result = new MtpProperty(property, MTP_TYPE_UINT32, false); break; + case MTP_PROPERTY_PARENT_OBJECT: result = new MtpProperty(property, MTP_TYPE_UINT32, true); break; + case MTP_PROPERTY_OBJECT_FORMAT: result = new MtpProperty(property, MTP_TYPE_UINT16, false); break; + case MTP_PROPERTY_OBJECT_SIZE: result = new MtpProperty(property, MTP_TYPE_UINT32, false); break; + case MTP_PROPERTY_WIDTH: result = new MtpProperty(property, MTP_TYPE_UINT32, false); break; + case MTP_PROPERTY_HEIGHT: result = new MtpProperty(property, MTP_TYPE_UINT32, false); break; + case MTP_PROPERTY_IMAGE_BIT_DEPTH: result = new MtpProperty(property, MTP_TYPE_UINT32, false); break; + case MTP_PROPERTY_DISPLAY_NAME: result = new MtpProperty(property, MTP_TYPE_STR, true); break; + case MTP_PROPERTY_OBJECT_FILE_NAME: result = new MtpProperty(property, MTP_TYPE_STR, true); break; + case MTP_PROPERTY_PERSISTENT_UID: result = new MtpProperty(property, MTP_TYPE_UINT128, false); break; + case MTP_PROPERTY_ASSOCIATION_TYPE: result = new MtpProperty(property, MTP_TYPE_UINT16, false); break; + case MTP_PROPERTY_ASSOCIATION_DESC: result = new MtpProperty(property, MTP_TYPE_UINT32, false); break; + case MTP_PROPERTY_PROTECTION_STATUS: result = new MtpProperty(property, MTP_TYPE_UINT16, false); break; + case MTP_PROPERTY_DATE_CREATED: result = new MtpProperty(property, MTP_TYPE_STR, false); break; + case MTP_PROPERTY_DATE_MODIFIED: result = new MtpProperty(property, MTP_TYPE_STR, false); break; + case MTP_PROPERTY_HIDDEN: result = new MtpProperty(property, MTP_TYPE_UINT16, false); break; + case MTP_PROPERTY_NON_CONSUMABLE: result = new MtpProperty(property, MTP_TYPE_UINT16, false); break; + default: break; + } + + return result; + } + + virtual MtpProperty* getDevicePropertyDesc(MtpDeviceProperty property) + { + VLOG(1) << __PRETTY_FUNCTION__ << MtpDebug::getDevicePropCodeName(property); + + MtpProperty* result = nullptr; + switch(property) + { + case MTP_DEVICE_PROPERTY_SYNCHRONIZATION_PARTNER: + case MTP_DEVICE_PROPERTY_DEVICE_FRIENDLY_NAME: + result = new MtpProperty(property, MTP_TYPE_STR, false); break; + default: break; + } + + return result; + } + + virtual void sessionStarted(MtpServer* server) + { + VLOG(1) << __PRETTY_FUNCTION__; + local_server = server; + } + + virtual void sessionEnded() + { + VLOG(1) << __PRETTY_FUNCTION__; + VLOG(1) << "objects in db at session end: " << db.size(); + local_server = nullptr; + } +}; +} + +#endif // STUB_MTP_DATABASE_H_ diff --git a/src/ThirdParty/mtp-server-nx/include/USBMtpInterface.h b/src/ThirdParty/mtp-server-nx/include/USBMtpInterface.h new file mode 100644 index 0000000..24302df --- /dev/null +++ b/src/ThirdParty/mtp-server-nx/include/USBMtpInterface.h @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef __USB_MTP_INTERFACE_H +#define __USB_MTP_INTERFACE_H + +#include "usb.h" + +class USBMtpInterface { +private: + + int interface_index; + + struct usb_interface_descriptor mtp_interface_descriptor = { + .bLength = USB_DT_INTERFACE_SIZE, + .bDescriptorType = USB_DT_INTERFACE, + .bNumEndpoints = 3, + .bInterfaceClass = 6, + .bInterfaceSubClass = 1, + .bInterfaceProtocol = 1, + }; + struct usb_endpoint_descriptor mtp_endpoint_descriptor_in = { + .bLength = USB_DT_ENDPOINT_SIZE, + .bDescriptorType = USB_DT_ENDPOINT, + .bEndpointAddress = USB_ENDPOINT_IN, + .bmAttributes = USB_TRANSFER_TYPE_BULK, + .wMaxPacketSize = 0x200, + }; + struct usb_endpoint_descriptor mtp_endpoint_descriptor_out = { + .bLength = USB_DT_ENDPOINT_SIZE, + .bDescriptorType = USB_DT_ENDPOINT, + .bEndpointAddress = USB_ENDPOINT_OUT, + .bmAttributes = USB_TRANSFER_TYPE_BULK, + .wMaxPacketSize = 0x200, + }; + struct usb_endpoint_descriptor mtp_endpoint_descriptor_interrupt = { + .bLength = USB_DT_ENDPOINT_SIZE, + .bDescriptorType = USB_DT_ENDPOINT, + .bEndpointAddress = USB_ENDPOINT_IN, + .bmAttributes = USB_TRANSFER_TYPE_INTERRUPT, + .wMaxPacketSize = 0x1c, + .bInterval = 6, + }; + + const char * mtp_string_descriptor = "MTP"; + +public: + + USBMtpInterface(int index, UsbInterfaceDesc *info); + virtual ~USBMtpInterface(); + + ssize_t read(char *ptr, size_t len); + ssize_t readWithTimeout(char *ptr, size_t len, u64 timeout); + ssize_t write(const char *ptr, size_t len); + ssize_t sendEvent(const char *ptr, size_t len); +}; + +#endif /* __USB_MTP_INTERFACE_H */ diff --git a/src/ThirdParty/mtp-server-nx/include/USBSerialInterface.h b/src/ThirdParty/mtp-server-nx/include/USBSerialInterface.h new file mode 100644 index 0000000..468037c --- /dev/null +++ b/src/ThirdParty/mtp-server-nx/include/USBSerialInterface.h @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef __USB_SERIAL_INTERFACE_H +#define __USB_SERIAL_INTERFACE_H + +#include "usb.h" + +class USBSerialInterface { +private: + + int interface_index; + + struct usb_interface_descriptor serial_interface_descriptor = { + .bLength = USB_DT_INTERFACE_SIZE, + .bDescriptorType = USB_DT_INTERFACE, + .bNumEndpoints = 2, + .bInterfaceClass = USB_CLASS_VENDOR_SPEC, + .bInterfaceSubClass = USB_CLASS_VENDOR_SPEC, + .bInterfaceProtocol = USB_CLASS_VENDOR_SPEC, + }; + + struct usb_endpoint_descriptor serial_endpoint_descriptor_in = { + .bLength = USB_DT_ENDPOINT_SIZE, + .bDescriptorType = USB_DT_ENDPOINT, + .bEndpointAddress = USB_ENDPOINT_IN, + .bmAttributes = USB_TRANSFER_TYPE_BULK, + .wMaxPacketSize = 0x200, + }; + + struct usb_endpoint_descriptor serial_endpoint_descriptor_out = { + .bLength = USB_DT_ENDPOINT_SIZE, + .bDescriptorType = USB_DT_ENDPOINT, + .bEndpointAddress = USB_ENDPOINT_OUT, + .bmAttributes = USB_TRANSFER_TYPE_BULK, + .wMaxPacketSize = 0x200, + }; + +public: + + USBSerialInterface(int index, UsbInterfaceDesc *info); + virtual ~USBSerialInterface(); + + ssize_t read(char *ptr, size_t len); + ssize_t write(const char *ptr, size_t len); +}; + +#endif /* __USB_SERIAL_INTERFACE_H */ diff --git a/src/ThirdParty/mtp-server-nx/include/log.h b/src/ThirdParty/mtp-server-nx/include/log.h new file mode 100644 index 0000000..b5ec548 --- /dev/null +++ b/src/ThirdParty/mtp-server-nx/include/log.h @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef __LOGGING_H +#define __LOGGING_H + +#include +#include "nxlink.h" + +#define VERBOSE 0 +#define INFO 1 +#define WARNING 2 +#define ERROR 3 +#define FATAL 4 + +extern int verbose_level; +extern char log_level_color[5][16]; + +#define LOG(level) std::cout << "\n" << (nxlink ? log_level_color[level] : "") + +#define VLOG_IS_ON(verboselevel) (verboselevel <= verbose_level) +#define VLOG(verboselevel) if(VLOG_IS_ON(verboselevel)) std::cout << "\n" << (nxlink ? log_level_color[VERBOSE]: "" ) + +#endif /* __LOGGING_H */ diff --git a/src/ThirdParty/mtp-server-nx/include/mtp.h b/src/ThirdParty/mtp-server-nx/include/mtp.h new file mode 100644 index 0000000..d270df5 --- /dev/null +++ b/src/ThirdParty/mtp-server-nx/include/mtp.h @@ -0,0 +1,492 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef _MTP_H +#define _MTP_H + +#include +#include + +#define MTP_STANDARD_VERSION 100 + +// Container Types +#define MTP_CONTAINER_TYPE_UNDEFINED 0 +#define MTP_CONTAINER_TYPE_COMMAND 1 +#define MTP_CONTAINER_TYPE_DATA 2 +#define MTP_CONTAINER_TYPE_RESPONSE 3 +#define MTP_CONTAINER_TYPE_EVENT 4 + +// Container Offsets +#define MTP_CONTAINER_LENGTH_OFFSET 0 +#define MTP_CONTAINER_TYPE_OFFSET 4 +#define MTP_CONTAINER_CODE_OFFSET 6 +#define MTP_CONTAINER_TRANSACTION_ID_OFFSET 8 +#define MTP_CONTAINER_PARAMETER_OFFSET 12 +#define MTP_CONTAINER_HEADER_SIZE 12 + +// MTP Data Types +#define MTP_TYPE_UNDEFINED 0x0000 // Undefined +#define MTP_TYPE_INT8 0x0001 // Signed 8-bit integer +#define MTP_TYPE_UINT8 0x0002 // Unsigned 8-bit integer +#define MTP_TYPE_INT16 0x0003 // Signed 16-bit integer +#define MTP_TYPE_UINT16 0x0004 // Unsigned 16-bit integer +#define MTP_TYPE_INT32 0x0005 // Signed 32-bit integer +#define MTP_TYPE_UINT32 0x0006 // Unsigned 32-bit integer +#define MTP_TYPE_INT64 0x0007 // Signed 64-bit integer +#define MTP_TYPE_UINT64 0x0008 // Unsigned 64-bit integer +#define MTP_TYPE_INT128 0x0009 // Signed 128-bit integer +#define MTP_TYPE_UINT128 0x000A // Unsigned 128-bit integer +#define MTP_TYPE_AINT8 0x4001 // Array of signed 8-bit integers +#define MTP_TYPE_AUINT8 0x4002 // Array of unsigned 8-bit integers +#define MTP_TYPE_AINT16 0x4003 // Array of signed 16-bit integers +#define MTP_TYPE_AUINT16 0x4004 // Array of unsigned 16-bit integers +#define MTP_TYPE_AINT32 0x4005 // Array of signed 32-bit integers +#define MTP_TYPE_AUINT32 0x4006 // Array of unsigned 32-bit integers +#define MTP_TYPE_AINT64 0x4007 // Array of signed 64-bit integers +#define MTP_TYPE_AUINT64 0x4008 // Array of unsigned 64-bit integers +#define MTP_TYPE_AINT128 0x4009 // Array of signed 128-bit integers +#define MTP_TYPE_AUINT128 0x400A // Array of unsigned 128-bit integers +#define MTP_TYPE_STR 0xFFFF // Variable-length Unicode string + +// MTP Format Codes +#define MTP_FORMAT_UNDEFINED 0x3000 // Undefined object +#define MTP_FORMAT_ASSOCIATION 0x3001 // Association (for example, a folder) +#define MTP_FORMAT_SCRIPT 0x3002 // Device model-specific script +#define MTP_FORMAT_EXECUTABLE 0x3003 // Device model-specific binary executable +#define MTP_FORMAT_TEXT 0x3004 // Text file +#define MTP_FORMAT_HTML 0x3005 // Hypertext Markup Language file (text) +#define MTP_FORMAT_DPOF 0x3006 // Digital Print Order Format file (text) +#define MTP_FORMAT_AIFF 0x3007 // Audio clip +#define MTP_FORMAT_WAV 0x3008 // Audio clip +#define MTP_FORMAT_MP3 0x3009 // Audio clip +#define MTP_FORMAT_AVI 0x300A // Video clip +#define MTP_FORMAT_MPEG 0x300B // Video clip +#define MTP_FORMAT_ASF 0x300C // Microsoft Advanced Streaming Format (video) +#define MTP_FORMAT_DEFINED 0x3800 // Unknown image object +#define MTP_FORMAT_EXIF_JPEG 0x3801 // Exchangeable File Format, JEIDA standard +#define MTP_FORMAT_TIFF_EP 0x3802 // Tag Image File Format for Electronic Photography +#define MTP_FORMAT_FLASHPIX 0x3803 // Structured Storage Image Format +#define MTP_FORMAT_BMP 0x3804 // Microsoft Windows Bitmap file +#define MTP_FORMAT_CIFF 0x3805 // Canon Camera Image File Format +#define MTP_FORMAT_GIF 0x3807 // Graphics Interchange Format +#define MTP_FORMAT_JFIF 0x3808 // JPEG File Interchange Format +#define MTP_FORMAT_CD 0x3809 // PhotoCD Image Pac +#define MTP_FORMAT_PICT 0x380A // Quickdraw Image Format +#define MTP_FORMAT_PNG 0x380B // Portable Network Graphics +#define MTP_FORMAT_TIFF 0x380D // Tag Image File Format +#define MTP_FORMAT_TIFF_IT 0x380E // Tag Image File Format for Information Technology (graphic arts) +#define MTP_FORMAT_JP2 0x380F // JPEG2000 Baseline File Format +#define MTP_FORMAT_JPX 0x3810 // JPEG2000 Extended File Format +#define MTP_FORMAT_UNDEFINED_FIRMWARE 0xB802 +#define MTP_FORMAT_WINDOWS_IMAGE_FORMAT 0xB881 +#define MTP_FORMAT_UNDEFINED_AUDIO 0xB900 +#define MTP_FORMAT_WMA 0xB901 +#define MTP_FORMAT_OGG 0xB902 +#define MTP_FORMAT_AAC 0xB903 +#define MTP_FORMAT_AUDIBLE 0xB904 +#define MTP_FORMAT_FLAC 0xB906 +#define MTP_FORMAT_UNDEFINED_VIDEO 0xB980 +#define MTP_FORMAT_WMV 0xB981 +#define MTP_FORMAT_MP4_CONTAINER 0xB982 // ISO 14496-1 +#define MTP_FORMAT_MP2 0xB983 +#define MTP_FORMAT_3GP_CONTAINER 0xB984 // 3GPP file format. Details: http://www.3gpp.org/ftp/Specs/html-info/26244.htm (page title - \u201cTransparent end-to-end packet switched streaming service, 3GPP file format\u201d). +#define MTP_FORMAT_UNDEFINED_COLLECTION 0xBA00 +#define MTP_FORMAT_ABSTRACT_MULTIMEDIA_ALBUM 0xBA01 +#define MTP_FORMAT_ABSTRACT_IMAGE_ALBUM 0xBA02 +#define MTP_FORMAT_ABSTRACT_AUDIO_ALBUM 0xBA03 +#define MTP_FORMAT_ABSTRACT_VIDEO_ALBUM 0xBA04 +#define MTP_FORMAT_ABSTRACT_AV_PLAYLIST 0xBA05 +#define MTP_FORMAT_ABSTRACT_CONTACT_GROUP 0xBA06 +#define MTP_FORMAT_ABSTRACT_MESSAGE_FOLDER 0xBA07 +#define MTP_FORMAT_ABSTRACT_CHAPTERED_PRODUCTION 0xBA08 +#define MTP_FORMAT_ABSTRACT_AUDIO_PLAYLIST 0xBA09 +#define MTP_FORMAT_ABSTRACT_VIDEO_PLAYLIST 0xBA0A +#define MTP_FORMAT_ABSTRACT_MEDIACAST 0xBA0B // For use with mediacasts; references multimedia enclosures of RSS feeds or episodic content +#define MTP_FORMAT_WPL_PLAYLIST 0xBA10 +#define MTP_FORMAT_M3U_PLAYLIST 0xBA11 +#define MTP_FORMAT_MPL_PLAYLIST 0xBA12 +#define MTP_FORMAT_ASX_PLAYLIST 0xBA13 +#define MTP_FORMAT_PLS_PLAYLIST 0xBA14 +#define MTP_FORMAT_UNDEFINED_DOCUMENT 0xBA80 +#define MTP_FORMAT_ABSTRACT_DOCUMENT 0xBA81 +#define MTP_FORMAT_XML_DOCUMENT 0xBA82 +#define MTP_FORMAT_MS_WORD_DOCUMENT 0xBA83 +#define MTP_FORMAT_MHT_COMPILED_HTML_DOCUMENT 0xBA84 +#define MTP_FORMAT_MS_EXCEL_SPREADSHEET 0xBA85 +#define MTP_FORMAT_MS_POWERPOINT_PRESENTATION 0xBA86 +#define MTP_FORMAT_UNDEFINED_MESSAGE 0xBB00 +#define MTP_FORMAT_ABSTRACT_MESSSAGE 0xBB01 +#define MTP_FORMAT_UNDEFINED_CONTACT 0xBB80 +#define MTP_FORMAT_ABSTRACT_CONTACT 0xBB81 +#define MTP_FORMAT_VCARD_2 0xBB82 + +// MTP Object Property Codes +#define MTP_PROPERTY_STORAGE_ID 0xDC01 +#define MTP_PROPERTY_OBJECT_FORMAT 0xDC02 +#define MTP_PROPERTY_PROTECTION_STATUS 0xDC03 +#define MTP_PROPERTY_OBJECT_SIZE 0xDC04 +#define MTP_PROPERTY_ASSOCIATION_TYPE 0xDC05 +#define MTP_PROPERTY_ASSOCIATION_DESC 0xDC06 +#define MTP_PROPERTY_OBJECT_FILE_NAME 0xDC07 +#define MTP_PROPERTY_DATE_CREATED 0xDC08 +#define MTP_PROPERTY_DATE_MODIFIED 0xDC09 +#define MTP_PROPERTY_KEYWORDS 0xDC0A +#define MTP_PROPERTY_PARENT_OBJECT 0xDC0B +#define MTP_PROPERTY_ALLOWED_FOLDER_CONTENTS 0xDC0C +#define MTP_PROPERTY_HIDDEN 0xDC0D +#define MTP_PROPERTY_SYSTEM_OBJECT 0xDC0E +#define MTP_PROPERTY_PERSISTENT_UID 0xDC41 +#define MTP_PROPERTY_SYNC_ID 0xDC42 +#define MTP_PROPERTY_PROPERTY_BAG 0xDC43 +#define MTP_PROPERTY_NAME 0xDC44 +#define MTP_PROPERTY_CREATED_BY 0xDC45 +#define MTP_PROPERTY_ARTIST 0xDC46 +#define MTP_PROPERTY_DATE_AUTHORED 0xDC47 +#define MTP_PROPERTY_DESCRIPTION 0xDC48 +#define MTP_PROPERTY_URL_REFERENCE 0xDC49 +#define MTP_PROPERTY_LANGUAGE_LOCALE 0xDC4A +#define MTP_PROPERTY_COPYRIGHT_INFORMATION 0xDC4B +#define MTP_PROPERTY_SOURCE 0xDC4C +#define MTP_PROPERTY_ORIGIN_LOCATION 0xDC4D +#define MTP_PROPERTY_DATE_ADDED 0xDC4E +#define MTP_PROPERTY_NON_CONSUMABLE 0xDC4F +#define MTP_PROPERTY_CORRUPT_UNPLAYABLE 0xDC50 +#define MTP_PROPERTY_PRODUCER_SERIAL_NUMBER 0xDC51 +#define MTP_PROPERTY_REPRESENTATIVE_SAMPLE_FORMAT 0xDC81 +#define MTP_PROPERTY_REPRESENTATIVE_SAMPLE_SIZE 0xDC82 +#define MTP_PROPERTY_REPRESENTATIVE_SAMPLE_HEIGHT 0xDC83 +#define MTP_PROPERTY_REPRESENTATIVE_SAMPLE_WIDTH 0xDC84 +#define MTP_PROPERTY_REPRESENTATIVE_SAMPLE_DURATION 0xDC85 +#define MTP_PROPERTY_REPRESENTATIVE_SAMPLE_DATA 0xDC86 +#define MTP_PROPERTY_WIDTH 0xDC87 +#define MTP_PROPERTY_HEIGHT 0xDC88 +#define MTP_PROPERTY_DURATION 0xDC89 +#define MTP_PROPERTY_RATING 0xDC8A +#define MTP_PROPERTY_TRACK 0xDC8B +#define MTP_PROPERTY_GENRE 0xDC8C +#define MTP_PROPERTY_CREDITS 0xDC8D +#define MTP_PROPERTY_LYRICS 0xDC8E +#define MTP_PROPERTY_SUBSCRIPTION_CONTENT_ID 0xDC8F +#define MTP_PROPERTY_PRODUCED_BY 0xDC90 +#define MTP_PROPERTY_USE_COUNT 0xDC91 +#define MTP_PROPERTY_SKIP_COUNT 0xDC92 +#define MTP_PROPERTY_LAST_ACCESSED 0xDC93 +#define MTP_PROPERTY_PARENTAL_RATING 0xDC94 +#define MTP_PROPERTY_META_GENRE 0xDC95 +#define MTP_PROPERTY_COMPOSER 0xDC96 +#define MTP_PROPERTY_EFFECTIVE_RATING 0xDC97 +#define MTP_PROPERTY_SUBTITLE 0xDC98 +#define MTP_PROPERTY_ORIGINAL_RELEASE_DATE 0xDC99 +#define MTP_PROPERTY_ALBUM_NAME 0xDC9A +#define MTP_PROPERTY_ALBUM_ARTIST 0xDC9B +#define MTP_PROPERTY_MOOD 0xDC9C +#define MTP_PROPERTY_DRM_STATUS 0xDC9D +#define MTP_PROPERTY_SUB_DESCRIPTION 0xDC9E +#define MTP_PROPERTY_IS_CROPPED 0xDCD1 +#define MTP_PROPERTY_IS_COLOUR_CORRECTED 0xDCD2 +#define MTP_PROPERTY_IMAGE_BIT_DEPTH 0xDCD3 +#define MTP_PROPERTY_F_NUMBER 0xDCD4 +#define MTP_PROPERTY_EXPOSURE_TIME 0xDCD5 +#define MTP_PROPERTY_EXPOSURE_INDEX 0xDCD6 +#define MTP_PROPERTY_TOTAL_BITRATE 0xDE91 +#define MTP_PROPERTY_BITRATE_TYPE 0xDE92 +#define MTP_PROPERTY_SAMPLE_RATE 0xDE93 +#define MTP_PROPERTY_NUMBER_OF_CHANNELS 0xDE94 +#define MTP_PROPERTY_AUDIO_BIT_DEPTH 0xDE95 +#define MTP_PROPERTY_SCAN_TYPE 0xDE97 +#define MTP_PROPERTY_AUDIO_WAVE_CODEC 0xDE99 +#define MTP_PROPERTY_AUDIO_BITRATE 0xDE9A +#define MTP_PROPERTY_VIDEO_FOURCC_CODEC 0xDE9B +#define MTP_PROPERTY_VIDEO_BITRATE 0xDE9C +#define MTP_PROPERTY_FRAMES_PER_THOUSAND_SECONDS 0xDE9D +#define MTP_PROPERTY_KEYFRAME_DISTANCE 0xDE9E +#define MTP_PROPERTY_BUFFER_SIZE 0xDE9F +#define MTP_PROPERTY_ENCODING_QUALITY 0xDEA0 +#define MTP_PROPERTY_ENCODING_PROFILE 0xDEA1 +#define MTP_PROPERTY_DISPLAY_NAME 0xDCE0 +#define MTP_PROPERTY_BODY_TEXT 0xDCE1 +#define MTP_PROPERTY_SUBJECT 0xDCE2 +#define MTP_PROPERTY_PRIORITY 0xDCE3 +#define MTP_PROPERTY_GIVEN_NAME 0xDD00 +#define MTP_PROPERTY_MIDDLE_NAMES 0xDD01 +#define MTP_PROPERTY_FAMILY_NAME 0xDD02 +#define MTP_PROPERTY_PREFIX 0xDD03 +#define MTP_PROPERTY_SUFFIX 0xDD04 +#define MTP_PROPERTY_PHONETIC_GIVEN_NAME 0xDD05 +#define MTP_PROPERTY_PHONETIC_FAMILY_NAME 0xDD06 +#define MTP_PROPERTY_EMAIL_PRIMARY 0xDD07 +#define MTP_PROPERTY_EMAIL_PERSONAL_1 0xDD08 +#define MTP_PROPERTY_EMAIL_PERSONAL_2 0xDD09 +#define MTP_PROPERTY_EMAIL_BUSINESS_1 0xDD0A +#define MTP_PROPERTY_EMAIL_BUSINESS_2 0xDD0B +#define MTP_PROPERTY_EMAIL_OTHERS 0xDD0C +#define MTP_PROPERTY_PHONE_NUMBER_PRIMARY 0xDD0D +#define MTP_PROPERTY_PHONE_NUMBER_PERSONAL 0xDD0E +#define MTP_PROPERTY_PHONE_NUMBER_PERSONAL_2 0xDD0F +#define MTP_PROPERTY_PHONE_NUMBER_BUSINESS 0xDD10 +#define MTP_PROPERTY_PHONE_NUMBER_BUSINESS_2 0xDD11 +#define MTP_PROPERTY_PHONE_NUMBER_MOBILE 0xDD12 +#define MTP_PROPERTY_PHONE_NUMBER_MOBILE_2 0xDD13 +#define MTP_PROPERTY_FAX_NUMBER_PRIMARY 0xDD14 +#define MTP_PROPERTY_FAX_NUMBER_PERSONAL 0xDD15 +#define MTP_PROPERTY_FAX_NUMBER_BUSINESS 0xDD16 +#define MTP_PROPERTY_PAGER_NUMBER 0xDD17 +#define MTP_PROPERTY_PHONE_NUMBER_OTHERS 0xDD18 +#define MTP_PROPERTY_PRIMARY_WEB_ADDRESS 0xDD19 +#define MTP_PROPERTY_PERSONAL_WEB_ADDRESS 0xDD1A +#define MTP_PROPERTY_BUSINESS_WEB_ADDRESS 0xDD1B +#define MTP_PROPERTY_INSTANT_MESSANGER_ADDRESS 0xDD1C +#define MTP_PROPERTY_INSTANT_MESSANGER_ADDRESS_2 0xDD1D +#define MTP_PROPERTY_INSTANT_MESSANGER_ADDRESS_3 0xDD1E +#define MTP_PROPERTY_POSTAL_ADDRESS_PERSONAL_FULL 0xDD1F +#define MTP_PROPERTY_POSTAL_ADDRESS_PERSONAL_LINE_1 0xDD20 +#define MTP_PROPERTY_POSTAL_ADDRESS_PERSONAL_LINE_2 0xDD21 +#define MTP_PROPERTY_POSTAL_ADDRESS_PERSONAL_CITY 0xDD22 +#define MTP_PROPERTY_POSTAL_ADDRESS_PERSONAL_REGION 0xDD23 +#define MTP_PROPERTY_POSTAL_ADDRESS_PERSONAL_POSTAL_CODE 0xDD24 +#define MTP_PROPERTY_POSTAL_ADDRESS_PERSONAL_COUNTRY 0xDD25 +#define MTP_PROPERTY_POSTAL_ADDRESS_BUSINESS_FULL 0xDD26 +#define MTP_PROPERTY_POSTAL_ADDRESS_BUSINESS_LINE_1 0xDD27 +#define MTP_PROPERTY_POSTAL_ADDRESS_BUSINESS_LINE_2 0xDD28 +#define MTP_PROPERTY_POSTAL_ADDRESS_BUSINESS_CITY 0xDD29 +#define MTP_PROPERTY_POSTAL_ADDRESS_BUSINESS_REGION 0xDD2A +#define MTP_PROPERTY_POSTAL_ADDRESS_BUSINESS_POSTAL_CODE 0xDD2B +#define MTP_PROPERTY_POSTAL_ADDRESS_BUSINESS_COUNTRY 0xDD2C +#define MTP_PROPERTY_POSTAL_ADDRESS_OTHER_FULL 0xDD2D +#define MTP_PROPERTY_POSTAL_ADDRESS_OTHER_LINE_1 0xDD2E +#define MTP_PROPERTY_POSTAL_ADDRESS_OTHER_LINE_2 0xDD2F +#define MTP_PROPERTY_POSTAL_ADDRESS_OTHER_CITY 0xDD30 +#define MTP_PROPERTY_POSTAL_ADDRESS_OTHER_REGION 0xDD31 +#define MTP_PROPERTY_POSTAL_ADDRESS_OTHER_POSTAL_CODE 0xDD32 +#define MTP_PROPERTY_POSTAL_ADDRESS_OTHER_COUNTRY 0xDD33 +#define MTP_PROPERTY_ORGANIZATION_NAME 0xDD34 +#define MTP_PROPERTY_PHONETIC_ORGANIZATION_NAME 0xDD35 +#define MTP_PROPERTY_ROLE 0xDD36 +#define MTP_PROPERTY_BIRTHDATE 0xDD37 +#define MTP_PROPERTY_MESSAGE_TO 0xDD40 +#define MTP_PROPERTY_MESSAGE_CC 0xDD41 +#define MTP_PROPERTY_MESSAGE_BCC 0xDD42 +#define MTP_PROPERTY_MESSAGE_READ 0xDD43 +#define MTP_PROPERTY_MESSAGE_RECEIVED_TIME 0xDD44 +#define MTP_PROPERTY_MESSAGE_SENDER 0xDD45 +#define MTP_PROPERTY_ACTIVITY_BEGIN_TIME 0xDD50 +#define MTP_PROPERTY_ACTIVITY_END_TIME 0xDD51 +#define MTP_PROPERTY_ACTIVITY_LOCATION 0xDD52 +#define MTP_PROPERTY_ACTIVITY_REQUIRED_ATTENDEES 0xDD54 +#define MTP_PROPERTY_ACTIVITY_OPTIONAL_ATTENDEES 0xDD55 +#define MTP_PROPERTY_ACTIVITY_RESOURCES 0xDD56 +#define MTP_PROPERTY_ACTIVITY_ACCEPTED 0xDD57 +#define MTP_PROPERTY_ACTIVITY_TENTATIVE 0xDD58 +#define MTP_PROPERTY_ACTIVITY_DECLINED 0xDD59 +#define MTP_PROPERTY_ACTIVITY_REMAINDER_TIME 0xDD5A +#define MTP_PROPERTY_ACTIVITY_OWNER 0xDD5B +#define MTP_PROPERTY_ACTIVITY_STATUS 0xDD5C +#define MTP_PROPERTY_OWNER 0xDD5D +#define MTP_PROPERTY_EDITOR 0xDD5E +#define MTP_PROPERTY_WEBMASTER 0xDD5F +#define MTP_PROPERTY_URL_SOURCE 0xDD60 +#define MTP_PROPERTY_URL_DESTINATION 0xDD61 +#define MTP_PROPERTY_TIME_BOOKMARK 0xDD62 +#define MTP_PROPERTY_OBJECT_BOOKMARK 0xDD63 +#define MTP_PROPERTY_BYTE_BOOKMARK 0xDD64 +#define MTP_PROPERTY_LAST_BUILD_DATE 0xDD70 +#define MTP_PROPERTY_TIME_TO_LIVE 0xDD71 +#define MTP_PROPERTY_MEDIA_GUID 0xDD72 + +// MTP Device Property Codes +#define MTP_DEVICE_PROPERTY_UNDEFINED 0x5000 +#define MTP_DEVICE_PROPERTY_BATTERY_LEVEL 0x5001 +#define MTP_DEVICE_PROPERTY_FUNCTIONAL_MODE 0x5002 +#define MTP_DEVICE_PROPERTY_IMAGE_SIZE 0x5003 +#define MTP_DEVICE_PROPERTY_COMPRESSION_SETTING 0x5004 +#define MTP_DEVICE_PROPERTY_WHITE_BALANCE 0x5005 +#define MTP_DEVICE_PROPERTY_RGB_GAIN 0x5006 +#define MTP_DEVICE_PROPERTY_F_NUMBER 0x5007 +#define MTP_DEVICE_PROPERTY_FOCAL_LENGTH 0x5008 +#define MTP_DEVICE_PROPERTY_FOCUS_DISTANCE 0x5009 +#define MTP_DEVICE_PROPERTY_FOCUS_MODE 0x500A +#define MTP_DEVICE_PROPERTY_EXPOSURE_METERING_MODE 0x500B +#define MTP_DEVICE_PROPERTY_FLASH_MODE 0x500C +#define MTP_DEVICE_PROPERTY_EXPOSURE_TIME 0x500D +#define MTP_DEVICE_PROPERTY_EXPOSURE_PROGRAM_MODE 0x500E +#define MTP_DEVICE_PROPERTY_EXPOSURE_INDEX 0x500F +#define MTP_DEVICE_PROPERTY_EXPOSURE_BIAS_COMPENSATION 0x5010 +#define MTP_DEVICE_PROPERTY_DATETIME 0x5011 +#define MTP_DEVICE_PROPERTY_CAPTURE_DELAY 0x5012 +#define MTP_DEVICE_PROPERTY_STILL_CAPTURE_MODE 0x5013 +#define MTP_DEVICE_PROPERTY_CONTRAST 0x5014 +#define MTP_DEVICE_PROPERTY_SHARPNESS 0x5015 +#define MTP_DEVICE_PROPERTY_DIGITAL_ZOOM 0x5016 +#define MTP_DEVICE_PROPERTY_EFFECT_MODE 0x5017 +#define MTP_DEVICE_PROPERTY_BURST_NUMBER 0x5018 +#define MTP_DEVICE_PROPERTY_BURST_INTERVAL 0x5019 +#define MTP_DEVICE_PROPERTY_TIMELAPSE_NUMBER 0x501A +#define MTP_DEVICE_PROPERTY_TIMELAPSE_INTERVAL 0x501B +#define MTP_DEVICE_PROPERTY_FOCUS_METERING_MODE 0x501C +#define MTP_DEVICE_PROPERTY_UPLOAD_URL 0x501D +#define MTP_DEVICE_PROPERTY_ARTIST 0x501E +#define MTP_DEVICE_PROPERTY_COPYRIGHT_INFO 0x501F +#define MTP_DEVICE_PROPERTY_SYNCHRONIZATION_PARTNER 0xD401 +#define MTP_DEVICE_PROPERTY_DEVICE_FRIENDLY_NAME 0xD402 +#define MTP_DEVICE_PROPERTY_VOLUME 0xD403 +#define MTP_DEVICE_PROPERTY_SUPPORTED_FORMATS_ORDERED 0xD404 +#define MTP_DEVICE_PROPERTY_DEVICE_ICON 0xD405 +#define MTP_DEVICE_PROPERTY_PLAYBACK_RATE 0xD410 +#define MTP_DEVICE_PROPERTY_PLAYBACK_OBJECT 0xD411 +#define MTP_DEVICE_PROPERTY_PLAYBACK_CONTAINER_INDEX 0xD412 +#define MTP_DEVICE_PROPERTY_SESSION_INITIATOR_VERSION_INFO 0xD406 +#define MTP_DEVICE_PROPERTY_PERCEIVED_DEVICE_TYPE 0xD407 + +// MTP Operation Codes +#define MTP_OPERATION_GET_DEVICE_INFO 0x1001 +#define MTP_OPERATION_OPEN_SESSION 0x1002 +#define MTP_OPERATION_CLOSE_SESSION 0x1003 +#define MTP_OPERATION_GET_STORAGE_IDS 0x1004 +#define MTP_OPERATION_GET_STORAGE_INFO 0x1005 +#define MTP_OPERATION_GET_NUM_OBJECTS 0x1006 +#define MTP_OPERATION_GET_OBJECT_HANDLES 0x1007 +#define MTP_OPERATION_GET_OBJECT_INFO 0x1008 +#define MTP_OPERATION_GET_OBJECT 0x1009 +#define MTP_OPERATION_GET_THUMB 0x100A +#define MTP_OPERATION_DELETE_OBJECT 0x100B +#define MTP_OPERATION_SEND_OBJECT_INFO 0x100C +#define MTP_OPERATION_SEND_OBJECT 0x100D +#define MTP_OPERATION_INITIATE_CAPTURE 0x100E +#define MTP_OPERATION_FORMAT_STORE 0x100F +#define MTP_OPERATION_RESET_DEVICE 0x1010 +#define MTP_OPERATION_SELF_TEST 0x1011 +#define MTP_OPERATION_SET_OBJECT_PROTECTION 0x1012 +#define MTP_OPERATION_POWER_DOWN 0x1013 +#define MTP_OPERATION_GET_DEVICE_PROP_DESC 0x1014 +#define MTP_OPERATION_GET_DEVICE_PROP_VALUE 0x1015 +#define MTP_OPERATION_SET_DEVICE_PROP_VALUE 0x1016 +#define MTP_OPERATION_RESET_DEVICE_PROP_VALUE 0x1017 +#define MTP_OPERATION_TERMINATE_OPEN_CAPTURE 0x1018 +#define MTP_OPERATION_MOVE_OBJECT 0x1019 +#define MTP_OPERATION_COPY_OBJECT 0x101A +#define MTP_OPERATION_GET_PARTIAL_OBJECT 0x101B +#define MTP_OPERATION_INITIATE_OPEN_CAPTURE 0x101C +#define MTP_OPERATION_GET_OBJECT_PROPS_SUPPORTED 0x9801 +#define MTP_OPERATION_GET_OBJECT_PROP_DESC 0x9802 +#define MTP_OPERATION_GET_OBJECT_PROP_VALUE 0x9803 +#define MTP_OPERATION_SET_OBJECT_PROP_VALUE 0x9804 +#define MTP_OPERATION_GET_OBJECT_PROP_LIST 0x9805 +#define MTP_OPERATION_SET_OBJECT_PROP_LIST 0x9806 +#define MTP_OPERATION_GET_INTERDEPENDENT_PROP_DESC 0x9807 +#define MTP_OPERATION_SEND_OBJECT_PROP_LIST 0x9808 +#define MTP_OPERATION_GET_OBJECT_REFERENCES 0x9810 +#define MTP_OPERATION_SET_OBJECT_REFERENCES 0x9811 +#define MTP_OPERATION_SKIP 0x9820 + +// Android extensions for direct file IO + +// Same as GetPartialObject, but with 64 bit offset +#define MTP_OPERATION_GET_PARTIAL_OBJECT_64 0x95C1 +// Same as GetPartialObject64, but copying host to device +#define MTP_OPERATION_SEND_PARTIAL_OBJECT 0x95C2 +// Truncates file to 64 bit length +#define MTP_OPERATION_TRUNCATE_OBJECT 0x95C3 +// Must be called before using SendPartialObject and TruncateObject +#define MTP_OPERATION_BEGIN_EDIT_OBJECT 0x95C4 +// Called to commit changes made by SendPartialObject and TruncateObject +#define MTP_OPERATION_END_EDIT_OBJECT 0x95C5 + +// MTP Response Codes +#define MTP_RESPONSE_UNDEFINED 0x2000 +#define MTP_RESPONSE_OK 0x2001 +#define MTP_RESPONSE_GENERAL_ERROR 0x2002 +#define MTP_RESPONSE_SESSION_NOT_OPEN 0x2003 +#define MTP_RESPONSE_INVALID_TRANSACTION_ID 0x2004 +#define MTP_RESPONSE_OPERATION_NOT_SUPPORTED 0x2005 +#define MTP_RESPONSE_PARAMETER_NOT_SUPPORTED 0x2006 +#define MTP_RESPONSE_INCOMPLETE_TRANSFER 0x2007 +#define MTP_RESPONSE_INVALID_STORAGE_ID 0x2008 +#define MTP_RESPONSE_INVALID_OBJECT_HANDLE 0x2009 +#define MTP_RESPONSE_DEVICE_PROP_NOT_SUPPORTED 0x200A +#define MTP_RESPONSE_INVALID_OBJECT_FORMAT_CODE 0x200B +#define MTP_RESPONSE_STORAGE_FULL 0x200C +#define MTP_RESPONSE_OBJECT_WRITE_PROTECTED 0x200D +#define MTP_RESPONSE_STORE_READ_ONLY 0x200E +#define MTP_RESPONSE_ACCESS_DENIED 0x200F +#define MTP_RESPONSE_NO_THUMBNAIL_PRESENT 0x2010 +#define MTP_RESPONSE_SELF_TEST_FAILED 0x2011 +#define MTP_RESPONSE_PARTIAL_DELETION 0x2012 +#define MTP_RESPONSE_STORE_NOT_AVAILABLE 0x2013 +#define MTP_RESPONSE_SPECIFICATION_BY_FORMAT_UNSUPPORTED 0x2014 +#define MTP_RESPONSE_NO_VALID_OBJECT_INFO 0x2015 +#define MTP_RESPONSE_INVALID_CODE_FORMAT 0x2016 +#define MTP_RESPONSE_UNKNOWN_VENDOR_CODE 0x2017 +#define MTP_RESPONSE_CAPTURE_ALREADY_TERMINATED 0x2018 +#define MTP_RESPONSE_DEVICE_BUSY 0x2019 +#define MTP_RESPONSE_INVALID_PARENT_OBJECT 0x201A +#define MTP_RESPONSE_INVALID_DEVICE_PROP_FORMAT 0x201B +#define MTP_RESPONSE_INVALID_DEVICE_PROP_VALUE 0x201C +#define MTP_RESPONSE_INVALID_PARAMETER 0x201D +#define MTP_RESPONSE_SESSION_ALREADY_OPEN 0x201E +#define MTP_RESPONSE_TRANSACTION_CANCELLED 0x201F +#define MTP_RESPONSE_SPECIFICATION_OF_DESTINATION_UNSUPPORTED 0x2020 +#define MTP_RESPONSE_INVALID_OBJECT_PROP_CODE 0xA801 +#define MTP_RESPONSE_INVALID_OBJECT_PROP_FORMAT 0xA802 +#define MTP_RESPONSE_INVALID_OBJECT_PROP_VALUE 0xA803 +#define MTP_RESPONSE_INVALID_OBJECT_REFERENCE 0xA804 +#define MTP_RESPONSE_GROUP_NOT_SUPPORTED 0xA805 +#define MTP_RESPONSE_INVALID_DATASET 0xA806 +#define MTP_RESPONSE_SPECIFICATION_BY_GROUP_UNSUPPORTED 0xA807 +#define MTP_RESPONSE_SPECIFICATION_BY_DEPTH_UNSUPPORTED 0xA808 +#define MTP_RESPONSE_OBJECT_TOO_LARGE 0xA809 +#define MTP_RESPONSE_OBJECT_PROP_NOT_SUPPORTED 0xA80A + +// MTP Event Codes +#define MTP_EVENT_UNDEFINED 0x4000 +#define MTP_EVENT_CANCEL_TRANSACTION 0x4001 +#define MTP_EVENT_OBJECT_ADDED 0x4002 +#define MTP_EVENT_OBJECT_REMOVED 0x4003 +#define MTP_EVENT_STORE_ADDED 0x4004 +#define MTP_EVENT_STORE_REMOVED 0x4005 +#define MTP_EVENT_DEVICE_PROP_CHANGED 0x4006 +#define MTP_EVENT_OBJECT_INFO_CHANGED 0x4007 +#define MTP_EVENT_DEVICE_INFO_CHANGED 0x4008 +#define MTP_EVENT_REQUEST_OBJECT_TRANSFER 0x4009 +#define MTP_EVENT_STORE_FULL 0x400A +#define MTP_EVENT_DEVICE_RESET 0x400B +#define MTP_EVENT_STORAGE_INFO_CHANGED 0x400C +#define MTP_EVENT_CAPTURE_COMPLETE 0x400D +#define MTP_EVENT_UNREPORTED_STATUS 0x400E +#define MTP_EVENT_OBJECT_PROP_CHANGED 0xC801 +#define MTP_EVENT_OBJECT_PROP_DESC_CHANGED 0xC802 +#define MTP_EVENT_OBJECT_REFERENCES_CHANGED 0xC803 + +// Storage Type +#define MTP_STORAGE_FIXED_ROM 0x0001 +#define MTP_STORAGE_REMOVABLE_ROM 0x0002 +#define MTP_STORAGE_FIXED_RAM 0x0003 +#define MTP_STORAGE_REMOVABLE_RAM 0x0004 + +// Storage File System +#define MTP_STORAGE_FILESYSTEM_FLAT 0x0001 +#define MTP_STORAGE_FILESYSTEM_HIERARCHICAL 0x0002 +#define MTP_STORAGE_FILESYSTEM_DCF 0x0003 + +// Storage Access Capability +#define MTP_STORAGE_READ_WRITE 0x0000 +#define MTP_STORAGE_READ_ONLY_WITHOUT_DELETE 0x0001 +#define MTP_STORAGE_READ_ONLY_WITH_DELETE 0x0002 + +// Association Type +#define MTP_ASSOCIATION_TYPE_UNDEFINED 0x0000 +#define MTP_ASSOCIATION_TYPE_GENERIC_FOLDER 0x0001 + +#endif // _MTP_H diff --git a/src/ThirdParty/mtp-server-nx/include/nxlink.h b/src/ThirdParty/mtp-server-nx/include/nxlink.h new file mode 100644 index 0000000..4239817 --- /dev/null +++ b/src/ThirdParty/mtp-server-nx/include/nxlink.h @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef __NXLINK_H +#define __NXLINK_H + +#include "USBSerialInterface.h" + +extern int nxlink; + +void nxlinkStdioInitialise(USBSerialInterface* usb); +void nxlinkStdioClose(USBSerialInterface* usb); + +#endif /* __NXLINK_H */ diff --git a/src/ThirdParty/mtp-server-nx/include/usb.h b/src/ThirdParty/mtp-server-nx/include/usb.h new file mode 100644 index 0000000..0c2b12b --- /dev/null +++ b/src/ThirdParty/mtp-server-nx/include/usb.h @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef __USB_H +#define __USB_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +typedef struct { + struct usb_interface_descriptor *interface_desc; + struct usb_endpoint_descriptor *endpoint_desc[4]; + const char *string_descriptor; +} UsbInterfaceDesc; + +typedef enum { + UsbDirection_Read = 0, + UsbDirection_Write = 1, +} UsbDirection; + +Result usbInitialize(struct usb_device_descriptor *device_descriptor, u32 num_interfaces, const UsbInterfaceDesc *infos); +void usbExit(void); +size_t usbTransfer(u32 interface, u32 endpoint, UsbDirection dir, void* buffer, size_t size, u64 timeout); + +#ifdef __cplusplus +} // extern "C" +#endif + +#endif /* __USB_H */ diff --git a/src/ThirdParty/mtp-server-nx/source/MtpDataPacket.cpp b/src/ThirdParty/mtp-server-nx/source/MtpDataPacket.cpp new file mode 100644 index 0000000..2a551ca --- /dev/null +++ b/src/ThirdParty/mtp-server-nx/source/MtpDataPacket.cpp @@ -0,0 +1,411 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include + +#include +#include +#include + +#include "MtpDataPacket.h" +#include "MtpStringBuffer.h" + +#include "log.h" + +#define MTP_BUFFER_SIZE (256 * 1024) + +namespace android { + +MtpDataPacket::MtpDataPacket() + : MtpPacket(MTP_BUFFER_SIZE), // MAX_USBFS_BUFFER_SIZE + mOffset(MTP_CONTAINER_HEADER_SIZE) +{ +} + +MtpDataPacket::~MtpDataPacket() { +} + +void MtpDataPacket::reset() { + MtpPacket::reset(); + mOffset = MTP_CONTAINER_HEADER_SIZE; +} + +void MtpDataPacket::setOperationCode(MtpOperationCode code) { + MtpPacket::putUInt16(MTP_CONTAINER_CODE_OFFSET, code); +} + +void MtpDataPacket::setTransactionID(MtpTransactionID id) { + MtpPacket::putUInt32(MTP_CONTAINER_TRANSACTION_ID_OFFSET, id); +} + +uint16_t MtpDataPacket::getUInt16() { + int offset = mOffset; + uint16_t result = (uint16_t)mBuffer[offset] | ((uint16_t)mBuffer[offset + 1] << 8); + mOffset += 2; + return result; +} + +uint32_t MtpDataPacket::getUInt32() { + int offset = mOffset; + uint32_t result = (uint32_t)mBuffer[offset] | ((uint32_t)mBuffer[offset + 1] << 8) | + ((uint32_t)mBuffer[offset + 2] << 16) | ((uint32_t)mBuffer[offset + 3] << 24); + mOffset += 4; + return result; +} + +uint64_t MtpDataPacket::getUInt64() { + int offset = mOffset; + uint64_t result = (uint64_t)mBuffer[offset] | ((uint64_t)mBuffer[offset + 1] << 8) | + ((uint64_t)mBuffer[offset + 2] << 16) | ((uint64_t)mBuffer[offset + 3] << 24) | + ((uint64_t)mBuffer[offset + 4] << 32) | ((uint64_t)mBuffer[offset + 5] << 40) | + ((uint64_t)mBuffer[offset + 6] << 48) | ((uint64_t)mBuffer[offset + 7] << 56); + mOffset += 8; + return result; +} + +void MtpDataPacket::getUInt128(uint128_t& value) { + value[0] = getUInt32(); + value[1] = getUInt32(); + value[2] = getUInt32(); + value[3] = getUInt32(); +} + +void MtpDataPacket::getString(MtpStringBuffer& string) +{ + string.readFromPacket(this); +} + +Int8List* MtpDataPacket::getAInt8() { + Int8List* result = new Int8List; + int count = getUInt32(); + for (int i = 0; i < count; i++) + result->push_back(getInt8()); + return result; +} + +UInt8List* MtpDataPacket::getAUInt8() { + UInt8List* result = new UInt8List; + int count = getUInt32(); + for (int i = 0; i < count; i++) + result->push_back(getUInt8()); + return result; +} + +Int16List* MtpDataPacket::getAInt16() { + Int16List* result = new Int16List; + int count = getUInt32(); + for (int i = 0; i < count; i++) + result->push_back(getInt16()); + return result; +} + +UInt16List* MtpDataPacket::getAUInt16() { + UInt16List* result = new UInt16List; + int count = getUInt32(); + for (int i = 0; i < count; i++) + result->push_back(getUInt16()); + return result; +} + +Int32List* MtpDataPacket::getAInt32() { + Int32List* result = new Int32List; + int count = getUInt32(); + for (int i = 0; i < count; i++) + result->push_back(getInt32()); + return result; +} + +UInt32List* MtpDataPacket::getAUInt32() { + UInt32List* result = new UInt32List; + int count = getUInt32(); + for (int i = 0; i < count; i++) + result->push_back(getUInt32()); + return result; +} + +Int64List* MtpDataPacket::getAInt64() { + Int64List* result = new Int64List; + int count = getUInt32(); + for (int i = 0; i < count; i++) + result->push_back(getInt64()); + return result; +} + +UInt64List* MtpDataPacket::getAUInt64() { + UInt64List* result = new UInt64List; + int count = getUInt32(); + for (int i = 0; i < count; i++) + result->push_back(getUInt64()); + return result; +} + +void MtpDataPacket::putInt8(int8_t value) { + allocate(mOffset + 1); + mBuffer[mOffset++] = (uint8_t)value; + if (mPacketSize < mOffset) + mPacketSize = mOffset; +} + +void MtpDataPacket::putUInt8(uint8_t value) { + allocate(mOffset + 1); + mBuffer[mOffset++] = (uint8_t)value; + if (mPacketSize < mOffset) + mPacketSize = mOffset; +} + +void MtpDataPacket::putInt16(int16_t value) { + allocate(mOffset + 2); + mBuffer[mOffset++] = (uint8_t)(value & 0xFF); + mBuffer[mOffset++] = (uint8_t)((value >> 8) & 0xFF); + if (mPacketSize < mOffset) + mPacketSize = mOffset; +} + +void MtpDataPacket::putUInt16(uint16_t value) { + allocate(mOffset + 2); + mBuffer[mOffset++] = (uint8_t)(value & 0xFF); + mBuffer[mOffset++] = (uint8_t)((value >> 8) & 0xFF); + if (mPacketSize < mOffset) + mPacketSize = mOffset; +} + +void MtpDataPacket::putInt32(int32_t value) { + allocate(mOffset + 4); + mBuffer[mOffset++] = (uint8_t)(value & 0xFF); + mBuffer[mOffset++] = (uint8_t)((value >> 8) & 0xFF); + mBuffer[mOffset++] = (uint8_t)((value >> 16) & 0xFF); + mBuffer[mOffset++] = (uint8_t)((value >> 24) & 0xFF); + if (mPacketSize < mOffset) + mPacketSize = mOffset; +} + +void MtpDataPacket::putUInt32(uint32_t value) { + allocate(mOffset + 4); + mBuffer[mOffset++] = (uint8_t)(value & 0xFF); + mBuffer[mOffset++] = (uint8_t)((value >> 8) & 0xFF); + mBuffer[mOffset++] = (uint8_t)((value >> 16) & 0xFF); + mBuffer[mOffset++] = (uint8_t)((value >> 24) & 0xFF); + if (mPacketSize < mOffset) + mPacketSize = mOffset; +} + +void MtpDataPacket::putInt64(int64_t value) { + allocate(mOffset + 8); + mBuffer[mOffset++] = (uint8_t)(value & 0xFF); + mBuffer[mOffset++] = (uint8_t)((value >> 8) & 0xFF); + mBuffer[mOffset++] = (uint8_t)((value >> 16) & 0xFF); + mBuffer[mOffset++] = (uint8_t)((value >> 24) & 0xFF); + mBuffer[mOffset++] = (uint8_t)((value >> 32) & 0xFF); + mBuffer[mOffset++] = (uint8_t)((value >> 40) & 0xFF); + mBuffer[mOffset++] = (uint8_t)((value >> 48) & 0xFF); + mBuffer[mOffset++] = (uint8_t)((value >> 56) & 0xFF); + if (mPacketSize < mOffset) + mPacketSize = mOffset; +} + +void MtpDataPacket::putUInt64(uint64_t value) { + allocate(mOffset + 8); + mBuffer[mOffset++] = (uint8_t)(value & 0xFF); + mBuffer[mOffset++] = (uint8_t)((value >> 8) & 0xFF); + mBuffer[mOffset++] = (uint8_t)((value >> 16) & 0xFF); + mBuffer[mOffset++] = (uint8_t)((value >> 24) & 0xFF); + mBuffer[mOffset++] = (uint8_t)((value >> 32) & 0xFF); + mBuffer[mOffset++] = (uint8_t)((value >> 40) & 0xFF); + mBuffer[mOffset++] = (uint8_t)((value >> 48) & 0xFF); + mBuffer[mOffset++] = (uint8_t)((value >> 56) & 0xFF); + if (mPacketSize < mOffset) + mPacketSize = mOffset; +} + +void MtpDataPacket::putInt128(const int128_t& value) { + putInt32(value[0]); + putInt32(value[1]); + putInt32(value[2]); + putInt32(value[3]); +} + +void MtpDataPacket::putUInt128(const uint128_t& value) { + putUInt32(value[0]); + putUInt32(value[1]); + putUInt32(value[2]); + putUInt32(value[3]); +} + +void MtpDataPacket::putInt128(int64_t value) { + putInt64(value); + putInt64(value < 0 ? -1 : 0); +} + +void MtpDataPacket::putUInt128(uint64_t value) { + putUInt64(value); + putUInt64(0); +} + +void MtpDataPacket::putAInt8(const int8_t* values, int count) { + putUInt32(count); + for (int i = 0; i < count; i++) + putInt8(*values++); +} + +void MtpDataPacket::putAUInt8(const uint8_t* values, int count) { + putUInt32(count); + for (int i = 0; i < count; i++) + putUInt8(*values++); +} + +void MtpDataPacket::putAInt16(const int16_t* values, int count) { + putUInt32(count); + for (int i = 0; i < count; i++) + putInt16(*values++); +} + +void MtpDataPacket::putAUInt16(const uint16_t* values, int count) { + putUInt32(count); + for (int i = 0; i < count; i++) + putUInt16(*values++); +} + +void MtpDataPacket::putAUInt16(const UInt16List* values) { + size_t count = (values ? values->size() : 0); + putUInt32(count); + for (size_t i = 0; i < count; i++) + putUInt16((*values)[i]); +} + +void MtpDataPacket::putAInt32(const int32_t* values, int count) { + putUInt32(count); + for (int i = 0; i < count; i++) + putInt32(*values++); +} + +void MtpDataPacket::putAUInt32(const uint32_t* values, int count) { + putUInt32(count); + for (int i = 0; i < count; i++) + putUInt32(*values++); +} + +void MtpDataPacket::putAUInt32(const UInt32List* list) { + if (!list) { + putEmptyArray(); + } else { + size_t size = list->size(); + putUInt32(size); + for (size_t i = 0; i < size; i++) + putUInt32((*list)[i]); + } +} + +void MtpDataPacket::putAInt64(const int64_t* values, int count) { + putUInt32(count); + for (int i = 0; i < count; i++) + putInt64(*values++); +} + +void MtpDataPacket::putAUInt64(const uint64_t* values, int count) { + putUInt32(count); + for (int i = 0; i < count; i++) + putUInt64(*values++); +} + +void MtpDataPacket::putString(const MtpStringBuffer& string) { + string.writeToPacket(this); +} + +void MtpDataPacket::putString(const char* s) { + MtpStringBuffer string(s); + string.writeToPacket(this); +} + +void MtpDataPacket::putString(const uint16_t* string) { + int count = 0; + for (int i = 0; i < 256; i++) { + if (string[i]) + count++; + else + break; + } + putUInt8(count > 0 ? count + 1 : 0); + for (int i = 0; i < count; i++) + putUInt16(string[i]); + // only terminate with zero if string is not empty + if (count > 0) + putUInt16(0); +} + +int MtpDataPacket::read(USBMtpInterface* usb) { + int ret = usb->read((char*)mBuffer, MTP_BUFFER_SIZE); + if (ret < MTP_CONTAINER_HEADER_SIZE) + return -1; + mPacketSize = ret; + mOffset = MTP_CONTAINER_HEADER_SIZE; + return ret; +} + +int MtpDataPacket::read(USBMtpInterface* usb, uint32_t length) { + int ret = usb->read((char*)mBuffer, length); + if (ret < MTP_CONTAINER_HEADER_SIZE) + return -1; + mPacketSize = ret; + mOffset = MTP_CONTAINER_HEADER_SIZE; + return ret; +} + +int MtpDataPacket::readWithTimeout(USBMtpInterface* usb, uint64_t timeout) { + return readWithTimeout(usb, MTP_BUFFER_SIZE, timeout); +} + +int MtpDataPacket::readWithTimeout(USBMtpInterface* usb, uint32_t length, uint64_t timeout) { + int ret = usb->readWithTimeout((char*)mBuffer, length, timeout); + if (ret < MTP_CONTAINER_HEADER_SIZE) + return -1; + mPacketSize = ret; + mOffset = MTP_CONTAINER_HEADER_SIZE; + return ret; +} + +int MtpDataPacket::write(USBMtpInterface* usb) { + MtpPacket::putUInt32(MTP_CONTAINER_LENGTH_OFFSET, mPacketSize); + MtpPacket::putUInt16(MTP_CONTAINER_TYPE_OFFSET, MTP_CONTAINER_TYPE_DATA); + int ret = usb->write((const char*)mBuffer, mPacketSize); + return (ret < 0 ? ret : 0); +} + +int MtpDataPacket::writeData(USBMtpInterface* usb, void* data, uint32_t length) { + allocate(length); + memcpy(mBuffer + MTP_CONTAINER_HEADER_SIZE, data, length); + length += MTP_CONTAINER_HEADER_SIZE; + MtpPacket::putUInt32(MTP_CONTAINER_LENGTH_OFFSET, length); + MtpPacket::putUInt16(MTP_CONTAINER_TYPE_OFFSET, MTP_CONTAINER_TYPE_DATA); + int ret = usb->write((const char*)mBuffer, length); + return (ret < 0 ? ret : 0); +} + +void* MtpDataPacket::getData(int& outLength) const { + int length = mPacketSize - MTP_CONTAINER_HEADER_SIZE; + if (length > 0) { + void* result = malloc(length); + if (result) { + memcpy(result, mBuffer + MTP_CONTAINER_HEADER_SIZE, length); + outLength = length; + return result; + } + } + outLength = 0; + return NULL; +} + +} // namespace android diff --git a/src/ThirdParty/mtp-server-nx/source/MtpDebug.cpp b/src/ThirdParty/mtp-server-nx/source/MtpDebug.cpp new file mode 100644 index 0000000..9f3037d --- /dev/null +++ b/src/ThirdParty/mtp-server-nx/source/MtpDebug.cpp @@ -0,0 +1,402 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "MtpDebug.h" + +namespace android { + +struct CodeEntry { + const char* name; + uint16_t code; +}; + +static const CodeEntry sOperationCodes[] = { + { "MTP_OPERATION_GET_DEVICE_INFO", 0x1001 }, + { "MTP_OPERATION_OPEN_SESSION", 0x1002 }, + { "MTP_OPERATION_CLOSE_SESSION", 0x1003 }, + { "MTP_OPERATION_GET_STORAGE_IDS", 0x1004 }, + { "MTP_OPERATION_GET_STORAGE_INFO", 0x1005 }, + { "MTP_OPERATION_GET_NUM_OBJECTS", 0x1006 }, + { "MTP_OPERATION_GET_OBJECT_HANDLES", 0x1007 }, + { "MTP_OPERATION_GET_OBJECT_INFO", 0x1008 }, + { "MTP_OPERATION_GET_OBJECT", 0x1009 }, + { "MTP_OPERATION_GET_THUMB", 0x100A }, + { "MTP_OPERATION_DELETE_OBJECT", 0x100B }, + { "MTP_OPERATION_SEND_OBJECT_INFO", 0x100C }, + { "MTP_OPERATION_SEND_OBJECT", 0x100D }, + { "MTP_OPERATION_INITIATE_CAPTURE", 0x100E }, + { "MTP_OPERATION_FORMAT_STORE", 0x100F }, + { "MTP_OPERATION_RESET_DEVICE", 0x1010 }, + { "MTP_OPERATION_SELF_TEST", 0x1011 }, + { "MTP_OPERATION_SET_OBJECT_PROTECTION", 0x1012 }, + { "MTP_OPERATION_POWER_DOWN", 0x1013 }, + { "MTP_OPERATION_GET_DEVICE_PROP_DESC", 0x1014 }, + { "MTP_OPERATION_GET_DEVICE_PROP_VALUE", 0x1015 }, + { "MTP_OPERATION_SET_DEVICE_PROP_VALUE", 0x1016 }, + { "MTP_OPERATION_RESET_DEVICE_PROP_VALUE", 0x1017 }, + { "MTP_OPERATION_TERMINATE_OPEN_CAPTURE", 0x1018 }, + { "MTP_OPERATION_MOVE_OBJECT", 0x1019 }, + { "MTP_OPERATION_COPY_OBJECT", 0x101A }, + { "MTP_OPERATION_GET_PARTIAL_OBJECT", 0x101B }, + { "MTP_OPERATION_INITIATE_OPEN_CAPTURE", 0x101C }, + { "MTP_OPERATION_GET_OBJECT_PROPS_SUPPORTED", 0x9801 }, + { "MTP_OPERATION_GET_OBJECT_PROP_DESC", 0x9802 }, + { "MTP_OPERATION_GET_OBJECT_PROP_VALUE", 0x9803 }, + { "MTP_OPERATION_SET_OBJECT_PROP_VALUE", 0x9804 }, + { "MTP_OPERATION_GET_OBJECT_PROP_LIST", 0x9805 }, + { "MTP_OPERATION_SET_OBJECT_PROP_LIST", 0x9806 }, + { "MTP_OPERATION_GET_INTERDEPENDENT_PROP_DESC", 0x9807 }, + { "MTP_OPERATION_SEND_OBJECT_PROP_LIST", 0x9808 }, + { "MTP_OPERATION_GET_OBJECT_REFERENCES", 0x9810 }, + { "MTP_OPERATION_SET_OBJECT_REFERENCES", 0x9811 }, + { "MTP_OPERATION_SKIP", 0x9820 }, + // android extensions + { "MTP_OPERATION_GET_PARTIAL_OBJECT_64", 0x95C1 }, + { "MTP_OPERATION_SEND_PARTIAL_OBJECT", 0x95C2 }, + { "MTP_OPERATION_TRUNCATE_OBJECT", 0x95C3 }, + { "MTP_OPERATION_BEGIN_EDIT_OBJECT", 0x95C4 }, + { "MTP_OPERATION_END_EDIT_OBJECT", 0x95C5 }, + { 0, 0 }, +}; + +static const CodeEntry sFormatCodes[] = { + { "MTP_FORMAT_UNDEFINED", 0x3000 }, + { "MTP_FORMAT_ASSOCIATION", 0x3001 }, + { "MTP_FORMAT_SCRIPT", 0x3002 }, + { "MTP_FORMAT_EXECUTABLE", 0x3003 }, + { "MTP_FORMAT_TEXT", 0x3004 }, + { "MTP_FORMAT_HTML", 0x3005 }, + { "MTP_FORMAT_DPOF", 0x3006 }, + { "MTP_FORMAT_AIFF", 0x3007 }, + { "MTP_FORMAT_WAV", 0x3008 }, + { "MTP_FORMAT_MP3", 0x3009 }, + { "MTP_FORMAT_AVI", 0x300A }, + { "MTP_FORMAT_MPEG", 0x300B }, + { "MTP_FORMAT_ASF", 0x300C }, + { "MTP_FORMAT_DEFINED", 0x3800 }, + { "MTP_FORMAT_EXIF_JPEG", 0x3801 }, + { "MTP_FORMAT_TIFF_EP", 0x3802 }, + { "MTP_FORMAT_FLASHPIX", 0x3803 }, + { "MTP_FORMAT_BMP", 0x3804 }, + { "MTP_FORMAT_CIFF", 0x3805 }, + { "MTP_FORMAT_GIF", 0x3807 }, + { "MTP_FORMAT_JFIF", 0x3808 }, + { "MTP_FORMAT_CD", 0x3809 }, + { "MTP_FORMAT_PICT", 0x380A }, + { "MTP_FORMAT_PNG", 0x380B }, + { "MTP_FORMAT_TIFF", 0x380D }, + { "MTP_FORMAT_TIFF_IT", 0x380E }, + { "MTP_FORMAT_JP2", 0x380F }, + { "MTP_FORMAT_JPX", 0x3810 }, + { "MTP_FORMAT_UNDEFINED_FIRMWARE", 0xB802 }, + { "MTP_FORMAT_WINDOWS_IMAGE_FORMAT", 0xB881 }, + { "MTP_FORMAT_UNDEFINED_AUDIO", 0xB900 }, + { "MTP_FORMAT_WMA", 0xB901 }, + { "MTP_FORMAT_OGG", 0xB902 }, + { "MTP_FORMAT_AAC", 0xB903 }, + { "MTP_FORMAT_AUDIBLE", 0xB904 }, + { "MTP_FORMAT_FLAC", 0xB906 }, + { "MTP_FORMAT_UNDEFINED_VIDEO", 0xB980 }, + { "MTP_FORMAT_WMV", 0xB981 }, + { "MTP_FORMAT_MP4_CONTAINER", 0xB982 }, + { "MTP_FORMAT_MP2", 0xB983 }, + { "MTP_FORMAT_3GP_CONTAINER", 0xB984 }, + { "MTP_FORMAT_UNDEFINED_COLLECTION", 0xBA00 }, + { "MTP_FORMAT_ABSTRACT_MULTIMEDIA_ALBUM", 0xBA01 }, + { "MTP_FORMAT_ABSTRACT_IMAGE_ALBUM", 0xBA02 }, + { "MTP_FORMAT_ABSTRACT_AUDIO_ALBUM", 0xBA03 }, + { "MTP_FORMAT_ABSTRACT_VIDEO_ALBUM", 0xBA04 }, + { "MTP_FORMAT_ABSTRACT_AV_PLAYLIST", 0xBA05 }, + { "MTP_FORMAT_ABSTRACT_CONTACT_GROUP", 0xBA06 }, + { "MTP_FORMAT_ABSTRACT_MESSAGE_FOLDER", 0xBA07 }, + { "MTP_FORMAT_ABSTRACT_CHAPTERED_PRODUCTION", 0xBA08 }, + { "MTP_FORMAT_ABSTRACT_AUDIO_PLAYLIST", 0xBA09 }, + { "MTP_FORMAT_ABSTRACT_VIDEO_PLAYLIST", 0xBA0A }, + { "MTP_FORMAT_ABSTRACT_MEDIACAST", 0xBA0B }, + { "MTP_FORMAT_WPL_PLAYLIST", 0xBA10 }, + { "MTP_FORMAT_M3U_PLAYLIST", 0xBA11 }, + { "MTP_FORMAT_MPL_PLAYLIST", 0xBA12 }, + { "MTP_FORMAT_ASX_PLAYLIST", 0xBA13 }, + { "MTP_FORMAT_PLS_PLAYLIST", 0xBA14 }, + { "MTP_FORMAT_UNDEFINED_DOCUMENT", 0xBA80 }, + { "MTP_FORMAT_ABSTRACT_DOCUMENT", 0xBA81 }, + { "MTP_FORMAT_XML_DOCUMENT", 0xBA82 }, + { "MTP_FORMAT_MS_WORD_DOCUMENT", 0xBA83 }, + { "MTP_FORMAT_MHT_COMPILED_HTML_DOCUMENT", 0xBA84 }, + { "MTP_FORMAT_MS_EXCEL_SPREADSHEET", 0xBA85 }, + { "MTP_FORMAT_MS_POWERPOINT_PRESENTATION", 0xBA86 }, + { "MTP_FORMAT_UNDEFINED_MESSAGE", 0xBB00 }, + { "MTP_FORMAT_ABSTRACT_MESSSAGE", 0xBB01 }, + { "MTP_FORMAT_UNDEFINED_CONTACT", 0xBB80 }, + { "MTP_FORMAT_ABSTRACT_CONTACT", 0xBB81 }, + { "MTP_FORMAT_VCARD_2", 0xBB82 }, + { 0, 0 }, +}; + +static const CodeEntry sObjectPropCodes[] = { + { "MTP_PROPERTY_STORAGE_ID", 0xDC01 }, + { "MTP_PROPERTY_OBJECT_FORMAT", 0xDC02 }, + { "MTP_PROPERTY_PROTECTION_STATUS", 0xDC03 }, + { "MTP_PROPERTY_OBJECT_SIZE", 0xDC04 }, + { "MTP_PROPERTY_ASSOCIATION_TYPE", 0xDC05 }, + { "MTP_PROPERTY_ASSOCIATION_DESC", 0xDC06 }, + { "MTP_PROPERTY_OBJECT_FILE_NAME", 0xDC07 }, + { "MTP_PROPERTY_DATE_CREATED", 0xDC08 }, + { "MTP_PROPERTY_DATE_MODIFIED", 0xDC09 }, + { "MTP_PROPERTY_KEYWORDS", 0xDC0A }, + { "MTP_PROPERTY_PARENT_OBJECT", 0xDC0B }, + { "MTP_PROPERTY_ALLOWED_FOLDER_CONTENTS", 0xDC0C }, + { "MTP_PROPERTY_HIDDEN", 0xDC0D }, + { "MTP_PROPERTY_SYSTEM_OBJECT", 0xDC0E }, + { "MTP_PROPERTY_PERSISTENT_UID", 0xDC41 }, + { "MTP_PROPERTY_SYNC_ID", 0xDC42 }, + { "MTP_PROPERTY_PROPERTY_BAG", 0xDC43 }, + { "MTP_PROPERTY_NAME", 0xDC44 }, + { "MTP_PROPERTY_CREATED_BY", 0xDC45 }, + { "MTP_PROPERTY_ARTIST", 0xDC46 }, + { "MTP_PROPERTY_DATE_AUTHORED", 0xDC47 }, + { "MTP_PROPERTY_DESCRIPTION", 0xDC48 }, + { "MTP_PROPERTY_URL_REFERENCE", 0xDC49 }, + { "MTP_PROPERTY_LANGUAGE_LOCALE", 0xDC4A }, + { "MTP_PROPERTY_COPYRIGHT_INFORMATION", 0xDC4B }, + { "MTP_PROPERTY_SOURCE", 0xDC4C }, + { "MTP_PROPERTY_ORIGIN_LOCATION", 0xDC4D }, + { "MTP_PROPERTY_DATE_ADDED", 0xDC4E }, + { "MTP_PROPERTY_NON_CONSUMABLE", 0xDC4F }, + { "MTP_PROPERTY_CORRUPT_UNPLAYABLE", 0xDC50 }, + { "MTP_PROPERTY_PRODUCER_SERIAL_NUMBER", 0xDC51 }, + { "MTP_PROPERTY_REPRESENTATIVE_SAMPLE_FORMAT", 0xDC81 }, + { "MTP_PROPERTY_REPRESENTATIVE_SAMPLE_SIZE", 0xDC82 }, + { "MTP_PROPERTY_REPRESENTATIVE_SAMPLE_HEIGHT", 0xDC83 }, + { "MTP_PROPERTY_REPRESENTATIVE_SAMPLE_WIDTH", 0xDC84 }, + { "MTP_PROPERTY_REPRESENTATIVE_SAMPLE_DURATION", 0xDC85 }, + { "MTP_PROPERTY_REPRESENTATIVE_SAMPLE_DATA", 0xDC86 }, + { "MTP_PROPERTY_WIDTH", 0xDC87 }, + { "MTP_PROPERTY_HEIGHT", 0xDC88 }, + { "MTP_PROPERTY_DURATION", 0xDC89 }, + { "MTP_PROPERTY_RATING", 0xDC8A }, + { "MTP_PROPERTY_TRACK", 0xDC8B }, + { "MTP_PROPERTY_GENRE", 0xDC8C }, + { "MTP_PROPERTY_CREDITS", 0xDC8D }, + { "MTP_PROPERTY_LYRICS", 0xDC8E }, + { "MTP_PROPERTY_SUBSCRIPTION_CONTENT_ID", 0xDC8F }, + { "MTP_PROPERTY_PRODUCED_BY", 0xDC90 }, + { "MTP_PROPERTY_USE_COUNT", 0xDC91 }, + { "MTP_PROPERTY_SKIP_COUNT", 0xDC92 }, + { "MTP_PROPERTY_LAST_ACCESSED", 0xDC93 }, + { "MTP_PROPERTY_PARENTAL_RATING", 0xDC94 }, + { "MTP_PROPERTY_META_GENRE", 0xDC95 }, + { "MTP_PROPERTY_COMPOSER", 0xDC96 }, + { "MTP_PROPERTY_EFFECTIVE_RATING", 0xDC97 }, + { "MTP_PROPERTY_SUBTITLE", 0xDC98 }, + { "MTP_PROPERTY_ORIGINAL_RELEASE_DATE", 0xDC99 }, + { "MTP_PROPERTY_ALBUM_NAME", 0xDC9A }, + { "MTP_PROPERTY_ALBUM_ARTIST", 0xDC9B }, + { "MTP_PROPERTY_MOOD", 0xDC9C }, + { "MTP_PROPERTY_DRM_STATUS", 0xDC9D }, + { "MTP_PROPERTY_SUB_DESCRIPTION", 0xDC9E }, + { "MTP_PROPERTY_IS_CROPPED", 0xDCD1 }, + { "MTP_PROPERTY_IS_COLOUR_CORRECTED", 0xDCD2 }, + { "MTP_PROPERTY_IMAGE_BIT_DEPTH", 0xDCD3 }, + { "MTP_PROPERTY_F_NUMBER", 0xDCD4 }, + { "MTP_PROPERTY_EXPOSURE_TIME", 0xDCD5 }, + { "MTP_PROPERTY_EXPOSURE_INDEX", 0xDCD6 }, + { "MTP_PROPERTY_TOTAL_BITRATE", 0xDE91 }, + { "MTP_PROPERTY_BITRATE_TYPE", 0xDE92 }, + { "MTP_PROPERTY_SAMPLE_RATE", 0xDE93 }, + { "MTP_PROPERTY_NUMBER_OF_CHANNELS", 0xDE94 }, + { "MTP_PROPERTY_AUDIO_BIT_DEPTH", 0xDE95 }, + { "MTP_PROPERTY_SCAN_TYPE", 0xDE97 }, + { "MTP_PROPERTY_AUDIO_WAVE_CODEC", 0xDE99 }, + { "MTP_PROPERTY_AUDIO_BITRATE", 0xDE9A }, + { "MTP_PROPERTY_VIDEO_FOURCC_CODEC", 0xDE9B }, + { "MTP_PROPERTY_VIDEO_BITRATE", 0xDE9C }, + { "MTP_PROPERTY_FRAMES_PER_THOUSAND_SECONDS", 0xDE9D }, + { "MTP_PROPERTY_KEYFRAME_DISTANCE", 0xDE9E }, + { "MTP_PROPERTY_BUFFER_SIZE", 0xDE9F }, + { "MTP_PROPERTY_ENCODING_QUALITY", 0xDEA0 }, + { "MTP_PROPERTY_ENCODING_PROFILE", 0xDEA1 }, + { "MTP_PROPERTY_DISPLAY_NAME", 0xDCE0 }, + { "MTP_PROPERTY_BODY_TEXT", 0xDCE1 }, + { "MTP_PROPERTY_SUBJECT", 0xDCE2 }, + { "MTP_PROPERTY_PRIORITY", 0xDCE3 }, + { "MTP_PROPERTY_GIVEN_NAME", 0xDD00 }, + { "MTP_PROPERTY_MIDDLE_NAMES", 0xDD01 }, + { "MTP_PROPERTY_FAMILY_NAME", 0xDD02 }, + { "MTP_PROPERTY_PREFIX", 0xDD03 }, + { "MTP_PROPERTY_SUFFIX", 0xDD04 }, + { "MTP_PROPERTY_PHONETIC_GIVEN_NAME", 0xDD05 }, + { "MTP_PROPERTY_PHONETIC_FAMILY_NAME", 0xDD06 }, + { "MTP_PROPERTY_EMAIL_PRIMARY", 0xDD07 }, + { "MTP_PROPERTY_EMAIL_PERSONAL_1", 0xDD08 }, + { "MTP_PROPERTY_EMAIL_PERSONAL_2", 0xDD09 }, + { "MTP_PROPERTY_EMAIL_BUSINESS_1", 0xDD0A }, + { "MTP_PROPERTY_EMAIL_BUSINESS_2", 0xDD0B }, + { "MTP_PROPERTY_EMAIL_OTHERS", 0xDD0C }, + { "MTP_PROPERTY_PHONE_NUMBER_PRIMARY", 0xDD0D }, + { "MTP_PROPERTY_PHONE_NUMBER_PERSONAL", 0xDD0E }, + { "MTP_PROPERTY_PHONE_NUMBER_PERSONAL_2", 0xDD0F }, + { "MTP_PROPERTY_PHONE_NUMBER_BUSINESS", 0xDD10 }, + { "MTP_PROPERTY_PHONE_NUMBER_BUSINESS_2", 0xDD11 }, + { "MTP_PROPERTY_PHONE_NUMBER_MOBILE", 0xDD12 }, + { "MTP_PROPERTY_PHONE_NUMBER_MOBILE_2", 0xDD13 }, + { "MTP_PROPERTY_FAX_NUMBER_PRIMARY", 0xDD14 }, + { "MTP_PROPERTY_FAX_NUMBER_PERSONAL", 0xDD15 }, + { "MTP_PROPERTY_FAX_NUMBER_BUSINESS", 0xDD16 }, + { "MTP_PROPERTY_PAGER_NUMBER", 0xDD17 }, + { "MTP_PROPERTY_PHONE_NUMBER_OTHERS", 0xDD18 }, + { "MTP_PROPERTY_PRIMARY_WEB_ADDRESS", 0xDD19 }, + { "MTP_PROPERTY_PERSONAL_WEB_ADDRESS", 0xDD1A }, + { "MTP_PROPERTY_BUSINESS_WEB_ADDRESS", 0xDD1B }, + { "MTP_PROPERTY_INSTANT_MESSANGER_ADDRESS", 0xDD1C }, + { "MTP_PROPERTY_INSTANT_MESSANGER_ADDRESS_2", 0xDD1D }, + { "MTP_PROPERTY_INSTANT_MESSANGER_ADDRESS_3", 0xDD1E }, + { "MTP_PROPERTY_POSTAL_ADDRESS_PERSONAL_FULL", 0xDD1F }, + { "MTP_PROPERTY_POSTAL_ADDRESS_PERSONAL_LINE_1", 0xDD20 }, + { "MTP_PROPERTY_POSTAL_ADDRESS_PERSONAL_LINE_2", 0xDD21 }, + { "MTP_PROPERTY_POSTAL_ADDRESS_PERSONAL_CITY", 0xDD22 }, + { "MTP_PROPERTY_POSTAL_ADDRESS_PERSONAL_REGION", 0xDD23 }, + { "MTP_PROPERTY_POSTAL_ADDRESS_PERSONAL_POSTAL_CODE", 0xDD24 }, + { "MTP_PROPERTY_POSTAL_ADDRESS_PERSONAL_COUNTRY", 0xDD25 }, + { "MTP_PROPERTY_POSTAL_ADDRESS_BUSINESS_FULL", 0xDD26 }, + { "MTP_PROPERTY_POSTAL_ADDRESS_BUSINESS_LINE_1", 0xDD27 }, + { "MTP_PROPERTY_POSTAL_ADDRESS_BUSINESS_LINE_2", 0xDD28 }, + { "MTP_PROPERTY_POSTAL_ADDRESS_BUSINESS_CITY", 0xDD29 }, + { "MTP_PROPERTY_POSTAL_ADDRESS_BUSINESS_REGION", 0xDD2A }, + { "MTP_PROPERTY_POSTAL_ADDRESS_BUSINESS_POSTAL_CODE", 0xDD2B }, + { "MTP_PROPERTY_POSTAL_ADDRESS_BUSINESS_COUNTRY", 0xDD2C }, + { "MTP_PROPERTY_POSTAL_ADDRESS_OTHER_FULL", 0xDD2D }, + { "MTP_PROPERTY_POSTAL_ADDRESS_OTHER_LINE_1", 0xDD2E }, + { "MTP_PROPERTY_POSTAL_ADDRESS_OTHER_LINE_2", 0xDD2F }, + { "MTP_PROPERTY_POSTAL_ADDRESS_OTHER_CITY", 0xDD30 }, + { "MTP_PROPERTY_POSTAL_ADDRESS_OTHER_REGION", 0xDD31 }, + { "MTP_PROPERTY_POSTAL_ADDRESS_OTHER_POSTAL_CODE", 0xDD32 }, + { "MTP_PROPERTY_POSTAL_ADDRESS_OTHER_COUNTRY", 0xDD33 }, + { "MTP_PROPERTY_ORGANIZATION_NAME", 0xDD34 }, + { "MTP_PROPERTY_PHONETIC_ORGANIZATION_NAME", 0xDD35 }, + { "MTP_PROPERTY_ROLE", 0xDD36 }, + { "MTP_PROPERTY_BIRTHDATE", 0xDD37 }, + { "MTP_PROPERTY_MESSAGE_TO", 0xDD40 }, + { "MTP_PROPERTY_MESSAGE_CC", 0xDD41 }, + { "MTP_PROPERTY_MESSAGE_BCC", 0xDD42 }, + { "MTP_PROPERTY_MESSAGE_READ", 0xDD43 }, + { "MTP_PROPERTY_MESSAGE_RECEIVED_TIME", 0xDD44 }, + { "MTP_PROPERTY_MESSAGE_SENDER", 0xDD45 }, + { "MTP_PROPERTY_ACTIVITY_BEGIN_TIME", 0xDD50 }, + { "MTP_PROPERTY_ACTIVITY_END_TIME", 0xDD51 }, + { "MTP_PROPERTY_ACTIVITY_LOCATION", 0xDD52 }, + { "MTP_PROPERTY_ACTIVITY_REQUIRED_ATTENDEES", 0xDD54 }, + { "MTP_PROPERTY_ACTIVITY_OPTIONAL_ATTENDEES", 0xDD55 }, + { "MTP_PROPERTY_ACTIVITY_RESOURCES", 0xDD56 }, + { "MTP_PROPERTY_ACTIVITY_ACCEPTED", 0xDD57 }, + { "MTP_PROPERTY_ACTIVITY_TENTATIVE", 0xDD58 }, + { "MTP_PROPERTY_ACTIVITY_DECLINED", 0xDD59 }, + { "MTP_PROPERTY_ACTIVITY_REMAINDER_TIME", 0xDD5A }, + { "MTP_PROPERTY_ACTIVITY_OWNER", 0xDD5B }, + { "MTP_PROPERTY_ACTIVITY_STATUS", 0xDD5C }, + { "MTP_PROPERTY_OWNER", 0xDD5D }, + { "MTP_PROPERTY_EDITOR", 0xDD5E }, + { "MTP_PROPERTY_WEBMASTER", 0xDD5F }, + { "MTP_PROPERTY_URL_SOURCE", 0xDD60 }, + { "MTP_PROPERTY_URL_DESTINATION", 0xDD61 }, + { "MTP_PROPERTY_TIME_BOOKMARK", 0xDD62 }, + { "MTP_PROPERTY_OBJECT_BOOKMARK", 0xDD63 }, + { "MTP_PROPERTY_BYTE_BOOKMARK", 0xDD64 }, + { "MTP_PROPERTY_LAST_BUILD_DATE", 0xDD70 }, + { "MTP_PROPERTY_TIME_TO_LIVE", 0xDD71 }, + { "MTP_PROPERTY_MEDIA_GUID", 0xDD72 }, + { 0, 0 }, +}; + +static const CodeEntry sDevicePropCodes[] = { + { "MTP_DEVICE_PROPERTY_UNDEFINED", 0x5000 }, + { "MTP_DEVICE_PROPERTY_BATTERY_LEVEL", 0x5001 }, + { "MTP_DEVICE_PROPERTY_FUNCTIONAL_MODE", 0x5002 }, + { "MTP_DEVICE_PROPERTY_IMAGE_SIZE", 0x5003 }, + { "MTP_DEVICE_PROPERTY_COMPRESSION_SETTING", 0x5004 }, + { "MTP_DEVICE_PROPERTY_WHITE_BALANCE", 0x5005 }, + { "MTP_DEVICE_PROPERTY_RGB_GAIN", 0x5006 }, + { "MTP_DEVICE_PROPERTY_F_NUMBER", 0x5007 }, + { "MTP_DEVICE_PROPERTY_FOCAL_LENGTH", 0x5008 }, + { "MTP_DEVICE_PROPERTY_FOCUS_DISTANCE", 0x5009 }, + { "MTP_DEVICE_PROPERTY_FOCUS_MODE", 0x500A }, + { "MTP_DEVICE_PROPERTY_EXPOSURE_METERING_MODE", 0x500B }, + { "MTP_DEVICE_PROPERTY_FLASH_MODE", 0x500C }, + { "MTP_DEVICE_PROPERTY_EXPOSURE_TIME", 0x500D }, + { "MTP_DEVICE_PROPERTY_EXPOSURE_PROGRAM_MODE", 0x500E }, + { "MTP_DEVICE_PROPERTY_EXPOSURE_INDEX", 0x500F }, + { "MTP_DEVICE_PROPERTY_EXPOSURE_BIAS_COMPENSATION", 0x5010 }, + { "MTP_DEVICE_PROPERTY_DATETIME", 0x5011 }, + { "MTP_DEVICE_PROPERTY_CAPTURE_DELAY", 0x5012 }, + { "MTP_DEVICE_PROPERTY_STILL_CAPTURE_MODE", 0x5013 }, + { "MTP_DEVICE_PROPERTY_CONTRAST", 0x5014 }, + { "MTP_DEVICE_PROPERTY_SHARPNESS", 0x5015 }, + { "MTP_DEVICE_PROPERTY_DIGITAL_ZOOM", 0x5016 }, + { "MTP_DEVICE_PROPERTY_EFFECT_MODE", 0x5017 }, + { "MTP_DEVICE_PROPERTY_BURST_NUMBER", 0x5018 }, + { "MTP_DEVICE_PROPERTY_BURST_INTERVAL", 0x5019 }, + { "MTP_DEVICE_PROPERTY_TIMELAPSE_NUMBER", 0x501A }, + { "MTP_DEVICE_PROPERTY_TIMELAPSE_INTERVAL", 0x501B }, + { "MTP_DEVICE_PROPERTY_FOCUS_METERING_MODE", 0x501C }, + { "MTP_DEVICE_PROPERTY_UPLOAD_URL", 0x501D }, + { "MTP_DEVICE_PROPERTY_ARTIST", 0x501E }, + { "MTP_DEVICE_PROPERTY_COPYRIGHT_INFO", 0x501F }, + { "MTP_DEVICE_PROPERTY_SYNCHRONIZATION_PARTNER", 0xD401 }, + { "MTP_DEVICE_PROPERTY_DEVICE_FRIENDLY_NAME", 0xD402 }, + { "MTP_DEVICE_PROPERTY_VOLUME", 0xD403 }, + { "MTP_DEVICE_PROPERTY_SUPPORTED_FORMATS_ORDERED", 0xD404 }, + { "MTP_DEVICE_PROPERTY_DEVICE_ICON", 0xD405 }, + { "MTP_DEVICE_PROPERTY_PLAYBACK_RATE", 0xD410 }, + { "MTP_DEVICE_PROPERTY_PLAYBACK_OBJECT", 0xD411 }, + { "MTP_DEVICE_PROPERTY_PLAYBACK_CONTAINER_INDEX", 0xD412 }, + { "MTP_DEVICE_PROPERTY_SESSION_INITIATOR_VERSION_INFO", 0xD406 }, + { "MTP_DEVICE_PROPERTY_PERCEIVED_DEVICE_TYPE", 0xD407 }, + { 0, 0 }, +}; + +static const char* getCodeName(uint16_t code, const CodeEntry* table) { + const CodeEntry* entry = table; + while (entry->name) { + if (entry->code == code) + return entry->name; + entry++; + } + return "UNKNOWN"; +} + +const char* MtpDebug::getOperationCodeName(MtpOperationCode code) { + return getCodeName(code, sOperationCodes); +} + +const char* MtpDebug::getFormatCodeName(MtpObjectFormat code) { + if (code == 0) + return "NONE"; + return getCodeName(code, sFormatCodes); +} + +const char* MtpDebug::getObjectPropCodeName(MtpPropertyCode code) { + if (code == 0) + return "NONE"; + return getCodeName(code, sObjectPropCodes); +} + +const char* MtpDebug::getDevicePropCodeName(MtpPropertyCode code) { + if (code == 0) + return "NONE"; + return getCodeName(code, sDevicePropCodes); +} + +} // namespace android diff --git a/src/ThirdParty/mtp-server-nx/source/MtpDeviceInfo.cpp b/src/ThirdParty/mtp-server-nx/source/MtpDeviceInfo.cpp new file mode 100644 index 0000000..30e2d54 --- /dev/null +++ b/src/ThirdParty/mtp-server-nx/source/MtpDeviceInfo.cpp @@ -0,0 +1,107 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#define LOG_TAG "MtpDeviceInfo" + +#include + +#include "MtpDebug.h" +#include "MtpDataPacket.h" +#include "MtpDeviceInfo.h" +#include "MtpStringBuffer.h" + +#include "log.h" + +namespace android { + +MtpDeviceInfo::MtpDeviceInfo() + : mStandardVersion(0), + mVendorExtensionID(0), + mVendorExtensionVersion(0), + mVendorExtensionDesc(NULL), + mFunctionalCode(0), + mOperations(NULL), + mEvents(NULL), + mDeviceProperties(NULL), + mCaptureFormats(NULL), + mPlaybackFormats(NULL), + mManufacturer(NULL), + mModel(NULL), + mVersion(NULL), + mSerial(NULL) +{ +} + +MtpDeviceInfo::~MtpDeviceInfo() { + if (mVendorExtensionDesc) + free(mVendorExtensionDesc); + delete mOperations; + delete mEvents; + delete mDeviceProperties; + delete mCaptureFormats; + delete mPlaybackFormats; + if (mManufacturer) + free(mManufacturer); + if (mModel) + free(mModel); + if (mVersion) + free(mVersion); + if (mSerial) + free(mSerial); +} + +void MtpDeviceInfo::read(MtpDataPacket& packet) { + MtpStringBuffer string; + + // read the device info + mStandardVersion = packet.getUInt16(); + mVendorExtensionID = packet.getUInt32(); + mVendorExtensionVersion = packet.getUInt16(); + + packet.getString(string); + mVendorExtensionDesc = strdup((const char *)string); + + mFunctionalCode = packet.getUInt16(); + mOperations = packet.getAUInt16(); + mEvents = packet.getAUInt16(); + mDeviceProperties = packet.getAUInt16(); + mCaptureFormats = packet.getAUInt16(); + mPlaybackFormats = packet.getAUInt16(); + + packet.getString(string); + mManufacturer = strdup((const char *)string); + packet.getString(string); + mModel = strdup((const char *)string); + packet.getString(string); + mVersion = strdup((const char *)string); + packet.getString(string); + mSerial = strdup((const char *)string); +} + +void MtpDeviceInfo::print() { + VLOG(2) << "Device Info:" + << "\n\tmStandardVersion: " << mStandardVersion + << "\n\tmVendorExtensionID: " << mVendorExtensionID + << "\n\tmVendorExtensionVersion: " << mVendorExtensionVersion + << "\n\tmVendorExtensionDesc: " << mVendorExtensionDesc + << "\n\tmFunctionalCode: " << mFunctionalCode + << "\n\tmManufacturer: " << mManufacturer + << "\n\tmModel: " << mModel + << "\n\tmVersion: " << mVersion + << "\n\tmSerial: " << mSerial; +} + +} // namespace android diff --git a/src/ThirdParty/mtp-server-nx/source/MtpEventPacket.cpp b/src/ThirdParty/mtp-server-nx/source/MtpEventPacket.cpp new file mode 100644 index 0000000..4015fd1 --- /dev/null +++ b/src/ThirdParty/mtp-server-nx/source/MtpEventPacket.cpp @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#define LOG_TAG "MtpEventPacket" + +#include + +#include +#include +#include + +#include "MtpEventPacket.h" + +namespace android { + +MtpEventPacket::MtpEventPacket() + : MtpPacket(512) +{ +} + +MtpEventPacket::~MtpEventPacket() { +} + +int MtpEventPacket::write(USBMtpInterface* usb) { + putUInt32(MTP_CONTAINER_LENGTH_OFFSET, mPacketSize); + putUInt16(MTP_CONTAINER_TYPE_OFFSET, MTP_CONTAINER_TYPE_EVENT); + + int ret = usb->sendEvent((const char*)mBuffer, mPacketSize); + return (ret < 0 ? ret : 0); +} + +} // namespace android diff --git a/src/ThirdParty/mtp-server-nx/source/MtpObjectInfo.cpp b/src/ThirdParty/mtp-server-nx/source/MtpObjectInfo.cpp new file mode 100644 index 0000000..a06b136 --- /dev/null +++ b/src/ThirdParty/mtp-server-nx/source/MtpObjectInfo.cpp @@ -0,0 +1,120 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#define LOG_TAG "MtpObjectInfo" + +#include +#include + +#include "MtpDebug.h" +#include "MtpDataPacket.h" +#include "MtpObjectInfo.h" +#include "MtpStringBuffer.h" +#include "MtpUtils.h" + +#include "log.h" + +namespace android { + +MtpObjectInfo::MtpObjectInfo(MtpObjectHandle handle) + : mHandle(handle), + mStorageID(0), + mFormat(0), + mProtectionStatus(0), + mCompressedSize(0), + mThumbFormat(0), + mThumbCompressedSize(0), + mThumbPixWidth(0), + mThumbPixHeight(0), + mImagePixWidth(0), + mImagePixHeight(0), + mImagePixDepth(0), + mParent(0), + mAssociationType(0), + mAssociationDesc(0), + mSequenceNumber(0), + mName(NULL), + mDateCreated(0), + mDateModified(0), + mKeywords(NULL) +{ +} + +MtpObjectInfo::~MtpObjectInfo() { + if (mName) + free(mName); + if (mKeywords) + free(mKeywords); +} + +void MtpObjectInfo::read(MtpDataPacket& packet) { + MtpStringBuffer string; + time_t time; + + mStorageID = packet.getUInt32(); + mFormat = packet.getUInt16(); + mProtectionStatus = packet.getUInt16(); + mCompressedSize = packet.getUInt32(); + mThumbFormat = packet.getUInt16(); + mThumbCompressedSize = packet.getUInt32(); + mThumbPixWidth = packet.getUInt32(); + mThumbPixHeight = packet.getUInt32(); + mImagePixWidth = packet.getUInt32(); + mImagePixHeight = packet.getUInt32(); + mImagePixDepth = packet.getUInt32(); + mParent = packet.getUInt32(); + mAssociationType = packet.getUInt16(); + mAssociationDesc = packet.getUInt32(); + mSequenceNumber = packet.getUInt32(); + + packet.getString(string); + mName = strdup((const char *)string); + + packet.getString(string); + if (parseDateTime((const char*)string, time)) + mDateCreated = time; + + packet.getString(string); + if (parseDateTime((const char*)string, time)) + mDateModified = time; + + packet.getString(string); + mKeywords = strdup((const char *)string); +} + +void MtpObjectInfo::print() { + VLOG(2) << "MtpObject Info " << mHandle << ": " << mName; + VLOG(2) << " mStorageID: " << std::hex << mStorageID + << " mFormat: " << mFormat << std::dec + << " mProtectionStatus: " << mProtectionStatus; + VLOG(2) << " mCompressedSize: " << mCompressedSize + << " mThumbFormat: " << std::hex << mThumbFormat << std::dec + << " mThumbCompressedSize: " << mThumbCompressedSize; + VLOG(2) << " mThumbPixWidth: " << mThumbPixWidth + << " mThumbPixHeight: " << mThumbPixHeight; + VLOG(2) << " mImagePixWidth: " << mImagePixWidth + << " mImagePixHeight: " << mImagePixHeight + << " mImagePixDepth: " << mImagePixDepth; + VLOG(2) << " mParent: " << std::hex << mParent + << " mAssociationType: " << mAssociationType << std::dec + << " mAssociationDesc: " << mAssociationDesc; + VLOG(2) << " mSequenceNumber: " << mSequenceNumber + << " mDateCreated: " << mDateCreated + << " mDateModified: " << mDateModified + << " mKeywords: " << mKeywords; +} + +} // namespace android diff --git a/src/ThirdParty/mtp-server-nx/source/MtpPacket.cpp b/src/ThirdParty/mtp-server-nx/source/MtpPacket.cpp new file mode 100644 index 0000000..0804cf5 --- /dev/null +++ b/src/ThirdParty/mtp-server-nx/source/MtpPacket.cpp @@ -0,0 +1,151 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#define LOG_TAG "MtpPacket" + +#include +#include +#include + +#include "MtpDebug.h" +#include "MtpPacket.h" +#include "mtp.h" + +#include "log.h" + +namespace android { + +MtpPacket::MtpPacket(int bufferSize) + : mBuffer(NULL), + mBufferSize(bufferSize), + mAllocationIncrement(bufferSize), + mPacketSize(0) +{ + mBuffer = (uint8_t *)malloc(bufferSize); + if (!mBuffer) { + LOG(FATAL) << "out of memory!"; + } +} + +MtpPacket::~MtpPacket() { + if (mBuffer) + free(mBuffer); +} + +void MtpPacket::reset() { + allocate(MTP_CONTAINER_HEADER_SIZE); + mPacketSize = MTP_CONTAINER_HEADER_SIZE; + memset(mBuffer, 0, MTP_CONTAINER_HEADER_SIZE); +} + +void MtpPacket::allocate(int length) { + if (length > mBufferSize) { + int newLength = length + mAllocationIncrement; + mBuffer = (uint8_t *)realloc(mBuffer, newLength); + if (!mBuffer) { + LOG(FATAL) << "out of memory!"; + } + mBufferSize = newLength; + } +} + +void MtpPacket::dump() { +#define DUMP_BYTES_PER_ROW 16 + char buffer[500]; + char* bufptr = buffer; + + for (int i = 0; i < mPacketSize; i++) { + sprintf(bufptr, "%02X ", mBuffer[i]); + bufptr += strlen(bufptr); + if (i % DUMP_BYTES_PER_ROW == (DUMP_BYTES_PER_ROW - 1)) { + VLOG(3) << buffer; + bufptr = buffer; + } + } + if (bufptr != buffer) { + // print last line + VLOG(3) << buffer; + } +} + +void MtpPacket::copyFrom(const MtpPacket& src) { + int length = src.mPacketSize; + allocate(length); + mPacketSize = length; + memcpy(mBuffer, src.mBuffer, length); +} + +uint16_t MtpPacket::getUInt16(int offset) const { + return ((uint16_t)mBuffer[offset + 1] << 8) | (uint16_t)mBuffer[offset]; +} + +uint32_t MtpPacket::getUInt32(int offset) const { + return ((uint32_t)mBuffer[offset + 3] << 24) | ((uint32_t)mBuffer[offset + 2] << 16) | + ((uint32_t)mBuffer[offset + 1] << 8) | (uint32_t)mBuffer[offset]; +} + +void MtpPacket::putUInt16(int offset, uint16_t value) { + mBuffer[offset++] = (uint8_t)(value & 0xFF); + mBuffer[offset++] = (uint8_t)((value >> 8) & 0xFF); +} + +void MtpPacket::putUInt32(int offset, uint32_t value) { + mBuffer[offset++] = (uint8_t)(value & 0xFF); + mBuffer[offset++] = (uint8_t)((value >> 8) & 0xFF); + mBuffer[offset++] = (uint8_t)((value >> 16) & 0xFF); + mBuffer[offset++] = (uint8_t)((value >> 24) & 0xFF); +} + +uint16_t MtpPacket::getContainerCode() const { + return getUInt16(MTP_CONTAINER_CODE_OFFSET); +} + +void MtpPacket::setContainerCode(uint16_t code) { + putUInt16(MTP_CONTAINER_CODE_OFFSET, code); +} + +uint16_t MtpPacket::getContainerType() const { + return getUInt16(MTP_CONTAINER_TYPE_OFFSET); +} + +MtpTransactionID MtpPacket::getTransactionID() const { + return getUInt32(MTP_CONTAINER_TRANSACTION_ID_OFFSET); +} + +void MtpPacket::setTransactionID(MtpTransactionID id) { + putUInt32(MTP_CONTAINER_TRANSACTION_ID_OFFSET, id); +} + +uint32_t MtpPacket::getParameter(int index) const { + if (index < 1 || index > 5) { + LOG(ERROR) << "index " << index << " out of range in MtpPacket::getParameter"; + return 0; + } + return getUInt32(MTP_CONTAINER_PARAMETER_OFFSET + (index - 1) * sizeof(uint32_t)); +} + +void MtpPacket::setParameter(int index, uint32_t value) { + if (index < 1 || index > 5) { + LOG(ERROR) << "index " << index << " out of range in MtpPacket::setParameter"; + return; + } + int offset = MTP_CONTAINER_PARAMETER_OFFSET + (index - 1) * sizeof(uint32_t); + if (mPacketSize < offset + sizeof(uint32_t)) + mPacketSize = offset + sizeof(uint32_t); + putUInt32(offset, value); +} + +} // namespace android diff --git a/src/ThirdParty/mtp-server-nx/source/MtpProperty.cpp b/src/ThirdParty/mtp-server-nx/source/MtpProperty.cpp new file mode 100644 index 0000000..c8f1f24 --- /dev/null +++ b/src/ThirdParty/mtp-server-nx/source/MtpProperty.cpp @@ -0,0 +1,555 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#define LOG_TAG "MtpProperty" + + +#include +#include +#include + +#include "MtpDataPacket.h" +#include "MtpDebug.h" +#include "MtpProperty.h" +#include "MtpStringBuffer.h" +#include "MtpUtils.h" + +#include "log.h" + +namespace android { + +MtpProperty::MtpProperty() + : mCode(0), + mType(0), + mWriteable(false), + mDefaultArrayLength(0), + mDefaultArrayValues(NULL), + mCurrentArrayLength(0), + mCurrentArrayValues(NULL), + mGroupCode(0), + mFormFlag(kFormNone), + mEnumLength(0), + mEnumValues(NULL) +{ + memset(&mDefaultValue, 0, sizeof(mDefaultValue)); + memset(&mCurrentValue, 0, sizeof(mCurrentValue)); + memset(&mMinimumValue, 0, sizeof(mMinimumValue)); + memset(&mMaximumValue, 0, sizeof(mMaximumValue)); +} + +MtpProperty::MtpProperty(MtpPropertyCode propCode, + MtpDataType type, + bool writeable, + int defaultValue) + : mCode(propCode), + mType(type), + mWriteable(writeable), + mDefaultArrayLength(0), + mDefaultArrayValues(NULL), + mCurrentArrayLength(0), + mCurrentArrayValues(NULL), + mGroupCode(0), + mFormFlag(kFormNone), + mEnumLength(0), + mEnumValues(NULL) +{ + memset(&mDefaultValue, 0, sizeof(mDefaultValue)); + memset(&mCurrentValue, 0, sizeof(mCurrentValue)); + memset(&mMinimumValue, 0, sizeof(mMinimumValue)); + memset(&mMaximumValue, 0, sizeof(mMaximumValue)); + + if (defaultValue) { + switch (type) { + case MTP_TYPE_INT8: + mDefaultValue.u.i8 = defaultValue; + break; + case MTP_TYPE_UINT8: + mDefaultValue.u.u8 = defaultValue; + break; + case MTP_TYPE_INT16: + mDefaultValue.u.i16 = defaultValue; + break; + case MTP_TYPE_UINT16: + mDefaultValue.u.u16 = defaultValue; + break; + case MTP_TYPE_INT32: + mDefaultValue.u.i32 = defaultValue; + break; + case MTP_TYPE_UINT32: + mDefaultValue.u.u32 = defaultValue; + break; + case MTP_TYPE_INT64: + mDefaultValue.u.i64 = defaultValue; + break; + case MTP_TYPE_UINT64: + mDefaultValue.u.u64 = defaultValue; + break; + default: + LOG(ERROR) << "unknown type " + << std::hex << type << std::dec + << " in MtpProperty::MtpProperty"; + } + } +} + +MtpProperty::~MtpProperty() { + if (mType == MTP_TYPE_STR) { + // free all strings + free(mDefaultValue.str); + free(mCurrentValue.str); + free(mMinimumValue.str); + free(mMaximumValue.str); + if (mDefaultArrayValues) { + for (int i = 0; i < mDefaultArrayLength; i++) + free(mDefaultArrayValues[i].str); + } + if (mCurrentArrayValues) { + for (int i = 0; i < mCurrentArrayLength; i++) + free(mCurrentArrayValues[i].str); + } + if (mEnumValues) { + for (int i = 0; i < mEnumLength; i++) + free(mEnumValues[i].str); + } + } + delete[] mDefaultArrayValues; + delete[] mCurrentArrayValues; + delete[] mEnumValues; +} + +void MtpProperty::read(MtpDataPacket& packet) { + mCode = packet.getUInt16(); + bool deviceProp = isDeviceProperty(); + mType = packet.getUInt16(); + mWriteable = (packet.getUInt8() == 1); + switch (mType) { + case MTP_TYPE_AINT8: + case MTP_TYPE_AUINT8: + case MTP_TYPE_AINT16: + case MTP_TYPE_AUINT16: + case MTP_TYPE_AINT32: + case MTP_TYPE_AUINT32: + case MTP_TYPE_AINT64: + case MTP_TYPE_AUINT64: + case MTP_TYPE_AINT128: + case MTP_TYPE_AUINT128: + mDefaultArrayValues = readArrayValues(packet, mDefaultArrayLength); + if (deviceProp) + mCurrentArrayValues = readArrayValues(packet, mCurrentArrayLength); + break; + default: + readValue(packet, mDefaultValue); + if (deviceProp) + readValue(packet, mCurrentValue); + } + if (!deviceProp) + mGroupCode = packet.getUInt32(); + mFormFlag = packet.getUInt8(); + + if (mFormFlag == kFormRange) { + readValue(packet, mMinimumValue); + readValue(packet, mMaximumValue); + readValue(packet, mStepSize); + } else if (mFormFlag == kFormEnum) { + mEnumLength = packet.getUInt16(); + mEnumValues = new MtpPropertyValue[mEnumLength]; + for (int i = 0; i < mEnumLength; i++) + readValue(packet, mEnumValues[i]); + } +} + +void MtpProperty::write(MtpDataPacket& packet) { + bool deviceProp = isDeviceProperty(); + + packet.putUInt16(mCode); + packet.putUInt16(mType); + packet.putUInt8(mWriteable ? 1 : 0); + + switch (mType) { + case MTP_TYPE_AINT8: + case MTP_TYPE_AUINT8: + case MTP_TYPE_AINT16: + case MTP_TYPE_AUINT16: + case MTP_TYPE_AINT32: + case MTP_TYPE_AUINT32: + case MTP_TYPE_AINT64: + case MTP_TYPE_AUINT64: + case MTP_TYPE_AINT128: + case MTP_TYPE_AUINT128: + writeArrayValues(packet, mDefaultArrayValues, mDefaultArrayLength); + if (deviceProp) + writeArrayValues(packet, mCurrentArrayValues, mCurrentArrayLength); + break; + default: + writeValue(packet, mDefaultValue); + if (deviceProp) + writeValue(packet, mCurrentValue); + } + packet.putUInt32(mGroupCode); + if (!deviceProp) + packet.putUInt8(mFormFlag); + if (mFormFlag == kFormRange) { + writeValue(packet, mMinimumValue); + writeValue(packet, mMaximumValue); + writeValue(packet, mStepSize); + } else if (mFormFlag == kFormEnum) { + packet.putUInt16(mEnumLength); + for (int i = 0; i < mEnumLength; i++) + writeValue(packet, mEnumValues[i]); + } +} + +void MtpProperty::setDefaultValue(const uint16_t* string) { + free(mDefaultValue.str); + if (string) { + MtpStringBuffer buffer(string); + mDefaultValue.str = strdup(buffer); + } + else + mDefaultValue.str = NULL; +} + +void MtpProperty::setCurrentValue(const uint16_t* string) { + free(mCurrentValue.str); + if (string) { + MtpStringBuffer buffer(string); + mCurrentValue.str = strdup(buffer); + } + else + mCurrentValue.str = NULL; +} + +void MtpProperty::setFormRange(int min, int max, int step) { + mFormFlag = kFormRange; + switch (mType) { + case MTP_TYPE_INT8: + mMinimumValue.u.i8 = min; + mMaximumValue.u.i8 = max; + mStepSize.u.i8 = step; + break; + case MTP_TYPE_UINT8: + mMinimumValue.u.u8 = min; + mMaximumValue.u.u8 = max; + mStepSize.u.u8 = step; + break; + case MTP_TYPE_INT16: + mMinimumValue.u.i16 = min; + mMaximumValue.u.i16 = max; + mStepSize.u.i16 = step; + break; + case MTP_TYPE_UINT16: + mMinimumValue.u.u16 = min; + mMaximumValue.u.u16 = max; + mStepSize.u.u16 = step; + break; + case MTP_TYPE_INT32: + mMinimumValue.u.i32 = min; + mMaximumValue.u.i32 = max; + mStepSize.u.i32 = step; + break; + case MTP_TYPE_UINT32: + mMinimumValue.u.u32 = min; + mMaximumValue.u.u32 = max; + mStepSize.u.u32 = step; + break; + case MTP_TYPE_INT64: + mMinimumValue.u.i64 = min; + mMaximumValue.u.i64 = max; + mStepSize.u.i64 = step; + break; + case MTP_TYPE_UINT64: + mMinimumValue.u.u64 = min; + mMaximumValue.u.u64 = max; + mStepSize.u.u64 = step; + break; + default: + LOG(ERROR) << "unsupported type for MtpProperty::setRange"; + break; + } +} + +void MtpProperty::setFormEnum(const int* values, int count) { + mFormFlag = kFormEnum; + delete[] mEnumValues; + mEnumValues = new MtpPropertyValue[count]; + mEnumLength = count; + + for (int i = 0; i < count; i++) { + int value = *values++; + switch (mType) { + case MTP_TYPE_INT8: + mEnumValues[i].u.i8 = value; + break; + case MTP_TYPE_UINT8: + mEnumValues[i].u.u8 = value; + break; + case MTP_TYPE_INT16: + mEnumValues[i].u.i16 = value; + break; + case MTP_TYPE_UINT16: + mEnumValues[i].u.u16 = value; + break; + case MTP_TYPE_INT32: + mEnumValues[i].u.i32 = value; + break; + case MTP_TYPE_UINT32: + mEnumValues[i].u.u32 = value; + break; + case MTP_TYPE_INT64: + mEnumValues[i].u.i64 = value; + break; + case MTP_TYPE_UINT64: + mEnumValues[i].u.u64 = value; + break; + default: + LOG(ERROR) << "unsupported type for MtpProperty::setEnum"; + break; + } + } +} + +void MtpProperty::setFormDateTime() { + mFormFlag = kFormDateTime; +} + +void MtpProperty::print() { + MtpString buffer; + bool deviceProp = isDeviceProperty(); + if (deviceProp) + VLOG(2) << MtpDebug::getDevicePropCodeName(mCode) + << " (" << std::hex << mCode << std::dec << ")"; + else + VLOG(2) << MtpDebug::getObjectPropCodeName(mCode) + << " (" << std::hex << mCode << std::dec << ")"; + VLOG(2) << mType; + VLOG(2) << "writeable " << (mWriteable ? "true" : "false"); + buffer = "default value: "; + print(mDefaultValue, buffer); + VLOG(2) << buffer.c_str(); + if (deviceProp) { + buffer = "current value: "; + print(mCurrentValue, buffer); + VLOG(2) << buffer.c_str(); + } + switch (mFormFlag) { + case kFormNone: + break; + case kFormRange: + buffer = "Range ("; + print(mMinimumValue, buffer); + buffer += ", "; + print(mMaximumValue, buffer); + buffer += ", "; + print(mStepSize, buffer); + buffer += ")"; + VLOG(2) << buffer.c_str(); + break; + case kFormEnum: + buffer = "Enum { "; + for (int i = 0; i < mEnumLength; i++) { + print(mEnumValues[i], buffer); + buffer += " "; + } + buffer += "}"; + VLOG(2) << buffer.c_str(); + break; + case kFormDateTime: + VLOG(2) << "DateTime"; + break; + default: + VLOG(2) << "form " << mFormFlag; + break; + } +} + +void MtpProperty::print(MtpPropertyValue& value, MtpString& buffer) { + std::stringstream ss; + switch (mType) { + case MTP_TYPE_INT8: + ss << value.u.i8; + break; + case MTP_TYPE_UINT8: + ss << value.u.u8; + break; + case MTP_TYPE_INT16: + ss << value.u.i16; + break; + case MTP_TYPE_UINT16: + ss << value.u.u16; + break; + case MTP_TYPE_INT32: + ss << value.u.i32; + break; + case MTP_TYPE_UINT32: + ss << value.u.u32; + break; + case MTP_TYPE_INT64: + ss << value.u.i64; + break; + case MTP_TYPE_UINT64: + ss << value.u.u64; + break; + + case MTP_TYPE_INT128: + ss << std::hex << value.u.i128[0] << std::hex << value.u.i128[1] << std::hex << value.u.i128[2] << std::hex << value.u.i128[3]; + //buffer.appendFormat("%08X%08X%08X%08X", value.u.i128[0], value.u.i128[1], + // value.u.i128[2], value.u.i128[3]); + break; + case MTP_TYPE_UINT128: + ss << std::hex << value.u.u128[0] << std::hex << value.u.u128[1] << std::hex << value.u.u128[2] << std::hex << value.u.u128[3]; + // buffer.appendFormat("%08X%08X%08X%08X", value.u.u128[0], value.u.u128[1], + // value.u.u128[2], value.u.u128[3]); + break; + case MTP_TYPE_STR: + ss << value.str; + break; + default: + LOG(ERROR) << "unsupported type for MtpProperty::print"; + break; + } + + buffer += ss.str(); +} + +void MtpProperty::readValue(MtpDataPacket& packet, MtpPropertyValue& value) { + MtpStringBuffer stringBuffer; + + switch (mType) { + case MTP_TYPE_INT8: + case MTP_TYPE_AINT8: + value.u.i8 = packet.getInt8(); + break; + case MTP_TYPE_UINT8: + case MTP_TYPE_AUINT8: + value.u.u8 = packet.getUInt8(); + break; + case MTP_TYPE_INT16: + case MTP_TYPE_AINT16: + value.u.i16 = packet.getInt16(); + break; + case MTP_TYPE_UINT16: + case MTP_TYPE_AUINT16: + value.u.u16 = packet.getUInt16(); + break; + case MTP_TYPE_INT32: + case MTP_TYPE_AINT32: + value.u.i32 = packet.getInt32(); + break; + case MTP_TYPE_UINT32: + case MTP_TYPE_AUINT32: + value.u.u32 = packet.getUInt32(); + break; + case MTP_TYPE_INT64: + case MTP_TYPE_AINT64: + value.u.i64 = packet.getInt64(); + break; + case MTP_TYPE_UINT64: + case MTP_TYPE_AUINT64: + value.u.u64 = packet.getUInt64(); + break; + case MTP_TYPE_INT128: + case MTP_TYPE_AINT128: + packet.getInt128(value.u.i128); + break; + case MTP_TYPE_UINT128: + case MTP_TYPE_AUINT128: + packet.getUInt128(value.u.u128); + break; + case MTP_TYPE_STR: + packet.getString(stringBuffer); + value.str = strdup(stringBuffer); + break; + default: + LOG(ERROR) << "unknown type " + << std::hex << mType << std::dec + << " in MtpProperty::readValue"; + } +} + +void MtpProperty::writeValue(MtpDataPacket& packet, MtpPropertyValue& value) { + MtpStringBuffer stringBuffer; + + switch (mType) { + case MTP_TYPE_INT8: + case MTP_TYPE_AINT8: + packet.putInt8(value.u.i8); + break; + case MTP_TYPE_UINT8: + case MTP_TYPE_AUINT8: + packet.putUInt8(value.u.u8); + break; + case MTP_TYPE_INT16: + case MTP_TYPE_AINT16: + packet.putInt16(value.u.i16); + break; + case MTP_TYPE_UINT16: + case MTP_TYPE_AUINT16: + packet.putUInt16(value.u.u16); + break; + case MTP_TYPE_INT32: + case MTP_TYPE_AINT32: + packet.putInt32(value.u.i32); + break; + case MTP_TYPE_UINT32: + case MTP_TYPE_AUINT32: + packet.putUInt32(value.u.u32); + break; + case MTP_TYPE_INT64: + case MTP_TYPE_AINT64: + packet.putInt64(value.u.i64); + break; + case MTP_TYPE_UINT64: + case MTP_TYPE_AUINT64: + packet.putUInt64(value.u.u64); + break; + case MTP_TYPE_INT128: + case MTP_TYPE_AINT128: + packet.putInt128(value.u.i128); + break; + case MTP_TYPE_UINT128: + case MTP_TYPE_AUINT128: + packet.putUInt128(value.u.u128); + break; + case MTP_TYPE_STR: + if (value.str) + packet.putString(value.str); + else + packet.putEmptyString(); + break; + default: + LOG(ERROR) << "unknown type " + << std::hex << mType << std::dec + << " in MtpProperty::writeValue"; + } +} + +MtpPropertyValue* MtpProperty::readArrayValues(MtpDataPacket& packet, int& length) { + length = packet.getUInt32(); + if (length == 0) + return NULL; + MtpPropertyValue* result = new MtpPropertyValue[length]; + for (int i = 0; i < length; i++) + readValue(packet, result[i]); + return result; +} + +void MtpProperty::writeArrayValues(MtpDataPacket& packet, MtpPropertyValue* values, int length) { + packet.putUInt32(length); + for (int i = 0; i < length; i++) + writeValue(packet, values[i]); +} + +} // namespace android diff --git a/src/ThirdParty/mtp-server-nx/source/MtpRequestPacket.cpp b/src/ThirdParty/mtp-server-nx/source/MtpRequestPacket.cpp new file mode 100644 index 0000000..48a3cde --- /dev/null +++ b/src/ThirdParty/mtp-server-nx/source/MtpRequestPacket.cpp @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#define LOG_TAG "MtpRequestPacket" + +#include +#include + +#include "MtpRequestPacket.h" + +namespace android { + +MtpRequestPacket::MtpRequestPacket() + : MtpPacket(512) +{ +} + +MtpRequestPacket::~MtpRequestPacket() { +} + +int MtpRequestPacket::read(USBMtpInterface* usb) { + int ret = usb->read((char*)mBuffer, mBufferSize); + if (ret >= MTP_CONTAINER_HEADER_SIZE) + mPacketSize = ret; + else { + mPacketSize = 0; + return -1; + } + return ret; +} + +} // namespace android diff --git a/src/ThirdParty/mtp-server-nx/source/MtpResponsePacket.cpp b/src/ThirdParty/mtp-server-nx/source/MtpResponsePacket.cpp new file mode 100644 index 0000000..8e04669 --- /dev/null +++ b/src/ThirdParty/mtp-server-nx/source/MtpResponsePacket.cpp @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#define LOG_TAG "MtpResponsePacket" + +#include +#include +#include +#include + +#include "MtpResponsePacket.h" + +namespace android { + +MtpResponsePacket::MtpResponsePacket() + : MtpPacket(512) +{ +} + +MtpResponsePacket::~MtpResponsePacket() { +} + +int MtpResponsePacket::write(USBMtpInterface* usb) { + putUInt32(MTP_CONTAINER_LENGTH_OFFSET, mPacketSize); + putUInt16(MTP_CONTAINER_TYPE_OFFSET, MTP_CONTAINER_TYPE_RESPONSE); + int ret = usb->write((const char*)mBuffer, mPacketSize); + return (ret < 0 ? ret : 0); +} + +} // namespace android diff --git a/src/ThirdParty/mtp-server-nx/source/MtpServer.cpp b/src/ThirdParty/mtp-server-nx/source/MtpServer.cpp new file mode 100644 index 0000000..de51316 --- /dev/null +++ b/src/ThirdParty/mtp-server-nx/source/MtpServer.cpp @@ -0,0 +1,1513 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include + +#define LOG_TAG "MtpServer" + +#include "MtpDebug.h" +#include "MtpDatabase.h" +#include "MtpObjectInfo.h" +#include "MtpProperty.h" +#include "MtpServer.h" +#include "MtpStorage.h" +#include "MtpStringBuffer.h" + +#include "log.h" + +namespace android { + +static const MtpOperationCode kSupportedOperationCodes[] = { + MTP_OPERATION_GET_DEVICE_INFO, + MTP_OPERATION_OPEN_SESSION, + MTP_OPERATION_CLOSE_SESSION, + MTP_OPERATION_GET_STORAGE_IDS, + MTP_OPERATION_GET_STORAGE_INFO, + MTP_OPERATION_GET_NUM_OBJECTS, + MTP_OPERATION_GET_OBJECT_HANDLES, + MTP_OPERATION_GET_OBJECT_INFO, + MTP_OPERATION_GET_OBJECT, + MTP_OPERATION_DELETE_OBJECT, + MTP_OPERATION_GET_PARTIAL_OBJECT, + MTP_OPERATION_SEND_OBJECT_INFO, + MTP_OPERATION_SEND_OBJECT, +// MTP_OPERATION_INITIATE_CAPTURE, +// MTP_OPERATION_FORMAT_STORE, +// MTP_OPERATION_RESET_DEVICE, +// MTP_OPERATION_SELF_TEST, +// MTP_OPERATION_SET_OBJECT_PROTECTION, +// MTP_OPERATION_POWER_DOWN, + MTP_OPERATION_GET_DEVICE_PROP_DESC, + MTP_OPERATION_GET_DEVICE_PROP_VALUE, + MTP_OPERATION_SET_DEVICE_PROP_VALUE, + MTP_OPERATION_RESET_DEVICE_PROP_VALUE, +// MTP_OPERATION_TERMINATE_OPEN_CAPTURE, + MTP_OPERATION_MOVE_OBJECT, +// MTP_OPERATION_COPY_OBJECT, +// MTP_OPERATION_INITIATE_OPEN_CAPTURE, + MTP_OPERATION_GET_OBJECT_PROPS_SUPPORTED, + MTP_OPERATION_GET_OBJECT_PROP_DESC, + MTP_OPERATION_GET_OBJECT_PROP_VALUE, + MTP_OPERATION_SET_OBJECT_PROP_VALUE, + MTP_OPERATION_GET_OBJECT_PROP_LIST, +// MTP_OPERATION_SET_OBJECT_PROP_LIST, +// MTP_OPERATION_GET_INTERDEPENDENT_PROP_DESC, +// MTP_OPERATION_SEND_OBJECT_PROP_LIST, +// MTP_OPERATION_GET_OBJECT_REFERENCES, +// MTP_OPERATION_SET_OBJECT_REFERENCES, +// MTP_OPERATION_SKIP, + // Android extension for direct file IO + MTP_OPERATION_GET_PARTIAL_OBJECT_64, + MTP_OPERATION_SEND_PARTIAL_OBJECT, + MTP_OPERATION_TRUNCATE_OBJECT, + MTP_OPERATION_BEGIN_EDIT_OBJECT, + MTP_OPERATION_END_EDIT_OBJECT, +}; + +static constexpr size_t kMtpTransferChunkSize = 1024 * 1024; +// Keep the first SendObject read tiny. Windows can start the data phase slowly +// and asking for too much here leaves Explorer waiting at 0% on some transfers. +static constexpr uint32_t kMtpInitialObjectReadSize = 512; +static constexpr uint64_t kMtpDataPhaseTimeoutNs = 10ULL * 1000ULL * 1000ULL * 1000ULL; + +static unsigned char *getReusableTransferBuffer(size_t size) +{ + static unsigned char *buffer = nullptr; + static size_t bufferSize = 0; + if (buffer == nullptr || bufferSize < size) { + if (buffer != nullptr) + free(buffer); + buffer = (unsigned char*)memalign(0x1000, size); + bufferSize = (buffer != nullptr) ? size : 0; + } + return buffer; +} + +static bool isBlockedReadPath(const char* path) { + if (path == nullptr) { + return false; + } + + std::string lower(path); + std::transform(lower.begin(), lower.end(), lower.begin(), + [](unsigned char c) { return static_cast(std::tolower(c)); }); + + static const char* kBlockedExts[] = { + ".nro", ".nso", ".exe", ".dll", ".com", ".bat", ".cmd", ".scr", ".msi", ".lnk", ".js", ".vbs" + }; + + for (const char* ext : kBlockedExts) { + const size_t extLen = std::strlen(ext); + if (lower.size() >= extLen && lower.compare(lower.size() - extLen, extLen, ext) == 0) { + return true; + } + } + return false; +} + +static const MtpEventCode kSupportedEventCodes[] = { + MTP_EVENT_OBJECT_ADDED, + MTP_EVENT_OBJECT_REMOVED, + MTP_EVENT_STORE_ADDED, + MTP_EVENT_STORE_REMOVED, + MTP_EVENT_OBJECT_INFO_CHANGED, + MTP_EVENT_OBJECT_PROP_CHANGED, +}; + +MtpServer::MtpServer(USBMtpInterface* usb, MtpDatabase* database, bool ptp, + int fileGroup, int filePerm, int directoryPerm) + : mUSB(usb), + mDatabase(database), + mRunning(false), + mStopRequested(false), + mPtp(ptp), + mFileGroup(fileGroup), + mFilePermission(filePerm), + mDirectoryPermission(directoryPerm), + mSessionID(0), + mSessionOpen(false), + mSendObjectHandle(kInvalidObjectHandle), + mSendObjectFormat(0), + mSendObjectFileSize(0) +{ +} + +MtpServer::~MtpServer() { +} + +void MtpServer::addStorage(MtpStorage* storage) { + MtpAutolock autoLock(mMutex); + + mStorages.push_back(storage); + sendStoreAdded(storage->getStorageID()); +} + +void MtpServer::removeStorage(MtpStorage* storage) { + MtpAutolock autoLock(mMutex); + + for (int i = 0; i < mStorages.size(); i++) { + if (mStorages[i] == storage) { + mStorages.erase(mStorages.begin()+i); + sendStoreRemoved(storage->getStorageID()); + break; + } + } +} + +MtpStorage* MtpServer::getStorage(MtpStorageID id) { + if (id == 0) + return mStorages[0]; + for (int i = 0; i < mStorages.size(); i++) { + MtpStorage* storage = mStorages[i]; + if (storage->getStorageID() == id) + return storage; + } + return NULL; +} + +bool MtpServer::hasStorage(MtpStorageID id) { + if (id == 0 || id == 0xFFFFFFFF) + return mStorages.size() > 0; + return (getStorage(id) != NULL); +} + +void MtpServer::stop() { + mStopRequested.store(true); + mRunning.store(false); +} + +void MtpServer::run() { + USBMtpInterface* usb = mUSB; + + VLOG(1) << "MtpServer::run"; + + if (mStopRequested.load()) { + mRunning.store(false); + return; + } + + bool wasConfigured = false; + auto refreshUsbState = [&wasConfigured]() { + UsbState st = UsbState_Detached; + if (R_SUCCEEDED(usbDsGetState(&st))) { + if (st == UsbState_Configured) { + wasConfigured = true; + } + return st; + } + return UsbState_Detached; + }; + auto shouldExitAfterIoError = [&]() { + if (!mRunning.load() || mStopRequested.load()) { + return true; + } + UsbState st = refreshUsbState(); + return wasConfigured && st != UsbState_Configured; + }; + + mRunning.store(true); + while (mRunning.load() && !mStopRequested.load()) { + refreshUsbState(); + + int ret = mRequest.read(usb); + if (ret < 0) { + VLOG(2) << "request read returned " << ret; + if (shouldExitAfterIoError()) { + break; + } + svcSleepThread(20'000'000); // 20ms backoff + continue; + } + MtpOperationCode operation = mRequest.getOperationCode(); + MtpTransactionID transaction = mRequest.getTransactionID(); + + VLOG(2) << "operation: " << MtpDebug::getOperationCodeName(operation); + mRequest.dump(); + + // FIXME need to generalize this + bool dataIn = (operation == MTP_OPERATION_SEND_OBJECT_INFO + || operation == MTP_OPERATION_SET_OBJECT_REFERENCES + || operation == MTP_OPERATION_SET_OBJECT_PROP_VALUE + || operation == MTP_OPERATION_SET_DEVICE_PROP_VALUE); + if (dataIn) { + int ret = mData.readWithTimeout(usb, kMtpDataPhaseTimeoutNs); + if (ret < 0) { + VLOG(2) << "data read returned " << ret; + continue; + } + VLOG(2) << "received data:"; + mData.dump(); + } else { + mData.reset(); + } + + if (handleRequest()) { + if (!dataIn && mData.hasData()) { + mData.setOperationCode(operation); + mData.setTransactionID(transaction); + VLOG(2) << "sending data:"; + mData.dump(); + ret = mData.write(usb); + if (ret < 0) { + VLOG(2) << "request write returned " << ret; + if (shouldExitAfterIoError()) { + break; + } + continue; + } + } + + mResponse.setTransactionID(transaction); + VLOG(2) << "sending response " + << std::hex << mResponse.getResponseCode() << std::dec; + ret = mResponse.write(usb); + mResponse.dump(); + if (ret < 0) { + VLOG(2) << "request write returned " << ret; + if (shouldExitAfterIoError()) { + break; + } + continue; + } + } else { + VLOG(2) << "skipping response"; + } + } + + // commit any open edits + int count = mObjectEditList.size(); + for (int i = 0; i < count; i++) { + ObjectEdit* edit = mObjectEditList[i]; + commitEdit(edit); + delete edit; + } + mObjectEditList.clear(); + + if (mSessionOpen) + mDatabase->sessionEnded(); + mUSB = NULL; + mRunning.store(false); +} + +void MtpServer::sendObjectAdded(MtpObjectHandle handle) { + VLOG(1) << "sendObjectAdded " << handle; + sendEvent(MTP_EVENT_OBJECT_ADDED, handle, 0, 0); +} + +void MtpServer::sendObjectRemoved(MtpObjectHandle handle) { + VLOG(1) << "sendObjectRemoved " << handle; + sendEvent(MTP_EVENT_OBJECT_REMOVED, handle, 0, 0); +} + +void MtpServer::sendObjectInfoChanged(MtpObjectHandle handle) { + VLOG(1) << "sendObjectInfoChanged " << handle; + sendEvent(MTP_EVENT_OBJECT_INFO_CHANGED, handle, 0, 0); +} + +void MtpServer::sendObjectPropChanged(MtpObjectHandle handle, + MtpObjectProperty prop) { + VLOG(1) << "sendObjectPropChanged " << handle << " " << prop; + sendEvent(MTP_EVENT_OBJECT_PROP_CHANGED, handle, prop, 0); +} + +void MtpServer::sendStoreAdded(MtpStorageID id) { + VLOG(1) << "sendStoreAdded " << std::hex << id << std::dec; + sendEvent(MTP_EVENT_STORE_ADDED, id, 0, 0); +} + +void MtpServer::sendStoreRemoved(MtpStorageID id) { + VLOG(1) << "sendStoreRemoved " << std::hex << id << std::dec; + sendEvent(MTP_EVENT_STORE_REMOVED, id, 0, 0); +} + +void MtpServer::sendEvent(MtpEventCode code, + uint32_t param1, + uint32_t param2, + uint32_t param3) { + if (mSessionOpen) { + mEvent.setEventCode(code); + mEvent.setTransactionID(mRequest.getTransactionID()); + mEvent.setParameter(1, param1); + mEvent.setParameter(2, param2); + mEvent.setParameter(3, param3); + int ret = mEvent.write(mUSB); + VLOG(2) << "mEvent.write returned " << ret; + } +} + +void MtpServer::addEditObject(MtpObjectHandle handle, MtpString& path, + uint64_t size, MtpObjectFormat format, int fd) { + ObjectEdit* edit = new ObjectEdit(handle, path, size, format, fd); + mObjectEditList.push_back(edit); +} + +MtpServer::ObjectEdit* MtpServer::getEditObject(MtpObjectHandle handle) { + int count = mObjectEditList.size(); + for (int i = 0; i < count; i++) { + ObjectEdit* edit = mObjectEditList[i]; + if (edit->mHandle == handle) return edit; + } + return NULL; +} + +void MtpServer::removeEditObject(MtpObjectHandle handle) { + int count = mObjectEditList.size(); + for (int i = 0; i < count; i++) { + ObjectEdit* edit = mObjectEditList[i]; + if (edit->mHandle == handle) { + delete edit; + mObjectEditList.erase(mObjectEditList.begin() + i); + return; + } + } + LOG(ERROR) << "ObjectEdit not found in removeEditObject"; +} + +void MtpServer::commitEdit(ObjectEdit* edit) { + mDatabase->endSendObject(edit->mPath.c_str(), edit->mHandle, edit->mFormat, true); +} + + +bool MtpServer::handleRequest() { + MtpAutolock autoLock(mMutex); + + MtpOperationCode operation = mRequest.getOperationCode(); + MtpResponseCode response; + + mResponse.reset(); + + if (mSendObjectHandle != kInvalidObjectHandle && operation != MTP_OPERATION_SEND_OBJECT) { + // FIXME - need to delete mSendObjectHandle from the database + LOG(ERROR) << "expected SendObject after SendObjectInfo"; + mSendObjectHandle = kInvalidObjectHandle; + } + + switch (operation) { + case MTP_OPERATION_GET_DEVICE_INFO: + response = doGetDeviceInfo(); + break; + case MTP_OPERATION_OPEN_SESSION: + response = doOpenSession(); + break; + case MTP_OPERATION_CLOSE_SESSION: + response = doCloseSession(); + break; + case MTP_OPERATION_GET_STORAGE_IDS: + response = doGetStorageIDs(); + break; + case MTP_OPERATION_GET_STORAGE_INFO: + response = doGetStorageInfo(); + break; + case MTP_OPERATION_GET_OBJECT_PROPS_SUPPORTED: + response = doGetObjectPropsSupported(); + break; + case MTP_OPERATION_GET_OBJECT_HANDLES: + response = doGetObjectHandles(); + break; + case MTP_OPERATION_GET_NUM_OBJECTS: + response = doGetNumObjects(); + break; + case MTP_OPERATION_GET_OBJECT_REFERENCES: + response = doGetObjectReferences(); + break; + case MTP_OPERATION_SET_OBJECT_REFERENCES: + response = doSetObjectReferences(); + break; + case MTP_OPERATION_GET_OBJECT_PROP_VALUE: + response = doGetObjectPropValue(); + break; + case MTP_OPERATION_SET_OBJECT_PROP_VALUE: + response = doSetObjectPropValue(); + break; + case MTP_OPERATION_GET_DEVICE_PROP_VALUE: + response = doGetDevicePropValue(); + break; + case MTP_OPERATION_SET_DEVICE_PROP_VALUE: + response = doSetDevicePropValue(); + break; + case MTP_OPERATION_RESET_DEVICE_PROP_VALUE: + response = doResetDevicePropValue(); + break; + case MTP_OPERATION_GET_OBJECT_PROP_LIST: + response = doGetObjectPropList(); + break; + case MTP_OPERATION_GET_OBJECT_INFO: + response = doGetObjectInfo(); + break; + case MTP_OPERATION_GET_OBJECT: + response = doGetObject(); + break; + case MTP_OPERATION_GET_THUMB: + response = doGetThumb(); + break; + case MTP_OPERATION_GET_PARTIAL_OBJECT: + case MTP_OPERATION_GET_PARTIAL_OBJECT_64: + response = doGetPartialObject(operation); + break; + case MTP_OPERATION_SEND_OBJECT_INFO: + response = doSendObjectInfo(); + break; + case MTP_OPERATION_SEND_OBJECT: + response = doSendObject(); + break; + case MTP_OPERATION_DELETE_OBJECT: + response = doDeleteObject(); + break; + case MTP_OPERATION_MOVE_OBJECT: + response = doMoveObject(); + break; + case MTP_OPERATION_GET_OBJECT_PROP_DESC: + response = doGetObjectPropDesc(); + break; + case MTP_OPERATION_GET_DEVICE_PROP_DESC: + response = doGetDevicePropDesc(); + break; + case MTP_OPERATION_SEND_PARTIAL_OBJECT: + response = doSendPartialObject(); + break; + case MTP_OPERATION_TRUNCATE_OBJECT: + response = doTruncateObject(); + break; + case MTP_OPERATION_BEGIN_EDIT_OBJECT: + response = doBeginEditObject(); + break; + case MTP_OPERATION_END_EDIT_OBJECT: + response = doEndEditObject(); + break; + default: + LOG(ERROR) << "got unsupported command " << MtpDebug::getOperationCodeName(operation); + response = MTP_RESPONSE_OPERATION_NOT_SUPPORTED; + break; + } + + mResponse.setResponseCode(response); + return true; +} + +MtpResponseCode MtpServer::doGetDeviceInfo() { + VLOG(1) << __PRETTY_FUNCTION__; + MtpStringBuffer string; + //char prop_value[PROP_VALUE_MAX]; + + MtpObjectFormatList* playbackFormats = mDatabase->getSupportedPlaybackFormats(); + MtpObjectFormatList* captureFormats = mDatabase->getSupportedCaptureFormats(); + MtpDevicePropertyList* deviceProperties = mDatabase->getSupportedDeviceProperties(); + + // fill in device info + mData.putUInt16(MTP_STANDARD_VERSION); + if (mPtp) { + mData.putUInt32(0); + } else { + // MTP Vendor Extension ID + mData.putUInt32(6); + } + mData.putUInt16(MTP_STANDARD_VERSION); + if (mPtp) { + // no extensions + string.set(""); + } else { + // MTP extensions + string.set("microsoft.com: 1.0; android.com: 1.0;"); + } + mData.putString(string); // MTP Extensions + mData.putUInt16(0); //Functional Mode + mData.putAUInt16(kSupportedOperationCodes, + sizeof(kSupportedOperationCodes) / sizeof(uint16_t)); // Operations Supported + mData.putAUInt16(kSupportedEventCodes, + sizeof(kSupportedEventCodes) / sizeof(uint16_t)); // Events Supported + mData.putAUInt16(deviceProperties); // Device Properties Supported + mData.putAUInt16(captureFormats); // Capture Formats + mData.putAUInt16(playbackFormats); // Playback Formats + + //property_get("ro.product.manufacturer", prop_value, "unknown manufacturer"); + string.set("Simple Mod Manager"); + mData.putString(string); // Manufacturer + + //property_get("ro.product.model", prop_value, "MTP Device"); + string.set("Simple Mod Manager MTP"); + mData.putString(string); // Model + string.set("1.0"); + mData.putString(string); // Device Version + + //property_get("ro.serialno", prop_value, "????????"); + string.set("SMMTP001"); + mData.putString(string); // Serial Number + + delete playbackFormats; + delete captureFormats; + delete deviceProperties; + + return MTP_RESPONSE_OK; +} + +MtpResponseCode MtpServer::doOpenSession() { + if (mSessionOpen) { + mResponse.setParameter(1, mSessionID); + return MTP_RESPONSE_SESSION_ALREADY_OPEN; + } + mSessionID = mRequest.getParameter(1); + mSessionOpen = true; + + mDatabase->sessionStarted(this); + + return MTP_RESPONSE_OK; +} + +MtpResponseCode MtpServer::doCloseSession() { + if (!mSessionOpen) + return MTP_RESPONSE_SESSION_NOT_OPEN; + mSessionID = 0; + mSessionOpen = false; + mDatabase->sessionEnded(); + return MTP_RESPONSE_OK; +} + +MtpResponseCode MtpServer::doGetStorageIDs() { + if (!mSessionOpen) + return MTP_RESPONSE_SESSION_NOT_OPEN; + + int count = mStorages.size(); + mData.putUInt32(count); + for (int i = 0; i < count; i++) + mData.putUInt32(mStorages[i]->getStorageID()); + + return MTP_RESPONSE_OK; +} + +MtpResponseCode MtpServer::doGetStorageInfo() { + MtpStringBuffer string; + + if (!mSessionOpen) + return MTP_RESPONSE_SESSION_NOT_OPEN; + MtpStorageID id = mRequest.getParameter(1); + MtpStorage* storage = getStorage(id); + if (!storage) + return MTP_RESPONSE_INVALID_STORAGE_ID; + + mData.putUInt16(storage->getType()); + mData.putUInt16(storage->getFileSystemType()); + mData.putUInt16(storage->getAccessCapability()); + mData.putUInt64(storage->getMaxCapacity()); + mData.putUInt64(storage->getFreeSpace()); + mData.putUInt32(1024*1024*1024); // Free Space in Objects + string.set(storage->getDescription()); + mData.putString(string); + mData.putEmptyString(); // Volume Identifier + + return MTP_RESPONSE_OK; +} + +MtpResponseCode MtpServer::doGetObjectPropsSupported() { + if (!mSessionOpen) + return MTP_RESPONSE_SESSION_NOT_OPEN; + MtpObjectFormat format = mRequest.getParameter(1); + MtpObjectPropertyList* properties = mDatabase->getSupportedObjectProperties(format); + mData.putAUInt16(properties); + delete properties; + return MTP_RESPONSE_OK; +} + +MtpResponseCode MtpServer::doGetObjectHandles() { + if (!mSessionOpen) + return MTP_RESPONSE_SESSION_NOT_OPEN; + MtpStorageID storageID = mRequest.getParameter(1); // 0xFFFFFFFF for all storage + MtpObjectFormat format = mRequest.getParameter(2); // 0 for all formats + MtpObjectHandle parent = mRequest.getParameter(3); // 0xFFFFFFFF for objects with no parent + // 0x00000000 for all objects + + if (!hasStorage(storageID)) + return MTP_RESPONSE_INVALID_STORAGE_ID; + + MtpObjectHandleList* handles = mDatabase->getObjectList(storageID, format, parent); + mData.putAUInt32(handles); + delete handles; + return MTP_RESPONSE_OK; +} + +MtpResponseCode MtpServer::doGetNumObjects() { + if (!mSessionOpen) + return MTP_RESPONSE_SESSION_NOT_OPEN; + MtpStorageID storageID = mRequest.getParameter(1); // 0xFFFFFFFF for all storage + MtpObjectFormat format = mRequest.getParameter(2); // 0 for all formats + MtpObjectHandle parent = mRequest.getParameter(3); // 0xFFFFFFFF for objects with no parent + // 0x00000000 for all objects + if (!hasStorage(storageID)) + return MTP_RESPONSE_INVALID_STORAGE_ID; + + int count = mDatabase->getNumObjects(storageID, format, parent); + if (count >= 0) { + mResponse.setParameter(1, count); + return MTP_RESPONSE_OK; + } else { + mResponse.setParameter(1, 0); + return MTP_RESPONSE_INVALID_OBJECT_HANDLE; + } +} + +MtpResponseCode MtpServer::doGetObjectReferences() { + if (!mSessionOpen) + return MTP_RESPONSE_SESSION_NOT_OPEN; + if (!hasStorage()) + return MTP_RESPONSE_INVALID_OBJECT_HANDLE; + MtpObjectHandle handle = mRequest.getParameter(1); + + if (!mDatabase->isHandleValid(handle)) { + return MTP_RESPONSE_INVALID_OBJECT_HANDLE; + } + + MtpObjectHandleList* handles = mDatabase->getObjectReferences(handle); + if (handles) { + mData.putAUInt32(handles); + delete handles; + } else { + mData.putEmptyArray(); + } + return MTP_RESPONSE_OK; +} + +MtpResponseCode MtpServer::doSetObjectReferences() { + if (!mSessionOpen) + return MTP_RESPONSE_SESSION_NOT_OPEN; + if (!hasStorage()) + return MTP_RESPONSE_INVALID_OBJECT_HANDLE; + MtpStorageID handle = mRequest.getParameter(1); + + MtpObjectHandleList* references = mData.getAUInt32(); + MtpResponseCode result = mDatabase->setObjectReferences(handle, references); + delete references; + return result; +} + +MtpResponseCode MtpServer::doGetObjectPropValue() { + if (!hasStorage()) + return MTP_RESPONSE_INVALID_OBJECT_HANDLE; + MtpObjectHandle handle = mRequest.getParameter(1); + MtpObjectProperty property = mRequest.getParameter(2); + VLOG(2) << "GetObjectPropValue " << handle + << " " << MtpDebug::getObjectPropCodeName(property); + + return mDatabase->getObjectPropertyValue(handle, property, mData); +} + +MtpResponseCode MtpServer::doSetObjectPropValue() { + MtpResponseCode response; + + if (!hasStorage()) + return MTP_RESPONSE_INVALID_OBJECT_HANDLE; + MtpObjectHandle handle = mRequest.getParameter(1); + MtpObjectProperty property = mRequest.getParameter(2); + VLOG(2) << "SetObjectPropValue " << handle + << " " << MtpDebug::getObjectPropCodeName(property); + + response = mDatabase->setObjectPropertyValue(handle, property, mData); + + //sendObjectPropChanged(handle, property); + + return response; +} + +MtpResponseCode MtpServer::doGetDevicePropValue() { + MtpDeviceProperty property = mRequest.getParameter(1); + VLOG(1) << "GetDevicePropValue " << MtpDebug::getDevicePropCodeName(property); + + return mDatabase->getDevicePropertyValue(property, mData); +} + +MtpResponseCode MtpServer::doSetDevicePropValue() { + MtpDeviceProperty property = mRequest.getParameter(1); + VLOG(1) << "SetDevicePropValue " << MtpDebug::getDevicePropCodeName(property); + + return mDatabase->setDevicePropertyValue(property, mData); +} + +MtpResponseCode MtpServer::doResetDevicePropValue() { + MtpDeviceProperty property = mRequest.getParameter(1); + VLOG(1) << "ResetDevicePropValue " << MtpDebug::getDevicePropCodeName(property); + + return mDatabase->resetDeviceProperty(property); +} + +MtpResponseCode MtpServer::doGetObjectPropList() { + if (!hasStorage()) + return MTP_RESPONSE_INVALID_OBJECT_HANDLE; + + MtpObjectHandle handle = mRequest.getParameter(1); + // use uint32_t so we can support 0xFFFFFFFF + uint32_t format = mRequest.getParameter(2); + uint32_t property = mRequest.getParameter(3); + int groupCode = mRequest.getParameter(4); + int depth = mRequest.getParameter(5); + VLOG(2) << "GetObjectPropList " << handle + << " format: " << MtpDebug::getFormatCodeName(format) + << " property: " << MtpDebug::getObjectPropCodeName(property) + << " group: " << groupCode + << " depth: " << depth; + + return mDatabase->getObjectPropertyList(handle, format, property, groupCode, depth, mData); +} + +MtpResponseCode MtpServer::doGetObjectInfo() { + if (!hasStorage()) + return MTP_RESPONSE_INVALID_OBJECT_HANDLE; + MtpObjectHandle handle = mRequest.getParameter(1); + MtpObjectInfo info(handle); + MtpResponseCode result = mDatabase->getObjectInfo(handle, info); + if (result == MTP_RESPONSE_OK) { + char date[20]; + + mData.putUInt32(info.mStorageID); + mData.putUInt16(info.mFormat); + mData.putUInt16(info.mProtectionStatus); + + // if object is being edited the database size may be out of date + uint32_t size = info.mCompressedSize; + ObjectEdit* edit = getEditObject(handle); + if (edit) + size = (edit->mSize > 0xFFFFFFFFLL ? 0xFFFFFFFF : (uint32_t)edit->mSize); + mData.putUInt32(size); + + mData.putUInt16(info.mThumbFormat); + mData.putUInt32(info.mThumbCompressedSize); + mData.putUInt32(info.mThumbPixWidth); + mData.putUInt32(info.mThumbPixHeight); + mData.putUInt32(info.mImagePixWidth); + mData.putUInt32(info.mImagePixHeight); + mData.putUInt32(info.mImagePixDepth); + mData.putUInt32(info.mParent); + mData.putUInt16(info.mAssociationType); + mData.putUInt32(info.mAssociationDesc); + mData.putUInt32(info.mSequenceNumber); + mData.putString(info.mName); + mData.putEmptyString(); // date created + formatDateTime(info.mDateModified, date, sizeof(date)); + mData.putString(date); // date modified + mData.putEmptyString(); // keywords + } + return result; +} + +struct mtp_file_range { + int fd; + off_t offset; + int64_t length; + uint16_t command; + uint32_t transaction_id; +}; + +static int send_file(USBMtpInterface* usb, struct mtp_file_range * mfr) +{ + int actualsize; + int j, ofs; + int blocksize; + + struct stat buf; + fstat(mfr->fd, &buf); + + unsigned char * buffer = getReusableTransferBuffer(kMtpTransferChunkSize + MTP_CONTAINER_HEADER_SIZE); + if (buffer == nullptr) + return -1; + *(uint32_t*)&buffer[0] = mfr->length + MTP_CONTAINER_HEADER_SIZE; + *(uint16_t*)&buffer[4] = MTP_CONTAINER_TYPE_DATA; + *(uint16_t*)&buffer[6] = mfr->command; + *(uint32_t*)&buffer[8] = mfr->transaction_id; + + if(mfr->offset >= buf.st_size) + { + actualsize = 0; + } + else + { + if(mfr->offset + mfr->length > buf.st_size) + actualsize = buf.st_size - mfr->offset; + else + actualsize = mfr->length; + } + + lseek(mfr->fd, mfr->offset, SEEK_SET); + ofs = MTP_CONTAINER_HEADER_SIZE; + j = 0; + do + { + if((j + (kMtpTransferChunkSize - ofs)) < actualsize) + blocksize = (kMtpTransferChunkSize - ofs); + else + blocksize = actualsize - j; + + read(mfr->fd, &buffer[ofs], blocksize); + j += blocksize; + ofs += blocksize; + + usb->write((const char*)buffer, ofs); + ofs = 0; + } while(j < actualsize); + + return actualsize; +} + +static bool writeFully(int fd, const uint8_t* buffer, size_t size) +{ + size_t total = 0; + + while (total < size) { + ssize_t written = write(fd, buffer + total, size - total); + if (written < 0) { + return false; + } + if (written == 0) { + errno = EIO; + return false; + } + total += static_cast(written); + } + + return true; +} + +static uint32_t getInitialObjectReadSize(uint64_t payloadSize) +{ + uint64_t desired = kMtpInitialObjectReadSize; + if (payloadSize != 0xFFFFFFFFULL) { + desired = payloadSize + MTP_CONTAINER_HEADER_SIZE; + if (desired < MTP_CONTAINER_HEADER_SIZE) + desired = MTP_CONTAINER_HEADER_SIZE; + if (desired > kMtpInitialObjectReadSize) + desired = kMtpInitialObjectReadSize; + } + return static_cast(desired); +} + +static int64_t receive_file(USBMtpInterface* usb, struct mtp_file_range * mfr) +{ + if(mfr->length < 0 || mfr->length == 0xFFFFFFFF) { + errno = EINVAL; + return -1; + } + + unsigned char * buffer = getReusableTransferBuffer(kMtpTransferChunkSize); + if (buffer == nullptr) + return -1; + + if (lseek(mfr->fd, mfr->offset, SEEK_SET) < 0) + return -1; + + int64_t total = 0; + while(total < mfr->length) + { + const size_t remaining = static_cast( + std::min(static_cast(kMtpTransferChunkSize), + mfr->length - total)); + ssize_t size = usb->readWithTimeout((char*)buffer, remaining, kMtpDataPhaseTimeoutNs); + if (size < 0) { + if (errno == 0) + errno = EIO; + return -1; + } + if (size == 0) { + errno = EIO; + return -1; + } + + if (!writeFully(mfr->fd, buffer, static_cast(size))) + return -1; + + total += size; + } + + return total; +} + +MtpResponseCode MtpServer::doGetObject() { + if (!hasStorage()) + return MTP_RESPONSE_INVALID_OBJECT_HANDLE; + MtpObjectHandle handle = mRequest.getParameter(1); + MtpString pathBuf; + int64_t fileLength; + MtpObjectFormat format; + int result = mDatabase->getObjectFilePath(handle, pathBuf, fileLength, format); + if (result != MTP_RESPONSE_OK) + return result; + if (isBlockedReadPath(pathBuf.c_str())) + return MTP_RESPONSE_ACCESS_DENIED; + + struct mtp_file_range mfr; + mfr.fd = open(pathBuf.c_str(), O_RDONLY); + if (mfr.fd < 0) { + return MTP_RESPONSE_GENERAL_ERROR; + } + mfr.offset = 0; + mfr.length = fileLength; + mfr.command = mRequest.getOperationCode(); + mfr.transaction_id = mRequest.getTransactionID(); + + // then transfer the file + int ret = send_file(mUSB, &mfr); + VLOG(2) << "MTP_SEND_FILE_WITH_HEADER returned " << ret; + close(mfr.fd); + if (ret < 0) { + if (errno == ECANCELED) + return MTP_RESPONSE_TRANSACTION_CANCELLED; + else + return MTP_RESPONSE_GENERAL_ERROR; + } + return MTP_RESPONSE_OK; +} + +MtpResponseCode MtpServer::doGetThumb() { + MtpObjectHandle handle = mRequest.getParameter(1); + size_t thumbSize; + void* thumb = mDatabase->getThumbnail(handle, thumbSize); + if (thumb) { + // send data + mData.setOperationCode(mRequest.getOperationCode()); + mData.setTransactionID(mRequest.getTransactionID()); + mData.writeData(mUSB, thumb, thumbSize); + free(thumb); + return MTP_RESPONSE_OK; + } else { + return MTP_RESPONSE_GENERAL_ERROR; + } +} + +MtpResponseCode MtpServer::doGetPartialObject(MtpOperationCode operation) { + if (!hasStorage()) + return MTP_RESPONSE_INVALID_OBJECT_HANDLE; + MtpObjectHandle handle = mRequest.getParameter(1); + uint64_t offset; + uint32_t length; + offset = mRequest.getParameter(2); + if (operation == MTP_OPERATION_GET_PARTIAL_OBJECT_64) { + // android extension with 64 bit offset + uint64_t offset2 = mRequest.getParameter(3); + offset = offset | (offset2 << 32); + length = mRequest.getParameter(4); + } else { + // standard GetPartialObject + length = mRequest.getParameter(3); + } + MtpString pathBuf; + int64_t fileLength; + MtpObjectFormat format; + int result = mDatabase->getObjectFilePath(handle, pathBuf, fileLength, format); + if (result != MTP_RESPONSE_OK) + return result; + if (isBlockedReadPath(pathBuf.c_str())) + return MTP_RESPONSE_ACCESS_DENIED; + if (offset + length > fileLength) + length = fileLength - offset; + + mtp_file_range mfr; + mfr.fd = open(pathBuf.c_str(), O_RDONLY); + if (mfr.fd < 0) { + return MTP_RESPONSE_GENERAL_ERROR; + } + mfr.offset = offset; + mfr.length = length; + mfr.command = mRequest.getOperationCode(); + mfr.transaction_id = mRequest.getTransactionID(); + mResponse.setParameter(1, length); + + // transfer the file + int ret = send_file(mUSB, &mfr); + VLOG(2) << "MTP_SEND_FILE_WITH_HEADER returned " << ret; + close(mfr.fd); + if (ret < 0) { + if (errno == ECANCELED) + return MTP_RESPONSE_TRANSACTION_CANCELLED; + else + return MTP_RESPONSE_GENERAL_ERROR; + } + return MTP_RESPONSE_OK; +} + +MtpResponseCode MtpServer::doSendObjectInfo() { + MtpString path; + MtpStorageID storageID = mRequest.getParameter(1); + MtpStorage* storage = getStorage(storageID); + MtpObjectHandle parent = mRequest.getParameter(2); + if (!storage) + return MTP_RESPONSE_INVALID_STORAGE_ID; + + // special case the root + if (parent == MTP_PARENT_ROOT) { + path = storage->getPath(); + parent = 0; + } else { + int64_t length; + MtpObjectFormat format; + int result = mDatabase->getObjectFilePath(parent, path, length, format); + if (result != MTP_RESPONSE_OK) + return result; + if (format != MTP_FORMAT_ASSOCIATION) + return MTP_RESPONSE_INVALID_PARENT_OBJECT; + } + + // read only the fields we need + mData.getUInt32(); // storage ID + MtpObjectFormat format = mData.getUInt16(); + mData.getUInt16(); // protection status + mSendObjectFileSize = mData.getUInt32(); + mData.getUInt16(); // thumb format + mData.getUInt32(); // thumb compressed size + mData.getUInt32(); // thumb pix width + mData.getUInt32(); // thumb pix height + mData.getUInt32(); // image pix width + mData.getUInt32(); // image pix height + mData.getUInt32(); // image bit depth + mData.getUInt32(); // parent + uint16_t associationType = mData.getUInt16(); + uint32_t associationDesc = mData.getUInt32(); // association desc + mData.getUInt32(); // sequence number + MtpStringBuffer name, created, modified; + mData.getString(name); // file name + mData.getString(created); // date created + mData.getString(modified); // date modified + // keywords follow + + VLOG(2) << "name: " << (const char *) name + << " format: " << std::hex << format << std::dec; + time_t modifiedTime; + if (!parseDateTime(modified, modifiedTime)) + modifiedTime = 0; + + if (path[path.size() - 1] != '/') + path += "/"; + path += (const char *)name; + + // check space first + if (mSendObjectFileSize > storage->getFreeSpace()) + return MTP_RESPONSE_STORAGE_FULL; + uint64_t maxFileSize = storage->getMaxFileSize(); + // check storage max file size + if (maxFileSize != 0) { + // if mSendObjectFileSize is 0xFFFFFFFF, then all we know is the file size + // is >= 0xFFFFFFFF + if (mSendObjectFileSize > maxFileSize || mSendObjectFileSize == 0xFFFFFFFF) + return MTP_RESPONSE_OBJECT_TOO_LARGE; + } + + VLOG(2) << "path: " << path.c_str() << " parent: " << parent + << " storageID: " << std::hex << storageID << std::dec; + MtpObjectHandle handle = mDatabase->beginSendObject(path.c_str(), + format, parent, storageID, mSendObjectFileSize, modifiedTime); + if (handle == kInvalidObjectHandle) { + return MTP_RESPONSE_GENERAL_ERROR; + } + + if (format == MTP_FORMAT_ASSOCIATION) { + int ret = mkdir(path.c_str(), mDirectoryPermission); + if (ret && errno != EEXIST) { + mDatabase->endSendObject(path, handle, MTP_FORMAT_ASSOCIATION, false); + return MTP_RESPONSE_GENERAL_ERROR; + } + if (ret && errno == EEXIST) { + struct stat st; + if (stat(path.c_str(), &st) != 0 || !S_ISDIR(st.st_mode)) { + mDatabase->endSendObject(path, handle, MTP_FORMAT_ASSOCIATION, false); + return MTP_RESPONSE_GENERAL_ERROR; + } + } + + // SendObject does not get sent for directories, so call endSendObject here instead + mDatabase->endSendObject(path, handle, MTP_FORMAT_ASSOCIATION, MTP_RESPONSE_OK); + } else { + mSendObjectFilePath = path; + // save the handle for the SendObject call, which should follow + mSendObjectHandle = handle; + mSendObjectFormat = format; + } + + mResponse.setParameter(1, storageID); + mResponse.setParameter(2, parent); + mResponse.setParameter(3, handle); + + return MTP_RESPONSE_OK; +} + +MtpResponseCode MtpServer::doSendObject() { + if (!hasStorage()) + return MTP_RESPONSE_GENERAL_ERROR; + MtpResponseCode result = MTP_RESPONSE_OK; + mtp_file_range mfr; + mfr.fd = -1; + int ret = 0; + int initialData = 0; + size_t initialPayload = 0; + uint64_t expectedPayload = mSendObjectFileSize; + uint32_t containerLength = 0; + + if (mSendObjectHandle == kInvalidObjectHandle) { + LOG(ERROR) << "Expected SendObjectInfo before SendObject"; + mData.reset(); + return MTP_RESPONSE_NO_VALID_OBJECT_INFO; + } + + // read the header, and possibly some data + ret = mData.readWithTimeout(mUSB, getInitialObjectReadSize(mSendObjectFileSize), kMtpDataPhaseTimeoutNs); + if (ret < MTP_CONTAINER_HEADER_SIZE) { + result = MTP_RESPONSE_GENERAL_ERROR; + goto done; + } + initialData = ret - MTP_CONTAINER_HEADER_SIZE; + initialPayload = static_cast(initialData); + + containerLength = mData.getContainerLength(); + if (containerLength != 0xFFFFFFFFU) { + if (containerLength < MTP_CONTAINER_HEADER_SIZE) { + result = MTP_RESPONSE_GENERAL_ERROR; + goto done; + } + expectedPayload = static_cast(containerLength - MTP_CONTAINER_HEADER_SIZE); + } else if (expectedPayload == 0xFFFFFFFFULL) { + result = MTP_RESPONSE_OBJECT_TOO_LARGE; + goto done; + } + + if (initialPayload > expectedPayload) { + result = MTP_RESPONSE_GENERAL_ERROR; + goto done; + } + + mfr.fd = open(mSendObjectFilePath.c_str(), O_WRONLY | O_CREAT | O_TRUNC, S_IRUSR | S_IWUSR); + if (mfr.fd < 0) { + result = MTP_RESPONSE_GENERAL_ERROR; + goto done; + } + + if (initialData > 0 && !writeFully(mfr.fd, mData.getData(), initialPayload)) { + result = (errno == ECANCELED) + ? MTP_RESPONSE_TRANSACTION_CANCELLED + : MTP_RESPONSE_GENERAL_ERROR; + goto done; + } + + if (expectedPayload > initialPayload) { + const uint64_t remainingPayload = expectedPayload - initialPayload; + if (remainingPayload > static_cast(std::numeric_limits::max())) { + result = MTP_RESPONSE_OBJECT_TOO_LARGE; + goto done; + } + + mfr.offset = initialPayload; + mfr.length = static_cast(remainingPayload); + + VLOG(2) << "receiving " << mSendObjectFilePath.c_str(); + // transfer the file + int64_t received = receive_file(mUSB, &mfr); + VLOG(2) << "MTP_RECEIVE_FILE returned " << received; + if (received < 0 || received != mfr.length) { + result = MTP_RESPONSE_TRANSACTION_CANCELLED; + if (errno != ECANCELED) + result = MTP_RESPONSE_GENERAL_ERROR; + goto done; + } + } + +done: + if (result == MTP_RESPONSE_OK) + mDatabase->updateObjectSize(mSendObjectHandle, expectedPayload); + if (mfr.fd >= 0) + close(mfr.fd); + if (result != MTP_RESPONSE_OK) + unlink(mSendObjectFilePath.c_str()); + + // reset so we don't attempt to send the data back + mData.reset(); + + mDatabase->endSendObject(mSendObjectFilePath, mSendObjectHandle, mSendObjectFormat, + result == MTP_RESPONSE_OK); + mSendObjectHandle = kInvalidObjectHandle; + mSendObjectFormat = 0; + return result; +} + +static bool deleteRecursive(const char* path) { + char pathbuf[PATH_MAX]; + size_t pathLength = strlen(path); + bool ok = true; + if (pathLength >= sizeof(pathbuf) - 2) { + LOG(ERROR) << "path too long: " << path; + return false; + } + strcpy(pathbuf, path); + if (pathbuf[pathLength - 1] != '/') { + pathbuf[pathLength++] = '/'; + pathbuf[pathLength] = '\0'; + } + char* fileSpot = pathbuf + pathLength; + size_t pathRemaining = sizeof(pathbuf) - pathLength - 1; + + DIR* dir = opendir(path); + if (!dir) { + if (errno != ENOENT) + LOG(ERROR) << "opendir " << path << " failed"; + return errno == ENOENT; + } + + struct dirent* entry; + while ((entry = readdir(dir))) { + const char* name = entry->d_name; + + // ignore "." and ".." + if (name[0] == '.' && (name[1] == 0 || (name[1] == '.' && name[2] == 0))) { + continue; + } + + size_t nameLength = strlen(name); + if (nameLength > pathRemaining) { + LOG(ERROR) << "path " << path << "/" << name << " too long"; + ok = false; + continue; + } + strcpy(fileSpot, name); + + bool isDir = entry->d_type == DT_DIR; + if (entry->d_type == DT_UNKNOWN) { + struct stat childStat {}; + isDir = stat(pathbuf, &childStat) == 0 && S_ISDIR(childStat.st_mode); + } + + if (isDir) { + ok = deleteRecursive(pathbuf) && ok; + if (rmdir(pathbuf) != 0 && errno != ENOENT) + ok = false; + } else { + if (unlink(pathbuf) != 0 && errno != ENOENT) + ok = false; + } + } + closedir(dir); + return ok; +} + +static bool deletePath(const char* path) { + struct stat statbuf; + if (stat(path, &statbuf) == 0) { + if (S_ISDIR(statbuf.st_mode)) { + bool ok = deleteRecursive(path); + if (rmdir(path) != 0 && errno != ENOENT) + ok = false; + return ok; + } else { + return unlink(path) == 0 || errno == ENOENT; + } + } else { + if (errno != ENOENT) + LOG(ERROR) << "deletePath stat failed for " << path; + return errno == ENOENT; + } +} + +MtpResponseCode MtpServer::doDeleteObject() { + if (!hasStorage()) + return MTP_RESPONSE_INVALID_OBJECT_HANDLE; + MtpObjectHandle handle = mRequest.getParameter(1); + MtpObjectFormat format = mRequest.getParameter(2); + // FIXME - support deleting all objects if handle is 0xFFFFFFFF + // FIXME - implement deleting objects by format + + MtpString filePath; + int64_t fileLength; + int result = mDatabase->getObjectFilePath(handle, filePath, fileLength, format); + if (result == MTP_RESPONSE_OK) { + VLOG(2) << "deleting " << filePath.c_str(); + result = mDatabase->deleteFile(handle); + // Don't delete the actual files unless the database deletion is allowed + if (result == MTP_RESPONSE_OK) { + deletePath(filePath.c_str()); + } + } + + return result; +} + +MtpResponseCode MtpServer::doMoveObject() { + if (!hasStorage()) + return MTP_RESPONSE_INVALID_OBJECT_HANDLE; + MtpObjectHandle handle = mRequest.getParameter(1); + MtpObjectHandle newparent = mRequest.getParameter(3); + + return mDatabase->moveFile(handle, newparent); +} + +MtpResponseCode MtpServer::doGetObjectPropDesc() { + MtpObjectProperty propCode = mRequest.getParameter(1); + MtpObjectFormat format = mRequest.getParameter(2); + VLOG(2) << "GetObjectPropDesc " << MtpDebug::getObjectPropCodeName(propCode) + << " " << MtpDebug::getFormatCodeName(format); + MtpProperty* property = mDatabase->getObjectPropertyDesc(propCode, format); + if (!property) + return MTP_RESPONSE_OBJECT_PROP_NOT_SUPPORTED; + property->write(mData); + delete property; + return MTP_RESPONSE_OK; +} + +MtpResponseCode MtpServer::doGetDevicePropDesc() { + MtpDeviceProperty propCode = mRequest.getParameter(1); + VLOG(1) << "GetDevicePropDesc " << MtpDebug::getDevicePropCodeName(propCode); + MtpProperty* property = mDatabase->getDevicePropertyDesc(propCode); + if (!property) + return MTP_RESPONSE_DEVICE_PROP_NOT_SUPPORTED; + property->write(mData); + delete property; + return MTP_RESPONSE_OK; +} + +MtpResponseCode MtpServer::doSendPartialObject() { + if (!hasStorage()) + return MTP_RESPONSE_INVALID_OBJECT_HANDLE; + MtpObjectHandle handle = mRequest.getParameter(1); + uint64_t offset = mRequest.getParameter(2); + uint64_t offset2 = mRequest.getParameter(3); + offset = offset | (offset2 << 32); + uint32_t length = mRequest.getParameter(4); + const uint64_t originalOffset = offset; + const uint32_t originalLength = length; + + ObjectEdit* edit = getEditObject(handle); + if (!edit) { + LOG(ERROR) << "object not open for edit in doSendPartialObject"; + return MTP_RESPONSE_GENERAL_ERROR; + } + + // can't start writing past the end of the file + if (offset > edit->mSize) { + VLOG(2) << "writing past end of object, offset: " << offset + << " edit->mSize: " << edit->mSize; + return MTP_RESPONSE_GENERAL_ERROR; + } + + const char* filePath = edit->mPath.c_str(); + VLOG(2) << "receiving partial " << filePath + << " " << offset << " " << length; + + // read the header, and possibly some data + int ret = mData.readWithTimeout(mUSB, getInitialObjectReadSize(length), kMtpDataPhaseTimeoutNs); + if (ret < MTP_CONTAINER_HEADER_SIZE) + return MTP_RESPONSE_GENERAL_ERROR; + int initialData = ret - MTP_CONTAINER_HEADER_SIZE; + if (static_cast(initialData) > length) + return MTP_RESPONSE_GENERAL_ERROR; + + if (initialData > 0) { + if (!writeFully(edit->mFD, mData.getData(), static_cast(initialData))) + ret = -1; + offset += initialData; + length -= initialData; + } + + if (ret >= 0 && length > 0) { + mtp_file_range mfr; + mfr.fd = edit->mFD; + mfr.offset = offset; + mfr.length = length; + + // transfer the file + int64_t received = receive_file(mUSB, &mfr); + VLOG(2) << "MTP_RECEIVE_FILE returned " << received; + ret = (received == mfr.length) ? 0 : -1; + } + if (ret < 0) { + mResponse.setParameter(1, 0); + if (errno == ECANCELED) + return MTP_RESPONSE_TRANSACTION_CANCELLED; + else + return MTP_RESPONSE_GENERAL_ERROR; + } + + // reset so we don't attempt to send this back + mData.reset(); + mResponse.setParameter(1, originalLength); + uint64_t end = originalOffset + originalLength; + if (end > edit->mSize) { + edit->mSize = end; + } + return MTP_RESPONSE_OK; +} + +MtpResponseCode MtpServer::doTruncateObject() { + MtpObjectHandle handle = mRequest.getParameter(1); + ObjectEdit* edit = getEditObject(handle); + if (!edit) { + LOG(ERROR) << "object not open for edit in doTruncateObject"; + return MTP_RESPONSE_GENERAL_ERROR; + } + + uint64_t offset = mRequest.getParameter(2); + uint64_t offset2 = mRequest.getParameter(3); + offset |= (offset2 << 32); + if (ftruncate(edit->mFD, offset) != 0) { + return MTP_RESPONSE_GENERAL_ERROR; + } else { + edit->mSize = offset; + return MTP_RESPONSE_OK; + } +} + +MtpResponseCode MtpServer::doBeginEditObject() { + MtpObjectHandle handle = mRequest.getParameter(1); + if (getEditObject(handle)) { + LOG(ERROR) << "object already open for edit in doBeginEditObject"; + return MTP_RESPONSE_GENERAL_ERROR; + } + + MtpString path; + int64_t fileLength; + MtpObjectFormat format; + int result = mDatabase->getObjectFilePath(handle, path, fileLength, format); + if (result != MTP_RESPONSE_OK) + return result; + + int fd = open(path.c_str(), O_RDWR | O_EXCL); + if (fd < 0) { + LOG(ERROR) << "open failed for " << path.c_str() << " in doBeginEditObject"; + return MTP_RESPONSE_GENERAL_ERROR; + } + + addEditObject(handle, path, fileLength, format, fd); + return MTP_RESPONSE_OK; +} + +MtpResponseCode MtpServer::doEndEditObject() { + MtpObjectHandle handle = mRequest.getParameter(1); + ObjectEdit* edit = getEditObject(handle); + if (!edit) { + LOG(ERROR) << "object not open for edit in doEndEditObject"; + return MTP_RESPONSE_GENERAL_ERROR; + } + + commitEdit(edit); + removeEditObject(handle); + return MTP_RESPONSE_OK; +} + +} // namespace android diff --git a/src/ThirdParty/mtp-server-nx/source/MtpStorage.cpp b/src/ThirdParty/mtp-server-nx/source/MtpStorage.cpp new file mode 100644 index 0000000..8a23f9f --- /dev/null +++ b/src/ThirdParty/mtp-server-nx/source/MtpStorage.cpp @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#define LOG_TAG "MtpStorage" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "MtpDebug.h" +#include "MtpDatabase.h" +#include "MtpStorage.h" +#include "log.h" + +namespace android { + +namespace { + +constexpr long long kFreeSpaceCacheMs = 2000; + +long long nowMs() { + const auto now = std::chrono::steady_clock::now(); + return std::chrono::duration_cast(now.time_since_epoch()).count(); +} + +} // namespace + +MtpStorage::MtpStorage(MtpStorageID id, const char* filePath, + const char* description, uint64_t reserveSpace, + bool removable, uint64_t maxFileSize) + : mStorageID(id), + mFilePath(filePath), + mDescription(description), + mMaxCapacity(0), + mCachedFreeSpace(0), + mCachedFreeSpaceMs(0), + mMaxFileSize(maxFileSize), + mReserveSpace(reserveSpace), + mRemovable(removable) +{ + VLOG(2) << "MtpStorage id: " << id << " path: " << filePath; +} + +MtpStorage::~MtpStorage() { +} + +int MtpStorage::getType() const { + return (mRemovable ? MTP_STORAGE_REMOVABLE_RAM : MTP_STORAGE_FIXED_RAM); +} + +int MtpStorage::getFileSystemType() const { + return MTP_STORAGE_FILESYSTEM_HIERARCHICAL; +} + +int MtpStorage::getAccessCapability() const { + return MTP_STORAGE_READ_WRITE; +} + +uint64_t MtpStorage::getMaxCapacity() { + if (mMaxCapacity == 0) { + struct statvfs stat; + if (statvfs(getPath(), &stat)) + return -1; + mMaxCapacity = (uint64_t)stat.f_blocks * (uint64_t)stat.f_bsize; + } + return mMaxCapacity; +} + +uint64_t MtpStorage::getFreeSpace() { + const long long now = nowMs(); + if (mCachedFreeSpaceMs > 0 && (now - mCachedFreeSpaceMs) < kFreeSpaceCacheMs) + return mCachedFreeSpace; + + struct statvfs stat; + if (statvfs(getPath(), &stat)) + return -1; + uint64_t freeSpace = (uint64_t)stat.f_bavail * (uint64_t)stat.f_bsize; + mCachedFreeSpace = (freeSpace > mReserveSpace ? freeSpace - mReserveSpace : 0); + mCachedFreeSpaceMs = now; + return mCachedFreeSpace; +} + +const char* MtpStorage::getDescription() const { + return mDescription.c_str(); +} + +} // namespace android diff --git a/src/ThirdParty/mtp-server-nx/source/MtpStorageInfo.cpp b/src/ThirdParty/mtp-server-nx/source/MtpStorageInfo.cpp new file mode 100644 index 0000000..9918f3b --- /dev/null +++ b/src/ThirdParty/mtp-server-nx/source/MtpStorageInfo.cpp @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#define LOG_TAG "MtpStorageInfo" + +#include +#include + +#include "MtpDebug.h" +#include "MtpDataPacket.h" +#include "MtpStorageInfo.h" +#include "MtpStringBuffer.h" + +#include "log.h" + +namespace android { + +MtpStorageInfo::MtpStorageInfo(MtpStorageID id) + : mStorageID(id), + mStorageType(0), + mFileSystemType(0), + mAccessCapability(0), + mMaxCapacity(0), + mFreeSpaceBytes(0), + mFreeSpaceObjects(0), + mStorageDescription(NULL), + mVolumeIdentifier(NULL) +{ +} + +MtpStorageInfo::~MtpStorageInfo() { + if (mStorageDescription) + free(mStorageDescription); + if (mVolumeIdentifier) + free(mVolumeIdentifier); +} + +void MtpStorageInfo::read(MtpDataPacket& packet) { + MtpStringBuffer string; + + // read the device info + mStorageType = packet.getUInt16(); + mFileSystemType = packet.getUInt16(); + mAccessCapability = packet.getUInt16(); + mMaxCapacity = packet.getUInt64(); + mFreeSpaceBytes = packet.getUInt64(); + mFreeSpaceObjects = packet.getUInt32(); + + packet.getString(string); + mStorageDescription = strdup((const char *)string); + packet.getString(string); + mVolumeIdentifier = strdup((const char *)string); +} + +void MtpStorageInfo::print() { + VLOG(2) << "Storage Info " << std::hex << mStorageID << std::dec << ":" + << "\n\tmStorageType: " << mStorageType + << "\n\tmFileSystemType: " << mFileSystemType + << "\n\tmAccessCapability: " << mAccessCapability; + VLOG(2) << "\tmMaxCapacity: " << mMaxCapacity + << "\n\tmFreeSpaceBytes: " << mFreeSpaceBytes + << "\n\tmFreeSpaceObjects: " << mFreeSpaceObjects; + VLOG(2) << "\tmStorageDescription: " << mStorageDescription + << "\n\tmVolumeIdentifier: " << mVolumeIdentifier; +} + +} // namespace android diff --git a/src/ThirdParty/mtp-server-nx/source/MtpStringBuffer.cpp b/src/ThirdParty/mtp-server-nx/source/MtpStringBuffer.cpp new file mode 100644 index 0000000..fe8cf04 --- /dev/null +++ b/src/ThirdParty/mtp-server-nx/source/MtpStringBuffer.cpp @@ -0,0 +1,171 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#define LOG_TAG "MtpStringBuffer" + +#include + +#include "MtpDataPacket.h" +#include "MtpStringBuffer.h" + +namespace android { + +MtpStringBuffer::MtpStringBuffer() + : mCharCount(0), + mByteCount(1) +{ + mBuffer[0] = 0; +} + +MtpStringBuffer::MtpStringBuffer(const char* src) + : mCharCount(0), + mByteCount(1) +{ + set(src); +} + +MtpStringBuffer::MtpStringBuffer(const uint16_t* src) + : mCharCount(0), + mByteCount(1) +{ + set(src); +} + +MtpStringBuffer::MtpStringBuffer(const MtpStringBuffer& src) + : mCharCount(src.mCharCount), + mByteCount(src.mByteCount) +{ + memcpy(mBuffer, src.mBuffer, mByteCount); +} + + +MtpStringBuffer::~MtpStringBuffer() { +} + +void MtpStringBuffer::set(const char* src) { + int length = strlen(src); + if (length >= sizeof(mBuffer)) + length = sizeof(mBuffer) - 1; + memcpy(mBuffer, src, length); + + // count the characters + int count = 0; + char ch; + while ((ch = *src++) != 0) { + if ((ch & 0x80) == 0) { + // single byte character + } else if ((ch & 0xE0) == 0xC0) { + // two byte character + if (! *src++) { + // last character was truncated, so ignore last byte + length--; + break; + } + } else if ((ch & 0xF0) == 0xE0) { + // 3 byte char + if (! *src++) { + // last character was truncated, so ignore last byte + length--; + break; + } + if (! *src++) { + // last character was truncated, so ignore last two bytes + length -= 2; + break; + } + } + count++; + } + + mByteCount = length + 1; + mBuffer[length] = 0; + mCharCount = count; +} + +void MtpStringBuffer::set(const uint16_t* src) { + int count = 0; + uint16_t ch; + uint8_t* dest = mBuffer; + + while ((ch = *src++) != 0 && count < 255) { + if (ch >= 0x0800) { + *dest++ = (uint8_t)(0xE0 | (ch >> 12)); + *dest++ = (uint8_t)(0x80 | ((ch >> 6) & 0x3F)); + *dest++ = (uint8_t)(0x80 | (ch & 0x3F)); + } else if (ch >= 0x80) { + *dest++ = (uint8_t)(0xC0 | (ch >> 6)); + *dest++ = (uint8_t)(0x80 | (ch & 0x3F)); + } else { + *dest++ = ch; + } + count++; + } + *dest++ = 0; + mCharCount = count; + mByteCount = dest - mBuffer; +} + +void MtpStringBuffer::readFromPacket(MtpDataPacket* packet) { + int count = packet->getUInt8(); + uint8_t* dest = mBuffer; + for (int i = 0; i < count; i++) { + uint16_t ch = packet->getUInt16(); + if (ch >= 0x0800) { + *dest++ = (uint8_t)(0xE0 | (ch >> 12)); + *dest++ = (uint8_t)(0x80 | ((ch >> 6) & 0x3F)); + *dest++ = (uint8_t)(0x80 | (ch & 0x3F)); + } else if (ch >= 0x80) { + *dest++ = (uint8_t)(0xC0 | (ch >> 6)); + *dest++ = (uint8_t)(0x80 | (ch & 0x3F)); + } else { + *dest++ = ch; + } + } + *dest++ = 0; + mCharCount = count; + mByteCount = dest - mBuffer; +} + +void MtpStringBuffer::writeToPacket(MtpDataPacket* packet) const { + int count = mCharCount; + const uint8_t* src = mBuffer; + packet->putUInt8(count > 0 ? count + 1 : 0); + + // expand utf8 to 16 bit chars + for (int i = 0; i < count; i++) { + uint16_t ch; + uint16_t ch1 = *src++; + if ((ch1 & 0x80) == 0) { + // single byte character + ch = ch1; + } else if ((ch1 & 0xE0) == 0xC0) { + // two byte character + uint16_t ch2 = *src++; + ch = ((ch1 & 0x1F) << 6) | (ch2 & 0x3F); + } else { + // three byte character + uint16_t ch2 = *src++; + uint16_t ch3 = *src++; + ch = ((ch1 & 0x0F) << 12) | ((ch2 & 0x3F) << 6) | (ch3 & 0x3F); + } + packet->putUInt16(ch); + } + // only terminate with zero if string is not empty + if (count > 0) + packet->putUInt16(0); +} + +} // namespace android diff --git a/src/ThirdParty/mtp-server-nx/source/MtpUtils.cpp b/src/ThirdParty/mtp-server-nx/source/MtpUtils.cpp new file mode 100644 index 0000000..d1d956f --- /dev/null +++ b/src/ThirdParty/mtp-server-nx/source/MtpUtils.cpp @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#define LOG_TAG "MtpUtils" + +#include +#include + +// #include +#include "MtpUtils.h" + +namespace android { + +/* +DateTime strings follow a compatible subset of the definition found in ISO 8601, and +take the form of a Unicode string formatted as: "YYYYMMDDThhmmss.s". In this +representation, YYYY shall be replaced by the year, MM replaced by the month (01-12), +DD replaced by the day (01-31), T is a constant character 'T' delimiting time from date, +hh is replaced by the hour (00-23), mm is replaced by the minute (00-59), and ss by the +second (00-59). The ".s" is optional, and represents tenths of a second. +*/ + +bool parseDateTime(const char* dateTime, time_t& outSeconds) { + int year, month, day, hour, minute, second; + struct tm tm; + + if (sscanf(dateTime, "%04d%02d%02dT%02d%02d%02d", + &year, &month, &day, &hour, &minute, &second) != 6) + return false; + const char* tail = dateTime + 15; + // skip optional tenth of second + if (tail[0] == '.' && tail[1]) + tail += 2; + //FIXME - support +/-hhmm + bool useUTC = (tail[0] == 'Z'); + + // hack to compute timezone + time_t dummy; + tzset(); + localtime_r(&dummy, &tm); + + tm.tm_sec = second; + tm.tm_min = minute; + tm.tm_hour = hour; + tm.tm_mday = day; + tm.tm_mon = month - 1; // mktime uses months in 0 - 11 range + tm.tm_year = year - 1900; + tm.tm_wday = 0; + tm.tm_isdst = -1; + outSeconds = mktime(&tm); + /*if (useUTC) + outSeconds = mktime(&tm); + else + outSeconds = mktime_tz(&tm, tm.tm_zone);*/ + + return true; +} + +void formatDateTime(time_t seconds, char* buffer, int bufferLength) { + struct tm tm; + + localtime_r(&seconds, &tm); + snprintf(buffer, bufferLength, "%04d%02d%02dT%02d%02d%02d", + tm.tm_year + 1900, + tm.tm_mon + 1, // localtime_r uses months in 0 - 11 range + tm.tm_mday, tm.tm_hour, tm.tm_min, tm.tm_sec); +} + +} // namespace android diff --git a/src/ThirdParty/mtp-server-nx/source/USBMtpInterface.cpp b/src/ThirdParty/mtp-server-nx/source/USBMtpInterface.cpp new file mode 100644 index 0000000..f19d806 --- /dev/null +++ b/src/ThirdParty/mtp-server-nx/source/USBMtpInterface.cpp @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "USBMtpInterface.h" + +#include + +#define EP_IN 0 +#define EP_OUT 1 +#define EP_INT 2 + +namespace { + +constexpr u64 kMtpDefaultReadTimeoutNs = 1ULL * 1000ULL * 1000ULL * 1000ULL; +constexpr u64 kMtpDefaultWriteTimeoutNs = 2ULL * 1000ULL * 1000ULL * 1000ULL; +constexpr u64 kMtpEventTimeoutNs = 1ULL * 1000ULL * 1000ULL * 1000ULL; + +ssize_t transferOrError(u32 interface_index, u32 endpoint, UsbDirection direction, void* ptr, size_t len, u64 timeout) +{ + size_t transferred = usbTransfer(interface_index, endpoint, direction, ptr, len, timeout); + if (transferred == static_cast(-1)) { + errno = EIO; + return -1; + } + return static_cast(transferred); +} + +} // namespace + +USBMtpInterface::USBMtpInterface(int index, UsbInterfaceDesc *info) +{ + interface_index = index; + info->interface_desc = &mtp_interface_descriptor; + info->endpoint_desc[EP_IN] = &mtp_endpoint_descriptor_in; + info->endpoint_desc[EP_OUT] = &mtp_endpoint_descriptor_out; + info->endpoint_desc[EP_INT] = &mtp_endpoint_descriptor_interrupt; + info->string_descriptor = mtp_string_descriptor; +} + +USBMtpInterface::~USBMtpInterface() { +} + +ssize_t USBMtpInterface::read(char *ptr, size_t len) +{ + return readWithTimeout(ptr, len, kMtpDefaultReadTimeoutNs); +} +ssize_t USBMtpInterface::readWithTimeout(char *ptr, size_t len, u64 timeout) +{ + return transferOrError(interface_index, EP_OUT, UsbDirection_Read, (void*)ptr, len, timeout); +} +ssize_t USBMtpInterface::write(const char *ptr, size_t len) +{ + return transferOrError(interface_index, EP_IN, UsbDirection_Write, (void*)ptr, len, kMtpDefaultWriteTimeoutNs); +} +ssize_t USBMtpInterface::sendEvent(const char *ptr, size_t len) +{ + return transferOrError(interface_index, EP_INT, UsbDirection_Write, (void*)ptr, len, kMtpEventTimeoutNs); +} diff --git a/src/ThirdParty/mtp-server-nx/source/USBSerialInterface.cpp b/src/ThirdParty/mtp-server-nx/source/USBSerialInterface.cpp new file mode 100644 index 0000000..3c9a020 --- /dev/null +++ b/src/ThirdParty/mtp-server-nx/source/USBSerialInterface.cpp @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "USBSerialInterface.h" + +#define EP_IN 0 +#define EP_OUT 1 + +USBSerialInterface::USBSerialInterface(int index, UsbInterfaceDesc *info) +{ + interface_index = index; + info->interface_desc = &serial_interface_descriptor; + info->endpoint_desc[EP_IN] = &serial_endpoint_descriptor_in; + info->endpoint_desc[EP_OUT] = &serial_endpoint_descriptor_out; + info->string_descriptor = NULL; +} + +USBSerialInterface::~USBSerialInterface() { +} + +ssize_t USBSerialInterface::read(char *ptr, size_t len) +{ + return usbTransfer(interface_index, EP_OUT, UsbDirection_Read, (void*)ptr, len, UINT64_MAX); +} +ssize_t USBSerialInterface::write(const char *ptr, size_t len) +{ + return usbTransfer(interface_index, EP_IN, UsbDirection_Write, (void*)ptr, len, UINT64_MAX); +} diff --git a/src/ThirdParty/mtp-server-nx/source/log.cpp b/src/ThirdParty/mtp-server-nx/source/log.cpp new file mode 100644 index 0000000..60bae75 --- /dev/null +++ b/src/ThirdParty/mtp-server-nx/source/log.cpp @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "log.h" + +int verbose_level = 0; + +char log_level_color[5][16] = +{ + "\033[37m", // White + "\033[32m", // Green + "\033[93m", // Yellow + "\033[31m", // Red + "\033[35m" // Purple +}; \ No newline at end of file diff --git a/src/ThirdParty/mtp-server-nx/source/main.cpp b/src/ThirdParty/mtp-server-nx/source/main.cpp new file mode 100644 index 0000000..3fb2745 --- /dev/null +++ b/src/ThirdParty/mtp-server-nx/source/main.cpp @@ -0,0 +1,209 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * Copyright (C) 2019 Gillou68310 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include +#include +#include + +#include "SwitchMtpDatabase.h" +#include "MtpServer.h" +#include "MtpStorage.h" + +#include "log.h" + +using namespace android; + +#ifdef WANT_SYSMODULE +extern "C" +{ +#define INNER_HEAP_SIZE 0x80000 + extern u32 __start__; + + size_t nx_inner_heap_size = INNER_HEAP_SIZE; + char nx_inner_heap[INNER_HEAP_SIZE]; + + void __libnx_initheap(void); + void __appInit(void); + void __appExit(void); +} + +u32 __nx_applet_type = AppletType_None; + +void __libnx_initheap(void) +{ + void *addr = nx_inner_heap; + size_t size = nx_inner_heap_size; + + extern char *fake_heap_start; + extern char *fake_heap_end; + + fake_heap_start = (char *)addr; + fake_heap_end = (char *)addr + size; +} + +void __appInit(void) +{ + smInitialize(); + Result rc = setsysInitialize(); + if (R_SUCCEEDED(rc)) { + SetSysFirmwareVersion fw; + rc = setsysGetFirmwareVersion(&fw); + if (R_SUCCEEDED(rc)) + hosversionSet(MAKEHOSVERSION(fw.major, fw.minor, fw.micro)); + setsysExit(); + } + fsInitialize(); + hidInitialize(); + fsdevMountSdmc(); +} + +MtpServer* serverExit = NULL; + +void __appExit(void) +{ + serverExit->stop(); + usbExit(); + hidExit(); + fsExit(); + smExit(); +} +#endif // WANT_SYSMODULE + +static void stop_thread(MtpServer* server) +{ +#ifdef WANT_APPLET + padConfigureInput(8, HidNpadStyleSet_NpadStandard); + + PadState pad; + padInitializeAny(&pad); + + while (appletMainLoop()) + { + padUpdate(&pad); + u64 kDown = padGetButtonsDown(&pad); + + if (kDown & HidNpadButton_B) + { + server->stop(); + break; + } + } +#endif // WANT_APPLET + +#ifdef WANT_SYSMODULE + serverExit = server; +#endif // WANT_SYSMODULE +} + +int main(int argc, char* argv[]) +{ + int c; + struct option long_options[] = + { + {"nxlink", no_argument, &nxlink, 1}, + {"verbose", required_argument, 0, 'v'}, + {0, 0, 0, 0} + }; + + while(1) + { + int option_index = 0; + c = getopt_long (argc, argv, "v:", long_options, &option_index); + if (c == -1) + break; + + switch (c) + { + case 'v': + verbose_level = atoi(optarg); + break; + default: + break; + } + } + +#ifdef WANT_APPLET + consoleInit(NULL); + std::cout << "MTP Server is running." << std::endl; + std::cout << "> Press B to exit."; +#endif // WANT_APPLET + + struct usb_device_descriptor device_descriptor = { + .bLength = USB_DT_DEVICE_SIZE, + .bDescriptorType = USB_DT_DEVICE, + .bcdUSB = 0x0110, + .bDeviceClass = 0x00, + .bDeviceSubClass = 0x00, + .bDeviceProtocol = 0x00, + .bMaxPacketSize0 = 0x40, + .idVendor = 0x057e, + .idProduct = 0x4000, + .bcdDevice = 0x0100, + .bNumConfigurations = 0x01 + }; + + UsbInterfaceDesc infos[2]; + int num_interface = 0; + USBMtpInterface *mtp_interface = NULL; + USBSerialInterface *serial_interface = NULL; + + mtp_interface = new USBMtpInterface(num_interface, &infos[num_interface]); + num_interface++; + + if(nxlink) + { + serial_interface = new USBSerialInterface(num_interface, &infos[num_interface]); + num_interface++; + } + + usbInitialize(&device_descriptor, num_interface, infos); + nxlinkStdioInitialise(serial_interface); + + MtpStorage* storage = new MtpStorage( + MTP_STORAGE_REMOVABLE_RAM, + "sdmc:/", + "sdcard", + 1024U * 1024U * 100U, /* 100 MB reserved space, to avoid filling the disk */ + false, + 1024U * 1024U * 1024U * 4U - 1 /* ~4GB arbitrary max file size */); + + MtpDatabase* mtp_database = new SwitchMtpDatabase(); + + mtp_database->addStoragePath("sdmc:/", + "sdcard", + MTP_STORAGE_REMOVABLE_RAM, true); + + MtpServer* server = new MtpServer( + mtp_interface, + mtp_database, + false, + 0, + 0, + 0); + + std::thread th(stop_thread, server); + server->addStorage(storage); + server->run(); + th.join(); + + nxlinkStdioClose(serial_interface); + consoleExit(NULL); + usbExit(); + return 0; +} diff --git a/src/ThirdParty/mtp-server-nx/source/nxlink.cpp b/src/ThirdParty/mtp-server-nx/source/nxlink.cpp new file mode 100644 index 0000000..de93d5e --- /dev/null +++ b/src/ThirdParty/mtp-server-nx/source/nxlink.cpp @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include + +#include "log.h" + +int nxlink = 0; + +static USBSerialInterface* _usb; + +static ssize_t write_stdout(struct _reent *r,void *fd,const char *ptr, size_t len) +{ + return _usb->write(ptr, len); +} + +static const devoptab_t dotab_stdout = { + "usb", + 0, + NULL, + NULL, + write_stdout, + NULL, + NULL, + NULL +}; + +void nxlinkStdioInitialise(USBSerialInterface* usb) +{ + if(usb != NULL) + { + _usb = usb; + devoptab_list[STD_OUT] = &dotab_stdout; + devoptab_list[STD_ERR] = &dotab_stdout; + setvbuf(stdout, NULL , _IONBF, 0); + setvbuf(stderr, NULL , _IONBF, 0); + + // Wait for start command + char start[7]; + while(strcmp(start, "#START#") != 0) + { + usb->read(start, 7); + } + } +} + +void nxlinkStdioClose(USBSerialInterface* usb) +{ + if(usb != NULL) + { + const char *stop = "#STOP#"; + usb->write(stop, 6); + } +} diff --git a/src/ThirdParty/mtp-server-nx/source/usb.c b/src/ThirdParty/mtp-server-nx/source/usb.c new file mode 100644 index 0000000..0073c95 --- /dev/null +++ b/src/ThirdParty/mtp-server-nx/source/usb.c @@ -0,0 +1,433 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include + +#include "usb.h" + +#define TOTAL_INTERFACES 4 +#define TOTAL_ENDPOINTS 4 + +typedef struct { + UsbDsEndpoint *endpoint; + u8 *buffer; + RwLock lock; +} usbCommsEndpoint; + +typedef struct { + RwLock lock; + bool initialized; + UsbDsInterface* interface; + u32 endpoint_number; + usbCommsEndpoint endpoint[TOTAL_ENDPOINTS]; +} usbCommsInterface; + +static bool g_usbCommsInitialized = false; +static usbCommsInterface g_usbCommsInterfaces[TOTAL_INTERFACES]; +static bool g_usbCommsErrorHandling = 0; +static RwLock g_usbCommsLock; +static int ep_in = 1; +static int ep_out = 1; + +#define USB_BULK_MAX_URB_SIZE (256 * 1024) +#define USB_CANCEL_TIMEOUT_NS (250ULL * 1000ULL * 1000ULL) + +static Result _usbCommsInterfaceInit5x(u32 intf_ind, const UsbInterfaceDesc *info); +static Result _usbCommsInterfaceInit(u32 intf_ind, const UsbInterfaceDesc *info); + +Result usbInitialize(struct usb_device_descriptor *device_descriptor, u32 num_interfaces, const UsbInterfaceDesc *infos) +{ + Result rc = 0; + rwlockWriteLock(&g_usbCommsLock); + + if (g_usbCommsInitialized) { + rc = MAKERESULT(Module_Libnx, LibnxError_AlreadyInitialized); + } else if (num_interfaces > TOTAL_INTERFACES) { + rc = MAKERESULT(Module_Libnx, LibnxError_OutOfMemory); + } else { + rc = usbDsInitialize(); + + if (R_SUCCEEDED(rc)) { + ep_in = 1; + ep_out = 1; + + if (hosversionAtLeast(5,0,0)) { + u8 iManufacturer, iProduct, iSerialNumber; + static const u16 supported_langs[1] = {0x0409}; + // Send language descriptor + rc = usbDsAddUsbLanguageStringDescriptor(NULL, supported_langs, sizeof(supported_langs)/sizeof(u16)); + // Send manufacturer + if (R_SUCCEEDED(rc)) rc = usbDsAddUsbStringDescriptor(&iManufacturer, "Nintendo"); + // Send product + if (R_SUCCEEDED(rc)) rc = usbDsAddUsbStringDescriptor(&iProduct, "Simple Mod Manager MTP"); + // Send serial number + if (R_SUCCEEDED(rc)) rc = usbDsAddUsbStringDescriptor(&iSerialNumber, "SerialNumber"); + + // Send device descriptors + device_descriptor->iManufacturer = iManufacturer; + device_descriptor->iProduct = iProduct; + device_descriptor->iSerialNumber = iSerialNumber; + + // Full Speed is USB 1.1 + if (R_SUCCEEDED(rc)) rc = usbDsSetUsbDeviceDescriptor(UsbDeviceSpeed_Full, device_descriptor); + + // High Speed is USB 2.0 + device_descriptor->bcdUSB = 0x0200; + if (R_SUCCEEDED(rc)) rc = usbDsSetUsbDeviceDescriptor(UsbDeviceSpeed_High, device_descriptor); + + // Super Speed is USB 3.0 + device_descriptor->bcdUSB = 0x0300; + // Upgrade packet size to 512 + device_descriptor->bMaxPacketSize0 = 0x09; + if (R_SUCCEEDED(rc)) rc = usbDsSetUsbDeviceDescriptor(UsbDeviceSpeed_Super, device_descriptor); + + // Define Binary Object Store + u8 bos[0x16] = { + 0x05, // .bLength + USB_DT_BOS, // .bDescriptorType + 0x16, 0x00, // .wTotalLength + 0x02, // .bNumDeviceCaps + + // USB 2.0 + 0x07, // .bLength + USB_DT_DEVICE_CAPABILITY, // .bDescriptorType + 0x02, // .bDevCapabilityType + 0x02, 0x00, 0x00, 0x00, // dev_capability_data + + // USB 3.0 + 0x0A, // .bLength + USB_DT_DEVICE_CAPABILITY, // .bDescriptorType + 0x03, // .bDevCapabilityType + 0x00, 0x0E, 0x00, 0x03, 0x00, 0x00, 0x00 + }; + if (R_SUCCEEDED(rc)) rc = usbDsSetBinaryObjectStore(bos, sizeof(bos)); + } + + if (R_SUCCEEDED(rc)) { + for (u32 i = 0; i < num_interfaces; i++) { + usbCommsInterface *intf = &g_usbCommsInterfaces[i]; + const UsbInterfaceDesc *info = &infos[i]; + intf->endpoint_number = info->interface_desc->bNumEndpoints; + rwlockWriteLock(&intf->lock); + for (u32 i = 0; i < intf->endpoint_number; i++) + { + rwlockWriteLock(&intf->endpoint[i].lock); + } + rc = _usbCommsInterfaceInit(i, info); + for (u32 i = 0; i < intf->endpoint_number; i++) + { + rwlockWriteUnlock(&intf->endpoint[i].lock); + } + rwlockWriteUnlock(&intf->lock); + if (R_FAILED(rc)) { + break; + } + } + } + } + + if (R_SUCCEEDED(rc) && hosversionAtLeast(5,0,0)) { + rc = usbDsEnable(); + } + + if (R_FAILED(rc)) { + usbExit(); + } + } + + if (R_SUCCEEDED(rc)) { + g_usbCommsInitialized = true; + g_usbCommsErrorHandling = false; + } + + rwlockWriteUnlock(&g_usbCommsLock); + return rc; +} + +static void _usbCommsInterfaceFree(usbCommsInterface *interface) +{ + rwlockWriteLock(&interface->lock); + if (!interface->initialized) { + rwlockWriteUnlock(&interface->lock); + return; + } + + interface->initialized = 0; + interface->interface = NULL; + + for (u32 i = 0; i < interface->endpoint_number; i++) + { + rwlockWriteLock(&interface->endpoint[i].lock); + interface->endpoint[i].endpoint = NULL; + free(interface->endpoint[i].buffer); + interface->endpoint[i].buffer = NULL; + rwlockWriteUnlock(&interface->endpoint[i].lock); + } + + rwlockWriteUnlock(&interface->lock); +} + +void usbExit(void) +{ + u32 i; + + rwlockWriteLock(&g_usbCommsLock); + + usbDsExit(); + + g_usbCommsInitialized = false; + ep_in = 1; + ep_out = 1; + + rwlockWriteUnlock(&g_usbCommsLock); + + for (i=0; istring_descriptor != NULL) + { + usbDsAddUsbStringDescriptor(&index, info->string_descriptor); + } + info->interface_desc->iInterface = index; + + struct usb_ss_endpoint_companion_descriptor endpoint_companion = { + .bLength = sizeof(struct usb_ss_endpoint_companion_descriptor), + .bDescriptorType = USB_DT_SS_ENDPOINT_COMPANION, + .bMaxBurst = 0x0F, + .bmAttributes = 0x00, + .wBytesPerInterval = 0x00, + }; + + interface->initialized = 1; + + //The buffer for PostBufferAsync commands must be 0x1000-byte aligned. + for (u32 i = 0; i < interface->endpoint_number; i++) + { + interface->endpoint[i].buffer = (u8*)memalign(0x1000, 0x1000); + if (interface->endpoint[i].buffer == NULL) + { + rc = MAKERESULT(Module_Libnx, LibnxError_OutOfMemory); + break; + } + memset(interface->endpoint[i].buffer, 0, 0x1000); + } + if (R_FAILED(rc)) return rc; + + rc = usbDsRegisterInterface(&interface->interface); + if (R_FAILED(rc)) return rc; + + info->interface_desc->bInterfaceNumber = interface->interface->interface_index; + for (u32 i = 0; i < interface->endpoint_number; i++) + { + if((info->endpoint_desc[i]->bEndpointAddress & USB_ENDPOINT_IN) != 0) + { + info->endpoint_desc[i]->bEndpointAddress |= ep_in; + ep_in++; + } + else + { + info->endpoint_desc[i]->bEndpointAddress |= ep_out; + ep_out++; + } + } + + // Full Speed Config + rc = usbDsInterface_AppendConfigurationData(interface->interface, UsbDeviceSpeed_Full, info->interface_desc, USB_DT_INTERFACE_SIZE); + if (R_FAILED(rc)) return rc; + + for (u32 i = 0; i < interface->endpoint_number; i++) + { + if(info->endpoint_desc[i]->bmAttributes == USB_TRANSFER_TYPE_BULK) + info->endpoint_desc[i]->wMaxPacketSize = 0x40; + rc = usbDsInterface_AppendConfigurationData(interface->interface, UsbDeviceSpeed_Full, info->endpoint_desc[i], USB_DT_ENDPOINT_SIZE); + if (R_FAILED(rc)) return rc; + } + + // High Speed Config + rc = usbDsInterface_AppendConfigurationData(interface->interface, UsbDeviceSpeed_High, info->interface_desc, USB_DT_INTERFACE_SIZE); + if (R_FAILED(rc)) return rc; + + for (u32 i = 0; i < interface->endpoint_number; i++) + { + if(info->endpoint_desc[i]->bmAttributes == USB_TRANSFER_TYPE_BULK) + info->endpoint_desc[i]->wMaxPacketSize = 0x200; + rc = usbDsInterface_AppendConfigurationData(interface->interface, UsbDeviceSpeed_High, info->endpoint_desc[i], USB_DT_ENDPOINT_SIZE); + if (R_FAILED(rc)) return rc; + } + + // Super Speed Config + rc = usbDsInterface_AppendConfigurationData(interface->interface, UsbDeviceSpeed_Super, info->interface_desc, USB_DT_INTERFACE_SIZE); + if (R_FAILED(rc)) return rc; + + for (u32 i = 0; i < interface->endpoint_number; i++) + { + if(info->endpoint_desc[i]->bmAttributes == USB_TRANSFER_TYPE_BULK) + info->endpoint_desc[i]->wMaxPacketSize = 0x400; + rc = usbDsInterface_AppendConfigurationData(interface->interface, UsbDeviceSpeed_Super, info->endpoint_desc[i], USB_DT_ENDPOINT_SIZE); + if (R_FAILED(rc)) return rc; + rc = usbDsInterface_AppendConfigurationData(interface->interface, UsbDeviceSpeed_Super, &endpoint_companion, USB_DT_SS_ENDPOINT_COMPANION_SIZE); + if (R_FAILED(rc)) return rc; + } + + //Setup endpoints. + for (u32 i = 0; i < interface->endpoint_number; i++) + { + rc = usbDsInterface_RegisterEndpoint(interface->interface, &interface->endpoint[i].endpoint, info->endpoint_desc[i]->bEndpointAddress); + if (R_FAILED(rc)) return rc; + } + + rc = usbDsInterface_EnableInterface(interface->interface); + if (R_FAILED(rc)) return rc; + + return rc; +} + +static Result _usbCommsTransfer(usbCommsEndpoint *ep, UsbDirection dir, const void* buffer, size_t size, u64 timeout, size_t *transferredSize) +{ + Result rc=0; + u32 urbId=0; + u32 chunksize=0; + u8 transfer_type=0; + u8 *bufptr = (u8*)buffer; + u8 *transfer_buffer = NULL; + u32 tmp_transferredSize = 0; + size_t total_transferredSize=0; + UsbDsReportData reportdata; + + //Makes sure endpoints are ready for data-transfer / wait for init if needed. + rc = usbDsWaitReady(timeout); + if (R_FAILED(rc)) return rc; + + while(size) + { + if(((u64)bufptr) & 0xfff)//When bufptr isn't page-aligned copy the data into g_usbComms_endpoint_in_buffer and transfer that, otherwise use the bufptr directly. + { + transfer_buffer = ep->buffer; + memset(ep->buffer, 0, 0x1000); + + chunksize = 0x1000; + chunksize-= ((u64)bufptr) & 0xfff;//After this transfer, bufptr will be page-aligned(if size is large enough for another transfer). + if (sizebuffer, bufptr, chunksize); + + transfer_type = 0; + } + else + { + transfer_buffer = bufptr; + chunksize = size > USB_BULK_MAX_URB_SIZE ? USB_BULK_MAX_URB_SIZE : size; + transfer_type = 1; + } + + //Start transfer. + rc = usbDsEndpoint_PostBufferAsync(ep->endpoint, transfer_buffer, chunksize, &urbId); + if(R_FAILED(rc))return rc; + + //Wait for the transfer to finish. + rc = eventWait(&ep->endpoint->CompletionEvent, timeout); + + if (R_FAILED(rc)) + { + usbDsEndpoint_Cancel(ep->endpoint); + Result cancel_rc = eventWait(&ep->endpoint->CompletionEvent, USB_CANCEL_TIMEOUT_NS); + if (R_SUCCEEDED(cancel_rc)) { + eventClear(&ep->endpoint->CompletionEvent); + } + return rc; + } + eventClear(&ep->endpoint->CompletionEvent); + + rc = usbDsEndpoint_GetReportData(ep->endpoint, &reportdata); + if (R_FAILED(rc)) return rc; + + rc = usbDsParseReportData(&reportdata, urbId, NULL, &tmp_transferredSize); + if (R_FAILED(rc)) return rc; + + if (tmp_transferredSize > chunksize) tmp_transferredSize = chunksize; + + total_transferredSize+= (size_t)tmp_transferredSize; + + if ((transfer_type==0) && (dir == UsbDirection_Read)) + memcpy(bufptr, transfer_buffer, tmp_transferredSize); + + bufptr+= tmp_transferredSize; + size-= tmp_transferredSize; + + if (tmp_transferredSize < chunksize) break; + } + + if (transferredSize) *transferredSize = total_transferredSize; + + return rc; +} + +size_t usbTransfer(u32 interface, u32 endpoint, UsbDirection dir, void* buffer, size_t size, u64 timeout) +{ + size_t transferredSize=-1; + u32 state=0; + Result rc, rc2; + bool initialized; + + usbCommsInterface *inter = &g_usbCommsInterfaces[interface]; + usbCommsEndpoint *ep = &inter->endpoint[endpoint]; + rwlockReadLock(&inter->lock); + initialized = inter->initialized; + rwlockReadUnlock(&inter->lock); + if (!initialized) return (size_t)-1; + + rwlockWriteLock(&ep->lock); + rc = _usbCommsTransfer(ep, dir, buffer, size, timeout, &transferredSize); + rwlockWriteUnlock(&ep->lock); + if (R_FAILED(rc)) { + rc2 = usbDsGetState(&state); + if (R_SUCCEEDED(rc2)) { + if (state == UsbState_Configured) { + rwlockWriteLock(&ep->lock); + rc = _usbCommsTransfer(ep, dir, buffer, size, timeout, &transferredSize); //If state changed during transfer, try again. usbDsWaitReady() will be called from this. + rwlockWriteUnlock(&ep->lock); + } + } + if (R_FAILED(rc) && g_usbCommsErrorHandling) + { + if(dir == UsbDirection_Write) + diagAbortWithResult(MAKERESULT(Module_Libnx, LibnxError_BadUsbCommsWrite)); + else + diagAbortWithResult(MAKERESULT(Module_Libnx, LibnxError_BadUsbCommsRead)); + } + } + return transferredSize; +} diff --git a/submodules/borealis b/submodules/borealis index db9bace..0c9d474 160000 --- a/submodules/borealis +++ b/submodules/borealis @@ -1 +1 @@ -Subproject commit db9bacea21f238e2d61cade72e6ee4a04c1119d2 +Subproject commit 0c9d47429de5656029768089196a886d604016c5 diff --git a/submodules/cpp-generic-toolbox b/submodules/cpp-generic-toolbox index ea4e16a..8dc093c 160000 --- a/submodules/cpp-generic-toolbox +++ b/submodules/cpp-generic-toolbox @@ -1 +1 @@ -Subproject commit ea4e16a7c137d3410033b56b92e15cdaa84a4468 +Subproject commit 8dc093c420b153de817b23ec89c90ac4cdd64f70 diff --git a/submodules/libtesla b/submodules/libtesla index bd5675e..c16c46e 160000 --- a/submodules/libtesla +++ b/submodules/libtesla @@ -1 +1 @@ -Subproject commit bd5675ede3d6a3b99c9859f8adbcf2a2d40800f0 +Subproject commit c16c46e2ce048c9ab8efa5301960ec19562ffd69 diff --git a/submodules/simple-cpp-logger b/submodules/simple-cpp-logger index 2e91437..a47adff 160000 --- a/submodules/simple-cpp-logger +++ b/submodules/simple-cpp-logger @@ -1 +1 @@ -Subproject commit 2e91437e052f8a1f70ec995a3e82dde7fbbe2169 +Subproject commit a47adff5c3098f6e9a98017e27e1e339b0dd1af9