From 86be27cd3117c3c008ad23f3b60108624e1752a2 Mon Sep 17 00:00:00 2001 From: Michael Theall Date: Sun, 5 Apr 2020 14:16:16 -0500 Subject: [PATCH] v3.0.0-rc1 --- .clang-format | 94 + .gitignore | 4 + LICENSE | 689 ++++- Makefile | 75 +- Makefile.3ds | 182 +- Makefile.linux | 37 +- Makefile.switch | 169 +- README.md | 16 +- delog.py | 9 - ftpd_qr.png | Bin 621 -> 3451 bytes gfx.3ds/battery0.png | Bin 0 -> 22332 bytes gfx.3ds/battery1.png | Bin 0 -> 22329 bytes gfx.3ds/battery2.png | Bin 0 -> 22329 bytes gfx.3ds/battery3.png | Bin 0 -> 22326 bytes gfx.3ds/battery4.png | Bin 0 -> 22310 bytes gfx.3ds/batteryCharge.png | Bin 0 -> 22334 bytes gfx.3ds/c3dlogo.png | Bin 0 -> 791 bytes gfx.3ds/gfx.t3s | 12 + gfx.3ds/wifi1.png | Bin 0 -> 22303 bytes gfx.3ds/wifi2.png | Bin 0 -> 22282 bytes gfx.3ds/wifi3.png | Bin 0 -> 22262 bytes gfx.3ds/wifiNull.png | Bin 0 -> 22355 bytes gfx.switch/deko3d.png | Bin 0 -> 55542 bytes include/console.h | 44 - include/fs.h | 113 + include/ftp.h | 13 - include/ftpServer.h | 73 + include/ftpSession.h | 205 ++ include/imgui.h | 1 + include/ioBuffer.h | 51 + include/log.h | 81 + include/platform.h | 95 + include/sockAddr.h | 70 + include/socket.h | 97 + source/3ds/imgui_citro3d.cpp | 623 +++++ source/3ds/imgui_citro3d.h | 33 + source/3ds/imgui_ctru.cpp | 163 ++ source/3ds/imgui_ctru.h | 32 + source/3ds/platform.cpp | 348 +++ source/3ds/vshader.v.pica | 61 + source/console.c | 276 -- source/fs.cpp | 211 ++ source/ftp.c | 4114 ------------------------------ source/ftpServer.cpp | 253 ++ source/ftpSession.cpp | 2482 ++++++++++++++++++ source/imgui/imgui.cpp | 1 + source/imgui/imgui_draw.cpp | 1 + source/imgui/imgui_internal.h | 1 + source/imgui/imgui_widgets.cpp | 1 + source/imgui/imstb_rectpack.h | 1 + source/imgui/imstb_textedit.h | 1 + source/imgui/imstb_truetype.h | 1 + source/ioBuffer.cpp | 107 + source/log.cpp | 196 ++ source/main.c | 182 -- source/main.cpp | 52 + source/nx/imgui_deko3d.cpp | 681 +++++ source/nx/imgui_deko3d.h | 35 + source/nx/imgui_fsh.glsl | 40 + source/nx/imgui_nx.cpp | 1655 ++++++++++++ source/nx/imgui_nx.h | 32 + source/nx/imgui_vsh.glsl | 40 + source/nx/init.c | 77 + source/nx/platform.cpp | 167 ++ source/pc/KHR/khrplatform.h | 1 + source/pc/glad.c | 1 + source/pc/glad/glad.h | 1 + source/pc/imgui_impl_glfw.cpp | 1 + source/pc/imgui_impl_glfw.h | 1 + source/pc/imgui_impl_opengl3.cpp | 1 + source/pc/imgui_impl_opengl3.h | 1 + source/pc/platform.cpp | 259 ++ source/sockAddr.cpp | 150 ++ source/socket.cpp | 334 +++ 74 files changed, 9974 insertions(+), 4773 deletions(-) create mode 100644 .clang-format delete mode 100755 delog.py create mode 100644 gfx.3ds/battery0.png create mode 100644 gfx.3ds/battery1.png create mode 100644 gfx.3ds/battery2.png create mode 100644 gfx.3ds/battery3.png create mode 100644 gfx.3ds/battery4.png create mode 100644 gfx.3ds/batteryCharge.png create mode 100644 gfx.3ds/c3dlogo.png create mode 100644 gfx.3ds/gfx.t3s create mode 100644 gfx.3ds/wifi1.png create mode 100644 gfx.3ds/wifi2.png create mode 100644 gfx.3ds/wifi3.png create mode 100644 gfx.3ds/wifiNull.png create mode 100644 gfx.switch/deko3d.png delete mode 100644 include/console.h create mode 100644 include/fs.h delete mode 100644 include/ftp.h create mode 100644 include/ftpServer.h create mode 100644 include/ftpSession.h create mode 100644 include/imgui.h create mode 100644 include/ioBuffer.h create mode 100644 include/log.h create mode 100644 include/platform.h create mode 100644 include/sockAddr.h create mode 100644 include/socket.h create mode 100644 source/3ds/imgui_citro3d.cpp create mode 100644 source/3ds/imgui_citro3d.h create mode 100644 source/3ds/imgui_ctru.cpp create mode 100644 source/3ds/imgui_ctru.h create mode 100644 source/3ds/platform.cpp create mode 100644 source/3ds/vshader.v.pica delete mode 100644 source/console.c create mode 100644 source/fs.cpp delete mode 100644 source/ftp.c create mode 100644 source/ftpServer.cpp create mode 100644 source/ftpSession.cpp create mode 100644 source/imgui/imgui.cpp create mode 100644 source/imgui/imgui_draw.cpp create mode 100644 source/imgui/imgui_internal.h create mode 100644 source/imgui/imgui_widgets.cpp create mode 100644 source/imgui/imstb_rectpack.h create mode 100644 source/imgui/imstb_textedit.h create mode 100644 source/imgui/imstb_truetype.h create mode 100644 source/ioBuffer.cpp create mode 100644 source/log.cpp delete mode 100644 source/main.c create mode 100644 source/main.cpp create mode 100644 source/nx/imgui_deko3d.cpp create mode 100644 source/nx/imgui_deko3d.h create mode 100644 source/nx/imgui_fsh.glsl create mode 100644 source/nx/imgui_nx.cpp create mode 100644 source/nx/imgui_nx.h create mode 100644 source/nx/imgui_vsh.glsl create mode 100644 source/nx/init.c create mode 100644 source/nx/platform.cpp create mode 100644 source/pc/KHR/khrplatform.h create mode 100644 source/pc/glad.c create mode 100644 source/pc/glad/glad.h create mode 100644 source/pc/imgui_impl_glfw.cpp create mode 100644 source/pc/imgui_impl_glfw.h create mode 100644 source/pc/imgui_impl_opengl3.cpp create mode 100644 source/pc/imgui_impl_opengl3.h create mode 100644 source/pc/platform.cpp create mode 100644 source/sockAddr.cpp create mode 100644 source/socket.cpp diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..30f66ed --- /dev/null +++ b/.clang-format @@ -0,0 +1,94 @@ +--- +Language: Cpp +# BasedOnStyle: LLVM +AccessModifierOffset: -4 +AlignAfterOpenBracket: DontAlign +AlignConsecutiveAssignments: true +AlignConsecutiveDeclarations: false +AlignEscapedNewlinesLeft: false +AlignOperands: true +AlignTrailingComments: true +AllowAllParametersOfDeclarationOnNextLine: false +AllowShortBlocksOnASingleLine: false +AllowShortCaseLabelsOnASingleLine: false +AllowShortFunctionsOnASingleLine: None +AllowShortIfStatementsOnASingleLine: false +AllowShortLoopsOnASingleLine: false +#AlwaysBreakAfterDefinitionReturnType: None +AlwaysBreakAfterReturnType: None +AlwaysBreakBeforeMultilineStrings: false +AlwaysBreakTemplateDeclarations: true +BinPackArguments: false +BinPackParameters: false +BraceWrapping: + AfterClass: true + AfterControlStatement: true + AfterEnum: true + AfterFunction: true + AfterNamespace: true + AfterStruct: true + AfterUnion: true + BeforeCatch: true + BeforeElse: true + IndentBraces: false +# SplitEmptyFunction: true +# SplitEmptyRecord: true +# SplitEmptyNamespace: true +BreakBeforeBinaryOperators: None +BreakBeforeBraces: Custom +BreakBeforeTernaryOperators: true +#BreakConstructorInitializers: AfterColon +BreakConstructorInitializersBeforeComma: false +BreakStringLiterals: true +ColumnLimit: 100 +CommentPragmas: '^ IWYU pragma:' +ConstructorInitializerAllOnOneLineOrOnePerLine: true +ConstructorInitializerIndentWidth: 4 +ContinuationIndentWidth: 4 +Cpp11BracedListStyle: true +DerivePointerAlignment: false +DisableFormat: false +ExperimentalAutoDetectBinPacking: false +FixNamespaceComments: false +ForEachMacros: [ foreach ] +IncludeCategories: + - Regex: '^"(llvm|llvm-c|clang|clang-c)/' + Priority: 2 + - Regex: '^(<|"(gtest|isl|json)/)' + Priority: 3 + - Regex: '.*' + Priority: 1 +IncludeIsMainRegex: '$' +IndentCaseLabels: false +IndentWidth: 4 +IndentWrappedFunctionNames: true +KeepEmptyLinesAtTheStartOfBlocks: false +MacroBlockBegin: '' +MacroBlockEnd: '' +MaxEmptyLinesToKeep: 1 +NamespaceIndentation: None +PenaltyBreakBeforeFirstCallParameter: 19 +PenaltyBreakComment: 300 +PenaltyBreakFirstLessLess: 120 +PenaltyBreakString: 1000 +PenaltyExcessCharacter: 1000000 +PenaltyReturnTypeOnItsOwnLine: 60 +PointerAlignment: Right +ReflowComments: true +SortIncludes: true +SpaceAfterCStyleCast: false +SpaceAfterTemplateKeyword: true +SpaceBeforeAssignmentOperators: true +SpaceBeforeParens: Always +SpaceInEmptyParentheses: false +SpacesBeforeTrailingComments: 1 +SpacesInAngles: false +SpacesInContainerLiterals: true +SpacesInCStyleCastParentheses: false +SpacesInParentheses: false +SpacesInSquareBrackets: false +Standard: Cpp11 +TabWidth: 4 +UseTab: ForIndentation +... + diff --git a/.gitignore b/.gitignore index 9b4e0d2..a46b511 100644 --- a/.gitignore +++ b/.gitignore @@ -9,5 +9,9 @@ *.nso *.pfs0 *.smdh +.gdb_history build.*/ ftpd +romfs.3ds/*.t3x +romfs.switch/*.zst +romfs.switch/shaders/*.dksh diff --git a/LICENSE b/LICENSE index a84c395..f288702 100644 --- a/LICENSE +++ b/LICENSE @@ -1,25 +1,674 @@ -This is free and unencumbered software released into the public domain. + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 -Anyone is free to copy, modify, publish, use, compile, sell, or -distribute this software, either in source code form or as a compiled -binary, for any purpose, commercial or non-commercial, and by any -means. + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. -In jurisdictions that recognize copyright laws, the author or authors -of this software dedicate any and all copyright interest in the -software to the public domain. We make this dedication for the benefit -of the public at large and to the detriment of our heirs and -successors. We intend this dedication to be an overt act of -relinquishment in perpetuity of all present and future rights to this -software under copyright law. + Preamble -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR -OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, -ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -OTHER DEALINGS IN THE SOFTWARE. + The GNU General Public License is a free, copyleft license for +software and other kinds of works. -For more information, please refer to + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + 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, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/Makefile b/Makefile index 3f76a8b..2feff58 100644 --- a/Makefile +++ b/Makefile @@ -1,42 +1,79 @@ -.PHONY: all nro 3dsx cia clean linux +.PHONY: all nro 3dsx cia clean linux 3dslink nxlink format release release-3dsx release-cia release-3ds release-nro + +TARGET := $(notdir $(CURDIR)) export GITREV := $(shell git rev-parse HEAD 2>/dev/null | cut -c1-8) -export VERSION_MAJOR := 2 -export VERSION_MINOR := 3 -export VERSION_MICRO := 1 -export VERSION := $(VERSION_MAJOR).$(VERSION_MINOR).$(VERSION_MICRO) +export VERSION_MAJOR := 3 +export VERSION_MINOR := 0 +export VERSION_MICRO := 0 +export VERSION := $(VERSION_MAJOR).$(VERSION_MINOR).$(VERSION_MICRO)-rc1 ifneq ($(strip $(GITREV)),) export VERSION := $(VERSION)-$(GITREV) endif -all: - @echo please choose 3dsx, cia, linux, or nro +all: 3dsx nro linux -release: - # can't let these three run in parallel with each other due to using same - # ftpd.elf file name - @$(MAKE) -f Makefile.switch all - @$(MAKE) -f Makefile.3ds 3dsx - @$(MAKE) -f Makefile.3ds cia - @xz -c ftpd.3dsx.xz - @xz -c ftpd.cia.xz - @xz -c ftpd.nro.xz +nxlink: + @$(MAKE) -f Makefile.switch nxlink + +3dslink: 3dsx + @/opt/devkitpro/tools/bin/3dslink $(TARGET)-3ds.3dsx + +format: + @clang-format -style=file -i $(filter-out \ + include/imgui.h \ + source/pc/imgui_impl_glfw.cpp \ + source/pc/imgui_impl_glfw.h \ + source/pc/imgui_impl_opengl3.cpp \ + source/pc/imgui_impl_opengl3.h \ + source/pc/KHR/khrplatform.h \ + source/pc/glad.c \ + source/pc/glad/glad.h \ + source/imgui/imgui.cpp \ + source/imgui/imgui_demo.cpp \ + source/imgui/imgui_draw.cpp \ + source/imgui/imgui_widgets.cpp \ + source/imgui/imstb_rectpack.h \ + source/imgui/imstb_textedit.h \ + source/imgui/imstb_truetype.h \ + source/imgui/imgui_internal.h, \ + $(shell find source include -type f -name \*.c -o -name \*.cpp -o -name \*.h)) + +release: release-3ds release-nro + @xz -c <$(TARGET)-3ds.3dsx >ftpd.3dsx.xz + @echo xz -c <$(TARGET)-3ds.cia >ftpd.cia.xz + @echo xz -c <$(TARGET)-nx.nro >ftpd.nro.xz nro: @$(MAKE) -f Makefile.switch all +release-nro: + @$(MAKE) DEFINES=-DNDEBUG -f Makefile.switch all + 3dsx: @$(MAKE) -f Makefile.3ds 3dsx -cia: +release-3dsx: + @$(MAKE) DEFINES=-DNDEBUG -f Makefile.3ds 3dsx + +cia: 3dsx @$(MAKE) -f Makefile.3ds cia +release-cia: release-3dsx + @$(MAKE) DEFINES=-NDEBUG -f Makefile.3ds cia + +release-3ds: + # can't let these three run in parallel with each other due to using same + # .elf file name + @$(MAKE) DEFINES=-DNDEBUG -f Makefile.3ds 3dsx + @$(MAKE) DEFINES=-DNDEBUG -f Makefile.3ds cia + linux: @$(MAKE) -f Makefile.linux clean: - @$(MAKE) -f Makefile.switch clean - @$(MAKE) -f Makefile.3ds clean + @$(MAKE) -f Makefile.switch clean + @$(MAKE) -f Makefile.3ds clean @$(MAKE) -f Makefile.linux clean @$(RM) ftpd.3dsx.xz ftpd.cia.xz ftpd.nro.xz diff --git a/Makefile.3ds b/Makefile.3ds index 6db2125..4603a50 100644 --- a/Makefile.3ds +++ b/Makefile.3ds @@ -15,6 +15,10 @@ include $(DEVKITARM)/3ds_rules # SOURCES is a list of directories containing source code # DATA is a list of directories containing data files # INCLUDES is a list of directories containing header files +# GRAPHICS is a list of directories containing graphics files +# GFXBUILD is the directory where converted graphics files will be placed +# If set to $(BUILD), it will statically link in the converted +# files as if they were data files. # # NO_SMDH: if set to anything, no SMDH file is generated. # ROMFS is the directory which contains the RomFS, relative to the Makefile (Optional) @@ -27,14 +31,16 @@ include $(DEVKITARM)/3ds_rules # - icon.png # - /default_icon.png #--------------------------------------------------------------------------------- -TARGET := ftpd +TARGET := $(notdir $(CURDIR))-3ds BUILD := build.3ds -SOURCES := source +SOURCES := source source/3ds source/imgui DATA := data INCLUDES := include -ROMFS := +GRAPHICS := gfx.3ds +ROMFS := romfs.3ds +GFXBUILD := $(ROMFS) -APP_TITLE := ftpd snap! +APP_TITLE := ftpd APP_DESCRIPTION := v$(VERSION) APP_AUTHOR := mtheall @@ -46,21 +52,23 @@ RSF_FILE := meta/ftpd-cia.rsf #--------------------------------------------------------------------------------- # options for code generation #--------------------------------------------------------------------------------- +OPTIMIZE := -O2 ARCH := -march=armv6k -mtune=mpcore -mfloat-abi=hard -mtp=soft -CFLAGS := -g -Wall -O3 -mword-relocations \ - -fomit-frame-pointer -ffunction-sections \ - $(ARCH) \ - -DSTATUS_STRING="\"ftpd v$(VERSION)\"" +CFLAGS := -g -Wall $(OPTIMIZE) -mword-relocations \ + -fomit-frame-pointer -ffunction-sections -fdata-sections \ + $(ARCH) $(DEFINES) -CFLAGS += $(INCLUDE) -DARM11 -D_3DS +CFLAGS += $(INCLUDE) -DARM11 -D_3DS \ + -DSTATUS_STRING="\"ftpd v$(VERSION)\"" \ + -DIMGUI_DISABLE_INCLUDE_IMCONFIG_H=1 -CXXFLAGS := $(CFLAGS) -fno-rtti -fno-exceptions -std=gnu++11 +CXXFLAGS := $(CFLAGS) -fno-rtti -fno-exceptions -std=gnu++17 ASFLAGS := -g $(ARCH) -LDFLAGS = -specs=3dsx.specs -g $(ARCH) -Wl,-Map,$(TARGET).map +LDFLAGS = -specs=3dsx.specs -g $(ARCH) -Wl,-Map,$(TARGET).map $(OPTIMIZE) -LIBS := -lctru -lm +LIBS := -lcitro3d -lctru -lm #--------------------------------------------------------------------------------- # list of directories containing libraries, this must be the top level containing @@ -80,14 +88,18 @@ export OUTPUT := $(CURDIR)/$(TARGET) export TOPDIR := $(CURDIR) export VPATH := $(foreach dir,$(SOURCES),$(CURDIR)/$(dir)) \ + $(foreach dir,$(GRAPHICS),$(CURDIR)/$(dir)) \ $(foreach dir,$(DATA),$(CURDIR)/$(dir)) export DEPSDIR := $(CURDIR)/$(BUILD) -CFILES := $(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.c))) -CPPFILES := $(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.cpp))) -SFILES := $(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.s))) -BINFILES := $(foreach dir,$(DATA),$(notdir $(wildcard $(dir)/*.*))) +CFILES := $(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.c))) +CPPFILES := $(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.cpp))) +SFILES := $(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.s))) +PICAFILES := $(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.v.pica))) +SHLISTFILES := $(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.shlist))) +GFXFILES := $(foreach dir,$(GRAPHICS),$(notdir $(wildcard $(dir)/*.t3s))) +BINFILES := $(foreach dir,$(DATA),$(notdir $(wildcard $(dir)/*.*))) #--------------------------------------------------------------------------------- # use CXX for linking C++ projects, CC for standard C @@ -99,16 +111,38 @@ else endif #--------------------------------------------------------------------------------- -export OFILES := $(addsuffix .o,$(BINFILES)) \ - $(CPPFILES:.cpp=.o) \ - $(CFILES:.c=.o) \ - $(SFILES:.s=.o) +#--------------------------------------------------------------------------------- +ifeq ($(GFXBUILD),$(BUILD)) +#--------------------------------------------------------------------------------- +export T3XFILES := $(GFXFILES:.t3s=.t3x) +#--------------------------------------------------------------------------------- +else +#--------------------------------------------------------------------------------- +export ROMFS_T3XFILES := $(patsubst %.t3s,$(GFXBUILD)/%.t3x,$(GFXFILES)) +export T3XHFILES := $(patsubst %.t3s,$(BUILD)/%.h,$(GFXFILES)) +#--------------------------------------------------------------------------------- +endif +#--------------------------------------------------------------------------------- -export INCLUDE := $(foreach dir,$(INCLUDES),-I$(CURDIR)/$(dir)) \ - $(foreach dir,$(LIBDIRS),-I$(dir)/include) \ - -I$(CURDIR)/$(BUILD) +export OFILES_SOURCES := $(CPPFILES:.cpp=.o) $(CFILES:.c=.o) $(SFILES:.s=.o) -export LIBPATHS := $(foreach dir,$(LIBDIRS),-L$(dir)/lib) +export OFILES_BIN := $(addsuffix .o,$(BINFILES)) \ + $(PICAFILES:.v.pica=.shbin.o) $(SHLISTFILES:.shlist=.shbin.o) \ + $(addsuffix .o,$(T3XFILES)) + +export OFILES := $(OFILES_BIN) $(OFILES_SOURCES) + +export HFILES := $(PICAFILES:.v.pica=_shbin.h) $(SHLISTFILES:.shlist=_shbin.h) \ + $(addsuffix .h,$(subst .,_,$(BINFILES))) \ + $(GFXFILES:.t3s=.h) + +export INCLUDE := $(foreach dir,$(INCLUDES),-I$(CURDIR)/$(dir)) \ + $(foreach dir,$(LIBDIRS),-I$(dir)/include) \ + -I$(CURDIR)/$(BUILD) + +export LIBPATHS := $(foreach dir,$(LIBDIRS),-L$(dir)/lib) + +export _3DSXDEPS := $(if $(NO_SMDH),,$(OUTPUT).smdh) ifeq ($(strip $(ICON)),) icons := $(wildcard *.png) @@ -131,36 +165,54 @@ ifneq ($(ROMFS),) export _3DSXFLAGS += --romfs=$(CURDIR)/$(ROMFS) endif -.PHONY: $(BUILD) clean all +.PHONY: $(BUILD) clean all 3dsx cia #--------------------------------------------------------------------------------- -all: $(BUILD) +all: $(BUILD) $(GFXBUILD) $(DEPSDIR) $(ROMFS_T3XFILES) $(T3XHFILES) @$(MAKE) --no-print-directory -C $(BUILD) -f $(CURDIR)/Makefile.3ds -3dsx: $(BUILD) +$(BUILD): + @mkdir -p $@ + +ifneq ($(GFXBUILD),$(BUILD)) +$(GFXBUILD): + @mkdir -p $@ +endif + +ifneq ($(DEPSDIR),$(BUILD)) +$(DEPSDIR): + mkdir -p $@ +endif + +#--------------------------------------------------------------------------------- +$(GFXBUILD)/%.t3x $(BUILD)/%.h: %.t3s +#--------------------------------------------------------------------------------- + @echo $(notdir $<) + tex3ds -i $< -H $(BUILD)/$*.h -d $(DEPSDIR)/$*.d -o $(GFXBUILD)/$*.t3x + +3dsx: $(BUILD) $(GFXBUILD) $(DEPSDIR) $(ROMFS_T3XFILES) $(T3XHFILES) @$(MAKE) --no-print-directory -C $(BUILD) -f $(CURDIR)/Makefile.3ds 3dsx -cia: $(BUILD) +cia: $(BUILD) $(GFXBUILD) $(DEPSDIR) $(ROMFS_T3XFILES) $(T3XHFILES) @$(MAKE) --no-print-directory -C $(BUILD) -f $(CURDIR)/Makefile.3ds cia -$(BUILD): - @[ -d $@ ] || mkdir -p $@ - #--------------------------------------------------------------------------------- clean: @echo clean ... - @$(RM) -r $(BUILD) $(TARGET).3dsx $(OUTPUT).smdh $(TARGET).elf $(TARGET).cia output/ - + @$(RM) -r $(BUILD) \ + $(TARGET).3dsx \ + $(OUTPUT).smdh \ + $(TARGET).elf \ + $(TARGET).cia \ + $(ROMFS_T3XFILES) \ + output/ #--------------------------------------------------------------------------------- else - -DEPENDS := $(OFILES:.o=.d) - #--------------------------------------------------------------------------------- # main targets #--------------------------------------------------------------------------------- -#all: $(OUTPUT).cia $(OUTPUT).3dsx +all: $(OUTPUT).cia $(OUTPUT).3dsx 3dsx: $(OUTPUT).3dsx @@ -173,11 +225,56 @@ $(OUTPUT).smdh: $(TOPDIR)/Makefile $(TOPDIR)/Makefile.3ds $(OUTPUT).3dsx: $(OUTPUT).smdh endif -$(OUTPUT).3dsx: $(OUTPUT).elf +$(OUTPUT).3dsx: $(OUTPUT).elf $(_3DSXDEPS) + +$(OFILES_SOURCES): $(HFILES) + $(OUTPUT).elf: $(OFILES) +#--------------------------------------------------------------------------------- +# you need a rule like this for each extension you use as binary data +#--------------------------------------------------------------------------------- +%.bin.o %_bin.h: %.bin +#--------------------------------------------------------------------------------- + @echo $(notdir $<) + @$(bin2o) + $(OFILES): $(TOPDIR)/Makefile $(TOPDIR)/Makefile.3ds +#--------------------------------------------------------------------------------- +.PRECIOUS: %.t3x +#--------------------------------------------------------------------------------- +%.t3x.o %_t3x.h: %.t3x +#--------------------------------------------------------------------------------- + @echo $(notdir $<) + @$(bin2o) + +#--------------------------------------------------------------------------------- +# rules for assembling GPU shaders +#--------------------------------------------------------------------------------- +define shader-as + $(eval CURBIN := $*.shbin) + $(eval DEPSFILE := $(DEPSDIR)/$*.shbin.d) + echo "$(CURBIN).o: $< $1" > $(DEPSFILE) + echo "extern const u8" `(echo $(CURBIN) | sed -e 's/^\([0-9]\)/_\1/' | tr . _)`"_end[];" > `(echo $(CURBIN) | tr . _)`.h + echo "extern const u8" `(echo $(CURBIN) | sed -e 's/^\([0-9]\)/_\1/' | tr . _)`"[];" >> `(echo $(CURBIN) | tr . _)`.h + echo "extern const u32" `(echo $(CURBIN) | sed -e 's/^\([0-9]\)/_\1/' | tr . _)`_size";" >> `(echo $(CURBIN) | tr . _)`.h + picasso -o $(CURBIN) $1 + bin2s $(CURBIN) | $(AS) -o $*.shbin.o +endef + +%.shbin.o %_shbin.h : %.v.pica %.g.pica + @echo $(notdir $^) + @$(call shader-as,$^) + +%.shbin.o %_shbin.h : %.v.pica + @echo $(notdir $<) + @$(call shader-as,$<) + +%.shbin.o %_shbin.h : %.shlist + @echo $(notdir $<) + @$(call shader-as,$(foreach file,$(shell cat $<),$(dir $<)$(file))) + $(OUTPUT).cia: $(OUTPUT).elf $(OUTPUT).smdh $(TARGET).bnr $(TOPDIR)/$(RSF_FILE) @makerom -f cia -target t -exefslogo -o $@ \ -elf $(OUTPUT).elf -rsf $(TOPDIR)/$(RSF_FILE) \ @@ -190,15 +287,8 @@ $(TARGET).bnr: $(TOPDIR)/$(BNR_IMAGE) $(TOPDIR)/$(BNR_AUDIO) @bannertool makebanner -o $@ -i $(TOPDIR)/$(BNR_IMAGE) -a $(TOPDIR)/$(BNR_AUDIO) @echo "built ... $@" -#--------------------------------------------------------------------------------- -# you need a rule like this for each extension you use as binary data -#--------------------------------------------------------------------------------- -%.bin.o: %.bin -#--------------------------------------------------------------------------------- - @echo $(notdir $<) - @$(bin2o) --include $(DEPENDS) +-include $(DEPSDIR)/*.d #--------------------------------------------------------------------------------------- endif diff --git a/Makefile.linux b/Makefile.linux index f8964af..a8c02f0 100644 --- a/Makefile.linux +++ b/Makefile.linux @@ -1,23 +1,36 @@ TARGET := ftpd +BUILD := build.linux -CFILES := $(wildcard source/*.c) -OFILES := $(patsubst source/%,build.linux/%,$(CFILES:.c=.o)) +CFILES := $(wildcard source/pc/*.c) +OFILES := $(patsubst source/%,$(BUILD)/%,$(CFILES:.c=.c.o)) +CXXFILES := $(wildcard source/*.cpp source/imgui/*.cpp source/pc/*.cpp) +OXXFILES := $(patsubst source/%,$(BUILD)/%,$(CXXFILES:.cpp=.cpp.o)) -CFLAGS := -g -Wall -Iinclude -DSTATUS_STRING="\"ftpd v$(VERSION)\"" -LDFLAGS := +CPPFLAGS := -g -Wall -pthread -Iinclude -Isource/pc \ + `pkg-config --cflags gl glfw3` \ + -DSTATUS_STRING="\"ftpd v$(VERSION)\"" \ + -DIMGUI_DISABLE_INCLUDE_IMCONFIG_H=1 \ + -DIMGUI_IMPL_OPENGL_LOADER_GLAD=1 +CFLAGS := $(CPPFLAGS) +CXXFLAGS := $(CPPFLAGS) -std=gnu++17 +LDFLAGS := -pthread `pkg-config --libs gl glfw3` -ldl .PHONY: all clean -all: build.linux $(TARGET) +all: $(TARGET) -build.linux: - @mkdir build.linux/ +$(TARGET): $(OFILES) $(OXXFILES) + $(CXX) -o $@ $^ $(LDFLAGS) -$(TARGET): $(OFILES) - @$(CC) -o $@ $^ $(LDFLAGS) +$(OFILES): $(BUILD)/%.c.o : source/%.c + @[ -d $(dir $@) ] || mkdir -p $(dir $@) + $(CC) -MMD -MP -MF $(BUILD)/$*.c.d $(CFLAGS) -c $< -o $@ -$(OFILES): build.linux/%.o : source/%.c - @$(CC) -o $@ -c $< $(CFLAGS) +$(OXXFILES): $(BUILD)/%.cpp.o : source/%.cpp + @[ -d $(dir $@) ] || mkdir -p $(dir $@) + $(CXX) -MMD -MP -MF $(BUILD)/$*.c.d $(CXXFLAGS) -c $< -o $@ clean: - @$(RM) -r build.linux/ $(TARGET) + @$(RM) -r $(BUILD) $(TARGET) + +-include $(shell find $(BUILD) -name \*.d 2>/dev/null) diff --git a/Makefile.switch b/Makefile.switch index 000989d..03329a6 100644 --- a/Makefile.switch +++ b/Makefile.switch @@ -15,7 +15,7 @@ include $(DEVKITPRO)/libnx/switch_rules # SOURCES is a list of directories containing source code # DATA is a list of directories containing data files # INCLUDES is a list of directories containing header files -# EXEFS_SRC is the optional input directory containing data copied into exefs, if anything this normally should only contain "main.npdm". +# ROMFS is the directory containing data to be added to RomFS, relative to the Makefile (Optional) # # NO_ICON: if set to anything, do not use icon. # NO_NACP: if set to anything, no .nacp file is generated. @@ -28,36 +28,51 @@ include $(DEVKITPRO)/libnx/switch_rules # - .jpg # - icon.jpg # - /default_icon.jpg +# +# CONFIG_JSON is the filename of the NPDM config file (.json), relative to the project folder. +# If not set, it attempts to use one of the following (in this order): +# - .json +# - config.json +# If a JSON file is provided or autodetected, an ExeFS PFS0 (.nsp) is built instead +# of a homebrew executable (.nro). This is intended to be used for sysmodules. +# NACP building is skipped as well. #--------------------------------------------------------------------------------- APP_TITLE := ftpd snap! $(VERSION) APP_AUTHOR := mtheall, TuxSH, WinterMute -ICON := meta/ftpd.jpg +ICON := meta/ftpd.jpg APP_VERSION := $(VERSION) -#--------------------------------------------------------------------------------- -TARGET := ftpd + +TARGET := $(notdir $(CURDIR))-nx BUILD := build.switch -SOURCES := source +SOURCES := source source/imgui source/nx DATA := data INCLUDES := include -EXEFS_SRC := exefs_src +GRAPHICS := gfx.switch +ROMFS := romfs.switch + +# Output folders for autogenerated files in romfs +OUT_SHADERS := shaders #--------------------------------------------------------------------------------- # options for code generation #--------------------------------------------------------------------------------- -ARCH := -march=armv8-a -mtp=soft -fPIE +ARCH := -march=armv8-a+crc+crypto -mtune=cortex-a57 -mtp=soft -fPIE -CFLAGS := -g -Wall -O2 \ - -ffast-math \ +CFLAGS := -g -Wall -Wno-narrowing -Os -ffunction-sections -fdata-sections -save-temps \ $(ARCH) $(DEFINES) -CFLAGS += $(INCLUDE) -D__SWITCH__ -DSTATUS_STRING="\"ftpd v$(VERSION)\"" +CFLAGS += $(INCLUDE) -D__SWITCH__ \ + -DSTATUS_STRING="\"ftpd v$(VERSION)\"" \ + -DIMGUI_DISABLE_INCLUDE_IMCONFIG_H=1 \ + `$(PREFIX)pkg-config --cflags libzstd` -CXXFLAGS := $(CFLAGS) -fno-rtti -fno-exceptions -std=gnu++11 +CXXFLAGS := $(CFLAGS) -std=gnu++17 -fno-exceptions -fno-rtti ASFLAGS := -g $(ARCH) -LDFLAGS = -specs=$(DEVKITPRO)/libnx/switch.specs -g $(ARCH) -Wl,-Map,$(notdir $*.map) +LDFLAGS = -specs=$(DEVKITPRO)/libnx/switch.specs -g $(ARCH) \ + -Wl,-Map,$(notdir $*.map) -Wl,--gc-sections -LIBS := -lnx +LIBS := `$(PREFIX)pkg-config --libs libzstd` -ldeko3dd -lnx #--------------------------------------------------------------------------------- # list of directories containing libraries, this must be the top level containing @@ -84,6 +99,8 @@ export DEPSDIR := $(CURDIR)/$(BUILD) CFILES := $(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.c))) CPPFILES := $(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.cpp))) SFILES := $(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.s))) +GLSLFILES := $(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.glsl))) +GFXFILES := $(foreach dir,$(GRAPHICS),$(notdir $(wildcard $(dir)/*.png))) BINFILES := $(foreach dir,$(DATA),$(notdir $(wildcard $(dir)/*.*))) #--------------------------------------------------------------------------------- @@ -91,17 +108,35 @@ BINFILES := $(foreach dir,$(DATA),$(notdir $(wildcard $(dir)/*.*))) #--------------------------------------------------------------------------------- ifeq ($(strip $(CPPFILES)),) #--------------------------------------------------------------------------------- - export LD := $(CC) +export LD := $(CC) #--------------------------------------------------------------------------------- else #--------------------------------------------------------------------------------- - export LD := $(CXX) +export LD := $(CXX) #--------------------------------------------------------------------------------- endif #--------------------------------------------------------------------------------- -export OFILES := $(addsuffix .o,$(BINFILES)) \ - $(CPPFILES:.cpp=.o) $(CFILES:.c=.o) $(SFILES:.s=.o) +export OFILES_BIN := $(addsuffix .o,$(BINFILES)) +export OFILES_SRC := $(CPPFILES:.cpp=.o) $(CFILES:.c=.o) $(SFILES:.s=.o) +export OFILES := $(OFILES_BIN) $(OFILES_SRC) +export HFILES_BIN := $(addsuffix .h,$(subst .,_,$(BINFILES))) + +ifneq ($(strip $(ROMFS)),) + ROMFS_TARGETS := + ROMFS_FOLDERS := + ifneq ($(strip $(OUT_SHADERS)),) + ROMFS_SHADERS := $(ROMFS)/$(OUT_SHADERS) + ROMFS_TARGETS += $(patsubst %.glsl, $(ROMFS_SHADERS)/%.dksh, $(GLSLFILES)) + ROMFS_FOLDERS += $(ROMFS_SHADERS) + endif + + ROMFS_GFX := $(addprefix $(ROMFS)/,$(GFXFILES:.png=.rgba.zst)) + ROMFS_TARGETS += $(ROMFS_GFX) + ROMFS_FOLDERS += $(ROMFS) + + export ROMFS_DEPS := $(foreach file,$(ROMFS_TARGETS),$(CURDIR)/$(file)) +endif export INCLUDE := $(foreach dir,$(INCLUDES),-I$(CURDIR)/$(dir)) \ $(foreach dir,$(LIBDIRS),-I$(dir)/include) \ @@ -109,7 +144,18 @@ export INCLUDE := $(foreach dir,$(INCLUDES),-I$(CURDIR)/$(dir)) \ export LIBPATHS := $(foreach dir,$(LIBDIRS),-L$(dir)/lib) -export BUILD_EXEFS_SRC := $(TOPDIR)/$(EXEFS_SRC) +ifeq ($(strip $(CONFIG_JSON)),) + jsons := $(wildcard *.json) + ifneq (,$(findstring $(TARGET).json,$(jsons))) + export APP_JSON := $(TOPDIR)/$(TARGET).json + else + ifneq (,$(findstring config.json,$(jsons))) + export APP_JSON := $(TOPDIR)/config.json + endif + endif +else + export APP_JSON := $(TOPDIR)/$(CONFIG_JSON) +endif ifeq ($(strip $(ICON)),) icons := $(wildcard *.jpg) @@ -136,19 +182,69 @@ ifneq ($(APP_TITLEID),) export NACPFLAGS += --titleid=$(APP_TITLEID) endif -.PHONY: $(BUILD) clean all +ifneq ($(ROMFS),) + export NROFLAGS += --romfsdir=$(CURDIR)/$(ROMFS) +endif + +.PHONY: all clean #--------------------------------------------------------------------------------- -all: $(BUILD) +all: $(ROMFS_TARGETS) | $(BUILD) + @$(MAKE) --no-print-directory -C $(BUILD) -f $(CURDIR)/Makefile.switch + +nxlink: all + @$(MAKE) --no-print-directory -C $(BUILD) -f $(CURDIR)/Makefile.switch nxlink $(BUILD): - @[ -d $@ ] || mkdir -p $@ - @$(MAKE) --no-print-directory -C $(BUILD) -f $(CURDIR)/Makefile.switch + @mkdir -p $@ + +ifneq ($(strip $(ROMFS_TARGETS)),) + +$(ROMFS_TARGETS): | $(ROMFS_FOLDERS) + +$(ROMFS_FOLDERS): + @mkdir -p $@ + +$(BUILD)/%.rgba: $(GRAPHICS)/%.png | $(BUILD) + @convert $< $@ + +$(ROMFS_GFX): $(ROMFS)/%.rgba.zst: $(BUILD)/%.rgba + @zstd $< -o $@ --ultra -22 + +$(ROMFS_SHADERS)/%_vsh.dksh: %_vsh.glsl + @echo {vert} $(notdir $<) + @uam -s vert -o $@ $< + +$(ROMFS_SHADERS)/%_tcsh.dksh: %_tcsh.glsl + @echo {tess_ctrl} $(notdir $<) + @uam -s tess_ctrl -o $@ $< + +$(ROMFS_SHADERS)/%_tesh.dksh: %_tesh.glsl + @echo {tess_eval} $(notdir $<) + @uam -s tess_eval -o $@ $< + +$(ROMFS_SHADERS)/%_gsh.dksh: %_gsh.glsl + @echo {geom} $(notdir $<) + @uam -s geom -o $@ $< + +$(ROMFS_SHADERS)/%_fsh.dksh: %_fsh.glsl + @echo {frag} $(notdir $<) + @uam -s frag -o $@ $< + +$(ROMFS_SHADERS)/%.dksh: %.glsl + @echo {comp} $(notdir $<) + @uam -s comp -o $@ $< + +endif #--------------------------------------------------------------------------------- clean: @echo clean ... - @rm -fr $(BUILD) $(TARGET).pfs0 $(TARGET).nso $(TARGET).nro $(TARGET).nacp $(TARGET).elf +ifeq ($(strip $(APP_JSON)),) + @$(RM) -r $(BUILD) $(ROMFS)/*.zst $(ROMFS_FOLDERS) $(TARGET).nro $(TARGET).nacp $(TARGET).elf +else + @$(RM) -r $(BUILD) $(ROMFS_FOLDERS) $(TARGET).nsp $(TARGET).nso $(TARGET).npdm $(TARGET).elf +endif #--------------------------------------------------------------------------------- @@ -160,24 +256,37 @@ DEPENDS := $(OFILES:.o=.d) #--------------------------------------------------------------------------------- # main targets #--------------------------------------------------------------------------------- -all : $(OUTPUT).pfs0 $(OUTPUT).nro +ifeq ($(strip $(APP_JSON)),) -$(OUTPUT).pfs0 : $(OUTPUT).nso +all : $(OUTPUT).nro + +nxlink: $(OUTPUT).nro + @nxlink -s $(OUTPUT).nro + +ifeq ($(strip $(NO_NACP)),) +$(OUTPUT).nro : $(OUTPUT).elf $(OUTPUT).nacp $(ROMFS_DEPS) +else +$(OUTPUT).nro : $(OUTPUT).elf $(ROMFS_DEPS) +endif + +else + +all : $(OUTPUT).nsp + +$(OUTPUT).nsp : $(OUTPUT).nso $(OUTPUT).npdm $(OUTPUT).nso : $(OUTPUT).elf -ifeq ($(strip $(NO_NACP)),) -$(OUTPUT).nro : $(OUTPUT).elf $(OUTPUT).nacp -else -$(OUTPUT).nro : $(OUTPUT).elf endif $(OUTPUT).elf : $(OFILES) +$(OFILES_SRC) : $(HFILES_BIN) + #--------------------------------------------------------------------------------- # you need a rule like this for each extension you use as binary data #--------------------------------------------------------------------------------- -%.bin.o : %.bin +%.bin.o %_bin.h : %.bin #--------------------------------------------------------------------------------- @echo $(notdir $<) @$(bin2o) diff --git a/README.md b/README.md index 7ecc378..8c11397 100644 --- a/README.md +++ b/README.md @@ -10,15 +10,15 @@ FTP Server for 3DS/Switch/Linux. ## Latest Builds -CIA: https://mtheall.com/~mtheall/ftpd.cia +CIA: https://mtheall.com/~mtheall/ftpd-3ds.cia -3DSX: https://mtheall.com/~mtheall/ftpd.3dsx +3DSX: https://mtheall.com/~mtheall/ftpd-3ds.3dsx -NRO: https://mtheall.com/~mtheall/ftpd.nro +NRO: https://mtheall.com/~mtheall/ftpd-nx.nro CIA QR Code -![ftpd.cia](https://github.com/mtheall/ftpd/raw/master/ftpd_qr.png) +![ftpd-3ds.cia](https://github.com/mtheall/ftpd/raw/master/ftpd_qr.png) ## Build and install @@ -26,7 +26,7 @@ You must set up the [development environment](https://devkitpro.org/wiki/Getting ### 3DSX -The following pacman packages are required to build `ftpd.3dsx`: +The following pacman packages are required to build `ftpd-3ds.3dsx`: 3dstools devkitARM @@ -34,13 +34,13 @@ The following pacman packages are required to build `ftpd.3dsx`: They are available as part of the `3ds-dev` meta-package. -Build `ftpd.3dsx`: +Build `ftpd-3ds.3dsx`: make 3dsx ### NRO -The following pacman packages are required to build `ftpd.nro`: +The following pacman packages are required to build `ftpd-nx.nro`: devkitA64 libnx @@ -48,7 +48,7 @@ The following pacman packages are required to build `ftpd.nro`: They are available as part of the `switch-dev` meta-package. -Build `ftpd.nro`: +Build `ftpd-nx.nro`: make nro diff --git a/delog.py b/delog.py deleted file mode 100755 index bfe3b77..0000000 --- a/delog.py +++ /dev/null @@ -1,9 +0,0 @@ -#!/usr/bin/env python - -import sys -import re - -if __name__ == '__main__': - regex = re.compile('\x1b\[[0-9]*;[0-9]*H') - for line in sys.stdin: - print(regex.sub('', line).strip()) diff --git a/ftpd_qr.png b/ftpd_qr.png index 9d04fbb4d091ec7abca2cd136902a1b6d9b1d291..a15c750004baf6f22b6e307dfde46dff989e35bd 100644 GIT binary patch literal 3451 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 MQJie+$j1}@3mUvHhyVZp literal 621 zcmV-z0+RiSP)|Nf&uC5B8!tvi=`(|XTtJ4P8gjAQHweg8gfw|OX_a|D#i!&40jWVO zZ#9$WgTxw@^nyMt6+l2LkO(K;u^gRtc}wznKL-d%3-TsxfF&7if9?eWQjd5h+thwU z%H(psr<~p(AZ^G@HkNjYdg#pvU8@#L1rU(m5=zPDuRPNhb<5P+r?ljFJqH14K}eD| zW$+M5o7R@L%abxXMoSQo2E+?QdiP!3QX(%nK|mUil%GEDvwho5-mQLlp7%jOsu3Mi zSMsTS;;37$=gmj`2@sG5WcE`T+I6$BG#}OQM$dT=kX9s;)oVd)8T@A62LY*1SX-@L zNw(ieWb?hpBRzZt0ck=~e%@m9kzZ;`s}-DvBKbi;Y7@KbF_z~oHDay0gMj>rP?RWr zV5G8Ylt)V)S3yAPlc(_GKpcgk#YRo1ARsNs>nSy({=m^+edNwexfKMYDzQIePrN7) z-7+oq#6dtBkn$gMiUN_w(x1{C1mp^0343P`$~%)Q2*@Rbqd>&lGv(7R4+uytVwpSI z7&?{*+Y;sUoCyS^6^Zanj>AH%n~$2wOYv|JklF-+_;>LQ2aQe;Q^u5Z00000NkvXX Hu0mjfw=WLz diff --git a/gfx.3ds/battery0.png b/gfx.3ds/battery0.png new file mode 100644 index 0000000000000000000000000000000000000000..eed0f49e0f990b998da6855ba92dd99d03ab2c91 GIT binary patch literal 22332 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 aLHB|;H<|YQ3;Va?LVkt;`gPio6aN?3c0a2C literal 0 HcmV?d00001 diff --git a/gfx.3ds/battery1.png b/gfx.3ds/battery1.png new file mode 100644 index 0000000000000000000000000000000000000000..855d8a866ff4d90a6e7d42ba68bbe2fdc0d0c992 GIT binary patch literal 22329 zcmeHP2T)Yk*MCNXioJKUVn-kQdRZxM1yod8088|>T~>ibSOql_u(zm)6@*x#C@OXg zR#fbYEmka1QKLrvA{va1?=D49F|d>GKi~XkmKol{edqk{y}wiLIq%KvjDh{#>)E%q zhajk)r-#M|e6|6odli3s* zYzmios@1aa2$S9zB0*5}wPas|pYOxwE(JGVsQONiUC`I$)2x=vM>Q+9X?z#Q4z(Nh znciW=)n?uY8fmpP+8kRxz51Lvv9p_b_pCe9{z}cf4lCzOS~Y#1g(m+j&mCsSqX z731vO!y&8YKB!vsg*vsMEU%<2r0ulA29H!rhg6H5TrJBS&-ASo+Z5_OZjNdV)LU0A zHhD=GUuby^X!uEizY7h7q2XPBxPJkPO@6g>d^KqBf)359Esuv}O{D1>D7Y82;mo%) zwNM}eHP!dcW}z!Egm?z%J)q4yp{$&Sb+$sa>OzS3^eNq;8snki$2xY5h9)nCn(Ceh zIRDurz2O5#z|_*M&Y7K9ZQS5m@Ti&r0Z#3^<#@K}(Uju8;u3p19UQx~6&6P{_$~hw z1TBwm26mhOE;^@vdQJ{CC!_u__|bm5LI<9|`~KdU_2IoC=(aiL-20yLZLMQ_*~Nyw zU)lL-jo|O2?;X(cht~q~!!6usSIo=2di7pj-jzPts=?fefC=vn2Zv<^ zybJj|#`$f*%is3h=@f?zjdQQ@>bHZZUi4et_RRd+llklS=eob$S*P%Ay9*B4o_uDY zW7E6djYrMVB+kyqRy)j4&F_A=^Z7#Y&aIwrs=MAIq0J*)tBsF`*A5k`|JAo^!$5P^ zodO7YzT0%**KW0{#|BTjeI}~#ncF+}<(;8ez32Q<5cHjRK_RO+td*JKq2kN{XPEA&I%08%SsInJh+eQrUzG0%O+tkyW+c^%G|Ju79k>wpc zH6gsd(?S1V>brzDc+J&HInZrSTTd0wX(LIh(_0H)3vKl z_CDCvAz^Lb?Ok>?`a8QNkD=27J$iP$)Oc7g4@;W>pY@f{c9`*Ap-#guW zPK@Z&>R!Wp#63G{38wU4mFRP0?Fzp}rHJo>F9Jrw^m6lYqm7BifXxO zNxwC%F3f6m)DNN2$4ee8ZLrkjALy9j_XUF}~Nt@lq$9{AX|jjv%{ueHEay_;bdbg18<0qUQ< zmv0$%ch3=79ii2>=$;)$)>}VybR3!8Wy|TS-;Z(Jz2sfH-LqWM)~#UGcpp4qn@|1% zy<5$@OGdp~X6|46LATV!`7twZHhtFpPWLuzh<=%*L$q@Uhn>RFO~iz zHF?cm?e3sAarKo)`*a=B)!i^R=j-GLzE^#3q}r!8_i|CZOm|8fxw_4Y-I}QVVX04- zr0n}HHl?2__2$^U4_>#OAb;ogF7$QkOZU2gcH8Q9vdgWT)6)4cIm+bRPJT4^PjB_m zL%r3jxrN-~+mqun&pEHUsQXdZV7Bqdkh`7kh8_t$vZ{++mkwR}dOh-bn&{(|xu*M? zUj2IYJ9_%m>6NE1EhUp&(976^qz6ftlWy%CJ}7!n&y2Mhzet{2^9HR6dYZ8(Y|0=u zXmz0fprPAz8 zFzh^>e&}h!-h>IeCj9u$rDnZpSL?5DH>h1q94I~&hll*CS zlJC^HJHEvd`LUtlV^`!Hd@&`p`8ub@Jr#?3_nY3IS@1u~hK_5S&+h!ZNmS=K_G1sW zJ7{o_pQFxAzoH)E*uMXB4@a+Y^Z$t7Lyqlq7+>A*nq0SFhiVBt=T|#4)l^ z%tf&moismw-Y8XC2d{(-!z06e{g{&*q$yk99}b(fQ{H}I&Eq4t&D-#G5Nw~JTJyW# zpl+8PYi>120s8%?XPjUctxxH3`pKznCvxY}%bxvI_@wc{#<%BZte)C80B#p}fA5A7 z>{jPdV{`5AHv78A)Nk!JIR7#;!!h*Yfb~xT7N^JT?J~}5WZZWLPE0(oquR*Fb^Pt` zO_}ic`sF7>2L#H4ZtdV+Pfc#)+HA(m^rpEj7B~ChO1JCL_u5P|o!Gzh`hxB2)@Nsm zXY}Xw5AWZEQwG;xl(zW(;`(9ru3p<-@7A~5pS=7_n=4;@x?AqH6YsTh1EU0~}pnW~XwK22DZ@69a!Z%~u8QRH5s1lzi>XLjD-wfsl z?;3E*_1vXLH^;qo&;6#sGQv4%Y@d9;6t@DWMsgF|_SC+lw7V+-k+f?-#@l2PL7VlTSKbhX5xjg38#mAR(&Mi5&V)66E3Hw98 zU;XY;*5g*2a*rH5(th9ZXdM0S#kX&s9EiFe)$ZA$|?v#fgrMy9|u=^@RxIhs8{%CbS2Y)|6Ck`&E{~0Tpkq_+0)e6@e8d{-dq**%#P*6f?g_@CmW5Xr6j}&Pd9U(~CF<>PfE82ae*+(j~P8Fbl zc!fY_qy7sxmOEc0Pl`5vLCtcfSgpvXicRedNK>dIB+hJ#@H3f0T#MbctPul!n8iJ1 z-2#kZqA4n}yR-aLl`_XELz6U|S#q^#!UzLnq#sHvFj9fBJx~O=cR7$^$xkr6KoE(X znfn-pvY;TS0!PcEd?foRj!87?$5siYOtKut2WM5NO(CWTFOw*_;?DAlhAY$pZS26} zF4i^D$6Y1!)T$|zr6?G4Ld(OHldBL%V-iMNoL?CxNRhx)k>yMX3|W*PRGi`C(b5|s zMpr~hp(?GGMU;dNQBf43MKDZ9A}EewS|v_6%PpAz2$s!W;vXgBD66G$M#qv2$`T}w zs&pEymPQc`rBms2xT;bB0%3J(nm|-qHL6CGIE89iQm1BVmZ1q1rK}tP#VIYOqDdNI z5R9RfN;RrNQ3S(jLaQRHumY_TRccujMF|b1QeqkwM+qg0snl8}ts*MtfK)5hY7I)$ z8Wz)PaV^VI8V!MJC>qnD3{6$K1q#D(6~SOEg^*gUN~_W8Xo^6!sFI*rl*B3pU`Qp2 z=zt=nBXFG#LA6?~MyWvwwN6RmYF*_3EUi+Z8bU?m2%*D4|Dsb-DwLqr2#IS*wsHU+ zO6XV}f+<;GL4>8%6rt3pXqrUT3` z5L`_%T5#AQj8aW7T8)ZO5hOzEz$TO`P2~W*x1Q|Ull zRuT)qY1Jf#Fgir1MJa+|l^T-9Swc;+EXJxT2hiYZic|v^08T?H88wXoI$1)4lN3fW zy2?2yE;dAyU=$T)xd;(xf)`1UTxeW?QB)#e9%V(C;VF(2X`E!3%ALGoXp-i5l3?{P z2EdA;8G*)Ny(mf~$}t#9=qu$7oEuIeSTRCl1OkFltpdcO8Xcq2AR6GlxQ+!eNmjnb zQXmY`^8`<^Fi1)gOwt4e^E@TMyvRyChp{xtfMi{XHA}&8g5XiTh`=JC3PvPCf>}MT zhk3moVMLPCOBgt|stU$20xt;FdC6S_MvdG$}1y8y~raB4}&G}7BC*>L0JnUB8KV(ltUSkt~!{W0tGAu zI74UzCMmE+7L{mNqDTf~Fa%hGuQC|Ph%8U?K%3xLn4k$g%t7!Ld+fu2@DwSXPjLYw7hc%@QEW!T%ug zMUv%FQ~*3y8BD3BbRd9PEm)IgO>ra-;*mr|5Jw1b zR35l4%t#^#NKWE;edT9gsc-4Q;YI6d64rzB96*3=vIxq73&RQ+LXeUb$xzR61dU)I zb8#S_iW1l+D}fwFi#!8jfM5xNtJna=qaRm&Ak!Zo?($@4iulB8dnPqMv%r)kq~)|(ZdXb>S2^1Fqi>WhB+JuMGbIw zoCU7)nfhnYrRxOH6(2r=Bv6Q6Ps5lXNhODmsFy%8o7(tKE zg_M&iH7Q1bx)4|rMmSP{alIv;coCRF0ClTGkOC=!;4I!t1u&mY0>`Gr;Dm| zUuo3Gqh(ckQ8ymWMMO%LUem>)wAuX7z89C|C53rW&#K@AOCRqlSUNZfoB|(>mM!ap zrgO1?@pE%lje^hAzdT1&uoU)B9bhq3k<^z#B29X86c-^W4WRp}N^@F_`mx%l@D<0? z|A`?YxiSCF3O2y z#WGet4xrB;e%LHmlKf0zW26Xc(jQ2&9C%JEf#)dt2vewx3l9%53Y;aC%EyF>B@a43 zbXGF6NmlNdEatr&#-}Fy59Sw9iZEJ}ValyiD5XjEU$*Ceol~n;sT7;l7MaDhLa`5j zT&Bf5#geC|U#u`O zLC>R@$bt$J15L5z$q7qxB+N0K!1E-@f*!<@x-D~+1FL8*)4we;i)X0-rfjxSJCvil zTute0c|V{4RVt{Mic*>@cp~-LSUI$!dx4f}Gh}p8HA!O#_>rTQYl&+4+n=fi{j+NM z+n=hIY5Ip5&90!bDO$`Y!{xLrFENUr#ruL6camjmpALO0JDh;Q4u^rB=p3 z+gPOY>5U2%gUS?9&}Eb<-esf9Dh8|)wt1J0E~^-@O4#OIHoB~0z$#&zciHH&iUF&HZQf<0 z%PIz}61I7ljV`Mguu9nGT{gO`V!$e4n|ImhvWfw#gl*nsqsuA=tP-|)myIr~7_ds% z=3O?rtYW|_VVif^=(36dtAuUdWuwa~2CNePNAI#P`zkIe41E7q6!@YohkZ%S!IyN& z1P>oC2pZQJf?_8@(BFmN^C1L{Mj+_Xa0pT?fuI(q`NIzDfb7qn8l_+KwLkoo^|NKQ zMC!d=Vd4DODG#+x(z36m|6%l;&7k#{&z5yR;xsGS4noA!2M(#j3O3Ilatz8_d&;Bz z@oKYt`)?S&!>4)|2ZgBbb8OeTg;0+xllysKUhSvE#Qj~6t8JWk)jcNohpjFm6d~e+ z`$jd+w@W(i(IzD3a<5%8mtL*E#p}j;*D>@#fl~N7WIp0FtPB?Qsk1>b^b(fAF^n_^n;JBKTaJvoaO@sS@QA8UsO9>o|VPLCK36LXb;Ot6tx$SD4Li ziwUzuseJW%RaCUiYz>znDDFyXfF&^CVSUfM>o0WO665D|v-#Jntn$}Qjjx;3#;rw_ zT3r)cEW2E<_uj9J#tKc2EKMvweR}+~dc8YVo8oe*(w!E|r;qy~amumRH%I+2eq3H4;;FA$GvlcORFfTR^=^XiR4(pZ)yhzoUrH9zbW(oJN4kam%EgZ_mt{|4x>b&^3w0hjUAGGA zY$_L@x}Z$}w6p>==(xb&hI+x!ptj%LI|s$5zFIh{9MpGCi+bgjCPAt?QlbG0>jbSo z_01F`6oNo?&D{>L&?Oi`e1pwC(8ldhR(7qbo1x0pAf$KVg!WK{QP7|xEnCJx;}=48 zP0xcpe(#W8>%JQxwQ#dXW-Hd1(6=%?v{G=ed-HbLz70CmrMNG+7@OOxt>-sZlI%dU~0olQ;8s4)~_RyN>EDjpI92 zjE{J?y!F!xVFRiz9hNuMyt!uQZP3z(SAuh+9BhosW@cW#eD}_sOI;7>`f|sDN58Y| z8<-XRHvG>q9&hqq{`&K+RtZ@Dgf10c{kreOi|#9%o|;uNzSFk%mv)uQ$A^u(c`7#lnfKc+OIt(nX5U#uA!vZ1m3!c^E#8MKLy%!< zg4;%IlNWm%cFd~Sbnll}_f~x~h#IeJbzq;amd-_tZyG(Q{ra)Gb`wu-Z00sd{Y&R& zL{{&xiL;|>xbF-4sfK4%&A+(Hi}$wMl~ARE_DJPAT#GA-<@hNfEvHv*uyDew7JGdv z)SlW(bu_VVZ`W=~qz~#pB~TUQ)9krt^cU=;ex@BsosK}**f$e*bwTGOZ0_y(OSNtK z%+++`@uuZfPm6Xmc}D%EDW{{iHujuGExUhuU0BU4GraVNk*o&|E8z#i5%)v;b(@%u zRb~svH@U8_w(d~ljo+*EH5U$2BR0Pve%ek6sanY_7!4>8R>(WWt@)JNxh8cbeR3@w~4~D&KkN zidO-*+W8+F+_ll&T6c-N6{Q83Ht2_B|7)w41%5@&=%d@PEW`iD-ji0>A%-QhC&$(I z?Y45m^10_4hcyVxv;O$NzT>NFbDQ^=e>(W=@rPX=%zse+MQpX|6YGsToM>s+D5YVg zhL%RL4S!tFeO04#QyU!)L}>K!g8K_=F0=)OxMjF~TQBLD%iOMW+stj^rVd=QcGcSJ zYpVw31|1Kav1UNP=yX#M9&me2m0#wq3tF4n>v2Gn0Lz+At0%3sthNPuuNV{hc5=ng z{oVKX)UW8hbko4wyAG+U3XQhJb!;)D`nrk3637E>Hl4iu?Qpl93*I){In{IRnq{mW z?}`U+@z0%O_O5h$!O&NW>^-X7ZwMPr zUf1REZYuprTI#CZ#+{+B6KZG=cWv9RZ5PXo>@QRA2V4%gmgbUH-_KL?GTnXckd;lA z?KH&hiA;OCVDZlb;um+frClGf`~F`|N2}ikzK!@R?PZs0Ar-e&YgI9)T6RN^gXB<~ zM>F-|oZoxv`|t0pU&+no=HDEjlzGPEhx4ZIO*N-k5B0m<>UPATh(kZLso17Pn{IxO z{GKNJ`(>_bzp7LBPTdcmJaKaQ$qNg~6i@Ubc0c8Q%Ego$+XwZD>(en~b;eJU@8&yw zR)s#z*cCaU4;#8NB&bjSEv5~r8(Lr3xnXD8S816&X7^aP_=08s#@{oJW~|xXXye^W z6?4)vA~HsE7QTUH`@!`6PiOC*J$lFJ@85b>sy^v*jdjiXG#eAMHM*|m8_o6P>_tz# zM<0AK@1fcfa;ny;OZe@KI>*}|u%-s2oa=}qMf!hQ(I3v zW9){pclT~+5q9LanHyT<-CvNMlx?jsq1TuP7fx5=?r~H1kEzyn!lVho6TPQLC5ALU zgU%;o&s^2V+%WuQm}j`2VnKVdKf+x}H?OtsgFUX$JBS_LhuqDfC(ehYhWLheYs$L2 zrBjjNKi_!sBzMxS)LT_=k7}?$eWZWXh-KOPUQ9@-4lB-bN)wL%WZZ2 zX|12viETaIWyHQ_`z)^NGt`;HOZt9p&3ioeaq}BF>$jv`lSx7`Q*fwV>vVFMbDPxKdHU1_RU!t zD<^gfhMR@l+r54;yV+yth#Z&O^}g&d@tcb4J${;!;TCbe=ej4s^V7%dZZpzvNWy@< z$Hwm6R&L1SszEMyCyai4_2QHMJww!?H@0zqO-ya#Rd4c?^tw3>=GXh~QoE~hcbiPI z9ow_;>YS}>)*Z+cPnpk}AKtqTFYa4o-rD*1=GTaNmdG`sC#w#vJwhlkIYL zj?2(z{;n(ZS>66z!d^PWNME?yvB8EOWT>fnNx0PDf$hesK%lT$!Jp=t` zTY@Mj1xjd6-w_Mqcyknk&Pnjaq-R(z&mPH?uTT z57s3=Pd23lB){&<_ubL+gx8r1kFJk=(V@9^ScVXL5RGzag1VNABJ>%AEJG$znSD+Kb&Iey^W@;rwFO^V;s*k+!^p zc`t9&zcwbi?c40*J1@t!eY#;+&s_swbbg|}tiLwtym#MsS1&!g`rY8H*rmB=u6D_* zIAPqWcS$d2)YN}DW8sYB6RS^ryqNJ~*<0SxZy&x%)1MMgwOQQY-ST(G(;L)Rk9l?e z@x|;j3(hQ?|9t-JJrUooeETTtaibq|4(&VC{O6-_I6C0PH?N=UjlCM%?AZ@DYWBSI zt?IjW`C)%4< z5rrTR@3<&V7$VtKykxOPdb&Q_xyMyy6+K;pNk7CdN-u?3eMUx00V8_^3L}RItmx|P z<>C>i0Rj<{om0g{gh$3`;yhgobTy#txU6?*7zGtMb&}-JYB==_9%^7 z9UB|#9*eu%qAhBaWmz?XsWA)&7I4gnNIMq?N5-@)6e-p*NHIdRHOg+aMXDUS9B&(D z_jGl2RP_F*s9zCL?<}&S&9Can{ecIqnp?70ju^q4ogKH2P?Xv)iHr zZMJZ)!s%La5Ci8h$MjUS3${jzw%C~V9_o)>N+wQ;nq=VYl9$61Mi>|)15sLoks6Hc zfFfXe%R~w-Khp36i%8_`+=nWZLj^%KC?i+-!1iMun`kwUD3eNwrA*`fWYy?x;kIZ$ zn<#nV9_rFAm+A$MvAqgUv0gF$U34m6qn<)pih?nBR4ya4D^m{^iQS%Sn-oylM{(kNn}OgfVZ*C`4R2y4>Q1fnzQQ9Yu?Db&c4COu2D z3{B`Lt+D`$Q$|cjlQhC07(;8ddQ^v^2!_*yQAd`c1x78ZHL@s*5(Y}A#SAQt5?U0~ z>5W=iM<_EO^;*5&fYP*q#f(PW$g-5dK%fSS#!M(fQ;J=nFbvlb48~FjX*BAL2BV3l z2-Jvb37SPoOi_R#wIpH!j+BYOO(q028jS|60VVV%Es5(*$^tB{)1d}JN8<=#!om5) zq@#2wLF*9`H;}BdfC(i`tO>!iEEqwArS%k{HRx!XMD+|yE4P3}ah#@D;DAv^BN#l2 zVNEP!z*#K{CW2nE1v&)RlZ+8;b_k=@6O7TIBXk6b&?e9ct!LV8bN#iV`Cs`I_b;<$;Tu+gDFa^M* zA+?O2#sE&1FyJJGk&H>1L1D5X+61epDTzghKoh)3g5W~q0*s;(0rMy;!VFJwoJiv& z!zfSk(xpk7<4J-w!x#`Nlx74PgUzBSktoMtC}CFQ2KEhi5wsYgF#-XLQLh8wQG&@vnc|B`}3pB4GC zcN3Rz9(0t3MVdxoFyjQ6#V`@(DU3!W5Pc|IEOW~U(Jb-^!^5BnyhDtKd63q^h=`$P z0p(DJq{}X5ra%Hq0cHq|z$69Q$f6PrOBBgq42FQw;L9vVG9t^9Jn$ws7A9!I4094q z!xY8PW>z3r30DrW#iL2lMuyO07^TH^Mlj2Cda&7OwW!WOnv6Q6>_Rk4VzdD@;UGq! zs1ajLIIYD^dWEpqng$a^Qnu z1q>lb>E8&3W{x9h1Ot(a1MyUpKsQ+l#4uXq8L$QjmLRy&2cYokhgqLIr~JE|(L)q8 zW?OUwXZNy3a281&6>0HMms~7K105;7pP$AzGRDq<*NJBroT9Ty~oW`TL=oray*7S5OK5V`}z89wCMTvRAnN`CH zj&r=1;5flaU>Eq{YDu%+dwLWaSU(-kvLX1y{quFihDniscLFSwDzN&zNQ}*FkL98z ztp%KZ%5pd@RQ=HHWBJn8(|=?jVz}Y|Y#0cz=n)$1&jNxXV2Xpxfinv%*scYM;!%(} zVBnna&xU~jM`)t>2>t&z3=ZIhwF_Zfq(u_F)Wtn3zVP2Pay-J~xL{^so-yMv2;Crm z;Xuwu(jo$OYnow5kd6MYNAA;yAI_003Vyc8;ZpR!6aGMu<-l`V2|PzJN82J)TvSxJ zRp1<L%3Y?+F4 zWxmf69Fe);sQY2IQk(&On%xv-iH5@2X>zP7$4OZ^nE@LBcmS>`B_AnkIQ_`|gSA+s zE*3pK{cMJb31%L}L>6S27&sI=o}92GN5UM#2|Q1dEI5NWLbszXnOJFc+5TygSy)Rc zG9|SYkAn=iET{BvydThjEEQx-1tHA~Jdyh3noOCt~!ST5(RL{3Ao?8K#rNN(W7C8wcScH&Y*BsXy4lG9KuJ8>x@k{dX2$!RE- zowyVc$qk&igmrKc4aY>Qj`?q4j7j4xo z|3C-6q)R3E`1?W7$kq@PKMsQa%m;rTLeMY-f*uWmAk6{@YG9i+@Sq9UuJAQz1LLmz z7No6lKvh|!-c`)cpY_+`%TwEIJ=?X;<#W+zt7zhx_!SL1MGS8YWzGEl%K-y6zu?-k zN#$0DE==;6Ql3g%lsIU&f1Nh28p+)C$Wx+ge5Z~+ZCZskPu7hKc{5s_w8H#H($`m( zgx72opZw>-4RhxnSidn??2o383^t0tc70wYH+v$s#clJGfS$T)oqd~6ZneJHlqS z#YNa+RQ`IsDkj!uwnj=2lyEsc*b)+azma$TwdcC-lal6iw*@t&kPEuK_q`t+n}4Ff%DPI0|h^>(WtrjK1ZY08mTH%2WTa(+kt z-Q#bzT=2QK_E~!7iK;o(YR>b+1IeVCt8^_t`(^CGeFtvb_Uw}E+9n37TP;&;6<5&O z1$vq2<<-e8r|Qp;OTw8N)u5b!v>fE~iG_6@=oSocNgC&pV^3kaS4(ODbs0Hbw*u;7 za!E>`-!2$hTp1dCRN!wxy?5vn{28hogA>jY@r0;qxM zX_(jVoz~aA=MI=!u-PlS4Qov9Ukx5wH7v}tWrtk<#+@2a+$Y?^E}nZvENF@)6Lo%h zb_{|Rr!)k+eO8c=TYG(OE;T)?_8|Ddt}2CYJb&xWofB(fxT3U)ESWEPtwbbDb_*pvCtuhdqmNaARCDGyBq|JGXCN?6yzWpF0vZ`i*7J zpq#LR$UhUkUgy8~<>#AilCgowT`Rx*WzVtaJ(hodVpjEW{FS>iy1w38qwsZ$Gj99* z`Rs7_2Dbw14V`XSIPDp>+-%EqPZx?euY0_zbHh$&fGAadLIZ&wdNB8!1 z!|gdY^C9TzcH8c=9jaAIiWqz2M10{B--51-+d@fZ|5-yJ=u1Nz&yXXTz6Yy8kYRDM z`$lc^=ewJD}_k{jb+dHPtUtG0CyF2Viu3lMts9Jrl)#XVp{FLz4)2lUJF#ct$-F}tp zO>Lt(JgGsTTlW;w4-J|Uq6+nE@zgu^6L#VN)3%h(hahas>j^u$qH~fr2YR2axkaD7 zl5RH6^h1r4q8(j0qtW8Dld(TG^PWa6xp#6+M4ihseDnvAoL`$%#rH)bo(J~oHZdO6 zXA4I+xvj0a=0LNJeb!dmKKX@kVpDwOddbF|>Qv=;or-Iz^$YJ2-t(g0O4a9Wh|exp zr>j>Q7r3Xr+w7Iye{8qyvp@GW;W2b#xSvPs^Yv!bSJxdiNxzS7Q*W{!Yo9&y6aP)i zLQdB^Z8{wt)cU^1sx-o{S?k${FN;GqS7qmTs5%`Qe>?s5-rM-?=C@lt?eCh-cNx0u zW$?`oK}Wvo*7Q!@JH(wT(tJ!Cx^!XC)s;&^J|k!J)2&;Q6|^C6;>!BOu!ZdLv5ow@ zFJJe=+%wH08b{dWBK>rJ6}@U2zV&(2#Dx;nl0!{Fw@mQ|fsPF!tSX$$dPme{voa+SV& zd+hC{UlzD{)1X^B4ybAfO*0cbS`DeSX2P&!a$mbm$1i<7+mOyLuh?na-uG2dGjUP z4e`68Gak=h^z)ZVi+b2Hu8r7v@2}5Cs|!L3qW;Qw(Y0oHmCTxLs^rzoZQ`|`9BT7w zp+1=Rd!T;c-Y)v(+*~g8#<-O1Q(jBYnZ7mEnPxpO;8vSkQ3s+9ENxe%U8{E810DoC zUKkXRy`tla&OJK!IC%Wn@gI(#UqGgLqZhDyY4_4Dq+Q=SxL-m)kF1qhKS}~Q>Ycyx!8(X&SU%&V9?47emZyWt>fp^tf6ED?X)1qIC#JC@08)*7yt}V>{?y>La z{mPi^O`1Bn`n=d`mPFtL*)Pjla6=fcy&{Uf`7&U(79 zr=lZ&zW(~rvxzs;Z`Qaqs_}gFp@A_YmgMevK0c$-D$jZznt5G%OzO$Z`5$dv_mz#N zwS8JYzU_3^5qnzfvAC&EQKu$d)DLiP+4HHNd%(z9zoqOTN3_|GFZZ~UJ+A$#nLB#a z>Txwqv#8O+;i@RiTeA}#J1b@8P~GZQ0kg9#4=i`h!;h|&#&3SJKYHp`b<4R`4-d(l zx%Sh(uxplX#jhd#I$UtCy4fOynRgwZe1x61W>Kf(kB(&?$(u=k_vHJ+NA>p9yD=+k z`GoFaaEtJ}JJ){2ZuS~FBG2_!!%sU+=u>5_*H2Tj+@sF*TJtC@b$#N_b|V9ZB!9X4 z$e7(*T!uWX5$bwp{OE^QE<768D_q_8`WEi53F*y!8cv?FzCm8&)P~<&>~JOFPVtH1YVGJ+moEQU>w2FbAHDd)n5RxX-XU-M z*erea(p_FZ?T|7K=N|9u8?r3qdB|TO)Z^1vw{RC;MxT1zeAJFVh~LiT_1}?S;B)xr zyik|WfOF<^(kuU0nfbo!C$GN$a^9<`f$Mrk4SjU5(&<+G-sO?@lK$$%bH4rGT)Ft<$~Rx-#4mn!>PpxA zD&xnVc$4yCMjid9GZxG^I-%Bthl>~=mc8j4TX6q%hW>`lbbaGS>cp4l z9$v^jHUHF-)TgPlcSU`@yx>93!=@YZ4(vJ5^5??|IQr%DKCd3_j=vJ$;>ptMb$Z?Y zTJ=qb!iYaNy$C*;@oIL{*|Q5PSDOFqwSdEU5Y+ugYe={~JfMe0utj-tqK%h46QW{3 zAqqiWz6mj$FhsJec*$ao_I7)+eV3ccDtfzxkpW0Rj9!Yc`i+d0f=BiY5k?LXSkcYb z$JHxA0|25VJEuyBij0oaBzU_O;cCFxaarxADiW~|@pjWXCRBw7^j7I@v671PBw+!; zP!-L3q9jSvn1>2OP(qEMY7~c2RznaP1W}dzxcRt(kyor})&v<$CDVcbyxk(~_85&? z9UmX>8IOC~Vl8TvWmz?XsWA)&5^&szXgikxN5{1;1}VicNO3}}HO6kWMXMaR9B&(D z_jYr0O!W4rWM5G+Zzqb5E8z~Xs7~Nw)Tk$-E>9_@w1_WX_3iDC0*Jy}&6r`akwtqG z1+^3@MM=^2IG~4?J29|HK)_qoa@QLbRjy{7-8dXjP(o?BnsFf`VkC8t6lWV2D@exS zU?r`~+kKopNGh{V1)zXB`GRG-HlMI|)@^NUw2m@ne2uf=(QiHLb zPz1QQ45V1{J&XVlL?UPB-bJAtC+F}E2 zqU3{nsmr@ut`=xxdlz@HK5;=^bt-?OoyOBoCVYM^M$gfcXxxCIKsa2>&5EQOFpqt0kBnrMnZ zji{EOS(L;S0T@zCA|{|nnF!ouLQtd8XwVu^LT}QNxZb1;z|uM$Y9Mqpju0js^e-kI zr9%l?kC3>5WR(F-C}Cnv2&QF$1re6kQ-s!_qiGV=Gc2vV0v5$_nqq+hMj4I3cof5$ zSjK>}S`;{fUU3CF1lN;{5gc|1qtz3P(V!!A1c}flunDcspbP*Ui9#{G!N3@3mNl9f zE$)Z|140`xoe9LHLM#BM)sq;)m=Kc@r3i-A8b}&v2|dZO7^_nTFyMNM)B_g)PD5%L zJ&gf6S;Bym6h<;8We$pq4bdi8MNL^QLIj%NMG_3^DTrPq8paN)k-c1O@XvCBVGMN<4?LG|7NutzgYkFq|NG)GQ*f z2&jS)iI8B{jGJNJY(^N7tF^tmUIwSBhogN%ES}m$GkS3!JsW=eLk{E43O*qIAC~Cx5 z6HaS!lOAJ?2x34h4#W^51Cp$RD{%f36hXi&FYvHnrV*6I1QHwz73PX%B!p!J3AmQo z4AU$Dq8$7WB3~p~9z_MfV}-%AdddU>m^FemIk;l9ET$t^#KfRRE%>Z35QifuP0=9j z5Wwpx5ao^$2qs$K1QwjxC|&U^!6Q6KXkbquCIwg^2nOb9RDe0)?j-m?X|dv41Zj#R zc@U2zB7!(VfTQxjbzw#lK|pd6&zqIczEa;ZgTsq9(fEXZHg5b(GK=IXgt3G*6`FAy=mndk=w%91n z?qiMOERs4V+Tx`yyI58RI!gM00F8fioSg&3r@z6+L4y^faS>&C0z)yF5kLxNDM!3I zvYkXwBq}f>#7ptGXVq|mqmTCy z937klPJwr>mM!b8rdP3m^+R)3jDio;KR!o%gcSXE9bhq3k<`aQ;%sJnJQpizEuj0U zNOM|@`mWmh@a4zTf5Z@R-0**91_A^JKu`p@IH(*rvjE~t6eNmALFIsfKH;C4 zfj~7VQQAWP|Czx-c=7B)1Q%_QL?3nO&Pp%*H(3rLC7$I#hN8?I1(T#BlOv!Q0*R18 zc?lbtaAfA9 zR`=a%rL+S2u(~O!5)H-PX>!Drqf=HcX21ae9)N4gsYfcBPTy01Cl*W4#geC|AFVJk z!OWwW$bt$J15L5x$q7qxB+N0K!1E-@f*!k%hm7p?T9HwTuY#9%;Ju;x?TbU1z2s5%A0C(O=syfB zhjUIMXG3x9q)U+^xqy={IU9;&CtZpZ$pxHr$=OgGJLyuSNG{-{OU{Pk*h!ZnMREZr zU2--Q$45{XdICj#dNReE?Ntc`r#j%qvMT+DCPP*i5D2|2fXmDlRD+eE(KF_@b>P z=k%+eFz$cK+uE15Tuz8L5*#*2JJTi*(Lr4ZAil9 z-$J#u_o`}Uwok18rf}BNmQOwjat+M0iWzq*_sL9VsOETiFL~ z+GDfG?hBSJtvtBKk;Co7H=G^gveCGp(~^;=JM7*n&RiCIszKIH{}rjr+(+G*_mg6Du0}<_mjt8 g+dVcl%^L-Mb5&ei@a-3mz+xePV^70+-QY3*0;8ljoB#j- literal 0 HcmV?d00001 diff --git a/gfx.3ds/battery4.png b/gfx.3ds/battery4.png new file mode 100644 index 0000000000000000000000000000000000000000..05d7b8b174dd5752572761b2c2e1fcdc8dace7de GIT binary patch literal 22310 zcmeHP2UJwo*1kpqioJI;U`H==dl@NC1yod8z!tr2hEZS;MnR1RtXQ6ks3?fB#D*Oe zgB5!RORT8G9yRKVXwX=AXDEV-fm!+A`qx`)W-VrhbNBwvIo~d4?|avp>HYh9)^%v* z06|b)Z!e7>_-qaS8r8G|?`gTKQo%>92(JNA5LCOd<*yo)yrcyLIrKBC)&2X2n9QcA z5L1NATdkHwM4I%*Pzi!!uBG}L0{kB~b1S&{Le)Dyc7AV@U(=d0Kh@0GCJCLK9cwk{ z74NwGYE$0>4Yk@Dt&cB@uReQr?5w7~-RsP7xMF|TamDOOE8}OJd~<8u$`P0L6x=`a ze#d3^2OIKJx8>Mp)vUA73-={s>#S3?YxwJ=V~3C4y6e(2&Y?pD)S%`z(J|^>`)bgu z39hc)oU-iqLDga|)UF9-`J`kaZKf60f23MGxLWMwYFXw4rgzQQCQ#3@vsJ61p1NwW zsf#-KL(6JF!%hkO9jHGH4eR{F{R>cR>Z`@$szHP1J2tJhECG@=mf|%~NDpX3&bKqP zP%r{D(f2;gLRVl2@ea~^L0fi0S=kM0r$aUCK!|Vrl&(;XanP{i?c2vdlNUoxbkBoa z|LC@-F-p#u;p}4QOxXun;d6VKmye=lc!L{A91Wu9>EeRuh`R)V2tzwW!;Ar2c7=UL;`uLn=R=(DO#&b(Ta`Rn)Rc)s0PyYOw>3r>f<`OIMF zCU<-rjhwAXo|TWSa+HuM<`bBJ#QogeW&T*5^!>x$FZ6aq*)f{yhYji z#eo*xv+UX&`1<;R+HZ$ZlT{rKA5=9^If$`sB8PR|Fj3WI>X|KVorlSP>DiXZ@(r1q z7*WsVVBpX7+#>4##noJTpv#`PS~Zl%Yc}Q_ufNg`m7Uv#8|{&aMxse{GJt`WTY+TMK*qa46z(^pI*R)4f)raB8d5 zhC1tyw%YRThU&YgzZ7z|M%QQ*r_HKG)reNfxOPgf;6A~9uXwGMwdp`KyjF{@ReiGW z!Ol*JYkP0+w5#Fk!!39WofhoXz5S&|a~jJVjEh$vraLs6uEsiN&i%@J>rVma8=cpk z5BF>Tu=~0c!mCyL#1q%V5$X2q{O+=D$EVy)y?f{`ez)~q$LE6_Qu&@Ee|qJAyNln+ z;k{bkYjBUaXD2Pflz}Ug{cfyX9?+1SGf=g0d4}I+-)UAo(h<= z?mPeSn{2?DS^*Pi}{gdyq ztwZnZIV!6ywA>cc-ElAvq`m-aEOxjAO=`#kb_n(XJ3xw`A> z9({WBId%e9Ay} zz^dTDfkU?GHl}Xucxm^>-D?`I$?Tihcm2{!hC^Ha$T*R)ZfDCa_paFGZpsMD7|$8_ z=7ya|HXVAJxHob9uJQkR=Vo7b+SPjN+YW3yA!>VM6UDcRo5|Tro_dTw@?zmbxgj{G zLCzKYPDbNXT@M>m{ZlS@8k!q2qbB9Y&z#vU62*S^@X0x%;2)kw4yXzvD{Z#BL54dYaDKd3@)oeuqp;hfc{;|Iofr zEF4>S4q6l2v6`X!^f+bQps(J%8vDKD{@EUiTiRSNN4vnm*KA0=v~AvkE1U70jDnprv`h6_HM(vIB%lDL-*yqeUd-z zcIxfgcg8heBtJeRV$AaFgDqnp3AWcbseu;DV{#qtH2u2U)Nk!Jxc)pN!#V6?zx7Xok~U4)+i9%Nh`8?#oSb-IN3{`;YX>^q zn==0K^~+C&^b3{`$lJmFH8r)hd(-JNHZ{p@p49Y*D_yR~+-p6}baMaV>+`p-TYorH z%+a6MKfHevUOK4W!nCCON%g|(UcI)xZr-=spS=82n=4N`(v4PjB!0h`J7`bAJNFa&as#Uc`drjsl-_v1 z*;e4OX?ohjR}0^S4cXW?Y~+(;)z4RdXWzbF#^uakF19`I7T`DMQn$yR^^SeB?|tgC zXt*v_H}GFej^1t`p&4`cVL`3Df_8Nk*GA78x8aukg>Oc;HMErvS0z7B)}{C-zZt|2 z+STv0`?*VxZjOEHnfp!sC4}pMF}?BwP8hE}oOmJ8@H}T$>Vnkq6I@)3aR-%(u>K3uiv)v8PA&;O9S^pQq>EYaH}*Q1WKQ(6C45N3QP^FZ4UMd&%zP zO__s7WIi3ccgHw=!Nb-|>FgUX_Kx|ZS>dINOIf!Yd-KPd3XT-K%xiXILS*N6*{612 zjqdz(Jbyc2_q!4jF$|? za5txCyZ1ZEjG~)U5b1;XM5v_@qu1C-$$xC$0AcJ1ffb!R+#OtF6aXMhGIO$+u+Z=* zMU0zM5v~G!w+zdjWJMz85pGUO%Y?FEpZ+qnDN>S=E+i}<7%HP#7nCGv8tX2@5R{N3 zs2s&%lvNOf0zqUYFDG{g@Xa++)GPcnx{~R@e{N18W^;r>E{~3mc8SJaOpyjT%Cf8+ z!Q>bQ0|_{4Ot_hgfy1NP7lV}IXrw41(imYjn!;rkT#h%5GP^lBStk1MD%n?9#D|H( zqe{2~EXrfJ2s!G4$SYEcC@tbER{e1KivXhVK{H}hWN6XeL_scvN?}sCISS~Z6;2Fn z;^Xr{wZip=g;l5-W!8=c6qHa}p=MOTm zULla#sQ&_v<;oYylVXftP_tYqRx9$UVpF>U(iG}Qi8Grb15Bn+_hNS~Ys5ewW^qqh zmmp)fXo`;N>MH+KrOa{4&?F6KmfS6xFv7qX8GzCXj8tH3HxvQxT@IvJ@)Han5JVzp z<|;rTCrfe6%ALY1=`sD#a*m> zl%JaU6vN*poT#%xGsiMl65E!y3Kd3mv$D^eeLXGZ- zl0sElEsH1#9ipNrLW^LSjzmx#!?a4AaFts!0T3*ky~ICC#!*&F2Ou000hG7)HH#pv}#n1C~*qavZPMU(kw$0DoR;70E$yuOhuD4!XOw! zE0t)lf91Lm8T?bPE)Q;VOc`SPCJvT9sC#)zK7zYEdOYvnYvG3c!#`643!g zN=M*29fE4LT8&bJ5^9~2#MQdW0a#k4LN$bn#t}k?gZ@RQqEskBs}T~{kZk1uI+W0{ zIs{X)z=8-%t0_XMQPDJssu`B9d<86u<21zr1&q>af$=DY)v=5QXO$>$1a+k=P$9UQ zWVGP0Ll~u+V6++)p(03x)`3kZRhr5HfFn^Trq*Z}4b8Gz9izl8aiBqH4W`n8xU3`= zfYYi;3}JMLPK#0m!zwi-jkAQBWLb<=RSuxR)fA})E&!Z{R5EHB19Y;41}7RS|qd_#leQ_NNVv?+Ujio>s zqUQ;oVquV!B$%WL3g&rAfO(OXcn)J}k^#xO5^I)%;RL~>dJ%y|KoyKggaosCTo3bl zJ;I12ro;4NT0%!9HPMnnwN3n+&&BwckdJp~F_3UG$d z2uxC7jVvnButbpz#$X7r247_`k`Y;+~mf;7dEJcvgU5kVXwz)^YN zx-cV&ARsx3=k=AJeWkvo2ZtA}r%6~3&T{|(w#gzW2i_P~zz~9ztVo7>jw5IU1DT5h z`Bap^HdzVeFk0jp5Ca5D5M0FuC?5T|>MLJU{$0)JDhdj{DKd;RyBotegCvg#H@M2n z2FuDoOG)qJqwo%oGIOB#^wzjrXfT2_&QlD7h$yfziBK>DwgEH1${=159@v@_2o4;D zpQ(QaUAj&HUGd=~i2}}X3;|;T22z&gTw_U&ggJ&2c%CF#;31_Gl>_^1qJEJ^(BpHV zhL1eSymjuc>AZ;2;f1f~!`-6|2JK#CwZi}z9i%x6=187)=%n2*cpqAJ~A z8u{^PS(RSYjYn{iQIe(CbaN_gHb1oQ#U*)3VP4d;DmcN?$GZ!b4o(86z(=EH%le?{ zS}b7v+?-XT;4}3v&k-FWh5ua#SPWGp^<|JKlinQ7MM_Eo=zglwoED>gtoA8<#qsoi zVu&bi^glBL0fHW(!TBs8Ahb9ZR1Tb8V8L-MNEDBP$^ip?!ap+ufhtg75fN$>I7=#(j}8}0ZghU= ztYl`BtlTkK%zHVEPfhq=%rBx8X|yK8lv|}xN|Wq=Y|sBVr&g^}DK@JuGK*`4Vjupv zS}Co7KCf;{szgn(cj_!LW$BbFFJ{020B(RQ%CY{TysTn6{Y3qvSS&#oOYWY2vBJaz zJ&$4{`}Y-Q@lwlyRWz6BpB9};h&9_ zLo2!#Y^gRwM-^3*G=_ke9JRccsFuI{sp^2gS1o_}Q`Iufz;L749aJ_&i}_@@oR;Mw zM)AFPfAHW=vTW_sp--jz8)Kx{T3Z|CRT$9sO6}i$QIZSVgXlE*o7|F<_Oj&AV)L zS;c@=!Zz=+(Pb3_Rtej@%SM+~3|J*>^DY}*Rxx0eu+6({bXmoKRl+v!ve9J~16B#! zyvs(HRSZ}qZ1XM~T~;w*m9WjbY;;-0fK|da@3PTl6$4fY+q}z0msJc{C2aF98(mg0 zV3n}VyKHn>#eh}9Ht(|0WfcQf3ERBOMwe9#SS4)pE*o7|F<_Oj&AV)LS;c@=!Zz=+ z(Pb3_Rtf*DcR7^(6qghZet#<({Lt3qhu0f`AL)__UVc6hG`1rI#ZH2t*M;EoAq0&= zAn4IB2vRJ9pysA|LyzcyY=XB&84z>r_XWNVhh=p%`7?G-FYxeo%Xx9^>-Fimstf67 zU;9o{^zrKCm2S8D&;n?g2v->LhzYi8sPXh^U1Dz zl23Q&s%@s7b0qw5??0!BJ!gmBKQ@2WqZWJf?qvtoJEAx3?^@gV?*sAGlD6B;UUWU% oe)EW0zdpbE`}3^7y5vGV#(DLPsC~fli@1=twy$QBYS_g809h|GlmGw# literal 0 HcmV?d00001 diff --git a/gfx.3ds/batteryCharge.png b/gfx.3ds/batteryCharge.png new file mode 100644 index 0000000000000000000000000000000000000000..792398c118b4eecc08c517a89156ad5f5d1c5b94 GIT binary patch literal 22334 zcmeHP2T&AQw{BNg6m!o0BN&lpI`{NQa3UaL02DB+cA8-n7=%$!R|O-6RWYF;u3^nN z;bO#`F|859n$xOp4U3s~haj!uHvikd-m6zNR54t-&-w1X-wF4e?y8yCt&49p=f=(u z1Xc6*(+7dSO~7yMN)^H9s@$a+;7{ckzwkH+s#3@PTMkN_-w=YFyIOR*ZrviRHfvmj zHAdmD(Pl_%TECAueBjW{J02aAoLj~~H7jivTEu;DEeE|G z>FL?tCEIB?R4(CMl}b={V0t#vbV7cON7{LP$|a61mu*W?1yo9`3w0beRl5x8Xe^hQ zF}HOvw6Fry|2WUxhPuH}|2CuUor4lHUe6m|4(dItMZIzhQy@hhFN&{( zg&|N~Q@{ZRx&%Xrf2he1+OQ4E&Z${t6I7`xgmh0H*AA*M9O{3>)inVcI}fUBd>-og zXZv+E@4Eq}=56xKYRMRqdRKx6IfaILG;f>Z-=KY6iv5mF>*%p>=)6W)5>ey#7bhTS zVM;x)+ZP`aa;mS($)ToZR__Nt+EX##h2w62ynAYOOh*X1X&ZUwqq}l*xK|knQdW0O?>~pvh1nCzhxoyxi zdAYZtdv?X9d%wTBx61qe)L3oH1N*c!wa!9f)7bv)){NG+9e;8|Gq?WA-#RuUvb#r& zpBYo#V_%P7t9!-Nc*|B=u($2bq{?@_KQw4kh@yvIv*%v1-!T*V7`LZ%I09j#-;dwvi_S{g)ZOd1s#|qgE9k~! zjf<@|g2bpQ0~h#FU>dFu`%*$)~z;RhlSk3;*l8&&R=XY$84 zx~!?X`cUHyJ=c`qG4U0DYGZtb+DV4&%2b7Tt%7Z?@eAu5*5#7l3PsbFM6D~8>B{BD zcHh^=W#)>2Ev>iLdUK#5hoKX~{M=nH)SgyHS#x-@?f~7g_Cy`lCTse4{u_S@Ia~Xz z@oaRE>qGaI>4aZn*O^DJ2m?1cF|*ti?T?JRlW}MN9sEv{J1w5~cFy2B4*KPF@U6B% z#|Ct2bhqYR;%-H8E~e?RBrWLLibWx{$Z5T_YZqk(t?xczMIB;r8gp_?egA-^YZuQx z*Epg~0zTx`0 zotHH_H@VT_5QIh_&%Hmd#yo3}Ft<#%AM2$YbDrI4cI(+q+>{|JS1nt0eN~knFM1pg znYQwW;1TPLJ@DY$D=YstXLXNN8QmTSHwiYc?66|OD)S0!h|e!0!#_-{7{0&r{;s-T zx-Z<=@Al3^iYk1g%?a);23A`=esB_bp!LR+mwz1Mwqx#xW;-T(ty;N=(czu&(9J3ZE~?QXSevW)1Qg^GO!u9}lNCv3If>h7z3AMc{p zJz1WyY?on2_`9U)n!}yi^l9U3o|f}{#{J;S!Pk~MFRvfyrGB-}W7WW=O&0CY$M1<= z{&enw-9IEQ=xkknedw+Cy->P91H&<<0F}G??L(hZcAggCH<>A~v zyX*Sy@2FeK&Sq0@j!nrr9s8UY39!8 zalM%ErC~jK_1$b-o3Xalg&k{mEU&dZtINzTs~21_@89re=F!ZR+Zt`Sd#PgXy3DA| z5v-YOVBU6c-TtRDcg-BJeZ)^6yqu~{xLkd8vtG?c#%+nMtL~}3o|ZHJsn3XmFXuc| zn!`@jJaq}bomuC2y91Vt;Pi98<_6}hxG^u1Cr=C*ecU6}+atZnp=(EW-RWd|HK^~+ zVP{`uq;F2&_hd!x`lr-W%hL&0e?CF{+-^ZQ9(Q{G5#8_I@1F6g^7CwD;EgpSyXv|o znYZ3|Rr1P|(6tvgY+D+?Dvy2E!22)n*q?8E-}a=hW7os`ouIR~9ocrg>wfEke&cTF zM!Dt-`NQ(hK+6+bl{1&0n50SS{oT9Q!+vbBXR41nSNvf^eNd~o5k6MK)HynkfXHsdCY3mxw>H6}T%@fkFgj6ZW# z7k5MdRzF97J>86UW!A%;NH_0Q9s@kD(A$aaK8HL^;U~_AWrX=h1~g?n+}2Uik-Kla zfAV6&t&Ce$ZVzuTS9zpw%+N(S`(BP)UVo)WZFlvYj-8XcsAm0_rl#A9`cqmxuM^*D zs`Jo&&GwmHlxL_j$(M9}+?scJ?&lUbY{nlcJISFf58_MR?`Dl{vvT^*&eb|!OII(b zpEg7hg?XuWp<`yGOdq6O)go|arumWio@vPOHR8BU9}h-P-ll9m+v(`Q&C}O>9}YWb zYL`6->DBh4o6{z<7;4&ca^f*&&gup2Pd+)Z`B?6Bdj7MY^Pkk-SNrCS%%$T4Lg8j% z_jaurz-;myG&I-wcD?W0kMCJ=jpwhEGToxicU}D?GVYzIyRV->zZG@Ecp%x8pOKc-NaaX~`j!YR{P)5Ck$;RU^`&smjvFSUAfwaZtwRJ+l0%ad1s8FH1WC)?)k7?Y{XTC&IU z*PY_>qd6x#_=Nlt@-pOY2=(;rwXN*M*U@L5HW|M2FXE5$xxIJheegcIJGV!<9)agg z=f!va?>6W8ted#%;p;i?qWZ4w5;f?_;qqt8e{gcGo_R6rxAV=;dW8f{yU_lzZ}r3f z+Wj%(Sv*{gs@CJD`G;<~#^{IMd6-xEMqcx3>MKL03}188>D<4DG&46-4$!7OPcxXkFF1U@0)Ngt!>oA+^IqMke{E!J zn-4k1cU+Ed^K|Xbt~>j^?D#};S$A#1d7s`NuU>j~b<}|D_=PXdT=mVXIBv|Tk14OF z)zE!EZQiuwbSKRkTDTz5)1)p|jLkBdJZU)P|%a^&mtk1yt&nR{kY z>hsi@d!l|^`r%Rb<3{Uq5A8eDeD~1=9R1;C&v#Gu#$SzZ_H4$V5&*N||1O;rdgoN3`0z0dDYm^5oSUJ%nAu0wGq7dZilMuu5 z14WyH6U~-rFPCRK_P8i4f|pAu8HfbN=)?$%->_IQcvzPZe%L^s5nOz{ojnuO03b@V zv5JJK$mlqAf|pAHt{RN(mz6Gx0ukFlFBgq{LPc0$H-*j`D=J7266O&MRnUwFN|H2< zxhpUPC6oxNL~$5p)C8eM5Jl0Ci?=fvdBzGRb&%dzG#&WQ%O%2Qi%~0;@$vB<@wkUI z)~rMshEXDz62o910mluEwy_CtbewA;NHLCHjN@Z1F*b`eT4Bd!IqP7Xmy3&iqEA0X z`-+PBG*NV15qE$^Wda+cL_H8?X-YB0MSSV1pKgB>K;S=V#te>)EZCdCE5%4LN{qI} z0X?+TiGfW513#&jy56X$QZ?gjh9Q80B1%iuj0+hWBPxT$IP2h8UNj5=D{(Dt_i?r$ zvBWxMfCA#B0@*C4Z{XPPe3Lve!SW3?`<+6y0-q{0wI?7=t&0^|n>9AXYK`RAcc}& zU<8675?CAiISLhmQfiV=qo1LCCi^9hRj`m3*5t5x_LbQH=^6pVSGQW%L`sW^HoKiKa4nrL2(1Ez|Tm=GAUAU|kX z^XFHKZ$w(W5emHg2Yj+QExEND59r~TB8xy$^sAwW7N?EqBZDH9iqW0)WDEN9YZrJn$S`jc>olr z449TCX+(u!Dq5q_p;{D0Fq|d~TCxl)FlbPXfk9D}&{J9sre|=J(4d%BXVB1ELY@Os zr_t&3C{61b%wWI`3`6Pl1gfWK%!sOJN_GnrhT&R5g)tOD8Vp*4-e9CD0yUr-f@V+> zlLb(b8WJ%AMaoFvMk9h63spiYEeC*rE!EX;-G&qYAG#B&^m;~ z^&}$?U_=QcV?;0w11yLzw2mS)dM!cP%#=kN#hKmBN+x`wDJIYTt|^Q-~zyDNR3KIV}MSE z(BmY9kt(A+2ZhCkVC5}>x+E7NJWX%{36cwq^Dv5v1k9m~0IN8PWd#~1RVsNWFCChs zS&k$a6N~|{LTD9FW3WjOL=t6H7)qFAc?0K$hX7WL&=`S$VAN>=@u=RY((4gDa9`ZW zfS4rZ*H{dMAtsLCCeNrXw7pkR)pc$gCykz+B2CRHF=%UH7*3?~QC;35;!{;2XmmTg%JTmO+3n?Dv~Zc zn27=fECo12XapuHuto+IX;`F46{f-vU=6O!V5CZ5IFbX}1k1n#O_*R-q-mIPe$Pi1QkfKcCNtrPf!E_GaS#syop9o2IEO^ER>lmMkOK)!;8SROeUCS z2oUAqe-QZs$#5vj10Kr^rqNMG5WtK9tjW%mO2c4Ufq#1f?k&q#Xiy zJq4oNJ_5l+^DNJRGaIGLo+UVh0|^c63B)81^8}%SIU40*7PvbJK2TaH`xZf(Vo46f zBZ&wgju7Ce9B^G&B?=%QS&`#R@@HSMZ<)a1MVn|6Hi7dTK!9yB2+D#F6~ki)L5lxI zGBmL)K_eK*Tr9|^f(W+Bh#-g20;d8oKrjTsmTrK;tDjeW(wg$`YDQ0iSDUP{QLN3| z62+QDWlXf$Q(1Dcqztr|^nrnD|L8ay3yM#Fy|=+P9>l`4_xOf^{=3d*9o9YKYUn|Afh1C!5A+JMTd`I5a1!B^_vo+Ca&jQ+b0un?+1>f0c3R+BBBjTJR!(EXI9IW0u} zTS16~tJ}zms7ktK^GmQv5&L^FPk1Wvf&ik@=}guxI9iR`>I2 zrMLq6y1FT<67_}NX|%_by;GJiX21ae9)PP$sYlA1PQOt9EEbEfh(-o&Ap zz<>%915L60$q7TUB+RNL4b=&8XfR#3v^`91*g|n0bQ!-nz9VB#1YD!Q0 z`vEnmQbEO3kkY)tvGdh630lFuFnhHbIk=#jq%j2iC{g=ok*f6em#X1^uPVL$rD}<0 zk7$d{8&oz0i}~WRM9cmXqwraLFnDn%+PC)Qr7xwsSrWv^FueslKaaE5${1)H3v|A` zQL18ii6RQRj1oo3jUvUukdeGzD=!icWkYuCpi7n_sepqnDI2n52VJrhNd+8qN!gGcJLr<7NGjlvSSBbvJ^=L9CS(9kRAUmUCt$6#U)0A@860CU$oVGM7aG+ zx)i)$P#^>iYXw1xV<6~FKKT0(f(9cH^r$}sspmpa1M7@_2aP~>rN3Sil5k~bNR;zI zMb&`u+Xm(N9QEw_mw9yDh}}aTIFIf}ENi{AB{8V?iK;V%@;%#BZT)7rZ-XP%rrg;* zY2GicjnYL5hu-XU@3jwV*s>jCV6Rp7asR&D+qj(XmXr_67~DKIJ}Koy;GuStI=ik^ zAJ{l5=n%a?c$Ix{Y|lnce*}cQ^{Mky;*xv$SAXk$y0`V+VyHjv-6!Sd#I&DBY_8C) l{`BKLM;#AqaJMNm|HjzV`C)Itci%#gzoCnMowonz{|1(XH(&q& literal 0 HcmV?d00001 diff --git a/gfx.3ds/c3dlogo.png b/gfx.3ds/c3dlogo.png new file mode 100644 index 0000000000000000000000000000000000000000..78634d8510262dc39506e44080d3257d5c3d723c GIT binary patch literal 791 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 Fe*g}s6B_^k literal 0 HcmV?d00001 diff --git a/gfx.3ds/wifiNull.png b/gfx.3ds/wifiNull.png new file mode 100644 index 0000000000000000000000000000000000000000..91c4176e0ef5a718417b9f89fbb7412a986a15af GIT binary patch literal 22355 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 FKLDnHLcIV0 literal 0 HcmV?d00001 diff --git a/gfx.switch/deko3d.png b/gfx.switch/deko3d.png new file mode 100644 index 0000000000000000000000000000000000000000..fbf1adf94422479ea6200267e072c4830ad92df7 GIT binary patch literal 55542 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 o2=g^ -#elif defined(__SWITCH__) -#include -#endif - -#if defined(_3DS) || defined(__SWITCH__) -#define ESC(x) "\x1b[" #x -#define RESET ESC(0m) -#define BLACK ESC(30m) -#define RED ESC(31;1m) -#define GREEN ESC(32;1m) -#define YELLOW ESC(33;1m) -#define BLUE ESC(34;1m) -#define MAGENTA ESC(35;1m) -#define CYAN ESC(36;1m) -#define WHITE ESC(37;1m) -#else -#define ESC(x) -#define RESET -#define BLACK -#define RED -#define GREEN -#define YELLOW -#define BLUE -#define MAGENTA -#define CYAN -#define WHITE -#endif - -void console_init(void); - -__attribute__((format(printf,1,2))) -void console_set_status(const char *fmt, ...); - -__attribute__((format(printf,1,2))) -void console_print(const char *fmt, ...); - -__attribute__((format(printf,1,2))) -void debug_print(const char *fmt, ...); - -void console_render(void); diff --git a/include/fs.h b/include/fs.h new file mode 100644 index 0000000..a50fc5c --- /dev/null +++ b/include/fs.h @@ -0,0 +1,113 @@ +// ftpd is a server implementation based on the following: +// - RFC 959 (https://tools.ietf.org/html/rfc959) +// - RFC 3659 (https://tools.ietf.org/html/rfc3659) +// - suggested implementation details from https://cr.yp.to/ftp/filesystem.html +// +// Copyright (C) 2020 Michael Theall +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// 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, see . + +#pragma once + +#include + +#include +#include +#include +#include + +namespace fs +{ +std::string printSize (std::uint64_t size_); + +class File +{ +public: + ~File (); + + File (); + + File (File const &that_) = delete; + + File (File &&that_); + + File &operator= (File const &that_) = delete; + + File &operator= (File &&that_); + + operator bool () const; + operator FILE * () const; + + void setBufferSize (std::size_t size_); + + bool open (char const *path_, char const *mode_ = "rb"); + void close (); + + ssize_t seek (std::size_t pos_, int origin_); + + ssize_t read (void *data_, std::size_t size_); + bool readAll (void *data_, std::size_t size_); + + ssize_t write (void const *data_, std::size_t size_); + bool writeAll (void const *data_, std::size_t size_); + + template + T read () + { + T data; + if (read (&data, sizeof (data)) != sizeof (data)) + std::abort (); + + return data; + } + + template + void write (T const &data_) + { + if (write (&data_, sizeof (data_)) != sizeof (data_)) + std::abort (); + } + +private: + std::unique_ptr m_fp{nullptr, nullptr}; + std::unique_ptr m_buffer; + std::size_t m_bufferSize = 0; +}; + +class Dir +{ +public: + ~Dir (); + + Dir (); + + Dir (Dir const &that_) = delete; + + Dir (Dir &&that_); + + Dir &operator= (Dir const &that_) = delete; + + Dir &operator= (Dir &&that_); + + operator bool () const; + operator DIR * () const; + + bool open (char const *const path_); + void close (); + struct dirent *read (); + +private: + std::unique_ptr m_dp{nullptr, nullptr}; +}; +} diff --git a/include/ftp.h b/include/ftp.h deleted file mode 100644 index 279586e..0000000 --- a/include/ftp.h +++ /dev/null @@ -1,13 +0,0 @@ -#pragma once - -/*! Loop status */ -typedef enum -{ - LOOP_CONTINUE, /*!< Continue looping */ - LOOP_RESTART, /*!< Reinitialize */ - LOOP_EXIT, /*!< Terminate looping */ -} loop_status_t; - -int ftp_init(void); -loop_status_t ftp_loop(void); -void ftp_exit(void); diff --git a/include/ftpServer.h b/include/ftpServer.h new file mode 100644 index 0000000..cabe8a0 --- /dev/null +++ b/include/ftpServer.h @@ -0,0 +1,73 @@ +// ftpd is a server implementation based on the following: +// - RFC 959 (https://tools.ietf.org/html/rfc959) +// - RFC 3659 (https://tools.ietf.org/html/rfc3659) +// - suggested implementation details from https://cr.yp.to/ftp/filesystem.html +// +// Copyright (C) 2020 Michael Theall +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// 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, see . + +#pragma once + +#include "ftpSession.h" +#include "log.h" +#include "platform.h" +#include "socket.h" + +#include +#include +#include +#include +#include + +class FtpServer; +using UniqueFtpServer = std::unique_ptr; + +class FtpServer +{ +public: + ~FtpServer (); + + void draw (); + + static UniqueFtpServer create (std::uint16_t port_); + + static void updateFreeSpace (); + + static std::time_t startTime (); + +private: + FtpServer (std::uint16_t port_); + + void handleStartButton (); + void handleStopButton (); + + void loop (); + void threadFunc (); + + platform::Thread m_thread; + platform::Mutex m_lock; + + UniqueSocket m_socket; + + std::string m_name; + + SharedLog m_log; + + std::vector m_sessions; + + std::uint16_t m_port; + + std::atomic m_quit; +}; diff --git a/include/ftpSession.h b/include/ftpSession.h new file mode 100644 index 0000000..fbcdbbb --- /dev/null +++ b/include/ftpSession.h @@ -0,0 +1,205 @@ +// ftpd is a server implementation based on the following: +// - RFC 959 (https://tools.ietf.org/html/rfc959) +// - RFC 3659 (https://tools.ietf.org/html/rfc3659) +// - suggested implementation details from https://cr.yp.to/ftp/filesystem.html +// +// Copyright (C) 2020 Michael Theall +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// 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, see . + +#pragma once + +#include "fs.h" +#include "ioBuffer.h" +#include "platform.h" +#include "socket.h" + +#include +#include +#include +#include +#include + +class FtpSession; +using UniqueFtpSession = std::unique_ptr; + +class FtpSession +{ +public: + ~FtpSession (); + + bool dead (); + + void draw (); + + static UniqueFtpSession create (UniqueSocket commandSocket_); + + static void poll (std::vector const &sessions_); + +private: + constexpr static auto COMMAND_BUFFERSIZE = 4096; + constexpr static auto RESPONSE_BUFFERSIZE = 32768; + constexpr static auto XFER_BUFFERSIZE = 65536; + constexpr static auto FILE_BUFFERSIZE = 4 * XFER_BUFFERSIZE; + +#ifdef _3DS + constexpr static auto SOCK_BUFFERSIZE = 32768; + constexpr static auto POSITION_HISTORY = 100; +#else + constexpr static auto SOCK_BUFFERSIZE = XFER_BUFFERSIZE; + constexpr static auto POSITION_HISTORY = 300; +#endif + + enum class State + { + COMMAND, + DATA_CONNECT, + DATA_TRANSFER, + }; + + enum class XferFileMode + { + RETR, + STOR, + APPE, + }; + + enum class XferDirMode + { + LIST, + MLSD, + MLST, + NLST, + STAT, + }; + + FtpSession (UniqueSocket commandSocket_); + + void setState (State state_, bool closePasv_, bool closeData_); + + void closeData (); + + bool changeDir (char const *args_); + + bool dataAccept (); + bool dataConnect (); + + void updateFreeSpace (); + + int fillDirent (struct stat const &st_, std::string_view path_, char const *type_ = nullptr); + int fillDirent (std::string const &path_, char const *type_ = nullptr); + void xferFile (char const *args_, XferFileMode mode_); + void xferDir (char const *args_, XferDirMode mode_, bool workaround_); + + void readCommand (int events_); + void writeResponse (); + + __attribute__ ((format (printf, 2, 3))) void sendResponse (char const *fmt_, ...); + void sendResponse (std::string_view response_); + + bool (FtpSession::*m_transfer) () = nullptr; + + bool listTransfer (); + bool retrieveTransfer (); + bool storeTransfer (); + + platform::Mutex m_lock; + + SharedSocket m_commandSocket; + UniqueSocket m_pasvSocket; + SharedSocket m_dataSocket; + std::vector m_pendingCloseSocket; + + IOBuffer m_commandBuffer; + IOBuffer m_responseBuffer; + IOBuffer m_xferBuffer; + + SockAddr m_pasvAddr; + SockAddr m_portAddr; + + std::string m_cwd = "/"; + std::string m_lwd; + std::string m_rename; + std::string m_workItem; + + std::string m_windowName; + std::string m_plotName; + + std::uint64_t m_restartPosition = 0; + std::uint64_t m_filePosition = 0; + std::uint64_t m_fileSize = 0; + + platform::steady_clock::time_point m_filePositionTime; + std::uint64_t m_filePositionHistory[POSITION_HISTORY]; + float m_filePositionDeltas[POSITION_HISTORY]; + float m_xferRate; + + State m_state = State::COMMAND; + + fs::File m_file; + fs::Dir m_dir; + + XferDirMode m_xferDirMode; + + bool m_pasv : 1; + bool m_port : 1; + bool m_recv : 1; + bool m_send : 1; + bool m_urgent : 1; + + bool m_mlstType : 1; + bool m_mlstSize : 1; + bool m_mlstModify : 1; + bool m_mlstPerm : 1; + bool m_mlstUnixMode : 1; + + void ABOR (char const *args_); + void ALLO (char const *args_); + void APPE (char const *args_); + void CDUP (char const *args_); + void CWD (char const *args_); + void DELE (char const *args_); + void FEAT (char const *args_); + void HELP (char const *args_); + void LIST (char const *args_); + void MDTM (char const *args_); + void MKD (char const *args_); + void MLSD (char const *args_); + void MLST (char const *args_); + void MODE (char const *args_); + void NLST (char const *args_); + void NOOP (char const *args_); + void OPTS (char const *args_); + void PASS (char const *args_); + void PASV (char const *args_); + void PORT (char const *args_); + void PWD (char const *args_); + void QUIT (char const *args_); + void REST (char const *args_); + void RETR (char const *args_); + void RMD (char const *args_); + void RNFR (char const *args_); + void RNTO (char const *args_); + void SIZE (char const *args_); + void STAT (char const *args_); + void STOR (char const *args_); + void STOU (char const *args_); + void STRU (char const *args_); + void SYST (char const *args_); + void TYPE (char const *args_); + void USER (char const *args_); + + static std::vector> const + handlers; +}; diff --git a/include/imgui.h b/include/imgui.h new file mode 100644 index 0000000..4b71f2a --- /dev/null +++ b/include/imgui.h @@ -0,0 +1 @@ +#error "Please use https://github.com/ocornut/imgui/releases/tag/v1.75" diff --git a/include/ioBuffer.h b/include/ioBuffer.h new file mode 100644 index 0000000..fd8c961 --- /dev/null +++ b/include/ioBuffer.h @@ -0,0 +1,51 @@ +// ftpd is a server implementation based on the following: +// - RFC 959 (https://tools.ietf.org/html/rfc959) +// - RFC 3659 (https://tools.ietf.org/html/rfc3659) +// - suggested implementation details from https://cr.yp.to/ftp/filesystem.html +// +// Copyright (C) 2020 Michael Theall +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// 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, see . + +#pragma once + +#include +#include + +class IOBuffer +{ +public: + ~IOBuffer (); + + IOBuffer (std::size_t size_); + + char *freeArea () const; + std::size_t freeSize () const; + void markFree (std::size_t size_); + + char *usedArea () const; + std::size_t usedSize () const; + void markUsed (std::size_t size_); + + bool empty () const; + std::size_t capacity () const; + void clear (); + void coalesce (); + +private: + std::unique_ptr m_buffer; + std::size_t const m_size; + std::size_t m_start = 0; + std::size_t m_end = 0; +}; diff --git a/include/log.h b/include/log.h new file mode 100644 index 0000000..1afcb05 --- /dev/null +++ b/include/log.h @@ -0,0 +1,81 @@ +// ftpd is a server implementation based on the following: +// - RFC 959 (https://tools.ietf.org/html/rfc959) +// - RFC 3659 (https://tools.ietf.org/html/rfc3659) +// - suggested implementation details from https://cr.yp.to/ftp/filesystem.html +// +// Copyright (C) 2020 Michael Theall +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// 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, see . + +#pragma once + +#include "platform.h" + +#include +#include +#include +#include +#include + +class Log; +using SharedLog = std::shared_ptr; +using WeakLog = std::weak_ptr; + +class Log +{ +public: + enum Level + { + DEBUG, + INFO, + ERROR, + COMMAND, + RESPONSE, + }; + + ~Log (); + + void draw (); + + static SharedLog create (); + static void bind (SharedLog log_); + + __attribute__ ((format (printf, 1, 2))) static void debug (char const *fmt_, ...); + __attribute__ ((format (printf, 1, 2))) static void info (char const *fmt_, ...); + __attribute__ ((format (printf, 1, 2))) static void error (char const *fmt_, ...); + __attribute__ ((format (printf, 1, 2))) static void command (char const *fmt_, ...); + __attribute__ ((format (printf, 1, 2))) static void response (char const *fmt_, ...); + + static void log (Level level_, char const *fmt_, va_list ap_); + static void log (Level level_, std::string_view message_); + +private: + Log (); + + void _log (Level level_, char const *fmt_, va_list ap_); + + struct Message + { + Message (Level const level_, std::string message_) + : level (level_), message (std::move (message_)) + { + } + + Level level; + std::string message; + }; + + std::vector m_messages; + platform::Mutex m_lock; +}; diff --git a/include/platform.h b/include/platform.h new file mode 100644 index 0000000..689d3b5 --- /dev/null +++ b/include/platform.h @@ -0,0 +1,95 @@ +// ftpd is a server implementation based on the following: +// - RFC 959 (https://tools.ietf.org/html/rfc959) +// - RFC 3659 (https://tools.ietf.org/html/rfc3659) +// - suggested implementation details from https://cr.yp.to/ftp/filesystem.html +// +// Copyright (C) 2020 Michael Theall +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// 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, see . + +#pragma once + +#ifdef _3DS +#include <3ds.h> +#endif + +#include +#include +#include +#include + +namespace platform +{ +bool init (); +bool loop (); +void render (); +void exit (); + +#ifdef _3DS +struct steady_clock +{ + using rep = std::uint64_t; + using period = std::ratio<1, SYSCLOCK_ARM11>; + using duration = std::chrono::duration; + using time_point = std::chrono::time_point; + + constexpr static bool is_steady = true; + static time_point now () noexcept + { + return time_point (duration (svcGetSystemTick ())); + } +}; +#else +using steady_clock = std::chrono::steady_clock; +#endif + +class Thread +{ +public: + ~Thread (); + Thread (); + + Thread (std::function func_); + + Thread (Thread const &that_) = delete; + + Thread (Thread &&that_); + + Thread &operator= (Thread const &that_) = delete; + + Thread &operator= (Thread &&that_); + + void join (); + + static void sleep (std::chrono::milliseconds timeout_); + +private: + class privateData_t; + std::unique_ptr m_d; +}; + +class Mutex +{ +public: + ~Mutex (); + Mutex (); + + void lock (); + void unlock (); + +private: + class privateData_t; + std::unique_ptr m_d; +}; +} diff --git a/include/sockAddr.h b/include/sockAddr.h new file mode 100644 index 0000000..d44f30e --- /dev/null +++ b/include/sockAddr.h @@ -0,0 +1,70 @@ +// ftpd is a server implementation based on the following: +// - RFC 959 (https://tools.ietf.org/html/rfc959) +// - RFC 3659 (https://tools.ietf.org/html/rfc3659) +// - suggested implementation details from https://cr.yp.to/ftp/filesystem.html +// +// Copyright (C) 2020 Michael Theall +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// 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, see . + +#pragma once + +#include +#include + +#include + +class SockAddr +{ +public: + ~SockAddr (); + + SockAddr (); + + SockAddr (SockAddr const &that_); + + SockAddr (SockAddr &&that_); + + SockAddr &operator= (SockAddr const &that_); + + SockAddr &operator= (SockAddr &&that_); + + SockAddr (struct sockaddr const &addr_); + + SockAddr (struct sockaddr_in const &addr_); + +#ifndef _3DS + SockAddr (struct sockaddr_in6 const &addr_); +#endif + + SockAddr (struct sockaddr_storage const &addr_); + + operator struct sockaddr_in const & () const; + +#ifndef _3DS + operator struct sockaddr_in6 const & () const; +#endif + + operator struct sockaddr_storage const & () const; + + operator struct sockaddr * (); + operator struct sockaddr const * () const; + + std::uint16_t port () const; + char const *name (char *buffer_, std::size_t size_) const; + char const *name () const; + +private: + struct sockaddr_storage m_addr = {}; +}; diff --git a/include/socket.h b/include/socket.h new file mode 100644 index 0000000..c3fac2a --- /dev/null +++ b/include/socket.h @@ -0,0 +1,97 @@ +// ftpd is a server implementation based on the following: +// - RFC 959 (https://tools.ietf.org/html/rfc959) +// - RFC 3659 (https://tools.ietf.org/html/rfc3659) +// - suggested implementation details from https://cr.yp.to/ftp/filesystem.html +// +// Copyright (C) 2020 Michael Theall +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// 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, see . + +#pragma once + +#include "ioBuffer.h" +#include "sockAddr.h" + +#include +#include + +class Socket; +using UniqueSocket = std::unique_ptr; +using SharedSocket = std::shared_ptr; + +class Socket +{ +public: + struct PollInfo + { + std::reference_wrapper socket; + int events; + int revents; + }; + + ~Socket (); + + UniqueSocket accept (); + int atMark (); + bool bind (SockAddr const &addr_); + bool connect (SockAddr const &addr_); + bool listen (int backlog_); + bool shutdown (int how_); + + bool setLinger (bool enable_, std::chrono::seconds time_); + bool setNonBlocking (bool nonBlocking_ = true); + bool setReuseAddress (bool reuse_ = true); + bool setRecvBufferSize (std::size_t size_); + bool setSendBufferSize (std::size_t size_); + + ssize_t read (void *buffer_, std::size_t size_, bool oob_ = false); + ssize_t read (IOBuffer &buffer_, bool oob_ = false); + ssize_t write (void const *buffer_, std::size_t size_); + ssize_t write (IOBuffer &buffer_); + + SockAddr const &sockName () const; + SockAddr const &peerName () const; + + static UniqueSocket create (); + + static int poll (PollInfo *info_, std::size_t count_, std::chrono::milliseconds timeout_); + + int fd () const + { + return m_fd; + } + +private: + Socket () = delete; + + Socket (int fd_); + + Socket (int fd_, SockAddr const &sockName_, SockAddr const &peerName_); + + Socket (Socket const &that_) = delete; + + Socket (Socket &&that_) = delete; + + Socket &operator= (Socket const &that_) = delete; + + Socket &operator= (Socket &&that_) = delete; + + SockAddr m_sockName; + SockAddr m_peerName; + + int const m_fd; + + bool m_listening : 1; + bool m_connected : 1; +}; diff --git a/source/3ds/imgui_citro3d.cpp b/source/3ds/imgui_citro3d.cpp new file mode 100644 index 0000000..48e7c74 --- /dev/null +++ b/source/3ds/imgui_citro3d.cpp @@ -0,0 +1,623 @@ +// ftpd is a server implementation based on the following: +// - RFC 959 (https://tools.ietf.org/html/rfc959) +// - RFC 3659 (https://tools.ietf.org/html/rfc3659) +// - suggested implementation details from https://cr.yp.to/ftp/filesystem.html +// +// Copyright (C) 2020 Michael Theall +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// 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, see . + +#include "imgui_citro3d.h" + +#include + +#include "vshader_shbin.h" + +#include "imgui.h" + +#include +#include +#include +#include + +namespace +{ +std::vector s_fontRanges; + +constexpr auto CLEAR_COLOR = 0x204B7AFF; + +constexpr auto DISPLAY_TRANSFER_FLAGS = + GX_TRANSFER_FLIP_VERT (0) | GX_TRANSFER_OUT_TILED (0) | GX_TRANSFER_RAW_COPY (0) | + GX_TRANSFER_IN_FORMAT (GX_TRANSFER_FMT_RGBA8) | GX_TRANSFER_OUT_FORMAT (GX_TRANSFER_FMT_RGB8) | + GX_TRANSFER_SCALING (GX_TRANSFER_SCALE_NO); + +C3D_RenderTarget *s_top = nullptr; +C3D_RenderTarget *s_bottom = nullptr; + +DVLB_s *s_vsh = nullptr; +shaderProgram_s s_program; + +int s_projLocation; +C3D_Mtx s_projTop; +C3D_Mtx s_projBottom; + +std::vector s_fontTextures; +float s_textScale; + +std::uint32_t s_boundScissor[4]; +ImDrawVert *s_boundVtxData; +C3D_Tex *s_boundTexture; + +ImDrawVert *s_vtxData = nullptr; +std::size_t s_vtxSize = 0; +ImDrawIdx *s_idxData = nullptr; +std::size_t s_idxSize = 0; + +std::uint32_t fontCodePointFromGlyphIndex (CFNT_s *const font_, int const glyphIndex_) +{ + for (auto cmap = fontGetInfo (font_)->cmap; cmap; cmap = cmap->next) + { + switch (cmap->mappingMethod) + { + case CMAP_TYPE_DIRECT: + assert (cmap->codeEnd >= cmap->codeBegin); + if (glyphIndex_ >= cmap->indexOffset && + glyphIndex_ <= cmap->codeEnd - cmap->codeBegin + cmap->indexOffset) + return glyphIndex_ - cmap->indexOffset + cmap->codeBegin; + break; + + case CMAP_TYPE_TABLE: + for (int i = 0; i <= cmap->codeEnd - cmap->codeBegin; ++i) + { + if (cmap->indexTable[i] == glyphIndex_) + return cmap->codeBegin + i; + } + break; + + case CMAP_TYPE_SCAN: + for (unsigned i = 0; i < cmap->nScanEntries; ++i) + { + assert (cmap->scanEntries[i].code >= cmap->codeBegin); + assert (cmap->scanEntries[i].code <= cmap->codeEnd); + if (glyphIndex_ == cmap->scanEntries[i].glyphIndex) + return cmap->scanEntries[i].code; + } + break; + } + } + + return 0; +} + +void setupRenderState (gfxScreen_t const screen_) +{ + C3D_CullFace (GPU_CULL_NONE); + + // configure attributes for user with vertex shader + auto const attrInfo = C3D_GetAttrInfo (); + AttrInfo_Init (attrInfo); + AttrInfo_AddLoader (attrInfo, 0, GPU_FLOAT, 2); // v0 = inPos + AttrInfo_AddLoader (attrInfo, 1, GPU_FLOAT, 2); // v1 = inUv + AttrInfo_AddLoader (attrInfo, 2, GPU_UNSIGNED_BYTE, 4); // v2 = inColor + + std::memset (s_boundScissor, 0xFF, sizeof (s_boundScissor)); + s_boundVtxData = nullptr; + s_boundTexture = nullptr; + + C3D_BindProgram (&s_program); + + C3D_DepthTest (true, GPU_GREATER, GPU_WRITE_COLOR); + + C3D_AlphaBlend (GPU_BLEND_ADD, + GPU_BLEND_ADD, + GPU_SRC_ALPHA, + GPU_ONE_MINUS_SRC_ALPHA, + GPU_SRC_ALPHA, + GPU_ONE_MINUS_SRC_ALPHA); + + if (screen_ == GFX_TOP) + C3D_FVUnifMtx4x4 (GPU_VERTEX_SHADER, s_projLocation, &s_projTop); + else + C3D_FVUnifMtx4x4 (GPU_VERTEX_SHADER, s_projLocation, &s_projBottom); +} +} + +void imgui::citro3d::init () +{ + // Setup back-end capabilities flags + ImGuiIO &io = ImGui::GetIO (); + + io.BackendRendererName = "citro3d"; + io.BackendFlags |= ImGuiBackendFlags_RendererHasVtxOffset; + + C3D_Init (C3D_DEFAULT_CMDBUF_SIZE); + + s_top = C3D_RenderTargetCreate (240, 400, GPU_RB_RGBA8, GPU_RB_DEPTH24_STENCIL8); + C3D_RenderTargetSetOutput (s_top, GFX_TOP, GFX_LEFT, DISPLAY_TRANSFER_FLAGS); + + s_bottom = C3D_RenderTargetCreate (240, 320, GPU_RB_RGBA8, GPU_RB_DEPTH24_STENCIL8); + C3D_RenderTargetSetOutput (s_bottom, GFX_BOTTOM, GFX_LEFT, DISPLAY_TRANSFER_FLAGS); + + s_vsh = DVLB_ParseFile ( + const_cast (reinterpret_cast (vshader_shbin)), + vshader_shbin_size); + shaderProgramInit (&s_program); + shaderProgramSetVsh (&s_program, &s_vsh->DVLE[0]); + + s_projLocation = shaderInstanceGetUniformLocation (s_program.vertexShader, "proj"); + + Mtx_OrthoTilt (&s_projTop, 0.0f, 800.0f, 480.0f, 0.0f, -1.0f, 1.0f, false); + Mtx_OrthoTilt (&s_projBottom, 80.0f, 720.0f, 960.0f, 480.0f, -1.0f, 1.0f, false); + + s_vtxSize = 65536; + s_vtxData = reinterpret_cast (linearAlloc (sizeof (ImDrawVert) * s_vtxSize)); + if (!s_vtxData) + svcBreak (USERBREAK_PANIC); + + s_idxSize = 65536; + s_idxData = reinterpret_cast (linearAlloc (sizeof (ImDrawIdx) * s_idxSize)); + if (!s_idxData) + svcBreak (USERBREAK_PANIC); + + // ensure the shared system font is mapped + if (R_FAILED (fontEnsureMapped ())) + svcBreak (USERBREAK_PANIC); + + // load the glyph texture sheets + auto const font = fontGetSystemFont (); + auto const fontInfo = fontGetInfo (font); + auto const glyphInfo = fontGetGlyphInfo (font); + assert (s_fontTextures.empty ()); + s_fontTextures.resize (glyphInfo->nSheets + 1); + std::memset (s_fontTextures.data (), 0x00, s_fontTextures.size () * sizeof (s_fontTextures[0])); + + s_textScale = 30.0f / glyphInfo->cellHeight; + + for (unsigned i = 0; i < glyphInfo->nSheets; ++i) + { + auto &tex = s_fontTextures[i]; + tex.data = fontGetGlyphSheetTex (font, i); + if (!tex.data) + svcBreak (USERBREAK_PANIC); + tex.fmt = static_cast (glyphInfo->sheetFmt); + tex.size = glyphInfo->sheetSize; + tex.width = glyphInfo->sheetWidth; + tex.height = glyphInfo->sheetHeight; + tex.param = GPU_TEXTURE_MAG_FILTER (GPU_LINEAR) | GPU_TEXTURE_MIN_FILTER (GPU_LINEAR) | + GPU_TEXTURE_WRAP_S (GPU_REPEAT) | GPU_TEXTURE_WRAP_T (GPU_REPEAT); + tex.border = 0xFFFFFFFF; + tex.lodParam = 0; + } + + { + auto &tex = s_fontTextures[glyphInfo->nSheets]; + C3D_TexInit (&tex, 8, 8, GPU_A4); + + std::uint32_t size; + auto data = C3D_Tex2DGetImagePtr (&tex, 0, &size); + if (!data || !size) + svcBreak (USERBREAK_PANIC); + std::memset (data, 0xFF, size); + } + + ImWchar alterChar = fontCodePointFromGlyphIndex (font, fontInfo->alterCharIndex); + if (!alterChar) + alterChar = '?'; + + std::vector charSet; + for (auto cmap = fontInfo->cmap; cmap; cmap = cmap->next) + { + switch (cmap->mappingMethod) + { + case CMAP_TYPE_DIRECT: + case CMAP_TYPE_TABLE: + assert (cmap->codeEnd >= cmap->codeBegin); + charSet.reserve (charSet.size () + cmap->codeEnd - cmap->codeBegin + 1); + for (auto i = cmap->codeBegin; i <= cmap->codeEnd; ++i) + charSet.emplace_back (i); + break; + case CMAP_TYPE_SCAN: + charSet.reserve (charSet.size () + cmap->nScanEntries); + for (unsigned i = 0; i < cmap->nScanEntries; ++i) + { + assert (cmap->scanEntries[i].code >= cmap->codeBegin); + assert (cmap->scanEntries[i].code <= cmap->codeEnd); + charSet.emplace_back (cmap->scanEntries[i].code); + } + break; + } + } + + if (charSet.empty ()) + svcBreak (USERBREAK_PANIC); + + std::sort (std::begin (charSet), std::end (charSet)); + charSet.erase (std::unique (std::begin (charSet), std::end (charSet)), std::end (charSet)); + + auto it = std::begin (charSet); + ImWchar start = *it++; + ImWchar prev = start; + while (it != std::end (charSet)) + { + if (*it != prev + 1) + { + s_fontRanges.emplace_back (start); + s_fontRanges.emplace_back (prev); + + start = *it; + } + + prev = *it++; + } + s_fontRanges.emplace_back (start); + s_fontRanges.emplace_back (prev); + s_fontRanges.emplace_back (0); + + auto const atlas = ImGui::GetIO ().Fonts; + atlas->Clear (); + atlas->TexWidth = glyphInfo->sheetWidth; + atlas->TexHeight = glyphInfo->sheetHeight * glyphInfo->nSheets; + atlas->TexUvScale = ImVec2 (1.0f / atlas->TexWidth, 1.0f / atlas->TexHeight); + atlas->TexUvWhitePixel = ImVec2 (0.5f / 8.0f, glyphInfo->nSheets + 0.5f / 8.0f); + atlas->TexPixelsAlpha8 = static_cast (IM_ALLOC (1)); // dummy allocation + + ImFontConfig config; + config.FontData = nullptr; + config.FontDataSize = 0; + config.FontDataOwnedByAtlas = true; + config.FontNo = 0; + config.SizePixels = 14.0f; + config.OversampleH = 3; + config.OversampleV = 1; + config.PixelSnapH = false; + config.GlyphExtraSpacing = ImVec2 (0.0f, 0.0f); + config.GlyphOffset = ImVec2 (0.0f, 0.0f); + config.GlyphRanges = s_fontRanges.data (); + config.GlyphMinAdvanceX = 0.0f; + config.GlyphMaxAdvanceX = std::numeric_limits::max (); + config.MergeMode = false; + config.RasterizerFlags = 0; + config.RasterizerMultiply = 1.0f; + config.EllipsisChar = 0x2026; + std::memset (config.Name, 0, sizeof (config.Name)); + + auto const imFont = IM_NEW (ImFont); + config.DstFont = imFont; + + atlas->ConfigData.push_back (config); + atlas->Fonts.push_back (imFont); + // atlas->CustomRectIds[0] = atlas->AddCustomRectRegular (0x80000000, 108 * 2 + 1, 27); + // atlas->CustomRects[0].X = 0; + // atlas->CustomRects[0].Y = 0; + atlas->SetTexID (s_fontTextures.data ()); + + imFont->FallbackAdvanceX = fontInfo->defaultWidth.charWidth; + imFont->FontSize = fontInfo->lineFeed; + + fontGlyphPos_s glyphPos; + for (auto const &code : charSet) + { + auto const glyphIndex = fontGlyphIndexFromCodePoint (font, code); + if (glyphIndex < 0) + svcBreak (USERBREAK_PANIC); + + fontCalcGlyphPos (&glyphPos, + font, + glyphIndex, + GLYPH_POS_CALC_VTXCOORD | GLYPH_POS_AT_BASELINE, + 1.0f, + 1.0f); + + ImFontGlyph glyph; + + glyph.Codepoint = code; + glyph.AdvanceX = glyphPos.xAdvance; + glyph.X0 = glyphPos.vtxcoord.left; + glyph.Y0 = glyphPos.vtxcoord.top; + glyph.X1 = glyphPos.vtxcoord.right; + glyph.Y1 = glyphPos.vtxcoord.bottom; + glyph.U0 = glyphPos.texcoord.left; + glyph.V0 = glyphPos.sheetIndex + glyphPos.texcoord.top; + glyph.U1 = glyphPos.texcoord.right; + glyph.V1 = glyphPos.sheetIndex + glyphPos.texcoord.bottom; + + imFont->Glyphs.push_back (glyph); + imFont->MetricsTotalSurface += + static_cast ((glyph.U1 - glyph.U0) * atlas->TexWidth + 1.99f) * + static_cast ((glyph.V1 - glyph.V0) * atlas->TexHeight + 1.99f); + } + + imFont->BuildLookupTable (); + + imFont->DisplayOffset.x = 0.0f; + imFont->DisplayOffset.y = fontInfo->ascent; + + imFont->ContainerAtlas = atlas; + imFont->ConfigData = &atlas->ConfigData[0]; + imFont->ConfigDataCount = 1; + imFont->FallbackChar = alterChar; + imFont->EllipsisChar = config.EllipsisChar; + imFont->Scale = 1.0f; + imFont->Ascent = fontInfo->ascent; + imFont->Descent = 0.0f; +} + +void imgui::citro3d::exit () +{ + linearFree (s_idxData); + linearFree (s_vtxData); + + assert (!s_fontTextures.empty ()); + C3D_TexDelete (&s_fontTextures.back ()); + + shaderProgramFree (&s_program); + DVLB_Free (s_vsh); + + C3D_RenderTargetDelete (s_bottom); + C3D_RenderTargetDelete (s_top); + + C3D_Fini (); +} + +void imgui::citro3d::newFrame () +{ +} + +void imgui::citro3d::render () +{ + C3D_FrameBegin (C3D_FRAME_SYNCDRAW); + C3D_RenderTargetClear (s_top, C3D_CLEAR_ALL, CLEAR_COLOR, 0); + C3D_RenderTargetClear (s_bottom, C3D_CLEAR_ALL, CLEAR_COLOR, 0); + + auto const drawData = ImGui::GetDrawData (); + if (drawData->CmdListsCount <= 0) + { + C3D_FrameEnd (0); + return; + } + + unsigned width = drawData->DisplaySize.x * drawData->FramebufferScale.x; + unsigned height = drawData->DisplaySize.y * drawData->FramebufferScale.y; + if (width <= 0 || height <= 0) + { + C3D_FrameEnd (0); + return; + } + + if (s_vtxSize < static_cast (drawData->TotalVtxCount)) + { + linearFree (s_vtxData); + + // add 10% to avoid growing many frames in a row + s_vtxSize = drawData->TotalVtxCount * 1.1f; + s_vtxData = reinterpret_cast (linearAlloc (sizeof (ImDrawVert) * s_vtxSize)); + if (!s_vtxData) + svcBreak (USERBREAK_PANIC); + } + + if (s_idxSize < static_cast (drawData->TotalIdxCount)) + { + // add 10% to avoid growing many frames in a row + s_idxSize = drawData->TotalIdxCount * 1.1f; + s_idxData = reinterpret_cast (linearAlloc (sizeof (ImDrawIdx) * s_idxSize)); + if (!s_vtxData) + svcBreak (USERBREAK_PANIC); + } + + // Will project scissor/clipping rectangles into framebuffer space + // (0,0) unless using multi-viewports + auto const clipOff = drawData->DisplayPos; + // (1,1) unless using retina display which are often (2,2) + auto const clipScale = drawData->FramebufferScale; + + // copy data into vertex/index buffers + std::size_t offsetVtx = 0; + std::size_t offsetIdx = 0; + for (int i = 0; i < drawData->CmdListsCount; ++i) + { + auto const &cmdList = *drawData->CmdLists[i]; + if (s_vtxSize - offsetVtx < static_cast (cmdList.VtxBuffer.Size)) + svcBreak (USERBREAK_PANIC); + if (s_idxSize - offsetIdx < static_cast (cmdList.IdxBuffer.Size)) + svcBreak (USERBREAK_PANIC); + + std::memcpy (&s_vtxData[offsetVtx], + cmdList.VtxBuffer.Data, + sizeof (ImDrawVert) * cmdList.VtxBuffer.Size); + std::memcpy (&s_idxData[offsetIdx], + cmdList.IdxBuffer.Data, + sizeof (ImDrawIdx) * cmdList.IdxBuffer.Size); + + offsetVtx += cmdList.VtxBuffer.Size; + offsetIdx += cmdList.IdxBuffer.Size; + } + + for (auto const &screen : {GFX_TOP, GFX_BOTTOM}) + { + if (screen == GFX_TOP) + C3D_FrameDrawOn (s_top); + else + C3D_FrameDrawOn (s_bottom); + + setupRenderState (screen); + + offsetVtx = 0; + offsetIdx = 0; + + // Render command lists + for (int i = 0; i < drawData->CmdListsCount; ++i) + { + auto const &cmdList = *drawData->CmdLists[i]; + for (auto const &cmd : cmdList.CmdBuffer) + { + if (cmd.UserCallback) + { + // User callback, registered via ImDrawList::AddCallback() + // (ImDrawCallback_ResetRenderState is a special callback value used by the user + // to request the renderer to reset render state.) + if (cmd.UserCallback == ImDrawCallback_ResetRenderState) + setupRenderState (screen); + else + cmd.UserCallback (&cmdList, &cmd); + } + else + { + // Project scissor/clipping rectangles into framebuffer space + ImVec4 clip; + clip.x = (cmd.ClipRect.x - clipOff.x) * clipScale.x; + clip.y = (cmd.ClipRect.y - clipOff.y) * clipScale.y; + clip.z = (cmd.ClipRect.z - clipOff.x) * clipScale.x; + clip.w = (cmd.ClipRect.w - clipOff.y) * clipScale.y; + + if (clip.x >= width || clip.y >= height || clip.z < 0.0f || clip.w < 0.0f) + continue; + if (clip.x < 0.0f) + clip.x = 0.0f; + if (clip.y < 0.0f) + clip.y = 0.0f; + + if (screen == GFX_TOP) + { + // check if clip starts on bottom screen + if (clip.y > 240.0f) + continue; + + auto const x1 = std::clamp (240.0f - clip.w, 0, 240); + auto const y1 = std::clamp (400.0f - clip.z, 0, 400); + auto const x2 = std::clamp (240.0f - clip.y, 0, 240); + auto const y2 = std::clamp (400.0f - clip.x, 0, 400); + + C3D_SetScissor (GPU_SCISSOR_NORMAL, x1, y1, x2, y2); + } + else + { + // check if clip ends on top screen + if (clip.w < 240.0f) + continue; + + // check if clip ends before left edge of bottom screen + if (clip.z < 40.0f) + continue; + + // check if clip starts after right edge of bottom screen + if (clip.x > 360.0f) + continue; + + auto const x1 = std::clamp (480.0f - clip.w, 0, 240); + auto const y1 = std::clamp (360.0f - clip.z, 0, 320); + auto const x2 = std::clamp (480.0f - clip.y, 0, 240); + auto const y2 = std::clamp (360.0f - clip.x, 0, 320); + + if (s_boundScissor[0] != x1 || s_boundScissor[1] != y1 || + s_boundScissor[2] != x2 || s_boundScissor[3] != y2) + { + s_boundScissor[0] = x1; + s_boundScissor[1] = y1; + s_boundScissor[2] = x2; + s_boundScissor[3] = y2; + C3D_SetScissor (GPU_SCISSOR_NORMAL, x1, y1, x2, y2); + } + } + + auto const vtxData = &s_vtxData[cmd.VtxOffset + offsetVtx]; + if (vtxData != s_boundVtxData) + { + s_boundVtxData = &s_vtxData[cmd.VtxOffset + offsetVtx]; + auto const bufInfo = C3D_GetBufInfo (); + BufInfo_Init (bufInfo); + BufInfo_Add (bufInfo, s_boundVtxData, sizeof (ImDrawVert), 3, 0x210); + } + + auto tex = static_cast (cmd.TextureId); + if (tex == s_fontTextures.data ()) + { + assert (cmd.ElemCount % 3 == 0); + + // TODO get by idx not consecutive vtx + auto const getSheet = [] (auto const vtx_, auto const idx_) { + unsigned const sheet = std::min ( + {vtx_[idx_[0]].uv.y, vtx_[idx_[1]].uv.y, vtx_[idx_[2]].uv.y}); + for (unsigned i = 0; i < 3; ++i) + assert (vtx_[idx_[i]].uv.y - sheet <= 1.0f); + return sheet; + }; + + unsigned boundSheet = getSheet (&s_vtxData[cmd.VtxOffset + offsetVtx], + &s_idxData[cmd.IdxOffset + offsetIdx]); + + unsigned offset = 0; + + C3D_TexBind (0, &s_fontTextures[boundSheet]); + + auto const env = C3D_GetTexEnv (0); + C3D_TexEnvInit (env); + C3D_TexEnvSrc ( + env, C3D_RGB, GPU_PRIMARY_COLOR, GPU_PRIMARY_COLOR, GPU_PRIMARY_COLOR); + C3D_TexEnvFunc (env, C3D_RGB, GPU_REPLACE); + C3D_TexEnvSrc ( + env, C3D_Alpha, GPU_TEXTURE0, GPU_PRIMARY_COLOR, GPU_PRIMARY_COLOR); + C3D_TexEnvFunc (env, C3D_Alpha, GPU_MODULATE); + + for (unsigned i = 3; i < cmd.ElemCount; i += 3) + { + unsigned const sheet = getSheet (&s_vtxData[cmd.VtxOffset + offsetVtx], + &s_idxData[cmd.IdxOffset + offsetIdx + i]); + if (boundSheet != sheet) + { + C3D_DrawElements (GPU_TRIANGLES, + i - offset, + C3D_UNSIGNED_SHORT, + &s_idxData[cmd.IdxOffset + offsetIdx + offset]); + + boundSheet = sheet; + offset = i; + C3D_TexBind (0, &s_fontTextures[boundSheet]); + } + } + + assert ((cmd.ElemCount - offset) % 3 == 0); + C3D_DrawElements (GPU_TRIANGLES, + cmd.ElemCount - offset, + C3D_UNSIGNED_SHORT, + &s_idxData[cmd.IdxOffset + offsetIdx + offset]); + } + else + { + if (tex != s_boundTexture) + { + C3D_TexBind (0, tex); + auto const env = C3D_GetTexEnv (0); + C3D_TexEnvInit (env); + C3D_TexEnvSrc ( + env, C3D_Both, GPU_TEXTURE0, GPU_PRIMARY_COLOR, GPU_PRIMARY_COLOR); + C3D_TexEnvFunc (env, C3D_Both, GPU_MODULATE); + } + + C3D_DrawElements (GPU_TRIANGLES, + cmd.ElemCount, + C3D_UNSIGNED_SHORT, + &s_idxData[cmd.IdxOffset + offsetIdx]); + } + + s_boundTexture = tex; + } + } + + offsetVtx += cmdList.VtxBuffer.Size; + offsetIdx += cmdList.IdxBuffer.Size; + } + } + + C3D_FrameEnd (0); +} diff --git a/source/3ds/imgui_citro3d.h b/source/3ds/imgui_citro3d.h new file mode 100644 index 0000000..a8e53ab --- /dev/null +++ b/source/3ds/imgui_citro3d.h @@ -0,0 +1,33 @@ +// ftpd is a server implementation based on the following: +// - RFC 959 (https://tools.ietf.org/html/rfc959) +// - RFC 3659 (https://tools.ietf.org/html/rfc3659) +// - suggested implementation details from https://cr.yp.to/ftp/filesystem.html +// +// Copyright (C) 2020 Michael Theall +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// 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, see . + +#pragma once + +namespace imgui +{ +namespace citro3d +{ +void init (); +void exit (); + +void newFrame (); +void render (); +} +} diff --git a/source/3ds/imgui_ctru.cpp b/source/3ds/imgui_ctru.cpp new file mode 100644 index 0000000..b04d314 --- /dev/null +++ b/source/3ds/imgui_ctru.cpp @@ -0,0 +1,163 @@ +// ftpd is a server implementation based on the following: +// - RFC 959 (https://tools.ietf.org/html/rfc959) +// - RFC 3659 (https://tools.ietf.org/html/rfc3659) +// - suggested implementation details from https://cr.yp.to/ftp/filesystem.html +// +// Copyright (C) 2020 Michael Theall +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// 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, see . + +#include "imgui_ctru.h" + +#include "imgui.h" + +#include "fs.h" + +#include <3ds.h> + +#include +#include +#include +#include +#include +using namespace std::chrono_literals; + +namespace +{ +constexpr auto SCREEN_WIDTH = 400.0f; +constexpr auto SCREEN_HEIGHT = 480.0f; + +std::string s_clipboard; + +char const *getClipboardText (void *const userData_) +{ + (void)userData_; + return s_clipboard.c_str (); +} + +void setClipboardText (void *const userData_, char const *const text_) +{ + (void)userData_; + s_clipboard = text_; +} + +void updateTouch (ImGuiIO &io_) +{ + if (!((hidKeysDown () | hidKeysHeld ()) & KEY_TOUCH)) + { + io_.MousePos = ImVec2 (-10.0f, -10.0f); + io_.MouseDown[0] = false; + return; + } + + touchPosition pos; + hidTouchRead (&pos); + + // transform to bottom-screen space + io_.MousePos = ImVec2 ((pos.px + 40.0f) * 2.0f, (pos.py + 240.0f) * 2.0f); + io_.MouseDown[0] = true; +} + +void updateGamepads (ImGuiIO &io_) +{ + std::memset (io_.NavInputs, 0, sizeof (io_.NavInputs)); + + auto const buttonMapping = { + std::make_pair (KEY_A, ImGuiNavInput_Activate), + std::make_pair (KEY_B, ImGuiNavInput_Cancel), + std::make_pair (KEY_X, ImGuiNavInput_Input), + std::make_pair (KEY_Y, ImGuiNavInput_Menu), + std::make_pair (KEY_L, ImGuiNavInput_FocusPrev), + std::make_pair (KEY_L, ImGuiNavInput_TweakSlow), + std::make_pair (KEY_R, ImGuiNavInput_FocusNext), + std::make_pair (KEY_R, ImGuiNavInput_TweakFast), + std::make_pair (KEY_DUP, ImGuiNavInput_DpadUp), + std::make_pair (KEY_DRIGHT, ImGuiNavInput_DpadRight), + std::make_pair (KEY_DDOWN, ImGuiNavInput_DpadDown), + std::make_pair (KEY_DLEFT, ImGuiNavInput_DpadLeft), + }; + + auto const keys = hidKeysHeld (); + for (auto const &[in, out] : buttonMapping) + { + if (keys & in) + io_.NavInputs[out] = 1.0f; + } + + circlePosition cpad; + auto const analogMapping = { + std::make_tuple (std::ref (cpad.dx), ImGuiNavInput_LStickLeft, -0.3f, -0.9f), + std::make_tuple (std::ref (cpad.dx), ImGuiNavInput_LStickRight, +0.3f, +0.9f), + std::make_tuple (std::ref (cpad.dy), ImGuiNavInput_LStickUp, +0.3f, +0.9f), + std::make_tuple (std::ref (cpad.dy), ImGuiNavInput_LStickDown, -0.3f, -0.9f), + }; + + hidCircleRead (&cpad); + for (auto const &[in, out, min, max] : analogMapping) + { + auto const value = in / static_cast (0x9C); + auto const v = std::min (1.0f, (value - min) / (max - min)); + io_.NavInputs[out] = std::max (io_.NavInputs[out], v); + } +} +} + +bool imgui::ctru::init () +{ + ImGuiIO &io = ImGui::GetIO (); + + io.IniFilename = nullptr; + + io.ConfigFlags |= ImGuiConfigFlags_IsTouchScreen; + io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad; + + io.BackendFlags |= ImGuiBackendFlags_HasGamepad; + + io.BackendPlatformName = "3ds"; + + io.MouseDrawCursor = false; + + io.SetClipboardTextFn = setClipboardText; + io.GetClipboardTextFn = getClipboardText; + io.ClipboardUserData = nullptr; + + return true; +} + +void imgui::ctru::newFrame () +{ + ImGuiIO &io = ImGui::GetIO (); + + IM_ASSERT (io.Fonts->IsBuilt () && + "Font atlas not built! It is generally built by the renderer back-end. Missing call " + "to renderer _NewFrame() function?"); + + io.DisplaySize = ImVec2 (SCREEN_WIDTH * 2.0f, SCREEN_HEIGHT * 2.0f); + io.DisplayFramebufferScale = ImVec2 (0.5f, 0.5f); + + // Setup time step + static auto const start = svcGetSystemTick (); + static auto prev = start; + auto const now = svcGetSystemTick (); + + io.DeltaTime = (now - prev) / static_cast (SYSCLOCK_ARM11); + prev = now; + + updateTouch (io); + updateGamepads (io); +} + +void imgui::ctru::exit () +{ +} diff --git a/source/3ds/imgui_ctru.h b/source/3ds/imgui_ctru.h new file mode 100644 index 0000000..2684b72 --- /dev/null +++ b/source/3ds/imgui_ctru.h @@ -0,0 +1,32 @@ +// ftpd is a server implementation based on the following: +// - RFC 959 (https://tools.ietf.org/html/rfc959) +// - RFC 3659 (https://tools.ietf.org/html/rfc3659) +// - suggested implementation details from https://cr.yp.to/ftp/filesystem.html +// +// Copyright (C) 2020 Michael Theall +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// 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, see . + +#pragma once + +namespace imgui +{ +namespace ctru +{ +bool init (); +void exit (); + +void newFrame (); +} +} diff --git a/source/3ds/platform.cpp b/source/3ds/platform.cpp new file mode 100644 index 0000000..c1e4f5d --- /dev/null +++ b/source/3ds/platform.cpp @@ -0,0 +1,348 @@ +// ftpd is a server implementation based on the following: +// - RFC 959 (https://tools.ietf.org/html/rfc959) +// - RFC 3659 (https://tools.ietf.org/html/rfc3659) +// - suggested implementation details from https://cr.yp.to/ftp/filesystem.html +// +// Copyright (C) 2020 Michael Theall +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// 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, see . + +#include "platform.h" + +#include "fs.h" +#include "log.h" + +#include "imgui_citro3d.h" +#include "imgui_ctru.h" + +#include "imgui.h" + +#include <3ds.h> +#include +#include + +#include "gfx.h" + +#include +#include +#include +#include + +namespace +{ +constexpr auto STACK_SIZE = 32768; +constexpr auto SOCU_ALIGN = 0x1000; +constexpr auto SOCU_BUFFERSIZE = 0x100000; + +static_assert (SOCU_BUFFERSIZE % SOCU_ALIGN == 0); + +bool s_socuActive = false; +u32 *s_socuBuffer = nullptr; + +C3D_Tex s_gfxTexture; +Tex3DS_Texture s_gfxT3x; + +void startNetwork () +{ + if (s_socuActive) + return; + + std::uint32_t wifi = 0; + if (R_FAILED (ACU_GetWifiStatus (&wifi)) || !wifi) + return; + + if (!s_socuBuffer) + s_socuBuffer = static_cast (::memalign (SOCU_ALIGN, SOCU_BUFFERSIZE)); + + if (!s_socuBuffer) + return; + + if (R_FAILED (socInit (s_socuBuffer, SOCU_BUFFERSIZE))) + return; + + s_socuActive = true; + Log::info ("Wifi connected\n"); +} + +void drawLogo () +{ + auto subTex = Tex3DS_GetSubTexture (s_gfxT3x, gfx_c3dlogo_idx); + + ImGuiIO &io = ImGui::GetIO (); + auto const screenWidth = io.DisplaySize.x; + auto const screenHeight = io.DisplaySize.y; + auto const logoWidth = subTex->width / io.DisplayFramebufferScale.x; + auto const logoHeight = subTex->height / io.DisplayFramebufferScale.y; + + auto const x1 = (screenWidth - logoWidth) / 2.0f; + auto const x2 = x1 + logoWidth; + auto const y1 = (screenHeight / 2.0f - logoHeight) / 2.0f; + auto const y2 = y1 + logoHeight; + + auto const uv1 = ImVec2 (subTex->left, subTex->top); + auto const uv2 = ImVec2 (subTex->right, subTex->bottom); + + ImGui::GetBackgroundDrawList ()->AddImage ( + &s_gfxTexture, ImVec2 (x1, y1), ImVec2 (x2, y2), uv1, uv2); + + ImGui::GetBackgroundDrawList ()->AddImage (&s_gfxTexture, + ImVec2 (x1, y1 + screenHeight / 2.0f), + ImVec2 (x2, y2 + screenHeight / 2.0f), + uv1, + uv2); +} + +void drawStatus () +{ + constexpr unsigned batteryLevels[] = { + gfx_battery0_idx, + gfx_battery0_idx, + gfx_battery1_idx, + gfx_battery2_idx, + gfx_battery3_idx, + gfx_battery4_idx, + }; + + constexpr unsigned wifiLevels[] = { + gfx_wifiNull_idx, + gfx_wifi1_idx, + gfx_wifi2_idx, + gfx_wifi3_idx, + }; + + static u8 charging = 0; + static u8 level = 5; + PTMU_GetBatteryChargeState (&charging); + if (!charging) + { + PTMU_GetBatteryLevel (&level); + if (level >= std::extent_v) + svcBreak (USERBREAK_PANIC); + } + + auto const &io = ImGui::GetIO (); + auto const &style = ImGui::GetStyle (); + + auto const screenWidth = io.DisplaySize.x; + + auto const battery = + Tex3DS_GetSubTexture (s_gfxT3x, charging ? gfx_batteryCharge_idx : batteryLevels[level]); + auto const batteryWidth = battery->width / io.DisplayFramebufferScale.x; + auto const batteryHeight = battery->height / io.DisplayFramebufferScale.y; + + auto const p1 = ImVec2 (screenWidth - batteryWidth, 0.0f); + auto const p2 = ImVec2 (screenWidth, batteryHeight); + + auto const uv1 = ImVec2 (battery->left, battery->top); + auto const uv2 = ImVec2 (battery->right, battery->bottom); + + ImGui::GetForegroundDrawList ()->AddImage (&s_gfxTexture, p1, p2, uv1, uv2); + + auto const wifiStrength = osGetWifiStrength (); + + auto const wifi = Tex3DS_GetSubTexture (s_gfxT3x, wifiLevels[wifiStrength]); + auto const wifiWidth = wifi->width / io.DisplayFramebufferScale.x; + auto const wifiHeight = wifi->height / io.DisplayFramebufferScale.y; + + auto const p3 = ImVec2 (p1.x - wifiWidth - 4.0f, 0.0f); + auto const p4 = ImVec2 (p1.x - 4.0f, wifiHeight); + + auto const uv3 = ImVec2 (wifi->left, wifi->top); + auto const uv4 = ImVec2 (wifi->right, wifi->bottom); + + ImGui::GetForegroundDrawList ()->AddImage (&s_gfxTexture, p3, p4, uv3, uv4); + + char buffer[64]; + auto const now = std::time (nullptr); + std::strftime (buffer, sizeof (buffer), "%H:%M:%S", std::localtime (&now)); + ImGui::GetForegroundDrawList ()->AddText ( + ImVec2 (p3.x - 130.0f, style.FramePadding.y), 0xFFFFFFFF, buffer); +} +} + +bool platform::init () +{ + osSetSpeedupEnable (true); + + acInit (); + ptmuInit (); + romfsInit (); + gfxInitDefault (); + gfxSet3D (false); + sdmcWriteSafe (false); + +#ifndef NDEBUG + consoleDebugInit (debugDevice_SVC); + std::setvbuf (stderr, nullptr, _IOLBF, 0); +#endif + + IMGUI_CHECKVERSION (); + ImGui::CreateContext (); + + if (!imgui::ctru::init ()) + { + ImGui::DestroyContext (); + return false; + } + + imgui::citro3d::init (); + + { + fs::File file; + if (!file.open ("romfs:/gfx.t3x")) + svcBreak (USERBREAK_PANIC); + + s_gfxT3x = Tex3DS_TextureImportStdio (file, &s_gfxTexture, nullptr, true); + if (!s_gfxT3x) + svcBreak (USERBREAK_PANIC); + } + + C3D_TexSetFilter (&s_gfxTexture, GPU_LINEAR, GPU_LINEAR); + + return true; +} + +bool platform::loop () +{ + if (!aptMainLoop ()) + return false; + + startNetwork (); + + hidScanInput (); + + if (hidKeysDown () & KEY_START) + return false; + + imgui::ctru::newFrame (); + imgui::citro3d::newFrame (); + ImGui::NewFrame (); + + return true; +} + +void platform::render () +{ + drawLogo (); + drawStatus (); + + ImGui::Render (); + + imgui::citro3d::render (); +} + +void platform::exit () +{ + Tex3DS_TextureFree (s_gfxT3x); + C3D_TexDelete (&s_gfxTexture); + + ImGui::DestroyContext (); + + imgui::citro3d::exit (); + imgui::ctru::exit (); + + socExit (); + s_socuActive = false; + std::free (s_socuBuffer); + + gfxExit (); + ptmuExit (); + acExit (); +} + +/////////////////////////////////////////////////////////////////////////// +class platform::Thread::privateData_t +{ +public: + privateData_t () + { + if (thread) + threadFree (thread); + } + + privateData_t (std::function func_) : thread (nullptr) + { + s32 priority = 0x30; + svcGetThreadPriority (&priority, CUR_THREAD_HANDLE); + + thread = threadCreate (&privateData_t::threadFunc, this, STACK_SIZE, priority, -1, false); + assert (thread); + } + + static void threadFunc (void *const arg_) + { + auto const t = static_cast (arg_); + t->func (); + } + + ::Thread thread = nullptr; + std::function func; +}; + +/////////////////////////////////////////////////////////////////////////// +platform::Thread::~Thread () = default; + +platform::Thread::Thread () : m_d (new privateData_t ()) +{ +} + +platform::Thread::Thread (std::function func_) : m_d (new privateData_t (func_)) +{ +} + +platform::Thread::Thread (Thread &&that_) : m_d (new privateData_t ()) +{ + std::swap (m_d, that_.m_d); +} + +platform::Thread &platform::Thread::operator= (Thread &&that_) +{ + std::swap (m_d, that_.m_d); + return *this; +} + +void platform::Thread::join () +{ + threadJoin (m_d->thread, UINT64_MAX); +} + +void platform::Thread::sleep (std::chrono::milliseconds const timeout_) +{ + svcSleepThread (std::chrono::nanoseconds (timeout_).count ()); +} + +/////////////////////////////////////////////////////////////////////////// +class platform::Mutex::privateData_t +{ +public: + LightLock mutex; +}; + +/////////////////////////////////////////////////////////////////////////// +platform::Mutex::~Mutex () = default; + +platform::Mutex::Mutex () : m_d (new privateData_t ()) +{ + LightLock_Init (&m_d->mutex); +} + +void platform::Mutex::lock () +{ + LightLock_Lock (&m_d->mutex); +} + +void platform::Mutex::unlock () +{ + LightLock_Unlock (&m_d->mutex); +} diff --git a/source/3ds/vshader.v.pica b/source/3ds/vshader.v.pica new file mode 100644 index 0000000..5b9955e --- /dev/null +++ b/source/3ds/vshader.v.pica @@ -0,0 +1,61 @@ +; ftpd is a server implementation based on the following: +; - RFC 959 (https://tools.ietf.org/html/rfc959) +; - RFC 3659 (https://tools.ietf.org/html/rfc3659) +; - suggested implementation details from https://cr.yp.to/ftp/filesystem.html +; +; Copyright (C) 2020 Michael Theall +; +; This program is free software: you can redistribute it and/or modify +; it under the terms of the GNU General Public License as published by +; the Free Software Foundation, either version 3 of the License, or +; (at your option) any later version. +; +; 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, see . + +; Example PICA200 vertex shader + +; Uniforms +.fvec proj[4] + +; Constants +.constf constants(1.0, 0.0, 0.00392156862745, 0.0) + +; Outputs +.out outPos position +.out outUv texcoord0 +.out outColor color + +; Inputs (defined as aliases for convenience) +.alias inPos v0 +.alias inUv v1 +.alias inColor v2 + +.proc main + ; Force inPos.z = 0.0, inPos.w = 1.0 + mov r0.xy, inPos.xy + mov r0.zw, constants.yx + + ; outPos = proj * inPos + dp4 outPos.x, proj[0], r0 + dp4 outPos.y, proj[1], r0 + dp4 outPos.z, proj[2], r0 + dp4 outPos.w, proj[3], r0 + + ; outUv = inUv + mov outUv, inUv + + ; normalize inColor + mul r1, constants.zzzz, inColor + + ; outColor = inColor + mov outColor, r1 + + ; We're finished + end +.end diff --git a/source/console.c b/source/console.c deleted file mode 100644 index 478beb1..0000000 --- a/source/console.c +++ /dev/null @@ -1,276 +0,0 @@ -#include "console.h" -#include -#include -#include -#include -#include -#include - -#ifdef _3DS -#include <3ds.h> -#define CONSOLE_WIDTH 50 -#define CONSOLE_HEIGHT 30 -#elif defined(__SWITCH__) -#include -#define CONSOLE_WIDTH 80 -#define CONSOLE_HEIGHT 45 -#endif - -#if defined(_3DS) || defined (__SWITCH__) -static PrintConsole status_console; -static PrintConsole main_console; -#endif - -#if ENABLE_LOGGING -static bool disable_logging = false; -#endif - -#if defined(_3DS) -static PrintConsole tcp_console; - -/*! initialize console subsystem */ -void -console_init(void) -{ - consoleInit(GFX_TOP, &status_console); - consoleSetWindow(&status_console, 0, 0, 50, 1); - - consoleInit(GFX_TOP, &main_console); - consoleSetWindow(&main_console, 0, 1, 50, 29); - - consoleInit(GFX_BOTTOM, &tcp_console); - - consoleSelect(&main_console); -} - - -/*! print tcp tables */ -static void -print_tcp_table(void) -{ - static SOCU_TCPTableEntry tcp_entries[32]; - socklen_t optlen; - size_t i; - int rc, lines = 0; - -#ifdef ENABLE_LOGGING - disable_logging = true; -#endif - - consoleSelect(&tcp_console); - console_print("\x1b[0;0H\x1b[K"); - optlen = sizeof(tcp_entries); - rc = SOCU_GetNetworkOpt(SOL_CONFIG, NETOPT_TCP_TABLE, tcp_entries, &optlen); - if(rc != 0 && errno != ENODEV) - console_print(RED "tcp table: %d %s\n\x1b[J\n" RESET, errno, strerror(errno)); - else if(rc == 0) - { - for(i = 0; lines < 30 && i < optlen / sizeof(SOCU_TCPTableEntry); ++i) - { - SOCU_TCPTableEntry *entry = &tcp_entries[i]; - struct sockaddr_in *local = (struct sockaddr_in*)&entry->local; - struct sockaddr_in *remote = (struct sockaddr_in*)&entry->remote; - - console_print(GREEN "%stcp[%zu]: ", i == 0 ? "" : "\n", i); - switch(entry->state) - { - case TCP_STATE_CLOSED: - console_print("CLOSED\x1b[K"); - local = remote = NULL; - break; - - case TCP_STATE_LISTEN: - console_print("LISTEN\x1b[K"); - remote = NULL; - break; - - case TCP_STATE_ESTABLISHED: - console_print("ESTABLISHED\x1b[K"); - break; - - case TCP_STATE_FINWAIT1: - console_print("FINWAIT1\x1b[K"); - break; - - case TCP_STATE_FINWAIT2: - console_print("FINWAIT2\x1b[K"); - break; - - case TCP_STATE_CLOSE_WAIT: - console_print("CLOSE_WAIT\x1b[K"); - break; - - case TCP_STATE_LAST_ACK: - console_print("LAST_ACK\x1b[K"); - break; - - case TCP_STATE_TIME_WAIT: - console_print("TIME_WAIT\x1b[K"); - break; - - default: - console_print("State %lu\x1b[K", entry->state); - break; - } - - ++lines; - - if(local && (lines++ < 30)) - console_print("\n Local %s:%u\x1b[K", inet_ntoa(local->sin_addr), - ntohs(local->sin_port)); - - if(remote && (lines++ < 30)) - console_print("\n Peer %s:%u\x1b[K", inet_ntoa(remote->sin_addr), - ntohs(remote->sin_port)); - } - - console_print(RESET "\x1b[J"); - } - else - console_print("\x1b[2J"); - - consoleSelect(&main_console); - -#ifdef ENABLE_LOGGING - disable_logging = false; -#endif -} - -#elif defined(__SWITCH__) -/*! initialize console subsystem */ -void -console_init(void) -{ - consoleInit(&status_console); - consoleSetWindow(&status_console, 0, 0, CONSOLE_WIDTH, 1); - - consoleInit( &main_console); - consoleSetWindow(&main_console, 0, 1, CONSOLE_WIDTH, CONSOLE_HEIGHT-1); - - consoleSelect(&main_console); -} -#endif - -#if defined(_3DS) || defined(__SWITCH__) - - -/*! set status bar contents - * - * @param[in] fmt format string - * @param[in] ... format arguments - */ -void -console_set_status(const char *fmt, ...) -{ - va_list ap; - - consoleSelect(&status_console); - va_start(ap, fmt); - vprintf(fmt, ap); -#ifdef ENABLE_LOGGING - vfprintf(stderr, fmt, ap); -#endif - va_end(ap); - consoleSelect(&main_console); -} - -/*! add text to the console - * - * @param[in] fmt format string - * @param[in] ... format arguments - */ -void -console_print(const char *fmt, ...) -{ - va_list ap; - - va_start(ap, fmt); - vprintf(fmt, ap); -#ifdef ENABLE_LOGGING - if(!disable_logging) - vfprintf(stderr, fmt, ap); -#endif - va_end(ap); -} - -/*! print debug message - * - * @param[in] fmt format string - * @param[in] ... format arguments - */ -void -debug_print(const char *fmt, ...) -{ -#ifdef ENABLE_LOGGING - va_list ap; - - va_start(ap, fmt); - vfprintf(stderr, fmt, ap); - va_end(ap); -#endif -} - -/*! draw console to screen */ -void -console_render(void) -{ - /* print tcp table */ -#ifdef _3DS - print_tcp_table(); -#endif - /* flush framebuffer */ -#ifdef _3DS - gfxFlushBuffers(); - gspWaitForVBlank(); - gfxSwapBuffers(); -#endif -#ifdef __SWITCH__ - consoleUpdate(NULL); -#endif -} - - -#else - -/* this is a lot easier when you have a real console */ - -void -console_init(void) -{ -} - -void -console_set_status(const char *fmt, ...) -{ - va_list ap; - va_start(ap, fmt); - vprintf(fmt, ap); - va_end(ap); - fputc('\n', stdout); -} - -void -console_print(const char *fmt, ...) -{ - va_list ap; - va_start(ap, fmt); - vprintf(fmt, ap); - va_end(ap); -} - -void -debug_print(const char *fmt, ...) -{ -#ifdef ENABLE_LOGGING - va_list ap; - va_start(ap, fmt); - vfprintf(stderr, fmt, ap); - va_end(ap); -#endif -} - -void console_render(void) -{ -} -#endif - diff --git a/source/fs.cpp b/source/fs.cpp new file mode 100644 index 0000000..43c4c61 --- /dev/null +++ b/source/fs.cpp @@ -0,0 +1,211 @@ +// ftpd is a server implementation based on the following: +// - RFC 959 (https://tools.ietf.org/html/rfc959) +// - RFC 3659 (https://tools.ietf.org/html/rfc3659) +// - suggested implementation details from https://cr.yp.to/ftp/filesystem.html +// +// Copyright (C) 2020 Michael Theall +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// 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, see . + +#include "fs.h" + +#include +#include + +std::string fs::printSize (std::uint64_t const size_) +{ + constexpr std::uint64_t const KiB = 1024; + constexpr std::uint64_t const MiB = 1024 * KiB; + constexpr std::uint64_t const GiB = 1024 * MiB; + constexpr std::uint64_t const TiB = 1024 * GiB; + constexpr std::uint64_t const PiB = 1024 * TiB; + constexpr std::uint64_t const EiB = 1024 * PiB; + + char buffer[64] = {}; + + for (auto const &[name, bin] : { + // clang-format off + std::make_pair ("EiB", EiB), + std::make_pair ("PiB", PiB), + std::make_pair ("TiB", TiB), + std::make_pair ("GiB", GiB), + std::make_pair ("MiB", MiB), + std::make_pair ("KiB", KiB) + // clang-format on + }) + { + auto const whole = size_ / bin; + if (size_ >= 100 * bin) + { + std::sprintf (buffer, "%" PRIu64 "%s", whole, name); + return buffer; + } + + auto const frac = size_ - whole * bin; + if (size_ >= 10 * bin) + { + std::sprintf (buffer, "%" PRIu64 ".%" PRIu64 "%s", whole, frac * 10 / bin, name); + return buffer; + } + + if (size_ >= 1000 * (bin / KiB)) + { + std::sprintf (buffer, "%" PRIu64 ".%02" PRIu64 "%s", whole, frac * 100 / bin, name); + return buffer; + } + } + + std::sprintf (buffer, "%" PRIu64, size_); + return buffer; +} + +/////////////////////////////////////////////////////////////////////////// +fs::File::~File () = default; + +fs::File::File () = default; + +fs::File::File (File &&that_) = default; + +fs::File &fs::File::operator= (File &&that_) = default; + +fs::File::operator bool () const +{ + return static_cast (m_fp); +} + +fs::File::operator FILE * () const +{ + return m_fp.get (); +} + +void fs::File::setBufferSize (std::size_t const size_) +{ + if (m_bufferSize != size_) + { + m_buffer = std::make_unique (size_); + m_bufferSize = size_; + } + + if (m_fp) + std::setvbuf (m_fp.get (), m_buffer.get (), _IOFBF, m_bufferSize); +} + +bool fs::File::open (char const *const path_, char const *const mode_) +{ + auto const fp = std::fopen (path_, mode_); + if (!fp) + return false; + + m_fp = std::unique_ptr (fp, &std::fclose); + + if (m_buffer) + std::setvbuf (m_fp.get (), m_buffer.get (), _IOFBF, m_bufferSize); + + return true; +} + +void fs::File::close () +{ + m_fp.reset (); +} + +ssize_t fs::File::seek (std::size_t const pos_, int const origin_) +{ + return std::fseek (m_fp.get (), pos_, origin_); +} + +ssize_t fs::File::read (void *const data_, std::size_t const size_) +{ + return std::fread (data_, 1, size_, m_fp.get ()); +} + +bool fs::File::readAll (void *const data_, std::size_t const size_) +{ + auto p = static_cast (data_); + std::size_t bytes = 0; + + while (bytes < size_) + { + auto const rc = read (p, size_ - bytes); + if (rc <= 0) + return false; + + p += rc; + bytes += rc; + } + + return true; +} + +ssize_t fs::File::write (void const *const data_, std::size_t const size_) +{ + return std::fwrite (data_, 1, size_, m_fp.get ()); +} + +bool fs::File::writeAll (void const *const data_, std::size_t const size_) +{ + auto p = static_cast (data_); + std::size_t bytes = 0; + + while (bytes < size_) + { + auto const rc = write (p, size_ - bytes); + if (rc <= 0) + return false; + + p += rc; + bytes += rc; + } + + return true; +} + +/////////////////////////////////////////////////////////////////////////// +fs::Dir::~Dir () = default; + +fs::Dir::Dir () = default; + +fs::Dir::Dir (Dir &&that_) = default; + +fs::Dir &fs::Dir::operator= (Dir &&that_) = default; + +fs::Dir::operator bool () const +{ + return static_cast (m_dp); +} + +fs::Dir::operator DIR * () const +{ + return m_dp.get (); +} + +bool fs::Dir::open (char const *const path_) +{ + auto const dp = ::opendir (path_); + if (!dp) + return false; + + m_dp = std::unique_ptr (dp, &::closedir); + return true; +} + +void fs::Dir::close () +{ + m_dp.reset (); +} + +struct dirent *fs::Dir::read () +{ + return ::readdir (m_dp.get ()); +} diff --git a/source/ftp.c b/source/ftp.c deleted file mode 100644 index 4778095..0000000 --- a/source/ftp.c +++ /dev/null @@ -1,4114 +0,0 @@ -/* This FTP server implementation is based on RFC 959, - * (https://tools.ietf.org/html/rfc959), RFC 3659 - * (https://tools.ietf.org/html/rfc3659) and suggested implementation details - * from https://cr.yp.to/ftp/filesystem.html - */ -#include "ftp.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#ifdef _3DS -#include <3ds.h> -#define lstat stat -#elif defined(__SWITCH__) -#include -#define lstat stat -#else -#include -#define BIT(x) (1<<(x)) -#endif -#include "console.h" - -#define POLL_UNKNOWN (~(POLLIN|POLLPRI|POLLOUT)) - -#ifndef __SWITCH__ -#define XFER_BUFFERSIZE 32768 -#define SOCK_BUFFERSIZE 32768 -#define FILE_BUFFERSIZE 65536 -#define CMD_BUFFERSIZE 4096 -#else -/* we have a lot of memory to waste on the Switch */ -#define XFER_BUFFERSIZE 65536 -#define SOCK_BUFFERSIZE 65536 -#define FILE_BUFFERSIZE 1048576 -#define CMD_BUFFERSIZE 4096 -#endif - -#ifdef _3DS -#define SOCU_ALIGN 0x1000 -#define SOCU_BUFFERSIZE 0x100000 -#endif -#define LISTEN_PORT 5000 -#ifdef _3DS -#define DATA_PORT (LISTEN_PORT+1) -#else -#define DATA_PORT 0 /* ephemeral port */ -#endif - -typedef struct ftp_session_t ftp_session_t; - -#define FTP_DECLARE(x) static void x(ftp_session_t *session, const char *args) -FTP_DECLARE(ABOR); -FTP_DECLARE(ALLO); -FTP_DECLARE(APPE); -FTP_DECLARE(CDUP); -FTP_DECLARE(CWD); -FTP_DECLARE(DELE); -FTP_DECLARE(FEAT); -FTP_DECLARE(HELP); -FTP_DECLARE(LIST); -FTP_DECLARE(MDTM); -FTP_DECLARE(MKD); -FTP_DECLARE(MLSD); -FTP_DECLARE(MLST); -FTP_DECLARE(MODE); -FTP_DECLARE(NLST); -FTP_DECLARE(NOOP); -FTP_DECLARE(OPTS); -FTP_DECLARE(PASS); -FTP_DECLARE(PASV); -FTP_DECLARE(PORT); -FTP_DECLARE(PWD); -FTP_DECLARE(QUIT); -FTP_DECLARE(REST); -FTP_DECLARE(RETR); -FTP_DECLARE(RMD); -FTP_DECLARE(RNFR); -FTP_DECLARE(RNTO); -FTP_DECLARE(SIZE); -FTP_DECLARE(STAT); -FTP_DECLARE(STOR); -FTP_DECLARE(STOU); -FTP_DECLARE(STRU); -FTP_DECLARE(SYST); -FTP_DECLARE(TYPE); -FTP_DECLARE(USER); - -/*! session state */ -typedef enum -{ - COMMAND_STATE, /*!< waiting for a command */ - DATA_CONNECT_STATE, /*!< waiting for connection after PASV command */ - DATA_TRANSFER_STATE, /*!< data transfer in progress */ -} session_state_t; - -/*! ftp_session_set_state flags */ -typedef enum -{ - CLOSE_PASV = BIT(0), /*!< Close the pasv_fd */ - CLOSE_DATA = BIT(1), /*!< Close the data_fd */ -} set_state_flags_t; - -/*! ftp_session_t flags */ -typedef enum -{ - SESSION_BINARY = BIT(0), /*!< data transfers in binary mode */ - SESSION_PASV = BIT(1), /*!< have pasv_addr ready for data transfer command */ - SESSION_PORT = BIT(2), /*!< have peer_addr ready for data transfer command */ - SESSION_RECV = BIT(3), /*!< data transfer in source mode */ - SESSION_SEND = BIT(4), /*!< data transfer in sink mode */ - SESSION_RENAME = BIT(5), /*!< last command was RNFR and buffer contains path */ - SESSION_URGENT = BIT(6), /*!< in telnet urgent mode */ -} session_flags_t; - -/*! ftp_xfer_dir mode */ -typedef enum -{ - XFER_DIR_LIST, /*!< Long list */ - XFER_DIR_MLSD, /*!< Machine list directory */ - XFER_DIR_MLST, /*!< Machine list */ - XFER_DIR_NLST, /*!< Short list */ - XFER_DIR_STAT, /*!< Stat command */ -} xfer_dir_mode_t; - -typedef enum -{ - SESSION_MLST_TYPE = BIT(0), - SESSION_MLST_SIZE = BIT(1), - SESSION_MLST_MODIFY = BIT(2), - SESSION_MLST_PERM = BIT(3), - SESSION_MLST_UNIX_MODE = BIT(4), -} session_mlst_flags_t; - -/*! ftp session */ -struct ftp_session_t -{ - char cwd[4096]; /*!< current working directory */ - char lwd[4096]; /*!< list working directory */ - struct sockaddr_in peer_addr; /*!< peer address for data connection */ - struct sockaddr_in pasv_addr; /*!< listen address for PASV connection */ - int cmd_fd; /*!< socket for command connection */ - int pasv_fd; /*!< listen socket for PASV */ - int data_fd; /*!< socket for data transfer */ - time_t timestamp; /*!< time from last command */ - session_flags_t flags; /*!< session flags */ - xfer_dir_mode_t dir_mode; /*!< dir transfer mode */ - session_mlst_flags_t mlst_flags; /*!< session MLST flags */ - session_state_t state; /*!< session state */ - ftp_session_t *next; /*!< link to next session */ - ftp_session_t *prev; /*!< link to prev session */ - - loop_status_t (*transfer)(ftp_session_t*); /*! data transfer callback */ - char buffer[XFER_BUFFERSIZE]; /*! persistent data between callbacks */ - char file_buffer[FILE_BUFFERSIZE]; /*! stdio file buffer */ - char cmd_buffer[CMD_BUFFERSIZE]; /*! command buffer */ - size_t bufferpos; /*! persistent buffer position between callbacks */ - size_t buffersize; /*! persistent buffer size between callbacks */ - size_t cmd_buffersize; - uint64_t filepos; /*! persistent file position between callbacks */ - uint64_t filesize; /*! persistent file size between callbacks */ - FILE *fp; /*! persistent open file pointer between callbacks */ - DIR *dp; /*! persistent open directory pointer between callbacks */ -}; - -/*! ftp command descriptor */ -typedef struct ftp_command -{ - const char *name; /*!< command name */ - void (*handler)(ftp_session_t*, const char*); /*!< command callback */ -} ftp_command_t; - -/*! ftp command list */ -static ftp_command_t ftp_commands[] = -{ -/*! ftp command */ -#define FTP_COMMAND(x) { #x, x, } -/*! ftp alias */ -#define FTP_ALIAS(x,y) { #x, y, } - FTP_COMMAND(ABOR), - FTP_COMMAND(ALLO), - FTP_COMMAND(APPE), - FTP_COMMAND(CDUP), - FTP_COMMAND(CWD), - FTP_COMMAND(DELE), - FTP_COMMAND(FEAT), - FTP_COMMAND(HELP), - FTP_COMMAND(LIST), - FTP_COMMAND(MDTM), - FTP_COMMAND(MKD), - FTP_COMMAND(MLSD), - FTP_COMMAND(MLST), - FTP_COMMAND(MODE), - FTP_COMMAND(NLST), - FTP_COMMAND(NOOP), - FTP_COMMAND(OPTS), - FTP_COMMAND(PASS), - FTP_COMMAND(PASV), - FTP_COMMAND(PORT), - FTP_COMMAND(PWD), - FTP_COMMAND(QUIT), - FTP_COMMAND(REST), - FTP_COMMAND(RETR), - FTP_COMMAND(RMD), - FTP_COMMAND(RNFR), - FTP_COMMAND(RNTO), - FTP_COMMAND(SIZE), - FTP_COMMAND(STAT), - FTP_COMMAND(STOR), - FTP_COMMAND(STOU), - FTP_COMMAND(STRU), - FTP_COMMAND(SYST), - FTP_COMMAND(TYPE), - FTP_COMMAND(USER), - FTP_ALIAS(XCUP, CDUP), - FTP_ALIAS(XCWD, CWD), - FTP_ALIAS(XMKD, MKD), - FTP_ALIAS(XPWD, PWD), - FTP_ALIAS(XRMD, RMD), -}; -/*! number of ftp commands */ -static const size_t num_ftp_commands = sizeof(ftp_commands)/sizeof(ftp_commands[0]); - -static void update_free_space(void); - -/*! compare ftp command descriptors - * - * @param[in] p1 left side of comparison (ftp_command_t*) - * @param[in] p2 right side of comparison (ftp_command_t*) - * - * @returns <0 if p1 < p2 - * @returns 0 if p1 == p2 - * @returns >0 if p1 > p2 - */ -static int -ftp_command_cmp(const void *p1, - const void *p2) -{ - ftp_command_t *c1 = (ftp_command_t*)p1; - ftp_command_t *c2 = (ftp_command_t*)p2; - - /* ordered by command name */ - return strcasecmp(c1->name, c2->name); -} - -#ifdef _3DS -/*! SOC service buffer */ -static u32 *SOCU_buffer = NULL; - -/*! Whether LCD is powered */ -static bool lcd_power = true; - -/*! aptHook cookie */ -static aptHookCookie cookie; -#elif defined(__SWITCH__) - -/*! appletHook cookie */ -static AppletHookCookie cookie; -#endif - -/*! server listen address */ -static struct sockaddr_in serv_addr; -/*! listen file descriptor */ -static int listenfd = -1; -#ifdef _3DS -/*! current data port */ -static in_port_t data_port = DATA_PORT; -#endif -/*! list of ftp sessions */ -static ftp_session_t *sessions = NULL; -/*! socket buffersize */ -static int sock_buffersize = SOCK_BUFFERSIZE; -/*! server start time */ -static time_t start_time = 0; - -/*! Allocate a new data port - * - * @returns next data port - */ -static in_port_t -next_data_port(void) -{ -#ifdef _3DS - if(++data_port >= 10000) - data_port = DATA_PORT; - return data_port; -#else - return 0; /* ephemeral port */ -#endif -} - -/*! set a socket to non-blocking - * - * @param[in] fd socket - * - * @returns error - */ -static int -ftp_set_socket_nonblocking(int fd) -{ - int rc, flags; - - /* get the socket flags */ - flags = fcntl(fd, F_GETFL, 0); - if(flags == -1) - { - console_print(RED "fcntl: %d %s\n" RESET, errno, strerror(errno)); - return -1; - } - - /* add O_NONBLOCK to the socket flags */ - rc = fcntl(fd, F_SETFL, flags | O_NONBLOCK); - if(rc != 0) - { - console_print(RED "fcntl: %d %s\n" RESET, errno, strerror(errno)); - return -1; - } - - return 0; -} - -/*! set socket options - * - * @param[in] fd socket - * - * @returns failure - */ -static int -ftp_set_socket_options(int fd) -{ - int rc; - - /* increase receive buffer size */ - rc = setsockopt(fd, SOL_SOCKET, SO_RCVBUF, - &sock_buffersize, sizeof(sock_buffersize)); - if(rc != 0) - { - console_print(RED "setsockopt: SO_RCVBUF %d %s\n" RESET, errno, strerror(errno)); - return -1; - } - - /* increase send buffer size */ - rc = setsockopt(fd, SOL_SOCKET, SO_SNDBUF, - &sock_buffersize, sizeof(sock_buffersize)); - if(rc != 0) - { - console_print(RED "setsockopt: SO_SNDBUF %d %s\n" RESET, errno, strerror(errno)); - return -1; - } - - return 0; -} - -/*! close a socket - * - * @param[in] fd socket to close - * @param[in] connected whether this socket is connected - */ -static void -ftp_closesocket(int fd, - bool connected) -{ - int rc; - struct sockaddr_in addr; - socklen_t addrlen = sizeof(addr); - struct pollfd pollinfo; - -// console_print("0x%X\n", socketGetLastBsdResult()); - - if(connected) - { - /* get peer address and print */ - rc = getpeername(fd, (struct sockaddr*)&addr, &addrlen); - if(rc != 0) - { - console_print(RED "getpeername: %d %s\n" RESET, errno, strerror(errno)); - console_print(YELLOW "closing connection to fd=%d\n" RESET, fd); - } - else - console_print(YELLOW "closing connection to %s:%u\n" RESET, - inet_ntoa(addr.sin_addr), ntohs(addr.sin_port)); - - /* shutdown connection */ - rc = shutdown(fd, SHUT_WR); - if(rc != 0) - console_print(RED "shutdown: %d %s\n" RESET, errno, strerror(errno)); - - /* wait for client to close connection */ - pollinfo.fd = fd; - pollinfo.events = POLLIN; - pollinfo.revents = 0; - rc = poll(&pollinfo, 1, 250); - if(rc < 0) - console_print(RED "poll: %d %s\n" RESET, errno, strerror(errno)); - } - - /* set linger to 0 */ - struct linger linger; - linger.l_onoff = 1; - linger.l_linger = 0; - rc = setsockopt(fd, SOL_SOCKET, SO_LINGER, - &linger, sizeof(linger)); - if(rc != 0) - console_print(RED "setsockopt: SO_LINGER %d %s\n" RESET, - errno, strerror(errno)); - - /* close socket */ - rc = close(fd); - if(rc != 0) - console_print(RED "close: %d %s\n" RESET, errno, strerror(errno)); -} - -/*! close command socket on ftp session - * - * @param[in] session ftp session - */ -static void -ftp_session_close_cmd(ftp_session_t *session) -{ - /* close command socket */ - if(session->cmd_fd >= 0) - ftp_closesocket(session->cmd_fd, true); - session->cmd_fd = -1; -} - -/*! close listen socket on ftp session - * - * @param[in] session ftp session - */ -static void -ftp_session_close_pasv(ftp_session_t *session) -{ - /* close pasv socket */ - if(session->pasv_fd >= 0) - { - console_print(YELLOW "stop listening on %s:%u\n" RESET, - inet_ntoa(session->pasv_addr.sin_addr), - ntohs(session->pasv_addr.sin_port)); - - ftp_closesocket(session->pasv_fd, false); - } - - session->pasv_fd = -1; -} - -/*! close data socket on ftp session - * - * @param[in] session ftp session - */ -static void -ftp_session_close_data(ftp_session_t *session) -{ - /* close data connection */ - if(session->data_fd >= 0 && session->data_fd != session->cmd_fd) - ftp_closesocket(session->data_fd, true); - session->data_fd = -1; - - /* clear send/recv flags */ - session->flags &= ~(SESSION_RECV|SESSION_SEND); -} - -/*! close open file for ftp session - * - * @param[in] session ftp session - */ -static void -ftp_session_close_file(ftp_session_t *session) -{ - int rc; - - if(session->fp != NULL) - { - rc = fclose(session->fp); - if(rc != 0) - console_print(RED "fclose: %d %s\n" RESET, errno, strerror(errno)); - } - - session->fp = NULL; - session->filepos = 0; -} - -/*! open file for reading for ftp session - * - * @param[in] session ftp session - * - * @returns -1 for error - */ -static int -ftp_session_open_file_read(ftp_session_t *session) -{ - int rc; - struct stat st; - - /* open file in read mode */ - session->fp = fopen(session->buffer, "rb"); - if(session->fp == NULL) - { - console_print(RED "fopen '%s': %d %s\n" RESET, session->buffer, errno, strerror(errno)); - return -1; - } - - /* it's okay if this fails */ - errno = 0; - rc = setvbuf(session->fp, session->file_buffer, _IOFBF, FILE_BUFFERSIZE); - if(rc != 0) - { - console_print(RED "setvbuf: %d %s\n" RESET, errno, strerror(errno)); - } - - /* get the file size */ - rc = fstat(fileno(session->fp), &st); - if(rc != 0) - { - console_print(RED "fstat '%s': %d %s\n" RESET, session->buffer, errno, strerror(errno)); - return -1; - } - session->filesize = st.st_size; - - if(session->filepos != 0) - { - rc = fseek(session->fp, session->filepos, SEEK_SET); - if(rc != 0) - { - console_print(RED "fseek '%s': %d %s\n" RESET, session->buffer, errno, strerror(errno)); - return -1; - } - } - - return 0; -} - -/*! read from an open file for ftp session - * - * @param[in] session ftp session - * - * @returns bytes read - */ -static ssize_t -ftp_session_read_file(ftp_session_t *session) -{ - ssize_t rc; - - /* read file at current position */ - rc = fread(session->buffer, 1, sizeof(session->buffer), session->fp); - if(rc < 0) - { - console_print(RED "fread: %d %s\n" RESET, errno, strerror(errno)); - return -1; - } - - /* adjust file position */ - session->filepos += rc; - - return rc; -} - -/*! open file for writing for ftp session - * - * @param[in] session ftp session - * @param[in] append whether to append - * - * @returns -1 for error - * - * @note truncates file - */ -static int -ftp_session_open_file_write(ftp_session_t *session, - bool append) -{ - int rc; - const char *mode = "wb"; - - if(append) - mode = "ab"; - else if(session->filepos != 0) - mode = "r+b"; - - /* open file in write mode */ - session->fp = fopen(session->buffer, mode); - if(session->fp == NULL) - { - console_print(RED "fopen '%s': %d %s\n" RESET, session->buffer, errno, strerror(errno)); - return -1; - } - - update_free_space(); - - /* it's okay if this fails */ - errno = 0; - rc = setvbuf(session->fp, session->file_buffer, _IOFBF, FILE_BUFFERSIZE); - if(rc != 0) - { - console_print(RED "setvbuf: %d %s\n" RESET, errno, strerror(errno)); - } - - /* check if this had REST but not APPE */ - if(session->filepos != 0 && !append) - { - /* seek to the REST offset */ - rc = fseek(session->fp, session->filepos, SEEK_SET); - if(rc != 0) - { - console_print(RED "fseek '%s': %d %s\n" RESET, session->buffer, errno, strerror(errno)); - return -1; - } - } - - return 0; -} - -/*! write to an open file for ftp session - * - * @param[in] session ftp session - * - * @returns bytes written - */ -static ssize_t -ftp_session_write_file(ftp_session_t *session) -{ - ssize_t rc; - - /* write to file at current position */ - rc = fwrite(session->buffer + session->bufferpos, - 1, session->buffersize - session->bufferpos, - session->fp); - if(rc < 0) - { - console_print(RED "fwrite: %d %s\n" RESET, errno, strerror(errno)); - return -1; - } - else if(rc == 0) - console_print(RED "fwrite: wrote 0 bytes\n" RESET); - - /* adjust file position */ - session->filepos += rc; - - update_free_space(); - return rc; -} - -/*! close current working directory for ftp session - * - * @param[in] session ftp session - */ -static void -ftp_session_close_cwd(ftp_session_t *session) -{ - int rc; - - /* close open directory pointer */ - if(session->dp != NULL) - { - rc = closedir(session->dp); - if(rc != 0) - console_print(RED "closedir: %d %s\n" RESET, errno, strerror(errno)); - } - session->dp = NULL; -} - -/*! open current working directory for ftp session - * - * @param[in] session ftp session - * - * @return -1 for failure - */ -static int -ftp_session_open_cwd(ftp_session_t *session) -{ - /* open current working directory */ - session->dp = opendir(session->cwd); - if(session->dp == NULL) - { - console_print(RED "opendir '%s': %d %s\n" RESET, session->cwd, errno, strerror(errno)); - return -1; - } - - return 0; -} - -/*! set state for ftp session - * - * @param[in] session ftp session - * @param[in] state state to set - * @param[in] flags flags - */ -static void -ftp_session_set_state(ftp_session_t *session, - session_state_t state, - set_state_flags_t flags) -{ - session->state = state; - - /* close pasv and data sockets */ - if(flags & CLOSE_PASV) - ftp_session_close_pasv(session); - if(flags & CLOSE_DATA) - ftp_session_close_data(session); - - if(state == COMMAND_STATE) - { - /* close file/cwd */ - ftp_session_close_file(session); - ftp_session_close_cwd(session); - } -} - -/*! fill directory entry - * - * @param[in] session ftp session - * @param[in] st stat data - * @param[in] path path to fill - * @param[in] len path length - * @param[in] type type fact - * - * @returns errno - */ -static int -ftp_session_fill_dirent_type(ftp_session_t *session, const struct stat *st, - const char *path, size_t len, const char *type) -{ - session->buffersize = 0; - - if(session->dir_mode == XFER_DIR_MLSD - || session->dir_mode == XFER_DIR_MLST) - { - if(session->dir_mode == XFER_DIR_MLST) - session->buffer[session->buffersize++] = ' '; - - if(session->mlst_flags & SESSION_MLST_TYPE) - { - /* type fact */ - if(!type) - { - type = "???"; - if(S_ISREG(st->st_mode)) - type = "file"; - else if(S_ISDIR(st->st_mode)) - type = "dir"; -#if !defined(_3DS) && !defined(__SWITCH__) - else if(S_ISLNK(st->st_mode)) - type = "os.unix=symlink"; - else if(S_ISCHR(st->st_mode)) - type = "os.unix=character"; - else if(S_ISBLK(st->st_mode)) - type = "os.unix=block"; - else if(S_ISFIFO(st->st_mode)) - type = "os.unix=fifo"; - else if(S_ISSOCK(st->st_mode)) - type = "os.unix=socket"; -#endif - } - - session->buffersize += - sprintf(session->buffer + session->buffersize, "Type=%s;", type); - } - - if(session->mlst_flags & SESSION_MLST_SIZE) - { - /* size fact */ - session->buffersize += - sprintf(session->buffer + session->buffersize, "Size=%lld;", - (signed long long)st->st_size); - } - - if(session->mlst_flags & SESSION_MLST_MODIFY) - { - /* mtime fact */ - struct tm *tm = gmtime(&st->st_mtime); - if(tm == NULL) - return errno; - - session->buffersize += - strftime(session->buffer + session->buffersize, - sizeof(session->buffer) - session->buffersize, - "Modify=%Y%m%d%H%M%S;", tm); - if(session->buffersize == 0) - return EOVERFLOW; - } - - if(session->mlst_flags & SESSION_MLST_PERM) - { - /* permission fact */ - strcpy(session->buffer + session->buffersize, "Perm="); - session->buffersize += strlen("Perm="); - - /* append permission */ - if(S_ISREG(st->st_mode) && (st->st_mode & S_IWUSR)) - session->buffer[session->buffersize++] = 'a'; - - /* create permission */ - if(S_ISDIR(st->st_mode) && (st->st_mode & S_IWUSR)) - session->buffer[session->buffersize++] = 'c'; - - /* delete permission */ - session->buffer[session->buffersize++] = 'd'; - - /* chdir permission */ - if(S_ISDIR(st->st_mode) && (st->st_mode & S_IXUSR)) - session->buffer[session->buffersize++] = 'e'; - - /* rename permission */ - session->buffer[session->buffersize++] = 'f'; - - /* list permission */ - if(S_ISDIR(st->st_mode) && (st->st_mode & S_IRUSR)) - session->buffer[session->buffersize++] = 'l'; - - /* mkdir permission */ - if(S_ISDIR(st->st_mode) && (st->st_mode & S_IWUSR)) - session->buffer[session->buffersize++] = 'm'; - - /* delete permission */ - if(S_ISDIR(st->st_mode) && (st->st_mode & S_IWUSR)) - session->buffer[session->buffersize++] = 'p'; - - /* read permission */ - if(S_ISREG(st->st_mode) && (st->st_mode & S_IRUSR)) - session->buffer[session->buffersize++] = 'r'; - - /* write permission */ - if(S_ISREG(st->st_mode) && (st->st_mode & S_IWUSR)) - session->buffer[session->buffersize++] = 'w'; - - session->buffer[session->buffersize++] = ';'; - } - - if(session->mlst_flags & SESSION_MLST_UNIX_MODE) - { - /* unix mode fact */ - mode_t mask = S_IRWXU | S_IRWXG | S_IRWXO | S_ISVTX | S_ISGID | S_ISUID; - session->buffersize += - sprintf(session->buffer + session->buffersize, "UNIX.mode=0%lo;", - (unsigned long)(st->st_mode & mask)); - } - - /* make sure space precedes name */ - if(session->buffer[session->buffersize-1] != ' ') - session->buffer[session->buffersize++] = ' '; - } - else if(session->dir_mode != XFER_DIR_NLST) - { - if(session->dir_mode == XFER_DIR_STAT) - session->buffer[session->buffersize++] = ' '; - - /* perms nlinks owner group size */ - session->buffersize += - sprintf(session->buffer + session->buffersize, - "%c%c%c%c%c%c%c%c%c%c %lu 3DS 3DS %lld ", - S_ISREG(st->st_mode) ? '-' : - S_ISDIR(st->st_mode) ? 'd' : -#if !defined(_3DS) && !defined(__SWITCH__) - S_ISLNK(st->st_mode) ? 'l' : - S_ISCHR(st->st_mode) ? 'c' : - S_ISBLK(st->st_mode) ? 'b' : - S_ISFIFO(st->st_mode) ? 'p' : - S_ISSOCK(st->st_mode) ? 's' : -#endif - '?', - st->st_mode & S_IRUSR ? 'r' : '-', - st->st_mode & S_IWUSR ? 'w' : '-', - st->st_mode & S_IXUSR ? 'x' : '-', - st->st_mode & S_IRGRP ? 'r' : '-', - st->st_mode & S_IWGRP ? 'w' : '-', - st->st_mode & S_IXGRP ? 'x' : '-', - st->st_mode & S_IROTH ? 'r' : '-', - st->st_mode & S_IWOTH ? 'w' : '-', - st->st_mode & S_IXOTH ? 'x' : '-', - (unsigned long)st->st_nlink, - (signed long long)st->st_size); - - /* timestamp */ - struct tm *tm = gmtime(&st->st_mtime); - if(tm) - { - const char *fmt = "%b %e %Y "; - if(session->timestamp > st->st_mtime - && session->timestamp - st->st_mtime < (60*60*24*365/2)) - { - fmt = "%b %e %H:%M "; - } - - session->buffersize += - strftime(session->buffer + session->buffersize, - sizeof(session->buffer) - session->buffersize, - fmt, tm); - } - else - { - session->buffersize += - sprintf(session->buffer + session->buffersize, "Jan 1 1970 "); - } - } - - if(session->buffersize + len + 2 > sizeof(session->buffer)) - { - /* buffer will overflow */ - return EOVERFLOW; - } - - /* copy path */ - memcpy(session->buffer+session->buffersize, path, len); - len = session->buffersize + len; - session->buffer[len++] = '\r'; - session->buffer[len++] = '\n'; - session->buffersize = len; - - return 0; -} - -/*! fill directory entry - * - * @param[in] session ftp session - * @param[in] st stat data - * @param[in] path path to fill - * @param[in] len path length - * - * @returns errno - */ -static int -ftp_session_fill_dirent(ftp_session_t *session, const struct stat *st, - const char *path, size_t len) -{ - return ftp_session_fill_dirent_type(session, st, path, len, NULL); -} - -/*! transfer loop - * - * Try to transfer as much data as the sockets will allow without blocking - * - * @param[in] session ftp session - */ -static void -ftp_session_transfer(ftp_session_t *session) -{ - int rc; - do - { - rc = session->transfer(session); - } while(rc == 0); -} - -/*! encode a path - * - * @param[in] path path to encode - * @param[in,out] len path length - * @param[in] quotes whether to encode quotes - * - * @returns encoded path - * - * @note The caller must free the returned path - */ -static char* -encode_path(const char *path, - size_t *len, - bool quotes) -{ - bool enc = false; - size_t i, diff = 0; - char *out, *p = (char*)path; - - /* check for \n that needs to be encoded */ - if(memchr(p, '\n', *len) != NULL) - enc = true; - - if(quotes) - { - /* check for " that needs to be encoded */ - p = (char*)path; - do - { - p = memchr(p, '"', path + *len - p); - if(p != NULL) - { - ++p; - ++diff; - } - } while(p != NULL); - } - - /* check if an encode was needed */ - if(!enc && diff == 0) - return strdup(path); - - /* allocate space for encoded path */ - p = out = (char*)malloc(*len + diff); - if(out == NULL) - return NULL; - - /* copy the path while performing encoding */ - for(i = 0; i < *len; ++i) - { - if(*path == '\n') - { - /* encoded \n is \0 */ - *p++ = 0; - } - else if(quotes && *path == '"') - { - /* encoded " is "" */ - *p++ = '"'; - *p++ = '"'; - } - else - *p++ = *path; - ++path; - } - - *len += diff; - return out; -} - -/*! decode a path - * - * @param[in] session ftp session - * @param[in] len command length - */ -static void -decode_path(ftp_session_t *session, - size_t len) -{ - size_t i; - - /* decode \0 from the first command */ - for(i = 0; i < len; ++i) - { - /* this is an encoded \n */ - if(session->cmd_buffer[i] == 0) - session->cmd_buffer[i] = '\n'; - } -} - -/*! fill cdir directory entry - * - * @param[in] session ftp session - * @param[in] path path to fill - * - * @returns errno - */ -static int -ftp_session_fill_dirent_cdir(ftp_session_t *session, const char *path) -{ - int rc; - struct stat st; - char *buffer; - size_t len; - - rc = stat(path, &st); - /* double-check this was a directory */ - if(rc == 0 && !S_ISDIR(st.st_mode)) - { - /* shouldn't happen but just in case */ - rc = -1; - errno = ENOTDIR; - } - if(rc != 0) - return errno; - - /* encode \n in path */ - len = strlen(path); - buffer = encode_path(path, &len, false); - if(!buffer) - return ENOMEM; - - /* fill dirent with listed directory as type=cdir */ - rc = ftp_session_fill_dirent_type(session, &st, buffer, len, "cdir"); - free(buffer); - - return rc; -} - -/*! send a response on the command socket - * - * @param[in] session ftp session - * @param[in] buffer buffer to send - * @param[in] len buffer length - */ -static void -ftp_send_response_buffer(ftp_session_t *session, - const char *buffer, - size_t len) -{ - ssize_t rc, to_send; - - if(session->cmd_fd < 0) - return; - - /* send response */ - to_send = len; - console_print(GREEN "%s" RESET, buffer); - rc = send(session->cmd_fd, buffer, to_send, 0); - if(rc < 0) - { - console_print(RED "send: %d %s\n" RESET, errno, strerror(errno)); - ftp_session_close_cmd(session); - } - else if(rc != to_send) - { - console_print(RED "only sent %u/%u bytes\n" RESET, - (unsigned int)rc, (unsigned int)to_send); - ftp_session_close_cmd(session); - } -} - -__attribute__((format(printf,3,4))) -/*! send ftp response to ftp session's peer - * - * @param[in] session ftp session - * @param[in] code response code - * @param[in] fmt format string - * @param[in] ... format arguments - */ -static void -ftp_send_response(ftp_session_t *session, - int code, - const char *fmt, ...) -{ - static char buffer[CMD_BUFFERSIZE]; - ssize_t rc; - va_list ap; - - if(session->cmd_fd < 0) - return; - - /* print response code and message to buffer */ - va_start(ap, fmt); - if(code > 0) - rc = sprintf(buffer, "%d ", code); - else - rc = sprintf(buffer, "%d-", -code); - rc += vsnprintf(buffer+rc, sizeof(buffer)-rc, fmt, ap); - va_end(ap); - - if(rc >= sizeof(buffer)) - { - /* couldn't fit message; just send code */ - console_print(RED "%s: buffersize too small\n" RESET, __func__); - if(code > 0) - rc = sprintf(buffer, "%d \r\n", code); - else - rc = sprintf(buffer, "%d-\r\n", -code); - } - - ftp_send_response_buffer(session, buffer, rc); -} - -/*! destroy ftp session - * - * @param[in] session ftp session - * - * @returns the next session in the list - */ -static ftp_session_t* -ftp_session_destroy(ftp_session_t *session) -{ - ftp_session_t *next = session->next; - - /* close all sockets/files */ - ftp_session_close_cmd(session); - ftp_session_close_pasv(session); - ftp_session_close_data(session); - ftp_session_close_file(session); - ftp_session_close_cwd(session); - - /* unlink from sessions list */ - if(session->next) - session->next->prev = session->prev; - if(session == sessions) - sessions = session->next; - else - { - session->prev->next = session->next; - if(session == sessions->prev) - sessions->prev = session->prev; - } - - /* deallocate */ - free(session); - - return next; -} - -/*! allocate new ftp session - * - * @param[in] listen_fd socket to accept connection from - */ -static void -ftp_session_new(int listen_fd) -{ - ssize_t rc; - int new_fd; - ftp_session_t *session; - struct sockaddr_in addr; - socklen_t addrlen = sizeof(addr); - - /* accept connection */ - new_fd = accept(listen_fd, (struct sockaddr*)&addr, &addrlen); - if(new_fd < 0) - { - console_print(RED "accept: %d %s\n" RESET, errno, strerror(errno)); - return; - } - - console_print(CYAN "accepted connection from %s:%u\n" RESET, - inet_ntoa(addr.sin_addr), ntohs(addr.sin_port)); - - /* allocate a new session */ - session = (ftp_session_t*)calloc(1, sizeof(ftp_session_t)); - if(session == NULL) - { - console_print(RED "failed to allocate session\n" RESET); - ftp_closesocket(new_fd, true); - return; - } - - /* initialize session */ - strcpy(session->cwd, "/"); - session->peer_addr.sin_addr.s_addr = INADDR_ANY; - session->cmd_fd = new_fd; - session->pasv_fd = -1; - session->data_fd = -1; - session->mlst_flags = SESSION_MLST_TYPE - | SESSION_MLST_SIZE - | SESSION_MLST_MODIFY - | SESSION_MLST_PERM; - session->state = COMMAND_STATE; - - /* link to the sessions list */ - if(sessions == NULL) - { - sessions = session; - session->prev = session; - } - else - { - sessions->prev->next = session; - session->prev = sessions->prev; - sessions->prev = session; - } - - /* copy socket address to pasv address */ - addrlen = sizeof(session->pasv_addr); - rc = getsockname(new_fd, (struct sockaddr*)&session->pasv_addr, &addrlen); - if(rc != 0) - { - console_print(RED "getsockname: %d %s\n" RESET, errno, strerror(errno)); - ftp_send_response(session, 451, "Failed to get connection info\r\n"); - ftp_session_destroy(session); - return; - } - - session->cmd_fd = new_fd; - - /* send initiator response */ - ftp_send_response(session, 220, "Hello!\r\n"); -} - -/*! accept PASV connection for ftp session - * - * @param[in] session ftp session - * - * @returns -1 for failure - */ -static int -ftp_session_accept(ftp_session_t *session) -{ - int rc, new_fd; - struct sockaddr_in addr; - socklen_t addrlen = sizeof(addr); - - if(session->flags & SESSION_PASV) - { - /* clear PASV flag */ - session->flags &= ~SESSION_PASV; - - /* tell the peer that we're ready to accept the connection */ - ftp_send_response(session, 150, "Ready\r\n"); - - /* accept connection from peer */ - new_fd = accept(session->pasv_fd, (struct sockaddr*)&addr, &addrlen); - if(new_fd < 0) - { - console_print(RED "accept: %d %s\n" RESET, errno, strerror(errno)); - ftp_session_set_state(session, COMMAND_STATE, CLOSE_PASV | CLOSE_DATA); - ftp_send_response(session, 425, "Failed to establish connection\r\n"); - return -1; - } - - /* set the socket to non-blocking */ - rc = ftp_set_socket_nonblocking(new_fd); - if(rc != 0) - { - ftp_closesocket(new_fd, true); - ftp_session_set_state(session, COMMAND_STATE, CLOSE_PASV | CLOSE_DATA); - ftp_send_response(session, 425, "Failed to establish connection\r\n"); - return -1; - } - - console_print(CYAN "accepted connection from %s:%u\n" RESET, - inet_ntoa(addr.sin_addr), ntohs(addr.sin_port)); - - /* we are ready to transfer data */ - ftp_session_set_state(session, DATA_TRANSFER_STATE, CLOSE_PASV); - session->data_fd = new_fd; - - return 0; - } - else - { - /* peer didn't send PASV command */ - ftp_send_response(session, 503, "Bad sequence of commands\r\n"); - return -1; - } -} - -/*! connect to peer for ftp session - * - * @param[in] session ftp session - * - * @returns -1 for failure - */ -static int -ftp_session_connect(ftp_session_t *session) -{ - int rc; - - /* clear PORT flag */ - session->flags &= ~SESSION_PORT; - - /* create a new socket */ - session->data_fd = socket(AF_INET, SOCK_STREAM, 0); - if(session->data_fd < 0) - { - console_print(RED "socket: %d %s\n" RESET, errno, strerror(errno)); - return -1; - } - - /* set socket options */ - rc = ftp_set_socket_options(session->data_fd); - if(rc != 0) - { - ftp_closesocket(session->data_fd, false); - session->data_fd = -1; - return -1; - } - - /* set socket to non-blocking */ - rc = ftp_set_socket_nonblocking(session->data_fd); - if(rc != 0) - return -1; - - /* connect to peer */ - rc = connect(session->data_fd, (struct sockaddr*)&session->peer_addr, - sizeof(session->peer_addr)); - if(rc != 0) - { - if(errno != EINPROGRESS) - { - console_print(RED "connect: %d %s\n" RESET, errno, strerror(errno)); - ftp_closesocket(session->data_fd, false); - session->data_fd = -1; - return -1; - } - } - else - { - console_print(CYAN "connected to %s:%u\n" RESET, - inet_ntoa(session->peer_addr.sin_addr), - ntohs(session->peer_addr.sin_port)); - - ftp_session_set_state(session, DATA_TRANSFER_STATE, CLOSE_PASV); - ftp_send_response(session, 150, "Ready\r\n"); - } - - return 0; -} - -/*! read command for ftp session - * - * @param[in] session ftp session - * @param[in] events poll events - */ -static void -ftp_session_read_command(ftp_session_t *session, - int events) -{ - char *buffer, *args, *next = NULL; - size_t i, len; - int atmark; - ssize_t rc; - ftp_command_t key, *command; - - /* check out-of-band data */ - if(events & POLLPRI) - { - session->flags |= SESSION_URGENT; - - /* check if we are at the urgent marker */ - atmark = sockatmark(session->cmd_fd); - if(atmark < 0) - { - console_print(RED "sockatmark: %d %s\n" RESET, errno, strerror(errno)); - ftp_session_close_cmd(session); - return; - } - - if(!atmark) - { - /* discard in-band data */ - rc = recv(session->cmd_fd, session->cmd_buffer, sizeof(session->cmd_buffer), 0); - if(rc < 0 && errno != EWOULDBLOCK) - { - console_print(RED "recv: %d %s\n" RESET, errno, strerror(errno)); - ftp_session_close_cmd(session); - } - - return; - } - - /* retrieve the urgent data */ - rc = recv(session->cmd_fd, session->cmd_buffer, sizeof(session->cmd_buffer), MSG_OOB); - if(rc < 0) - { - /* EWOULDBLOCK means out-of-band data is on the way */ - if(errno == EWOULDBLOCK) - return; - - /* error retrieving out-of-band data */ - console_print(RED "recv (oob): %d %s\n" RESET, errno, strerror(errno)); - ftp_session_close_cmd(session); - return; - } - - /* reset the command buffer */ - session->cmd_buffersize = 0; - return; - } - - /* prepare to receive data */ - buffer = session->cmd_buffer + session->cmd_buffersize; - len = sizeof(session->cmd_buffer) - session->cmd_buffersize; - if(len == 0) - { - /* error retrieving command */ - console_print(RED "Exceeded command buffer size\n" RESET); - ftp_session_close_cmd(session); - return; - } - - /* retrieve command data */ - rc = recv(session->cmd_fd, buffer, len, 0); - if(rc < 0) - { - /* error retrieving command */ - console_print(RED "recv: %d %s\n" RESET, errno, strerror(errno)); - ftp_session_close_cmd(session); - return; - } - if(rc == 0) - { - /* peer closed connection */ - debug_print("peer closed connection\n"); - ftp_session_close_cmd(session); - return; - } - else - { - session->cmd_buffersize += rc; - len = sizeof(session->cmd_buffer) - session->cmd_buffersize; - - if(session->flags & SESSION_URGENT) - { - /* look for telnet data mark */ - for(i = 0; i < session->cmd_buffersize; ++i) - { - if((unsigned char)session->cmd_buffer[i] == 0xF2) - { - /* ignore all data that precedes the data mark */ - if(i < session->cmd_buffersize - 1) - memmove(session->cmd_buffer, session->cmd_buffer + i + 1, len - i - 1); - session->cmd_buffersize -= i + 1; - session->flags &= ~SESSION_URGENT; - break; - } - } - } - - /* loop through commands */ - while(true) - { - /* must have at least enough data for the delimiter */ - if(session->cmd_buffersize < 1) - return; - - /* look for \r\n or \n delimiter */ - for(i = 0; i < session->cmd_buffersize; ++i) - { - if(i < session->cmd_buffersize-1 - && session->cmd_buffer[i] == '\r' - && session->cmd_buffer[i+1] == '\n') - { - /* we found a \r\n delimiter */ - session->cmd_buffer[i] = 0; - next = &session->cmd_buffer[i+2]; - break; - } - else if(session->cmd_buffer[i] == '\n') - { - /* we found a \n delimiter */ - session->cmd_buffer[i] = 0; - next = &session->cmd_buffer[i+1]; - break; - } - } - - /* check if a delimiter was found */ - if(i == session->cmd_buffersize) - return; - - /* decode the command */ - decode_path(session, i); - - /* split command from arguments */ - args = buffer = session->cmd_buffer; - while(*args && !isspace((int)*args)) - ++args; - if(*args) - *args++ = 0; - - /* look up the command */ - key.name = buffer; - command = bsearch(&key, ftp_commands, - num_ftp_commands, sizeof(ftp_command_t), - ftp_command_cmp); - - /* update command timestamp */ - session->timestamp = time(NULL); - - /* execute the command */ - if(command == NULL) - { - /* send header */ - ftp_send_response(session, 502, "Invalid command \""); - - /* send command */ - len = strlen(buffer); - buffer = encode_path(buffer, &len, false); - if(buffer != NULL) - ftp_send_response_buffer(session, buffer, len); - else - ftp_send_response_buffer(session, key.name, strlen(key.name)); - free(buffer); - - /* send args (if any) */ - if(*args != 0) - { - ftp_send_response_buffer(session, " ", 1); - - len = strlen(args); - buffer = encode_path(args, &len, false); - if(buffer != NULL) - ftp_send_response_buffer(session, buffer, len); - else - ftp_send_response_buffer(session, args, strlen(args)); - free(buffer); - } - - /* send footer */ - ftp_send_response_buffer(session, "\"\r\n", 3); - } - else if(session->state != COMMAND_STATE) - { - /* only some commands are available during data transfer */ - if(strcasecmp(command->name, "ABOR") != 0 - && strcasecmp(command->name, "STAT") != 0 - && strcasecmp(command->name, "QUIT") != 0) - { - ftp_send_response(session, 503, "Invalid command during transfer\r\n"); - ftp_session_set_state(session, COMMAND_STATE, CLOSE_PASV | CLOSE_DATA); - ftp_session_close_cmd(session); - } - else - command->handler(session, args); - } - else - { - /* clear RENAME flag for all commands except RNTO */ - if(strcasecmp(command->name, "RNTO") != 0) - session->flags &= ~SESSION_RENAME; - - command->handler(session, args); - } - - /* remove executed command from the command buffer */ - len = session->cmd_buffer + session->cmd_buffersize - next; - if(len > 0) - memmove(session->cmd_buffer, next, len); - session->cmd_buffersize = len; - } - } -} - -/*! poll sockets for ftp session - * - * @param[in] session ftp session - * - * @returns next session - */ -static ftp_session_t* -ftp_session_poll(ftp_session_t *session) -{ - int rc; - struct pollfd pollinfo[2]; - nfds_t nfds = 1; - - /* the first pollfd is the command socket */ - pollinfo[0].fd = session->cmd_fd; - pollinfo[0].events = POLLIN | POLLPRI; - pollinfo[0].revents = 0; - - switch(session->state) - { - case COMMAND_STATE: - /* we are waiting to read a command */ - break; - - case DATA_CONNECT_STATE: - if(session->flags & SESSION_PASV) - { - /* we are waiting for a PASV connection */ - pollinfo[1].fd = session->pasv_fd; - pollinfo[1].events = POLLIN; - } - else - { - /* we are waiting to complete a PORT connection */ - pollinfo[1].fd = session->data_fd; - pollinfo[1].events = POLLOUT; - } - pollinfo[1].revents = 0; - nfds = 2; - break; - - case DATA_TRANSFER_STATE: - /* we need to transfer data */ - pollinfo[1].fd = session->data_fd; - if(session->flags & SESSION_RECV) - pollinfo[1].events = POLLIN; - else - pollinfo[1].events = POLLOUT; - pollinfo[1].revents = 0; - nfds = 2; - break; - } - - /* poll the selected sockets */ - rc = poll(pollinfo, nfds, 0); - if(rc < 0) - { - console_print(RED "poll: %d %s\n" RESET, errno, strerror(errno)); - ftp_session_close_cmd(session); - } - else if(rc > 0) - { - /* check the command socket */ - if(pollinfo[0].revents != 0) - { - /* handle command */ - if(pollinfo[0].revents & POLL_UNKNOWN) - console_print(YELLOW "cmd_fd: revents=0x%08X\n" RESET, pollinfo[0].revents); - - /* we need to read a new command */ - if(pollinfo[0].revents & (POLLERR|POLLHUP)) - { - debug_print("cmd revents=0x%x\n", pollinfo[0].revents); - ftp_session_close_cmd(session); - } - else if(pollinfo[0].revents & (POLLIN | POLLPRI)) - ftp_session_read_command(session, pollinfo[0].revents); - } - - /* check the data/pasv socket */ - if(nfds > 1 && pollinfo[1].revents != 0) - { - switch(session->state) - { - case COMMAND_STATE: - /* this shouldn't happen? */ - break; - - case DATA_CONNECT_STATE: - if(pollinfo[1].revents & POLL_UNKNOWN) - console_print(YELLOW "pasv_fd: revents=0x%08X\n" RESET, pollinfo[1].revents); - - /* we need to accept the PASV connection */ - if(pollinfo[1].revents & (POLLERR|POLLHUP)) - { - ftp_session_set_state(session, COMMAND_STATE, CLOSE_PASV | CLOSE_DATA); - ftp_send_response(session, 426, "Data connection failed\r\n"); - } - else if(pollinfo[1].revents & POLLIN) - { - if(ftp_session_accept(session) != 0) - ftp_session_set_state(session, COMMAND_STATE, CLOSE_PASV | CLOSE_DATA); - } - else if(pollinfo[1].revents & POLLOUT) - { - - console_print(CYAN "connected to %s:%u\n" RESET, - inet_ntoa(session->peer_addr.sin_addr), - ntohs(session->peer_addr.sin_port)); - - ftp_session_set_state(session, DATA_TRANSFER_STATE, CLOSE_PASV); - ftp_send_response(session, 150, "Ready\r\n"); - } - break; - - case DATA_TRANSFER_STATE: - if(pollinfo[1].revents & POLL_UNKNOWN) - console_print(YELLOW "data_fd: revents=0x%08X\n" RESET, pollinfo[1].revents); - - /* we need to transfer data */ - if(pollinfo[1].revents & (POLLERR|POLLHUP)) - { - ftp_session_set_state(session, COMMAND_STATE, CLOSE_PASV | CLOSE_DATA); - ftp_send_response(session, 426, "Data connection failed\r\n"); - } - else if(pollinfo[1].revents & (POLLIN|POLLOUT)) - ftp_session_transfer(session); - break; - } - } - } - - /* still connected to peer; return next session */ - if(session->cmd_fd >= 0) - return session->next; - - /* disconnected from peer; destroy it and return next session */ - debug_print("disconnected from peer\n"); - return ftp_session_destroy(session); -} - -/* Update free space in status bar */ -static void -update_free_space(void) -{ -#if defined(_3DS) || defined(__SWITCH__) -#define KiB (1024.0) -#define MiB (1024.0*KiB) -#define GiB (1024.0*MiB) - char buffer[16]; - struct statvfs st; - double bytes_free; - int rc, len; - - rc = statvfs("sdmc:/", &st); - if(rc != 0) - console_print(RED "statvfs: %d %s\n" RESET, errno, strerror(errno)); - else - { - bytes_free = (double)st.f_bsize * st.f_bfree; - - if (bytes_free < 1000.0) - len = snprintf(buffer, sizeof(buffer), "%.0lfB", bytes_free); - else if(bytes_free < 10.0*KiB) - len = snprintf(buffer, sizeof(buffer), "%.2lfKiB", floor((bytes_free*100.0)/KiB)/100.0); - else if(bytes_free < 100.0*KiB) - len = snprintf(buffer, sizeof(buffer), "%.1lfKiB", floor((bytes_free*10.0)/KiB)/10.0); - else if(bytes_free < 1000.0*KiB) - len = snprintf(buffer, sizeof(buffer), "%.0lfKiB", floor(bytes_free/KiB)); - else if(bytes_free < 10.0*MiB) - len = snprintf(buffer, sizeof(buffer), "%.2lfMiB", floor((bytes_free*100.0)/MiB)/100.0); - else if(bytes_free < 100.0*MiB) - len = snprintf(buffer, sizeof(buffer), "%.1lfMiB", floor((bytes_free*10.0)/MiB)/10.0); - else if(bytes_free < 1000.0*MiB) - len = snprintf(buffer, sizeof(buffer), "%.0lfMiB", floor(bytes_free/MiB)); - else if(bytes_free < 10.0*GiB) - len = snprintf(buffer, sizeof(buffer), "%.2lfGiB", floor((bytes_free*100.0)/GiB)/100.0); - else if(bytes_free < 100.0*GiB) - len = snprintf(buffer, sizeof(buffer), "%.1lfGiB", floor((bytes_free*10.0)/GiB)/10.0); - else - len = snprintf(buffer, sizeof(buffer), "%.0lfGiB", floor(bytes_free/GiB)); - - console_set_status("\x1b[0;%dH" GREEN "%s", 50-len, buffer); - } -#endif -} - -/*! Update status bar */ -static int -update_status(void) -{ -#if defined(_3DS) || defined(__SWITCH__) - console_set_status("\n" GREEN STATUS_STRING " " -#ifdef ENABLE_LOGGING - "DEBUG " -#endif - CYAN "%s:%u" RESET, - inet_ntoa(serv_addr.sin_addr), - ntohs(serv_addr.sin_port)); - update_free_space(); -#else - char hostname[128]; - socklen_t addrlen = sizeof(serv_addr); - int rc; - - rc = getsockname(listenfd, (struct sockaddr*)&serv_addr, &addrlen); - if(rc != 0) - { - console_print(RED "getsockname: %d %s\n" RESET, errno, strerror(errno)); - return -1; - } - - rc = gethostname(hostname, sizeof(hostname)); - if(rc != 0) - { - console_print(RED "gethostname: %d %s\n" RESET, errno, strerror(errno)); - return -1; - } - - console_set_status(GREEN STATUS_STRING " " -#ifdef ENABLE_LOGGING - "DEBUG " -#endif - YELLOW "IP:" CYAN "%s " - YELLOW "Port:" CYAN "%u" - RESET, - hostname, - ntohs(serv_addr.sin_port)); -#endif - - return 0; -} - -#ifdef _3DS -/*! Handle apt events - * - * @param[in] type Event type - * @param[in] closure Callback closure - */ -static void -apt_hook(APT_HookType type, - void *closure) -{ - switch(type) - { - case APTHOOK_ONSUSPEND: - case APTHOOK_ONSLEEP: - /* turn on backlight, or you can't see the home menu! */ - if(R_SUCCEEDED(gspLcdInit())) - { - GSPLCD_PowerOnBacklight(GSPLCD_SCREEN_BOTH); - gspLcdExit(); - } - break; - - case APTHOOK_ONRESTORE: - case APTHOOK_ONWAKEUP: - /* restore backlight power state */ - if(R_SUCCEEDED(gspLcdInit())) - { - (lcd_power ? GSPLCD_PowerOnBacklight : GSPLCD_PowerOffBacklight)(GSPLCD_SCREEN_BOTH); - gspLcdExit(); - } - break; - - default: - break; - } -} -#elif defined(__SWITCH__) -/*! Handle applet events - * - * @param[in] type Event type - * @param[in] closure Callback closure - */ -static void -applet_hook(AppletHookType type, - void *closure) -{ - (void)closure; - (void)type; - /* stubbed for now */ - switch(type) - { - default: - break; - } -} -#endif - -/*! initialize ftp subsystem */ -int -ftp_init(void) -{ - int rc; - - start_time = time(NULL); - -#ifdef _3DS - Result ret = 0; - u32 wifi = 0; - bool loop; - - /* register apt hook */ - aptHook(&cookie, apt_hook, NULL); - - console_print(GREEN "Waiting for wifi...\n" RESET); - - /* wait for wifi to be available */ - while((loop = aptMainLoop()) && !wifi && (ret == 0 || ret == 0xE0A09D2E)) - { - ret = 0; - - hidScanInput(); - if(hidKeysDown() & KEY_B) - { - /* user canceled */ - loop = false; - break; - } - - /* update the wifi status */ - ret = ACU_GetWifiStatus(&wifi); - if(ret != 0) - wifi = 0; - } - - /* check if there was a wifi error */ - if(ret != 0) - console_print(RED "ACU_GetWifiStatus returns 0x%lx\n" RESET, ret); - - /* check if we need to exit */ - if(!loop || ret != 0) - return -1; - - console_print(GREEN "Ready!\n" RESET); - - /* allocate buffer for SOC service */ - SOCU_buffer = (u32*)memalign(SOCU_ALIGN, SOCU_BUFFERSIZE); - if(SOCU_buffer == NULL) - { - console_print(RED "memalign: failed to allocate\n" RESET); - goto memalign_fail; - } - - /* initialize SOC service */ - ret = socInit(SOCU_buffer, SOCU_BUFFERSIZE); - if(ret != 0) - { - console_print(RED "socInit: %08X\n" RESET, (unsigned int)ret); - goto soc_fail; - } -#elif defined(__SWITCH__) - static const SocketInitConfig socketInitConfig = { - .bsdsockets_version = 1, - - .tcp_tx_buf_size = 8 * SOCK_BUFFERSIZE, - .tcp_rx_buf_size = 8 * SOCK_BUFFERSIZE, - .tcp_tx_buf_max_size = 16 * SOCK_BUFFERSIZE, - .tcp_rx_buf_max_size = 16 * SOCK_BUFFERSIZE, - - .udp_tx_buf_size = 0x2400, - .udp_rx_buf_size = 0xA500, - - .sb_efficiency = 8, - }; - - Result ret = socketInitialize(&socketInitConfig); - if(ret != 0) - { - console_print(RED "socketInitialize: %X\n" RESET, (unsigned int)ret); - return -1; - } - - /* register applet hook */ - appletHook(&cookie, applet_hook, NULL); -#endif - - /* allocate socket to listen for clients */ - listenfd = socket(AF_INET, SOCK_STREAM, 0); - if(listenfd < 0) - { - console_print(RED "socket: %d %s\n" RESET, errno, strerror(errno)); - ftp_exit(); - return -1; - } - - /* get address to listen on */ - serv_addr.sin_family = AF_INET; -#if defined(_3DS) || defined(__SWITCH__) - serv_addr.sin_addr.s_addr = gethostid(); - serv_addr.sin_port = htons(LISTEN_PORT); -#else - serv_addr.sin_addr.s_addr = INADDR_ANY; - serv_addr.sin_port = htons(LISTEN_PORT); -#endif - - /* reuse address */ - { - int yes = 1; - rc = setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes)); - if(rc != 0) - { - console_print(RED "setsockopt: %d %s\n" RESET, errno, strerror(errno)); - ftp_exit(); - return -1; - } - } - - /* bind socket to listen address */ - rc = bind(listenfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr)); - if(rc != 0) - { - console_print(RED "bind: %d %s\n" RESET, errno, strerror(errno)); - ftp_exit(); - return -1; - } - - /* listen on socket */ - rc = listen(listenfd, 5); - if(rc != 0) - { - console_print(RED "listen: %d %s\n" RESET, errno, strerror(errno)); - ftp_exit(); - return -1; - } - - /* print server address */ - rc = update_status(); - if(rc != 0) - { - ftp_exit(); - return -1; - } - - return 0; - -#ifdef _3DS -soc_fail: - free(SOCU_buffer); - SOCU_buffer = NULL; - -memalign_fail: - return -1; -#endif -} - -/*! deinitialize ftp subsystem */ -void -ftp_exit(void) -{ -#if defined(_3DS) - Result ret; -#endif - - debug_print("exiting ftp server\n"); - - /* clean up all sessions */ - while(sessions != NULL) - ftp_session_destroy(sessions); - - /* stop listening for new clients */ - if(listenfd >= 0) - ftp_closesocket(listenfd, false); - -#ifdef _3DS - /* deinitialize SOC service */ - console_render(); - console_print(CYAN "Waiting for socExit()...\n" RESET); - - if(SOCU_buffer != NULL) - { - ret = socExit(); - if(ret != 0) - console_print(RED "socExit: 0x%08X\n" RESET, (unsigned int)ret); - free(SOCU_buffer); - } -#elif defined(__SWITCH__) - /* deinitialize socket driver */ - console_render(); - console_print(CYAN "Waiting for socketExit()...\n" RESET); - - socketExit(); - -#endif -} - -/*! ftp look - * - * @returns whether to keep looping - */ -loop_status_t -ftp_loop(void) -{ - int rc; - struct pollfd pollinfo; - ftp_session_t *session; - - /* we will poll for new client connections */ - pollinfo.fd = listenfd; - pollinfo.events = POLLIN; - pollinfo.revents = 0; - - /* poll for a new client */ - rc = poll(&pollinfo, 1, 0); - if(rc < 0) - { - /* wifi got disabled */ - if(errno == ENETDOWN) - return LOOP_RESTART; - - console_print(RED "poll: %d %s\n" RESET, errno, strerror(errno)); - return LOOP_EXIT; - } - else if(rc > 0) - { - if(pollinfo.revents & POLLIN) - { - /* we got a new client */ - ftp_session_new(listenfd); - } - else - { - console_print(YELLOW "listenfd: revents=0x%08X\n" RESET, pollinfo.revents); - } - } - - /* poll each session */ - session = sessions; - while(session != NULL) - session = ftp_session_poll(session); - -#ifdef _3DS - /* check if the user wants to exit */ - hidScanInput(); - u32 down = hidKeysDown(); - - if(down & KEY_B) - return LOOP_EXIT; - - /* check if the user wants to toggle the LCD power */ - if(down & KEY_START) - { - lcd_power = !lcd_power; - apt_hook(APTHOOK_ONRESTORE, NULL); - } -#elif defined(__SWITCH__) - /* check if the user wants to exit */ - hidScanInput(); - u32 down = hidKeysDown(CONTROLLER_P1_AUTO); - - if(down & KEY_B) - return LOOP_EXIT; -#endif - - return LOOP_CONTINUE; -} - -/*! change to parent directory - * - * @param[in] session ftp session - */ -static void -cd_up(ftp_session_t *session) -{ - char *slash = NULL, *p; - - /* remove basename from cwd */ - for(p = session->cwd; *p; ++p) - { - if(*p == '/') - slash = p; - } - *slash = 0; - if(strlen(session->cwd) == 0) - strcat(session->cwd, "/"); -} - -/*! validate a path - * - * @param[in] args path to validate - */ -static int -validate_path(const char *args) -{ - const char *p; - - /* make sure no path components are '..' */ - p = args; - while((p = strstr(p, "/..")) != NULL) - { - if(p[3] == 0 || p[3] == '/') - return -1; - } - - /* make sure there are no '//' */ - if(strstr(args, "//") != NULL) - return -1; - - return 0; -} - -/*! get a path relative to cwd - * - * @param[in] session ftp session - * @param[in] cwd working directory - * @param[in] args path to make - * - * @returns error - * - * @note the output goes to session->buffer - */ -static int -build_path(ftp_session_t *session, - const char *cwd, - const char *args) -{ - int rc; - char *p; - - session->buffersize = 0; - memset(session->buffer, 0, sizeof(session->buffer)); - - /* make sure the input is a valid path */ - if(validate_path(args) != 0) - { - errno = EINVAL; - return -1; - } - - if(args[0] == '/') - { - /* this is an absolute path */ - size_t len = strlen(args); - if(len > sizeof(session->buffer)-1) - { - errno = ENAMETOOLONG; - return -1; - } - - memcpy(session->buffer, args, len); - session->buffersize = len; - } - else - { - /* this is a relative path */ - if(strcmp(cwd, "/") == 0) - rc = snprintf(session->buffer, sizeof(session->buffer), "/%s", - args); - else - rc = snprintf(session->buffer, sizeof(session->buffer), "%s/%s", - cwd, args); - - if(rc >= sizeof(session->buffer)) - { - errno = ENAMETOOLONG; - return -1; - } - - session->buffersize = rc; - } - - /* remove trailing / */ - p = session->buffer + session->buffersize; - while(p > session->buffer && *--p == '/') - { - *p = 0; - --session->buffersize; - } - - /* if we ended with an empty path, it is the root directory */ - if(session->buffersize == 0) - session->buffer[session->buffersize++] = '/'; - - return 0; -} - -/*! transfer a directory listing - * - * @param[in] session ftp session - * - * @returns whether to call again - */ -static loop_status_t -list_transfer(ftp_session_t *session) -{ - ssize_t rc; - size_t len; - char *buffer; - struct stat st; - struct dirent *dent; - - /* check if we sent all available data */ - if(session->bufferpos == session->buffersize) - { - /* check xfer dir type */ - if(session->dir_mode == XFER_DIR_STAT) - rc = 213; - else - rc = 226; - - /* check if this was for a file */ - if(session->dp == NULL) - { - /* we already sent the file's listing */ - ftp_session_set_state(session, COMMAND_STATE, CLOSE_PASV | CLOSE_DATA); - ftp_send_response(session, rc, "OK\r\n"); - return LOOP_EXIT; - } - - /* get the next directory entry */ - dent = readdir(session->dp); - if(dent == NULL) - { - /* we have exhausted the directory listing */ - ftp_session_set_state(session, COMMAND_STATE, CLOSE_PASV | CLOSE_DATA); - ftp_send_response(session, rc, "OK\r\n"); - return LOOP_EXIT; - } - - /* TODO I think we are supposed to return entries for . and .. */ - if(strcmp(dent->d_name, ".") == 0 || strcmp(dent->d_name, "..") == 0) - return LOOP_CONTINUE; - - /* check if this was a NLST */ - if(session->dir_mode == XFER_DIR_NLST) - { - /* NLST gives the whole path name */ - session->buffersize = 0; - if(build_path(session, session->lwd, dent->d_name) == 0) - { - /* encode \n in path */ - len = session->buffersize; - buffer = encode_path(session->buffer, &len, false); - if(buffer != NULL) - { - /* copy to the session buffer to send */ - memcpy(session->buffer, buffer, len); - free(buffer); - session->buffer[len++] = '\r'; - session->buffer[len++] = '\n'; - session->buffersize = len; - } - } - } - else - { -#ifdef _3DS - /* the sdmc directory entry already has the type and size, so no need to do a slow stat */ - u32 magic = *(u32*)session->dp->dirData->dirStruct; - - if(magic == SDMC_DIRITER_MAGIC) - { - sdmc_dir_t *dir = (sdmc_dir_t*)session->dp->dirData->dirStruct; - FS_DirectoryEntry *entry = &dir->entry_data[dir->index]; - - if(entry->attributes & FS_ATTRIBUTE_DIRECTORY) - st.st_mode = S_IFDIR | S_IRUSR | S_IRGRP | S_IROTH; - else - st.st_mode = S_IFREG | S_IRUSR | S_IRGRP | S_IROTH; - - if(!(entry->attributes & FS_ATTRIBUTE_READ_ONLY)) - st.st_mode |= S_IWUSR | S_IWGRP | S_IWOTH; - - st.st_size = entry->fileSize; - st.st_mtime = 0; - - bool getmtime = true; - if(session->dir_mode == XFER_DIR_MLSD - || session->dir_mode == XFER_DIR_MLST) - { - if(!(session->mlst_flags & SESSION_MLST_MODIFY)) - getmtime = false; - } - else if(session->dir_mode == XFER_DIR_NLST) - getmtime = false; - - if((rc = build_path(session, session->lwd, dent->d_name)) != 0) - console_print(RED "build_path: %d %s\n" RESET, errno, strerror(errno)); - else if(getmtime) - { - uint64_t mtime = 0; - if((rc = sdmc_getmtime(session->buffer, &mtime)) != 0) - console_print(RED "sdmc_getmtime '%s': 0x%x\n" RESET, session->buffer, rc); - else - st.st_mtime = mtime; - } - } - else - { - /* lstat the entry */ - if((rc = build_path(session, session->lwd, dent->d_name)) != 0) - console_print(RED "build_path: %d %s\n" RESET, errno, strerror(errno)); - else if((rc = lstat(session->buffer, &st)) != 0) - console_print(RED "stat '%s': %d %s\n" RESET, session->buffer, errno, strerror(errno)); - - if(rc != 0) - { - /* an error occurred */ - ftp_session_set_state(session, COMMAND_STATE, CLOSE_PASV | CLOSE_DATA); - ftp_send_response(session, 550, "unavailable\r\n"); - return LOOP_EXIT; - } - } -#else - /* lstat the entry */ - if((rc = build_path(session, session->lwd, dent->d_name)) != 0) - console_print(RED "build_path: %d %s\n" RESET, errno, strerror(errno)); - else if((rc = lstat(session->buffer, &st)) != 0) - console_print(RED "stat '%s': %d %s\n" RESET, session->buffer, errno, strerror(errno)); - - if(rc != 0) - { - /* an error occurred */ - ftp_session_set_state(session, COMMAND_STATE, CLOSE_PASV | CLOSE_DATA); - ftp_send_response(session, 550, "unavailable\r\n"); - return LOOP_EXIT; - } -#endif - /* encode \n in path */ - len = strlen(dent->d_name); - buffer = encode_path(dent->d_name, &len, false); - if(buffer != NULL) - { - rc = ftp_session_fill_dirent(session, &st, buffer, len); - free(buffer); - if(rc != 0) - { - ftp_session_set_state(session, COMMAND_STATE, CLOSE_PASV | CLOSE_DATA); - ftp_send_response(session, 425, "%s\r\n", strerror(rc)); - return LOOP_EXIT; - } - } - else - session->buffersize = 0; - } - session->bufferpos = 0; - } - - /* send any pending data */ - rc = send(session->data_fd, session->buffer + session->bufferpos, - session->buffersize - session->bufferpos, 0); - if(rc <= 0) - { - /* error sending data */ - if(rc < 0) - { - if(errno == EWOULDBLOCK) - return LOOP_EXIT; - console_print(RED "send: %d %s\n" RESET, errno, strerror(errno)); - } - else - console_print(YELLOW "send: %d %s\n" RESET, ECONNRESET, strerror(ECONNRESET)); - - ftp_session_set_state(session, COMMAND_STATE, CLOSE_PASV | CLOSE_DATA); - ftp_send_response(session, 426, "Connection broken during transfer\r\n"); - return LOOP_EXIT; - } - - /* we can try to send more data */ - session->bufferpos += rc; - return LOOP_CONTINUE; -} - -/*! send a file to the client - * - * @param[in] session ftp session - * - * @returns whether to call again - */ -static loop_status_t -retrieve_transfer(ftp_session_t *session) -{ - ssize_t rc; - - if(session->bufferpos == session->buffersize) - { - /* we have sent all the data so read some more */ - rc = ftp_session_read_file(session); - if(rc <= 0) - { - /* can't read any more data */ - ftp_session_set_state(session, COMMAND_STATE, CLOSE_PASV | CLOSE_DATA); - if(rc < 0) - ftp_send_response(session, 451, "Failed to read file\r\n"); - else - ftp_send_response(session, 226, "OK\r\n"); - return LOOP_EXIT; - } - - /* we read some data so reset the session buffer to send */ - session->bufferpos = 0; - session->buffersize = rc; - } - - /* send any pending data */ - rc = send(session->data_fd, session->buffer + session->bufferpos, - session->buffersize - session->bufferpos, 0); - if(rc <= 0) - { - /* error sending data */ - if(rc < 0) - { - if(errno == EWOULDBLOCK) - return LOOP_EXIT; - console_print(RED "send: %d %s\n" RESET, errno, strerror(errno)); - } - else - console_print(YELLOW "send: %d %s\n" RESET, ECONNRESET, strerror(ECONNRESET)); - - ftp_session_set_state(session, COMMAND_STATE, CLOSE_PASV | CLOSE_DATA); - ftp_send_response(session, 426, "Connection broken during transfer\r\n"); - return LOOP_EXIT; - } - - /* we can try to send more data */ - session->bufferpos += rc; - return LOOP_CONTINUE; -} - -/*! send a file to the client - * - * @param[in] session ftp session - * - * @returns whether to call again - */ -static loop_status_t -store_transfer(ftp_session_t *session) -{ - ssize_t rc; - - if(session->bufferpos == session->buffersize) - { - /* we have written all the received data, so try to get some more */ - rc = recv(session->data_fd, session->buffer, sizeof(session->buffer), 0); - if(rc <= 0) - { - /* can't read any more data */ - if(rc < 0) - { - if(errno == EWOULDBLOCK) - return LOOP_EXIT; - console_print(RED "recv: %d %s\n" RESET, errno, strerror(errno)); - } - - ftp_session_set_state(session, COMMAND_STATE, CLOSE_PASV | CLOSE_DATA); - - if(rc == 0) - ftp_send_response(session, 226, "OK\r\n"); - else - ftp_send_response(session, 426, "Connection broken during transfer\r\n"); - return LOOP_EXIT; - } - - /* we received some data so reset the session buffer to write */ - session->bufferpos = 0; - session->buffersize = rc; - } - - rc = ftp_session_write_file(session); - if(rc <= 0) - { - /* error writing data */ - ftp_session_set_state(session, COMMAND_STATE, CLOSE_PASV | CLOSE_DATA); - ftp_send_response(session, 451, "Failed to write file\r\n"); - return LOOP_EXIT; - } - - /* we can try to receive more data */ - session->bufferpos += rc; - return LOOP_CONTINUE; -} - -/*! ftp_xfer_file mode */ -typedef enum -{ - XFER_FILE_RETR, /*!< Retrieve a file */ - XFER_FILE_STOR, /*!< Store a file */ - XFER_FILE_APPE, /*!< Append a file */ -} xfer_file_mode_t; - -/*! Transfer a file - * - * @param[in] session ftp session - * @param[in] args ftp arguments - * @param[in] mode transfer mode - * - * @returns failure - */ -static void -ftp_xfer_file(ftp_session_t *session, - const char *args, - xfer_file_mode_t mode) -{ - int rc; - - /* build the path of the file to transfer */ - if(build_path(session, session->cwd, args) != 0) - { - rc = errno; - ftp_session_set_state(session, COMMAND_STATE, CLOSE_PASV | CLOSE_DATA); - ftp_send_response(session, 553, "%s\r\n", strerror(rc)); - return; - } - - /* open the file for retrieving or storing */ - if(mode == XFER_FILE_RETR) - rc = ftp_session_open_file_read(session); - else - rc = ftp_session_open_file_write(session, mode == XFER_FILE_APPE); - - if(rc != 0) - { - /* error opening the file */ - ftp_session_set_state(session, COMMAND_STATE, CLOSE_PASV | CLOSE_DATA); - ftp_send_response(session, 450, "failed to open file\r\n"); - return; - } - - if(session->flags & (SESSION_PORT|SESSION_PASV)) - { - ftp_session_set_state(session, DATA_CONNECT_STATE, CLOSE_DATA); - - if(session->flags & SESSION_PORT) - { - /* setup connection */ - rc = ftp_session_connect(session); - if(rc != 0) - { - /* error connecting */ - ftp_session_set_state(session, COMMAND_STATE, CLOSE_PASV | CLOSE_DATA); - ftp_send_response(session, 425, "can't open data connection\r\n"); - return; - } - } - - /* set up the transfer */ - session->flags &= ~(SESSION_RECV|SESSION_SEND); - if(mode == XFER_FILE_RETR) - { - session->flags |= SESSION_SEND; - session->transfer = retrieve_transfer; - } - else - { - session->flags |= SESSION_RECV; - session->transfer = store_transfer; - } - - session->bufferpos = 0; - session->buffersize = 0; - - return; - } - - ftp_session_set_state(session, COMMAND_STATE, CLOSE_PASV | CLOSE_DATA); - ftp_send_response(session, 503, "Bad sequence of commands\r\n"); -} - -/*! Transfer a directory - * - * @param[in] session ftp session - * @param[in] args ftp arguments - * @param[in] mode transfer mode - * @param[in] workaround whether to workaround LIST -a - */ -static void -ftp_xfer_dir(ftp_session_t *session, - const char *args, - xfer_dir_mode_t mode, - bool workaround) -{ - ssize_t rc; - size_t len; - struct stat st; - char *buffer; - - /* set up the transfer */ - session->dir_mode = mode; - session->flags &= ~SESSION_RECV; - session->flags |= SESSION_SEND; - - session->transfer = list_transfer; - session->buffersize = 0; - session->bufferpos = 0; - - if(strlen(args) > 0) - { - /* an argument was provided */ - if(build_path(session, session->cwd, args) != 0) - { - /* error building path */ - ftp_session_set_state(session, COMMAND_STATE, CLOSE_PASV | CLOSE_DATA); - ftp_send_response(session, 550, "%s\r\n", strerror(errno)); - return; - } - - /* check if this is a directory */ - session->dp = opendir(session->buffer); - if(session->dp == NULL) - { - /* not a directory; check if it is a file */ - rc = stat(session->buffer, &st); - if(rc != 0) - { - /* error getting stat */ - rc = errno; - - /* work around broken clients that think LIST -a is valid */ - if(workaround && mode == XFER_DIR_LIST) - { - if(args[0] == '-' && (args[1] == 'a' || args[1] == 'l')) - { - if(args[2] == 0) - buffer = strdup(args+2); - else - buffer = strdup(args+3); - - if(buffer != NULL) - { - ftp_xfer_dir(session, buffer, mode, false); - free(buffer); - return; - } - - rc = ENOMEM; - } - } - - ftp_session_set_state(session, COMMAND_STATE, CLOSE_PASV | CLOSE_DATA); - ftp_send_response(session, 550, "%s\r\n", strerror(rc)); - return; - } - else if(mode == XFER_DIR_MLSD) - { - /* specified file instead of directory for MLSD */ - ftp_session_set_state(session, COMMAND_STATE, CLOSE_PASV | CLOSE_DATA); - ftp_send_response(session, 501, "%s\r\n", strerror(EINVAL)); - return; - } - else if(mode == XFER_DIR_NLST) - { - /* NLST uses full path name */ - len = session->buffersize; - buffer = encode_path(session->buffer, &len, false); - } - else - { - /* everything else uses base name */ - const char *base = strrchr(session->buffer, '/') + 1; - - len = strlen(base); - buffer = encode_path(base, &len, false); - } - - if(buffer) - { - rc = ftp_session_fill_dirent(session, &st, buffer, len); - free(buffer); - } - else - rc = ENOMEM; - - if(rc != 0) - { - ftp_session_set_state(session, COMMAND_STATE, CLOSE_PASV | CLOSE_DATA); - ftp_send_response(session, 550, "%s\r\n", strerror(rc)); - return; - } - } - else - { - /* it was a directory, so set it as the lwd */ - memcpy(session->lwd, session->buffer, session->buffersize); - session->lwd[session->buffersize] = 0; - session->buffersize = 0; - - if(session->dir_mode == XFER_DIR_MLSD - && (session->mlst_flags & SESSION_MLST_TYPE)) - { - /* send this directory as type=cdir */ - rc = ftp_session_fill_dirent_cdir(session, session->lwd); - if(rc != 0) - { - ftp_session_set_state(session, COMMAND_STATE, CLOSE_PASV | CLOSE_DATA); - ftp_send_response(session, 550, "%s\r\n", strerror(rc)); - return; - } - } - } - } - else if(ftp_session_open_cwd(session) != 0) - { - /* no argument, but opening cwd failed */ - ftp_session_set_state(session, COMMAND_STATE, CLOSE_PASV | CLOSE_DATA); - ftp_send_response(session, 550, "%s\r\n", strerror(errno)); - return; - } - else - { - /* set the cwd as the lwd */ - strcpy(session->lwd, session->cwd); - session->buffersize = 0; - - if(session->dir_mode == XFER_DIR_MLSD - && (session->mlst_flags & SESSION_MLST_TYPE)) - { - /* send this directory as type=cdir */ - rc = ftp_session_fill_dirent_cdir(session, session->lwd); - if(rc != 0) - { - ftp_session_set_state(session, COMMAND_STATE, CLOSE_PASV | CLOSE_DATA); - ftp_send_response(session, 550, "%s\r\n", strerror(rc)); - return; - } - } - } - - if(mode == XFER_DIR_MLST || mode == XFER_DIR_STAT) - { - /* this is a little different; we have to send the data over the command socket */ - ftp_session_set_state(session, DATA_TRANSFER_STATE, CLOSE_PASV | CLOSE_DATA); - session->data_fd = session->cmd_fd; - session->flags |= SESSION_SEND; - ftp_send_response(session, -213, "Status\r\n"); - return; - } - else if(session->flags & (SESSION_PORT|SESSION_PASV)) - { - ftp_session_set_state(session, DATA_CONNECT_STATE, CLOSE_DATA); - - if(session->flags & SESSION_PORT) - { - /* setup connection */ - rc = ftp_session_connect(session); - if(rc != 0) - { - /* error connecting */ - ftp_session_set_state(session, COMMAND_STATE, CLOSE_PASV | CLOSE_DATA); - ftp_send_response(session, 425, "can't open data connection\r\n"); - } - } - - return; - } - - /* we must have got LIST/MLSD/MLST/NLST without a preceding PORT or PASV */ - ftp_session_set_state(session, COMMAND_STATE, CLOSE_PASV | CLOSE_DATA); - ftp_send_response(session, 503, "Bad sequence of commands\r\n"); -} - -/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * - * * - * F T P C O M M A N D S * - * * - * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ - -/*! @fn static void ABOR(ftp_session_t *session, const char *args) - * - * @brief abort a transfer - * - * @param[in] session ftp session - * @param[in] args arguments - */ -FTP_DECLARE(ABOR) -{ - console_print(CYAN "%s %s\n" RESET, __func__, args ? args : ""); - - if(session->state == COMMAND_STATE) - { - ftp_send_response(session, 225, "No transfer to abort\r\n"); - return; - } - - /* abort the transfer */ - ftp_session_set_state(session, COMMAND_STATE, CLOSE_PASV | CLOSE_DATA); - - /* send response for this request */ - ftp_send_response(session, 225, "Aborted\r\n"); - - /* send response for transfer */ - ftp_send_response(session, 425, "Transfer aborted\r\n"); -} - -/*! @fn static void ALLO(ftp_session_t *session, const char *args) - * - * @brief allocate space - * - * @param[in] session ftp session - * @param[in] args arguments - */ -FTP_DECLARE(ALLO) -{ - console_print(CYAN "%s %s\n" RESET, __func__, args ? args : ""); - - ftp_session_set_state(session, COMMAND_STATE, 0); - - ftp_send_response(session, 202, "superfluous command\r\n"); -} - -/*! @fn static void APPE(ftp_session_t *session, const char *args) - * - * @brief append data to a file - * - * @note requires a PASV or PORT connection - * - * @param[in] session ftp session - * @param[in] args arguments - */ -FTP_DECLARE(APPE) -{ - console_print(CYAN "%s %s\n" RESET, __func__, args ? args : ""); - - /* open the file in append mode */ - ftp_xfer_file(session, args, XFER_FILE_APPE); -} - -/*! @fn static void CDUP(ftp_session_t *session, const char *args) - * - * @brief CWD to parent directory - * - * @param[in] session ftp session - * @param[in] args arguments - */ -FTP_DECLARE(CDUP) -{ - console_print(CYAN "%s %s\n" RESET, __func__, args ? args : ""); - - ftp_session_set_state(session, COMMAND_STATE, 0); - - /* change to parent directory */ - cd_up(session); - - ftp_send_response(session, 200, "OK\r\n"); -} - -/*! @fn static void CWD(ftp_session_t *session, const char *args) - * - * @brief change working directory - * - * @param[in] session ftp session - * @param[in] args arguments - */ -FTP_DECLARE(CWD) -{ - struct stat st; - int rc; - - console_print(CYAN "%s %s\n" RESET, __func__, args ? args : ""); - - ftp_session_set_state(session, COMMAND_STATE, 0); - - /* .. is equivalent to CDUP */ - if(strcmp(args, "..") == 0) - { - cd_up(session); - ftp_send_response(session, 200, "OK\r\n"); - return; - } - - /* build the new cwd path */ - if(build_path(session, session->cwd, args) != 0) - { - ftp_send_response(session, 553, "%s\r\n", strerror(errno)); - return; - } - - /* get the path status */ - rc = stat(session->buffer, &st); - if(rc != 0) - { - console_print(RED "stat '%s': %d %s\n" RESET, session->buffer, errno, strerror(errno)); - ftp_send_response(session, 550, "unavailable\r\n"); - return; - } - - /* make sure it is a directory */ - if(!S_ISDIR(st.st_mode)) - { - ftp_send_response(session, 553, "not a directory\r\n"); - return; - } - - /* copy the path into the cwd */ - strncpy(session->cwd, session->buffer, sizeof(session->cwd)); - session->cwd[sizeof(session->cwd)-1] = '\0'; - ftp_send_response(session, 200, "OK\r\n"); -} - -/*! @fn static void DELE(ftp_session_t *session, const char *args) - * - * @brief delete a file - * - * @param[in] session ftp session - * @param[in] args arguments - */ -FTP_DECLARE(DELE) -{ - int rc; - - console_print(CYAN "%s %s\n" RESET, __func__, args ? args : ""); - - ftp_session_set_state(session, COMMAND_STATE, 0); - - /* build the file path */ - if(build_path(session, session->cwd, args) != 0) - { - ftp_send_response(session, 553, "%s\r\n", strerror(errno)); - return; - } - - /* try to unlink the path */ - rc = unlink(session->buffer); - if(rc != 0) - { - /* error unlinking the file */ - console_print(RED "unlink: %d %s\n" RESET, errno, strerror(errno)); - ftp_send_response(session, 550, "failed to delete file\r\n"); - return; - } - - update_free_space(); - ftp_send_response(session, 250, "OK\r\n"); -} - -/*! @fn static void FEAT(ftp_session_t *session, const char *args) - * - * @brief list server features - * - * @param[in] session ftp session - * @param[in] args arguments - */ -FTP_DECLARE(FEAT) -{ - console_print(CYAN "%s %s\n" RESET, __func__, args ? args : ""); - - ftp_session_set_state(session, COMMAND_STATE, 0); - - /* list our features */ - ftp_send_response(session, -211, "\r\n" - " MDTM\r\n" - " MLST Type%s;Size%s;Modify%s;Perm%s;UNIX.mode%s;\r\n" - " PASV\r\n" - " SIZE\r\n" - " TVFS\r\n" - " UTF8\r\n" - "\r\n" - "211 End\r\n", - session->mlst_flags & SESSION_MLST_TYPE ? "*" : "", - session->mlst_flags & SESSION_MLST_SIZE ? "*" : "", - session->mlst_flags & SESSION_MLST_MODIFY ? "*" : "", - session->mlst_flags & SESSION_MLST_PERM ? "*" : "", - session->mlst_flags & SESSION_MLST_UNIX_MODE ? "*" : ""); -} - -/*! @fn static void HELP(ftp_session_t *session, const char *args) - * - * @brief print server help - * - * @param[in] session ftp session - * @param[in] args arguments - */ -FTP_DECLARE(HELP) -{ - console_print(CYAN "%s %s\n" RESET, __func__, args ? args : ""); - - ftp_session_set_state(session, COMMAND_STATE, 0); - - /* list our accepted commands */ - ftp_send_response(session, -214, - "The following commands are recognized\r\n" - " ABOR ALLO APPE CDUP CWD DELE FEAT HELP LIST MDTM MKD MLSD MLST MODE\r\n" - " NLST NOOP OPTS PASS PASV PORT PWD QUIT REST RETR RMD RNFR RNTO STAT\r\n" - " STOR STOU STRU SYST TYPE USER XCUP XCWD XMKD XPWD XRMD\r\n" - "214 End\r\n"); -} - -/*! @fn static void LIST(ftp_session_t *session, const char *args) - * - * @brief retrieve a directory listing - * - * @note Requires a PORT or PASV connection - * - * @param[in] session ftp session - * @param[in] args arguments - */ -FTP_DECLARE(LIST) -{ - console_print(CYAN "%s %s\n" RESET, __func__, args ? args : ""); - - /* open the path in LIST mode */ - ftp_xfer_dir(session, args, XFER_DIR_LIST, true); -} - -/*! @fn static void MDTM(ftp_session_t *session, const char *args) - * - * @brief get last modification time - * - * @param[in] session ftp session - * @param[in] args arguments - */ -FTP_DECLARE(MDTM) -{ - int rc; -#ifdef _3DS - uint64_t mtime; -#else - struct stat st; -#endif - time_t t_mtime; - struct tm *tm; - - console_print(CYAN "%s %s\n" RESET, __func__, args ? args : ""); - - ftp_session_set_state(session, COMMAND_STATE, 0); - - /* build the path */ - if(build_path(session, session->cwd, args) != 0) - { - ftp_send_response(session, 553, "%s\r\n", strerror(errno)); - return; - } - -#ifdef _3DS - rc = sdmc_getmtime(session->buffer, &mtime); - if(rc != 0) - { - ftp_send_response(session, 550, "Error getting mtime\r\n"); - return; - } - t_mtime = mtime; -#else - rc = stat(session->buffer, &st); - if(rc != 0) - { - ftp_send_response(session, 550, "Error getting mtime\r\n"); - return; - } - t_mtime = st.st_mtime; -#endif - - tm = gmtime(&t_mtime); - if(tm == NULL) - { - ftp_send_response(session, 550, "Error getting mtime\r\n"); - return; - } - - session->buffersize = strftime(session->buffer, sizeof(session->buffer), "%Y%m%d%H%M%S", tm); - if(session->buffersize == 0) - { - ftp_send_response(session, 550, "Error getting mtime\r\n"); - return; - } - - session->buffer[session->buffersize] = 0; - - ftp_send_response(session, 213, "%s\r\n", session->buffer); -} -/*! @fn static void MKD(ftp_session_t *session, const char *args) - * - * @brief create a directory - * - * @param[in] session ftp session - * @param[in] args arguments - */ -FTP_DECLARE(MKD) -{ - int rc; - - console_print(CYAN "%s %s\n" RESET, __func__, args ? args : ""); - - ftp_session_set_state(session, COMMAND_STATE, 0); - - /* build the path */ - if(build_path(session, session->cwd, args) != 0) - { - ftp_send_response(session, 553, "%s\r\n", strerror(errno)); - return; - } - - /* try to create the directory */ - rc = mkdir(session->buffer, 0755); - if(rc != 0 && errno != EEXIST) - { - /* mkdir failure */ - console_print(RED "mkdir: %d %s\n" RESET, errno, strerror(errno)); - ftp_send_response(session, 550, "failed to create directory\r\n"); - return; - } - - update_free_space(); - ftp_send_response(session, 250, "OK\r\n"); -} - -/*! @fn static void MLSD(ftp_session_t *session, const char *args) - * - * @brief set transfer mode - * - * @param[in] session ftp session - * @param[in] args arguments - */ -FTP_DECLARE(MLSD) -{ - console_print(CYAN "%s %s\n" RESET, __func__, args ? args : ""); - - /* open the path in MLSD mode */ - ftp_xfer_dir(session, args, XFER_DIR_MLSD, true); -} - -/*! @fn static void MLST(ftp_session_t *session, const char *args) - * - * @brief set transfer mode - * - * @param[in] session ftp session - * @param[in] args arguments - */ -FTP_DECLARE(MLST) -{ - struct stat st; - int rc; - char *path; - size_t len; - - console_print(CYAN "%s %s\n" RESET, __func__, args ? args : ""); - - ftp_session_set_state(session, COMMAND_STATE, 0); - - /* build the path */ - if(build_path(session, session->cwd, args) != 0) - { - ftp_send_response(session, 501, "%s\r\n", strerror(errno)); - return; - } - - /* stat path */ - rc = lstat(session->buffer, &st); - if(rc != 0) - { - ftp_send_response(session, 550, "%s\r\n", strerror(errno)); - return; - } - - /* encode \n in path */ - len = session->buffersize; - path = encode_path(session->buffer, &len, true); - if(!path) - { - ftp_send_response(session, 550, "%s\r\n", strerror(ENOMEM)); - return; - } - - session->dir_mode = XFER_DIR_MLST; - rc = ftp_session_fill_dirent(session, &st, path, len); - free(path); - if(rc != 0) - { - ftp_send_response(session, 550, "%s\r\n", strerror(errno)); - return; - } - - path = malloc(session->buffersize + 1); - if(!path) - { - ftp_send_response(session, 550, "%s\r\n", strerror(ENOMEM)); - return; - } - - memcpy(path, session->buffer, session->buffersize); - path[session->buffersize] = 0; - ftp_send_response(session, -250, "Status\r\n%s250 End\r\n", path); - free(path); -} - -/*! @fn static void MODE(ftp_session_t *session, const char *args) - * - * @brief set transfer mode - * - * @param[in] session ftp session - * @param[in] args arguments - */ -FTP_DECLARE(MODE) -{ - console_print(CYAN "%s %s\n" RESET, __func__, args ? args : ""); - - ftp_session_set_state(session, COMMAND_STATE, 0); - - /* we only accept S (stream) mode */ - if(strcasecmp(args, "S") == 0) - { - ftp_send_response(session, 200, "OK\r\n"); - return; - } - - ftp_send_response(session, 504, "unavailable\r\n"); -} - -/*! @fn static void NLST(ftp_session_t *session, const char *args) - * - * @brief retrieve a name list - * - * @note Requires a PASV or PORT connection - * - * @param[in] session ftp session - * @param[in] args arguments - */ -FTP_DECLARE(NLST) -{ - console_print(CYAN "%s %s\n" RESET, __func__, args ? args : ""); - - /* open the path in NLST mode */ - return ftp_xfer_dir(session, args, XFER_DIR_NLST, false); -} - -/*! @fn static void NOOP(ftp_session_t *session, const char *args) - * - * @brief no-op - * - * @param[in] session ftp session - * @param[in] args arguments - */ -FTP_DECLARE(NOOP) -{ - console_print(CYAN "%s %s\n" RESET, __func__, args ? args : ""); - - /* this is a no-op */ - ftp_send_response(session, 200, "OK\r\n"); -} - -/*! @fn static void OPTS(ftp_session_t *session, const char *args) - * - * @brief set options - * - * @param[in] session ftp session - * @param[in] args arguments - */ -FTP_DECLARE(OPTS) -{ - console_print(CYAN "%s %s\n" RESET, __func__, args ? args : ""); - - ftp_session_set_state(session, COMMAND_STATE, 0); - - /* we accept the following UTF8 options */ - if(strcasecmp(args, "UTF8") == 0 - || strcasecmp(args, "UTF8 ON") == 0 - || strcasecmp(args, "UTF8 NLST") == 0) - { - ftp_send_response(session, 200, "OK\r\n"); - return; - } - - /* check MLST options */ - if(strncasecmp(args, "MLST ", 5) == 0) - { - static const struct - { - const char *name; - session_mlst_flags_t flag; - } mlst_flags[] = - { - { "Type;", SESSION_MLST_TYPE, }, - { "Size;", SESSION_MLST_SIZE, }, - { "Modify;", SESSION_MLST_MODIFY, }, - { "Perm;", SESSION_MLST_PERM, }, - { "UNIX.mode;", SESSION_MLST_UNIX_MODE, }, - }; - static const size_t num_mlst_flags = sizeof(mlst_flags)/sizeof(mlst_flags[0]); - - session_mlst_flags_t flags = 0; - args += 5; - const char *p = args; - while(*p) - { - for(size_t i = 0; i < num_mlst_flags; ++i) - { - if(strncasecmp(mlst_flags[i].name, p, strlen(mlst_flags[i].name)) == 0) - { - flags |= mlst_flags[i].flag; - p += strlen(mlst_flags[i].name)-1; - break; - } - } - - while(*p && *p != ';') - ++p; - - if(*p == ';') - ++p; - } - - session->mlst_flags = flags; - ftp_send_response(session, 200, "MLST OPTS%s%s%s%s%s%s\r\n", - flags ? " " : "", - flags & SESSION_MLST_TYPE ? "Type;" : "", - flags & SESSION_MLST_SIZE ? "Size;" : "", - flags & SESSION_MLST_MODIFY ? "Modify;" : "", - flags & SESSION_MLST_PERM ? "Perm;" : "", - flags & SESSION_MLST_UNIX_MODE ? "UNIX.mode;" : ""); - return; - } - - ftp_send_response(session, 504, "invalid argument\r\n"); -} - -/*! @fn static void PASS(ftp_session_t *session, const char *args) - * - * @brief provide password - * - * @param[in] session ftp session - * @param[in] args arguments - */ -FTP_DECLARE(PASS) -{ - console_print(CYAN "%s %s\n" RESET, __func__, args ? args : ""); - - /* we accept any password */ - ftp_session_set_state(session, COMMAND_STATE, 0); - - ftp_send_response(session, 230, "OK\r\n"); -} - -/*! @fn static void PASV(ftp_session_t *session, const char *args) - * - * @brief request an address to connect to - * - * @param[in] session ftp session - * @param[in] args arguments - */ -FTP_DECLARE(PASV) -{ - int rc; - char buffer[INET_ADDRSTRLEN + 10]; - char *p; - in_port_t port; - - console_print(CYAN "%s %s\n" RESET, __func__, args ? args : ""); - - memset(buffer, 0, sizeof(buffer)); - - /* reset the state */ - ftp_session_set_state(session, COMMAND_STATE, CLOSE_PASV | CLOSE_DATA); - session->flags &= ~(SESSION_PASV|SESSION_PORT); - - /* create a socket to listen on */ - session->pasv_fd = socket(AF_INET, SOCK_STREAM, 0); - if(session->pasv_fd < 0) - { - console_print(RED "socket: %d %s\n" RESET, errno, strerror(errno)); - ftp_send_response(session, 451, "\r\n"); - return; - } - - /* set the socket options */ - rc = ftp_set_socket_options(session->pasv_fd); - if(rc != 0) - { - /* failed to set socket options */ - ftp_session_close_pasv(session); - ftp_send_response(session, 451, "\r\n"); - return; - } - - /* grab a new port */ - session->pasv_addr.sin_port = htons(next_data_port()); - -#if defined(_3DS) || defined(__SWITCH__) - console_print(YELLOW "binding to %s:%u\n" RESET, - inet_ntoa(session->pasv_addr.sin_addr), - ntohs(session->pasv_addr.sin_port)); -#endif - - /* bind to the port */ - rc = bind(session->pasv_fd, (struct sockaddr*)&session->pasv_addr, - sizeof(session->pasv_addr)); - if(rc != 0) - { - /* failed to bind */ - console_print(RED "bind: %d %s\n" RESET, errno, strerror(errno)); - ftp_session_close_pasv(session); - ftp_send_response(session, 451, "\r\n"); - return; - } - - /* listen on the socket */ - rc = listen(session->pasv_fd, 1); - if(rc != 0) - { - /* failed to listen */ - console_print(RED "listen: %d %s\n" RESET, errno, strerror(errno)); - ftp_session_close_pasv(session); - ftp_send_response(session, 451, "\r\n"); - return; - } - -#ifndef _3DS - { - /* get the socket address since we requested an ephemeral port */ - socklen_t addrlen = sizeof(session->pasv_addr); - rc = getsockname(session->pasv_fd, (struct sockaddr*)&session->pasv_addr, - &addrlen); - if(rc != 0) - { - /* failed to get socket address */ - console_print(RED "getsockname: %d %s\n" RESET, errno, strerror(errno)); - ftp_session_close_pasv(session); - ftp_send_response(session, 451, "\r\n"); - return; - } - } -#endif - - /* we are now listening on the socket */ - console_print(YELLOW "listening on %s:%u\n" RESET, - inet_ntoa(session->pasv_addr.sin_addr), - ntohs(session->pasv_addr.sin_port)); - session->flags |= SESSION_PASV; - - /* print the address in the ftp format */ - port = ntohs(session->pasv_addr.sin_port); - strcpy(buffer, inet_ntoa(session->pasv_addr.sin_addr)); - sprintf(buffer+strlen(buffer), ",%u,%u", - port >> 8, port & 0xFF); - for(p = buffer; *p; ++p) - { - if(*p == '.') - *p = ','; - } - - ftp_send_response(session, 227, "%s\r\n", buffer); -} - -/*! @fn static void PORT(ftp_session_t *session, const char *args) - * - * @brief provide an address for the server to connect to - * - * @param[in] session ftp session - * @param[in] args arguments - */ -FTP_DECLARE(PORT) -{ - char *addrstr, *p, *portstr; - int commas = 0, rc; - short port = 0; - unsigned long val; - struct sockaddr_in addr; - - console_print(CYAN "%s %s\n" RESET, __func__, args ? args : ""); - - /* reset the state */ - ftp_session_set_state(session, COMMAND_STATE, CLOSE_PASV | CLOSE_DATA); - session->flags &= ~(SESSION_PASV|SESSION_PORT); - - /* dup the args since they are const and we need to change it */ - addrstr = strdup(args); - if(addrstr == NULL) - { - ftp_send_response(session, 425, "%s\r\n", strerror(ENOMEM)); - return; - } - - /* replace a,b,c,d,e,f with a.b.c.d\0e.f */ - for(p = addrstr; *p; ++p) - { - if(*p == ',') - { - if(commas != 3) - *p = '.'; - else - { - *p = 0; - portstr = p+1; - } - ++commas; - } - } - - /* make sure we got the right number of values */ - if(commas != 5) - { - free(addrstr); - ftp_send_response(session, 501, "%s\r\n", strerror(EINVAL)); - return; - } - - /* parse the address */ - rc = inet_aton(addrstr, &addr.sin_addr); - if(rc == 0) - { - free(addrstr); - ftp_send_response(session, 501, "%s\r\n", strerror(EINVAL)); - return; - } - - /* parse the port */ - val = 0; - port = 0; - for(p = portstr; *p; ++p) - { - if(!isdigit((int)*p)) - { - if(p == portstr || *p != '.' || val > 0xFF) - { - free(addrstr); - ftp_send_response(session, 501, "%s\r\n", strerror(EINVAL)); - return; - } - port <<= 8; - port += val; - val = 0; - } - else - { - val *= 10; - val += *p - '0'; - } - } - - /* validate the port */ - if(val > 0xFF || port > 0xFF) - { - free(addrstr); - ftp_send_response(session, 501, "%s\r\n", strerror(EINVAL)); - return; - } - port <<= 8; - port += val; - - /* fill in the address port and family */ - addr.sin_family = AF_INET; - addr.sin_port = htons(port); - - free(addrstr); - - memcpy(&session->peer_addr, &addr, sizeof(addr)); - - /* we are ready to connect to the client */ - session->flags |= SESSION_PORT; - ftp_send_response(session, 200, "OK\r\n"); -} - -/*! @fn static void PWD(ftp_session_t *session, const char *args) - * - * @brief print working directory - * - * @param[in] session ftp session - * @param[in] args arguments - */ -FTP_DECLARE(PWD) -{ - static char buffer[CMD_BUFFERSIZE]; - size_t len = sizeof(buffer), i; - char *path; - - console_print(CYAN "%s %s\n" RESET, __func__, args ? args : ""); - - ftp_session_set_state(session, COMMAND_STATE, 0); - - /* encode the cwd */ - len = strlen(session->cwd); - path = encode_path(session->cwd, &len, true); - if(path != NULL) - { - i = sprintf(buffer, "257 \""); - if(i + len + 3 > sizeof(buffer)) - { - /* buffer will overflow */ - free(path); - ftp_session_set_state(session, COMMAND_STATE, CLOSE_PASV | CLOSE_DATA); - ftp_send_response(session, 550, "unavailable\r\n"); - ftp_send_response(session, 425, "%s\r\n", strerror(EOVERFLOW)); - return; - } - memcpy(buffer+i, path, len); - free(path); - len += i; - buffer[len++] = '"'; - buffer[len++] = '\r'; - buffer[len++] = '\n'; - - ftp_send_response_buffer(session, buffer, len); - return; - } - - ftp_send_response(session, 425, "%s\r\n", strerror(ENOMEM)); -} - -/*! @fn static void QUIT(ftp_session_t *session, const char *args) - * - * @brief terminate ftp session - * - * @param[in] session ftp session - * @param[in] args arguments - */ -FTP_DECLARE(QUIT) -{ - console_print(CYAN "%s %s\n" RESET, __func__, args ? args : ""); - - /* disconnect from the client */ - ftp_send_response(session, 221, "disconnecting\r\n"); - ftp_session_close_cmd(session); -} - -/*! @fn static void REST(ftp_session_t *session, const char *args) - * - * @brief restart a transfer - * - * @note sets file position for a subsequent STOR operation - * - * @param[in] session ftp session - * @param[in] args arguments - */ -FTP_DECLARE(REST) -{ - const char *p; - uint64_t pos = 0; - - console_print(CYAN "%s %s\n" RESET, __func__, args ? args : ""); - - ftp_session_set_state(session, COMMAND_STATE, 0); - - /* make sure an argument is provided */ - if(args == NULL) - { - ftp_send_response(session, 504, "invalid argument\r\n"); - return; - } - - /* parse the offset */ - for(p = args; *p; ++p) - { - if(!isdigit((int)*p)) - { - ftp_send_response(session, 504, "invalid argument\r\n"); - return; - } - - if(UINT64_MAX / 10 < pos) - { - ftp_send_response(session, 504, "invalid argument\r\n"); - return; - } - - pos *= 10; - - if(UINT64_MAX - (*p - '0') < pos) - { - ftp_send_response(session, 504, "invalid argument\r\n"); - return; - } - - pos += (*p - '0'); - } - - /* set the restart offset */ - session->filepos = pos; - ftp_send_response(session, 200, "OK\r\n"); -} - -/*! @fn static void RETR(ftp_session_t *session, const char *args) - * - * @brief retrieve a file - * - * @note Requires a PASV or PORT connection - * - * @param[in] session ftp session - * @param[in] args arguments - */ -FTP_DECLARE(RETR) -{ - console_print(CYAN "%s %s\n" RESET, __func__, args ? args : ""); - - /* open the file to retrieve */ - return ftp_xfer_file(session, args, XFER_FILE_RETR); -} - -/*! @fn static void RMD(ftp_session_t *session, const char *args) - * - * @brief remove a directory - * - * @param[in] session ftp session - * @param[in] args arguments - */ -FTP_DECLARE(RMD) -{ - int rc; - - console_print(CYAN "%s %s\n" RESET, __func__, args ? args : ""); - - ftp_session_set_state(session, COMMAND_STATE, 0); - - /* build the path to remove */ - if(build_path(session, session->cwd, args) != 0) - { - ftp_send_response(session, 553, "%s\r\n", strerror(errno)); - return; - } - - /* remove the directory */ - rc = rmdir(session->buffer); - if(rc != 0) - { - /* rmdir error */ - console_print(RED "rmdir: %d %s\n" RESET, errno, strerror(errno)); - ftp_send_response(session, 550, "failed to delete directory\r\n"); - return; - } - - update_free_space(); - ftp_send_response(session, 250, "OK\r\n"); -} - -/*! @fn static void RNFR(ftp_session_t *session, const char *args) - * - * @brief rename from - * - * @note Must be followed by RNTO - * - * @param[in] session ftp session - * @param[in] args arguments - */ -FTP_DECLARE(RNFR) -{ - int rc; - struct stat st; - console_print(CYAN "%s %s\n" RESET, __func__, args ? args : ""); - - ftp_session_set_state(session, COMMAND_STATE, 0); - - /* build the path to rename from */ - if(build_path(session, session->cwd, args) != 0) - { - ftp_send_response(session, 553, "%s\r\n", strerror(errno)); - return; - } - - /* make sure the path exists */ - rc = lstat(session->buffer, &st); - if(rc != 0) - { - /* error getting path status */ - console_print(RED "lstat: %d %s\n" RESET, errno, strerror(errno)); - ftp_send_response(session, 450, "no such file or directory\r\n"); - return; - } - - /* we are ready for RNTO */ - session->flags |= SESSION_RENAME; - ftp_send_response(session, 350, "OK\r\n"); -} - -/*! @fn static void RNTO(ftp_session_t *session, const char *args) - * - * @brief rename to - * - * @note Must be preceded by RNFR - * - * @param[in] session ftp session - * @param[in] args arguments - */ -FTP_DECLARE(RNTO) -{ - static char rnfr[XFER_BUFFERSIZE]; // rename-from buffer - int rc; - - console_print(CYAN "%s %s\n" RESET, __func__, args ? args : ""); - - ftp_session_set_state(session, COMMAND_STATE, 0); - - /* make sure the previous command was RNFR */ - if(!(session->flags & SESSION_RENAME)) - { - ftp_send_response(session, 503, "Bad sequence of commands\r\n"); - return; - } - - /* clear the rename state */ - session->flags &= ~SESSION_RENAME; - - /* copy the RNFR path */ - memcpy(rnfr, session->buffer, XFER_BUFFERSIZE); - - /* build the path to rename to */ - if(build_path(session, session->cwd, args) != 0) - { - ftp_send_response(session, 554, "%s\r\n", strerror(errno)); - return; - } - - /* rename the file */ - rc = rename(rnfr, session->buffer); - if(rc != 0) - { - /* rename failure */ - console_print(RED "rename: %d %s\n" RESET, errno, strerror(errno)); - ftp_send_response(session, 550, "failed to rename file/directory\r\n"); - return; - } - - update_free_space(); - ftp_send_response(session, 250, "OK\r\n"); -} - -/*! @fn static void SIZE(ftp_session_t *session, const char *args) - * - * @brief get file size - * - * @param[in] session ftp session - * @param[in] args arguments - */ -FTP_DECLARE(SIZE) -{ - int rc; - struct stat st; - - console_print(CYAN "%s %s\n" RESET, __func__, args ? args : ""); - - ftp_session_set_state(session, COMMAND_STATE, 0); - - /* build the path to stat */ - if(build_path(session, session->cwd, args) != 0) - { - ftp_send_response(session, 553, "%s\r\n", strerror(errno)); - return; - } - - rc = stat(session->buffer, &st); - if(rc != 0 || !S_ISREG(st.st_mode)) - { - ftp_send_response(session, 550, "Could not get file size.\r\n"); - return; - } - - ftp_send_response(session, 213, "%" PRIu64 "\r\n", - (uint64_t)st.st_size); -} - -/*! @fn static void STAT(ftp_session_t *session, const char *args) - * - * @brief get status - * - * @note If no argument is supplied, and a transfer is occurring, get the - * current transfer status. If no argument is supplied, and no transfer - * is occurring, get the server status. If an argument is supplied, this - * is equivalent to LIST, except the data is sent over the command - * socket. - * - * @param[in] session ftp session - * @param[in] args arguments - */ -FTP_DECLARE(STAT) -{ - time_t uptime = time(NULL) - start_time; - int hours = uptime / 3600; - int minutes = (uptime / 60) % 60; - int seconds = uptime % 60; - - console_print(CYAN "%s %s\n" RESET, __func__, args ? args : ""); - - if(session->state == DATA_CONNECT_STATE) - { - /* we are waiting to connect to the client */ - ftp_send_response(session, -211, "FTP server status\r\n" - " Waiting for data connection\r\n" - "211 End\r\n"); - return; - } - else if(session->state == DATA_TRANSFER_STATE) - { - /* we are in the middle of a transfer */ - ftp_send_response(session, -211, "FTP server status\r\n" - " Transferred %" PRIu64 " bytes\r\n" - "211 End\r\n", - session->filepos); - return; - } - - if(strlen(args) == 0) - { - /* no argument provided, send the server status */ - ftp_send_response(session, -211, "FTP server status\r\n" - " Uptime: %02d:%02d:%02d\r\n" - "211 End\r\n", - hours, minutes, seconds); - return; - } - - /* argument provided, open the path in STAT mode */ - ftp_xfer_dir(session, args, XFER_DIR_STAT, false); -} - -/*! @fn static void STOR(ftp_session_t *session, const char *args) - * - * @brief store a file - * - * @note Requires a PASV or PORT connection - * - * @param[in] session ftp session - * @param[in] args arguments - */ -FTP_DECLARE(STOR) -{ - console_print(CYAN "%s %s\n" RESET, __func__, args ? args : ""); - - /* open the file to store */ - return ftp_xfer_file(session, args, XFER_FILE_STOR); -} - -/*! @fn static void STOU(ftp_session_t *session, const char *args) - * - * @brief store a unique file - * - * @param[in] session ftp session - * @param[in] args arguments - */ -FTP_DECLARE(STOU) -{ - console_print(CYAN "%s %s\n" RESET, __func__, args ? args : ""); - - /* we do not support this yet */ - ftp_session_set_state(session, COMMAND_STATE, 0); - - ftp_send_response(session, 502, "unavailable\r\n"); -} - -/*! @fn static void STRU(ftp_session_t *session, const char *args) - * - * @brief set file structure - * - * @param[in] session ftp session - * @param[in] args arguments - */ -FTP_DECLARE(STRU) -{ - console_print(CYAN "%s %s\n" RESET, __func__, args ? args : ""); - - ftp_session_set_state(session, COMMAND_STATE, 0); - - /* we only support F (no structure) mode */ - if(strcasecmp(args, "F") == 0) - { - ftp_send_response(session, 200, "OK\r\n"); - return; - } - - ftp_send_response(session, 504, "unavailable\r\n"); -} - -/*! @fn static void SYST(ftp_session_t *session, const char *args) - * - * @brief identify system - * - * @param[in] session ftp session - * @param[in] args arguments - */ -FTP_DECLARE(SYST) -{ - console_print(CYAN "%s %s\n" RESET, __func__, args ? args : ""); - - ftp_session_set_state(session, COMMAND_STATE, 0); - - /* we are UNIX compliant with 8-bit characters */ - ftp_send_response(session, 215, "UNIX Type: L8\r\n"); -} - -/*! @fn static void TYPE(ftp_session_t *session, const char *args) - * - * @brief set transfer mode - * - * @note transfer mode is always binary - * - * @param[in] session ftp session - * @param[in] args arguments - */ -FTP_DECLARE(TYPE) -{ - console_print(CYAN "%s %s\n" RESET, __func__, args ? args : ""); - - ftp_session_set_state(session, COMMAND_STATE, 0); - - /* we always transfer in binary mode */ - ftp_send_response(session, 200, "OK\r\n"); -} - -/*! @fn static void USER(ftp_session_t *session, const char *args) - * - * @brief provide user name - * - * @param[in] session ftp session - * @param[in] args arguments - */ -FTP_DECLARE(USER) -{ - console_print(CYAN "%s %s\n" RESET, __func__, args ? args : ""); - - ftp_session_set_state(session, COMMAND_STATE, 0); - - /* we accept any user name */ - ftp_send_response(session, 230, "OK\r\n"); -} diff --git a/source/ftpServer.cpp b/source/ftpServer.cpp new file mode 100644 index 0000000..935bc62 --- /dev/null +++ b/source/ftpServer.cpp @@ -0,0 +1,253 @@ +// ftpd is a server implementation based on the following: +// - RFC 959 (https://tools.ietf.org/html/rfc959) +// - RFC 3659 (https://tools.ietf.org/html/rfc3659) +// - suggested implementation details from https://cr.yp.to/ftp/filesystem.html +// +// Copyright (C) 2020 Michael Theall +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// 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, see . + +#include "ftpServer.h" + +#include "fs.h" + +#include "imgui.h" + +#include +#include +#include + +#include +#include +#include +#include +#include +using namespace std::chrono_literals; + +#ifndef _3DS +#define MULTITHREADED 1 +#else +#define MULTITHREADED 0 +#endif + +namespace +{ +auto const s_startTime = std::chrono::system_clock::to_time_t (std::chrono::system_clock::now ()); +platform::Mutex s_lock; +std::string s_freeSpace; +} + +/////////////////////////////////////////////////////////////////////////// +FtpServer::~FtpServer () +{ + m_quit = true; + + m_thread.join (); +} + +FtpServer::FtpServer (std::uint16_t const port_) + : m_log (Log::create ()), m_port (port_), m_quit (false) +{ + Log::bind (m_log); + + handleStartButton (); + +#if MULTITHREADED + m_thread = platform::Thread (std::bind (&FtpServer::threadFunc, this)); +#endif +} + +void FtpServer::draw () +{ +#if !MULTITHREADED + loop (); +#endif + + ImGuiIO &io = ImGui::GetIO (); + auto const width = io.DisplaySize.x; + auto const height = io.DisplaySize.y; + + ImGui::SetNextWindowPos (ImVec2 (0, 0), ImGuiCond_FirstUseEver); +#ifdef _3DS + // top screen + ImGui::SetNextWindowSize (ImVec2 (width, height / 2.0f)); +#else + ImGui::SetNextWindowSize (ImVec2 (width, height)); +#endif + ImGui::Begin (STATUS_STRING, + nullptr, + ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize); + + { + auto const lock = std::scoped_lock (m_lock); + if (!m_socket) + { + if (ImGui::Button ("Start")) + handleStartButton (); + } + else if (ImGui::Button ("Stop")) + handleStopButton (); + + if (m_socket) + { + ImGui::SameLine (); + ImGui::TextUnformatted (m_name.c_str ()); + } + } + + { + auto const lock = std::scoped_lock (s_lock); + if (!s_freeSpace.empty ()) + { + ImGui::SameLine (); + ImGui::TextUnformatted (s_freeSpace.c_str ()); + } + } + + ImGui::Separator (); + +#ifdef _3DS + ImGui::BeginChild ("Logs", ImVec2 (0, 0), false, ImGuiWindowFlags_HorizontalScrollbar); +#else + ImGui::BeginChild ("Logs", ImVec2 (0, 200), false, ImGuiWindowFlags_HorizontalScrollbar); +#endif + m_log->draw (); + ImGui::EndChild (); + +#ifdef _3DS + ImGui::End (); + + // bottom screen + ImGui::SetNextWindowSize (ImVec2 (width * 0.8f, height / 2.0f)); + ImGui::SetNextWindowPos (ImVec2 (width * 0.1f, height / 2.0f), ImGuiCond_FirstUseEver); + ImGui::Begin ("Sessions", + nullptr, + ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize); +#else + ImGui::Separator (); +#endif + + for (auto &session : m_sessions) + session->draw (); + + ImGui::End (); +} + +UniqueFtpServer FtpServer::create (std::uint16_t const port_) +{ + updateFreeSpace (); + return UniqueFtpServer (new FtpServer (port_)); +} + +void FtpServer::updateFreeSpace () +{ +#if defined(_3DS) || defined(__SWITCH__) + struct statvfs st; + if (::statvfs ("sdmc:/", &st) != 0) + return; + + auto const lock = std::scoped_lock (s_lock); + s_freeSpace = fs::printSize (static_cast (st.f_bsize) * st.f_bfree); +#endif +} + +std::time_t FtpServer::startTime () +{ + return s_startTime; +} + +void FtpServer::handleStartButton () +{ + if (m_socket) + return; + + struct sockaddr_in addr; + addr.sin_family = AF_INET; +#if defined(_3DS) || defined(__SWITCH__) + addr.sin_addr.s_addr = gethostid (); +#else + addr.sin_addr.s_addr = INADDR_ANY; +#endif + addr.sin_port = htons (m_port); + + auto socket = Socket::create (); + if (!socket) + return; + + if (m_port != 0 && !socket->setReuseAddress (true)) + return; + + if (!socket->bind (addr)) + return; + + if (!socket->listen (10)) + return; + + auto const &sockName = socket->sockName (); + auto const name = sockName.name (); + + m_name.resize (std::strlen (name) + 3 + 5); + m_name.resize (std::sprintf (&m_name[0], "[%s]:%u", name, sockName.port ())); + + Log::info ("Started server at %s\n", m_name.c_str ()); + + m_socket = std::move (socket); +} + +void FtpServer::handleStopButton () +{ + m_socket.reset (); + Log::info ("Stopped server at %s\n", m_name.c_str ()); +} + +void FtpServer::loop () +{ + { + auto const lock = std::scoped_lock (m_lock); + if (m_socket) + { + Socket::PollInfo info{*m_socket, POLLIN, 0}; + if (Socket::poll (&info, 1, 0ms) > 0) + { + auto socket = m_socket->accept (); + if (socket) + m_sessions.emplace_back (FtpSession::create (std::move (socket))); + } + } + } + + for (auto it = std::begin (m_sessions); it != std::end (m_sessions);) + { + auto const &session = *it; + if (session->dead ()) + it = m_sessions.erase (it); + else + ++it; + } + + if (!m_sessions.empty ()) + FtpSession::poll (m_sessions); +#if MULTITHREADED + else + platform::Thread::sleep (16ms); +#endif +} + +void FtpServer::threadFunc () +{ + Log::bind (m_log); + + while (!m_quit) + loop (); +} diff --git a/source/ftpSession.cpp b/source/ftpSession.cpp new file mode 100644 index 0000000..1f9239f --- /dev/null +++ b/source/ftpSession.cpp @@ -0,0 +1,2482 @@ +// ftpd is a server implementation based on the following: +// - RFC 959 (https://tools.ietf.org/html/rfc959) +// - RFC 3659 (https://tools.ietf.org/html/rfc3659) +// - suggested implementation details from https://cr.yp.to/ftp/filesystem.html +// +// Copyright (C) 2020 Michael Theall +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// 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, see . + +#include "ftpSession.h" + +#include "ftpServer.h" + +#include "log.h" + +#include "imgui.h" + +#ifdef _3DS +#include <3ds.h> +#endif + +#ifdef __SWITCH__ +#include +#endif + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +using namespace std::chrono_literals; + +#if defined(_3DS) || defined(__SWITCH__) +#define lstat stat +#endif + +namespace +{ +std::pair parseCommand (char *const buffer_, std::size_t const size_) +{ + auto const end = &buffer_[size_]; + for (auto p = buffer_; p < end; ++p) + { + if (p[0] == '\r' && p < end - 1 && p[1] == '\n') + return {p, &p[2]}; + + if (p[0] == '\n') + return {p, &p[1]}; + } + + return {nullptr, nullptr}; +} + +void decodePath (char *const buffer_, std::size_t const size_) +{ + auto const end = &buffer_[size_]; + for (auto p = buffer_; p < end; ++p) + { + // this is an encoded \n + if (*p == '\0') + *p = '\n'; + } +} + +std::string encodePath (std::string_view const buffer_, bool const quotes_ = false) +{ + // check if the buffer has \n + bool const lf = std::memchr (buffer_.data (), '\n', buffer_.size ()); + + auto end = std::end (buffer_); + + std::size_t numQuotes = 0; + if (quotes_) + { + // check for \" that needs to be encoded + auto p = buffer_.data (); + do + { + p = static_cast (std::memchr (p, '"', end - p)); + if (p) + { + ++p; + ++numQuotes; + } + } while (p); + } + + if (!lf && !numQuotes) + return std::string (buffer_); + + std::string path (buffer_.size () + numQuotes, '\0'); + auto in = buffer_.data (); + auto out = path.data (); + + while (in < end) + { + if (*in == '\n') + { + // encoded \n is \0 + *out++ = '\0'; + } + else if (quotes_ && *in == '"') + { + // encoded \" is \"\" + *out++ = '"'; + *out++ = '"'; + } + else + *out++ = *in; + ++in; + } + + return path; +} + +std::string dirName (std::string_view const path_) +{ + // remove last path component + auto const dir = std::string (path_.substr (0, path_.rfind ('/'))); + if (dir.empty ()) + return "/"; + + return dir; +} + +std::string resolvePath (std::string_view const path_) +{ + assert (!path_.empty ()); + assert (path_[0] == '/'); + + // make sure parent is a directory + struct stat st; + if (::stat (dirName (path_).c_str (), &st) != 0) + return {}; + + if (!S_ISDIR (st.st_mode)) + { + errno = ENOTDIR; + return {}; + } + + // split path components + std::vector components; + + std::size_t pos = 1; + auto next = path_.find ('/', pos); + while (next != std::string::npos) + { + if (next != pos) + components.emplace_back (path_.substr (pos, next - pos)); + pos = next + 1; + next = path_.find ('/', pos); + } + + if (pos != path_.size ()) + components.emplace_back (path_.substr (pos)); + + // collapse . and .. + auto it = std::begin (components); + while (it != std::end (components)) + { + if (*it == ".") + { + it = components.erase (it); + continue; + } + + if (*it == "..") + { + if (it != std::begin (components)) + it = components.erase (std::prev (it)); + it = components.erase (it); + continue; + } + + ++it; + } + + // join path components + std::string outPath = "/"; + for (auto const &component : components) + { + outPath += component; + outPath.push_back ('/'); + } + + if (outPath.size () > 1) + outPath.pop_back (); + + return outPath; +} + +std::string buildPath (std::string_view const cwd_, std::string_view const args_) +{ + // absolute path + if (args_[0] == '/') + return std::string (args_); + + // root directory + if (cwd_.size () == 1) + return std::string (cwd_) + std::string (args_); + + return std::string (cwd_) + '/' + std::string (args_); +} + +std::string buildResolvedPath (std::string_view const cwd_, std::string_view const args_) +{ + return resolvePath (buildPath (cwd_, args_)); +} +} + +/////////////////////////////////////////////////////////////////////////// +FtpSession::~FtpSession () +{ + m_commandSocket.reset (); + m_pasvSocket.reset (); + closeData (); +} + +FtpSession::FtpSession (UniqueSocket commandSocket_) + : m_commandSocket (std::move (commandSocket_)), + m_commandBuffer (COMMAND_BUFFERSIZE), + m_responseBuffer (RESPONSE_BUFFERSIZE), + m_xferBuffer (XFER_BUFFERSIZE), + m_pasv (false), + m_port (false), + m_recv (false), + m_send (false), + m_urgent (false), + m_mlstType (true), + m_mlstSize (true), + m_mlstModify (true), + m_mlstPerm (true), + m_mlstUnixMode (false) +{ + char buffer[32]; + std::sprintf (buffer, "Session#%p", this); + m_windowName = buffer; + + std::sprintf (buffer, "Plot#%p", this); + m_plotName = buffer; + + m_commandSocket->setNonBlocking (); + + auto const lock = std::scoped_lock (m_lock); + sendResponse ("220 Hello!\r\n"); +} + +bool FtpSession::dead () +{ + auto const lock = std::scoped_lock (m_lock); + if (m_commandSocket || m_pasvSocket || m_dataSocket) + return false; + + return true; +} + +void FtpSession::draw () +{ + auto const lock = std::scoped_lock (m_lock); + + ImGuiIO &io = ImGui::GetIO (); + auto const scale = io.DisplayFramebufferScale.y; + + ImGui::BeginChild (m_windowName.c_str (), ImVec2 (0.0f, 50.0f / scale), true); + + if (!m_workItem.empty ()) + ImGui::TextUnformatted (m_workItem.c_str ()); + else + ImGui::TextUnformatted (m_cwd.c_str ()); + + if (m_fileSize) + ImGui::Text ( + "%s/%s", fs::printSize (m_filePosition).c_str (), fs::printSize (m_fileSize).c_str ()); + else if (m_filePosition) + ImGui::Text ("%s/???", fs::printSize (m_filePosition).c_str ()); + + if (m_fileSize || m_filePosition) + { + // MiB/s plot lines + for (std::size_t i = 0; i < POSITION_HISTORY - 1; ++i) + { + m_filePositionDeltas[i] = m_filePositionHistory[i + 1] - m_filePositionHistory[i]; + m_filePositionHistory[i] = m_filePositionHistory[i + 1]; + } + + auto const diff = m_filePosition - m_filePositionHistory[POSITION_HISTORY - 1]; + m_filePositionDeltas[POSITION_HISTORY - 1] = diff; + m_filePositionHistory[POSITION_HISTORY - 1] = m_filePosition; + + if (m_xferRate == -1.0f) + { + m_xferRate = 0.0f; + m_filePositionTime = platform::steady_clock::now (); + } + else + { + auto const now = platform::steady_clock::now (); + auto const timeDiff = now - m_filePositionTime; + m_filePositionTime = now; + + auto const rate = diff / std::chrono::duration (timeDiff).count (); + auto const alpha = 0.01f; + m_xferRate = alpha * rate + (1.0f - alpha) * m_xferRate; + } + + auto const rateString = fs::printSize (m_xferRate) + "/s"; + + ImGui::SameLine (); + ImGui::PlotLines ("Rate", + m_filePositionDeltas, + IM_ARRAYSIZE (m_filePositionDeltas), + 0, + rateString.c_str ()); + } + + ImGui::EndChild (); +} + +UniqueFtpSession FtpSession::create (UniqueSocket commandSocket_) +{ + return UniqueFtpSession (new FtpSession (std::move (commandSocket_))); +} + +void FtpSession::poll (std::vector const &sessions_) +{ +#if 0 + auto const printEvents = [] (int const events_) { + std::string out; + if (events_ & POLLIN) + out += "[IN]"; + if (events_ & POLLPRI) + out += "[PRI]"; + if (events_ & POLLOUT) + out += "[OUT]"; + if (events_ & POLLHUP) + out += "[HUP]"; + if (events_ & POLLERR) + out += "[ERR]"; + + return out; + }; +#endif + + // poll for pending close sockets first + std::vector info; + for (auto &session : sessions_) + { + auto const lock = std::scoped_lock (session->m_lock); + for (auto &pending : session->m_pendingCloseSocket) + { + assert (pending.unique ()); + info.emplace_back (Socket::PollInfo{*pending, POLLIN, 0}); + } + } + + if (!info.empty ()) + { + auto const rc = Socket::poll (info.data (), info.size (), 0ms); + if (rc < 0) + Log::error ("poll: %s\n", std::strerror (errno)); + else + { + for (auto const &i : info) + { + if (!i.revents) + continue; + + for (auto &session : sessions_) + { + auto const lock = std::scoped_lock (session->m_lock); + for (auto it = std::begin (session->m_pendingCloseSocket); + it != std::end (session->m_pendingCloseSocket);) + { + auto &socket = *it; + if (&i.socket.get () != socket.get ()) + { + ++it; + continue; + } + + it = session->m_pendingCloseSocket.erase (it); + } + } + } + } + } + + // poll for everything else + info.clear (); + for (auto &session : sessions_) + { + auto const lock = std::scoped_lock (session->m_lock); + if (session->m_commandSocket) + { + info.emplace_back (Socket::PollInfo{*session->m_commandSocket, POLLIN | POLLPRI, 0}); + if (session->m_responseBuffer.usedSize () != 0) + info.back ().events |= POLLOUT; + } + + switch (session->m_state) + { + case State::COMMAND: + // we are waiting to read a command + break; + + case State::DATA_CONNECT: + if (session->m_pasv) + { + assert (!session->m_port); + // we are waiting for a PASV connection + info.emplace_back (Socket::PollInfo{*session->m_pasvSocket, POLLIN, 0}); + } + else + { + // we are waiting to complete a PORT connection + info.emplace_back (Socket::PollInfo{*session->m_dataSocket, POLLOUT, 0}); + } + break; + + case State::DATA_TRANSFER: + // we need to transfer data + if (session->m_recv) + { + assert (!session->m_send); + info.emplace_back (Socket::PollInfo{*session->m_dataSocket, POLLIN, 0}); + } + else + { + assert (session->m_send); + info.emplace_back (Socket::PollInfo{*session->m_dataSocket, POLLOUT, 0}); + } + break; + } + } + + if (info.empty ()) + return; + + // poll for activity +#if MULTITHREADED + auto const rc = Socket::poll (info.data (), info.size (), 16ms); +#else + auto const rc = Socket::poll (info.data (), info.size (), 0ms); +#endif + if (rc < 0) + { + Log::error ("poll: %s\n", std::strerror (errno)); + return; + } + + if (rc == 0) + return; + + for (auto &session : sessions_) + { + auto const lock = std::scoped_lock (session->m_lock); + + for (auto const &i : info) + { + if (!i.revents) + continue; + + // check command socket + if (&i.socket.get () == session->m_commandSocket.get ()) + { + if (i.revents & ~(POLLIN | POLLPRI | POLLOUT)) + Log::debug ("Command revents 0x%X\n", i.revents); + + if (i.revents & POLLOUT) + session->writeResponse (); + + if (i.revents & (POLLIN | POLLPRI)) + session->readCommand (i.revents); + + if (i.revents & (POLLERR | POLLHUP)) + session->m_commandSocket.reset (); + } + + // check the data socket + if (&i.socket.get () == session->m_pasvSocket.get () || + &i.socket.get () == session->m_dataSocket.get ()) + { + switch (session->m_state) + { + case State::COMMAND: + assert (false); + break; + + case State::DATA_CONNECT: + if (i.revents & ~(POLLIN | POLLPRI | POLLOUT)) + Log::debug ("Data revents 0x%X\n", i.revents); + + if (i.revents & (POLLERR | POLLHUP)) + { + session->sendResponse ("426 Data connection failed\r\n"); + session->setState (State::COMMAND, true, true); + } + else if (i.revents & POLLIN) + { + // we need to accept the PASV connection + session->dataAccept (); + } + else if (i.revents & POLLOUT) + { + // PORT connection completed + auto const &sockName = session->m_dataSocket->peerName (); + Log::info ("Connected to [%s]:%u\n", sockName.name (), sockName.port ()); + + session->sendResponse ("150 Ready\r\n"); + session->setState (State::DATA_TRANSFER, true, false); + } + break; + + case State::DATA_TRANSFER: + if (i.revents & ~(POLLIN | POLLPRI | POLLOUT)) + Log::debug ("Data revents 0x%X\n", i.revents); + + // we need to transfer data + if (i.revents & (POLLERR | POLLHUP)) + { + session->sendResponse ("426 Data connection failed\r\n"); + session->setState (State::COMMAND, true, true); + } + else if (i.revents & (POLLIN | POLLOUT)) + { + while (((*session).*(session->m_transfer)) ()) + ; + } + } + } + } + } +} + +void FtpSession::setState (State const state_, bool const closePasv_, bool const closeData_) +{ + m_state = state_; + + if (closePasv_) + m_pasvSocket.reset (); + if (closeData_) + closeData (); + + if (state_ == State::COMMAND) + { + m_restartPosition = 0; + m_fileSize = 0; + m_filePosition = 0; + + for (auto &pos : m_filePositionHistory) + pos = 0; + m_xferRate = -1.0f; + + m_workItem.clear (); + + m_file.close (); + m_dir.close (); + } +} + +void FtpSession::closeData () +{ + if (m_dataSocket && m_dataSocket.unique ()) + { + m_dataSocket->shutdown (SHUT_WR); + m_dataSocket->setLinger (true, 0s); + m_pendingCloseSocket.emplace_back (std::move (m_dataSocket)); + } + m_dataSocket.reset (); + + m_recv = false; + m_send = false; +} + +bool FtpSession::changeDir (char const *const args_) +{ + if (std::strcmp (args_, "..") == 0) + { + // cd up + auto const pos = m_cwd.find_last_of ('/'); + assert (pos != std::string::npos); + if (pos == 0) + m_cwd = "/"; + else + m_cwd = m_cwd.substr (0, pos); + return true; + } + + auto const path = buildResolvedPath (m_cwd, args_); + if (path.empty ()) + return false; + + struct stat st; + if (::stat (path.c_str (), &st) != 0) + return false; + + if (!S_ISDIR (st.st_mode)) + { + errno = ENOTDIR; + return false; + } + + m_cwd = path; + return true; +} + +bool FtpSession::dataAccept () +{ + if (!m_pasv) + { + sendResponse ("503 Bad sequence of commands\r\n"); + setState (State::COMMAND, true, true); + return false; + } + + m_pasv = false; + + m_dataSocket = m_pasvSocket->accept (); + if (!m_dataSocket) + { + sendResponse ("425 Failed to establish connection\r\n"); + setState (State::COMMAND, true, true); + return false; + } + +#ifndef _3DS + m_dataSocket->setRecvBufferSize (SOCK_BUFFERSIZE); + m_dataSocket->setSendBufferSize (SOCK_BUFFERSIZE); +#endif + + if (!m_dataSocket->setNonBlocking ()) + { + sendResponse ("425 Failed to establish connection\r\n"); + setState (State::COMMAND, true, true); + return false; + } + + // we are ready to transfer data + sendResponse ("150 Ready\r\n"); + setState (State::DATA_TRANSFER, true, false); + return true; +} + +bool FtpSession::dataConnect () +{ + assert (m_port); + + m_port = false; + + m_dataSocket = Socket::create (); + if (!m_dataSocket) + return false; + + m_dataSocket->setRecvBufferSize (SOCK_BUFFERSIZE); + m_dataSocket->setSendBufferSize (SOCK_BUFFERSIZE); + + if (!m_dataSocket->setNonBlocking ()) + return false; + + if (!m_dataSocket->connect (m_portAddr)) + { + if (errno != EINPROGRESS) + { + m_dataSocket.reset (); + return false; + } + + return true; + } + + // we are ready to transfer data + sendResponse ("150 Ready\r\n"); + setState (State::DATA_TRANSFER, true, false); + return true; +} + +int FtpSession::fillDirent (struct stat const &st_, std::string_view const path_, char const *type_) +{ + auto const buffer = m_xferBuffer.freeArea (); + auto const size = m_xferBuffer.freeSize (); + + std::size_t pos = 0; + + if (m_xferDirMode == XferDirMode::MLSD || m_xferDirMode == XferDirMode::MLST) + { + if (m_xferDirMode == XferDirMode::MLST) + { + if (pos >= size) + return EAGAIN; + buffer[pos++] = ' '; + } + + // type fact + if (m_mlstType) + { + if (!type_) + { + type_ = "???"; + if (S_ISREG (st_.st_mode)) + type_ = "file"; + else if (S_ISDIR (st_.st_mode)) + type_ = "dir"; +#if !defined(_3DS) && !defined(__SWITCH__) + else if (S_ISLNK (st_.st_mode)) + type_ = "os.unix=symlink"; + else if (S_ISCHR (st_.st_mode)) + type_ = "os.unix=character"; + else if (S_ISBLK (st_.st_mode)) + type_ = "os.unix=block"; + else if (S_ISFIFO (st_.st_mode)) + type_ = "os.unix=fifo"; + else if (S_ISSOCK (st_.st_mode)) + type_ = "os.unix=socket"; +#endif + } + + auto const rc = std::snprintf (&buffer[pos], size - pos, "Type=%s;", type_); + if (rc < 0) + return errno; + if (static_cast (rc) > size - pos) + return EAGAIN; + + pos += rc; + } + + // size fact + if (m_mlstSize) + { + auto const rc = std::snprintf (&buffer[pos], + size - pos, + "Size=%llu;", + static_cast (st_.st_size)); + if (rc < 0) + return errno; + if (static_cast (rc) > size - pos) + return EAGAIN; + + pos += rc; + } + + // mtime fact + if (m_mlstModify) + { + auto const tm = std::gmtime (&st_.st_mtime); + if (!tm) + return errno; + + auto const rc = std::strftime (&buffer[pos], size - pos, "Modify=%Y%m%d%H%M%S;", tm); + if (rc == 0) + return EAGAIN; + + pos += rc; + } + + // permission fact + if (m_mlstPerm) + { + auto const header = "Perm="; + if (size - pos < std::strlen (header)) + return EAGAIN; + + std::strcpy (&buffer[pos], header); + pos += std::strlen (header); + + // append permission + if (S_ISREG (st_.st_mode) && (st_.st_mode & S_IWUSR)) + { + if (pos >= size) + return EAGAIN; + buffer[pos++] = 'a'; + } + + // create permission + if (S_ISDIR (st_.st_mode) && (st_.st_mode & S_IWUSR)) + { + if (pos >= size) + return EAGAIN; + buffer[pos++] = 'c'; + } + + // delete permission + if (pos >= size) + return EAGAIN; + buffer[pos++] = 'd'; + + // chdir permission + if (S_ISDIR (st_.st_mode) && (st_.st_mode & S_IXUSR)) + { + if (pos >= size) + return EAGAIN; + buffer[pos++] = 'e'; + } + + // rename permission + if (pos >= size) + return EAGAIN; + buffer[pos++] = 'f'; + + // list permission + if (S_ISDIR (st_.st_mode) && (st_.st_mode & S_IRUSR)) + { + if (pos >= size) + return EAGAIN; + buffer[pos++] = 'l'; + } + + // mkdir permission + if (S_ISDIR (st_.st_mode) && (st_.st_mode & S_IWUSR)) + { + if (pos >= size) + return EAGAIN; + buffer[pos++] = 'm'; + } + + // purge permission + if (S_ISDIR (st_.st_mode) && (st_.st_mode & S_IWUSR)) + { + if (pos >= size) + return EAGAIN; + buffer[pos++] = 'p'; + } + + // read permission + if (S_ISREG (st_.st_mode) && (st_.st_mode & S_IRUSR)) + { + if (pos >= size) + return EAGAIN; + buffer[pos++] = 'r'; + } + + // write permission + if (S_ISREG (st_.st_mode) && (st_.st_mode & S_IWUSR)) + { + if (pos >= size) + return EAGAIN; + buffer[pos++] = 'w'; + } + + if (pos >= size) + return EAGAIN; + buffer[pos++] = ';'; + } + + // unix mode fact + if (m_mlstUnixMode) + { + auto const mask = S_IRWXU | S_IRWXG | S_IRWXO | S_ISVTX | S_ISGID | S_ISUID; + + auto const rc = std::snprintf (&buffer[pos], + size - pos, + "UNIX.mode=0%lo;", + static_cast (st_.st_mode & mask)); + if (rc < 0) + return errno; + if (static_cast (rc) > size - pos) + return EAGAIN; + + pos += rc; + } + + // make sure space precedes name + if (buffer[pos - 1] != ' ') + { + if (pos >= size) + return EAGAIN; + + buffer[pos++] = ' '; + } + } + else if (m_xferDirMode != XferDirMode::NLST) + { + if (m_xferDirMode == XferDirMode::STAT) + { + if (pos >= size) + return EAGAIN; + + buffer[pos++] = ' '; + } + +#ifdef _3DS + auto const owner = "3DS"; + auto const group = "3DS"; +#elif defined(__SWITCH__) + auto const owner = "Switch"; + auto const group = "Switch"; +#else + char owner[32]; + char group[32]; + std::sprintf (owner, "%d", st_.st_uid); + std::sprintf (group, "%d", st_.st_gid); +#endif + // perms nlinks owner group size + auto rc = std::snprintf (&buffer[pos], + size - pos, + "%c%c%c%c%c%c%c%c%c%c %lu %s %s %llu ", + // clang-format off + S_ISREG (st_.st_mode) ? '-' : + S_ISDIR (st_.st_mode) ? 'd' : +#if !defined(_3DS) && !defined(__SWITCH__) + S_ISLNK (st_.st_mode) ? 'l' : + S_ISCHR (st_.st_mode) ? 'c' : + S_ISBLK (st_.st_mode) ? 'b' : + S_ISFIFO (st_.st_mode) ? 'p' : + S_ISSOCK (st_.st_mode) ? 's' : +#endif + '?', + // clang-format on + st_.st_mode & S_IRUSR ? 'r' : '-', + st_.st_mode & S_IWUSR ? 'w' : '-', + st_.st_mode & S_IXUSR ? 'x' : '-', + st_.st_mode & S_IRGRP ? 'r' : '-', + st_.st_mode & S_IWGRP ? 'w' : '-', + st_.st_mode & S_IXGRP ? 'x' : '-', + st_.st_mode & S_IROTH ? 'r' : '-', + st_.st_mode & S_IWOTH ? 'w' : '-', + st_.st_mode & S_IXOTH ? 'x' : '-', + static_cast (st_.st_nlink), + owner, + group, + static_cast (st_.st_size)); + if (rc < 0) + return errno; + + if (static_cast (rc) > size - pos) + return EAGAIN; + + pos += rc; + + // timestamp + auto const tm = std::gmtime (&st_.st_mtime); + if (!tm) + return errno; + + auto fmt = "%b %e %H:%M "; + rc = std::strftime (&buffer[pos], size - pos, fmt, tm); + if (rc < 0) + return errno; + if (static_cast (rc) > size - pos) + return EAGAIN; + + pos += rc; + } + + if (size - pos < path_.size () + 2) + return EAGAIN; + + // path + std::memcpy (&buffer[pos], path_.data (), path_.size ()); + pos += path_.size (); + buffer[pos++] = '\r'; + buffer[pos++] = '\n'; + + m_xferBuffer.markUsed (pos); + + return 0; +} + +int FtpSession::fillDirent (std::string const &path_, char const *type_) +{ + struct stat st; + if (::stat (path_.c_str (), &st) != 0) + return errno; + + return fillDirent (st, encodePath (path_), type_); +} + +void FtpSession::xferFile (char const *const args_, XferFileMode const mode_) +{ + m_xferBuffer.clear (); + + // build the path of the file to transfer + auto const path = buildResolvedPath (m_cwd, args_); + if (path.empty ()) + { + sendResponse ("553 %s\r\n", std::strerror (errno)); + setState (State::COMMAND, true, true); + return; + } + + if (mode_ == XferFileMode::RETR) + { + // stat the file + struct stat st; + if (::stat (path.c_str (), &st) != 0) + { + sendResponse ("450 %s\r\n", std::strerror (errno)); + return; + } + + // open the file in read mode + if (!m_file.open (path.c_str (), "rb")) + { + sendResponse ("450 %s\r\n", std::strerror (errno)); + return; + } + + m_fileSize = st.st_size; + + m_file.setBufferSize (FILE_BUFFERSIZE); + + if (m_restartPosition != 0) + { + if (m_file.seek (m_restartPosition, SEEK_SET) != 0) + { + sendResponse ("450 %s\r\n", std::strerror (errno)); + return; + } + } + + m_filePosition = m_restartPosition; + } + else + { + auto const append = mode_ == XferFileMode::APPE; + + char const *mode = "wb"; + if (append) + mode = "ab"; + else if (m_restartPosition != 0) + mode = "r+b"; + + // open file in write mode + if (!m_file.open (path.c_str (), mode)) + { + sendResponse ("450 %s\r\n", std::strerror (errno)); + return; + } + + FtpServer::updateFreeSpace (); + + m_file.setBufferSize (FILE_BUFFERSIZE); + + // check if this had REST but not APPE + if (m_restartPosition != 0 && !append) + { + // seek to the REST offset + if (m_file.seek (m_restartPosition, SEEK_SET) != 0) + { + sendResponse ("450 %s\r\n", std::strerror (errno)); + return; + } + } + + m_filePosition = m_restartPosition; + } + + if (!m_port && !m_pasv) + { + sendResponse ("503 Bad sequence of commands\r\n"); + setState (State::COMMAND, true, true); + return; + } + + setState (State::DATA_CONNECT, false, true); + + // setup connection + if (m_port && !dataConnect ()) + { + sendResponse ("425 Can't open data connection\r\n"); + setState (State::COMMAND, true, true); + return; + } + + // set up the transfer + if (mode_ == XferFileMode::RETR) + { + m_recv = false; + m_send = true; + m_transfer = &FtpSession::retrieveTransfer; + } + else + { + m_recv = true; + m_send = false; + m_transfer = &FtpSession::storeTransfer; + } + + m_xferBuffer.clear (); + + m_workItem = path; +} + +void FtpSession::xferDir (char const *const args_, XferDirMode const mode_, bool const workaround_) +{ + // set up the transfer + m_xferDirMode = mode_; + m_recv = false; + m_send = true; + + m_xferBuffer.clear (); + + m_transfer = &FtpSession::listTransfer; + + if (std::strlen (args_) > 0) + { + // an argument was provided + auto const path = buildResolvedPath (m_cwd, args_); + if (path.empty ()) + { + sendResponse ("550 %s\r\n", std::strerror (errno)); + setState (State::COMMAND, true, true); + return; + } + + struct stat st; + if (::stat (path.c_str (), &st) != 0) + { + auto const rc = errno; + + // work around broken clients that think LIST -a/-l is valid + if (workaround_ && mode_ == XferDirMode::LIST) + { + if (args_[0] == '-' && (args_[1] == 'a' || args_[1] == 'l')) + { + char const *args = &args_[2]; + if (*args == '\0' || *args == ' ') + { + if (*args == ' ') + ++args; + + xferDir (args, mode_, false); + return; + } + } + } + + sendResponse ("550 %s\r\n", std::strerror (rc)); + setState (State::COMMAND, true, true); + return; + } + + if (S_ISDIR (st.st_mode)) + { + if (!m_dir.open (path.c_str ())) + { + sendResponse ("550 %s\r\n", std::strerror (errno)); + setState (State::COMMAND, true, true); + return; + } + + // set as lwd + m_lwd = std::move (path); + + if (mode_ == XferDirMode::MLSD && m_mlstType) + { + // send this directory as type=cdir + auto const rc = fillDirent (m_lwd, "cdir"); + if (rc != 0) + { + sendResponse ("550 %s\r\n", std::strerror (rc)); + setState (State::COMMAND, true, true); + return; + } + } + + m_workItem = m_lwd; + } + else if (mode_ == XferDirMode::MLSD) + { + // specified file instead of directory for MLSD + sendResponse ("501 %s\r\n", std::strerror (ENOTDIR)); + setState (State::COMMAND, true, true); + return; + } + else + { + std::string name; + if (mode_ == XferDirMode::NLST) + { + // NLST uses full path name + name = encodePath (path); + } + else + { + // everything else uses basename + auto const pos = path.find_last_of ('/'); + assert (pos != std::string::npos); + name = encodePath (std::string_view (path).substr (pos)); + } + + auto const rc = fillDirent (st, name); + if (rc != 0) + { + sendResponse ("550 %s\r\n", std::strerror (rc)); + setState (State::COMMAND, true, true); + return; + } + + m_workItem = path; + } + } + else if (!m_dir.open (m_cwd.c_str ())) + { + // no argument, but opening cwd failed + sendResponse ("550 %s\r\n", std::strerror (errno)); + setState (State::COMMAND, true, true); + return; + } + else + { + // set the cwd as the lwd + m_lwd = m_cwd; + + if (mode_ == XferDirMode::MLSD && m_mlstType) + { + // send this directory as type=cdir + auto const rc = fillDirent (m_lwd, "cdir"); + if (rc != 0) + { + sendResponse ("550 %s\r\n", std::strerror (rc)); + setState (State::COMMAND, true, true); + return; + } + } + + m_workItem = m_lwd; + } + + if (mode_ == XferDirMode::MLST || mode_ == XferDirMode::STAT) + { + // this is a little different; we have to send the data over the command socket + sendResponse ("213-Status\r\n"); + setState (State::DATA_TRANSFER, true, true); + m_dataSocket = m_commandSocket; + m_send = true; + return; + } + + if (!m_port && !m_pasv) + { + // Prior PORT or PASV required + sendResponse ("503 Bad sequence of commands\r\n"); + setState (State::COMMAND, true, true); + return; + } + + setState (State::DATA_CONNECT, false, true); + m_send = true; + + // setup connection + if (m_port && !dataConnect ()) + { + sendResponse ("425 Can't open data connection\r\n"); + setState (State::COMMAND, true, true); + } +} + +void FtpSession::readCommand (int const events_) +{ + // check out-of-band data + if (events_ & POLLPRI) + { + m_urgent = true; + + // check if we are at the urgent marker + auto const atMark = m_commandSocket->atMark (); + if (atMark < 0) + { + m_commandSocket.reset (); + return; + } + + if (!atMark) + { + // discard in-band data + m_commandBuffer.clear (); + m_lock.unlock (); + auto const rc = m_commandSocket->read (m_commandBuffer); + m_lock.lock (); + if (rc < 0 && errno != EWOULDBLOCK) + m_commandSocket.reset (); + + return; + } + + // retrieve the urgent data + m_commandBuffer.clear (); + m_lock.unlock (); + auto const rc = m_commandSocket->read (m_commandBuffer, true); + m_lock.lock (); + if (rc < 0) + { + // EWOULDBLOCK means out-of-band data is on the way + if (errno != EWOULDBLOCK) + m_commandSocket.reset (); + return; + } + + // reset the command buffer + m_commandBuffer.clear (); + return; + } + + if (events_ & POLLIN) + { + // prepare to receive data + if (m_commandBuffer.freeSize () == 0) + { + Log::error ("Exceeded command buffer size\n"); + m_commandSocket.reset (); + return; + } + + m_lock.unlock (); + auto const rc = m_commandSocket->read (m_commandBuffer); + m_lock.lock (); + if (rc < 0) + { + m_commandSocket.reset (); + return; + } + + if (rc == 0) + { + // peer closed connection + Log::info ("Peer closed connection\n"); + m_commandSocket.reset (); + return; + } + + if (m_urgent) + { + // look for telnet data mark + auto const buffer = m_commandBuffer.usedArea (); + auto const size = m_commandBuffer.usedSize (); + auto const mark = static_cast (std::memchr (buffer, 0xF2, size)); + if (!mark) + return; + + // ignore all data that precedes the data mark + m_commandBuffer.markFree (mark + 1 - buffer); + m_commandBuffer.coalesce (); + m_urgent = false; + } + } + + // loop through commands + while (true) + { + // must have at least enough data for the delimiter + auto const size = m_commandBuffer.usedSize (); + if (size < 1) + return; + + auto const buffer = m_commandBuffer.usedArea (); + auto const [delim, next] = parseCommand (buffer, size); + if (!next) + return; + + *delim = '\0'; + decodePath (buffer, delim - buffer); + Log::command ("%s\n", buffer); + + char const *const command = buffer; + + char *args = buffer; + while (*args && !std::isspace (*args)) + ++args; + if (*args) + *args++ = 0; + + auto const it = std::lower_bound (std::begin (handlers), + std::end (handlers), + command, + [] (auto const &lhs_, auto const &rhs_) { + return ::strcasecmp (lhs_.first.data (), rhs_) < 0; + }); + + if (it == std::end (handlers) || ::strcasecmp (it->first.data (), command) != 0) + { + std::string response = "502 Invalid command \""; + response += encodePath (command); + + if (*args) + { + response.push_back (' '); + response += encodePath (args); + } + + response += "\"\r\n"; + + sendResponse (response); + } + else if (m_state != State::COMMAND) + { + // only some commands are available during data transfer + if (::strcasecmp (command, "ABOR") != 0 && ::strcasecmp (command, "STAT") != 0 && + ::strcasecmp (command, "QUIT") != 0) + { + sendResponse ("503 Invalid command during transfer\r\n"); + setState (State::COMMAND, true, true); + m_commandSocket.reset (); + } + else + { + auto const handler = it->second; + (this->*handler) (args); + } + } + else + { + // clear rename for all commands except RNTO + if (::strcasecmp (command, "RNTO") != 0) + m_rename.clear (); + + auto const handler = it->second; + (this->*handler) (args); + } + + m_commandBuffer.markFree (next - buffer); + m_commandBuffer.coalesce (); + } +} + +void FtpSession::writeResponse () +{ + m_lock.unlock (); + auto const rc = m_commandSocket->write (m_responseBuffer); + m_lock.lock (); + if (rc <= 0) + { + m_commandSocket.reset (); + return; + } + + m_responseBuffer.coalesce (); +} + +void FtpSession::sendResponse (char const *fmt_, ...) +{ + if (!m_commandSocket) + return; + + auto const buffer = m_responseBuffer.freeArea (); + auto const size = m_responseBuffer.freeSize (); + + va_list ap; + + va_start (ap, fmt_); + Log::log (Log::RESPONSE, fmt_, ap); + va_end (ap); + + va_start (ap, fmt_); + auto const rc = std::vsnprintf (buffer, size, fmt_, ap); + va_end (ap); + + if (rc < 0) + { + Log::error ("vsnprintf: %s\n", std::strerror (errno)); + m_commandSocket.reset (); + return; + } + + if (static_cast (rc) > size) + { + Log::error ("Not enough space for response\n"); + m_commandSocket.reset (); + return; + } + + m_responseBuffer.markUsed (rc); + + // try to write data immediately + assert (m_commandSocket); + m_lock.unlock (); + auto const bytes = + m_commandSocket->write (m_responseBuffer.usedArea (), m_responseBuffer.usedSize ()); + m_lock.lock (); + if (bytes < 0 && errno != EWOULDBLOCK) + m_commandSocket.reset (); + else if (bytes > 0) + { + m_responseBuffer.markFree (bytes); + m_responseBuffer.coalesce (); + } +} + +void FtpSession::sendResponse (std::string_view const response_) +{ + if (!m_commandSocket) + return; + + Log::log (Log::RESPONSE, response_); + + auto const buffer = m_responseBuffer.freeArea (); + auto const size = m_responseBuffer.freeSize (); + + if (response_.size () > size) + { + Log::error ("Not enough space for response\n"); + m_commandSocket.reset (); + return; + } + + std::memcpy (buffer, response_.data (), response_.size ()); + m_responseBuffer.markUsed (response_.size ()); +} + +bool FtpSession::listTransfer () +{ + // check if we sent all available data + if (m_xferBuffer.empty ()) + { + m_xferBuffer.clear (); + + // check xfer dir type + int rc = 226; + if (m_xferDirMode == XferDirMode::STAT) + rc = 213; + + // check if this was for a file + if (!m_dir) + { + // we already sent the file's listing + sendResponse ("%d OK\r\n", rc); + setState (State::COMMAND, true, true); + return false; + } + + // get the next directory entry + m_lock.unlock (); + auto const dent = m_dir.read (); + m_lock.lock (); + if (!dent) + { + // we have exhausted the directory listing + sendResponse ("%d OK\r\n", rc); + setState (State::COMMAND, true, true); + return false; + } + + // I think we are supposed to return entries for . and .. + if (std::strcmp (dent->d_name, ".") == 0 || std::strcmp (dent->d_name, "..") == 0) + return true; + + // check if this was NLST + if (m_xferDirMode == XferDirMode::NLST) + { + // NLST gives the whole path name + auto const path = encodePath (buildPath (m_lwd, dent->d_name)); + if (m_xferBuffer.freeSize () < path.size ()) + { + sendResponse ("501 %s\r\n", std::strerror (ENOMEM)); + setState (State::COMMAND, true, true); + return false; + } + } + else + { + // build the path + auto const fullPath = buildPath (m_lwd, dent->d_name); + struct stat st; + +#ifdef _3DS + // the sdmc directory entry already has the type and size, so no need to do a slow stat + auto const dp = static_cast

(m_dir); + auto const magic = *reinterpret_cast (dp->dirData->dirStruct); + + if (magic == SDMC_DIRITER_MAGIC) + { + auto const dir = reinterpret_cast (dp->dirData->dirStruct); + auto const entry = &dir->entry_data[dir->index]; + + if (entry->attributes & FS_ATTRIBUTE_DIRECTORY) + st.st_mode = S_IFDIR | S_IRUSR | S_IRGRP | S_IROTH; + else + st.st_mode = S_IFREG | S_IRUSR | S_IRGRP | S_IROTH; + + if (!(entry->attributes & FS_ATTRIBUTE_READ_ONLY)) + st.st_mode |= S_IWUSR | S_IWGRP | S_IWOTH; + + st.st_size = entry->fileSize; + st.st_mtime = 0; + + bool getmtime = true; + if (m_xferDirMode == XferDirMode::MLSD || m_xferDirMode == XferDirMode::MLST) + { + if (!m_mlstModify) + getmtime = false; + } + else if (m_xferDirMode == XferDirMode::NLST) + getmtime = false; + + if (getmtime) + { + std::uint64_t mtime = 0; + auto const rc = sdmc_getmtime (fullPath.c_str (), &mtime); + if (rc != 0) + Log::error ("sdmc_getmtime %s 0x%lx\n", fullPath.c_str (), rc); + else + st.st_mtime = mtime; + } + } + else +#endif + // lstat the entry + if (::lstat (fullPath.c_str (), &st) != 0) + { + sendResponse ("550 %s\r\n", std::strerror (errno)); + setState (State::COMMAND, true, true); + return false; + } + + auto const path = encodePath (dent->d_name); + auto const rc = fillDirent (st, path); + if (rc != 0) + { + sendResponse ("425 %s\r\n", std::strerror (errno)); + setState (State::COMMAND, true, true); + return false; + } + } + } + + // send any pending data + m_lock.unlock (); + auto const rc = m_dataSocket->write (m_xferBuffer.usedArea (), m_xferBuffer.usedSize ()); + m_lock.lock (); + if (rc <= 0) + { + // error sending data + if (rc < 0 && errno == EWOULDBLOCK) + return false; + + sendResponse ("426 Connection broken during transfer\r\n"); + setState (State::COMMAND, true, true); + return false; + } + + // we can try to send more data + m_xferBuffer.markFree (rc); + return true; +} + +bool FtpSession::retrieveTransfer () +{ + if (m_xferBuffer.empty ()) + { + m_xferBuffer.clear (); + + auto const buffer = m_xferBuffer.freeArea (); + auto const size = m_xferBuffer.freeSize (); + + // we have sent all the data, so read some more + m_lock.unlock (); + auto const rc = m_file.read (buffer, size); + m_lock.lock (); + if (rc < 0) + { + // failed to read data + sendResponse ("451 %s\r\n", std::strerror (errno)); + setState (State::COMMAND, true, true); + return false; + } + + if (rc == 0) + { + // reached end of file + sendResponse ("226 OK\r\n"); + setState (State::COMMAND, true, true); + return false; + } + + // we read some data + m_xferBuffer.markUsed (rc); + } + + // send any pending data + m_lock.unlock (); + auto const rc = m_dataSocket->write (m_xferBuffer.usedArea (), m_xferBuffer.usedSize ()); + m_lock.lock (); + if (rc <= 0) + { + // error sending data + if (rc < 0 && errno == EWOULDBLOCK) + return false; + + sendResponse ("426 Connection broken during transfer\r\n"); + setState (State::COMMAND, true, true); + return false; + } + + // we can try to read/send more data + m_filePosition += rc; + m_xferBuffer.markFree (rc); + return true; +} + +bool FtpSession::storeTransfer () +{ + if (m_xferBuffer.empty ()) + { + m_xferBuffer.clear (); + + auto const buffer = m_xferBuffer.freeArea (); + auto const size = m_xferBuffer.freeSize (); + + // we have written all the received data, so try to get some more + m_lock.unlock (); + auto const rc = m_dataSocket->read (buffer, size); + m_lock.lock (); + if (rc < 0) + { + // failed to read data + if (errno == EWOULDBLOCK) + return false; + + sendResponse ("451 %s\r\n", std::strerror (errno)); + setState (State::COMMAND, true, true); + return false; + } + + if (rc == 0) + { + // reached end of file + sendResponse ("226 OK\r\n"); + setState (State::COMMAND, true, true); + return false; + } + + // we received some data + m_xferBuffer.markUsed (rc); + } + + // write any pending data + m_lock.unlock (); + auto const rc = m_file.write (m_xferBuffer.usedArea (), m_xferBuffer.usedSize ()); + m_lock.lock (); + if (rc <= 0) + { + // error writing data + sendResponse ("426 %s\r\n", rc < 0 ? std::strerror (errno) : "Failed to write data"); + setState (State::COMMAND, true, true); + return false; + } + + // we can try to recv/write more data + m_filePosition += rc; + m_xferBuffer.markFree (rc); + return true; +} + +/////////////////////////////////////////////////////////////////////////// +void FtpSession::ABOR (char const *args_) +{ + if (m_state == State::COMMAND) + { + sendResponse ("225 No transfer to abort\r\n"); + return; + } + + // abort the transfer + sendResponse ("225 Aborted\r\n"); + sendResponse ("425 Transfer aborted\r\n"); + setState (State::COMMAND, true, true); +} + +void FtpSession::ALLO (char const *args_) +{ + sendResponse ("202 Superfluous command\r\n"); + setState (State::COMMAND, false, false); +} + +void FtpSession::APPE (char const *args_) +{ + // open the file in append mode + xferFile (args_, XferFileMode::APPE); +} + +void FtpSession::CDUP (char const *args_) +{ + setState (State::COMMAND, false, false); + + if (!changeDir ("..")) + { + sendResponse ("550 %s\r\n", std::strerror (errno)); + return; + } + + sendResponse ("200 OK\r\n"); +} + +void FtpSession::CWD (char const *args_) +{ + setState (State::COMMAND, false, false); + + if (!changeDir (args_)) + { + sendResponse ("550 %s\r\n", std::strerror (errno)); + return; + } + + sendResponse ("200 OK\r\n"); +} + +void FtpSession::DELE (char const *args_) +{ + setState (State::COMMAND, false, false); + + // build the path to remove + auto const path = buildResolvedPath (m_cwd, args_); + if (path.empty ()) + { + sendResponse ("553 %s\r\n", std::strerror (errno)); + return; + } + + // unlink the path + if (::unlink (path.c_str ()) != 0) + { + sendResponse ("550 %s\r\n", std::strerror (errno)); + return; + } + + FtpServer::updateFreeSpace (); + sendResponse ("250 OK\r\n"); +} +void FtpSession::FEAT (char const *args_) +{ + setState (State::COMMAND, false, false); + sendResponse ("211-\r\n" + " MDTM\r\n" + " MLST Type%s;Size%s;Modify%s;Perm%s;UNIX.mode%s;\r\n" + " PASV\r\n" + " SIZE\r\n" + " TVFS\r\n" + " UTF8\r\n" + "\r\n" + "211 End\r\n", + m_mlstType ? "*" : "", + m_mlstSize ? "*" : "", + m_mlstModify ? "*" : "", + m_mlstPerm ? "*" : "", + m_mlstUnixMode ? "*" : ""); +} + +void FtpSession::HELP (char const *args_) +{ + setState (State::COMMAND, false, false); + sendResponse ("214-\r\n" + "The following commands are recognized\r\n" + " ABOR ALLO APPE CDUP CWD DELE FEAT HELP LIST MDTM MKD MLSD MLST MODE\r\n" + " NLST NOOP OPTS PASS PASV PORT PWD QUIT REST RETR RMD RNFR RNTO STAT\r\n" + " STOR STOU STRU SYST TYPE USER XCUP XCWD XMKD XPWD XRMD\r\n" + "214 End\r\n"); +} + +void FtpSession::LIST (char const *args_) +{ + // open the path in LIST mode + xferDir (args_, XferDirMode::LIST, true); +} + +void FtpSession::MDTM (char const *args_) +{ + setState (State::COMMAND, false, false); + sendResponse ("502 Command not implemented\r\n"); +} + +void FtpSession::MKD (char const *args_) +{ + setState (State::COMMAND, false, false); + + // build the path to create + auto const path = buildResolvedPath (m_cwd, args_); + if (path.empty ()) + { + sendResponse ("553 %s\r\n", std::strerror (errno)); + return; + } + + // create the directory + if (::mkdir (path.c_str (), 0755) != 0) + { + sendResponse ("550 %s\r\n", std::strerror (errno)); + return; + } + + FtpServer::updateFreeSpace (); + sendResponse ("250 OK\r\n"); +} + +void FtpSession::MLSD (char const *args_) +{ + // open the path in MLSD mode + xferDir (args_, XferDirMode::MLSD, true); +} + +void FtpSession::MLST (char const *args_) +{ + setState (State::COMMAND, false, false); + + // build the path to list + auto const path = buildResolvedPath (m_cwd, args_); + if (path.empty ()) + { + sendResponse ("501 %s\r\n", std::strerror (errno)); + return; + } + + // stat path + struct stat st; + if (::lstat (path.c_str (), &st) != 0) + { + sendResponse ("550 %s\r\n", std::strerror (errno)); + return; + } + + // encode path + auto const encodedPath = encodePath (path); + + m_xferDirMode = XferDirMode::MLST; + auto const rc = fillDirent (st, path); + if (rc != 0) + { + sendResponse ("550 %s\r\n", std::strerror (rc)); + return; + } + + sendResponse ("250-Status\r\n" + " %s\r\n" + "250 End\r\n", + encodedPath.c_str ()); +} + +void FtpSession::MODE (char const *args_) +{ + setState (State::COMMAND, false, false); + + // we only accept S (stream) mode + if (::strcasecmp (args_, "S") == 0) + { + sendResponse ("200 OK\r\n"); + return; + } + + sendResponse ("504 Unavailable\r\n"); +} + +void FtpSession::NLST (char const *args_) +{ + // open the path in NLST mode + xferDir (args_, XferDirMode::NLST, false); +} + +void FtpSession::NOOP (char const *args_) +{ + sendResponse ("200 OK\r\n"); +} + +void FtpSession::OPTS (char const *args_) +{ + setState (State::COMMAND, false, false); + + // check UTF8 options + if (::strcasecmp (args_, "UTF8") == 0 || ::strcasecmp (args_, "UTF8 ON") == 0 || + ::strcasecmp (args_, "UTF8 NLST") == 0) + { + sendResponse ("200 OK\r\n"); + return; + } + + // check MLST options + if (::strncasecmp (args_, "MLST ", 5) == 0) + { + m_mlstType = false; + m_mlstSize = false; + m_mlstModify = false; + m_mlstPerm = false; + m_mlstUnixMode = false; + + auto p = args_ + 5; + while (*p) + { + auto const match = [] (auto const &name_, auto const &arg_) { + return ::strncasecmp (name_, arg_, std::strlen (name_)) == 0; + }; + + if (match ("Type;", p)) + m_mlstType = true; + else if (match ("Size;", p)) + m_mlstSize = true; + else if (match ("Modify;", p)) + m_mlstModify = true; + else if (match ("Perm;", p)) + m_mlstPerm = true; + else if (match ("UNIX.mode;", p)) + m_mlstUnixMode = true; + + p = std::strchr (p, ';'); + if (!p) + break; + + ++p; + } + + sendResponse ("200 MLST OPTS%s%s%s%s%s%s\r\n", + m_mlstType || m_mlstSize || m_mlstModify || m_mlstPerm || m_mlstUnixMode ? " " : "", + m_mlstType ? "Type;" : "", + m_mlstSize ? "Size;" : "", + m_mlstModify ? "Modify;" : "", + m_mlstPerm ? "Perm;" : "", + m_mlstUnixMode ? "UNIX.mode;" : ""); + return; + } + + sendResponse ("504 %s\r\n", std::strerror (EINVAL)); +} + +void FtpSession::PASS (char const *args_) +{ + setState (State::COMMAND, false, false); + sendResponse ("230 OK\r\n"); +} + +void FtpSession::PASV (char const *args_) +{ + // reset state + setState (State::COMMAND, true, true); + m_pasv = false; + m_port = false; + + // create a socket to listen on + m_pasvSocket = Socket::create (); + if (!m_pasvSocket) + { + sendResponse ("451 Failed to create listening socket\r\n"); + return; + } + + // set the socket options + m_pasvSocket->setRecvBufferSize (SOCK_BUFFERSIZE); + m_pasvSocket->setSendBufferSize (SOCK_BUFFERSIZE); + + // create an address to bind + struct sockaddr_in addr = m_commandSocket->sockName (); +#ifdef _3DS + static std::uint16_t ephemeralPort = 5001; + if (ephemeralPort > 10000) + ephemeralPort = 5001; + addr.sin_port = htons (ephemeralPort++); +#else + addr.sin_port = htons (0); +#endif + + // bind to the address + if (!m_pasvSocket->bind (addr)) + { + m_pasvSocket.reset (); + sendResponse ("451 Failed to bind address\r\n"); + return; + } + + // listen on the socket + if (!m_pasvSocket->listen (1)) + { + m_pasvSocket.reset (); + sendResponse ("451 Failed to listen on socket\r\n"); + return; + } + + // we are now listening on the socket + auto const &sockName = m_pasvSocket->sockName (); + std::string name = sockName.name (); + auto const port = sockName.port (); + Log::info ("Listening on [%s]:%u\n", name.c_str (), port); + + // send the address in the ftp format + for (auto &c : name) + { + if (c == '.') + c = ','; + } + + m_pasv = true; + sendResponse ("227 %s,%u,%u\r\n", name.c_str (), port >> 8, port & 0xFF); +} + +void FtpSession::PORT (char const *args_) +{ + // reset state + setState (State::COMMAND, true, true); + m_pasv = false; + m_port = false; + + std::string addrString = args_; + + // convert a,b,c,d,e,f with a.b.c.d\0e.f + unsigned commas = 0; + char const *portString = nullptr; + for (auto &p : addrString) + { + if (p == ',') + { + if (commas++ != 3) + p = '.'; + else + { + p = '\0'; + portString = &p + 1; + } + } + } + + // check for the expected number of fields + if (commas != 5) + { + sendResponse ("501 %s\r\n", std::strerror (EINVAL)); + return; + } + + struct sockaddr_in addr = {}; + + // parse the address + if (!inet_aton (addrString.data (), &addr.sin_addr)) + { + sendResponse ("501 %s\r\n", std::strerror (EINVAL)); + return; + } + + // parse the port + int val = 0; + std::uint16_t port = 0; + for (auto p = portString; *p; ++p) + { + if (!std::isdigit (*p)) + { + if (p == portString || *p != '.' || val > 0xFF) + { + sendResponse ("501 %s\r\n", std::strerror (EINVAL)); + return; + } + + port <<= 8; + port += val; + val = 0; + } + else + { + val *= 10; + val += *p - '0'; + } + } + + if (val > 0xFF || port > 0xFF) + { + sendResponse ("501 %s\r\n", std::strerror (EINVAL)); + return; + } + + port <<= 8; + port += val; + + addr.sin_family = AF_INET; + addr.sin_port = htons (port); + + // we are ready to connect to the client + m_portAddr = addr; + m_port = true; + sendResponse ("200 OK\r\n"); +} + +void FtpSession::PWD (char const *args_) +{ + setState (State::COMMAND, false, false); + + auto const path = encodePath (m_cwd); + + std::string response = "257 \""; + response += encodePath (m_cwd, true); + response += "\"\r\n"; + + sendResponse (response); +} + +void FtpSession::QUIT (char const *args_) +{ + sendResponse ("221 Disconnecting\r\n"); + m_commandSocket.reset (); +} + +void FtpSession::REST (char const *args_) +{ + setState (State::COMMAND, false, false); + + // parse the offset + std::uint64_t pos = 0; + for (auto p = args_; *p; ++p) + { + if (!std::isdigit (*p) || UINT64_MAX / 10 < pos) + { + sendResponse ("504 %s\r\n", std::strerror (errno)); + return; + } + + pos *= 10; + + if (UINT64_MAX - (*p - '0') < pos) + { + sendResponse ("504 %s\r\n", std::strerror (errno)); + return; + } + + pos += (*p - '0'); + } + + // set the restart offset + m_restartPosition = pos; + sendResponse ("200 OK\r\n"); +} + +void FtpSession::RETR (char const *args_) +{ + // open the file to retrieve + xferFile (args_, XferFileMode::RETR); +} + +void FtpSession::RMD (char const *args_) +{ + setState (State::COMMAND, false, false); + + // build the path to remove + auto const path = buildResolvedPath (m_cwd, args_); + if (path.empty ()) + { + sendResponse ("553 %s\r\n", std::strerror (errno)); + return; + } + + // remove the directory + if (::rmdir (path.c_str ()) != 0) + { + sendResponse ("550 %s\r\n", std::strerror (errno)); + return; + } + + FtpServer::updateFreeSpace (); + sendResponse ("250 OK\r\n"); +} + +void FtpSession::RNFR (char const *args_) +{ + setState (State::COMMAND, false, false); + + // build the path to rename from + auto const path = buildResolvedPath (m_cwd, args_); + if (path.empty ()) + { + sendResponse ("553 %s\r\n", std::strerror (errno)); + return; + } + + // make sure the path exists + struct stat st; + if (::lstat (path.c_str (), &st) != 0) + { + sendResponse ("450 %s\r\n", std::strerror (errno)); + return; + } + + // we are ready for RNTO + m_rename = path; + sendResponse ("350 OK\r\n"); +} + +void FtpSession::RNTO (char const *args_) +{ + setState (State::COMMAND, false, false); + + // make sure the previous command was RNFR + if (m_rename.empty ()) + { + sendResponse ("503 Bad sequence of commands\r\n"); + return; + } + + // build the path to rename to + auto const path = buildResolvedPath (m_cwd, args_); + if (path.empty ()) + { + m_rename.clear (); + sendResponse ("554 %s\r\n", std::strerror (errno)); + return; + } + + // rename the file + if (::rename (m_rename.c_str (), path.c_str ()) != 0) + { + m_rename.clear (); + sendResponse ("550 %s\r\n", std::strerror (errno)); + return; + } + + // clear the rename state + m_rename.clear (); + + FtpServer::updateFreeSpace (); + sendResponse ("250 OK\r\n"); +} + +void FtpSession::SIZE (char const *args_) +{ + setState (State::COMMAND, false, false); + + // build the path to stat + auto const path = buildResolvedPath (m_cwd, args_); + if (path.empty ()) + { + sendResponse ("553 %s\r\n", std::strerror (errno)); + return; + } + + // stat the path + struct stat st; + if (::stat (path.c_str (), &st) != 0) + { + sendResponse ("550 %s\r\n", std::strerror (errno)); + return; + } + + if (!S_ISREG (st.st_mode)) + { + sendResponse ("550 Not a file\r\n"); + return; + } + + sendResponse ("213 %" PRIu64 "\r\n", static_cast (st.st_size)); +} + +void FtpSession::STAT (char const *args_) +{ + if (m_state == State::DATA_CONNECT) + { + sendResponse ("211-FTP server status\r\n" + " Waitin for data connection\r\n" + "211 End\r\n"); + return; + } + + if (m_state == State::DATA_TRANSFER) + { + sendResponse ("211-FTP server status\r\n" + " Transferred %" PRIu64 " bytes\r\n" + "211 End\r\n", + m_filePosition); + return; + } + + if (std::strlen (args_) == 0) + { + // TODO keep track of start time + auto const uptime = + std::chrono::system_clock::to_time_t (std::chrono::system_clock::now ()) - + FtpServer::startTime (); + unsigned const hours = uptime / 3600; + unsigned const minutes = (uptime / 60) % 60; + unsigned const seconds = uptime % 60; + + sendResponse ("211-FTP server status\r\n" + " Uptime: %02u:%02u:%02u\r\n" + "211 End\r\n", + hours, + minutes, + seconds); + return; + } + + xferDir (args_, XferDirMode::STAT, false); +} + +void FtpSession::STOR (char const *args_) +{ + // open the file to store + xferFile (args_, XferFileMode::STOR); +} + +void FtpSession::STOU (char const *args_) +{ + setState (State::COMMAND, false, false); + sendResponse ("502 Command not implemented\r\n"); +} + +void FtpSession::STRU (char const *args_) +{ + setState (State::COMMAND, false, false); + + // we only support F (no structure) mode + if (::strcasecmp (args_, "F") == 0) + { + sendResponse ("200 OK\r\n"); + return; + } + + sendResponse ("504 Unavailable\r\n"); +} + +void FtpSession::SYST (char const *args_) +{ + setState (State::COMMAND, false, false); + sendResponse ("215 UNIX Type: L8\r\n"); +} + +void FtpSession::TYPE (char const *args_) +{ + setState (State::COMMAND, false, false); + + // we always transfer in binary mode + sendResponse ("200 OK\r\n"); +} + +void FtpSession::USER (char const *args_) +{ + setState (State::COMMAND, false, false); + sendResponse ("230 OK\r\n"); +} + +// clang-format off +std::vector> const + FtpSession::handlers = +{ + {"ABOR", &FtpSession::ABOR}, + {"ALLO", &FtpSession::ALLO}, + {"APPE", &FtpSession::APPE}, + {"CDUP", &FtpSession::CDUP}, + {"CWD", &FtpSession::CWD}, + {"DELE", &FtpSession::DELE}, + {"FEAT", &FtpSession::FEAT}, + {"HELP", &FtpSession::HELP}, + {"LIST", &FtpSession::LIST}, + {"MDTM", &FtpSession::MDTM}, + {"MKD", &FtpSession::MKD}, + {"MLSD", &FtpSession::MLSD}, + {"MLST", &FtpSession::MLST}, + {"MODE", &FtpSession::MODE}, + {"NLST", &FtpSession::NLST}, + {"NOOP", &FtpSession::NOOP}, + {"OPTS", &FtpSession::OPTS}, + {"PASS", &FtpSession::PASS}, + {"PASV", &FtpSession::PASV}, + {"PORT", &FtpSession::PORT}, + {"PWD", &FtpSession::PWD}, + {"QUIT", &FtpSession::QUIT}, + {"REST", &FtpSession::REST}, + {"RETR", &FtpSession::RETR}, + {"RMD", &FtpSession::RMD}, + {"RNFR", &FtpSession::RNFR}, + {"RNTO", &FtpSession::RNTO}, + {"SIZE", &FtpSession::SIZE}, + {"STAT", &FtpSession::STAT}, + {"STOR", &FtpSession::STOR}, + {"STOU", &FtpSession::STOU}, + {"STRU", &FtpSession::STRU}, + {"SYST", &FtpSession::SYST}, + {"TYPE", &FtpSession::TYPE}, + {"USER", &FtpSession::USER}, + {"XCUP", &FtpSession::CDUP}, + {"XCWD", &FtpSession::CWD}, + {"XMKD", &FtpSession::MKD}, + {"XPWD", &FtpSession::PWD}, + {"XRMD", &FtpSession::RMD}, +}; +// clang-format on diff --git a/source/imgui/imgui.cpp b/source/imgui/imgui.cpp new file mode 100644 index 0000000..4b71f2a --- /dev/null +++ b/source/imgui/imgui.cpp @@ -0,0 +1 @@ +#error "Please use https://github.com/ocornut/imgui/releases/tag/v1.75" diff --git a/source/imgui/imgui_draw.cpp b/source/imgui/imgui_draw.cpp new file mode 100644 index 0000000..4b71f2a --- /dev/null +++ b/source/imgui/imgui_draw.cpp @@ -0,0 +1 @@ +#error "Please use https://github.com/ocornut/imgui/releases/tag/v1.75" diff --git a/source/imgui/imgui_internal.h b/source/imgui/imgui_internal.h new file mode 100644 index 0000000..4b71f2a --- /dev/null +++ b/source/imgui/imgui_internal.h @@ -0,0 +1 @@ +#error "Please use https://github.com/ocornut/imgui/releases/tag/v1.75" diff --git a/source/imgui/imgui_widgets.cpp b/source/imgui/imgui_widgets.cpp new file mode 100644 index 0000000..4b71f2a --- /dev/null +++ b/source/imgui/imgui_widgets.cpp @@ -0,0 +1 @@ +#error "Please use https://github.com/ocornut/imgui/releases/tag/v1.75" diff --git a/source/imgui/imstb_rectpack.h b/source/imgui/imstb_rectpack.h new file mode 100644 index 0000000..4b71f2a --- /dev/null +++ b/source/imgui/imstb_rectpack.h @@ -0,0 +1 @@ +#error "Please use https://github.com/ocornut/imgui/releases/tag/v1.75" diff --git a/source/imgui/imstb_textedit.h b/source/imgui/imstb_textedit.h new file mode 100644 index 0000000..4b71f2a --- /dev/null +++ b/source/imgui/imstb_textedit.h @@ -0,0 +1 @@ +#error "Please use https://github.com/ocornut/imgui/releases/tag/v1.75" diff --git a/source/imgui/imstb_truetype.h b/source/imgui/imstb_truetype.h new file mode 100644 index 0000000..4b71f2a --- /dev/null +++ b/source/imgui/imstb_truetype.h @@ -0,0 +1 @@ +#error "Please use https://github.com/ocornut/imgui/releases/tag/v1.75" diff --git a/source/ioBuffer.cpp b/source/ioBuffer.cpp new file mode 100644 index 0000000..2eb42d0 --- /dev/null +++ b/source/ioBuffer.cpp @@ -0,0 +1,107 @@ +// ftpd is a server implementation based on the following: +// - RFC 959 (https://tools.ietf.org/html/rfc959) +// - RFC 3659 (https://tools.ietf.org/html/rfc3659) +// - suggested implementation details from https://cr.yp.to/ftp/filesystem.html +// +// Copyright (C) 2020 Michael Theall +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// 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, see . + +#include "ioBuffer.h" + +#include +#include + +/////////////////////////////////////////////////////////////////////////// +IOBuffer::~IOBuffer () = default; + +IOBuffer::IOBuffer (std::size_t const size_) + : m_buffer (std::make_unique (size_)), m_size (size_) +{ +} + +char *IOBuffer::freeArea () const +{ + assert (m_end < m_size); + return &m_buffer[m_end]; +} + +std::size_t IOBuffer::freeSize () const +{ + assert (m_size >= m_end); + return m_size - m_end; +} + +void IOBuffer::markFree (std::size_t size_) +{ + assert (m_end >= m_start); + assert (m_end - m_start >= size_); + m_start += size_; + + // reset back to beginning + if (m_start == m_end) + { + m_start = 0; + m_end = 0; + } +} + +char *IOBuffer::usedArea () const +{ + assert (m_start < m_size); + return &m_buffer[m_start]; +} + +std::size_t IOBuffer::usedSize () const +{ + assert (m_end >= m_start); + return m_end - m_start; +} + +void IOBuffer::markUsed (std::size_t size_) +{ + assert (m_size >= m_end); + assert (m_size - m_end >= size_); + m_end += size_; +} + +bool IOBuffer::empty () const +{ + assert (m_end >= m_start); + return (m_end - m_start) == 0; +} + +std::size_t IOBuffer::capacity () const +{ + return m_size; +} + +void IOBuffer::clear () +{ + m_start = 0; + m_end = 0; +} + +void IOBuffer::coalesce () +{ + assert (m_size >= m_end); + assert (m_end >= m_start); + + auto const size = m_end - m_start; + if (size != 0) + std::memmove (&m_buffer[0], &m_buffer[m_start], size); + + m_end -= m_start; + m_start = 0; +} diff --git a/source/log.cpp b/source/log.cpp new file mode 100644 index 0000000..5b83cfb --- /dev/null +++ b/source/log.cpp @@ -0,0 +1,196 @@ +// ftpd is a server implementation based on the following: +// - RFC 959 (https://tools.ietf.org/html/rfc959) +// - RFC 3659 (https://tools.ietf.org/html/rfc3659) +// - suggested implementation details from https://cr.yp.to/ftp/filesystem.html +// +// Copyright (C) 2020 Michael Theall +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// 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, see . + +#include "log.h" + +#include "imgui.h" + +#include + +namespace +{ +#ifdef _3DS +constexpr auto MAX_LOGS = 250; +#else +constexpr auto MAX_LOGS = 10000; +#endif +thread_local WeakLog s_log; + +static char const *const s_prefix[] = { + [Log::DEBUG] = "[DEBUG]", + [Log::INFO] = "[INFO]", + [Log::ERROR] = "[ERROR]", + [Log::COMMAND] = "[COMMAND]", + [Log::RESPONSE] = "[RESPONSE]", +}; +} + +/////////////////////////////////////////////////////////////////////////// +Log::~Log () = default; + +Log::Log () = default; + +void Log::draw () +{ + auto const lock = std::scoped_lock (m_lock); + + if (m_messages.size () > MAX_LOGS) + { + auto const begin = std::begin (m_messages); + auto const end = std::next (begin, m_messages.size () - MAX_LOGS); + m_messages.erase (begin, end); + } + + static ImVec4 const s_colors[] = { + [Log::DEBUG] = ImVec4 (1.0f, 1.0f, 0.4f, 1.0f), + [Log::INFO] = ImVec4 (1.0f, 1.0f, 1.0f, 1.0f), + [Log::ERROR] = ImVec4 (1.0f, 0.4f, 0.4f, 1.0f), + [Log::COMMAND] = ImVec4 (0.4f, 1.0f, 0.4f, 1.0f), + [Log::RESPONSE] = ImVec4 (0.4f, 1.0f, 1.0f, 1.0f), + }; + + static char const *const s_prefix[] = { + [Log::DEBUG] = "[DEBUG]", + [Log::INFO] = "[INFO]", + [Log::ERROR] = "[ERROR]", + [Log::COMMAND] = "[COMMAND]", + [Log::RESPONSE] = "[RESPONSE]", + }; + + for (auto const &message : m_messages) + { + ImGui::PushStyleColor (ImGuiCol_Text, s_colors[message.level]); + ImGui::TextUnformatted (s_prefix[message.level]); + ImGui::SameLine (); + ImGui::TextUnformatted (message.message.c_str ()); + ImGui::PopStyleColor (); + } + + // auto scroll if scroll bar is at end + if (ImGui::GetScrollY () >= ImGui::GetScrollMaxY ()) + ImGui::SetScrollHereY (1.0f); +} + +SharedLog Log::create () +{ + return std::shared_ptr (new Log ()); +} + +void Log::bind (SharedLog log_) +{ + s_log = log_; +} + +void Log::debug (char const *const fmt_, ...) +{ +#ifndef NDEBUG + va_list ap; + + va_start (ap, fmt_); + log (DEBUG, fmt_, ap); + va_end (ap); +#endif +} + +void Log::info (char const *const fmt_, ...) +{ + va_list ap; + + va_start (ap, fmt_); + log (INFO, fmt_, ap); + va_end (ap); +} + +void Log::error (char const *const fmt_, ...) +{ + va_list ap; + + va_start (ap, fmt_); + log (ERROR, fmt_, ap); + va_end (ap); +} + +void Log::command (char const *const fmt_, ...) +{ + va_list ap; + + va_start (ap, fmt_); + log (COMMAND, fmt_, ap); + va_end (ap); +} + +void Log::response (char const *const fmt_, ...) +{ + va_list ap; + + va_start (ap, fmt_); + log (RESPONSE, fmt_, ap); + va_end (ap); +} + +void Log::log (Level const level_, char const *const fmt_, va_list ap_) +{ +#ifdef NDEBUG + if (level_ == DEBUG) + return; +#endif + + auto log = s_log.lock (); + if (!log) + return; + + thread_local static char buffer[1024]; + + std::vsnprintf (buffer, sizeof (buffer), fmt_, ap_); + buffer[sizeof (buffer) - 1] = '\0'; + + auto const lock = std::scoped_lock (log->m_lock); +#ifndef NDEBUG + std::fprintf (stderr, "%s", s_prefix[level_]); + std::fputs (buffer, stderr); +#endif + log->m_messages.emplace_back (level_, buffer); +} + +void Log::log (Level const level_, std::string_view const message_) +{ +#ifdef NDEBUG + if (level_ == DEBUG) + return; +#endif + + auto log = s_log.lock (); + if (!log) + return; + + auto msg = std::string (message_); + for (auto &c : msg) + { + if (c == '\0') + c = '?'; + } + + auto const lock = std::scoped_lock (log->m_lock); +#ifndef NDEBUG + std::fprintf (stderr, "%s", s_prefix[level_]); + std::fwrite (msg.data (), 1, msg.size (), stderr); +#endif + log->m_messages.emplace_back (level_, msg); +} diff --git a/source/main.c b/source/main.c deleted file mode 100644 index e3cfe4f..0000000 --- a/source/main.c +++ /dev/null @@ -1,182 +0,0 @@ - -#include -#include -#include -#include -#include -#ifdef _3DS -#include <3ds.h> -#elif defined(__SWITCH__) -#include -#endif -#include "console.h" -#include "ftp.h" - -/*! looping mechanism - * - * @param[in] callback function to call during each iteration - * - * @returns loop status - */ -static loop_status_t -loop(loop_status_t (*callback)(void)) -{ - loop_status_t status = LOOP_CONTINUE; - -#ifdef _3DS - while(aptMainLoop()) - { - status = callback(); - console_render(); - if(status != LOOP_CONTINUE) - return status; - } - return LOOP_EXIT; -#elif defined(__SWITCH__) - while(appletMainLoop()) - { - console_render(); - status = callback(); - if(status != LOOP_CONTINUE) - return status; - } - return LOOP_EXIT; -#else - while(status == LOOP_CONTINUE) - status = callback(); - return status; -#endif -} - -#ifdef _3DS -/*! wait until the B button is pressed - * - * @returns loop status - */ -static loop_status_t -wait_for_b(void) -{ - /* update button state */ - hidScanInput(); - - /* check if B was pressed */ - if(hidKeysDown() & KEY_B) - return LOOP_EXIT; - - /* B was not pressed */ - return LOOP_CONTINUE; -} -#elif defined(__SWITCH__) -/*! wait until the B button is pressed - * - * @returns loop status - */ -static loop_status_t -wait_for_b(void) -{ - /* update button state */ - hidScanInput(); - - /* check if B was pressed */ - if(hidKeysDown(CONTROLLER_P1_AUTO) & KEY_B) - return LOOP_EXIT; - - /* B was not pressed */ - return LOOP_CONTINUE; -} -#endif - -/*! entry point - * - * @param[in] argc unused - * @param[in] argv unused - * - * returns exit status - */ -int -main(int argc, - char *argv[]) -{ - loop_status_t status = LOOP_RESTART; - -#ifdef _3DS - /* initialize needed 3DS services */ - acInit(); - gfxInitDefault(); - gfxSet3D(false); - sdmcWriteSafe(false); -#elif defined(__SWITCH__) - /* initialize needed Switch services */ - nifmInitialize(NifmServiceType_User); -#endif - - /* initialize console subsystem */ - console_init(); - -#ifdef ENABLE_LOGGING - /* open log file */ -#ifdef _3DS - FILE *fp = freopen("/ftpd.log", "wb", stderr); -#else - FILE *fp = freopen("ftpd.log", "wb", stderr); -#endif - if(fp == NULL) - { - console_print(RED "freopen: 0x%08X\n" RESET, errno); - goto log_fail; - } - - /* truncate log file */ - if(ftruncate(fileno(fp), 0) != 0) - { - console_print(RED "ftruncate: 0x%08X\n" RESET, errno); - goto log_fail; - } -#endif - - console_set_status("\n" GREEN STATUS_STRING -#ifdef ENABLE_LOGGING - " DEBUG" -#endif - RESET); - - while(status == LOOP_RESTART) - { - /* initialize ftp subsystem */ - if(ftp_init() == 0) - { - /* ftp loop */ - status = loop(ftp_loop); - - /* done with ftp */ - ftp_exit(); - } - else - status = LOOP_EXIT; - } - -#if defined(_3DS) || defined(__SWITCH__) - console_print("Press B to exit\n"); -#endif - -#ifdef ENABLE_LOGGING -log_fail: - if(fclose(stderr) != 0) - console_print(RED "fclose(%d): 0x%08X\n" RESET, fileno(stderr), errno); -#endif - -#ifdef _3DS - loop(wait_for_b); - - /* deinitialize 3DS services */ - gfxExit(); - acExit(); -#elif defined(__SWITCH__) - loop(wait_for_b); - - /* deinitialize Switch services */ - consoleExit(NULL); - nifmExit(); -#endif - return 0; -} diff --git a/source/main.cpp b/source/main.cpp new file mode 100644 index 0000000..cfc6703 --- /dev/null +++ b/source/main.cpp @@ -0,0 +1,52 @@ +// ftpd is a server implementation based on the following: +// - RFC 959 (https://tools.ietf.org/html/rfc959) +// - RFC 3659 (https://tools.ietf.org/html/rfc3659) +// - suggested implementation details from https://cr.yp.to/ftp/filesystem.html +// +// Copyright (C) 2020 Michael Theall +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// 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, see . + +#include "platform.h" + +#include "ftpServer.h" + +#include "imgui.h" + +#include +#include + +int main (int argc_, char *argv_[]) +{ + if (!platform::init ()) + return EXIT_FAILURE; + + auto &style = ImGui::GetStyle (); + style.WindowRounding = 0.0f; + +#ifdef _3DS + style.Colors[ImGuiCol_WindowBg].w = 0.5f; +#endif + + auto server = FtpServer::create (5000); + + while (platform::loop ()) + { + server->draw (); + + platform::render (); + } + + platform::exit (); +} diff --git a/source/nx/imgui_deko3d.cpp b/source/nx/imgui_deko3d.cpp new file mode 100644 index 0000000..4e86617 --- /dev/null +++ b/source/nx/imgui_deko3d.cpp @@ -0,0 +1,681 @@ +// ftpd is a server implementation based on the following: +// - RFC 959 (https://tools.ietf.org/html/rfc959) +// - RFC 3659 (https://tools.ietf.org/html/rfc3659) +// - suggested implementation details from https://cr.yp.to/ftp/filesystem.html +// +// Copyright (C) 2020 Michael Theall +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// 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, see . + +#include "imgui_deko3d.h" + +#include "imgui.h" + +#include "fs.h" +#include "log.h" + +#include +#include + +#include + +#define GLM_FORCE_DEFAULT_ALIGNED_GENTYPES +#define GLM_FORCE_INTRINSICS +#include +#include +#include + +#include +#include +#include + +#include +#include +#include +#include +#include + +namespace +{ +constexpr auto LOGO_WIDTH = 500; +constexpr auto LOGO_HEIGHT = 493; + +constexpr auto FB_NUM = 2u; + +constexpr auto CMDBUF_SIZE = 1024 * 1024; +constexpr auto DATABUF_SIZE = 1024 * 1024; +constexpr auto INDEXBUF_SIZE = 1024 * 1024; +constexpr auto IMAGEBUF_SIZE = 16 * 1024 * 1024; + +struct VertUBO +{ + glm::mat4 projMtx; +}; + +struct FragUBO +{ + std::uint32_t font; +}; + +constexpr std::array VertexAttribState = { + // clang-format off + DkVtxAttribState{0, 0, offsetof (ImDrawVert, pos), DkVtxAttribSize_2x32, DkVtxAttribType_Float, 0}, + DkVtxAttribState{0, 0, offsetof (ImDrawVert, uv), DkVtxAttribSize_2x32, DkVtxAttribType_Float, 0}, + DkVtxAttribState{0, 0, offsetof (ImDrawVert, col), DkVtxAttribSize_4x8, DkVtxAttribType_Unorm, 0}, + // clang-format on +}; + +constexpr std::array VertexBufferState = { + DkVtxBufferState{sizeof (ImDrawVert), 0}, +}; + +dk::UniqueDevice s_device; + +dk::UniqueMemBlock s_depthMemBlock; +dk::Image s_depthBuffer; + +dk::UniqueMemBlock s_fbMemBlock; +dk::Image s_frameBuffers[FB_NUM]; + +dk::Image s_fontTexture; +dk::Image s_logoTexture; + +dk::UniqueSwapchain s_swapchain; + +dk::UniqueMemBlock s_codeMemBlock; +dk::Shader s_shaders[2]; + +dk::UniqueMemBlock s_uboMemBlock; + +dk::UniqueMemBlock s_vtxMemBlock[FB_NUM]; +dk::UniqueMemBlock s_idxMemBlock[FB_NUM]; +dk::UniqueMemBlock s_cmdMemBlock[FB_NUM]; +dk::UniqueCmdBuf s_cmdBuf[FB_NUM]; + +dk::UniqueMemBlock s_imageMemBlock; +dk::UniqueMemBlock s_descriptorMemBlock; + +dk::UniqueQueue s_queue; + +constexpr auto MAX_SAMPLERS = 1; +constexpr auto MAX_IMAGES = 2; +dk::SamplerDescriptor *s_samplerDescriptors = nullptr; +dk::ImageDescriptor *s_imageDescriptors = nullptr; + +std::uintptr_t s_boundDescriptor = 0; + +unsigned s_width = 0; +unsigned s_height = 0; + +template +constexpr inline std::uint32_t align (T const &size_, U const &align_) +{ + return static_cast (size_ + align_ - 1) & ~(align_ - 1); +} + +void rebuildSwapchain (unsigned const width_, unsigned const height_) +{ + s_swapchain = nullptr; + + dk::ImageLayout depthLayout; + dk::ImageLayoutMaker{s_device} + .setFlags (DkImageFlags_UsageRender | DkImageFlags_HwCompression) + .setFormat (DkImageFormat_Z24S8) + .setDimensions (width_, height_) + .initialize (depthLayout); + + auto const depthAlign = depthLayout.getAlignment (); + auto const depthSize = depthLayout.getSize (); + + if (!s_depthMemBlock) + { + s_depthMemBlock = dk::MemBlockMaker{s_device, + align (depthSize, std::max (depthAlign, DK_MEMBLOCK_ALIGNMENT))} + .setFlags (DkMemBlockFlags_GpuCached | DkMemBlockFlags_Image) + .create (); + } + + s_depthBuffer.initialize (depthLayout, s_depthMemBlock, 0); + + dk::ImageLayout fbLayout; + dk::ImageLayoutMaker{s_device} + .setFlags ( + DkImageFlags_UsageRender | DkImageFlags_UsagePresent | DkImageFlags_HwCompression) + .setFormat (DkImageFormat_RGBA8_Unorm) + .setDimensions (width_, height_) + .initialize (fbLayout); + + auto const fbAlign = fbLayout.getAlignment (); + auto const fbSize = fbLayout.getSize (); + + if (!s_fbMemBlock) + { + s_fbMemBlock = dk::MemBlockMaker{s_device, + align (FB_NUM * fbSize, std::max (fbAlign, DK_MEMBLOCK_ALIGNMENT))} + .setFlags (DkMemBlockFlags_GpuCached | DkMemBlockFlags_Image) + .create (); + } + + std::array swapchainImages; + for (unsigned i = 0; i < FB_NUM; ++i) + { + swapchainImages[i] = &s_frameBuffers[i]; + s_frameBuffers[i].initialize (fbLayout, s_fbMemBlock, i * fbSize); + } + + s_swapchain = dk::SwapchainMaker{s_device, nwindowGetDefault (), swapchainImages}.create (); +} + +void loadShaders () +{ + struct ShaderFile + { + ShaderFile (dk::Shader &shader_, char const *const path_) + : shader (shader_), path (path_), size (getSize (path_)) + { + } + + static std::size_t getSize (char const *const path_) + { + struct stat st; + auto const rc = stat (path_, &st); + if (rc != 0) + { + std::fprintf (stderr, "stat(%s): %s\n", path_, std::strerror (errno)); + std::abort (); + } + + return st.st_size; + } + + dk::Shader &shader; + char const *const path; + std::size_t const size; + }; + + auto shaderFiles = {ShaderFile{s_shaders[0], "romfs:/shaders/imgui_vsh.dksh"}, + ShaderFile{s_shaders[1], "romfs:/shaders/imgui_fsh.dksh"}}; + + auto const codeSize = std::accumulate (std::begin (shaderFiles), + std::end (shaderFiles), + DK_SHADER_CODE_UNUSABLE_SIZE, + [] (auto const sum_, auto const &file_) { + return sum_ + align (file_.size, DK_SHADER_CODE_ALIGNMENT); + }); + + s_codeMemBlock = dk::MemBlockMaker{s_device, align (codeSize, DK_MEMBLOCK_ALIGNMENT)} + .setFlags (DkMemBlockFlags_CpuUncached | DkMemBlockFlags_GpuCached | + DkMemBlockFlags_Code) + .create (); + + auto const addr = static_cast (s_codeMemBlock.getCpuAddr ()); + std::size_t offset = 0; + + for (auto &file : shaderFiles) + { + std::uint32_t const codeOffset = offset; + + fs::File fp; + if (!fp.open (file.path)) + { + std::fprintf (stderr, "open(%s): %s\n", file.path, std::strerror (errno)); + std::abort (); + } + + if (!fp.readAll (&addr[offset], file.size)) + { + std::fprintf (stderr, "read(%s): %s\n", file.path, std::strerror (errno)); + std::abort (); + } + + dk::ShaderMaker{s_codeMemBlock, codeOffset}.initialize (file.shader); + + offset = align (offset + file.size, DK_SHADER_CODE_ALIGNMENT); + } +} + +DkCmdList setupRenderState (int const slot_, + ImDrawData *const drawData_, + unsigned const width_, + unsigned const height_) +{ + // Setup viewport, orthographic projection matrix + // Our visible imgui space lies from drawData_->DisplayPos (top left) to + // drawData_->DisplayPos+data_data->DisplaySize (bottom right). DisplayPos is (0,0) for single + // viewport apps. + auto const L = drawData_->DisplayPos.x; + auto const R = drawData_->DisplayPos.x + drawData_->DisplaySize.x; + auto const T = drawData_->DisplayPos.y; + auto const B = drawData_->DisplayPos.y + drawData_->DisplaySize.y; + + VertUBO vertUBO; + vertUBO.projMtx = glm::orthoRH_ZO (L, R, B, T, -1.0f, 1.0f); + + s_cmdBuf[slot_].setViewports (0, DkViewport{0.0f, 0.0f, width_, height_}); + s_cmdBuf[slot_].bindShaders (DkStageFlag_GraphicsMask, {&s_shaders[0], &s_shaders[1]}); + s_cmdBuf[slot_].bindUniformBuffer (DkStage_Vertex, + 0, + s_uboMemBlock.getGpuAddr (), + align (sizeof (VertUBO), DK_UNIFORM_BUF_ALIGNMENT)); + s_cmdBuf[slot_].pushConstants (s_uboMemBlock.getGpuAddr (), + align (sizeof (VertUBO), DK_UNIFORM_BUF_ALIGNMENT), + 0, + sizeof (VertUBO), + &vertUBO); + s_cmdBuf[slot_].bindUniformBuffer (DkStage_Fragment, + 0, + s_uboMemBlock.getGpuAddr () + align (sizeof (VertUBO), DK_UNIFORM_BUF_ALIGNMENT), + align (sizeof (FragUBO), DK_UNIFORM_BUF_ALIGNMENT)); + s_cmdBuf[slot_].bindRasterizerState (dk::RasterizerState{}.setCullMode (DkFace_None)); + s_cmdBuf[slot_].bindColorState (dk::ColorState{}.setBlendEnable (0, true)); + s_cmdBuf[slot_].bindColorWriteState (dk::ColorWriteState{}); + s_cmdBuf[slot_].bindDepthStencilState (dk::DepthStencilState{}.setDepthTestEnable (false)); + s_cmdBuf[slot_].bindBlendStates (0, + dk::BlendState{}.setFactors (DkBlendFactor_SrcAlpha, + DkBlendFactor_InvSrcAlpha, + DkBlendFactor_InvSrcAlpha, + DkBlendFactor_Zero)); + s_cmdBuf[slot_].bindVtxAttribState (VertexAttribState); + s_cmdBuf[slot_].bindVtxBufferState (VertexBufferState); + + return s_cmdBuf[slot_].finishList (); +} +} + +void imgui::deko3d::init () +{ + // Setup back-end capabilities flags + ImGuiIO &io = ImGui::GetIO (); + + io.BackendRendererName = "deko3d"; + io.BackendFlags |= ImGuiBackendFlags_RendererHasVtxOffset; +} + +void imgui::deko3d::exit () +{ + s_queue.waitIdle (); + + s_queue = nullptr; + s_descriptorMemBlock = nullptr; + s_imageMemBlock = nullptr; + + for (unsigned i = 0; i < FB_NUM; ++i) + { + s_cmdBuf[i] = nullptr; + s_cmdMemBlock[i] = nullptr; + s_idxMemBlock[i] = nullptr; + s_vtxMemBlock[i] = nullptr; + } + + s_uboMemBlock = nullptr; + s_codeMemBlock = nullptr; + s_swapchain = nullptr; + s_fbMemBlock = nullptr; + s_depthMemBlock = nullptr; + s_device = nullptr; +} + +void imgui::deko3d::newFrame () +{ + if (s_device) + return; + + s_device = dk::DeviceMaker{}.create (); + + rebuildSwapchain (1920, 1080); + + loadShaders (); + + s_uboMemBlock = dk::MemBlockMaker{s_device, + align (align (sizeof (VertUBO), DK_UNIFORM_BUF_ALIGNMENT) + + align (sizeof (FragUBO), DK_UNIFORM_BUF_ALIGNMENT), + DK_MEMBLOCK_ALIGNMENT)} + .setFlags (DkMemBlockFlags_CpuUncached | DkMemBlockFlags_GpuCached) + .create (); + + for (std::size_t i = 0; i < FB_NUM; ++i) + { + s_vtxMemBlock[i] = dk::MemBlockMaker{s_device, align (DATABUF_SIZE, DK_MEMBLOCK_ALIGNMENT)} + .setFlags (DkMemBlockFlags_CpuUncached | DkMemBlockFlags_GpuCached) + .create (); + + s_idxMemBlock[i] = dk::MemBlockMaker{s_device, align (INDEXBUF_SIZE, DK_MEMBLOCK_ALIGNMENT)} + .setFlags (DkMemBlockFlags_CpuUncached | DkMemBlockFlags_GpuCached) + .create (); + + s_cmdMemBlock[i] = dk::MemBlockMaker{s_device, align (CMDBUF_SIZE, DK_MEMBLOCK_ALIGNMENT)} + .setFlags (DkMemBlockFlags_CpuUncached | DkMemBlockFlags_GpuCached) + .create (); + + s_cmdBuf[i] = dk::CmdBufMaker{s_device}.create (); + + s_cmdBuf[i].addMemory (s_cmdMemBlock[i], 0, s_cmdMemBlock[i].getSize ()); + } + + s_queue = dk::QueueMaker{s_device}.setFlags (DkQueueFlags_Graphics).create (); + + s_imageMemBlock = dk::MemBlockMaker{s_device, align (IMAGEBUF_SIZE, DK_MEMBLOCK_ALIGNMENT)} + .setFlags (DkMemBlockFlags_GpuCached | DkMemBlockFlags_Image) + .create (); + + // Build texture atlas + ImGuiIO &io = ImGui::GetIO (); + io.Fonts->SetTexID (nullptr); + unsigned char *pixels; + int width; + int height; + io.Fonts->GetTexDataAsAlpha8 (&pixels, &width, &height); + + dk::UniqueMemBlock memBlock = + dk::MemBlockMaker{s_device, align (width * height, DK_MEMBLOCK_ALIGNMENT)} + .setFlags (DkMemBlockFlags_CpuUncached | DkMemBlockFlags_GpuCached) + .create (); + std::memcpy (memBlock.getCpuAddr (), pixels, width * height); + + static_assert (sizeof (dk::ImageDescriptor) == DK_IMAGE_DESCRIPTOR_ALIGNMENT); + static_assert (sizeof (dk::SamplerDescriptor) == DK_SAMPLER_DESCRIPTOR_ALIGNMENT); + static_assert (DK_IMAGE_DESCRIPTOR_ALIGNMENT == DK_SAMPLER_DESCRIPTOR_ALIGNMENT); + s_descriptorMemBlock = dk::MemBlockMaker{s_device, + align ((MAX_SAMPLERS + MAX_IMAGES) * sizeof (dk::ImageDescriptor), DK_MEMBLOCK_ALIGNMENT)} + .setFlags (DkMemBlockFlags_CpuUncached | DkMemBlockFlags_GpuCached) + .create (); + + s_samplerDescriptors = + static_cast (s_descriptorMemBlock.getCpuAddr ()); + s_imageDescriptors = + reinterpret_cast (&s_samplerDescriptors[MAX_SAMPLERS]); + + s_samplerDescriptors[0].initialize ( + dk::Sampler{} + .setFilter (DkFilter_Linear, DkFilter_Linear) + .setWrapMode (DkWrapMode_ClampToEdge, DkWrapMode_ClampToEdge, DkWrapMode_ClampToEdge)); + + auto &cmdBuf = s_cmdBuf[0]; + dk::ImageLayout layout; + dk::ImageLayoutMaker{s_device} + .setFlags (0) + .setFormat (DkImageFormat_R8_Unorm) + .setDimensions (width, height) + .initialize (layout); + + s_fontTexture.initialize (layout, s_imageMemBlock, 0); + s_imageDescriptors[0].initialize (s_fontTexture); + + dk::ImageView imageView{s_fontTexture}; + cmdBuf.copyBufferToImage ({memBlock.getGpuAddr ()}, imageView, {0, 0, 0, width, height, 1}); + + cmdBuf.bindSamplerDescriptorSet (s_descriptorMemBlock.getGpuAddr (), MAX_SAMPLERS); + cmdBuf.bindImageDescriptorSet ( + s_descriptorMemBlock.getGpuAddr () + MAX_SAMPLERS * sizeof (dk::SamplerDescriptor), + MAX_IMAGES); + + s_queue.submitCommands (cmdBuf.finishList ()); + s_queue.waitIdle (); + + { + auto const path = "romfs:/deko3d.rgba.zst"; + + struct stat st; + if (stat (path, &st) != 0) + { + std::fprintf (stderr, "stat(%s): %s\n", path, std::strerror (errno)); + std::abort (); + } + + fs::File fp; + if (!fp.open (path)) + { + std::fprintf (stderr, "open(%s): %s\n", path, std::strerror (errno)); + std::abort (); + } + + std::vector buffer (st.st_size); + if (!fp.readAll (buffer.data (), st.st_size)) + { + std::fprintf (stderr, "read(%s): %s\n", path, std::strerror (errno)); + std::abort (); + } + + fp.close (); + + auto const size = ZSTD_getFrameContentSize (buffer.data (), st.st_size); + if (ZSTD_isError (size)) + { + std::fprintf (stderr, "ZSTD_getFrameContentSize: %s\n", ZSTD_getErrorName (size)); + std::abort (); + } + + memBlock = dk::MemBlockMaker{s_device, align (size, DK_MEMBLOCK_ALIGNMENT)} + .setFlags (DkMemBlockFlags_CpuUncached | DkMemBlockFlags_GpuCached) + .create (); + + auto const decoded = + ZSTD_decompress (memBlock.getCpuAddr (), size, buffer.data (), st.st_size); + if (ZSTD_isError (decoded)) + { + std::fprintf (stderr, "ZSTD_decompress: %s\n", ZSTD_getErrorName (decoded)); + std::abort (); + } + + dk::ImageLayout layout; + dk::ImageLayoutMaker{s_device} + .setFlags (0) + .setFormat (DkImageFormat_RGBA8_Unorm) + .setDimensions (LOGO_WIDTH, LOGO_HEIGHT) + .initialize (layout); + + auto const offset = align (width * height, DK_IMAGE_LINEAR_STRIDE_ALIGNMENT); + s_logoTexture.initialize (layout, s_imageMemBlock, offset); + s_imageDescriptors[1].initialize (s_logoTexture); + + dk::ImageView imageView{s_logoTexture}; + cmdBuf.copyBufferToImage ( + {memBlock.getGpuAddr ()}, imageView, {0, 0, 0, LOGO_WIDTH, LOGO_HEIGHT, 1}); + + s_queue.submitCommands (cmdBuf.finishList ()); + s_queue.waitIdle (); + } + + cmdBuf.clear (); +} + +void imgui::deko3d::render () +{ + auto const drawData = ImGui::GetDrawData (); + if (drawData->CmdListsCount <= 0) + return; + + unsigned width = drawData->DisplaySize.x * drawData->FramebufferScale.x; + unsigned height = drawData->DisplaySize.y * drawData->FramebufferScale.y; + if (width <= 0 || height <= 0) + return; + + if (width != s_width || height != s_height) + { + s_width = width; + s_height = height; + + s_queue.waitIdle (); + rebuildSwapchain (width, height); + } + + auto const slot = s_queue.acquireImage (s_swapchain); + s_cmdBuf[slot].clear (); + + dk::ImageView colorTarget{s_frameBuffers[slot]}; + dk::ImageView depthTarget{s_depthBuffer}; + s_cmdBuf[slot].bindRenderTargets (&colorTarget, &depthTarget); + s_cmdBuf[slot].clearColor (0, DkColorMask_RGBA, 0.125f, 0.294f, 0.478f, 1.0f); + s_cmdBuf[slot].clearDepthStencil (true, 1.0f, 0xFF, 0); + s_queue.submitCommands (s_cmdBuf[slot].finishList ()); + + // Setup desired render state + auto const setupCmd = setupRenderState (slot, drawData, width, height); + s_queue.submitCommands (setupCmd); + + s_boundDescriptor = ~static_cast (0); + + // Will project scissor/clipping rectangles into framebuffer space + // (0,0) unless using multi-viewports + auto const clipOff = drawData->DisplayPos; + // (1,1) unless using retina display which are often (2,2) + auto const clipScale = drawData->FramebufferScale; + + if (s_vtxMemBlock[slot].getSize () < drawData->TotalVtxCount * sizeof (ImDrawVert)) + { + s_vtxMemBlock[slot] = nullptr; + s_vtxMemBlock[slot] = + dk::MemBlockMaker{s_device, + align (drawData->TotalVtxCount * sizeof (ImDrawVert), DK_MEMBLOCK_ALIGNMENT)} + .setFlags (DkMemBlockFlags_CpuUncached | DkMemBlockFlags_GpuCached) + .create (); + } + + if (s_idxMemBlock[slot].getSize () < drawData->TotalIdxCount * sizeof (ImDrawIdx)) + { + s_idxMemBlock[slot] = nullptr; + s_idxMemBlock[slot] = + dk::MemBlockMaker{s_device, + align (drawData->TotalIdxCount * sizeof (ImDrawIdx), DK_MEMBLOCK_ALIGNMENT)} + .setFlags (DkMemBlockFlags_CpuUncached | DkMemBlockFlags_GpuCached) + .create (); + } + + auto const cpuVtx = static_cast (s_vtxMemBlock[slot].getCpuAddr ()); + auto const cpuIdx = static_cast (s_idxMemBlock[slot].getCpuAddr ()); + + auto const gpuVtx = s_vtxMemBlock[slot].getGpuAddr (); + auto const gpuIdx = s_idxMemBlock[slot].getGpuAddr (); + + auto const sizeVtx = s_vtxMemBlock[slot].getSize (); + auto const sizeIdx = s_idxMemBlock[slot].getSize (); + + static_assert (sizeof (ImDrawIdx) == 2); + s_cmdBuf[slot].bindVtxBuffer (0, gpuVtx, sizeVtx); + s_cmdBuf[slot].bindIdxBuffer (DkIdxFormat_Uint16, gpuIdx); + + // Render command lists + std::size_t offsetVtx = 0; + std::size_t offsetIdx = 0; + for (int i = 0; i < drawData->CmdListsCount; ++i) + { + auto const &cmdList = *drawData->CmdLists[i]; + + auto const vtxSize = cmdList.VtxBuffer.Size * sizeof (ImDrawVert); + auto const idxSize = cmdList.IdxBuffer.Size * sizeof (ImDrawIdx); + + if (sizeVtx - offsetVtx < vtxSize) + { + std::fprintf (stderr, "Not enough vertex buffer\n"); + std::fprintf (stderr, "\t%zu/%u used, need %zu\n", offsetVtx, sizeVtx, vtxSize); + continue; + } + + if (sizeIdx - offsetIdx < idxSize) + { + std::fprintf (stderr, "Not enough index buffer\n"); + std::fprintf (stderr, "\t%zu/%u used, need %zu\n", offsetIdx, sizeIdx, idxSize); + continue; + } + + std::memcpy (cpuVtx + offsetVtx, cmdList.VtxBuffer.Data, vtxSize); + std::memcpy (cpuIdx + offsetIdx, cmdList.IdxBuffer.Data, idxSize); + + for (auto const &cmd : cmdList.CmdBuffer) + { + if (cmd.UserCallback) + { + s_queue.submitCommands (s_cmdBuf[slot].finishList ()); + + // User callback, registered via ImDrawList::AddCallback() + // (ImDrawCallback_ResetRenderState is a special callback value used by the user to + // request the renderer to reset render state.) + if (cmd.UserCallback == ImDrawCallback_ResetRenderState) + s_queue.submitCommands (setupCmd); + else + cmd.UserCallback (&cmdList, &cmd); + } + else + { + // Project scissor/clipping rectangles into framebuffer space + ImVec4 clip; + clip.x = (cmd.ClipRect.x - clipOff.x) * clipScale.x; + clip.y = (cmd.ClipRect.y - clipOff.y) * clipScale.y; + clip.z = (cmd.ClipRect.z - clipOff.x) * clipScale.x; + clip.w = (cmd.ClipRect.w - clipOff.y) * clipScale.y; + + if (clip.x < width && clip.y < height && clip.z >= 0.0f && clip.w >= 0.0f) + { + if (clip.x < 0.0f) + clip.x = 0.0f; + if (clip.y < 0.0f) + clip.y = 0.0f; + + s_cmdBuf[slot].setScissors ( + 0, DkScissor{clip.x, clip.y, clip.z - clip.x, clip.w - clip.y}); + + auto const descriptor = reinterpret_cast (cmd.TextureId); + if (descriptor >= MAX_IMAGES) + continue; + + if (descriptor != s_boundDescriptor) + { + s_boundDescriptor = descriptor; + + s_cmdBuf[slot].bindTextures ( + DkStage_Fragment, 0, dkMakeTextureHandle (descriptor, 0)); + + FragUBO fragUBO; + fragUBO.font = (descriptor == 0); + + s_cmdBuf[slot].pushConstants ( + s_uboMemBlock.getGpuAddr () + + align (sizeof (VertUBO), DK_UNIFORM_BUF_ALIGNMENT), + align (sizeof (FragUBO), DK_UNIFORM_BUF_ALIGNMENT), + 0, + sizeof (FragUBO), + &fragUBO); + } + + s_cmdBuf[slot].drawIndexed (DkPrimitive_Triangles, + cmd.ElemCount, + 1, + cmd.IdxOffset + offsetIdx / sizeof (ImDrawIdx), + cmd.VtxOffset + offsetVtx / sizeof (ImDrawVert), + 0); + } + } + } + + offsetVtx += vtxSize; + offsetIdx += idxSize; + } + + s_cmdBuf[slot].barrier (DkBarrier_Fragments, 0); + s_cmdBuf[slot].discardDepthStencil (); + s_queue.submitCommands (s_cmdBuf[slot].finishList ()); + + s_queue.presentImage (s_swapchain, slot); +} + +void imgui::deko3d::test () +{ + auto const x1 = (s_width - LOGO_WIDTH) / 2.0f; + auto const x2 = x1 + LOGO_WIDTH; + auto const y1 = (s_height - LOGO_HEIGHT) / 2.0f; + auto const y2 = y1 + LOGO_HEIGHT; + + ImGui::GetBackgroundDrawList ()->AddImage ( + reinterpret_cast (1), ImVec2 (x1, y1), ImVec2 (x2, y2)); +} diff --git a/source/nx/imgui_deko3d.h b/source/nx/imgui_deko3d.h new file mode 100644 index 0000000..6b196af --- /dev/null +++ b/source/nx/imgui_deko3d.h @@ -0,0 +1,35 @@ +// ftpd is a server implementation based on the following: +// - RFC 959 (https://tools.ietf.org/html/rfc959) +// - RFC 3659 (https://tools.ietf.org/html/rfc3659) +// - suggested implementation details from https://cr.yp.to/ftp/filesystem.html +// +// Copyright (C) 2020 Michael Theall +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// 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, see . + +#pragma once + +namespace imgui +{ +namespace deko3d +{ +void init (); +void exit (); + +void newFrame (); +void render (); + +void test (); +} +} diff --git a/source/nx/imgui_fsh.glsl b/source/nx/imgui_fsh.glsl new file mode 100644 index 0000000..8398b4f --- /dev/null +++ b/source/nx/imgui_fsh.glsl @@ -0,0 +1,40 @@ +// ftpd is a server implementation based on the following: +// - RFC 959 (https://tools.ietf.org/html/rfc959) +// - RFC 3659 (https://tools.ietf.org/html/rfc3659) +// - suggested implementation details from https://cr.yp.to/ftp/filesystem.html +// +// Copyright (C) 2020 Michael Theall +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// 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, see . + +#version 460 + +layout (location = 0) in vec2 vtxUv; +layout (location = 1) in vec4 vtxColor; + +layout (binding = 0) uniform sampler2D tex; + +layout (std140, binding = 0) uniform FragUBO { + uint font; +} ubo; + +layout (location = 0) out vec4 outColor; + +void main() +{ + if (ubo.font != 0) + outColor = vtxColor * vec4 (vec3 (1.0), texture (tex, vtxUv).r); + else + outColor = vtxColor * texture (tex, vtxUv); +} diff --git a/source/nx/imgui_nx.cpp b/source/nx/imgui_nx.cpp new file mode 100644 index 0000000..ae1ef48 --- /dev/null +++ b/source/nx/imgui_nx.cpp @@ -0,0 +1,1655 @@ +// ftpd is a server implementation based on the following: +// - RFC 959 (https://tools.ietf.org/html/rfc959) +// - RFC 3659 (https://tools.ietf.org/html/rfc3659) +// - suggested implementation details from https://cr.yp.to/ftp/filesystem.html +// +// Copyright (C) 2020 Michael Theall +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// 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, see . + +#include "imgui_nx.h" + +#include "imgui.h" + +#include "fs.h" + +#include + +#include +#include +#include +#include +#include +using namespace std::chrono_literals; + +namespace +{ +constexpr auto FONT_ATLAS_BIN = "ftpd-font.bin"; + +bool s_mouseJustPressed[IM_ARRAYSIZE (ImGuiIO::MouseDown)]; + +std::chrono::high_resolution_clock::time_point s_lastMouseUpdate; + +bool s_focused = true; +float s_width = 1280.0f; +float s_height = 720.0f; + +float s_showMouse = false; +ImVec2 s_mousePos = ImVec2 (0.0f, 0.0f); + +std::string s_clipboard; + +AppletHookCookie s_appletHookCookie; + +ImWchar const nxFontRanges[] = { + // clang-format off + 0x0020, 0x007e, 0x00a0, 0x017f, 0x0192, 0x0192, 0x01c0, 0x01c0, + 0x01c5, 0x01c6, 0x01ce, 0x01ce, 0x01d0, 0x01d0, 0x01d2, 0x01d2, + 0x01d4, 0x01d4, 0x01d6, 0x01d6, 0x01d8, 0x01d8, 0x01da, 0x01da, + 0x01dc, 0x01dc, 0x01f2, 0x01f3, 0x01f9, 0x01ff, 0x0218, 0x021b, + 0x0251, 0x0251, 0x0261, 0x0261, 0x02bc, 0x02bd, 0x02c6, 0x02c7, + 0x02c9, 0x02cb, 0x02cd, 0x02cd, 0x02d0, 0x02d0, 0x02d8, 0x02dd, + 0x0300, 0x0308, 0x030a, 0x030c, 0x0326, 0x0328, 0x0332, 0x0332, + 0x0335, 0x0337, 0x0361, 0x0361, 0x0363, 0x0364, 0x0366, 0x0366, + 0x036c, 0x036c, 0x037a, 0x037a, 0x037e, 0x037e, 0x0384, 0x038a, + 0x038c, 0x038c, 0x038e, 0x03a1, 0x03a3, 0x03ce, 0x0400, 0x045f, + 0x0490, 0x0491, 0x1e02, 0x1e03, 0x1e0a, 0x1e0b, 0x1e1e, 0x1e1f, + 0x1e3f, 0x1e41, 0x1e56, 0x1e57, 0x1e60, 0x1e61, 0x1e6a, 0x1e6b, + 0x1e80, 0x1e85, 0x1e9e, 0x1e9e, 0x1ef2, 0x1ef3, 0x2010, 0x2022, + 0x2024, 0x2027, 0x2030, 0x2030, 0x2032, 0x2036, 0x2039, 0x203c, + 0x203e, 0x203e, 0x2042, 0x2042, 0x2044, 0x2044, 0x2060, 0x2060, + 0x2074, 0x2074, 0x207a, 0x207f, 0x2081, 0x2084, 0x20a3, 0x20a4, + 0x20a7, 0x20a7, 0x20a9, 0x20a9, 0x20ac, 0x20ac, 0x20af, 0x20af, + 0x20bd, 0x20bd, 0x20dd, 0x20dd, 0x2103, 0x2103, 0x2105, 0x2105, + 0x2109, 0x210a, 0x2113, 0x2113, 0x2116, 0x2116, 0x2121, 0x2122, + 0x2126, 0x2126, 0x212b, 0x212b, 0x212e, 0x212e, 0x2153, 0x2154, + 0x215b, 0x215e, 0x2160, 0x216b, 0x2170, 0x217b, 0x217f, 0x217f, + 0x2190, 0x2199, 0x21a8, 0x21a8, 0x21b0, 0x21b4, 0x21bc, 0x21bc, + 0x21c0, 0x21c0, 0x21c4, 0x21c6, 0x21cd, 0x21cd, 0x21cf, 0x21d4, + 0x21e0, 0x21e3, 0x21e6, 0x21e9, 0x2200, 0x2200, 0x2202, 0x2203, + 0x2206, 0x2209, 0x220b, 0x220c, 0x220f, 0x220f, 0x2211, 0x2213, + 0x2215, 0x2215, 0x2219, 0x221a, 0x221d, 0x2220, 0x2222, 0x2222, + 0x2225, 0x222c, 0x222e, 0x222e, 0x2234, 0x2237, 0x223c, 0x223d, + 0x2243, 0x2243, 0x2245, 0x2245, 0x2248, 0x2248, 0x2250, 0x2253, + 0x225a, 0x225a, 0x2260, 0x2262, 0x2264, 0x2267, 0x226a, 0x226b, + 0x226e, 0x2273, 0x2276, 0x2277, 0x2279, 0x227b, 0x2280, 0x2287, + 0x228a, 0x228b, 0x2295, 0x2297, 0x2299, 0x2299, 0x22a3, 0x22a5, + 0x22bb, 0x22bc, 0x22bf, 0x22bf, 0x22ce, 0x22cf, 0x22da, 0x22db, + 0x22ee, 0x22ef, 0x2302, 0x2302, 0x2306, 0x2306, 0x2310, 0x2310, + 0x2312, 0x2312, 0x2314, 0x2314, 0x2320, 0x2321, 0x2460, 0x2487, + 0x249c, 0x24f4, 0x2500, 0x2503, 0x250c, 0x254b, 0x2550, 0x256c, + 0x2573, 0x2573, 0x2580, 0x2580, 0x2584, 0x2584, 0x2588, 0x2588, + 0x258c, 0x258c, 0x2590, 0x2593, 0x25a0, 0x25a1, 0x25a3, 0x25ac, + 0x25b1, 0x25b3, 0x25b5, 0x25b7, 0x25b9, 0x25ba, 0x25bc, 0x25bd, + 0x25bf, 0x25c1, 0x25c3, 0x25c4, 0x25c6, 0x25cc, 0x25ce, 0x25d1, + 0x25d8, 0x25d9, 0x25e6, 0x25e6, 0x25ef, 0x25ef, 0x2600, 0x2603, + 0x2605, 0x2606, 0x260e, 0x260f, 0x261c, 0x261f, 0x262f, 0x262f, + 0x263a, 0x263c, 0x2640, 0x2640, 0x2642, 0x2642, 0x2660, 0x266d, + 0x266f, 0x266f, 0x2716, 0x2716, 0x271a, 0x271a, 0x273d, 0x273d, + 0x2756, 0x2756, 0x2776, 0x277f, 0x278a, 0x2793, 0x2f00, 0x2f00, + 0x2f04, 0x2f04, 0x2f06, 0x2f06, 0x2f08, 0x2f08, 0x2f0a, 0x2f0b, + 0x2f11, 0x2f12, 0x2f14, 0x2f14, 0x2f17, 0x2f18, 0x2f1c, 0x2f1d, + 0x2f1f, 0x2f20, 0x2f23, 0x2f26, 0x2f28, 0x2f29, 0x2f2b, 0x2f2b, + 0x2f2d, 0x2f2d, 0x2f2f, 0x2f32, 0x2f38, 0x2f38, 0x2f3c, 0x2f40, + 0x2f42, 0x2f4c, 0x2f4f, 0x2f52, 0x2f54, 0x2f58, 0x2f5a, 0x2f66, + 0x2f69, 0x2f70, 0x2f72, 0x2f76, 0x2f78, 0x2f78, 0x2f7a, 0x2f7d, + 0x2f7f, 0x2f8b, 0x2f8e, 0x2f90, 0x2f92, 0x2f97, 0x2f99, 0x2fa0, + 0x2fa2, 0x2fa3, 0x2fa5, 0x2fa9, 0x2fac, 0x2fb1, 0x2fb3, 0x2fbc, + 0x2fc1, 0x2fca, 0x2fcd, 0x2fd4, 0x3000, 0x3019, 0x301c, 0x3020, + 0x3036, 0x3036, 0x3041, 0x3094, 0x309b, 0x309e, 0x30a1, 0x30f6, + 0x30fb, 0x30fe, 0x3131, 0x318e, 0x3200, 0x321c, 0x322a, 0x3243, + 0x3260, 0x327b, 0x327f, 0x327f, 0x328a, 0x3290, 0x3294, 0x3294, + 0x329e, 0x329e, 0x32a4, 0x32a8, 0x3303, 0x3303, 0x330d, 0x330d, + 0x3314, 0x3314, 0x3318, 0x3318, 0x3322, 0x3323, 0x3326, 0x3327, + 0x332b, 0x332b, 0x3336, 0x3336, 0x333b, 0x333b, 0x3349, 0x334a, + 0x334d, 0x334d, 0x3351, 0x3351, 0x3357, 0x3357, 0x337b, 0x33cd, + 0x33cf, 0x33d0, 0x33d3, 0x33d4, 0x33d6, 0x33d6, 0x33d8, 0x33d8, + 0x33db, 0x33dd, 0x4e00, 0x4e01, 0x4e03, 0x4e03, 0x4e07, 0x4e0b, + 0x4e0d, 0x4e0e, 0x4e10, 0x4e11, 0x4e14, 0x4e19, 0x4e1e, 0x4e1e, + 0x4e21, 0x4e21, 0x4e26, 0x4e26, 0x4e28, 0x4e28, 0x4e2a, 0x4e2a, + 0x4e2d, 0x4e2d, 0x4e31, 0x4e32, 0x4e36, 0x4e36, 0x4e38, 0x4e39, + 0x4e3b, 0x4e3c, 0x4e3f, 0x4e3f, 0x4e42, 0x4e43, 0x4e45, 0x4e45, + 0x4e4b, 0x4e4b, 0x4e4d, 0x4e4f, 0x4e55, 0x4e59, 0x4e5d, 0x4e5f, + 0x4e62, 0x4e62, 0x4e6b, 0x4e6b, 0x4e6d, 0x4e6d, 0x4e71, 0x4e71, + 0x4e73, 0x4e73, 0x4e76, 0x4e77, 0x4e7e, 0x4e7e, 0x4e80, 0x4e80, + 0x4e82, 0x4e82, 0x4e85, 0x4e86, 0x4e88, 0x4e8c, 0x4e8e, 0x4e8e, + 0x4e90, 0x4e92, 0x4e94, 0x4e95, 0x4e98, 0x4e99, 0x4e9b, 0x4e9c, + 0x4e9e, 0x4ea2, 0x4ea4, 0x4ea6, 0x4ea8, 0x4ea8, 0x4eab, 0x4eae, + 0x4eb0, 0x4eb0, 0x4eb3, 0x4eb3, 0x4eb6, 0x4eb6, 0x4eba, 0x4eba, + 0x4ec0, 0x4ec2, 0x4ec4, 0x4ec4, 0x4ec6, 0x4ec7, 0x4eca, 0x4ecb, + 0x4ecd, 0x4ecf, 0x4ed4, 0x4ed9, 0x4edd, 0x4edf, 0x4ee1, 0x4ee1, + 0x4ee3, 0x4ee5, 0x4eed, 0x4eee, 0x4ef0, 0x4ef0, 0x4ef2, 0x4ef2, + 0x4ef6, 0x4ef7, 0x4efb, 0x4efc, 0x4f00, 0x4f01, 0x4f03, 0x4f03, + 0x4f09, 0x4f0b, 0x4f0d, 0x4f11, 0x4f1a, 0x4f1a, 0x4f1c, 0x4f1d, + 0x4f2f, 0x4f30, 0x4f34, 0x4f34, 0x4f36, 0x4f36, 0x4f38, 0x4f3a, + 0x4f3c, 0x4f3d, 0x4f43, 0x4f43, 0x4f46, 0x4f48, 0x4f4d, 0x4f51, + 0x4f53, 0x4f53, 0x4f55, 0x4f57, 0x4f59, 0x4f5e, 0x4f69, 0x4f69, + 0x4f6f, 0x4f70, 0x4f73, 0x4f73, 0x4f75, 0x4f76, 0x4f7a, 0x4f7c, + 0x4f7e, 0x4f7f, 0x4f81, 0x4f81, 0x4f83, 0x4f84, 0x4f86, 0x4f86, + 0x4f88, 0x4f88, 0x4f8a, 0x4f8b, 0x4f8d, 0x4f8d, 0x4f8f, 0x4f8f, + 0x4f91, 0x4f92, 0x4f94, 0x4f94, 0x4f96, 0x4f96, 0x4f98, 0x4f98, + 0x4f9a, 0x4f9b, 0x4f9d, 0x4f9d, 0x4fa0, 0x4fa1, 0x4fab, 0x4fab, + 0x4fad, 0x4faf, 0x4fb5, 0x4fb6, 0x4fbf, 0x4fbf, 0x4fc2, 0x4fc4, + 0x4fc9, 0x4fca, 0x4fcd, 0x4fce, 0x4fd0, 0x4fd1, 0x4fd3, 0x4fd4, + 0x4fd7, 0x4fd8, 0x4fda, 0x4fdb, 0x4fdd, 0x4fdd, 0x4fdf, 0x4fe1, + 0x4fe3, 0x4fe5, 0x4fee, 0x4fef, 0x4ff1, 0x4ff1, 0x4ff3, 0x4ff3, + 0x4ff5, 0x4ff6, 0x4ff8, 0x4ff8, 0x4ffa, 0x4ffa, 0x4ffe, 0x4fff, + 0x5002, 0x5002, 0x5005, 0x5006, 0x5009, 0x5009, 0x500b, 0x500b, + 0x500d, 0x500d, 0x500f, 0x500f, 0x5011, 0x5012, 0x5014, 0x5014, + 0x5016, 0x5016, 0x5019, 0x501a, 0x501c, 0x501c, 0x501e, 0x501f, + 0x5021, 0x502d, 0x5036, 0x5036, 0x5039, 0x5039, 0x503b, 0x503b, + 0x5040, 0x5040, 0x5042, 0x5043, 0x5046, 0x5049, 0x504f, 0x5050, + 0x5055, 0x5056, 0x505a, 0x505a, 0x505c, 0x505c, 0x5065, 0x5065, + 0x506c, 0x506c, 0x5070, 0x5070, 0x5072, 0x5072, 0x5074, 0x5076, + 0x5078, 0x5078, 0x507d, 0x507d, 0x5080, 0x5080, 0x5085, 0x5085, + 0x508d, 0x508d, 0x5091, 0x5091, 0x5094, 0x5094, 0x5098, 0x509a, + 0x50ac, 0x50ad, 0x50b2, 0x50b5, 0x50b7, 0x50b7, 0x50be, 0x50be, + 0x50c2, 0x50c2, 0x50c5, 0x50c5, 0x50c9, 0x50ca, 0x50cd, 0x50cd, + 0x50cf, 0x50cf, 0x50d1, 0x50d1, 0x50d5, 0x50d6, 0x50d8, 0x50d8, + 0x50da, 0x50da, 0x50de, 0x50de, 0x50e3, 0x50e3, 0x50e5, 0x50e5, + 0x50e7, 0x50e7, 0x50ed, 0x50ee, 0x50f4, 0x50f5, 0x50f9, 0x50f9, + 0x50fb, 0x50fb, 0x50ff, 0x5102, 0x5104, 0x5104, 0x5106, 0x5106, + 0x5109, 0x5109, 0x5112, 0x5112, 0x5114, 0x5116, 0x5118, 0x5118, + 0x511a, 0x511a, 0x511f, 0x511f, 0x5121, 0x5121, 0x512a, 0x512a, + 0x5132, 0x5132, 0x5137, 0x5137, 0x513a, 0x513c, 0x513f, 0x5141, + 0x5143, 0x514e, 0x5150, 0x5150, 0x5152, 0x5152, 0x5154, 0x5154, + 0x515a, 0x515a, 0x515c, 0x515c, 0x5162, 0x5162, 0x5164, 0x5165, + 0x5167, 0x516e, 0x5171, 0x5171, 0x5175, 0x5178, 0x517c, 0x517c, + 0x5180, 0x5180, 0x5182, 0x5182, 0x5185, 0x5186, 0x5189, 0x518a, + 0x518c, 0x518d, 0x518f, 0x5193, 0x5195, 0x5197, 0x5199, 0x5199, + 0x519d, 0x519d, 0x51a0, 0x51a0, 0x51a2, 0x51a2, 0x51a4, 0x51a6, + 0x51a8, 0x51ac, 0x51b0, 0x51b7, 0x51bd, 0x51be, 0x51c4, 0x51c6, + 0x51c9, 0x51c9, 0x51cb, 0x51cd, 0x51d6, 0x51d6, 0x51db, 0x51de, + 0x51e0, 0x51e1, 0x51e6, 0x51e7, 0x51e9, 0x51ea, 0x51ec, 0x51ed, + 0x51f0, 0x51f1, 0x51f5, 0x51f6, 0x51f8, 0x51fa, 0x51fd, 0x51fe, + 0x5200, 0x5200, 0x5203, 0x5204, 0x5206, 0x5208, 0x520a, 0x520b, + 0x520e, 0x520e, 0x5211, 0x5211, 0x5214, 0x5215, 0x5217, 0x5217, + 0x521d, 0x521d, 0x5224, 0x5225, 0x5227, 0x5227, 0x5229, 0x522a, + 0x522e, 0x522e, 0x5230, 0x5230, 0x5233, 0x5233, 0x5236, 0x523b, + 0x5243, 0x5244, 0x5247, 0x5247, 0x524a, 0x524d, 0x524f, 0x524f, + 0x5254, 0x5254, 0x5256, 0x5256, 0x525b, 0x525b, 0x525d, 0x525e, + 0x5261, 0x5261, 0x5263, 0x5265, 0x5269, 0x526a, 0x526f, 0x5275, + 0x527d, 0x527d, 0x527f, 0x527f, 0x5283, 0x5283, 0x5287, 0x5289, + 0x528d, 0x528d, 0x5291, 0x5292, 0x5294, 0x5294, 0x529b, 0x529c, + 0x529f, 0x52a0, 0x52a3, 0x52a4, 0x52a6, 0x52a6, 0x52a9, 0x52ad, + 0x52af, 0x52af, 0x52b1, 0x52b1, 0x52b4, 0x52b5, 0x52b9, 0x52b9, + 0x52bc, 0x52bc, 0x52be, 0x52be, 0x52c0, 0x52c1, 0x52c3, 0x52c3, + 0x52c5, 0x52c5, 0x52c7, 0x52c7, 0x52c9, 0x52c9, 0x52cd, 0x52cd, + 0x52d2, 0x52d2, 0x52d5, 0x52d9, 0x52db, 0x52db, 0x52dd, 0x52e0, + 0x52e2, 0x52e4, 0x52e6, 0x52e7, 0x52f2, 0x52f3, 0x52f5, 0x52f5, + 0x52f8, 0x52fb, 0x52fe, 0x5302, 0x5305, 0x5308, 0x530d, 0x530d, + 0x530f, 0x5310, 0x5315, 0x5317, 0x5319, 0x531a, 0x531d, 0x531d, + 0x5320, 0x5321, 0x5323, 0x5324, 0x532a, 0x532a, 0x532f, 0x532f, + 0x5331, 0x5331, 0x5333, 0x5333, 0x5338, 0x533b, 0x533f, 0x5341, + 0x5343, 0x534a, 0x534d, 0x534d, 0x5351, 0x5354, 0x5357, 0x5358, + 0x535a, 0x535a, 0x535c, 0x535c, 0x535e, 0x535e, 0x5360, 0x5360, + 0x5366, 0x5366, 0x5368, 0x5369, 0x536e, 0x5375, 0x5377, 0x5378, + 0x537b, 0x537b, 0x537d, 0x537d, 0x537f, 0x537f, 0x5382, 0x5382, + 0x5384, 0x5384, 0x5393, 0x5393, 0x5396, 0x5396, 0x5398, 0x5398, + 0x539a, 0x539a, 0x539f, 0x53a0, 0x53a5, 0x53a6, 0x53a8, 0x53a9, + 0x53ad, 0x53ae, 0x53b0, 0x53b0, 0x53b2, 0x53b3, 0x53b6, 0x53b6, + 0x53bb, 0x53bb, 0x53c2, 0x53c3, 0x53c8, 0x53ce, 0x53d4, 0x53d4, + 0x53d6, 0x53d7, 0x53d9, 0x53d9, 0x53db, 0x53db, 0x53dd, 0x53dd, + 0x53df, 0x53df, 0x53e1, 0x53e5, 0x53e8, 0x53f3, 0x53f6, 0x53f8, + 0x53fa, 0x53fa, 0x5401, 0x5401, 0x5403, 0x5404, 0x5408, 0x5411, + 0x541b, 0x541b, 0x541d, 0x541d, 0x541f, 0x5420, 0x5426, 0x5426, + 0x5429, 0x5429, 0x542b, 0x542e, 0x5433, 0x5433, 0x5436, 0x5436, + 0x5438, 0x5439, 0x543b, 0x543e, 0x5440, 0x5440, 0x5442, 0x5442, + 0x5446, 0x5446, 0x5448, 0x544a, 0x544e, 0x544e, 0x5451, 0x5451, + 0x545f, 0x545f, 0x5468, 0x5468, 0x546a, 0x546a, 0x5470, 0x5471, + 0x5473, 0x5473, 0x5475, 0x5477, 0x547b, 0x547d, 0x5480, 0x5480, + 0x5484, 0x5484, 0x5486, 0x5486, 0x548a, 0x548c, 0x548e, 0x5490, + 0x5492, 0x5492, 0x549c, 0x549c, 0x54a2, 0x54a2, 0x54a4, 0x54a5, + 0x54a8, 0x54a9, 0x54ab, 0x54ac, 0x54af, 0x54af, 0x54b2, 0x54b3, + 0x54b8, 0x54b8, 0x54bc, 0x54be, 0x54c0, 0x54c2, 0x54c4, 0x54c4, + 0x54c7, 0x54c9, 0x54d8, 0x54d8, 0x54e1, 0x54e2, 0x54e5, 0x54e6, + 0x54e8, 0x54e9, 0x54ed, 0x54ee, 0x54f2, 0x54f2, 0x54fa, 0x54fa, + 0x54fd, 0x54fd, 0x54ff, 0x54ff, 0x5504, 0x5504, 0x5506, 0x5507, + 0x550e, 0x5510, 0x5514, 0x5514, 0x5516, 0x5516, 0x551c, 0x551c, + 0x552e, 0x552f, 0x5531, 0x5531, 0x5533, 0x5533, 0x5535, 0x5535, + 0x5538, 0x5539, 0x553e, 0x553e, 0x5540, 0x5540, 0x5544, 0x5546, + 0x554c, 0x554c, 0x554f, 0x554f, 0x5553, 0x5553, 0x5556, 0x5557, + 0x555c, 0x555e, 0x5563, 0x5563, 0x557b, 0x557c, 0x557e, 0x557e, + 0x5580, 0x5580, 0x5583, 0x5584, 0x5586, 0x5587, 0x5589, 0x558b, + 0x5598, 0x559a, 0x559c, 0x559f, 0x55a7, 0x55ac, 0x55ae, 0x55ae, + 0x55b0, 0x55b0, 0x55b6, 0x55b6, 0x55c4, 0x55c5, 0x55c7, 0x55c7, + 0x55d4, 0x55d4, 0x55da, 0x55da, 0x55dc, 0x55dc, 0x55df, 0x55df, + 0x55e3, 0x55e4, 0x55f7, 0x55f7, 0x55f9, 0x55f9, 0x55fd, 0x55fe, + 0x5606, 0x5606, 0x5609, 0x5609, 0x5614, 0x5614, 0x5616, 0x5618, + 0x561b, 0x561b, 0x5629, 0x5629, 0x562f, 0x562f, 0x5631, 0x5632, + 0x5634, 0x5634, 0x5636, 0x5636, 0x5638, 0x5638, 0x5642, 0x5642, + 0x564c, 0x564c, 0x564e, 0x564e, 0x5650, 0x5650, 0x5653, 0x5653, + 0x565b, 0x565b, 0x5664, 0x5664, 0x5668, 0x5668, 0x566a, 0x566c, + 0x5674, 0x5674, 0x5678, 0x5678, 0x567a, 0x567a, 0x5680, 0x5680, + 0x5686, 0x5687, 0x568a, 0x568a, 0x568f, 0x568f, 0x5694, 0x5694, + 0x56a0, 0x56a0, 0x56a2, 0x56a2, 0x56a5, 0x56a5, 0x56ac, 0x56ac, + 0x56ae, 0x56ae, 0x56b4, 0x56b4, 0x56b6, 0x56b6, 0x56bc, 0x56bc, + 0x56c0, 0x56c3, 0x56c8, 0x56c8, 0x56ca, 0x56ca, 0x56cd, 0x56ce, + 0x56d1, 0x56d1, 0x56d3, 0x56d3, 0x56d7, 0x56d8, 0x56da, 0x56db, + 0x56de, 0x56de, 0x56e0, 0x56e0, 0x56e3, 0x56e3, 0x56ee, 0x56ee, + 0x56f0, 0x56f0, 0x56f2, 0x56f3, 0x56f9, 0x56fa, 0x56fd, 0x56fd, + 0x56ff, 0x5700, 0x5703, 0x5704, 0x5708, 0x5709, 0x570b, 0x570b, + 0x570d, 0x570d, 0x570f, 0x570f, 0x5712, 0x5713, 0x5716, 0x5716, + 0x5718, 0x5718, 0x571c, 0x571c, 0x571f, 0x571f, 0x5726, 0x5728, + 0x572d, 0x572d, 0x5730, 0x5730, 0x5737, 0x5738, 0x573b, 0x573b, + 0x5740, 0x5740, 0x5742, 0x5742, 0x5747, 0x5747, 0x574a, 0x574a, + 0x574d, 0x5751, 0x5759, 0x5759, 0x5761, 0x5761, 0x5764, 0x5766, + 0x5769, 0x576a, 0x576e, 0x576e, 0x5770, 0x5770, 0x5775, 0x5775, + 0x577c, 0x577c, 0x577f, 0x577f, 0x5782, 0x5782, 0x5788, 0x5789, + 0x578b, 0x578b, 0x5793, 0x5793, 0x57a0, 0x57a0, 0x57a2, 0x57a4, + 0x57aa, 0x57aa, 0x57ac, 0x57ac, 0x57b0, 0x57b0, 0x57b3, 0x57b3, + 0x57c0, 0x57c0, 0x57c3, 0x57c3, 0x57c6, 0x57c8, 0x57cb, 0x57cb, + 0x57ce, 0x57ce, 0x57d2, 0x57d4, 0x57d6, 0x57d6, 0x57dc, 0x57dc, + 0x57df, 0x57e0, 0x57e3, 0x57e3, 0x57f0, 0x57f0, 0x57f4, 0x57f4, + 0x57f7, 0x57f7, 0x57f9, 0x57fa, 0x57fc, 0x57fc, 0x5800, 0x5800, + 0x5802, 0x5802, 0x5805, 0x5806, 0x5808, 0x580b, 0x5815, 0x5815, + 0x5819, 0x5819, 0x581d, 0x581e, 0x5821, 0x5821, 0x5824, 0x5824, + 0x5827, 0x5827, 0x582a, 0x582a, 0x582f, 0x5831, 0x5834, 0x5835, + 0x583a, 0x583a, 0x583d, 0x583d, 0x5840, 0x5841, 0x584a, 0x584b, + 0x584f, 0x584f, 0x5851, 0x5852, 0x5854, 0x5854, 0x5857, 0x585a, + 0x585e, 0x585e, 0x5861, 0x5862, 0x5864, 0x5864, 0x5869, 0x5869, + 0x586b, 0x586b, 0x5870, 0x5870, 0x5872, 0x5872, 0x5875, 0x5875, + 0x5879, 0x5879, 0x587c, 0x587c, 0x587e, 0x587e, 0x5883, 0x5883, + 0x5885, 0x5885, 0x5889, 0x5889, 0x5893, 0x5893, 0x5897, 0x5897, + 0x589c, 0x589c, 0x589e, 0x589f, 0x58a8, 0x58a9, 0x58ab, 0x58ab, + 0x58ae, 0x58ae, 0x58b2, 0x58b3, 0x58b8, 0x58bb, 0x58be, 0x58be, + 0x58c1, 0x58c1, 0x58c5, 0x58c5, 0x58c7, 0x58c7, 0x58ca, 0x58ca, + 0x58cc, 0x58cc, 0x58ce, 0x58ce, 0x58d1, 0x58d1, 0x58d3, 0x58d3, + 0x58d5, 0x58d5, 0x58d7, 0x58d9, 0x58dc, 0x58dc, 0x58de, 0x58df, + 0x58e4, 0x58e5, 0x58eb, 0x58ec, 0x58ee, 0x58f2, 0x58f7, 0x58f7, + 0x58f9, 0x58fd, 0x5902, 0x5902, 0x5909, 0x590b, 0x590f, 0x5910, + 0x5914, 0x5916, 0x5918, 0x591c, 0x5922, 0x5922, 0x5925, 0x5925, + 0x5927, 0x5927, 0x5929, 0x592e, 0x5931, 0x5932, 0x5937, 0x5938, + 0x593e, 0x593e, 0x5944, 0x5944, 0x5947, 0x5949, 0x594e, 0x5951, + 0x5953, 0x5955, 0x5957, 0x5958, 0x595a, 0x595b, 0x595d, 0x595d, + 0x5960, 0x5960, 0x5962, 0x5963, 0x5965, 0x5965, 0x5967, 0x596e, + 0x5973, 0x5974, 0x5978, 0x5978, 0x597d, 0x597d, 0x5981, 0x5984, + 0x598a, 0x598a, 0x598d, 0x598d, 0x5993, 0x5993, 0x5996, 0x5997, + 0x5999, 0x5999, 0x599b, 0x599b, 0x599d, 0x599d, 0x59a3, 0x59a5, + 0x59a8, 0x59a8, 0x59ac, 0x59ac, 0x59b2, 0x59b2, 0x59b9, 0x59bb, + 0x59be, 0x59be, 0x59c3, 0x59c3, 0x59c6, 0x59c6, 0x59c9, 0x59c9, + 0x59cb, 0x59cb, 0x59d0, 0x59d1, 0x59d3, 0x59d4, 0x59d9, 0x59da, + 0x59dc, 0x59dd, 0x59e5, 0x59e6, 0x59e8, 0x59e8, 0x59ea, 0x59ec, + 0x59ee, 0x59ee, 0x59f6, 0x59f6, 0x59f8, 0x59f8, 0x59fb, 0x59fb, + 0x59ff, 0x59ff, 0x5a01, 0x5a01, 0x5a03, 0x5a03, 0x5a09, 0x5a09, + 0x5a11, 0x5a11, 0x5a18, 0x5a18, 0x5a1a, 0x5a1c, 0x5a1f, 0x5a20, + 0x5a25, 0x5a25, 0x5a29, 0x5a29, 0x5a2f, 0x5a2f, 0x5a35, 0x5a36, + 0x5a3c, 0x5a3c, 0x5a40, 0x5a41, 0x5a46, 0x5a46, 0x5a49, 0x5a49, + 0x5a5a, 0x5a5a, 0x5a62, 0x5a62, 0x5a66, 0x5a66, 0x5a6a, 0x5a6a, + 0x5a6c, 0x5a6c, 0x5a7f, 0x5a7f, 0x5a92, 0x5a92, 0x5a9a, 0x5a9b, + 0x5aa4, 0x5aa4, 0x5abc, 0x5abe, 0x5ac1, 0x5ac2, 0x5ac4, 0x5ac4, + 0x5ac9, 0x5ac9, 0x5acb, 0x5acc, 0x5ad0, 0x5ad0, 0x5ad6, 0x5ad7, + 0x5ae1, 0x5ae1, 0x5ae3, 0x5ae3, 0x5ae6, 0x5ae6, 0x5ae9, 0x5ae9, + 0x5afa, 0x5afb, 0x5b05, 0x5b05, 0x5b09, 0x5b09, 0x5b0b, 0x5b0c, + 0x5b16, 0x5b16, 0x5b22, 0x5b22, 0x5b2a, 0x5b2a, 0x5b2c, 0x5b2c, + 0x5b30, 0x5b30, 0x5b32, 0x5b32, 0x5b36, 0x5b36, 0x5b3e, 0x5b3e, + 0x5b40, 0x5b40, 0x5b43, 0x5b43, 0x5b45, 0x5b45, 0x5b50, 0x5b51, + 0x5b54, 0x5b58, 0x5b5a, 0x5b5d, 0x5b5f, 0x5b5f, 0x5b63, 0x5b66, + 0x5b69, 0x5b69, 0x5b6b, 0x5b6b, 0x5b70, 0x5b71, 0x5b73, 0x5b73, + 0x5b75, 0x5b75, 0x5b78, 0x5b78, 0x5b7a, 0x5b7a, 0x5b7c, 0x5b7c, + 0x5b80, 0x5b80, 0x5b83, 0x5b83, 0x5b85, 0x5b85, 0x5b87, 0x5b89, + 0x5b8b, 0x5b8d, 0x5b8f, 0x5b8f, 0x5b93, 0x5b93, 0x5b95, 0x5b9d, + 0x5b9f, 0x5b9f, 0x5ba2, 0x5ba6, 0x5bac, 0x5bac, 0x5bae, 0x5bae, + 0x5bb0, 0x5bb0, 0x5bb3, 0x5bb6, 0x5bb8, 0x5bb9, 0x5bbf, 0x5bc0, + 0x5bc2, 0x5bc7, 0x5bc9, 0x5bc9, 0x5bcc, 0x5bcc, 0x5bd0, 0x5bd0, + 0x5bd2, 0x5bd4, 0x5bd7, 0x5bd8, 0x5bdb, 0x5bdb, 0x5bdd, 0x5bdf, + 0x5be1, 0x5be2, 0x5be4, 0x5be9, 0x5beb, 0x5bec, 0x5bee, 0x5bf0, + 0x5bf3, 0x5bf3, 0x5bf5, 0x5bf6, 0x5bf8, 0x5bf8, 0x5bfa, 0x5bfa, + 0x5bfe, 0x5bff, 0x5c01, 0x5c02, 0x5c04, 0x5c0b, 0x5c0d, 0x5c0f, + 0x5c11, 0x5c11, 0x5c13, 0x5c13, 0x5c16, 0x5c16, 0x5c19, 0x5c1a, + 0x5c1e, 0x5c1e, 0x5c20, 0x5c20, 0x5c22, 0x5c22, 0x5c24, 0x5c24, + 0x5c28, 0x5c28, 0x5c2d, 0x5c2d, 0x5c31, 0x5c31, 0x5c38, 0x5c41, + 0x5c45, 0x5c46, 0x5c48, 0x5c48, 0x5c4a, 0x5c4b, 0x5c4d, 0x5c51, + 0x5c53, 0x5c53, 0x5c55, 0x5c55, 0x5c5b, 0x5c5b, 0x5c5e, 0x5c5e, + 0x5c60, 0x5c62, 0x5c64, 0x5c65, 0x5c6c, 0x5c6c, 0x5c6e, 0x5c6f, + 0x5c71, 0x5c71, 0x5c76, 0x5c76, 0x5c79, 0x5c79, 0x5c8c, 0x5c8c, + 0x5c90, 0x5c91, 0x5c94, 0x5c94, 0x5ca1, 0x5ca1, 0x5ca6, 0x5ca6, + 0x5ca8, 0x5ca9, 0x5cab, 0x5cac, 0x5cb1, 0x5cb1, 0x5cb3, 0x5cb3, + 0x5cb5, 0x5cb8, 0x5cba, 0x5cbc, 0x5cbe, 0x5cbe, 0x5cc0, 0x5cc0, + 0x5cc5, 0x5cc5, 0x5cc7, 0x5cc7, 0x5cd9, 0x5cd9, 0x5ce0, 0x5ce1, + 0x5ce8, 0x5cea, 0x5ced, 0x5ced, 0x5cef, 0x5cf0, 0x5cf4, 0x5cf6, + 0x5cfa, 0x5cfb, 0x5cfd, 0x5cfd, 0x5d07, 0x5d07, 0x5d0b, 0x5d0b, + 0x5d0d, 0x5d0e, 0x5d11, 0x5d11, 0x5d14, 0x5d1b, 0x5d1f, 0x5d1f, + 0x5d22, 0x5d22, 0x5d27, 0x5d27, 0x5d29, 0x5d29, 0x5d42, 0x5d42, + 0x5d4b, 0x5d4c, 0x5d4e, 0x5d4e, 0x5d50, 0x5d50, 0x5d52, 0x5d53, + 0x5d5c, 0x5d5c, 0x5d69, 0x5d69, 0x5d6c, 0x5d6d, 0x5d6f, 0x5d6f, + 0x5d73, 0x5d73, 0x5d76, 0x5d76, 0x5d82, 0x5d82, 0x5d84, 0x5d84, + 0x5d87, 0x5d87, 0x5d8b, 0x5d8c, 0x5d90, 0x5d90, 0x5d9d, 0x5d9d, + 0x5da0, 0x5da0, 0x5da2, 0x5da2, 0x5daa, 0x5daa, 0x5dac, 0x5dac, + 0x5dae, 0x5dae, 0x5db7, 0x5dba, 0x5dbc, 0x5dbd, 0x5dc9, 0x5dc9, + 0x5dcc, 0x5dcd, 0x5dd0, 0x5dd0, 0x5dd2, 0x5dd3, 0x5dd6, 0x5dd6, + 0x5ddb, 0x5ddb, 0x5ddd, 0x5dde, 0x5de1, 0x5de3, 0x5de5, 0x5de8, + 0x5deb, 0x5deb, 0x5dee, 0x5dee, 0x5df1, 0x5df5, 0x5df7, 0x5df7, + 0x5dfb, 0x5dfb, 0x5dfd, 0x5dfe, 0x5e02, 0x5e03, 0x5e06, 0x5e06, + 0x5e0b, 0x5e0c, 0x5e11, 0x5e11, 0x5e16, 0x5e16, 0x5e19, 0x5e1b, + 0x5e1d, 0x5e1d, 0x5e25, 0x5e25, 0x5e2b, 0x5e2b, 0x5e2d, 0x5e2d, + 0x5e2f, 0x5e30, 0x5e33, 0x5e33, 0x5e36, 0x5e38, 0x5e3d, 0x5e3d, + 0x5e3f, 0x5e40, 0x5e43, 0x5e45, 0x5e47, 0x5e47, 0x5e4c, 0x5e4c, + 0x5e4e, 0x5e4e, 0x5e54, 0x5e55, 0x5e57, 0x5e57, 0x5e5f, 0x5e5f, + 0x5e61, 0x5e64, 0x5e72, 0x5e7f, 0x5e81, 0x5e81, 0x5e83, 0x5e84, + 0x5e87, 0x5e87, 0x5e8a, 0x5e8a, 0x5e8f, 0x5e8f, 0x5e95, 0x5e97, + 0x5e9a, 0x5e9a, 0x5e9c, 0x5e9c, 0x5ea0, 0x5ea0, 0x5ea6, 0x5ea7, + 0x5eab, 0x5eab, 0x5ead, 0x5ead, 0x5eb5, 0x5eb8, 0x5ebe, 0x5ebe, + 0x5ec1, 0x5ec3, 0x5ec8, 0x5eca, 0x5ecf, 0x5ed0, 0x5ed3, 0x5ed3, + 0x5ed6, 0x5ed6, 0x5eda, 0x5edb, 0x5edd, 0x5edd, 0x5edf, 0x5ee3, + 0x5ee8, 0x5ee9, 0x5eec, 0x5eec, 0x5ef0, 0x5ef1, 0x5ef3, 0x5ef4, + 0x5ef6, 0x5ef8, 0x5efa, 0x5efc, 0x5efe, 0x5eff, 0x5f01, 0x5f01, + 0x5f03, 0x5f04, 0x5f09, 0x5f0d, 0x5f0f, 0x5f11, 0x5f13, 0x5f18, + 0x5f1b, 0x5f1b, 0x5f1f, 0x5f1f, 0x5f21, 0x5f21, 0x5f25, 0x5f27, + 0x5f29, 0x5f29, 0x5f2d, 0x5f2d, 0x5f2f, 0x5f2f, 0x5f31, 0x5f31, + 0x5f34, 0x5f35, 0x5f37, 0x5f38, 0x5f3a, 0x5f3a, 0x5f3c, 0x5f3c, + 0x5f3e, 0x5f3e, 0x5f41, 0x5f41, 0x5f45, 0x5f45, 0x5f48, 0x5f48, + 0x5f4a, 0x5f4a, 0x5f4c, 0x5f4c, 0x5f4e, 0x5f4e, 0x5f51, 0x5f51, + 0x5f53, 0x5f53, 0x5f56, 0x5f57, 0x5f59, 0x5f59, 0x5f5b, 0x5f5d, + 0x5f61, 0x5f62, 0x5f66, 0x5f67, 0x5f69, 0x5f6d, 0x5f70, 0x5f71, + 0x5f73, 0x5f73, 0x5f77, 0x5f77, 0x5f79, 0x5f79, 0x5f7c, 0x5f7c, + 0x5f7f, 0x5f85, 0x5f87, 0x5f88, 0x5f8a, 0x5f8c, 0x5f90, 0x5f93, + 0x5f97, 0x5f99, 0x5f9e, 0x5f9e, 0x5fa0, 0x5fa1, 0x5fa8, 0x5faa, + 0x5fad, 0x5fae, 0x5fb3, 0x5fb5, 0x5fb7, 0x5fb7, 0x5fb9, 0x5fb9, + 0x5fbc, 0x5fbd, 0x5fc3, 0x5fc3, 0x5fc5, 0x5fc5, 0x5fcc, 0x5fcd, + 0x5fd6, 0x5fd9, 0x5fdc, 0x5fde, 0x5fe0, 0x5fe0, 0x5fe4, 0x5fe4, + 0x5feb, 0x5feb, 0x5ff0, 0x5ff1, 0x5ff5, 0x5ff5, 0x5ff8, 0x5ff8, + 0x5ffb, 0x5ffb, 0x5ffd, 0x5ffd, 0x5fff, 0x5fff, 0x600e, 0x6010, + 0x6012, 0x6012, 0x6015, 0x6016, 0x6019, 0x6019, 0x601b, 0x601d, + 0x6020, 0x6021, 0x6025, 0x602b, 0x602f, 0x602f, 0x6031, 0x6031, + 0x603a, 0x603a, 0x6041, 0x6043, 0x6046, 0x6046, 0x604a, 0x604b, + 0x604d, 0x604d, 0x6050, 0x6050, 0x6052, 0x6052, 0x6055, 0x6055, + 0x6059, 0x605a, 0x605d, 0x605d, 0x605f, 0x6060, 0x6062, 0x6065, + 0x6068, 0x606d, 0x606f, 0x6070, 0x6075, 0x6075, 0x6077, 0x6077, + 0x6081, 0x6081, 0x6083, 0x6085, 0x6089, 0x608d, 0x6092, 0x6092, + 0x6094, 0x6094, 0x6096, 0x6097, 0x609a, 0x609b, 0x609f, 0x60a0, + 0x60a3, 0x60a4, 0x60a6, 0x60a7, 0x60a9, 0x60aa, 0x60b0, 0x60b0, + 0x60b2, 0x60b6, 0x60b8, 0x60b8, 0x60bc, 0x60bd, 0x60c5, 0x60c7, + 0x60d1, 0x60d1, 0x60d3, 0x60d3, 0x60d5, 0x60d5, 0x60d8, 0x60d8, + 0x60da, 0x60da, 0x60dc, 0x60dc, 0x60de, 0x60e1, 0x60e3, 0x60e3, + 0x60e7, 0x60e8, 0x60f0, 0x60f4, 0x60f6, 0x60f7, 0x60f9, 0x60fb, + 0x6100, 0x6101, 0x6103, 0x6103, 0x6106, 0x6106, 0x6108, 0x6109, + 0x610d, 0x610f, 0x6111, 0x6111, 0x6115, 0x6115, 0x611a, 0x611b, + 0x611f, 0x6121, 0x6127, 0x6128, 0x612c, 0x612c, 0x6130, 0x6130, + 0x6134, 0x6134, 0x6137, 0x6137, 0x613c, 0x613f, 0x6142, 0x6142, + 0x6144, 0x6144, 0x6147, 0x6148, 0x614a, 0x614e, 0x6153, 0x6153, + 0x6155, 0x6155, 0x6158, 0x615a, 0x615d, 0x615d, 0x615f, 0x615f, + 0x6162, 0x6165, 0x6167, 0x6168, 0x616b, 0x616b, 0x616e, 0x6171, + 0x6173, 0x6177, 0x617d, 0x617e, 0x6181, 0x6182, 0x6187, 0x6187, + 0x618a, 0x618a, 0x618e, 0x618e, 0x6190, 0x6191, 0x6194, 0x6194, + 0x6196, 0x6196, 0x6198, 0x619a, 0x61a4, 0x61a4, 0x61a7, 0x61a7, + 0x61a9, 0x61a9, 0x61ab, 0x61ac, 0x61ae, 0x61ae, 0x61b2, 0x61b2, + 0x61b6, 0x61b6, 0x61ba, 0x61ba, 0x61be, 0x61be, 0x61c3, 0x61c3, + 0x61c6, 0x61cd, 0x61d0, 0x61d0, 0x61e3, 0x61e3, 0x61e6, 0x61e6, + 0x61f2, 0x61f2, 0x61f4, 0x61f4, 0x61f6, 0x61f8, 0x61fa, 0x61fa, + 0x61fc, 0x6200, 0x6207, 0x620a, 0x620c, 0x620e, 0x6210, 0x6214, + 0x6216, 0x6216, 0x621a, 0x621b, 0x621d, 0x621f, 0x6221, 0x6221, + 0x6226, 0x6226, 0x622a, 0x622a, 0x622e, 0x6234, 0x6236, 0x6236, + 0x6238, 0x6238, 0x623b, 0x623b, 0x623e, 0x6241, 0x6247, 0x6249, + 0x624b, 0x624b, 0x624d, 0x624e, 0x6253, 0x6253, 0x6255, 0x6255, + 0x6258, 0x6258, 0x625b, 0x625b, 0x625e, 0x625e, 0x6260, 0x6260, + 0x6263, 0x6263, 0x6268, 0x6268, 0x626e, 0x626e, 0x6271, 0x6271, + 0x6276, 0x6276, 0x6279, 0x6279, 0x627c, 0x627c, 0x627e, 0x6280, + 0x6282, 0x6284, 0x6289, 0x628a, 0x6291, 0x6298, 0x629b, 0x629c, + 0x629e, 0x629e, 0x62a6, 0x62a6, 0x62ab, 0x62ac, 0x62b1, 0x62b1, + 0x62b5, 0x62b5, 0x62b9, 0x62b9, 0x62bb, 0x62bd, 0x62c2, 0x62c2, + 0x62c5, 0x62ca, 0x62cc, 0x62cd, 0x62cf, 0x62d4, 0x62d6, 0x62d9, + 0x62db, 0x62dd, 0x62e0, 0x62e1, 0x62ec, 0x62ef, 0x62f1, 0x62f1, + 0x62f3, 0x62f3, 0x62f5, 0x62f7, 0x62fe, 0x62ff, 0x6301, 0x6302, + 0x6307, 0x6309, 0x630c, 0x630c, 0x6311, 0x6311, 0x6319, 0x6319, + 0x631f, 0x631f, 0x6327, 0x6328, 0x632b, 0x632b, 0x632f, 0x632f, + 0x633a, 0x633b, 0x633d, 0x633f, 0x6349, 0x6349, 0x634c, 0x634d, + 0x634f, 0x6350, 0x6355, 0x6355, 0x6357, 0x6357, 0x635c, 0x635c, + 0x6367, 0x6369, 0x636b, 0x636b, 0x636e, 0x636e, 0x6372, 0x6372, + 0x6376, 0x6377, 0x637a, 0x637b, 0x637f, 0x6380, 0x6383, 0x6383, + 0x6388, 0x6389, 0x638c, 0x638c, 0x638e, 0x638f, 0x6392, 0x6392, + 0x6396, 0x6396, 0x6398, 0x6398, 0x639b, 0x639b, 0x639f, 0x63a3, + 0x63a5, 0x63a5, 0x63a7, 0x63ac, 0x63b2, 0x63b2, 0x63b4, 0x63b5, + 0x63bb, 0x63bb, 0x63be, 0x63be, 0x63c0, 0x63c0, 0x63c3, 0x63c4, + 0x63c6, 0x63c6, 0x63c9, 0x63c9, 0x63cf, 0x63d0, 0x63d2, 0x63d2, + 0x63d6, 0x63d6, 0x63da, 0x63db, 0x63e1, 0x63e1, 0x63e3, 0x63e3, + 0x63e9, 0x63e9, 0x63ed, 0x63ee, 0x63f4, 0x63f7, 0x63fa, 0x63fa, + 0x6406, 0x6406, 0x640d, 0x640d, 0x640f, 0x640f, 0x6413, 0x6414, + 0x6416, 0x6417, 0x641c, 0x641c, 0x6422, 0x6422, 0x6426, 0x6426, + 0x6428, 0x6428, 0x642c, 0x642d, 0x6434, 0x6434, 0x6436, 0x6436, + 0x643a, 0x643a, 0x643e, 0x643e, 0x6442, 0x6442, 0x644e, 0x644e, + 0x6458, 0x6458, 0x6460, 0x6460, 0x6467, 0x6467, 0x6469, 0x6469, + 0x646f, 0x646f, 0x6476, 0x6476, 0x6478, 0x647a, 0x6483, 0x6483, + 0x6488, 0x6488, 0x6491, 0x6493, 0x6495, 0x6495, 0x649a, 0x649a, + 0x649d, 0x649e, 0x64a4, 0x64a5, 0x64a9, 0x64a9, 0x64ab, 0x64ab, + 0x64ad, 0x64ae, 0x64b0, 0x64b0, 0x64b2, 0x64b2, 0x64b9, 0x64b9, + 0x64bb, 0x64bc, 0x64c1, 0x64c2, 0x64c4, 0x64c5, 0x64c7, 0x64c7, + 0x64ca, 0x64ca, 0x64cd, 0x64ce, 0x64d2, 0x64d2, 0x64d4, 0x64d4, + 0x64d8, 0x64d8, 0x64da, 0x64da, 0x64e0, 0x64e3, 0x64e5, 0x64e7, + 0x64ec, 0x64ec, 0x64ef, 0x64ef, 0x64f1, 0x64f2, 0x64f4, 0x64f4, + 0x64f6, 0x64f6, 0x64fa, 0x64fa, 0x64fd, 0x64fe, 0x6500, 0x6500, + 0x6504, 0x6505, 0x6518, 0x6518, 0x651c, 0x651d, 0x6523, 0x6524, + 0x652a, 0x652c, 0x652f, 0x652f, 0x6534, 0x6539, 0x653b, 0x653b, + 0x653e, 0x653f, 0x6545, 0x6545, 0x6548, 0x6548, 0x654d, 0x654f, + 0x6551, 0x6551, 0x6555, 0x6559, 0x655d, 0x655e, 0x6562, 0x6563, + 0x6566, 0x6566, 0x656c, 0x656d, 0x6570, 0x6570, 0x6572, 0x6572, + 0x6574, 0x6575, 0x6577, 0x6578, 0x657e, 0x657e, 0x6582, 0x6583, + 0x6585, 0x6585, 0x6587, 0x6589, 0x658c, 0x658c, 0x658e, 0x658e, + 0x6590, 0x6591, 0x6597, 0x6597, 0x6599, 0x6599, 0x659b, 0x659c, + 0x659f, 0x659f, 0x65a1, 0x65a1, 0x65a4, 0x65a5, 0x65a7, 0x65a7, + 0x65ab, 0x65ad, 0x65af, 0x65b0, 0x65b7, 0x65b7, 0x65b9, 0x65b9, + 0x65bc, 0x65bd, 0x65c1, 0x65c1, 0x65c3, 0x65c6, 0x65cb, 0x65cc, + 0x65cf, 0x65cf, 0x65d2, 0x65d2, 0x65d7, 0x65d7, 0x65d9, 0x65d9, + 0x65db, 0x65db, 0x65e0, 0x65e3, 0x65e5, 0x65e9, 0x65ec, 0x65ed, + 0x65f1, 0x65f1, 0x65f4, 0x65f4, 0x65fa, 0x65fd, 0x65ff, 0x6600, + 0x6602, 0x6603, 0x6606, 0x6607, 0x6609, 0x660a, 0x660c, 0x660c, + 0x660e, 0x6611, 0x6613, 0x6615, 0x661c, 0x661c, 0x661e, 0x6620, + 0x6624, 0x6625, 0x6627, 0x6628, 0x662d, 0x6631, 0x6634, 0x6636, + 0x663a, 0x663c, 0x663f, 0x663f, 0x6641, 0x6644, 0x6649, 0x6649, + 0x664b, 0x664b, 0x664f, 0x664f, 0x6652, 0x6652, 0x6657, 0x6657, + 0x6659, 0x6659, 0x665b, 0x665b, 0x665d, 0x665f, 0x6662, 0x6662, + 0x6664, 0x6669, 0x666b, 0x666b, 0x666e, 0x6670, 0x6673, 0x6674, + 0x6676, 0x6678, 0x667a, 0x667a, 0x6681, 0x6681, 0x6683, 0x6684, + 0x6687, 0x6689, 0x668e, 0x668e, 0x6690, 0x6691, 0x6696, 0x6699, + 0x669d, 0x669d, 0x66a0, 0x66a0, 0x66a2, 0x66a2, 0x66a6, 0x66a6, + 0x66ab, 0x66ab, 0x66ae, 0x66ae, 0x66b2, 0x66b4, 0x66b8, 0x66b9, + 0x66bb, 0x66bc, 0x66be, 0x66bf, 0x66c1, 0x66c1, 0x66c4, 0x66c4, + 0x66c6, 0x66c7, 0x66c9, 0x66c9, 0x66d6, 0x66d6, 0x66d9, 0x66da, + 0x66dc, 0x66dd, 0x66e0, 0x66e0, 0x66e6, 0x66e6, 0x66e9, 0x66e9, + 0x66f0, 0x66f0, 0x66f2, 0x66f5, 0x66f7, 0x6700, 0x6703, 0x6703, + 0x6708, 0x6709, 0x670b, 0x670b, 0x670d, 0x670f, 0x6714, 0x6717, + 0x671b, 0x671b, 0x671d, 0x671f, 0x6726, 0x6728, 0x672a, 0x672e, + 0x6731, 0x6731, 0x6734, 0x6734, 0x6736, 0x6738, 0x673a, 0x673a, + 0x673d, 0x673d, 0x673f, 0x673f, 0x6741, 0x6741, 0x6746, 0x6746, + 0x6749, 0x6749, 0x674e, 0x6751, 0x6753, 0x6753, 0x6756, 0x6756, + 0x6759, 0x6759, 0x675c, 0x675c, 0x675e, 0x6766, 0x676a, 0x676a, + 0x676d, 0x676d, 0x676f, 0x6773, 0x6775, 0x6775, 0x6777, 0x6777, + 0x677b, 0x677c, 0x677e, 0x677f, 0x6785, 0x6785, 0x6787, 0x6787, + 0x6789, 0x6789, 0x678b, 0x678c, 0x678f, 0x6790, 0x6793, 0x6793, + 0x6795, 0x6795, 0x6797, 0x6797, 0x679a, 0x679a, 0x679c, 0x679d, + 0x67a0, 0x67a2, 0x67a6, 0x67a6, 0x67a9, 0x67a9, 0x67af, 0x67b0, + 0x67b3, 0x67b4, 0x67b6, 0x67b9, 0x67bb, 0x67bb, 0x67be, 0x67be, + 0x67c0, 0x67c1, 0x67c4, 0x67c4, 0x67c6, 0x67c6, 0x67ca, 0x67ca, + 0x67ce, 0x67d4, 0x67d8, 0x67d8, 0x67da, 0x67da, 0x67dd, 0x67de, + 0x67e2, 0x67e2, 0x67e4, 0x67e4, 0x67e7, 0x67e7, 0x67e9, 0x67e9, + 0x67ec, 0x67ec, 0x67ee, 0x67f1, 0x67f3, 0x67f6, 0x67fb, 0x67fb, + 0x67fe, 0x67ff, 0x6801, 0x6804, 0x6812, 0x6813, 0x6816, 0x6817, + 0x681e, 0x681e, 0x6821, 0x6822, 0x6829, 0x682b, 0x682f, 0x682f, + 0x6832, 0x6832, 0x6834, 0x6834, 0x6838, 0x6839, 0x683c, 0x683d, + 0x6840, 0x6844, 0x6846, 0x6846, 0x6848, 0x6848, 0x684d, 0x684e, + 0x6850, 0x6854, 0x6859, 0x6859, 0x685c, 0x685d, 0x685f, 0x685f, + 0x6863, 0x6863, 0x6867, 0x6867, 0x686d, 0x686d, 0x6874, 0x6874, + 0x6876, 0x6877, 0x687e, 0x687f, 0x6881, 0x6881, 0x6883, 0x6883, + 0x6885, 0x6885, 0x688d, 0x688d, 0x688f, 0x688f, 0x6893, 0x6894, + 0x6897, 0x6897, 0x689b, 0x689b, 0x689d, 0x689d, 0x689f, 0x68a2, + 0x68a6, 0x68a8, 0x68ad, 0x68ad, 0x68af, 0x68b1, 0x68b3, 0x68b3, + 0x68b5, 0x68b6, 0x68b9, 0x68ba, 0x68bc, 0x68bc, 0x68c4, 0x68c6, + 0x68c8, 0x68cb, 0x68cd, 0x68cd, 0x68cf, 0x68cf, 0x68d2, 0x68d2, + 0x68d4, 0x68d5, 0x68d7, 0x68d8, 0x68da, 0x68da, 0x68df, 0x68e1, + 0x68e3, 0x68e3, 0x68e7, 0x68e8, 0x68ee, 0x68ef, 0x68f2, 0x68f2, + 0x68f9, 0x68fa, 0x6900, 0x6901, 0x6904, 0x6905, 0x6908, 0x6908, + 0x690b, 0x690f, 0x6912, 0x6912, 0x6919, 0x691c, 0x6921, 0x6923, + 0x6925, 0x6928, 0x692a, 0x692a, 0x6930, 0x6930, 0x6934, 0x6934, + 0x6936, 0x6936, 0x6939, 0x6939, 0x693d, 0x693d, 0x693f, 0x693f, + 0x694a, 0x694a, 0x6953, 0x6955, 0x6957, 0x6957, 0x6959, 0x695a, + 0x695c, 0x695e, 0x6960, 0x6963, 0x6968, 0x6968, 0x696a, 0x696b, + 0x696d, 0x696f, 0x6973, 0x6975, 0x6977, 0x6979, 0x697c, 0x697e, + 0x6981, 0x6982, 0x698a, 0x698a, 0x698e, 0x698e, 0x6991, 0x6991, + 0x6994, 0x6995, 0x6998, 0x6998, 0x699b, 0x699c, 0x69a0, 0x69a0, + 0x69a5, 0x69a5, 0x69a7, 0x69a7, 0x69ae, 0x69ae, 0x69b1, 0x69b2, + 0x69b4, 0x69b4, 0x69bb, 0x69bb, 0x69be, 0x69bf, 0x69c1, 0x69c1, + 0x69c3, 0x69c3, 0x69c7, 0x69c7, 0x69ca, 0x69ce, 0x69d0, 0x69d0, + 0x69d3, 0x69d3, 0x69d8, 0x69d9, 0x69dd, 0x69de, 0x69e2, 0x69e2, + 0x69e7, 0x69e8, 0x69ea, 0x69eb, 0x69ed, 0x69ed, 0x69f2, 0x69f2, + 0x69f9, 0x69f9, 0x69fb, 0x69fb, 0x69fd, 0x69fd, 0x69ff, 0x69ff, + 0x6a02, 0x6a02, 0x6a05, 0x6a05, 0x6a0a, 0x6a0c, 0x6a11, 0x6a14, + 0x6a17, 0x6a17, 0x6a19, 0x6a19, 0x6a1b, 0x6a1b, 0x6a1e, 0x6a1f, + 0x6a21, 0x6a23, 0x6a29, 0x6a2b, 0x6a2e, 0x6a2e, 0x6a30, 0x6a30, + 0x6a35, 0x6a36, 0x6a38, 0x6a3a, 0x6a3d, 0x6a3d, 0x6a44, 0x6a44, + 0x6a46, 0x6a48, 0x6a4b, 0x6a4b, 0x6a52, 0x6a53, 0x6a58, 0x6a59, + 0x6a5f, 0x6a5f, 0x6a61, 0x6a62, 0x6a66, 0x6a66, 0x6a6b, 0x6a6b, + 0x6a72, 0x6a73, 0x6a78, 0x6a78, 0x6a7e, 0x6a80, 0x6a84, 0x6a84, + 0x6a89, 0x6a89, 0x6a8d, 0x6a8e, 0x6a90, 0x6a90, 0x6a97, 0x6a97, + 0x6a9c, 0x6a9c, 0x6aa0, 0x6aa0, 0x6aa2, 0x6aa3, 0x6aaa, 0x6aaa, + 0x6aac, 0x6aac, 0x6aae, 0x6aae, 0x6ab3, 0x6ab3, 0x6ab8, 0x6ab8, + 0x6abb, 0x6abb, 0x6ac1, 0x6ac3, 0x6ad1, 0x6ad1, 0x6ad3, 0x6ad3, + 0x6ada, 0x6adb, 0x6ade, 0x6adf, 0x6ae2, 0x6ae2, 0x6ae4, 0x6ae4, + 0x6ae8, 0x6ae8, 0x6aea, 0x6aea, 0x6af6, 0x6af6, 0x6afa, 0x6afb, + 0x6b04, 0x6b05, 0x6b0a, 0x6b0a, 0x6b0c, 0x6b0c, 0x6b12, 0x6b12, + 0x6b16, 0x6b16, 0x6b1d, 0x6b1d, 0x6b1f, 0x6b21, 0x6b23, 0x6b23, + 0x6b27, 0x6b27, 0x6b32, 0x6b32, 0x6b37, 0x6b3a, 0x6b3d, 0x6b3e, + 0x6b43, 0x6b43, 0x6b46, 0x6b47, 0x6b49, 0x6b49, 0x6b4c, 0x6b4c, + 0x6b4e, 0x6b4e, 0x6b50, 0x6b50, 0x6b53, 0x6b54, 0x6b59, 0x6b59, + 0x6b5b, 0x6b5b, 0x6b5f, 0x6b5f, 0x6b61, 0x6b66, 0x6b69, 0x6b6a, + 0x6b6f, 0x6b6f, 0x6b72, 0x6b74, 0x6b77, 0x6b79, 0x6b7b, 0x6b7b, + 0x6b7f, 0x6b80, 0x6b83, 0x6b84, 0x6b86, 0x6b86, 0x6b89, 0x6b8b, + 0x6b8d, 0x6b8d, 0x6b95, 0x6b96, 0x6b98, 0x6b98, 0x6b9e, 0x6b9e, + 0x6ba4, 0x6ba4, 0x6baa, 0x6bab, 0x6bae, 0x6baf, 0x6bb1, 0x6bb5, + 0x6bb7, 0x6bb7, 0x6bba, 0x6bbc, 0x6bbf, 0x6bc1, 0x6bc5, 0x6bc6, + 0x6bcb, 0x6bcb, 0x6bcd, 0x6bcf, 0x6bd2, 0x6bd4, 0x6bd6, 0x6bd8, + 0x6bdb, 0x6bdb, 0x6bdf, 0x6bdf, 0x6beb, 0x6bec, 0x6bef, 0x6bef, + 0x6bf3, 0x6bf3, 0x6c08, 0x6c08, 0x6c0f, 0x6c0f, 0x6c11, 0x6c11, + 0x6c13, 0x6c14, 0x6c17, 0x6c17, 0x6c1b, 0x6c1b, 0x6c23, 0x6c24, + 0x6c34, 0x6c34, 0x6c37, 0x6c38, 0x6c3e, 0x6c42, 0x6c4e, 0x6c4e, + 0x6c50, 0x6c50, 0x6c55, 0x6c55, 0x6c57, 0x6c57, 0x6c5a, 0x6c5a, + 0x6c5c, 0x6c60, 0x6c62, 0x6c62, 0x6c68, 0x6c68, 0x6c6a, 0x6c6a, + 0x6c6d, 0x6c6d, 0x6c6f, 0x6c70, 0x6c72, 0x6c73, 0x6c76, 0x6c76, + 0x6c7a, 0x6c7a, 0x6c7d, 0x6c7e, 0x6c81, 0x6c83, 0x6c85, 0x6c88, + 0x6c8c, 0x6c8d, 0x6c90, 0x6c90, 0x6c92, 0x6c96, 0x6c99, 0x6c9b, + 0x6ca1, 0x6ca2, 0x6cab, 0x6cab, 0x6cae, 0x6cae, 0x6cb1, 0x6cb1, + 0x6cb3, 0x6cb3, 0x6cb8, 0x6cbf, 0x6cc1, 0x6cc2, 0x6cc4, 0x6cc5, + 0x6cc9, 0x6cca, 0x6ccc, 0x6ccc, 0x6cd3, 0x6cd3, 0x6cd5, 0x6cd5, + 0x6cd7, 0x6cd7, 0x6cd9, 0x6cdb, 0x6cdd, 0x6cdd, 0x6ce1, 0x6ce3, + 0x6ce5, 0x6ce5, 0x6ce8, 0x6ce8, 0x6cea, 0x6ceb, 0x6cee, 0x6cf1, + 0x6cf3, 0x6cf3, 0x6d04, 0x6d04, 0x6d0b, 0x6d0c, 0x6d11, 0x6d12, + 0x6d17, 0x6d17, 0x6d19, 0x6d19, 0x6d1b, 0x6d1b, 0x6d1e, 0x6d1f, + 0x6d25, 0x6d25, 0x6d27, 0x6d27, 0x6d29, 0x6d2b, 0x6d32, 0x6d33, + 0x6d35, 0x6d36, 0x6d38, 0x6d39, 0x6d3b, 0x6d3b, 0x6d3d, 0x6d3e, + 0x6d41, 0x6d41, 0x6d44, 0x6d45, 0x6d59, 0x6d5a, 0x6d5c, 0x6d5c, + 0x6d63, 0x6d64, 0x6d66, 0x6d66, 0x6d69, 0x6d6a, 0x6d6c, 0x6d6c, + 0x6d6e, 0x6d6f, 0x6d74, 0x6d74, 0x6d77, 0x6d79, 0x6d7f, 0x6d7f, + 0x6d85, 0x6d85, 0x6d87, 0x6d89, 0x6d8c, 0x6d8e, 0x6d91, 0x6d91, + 0x6d93, 0x6d93, 0x6d95, 0x6d96, 0x6d99, 0x6d99, 0x6d9b, 0x6d9c, + 0x6dac, 0x6dac, 0x6daf, 0x6daf, 0x6db2, 0x6db2, 0x6db5, 0x6db5, + 0x6db8, 0x6db8, 0x6dbc, 0x6dbc, 0x6dc0, 0x6dc0, 0x6dc3, 0x6dc7, + 0x6dcb, 0x6dcc, 0x6dcf, 0x6dcf, 0x6dd1, 0x6dd2, 0x6dd5, 0x6dd5, + 0x6dd8, 0x6dda, 0x6dde, 0x6dde, 0x6de1, 0x6de1, 0x6de4, 0x6de4, + 0x6de6, 0x6de6, 0x6de8, 0x6de8, 0x6dea, 0x6dec, 0x6dee, 0x6dee, + 0x6df1, 0x6df3, 0x6df5, 0x6df5, 0x6df7, 0x6dfc, 0x6e05, 0x6e05, + 0x6e07, 0x6e0b, 0x6e13, 0x6e13, 0x6e15, 0x6e15, 0x6e17, 0x6e17, + 0x6e19, 0x6e1b, 0x6e1d, 0x6e1d, 0x6e1f, 0x6e21, 0x6e23, 0x6e27, + 0x6e29, 0x6e29, 0x6e2b, 0x6e2f, 0x6e32, 0x6e32, 0x6e34, 0x6e34, + 0x6e36, 0x6e36, 0x6e38, 0x6e3a, 0x6e3c, 0x6e3e, 0x6e43, 0x6e44, + 0x6e4a, 0x6e4a, 0x6e4d, 0x6e4e, 0x6e56, 0x6e56, 0x6e58, 0x6e58, + 0x6e5b, 0x6e5c, 0x6e5e, 0x6e5f, 0x6e67, 0x6e67, 0x6e6b, 0x6e6b, + 0x6e6e, 0x6e6f, 0x6e72, 0x6e73, 0x6e76, 0x6e76, 0x6e7a, 0x6e7a, + 0x6e7e, 0x6e80, 0x6e82, 0x6e82, 0x6e8c, 0x6e8c, 0x6e8f, 0x6e90, + 0x6e96, 0x6e96, 0x6e98, 0x6e98, 0x6e9c, 0x6e9d, 0x6e9f, 0x6e9f, + 0x6ea2, 0x6ea2, 0x6ea5, 0x6ea5, 0x6eaa, 0x6eab, 0x6eaf, 0x6eaf, + 0x6eb1, 0x6eb2, 0x6eb6, 0x6eb7, 0x6eba, 0x6eba, 0x6ebd, 0x6ebd, + 0x6ebf, 0x6ebf, 0x6ec2, 0x6ec2, 0x6ec4, 0x6ec5, 0x6ec9, 0x6ec9, + 0x6ecb, 0x6ecc, 0x6ece, 0x6ece, 0x6ed1, 0x6ed1, 0x6ed3, 0x6ed5, + 0x6edd, 0x6ede, 0x6eec, 0x6eec, 0x6eef, 0x6eef, 0x6ef2, 0x6ef2, + 0x6ef4, 0x6ef4, 0x6ef7, 0x6ef8, 0x6efe, 0x6eff, 0x6f01, 0x6f02, + 0x6f06, 0x6f06, 0x6f09, 0x6f09, 0x6f0f, 0x6f0f, 0x6f11, 0x6f11, + 0x6f13, 0x6f15, 0x6f20, 0x6f20, 0x6f22, 0x6f23, 0x6f2b, 0x6f2c, + 0x6f31, 0x6f32, 0x6f38, 0x6f38, 0x6f3e, 0x6f3f, 0x6f41, 0x6f41, + 0x6f45, 0x6f45, 0x6f51, 0x6f51, 0x6f54, 0x6f54, 0x6f57, 0x6f58, + 0x6f5a, 0x6f5c, 0x6f5e, 0x6f5f, 0x6f62, 0x6f62, 0x6f64, 0x6f64, + 0x6f66, 0x6f66, 0x6f6d, 0x6f70, 0x6f74, 0x6f74, 0x6f78, 0x6f78, + 0x6f7a, 0x6f7a, 0x6f7c, 0x6f7e, 0x6f80, 0x6f82, 0x6f84, 0x6f84, + 0x6f86, 0x6f86, 0x6f88, 0x6f88, 0x6f8d, 0x6f8e, 0x6f90, 0x6f91, + 0x6f94, 0x6f94, 0x6f97, 0x6f97, 0x6fa1, 0x6fa1, 0x6fa3, 0x6fa4, + 0x6fa7, 0x6fa7, 0x6faa, 0x6faa, 0x6fae, 0x6faf, 0x6fb1, 0x6fb1, + 0x6fb3, 0x6fb3, 0x6fb5, 0x6fb5, 0x6fb9, 0x6fb9, 0x6fbe, 0x6fbe, + 0x6fc0, 0x6fc3, 0x6fc6, 0x6fc6, 0x6fca, 0x6fca, 0x6fd4, 0x6fd5, + 0x6fd8, 0x6fd8, 0x6fda, 0x6fdb, 0x6fdf, 0x6fe1, 0x6fe4, 0x6fe4, + 0x6fe9, 0x6fe9, 0x6feb, 0x6fec, 0x6fee, 0x6fef, 0x6ff1, 0x6ff1, + 0x6ff3, 0x6ff3, 0x6ff5, 0x6ff6, 0x6ffa, 0x6ffa, 0x6ffe, 0x6ffe, + 0x7001, 0x7001, 0x7005, 0x7007, 0x7009, 0x7009, 0x700b, 0x700b, + 0x700f, 0x700f, 0x7011, 0x7011, 0x7015, 0x7015, 0x7018, 0x7018, + 0x701a, 0x701f, 0x7023, 0x7023, 0x7026, 0x7028, 0x702c, 0x702c, + 0x702f, 0x7030, 0x7032, 0x7032, 0x7037, 0x7037, 0x703e, 0x703e, + 0x704c, 0x704c, 0x7050, 0x7051, 0x7058, 0x7058, 0x705d, 0x705d, + 0x7063, 0x7063, 0x706b, 0x706b, 0x706f, 0x7070, 0x7078, 0x7078, + 0x707c, 0x707d, 0x7085, 0x7085, 0x7089, 0x708a, 0x708e, 0x708e, + 0x7092, 0x7092, 0x7098, 0x709a, 0x70a1, 0x70a1, 0x70a4, 0x70a4, + 0x70ab, 0x70af, 0x70b3, 0x70b3, 0x70b7, 0x70bb, 0x70c8, 0x70c8, + 0x70cb, 0x70cb, 0x70cf, 0x70cf, 0x70d8, 0x70d9, 0x70dd, 0x70dd, + 0x70df, 0x70df, 0x70f1, 0x70f1, 0x70f9, 0x70f9, 0x70fd, 0x70fd, + 0x7104, 0x7104, 0x7109, 0x7109, 0x710c, 0x710c, 0x710f, 0x710f, + 0x7114, 0x7114, 0x7119, 0x711a, 0x711c, 0x711c, 0x711e, 0x711e, + 0x7121, 0x7121, 0x7126, 0x7126, 0x7130, 0x7130, 0x7136, 0x7136, + 0x713c, 0x713c, 0x7146, 0x7147, 0x7149, 0x714a, 0x714c, 0x714c, + 0x714e, 0x714e, 0x7150, 0x7150, 0x7155, 0x7156, 0x7159, 0x7159, + 0x715c, 0x715c, 0x715e, 0x715e, 0x7162, 0x7162, 0x7164, 0x7167, + 0x7169, 0x7169, 0x716c, 0x716c, 0x716e, 0x716e, 0x717d, 0x717d, + 0x7184, 0x7184, 0x7188, 0x718a, 0x718f, 0x718f, 0x7192, 0x7192, + 0x7194, 0x7195, 0x7199, 0x7199, 0x719f, 0x719f, 0x71a2, 0x71a2, + 0x71a8, 0x71a8, 0x71ac, 0x71ac, 0x71b1, 0x71b1, 0x71b9, 0x71ba, + 0x71be, 0x71be, 0x71c1, 0x71c1, 0x71c3, 0x71c3, 0x71c8, 0x71c9, + 0x71ce, 0x71ce, 0x71d0, 0x71d0, 0x71d2, 0x71d2, 0x71d4, 0x71d5, + 0x71d7, 0x71d7, 0x71df, 0x71e0, 0x71e5, 0x71e7, 0x71ec, 0x71ee, + 0x71f5, 0x71f5, 0x71f9, 0x71f9, 0x71fb, 0x71fc, 0x71fe, 0x7200, + 0x7206, 0x7206, 0x720d, 0x720d, 0x7210, 0x7210, 0x721b, 0x721b, + 0x7228, 0x7228, 0x722a, 0x722a, 0x722c, 0x722d, 0x7230, 0x7230, + 0x7232, 0x7232, 0x7235, 0x7236, 0x723a, 0x7240, 0x7246, 0x7248, + 0x724b, 0x724c, 0x7252, 0x7252, 0x7258, 0x7259, 0x725b, 0x725b, + 0x725d, 0x725d, 0x725f, 0x725f, 0x7261, 0x7262, 0x7267, 0x7267, + 0x7269, 0x7269, 0x7272, 0x7272, 0x7274, 0x7274, 0x7279, 0x7279, + 0x727d, 0x727e, 0x7280, 0x7282, 0x7287, 0x7287, 0x7292, 0x7292, + 0x7296, 0x7296, 0x72a0, 0x72a0, 0x72a2, 0x72a2, 0x72a7, 0x72a7, + 0x72ac, 0x72ac, 0x72af, 0x72af, 0x72b1, 0x72b2, 0x72b6, 0x72b6, + 0x72b9, 0x72b9, 0x72be, 0x72be, 0x72c0, 0x72c0, 0x72c2, 0x72c4, + 0x72c6, 0x72c6, 0x72ce, 0x72ce, 0x72d0, 0x72d0, 0x72d2, 0x72d2, + 0x72d7, 0x72d7, 0x72d9, 0x72d9, 0x72db, 0x72db, 0x72e0, 0x72e2, + 0x72e9, 0x72e9, 0x72ec, 0x72ed, 0x72f7, 0x72f9, 0x72fc, 0x72fd, + 0x730a, 0x730a, 0x7316, 0x7317, 0x731b, 0x731d, 0x731f, 0x731f, + 0x7324, 0x7325, 0x7329, 0x732b, 0x732e, 0x732f, 0x7334, 0x7334, + 0x7336, 0x7337, 0x733e, 0x733f, 0x7344, 0x7345, 0x734e, 0x7350, + 0x7352, 0x7352, 0x7357, 0x7357, 0x7363, 0x7363, 0x7368, 0x7368, + 0x736a, 0x736a, 0x7370, 0x7370, 0x7372, 0x7372, 0x7375, 0x7375, + 0x7377, 0x7378, 0x737a, 0x737b, 0x7384, 0x7384, 0x7386, 0x7387, + 0x7389, 0x7389, 0x738b, 0x738b, 0x738e, 0x738e, 0x7394, 0x7394, + 0x7396, 0x7398, 0x739f, 0x739f, 0x73a7, 0x73a7, 0x73a9, 0x73a9, + 0x73ad, 0x73ad, 0x73b2, 0x73b3, 0x73b9, 0x73b9, 0x73bb, 0x73bb, + 0x73bd, 0x73bd, 0x73c0, 0x73c0, 0x73c2, 0x73c2, 0x73c8, 0x73ca, + 0x73cc, 0x73cf, 0x73d2, 0x73d2, 0x73d6, 0x73d6, 0x73d9, 0x73d9, + 0x73dd, 0x73de, 0x73e0, 0x73e0, 0x73e3, 0x73e6, 0x73e9, 0x73ea, + 0x73ed, 0x73ee, 0x73f1, 0x73f1, 0x73f5, 0x73f5, 0x73f7, 0x73f9, + 0x73fd, 0x73fe, 0x7401, 0x7401, 0x7403, 0x7403, 0x7405, 0x7407, + 0x7409, 0x7409, 0x7413, 0x7413, 0x741b, 0x741b, 0x7420, 0x7422, + 0x7425, 0x7426, 0x7428, 0x742c, 0x742e, 0x7430, 0x7432, 0x7436, + 0x7438, 0x7438, 0x743a, 0x743a, 0x743f, 0x7441, 0x7443, 0x7444, + 0x744b, 0x744b, 0x7455, 0x7455, 0x7457, 0x7457, 0x7459, 0x745c, + 0x745e, 0x7460, 0x7462, 0x7465, 0x7468, 0x746a, 0x746f, 0x7470, + 0x7473, 0x7473, 0x7476, 0x7476, 0x747e, 0x747e, 0x7482, 0x7483, + 0x7487, 0x7487, 0x7489, 0x7489, 0x748b, 0x748b, 0x7498, 0x7498, + 0x749c, 0x749c, 0x749e, 0x749f, 0x74a1, 0x74a3, 0x74a5, 0x74a5, + 0x74a7, 0x74a8, 0x74aa, 0x74aa, 0x74b0, 0x74b0, 0x74b2, 0x74b2, + 0x74b5, 0x74b5, 0x74b9, 0x74b9, 0x74bd, 0x74bd, 0x74bf, 0x74bf, + 0x74c6, 0x74c6, 0x74ca, 0x74ca, 0x74cf, 0x74cf, 0x74d4, 0x74d4, + 0x74d8, 0x74d8, 0x74da, 0x74da, 0x74dc, 0x74dc, 0x74e0, 0x74e0, + 0x74e2, 0x74e3, 0x74e6, 0x74e7, 0x74e9, 0x74e9, 0x74ee, 0x74ee, + 0x74f0, 0x74f2, 0x74f6, 0x74f8, 0x7501, 0x7501, 0x7503, 0x7505, + 0x750c, 0x750e, 0x7511, 0x7511, 0x7513, 0x7513, 0x7515, 0x7515, + 0x7518, 0x7518, 0x751a, 0x751c, 0x751e, 0x751f, 0x7523, 0x7523, + 0x7525, 0x7526, 0x7528, 0x7528, 0x752b, 0x752c, 0x752f, 0x7533, + 0x7537, 0x7538, 0x753a, 0x753c, 0x7544, 0x7544, 0x7546, 0x7547, + 0x7549, 0x754d, 0x754f, 0x754f, 0x7551, 0x7551, 0x7553, 0x7554, + 0x7559, 0x755d, 0x7560, 0x7560, 0x7562, 0x7562, 0x7564, 0x7567, + 0x7569, 0x756b, 0x756d, 0x756d, 0x756f, 0x7570, 0x7573, 0x7578, + 0x757a, 0x757a, 0x757f, 0x757f, 0x7582, 0x7582, 0x7586, 0x7587, + 0x7589, 0x758b, 0x758e, 0x758f, 0x7591, 0x7591, 0x7594, 0x7594, + 0x759a, 0x759a, 0x759d, 0x759d, 0x75a3, 0x75a3, 0x75a5, 0x75a5, + 0x75ab, 0x75ab, 0x75b1, 0x75b3, 0x75b5, 0x75b5, 0x75b8, 0x75b9, + 0x75bc, 0x75be, 0x75c2, 0x75c3, 0x75c5, 0x75c5, 0x75c7, 0x75c7, + 0x75ca, 0x75ca, 0x75cd, 0x75cd, 0x75d2, 0x75d2, 0x75d4, 0x75d5, + 0x75d8, 0x75d9, 0x75db, 0x75db, 0x75de, 0x75de, 0x75e2, 0x75e3, + 0x75e9, 0x75e9, 0x75f0, 0x75f0, 0x75f2, 0x75f4, 0x75fa, 0x75fa, + 0x75fc, 0x75fc, 0x75fe, 0x7601, 0x7609, 0x7609, 0x760b, 0x760b, + 0x760d, 0x760d, 0x7619, 0x7619, 0x761f, 0x7622, 0x7624, 0x7624, + 0x7626, 0x7627, 0x7630, 0x7630, 0x7634, 0x7634, 0x763b, 0x763b, + 0x7642, 0x7642, 0x7646, 0x7648, 0x764c, 0x764c, 0x764e, 0x764e, + 0x7652, 0x7652, 0x7656, 0x7656, 0x7658, 0x7658, 0x765c, 0x765c, + 0x7661, 0x7662, 0x7664, 0x7664, 0x7667, 0x766a, 0x766c, 0x766c, + 0x7670, 0x7670, 0x7672, 0x7672, 0x7676, 0x7676, 0x7678, 0x7678, + 0x767a, 0x767e, 0x7680, 0x7680, 0x7682, 0x7684, 0x7686, 0x7688, + 0x768b, 0x768b, 0x768e, 0x768e, 0x7690, 0x7690, 0x7693, 0x7693, + 0x7696, 0x7696, 0x7699, 0x769c, 0x769e, 0x769e, 0x76a6, 0x76a6, + 0x76ae, 0x76ae, 0x76b0, 0x76b0, 0x76b4, 0x76b4, 0x76b7, 0x76ba, + 0x76bf, 0x76bf, 0x76c2, 0x76c3, 0x76c6, 0x76c6, 0x76c8, 0x76c8, + 0x76ca, 0x76ca, 0x76cd, 0x76cd, 0x76d2, 0x76d2, 0x76d6, 0x76d7, + 0x76db, 0x76dc, 0x76de, 0x76df, 0x76e1, 0x76e1, 0x76e3, 0x76e5, + 0x76e7, 0x76e7, 0x76ea, 0x76ea, 0x76ee, 0x76ee, 0x76f2, 0x76f2, + 0x76f4, 0x76f4, 0x76f8, 0x76f8, 0x76fb, 0x76fc, 0x76fe, 0x76fe, + 0x7701, 0x7701, 0x7704, 0x7704, 0x7707, 0x7709, 0x770b, 0x770c, + 0x771b, 0x771b, 0x771e, 0x7720, 0x7724, 0x7726, 0x7729, 0x7729, + 0x7737, 0x7738, 0x773a, 0x773a, 0x773c, 0x773c, 0x7740, 0x7740, + 0x7746, 0x7747, 0x774d, 0x774d, 0x775a, 0x775b, 0x7761, 0x7761, + 0x7763, 0x7763, 0x7765, 0x7766, 0x7768, 0x7768, 0x776b, 0x776b, + 0x7779, 0x7779, 0x777e, 0x777f, 0x778b, 0x778b, 0x778e, 0x778e, + 0x7791, 0x7791, 0x779e, 0x779e, 0x77a0, 0x77a0, 0x77a5, 0x77a5, + 0x77ac, 0x77ad, 0x77b0, 0x77b0, 0x77b3, 0x77b3, 0x77b6, 0x77b6, + 0x77b9, 0x77b9, 0x77bb, 0x77bd, 0x77bf, 0x77bf, 0x77c7, 0x77c7, + 0x77cd, 0x77cd, 0x77d7, 0x77d7, 0x77da, 0x77dc, 0x77e2, 0x77e3, + 0x77e5, 0x77e5, 0x77e7, 0x77e7, 0x77e9, 0x77e9, 0x77ed, 0x77ef, + 0x77f3, 0x77f3, 0x77fc, 0x77fc, 0x7802, 0x7802, 0x780c, 0x780c, + 0x7812, 0x7812, 0x7814, 0x7815, 0x7820, 0x7821, 0x7825, 0x7827, + 0x782c, 0x782c, 0x7832, 0x7832, 0x7834, 0x7834, 0x783a, 0x783a, + 0x783f, 0x783f, 0x7845, 0x7845, 0x784e, 0x784f, 0x785d, 0x785d, + 0x7864, 0x7864, 0x786b, 0x786c, 0x786f, 0x786f, 0x7872, 0x7872, + 0x7874, 0x7874, 0x787a, 0x787a, 0x787c, 0x787c, 0x7881, 0x7881, + 0x7886, 0x7887, 0x788c, 0x788e, 0x7891, 0x7891, 0x7893, 0x7893, + 0x7895, 0x7895, 0x7897, 0x7897, 0x789a, 0x789a, 0x78a3, 0x78a3, + 0x78a7, 0x78a7, 0x78a9, 0x78aa, 0x78af, 0x78af, 0x78b5, 0x78b5, + 0x78ba, 0x78bc, 0x78be, 0x78be, 0x78c1, 0x78c1, 0x78c5, 0x78c6, + 0x78ca, 0x78cb, 0x78ce, 0x78ce, 0x78d0, 0x78d1, 0x78d4, 0x78d4, + 0x78da, 0x78da, 0x78e7, 0x78e8, 0x78ec, 0x78ec, 0x78ef, 0x78ef, + 0x78f4, 0x78f5, 0x78fb, 0x78fb, 0x78fd, 0x78fd, 0x7901, 0x7901, + 0x7907, 0x7907, 0x790e, 0x790e, 0x7911, 0x7912, 0x7916, 0x7916, + 0x7919, 0x7919, 0x7926, 0x7926, 0x792a, 0x792c, 0x7930, 0x7930, + 0x793a, 0x793a, 0x793c, 0x793c, 0x793e, 0x793e, 0x7940, 0x7941, + 0x7947, 0x7949, 0x7950, 0x7950, 0x7953, 0x7953, 0x7955, 0x7957, + 0x795a, 0x7960, 0x7962, 0x7962, 0x7965, 0x7965, 0x7968, 0x7968, + 0x796d, 0x796d, 0x7977, 0x7977, 0x797a, 0x797a, 0x797f, 0x7981, + 0x7984, 0x7985, 0x798a, 0x798a, 0x798d, 0x798f, 0x7991, 0x7991, + 0x7994, 0x7994, 0x799b, 0x799b, 0x799d, 0x799d, 0x79a6, 0x79a7, + 0x79aa, 0x79aa, 0x79ae, 0x79ae, 0x79b0, 0x79b1, 0x79b3, 0x79b3, + 0x79b9, 0x79ba, 0x79bd, 0x79c1, 0x79c9, 0x79cb, 0x79d1, 0x79d2, + 0x79d5, 0x79d5, 0x79d8, 0x79d8, 0x79df, 0x79df, 0x79e1, 0x79e1, + 0x79e3, 0x79e4, 0x79e6, 0x79e7, 0x79e9, 0x79e9, 0x79ec, 0x79ec, + 0x79f0, 0x79f0, 0x79fb, 0x79fb, 0x7a00, 0x7a00, 0x7a05, 0x7a05, + 0x7a08, 0x7a08, 0x7a0b, 0x7a0b, 0x7a0d, 0x7a0e, 0x7a14, 0x7a14, + 0x7a17, 0x7a1a, 0x7a1c, 0x7a1c, 0x7a1f, 0x7a20, 0x7a2e, 0x7a2e, + 0x7a31, 0x7a32, 0x7a36, 0x7a37, 0x7a3b, 0x7a40, 0x7a42, 0x7a43, + 0x7a46, 0x7a46, 0x7a49, 0x7a49, 0x7a4d, 0x7a50, 0x7a57, 0x7a57, + 0x7a61, 0x7a63, 0x7a69, 0x7a69, 0x7a6b, 0x7a6b, 0x7a70, 0x7a70, + 0x7a74, 0x7a74, 0x7a76, 0x7a76, 0x7a79, 0x7a7a, 0x7a7d, 0x7a7d, + 0x7a7f, 0x7a7f, 0x7a81, 0x7a81, 0x7a83, 0x7a84, 0x7a88, 0x7a88, + 0x7a92, 0x7a93, 0x7a95, 0x7a98, 0x7a9f, 0x7a9f, 0x7aa9, 0x7aaa, + 0x7aae, 0x7ab0, 0x7ab6, 0x7ab6, 0x7aba, 0x7aba, 0x7abf, 0x7abf, + 0x7ac3, 0x7ac5, 0x7ac7, 0x7ac8, 0x7aca, 0x7acb, 0x7acd, 0x7acd, + 0x7acf, 0x7acf, 0x7ad1, 0x7ad3, 0x7ad5, 0x7ad5, 0x7ad7, 0x7ad7, + 0x7ad9, 0x7ada, 0x7adc, 0x7add, 0x7adf, 0x7ae3, 0x7ae5, 0x7ae7, + 0x7aea, 0x7aeb, 0x7aed, 0x7aed, 0x7aef, 0x7af0, 0x7af6, 0x7af6, + 0x7af8, 0x7afa, 0x7aff, 0x7aff, 0x7b02, 0x7b02, 0x7b04, 0x7b04, + 0x7b06, 0x7b06, 0x7b08, 0x7b08, 0x7b0a, 0x7b0b, 0x7b0f, 0x7b0f, + 0x7b11, 0x7b11, 0x7b18, 0x7b19, 0x7b1b, 0x7b1b, 0x7b1e, 0x7b1e, + 0x7b20, 0x7b20, 0x7b25, 0x7b26, 0x7b28, 0x7b28, 0x7b2c, 0x7b2d, + 0x7b33, 0x7b33, 0x7b35, 0x7b36, 0x7b39, 0x7b39, 0x7b45, 0x7b46, + 0x7b48, 0x7b49, 0x7b4b, 0x7b4d, 0x7b4f, 0x7b52, 0x7b54, 0x7b54, + 0x7b56, 0x7b56, 0x7b5d, 0x7b5d, 0x7b60, 0x7b60, 0x7b65, 0x7b65, + 0x7b67, 0x7b67, 0x7b6c, 0x7b6c, 0x7b6e, 0x7b6e, 0x7b70, 0x7b71, + 0x7b74, 0x7b75, 0x7b7a, 0x7b7a, 0x7b7d, 0x7b7d, 0x7b86, 0x7b87, + 0x7b8b, 0x7b8b, 0x7b8d, 0x7b8d, 0x7b8f, 0x7b8f, 0x7b92, 0x7b92, + 0x7b94, 0x7b95, 0x7b97, 0x7b9a, 0x7b9c, 0x7b9f, 0x7ba1, 0x7ba1, + 0x7baa, 0x7baa, 0x7bad, 0x7bad, 0x7bb1, 0x7bb1, 0x7bb4, 0x7bb4, + 0x7bb8, 0x7bb8, 0x7bc0, 0x7bc1, 0x7bc4, 0x7bc4, 0x7bc6, 0x7bc7, + 0x7bc9, 0x7bc9, 0x7bcb, 0x7bcc, 0x7bcf, 0x7bcf, 0x7bd2, 0x7bd2, + 0x7bdd, 0x7bdd, 0x7be0, 0x7be0, 0x7be4, 0x7be6, 0x7be9, 0x7be9, + 0x7bed, 0x7bed, 0x7bf3, 0x7bf3, 0x7bf6, 0x7bf7, 0x7c00, 0x7c00, + 0x7c07, 0x7c07, 0x7c0d, 0x7c0d, 0x7c11, 0x7c14, 0x7c17, 0x7c17, + 0x7c1e, 0x7c1f, 0x7c21, 0x7c21, 0x7c23, 0x7c23, 0x7c27, 0x7c27, + 0x7c2a, 0x7c2b, 0x7c37, 0x7c38, 0x7c3d, 0x7c40, 0x7c43, 0x7c43, + 0x7c4c, 0x7c4d, 0x7c4f, 0x7c50, 0x7c54, 0x7c54, 0x7c56, 0x7c56, + 0x7c58, 0x7c58, 0x7c5f, 0x7c60, 0x7c64, 0x7c65, 0x7c6c, 0x7c6c, + 0x7c73, 0x7c73, 0x7c75, 0x7c75, 0x7c7e, 0x7c7e, 0x7c81, 0x7c83, + 0x7c89, 0x7c89, 0x7c8b, 0x7c8b, 0x7c8d, 0x7c8d, 0x7c90, 0x7c90, + 0x7c92, 0x7c92, 0x7c95, 0x7c95, 0x7c97, 0x7c98, 0x7c9b, 0x7c9b, + 0x7c9f, 0x7c9f, 0x7ca1, 0x7ca2, 0x7ca4, 0x7ca5, 0x7ca7, 0x7ca8, + 0x7cab, 0x7cab, 0x7cad, 0x7cae, 0x7cb1, 0x7cb3, 0x7cb9, 0x7cb9, + 0x7cbd, 0x7cbe, 0x7cc0, 0x7cc0, 0x7cc2, 0x7cc2, 0x7cc5, 0x7cc5, + 0x7cca, 0x7cca, 0x7cce, 0x7cce, 0x7cd2, 0x7cd2, 0x7cd6, 0x7cd6, + 0x7cd8, 0x7cd8, 0x7cdc, 0x7cdc, 0x7cde, 0x7ce0, 0x7ce2, 0x7ce2, + 0x7ce7, 0x7ce7, 0x7cef, 0x7cef, 0x7cf2, 0x7cf2, 0x7cf4, 0x7cf4, + 0x7cf6, 0x7cf6, 0x7cf8, 0x7cf8, 0x7cfa, 0x7cfb, 0x7cfe, 0x7cfe, + 0x7d00, 0x7d00, 0x7d02, 0x7d02, 0x7d04, 0x7d08, 0x7d0a, 0x7d0b, + 0x7d0d, 0x7d0d, 0x7d10, 0x7d10, 0x7d14, 0x7d15, 0x7d17, 0x7d1c, + 0x7d20, 0x7d22, 0x7d2b, 0x7d2c, 0x7d2e, 0x7d30, 0x7d32, 0x7d33, + 0x7d35, 0x7d35, 0x7d39, 0x7d3a, 0x7d3f, 0x7d3f, 0x7d42, 0x7d46, + 0x7d48, 0x7d48, 0x7d4b, 0x7d4c, 0x7d4e, 0x7d50, 0x7d56, 0x7d56, + 0x7d5b, 0x7d5c, 0x7d5e, 0x7d5e, 0x7d61, 0x7d63, 0x7d66, 0x7d66, + 0x7d68, 0x7d68, 0x7d6a, 0x7d6a, 0x7d6e, 0x7d6e, 0x7d71, 0x7d73, + 0x7d75, 0x7d76, 0x7d79, 0x7d79, 0x7d7d, 0x7d7d, 0x7d7f, 0x7d7f, + 0x7d89, 0x7d89, 0x7d8e, 0x7d8f, 0x7d93, 0x7d93, 0x7d99, 0x7d9c, + 0x7d9f, 0x7da0, 0x7da2, 0x7da3, 0x7dab, 0x7db2, 0x7db4, 0x7db5, + 0x7db7, 0x7db8, 0x7dba, 0x7dbb, 0x7dbd, 0x7dbf, 0x7dc7, 0x7dc7, + 0x7dca, 0x7dcb, 0x7dcf, 0x7dcf, 0x7dd1, 0x7dd2, 0x7dd5, 0x7dd6, + 0x7dd8, 0x7dd8, 0x7dda, 0x7dda, 0x7ddc, 0x7dde, 0x7de0, 0x7de1, + 0x7de3, 0x7de4, 0x7de8, 0x7de9, 0x7dec, 0x7dec, 0x7def, 0x7def, + 0x7df2, 0x7df2, 0x7df4, 0x7df4, 0x7dfb, 0x7dfb, 0x7e01, 0x7e01, + 0x7e04, 0x7e05, 0x7e09, 0x7e0b, 0x7e12, 0x7e12, 0x7e15, 0x7e15, + 0x7e1b, 0x7e1b, 0x7e1d, 0x7e1f, 0x7e21, 0x7e23, 0x7e26, 0x7e26, + 0x7e2b, 0x7e2b, 0x7e2e, 0x7e2f, 0x7e31, 0x7e32, 0x7e35, 0x7e35, + 0x7e37, 0x7e37, 0x7e39, 0x7e3b, 0x7e3d, 0x7e3e, 0x7e41, 0x7e41, + 0x7e43, 0x7e43, 0x7e46, 0x7e47, 0x7e4a, 0x7e4b, 0x7e4d, 0x7e4d, + 0x7e52, 0x7e52, 0x7e54, 0x7e56, 0x7e59, 0x7e5a, 0x7e5d, 0x7e5e, + 0x7e61, 0x7e61, 0x7e66, 0x7e67, 0x7e69, 0x7e6b, 0x7e6d, 0x7e6d, + 0x7e70, 0x7e70, 0x7e79, 0x7e79, 0x7e7b, 0x7e7d, 0x7e7f, 0x7e7f, + 0x7e82, 0x7e83, 0x7e88, 0x7e8a, 0x7e8c, 0x7e8c, 0x7e8e, 0x7e90, + 0x7e92, 0x7e94, 0x7e96, 0x7e96, 0x7e98, 0x7e98, 0x7e9b, 0x7e9c, + 0x7f36, 0x7f36, 0x7f38, 0x7f38, 0x7f3a, 0x7f3a, 0x7f45, 0x7f45, + 0x7f47, 0x7f47, 0x7f4c, 0x7f4e, 0x7f50, 0x7f51, 0x7f54, 0x7f55, + 0x7f58, 0x7f58, 0x7f5f, 0x7f60, 0x7f67, 0x7f6b, 0x7f6e, 0x7f6e, + 0x7f70, 0x7f70, 0x7f72, 0x7f72, 0x7f75, 0x7f75, 0x7f77, 0x7f79, + 0x7f82, 0x7f83, 0x7f85, 0x7f88, 0x7f8a, 0x7f8a, 0x7f8c, 0x7f8c, + 0x7f8e, 0x7f8e, 0x7f94, 0x7f94, 0x7f9a, 0x7f9a, 0x7f9d, 0x7f9e, + 0x7fa1, 0x7fa1, 0x7fa3, 0x7fa4, 0x7fa8, 0x7fa9, 0x7fae, 0x7faf, + 0x7fb2, 0x7fb2, 0x7fb6, 0x7fb6, 0x7fb8, 0x7fb9, 0x7fbd, 0x7fbd, + 0x7fc1, 0x7fc1, 0x7fc5, 0x7fc6, 0x7fca, 0x7fca, 0x7fcc, 0x7fcc, + 0x7fce, 0x7fce, 0x7fd2, 0x7fd2, 0x7fd4, 0x7fd5, 0x7fdf, 0x7fe1, + 0x7fe6, 0x7fe6, 0x7fe9, 0x7fe9, 0x7feb, 0x7feb, 0x7ff0, 0x7ff0, + 0x7ff3, 0x7ff3, 0x7ff9, 0x7ff9, 0x7ffb, 0x7ffc, 0x8000, 0x8001, + 0x8003, 0x8006, 0x8009, 0x8009, 0x800b, 0x800c, 0x8010, 0x8010, + 0x8012, 0x8012, 0x8015, 0x8015, 0x8017, 0x8019, 0x801c, 0x801c, + 0x8021, 0x8021, 0x8028, 0x8028, 0x802d, 0x802d, 0x8033, 0x8033, + 0x8036, 0x8036, 0x803b, 0x803b, 0x803d, 0x803d, 0x803f, 0x803f, + 0x8043, 0x8043, 0x8046, 0x8046, 0x804a, 0x804a, 0x8052, 0x8052, + 0x8056, 0x8056, 0x8058, 0x8058, 0x805a, 0x805a, 0x805e, 0x805f, + 0x8061, 0x8062, 0x8068, 0x8068, 0x806f, 0x8070, 0x8072, 0x8074, + 0x8076, 0x8077, 0x8079, 0x8079, 0x807d, 0x807f, 0x8084, 0x8087, + 0x8089, 0x8089, 0x808b, 0x808c, 0x8093, 0x8093, 0x8096, 0x8096, + 0x8098, 0x8098, 0x809a, 0x809b, 0x809d, 0x809d, 0x80a1, 0x80a2, + 0x80a5, 0x80a5, 0x80a9, 0x80aa, 0x80ac, 0x80ad, 0x80af, 0x80af, + 0x80b1, 0x80b2, 0x80b4, 0x80b4, 0x80ba, 0x80ba, 0x80c3, 0x80c4, + 0x80c6, 0x80c6, 0x80cc, 0x80cc, 0x80ce, 0x80ce, 0x80d6, 0x80d6, + 0x80d9, 0x80db, 0x80dd, 0x80de, 0x80e1, 0x80e1, 0x80e4, 0x80e5, + 0x80ef, 0x80ef, 0x80f1, 0x80f1, 0x80f4, 0x80f4, 0x80f8, 0x80f8, + 0x80fc, 0x80fd, 0x8102, 0x8102, 0x8105, 0x810a, 0x8118, 0x8118, + 0x811a, 0x811b, 0x8123, 0x8123, 0x8129, 0x8129, 0x812b, 0x812b, + 0x812f, 0x812f, 0x8131, 0x8131, 0x8133, 0x8133, 0x8139, 0x8139, + 0x813e, 0x813e, 0x8146, 0x8146, 0x814b, 0x814b, 0x814e, 0x814e, + 0x8150, 0x8151, 0x8153, 0x8155, 0x815f, 0x815f, 0x8165, 0x8166, + 0x816b, 0x816b, 0x816e, 0x816e, 0x8170, 0x8171, 0x8174, 0x8174, + 0x8178, 0x817a, 0x817f, 0x8180, 0x8182, 0x8183, 0x8188, 0x8188, + 0x818a, 0x818a, 0x818f, 0x818f, 0x8193, 0x8193, 0x8195, 0x8195, + 0x819a, 0x819a, 0x819c, 0x819d, 0x81a0, 0x81a0, 0x81a3, 0x81a4, + 0x81a8, 0x81a9, 0x81b0, 0x81b0, 0x81b3, 0x81b3, 0x81b5, 0x81b5, + 0x81b8, 0x81b8, 0x81ba, 0x81ba, 0x81bd, 0x81c0, 0x81c2, 0x81c2, + 0x81c6, 0x81c6, 0x81c8, 0x81c9, 0x81cd, 0x81cd, 0x81d1, 0x81d1, + 0x81d3, 0x81d3, 0x81d8, 0x81da, 0x81df, 0x81e0, 0x81e3, 0x81e3, + 0x81e5, 0x81e5, 0x81e7, 0x81e8, 0x81ea, 0x81ea, 0x81ed, 0x81ed, + 0x81f3, 0x81f4, 0x81fa, 0x81fc, 0x81fe, 0x81fe, 0x8201, 0x8202, + 0x8205, 0x8205, 0x8207, 0x820a, 0x820c, 0x820e, 0x8210, 0x8210, + 0x8212, 0x8212, 0x8216, 0x8218, 0x821b, 0x821c, 0x821e, 0x821f, + 0x8221, 0x8221, 0x8229, 0x822c, 0x822e, 0x822e, 0x8233, 0x8233, + 0x8235, 0x8239, 0x8240, 0x8240, 0x8245, 0x8245, 0x8247, 0x8247, + 0x8258, 0x825a, 0x825d, 0x825d, 0x825f, 0x825f, 0x8262, 0x8262, + 0x8264, 0x8264, 0x8266, 0x8266, 0x8268, 0x8268, 0x826a, 0x826b, + 0x826e, 0x826f, 0x8271, 0x8272, 0x8276, 0x8278, 0x827e, 0x827e, + 0x828b, 0x828b, 0x828d, 0x828e, 0x8292, 0x8292, 0x8299, 0x829a, + 0x829d, 0x829d, 0x829f, 0x829f, 0x82a5, 0x82a6, 0x82a9, 0x82a9, + 0x82ab, 0x82af, 0x82b1, 0x82b1, 0x82b3, 0x82b3, 0x82b7, 0x82b9, + 0x82bb, 0x82bd, 0x82bf, 0x82bf, 0x82c5, 0x82c5, 0x82d1, 0x82d5, + 0x82d7, 0x82d7, 0x82d9, 0x82d9, 0x82db, 0x82dc, 0x82de, 0x82df, + 0x82e1, 0x82e1, 0x82e3, 0x82e3, 0x82e5, 0x82e7, 0x82eb, 0x82eb, + 0x82f1, 0x82f1, 0x82f3, 0x82f4, 0x82f9, 0x82fb, 0x82fd, 0x82fe, + 0x8301, 0x8306, 0x8309, 0x8309, 0x830e, 0x830e, 0x8316, 0x8318, + 0x831c, 0x831c, 0x8323, 0x8323, 0x8328, 0x8328, 0x832b, 0x832b, + 0x832f, 0x832f, 0x8331, 0x8332, 0x8334, 0x8336, 0x8338, 0x8339, + 0x8340, 0x8340, 0x8345, 0x8345, 0x8347, 0x8347, 0x8349, 0x834a, + 0x834f, 0x8352, 0x8358, 0x8358, 0x8362, 0x8362, 0x8373, 0x8373, + 0x8375, 0x8375, 0x8377, 0x8377, 0x837b, 0x837c, 0x837f, 0x837f, + 0x8385, 0x8385, 0x8387, 0x8387, 0x8389, 0x838a, 0x838e, 0x838e, + 0x8393, 0x8393, 0x8396, 0x8396, 0x8398, 0x8398, 0x839a, 0x839a, + 0x839e, 0x83a0, 0x83a2, 0x83a2, 0x83a8, 0x83ab, 0x83b1, 0x83b1, + 0x83b5, 0x83b5, 0x83bd, 0x83bd, 0x83c1, 0x83c1, 0x83c5, 0x83c5, + 0x83c7, 0x83c7, 0x83c9, 0x83ca, 0x83cc, 0x83cc, 0x83ce, 0x83ce, + 0x83d3, 0x83d3, 0x83d6, 0x83d6, 0x83d8, 0x83d8, 0x83dc, 0x83dc, + 0x83df, 0x83e0, 0x83e9, 0x83e9, 0x83eb, 0x83eb, 0x83ef, 0x83f2, + 0x83f4, 0x83f4, 0x83f6, 0x83f7, 0x83f9, 0x83f9, 0x83fb, 0x83fb, + 0x83fd, 0x83fd, 0x8403, 0x8404, 0x8407, 0x8407, 0x840a, 0x840e, + 0x8413, 0x8413, 0x8420, 0x8420, 0x8422, 0x8422, 0x8429, 0x842a, + 0x842c, 0x842c, 0x8431, 0x8431, 0x8435, 0x8435, 0x8438, 0x8438, + 0x843c, 0x843d, 0x8446, 0x8446, 0x8448, 0x8449, 0x844e, 0x844e, + 0x8457, 0x8457, 0x845b, 0x845b, 0x8461, 0x8463, 0x8466, 0x8466, + 0x8469, 0x8469, 0x846b, 0x846f, 0x8471, 0x8471, 0x8475, 0x8475, + 0x8477, 0x8477, 0x8479, 0x847a, 0x8482, 0x8482, 0x8484, 0x8484, + 0x848b, 0x848b, 0x8490, 0x8490, 0x8494, 0x8494, 0x8499, 0x8499, + 0x849c, 0x849c, 0x849f, 0x849f, 0x84a1, 0x84a1, 0x84ad, 0x84ad, + 0x84b2, 0x84b2, 0x84b4, 0x84b4, 0x84b8, 0x84b9, 0x84bb, 0x84bc, + 0x84bf, 0x84c2, 0x84c4, 0x84c4, 0x84c6, 0x84c6, 0x84c9, 0x84cb, + 0x84cd, 0x84cd, 0x84d0, 0x84d1, 0x84d6, 0x84d6, 0x84d9, 0x84da, + 0x84dc, 0x84dc, 0x84ec, 0x84ec, 0x84ee, 0x84ee, 0x84f4, 0x84f4, + 0x84fc, 0x84fc, 0x84ff, 0x8500, 0x8506, 0x8506, 0x8511, 0x8511, + 0x8513, 0x8515, 0x8517, 0x8518, 0x851a, 0x851a, 0x851e, 0x851f, + 0x8521, 0x8521, 0x8523, 0x8523, 0x8525, 0x8526, 0x852c, 0x852d, + 0x852f, 0x852f, 0x8535, 0x8535, 0x853d, 0x853d, 0x853f, 0x8541, + 0x8543, 0x8543, 0x8548, 0x854b, 0x854e, 0x854e, 0x8553, 0x8553, + 0x8555, 0x8555, 0x8557, 0x855a, 0x8563, 0x8563, 0x8568, 0x856b, + 0x856d, 0x856d, 0x8577, 0x8577, 0x857e, 0x857e, 0x8580, 0x8580, + 0x8584, 0x8584, 0x8587, 0x8588, 0x858a, 0x858a, 0x858f, 0x8591, + 0x8594, 0x8594, 0x8597, 0x8597, 0x8599, 0x8599, 0x859b, 0x859c, + 0x85a4, 0x85a4, 0x85a6, 0x85a6, 0x85a8, 0x85ac, 0x85ae, 0x85b0, + 0x85b9, 0x85ba, 0x85c1, 0x85c1, 0x85c9, 0x85c9, 0x85cd, 0x85d0, + 0x85d5, 0x85d5, 0x85dc, 0x85dd, 0x85e4, 0x85e5, 0x85e9, 0x85ea, + 0x85f7, 0x85f7, 0x85f9, 0x85fb, 0x85fe, 0x85ff, 0x8602, 0x8602, + 0x8606, 0x8607, 0x860a, 0x860b, 0x8613, 0x8613, 0x8616, 0x8617, + 0x861a, 0x861a, 0x8622, 0x8622, 0x862d, 0x862d, 0x862f, 0x8630, + 0x863f, 0x863f, 0x864d, 0x864e, 0x8650, 0x8650, 0x8654, 0x8655, + 0x865a, 0x865c, 0x865e, 0x865f, 0x8667, 0x8667, 0x866b, 0x866b, + 0x8671, 0x8671, 0x8679, 0x8679, 0x867b, 0x867b, 0x868a, 0x868c, + 0x8693, 0x8693, 0x8695, 0x8695, 0x86a3, 0x86a4, 0x86a9, 0x86ab, + 0x86af, 0x86b0, 0x86b6, 0x86b6, 0x86c4, 0x86c4, 0x86c6, 0x86c7, + 0x86c9, 0x86c9, 0x86cb, 0x86cb, 0x86cd, 0x86ce, 0x86d4, 0x86d4, + 0x86d9, 0x86d9, 0x86db, 0x86db, 0x86de, 0x86df, 0x86e4, 0x86e4, + 0x86e9, 0x86e9, 0x86ec, 0x86ef, 0x86f8, 0x86f9, 0x86fb, 0x86fb, + 0x86fe, 0x86fe, 0x8700, 0x8700, 0x8702, 0x8703, 0x8706, 0x8706, + 0x8708, 0x870a, 0x870d, 0x870d, 0x8711, 0x8712, 0x8718, 0x8718, + 0x871a, 0x871a, 0x871c, 0x871c, 0x8725, 0x8725, 0x8729, 0x8729, + 0x8734, 0x8734, 0x8737, 0x8737, 0x873b, 0x873b, 0x873f, 0x873f, + 0x8749, 0x8749, 0x874b, 0x874c, 0x874e, 0x874e, 0x8753, 0x8753, + 0x8755, 0x8755, 0x8757, 0x8757, 0x8759, 0x8759, 0x875f, 0x8760, + 0x8763, 0x8763, 0x8766, 0x8766, 0x8768, 0x8768, 0x876a, 0x876a, + 0x876e, 0x876e, 0x8774, 0x8774, 0x8776, 0x8776, 0x8778, 0x8778, + 0x877f, 0x877f, 0x8782, 0x8782, 0x878d, 0x878d, 0x879f, 0x879f, + 0x87a2, 0x87a2, 0x87ab, 0x87ab, 0x87af, 0x87af, 0x87b3, 0x87b3, + 0x87ba, 0x87bb, 0x87bd, 0x87bd, 0x87c0, 0x87c0, 0x87c4, 0x87c4, + 0x87c6, 0x87c7, 0x87cb, 0x87cb, 0x87d0, 0x87d0, 0x87d2, 0x87d2, + 0x87e0, 0x87e0, 0x87ec, 0x87ec, 0x87ef, 0x87ef, 0x87f2, 0x87f2, + 0x87f6, 0x87f7, 0x87f9, 0x87f9, 0x87fb, 0x87fb, 0x87fe, 0x87fe, + 0x8805, 0x8805, 0x8807, 0x8807, 0x880d, 0x880f, 0x8811, 0x8811, + 0x8815, 0x8816, 0x881f, 0x881f, 0x8821, 0x8823, 0x8827, 0x8827, + 0x8831, 0x8831, 0x8836, 0x8836, 0x8839, 0x8839, 0x883b, 0x883b, + 0x8840, 0x8840, 0x8842, 0x8842, 0x8844, 0x8844, 0x8846, 0x8846, + 0x884c, 0x884d, 0x8852, 0x8853, 0x8857, 0x8857, 0x8859, 0x8859, + 0x885b, 0x885b, 0x885d, 0x885e, 0x8861, 0x8863, 0x8868, 0x8868, + 0x886b, 0x886b, 0x8870, 0x8870, 0x8872, 0x8872, 0x8875, 0x8875, + 0x8877, 0x8877, 0x887d, 0x887f, 0x8881, 0x8882, 0x8888, 0x8888, + 0x888b, 0x888b, 0x888d, 0x888d, 0x8892, 0x8892, 0x8896, 0x8897, + 0x8899, 0x8899, 0x889e, 0x889e, 0x88a2, 0x88a2, 0x88a4, 0x88a4, + 0x88ab, 0x88ab, 0x88ae, 0x88ae, 0x88b0, 0x88b1, 0x88b4, 0x88b5, + 0x88b7, 0x88b7, 0x88bf, 0x88bf, 0x88c1, 0x88c5, 0x88cf, 0x88cf, + 0x88d4, 0x88d5, 0x88d8, 0x88d9, 0x88dc, 0x88dd, 0x88df, 0x88df, + 0x88e1, 0x88e1, 0x88e8, 0x88e8, 0x88f2, 0x88f5, 0x88f8, 0x88f9, + 0x88fc, 0x88fe, 0x8902, 0x8902, 0x8904, 0x8904, 0x8907, 0x8907, + 0x890a, 0x890a, 0x890c, 0x890c, 0x8910, 0x8910, 0x8912, 0x8913, + 0x8918, 0x8919, 0x891c, 0x891e, 0x8925, 0x8925, 0x892a, 0x892b, + 0x8936, 0x8936, 0x8938, 0x8938, 0x893b, 0x893b, 0x8941, 0x8941, + 0x8943, 0x8944, 0x894c, 0x894d, 0x8956, 0x8956, 0x895e, 0x8960, + 0x8964, 0x8964, 0x8966, 0x8966, 0x896a, 0x896a, 0x896d, 0x896d, + 0x896f, 0x896f, 0x8972, 0x8972, 0x8974, 0x8974, 0x8977, 0x8977, + 0x897e, 0x897f, 0x8981, 0x8981, 0x8983, 0x8983, 0x8986, 0x8988, + 0x898a, 0x898b, 0x898f, 0x898f, 0x8993, 0x8993, 0x8996, 0x8998, + 0x899a, 0x899a, 0x89a1, 0x89a1, 0x89a6, 0x89a7, 0x89a9, 0x89aa, + 0x89ac, 0x89ac, 0x89af, 0x89af, 0x89b2, 0x89b3, 0x89ba, 0x89ba, + 0x89bd, 0x89bd, 0x89bf, 0x89c0, 0x89d2, 0x89d2, 0x89da, 0x89da, + 0x89dc, 0x89dd, 0x89e3, 0x89e3, 0x89e6, 0x89e7, 0x89f4, 0x89f4, + 0x89f8, 0x89f8, 0x8a00, 0x8a00, 0x8a02, 0x8a03, 0x8a08, 0x8a08, + 0x8a0a, 0x8a0a, 0x8a0c, 0x8a0c, 0x8a0e, 0x8a0e, 0x8a10, 0x8a10, + 0x8a12, 0x8a13, 0x8a16, 0x8a18, 0x8a1b, 0x8a1b, 0x8a1d, 0x8a1d, + 0x8a1f, 0x8a1f, 0x8a23, 0x8a23, 0x8a25, 0x8a25, 0x8a2a, 0x8a2a, + 0x8a2d, 0x8a2d, 0x8a31, 0x8a31, 0x8a33, 0x8a34, 0x8a36, 0x8a37, + 0x8a3a, 0x8a3c, 0x8a41, 0x8a41, 0x8a46, 0x8a46, 0x8a48, 0x8a48, + 0x8a50, 0x8a52, 0x8a54, 0x8a55, 0x8a5b, 0x8a5b, 0x8a5e, 0x8a5e, + 0x8a60, 0x8a60, 0x8a62, 0x8a63, 0x8a66, 0x8a66, 0x8a69, 0x8a69, + 0x8a6b, 0x8a6e, 0x8a70, 0x8a73, 0x8a75, 0x8a75, 0x8a79, 0x8a79, + 0x8a7c, 0x8a7c, 0x8a82, 0x8a82, 0x8a84, 0x8a85, 0x8a87, 0x8a87, + 0x8a89, 0x8a89, 0x8a8c, 0x8a8d, 0x8a91, 0x8a91, 0x8a93, 0x8a93, + 0x8a95, 0x8a95, 0x8a98, 0x8a98, 0x8a9a, 0x8a9a, 0x8a9e, 0x8a9e, + 0x8aa0, 0x8aa1, 0x8aa3, 0x8aa8, 0x8aaa, 0x8aaa, 0x8aac, 0x8aad, + 0x8ab0, 0x8ab0, 0x8ab2, 0x8ab2, 0x8ab9, 0x8ab9, 0x8abc, 0x8abc, + 0x8abe, 0x8abf, 0x8ac2, 0x8ac2, 0x8ac4, 0x8ac4, 0x8ac7, 0x8ac7, + 0x8acb, 0x8acd, 0x8acf, 0x8acf, 0x8ad2, 0x8ad2, 0x8ad6, 0x8ad6, + 0x8ada, 0x8adc, 0x8ade, 0x8ae2, 0x8ae4, 0x8ae4, 0x8ae6, 0x8ae7, + 0x8aea, 0x8aeb, 0x8aed, 0x8aee, 0x8af1, 0x8af1, 0x8af3, 0x8af3, + 0x8af6, 0x8af8, 0x8afa, 0x8afa, 0x8afe, 0x8afe, 0x8b00, 0x8b02, + 0x8b04, 0x8b04, 0x8b07, 0x8b07, 0x8b0c, 0x8b0c, 0x8b0e, 0x8b0e, + 0x8b10, 0x8b10, 0x8b14, 0x8b14, 0x8b16, 0x8b17, 0x8b19, 0x8b1b, + 0x8b1d, 0x8b1d, 0x8b20, 0x8b21, 0x8b26, 0x8b26, 0x8b28, 0x8b28, + 0x8b2b, 0x8b2c, 0x8b33, 0x8b33, 0x8b39, 0x8b39, 0x8b3e, 0x8b3e, + 0x8b41, 0x8b41, 0x8b49, 0x8b49, 0x8b4c, 0x8b4c, 0x8b4e, 0x8b4f, + 0x8b53, 0x8b53, 0x8b56, 0x8b56, 0x8b58, 0x8b58, 0x8b5a, 0x8b5c, + 0x8b5f, 0x8b5f, 0x8b66, 0x8b66, 0x8b6b, 0x8b6c, 0x8b6f, 0x8b72, + 0x8b74, 0x8b74, 0x8b77, 0x8b77, 0x8b7d, 0x8b7d, 0x8b7f, 0x8b80, + 0x8b83, 0x8b83, 0x8b8a, 0x8b8a, 0x8b8c, 0x8b8c, 0x8b8e, 0x8b8e, + 0x8b90, 0x8b90, 0x8b92, 0x8b93, 0x8b96, 0x8b96, 0x8b99, 0x8b9a, + 0x8c37, 0x8c37, 0x8c3a, 0x8c3a, 0x8c3f, 0x8c3f, 0x8c41, 0x8c41, + 0x8c46, 0x8c46, 0x8c48, 0x8c48, 0x8c4a, 0x8c4a, 0x8c4c, 0x8c4c, + 0x8c4e, 0x8c4e, 0x8c50, 0x8c50, 0x8c55, 0x8c55, 0x8c5a, 0x8c5a, + 0x8c61, 0x8c62, 0x8c6a, 0x8c6c, 0x8c78, 0x8c7a, 0x8c7c, 0x8c7c, + 0x8c82, 0x8c82, 0x8c85, 0x8c85, 0x8c89, 0x8c8a, 0x8c8c, 0x8c8e, + 0x8c94, 0x8c94, 0x8c98, 0x8c98, 0x8c9d, 0x8c9e, 0x8ca0, 0x8ca2, + 0x8ca7, 0x8cb0, 0x8cb2, 0x8cb4, 0x8cb6, 0x8cb8, 0x8cbb, 0x8cbd, + 0x8cbf, 0x8cc4, 0x8cc7, 0x8cc8, 0x8cca, 0x8cca, 0x8ccd, 0x8cce, + 0x8cd1, 0x8cd1, 0x8cd3, 0x8cd3, 0x8cda, 0x8cdc, 0x8cde, 0x8cde, + 0x8ce0, 0x8ce0, 0x8ce2, 0x8ce4, 0x8ce6, 0x8ce6, 0x8cea, 0x8cea, + 0x8ced, 0x8ced, 0x8cf0, 0x8cf0, 0x8cf4, 0x8cf4, 0x8cfa, 0x8cfd, + 0x8d04, 0x8d05, 0x8d07, 0x8d08, 0x8d0a, 0x8d0b, 0x8d0d, 0x8d0d, + 0x8d0f, 0x8d10, 0x8d12, 0x8d14, 0x8d16, 0x8d16, 0x8d64, 0x8d64, + 0x8d66, 0x8d67, 0x8d6b, 0x8d6b, 0x8d6d, 0x8d6d, 0x8d70, 0x8d71, + 0x8d73, 0x8d74, 0x8d76, 0x8d77, 0x8d81, 0x8d81, 0x8d85, 0x8d85, + 0x8d8a, 0x8d8a, 0x8d99, 0x8d99, 0x8da3, 0x8da3, 0x8da8, 0x8da8, + 0x8db3, 0x8db3, 0x8dba, 0x8dba, 0x8dbe, 0x8dbe, 0x8dc2, 0x8dc2, + 0x8dc6, 0x8dc6, 0x8dcb, 0x8dcc, 0x8dcf, 0x8dcf, 0x8dd6, 0x8dd6, + 0x8dda, 0x8ddb, 0x8ddd, 0x8ddd, 0x8ddf, 0x8ddf, 0x8de1, 0x8de1, + 0x8de3, 0x8de3, 0x8de8, 0x8de8, 0x8dea, 0x8deb, 0x8def, 0x8def, + 0x8df3, 0x8df3, 0x8df5, 0x8df5, 0x8dfc, 0x8dfc, 0x8dff, 0x8dff, + 0x8e08, 0x8e0a, 0x8e0f, 0x8e10, 0x8e1d, 0x8e1f, 0x8e2a, 0x8e2a, + 0x8e30, 0x8e30, 0x8e34, 0x8e35, 0x8e42, 0x8e42, 0x8e44, 0x8e44, + 0x8e47, 0x8e4a, 0x8e4c, 0x8e4c, 0x8e50, 0x8e50, 0x8e55, 0x8e55, + 0x8e59, 0x8e59, 0x8e5f, 0x8e60, 0x8e63, 0x8e64, 0x8e72, 0x8e72, + 0x8e74, 0x8e74, 0x8e76, 0x8e76, 0x8e7c, 0x8e7c, 0x8e81, 0x8e81, + 0x8e84, 0x8e85, 0x8e87, 0x8e87, 0x8e8a, 0x8e8b, 0x8e8d, 0x8e8d, + 0x8e91, 0x8e91, 0x8e93, 0x8e94, 0x8e99, 0x8e99, 0x8ea1, 0x8ea1, + 0x8eaa, 0x8eac, 0x8eaf, 0x8eb1, 0x8ebe, 0x8ebe, 0x8ec0, 0x8ec0, + 0x8ec5, 0x8ec6, 0x8ec8, 0x8ec8, 0x8eca, 0x8ecd, 0x8ecf, 0x8ecf, + 0x8ed2, 0x8ed2, 0x8edb, 0x8edb, 0x8edf, 0x8edf, 0x8ee2, 0x8ee3, + 0x8eeb, 0x8eeb, 0x8ef8, 0x8ef8, 0x8efb, 0x8efe, 0x8f03, 0x8f03, + 0x8f05, 0x8f05, 0x8f09, 0x8f0a, 0x8f0c, 0x8f0c, 0x8f12, 0x8f15, + 0x8f19, 0x8f19, 0x8f1b, 0x8f1f, 0x8f26, 0x8f27, 0x8f29, 0x8f2a, + 0x8f2f, 0x8f2f, 0x8f33, 0x8f33, 0x8f38, 0x8f39, 0x8f3b, 0x8f3b, + 0x8f3e, 0x8f3f, 0x8f42, 0x8f42, 0x8f44, 0x8f46, 0x8f49, 0x8f49, + 0x8f4c, 0x8f4e, 0x8f57, 0x8f57, 0x8f5c, 0x8f5d, 0x8f5f, 0x8f5f, + 0x8f61, 0x8f64, 0x8f9b, 0x8f9c, 0x8f9e, 0x8f9f, 0x8fa3, 0x8fa3, + 0x8fa6, 0x8fa8, 0x8fad, 0x8fb2, 0x8fb7, 0x8fb7, 0x8fba, 0x8fbc, + 0x8fbf, 0x8fbf, 0x8fc2, 0x8fc2, 0x8fc4, 0x8fc5, 0x8fce, 0x8fce, + 0x8fd1, 0x8fd1, 0x8fd4, 0x8fd4, 0x8fda, 0x8fda, 0x8fe2, 0x8fe2, + 0x8fe5, 0x8fe6, 0x8fe9, 0x8feb, 0x8fed, 0x8fed, 0x8fef, 0x8ff0, + 0x8ff2, 0x8ff2, 0x8ff4, 0x8ff4, 0x8ff7, 0x8ffa, 0x8ffd, 0x8ffd, + 0x9000, 0x9003, 0x9005, 0x9006, 0x9008, 0x9008, 0x900b, 0x900b, + 0x900d, 0x9011, 0x9013, 0x9017, 0x9019, 0x901a, 0x901d, 0x9023, + 0x9027, 0x9027, 0x902e, 0x902e, 0x9031, 0x9032, 0x9035, 0x9036, + 0x9038, 0x9039, 0x903c, 0x903c, 0x903e, 0x903e, 0x9041, 0x9042, + 0x9045, 0x9045, 0x9047, 0x9047, 0x9049, 0x904b, 0x904d, 0x9056, + 0x9058, 0x9059, 0x905c, 0x905e, 0x9060, 0x9061, 0x9063, 0x9063, + 0x9065, 0x9065, 0x9067, 0x9069, 0x906d, 0x906f, 0x9072, 0x9072, + 0x9075, 0x9078, 0x907a, 0x907a, 0x907c, 0x907d, 0x907f, 0x9084, + 0x9087, 0x908a, 0x908f, 0x908f, 0x9091, 0x9091, 0x9095, 0x9095, + 0x9099, 0x9099, 0x90a2, 0x90a3, 0x90a6, 0x90a6, 0x90a8, 0x90a8, + 0x90aa, 0x90aa, 0x90af, 0x90b1, 0x90b5, 0x90b5, 0x90b8, 0x90b8, + 0x90c1, 0x90c1, 0x90ca, 0x90ca, 0x90ce, 0x90ce, 0x90db, 0x90db, + 0x90de, 0x90de, 0x90e1, 0x90e2, 0x90e4, 0x90e4, 0x90e8, 0x90e8, + 0x90ed, 0x90ed, 0x90f5, 0x90f5, 0x90f7, 0x90f7, 0x90fd, 0x90fd, + 0x9102, 0x9102, 0x9112, 0x9112, 0x9115, 0x9115, 0x9119, 0x9119, + 0x9127, 0x9127, 0x912d, 0x912d, 0x9130, 0x9130, 0x9132, 0x9132, + 0x9149, 0x914e, 0x9152, 0x9152, 0x9154, 0x9154, 0x9156, 0x9156, + 0x9158, 0x9158, 0x9162, 0x9163, 0x9165, 0x9165, 0x9169, 0x916a, + 0x916c, 0x916c, 0x9172, 0x9173, 0x9175, 0x9175, 0x9177, 0x9178, + 0x9182, 0x9182, 0x9187, 0x9187, 0x9189, 0x9189, 0x918b, 0x918b, + 0x918d, 0x918d, 0x9190, 0x9190, 0x9192, 0x9192, 0x9197, 0x9197, + 0x919c, 0x919c, 0x91a2, 0x91a2, 0x91a4, 0x91a4, 0x91aa, 0x91ac, + 0x91ae, 0x91af, 0x91b1, 0x91b1, 0x91b4, 0x91b5, 0x91b8, 0x91b8, + 0x91ba, 0x91ba, 0x91c0, 0x91c1, 0x91c6, 0x91c9, 0x91cb, 0x91d1, + 0x91d6, 0x91d8, 0x91da, 0x91df, 0x91e1, 0x91e1, 0x91e3, 0x91e7, + 0x91ea, 0x91ea, 0x91ed, 0x91ee, 0x91f5, 0x91f6, 0x91fc, 0x91fc, + 0x91ff, 0x91ff, 0x9206, 0x9206, 0x920a, 0x920a, 0x920d, 0x920e, + 0x9210, 0x9212, 0x9214, 0x9215, 0x9217, 0x9217, 0x921e, 0x921e, + 0x9229, 0x9229, 0x922c, 0x922c, 0x9234, 0x9234, 0x9237, 0x9237, + 0x9239, 0x923a, 0x923c, 0x923c, 0x923f, 0x9240, 0x9244, 0x9245, + 0x9248, 0x9249, 0x924b, 0x924b, 0x924e, 0x924e, 0x9250, 0x9251, + 0x9257, 0x9257, 0x9259, 0x925b, 0x925e, 0x925e, 0x9262, 0x9262, + 0x9264, 0x9267, 0x9271, 0x9271, 0x9277, 0x9278, 0x927e, 0x927e, + 0x9280, 0x9280, 0x9283, 0x9283, 0x9285, 0x9285, 0x9288, 0x9288, + 0x9291, 0x9291, 0x9293, 0x9293, 0x9295, 0x9296, 0x9298, 0x9298, + 0x929a, 0x929c, 0x92a7, 0x92a7, 0x92ad, 0x92ad, 0x92b3, 0x92b3, + 0x92b6, 0x92b7, 0x92b9, 0x92b9, 0x92cc, 0x92cc, 0x92cf, 0x92d0, + 0x92d2, 0x92d3, 0x92d5, 0x92d5, 0x92d7, 0x92d7, 0x92d9, 0x92d9, + 0x92e0, 0x92e0, 0x92e4, 0x92e4, 0x92e7, 0x92e7, 0x92e9, 0x92ea, + 0x92ed, 0x92ed, 0x92f2, 0x92f3, 0x92f8, 0x92fc, 0x92ff, 0x92ff, + 0x9302, 0x9302, 0x9304, 0x9304, 0x9306, 0x9306, 0x930f, 0x9310, + 0x9318, 0x931a, 0x931d, 0x9326, 0x9328, 0x9328, 0x932b, 0x932c, + 0x932e, 0x932f, 0x9332, 0x9332, 0x9335, 0x9335, 0x933a, 0x933b, + 0x9344, 0x9344, 0x9348, 0x9348, 0x934a, 0x934b, 0x934d, 0x934d, + 0x9354, 0x9354, 0x9356, 0x9357, 0x935b, 0x935c, 0x9360, 0x9360, + 0x936c, 0x936c, 0x936e, 0x936e, 0x9370, 0x9370, 0x9375, 0x9375, + 0x937c, 0x937c, 0x937e, 0x937e, 0x938c, 0x938c, 0x9394, 0x9394, + 0x9396, 0x9397, 0x939a, 0x939a, 0x93a3, 0x93a4, 0x93a7, 0x93a7, + 0x93ac, 0x93ae, 0x93b0, 0x93b0, 0x93b9, 0x93b9, 0x93c3, 0x93c3, + 0x93c6, 0x93c6, 0x93c8, 0x93c8, 0x93d0, 0x93d1, 0x93d6, 0x93d8, + 0x93dd, 0x93de, 0x93e1, 0x93e1, 0x93e4, 0x93e5, 0x93e8, 0x93e8, + 0x93f6, 0x93f6, 0x93f8, 0x93f8, 0x9403, 0x9404, 0x9407, 0x9407, + 0x9410, 0x9410, 0x9413, 0x9414, 0x9418, 0x941a, 0x9421, 0x9421, + 0x9425, 0x9425, 0x942b, 0x942b, 0x9431, 0x9431, 0x9435, 0x9436, + 0x9438, 0x9438, 0x943a, 0x943a, 0x9441, 0x9441, 0x9444, 0x9445, + 0x9448, 0x9448, 0x9451, 0x9453, 0x945a, 0x945b, 0x945e, 0x945e, + 0x9460, 0x9460, 0x9462, 0x9462, 0x946a, 0x946a, 0x9470, 0x9470, + 0x9475, 0x9475, 0x9477, 0x9477, 0x947c, 0x947f, 0x9481, 0x9481, + 0x9577, 0x9577, 0x9580, 0x9580, 0x9582, 0x9583, 0x9587, 0x9587, + 0x9589, 0x958b, 0x958f, 0x958f, 0x9591, 0x9594, 0x9596, 0x9596, + 0x9598, 0x9599, 0x95a0, 0x95a0, 0x95a2, 0x95a5, 0x95a7, 0x95a8, + 0x95ad, 0x95ad, 0x95b1, 0x95b2, 0x95b9, 0x95b9, 0x95bb, 0x95bc, + 0x95be, 0x95be, 0x95c3, 0x95c3, 0x95c7, 0x95c7, 0x95ca, 0x95ca, + 0x95cc, 0x95cd, 0x95d4, 0x95d6, 0x95d8, 0x95d8, 0x95dc, 0x95dc, + 0x95e1, 0x95e2, 0x95e5, 0x95e5, 0x961c, 0x961c, 0x9621, 0x9621, + 0x9628, 0x9628, 0x962a, 0x962a, 0x962e, 0x962f, 0x9632, 0x9632, + 0x963b, 0x963b, 0x963f, 0x9640, 0x9642, 0x9642, 0x9644, 0x9644, + 0x964b, 0x964d, 0x964f, 0x9650, 0x965b, 0x965f, 0x9662, 0x9666, + 0x966a, 0x966a, 0x966c, 0x966c, 0x9670, 0x9670, 0x9672, 0x9673, + 0x9675, 0x9678, 0x967a, 0x967a, 0x967d, 0x967d, 0x9685, 0x9686, + 0x9688, 0x9688, 0x968a, 0x968b, 0x968d, 0x968f, 0x9694, 0x9695, + 0x9697, 0x9699, 0x969b, 0x969d, 0x96a0, 0x96a0, 0x96a3, 0x96a3, + 0x96a7, 0x96a8, 0x96aa, 0x96aa, 0x96af, 0x96b2, 0x96b4, 0x96b4, + 0x96b6, 0x96b9, 0x96bb, 0x96bc, 0x96c0, 0x96c1, 0x96c4, 0x96c7, + 0x96c9, 0x96c9, 0x96cb, 0x96ce, 0x96d1, 0x96d1, 0x96d5, 0x96d6, + 0x96d9, 0x96d9, 0x96db, 0x96dc, 0x96e2, 0x96e3, 0x96e8, 0x96eb, + 0x96ef, 0x96f0, 0x96f2, 0x96f2, 0x96f6, 0x96f7, 0x96f9, 0x96f9, + 0x96fb, 0x96fb, 0x9700, 0x9700, 0x9704, 0x9704, 0x9706, 0x9708, + 0x970a, 0x970a, 0x970d, 0x970f, 0x9711, 0x9711, 0x9713, 0x9713, + 0x9716, 0x9716, 0x9719, 0x9719, 0x971c, 0x971c, 0x971e, 0x971e, + 0x9724, 0x9724, 0x9727, 0x9727, 0x972a, 0x972a, 0x9730, 0x9730, + 0x9732, 0x9733, 0x9738, 0x9739, 0x973b, 0x973b, 0x973d, 0x973e, + 0x9742, 0x9744, 0x9746, 0x9746, 0x9748, 0x9749, 0x974d, 0x974d, + 0x974f, 0x974f, 0x9751, 0x9752, 0x9755, 0x9756, 0x9759, 0x9759, + 0x975c, 0x975c, 0x975e, 0x975e, 0x9760, 0x9762, 0x9764, 0x9764, + 0x9766, 0x9766, 0x9768, 0x9769, 0x976b, 0x976b, 0x976d, 0x976d, + 0x9771, 0x9771, 0x9774, 0x9774, 0x9777, 0x9777, 0x9779, 0x977a, + 0x977c, 0x977c, 0x9781, 0x9781, 0x9784, 0x9786, 0x978b, 0x978b, + 0x978d, 0x978d, 0x978f, 0x9790, 0x9798, 0x9798, 0x979c, 0x979c, + 0x97a0, 0x97a0, 0x97a3, 0x97a3, 0x97a6, 0x97a6, 0x97a8, 0x97a8, + 0x97ab, 0x97ab, 0x97ad, 0x97ad, 0x97b3, 0x97b4, 0x97c3, 0x97c3, + 0x97c6, 0x97c6, 0x97c8, 0x97c8, 0x97cb, 0x97cb, 0x97d3, 0x97d3, + 0x97dc, 0x97dc, 0x97ed, 0x97ee, 0x97f2, 0x97f3, 0x97f5, 0x97f6, + 0x97fb, 0x97fb, 0x97ff, 0x9803, 0x9805, 0x9806, 0x9808, 0x9808, + 0x980a, 0x980a, 0x980c, 0x980c, 0x980f, 0x9813, 0x9817, 0x9818, + 0x981a, 0x981a, 0x9821, 0x9821, 0x9824, 0x9824, 0x982c, 0x982d, + 0x9830, 0x9830, 0x9834, 0x9834, 0x9837, 0x9839, 0x983b, 0x983d, + 0x9846, 0x9846, 0x984b, 0x984f, 0x9854, 0x9855, 0x9857, 0x9858, + 0x985a, 0x985b, 0x985e, 0x985e, 0x9865, 0x9865, 0x9867, 0x9867, + 0x986b, 0x986b, 0x986f, 0x9871, 0x9873, 0x9874, 0x98a8, 0x98a8, + 0x98aa, 0x98aa, 0x98af, 0x98af, 0x98b1, 0x98b1, 0x98b6, 0x98b6, + 0x98c3, 0x98c4, 0x98c6, 0x98c7, 0x98db, 0x98dc, 0x98df, 0x98df, + 0x98e1, 0x98e2, 0x98e9, 0x98e9, 0x98eb, 0x98eb, 0x98ed, 0x98ef, + 0x98f2, 0x98f2, 0x98f4, 0x98f4, 0x98fc, 0x98fe, 0x9903, 0x9903, + 0x9905, 0x9905, 0x9909, 0x990a, 0x990c, 0x990c, 0x9910, 0x9910, + 0x9912, 0x9914, 0x9918, 0x9918, 0x991d, 0x991e, 0x9920, 0x9921, + 0x9924, 0x9924, 0x9927, 0x9928, 0x992c, 0x992c, 0x992e, 0x992e, + 0x993d, 0x993e, 0x9942, 0x9942, 0x9945, 0x9945, 0x9949, 0x9949, + 0x994b, 0x994d, 0x9950, 0x9952, 0x9954, 0x9955, 0x9957, 0x9957, + 0x9996, 0x9999, 0x999d, 0x999e, 0x99a5, 0x99a5, 0x99a8, 0x99a8, + 0x99ac, 0x99ae, 0x99b1, 0x99b1, 0x99b3, 0x99b4, 0x99b9, 0x99b9, + 0x99bc, 0x99bc, 0x99c1, 0x99c1, 0x99c4, 0x99c6, 0x99c8, 0x99c8, + 0x99d0, 0x99d2, 0x99d5, 0x99d5, 0x99d8, 0x99d9, 0x99db, 0x99db, + 0x99dd, 0x99dd, 0x99df, 0x99df, 0x99e2, 0x99e2, 0x99ed, 0x99ee, + 0x99f1, 0x99f2, 0x99f8, 0x99f8, 0x99fb, 0x99fb, 0x99ff, 0x99ff, + 0x9a01, 0x9a01, 0x9a05, 0x9a05, 0x9a08, 0x9a08, 0x9a0e, 0x9a0f, + 0x9a12, 0x9a13, 0x9a19, 0x9a19, 0x9a28, 0x9a28, 0x9a2b, 0x9a2b, + 0x9a30, 0x9a30, 0x9a36, 0x9a37, 0x9a3e, 0x9a3e, 0x9a40, 0x9a40, + 0x9a42, 0x9a43, 0x9a45, 0x9a45, 0x9a4d, 0x9a4e, 0x9a55, 0x9a55, + 0x9a57, 0x9a57, 0x9a5a, 0x9a5b, 0x9a5f, 0x9a5f, 0x9a62, 0x9a62, + 0x9a64, 0x9a65, 0x9a69, 0x9a6b, 0x9aa8, 0x9aa8, 0x9aad, 0x9aad, + 0x9ab0, 0x9ab0, 0x9ab8, 0x9ab8, 0x9abc, 0x9abc, 0x9ac0, 0x9ac0, + 0x9ac4, 0x9ac4, 0x9acf, 0x9acf, 0x9ad1, 0x9ad1, 0x9ad3, 0x9ad4, + 0x9ad8, 0x9ad9, 0x9adc, 0x9adc, 0x9ade, 0x9adf, 0x9ae2, 0x9ae3, + 0x9ae5, 0x9ae6, 0x9aea, 0x9aeb, 0x9aed, 0x9aef, 0x9af1, 0x9af1, + 0x9af4, 0x9af4, 0x9af7, 0x9af7, 0x9afb, 0x9afb, 0x9b06, 0x9b06, + 0x9b18, 0x9b18, 0x9b1a, 0x9b1a, 0x9b1f, 0x9b1f, 0x9b22, 0x9b23, + 0x9b25, 0x9b25, 0x9b27, 0x9b2a, 0x9b2e, 0x9b2f, 0x9b31, 0x9b32, + 0x9b3b, 0x9b3c, 0x9b41, 0x9b45, 0x9b4d, 0x9b4f, 0x9b51, 0x9b51, + 0x9b54, 0x9b54, 0x9b58, 0x9b58, 0x9b5a, 0x9b5a, 0x9b6f, 0x9b6f, + 0x9b72, 0x9b72, 0x9b74, 0x9b75, 0x9b83, 0x9b83, 0x9b8e, 0x9b8f, + 0x9b91, 0x9b93, 0x9b96, 0x9b97, 0x9b9f, 0x9ba0, 0x9ba8, 0x9ba8, + 0x9baa, 0x9bab, 0x9bad, 0x9bae, 0x9bb1, 0x9bb1, 0x9bb4, 0x9bb4, + 0x9bb9, 0x9bb9, 0x9bbb, 0x9bbb, 0x9bc0, 0x9bc0, 0x9bc6, 0x9bc6, + 0x9bc9, 0x9bca, 0x9bcf, 0x9bcf, 0x9bd1, 0x9bd2, 0x9bd4, 0x9bd4, + 0x9bd6, 0x9bd6, 0x9bdb, 0x9bdb, 0x9be1, 0x9be4, 0x9be8, 0x9be8, + 0x9bf0, 0x9bf2, 0x9bf5, 0x9bf5, 0x9c00, 0x9c00, 0x9c04, 0x9c04, + 0x9c06, 0x9c06, 0x9c08, 0x9c0a, 0x9c0c, 0x9c0d, 0x9c10, 0x9c10, + 0x9c12, 0x9c15, 0x9c1b, 0x9c1b, 0x9c21, 0x9c21, 0x9c24, 0x9c25, + 0x9c2d, 0x9c30, 0x9c32, 0x9c32, 0x9c39, 0x9c3b, 0x9c3e, 0x9c3e, + 0x9c46, 0x9c49, 0x9c52, 0x9c52, 0x9c57, 0x9c57, 0x9c5a, 0x9c5a, + 0x9c60, 0x9c60, 0x9c67, 0x9c67, 0x9c76, 0x9c76, 0x9c78, 0x9c78, + 0x9ce5, 0x9ce5, 0x9ce7, 0x9ce7, 0x9ce9, 0x9ce9, 0x9ceb, 0x9cec, + 0x9cf0, 0x9cf0, 0x9cf3, 0x9cf4, 0x9cf6, 0x9cf6, 0x9d03, 0x9d03, + 0x9d06, 0x9d09, 0x9d0e, 0x9d0e, 0x9d12, 0x9d12, 0x9d15, 0x9d15, + 0x9d1b, 0x9d1b, 0x9d1f, 0x9d1f, 0x9d23, 0x9d23, 0x9d26, 0x9d26, + 0x9d28, 0x9d28, 0x9d2a, 0x9d2c, 0x9d3b, 0x9d3b, 0x9d3e, 0x9d3f, + 0x9d41, 0x9d41, 0x9d44, 0x9d44, 0x9d46, 0x9d46, 0x9d48, 0x9d48, + 0x9d50, 0x9d51, 0x9d59, 0x9d59, 0x9d5c, 0x9d5e, 0x9d60, 0x9d61, + 0x9d64, 0x9d64, 0x9d6b, 0x9d6c, 0x9d6f, 0x9d70, 0x9d72, 0x9d72, + 0x9d7a, 0x9d7a, 0x9d87, 0x9d87, 0x9d89, 0x9d89, 0x9d8f, 0x9d8f, + 0x9d9a, 0x9d9a, 0x9da4, 0x9da4, 0x9da9, 0x9da9, 0x9dab, 0x9dab, + 0x9daf, 0x9daf, 0x9db2, 0x9db2, 0x9db4, 0x9db4, 0x9db8, 0x9db8, + 0x9dba, 0x9dbb, 0x9dc1, 0x9dc2, 0x9dc4, 0x9dc4, 0x9dc6, 0x9dc6, + 0x9dcf, 0x9dcf, 0x9dd3, 0x9dd3, 0x9dd7, 0x9dd7, 0x9dd9, 0x9dd9, + 0x9de6, 0x9de6, 0x9ded, 0x9ded, 0x9def, 0x9def, 0x9df2, 0x9df2, + 0x9df8, 0x9dfa, 0x9dfd, 0x9dfd, 0x9e19, 0x9e1b, 0x9e1e, 0x9e1e, + 0x9e75, 0x9e75, 0x9e78, 0x9e79, 0x9e7d, 0x9e7d, 0x9e7f, 0x9e7f, + 0x9e81, 0x9e81, 0x9e88, 0x9e88, 0x9e8b, 0x9e8c, 0x9e91, 0x9e93, + 0x9e95, 0x9e95, 0x9e97, 0x9e97, 0x9e9d, 0x9e9d, 0x9e9f, 0x9e9f, + 0x9ea5, 0x9ea6, 0x9ea9, 0x9eaa, 0x9ead, 0x9ead, 0x9eb4, 0x9eb5, + 0x9eb8, 0x9ebc, 0x9ebe, 0x9ebf, 0x9ec3, 0x9ec4, 0x9ecc, 0x9ed2, + 0x9ed4, 0x9ed4, 0x9ed8, 0x9ed9, 0x9edb, 0x9ede, 0x9ee0, 0x9ee0, + 0x9ee5, 0x9ee5, 0x9ee8, 0x9ee8, 0x9eef, 0x9eef, 0x9ef4, 0x9ef4, + 0x9ef6, 0x9ef7, 0x9ef9, 0x9ef9, 0x9efb, 0x9efd, 0x9f07, 0x9f08, + 0x9f0e, 0x9f0e, 0x9f13, 0x9f13, 0x9f15, 0x9f15, 0x9f20, 0x9f21, + 0x9f2c, 0x9f2c, 0x9f3b, 0x9f3b, 0x9f3e, 0x9f3e, 0x9f4a, 0x9f4b, + 0x9f4e, 0x9f4f, 0x9f52, 0x9f52, 0x9f54, 0x9f54, 0x9f5f, 0x9f63, + 0x9f66, 0x9f67, 0x9f6a, 0x9f6a, 0x9f6c, 0x9f6c, 0x9f72, 0x9f72, + 0x9f76, 0x9f77, 0x9f8d, 0x9f8d, 0x9f90, 0x9f90, 0x9f95, 0x9f95, + 0x9f9c, 0x9f9d, 0x9fa0, 0x9fa0, 0xac00, 0xd7a3, 0xe000, 0xe06b, + 0xe070, 0xe07e, 0xe080, 0xe099, 0xe0a0, 0xe0ba, 0xe0c0, 0xe0d6, + 0xe0e0, 0xe0f5, 0xe100, 0xe105, 0xe110, 0xe116, 0xe121, 0xe12c, + 0xe130, 0xe13c, 0xe140, 0xe14d, 0xe150, 0xe153, 0xf900, 0xfa0b, + 0xfa0e, 0xfa2d, 0xfb01, 0xfb02, 0xfe30, 0xfe33, 0xfe35, 0xfe44, + 0xff01, 0xff5e, 0xff61, 0xff9f, 0xffe0, 0xffe6, + 0 + // clang-format on +}; + +void handleAppletHook (AppletHookType const hook_, void *const param_) +{ + (void)param_; + switch (hook_) + { + case AppletHookType_OnFocusState: + s_focused = (appletGetFocusState () == AppletFocusState_Focused); + break; + + case AppletHookType_OnOperationMode: + switch (appletGetOperationMode ()) + { + default: + case AppletOperationMode_Handheld: + s_width = 1280.0f; + s_height = 720.0f; + break; + + case AppletOperationMode_Docked: +#if 0 + s_width = 1920.0f; + s_height = 1080.0f; +#else + s_width = 1280.0f; + s_height = 720.0f; +#endif + break; + } + break; + + default: + break; + } +} + +char const *getClipboardText (void *const userData_) +{ + (void)userData_; + return s_clipboard.c_str (); +} + +void setClipboardText (void *const userData_, char const *const text_) +{ + (void)userData_; + s_clipboard = text_; +} + +void moveMouse (ImGuiIO &io_, ImVec2 const &pos_, bool const force_ = false) +{ + auto const now = std::chrono::high_resolution_clock::now (); + + if (!force_ && pos_.x == s_mousePos.x && pos_.y == s_mousePos.y) + { + if (now - s_lastMouseUpdate > 1s) + s_showMouse = false; + + return; + } + + s_showMouse = true; + s_lastMouseUpdate = now; + s_mousePos = pos_; + + io_.MousePos = s_mousePos; +} + +void updateMouseButtons (ImGuiIO &io_) +{ + auto const buttons = hidMouseButtonsHeld (); + + for (std::size_t i = 0; i < IM_ARRAYSIZE (io_.MouseDown); ++i) + { + // If a mouse press event came, always pass it as "mouse held this frame", so we don't miss + // click-release events that are shorter than 1 frame. + io_.MouseDown[i] = s_mouseJustPressed[i] || (buttons & BIT (i)); + s_mouseJustPressed[i] = false; + + if (io_.MouseDown[i]) + moveMouse (io_, s_mousePos, true); + } +} + +void updateMousePos (ImGuiIO &io_) +{ + if (!s_focused) + return; + + MousePosition pos; + hidMouseRead (&pos); + + io_.MouseWheelH += pos.scrollVelocityX; + io_.MouseWheel += pos.scrollVelocityY; + + moveMouse ( + io_, ImVec2 (s_mousePos.x + 2.0f * pos.velocityX, s_mousePos.y + 2.0f * pos.velocityY)); +} + +void updateTouch (ImGuiIO &io_) +{ + if (!s_focused) + return; + + auto const touchCount = hidTouchCount (); + if (touchCount < 1) + return; + + touchPosition pos; + hidTouchRead (&pos, 0); + + moveMouse (io_, ImVec2 (pos.px, pos.py)); + io_.MouseDown[0] = true; + s_showMouse = false; +} + +void updateGamepads (ImGuiIO &io_) +{ + std::memset (io_.NavInputs, 0, sizeof (io_.NavInputs)); + + auto const buttonMapping = { + std::make_pair (KEY_A, ImGuiNavInput_Activate), + std::make_pair (KEY_B, ImGuiNavInput_Cancel), + std::make_pair (KEY_X, ImGuiNavInput_Input), + std::make_pair (KEY_Y, ImGuiNavInput_Menu), + std::make_pair (KEY_L, ImGuiNavInput_FocusPrev), + std::make_pair (KEY_L, ImGuiNavInput_TweakSlow), + std::make_pair (KEY_R, ImGuiNavInput_FocusNext), + std::make_pair (KEY_R, ImGuiNavInput_TweakFast), + std::make_pair (KEY_DUP, ImGuiNavInput_DpadUp), + std::make_pair (KEY_DRIGHT, ImGuiNavInput_DpadRight), + std::make_pair (KEY_DDOWN, ImGuiNavInput_DpadDown), + std::make_pair (KEY_DLEFT, ImGuiNavInput_DpadLeft), + }; + + auto const keys = hidKeysHeld (CONTROLLER_P1_AUTO); + for (auto const &[in, out] : buttonMapping) + { + if (keys & in) + io_.NavInputs[out] = 1.0f; + } + + // Use ZR/ZL as Mouse0/Mouse1, respectively + if (keys & KEY_ZR) + { + io_.MouseDown[0] = true; + moveMouse (io_, s_mousePos, true); + } + if (keys & KEY_ZL) + { + io_.MouseDown[1] = true; + moveMouse (io_, s_mousePos, true); + } + + JoystickPosition js; + auto const analogMapping = { + std::make_tuple (std::ref (js.dx), ImGuiNavInput_LStickLeft, -0.3f, -0.9f), + std::make_tuple (std::ref (js.dx), ImGuiNavInput_LStickRight, +0.3f, +0.9f), + std::make_tuple (std::ref (js.dy), ImGuiNavInput_LStickUp, +0.3f, +0.9f), + std::make_tuple (std::ref (js.dy), ImGuiNavInput_LStickDown, -0.3f, -0.9f), + }; + + hidJoystickRead (&js, CONTROLLER_P1_AUTO, JOYSTICK_LEFT); + for (auto const &[in, out, min, max] : analogMapping) + { + auto const value = in / static_cast (JOYSTICK_MAX); + auto const v = std::min (1.0f, (value - min) / (max - min)); + io_.NavInputs[out] = std::max (io_.NavInputs[out], v); + } + + // Use right stick as mouse + auto scale = 5.0f; + if (keys & KEY_L) + scale = 1.0f; + if (keys & KEY_R) + scale = 20.0f; + hidJoystickRead (&js, CONTROLLER_P1_AUTO, JOYSTICK_RIGHT); + + moveMouse (io_, + ImVec2 (s_mousePos.x + js.dx / static_cast (JOYSTICK_MAX) * scale, + s_mousePos.y - js.dy / static_cast (JOYSTICK_MAX) * scale)); +} + +void updateKeyboard (ImGuiIO &io_) +{ + io_.KeyCtrl = + hidKeyboardModifierHeld (static_cast (KBD_MOD_LCTRL | KBD_MOD_RCTRL)); + io_.KeyShift = hidKeyboardModifierHeld ( + static_cast (KBD_MOD_LSHIFT | KBD_MOD_RSHIFT)); + io_.KeyAlt = + hidKeyboardModifierHeld (static_cast (KBD_MOD_LALT | KBD_MOD_RALT)); + io_.KeySuper = + hidKeyboardModifierHeld (static_cast (KBD_MOD_LMETA | KBD_MOD_RMETA)); + + for (int i = 0; i < 256; ++i) + io_.KeysDown[i] = hidKeyboardHeld (static_cast (i)); + + if (!io_.WantTextInput) + return; + + // io_.AddInputCharacter (c); +} + +bool loadFontAtlas () +{ + fs::File fp; + if (!fp.open (FONT_ATLAS_BIN)) + return false; + + auto const atlas = ImGui::GetIO ().Fonts; + atlas->Clear (); + atlas->TexWidth = fp.read (); + atlas->TexHeight = fp.read (); + atlas->TexUvScale = ImVec2 (1.0f / atlas->TexWidth, 1.0f / atlas->TexHeight); + atlas->TexUvWhitePixel = ImVec2 (0.5f / atlas->TexWidth, 0.5f / atlas->TexHeight); + atlas->TexPixelsAlpha8 = + reinterpret_cast (IM_ALLOC (atlas->TexWidth * atlas->TexHeight)); + + if (!fp.readAll (atlas->TexPixelsAlpha8, atlas->TexWidth * atlas->TexHeight)) + return false; + + ImFontConfig config; + config.FontData = nullptr; + config.FontDataSize = 0; + config.FontDataOwnedByAtlas = true; + config.FontNo = 0; + config.SizePixels = 14.0f; + config.OversampleH = 3; + config.OversampleV = 1; + config.PixelSnapH = false; + config.GlyphExtraSpacing = ImVec2 (0.0f, 0.0f); + config.GlyphOffset = ImVec2 (0.0f, 0.0f); + config.GlyphRanges = nxFontRanges; + config.GlyphMinAdvanceX = 0.0f; + config.GlyphMaxAdvanceX = std::numeric_limits::max (); + config.MergeMode = false; + config.RasterizerFlags = 0; + config.RasterizerMultiply = 1.0f; + config.EllipsisChar = 0x2026; + std::memset (config.Name, 0, sizeof (config.Name)); + + auto const font = IM_NEW (ImFont); + config.DstFont = font; + + atlas->ConfigData.push_back (config); + atlas->Fonts.push_back (font); + atlas->CustomRectIds[0] = atlas->AddCustomRectRegular (0x80000000, 108 * 2 + 1, 27); + atlas->CustomRects[0].X = 0; + atlas->CustomRects[0].Y = 0; + + font->FallbackAdvanceX = fp.read (); + font->FontSize = fp.read (); + + auto const glyphCount = fp.read (); + for (unsigned i = 0; i < glyphCount; ++i) + { + ImFontGlyph glyph; + + glyph.Codepoint = fp.read (); + glyph.AdvanceX = fp.read (); + glyph.X0 = fp.read (); + glyph.Y0 = fp.read (); + glyph.X1 = fp.read (); + glyph.Y1 = fp.read (); + glyph.U0 = fp.read (); + glyph.V0 = fp.read (); + glyph.U1 = fp.read (); + glyph.V1 = fp.read (); + + font->Glyphs.push_back (glyph); + font->MetricsTotalSurface += + static_cast ((glyph.U1 - glyph.U0) * atlas->TexWidth + 1.99f) * + static_cast ((glyph.V1 - glyph.V0) * atlas->TexHeight + 1.99f); + } + + font->BuildLookupTable (); + + font->DisplayOffset.x = fp.read (); + font->DisplayOffset.y = fp.read (); + + font->ContainerAtlas = atlas; + font->ConfigData = &atlas->ConfigData[0]; + font->ConfigDataCount = 1; + font->FallbackChar = '?'; + font->EllipsisChar = config.EllipsisChar; + font->Scale = 1.0f; + font->Ascent = fp.read (); + font->Descent = fp.read (); + + return true; +} + +bool saveFontAtlas () +{ + auto const atlas = ImGui::GetIO ().Fonts; + + unsigned char *pixels; + int width; + int height; + atlas->GetTexDataAsAlpha8 (&pixels, &width, &height); + + fs::File fp; + if (!fp.open (FONT_ATLAS_BIN, "wb")) + return false; + + fp.write (width); + fp.write (height); + + if (!fp.writeAll (pixels, width * height)) + return false; + + auto const font = atlas->ConfigData[0].DstFont; + + fp.write (font->FallbackAdvanceX); + fp.write (font->FontSize); + + fp.write (font->Glyphs.size ()); + for (auto const &glyph : font->Glyphs) + { + fp.write (glyph.Codepoint); + fp.write (glyph.AdvanceX); + fp.write (glyph.X0); + fp.write (glyph.Y0); + fp.write (glyph.X1); + fp.write (glyph.Y1); + fp.write (glyph.U0); + fp.write (glyph.V0); + fp.write (glyph.U1); + fp.write (glyph.V1); + } + + fp.write (font->DisplayOffset.x); + fp.write (font->DisplayOffset.y); + fp.write (font->Ascent); + fp.write (font->Descent); + + return true; +} +} + +bool imgui::nx::init () +{ + u64 languageCode; + auto rc = setInitialize (); + if (R_FAILED (rc)) + return false; + + rc = setGetSystemLanguage (&languageCode); + if (R_FAILED (rc)) + { + setExit (); + return false; + } + setExit (); + + std::vector fonts (PlSharedFontType_Total); + s32 numFonts = 0; + rc = plGetSharedFont (languageCode, fonts.data (), fonts.size (), &numFonts); + if (R_FAILED (rc)) + return false; + fonts.resize (numFonts); + + appletSetFocusHandlingMode (AppletFocusHandlingMode_NoSuspend); + appletHook (&s_appletHookCookie, handleAppletHook, nullptr); + handleAppletHook (AppletHookType_OnFocusState, nullptr); + handleAppletHook (AppletHookType_OnOperationMode, nullptr); + + ImGuiIO &io = ImGui::GetIO (); + + io.IniFilename = nullptr; + + io.ConfigFlags |= ImGuiConfigFlags_IsTouchScreen; + io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad; + io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard; + + io.BackendFlags |= ImGuiBackendFlags_HasGamepad; + + io.BackendPlatformName = "switch"; + + // Keyboard mapping. ImGui will use those indices to peek into the io.KeysDown[] array. + io.KeyMap[ImGuiKey_Tab] = KBD_TAB; + io.KeyMap[ImGuiKey_LeftArrow] = KBD_LEFT; + io.KeyMap[ImGuiKey_RightArrow] = KBD_RIGHT; + io.KeyMap[ImGuiKey_UpArrow] = KBD_UP; + io.KeyMap[ImGuiKey_DownArrow] = KBD_DOWN; + io.KeyMap[ImGuiKey_PageUp] = KBD_PAGEUP; + io.KeyMap[ImGuiKey_PageDown] = KBD_PAGEDOWN; + io.KeyMap[ImGuiKey_Home] = KBD_HOME; + io.KeyMap[ImGuiKey_End] = KBD_END; + io.KeyMap[ImGuiKey_Insert] = KBD_INSERT; + io.KeyMap[ImGuiKey_Delete] = KBD_DELETE; + io.KeyMap[ImGuiKey_Backspace] = KBD_BACKSPACE; + io.KeyMap[ImGuiKey_Space] = KBD_SPACE; + io.KeyMap[ImGuiKey_Enter] = KBD_ENTER; + io.KeyMap[ImGuiKey_Escape] = KBD_ESC; + io.KeyMap[ImGuiKey_KeyPadEnter] = KBD_KPENTER; + io.KeyMap[ImGuiKey_A] = KBD_A; + io.KeyMap[ImGuiKey_C] = KBD_C; + io.KeyMap[ImGuiKey_V] = KBD_V; + io.KeyMap[ImGuiKey_X] = KBD_X; + io.KeyMap[ImGuiKey_Y] = KBD_Y; + io.KeyMap[ImGuiKey_Z] = KBD_Z; + + io.MouseDrawCursor = false; + + io.SetClipboardTextFn = setClipboardText; + io.GetClipboardTextFn = getClipboardText; + io.ClipboardUserData = nullptr; + + if (!loadFontAtlas ()) + { + ImFontConfig config; + config.MergeMode = false; + config.FontDataOwnedByAtlas = false; + + for (auto const &font : fonts) + { + io.Fonts->AddFontFromMemoryTTF (font.address, font.size, 14.0f, &config, nxFontRanges); + config.MergeMode = true; + } + io.Fonts->Flags |= ImFontAtlasFlags_NoPowerOfTwoHeight; + io.Fonts->Build (); + + saveFontAtlas (); + } + else + std::printf ("Loaded font atlas from disk\n"); + + return true; +} + +void imgui::nx::newFrame () +{ + ImGuiIO &io = ImGui::GetIO (); + IM_ASSERT (io.Fonts->IsBuilt () && + "Font atlas not built! It is generally built by the renderer back-end. Missing call " + "to renderer _NewFrame() function?"); + + io.DisplaySize = ImVec2 (s_width, s_height); + io.DisplayFramebufferScale = ImVec2 (1.0f, 1.0f); + + // Setup time step + static auto const start = std::chrono::high_resolution_clock::now (); + static auto prev = start; + auto const now = std::chrono::high_resolution_clock::now (); + + io.DeltaTime = std::chrono::duration (now - prev).count (); + prev = now; + + updateMouseButtons (io); + updateMousePos (io); + updateTouch (io); + updateGamepads (io); + updateKeyboard (io); + + io.MouseDrawCursor = s_showMouse; + + // Clamp mouse to screen + s_mousePos.x = std::clamp (s_mousePos.x, 0.0f, s_width); + s_mousePos.y = std::clamp (s_mousePos.y, 0.0f, s_height); +} + +void imgui::nx::exit () +{ + appletUnhook (&s_appletHookCookie); +} diff --git a/source/nx/imgui_nx.h b/source/nx/imgui_nx.h new file mode 100644 index 0000000..a8ed0ae --- /dev/null +++ b/source/nx/imgui_nx.h @@ -0,0 +1,32 @@ +// ftpd is a server implementation based on the following: +// - RFC 959 (https://tools.ietf.org/html/rfc959) +// - RFC 3659 (https://tools.ietf.org/html/rfc3659) +// - suggested implementation details from https://cr.yp.to/ftp/filesystem.html +// +// Copyright (C) 2020 Michael Theall +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// 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, see . + +#pragma once + +namespace imgui +{ +namespace nx +{ +bool init (); +void exit (); + +void newFrame (); +} +} diff --git a/source/nx/imgui_vsh.glsl b/source/nx/imgui_vsh.glsl new file mode 100644 index 0000000..5682eae --- /dev/null +++ b/source/nx/imgui_vsh.glsl @@ -0,0 +1,40 @@ +// ftpd is a server implementation based on the following: +// - RFC 959 (https://tools.ietf.org/html/rfc959) +// - RFC 3659 (https://tools.ietf.org/html/rfc3659) +// - suggested implementation details from https://cr.yp.to/ftp/filesystem.html +// +// Copyright (C) 2020 Michael Theall +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// 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, see . + +#version 460 + +layout (location = 0) in vec2 inPos; +layout (location = 1) in vec2 inUv; +layout (location = 2) in vec4 inColor; + +layout (location = 0) out vec2 vtxUv; +layout (location = 1) out vec4 vtxColor; + +layout (std140, binding = 0) uniform VertUBO +{ + mat4 projMtx; +} ubo; + +void main() +{ + gl_Position = ubo.projMtx * vec4 (inPos, 0.0, 1.0); + vtxUv = inUv; + vtxColor = inColor; +} diff --git a/source/nx/init.c b/source/nx/init.c new file mode 100644 index 0000000..6f084e1 --- /dev/null +++ b/source/nx/init.c @@ -0,0 +1,77 @@ +// ftpd is a server implementation based on the following: +// - RFC 959 (https://tools.ietf.org/html/rfc959) +// - RFC 3659 (https://tools.ietf.org/html/rfc3659) +// - suggested implementation details from https://cr.yp.to/ftp/filesystem.html +// +// Copyright (C) 2020 Michael Theall +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// 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, see . + +#include + +#include + +#ifndef NDEBUG +static int s_fd = -1; +#endif + +static SocketInitConfig const s_socketInitConfig = { + .bsdsockets_version = 1, + + .tcp_tx_buf_size = 1 * 1024 * 1024, + .tcp_rx_buf_size = 1 * 1024 * 1024, + .tcp_tx_buf_max_size = 4 * 1024 * 1024, + .tcp_rx_buf_max_size = 4 * 1024 * 1024, + + .udp_tx_buf_size = 0x2400, + .udp_rx_buf_size = 0xA500, + + .sb_efficiency = 8, + + .num_bsd_sessions = 3, + .bsd_service_type = BsdServiceType_User, +}; + +u32 __nx_fs_num_sessions = 3; + +void userAppInit () +{ + appletLockExit (); + + romfsInit (); + plInitialize (); + + if (R_FAILED (socketInitialize (&s_socketInitConfig))) + return; + +#ifndef NDEBUG + s_fd = nxlinkStdio (); +#endif +} + +void userAppExit () +{ +#ifndef NDEBUG + if (s_fd >= 0) + { + close (s_fd); + socketExit (); + s_fd = -1; + } +#endif + + plExit (); + romfsExit (); + appletUnlockExit (); +} diff --git a/source/nx/platform.cpp b/source/nx/platform.cpp new file mode 100644 index 0000000..0c6b1ed --- /dev/null +++ b/source/nx/platform.cpp @@ -0,0 +1,167 @@ +// ftpd is a server implementation based on the following: +// - RFC 959 (https://tools.ietf.org/html/rfc959) +// - RFC 3659 (https://tools.ietf.org/html/rfc3659) +// - suggested implementation details from https://cr.yp.to/ftp/filesystem.html +// +// Copyright (C) 2020 Michael Theall +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// 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, see . + +#include "platform.h" + +#include "imgui_deko3d.h" +#include "imgui_nx.h" + +#include "imgui.h" + +#include + +#include +#include + +bool platform::init () +{ + IMGUI_CHECKVERSION (); + ImGui::CreateContext (); + + if (!imgui::nx::init ()) + { + ImGui::DestroyContext (); + return false; + } + + imgui::deko3d::init (); + + return true; +} + +bool platform::loop () +{ + if (!appletMainLoop ()) + return false; + + hidScanInput (); + + auto const keysDown = hidKeysDown (CONTROLLER_P1_AUTO); + if (keysDown & KEY_PLUS) + return false; + + imgui::nx::newFrame (); + imgui::deko3d::newFrame (); + ImGui::NewFrame (); + + imgui::deko3d::test (); + + return true; +} + +void platform::render () +{ + ImGui::Render (); + + imgui::deko3d::render (); +} + +void platform::exit () +{ + imgui::nx::exit (); + imgui::deko3d::exit (); + + ImGui::DestroyContext (); +} + +/////////////////////////////////////////////////////////////////////////// +class platform::Thread::privateData_t +{ +public: + privateData_t () = default; + + privateData_t (std::function func_) : thread (func_) + { + } + + std::thread thread; +}; + +/////////////////////////////////////////////////////////////////////////// +platform::Thread::~Thread () = default; + +platform::Thread::Thread () : m_d (new privateData_t ()) +{ +} + +platform::Thread::Thread (std::function func_) : m_d (new privateData_t (func_)) +{ +} + +platform::Thread::Thread (Thread &&that_) : m_d (new privateData_t ()) +{ + std::swap (m_d, that_.m_d); +} + +platform::Thread &platform::Thread::operator= (Thread &&that_) +{ + std::swap (m_d, that_.m_d); + return *this; +} + +void platform::Thread::join () +{ + m_d->thread.join (); +} + +void platform::Thread::sleep (std::chrono::milliseconds const timeout_) +{ + std::this_thread::sleep_for (timeout_); +} + +/////////////////////////////////////////////////////////////////////////// +#define USE_STD_MUTEX 1 +class platform::Mutex::privateData_t +{ +public: +#if USE_STD_MUTEX + std::mutex mutex; +#else + ::Mutex mutex; +#endif +}; + +/////////////////////////////////////////////////////////////////////////// +platform::Mutex::~Mutex () = default; + +platform::Mutex::Mutex () : m_d (new privateData_t ()) +{ +#if !USE_STD_MUTEX + mutexInit (&m_d->mutex); +#endif +} + +void platform::Mutex::lock () +{ +#if USE_STD_MUTEX + m_d->mutex.lock (); +#else + mutexLock (&m_d->mutex); +#endif +} + +void platform::Mutex::unlock () +{ +#if USE_STD_MUTEX + m_d->mutex.unlock (); +#else + mutexUnlock (&m_d->mutex); +#endif +} diff --git a/source/pc/KHR/khrplatform.h b/source/pc/KHR/khrplatform.h new file mode 100644 index 0000000..7783cad --- /dev/null +++ b/source/pc/KHR/khrplatform.h @@ -0,0 +1 @@ +#error "Please use https://glad.dav1d.de/#profile=core&language=c&specification=gl&loader=on&api=gl%3D4.3" diff --git a/source/pc/glad.c b/source/pc/glad.c new file mode 100644 index 0000000..7783cad --- /dev/null +++ b/source/pc/glad.c @@ -0,0 +1 @@ +#error "Please use https://glad.dav1d.de/#profile=core&language=c&specification=gl&loader=on&api=gl%3D4.3" diff --git a/source/pc/glad/glad.h b/source/pc/glad/glad.h new file mode 100644 index 0000000..7783cad --- /dev/null +++ b/source/pc/glad/glad.h @@ -0,0 +1 @@ +#error "Please use https://glad.dav1d.de/#profile=core&language=c&specification=gl&loader=on&api=gl%3D4.3" diff --git a/source/pc/imgui_impl_glfw.cpp b/source/pc/imgui_impl_glfw.cpp new file mode 100644 index 0000000..10e67bc --- /dev/null +++ b/source/pc/imgui_impl_glfw.cpp @@ -0,0 +1 @@ +#error "Please use https://github.com/ocornut/imgui" diff --git a/source/pc/imgui_impl_glfw.h b/source/pc/imgui_impl_glfw.h new file mode 100644 index 0000000..10e67bc --- /dev/null +++ b/source/pc/imgui_impl_glfw.h @@ -0,0 +1 @@ +#error "Please use https://github.com/ocornut/imgui" diff --git a/source/pc/imgui_impl_opengl3.cpp b/source/pc/imgui_impl_opengl3.cpp new file mode 100644 index 0000000..10e67bc --- /dev/null +++ b/source/pc/imgui_impl_opengl3.cpp @@ -0,0 +1 @@ +#error "Please use https://github.com/ocornut/imgui" diff --git a/source/pc/imgui_impl_opengl3.h b/source/pc/imgui_impl_opengl3.h new file mode 100644 index 0000000..10e67bc --- /dev/null +++ b/source/pc/imgui_impl_opengl3.h @@ -0,0 +1 @@ +#error "Please use https://github.com/ocornut/imgui" diff --git a/source/pc/platform.cpp b/source/pc/platform.cpp new file mode 100644 index 0000000..3cc737f --- /dev/null +++ b/source/pc/platform.cpp @@ -0,0 +1,259 @@ +// ftpd is a server implementation based on the following: +// - RFC 959 (https://tools.ietf.org/html/rfc959) +// - RFC 3659 (https://tools.ietf.org/html/rfc3659) +// - suggested implementation details from https://cr.yp.to/ftp/filesystem.html +// +// Copyright (C) 2020 Michael Theall +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// 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, see . + +#include "platform.h" + +#include "imgui.h" + +#include + +#include + +#include "imgui_impl_glfw.h" +#include "imgui_impl_opengl3.h" + +#include +#include +#include +#include + +namespace +{ +std::unique_ptr s_mainWindow (nullptr, glfwDestroyWindow); + +void windowResize (GLFWwindow *const window_, int const width_, int const height_) +{ + (void)window_; + + if (!width_ || !height_) + return; + + glViewport (0, 0, width_, height_); +} + +#ifndef NDEBUG +void logCallback (GLenum const source_, + GLenum const type_, + GLuint const id_, + GLenum const severity_, + GLsizei const length_, + GLchar const *const message_, + void const *const userParam_) +{ + (void)source_; + (void)type_; + (void)id_; + (void)severity_; + (void)length_; + (void)userParam_; + // std::fprintf (stderr, "%s\n", message_); +} +#endif +} + +bool platform::init () +{ + if (!glfwInit ()) + { + std::fprintf (stderr, "Failed to initialize GLFW\n"); + return false; + } + + glfwWindowHint (GLFW_CONTEXT_VERSION_MAJOR, 4); + glfwWindowHint (GLFW_CONTEXT_VERSION_MINOR, 3); + glfwWindowHint (GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); +#ifndef NDEBUG + glfwWindowHint (GLFW_OPENGL_DEBUG_CONTEXT, GL_TRUE); +#endif + + // set depth buffer size + glfwWindowHint (GLFW_DEPTH_BITS, 24); + glfwWindowHint (GLFW_STENCIL_BITS, 8); + + s_mainWindow.reset (glfwCreateWindow (1280, 720, "Test Game", nullptr, nullptr)); + if (!s_mainWindow) + { + std::fprintf (stderr, "Failed to create window\n"); + glfwTerminate (); + return false; + } + + glfwSwapInterval (1); + + // create context + glfwMakeContextCurrent (s_mainWindow.get ()); + glfwSetFramebufferSizeCallback (s_mainWindow.get (), windowResize); + + if (!gladLoadGL ()) + { + std::fprintf (stderr, "gladLoadGL: failed\n"); + platform::exit (); + return false; + } + +#ifndef NDEBUG + GLint flags; + glGetIntegerv (GL_CONTEXT_FLAGS, &flags); + if (flags & GL_CONTEXT_FLAG_DEBUG_BIT) + { + glEnable (GL_DEBUG_OUTPUT); + glEnable (GL_DEBUG_OUTPUT_SYNCHRONOUS); + glDebugMessageCallback (logCallback, nullptr); + glDebugMessageControl (GL_DONT_CARE, GL_DONT_CARE, GL_DONT_CARE, 0, nullptr, GL_TRUE); + } +#endif + + glEnable (GL_CULL_FACE); + glFrontFace (GL_CCW); + glCullFace (GL_BACK); + + glEnable (GL_DEPTH_TEST); + glDepthFunc (GL_LEQUAL); + + glClearColor (104.0f / 255.0f, 176.0f / 255.0f, 216.0f / 255.0f, 1.0f); + glEnable (GL_BLEND); + glBlendFunc (GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + + std::printf ("Renderer: %s\n", glGetString (GL_RENDERER)); + std::printf ("OpenGL Version: %s\n", glGetString (GL_VERSION)); + + IMGUI_CHECKVERSION (); + ImGui::CreateContext (); + + ImGui_ImplGlfw_InitForOpenGL (s_mainWindow.get (), true); + ImGui_ImplOpenGL3_Init ("#version 150"); + + ImGuiIO &io = ImGui::GetIO (); + io.IniFilename = nullptr; + + return true; +} + +bool platform::loop () +{ + bool inactive; + do + { + inactive = glfwGetWindowAttrib (s_mainWindow.get (), GLFW_ICONIFIED); + (inactive ? glfwWaitEvents : glfwPollEvents) (); + + if (glfwWindowShouldClose (s_mainWindow.get ())) + return false; + } while (inactive); + + ImGui_ImplOpenGL3_NewFrame (); + ImGui_ImplGlfw_NewFrame (); + ImGui::NewFrame (); + + return true; +} + +void platform::render () +{ + ImGui::Render (); + + int width; + int height; + glfwGetFramebufferSize (s_mainWindow.get (), &width, &height); + glViewport (0, 0, width, height); + glClearColor (0.45f, 0.55f, 0.60f, 1.00f); + glClear (GL_COLOR_BUFFER_BIT); + + ImGui_ImplOpenGL3_RenderDrawData (ImGui::GetDrawData ()); + + glfwSwapBuffers (s_mainWindow.get ()); +} + +void platform::exit () +{ + ImGui::DestroyContext (); + + s_mainWindow.reset (); + glfwTerminate (); +} + +/////////////////////////////////////////////////////////////////////////// +class platform::Thread::privateData_t +{ +public: + privateData_t () = default; + + privateData_t (std::function func_) : thread (func_) + { + } + + std::thread thread; +}; + +/////////////////////////////////////////////////////////////////////////// +platform::Thread::~Thread () = default; + +platform::Thread::Thread () : m_d (new privateData_t ()) +{ +} + +platform::Thread::Thread (std::function func_) : m_d (new privateData_t (func_)) +{ +} + +platform::Thread::Thread (Thread &&that_) : m_d (new privateData_t ()) +{ + std::swap (m_d, that_.m_d); +} + +platform::Thread &platform::Thread::operator= (Thread &&that_) +{ + std::swap (m_d, that_.m_d); + return *this; +} + +void platform::Thread::join () +{ + m_d->thread.join (); +} + +void platform::Thread::sleep (std::chrono::milliseconds const timeout_) +{ + std::this_thread::sleep_for (timeout_); +} + +/////////////////////////////////////////////////////////////////////////// +class platform::Mutex::privateData_t +{ +public: + std::mutex mutex; +}; + +/////////////////////////////////////////////////////////////////////////// +platform::Mutex::~Mutex () = default; + +platform::Mutex::Mutex () : m_d (new privateData_t ()) +{ +} + +void platform::Mutex::lock () +{ + m_d->mutex.lock (); +} + +void platform::Mutex::unlock () +{ + m_d->mutex.unlock (); +} diff --git a/source/sockAddr.cpp b/source/sockAddr.cpp new file mode 100644 index 0000000..26fecec --- /dev/null +++ b/source/sockAddr.cpp @@ -0,0 +1,150 @@ +// ftpd is a server implementation based on the following: +// - RFC 959 (https://tools.ietf.org/html/rfc959) +// - RFC 3659 (https://tools.ietf.org/html/rfc3659) +// - suggested implementation details from https://cr.yp.to/ftp/filesystem.html +// +// Copyright (C) 2020 Michael Theall +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// 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, see . + +#include "sockAddr.h" + +#include + +#include +#include + +/////////////////////////////////////////////////////////////////////////// +SockAddr::~SockAddr () = default; + +SockAddr::SockAddr () = default; + +SockAddr::SockAddr (SockAddr const &that_) = default; + +SockAddr::SockAddr (SockAddr &&that_) = default; + +SockAddr &SockAddr::operator= (SockAddr const &that_) = default; + +SockAddr &SockAddr::operator= (SockAddr &&that_) = default; + +SockAddr::SockAddr (struct sockaddr const &addr_) +{ + switch (addr_.sa_family) + { + case AF_INET: + std::memcpy (&m_addr, &addr_, sizeof (struct sockaddr_in)); + break; + +#ifndef _3DS + case AF_INET6: + std::memcpy (&m_addr, &addr_, sizeof (struct sockaddr_in6)); + break; +#endif + } +} + +SockAddr::SockAddr (struct sockaddr_in const &addr_) + : SockAddr (reinterpret_cast (addr_)) +{ +} + +#ifndef _3DS +SockAddr::SockAddr (struct sockaddr_in6 const &addr_) + : SockAddr (reinterpret_cast (addr_)) +{ +} +#endif + +SockAddr::SockAddr (struct sockaddr_storage const &addr_) + : SockAddr (reinterpret_cast (addr_)) +{ +} + +SockAddr::operator struct sockaddr_in const & () const +{ + assert (m_addr.ss_family == AF_INET); + return reinterpret_cast (m_addr); +} + +#ifndef _3DS +SockAddr::operator struct sockaddr_in6 const & () const +{ + assert (m_addr.ss_family == AF_INET6); + return reinterpret_cast (m_addr); +} +#endif + +SockAddr::operator struct sockaddr_storage const & () const +{ + return m_addr; +} + +SockAddr::operator struct sockaddr * () +{ + return reinterpret_cast (&m_addr); +} + +SockAddr::operator struct sockaddr const * () const +{ + return reinterpret_cast (&m_addr); +} + +std::uint16_t SockAddr::port () const +{ + switch (m_addr.ss_family) + { + case AF_INET: + return ntohs (reinterpret_cast (&m_addr)->sin_port); + +#ifndef _3DS + case AF_INET6: + return ntohs (reinterpret_cast (&m_addr)->sin6_port); +#endif + } + + return 0; +} + +char const *SockAddr::name (char *buffer_, std::size_t size_) const +{ + switch (m_addr.ss_family) + { + case AF_INET: + return inet_ntop (AF_INET, + &reinterpret_cast (&m_addr)->sin_addr, + buffer_, + size_); + +#ifndef _3DS + case AF_INET6: + return inet_ntop (AF_INET6, + &reinterpret_cast (&m_addr)->sin6_addr, + buffer_, + size_); +#endif + } + + return nullptr; +} + +char const *SockAddr::name () const +{ +#if defined(_3DS) + thread_local static char buffer[INET_ADDRSTRLEN]; +#else + thread_local static char buffer[INET6_ADDRSTRLEN]; +#endif + + return name (buffer, sizeof (buffer)); +} diff --git a/source/socket.cpp b/source/socket.cpp new file mode 100644 index 0000000..3c88ecf --- /dev/null +++ b/source/socket.cpp @@ -0,0 +1,334 @@ +// ftpd is a server implementation based on the following: +// - RFC 959 (https://tools.ietf.org/html/rfc959) +// - RFC 3659 (https://tools.ietf.org/html/rfc3659) +// - suggested implementation details from https://cr.yp.to/ftp/filesystem.html +// +// Copyright (C) 2020 Michael Theall +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// 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, see . + +#include "socket.h" + +#include "log.h" + +#include +#include +#include + +#include + +#include +#include +#include +#include + +/////////////////////////////////////////////////////////////////////////// +Socket::~Socket () +{ + if (m_listening) + Log::info ("Stop listening on [%s]:%u\n", m_sockName.name (), m_sockName.port ()); + if (m_connected) + Log::info ("Closing connection to [%s]:%u\n", m_peerName.name (), m_peerName.port ()); + if (::close (m_fd) != 0) + Log::error ("close: %s\n", std::strerror (errno)); +} + +Socket::Socket (int const fd_) : m_fd (fd_), m_listening (false), m_connected (false) +{ +} + +Socket::Socket (int const fd_, SockAddr const &sockName_, SockAddr const &peerName_) + : m_sockName (sockName_), + m_peerName (peerName_), + m_fd (fd_), + m_listening (false), + m_connected (true) +{ +} + +UniqueSocket Socket::accept () +{ + SockAddr addr; + socklen_t addrLen = sizeof (struct sockaddr_storage); + + auto const fd = ::accept (m_fd, addr, &addrLen); + if (fd < 0) + { + Log::error ("accept: %s\n", std::strerror (errno)); + return {nullptr, {}}; + } + + Log::info ("Accepted connection from [%s]:%u\n", addr.name (), addr.port ()); + return UniqueSocket (new Socket (fd, m_sockName, addr)); +} + +int Socket::atMark () +{ + auto const rc = ::sockatmark (m_fd); + if (rc < 0) + Log::error ("sockatmark: %s\n", std::strerror (errno)); + + return rc; +} + +bool Socket::bind (SockAddr const &addr_) +{ + switch (static_cast (addr_).ss_family) + { + case AF_INET: + if (::bind (m_fd, addr_, sizeof (struct sockaddr_in)) != 0) + { + Log::error ("bind: %s\n", std::strerror (errno)); + return false; + } + break; + +#ifndef _3DS + case AF_INET6: + if (::bind (m_fd, addr_, sizeof (struct sockaddr_in6)) != 0) + { + Log::error ("bind: %s\n", std::strerror (errno)); + return false; + } + break; +#endif + + default: + errno = EINVAL; + Log::error ("bind: %s\n", std::strerror (errno)); + break; + } + + if (addr_.port () == 0) + { + socklen_t addrLen = sizeof (struct sockaddr_storage); + if (::getsockname (m_fd, m_sockName, &addrLen) != 0) + Log::error ("getsockname: %s\n", std::strerror (errno)); + } + else + m_sockName = addr_; + + return true; +} + +bool Socket::connect (SockAddr const &addr_) +{ + if (::connect (m_fd, addr_, sizeof (struct sockaddr_storage)) != 0) + { + if (errno != EINPROGRESS) + Log::error ("connect: %s\n", std::strerror (errno)); + else + { + m_peerName = addr_; + m_connected = true; + Log::info ("Connecting to [%s]:%u\n", addr_.name (), addr_.port ()); + } + return false; + } + + m_peerName = addr_; + m_connected = true; + Log::info ("Connected to [%s]:%u\n", addr_.name (), addr_.port ()); + return true; +} + +bool Socket::listen (int const backlog_) +{ + if (::listen (m_fd, backlog_) != 0) + { + Log::error ("listen: %s\n", std::strerror (errno)); + return false; + } + + m_listening = true; + return true; +} + +bool Socket::shutdown (int const how_) +{ + if (::shutdown (m_fd, how_) != 0) + { + Log::info ("shutdown: %s\n", std::strerror (errno)); + return false; + } + + return true; +} + +bool Socket::setLinger (bool const enable_, std::chrono::seconds const time_) +{ + struct linger linger; + linger.l_onoff = enable_; + linger.l_linger = time_.count (); + + if (::setsockopt (m_fd, SOL_SOCKET, SO_LINGER, &linger, sizeof (linger)) != 0) + { + Log::error ("setsockopt(SO_LINGER, %s, %lus): %s\n", + enable_ ? "on" : "off", + static_cast (time_.count ()), + std::strerror (errno)); + return false; + } + + return true; +} + +bool Socket::setNonBlocking (bool const nonBlocking_) +{ + auto flags = ::fcntl (m_fd, F_GETFL, 0); + if (flags == -1) + { + Log::error ("fcntl(F_GETFL): %s\n", std::strerror (errno)); + return false; + } + + if (nonBlocking_) + flags |= O_NONBLOCK; + else + flags &= ~O_NONBLOCK; + + if (::fcntl (m_fd, F_SETFL, flags) != 0) + { + Log::error ("fcntl(F_SETFL): %s\n", std::strerror (errno)); + return false; + } + + return true; +} + +bool Socket::setReuseAddress (bool const reuse_) +{ + int reuse = reuse_; + if (::setsockopt (m_fd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof (reuse)) != 0) + { + Log::error ("setsockopt(SO_REUSEADDR, %u): %s\n", reuse_, std::strerror (errno)); + return false; + } + + return true; +} + +bool Socket::setRecvBufferSize (std::size_t const size_) +{ + int size = size_; + if (::setsockopt (m_fd, SOL_SOCKET, SO_RCVBUF, &size, sizeof (size)) != 0) + { + Log::error ("setsockopt(SO_RCVBUF, %zu): %s\n", size_, std::strerror (errno)); + return false; + } + + return true; +} + +bool Socket::setSendBufferSize (std::size_t const size_) +{ + int size = size_; + if (::setsockopt (m_fd, SOL_SOCKET, SO_SNDBUF, &size, sizeof (size)) != 0) + { + Log::error ("setsockopt(SO_SNDBUF, %zu): %s\n", size_, std::strerror (errno)); + return false; + } + + return true; +} + +ssize_t Socket::read (void *const buffer_, std::size_t const size_, bool const oob_) +{ + assert (buffer_); + assert (size_); + auto const rc = ::recv (m_fd, buffer_, size_, oob_ ? MSG_OOB : 0); + if (rc < 0 && errno != EWOULDBLOCK) + Log::error ("recv: %s\n", std::strerror (errno)); + + return rc; +} + +ssize_t Socket::read (IOBuffer &buffer_, bool const oob_) +{ + auto const rc = read (buffer_.freeArea (), buffer_.freeSize (), oob_); + if (rc > 0) + buffer_.markUsed (rc); + + return rc; +} + +ssize_t Socket::write (void const *const buffer_, std::size_t const size_) +{ + assert (buffer_); + assert (size_); + auto const rc = ::send (m_fd, buffer_, size_, 0); + if (rc < 0 && errno != EWOULDBLOCK) + Log::error ("send: %s\n", std::strerror (errno)); + + return rc; +} + +ssize_t Socket::write (IOBuffer &buffer_) +{ + auto const rc = write (buffer_.usedArea (), buffer_.usedSize ()); + if (rc > 0) + buffer_.markFree (rc); + + return rc; +} + +SockAddr const &Socket::sockName () const +{ + return m_sockName; +} + +SockAddr const &Socket::peerName () const +{ + return m_peerName; +} + +UniqueSocket Socket::create () +{ + auto const fd = ::socket (AF_INET, SOCK_STREAM, 0); + if (fd < 0) + { + Log::error ("socket: %s\n", std::strerror (errno)); + return nullptr; + } + + return UniqueSocket (new Socket (fd)); +} + +int Socket::poll (PollInfo *const info_, + std::size_t const count_, + std::chrono::milliseconds const timeout_) +{ + if (count_ == 0) + return 0; + + auto const pfd = std::make_unique (count_); + for (std::size_t i = 0; i < count_; ++i) + { + pfd[i].fd = info_[i].socket.get ().m_fd; + pfd[i].events = info_[i].events; + pfd[i].revents = 0; + } + + auto const rc = ::poll (pfd.get (), count_, timeout_.count ()); + if (rc < 0) + { + Log::error ("poll: %s\n", std::strerror (errno)); + return rc; + } + + for (std::size_t i = 0; i < count_; ++i) + info_[i].revents = pfd[i].revents; + + return rc; +}