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 9d04fbb..a15c750 100644
Binary files a/ftpd_qr.png and b/ftpd_qr.png differ
diff --git a/gfx.3ds/battery0.png b/gfx.3ds/battery0.png
new file mode 100644
index 0000000..eed0f49
Binary files /dev/null and b/gfx.3ds/battery0.png differ
diff --git a/gfx.3ds/battery1.png b/gfx.3ds/battery1.png
new file mode 100644
index 0000000..855d8a8
Binary files /dev/null and b/gfx.3ds/battery1.png differ
diff --git a/gfx.3ds/battery2.png b/gfx.3ds/battery2.png
new file mode 100644
index 0000000..45c5875
Binary files /dev/null and b/gfx.3ds/battery2.png differ
diff --git a/gfx.3ds/battery3.png b/gfx.3ds/battery3.png
new file mode 100644
index 0000000..713d244
Binary files /dev/null and b/gfx.3ds/battery3.png differ
diff --git a/gfx.3ds/battery4.png b/gfx.3ds/battery4.png
new file mode 100644
index 0000000..05d7b8b
Binary files /dev/null and b/gfx.3ds/battery4.png differ
diff --git a/gfx.3ds/batteryCharge.png b/gfx.3ds/batteryCharge.png
new file mode 100644
index 0000000..792398c
Binary files /dev/null and b/gfx.3ds/batteryCharge.png differ
diff --git a/gfx.3ds/c3dlogo.png b/gfx.3ds/c3dlogo.png
new file mode 100644
index 0000000..78634d8
Binary files /dev/null and b/gfx.3ds/c3dlogo.png differ
diff --git a/gfx.3ds/gfx.t3s b/gfx.3ds/gfx.t3s
new file mode 100644
index 0000000..7a959ef
--- /dev/null
+++ b/gfx.3ds/gfx.t3s
@@ -0,0 +1,12 @@
+-f rgba -z auto --atlas
+battery0.png
+battery1.png
+battery2.png
+battery3.png
+battery4.png
+batteryCharge.png
+c3dlogo.png
+wifiNull.png
+wifi1.png
+wifi2.png
+wifi3.png
diff --git a/gfx.3ds/wifi1.png b/gfx.3ds/wifi1.png
new file mode 100644
index 0000000..bf5d1d4
Binary files /dev/null and b/gfx.3ds/wifi1.png differ
diff --git a/gfx.3ds/wifi2.png b/gfx.3ds/wifi2.png
new file mode 100644
index 0000000..428d1d8
Binary files /dev/null and b/gfx.3ds/wifi2.png differ
diff --git a/gfx.3ds/wifi3.png b/gfx.3ds/wifi3.png
new file mode 100644
index 0000000..92ea210
Binary files /dev/null and b/gfx.3ds/wifi3.png differ
diff --git a/gfx.3ds/wifiNull.png b/gfx.3ds/wifiNull.png
new file mode 100644
index 0000000..91c4176
Binary files /dev/null and b/gfx.3ds/wifiNull.png differ
diff --git a/gfx.switch/deko3d.png b/gfx.switch/deko3d.png
new file mode 100644
index 0000000..fbf1adf
Binary files /dev/null and b/gfx.switch/deko3d.png differ
diff --git a/include/console.h b/include/console.h
deleted file mode 100644
index cc489f7..0000000
--- a/include/console.h
+++ /dev/null
@@ -1,44 +0,0 @@
-#pragma once
-
-#ifdef _3DS
-#include <3ds.h>
-#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