diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..64ef6bf --- /dev/null +++ b/.travis.yml @@ -0,0 +1,28 @@ +language: c +sudo: required +dist: trusty + +services: + - docker + +env: + - OS=debian + - OS=fedora + - OS=fedora-w64 + +before_script: + - docker build -t libxmlb-${OS} -f contrib/ci/Dockerfile-${OS} . + +script: + - docker run -t -v `pwd`:/build libxmlb-${OS} ./contrib/ci/build-${OS}.sh + +jobs: + include: + - stage: abi + name: "Check for ABI breaks" + env: + - OS=fedora + before_script: + - docker build -t libxmlb-${OS} -f contrib/ci/Dockerfile-${OS} . + script: + - docker run -t -v `pwd`:/build libxmlb-${OS} ./contrib/ci/check-abi $(git describe --abbrev=0 --tags) $(git rev-parse HEAD) diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..20a7236 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,46 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at richard@hughsie.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] + +[homepage]: http://contributor-covenant.org +[version]: http://contributor-covenant.org/version/1/4/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..8000a6f --- /dev/null +++ b/LICENSE @@ -0,0 +1,504 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 2.1, February 1999 + + Copyright (C) 1991, 1999 Free Software Foundation, Inc. + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + +[This is the first released version of the Lesser GPL. It also counts + as the successor of the GNU Library Public License, version 2, hence + the version number 2.1.] + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +Licenses are intended to guarantee your freedom to share and change +free software--to make sure the software is free for all its users. + + This license, the Lesser General Public License, applies to some +specially designated software packages--typically libraries--of the +Free Software Foundation and other authors who decide to use it. You +can use it too, but we suggest you first think carefully about whether +this license or the ordinary General Public License is the better +strategy to use in any particular case, based on the explanations below. + + When we speak of free software, we are referring to freedom of use, +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 this service if you wish); that you receive source code or can get +it if you want it; that you can change the software and use pieces of +it in new free programs; and that you are informed that you can do +these things. + + To protect your rights, we need to make restrictions that forbid +distributors to deny you these rights or to ask you to surrender these +rights. These restrictions translate to certain responsibilities for +you if you distribute copies of the library or if you modify it. + + For example, if you distribute copies of the library, whether gratis +or for a fee, you must give the recipients all the rights that we gave +you. You must make sure that they, too, receive or can get the source +code. If you link other code with the library, you must provide +complete object files to the recipients, so that they can relink them +with the library after making changes to the library and recompiling +it. And you must show them these terms so they know their rights. + + We protect your rights with a two-step method: (1) we copyright the +library, and (2) we offer you this license, which gives you legal +permission to copy, distribute and/or modify the library. + + To protect each distributor, we want to make it very clear that +there is no warranty for the free library. Also, if the library is +modified by someone else and passed on, the recipients should know +that what they have is not the original version, so that the original +author's reputation will not be affected by problems that might be +introduced by others. + + Finally, software patents pose a constant threat to the existence of +any free program. We wish to make sure that a company cannot +effectively restrict the users of a free program by obtaining a +restrictive license from a patent holder. Therefore, we insist that +any patent license obtained for a version of the library must be +consistent with the full freedom of use specified in this license. + + Most GNU software, including some libraries, is covered by the +ordinary GNU General Public License. This license, the GNU Lesser +General Public License, applies to certain designated libraries, and +is quite different from the ordinary General Public License. We use +this license for certain libraries in order to permit linking those +libraries into non-free programs. + + When a program is linked with a library, whether statically or using +a shared library, the combination of the two is legally speaking a +combined work, a derivative of the original library. The ordinary +General Public License therefore permits such linking only if the +entire combination fits its criteria of freedom. The Lesser General +Public License permits more lax criteria for linking other code with +the library. + + We call this license the "Lesser" General Public License because it +does Less to protect the user's freedom than the ordinary General +Public License. It also provides other free software developers Less +of an advantage over competing non-free programs. These disadvantages +are the reason we use the ordinary General Public License for many +libraries. However, the Lesser license provides advantages in certain +special circumstances. + + For example, on rare occasions, there may be a special need to +encourage the widest possible use of a certain library, so that it becomes +a de-facto standard. To achieve this, non-free programs must be +allowed to use the library. A more frequent case is that a free +library does the same job as widely used non-free libraries. In this +case, there is little to gain by limiting the free library to free +software only, so we use the Lesser General Public License. + + In other cases, permission to use a particular library in non-free +programs enables a greater number of people to use a large body of +free software. For example, permission to use the GNU C Library in +non-free programs enables many more people to use the whole GNU +operating system, as well as its variant, the GNU/Linux operating +system. + + Although the Lesser General Public License is Less protective of the +users' freedom, it does ensure that the user of a program that is +linked with the Library has the freedom and the wherewithal to run +that program using a modified version of the Library. + + The precise terms and conditions for copying, distribution and +modification follow. Pay close attention to the difference between a +"work based on the library" and a "work that uses the library". The +former contains code derived from the library, whereas the latter must +be combined with the library in order to run. + + GNU LESSER GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License Agreement applies to any software library or other +program which contains a notice placed by the copyright holder or +other authorized party saying it may be distributed under the terms of +this Lesser General Public License (also called "this License"). +Each licensee is addressed as "you". + + A "library" means a collection of software functions and/or data +prepared so as to be conveniently linked with application programs +(which use some of those functions and data) to form executables. + + The "Library", below, refers to any such software library or work +which has been distributed under these terms. A "work based on the +Library" means either the Library or any derivative work under +copyright law: that is to say, a work containing the Library or a +portion of it, either verbatim or with modifications and/or translated +straightforwardly into another language. (Hereinafter, translation is +included without limitation in the term "modification".) + + "Source code" for a work means the preferred form of the work for +making modifications to it. For a library, complete source code means +all the source code for all modules it contains, plus any associated +interface definition files, plus the scripts used to control compilation +and installation of the library. + + Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running a program using the Library is not restricted, and output from +such a program is covered only if its contents constitute a work based +on the Library (independent of the use of the Library in a tool for +writing it). Whether that is true depends on what the Library does +and what the program that uses the Library does. + + 1. You may copy and distribute verbatim copies of the Library's +complete source code as you receive it, in any medium, provided that +you conspicuously and appropriately publish on each copy an +appropriate copyright notice and disclaimer of warranty; keep intact +all the notices that refer to this License and to the absence of any +warranty; and distribute a copy of this License along with the +Library. + + You may charge a fee for the physical act of transferring a copy, +and you may at your option offer warranty protection in exchange for a +fee. + + 2. You may modify your copy or copies of the Library or any portion +of it, thus forming a work based on the Library, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) The modified work must itself be a software library. + + b) You must cause the files modified to carry prominent notices + stating that you changed the files and the date of any change. + + c) You must cause the whole of the work to be licensed at no + charge to all third parties under the terms of this License. + + d) If a facility in the modified Library refers to a function or a + table of data to be supplied by an application program that uses + the facility, other than as an argument passed when the facility + is invoked, then you must make a good faith effort to ensure that, + in the event an application does not supply such function or + table, the facility still operates, and performs whatever part of + its purpose remains meaningful. + + (For example, a function in a library to compute square roots has + a purpose that is entirely well-defined independent of the + application. Therefore, Subsection 2d requires that any + application-supplied function or table used by this function must + be optional: if the application does not supply it, the square + root function must still compute square roots.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Library, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Library, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote +it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Library. + +In addition, mere aggregation of another work not based on the Library +with the Library (or with a work based on the Library) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may opt to apply the terms of the ordinary GNU General Public +License instead of this License to a given copy of the Library. To do +this, you must alter all the notices that refer to this License, so +that they refer to the ordinary GNU General Public License, version 2, +instead of to this License. (If a newer version than version 2 of the +ordinary GNU General Public License has appeared, then you can specify +that version instead if you wish.) Do not make any other change in +these notices. + + Once this change is made in a given copy, it is irreversible for +that copy, so the ordinary GNU General Public License applies to all +subsequent copies and derivative works made from that copy. + + This option is useful when you wish to copy part of the code of +the Library into a program that is not a library. + + 4. You may copy and distribute the Library (or a portion or +derivative of it, under Section 2) in object code or executable form +under the terms of Sections 1 and 2 above provided that you accompany +it with the complete corresponding machine-readable source code, which +must be distributed under the terms of Sections 1 and 2 above on a +medium customarily used for software interchange. + + If distribution of object code is made by offering access to copy +from a designated place, then offering equivalent access to copy the +source code from the same place satisfies the requirement to +distribute the source code, even though third parties are not +compelled to copy the source along with the object code. + + 5. A program that contains no derivative of any portion of the +Library, but is designed to work with the Library by being compiled or +linked with it, is called a "work that uses the Library". Such a +work, in isolation, is not a derivative work of the Library, and +therefore falls outside the scope of this License. + + However, linking a "work that uses the Library" with the Library +creates an executable that is a derivative of the Library (because it +contains portions of the Library), rather than a "work that uses the +library". The executable is therefore covered by this License. +Section 6 states terms for distribution of such executables. + + When a "work that uses the Library" uses material from a header file +that is part of the Library, the object code for the work may be a +derivative work of the Library even though the source code is not. +Whether this is true is especially significant if the work can be +linked without the Library, or if the work is itself a library. The +threshold for this to be true is not precisely defined by law. + + If such an object file uses only numerical parameters, data +structure layouts and accessors, and small macros and small inline +functions (ten lines or less in length), then the use of the object +file is unrestricted, regardless of whether it is legally a derivative +work. (Executables containing this object code plus portions of the +Library will still fall under Section 6.) + + Otherwise, if the work is a derivative of the Library, you may +distribute the object code for the work under the terms of Section 6. +Any executables containing that work also fall under Section 6, +whether or not they are linked directly with the Library itself. + + 6. As an exception to the Sections above, you may also combine or +link a "work that uses the Library" with the Library to produce a +work containing portions of the Library, and distribute that work +under terms of your choice, provided that the terms permit +modification of the work for the customer's own use and reverse +engineering for debugging such modifications. + + You must give prominent notice with each copy of the work that the +Library is used in it and that the Library and its use are covered by +this License. You must supply a copy of this License. If the work +during execution displays copyright notices, you must include the +copyright notice for the Library among them, as well as a reference +directing the user to the copy of this License. Also, you must do one +of these things: + + a) Accompany the work with the complete corresponding + machine-readable source code for the Library including whatever + changes were used in the work (which must be distributed under + Sections 1 and 2 above); and, if the work is an executable linked + with the Library, with the complete machine-readable "work that + uses the Library", as object code and/or source code, so that the + user can modify the Library and then relink to produce a modified + executable containing the modified Library. (It is understood + that the user who changes the contents of definitions files in the + Library will not necessarily be able to recompile the application + to use the modified definitions.) + + b) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (1) uses at run time a + copy of the library already present on the user's computer system, + rather than copying library functions into the executable, and (2) + will operate properly with a modified version of the library, if + the user installs one, as long as the modified version is + interface-compatible with the version that the work was made with. + + c) Accompany the work with a written offer, valid for at + least three years, to give the same user the materials + specified in Subsection 6a, above, for a charge no more + than the cost of performing this distribution. + + d) If distribution of the work is made by offering access to copy + from a designated place, offer equivalent access to copy the above + specified materials from the same place. + + e) Verify that the user has already received a copy of these + materials or that you have already sent this user a copy. + + For an executable, the required form of the "work that uses the +Library" must include any data and utility programs needed for +reproducing the executable from it. However, as a special exception, +the materials to be distributed need not include anything that is +normally distributed (in either source or binary form) with the major +components (compiler, kernel, and so on) of the operating system on +which the executable runs, unless that component itself accompanies +the executable. + + It may happen that this requirement contradicts the license +restrictions of other proprietary libraries that do not normally +accompany the operating system. Such a contradiction means you cannot +use both them and the Library together in an executable that you +distribute. + + 7. You may place library facilities that are a work based on the +Library side-by-side in a single library together with other library +facilities not covered by this License, and distribute such a combined +library, provided that the separate distribution of the work based on +the Library and of the other library facilities is otherwise +permitted, and provided that you do these two things: + + a) Accompany the combined library with a copy of the same work + based on the Library, uncombined with any other library + facilities. This must be distributed under the terms of the + Sections above. + + b) Give prominent notice with the combined library of the fact + that part of it is a work based on the Library, and explaining + where to find the accompanying uncombined form of the same work. + + 8. You may not copy, modify, sublicense, link with, or distribute +the Library except as expressly provided under this License. Any +attempt otherwise to copy, modify, sublicense, link with, or +distribute the Library is void, and will automatically terminate your +rights under this License. However, parties who have received copies, +or rights, from you under this License will not have their licenses +terminated so long as such parties remain in full compliance. + + 9. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Library or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Library (or any work based on the +Library), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Library or works based on it. + + 10. Each time you redistribute the Library (or any work based on the +Library), the recipient automatically receives a license from the +original licensor to copy, distribute, link with or modify the Library +subject to these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties with +this License. + + 11. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +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 +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Library at all. For example, if a patent +license would not permit royalty-free redistribution of the Library by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Library. + +If any portion of this section is held invalid or unenforceable under any +particular circumstance, the balance of the section is intended to apply, +and the section as a whole is intended to apply in other circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 12. If the distribution and/or use of the Library is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Library under this License may add +an explicit geographical distribution limitation excluding those countries, +so that distribution is permitted only in or among countries not thus +excluded. In such case, this License incorporates the limitation as if +written in the body of this License. + + 13. The Free Software Foundation may publish revised and/or new +versions of the Lesser 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 Library +specifies a version number of this License which applies to it and +"any later version", you have the option of following the terms and +conditions either of that version or of any later version published by +the Free Software Foundation. If the Library does not specify a +license version number, you may choose any version ever published by +the Free Software Foundation. + + 14. If you wish to incorporate parts of the Library into other free +programs whose distribution conditions are incompatible with these, +write to the author to ask for permission. For software which is +copyrighted by the Free Software Foundation, write to the Free +Software Foundation; we sometimes make exceptions for this. Our +decision will be guided by the two goals of preserving the free status +of all derivatives of our free software and of promoting the sharing +and reuse of software generally. + + NO WARRANTY + + 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO +WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. +EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR +OTHER PARTIES PROVIDE THE LIBRARY "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 +LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME +THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN +WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY +AND/OR REDISTRIBUTE THE LIBRARY 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 +LIBRARY (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 LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF +SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH +DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Libraries + + If you develop a new library, and you want it to be of the greatest +possible use to the public, we recommend making it free software that +everyone can redistribute and change. You can do so by permitting +redistribution under these terms (or, alternatively, under the terms of the +ordinary General Public License). + + To apply these terms, attach the following notices to the library. It is +safest to attach them to the start of each source file to most effectively +convey 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 library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + USA + +Also add information on how to contact you by electronic and paper mail. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the library, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the + library `Frob' (a library for tweaking knobs) written by James Random + Hacker. + + , 1 April 1990 + Ty Coon, President of Vice + +That's all there is to it! diff --git a/MAINTAINERS b/MAINTAINERS new file mode 100644 index 0000000..026b705 --- /dev/null +++ b/MAINTAINERS @@ -0,0 +1 @@ +Richard Hughes diff --git a/NEWS b/NEWS new file mode 100644 index 0000000..222f02b --- /dev/null +++ b/NEWS @@ -0,0 +1,195 @@ +Version 0.1.15 +~~~~~~~~~~~~~~ +Released: 2020-xx-xx + +New Features: + - Add xb_builder_source_add_simple_adapter (Daniel Campello) + - Allow reversing the query results (Richard Hughes) + +Version 0.1.14 +~~~~~~~~~~~~~~ +Released: 2019-12-03 + +New Features: + - Add MinGW Windows 64 builds to CI (Richard Hughes) + +Bugfixes: + - Allow compiling without gio-unix (Richard Hughes) + - Do not use libuuid (Richard Hughes) + - Do not unconditionally use -fstack-protector-strong (Richard Hughes) + - Ignore adaptors added with xb_builder_source_add_converter() (Richard Hughes) + +Version 0.1.13 +~~~~~~~~~~~~~~ +Released: 2019-10-17 + +New Features: + - Export xb_silo_query_full() (Richard Hughes) + +Bugfixes: + - Show the XPath that was used in the query in the error message (Richard Hughes) + +Version 0.1.12 +~~~~~~~~~~~~~~ +Released: 2019-09-27 + +New Features: + - Add helper functions to get the first and last child XbBuilerNode (Richard Hughes) + - Add xb_node_transmogrify to allow changing XML format (Richard Hughes) + - Support 'tail' XML data in the stored silo (Richard Hughes) + +Bugfixes: + - Do not escape a single quote with ' (Richard Hughes) + - Don't invalidate the silo for a GIO temp file (Richard Hughes) + - Fix up two memory leaks if using libxmlb from an introspected binding (Richard Hughes) + +Version 0.1.11 +~~~~~~~~~~~~~~ +Released: 2019-07-15 + +New Features: + - Add xb_node_query_first_full() convenience helper (Richard Hughes) + - Rebuild the XbMachine parser to support 'and' and 'or' predicates (Richard Hughes) + - Verify we never break the ABI (Mathieu Bridon) + +Bugfixes: + - Conditionalize installation of test data (Mathieu Bridon) + - Generate a GUID of zero for no silo data (Richard Hughes) + - Run CI jobs in parallel (Mathieu Bridon) + +Version 0.1.10 +~~~~~~~~~~~~~~ +Released: 2019-05-16 + +Bugfixes: + - Do not mistake gzipped files as being application/x-zerosize content type (Richard Hughes) + - Fix running the installed tests with no checkout directory (Richard Hughes) + +Version 0.1.9 +~~~~~~~~~~~~~ +Released: 2019-05-07 + +Bugfixes: + - Correctly implement building a silo with _SINGLE_LANG set (Richard Hughes) + +Version 0.1.8 +~~~~~~~~~~~~~ +Released: 2019-03-26 + +New Features: + - Add some installed tests (Richard Hughes) + +Bugfixes: + - Always add all children when importing parent-less XML data (Richard Hughes) + +Version 0.1.7 +~~~~~~~~~~~~~ +Released: 2019-03-08 + +New Features: + - Add XB_BUILDER_COMPILE_FLAG_IGNORE_GUID (Richard Hughes) + - Allow nesting XbBuilderSource content type handlers (Richard Hughes) + +Bugfixes: + - Correct return type for xb_builder_compile() (Mario Limonciello) + - Increase the amount of time for the inotify event to happen (Richard Hughes) + - Only run the speed tests when using '-m perf' (Richard Hughes) + +Version 0.1.6 +~~~~~~~~~~~~~ +Released: 2018-12-30 + +New Features: + - Allow controlling how the XbQuery is parsed (Richard Hughes) + +Version 0.1.5 +~~~~~~~~~~~~~ +Released: 2018-11-21 + +New Features: + - Add xb_builder_node_export() (Richard Hughes) + +Bugfixes: + - Do not start a GIO session bus when using xb-tool (Richard Hughes) + - Ignore calls to xb_silo_query_build_index() with no results (Richard Hughes) + - Lazy load the stemmer when required (Richard Hughes) + +Version 0.1.4 +~~~~~~~~~~~~~ +Released: 2018-11-08 + +New Features: + - Add support for bound variables (Richard Hughes) + - Add support for indexed strings (Richard Hughes) + - Optionally optimize predicates (Richard Hughes) + - Split out the XPath query logic into XbQuery (Richard Hughes) + - Support XPath method to do a Porter stem operation on text (Richard Hughes) + +Bugfixes: + - Don't hardcode the path of python3 (Ting-Wei Lan) + - Only attempt to pass TEXT opcodes for upper-case() (Richard Hughes) + - Revert the optional text space saving feature (Richard Hughes) + - Use INTE:INTE for comparison where available (Richard Hughes) + +Version 0.1.3 +~~~~~~~~~~~~~ +Released: 2018-10-22 + +New Features: + - Add XbBuilderFixup (Richard Hughes) + - Add xb_builder_node_get_attr_as_uint() (Richard Hughes) + - Add xb_builder_node_get_text_as_uint() (Richard Hughes) + - Add xb_builder_node_sort_children() (Richard Hughes) + +Bugfixes: + - Allow being used as a meson subproject (Jan Tojnar) + - Fix compiling on Ubuntu Xenial (Richard Hughes) + - Switch from GPtrArray to XbStack for performance reasons (Richard Hughes) + - Update meson relro checks (Jan Tojnar) + +Version 0.1.2 +~~~~~~~~~~~~~ +Released: 2018-10-16 + +New Features: + - Add support for XPath string-length() (Richard Hughes) + - Add xb_builder_source_load_bytes() (Richard Hughes) + - Add xb_silo_export_file() (Richard Hughes) + - Add xb_string_append_union() (Richard Hughes) + - Add xb_string_escape() (Richard Hughes) + +Bugfixes: + - Allow the use as meson subproject (Jan Tojnar) + - Do not query with the first child (Richard Hughes) + - Fix a crash when using xb_builder_node_set_text() in a fixup (Richard Hughes) + - Only run the XbBuilderSourceConverterFunc if the silo needs rebuilding (Richard Hughes) + - Return an error when the XPath predicate has invalid syntax (Richard Hughes) + +Version 0.1.1 +~~~~~~~~~~~~~ +Released: 2018-10-11 + +New Features: + - Add support for XPath starts-with() and ends-with() (Richard Hughes) + - Add support for number() and text() type conversion (Richard Hughes) + - Add support for predicate not() (Richard Hughes) + - Add xb_silo_invalidate() (Richard Hughes) + - Export XbMachine and XbOpcode (Richard Hughes) + +Bugfixes: + - Add the XbBuilderSource info keys to the generated GUID (Richard Hughes) + - Add the XbBuilderSourceNodeFunc ID to the generated GUID (Richard Hughes) + - Add the source prefix to the generated GUID (Richard Hughes) + - Do not use GNode when compiling the blob (Richard Hughes) + - Ignore invalid arguments if OR queries have other sections to parse (Richard Hughes) + - Load back the saved new silo to get a mmapped version (Richard Hughes) + - Support XPath wildcard nodes with predicates (Richard Hughes) + - Support some relative parent queries (Richard Hughes) + - Use the time::changed-usec if set in the GUID (Richard Hughes) + +Version 0.1.0 +~~~~~~~~~~~~~ +Released: 2018-10-05 + +Notes: + - A library for querying compressed XML metadata. diff --git a/README.md b/README.md new file mode 100644 index 0000000..a2d42c1 --- /dev/null +++ b/README.md @@ -0,0 +1,119 @@ +libxmlb +======= + +Introduction +------------ + +XML is slow to parse and strings inside the document cannot be memory mapped as +they do not have a trailing NUL char. The libxmlb library takes XML source, and +converts it to a structured binary representation with a deduplicated string +table -- where the strings have the NULs included. + +This allows an application to mmap the binary XML file, do an XPath query and +return some strings without actually parsing the entire document. This is all +done using (almost) zero allocations and no actual copying of the binary data. + +As each node in the binary XML file encodes the 'next' node at the same level +it makes skipping whole subtrees trivial. A 10Mb binary XML file can be loaded +from disk **and** queried in less than a few milliseconds. + +The binary XML is not supposed to be small. It's usually about half the size of +the text XML data where a lot of the tag content is duplicated, but can actually +be larger than the original XML file. This isn't important; the fast query speed +and the ability to mmap strings without copies more than makes up for the larger +on-disk size. If you want to compress your XML, this library probably isn't for +you -- just use gzip -- its gives you an almost a perfect compression ratio for +data like this. + +For example: + + $ xb-tool compile fedora.xmlb fedora.xml.gz + + $ du -h fedora.xml* + 12M fedora.xmlb + 3.6M fedora.xml.gz + + $ xb-tool query fedora.xmlb "components/component[@type=desktop]/id[text()=firefox.desktop]" + RESULT: firefox.desktop + real 0m0.011s + user 0m0.010s + sys 0m0.001s + +XPath +===== + +This library only implements a tiny subset of XPath. See the examples for the +full list, but it's basically restricted to element_name, attributes and text. + +We will use the following XML document in the examples below. + + + + + Harry Potter + 29.99 + + + Learning XML + 39.95 + + + +Selecting Nodes +--------------- + +XPath uses path expressions to select nodes in an XML document. The only thing +that libxmlb can return are nodes. + +| Example | Description | Supported | +| --- | --- | --- | +| `/bookstore` | Returns the root bookstore element | ✔ | +| `/bookstore/book` | Returns all `book` elements | ✔ | +| `//book` | Returns books no matter where they are | ✖ | +| `bookstore//book` | Returns books that are descendant of `bookstore` | ✖ | +| `@lang` | Returns attributes that are named `lang` | ✖ | +| `/bookstore/.` | Returns the `bookstore` node | ✖ | +| `/bookstore/book/*` | Returns all `title` and `price` nodes of each `book` node | ✔ | +| `/bookstore/book/child::*` | Returns all `title` and `price` nodes of each `book` node | ✔ | +| `/bookstore/book/title/..` | Returns the `book` nodes with a title | ✔ | +| `/bookstore/book/parent::*` | Returns `bookstore`, the parent of `book` | ✔ | +| `/bookstore/book/parent::bookstore` | Returns the parent `bookstore` of `book` | ✖ | + +Predicates +---------- + +Predicates are used to find a specific node or a node that contains a specific +value. Predicates are always embedded in square brackets. + +| Example | Description | Supported | +| --- | --- | --- | +| `/bookstore/book[1]` | Returns the first book element | ✔ | +| `/bookstore/book[first()]` | Returns the first book element | ✔ | +| `/bookstore/book[last()]` | Returns the last book element | ✔ | +| `/bookstore/book[last()-1]` | Returns the last but one book element | ✖ | +| `/bookstore/book[position()<3]` | Returns the first two books | ✔ | +| `/bookstore/book[upper-case(text())=='HARRY POTTER']` | Returns the first book | ✔ | +| `/bookstore/book[@percentage>=90]` | Returns the book with `>=` 90% completion | ✔ | +| `/bookstore/book/title[@lang]` | Returns titles with an attribute named `lang` | ✔ | +| `/bookstore/book/title[@lang='en']` | Returns titles that have a `lang`equal `en` | ✔ | +| `/bookstore/book/title[@lang!='en']` | Returns titles that have a `lang` not equal `en` | ✔ | +| `/bookstore/book/title[@lang<='zz_ZZ']` | Returns titles that `lang` <= `zz_ZZ` | ✔ | +| `/bookstore/book[price>35.00]` | Returns the books with a price greater than 35 | ✖ | +| `/bookstore/book[price>35.00]/title` | Returns the titles that have a price greater than 35 | ✖ | +| `/bookstore/book/title[text()='Learning XML']` | Returns the book node with matching content | ✔ | + +Compilation +---------- + +libxmlb is a standard meson project. It can be compiled using the following basic steps: + +``` +# meson build +# ninja -C build +# ninja -C build install +# ldconfig +``` + +This will by default install the library into `/usr/local`. On some Linux distributions you may +need to configure the linker path in `/etc/ld.so.conf` to be able to locate it. +The call to `ldconfig` is needed to refresh the linker cache. diff --git a/RELEASE b/RELEASE new file mode 100644 index 0000000..fb6365d --- /dev/null +++ b/RELEASE @@ -0,0 +1,40 @@ +libxmlb Release Notes + +1. Write NEWS entries for libxmlb in the same format as usual. + +git shortlog 0.1.14.. | grep -i -v trivial | grep -v Merge > NEWS.new + +Version 0.1.15 +~~~~~~~~~~~~~~ +Released: 2020-xx-xx + +New Features: +Bugfixes: + + +Commit changes to git: + +# MAKE SURE THESE ARE CORRECT +export release_ver="0.1.15" + +git commit -a -m "Release libxmlb ${release_ver}" +git tag -s -f -m "Release libxmlb ${release_ver}" "${release_ver}" +git push --tags +git push + +Generate the tarball: + +ninja dist + +Generate the additional verification metadata + +gpg -b -a meson-dist/libxmlb-${release_ver}.tar.xz + +Upload tarball: + +scp meson-dist/libxmlb-${release_ver}.tar.* hughsient@people.freedesktop.org:~/public_html/releases + +Do post release version bump in meson.build + +git commit -a -m "trivial: post release version bump" +git push diff --git a/contrib/ci/Dockerfile-debian b/contrib/ci/Dockerfile-debian new file mode 100644 index 0000000..5a32140 --- /dev/null +++ b/contrib/ci/Dockerfile-debian @@ -0,0 +1,23 @@ +FROM debian:unstable + +RUN apt-get update -qq +RUN apt-get install -yq --no-install-recommends \ + gcovr \ + gobject-introspection \ + gtk-doc-tools \ + libgirepository1.0-dev \ + libglib2.0-bin \ + libglib2.0-dev \ + libstemmer-dev \ + ninja-build \ + python3-pip \ + python3-setuptools \ + python3-wheel \ + shared-mime-info \ + uuid-dev \ + pkg-config + +# Meson is too old in unstable, and that won't change until Buster is released +RUN pip3 install meson + +WORKDIR /build diff --git a/contrib/ci/Dockerfile-fedora b/contrib/ci/Dockerfile-fedora new file mode 100644 index 0000000..2fcc5fa --- /dev/null +++ b/contrib/ci/Dockerfile-fedora @@ -0,0 +1,18 @@ +FROM fedora:31 + +RUN dnf -y update +RUN dnf -y install \ + diffutils \ + gcovr \ + git-core \ + glib2-devel \ + gobject-introspection-devel \ + gtk-doc \ + libabigail \ + libstemmer-devel \ + libuuid-devel \ + meson \ + shared-mime-info \ + redhat-rpm-config + +WORKDIR /build diff --git a/contrib/ci/Dockerfile-fedora-w64 b/contrib/ci/Dockerfile-fedora-w64 new file mode 100644 index 0000000..6ef4e82 --- /dev/null +++ b/contrib/ci/Dockerfile-fedora-w64 @@ -0,0 +1,16 @@ +FROM fedora:31 + +RUN dnf -y update +RUN dnf -y install \ + diffutils \ + gcc \ + git-core \ + meson \ + mingw64-gcc \ + mingw64-glib2 \ + mingw64-pkg-config \ + redhat-rpm-config \ + shared-mime-info \ + wine-core + +WORKDIR /build diff --git a/contrib/ci/abidiff.suppr b/contrib/ci/abidiff.suppr new file mode 100644 index 0000000..5c706c7 --- /dev/null +++ b/contrib/ci/abidiff.suppr @@ -0,0 +1,3 @@ +[suppress_type] + type_kind = enum + changed_enumerators = XB_BUILDER_NODE_FLAG_LAST,XB_BUILDER_SOURCE_FLAG_LAST,XB_BUILDER_COMPILE_FLAG_LAST,XB_MACHINE_DEBUG_FLAG_LAST,XB_MACHINE_PARSE_FLAG_LAST,XB_NODE_EXPORT_FLAG_LAST,XB_OPCODE_FLAG_LAST,XB_OPCODE_KIND_LAST,XB_SILO_QUERY_KIND_LAST,XB_QUERY_FLAG_LAST,XB_SILO_LOAD_FLAG_LAST,XB_SILO_PROFILE_FLAG_LAST diff --git a/contrib/ci/build-debian.sh b/contrib/ci/build-debian.sh new file mode 100755 index 0000000..897e4ef --- /dev/null +++ b/contrib/ci/build-debian.sh @@ -0,0 +1,12 @@ +#!/bin/sh +set -e +export LC_ALL=C.UTF-8 +mkdir -p build && cd build +rm * -rf +meson .. \ + -Dgtkdoc=true \ + -Dtests=true $@ +ninja -v || bash +ninja test -v +DESTDIR=/tmp/install-ninja ninja install +cd .. diff --git a/contrib/ci/build-fedora-w64.sh b/contrib/ci/build-fedora-w64.sh new file mode 100755 index 0000000..a4e11cf --- /dev/null +++ b/contrib/ci/build-fedora-w64.sh @@ -0,0 +1,15 @@ +#!/bin/sh +set -e +export LC_ALL=C.UTF-8 +mkdir -p build && cd build +rm * -rf +meson .. \ + --cross-file=../contrib/mingw64.cross \ + -Dintrospection=false \ + -Dgtkdoc=false \ + -Dtests=true $@ +ninja -v || bash +wine reg add "HKEY_CURRENT_USER\Environment" /v PATH /d /usr/x86_64-w64-mingw32/sys-root/mingw/bin +ninja test -v +DESTDIR=/tmp/install-ninja ninja install +cd .. diff --git a/contrib/ci/build-fedora.sh b/contrib/ci/build-fedora.sh new file mode 100755 index 0000000..1b24329 --- /dev/null +++ b/contrib/ci/build-fedora.sh @@ -0,0 +1,15 @@ +#!/bin/sh +set -e +export LC_ALL=C.UTF-8 +mkdir -p build && cd build +rm * -rf +meson .. \ + -Db_coverage=true \ + -Dgtkdoc=true \ + -Dtests=true $@ +ninja -v || bash +ninja test -v +ninja coverage-text +cat meson-logs/coverage.txt +DESTDIR=/tmp/install-ninja ninja install +cd .. diff --git a/contrib/ci/check-abi b/contrib/ci/check-abi new file mode 100755 index 0000000..ad0e4b6 --- /dev/null +++ b/contrib/ci/check-abi @@ -0,0 +1,114 @@ +#!/usr/bin/python3 + + +import argparse +import contextlib +import os +import shutil +import subprocess +import sys + + +def format_title(title): + box = { + 'tl': '╔', 'tr': '╗', 'bl': '╚', 'br': '╝', 'h': '═', 'v': '║', + } + hline = box['h'] * (len(title) + 2) + + return '\n'.join([ + f"{box['tl']}{hline}{box['tr']}", + f"{box['v']} {title} {box['v']}", + f"{box['bl']}{hline}{box['br']}", + ]) + + +def rm_rf(path): + try: + shutil.rmtree(path) + except FileNotFoundError: + pass + + +def sanitize_path(name): + return name.replace('/', '-') + + +def get_current_revision(): + revision = subprocess.check_output(['git', 'rev-parse', '--abbrev-ref', 'HEAD'], + encoding='utf-8').strip() + + if revision == 'HEAD': + # This is a detached HEAD, get the commit hash + revision = subprocess.check_output(['git', 'rev-parse', 'HEAD']).strip().decode('utf-8') + + return revision + + +@contextlib.contextmanager +def checkout_git_revision(revision): + current_revision = get_current_revision() + subprocess.check_call(['git', 'checkout', '-q', revision]) + + try: + yield + finally: + subprocess.check_call(['git', 'checkout', '-q', current_revision]) + + +def build_install(revision): + build_dir = '_build' + dest_dir = os.path.abspath(sanitize_path(revision)) + print(format_title(f'# Building and installing {revision} in {dest_dir}'), + end='\n\n', flush=True) + + with checkout_git_revision(revision): + rm_rf(build_dir) + rm_rf(revision) + + subprocess.check_call(['meson', build_dir, + '--prefix=/usr', '--libdir=lib', + '-Db_coverage=false', '-Dgtkdoc=false', '-Dtests=false']) + subprocess.check_call(['ninja', '-v', '-C', build_dir]) + subprocess.check_call(['ninja', '-v', '-C', build_dir, 'install'], + env={'DESTDIR': dest_dir}) + + return dest_dir + + +def compare(old_tree, new_tree): + print(format_title(f'# Comparing the two ABIs'), end='\n\n', flush=True) + + old_headers = os.path.join(old_tree, 'usr', 'include') + old_lib = os.path.join(old_tree, 'usr', 'lib', 'libxmlb.so') + + new_headers = os.path.join(new_tree, 'usr', 'include') + new_lib = os.path.join(new_tree, 'usr', 'lib', 'libxmlb.so') + + subprocess.check_call([ + 'abidiff', '--headers-dir1', old_headers, '--headers-dir2', new_headers, + '--drop-private-types', '--suppressions', 'contrib/ci/abidiff.suppr', + '--fail-no-debug-info', '--no-added-syms', old_lib, new_lib]) + + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + + parser.add_argument('old', help='the previous revision, considered the reference') + parser.add_argument('new', help='the new revision, to compare to the reference') + + args = parser.parse_args() + + if args.old == args.new: + print("Let's not waste time comparing something to itself") + sys.exit(0) + + old_tree = build_install(args.old) + new_tree = build_install(args.new) + + try: + compare(old_tree, new_tree) + + except Exception: + sys.exit(1) + + print(f'Hurray! {args.old} and {args.new} are ABI-compatible!') diff --git a/contrib/libxmlb.spec.in b/contrib/libxmlb.spec.in new file mode 100644 index 0000000..248e4a0 --- /dev/null +++ b/contrib/libxmlb.spec.in @@ -0,0 +1,101 @@ +%global glib2_version 2.45.8 + +%define alphatag #ALPHATAG# + +Summary: Library for querying compressed XML metadata +Name: libxmlb +Version: #VERSION# +Release: 0.#BUILD#%{?alphatag}%{?dist} +License: LGPLv2+ +URL: https://github.com/hughsie/libxmlb +Source0: http://people.freedesktop.org/~hughsient/releases/%{name}-%{version}.tar.xz + +BuildRequires: glib2-devel >= %{glib2_version} +BuildRequires: gtk-doc +BuildRequires: libstemmer-devel +BuildRequires: meson +BuildRequires: gobject-introspection-devel +%if 0%{?rhel} == 7 +BuildRequires: python36-setuptools +%else +BuildRequires: python-setuptools +%endif + +# needed for the self tests +BuildRequires: shared-mime-info + +%if 0%{?rhel} < 8 +BuildRequires: python34 +%endif + +Requires: glib2%{?_isa} >= %{glib2_version} +Requires: shared-mime-info + +%description +XML is slow to parse and strings inside the document cannot be memory mapped as +they do not have a trailing NUL char. The libxmlb library takes XML source, and +converts it to a structured binary representation with a deduplicated string +table -- where the strings have the NULs included. + +This allows an application to mmap the binary XML file, do an XPath query and +return some strings without actually parsing the entire document. This is all +done using (almost) zero allocations and no actual copying of the binary data. + +%package devel +Summary: Development package for %{name} +Requires: %{name}%{?_isa} = %{version}-%{release} + +%description devel +Files for development with %{name}. + +%package tests +Summary: Files for installed tests + +%description tests +Executable and data files for installed tests. + +%prep +%setup -q + +%build + +%meson \ + -Dgtkdoc=true \ + -Dtests=true + +%meson_build + +%check +%meson_test + +%install +%meson_install + +%files +%doc README.md +%license LICENSE +%{_libexecdir}/xb-tool +%dir %{_libdir}/girepository-1.0 +%{_libdir}/girepository-1.0/*.typelib +%{_libdir}/libxmlb.so.1* + +%files devel +%dir %{_datadir}/gir-1.0 +%{_datadir}/gir-1.0/*.gir +%dir %{_datadir}/gtk-doc +%dir %{_datadir}/gtk-doc/html +%{_datadir}/gtk-doc/html/libxmlb +%{_includedir}/libxmlb-1 +%{_libdir}/libxmlb.so +%{_libdir}/pkgconfig/xmlb.pc + +%files tests +%{_libexecdir}/installed-tests/libxmlb/xb-self-test +%{_datadir}/installed-tests/libxmlb/libxmlb.test +%{_datadir}/installed-tests/libxmlb/test.xml.gz.gz.gz +%dir %{_datadir}/installed-tests/libxmlb + +%changelog +* #LONGDATE# Richard Hughes #VERSION#-0.#BUILD##ALPHATAG# +- Update from git + diff --git a/contrib/mingw64.cross b/contrib/mingw64.cross new file mode 100644 index 0000000..6cb560e --- /dev/null +++ b/contrib/mingw64.cross @@ -0,0 +1,13 @@ +[binaries] +c = '/usr/bin/x86_64-w64-mingw32-gcc' +cpp = '/usr/bin/x86_64-w64-mingw32-g++' +ar = '/usr/bin/x86_64-w64-mingw32-ar' +strip = '/usr/bin/x86_64-w64-mingw32-strip' +pkgconfig = '/usr/bin/x86_64-w64-mingw32-pkg-config' +exe_wrapper = 'wine' + +[host_machine] +system = 'windows' +cpu_family = 'x86_64' +cpu = 'i686' +endian = 'little' diff --git a/data/fuzzing-src/appdata.xml b/data/fuzzing-src/appdata.xml new file mode 100644 index 0000000..d779466 --- /dev/null +++ b/data/fuzzing-src/appdata.xml @@ -0,0 +1,30 @@ + + + + test.firmware + ColorHug2 + Firmware +

New features!

+ + 2082b5e0 + + + fwupd + + + http://com/ + CC0-1.0 + GPL-2.0+ + richard + Hughski + + + +

stable:

+
    +
  • Quicker
  • +
+
+
+
+
diff --git a/data/fuzzing-xpath/attr.txt b/data/fuzzing-xpath/attr.txt new file mode 100644 index 0000000..1366b27 --- /dev/null +++ b/data/fuzzing-xpath/attr.txt @@ -0,0 +1 @@ +component[@type='firmware']/id diff --git a/data/fuzzing-xpath/down-then-up-then-down.txt b/data/fuzzing-xpath/down-then-up-then-down.txt new file mode 100644 index 0000000..c9fd106 --- /dev/null +++ b/data/fuzzing-xpath/down-then-up-then-down.txt @@ -0,0 +1 @@ +component/url[@type='homepage']/../id diff --git a/data/fuzzing-xpath/or.txt b/data/fuzzing-xpath/or.txt new file mode 100644 index 0000000..aee2f96 --- /dev/null +++ b/data/fuzzing-xpath/or.txt @@ -0,0 +1 @@ +component/id[text()='foo']|component/id[text()='test.firmware'] diff --git a/data/fuzzing-xpath/search.txt b/data/fuzzing-xpath/search.txt new file mode 100644 index 0000000..7ad87d5 --- /dev/null +++ b/data/fuzzing-xpath/search.txt @@ -0,0 +1 @@ +component/id[text()~='firm'] diff --git a/data/fuzzing-xpath/simple.txt b/data/fuzzing-xpath/simple.txt new file mode 100644 index 0000000..99e1b95 --- /dev/null +++ b/data/fuzzing-xpath/simple.txt @@ -0,0 +1 @@ +/component/id diff --git a/data/fuzzing-xpath/text.txt b/data/fuzzing-xpath/text.txt new file mode 100644 index 0000000..1c973b2 --- /dev/null +++ b/data/fuzzing-xpath/text.txt @@ -0,0 +1 @@ +component/id[text()='test.firmware'] diff --git a/data/libxmlb.test.in b/data/libxmlb.test.in new file mode 100644 index 0000000..bdb4ec5 --- /dev/null +++ b/data/libxmlb.test.in @@ -0,0 +1,3 @@ +[Test] +Type=session +Exec=@installed_test_bindir@/xb-self-test diff --git a/data/meson.build b/data/meson.build new file mode 100644 index 0000000..5db35bf --- /dev/null +++ b/data/meson.build @@ -0,0 +1,13 @@ +if get_option('tests') and host_machine.system() != 'windows' + configure_file( + input : 'libxmlb.test.in', + output : 'libxmlb.test', + configuration : conf, + install: true, + install_dir: installed_test_datadir, + ) + + install_data(['test.xml.gz.gz.gz'], + install_dir: installed_test_datadir, + ) +endif diff --git a/data/test.xml.gz.gz.gz b/data/test.xml.gz.gz.gz new file mode 100644 index 0000000..7e8092c Binary files /dev/null and b/data/test.xml.gz.gz.gz differ diff --git a/docs/fuzzing.md b/docs/fuzzing.md new file mode 100644 index 0000000..e77a8c1 --- /dev/null +++ b/docs/fuzzing.md @@ -0,0 +1,18 @@ +Fuzzing +======= + + CC=afl-gcc meson --default-library=static ../ + AFL_HARDEN=1 ninja + +Breaking XMLb +------------- + + afl-fuzz -m 300 -i fuzzing-src -o findings ./src/xb-tool --force dump @@ + mkdir -p fuzzing-src + ./src/xb-tool compile fuzzing-src/appdata.xmlb ../data/fuzzing-src/appdata.xml + +Breaking XPath +-------------- + + ./src/xb-tool compile xpath.xmlb ../data/fuzzing-src/appdata.xml + afl-fuzz -m 300 -i ../data/fuzzing-xpath/ -o findings ./src/xb-tool query-file xpath.xmlb @@ diff --git a/docs/libxmlb-docs.xml b/docs/libxmlb-docs.xml new file mode 100644 index 0000000..5bac39d --- /dev/null +++ b/docs/libxmlb-docs.xml @@ -0,0 +1,55 @@ + + +]> + + + libxmlb Reference Manual + + + + About libxmlb + + + libxmlb is a library for querying binary XML. + + + + + + libxmlb + + + Functionality exported by libxmlb for client applications. + + + + + + + + + + + + + + + + + + + + + API Index + + + + + Index of deprecated API + + + + diff --git a/docs/libxmlb.types b/docs/libxmlb.types new file mode 100644 index 0000000..fb54fe6 --- /dev/null +++ b/docs/libxmlb.types @@ -0,0 +1,10 @@ +xb_silo_get_type +xb_machine_get_type +xb_node_get_type +xb_opcode_get_type +xb_stack_get_type +xb_builder_get_type +xb_builder_fixup_get_type +xb_builder_node_get_type +xb_builder_source_get_type +xb_query_get_type diff --git a/docs/meson.build b/docs/meson.build new file mode 100644 index 0000000..e0a70d0 --- /dev/null +++ b/docs/meson.build @@ -0,0 +1,9 @@ +gnome.gtkdoc( + 'libxmlb', + src_dir : [ + 'src', + join_paths(meson.current_build_dir(), '..', 'src'), + ], + main_xml : 'libxmlb-docs.xml', + install : true +) diff --git a/meson.build b/meson.build new file mode 100644 index 0000000..38486c9 --- /dev/null +++ b/meson.build @@ -0,0 +1,153 @@ +project('libxmlb', 'c', + version : '0.1.15', + license : 'LGPL-2.1+', + meson_version : '>=0.47.0', + default_options : ['warning_level=2', 'c_std=c99'], +) + +libxmlb_version = meson.project_version() +varr = libxmlb_version.split('.') +libxmlb_major_version = varr[0] +libxmlb_minor_version = varr[1] +libxmlb_micro_version = varr[2] + +conf = configuration_data() +conf.set('XMLB_MAJOR_VERSION', libxmlb_major_version) +conf.set('XMLB_MINOR_VERSION', libxmlb_minor_version) +conf.set('XMLB_MICRO_VERSION', libxmlb_micro_version) +conf.set_quoted('PACKAGE_VERSION', libxmlb_version) + +# libtool versioning - this applies to libxmlb +lt_current = '1' +lt_revision = '0' +lt_age = '0' +lt_version = '@0@.@1@.@2@'.format(lt_current, lt_age, lt_revision) + +configinc = include_directories('.') + +# get suported warning flags +warning_flags = [ + '-Waggregate-return', + '-Wunused', + '-Warray-bounds', + '-Wcast-align', + '-Wclobbered', + '-Wdeclaration-after-statement', + '-Wduplicated-branches', + '-Wduplicated-cond', + '-Wempty-body', + '-Wformat=2', + '-Wformat-nonliteral', + '-Wformat-security', + '-Wformat-signedness', + '-Wignored-qualifiers', + '-Wimplicit-function-declaration', + '-Wincompatible-pointer-types-discards-qualifiers', + '-Winit-self', + '-Wlogical-op', + '-Wmissing-declarations', + '-Wmissing-format-attribute', + '-Wmissing-include-dirs', + '-Wmissing-noreturn', + '-Wmissing-parameter-type', + '-Wmissing-prototypes', + '-Wnested-externs', + '-Wno-cast-function-type', + '-Wno-error=cpp', + '-Wno-unknown-pragmas', + '-Wno-discarded-qualifiers', + '-Wno-missing-field-initializers', + '-Wno-strict-aliasing', + '-Wno-suggest-attribute=format', + '-Wno-unused-parameter', + '-Wnull-dereference', + '-Wold-style-definition', + '-Woverride-init', + '-Wpointer-arith', + '-Wredundant-decls', + '-Wreturn-type', + '-Wshadow', + '-Wsign-compare', + '-Wstrict-aliasing', + '-Wstrict-prototypes', + '-Wswitch-default', + '-Wtype-limits', + '-Wundef', + '-Wuninitialized', + '-Wunused-but-set-variable', + '-Wunused-variable', + '-Wwrite-strings' +] +cc = meson.get_compiler('c') +add_project_arguments(cc.get_supported_arguments(warning_flags), language : 'c') + +if not meson.is_cross_build() + add_project_arguments('-fstack-protector-strong', language : 'c') +endif + +# enable full RELRO where possible +# FIXME: until https://github.com/mesonbuild/meson/issues/1140 is fixed +global_link_args = [] +test_link_args = [ + '-Wl,-z,relro', + '-Wl,-z,now', +] +foreach link_arg: test_link_args + if cc.has_link_argument(link_arg) + global_link_args += link_arg + endif +endforeach +add_project_link_arguments( + global_link_args, + language: 'c' +) + +if host_machine.system() == 'windows' + libexecdir = get_option('libexecdir') + installed_test_bindir = get_option('libexecdir') + installed_test_datadir = get_option('datadir') +else + prefix = get_option('prefix') + datadir = join_paths(prefix, get_option('datadir')) + libexecdir = join_paths(prefix, get_option('libexecdir')) + installed_test_bindir = join_paths(libexecdir, 'installed-tests', meson.project_name()) + installed_test_datadir = join_paths(datadir, 'installed-tests', meson.project_name()) +endif + +gio = dependency('gio-2.0', version : '>= 2.45.8') +giounix = dependency('gio-unix-2.0', version : '>= 2.45.8', required: false) +if giounix.found() + conf.set('HAVE_GIO_UNIX', '1') +endif + +libxmlb_deps = [ + gio, +] + +# support stemming of search tokens +if get_option('stemmer') + cc = meson.get_compiler('c') + stemmer = cc.find_library('stemmer') + libxmlb_deps += stemmer + conf.set('HAVE_LIBSTEMMER', 1) +endif + +gnome = import('gnome') + +conf.set('installed_test_bindir', installed_test_bindir) +conf.set_quoted('PACKAGE_NAME', meson.project_name()) +conf.set_quoted('VERSION', meson.project_version()) +configure_file( + output : 'config.h', + configuration : conf +) + +python = import('python') +python3 = python.find_installation('python3') + +subdir('data') +subdir('src') +if get_option('gtkdoc') + gtkdocscan = find_program('gtkdoc-scan', required : true) + subdir('docs') +endif diff --git a/meson_options.txt b/meson_options.txt new file mode 100644 index 0000000..27e8cb6 --- /dev/null +++ b/meson_options.txt @@ -0,0 +1,4 @@ +option('gtkdoc', type : 'boolean', value : true, description : 'enable developer documentation') +option('introspection', type : 'boolean', value : true, description : 'generate GObject Introspection data') +option('tests', type : 'boolean', value : true, description : 'enable tests') +option('stemmer', type : 'boolean', value : false, description : 'enable stemmer support') diff --git a/src/generate-version-script.py b/src/generate-version-script.py new file mode 100644 index 0000000..6983a7f --- /dev/null +++ b/src/generate-version-script.py @@ -0,0 +1,118 @@ +#!/usr/bin/python3 +# pylint: disable=invalid-name,missing-docstring +# +# Copyright (C) 2017 Richard Hughes +# +# SPDX-License-Identifier: LGPL-2.1+ + +import sys +import xml.etree.ElementTree as ET + +from pkg_resources import parse_version + +XMLNS = '{http://www.gtk.org/introspection/core/1.0}' +XMLNS_C = '{http://www.gtk.org/introspection/c/1.0}' + +def usage(return_code): + """ print usage and exit with the supplied return code """ + if return_code == 0: + out = sys.stdout + else: + out = sys.stderr + out.write("usage: %s \n" % sys.argv[0]) + sys.exit(return_code) + +class LdVersionScript: + """ Rasterize some text """ + + def __init__(self, library_name): + self.library_name = library_name + self.releases = {} + + def _add_node(self, node): + identifier = node.attrib[XMLNS_C + 'identifier'] + if 'version' not in node.attrib: + print('No version for', identifier) + sys.exit(1) + version = node.attrib['version'] + if version not in self.releases: + self.releases[version] = [] + release = self.releases[version] + if identifier not in release: + release.append(identifier) + return version + + def _add_cls(self, cls): + + # add all class functions + for node in cls.findall(XMLNS + 'function'): + self._add_node(node) + + # choose the lowest version method for the _get_type symbol + version_lowest = None + if '{http://www.gtk.org/introspection/glib/1.0}get-type' not in cls.attrib: + return + type_name = cls.attrib['{http://www.gtk.org/introspection/glib/1.0}get-type'] + + # add all class methods + for node in cls.findall(XMLNS + 'method'): + version_tmp = self._add_node(node) + if version_tmp: + if not version_lowest or parse_version(version_tmp) < parse_version(version_lowest): + version_lowest = version_tmp + + # add the constructor + for node in cls.findall(XMLNS + 'constructor'): + version_tmp = self._add_node(node) + if version_tmp: + if not version_lowest or parse_version(version_tmp) < parse_version(version_lowest): + version_lowest = version_tmp + + # finally add the get_type symbol + if version_lowest: + self.releases[version_lowest].append(type_name) + + def import_gir(self, filename): + tree = ET.parse(filename) + root = tree.getroot() + for ns in root.findall(XMLNS + 'namespace'): + for node in ns.findall(XMLNS + 'function'): + self._add_node(node) + for cls in ns.findall(XMLNS + 'record'): + self._add_cls(cls) + for cls in ns.findall(XMLNS + 'class'): + self._add_cls(cls) + + def render(self): + + # get a sorted list of all the versions + versions = [] + for version in self.releases: + versions.append(version) + + # output the version data to a file + verout = '# generated automatically, do not edit!\n' + oldversion = None + for version in sorted(versions, key=parse_version): + symbols = sorted(self.releases[version]) + verout += '\n%s_%s {\n' % (self.library_name, version) + verout += ' global:\n' + for symbol in symbols: + verout += ' %s;\n' % symbol + verout += ' local: *;\n' + if oldversion: + verout += '} %s_%s;\n' % (self.library_name, oldversion) + else: + verout += '};\n' + oldversion = version + return verout + +if __name__ == '__main__': + if {'-?', '--help', '--usage'}.intersection(set(sys.argv)): + usage(0) + if len(sys.argv) != 4: + usage(1) + + ld = LdVersionScript(library_name=sys.argv[1]) + ld.import_gir(sys.argv[2]) + open(sys.argv[3], 'w').write(ld.render()) diff --git a/src/libxmlb.map b/src/libxmlb.map new file mode 100644 index 0000000..dbb847e --- /dev/null +++ b/src/libxmlb.map @@ -0,0 +1,224 @@ +# generated automatically, do not edit! + +LIBXMLB_0.1.0 { + global: + xb_builder_add_locale; + xb_builder_append_guid; + xb_builder_compile; + xb_builder_ensure; + xb_builder_get_type; + xb_builder_import_node; + xb_builder_import_source; + xb_builder_new; + xb_builder_node_add_child; + xb_builder_node_add_flag; + xb_builder_node_get_attr; + xb_builder_node_get_children; + xb_builder_node_get_element; + xb_builder_node_get_text; + xb_builder_node_get_type; + xb_builder_node_has_flag; + xb_builder_node_insert; + xb_builder_node_insert_text; + xb_builder_node_new; + xb_builder_node_remove_attr; + xb_builder_node_set_attr; + xb_builder_node_set_element; + xb_builder_node_set_text; + xb_builder_source_add_node_func; + xb_builder_source_get_type; + xb_builder_source_set_info; + xb_builder_source_set_prefix; + xb_node_export; + xb_node_get_attr; + xb_node_get_attr_as_uint; + xb_node_get_child; + xb_node_get_children; + xb_node_get_data; + xb_node_get_depth; + xb_node_get_element; + xb_node_get_next; + xb_node_get_parent; + xb_node_get_root; + xb_node_get_text; + xb_node_get_text_as_uint; + xb_node_get_type; + xb_node_query; + xb_node_query_attr; + xb_node_query_attr_as_uint; + xb_node_query_export; + xb_node_query_first; + xb_node_query_text; + xb_node_query_text_as_uint; + xb_node_set_data; + xb_silo_export; + xb_silo_get_bytes; + xb_silo_get_guid; + xb_silo_get_root; + xb_silo_get_size; + xb_silo_get_type; + xb_silo_is_valid; + xb_silo_load_from_bytes; + xb_silo_load_from_file; + xb_silo_new; + xb_silo_new_from_xml; + xb_silo_query; + xb_silo_query_first; + xb_silo_save_to_file; + xb_silo_to_string; + xb_silo_watch_file; + local: *; +}; + +LIBXMLB_0.1.1 { + global: + xb_builder_node_depth; + xb_builder_node_get_child; + xb_builder_node_get_parent; + xb_builder_node_remove_child; + xb_builder_node_traverse; + xb_builder_node_unlink; + xb_builder_set_profile_flags; + xb_builder_source_add_converter; + xb_builder_source_load_file; + xb_builder_source_load_xml; + xb_builder_source_new; + xb_machine_add_method; + xb_machine_add_opcode_fixup; + xb_machine_add_operator; + xb_machine_add_text_handler; + xb_machine_get_type; + xb_machine_new; + xb_machine_opcode_func_new; + xb_machine_opcode_to_string; + xb_machine_opcodes_to_string; + xb_machine_parse; + xb_machine_run; + xb_machine_set_debug_flags; + xb_machine_stack_pop; + xb_machine_stack_push; + xb_machine_stack_push_integer; + xb_machine_stack_push_text; + xb_machine_stack_push_text_static; + xb_machine_stack_push_text_steal; + xb_opcode_cmp_str; + xb_opcode_cmp_val; + xb_opcode_func_new; + xb_opcode_get_kind; + xb_opcode_get_str; + xb_opcode_get_type; + xb_opcode_get_val; + xb_opcode_integer_new; + xb_opcode_kind_from_string; + xb_opcode_kind_to_string; + xb_opcode_ref; + xb_opcode_text_new; + xb_opcode_text_new_static; + xb_opcode_text_new_steal; + xb_opcode_unref; + xb_silo_get_profile_string; + xb_silo_invalidate; + xb_silo_set_profile_flags; + local: *; +} LIBXMLB_0.1.0; + +LIBXMLB_0.1.2 { + global: + xb_builder_source_load_bytes; + xb_silo_export_file; + xb_string_append_union; + xb_string_escape; + local: *; +} LIBXMLB_0.1.1; + +LIBXMLB_0.1.3 { + global: + xb_builder_add_fixup; + xb_builder_fixup_get_max_depth; + xb_builder_fixup_get_type; + xb_builder_fixup_new; + xb_builder_fixup_set_max_depth; + xb_builder_node_get_attr_as_uint; + xb_builder_node_get_text_as_uint; + xb_builder_node_sort_children; + xb_builder_source_add_fixup; + xb_machine_get_stack_size; + xb_machine_set_stack_size; + xb_stack_get_type; + xb_stack_pop; + xb_stack_push; + xb_stack_push_steal; + local: *; +} LIBXMLB_0.1.2; + +LIBXMLB_0.1.4 { + global: + xb_machine_parse_full; + xb_machine_stack_push_steal; + xb_node_query_full; + xb_opcode_to_string; + xb_query_bind_str; + xb_query_bind_val; + xb_query_get_limit; + xb_query_get_type; + xb_query_get_xpath; + xb_query_new; + xb_query_set_limit; + xb_silo_query_build_index; + xb_stack_to_string; + local: *; +} LIBXMLB_0.1.3; + +LIBXMLB_0.1.5 { + global: + xb_builder_node_export; + local: *; +} LIBXMLB_0.1.4; + +LIBXMLB_0.1.6 { + global: + xb_query_new_full; + local: *; +} LIBXMLB_0.1.5; + +LIBXMLB_0.1.7 { + global: + xb_builder_source_add_adapter; + xb_builder_source_ctx_get_bytes; + xb_builder_source_ctx_get_filename; + xb_builder_source_ctx_get_stream; + xb_builder_source_ctx_get_type; + local: *; +} LIBXMLB_0.1.6; + +LIBXMLB_0.1.11 { + global: + xb_node_query_first_full; + local: *; +} LIBXMLB_0.1.7; + +LIBXMLB_0.1.12 { + global: + xb_builder_node_get_first_child; + xb_builder_node_get_last_child; + xb_builder_node_get_tail; + xb_builder_node_set_tail; + xb_node_get_tail; + xb_node_transmogrify; + local: *; +} LIBXMLB_0.1.11; + +LIBXMLB_0.1.13 { + global: + xb_silo_query_first_full; + xb_silo_query_full; + local: *; +} LIBXMLB_0.1.12; + +LIBXMLB_0.1.15 { + global: + xb_builder_source_add_simple_adapter; + xb_query_get_flags; + xb_query_set_flags; + local: *; +} LIBXMLB_0.1.13; diff --git a/src/libxmlb/meson.build b/src/libxmlb/meson.build new file mode 100644 index 0000000..367a4d1 --- /dev/null +++ b/src/libxmlb/meson.build @@ -0,0 +1,10 @@ +# FIXME: We need to copy the headers to ${build_root}/libxmlb +# in order for including the headers from parent projects to work +# until https://github.com/mesonbuild/meson/issues/2546 is fixed +foreach header : xb_headers + configure_file( + copy : true, + input : header, + output : '@PLAINNAME@', + ) +endforeach diff --git a/src/meson.build b/src/meson.build new file mode 100644 index 0000000..936cc1d --- /dev/null +++ b/src/meson.build @@ -0,0 +1,223 @@ +libxmlb_version_h = configure_file( + input : 'xb-version.h.in', + output : 'xb-version.h', + configuration : conf +) + +install_headers( + 'xmlb.h', + subdir : 'libxmlb-1', +) + +xb_headers = files( + 'xb-builder.h', + 'xb-builder-fixup.h', + 'xb-builder-node.h', + 'xb-builder-source.h', + 'xb-builder-source-ctx.h', + 'xb-machine.h', + 'xb-node.h', + 'xb-node-query.h', + 'xb-opcode.h', + 'xb-query.h', + 'xb-silo-export.h', + 'xb-silo.h', + 'xb-silo-query.h', + 'xb-stack.h', + 'xb-string.h', +) + [libxmlb_version_h] + +install_headers( + xb_headers, + subdir : 'libxmlb-1/libxmlb', +) + +subdir('libxmlb') + +mapfile = 'libxmlb.map' +vflag = '-Wl,--version-script,@0@/@1@'.format(meson.current_source_dir(), mapfile) +libxmlb = library( + 'xmlb', + sources : [ + 'xb-builder.c', + 'xb-builder-fixup.c', + 'xb-builder-node.c', + 'xb-builder-source.c', + 'xb-builder-source-ctx.c', + 'xb-machine.c', + 'xb-opcode.c', + 'xb-node.c', + 'xb-node-query.c', + 'xb-query.c', + 'xb-silo.c', + 'xb-silo-export.c', + 'xb-silo-query.c', + 'xb-stack.c', + 'xb-string.c', + ], + soversion : lt_current, + version : lt_version, + include_directories : [ + configinc, + ], + dependencies : libxmlb_deps, + link_args : vflag, + link_depends : mapfile, + install : true +) + +libxmlb_dep = declare_dependency( + link_with : libxmlb, + include_directories : include_directories('.'), + dependencies : libxmlb_deps +) + +xb_tool = executable( + 'xb-tool', + sources : [ + 'xb-tool.c', + ], + include_directories : [ + configinc, + ], + dependencies : [ + gio, + ], + link_with : [ + libxmlb, + ], + install : true, + install_dir : libexecdir +) + +pkgg = import('pkgconfig') +pkgg.generate( + libraries : libxmlb, + requires : [ 'gio-2.0' ], + subdirs : 'libxmlb-1', + version : meson.project_version(), + name : 'libxmlb', + filebase : 'xmlb', + description : 'libxmlb is a library to create or query compressed XML files', +) + +if get_option('introspection') + gir = gnome.generate_gir(libxmlb, + sources : [ + 'xb-builder.c', + 'xb-builder.h', + 'xb-builder-fixup.c', + 'xb-builder-fixup.h', + 'xb-builder-node.c', + 'xb-builder-node.h', + 'xb-builder-source.c', + 'xb-builder-source.h', + 'xb-builder-source-ctx.c', + 'xb-builder-source-ctx.h', + 'xb-machine.c', + 'xb-machine.h', + 'xb-node.c', + 'xb-node.h', + 'xb-node-query.c', + 'xb-node-query.h', + 'xb-opcode.c', + 'xb-opcode.h', + 'xb-query.c', + 'xb-query.h', + 'xb-silo.c', + 'xb-silo.h', + 'xb-silo-export.c', + 'xb-silo-export.h', + 'xb-silo-query.c', + 'xb-silo-query.h', + 'xb-stack.c', + 'xb-stack.h', + 'xb-string.c', + 'xb-string.h', + ], + nsversion : '1.0', + namespace : 'Xmlb', + symbol_prefix : 'xb', + identifier_prefix : 'Xb', + export_packages : 'libxmlb', + header : 'xmlb.h', + dependencies : [ + gio, + ], + includes : [ + 'Gio-2.0', + 'GObject-2.0', + ], + link_with : [ + libxmlb, + ], + install : true + ) + + # Verify the map file is correct -- note we can't actually use the generated + # file for two reasons: + # + # 1. We don't hard depend on GObject Introspection + # 2. The map file is required to build the lib that the GIR is built from + # + # To avoid the circular dep, and to ensure we don't change exported API + # accidentally actually check in a version of the version script to git. + mapfile_target = custom_target('libxmlb-mapfile', + input: gir[0], + output: 'libxmlb.map', + command: [ + python3, + join_paths(meson.current_source_dir(), 'generate-version-script.py'), + 'LIBXMLB', + '@INPUT@', + '@OUTPUT@', + ], + ) + diffcmd = find_program('diff') + test('libxmlb-exported-api', diffcmd, + args : [ + '-urNp', + join_paths(meson.current_source_dir(), 'libxmlb.map'), + mapfile_target, + ], + ) +endif + +if get_option('tests') + e = executable( + 'xb-self-test', + sources : [ + 'xb-builder.c', + 'xb-builder-fixup.c', + 'xb-builder-fixup.c', + 'xb-builder-node.c', + 'xb-builder-source.c', + 'xb-builder-source-ctx.c', + 'xb-machine.c', + 'xb-node.c', + 'xb-node-query.c', + 'xb-opcode.c', + 'xb-self-test.c', + 'xb-query.c', + 'xb-silo.c', + 'xb-silo-export.c', + 'xb-silo-query.c', + 'xb-stack.c', + 'xb-string.c', + ], + include_directories : [ + configinc, + ], + c_args : [ + '-DTESTDIR="@0@/../data"'.format(meson.current_source_dir()), + '-DINSTALLEDTESTDIR="' + installed_test_datadir + '"', + ], + dependencies : [ + gio, + libxmlb_dep, + ], + install : true, + install_dir : installed_test_bindir + ) + test('xb-self-test', e) +endif diff --git a/src/xb-builder-fixup-private.h b/src/xb-builder-fixup-private.h new file mode 100644 index 0000000..cf27ece --- /dev/null +++ b/src/xb-builder-fixup-private.h @@ -0,0 +1,21 @@ +/* + * Copyright (C) 2018 Richard Hughes + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#pragma once + +#include + +#include "xb-builder-fixup.h" + +G_BEGIN_DECLS + +gboolean xb_builder_fixup_node (XbBuilderFixup *self, + XbBuilderNode *bn, + GError **error); +const gchar *xb_builder_fixup_get_id (XbBuilderFixup *self); +gchar *xb_builder_fixup_get_guid (XbBuilderFixup *self); + +G_END_DECLS diff --git a/src/xb-builder-fixup.c b/src/xb-builder-fixup.c new file mode 100644 index 0000000..a684f2b --- /dev/null +++ b/src/xb-builder-fixup.c @@ -0,0 +1,202 @@ +/* + * Copyright (C) 2018 Richard Hughes + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#define G_LOG_DOMAIN "XbSilo" + +#include "config.h" + +#include + +#include "xb-builder-fixup-private.h" + +typedef struct { + GObject parent_instance; + gchar *id; + XbBuilderFixupFunc func; + gpointer user_data; + GDestroyNotify user_data_free; + gint max_depth; +} XbBuilderFixupPrivate; + +G_DEFINE_TYPE_WITH_PRIVATE (XbBuilderFixup, xb_builder_fixup, G_TYPE_OBJECT) +#define GET_PRIVATE(o) (xb_builder_fixup_get_instance_private (o)) + +typedef struct { + XbBuilderFixup *self; + gboolean ret; + GError *error; +} XbBuilderFixupHelper; + +static gboolean +xb_builder_fixup_cb (XbBuilderNode *bn, gpointer data) +{ + XbBuilderFixupHelper *helper = (XbBuilderFixupHelper *) data; + XbBuilderFixup *self = XB_BUILDER_FIXUP (helper->self); + XbBuilderFixupPrivate *priv = GET_PRIVATE (self); + + /* run all node funcs on the source */ + if (!priv->func (self, bn, priv->user_data, &helper->error)) { + helper->ret = FALSE; + return TRUE; + } + + /* keep going */ + return FALSE; +} + +/* private */ +gboolean +xb_builder_fixup_node (XbBuilderFixup *self, XbBuilderNode *bn, GError **error) +{ + XbBuilderFixupPrivate *priv = GET_PRIVATE (self); + XbBuilderFixupHelper helper = { + .self = self, + .ret = TRUE, + .error = NULL, + }; + + /* visit each node */ + xb_builder_node_traverse (bn, G_PRE_ORDER, G_TRAVERSE_ALL, priv->max_depth, + xb_builder_fixup_cb, &helper); + if (!helper.ret) { + g_propagate_error (error, helper.error); + return FALSE; + } + return TRUE; +} + +/** + * xb_builder_fixup_get_id: + * @self: a #XbBuilderFixup + * + * Gets the fixup ID. + * + * Returns: string, e.g. `AppStreamUpgrade` + * + * Since: 0.1.3 + **/ +const gchar * +xb_builder_fixup_get_id (XbBuilderFixup *self) +{ + XbBuilderFixupPrivate *priv = GET_PRIVATE (self); + g_return_val_if_fail (XB_IS_BUILDER_FIXUP (self), NULL); + return priv->id; +} + +/* private */ +gchar * +xb_builder_fixup_get_guid (XbBuilderFixup *self) +{ + GString *str = g_string_new ("func-id="); + XbBuilderFixupPrivate *priv = GET_PRIVATE (self); + + g_return_val_if_fail (XB_IS_BUILDER_FIXUP (self), 0); + + /* build GUID using ID and max-depth, if set */ + g_string_append (str, priv->id); + if (priv->max_depth != -1) + g_string_append_printf (str, "@%i", priv->max_depth); + return g_string_free (str, FALSE); +} + +/** + * xb_builder_fixup_get_max_depth: + * @self: a #XbBuilderFixup + * + * Gets the maximum depth used for this fixup, if each node is being visited. + * + * Returns: integer, or -1 if unset + * + * Since: 0.1.3 + **/ +gint +xb_builder_fixup_get_max_depth (XbBuilderFixup *self) +{ + XbBuilderFixupPrivate *priv = GET_PRIVATE (self); + g_return_val_if_fail (XB_IS_BUILDER_FIXUP (self), 0); + return priv->max_depth; +} + +/** + * xb_builder_fixup_set_max_depth: + * @self: a #XbBuilderFixup + * @max_depth: integer, -1 for "all" + * + * Sets the maximum depth used for this fixup. Use a @max_depth of 0 to only + * visit the root node. + * + * Setting a maximum depth may increase performance considerably if using + * fixup functions on large and deeply nested XML files. + * + * Since: 0.1.3 + **/ +void +xb_builder_fixup_set_max_depth (XbBuilderFixup *self, gint max_depth) +{ + XbBuilderFixupPrivate *priv = GET_PRIVATE (self); + g_return_if_fail (XB_IS_BUILDER_FIXUP (self)); + priv->max_depth = max_depth; +} + +static void +xb_builder_fixup_finalize (GObject *obj) +{ + XbBuilderFixup *self = XB_BUILDER_FIXUP (obj); + XbBuilderFixupPrivate *priv = GET_PRIVATE (self); + + if (priv->user_data_free != NULL) + priv->user_data_free (priv->user_data); + g_free (priv->id); + + G_OBJECT_CLASS (xb_builder_fixup_parent_class)->finalize (obj); +} + +static void +xb_builder_fixup_class_init (XbBuilderFixupClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + object_class->finalize = xb_builder_fixup_finalize; +} + +static void +xb_builder_fixup_init (XbBuilderFixup *self) +{ + XbBuilderFixupPrivate *priv = GET_PRIVATE (self); + priv->max_depth = -1; +} + +/** + * xb_builder_fixup_new: + * @id: a text ID value, e.g. `AppStreamUpgrade` + * @func: a callback + * @user_data: user pointer to pass to @func, or %NULL + * @user_data_free: a function which gets called to free @user_data, or %NULL + * + * Creates a function that will get run on every #XbBuilderNode compile creates. + * + * Returns: a new #XbBuilderFixup + * + * Since: 0.1.3 + **/ +XbBuilderFixup * +xb_builder_fixup_new (const gchar *id, + XbBuilderFixupFunc func, + gpointer user_data, + GDestroyNotify user_data_free) +{ + XbBuilderFixup *self = g_object_new (XB_TYPE_BUILDER_FIXUP, NULL); + XbBuilderFixupPrivate *priv = GET_PRIVATE (self); + + g_return_val_if_fail (XB_IS_BUILDER_FIXUP (self), NULL); + g_return_val_if_fail (id != NULL, NULL); + g_return_val_if_fail (func != NULL, NULL); + + priv->id = g_strdup (id); + priv->func = func; + priv->user_data = user_data; + priv->user_data_free = user_data_free; + return self; +} diff --git a/src/xb-builder-fixup.h b/src/xb-builder-fixup.h new file mode 100644 index 0000000..674474c --- /dev/null +++ b/src/xb-builder-fixup.h @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2018 Richard Hughes + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#pragma once + +#include + +#include "xb-builder-node.h" + +G_BEGIN_DECLS + +#define XB_TYPE_BUILDER_FIXUP (xb_builder_fixup_get_type ()) + +G_DECLARE_DERIVABLE_TYPE (XbBuilderFixup, xb_builder_fixup, XB, BUILDER_FIXUP, GObject) + +struct _XbBuilderFixupClass { + GObjectClass parent_class; + /*< private >*/ + void (*_xb_reserved1) (void); + void (*_xb_reserved2) (void); + void (*_xb_reserved3) (void); + void (*_xb_reserved4) (void); + void (*_xb_reserved5) (void); + void (*_xb_reserved6) (void); + void (*_xb_reserved7) (void); +}; + +typedef gboolean (*XbBuilderFixupFunc) (XbBuilderFixup *self, + XbBuilderNode *bn, + gpointer user_data, + GError **error); + +XbBuilderFixup *xb_builder_fixup_new (const gchar *id, + XbBuilderFixupFunc func, + gpointer user_data, + GDestroyNotify user_data_free); +gint xb_builder_fixup_get_max_depth (XbBuilderFixup *self); +void xb_builder_fixup_set_max_depth (XbBuilderFixup *self, + gint max_depth); + +G_END_DECLS diff --git a/src/xb-builder-node-private.h b/src/xb-builder-node-private.h new file mode 100644 index 0000000..1fb4709 --- /dev/null +++ b/src/xb-builder-node-private.h @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2018 Richard Hughes + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#pragma once + +#include + +#include "xb-builder-node.h" + +G_BEGIN_DECLS + +typedef struct { + /*< private >*/ + gchar *name; + guint32 name_idx; + gchar *value; + guint32 value_idx; +} XbBuilderNodeAttr; + +GPtrArray *xb_builder_node_get_attrs (XbBuilderNode *self); +guint32 xb_builder_node_size (XbBuilderNode *self); +guint32 xb_builder_node_get_offset (XbBuilderNode *self); +void xb_builder_node_set_offset (XbBuilderNode *self, + guint32 offset); +gint xb_builder_node_get_priority (XbBuilderNode *self); +void xb_builder_node_set_priority (XbBuilderNode *self, + gint priority); +guint32 xb_builder_node_get_element_idx(XbBuilderNode *self); +void xb_builder_node_set_element_idx(XbBuilderNode *self, + guint32 element_idx); +guint32 xb_builder_node_get_text_idx (XbBuilderNode *self); +void xb_builder_node_set_text_idx (XbBuilderNode *self, + guint32 text_idx); +guint32 xb_builder_node_get_tail_idx (XbBuilderNode *self); +void xb_builder_node_set_tail_idx (XbBuilderNode *self, + guint32 tail_idx); + +G_END_DECLS diff --git a/src/xb-builder-node.c b/src/xb-builder-node.c new file mode 100644 index 0000000..890ee4d --- /dev/null +++ b/src/xb-builder-node.c @@ -0,0 +1,1050 @@ +/* + * Copyright (C) 2018 Richard Hughes + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#define G_LOG_DOMAIN "XbSilo" + +#include "config.h" + +#include +#include + +#include "xb-builder-node-private.h" +#include "xb-silo-private.h" +#include "xb-string-private.h" + +typedef struct { + GObject parent_instance; + guint32 offset; + gint priority; + XbBuilderNodeFlags flags; + gchar *element; + guint32 element_idx; + gchar *text; + guint32 text_idx; + gchar *tail; + guint32 tail_idx; + XbBuilderNode *parent; /* noref */ + GPtrArray *children; /* of XbBuilderNode */ + GPtrArray *attrs; /* of XbBuilderNodeAttr */ +} XbBuilderNodePrivate; + +G_DEFINE_TYPE_WITH_PRIVATE (XbBuilderNode, xb_builder_node, G_TYPE_OBJECT) +#define GET_PRIVATE(o) (xb_builder_node_get_instance_private (o)) + +/** + * xb_builder_node_has_flag: + * @self: a #XbBuilderNode + * @flag: a #XbBuilderNodeFlags + * + * Checks a flag on the builder node. + * + * Returns: %TRUE if @flag is set + * + * Since: 0.1.0 + **/ +gboolean +xb_builder_node_has_flag (XbBuilderNode *self, XbBuilderNodeFlags flag) +{ + XbBuilderNodePrivate *priv = GET_PRIVATE (self); + g_return_val_if_fail (XB_IS_BUILDER_NODE (self), FALSE); + return (priv->flags & flag) > 0; +} + +/** + * xb_builder_node_add_flag: + * @self: a #XbBuilderNode + * @flag: a #XbBuilderNodeFlags + * + * Adds a flag to the builder node. + * + * Since: 0.1.0 + **/ +void +xb_builder_node_add_flag (XbBuilderNode *self, XbBuilderNodeFlags flag) +{ + XbBuilderNodePrivate *priv = GET_PRIVATE (self); + g_return_if_fail (XB_IS_BUILDER_NODE (self)); + priv->flags |= flag; + for (guint i = 0; i < priv->children->len; i++) { + XbBuilderNode *c = g_ptr_array_index (priv->children, i); + xb_builder_node_add_flag (c, flag); + } +} + +/** + * xb_builder_node_get_element: + * @self: a #XbBuilderNode + * + * Gets the element from the builder node. + * + * Returns: string, or %NULL if unset + * + * Since: 0.1.0 + **/ +const gchar * +xb_builder_node_get_element (XbBuilderNode *self) +{ + XbBuilderNodePrivate *priv = GET_PRIVATE (self); + g_return_val_if_fail (XB_IS_BUILDER_NODE (self), NULL); + return priv->element; +} + +/** + * xb_builder_node_set_element: + * @self: a #XbBuilderNode + * @element: a string element + * + * Sets the element name on the builder node. + * + * Since: 0.1.0 + **/ +void +xb_builder_node_set_element (XbBuilderNode *self, const gchar *element) +{ + XbBuilderNodePrivate *priv = GET_PRIVATE (self); + g_return_if_fail (XB_IS_BUILDER_NODE (self)); + g_free (priv->element); + priv->element = g_strdup (element); +} + +/** + * xb_builder_node_get_attr: + * @self: a #XbBuilderNode + * @name: attribute name, e.g. `type` + * + * Gets an attribute from the builder node. + * + * Returns: string, or %NULL if unset + * + * Since: 0.1.0 + **/ +const gchar * +xb_builder_node_get_attr (XbBuilderNode *self, const gchar *name) +{ + XbBuilderNodePrivate *priv = GET_PRIVATE (self); + g_return_val_if_fail (XB_IS_BUILDER_NODE (self), NULL); + g_return_val_if_fail (name != NULL, NULL); + for (guint i = 0; i < priv->attrs->len; i++) { + XbBuilderNodeAttr *a = g_ptr_array_index (priv->attrs, i); + if (g_strcmp0 (a->name, name) == 0) + return a->value; + } + return NULL; +} + +/** + * xb_builder_node_get_attr_as_uint: + * @self: a #XbBuilderNode + * @name: attribute name, e.g. `priority` + * + * Gets an attribute from the builder node. + * + * Returns: integer, or 0 if unset + * + * Since: 0.1.3 + **/ +guint64 +xb_builder_node_get_attr_as_uint (XbBuilderNode *self, const gchar *name) +{ + const gchar *tmp = xb_builder_node_get_attr (self, name); + if (tmp == NULL) + return 0; + if (g_str_has_prefix (tmp, "0x")) + return g_ascii_strtoull (tmp + 2, NULL, 16); + return g_ascii_strtoll (tmp, NULL, 10); +} + +/** + * xb_builder_node_get_text: + * @self: a #XbBuilderNode + * + * Gets the text from the builder node. + * + * Returns: string, or %NULL if unset + * + * Since: 0.1.0 + **/ +const gchar * +xb_builder_node_get_text (XbBuilderNode *self) +{ + XbBuilderNodePrivate *priv = GET_PRIVATE (self); + g_return_val_if_fail (XB_IS_BUILDER_NODE (self), NULL); + return priv->text; +} + +/** + * xb_builder_node_get_text_as_uint: + * @self: a #XbBuilderNode + * + * Gets the text from the builder node. + * + * Returns: integer, or 0 if unset + * + * Since: 0.1.3 + **/ +guint64 +xb_builder_node_get_text_as_uint (XbBuilderNode *self) +{ + const gchar *tmp = xb_builder_node_get_text (self); + if (tmp == NULL) + return 0; + if (g_str_has_prefix (tmp, "0x")) + return g_ascii_strtoull (tmp + 2, NULL, 16); + return g_ascii_strtoll (tmp, NULL, 10); +} + +/** + * xb_builder_node_get_tail: + * @self: a #XbBuilderNode + * + * Gets the tail from the builder node. + * + * Returns: string, or %NULL if unset + * + * Since: 0.1.12 + **/ +const gchar * +xb_builder_node_get_tail (XbBuilderNode *self) +{ + XbBuilderNodePrivate *priv = GET_PRIVATE (self); + g_return_val_if_fail (XB_IS_BUILDER_NODE (self), NULL); + return priv->tail; +} + +/* private */ +GPtrArray * +xb_builder_node_get_attrs (XbBuilderNode *self) +{ + XbBuilderNodePrivate *priv = GET_PRIVATE (self); + g_return_val_if_fail (XB_IS_BUILDER_NODE (self), NULL); + return priv->attrs; +} + +static gchar * +xb_builder_node_parse_literal_text (XbBuilderNode *self, const gchar *text, gssize text_len) +{ + GString *tmp; + guint newline_count = 0; + g_auto(GStrv) split = NULL; + gsize text_len_safe; + + /* we know this has been pre-fixed */ + text_len_safe = text_len >= 0 ? (gsize) text_len : strlen (text); + if (xb_builder_node_has_flag (self, XB_BUILDER_NODE_FLAG_LITERAL_TEXT)) + return g_strndup (text, text_len_safe); + + /* all whitespace? */ + if (xb_string_isspace (text, text_len_safe)) + return NULL; + + /* all on one line, no trailing or leading whitespace */ + if (g_strstr_len (text, text_len, "\n") == NULL) + return g_strndup (text, text_len_safe); + + /* split the text into lines */ + tmp = g_string_sized_new ((gsize) text_len_safe + 1); + split = g_strsplit (text, "\n", -1); + for (guint i = 0; split[i] != NULL; i++) { + + /* remove leading and trailing whitespace */ + g_strstrip (split[i]); + + /* if this is a blank line we end the paragraph mode + * and swallow the newline. If we see exactly two + * newlines in sequence then do a paragraph break */ + if (split[i][0] == '\0') { + newline_count++; + continue; + } + + /* if the line just before this one was not a newline + * then seporate the words with a space */ + if (newline_count == 1 && tmp->len > 0) + g_string_append (tmp, " "); + + /* if we had more than one newline in sequence add a paragraph + * break */ + if (newline_count > 1) + g_string_append (tmp, "\n\n"); + + /* add the actual stripped text */ + g_string_append (tmp, split[i]); + + /* this last section was paragraph */ + newline_count = 1; + } + + /* success */ + return g_string_free (tmp, FALSE); +} + +/** + * xb_builder_node_set_text: + * @self: a #XbBuilderNode + * @text: a string + * @text_len: length of @text, or -1 if @text is NUL terminated + * + * Sets the text on the builder node. + * + * Since: 0.1.0 + **/ +void +xb_builder_node_set_text (XbBuilderNode *self, const gchar *text, gssize text_len) +{ + XbBuilderNodePrivate *priv = GET_PRIVATE (self); + + g_return_if_fail (XB_IS_BUILDER_NODE (self)); + g_return_if_fail (text != NULL); + + /* old data */ + g_free (priv->text); + priv->text = xb_builder_node_parse_literal_text (self, text, text_len); + priv->flags |= XB_BUILDER_NODE_FLAG_HAS_TEXT; +} + +/** + * xb_builder_node_set_tail: + * @self: a #XbBuilderNode + * @tail: a string + * @tail_len: length of @tail, or -1 if @tail is NUL terminated + * + * Sets the tail on the builder node. + * + * Since: 0.1.12 + **/ +void +xb_builder_node_set_tail (XbBuilderNode *self, const gchar *tail, gssize tail_len) +{ + XbBuilderNodePrivate *priv = GET_PRIVATE (self); + + g_return_if_fail (XB_IS_BUILDER_NODE (self)); + g_return_if_fail (tail != NULL); + + /* old data */ + g_free (priv->tail); + priv->tail = xb_builder_node_parse_literal_text (self, tail, tail_len); + priv->flags |= XB_BUILDER_NODE_FLAG_HAS_TAIL; +} + +/** + * xb_builder_node_set_attr: + * @self: a #XbBuilderNode + * @name: attribute name, e.g. `type` + * @value: attribute value, e.g. `desktop` + * + * Adds an attribute to the builder node. + * + * Since: 0.1.0 + **/ +void +xb_builder_node_set_attr (XbBuilderNode *self, const gchar *name, const gchar *value) +{ + XbBuilderNodeAttr *a; + XbBuilderNodePrivate *priv = GET_PRIVATE (self); + + g_return_if_fail (XB_IS_BUILDER_NODE (self)); + g_return_if_fail (name != NULL); + + /* check for existing name */ + for (guint i = 0; i < priv->attrs->len; i++) { + a = g_ptr_array_index (priv->attrs, i); + if (g_strcmp0 (a->name, name) == 0) { + g_free (a->value); + a->value = g_strdup (value); + return; + } + } + + /* create new */ + a = g_slice_new0 (XbBuilderNodeAttr); + a->name = g_strdup (name); + a->name_idx = XB_SILO_UNSET; + a->value = g_strdup (value); + a->value_idx = XB_SILO_UNSET; + g_ptr_array_add (priv->attrs, a); +} + +/** + * xb_builder_node_remove_attr: + * @self: a #XbBuilderNode + * @name: attribute name, e.g. `type` + * + * Removes an attribute from the builder node. + * + * Since: 0.1.0 + **/ +void +xb_builder_node_remove_attr (XbBuilderNode *self, const gchar *name) +{ + XbBuilderNodePrivate *priv = GET_PRIVATE (self); + + g_return_if_fail (XB_IS_BUILDER_NODE (self)); + g_return_if_fail (name != NULL); + + for (guint i = 0; i < priv->attrs->len; i++) { + XbBuilderNodeAttr *a = g_ptr_array_index (priv->attrs, i); + if (g_strcmp0 (a->name, name) == 0) { + g_ptr_array_remove_index (priv->attrs, i); + break; + } + } +} + +/** + * xb_builder_node_depth: + * @self: a #XbBuilderNode + * + * Gets the depth of the node tree, where 0 is the root node. + * + * Since: 0.1.1 + **/ +guint +xb_builder_node_depth (XbBuilderNode *self) +{ + for (guint i = 0; ; i++) { + XbBuilderNodePrivate *priv = GET_PRIVATE (self); + if (priv->parent == NULL) + return i; + self = priv->parent; + } + return 0; +} + +/** + * xb_builder_node_add_child: + * @self: A XbBuilderNode + * @child: A XbBuilderNode + * + * Adds a child builder node. + * + * Since: 0.1.0 + **/ +void +xb_builder_node_add_child (XbBuilderNode *self, XbBuilderNode *child) +{ + XbBuilderNodePrivate *priv = GET_PRIVATE (self); + XbBuilderNodePrivate *priv_child = GET_PRIVATE (child); + g_return_if_fail (XB_IS_BUILDER_NODE (self)); + g_return_if_fail (XB_IS_BUILDER_NODE (child)); + g_return_if_fail (priv_child->parent == NULL); + + /* no refcount */ + priv_child->parent = self; + g_object_add_weak_pointer (G_OBJECT (self), (gpointer *) &priv_child->parent); + + g_ptr_array_add (priv->children, g_object_ref (child)); +} + +/** + * xb_builder_node_remove_child: + * @self: A XbBuilderNode + * @child: A XbBuilderNode + * + * Removes a child builder node. + * + * Since: 0.1.1 + **/ +void +xb_builder_node_remove_child (XbBuilderNode *self, XbBuilderNode *child) +{ + XbBuilderNodePrivate *priv = GET_PRIVATE (self); + XbBuilderNodePrivate *priv_child = GET_PRIVATE (child); + + /* no refcount */ + g_object_remove_weak_pointer (G_OBJECT (self), (gpointer *) &priv_child->parent); + priv_child->parent = NULL; + + g_ptr_array_remove (priv->children, child); +} + +/** + * xb_builder_node_unlink: + * @self: a #XbBuilderNode + * + * Unlinks a #XbBuilderNode from a tree, resulting in two separate trees. + * + * This should not be used from the function called by xb_builder_node_traverse() + * otherwise the entire tree will not be traversed. + * + * Instead use xb_builder_node_add_flag(bn,XB_BUILDER_NODE_FLAG_IGNORE); + * + * Since: 0.1.1 + **/ +void +xb_builder_node_unlink (XbBuilderNode *self) +{ + XbBuilderNodePrivate *priv = GET_PRIVATE (self); + g_return_if_fail (XB_IS_BUILDER_NODE (self)); + if (priv->parent == NULL) + return; + xb_builder_node_remove_child (priv->parent, self); +} + +/** + * xb_builder_node_get_parent: + * @self: a #XbBuilderNode + * + * Gets the parent node for the current node. + * + * Returns: (transfer full): a new #XbBuilderNode, or %NULL no parent exists. + * + * Since: 0.1.1 + **/ +XbBuilderNode * +xb_builder_node_get_parent (XbBuilderNode *self) +{ + XbBuilderNodePrivate *priv = GET_PRIVATE (self); + g_return_val_if_fail (XB_IS_BUILDER_NODE (self), NULL); + if (priv->parent == NULL) + return NULL; + return g_object_ref (priv->parent); +} + +/** + * xb_builder_node_get_children: + * @self: a #XbBuilderNode + * + * Gets the children of the builder node. + * + * Returns: (transfer none) (element-type XbBuilderNode): children + * + * Since: 0.1.0 + **/ +GPtrArray * +xb_builder_node_get_children (XbBuilderNode *self) +{ + XbBuilderNodePrivate *priv = GET_PRIVATE (self); + g_return_val_if_fail (XB_IS_BUILDER_NODE (self), NULL); + return priv->children; +} + +/** + * xb_builder_node_get_first_child: + * @self: a #XbBuilderNode + * + * Gets the first child of the builder node. + * + * Returns: (transfer none): a #XbBuilderNode, or %NULL + * + * Since: 0.1.12 + **/ +XbBuilderNode * +xb_builder_node_get_first_child (XbBuilderNode *self) +{ + XbBuilderNodePrivate *priv = GET_PRIVATE (self); + g_return_val_if_fail (XB_IS_BUILDER_NODE (self), NULL); + if (priv->children->len == 0) + return NULL; + return g_ptr_array_index (priv->children, 0); +} + +/** + * xb_builder_node_get_last_child: + * @self: a #XbBuilderNode + * + * Gets the last child of the builder node. + * + * Returns: (transfer none): a #XbBuilderNode, or %NULL + * + * Since: 0.1.12 + **/ +XbBuilderNode * +xb_builder_node_get_last_child (XbBuilderNode *self) +{ + XbBuilderNodePrivate *priv = GET_PRIVATE (self); + g_return_val_if_fail (XB_IS_BUILDER_NODE (self), NULL); + if (priv->children->len == 0) + return NULL; + return g_ptr_array_index (priv->children, priv->children->len - 1); +} + +/** + * xb_builder_node_get_child: + * @self: a #XbBuilderNode + * @element: An element name, e.g. "url" + * @text: (allow-none): node text, e.g. "gimp.desktop" + * + * Finds a child builder node by the element name, and optionally text value. + * + * Returns: (transfer full): a new #XbBuilderNode, or %NULL if not found + * + * Since: 0.1.1 + **/ +XbBuilderNode * +xb_builder_node_get_child (XbBuilderNode *self, const gchar *element, const gchar *text) +{ + XbBuilderNodePrivate *priv = GET_PRIVATE (self); + + g_return_val_if_fail (XB_IS_BUILDER_NODE (self), NULL); + g_return_val_if_fail (element != NULL, NULL); + + for (guint i = 0; i < priv->children->len; i++) { + XbBuilderNode *child = g_ptr_array_index (priv->children, i); + if (g_strcmp0 (xb_builder_node_get_element (child), element) != 0) + continue; + if (text != NULL && g_strcmp0 (xb_builder_node_get_text (child), text) != 0) + continue; + return g_object_ref (child); + } + return NULL; +} + +typedef struct { + gint max_depth; + XbBuilderNodeTraverseFunc func; + gpointer user_data; + GTraverseFlags flags; + GTraverseType order; +} XbBuilderNodeTraverseHelper; + +static void +xb_builder_node_traverse_cb (XbBuilderNodeTraverseHelper *helper, + XbBuilderNode *bn, + gint depth) +{ + GPtrArray *children = xb_builder_node_get_children (bn); + + /* only leaves */ + if (helper->flags == G_TRAVERSE_LEAVES && + children->len > 0) + return; + + /* only non-leaves */ + if (helper->flags == G_TRAVERSE_NON_LEAVES && + children->len == 0) + return; + + /* recurse */ + if (helper->order == G_PRE_ORDER) { + if (helper->func (bn, helper->user_data)) + return; + } + if (helper->max_depth < 0 || depth < helper->max_depth) { + for (guint i = 0; i < children->len; i++) { + XbBuilderNode *bc = g_ptr_array_index (children, i); + xb_builder_node_traverse_cb (helper, bc, depth + 1); + } + } + if (helper->order == G_POST_ORDER) { + if (helper->func (bn, helper->user_data)) + return; + } +} + +/** + * xb_builder_node_traverse: + * @self: a #XbBuilderNode + * @order: a #GTraverseType, e.g. %G_PRE_ORDER + * @flags: a #GTraverseFlags, e.g. %G_TRAVERSE_ALL + * @max_depth: the maximum depth of the traversal, or -1 for no limit + * @func: (scope call): a #XbBuilderNodeTraverseFunc + * @user_data: user pointer to pass to @func, or %NULL + * + * Traverses a tree starting from @self. It calls the given function for each + * node visited. + * + * The traversal can be halted at any point by returning TRUE from @func. + * + * Since: 0.1.1 + **/ +void +xb_builder_node_traverse (XbBuilderNode *self, + GTraverseType order, + GTraverseFlags flags, + gint max_depth, + XbBuilderNodeTraverseFunc func, + gpointer user_data) +{ + XbBuilderNodeTraverseHelper helper = { + .max_depth = max_depth, + .order = order, + .flags = flags, + .func = func, + .user_data = user_data, + }; + if (order == G_PRE_ORDER || order == G_POST_ORDER) { + xb_builder_node_traverse_cb (&helper, self, 0); + return; + } + g_critical ("order %u not supported", order); +} + +typedef struct { + XbBuilderNodeSortFunc func; + gpointer user_data; +} XbBuilderNodeSortHelper; + +static gint +xb_builder_node_sort_children_cb (gconstpointer a, gconstpointer b, gpointer user_data) +{ + XbBuilderNodeSortHelper *helper = (XbBuilderNodeSortHelper *) user_data; + XbBuilderNode *bn1 = *((XbBuilderNode **) a); + XbBuilderNode *bn2 = *((XbBuilderNode **) b); + return helper->func (bn1, bn2, helper->user_data); +} + +/** + * xb_builder_node_sort_children: + * @self: a #XbBuilderNode + * @func: (scope call): a #XbBuilderNodeSortFunc + * @user_data: user pointer to pass to @func, or %NULL + * + * Sorts the node children using a custom sort function. + * + * Since: 0.1.3 + **/ +void +xb_builder_node_sort_children (XbBuilderNode *self, + XbBuilderNodeSortFunc func, + gpointer user_data) +{ + XbBuilderNodePrivate *priv = GET_PRIVATE (self); + XbBuilderNodeSortHelper helper = { + .func = func, + .user_data = user_data, + }; + g_return_if_fail (XB_IS_BUILDER_NODE (self)); + g_return_if_fail (func != NULL); + g_ptr_array_sort_with_data (priv->children, + xb_builder_node_sort_children_cb, + &helper); +} + +/* private */ +guint32 +xb_builder_node_get_offset (XbBuilderNode *self) +{ + XbBuilderNodePrivate *priv = GET_PRIVATE (self); + g_return_val_if_fail (XB_IS_BUILDER_NODE (self), 0); + return priv->offset; +} + +/* private */ +void +xb_builder_node_set_offset (XbBuilderNode *self, guint32 offset) +{ + XbBuilderNodePrivate *priv = GET_PRIVATE (self); + g_return_if_fail (XB_IS_BUILDER_NODE (self)); + priv->offset = offset; +} + +/* private */ +gint +xb_builder_node_get_priority (XbBuilderNode *self) +{ + XbBuilderNodePrivate *priv = GET_PRIVATE (self); + g_return_val_if_fail (XB_IS_BUILDER_NODE (self), 0); + return priv->priority; +} + +/* private */ +void +xb_builder_node_set_priority (XbBuilderNode *self, gint priority) +{ + XbBuilderNodePrivate *priv = GET_PRIVATE (self); + g_return_if_fail (XB_IS_BUILDER_NODE (self)); + priv->priority = priority; +} + +/* private */ +guint32 +xb_builder_node_get_element_idx (XbBuilderNode *self) +{ + XbBuilderNodePrivate *priv = GET_PRIVATE (self); + g_return_val_if_fail (XB_IS_BUILDER_NODE (self), 0); + return priv->element_idx; +} + +/* private */ +void +xb_builder_node_set_element_idx (XbBuilderNode *self, guint32 element_idx) +{ + XbBuilderNodePrivate *priv = GET_PRIVATE (self); + g_return_if_fail (XB_IS_BUILDER_NODE (self)); + priv->element_idx = element_idx; +} + +/* private */ +guint32 +xb_builder_node_get_text_idx (XbBuilderNode *self) +{ + XbBuilderNodePrivate *priv = GET_PRIVATE (self); + g_return_val_if_fail (XB_IS_BUILDER_NODE (self), 0); + return priv->text_idx; +} + +/* private */ +void +xb_builder_node_set_text_idx (XbBuilderNode *self, guint32 text_idx) +{ + XbBuilderNodePrivate *priv = GET_PRIVATE (self); + g_return_if_fail (XB_IS_BUILDER_NODE (self)); + priv->text_idx = text_idx; +} + +/* private */ +guint32 +xb_builder_node_get_tail_idx (XbBuilderNode *self) +{ + XbBuilderNodePrivate *priv = GET_PRIVATE (self); + g_return_val_if_fail (XB_IS_BUILDER_NODE (self), 0); + return priv->tail_idx; +} + +/* private */ +void +xb_builder_node_set_tail_idx (XbBuilderNode *self, guint32 tail_idx) +{ + XbBuilderNodePrivate *priv = GET_PRIVATE (self); + g_return_if_fail (XB_IS_BUILDER_NODE (self)); + priv->tail_idx = tail_idx; +} + +/* private */ +guint32 +xb_builder_node_size (XbBuilderNode *self) +{ + XbBuilderNodePrivate *priv = GET_PRIVATE (self); + guint32 sz = sizeof(XbSiloNode); + return sz + priv->attrs->len * sizeof(XbSiloAttr); +} + +static void +xb_builder_node_attr_free (XbBuilderNodeAttr *attr) +{ + g_free (attr->name); + g_free (attr->value); + g_slice_free (XbBuilderNodeAttr, attr); +} + +static void +xb_builder_node_init (XbBuilderNode *self) +{ + XbBuilderNodePrivate *priv = GET_PRIVATE (self); + priv->element_idx = XB_SILO_UNSET; + priv->text_idx = XB_SILO_UNSET; + priv->tail_idx = XB_SILO_UNSET; + priv->attrs = g_ptr_array_new_with_free_func ((GDestroyNotify) xb_builder_node_attr_free); + priv->children = g_ptr_array_new_with_free_func ((GDestroyNotify) g_object_unref); +} + +static void +xb_builder_node_finalize (GObject *obj) +{ + XbBuilderNode *self = XB_BUILDER_NODE (obj); + XbBuilderNodePrivate *priv = GET_PRIVATE (self); + g_free (priv->element); + g_free (priv->text); + g_free (priv->tail); + g_ptr_array_unref (priv->attrs); + g_ptr_array_unref (priv->children); + G_OBJECT_CLASS (xb_builder_node_parent_class)->finalize (obj); +} + +static void +xb_builder_node_class_init (XbBuilderNodeClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + object_class->finalize = xb_builder_node_finalize; +} + +/** + * xb_builder_node_new: + * @element: An element name, e.g. "component" + * + * Creates a new builder node. + * + * Returns: (transfer full): a new #XbBuilderNode + * + * Since: 0.1.0 + **/ +XbBuilderNode * +xb_builder_node_new (const gchar *element) +{ + XbBuilderNode *self = g_object_new (XB_TYPE_BUILDER_NODE, NULL); + XbBuilderNodePrivate *priv = GET_PRIVATE (self); + priv->element = g_strdup (element); + return self; +} + +/** + * xb_builder_node_insert: (skip) + * @parent: A XbBuilderNode, or %NULL + * @element: An element name, e.g. "component" + * @...: any attributes to add to the node, terminated by %NULL + * + * Creates a new builder node. + * + * Returns: (transfer full): a new #XbBuilderNode + * + * Since: 0.1.0 + **/ +XbBuilderNode * +xb_builder_node_insert (XbBuilderNode *parent, const gchar *element, ...) +{ + XbBuilderNode *self = xb_builder_node_new (element); + va_list args; + const gchar *key; + const gchar *value; + + /* add this node to the parent */ + if (parent != NULL) + xb_builder_node_add_child (parent, self); + + /* process the attrs valist */ + va_start (args, element); + for (guint i = 0;; i++) { + key = va_arg (args, const gchar *); + if (key == NULL) + break; + value = va_arg (args, const gchar *); + if (value == NULL) + break; + xb_builder_node_set_attr (self, key, value); + } + va_end (args); + + return self; +} + +/** + * xb_builder_node_insert_text: (skip) + * @parent: A XbBuilderNode, or %NULL + * @element: An element name, e.g. "id" + * @text: (allow-none): node text, e.g. "gimp.desktop" + * @...: any attributes to add to the node, terminated by %NULL + * + * Creates a new builder node with optional node text. + * + * Since: 0.1.0 + **/ +void +xb_builder_node_insert_text (XbBuilderNode *parent, + const gchar *element, + const gchar *text, + ...) +{ + g_autoptr(XbBuilderNode) self = xb_builder_node_new (element); + va_list args; + const gchar *key; + const gchar *value; + + g_return_if_fail (parent != NULL); + + /* add this node to the parent */ + xb_builder_node_add_child (parent, self); + if (text != NULL) + xb_builder_node_set_text (self, text, -1); + + /* process the attrs valist */ + va_start (args, text); + for (guint i = 0;; i++) { + key = va_arg (args, const gchar *); + if (key == NULL) + break; + value = va_arg (args, const gchar *); + if (value == NULL) + break; + xb_builder_node_set_attr (self, key, value); + } + va_end (args); +} + +typedef struct { + GString *xml; + XbNodeExportFlags flags; + guint level; +} XbBuilderNodeExportHelper; + +static gboolean +xb_builder_node_export_helper (XbBuilderNode *self, + XbBuilderNodeExportHelper *helper, + GError **error) +{ + XbBuilderNodePrivate *priv = GET_PRIVATE (self); + + /* add start of opening tag */ + if (helper->flags & XB_NODE_EXPORT_FLAG_FORMAT_INDENT) { + for (guint i = 0; i < helper->level; i++) + g_string_append (helper->xml, " "); + } + g_string_append_printf (helper->xml, "<%s", priv->element); + + /* add any attributes */ + for (guint i = 0; i < priv->attrs->len; i++) { + XbBuilderNodeAttr *a = g_ptr_array_index (priv->attrs, i); + g_autofree gchar *key = xb_string_xml_escape (a->name); + g_autofree gchar *val = xb_string_xml_escape (a->value); + g_string_append_printf (helper->xml, " %s=\"%s\"", key, val); + } + + /* finish the opening tag and add any text if it exists */ + if (priv->text != NULL) { + g_autofree gchar *text = xb_string_xml_escape (priv->text); + g_string_append (helper->xml, ">"); + g_string_append (helper->xml, text); + } else { + g_string_append (helper->xml, ">"); + if (helper->flags & XB_NODE_EXPORT_FLAG_FORMAT_MULTILINE) + g_string_append (helper->xml, "\n"); + } + + /* recurse deeper */ + for (guint i = 0; i < priv->children->len; i++) { + XbBuilderNode *child = g_ptr_array_index (priv->children, i); + helper->level++; + if (!xb_builder_node_export_helper (child, helper, error)) + return FALSE; + helper->level--; + } + + /* add any tail if it exists */ + if (priv->tail != NULL) { + g_autofree gchar *tail = xb_string_xml_escape (priv->tail); + g_string_append (helper->xml, tail); + } + + /* add closing tag */ + if ((helper->flags & XB_NODE_EXPORT_FLAG_FORMAT_INDENT) > 0 && + priv->text == NULL) { + for (guint i = 0; i < helper->level; i++) + g_string_append (helper->xml, " "); + } + g_string_append_printf (helper->xml, "", priv->element); + if (helper->flags & XB_NODE_EXPORT_FLAG_FORMAT_MULTILINE) + g_string_append (helper->xml, "\n"); + return TRUE; +} + +/** + * xb_builder_node_export: + * @self: a #XbBuilderNode + * @flags: some #XbNodeExportFlags, e.g. #XB_NODE_EXPORT_FLAG_NONE + * @error: the #GError, or %NULL + * + * Exports the node to XML. + * + * Returns: XML data, or %NULL for an error + * + * Since: 0.1.5 + **/ +gchar * +xb_builder_node_export (XbBuilderNode *self, XbNodeExportFlags flags, GError **error) +{ + g_autoptr(GString) xml = g_string_new (NULL); + XbBuilderNodeExportHelper helper = { + .flags = flags, + .level = 0, + .xml = xml, + }; + g_return_val_if_fail (XB_IS_BUILDER_NODE (self), NULL); + g_return_val_if_fail (error == NULL || *error == NULL, NULL); + if ((flags & XB_NODE_EXPORT_FLAG_ADD_HEADER) > 0) + g_string_append (xml, "\n"); + if (!xb_builder_node_export_helper (self, &helper, error)) + return NULL; + return g_string_free (g_steal_pointer (&xml), FALSE); +} diff --git a/src/xb-builder-node.h b/src/xb-builder-node.h new file mode 100644 index 0000000..ae37f04 --- /dev/null +++ b/src/xb-builder-node.h @@ -0,0 +1,116 @@ +/* + * Copyright (C) 2018 Richard Hughes + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#pragma once + +#include + +#include "xb-node.h" + +G_BEGIN_DECLS + +#define XB_TYPE_BUILDER_NODE (xb_builder_node_get_type ()) +G_DECLARE_DERIVABLE_TYPE (XbBuilderNode, xb_builder_node, XB, BUILDER_NODE, GObject) + +struct _XbBuilderNodeClass { + GObjectClass parent_class; + /*< private >*/ + void (*_xb_reserved1) (void); + void (*_xb_reserved2) (void); + void (*_xb_reserved3) (void); + void (*_xb_reserved4) (void); + void (*_xb_reserved5) (void); + void (*_xb_reserved6) (void); + void (*_xb_reserved7) (void); +}; + +/** + * XbBuilderNodeFlags: + * @XB_BUILDER_NODE_FLAG_NONE: No extra flags to use + * @XB_BUILDER_NODE_FLAG_IGNORE: Do not include this node in the silo + * @XB_BUILDER_NODE_FLAG_LITERAL_TEXT: Assume the node CDATA is already valid + * @XB_BUILDER_NODE_FLAG_HAS_TEXT: If the node has leading text + * @XB_BUILDER_NODE_FLAG_HAS_TAIL: If the node has trailing text + * + * The flags used when building a node. + **/ +typedef enum { + XB_BUILDER_NODE_FLAG_NONE = 0, /* Since: 0.1.0 */ + XB_BUILDER_NODE_FLAG_IGNORE = 1 << 0, /* Since: 0.1.0 */ + XB_BUILDER_NODE_FLAG_LITERAL_TEXT = 1 << 1, /* Since: 0.1.0 */ + XB_BUILDER_NODE_FLAG_HAS_TEXT = 1 << 2, /* Since: 0.1.12 */ + XB_BUILDER_NODE_FLAG_HAS_TAIL = 1 << 3, /* Since: 0.1.12 */ + /*< private >*/ + XB_BUILDER_NODE_FLAG_LAST +} XbBuilderNodeFlags; + +typedef gboolean (*XbBuilderNodeTraverseFunc) (XbBuilderNode *bn, + gpointer user_data); +typedef gint (*XbBuilderNodeSortFunc) (XbBuilderNode *bn1, + XbBuilderNode *bn2, + gpointer user_data); + +XbBuilderNode *xb_builder_node_new (const gchar *element); +XbBuilderNode *xb_builder_node_insert (XbBuilderNode *parent, + const gchar *element, + ...) G_GNUC_NULL_TERMINATED; +void xb_builder_node_insert_text (XbBuilderNode *parent, + const gchar *element, + const gchar *text, + ...) G_GNUC_NULL_TERMINATED; + +gboolean xb_builder_node_has_flag (XbBuilderNode *self, + XbBuilderNodeFlags flag); +void xb_builder_node_add_flag (XbBuilderNode *self, + XbBuilderNodeFlags flag); +const gchar *xb_builder_node_get_element (XbBuilderNode *self); +void xb_builder_node_set_element (XbBuilderNode *self, + const gchar *element); +const gchar *xb_builder_node_get_text (XbBuilderNode *self); +guint64 xb_builder_node_get_text_as_uint(XbBuilderNode *self); +void xb_builder_node_set_text (XbBuilderNode *self, + const gchar *text, + gssize text_len); +const gchar *xb_builder_node_get_tail (XbBuilderNode *self); +void xb_builder_node_set_tail (XbBuilderNode *self, + const gchar *tail, + gssize tail_len); +const gchar *xb_builder_node_get_attr (XbBuilderNode *self, + const gchar *name); +guint64 xb_builder_node_get_attr_as_uint(XbBuilderNode *self, + const gchar *name); +void xb_builder_node_set_attr (XbBuilderNode *self, + const gchar *name, + const gchar *value); +void xb_builder_node_remove_attr (XbBuilderNode *self, + const gchar *name); +void xb_builder_node_add_child (XbBuilderNode *self, + XbBuilderNode *child); +void xb_builder_node_remove_child (XbBuilderNode *self, + XbBuilderNode *child); +GPtrArray *xb_builder_node_get_children (XbBuilderNode *self); +XbBuilderNode *xb_builder_node_get_first_child(XbBuilderNode *self); +XbBuilderNode *xb_builder_node_get_last_child (XbBuilderNode *self); +XbBuilderNode *xb_builder_node_get_child (XbBuilderNode *self, + const gchar *element, + const gchar *text); +void xb_builder_node_unlink (XbBuilderNode *self); +XbBuilderNode *xb_builder_node_get_parent (XbBuilderNode *self); +guint xb_builder_node_depth (XbBuilderNode *self); +void xb_builder_node_traverse (XbBuilderNode *self, + GTraverseType order, + GTraverseFlags flags, + gint max_depth, + XbBuilderNodeTraverseFunc func, + gpointer user_data); +void xb_builder_node_sort_children (XbBuilderNode *self, + XbBuilderNodeSortFunc func, + gpointer user_data); +gchar *xb_builder_node_export (XbBuilderNode *self, + XbNodeExportFlags flags, + GError **error); + +G_END_DECLS diff --git a/src/xb-builder-source-ctx-private.h b/src/xb-builder-source-ctx-private.h new file mode 100644 index 0000000..73cb636 --- /dev/null +++ b/src/xb-builder-source-ctx-private.h @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2019 Richard Hughes + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#pragma once + +#include + +#include "xb-builder-source-ctx.h" + +G_BEGIN_DECLS + +XbBuilderSourceCtx *xb_builder_source_ctx_new (GInputStream *istream); +void xb_builder_source_ctx_set_filename (XbBuilderSourceCtx *self, + const gchar *filename); +gchar *xb_builder_source_ctx_get_content_type (XbBuilderSourceCtx *self, + GCancellable *cancellable, + GError **error); + +G_END_DECLS diff --git a/src/xb-builder-source-ctx.c b/src/xb-builder-source-ctx.c new file mode 100644 index 0000000..b7b0b36 --- /dev/null +++ b/src/xb-builder-source-ctx.c @@ -0,0 +1,189 @@ +/* + * Copyright (C) 2018 Richard Hughes + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#define G_LOG_DOMAIN "XbSilo" + +#include "config.h" + +#include + +#include "xb-builder-source-ctx-private.h" + +typedef struct { + GObject parent_instance; + GInputStream *istream; + gchar *filename; + gchar *content_type; +} XbBuilderSourceCtxPrivate; + +G_DEFINE_TYPE_WITH_PRIVATE (XbBuilderSourceCtx, xb_builder_source_ctx, G_TYPE_OBJECT) +#define GET_PRIVATE(o) (xb_builder_source_ctx_get_instance_private (o)) + +/** + * xb_builder_source_ctx_get_stream: + * @self: a #XbBuilderSourceCtx + * + * Returns the input stream currently being processed. + * + * Returns: (transfer none): a #GInputStream + * + * Since: 0.1.7 + **/ +GInputStream * +xb_builder_source_ctx_get_stream (XbBuilderSourceCtx *self) +{ + XbBuilderSourceCtxPrivate *priv = GET_PRIVATE (self); + g_return_val_if_fail (XB_IS_BUILDER_SOURCE_CTX (self), NULL); + return priv->istream; +} + +/** + * xb_builder_source_ctx_get_bytes: + * @self: a #XbBuilderSourceCtx + * @cancellable: a #GCancellable, or %NULL + * @error: the #GError, or %NULL + * + * Returns the data currently being processed. + * + * Returns: (transfer none): a #GInputStream + * + * Since: 0.1.7 + **/ +GBytes * +xb_builder_source_ctx_get_bytes (XbBuilderSourceCtx *self, + GCancellable *cancellable, + GError **error) +{ + XbBuilderSourceCtxPrivate *priv = GET_PRIVATE (self); + g_return_val_if_fail (XB_IS_BUILDER_SOURCE_CTX (self), NULL); + g_return_val_if_fail (cancellable == NULL || G_IS_CANCELLABLE (cancellable), NULL); + g_return_val_if_fail (error == NULL || *error == NULL, NULL); + return g_input_stream_read_bytes (priv->istream, + 128 * 1024 * 1024, /* 128Mb */ + cancellable, error); +} + +/** + * xb_builder_source_ctx_get_filename: + * @self: a #XbBuilderSourceCtx + * + * Returns the basename of the file currently being processed. + * + * Returns: a filename, or %NULL if unset + * + * Since: 0.1.7 + **/ +const gchar * +xb_builder_source_ctx_get_filename (XbBuilderSourceCtx *self) +{ + XbBuilderSourceCtxPrivate *priv = GET_PRIVATE (self); + g_return_val_if_fail (XB_IS_BUILDER_SOURCE_CTX (self), NULL); + return priv->filename; +} + +/** + * xb_builder_source_ctx_get_content_type: + * @self: a #XbBuilderSourceCtx + * + * Returns the content type of the input stream currently being + * processed. + * + * Returns: (transfer full): a content type (e.g. `application/x-desktop`), or %NULL + * + * Since: 0.1.7 + **/ +gchar * +xb_builder_source_ctx_get_content_type (XbBuilderSourceCtx *self, + GCancellable *cancellable, + GError **error) +{ + XbBuilderSourceCtxPrivate *priv = GET_PRIVATE (self); + g_autofree gchar *content_type = NULL; + + g_return_val_if_fail (XB_IS_BUILDER_SOURCE_CTX (self), NULL); + + if (G_IS_SEEKABLE (priv->istream)) { + gsize bufsz = 0; + guchar buf[4096] = { 0x00 }; + if (!g_input_stream_read_all (priv->istream, buf, sizeof(buf), + &bufsz, cancellable, error)) + return NULL; + if (!g_seekable_seek (G_SEEKABLE (priv->istream), 0, G_SEEK_SET, + cancellable, error)) + return NULL; + if (bufsz > 0) + content_type = g_content_type_guess (priv->filename, buf, bufsz, NULL); + } + + /* either unseekable, or empty */ + if (content_type == NULL) + content_type = g_content_type_guess (priv->filename, NULL, 0, NULL); + +#ifdef _WIN32 + /* map Windows "mime-type" to a content type */ + if (g_strcmp0 (content_type, ".gz") == 0) + return g_strdup ("application/gzip"); + if (g_strcmp0 (content_type, ".txt") == 0 || + g_strcmp0 (content_type, ".xml") == 0) + return g_strdup ("application/xml"); + if (g_strcmp0 (content_type, ".desktop") == 0) + return g_strdup ("application/x-desktop"); +#endif + + return g_steal_pointer (&content_type); +} + +/* private */ +void +xb_builder_source_ctx_set_filename (XbBuilderSourceCtx *self, const gchar *filename) +{ + XbBuilderSourceCtxPrivate *priv = GET_PRIVATE (self); + g_return_if_fail (XB_IS_BUILDER_SOURCE_CTX (self)); + g_return_if_fail (filename != NULL); + g_free (priv->filename); + priv->filename = g_strdup (filename); +} + +static void +xb_builder_source_ctx_init (XbBuilderSourceCtx *self) +{ +} + +static void +xb_builder_source_ctx_finalize (GObject *obj) +{ + XbBuilderSourceCtx *self = XB_BUILDER_SOURCE_CTX (obj); + XbBuilderSourceCtxPrivate *priv = GET_PRIVATE (self); + g_free (priv->filename); + g_object_unref (priv->istream); + G_OBJECT_CLASS (xb_builder_source_ctx_parent_class)->finalize (obj); +} + +static void +xb_builder_source_ctx_class_init (XbBuilderSourceCtxClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + object_class->finalize = xb_builder_source_ctx_finalize; +} + +/** + * xb_builder_source_ctx_new: + * @element: An element name, e.g. "component" + * + * Creates a new builder source_ctx. + * + * Returns: (transfer full): a new #XbBuilderSourceCtx + * + * Since: 0.1.7 + **/ +XbBuilderSourceCtx * +xb_builder_source_ctx_new (GInputStream *istream) +{ + XbBuilderSourceCtx *self = g_object_new (XB_TYPE_BUILDER_SOURCE_CTX, NULL); + XbBuilderSourceCtxPrivate *priv = GET_PRIVATE (self); + priv->istream = g_object_ref (istream); + return self; +} diff --git a/src/xb-builder-source-ctx.h b/src/xb-builder-source-ctx.h new file mode 100644 index 0000000..f556706 --- /dev/null +++ b/src/xb-builder-source-ctx.h @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2019 Richard Hughes + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#pragma once + +#include + +G_BEGIN_DECLS + +#define XB_TYPE_BUILDER_SOURCE_CTX (xb_builder_source_ctx_get_type ()) +G_DECLARE_DERIVABLE_TYPE (XbBuilderSourceCtx, xb_builder_source_ctx, XB, BUILDER_SOURCE_CTX, GObject) + +struct _XbBuilderSourceCtxClass { + GObjectClass parent_class; + /*< private >*/ + void (*_xb_reserved1) (void); + void (*_xb_reserved2) (void); + void (*_xb_reserved3) (void); + void (*_xb_reserved4) (void); + void (*_xb_reserved5) (void); + void (*_xb_reserved6) (void); + void (*_xb_reserved7) (void); +}; + +GInputStream *xb_builder_source_ctx_get_stream (XbBuilderSourceCtx *self); +const gchar *xb_builder_source_ctx_get_filename (XbBuilderSourceCtx *self); +GBytes *xb_builder_source_ctx_get_bytes (XbBuilderSourceCtx *self, + GCancellable *cancellable, + GError **error); + +G_END_DECLS diff --git a/src/xb-builder-source-private.h b/src/xb-builder-source-private.h new file mode 100644 index 0000000..eede9dd --- /dev/null +++ b/src/xb-builder-source-private.h @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2018 Richard Hughes + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#pragma once + +#include + +#include "xb-builder-source.h" +#include "xb-builder-node.h" + +G_BEGIN_DECLS + +XbBuilderNode *xb_builder_source_get_info (XbBuilderSource *self); +gchar *xb_builder_source_get_guid (XbBuilderSource *self); +const gchar *xb_builder_source_get_prefix (XbBuilderSource *self); +GInputStream *xb_builder_source_get_istream (XbBuilderSource *self, + GCancellable *cancellable, + GError **error); +GFile *xb_builder_source_get_file (XbBuilderSource *self); +gboolean xb_builder_source_fixup (XbBuilderSource *self, + XbBuilderNode *bn, + GError **error); +XbBuilderSourceFlags xb_builder_source_get_flags(XbBuilderSource *self); + +G_END_DECLS diff --git a/src/xb-builder-source.c b/src/xb-builder-source.c new file mode 100644 index 0000000..49f114d --- /dev/null +++ b/src/xb-builder-source.c @@ -0,0 +1,621 @@ +/* + * Copyright (C) 2018 Richard Hughes + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#define G_LOG_DOMAIN "XbSilo" + +#include "config.h" + +#include +#include + +#include "xb-builder-fixup-private.h" +#include "xb-builder-source-ctx-private.h" +#include "xb-builder-source-private.h" + +typedef struct { + GObject parent_instance; + GInputStream *istream; + GFile *file; + GPtrArray *fixups; /* of XbBuilderFixup */ + GPtrArray *adapters; /* of XbBuilderSourceAdapter */ + XbBuilderNode *info; + gchar *guid; + gchar *prefix; + gchar *content_type; + XbBuilderSourceFlags flags; +} XbBuilderSourcePrivate; + +G_DEFINE_TYPE_WITH_PRIVATE (XbBuilderSource, xb_builder_source, G_TYPE_OBJECT) +#define GET_PRIVATE(o) (xb_builder_source_get_instance_private (o)) + +typedef struct { + gchar *content_type; + XbBuilderSourceAdapterFunc func_adapter; + gpointer user_data; + GDestroyNotify user_data_free; + gboolean is_simple; +} XbBuilderSourceAdapter; + +static XbBuilderSourceAdapter * +xb_builder_source_get_adapter_by_mime (XbBuilderSource *self, + const gchar *content_type) +{ + XbBuilderSourcePrivate *priv = GET_PRIVATE (self); + for (guint i = 0; i < priv->adapters->len; i++) { + XbBuilderSourceAdapter *item = g_ptr_array_index (priv->adapters, i); + if (item->func_adapter == NULL) + continue; + if (g_strcmp0 (item->content_type, content_type) == 0) + return item; + } + return NULL; +} + +/** + * xb_builder_source_load_file: + * @self: a #XbBuilderSource + * @file: a #GFile + * @flags: some #XbBuilderSourceFlags, e.g. %XB_BUILDER_SOURCE_FLAG_LITERAL_TEXT + * @cancellable: a #GCancellable, or %NULL + * @error: the #GError, or %NULL + * + * Loads an optionally compressed XML file to build a #XbSilo. + * + * Returns: %TRUE for success + * + * Since: 0.1.1 + **/ +gboolean +xb_builder_source_load_file (XbBuilderSource *self, + GFile *file, + XbBuilderSourceFlags flags, + GCancellable *cancellable, + GError **error) +{ + const gchar *content_type = NULL; + guint32 ctime_usec; + guint64 ctime; + g_autofree gchar *fn = NULL; + g_autoptr(GFileInfo) fileinfo = NULL; + g_autoptr(GString) guid = NULL; + XbBuilderSourcePrivate *priv = GET_PRIVATE (self); + + g_return_val_if_fail (XB_IS_BUILDER_SOURCE (self), FALSE); + g_return_val_if_fail (G_IS_FILE (file), FALSE); + g_return_val_if_fail (cancellable == NULL || G_IS_CANCELLABLE (cancellable), FALSE); + g_return_val_if_fail (error == NULL || *error == NULL, FALSE); + + /* what kind of file is this */ + fileinfo = g_file_query_info (file, + G_FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE "," + G_FILE_ATTRIBUTE_TIME_CHANGED "," + G_FILE_ATTRIBUTE_TIME_CHANGED_USEC, + G_FILE_QUERY_INFO_NONE, + cancellable, + error); + if (fileinfo == NULL) + return FALSE; + + /* add data to GUID */ + fn = g_file_get_path (file); + guid = g_string_new (fn); + ctime = g_file_info_get_attribute_uint64 (fileinfo, G_FILE_ATTRIBUTE_TIME_CHANGED); + if (ctime != 0) + g_string_append_printf (guid, ":ctime=%" G_GUINT64_FORMAT, ctime); + ctime_usec = g_file_info_get_attribute_uint32 (fileinfo, G_FILE_ATTRIBUTE_TIME_CHANGED_USEC); + if (ctime_usec != 0) + g_string_append_printf (guid, ".%" G_GUINT32_FORMAT, ctime_usec); + priv->guid = g_string_free (g_steal_pointer (&guid), FALSE); + + /* check content type of file */ + content_type = g_file_info_get_attribute_string (fileinfo, G_FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE); + if (content_type == NULL) { + g_set_error_literal (error, + G_IO_ERROR, + G_IO_ERROR_NOT_SUPPORTED, + "cannot get content type for file"); + return FALSE; + } + + /* success */ + priv->flags = flags; + priv->content_type = g_strdup (content_type); + priv->file = g_object_ref (file); + return TRUE; +} + +/** + * xb_builder_source_set_info: + * @self: a #XbBuilderSource + * @info: (allow-none): a #XbBuilderNode + * + * Sets an optional information metadata node on the root node. + * + * Since: 0.1.0 + **/ +void +xb_builder_source_set_info (XbBuilderSource *self, XbBuilderNode *info) +{ + XbBuilderSourcePrivate *priv = GET_PRIVATE (self); + g_return_if_fail (XB_IS_BUILDER_SOURCE (self)); + g_set_object (&priv->info, info); +} + +/** + * xb_builder_source_set_prefix: + * @self: a #XbBuilderSource + * @prefix: (allow-none): an XPath prefix, e.g. `installed` + * + * Sets an optional prefix on the root node. This makes any nodes added + * using this source reside under a common shared parent node. + * + * Since: 0.1.0 + **/ +void +xb_builder_source_set_prefix (XbBuilderSource *self, const gchar *prefix) +{ + XbBuilderSourcePrivate *priv = GET_PRIVATE (self); + g_return_if_fail (XB_IS_BUILDER_SOURCE (self)); + g_free (priv->prefix); + priv->prefix = g_strdup (prefix); +} + +/** + * xb_builder_source_load_xml: + * @self: a #XbBuilderSource + * @xml: XML data + * @flags: some #XbBuilderSourceFlags, e.g. %XB_BUILDER_SOURCE_FLAG_LITERAL_TEXT + * @error: the #GError, or %NULL + * + * Loads XML data and begins to build a #XbSilo. + * + * Returns: %TRUE for success + * + * Since: 0.1.1 + **/ +gboolean +xb_builder_source_load_xml (XbBuilderSource *self, + const gchar *xml, + XbBuilderSourceFlags flags, + GError **error) +{ + g_autoptr(GBytes) blob = NULL; + g_autoptr(GChecksum) csum = g_checksum_new (G_CHECKSUM_SHA1); + XbBuilderSourcePrivate *priv = GET_PRIVATE (self); + + g_return_val_if_fail (XB_IS_BUILDER_SOURCE (self), FALSE); + g_return_val_if_fail (xml != NULL, FALSE); + g_return_val_if_fail (error == NULL || *error == NULL, FALSE); + + /* add a GUID of the SHA1 hash of the entire string */ + g_checksum_update (csum, (const guchar *) xml, -1); + priv->guid = g_strdup (g_checksum_get_string (csum)); + + /* create input stream */ + blob = g_bytes_new (xml, strlen (xml)); + priv->istream = g_memory_input_stream_new_from_bytes (blob); + if (priv->istream == NULL) + return FALSE; + + /* success */ + priv->flags = flags; + return TRUE; +} + +/** + * xb_builder_source_load_bytes: + * @self: a #XbBuilderSource + * @bytes: a #GBytes + * @flags: some #XbBuilderSourceFlags, e.g. %XB_BUILDER_SOURCE_FLAG_LITERAL_TEXT + * @error: the #GError, or %NULL + * + * Loads XML data and begins to build a #XbSilo. + * + * Returns: %TRUE for success + * + * Since: 0.1.2 + **/ +gboolean +xb_builder_source_load_bytes (XbBuilderSource *self, + GBytes *bytes, + XbBuilderSourceFlags flags, + GError **error) +{ + XbBuilderSourcePrivate *priv = GET_PRIVATE (self); + g_autoptr(GChecksum) csum = g_checksum_new (G_CHECKSUM_SHA1); + + g_return_val_if_fail (XB_IS_BUILDER_SOURCE (self), FALSE); + g_return_val_if_fail (bytes != NULL, FALSE); + g_return_val_if_fail (error == NULL || *error == NULL, FALSE); + + /* add a GUID of the SHA1 hash of the entire blob */ + g_checksum_update (csum, + (const guchar *) g_bytes_get_data (bytes, NULL), + (gssize) g_bytes_get_size (bytes)); + priv->guid = g_strdup (g_checksum_get_string (csum)); + + /* create input stream */ + priv->istream = g_memory_input_stream_new_from_bytes (bytes); + if (priv->istream == NULL) + return FALSE; + + /* success */ + priv->flags = flags; + return TRUE; +} + +/** + * xb_builder_source_add_fixup: + * @self: a #XbBuilderSource + * @fixup: a #XbBuilderFixup + * + * Adds a function that will get run on every #XbBuilderNode compile creates + * with this source. + * + * Since: 0.1.3 + **/ +void +xb_builder_source_add_fixup (XbBuilderSource *self, XbBuilderFixup *fixup) +{ + XbBuilderSourcePrivate *priv = GET_PRIVATE (self); + g_return_if_fail (XB_IS_BUILDER_SOURCE (self)); + g_return_if_fail (XB_IS_BUILDER_FIXUP (fixup)); + g_ptr_array_add (priv->fixups, g_object_ref (fixup)); +} + +/** + * xb_builder_source_add_node_func: + * @self: a #XbBuilderSource + * @id: a text ID value, e.g. `AppStreamUpgrade` + * @func: a callback + * @user_data: user pointer to pass to @func, or %NULL + * @user_data_free: a function which gets called to free @user_data, or %NULL + * + * Adds a function that will get run on every #XbBuilderNode compile creates. + * + * Since: 0.1.0 + **/ +void +xb_builder_source_add_node_func (XbBuilderSource *self, + const gchar *id, + XbBuilderSourceNodeFunc func, + gpointer user_data, + GDestroyNotify user_data_free) +{ + g_autoptr(XbBuilderFixup) fixup = NULL; + /* close enough... */ + fixup = xb_builder_fixup_new (id, (XbBuilderFixupFunc) func, + user_data, user_data_free); + xb_builder_source_add_fixup (self, fixup); +} + +/** + * xb_builder_source_add_converter: + * @self: a #XbBuilderSource + * @content_types: mimetypes, e.g. `application/x-desktop,application/gzip` + * @func: a callback + * @user_data: user pointer to pass to @func, or %NULL + * @user_data_free: a function which gets called to free @user_data, or %NULL + * + * This function is now deprecated, and does nothing. + * + * See also: xb_builder_source_add_adapter() + * + * Since: 0.1.1 + **/ +void +xb_builder_source_add_converter (XbBuilderSource *self, + const gchar *content_types, + XbBuilderSourceConverterFunc func, + gpointer user_data, + GDestroyNotify user_data_free) +{ + g_warning ("%s() does nothing", G_STRFUNC); +} + +static void +xb_builder_source_init_adapter (XbBuilderSource *self, + const gchar *content_types, + XbBuilderSourceAdapterFunc func, + gpointer user_data, + GDestroyNotify user_data_free, + gboolean is_simple) +{ + XbBuilderSourcePrivate *priv = GET_PRIVATE (self); + g_auto(GStrv) split = NULL; + + g_return_if_fail (XB_IS_BUILDER_SOURCE (self)); + g_return_if_fail (content_types != NULL); + g_return_if_fail (func != NULL); + + /* add each */ + split = g_strsplit (content_types, ",", -1); + for (guint i = 0; split[i] != NULL; i++) { + XbBuilderSourceAdapter *item; + item = g_slice_new0 (XbBuilderSourceAdapter); + item->content_type = g_strdup (split[i]); + item->func_adapter = func; + item->user_data = user_data; + item->user_data_free = user_data_free; + item->is_simple = is_simple; + g_ptr_array_add (priv->adapters, item); + } +} + +/** + * xb_builder_source_add_adapter: + * @self: a #XbBuilderSource + * @content_types: mimetypes, e.g. `application/x-desktop,application/gzip` + * @func: a callback, or %NULL + * @user_data: user pointer to pass to @func, or %NULL + * @user_data_free: a function which gets called to free @user_data, or %NULL + * + * Adds a function that can be used to convert streams loaded with + * xb_builder_source_load_xml(). + * + * Since: 0.1.7 + **/ +void +xb_builder_source_add_adapter (XbBuilderSource *self, + const gchar *content_types, + XbBuilderSourceAdapterFunc func, + gpointer user_data, + GDestroyNotify user_data_free) +{ + xb_builder_source_init_adapter (self, content_types, func, + user_data, user_data_free, FALSE); +} + +/** + * xb_builder_source_add_simple_adapter: + * @self: a #XbBuilderSource + * @content_types: mimetypes, e.g. `application/x-desktop,application/gzip` + * @func: a callback, or %NULL + * @user_data: user pointer to pass to @func, or %NULL + * @user_data_free: a function which gets called to free @user_data, or %NULL + * + * Adds a function that can be used to convert streams loaded with + * xb_builder_source_load_xml(). + * + * Since: 0.1.15 + **/ +void +xb_builder_source_add_simple_adapter (XbBuilderSource *self, + const gchar *content_types, + XbBuilderSourceAdapterFunc func, + gpointer user_data, + GDestroyNotify user_data_free) +{ + xb_builder_source_init_adapter (self, content_types, func, + user_data, user_data_free, TRUE); +} + +gboolean +xb_builder_source_fixup (XbBuilderSource *self, XbBuilderNode *bn, GError **error) +{ + XbBuilderSourcePrivate *priv = GET_PRIVATE (self); + for (guint i = 0; i < priv->fixups->len; i++) { + XbBuilderFixup *fixup = g_ptr_array_index (priv->fixups, i); + if (!xb_builder_fixup_node (fixup, bn, error)) + return FALSE; + } + return TRUE; +} + +static gboolean +xb_builder_source_info_guid_cb (XbBuilderNode *bn, gpointer data) +{ + GString *str = (GString *) data; + if (xb_builder_node_get_text (bn) != NULL) { + g_string_append_printf (str, ":%s=%s", + xb_builder_node_get_element (bn), + xb_builder_node_get_text (bn)); + } + return FALSE; +} + +gchar * +xb_builder_source_get_guid (XbBuilderSource *self) +{ + XbBuilderSourcePrivate *priv = GET_PRIVATE (self); + GString *str = g_string_new (priv->guid); + + g_return_val_if_fail (XB_IS_BUILDER_SOURCE (self), NULL); + + /* append function IDs */ + for (guint i = 0; i < priv->fixups->len; i++) { + XbBuilderFixup *fixup = g_ptr_array_index (priv->fixups, i); + g_autofree gchar *tmp = xb_builder_fixup_get_guid (fixup); + g_string_append_printf (str, ":%s", tmp); + } + + /* append any info */ + if (priv->info != NULL) { + xb_builder_node_traverse (priv->info, G_PRE_ORDER, G_TRAVERSE_ALL, -1, + xb_builder_source_info_guid_cb, str); + } + + /* append prefix */ + if (priv->prefix != NULL) + g_string_append_printf (str, ":prefix=%s", priv->prefix); + return g_string_free (str, FALSE); +} + +const gchar * +xb_builder_source_get_prefix (XbBuilderSource *self) +{ + XbBuilderSourcePrivate *priv = GET_PRIVATE (self); + g_return_val_if_fail (XB_IS_BUILDER_SOURCE (self), NULL); + return priv->prefix; +} + +XbBuilderNode * +xb_builder_source_get_info (XbBuilderSource *self) +{ + XbBuilderSourcePrivate *priv = GET_PRIVATE (self); + g_return_val_if_fail (XB_IS_BUILDER_SOURCE (self), NULL); + return priv->info; +} + +/* converts foo.xml.gz to foo.xml */ +static void +xb_builder_source_remove_last_extension (gchar *basename) +{ + gchar *tmp = g_strrstr (basename, "."); + if (tmp != NULL) + *tmp = '\0'; +} + +GInputStream * +xb_builder_source_get_istream (XbBuilderSource *self, + GCancellable *cancellable, + GError **error) +{ + XbBuilderSourcePrivate *priv = GET_PRIVATE (self); + g_autofree gchar *basename = NULL; + + g_return_val_if_fail (XB_IS_BUILDER_SOURCE (self), NULL); + + /* nothing required */ + if (priv->istream != NULL) + return g_object_ref (priv->istream); + + /* convert the file to a GFileInputStream */ + priv->istream = G_INPUT_STREAM (g_file_read (priv->file, cancellable, error)); + if (priv->istream == NULL) + return NULL; + + /* run the content type handlers until we get application/xml */ + basename = g_file_get_basename (priv->file); + do { + XbBuilderSourceAdapter *item; + g_autofree gchar *content_type = NULL; + g_autoptr(GInputStream) istream_tmp = NULL; + g_autoptr(XbBuilderSourceCtx) ctx = xb_builder_source_ctx_new (priv->istream); + + /* get the content type of the stream */ + xb_builder_source_ctx_set_filename (ctx, basename); + content_type = xb_builder_source_ctx_get_content_type (ctx, + cancellable, + error); + if (content_type == NULL) + return NULL; + if (g_strcmp0 (content_type, "application/xml") == 0) + break; + + /* convert the stream */ + item = xb_builder_source_get_adapter_by_mime (self, content_type); + if (item == NULL || item->func_adapter == NULL) { + g_set_error (error, + G_IO_ERROR, + G_IO_ERROR_NOT_SUPPORTED, + "cannot process content type %s", + content_type); + return NULL; + } + istream_tmp = item->func_adapter (self, ctx, item->user_data, + cancellable, error); + if (istream_tmp == NULL) + return NULL; + xb_builder_source_remove_last_extension (basename); + g_set_object (&priv->istream, istream_tmp); + + if (item->is_simple) + break; + } while (TRUE); + return g_object_ref (priv->istream); +} + +GFile * +xb_builder_source_get_file (XbBuilderSource *self) +{ + XbBuilderSourcePrivate *priv = GET_PRIVATE (self); + g_return_val_if_fail (XB_IS_BUILDER_SOURCE (self), NULL); + return priv->file; +} + +XbBuilderSourceFlags +xb_builder_source_get_flags (XbBuilderSource *self) +{ + XbBuilderSourcePrivate *priv = GET_PRIVATE (self); + g_return_val_if_fail (XB_IS_BUILDER_SOURCE (self), 0); + return priv->flags; +} + +static GInputStream * +xb_builder_source_load_gzip_cb (XbBuilderSource *self, + XbBuilderSourceCtx *ctx, + gpointer user_data, + GCancellable *cancellable, + GError **error) +{ + GInputStream *istream = xb_builder_source_ctx_get_stream (ctx); + g_autoptr(GConverter) conv = NULL; + conv = G_CONVERTER (g_zlib_decompressor_new (G_ZLIB_COMPRESSOR_FORMAT_GZIP)); + return g_converter_input_stream_new (istream, conv); +} + +static void +xb_builder_source_adapter_free (XbBuilderSourceAdapter *item) +{ + if (item->user_data_free != NULL) + item->user_data_free (item->user_data); + g_free (item->content_type); + g_slice_free (XbBuilderSourceAdapter, item); +} + +static void +xb_builder_source_finalize (GObject *obj) +{ + XbBuilderSource *self = XB_BUILDER_SOURCE (obj); + XbBuilderSourcePrivate *priv = GET_PRIVATE (self); + + if (priv->istream != NULL) + g_object_unref (priv->istream); + if (priv->info != NULL) + g_object_unref (priv->info); + if (priv->file != NULL) + g_object_unref (priv->file); + g_ptr_array_unref (priv->fixups); + g_ptr_array_unref (priv->adapters); + g_free (priv->guid); + g_free (priv->prefix); + g_free (priv->content_type); + + G_OBJECT_CLASS (xb_builder_source_parent_class)->finalize (obj); +} + +static void +xb_builder_source_class_init (XbBuilderSourceClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + object_class->finalize = xb_builder_source_finalize; +} + +static void +xb_builder_source_init (XbBuilderSource *self) +{ + XbBuilderSourcePrivate *priv = GET_PRIVATE (self); + priv->fixups = g_ptr_array_new_with_free_func ((GDestroyNotify) g_object_unref); + priv->adapters = g_ptr_array_new_with_free_func ((GDestroyNotify) xb_builder_source_adapter_free); + xb_builder_source_add_adapter (self, "application/gzip,application/x-gzip", + xb_builder_source_load_gzip_cb, NULL, NULL); +} + +/** + * xb_builder_source_new: + * + * Creates a new builder source. + * + * Returns: a new #XbBuilderSource + * + * Since: 0.1.1 + **/ +XbBuilderSource * +xb_builder_source_new (void) +{ + return g_object_new (XB_TYPE_BUILDER_SOURCE, NULL); +} diff --git a/src/xb-builder-source.h b/src/xb-builder-source.h new file mode 100644 index 0000000..c39a1a2 --- /dev/null +++ b/src/xb-builder-source.h @@ -0,0 +1,107 @@ +/* + * Copyright (C) 2018 Richard Hughes + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#pragma once + +#include + +#include "xb-builder-fixup.h" +#include "xb-builder-node.h" +#include "xb-builder-source-ctx.h" + +G_BEGIN_DECLS + +#define XB_TYPE_BUILDER_SOURCE (xb_builder_source_get_type ()) + +G_DECLARE_DERIVABLE_TYPE (XbBuilderSource, xb_builder_source, XB, BUILDER_SOURCE, GObject) + +struct _XbBuilderSourceClass { + GObjectClass parent_class; + /*< private >*/ + void (*_xb_reserved1) (void); + void (*_xb_reserved2) (void); + void (*_xb_reserved3) (void); + void (*_xb_reserved4) (void); + void (*_xb_reserved5) (void); + void (*_xb_reserved6) (void); + void (*_xb_reserved7) (void); +}; + +/** + * XbBuilderSourceFlags: + * @XB_BUILDER_SOURCE_FLAG_NONE: No extra flags to use + * @XB_BUILDER_SOURCE_FLAG_LITERAL_TEXT: Do not attempt to repair XML whitespace + * @XB_BUILDER_SOURCE_FLAG_WATCH_FILE: Watch the source file for changes + * + * The flags for converting to XML. + **/ +typedef enum { + XB_BUILDER_SOURCE_FLAG_NONE = 0, /* Since: 0.1.0 */ + XB_BUILDER_SOURCE_FLAG_LITERAL_TEXT = 1 << 0, /* Since: 0.1.0 */ + XB_BUILDER_SOURCE_FLAG_WATCH_FILE = 1 << 1, /* Since: 0.1.0 */ + /*< private >*/ + XB_BUILDER_SOURCE_FLAG_LAST +} XbBuilderSourceFlags; + +typedef gboolean (*XbBuilderSourceNodeFunc) (XbBuilderSource *self, + XbBuilderNode *bn, + gpointer user_data, + GError **error); +typedef GInputStream *(*XbBuilderSourceConverterFunc) (XbBuilderSource *self, + GFile *file, + gpointer user_data, + GCancellable *cancellable, + GError **error); +typedef GInputStream *(*XbBuilderSourceAdapterFunc) (XbBuilderSource *self, + XbBuilderSourceCtx *ctx, + gpointer user_data, + GCancellable *cancellable, + GError **error); + +XbBuilderSource *xb_builder_source_new (void); +gboolean xb_builder_source_load_file (XbBuilderSource *self, + GFile *file, + XbBuilderSourceFlags flags, + GCancellable *cancellable, + GError **error); +gboolean xb_builder_source_load_xml (XbBuilderSource *self, + const gchar *xml, + XbBuilderSourceFlags flags, + GError **error); +gboolean xb_builder_source_load_bytes (XbBuilderSource *self, + GBytes *bytes, + XbBuilderSourceFlags flags, + GError **error); +void xb_builder_source_set_info (XbBuilderSource *self, + XbBuilderNode *info); +void xb_builder_source_set_prefix (XbBuilderSource *self, + const gchar *prefix); +void xb_builder_source_add_node_func (XbBuilderSource *self, + const gchar *id, + XbBuilderSourceNodeFunc func, + gpointer user_data, + GDestroyNotify user_data_free) +G_DEPRECATED_FOR(xb_builder_source_add_fixup); +void xb_builder_source_add_fixup (XbBuilderSource *self, + XbBuilderFixup *fixup); +void xb_builder_source_add_converter (XbBuilderSource *self, + const gchar *content_types, + XbBuilderSourceConverterFunc func, + gpointer user_data, + GDestroyNotify user_data_free) +G_DEPRECATED_FOR(xb_builder_source_add_adapter); +void xb_builder_source_add_adapter (XbBuilderSource *self, + const gchar *content_types, + XbBuilderSourceAdapterFunc func, + gpointer user_data, + GDestroyNotify user_data_free); +void xb_builder_source_add_simple_adapter (XbBuilderSource *self, + const gchar *content_types, + XbBuilderSourceAdapterFunc func, + gpointer user_data, + GDestroyNotify user_data_free); + +G_END_DECLS diff --git a/src/xb-builder.c b/src/xb-builder.c new file mode 100644 index 0000000..6c47bbb --- /dev/null +++ b/src/xb-builder.c @@ -0,0 +1,1065 @@ +/* + * Copyright (C) 2018 Richard Hughes + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#define G_LOG_DOMAIN "XbSilo" + +#include "config.h" + +#include +#include + +#include "xb-silo-private.h" +#include "xb-string-private.h" +#include "xb-builder.h" +#include "xb-builder-fixup-private.h" +#include "xb-builder-source-private.h" +#include "xb-builder-node-private.h" + +typedef struct { + GObject parent_instance; + GPtrArray *sources; /* of XbBuilderSource */ + GPtrArray *nodes; /* of XbBuilderNode */ + GPtrArray *fixups; /* of XbBuilderFixup */ + GPtrArray *locales; /* of str */ + XbSilo *silo; + XbSiloProfileFlags profile_flags; + GString *guid; +} XbBuilderPrivate; + +G_DEFINE_TYPE_WITH_PRIVATE (XbBuilder, xb_builder, G_TYPE_OBJECT) +#define GET_PRIVATE(o) (xb_builder_get_instance_private (o)) + +#define XB_SILO_APPENDBUF(str,data,sz) g_string_append_len(str,(const gchar *)data, sz); + +typedef struct { + XbSilo *silo; + XbBuilderNode *root; /* transfer full */ + XbBuilderNode *current; /* transfer none */ + XbBuilderCompileFlags compile_flags; + XbBuilderSourceFlags source_flags; + GHashTable *strtab_hash; + GString *strtab; + GPtrArray *locales; +} XbBuilderCompileHelper; + +static guint32 +xb_builder_compile_add_to_strtab (XbBuilderCompileHelper *helper, const gchar *str) +{ + gpointer val; + guint32 idx; + + /* already exists */ + if (g_hash_table_lookup_extended (helper->strtab_hash, str, NULL, &val)) + return GPOINTER_TO_UINT (val); + + /* new */ + idx = helper->strtab->len; + XB_SILO_APPENDBUF (helper->strtab, str, strlen (str) + 1); + g_hash_table_insert (helper->strtab_hash, g_strdup (str), GUINT_TO_POINTER (idx)); + return idx; +} + +static gint +xb_builder_get_locale_priority (XbBuilderCompileHelper *helper, const gchar *locale) +{ + for (guint i = 0; i < helper->locales->len; i++) { + const gchar *locale_tmp = g_ptr_array_index (helper->locales, i); + if (g_strcmp0 (locale_tmp, locale) == 0) + return helper->locales->len - i; + } + return -1; +} + +static void +xb_builder_compile_start_element_cb (GMarkupParseContext *context, + const gchar *element_name, + const gchar **attr_names, + const gchar **attr_values, + gpointer user_data, + GError **error) +{ + XbBuilderCompileHelper *helper = (XbBuilderCompileHelper *) user_data; + g_autoptr(XbBuilderNode) bn = xb_builder_node_new (element_name); + + /* parent node is being ignored */ + if (helper->current != NULL && + xb_builder_node_has_flag (helper->current, XB_BUILDER_NODE_FLAG_IGNORE)) + xb_builder_node_add_flag (bn, XB_BUILDER_NODE_FLAG_IGNORE); + + /* check if we should ignore the locale */ + if (!xb_builder_node_has_flag (bn, XB_BUILDER_NODE_FLAG_IGNORE) && + helper->compile_flags & XB_BUILDER_COMPILE_FLAG_NATIVE_LANGS) { + const gchar *xml_lang = NULL; + for (guint i = 0; attr_names[i] != NULL; i++) { + if (g_strcmp0 (attr_names[i], "xml:lang") == 0) { + xml_lang = attr_values[i]; + break; + } + } + if (xml_lang == NULL) { + if (helper->current != NULL) { + gint prio = xb_builder_node_get_priority (helper->current); + xb_builder_node_set_priority (bn, prio); + } + } else { + gint prio = xb_builder_get_locale_priority (helper, xml_lang); + if (prio < 0) + xb_builder_node_add_flag (bn, XB_BUILDER_NODE_FLAG_IGNORE); + xb_builder_node_set_priority (bn, prio); + } + } + + /* add attributes */ + if (!xb_builder_node_has_flag (bn, XB_BUILDER_NODE_FLAG_IGNORE)) { + for (guint i = 0; attr_names[i] != NULL; i++) + xb_builder_node_set_attr (bn, attr_names[i], attr_values[i]); + } + + /* add to tree */ + xb_builder_node_add_child (helper->current, bn); + helper->current = bn; +} + +static void +xb_builder_compile_end_element_cb (GMarkupParseContext *context, + const gchar *element_name, + gpointer user_data, + GError **error) +{ + XbBuilderCompileHelper *helper = (XbBuilderCompileHelper *) user_data; + g_autoptr(XbBuilderNode) parent = xb_builder_node_get_parent (helper->current); + if (parent == NULL) { + g_set_error_literal (error, + G_IO_ERROR, + G_IO_ERROR_INVALID_DATA, + "Mismatched XML; no parent"); + return; + } + helper->current = parent; +} + +static void +xb_builder_compile_text_cb (GMarkupParseContext *context, + const gchar *text, + gsize text_len, + gpointer user_data, + GError **error) +{ + XbBuilderCompileHelper *helper = (XbBuilderCompileHelper *) user_data; + XbBuilderNode *bn = helper->current; + XbBuilderNode *bc = xb_builder_node_get_last_child (bn); + + /* unimportant */ + if (xb_builder_node_has_flag (bn, XB_BUILDER_NODE_FLAG_IGNORE)) + return; + + /* repair text unless we know it's valid */ + if (helper->source_flags & XB_BUILDER_SOURCE_FLAG_LITERAL_TEXT) + xb_builder_node_add_flag (bn, XB_BUILDER_NODE_FLAG_LITERAL_TEXT); + + /* text or tail */ + if (!xb_builder_node_has_flag (bn, XB_BUILDER_NODE_FLAG_HAS_TEXT)) { + xb_builder_node_set_text (bn, text, text_len); + return; + } + + /* does this node have a child */ + if (bc != NULL) { + xb_builder_node_set_tail (bc, text, text_len); + return; + } + if (!xb_builder_node_has_flag (bn, XB_BUILDER_NODE_FLAG_HAS_TAIL)) { + xb_builder_node_set_tail (bn, text, text_len); + return; + } + g_set_error (error, + G_IO_ERROR, + G_IO_ERROR_INVALID_DATA, + "Mismatched XML; cannot store %s", text); +} + +/** + * xb_builder_import_source: + * @self: a #XbSilo + * @source: a #XbBuilderSource + * + * Adds a #XbBuilderSource to the #XbBuilder. + * + * Since: 0.1.0 + **/ +void +xb_builder_import_source (XbBuilder *self, XbBuilderSource *source) +{ + XbBuilderPrivate *priv = GET_PRIVATE (self); + g_autofree gchar *guid = NULL; + + g_return_if_fail (XB_IS_BUILDER (self)); + g_return_if_fail (XB_IS_BUILDER_SOURCE (source)); + + /* get latest GUID */ + guid = xb_builder_source_get_guid (source); + xb_builder_append_guid (self, guid); + g_ptr_array_add (priv->sources, g_object_ref (source)); +} + +static gboolean +xb_builder_compile_source (XbBuilderCompileHelper *helper, + XbBuilderSource *source, + XbBuilderNode *root, + GCancellable *cancellable, + GError **error) +{ + GPtrArray *children; + XbBuilderNode *info; + gsize chunk_size = 32 * 1024; + gssize len; + g_autofree gchar *data = NULL; + g_autofree gchar *guid = xb_builder_source_get_guid (source); + g_autoptr(GPtrArray) children_copy = NULL; + g_autoptr(GInputStream) istream = NULL; + g_autoptr(GMarkupParseContext) ctx = NULL; + g_autoptr(GTimer) timer = g_timer_new (); + g_autoptr(XbBuilderNode) root_tmp = xb_builder_node_new (NULL); + const GMarkupParser parser = { + xb_builder_compile_start_element_cb, + xb_builder_compile_end_element_cb, + xb_builder_compile_text_cb, + NULL, NULL }; + + /* add the source to a fake root in case it fails during processing */ + helper->current = root_tmp; + helper->source_flags = xb_builder_source_get_flags (source); + + /* decompress */ + istream = xb_builder_source_get_istream (source, cancellable, error); + if (istream == NULL) + return FALSE; + + /* parse */ + ctx = g_markup_parse_context_new (&parser, G_MARKUP_PREFIX_ERROR_POSITION, helper, NULL); + data = g_malloc (chunk_size); + while ((len = g_input_stream_read (istream, + data, + chunk_size, + cancellable, + error)) > 0) { + if (!g_markup_parse_context_parse (ctx, data, len, error)) + return FALSE; + } + if (len < 0) + return FALSE; + + /* more opening than closing */ + if (root_tmp != helper->current) { + g_set_error_literal (error, + G_IO_ERROR, + G_IO_ERROR_INVALID_DATA, + "Mismatched XML"); + return FALSE; + } + + /* run any node functions */ + if (!xb_builder_source_fixup (source, root_tmp, error)) + return FALSE; + + /* this is something we can query with later */ + info = xb_builder_source_get_info (source); + if (info != NULL) { + children = xb_builder_node_get_children (helper->current); + for (guint i = 0; i < children->len; i++) { + XbBuilderNode *bn = g_ptr_array_index (children, i); + xb_builder_node_add_child (bn, info); + } + } + + /* add the children to the main document */ + children = xb_builder_node_get_children (root_tmp); + children_copy = g_ptr_array_new_with_free_func ((GDestroyNotify) g_object_unref); + for (guint i = 0; i < children->len; i++) { + XbBuilderNode *bn = g_ptr_array_index (children, i); + g_ptr_array_add (children_copy, g_object_ref (bn)); + } + for (guint i = 0; i < children_copy->len; i++) { + XbBuilderNode *bn = g_ptr_array_index (children_copy, i); + xb_builder_node_unlink (bn); + xb_builder_node_add_child (root, bn); + } + + /* success */ + xb_silo_add_profile (helper->silo, timer, "compile %s", guid); + return TRUE; +} + +static gboolean +xb_builder_strtab_element_names_cb (XbBuilderNode *bn, gpointer user_data) +{ + XbBuilderCompileHelper *helper = (XbBuilderCompileHelper *) user_data; + const gchar *tmp; + + /* root node */ + if (xb_builder_node_get_element (bn) == NULL) + return FALSE; + if (xb_builder_node_has_flag (bn, XB_BUILDER_NODE_FLAG_IGNORE)) + return FALSE; + tmp = xb_builder_node_get_element (bn); + xb_builder_node_set_element_idx (bn, xb_builder_compile_add_to_strtab (helper, tmp)); + return FALSE; +} + +static gboolean +xb_builder_strtab_attr_name_cb (XbBuilderNode *bn, gpointer user_data) +{ + GPtrArray *attrs; + XbBuilderCompileHelper *helper = (XbBuilderCompileHelper *) user_data; + + /* root node */ + if (xb_builder_node_get_element (bn) == NULL) + return FALSE; + if (xb_builder_node_has_flag (bn, XB_BUILDER_NODE_FLAG_IGNORE)) + return FALSE; + attrs = xb_builder_node_get_attrs (bn); + for (guint i = 0; i < attrs->len; i++) { + XbBuilderNodeAttr *attr = g_ptr_array_index (attrs, i); + attr->name_idx = xb_builder_compile_add_to_strtab (helper, attr->name); + } + return FALSE; +} + +static gboolean +xb_builder_strtab_attr_value_cb (XbBuilderNode *bn, gpointer user_data) +{ + GPtrArray *attrs; + XbBuilderCompileHelper *helper = (XbBuilderCompileHelper *) user_data; + + /* root node */ + if (xb_builder_node_get_element (bn) == NULL) + return FALSE; + if (xb_builder_node_has_flag (bn, XB_BUILDER_NODE_FLAG_IGNORE)) + return FALSE; + attrs = xb_builder_node_get_attrs (bn); + for (guint i = 0; i < attrs->len; i++) { + XbBuilderNodeAttr *attr = g_ptr_array_index (attrs, i); + attr->value_idx = xb_builder_compile_add_to_strtab (helper, attr->value); + } + return FALSE; +} + +static gboolean +xb_builder_strtab_text_cb (XbBuilderNode *bn, gpointer user_data) +{ + XbBuilderCompileHelper *helper = (XbBuilderCompileHelper *) user_data; + const gchar *tmp; + + /* root node */ + if (xb_builder_node_get_element (bn) == NULL) + return FALSE; + if (xb_builder_node_has_flag (bn, XB_BUILDER_NODE_FLAG_IGNORE)) + return FALSE; + if (xb_builder_node_get_text (bn) != NULL) { + tmp = xb_builder_node_get_text (bn); + xb_builder_node_set_text_idx (bn, xb_builder_compile_add_to_strtab (helper, tmp)); + } + if (xb_builder_node_get_tail (bn) != NULL) { + tmp = xb_builder_node_get_tail (bn); + xb_builder_node_set_tail_idx (bn, xb_builder_compile_add_to_strtab (helper, tmp)); + } + return FALSE; +} + +static gboolean +xb_builder_xml_lang_prio_cb (XbBuilderNode *bn, gpointer user_data) +{ + GPtrArray *nodes_to_destroy = (GPtrArray *) user_data; + gint prio_best = 0; + g_autoptr(GPtrArray) nodes = g_ptr_array_new (); + GPtrArray *siblings; + g_autoptr(XbBuilderNode) parent = xb_builder_node_get_parent (bn); + + /* root node */ + if (xb_builder_node_get_element (bn) == NULL) + return FALSE; + + /* already ignored */ + if (xb_builder_node_get_priority (bn) == -2) + return FALSE; + + /* get all the siblings with the same name */ + siblings = xb_builder_node_get_children (parent); + for (guint i = 0; i < siblings->len; i++) { + XbBuilderNode *bn2 = g_ptr_array_index (siblings, i); + if (g_strcmp0 (xb_builder_node_get_element (bn), + xb_builder_node_get_element (bn2)) == 0) + g_ptr_array_add (nodes, bn2); + } + + /* only one thing, so bail early */ + if (nodes->len == 1) + return FALSE; + + /* find the best locale */ + for (guint i = 0; i < nodes->len; i++) { + XbBuilderNode *bn2 = g_ptr_array_index (nodes, i); + if (xb_builder_node_get_priority (bn2) > prio_best) + prio_best = xb_builder_node_get_priority (bn2); + } + + /* add any nodes not as good as the bext locale to the kill list */ + for (guint i = 0; i < nodes->len; i++) { + XbBuilderNode *bn2 = g_ptr_array_index (nodes, i); + if (xb_builder_node_get_priority (bn2) < prio_best) + g_ptr_array_add (nodes_to_destroy, bn2); + + /* never visit this node again */ + xb_builder_node_set_priority (bn2, -2); + } + + return FALSE; +} + +static gboolean +xb_builder_nodetab_size_cb (XbBuilderNode *bn, gpointer user_data) +{ + guint32 *sz = (guint32 *) user_data; + + /* root node */ + if (xb_builder_node_get_element (bn) == NULL) + return FALSE; + if (xb_builder_node_has_flag (bn, XB_BUILDER_NODE_FLAG_IGNORE)) + return FALSE; + *sz += xb_builder_node_size (bn) + 1; /* +1 for the sentinel */ + return FALSE; +} + +typedef struct { + GString *buf; +} XbBuilderNodetabHelper; + +static void +xb_builder_nodetab_write_sentinel (XbBuilderNodetabHelper *helper) +{ + XbSiloNode sn = { + .is_node = FALSE, + .nr_attrs = 0, + }; +// g_debug ("SENT @%u", (guint) helper->buf->len); + XB_SILO_APPENDBUF (helper->buf, &sn, xb_silo_node_get_size (&sn)); +} + +static void +xb_builder_nodetab_write_node (XbBuilderNodetabHelper *helper, XbBuilderNode *bn) +{ + GPtrArray *attrs = xb_builder_node_get_attrs (bn); + XbSiloNode sn = { + .is_node = TRUE, + .nr_attrs = attrs->len, + .element_name = xb_builder_node_get_element_idx (bn), + .next = 0x0, + .parent = 0x0, + .text = xb_builder_node_get_text_idx (bn), + .tail = xb_builder_node_get_tail_idx (bn), + }; + + /* if the node had no children and the text is just whitespace then + * remove it even in literal mode */ + if (xb_builder_node_has_flag (bn, XB_BUILDER_NODE_FLAG_LITERAL_TEXT)) { + if (xb_string_isspace (xb_builder_node_get_text (bn), -1)) + sn.text = XB_SILO_UNSET; + if (xb_string_isspace (xb_builder_node_get_tail (bn), -1)) + sn.tail = XB_SILO_UNSET; + } + + /* save this so we can set up the ->next pointers correctly */ + xb_builder_node_set_offset (bn, helper->buf->len); + +// g_debug ("NODE @%u (%s)", (guint) helper->buf->len, xb_builder_node_get_element (bn)); + + /* add to the buf */ + XB_SILO_APPENDBUF (helper->buf, &sn, sizeof(XbSiloNode)); + + /* add to the buf */ + for (guint i = 0; i < attrs->len; i++) { + XbBuilderNodeAttr *ba = g_ptr_array_index (attrs, i); + XbSiloAttr attr = { + .attr_name = ba->name_idx, + .attr_value = ba->value_idx, + }; + XB_SILO_APPENDBUF (helper->buf, &attr, sizeof(attr)); + } +} + +static void +xb_builder_nodetab_write (XbBuilderNodetabHelper *helper, XbBuilderNode *bn) +{ + GPtrArray *children; + + /* ignore this */ + if (xb_builder_node_has_flag (bn, XB_BUILDER_NODE_FLAG_IGNORE)) + return; + + /* element */ + if (xb_builder_node_get_element (bn) != NULL) + xb_builder_nodetab_write_node (helper, bn); + + /* children */ + children = xb_builder_node_get_children (bn); + for (guint i = 0; i < children->len; i++) { + XbBuilderNode *bc = g_ptr_array_index (children, i); + xb_builder_nodetab_write (helper, bc); + } + + /* sentinel */ + if (xb_builder_node_get_element (bn) != NULL) + xb_builder_nodetab_write_sentinel (helper); +} + + +static XbSiloNode * +xb_builder_get_node (GString *str, guint32 off) +{ + return (XbSiloNode *) (str->str + off); +} + +static gboolean +xb_builder_nodetab_fix_cb (XbBuilderNode *bn, gpointer user_data) +{ + GPtrArray *siblings; + XbBuilderNodetabHelper *helper = (XbBuilderNodetabHelper *) user_data; + XbSiloNode *sn; + gboolean found = FALSE; + g_autoptr(XbBuilderNode) parent = xb_builder_node_get_parent (bn); + + /* root node */ + if (xb_builder_node_get_element (bn) == NULL) + return FALSE; + if (xb_builder_node_has_flag (bn, XB_BUILDER_NODE_FLAG_IGNORE)) + return FALSE; + + /* get the position in the buffer */ + sn = xb_builder_get_node (helper->buf, xb_builder_node_get_offset (bn)); + if (sn == NULL) + return FALSE; + + /* set the parent if the node has one */ + if (xb_builder_node_get_element (parent) != NULL) + sn->parent = xb_builder_node_get_offset (parent); + + /* set ->next if the node has one */ + siblings = xb_builder_node_get_children (parent); + for (guint i = 0; i < siblings->len; i++) { + XbBuilderNode *bn2 = g_ptr_array_index (siblings, i); + if (bn2 == bn) { + found = TRUE; + continue; + } + if (!found) + continue; + if (!xb_builder_node_has_flag (bn2, XB_BUILDER_NODE_FLAG_IGNORE)) { + sn->next = xb_builder_node_get_offset (bn2); + break; + } + } + + return FALSE; +} + +static void +xb_builder_compile_helper_free (XbBuilderCompileHelper *helper) +{ + g_hash_table_unref (helper->strtab_hash); + g_string_free (helper->strtab, TRUE); + g_object_unref (helper->root); + g_free (helper); +} + +G_DEFINE_AUTOPTR_CLEANUP_FUNC(XbBuilderCompileHelper, xb_builder_compile_helper_free) + +static gchar * +xb_builder_generate_guid (XbBuilder *self) +{ + XbBuilderPrivate *priv = GET_PRIVATE (self); + XbGuid guid = { 0x0 }; + if (priv->guid->len > 0) { + xb_guid_compute_for_data (&guid, + (const guint8 *) priv->guid->str, + priv->guid->len); + } + return xb_guid_to_string (&guid); +} + +/** + * xb_builder_import_node: + * @self: a #XbSilo + * @bn: a #XbBuilderNode + * + * Adds a node tree to the builder. + * + * Since: 0.1.0 + **/ +void +xb_builder_import_node (XbBuilder *self, XbBuilderNode *bn) +{ + XbBuilderPrivate *priv = GET_PRIVATE (self); + g_autofree gchar *guid = g_strdup_printf ("bn@%p", bn); + g_return_if_fail (XB_IS_BUILDER (self)); + g_return_if_fail (XB_IS_BUILDER_NODE (bn)); + g_ptr_array_add (priv->nodes, g_object_ref (bn)); + xb_builder_append_guid (self, guid); +} + +/** + * xb_builder_add_locale: + * @self: a #XbSilo + * @locale: a locale, e.g. "en_US" + * + * Adds a locale to the builder. Locales added first will be prioritised over + * locales added later. + * + * Since: 0.1.0 + **/ +void +xb_builder_add_locale (XbBuilder *self, const gchar *locale) +{ + XbBuilderPrivate *priv = GET_PRIVATE (self); + + g_return_if_fail (XB_IS_BUILDER (self)); + g_return_if_fail (locale != NULL); + + if (g_str_has_suffix (locale, ".UTF-8")) + return; + for (guint i = 0; i < priv->locales->len; i++) { + const gchar *locale_tmp = g_ptr_array_index (priv->locales, i); + if (g_strcmp0 (locale_tmp, locale) == 0) + return; + } + g_ptr_array_add (priv->locales, g_strdup (locale)); + + /* if the user changes LANG, the blob is no longer valid */ + xb_builder_append_guid (self, locale); +} + +static gboolean +xb_builder_watch_source (XbBuilder *self, + XbBuilderSource *source, + GCancellable *cancellable, + GError **error) +{ + XbBuilderPrivate *priv = GET_PRIVATE (self); + GFile *file = xb_builder_source_get_file (source); + if (file == NULL) + return TRUE; + if ((xb_builder_source_get_flags (source) & XB_BUILDER_SOURCE_FLAG_WATCH_FILE) == 0) + return TRUE; + if (!xb_silo_watch_file (priv->silo, file, cancellable, error)) + return FALSE; + return TRUE; +} + +static gboolean +xb_builder_watch_sources (XbBuilder *self, GCancellable *cancellable, GError **error) +{ + XbBuilderPrivate *priv = GET_PRIVATE (self); + for (guint i = 0; i < priv->sources->len; i++) { + XbBuilderSource *source = g_ptr_array_index (priv->sources, i); + if (!xb_builder_watch_source (self, source, cancellable, error)) + return FALSE; + } + return TRUE; +} + +/** + * xb_builder_compile: + * @self: a #XbSilo + * @flags: some #XbBuilderCompileFlags, e.g. %XB_BUILDER_SOURCE_FLAG_LITERAL_TEXT + * @cancellable: a #GCancellable, or %NULL + * @error: the #GError, or %NULL + * + * Compiles a #XbSilo. + * + * Returns: (transfer full): a #XbSilo, or %NULL for error + * + * Since: 0.1.0 + **/ +XbSilo * +xb_builder_compile (XbBuilder *self, XbBuilderCompileFlags flags, GCancellable *cancellable, GError **error) +{ + XbBuilderPrivate *priv = GET_PRIVATE (self); + guint32 nodetabsz = sizeof(XbSiloHeader); + g_autoptr(GBytes) blob = NULL; + g_autoptr(GString) buf = NULL; + XbSiloHeader hdr = { + .magic = XB_SILO_MAGIC_BYTES, + .version = XB_SILO_VERSION, + .strtab = 0, + .strtab_ntags = 0, + .padding = { 0x0 }, + .guid = { 0x0 }, + }; + XbBuilderNodetabHelper nodetab_helper = { + .buf = NULL, + }; + g_autoptr(GPtrArray) nodes_to_destroy = g_ptr_array_new (); + g_autoptr(GTimer) timer = g_timer_new (); + g_autoptr(XbBuilderCompileHelper) helper = NULL; + + g_return_val_if_fail (XB_IS_BUILDER (self), NULL); + g_return_val_if_fail (cancellable == NULL || G_IS_CANCELLABLE (cancellable), NULL); + g_return_val_if_fail (error == NULL || *error == NULL, NULL); + + /* this is inferred */ + if (flags & XB_BUILDER_COMPILE_FLAG_SINGLE_LANG) + flags |= XB_BUILDER_COMPILE_FLAG_NATIVE_LANGS; + + /* the builder needs to know the locales */ + if (priv->locales->len == 0 && (flags & XB_BUILDER_COMPILE_FLAG_NATIVE_LANGS)) { + g_set_error_literal (error, + G_IO_ERROR, + G_IO_ERROR_INVALID_DATA, + "No locales set and using NATIVE_LANGS"); + return NULL; + } + + /* create helper used for compiling */ + helper = g_new0 (XbBuilderCompileHelper, 1); + helper->compile_flags = flags; + helper->root = xb_builder_node_new (NULL); + helper->silo = priv->silo; + helper->locales = priv->locales; + helper->strtab = g_string_new (NULL); + helper->strtab_hash = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL); + + /* build node tree */ + for (guint i = 0; i < priv->sources->len; i++) { + XbBuilderSource *source = g_ptr_array_index (priv->sources, i); + const gchar *prefix = xb_builder_source_get_prefix (source); + g_autofree gchar *source_guid = xb_builder_source_get_guid (source); + g_autoptr(XbBuilderNode) root = NULL; + g_autoptr(GError) error_local = NULL; + + /* find, or create the prefix */ + if (prefix != NULL) { + root = xb_builder_node_get_child (helper->root, prefix, NULL); + if (root == NULL) + root = xb_builder_node_insert (helper->root, prefix, NULL); + } else { + /* don't allow damaged XML files to ruin all the next ones */ + root = g_object_ref (helper->root); + } + + g_debug ("compiling %s…", source_guid); + if (!xb_builder_compile_source (helper, source, root, + cancellable, &error_local)) { + if (flags & XB_BUILDER_COMPILE_FLAG_IGNORE_INVALID) { + g_debug ("ignoring invalid file %s: %s", + source_guid, + error_local->message); + continue; + } + g_propagate_prefixed_error (error, + g_steal_pointer (&error_local), + "failed to compile %s: ", + source_guid); + return NULL; + } + + /* watch the source */ + if (!xb_builder_watch_source (self, source, cancellable, error)) + return NULL; + } + + /* run any node functions */ + for (guint i = 0; i < priv->fixups->len; i++) { + XbBuilderFixup *fixup = g_ptr_array_index (priv->fixups, i); + if (!xb_builder_fixup_node (fixup, helper->root, error)) + return NULL; + } + + /* only include the highest priority translation */ + if (flags & XB_BUILDER_COMPILE_FLAG_SINGLE_LANG) { + xb_builder_node_traverse (helper->root, G_PRE_ORDER, G_TRAVERSE_ALL, -1, + xb_builder_xml_lang_prio_cb, nodes_to_destroy); + for (guint i = 0; i < nodes_to_destroy->len; i++) { + XbBuilderNode *bn = g_ptr_array_index (nodes_to_destroy, i); + xb_builder_node_unlink (bn); + } + xb_silo_add_profile (priv->silo, timer, "filter single-lang"); + } + + /* add any manually build nodes */ + for (guint i = 0; i < priv->nodes->len; i++) { + XbBuilderNode *bn = g_ptr_array_index (priv->nodes, i); + xb_builder_node_add_child (helper->root, bn); + } + + /* get the size of the nodetab */ + xb_builder_node_traverse (helper->root, G_PRE_ORDER, G_TRAVERSE_ALL, -1, + xb_builder_nodetab_size_cb, &nodetabsz); + buf = g_string_sized_new (nodetabsz); + xb_silo_add_profile (priv->silo, timer, "get size nodetab"); + + /* add element names, attr name, attr value, then text to the strtab */ + xb_builder_node_traverse (helper->root, G_PRE_ORDER, G_TRAVERSE_ALL, -1, + xb_builder_strtab_element_names_cb, helper); + hdr.strtab_ntags = g_hash_table_size (helper->strtab_hash); + xb_silo_add_profile (priv->silo, timer, "adding strtab element"); + xb_builder_node_traverse (helper->root, G_PRE_ORDER, G_TRAVERSE_ALL, -1, + xb_builder_strtab_attr_name_cb, helper); + xb_silo_add_profile (priv->silo, timer, "adding strtab attr name"); + xb_builder_node_traverse (helper->root, G_PRE_ORDER, G_TRAVERSE_ALL, -1, + xb_builder_strtab_attr_value_cb, helper); + xb_silo_add_profile (priv->silo, timer, "adding strtab attr value"); + xb_builder_node_traverse (helper->root, G_PRE_ORDER, G_TRAVERSE_ALL, -1, + xb_builder_strtab_text_cb, helper); + xb_silo_add_profile (priv->silo, timer, "adding strtab text"); + + /* add the initial header */ + hdr.strtab = nodetabsz; + if (priv->guid->len > 0) { + XbGuid guid_tmp; + xb_guid_compute_for_data (&guid_tmp, + (const guint8 *) priv->guid->str, + priv->guid->len); + memcpy (&hdr.guid, &guid_tmp, sizeof(guid_tmp)); + } + XB_SILO_APPENDBUF (buf, &hdr, sizeof(XbSiloHeader)); + + /* write nodes to the nodetab */ + nodetab_helper.buf = buf; + xb_builder_nodetab_write (&nodetab_helper, helper->root); + xb_silo_add_profile (priv->silo, timer, "writing nodetab"); + + /* set all the ->next and ->parent offsets */ + xb_builder_node_traverse (helper->root, G_PRE_ORDER, G_TRAVERSE_ALL, -1, + xb_builder_nodetab_fix_cb, &nodetab_helper); + xb_silo_add_profile (priv->silo, timer, "fixing ->parent and ->next"); + + /* append the string table */ + XB_SILO_APPENDBUF (buf, helper->strtab->str, helper->strtab->len); + xb_silo_add_profile (priv->silo, timer, "appending strtab"); + + /* create data */ + blob = g_bytes_new (buf->str, buf->len); + if (!xb_silo_load_from_bytes (priv->silo, blob, XB_SILO_LOAD_FLAG_NONE, error)) + return NULL; + + /* success */ + return g_object_ref (priv->silo); +} + +/** + * xb_builder_ensure: + * @self: a #XbSilo + * @file: a #GFile + * @flags: some #XbBuilderCompileFlags, e.g. %XB_BUILDER_COMPILE_FLAG_IGNORE_INVALID + * @cancellable: a #GCancellable, or %NULL + * @error: the #GError, or %NULL + * + * Ensures @file is up to date, and returns a compiled #XbSilo. + * + * If @silo is being used by a query (e.g. in another thread) then all node + * data is immediately invalid. + * + * Returns: (transfer full): a #XbSilo, or %NULL for error + * + * Since: 0.1.0 + **/ +XbSilo * +xb_builder_ensure (XbBuilder *self, GFile *file, XbBuilderCompileFlags flags, + GCancellable *cancellable, GError **error) +{ + XbBuilderPrivate *priv = GET_PRIVATE (self); + XbSiloLoadFlags load_flags = XB_SILO_LOAD_FLAG_NONE; + g_autofree gchar *fn = NULL; + g_autoptr(XbSilo) silo_tmp = xb_silo_new (); + g_autoptr(XbSilo) silo_new = NULL; + g_autoptr(GError) error_local = NULL; + + g_return_val_if_fail (XB_IS_BUILDER (self), NULL); + g_return_val_if_fail (G_IS_FILE (file), NULL); + g_return_val_if_fail (cancellable == NULL || G_IS_CANCELLABLE (cancellable), NULL); + g_return_val_if_fail (error == NULL || *error == NULL, NULL); + + /* watch the blob, so propagate flags */ + if (flags & XB_BUILDER_COMPILE_FLAG_WATCH_BLOB) + load_flags |= XB_SILO_LOAD_FLAG_WATCH_BLOB; + + /* profile new silo if needed */ + xb_silo_set_profile_flags (silo_tmp, priv->profile_flags); + + /* load the file and peek at the GUIDs */ + fn = g_file_get_path (file); + g_debug ("attempting to load %s", fn); + if (!xb_silo_load_from_file (silo_tmp, file, + XB_SILO_LOAD_FLAG_NONE, + cancellable, + &error_local)) { + g_debug ("failed to load silo: %s", error_local->message); + } else { + g_autofree gchar *guid = xb_builder_generate_guid (self); + g_debug ("file: %s, current:%s, cached: %s", + xb_silo_get_guid (silo_tmp), guid, + xb_silo_get_guid (priv->silo)); + + /* GUIDs match exactly with the thing that's already loaded */ + if (g_strcmp0 (xb_silo_get_guid (silo_tmp), + xb_silo_get_guid (priv->silo)) == 0) { + g_debug ("returning unchanged silo"); + xb_silo_uninvalidate (priv->silo); + return g_object_ref (priv->silo); + } + + /* reload the cached silo with the new file data */ + if (g_strcmp0 (xb_silo_get_guid (silo_tmp), guid) == 0 || + (flags & XB_BUILDER_COMPILE_FLAG_IGNORE_GUID) > 0) { + g_autoptr(GBytes) blob = xb_silo_get_bytes (silo_tmp); + g_debug ("loading silo with file contents"); + if (!xb_silo_load_from_bytes (priv->silo, blob, + load_flags, error)) + return NULL; + + /* ensure all the sources are watched */ + if (!xb_builder_watch_sources (self, cancellable, error)) + return NULL; + + /* ensure backing file is watched for changes */ + if (flags & XB_BUILDER_COMPILE_FLAG_WATCH_BLOB) { + if (!xb_silo_watch_file (priv->silo, file, + cancellable, error)) + return NULL; + } + return g_object_ref (priv->silo); + } + } + + /* fallback to just creating a new file */ + silo_new = xb_builder_compile (self, flags, cancellable, error); + if (silo_new == NULL) + return NULL; + if (!xb_silo_save_to_file (silo_new, file, NULL, error)) + return NULL; + + /* load from a file to re-mmap it */ + if (!xb_silo_load_from_file (priv->silo, file, load_flags, cancellable, error)) + return NULL; + + /* ensure all the sources are watched on the reloaded silo */ + if (!xb_builder_watch_sources (self, cancellable, error)) + return NULL; + + /* success */ + return g_steal_pointer (&silo_new); +} + +/** + * xb_builder_append_guid: + * @self: a #XbSilo + * @guid: any text, typcically a filename or GUID + * + * Adds the GUID to the internal correctness hash. + * + * Since: 0.1.0 + **/ +void +xb_builder_append_guid (XbBuilder *self, const gchar *guid) +{ + XbBuilderPrivate *priv = GET_PRIVATE (self); + if (priv->guid->len > 0) + g_string_append (priv->guid, "&"); + g_string_append (priv->guid, guid); +} + +/** + * xb_builder_set_profile_flags: + * @self: a #XbBuilder + * @profile_flags: some #XbSiloProfileFlags, e.g. %XB_SILO_PROFILE_FLAG_DEBUG + * + * Enables or disables the collection of profiling data. + * + * Since: 0.1.1 + **/ +void +xb_builder_set_profile_flags (XbBuilder *self, XbSiloProfileFlags profile_flags) +{ + XbBuilderPrivate *priv = GET_PRIVATE (self); + g_return_if_fail (XB_IS_BUILDER (self)); + priv->profile_flags = profile_flags; + xb_silo_set_profile_flags (priv->silo, profile_flags); +} + +/** + * xb_builder_add_fixup: + * @self: a #XbBuilder + * @fixup: a #XbBuilderFixup + * + * Adds a function that will get run on every #XbBuilderNode compile creates + * for the silo. This is run after all the #XbBuilderSource fixups have been + * run. + * + * Since: 0.1.3 + **/ +void +xb_builder_add_fixup (XbBuilder *self, XbBuilderFixup *fixup) +{ + XbBuilderPrivate *priv = GET_PRIVATE (self); + g_autofree gchar *guid = NULL; + + g_return_if_fail (XB_IS_BUILDER (self)); + g_return_if_fail (XB_IS_BUILDER_FIXUP (fixup)); + + /* append function IDs */ + guid = xb_builder_fixup_get_guid (fixup); + xb_builder_append_guid (self, guid); + g_ptr_array_add (priv->fixups, g_object_ref (fixup)); +} + +static void +xb_builder_finalize (GObject *obj) +{ + XbBuilder *self = XB_BUILDER (obj); + XbBuilderPrivate *priv = GET_PRIVATE (self); + + g_ptr_array_unref (priv->sources); + g_ptr_array_unref (priv->nodes); + g_ptr_array_unref (priv->locales); + g_ptr_array_unref (priv->fixups); + g_object_unref (priv->silo); + g_string_free (priv->guid, TRUE); + + G_OBJECT_CLASS (xb_builder_parent_class)->finalize (obj); +} + +static void +xb_builder_class_init (XbBuilderClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + object_class->finalize = xb_builder_finalize; +} + +static void +xb_builder_init (XbBuilder *self) +{ + XbBuilderPrivate *priv = GET_PRIVATE (self); + priv->sources = g_ptr_array_new_with_free_func ((GDestroyNotify) g_object_unref); + priv->nodes = g_ptr_array_new_with_free_func ((GDestroyNotify) g_object_unref); + priv->fixups = g_ptr_array_new_with_free_func ((GDestroyNotify) g_object_unref); + priv->locales = g_ptr_array_new_with_free_func (g_free); + priv->silo = xb_silo_new (); + priv->guid = g_string_new (NULL); +} + +/** + * xb_builder_new: + * + * Creates a new builder. + * + * Returns: a new #XbBuilder + * + * Since: 0.1.0 + **/ +XbBuilder * +xb_builder_new (void) +{ + return g_object_new (XB_TYPE_BUILDER, NULL); +} diff --git a/src/xb-builder.h b/src/xb-builder.h new file mode 100644 index 0000000..149f4ae --- /dev/null +++ b/src/xb-builder.h @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2018 Richard Hughes + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#pragma once + +#include + +#include "xb-builder-fixup.h" +#include "xb-builder-source.h" +#include "xb-builder-node.h" +#include "xb-silo.h" + +G_BEGIN_DECLS + +#define XB_TYPE_BUILDER (xb_builder_get_type ()) + +G_DECLARE_DERIVABLE_TYPE (XbBuilder, xb_builder, XB, BUILDER, GObject) + +struct _XbBuilderClass { + GObjectClass parent_class; + /*< private >*/ + void (*_xb_reserved1) (void); + void (*_xb_reserved2) (void); + void (*_xb_reserved3) (void); + void (*_xb_reserved4) (void); + void (*_xb_reserved5) (void); + void (*_xb_reserved6) (void); + void (*_xb_reserved7) (void); +}; + +/** + * XbBuilderCompileFlags: + * @XB_BUILDER_COMPILE_FLAG_NONE: No extra flags to use + * @XB_BUILDER_COMPILE_FLAG_NATIVE_LANGS: Only load native languages + * @XB_BUILDER_COMPILE_FLAG_IGNORE_INVALID: Ignore invalid files without an error + * @XB_BUILDER_COMPILE_FLAG_SINGLE_LANG: Only store a single language + * @XB_BUILDER_COMPILE_FLAG_WATCH_BLOB: Watch the XMLB file for changes + * @XB_BUILDER_COMPILE_FLAG_IGNORE_GUID: Ignore the cache GUID value + * + * The flags for converting to XML. + **/ +typedef enum { + XB_BUILDER_COMPILE_FLAG_NONE = 0, /* Since: 0.1.0 */ + XB_BUILDER_COMPILE_FLAG_NATIVE_LANGS = 1 << 1, /* Since: 0.1.0 */ + XB_BUILDER_COMPILE_FLAG_IGNORE_INVALID = 1 << 2, /* Since: 0.1.0 */ + XB_BUILDER_COMPILE_FLAG_SINGLE_LANG = 1 << 3, /* Since: 0.1.0 */ + XB_BUILDER_COMPILE_FLAG_WATCH_BLOB = 1 << 4, /* Since: 0.1.0 */ + XB_BUILDER_COMPILE_FLAG_IGNORE_GUID = 1 << 5, /* Since: 0.1.7 */ + /*< private >*/ + XB_BUILDER_COMPILE_FLAG_LAST +} XbBuilderCompileFlags; + +XbBuilder *xb_builder_new (void); +void xb_builder_append_guid (XbBuilder *self, + const gchar *guid); +void xb_builder_import_source (XbBuilder *self, + XbBuilderSource *source); +void xb_builder_import_node (XbBuilder *self, + XbBuilderNode *bn); +XbSilo *xb_builder_compile (XbBuilder *self, + XbBuilderCompileFlags flags, + GCancellable *cancellable, + GError **error); +XbSilo *xb_builder_ensure (XbBuilder *self, + GFile *file, + XbBuilderCompileFlags flags, + GCancellable *cancellable, + GError **error); +void xb_builder_add_locale (XbBuilder *self, + const gchar *locale); +void xb_builder_add_fixup (XbBuilder *self, + XbBuilderFixup *fixup); +void xb_builder_set_profile_flags (XbBuilder *self, + XbSiloProfileFlags profile_flags); + +G_END_DECLS diff --git a/src/xb-machine.c b/src/xb-machine.c new file mode 100644 index 0000000..30888f3 --- /dev/null +++ b/src/xb-machine.c @@ -0,0 +1,1954 @@ +/* + * Copyright (C) 2018 Richard Hughes + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#define G_LOG_DOMAIN "XbMachine" + +#include "config.h" + +#include +#include + +#if !GLIB_CHECK_VERSION(2,54,0) +#include +#endif + +#include "xb-machine.h" +#include "xb-opcode-private.h" +#include "xb-silo-private.h" +#include "xb-stack-private.h" +#include "xb-string-private.h" + +typedef struct { + GObject parent_instance; + XbMachineDebugFlags debug_flags; + GPtrArray *methods; /* of XbMachineMethodItem */ + GPtrArray *operators; /* of XbMachineOperator */ + GPtrArray *text_handlers; /* of XbMachineTextHandlerItem */ + GHashTable *opcode_fixup; /* of str[XbMachineOpcodeFixupItem] */ + guint stack_size; +} XbMachinePrivate; + +G_DEFINE_TYPE_WITH_PRIVATE (XbMachine, xb_machine, G_TYPE_OBJECT) +#define GET_PRIVATE(o) (xb_machine_get_instance_private (o)) + +typedef struct { + gchar *str; + gsize strsz; + gchar *name; +} XbMachineOperator; + +typedef struct { + XbMachineOpcodeFixupFunc fixup_cb; + gpointer user_data; + GDestroyNotify user_data_free; +} XbMachineOpcodeFixupItem; + +typedef struct { + XbMachineTextHandlerFunc handler_cb; + gpointer user_data; + GDestroyNotify user_data_free; +} XbMachineTextHandlerItem; + +typedef struct { + guint32 idx; + gchar *name; + guint n_opcodes; + XbMachineMethodFunc method_cb; + gpointer user_data; + GDestroyNotify user_data_free; +} XbMachineMethodItem; + +/** + * xb_machine_set_debug_flags: + * @self: a #XbMachine + * @flags: #XbMachineDebugFlags, e.g. %XB_MACHINE_DEBUG_FLAG_SHOW_STACK + * + * Sets the debug level of the virtual machine. + * + * Since: 0.1.1 + **/ +void +xb_machine_set_debug_flags (XbMachine *self, XbMachineDebugFlags flags) +{ + XbMachinePrivate *priv = GET_PRIVATE (self); + g_return_if_fail (XB_IS_MACHINE (self)); + priv->debug_flags = flags; +} + +/** + * xb_machine_add_operator: + * @self: a #XbMachine + * @str: operator string, e.g. `==` + * @name: function name, e.g. `contains` + * + * Adds a new operator to the virtual machine. Operators can then be used + * instead of explicit methods like `eq()`. + * + * You need to add a custom operator using xb_machine_add_operator() before + * using xb_machine_parse(). Common operators like `<=` and `=` are built-in + * and do not have to be added manually. + * + * Since: 0.1.1 + **/ +void +xb_machine_add_operator (XbMachine *self, const gchar *str, const gchar *name) +{ + XbMachineOperator *op; + XbMachinePrivate *priv = GET_PRIVATE (self); + + g_return_if_fail (XB_IS_MACHINE (self)); + g_return_if_fail (str != NULL); + g_return_if_fail (name != NULL); + + op = g_slice_new0 (XbMachineOperator); + op->str = g_strdup (str); + op->strsz = strlen (str); + op->name = g_strdup (name); + g_ptr_array_add (priv->operators, op); +} + +/** + * xb_machine_add_method: + * @self: a #XbMachine + * @name: function name, e.g. `contains` + * @n_opcodes: minimum number of opcodes required on the stack + * @method_cb: function to call + * @user_data: user pointer to pass to @method_cb, or %NULL + * @user_data_free: a function which gets called to free @user_data, or %NULL + * + * Adds a new function to the virtual machine. Registered functions can then be + * used as methods. + * + * You need to add a custom function using xb_machine_add_method() before using + * methods that may reference it, for example xb_machine_add_opcode_fixup(). + * + * Since: 0.1.1 + **/ +void +xb_machine_add_method (XbMachine *self, + const gchar *name, + guint n_opcodes, + XbMachineMethodFunc method_cb, + gpointer user_data, + GDestroyNotify user_data_free) +{ + XbMachineMethodItem *item; + XbMachinePrivate *priv = GET_PRIVATE (self); + + g_return_if_fail (XB_IS_MACHINE (self)); + g_return_if_fail (name != NULL); + g_return_if_fail (method_cb != NULL); + + item = g_slice_new0 (XbMachineMethodItem); + item->idx = priv->methods->len; + item->name = g_strdup (name); + item->n_opcodes = n_opcodes; + item->method_cb = method_cb; + item->user_data = user_data; + item->user_data_free = user_data_free; + g_ptr_array_add (priv->methods, item); +} + +/** + * xb_machine_add_opcode_fixup: + * @self: a #XbMachine + * @opcodes_sig: signature, e.g. `INTE,TEXT` + * @fixup_cb: callback + * @user_data: user pointer to pass to @fixup_cb + * @user_data_free: a function which gets called to free @user_data, or %NULL + * + * Adds an opcode fixup. Fixups can be used to optimize the stack of opcodes or + * to add support for a nonstandard feature, for instance supporting missing + * attributes to functions. + * + * Since: 0.1.1 + **/ +void +xb_machine_add_opcode_fixup (XbMachine *self, + const gchar *opcodes_sig, + XbMachineOpcodeFixupFunc fixup_cb, + gpointer user_data, + GDestroyNotify user_data_free) +{ + XbMachineOpcodeFixupItem *item = g_slice_new0 (XbMachineOpcodeFixupItem); + XbMachinePrivate *priv = GET_PRIVATE (self); + item->fixup_cb = fixup_cb; + item->user_data = user_data; + item->user_data_free = user_data_free; + g_hash_table_insert (priv->opcode_fixup, g_strdup (opcodes_sig), item); +} + +/** + * xb_machine_add_text_handler: + * @self: a #XbMachine + * @handler_cb: callback + * @user_data: user pointer to pass to @handler_cb + * @user_data_free: a function which gets called to free @user_data, or %NULL + * + * Adds a text handler. This allows the virtual machine to support nonstandard + * encoding or shorthand mnemonics for standard functions. + * + * Since: 0.1.1 + **/ +void +xb_machine_add_text_handler (XbMachine *self, + XbMachineTextHandlerFunc handler_cb, + gpointer user_data, + GDestroyNotify user_data_free) +{ + XbMachineTextHandlerItem *item = g_slice_new0 (XbMachineTextHandlerItem); + XbMachinePrivate *priv = GET_PRIVATE (self); + item->handler_cb = handler_cb; + item->user_data = user_data; + item->user_data_free = user_data_free; + g_ptr_array_add (priv->text_handlers, item); +} + +static XbMachineMethodItem * +xb_machine_find_func (XbMachine *self, const gchar *func_name) +{ + XbMachinePrivate *priv = GET_PRIVATE (self); + for (guint i = 0; i < priv->methods->len; i++) { + XbMachineMethodItem *item = g_ptr_array_index (priv->methods, i); + if (g_strcmp0 (item->name, func_name) == 0) + return item; + } + return NULL; +} + +/** + * xb_machine_opcode_func_new: + * @self: a #XbMachine + * @func_name: function name, e.g. `eq` + * + * Creates a new opcode for a registered function. Some standard opcodes are + * registered by default, for instance `eq` or `ge`. Other opcodes have to be + * added using xb_machine_add_method(). + * + * Returns: a new #XbOpcode, or %NULL + * + * Since: 0.1.1 + **/ +XbOpcode * +xb_machine_opcode_func_new (XbMachine *self, const gchar *func_name) +{ + XbMachineMethodItem *item = xb_machine_find_func (self, func_name); + if (item == NULL) + return NULL; + return xb_opcode_new (XB_OPCODE_KIND_FUNCTION, + g_strdup (func_name), + item->idx, g_free); +} + +static gboolean +xb_machine_parse_add_func (XbMachine *self, + XbStack *opcodes, + const gchar *func_name, + GError **error) +{ + XbMachinePrivate *priv = GET_PRIVATE (self); + g_autoptr(XbOpcode) opcode = NULL; + + /* match opcode, which should always exist */ + opcode = xb_machine_opcode_func_new (self, func_name); + if (opcode == NULL) { + g_set_error (error, + G_IO_ERROR, + G_IO_ERROR_NOT_SUPPORTED, + "built-in function not found: %s", func_name); + return FALSE; + } + if (!xb_stack_push_steal (opcodes, g_steal_pointer (&opcode))) { + g_set_error (error, + G_IO_ERROR, + G_IO_ERROR_NOT_SUPPORTED, + "stack size %u exhausted", + priv->stack_size); + return FALSE; + } + + return TRUE; +} + +#if !GLIB_CHECK_VERSION(2,54,0) +static gboolean +str_has_sign (const gchar *str) +{ + return str[0] == '-' || str[0] == '+'; +} + +static gboolean +str_has_hex_prefix (const gchar *str) +{ + return str[0] == '0' && g_ascii_tolower (str[1]) == 'x'; +} + +static gboolean +g_ascii_string_to_unsigned (const gchar *str, + guint base, + guint64 min, + guint64 max, + guint64 *out_num, + GError **error) +{ + const gchar *end_ptr = NULL; + gint saved_errno = 0; + guint64 number; + + g_return_val_if_fail (str != NULL, FALSE); + g_return_val_if_fail (base >= 2 && base <= 36, FALSE); + g_return_val_if_fail (min <= max, FALSE); + g_return_val_if_fail (error == NULL || *error == NULL, FALSE); + + if (str[0] == '\0') { + g_set_error_literal (error, + G_IO_ERROR, G_IO_ERROR_INVALID_DATA, + "Empty string is not a number"); + return FALSE; + } + + errno = 0; + number = g_ascii_strtoull (str, (gchar **)&end_ptr, base); + saved_errno = errno; + + if (g_ascii_isspace (str[0]) || str_has_sign (str) || + (base == 16 && str_has_hex_prefix (str)) || + (saved_errno != 0 && saved_errno != ERANGE) || + end_ptr == NULL || + *end_ptr != '\0') { + g_set_error (error, + G_IO_ERROR, G_IO_ERROR_INVALID_DATA, + "“%s” is not an unsigned number", str); + return FALSE; + } + if (saved_errno == ERANGE || number < min || number > max) { + g_autofree gchar *min_str = g_strdup_printf ("%" G_GUINT64_FORMAT, min); + g_autofree gchar *max_str = g_strdup_printf ("%" G_GUINT64_FORMAT, max); + g_set_error (error, + G_IO_ERROR, G_IO_ERROR_INVALID_DATA, + "Number “%s” is out of bounds [%s, %s]", + str, min_str, max_str); + return FALSE; + } + if (out_num != NULL) + *out_num = number; + return TRUE; +} +#endif + +static gboolean +xb_machine_parse_add_text (XbMachine *self, + XbStack *opcodes, + const gchar *text, + gssize text_len, + GError **error) +{ + XbMachinePrivate *priv = GET_PRIVATE (self); + g_autofree gchar *str = NULL; + guint64 val; + + /* NULL is perfectly valid */ + if (text == NULL) { + xb_stack_push_steal (opcodes, xb_opcode_text_new_static (str)); + return TRUE; + } + + /* never add empty literals */ + if (text_len < 0) + text_len = strlen (text); + if (text_len == 0) + return TRUE; + + /* do any additional handlers */ + str = g_strndup (text, text_len); + for (guint i = 0; i < priv->text_handlers->len; i++) { + XbMachineTextHandlerItem *item = g_ptr_array_index (priv->text_handlers, i); + gboolean handled = FALSE; + if (!item->handler_cb (self, opcodes, str, &handled, item->user_data, error)) + return FALSE; + if (handled) + return TRUE; + } + + /* quoted text */ + if (text_len >= 2) { + if (str[0] == '\'' && str[text_len - 1] == '\'') { + gchar *tmp = g_strndup (str + 1, text_len - 2); + xb_stack_push_steal (opcodes, xb_opcode_text_new_steal (tmp)); + return TRUE; + } + } + + /* indexed text */ + if (text_len >= 3) { + if (str[0] == '$' && str[1] == '\'' && str[text_len - 1] == '\'') { + gchar *tmp = g_strndup (str + 2, text_len - 3); + XbOpcode *op = xb_opcode_new (XB_OPCODE_KIND_INDEXED_TEXT, + tmp, XB_SILO_UNSET, g_free); + xb_stack_push_steal (opcodes, op); + return TRUE; + } + } + + /* bind variables */ + if (g_strcmp0 (str, "?") == 0) { + xb_stack_push_steal (opcodes, xb_opcode_bind_new ()); + return TRUE; + } + + /* check for plain integer */ + if (g_ascii_string_to_unsigned (str, 10, 0, G_MAXUINT32, &val, NULL)) { + xb_stack_push_steal (opcodes, xb_opcode_integer_new (val)); + return TRUE; + } + + /* not supported */ + g_set_error (error, + G_IO_ERROR, + G_IO_ERROR_NOT_SUPPORTED, + "cannot parse text or number `%s`", str); + return FALSE; + +} + +static gboolean +xb_machine_parse_section (XbMachine *self, + XbStack *opcodes, + const gchar *text, + gssize text_len, + gboolean is_method, + GError **error) +{ + XbMachinePrivate *priv = GET_PRIVATE (self); + + /* fall back for simplicity */ + if (text_len < 0) + text_len = strlen (text); + if (text_len == 0) + return TRUE; + +// g_debug ("xb_machine_parse_section{%s} method=%i", g_strndup (text, text_len), is_method); + for (gssize i = 0; i < text_len; i++) { + for (guint j = 0; j < priv->operators->len; j++) { + XbMachineOperator *op = g_ptr_array_index (priv->operators, j); + if (strncmp (text + i, op->str, op->strsz) == 0) { + if (is_method) { + /* after then before */ + if (!xb_machine_parse_section (self, opcodes, text + i + op->strsz, -1, is_method, error)) + return FALSE; + if (i > 0) { + if (!xb_machine_parse_section (self, opcodes, text, i, FALSE, error)) + return FALSE; + } + if (!xb_machine_parse_add_func (self, opcodes, op->name, error)) + return FALSE; + } else { + /* before then after */ + if (i > 0) { + if (!xb_machine_parse_section (self, opcodes, text, i, FALSE, error)) + return FALSE; + } + if (!xb_machine_parse_section (self, opcodes, text + i + op->strsz, -1, is_method, error)) + return FALSE; + if (!xb_machine_parse_add_func (self, opcodes, op->name, error)) + return FALSE; + } + return TRUE; + } + } + } + + /* nothing matched */ + if (is_method) + return xb_machine_parse_add_func (self, opcodes, text, error); + return xb_machine_parse_add_text (self, opcodes, text, text_len, error); +} + +static gboolean +xb_machine_parse_sections (XbMachine *self, + XbStack *opcodes, + const gchar *text, + gsize text_len, + gboolean is_method, + GError **error) +{ + g_autofree gchar *tmp = NULL; + if (text_len == 0) + return TRUE; + + /* leading comma */ + if (text[0] == ',') { + tmp = g_strndup (text + 1, text_len - 1); + } else { + tmp = g_strndup (text, text_len); + } + +// g_debug ("xb_machine_parse_sections{%s} method=%i", tmp, is_method); + for (gint i = text_len - 1; i >= 0; i--) { +// g_debug ("%u\t\t%c", i, tmp[i]); + if (tmp[i] == ',') { + tmp[i] = '\0'; + if (is_method) { + if (!xb_machine_parse_add_func (self, + opcodes, + tmp + i + 1, + error)) + return FALSE; + is_method = FALSE; + } else { + if (!xb_machine_parse_section (self, + opcodes, + tmp + i + 1, + -1, + TRUE, + error)) + return FALSE; + } + } + } + if (tmp[0] != '\0') { + if (!xb_machine_parse_section (self, + opcodes, + tmp, + -1, + is_method, + error)) + return FALSE; + } + return TRUE; +} + +static gchar * +xb_machine_get_opcodes_sig (XbMachine *self, XbStack *opcodes) +{ + GString *str = g_string_new (NULL); + for (guint i = 0; i < xb_stack_get_size (opcodes); i++) { + XbOpcode *op = xb_stack_peek (opcodes, i); + g_autofree gchar *sig = xb_opcode_get_sig (op); + g_string_append_printf (str, "%s,", sig); + } + if (str->len > 0) + g_string_truncate (str, str->len - 1); + return g_string_free (str, FALSE); +} + +static gboolean +xb_machine_opcodes_optimize_fn (XbMachine *self, + XbOpcode *op, + guint *idx, + GPtrArray *src, + GPtrArray *dst, + GError **error) +{ + XbMachineMethodItem *item; + XbMachinePrivate *priv = GET_PRIVATE (self); + g_autofree gchar *stack_str = NULL; + g_autoptr(GError) error_local = NULL; + g_autoptr(XbOpcode) op_result = NULL; + g_autoptr(XbStack) stack = NULL; + + /* a function! lets check the arg length */ + if (xb_opcode_get_kind (op) != XB_OPCODE_KIND_FUNCTION) { + g_ptr_array_add (dst, xb_opcode_ref (op)); + return TRUE; + } + + /* get function, check if we have enough arguments */ + item = g_ptr_array_index (priv->methods, xb_opcode_get_val (op)); + if (item->n_opcodes >= *idx) { + g_set_error_literal (error, + G_IO_ERROR, + G_IO_ERROR_INVALID_DATA, + "predicate invalid -- not enough args"); + return FALSE; + } + + /* make a copy of the stack with the arguments */ + stack = xb_stack_new (item->n_opcodes); + for (guint i = item->n_opcodes; i > 0; i--) { + XbOpcode *op_tmp = g_ptr_array_index (src, *idx - (i + 1)); + xb_stack_push (stack, op_tmp); + } + + /* run the method */ + stack_str = xb_stack_to_string (stack); + if (!item->method_cb (self, stack, NULL, item->user_data, NULL, &error_local)) { + if (priv->debug_flags & XB_MACHINE_DEBUG_FLAG_SHOW_OPTIMIZER) { + g_debug ("ignoring opimized call to %s(%s): %s", + item->name, + stack_str, + error_local->message); + } + g_ptr_array_add (dst, xb_opcode_ref (op)); + return TRUE; + } + + /* the method ran, add the result and discard the arguments */ + op_result = xb_stack_pop (stack); + if (op_result == NULL) { + g_set_error_literal (error, + G_IO_ERROR, + G_IO_ERROR_INVALID_DATA, + "internal error; no retval on stack"); + return FALSE; + } + if (xb_opcode_get_kind (op_result) != XB_OPCODE_KIND_BOOLEAN) { + if (priv->debug_flags & XB_MACHINE_DEBUG_FLAG_SHOW_OPTIMIZER) + g_debug ("method ran, adding result"); + *idx -= item->n_opcodes; + g_ptr_array_add (dst, g_steal_pointer (&op_result)); + return TRUE; + } + + /* nothing was added to the stack, so check if the predicate will + * always evaluate to TRUE */ + if (priv->debug_flags & XB_MACHINE_DEBUG_FLAG_SHOW_OPTIMIZER) { + g_autofree gchar *tmp = xb_opcode_to_string (op_result); + g_debug ("method ran, result %s", tmp); + } + if (xb_opcode_get_val (op_result) == TRUE) { + *idx -= item->n_opcodes; + g_ptr_array_add (dst, g_steal_pointer (&op_result)); + return TRUE; + } + + /* the predicate will always evalulate to FALSE */ + g_set_error (error, + G_IO_ERROR, + G_IO_ERROR_INVALID_DATA, + "the predicate will always evalulate to FALSE: %s", + stack_str); + return FALSE; +} + +static gboolean +xb_machine_opcodes_optimize (XbMachine *self, XbStack *opcodes, GError **error) +{ + XbMachinePrivate *priv = GET_PRIVATE (self); + g_autoptr(GPtrArray) dst = NULL; + g_autoptr(GPtrArray) src = NULL; + + /* debug */ + if (priv->debug_flags & XB_MACHINE_DEBUG_FLAG_SHOW_STACK) { + g_autofree gchar *str = xb_stack_to_string (opcodes); + g_debug ("before optimizing: %s", str); + } + + /* process the stack in reverse order */ + src = xb_stack_steal_all (opcodes); + dst = xb_stack_steal_all (opcodes); + for (guint i = src->len; i > 0; i--) { + XbOpcode *op = g_ptr_array_index (src, i - 1); + if (!xb_machine_opcodes_optimize_fn (self, op, &i, src, dst, error)) + return FALSE; + } + + /* copy back the result into the opcodes stack */ + for (guint i = dst->len; i > 0; i--) { + XbOpcode *op = g_ptr_array_index (dst, i - 1); + xb_stack_push (opcodes, op); + } + + /* debug */ + if (priv->debug_flags & XB_MACHINE_DEBUG_FLAG_SHOW_STACK) { + g_autofree gchar *str = xb_stack_to_string (opcodes); + g_debug ("after optimizing: %s", str); + } + return TRUE; +} + +static gsize +xb_machine_parse_text (XbMachine *self, + XbStack *opcodes, + const gchar *text, + gsize text_len, + guint level, + GError **error) +{ + XbMachinePrivate *priv = GET_PRIVATE (self); + guint tail = 0; + + /* sanity check */ + if (level > 20) { + g_autofree gchar *tmp = g_strndup (text, text_len); + g_set_error (error, + G_IO_ERROR, + G_IO_ERROR_INVALID_DATA, + "nesting deeper than 20 levels supported: %s", tmp); + return G_MAXSIZE; + } + + //g_debug ("%u xb_machine_parse_text{%s}", level, g_strndup (text, text_len)); + for (guint i = 0; i < text_len; i++) { + if (priv->debug_flags & XB_MACHINE_DEBUG_FLAG_SHOW_PARSING) + g_debug ("LVL %u\t%u:\t\t%c", level, i, text[i]); + if (text[i] == '(') { + gsize j = 0; + j = xb_machine_parse_text (self, + opcodes, + text + i + 1, + text_len - i, + level + 1, + error); + if (j == G_MAXSIZE) + return G_MAXSIZE; + if (!xb_machine_parse_sections (self, + opcodes, + text + tail, + i - tail, + TRUE, + error)) + return G_MAXSIZE; + i += j; + tail = i + 1; + continue; + } + if (text[i] == ')') { + if (!xb_machine_parse_sections (self, + opcodes, + text + tail, + i - tail, + FALSE, + error)) + return G_MAXSIZE; + return i + 1; + } + } + if (tail != text_len && level > 0) { + g_autofree gchar *tmp = g_strndup (text, text_len); + g_set_error (error, + G_IO_ERROR, + G_IO_ERROR_INVALID_DATA, + "brackets did not match: %s", tmp); + return G_MAXSIZE; + } + if (!xb_machine_parse_sections (self, + opcodes, + text + tail, + text_len - tail, + FALSE, + error)) + return G_MAXSIZE; + return 0; +} + + +/** + * xb_machine_parse_full: + * @self: a #XbMachine + * @text: predicate to parse, e.g. `contains(text(),'xyx')` + * @text_len: length of @text, or -1 if @text is `NUL` terminated + * @flags: #XbMachineParseFlags, e.g. %XB_MACHINE_PARSE_FLAG_OPTIMIZE + * @error: a #GError, or %NULL + * + * Parses an XPath predicate. Not all of XPath 1.0 or XPath 1.0 is supported, + * and new functions and mnemonics can be added using xb_machine_add_method() + * and xb_machine_add_text_handler(). + * + * Returns: (transfer full): opcodes, or %NULL on error + * + * Since: 0.1.4 + **/ +XbStack * +xb_machine_parse_full (XbMachine *self, + const gchar *text, + gssize text_len, + XbMachineParseFlags flags, + GError **error) +{ + XbMachineOpcodeFixupItem *item; + XbMachinePrivate *priv = GET_PRIVATE (self); + guint level = 0; + g_autoptr(XbStack) opcodes = NULL; + g_autofree gchar *opcodes_sig = NULL; + + g_return_val_if_fail (XB_IS_MACHINE (self), NULL); + g_return_val_if_fail (text != NULL, NULL); + g_return_val_if_fail (error == NULL || *error == NULL, NULL); + + /* assume NUL terminated */ + if (text_len < 0) + text_len = strlen (text); + if (text_len == 0) { + g_set_error_literal (error, + G_IO_ERROR, + G_IO_ERROR_INVALID_DATA, + "string was zero size"); + return NULL; + } + + /* parse into opcodes */ + opcodes = xb_stack_new (priv->stack_size); + if (xb_machine_parse_text (self, opcodes, text, text_len, level, error) == G_MAXSIZE) + return NULL; + + /* do any fixups */ + opcodes_sig = xb_machine_get_opcodes_sig (self, opcodes); + item = g_hash_table_lookup (priv->opcode_fixup, opcodes_sig); + if (item != NULL) { + if (!item->fixup_cb (self, opcodes, item->user_data, error)) + return NULL; + } + + /* optimize */ + if (flags & XB_MACHINE_PARSE_FLAG_OPTIMIZE) { + for (guint i = 0; i < 10; i++) { + guint oldsz = xb_stack_get_size (opcodes); + if (!xb_machine_opcodes_optimize (self, opcodes, error)) + return NULL; + if (oldsz == xb_stack_get_size (opcodes)) + break; + } + } + + /* success */ + return g_steal_pointer (&opcodes); +} + +/** + * xb_machine_parse: + * @self: a #XbMachine + * @text: predicate to parse, e.g. `contains(text(),'xyx')` + * @text_len: length of @text, or -1 if @text is `NUL` terminated + * @error: a #GError, or %NULL + * + * Parses an XPath predicate. Not all of XPath 1.0 or XPath 1.0 is supported, + * and new functions and mnemonics can be added using xb_machine_add_method() + * and xb_machine_add_text_handler(). + * + * Returns: (transfer full): opcodes, or %NULL on error + * + * Since: 0.1.1 + **/ +XbStack * +xb_machine_parse (XbMachine *self, + const gchar *text, + gssize text_len, + GError **error) +{ + return xb_machine_parse_full (self, text, text_len, + XB_MACHINE_PARSE_FLAG_OPTIMIZE, + error); +} + +static void +xb_machine_debug_show_stack (XbMachine *self, XbStack *stack) +{ + g_autofree gchar *str = NULL; + if (xb_stack_get_size (stack) == 0) { + g_debug ("stack is empty"); + return; + } + str = xb_stack_to_string (stack); + g_debug ("stack: %s", str); +} + +static gboolean +xb_machine_run_func (XbMachine *self, + XbStack *stack, + XbOpcode *opcode, + gpointer exec_data, + GError **error) +{ + XbMachinePrivate *priv = GET_PRIVATE (self); + XbMachineMethodItem *item = g_ptr_array_index (priv->methods, xb_opcode_get_val (opcode)); + + /* optional debugging */ + if (priv->debug_flags & XB_MACHINE_DEBUG_FLAG_SHOW_STACK) { + g_autofree gchar *str = xb_opcode_to_string (opcode); + g_debug ("running: %s", str); + xb_machine_debug_show_stack (self, stack); + } + + /* check we have enough stack elements */ + if (item->n_opcodes > xb_stack_get_size (stack)) { + g_set_error (error, + G_IO_ERROR, + G_IO_ERROR_NOT_SUPPORTED, + "function required %u arguments, stack only has %u", + item->n_opcodes, xb_stack_get_size (stack)); + return FALSE; + } + if (!item->method_cb (self, stack, NULL, item->user_data, exec_data, error)) { + g_prefix_error (error, "failed to call %s(): ", item->name); + return FALSE; + } + return TRUE; +} + +/** + * xb_machine_opcode_to_string: + * @self: a #XbMachine + * @opcode: a #XbOpcode + * + * Returns a string representing the specific opcode. + * + * Returns: text + * + * Since: 0.1.1 + **/ +gchar * +xb_machine_opcode_to_string (XbMachine *self, XbOpcode *opcode) +{ + return xb_opcode_to_string (opcode); +} + +/** + * xb_machine_opcodes_to_string: + * @self: a #XbMachine + * @opcodes: a #XbStack of opcodes + * + * Returns a string representing a set of opcodes. + * + * Returns: text + * + * Since: 0.1.1 + **/ +gchar * +xb_machine_opcodes_to_string (XbMachine *self, XbStack *opcodes) +{ + return xb_stack_to_string (opcodes); +} + +/** + * xb_machine_run: + * @self: a #XbMachine + * @opcodes: a #XbStack of opcodes + * @result: (out): return status after running @opcodes + * @exec_data: per-run user data that is passed to all the XbMachineMethodFunc functions + * @error: a #GError, or %NULL + * + * Runs a set of opcodes on the virtual machine. + * + * It is safe to call this function from a different thread to the one that + * created the #XbMachine. + * + * Returns: a new #XbOpcode, or %NULL + * + * Since: 0.1.1 + **/ +gboolean +xb_machine_run (XbMachine *self, + XbStack *opcodes, + gboolean *result, + gpointer exec_data, + GError **error) +{ + XbMachinePrivate *priv = GET_PRIVATE (self); + g_autoptr(XbOpcode) opcode_success = NULL; + g_autoptr(XbStack) stack = NULL; + + g_return_val_if_fail (XB_IS_MACHINE (self), FALSE); + g_return_val_if_fail (opcodes != NULL, FALSE); + g_return_val_if_fail (result != NULL, FALSE); + g_return_val_if_fail (error == NULL || *error == NULL, FALSE); + + /* process each opcode */ + stack = xb_stack_new (priv->stack_size); + for (guint i = 0; i < xb_stack_get_size (opcodes); i++) { + XbOpcode *opcode = xb_stack_peek (opcodes, i); + XbOpcodeKind kind = xb_opcode_get_kind (opcode); + + /* process the stack */ + if (kind == XB_OPCODE_KIND_FUNCTION) { + if (!xb_machine_run_func (self, + stack, + opcode, + exec_data, + error)) + return FALSE; + continue; + } + + /* add to stack */ + if (kind == XB_OPCODE_KIND_TEXT || + kind == XB_OPCODE_KIND_BOOLEAN || + kind == XB_OPCODE_KIND_INTEGER || + kind == XB_OPCODE_KIND_INDEXED_TEXT || + kind == XB_OPCODE_KIND_BOUND_TEXT || + kind == XB_OPCODE_KIND_BOUND_INTEGER) { + xb_machine_stack_push (self, stack, opcode); + continue; + } + + /* unbound */ + if (kind == XB_OPCODE_KIND_BOUND_UNSET) { + g_autofree gchar *tmp1 = xb_stack_to_string (stack); + g_autofree gchar *tmp2 = xb_stack_to_string (opcodes); + g_set_error (error, + G_IO_ERROR, + G_IO_ERROR_INVALID_DATA, + "opcode was not bound at runtime, stack:%s, opcodes:%s", + tmp1, tmp2); + return FALSE; + } + + /* invalid */ + g_set_error (error, + G_IO_ERROR, + G_IO_ERROR_INVALID_DATA, + "opcode kind %u not recognised", + kind); + return FALSE; + } + + /* the stack should have one boolean left on the stack */ + if (xb_stack_get_size (stack) != 1) { + g_autofree gchar *tmp = xb_stack_to_string (stack); + g_set_error (error, + G_IO_ERROR, + G_IO_ERROR_INVALID_DATA, + "%u opcodes remain on the stack (%s)", + xb_stack_get_size (stack), tmp); + return FALSE; + } + opcode_success = xb_stack_pop (stack); + if (xb_opcode_get_kind (opcode_success) != XB_OPCODE_KIND_BOOLEAN) { + g_autofree gchar *tmp = xb_stack_to_string (stack); + g_set_error (error, + G_IO_ERROR, + G_IO_ERROR_INVALID_DATA, + "Expected boolean, got: %s", tmp); + return FALSE; + } + if (result != NULL) + *result = xb_opcode_get_val (opcode_success); + + /* success */ + return TRUE; +} + +/** + * xb_machine_stack_pop: + * @self: a #XbMachine + * @stack: a #XbStack + * + * Pops an opcode from the stack. + * + * Returns: (transfer full): a new #XbOpcode, or %NULL + * + * Since: 0.1.1 + **/ +XbOpcode * +xb_machine_stack_pop (XbMachine *self, XbStack *stack) +{ + XbMachinePrivate *priv = GET_PRIVATE (self); + if (priv->debug_flags & XB_MACHINE_DEBUG_FLAG_SHOW_STACK) { + XbOpcode *opcode = xb_stack_peek (stack, xb_stack_get_size (stack) - 1); + g_autofree gchar *str = xb_opcode_to_string (opcode); + g_debug ("popping: %s", str); + xb_machine_debug_show_stack (self, stack); + } + return xb_stack_pop (stack); +} + +/** + * xb_machine_stack_push: + * @self: a #XbMachine + * @stack: a #XbStack + * @opcode: a #XbOpcode + * + * Adds an opcode to the stack. + * + * Since: 0.1.1 + **/ +void +xb_machine_stack_push (XbMachine *self, XbStack *stack, XbOpcode *opcode) +{ + XbMachinePrivate *priv = GET_PRIVATE (self); + if (priv->debug_flags & XB_MACHINE_DEBUG_FLAG_SHOW_STACK) { + g_autofree gchar *str = xb_opcode_to_string (opcode); + g_debug ("pushing: %s", str); + } + xb_stack_push (stack, opcode); + if (priv->debug_flags & XB_MACHINE_DEBUG_FLAG_SHOW_STACK) + xb_machine_debug_show_stack (self, stack); +} + +/** + * xb_machine_stack_push_steal: + * @self: a #XbMachine + * @stack: a #XbStack + * @opcode: a #XbOpcode + * + * Adds an stolen opcode to the stack. + * + * Since: 0.1.4 + **/ +void +xb_machine_stack_push_steal (XbMachine *self, XbStack *stack, XbOpcode *opcode) +{ + XbMachinePrivate *priv = GET_PRIVATE (self); + if (priv->debug_flags & XB_MACHINE_DEBUG_FLAG_SHOW_STACK) { + g_autofree gchar *str = xb_opcode_to_string (opcode); + g_debug ("pushing: %s", str); + } + xb_stack_push_steal (stack, opcode); + if (priv->debug_flags & XB_MACHINE_DEBUG_FLAG_SHOW_STACK) + xb_machine_debug_show_stack (self, stack); +} + +/** + * xb_machine_stack_push_text: + * @self: a #XbMachine + * @stack: a #XbStack + * @str: text literal + * + * Adds a text literal to the stack, copying @str. + * + * Since: 0.1.1 + **/ +void +xb_machine_stack_push_text (XbMachine *self, XbStack *stack, const gchar *str) +{ + XbMachinePrivate *priv = GET_PRIVATE (self); + if (priv->debug_flags & XB_MACHINE_DEBUG_FLAG_SHOW_STACK) + g_debug ("pushing: %s", str); + xb_stack_push_steal (stack, xb_opcode_text_new (str)); + if (priv->debug_flags & XB_MACHINE_DEBUG_FLAG_SHOW_STACK) + xb_machine_debug_show_stack (self, stack); +} + +/** + * xb_machine_stack_push_text_static: + * @self: a #XbMachine + * @stack: a #XbStack + * @str: text literal + * + * Adds static text literal to the stack. + * + * Since: 0.1.1 + **/ +void +xb_machine_stack_push_text_static (XbMachine *self, XbStack *stack, const gchar *str) +{ + XbMachinePrivate *priv = GET_PRIVATE (self); + if (priv->debug_flags & XB_MACHINE_DEBUG_FLAG_SHOW_STACK) + g_debug ("pushing: %s", str); + xb_stack_push_steal (stack, xb_opcode_text_new_static (str)); + if (priv->debug_flags & XB_MACHINE_DEBUG_FLAG_SHOW_STACK) + xb_machine_debug_show_stack (self, stack); +} + +/** + * xb_machine_stack_push_text_steal: + * @self: a #XbMachine + * @stack: a #XbStack + * @str: text literal + * + * Adds a stolen text literal to the stack. + * + * Since: 0.1.1 + **/ +void +xb_machine_stack_push_text_steal (XbMachine *self, XbStack *stack, gchar *str) +{ + XbMachinePrivate *priv = GET_PRIVATE (self); + if (priv->debug_flags & XB_MACHINE_DEBUG_FLAG_SHOW_STACK) + g_debug ("pushing: %s", str); + xb_stack_push_steal (stack, xb_opcode_text_new_steal (str)); + if (priv->debug_flags & XB_MACHINE_DEBUG_FLAG_SHOW_STACK) + xb_machine_debug_show_stack (self, stack); +} + +/** + * xb_machine_stack_push_integer: + * @self: a #XbMachine + * @stack: a #XbStack + * @val: integer literal + * + * Adds an integer literal to the stack. + * + * Since: 0.1.1 + **/ +void +xb_machine_stack_push_integer (XbMachine *self, XbStack *stack, guint32 val) +{ + XbMachinePrivate *priv = GET_PRIVATE (self); + if (priv->debug_flags & XB_MACHINE_DEBUG_FLAG_SHOW_STACK) + g_debug ("pushing: %u", val); + xb_stack_push_steal (stack, xb_opcode_integer_new (val)); + if (priv->debug_flags & XB_MACHINE_DEBUG_FLAG_SHOW_STACK) + xb_machine_debug_show_stack (self, stack); +} + +/** + * xb_machine_set_stack_size: + * @self: a #XbMachine + * @stack_size: integer + * + * Sets the maximum stack size used for the machine. + * + * The stack size will be affective for new jobs started with xb_machine_run() + * and xb_machine_parse(). + * + * Since: 0.1.3 + **/ +void +xb_machine_set_stack_size (XbMachine *self, guint stack_size) +{ + XbMachinePrivate *priv = GET_PRIVATE (self); + g_return_if_fail (XB_IS_MACHINE (self)); + g_return_if_fail (stack_size != 0); + priv->stack_size = stack_size; +} + +/** + * xb_machine_get_stack_size: + * @self: a #XbMachine + * + * Gets the maximum stack size used for the machine. + * + * Returns: integer + * + * Since: 0.1.3 + **/ +guint +xb_machine_get_stack_size (XbMachine *self) +{ + XbMachinePrivate *priv = GET_PRIVATE (self); + g_return_val_if_fail (XB_IS_MACHINE (self), 0); + return priv->stack_size; +} + +static gboolean +xb_machine_func_and_cb (XbMachine *self, + XbStack *stack, + gboolean *result, + gpointer user_data, + gpointer exec_data, + GError **error) +{ + g_autoptr(XbOpcode) op1 = xb_machine_stack_pop (self, stack); + g_autoptr(XbOpcode) op2 = xb_machine_stack_pop (self, stack); + + /* INTE:INTE */ + if (xb_opcode_cmp_val (op1) && xb_opcode_cmp_val (op2)) { + xb_stack_push_bool (stack, xb_opcode_get_val (op1) && xb_opcode_get_val (op2)); + return TRUE; + } + + /* fail */ + g_set_error (error, + G_IO_ERROR, + G_IO_ERROR_NOT_SUPPORTED, + "%s:%s types not supported", + xb_opcode_kind_to_string (xb_opcode_get_kind (op1)), + xb_opcode_kind_to_string (xb_opcode_get_kind (op2))); + return FALSE; +} + +static gboolean +xb_machine_func_or_cb (XbMachine *self, + XbStack *stack, + gboolean *result, + gpointer user_data, + gpointer exec_data, + GError **error) +{ + g_autoptr(XbOpcode) op1 = xb_machine_stack_pop (self, stack); + g_autoptr(XbOpcode) op2 = xb_machine_stack_pop (self, stack); + + /* INTE:INTE */ + if (xb_opcode_cmp_val (op1) && xb_opcode_cmp_val (op2)) { + xb_stack_push_bool (stack, xb_opcode_get_val (op1) || xb_opcode_get_val (op2)); + return TRUE; + } + + /* fail */ + g_set_error (error, + G_IO_ERROR, + G_IO_ERROR_NOT_SUPPORTED, + "%s:%s types not supported", + xb_opcode_kind_to_string (xb_opcode_get_kind (op1)), + xb_opcode_kind_to_string (xb_opcode_get_kind (op2))); + return FALSE; +} + +static gboolean +xb_machine_func_eq_cb (XbMachine *self, + XbStack *stack, + gboolean *result, + gpointer user_data, + gpointer exec_data, + GError **error) +{ + g_autoptr(XbOpcode) op1 = xb_machine_stack_pop (self, stack); + g_autoptr(XbOpcode) op2 = xb_machine_stack_pop (self, stack); + + /* INTE:INTE */ + if (xb_opcode_cmp_val (op1) && xb_opcode_cmp_val (op2)) { + xb_stack_push_bool (stack, xb_opcode_get_val (op1) == xb_opcode_get_val (op2)); + return TRUE; + } + + /* TEXT:TEXT */ + if (xb_opcode_cmp_str (op1) && xb_opcode_cmp_str (op2)) { + xb_stack_push_bool (stack, g_strcmp0 (xb_opcode_get_str (op1), + xb_opcode_get_str (op2)) == 0); + return TRUE; + } + + /* INTE:TEXT */ + if (xb_opcode_cmp_val (op1) && xb_opcode_cmp_str (op2)) { + guint64 val = 0; + if (xb_opcode_get_str (op2) == NULL) { + xb_stack_push_bool (stack, FALSE); + return TRUE; + } + if (!g_ascii_string_to_unsigned (xb_opcode_get_str (op2), + 10, 0, G_MAXUINT32, + &val, error)) { + return FALSE; + } + xb_stack_push_bool (stack, val == xb_opcode_get_val (op1)); + return TRUE; + } + + /* INTE:TEXT */ + if (xb_opcode_cmp_val (op2) && xb_opcode_cmp_str (op1)) { + guint64 val = 0; + if (xb_opcode_get_str (op1) == NULL) { + xb_stack_push_bool (stack, FALSE); + return TRUE; + } + if (!g_ascii_string_to_unsigned (xb_opcode_get_str (op1), + 10, 0, G_MAXUINT32, + &val, error)) { + return FALSE; + } + xb_stack_push_bool (stack, val == xb_opcode_get_val (op2)); + return TRUE; + } + + /* fail */ + g_set_error (error, + G_IO_ERROR, + G_IO_ERROR_NOT_SUPPORTED, + "%s:%s types not supported", + xb_opcode_kind_to_string (xb_opcode_get_kind (op1)), + xb_opcode_kind_to_string (xb_opcode_get_kind (op2))); + return FALSE; +} + +static gboolean +xb_machine_func_ne_cb (XbMachine *self, + XbStack *stack, + gboolean *result, + gpointer user_data, + gpointer exec_data, + GError **error) +{ + g_autoptr(XbOpcode) op1 = xb_machine_stack_pop (self, stack); + g_autoptr(XbOpcode) op2 = xb_machine_stack_pop (self, stack); + + /* INTE:INTE */ + if (xb_opcode_cmp_val (op1) && xb_opcode_cmp_val (op2)) { + xb_stack_push_bool (stack, xb_opcode_get_val (op1) != xb_opcode_get_val (op2)); + return TRUE; + } + + /* TEXT:TEXT */ + if (xb_opcode_cmp_str (op1) && xb_opcode_cmp_str (op2)) { + xb_stack_push_bool (stack, g_strcmp0 (xb_opcode_get_str (op1), + xb_opcode_get_str (op2)) != 0); + return TRUE; + } + + /* INTE:TEXT */ + if (xb_opcode_cmp_val (op1) && xb_opcode_cmp_str (op2)) { + guint64 val = 0; + if (xb_opcode_get_str (op2) == NULL) { + xb_stack_push_bool (stack, FALSE); + return TRUE; + } + if (!g_ascii_string_to_unsigned (xb_opcode_get_str (op2), + 10, 0, G_MAXUINT32, + &val, error)) { + return FALSE; + } + xb_stack_push_bool (stack, val != xb_opcode_get_val (op1)); + return TRUE; + } + + /* fail */ + g_set_error (error, + G_IO_ERROR, + G_IO_ERROR_NOT_SUPPORTED, + "%s:%s types not supported", + xb_opcode_kind_to_string (xb_opcode_get_kind (op1)), + xb_opcode_kind_to_string (xb_opcode_get_kind (op2))); + return FALSE; +} + +static gboolean +xb_machine_func_lt_cb (XbMachine *self, + XbStack *stack, + gboolean *result, + gpointer user_data, + gpointer exec_data, + GError **error) +{ + g_autoptr(XbOpcode) op1 = xb_machine_stack_pop (self, stack); + g_autoptr(XbOpcode) op2 = xb_machine_stack_pop (self, stack); + + /* INTE:INTE */ + if (xb_opcode_cmp_val (op1) && xb_opcode_cmp_val (op2)) { + xb_stack_push_bool (stack, xb_opcode_get_val (op2) < xb_opcode_get_val (op1)); + return TRUE; + } + + /* TEXT:TEXT */ + if (xb_opcode_cmp_str (op1) && xb_opcode_cmp_str (op2)) { + xb_stack_push_bool (stack, g_strcmp0 (xb_opcode_get_str (op2), + xb_opcode_get_str (op1)) < 0); + return TRUE; + } + + /* INTE:TEXT */ + if (xb_opcode_cmp_val (op1) && xb_opcode_cmp_str (op2)) { + guint64 val = 0; + if (xb_opcode_get_str (op2) == NULL) { + xb_stack_push_bool (stack, FALSE); + return TRUE; + } + if (!g_ascii_string_to_unsigned (xb_opcode_get_str (op2), + 10, 0, G_MAXUINT32, + &val, error)) { + return FALSE; + } + xb_stack_push_bool (stack, val < xb_opcode_get_val (op1)); + return TRUE; + } + + /* fail */ + g_set_error (error, + G_IO_ERROR, + G_IO_ERROR_NOT_SUPPORTED, + "%s:%s types not supported", + xb_opcode_kind_to_string (xb_opcode_get_kind (op1)), + xb_opcode_kind_to_string (xb_opcode_get_kind (op2))); + return FALSE; +} + +static gboolean +xb_machine_func_gt_cb (XbMachine *self, + XbStack *stack, + gboolean *result, + gpointer user_data, + gpointer exec_data, + GError **error) +{ + g_autoptr(XbOpcode) op1 = xb_machine_stack_pop (self, stack); + g_autoptr(XbOpcode) op2 = xb_machine_stack_pop (self, stack); + + /* INTE:INTE */ + if (xb_opcode_cmp_val (op1) && xb_opcode_cmp_val (op2)) { + xb_stack_push_bool (stack, xb_opcode_get_val (op2) > xb_opcode_get_val (op1)); + return TRUE; + } + + /* TEXT:TEXT */ + if (xb_opcode_cmp_str (op1) && xb_opcode_cmp_str (op2)) { + xb_stack_push_bool (stack, g_strcmp0 (xb_opcode_get_str (op2), + xb_opcode_get_str (op1)) > 0); + return TRUE; + } + + /* INTE:TEXT */ + if (xb_opcode_cmp_val (op1) && xb_opcode_cmp_str (op2)) { + guint64 val = 0; + if (xb_opcode_get_str (op2) == NULL) { + xb_stack_push_bool (stack, FALSE); + return TRUE; + } + if (!g_ascii_string_to_unsigned (xb_opcode_get_str (op2), + 10, 0, G_MAXUINT32, + &val, error)) { + return FALSE; + } + xb_stack_push_bool (stack, val > xb_opcode_get_val (op1)); + return TRUE; + } + + /* fail */ + g_set_error (error, + G_IO_ERROR, + G_IO_ERROR_NOT_SUPPORTED, + "%s:%s types not supported", + xb_opcode_kind_to_string (xb_opcode_get_kind (op1)), + xb_opcode_kind_to_string (xb_opcode_get_kind (op2))); + return FALSE; +} + +static gboolean +xb_machine_func_le_cb (XbMachine *self, + XbStack *stack, + gboolean *result, + gpointer user_data, + gpointer exec_data, + GError **error) +{ + g_autoptr(XbOpcode) op1 = xb_machine_stack_pop (self, stack); + g_autoptr(XbOpcode) op2 = xb_machine_stack_pop (self, stack); + + /* INTE:INTE */ + if (xb_opcode_cmp_val (op1) && xb_opcode_cmp_val (op2)) { + xb_stack_push_bool (stack, xb_opcode_get_val (op2) <= xb_opcode_get_val (op1)); + return TRUE; + } + + /* TEXT:TEXT */ + if (xb_opcode_cmp_str (op1) && xb_opcode_cmp_str (op2)) { + xb_stack_push_bool (stack, g_strcmp0 (xb_opcode_get_str (op2), + xb_opcode_get_str (op1)) <= 0); + return TRUE; + } + + /* INTE:TEXT */ + if (xb_opcode_cmp_val (op1) && xb_opcode_cmp_str (op2)) { + guint64 val = 0; + if (xb_opcode_get_str (op2) == NULL) { + xb_stack_push_bool (stack, FALSE); + return TRUE; + } + if (!g_ascii_string_to_unsigned (xb_opcode_get_str (op2), + 10, 0, G_MAXUINT32, + &val, error)) { + return FALSE; + } + xb_stack_push_bool (stack, val <= xb_opcode_get_val (op1)); + return TRUE; + } + + /* fail */ + g_set_error (error, + G_IO_ERROR, + G_IO_ERROR_NOT_SUPPORTED, + "%s:%s types not supported", + xb_opcode_kind_to_string (xb_opcode_get_kind (op1)), + xb_opcode_kind_to_string (xb_opcode_get_kind (op2))); + return FALSE; +} + +static gboolean +xb_machine_func_lower_cb (XbMachine *self, + XbStack *stack, + gboolean *result, + gpointer user_data, + gpointer exec_data, + GError **error) +{ + g_autoptr(XbOpcode) op = xb_machine_stack_pop (self, stack); + + /* TEXT */ + if (xb_opcode_cmp_str (op)) { + xb_machine_stack_push_text_steal (self, stack, + g_ascii_strdown (xb_opcode_get_str (op), -1)); + return TRUE; + } + + /* fail */ + g_set_error (error, + G_IO_ERROR, + G_IO_ERROR_NOT_SUPPORTED, + "%s type not supported", + xb_opcode_kind_to_string (xb_opcode_get_kind (op))); + return FALSE; +} + +static gboolean +xb_machine_func_upper_cb (XbMachine *self, + XbStack *stack, + gboolean *result, + gpointer user_data, + gpointer exec_data, + GError **error) +{ + g_autoptr(XbOpcode) op = xb_machine_stack_pop (self, stack); + + /* TEXT */ + if (xb_opcode_cmp_str (op)) { + xb_machine_stack_push_text_steal (self, stack, + g_ascii_strup (xb_opcode_get_str (op), -1)); + return TRUE; + } + + /* fail */ + g_set_error (error, + G_IO_ERROR, + G_IO_ERROR_NOT_SUPPORTED, + "%s type not supported", + xb_opcode_kind_to_string (xb_opcode_get_kind (op))); + return FALSE; +} + +static gboolean +xb_machine_func_not_cb (XbMachine *self, + XbStack *stack, + gboolean *result, + gpointer user_data, + gpointer exec_data, + GError **error) +{ + g_autoptr(XbOpcode) op1 = xb_machine_stack_pop (self, stack); + + /* TEXT */ + if (xb_opcode_cmp_str (op1)) { + xb_stack_push_bool (stack, xb_opcode_get_str (op1) == NULL); + return TRUE; + } + + /* INTE */ + if (xb_opcode_cmp_val (op1)) { + xb_stack_push_bool (stack, xb_opcode_get_val (op1) == 0); + return TRUE; + } + + /* fail */ + g_set_error (error, + G_IO_ERROR, + G_IO_ERROR_NOT_SUPPORTED, + "%s type not supported", + xb_opcode_kind_to_string (xb_opcode_get_kind (op1))); + return FALSE; +} + +static gboolean +xb_machine_func_ge_cb (XbMachine *self, + XbStack *stack, + gboolean *result, + gpointer user_data, + gpointer exec_data, + GError **error) +{ + g_autoptr(XbOpcode) op1 = xb_machine_stack_pop (self, stack); + g_autoptr(XbOpcode) op2 = xb_machine_stack_pop (self, stack); + + /* TEXT:TEXT */ + if (xb_opcode_cmp_str (op1) && xb_opcode_cmp_str (op2)) { + xb_stack_push_bool (stack, g_strcmp0 (xb_opcode_get_str (op2), + xb_opcode_get_str (op1)) >= 0); + return TRUE; + } + + /* INTE:INTE */ + if (xb_opcode_cmp_val (op1) && xb_opcode_cmp_val (op2)) { + xb_stack_push_bool (stack, xb_opcode_get_val (op2) >= xb_opcode_get_val (op1)); + return TRUE; + } + + /* INTE:TEXT */ + if (xb_opcode_cmp_val (op1) && xb_opcode_cmp_str (op2)) { + guint64 val = 0; + if (xb_opcode_get_str (op2) == NULL) { + xb_stack_push_bool (stack, FALSE); + return TRUE; + } + if (!g_ascii_string_to_unsigned (xb_opcode_get_str (op2), + 10, 0, G_MAXUINT32, + &val, error)) { + return FALSE; + } + xb_stack_push_bool (stack, val >= xb_opcode_get_val (op1)); + return TRUE; + } + + /* fail */ + g_set_error (error, + G_IO_ERROR, + G_IO_ERROR_NOT_SUPPORTED, + "%s:%s types not supported", + xb_opcode_kind_to_string (xb_opcode_get_kind (op1)), + xb_opcode_kind_to_string (xb_opcode_get_kind (op2))); + return FALSE; +} + +static gboolean +xb_machine_func_contains_cb (XbMachine *self, + XbStack *stack, + gboolean *result, + gpointer user_data, + gpointer exec_data, + GError **error) +{ + g_autoptr(XbOpcode) op1 = xb_machine_stack_pop (self, stack); + g_autoptr(XbOpcode) op2 = xb_machine_stack_pop (self, stack); + + /* TEXT:TEXT */ + if (xb_opcode_cmp_str (op1) && xb_opcode_cmp_str (op2)) { + xb_stack_push_bool (stack, xb_string_contains (xb_opcode_get_str (op2), + xb_opcode_get_str (op1))); + return TRUE; + } + + /* fail */ + g_set_error (error, + G_IO_ERROR, + G_IO_ERROR_NOT_SUPPORTED, + "%s:%s types not supported", + xb_opcode_kind_to_string (xb_opcode_get_kind (op1)), + xb_opcode_kind_to_string (xb_opcode_get_kind (op2))); + return FALSE; +} + +static gboolean +xb_machine_func_starts_with_cb (XbMachine *self, + XbStack *stack, + gboolean *result, + gpointer user_data, + gpointer exec_data, + GError **error) +{ + g_autoptr(XbOpcode) op1 = xb_machine_stack_pop (self, stack); + g_autoptr(XbOpcode) op2 = xb_machine_stack_pop (self, stack); + + /* TEXT:TEXT */ + if (xb_opcode_cmp_str (op1) && xb_opcode_cmp_str (op2)) { + xb_stack_push_bool (stack, g_str_has_prefix (xb_opcode_get_str (op2), + xb_opcode_get_str (op1))); + return TRUE; + } + + /* fail */ + g_set_error (error, + G_IO_ERROR, + G_IO_ERROR_NOT_SUPPORTED, + "%s:%s types not supported", + xb_opcode_kind_to_string (xb_opcode_get_kind (op1)), + xb_opcode_kind_to_string (xb_opcode_get_kind (op2))); + return FALSE; +} + +static gboolean +xb_machine_func_ends_with_cb (XbMachine *self, + XbStack *stack, + gboolean *result, + gpointer user_data, + gpointer exec_data, + GError **error) +{ + g_autoptr(XbOpcode) op1 = xb_machine_stack_pop (self, stack); + g_autoptr(XbOpcode) op2 = xb_machine_stack_pop (self, stack); + + /* TEXT:TEXT */ + if (xb_opcode_cmp_str (op1) && xb_opcode_cmp_str (op2)) { + xb_stack_push_bool (stack, g_str_has_suffix (xb_opcode_get_str (op2), + xb_opcode_get_str (op1))); + return TRUE; + } + + /* fail */ + g_set_error (error, + G_IO_ERROR, + G_IO_ERROR_NOT_SUPPORTED, + "%s:%s types not supported", + xb_opcode_kind_to_string (xb_opcode_get_kind (op1)), + xb_opcode_kind_to_string (xb_opcode_get_kind (op2))); + return FALSE; +} + +static gboolean +xb_machine_func_number_cb (XbMachine *self, + XbStack *stack, + gboolean *result, + gpointer user_data, + gpointer exec_data, + GError **error) +{ + g_autoptr(XbOpcode) op1 = xb_machine_stack_pop (self, stack); + + /* TEXT */ + if (xb_opcode_cmp_str (op1)) { + guint64 val = 0; + if (xb_opcode_get_str (op1) == NULL) { + xb_stack_push_bool (stack, FALSE); + return TRUE; + } + if (!g_ascii_string_to_unsigned (xb_opcode_get_str (op1), + 10, 0, G_MAXUINT32, + &val, error)) { + return FALSE; + } + xb_machine_stack_push_integer (self, stack, val); + return TRUE; + } + + /* fail */ + g_set_error (error, + G_IO_ERROR, + G_IO_ERROR_NOT_SUPPORTED, + "%s types not supported", + xb_opcode_kind_to_string (xb_opcode_get_kind (op1))); + return FALSE; +} + +static gboolean +xb_machine_func_strlen_cb (XbMachine *self, + XbStack *stack, + gboolean *result, + gpointer user_data, + gpointer exec_data, + GError **error) +{ + g_autoptr(XbOpcode) op1 = xb_machine_stack_pop (self, stack); + + /* TEXT */ + if (xb_opcode_cmp_str (op1)) { + if (xb_opcode_get_str (op1) == NULL) { + xb_stack_push_bool (stack, FALSE); + return TRUE; + } + xb_machine_stack_push_integer (self, stack, strlen (xb_opcode_get_str (op1))); + return TRUE; + } + + /* fail */ + g_set_error (error, + G_IO_ERROR, + G_IO_ERROR_NOT_SUPPORTED, + "%s types not supported", + xb_opcode_kind_to_string (xb_opcode_get_kind (op1))); + return FALSE; +} + +static gboolean +xb_machine_func_string_cb (XbMachine *self, + XbStack *stack, + gboolean *result, + gpointer user_data, + gpointer exec_data, + GError **error) +{ + g_autoptr(XbOpcode) op1 = xb_machine_stack_pop (self, stack); + + /* INTE */ + if (xb_opcode_cmp_val (op1)) { + gchar *tmp = g_strdup_printf ("%" G_GUINT32_FORMAT, + xb_opcode_get_val (op1)); + xb_machine_stack_push_text_steal (self, stack, tmp); + return TRUE; + } + + /* fail */ + g_set_error (error, + G_IO_ERROR, + G_IO_ERROR_NOT_SUPPORTED, + "%s types not supported", + xb_opcode_kind_to_string (xb_opcode_get_kind (op1))); + return FALSE; +} + +static void +xb_machine_opcode_fixup_free (XbMachineOpcodeFixupItem *item) +{ + if (item->user_data_free != NULL) + item->user_data_free (item->user_data); + g_slice_free (XbMachineOpcodeFixupItem, item); +} + +static void +xb_machine_func_free (XbMachineMethodItem *item) +{ + if (item->user_data_free != NULL) + item->user_data_free (item->user_data); + g_free (item->name); + g_slice_free (XbMachineMethodItem, item); +} + +static void +xb_machine_text_handler_free (XbMachineTextHandlerItem *item) +{ + if (item->user_data_free != NULL) + item->user_data_free (item->user_data); + g_slice_free (XbMachineTextHandlerItem, item); +} + +static void +xb_machine_operator_free (XbMachineOperator *op) +{ + g_free (op->str); + g_free (op->name); + g_slice_free (XbMachineOperator, op); +} + +static void +xb_machine_init (XbMachine *self) +{ + XbMachinePrivate *priv = GET_PRIVATE (self); + priv->stack_size = 10; + priv->methods = g_ptr_array_new_with_free_func ((GDestroyNotify) xb_machine_func_free); + priv->operators = g_ptr_array_new_with_free_func ((GDestroyNotify) xb_machine_operator_free); + priv->text_handlers = g_ptr_array_new_with_free_func ((GDestroyNotify) xb_machine_text_handler_free); + priv->opcode_fixup = g_hash_table_new_full (g_str_hash, g_str_equal, + g_free, (GDestroyNotify) xb_machine_opcode_fixup_free); + + /* built-in functions */ + xb_machine_add_method (self, "and", 2, xb_machine_func_and_cb, NULL, NULL); + xb_machine_add_method (self, "or", 2, xb_machine_func_or_cb, NULL, NULL); + xb_machine_add_method (self, "eq", 2, xb_machine_func_eq_cb, NULL, NULL); + xb_machine_add_method (self, "ne", 2, xb_machine_func_ne_cb, NULL, NULL); + xb_machine_add_method (self, "lt", 2, xb_machine_func_lt_cb, NULL, NULL); + xb_machine_add_method (self, "gt", 2, xb_machine_func_gt_cb, NULL, NULL); + xb_machine_add_method (self, "le", 2, xb_machine_func_le_cb, NULL, NULL); + xb_machine_add_method (self, "ge", 2, xb_machine_func_ge_cb, NULL, NULL); + xb_machine_add_method (self, "not", 1, xb_machine_func_not_cb, NULL, NULL); + xb_machine_add_method (self, "lower-case", 1, xb_machine_func_lower_cb, NULL, NULL); + xb_machine_add_method (self, "upper-case", 1, xb_machine_func_upper_cb, NULL, NULL); + xb_machine_add_method (self, "contains", 2, xb_machine_func_contains_cb, NULL, NULL); + xb_machine_add_method (self, "starts-with", 2, xb_machine_func_starts_with_cb, NULL, NULL); + xb_machine_add_method (self, "ends-with", 2, xb_machine_func_ends_with_cb, NULL, NULL); + xb_machine_add_method (self, "string", 1, xb_machine_func_string_cb, NULL, NULL); + xb_machine_add_method (self, "number", 1, xb_machine_func_number_cb, NULL, NULL); + xb_machine_add_method (self, "string-length", 1, xb_machine_func_strlen_cb, NULL, NULL); + + /* built-in operators */ + xb_machine_add_operator (self, " and ", "and"); + xb_machine_add_operator (self, " or ", "or"); + xb_machine_add_operator (self, "&&", "and"); + xb_machine_add_operator (self, "||", "or"); + xb_machine_add_operator (self, "!=", "ne"); + xb_machine_add_operator (self, "<=", "le"); + xb_machine_add_operator (self, ">=", "ge"); + xb_machine_add_operator (self, "==", "eq"); + xb_machine_add_operator (self, "=", "eq"); + xb_machine_add_operator (self, ">", "gt"); + xb_machine_add_operator (self, "<", "lt"); +} + +static void +xb_machine_finalize (GObject *obj) +{ + XbMachine *self = XB_MACHINE (obj); + XbMachinePrivate *priv = GET_PRIVATE (self); + g_ptr_array_unref (priv->methods); + g_ptr_array_unref (priv->operators); + g_ptr_array_unref (priv->text_handlers); + g_hash_table_unref (priv->opcode_fixup); + G_OBJECT_CLASS (xb_machine_parent_class)->finalize (obj); +} + +static void +xb_machine_class_init (XbMachineClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + object_class->finalize = xb_machine_finalize; +} + +/** + * xb_machine_new: + * + * Creates a new virtual machine. + * + * Returns: a new #XbMachine + * + * Since: 0.1.1 + **/ +XbMachine * +xb_machine_new (void) +{ + return g_object_new (XB_TYPE_MACHINE, NULL); +} diff --git a/src/xb-machine.h b/src/xb-machine.h new file mode 100644 index 0000000..4f28681 --- /dev/null +++ b/src/xb-machine.h @@ -0,0 +1,151 @@ +/* + * Copyright (C) 2018 Richard Hughes + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#pragma once + +#include + +#include "xb-opcode.h" +#include "xb-stack.h" + +G_BEGIN_DECLS + +#define XB_TYPE_MACHINE (xb_machine_get_type ()) +G_DECLARE_DERIVABLE_TYPE (XbMachine, xb_machine, XB, MACHINE, GObject) + +struct _XbMachineClass { + GObjectClass parent_class; + /*< private >*/ + void (*_xb_reserved1) (void); + void (*_xb_reserved2) (void); + void (*_xb_reserved3) (void); + void (*_xb_reserved4) (void); + void (*_xb_reserved5) (void); + void (*_xb_reserved6) (void); + void (*_xb_reserved7) (void); +}; + +/** + * XbMachineDebugFlags: + * @XB_MACHINE_DEBUG_FLAG_NONE: No debug flags to use + * @XB_MACHINE_DEBUG_FLAG_SHOW_STACK: Show the stack addition and removal + * @XB_MACHINE_DEBUG_FLAG_SHOW_PARSING: Show the XPath predicate parsing + * @XB_MACHINE_DEBUG_FLAG_SHOW_OPTIMIZER: Show the optimizer operation + * + * The flags to control the amount of debugging is generated. + **/ +typedef enum { + XB_MACHINE_DEBUG_FLAG_NONE = 0, + XB_MACHINE_DEBUG_FLAG_SHOW_STACK = 1 << 0, + XB_MACHINE_DEBUG_FLAG_SHOW_PARSING = 1 << 1, + XB_MACHINE_DEBUG_FLAG_SHOW_OPTIMIZER = 1 << 2, + /*< private >*/ + XB_MACHINE_DEBUG_FLAG_LAST +} XbMachineDebugFlags; + +/** + * XbMachineParseFlags: + * @XB_MACHINE_PARSE_FLAG_NONE: No flags set + * @XB_MACHINE_PARSE_FLAG_OPTIMIZE: Run an optimization pass on the predicate + * + * The flags to control the parsing behaviour. + **/ +typedef enum { + XB_MACHINE_PARSE_FLAG_NONE = 0, + XB_MACHINE_PARSE_FLAG_OPTIMIZE = 1 << 0, + /*< private >*/ + XB_MACHINE_PARSE_FLAG_LAST +} XbMachineParseFlags; + +typedef gboolean (*XbMachineOpcodeFixupFunc) (XbMachine *self, + XbStack *opcodes, + gpointer user_data, + GError **error); +typedef gboolean (*XbMachineTextHandlerFunc) (XbMachine *self, + XbStack *opcodes, + const gchar *text, + gboolean *handled, + gpointer user_data, + GError **error); +typedef gboolean (*XbMachineMethodFunc) (XbMachine *self, + XbStack *stack, + gboolean *result_unused, + gpointer exec_data, + gpointer user_data, + GError **error); + +XbMachine *xb_machine_new (void); +void xb_machine_set_debug_flags (XbMachine *self, + XbMachineDebugFlags flags); +XbStack *xb_machine_parse (XbMachine *self, + const gchar *text, + gssize text_len, + GError **error) +G_DEPRECATED_FOR(xb_machine_parse_full); +XbStack *xb_machine_parse_full (XbMachine *self, + const gchar *text, + gssize text_len, + XbMachineParseFlags flags, + GError **error); +gboolean xb_machine_run (XbMachine *self, + XbStack *opcodes, + gboolean *result, + gpointer exec_data, + GError **error); + +void xb_machine_add_opcode_fixup (XbMachine *self, + const gchar *opcodes_sig, + XbMachineOpcodeFixupFunc fixup_cb, + gpointer user_data, + GDestroyNotify user_data_free); +void xb_machine_add_text_handler (XbMachine *self, + XbMachineTextHandlerFunc handler_cb, + gpointer user_data, + GDestroyNotify user_data_free); +void xb_machine_add_method (XbMachine *self, + const gchar *name, + guint n_opcodes, + XbMachineMethodFunc method_cb, + gpointer user_data, + GDestroyNotify user_data_free); +void xb_machine_add_operator (XbMachine *self, + const gchar *str, + const gchar *name); + +XbOpcode *xb_machine_opcode_func_new (XbMachine *self, + const gchar *func_name); +gchar *xb_machine_opcode_to_string (XbMachine *self, + XbOpcode *opcode) +G_DEPRECATED_FOR(xb_opcode_to_string); +gchar *xb_machine_opcodes_to_string (XbMachine *self, + XbStack *opcodes) +G_DEPRECATED_FOR(xb_stack_to_string); + +XbOpcode *xb_machine_stack_pop (XbMachine *self, + XbStack *stack); +void xb_machine_stack_push (XbMachine *self, + XbStack *stack, + XbOpcode *opcode); +void xb_machine_stack_push_steal (XbMachine *self, + XbStack *stack, + XbOpcode *opcode); +void xb_machine_stack_push_text (XbMachine *self, + XbStack *stack, + const gchar *str); +void xb_machine_stack_push_text_static (XbMachine *self, + XbStack *stack, + const gchar *str); +void xb_machine_stack_push_text_steal (XbMachine *self, + XbStack *stack, + gchar *str); +void xb_machine_stack_push_integer (XbMachine *self, + XbStack *stack, + guint32 val); +void xb_machine_set_stack_size (XbMachine *self, + guint stack_size); +guint xb_machine_get_stack_size (XbMachine *self); + +G_END_DECLS diff --git a/src/xb-node-private.h b/src/xb-node-private.h new file mode 100644 index 0000000..46461c3 --- /dev/null +++ b/src/xb-node-private.h @@ -0,0 +1,21 @@ +/* + * Copyright (C) 2018 Richard Hughes + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#pragma once + +#include + +#include "xb-silo-private.h" +#include "xb-node.h" + +G_BEGIN_DECLS + +XbNode *xb_node_new (XbSilo *silo, + XbSiloNode *sn); +XbSiloNode *xb_node_get_sn (XbNode *self); +XbSilo *xb_node_get_silo (XbNode *self); + +G_END_DECLS diff --git a/src/xb-node-query.c b/src/xb-node-query.c new file mode 100644 index 0000000..1e2c137 --- /dev/null +++ b/src/xb-node-query.c @@ -0,0 +1,312 @@ +/* + * Copyright (C) 2018 Richard Hughes + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#define G_LOG_DOMAIN "XbNode" + +#include "config.h" + +#include + +#include "xb-node-private.h" +#include "xb-node-query.h" +#include "xb-silo-query-private.h" + +/** + * xb_node_query: + * @self: a #XbNode + * @xpath: an XPath, e.g. `id[abe.desktop]` + * @limit: maximum number of results to return, or 0 for "all" + * @error: the #GError, or %NULL + * + * Searches the silo using an XPath query, returning up to @limit results. + * + * It is safe to call this function from a different thread to the one that + * created the #XbSilo. + * + * Please note: Only a subset of XPath is supported. + * + * Returns: (transfer container) (element-type XbNode): results, or %NULL if unfound + * + * Since: 0.1.0 + **/ +GPtrArray * +xb_node_query (XbNode *self, const gchar *xpath, guint limit, GError **error) +{ + g_return_val_if_fail (XB_IS_NODE (self), NULL); + g_return_val_if_fail (xpath != NULL, NULL); + g_return_val_if_fail (error == NULL || *error == NULL, NULL); + return xb_silo_query_with_root (xb_node_get_silo (self), self, xpath, limit, error); +} + +/** + * xb_node_query_full: + * @self: a #XbNode + * @query: an #XbQuery + * @error: the #GError, or %NULL + * + * Searches the silo using an prepared query. + * + * It is safe to call this function from a different thread to the one that + * created the #XbSilo. + * + * Please note: Only a subset of XPath is supported. + * + * Returns: (transfer container) (element-type XbNode): results, or %NULL if unfound + * + * Since: 0.1.4 + **/ +GPtrArray * +xb_node_query_full (XbNode *self, XbQuery *query, GError **error) +{ + g_return_val_if_fail (XB_IS_NODE (self), NULL); + g_return_val_if_fail (XB_IS_QUERY (query), NULL); + g_return_val_if_fail (error == NULL || *error == NULL, NULL); + return xb_silo_query_with_root_full (xb_node_get_silo (self), self, query, error); +} + +/** + * xb_node_query_first_full: + * @self: a #XbNode + * @query: an #XbQuery + * @error: the #GError, or %NULL + * + * Searches the silo using an prepared query, returning up to one result. + * + * It is safe to call this function from a different thread to the one that + * created the #XbSilo. + * + * Please note: Only a subset of XPath is supported. + * + * Returns: (transfer full): a #XbNode, or %NULL if unfound + * + * Since: 0.1.11 + **/ +XbNode * +xb_node_query_first_full (XbNode *self, XbQuery *query, GError **error) +{ + g_autoptr(GPtrArray) results = NULL; + + g_return_val_if_fail (XB_IS_NODE (self), NULL); + g_return_val_if_fail (XB_IS_QUERY (query), NULL); + g_return_val_if_fail (error == NULL || *error == NULL, NULL); + + /* nodes don't have to include themselves as part of the query */ + results = xb_silo_query_with_root_full (xb_node_get_silo (self), self, query, error); + if (results == NULL) + return NULL; + return g_object_ref (g_ptr_array_index (results, 0)); +} + +/** + * xb_node_query_first: + * @self: a #XbNode + * @xpath: An XPath, e.g. `/components/component[@type=desktop]/id[abe.desktop]` + * @error: the #GError, or %NULL + * + * Searches the node using an XPath query, returning up to one result. + * + * Please note: Only a tiny subset of XPath 1.0 is supported. + * + * Returns: (transfer full): a #XbNode, or %NULL if unfound + * + * Since: 0.1.0 + **/ +XbNode * +xb_node_query_first (XbNode *self, const gchar *xpath, GError **error) +{ + g_autoptr(GPtrArray) results = NULL; + + g_return_val_if_fail (XB_IS_NODE (self), NULL); + g_return_val_if_fail (xpath != NULL, NULL); + g_return_val_if_fail (error == NULL || *error == NULL, NULL); + + /* nodes don't have to include themselves as part of the query */ + results = xb_silo_query_with_root (xb_node_get_silo (self), self, xpath, 1, error); + if (results == NULL) + return NULL; + return g_object_ref (g_ptr_array_index (results, 0)); +} + +/** + * xb_node_query_text: + * @self: a #XbNode + * @xpath: An XPath, e.g. `/components/component[@type=desktop]/id[abe.desktop]` + * @error: the #GError, or %NULL + * + * Searches the node using an XPath query, returning up to one result. + * + * It is safe to call this function from a different thread to the one that + * created the #XbSilo. + * + * Please note: Only a subset of XPath is supported. + * + * Returns: (transfer none): a string, or %NULL if unfound + * + * Since: 0.1.0 + **/ +const gchar * +xb_node_query_text (XbNode *self, const gchar *xpath, GError **error) +{ + const gchar *tmp; + g_autoptr(XbNode) n = NULL; + + g_return_val_if_fail (XB_IS_NODE (self), NULL); + g_return_val_if_fail (xpath != NULL, NULL); + g_return_val_if_fail (error == NULL || *error == NULL, NULL); + + n = xb_node_query_first (self, xpath, error); + if (n == NULL) + return NULL; + tmp = xb_node_get_text (n); + if (tmp == NULL) { + g_set_error_literal (error, + G_IO_ERROR, + G_IO_ERROR_NOT_FOUND, + "no text data"); + return NULL; + } + return tmp; +} + +/** + * xb_node_query_attr: + * @self: a #XbNode + * @xpath: An XPath, e.g. `/components/component[@type=desktop]/id[abe.desktop]` + * @name: an attribute name, e.g. `type` + * @error: the #GError, or %NULL + * + * Searches the node using an XPath query, returning up to one result. + * + * It is safe to call this function from a different thread to the one that + * created the #XbSilo. + * + * Please note: Only a subset of XPath is supported. + * + * Returns: (transfer none): a string, or %NULL if unfound + * + * Since: 0.1.0 + **/ +const gchar * +xb_node_query_attr (XbNode *self, const gchar *xpath, const gchar *name, GError **error) +{ + const gchar *tmp; + g_autoptr(XbNode) n = NULL; + + g_return_val_if_fail (XB_IS_NODE (self), NULL); + g_return_val_if_fail (xpath != NULL, NULL); + g_return_val_if_fail (error == NULL || *error == NULL, NULL); + + n = xb_node_query_first (self, xpath, error); + if (n == NULL) + return NULL; + tmp = xb_node_get_attr (n, name); + if (tmp == NULL) { + g_set_error_literal (error, + G_IO_ERROR, + G_IO_ERROR_NOT_FOUND, + "no text data"); + return NULL; + } + return tmp; +} + +/** + * xb_node_query_export: + * @self: a #XbNode + * @xpath: An XPath, e.g. `/components/component[@type=desktop]/id[abe.desktop]` + * @error: the #GError, or %NULL + * + * Searches the node using an XPath query, returning an XML string of the + * result and any children. + * + * It is safe to call this function from a different thread to the one that + * created the #XbSilo. + * + * Please note: Only a subset of XPath is supported. + * + * Returns: (transfer none): a string, or %NULL if unfound + * + * Since: 0.1.0 + **/ +gchar * +xb_node_query_export (XbNode *self, const gchar *xpath, GError **error) +{ + g_autoptr(XbNode) n = NULL; + + g_return_val_if_fail (XB_IS_NODE (self), NULL); + g_return_val_if_fail (xpath != NULL, NULL); + g_return_val_if_fail (error == NULL || *error == NULL, NULL); + + n = xb_node_query_first (self, xpath, error); + if (n == NULL) + return NULL; + return xb_node_export (n, XB_NODE_EXPORT_FLAG_NONE, error); +} + +/** + * xb_node_query_text_as_uint: + * @self: a #XbNode + * @xpath: An XPath, e.g. `/components/component[@type=desktop]/id[abe.desktop]` + * @error: the #GError, or %NULL + * + * Searches the node using an XPath query, returning up to one result. + * + * It is safe to call this function from a different thread to the one that + * created the #XbSilo. + * + * Please note: Only a subset of XPath is supported. + * + * Returns: a guint64, or %G_MAXUINT64 if unfound + * + * Since: 0.1.0 + **/ +guint64 +xb_node_query_text_as_uint (XbNode *self, const gchar *xpath, GError **error) +{ + g_autoptr(XbNode) n = NULL; + + g_return_val_if_fail (XB_IS_NODE (self), G_MAXUINT64); + g_return_val_if_fail (xpath != NULL, G_MAXUINT64); + g_return_val_if_fail (error == NULL || *error == NULL, G_MAXUINT64); + + n = xb_node_query_first (self, xpath, error); + if (n == NULL) + return G_MAXUINT64; + return xb_node_get_text_as_uint (n); +} + +/** + * xb_node_query_attr_as_uint: + * @self: a #XbNode + * @xpath: An XPath, e.g. `/components/component[@type=desktop]/id[abe.desktop]` + * @name: an attribute name, e.g. `type` + * @error: the #GError, or %NULL + * + * Searches the node using an XPath query, returning up to one result. + * + * It is safe to call this function from a different thread to the one that + * created the #XbSilo. + * + * Please note: Only a subset of XPath is supported. + * + * Returns: a guint64, or %G_MAXUINT64 if unfound + * + * Since: 0.1.0 + **/ +guint64 +xb_node_query_attr_as_uint (XbNode *self, const gchar *xpath, const gchar *name, GError **error) +{ + g_autoptr(XbNode) n = NULL; + + g_return_val_if_fail (XB_IS_NODE (self), G_MAXUINT64); + g_return_val_if_fail (xpath != NULL, G_MAXUINT64); + g_return_val_if_fail (error == NULL || *error == NULL, G_MAXUINT64); + + n = xb_node_query_first (self, xpath, error); + if (n == NULL) + return G_MAXUINT64; + return xb_node_get_attr_as_uint (n, name); +} diff --git a/src/xb-node-query.h b/src/xb-node-query.h new file mode 100644 index 0000000..f3990c6 --- /dev/null +++ b/src/xb-node-query.h @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2018 Richard Hughes + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#pragma once + +#include + +#include "xb-query.h" +#include "xb-node.h" + +G_BEGIN_DECLS + +GPtrArray *xb_node_query (XbNode *self, + const gchar *xpath, + guint limit, + GError **error); +GPtrArray *xb_node_query_full (XbNode *self, + XbQuery *query, + GError **error); +XbNode *xb_node_query_first (XbNode *self, + const gchar *xpath, + GError **error); +XbNode *xb_node_query_first_full (XbNode *self, + XbQuery *query, + GError **error); +const gchar *xb_node_query_text (XbNode *self, + const gchar *xpath, + GError **error); +guint64 xb_node_query_text_as_uint (XbNode *self, + const gchar *xpath, + GError **error); +const gchar *xb_node_query_attr (XbNode *self, + const gchar *xpath, + const gchar *name, + GError **error); +guint64 xb_node_query_attr_as_uint (XbNode *self, + const gchar *xpath, + const gchar *name, + GError **error); +gchar *xb_node_query_export (XbNode *self, + const gchar *xpath, + GError **error); + +G_END_DECLS diff --git a/src/xb-node.c b/src/xb-node.c new file mode 100644 index 0000000..5376a2e --- /dev/null +++ b/src/xb-node.c @@ -0,0 +1,493 @@ +/* + * Copyright (C) 2018 Richard Hughes + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#define G_LOG_DOMAIN "XbNode" + +#include "config.h" + +#include +#include + +#include "xb-node-private.h" +#include "xb-silo-export-private.h" + +typedef struct { + GObject parent_instance; + XbSilo *silo; + XbSiloNode *sn; +} XbNodePrivate; + +G_DEFINE_TYPE_WITH_PRIVATE (XbNode, xb_node, G_TYPE_OBJECT) +#define GET_PRIVATE(o) (xb_node_get_instance_private (o)) + +/** + * xb_node_get_data: + * @self: a #XbNode + * @key: a string key, e.g. `fwupd::RemoteId` + * + * Gets any data that has been set on the node using xb_node_set_data(). + * + * Returns: (transfer none): a #GBytes, or %NULL if not found + * + * Since: 0.1.0 + **/ +GBytes * +xb_node_get_data (XbNode *self, const gchar *key) +{ + g_return_val_if_fail (XB_IS_NODE (self), NULL); + g_return_val_if_fail (key != NULL, NULL); + return g_object_get_data (G_OBJECT (self), key); +} + +/** + * xb_node_set_data: + * @self: a #XbNode + * @key: a string key, e.g. `fwupd::RemoteId` + * @data: a #GBytes + * + * Sets some data on the node which can be retrieved using xb_node_get_data(). + * + * Since: 0.1.0 + **/ +void +xb_node_set_data (XbNode *self, const gchar *key, GBytes *data) +{ + g_return_if_fail (XB_IS_NODE (self)); + g_return_if_fail (key != NULL); + g_return_if_fail (data != NULL); + g_object_set_data_full (G_OBJECT (self), key, + g_bytes_ref (data), + (GDestroyNotify) g_bytes_unref); +} + +/** + * xb_node_get_sn: (skip) + * @self: a #XbNode + * + * Gets the #XbSiloNode for the node. + * + * Returns: (transfer none): a #XbSiloNode + * + * Since: 0.1.0 + **/ +XbSiloNode * +xb_node_get_sn (XbNode *self) +{ + XbNodePrivate *priv = GET_PRIVATE (self); + return priv->sn; +} + +/** + * xb_node_get_silo: (skip) + * @self: a #XbNode + * + * Gets the #XbSilo for the node. + * + * Returns: (transfer none): a #XbSilo + * + * Since: 0.1.0 + **/ +XbSilo * +xb_node_get_silo (XbNode *self) +{ + XbNodePrivate *priv = GET_PRIVATE (self); + return priv->silo; +} + +/** + * xb_node_get_root: + * @self: a #XbNode + * + * Gets the root node for the node. + * + * Returns: (transfer full): a #XbNode, or %NULL + * + * Since: 0.1.0 + **/ +XbNode * +xb_node_get_root (XbNode *self) +{ + XbNodePrivate *priv = GET_PRIVATE (self); + XbSiloNode *sn; + + g_return_val_if_fail (XB_IS_NODE (self), NULL); + + sn = xb_silo_get_sroot (priv->silo); + if (sn == NULL) + return NULL; + return xb_silo_node_create (priv->silo, sn); +} + +/** + * xb_node_get_parent: + * @self: a #XbNode + * + * Gets the parent node for the current node. + * + * Returns: (transfer full): a #XbNode, or %NULL + * + * Since: 0.1.0 + **/ +XbNode * +xb_node_get_parent (XbNode *self) +{ + XbNodePrivate *priv = GET_PRIVATE (self); + XbSiloNode *sn; + + g_return_val_if_fail (XB_IS_NODE (self), NULL); + + sn = xb_silo_node_get_parent (priv->silo, priv->sn); + if (sn == NULL) + return NULL; + return xb_silo_node_create (priv->silo, sn); +} + +/** + * xb_node_get_next: + * @self: a #XbNode + * + * Gets the next sibling node for the current node. + * + * Returns: (transfer full): a #XbNode, or %NULL + * + * Since: 0.1.0 + **/ +XbNode * +xb_node_get_next (XbNode *self) +{ + XbNodePrivate *priv = GET_PRIVATE (self); + XbSiloNode *sn; + + g_return_val_if_fail (XB_IS_NODE (self), NULL); + + sn = xb_silo_node_get_next (priv->silo, priv->sn); + if (sn == NULL) + return NULL; + return xb_silo_node_create (priv->silo, sn); +} + +/** + * xb_node_get_child: + * @self: a #XbNode + * + * Gets the first child node for the current node. + * + * Returns: (transfer full): a #XbNode, or %NULL + * + * Since: 0.1.0 + **/ +XbNode * +xb_node_get_child (XbNode *self) +{ + XbNodePrivate *priv = GET_PRIVATE (self); + XbSiloNode *sn; + + g_return_val_if_fail (XB_IS_NODE (self), NULL); + + sn = xb_silo_node_get_child (priv->silo, priv->sn); + if (sn == NULL) + return NULL; + return xb_silo_node_create (priv->silo, sn); +} + +/** + * xb_node_get_children: + * @self: a #XbNode + * + * Gets all the children for the current node. + * + * Returns: (transfer container) (element-type XbNode): an array of children + * + * Since: 0.1.0 + **/ +GPtrArray * +xb_node_get_children (XbNode *self) +{ + XbNode *n; + GPtrArray *array = g_ptr_array_new_with_free_func ((GDestroyNotify) g_object_unref); + + /* add all children */ + n = xb_node_get_child (self); + while (n != NULL) { + g_ptr_array_add (array, n); + n = xb_node_get_next (n); + } + return array; +} + +/** + * xb_node_get_text: + * @self: a #XbNode + * + * Gets the text data for a specific node. + * + * Returns: a string, or %NULL for unset + * + * Since: 0.1.0 + **/ +const gchar * +xb_node_get_text (XbNode *self) +{ + XbNodePrivate *priv = GET_PRIVATE (self); + g_return_val_if_fail (XB_IS_NODE (self), NULL); + return xb_silo_node_get_text (priv->silo, priv->sn); +} + +/** + * xb_node_get_text_as_uint: + * @self: a #XbNode + * + * Gets some attribute text data for a specific node. + * + * Returns: a guint64, or %G_MAXUINT64 if unfound + * + * Since: 0.1.0 + **/ +guint64 +xb_node_get_text_as_uint (XbNode *self) +{ + XbNodePrivate *priv = GET_PRIVATE (self); + const gchar *tmp; + + g_return_val_if_fail (XB_IS_NODE (self), G_MAXUINT64); + + tmp = xb_silo_node_get_text (priv->silo, priv->sn);; + if (tmp == NULL) + return G_MAXUINT64; + if (g_str_has_prefix (tmp, "0x")) + return g_ascii_strtoull (tmp + 2, NULL, 16); + return g_ascii_strtoull (tmp, NULL, 10); +} + +/** + * xb_node_get_tail: + * @self: a #XbNode + * + * Gets the tail data for a specific node. + * + * Returns: a string, or %NULL for unset + * + * Since: 0.1.12 + **/ +const gchar * +xb_node_get_tail (XbNode *self) +{ + XbNodePrivate *priv = GET_PRIVATE (self); + g_return_val_if_fail (XB_IS_NODE (self), NULL); + return xb_silo_node_get_tail (priv->silo, priv->sn); +} + +/** + * xb_node_get_element: + * @self: a #XbNode + * + * Gets the element name for a specific node. + * + * Returns: a string, or %NULL for unset + * + * Since: 0.1.0 + **/ +const gchar * +xb_node_get_element (XbNode *self) +{ + XbNodePrivate *priv = GET_PRIVATE (self); + g_return_val_if_fail (XB_IS_NODE (self), NULL); + return xb_silo_node_get_element (priv->silo, priv->sn); +} + +/** + * xb_node_get_attr: + * @self: a #XbNode + * @name: an attribute name, e.g. "type" + * + * Gets some attribute text data for a specific node. + * + * Returns: a string, or %NULL for unset + * + * Since: 0.1.0 + **/ +const gchar * +xb_node_get_attr (XbNode *self, const gchar *name) +{ + XbNodePrivate *priv = GET_PRIVATE (self); + XbSiloAttr *a; + + g_return_val_if_fail (XB_IS_NODE (self), NULL); + g_return_val_if_fail (name != NULL, NULL); + + a = xb_silo_node_get_attr_by_str (priv->silo, priv->sn, name); + if (a == NULL) + return NULL; + return xb_silo_from_strtab (priv->silo, a->attr_value); +} + +/** + * xb_node_get_attr_as_uint: + * @self: a #XbNode + * @name: an attribute name, e.g. `type` + * + * Gets some attribute text data for a specific node. + * + * Returns: a guint64, or %G_MAXUINT64 if unfound + * + * Since: 0.1.0 + **/ +guint64 +xb_node_get_attr_as_uint (XbNode *self, const gchar *name) +{ + const gchar *tmp; + + g_return_val_if_fail (XB_IS_NODE (self), G_MAXUINT64); + g_return_val_if_fail (name != NULL, G_MAXUINT64); + + tmp = xb_node_get_attr (self, name); + if (tmp == NULL) + return G_MAXUINT64; + if (g_str_has_prefix (tmp, "0x")) + return g_ascii_strtoull (tmp + 2, NULL, 16); + return g_ascii_strtoull (tmp, NULL, 10); +} + +/** + * xb_node_get_depth: + * @self: a #XbNode + * + * Gets the depth of the node to a root. + * + * Returns: a integer, where 0 is the root node iself. + * + * Since: 0.1.0 + **/ +guint +xb_node_get_depth (XbNode *self) +{ + XbNodePrivate *priv = GET_PRIVATE (self); + g_return_val_if_fail (XB_IS_NODE (self), 0); + return xb_silo_node_get_depth (priv->silo, priv->sn); +} + +/** + * xb_node_export: + * @self: a #XbNode + * @flags: some #XbNodeExportFlags, e.g. #XB_NODE_EXPORT_FLAG_NONE + * @error: the #GError, or %NULL + * + * Exports the node back to XML. + * + * Returns: XML data, or %NULL for an error + * + * Since: 0.1.0 + **/ +gchar * +xb_node_export (XbNode *self, XbNodeExportFlags flags, GError **error) +{ + GString *xml; + g_return_val_if_fail (XB_IS_NODE (self), NULL); + g_return_val_if_fail (error == NULL || *error == NULL, NULL); + xml = xb_silo_export_with_root (xb_node_get_silo (self), self, flags, error); + if (xml == NULL) + return NULL; + return g_string_free (xml, FALSE); +} + +/** + * xb_node_transmogrify: + * @self: a #XbNode + * @func_text: (scope call): (allow-none): a #XbBuilderNodeTraverseFunc + * @func_tail: (scope call): (allow-none): a #XbBuilderNodeTraverseFunc + * @user_data: user pointer to pass to @func, or %NULL + * + * Traverses a tree starting from @self. It calls the given functions for each + * node visited. This allows transmogrification of the source, for instance + * converting the XML description to PangoMarkup or even something completely + * different like markdown. + * + * The traversal can be halted at any point by returning TRUE from @func. + * + * Returns: %TRUE if all nodes were visited + * + * Since: 0.1.12 + **/ +gboolean +xb_node_transmogrify (XbNode *self, + XbNodeTransmogrifyFunc func_text, + XbNodeTransmogrifyFunc func_tail, + gpointer user_data) +{ + g_autoptr(XbNode) n = NULL; + + g_return_val_if_fail (XB_IS_NODE (self), FALSE); + + /* all siblings */ + n = g_object_ref (self); + while (n != NULL) { + g_autoptr(XbNode) c = NULL; + g_autoptr(XbNode) tmp = NULL; + + /* head */ + if (func_text != NULL) { + if (func_text (n, user_data)) + return FALSE; + } + + /* all children */ + c = xb_node_get_child (n); + if (c != NULL) { + if (!xb_node_transmogrify (c, func_text, func_tail, user_data)) + return FALSE; + } + + /* tail */ + if (func_tail != NULL) { + if (func_tail (n, user_data)) + return FALSE; + } + + /* next sibling */ + tmp = xb_node_get_next (n); + g_set_object (&n, tmp); + } + return TRUE; +} + +static void +xb_node_init (XbNode *self) +{ +} + +static void +xb_node_finalize (GObject *obj) +{ + G_OBJECT_CLASS (xb_node_parent_class)->finalize (obj); +} + +static void +xb_node_class_init (XbNodeClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + object_class->finalize = xb_node_finalize; +} + +/** + * xb_node_new: (skip) + * @silo: A #XbSilo + * @sn: A #XbSiloNode + * + * Creates a new node. + * + * Returns: a new #XbNode + * + * Since: 0.1.0 + **/ +XbNode * +xb_node_new (XbSilo *silo, XbSiloNode *sn) +{ + XbNode *self = g_object_new (XB_TYPE_NODE, NULL); + XbNodePrivate *priv = GET_PRIVATE (self); + priv->silo = silo; + priv->sn = sn; + return self; +} diff --git a/src/xb-node.h b/src/xb-node.h new file mode 100644 index 0000000..a30cf5a --- /dev/null +++ b/src/xb-node.h @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2018 Richard Hughes + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#pragma once + +#include + +G_BEGIN_DECLS + +#define XB_TYPE_NODE (xb_node_get_type ()) +G_DECLARE_DERIVABLE_TYPE (XbNode, xb_node, XB, NODE, GObject) + +struct _XbNodeClass { + GObjectClass parent_class; + /*< private >*/ + void (*_xb_reserved1) (void); + void (*_xb_reserved2) (void); + void (*_xb_reserved3) (void); + void (*_xb_reserved4) (void); + void (*_xb_reserved5) (void); + void (*_xb_reserved6) (void); + void (*_xb_reserved7) (void); +}; + +/** + * XbNodeExportFlags: + * @XB_NODE_EXPORT_FLAG_NONE: No extra flags to use + * @XB_NODE_EXPORT_FLAG_ADD_HEADER: Add an XML header to the data + * @XB_NODE_EXPORT_FLAG_FORMAT_MULTILINE: Split up children with a newline + * @XB_NODE_EXPORT_FLAG_FORMAT_INDENT: Indent the XML by child depth + * @XB_NODE_EXPORT_FLAG_INCLUDE_SIBLINGS: Include the siblings when converting + * @XB_NODE_EXPORT_FLAG_ONLY_CHILDREN: Only export the children of the node + * + * The flags for converting to XML. + **/ +typedef enum { + XB_NODE_EXPORT_FLAG_NONE = 0, /* Since: 0.1.0 */ + XB_NODE_EXPORT_FLAG_ADD_HEADER = 1 << 0, /* Since: 0.1.0 */ + XB_NODE_EXPORT_FLAG_FORMAT_MULTILINE = 1 << 1, /* Since: 0.1.0 */ + XB_NODE_EXPORT_FLAG_FORMAT_INDENT = 1 << 2, /* Since: 0.1.0 */ + XB_NODE_EXPORT_FLAG_INCLUDE_SIBLINGS = 1 << 3, /* Since: 0.1.0 */ + XB_NODE_EXPORT_FLAG_ONLY_CHILDREN = 1 << 4, /* Since: 0.1.0 */ + /*< private >*/ + XB_NODE_EXPORT_FLAG_LAST +} XbNodeExportFlags; + +typedef gboolean (*XbNodeTransmogrifyFunc) (XbNode *self, + gpointer user_data); +gboolean xb_node_transmogrify (XbNode *self, + XbNodeTransmogrifyFunc func_text, + XbNodeTransmogrifyFunc func_tail, + gpointer user_data); + +gchar *xb_node_export (XbNode *self, + XbNodeExportFlags flags, + GError **error); +GBytes *xb_node_get_data (XbNode *self, + const gchar *key); +void xb_node_set_data (XbNode *self, + const gchar *key, + GBytes *data); + +XbNode *xb_node_get_root (XbNode *self); +XbNode *xb_node_get_parent (XbNode *self); +XbNode *xb_node_get_next (XbNode *self); +XbNode *xb_node_get_child (XbNode *self); +GPtrArray *xb_node_get_children (XbNode *self); +const gchar *xb_node_get_element (XbNode *self); +const gchar *xb_node_get_text (XbNode *self); +guint64 xb_node_get_text_as_uint (XbNode *self); +const gchar *xb_node_get_tail (XbNode *self); +const gchar *xb_node_get_attr (XbNode *self, + const gchar *name); +guint64 xb_node_get_attr_as_uint (XbNode *self, + const gchar *name); +guint xb_node_get_depth (XbNode *self); + + +G_END_DECLS diff --git a/src/xb-opcode-private.h b/src/xb-opcode-private.h new file mode 100644 index 0000000..a731165 --- /dev/null +++ b/src/xb-opcode-private.h @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2018 Richard Hughes + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#pragma once + +#include "xb-opcode.h" + +G_BEGIN_DECLS + +XbOpcode *xb_opcode_new (XbOpcodeKind kind, + const gchar *str, + guint32 val, + GDestroyNotify destroy_func); +XbOpcode *xb_opcode_bind_new (void); +gboolean xb_opcode_is_bound (XbOpcode *self); +void xb_opcode_bind_str (XbOpcode *self, + gchar *str, + GDestroyNotify destroy_func); +void xb_opcode_bind_val (XbOpcode *self, + guint32 val); +void xb_opcode_set_kind (XbOpcode *self, + XbOpcodeKind kind); +void xb_opcode_set_val (XbOpcode *self, + guint32 val); +gchar *xb_opcode_get_sig (XbOpcode *self); +XbOpcode *xb_opcode_bool_new (gboolean val); + +G_END_DECLS diff --git a/src/xb-opcode.c b/src/xb-opcode.c new file mode 100644 index 0000000..e128f92 --- /dev/null +++ b/src/xb-opcode.c @@ -0,0 +1,482 @@ +/* + * Copyright (C) 2018 Richard Hughes + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#define G_LOG_DOMAIN "XbMachine" + +#include "config.h" + +#include + +#include "xb-opcode-private.h" + +struct _XbOpcode { + gint ref; + XbOpcodeKind kind; + guint32 val; + gpointer ptr; + GDestroyNotify destroy_func; +}; + +/** + * xb_opcode_kind_to_string: + * @kind: a #XbOpcodeKind, e.g. %XB_OPCODE_KIND_FUNCTION + * + * Converts the opcode kind to a string. + * + * Returns: opcode kind, e.g. `FUNC` + * + * Since: 0.1.1 + **/ +const gchar * +xb_opcode_kind_to_string (XbOpcodeKind kind) +{ + if (kind == XB_OPCODE_KIND_FUNCTION) + return "FUNC"; + if (kind == XB_OPCODE_KIND_TEXT) + return "TEXT"; + if (kind == XB_OPCODE_KIND_INTEGER) + return "INTE"; + if (kind == XB_OPCODE_KIND_BOUND_UNSET) + return "BIND"; + if (kind == XB_OPCODE_KIND_BOUND_TEXT) + return "?TXT"; + if (kind == XB_OPCODE_KIND_BOUND_INTEGER) + return "?INT"; + if (kind == XB_OPCODE_KIND_INDEXED_TEXT) + return "TEXI"; + if (kind == XB_OPCODE_KIND_BOOLEAN) + return "BOOL"; + return NULL; +} + +/** + * xb_opcode_kind_from_string: + * @str: a string, e.g. `FUNC` + * + * Converts a string to an opcode kind. + * + * Returns: a #XbOpcodeKind, e.g. %XB_OPCODE_KIND_TEXT + * + * Since: 0.1.1 + **/ +XbOpcodeKind +xb_opcode_kind_from_string (const gchar *str) +{ + if (g_strcmp0 (str, "FUNC") == 0) + return XB_OPCODE_KIND_FUNCTION; + if (g_strcmp0 (str, "TEXT") == 0) + return XB_OPCODE_KIND_TEXT; + if (g_strcmp0 (str, "INTE") == 0) + return XB_OPCODE_KIND_INTEGER; + if (g_strcmp0 (str, "BIND") == 0) + return XB_OPCODE_KIND_BOUND_INTEGER; + if (g_strcmp0 (str, "?TXT") == 0) + return XB_OPCODE_KIND_BOUND_TEXT; + if (g_strcmp0 (str, "?INT") == 0) + return XB_OPCODE_KIND_BOUND_INTEGER; + if (g_strcmp0 (str, "TEXI") == 0) + return XB_OPCODE_KIND_INDEXED_TEXT; + if (g_strcmp0 (str, "BOOL") == 0) + return XB_OPCODE_KIND_BOOLEAN; + return XB_OPCODE_KIND_UNKNOWN; +} + +/* private */ +gchar * +xb_opcode_get_sig (XbOpcode *self) +{ + GString *str = g_string_new (xb_opcode_kind_to_string (self->kind)); + if (self->kind == XB_OPCODE_KIND_FUNCTION) { + g_string_append_printf (str, ":%s", + self->ptr != NULL ? (gchar *) self->ptr : "???"); + } + return g_string_free (str, FALSE); +} + +static const gchar * +xb_opcode_get_str_for_display (XbOpcode *self) +{ + if (self->ptr == NULL) + return "(null)"; + return self->ptr; +} + +/** + * xb_opcode_to_string: + * @self: a #XbOpcode + * + * Returns a string representing the specific opcode. + * + * Returns: text + * + * Since: 0.1.4 + **/ +gchar * +xb_opcode_to_string (XbOpcode *self) +{ + if (self->kind == XB_OPCODE_KIND_FUNCTION) + return g_strdup_printf ("%s()", xb_opcode_get_str_for_display (self)); + if (self->kind == XB_OPCODE_KIND_TEXT) + return g_strdup_printf ("'%s'", xb_opcode_get_str_for_display (self)); + if (self->kind == XB_OPCODE_KIND_INDEXED_TEXT) + return g_strdup_printf ("$'%s'", xb_opcode_get_str_for_display (self)); + if (self->kind == XB_OPCODE_KIND_INTEGER) + return g_strdup_printf ("%u", xb_opcode_get_val (self)); + if (self->kind == XB_OPCODE_KIND_BOUND_INTEGER) + return g_strdup ("?"); + if (self->kind == XB_OPCODE_KIND_BOUND_TEXT) + return g_strdup_printf ("?'%s'", xb_opcode_get_str_for_display (self)); + if (self->kind == XB_OPCODE_KIND_BOUND_INTEGER) + return g_strdup_printf ("?%u", xb_opcode_get_val (self)); + if (self->kind == XB_OPCODE_KIND_BOOLEAN) + return g_strdup (xb_opcode_get_val (self) ? "True" : "False"); + g_critical ("no to_string for kind %u", self->kind); + return NULL; +} + +/** + * xb_opcode_get_kind: + * @self: a #XbOpcode + * + * Gets the opcode kind. + * + * Returns: a #XbOpcodeKind, e.g. %XB_OPCODE_KIND_INTEGER + * + * Since: 0.1.1 + **/ +XbOpcodeKind +xb_opcode_get_kind (XbOpcode *self) +{ + return self->kind; +} + +/** + * xb_opcode_cmp_val: + * @self: a #XbOpcode + * + * Checks if the opcode can be compared using the integer value. + * + * Returns: #%TRUE if this opcode can be compared as an integer + * + * Since: 0.1.1 + **/ +inline gboolean +xb_opcode_cmp_val (XbOpcode *self) +{ + return self->kind == XB_OPCODE_KIND_INTEGER || + self->kind == XB_OPCODE_KIND_BOOLEAN || + self->kind == XB_OPCODE_KIND_BOUND_INTEGER; +} + +/** + * xb_opcode_cmp_str: + * @self: a #XbOpcode + * + * Checks if the opcode can be compared using the string value. + * + * Returns: #%TRUE if this opcode can be compared as an string + * + * Since: 0.1.1 + **/ +inline gboolean +xb_opcode_cmp_str (XbOpcode *self) +{ + return self->kind == XB_OPCODE_KIND_TEXT || + self->kind == XB_OPCODE_KIND_BOUND_TEXT || + self->kind == XB_OPCODE_KIND_INDEXED_TEXT; +} + +/* private */ +gboolean +xb_opcode_is_bound (XbOpcode *self) +{ + return (self->kind & XB_OPCODE_FLAG_BOUND) > 0; +} + +/** + * xb_opcode_get_val: + * @self: a #XbOpcode + * + * Gets the integer value stored in the opcode. This may be a function ID, + * a index into the string table or a literal integer. + * + * Returns: value, or 0 for unset. + * + * Since: 0.1.1 + **/ +guint32 +xb_opcode_get_val (XbOpcode *self) +{ + return self->val; +} + +/** + * xb_opcode_get_str: + * @self: a #XbOpcode + * + * Gets the string value stored on the opcode. + * + * Returns: a string, or %NULL if unset + * + * Since: 0.1.1 + **/ +const gchar * +xb_opcode_get_str (XbOpcode *self) +{ + return self->ptr; +} + +/** + * xb_opcode_unref: + * @self: a #XbOpcode + * + * Decrements the reference count of the opcode, freeing the object when the + * refcount drops to zero. + * + * Since: 0.1.1 + **/ +void +xb_opcode_unref (XbOpcode *self) +{ + g_assert (self->ref > 0); + if (--self->ref > 0) + return; + if (self->destroy_func) + self->destroy_func (self->ptr); + g_slice_free (XbOpcode, self); +} + +/** + * xb_opcode_ref: + * @self: a #XbOpcode + * + * Increments the refcount of the opcode. + * + * Returns: (transfer none): the original @self #XbOpcode instance + * + * Since: 0.1.1 + **/ +XbOpcode * +xb_opcode_ref (XbOpcode *self) +{ + self->ref++; + return self; +} + +/** + * xb_opcode_text_new: + * @str: a string + * + * Creates a new text literal opcode. The @str argument is copied internally + * and is not tied to the lifecycle of the #XbOpcode. + * + * Returns: (transfer full): a #XbOpcode + * + * Since: 0.1.1 + **/ +XbOpcode * +xb_opcode_text_new (const gchar *str) +{ + XbOpcode *self = g_slice_new0 (XbOpcode); + self->ref = 1; + self->kind = XB_OPCODE_KIND_TEXT; + self->ptr = g_strdup (str); + self->destroy_func = g_free; + return self; +} + +/** + * xb_opcode_new: + * @kind: a #XbOpcodeKind, e.g. %XB_OPCODE_KIND_INTEGER + * @str: a string + * @val: a integer value + * @destroy_func: (nullable): a #GDestroyNotify, e.g. g_free() + * + * Creates a new opcode. + * + * Returns: (transfer full): a #XbOpcode + * + * Since: 0.1.4 + **/ +XbOpcode * +xb_opcode_new (XbOpcodeKind kind, + const gchar *str, + guint32 val, + GDestroyNotify destroy_func) +{ + XbOpcode *self = g_slice_new0 (XbOpcode); + self->ref = 1; + self->kind = kind; + self->ptr = (gpointer) str; + self->val = val; + self->destroy_func = destroy_func; + return self; +} + +/** + * xb_opcode_text_new_static: + * @str: a string + * + * Creates a new text literal opcode, where @str is either static text or will + * outlive the #XbOpcode lifecycle. + * + * Returns: (transfer full): a #XbOpcode + * + * Since: 0.1.1 + **/ +XbOpcode * +xb_opcode_text_new_static (const gchar *str) +{ + XbOpcode *self = g_slice_new0 (XbOpcode); + self->ref = 1; + self->kind = XB_OPCODE_KIND_TEXT; + self->ptr = (gpointer) str; + return self; +} + +/** + * xb_opcode_text_new_steal: + * @str: a string + * + * Creates a new text literal opcode, stealing the @str. Once the opcode is + * finalized g_free() will be called on @str. + * + * Returns: (transfer full): a #XbOpcode + * + * Since: 0.1.1 + **/ +XbOpcode * +xb_opcode_text_new_steal (gchar *str) +{ + XbOpcode *self = g_slice_new0 (XbOpcode); + self->ref = 1; + self->kind = XB_OPCODE_KIND_TEXT; + self->ptr = (gpointer) str; + self->destroy_func = g_free; + return self; +} + +/** + * xb_opcode_func_new: + * @func: a function index + * + * Creates an opcode for a specific function. Custom functions can be registered + * using xb_machine_add_func() and retrieved using xb_machine_opcode_func_new(). + * + * Returns: (transfer full): a #XbOpcode + * + * Since: 0.1.1 + **/ +XbOpcode * +xb_opcode_func_new (guint32 func) +{ + XbOpcode *self = g_slice_new0 (XbOpcode); + self->ref = 1; + self->kind = XB_OPCODE_KIND_FUNCTION; + self->val = func; + return self; +} + +/** + * xb_opcode_bind_new: + * + * Creates an opcode for a bind variable. A value needs to be assigned to this + * opcode at runtime using xb_query_bind_str(). + * + * Returns: (transfer full): a #XbOpcode + * + * Since: 0.1.4 + **/ +XbOpcode * +xb_opcode_bind_new (void) +{ + XbOpcode *self = g_slice_new0 (XbOpcode); + self->ref = 1; + self->kind = XB_OPCODE_KIND_BOUND_INTEGER; + return self; +} + +/* private */ +void +xb_opcode_bind_str (XbOpcode *self, gchar *str, GDestroyNotify destroy_func) +{ + if (self->destroy_func) { + self->destroy_func (self->ptr); + self->destroy_func = NULL; + } + self->kind = XB_OPCODE_KIND_BOUND_TEXT; + self->ptr = (gpointer) str; + self->destroy_func = (gpointer) destroy_func; +} + +/* private */ +void +xb_opcode_bind_val (XbOpcode *self, guint32 val) +{ + if (self->destroy_func) { + self->destroy_func (self->ptr); + self->destroy_func = NULL; + } + self->kind = XB_OPCODE_KIND_BOUND_INTEGER; + self->val = val; +} + +/* private */ +void +xb_opcode_set_val (XbOpcode *self, guint32 val) +{ + self->val = val; +} + +/* private */ +void +xb_opcode_set_kind (XbOpcode *self, XbOpcodeKind kind) +{ + self->kind = kind; +} + +/** + * xb_opcode_integer_new: + * @val: a integer value + * + * Creates an opcode with an literal integer. + * + * Returns: (transfer full): a #XbOpcode + * + * Since: 0.1.1 + **/ +XbOpcode * +xb_opcode_integer_new (guint32 val) +{ + XbOpcode *self = g_slice_new0 (XbOpcode); + self->ref = 1; + self->kind = XB_OPCODE_KIND_INTEGER; + self->val = val; + return self; +} + +/* private */ +XbOpcode * +xb_opcode_bool_new (gboolean val) +{ + XbOpcode *self = g_slice_new0 (XbOpcode); + self->ref = 1; + self->kind = XB_OPCODE_KIND_BOOLEAN; + self->val = val; + return self; +} + +GType +xb_opcode_get_type (void) +{ + static GType type = 0; + if (G_UNLIKELY (!type)) { + type = g_boxed_type_register_static ("XbOpcode", + (GBoxedCopyFunc) xb_opcode_ref, + (GBoxedFreeFunc) xb_opcode_unref); + } + return type; +} diff --git a/src/xb-opcode.h b/src/xb-opcode.h new file mode 100644 index 0000000..122b4c6 --- /dev/null +++ b/src/xb-opcode.h @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2018 Richard Hughes + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#pragma once + +#include + +G_BEGIN_DECLS + +/** + * XbOpcodeFlags: + * @XB_OPCODE_FLAG_NONE: No flags set + * @XB_OPCODE_FLAG_INTEGER: Integer value set + * @XB_OPCODE_FLAG_TEXT: Text value set + * @XB_OPCODE_FLAG_FUNCTION: An operator + * @XB_OPCODE_FLAG_BOUND: A bound value, assigned later + * + * The opcode flags. The values have been carefully chosen so that a simple + * bitmask can be done to know how to compare for equality. + * + * function─┐ ┌─string + * bound──┐ │ │ ┌──integer + * │ │ │ │ + * X X X X X X X + * 8 4 2 1 + **/ +typedef enum { + XB_OPCODE_FLAG_UNKNOWN = 0x0, /* Since: 0.1.4 */ + XB_OPCODE_FLAG_INTEGER = 1 << 0, /* Since: 0.1.4 */ + XB_OPCODE_FLAG_TEXT = 1 << 1, /* Since: 0.1.4 */ + XB_OPCODE_FLAG_FUNCTION = 1 << 2, /* Since: 0.1.4 */ + XB_OPCODE_FLAG_BOUND = 1 << 3, /* Since: 0.1.4 */ + XB_OPCODE_FLAG_BOOLEAN = 1 << 4, /* Since: 0.1.11 */ + /*< private >*/ + XB_OPCODE_FLAG_LAST +} XbOpcodeFlags; + +/** + * XbOpcodeKind: + * @XB_OPCODE_KIND_UNKNOWN: Unknown opcode + * @XB_OPCODE_KIND_INTEGER: A literal integer value + * @XB_OPCODE_KIND_TEXT: A literal text value + * @XB_OPCODE_KIND_FUNCTION: An operator + * @XB_OPCODE_KIND_BOUND_INTEGER: A bound integer value + * @XB_OPCODE_KIND_BOUND_TEXT: A bound text value + * @XB_OPCODE_KIND_INDEXED_TEXT: An indexed text value + **/ +typedef enum { + XB_OPCODE_KIND_UNKNOWN = 0x0, /* Since: 0.1.1 */ + XB_OPCODE_KIND_INTEGER = XB_OPCODE_FLAG_INTEGER, /* Since: 0.1.1 */ + XB_OPCODE_KIND_TEXT = XB_OPCODE_FLAG_TEXT, /* Since: 0.1.1 */ + XB_OPCODE_KIND_FUNCTION = XB_OPCODE_FLAG_FUNCTION | XB_OPCODE_FLAG_INTEGER, /* Since: 0.1.1 */ + XB_OPCODE_KIND_BOUND_UNSET = XB_OPCODE_FLAG_BOUND, /* Since: 0.1.4 */ + XB_OPCODE_KIND_BOUND_INTEGER = XB_OPCODE_FLAG_BOUND | XB_OPCODE_FLAG_INTEGER, /* Since: 0.1.4 */ + XB_OPCODE_KIND_BOUND_TEXT = XB_OPCODE_FLAG_BOUND | XB_OPCODE_FLAG_TEXT, /* Since: 0.1.4 */ + XB_OPCODE_KIND_INDEXED_TEXT = XB_OPCODE_FLAG_INTEGER | XB_OPCODE_FLAG_TEXT, /* Since: 0.1.4 */ + XB_OPCODE_KIND_BOOLEAN = XB_OPCODE_FLAG_INTEGER | XB_OPCODE_FLAG_BOOLEAN, /* Since: 0.1.11 */ + /*< private >*/ + XB_OPCODE_KIND_LAST +} XbOpcodeKind; + +typedef struct _XbOpcode XbOpcode; + +gboolean xb_opcode_cmp_val (XbOpcode *self); +gboolean xb_opcode_cmp_str (XbOpcode *self); + +GType xb_opcode_get_type (void); +gchar *xb_opcode_to_string (XbOpcode *self); +const gchar *xb_opcode_kind_to_string (XbOpcodeKind kind); +XbOpcodeKind xb_opcode_kind_from_string (const gchar *str); + +void xb_opcode_unref (XbOpcode *self); +XbOpcode *xb_opcode_ref (XbOpcode *self); + +XbOpcodeKind xb_opcode_get_kind (XbOpcode *self); +const gchar *xb_opcode_get_str (XbOpcode *self); +guint32 xb_opcode_get_val (XbOpcode *self); + +XbOpcode *xb_opcode_func_new (guint32 func); +XbOpcode *xb_opcode_integer_new (guint32 val); +XbOpcode *xb_opcode_text_new (const gchar *str); +XbOpcode *xb_opcode_text_new_static (const gchar *str); +XbOpcode *xb_opcode_text_new_steal (gchar *str); + +G_DEFINE_AUTOPTR_CLEANUP_FUNC(XbOpcode, xb_opcode_unref) + +G_END_DECLS diff --git a/src/xb-query-private.h b/src/xb-query-private.h new file mode 100644 index 0000000..18773be --- /dev/null +++ b/src/xb-query-private.h @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2018 Richard Hughes + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#pragma once + +#include + +#include "xb-query.h" + +G_BEGIN_DECLS + +typedef enum { + XB_SILO_QUERY_KIND_UNKNOWN, + XB_SILO_QUERY_KIND_WILDCARD, + XB_SILO_QUERY_KIND_PARENT, + XB_SILO_QUERY_KIND_LAST +} XbSiloQueryKind; + +typedef struct { + gchar *element; + guint32 element_idx; + GPtrArray *predicates; /* of XbStack */ + XbSiloQueryKind kind; +} XbQuerySection; + +GPtrArray *xb_query_get_sections (XbQuery *self); +gchar *xb_query_to_string (XbQuery *self); + +G_END_DECLS diff --git a/src/xb-query.c b/src/xb-query.c new file mode 100644 index 0000000..bae6cb7 --- /dev/null +++ b/src/xb-query.c @@ -0,0 +1,556 @@ +/* + * Copyright (C) 2018 Richard Hughes + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#define G_LOG_DOMAIN "XbSilo" + +#include "config.h" + +#include + +#include "xb-machine.h" +#include "xb-opcode-private.h" +#include "xb-query-private.h" +#include "xb-silo-private.h" +#include "xb-stack-private.h" + +typedef struct { + GObject parent_instance; + GPtrArray *sections; /* of XbQuerySection */ + XbSilo *silo; + XbQueryFlags flags; + gchar *xpath; + guint limit; +} XbQueryPrivate; + +G_DEFINE_TYPE_WITH_PRIVATE (XbQuery, xb_query, G_TYPE_OBJECT) +#define GET_PRIVATE(o) (xb_query_get_instance_private (o)) + +/** + * xb_query_get_sections: + * @self: a #XbQuery + * + * Gets the sections that make up the query. + * + * Returns: (transfer none) (element-type XbQuerySection): sections + * + * Since: 0.1.4 + **/ +GPtrArray * +xb_query_get_sections (XbQuery *self) +{ + XbQueryPrivate *priv = GET_PRIVATE (self); + g_return_val_if_fail (XB_IS_QUERY (self), NULL); + return priv->sections; +} + +/** + * xb_query_get_xpath: + * @self: a #XbQuery + * + * Gets the XPath string that created the query. + * + * Returns: string + * + * Since: 0.1.4 + **/ +const gchar * +xb_query_get_xpath (XbQuery *self) +{ + XbQueryPrivate *priv = GET_PRIVATE (self); + g_return_val_if_fail (XB_IS_QUERY (self), NULL); + return priv->xpath; +} + +static gchar * +xb_query_section_to_string (XbQuerySection *sect) +{ + GString *str = g_string_new (NULL); + if (sect->kind == XB_SILO_QUERY_KIND_PARENT) + g_string_append (str, ".."); + else if (sect->kind == XB_SILO_QUERY_KIND_WILDCARD) + g_string_append (str, "*"); + else + g_string_append (str, sect->element); + if (sect->predicates != NULL && sect->predicates->len > 0) { + g_string_append (str, "["); + for (guint j = 0; j < sect->predicates->len; j++) { + XbStack *stack = g_ptr_array_index (sect->predicates, j); + g_autofree gchar *tmp = xb_stack_to_string (stack); + g_string_append (str, tmp); + } + g_string_append (str, "]"); + } + return g_string_free (str, FALSE); +} + +/** + * xb_query_to_string: + * @self: a #XbQuery + * + * Gets the XPath that was used for the query. + * + * Returns: string + * + * Since: 0.1.13 + **/ +gchar * +xb_query_to_string (XbQuery *self) +{ + XbQueryPrivate *priv = GET_PRIVATE (self); + GString *str = g_string_new (NULL); + for (guint i = 0; i < priv->sections->len; i++) { + XbQuerySection *sect = g_ptr_array_index (priv->sections, i); + g_autofree gchar *tmp = xb_query_section_to_string (sect); + g_string_append (str, tmp); + if (i != priv->sections->len - 1) + g_string_append (str, "/"); + } + return g_string_free (str, FALSE); +} + +/** + * xb_query_get_limit: + * @self: a #XbQuery + * + * Gets the results limit on this query, where 0 is 'all'. + * + * Returns: integer, default 0 + * + * Since: 0.1.4 + **/ +guint +xb_query_get_limit (XbQuery *self) +{ + XbQueryPrivate *priv = GET_PRIVATE (self); + g_return_val_if_fail (XB_IS_QUERY (self), 0); + return priv->limit; +} + +/** + * xb_query_set_limit: + * @self: a #XbQuery + * @limit: integer + * + * Sets the results limit on this query, where 0 is 'all'. + * + * Since: 0.1.4 + **/ +void +xb_query_set_limit (XbQuery *self, guint limit) +{ + XbQueryPrivate *priv = GET_PRIVATE (self); + g_return_if_fail (XB_IS_QUERY (self)); + priv->limit = limit; +} + +/** + * xb_query_get_flags: + * @self: a #XbQuery + * + * Gets the flags used for this query. + * + * Returns: #XbQueryFlags, default %XB_QUERY_FLAG_NONE + * + * Since: 0.1.15 + **/ +XbQueryFlags +xb_query_get_flags (XbQuery *self) +{ + XbQueryPrivate *priv = GET_PRIVATE (self); + g_return_val_if_fail (XB_IS_QUERY (self), 0); + return priv->flags; +} + +/** + * xb_query_set_flags: + * @self: a #XbQuery + * @flags: a #XbQueryFlags, e.g. %XB_QUERY_FLAG_USE_INDEXES + * + * Sets the flags to use for this query. + * + * Since: 0.1.15 + **/ +void +xb_query_set_flags (XbQuery *self, XbQueryFlags flags) +{ + XbQueryPrivate *priv = GET_PRIVATE (self); + g_return_if_fail (XB_IS_QUERY (self)); + priv->flags = flags; +} + +static XbOpcode * +xb_query_get_bound_opcode (XbQuery *self, guint idx) +{ + XbQueryPrivate *priv = GET_PRIVATE (self); + guint idx_cnt = 0; + + for (guint i = 0; i < priv->sections->len; i++) { + XbQuerySection *section = g_ptr_array_index (priv->sections, i); + if (section->predicates == NULL) + continue; + for (guint j = 0; j < section->predicates->len; j++) { + XbStack *stack = g_ptr_array_index (section->predicates, j); + for (guint k = 0; k < xb_stack_get_size (stack); k++) { + XbOpcode *op = xb_stack_peek (stack, k); + if (xb_opcode_is_bound (op)) { + if (idx == idx_cnt++) + return op; + } + } + } + } + return NULL; +} + +/** + * xb_query_bind_str: + * @self: a #XbQuery + * @idx: an integer index + * @str: string to assign to the bound variable + * @error: a #GError, or %NULL + * + * Assigns a string to a bound value specified using `?`. + * + * Returns: %TRUE if the @idx existed + * + * Since: 0.1.4 + **/ +gboolean +xb_query_bind_str (XbQuery *self, guint idx, const gchar *str, GError **error) +{ + XbOpcode *op; + + g_return_val_if_fail (XB_IS_QUERY (self), FALSE); + g_return_val_if_fail (error == NULL || *error == NULL, FALSE); + + /* get the correct opcode */ + op = xb_query_get_bound_opcode (self, idx); + if (op == NULL) { + g_set_error (error, + G_IO_ERROR, + G_IO_ERROR_INVALID_ARGUMENT, + "no bound opcode with index %u", idx); + return FALSE; + } + xb_opcode_bind_str (op, g_strdup (str), g_free); + return TRUE; +} + +/** + * xb_query_bind_val: + * @self: a #XbQuery + * @idx: an integer index + * @val: value to assign to the bound variable + * @error: a #GError, or %NULL + * + * Assigns a string to a bound value specified using `?`. + * + * Returns: %TRUE if the @idx existed + * + * Since: 0.1.4 + **/ +gboolean +xb_query_bind_val (XbQuery *self, guint idx, guint32 val, GError **error) +{ + XbOpcode *op; + + g_return_val_if_fail (XB_IS_QUERY (self), FALSE); + g_return_val_if_fail (error == NULL || *error == NULL, FALSE); + + /* get the correct opcode */ + op = xb_query_get_bound_opcode (self, idx); + if (op == NULL) { + g_set_error (error, + G_IO_ERROR, + G_IO_ERROR_INVALID_ARGUMENT, + "no bound opcode with index %u", idx); + return FALSE; + } + xb_opcode_bind_val (op, val); + return TRUE; +} + +static void +xb_query_section_free (XbQuerySection *section) +{ + if (section->predicates != NULL) + g_ptr_array_unref (section->predicates); + g_free (section->element); + g_slice_free (XbQuerySection, section); +} + +G_DEFINE_AUTOPTR_CLEANUP_FUNC(XbQuerySection, xb_query_section_free) + +static gboolean +xb_query_repair_opcode_texi (XbQuery *self, XbOpcode *op, GError **error) +{ + XbQueryPrivate *priv = GET_PRIVATE (self); + if (xb_opcode_get_val (op) == XB_SILO_UNSET) { + const gchar *tmp = xb_opcode_get_str (op); + guint32 val = xb_silo_strtab_index_lookup (priv->silo, tmp); + if (val == XB_SILO_UNSET) { + g_set_error (error, + G_IO_ERROR, + G_IO_ERROR_INVALID_ARGUMENT, + "indexed string '%s' was unfound", + tmp); + return FALSE; + } + xb_opcode_set_val (op, val); + } + return TRUE; +} + +static gboolean +xb_query_parse_predicate (XbQuery *self, + XbQuerySection *section, + const gchar *text, + gssize text_len, + GError **error) +{ + XbQueryPrivate *priv = GET_PRIVATE (self); + XbMachineParseFlags machine_flags = XB_MACHINE_PARSE_FLAG_NONE; + g_autoptr(XbStack) opcodes = NULL; + + /* set flags */ + if (priv->flags & XB_QUERY_FLAG_OPTIMIZE) + machine_flags |= XB_MACHINE_PARSE_FLAG_OPTIMIZE; + + /* parse */ + opcodes = xb_machine_parse_full (xb_silo_get_machine (priv->silo), + text, text_len, + machine_flags, + error); + if (opcodes == NULL) + return FALSE; + + /* repair or convert the indexed strings */ + if (priv->flags & XB_QUERY_FLAG_USE_INDEXES) { + for (guint i = 0; i < xb_stack_get_size (opcodes); i++) { + XbOpcode *op = xb_stack_peek (opcodes, i); + if (xb_opcode_get_kind (op) != XB_OPCODE_KIND_INDEXED_TEXT) + continue; + if (!xb_query_repair_opcode_texi (self, op, error)) + return FALSE; + } + } else { + for (guint i = 0; i < xb_stack_get_size (opcodes); i++) { + XbOpcode *op = xb_stack_peek (opcodes, i); + if (xb_opcode_get_kind (op) == XB_OPCODE_KIND_INDEXED_TEXT) + xb_opcode_set_kind (op, XB_OPCODE_KIND_TEXT); + } + } + + /* create array if it does not exist */ + if (section->predicates == NULL) + section->predicates = g_ptr_array_new_with_free_func ((GDestroyNotify) xb_stack_unref); + g_ptr_array_add (section->predicates, g_steal_pointer (&opcodes)); + return TRUE; +} + +static XbQuerySection * +xb_query_parse_section (XbQuery *self, const gchar *xpath, GError **error) +{ + XbQueryPrivate *priv = GET_PRIVATE (self); + g_autoptr(XbQuerySection) section = g_slice_new0 (XbQuerySection); + guint start = 0; + + /* common XPath sections */ + if (g_strcmp0 (xpath, "parent::*") == 0 || + g_strcmp0 (xpath, "..") == 0) { + section->kind = XB_SILO_QUERY_KIND_PARENT; + return g_steal_pointer (§ion); + } + + /* parse element and predicate */ + for (guint i = 0; xpath[i] != '\0'; i++) { + if (start == 0 && xpath[i] == '[') { + if (section->element == NULL) + section->element = g_strndup (xpath, i); + start = i; + continue; + } + if (start > 0 && xpath[i] == ']') { + if (!xb_query_parse_predicate (self, + section, + xpath + start + 1, + i - start - 1, + error)) { + return NULL; + } + start = 0; + continue; + } + } + + /* incomplete predicate */ + if (start != 0) { + g_set_error (error, + G_IO_ERROR, + G_IO_ERROR_INVALID_ARGUMENT, + "predicate %s was unfinished, missing ']'", + xpath + start); + return NULL; + } + + if (section->element == NULL) + section->element = g_strdup (xpath); + if (g_strcmp0 (section->element, "child::*") == 0 || + g_strcmp0 (section->element, "*") == 0) { + section->kind = XB_SILO_QUERY_KIND_WILDCARD; + return g_steal_pointer (§ion); + } + section->element_idx = xb_silo_get_strtab_idx (priv->silo, section->element); + if (section->element_idx == XB_SILO_UNSET) { + g_set_error (error, + G_IO_ERROR, + G_IO_ERROR_INVALID_ARGUMENT, + "element name %s is unknown in silo", + section->element); + return NULL; + } + return g_steal_pointer (§ion); +} + +static gboolean +xb_query_parse (XbQuery *self, const gchar *xpath, GError **error) +{ + XbQueryPrivate *priv = GET_PRIVATE (self); + XbQuerySection *section; + g_autoptr(GString) acc = g_string_new (NULL); + +// g_debug ("parsing XPath %s", xpath); + for (gsize i = 0; xpath[i] != '\0'; i++) { + + /* escaped chars */ + if (xpath[i] == '\\') { + if (xpath[i+1] == '/' || + xpath[i+1] == 't' || + xpath[i+1] == 'n') { + g_string_append_c (acc, xpath[i+1]); + i += 1; + continue; + } + } + + /* split */ + if (xpath[i] == '/') { + if (acc->len == 0) { + g_set_error_literal (error, + G_IO_ERROR, + G_IO_ERROR_NOT_FOUND, + "xpath section empty"); + return FALSE; + } + section = xb_query_parse_section (self, acc->str, error); + if (section == NULL) + return FALSE; + g_ptr_array_add (priv->sections, section); + g_string_truncate (acc, 0); + continue; + } + g_string_append_c (acc, xpath[i]); + } + + /* add any remaining section */ + section = xb_query_parse_section (self, acc->str, error); + if (section == NULL) + return FALSE; + g_ptr_array_add (priv->sections, section); + return TRUE; +} + +/** + * xb_query_new_full: + * @silo: a #XbSilo + * @xpath: The XPath query + * @flags: some #XbQueryFlags, e.g. #XB_QUERY_FLAG_USE_INDEXES + * @error: the #GError, or %NULL + * + * Creates a query to be used by @silo. It may be quicker to create a query + * manually and re-use it multiple times. + * + * Returns: (transfer full): a #XbQuery + * + * Since: 0.1.6 + **/ +XbQuery * +xb_query_new_full (XbSilo *silo, const gchar *xpath, XbQueryFlags flags, GError **error) +{ + g_autoptr(XbQuery) self = g_object_new (XB_TYPE_QUERY, NULL); + XbQueryPrivate *priv = GET_PRIVATE (self); + + g_return_val_if_fail (XB_IS_SILO (silo), NULL); + g_return_val_if_fail (error == NULL || *error == NULL, NULL); + + /* create */ + priv->silo = g_object_ref (silo); + priv->xpath = g_strdup (xpath); + priv->flags = flags; + priv->sections = g_ptr_array_new_with_free_func ((GDestroyNotify) xb_query_section_free); + + /* add each section */ + if (!xb_query_parse (self, xpath, error)) + return NULL; + + /* nothing here! */ + if (priv->sections->len == 0) { + g_set_error (error, + G_IO_ERROR, + G_IO_ERROR_NOT_SUPPORTED, + "No query sections for '%s'", + xpath); + return NULL; + } + + /* success */ + return g_steal_pointer (&self); +} + +/** + * xb_query_new: + * @silo: a #XbSilo + * @xpath: The XPath query + * @error: the #GError, or %NULL + * + * Creates a query to be used by @silo. It may be quicker to create a query + * manually and re-use it multiple times. + * + * Returns: (transfer full): a #XbQuery + * + * Since: 0.1.4 + **/ +XbQuery * +xb_query_new (XbSilo *silo, const gchar *xpath, GError **error) +{ + return xb_query_new_full (silo, xpath, + XB_QUERY_FLAG_OPTIMIZE | + XB_QUERY_FLAG_USE_INDEXES, + error); +} + +static void +xb_query_init (XbQuery *self) +{ +} + +static void +xb_query_finalize (GObject *obj) +{ + XbQuery *self = XB_QUERY (obj); + XbQueryPrivate *priv = GET_PRIVATE (self); + g_object_unref (priv->silo); + g_ptr_array_unref (priv->sections); + g_free (priv->xpath); + G_OBJECT_CLASS (xb_query_parent_class)->finalize (obj); +} + +static void +xb_query_class_init (XbQueryClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + object_class->finalize = xb_query_finalize; +} diff --git a/src/xb-query.h b/src/xb-query.h new file mode 100644 index 0000000..a7d0eec --- /dev/null +++ b/src/xb-query.h @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2018 Richard Hughes + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#pragma once + +#include + +#include "xb-silo.h" + +G_BEGIN_DECLS + +#define XB_TYPE_QUERY (xb_query_get_type ()) +G_DECLARE_DERIVABLE_TYPE (XbQuery, xb_query, XB, QUERY, GObject) + +struct _XbQueryClass { + GObjectClass parent_class; + /*< private >*/ + void (*_xb_reserved1) (void); + void (*_xb_reserved2) (void); + void (*_xb_reserved3) (void); + void (*_xb_reserved4) (void); + void (*_xb_reserved5) (void); + void (*_xb_reserved6) (void); + void (*_xb_reserved7) (void); +}; + +/** + * XbQueryFlags: + * @XB_QUERY_FLAG_NONE: No extra flags to use + * @XB_QUERY_FLAG_OPTIMIZE: Optimize the query when possible + * @XB_QUERY_FLAG_USE_INDEXES: Use the indexed parameters + * @XB_QUERY_FLAG_REVERSE: Reverse the results order + * + * The flags used fo query. + **/ +typedef enum { + XB_QUERY_FLAG_NONE = 0, /* Since: 0.1.6 */ + XB_QUERY_FLAG_OPTIMIZE = 1 << 0, /* Since: 0.1.6 */ + XB_QUERY_FLAG_USE_INDEXES = 1 << 1, /* Since: 0.1.6 */ + XB_QUERY_FLAG_REVERSE = 1 << 2, /* Since: 0.1.15 */ + /*< private >*/ + XB_QUERY_FLAG_LAST +} XbQueryFlags; + +XbQuery *xb_query_new (XbSilo *silo, + const gchar *xpath, + GError **error); +XbQuery *xb_query_new_full (XbSilo *silo, + const gchar *xpath, + XbQueryFlags flags, + GError **error); +const gchar *xb_query_get_xpath (XbQuery *self); +guint xb_query_get_limit (XbQuery *self); +void xb_query_set_limit (XbQuery *self, + guint limit); +XbQueryFlags xb_query_get_flags (XbQuery *self); +void xb_query_set_flags (XbQuery *self, + XbQueryFlags flags); +gboolean xb_query_bind_str (XbQuery *self, + guint idx, + const gchar *str, + GError **error); +gboolean xb_query_bind_val (XbQuery *self, + guint idx, + guint32 val, + GError **error); + +G_END_DECLS diff --git a/src/xb-self-test.c b/src/xb-self-test.c new file mode 100644 index 0000000..f1b4f2a --- /dev/null +++ b/src/xb-self-test.c @@ -0,0 +1,2422 @@ +/* + * Copyright (C) 2018 Richard Hughes + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#include "config.h" + +#include + +#include "xb-builder.h" +#include "xb-builder-node.h" +#include "xb-machine.h" +#include "xb-node-query.h" +#include "xb-opcode.h" +#include "xb-silo-export.h" +#include "xb-silo-private.h" +#include "xb-silo-query-private.h" +#include "xb-stack-private.h" +#include "xb-string-private.h" + +static GMainLoop *_test_loop = NULL; +static guint _test_loop_timeout_id = 0; + +#define XB_SELF_TEST_INOTIFY_TIMEOUT 10000 /* ms */ + +static gboolean +xb_test_hang_check_cb (gpointer user_data) +{ + g_main_loop_quit (_test_loop); + _test_loop_timeout_id = 0; + return G_SOURCE_REMOVE; +} + +static void +xb_test_loop_run_with_timeout (guint timeout_ms) +{ + g_assert (_test_loop_timeout_id == 0); + g_assert (_test_loop == NULL); + _test_loop = g_main_loop_new (NULL, FALSE); + _test_loop_timeout_id = g_timeout_add (timeout_ms, xb_test_hang_check_cb, NULL); + g_main_loop_run (_test_loop); +} + +static void +xb_test_loop_quit (void) +{ + if (_test_loop_timeout_id > 0) { + g_source_remove (_test_loop_timeout_id); + _test_loop_timeout_id = 0; + } + if (_test_loop != NULL) { + g_main_loop_quit (_test_loop); + g_main_loop_unref (_test_loop); + _test_loop = NULL; + } +} + +static gboolean +xb_test_import_xml (XbBuilder *self, const gchar *xml, GError **error) +{ + g_autoptr(XbBuilderSource) source = xb_builder_source_new (); + + g_return_val_if_fail (XB_IS_BUILDER (self), FALSE); + g_return_val_if_fail (xml != NULL, FALSE); + + /* add source */ + if (!xb_builder_source_load_xml (source, xml, XB_BUILDER_SOURCE_FLAG_NONE, error)) + return FALSE; + + /* success */ + xb_builder_import_source (self, source); + return TRUE; +} + +static void +xb_stack_func (void) +{ + XbOpcode *op; + g_autoptr(XbOpcode) op1 = xb_opcode_func_new (0); + g_autoptr(XbOpcode) op2 = xb_opcode_integer_new (1); + g_autoptr(XbOpcode) op3 = xb_opcode_text_new ("dave"); + g_autoptr(XbStack) stack = xb_stack_new (3); + + /* push three opcodes */ + g_assert_true (xb_stack_push (stack, op3)); + g_assert_true (xb_stack_push (stack, op2)); + g_assert_true (xb_stack_push (stack, op1)); + g_assert_false (xb_stack_push (stack, op3)); + + /* pop the same opcodes */ + op = xb_stack_pop (stack); + g_assert (op == op1); + xb_opcode_unref (op); + op = xb_stack_pop (stack); + g_assert (op == op2); + xb_opcode_unref (op); + op = xb_stack_pop (stack); + g_assert (op == op3); + xb_opcode_unref (op); + + /* re-add one opcode */ + g_assert_true (xb_stack_push (stack, op3)); + + /* finish, cleaning up the stack properly... */ +} + +static void +xb_stack_peek_func (void) +{ + g_autoptr(XbOpcode) op1 = xb_opcode_func_new (0); + g_autoptr(XbOpcode) op2 = xb_opcode_integer_new (1); + g_autoptr(XbOpcode) op3 = xb_opcode_text_new ("dave"); + g_autoptr(XbStack) stack = xb_stack_new (3); + + /* push three opcodes */ + g_assert_true (xb_stack_push (stack, op1)); + g_assert_true (xb_stack_push (stack, op2)); + g_assert_true (xb_stack_push (stack, op3)); + + /* pop the same opcodes */ + g_assert (xb_stack_peek_head (stack) == op1); + g_assert (xb_stack_peek_tail (stack) == op3); + g_assert (xb_stack_peek (stack, 0) == op1); + g_assert (xb_stack_peek (stack, 1) == op2); + g_assert (xb_stack_peek (stack, 2) == op3); +} + +static void +xb_common_union_func (void) +{ + g_autoptr(GString) xpath = g_string_new (NULL); + xb_string_append_union (xpath, "components/component"); + g_assert_cmpstr (xpath->str, ==, "components/component"); + xb_string_append_union (xpath, "applications/application"); + g_assert_cmpstr (xpath->str, ==, "components/component|applications/application"); +} + +static void +xb_common_func (void) +{ + g_assert_true (xb_string_search ("gimp", "gimp")); + g_assert_true (xb_string_search ("GIMP", "gimp")); + g_assert_true (xb_string_search ("The GIMP", "gimp")); + g_assert_true (xb_string_search ("The GIMP Editor", "gimp")); + g_assert_false (xb_string_search ("gimp", "")); + g_assert_false (xb_string_search ("gimp", "imp")); + g_assert_false (xb_string_search ("the gimp editor", "imp")); +} + +static void +xb_opcodes_kind_func (void) +{ + g_autoptr(XbOpcode) op1 = xb_opcode_func_new (0); + g_autoptr(XbOpcode) op2 = xb_opcode_integer_new (1); + g_autoptr(XbOpcode) op3 = xb_opcode_text_new ("dave"); + + /* check kind */ + g_assert_cmpint (xb_opcode_get_kind (op1), ==, XB_OPCODE_KIND_FUNCTION); + g_assert_cmpint (xb_opcode_get_kind (op2), ==, XB_OPCODE_KIND_INTEGER); + g_assert_cmpint (xb_opcode_get_kind (op3), ==, XB_OPCODE_KIND_TEXT); + + /* to and from string */ + g_assert_cmpint (xb_opcode_kind_from_string ("TEXT"), ==, XB_OPCODE_KIND_TEXT); + g_assert_cmpint (xb_opcode_kind_from_string ("FUNC"), ==, XB_OPCODE_KIND_FUNCTION); + g_assert_cmpint (xb_opcode_kind_from_string ("INTE"), ==, XB_OPCODE_KIND_INTEGER); + g_assert_cmpint (xb_opcode_kind_from_string ("dave"), ==, XB_OPCODE_KIND_UNKNOWN); + g_assert_cmpstr (xb_opcode_kind_to_string (XB_OPCODE_KIND_TEXT), ==, "TEXT"); + g_assert_cmpstr (xb_opcode_kind_to_string (XB_OPCODE_KIND_FUNCTION), ==, "FUNC"); + g_assert_cmpstr (xb_opcode_kind_to_string (XB_OPCODE_KIND_INTEGER), ==, "INTE"); + g_assert_cmpstr (xb_opcode_kind_to_string (XB_OPCODE_KIND_UNKNOWN), ==, NULL); + + /* integer compare */ + g_assert_false (xb_opcode_cmp_val (op1)); + g_assert_true (xb_opcode_cmp_val (op2)); + g_assert_false (xb_opcode_cmp_val (op3)); + + /* string compare */ + g_assert_false (xb_opcode_cmp_str (op1)); + g_assert_false (xb_opcode_cmp_str (op2)); + g_assert_true (xb_opcode_cmp_str (op3)); +} + +static void +xb_predicate_func (void) +{ + g_autoptr(XbSilo) silo = xb_silo_new (); + struct { + const gchar *pred; + const gchar *str; + } tests[] = { + { "'a'='b'", + "'a','b',eq()" }, + { "@a='b'", + "'a',attr(),'b',eq()" }, + { "@a=='b'", + "'a',attr(),'b',eq()" }, + { "'a'<'b'", + "'a','b',lt()" }, + { "999>=123", + "999,123,ge()" }, + { "not(0)", + "0,not()" }, + { "@a", + "'a',attr(),'(null)',ne()" }, + { "not(@a)", + "'a',attr(),not()" }, + { "'a'=", + "'a',eq()" }, + { "='b'", + "'b',eq()" }, + { "999=\'b\'", + "999,'b',eq()" }, + { "text()=\'b\'", + "text(),'b',eq()" }, + { "last()", + "last()" }, + { "text()~='beef'", + "text(),'beef',search()" }, + { "@type~='dead'", + "'type',attr(),'dead',search()" }, + { "2", + "2,position(),eq()" }, + { "text()=lower-case('firefox')", + "text(),'firefox',lower-case(),eq()" }, + { "$'a'=$'b'", + "$'a',$'b',eq()" }, + { "('a'='b')&&('c'='d')", + "'a','b',eq(),'c','d',eq(),and()" }, + /* sentinel */ + { NULL, NULL } + }; + const gchar *invalid[] = { + "text(", + "text((((((((((((((((((((text()))))))))))))))))))))", + NULL + }; + xb_machine_set_debug_flags (xb_silo_get_machine (silo), + XB_MACHINE_DEBUG_FLAG_SHOW_STACK | + XB_MACHINE_DEBUG_FLAG_SHOW_PARSING); + for (guint i = 0; tests[i].pred != NULL; i++) { + g_autofree gchar *str = NULL; + g_autoptr(GError) error = NULL; + g_autoptr(XbStack) opcodes = NULL; + + g_debug ("testing %s", tests[i].pred); + opcodes = xb_machine_parse_full (xb_silo_get_machine (silo), + tests[i].pred, -1, + XB_MACHINE_PARSE_FLAG_NONE, + &error); + g_assert_no_error (error); + g_assert_nonnull (opcodes); + str = xb_stack_to_string (opcodes); + g_assert_nonnull (str); + g_assert_cmpstr (str, ==, tests[i].str); + } + for (guint i = 0; invalid[i] != NULL; i++) { + g_autoptr(GError) error = NULL; + g_autoptr(XbStack) opcodes = NULL; + g_debug ("testing %s", invalid[i]); + opcodes = xb_machine_parse_full (xb_silo_get_machine (silo), + invalid[i], -1, + XB_MACHINE_PARSE_FLAG_NONE, + &error); + g_assert_error (error, G_IO_ERROR, G_IO_ERROR_INVALID_DATA); + g_assert_null (opcodes); + } +} + +static void +xb_predicate_optimize_func (void) +{ + g_autoptr(XbSilo) silo = xb_silo_new (); + struct { + const gchar *pred; + const gchar *str; + } tests[] = { + { "@a='b'", "'a',attr(),'b',eq()" }, + { "'a'<'b'", "True" }, /* success! */ + { "999>=123", "True" }, /* success! */ + { "not(0)", "True" }, /* success! */ + { "lower-case('Fire')", "'fire'" }, + { "upper-case(lower-case('Fire'))", + "'FIRE'" }, /* 2nd pass */ + /* sentinel */ + { NULL, NULL } + }; + const gchar *invalid[] = { + "'a'='b'", + "123>=999", + "not(1)", + NULL + }; + xb_machine_set_debug_flags (xb_silo_get_machine (silo), + XB_MACHINE_DEBUG_FLAG_SHOW_STACK | + XB_MACHINE_DEBUG_FLAG_SHOW_OPTIMIZER); + for (guint i = 0; tests[i].pred != NULL; i++) { + g_autofree gchar *str = NULL; + g_autoptr(GError) error = NULL; + g_autoptr(XbStack) opcodes = NULL; + + g_debug ("testing %s", tests[i].pred); + opcodes = xb_machine_parse_full (xb_silo_get_machine (silo), + tests[i].pred, -1, + XB_MACHINE_PARSE_FLAG_OPTIMIZE, + &error); + g_assert_no_error (error); + g_assert_nonnull (opcodes); + str = xb_stack_to_string (opcodes); + g_assert_nonnull (str); + g_assert_cmpstr (str, ==, tests[i].str); + } + for (guint i = 0; invalid[i] != NULL; i++) { + g_autoptr(GError) error = NULL; + g_autoptr(XbStack) opcodes = NULL; + opcodes = xb_machine_parse_full (xb_silo_get_machine (silo), + invalid[i], -1, + XB_MACHINE_PARSE_FLAG_OPTIMIZE, + &error); + g_assert_error (error, G_IO_ERROR, G_IO_ERROR_INVALID_DATA); + g_assert_null (opcodes); + } +} + +static void +xb_builder_func (void) +{ + g_autofree gchar *str = NULL; + g_autofree gchar *xml_new = NULL; + g_autoptr(GBytes) bytes = NULL; + g_autoptr(GError) error = NULL; + g_autoptr(XbSilo) silo = NULL; + const gchar *xml = + "\n" + "
\n" + " dead\n" + "
\n" + " \n" + " gimp.desktop\n" + " GIMP & Friends\n" + " org.gnome.Gimp.desktop\n" + " \n" + " \n" + " gnome-software.desktop\n" + " \n" + " \n" + " org.hughski.ColorHug2.firmware\n" + " \n" + " 1.2.3\n" + " \n" + " \n" + "
\n"; + + /* import from XML */ + silo = xb_silo_new_from_xml (xml, &error); + g_assert_no_error (error); + g_assert_nonnull (silo); + g_assert_true (xb_silo_is_valid (silo)); + + /* convert back to XML */ + str = xb_silo_to_string (silo, &error); + g_assert_no_error (error); + g_assert_nonnull (str); + g_debug ("\n%s", str); + xml_new = xb_silo_export (silo, + XB_NODE_EXPORT_FLAG_FORMAT_MULTILINE | + XB_NODE_EXPORT_FLAG_FORMAT_INDENT, + &error); + g_assert_no_error (error); + g_assert_nonnull (xml_new); + g_print ("%s", xml_new); + g_assert_cmpstr (xml, ==, xml_new); + + /* check size */ + bytes = xb_silo_get_bytes (silo); + g_assert_cmpint (g_bytes_get_size (bytes), ==, 605); +} + +static void +xb_builder_ensure_invalidate_cb (XbSilo *silo, GParamSpec *pspec, gpointer user_data) +{ + guint *invalidate_cnt = (guint *) user_data; + (*invalidate_cnt)++; + xb_test_loop_quit (); +} + +static GInputStream * +xb_builder_custom_mime_cb (XbBuilderSource *self, + XbBuilderSourceCtx *ctx, + gpointer user_data, + GCancellable *cancellable, + GError **error) +{ + gchar *xml = g_strdup_printf ("" + "" + "%s", + xb_builder_source_ctx_get_filename (ctx)); + return g_memory_input_stream_new_from_data (xml, -1, g_free); +} + +static void +xb_builder_custom_mime_func (void) +{ + gboolean ret; + g_autofree gchar *xml = NULL; + g_autofree gchar *tmp_desktop = g_build_filename (g_get_tmp_dir (), "temp.desktop", NULL); + g_autofree gchar *tmp_xmlb = g_build_filename (g_get_tmp_dir (), "temp.xmlb", NULL); + g_autoptr(GError) error = NULL; + g_autoptr(GFile) file_desktop = NULL; + g_autoptr(GFile) file = NULL; + g_autoptr(XbBuilder) builder = xb_builder_new (); + g_autoptr(XbBuilderSource) source = xb_builder_source_new (); + g_autoptr(XbSilo) silo = NULL; + + /* add support for desktop files */ + xb_builder_source_add_adapter (source, "application/x-desktop", + xb_builder_custom_mime_cb, NULL, NULL); + + /* import a source file */ + ret = g_file_set_contents (tmp_desktop, "[Desktop Entry]", -1, &error); + g_assert_no_error (error); + g_assert_true (ret); + file_desktop = g_file_new_for_path (tmp_desktop); + ret = xb_builder_source_load_file (source, file_desktop, + XB_BUILDER_SOURCE_FLAG_WATCH_FILE, + NULL, &error); + g_assert_no_error (error); + g_assert_true (ret); + xb_builder_import_source (builder, source); + file = g_file_new_for_path (tmp_xmlb); + silo = xb_builder_ensure (builder, file, + XB_BUILDER_COMPILE_FLAG_WATCH_BLOB, + NULL, &error); + g_assert_no_error (error); + g_assert_nonnull (silo); + + /* check contents */ + xml = xb_silo_export (silo, XB_NODE_EXPORT_FLAG_NONE, &error); + g_assert_no_error (error); + g_assert_nonnull (xml); + g_print ("%s", xml); + g_assert_cmpstr ("" + "temp.desktop" + "", ==, xml); + +} + +static void +xb_builder_chained_adapters_func (void) +{ + gboolean ret; + g_autofree gchar *xml = NULL; + g_autofree gchar *path = NULL; + g_autofree gchar *tmp_xmlb = g_build_filename (g_get_tmp_dir (), "temp.xmlb", NULL); + g_autoptr(GError) error = NULL; + g_autoptr(GFile) file_src = NULL; + g_autoptr(GFile) file = NULL; + g_autoptr(XbBuilder) builder = xb_builder_new (); + g_autoptr(XbBuilderSource) source = xb_builder_source_new (); + g_autoptr(XbSilo) silo = NULL; + + /* import a source file */ + path = g_build_filename (TESTDIR, "test.xml.gz.gz.gz", NULL); + if (!g_file_test (path, G_FILE_TEST_EXISTS)) { + g_free (path); + path = g_build_filename (INSTALLEDTESTDIR, "test.xml.gz.gz.gz", NULL); + } + file_src = g_file_new_for_path (path); + ret = xb_builder_source_load_file (source, file_src, + XB_BUILDER_SOURCE_FLAG_NONE, + NULL, &error); + g_assert_no_error (error); + g_assert_true (ret); + xb_builder_import_source (builder, source); + file = g_file_new_for_path (tmp_xmlb); + silo = xb_builder_ensure (builder, file, + XB_BUILDER_COMPILE_FLAG_NONE, + NULL, &error); + g_assert_no_error (error); + g_assert_nonnull (silo); + + /* check contents */ + xml = xb_silo_export (silo, XB_NODE_EXPORT_FLAG_NONE, &error); + g_assert_no_error (error); + g_assert_nonnull (xml); + g_print ("%s", xml); + g_assert_cmpstr ("Hello world!", ==, xml); + +} + +static void +xb_builder_ensure_watch_source_func (void) +{ + gboolean ret; + guint invalidate_cnt = 0; + g_autoptr(GError) error = NULL; + g_autoptr(GFile) file = NULL; + g_autoptr(GFile) file_xml = NULL; + g_autoptr(XbBuilder) builder = xb_builder_new (); + g_autoptr(XbBuilderSource) source = xb_builder_source_new (); + g_autoptr(XbSilo) silo = NULL; + g_autofree gchar *tmp_xml = g_build_filename (g_get_tmp_dir (), "temp.xml", NULL); + g_autofree gchar *tmp_xmlb = g_build_filename (g_get_tmp_dir (), "temp.xmlb", NULL); + +#ifdef _WIN32 + /* no inotify */ + g_test_skip ("inotify does not work on mingw"); + return; +#endif + + /* import a source file */ + ret = g_file_set_contents (tmp_xml, + "\n" + "gimp", -1, &error); + g_assert_no_error (error); + g_assert_true (ret); + file_xml = g_file_new_for_path (tmp_xml); + ret = xb_builder_source_load_file (source, file_xml, + XB_BUILDER_SOURCE_FLAG_WATCH_FILE, + NULL, &error); + g_assert_no_error (error); + g_assert_true (ret); + xb_builder_import_source (builder, source); + file = g_file_new_for_path (tmp_xmlb); + g_file_delete (file, NULL, NULL); + silo = xb_builder_ensure (builder, file, + XB_BUILDER_COMPILE_FLAG_WATCH_BLOB, + NULL, &error); + g_assert_no_error (error); + g_assert_nonnull (silo); + g_assert_true (xb_silo_is_valid (silo)); + g_signal_connect (silo, "notify::valid", + G_CALLBACK (xb_builder_ensure_invalidate_cb), + &invalidate_cnt); + + /* change source file */ + ret = g_file_set_contents (tmp_xml, + "\n" + "inkscape", -1, &error); + g_assert_no_error (error); + g_assert_true (ret); + xb_test_loop_run_with_timeout (XB_SELF_TEST_INOTIFY_TIMEOUT); + g_assert_false (xb_silo_is_valid (silo)); + g_assert_cmpint (invalidate_cnt, ==, 1); + g_assert_false (xb_silo_is_valid (silo)); +} + +static void +xb_builder_ensure_func (void) +{ + gboolean ret; + guint invalidate_cnt = 0; + g_autofree gchar *tmp_xmlb = g_build_filename (g_get_tmp_dir (), "temp.xmlb", NULL); + g_autoptr(GBytes) bytes1 = NULL; + g_autoptr(GBytes) bytes2 = NULL; + g_autoptr(GBytes) bytes3 = NULL; + g_autoptr(GError) error = NULL; + g_autoptr(GFile) file = NULL; + g_autoptr(XbBuilder) builder = xb_builder_new (); + g_autoptr(XbSilo) silo = NULL; + const gchar *xml = + "\n" + "
\n" + " dead\n" + "
\n" + " \n" + " gimp.desktop\n" + " GIMP & Friends\n" + " org.gnome.Gimp.desktop\n" + " \n" + " \n" + " gnome-software.desktop\n" + " \n" + " \n" + " org.hughski.ColorHug2.firmware\n" + " \n" + " 1.2.3\n" + " \n" + " \n" + "
\n"; + +#ifdef _WIN32 + /* no inotify */ + g_test_skip ("inotify does not work on mingw"); + return; +#endif + + /* import some XML */ + xb_builder_set_profile_flags (builder, XB_SILO_PROFILE_FLAG_DEBUG); + ret = xb_test_import_xml (builder, xml, &error); + g_assert_no_error (error); + g_assert_true (ret); + + /* create file if it does not exist */ + file = g_file_new_for_path (tmp_xmlb); + g_file_delete (file, NULL, NULL); + silo = xb_builder_ensure (builder, file, + XB_BUILDER_COMPILE_FLAG_WATCH_BLOB, + NULL, &error); + g_assert_no_error (error); + g_assert_nonnull (silo); + g_signal_connect (silo, "notify::valid", + G_CALLBACK (xb_builder_ensure_invalidate_cb), + &invalidate_cnt); + g_assert_cmpint (invalidate_cnt, ==, 0); + bytes1 = xb_silo_get_bytes (silo); + + /* recreate file if it is invalid */ + ret = g_file_replace_contents (file, "dave", 4, NULL, FALSE, + G_FILE_CREATE_NONE, NULL, NULL, &error); + g_assert_no_error (error); + g_assert_true (ret); + xb_test_loop_run_with_timeout (XB_SELF_TEST_INOTIFY_TIMEOUT); + g_assert_false (xb_silo_is_valid (silo)); + g_assert_cmpint (invalidate_cnt, ==, 1); + + g_clear_object (&silo); + silo = xb_builder_ensure (builder, file, + XB_BUILDER_COMPILE_FLAG_NONE, + NULL, &error); + g_assert_no_error (error); + g_assert_nonnull (silo); + g_assert_true (xb_silo_is_valid (silo)); + bytes2 = xb_silo_get_bytes (silo); + g_assert (bytes1 != bytes2); + g_clear_object (&silo); + + /* don't recreate file if perfectly valid */ + silo = xb_builder_ensure (builder, file, + XB_BUILDER_COMPILE_FLAG_NONE, + NULL, &error); + g_assert_no_error (error); + g_assert_nonnull (silo); + g_assert_true (xb_silo_is_valid (silo)); + bytes3 = xb_silo_get_bytes (silo); + g_assert (bytes2 == bytes3); + g_clear_object (&silo); + g_clear_object (&builder); + + /* don't re-create for a new builder with the same XML added */ + builder = xb_builder_new (); + ret = xb_test_import_xml (builder, xml, &error); + g_assert_no_error (error); + g_assert_true (ret); + silo = xb_builder_ensure (builder, file, + XB_BUILDER_COMPILE_FLAG_NONE, + NULL, &error); + g_assert_no_error (error); + g_assert_nonnull (silo); +} + +static gboolean +xb_builder_error_cb (XbBuilderFixup *self, + XbBuilderNode *bn, + gpointer user_data, + GError **error) +{ + g_set_error_literal (error, + G_IO_ERROR, + G_IO_ERROR_BUSY, + "engine was busy"); + return FALSE; +} + +static void +xb_builder_node_vfunc_error_func (void) +{ + gboolean ret; + g_autoptr(GError) error = NULL; + g_autoptr(XbBuilder) builder = xb_builder_new (); + g_autoptr(XbBuilderFixup) fixup = NULL; + g_autoptr(XbBuilderSource) source = xb_builder_source_new (); + g_autoptr(XbSilo) silo = NULL; + + /* add fixup */ + fixup = xb_builder_fixup_new ("AlwaysError", + xb_builder_error_cb, + NULL, NULL); + xb_builder_source_add_fixup (source, fixup); + + /* import some XML */ + ret = xb_builder_source_load_xml (source, "gimp.desktop", + XB_BUILDER_SOURCE_FLAG_NONE, &error); + g_assert_no_error (error); + g_assert_true (ret); + xb_builder_import_source (builder, source); + silo = xb_builder_compile (builder, XB_BUILDER_COMPILE_FLAG_NONE, + NULL, &error); + g_assert_error (error, G_IO_ERROR, G_IO_ERROR_BUSY); + g_assert_null (silo); +} + +static gboolean +xb_builder_upgrade_appstream_cb (XbBuilderFixup *self, + XbBuilderNode *bn, + gpointer user_data, + GError **error) +{ + if (g_strcmp0 (xb_builder_node_get_element (bn), "application") == 0) { + g_autoptr(XbBuilderNode) id = xb_builder_node_get_child (bn, "id", NULL); + g_autofree gchar *kind = NULL; + if (id != NULL) { + kind = g_strdup (xb_builder_node_get_attr (id, "type")); + xb_builder_node_remove_attr (id, "type"); + } + if (kind != NULL) + xb_builder_node_set_attr (bn, "type", kind); + xb_builder_node_set_element (bn, "component"); + } else if (g_strcmp0 (xb_builder_node_get_element (bn), "metadata") == 0) { + xb_builder_node_set_element (bn, "custom"); + } + return TRUE; +} + +static void +xb_builder_node_vfunc_func (void) +{ + gboolean ret; + g_autofree gchar *xml2 = NULL; + g_autoptr(GError) error = NULL; + g_autoptr(XbBuilder) builder = xb_builder_new (); + g_autoptr(XbBuilderFixup) fixup = NULL; + g_autoptr(XbBuilderSource) source = xb_builder_source_new (); + g_autoptr(XbSilo) silo = NULL; + const gchar *xml = + " \n" + " gimp.desktop\n" + " \n"; + + /* add fixup */ + fixup = xb_builder_fixup_new ("AppStreamUpgrade", + xb_builder_upgrade_appstream_cb, + NULL, NULL); + xb_builder_source_add_fixup (source, fixup); + + /* import some XML */ + ret = xb_builder_source_load_xml (source, xml, XB_BUILDER_SOURCE_FLAG_NONE, &error); + g_assert_no_error (error); + g_assert_true (ret); + xb_builder_import_source (builder, source); + silo = xb_builder_compile (builder, + XB_BUILDER_COMPILE_FLAG_NONE, + NULL, &error); + g_assert_no_error (error); + g_assert_nonnull (silo); + + /* check the XML */ + xml2 = xb_silo_export (silo, XB_NODE_EXPORT_FLAG_NONE, &error); + g_assert_no_error (error); + g_assert_nonnull (xml2); + g_print ("%s\n", xml2); + g_assert_cmpstr ("" + "gimp.desktop" + "", ==, xml2); +} + +static gboolean +xb_builder_fixup_ignore_node_cb (XbBuilderFixup *self, + XbBuilderNode *bn, + gpointer user_data, + GError **error) +{ + if (g_strcmp0 (xb_builder_node_get_element (bn), "component") == 0) { + g_autoptr(XbBuilderNode) id = xb_builder_node_get_child (bn, "id", NULL); + if (g_strcmp0 (xb_builder_node_get_text (id), "gimp.desktop") == 0) + xb_builder_node_add_flag (bn, XB_BUILDER_NODE_FLAG_IGNORE); + } else { + g_debug ("ignoring %s", xb_builder_node_get_element (bn)); + } + return TRUE; +} + +static void +xb_builder_node_vfunc_remove_func (void) +{ + gboolean ret; + g_autofree gchar *xml2 = NULL; + g_autoptr(GError) error = NULL; + g_autoptr(XbBuilder) builder = xb_builder_new (); + g_autoptr(XbBuilderFixup) fixup = NULL; + g_autoptr(XbBuilderSource) source = xb_builder_source_new (); + g_autoptr(XbSilo) silo = NULL; + const gchar *xml = + " \n" + " \n" + " gimp.desktop\n" + " \n" + " \n" + " inkscape.desktop\n" + " \n" + " \n"; + + /* add fixup */ + fixup = xb_builder_fixup_new ("RemoveGimp", + xb_builder_fixup_ignore_node_cb, + NULL, NULL); + xb_builder_source_add_fixup (source, fixup); + + /* import some XML */ + ret = xb_builder_source_load_xml (source, xml, XB_BUILDER_SOURCE_FLAG_NONE, &error); + g_assert_no_error (error); + g_assert_true (ret); + xb_builder_import_source (builder, source); + silo = xb_builder_compile (builder, + XB_BUILDER_COMPILE_FLAG_NONE, + NULL, &error); + g_assert_no_error (error); + g_assert_nonnull (silo); + + /* check the XML */ + xml2 = xb_silo_export (silo, XB_NODE_EXPORT_FLAG_NONE, &error); + g_assert_no_error (error); + g_assert_nonnull (xml2); + g_print ("%s\n", xml2); + g_assert_cmpstr ("" + "" + "inkscape.desktop" + "" + "", ==, xml2); +} + +static gboolean +xb_builder_fixup_root_node_only_cb (XbBuilderFixup *self, + XbBuilderNode *bn, + gpointer user_data, + GError **error) +{ + g_debug (">%s<", xb_builder_node_get_element (bn)); + g_assert_cmpstr (xb_builder_node_get_element (bn), ==, NULL); + return TRUE; +} + +static void +xb_builder_node_vfunc_depth_func (void) +{ + gboolean ret; + g_autoptr(GError) error = NULL; + g_autoptr(XbBuilder) builder = xb_builder_new (); + g_autoptr(XbBuilderFixup) fixup = NULL; + g_autoptr(XbBuilderSource) source = xb_builder_source_new (); + g_autoptr(XbSilo) silo = NULL; + const gchar *xml = + " \n" + " \n" + " gimp.desktop\n" + " \n" + " \n"; + + /* add fixup */ + fixup = xb_builder_fixup_new ("OnlyRoot", + xb_builder_fixup_root_node_only_cb, + NULL, NULL); + xb_builder_fixup_set_max_depth (fixup, 0); + xb_builder_source_add_fixup (source, fixup); + + /* import some XML */ + ret = xb_builder_source_load_xml (source, xml, XB_BUILDER_SOURCE_FLAG_NONE, &error); + g_assert_no_error (error); + g_assert_true (ret); + xb_builder_import_source (builder, source); + silo = xb_builder_compile (builder, + XB_BUILDER_COMPILE_FLAG_NONE, + NULL, &error); + g_assert_no_error (error); + g_assert_nonnull (silo); +} + +static void +xb_builder_ignore_invalid_func (void) +{ + gboolean ret; + g_autofree gchar *xml2 = NULL; + g_autoptr(GError) error = NULL; + g_autoptr(XbBuilder) builder = xb_builder_new (); + g_autoptr(XbSilo) silo = NULL; + + /* import some correct XML */ + ret = xb_test_import_xml (builder, "foobar", &error); + g_assert_no_error (error); + g_assert_true (ret); + + /* import some incorrect XML */ + ret = xb_test_import_xml (builder, "foobar", &error); + g_assert_no_error (error); + g_assert_true (ret); + silo = xb_builder_compile (builder, + XB_BUILDER_COMPILE_FLAG_IGNORE_INVALID, + NULL, &error); + g_assert_no_error (error); + g_assert_nonnull (silo); + + /* check the XML */ + xml2 = xb_silo_export (silo, XB_NODE_EXPORT_FLAG_NONE, &error); + g_assert_no_error (error); + g_assert_nonnull (xml2); + g_print ("%s\n", xml2); + g_assert_cmpstr ("foobar", ==, xml2); +} + +static void +xb_builder_empty_func (void) +{ + gboolean ret; + g_autofree gchar *str = NULL; + g_autofree gchar *xml = NULL; + g_autoptr(GBytes) bytes = NULL; + g_autoptr(GError) error = NULL; + g_autoptr(GPtrArray) results = NULL; + g_autoptr(XbBuilder) builder = xb_builder_new (); + g_autoptr(XbSilo) silo2 = xb_silo_new (); + g_autoptr(XbSilo) silo = NULL; + + /* import from XML */ + silo = xb_builder_compile (builder, XB_BUILDER_COMPILE_FLAG_NONE, NULL, &error); + g_assert_no_error (error); + g_assert_nonnull (silo); + g_assert_true (xb_silo_is_valid (silo)); + + /* check size */ + bytes = xb_silo_get_bytes (silo); + g_assert_cmpint (g_bytes_get_size (bytes), ==, 32); + + /* try to dump */ + str = xb_silo_to_string (silo, &error); + g_assert_no_error (error); + g_assert_nonnull (str); + g_debug ("%s", str); + + /* try to export */ + xml = xb_silo_export (silo, XB_NODE_EXPORT_FLAG_NONE, &error); + g_assert_error (error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND); + g_assert_null (xml); + g_clear_error (&error); + + /* try to query empty silo */ + results = xb_silo_query (silo, "components/component", 0, &error); + g_assert_error (error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND); + g_assert_null (results); + g_clear_error (&error); + + /* load blob */ + g_assert_nonnull (bytes); + ret = xb_silo_load_from_bytes (silo2, bytes, 0, &error); + g_assert_no_error (error); + g_assert_true (ret); +} + +static void +xb_xpath_node_func (void) +{ + g_autoptr(GError) error = NULL; + g_autoptr(GPtrArray) results = NULL; + g_autoptr(XbNode) n = NULL; + g_autoptr(XbSilo) silo = NULL; + const gchar *xml = + "\n" + " \n" + " gimp.desktop\n" + " org.gnome.Gimp.desktop\n" + " \n" + " \n" + " org.hughski.ColorHug2.firmware\n" + " \n" + "\n"; + + /* import from XML */ + silo = xb_silo_new_from_xml (xml, &error); + g_assert_no_error (error); + g_assert_nonnull (silo); + + /* get node */ + n = xb_silo_query_first (silo, "components/component", &error); + g_assert_no_error (error); + g_assert_nonnull (n); + g_assert_cmpstr (xb_node_get_attr (n, "type"), ==, "desktop"); + + /* query with text opcodes */ + results = xb_node_query (n, "id", 0, &error); + g_assert_no_error (error); + g_assert_nonnull (results); + g_assert_cmpint (results->len, ==, 2); +} + +static void +xb_node_data_func (void) +{ + g_autoptr(GError) error = NULL; + g_autoptr(XbNode) n = NULL; + g_autoptr(XbSilo) silo = NULL; + g_autoptr(GBytes) bytes = g_bytes_new ("foo", 4); + + /* import from XML */ + silo = xb_silo_new_from_xml ("gimp.desktop", &error); + g_assert_no_error (error); + g_assert_nonnull (silo); + + /* get node */ + n = xb_silo_query_first (silo, "id", &error); + g_assert_no_error (error); + g_assert_nonnull (n); + xb_node_set_data (n, "store", bytes); + xb_node_set_data (n, "store", bytes); + g_assert_nonnull (xb_node_get_data (n, "store")); + g_assert_null (xb_node_get_data (n, "dave")); +} + +static void +xb_xpath_parent_subnode_func (void) +{ + g_autofree gchar *xml2 = NULL; + g_autoptr(GError) error = NULL; + g_autoptr(GPtrArray) children = NULL; + g_autoptr(XbNode) n = NULL; + g_autoptr(XbNode) c = NULL; + g_autoptr(XbNode) p = NULL; + g_autoptr(XbSilo) silo = NULL; + const gchar *xml = + "\n" + " \n" + " gimp.desktop\n" + " org.gnome.Gimp.desktop\n" + " \n" + " \n" + " org.hughski.ColorHug2.firmware\n" + " \n" + "\n"; + + /* import from XML */ + silo = xb_silo_new_from_xml (xml, &error); + g_assert_no_error (error); + g_assert_nonnull (silo); + + /* get node */ + n = xb_silo_query_first (silo, "components/component", &error); + g_assert_no_error (error); + g_assert_nonnull (n); + g_assert_cmpstr (xb_node_get_attr (n, "type"), ==, "desktop"); + g_assert_cmpint (xb_node_get_depth (n), ==, 1); + + /* export a child */ + xml2 = xb_node_query_export (n, "id", &error); + g_assert_cmpstr (xml2, ==, "gimp.desktop"); + + /* get sibling */ + c = xb_node_get_next (n); + g_assert_nonnull (c); + g_assert_cmpstr (xb_node_get_attr (c, "type"), ==, "firmware"); + p = xb_node_get_next (c); + g_assert_null (p); + g_clear_object (&c); + + /* use the node to go back up the tree */ + c = xb_node_query_first (n, "..", &error); + g_assert_no_error (error); + g_assert_nonnull (c); + g_assert_cmpstr (xb_node_get_attr (c, "origin"), ==, "lvfs"); + + /* verify this is the parent */ + p = xb_node_get_root (n); + g_assert_cmpint (xb_node_get_depth (p), ==, 0); + g_assert (c == p); + children = xb_node_get_children (p); + g_assert_nonnull (children); + g_assert_cmpint (children->len, ==, 2); +} + +static void +xb_xpath_helpers_func (void) +{ + const gchar *tmp; + guint64 val; + g_autoptr(GError) error = NULL; + g_autoptr(XbNode) n = NULL; + g_autoptr(XbSilo) silo = NULL; + + /* import from XML */ + silo = xb_silo_new_from_xml ("456", &error); + g_assert_no_error (error); + g_assert_nonnull (silo); + + /* as char */ + n = xb_silo_get_root (silo); + g_assert_nonnull (n); + tmp = xb_node_query_text (n, "checksum", &error); + g_assert_no_error (error); + g_assert_cmpstr (tmp, ==, "456"); + tmp = xb_node_query_attr (n, "checksum", "size", &error); + g_assert_no_error (error); + g_assert_cmpstr (tmp, ==, "123"); + + /* as uint64 */ + val = xb_node_query_text_as_uint (n, "checksum", &error); + g_assert_no_error (error); + g_assert_cmpint (val, ==, 456); + val = xb_node_query_attr_as_uint (n, "checksum", "size", &error); + g_assert_no_error (error); + g_assert_cmpint (val, ==, 123); +} + +static void +xb_xpath_query_func (void) +{ + g_autoptr(GError) error = NULL; + g_autoptr(XbNode) n = NULL; + g_autoptr(XbSilo) silo = NULL; + const gchar *xml = + "\n" + " \n" + " n/a\n" + " \n" + "\n"; + + /* import from XML */ + silo = xb_silo_new_from_xml (xml, &error); + g_assert_no_error (error); + g_assert_nonnull (silo); + + /* query with slash */ + n = xb_silo_query_first (silo, "components/component/id[text()='n\\/a']", &error); + g_assert_no_error (error); + g_assert_nonnull (n); + g_assert_cmpstr (xb_node_get_text (n), ==, "n/a"); + g_clear_object (&n); + + /* query with an OR, where the first section contains an unknown element */ + n = xb_silo_query_first (silo, "components/dave|components/component/id", &error); + g_assert_no_error (error); + g_assert_nonnull (n); + g_assert_cmpstr (xb_node_get_text (n), ==, "n/a"); + g_clear_object (&n); + + /* query with an OR, where the last section contains an unknown element */ + n = xb_silo_query_first (silo, "components/component/id|components/dave", &error); + g_assert_no_error (error); + g_assert_nonnull (n); + g_assert_cmpstr (xb_node_get_text (n), ==, "n/a"); + g_clear_object (&n); + + /* query with an OR, all sections contains an unknown element */ + n = xb_silo_query_first (silo, "components/dave|components/mike", &error); + g_assert_error (error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT); + g_assert_null (n); +} + +static void +xb_xpath_incomplete_func (void) +{ + g_autoptr(GError) error = NULL; + g_autoptr(XbNode) n = NULL; + g_autoptr(XbSilo) silo = NULL; + const gchar *xml = + "\n" + " \n" + " gimp.desktop\n" + " \n" + "\n"; + + /* import from XML */ + silo = xb_silo_new_from_xml (xml, &error); + g_assert_no_error (error); + g_assert_nonnull (silo); + + /* query with MISSING '[' */ + n = xb_silo_query_first (silo, "components/component/id[text()='dave'", &error); + g_assert_error (error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT); + g_assert_null (n); +} + +static void +xb_xpath_func (void) +{ + XbNode *n; + XbNode *n2; + g_autofree gchar *str = NULL; + g_autofree gchar *xml_sub1 = NULL; + g_autofree gchar *xml_sub2 = NULL; + g_autofree gchar *xml_sub3 = NULL; + g_autoptr(GError) error = NULL; + g_autoptr(GPtrArray) results = NULL; + g_autoptr(XbNode) n3 = NULL; + g_autoptr(XbSilo) silo = NULL; + const gchar *xml = + "\n" + "
\n" + " dead\n" + "
\n" + " \n" + " gimp.desktop\n" + " org.gnome.Gimp.desktop\n" + " \n" + " TRUE\n" + " \n" + " \n" + " \n" + " org.hughski.ColorHug2.firmware\n" + " \n" + "
\n"; + + /* import from XML */ + silo = xb_silo_new_from_xml (xml, &error); + g_assert_no_error (error); + g_assert_nonnull (silo); + + /* set up debugging */ + xb_machine_set_debug_flags (xb_silo_get_machine (silo), + XB_MACHINE_DEBUG_FLAG_SHOW_STACK); + + /* dump to screen */ + str = xb_silo_to_string (silo, &error); + g_assert_no_error (error); + g_assert_nonnull (str); + g_debug ("\n%s", str); + + /* query with predicate logical and */ + n = xb_silo_query_first (silo, "components/component/custom/value[(@key='KEY') and (text()='TRUE')]/../../id", &error); + g_assert_no_error (error); + g_assert_nonnull (n); + g_assert_cmpstr (xb_node_get_text (n), ==, "gimp.desktop"); + g_clear_object (&n); + + /* query with predicate logical and; failure */ + n = xb_silo_query_first (silo, "components/component/custom/value[(@key='KEY')&&(text()='FALSE')]/../../id", &error); + g_assert_error (error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND); + g_assert_null (n); + g_clear_error (&error); + g_clear_object (&n); + + /* query with predicate logical and, alternate form */ + n = xb_silo_query_first (silo, "components/component/custom/value[and((@key='KEY'),(text()='TRUE'))]/../../id", &error); + g_assert_no_error (error); + g_assert_nonnull (n); + g_assert_cmpstr (xb_node_get_text (n), ==, "gimp.desktop"); + g_clear_object (&n); + + /* query that doesn't find anything */ + n = xb_silo_query_first (silo, "dave", &error); + g_assert_error (error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT); + g_assert_null (n); + g_clear_error (&error); + g_clear_object (&n); + + n = xb_silo_query_first (silo, "dave/dave", &error); + g_assert_error (error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT); + g_assert_null (n); + g_clear_error (&error); + g_clear_object (&n); + + n = xb_silo_query_first (silo, "components/dave", &error); + g_assert_error (error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT); + g_assert_null (n); + g_clear_error (&error); + g_clear_object (&n); + + n = xb_silo_query_first (silo, "components/component[@type='dave']/id", &error); + g_assert_error (error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND); + g_assert_null (n); + g_clear_error (&error); + g_clear_object (&n); + + n = xb_silo_query_first (silo, "components/component[@percentage>=90]", &error); + g_assert_error (error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND); + g_assert_null (n); + g_clear_error (&error); + g_clear_object (&n); + + n = xb_silo_query_first (silo, "components/component/id[text()='dave']", &error); + g_assert_error (error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND); + g_assert_null (n); + g_clear_error (&error); + g_clear_object (&n); + + /* query with attr opcodes */ + n = xb_silo_query_first (silo, "components/component[@type='firmware']/id", &error); + g_assert_no_error (error); + g_assert_nonnull (n); + g_assert_cmpstr (xb_node_get_text (n), ==, "org.hughski.ColorHug2.firmware"); + g_clear_object (&n); + + /* query with attr opcodes */ + n = xb_silo_query_first (silo, "components/component[@type!='firmware']/id", &error); + g_assert_no_error (error); + g_assert_nonnull (n); + g_assert_cmpstr (xb_node_get_text (n), ==, "gimp.desktop"); + g_clear_object (&n); + + /* query with attr opcodes with quotes */ + n = xb_silo_query_first (silo, "components/component[@type='firmware']/id", &error); + g_assert_no_error (error); + g_assert_nonnull (n); + g_assert_cmpstr (xb_node_get_text (n), ==, "org.hughski.ColorHug2.firmware"); + g_clear_object (&n); + + /* query with position */ + n = xb_silo_query_first (silo, "components/component[2]/id", &error); + g_assert_no_error (error); + g_assert_nonnull (n); + g_assert_cmpstr (xb_node_get_text (n), ==, "org.hughski.ColorHug2.firmware"); + g_clear_object (&n); + + /* last() with position */ + n = xb_silo_query_first (silo, "components/component[last()]/id", &error); + g_assert_no_error (error); + g_assert_nonnull (n); + g_assert_cmpstr (xb_node_get_text (n), ==, "org.hughski.ColorHug2.firmware"); + g_clear_object (&n); + + /* query with attr opcodes that exists */ + n = xb_silo_query_first (silo, "components/component[@type]/id", &error); + g_assert_no_error (error); + g_assert_nonnull (n); + g_assert_cmpstr (xb_node_get_text (n), ==, "gimp.desktop"); + g_clear_object (&n); + + /* query with attrs that do not exist */ + n = xb_silo_query_first (silo, "components/component[not(@dave)]/id", &error); + g_assert_no_error (error); + g_assert_nonnull (n); + g_assert_cmpstr (xb_node_get_text (n), ==, "gimp.desktop"); + g_clear_object (&n); + + /* query with wildcard with predicate */ + n = xb_silo_query_first (silo, "components/*[@type]/id", &error); + g_assert_no_error (error); + g_assert_nonnull (n); + g_assert_cmpstr (xb_node_get_text (n), ==, "gimp.desktop"); + g_clear_object (&n); + + /* query with text opcodes */ + n = xb_silo_query_first (silo, "components/header/csum[text()='dead']", &error); + g_assert_no_error (error); + g_assert_nonnull (n); + g_assert_cmpstr (xb_node_get_attr (n, "type"), ==, "sha1"); + g_clear_object (&n); + + /* query with search */ + n = xb_silo_query_first (silo, "components/component/id[text()~='gimp']", &error); + g_assert_no_error (error); + g_assert_nonnull (n); + g_assert_cmpstr (xb_node_get_text (n), ==, "gimp.desktop"); + g_clear_object (&n); + + /* query with stem */ +#ifdef HAVE_LIBSTEMMER + xb_machine_set_debug_flags (xb_silo_get_machine (silo), + XB_MACHINE_DEBUG_FLAG_SHOW_STACK | + XB_MACHINE_DEBUG_FLAG_SHOW_PARSING); + n = xb_silo_query_first (silo, "components/component/id[text()~=stem('gimping')]", &error); + g_assert_no_error (error); + g_assert_nonnull (n); + g_assert_cmpstr (xb_node_get_text (n), ==, "gimp.desktop"); + g_clear_object (&n); + xb_machine_set_debug_flags (xb_silo_get_machine (silo), + XB_MACHINE_DEBUG_FLAG_SHOW_STACK); +#endif + + /* query with text:integer */ + n = xb_silo_query_first (silo, "components/component/id['123'=123]", &error); + g_assert_no_error (error); + g_assert_nonnull (n); + g_assert_cmpstr (xb_node_get_text (n), ==, "gimp.desktop"); + g_clear_object (&n); + + /* query with integer:text */ + n = xb_silo_query_first (silo, "components/component/id[123='123']", &error); + g_assert_no_error (error); + g_assert_nonnull (n); + g_assert_cmpstr (xb_node_get_text (n), ==, "gimp.desktop"); + g_clear_object (&n); + + /* query with prefix */ + n = xb_silo_query_first (silo, "components/component/id[starts-with(text(),'gimp')]", &error); + g_assert_no_error (error); + g_assert_nonnull (n); + g_assert_cmpstr (xb_node_get_text (n), ==, "gimp.desktop"); + g_clear_object (&n); + + /* query with suffix */ + n = xb_silo_query_first (silo, "components/component/id[ends-with(text(),'.desktop')]", &error); + g_assert_no_error (error); + g_assert_nonnull (n); + g_assert_cmpstr (xb_node_get_text (n), ==, "gimp.desktop"); + g_clear_object (&n); + + /* query with contains */ + n = xb_silo_query_first (silo, "components/component/id[contains(text(),'imp')]", &error); + g_assert_no_error (error); + g_assert_nonnull (n); + g_assert_cmpstr (xb_node_get_text (n), ==, "gimp.desktop"); + g_clear_object (&n); + + /* query with type-conversion */ + n = xb_silo_query_first (silo, "components/component[position()=number('2')]/id", &error); + g_assert_no_error (error); + g_assert_nonnull (n); + g_assert_cmpstr (xb_node_get_text (n), ==, "org.hughski.ColorHug2.firmware"); + g_clear_object (&n); + + /* query with another type-conversion */ + n = xb_silo_query_first (silo, "components/component['2'=string(2)]/id", &error); + g_assert_no_error (error); + g_assert_nonnull (n); + g_assert_cmpstr (xb_node_get_text (n), ==, "gimp.desktop"); + g_clear_object (&n); + + /* query with backtrack */ + g_debug ("\n%s", xml); + n = xb_silo_query_first (silo, "components/component[@type='firmware']/id[text()='org.hughski.ColorHug2.firmware']", &error); + g_assert_no_error (error); + g_assert_nonnull (n); + g_assert_cmpstr (xb_node_get_text (n), ==, "org.hughski.ColorHug2.firmware"); + g_clear_object (&n); + + /* query with nesting */ + g_debug ("\n%s", xml); + n = xb_silo_query_first (silo, "components/component/id[text()=lower-case(upper-case('Gimp.DESKTOP'))]", &error); + g_assert_no_error (error); + g_assert_nonnull (n); + g_assert_cmpstr (xb_node_get_text (n), ==, "gimp.desktop"); + g_clear_object (&n); + + /* query for multiple results */ + results = xb_silo_query (silo, "components/component/id", 5, &error); + g_assert_no_error (error); + g_assert_nonnull (results); + g_assert_cmpint (results->len, ==, 3); + n2 = g_ptr_array_index (results, 2); + g_assert_cmpstr (xb_node_get_text (n2), ==, "org.hughski.ColorHug2.firmware"); + + /* subtree export */ + xml_sub1 = xb_node_export (n2, XB_NODE_EXPORT_FLAG_NONE, &error); + g_assert_no_error (error); + g_assert_nonnull (xml_sub1); + g_assert_cmpstr (xml_sub1, ==, "org.hughski.ColorHug2.firmware"); + + /* parent of subtree */ + n3 = xb_node_get_parent (n2); + g_assert (n3 != NULL); + xml_sub2 = xb_node_export (n3, XB_NODE_EXPORT_FLAG_NONE, &error); + g_assert_no_error (error); + g_assert_nonnull (xml_sub2); + g_assert_cmpstr (xml_sub2, ==, "org.hughski.ColorHug2.firmware"); + + /* only children of parent */ + xml_sub3 = xb_node_export (n3, XB_NODE_EXPORT_FLAG_ONLY_CHILDREN, &error); + g_assert_no_error (error); + g_assert_nonnull (xml_sub3); + g_assert_cmpstr (xml_sub3, ==, "org.hughski.ColorHug2.firmware"); +} + +static void +xb_builder_native_lang_func (void) +{ + gboolean ret; + g_autoptr(GError) error = NULL; + g_autofree gchar *str = NULL; + g_autofree gchar *tmp = NULL; + g_autoptr(XbBuilder) builder = xb_builder_new (); + g_autoptr(XbNode) n = NULL; + g_autoptr(XbSilo) silo = NULL; + const gchar *xml = + "\n" + " \n" + "

Wilcommen

\n" + "

Hello

\n" + "

Salut

\n" + "

Goodbye

\n" + "

Auf Wiedersehen

\n" + "

Au revoir

\n" + "
\n" + "
\n"; + + /* import from XML */ + ret = xb_test_import_xml (builder, xml, &error); + g_assert_no_error (error); + g_assert_true (ret); + xb_builder_add_locale (builder, "fr_FR.UTF-8"); + xb_builder_add_locale (builder, "fr_FR"); + xb_builder_add_locale (builder, "fr_FR"); + xb_builder_add_locale (builder, "fr"); + xb_builder_add_locale (builder, "C"); + silo = xb_builder_compile (builder, XB_BUILDER_COMPILE_FLAG_SINGLE_LANG, NULL, &error); + g_assert_no_error (error); + g_assert_nonnull (silo); + + /* test we removed other languages */ + str = xb_silo_to_string (silo, &error); + g_assert_no_error (error); + g_assert_nonnull (str); + g_debug ("\n%s", str); + g_assert_null (g_strstr_len (str, -1, "Wilcommen")); + g_assert_null (g_strstr_len (str, -1, "Hello")); + g_assert_nonnull (g_strstr_len (str, -1, "Salut")); + g_assert_null (g_strstr_len (str, -1, "Goodbye")); + g_assert_null (g_strstr_len (str, -1, "Auf Wiedersehen")); + g_assert_nonnull (g_strstr_len (str, -1, "Au revoir")); + + /* test we traversed the tree correctly */ + n = xb_silo_query_first (silo, "components/component/*", &error); + g_assert_no_error (error); + g_assert_nonnull (n); + tmp = xb_node_export (n, XB_NODE_EXPORT_FLAG_INCLUDE_SIBLINGS, &error); + g_assert_cmpstr (tmp, ==, "

Salut

Au revoir

"); +} + +static void +xb_builder_native_lang2_func (void) +{ + gboolean ret; + g_autoptr(GError) error = NULL; + g_autofree gchar *str = NULL; + g_autoptr(XbBuilder) builder = xb_builder_new (); + g_autoptr(XbSilo) silo = NULL; + const gchar *xml = + "\n" + " \n" + "

Wilcommen

\n" + "

Hello

\n" + "

Salut

\n" + "
\n" + "
\n"; + + /* import from XML */ + ret = xb_test_import_xml (builder, xml, &error); + g_assert_no_error (error); + g_assert_true (ret); + xb_builder_add_locale (builder, "fr_FR"); + xb_builder_add_locale (builder, "fr"); + xb_builder_add_locale (builder, "C"); + silo = xb_builder_compile (builder, XB_BUILDER_COMPILE_FLAG_SINGLE_LANG, NULL, &error); + g_assert_no_error (error); + g_assert_nonnull (silo); + + /* test we removed other languages */ + str = xb_silo_to_string (silo, &error); + g_assert_no_error (error); + g_assert_nonnull (str); + g_assert_null (g_strstr_len (str, -1, "Wilcommen")); + g_assert_null (g_strstr_len (str, -1, "Hello")); + g_assert_nonnull (g_strstr_len (str, -1, "Salut")); + g_debug ("\n%s", str); +} + +static void +xb_builder_native_lang_no_locales_func (void) +{ + gboolean ret; + g_autoptr(GError) error = NULL; + g_autoptr(XbBuilder) builder = xb_builder_new (); + g_autoptr(XbSilo) silo = NULL; + + /* import from XML */ + ret = xb_test_import_xml (builder, "gimp.desktop", &error); + g_assert_no_error (error); + g_assert_true (ret); + silo = xb_builder_compile (builder, XB_BUILDER_COMPILE_FLAG_SINGLE_LANG, NULL, &error); + g_assert_error (error, G_IO_ERROR, G_IO_ERROR_INVALID_DATA); + g_assert_null (silo); +} + +static void +xb_xpath_parent_func (void) +{ + XbNode *n; + gboolean ret; + g_autoptr(GError) error = NULL; + g_autoptr(XbBuilder) builder = xb_builder_new (); + g_autoptr(XbSilo) silo = NULL; + const gchar *xml = + "\n" + "
\n" + " dead\n" + "
\n" + " \n" + " gimp.desktop\n" + " org.gnome.Gimp.desktop\n" + " \n" + " \n" + " org.hughski.ColorHug2.firmware\n" + " colorhug-client\n" + "

Wilcommen!

\n" + "

hello!

\n" + "

Bonjour!

\n" + " GPL-2.0\n" + "
\n" + "
\n"; + + /* import from XML */ + ret = xb_test_import_xml (builder, xml, &error); + g_assert_no_error (error); + g_assert_true (ret); + xb_builder_add_locale (builder, "C"); + silo = xb_builder_compile (builder, XB_BUILDER_COMPILE_FLAG_NATIVE_LANGS, NULL, &error); + g_assert_no_error (error); + g_assert_nonnull (silo); + + /* get node, no parent */ + n = xb_silo_query_first (silo, "components/component[@type='firmware']/id", &error); + g_assert_no_error (error); + g_assert_nonnull (n); + g_assert_cmpstr (xb_node_get_text (n), ==, "org.hughski.ColorHug2.firmware"); + g_assert_cmpstr (xb_node_get_element (n), ==, "id"); + g_clear_object (&n); + + /* get node, one parent */ + n = xb_silo_query_first (silo, "components/component[@type='firmware']/id/..", &error); + g_assert_no_error (error); + g_assert_nonnull (n); + g_assert_cmpstr (xb_node_get_element (n), ==, "component"); + g_clear_object (&n); + + /* get node, multiple parents */ + n = xb_silo_query_first (silo, "components/component[@type='firmware']/id/../..", &error); + g_assert_no_error (error); + g_assert_nonnull (n); + g_assert_cmpstr (xb_node_get_element (n), ==, "components"); + g_clear_object (&n); + + /* descend, ascend, descend */ + n = xb_silo_query_first (silo, "components/component[@type='firmware']/pkgname/../project_license", &error); + g_assert_no_error (error); + g_assert_nonnull (n); + g_assert_cmpstr (xb_node_get_text (n), ==, "GPL-2.0"); + g_clear_object (&n); + + /* descend, ascend, descend */ + n = xb_silo_query_first (silo, "components/component/pkgname[text()~='colorhug']/../id", &error); + g_assert_no_error (error); + g_assert_nonnull (n); + g_assert_cmpstr (xb_node_get_text (n), ==, "org.hughski.ColorHug2.firmware"); + g_clear_object (&n); + + /* get node, too many parents */ + n = xb_silo_query_first (silo, "components/component[@type='firmware']/id/../../..", &error); + g_assert_error (error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT); + g_assert_null (n); + g_clear_error (&error); + + /* can't go lower than root */ + n = xb_silo_query_first (silo, "..", &error); + g_assert_error (error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT); + g_assert_null (n); + g_clear_error (&error); + + /* fuzzy substring match */ + n = xb_silo_query_first (silo, "components/component/pkgname[text()~='colorhug']", &error); + g_assert_no_error (error); + g_assert_nonnull (n); + g_assert_cmpstr (xb_node_get_text (n), ==, "colorhug-client"); + g_clear_object (&n); + + /* strlen match */ + n = xb_silo_query_first (silo, "components/component/pkgname[string-length(text())==15]", &error); + g_assert_no_error (error); + g_assert_nonnull (n); + g_assert_cmpstr (xb_node_get_text (n), ==, "colorhug-client"); + g_clear_object (&n); + + /* fuzzy substring match */ + n = xb_silo_query_first (silo, "components/component[@type~='firm']/pkgname", &error); + g_assert_no_error (error); + g_assert_nonnull (n); + g_assert_cmpstr (xb_node_get_text (n), ==, "colorhug-client"); + g_clear_object (&n); +} + +static void +xb_xpath_prepared_func (void) +{ + XbNode *n; + gboolean ret; + g_autoptr(GError) error = NULL; + g_autoptr(XbBuilder) builder = xb_builder_new (); + g_autoptr(XbSilo) silo = NULL; + g_autoptr(XbQuery) query = NULL; + g_autoptr(XbNode) component = NULL; + g_autoptr(GPtrArray) components = NULL; + const gchar *xml = + "\n" + " \n" + " gimp.desktop\n" + " org.gnome.Gimp.desktop\n" + " \n" + " \n" + " org.hughski.ColorHug2.firmware\n" + " colorhug-client\n" + " \n" + "\n"; + + /* import from XML */ + ret = xb_test_import_xml (builder, xml, &error); + g_assert_no_error (error); + g_assert_true (ret); + xb_builder_add_locale (builder, "C"); + silo = xb_builder_compile (builder, XB_BUILDER_COMPILE_FLAG_NATIVE_LANGS, NULL, &error); + g_assert_no_error (error); + g_assert_nonnull (silo); + + /* get first component */ + component = xb_silo_query_first (silo, "components/component", &error); + g_assert_no_error (error); + g_assert_nonnull (component); + + /* prepared statement on node */ + query = xb_query_new (silo, "id[text()=?]/..", &error); + g_assert_no_error (error); + g_assert_nonnull (query); + ret = xb_query_bind_str (query, 0, "gimp.desktop", &error); + g_assert_no_error (error); + g_assert_true (ret); + components = xb_node_query_full (component, query, &error); + g_assert_no_error (error); + g_assert_nonnull (components); + g_assert_cmpint (components->len, ==, 1); + n = g_ptr_array_index (components, 0); + g_assert_cmpstr (xb_node_get_attr (n, "type"), ==, "desktop"); +} + +static void +xb_xpath_query_reverse_func (void) +{ + XbNode *n; + gboolean ret; + g_autoptr(GError) error = NULL; + g_autoptr(XbBuilder) builder = xb_builder_new (); + g_autoptr(XbSilo) silo = NULL; + g_autoptr(XbQuery) query = NULL; + g_autoptr(GPtrArray) names = NULL; + const gchar *xml = + "\n" + " foo\n" + " bar\n" + " baz\n" + "\n"; + + /* import from XML */ + ret = xb_test_import_xml (builder, xml, &error); + g_assert_no_error (error); + g_assert_true (ret); + silo = xb_builder_compile (builder, XB_BUILDER_COMPILE_FLAG_NONE, NULL, &error); + g_assert_no_error (error); + g_assert_nonnull (silo); + + /* get first when reversed */ + query = xb_query_new_full (silo, "names/name", XB_QUERY_FLAG_REVERSE, &error); + g_assert_no_error (error); + g_assert_nonnull (query); + names = xb_silo_query_full (silo, query, &error); + g_assert_no_error (error); + g_assert_nonnull (names); + g_assert_cmpint (names->len, ==, 3); + n = g_ptr_array_index (names, 0); + g_assert_cmpstr (xb_node_get_text (n), ==, "baz"); +} + +static void +xb_xpath_glob_func (void) +{ + g_autofree gchar *xml2 = NULL; + g_autoptr(GError) error = NULL; + g_autoptr(XbNode) n = NULL; + g_autoptr(XbSilo) silo = NULL; + const gchar *xml = + "\n" + " \n" + " gimp.desktop\n" + " org.gnome.GIMP.desktop\n" + " \n" + "\n"; + + /* import from XML */ + silo = xb_silo_new_from_xml (xml, &error); + g_assert_no_error (error); + g_assert_nonnull (silo); + + /* get node, no parent */ + n = xb_silo_query_first (silo, "components/component[@type='desktop']/*", &error); + g_assert_no_error (error); + g_assert_nonnull (n); + g_assert_cmpstr (xb_node_get_element (n), ==, "id"); + + /* export this one node */ + xml2 = xb_node_export (n, XB_NODE_EXPORT_FLAG_NONE, &error); + g_assert_no_error (error); + g_assert_cmpstr (xml2, ==, "gimp.desktop"); +} + +static void +xb_builder_multiple_roots_func (void) +{ + gboolean ret; + g_autofree gchar *str = NULL; + g_autofree gchar *xml_new = NULL; + g_autoptr(GError) error = NULL; + g_autoptr(GPtrArray) results = NULL; + g_autoptr(XbBuilder) builder = NULL; + g_autoptr(XbSilo) silo = NULL; + + /* import from XML */ + builder = xb_builder_new (); + ret = xb_test_import_xml (builder, "value", &error); + g_assert_no_error (error); + g_assert_true (ret); + ret = xb_test_import_xml (builder, "value2value3", &error); + g_assert_no_error (error); + g_assert_true (ret); + silo = xb_builder_compile (builder, XB_BUILDER_COMPILE_FLAG_NONE, NULL, &error); + g_assert_no_error (error); + g_assert_nonnull (silo); + + /* convert back to XML */ + str = xb_silo_to_string (silo, &error); + g_assert_no_error (error); + g_assert_nonnull (str); + g_debug ("\n%s", str); + xml_new = xb_silo_export (silo, XB_NODE_EXPORT_FLAG_INCLUDE_SIBLINGS, &error); + g_assert_no_error (error); + g_assert_nonnull (xml_new); + g_print ("%s", xml_new); + g_assert_cmpstr ("valuevalue2value3", ==, xml_new); + + /* query for multiple results */ + results = xb_silo_query (silo, "tag", 5, &error); + g_assert_no_error (error); + g_assert_nonnull (results); + g_assert_cmpint (results->len, ==, 3); +} + +static void +xb_builder_node_func (void) +{ + g_autofree gchar *xml = NULL; + g_autofree gchar *xml_src = NULL; + g_autoptr(GError) error = NULL; + g_autoptr(XbBuilder) builder = xb_builder_new (); + g_autoptr(XbBuilderNode) child_by_element = NULL; + g_autoptr(XbBuilderNode) child_by_text = NULL; + g_autoptr(XbBuilderNode) component = NULL; + g_autoptr(XbBuilderNode) components = NULL; + g_autoptr(XbBuilderNode) id = NULL; + g_autoptr(XbBuilderNode) root = xb_builder_node_new (NULL); + g_autoptr(XbSilo) silo = NULL; + + /* create a simple document */ + components = xb_builder_node_insert (root, "components", + "origin", "lvfs", + NULL); + g_assert_cmpint (xb_builder_node_depth (components), ==, 1); + component = xb_builder_node_insert (components, "component", NULL); + g_assert_cmpint (xb_builder_node_depth (component), ==, 2); + xb_builder_node_set_attr (component, "type", "firmware"); + xb_builder_node_set_attr (component, "type", "desktop"); + g_assert_cmpstr (xb_builder_node_get_attr (component, "type"), ==, "desktop"); + g_assert_cmpstr (xb_builder_node_get_attr (component, "dave"), ==, NULL); + id = xb_builder_node_new ("id"); + xb_builder_node_add_child (component, id); + xb_builder_node_set_text (id, "gimp.desktop", -1); + xb_builder_node_insert_text (component, "icon", "dave", "type", "stock", NULL); + g_assert_cmpint (xb_builder_node_depth (id), ==, 3); + + /* get specific child */ + child_by_element = xb_builder_node_get_child (components, "component", NULL); + g_assert_nonnull (child_by_element); + g_assert_cmpstr (xb_builder_node_get_element (child_by_element), ==, "component"); + child_by_text = xb_builder_node_get_child (component, "id", "gimp.desktop"); + g_assert_nonnull (child_by_text); + g_assert_cmpstr (xb_builder_node_get_element (child_by_text), ==, "id"); + + /* check the source XML */ + xml_src = xb_builder_node_export (components, + XB_NODE_EXPORT_FLAG_FORMAT_MULTILINE, + &error); + g_assert_no_error (error); + g_assert_nonnull (xml_src); + g_print ("%s", xml_src); + g_assert_cmpstr ("\n" + "\n" + "gimp.desktop\n" + "dave\n" + "\n" + "\n", ==, xml_src); + + /* import the doc */ + xb_builder_import_node (builder, root); + silo = xb_builder_compile (builder, XB_BUILDER_COMPILE_FLAG_NONE, NULL, &error); + g_assert_no_error (error); + g_assert_nonnull (silo); + + /* check the XML */ + xml = xb_silo_export (silo, XB_NODE_EXPORT_FLAG_INCLUDE_SIBLINGS, &error); + g_assert_no_error (error); + g_assert_nonnull (xml); + g_print ("%s", xml); + g_assert_cmpstr ("" + "" + "gimp.desktop" + "dave" + "" + "", ==, xml); +} + +static void +xb_builder_node_literal_text_func (void) +{ + gboolean ret; + g_autofree gchar *xml2 = NULL; + g_autoptr(GError) error = NULL; + g_autoptr(XbBuilder) builder = xb_builder_new (); + g_autoptr(XbBuilderSource) source = xb_builder_source_new (); + g_autoptr(XbSilo) silo = NULL; + const gchar *xml = + " \n" + " \n" + "

Really long content\n" + "spanning multiple lines\n" + "

\n" + "
\n" + "
\n"; + + /* import some XML */ + ret = xb_builder_source_load_xml (source, xml, XB_BUILDER_SOURCE_FLAG_LITERAL_TEXT, &error); + g_assert_no_error (error); + g_assert_true (ret); + xb_builder_import_source (builder, source); + silo = xb_builder_compile (builder, + XB_BUILDER_COMPILE_FLAG_NONE, + NULL, &error); + g_assert_no_error (error); + g_assert_nonnull (silo); + + /* check the XML */ + xml2 = xb_silo_export (silo, XB_NODE_EXPORT_FLAG_NONE, &error); + g_assert_no_error (error); + g_assert_nonnull (xml2); + g_print ("%s\n", xml2); + g_assert_cmpstr ("" + "

Really long content\nspanning multiple lines\n

" + "
", ==, xml2); +} + +static void +xb_builder_node_source_text_func (void) +{ + gboolean ret; + g_autofree gchar *xml2 = NULL; + g_autoptr(GError) error = NULL; + g_autoptr(XbBuilder) builder = xb_builder_new (); + g_autoptr(XbBuilderSource) source = xb_builder_source_new (); + g_autoptr(XbSilo) silo = NULL; + const gchar *xml = + " \n" + " \n" + "

Really long content\n" + "spanning multiple lines\n" + "

\n" + "
\n" + "
\n"; + + /* import some XML */ + ret = xb_builder_source_load_xml (source, xml, XB_BUILDER_SOURCE_FLAG_NONE, &error); + g_assert_no_error (error); + g_assert_true (ret); + xb_builder_import_source (builder, source); + silo = xb_builder_compile (builder, + XB_BUILDER_COMPILE_FLAG_NONE, + NULL, &error); + g_assert_no_error (error); + g_assert_nonnull (silo); + + /* check the XML */ + xml2 = xb_silo_export (silo, XB_NODE_EXPORT_FLAG_NONE, &error); + g_assert_no_error (error); + g_assert_nonnull (xml2); + g_print ("%s\n", xml2); + g_assert_cmpstr ("" + "

Really long content spanning multiple lines

" + "
", ==, xml2); +} + +static void +xb_builder_node_info_func (void) +{ + gboolean ret; + g_autofree gchar *xml = NULL; + g_autofree gchar *tmp_xml = g_build_filename (g_get_tmp_dir (), "temp.xml", NULL); + g_autoptr(GError) error = NULL; + g_autoptr(XbBuilderSource) import1 = xb_builder_source_new (); + g_autoptr(XbBuilderSource) import2 = xb_builder_source_new (); + g_autoptr(XbBuilder) builder = xb_builder_new (); + g_autoptr(XbNode) n = NULL; + g_autoptr(XbBuilderNode) info1 = NULL; + g_autoptr(XbBuilderNode) info2 = NULL; + g_autoptr(XbSilo) silo = NULL; + g_autoptr(GFile) file = NULL; + + /* create a simple document with some info */ + ret = g_file_set_contents (tmp_xml, + "\n" + "dave", + -1, &error); + g_assert_no_error (error); + g_assert_true (ret); + info1 = xb_builder_node_insert (NULL, "info", NULL); + xb_builder_node_insert_text (info1, "scope", "user", NULL); + info2 = xb_builder_node_insert (NULL, "info", NULL); + xb_builder_node_insert_text (info2, "scope", "system", NULL); + + /* import the doc */ + file = g_file_new_for_path (tmp_xml); + ret = xb_builder_source_load_file (import1, file, XB_BUILDER_SOURCE_FLAG_NONE, NULL, &error); + g_assert_no_error (error); + g_assert_true (ret); + xb_builder_source_set_info (import1, info1); + xb_builder_source_set_prefix (import1, "local"); + xb_builder_import_source (builder, import1); + ret = xb_builder_source_load_file (import2, file, XB_BUILDER_SOURCE_FLAG_NONE, NULL, &error); + g_assert_no_error (error); + g_assert_true (ret); + xb_builder_source_set_info (import2, info2); + xb_builder_source_set_prefix (import2, "local"); + xb_builder_import_source (builder, import2); + silo = xb_builder_compile (builder, XB_BUILDER_COMPILE_FLAG_NONE, NULL, &error); + g_assert_no_error (error); + g_assert_nonnull (silo); + + /* get info */ + n = xb_silo_query_first (silo, "local/component/id[text()='dave']/../info/scope", &error); + g_assert_no_error (error); + g_assert_nonnull (n); + g_assert_cmpstr (xb_node_get_text (n), ==, "user"); + + /* check the XML */ + xml = xb_silo_export (silo, XB_NODE_EXPORT_FLAG_INCLUDE_SIBLINGS, &error); + g_assert_no_error (error); + g_assert_nonnull (xml); + g_assert_cmpstr ("" + "" + "dave" + "" + "user" + "" + "" + "" + "dave" + "" + "system" + "" + "" + "" + , ==, xml); +} + +static void +xb_threading_cb (gpointer data, gpointer user_data) +{ + XbSilo *silo = XB_SILO (user_data); + gint i = g_random_int_range (0, 50); + g_autofree gchar *xpath = NULL; + g_autoptr(GError) error = NULL; + g_autoptr(GPtrArray) components = NULL; + + /* do query */ + xpath = g_strdup_printf ("components/component/id[text()='%06i.firmware']", i); + components = xb_silo_query (silo, xpath, 0, &error); + g_assert_no_error (error); + g_assert_nonnull (components); + g_assert_cmpint (components->len, ==, 1); + g_print ("."); +} + +static void +xb_threading_func (void) +{ + GThreadPool *pool; + gboolean ret; + guint n_components = 10000; + g_autoptr(GError) error = NULL; + g_autoptr(GString) xml = g_string_new (NULL); + g_autoptr(XbSilo) silo = NULL; + +#ifdef __s390x__ + /* this is run with qemu and takes too much time */ + g_test_skip ("s390 too slow, skipping"); + return; +#endif + + /* create a huge document */ + g_string_append (xml, ""); + for (guint i = 0; i < n_components; i++) { + g_string_append (xml, ""); + g_string_append_printf (xml, " %06u.firmware", i); + g_string_append (xml, " ColorHug2"); + g_string_append (xml, " Firmware"); + g_string_append (xml, "

New features!

"); + g_string_append (xml, "
"); + } + g_string_append (xml, "
"); + + /* import from XML */ + silo = xb_silo_new_from_xml (xml->str, &error); + g_assert_no_error (error); + g_assert_nonnull (silo); + + /* create thread pool */ + pool = g_thread_pool_new (xb_threading_cb, silo, 20, TRUE, &error); + g_assert_no_error (error); + g_assert_nonnull (pool); + + /* run threads */ + for (guint i = 0; i < 100; i++) { + ret = g_thread_pool_push (pool, &i, &error); + g_assert_no_error (error); + g_assert_true (ret); + } + g_thread_pool_free (pool, FALSE, TRUE); +} + +typedef struct { + guint cnt; + GString *str; +} XbMarkupHelper; + +static gboolean +xb_markup_head_cb (XbNode *n, gpointer user_data) +{ + XbMarkupHelper *helper = (XbMarkupHelper *) user_data; + helper->cnt++; + + if (xb_node_get_text (n) == NULL) + return FALSE; + + /* start */ + if (g_strcmp0 (xb_node_get_element (n), "em") == 0) { + g_string_append (helper->str, "*"); + } else if (g_strcmp0 (xb_node_get_element (n), "strong") == 0) { + g_string_append (helper->str, "**"); + } else if (g_strcmp0 (xb_node_get_element (n), "code") == 0) { + g_string_append (helper->str, "`"); + } + + /* text */ + if (xb_node_get_text (n) != NULL) + g_string_append (helper->str, xb_node_get_text (n)); + + return FALSE; +} + +static gboolean +xb_markup_tail_cb (XbNode *n, gpointer user_data) +{ + XbMarkupHelper *helper = (XbMarkupHelper *) user_data; + helper->cnt++; + + /* end */ + if (g_strcmp0 (xb_node_get_element (n), "em") == 0) { + g_string_append (helper->str, "*"); + } else if (g_strcmp0 (xb_node_get_element (n), "strong") == 0) { + g_string_append (helper->str, "**"); + } else if (g_strcmp0 (xb_node_get_element (n), "code") == 0) { + g_string_append (helper->str, "`"); + } else if (g_strcmp0 (xb_node_get_element (n), "p") == 0) { + g_string_append (helper->str, "\n\n"); + } + + /* tail */ + if (xb_node_get_tail (n) != NULL) + g_string_append (helper->str, xb_node_get_tail (n)); + + return FALSE; +} + +static void +xb_markup_func (void) +{ + gboolean ret; + g_autofree gchar *new = NULL; + g_autofree gchar *tmp = NULL; + g_autoptr(GError) error = NULL; + g_autoptr(XbNode) n = NULL; + g_autoptr(XbSilo) silo = NULL; + XbMarkupHelper helper = { + .cnt = 0, + .str = g_string_new (NULL), + }; + const gchar *xml = "" + "

Title:

" + "

There is a slight risk of death here!

" + "
"; + + /* import from XML */ + silo = xb_silo_new_from_xml (xml, &error); + g_assert_no_error (error); + g_assert_nonnull (silo); + + /* ensure we can round-trip */ + tmp = xb_silo_to_string (silo, &error); + g_assert_no_error (error); + g_assert_nonnull (silo); + g_debug ("\n%s", tmp); + n = xb_silo_get_root (silo); + g_assert_nonnull (n); + new = xb_node_export (n, XB_NODE_EXPORT_FLAG_NONE, &error); + g_assert_no_error (error); + g_assert_nonnull (new); + g_assert_cmpstr (xml, ==, new); + + /* ensure we can convert this to another format */ + ret = xb_node_transmogrify (n, xb_markup_head_cb, xb_markup_tail_cb, &helper); + g_assert_true (ret); + g_assert_cmpstr (helper.str->str, ==, + "`Title`:\n\nThere is a *slight* risk of **death** here!\n\n"); + g_assert_cmpint (helper.cnt, ==, 14); + g_string_free (helper.str, TRUE); +} + +static void +xb_speed_func (void) +{ + XbNode *n; + gboolean ret; + guint n_components = 5000; + g_autofree gchar *tmp_xmlb = g_build_filename (g_get_tmp_dir (), "test.xmlb", NULL); + g_autofree gchar *xpath1 = NULL; + g_autoptr(GError) error = NULL; + g_autoptr(GFile) file = NULL; + g_autoptr(GPtrArray) results = NULL; + g_autoptr(GString) xml = g_string_new (NULL); + g_autoptr(GTimer) timer = g_timer_new (); + g_autoptr(XbSilo) silo = NULL; + +#ifdef __s390x__ + /* this is run with qemu and takes too much time */ + g_test_skip ("s390 too slow, skipping"); + return; +#endif + + /* create a huge document */ + g_string_append (xml, ""); + for (guint i = 0; i < n_components; i++) { + g_string_append (xml, ""); + g_string_append_printf (xml, " %06u.firmware", i); + g_string_append (xml, " ColorHug2"); + g_string_append (xml, " Firmware"); + g_string_append (xml, "

New features!

"); + g_string_append (xml, " "); + g_string_append (xml, " 2082b5e0"); + g_string_append (xml, " "); + g_string_append (xml, " "); + g_string_append (xml, " fwupd"); + g_string_append (xml, " "); + g_string_append (xml, " "); + g_string_append (xml, " http://com/"); + g_string_append (xml, " CC0-1.0"); + g_string_append (xml, " GPL-2.0+"); + g_string_append (xml, " richard"); + g_string_append (xml, " Hughski"); + g_string_append (xml, " "); + g_string_append (xml, " "); + g_string_append (xml, "

stable:

  • Quicker
"); + g_string_append (xml, "
"); + g_string_append (xml, "
"); + g_string_append (xml, "
"); + } + g_string_append (xml, "
"); + + /* import from XML */ + silo = xb_silo_new_from_xml (xml->str, &error); + g_assert_no_error (error); + g_assert_nonnull (silo); + file = g_file_new_for_path (tmp_xmlb); + ret = xb_silo_save_to_file (silo, file, NULL, &error); + g_assert_no_error (error); + g_assert_true (ret); + g_clear_object (&silo); + g_print ("import+save: %.3fms\n", g_timer_elapsed (timer, NULL) * 1000); + g_timer_reset (timer); + + /* load from file */ + silo = xb_silo_new (); + ret = xb_silo_load_from_file (silo, file, XB_SILO_LOAD_FLAG_NONE, NULL, &error); + g_assert_no_error (error); + g_assert_true (ret); + g_print ("mmap load: %.3fms\n", g_timer_elapsed (timer, NULL) * 1000); + g_timer_reset (timer); + + /* query best case */ + n = xb_silo_query_first (silo, "components/component/id[text()='000000.firmware']", &error); + g_assert_no_error (error); + g_assert_nonnull (n); + g_print ("query[first]: %.3fms\n", g_timer_elapsed (timer, NULL) * 1000); + g_timer_reset (timer); + g_clear_object (&n); + + /* query worst case */ + xpath1 = g_strdup_printf ("components/component/id[text()='%06u.firmware']", n_components - 1); + n = xb_silo_query_first (silo, xpath1, &error); + g_assert_no_error (error); + g_assert_nonnull (n); + g_print ("query[last]: %.3fms\n", g_timer_elapsed (timer, NULL) * 1000); + g_timer_reset (timer); + g_clear_object (&n); + + /* query all components */ + results = xb_silo_query (silo, "components/component", 0, &error); + g_assert_no_error (error); + g_assert_nonnull (results); + g_assert_cmpint (results->len, ==, n_components); + g_print ("query[all]: %.3fms\n", g_timer_elapsed (timer, NULL) * 1000); + g_timer_reset (timer); + + /* factorial search */ + for (guint i = 0; i < n_components; i += 20) { + g_autofree gchar *xpath2 = NULL; + xpath2 = g_strdup_printf ("components/component[@type='firmware']/id[text()='%06u.firmware']", i); + n = xb_silo_query_first (silo, xpath2, &error); + g_assert_no_error (error); + g_assert_nonnull (n); + g_clear_object (&n); + } + g_print ("query[x%u]: %.3fms\n", n_components, g_timer_elapsed (timer, NULL) * 1000); + g_timer_reset (timer); + + /* factorial search, again */ + for (guint i = 0; i < n_components; i += 20) { + g_autofree gchar *xpath2 = NULL; + xpath2 = g_strdup_printf ("components/component[@type='firmware']/id[text()='%06u.firmware']", i); + n = xb_silo_query_first (silo, xpath2, &error); + g_assert_no_error (error); + g_assert_nonnull (n); + g_clear_object (&n); + } + g_print ("query[x%u]: %.3fms\n", n_components, g_timer_elapsed (timer, NULL) * 1000); + g_timer_reset (timer); + + /* create an index */ + ret = xb_silo_query_build_index (silo, "components/component/id", NULL, &error); + g_assert_no_error (error); + g_assert_true (ret); + ret = xb_silo_query_build_index (silo, "components/component/id[text()='dave']", NULL, &error); + g_assert_no_error (error); + g_assert_true (ret); + ret = xb_silo_query_build_index (silo, "components/component/DAVE", NULL, &error); + g_assert_no_error (error); + g_assert_true (ret); + ret = xb_silo_query_build_index (silo, "components/component", "type", &error); + g_assert_no_error (error); + g_assert_true (ret); + g_print ("create index: %.3fms\n", g_timer_elapsed (timer, NULL) * 1000); + g_timer_reset (timer); + + /* index not found */ + n = xb_silo_query_first (silo, "components[text()=$'dave']", &error); + g_assert_error (error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT); + g_assert_null (n); + g_clear_error (&error); + + /* do the search again, this time with an index */ + g_timer_reset (timer); + for (guint i = 0; i < n_components; i += 20) { + g_autofree gchar *xpath2 = NULL; + xpath2 = g_strdup_printf ("components/component[attr($'type')=$'firmware']/id[text()=$'%06u.firmware']", i); + n = xb_silo_query_first (silo, xpath2, &error); + g_assert_no_error (error); + g_assert_nonnull (n); + g_clear_object (&n); + } + g_print ("query[x%u]: %.3fms\n", n_components, g_timer_elapsed (timer, NULL) * 1000); +} + +int +main (int argc, char **argv) +{ + g_test_init (&argc, &argv, NULL); + + /* only critical and error are fatal */ + g_log_set_fatal_mask (NULL, G_LOG_LEVEL_ERROR | G_LOG_LEVEL_CRITICAL); + g_setenv ("G_MESSAGES_DEBUG", "all", TRUE); + + /* tests go here */ + g_test_add_func ("/libxmlb/common", xb_common_func); + g_test_add_func ("/libxmlb/common{union}", xb_common_union_func); + g_test_add_func ("/libxmlb/opcodes", xb_predicate_func); + g_test_add_func ("/libxmlb/opcodes{optimize}", xb_predicate_optimize_func); + g_test_add_func ("/libxmlb/opcodes{kind}", xb_opcodes_kind_func); + g_test_add_func ("/libxmlb/stack", xb_stack_func); + g_test_add_func ("/libxmlb/stack{peek}", xb_stack_peek_func); + g_test_add_func ("/libxmlb/node{data}", xb_node_data_func); + g_test_add_func ("/libxmlb/builder", xb_builder_func); + g_test_add_func ("/libxmlb/builder{native-lang}", xb_builder_native_lang_func); + g_test_add_func ("/libxmlb/builder{native-lang-nested}", xb_builder_native_lang2_func); + g_test_add_func ("/libxmlb/builder{native-lang-locale}", xb_builder_native_lang_no_locales_func); + g_test_add_func ("/libxmlb/builder{empty}", xb_builder_empty_func); + g_test_add_func ("/libxmlb/builder{ensure}", xb_builder_ensure_func); + g_test_add_func ("/libxmlb/builder{ensure-watch-source}", xb_builder_ensure_watch_source_func); + g_test_add_func ("/libxmlb/builder{node-vfunc}", xb_builder_node_vfunc_func); + g_test_add_func ("/libxmlb/builder{node-vfunc-remove}", xb_builder_node_vfunc_remove_func); + g_test_add_func ("/libxmlb/builder{node-vfunc-depth}", xb_builder_node_vfunc_depth_func); + g_test_add_func ("/libxmlb/builder{node-vfunc-error}", xb_builder_node_vfunc_error_func); + g_test_add_func ("/libxmlb/builder{ignore-invalid}", xb_builder_ignore_invalid_func); + g_test_add_func ("/libxmlb/builder{custom-mime}", xb_builder_custom_mime_func); + g_test_add_func ("/libxmlb/builder{chained-adapters}", xb_builder_chained_adapters_func); + g_test_add_func ("/libxmlb/builder-node", xb_builder_node_func); + g_test_add_func ("/libxmlb/builder-node{info}", xb_builder_node_info_func); + g_test_add_func ("/libxmlb/builder-node{literal-text}", xb_builder_node_literal_text_func); + g_test_add_func ("/libxmlb/builder-node{source-text}", xb_builder_node_source_text_func); + g_test_add_func ("/libxmlb/markup", xb_markup_func); + g_test_add_func ("/libxmlb/xpath", xb_xpath_func); + g_test_add_func ("/libxmlb/xpath-query", xb_xpath_query_func); + g_test_add_func ("/libxmlb/xpath-query{reverse}", xb_xpath_query_reverse_func); + g_test_add_func ("/libxmlb/xpath{helpers}", xb_xpath_helpers_func); + g_test_add_func ("/libxmlb/xpath{prepared}", xb_xpath_prepared_func); + g_test_add_func ("/libxmlb/xpath{incomplete}", xb_xpath_incomplete_func); + g_test_add_func ("/libxmlb/xpath-parent", xb_xpath_parent_func); + g_test_add_func ("/libxmlb/xpath-glob", xb_xpath_glob_func); + g_test_add_func ("/libxmlb/xpath-node", xb_xpath_node_func); + g_test_add_func ("/libxmlb/xpath-parent-subnode", xb_xpath_parent_subnode_func); + g_test_add_func ("/libxmlb/multiple-roots", xb_builder_multiple_roots_func); + if (g_test_perf ()) + g_test_add_func ("/libxmlb/threading", xb_threading_func); + if (g_test_perf ()) + g_test_add_func ("/libxmlb/speed", xb_speed_func); + return g_test_run (); +} diff --git a/src/xb-silo-export-private.h b/src/xb-silo-export-private.h new file mode 100644 index 0000000..13c3896 --- /dev/null +++ b/src/xb-silo-export-private.h @@ -0,0 +1,18 @@ +/* + * Copyright (C) 2018 Richard Hughes + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#pragma once + +#include "xb-silo-export.h" + +G_BEGIN_DECLS + +GString *xb_silo_export_with_root (XbSilo *self, + XbNode *root, + XbNodeExportFlags flags, + GError **error); + +G_END_DECLS diff --git a/src/xb-silo-export.c b/src/xb-silo-export.c new file mode 100644 index 0000000..80e341f --- /dev/null +++ b/src/xb-silo-export.c @@ -0,0 +1,219 @@ +/* + * Copyright (C) 2018 Richard Hughes + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#define G_LOG_DOMAIN "XbSilo" + +#include "config.h" + +#include + +#include "xb-node-private.h" +#include "xb-silo-export-private.h" +#include "xb-silo-private.h" +#include "xb-string-private.h" + +typedef struct { + GString *xml; + XbNodeExportFlags flags; + guint32 off; + guint level; +} XbSiloExportHelper; + +static gboolean +xb_silo_export_node (XbSilo *self, XbSiloExportHelper *helper, XbSiloNode *sn, GError **error) +{ + XbSiloNode *sn2; + + helper->off = xb_silo_get_offset_for_node (self, sn); + + /* add start of opening tag */ + if (helper->flags & XB_NODE_EXPORT_FLAG_FORMAT_INDENT) { + for (guint i = 0; i < helper->level; i++) + g_string_append (helper->xml, " "); + } + g_string_append_printf (helper->xml, "<%s", + xb_silo_from_strtab (self, sn->element_name)); + + /* add any attributes */ + for (guint8 i = 0; i < sn->nr_attrs; i++) { + XbSiloAttr *a = xb_silo_get_attr (self, helper->off, i); + g_autofree gchar *key = xb_string_xml_escape (xb_silo_from_strtab (self, a->attr_name)); + g_autofree gchar *val = xb_string_xml_escape (xb_silo_from_strtab (self, a->attr_value)); + g_string_append_printf (helper->xml, " %s=\"%s\"", key, val); + } + + /* finish the opening tag and add any text if it exists */ + if (sn->text != XB_SILO_UNSET) { + g_autofree gchar *text = xb_string_xml_escape (xb_silo_from_strtab (self, sn->text)); + g_string_append (helper->xml, ">"); + g_string_append (helper->xml, text); + } else { + g_string_append (helper->xml, ">"); + if (helper->flags & XB_NODE_EXPORT_FLAG_FORMAT_MULTILINE) + g_string_append (helper->xml, "\n"); + } + helper->off += xb_silo_node_get_size (sn); + + /* recurse deeper */ + while (xb_silo_get_node(self, helper->off)->is_node) { + XbSiloNode *child = xb_silo_get_node (self, helper->off); + helper->level++; + if (!xb_silo_export_node (self, helper, child, error)) + return FALSE; + helper->level--; + } + + /* check for the single byte sentinel */ + sn2 = xb_silo_get_node (self, helper->off); + if (sn2->is_node) { + g_set_error (error, + G_IO_ERROR, + G_IO_ERROR_INVALID_DATA, + "no seninel at %" G_GUINT32_FORMAT, + helper->off); + return FALSE; + } + helper->off += xb_silo_node_get_size (sn2); + + /* add closing tag */ + if ((helper->flags & XB_NODE_EXPORT_FLAG_FORMAT_INDENT) > 0 && + sn->text == XB_SILO_UNSET) { + for (guint i = 0; i < helper->level; i++) + g_string_append (helper->xml, " "); + } + g_string_append_printf (helper->xml, "", + xb_silo_from_strtab (self, sn->element_name)); + + /* add any optional tail */ + if (sn->tail != XB_SILO_UNSET) { + g_autofree gchar *tail = xb_string_xml_escape (xb_silo_from_strtab (self, sn->tail)); + g_string_append (helper->xml, tail); + } + + if (helper->flags & XB_NODE_EXPORT_FLAG_FORMAT_MULTILINE) + g_string_append (helper->xml, "\n"); + + return TRUE; +} + +/* private */ +GString * +xb_silo_export_with_root (XbSilo *self, XbNode *root, XbNodeExportFlags flags, GError **error) +{ + XbSiloNode *sn; + XbSiloExportHelper helper = { + .flags = flags, + .level = 0, + .off = sizeof(XbSiloHeader), + }; + + g_return_val_if_fail (XB_IS_SILO (self), NULL); + + /* this implies the other */ + if (flags & XB_NODE_EXPORT_FLAG_ONLY_CHILDREN) + flags |= XB_NODE_EXPORT_FLAG_INCLUDE_SIBLINGS; + + /* optional subtree export */ + if (root != NULL) { + sn = xb_node_get_sn (root); + if (sn != NULL && flags & XB_NODE_EXPORT_FLAG_ONLY_CHILDREN) + sn = xb_silo_node_get_child (self, sn); + } else { + sn = xb_silo_get_sroot (self); + } + + /* no root */ + if (sn == NULL) { + g_set_error_literal (error, + G_IO_ERROR, + G_IO_ERROR_NOT_FOUND, + "no data to export"); + return NULL; + } + + /* root node */ + helper.xml = g_string_new (NULL); + if ((flags & XB_NODE_EXPORT_FLAG_ADD_HEADER) > 0) + g_string_append (helper.xml, "\n"); + do { + if (!xb_silo_export_node (self, &helper, sn, error)) { + g_string_free (helper.xml, TRUE); + return NULL; + } + if ((flags & XB_NODE_EXPORT_FLAG_INCLUDE_SIBLINGS) == 0) + break; + sn = xb_silo_node_get_next (self, sn); + } while (sn != NULL); + + /* success */ + return helper.xml; +} + +/** + * xb_silo_export: + * @self: a #XbSilo + * @flags: some #XbNodeExportFlags, e.g. #XB_NODE_EXPORT_FLAG_NONE + * @error: the #GError, or %NULL + * + * Exports the silo back to XML. + * + * Returns: XML data, or %NULL for an error + * + * Since: 0.1.0 + **/ +gchar * +xb_silo_export (XbSilo *self, XbNodeExportFlags flags, GError **error) +{ + GString *xml; + g_return_val_if_fail (XB_IS_SILO (self), NULL); + g_return_val_if_fail (error == NULL || *error == NULL, NULL); + xml = xb_silo_export_with_root (self, NULL, flags, error); + if (xml == NULL) + return NULL; + return g_string_free (xml, FALSE); +} + +/** + * xb_silo_export_file: + * @self: a #XbSilo + * @file: a #GFile + * @flags: some #XbNodeExportFlags, e.g. #XB_NODE_EXPORT_FLAG_NONE + * @cancellable: a #GCancellable, or %NULL + * @error: the #GError, or %NULL + * + * Exports the silo back to an XML file. + * + * Returns: %TRUE on success + * + * Since: 0.1.2 + **/ +gboolean +xb_silo_export_file (XbSilo *self, + GFile *file, + XbNodeExportFlags flags, + GCancellable *cancellable, + GError **error) +{ + g_autoptr(GString) xml = NULL; + + g_return_val_if_fail (XB_IS_SILO (self), FALSE); + g_return_val_if_fail (G_IS_FILE (file), FALSE); + g_return_val_if_fail (cancellable == NULL || G_IS_CANCELLABLE (cancellable), FALSE); + g_return_val_if_fail (error == NULL || *error == NULL, FALSE); + + xml = xb_silo_export_with_root (self, NULL, flags, error); + if (xml == NULL) + return FALSE; + return g_file_replace_contents (file, + xml->str, + xml->len, + NULL, /* etag */ + FALSE, /* make-backup */ + G_FILE_CREATE_NONE, + NULL, /* new etag */ + cancellable, + error); +} diff --git a/src/xb-silo-export.h b/src/xb-silo-export.h new file mode 100644 index 0000000..ae60656 --- /dev/null +++ b/src/xb-silo-export.h @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2018 Richard Hughes + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#pragma once + +#include + +#include "xb-node.h" +#include "xb-silo.h" + +G_BEGIN_DECLS + +gchar *xb_silo_export (XbSilo *self, + XbNodeExportFlags flags, + GError **error); +gboolean xb_silo_export_file (XbSilo *self, + GFile *file, + XbNodeExportFlags flags, + GCancellable *cancellable, + GError **error); + +G_END_DECLS diff --git a/src/xb-silo-private.h b/src/xb-silo-private.h new file mode 100644 index 0000000..c64e82e --- /dev/null +++ b/src/xb-silo-private.h @@ -0,0 +1,98 @@ +/* + * Copyright (C) 2018 Richard Hughes + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#pragma once + +#include "xb-machine.h" +#include "xb-node.h" +#include "xb-silo.h" + +#include "xb-string-private.h" + +G_BEGIN_DECLS + +/* 32 bytes, native byte order */ +typedef struct __attribute__ ((packed)) { + guint32 magic; + guint32 version; + XbGuid guid; + guint16 strtab_ntags; + guint8 padding[2]; + guint32 strtab; +} XbSiloHeader; + +#define XB_SILO_MAGIC_BYTES 0x624c4d58 +#define XB_SILO_VERSION 0x00000007 +#define XB_SILO_UNSET 0xffffffff + +typedef struct __attribute__ ((packed)) { + guint8 is_node:1; + guint8 nr_attrs:7; + guint32 element_name; /* ONLY when is_node: from strtab */ + guint32 parent; /* ONLY when is_node: from 0 */ + guint32 next; /* ONLY when is_node: from 0 */ + guint32 text; /* ONLY when is_node: from strtab */ + guint32 tail; /* ONLY when is_node: from strtab */ +} XbSiloNode; + +typedef struct __attribute__ ((packed)) { + guint32 attr_name; /* from strtab */ + guint32 attr_value; /* from strtab */ +} XbSiloAttr; + +typedef struct { + /*< private >*/ + XbSiloNode *sn; + guint position; +} XbSiloQueryData; + +const gchar *xb_silo_from_strtab (XbSilo *self, + guint32 offset); +void xb_silo_strtab_index_insert (XbSilo *self, + guint32 offset); +guint32 xb_silo_strtab_index_lookup (XbSilo *self, + const gchar *str); +XbSiloNode *xb_silo_get_node (XbSilo *self, + guint32 off); +XbSiloAttr *xb_silo_get_attr (XbSilo *self, + guint32 off, + guint8 idx); +XbMachine *xb_silo_get_machine (XbSilo *self); +guint32 xb_silo_get_strtab (XbSilo *self); +guint32 xb_silo_get_strtab_idx (XbSilo *self, + const gchar *element); +guint32 xb_silo_get_offset_for_node (XbSilo *self, + XbSiloNode *n); +guint8 xb_silo_node_get_size (XbSiloNode *n); +XbSiloNode *xb_silo_get_sroot (XbSilo *self); +XbSiloNode *xb_silo_node_get_parent (XbSilo *self, + XbSiloNode *n); +XbSiloNode *xb_silo_node_get_next (XbSilo *self, + XbSiloNode *n); +XbSiloNode *xb_silo_node_get_child (XbSilo *self, + XbSiloNode *n); +const gchar *xb_silo_node_get_element (XbSilo *self, + XbSiloNode *n); +const gchar *xb_silo_node_get_text (XbSilo *self, + XbSiloNode *n); +const gchar *xb_silo_node_get_tail (XbSilo *self, + XbSiloNode *n); +XbSiloAttr *xb_silo_node_get_attr_by_str (XbSilo *self, + XbSiloNode *n, + const gchar *name); +guint xb_silo_node_get_depth (XbSilo *self, + XbSiloNode *n); +XbNode *xb_silo_node_create (XbSilo *self, + XbSiloNode *sn); +void xb_silo_add_profile (XbSilo *self, + GTimer *timer, + const gchar *fmt, + ...) G_GNUC_PRINTF (3, 4); +gboolean xb_silo_is_empty (XbSilo *self); +void xb_silo_uninvalidate (XbSilo *self); +XbSiloProfileFlags xb_silo_get_profile_flags (XbSilo *self); + +G_END_DECLS diff --git a/src/xb-silo-query-private.h b/src/xb-silo-query-private.h new file mode 100644 index 0000000..acc961f --- /dev/null +++ b/src/xb-silo-query-private.h @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2018 Richard Hughes + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#pragma once + +#include + +#include "xb-silo-query.h" +#include "xb-query.h" + +G_BEGIN_DECLS + +GPtrArray *xb_silo_query_with_root (XbSilo *self, + XbNode *n, + const gchar *xpath, + guint limit, + GError **error); +GPtrArray *xb_silo_query_with_root_full (XbSilo *self, + XbNode *n, + XbQuery *query, + GError **error); + +G_END_DECLS diff --git a/src/xb-silo-query.c b/src/xb-silo-query.c new file mode 100644 index 0000000..8a97212 --- /dev/null +++ b/src/xb-silo-query.c @@ -0,0 +1,547 @@ +/* + * Copyright (C) 2018 Richard Hughes + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#define G_LOG_DOMAIN "XbSilo" + +#include "config.h" + +#include +#include + +#include "xb-node-private.h" +#include "xb-opcode.h" +#include "xb-silo-private.h" +#include "xb-silo-query-private.h" +#include "xb-stack-private.h" +#include "xb-query-private.h" + +static gboolean +xb_silo_query_node_matches (XbSilo *self, + XbMachine *machine, + XbSiloNode *sn, + XbQuerySection *section, + XbSiloQueryData *query_data, + gboolean *result, + GError **error) +{ + /* we have an index into the string table */ + if (section->element_idx != sn->element_name && + section->kind != XB_SILO_QUERY_KIND_WILDCARD) { + *result = FALSE; + return TRUE; + } + + /* for section */ + query_data->position += 1; + + /* check predicates */ + if (section->predicates != NULL) { + for (guint i = 0; i < section->predicates->len; i++) { + XbStack *opcodes = g_ptr_array_index (section->predicates, i); + if (!xb_machine_run (machine, opcodes, result, query_data, error)) + return FALSE; + } + } + + /* success */ + return TRUE; +} + +typedef struct { + GPtrArray *sections; /* of XbQuerySection */ + GPtrArray *results; /* of XbNode */ + GHashTable *results_hash; /* of sn:1 */ + guint limit; + XbSiloQueryData *query_data; +} XbSiloQueryHelper; + +static gboolean +xb_silo_query_section_add_result (XbSilo *self, XbSiloQueryHelper *helper, XbSiloNode *sn) +{ + if (g_hash_table_lookup (helper->results_hash, sn) != NULL) + return FALSE; + g_ptr_array_add (helper->results, xb_silo_node_create (self, sn)); + g_hash_table_add (helper->results_hash, sn); + return helper->results->len == helper->limit; +} + +/* + * @parent: (allow-none) + */ +static gboolean +xb_silo_query_section_root (XbSilo *self, + XbSiloNode *sn, + guint i, + XbSiloQueryHelper *helper, + GError **error) +{ + XbMachine *machine = xb_silo_get_machine (self); + XbSiloQueryData *query_data = helper->query_data; + XbQuerySection *section = g_ptr_array_index (helper->sections, i); + + /* handle parent */ + if (section->kind == XB_SILO_QUERY_KIND_PARENT) { + XbSiloNode *parent; + if (sn == NULL) { + g_set_error_literal (error, + G_IO_ERROR, + G_IO_ERROR_INVALID_ARGUMENT, + "cannot obtain parent for root"); + return FALSE; + } + parent = xb_silo_node_get_parent (self, sn); + if (parent == NULL) { + g_set_error (error, + G_IO_ERROR, + G_IO_ERROR_INVALID_ARGUMENT, + "no parent set for %s", + xb_silo_node_get_element (self, sn)); + return FALSE; + } + if (i == helper->sections->len - 1) { + xb_silo_query_section_add_result (self, helper, parent); + return TRUE; + } +// g_debug ("PARENT @%u", +// xb_silo_get_offset_for_node (self, parent)); + return xb_silo_query_section_root (self, parent, i + 1, helper, error); + } + + /* no node means root */ + if (sn == NULL) { + sn = xb_silo_get_sroot (self); + if (sn == NULL) { + g_set_error_literal (error, + G_IO_ERROR, + G_IO_ERROR_NOT_FOUND, + "silo root not found"); + return FALSE; + } + } else { + sn = xb_silo_node_get_child (self, sn); + if (sn == NULL) + return TRUE; + } + + /* set up level pointer */ + query_data->position = 0; + + /* continue matching children ".." */ + do { + gboolean result = TRUE; + query_data->sn = sn; + if (!xb_silo_query_node_matches (self, machine, sn, section, + query_data, &result, error)) + return FALSE; + if (result) { + if (i == helper->sections->len - 1) { +// g_debug ("add result %u", +// xb_silo_get_offset_for_node (self, sn)); + if (xb_silo_query_section_add_result (self, helper, sn)) + break; + } else { +// g_debug ("MATCH %s at @%u, deeper", +// xb_silo_node_get_element (self, sn), +// xb_silo_get_offset_for_node (self, sn)); + if (!xb_silo_query_section_root (self, sn, i + 1, helper, error)) + return FALSE; + if (helper->results->len > 0 && + helper->results->len == helper->limit) + break; + } + } + if (sn->next == 0x0) + break; + sn = xb_silo_get_node (self, sn->next); + } while (TRUE); + return TRUE; +} + +static gboolean +xb_silo_query_part (XbSilo *self, + XbSiloNode *sroot, + GPtrArray *results, + GHashTable *results_hash, + XbQuery *query, + XbSiloQueryData *query_data, + GError **error) +{ + XbSiloQueryHelper helper = { + .results = results, + .limit = xb_query_get_limit (query), + .results_hash = results_hash, + .query_data = query_data, + }; + + /* find each section */ + helper.sections = xb_query_get_sections (query); + return xb_silo_query_section_root (self, sroot, 0, &helper, error); +} + +/** + * xb_silo_query_with_root: (skip) + * @self: a #XbSilo + * @n: (allow-none): a #XbNode + * @xpath: an XPath, e.g. `/components/component[@type=desktop]/id[abe.desktop]` + * @limit: maximum number of results to return, or 0 for "all" + * @error: the #GError, or %NULL + * + * Searches the silo using an XPath query, returning up to @limit results. + * + * It is safe to call this function from a different thread to the one that + * created the #XbSilo. + * + * Please note: Only a subset of XPath is supported. + * + * Returns: (transfer container) (element-type XbNode): results, or %NULL if unfound + * + * Since: 0.1.0 + **/ +GPtrArray * +xb_silo_query_with_root (XbSilo *self, XbNode *n, const gchar *xpath, guint limit, GError **error) +{ + XbSiloNode *sn = NULL; + g_auto(GStrv) split = NULL; + g_autoptr(GHashTable) results_hash = g_hash_table_new (g_direct_hash, g_direct_equal); + g_autoptr(GPtrArray) results = g_ptr_array_new_with_free_func ((GDestroyNotify) g_object_unref); + g_autoptr(GTimer) timer = g_timer_new (); + XbSiloQueryData query_data = { + .sn = NULL, + .position = 0, + }; + + g_return_val_if_fail (XB_IS_SILO (self), NULL); + g_return_val_if_fail (xpath != NULL, NULL); + g_return_val_if_fail (error == NULL || *error == NULL, NULL); + + /* empty silo */ + if (xb_silo_is_empty (self)) { + g_set_error_literal (error, + G_IO_ERROR, + G_IO_ERROR_NOT_FOUND, + "silo has no data"); + return NULL; + } + + /* subtree query */ + if (n != NULL) { + sn = xb_node_get_sn (n); + if (xpath[0] == '/') { + g_set_error_literal (error, + G_IO_ERROR, + G_IO_ERROR_NOT_SUPPORTED, + "XPath node query not supported"); + return NULL; + } + } else { + /* assume it's just a root query */ + if (xpath[0] == '/') + xpath++; + } + + /* do 'or' searches */ + split = g_strsplit (xpath, "|", -1); + for (guint i = 0; split[i] != NULL; i++) { + g_autoptr(GError) error_local = NULL; + g_autoptr(XbQuery) query = xb_query_new (self, split[i], &error_local); + if (query == NULL) { + if (g_error_matches (error_local, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT) && + (split[i + 1] != NULL || results->len > 0)) { + g_debug ("ignoring for OR statement: %s", + error_local->message); + continue; + } + g_propagate_prefixed_error (error, + g_steal_pointer (&error_local), + "failed to process %s: ", + xpath); + return NULL; + } + xb_query_set_limit (query, limit); + if (!xb_silo_query_part (self, sn, + results, results_hash, + query, &query_data, + error)) { + return NULL; + } + } + + /* profile */ + if (xb_silo_get_profile_flags (self) & XB_SILO_PROFILE_FLAG_XPATH) { + xb_silo_add_profile (self, timer, + "query on %s with `%s` limit=%u -> %u results", + n != NULL ? xb_node_get_element (n) : "/", + xpath, limit, results->len); + } + + /* nothing found */ + if (results->len == 0) { + g_set_error (error, + G_IO_ERROR, + G_IO_ERROR_NOT_FOUND, + "no results for XPath query '%s'", + xpath); + return NULL; + } + return g_steal_pointer (&results); +} + +static void +_g_ptr_array_reverse (GPtrArray *array) +{ + guint last_idx = array->len - 1; + for (guint i = 0; i < array->len / 2; i++) { + gpointer tmp = array->pdata[i]; + array->pdata[i] = array->pdata[last_idx - i]; + array->pdata[last_idx - i] = tmp; + } +} + +/** + * xb_silo_query_with_root_full: (skip) + * @self: a #XbSilo + * @n: (allow-none): a #XbNode + * @query: an #XbQuery + * @error: the #GError, or %NULL + * + * Searches the silo using an XPath query, returning up to @limit results. + * + * It is safe to call this function from a different thread to the one that + * created the #XbSilo. + * + * Please note: Only a subset of XPath is supported. + * + * Returns: (transfer container) (element-type XbNode): results, or %NULL if unfound + * + * Since: 0.1.4 + **/ +GPtrArray * +xb_silo_query_with_root_full (XbSilo *self, XbNode *n, XbQuery *query, GError **error) +{ + XbSiloNode *sn = NULL; + g_autoptr(GHashTable) results_hash = g_hash_table_new (g_direct_hash, g_direct_equal); + g_autoptr(GPtrArray) results = g_ptr_array_new_with_free_func ((GDestroyNotify) g_object_unref); + g_autoptr(GTimer) timer = g_timer_new (); + XbSiloQueryData query_data = { + .sn = NULL, + .position = 0, + }; + + /* empty silo */ + if (xb_silo_is_empty (self)) { + g_set_error_literal (error, + G_IO_ERROR, + G_IO_ERROR_NOT_FOUND, + "silo has no data"); + return NULL; + } + + /* subtree query */ + if (n != NULL) + sn = xb_node_get_sn (n); + + /* only one query allowed */ + if (!xb_silo_query_part (self, sn, results, results_hash, + query, &query_data, error)) + return NULL; + + /* profile */ + if (xb_silo_get_profile_flags (self) & XB_SILO_PROFILE_FLAG_XPATH) { + g_autofree gchar *tmp = xb_query_to_string (query); + xb_silo_add_profile (self, timer, + "query on %s with `%s` limit=%u -> %u results", + n != NULL ? xb_node_get_element (n) : "/", + tmp, + xb_query_get_limit (query), + results->len); + } + + /* nothing found */ + if (results->len == 0) { + g_autofree gchar *tmp = xb_query_to_string (query); + g_set_error (error, + G_IO_ERROR, + G_IO_ERROR_NOT_FOUND, + "no results for XPath query '%s'", + tmp); + return NULL; + } + + /* reverse order */ + if (xb_query_get_flags (query) & XB_QUERY_FLAG_REVERSE) + _g_ptr_array_reverse (results); + + return g_steal_pointer (&results); +} + +/** + * xb_silo_query_full: + * @self: a #XbSilo + * @query: an #XbQuery + * @error: the #GError, or %NULL + * + * Searches the silo using an XPath query. + * + * It is safe to call this function from a different thread to the one that + * created the #XbSilo. + * + * Please note: Only a subset of XPath is supported. + * + * Returns: (transfer container) (element-type XbNode): results, or %NULL if unfound + * + * Since: 0.1.13 + **/ +GPtrArray * +xb_silo_query_full (XbSilo *self, XbQuery *query, GError **error) +{ + g_return_val_if_fail (XB_IS_SILO (self), NULL); + g_return_val_if_fail (XB_IS_QUERY (query), NULL); + g_return_val_if_fail (error == NULL || *error == NULL, NULL); + return xb_silo_query_with_root_full (self, NULL, query, error); +} + +/** + * xb_silo_query_first_full: + * @self: a #XbSilo + * @query: an #XbQuery + * @error: the #GError, or %NULL + * + * Searches the silo using an XPath query, returning up to one result. + * + * It is safe to call this function from a different thread to the one that + * created the #XbSilo. + * + * Please note: Only a tiny subset of XPath 1.0 is supported. + * + * Returns: (transfer none): a #XbNode, or %NULL if unfound + * + * Since: 0.1.13 + **/ +XbNode * +xb_silo_query_first_full (XbSilo *self, XbQuery *query, GError **error) +{ + g_autoptr(GPtrArray) results = NULL; + results = xb_silo_query_full (self, query, error); + if (results == NULL) + return NULL; + return g_object_ref (g_ptr_array_index (results, 0)); +} + +/** + * xb_silo_query: + * @self: a #XbSilo + * @xpath: an XPath, e.g. `/components/component[@type=desktop]/id[abe.desktop]` + * @limit: maximum number of results to return, or 0 for "all" + * @error: the #GError, or %NULL + * + * Searches the silo using an XPath query, returning up to @limit results. + * + * It is safe to call this function from a different thread to the one that + * created the #XbSilo. + * + * Please note: Only a subset of XPath is supported. + * + * Returns: (transfer container) (element-type XbNode): results, or %NULL if unfound + * + * Since: 0.1.0 + **/ +GPtrArray * +xb_silo_query (XbSilo *self, const gchar *xpath, guint limit, GError **error) +{ + g_return_val_if_fail (XB_IS_SILO (self), NULL); + g_return_val_if_fail (xpath != NULL, NULL); + g_return_val_if_fail (error == NULL || *error == NULL, NULL); + return xb_silo_query_with_root (self, NULL, xpath, limit, error); +} + +/** + * xb_silo_query_first: + * @self: a #XbSilo + * @xpath: An XPath, e.g. `/components/component[@type=desktop]/id[abe.desktop]` + * @error: the #GError, or %NULL + * + * Searches the silo using an XPath query, returning up to one result. + * + * It is safe to call this function from a different thread to the one that + * created the #XbSilo. + * + * Please note: Only a tiny subset of XPath 1.0 is supported. + * + * Returns: (transfer none): a #XbNode, or %NULL if unfound + * + * Since: 0.1.0 + **/ +XbNode * +xb_silo_query_first (XbSilo *self, const gchar *xpath, GError **error) +{ + g_autoptr(GPtrArray) results = NULL; + results = xb_silo_query_with_root (self, NULL, xpath, 1, error); + if (results == NULL) + return NULL; + return g_object_ref (g_ptr_array_index (results, 0)); +} + +/** + * xb_silo_query_build_index: + * @self: a #XbSilo + * @xpath: An XPath, e.g. `/components/component[@type=desktop]/id[abe.desktop]` + * @attr: (nullable): Attribute name, e.g. `type`, or NULL + * @error: the #GError, or %NULL + * + * Adds the `attr()` or `text()` results of a query to the index. + * + * Returns: %TRUE for success + * + * Since: 0.1.4 + **/ +gboolean +xb_silo_query_build_index (XbSilo *self, + const gchar *xpath, + const gchar *attr, + GError **error) +{ + g_autoptr(GError) error_local = NULL; + g_autoptr(GPtrArray) array = NULL; + + g_return_val_if_fail (XB_IS_SILO (self), FALSE); + g_return_val_if_fail (xpath != NULL, FALSE); + g_return_val_if_fail (error == NULL || *error == NULL, FALSE); + + /* do the query */ + array = xb_silo_query_with_root (self, NULL, xpath, 0, &error_local); + if (array == NULL) { + if (g_error_matches (error_local, + G_IO_ERROR, + G_IO_ERROR_INVALID_ARGUMENT) || + g_error_matches (error_local, + G_IO_ERROR, + G_IO_ERROR_NOT_FOUND)) { + g_debug ("ignoring index: %s", error_local->message); + return TRUE; + } + g_propagate_error (error, g_steal_pointer (&error_local)); + return FALSE; + } + + /* add each attribute name AND value */ + for (guint i = 0; i < array->len; i++) { + XbNode *n = g_ptr_array_index (array, i); + XbSiloNode *sn = xb_node_get_sn (n); + if (attr != NULL) { + guint32 off = xb_silo_get_offset_for_node (self, sn); + for (guint8 j = 0; j < sn->nr_attrs; j++) { + XbSiloAttr *a = xb_silo_get_attr (self, off, j); + xb_silo_strtab_index_insert (self, a->attr_name); + xb_silo_strtab_index_insert (self, a->attr_value); + } + } else { + xb_silo_strtab_index_insert (self, sn->text); + } + } + + /* success */ + return TRUE; +} diff --git a/src/xb-silo-query.h b/src/xb-silo-query.h new file mode 100644 index 0000000..00058e5 --- /dev/null +++ b/src/xb-silo-query.h @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2018 Richard Hughes + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#pragma once + +#include + +#include "xb-node.h" +#include "xb-query.h" +#include "xb-silo.h" + +G_BEGIN_DECLS + +GPtrArray *xb_silo_query (XbSilo *self, + const gchar *xpath, + guint limit, + GError **error); +GPtrArray *xb_silo_query_full (XbSilo *self, + XbQuery *query, + GError **error); +XbNode *xb_silo_query_first (XbSilo *self, + const gchar *xpath, + GError **error); +XbNode *xb_silo_query_first_full (XbSilo *self, + XbQuery *query, + GError **error); +gboolean xb_silo_query_build_index (XbSilo *self, + const gchar *xpath, + const gchar *attr, + GError **error); + +G_END_DECLS diff --git a/src/xb-silo.c b/src/xb-silo.c new file mode 100644 index 0000000..fb245e3 --- /dev/null +++ b/src/xb-silo.c @@ -0,0 +1,1370 @@ +/* + * Copyright (C) 2018 Richard Hughes + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#define G_LOG_DOMAIN "XbSilo" + +#include "config.h" + +#include +#include +#include + +#ifdef HAVE_LIBSTEMMER +#include +#endif + +#include "xb-builder.h" +#include "xb-node-private.h" +#include "xb-opcode-private.h" +#include "xb-silo-private.h" +#include "xb-stack-private.h" +#include "xb-string-private.h" + +typedef struct { + GObject parent_instance; + GMappedFile *mmap; + gchar *guid; + gboolean valid; + GBytes *blob; + const guint8 *data; /* pointers into ->blob */ + guint32 datasz; + guint32 strtab; + GHashTable *strtab_tags; + GHashTable *strindex; + GHashTable *nodes; + GMutex nodes_mutex; + GHashTable *file_monitors; /* of fn:XbSiloFileMonitorItem */ + XbMachine *machine; + XbSiloProfileFlags profile_flags; + GString *profile_str; +#ifdef HAVE_LIBSTEMMER + struct sb_stemmer *stemmer_ctx; /* lazy loaded */ + GMutex stemmer_mutex; +#endif +} XbSiloPrivate; + +typedef struct { + GFileMonitor *file_monitor; + gulong file_monitor_id; +} XbSiloFileMonitorItem; + +G_DEFINE_TYPE_WITH_PRIVATE (XbSilo, xb_silo, G_TYPE_OBJECT) +#define GET_PRIVATE(o) (xb_silo_get_instance_private (o)) + +enum { + PROP_0, + PROP_GUID, + PROP_VALID, + PROP_LAST +}; + +/* private */ +void +xb_silo_add_profile (XbSilo *self, GTimer *timer, const gchar *fmt, ...) +{ + XbSiloPrivate *priv = GET_PRIVATE (self); + va_list args; + g_autoptr(GString) str = g_string_new (NULL); + + /* nothing to do */ + if (!priv->profile_flags) + return; + + /* add duration */ + if (timer != NULL) { + g_string_append_printf (str, "%.2fms", + g_timer_elapsed (timer, NULL) * 1000); + for (guint i = str->len; i < 12; i++) + g_string_append (str, " "); + } + + /* add varargs */ + va_start (args, fmt); + g_string_append_vprintf (str, fmt, args); + va_end (args); + + /* do the right thing */ + if (priv->profile_flags & XB_SILO_PROFILE_FLAG_DEBUG) + g_debug ("%s", str->str); + if (priv->profile_flags & XB_SILO_PROFILE_FLAG_APPEND) + g_string_append_printf (priv->profile_str, "%s\n", str->str); + + /* reset automatically */ + if (timer != NULL) + g_timer_reset (timer); +} + +/* private */ +static gchar * +xb_silo_stem (XbSilo *self, const gchar *value) +{ +#ifdef HAVE_LIBSTEMMER + XbSiloPrivate *priv = GET_PRIVATE (self); + const gchar *tmp; + gsize len_dst; + gsize len_src; + g_autofree gchar *value_casefold = NULL; + g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&priv->stemmer_mutex); + + /* not enabled */ + value_casefold = g_utf8_casefold (value, -1); + if (priv->stemmer_ctx == NULL) + priv->stemmer_ctx = sb_stemmer_new ("en", NULL); + + /* stem */ + len_src = strlen (value_casefold); + tmp = (const gchar *) sb_stemmer_stem (priv->stemmer_ctx, + (guchar *) value_casefold, + (gint) len_src); + len_dst = (gsize) sb_stemmer_length (priv->stemmer_ctx); + if (len_src == len_dst) + return g_steal_pointer (&value_casefold); + return g_strndup (tmp, len_dst); +#else + return g_utf8_casefold (value, -1); +#endif +} + +/* private */ +const gchar * +xb_silo_from_strtab (XbSilo *self, guint32 offset) +{ + XbSiloPrivate *priv = GET_PRIVATE (self); + if (offset == XB_SILO_UNSET) + return NULL; + if (offset >= priv->datasz - priv->strtab) { + g_critical ("strtab+offset is outside the data range for %u", offset); + return NULL; + } + return (const gchar *) (priv->data + priv->strtab + offset); +} + +/* private */ +void +xb_silo_strtab_index_insert (XbSilo *self, guint32 offset) +{ + XbSiloPrivate *priv = GET_PRIVATE (self); + const gchar *tmp; + + /* get the string version */ + tmp = xb_silo_from_strtab (self, offset); + if (tmp == NULL) + return; + if (g_hash_table_lookup (priv->strindex, tmp) != NULL) + return; + g_hash_table_insert (priv->strindex, + (gpointer) tmp, + GUINT_TO_POINTER (offset)); +} + +/* private */ +guint32 +xb_silo_strtab_index_lookup (XbSilo *self, const gchar *str) +{ + XbSiloPrivate *priv = GET_PRIVATE (self); + gpointer val = NULL; + if (!g_hash_table_lookup_extended (priv->strindex, str, NULL, &val)) + return XB_SILO_UNSET; + return GPOINTER_TO_INT (val); +} + +/* private */ +inline XbSiloNode * +xb_silo_get_node (XbSilo *self, guint32 off) +{ + XbSiloPrivate *priv = GET_PRIVATE (self); + return (XbSiloNode *) (priv->data + off); +} + +/* private */ +XbSiloAttr * +xb_silo_get_attr (XbSilo *self, guint32 off, guint8 idx) +{ + XbSiloPrivate *priv = GET_PRIVATE (self); + off += sizeof(XbSiloNode); + off += sizeof(XbSiloAttr) * idx; + return (XbSiloAttr *) (priv->data + off); +} + +/* private */ +guint8 +xb_silo_node_get_size (XbSiloNode *n) +{ + if (n->is_node) { + guint8 sz = sizeof(XbSiloNode); + sz += n->nr_attrs * sizeof(XbSiloAttr); + return sz; + } + /* sentinel */ + return 1; +} + +/* private */ +guint32 +xb_silo_get_offset_for_node (XbSilo *self, XbSiloNode *n) +{ + XbSiloPrivate *priv = GET_PRIVATE (self); + return ((const guint8 *) n) - priv->data; +} + +/* private */ +guint32 +xb_silo_get_strtab (XbSilo *self) +{ + XbSiloPrivate *priv = GET_PRIVATE (self); + return priv->strtab; +} + +/* private */ +XbSiloNode * +xb_silo_get_sroot (XbSilo *self) +{ + XbSiloPrivate *priv = GET_PRIVATE (self); + if (priv->blob == NULL) + return NULL; + if (g_bytes_get_size (priv->blob) <= sizeof(XbSiloHeader)) + return NULL; + return xb_silo_get_node (self, sizeof(XbSiloHeader)); +} + +/* private */ +XbSiloNode * +xb_silo_node_get_parent (XbSilo *self, XbSiloNode *n) +{ + if (n->parent == 0x0) + return NULL; + return xb_silo_get_node (self, n->parent); +} + +/* private */ +XbSiloNode * +xb_silo_node_get_next (XbSilo *self, XbSiloNode *n) +{ + if (n->next == 0x0) + return NULL; + return xb_silo_get_node (self, n->next); +} + +/* private */ +XbSiloNode * +xb_silo_node_get_child (XbSilo *self, XbSiloNode *n) +{ + XbSiloNode *c; + guint32 off = xb_silo_get_offset_for_node (self, n); + off += xb_silo_node_get_size (n); + + /* check for sentinel */ + c = xb_silo_get_node (self, off); + if (!c->is_node) + return NULL; + return c; +} + +/** + * xb_silo_get_root: + * @self: a #XbSilo + * + * Gets the root node for the silo. (MIGHT BE MORE). + * + * Returns: (transfer full): A #XbNode, or %NULL for an error + * + * Since: 0.1.0 + **/ +XbNode * +xb_silo_get_root (XbSilo *self) +{ + g_return_val_if_fail (XB_IS_SILO (self), NULL); + return xb_silo_node_create (self, xb_silo_get_sroot (self)); +} + +/* private */ +guint32 +xb_silo_get_strtab_idx (XbSilo *self, const gchar *element) +{ + XbSiloPrivate *priv = GET_PRIVATE (self); + gpointer value = NULL; + if (!g_hash_table_lookup_extended (priv->strtab_tags, element, NULL, &value)) + return XB_SILO_UNSET; + return GPOINTER_TO_UINT (value); +} + +/** + * xb_silo_to_string: + * @self: a #XbSilo + * @error: the #GError, or %NULL + * + * Converts the silo to an internal string representation. This is only + * really useful for debugging #XbSilo itself. + * + * Returns: A string, or %NULL for an error + * + * Since: 0.1.0 + **/ +gchar * +xb_silo_to_string (XbSilo *self, GError **error) +{ + guint32 off = sizeof(XbSiloHeader); + XbSiloPrivate *priv = GET_PRIVATE (self); + XbSiloHeader *hdr = (XbSiloHeader *) priv->data; + g_autoptr(GString) str = g_string_new (NULL); + + g_return_val_if_fail (XB_IS_SILO (self), NULL); + g_return_val_if_fail (error == NULL || *error == NULL, NULL); + + g_string_append_printf (str, "magic: %08x\n", (guint) hdr->magic); + g_string_append_printf (str, "guid: %s\n", priv->guid); + g_string_append_printf (str, "strtab: @%" G_GUINT32_FORMAT "\n", hdr->strtab); + g_string_append_printf (str, "strtab_ntags: %" G_GUINT16_FORMAT "\n", hdr->strtab_ntags); + while (off < priv->strtab) { + XbSiloNode *n = xb_silo_get_node (self, off); + if (n->is_node) { + g_string_append_printf (str, "NODE @%" G_GUINT32_FORMAT "\n", off); + g_string_append_printf (str, "element_name: %s [%03u]\n", + xb_silo_from_strtab (self, n->element_name), + n->element_name); + g_string_append_printf (str, "next: %" G_GUINT32_FORMAT "\n", n->next); + g_string_append_printf (str, "parent: %" G_GUINT32_FORMAT "\n", n->parent); + if (n->text != XB_SILO_UNSET) { + g_string_append_printf (str, "text: %s\n", + xb_silo_from_strtab (self, n->text)); + } + if (n->tail != XB_SILO_UNSET) { + g_string_append_printf (str, "tail: %s\n", + xb_silo_from_strtab (self, n->tail)); + } + for (guint8 i = 0; i < n->nr_attrs; i++) { + XbSiloAttr *a = xb_silo_get_attr (self, off, i); + g_string_append_printf (str, "attr_name: %s [%03u]\n", + xb_silo_from_strtab (self, a->attr_name), + a->attr_name); + g_string_append_printf (str, "attr_value: %s [%03u]\n", + xb_silo_from_strtab (self, a->attr_value), + a->attr_value); + } + } else { + g_string_append_printf (str, "SENT @%" G_GUINT32_FORMAT "\n", off); + } + off += xb_silo_node_get_size (n); + } + + /* add strtab */ + g_string_append_printf (str, "STRTAB @%" G_GUINT32_FORMAT "\n", hdr->strtab); + for (off = 0; off < priv->datasz - hdr->strtab;) { + const gchar *tmp = xb_silo_from_strtab (self, off); + if (tmp == NULL) + break; + g_string_append_printf (str, "[%03u]: %s\n", off, tmp); + off += strlen (tmp) + 1; + } + + /* success */ + return g_string_free (g_steal_pointer (&str), FALSE); +} + +/* private */ +const gchar * +xb_silo_node_get_text (XbSilo *self, XbSiloNode *n) +{ + if (n->text == XB_SILO_UNSET) + return NULL; + return xb_silo_from_strtab (self, n->text); +} + +/* private */ +const gchar * +xb_silo_node_get_tail (XbSilo *self, XbSiloNode *n) +{ + if (n->tail == XB_SILO_UNSET) + return NULL; + return xb_silo_from_strtab (self, n->tail); +} + +/* private */ +const gchar * +xb_silo_node_get_element (XbSilo *self, XbSiloNode *n) +{ + return xb_silo_from_strtab (self, n->element_name); +} + +/* private */ +XbSiloAttr * +xb_silo_node_get_attr_by_str (XbSilo *self, XbSiloNode *n, const gchar *name) +{ + guint32 off; + + /* calculate offset to first attribute */ + off = xb_silo_get_offset_for_node (self, n); + for (guint8 i = 0; i < n->nr_attrs; i++) { + XbSiloAttr *a = xb_silo_get_attr (self, off, i); + if (g_strcmp0 (xb_silo_from_strtab (self, a->attr_name), name) == 0) + return a; + } + + /* nothing matched */ + return NULL; +} + +static XbSiloAttr * +xb_silo_node_get_attr_by_val (XbSilo *self, XbSiloNode *n, guint32 name) +{ + guint32 off; + + /* calculate offset to first attribute */ + off = xb_silo_get_offset_for_node (self, n); + for (guint8 i = 0; i < n->nr_attrs; i++) { + XbSiloAttr *a = xb_silo_get_attr (self, off, i); + if (a->attr_name == name) + return a; + } + + /* nothing matched */ + return NULL; +} + +/** + * xb_silo_get_size: + * @self: a #XbSilo + * + * Gets the number of nodes in the silo. + * + * Returns: a integer, or 0 is an empty blob + * + * Since: 0.1.0 + **/ +guint +xb_silo_get_size (XbSilo *self) +{ + XbSiloPrivate *priv = GET_PRIVATE (self); + guint32 off = sizeof(XbSiloHeader); + guint nodes_cnt = 0; + + g_return_val_if_fail (XB_IS_SILO (self), 0); + + while (off < priv->strtab) { + XbSiloNode *n = xb_silo_get_node (self, off); + if (n->is_node) + nodes_cnt += 1; + off += xb_silo_node_get_size (n); + } + + /* success */ + return nodes_cnt; +} + +/** + * xb_silo_is_valid: + * @self: a #XbSilo + * + * Checks is the silo is valid. The usual reason the silo is invalidated is + * when the backing mmapped file has changed, or one of the imported files have + * been modified. + * + * Returns: %TRUE if valid + * + * Since: 0.1.0 + **/ +gboolean +xb_silo_is_valid (XbSilo *self) +{ + XbSiloPrivate *priv = GET_PRIVATE (self); + g_return_val_if_fail (XB_IS_SILO (self), FALSE); + return priv->valid; +} + +/* private */ +gboolean +xb_silo_is_empty (XbSilo *self) +{ + XbSiloPrivate *priv = GET_PRIVATE (self); + g_return_val_if_fail (XB_IS_SILO (self), FALSE); + return priv->strtab == sizeof(XbSiloHeader); +} + +/** + * xb_silo_invalidate: + * @self: a #XbSilo + * + * Invalidates a silo. Future calls xb_silo_is_valid() will return %FALSE. + * + * Since: 0.1.1 + **/ +void +xb_silo_invalidate (XbSilo *self) +{ + XbSiloPrivate *priv = GET_PRIVATE (self); + if (!priv->valid) + return; + priv->valid = FALSE; + g_object_notify (G_OBJECT (self), "valid"); +} + +/* private */ +void +xb_silo_uninvalidate (XbSilo *self) +{ + XbSiloPrivate *priv = GET_PRIVATE (self); + if (priv->valid) + return; + priv->valid = TRUE; + g_object_notify (G_OBJECT (self), "valid"); +} + +/* private */ +guint +xb_silo_node_get_depth (XbSilo *self, XbSiloNode *n) +{ + guint depth = 0; + while (n->parent != 0) { + depth++; + n = xb_silo_get_node (self, n->parent); + } + return depth; +} + +/** + * xb_silo_get_bytes: + * @self: a #XbSilo + * + * Gets the backing object that created the blob. + * + * You should never *ever* modify this data. + * + * Returns: (transfer full): A #GBytes, or %NULL if never set + * + * Since: 0.1.0 + **/ +GBytes * +xb_silo_get_bytes (XbSilo *self) +{ + XbSiloPrivate *priv = GET_PRIVATE (self); + g_return_val_if_fail (XB_IS_SILO (self), NULL); + if (priv->blob == NULL) + return NULL; + return g_bytes_ref (priv->blob); +} + +/** + * xb_silo_get_guid: + * @self: a #XbSilo + * + * Gets the GUID used to identify this silo. + * + * Returns: a string, otherwise %NULL + * + * Since: 0.1.0 + **/ +const gchar * +xb_silo_get_guid (XbSilo *self) +{ + XbSiloPrivate *priv = GET_PRIVATE (self); + g_return_val_if_fail (XB_IS_SILO (self), NULL); + return priv->guid; +} + +/* private */ +XbMachine * +xb_silo_get_machine (XbSilo *self) +{ + XbSiloPrivate *priv = GET_PRIVATE (self); + g_return_val_if_fail (XB_IS_SILO (self), NULL); + return priv->machine; +} + +/** + * xb_silo_load_from_bytes: + * @self: a #XbSilo + * @blob: a #GBytes + * @flags: #XbSiloLoadFlags, e.g. %XB_SILO_LOAD_FLAG_NONE + * @error: the #GError, or %NULL + * + * Loads a silo from memory location. + * + * Returns: %TRUE for success, otherwise @error is set. + * + * Since: 0.1.0 + **/ +gboolean +xb_silo_load_from_bytes (XbSilo *self, GBytes *blob, XbSiloLoadFlags flags, GError **error) +{ + XbGuid guid_tmp; + XbSiloHeader *hdr; + XbSiloPrivate *priv = GET_PRIVATE (self); + gsize sz = 0; + guint32 off = 0; + g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&priv->nodes_mutex); + g_autoptr(GTimer) timer = g_timer_new (); + + g_return_val_if_fail (XB_IS_SILO (self), FALSE); + g_return_val_if_fail (blob != NULL, FALSE); + g_return_val_if_fail (error == NULL || *error == NULL, FALSE); + g_return_val_if_fail (locker != NULL, FALSE); + + /* no longer valid */ + g_hash_table_remove_all (priv->nodes); + g_hash_table_remove_all (priv->strtab_tags); + g_clear_pointer (&priv->guid, g_free); + + /* refcount internally */ + if (priv->blob != NULL) + g_bytes_unref (priv->blob); + priv->blob = g_bytes_ref (blob); + + /* update pointers into blob */ + priv->data = g_bytes_get_data (priv->blob, &sz); + priv->datasz = (guint32) sz; + + /* check size */ + if (sz < sizeof(XbSiloHeader)) { + g_set_error_literal (error, + G_IO_ERROR, + G_IO_ERROR_INVALID_DATA, + "blob too small"); + return FALSE; + } + + /* check header magic */ + hdr = (XbSiloHeader *) priv->data; + if ((flags & XB_SILO_LOAD_FLAG_NO_MAGIC) == 0) { + if (hdr->magic != XB_SILO_MAGIC_BYTES) { + g_set_error_literal (error, + G_IO_ERROR, + G_IO_ERROR_INVALID_DATA, + "magic incorrect"); + return FALSE; + } + if (hdr->version != XB_SILO_VERSION) { + g_set_error_literal (error, + G_IO_ERROR, + G_IO_ERROR_INVALID_DATA, + "version incorrect"); + return FALSE; + } + } + + /* get GUID */ + memcpy (&guid_tmp, &hdr->guid, sizeof(guid_tmp)); + priv->guid = xb_guid_to_string (&guid_tmp); + + /* check strtab */ + priv->strtab = hdr->strtab; + if (priv->strtab > priv->datasz) { + g_set_error_literal (error, + G_IO_ERROR, + G_IO_ERROR_INVALID_DATA, + "strtab incorrect"); + return FALSE; + } + + /* load strtab_tags */ + for (guint16 i = 0; i < hdr->strtab_ntags; i++) { + const gchar *tmp = xb_silo_from_strtab (self, off); + if (tmp == NULL) { + g_set_error_literal (error, + G_IO_ERROR, + G_IO_ERROR_INVALID_DATA, + "strtab_ntags incorrect"); + return FALSE; + } + g_hash_table_insert (priv->strtab_tags, + (gpointer) tmp, + GUINT_TO_POINTER (off)); + off += strlen (tmp) + 1; + } + + /* profile */ + xb_silo_add_profile (self, timer, "parse blob"); + + /* success */ + priv->valid = TRUE; + return TRUE; +} + +/** + * xb_silo_get_profile_string: + * @self: a #XbSilo + * + * Returns the profiling data. This will only return profiling text if + * xb_silo_set_profile_flags() was used with %XB_SILO_PROFILE_FLAG_APPEND. + * + * Returns: text profiling data + * + * Since: 0.1.1 + **/ +const gchar * +xb_silo_get_profile_string (XbSilo *self) +{ + XbSiloPrivate *priv = GET_PRIVATE (self); + g_return_val_if_fail (XB_IS_SILO (self), NULL); + return priv->profile_str->str; +} + +/** + * xb_silo_set_profile_flags: + * @self: a #XbSilo + * @profile_flags: some #XbSiloProfileFlags, e.g. %XB_SILO_PROFILE_FLAG_DEBUG + * + * Enables or disables the collection of profiling data. + * + * Since: 0.1.1 + **/ +void +xb_silo_set_profile_flags (XbSilo *self, XbSiloProfileFlags profile_flags) +{ + XbSiloPrivate *priv = GET_PRIVATE (self); + g_return_if_fail (XB_IS_SILO (self)); + priv->profile_flags = profile_flags; +} + +/* private */ +XbSiloProfileFlags +xb_silo_get_profile_flags (XbSilo *self) +{ + XbSiloPrivate *priv = GET_PRIVATE (self); + return priv->profile_flags; +} + +static void +xb_silo_watch_file_cb (GFileMonitor *monitor, + GFile *file, + GFile *other_file, + GFileMonitorEvent event_type, + gpointer user_data) +{ + XbSilo *silo = XB_SILO (user_data); + g_autofree gchar *fn = g_file_get_path (file); + g_autofree gchar *basename = g_file_get_basename (file); + if (g_str_has_prefix (basename, ".goutputstream")) + return; + g_debug ("%s changed, invalidating", fn); + xb_silo_invalidate (silo); +} + +/** + * xb_silo_watch_file: + * @self: a #XbSilo + * @file: a #GFile + * @cancellable: a #GCancellable, or %NULL + * @error: the #GError, or %NULL + * + * Adds a file monitor to the silo. If the file or directory for @file changes + * then the silo will be invalidated. + * + * Returns: %TRUE for success, otherwise @error is set. + * + * Since: 0.1.0 + **/ +gboolean +xb_silo_watch_file (XbSilo *self, + GFile *file, + GCancellable *cancellable, + GError **error) +{ + XbSiloFileMonitorItem *item; + XbSiloPrivate *priv = GET_PRIVATE (self); + g_autofree gchar *fn = g_file_get_path (file); + g_autoptr(GFileMonitor) file_monitor = NULL; + + g_return_val_if_fail (XB_IS_SILO (self), FALSE); + g_return_val_if_fail (cancellable == NULL || G_IS_CANCELLABLE (cancellable), FALSE); + g_return_val_if_fail (error == NULL || *error == NULL, FALSE); + + /* already exists */ + item = g_hash_table_lookup (priv->file_monitors, fn); + if (item != NULL) + return TRUE; + + /* try to create */ + file_monitor = g_file_monitor_file (file, G_FILE_MONITOR_NONE, + cancellable, error); + if (file_monitor == NULL) + return FALSE; + g_file_monitor_set_rate_limit (file_monitor, 20); + + /* add */ + item = g_slice_new0 (XbSiloFileMonitorItem); + item->file_monitor = g_object_ref (file_monitor); + item->file_monitor_id = g_signal_connect (file_monitor, "changed", + G_CALLBACK (xb_silo_watch_file_cb), self); + g_hash_table_insert (priv->file_monitors, g_steal_pointer (&fn), item); + return TRUE; +} + +/** + * xb_silo_load_from_file: + * @self: a #XbSilo + * @file: a #GFile + * @flags: #XbSiloLoadFlags, e.g. %XB_SILO_LOAD_FLAG_NONE + * @cancellable: a #GCancellable, or %NULL + * @error: the #GError, or %NULL + * + * Loads a silo from file. + * + * Returns: %TRUE for success, otherwise @error is set. + * + * Since: 0.1.0 + **/ +gboolean +xb_silo_load_from_file (XbSilo *self, + GFile *file, + XbSiloLoadFlags flags, + GCancellable *cancellable, + GError **error) +{ + XbSiloPrivate *priv = GET_PRIVATE (self); + g_autofree gchar *fn = NULL; + g_autoptr(GBytes) blob = NULL; + g_autoptr(GTimer) timer = g_timer_new (); + + g_return_val_if_fail (XB_IS_SILO (self), FALSE); + g_return_val_if_fail (G_IS_FILE (file), FALSE); + g_return_val_if_fail (cancellable == NULL || G_IS_CANCELLABLE (cancellable), FALSE); + g_return_val_if_fail (error == NULL || *error == NULL, FALSE); + + /* no longer valid */ + g_hash_table_remove_all (priv->file_monitors); + g_hash_table_remove_all (priv->nodes); + g_hash_table_remove_all (priv->strtab_tags); + g_clear_pointer (&priv->guid, g_free); + if (priv->mmap != NULL) + g_mapped_file_unref (priv->mmap); + + fn = g_file_get_path (file); + priv->mmap = g_mapped_file_new (fn, FALSE, error); + if (priv->mmap == NULL) + return FALSE; + blob = g_mapped_file_get_bytes (priv->mmap); + if (!xb_silo_load_from_bytes (self, blob, flags, error)) + return FALSE; + + /* watch file for changes */ + if (flags & XB_SILO_LOAD_FLAG_WATCH_BLOB) { + if (!xb_silo_watch_file (self, file, cancellable, error)) + return FALSE; + } + + /* success */ + xb_silo_add_profile (self, timer, "loaded file"); + return TRUE; +} + +/** + * xb_silo_save_to_file: + * @self: a #XbSilo + * @file: a #GFile + * @cancellable: a #GCancellable, or %NULL + * @error: the #GError, or %NULL + * + * Saves a silo to a file. + * + * Returns: %TRUE for success, otherwise @error is set. + * + * Since: 0.1.0 + **/ +gboolean +xb_silo_save_to_file (XbSilo *self, + GFile *file, + GCancellable *cancellable, + GError **error) +{ + XbSiloPrivate *priv = GET_PRIVATE (self); + g_autoptr(GFile) file_parent = NULL; + g_autoptr(GTimer) timer = g_timer_new (); + + g_return_val_if_fail (XB_IS_SILO (self), FALSE); + g_return_val_if_fail (G_IS_FILE (file), FALSE); + g_return_val_if_fail (cancellable == NULL || G_IS_CANCELLABLE (cancellable), FALSE); + g_return_val_if_fail (error == NULL || *error == NULL, FALSE); + + /* invalid */ + if (priv->data == NULL) { + g_set_error_literal (error, + G_IO_ERROR, + G_IO_ERROR_NOT_INITIALIZED, + "no data to save"); + return FALSE; + } + + /* ensure parent directories exist */ + file_parent = g_file_get_parent (file); + if (file_parent != NULL && + !g_file_query_exists (file_parent, cancellable)) { + if (!g_file_make_directory_with_parents (file_parent, + cancellable, + error)) + return FALSE; + } + + /* save and then rename */ + if (!g_file_replace_contents (file, + (const gchar *) priv->data, + (gsize) priv->datasz, NULL, FALSE, + G_FILE_CREATE_NONE, NULL, + cancellable, error)) { + return FALSE; + } + xb_silo_add_profile (self, timer, "save file"); + return TRUE; +} + +/** + * xb_silo_new_from_xml: + * @xml: XML string + * @error: the #GError, or %NULL + * + * Creates a new silo from an XML string. + * + * Returns: a new #XbSilo, or %NULL + * + * Since: 0.1.0 + **/ +XbSilo * +xb_silo_new_from_xml (const gchar *xml, GError **error) +{ + g_autoptr(XbBuilder) builder = xb_builder_new (); + g_autoptr(XbBuilderSource) source = xb_builder_source_new (); + g_return_val_if_fail (xml != NULL, NULL); + g_return_val_if_fail (error == NULL || *error == NULL, NULL); + if (!xb_builder_source_load_xml (source, xml, XB_BUILDER_SOURCE_FLAG_NONE, error)) + return NULL; + xb_builder_import_source (builder, source); + return xb_builder_compile (builder, XB_BUILDER_COMPILE_FLAG_NONE, NULL, error); +} + +/* private */ +XbNode * +xb_silo_node_create (XbSilo *self, XbSiloNode *sn) +{ + XbNode *n; + XbSiloPrivate *priv = GET_PRIVATE (self); + g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&priv->nodes_mutex); + + g_return_val_if_fail (locker != NULL, NULL); + + /* does already exist */ + n = g_hash_table_lookup (priv->nodes, sn); + if (n != NULL) + return g_object_ref (n); + + /* create and add */ + n = xb_node_new (self, sn); + g_hash_table_insert (priv->nodes, sn, g_object_ref (n)); + return n; +} + +/* convert [2] to position()=2 */ +static gboolean +xb_silo_machine_fixup_position_cb (XbMachine *self, + XbStack *opcodes, + gpointer user_data, + GError **error) +{ + xb_stack_push_steal (opcodes, xb_machine_opcode_func_new (self, "position")); + xb_stack_push_steal (opcodes, xb_machine_opcode_func_new (self, "eq")); + return TRUE; +} + +/* convert "'type' attr()" -> "'type' attr() '(null)' ne()" */ +static gboolean +xb_silo_machine_fixup_attr_exists_cb (XbMachine *self, + XbStack *opcodes, + gpointer user_data, + GError **error) +{ + xb_stack_push_steal (opcodes, xb_opcode_text_new_static (NULL)); + xb_stack_push_steal (opcodes, xb_machine_opcode_func_new (self, "ne")); + return TRUE; +} + +static gboolean +xb_silo_machine_func_attr_cb (XbMachine *self, + XbStack *stack, + gboolean *result, + gpointer user_data, + gpointer exec_data, + GError **error) +{ + XbOpcode *op2; + XbSiloAttr *a; + XbSilo *silo = XB_SILO (user_data); + XbSiloQueryData *query_data = (XbSiloQueryData *) exec_data; + g_autoptr(XbOpcode) op = xb_machine_stack_pop (self, stack); + + /* optimize pass */ + if (query_data == NULL) { + g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_FAILED_HANDLED, + "cannot optimize: no silo to query"); + return FALSE; + } + + /* indexed string */ + if (xb_opcode_get_kind (op) == XB_OPCODE_KIND_INDEXED_TEXT) { + guint32 val = xb_opcode_get_val (op); + a = xb_silo_node_get_attr_by_val (silo, query_data->sn, val); + } else { + const gchar *str = xb_opcode_get_str (op); + a = xb_silo_node_get_attr_by_str (silo, query_data->sn, str); + } + if (a == NULL) { + xb_machine_stack_push_text_static (self, stack, NULL); + return TRUE; + } + op2 = xb_opcode_new (XB_OPCODE_KIND_INDEXED_TEXT, + xb_silo_from_strtab (silo, a->attr_value), + a->attr_value, + NULL); + xb_machine_stack_push_steal (self, stack, op2); + return TRUE; +} + +static gboolean +xb_silo_machine_func_stem_cb (XbMachine *self, + XbStack *stack, + gboolean *result, + gpointer user_data, + gpointer exec_data, + GError **error) +{ + XbSilo *silo = XB_SILO (user_data); + g_autoptr(XbOpcode) op = xb_machine_stack_pop (self, stack); + + /* TEXT */ + if (xb_opcode_cmp_str (op)) { + const gchar *str = xb_opcode_get_str (op); + xb_machine_stack_push_text_steal (self, stack, xb_silo_stem (silo, str)); + return TRUE; + } + + /* fail */ + g_set_error (error, + G_IO_ERROR, + G_IO_ERROR_NOT_SUPPORTED, + "%s type not supported", + xb_opcode_kind_to_string (xb_opcode_get_kind (op))); + return FALSE; +} + +static gboolean +xb_silo_machine_func_text_cb (XbMachine *self, + XbStack *stack, + gboolean *result, + gpointer user_data, + gpointer exec_data, + GError **error) +{ + XbSilo *silo = XB_SILO (user_data); + XbSiloQueryData *query_data = (XbSiloQueryData *) exec_data; + + /* optimize pass */ + if (query_data == NULL) { + g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_FAILED_HANDLED, + "cannot optimize: no silo to query"); + return FALSE; + } + xb_machine_stack_push_steal (self, stack, + xb_opcode_new (XB_OPCODE_KIND_INDEXED_TEXT, + xb_silo_node_get_text (silo, query_data->sn), + query_data->sn->text, + NULL)); + return TRUE; +} + +static gboolean +xb_silo_machine_func_tail_cb (XbMachine *self, + XbStack *stack, + gboolean *result, + gpointer user_data, + gpointer exec_data, + GError **error) +{ + XbSilo *silo = XB_SILO (user_data); + XbSiloQueryData *query_data = (XbSiloQueryData *) exec_data; + + /* optimize pass */ + if (query_data == NULL) { + g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_FAILED_HANDLED, + "cannot optimize: no silo to query"); + return FALSE; + } + xb_machine_stack_push_steal (self, stack, + xb_opcode_new (XB_OPCODE_KIND_INDEXED_TEXT, + xb_silo_node_get_tail (silo, query_data->sn), + query_data->sn->tail, + NULL)); + return TRUE; +} + +static gboolean +xb_silo_machine_func_first_cb (XbMachine *self, + XbStack *stack, + gboolean *result, + gpointer user_data, + gpointer exec_data, + GError **error) +{ + XbSiloQueryData *query_data = (XbSiloQueryData *) exec_data; + + /* optimize pass */ + if (query_data == NULL) { + g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_FAILED_HANDLED, + "cannot optimize: no silo to query"); + return FALSE; + } + xb_stack_push_bool (stack, query_data->position == 1); + return TRUE; +} + +static gboolean +xb_silo_machine_func_last_cb (XbMachine *self, + XbStack *stack, + gboolean *result, + gpointer user_data, + gpointer exec_data, + GError **error) +{ + XbSiloQueryData *query_data = (XbSiloQueryData *) exec_data; + + /* optimize pass */ + if (query_data == NULL) { + g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_FAILED_HANDLED, + "cannot optimize: no silo to query"); + return FALSE; + } + xb_stack_push_bool (stack, query_data->sn->next == 0); + return TRUE; +} + +static gboolean +xb_silo_machine_func_position_cb (XbMachine *self, + XbStack *stack, + gboolean *result, + gpointer user_data, + gpointer exec_data, + GError **error) +{ + XbSiloQueryData *query_data = (XbSiloQueryData *) exec_data; + + /* optimize pass */ + if (query_data == NULL) { + g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_FAILED_HANDLED, + "cannot optimize: no silo to query"); + return FALSE; + } + xb_machine_stack_push_integer (self, stack, query_data->position); + return TRUE; +} + +static gboolean +xb_silo_machine_func_search_cb (XbMachine *self, + XbStack *stack, + gboolean *result, + gpointer user_data, + gpointer exec_data, + GError **error) +{ + g_autoptr(XbOpcode) op1 = xb_machine_stack_pop (self, stack); + g_autoptr(XbOpcode) op2 = xb_machine_stack_pop (self, stack); + + /* TEXT:TEXT */ + if (xb_opcode_cmp_str (op1) && xb_opcode_cmp_str (op2)) { + xb_stack_push_bool (stack, xb_string_search (xb_opcode_get_str (op2), + xb_opcode_get_str (op1))); + return TRUE; + } + + /* fail */ + g_set_error (error, + G_IO_ERROR, + G_IO_ERROR_NOT_SUPPORTED, + "%s:%s types not supported", + xb_opcode_kind_to_string (xb_opcode_get_kind (op1)), + xb_opcode_kind_to_string (xb_opcode_get_kind (op2))); + return FALSE; +} + +static gboolean +xb_silo_machine_fixup_attr_text_cb (XbMachine *self, + XbStack *opcodes, + const gchar *text, + gboolean *handled, + gpointer user_data, + GError **error) +{ + /* @foo -> attr(foo) */ + if (g_str_has_prefix (text, "@")) { + XbOpcode *opcode; + opcode = xb_machine_opcode_func_new (self, "attr"); + if (opcode == NULL) { + g_set_error_literal (error, + G_IO_ERROR, + G_IO_ERROR_NOT_SUPPORTED, + "no attr opcode"); + return FALSE; + } + xb_stack_push_steal (opcodes, xb_opcode_text_new (text + 1)); + xb_stack_push_steal (opcodes, opcode); + *handled = TRUE; + return TRUE; + } + + /* not us */ + return TRUE; +} + +static void +xb_silo_file_monitor_item_free (XbSiloFileMonitorItem *item) +{ + g_signal_handler_disconnect (item->file_monitor, item->file_monitor_id); + g_object_unref (item->file_monitor); + g_slice_free (XbSiloFileMonitorItem, item); +} + +static void +xb_silo_get_property (GObject *obj, guint prop_id, GValue *value, GParamSpec *pspec) +{ + XbSilo *self = XB_SILO (obj); + XbSiloPrivate *priv = GET_PRIVATE (self); + switch (prop_id) { + case PROP_GUID: + g_value_set_string (value, priv->guid); + break; + case PROP_VALID: + g_value_set_boolean (value, priv->valid); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (obj, prop_id, pspec); + break; + } +} + +static void +xb_silo_set_property (GObject *obj, guint prop_id, const GValue *value, GParamSpec *pspec) +{ + XbSilo *self = XB_SILO (obj); + XbSiloPrivate *priv = GET_PRIVATE (self); + switch (prop_id) { + case PROP_GUID: + g_free (priv->guid); + priv->guid = g_value_dup_string (value); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (obj, prop_id, pspec); + break; + } +} + +static void +xb_silo_init (XbSilo *self) +{ + XbSiloPrivate *priv = GET_PRIVATE (self); + priv->file_monitors = g_hash_table_new_full (g_str_hash, g_str_equal, + g_free, (GDestroyNotify) xb_silo_file_monitor_item_free); + priv->nodes = g_hash_table_new_full (g_direct_hash, g_direct_equal, + NULL, (GDestroyNotify) g_object_unref); + priv->strtab_tags = g_hash_table_new (g_str_hash, g_str_equal); + priv->strindex = g_hash_table_new (g_str_hash, g_str_equal); + priv->profile_str = g_string_new (NULL); + + g_mutex_init (&priv->nodes_mutex); + +#ifdef HAVE_LIBSTEMMER + g_mutex_init (&priv->stemmer_mutex); +#endif + + priv->machine = xb_machine_new (); + xb_machine_add_method (priv->machine, "attr", 1, + xb_silo_machine_func_attr_cb, self, NULL); + xb_machine_add_method (priv->machine, "stem", 1, + xb_silo_machine_func_stem_cb, self, NULL); + xb_machine_add_method (priv->machine, "text", 0, + xb_silo_machine_func_text_cb, self, NULL); + xb_machine_add_method (priv->machine, "tail", 0, + xb_silo_machine_func_tail_cb, self, NULL); + xb_machine_add_method (priv->machine, "first", 0, + xb_silo_machine_func_first_cb, self, NULL); + xb_machine_add_method (priv->machine, "last", 0, + xb_silo_machine_func_last_cb, self, NULL); + xb_machine_add_method (priv->machine, "position", 0, + xb_silo_machine_func_position_cb, self, NULL); + xb_machine_add_method (priv->machine, "search", 2, + xb_silo_machine_func_search_cb, self, NULL); + xb_machine_add_operator (priv->machine, "~=", "search"); + xb_machine_add_opcode_fixup (priv->machine, "INTE", + xb_silo_machine_fixup_position_cb, self, NULL); + xb_machine_add_opcode_fixup (priv->machine, "TEXT,FUNC:attr", + xb_silo_machine_fixup_attr_exists_cb, self, NULL); + xb_machine_add_text_handler (priv->machine, + xb_silo_machine_fixup_attr_text_cb, self, NULL); +} + +static void +xb_silo_finalize (GObject *obj) +{ + XbSilo *self = XB_SILO (obj); + XbSiloPrivate *priv = GET_PRIVATE (self); + + g_mutex_clear (&priv->nodes_mutex); + +#ifdef HAVE_LIBSTEMMER + if (priv->stemmer_ctx != NULL) + sb_stemmer_delete (priv->stemmer_ctx); + g_mutex_clear (&priv->stemmer_mutex); +#endif + + g_free (priv->guid); + g_string_free (priv->profile_str, TRUE); + g_object_unref (priv->machine); + g_hash_table_unref (priv->strindex); + g_hash_table_unref (priv->file_monitors); + g_hash_table_unref (priv->nodes); + g_hash_table_unref (priv->strtab_tags); + if (priv->mmap != NULL) + g_mapped_file_unref (priv->mmap); + if (priv->blob != NULL) + g_bytes_unref (priv->blob); + G_OBJECT_CLASS (xb_silo_parent_class)->finalize (obj); +} + +static void +xb_silo_class_init (XbSiloClass *klass) +{ + GParamSpec *pspec; + GObjectClass *object_class = G_OBJECT_CLASS (klass); + object_class->finalize = xb_silo_finalize; + object_class->get_property = xb_silo_get_property; + object_class->set_property = xb_silo_set_property; + + /** + * XbSilo:guid: + */ + pspec = g_param_spec_string ("guid", NULL, NULL, NULL, + G_PARAM_READWRITE | + G_PARAM_CONSTRUCT | + G_PARAM_STATIC_NAME); + g_object_class_install_property (object_class, PROP_GUID, pspec); + + /** + * XbSilo:allow-cancel: + */ + pspec = g_param_spec_boolean ("valid", NULL, NULL, TRUE, + G_PARAM_READABLE | + G_PARAM_STATIC_NAME); + g_object_class_install_property (object_class, PROP_VALID, pspec); +} + +/** + * xb_silo_new: + * + * Creates a new silo. + * + * Returns: a new #XbSilo + * + * Since: 0.1.0 + **/ +XbSilo * +xb_silo_new (void) +{ + return g_object_new (XB_TYPE_SILO, NULL); +} diff --git a/src/xb-silo.h b/src/xb-silo.h new file mode 100644 index 0000000..329ad53 --- /dev/null +++ b/src/xb-silo.h @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2018 Richard Hughes + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#pragma once + +#include + +#include "xb-node.h" + +G_BEGIN_DECLS + +#define XB_TYPE_SILO (xb_silo_get_type ()) +G_DECLARE_DERIVABLE_TYPE (XbSilo, xb_silo, XB, SILO, GObject) + +struct _XbSiloClass { + GObjectClass parent_class; + /*< private >*/ + void (*_xb_reserved1) (void); + void (*_xb_reserved2) (void); + void (*_xb_reserved3) (void); + void (*_xb_reserved4) (void); + void (*_xb_reserved5) (void); + void (*_xb_reserved6) (void); + void (*_xb_reserved7) (void); +}; + +/** + * XbSiloLoadFlags: + * @XB_SILO_LOAD_FLAG_NONE: No extra flags to use + * @XB_SILO_LOAD_FLAG_NO_MAGIC: No not check header signature + * @XB_SILO_LOAD_FLAG_WATCH_BLOB: Watch the XMLB file for changes + * + * The flags for loading a silo. + **/ +typedef enum { + XB_SILO_LOAD_FLAG_NONE = 0, /* Since: 0.1.0 */ + XB_SILO_LOAD_FLAG_NO_MAGIC = 1 << 0, /* Since: 0.1.0 */ + XB_SILO_LOAD_FLAG_WATCH_BLOB = 1 << 1, /* Since: 0.1.0 */ + /*< private >*/ + XB_SILO_LOAD_FLAG_LAST +} XbSiloLoadFlags; + +/** + * XbSiloProfileFlags: + * @XB_SILO_PROFILE_FLAG_NONE: No extra flags to use + * @XB_SILO_PROFILE_FLAG_DEBUG: Output profiling as debug + * @XB_SILO_PROFILE_FLAG_APPEND: Save profiling in an appended string + * @XB_SILO_PROFILE_FLAG_XPATH: Save XPATH queries + * + * The flags used when profiling a silo. + **/ +typedef enum { + XB_SILO_PROFILE_FLAG_NONE = 0, /* Since: 0.1.1 */ + XB_SILO_PROFILE_FLAG_DEBUG = 1 << 0, /* Since: 0.1.1 */ + XB_SILO_PROFILE_FLAG_APPEND = 1 << 1, /* Since: 0.1.1 */ + XB_SILO_PROFILE_FLAG_XPATH = 1 << 2, /* Since: 0.1.1 */ + /*< private >*/ + XB_SILO_PROFILE_FLAG_LAST +} XbSiloProfileFlags; + +XbSilo *xb_silo_new (void); +XbSilo *xb_silo_new_from_xml (const gchar *xml, + GError **error); +GBytes *xb_silo_get_bytes (XbSilo *self); +gboolean xb_silo_load_from_bytes (XbSilo *self, + GBytes *blob, + XbSiloLoadFlags flags, + GError **error); +gboolean xb_silo_load_from_file (XbSilo *self, + GFile *file, + XbSiloLoadFlags flags, + GCancellable *cancellable, + GError **error); +gboolean xb_silo_save_to_file (XbSilo *self, + GFile *file, + GCancellable *cancellable, + GError **error); +gchar *xb_silo_to_string (XbSilo *self, + GError **error); +guint xb_silo_get_size (XbSilo *self); +const gchar *xb_silo_get_guid (XbSilo *self); +XbNode *xb_silo_get_root (XbSilo *self); +void xb_silo_invalidate (XbSilo *self); +gboolean xb_silo_is_valid (XbSilo *self); +gboolean xb_silo_watch_file (XbSilo *self, + GFile *file, + GCancellable *cancellable, + GError **error); +void xb_silo_set_profile_flags (XbSilo *self, + XbSiloProfileFlags profile_flags); +const gchar *xb_silo_get_profile_string (XbSilo *self); + +G_END_DECLS diff --git a/src/xb-stack-private.h b/src/xb-stack-private.h new file mode 100644 index 0000000..b233548 --- /dev/null +++ b/src/xb-stack-private.h @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2018 Richard Hughes + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#pragma once + +#include + +#include "xb-stack.h" + +G_BEGIN_DECLS + +XbStack *xb_stack_new (guint max_size); +void xb_stack_unref (XbStack *self); +XbStack *xb_stack_ref (XbStack *self); +guint xb_stack_get_size (XbStack *self); +guint xb_stack_get_max_size (XbStack *self); +gboolean xb_stack_push_bool (XbStack *self, + gboolean val); +XbOpcode *xb_stack_peek (XbStack *self, + guint idx); +XbOpcode *xb_stack_peek_head (XbStack *self); +XbOpcode *xb_stack_peek_tail (XbStack *self); +GPtrArray *xb_stack_steal_all (XbStack *self); + +G_DEFINE_AUTOPTR_CLEANUP_FUNC(XbStack, xb_stack_unref) + +G_END_DECLS diff --git a/src/xb-stack.c b/src/xb-stack.c new file mode 100644 index 0000000..7a25e90 --- /dev/null +++ b/src/xb-stack.c @@ -0,0 +1,262 @@ +/* + * Copyright (C) 2018 Richard Hughes + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#define G_LOG_DOMAIN "XbMachine" + +#include "config.h" + +#include + +#include "xb-opcode-private.h" +#include "xb-stack-private.h" + +struct _XbStack { + gint ref; + guint pos; + guint max_size; + XbOpcode *opcodes[]; /* allocated as part of XbStack */ +}; + +/** + * xb_stack_unref: + * @self: a #XbStack + * + * Decrements the reference count of the stack, freeing the object when the + * refcount drops to zero. + * + * Since: 0.1.3 + **/ +void +xb_stack_unref (XbStack *self) +{ + g_assert (self->ref > 0); + if (--self->ref > 0) + return; + for (guint i = 0; i < self->pos; i++) + xb_opcode_unref (self->opcodes[i]); + g_free (self); +} + +/** + * xb_stack_ref: + * @self: a #XbStack + * + * Increments the refcount of the stack. + * + * Returns: (transfer none): the original @self #XbStack instance + * + * Since: 0.1.3 + **/ +XbStack * +xb_stack_ref (XbStack *self) +{ + self->ref++; + return self; +} + + +/** + * xb_stack_pop: + * @self: a #XbStack + * + * Pops an opcode off the stack. + * + * Returns: (transfer full): a #XbOpcode + * + * Since: 0.1.3 + **/ +XbOpcode * +xb_stack_pop (XbStack *self) +{ + if (self->pos == 0) + return NULL; + return self->opcodes[--self->pos]; +} + +/* private */ +GPtrArray * +xb_stack_steal_all (XbStack *self) +{ + GPtrArray *array; + + /* array takes ownership of the opcodes */ + array = g_ptr_array_new_with_free_func ((GDestroyNotify) xb_opcode_unref); + for (guint i = 0; i < self->pos; i++) + g_ptr_array_add (array, self->opcodes[i]); + self->pos = 0; + return array; +} + +/** + * xb_stack_peek: + * @self: a #XbStack + * @idx: index + * + * Peeks an opcode from the stack. + * + * Returns: (transfer none): a #XbOpcode + * + * Since: 0.1.3 + **/ +XbOpcode * +xb_stack_peek (XbStack *self, guint idx) +{ + if (idx >= self->pos) + return NULL; + return self->opcodes[idx]; +} + +/* private */ +gboolean +xb_stack_push_bool (XbStack *self, gboolean val) +{ + return xb_stack_push_steal (self, xb_opcode_bool_new (val)); +} + +/* private */ +XbOpcode * +xb_stack_peek_head (XbStack *self) +{ + if (self->pos == 0) + return NULL; + return self->opcodes[0]; +} + +/* private */ +XbOpcode * +xb_stack_peek_tail (XbStack *self) +{ + if (self->pos == 0) + return NULL; + return self->opcodes[self->pos - 1]; +} + +/** + * xb_stack_push: + * @self: a #XbStack + * @opcode: a #XbOpcode + * + * Pushes a new opcode onto the end of the stack + * + * Returns: %TRUE if the opcode was stored on the stack + * + * Since: 0.1.3 + **/ +gboolean +xb_stack_push (XbStack *self, XbOpcode *opcode) +{ + if (self->pos >= self->max_size) + return FALSE; + self->opcodes[self->pos++] = xb_opcode_ref (opcode); + return TRUE; +} + +/** + * xb_stack_push_steal: + * @self: a #XbStack + * @opcode: a #XbOpcode, which is consumed + * + * Pushes a new opcode onto the end of the stack + * + * Returns: %TRUE if the opcode was stored on the stack + * + * Since: 0.1.3 + **/ +gboolean +xb_stack_push_steal (XbStack *self, XbOpcode *opcode) +{ + if (self->pos >= self->max_size) + return FALSE; + self->opcodes[self->pos++] = opcode; + return TRUE; +} + +/** + * xb_stack_get_size: + * @self: a #XbStack + * + * Gets the current size of the stack. + * + * Returns: integer, where 0 is "empty" + * + * Since: 0.1.3 + **/ +guint +xb_stack_get_size (XbStack *self) +{ + return self->pos; +} + +/** + * xb_stack_get_max_size: + * @self: a #XbStack + * + * Gets the maximum size of the stack. + * + * Returns: integer + * + * Since: 0.1.3 + **/ +guint +xb_stack_get_max_size (XbStack *self) +{ + return self->max_size; +} + +/** + * xb_stack_to_string: + * @self: a #XbStack + * + * Returns a string representing a stack. + * + * Returns: text + * + * Since: 0.1.4 + **/ +gchar * +xb_stack_to_string (XbStack *self) +{ + GString *str = g_string_new (NULL); + for (guint i = 0; i < self->pos; i++) { + g_autofree gchar *tmp = xb_opcode_to_string (self->opcodes[i]); + g_string_append_printf (str, "%s,", tmp); + } + if (str->len > 0) + g_string_truncate (str, str->len - 1); + return g_string_free (str, FALSE); +} + +/** + * xb_stack_new: + * @max_size: maximum size of the stack + * + * Creates a stack for the XbMachine request. Only #XbOpcode's can be pushed and + * popped from the stack. + * + * Returns: (transfer full): a #XbStack + * + * Since: 0.1.3 + **/ +XbStack * +xb_stack_new (guint max_size) +{ + XbStack *self = g_malloc0 (sizeof(XbStack) + max_size * sizeof(XbOpcode*)); + self->ref = 1; + self->max_size = max_size; + return self; +} + +GType +xb_stack_get_type (void) +{ + static GType type = 0; + if (G_UNLIKELY (!type)) { + type = g_boxed_type_register_static ("XbStack", + (GBoxedCopyFunc) xb_stack_ref, + (GBoxedFreeFunc) xb_stack_unref); + } + return type; +} diff --git a/src/xb-stack.h b/src/xb-stack.h new file mode 100644 index 0000000..2bd5b27 --- /dev/null +++ b/src/xb-stack.h @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2018 Richard Hughes + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#pragma once + +#include + +#include "xb-opcode.h" + +G_BEGIN_DECLS + +typedef struct _XbStack XbStack; + +GType xb_stack_get_type (void); +gchar *xb_stack_to_string (XbStack *self); +XbOpcode *xb_stack_pop (XbStack *self); +gboolean xb_stack_push (XbStack *self, + XbOpcode *opcode); +gboolean xb_stack_push_steal (XbStack *self, + XbOpcode *opcode); + +G_END_DECLS diff --git a/src/xb-string-private.h b/src/xb-string-private.h new file mode 100644 index 0000000..0238677 --- /dev/null +++ b/src/xb-string-private.h @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2018 Richard Hughes + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#pragma once + +#include +#include "xb-string.h" + +G_BEGIN_DECLS + +guint xb_string_replace (GString *str, + const gchar *search, + const gchar *replace); +gboolean xb_string_contains (const gchar *text, + const gchar *search); +gboolean xb_string_search (const gchar *text, + const gchar *search); +gchar *xb_string_xml_escape (const gchar *str); +gboolean xb_string_isspace (const gchar *str, + gssize strsz); + +typedef struct __attribute__ ((packed)) { + guint32 tlo; + guint16 tmi; + guint16 thi; + guint16 clo; + guint8 nde[6]; +} XbGuid; + +gchar *xb_guid_to_string (XbGuid *guid); +void xb_guid_compute_for_data (XbGuid *out, + const guint8 *buf, + gsize bufsz); + +G_END_DECLS diff --git a/src/xb-string.c b/src/xb-string.c new file mode 100644 index 0000000..a3f40ad --- /dev/null +++ b/src/xb-string.c @@ -0,0 +1,250 @@ +/* + * Copyright (C) 2018 Richard Hughes + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#define G_LOG_DOMAIN "XbSilo" + +#include "config.h" + +#include +#include + +#include "xb-string-private.h" + +/** + * xb_string_replace: (skip) + * @str: The #GString to operate on + * @search: The text to search for + * @replace: The text to use for substitutions + * + * Performs multiple search and replace operations on the given string. + * + * Returns: the number of replacements done, or 0 if @search is not found. + **/ +guint +xb_string_replace (GString *str, const gchar *search, const gchar *replace) +{ + gchar *tmp; + guint count = 0; + gsize search_idx = 0; + gsize replace_len; + gsize search_len; + + g_return_val_if_fail (str != NULL, 0); + g_return_val_if_fail (search != NULL, 0); + g_return_val_if_fail (replace != NULL, 0); + + /* nothing to do */ + if (str->len == 0) + return 0; + + search_len = strlen (search); + replace_len = strlen (replace); + + do { + tmp = g_strstr_len (str->str + search_idx, -1, search); + if (tmp == NULL) + break; + + /* advance the counter in case @replace contains @search */ + search_idx = (gsize) (tmp - str->str); + + /* reallocate the string if required */ + if (search_len > replace_len) { + g_string_erase (str, + (gssize) search_idx, + (gssize) (search_len - replace_len)); + memcpy (tmp, replace, replace_len); + } else if (search_len < replace_len) { + g_string_insert_len (str, + (gssize) search_idx, + replace, + (gssize) (replace_len - search_len)); + /* we have to treat this specially as it could have + * been reallocated when the insertion happened */ + memcpy (str->str + search_idx, replace, replace_len); + } else { + /* just memcmp in the new string */ + memcpy (tmp, replace, replace_len); + } + search_idx += replace_len; + count++; + } while (TRUE); + + return count; +} + +/** + * xb_string_append_union: + * @xpath: The #GString to operate on + * @fmt: The format string + * @...: varargs for @fmt + * + * Appends an XPath query into the string, automatically adding the union + * operator (`|`) if required. + * + * Since: 0.1.2 + **/ +void +xb_string_append_union (GString *xpath, const gchar *fmt, ...) +{ + va_list args; + + g_return_if_fail (xpath != NULL); + g_return_if_fail (fmt != NULL); + + if (xpath->len > 0) + g_string_append (xpath, "|"); + va_start (args, fmt); +#pragma clang diagnostic ignored "-Wformat-nonliteral" + g_string_append_vprintf (xpath, fmt, args); +#pragma clang diagnostic pop + va_end (args); +} + +/** + * xb_string_contains: (skip) + * @text: The source string + * @search: The text to search for + * + * Searches for a substring match. + * + * Returns: %TRUE if the string @search is contained in @text. + **/ +gboolean +xb_string_contains (const gchar *text, const gchar *search) +{ + guint search_sz; + guint text_sz; + + /* can't possibly match */ + if (text == NULL || search == NULL) + return FALSE; + + /* sanity check */ + text_sz = strlen (text); + search_sz = strlen (search); + if (search_sz > text_sz) + return FALSE; + for (guint i = 0; i < text_sz - search_sz + 1; i++) { + if (strncmp (text + i, search, search_sz) == 0) + return TRUE; + } + return FALSE; +} + +/** + * xb_string_search: (skip) + * @text: The source string + * @search: The text to search for + * + * Searches for a fuzzy search match, ignoring search matches that are not at + * the start of the token. + * + * Returns: %TRUE if the string @search is contained in @text. + **/ +gboolean +xb_string_search (const gchar *text, const gchar *search) +{ + guint search_sz; + guint text_sz; + gboolean is_sow = TRUE; + + /* can't possibly match */ + if (text == NULL || text[0] == '\0') + return FALSE; + if (search == NULL || search[0] == '\0') + return FALSE; + + /* sanity check */ + text_sz = strlen (text); + search_sz = strlen (search); + if (search_sz > text_sz) + return FALSE; + for (guint i = 0; i < text_sz - search_sz + 1; i++) { + if (!g_ascii_isalnum (text[i])) { + is_sow = TRUE; + continue; + } + if (!is_sow) + continue; + if (g_ascii_strncasecmp (text + i, search, search_sz) == 0) + return TRUE; + /* no longer the start of the word */ + is_sow = FALSE; + } + return FALSE; +} + +/** + * xb_string_escape: + * @str: string, e.g. `app/org.gnome.ghex/x86_64/stable` + * + * Escapes XPath control sequences such as newlines, tabs, and forward slashes. + * + * Returns: (transfer full): new string that is safe to use for queries + * + * Since: 0.1.2 + **/ +gchar * +xb_string_escape (const gchar *str) +{ + GString *tmp = g_string_new (str); + xb_string_replace (tmp, "/", "\\/"); + xb_string_replace (tmp, "\t", "\\t"); + xb_string_replace (tmp, "\n", "\\n"); + return g_string_free (tmp, FALSE); +} + +gchar * +xb_string_xml_escape (const gchar *str) +{ + GString *tmp = g_string_new (str); + xb_string_replace (tmp, "&", "&"); + xb_string_replace (tmp, "<", "<"); + xb_string_replace (tmp, ">", ">"); + xb_string_replace (tmp, "\"", """); + return g_string_free (tmp, FALSE); +} + +/* private */ +gboolean +xb_string_isspace (const gchar *str, gssize strsz) +{ + gsize strsz_safe; + if (str == NULL) + return TRUE; + strsz_safe = strsz >= 0 ? (gsize) strsz : strlen (str); + for (gsize i = 0; i < strsz_safe; i++) { + if (!g_ascii_isspace (str[i])) + return FALSE; + } + return TRUE; +} + +void +xb_guid_compute_for_data (XbGuid *out, const guint8 *buf, gsize bufsz) +{ + guint8 buf_tmp[20] = { 0x0 }; + gsize buf_tmpsz = sizeof(buf_tmp); + g_autoptr(GChecksum) checksum = g_checksum_new (G_CHECKSUM_SHA1); + if (buf != NULL && bufsz != 0) + g_checksum_update (checksum, (const guchar *) buf, bufsz); + g_checksum_get_digest (checksum, buf_tmp, &buf_tmpsz); + memcpy (out, buf_tmp, sizeof(XbGuid)); +} + +gchar * +xb_guid_to_string (XbGuid *guid) +{ + return g_strdup_printf ("%08x-%04x-%04x-%04x-%02x%02x%02x%02x%02x%02x", + (guint) GUINT32_TO_BE (guid->tlo), + (guint) GUINT16_TO_BE (guid->tmi), + (guint) GUINT16_TO_BE (guid->thi), + (guint) GUINT16_TO_BE (guid->clo), + guid->nde[0], guid->nde[1], + guid->nde[2], guid->nde[3], + guid->nde[4], guid->nde[5]); +} diff --git a/src/xb-string.h b/src/xb-string.h new file mode 100644 index 0000000..80052cd --- /dev/null +++ b/src/xb-string.h @@ -0,0 +1,18 @@ +/* + * Copyright (C) 2018 Richard Hughes + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#pragma once + +#include + +G_BEGIN_DECLS + +void xb_string_append_union (GString *xpath, + const gchar *fmt, + ...) G_GNUC_PRINTF (2, 3); +gchar *xb_string_escape (const gchar *str); + +G_END_DECLS diff --git a/src/xb-tool.c b/src/xb-tool.c new file mode 100644 index 0000000..f496beb --- /dev/null +++ b/src/xb-tool.c @@ -0,0 +1,545 @@ +/* + * Copyright (C) 2018 Richard Hughes + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#include "config.h" + +#include +#include +#ifdef HAVE_GIO_UNIX +#include +#endif +#include + +#include "xb-builder.h" +#include "xb-silo-export.h" +#include "xb-silo-query.h" +#include "xb-node.h" + +typedef struct { + GCancellable *cancellable; + GMainLoop *loop; + GPtrArray *cmd_array; + gboolean force; + gboolean wait; + gboolean profile; +} XbToolPrivate; + +static void +xb_tool_private_free (XbToolPrivate *priv) +{ + if (priv == NULL) + return; + if (priv->cmd_array != NULL) + g_ptr_array_unref (priv->cmd_array); + g_main_loop_unref (priv->loop); + g_object_unref (priv->cancellable); + g_free (priv); +} + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wunused-function" +G_DEFINE_AUTOPTR_CLEANUP_FUNC(XbToolPrivate, xb_tool_private_free) +#pragma clang diagnostic pop + +typedef gboolean (*FuUtilPrivateCb) (XbToolPrivate *util, + gchar **values, + GError **error); + +typedef struct { + gchar *name; + gchar *arguments; + gchar *description; + FuUtilPrivateCb callback; +} FuUtilItem; + +static void +xb_tool_item_free (FuUtilItem *item) +{ + g_free (item->name); + g_free (item->arguments); + g_free (item->description); + g_free (item); +} + +static gint +xb_tool_sort_command_name_cb (FuUtilItem **item1, FuUtilItem **item2) +{ + return g_strcmp0 ((*item1)->name, (*item2)->name); +} + +static void +xb_tool_add (GPtrArray *array, + const gchar *name, + const gchar *arguments, + const gchar *description, + FuUtilPrivateCb callback) +{ + g_auto(GStrv) names = NULL; + + g_return_if_fail (name != NULL); + g_return_if_fail (description != NULL); + g_return_if_fail (callback != NULL); + + /* add each one */ + names = g_strsplit (name, ",", -1); + for (guint i = 0; names[i] != NULL; i++) { + FuUtilItem *item = g_new0 (FuUtilItem, 1); + item->name = g_strdup (names[i]); + if (i == 0) { + item->description = g_strdup (description); + } else { + /* TRANSLATORS: this is a command alias, e.g. 'get-devices' */ + item->description = g_strdup_printf ("Alias to %s", + names[0]); + } + item->arguments = g_strdup (arguments); + item->callback = callback; + g_ptr_array_add (array, item); + } +} + +static void +xb_tool_cancelled_cb (GCancellable *cancellable, gpointer user_data) +{ + XbToolPrivate *priv = (XbToolPrivate *) user_data; + g_print ("Cancelled!\n"); + g_main_loop_quit (priv->loop); +} + +static gchar * +xb_tool_get_descriptions (GPtrArray *array) +{ + gsize len; + const gsize max_len = 31; + FuUtilItem *item; + GString *string; + + /* print each command */ + string = g_string_new (""); + for (guint i = 0; i < array->len; i++) { + item = g_ptr_array_index (array, i); + g_string_append (string, " "); + g_string_append (string, item->name); + len = strlen (item->name) + 2; + if (item->arguments != NULL) { + g_string_append (string, " "); + g_string_append (string, item->arguments); + len += strlen (item->arguments) + 1; + } + if (len < max_len) { + for (guint j = len; j < max_len + 1; j++) + g_string_append_c (string, ' '); + g_string_append (string, item->description); + g_string_append_c (string, '\n'); + } else { + g_string_append_c (string, '\n'); + for (guint j = 0; j < max_len + 1; j++) + g_string_append_c (string, ' '); + g_string_append (string, item->description); + g_string_append_c (string, '\n'); + } + } + + /* remove trailing newline */ + if (string->len > 0) + g_string_set_size (string, string->len - 1); + + return g_string_free (string, FALSE); +} + +static gboolean +xb_tool_run (XbToolPrivate *priv, + const gchar *command, + gchar **values, + GError **error) +{ + /* find command */ + for (guint i = 0; i < priv->cmd_array->len; i++) { + FuUtilItem *item = g_ptr_array_index (priv->cmd_array, i); + if (g_strcmp0 (item->name, command) == 0) + return item->callback (priv, values, error); + } + + /* not found */ + g_set_error_literal (error, + G_IO_ERROR, + G_IO_ERROR_FAILED, + "Command not found"); + return FALSE; +} + +static gboolean +xb_tool_dump (XbToolPrivate *priv, gchar **values, GError **error) +{ + XbSiloLoadFlags flags = XB_SILO_LOAD_FLAG_NONE; + + /* check args */ + if (g_strv_length (values) < 1) { + g_set_error_literal (error, + G_IO_ERROR, + G_IO_ERROR_FAILED, + "Invalid arguments, expected " + "FILENAME" + " -- e.g. `example.xmlb`"); + return FALSE; + } + + /* don't check the magic to make fuzzing easier */ + if (priv->force) + flags |= XB_SILO_LOAD_FLAG_NO_MAGIC; + + /* load blobs */ + for (guint i = 0; values[i] != NULL; i++) { + g_autofree gchar *str = NULL; + g_autoptr(GFile) file = g_file_new_for_path (values[0]); + g_autoptr(XbSilo) silo = xb_silo_new (); + if (!xb_silo_load_from_file (silo, file, flags, NULL, error)) + return FALSE; + str = xb_silo_to_string (silo, error); + if (str == NULL) + return FALSE; + g_print ("%s", str); + } + return TRUE; +} + +static gboolean +xb_tool_export (XbToolPrivate *priv, gchar **values, GError **error) +{ + XbSiloLoadFlags flags = XB_SILO_LOAD_FLAG_NONE; + + /* check args */ + if (g_strv_length (values) < 1) { + g_set_error_literal (error, + G_IO_ERROR, + G_IO_ERROR_FAILED, + "Invalid arguments, expected " + "FILENAME" + " -- e.g. `example.xmlb`"); + return FALSE; + } + + /* don't check the magic to make fuzzing easier */ + if (priv->force) + flags |= XB_SILO_LOAD_FLAG_NO_MAGIC; + + /* load blobs */ + for (guint i = 0; values[i] != NULL; i++) { + g_autofree gchar *str = NULL; + g_autoptr(GFile) file = g_file_new_for_path (values[0]); + g_autoptr(XbSilo) silo = xb_silo_new (); + if (!xb_silo_load_from_file (silo, file, flags, NULL, error)) + return FALSE; + str = xb_silo_export (silo, + XB_NODE_EXPORT_FLAG_ADD_HEADER | + XB_NODE_EXPORT_FLAG_FORMAT_MULTILINE | + XB_NODE_EXPORT_FLAG_FORMAT_INDENT | + XB_NODE_EXPORT_FLAG_INCLUDE_SIBLINGS, + error); + if (str == NULL) + return FALSE; + g_print ("%s", str); + } + return TRUE; +} + +static gboolean +xb_tool_query (XbToolPrivate *priv, gchar **values, GError **error) +{ + guint limit = 0; + g_autoptr(GFile) file = NULL; + g_autoptr(GPtrArray) results = NULL; + g_autoptr(XbSilo) silo = xb_silo_new (); + + /* check args */ + if (g_strv_length (values) < 2) { + g_set_error_literal (error, + G_IO_ERROR, + G_IO_ERROR_FAILED, + "Invalid arguments, expected " + "FILENAME QUERY [LIMIT]" + " -- e.g. `example.xmlb`"); + return FALSE; + } + + /* load blob */ + file = g_file_new_for_path (values[0]); + if (priv->profile) { + xb_silo_set_profile_flags (silo, + XB_SILO_PROFILE_FLAG_XPATH | + XB_SILO_PROFILE_FLAG_APPEND); + } + if (!xb_silo_load_from_file (silo, file, XB_SILO_LOAD_FLAG_NONE, NULL, error)) + return FALSE; + + /* parse optional limit */ + if (g_strv_length (values) == 3) + limit = g_ascii_strtoull (values[2], NULL, 10); + + /* query */ + results = xb_silo_query (silo, values[1], limit, error); + if (results == NULL) + return FALSE; + for (guint i = 0; i < results->len; i++) { + XbNode *n = g_ptr_array_index (results, i); + g_autofree gchar *xml = NULL; + xml = xb_node_export (n, + XB_NODE_EXPORT_FLAG_FORMAT_MULTILINE | + XB_NODE_EXPORT_FLAG_FORMAT_INDENT, + error); + if (xml == NULL) + return FALSE; + g_print ("RESULT: %s\n", xml); + } + + /* profile */ + if (priv->profile) + g_print ("%s", xb_silo_get_profile_string (silo)); + + return TRUE; +} + +static gboolean +xb_tool_query_file (XbToolPrivate *priv, gchar **values, GError **error) +{ + g_autoptr(GFile) file = NULL; + g_autoptr(XbSilo) silo = xb_silo_new (); + + /* check args */ + if (g_strv_length (values) < 2) { + g_set_error_literal (error, + G_IO_ERROR, + G_IO_ERROR_FAILED, + "Invalid arguments, expected " + "FILENAME FILENAME"); + return FALSE; + } + + /* load blob */ + file = g_file_new_for_path (values[0]); + if (!xb_silo_load_from_file (silo, file, XB_SILO_LOAD_FLAG_NONE, NULL, error)) + return FALSE; + + /* optionally load file */ + for (guint i = 1; values[i] != NULL; i++) { + g_autofree gchar *xpath = NULL; + g_autoptr(GPtrArray) results = NULL; + g_autoptr(GError) error_local = NULL; + + /* load XPath from file */ + if (!g_file_get_contents (values[i], &xpath, NULL, error)) + return FALSE; + g_strdelimit (xpath, "\n", '\0'); + + /* query */ + results = xb_silo_query (silo, xpath, 0, &error_local); + if (results == NULL) { + g_print ("FAILED: %s\n", error_local->message); + continue; + } + for (guint j = 0; j < results->len; j++) { + XbNode *n = g_ptr_array_index (results, j); + g_autofree gchar *xml = NULL; + xml = xb_node_export (n, XB_NODE_EXPORT_FLAG_NONE, error); + if (xml == NULL) + return FALSE; + g_print ("RESULT: %s\n", xml); + } + } + + /* profile */ + if (priv->profile) + g_print ("%s", xb_silo_get_profile_string (silo)); + + return TRUE; +} + +static void +xb_tool_silo_invalidated_cb (XbSilo *silo, GParamSpec *pspec, gpointer user_data) +{ + XbToolPrivate *priv = (XbToolPrivate *) user_data; + g_main_loop_quit (priv->loop); +} + +static gboolean +xb_tool_compile (XbToolPrivate *priv, gchar **values, GError **error) +{ + const gchar *const *locales = g_get_language_names (); + g_autoptr(XbBuilder) builder = xb_builder_new (); + g_autoptr(XbSilo) silo = NULL; + g_autoptr(GFile) file_dst = NULL; + + /* check args */ + if (g_strv_length (values) < 2) { + g_set_error_literal (error, + G_IO_ERROR, + G_IO_ERROR_FAILED, + "Invalid arguments, expected " + "FILE-OUT FILE [FILE]" + " -- e.g. `example.xmlb example.xml`"); + return FALSE; + } + + /* load file */ + for (guint i = 0; locales[i] != NULL; i++) + xb_builder_add_locale (builder, locales[i]); + + for (guint i = 1; values[i] != NULL; i++) { + g_autoptr(GFile) file = g_file_new_for_path (values[i]); + g_autoptr(XbBuilderSource) source = xb_builder_source_new (); + if (!xb_builder_source_load_file (source, file, + XB_BUILDER_SOURCE_FLAG_WATCH_FILE | + XB_BUILDER_SOURCE_FLAG_LITERAL_TEXT, + NULL, error)) + return FALSE; + xb_builder_import_source (builder, source); + } + file_dst = g_file_new_for_path (values[0]); + xb_builder_set_profile_flags (builder, + priv->profile ? XB_SILO_PROFILE_FLAG_APPEND : + XB_SILO_PROFILE_FLAG_NONE); + silo = xb_builder_ensure (builder, file_dst, + XB_BUILDER_COMPILE_FLAG_WATCH_BLOB | + XB_BUILDER_COMPILE_FLAG_IGNORE_INVALID | + XB_BUILDER_COMPILE_FLAG_SINGLE_LANG, + NULL, error); + if (silo == NULL) + return FALSE; + + /* wait for invalidation */ + if (priv->wait) { + g_print ("Waiting for invalidation…\n"); + g_signal_connect (silo, "notify::valid", + G_CALLBACK (xb_tool_silo_invalidated_cb), + priv); + g_main_loop_run (priv->loop); + } + + /* profile */ + if (priv->profile) + g_print ("%s", xb_silo_get_profile_string (silo)); + + /* success */ + return TRUE; +} + +#ifdef HAVE_GIO_UNIX +static gboolean +xb_tool_sigint_cb (gpointer user_data) +{ + XbToolPrivate *priv = (XbToolPrivate *) user_data; + g_debug ("Handling SIGINT"); + g_cancellable_cancel (priv->cancellable); + return FALSE; +} +#endif + +int +main (int argc, char *argv[]) +{ + gboolean ret; + gboolean verbose = FALSE; + g_autofree gchar *cmd_descriptions = NULL; + g_autoptr(XbToolPrivate) priv = g_new0 (XbToolPrivate, 1); + g_autoptr(GError) error = NULL; + g_autoptr(GOptionContext) context = NULL; + const GOptionEntry options[] = { + { "verbose", 'v', 0, G_OPTION_ARG_NONE, &verbose, + "Print verbose debug statements", NULL }, + { "force", 'v', 0, G_OPTION_ARG_NONE, &priv->force, + "Force parsing of invalid files", NULL }, + { "wait", 'w', 0, G_OPTION_ARG_NONE, &priv->wait, + "Return only when the silo is no longer valid", NULL }, + { "profile", 'p', 0, G_OPTION_ARG_NONE, &priv->profile, + "Show profiling information", NULL }, + { NULL} + }; + + /* do not let GIO start a session bus */ + g_setenv ("GIO_USE_VFS", "local", 1); + + /* add commands */ + priv->cmd_array = g_ptr_array_new_with_free_func ((GDestroyNotify) xb_tool_item_free); + xb_tool_add (priv->cmd_array, + "dump", + "XMLBFILE", + /* TRANSLATORS: command description */ + "Dumps a XMLb file", + xb_tool_dump); + xb_tool_add (priv->cmd_array, + "export", + "XMLFILE", + /* TRANSLATORS: command description */ + "Exports a XMLb file", + xb_tool_export); + xb_tool_add (priv->cmd_array, + "query", + "XMLBFILE XPATH [LIMIT]", + /* TRANSLATORS: command description */ + "Queries a XMLb file", + xb_tool_query); + xb_tool_add (priv->cmd_array, + "query-file", + "XMLBFILE [FILE] [FILE]", + /* TRANSLATORS: command description */ + "Queries a XMLb file using an external XPath query", + xb_tool_query_file); + xb_tool_add (priv->cmd_array, + "compile", + "XMLBFILE XMLFILE [XMLFILE]", + /* TRANSLATORS: command description */ + "Compile XML to XMLb", + xb_tool_compile); + + /* do stuff on ctrl+c */ + priv->loop = g_main_loop_new (NULL, FALSE); + priv->cancellable = g_cancellable_new (); + g_signal_connect (priv->cancellable, "cancelled", + G_CALLBACK (xb_tool_cancelled_cb), priv); +#ifdef HAVE_GIO_UNIX + g_unix_signal_add_full (G_PRIORITY_DEFAULT, + SIGINT, xb_tool_sigint_cb, + priv, NULL); +#endif + + /* sort by command name */ + g_ptr_array_sort (priv->cmd_array, + (GCompareFunc) xb_tool_sort_command_name_cb); + + /* get a list of the commands */ + context = g_option_context_new (NULL); + cmd_descriptions = xb_tool_get_descriptions (priv->cmd_array); + g_option_context_set_summary (context, cmd_descriptions); + + /* TRANSLATORS: DFU stands for device firmware update */ + g_set_application_name ("Binary XML Utility"); + g_option_context_add_main_entries (context, options, NULL); + ret = g_option_context_parse (context, &argc, &argv, &error); + if (!ret) { + g_print ("%s: %s\n", "Failed to parse arguments", error->message); + return EXIT_FAILURE; + } + + /* set verbose? */ + if (verbose) + g_setenv ("G_MESSAGES_DEBUG", "all", FALSE); + + /* run the specified command */ + ret = xb_tool_run (priv, argv[1], (gchar**) &argv[2], &error); + if (!ret) { + if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_FAILED)) { + g_autofree gchar *tmp = NULL; + tmp = g_option_context_get_help (context, TRUE, NULL); + g_print ("%s\n\n%s", error->message, tmp); + } else { + g_print ("%s\n", error->message); + } + return EXIT_FAILURE; + } + + /* success/ */ + return EXIT_SUCCESS; +} diff --git a/src/xb-version.h.in b/src/xb-version.h.in new file mode 100644 index 0000000..547e462 --- /dev/null +++ b/src/xb-version.h.in @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2015 Richard Hughes + * + * SPDX-License-Identifier: LGPL-2.1+ + */ +/** + * SECTION:xb-version + * @short_description: Obtains the version for the installed libxmlb + * + * These compile time macros allow the user to enable parts of client code + * depending on the version of libxmlb installed. + */ + +#if !defined (__LIBXMLB_H_INSIDE__) && !defined (LIBXMLB_COMPILATION) +#error "Only can be included directly." +#endif + +#ifndef __LIBXMLB_VERSION_H +#define __LIBXMLB_VERSION_H + +/** + * XMLB_MAJOR_VERSION: + * + * The compile-time major version + */ +#ifndef XMLB_MAJOR_VERSION +#define XMLB_MAJOR_VERSION (@XMLB_MAJOR_VERSION@) +#endif + +/** + * XMLB_MINOR_VERSION: + * + * The compile-time minor version + */ +#ifndef XMLB_MINOR_VERSION +#define XMLB_MINOR_VERSION (@XMLB_MINOR_VERSION@) +#endif + +/** + * XMLB_MICRO_VERSION: + * + * The compile-time micro version + */ +#ifndef XMLB_MICRO_VERSION +#define XMLB_MICRO_VERSION (@XMLB_MICRO_VERSION@) +#endif + +/** + * LIBXMLB_CHECK_VERSION: + * @major: Major version number + * @minor: Minor version number + * @micro: Micro version number + * + * Check whether a libxmlb version equal to or greater than + * major.minor.micro. + */ +#define LIBXMLB_CHECK_VERSION(major,minor,micro) \ + (XMLB_MAJOR_VERSION > (major) || \ + (XMLB_MAJOR_VERSION == (major) && XMLB_MINOR_VERSION > (minor)) || \ + (XMLB_MAJOR_VERSION == (major) && XMLB_MINOR_VERSION == (minor) && \ + XMLB_MICRO_VERSION >= (micro))) + +#endif /* __LIBXMLB_VERSION_H */ diff --git a/src/xmlb.h b/src/xmlb.h new file mode 100644 index 0000000..ff3615f --- /dev/null +++ b/src/xmlb.h @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2018 Richard Hughes + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#pragma once + +#define __LIBXMLB_H_INSIDE__ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#undef __LIBXMLB_H_INSIDE__