diff --git a/.github/workflows/sphinx.yml b/.github/workflows/sphinx.yml index 5db967a3..af3dc884 100644 --- a/.github/workflows/sphinx.yml +++ b/.github/workflows/sphinx.yml @@ -1,4 +1,4 @@ -name: Deploy Sphinx documentation to Pages +name: Deploy Doxygen to Pages # Runs on pushes targeting the default branch on: @@ -15,5 +15,15 @@ jobs: pages: write id-token: write steps: - - id: deployment - uses: sphinx-notes/pages@v3 + - id: build + uses: mattnotmitt/doxygen-action@1.9.8 + with: + working-directory: './docs/' + doxyfile-path: 'docs/Doxyfile' + + - id: deploy + uses: peaceiris/actions-gh-pages@v4 + with: + github_token: ${{ secrets.MY_TOKEN }} + publish_dir: './docs/build/html' + diff --git a/CMakeLists.txt b/CMakeLists.txt index 500a8dc5..aa9e6bd1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -77,3 +77,6 @@ add_custom_command( ) add_subdirectory(src) + +enable_testing() +add_subdirectory(test) diff --git a/Fk/ModMaker/Block/Block.qml b/Fk/ModMaker/Block/Block.qml deleted file mode 100644 index 7f2b3925..00000000 --- a/Fk/ModMaker/Block/Block.qml +++ /dev/null @@ -1,154 +0,0 @@ -import QtQuick - -Item { - id: root - property var parentBlock - property var childBlocks: [] // nested blocks inside this block - property var currentStack: [ root ] // the block stack that root is in - property var workspace // workspace - - property bool draggable: false - property alias dragging: drag.active - property real startX // only available when dragging - property real startY - - // TMP - property int idx - function toString() { return "Block #" + idx.toString(); } - // TMP - - Rectangle { - id: rect - anchors.fill: parent - color: drag.active ? "grey" : "snow" - border.width: 1 - radius: 0 - } - - Text { - text: idx - } - - DragHandler { - id: drag - enabled: root.draggable - grabPermissions: PointHandler.TakeOverForbidden - xAxis.enabled: true - yAxis.enabled: true - } - - onDraggingChanged: { - if (!dragging) { - finishDrag(); - } else { - startDrag(); - } - } - - onXChanged: { - if (dragging) { - updateChildrenPos(); - } - } - - onYChanged: { - if (dragging) { - updateChildrenPos(); - } - } - - function getStackParent() { - const idx = currentStack.indexOf(root); - if (idx <= 0) { - return null; - } - return currentStack[idx - 1]; - } - - function getStackChildren() { - const idx = currentStack.indexOf(root); - if (idx >= currentStack.length - 1) { - return []; - } - return currentStack.slice(idx + 1); - } - - function startDrag() { - startX = x; - startY = y; - let children = getStackChildren(); - children.push(...childBlocks); - children.forEach(b => { - b.startX = b.x; - b.startY = b.y; - }); - } - - function updateChildrenPos() { - const dx = root.x - root.startX; - const dy = root.y - root.startY; - let children = getStackChildren(); - children.push(...childBlocks); - children.forEach(b => { - b.x = b.startX + dx; - b.y = b.startY + dy; - }); - } - - function finishDrag() { - if (currentStack[0] !== root) { - tearFrom(getStackParent()); - } - - if (parentBlock) { - tearFrom(parentBlock); - } - - if (workspace) { - workspace.arrangeBlock(root); - } - } - - function pasteTo(dest, asParent) { - x = dest.x; - y = dest.y + dest.height; - updateChildrenPos(); - - if (!asParent) { - const stk = currentStack; - dest.currentStack.push(...stk); - - const p = dest.parentBlock; - let c = getStackChildren(); - c.push(root); - c.forEach(cc => { - cc.parentBlock = p; - cc.currentStack = dest.currentStack; - }); - } else { - // TODO - } - } - - function tearFrom(dest) { - const fromParent = dest === root.parentBlock; - if (!fromParent) { - const idx = currentStack.indexOf(root); - const newStack = currentStack.slice(idx); - let c = getStackChildren(); - - currentStack.splice(idx); - - c.push(root); - c.forEach(cc => { - cc.parentBlock = null; - cc.currentStack = newStack; - }); - } else { - // TODO - } - } - - Component.onCompleted: { - } -} diff --git a/Fk/ModMaker/Block/Workspace.qml b/Fk/ModMaker/Block/Workspace.qml deleted file mode 100644 index 556a4e8e..00000000 --- a/Fk/ModMaker/Block/Workspace.qml +++ /dev/null @@ -1,79 +0,0 @@ -import QtQuick -import QtQuick.Controls - -Item { - id: root - property var blockComponent - property var allBlocks: [] - - // ====== TMP ====== - property int idx: 0 - Row { - Button { - text: "quit" - onClicked: modStack.pop(); - } - Button { - text: "New" - onClicked: newBlock(); - } - Button { - text: "Del" - onClicked: rmFirstBlock_(); - } - } - - function newBlock() { - let obj = blockComponent.createObject(root, { - width: 50, height: 50, - x: Math.random() * root.width, y: Math.random() * root.height, - workspace: root, draggable: true, - idx: ++idx, - }); - allBlocks.push(obj); - } - - function rmFirstBlock_() { - let obj = allBlocks[0]; - if (!obj) return; - obj.destroy(); - allBlocks.splice(0,1); - } - // ====== TMP ====== - - function getPasteBlock(block) { - let ret; - let min = Infinity; - const x = block.x; - const y = block.y; - allBlocks.forEach(b => { - if (b === block) return; - let dx = Math.abs(b.x - x); - let dy = y - b.y - b.height; - let tot = dx + dy; - if (dx < 60 && dy < 60 && dy > 0 && tot < 100) { - if (min > tot) { - if (!allBlocks.find(bb => bb.x === b.x && bb.y === b.y + b.height)) { - ret = b; - min = tot; - } - } - } - }); - return ret; - } - - function showPasteBlock(block) { - } - - function arrangeBlock(block) { - let b = getPasteBlock(block); - if (b) { - block.pasteTo(b); - } - } - - Component.onCompleted: { - blockComponent = Qt.createComponent('Block.qml'); - } -} diff --git a/Fk/ModMaker/CreateSomething.qml b/Fk/ModMaker/CreateSomething.qml deleted file mode 100644 index 6db0988c..00000000 --- a/Fk/ModMaker/CreateSomething.qml +++ /dev/null @@ -1,52 +0,0 @@ -import QtQuick -import QtQuick.Controls -import QtQuick.Layouts - -ColumnLayout { - id: root - anchors.fill: parent - anchors.margins: 16 - signal finished() - signal accepted(string result) - - property string head - property string hint - - Text { - text: qsTr(head) - font.pixelSize: 20 - font.bold: true - Layout.fillWidth: true - wrapMode: Text.WordWrap - } - - Text { - text: qsTr(hint) - Layout.fillWidth: true - wrapMode: Text.WordWrap - } - - Text { - text: qsTr("validator_hint") - Layout.fillWidth: true - wrapMode: Text.WordWrap - } - - TextField { - id: edit - font.pixelSize: 18 - Layout.fillWidth: true - validator: RegularExpressionValidator { - regularExpression: /[0-9A-Za-z_]+/ - } - } - - Button { - text: "OK" - enabled: edit.text.length >= 4 - onClicked: { - accepted(edit.text); - finished(); - } - } -} diff --git a/Fk/ModMaker/ModConfig.qml b/Fk/ModMaker/ModConfig.qml deleted file mode 100644 index 2ac5e04a..00000000 --- a/Fk/ModMaker/ModConfig.qml +++ /dev/null @@ -1,37 +0,0 @@ -import QtQuick - -QtObject { - property var conf - - property string userName - property string email - property var modList: [] - - function loadConf() { - conf = JSON.parse(ModBackend.readFile("mymod/config.json")); - userName = conf.userName ?? ""; - email = conf.email ?? ""; - modList = conf.modList ?? []; - } - - function saveConf() { - conf.userName = userName; - conf.email = email; - conf.modList = modList; - - ModBackend.saveToFile("mymod/config.json", - JSON.stringify(conf, undefined, 2)); - } - - function addMod(mod) { - modList.push(mod); - saveConf(); - modListChanged(); - } - - function removeMod(mod) { - modList.splice(modList.indexOf(mod), 1); - saveConf(); - modListChanged(); - } -} diff --git a/Fk/ModMaker/ModDetail.qml b/Fk/ModMaker/ModDetail.qml deleted file mode 100644 index 779ec4c9..00000000 --- a/Fk/ModMaker/ModDetail.qml +++ /dev/null @@ -1,147 +0,0 @@ -import QtQuick -import QtQuick.Controls -import QtQuick.Layouts - -Item { - id: root - property var mod: ({}) - property string modName - property string modPath: "mymod/" + modName + "/" - - onModNameChanged: { - mod = JSON.parse(ModBackend.readFile(modPath + "mod.json")); - } - - ToolBar { - id: bar - width: parent.width - RowLayout { - anchors.fill: parent - ToolButton { - icon.source: AppPath + "/image/modmaker/back" - onClicked: modStack.pop(); - } - Label { - text: qsTr("ModMaker") + " - " + modName - horizontalAlignment: Qt.AlignHCenter - Layout.fillWidth: true - } - ToolButton { - icon.source: AppPath + "/image/modmaker/menu" - onClicked: { - } - } - } - } - - Rectangle { - width: parent.width - height: parent.height - bar.height - anchors.top: bar.bottom - color: "snow" - opacity: 0.75 - - ListView { - anchors.fill: parent - model: mod.packages ?? [] - delegate: SwipeDelegate { - width: root.width - text: modelData - - swipe.right: Label { - id: deleteLabel - text: qsTr("Delete") - color: "white" - verticalAlignment: Label.AlignVCenter - padding: 12 - height: parent.height - anchors.right: parent.right - opacity: swipe.complete ? 1 : 0 - Behavior on opacity { NumberAnimation { } } - - SwipeDelegate.onClicked: deletePackage(modelData); - - background: Rectangle { - color: deleteLabel.SwipeDelegate.pressed ? Qt.darker("tomato", 1.1) - : "tomato" - } - } - } - } - } - - RoundButton { - anchors.right: parent.right - anchors.bottom: parent.bottom - anchors.margins: 40 - scale: 2 - icon.source: AppPath + "/image/modmaker/add" - onClicked: { - dialog.source = "CreateSomething.qml"; - dialog.item.head = "create_package"; - dialog.item.hint = "create_package_hint"; - drawer.open(); - dialog.item.accepted.connect((name) => { - createNewPackage(name); - }); - } - } - - Drawer { - id: drawer - width: parent.width * 0.4 / mainWindow.scale - height: parent.height / mainWindow.scale - dim: false - clip: true - dragMargin: 0 - scale: mainWindow.scale - transformOrigin: Item.TopLeft - - Loader { - id: dialog - anchors.fill: parent - onSourceChanged: { - if (item === null) - return; - item.finished.connect(() => { - sourceComponent = undefined; - drawer.close(); - }); - } - onSourceComponentChanged: sourceChanged(); - } - } - - function createNewPackage(name) { - const new_name = modName + "_" + name; - mod.packages = mod.packages ?? []; - if (mod.packages.indexOf(new_name) !== -1) { - toast.show(qsTr("cannot use this package name")); - return; - } - - const path = modPath + new_name + "/"; - ModBackend.mkdir(path); - mod.packages.push(new_name); - ModBackend.saveToFile(modPath + "mod.json", - JSON.stringify(mod, undefined, 2)); - - const pkgInfo = { - name: new_name, - }; - ModBackend.saveToFile(path + "pkg.json", - JSON.stringify(pkgInfo, undefined, 2)); - - root.modChanged(); - } - - function deletePackage(name) { - const path = modPath + name + "/"; - ModBackend.rmrf(path); - mod.packages.splice(mod.packages.indexOf(name), 1); - ModBackend.saveToFile(modPath + "mod.json", - JSON.stringify(mod, undefined, 2)); - - root.modChanged(); - } -} diff --git a/Fk/ModMaker/ModInit.qml b/Fk/ModMaker/ModInit.qml deleted file mode 100644 index dd6a6fbe..00000000 --- a/Fk/ModMaker/ModInit.qml +++ /dev/null @@ -1,164 +0,0 @@ -import QtQuick -import QtQuick.Controls -import QtQuick.Layouts - -Item { - id: root - property bool configOK: modConfig.userName !== "" && modConfig.email !== "" - - ToolBar { - id: bar - width: parent.width - RowLayout { - anchors.fill: parent - ToolButton { - icon.source: AppPath + "/image/modmaker/back" - onClicked: mainStack.pop(); - } - Label { - text: qsTr("ModMaker") - horizontalAlignment: Qt.AlignHCenter - Layout.fillWidth: true - } - Button { - text: "Test" - onClicked: { - const component = Qt.createComponent("Block/Workspace.qml"); - if (component.status !== Component.Ready) { - return; - } - const page = component.createObject(null); - modStack.push(page); - } - } - ToolButton { - icon.source: AppPath + "/image/modmaker/menu" - onClicked: { - dialog.source = "UserInfo.qml"; - drawer.open(); - } - } - } - } - - Rectangle { - width: parent.width - height: parent.height - bar.height - anchors.top: bar.bottom - color: "snow" - opacity: 0.75 - - Text { - anchors.centerIn: parent - text: root.configOK ? "" : qsTr("config is incomplete") - } - - ListView { - anchors.fill: parent - model: modConfig.modList - clip: true - delegate: SwipeDelegate { - width: root.width - text: modelData - - onClicked: { - const component = Qt.createComponent("ModDetail.qml"); - if (component.status !== Component.Ready) { - return; - } - const page = component.createObject(null, { modName: modelData }); - modStack.push(page); - } - - swipe.right: Label { - id: deleteLabel - text: qsTr("Delete") - color: "white" - verticalAlignment: Label.AlignVCenter - padding: 12 - height: parent.height - anchors.right: parent.right - opacity: swipe.complete ? 1 : 0 - Behavior on opacity { NumberAnimation { } } - - SwipeDelegate.onClicked: deleteMod(modelData); - - background: Rectangle { - color: deleteLabel.SwipeDelegate.pressed ? Qt.darker("tomato", 1.1) - : "tomato" - } - } - } - } - } - - RoundButton { - visible: root.configOK - anchors.right: parent.right - anchors.bottom: parent.bottom - anchors.margins: 40 - scale: 2 - icon.source: AppPath + "/image/modmaker/add" - onClicked: { - dialog.source = "CreateSomething.qml"; - dialog.item.head = "create_mod"; - dialog.item.hint = "create_mod_hint"; - drawer.open(); - dialog.item.accepted.connect((name) => { - createNewMod(name); - }); - } - } - - Drawer { - id: drawer - width: parent.width * 0.4 / mainWindow.scale - height: parent.height / mainWindow.scale - dim: false - clip: true - dragMargin: 0 - scale: mainWindow.scale - transformOrigin: Item.TopLeft - - Loader { - id: dialog - anchors.fill: parent - onSourceChanged: { - if (item === null) - return; - item.finished.connect(() => { - sourceComponent = undefined; - drawer.close(); - }); - } - onSourceComponentChanged: sourceChanged(); - } - } - - function createNewMod(name) { - const banned = [ "test", "standard", "standard_cards", "maneuvering" ]; - if (banned.indexOf(name) !== -1 || - modConfig.modList.indexOf(name) !== -1) { - toast.show(qsTr("cannot use this mod name")); - return; - } - ModBackend.createMod(name); - const modInfo = { - name: name, - descrption: "", - author: modConfig.userName, - }; - ModBackend.saveToFile(`mymod/${name}/mod.json`, - JSON.stringify(modInfo, undefined, 2)); - ModBackend.saveToFile(`mymod/${name}/.gitignore`, "init.lua"); - ModBackend.stageFiles(name); - ModBackend.commitChanges(name, "Initial commit", modConfig.userName, - modConfig.email); - modConfig.addMod(name); - } - - function deleteMod(name) { - ModBackend.removeMod(name); - modConfig.removeMod(name); - } -} diff --git a/Fk/ModMaker/UserInfo.qml b/Fk/ModMaker/UserInfo.qml deleted file mode 100644 index 2c91b5c0..00000000 --- a/Fk/ModMaker/UserInfo.qml +++ /dev/null @@ -1,75 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later - -import QtQuick -import QtQuick.Controls -import QtQuick.Layouts - -ColumnLayout { - anchors.fill: parent - anchors.margins: 16 - signal finished() - - Text { - text: qsTr("help_text") - Layout.fillWidth: true - wrapMode: Text.WordWrap - } - - RowLayout { - anchors.rightMargin: 8 - spacing: 16 - Text { - text: qsTr("username") - } - TextField { - id: userName - font.pixelSize: 18 - text: modConfig.userName - Layout.fillWidth: true - onTextChanged: { - modConfig.userName = text; - modConfig.saveConf(); - } - } - } - - RowLayout { - anchors.rightMargin: 8 - spacing: 16 - Text { - text: qsTr("email") - } - TextField { - id: emailAddr - font.pixelSize: 18 - Layout.fillWidth: true - text: modConfig.email - onTextChanged: { - modConfig.email = text; - modConfig.saveConf(); - } - } - } - - Text { - text: qsTr("key_help_text") - Layout.fillWidth: true - wrapMode: Text.WordWrap - textFormat: Text.RichText - onLinkActivated: Qt.openUrlExternally(link); - } - - Button { - text: qsTr("copy pubkey") - Layout.fillWidth: true - onClicked: { - const key = "mymod/id_rsa.pub"; - if (!Backend.exists(key)) { - ModBackend.initKey(); - } - const pubkey = ModBackend.readFile(key); - Backend.copyToClipboard(pubkey); - toast.show(qsTr("pubkey copied")); - } - } -} diff --git a/Fk/ModMaker/main.qml b/Fk/ModMaker/main.qml deleted file mode 100644 index 49a8e2b6..00000000 --- a/Fk/ModMaker/main.qml +++ /dev/null @@ -1,57 +0,0 @@ -import QtQuick -import QtQuick.Controls - -Item { - Component { id: modInit; ModInit {} } - - StackView { - id: modStack - anchors.fill: parent - /* - pushEnter: Transition { - PropertyAnimation { - property: "opacity" - from: 0 - to:1 - duration: 200 - } - } - pushExit: Transition { - PropertyAnimation { - property: "opacity" - from: 1 - to:0 - duration: 200 - } - } - popEnter: Transition { - PropertyAnimation { - property: "opacity" - from: 0 - to:1 - duration: 200 - } - } - popExit: Transition { - PropertyAnimation { - property: "opacity" - from: 1 - to:0 - duration: 200 - } - } - */ - } - - ModConfig { - id: modConfig - } - - Component.onCompleted: { - if (!ModBackend) { - Backend.createModBackend(); - } - modConfig.loadConf(); - modStack.push(modInit); - } -} diff --git a/Fk/ModMaker/qmldir b/Fk/ModMaker/qmldir deleted file mode 100644 index 4a0e4264..00000000 --- a/Fk/ModMaker/qmldir +++ /dev/null @@ -1,2 +0,0 @@ -module Fk.ModMaker -ModMaker 1.0 main.qml diff --git a/docs/Doxyfile b/docs/Doxyfile new file mode 100644 index 00000000..e39e35cf --- /dev/null +++ b/docs/Doxyfile @@ -0,0 +1,364 @@ +# Doxyfile 1.12.0 - comments removed + +DOXYFILE_ENCODING = UTF-8 +PROJECT_NAME = "FreeKill" +PROJECT_NUMBER = +PROJECT_BRIEF = +PROJECT_LOGO = +PROJECT_ICON = +OUTPUT_DIRECTORY = build +CREATE_SUBDIRS = NO +CREATE_SUBDIRS_LEVEL = 8 +ALLOW_UNICODE_NAMES = NO +OUTPUT_LANGUAGE = Chinese +BRIEF_MEMBER_DESC = YES +REPEAT_BRIEF = NO +ABBREVIATE_BRIEF = "The $name class" \ + "The $name widget" \ + "The $name file" \ + is \ + provides \ + specifies \ + contains \ + represents \ + a \ + an \ + the + +ALWAYS_DETAILED_SEC = NO +INLINE_INHERITED_MEMB = NO +FULL_PATH_NAMES = NO +STRIP_FROM_PATH = +STRIP_FROM_INC_PATH = +SHORT_NAMES = NO +JAVADOC_AUTOBRIEF = NO +JAVADOC_BANNER = NO +QT_AUTOBRIEF = NO +MULTILINE_CPP_IS_BRIEF = NO +PYTHON_DOCSTRING = YES +INHERIT_DOCS = YES +SEPARATE_MEMBER_PAGES = NO +TAB_SIZE = 4 +ALIASES = +OPTIMIZE_OUTPUT_FOR_C = NO +OPTIMIZE_OUTPUT_JAVA = NO +OPTIMIZE_FOR_FORTRAN = NO +OPTIMIZE_OUTPUT_VHDL = NO +OPTIMIZE_OUTPUT_SLICE = NO +EXTENSION_MAPPING = +MARKDOWN_SUPPORT = YES +TOC_INCLUDE_HEADINGS = 6 +MARKDOWN_ID_STYLE = DOXYGEN +AUTOLINK_SUPPORT = YES +BUILTIN_STL_SUPPORT = NO +CPP_CLI_SUPPORT = NO +SIP_SUPPORT = NO +IDL_PROPERTY_SUPPORT = YES +DISTRIBUTE_GROUP_DOC = NO +GROUP_NESTED_COMPOUNDS = NO +SUBGROUPING = YES +INLINE_GROUPED_CLASSES = NO +INLINE_SIMPLE_STRUCTS = NO +TYPEDEF_HIDES_STRUCT = NO +LOOKUP_CACHE_SIZE = 0 +NUM_PROC_THREADS = 4 +TIMESTAMP = YES +EXTRACT_ALL = NO +EXTRACT_PRIVATE = YES +EXTRACT_PRIV_VIRTUAL = NO +EXTRACT_PACKAGE = NO +EXTRACT_STATIC = NO +EXTRACT_LOCAL_CLASSES = YES +EXTRACT_LOCAL_METHODS = NO +EXTRACT_ANON_NSPACES = NO +RESOLVE_UNNAMED_PARAMS = YES +HIDE_UNDOC_MEMBERS = NO +HIDE_UNDOC_CLASSES = NO +HIDE_FRIEND_COMPOUNDS = NO +HIDE_IN_BODY_DOCS = NO +INTERNAL_DOCS = NO +CASE_SENSE_NAMES = SYSTEM +HIDE_SCOPE_NAMES = NO +HIDE_COMPOUND_REFERENCE= NO +SHOW_HEADERFILE = NO +SHOW_INCLUDE_FILES = NO +SHOW_GROUPED_MEMB_INC = NO +FORCE_LOCAL_INCLUDES = NO +INLINE_INFO = YES +SORT_MEMBER_DOCS = YES +SORT_BRIEF_DOCS = NO +SORT_MEMBERS_CTORS_1ST = NO +SORT_GROUP_NAMES = NO +SORT_BY_SCOPE_NAME = NO +STRICT_PROTO_MATCHING = NO +GENERATE_TODOLIST = YES +GENERATE_TESTLIST = YES +GENERATE_BUGLIST = YES +GENERATE_DEPRECATEDLIST= YES +ENABLED_SECTIONS = +MAX_INITIALIZER_LINES = 30 +SHOW_USED_FILES = YES +SHOW_FILES = YES +SHOW_NAMESPACES = YES +FILE_VERSION_FILTER = +LAYOUT_FILE = +CITE_BIB_FILES = +EXTERNAL_TOOL_PATH = +QUIET = NO +WARNINGS = YES +WARN_IF_UNDOCUMENTED = YES +WARN_IF_DOC_ERROR = YES +WARN_IF_INCOMPLETE_DOC = YES +WARN_NO_PARAMDOC = NO +WARN_IF_UNDOC_ENUM_VAL = NO +WARN_AS_ERROR = NO +WARN_FORMAT = "$file:$line: $text" +WARN_LINE_FORMAT = "at line $line of file $file" +WARN_LOGFILE = +INPUT = ../src +INPUT_ENCODING = UTF-8 +INPUT_FILE_ENCODING = +FILE_PATTERNS = *.c \ + *.cc \ + *.cxx \ + *.cxxm \ + *.cpp \ + *.cppm \ + *.ccm \ + *.c++ \ + *.c++m \ + *.java \ + *.ii \ + *.ixx \ + *.ipp \ + *.i++ \ + *.inl \ + *.idl \ + *.ddl \ + *.odl \ + *.h \ + *.hh \ + *.hxx \ + *.hpp \ + *.h++ \ + *.ixx \ + *.l \ + *.cs \ + *.d \ + *.php \ + *.php4 \ + *.php5 \ + *.phtml \ + *.inc \ + *.m \ + *.markdown \ + *.md \ + *.mm \ + *.dox \ + *.py \ + *.pyw \ + *.f90 \ + *.f95 \ + *.f03 \ + *.f08 \ + *.f18 \ + *.f \ + *.for \ + *.vhd \ + *.vhdl \ + *.ucf \ + *.qsf \ + *.ice + +RECURSIVE = YES +EXCLUDE = ../src/swig/ +EXCLUDE_SYMLINKS = NO +EXCLUDE_PATTERNS = +EXCLUDE_SYMBOLS = +EXAMPLE_PATH = +EXAMPLE_PATTERNS = * +EXAMPLE_RECURSIVE = NO +IMAGE_PATH = +INPUT_FILTER = +FILTER_PATTERNS = +FILTER_SOURCE_FILES = NO +FILTER_SOURCE_PATTERNS = +USE_MDFILE_AS_MAINPAGE = +FORTRAN_COMMENT_AFTER = 72 +SOURCE_BROWSER = NO +INLINE_SOURCES = NO +STRIP_CODE_COMMENTS = YES +REFERENCED_BY_RELATION = NO +REFERENCES_RELATION = NO +REFERENCES_LINK_SOURCE = YES +SOURCE_TOOLTIPS = YES +USE_HTAGS = NO +VERBATIM_HEADERS = YES +CLANG_ASSISTED_PARSING = NO +CLANG_ADD_INC_PATHS = YES +CLANG_OPTIONS = +CLANG_DATABASE_PATH = +ALPHABETICAL_INDEX = YES +IGNORE_PREFIX = +GENERATE_HTML = YES +HTML_OUTPUT = html +HTML_FILE_EXTENSION = .html +HTML_HEADER = +HTML_FOOTER = +HTML_STYLESHEET = +HTML_EXTRA_STYLESHEET = +HTML_EXTRA_FILES = +HTML_COLORSTYLE = AUTO_LIGHT +HTML_COLORSTYLE_HUE = 220 +HTML_COLORSTYLE_SAT = 100 +HTML_COLORSTYLE_GAMMA = 80 +HTML_DYNAMIC_MENUS = YES +HTML_DYNAMIC_SECTIONS = NO +HTML_CODE_FOLDING = YES +HTML_COPY_CLIPBOARD = YES +HTML_PROJECT_COOKIE = +HTML_INDEX_NUM_ENTRIES = 100 +GENERATE_DOCSET = NO +DOCSET_FEEDNAME = "Doxygen generated docs" +DOCSET_FEEDURL = +DOCSET_BUNDLE_ID = org.doxygen.Project +DOCSET_PUBLISHER_ID = org.doxygen.Publisher +DOCSET_PUBLISHER_NAME = Publisher +GENERATE_HTMLHELP = NO +CHM_FILE = +HHC_LOCATION = +GENERATE_CHI = NO +CHM_INDEX_ENCODING = +BINARY_TOC = NO +TOC_EXPAND = NO +SITEMAP_URL = +GENERATE_QHP = NO +QCH_FILE = +QHP_NAMESPACE = org.doxygen.Project +QHP_VIRTUAL_FOLDER = doc +QHP_CUST_FILTER_NAME = +QHP_CUST_FILTER_ATTRS = +QHP_SECT_FILTER_ATTRS = +QHG_LOCATION = +GENERATE_ECLIPSEHELP = NO +ECLIPSE_DOC_ID = org.doxygen.Project +DISABLE_INDEX = NO +GENERATE_TREEVIEW = YES +FULL_SIDEBAR = NO +ENUM_VALUES_PER_LINE = 4 +SHOW_ENUM_VALUES = NO +TREEVIEW_WIDTH = 250 +EXT_LINKS_IN_WINDOW = NO +OBFUSCATE_EMAILS = YES +HTML_FORMULA_FORMAT = png +FORMULA_FONTSIZE = 10 +FORMULA_MACROFILE = +USE_MATHJAX = NO +MATHJAX_VERSION = MathJax_2 +MATHJAX_FORMAT = HTML-CSS +MATHJAX_RELPATH = +MATHJAX_EXTENSIONS = +MATHJAX_CODEFILE = +SEARCHENGINE = YES +SERVER_BASED_SEARCH = NO +EXTERNAL_SEARCH = NO +SEARCHENGINE_URL = +SEARCHDATA_FILE = searchdata.xml +EXTERNAL_SEARCH_ID = +EXTRA_SEARCH_MAPPINGS = +GENERATE_LATEX = YES +LATEX_OUTPUT = latex +LATEX_CMD_NAME = +MAKEINDEX_CMD_NAME = makeindex +LATEX_MAKEINDEX_CMD = makeindex +COMPACT_LATEX = NO +PAPER_TYPE = a4 +EXTRA_PACKAGES = +LATEX_HEADER = +LATEX_FOOTER = +LATEX_EXTRA_STYLESHEET = +LATEX_EXTRA_FILES = +PDF_HYPERLINKS = YES +USE_PDFLATEX = YES +LATEX_BATCHMODE = NO +LATEX_HIDE_INDICES = NO +LATEX_BIB_STYLE = plain +LATEX_EMOJI_DIRECTORY = +GENERATE_RTF = NO +RTF_OUTPUT = rtf +COMPACT_RTF = NO +RTF_HYPERLINKS = NO +RTF_STYLESHEET_FILE = +RTF_EXTENSIONS_FILE = +RTF_EXTRA_FILES = +GENERATE_MAN = NO +MAN_OUTPUT = man +MAN_EXTENSION = .3 +MAN_SUBDIR = +MAN_LINKS = NO +GENERATE_XML = NO +XML_OUTPUT = xml +XML_PROGRAMLISTING = YES +XML_NS_MEMB_FILE_SCOPE = NO +GENERATE_DOCBOOK = NO +DOCBOOK_OUTPUT = docbook +GENERATE_AUTOGEN_DEF = NO +GENERATE_SQLITE3 = NO +SQLITE3_OUTPUT = sqlite3 +SQLITE3_RECREATE_DB = YES +GENERATE_PERLMOD = NO +PERLMOD_LATEX = NO +PERLMOD_PRETTY = YES +PERLMOD_MAKEVAR_PREFIX = +ENABLE_PREPROCESSING = YES +MACRO_EXPANSION = NO +EXPAND_ONLY_PREDEF = NO +SEARCH_INCLUDES = YES +INCLUDE_PATH = +INCLUDE_FILE_PATTERNS = +PREDEFINED = +EXPAND_AS_DEFINED = +SKIP_FUNCTION_MACROS = YES +TAGFILES = +GENERATE_TAGFILE = +ALLEXTERNALS = NO +EXTERNAL_GROUPS = YES +EXTERNAL_PAGES = YES +HIDE_UNDOC_RELATIONS = YES +HAVE_DOT = NO +DOT_NUM_THREADS = 0 +DOT_COMMON_ATTR = "fontname=Helvetica,fontsize=10" +DOT_EDGE_ATTR = "labelfontname=Helvetica,labelfontsize=10" +DOT_NODE_ATTR = "shape=box,height=0.2,width=0.4" +DOT_FONTPATH = +CLASS_GRAPH = YES +COLLABORATION_GRAPH = YES +GROUP_GRAPHS = YES +UML_LOOK = NO +UML_LIMIT_NUM_FIELDS = 10 +DOT_UML_DETAILS = NO +DOT_WRAP_THRESHOLD = 17 +TEMPLATE_RELATIONS = NO +INCLUDE_GRAPH = NO +INCLUDED_BY_GRAPH = NO +CALL_GRAPH = NO +CALLER_GRAPH = NO +GRAPHICAL_HIERARCHY = YES +DIRECTORY_GRAPH = YES +DIR_GRAPH_MAX_DEPTH = 1 +DOT_IMAGE_FORMAT = png +INTERACTIVE_SVG = NO +DOT_PATH = +DOTFILE_DIRS = +DIA_PATH = +DIAFILE_DIRS = +PLANTUML_JAR_PATH = $(PLANTUML_INSTALL_DIR)/plantuml.jar +PLANTUML_CFG_FILE = +PLANTUML_INCLUDE_PATH = +DOT_GRAPH_MAX_NODES = 50 +MAX_DOT_GRAPH_DEPTH = 0 +DOT_MULTI_TARGETS = NO +GENERATE_LEGEND = YES +DOT_CLEANUP = YES +MSCGEN_TOOL = +MSCFILE_DIRS = diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 5ecec622..a1361a5e 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,7 +1,8 @@ # SPDX-License-Identifier: GPL-3.0-or-later set(freekill_SRCS - "main.cpp" + # "main.cpp" + "freekill.cpp" "core/player.cpp" "core/util.cpp" "core/packman.cpp" @@ -95,9 +96,9 @@ else () set(GIT_LIB git2) endif () -target_sources(FreeKill PRIVATE ${freekill_SRCS}) -target_precompile_headers(FreeKill PRIVATE "pch.h") -target_link_libraries(FreeKill PRIVATE +add_library(libFreeKill STATIC ${freekill_SRCS}) +target_precompile_headers(libFreeKill PRIVATE "pch.h") +target_link_libraries(libFreeKill PRIVATE ${LUA_LIB} ${SQLITE3_LIB} ${CRYPTO_LIB} @@ -108,6 +109,10 @@ target_link_libraries(FreeKill PRIVATE ${GIT_LIB} ${IDBFS_LIB} ) +target_sources(FreeKill PRIVATE main.cpp) +target_link_libraries(FreeKill PRIVATE + libFreeKill +) install(TARGETS FreeKill DESTINATION bin) install(DIRECTORY diff --git a/src/core/util.h b/src/core/util.h index 6ac6a439..a0992a61 100644 --- a/src/core/util.h +++ b/src/core/util.h @@ -1,5 +1,14 @@ // SPDX-License-Identifier: GPL-3.0-or-later +/** @file util.h + + util.h负责提供各种全局函数、全局变量等。 + + @todo 好像在调用C库时C++程序会为它们创建包装类才对吧? + 总之这种写法实在是不太推荐,以后说不定会改成另外的写法。 + + */ + #ifndef _GLOBAL_H #define _GLOBAL_H diff --git a/src/freekill.cpp b/src/freekill.cpp new file mode 100644 index 00000000..9ef79e36 --- /dev/null +++ b/src/freekill.cpp @@ -0,0 +1,374 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "client/client.h" +#include "core/util.h" +using namespace fkShell; + +#include "core/packman.h" +#include "server/server.h" +#include "server/shell.h" + +#if defined(Q_OS_WIN32) +#include "applink.c" +#endif + +#ifndef FK_SERVER_ONLY +#include +#include +#include +#ifndef Q_OS_ANDROID +#include +#endif +#include "ui/qmlbackend.h" +#endif + +#if defined(Q_OS_ANDROID) +static bool copyPath(const QString &srcFilePath, const QString &tgtFilePath) { + QFileInfo srcFileInfo(srcFilePath); + if (srcFileInfo.isDir()) { + QDir targetDir(tgtFilePath); + if (!targetDir.exists()) { + targetDir.cdUp(); + if (!targetDir.mkdir(QFileInfo(tgtFilePath).fileName())) + return false; + } + QDir sourceDir(srcFilePath); + QStringList fileNames = + sourceDir.entryList(QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot | + QDir::Hidden | QDir::System); + foreach (const QString &fileName, fileNames) { + const QString newSrcFilePath = srcFilePath + QLatin1Char('/') + fileName; + const QString newTgtFilePath = tgtFilePath + QLatin1Char('/') + fileName; + if (!copyPath(newSrcFilePath, newTgtFilePath)) + return false; + } + } else { + QFile::remove(tgtFilePath); + if (!QFile::copy(srcFilePath, tgtFilePath)) + return false; + } + return true; +} +#endif + +static void installFkAssets(const QString &src, const QString &dest) { + QFile f(dest + "/fk_ver"); + if (f.exists() && f.open(QIODevice::ReadOnly)) { + auto ver = f.readLine().simplified(); + if (ver == FK_VERSION) { + return; + } + } +#ifdef Q_OS_ANDROID + copyPath(src, dest); +#elif defined(Q_OS_LINUX) + system(QString("cp -r %1 %2/..").arg(src).arg(dest).toUtf8()); +#endif +} + +#if defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID) +#include +#include +static void prepareForLinux() { + // 如果用户执行的是 /usr/bin/FreeKill,那么这意味着 freekill 是被包管理器安装 + // 的,所以我们就需要把资源文件都复制到 ~/.local 中,并且切换当前目录 + // TODO: AppImage + char buf[256] = {0}; + int len = readlink("/proc/self/exe", buf, 256); + const char *home = getenv("HOME"); + if (!strcmp(buf, "/usr/bin/FreeKill")) { + system("mkdir -p ~/.local/share/FreeKill"); + installFkAssets("/usr/share/FreeKill", QString("%1/.local/share/FreeKill").arg(home)); + chdir(home); + chdir(".local/share/FreeKill"); + } else if (!strcmp(buf, "/usr/local/bin/FreeKill")) { + system("mkdir -p ~/.local/share/FreeKill"); + installFkAssets("/usr/local/share/FreeKill", QString("%1/.local/share/FreeKill").arg(home)); + chdir(home); + chdir(".local/share/FreeKill"); + } +} +#endif + +static FILE *info_log = nullptr; +static FILE *err_log = nullptr; + +void fkMsgHandler(QtMsgType type, const QMessageLogContext &context, + const QString &msg) { + auto date = QDate::currentDate(); + + FILE *file; + switch (type) { + case QtDebugMsg: + case QtInfoMsg: + file = info_log; + break; + case QtWarningMsg: + case QtCriticalMsg: + case QtFatalMsg: + file = err_log; + break; + } + +#ifdef FK_USE_READLINE + ShellInstance->clearLine(); +#else + printf("\r"); +#endif + printf("%02d/%02d ", date.month(), date.day()); + printf("%s ", + QTime::currentTime().toString("hh:mm:ss").toLatin1().constData()); + fprintf(file, "%02d/%02d ", date.month(), date.day()); + fprintf(file, "%s ", + QTime::currentTime().toString("hh:mm:ss").toLatin1().constData()); + + auto localMsg = msg.toUtf8(); + auto threadName = QThread::currentThread()->objectName().toLatin1(); + + switch (type) { + case QtDebugMsg: + printf("%s[D] %s\n", threadName.constData(), + localMsg.constData()); + fprintf(file, "%s[D] %s\n", threadName.constData(), + localMsg.constData()); + break; + case QtInfoMsg: + printf("%s[%s] %s\n", threadName.constData(), + Color("I", Green).toUtf8().constData(), localMsg.constData()); + fprintf(file, "%s[%s] %s\n", threadName.constData(), + "I", localMsg.constData()); + break; + case QtWarningMsg: + printf("%s[%s] %s\n", threadName.constData(), + Color("W", Yellow, Bold).toUtf8().constData(), + localMsg.constData()); + fprintf(file, "%s[%s] %s\n", threadName.constData(), + "W", localMsg.constData()); + break; + case QtCriticalMsg: + printf("%s[%s] %s\n", threadName.constData(), + Color("C", Red, Bold).toUtf8().constData(), localMsg.constData()); + fprintf(file, "%s[%s] %s\n", threadName.constData(), + "C", localMsg.constData()); +#ifndef FK_SERVER_ONLY + if (Backend != nullptr) { + Backend->notifyUI("ErrorDialog", + QString("⛔ %1/Error occured!\n %2").arg(threadName).arg(localMsg)); + } +#endif + break; + case QtFatalMsg: + printf("%s[%s] %s\n", threadName.constData(), + Color("E", Red, Bold).toUtf8().constData(), localMsg.constData()); + fprintf(file, "%s[%s] %s\n", threadName.constData(), + "E", localMsg.constData()); + break; + } + +#ifdef FK_USE_READLINE + if (ShellInstance && !ShellInstance->lineDone()) { + ShellInstance->redisplay(); + } +#endif +} + +// FreeKill 的程序主入口。整个程序就是从这里开始执行的。 +int freekill_main(int argc, char *argv[]) { + // 初始化一下各种杂项信息 + QThread::currentThread()->setObjectName("Main"); + + qInstallMessageHandler(fkMsgHandler); + QCoreApplication *app; + QCoreApplication::setApplicationName("FreeKill"); + QCoreApplication::setApplicationVersion(FK_VERSION); + + if (GetDeviceUuid() == "c5e8983a3d85a07c") return 1; + +#if defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID) + prepareForLinux(); +#endif + + if (!info_log) { + info_log = fopen("freekill.server.info.log", "w+"); + if (!info_log) { + qFatal("Cannot open info.log"); + } + } + if (!err_log) { + err_log = fopen("freekill.server.error.log", "w+"); + if (!err_log) { + qFatal("Cannot open error.log"); + } + } + + // 分析命令行,如果有 -s 或者 --server 就在命令行直接开服务器 + QCommandLineParser parser; + parser.setApplicationDescription("FreeKill server"); + parser.addVersionOption(); + parser.addOption({{"s", "server"}, "start server at ", "port"}); + parser.addOption({{"h", "help"}, "display help information"}); + QStringList cliOptions; + for (int i = 0; i < argc; i++) + cliOptions << argv[i]; + + parser.parse(cliOptions); + if (parser.isSet("version")) { + parser.showVersion(); + return 0; + } else if (parser.isSet("help")) { + parser.showHelp(); + return 0; + } + + bool startServer = parser.isSet("server"); + ushort serverPort = 9527; + + if (startServer) { + app = new QCoreApplication(argc, argv); + QTranslator translator; + Q_UNUSED(translator.load("zh_CN.qm")); + QCoreApplication::installTranslator(&translator); + + bool ok = false; + if (parser.value("server").toInt(&ok) && ok) + serverPort = parser.value("server").toInt(); + + Pacman = new PackMan; + Server *server = new Server; + if (!server->listen(QHostAddress::Any, serverPort)) { + qFatal("cannot listen on port %d!\n", serverPort); + app->exit(1); + } else { + qInfo("Server is listening on port %d", serverPort); + auto shell = new Shell; + shell->start(); + } + return app->exec(); + } + +#ifdef FK_SERVER_ONLY + // 根本没编译 GUI 相关的功能,直接在此退出 + qFatal("This is server-only build and have no GUI support.\n\ + Please use ./FreeKill -s to start a server in command line."); +#else + + app = new QApplication(argc, argv); +#ifdef DESKTOP_BUILD + ((QApplication *)app)->setWindowIcon(QIcon("image/icon.png")); +#endif + +#define SHOW_SPLASH_MSG(msg) \ + splash.showMessage(msg, Qt::AlignHCenter | Qt::AlignBottom); + +#ifdef Q_OS_ANDROID + // 投降喵,设为android根本无效 + // 直接改用Android原生Mediaplayer了,不用你Qt家的 + // qputenv("QT_MEDIA_BACKEND", "android"); + + // 安卓:获取系统语言需要使用Java才行 + QString localeName = QJniObject::callStaticObjectMethod("org/notify/FreeKill/Helper", "GetLocaleCode", "()Ljava/lang/String;").toString(); + + // 安卓:先切换到我们安装程序的那个外部存储目录去 + QJniObject::callStaticMethod("org/notify/FreeKill/Helper", "InitView", + "()V"); + QDir::setCurrent( + "/storage/emulated/0/Android/data/org.notify.FreeKill/files"); + + // 然后显示欢迎界面,并在需要时复制资源素材等 + QScreen *screen = qobject_cast(app)->primaryScreen(); + QRect screenGeometry = screen->geometry(); + int screenWidth = screenGeometry.width(); + int screenHeight = screenGeometry.height(); + QSplashScreen splash(QPixmap("assets:/res/image/splash.jpg") + .scaled(screenWidth, screenHeight)); + splash.showFullScreen(); + SHOW_SPLASH_MSG("Copying resources..."); + installFkAssets("assets:/res", QDir::currentPath()); + + info_log = freopen("freekill.server.info.log", "w+", info_log); + err_log = freopen("freekill.server.error.log", "w+", err_log); +#else + // 不是安卓,使用QLocale获得系统语言 + QLocale l = QLocale::system(); + auto localeName = l.name(); + + // 不是安卓,那么直接启动欢迎界面,也就是不复制东西了 + QSplashScreen splash(QPixmap("image/splash.jpg")); + splash.show(); +#endif + + SHOW_SPLASH_MSG("Loading qml files..."); + QQmlApplicationEngine *engine = new QQmlApplicationEngine; +#ifndef Q_OS_ANDROID + QQuickStyle::setStyle("Material"); +#endif + + QTranslator translator; + if (localeName.startsWith("zh_")) { + Q_UNUSED(translator.load("zh_CN.qm")); + } else { + Q_UNUSED(translator.load("en_US.qm")); + } + QCoreApplication::installTranslator(&translator); + + QmlBackend backend; + backend.setEngine(engine); + + Pacman = new PackMan; + + // 向 Qml 中先定义几个全局变量 + auto root = engine->rootContext(); + root->setContextProperty("FkVersion", FK_VERSION); + root->setContextProperty("Backend", &backend); + root->setContextProperty("ModBackend", nullptr); + root->setContextProperty("Pacman", Pacman); + root->setContextProperty("SysLocale", localeName); + +#ifdef QT_DEBUG + bool debugging = true; +#else + bool debugging = false; +#endif + engine->rootContext()->setContextProperty("Debugging", debugging); + + QString system; +#if defined(Q_OS_ANDROID) + system = "Android"; +#elif defined(Q_OS_WIN32) + qputenv("QT_MEDIA_BACKEND", "windows"); + system = "Win"; + ::system("chcp 65001"); +#elif defined(Q_OS_LINUX) + system = "Linux"; +#else + system = "Other"; +#endif + root->setContextProperty("OS", system); + + root->setContextProperty( + "AppPath", QUrl::fromLocalFile(QDir::currentPath())); + + engine->addImportPath(QDir::currentPath()); + + // 加载完全局变量后,就再去加载 main.qml,此时UI界面正式显示 + engine->load("Fk/main.qml"); + + // qml 报错了就直接退出吧 + if (engine->rootObjects().isEmpty()) + return -1; + + // 关闭欢迎界面,然后进入Qt主循环 + splash.close(); + int ret = app->exec(); + + // 先删除 engine + // 防止报一堆错 "TypeError: Cannot read property 'xxx' of null" + delete engine; + delete Pacman; + + if (info_log) fclose(info_log); + if (err_log) fclose(err_log); + + return ret; +#endif +} diff --git a/src/freekill.h b/src/freekill.h new file mode 100644 index 00000000..28c79e9b --- /dev/null +++ b/src/freekill.h @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef _FREEKILL_H +#define _FREEKILL_H + +int freekill_main(int argc, char **argv); + +#endif // _FREEKILL_H diff --git a/src/main.cpp b/src/main.cpp index 9408d686..89525db4 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,374 +1,39 @@ // SPDX-License-Identifier: GPL-3.0-or-later -#include "client/client.h" -#include "core/util.h" -using namespace fkShell; - -#include "core/packman.h" -#include "server/server.h" -#include "server/shell.h" - -#if defined(Q_OS_WIN32) -#include "applink.c" -#endif - -#ifndef FK_SERVER_ONLY -#include -#include -#include -#ifndef Q_OS_ANDROID -#include -#endif -#include "ui/qmlbackend.h" -#endif - -#if defined(Q_OS_ANDROID) -static bool copyPath(const QString &srcFilePath, const QString &tgtFilePath) { - QFileInfo srcFileInfo(srcFilePath); - if (srcFileInfo.isDir()) { - QDir targetDir(tgtFilePath); - if (!targetDir.exists()) { - targetDir.cdUp(); - if (!targetDir.mkdir(QFileInfo(tgtFilePath).fileName())) - return false; - } - QDir sourceDir(srcFilePath); - QStringList fileNames = - sourceDir.entryList(QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot | - QDir::Hidden | QDir::System); - foreach (const QString &fileName, fileNames) { - const QString newSrcFilePath = srcFilePath + QLatin1Char('/') + fileName; - const QString newTgtFilePath = tgtFilePath + QLatin1Char('/') + fileName; - if (!copyPath(newSrcFilePath, newTgtFilePath)) - return false; - } - } else { - QFile::remove(tgtFilePath); - if (!QFile::copy(srcFilePath, tgtFilePath)) - return false; - } - return true; -} -#endif - -static void installFkAssets(const QString &src, const QString &dest) { - QFile f(dest + "/fk_ver"); - if (f.exists() && f.open(QIODevice::ReadOnly)) { - auto ver = f.readLine().simplified(); - if (ver == FK_VERSION) { - return; - } - } -#ifdef Q_OS_ANDROID - copyPath(src, dest); -#elif defined(Q_OS_LINUX) - system(QString("cp -r %1 %2/..").arg(src).arg(dest).toUtf8()); -#endif +// 为了写测试而特意给程序本身单独分出一个main.cpp 顺便包含项目文档(这样真的好吗) +#include "freekill.h" +int main(int argc, char **argv) { + return freekill_main(argc, argv); } -#if defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID) -#include -#include -static void prepareForLinux() { - // 如果用户执行的是 /usr/bin/FreeKill,那么这意味着 freekill 是被包管理器安装 - // 的,所以我们就需要把资源文件都复制到 ~/.local 中,并且切换当前目录 - // TODO: AppImage - char buf[256] = {0}; - int len = readlink("/proc/self/exe", buf, 256); - const char *home = getenv("HOME"); - if (!strcmp(buf, "/usr/bin/FreeKill")) { - system("mkdir -p ~/.local/share/FreeKill"); - installFkAssets("/usr/share/FreeKill", QString("%1/.local/share/FreeKill").arg(home)); - chdir(home); - chdir(".local/share/FreeKill"); - } else if (!strcmp(buf, "/usr/local/bin/FreeKill")) { - system("mkdir -p ~/.local/share/FreeKill"); - installFkAssets("/usr/local/share/FreeKill", QString("%1/.local/share/FreeKill").arg(home)); - chdir(home); - chdir(".local/share/FreeKill"); - } -} -#endif +/** @mainpage 新月杀文档 - Cpp代码部分 -static FILE *info_log = nullptr; -static FILE *err_log = nullptr; + 本文档专门针对新月杀的C++代码部分,采用Doxygen生成。 + 关于项目的主文档请参见新月之书: https://fkbook-all-in-one.readthedocs.io/ -void fkMsgHandler(QtMsgType type, const QMessageLogContext &context, - const QString &msg) { - auto date = QDate::currentDate(); + > 单独分出一个Doxygen页面而不是合并在新月之书中, + 完全因为懒得给新月之书拖一个新的submodule =.= - FILE *file; - switch (type) { - case QtDebugMsg: - case QtInfoMsg: - file = info_log; - break; - case QtWarningMsg: - case QtCriticalMsg: - case QtFatalMsg: - file = err_log; - break; - } + C++的代码位于src/文件夹下,其覆盖的功能为: -#ifdef FK_USE_READLINE - ShellInstance->clearLine(); -#else - printf("\r"); -#endif - printf("%02d/%02d ", date.month(), date.day()); - printf("%s ", - QTime::currentTime().toString("hh:mm:ss").toLatin1().constData()); - fprintf(file, "%02d/%02d ", date.month(), date.day()); - fprintf(file, "%s ", - QTime::currentTime().toString("hh:mm:ss").toLatin1().constData()); + - freekill.cpp: 程序入口,Linux与Android环境下提前部署环境等 + - swig/: 基于SWIG的Lua-cpp接口。需要测试的是里面的函数定义(而非函数声明)。 + - ui/: Qml-cpp接口,以及一些Lua-cpp接口,借此实现lua和qml之间的交互 + - core/: 主要是拓展包管理,以及对于用户(玩家)的定义,以及一些第三方库的封装 + - client/: 主要负责录像,以及加载client侧的Lua,功能其实不多 + - network/: 对Qt Network模块封装,加密/压缩传输,基于JSON通信协议 + - server/: 登录,大厅,房间管理,房间调度,管理员shell - auto localMsg = msg.toUtf8(); - auto threadName = QThread::currentThread()->objectName().toLatin1(); + 比较复杂的主要是服务端代码。具体可以查看每个类的文档 - switch (type) { - case QtDebugMsg: - printf("%s[D] %s\n", threadName.constData(), - localMsg.constData()); - fprintf(file, "%s[D] %s\n", threadName.constData(), - localMsg.constData()); - break; - case QtInfoMsg: - printf("%s[%s] %s\n", threadName.constData(), - Color("I", Green).toUtf8().constData(), localMsg.constData()); - fprintf(file, "%s[%s] %s\n", threadName.constData(), - "I", localMsg.constData()); - break; - case QtWarningMsg: - printf("%s[%s] %s\n", threadName.constData(), - Color("W", Yellow, Bold).toUtf8().constData(), - localMsg.constData()); - fprintf(file, "%s[%s] %s\n", threadName.constData(), - "W", localMsg.constData()); - break; - case QtCriticalMsg: - printf("%s[%s] %s\n", threadName.constData(), - Color("C", Red, Bold).toUtf8().constData(), localMsg.constData()); - fprintf(file, "%s[%s] %s\n", threadName.constData(), - "C", localMsg.constData()); -#ifndef FK_SERVER_ONLY - if (Backend != nullptr) { - Backend->notifyUI("ErrorDialog", - QString("⛔ %1/Error occured!\n %2").arg(threadName).arg(localMsg)); - } -#endif - break; - case QtFatalMsg: - printf("%s[%s] %s\n", threadName.constData(), - Color("E", Red, Bold).toUtf8().constData(), localMsg.constData()); - fprintf(file, "%s[%s] %s\n", threadName.constData(), - "E", localMsg.constData()); - break; - } + @note 为了详细说明程序运行原理,private成员也将会在文档中呈现。 +*/ -#ifdef FK_USE_READLINE - if (ShellInstance && !ShellInstance->lineDone()) { - ShellInstance->redisplay(); - } -#endif -} +/** @page page_network 网络连接 -// FreeKill 的程序主入口。整个程序就是从这里开始执行的。 -int main(int argc, char *argv[]) { - // 初始化一下各种杂项信息 - QThread::currentThread()->setObjectName("Main"); + 相关类: - qInstallMessageHandler(fkMsgHandler); - QCoreApplication *app; - QCoreApplication::setApplicationName("FreeKill"); - QCoreApplication::setApplicationVersion(FK_VERSION); - - if (GetDeviceUuid() == "c5e8983a3d85a07c") return 1; - -#if defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID) - prepareForLinux(); -#endif - - if (!info_log) { - info_log = fopen("freekill.server.info.log", "w+"); - if (!info_log) { - qFatal("Cannot open info.log"); - } - } - if (!err_log) { - err_log = fopen("freekill.server.error.log", "w+"); - if (!err_log) { - qFatal("Cannot open error.log"); - } - } - - // 分析命令行,如果有 -s 或者 --server 就在命令行直接开服务器 - QCommandLineParser parser; - parser.setApplicationDescription("FreeKill server"); - parser.addVersionOption(); - parser.addOption({{"s", "server"}, "start server at ", "port"}); - parser.addOption({{"h", "help"}, "display help information"}); - QStringList cliOptions; - for (int i = 0; i < argc; i++) - cliOptions << argv[i]; - - parser.parse(cliOptions); - if (parser.isSet("version")) { - parser.showVersion(); - return 0; - } else if (parser.isSet("help")) { - parser.showHelp(); - return 0; - } - - bool startServer = parser.isSet("server"); - ushort serverPort = 9527; - - if (startServer) { - app = new QCoreApplication(argc, argv); - QTranslator translator; - Q_UNUSED(translator.load("zh_CN.qm")); - QCoreApplication::installTranslator(&translator); - - bool ok = false; - if (parser.value("server").toInt(&ok) && ok) - serverPort = parser.value("server").toInt(); - - Pacman = new PackMan; - Server *server = new Server; - if (!server->listen(QHostAddress::Any, serverPort)) { - qFatal("cannot listen on port %d!\n", serverPort); - app->exit(1); - } else { - qInfo("Server is listening on port %d", serverPort); - auto shell = new Shell; - shell->start(); - } - return app->exec(); - } - -#ifdef FK_SERVER_ONLY - // 根本没编译 GUI 相关的功能,直接在此退出 - qFatal("This is server-only build and have no GUI support.\n\ - Please use ./FreeKill -s to start a server in command line."); -#else - - app = new QApplication(argc, argv); -#ifdef DESKTOP_BUILD - ((QApplication *)app)->setWindowIcon(QIcon("image/icon.png")); -#endif - -#define SHOW_SPLASH_MSG(msg) \ - splash.showMessage(msg, Qt::AlignHCenter | Qt::AlignBottom); - -#ifdef Q_OS_ANDROID - // 投降喵,设为android根本无效 - // 直接改用Android原生Mediaplayer了,不用你Qt家的 - // qputenv("QT_MEDIA_BACKEND", "android"); - - // 安卓:获取系统语言需要使用Java才行 - QString localeName = QJniObject::callStaticObjectMethod("org/notify/FreeKill/Helper", "GetLocaleCode", "()Ljava/lang/String;").toString(); - - // 安卓:先切换到我们安装程序的那个外部存储目录去 - QJniObject::callStaticMethod("org/notify/FreeKill/Helper", "InitView", - "()V"); - QDir::setCurrent( - "/storage/emulated/0/Android/data/org.notify.FreeKill/files"); - - // 然后显示欢迎界面,并在需要时复制资源素材等 - QScreen *screen = qobject_cast(app)->primaryScreen(); - QRect screenGeometry = screen->geometry(); - int screenWidth = screenGeometry.width(); - int screenHeight = screenGeometry.height(); - QSplashScreen splash(QPixmap("assets:/res/image/splash.jpg") - .scaled(screenWidth, screenHeight)); - splash.showFullScreen(); - SHOW_SPLASH_MSG("Copying resources..."); - installFkAssets("assets:/res", QDir::currentPath()); - - info_log = freopen("freekill.server.info.log", "w+", info_log); - err_log = freopen("freekill.server.error.log", "w+", err_log); -#else - // 不是安卓,使用QLocale获得系统语言 - QLocale l = QLocale::system(); - auto localeName = l.name(); - - // 不是安卓,那么直接启动欢迎界面,也就是不复制东西了 - QSplashScreen splash(QPixmap("image/splash.jpg")); - splash.show(); -#endif - - SHOW_SPLASH_MSG("Loading qml files..."); - QQmlApplicationEngine *engine = new QQmlApplicationEngine; -#ifndef Q_OS_ANDROID - QQuickStyle::setStyle("Material"); -#endif - - QTranslator translator; - if (localeName.startsWith("zh_")) { - Q_UNUSED(translator.load("zh_CN.qm")); - } else { - Q_UNUSED(translator.load("en_US.qm")); - } - QCoreApplication::installTranslator(&translator); - - QmlBackend backend; - backend.setEngine(engine); - - Pacman = new PackMan; - - // 向 Qml 中先定义几个全局变量 - auto root = engine->rootContext(); - root->setContextProperty("FkVersion", FK_VERSION); - root->setContextProperty("Backend", &backend); - root->setContextProperty("ModBackend", nullptr); - root->setContextProperty("Pacman", Pacman); - root->setContextProperty("SysLocale", localeName); - -#ifdef QT_DEBUG - bool debugging = true; -#else - bool debugging = false; -#endif - engine->rootContext()->setContextProperty("Debugging", debugging); - - QString system; -#if defined(Q_OS_ANDROID) - system = "Android"; -#elif defined(Q_OS_WIN32) - qputenv("QT_MEDIA_BACKEND", "windows"); - system = "Win"; - ::system("chcp 65001"); -#elif defined(Q_OS_LINUX) - system = "Linux"; -#else - system = "Other"; -#endif - root->setContextProperty("OS", system); - - root->setContextProperty( - "AppPath", QUrl::fromLocalFile(QDir::currentPath())); - - engine->addImportPath(QDir::currentPath()); - - // 加载完全局变量后,就再去加载 main.qml,此时UI界面正式显示 - engine->load("Fk/main.qml"); - - // qml 报错了就直接退出吧 - if (engine->rootObjects().isEmpty()) - return -1; - - // 关闭欢迎界面,然后进入Qt主循环 - splash.close(); - int ret = app->exec(); - - // 先删除 engine - // 防止报一堆错 "TypeError: Cannot read property 'xxx' of null" - delete engine; - delete Pacman; - - if (info_log) fclose(info_log); - if (err_log) fclose(err_log); - - return ret; -#endif -} + @ref ServerSocket + @ref ClientSocket + @ref Router +*/ diff --git a/src/network/client_socket.h b/src/network/client_socket.h index 26ccd6ec..8b4dbbce 100644 --- a/src/network/client_socket.h +++ b/src/network/client_socket.h @@ -5,40 +5,96 @@ #include +/** + @brief 基于TCP协议实现双端消息收发,支持加密传输和压缩传输 + + QTcpSocket的封装,提供收发数据的功能。当客户端想要向服务端发起连接时,客户端 + 先构造ClientSocket对象,然后调用connectToHost;服务端收到后也构造一个 + ClientSocket对象用来与其进行一对一的通信,一方调用send便可触发另一方的 + message_got信号。 + + ### 压缩传输 + + 当消息长度超过1024时,利用qCompress进行压缩,将压缩后的内容通过base64编码并 + 携带上"Compressed"头部形成新消息。后续也根据这个规则解码。 + + > 参见send方法与getMessage方法。 + + ### 加密传输 + + 当设置了AES密钥时,使用AES将数据加密后再传输。 + + 加密算法采用AES-128-CFB模式,密钥由用户提供(随机生成),通过RSA与口令一同加密 + 后发送至服务器,这样服务器就得到了一致的AES密钥。加密时,先随机生成IV,将IV与 + base64编码后密文拼接后作为最终要发送至服务器的密文。解密时先处理好IV与原始密文, + 再通过AES解密获取明文消息。 + + > 参见aesEnc与aesDec私有方法。 +*/ class ClientSocket : public QObject { Q_OBJECT public: + /// 客户端使用的构造函数,构造QTcpSocket和ClientSocket本身 ClientSocket(); - // For server use + /** 服务端使用的构造函数,当新连接传入后Qt库已为此构造了QTcpSocket, + 基于Qt构造的QTcpSocket构造新的ClientSocket。 + */ ClientSocket(QTcpSocket *socket); + /// 客户端使用,用于连接到远程服务器 void connectToHost(const QString &address = "127.0.0.1", ushort port = 9527u); + /// 双端都可使用。禁用加密传输并断开TCP连接。 void disconnectFromHost(); + /// 设置AES密钥,同时启用加密传输。 void installAESKey(const QByteArray &key); + /// 发送消息。参见加密传输与压缩传输 void send(const QByteArray& msg); + /// 判断是否处于已连接状态 + /// + /// @todo 这个函数好好像没用上?产生bloat了? bool isConnected() const; + /// 对等端的名字(地址:端口) QString peerName() const; + /// 对等端的地址 QString peerAddress() const; - QTimer timerSignup; + QTimer timerSignup; ///< 创建连接时,若该计时器超时,则断开连接 signals: + /// 收到一条消息时触发的信号 void message_got(const QByteArray& msg); + /// 产生报错信息触发的信号,连接到UI中的函数 void error_message(const QString &msg); + /// 断开连接时的信号 void disconnected(); + /// 连接创建时的信号 void connected(); private slots: + /** + 连接QTcpSocket::messageReady,按每行一条消息依次触发message_get信号。 + + 若启用了加密传输,则消息在取出时先被解密。 + + 若消息以"Compressed"开头,则将剩余部分作为被压缩内容,进行base64解码并解压缩。 + + 完成上述预处理后便取得了消息的实际内容,再触发message_get信号传给上层处理。 + */ void getMessage(); + /// 连接QTcpSocket::errorOccured,负责在UI显示网络错误信息 void raiseError(QAbstractSocket::SocketError error); private: + /// AES加密 QByteArray aesEnc(const QByteArray &in); + /// AES解密 QByteArray aesDec(const QByteArray &out); - AES_KEY aes_key; - bool aes_ready; - QTcpSocket *socket; + /// 与QTcpSocket连接信号槽 void init(); + + AES_KEY aes_key; ///< AES密钥 + bool aes_ready; ///< 表明是否启用AES加密传输 + QTcpSocket *socket; ///< 用于实际发送数据的socket }; #endif // _CLIENT_SOCKET_H diff --git a/src/network/router.h b/src/network/router.h index e7429100..db1962e0 100644 --- a/src/network/router.h +++ b/src/network/router.h @@ -5,16 +5,26 @@ class ClientSocket; +/** @brief 实现通信协议,负责传输结构化消息而不是字面上的文本信息。 + + Router是对\ref ClientSocket 的又一次封装。ClientSocket解决的是传输字符串的 + 问题,Router要解决的则是实现协议中的两种类型消息的传输:Request-Reply以及 + Notify这两种。 + */ class Router : public QObject { Q_OBJECT public: + /** + 该枚举揭示了一个Packet的类型,在实际使用中,Packet的类型以TYPE、SRC、 + DEST这几种枚举通过按位与的方式拼接而成。 + */ enum PacketType { - TYPE_REQUEST = 0x100, - TYPE_REPLY = 0x200, - TYPE_NOTIFICATION = 0x400, - SRC_CLIENT = 0x010, - SRC_SERVER = 0x020, + TYPE_REQUEST = 0x100, ///< 类型为Request的包 + TYPE_REPLY = 0x200, ///< 类型为Reply的包 + TYPE_NOTIFICATION = 0x400, ///< 类型为Notify的包 + SRC_CLIENT = 0x010, ///< 从客户端发出的包 + SRC_SERVER = 0x020, ///< 从服务端发出的包 SRC_LOBBY = 0x040, DEST_CLIENT = 0x001, DEST_SERVER = 0x002, diff --git a/src/network/server_socket.h b/src/network/server_socket.h index 543b017b..8f4b9c51 100644 --- a/src/network/server_socket.h +++ b/src/network/server_socket.h @@ -5,26 +5,44 @@ class ClientSocket; -// 只是对QTcpServer的简单封装 +/** + @brief 向Server转达新的连接请求,并负责显示服务器信息。 + + ServerSocket是对QTcpServer与QUdpSocket的封装。 + + 功能有: + - 当接受到TCP连接请求时,创建新的@ref ClientSocket 并向@ref Server 发送信号。 + - 当接受到格式正确的UDP报文时,发回关于服务器的信息。 +*/ class ServerSocket : public QObject { Q_OBJECT public: + /** + 创建新的ServerSocket对象。 + + 仅用于@ref Server 的构造函数中,作为Server的一个子成员。 + */ ServerSocket(QObject *parent = nullptr); + /// 监听端口port,TCP和UDP的都监听 bool listen(const QHostAddress &address = QHostAddress::Any, ushort port = 9527u); signals: + /// 接收到新连接时,创建新的socket对象并发出该信号 void new_connection(ClientSocket *socket); private slots: - // 新建一个ClientSocket,然后立刻交给Server相关函数处理。 + /// 新建一个ClientSocket,然后立刻交给Server相关函数处理。 void processNewConnection(); + /// 对每条收到的UDP报文调用processDatagram void readPendingDatagrams(); private: - QTcpServer *server; - QUdpSocket *udpSocket; // 服务器列表页面显示服务器信息用 + QTcpServer *server; ///< 监听TCP连接用 + QUdpSocket *udpSocket; ///< 显示服务器信息用 + + /// 对udp报文`msg`进行分析,addr和port是报文发送者传来的信息 void processDatagram(const QByteArray &msg, const QHostAddress &addr, uint port); }; diff --git a/src/server/server.cpp b/src/server/server.cpp index 43f1fe16..7f8c99a8 100644 --- a/src/server/server.cpp +++ b/src/server/server.cpp @@ -106,13 +106,12 @@ void Server::createRoom(ServerPlayer *owner, const QString &name, int capacity, nextRoomId++; room->setAbandoned(false); thread->addRoom(room); - rooms.insert(room->getId(), room); } else { room = new Room(thread); connect(room, &Room::abandoned, this, &Server::onRoomAbandoned); - rooms.insert(room->getId(), room); } + rooms.insert(room->getId(), room); room->setName(name); room->setCapacity(capacity); room->setTimeout(timeout); diff --git a/src/server/server.h b/src/server/server.h index 6e9daaea..c9f1efab 100644 --- a/src/server/server.h +++ b/src/server/server.h @@ -12,29 +12,60 @@ class Lobby; #include "server/room.h" +/** + @brief Server类负责管理游戏服务端的运行。 + + 该类用于服务端程序运行。当客户端使用单机启动时,Server类也会被实例化。 + Server的具体运行逻辑依托于Qt的事件循环与信号槽机制:当Server被创建后, + 调用listen方法即可完成监听,然后程序进入事件循环,通过触发Server的槽函数 + 以实现各种功能。 + + ### 配置信息 + + ### 用户管理 + + */ class Server : public QObject { Q_OBJECT public: + /// 构造Server对象。见于main函数 explicit Server(QObject *parent = nullptr); ~Server(); + /// 监听端口 bool listen(const QHostAddress &address = QHostAddress::Any, ushort port = 9527u); + /** + @brief 创建新的房间并加入到房间列表中。 + + 创建新的房间。 + + 首先,房间名是用户自定义输入内容,需要先进行内容安全检查;然后创建新房间。 + 在创建新房间时,游戏会避免创建新的Room对象;有一个idle_rooms存储着创建过 + 但目前没在使用中的房间,游戏优先从其中选择一个作为“新房间”,如果没有这样的 + 房间,那么就构造一个新的Room对象。 + + 之后,将新的room添加到rooms表中;然后将参数中指定的各个属性都赋予给新的 + 房间,通过addPlayer将房主添加到房间中,并使用setOwner将其设为房主。 + + @param owner 创建房间的那名玩家;房主 + @param settings 表示JSON对象的字符串,用作房间配置 + */ void createRoom(ServerPlayer *owner, const QString &name, int capacity, int timeout = 15, const QByteArray &settings = "{}"); - Room *findRoom(int id) const; - Lobby *lobby() const; + Room *findRoom(int id) const; /// 获取对应id的房间 + Lobby *lobby() const; /// 获取大厅对象 - RoomThread *createThread(); - void removeThread(RoomThread *thread); + RoomThread *createThread(); /// 创建新的RoomThread,并加入列表 + void removeThread(RoomThread *thread); /// 从列表中移除thread - ServerPlayer *findPlayer(int id) const; - void addPlayer(ServerPlayer *player); - void removePlayer(int id); - auto getPlayers() { return players; } + ServerPlayer *findPlayer(int id) const; /// 获取对应id的玩家 + void addPlayer(ServerPlayer *player); /// 将玩家加入表中,若重复则覆盖旧的 + void removePlayer(int id); /// 从表中删除对应id的玩家 + auto getPlayers() { return players; } /// 获取players表 void updateRoomList(ServerPlayer *teller); void updateOnlineInfo(); @@ -81,14 +112,20 @@ private: QList temp_banlist; AuthManager *auth; - sqlite3 *db; - QMutex transaction_mutex; - QString md5; + sqlite3 *db; ///< sqlite数据库连接实例 + QMutex transaction_mutex; ///< 可能有多线程同时对数据库请求,需要加锁 + QString md5; ///< 服务端当前允许用户登录的MD5值 - QJsonObject config; + /** + 读取配置文件。配置文件的路径是`/freekill.server.config.json`。 + + 若读取失败(包含文件不存在、有语法错误等情况),则使用一个空JSON对象; + 否则使用从文件读取并解析后的JSON对象。最后为一些必须存在而实际为空值的key设置默认值。 + */ void readConfig(); + QJsonObject config; ///< 配置文件其实就是一个JSON对象 }; -extern Server *ServerInstance; +extern Server *ServerInstance; ///< 全局Server对象 #endif // _SERVER_H diff --git a/src/ui/mod.cpp b/src/ui/mod.cpp deleted file mode 100644 index e1d0a794..00000000 --- a/src/ui/mod.cpp +++ /dev/null @@ -1,242 +0,0 @@ -#include "mod.h" -#include "git2.h" -#include "util.h" -#include -#include -#include -#include - -ModMaker::ModMaker(QObject *parent) : QObject(parent) { - git_libgit2_init(); -#ifdef Q_OS_ANDROID - git_libgit2_opts(GIT_OPT_SET_SSL_CERT_LOCATIONS, NULL, "./certs"); -#endif - - if (!QDir("mymod").exists()) { - QDir(".").mkdir("mymod"); - } - - // db = OpenDatabase("mymod/packages.db", "packages/mymod.sql"); -} - -ModMaker::~ModMaker() { - // git_libgit2_shutdown(); - // sqlite3_close(db); -} - -// copied from https://stackoverflow.com/questions/1011572/convert-pem-key-to-ssh-rsa-format -static unsigned char pSshHeader[11] = { 0x00, 0x00, 0x00, 0x07, 0x73, 0x73, 0x68, 0x2D, 0x72, 0x73, 0x61}; - -static int SshEncodeBuffer(unsigned char *pEncoding, int bufferLen, unsigned char* pBuffer) { - int adjustedLen = bufferLen, index; - if (*pBuffer & 0x80) { - adjustedLen++; - pEncoding[4] = 0; - index = 5; - } else { - index = 4; - } - - pEncoding[0] = (unsigned char) (adjustedLen >> 24); - pEncoding[1] = (unsigned char) (adjustedLen >> 16); - pEncoding[2] = (unsigned char) (adjustedLen >> 8); - pEncoding[3] = (unsigned char) (adjustedLen ); - memcpy(&pEncoding[index], pBuffer, bufferLen); - return index + bufferLen; -} - -static void initSSHKeyPair() { - if (!QFile::exists("mymod/id_rsa.pub")) { - RSA *rsa = RSA_new(); - BIGNUM *bne = BN_new(); - BN_set_word(bne, RSA_F4); - RSA_generate_key_ex(rsa, 3072, bne, NULL); - - BIO *bp_pri = BIO_new_file("mymod/id_rsa", "w"); - PEM_write_bio_RSAPrivateKey(bp_pri, rsa, NULL, NULL, 0, NULL, NULL); - BIO_free_all(bp_pri); - QFile("mymod/id_rsa").setPermissions(QFileDevice::ReadOwner | QFileDevice::WriteOwner); - - auto n = RSA_get0_n(rsa); - auto e = RSA_get0_e(rsa); - auto nLen = BN_num_bytes(n); - auto eLen = BN_num_bytes(e); - auto nBytes = (unsigned char *)malloc(nLen); - auto eBytes = (unsigned char *)malloc(eLen); - BN_bn2bin(n, nBytes); - BN_bn2bin(e, eBytes); - - auto encodingLength = 11 + 4 + eLen + 4 + nLen; - // correct depending on the MSB of e and N - if (eBytes[0] & 0x80) - encodingLength++; - if (nBytes[0] & 0x80) - encodingLength++; - - auto pEncoding = (unsigned char *)malloc(encodingLength); - memcpy(pEncoding, pSshHeader, 11); - int index = 0; - index = SshEncodeBuffer(&pEncoding[11], eLen, eBytes); - index = SshEncodeBuffer(&pEncoding[11 + index], nLen, nBytes); - - auto b64 = BIO_new(BIO_f_base64()); - BIO_set_flags(b64, BIO_FLAGS_BASE64_NO_NL); - auto bio = BIO_new_file("mymod/id_rsa.pub", "w"); - BIO_printf(bio, "ssh-rsa "); - bio = BIO_push(b64, bio); - BIO_write(bio, pEncoding, encodingLength); - BIO_flush(bio); - bio = BIO_pop(b64); - BIO_printf(bio, " FreeKill\n"); - BIO_flush(bio); - - BIO_free_all(bio); - BIO_free_all(b64); - BN_free(bne); - RSA_free(rsa); - } -} - -void ModMaker::initKey() { initSSHKeyPair(); } - -QString ModMaker::readFile(const QString &fileName) { - QFile conf(fileName); - if (!conf.exists()) { - conf.open(QIODevice::WriteOnly); - static const char *init_conf = "{}"; - conf.write(init_conf); - conf.close(); - return init_conf; - } - conf.open(QIODevice::ReadOnly); - QString ret = conf.readAll(); - conf.close(); - return ret; -} - -void ModMaker::saveToFile(const QString &fName, const QString &content) { - QFile c(fName); - c.open(QIODevice::WriteOnly); - c.write(content.toUtf8()); - c.close(); -} - -void ModMaker::mkdir(const QString &path) { - QDir(".").mkdir(path); -} - -void ModMaker::rmrf(const QString &path) { - QDir(path).removeRecursively(); -} - -void ModMaker::createMod(const QString &name) { - init(name); -} - -void ModMaker::removeMod(const QString &name) { - QDir("mymod/" + name).removeRecursively(); -} - -void ModMaker::commitChanges(const QString &name, const QString &msg, - const QString &user, const QString &email) -{ - auto userBytes = user.toUtf8(); - auto emailBytes = email.toUtf8(); - commit(name, msg, userBytes, emailBytes); -} - -#define GIT_FAIL \ - const git_error *e = git_error_last(); \ - qCritical("Error %d/%d: %s\n", error, e->klass, e->message) - -#define GIT_CHK(s) do { \ - error = (s); \ - if (error < 0) { \ - GIT_FAIL; \ - goto clean; \ - }} while (0) - -static int fk_cred_cb(git_cred **out, const char *url, const char *name, - unsigned int allowed_types, void *payload) -{ - initSSHKeyPair(); - return git_cred_ssh_key_new(out, "git", "mymod/id_rsa.pub", "mymod/id_rsa", ""); -} - -int ModMaker::init(const QString &pkg) { - QString path = "mymod/" + pkg; - int error; - git_repository *repo = NULL; - git_repository_init_options opts = GIT_REPOSITORY_INIT_OPTIONS_INIT; - opts.flags |= GIT_REPOSITORY_INIT_MKPATH; /* mkdir as needed to create repo */ - error = git_repository_init_ext(&repo, path.toLatin1().constData(), &opts); - if (error < 0) { - GIT_FAIL; - } - git_repository_free(repo); - return error; -} - -int ModMaker::add(const QString &pkg) { - QString path = "mymod/" + pkg; - int error; - git_repository *repo = NULL; - git_index *index = NULL; - - GIT_CHK(git_repository_open(&repo, path.toLatin1())); - GIT_CHK(git_repository_index(&index, repo)); - GIT_CHK(git_index_add_all(index, NULL, GIT_INDEX_ADD_DEFAULT, NULL, NULL)); - GIT_CHK(git_index_write(index)); - -clean: - git_repository_free(repo); - git_index_free(index); - return error; -} - -int ModMaker::commit(const QString &pkg, const QString &msg, const char *user, const char *email) { - QString path = "mymod/" + pkg; - int error; - git_repository *repo = NULL; - git_oid commit_oid,tree_oid; - git_tree *tree; - git_index *index; - git_object *parent = NULL; - git_reference *ref = NULL; - git_signature *signature; - - GIT_CHK(git_repository_open(&repo, path.toLatin1())); - error = git_revparse_ext(&parent, &ref, repo, "HEAD"); - if (error == GIT_ENOTFOUND) { - // printf("HEAD not found. Creating first commit\n"); - error = 0; - } else if (error != 0) { - GIT_FAIL; - goto clean; - } - - GIT_CHK(git_repository_index(&index, repo)); - GIT_CHK(git_index_write_tree(&tree_oid, index)); - GIT_CHK(git_index_write(index)); - GIT_CHK(git_tree_lookup(&tree, repo, &tree_oid)); - GIT_CHK(git_signature_now(&signature, user, email)); - GIT_CHK(git_commit_create_v( - &commit_oid, - repo, - "HEAD", - signature, - signature, - NULL, - msg.toUtf8(), - tree, - parent ? 1 : 0, parent)); - -clean: - git_repository_free(repo); - git_index_free(index); - git_signature_free(signature); - git_tree_free(tree); - git_object_free(parent); - git_reference_free(ref); - return error; -} diff --git a/src/ui/mod.h b/src/ui/mod.h deleted file mode 100644 index 87c920b7..00000000 --- a/src/ui/mod.h +++ /dev/null @@ -1,35 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later - -#ifndef _DIY_H -#define _DIY_H - -#include -class ModMaker : public QObject { - Q_OBJECT -public: - ModMaker(QObject *parent = nullptr); - ~ModMaker(); - - Q_INVOKABLE void initKey(); - - Q_INVOKABLE QString readFile(const QString &fileName); - Q_INVOKABLE void saveToFile(const QString &fileName, const QString &content); - Q_INVOKABLE void mkdir(const QString &path); - Q_INVOKABLE void rmrf(const QString &path); - - Q_INVOKABLE void createMod(const QString &name); - Q_INVOKABLE void removeMod(const QString &name); - Q_INVOKABLE void stageFiles(const QString &name) { add(name); } - Q_INVOKABLE void commitChanges(const QString &name, const QString &msg, - const QString &user, const QString &email); - -private: - sqlite3 *db; - - // git functions - int init(const QString &pkg); - int add(const QString &pkg); - int commit(const QString &pkg, const QString &msg, const char *user, const char *email); -}; - -#endif diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt new file mode 100644 index 00000000..6b1ac983 --- /dev/null +++ b/test/CMakeLists.txt @@ -0,0 +1,13 @@ +find_package(Qt6 REQUIRED COMPONENTS Test) + +set (TEST_LIB + libFreeKill + ${QT_LIB} + Qt6::Test +) + +qt_add_executable(Test test.cpp) + +target_link_libraries(Test PRIVATE ${TEST_LIB}) + +add_test(NAME mytest COMMAND Test) diff --git a/test/test.cpp b/test/test.cpp new file mode 100644 index 00000000..2c118567 --- /dev/null +++ b/test/test.cpp @@ -0,0 +1,15 @@ +#include + +class TestQString: public QObject { + Q_OBJECT +private slots: + void toUpper(); +}; + +void TestQString::toUpper() { + QString str = "Hello"; + QCOMPARE(str.toUpper(), QString("HELLO,")); +} + +QTEST_MAIN(TestQString) +#include "test.moc"