pax_global_header00006660000000000000000000000064145141362310014512gustar00rootroot0000000000000052 comment=eb9d5eaf1ac00b446ec13fdcf605959c5d78194b ostinato-1.3.0/000077500000000000000000000000001451413623100133535ustar00rootroot00000000000000ostinato-1.3.0/.github/000077500000000000000000000000001451413623100147135ustar00rootroot00000000000000ostinato-1.3.0/.github/CODE_OF_CONDUCT.md000066400000000000000000000064301451413623100175150ustar00rootroot00000000000000# 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, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, 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 support@ostinato.org. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and 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 https://www.contributor-covenant.org/version/1/4/code-of-conduct.html [homepage]: https://www.contributor-covenant.org For answers to common questions about this code of conduct, see https://www.contributor-covenant.org/faq ostinato-1.3.0/.github/CONTRIBUTING.md000066400000000000000000000020371451413623100171460ustar00rootroot00000000000000# Contributing Guidelines Please use a Pull Request to contribute code. Very small fixes (< 10 lines) can provide the diff via an issue instead of a PR. In either case, you agree to the below legal terms and you indicate your acceptance by explicitly adding a comment to the issue or PR stating - ``` I have read the contributing guidelines (CONTRIBUTING.md) and I hereby assign the copyrights of these changes to [Srivats P](https://github.com/pstavirs). ``` ## Legal By submitting a Pull Request, you disavow any rights or claims to any changes submitted to the Ostinato project and assign the copyright of those changes to [Srivats P](https://github.com/pstavirs). If you cannot or do not want to reassign those rights (your employment contract for your employer may not allow this), you should not submit a PR. Open an issue and someone else can do the work. This is a legal way of saying "_If you submit a PR to us, that code becomes ours_". 99.9% of the time that's what you intend anyways; we hope it doesn't scare you away from contributing. ostinato-1.3.0/.github/FUNDING.yml000066400000000000000000000000211451413623100165210ustar00rootroot00000000000000github: pstavirs ostinato-1.3.0/.gitignore000066400000000000000000000005641451413623100153500ustar00rootroot00000000000000# generated object files *.pyc *.o *.a *.dll *.so *.exe *.app drone ostinato # Qt generated files ui_*.h moc_*.h moc_*.cpp qrc_*.cpp # QMake generated files Makefile* *\object_script.* .qmake.stash # protobuf generated files *.pb.h *.pb.cc *_pb2.py # ostinato generated files version.cpp pkg_info.json # vim swap files *.swp .DS_Store # ctags/cscope tags cscope.out ostinato-1.3.0/.travis.yml000066400000000000000000000027711451413623100154730ustar00rootroot00000000000000language: cpp osx_image: xcode11.3 os: - linux - osx compiler: - gcc - clang matrix: exclude: - os: osx compiler: gcc before_script: - "if [ $TRAVIS_OS_NAME = 'osx' ]; then \ ls -lR /usr/local/include/google/protobuf; \ which clang++; \ clang++ -E -x c++ - -v < /dev/null; \ export CPLUS_INCLUDE_PATH=/usr/local/include; \ export LIBRARY_PATH=/usr/local/lib; \ export PATH=/usr/local/opt/qt/bin:$PATH; \ fi" addons: apt: packages: - qtbase5-dev - qtscript5-dev - libqt5svg5-dev - libpcap-dev - libprotobuf-dev - protobuf-compiler - libnl-3-dev - libnl-route-3-dev homebrew: packages: - qt5 - protobuf script: - QT_SELECT=qt5 qmake -config debug - make notifications: email: - secure: "LUIBAz/phzOdxlnFuoC6hfNfl3HAGYTkacVfLu0GsrOgNBhqPqsRszhdFuX/kEdGb18GIcqCQn4tLfFVD9YyjZKFCTjjfTEaUlbHYrear4VU6HihBvhu4I+F8nffJ9y77kyZhUfC9RdgTwWEVdwYMi3rjIK8UggPVV1s/FyE2t+UAjmzdGPAb6uxu9znYpldKtY9FosqccPe7tB5uLLcdbX6ojvsOH7lyQPLclFFS7F2yM9nNNxnRwl1v4KlN6vYwBF2scKB8altEMEGnLJKB41S6piVWyQlXc8FotGJf4fg/6lwmKWBzT/aIw8UH8cxJW2q3sHmdcf/nBhSKojLN5HNIQOJ45Xfw0MKQy2uXGHG15DzYRKjsw6zdb7oVJJCVDiwh1wDvfExgJwpIZJuNsMaGEbSo/TxN+6iPaCf2iyFaCW/KkBt5gmcgck2dA0Xc8qd0z3Zjii7cvuM8awrsmtE7UuX7M/lV9M8w5EaYjhr0R2NwgqvRa0hQv+1Ycec/1eVVfMCMFWNsxmVSkQFFbFP2db195axMzeU13G8RIbPY3JsIUEGw1ykj4nXiH77CLYRfZuKvomjhBkzE+P+EyIxRMH65M4zO9lO7SW01hrHty60jKajt5ljPI5AfuKAFHQvoLOwEMwLwbtEy/ggI/SRAXjsKJbueTHaI4719Gg=" ostinato-1.3.0/.vimrc000066400000000000000000000001161451413623100144720ustar00rootroot00000000000000set shiftwidth=4 set tabstop=8 set softtabstop=4 set expandtab set cindent ostinato-1.3.0/COPYING000066400000000000000000001045131451413623100144120ustar00rootroot00000000000000 GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Use with the GNU Affero General Public License. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: Copyright (C) This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an "about box". You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see . The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read . ostinato-1.3.0/README.md000066400000000000000000000027531451413623100146410ustar00rootroot00000000000000# Ostinato [![Build Status](https://app.travis-ci.com/pstavirs/ostinato.svg?branch=master)](https://app.travis-ci.com/pstavirs/ostinato) This is the code repository for the Ostinato network packet crafter and traffic generator Visit https://ostinato.org for demo video and details Source License: GPLv3+ (see [COPYING](https://raw.githubusercontent.com/pstavirs/ostinato/master/COPYING)) ## Author's note I have been developing and maintaining Ostinato [single-handedly](https://github.com/pstavirs/ostinato/graphs/contributors) for more than 12 years. And during this time I have grudgingly come around to the view that open source cannot survive and thrive without money. Mixing money with open-source is messy, but I don't see a way forward unless we as a community become open to the idea of talking about it and changing our culture so that money is no longer a bad word. I sell binary licenses on [ostinato.org](https://ostinato.org/downloads) to try and cover the costs of development. Please consider buying those - they are priced low enough that you can afford it or you could just as easily expense them to your organisation. If you build Ostinato from source and find it useful, please sponsor to keep the lights on and sustain the project. [![](https://img.shields.io/static/v1?label=Sponsor&message=%E2%9D%A4&logo=GitHub&color=%23fe8e86&style=for-the-badge)](https://github.com/sponsors/pstavirs) Read the Ostinato [origin story](https://ostinato.org/about). Srivats P
Author, Ostinato ostinato-1.3.0/client/000077500000000000000000000000001451413623100146315ustar00rootroot00000000000000ostinato-1.3.0/client/about.ui000066400000000000000000000152661451413623100163140ustar00rootroot00000000000000 About 0 0 500 327 0 0 About Ostinato 0 Ostinato 0 0 :/icons/logo.png false Qt::AlignCenter Qt::Vertical 20 21 :/icons/name.png Qt::AlignCenter Version/Revision Placeholder Qt::AlignCenter Copyright (c) 2007-2023 Srivats P. Qt::AlignCenter <a href="https://ostinato.org">ostinato.org</a><br><a href="https://twitter.com/ostinato">@ostinato</a> Qt::RichText Qt::AlignCenter true Qt::Vertical 20 21 Logo (c): Dhiman Sengupta Icons (c): Mark James (http://www.famfamfam.com/lab/icons/silk/) Qt::AlignCenter License <p>This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.</p><p>This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.</p><p>You should have received a copy of the GNU General Public License along with this program. If not, see <a href="https://www.gnu.org/licenses/">https://www.gnu.org/licenses/</a></p> Qt::RichText Qt::AlignCenter true true Qt::Horizontal QDialogButtonBox::Ok buttonBox accepted() About accept() 353 280 286 262 ostinato-1.3.0/client/applymsg.h000066400000000000000000000042121451413623100166350ustar00rootroot00000000000000/* Copyright (C) 2019 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _APPLY_MESSAGE_H #define _APPLY_MESSAGE_H #include #include #include #include extern QMainWindow *mainWindow; class ApplyMessage: public QDialog { public: ApplyMessage(QWidget *parent = Q_NULLPTR); public slots: void show(); virtual void done(int r); private: QLabel *help_; QTimer *helpTimer_; }; ApplyMessage::ApplyMessage(QWidget *parent) : QDialog(parent) { auto layout = new QVBoxLayout(this); auto message = new QLabel(tr("Pushing configuration changes to agent " "and re-building packets ..."), this); auto progress = new QProgressBar(this); progress->setRange(0, 0); progress->setTextVisible(false); help_ = new QLabel(tr("This may take some time"), this); help_->setAlignment(Qt::AlignCenter); layout->addWidget(message); layout->addWidget(progress); layout->addWidget(help_); helpTimer_ = new QTimer(this); helpTimer_->setSingleShot(true); helpTimer_->setInterval(4000); connect(helpTimer_, SIGNAL(timeout()), help_, SLOT(show())); } void ApplyMessage::show() { help_->hide(); // shown when helpTimer_ expires QWidget *parent = parentWidget(); if (!parent) parent = mainWindow; move(parent->frameGeometry().center() - rect().center()); setModal(true); QDialog::show(); helpTimer_->start(); } void ApplyMessage::done(int r) { helpTimer_->stop(); QDialog::done(r); } #endif ostinato-1.3.0/client/arpstatusmodel.cpp000066400000000000000000000074501451413623100204120ustar00rootroot00000000000000/* Copyright (C) 2016 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #include "arpstatusmodel.h" #include "port.h" #include "emulproto.pb.h" #include enum { kIp4Address, kMacAddress, kStatus, kFieldCount }; static QStringList columns_ = QStringList() << "IPv4 Address" << "Mac Address" << "Status"; ArpStatusModel::ArpStatusModel(QObject *parent) : QAbstractTableModel(parent) { port_ = NULL; deviceIndex_ = -1; neighbors_ = NULL; } int ArpStatusModel::rowCount(const QModelIndex &parent) const { if (!port_ || deviceIndex_ < 0 || parent.isValid()) return 0; return port_->numArp(deviceIndex_); } int ArpStatusModel::columnCount(const QModelIndex &parent) const { if (parent.isValid()) return 0; return columns_.size(); } QVariant ArpStatusModel::headerData( int section, Qt::Orientation orientation, int role) const { if (role != Qt::DisplayRole) return QVariant(); switch (orientation) { case Qt::Horizontal: return columns_[section]; case Qt::Vertical: return QString("%1").arg(section + 1); default: Q_ASSERT(false); // Unreachable } return QVariant(); } QVariant ArpStatusModel::data(const QModelIndex &index, int role) const { QString str; if (!port_ || deviceIndex_ < 0 || !index.isValid()) return QVariant(); int arpIdx = index.row(); int field = index.column(); Q_ASSERT(arpIdx < port_->numArp(deviceIndex_)); Q_ASSERT(field < kFieldCount); const OstEmul::ArpEntry &arp = neighbors_->arp(arpIdx); switch (field) { case kIp4Address: switch (role) { case Qt::DisplayRole: return QHostAddress(arp.ip4()).toString(); default: break; } return QVariant(); case kMacAddress: switch (role) { case Qt::DisplayRole: return QString("%1").arg(arp.mac(), 6*2, 16, QChar('0')) .replace(QRegExp("([0-9a-fA-F]{2}\\B)"), "\\1:") .toUpper(); default: break; } return QVariant(); case kStatus: switch (role) { case Qt::DisplayRole: return arp.mac() ? QString("Resolved") : QString("Failed"); default: break; } return QVariant(); default: Q_ASSERT(false); // unreachable! break; } qWarning("%s: Unsupported field #%d", __FUNCTION__, field); return QVariant(); } Qt::DropActions ArpStatusModel::supportedDropActions() const { return Qt::IgnoreAction; // read-only model, doesn't accept any data } void ArpStatusModel::setDeviceIndex(Port *port, int deviceIndex) { beginResetModel(); port_ = port; deviceIndex_ = deviceIndex; if (port_) neighbors_ = port_->deviceNeighbors(deviceIndex); endResetModel(); } void ArpStatusModel::updateArpStatus() { // FIXME: why needed? beginResetModel(); endResetModel(); } ostinato-1.3.0/client/arpstatusmodel.h000066400000000000000000000027531451413623100200600ustar00rootroot00000000000000/* Copyright (C) 2016 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _ARP_STATUS_MODEL_H #define _ARP_STATUS_MODEL_H #include class Port; namespace OstEmul { class DeviceNeighborList; } class ArpStatusModel: public QAbstractTableModel { Q_OBJECT public: ArpStatusModel(QObject *parent = 0); int rowCount(const QModelIndex &parent = QModelIndex()) const; int columnCount(const QModelIndex &parent = QModelIndex()) const; QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const; QVariant data(const QModelIndex &index, int role) const; Qt::DropActions supportedDropActions() const; void setDeviceIndex(Port *port, int deviceIndex); public slots: void updateArpStatus(); private: Port *port_; int deviceIndex_; const OstEmul::DeviceNeighborList *neighbors_; }; #endif ostinato-1.3.0/client/clipboardhelper.cpp000066400000000000000000000166171451413623100205070ustar00rootroot00000000000000/* Copyright (C) 2020 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #include "clipboardhelper.h" #include "xtableview.h" #include #include #include #include #include #if 0 // change 0 to 1 for debug #define xDebug(...) qDebug(__VA_ARGS__) #else #define xDebug(...) #endif ClipboardHelper::ClipboardHelper(QObject *parent) : QObject(parent) { actionCut_ = new QAction(tr("&Cut"), this); actionCut_->setObjectName(QStringLiteral("actionCut")); actionCut_->setIcon(QIcon(QString::fromUtf8(":/icons/cut.png"))); actionCopy_ = new QAction(tr("Cop&y"), this); actionCopy_->setObjectName(QStringLiteral("actionCopy")); actionCopy_->setIcon(QIcon(QString::fromUtf8(":/icons/copy.png"))); actionPaste_ = new QAction(tr("&Paste"), this); actionPaste_->setObjectName(QStringLiteral("actionPaste")); actionPaste_->setIcon(QIcon(QString::fromUtf8(":/icons/paste.png"))); connect(actionCut_, SIGNAL(triggered()), SLOT(actionTriggered())); connect(actionCopy_, SIGNAL(triggered()), SLOT(actionTriggered())); connect(actionPaste_, SIGNAL(triggered()), SLOT(actionTriggered())); // XXX: failsafe in case updation of cut/copy/status causes issues // Temporary for 1 release - will be removed after that if (qEnvironmentVariableIsSet("X-OSTINATO-CCP-STATUS")) { qWarning("FAILSAFE: Cut-Copy-Paste action status will not be updated"); return; } connect(qApp, SIGNAL(focusChanged(QWidget*, QWidget*)), SLOT(updateCutCopyStatus(QWidget*, QWidget*))); connect(qApp, SIGNAL(focusChanged(QWidget*, QWidget*)), SLOT(updatePasteStatus())); connect(QGuiApplication::clipboard(), SIGNAL(dataChanged()), SLOT(updatePasteStatus())); } QList ClipboardHelper::actions() { QList actionList({actionCut_, actionCopy_, actionPaste_}); return actionList; } void ClipboardHelper::actionTriggered() { QWidget *focusWidget = qApp->focusWidget(); if (!focusWidget) return; // single slot to handle cut/copy/paste - find which action was triggered QString action = sender()->objectName() .remove("action").append("()").toLower(); if (focusWidget->metaObject()->indexOfSlot(qPrintable(action)) < 0) { xDebug("%s slot not found for object %s:%s ", qPrintable(action), qPrintable(focusWidget->objectName()), focusWidget->metaObject()->className()); return; } action.remove("()"); QMetaObject::invokeMethod(focusWidget, qPrintable(action), Qt::DirectConnection); } void ClipboardHelper::updateCutCopyStatus(QWidget *old, QWidget *now) { xDebug("In %s", __FUNCTION__); const XTableView *view = dynamic_cast(old); if (view) { disconnect(view->selectionModel(), SIGNAL(selectionChanged(const QItemSelection&, const QItemSelection&)), this, SLOT(focusWidgetSelectionChanged(const QItemSelection&, const QItemSelection&))); disconnect(view->model(), SIGNAL(modelReset()), this, SLOT(focusWidgetModelReset())); } if (!now) { xDebug("No focus widget to copy from"); actionCut_->setEnabled(false); actionCopy_->setEnabled(false); return; } const QMetaObject *meta = now->metaObject(); if (meta->indexOfSlot("copy()") < 0) { xDebug("Focus Widget (%s) doesn't have a copy slot", qPrintable(now->objectName())); actionCut_->setEnabled(false); actionCopy_->setEnabled(false); return; } view = dynamic_cast(now); if (view) { connect(view->selectionModel(), SIGNAL(selectionChanged(const QItemSelection&, const QItemSelection&)), SLOT(focusWidgetSelectionChanged(const QItemSelection&, const QItemSelection&))); connect(view->model(), SIGNAL(modelReset()), this, SLOT(focusWidgetModelReset())); if (!view->hasSelection()) { xDebug("%s doesn't have anything selected to copy", qPrintable(view->objectName())); actionCut_->setEnabled(false); actionCopy_->setEnabled(false); return; } xDebug("%s model can cut: %d", qPrintable(view->objectName()), view->canCut()); actionCut_->setEnabled(view->canCut()); } xDebug("%s has a selection and copy slot: copy possible", qPrintable(now->objectName())); actionCopy_->setEnabled(true); } void ClipboardHelper::focusWidgetSelectionChanged( const QItemSelection &selected, const QItemSelection &/*deselected*/) { xDebug("In %s", __FUNCTION__); // Selection changed in the XTableView that has focus const XTableView *view = dynamic_cast(qApp->focusWidget()); xDebug("canCut:%d empty:%d", view->canCut(), selected.indexes().isEmpty()); actionCut_->setEnabled(!selected.indexes().isEmpty() && view && view->canCut()); actionCopy_->setEnabled(!selected.indexes().isEmpty()); } void ClipboardHelper::updatePasteStatus() { xDebug("In %s", __FUNCTION__); QWidget *focusWidget = qApp->focusWidget(); if (!focusWidget) { xDebug("No focus widget to paste into"); actionPaste_->setEnabled(false); return; } const QMimeData *item = QGuiApplication::clipboard()->mimeData(); if (!item || item->formats().isEmpty()) { xDebug("Nothing on clipboard to paste"); actionPaste_->setEnabled(false); return; } const QMetaObject *meta = focusWidget->metaObject(); if (meta->indexOfSlot("paste()") < 0) { xDebug("Focus Widget (%s) doesn't have a paste slot", qPrintable(focusWidget->objectName())); actionPaste_->setEnabled(false); return; } const XTableView *view = dynamic_cast(focusWidget); if (view && !view->canPaste(item)) { xDebug("%s cannot accept this item (%s)", qPrintable(view->objectName()), qPrintable(item->formats().join("|"))); actionPaste_->setEnabled(false); return; } xDebug("%s can accept this item (%s): paste possible", qPrintable(focusWidget->objectName()), qPrintable(item->formats().join("|"))); actionPaste_->setEnabled(true); } void ClipboardHelper::focusWidgetModelReset() { xDebug("In %s", __FUNCTION__); QWidget *focusWidget = qApp->focusWidget(); updateCutCopyStatus(focusWidget, focusWidget); // re-eval cut/copy status } #undef xDebug ostinato-1.3.0/client/clipboardhelper.h000066400000000000000000000025431451413623100201450ustar00rootroot00000000000000/* Copyright (C) 2020 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _CLIPBOARD_HELPER_H #define _CLIPBOARD_HELPER_H #include #include class QAction; class QItemSelection; class ClipboardHelper : public QObject { Q_OBJECT public: ClipboardHelper(QObject *parent=nullptr); QList actions(); private slots: void actionTriggered(); void updateCutCopyStatus(QWidget *old, QWidget *now); void focusWidgetSelectionChanged(const QItemSelection &selected, const QItemSelection &deselected); void focusWidgetModelReset(); void updatePasteStatus(); private: QAction *actionCut_{nullptr}; QAction *actionCopy_{nullptr}; QAction *actionPaste_{nullptr}; }; #endif ostinato-1.3.0/client/devicegroupdialog.cpp000066400000000000000000000275761451413623100210520ustar00rootroot00000000000000/* Copyright (C) 2016 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #include "devicegroupdialog.h" #include "port.h" #include "spinboxdelegate.h" #include "emulproto.pb.h" #include "uint128.h" #include #include enum { kVlanId, kVlanCount, kVlanStep, kVlanCfi, kVlanPrio, kVlanTpid, kVlanColumns }; static QStringList vlanTableColumnHeaders = QStringList() << "Vlan Id" << "Count" << "Step" << "CFI/DE" << "Prio" << "TPID"; enum { kIpNone, kIp4, kIp6, kIpDual }; static QStringList ipStackItems = QStringList() << "None" << "IPv4" << "IPv6" << "Dual"; inline UInt128 UINT128(OstEmul::Ip6Address x) { return UInt128(x.hi(), x.lo()); } inline OstEmul::Ip6Address IP6ADDR(UInt128 x) { OstEmul::Ip6Address ip; ip.set_hi(x.hi64()); ip.set_lo(x.lo64()); return ip; } DeviceGroupDialog::DeviceGroupDialog( Port *port, int deviceGroupIndex, QWidget *parent, Qt::WindowFlags flags) : QDialog(parent, flags), port_(port), index_(deviceGroupIndex) { // Setup the Dialog setupUi(this); vlanTagCount->setRange(0, kMaxVlanTags); // Populate the Vlan Table with placeholders - we do this so that // user entered values are retained during the lifetime of the dialog // even if user is playing around with number of vlan tags vlans->setRowCount(kMaxVlanTags); vlans->setColumnCount(kVlanColumns); vlans->setHorizontalHeaderLabels(vlanTableColumnHeaders); for (int i = 0; i < kMaxVlanTags; i++) { // Use same default values as defined in .proto vlans->setItem(i, kVlanId, new QTableWidgetItem(QString::number(100*(i+1)))); vlans->setItem(i, kVlanCount, new QTableWidgetItem(QString::number(1))); vlans->setItem(i, kVlanStep, new QTableWidgetItem(QString::number(1))); vlans->setItem(i, kVlanCfi, new QTableWidgetItem(QString::number(0))); vlans->setItem(i, kVlanPrio, new QTableWidgetItem(QString::number(0))); vlans->setItem(i, kVlanTpid, new QTableWidgetItem(QString("0x8100"))); } // Set SpinBoxDelegate for all columns except TPID SpinBoxDelegate *spd = new SpinBoxDelegate(this); spd->setColumnRange(kVlanId, 0, 4095); spd->setColumnRange(kVlanStep, 0, 4095); spd->setColumnRange(kVlanCfi, 0, 1); spd->setColumnRange(kVlanPrio, 0, 7); for (int i = 0; i < kVlanColumns; i++) { if (i != kVlanTpid) vlans->setItemDelegateForColumn(i, spd); } vlans->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch); vlans->resizeRowsToContents(); // Set vlan tag count *after* adding all items, so connected slots // can access the items vlanTagCount->setValue(kMaxVlanTags); devicePerVlanCount->setRange(1, 0x7fffffff); ipStack->insertItems(0, ipStackItems); // TODO: DeviceGroup Traversal; hide buttons for now // NOTE for implementation: Use a QHash // to store modified values while traversing; in accept() // update port->deviceGroups[] from the QHash prev->setHidden(true); next->setHidden(true); // TODO: Preview devices expanded from deviceGroup configuration // for user convenience // setup dialog to auto-resize as widgets are hidden or shown layout()->setSizeConstraint(QLayout::SetFixedSize); connect(devicePerVlanCount, SIGNAL(valueChanged(const QString&)), this, SLOT(updateTotalDeviceCount())); connect(ip4Address, SIGNAL(textEdited(const QString&)), this, SLOT(updateIp4Gateway())); connect(ip4PrefixLength, SIGNAL(valueChanged(const QString&)), this, SLOT(updateIp4Gateway())); connect(ip6Address, SIGNAL(textEdited(const QString&)), this, SLOT(updateIp6Gateway())); connect(ip6PrefixLength, SIGNAL(valueChanged(const QString&)), this, SLOT(updateIp6Gateway())); loadDeviceGroup(); } void DeviceGroupDialog::accept() { storeDeviceGroup(); QDialog::accept(); } // // Private Slots // void DeviceGroupDialog::on_vlanTagCount_valueChanged(int value) { Q_ASSERT((value >= 0) && (value <= kMaxVlanTags)); for (int row = 0; row < kMaxVlanTags; row++) vlans->setRowHidden(row, row >= value); vlans->setVisible(value > 0); updateTotalVlanCount(); } void DeviceGroupDialog::on_vlans_cellChanged(int row, int col) { if (col != kVlanCount) return; if (vlans->isRowHidden(row)) return; updateTotalVlanCount(); } void DeviceGroupDialog::on_ipStack_currentIndexChanged(int index) { switch (index) { case kIpNone: ip4->hide(); ip6->hide(); break; case kIp4: ip4->show(); ip6->hide(); break; case kIp6: ip4->hide(); ip6->show(); break; case kIpDual: ip4->show(); ip6->show(); break; default: Q_ASSERT(false); // Unreachable! break; } } void DeviceGroupDialog::updateTotalVlanCount() { int count = vlanTagCount->value() ? 1 : 0; for (int i = 0; i < vlanTagCount->value(); i++) count *= vlans->item(i, kVlanCount)->text().toUInt(); vlanCount->setValue(count); updateTotalDeviceCount(); } void DeviceGroupDialog::updateTotalDeviceCount() { totalDeviceCount->setValue(qMax(vlanCount->value(), 1) * devicePerVlanCount->value()); } void DeviceGroupDialog::updateIp4Gateway() { quint32 net = ip4Address->value() & (~0UL << (32 - ip4PrefixLength->value())); ip4Gateway->setValue(net | 0x01); } void DeviceGroupDialog::updateIp6Gateway() { UInt128 net = ip6Address->value() & (~UInt128(0, 0) << (128 - ip6PrefixLength->value())); ip6Gateway->setValue(net | UInt128(0, 1)); } void DeviceGroupDialog::loadDeviceGroup() { const OstProto::DeviceGroup *devGrp = port_->deviceGroupByIndex(index_); int tagCount = 0; // use 1-indexed id so that it matches the port id used in the // RFC 4814 compliant mac addresses assigned by default to deviceGroups // XXX: use deviceGroupId also as part of the id? quint32 id = (port_->id()+1) & 0xff; Q_ASSERT(devGrp); name->setText(QString::fromStdString(devGrp->core().name())); if (devGrp->has_encap() && devGrp->encap().HasExtension(OstEmul::vlan)) { OstEmul::VlanEmulation vlan = devGrp->encap() .GetExtension(OstEmul::vlan); tagCount = vlan.stack_size(); for (int i = 0; i < tagCount; i++) { OstEmul::VlanEmulation::Vlan v = vlan.stack(i); vlans->item(i, kVlanPrio)->setText( QString::number((v.vlan_tag() >> 13) & 0x7)); vlans->item(i, kVlanCfi)->setText( QString::number((v.vlan_tag() >> 12) & 0x1)); vlans->item(i, kVlanId)->setText( QString::number(v.vlan_tag() & 0x0fff)); vlans->item(i, kVlanCount)->setText(QString::number(v.count())); vlans->item(i, kVlanStep)->setText(QString::number(v.step())); vlans->item(i, kVlanTpid)->setText(QString("0x%1") .arg(v.tpid(), 0, 16)); } } vlanTagCount->setValue(tagCount); updateTotalVlanCount(); devicePerVlanCount->setValue(devGrp->device_count()); OstEmul::MacEmulation mac = devGrp->GetExtension(OstEmul::mac); Q_ASSERT(mac.has_address()); macAddress->setValue(mac.address()); macStep->setValue(mac.step()); OstEmul::Ip4Emulation ip4 = devGrp->GetExtension(OstEmul::ip4); // If address is not set, assign one from RFC 2544 space - 192.18.0.0/15 // Use port Id as the 3rd octet of the address ip4Address->setValue(ip4.has_address() ? ip4.address() : 0xc6120002 | (id << 8)); ip4PrefixLength->setValue(ip4.prefix_length()); ip4Step->setValue(ip4.has_step()? ip4.step() : 1); ip4Gateway->setValue(ip4.has_default_gateway() ? ip4.default_gateway() : 0xc6120001 | (id << 8)); OstEmul::Ip6Emulation ip6 = devGrp->GetExtension(OstEmul::ip6); // If address is not set, assign one from RFC 5180 space 2001:0200::/64 // Use port Id as the 3rd hextet of the address ip6Address->setValue(ip6.has_address() ? UINT128(ip6.address()) : UInt128((0x200102000000ULL | id) << 16, 2)); ip6PrefixLength->setValue(ip6.prefix_length()); ip6Step->setValue(ip6.has_step() ? UINT128(ip6.step()) : UInt128(0, 1)); ip6Gateway->setValue(ip6.has_default_gateway() ? UINT128(ip6.default_gateway()) : UInt128((0x200102000000ULL | id) << 16, 1)); int stk = kIpNone; if (devGrp->HasExtension(OstEmul::ip4)) if (devGrp->HasExtension(OstEmul::ip6)) stk = kIpDual; else stk = kIp4; else if (devGrp->HasExtension(OstEmul::ip6)) stk = kIp6; ipStack->setCurrentIndex(stk); } void DeviceGroupDialog::storeDeviceGroup() { OstProto::DeviceGroup *devGrp = port_->mutableDeviceGroupByIndex(index_); int tagCount = vlanTagCount->value(); Q_ASSERT(devGrp); devGrp->mutable_core()->set_name(name->text().toStdString()); OstEmul::VlanEmulation *vlan = devGrp->mutable_encap() ->MutableExtension(OstEmul::vlan); vlan->clear_stack(); for (int i = 0; i < tagCount; i++) { OstEmul::VlanEmulation::Vlan *v = vlan->add_stack(); v->set_vlan_tag( vlans->item(i, kVlanPrio)->text().toUInt() << 13 | vlans->item(i, kVlanCfi)->text().toUInt() << 12 | vlans->item(i, kVlanId)->text().toUInt()); v->set_count(vlans->item(i, kVlanCount)->text().toUInt()); v->set_step(vlans->item(i, kVlanStep)->text().toUInt()); v->set_tpid(vlans->item(i, kVlanTpid)->text().toUInt(NULL, 16)); } if (!tagCount) devGrp->clear_encap(); devGrp->set_device_count(devicePerVlanCount->value()); OstEmul::MacEmulation *mac = devGrp->MutableExtension(OstEmul::mac); mac->set_address(macAddress->value()); mac->set_step(macStep->value()); if (ipStack->currentIndex() == kIp4 || ipStack->currentIndex() == kIpDual) { OstEmul::Ip4Emulation *ip4 = devGrp->MutableExtension(OstEmul::ip4); ip4->set_address(ip4Address->value()); ip4->set_prefix_length(ip4PrefixLength->value()); ip4->set_default_gateway(ip4Gateway->value()); ip4->set_step(ip4Step->value()); if (ipStack->currentIndex() == kIp4) devGrp->ClearExtension(OstEmul::ip6); } if (ipStack->currentIndex() == kIp6 || ipStack->currentIndex() == kIpDual) { OstEmul::Ip6Emulation *ip6 = devGrp->MutableExtension(OstEmul::ip6); ip6->mutable_address()->CopyFrom(IP6ADDR(ip6Address->value())); ip6->set_prefix_length(ip6PrefixLength->value()); ip6->mutable_step()->CopyFrom(IP6ADDR(ip6Step->value())); ip6->mutable_default_gateway()->CopyFrom(IP6ADDR(ip6Gateway->value())); if (ipStack->currentIndex() == kIp6) devGrp->ClearExtension(OstEmul::ip4); } if (ipStack->currentIndex() == kIpNone) { devGrp->ClearExtension(OstEmul::ip4); devGrp->ClearExtension(OstEmul::ip6); } } ostinato-1.3.0/client/devicegroupdialog.h000066400000000000000000000027231451413623100205020ustar00rootroot00000000000000/* Copyright (C) 2016 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _DEVICE_GROUP_DIALOG_H #define _DEVICE_GROUP_DIALOG_H #include "ui_devicegroupdialog.h" #include class Port; class DeviceGroupDialog: public QDialog, private Ui::DeviceGroupDialog { Q_OBJECT public: DeviceGroupDialog(Port *port, int deviceGroupIndex, QWidget *parent = NULL, Qt::WindowFlags flags = 0); virtual void accept(); private slots: void on_vlanTagCount_valueChanged(int value); void on_vlans_cellChanged(int row, int col); void on_ipStack_currentIndexChanged(int index); void updateTotalVlanCount(); void updateTotalDeviceCount(); void updateIp4Gateway(); void updateIp6Gateway(); void loadDeviceGroup(); void storeDeviceGroup(); private: static const int kMaxVlanTags = 4; Port *port_; int index_; }; #endif ostinato-1.3.0/client/devicegroupdialog.ui000066400000000000000000000236641451413623100206770ustar00rootroot00000000000000 DeviceGroupDialog 0 0 504 465 Devices Name Vlan Tags Qt::Horizontal 40 20 Total Vlans false Devices Per Vlan devicePerVlanCount Total Devices false Mac Address Step IP Stack QFrame::Box QFrame::Plain IPv4 Address 0 0 / Step Gateway 1 32 QFrame::Box QFrame::Plain IPv6 Address 0 0 / Step Gateway 1 128 < > Qt::Horizontal 40 20 Qt::Horizontal QDialogButtonBox::Cancel|QDialogButtonBox::NoButton|QDialogButtonBox::Ok MacEdit QLineEdit
macedit.h
Ip4Edit QLineEdit
ip4edit.h
Ip6Edit QLineEdit
ip6edit.h
IntEdit QSpinBox
intedit.h
name vlanTagCount vlans devicePerVlanCount macAddress macStep ipStack ip4Address ip4PrefixLength ip4Step ip4Gateway ip6Address ip6PrefixLength ip6Step ip6Gateway prev next buttonBox buttonBox accepted() DeviceGroupDialog accept() 227 289 157 274 buttonBox rejected() DeviceGroupDialog reject() 295 295 286 274
ostinato-1.3.0/client/devicegroupmodel.cpp000066400000000000000000000234671451413623100207060ustar00rootroot00000000000000/* Copyright (C) 2016 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #include "devicegroupmodel.h" #include "port.h" #include "emulproto.pb.h" #include "uint128.h" #include #include const QLatin1String kDeviceGroupsMimeType( "application/vnd.ostinato.devicegroups"); enum { kName, kVlanCount, kDeviceCount, // Across all vlans kIp, kIp4Address, kIp6Address, kFieldCount }; static QStringList columns_ = QStringList() << "Name" << "Vlans" << "Devices" << "IP Stack" << "IPv4 Address" << "IPv6 Address"; DeviceGroupModel::DeviceGroupModel(QObject *parent) : QAbstractTableModel(parent) { port_ = NULL; } int DeviceGroupModel::rowCount(const QModelIndex &parent) const { if (!port_ || parent.isValid()) return 0; return port_->numDeviceGroups(); } int DeviceGroupModel::columnCount(const QModelIndex &parent) const { if (parent.isValid()) return 0; return columns_.size(); } QVariant DeviceGroupModel::headerData( int section, Qt::Orientation orientation, int role) const { if (role != Qt::DisplayRole) return QVariant(); switch (orientation) { case Qt::Horizontal: return columns_[section]; case Qt::Vertical: return QString("%1").arg(section + 1); default: Q_ASSERT(false); // Unreachable } return QVariant(); } QVariant DeviceGroupModel::data(const QModelIndex &index, int role) const { if (!port_ || !index.isValid()) return QVariant(); int dgIdx = index.row(); int field = index.column(); Q_ASSERT(dgIdx < port_->numDeviceGroups()); Q_ASSERT(field < kFieldCount); const OstProto::DeviceGroup *devGrp = port_->deviceGroupByIndex(dgIdx); Q_ASSERT(devGrp); switch (field) { case kName: switch (role) { case Qt::DisplayRole: return QString::fromStdString(devGrp->core().name()); default: break; } return QVariant(); case kVlanCount: switch (role) { case Qt::DisplayRole: if (int v = vlanCount(devGrp)) return v; return QString("None"); case Qt::TextAlignmentRole: return static_cast(Qt::AlignRight|Qt::AlignVCenter); default: break; } return QVariant(); case kDeviceCount: switch (role) { case Qt::DisplayRole: return qMax(vlanCount(devGrp), 1)*devGrp->device_count(); case Qt::TextAlignmentRole: return static_cast(Qt::AlignRight|Qt::AlignVCenter); default: break; } return QVariant(); case kIp: switch (role) { case Qt::DisplayRole: if (devGrp->HasExtension(OstEmul::ip4)) if (devGrp->HasExtension(OstEmul::ip6)) return QString("Dual Stack"); else return QString("IPv4"); else if (devGrp->HasExtension(OstEmul::ip6)) return QString("IPv6"); else return QString("None"); default: break; } return QVariant(); case kIp4Address: switch (role) { case Qt::DisplayRole: if (devGrp->HasExtension(OstEmul::ip4)) return QHostAddress( devGrp->GetExtension(OstEmul::ip4) .address()).toString(); else return QString("--"); default: break; } return QVariant(); case kIp6Address: switch (role) { case Qt::DisplayRole: if (devGrp->HasExtension(OstEmul::ip6)) { OstEmul::Ip6Address ip = devGrp->GetExtension( OstEmul::ip6).address(); return QHostAddress( UInt128(ip.hi(), ip.lo()).toArray()) .toString(); } else return QString("--"); default: break; } return QVariant(); default: Q_ASSERT(false); // unreachable! break; } qWarning("%s: Unsupported field #%d", __FUNCTION__, field); return QVariant(); } bool DeviceGroupModel::setData( const QModelIndex & /*index*/, const QVariant & /*value*/, int /*role*/) { if (!port_) return false; // TODO; when implementing also implement flags() to // return ItemIsEditable return false; } QStringList DeviceGroupModel::mimeTypes() const { return QStringList() << kDeviceGroupsMimeType; } QMimeData* DeviceGroupModel::mimeData(const QModelIndexList &indexes) const { using ::google::protobuf::uint8; if (indexes.isEmpty()) return nullptr; // indexes may include multiple columns for a row - but we are only // interested in rows 'coz we have a single data for all columns // XXX: use QMap instead of QSet to keep rows in sorted order QMap rows; foreach(QModelIndex index, indexes) rows.insert(index.row(), index.row()); OstProto::DeviceGroupConfigList dgList; dgList.mutable_port_id()->set_id(port_->id()); foreach(int row, rows) { OstProto::DeviceGroup *devGrp = dgList.add_device_group(); devGrp->CopyFrom(*port_->deviceGroupByIndex(row)); } QByteArray data; data.resize(dgList.ByteSize()); dgList.SerializeWithCachedSizesToArray((uint8*)data.data()); //qDebug("copy %s", dgList.DebugString().c_str()); //TODO: copy DebugString as text/plain? QMimeData *mimeData = new QMimeData(); mimeData->setData(kDeviceGroupsMimeType, data); return mimeData; // XXX: caller is expected to take ownership and free! } bool DeviceGroupModel::dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int /*column*/, const QModelIndex &parent) { if (!data) return false; if (!data->hasFormat(kDeviceGroupsMimeType)) return false; if (action != Qt::CopyAction) return false; OstProto::DeviceGroupConfigList dgList; QByteArray ba(data->data(kDeviceGroupsMimeType)); dgList.ParseFromArray((void*)ba.constData(), ba.size()); //qDebug("paste %s", dgList.DebugString().c_str()); if ((row < 0) || (row > rowCount(parent))) row = rowCount(parent); // Delete rows that we are going to overwrite int c = 0, count = dgList.device_group_size(); if (row < rowCount(parent)) removeRows(row, qMin(rowCount() - row, count)); beginInsertRows(parent, row, row+count-1); for (int i = 0; i < count; i++) { if (port_->newDeviceGroupAt(row+i, &dgList.device_group(i))) c++; } endInsertRows(); if (c != count) { qWarning("failed to copy rows in DeviceGroupModel at row %d; " "requested = %d, actual = %d", row, count, c); return false; } return true; } bool DeviceGroupModel::insertRows( int row, int count, const QModelIndex &parent) { int c = 0; Q_ASSERT(!parent.isValid()); beginInsertRows(parent, row, row+count-1); for (int i = 0; i < count; i++) { if (port_->newDeviceGroupAt(row)) c++; } endInsertRows(); if (c != count) { qWarning("failed to insert rows in DeviceGroupModel at row %d; " "requested = %d, actual = %d", row, count, c); return false; } return true; } bool DeviceGroupModel::removeRows( int row, int count, const QModelIndex &parent) { int c = 0; Q_ASSERT(!parent.isValid()); beginRemoveRows(parent, row, row+count-1); for (int i = 0; i < count; i++) { if (port_->deleteDeviceGroupAt(row)) c++; } endRemoveRows(); if (c != count) { qWarning("failed to delete rows in DeviceGroupModel at row %d; " "requested = %d, actual = %d", row, count, c); return false; } return true; } void DeviceGroupModel::setPort(Port *port) { beginResetModel(); port_ = port; endResetModel(); } // // ---------------------- Private Methods ----------------------- // int DeviceGroupModel::vlanCount(const OstProto::DeviceGroup *deviceGroup) const { if (!deviceGroup->has_encap() || !deviceGroup->encap().HasExtension(OstEmul::vlan)) return 0; OstEmul::VlanEmulation vlan = deviceGroup->encap() .GetExtension(OstEmul::vlan); int numTags = vlan.stack_size(); int count = 1; for (int i = 0; i < numTags; i++) count *= vlan.stack(i).count(); return count; } ostinato-1.3.0/client/devicegroupmodel.h000066400000000000000000000036311451413623100203420ustar00rootroot00000000000000/* Copyright (C) 2016 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _DEVICE_GROUP_MODEL_H #define _DEVICE_GROUP_MODEL_H #include #include class Port; namespace OstProto { class DeviceGroup; }; class DeviceGroupModel: public QAbstractTableModel { Q_OBJECT public: DeviceGroupModel(QObject *parent = 0); int rowCount(const QModelIndex &parent = QModelIndex()) const; int columnCount(const QModelIndex &parent = QModelIndex()) const; QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const; QVariant data(const QModelIndex &index, int role) const; bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole); QStringList mimeTypes() const; QMimeData* mimeData(const QModelIndexList &indexes) const; bool dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent); bool insertRows (int row, int count, const QModelIndex &parent = QModelIndex()); bool removeRows (int row, int count, const QModelIndex &parent = QModelIndex()); void setPort(Port *port); private: int vlanCount(const OstProto::DeviceGroup *deviceGroup) const; Port *port_; }; #endif ostinato-1.3.0/client/devicemodel.cpp000066400000000000000000000201501451413623100176130ustar00rootroot00000000000000/* Copyright (C) 2016 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #include "devicemodel.h" #include "arpstatusmodel.h" #include "ndpstatusmodel.h" #include "port.h" #include "emulproto.pb.h" #include "uint128.h" #include #include #include #include enum { kMacAddress, kVlans, kIp4Address, kIp4Gateway, kIp6Address, kIp6Gateway, kArpInfo, kNdpInfo, kFieldCount }; static QStringList columns_ = QStringList() << "Mac" << "Vlans" << "IPv4 Address" << "IPv4 Gateway" << "IPv6 Address" << "IPv6 Gateway" << "ARP" << "NDP"; DeviceModel::DeviceModel(QObject *parent) : QAbstractTableModel(parent) { port_ = NULL; arpStatusModel_ = new ArpStatusModel(this); ndpStatusModel_ = new NdpStatusModel(this); } int DeviceModel::rowCount(const QModelIndex &parent) const { if (!port_ || parent.isValid()) return 0; return port_->numDevices(); } int DeviceModel::columnCount(const QModelIndex &parent) const { if (parent.isValid()) return 0; return columns_.size(); } QVariant DeviceModel::headerData( int section, Qt::Orientation orientation, int role) const { if (role != Qt::DisplayRole) return QVariant(); switch (orientation) { case Qt::Horizontal: return columns_[section]; case Qt::Vertical: return QString("%1").arg(section + 1); default: Q_ASSERT(false); // Unreachable } return QVariant(); } QVariant DeviceModel::data(const QModelIndex &index, int role) const { QString str; if (!port_ || !index.isValid()) return QVariant(); int devIdx = index.row(); int field = index.column(); Q_ASSERT(devIdx < port_->numDevices()); Q_ASSERT(field < kFieldCount); const OstEmul::Device *dev = port_->deviceByIndex(devIdx); Q_ASSERT(dev); switch (field) { case kMacAddress: switch (role) { case Qt::DisplayRole: return QString("%1").arg(dev->mac(), 6*2, 16, QChar('0')) .replace(QRegExp("([0-9a-fA-F]{2}\\B)"), "\\1:") .toUpper(); default: break; } return QVariant(); case kVlans: switch (role) { case Qt::DisplayRole: if (!dev->vlan_size()) return QString("None"); for (int i = 0; i < dev->vlan_size(); i++) str.append(i == 0 ? "" : ", ") .append(QString::number(dev->vlan(i) & 0xfff)); return str; default: break; } return QVariant(); case kIp4Address: switch (role) { case Qt::DisplayRole: if (dev->has_ip4_prefix_length()) return QString("%1/%2") .arg(QHostAddress(dev->ip4()).toString()) .arg(dev->ip4_prefix_length()); else return QString("--"); default: break; } return QVariant(); case kIp4Gateway: switch (role) { case Qt::DisplayRole: if (dev->has_ip4_prefix_length()) return QHostAddress(dev->ip4_default_gateway()) .toString(); else return QString("--"); default: break; } return QVariant(); case kIp6Address: switch (role) { case Qt::DisplayRole: if (dev->has_ip6_prefix_length()) { OstEmul::Ip6Address ip = dev->ip6(); return QString("%1/%2") .arg(QHostAddress(UInt128(ip.hi(), ip.lo()) .toArray()).toString()) .arg(dev->ip6_prefix_length()); } else return QString("--"); default: break; } return QVariant(); case kIp6Gateway: switch (role) { case Qt::DisplayRole: if (dev->has_ip6_prefix_length()) { OstEmul::Ip6Address ip = dev->ip6_default_gateway(); return QHostAddress( UInt128(ip.hi(), ip.lo()).toArray()) .toString(); } else return QString("--"); default: break; } return QVariant(); case kArpInfo: switch (role) { case Qt::DisplayRole: if (dev->has_ip4_prefix_length()) return QString("%1/%2") .arg(port_->numArpResolved(devIdx)) .arg(port_->numArp(devIdx)); else return QString("--"); default: if (dev->has_ip4_prefix_length()) return drillableStyle(role); break; } return QVariant(); case kNdpInfo: switch (role) { case Qt::DisplayRole: if (dev->has_ip6_prefix_length()) return QString("%1/%2") .arg(port_->numNdpResolved(devIdx)) .arg(port_->numNdp(devIdx)); else return QString("--"); default: if (dev->has_ip6_prefix_length()) return drillableStyle(role); break; } return QVariant(); default: Q_ASSERT(false); // unreachable! break; } qWarning("%s: Unsupported field #%d", __FUNCTION__, field); return QVariant(); } Qt::DropActions DeviceModel::supportedDropActions() const { return Qt::IgnoreAction; // read-only model, doesn't accept any data } void DeviceModel::setPort(Port *port) { beginResetModel(); port_ = port; if (port_) connect(port_, SIGNAL(deviceInfoChanged()), SLOT(updateDeviceList())); endResetModel(); } QAbstractItemModel* DeviceModel::detailModel(const QModelIndex &index) { if (!index.isValid()) return NULL; switch(index.column()) { case kArpInfo: arpStatusModel_->setDeviceIndex(port_, index.row()); return arpStatusModel_; case kNdpInfo: ndpStatusModel_->setDeviceIndex(port_, index.row()); return ndpStatusModel_; default: return NULL; } } void DeviceModel::updateDeviceList() { beginResetModel(); endResetModel(); } // Style roles for drillable fields QVariant DeviceModel::drillableStyle(int role) const { QFont f; switch (role) { case Qt::ToolTipRole: return QString("Click for details ..."); case Qt::ForegroundRole: return QBrush(QColor(Qt::blue)); case Qt::FontRole: f.setUnderline(true); return f; default: break; } return QVariant(); } ostinato-1.3.0/client/devicemodel.h000066400000000000000000000030541451413623100172640ustar00rootroot00000000000000/* Copyright (C) 2016 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _DEVICE_MODEL_H #define _DEVICE_MODEL_H #include class ArpStatusModel; class NdpStatusModel; class Port; class DeviceModel: public QAbstractTableModel { Q_OBJECT public: DeviceModel(QObject *parent = 0); int rowCount(const QModelIndex &parent = QModelIndex()) const; int columnCount(const QModelIndex &parent = QModelIndex()) const; QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const; QVariant data(const QModelIndex &index, int role) const; Qt::DropActions supportedDropActions() const; void setPort(Port *port); QAbstractItemModel* detailModel(const QModelIndex &index); public slots: void updateDeviceList(); private: QVariant drillableStyle(int role) const; Port *port_; ArpStatusModel *arpStatusModel_; NdpStatusModel *ndpStatusModel_; }; #endif ostinato-1.3.0/client/deviceswidget.cpp000066400000000000000000000233571451413623100201750ustar00rootroot00000000000000/* Copyright (C) 2016 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #include "deviceswidget.h" #include "clipboardhelper.h" #include "devicegroupdialog.h" #include "port.h" #include "portgrouplist.h" #include #include extern ClipboardHelper *clipboardHelper; DevicesWidget::DevicesWidget(QWidget *parent) : QWidget(parent), portGroups_(NULL) { setupUi(this); deviceGroupList->setVisible(deviceConfig->isChecked()); deviceList->setVisible(deviceInfo->isChecked()); setDeviceInfoButtonsVisible(deviceInfo->isChecked()); deviceDetail->hide(); deviceGroupList->verticalHeader()->setDefaultSectionSize( deviceGroupList->verticalHeader()->minimumSectionSize()); deviceList->verticalHeader()->setDefaultSectionSize( deviceList->verticalHeader()->minimumSectionSize()); deviceDetail->verticalHeader()->setDefaultSectionSize( deviceDetail->verticalHeader()->minimumSectionSize()); // Populate DeviceGroup Context Menu Actions deviceGroupList->addAction(actionNewDeviceGroup); deviceGroupList->addAction(actionEditDeviceGroup); deviceGroupList->addAction(actionDeleteDeviceGroup); // DevicesWidget's actions is an aggegate of all sub-widget's actions addActions(deviceGroupList->actions()); // Add the clipboard actions to the context menu of deviceGroupList // but not to DeviceWidget's actions since they are already available // in the global Edit Menu QAction *sep = new QAction("Clipboard", this); sep->setSeparator(true); deviceGroupList->addAction(sep); deviceGroupList->addActions(clipboardHelper->actions()); } void DevicesWidget::setPortGroupList(PortGroupList *portGroups) { portGroups_ = portGroups; deviceGroupList->setModel(portGroups_->getDeviceGroupModel()); deviceList->setModel(portGroups_->getDeviceModel()); connect(portGroups_->getDeviceGroupModel(), SIGNAL(rowsInserted(QModelIndex, int, int)), SLOT(updateDeviceViewActions())); connect(portGroups_->getDeviceGroupModel(), SIGNAL(rowsRemoved(QModelIndex, int, int)), SLOT(updateDeviceViewActions())); connect(deviceGroupList->selectionModel(), SIGNAL(currentChanged(const QModelIndex&, const QModelIndex&)), SLOT(updateDeviceViewActions())); connect(deviceGroupList->selectionModel(), SIGNAL(selectionChanged(const QItemSelection&, const QItemSelection&)), SLOT(updateDeviceViewActions())); connect(deviceList->selectionModel(), SIGNAL(currentChanged(const QModelIndex&, const QModelIndex&)), SLOT(when_deviceList_currentChanged(const QModelIndex&))); // FIXME: hardcoding deviceGroupList->resizeColumnToContents(1); // Vlan Count deviceGroupList->resizeColumnToContents(2); // Device Count // FIXME: hardcoding deviceList->resizeColumnToContents(1); // Vlan Id(s) deviceList->resizeColumnToContents(6); // ARP Info deviceList->resizeColumnToContents(7); // NDP Info updateDeviceViewActions(); } void DevicesWidget::keyPressEvent(QKeyEvent *event) { if (event->key() == Qt::Key_Escape) { deviceDetail->hide(); event->accept(); } else event->ignore(); } void DevicesWidget::setCurrentPortIndex(const QModelIndex &portIndex) { Port *port = NULL; if (!portGroups_) return; // XXX: We assume portIndex corresponds to sourceModel, not proxyModel if (portGroups_->isPort(portIndex)) port = &portGroups_->port(portIndex); portGroups_->getDeviceGroupModel()->setPort(port); portGroups_->getDeviceModel()->setPort(port); currentPortIndex_ = portIndex; updateDeviceViewActions(); } void DevicesWidget::updateDeviceViewActions() { QItemSelectionModel *devSel = deviceGroupList->selectionModel(); if (!portGroups_) return; // For some reason hasSelection() returns true even if selection size is 0 // so additional check for size introduced if (devSel->hasSelection() && (devSel->selection().size() > 0)) { // If more than one non-contiguous ranges selected, // disable "New" and "Edit" if (devSel->selection().size() > 1) { actionNewDeviceGroup->setDisabled(true); actionEditDeviceGroup->setDisabled(true); } else { actionNewDeviceGroup->setEnabled(true); // Enable "Edit" only if the single range has a single row if (devSel->selection().at(0).height() > 1) actionEditDeviceGroup->setDisabled(true); else actionEditDeviceGroup->setEnabled(true); } // Delete is always enabled as long as we have a selection actionDeleteDeviceGroup->setEnabled(true); } else { qDebug("No device selection"); if (portGroups_->isPort(currentPortIndex_)) actionNewDeviceGroup->setEnabled(true); else actionNewDeviceGroup->setDisabled(true); actionEditDeviceGroup->setDisabled(true); actionDeleteDeviceGroup->setDisabled(true); } } // // DeviceGroup slots // void DevicesWidget::on_deviceInfo_toggled(bool checked) { setDeviceInfoButtonsVisible(checked); deviceGroupList->setHidden(checked); deviceList->setVisible(checked); deviceDetail->hide(); } void DevicesWidget::on_actionNewDeviceGroup_triggered() { if (!portGroups_) return; // In case nothing is selected, insert 1 row at the end int row = portGroups_->getDeviceGroupModel()->rowCount(), count = 1; QItemSelection selection = deviceGroupList->selectionModel()->selection(); // In case we have a single range selected; insert as many rows as // in the singe selected range before the top of the selected range if (selection.size() == 1) { row = selection.at(0).top(); count = selection.at(0).height(); } portGroups_->getDeviceGroupModel()->insertRows(row, count); } void DevicesWidget::on_actionDeleteDeviceGroup_triggered() { QModelIndex index; if (!portGroups_) return; if (deviceGroupList->selectionModel()->hasSelection()) { while(deviceGroupList->selectionModel()->selectedRows().size()) { index = deviceGroupList->selectionModel()->selectedRows().at(0); portGroups_->getDeviceGroupModel()->removeRows(index.row(), 1); } } } void DevicesWidget::on_actionEditDeviceGroup_triggered() { QItemSelection selection = deviceGroupList->selectionModel()->selection(); // Ensure we have only one range selected which contains only one row if ((selection.size() == 1) && (selection.at(0).height() == 1)) on_deviceGroupList_activated(selection.at(0).topLeft()); } void DevicesWidget::on_deviceGroupList_activated(const QModelIndex &index) { if (!index.isValid() || !portGroups_) return; DeviceGroupDialog dgd(&portGroups_->port(currentPortIndex_), index.row()); dgd.exec(); } void DevicesWidget::on_refresh_clicked() { if (!portGroups_) return; Q_ASSERT(portGroups_->isPort(currentPortIndex_)); QModelIndex curPortGroup = portGroups_->getPortModel() ->parent(currentPortIndex_); Q_ASSERT(curPortGroup.isValid()); Q_ASSERT(portGroups_->isPortGroup(curPortGroup)); deviceDetail->hide(); portGroups_->portGroup(curPortGroup) .getDeviceInfo(portGroups_->port(currentPortIndex_).id()); } void DevicesWidget::on_resolveNeighbors_clicked() { if (!portGroups_) return; Q_ASSERT(portGroups_->isPort(currentPortIndex_)); QModelIndex curPortGroup = portGroups_->getPortModel() ->parent(currentPortIndex_); Q_ASSERT(curPortGroup.isValid()); Q_ASSERT(portGroups_->isPortGroup(curPortGroup)); deviceDetail->hide(); QList portList({portGroups_->port(currentPortIndex_).id()}); portGroups_->portGroup(curPortGroup).resolveDeviceNeighbors(&portList); portGroups_->portGroup(curPortGroup).getDeviceInfo(portList.at(0)); } void DevicesWidget::on_clearNeighbors_clicked() { if (!portGroups_) return; Q_ASSERT(portGroups_->isPort(currentPortIndex_)); QModelIndex curPortGroup = portGroups_->getPortModel() ->parent(currentPortIndex_); Q_ASSERT(curPortGroup.isValid()); Q_ASSERT(portGroups_->isPortGroup(curPortGroup)); deviceDetail->hide(); QList portList({portGroups_->port(currentPortIndex_).id()}); portGroups_->portGroup(curPortGroup).clearDeviceNeighbors(&portList); portGroups_->portGroup(curPortGroup).getDeviceInfo(portList.at(0)); } void DevicesWidget::when_deviceList_currentChanged(const QModelIndex &index) { if (!index.isValid() || !portGroups_) return; QAbstractItemModel *detailModel = portGroups_->getDeviceModel() ->detailModel(index); deviceDetail->setModel(detailModel); deviceDetail->setVisible(detailModel != NULL); } void DevicesWidget::setDeviceInfoButtonsVisible(bool show) { refresh->setVisible(show); arpNdpLabel->setVisible(show); resolveNeighbors->setVisible(show); clearNeighbors->setVisible(show); } ostinato-1.3.0/client/deviceswidget.h000066400000000000000000000033041451413623100176300ustar00rootroot00000000000000/* Copyright (C) 2016 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _DEVICES_WIDGET_H #define _DEVICES_WIDGET_H #include "ui_deviceswidget.h" #include class PortGroupList; class DevicesWidget: public QWidget, private Ui::DevicesWidget { Q_OBJECT public: DevicesWidget(QWidget *parent = NULL); void setPortGroupList(PortGroupList *portGroups); virtual void keyPressEvent(QKeyEvent *event); public slots: void setCurrentPortIndex(const QModelIndex &portIndex); private slots: void updateDeviceViewActions(); void on_deviceInfo_toggled(bool checked); void on_actionNewDeviceGroup_triggered(); void on_actionDeleteDeviceGroup_triggered(); void on_actionEditDeviceGroup_triggered(); void on_deviceGroupList_activated(const QModelIndex &index); void on_refresh_clicked(); void on_resolveNeighbors_clicked(); void on_clearNeighbors_clicked(); void when_deviceList_currentChanged(const QModelIndex &index); private: void setDeviceInfoButtonsVisible(bool show); PortGroupList *portGroups_; QModelIndex currentPortIndex_; }; #endif ostinato-1.3.0/client/deviceswidget.ui000066400000000000000000000135531451413623100200250ustar00rootroot00000000000000 DevicesWidget 0 0 675 328 Form 0 Configuration true Information Refresh information Refresh device and neighbor information :/icons/refresh.png:/icons/refresh.png Qt::Horizontal 131 23 ARP/ND Resolve Neighbors Resolve Device Neighbors on selected port(s) :/icons/neighbor_resolve.png:/icons/neighbor_resolve.png Clear Neighbors Clear Device Neighbors on selected port(s) :/icons/neighbor_clear.png:/icons/neighbor_clear.png Qt::ActionsContextMenu This is the device group list for the selected port A device group is a set of one or more devices/hosts which will be emulated by Ostinato Right-click to create/edit a device group QFrame::StyledPanel 1 QAbstractItemView::ExtendedSelection QAbstractItemView::SelectRows 0 1 No devices being emulated To emulate a device, click on Configuration and create a device group QAbstractItemView::SelectRows IP neighbor cache is empty QAbstractItemView::SelectRows :/icons/devicegroup_add.png:/icons/devicegroup_add.png New Device Group :/icons/devicegroup_delete.png:/icons/devicegroup_delete.png Delete Device Group :/icons/devicegroup_edit.png:/icons/devicegroup_edit.png Edit Device Group XTableView QTableView
xtableview.h
ostinato-1.3.0/client/dumpview.cpp000066400000000000000000000276031451413623100172050ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #include "dumpview.h" #include #include //! \todo Enable Scrollbars DumpView::DumpView(QWidget *parent) : QAbstractItemView(parent) { int w, h; // NOTE: Monospaced fonts only !!!!!!!!!!! setFont(QFont("Courier")); w = fontMetrics().width('X'); h = fontMetrics().height(); mLineHeight = h; mCharWidth = w; mSelectedRow = mSelectedCol = -1; // calculate width for offset column and the whitespace that follows it // 0010 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ........ ........ mOffsetPaneTopRect = QRect(0, 0, w*4, h); mDumpPaneTopRect = QRect(mOffsetPaneTopRect.right()+w*3, 0, w*((8*3-1)+2+(8*3-1)), h); mAsciiPaneTopRect = QRect(mDumpPaneTopRect.right()+w*3, 0, w*(8+1+8), h); qDebug("DumpView::DumpView"); } QModelIndex DumpView::indexAt(const QPoint &/*point*/) const { #if 0 int x = point.x(); int row, col; if (x > mAsciiPaneTopRect.left()) { col = (x - mAsciiPaneTopRect.left()) / mCharWidth; if (col == 8) // don't select whitespace goto _exit; else if (col > 8) // adjust for whitespace col--; } else if (x > mDumpPaneTopRect.left()) { col = (x - mDumpPaneTopRect.left()) / (mCharWidth*3); } row = point.y()/mLineHeight; if ((col < 16) && (row < ((data.size()+16)/16))) { selrow = row; selcol = col; } else goto _exit; // last row check col if ((row == (((data.size()+16)/16) - 1)) && (col >= (data.size() % 16))) goto _exit; qDebug("dumpview::selection(%d, %d)", selrow, selcol); offset = selrow * 16 + selcol; #if 0 for(int i = 0; i < model()->rowCount(parent); i++) { QModelIndex index = model()->index(i, 0, parent); if (model()->hasChildren(index)) indexAtOffset(offset, index); // Non Leaf else if ( dump.append(model()->data(index, Qt::UserRole).toByteArray()); // Leaf // FIXME: Use RawValueRole instead of UserRole } #endif } _exit: // Clear existing selection selrow = -1; #endif return QModelIndex(); } void DumpView::scrollTo(const QModelIndex &/*index*/, ScrollHint /*hint*/) { // FIXME: implement scrolling } QRect DumpView::visualRect(const QModelIndex &/*index*/) const { // FIXME: calculate actual rect return rect(); } //protected: int DumpView::horizontalOffset() const { return horizontalScrollBar()->value(); } bool DumpView::isIndexHidden(const QModelIndex &/*index*/) const { return false; } QModelIndex DumpView::moveCursor(CursorAction /*cursorAction*/, Qt::KeyboardModifiers /*modifiers*/) { // FIXME(MED): need to implement movement using cursor return currentIndex(); } void DumpView::setSelection(const QRect &/*rect*/, QItemSelectionModel::SelectionFlags flags) { // FIXME(HI): calculate indexes using rect selectionModel()->select(QModelIndex(), flags); } int DumpView::verticalOffset() const { return verticalScrollBar()->value(); } QRegion DumpView::visualRegionForSelection( const QItemSelection &/*selection*/) const { // FIXME(HI) return QRegion(rect()); } //protected slots: void DumpView::dataChanged(const QModelIndex &/*topLeft*/, const QModelIndex &/*bottomRight*/, const QVector &/*roles*/) { // FIXME(HI) update(); } void DumpView::selectionChanged(const QItemSelection &/*selected*/, const QItemSelection &/*deselected*/) { // FIXME(HI) update(); } void DumpView::populateDump(QByteArray &dump, int &selOfs, int &selSize, QModelIndex parent) { // FIXME: Use new enum instead of Qt::UserRole //! \todo (low): generalize this for any model not just our pkt model Q_ASSERT(!parent.isValid()); qDebug("!!!! %d $$$$", dump.size()); for(int i = 0; i < model()->rowCount(parent); i++) { QModelIndex index = model()->index(i, 0, parent); Q_ASSERT(index.isValid()); // Assumption: protocol data is in bytes (not bits) qDebug("%d: %d bytes", i, model()->data(index, Qt::UserRole).toByteArray().size()); dump.append(model()->data(index, Qt::UserRole).toByteArray()); } if (selectionModel()->selectedIndexes().size()) { int j, bits; QModelIndex index; Q_ASSERT(selectionModel()->selectedIndexes().size() == 1); index = selectionModel()->selectedIndexes().at(0); if (index.parent().isValid()) { // Field // SelOfs = SUM(protocol sizes before selected field's protocol) + // SUM(field sizes before selected field) selOfs = 0; j = index.parent().row() - 1; while (j >= 0) { selOfs += model()->data(index.parent().sibling(j,0), Qt::UserRole).toByteArray().size(); j--; } bits = 0; j = index.row() - 1; while (j >= 0) { bits += model()->data(index.sibling(j,0), Qt::UserRole+1). toInt(); j--; } selOfs += bits/8; selSize = model()->data(index, Qt::UserRole).toByteArray().size(); } else { // Protocol selOfs = 0; j = index.row() - 1; while (j >= 0) { selOfs += model()->data(index.sibling(j,0), Qt::UserRole). toByteArray().size(); j--; } selSize = model()->data(index, Qt::UserRole).toByteArray().size(); } } } // TODO(LOW): rewrite this function - it's a mess! void DumpView::paintEvent(QPaintEvent* /*event*/) { QStylePainter painter(viewport()); QRect offsetRect = mOffsetPaneTopRect; QRect dumpRect = mDumpPaneTopRect; QRect asciiRect = mAsciiPaneTopRect; QPalette pal = palette(); static QByteArray data; //QByteArray ba; int selOfs = -1, selSize; int curSelOfs, curSelSize; qDebug("dumpview::paintEvent"); // FIXME(LOW): unable to set the self widget's font in constructor painter.setFont(QFont("Courier")); // Qt automatically clears the background before we are called // QWidget::paintEvent doc: // When the paint event occurs, the update region has normally // been erased, so you are painting on the widget's background. if (model()) { data.clear(); populateDump(data, selOfs, selSize); } // display the offset, dump and ascii panes 8 + 8 bytes on a line for (int i = 0; i < data.size(); i+=16) { QString dumpStr, asciiStr; //ba = data.mid(i, 16); // display offset painter.drawItemText(offsetRect, Qt::AlignLeft | Qt::AlignTop, pal, true, QString("%1").arg(i, 4, 16, QChar('0')), QPalette::WindowText); // construct the dumpStr and asciiStr for (int j = i; (j < (i+16)) && (j < data.size()); j++) { unsigned char c = data.at(j); // extra space after 8 bytes if (((j+8) % 16) == 0) { dumpStr.append(" "); asciiStr.append(" "); } dumpStr.append(QString("%1").arg((uint)c, 2, 16, QChar('0')). toUpper()).append(" "); if (isPrintable(c)) asciiStr.append(QChar(c)); else asciiStr.append(QChar('.')); } // display dump painter.drawItemText(dumpRect, Qt::AlignLeft | Qt::AlignTop, pal, true, dumpStr, QPalette::WindowText); // display ascii painter.drawItemText(asciiRect, Qt::AlignLeft | Qt::AlignTop, pal, true, asciiStr, QPalette::WindowText); // if no selection, skip selection painting if (selOfs < 0) goto _next; // Check overlap between current row and selection { QRect r1(i, 0, qMin(16, data.size()-i), 8); QRect s1(selOfs, 0, selSize, 8); if (r1.intersects(s1)) { QRect t = r1.intersected(s1); curSelOfs = t.x(); curSelSize = t.width(); } else curSelSize = 0; } // overpaint selection on current row (if any) if (curSelSize > 0) { QRect r; QString selectedAsciiStr, selectedDumpStr; qDebug("dumpview::paintEvent - Highlighted (%d, %d)", curSelOfs, curSelSize); // construct the dumpStr and asciiStr for (int k = curSelOfs; (k < (curSelOfs + curSelSize)); k++) { unsigned char c = data.at(k); // extra space after 8 bytes if (((k+8) % 16) == 0) { // Avoid adding space at the start for fields starting // at second column 8 byte boundary if (k!=curSelOfs) { selectedDumpStr.append(" "); selectedAsciiStr.append(" "); } } selectedDumpStr.append(QString("%1").arg((uint)c, 2, 16, QChar('0')).toUpper()).append(" "); if (isPrintable(c)) selectedAsciiStr.append(QChar(c)); else selectedAsciiStr.append(QChar('.')); } // display dump r = dumpRect; if ((curSelOfs - i) < 8) r.translate(mCharWidth*(curSelOfs-i)*3, 0); else r.translate(mCharWidth*((curSelOfs-i)*3+1), 0); // adjust width taking care of selection stretching between // the two 8byte columns if (( (curSelOfs-i) < 8 ) && ( (curSelOfs-i+curSelSize) > 8 )) r.setWidth((curSelSize * 3 + 1) * mCharWidth); else r.setWidth((curSelSize * 3) * mCharWidth); painter.fillRect(r, pal.highlight()); painter.drawItemText(r, Qt::AlignLeft | Qt::AlignTop, pal, true, selectedDumpStr, QPalette::HighlightedText); // display ascii r = asciiRect; if ((curSelOfs - i) < 8) r.translate(mCharWidth*(curSelOfs-i)*1, 0); else r.translate(mCharWidth*((curSelOfs-i)*1+1), 0); // adjust width taking care of selection stretching between // the two 8byte columns if (( (curSelOfs-i) < 8 ) && ( (curSelOfs-i+curSelSize) > 8 )) r.setWidth((curSelSize * 1 + 1) * mCharWidth); else r.setWidth((curSelSize * 1) * mCharWidth); painter.fillRect(r, pal.highlight()); painter.drawItemText(r, Qt::AlignLeft | Qt::AlignTop, pal, true, selectedAsciiStr, QPalette::HighlightedText); } _next: // move the rects down offsetRect.translate(0, mLineHeight); dumpRect.translate(0, mLineHeight); asciiRect.translate(0, mLineHeight); } } ostinato-1.3.0/client/dumpview.h000066400000000000000000000041351451413623100166450ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #include class DumpView: public QAbstractItemView { public: DumpView(QWidget *parent=0); QModelIndex indexAt( const QPoint &point ) const; void scrollTo( const QModelIndex &index, ScrollHint hint = EnsureVisible ); QRect visualRect( const QModelIndex &index ) const; protected: int horizontalOffset() const; bool isIndexHidden( const QModelIndex &index ) const; QModelIndex moveCursor( CursorAction cursorAction, Qt::KeyboardModifiers modifiers ); void setSelection( const QRect &rect, QItemSelectionModel::SelectionFlags flags ); int verticalOffset() const; QRegion visualRegionForSelection( const QItemSelection &selection ) const; protected slots: void dataChanged( const QModelIndex &topLeft, const QModelIndex &bottomRight, const QVector &roles ); void selectionChanged( const QItemSelection &selected, const QItemSelection &deselected ); void paintEvent(QPaintEvent *event); private: void populateDump(QByteArray &dump, int &selOfs, int &selSize, QModelIndex parent = QModelIndex()); bool inline isPrintable(char c) {if ((c >= 32) && (c <= 126)) return true; else return false; } private: QRect mOffsetPaneTopRect; QRect mDumpPaneTopRect; QRect mAsciiPaneTopRect; int mSelectedRow, mSelectedCol; int mLineHeight; int mCharWidth; }; ostinato-1.3.0/client/fieldedit.cpp000066400000000000000000000051411451413623100172670ustar00rootroot00000000000000/* Copyright (C) 2021 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #include "fieldedit.h" FieldEdit::FieldEdit(QWidget *parent) : QLineEdit(parent) { setType(kUInt64); } void FieldEdit::setType(FieldType type) { // clear existing contents before changing the validator clear(); setPlaceholderText(""); type_ = type; switch (type_) { case kUInt64: setValidator(&uint64Validator_); if (isMaskMode_) setText("0xFFFFFFFFFFFFFFFF"); break; case kMacAddress: setValidator(&macValidator_); setPlaceholderText("00:00:00:00:00:00"); if (isMaskMode_) setText("FF:FF:FF:FF:FF:FF"); break; case kIp4Address: setValidator(&ip4Validator_); setPlaceholderText("0.0.0.0"); if (isMaskMode_) setText("255.255.255.255"); break; case kIp6Address: setValidator(&ip6Validator_); setPlaceholderText("::"); if (isMaskMode_) setText("FFFF:FFFF:FFFF:FFFF:FFFF:FFFF:FFFF:FFFF"); break; default: setValidator(nullptr); break; } } // Applicable only if type is kUInt64 void FieldEdit::setRange(quint64 min, quint64 max) { uint64Validator_.setRange(min, max); if (type_ == kUInt64) { setPlaceholderText(QString("%1 - %2").arg(min).arg(max)); if (isMaskMode_) setText(QString::number(max, 16).toUpper().prepend("0x")); } } void FieldEdit::setMaskMode(bool maskMode) { isMaskMode_ = maskMode; } QString FieldEdit::text() const { QString str = QLineEdit::text(); switch (type_) { case kMacAddress: str.remove(QRegularExpression("[:-]")); str.prepend("0x"); break; case kIp4Address: str = QString::number(QHostAddress(str).toIPv4Address()); break; default: break; } return str; } ostinato-1.3.0/client/fieldedit.h000066400000000000000000000026661451413623100167450ustar00rootroot00000000000000/* Copyright (C) 2021 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _FIELD_EDIT_H #define _FIELD_EDIT_H #include "ipv4addressvalidator.h" #include "ipv6addressvalidator.h" #include "macaddressvalidator.h" #include "ulonglongvalidator.h" #include class FieldEdit: public QLineEdit { Q_OBJECT public: enum FieldType { kUInt64, kMacAddress, kIp4Address, kIp6Address }; FieldEdit(QWidget *parent = 0); void setType(FieldType type); void setRange(quint64 min, quint64 max); void setMaskMode(bool maskMode); QString text() const; private: FieldType type_{kUInt64}; bool isMaskMode_{false}; IPv4AddressValidator ip4Validator_; IPv6AddressValidator ip6Validator_; MacAddressValidator macValidator_; ULongLongValidator uint64Validator_; }; #endif ostinato-1.3.0/client/findreplace.cpp000066400000000000000000000177451451413623100176270ustar00rootroot00000000000000/* Copyright (C) 2021 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #include "findreplace.h" #include "abstractprotocol.h" #include "iputils.h" #include "mandatoryfieldsgroup.h" #include "protocolmanager.h" #include "stream.h" #include "uint128.h" #include extern ProtocolManager *OstProtocolManager; // TODO: It might be useful for this dialog to support a "preview" // of the replacements FindReplaceDialog::FindReplaceDialog(Action *action, QWidget *parent) : QDialog(parent), action_(action) { setupUi(this); findMask->setMaskMode(true); replaceMask->setMaskMode(true); // Keep things simple and don't use mask(s) (default) useFindMask->setChecked(false); useReplaceMask->setChecked(false); // TODO: remove combo protocols - see note in StreamBase::findReplace QStringList protocolList = OstProtocolManager->protocolDatabase(); protocolList.sort(); protocol->addItems(protocolList); // Enable this setting if we have streams selected on input selectedStreamsOnly->setEnabled(action->selectedStreamsOnly); // Reset for user input action->selectedStreamsOnly = false; QPushButton *ok = buttonBox->button(QDialogButtonBox::Ok); ok->setText(tr("Replace All")); ok->setDisabled(true); mandatoryFields_ = new MandatoryFieldsGroup(this); mandatoryFields_->add(protocol); mandatoryFields_->add(field); mandatoryFields_->add(findValue); mandatoryFields_->add(findMask); mandatoryFields_->add(replaceValue); mandatoryFields_->add(replaceMask); mandatoryFields_->setSubmitButton(ok); } FindReplaceDialog::~FindReplaceDialog() { delete mandatoryFields_; } void FindReplaceDialog::on_protocol_currentIndexChanged(const QString &name) { field->clear(); fieldAttrib_.clear(); Stream stream; AbstractProtocol *protocol = OstProtocolManager->createProtocol( name, &stream); int count = protocol->fieldCount(); for (int i = 0; i < count; i++) { // XXX: It might be useful to support meta fields too, later! if (!protocol->fieldFlags(i).testFlag(AbstractProtocol::FrameField)) continue; int bitSize = protocol->fieldData(i, AbstractProtocol::FieldBitSize) .toInt(); if (bitSize <= 0) // skip optional fields continue; FieldAttrib fieldAttrib; fieldAttrib.index = i; // fieldIndex fieldAttrib.bitSize = bitSize; // field and fieldAttrib_ have same count and order of fields fieldAttrib_.append(fieldAttrib); field->addItem(protocol->fieldData(i, AbstractProtocol::FieldName) .toString()); } protocolId_ = protocol->protocolNumber(); delete protocol; } void FindReplaceDialog::on_field_currentIndexChanged(int index) { if (index < 0) return; QString fieldName = field->currentText(); FieldAttrib fieldAttrib = fieldAttrib_.at(index); // Use heuristics to determine field type if (fieldAttrib.bitSize == 48) { findMask->setType(FieldEdit::kMacAddress); findValue->setType(FieldEdit::kMacAddress); replaceMask->setType(FieldEdit::kMacAddress); replaceValue->setType(FieldEdit::kMacAddress); } else if ((fieldAttrib.bitSize == 32) && (fieldName.contains(QRegularExpression( "address|source|destination", QRegularExpression::CaseInsensitiveOption)))) { findMask->setType(FieldEdit::kIp4Address); findValue->setType(FieldEdit::kIp4Address); replaceMask->setType(FieldEdit::kIp4Address); replaceValue->setType(FieldEdit::kIp4Address); } else if ((fieldAttrib.bitSize == 128) && (fieldName.contains(QRegularExpression( "address|source|destination", QRegularExpression::CaseInsensitiveOption)))) { findMask->setType(FieldEdit::kIp6Address); findValue->setType(FieldEdit::kIp6Address); replaceMask->setType(FieldEdit::kIp6Address); replaceValue->setType(FieldEdit::kIp6Address); } else { quint64 max = quint64(~0) >> (64-fieldAttrib.bitSize); qDebug("XXXXXX %s bitSize %d max %llx", qPrintable(field->currentText()), fieldAttrib.bitSize, max); findMask->setType(FieldEdit::kUInt64); findMask->setRange(0, max); findValue->setType(FieldEdit::kUInt64); findValue->setRange(0, max); replaceMask->setType(FieldEdit::kUInt64); replaceMask->setRange(0, max); replaceValue->setType(FieldEdit::kUInt64); replaceValue->setRange(0, max); } } void FindReplaceDialog::on_matchAny_toggled(bool checked) { if (checked) { findValueLabel->setHidden(true); findValue->setHidden(true); useFindMask->setHidden(true); findMask->setHidden(true); findMaskHint->setHidden(true); } else { findValueLabel->setVisible(true); findValue->setVisible(true); useFindMask->setVisible(true); if (useFindMask->isChecked()) { findMask->setVisible(true); findMaskHint->setVisible(true); } } } void FindReplaceDialog::on_buttonBox_accepted() { FieldAttrib fieldAttrib = fieldAttrib_.at(field->currentIndex()); action_->protocolField = QString("%1 %2") .arg(protocol->currentText()) .arg(field->currentText()); action_->protocolNumber = protocolId_; action_->fieldIndex = fieldAttrib.index; action_->fieldBitSize = fieldAttrib.bitSize; if (fieldAttrib.bitSize == 128) { // IPv6 address if (matchAny->isChecked()) { action_->findMask.setValue(UInt128(0)); action_->findValue.setValue(UInt128(0)); } else { action_->findMask.setValue( useFindMask->isChecked() ? ipUtils::ip6StringToUInt128(findMask->text()) : ~UInt128(0)); action_->findValue.setValue( ipUtils::ip6StringToUInt128(findValue->text())); } action_->replaceMask.setValue( useReplaceMask->isChecked() ? ipUtils::ip6StringToUInt128(replaceMask->text()) : ~UInt128(0)); action_->replaceValue.setValue( ipUtils::ip6StringToUInt128(replaceValue->text())); } else { // everything else if (matchAny->isChecked()) { action_->findMask.setValue(0); action_->findValue.setValue(0); } else { action_->findMask.setValue( useFindMask->isChecked() ? findMask->text().toULongLong(nullptr, 0) : ~quint64(0)); action_->findValue.setValue( findValue->text().toULongLong(nullptr, 0)); } action_->replaceMask.setValue( useReplaceMask->isChecked() ? replaceMask->text().toULongLong(nullptr, 0) : ~quint64(0)); action_->replaceValue.setValue(QString::number( replaceValue->text().toULongLong(nullptr, 0))); } action_->selectedStreamsOnly = selectedStreamsOnly->isChecked(); } ostinato-1.3.0/client/findreplace.h000066400000000000000000000033161451413623100172610ustar00rootroot00000000000000/* Copyright (C) 2021 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _FIND_REPLACE_H #define _FIND_REPLACE_H #include "ui_findreplace.h" class MandatoryFieldsGroup; class FindReplaceDialog: public QDialog, public Ui::FindReplace { Q_OBJECT public: struct Action; FindReplaceDialog(Action *action, QWidget *parent = 0); ~FindReplaceDialog(); private slots: void on_protocol_currentIndexChanged(const QString &name); void on_field_currentIndexChanged(int index); void on_matchAny_toggled(bool checked); void on_buttonBox_accepted(); private: struct FieldAttrib; quint32 protocolId_{0}; Action *action_{nullptr}; QList fieldAttrib_; MandatoryFieldsGroup *mandatoryFields_{nullptr}; }; struct FindReplaceDialog::Action { QString protocolField; quint32 protocolNumber; quint32 fieldIndex; int fieldBitSize; QVariant findValue; QVariant findMask; QVariant replaceValue; QVariant replaceMask; bool selectedStreamsOnly; // in-out param }; struct FindReplaceDialog::FieldAttrib { quint32 index; int bitSize; }; #endif ostinato-1.3.0/client/findreplace.ui000066400000000000000000000163051451413623100174510ustar00rootroot00000000000000 FindReplace 0 0 361 309 Find & Replace :/icons/find.png:/icons/find.png Find Protocol protocol Field field Value Mask true <html><head/><body><p align="center">Matches a field only if <span style="white-space:nowrap">(FieldValue &amp; FindMask) = FindValue</span></p></body></html> :/icons/info.png Match any value Replace with Value Mask true <html><head/><body><p align="center">New field value = <span style="white-space:nowrap">(OldFieldValue &amp; ~ReplaceMask) | (ReplaceValue &amp; ReplaceMask)</span></p></body></html> :/icons/info.png Selected Streams Only Qt::Horizontal QDialogButtonBox::Cancel|QDialogButtonBox::Ok FieldEdit QLineEdit
fieldedit.h
buttonBox accepted() FindReplace accept() 248 277 157 274 buttonBox rejected() FindReplace reject() 316 283 286 274 useFindMask toggled(bool) findMask setVisible(bool) 60 115 76 119 useReplaceMask toggled(bool) replaceMask setVisible(bool) 56 228 73 227 useReplaceMask toggled(bool) replaceMaskHint setVisible(bool) 50 230 333 233 useFindMask toggled(bool) findMaskHint setVisible(bool) 46 116 335 122
ostinato-1.3.0/client/hexlineedit.cpp000066400000000000000000000044551451413623100176470ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #include "hexlineedit.h" #include "qdebug.h" QString & uintToHexStr(quint64 num, QString &hexStr, quint8 octets); HexLineEdit::HexLineEdit( QWidget * parent) : QLineEdit(parent) { //QLineEdit::QLineEdit(parent); } void HexLineEdit::focusOutEvent(QFocusEvent* /*e*/) { #if 0 const QValidator *v = validator(); if ( v ) { int curpos = cursorPosition(); QString str = text(); if ( v->validate( str, curpos ) == QValidator::Acceptable ) { if ( curpos != cursorPosition() ) setCursorPosition( curpos ); if ( str != text() ) setText( str ); } else { if ( curpos != cursorPosition() ) setCursorPosition( curpos ); str = text(); v->fixup( str ); if ( str != text() ) { setText( str ); } } } QLineEdit::focusOutEvent( e ); emit focusOut(); #else #define uintToHexStr(num, bytesize) \ QString("%1").arg((num), (bytesize)*2 , 16, QChar('0')) bool isOk; ulong num; qDebug("before = %s\n", qPrintable(text())); num = text().remove(QChar(' ')).toULong(&isOk, 16); setText(uintToHexStr(num, 4)); qDebug("after = %s\n", qPrintable(text())); #undef uintToHexStr #endif } #if 0 void HexLineEdit::focusInEvent( QFocusEvent *e ) { QLineEdit::focusInEvent( e ); emit focusIn(); } void HexLineEdit::keyPressEvent( QKeyEvent *e ) { QLineEdit::keyPressEvent( e ); if ( e->key() == Key_Enter || e->key() == Key_Return ) { setSelection( 0, text().length() ); } } #endif ostinato-1.3.0/client/hexlineedit.h000066400000000000000000000020441451413623100173040ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _HEXLINEEDIT #define _HEXLINEEDIT #include class HexLineEdit : public QLineEdit { Q_OBJECT public: // Constructors HexLineEdit ( QWidget * parent); protected: void focusOutEvent( QFocusEvent *e ); //void focusInEvent( QFocusEvent *e ); //void keyPressEvent( QKeyEvent *e ); signals: //void focusIn(); void focusOut(); }; #endif ostinato-1.3.0/client/icononlydelegate.h000066400000000000000000000023711451413623100203320ustar00rootroot00000000000000/* Copyright (C) 2018 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _ICON_ONLY_DELEGATE #define _ICON_ONLY_DELEGATE #include class IconOnlyDelegate : public QStyledItemDelegate { using QStyledItemDelegate::QStyledItemDelegate; void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const { QStyleOptionViewItem opt = option; opt.decorationPosition = QStyleOptionViewItem::Top; QStyledItemDelegate::paint(painter, opt, index); } QString displayText(const QVariant&, const QLocale&) const { return QString(); } }; #endif ostinato-1.3.0/client/icons/000077500000000000000000000000001451413623100157445ustar00rootroot00000000000000ostinato-1.3.0/client/icons/about.png000066400000000000000000000067731451413623100176010ustar00rootroot00000000000000‰PNG  IHDR00Wù‡sBIT|dˆ pHYs]]Á{ëÓtEXtSoftwarewww.inkscape.org›î< xIDAThÅšyT•ջǿï™S@@º¢®ê.¥Zë§¢•bKÅ””r(µ20»±Ôu]7Ñü™fš¶œP\…¥¤+ÍáæTZY—rržÐD†ÏýÎGЭöZï?g?ï~>ß³÷³÷óìs ÝÃxJê )FR°¤s’¾—ô¿†aTÜK_÷´>À$ ˜†Û%àMÀû^û6îv ­¤$Ý/I/^Ô¶mÛtîÜ95mÚT t™Ÿ”ô¤aÿw·~ïIþ”\ºt‰ôôtl6’ÌÇáp0dÈN:åš @ë]¢\KfûöíDDDàááÁ°aÃXºt)ëׯgþüùôèÑÃ0 á»ï¾s‰ØñOó X °{÷n|||¸ÿþû9tèPƒðÉ'Ÿàp8hÕªåå宗ɵÿ·Ã?PUUE\\N§“ƒºA_½z•£Grøða>Ì«¯¾Š$ oÕW,Zý]ð6`1Àš5kDZZyyy 4ˆØØX¼½½Ýâ îFZZ³gÏæôéÓu…T³€À;¸¿køVÀN—ÇŒŒŒz€áááŒ5Šüü|víÚÅþýûÙ²e óæÍCN§Ó´µX,<ñÄlÚ´©®“@‡¿¾‹+hKJJÈÌÌtÛqÂÃÃY¸p!õb 77—ÐÐP$ñöÛoóùçŸç&<%%…“'OÖ±÷þ…ÚAÙ·oQQQnÎûöí˵k× `€¡C‡š¶ƒ ¢¢‚ììl Ã0û‚‚‚nwî|?     7xÃ0¸|ùòmá ‘„¿¿ÿ­€,Y²«ÕjŽg³Ù˜:uj]“ÿ¹ø‡€2€¼¼<¬V+v»ÝtÖ¬Y3$±råÊ; èÒ¥ /½ôW®\i°îܹ昮ñÇŽ[×$íÏÀ{?lܸ›ÍF»víhÙ²%’xä‘G8pà†aУG; ÈÈÈ ººúŽ6ƒ6géé§ŸF3fÌpu_"ÿ¨€‰.\ ,,Œ   -Z„$¬V+{öì sçÎX­Vvî¬ÙœöíÛGVV‰‰‰DGGÍ£>Êk¯½Æ®]»n+àòåË4iÒI¼õÖ[ÄÇÇãt:Ù½{·Ëä“?\4h’X½z5)))HbäÈ‘¦ã½{÷bµZ‰¥wïÞH" €ž={’••Å›o¾É /¼`þ“O>ÉÑ£G1{öl$É¡C‡ðòò¢mÛ¶TUU¹L’~¯€×…KIIáüùóØívœN'/^ts>€ž={"‰µk׺uëÖ /// ¸ï¾û°Ùl å;n­¼¼œÄÄDÚ´iCUU¤eË–xzzRTT„Õj¥yóæ¼øâ‹Hbݺu®×ÿ}'ø@`+À´iÓÄ„ ¨®®&00€€*++M/¿üILš4 €M›6a·Ûñññá›o¾¹£ˆƒb·ÛY¶lýúõs;è:tè€$Nž<ɪU«D¿~ý\¯»ü(j—@RR’عs'ÇŽCIIInC‡ÅÃÃÃívå-‡Ã¡‚‚5kÖLÚ¿¿›ÝÆÕªU+õîÝ[)))úì³ÏT\\l2X,7§Ó©¨  À5ÄZ$í—¤þýûkñâź~ýº|||$IÇ—$5mÚT’TYY©àà`mÛ¶M6›M¹¹¹ºpá‚>úè#IÒ?þ¨¤¤$…††*,,L;v”aZ¾|¹*++Õ·o_•””H’Š‹‹µ|ùr¥¤¤H’š7o®äädeffêÆL¿.___9rÄ&©RÀ ¨9\:tè@›6mX°`Yò•——³víZ$…ÕjÅÛÛ›¥K—RZZJpp0âïï_¯ÄtåM“'OF¯¼ò Ps@y{{×­Â())!..ŽÇILŸ>€xItïÞ‡ÃÁ¼yó\¯|.Àâ äòòr&NœèâÚT»-nÞ¼™ØØX’““¹zõ*k×®Åb±Ü¶ˆwýÍ›7iݺ56›Q£F!‰™3gÖ ìk×®™gPhh(IIIfít:™>}zÝó ›kùfŽPRRÂc=†$ºuëÆªU«&&&Æüv$1dÈ„‹‹ãرc&ÜŠ+̾#F4¸3ôéÓÃ0˜2eŠ™Â4nܘóçÏ×5›îP€ NÌœ9I$&&ðì³Ï"‰ôôt3cœ5k'NœÀb±Ð¾}ûzK(;;›5kÖ0wî\ÒÓÓñôô4ËЫW¯6_VV†¿¿?>ø £Gvû²¨)n†êv H…šìÐÇÇÃ0(**bÙ²eHb̘1´hÑ‚þýûS]]m9›6mâÌ™3¼üòË4mÚ´Þl4jÔˆ‘#G’ššŠ$¶nÝÚ €¥K—š§~ii)5Bß~û­ËdÒmákب¹›aذaHbàÀ”––âïïO£F ãçŸ~-BöïßoBTUUa·ÛÉÎÎfË–-?~Ü<]3Û€êêjâãã‘DQQ‘™P¶oßÞeR„ßQ@­ˆl€Ã‡ãááÍfã‡~`ìØ±ænä ¤õë×#Õ¿2´Ûí¬X±¢dvv6’ÜbÃÕV®\‰$žzê)Š‹‹ ¾5…_þ›ðµÆ‡$8sæ ~~~øùù™uóæMBBBèÚµëo ¨®®¦M›6DEEÕƒ¯¨¨à¡‡Â0 ¾þúk†ޤºãVÿ.µ"†üòË/DGG#‰œœ3/JII1øá‡HbâĉwðÎ;ï ‰÷ß¿žW =hÐ ±X,øúúràÀ—Éìß _+À |5µ±——V«•Õ«WÓ¹sg$1wî\ 33I$''³{÷n7GŽaøðá†Ajjj½+–íÛ·c³ÙˆŽŽfÇŽ"‰åË—»LÎÿ€Z÷S»”òóó±Z­xzz’ŸŸOTTžžžlÞ¼ÙY¸p¡Ûi^•øûû“““ã–Ž»Ä…‡‡ãïïOAAaaa·ÖUÀ¾Žˆ§k!??»Ýއ‡“'O&<<___vìØa•••QPP€Õj¥sçÎdggóñÇ7¸ïŸ8q‚æÍ›ãííÍÔ©S j¨pú¯? _GD_j¶0¾øâ BBB0 ƒgžy†èèhüüüX²d‰Üív!WÛ²e ‘––†‡‡žžž¼÷Þ{uÍrïþ7 æÆÂaaaæv—––f&g·pýúu²³³±Z­˜^­[·®û@Î=ƒ¯#¢ pj«¼¼U ãj4,´pÀV±"4L$eÎ@.ArBù™èY a~m€y÷Y])Q8tN¸L×ô™ÌÜžt2»ó"•¡I §µŸ Ÿo=CS±Èdå)æ_·ñ_óAFË(ÓÁIEND®B`‚ostinato-1.3.0/client/icons/anime_error.gif000066400000000000000000000415751451413623100207510ustar00rootroot00000000000000GIF89a÷  !!!"""###$$$%%%&&&'''((()))***+++,,,---...///000111222333444555666777888999:::;;;<<<===>>>???@@@AAABBBCCCDDDEEEFFFGGGHHHIIIJJJKKKLLLMMMNNNOOOPPPQQQRRRSSSTTTUUUVVVWWWXXXYYYZZZ[[[\\\]]]^^^___```aaabbbcccdddeeefffggghhhiiijjjkkklllmmmnnnooopppqqqrrrssstttuuuvvvwww~Œ®ª§ÇÁ½æÝØõêæúíêûîëûîëûîëûïëûïëûðëûñìûñìûñìûòìûòíûóïûõòûõóûöôûöôûöôûöôûõôûõóûôóüóóüòôüòôüòôüñôüñôüîôüâôü½öý…ùþ<üþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþÿÿþþþþþþÿÿ!ÿ NETSCAPE2.0!ù û,q÷ H° Aƒ\¸ï’£‡Ž1ÑÑ%DN|ˆHbCŒ—:DtQ`GŒ r$iÐ%*#^ôèȘm6Ú¨çË*/ºtX³äAŒ5öäȦ.¡ZÜèªÃ—•V½ä4+Á‡^à !ù û,‡  !!!"""###$$$%%%&&&'''((()))***+++,,,---...///000111222333444555666777888999:::;;;<<<===>>>???@@@AAABBBCCCDDDEEEFFFGGGHHHIIIJJJKKKLLLMMMNNNOOOPPPQQQRRRSSSTTTUUUVVVWWWXXXYYYZZZ[[[\\\]]]^^^___```aaabbbcccdddeeefffggghhhiiijjjkkklllmmmnnnooopppqqqrrrssstttuuuvvvwwwxxx›•“»°­ÜËÇíÙÕóÞÚôßÛõàÛõáÛõáÛ÷áÛúâÛûâÜûâÜûáÜûáÜûàÜûáÜûãÝûåàûæâûæâûæãûæãûçäûçäûèåûéæûêèúêêûëêûíìûîìûïíûðïüñðüñðüòïüòïüñîüíîüáîüÉïýŸòýVøþüþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþÿÿþþþþþþÿÿ˜÷ H° Á‚ž&Mòtð`'G˜0uª¤Sà ;Mr¤h’FG5>T$Dˆ u2Ir!BôTi#Ì’ÕìdP¥"“„J¾ôƒIfÁ“/ 1$ä‡P JF &œäTgQDˆUâi0¦¡ù!ê§!&ˆMªäQQ‹ lŠ6jC¶k+9òSébÁN1ôK¸a@!ù û,‡  !!!"""###$$$%%%&&&'''((()))***+++,,,---...///000111222333444555666777888999:::;;;<<<===>>>???@@@AAABBBCCCDDDEEEFFFGGGHHHIIIJJJKKKLLLMMMNNNOOOPPPQQQRRRSSSTTTUUUVVVWWWXXXYYYZZZ[[[\\\]]]^^^___```aaabbbcccdddeeefffggghhhiiijjjkkklllmmmnnnooopppqqqrrrzxxŠ„ƒ«šÄ¯«Ú¿ºêËÆðÏÊóÐËôÑËôÐËôÐËõÑËøÓËûÓÌûÓÌûÓÌûÓÌûÔÌúÔÍùÕÏö×Ñõ×Óõ×ÓõØÓö×Ô÷ØÕøÚÖùÜØúÝÙúÞÚøÞÛøàÛøàÜøáÜùâÜúâÝûãÞûåàûæâûèãûéäûèåüãæüØèüÅêýíýXôþ"úþ ýþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþÿÿþþþþþþÿÿ¢÷ H° Á‚› AÚtð ¦F•2aª„¨Rà 7Uj1¡¢‹™"$ОM"5T´‰P#’ûF"Ú´ç ÆJ#a"´G¡Å‚ZBsO@•jD„i(!@ îÙS©ÁL• IØÇ¨"H? îÉ$U`Ÿ³•ò4Œ¤(SWHŠõ¡ø±áž¶ºVD¤öâ>E}"Uª)"¿ÓæQ‰¸±Á€!ù û,‡  !!!"""###$$$%%%&&&'''((()))***+++,,,---...///000111222333444555666777888999:::;;;<<<===>>>???@@@AAABBBCCCDDDEEEFFFGGGHHHIIIJJJKKKLLLMMMNNNOOOPPPQQQRRRSSSTTTUUUVVVWWWXXXYYYZZZ[[[\\\]]]^^^___```aaabbbcccdddeeefffggghhhiiijjjkkklllmmmnnnwtszxŸŽ‹Å¨¢à¸±î¿¸ñÀ»òÁ»ó»óüóļõż÷ƼùǼúɼûʽúʾùÊ¿÷ÊÀöÊÂõÊÃõËÃöÌÄõÍÅõÎÇõÐÉõÑÊõÓËõÔÌõÖÍö×ÎöØÐöÙÑ÷ÚÑùÚÒúÚÒûÛÔûÜÕûÞ×ûàÙûâÛûâÜüâÜüßÝüÓßü½âý”èýXñþ!ùþ ýþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþÿÿþþþþþþÿÿª÷ H° Á‚•)ªtðà$C“2Uš4ˆQÊe’TH‘¤LŠ]œ¤±!~TÔPe!?…òã§’žƒ%¥ôƒRžŒ ºœ)rŸž;~îLºcpP¥B3õdÚç§Î;è„$‰QR=“ ]½sHáA=šŽ2Ýw§NMk 2÷Ž¢In! *z0]EWA24ç¢L»Œ )¢“Ç0ÁCtèrLù`@!ù û,‡  !!!"""###$$$%%%&&&'''((()))***+++,,,---...///000111222333444555666777888999:::;;;<<<===>>>???@@@AAABBBCCCDDDEEEFFFGGGHHHIIIJJJKKKLLLMMMNNNOOOPPPQQQRRRSSSTTTUUUVVVWWWXXXYYYZZZ[[[\\\]]]^^^___```aaabbbcccdddeeefffggghhhiiijjjkkkspo‚yw™†‚·—×§žã®¤é±¨í²ªð²«ó³«ó´«ôµ«ô·¬ô¸¬ô¹¬õ»¬ö¼¬÷½­ø¾®ù¾¯ù¿°ø¿²ô¿³ó¿´ó¿´õÀ¶õÁ¸õúôüôÆ¿õÇÁõÈÂõÊÃöËÄöÌÅöÎÇ÷ÐÈ÷ÑÉøÒÊùÔÌú×ÏûÙÒûÚÓûÛÔûÛÕûØÖüÌÙü»Üü¡àýxçýGðþøþ ýþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþÿÿþþþþþþÿÿ¦÷ H° Á‚“%ztðà£A’,M’4(Qà /= ´ðR"=%º(@>~,%"ÔpI>&÷ñÑ£ÇÒ‡’6¢¨'žDŒd)Jûðà©£gRƒ|& òƒG¦¤uêà‘ô´ £Gô(}”)ëD‰¬©´ŽÀ9uæ8m‰HÒRF‰âR”sñ¬¤¼~u½¨gÎB‘qø^$(8ŽÚÅ!ù û,‡  !!!"""###$$$%%%&&&'''((()))***+++,,,---...///000111222333444555666777888999:::;;;<<<===>>>???@@@AAABBBCCCDDDEEEFFFGGGHHHIIIJJJKKKLLLMMMNNNOOOPPPQQQRRRSSSTTTUUUVVVWWWXXXYYYZZZ[[[\\\]]]^^^___```aaabbbcccdddeeefffggghhhiii†wsŸ‚|½‡Ù›âŸ”硗ꢘ줛쥜列ó§œó¨œó¨œó©œóªœó¬œó®ó°žó° ô±¡õ²¢ö²¢ö²£÷²£÷³¤÷´¦õµ¨ô¶ªô¸¬ô»°ô½²õ¾´õÁ¶ö÷öĹ÷ƺøÈ¼ùÊ¿ùÍÂøÏÇøÑÊøÒÌøÓÍùÖÐúÚÔûàÚûãÝûâàüØáüÉäü­èýŒîýYõþ,úþýþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþÿÿþþþþþþÿÿ·÷ H° A‚™¤(ÓAƒ‰ 9‚É‘ŸBJT©Q¡B*%Ò“1‘¢J‚üÜ£§’!•3ù©´'Ðôà™$ÇaÁEŽÙ¼¹ÑD‰`ô©žœñСg’ƒz&å4ê*ÕGm j¤È(F“¨Æ)tHNL9i© Œ$7> ò)”6Òºä\}è¦Ð£CqâzT(lÆ}‚5jtÈÍàÇû2ÉiÓ¦'æÏ!ù û,‡  !!!"""###$$$%%%&&&'''((()))***+++,,,---...///000111222333444555666777888999:::;;;<<<===>>>???@@@AAABBBCCCDDDEEEFFFGGGHHHIIIJJJKKKLLLMMMNNNOOOPPPQQQRRRSSSTTTUUUVVVWWWXXXYYYZZZ[[[\\\]]]^^^___```aaabbbcccdddeeefffggghhhiiijjjkkklllmmmnnnooopppqqqrrrssstttuuuvvvwwwxxxyyyzzz{{{|||‹‹‹ Ÿ °°°¾½¾ÈÈÈÑÑÑÚÙÚãáâêèèïëìóîï÷ññøïïùíìùëéúèæúæäùäàùáÝøßÚøÝ×øÚÓùØÐùÖÍúÔÊúÒÈúÏÄúÍÁúʾúɽøÇ»÷ź÷øöÁµö¿³ö¼¯öº®ö¸«ô¶ªôµ©ó´¨ó²¦ó¯£ò¬Ÿñªœñ©™ñ¦–ñ¥•ñ¥“ñ¤“ð£’ð¡òŸŽñžñŒïœŒî›‹ë™‹ë—‹é•Šè’ˆä‡æ†‹íj¦ø'Þþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþÿÿþþþþþþÿÿ¼÷ H° A‚B¡JeªÑÁ‚ˆL¥ò”)“§U©=܇(Õ¨K T¡uÉ-M}TµJàªU˜N½:ØhÕ¥—-÷ÅŠ%ë’-HGõ2–@Y²f‘5³ MU;] œe‹j']ieZå ©ÃY·nÙú„µà©O£’Ú µ)ì­S¦l„¤µê-au]•tðUª¶·H‘Ò¥ëbYƒˆt¥Êt*lªN¯v¡<˜X—©PŸNž¼’­]»pñÝHzc@!ù û,‡  !!!"""###$$$%%%&&&'''((()))***+++,,,---...///000111222333444555666777888999:::;;;<<<===>>>???@@@AAABBBCCCDDDEEEFFFGGGHHHIIIJJJKKKLLLMMMNNNOOOPPPQQQRRRSSSTTTUUUVVVWWWXXXYYYZZZ[[[\\\]]]^^^___```aaabbbcccdddeeefffggghhhiiijjjkkklllmmmnnnooopppqqqrrrssstttuuuvvvwwwxxx€€£Ÿž³­¬À¸·ÊÁÀÒÉÇÛÐÏå×ÕìÝÚñàÝõâßøâÞùâÝúßÚúÝÖúÚÓùÖÏùÔËùÒÉùÑÇøÐÆøÎÃ÷ÌÁøË¿ùɽùǺùĸøÂ¶øÀµ÷¾´ö¼²õ»±õ¸­ô¶ªô³¦ó±£ò¯¡ò­Ÿñ«ð¨šï¦—îŸïŒð›ˆóšƒó˜ó—ó–~ò”}ò“}ò’|ñ‘|ï|ì|ëŠ{ê‰{é‡zæ…wã‚uâ~uáxwêc•ô8Åþýþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþÿÿþþþþþþÿÿ½÷ H° A‚‰>•**ÑÁ‚†D•ÚTé'T¥ =Üg¨Ô'Iœr’$ –ƃ¢>>???@@@AAABBBCCCDDDEEEFFFGGGHHHIIIJJJKKKLLLMMMNNNOOOPPPQQQRRRSSSTTTUUUVVVWWWXXXYYYZZZ[[[\\\]]]^^^___```aaabbbcccdddeeefffgggoihƒniŸqg³rdÃsbÊq^ÒubÛudàufåugçvgéyië{jë{jë}kìkë€lë‚líƒmð…nòˆoó‰qóŠqó‹sòŒtòŒtòŒtòuðwîyí‘}î•ïœñŸò Žó¡Žó£’ô¥•õ©™ô«›ö¬žö¯¡õ²¥ô´¨ó¶©õ¹ªö»¬÷½­÷¾®÷¿¯÷¿°öÀ²öÁ´ö¶öĸöǽ÷ÌÃöÑÉøÕÎùØÒúÜÖûßÚûâÝûåáûëèüïïüèïüÚðü¼õýœùý{ûþFýþþþ þþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþÿÿþþþþþþÿÿÆ÷ H° Á­H=2d(©V ªJdh’&M–BqŸ*C@I:4hR'Fy8l•èQ¨Aƒþùà QˆIê3æ>>|òpÊÊ`¢I’͘'ÏF( ⤨À;tèÜ©ÔÆ`Mƒòå˜UŽ®+>rJg&9qâ,ZäÆ`¨¯XéŒç’L+óz‡Ñ¢¸• µÁYðÔ›C—щcè’âRO¹y³hÒ¤EmÚ`&7¡Û`bLº5Á€!ù û,‡  !!!"""###$$$%%%&&&'''((()))***+++,,,---...///000111222333444555666777888999:::;;;<<<===>>>???@@@AAABBBCCCDDDEEEFFFGGGHHHIIIJJJKKKLLLMMMNNNOOOPPPQQQRRRSSSTTTUUUVVVWWWXXXYYYZZZ[[[\\\]]]^^^___```aaabbbcccdddeeefffgggoihƒniŸqg³rdÃsbÊq^ÒubÛudàufåugçvgéyië{jë{jë}kìkë€lë‚líƒmð…nòˆoó‰qóŠqó‹sòŒtòŒtòŒtòuðwîyí‘}î•ïœñŸò Žó¡Žó£’ô¥•õ©™ô«›ö¬žö¯¡õ²¥ô´¨ó¶©õ¹ªö»¬÷½­÷¾®÷¿¯÷¿°öÀ²öÁ´ö¶öĸöǽ÷ÌÃöÑÉøÕÎùØÒúÜÖûßÚûâÝûåáûëèüïïüèïüÚðü¼õýœùý{ûþFýþþþ þþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþÿÿþþþþþþÿÿÆ÷ H° Á­H=2d(©V ªJdh’&M–BqŸ*C@I:4hR'Fy8l•èQ¨Aƒþùà QˆIê3æ>>|òpÊÊ`¢I’͘'ÏF( ⤨À;tèÜ©ÔÆ`Mƒòå˜UŽ®+>rJg&9qâ,ZäÆ`¨¯XéŒç’L+óz‡Ñ¢¸• µÁYðÔ›C—щcè’âRO¹y³hÒ¤EmÚ`&7¡Û`bLº5Á€!ù û,‡  !!!"""###$$$%%%&&&'''((()))***+++,,,---...///000111222333444555666777888999:::;;;<<<===>>>???@@@AAABBBCCCDDDEEEFFFGGGHHHIIIJJJKKKLLLMMMNNNOOOPPPQQQRRRSSSTTTUUUVVVWWWXXXYYYZZZ[[[\\\]]]^^^___```aaabbbcccdddeeefffggghhhiiijjjkkklllmmmnnnooopppqqqrrrssstttuuuvvvwwwxxx€€£Ÿž³­¬À¸·ÊÁÀÒÉÇÛÐÏå×ÕìÝÚñàÝõâßøâÞùâÝúßÚúÝÖúÚÓùÖÏùÔËùÒÉùÑÇøÐÆøÎÃ÷ÌÁøË¿ùɽùǺùĸøÂ¶øÀµ÷¾´ö¼²õ»±õ¸­ô¶ªô³¦ó±£ò¯¡ò­Ÿñ«ð¨šï¦—îŸïŒð›ˆóšƒó˜ó—ó–~ò”}ò“}ò’|ñ‘|ï|ì|ëŠ{ê‰{é‡zæ…wã‚uâ~uáxwêc•ô8Åþýþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþÿÿþþþþþþÿÿ½÷ H° A‚‰>•**ÑÁ‚†D•ÚTé'T¥ =Üg¨Ô'Iœr’$ –ƃ¢>>???@@@AAABBBCCCDDDEEEFFFGGGHHHIIIJJJKKKLLLMMMNNNOOOPPPQQQRRRSSSTTTUUUVVVWWWXXXYYYZZZ[[[\\\]]]^^^___```aaabbbcccdddeeefffggghhhiiijjjkkklllmmmnnnooopppqqqrrrssstttuuuvvvwwwxxxyyyzzz{{{|||‹‹‹ Ÿ °°°¾½¾ÈÈÈÑÑÑÚÙÚãáâêèèïëìóîï÷ññøïïùíìùëéúèæúæäùäàùáÝøßÚøÝ×øÚÓùØÐùÖÍúÔÊúÒÈúÏÄúÍÁúʾúɽøÇ»÷ź÷øöÁµö¿³ö¼¯öº®ö¸«ô¶ªôµ©ó´¨ó²¦ó¯£ò¬Ÿñªœñ©™ñ¦–ñ¥•ñ¥“ñ¤“ð£’ð¡òŸŽñžñŒïœŒî›‹ë™‹ë—‹é•Šè’ˆä‡æ†‹íj¦ø'Þþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþÿÿþþþþþþÿÿ¼÷ H° A‚B¡JeªÑÁ‚ˆL¥ò”)“§U©=܇(Õ¨K T¡uÉ-M}TµJàªU˜N½:ØhÕ¥—-÷ÅŠ%ë’-HGõ2–@Y²f‘5³ MU;] œe‹j']ieZå ©ÃY·nÙú„µà©O£’Ú µ)ì­S¦l„¤µê-au]•tðUª¶·H‘Ò¥ëbYƒˆt¥Êt*lªN¯v¡<˜X—©PŸNž¼’­]»pñÝHzc@!ù û,‡  !!!"""###$$$%%%&&&'''((()))***+++,,,---...///000111222333444555666777888999:::;;;<<<===>>>???@@@AAABBBCCCDDDEEEFFFGGGHHHIIIJJJKKKLLLMMMNNNOOOPPPQQQRRRSSSTTTUUUVVVWWWXXXYYYZZZ[[[\\\]]]^^^___```aaabbbcccdddeeefffggghhhiii†wsŸ‚|½‡Ù›âŸ”硗ꢘ줛쥜列ó§œó¨œó¨œó©œóªœó¬œó®ó°žó° ô±¡õ²¢ö²¢ö²£÷²£÷³¤÷´¦õµ¨ô¶ªô¸¬ô»°ô½²õ¾´õÁ¶ö÷öĹ÷ƺøÈ¼ùÊ¿ùÍÂøÏÇøÑÊøÒÌøÓÍùÖÐúÚÔûàÚûãÝûâàüØáüÉäü­èýŒîýYõþ,úþýþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþÿÿþþþþþþÿÿ·÷ H° A‚™¤(ÓAƒ‰ 9‚É‘ŸBJT©Q¡B*%Ò“1‘¢J‚üÜ£§’!•3ù©´'Ðôà™$ÇaÁEŽÙ¼¹ÑD‰`ô©žœñСg’ƒz&å4ê*ÕGm j¤È(F“¨Æ)tHNL9i© Œ$7> ò)”6Òºä\}è¦Ð£CqâzT(lÆ}‚5jtÈÍàÇû2ÉiÓ¦'æÏ!ù û,‡  !!!"""###$$$%%%&&&'''((()))***+++,,,---...///000111222333444555666777888999:::;;;<<<===>>>???@@@AAABBBCCCDDDEEEFFFGGGHHHIIIJJJKKKLLLMMMNNNOOOPPPQQQRRRSSSTTTUUUVVVWWWXXXYYYZZZ[[[\\\]]]^^^___```aaabbbcccdddeeefffggghhhiiijjjkkkspo‚yw™†‚·—×§žã®¤é±¨í²ªð²«ó³«ó´«ôµ«ô·¬ô¸¬ô¹¬õ»¬ö¼¬÷½­ø¾®ù¾¯ù¿°ø¿²ô¿³ó¿´ó¿´õÀ¶õÁ¸õúôüôÆ¿õÇÁõÈÂõÊÃöËÄöÌÅöÎÇ÷ÐÈ÷ÑÉøÒÊùÔÌú×ÏûÙÒûÚÓûÛÔûÛÕûØÖüÌÙü»Üü¡àýxçýGðþøþ ýþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþÿÿþþþþþþÿÿ¦÷ H° Á‚“%ztðà£A’,M’4(Qà /= ´ðR"=%º(@>~,%"ÔpI>&÷ñÑ£ÇÒ‡’6¢¨'žDŒd)Jûðà©£gRƒ|& òƒG¦¤uêà‘ô´ £Gô(}”)ëD‰¬©´ŽÀ9uæ8m‰HÒRF‰âR”sñ¬¤¼~u½¨gÎB‘qø^$(8ŽÚÅ!ù û,‡  !!!"""###$$$%%%&&&'''((()))***+++,,,---...///000111222333444555666777888999:::;;;<<<===>>>???@@@AAABBBCCCDDDEEEFFFGGGHHHIIIJJJKKKLLLMMMNNNOOOPPPQQQRRRSSSTTTUUUVVVWWWXXXYYYZZZ[[[\\\]]]^^^___```aaabbbcccdddeeefffggghhhiiijjjkkklllmmmnnnwtszxŸŽ‹Å¨¢à¸±î¿¸ñÀ»òÁ»ó»óüóļõż÷ƼùǼúɼûʽúʾùÊ¿÷ÊÀöÊÂõÊÃõËÃöÌÄõÍÅõÎÇõÐÉõÑÊõÓËõÔÌõÖÍö×ÎöØÐöÙÑ÷ÚÑùÚÒúÚÒûÛÔûÜÕûÞ×ûàÙûâÛûâÜüâÜüßÝüÓßü½âý”èýXñþ!ùþ ýþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþÿÿþþþþþþÿÿª÷ H° Á‚•)ªtðà$C“2Uš4ˆQÊe’TH‘¤LŠ]œ¤±!~TÔPe!?…òã§’žƒ%¥ôƒRžŒ ºœ)rŸž;~îLºcpP¥B3õdÚç§Î;è„$‰QR=“ ]½sHáA=šŽ2Ýw§NMk 2÷Ž¢In! *z0]EWA24ç¢L»Œ )¢“Ç0ÁCtèrLù`@!ù û,‡  !!!"""###$$$%%%&&&'''((()))***+++,,,---...///000111222333444555666777888999:::;;;<<<===>>>???@@@AAABBBCCCDDDEEEFFFGGGHHHIIIJJJKKKLLLMMMNNNOOOPPPQQQRRRSSSTTTUUUVVVWWWXXXYYYZZZ[[[\\\]]]^^^___```aaabbbcccdddeeefffggghhhiiijjjkkklllmmmnnnooopppqqqrrrzxxŠ„ƒ«šÄ¯«Ú¿ºêËÆðÏÊóÐËôÑËôÐËôÐËõÑËøÓËûÓÌûÓÌûÓÌûÓÌûÔÌúÔÍùÕÏö×Ñõ×Óõ×ÓõØÓö×Ô÷ØÕøÚÖùÜØúÝÙúÞÚøÞÛøàÛøàÜøáÜùâÜúâÝûãÞûåàûæâûèãûéäûèåüãæüØèüÅêýíýXôþ"úþ ýþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþÿÿþþþþþþÿÿ¢÷ H° Á‚› AÚtð ¦F•2aª„¨Rà 7Uj1¡¢‹™"$ОM"5T´‰P#’ûF"Ú´ç ÆJ#a"´G¡Å‚ZBsO@•jD„i(!@ îÙS©ÁL• IØÇ¨"H? îÉ$U`Ÿ³•ò4Œ¤(SWHŠõ¡ø±áž¶ºVD¤öâ>E}"Uª)"¿ÓæQ‰¸±Á€!ù û,‡  !!!"""###$$$%%%&&&'''((()))***+++,,,---...///000111222333444555666777888999:::;;;<<<===>>>???@@@AAABBBCCCDDDEEEFFFGGGHHHIIIJJJKKKLLLMMMNNNOOOPPPQQQRRRSSSTTTUUUVVVWWWXXXYYYZZZ[[[\\\]]]^^^___```aaabbbcccdddeeefffggghhhiiijjjkkklllmmmnnnooopppqqqrrrssstttuuuvvvwwwxxx›•“»°­ÜËÇíÙÕóÞÚôßÛõàÛõáÛõáÛ÷áÛúâÛûâÜûâÜûáÜûáÜûàÜûáÜûãÝûåàûæâûæâûæãûæãûçäûçäûèåûéæûêèúêêûëêûíìûîìûïíûðïüñðüñðüòïüòïüñîüíîüáîüÉïýŸòýVøþüþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþÿÿþþþþþþÿÿ˜÷ H° Á‚ž&Mòtð`'G˜0uª¤Sà ;Mr¤h’FG5>T$Dˆ u2Ir!BôTi#Ì’ÕìdP¥"“„J¾ôƒIfÁ“/ 1$ä‡P JF &œäTgQDˆUâi0¦¡ù!ê§!&ˆMªäQQ‹ lŠ6jC¶k+9òSébÁN1ôK¸a@!ù û,‡  !!!"""###$$$%%%&&&'''((()))***+++,,,---...///000111222333444555666777888999:::;;;<<<===>>>???@@@AAABBBCCCDDDEEEFFFGGGHHHIIIJJJKKKLLLMMMNNNOOOPPPQQQRRRSSSTTTUUUVVVWWWXXXYYYZZZ[[[\\\]]]^^^___```aaabbbcccdddeeefffggghhhiiijjjkkklllmmmnnnooopppqqqrrrssstttuuuvvvwww~Œ®ª§ÇÁ½æÝØõêæúíêûîëûîëûîëûïëûïëûðëûñìûñìûñìûòìûòíûóïûõòûõóûöôûöôûöôûöôûõôûõóûôóüóóüòôüòôüòôüñôüñôüîôüâôü½öý…ùþ<üþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþÿÿþþþþþþÿÿq÷ H° Aƒ\¸ï’£‡Ž1ÑÑ%DN|ˆHbCŒ—:DtQ`GŒ r$iÐ%*#^ôèȘm6Ú¨çË*/ºtX³äAŒ5öäȦ.¡ZÜèªÃ—•V½ä4+Á‡^à ;ostinato-1.3.0/client/icons/anime_warn.gif000066400000000000000000000407111451413623100205560ustar00rootroot00000000000000GIF89a÷  !!!"""###$$$%%%&&&'''((()))***+++,,,---...///000111222333444555666777888999:::;;;<<<===>>>???@@@AAABBBCCCDDDEEEFFFGGGHHHIIIJJJKKKLLLMMMNNNOOOPPPQQQRRRSSSTTTUUUVVVWWWXXXYYYZZZ[[[\\\]]]^^^___```aaabbbcccdddeeefffggghhhiiijjjkkklll~}|§¥¡ËÇÁâÝÖõíåúóêûõëûöìû÷ìûøìûùìûùíûùíûúðûúôûúõûúõûû÷ûüúûýûüýûüúûüñûüÍüý•ýýJýþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþÿÿþþþþþþÿÿ!ÿ NETSCAPE2.0!ù û,p÷ H° Áƒ ܳGáÁ= rXb †Ò¹‘NÆ}7btñ9wyTx1I:wDqI”)gyÇfÏž*i^¼ó’h΃)ý4j4ƒ‚"J*Uç>:'åhÝGNœ¯!ù û,‡  !!!"""###$$$%%%&&&'''((()))***+++,,,---...///000111222333444555666777888999:::;;;<<<===>>>???@@@AAABBBCCCDDDEEEFFFGGGHHHIIIJJJKKKLLLMMMNNNOOOPPPQQQRRRSSSTTTUUUVVVWWWXXXYYYZZZ[[[\\\]]]^^^___```aaabbbcccdddeeefffggghhhiiijjjkkklllmmmnnnooopppqqqrrrssstttuuuvvvwwwxxxyyy‰ˆ…£Ÿ˜Ä¾±ØÐÁéÞÌïåÒóêÖöìÙùðÙúòÛûóÜûôÝûõÝûõÞûõäûöæû÷éûøîûúóûúõûûõûüøüûùüûûüðúüÙûü«üý_ýþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþÿÿþþþþþþÿÿ~÷ H° Aƒ*$ˆÑB…ˆ"5rø°`ÄH+ $IÑ¢H‚4îk¤$ ’"#ÈÆŠˆEZÈP£‰#"2$¨¥!G º¹!C†5¤°äNC|þ :c#¤X³FbJP£H`Ê•èµ  žhYþYËV¤Æ€!ù û,‡  !!!"""###$$$%%%&&&'''((()))***+++,,,---...///000111222333444555666777888999:::;;;<<<===>>>???@@@AAABBBCCCDDDEEEFFFGGGHHHIIIJJJKKKLLLMMMNNNOOOPPPQQQRRRSSSTTTUUUVVVWWWXXXYYYZZZ[[[\\\]]]^^^___```aaabbbcccdddeeefffggghhhiiijjjkkklllmmmnnnooopppqqqrrrssstttuuuvvvwwwxxx€}¸° ØË´è׺îÝÀòáÁóäÄõçÇöêÉøîËúñÌûñÌúñÍùïÒøïØùðÚúóàûôåûöèû÷êûøìûùîûúïûûñûüóüûõüúõüùøüð÷üÎ÷ý˜ùýHüþ þþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþÿÿþþþþþþÿÿ‰÷ H° AƒŠ&\80!† MšdbAJ‰(MJd‘` JŠEúÓq_ÈIþLRÔ¨£!J„RŠT¢¡F“bþ!Ô¨QÍ…‰")ÒIhæOƒ%d¨h¢I~6zêhÑI‡(!¥¤t©×™$ †¤D)Y²‘ÒNy¨ Ÿ·pûÈåC—nÉŽ!ù û,‡  !!!"""###$$$%%%&&&'''((()))***+++,,,---...///000111222333444555666777888999:::;;;<<<===>>>???@@@AAABBBCCCDDDEEEFFFGGGHHHIIIJJJKKKLLLMMMNNNOOOPPPQQQRRRSSSTTTUUUVVVWWWXXXYYYZZZ[[[\\\]]]^^^___```aaabbbcccdddeeefffggghhhiiijjjkkklllmmmnnnooopppqqqrrrsss|zw‘ƒ½²šØÆ¤äЪéÖ°ìØ²ïÛ³ðÞ¶óâ¸õæ»÷ê½ùì½úî¾ûïÁúïÆùîÎùïÖúñÞúóáûöæû÷éûøíûúñûúóüûõü÷õüèöüÁ÷ývúþ%ýþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþÿÿþþþþþþÿÿ•÷ H° Aƒ… \8ðO!† ý4RäbAE…1zhq`ŸF…2òé¸!F{ô(2¤Ð¢ÄAzôjÔÇ¥¢FòÈTt¨æÂñ‘ˆ!†|! 4hП? 1Êse£¦b‚ªˆÐ!ƒ—Ú:¶l#=ýÒØ¨m[FŒÜB;°Ñž»{òìÁ×ᅩHZ !ù û,‡  !!!"""###$$$%%%&&&'''((()))***+++,,,---...///000111222333444555666777888999:::;;;<<<===>>>???@@@AAABBBCCCDDDEEEFFFGGGHHHIIIJJJKKKLLLMMMNNNOOOPPPQQQRRRSSSTTTUUUVVVWWWXXXYYYZZZ[[[\\\]]]^^^___```aaabbbcccdddeeefffggghhhiiijjjkkklllmmmnnnooopppqqqzxu‰}´¥‰Ò¹àÅ—èÌ™éÏžìÔ¡ïØ£ñÞ¨ôãªöå«ùé¬ùê®øé°öç½öèÃ÷éÇøíÎúðÓúòÙûôßû÷äûøçûùëûúïûúóûùõûúôüéôüÀöýwúþ&ýþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþÿÿþþþþþþÿÿ”÷ H° Aƒ\(P¡@ îq¤hOÄ‚…9rDHÑÅ r”çã¾@#óäq(èc G~ðàñãÈbÄ=ˆ`âÉÓQ¡’ rT¨Ê>½\¨Ç"?~õñÓ§¢;i5ž©= 28ñÕ³S¡’,¸ÇÐÆ·pß*‘Ê;xóÚ±S§oC&/!ù û,‡  !!!"""###$$$%%%&&&'''((()))***+++,,,---...///000111222333444555666777888999:::;;;<<<===>>>???@@@AAABBBCCCDDDEEEFFFGGGHHHIIIJJJKKKLLLMMMNNNOOOPPPQQQRRRSSSTTTUUUVVVWWWXXXYYYZZZ[[[\\\]]]^^^___```aaabbbcccdddeeefffggghhhiiijjjkkklllmmmnnnooopppyws˜Žz¼¨‚Ò´Û¾ˆäÀ„äÆéÍíÓ•õâ™öä™öåšõãžõá¡óߪôà°õä¶÷ç»÷èÁøëÈùîÏúðÔúñÖûóÛûõâû÷çûùêûùîûøñûøóüðóüÚõü¬÷ýhúþ"ýþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþÿÿþþþþþþÿÿ”÷ H° Aƒ€\(Q @ŽÔã(Q‰ räH!Œó8"DÈûô”¼ƒÑ @ ÿ8òsçG%æAäˆOM>ˆ \H¢ >ylòYÑB‘†øH*h‘ƒ-:êS)E€`ôã(êÔ³7ïÌcˆ£ÛEŽà©v !>>???@@@AAABBBCCCDDDEEEFFFGGGHHHIIIJJJKKKLLLMMMNNNOOOPPPQQQRRRSSSTTTUUUVVVWWWXXXYYYZZZ[[[\\\]]]^^^___```aaabbbcccdddeeefffggghhhiiijjjkkklllmmmnnn~yo®šrË©o×±qÞ¸sâ¼wãÁ{æÄ|èÈ‚ê̈íÒŒï×òÞõâŒõãŒõâŽõâõá©÷å³÷è½øéÃùíÉúîÎúñÑûôØûöàûøèûùíûøðûøóüöóüæôüËöýšøýSûþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþÿÿþþþþþþÿÿž÷ H° Aƒ{\(° @ŠÄÉb¤(Ð Œ)ÄÈÈ}{ )ªSç =ýaÔ‡eFyIÄcˆf:|ªÃ0 Fúةç¢<„Þat¨Õ=|øR$çà’Iù°ÌÚç ‹sꓵ-Û›s â)Ĩ®]Eíê:P:€ç–CXNœÃ OJ !ù û,‡  !!!"""###$$$%%%&&&'''((()))***+++,,,---...///000111222333444555666777888999:::;;;<<<===>>>???@@@AAABBBCCCDDDEEEFFFGGGHHHIIIJJJKKKLLLMMMNNNOOOPPPQQQRRRSSSTTTUUUVVVWWWXXXYYYZZZ[[[\\\]]]^^^___```aaabbbcccdddeeefffggghhhiiijjjkkklllmmmnnn}xmžj¾¢fΧ_Ø©WÛ²`ß·bá»käÀlêÊpñÚwòÝxòÝzñØ€ðׄñ׌òÙ–ôÜŸõߥöáªöã²÷æ¾øëÈùíÍúïÓúñØûóÞûõâû÷çû÷ëûöíü÷ðüäðü»óýuøþ%üþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþÿÿþþþþþþÿÿ¡÷ H° Aƒ}þ\(p @‡Äã(‰ rÄH#Æm¬óq_BŒêÔI4ÈÆDí¡Sg£>‰$â9ĨÊ=‰ ‘\øÐQ =wjîiôgÐÂ;Ž íÙÓ§=Í9ØG££3ë\Õ³‚ú8*¤gªÛ«{ÑQDO!GxóräÈÈ!9©¬CgŽáÆåÈ9[Rb@!ù û,‡  !!!"""###$$$%%%&&&'''((()))***+++,,,---...///000111222333444555666777888999:::;;;<<<===>>>???@@@AAABBBCCCDDDEEEFFFGGGHHHIIIJJJKKKLLLMMMNNNOOOPPPQQQRRRSSSTTTUUUVVVWWWXXXYYYZZZ[[[\\\]]]^^^___```aaabbbcccdddeeefffggghhhiiijjjkkkspiž‰a¼™XËšJÔ LÙ§MÛ¬Wß°VáµYäºZçÀaéÆfìÌjïÔjòÙjòÚióÛkòÙtò×óÚ˜õÞ¢öâ­÷ç¹÷éÄ÷ëÈøíÍùñÕúóÛûöáû÷çûøìû÷îüøïüîðüÕòý“õý;úþ þþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþÿÿþþþþþþÿÿ¤÷ H° Aƒxð\(Ð?ƒ¼Ãȉýb¤èÏŒÕa¢9Š@â ¤HŽDî`,„‡—|݉ÈÐN!E7åì)4hC?:ò©#”O"¤,ćOŸ<{öB‡B“ªºÌº‘9 ÕT•Ù¬9á"Xg#ŽŠ8ÞUÄ—Q 8ýÌ™§p8ˆá¼Yü-HŒ!ù û,‡  !!!"""###$$$%%%&&&'''((()))***+++,,,---...///000111222333444555666777888999:::;;;<<<===>>>???@@@AAABBBCCCDDDEEEFFFGGGHHHIIIJJJKKKLLLMMMNNNOOOPPPQQQRRRSSSTTTUUUVVVWWWXXXYYYZZZ[[[\\\]]]^^^___```aaabbbcccdddeeefffggghhhiiijjjkkkspiž‰a¼™XËšJÔ LÙ§MÛ¬Wß°VáµYäºZçÀaéÆfìÌjïÔjòÙjòÚióÛkòÙtò×óÚ˜õÞ¢öâ­÷ç¹÷éÄ÷ëÈøíÍùñÕúóÛûöáû÷çûøìû÷îüøïüîðüÕòý“õý;úþ þþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþÿÿþþþþþþÿÿ¤÷ H° Aƒxð\(Ð?ƒ¼Ãȉýb¤èÏŒÕa¢9Š@â ¤HŽDî`,„‡—|݉ÈÐN!E7åì)4hC?:ò©#”O"¤,ćOŸ<{öB‡B“ªºÌº‘9 ÕT•Ù¬9á"Xg#ŽŠ8ÞUÄ—Q 8ýÌ™§p8ˆá¼Yü-HŒ!ù û,‡  !!!"""###$$$%%%&&&'''((()))***+++,,,---...///000111222333444555666777888999:::;;;<<<===>>>???@@@AAABBBCCCDDDEEEFFFGGGHHHIIIJJJKKKLLLMMMNNNOOOPPPQQQRRRSSSTTTUUUVVVWWWXXXYYYZZZ[[[\\\]]]^^^___```aaabbbcccdddeeefffggghhhiiijjjkkklllmmmnnn}xmžj¾¢fΧ_Ø©WÛ²`ß·bá»käÀlêÊpñÚwòÝxòÝzñØ€ðׄñ׌òÙ–ôÜŸõߥöáªöã²÷æ¾øëÈùíÍúïÓúñØûóÞûõâû÷çû÷ëûöíü÷ðüäðü»óýuøþ%üþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþÿÿþþþþþþÿÿ¡÷ H° Aƒ}þ\(p @‡Äã(‰ rÄH#Æm¬óq_BŒêÔI4ÈÆDí¡Sg£>‰$â9ĨÊ=‰ ‘\øÐQ =wjîiôgÐÂ;Ž íÙÓ§=Í9ØG££3ë\Õ³‚ú8*¤gªÛ«{ÑQDO!GxóräÈÈ!9©¬CgŽáÆåÈ9[Rb@!ù û,‡  !!!"""###$$$%%%&&&'''((()))***+++,,,---...///000111222333444555666777888999:::;;;<<<===>>>???@@@AAABBBCCCDDDEEEFFFGGGHHHIIIJJJKKKLLLMMMNNNOOOPPPQQQRRRSSSTTTUUUVVVWWWXXXYYYZZZ[[[\\\]]]^^^___```aaabbbcccdddeeefffggghhhiiijjjkkklllmmmnnn~yo®šrË©o×±qÞ¸sâ¼wãÁ{æÄ|èÈ‚ê̈íÒŒï×òÞõâŒõãŒõâŽõâõá©÷å³÷è½øéÃùíÉúîÎúñÑûôØûöàûøèûùíûøðûøóüöóüæôüËöýšøýSûþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþÿÿþþþþþþÿÿž÷ H° Aƒ{\(° @ŠÄÉb¤(Ð Œ)ÄÈÈ}{ )ªSç =ýaÔ‡eFyIÄcˆf:|ªÃ0 Fúةç¢<„Þat¨Õ=|øR$çà’Iù°ÌÚç ‹sꓵ-Û›s â)Ĩ®]Eíê:P:€ç–CXNœÃ OJ !ù û,‡  !!!"""###$$$%%%&&&'''((()))***+++,,,---...///000111222333444555666777888999:::;;;<<<===>>>???@@@AAABBBCCCDDDEEEFFFGGGHHHIIIJJJKKKLLLMMMNNNOOOPPPQQQRRRSSSTTTUUUVVVWWWXXXYYYZZZ[[[\\\]]]^^^___```aaabbbcccdddeeefffggghhhiiijjjkkklllmmmnnnooopppyws˜Žz¼¨‚Ò´Û¾ˆäÀ„äÆéÍíÓ•õâ™öä™öåšõãžõá¡óߪôà°õä¶÷ç»÷èÁøëÈùîÏúðÔúñÖûóÛûõâû÷çûùêûùîûøñûøóüðóüÚõü¬÷ýhúþ"ýþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþÿÿþþþþþþÿÿ”÷ H° Aƒ€\(Q @ŽÔã(Q‰ räH!Œó8"DÈûô”¼ƒÑ @ ÿ8òsçG%æAäˆOM>ˆ \H¢ >ylòYÑB‘†øH*h‘ƒ-:êS)E€`ôã(êÔ³7ïÌcˆ£ÛEŽà©v !>>???@@@AAABBBCCCDDDEEEFFFGGGHHHIIIJJJKKKLLLMMMNNNOOOPPPQQQRRRSSSTTTUUUVVVWWWXXXYYYZZZ[[[\\\]]]^^^___```aaabbbcccdddeeefffggghhhiiijjjkkklllmmmnnnooopppqqqzxu‰}´¥‰Ò¹àÅ—èÌ™éÏžìÔ¡ïØ£ñÞ¨ôãªöå«ùé¬ùê®øé°öç½öèÃ÷éÇøíÎúðÓúòÙûôßû÷äûøçûùëûúïûúóûùõûúôüéôüÀöýwúþ&ýþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþÿÿþþþþþþÿÿ”÷ H° Aƒ\(P¡@ îq¤hOÄ‚…9rDHÑÅ r”çã¾@#óäq(èc G~ðàñãÈbÄ=ˆ`âÉÓQ¡’ rT¨Ê>½\¨Ç"?~õñÓ§¢;i5ž©= 28ñÕ³S¡’,¸ÇÐÆ·pß*‘Ê;xóÚ±S§oC&/!ù û,‡  !!!"""###$$$%%%&&&'''((()))***+++,,,---...///000111222333444555666777888999:::;;;<<<===>>>???@@@AAABBBCCCDDDEEEFFFGGGHHHIIIJJJKKKLLLMMMNNNOOOPPPQQQRRRSSSTTTUUUVVVWWWXXXYYYZZZ[[[\\\]]]^^^___```aaabbbcccdddeeefffggghhhiiijjjkkklllmmmnnnooopppqqqrrrsss|zw‘ƒ½²šØÆ¤äЪéÖ°ìØ²ïÛ³ðÞ¶óâ¸õæ»÷ê½ùì½úî¾ûïÁúïÆùîÎùïÖúñÞúóáûöæû÷éûøíûúñûúóüûõü÷õüèöüÁ÷ývúþ%ýþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþÿÿþþþþþþÿÿ•÷ H° Aƒ… \8ðO!† ý4RäbAE…1zhq`ŸF…2òé¸!F{ô(2¤Ð¢ÄAzôjÔÇ¥¢FòÈTt¨æÂñ‘ˆ!†|! 4hП? 1Êse£¦b‚ªˆÐ!ƒ—Ú:¶l#=ýÒØ¨m[FŒÜB;°Ñž»{òìÁ×ᅩHZ !ù û,‡  !!!"""###$$$%%%&&&'''((()))***+++,,,---...///000111222333444555666777888999:::;;;<<<===>>>???@@@AAABBBCCCDDDEEEFFFGGGHHHIIIJJJKKKLLLMMMNNNOOOPPPQQQRRRSSSTTTUUUVVVWWWXXXYYYZZZ[[[\\\]]]^^^___```aaabbbcccdddeeefffggghhhiiijjjkkklllmmmnnnooopppqqqrrrssstttuuuvvvwwwxxx€}¸° ØË´è׺îÝÀòáÁóäÄõçÇöêÉøîËúñÌûñÌúñÍùïÒøïØùðÚúóàûôåûöèû÷êûøìûùîûúïûûñûüóüûõüúõüùøüð÷üÎ÷ý˜ùýHüþ þþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþÿÿþþþþþþÿÿ‰÷ H° AƒŠ&\80!† MšdbAJ‰(MJd‘` JŠEúÓq_ÈIþLRÔ¨£!J„RŠT¢¡F“bþ!Ô¨QÍ…‰")ÒIhæOƒ%d¨h¢I~6zêhÑI‡(!¥¤t©×™$ †¤D)Y²‘ÒNy¨ Ÿ·pûÈåC—nÉŽ!ù û,‡  !!!"""###$$$%%%&&&'''((()))***+++,,,---...///000111222333444555666777888999:::;;;<<<===>>>???@@@AAABBBCCCDDDEEEFFFGGGHHHIIIJJJKKKLLLMMMNNNOOOPPPQQQRRRSSSTTTUUUVVVWWWXXXYYYZZZ[[[\\\]]]^^^___```aaabbbcccdddeeefffggghhhiiijjjkkklllmmmnnnooopppqqqrrrssstttuuuvvvwwwxxxyyy‰ˆ…£Ÿ˜Ä¾±ØÐÁéÞÌïåÒóêÖöìÙùðÙúòÛûóÜûôÝûõÝûõÞûõäûöæû÷éûøîûúóûúõûûõûüøüûùüûûüðúüÙûü«üý_ýþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþÿÿþþþþþþÿÿ~÷ H° Aƒ*$ˆÑB…ˆ"5rø°`ÄH+ $IÑ¢H‚4îk¤$ ’"#ÈÆŠˆEZÈP£‰#"2$¨¥!G º¹!C†5¤°äNC|þ :c#¤X³FbJP£H`Ê•èµ  žhYþYËV¤Æ€!ù û,‡  !!!"""###$$$%%%&&&'''((()))***+++,,,---...///000111222333444555666777888999:::;;;<<<===>>>???@@@AAABBBCCCDDDEEEFFFGGGHHHIIIJJJKKKLLLMMMNNNOOOPPPQQQRRRSSSTTTUUUVVVWWWXXXYYYZZZ[[[\\\]]]^^^___```aaabbbcccdddeeefffggghhhiiijjjkkklll~}|§¥¡ËÇÁâÝÖõíåúóêûõëûöìû÷ìûøìûùìûùíûùíûúðûúôûúõûúõûû÷ûüúûýûüýûüúûüñûüÍüý•ýýJýþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþþÿÿþþþþþþÿÿp÷ H° Áƒ ܳGáÁ= rXb †Ò¹‘NÆ}7btñ9wyTx1I:wDqI”)gyÇfÏž*i^¼ó’h΃)ý4j4ƒ‚"J*Uç>:'åhÝGNœ¯;ostinato-1.3.0/client/icons/arrow_down.png000066400000000000000000000005731451413623100206400ustar00rootroot00000000000000‰PNG  IHDRóÿagAMA¯È7ŠétEXtSoftwareAdobe ImageReadyqÉe< IDAT8Ë¥“?KQÄçÄi‰œµ¢‚ ÁH*Ñ.â÷ˆEºøAÅï!XØXØ vc!v¦¿{;»û,®Ó»„\¶}ÙÝ7IŒóÌbÕÃà¥cŒ0u˜®Žï“2ÝBÀÝÑ^êàh¥ gw`4¨+Ü@jˆÉ¥ s‡ä5"H h†PÇðhõ"H.E„ºBFЛâ ôœÝœ¶›&è 5E˜°Ä€îõɃäòÔZn¦ +ÒÕ;ýÍl«·þ8r9¿~Æc¨§$ ý¦h÷¯>©*ÓÁåîkš®í›>†£·áíh¯L—LjãöÅÆ³RïwŸ‡UšdÞ:ÿ<$·ÇhݸIEND®B`‚ostinato-1.3.0/client/icons/arrow_left.png000066400000000000000000000005311451413623100206150ustar00rootroot00000000000000‰PNG  IHDRóÿagAMA¯È7ŠétEXtSoftwareAdobe ImageReadyqÉe<ëIDAT8Ëcüÿÿ?%€‰B0È (=‘ÉÄ»I1€õ2¤À‹ ÍÅÙ$ôþüùÃððýC†_?3üú¿~1˜þŽ`ƒä.ô_edj>)Æ*®§À­Äðçßn90ýûï_ þÍðûÿýÑ¿V¯\‹ðÐÙ¿ÿ0þaøõ÷XÑ ¢ß`Å¿4ÔÜŸÿ@±_P½¹3î¼0›ˆ3'Ã×^¿ƒÕÙ ¯4ÿùý‡áþÂGŒŒÈ)1vUÈM B9 Ç i;N@ͺ@͉Ռ⅜㡻¼zZÓÉIEND®B`‚ostinato-1.3.0/client/icons/arrow_right.png000066400000000000000000000005351451413623100210040ustar00rootroot00000000000000‰PNG  IHDRóÿagAMA¯È7ŠétEXtSoftwareAdobe ImageReadyqÉe<ïIDAT8Ëcüÿÿ?%€‰B0Ä ˜å±Ûk’ÙüúñËæç÷_W‘ÅA±Pz"ÿüeøóûÃï_~ýü Ô¿~1ˆ–’døõë7ÃÍÛ·.j¿ ÒÃ3ÉJØ–áï? ÿþÀñï¿ø7Ãï@ü&ö‡áË—¯zZYj'¯M»e㤮¨è7Xñoý!÷ë÷o+Ã]röþ‡{±:èg0›‹ƒ‹ARRŠáõ«× ·oܽp{Î=x µ-€ìÚü¨YäX¸ÐõPóA ku1ba„ççÀ7Ýl¬ÐIEND®B`‚ostinato-1.3.0/client/icons/arrow_up.png000066400000000000000000000005641451413623100203150ustar00rootroot00000000000000‰PNG  IHDRóÿagAMA¯È7ŠétEXtSoftwareAdobe ImageReadyqÉe<IDAT8Ë¥“?/QÅϲ™„hg5²Ñj%ì'‘ˆF,[ ¾’ŠLH–¨ 3•(„ˆ†v‡è¼wÿ=•fcoN{O~9çÞÜVM4Q7Ü¿Ù)v/ûWQ€½ëí»´=»œ&Þàbã¶ÊׯpS»O“ÎâüÌÌ å^Ê×'&^:\˨6Š…eND!& ˜šœ9ê’£Ó_|«?\«ÝÔ Ås·råxó,÷µ«º‘g°*,(Fï#d[çùO¾•aAAŽ*¯P p1…øO+C›$`˜)¼ãˆŽÀ*Íw`AÁž#Ê0˜$àÙÑ „*Š?ÿÂõëºb&NÛRIEND®B`‚ostinato-1.3.0/client/icons/bullet_error.png000066400000000000000000000007061451413623100211550ustar00rootroot00000000000000‰PNG  IHDRóÿagAMA¯È7ŠétEXtSoftwareAdobe ImageReadyqÉe<XIDAT8Ëcüÿÿ?%€‰B@±,¸$ˆýÿï_ñÿÿý”\–¼"É÷öÄðýûý»‘_ÎßH—‘ì ¦>YoM>IC†??'__`J´·6…h5åóŠ0üý´‰A\/EàïÏ_µ——x°4hÓŸŸ¿*Eµ¢…~œc8µh)Àw†??~ù±A€6¹q ëÅð ~cø÷ó>Ãÿ ¿œfPt(füóógó©I–¼8 ¸¸Ð hK‹€œÿo—€z¿3«3üûõŒƒãƒ¸n„&Ðu…8 Úž*¤âmÌÅû†áÿŸ· ŒÌ g×Ý)ÿ·DÕäj~gjÒÑšþüø ô»?ÐÈ L|¢@‘ÿ fIp{˜9•”\ËÅnmnj „bðóǹÙÁß èüHø?œfÑÿÿK »€qèg&L³¨âØ© IEND®B`‚ostinato-1.3.0/client/icons/bullet_green.png000066400000000000000000000004471451413623100211260ustar00rootroot00000000000000‰PNG  IHDRóÿagAMA¯È7ŠétEXtSoftwareAdobe ImageReadyqÉe<¹IDAT8Ëcøÿÿ?%˜aÔì8ì²’âûí–Wí7[\µ]kÖ`³ÌDŠf_›þÿУƒÿwßßõê™Iÿ-æ6m€ý6Ë«ø?õÒÔÿg:þ/¹²è¿ù½«D`·ÁüêÖ;[þwŸéúßzªùÿÜ ³þ›tio€õr“†©§'þ_|eáÿÙfþŸp¨ç¿~™*ñ^°œg(e1]¿Á´OçªQ«æUÝ"å­D9©Ñ”H#"⫚¦üIEND®B`‚ostinato-1.3.0/client/icons/bullet_orange.png000066400000000000000000000004331451413623100212740ustar00rootroot00000000000000‰PNG  IHDRóÿagAMA¯È7ŠétEXtSoftwareAdobe ImageReadyqÉe<­IDAT8ËíÒ1 qÆñ_ɦ ºîÕx¼e:%2\éŸä(¢ãeÈÆtW'9ƒ[ÿ%ÝbRF™¾^ºK6ó~ꩯòÍä|\ÓÀ5Ó’fRÐ89E/c¤áb.º— &¾Ï*²0ð0¡òxÍ,ìQ·Ôã!w0Ta PÙ|FÝnò^¯1Bư‹yó<êÑ€iðÜér §ø…#„»‡[Xj[MàÈU·,ñOÀñ|Þ‹ëds)IEND®B`‚ostinato-1.3.0/client/icons/bullet_white.png000066400000000000000000000003111451413623100211340ustar00rootroot00000000000000‰PNG  IHDRµú7êgAMA¯È7ŠétEXtSoftwareAdobe ImageReadyqÉe<[IDAT(ÏÍÐ1€ DQÎÁi÷’[€H ’å÷kòÛ—L2÷Ÿ[ _¤è­Y.oéL&Il “NcÕ·N•A°A–Æà¥rØÉG zê.›_öI»;þ“k9ÈIEND®B`‚ostinato-1.3.0/client/icons/bullet_yellow.png000066400000000000000000000004371451413623100213400ustar00rootroot00000000000000‰PNG  IHDRóÿagAMA¯È7ŠétEXtSoftwareAdobe ImageReadyqÉe<±IDAT8ËíÒ= Aqðó9îçòd1˜ŒÍd°(Yå¥n]q£än¥<‰òΤ«®pÓñ ”VÃYuN$ñKð>,z¨ ]¨oCvUX&€¼VÂÈïðytL„Ó2äkàуF'—÷m†á<Åpßä°ýèãØæm™æU㼬ëìæ €C â…áÁæyÓàb ¬$ *Ì*°FEˆ—‡¶2ÐR’™Œø¢QÞ(ð_θ7iIEND®B`‚ostinato-1.3.0/client/icons/control_play.png000066400000000000000000000011201451413623100211510ustar00rootroot00000000000000‰PNG  IHDRóÿagAMA¯È7ŠétEXtSoftwareAdobe ImageReadyqÉe<âIDAT8Ë¥“˪â@†ÏCåò8«<ˆ‚"èÆ¢ ®‘ ¢*ÞÚËÂ…÷kÔxAQ±¦ÿæ$"s8Ã0 EBº¿¿ªþê|ÑÇÿÄúý¾Ôëõ´n·«w:ƒ1f´Z-½Ùljõz]úV€Ã2‡Ùz½¦óùLÇCÄét¢ÅbAµZU*ùKOؼ^¯„…çáp Ó4ér¹ˆo-—Ëf©T’ß>Ëf¼Ûíh»ÝR>Ÿ'Ã0h³Ùª²Dt]gÙlV²x¿šuÀ‚:?)—Ë x¹\ŠÀ ”J¥4[€›¥C¥´²:N<£€áü@‚D"¡ÛívÛ€Yûý^ÀȸZ­8ìárý¢jµJ³ÙLÝn7ŠF£†-ÀÇ$P¾ã `Uu‘¢ü{ÓéÔ®"‰¼†ÇцÕ+ªªúO&1™ápHápøÕŸ¯†Íçó)Àù|.ÊUEÀØÇ4è~¿S±X¤`0ø2‘ÏU* `!°"+àÃŒ‡ôv‘2™ŒœN§M¸ƒÇãÑ®— ™!ÂAÓçóÉ_^åd2)ÇãqÆ[-ÀôŒ¹c”dGþögŠÅbR(Ò€î÷û ¯×kpHw»Ý—þú7þküFF?ÔE·E IEND®B`‚ostinato-1.3.0/client/icons/control_stop.png000066400000000000000000000006231451413623100212000ustar00rootroot00000000000000‰PNG  IHDRµú7êgAMA¯È7ŠétEXtSoftwareAdobe ImageReadyqÉe<%IDAT(Ï}‘MKAÆýPóæ”}…Iï! ÅB]ºuèÐ)ˆ˜C(AHeãËÁÚ–µŽ:Ö’”ôô쬔lÏeØßïÿ²39äþÏ÷¡#Úª¥›Ö؆¾U×"#tdÛDˆ±`^1DÝ\ÉØÍÌ1ƒÃO1.]M.67 ž` ËŒyE›sá…–Š2ø‰º8Q^hê˜m¸ÎäQ$r“1Ž´îìSâˆ0ï…G>Y‘4xx>PE.YfÊ­`È‚ˆåEÉS7âÇßDZ:Dà*råªå¡©sDÌÙ]hTLIfë@ì¨m½e˶¤7UYüzÍ¿ò(Þyv-|úHIEND®B`‚ostinato-1.3.0/client/icons/copy.png000066400000000000000000000012271451413623100174260ustar00rootroot00000000000000‰PNG  IHDRóÿagAMA¯È7ŠétEXtSoftwareAdobe ImageReadyqÉe<)IDAT8ËuÓ=hTA†á÷ÌÌÝl²nv‚’B0I%VA‹Q‹šJA0¥(Z‰  ±°—`!¬´N'Q ¨eˆi’ˆ$¸Æ¸ÿwî‹»‚’ÝÓL3çcÎ332vïÃËÞ|tÌŠŠ& ˆ* ¯•jóy³Tz6?5^§CÉåG‹Ÿ^ÝÎm–ƒ ¨'gaza›¥Å7ß×7nÏ=½Pm`ŒHO©ªveKùú#°¼™ðy=fO—cd¨ÈèHÿ¹ÜþÂÝN'0 b²2VÈX!›1ln×Ú ' ô÷õ\íàD ²  j…¬3Ì.ÿ>ÚðïÇqkúKE‚ÊNݯ¯­Õ_4K¥'óSã5§€ XcZ*JJ#޹~ö0[å@jÓ×Ó²˜^Ø~°´¸1‡7›°8ÜÉ€Qe6ÿLžI¬Ìèt‚ìæ®·c‰q!zñ |v ü¶j„/Xi ¶ž@øÞ Ì%1|hŸû±l !ˆÁô|­‹®±ø! ïY#‚uºUáN’w]Á˼ H3è„àu„ t]E´³>k%¾I“f¡’o«ÇR…‡D:“0åÚ`ä~¢ |§ øÓñ(rॠáon„3oG0!˜$‹‚¡ÎV„ë ž*[W0_ª‚¿©ýâ-+‚‰ãµÖ d§ÁWÇ&2¾ZfMFô‰ÒVJpËiF&B°³ >­ ÞRɘ•gƒ- Ð~ CâmèÍÚ´ÒÄ×ERÁ ឫРp«5Þ°y•ø¨È+‹Á21ø¶ŒK—aw·h£`Õ ä#Šüôa×Zñ½ž†‡Tâ³ZoüåL¨óÑ“•ÊÇ`"é(?•ï'žÜËŽJváKµÞ†óñ|ª:†G9[—aöw8é2 Jw Äéf'±“y¿ëmæzsÓ˜žìTswæá_·ñ_óÒιIrþIEND®B`‚ostinato-1.3.0/client/icons/devicegroup_add.png000066400000000000000000000013311451413623100215740ustar00rootroot00000000000000‰PNG  IHDRóÿagAMA¯È7ŠétEXtSoftwareAdobe ImageReadyqÉe<kIDAT8ËuSKkQž•?¡ ·‚¸7®K.\ˆÅ*(¸wÁ…¦¥Õˆ˜4¯&4%Á<ˆ1‚Iò*Yäe“&) ̓“&¥NJ£IúyÏ¥-:©>æÎ™s¾ï»çÜR„Ãá7n·»n2™Ž´§ØY¹#•••1«Õú’E1‹ÅráL‚cÅ2S2Àår!ŸÏc8b0 ›Íò}£Ê=qÄ {ïoµJ¥‡Ãõõu»ÝÎcG=Nàt: Èår\‘‹Åàñx°¼¼ÌA{Š‘›~¿L&ªY\\´L©¿¹¹‰d2‰T*ÅI]ºN% „B!ètº¾Àì‰Ýn;;;ØÞÞF©T;¶¶¶NÑ>‰ P(Àçóq¤ÓihµZQ°Ùlœ`{{{¨V«Ün<?í©R©0 r[F#R9(ŠØÝÝE«ÕB½^çªt´h4Ê É29#'D@jµZØl Óé Ñhðâr¹Œb±ÈGIÉT¨u(ð\uÞÝÄ=ù8æL/¡T*{‚Ùlö{­VãÇ ê9 úöñÓ,^Ùïãˆßx¡ð=ÁmÅÜ™ž¨ lLçØå˜ZZZ*Rw›Í&Úí6o&}Ùì\Ù÷på?ð‰Ì¯NA±úןž?<½’F£qÌ`0¼f³ý¹¶¶ÆIˆŒ89}_7ôÿŒõsFI£?‡^¯¿Ìækf#:¢»á÷ûqãÙEÌy@î•ñbù7Ù¨)X‡Ç*•*:ùâšõÖü%¼õ>äÊô¤wF0ÿ_)Xò C‡l?g(þUÚuO¢IEND®B`‚ostinato-1.3.0/client/icons/devicegroup_delete.png000066400000000000000000000013511451413623100223100ustar00rootroot00000000000000‰PNG  IHDRóÿagAMA¯È7ŠétEXtSoftwareAdobe ImageReadyqÉe<{IDAT8ËuSMh’aÃÙ=ˆ1¢#:»t‰jÝF‡‚Û ÃNÁZuéÔEß_¸¡µ¹ 2Œ™StsH¨~0S'JNîufY¯úëù?´QÎ^øñ>üßç÷ñüÿÏ+ú±¾¾>»²²Rš››ëhMµA{O–——‡~¡ï¡šÕj=7Pàcž¹uàp8L&ÑívÑét°½½ÍkôöÐÞ£D\€©·ÿvÛÝÝÅÒÒb±‡Ífãµ¾Dm.`·ÛM H$Ü‘ át:1??ÏAkªQI’ÇAƒÁ`˜“”N§‰D°µµÅɽÿ9J‡á÷ûáõz¡Óé$Å›Í&*• ²Ù,r¹Øù°³³sœˆÖH¥Rp»ÝÑhZ­V¹@£Ñ@½^G¡PàqC¡ÐqÈ•šHDŸÏÇAi5H ä¢(bÕj¥R‰»ÒÑ'RdJFIH€ Ôjµ(°ÙþlµZ8<L&ÃGI›9ñí4|.b톟îŽÀÿz …¢-X,7©‹E~ ¡^PàÎŒ›¾†Î7è%WÑúð¡©+x?5þE`c:Í.ǤÙlÎPw÷ööP«Õx3I€â»îÇwFÆ»q`æ 0;ŠÚ«ëpÝÊ_I“É4l4_²Ùlnnr£®Ý”£±ÿ3Öƒ#ì8C½ÿ‚^¯¿Ìæka#êÑÝðx<øxç,¾ŒÔ~" ÎPx$ƒkLVýaÖáÛ*•Ê«T*®É«ÖÏ—¤¯3£¨>“#;q žñ¡Îê˜ìùú¸á)sÌSlö.™ê¿ç›Aáé¶IEND®B`‚ostinato-1.3.0/client/icons/devicegroup_edit.png000066400000000000000000000014071451413623100217750ustar00rootroot00000000000000‰PNG  IHDRóÿagAMA¯È7ŠétEXtSoftwareAdobe ImageReadyqÉe<™IDAT8Ë…SßkRQ¾O=ôÖËÖKôDýDc½4ˆFía,´ —V+ØCX[!T:lÑÕ¦Ó¡s–™E?v›¶%N&S§S§á66ówjW÷uÎa0G>8|÷|?Î9÷p¸z8Î6›-­Õj·)èœrÖþCLLL4Æ_¨”3 Í vã$­F«ÕŠP(„Z­†jµŠ@ À8ú®¡kÿ4bĽüwZ"‘€Ùl†Ïçcg\]£23°X,ƒA–Hár¹`·Û¡Óéèœr´$Iðûý žç I’Âá0æææàõzY"M¯” ~çÖ_DÔ*Ã<¯‡oV9R/W,±¶¶†X,†ååeý!‰ì6¢sï×GȽÈùßÅøé7ã›üü6g2™˜A>ŸG6›E2™du=Ïî¦Ø\èG%õ™©ÇØ XPMLç¹Æär9lll`}}ét𥲭M©]¸‡ÚÖ4¶b×±9ÛƒE]'wá•J^àÈÝVDQD¡P@&“aâx<Žh4аG‹làj%Ä¥”VÚQõaée+f…7*sz½þ³ H¥RlÔ„ž…wrOïŽø JÉKÈ/ÜEôy fì£ ÁP*•Ž\Ó>òstŒŒDVWWq!ôñ©íD)Þ‰òÊeäH“ˆê¬c<&z ÅþÝ_R£Ñ4©Õj?Ð-ß÷Á÷á Òî6ˆñvlÎ߯¢ªjå€H„r"<¸ç[0 ž „_\€Ð׌RY„îYÿ'">þßÇt¿ë¨Tq)Qq?…[v Æ«‡ª£ƒÝ7½Ä†m'”v‘&Z¤/òÓ[ï±Ã{‰)~¶ çµÛã“IEND®B`‚ostinato-1.3.0/client/icons/donate.png000066400000000000000000000013341451413623100177250ustar00rootroot00000000000000‰PNG  IHDRóÿagAMA¯È7ŠétEXtSoftwareAdobe ImageReadyqÉe<nIDAT8ËËk\u†ŸsË™©IL2i¬-^Ò hK ‚PêÚ.ܸôOèÆµî²rå?PpÓU ‚º” ‚Ù¨± D%Ú€½N;™IfÎ93çü¾‹k“|áÛ½<¼Ï¹;ÇÓÜ{ý¢›ïÎ7KܨÝ|cý…ë¿þWÙÝOÜxûÕQxrÛ½Úq/¶Ü6=toûð‡³ï¦L‰«Í¦ oÃä1È€8ŸÃÄ.ïN˜‚Õ‰Â!øïC£rR÷?8:ü™Ðý« ¬:"ë\Ãþç#ÎϑͿ‰×O±ì憊Ôۯͻù®›q#qõ:òý†ôwðIV#Úç¯NW0µOÒ•Îe/½>-óPþEõÇ:íåwðÖS’|.h`À?NWÞ‚A Òö ¡< ‰wññ-GdÑ ¦(¨VâR€7Xýš1"ÆÌ‹ñÖ34ï“Ì. Óžhbh¹‡þ‚5]¼~@2mœºÿ;^•DÍ/qz)æè«eW¥V±]SÖS N”uHÚ«ÄÙ"žup ÁÉ/a­.Mï9íë_§³ÀÃ|rt÷­ÞwŸ~«Q#Õ2¼‡ %½‚4N({4ÏÐF‰óe?"š<¡µp‰¦ñ ±6ƽm²¹Ë¤s—É–®QÜ'É—ÐSgi½ñ.–¿LõàîAäÑþMí©Ûªûw?ˆO_!ɸ èïQö¾-ürÁÅ×ff¢ngî§W  N)Stÿ&ԾݿÓY5ᦩÝPeÍ„Sß4åÖÕûûŸ/®ŠøM ~C…5ßQcÓ„[ÿòs®ZÅžÎÚIEND®B`‚ostinato-1.3.0/client/icons/error.svg000066400000000000000000000036201451413623100176170ustar00rootroot00000000000000 image/svg+xml ostinato-1.3.0/client/icons/exit.png000066400000000000000000000012601451413623100174220ustar00rootroot00000000000000‰PNG  IHDRóÿagAMA¯È7ŠétEXtSoftwareAdobe ImageReadyqÉe<BIDAT8Ë•“»kÔA…¿ùíf³I ˜à›D¢†h AP+-´Ó?ÁN±A›”‚–‘ˆˆµXY±ÄW4>Œ…(Ñ ‘dgîc,V³YltàrªóÍÜÙsæÏÔõÓ—Üí¬™.–ÿÇøúÆ™wî6XY¿‰™Ù)z‹Š®ÆÆÆ²»ógÌlEOŒd œÊºM¬Ù6ˆ›“g^ Y˜}}}„Z4ç—ôì;âÏyL"9;"J±ÉLÖï­˜ÿŒ™áfÔkŸ‘¥\ÙíïÜ_¾ËBª:›fÕ„›`špM@ »“R½ ˜î}Ê–›é±µÜú8©¡b˜Wû÷UqI˜&È €ÊoÀ‘+‡žô0:4Šº²kp'⊘pûöä÷Í&‘¬u6®ÿÊû¬¤)¾|ðæÀ–ÑÑ¡jõK²„˜\è*¯AÕI˜*–"Ý]?è];O[i™E©SÜ;÷àØ›o§={LJ´—ªTËTKDK¨"u\#&‘Ú÷³³ëˆ©­âäŇ#ÏïÏËõeT•f0uL 7'¥ˆ«à1æcÜIÍ·ÝÉLxK‘KœÛ{¡¥ úêZ#L#!”qwTS³ªÊñáSdË-!$6ÌqIä ¼P'vŸjé@‘Æ =ý¸ — ÁL ãããyzzš¹¹¹–þ¯Ö£ÛgéêÚE¥ÚI[¥ÅŸ5>½yò-üËwž8¹uƒ™î0“a3r·¾¢\9j~ÜM£÷w{ͽIEND®B`‚ostinato-1.3.0/client/icons/find.png000066400000000000000000000012231451413623100173700ustar00rootroot00000000000000‰PNG  IHDRóÿagAMA¯È7ŠétEXtSoftwareAdobe ImageReadyqÉe<%IDAT8Ëcøÿÿ?%˜ê4÷Ïy´dÞÿ­çþñ'/Xf‡Æ¦Þ"Ê€Šæ¾GëviQTó¿oöŠÿ@ƒŸà5 )»ä×ô%ÿk;§þo›4ÿ¿‰¥Ø7ŸÿY%uÿ ªZÁbVö®Ë”Ty…E™À,X³“£¨¶Ý£©o6Ø©…Õmÿ=üÃþ»xüÏ)kø›–÷?"§éíÄeÿKZ¦€å¸¹yªØØÙÁL˜»šeÊÂõ­Ûõ?·¼ Œ«'®ú?eù®ÿ}K¶ÿjœ¾ámïüÿѹÍÿÓ Á.dfayÀÀÀÀ 6è\VÍÓoüŸ]Zÿäç”ÜòÿÑÉ9ÿÕ´ôÀ6‚\K/êbøĬ`Ò *YýBc~ZØ:ý÷ Œøolaû_[ßø¿¬‚òS1qI¹­bRÿþþ/*.ùŸ—ÿ?##ã; Lð@ô Š: ¸]‡Ïü_µåÀÿîË@¶$§ª©³¢²eÂÿµ;å@1ÂÇ/x%Ü|‚ÿäW4ƒ ºè|Fá=có« 9 ÍWA12 5¿âzaõN®?(8ºûþ‡S¤¡­ÿSQEQBZö(à`€Â‚••í?ŠÖŽnàx% P`ê™ýúŸ_IUã ÈP,Áä€a€j(˜X‚èçÿ@›ÿKË*ü¢п·9¹¸ÿã} cÚe&R1ž2hZºFT¹4úZc ·‰S¤HỳÅ̰ppð )å’s†yž!¥1F!”‰Œ±óæ{ Z¡”RhÛº®Œ1LÓô¨ŽµöjŒñã8Ö†a¥TôÞß7ÏGš²„çÜ“rÒZǵèÆ9×Õ±*«ï{³J-+úmøxe‡H 4îûcIEND®B`‚ostinato-1.3.0/client/icons/frag_exclusive.png000066400000000000000000000004751451413623100214660ustar00rootroot00000000000000‰PNG  IHDRóÿasRGB®ÎégAMA± üa pHYsÃÃÇo¨dÒIDAT8Oí= „0…³ þ`¥`%b-v‚¥•`ikáÙ¼…`e¹#xµ±ñg_dp+Éö» Û$/3Ãþ°å[\×uEIö}O㈶mcÝ ·Rß÷‹ 2Ó4c]×ma0Ïó8 à ÔtíÏóª²,;’yžwªªVOÒ·àÇHÓ4›äƲ1F$5Xוá"©¢&ξ1àÓ4$/–e5.]¢eYE†rŒ™EÛçã¾ï_œóZj`†ƒ.ì"û 2G4ˆöT? co1.dh4[ÛHIEND®B`‚ostinato-1.3.0/client/icons/frag_link_down.png000066400000000000000000000004451451413623100214400ustar00rootroot00000000000000‰PNG  IHDRóÿasRGB®ÎégAMA± üa pHYsÃÃÇo¨dºIDAT8Oí; ÂPǧ±„Äe®#[p V¸ ·`ea!(V6V‚ˆ¶Z©äc‚’g^H±°ÊÀ­æœS\Jþ„l¢K ;ª±Ò» ìè¹.F•ã‘-£Å\¾gSéX–< låD–(à`&ýñfM´ZRo·ð¦r_ „ ÄŽKìzÈç‹äàåÕï`nw`t‘F×ñ9£M”«d‰ŽéÃÄ ¤júiE•ï4ÆC®—½ÛôÎiˆô(Ä$:.MK4O‘IEND®B`‚ostinato-1.3.0/client/icons/gaps.png000066400000000000000000000066131451413623100174120ustar00rootroot00000000000000‰PNG  IHDR,zë¡ sBIT|dˆ pHYs``ÓŸŸtEXtSoftwarewww.inkscape.org›î< IDATxœíœyxMçÚ‡ïµ÷NöÎÎ1Ï[G |%†olålÇqNQ¶òg+^ò¹xN!˜#Ÿ·ŽêdÜ ;­7Ëÿëæ‹gX2[+‘øÿBJXõ)aIHHÔ¤„%!!Qo–„„D½AJXõEm:ë z´:-.N.˜L&nk hàãoÕ×l6s[S€ŸwÛ(­Ä ×¡×•#JGärEe<å¥Å¨\Á²mg¯Ä ×YùQ©,ûVñ­-ÃÑÅý™ušÍð Ô„‹£½AD«7#Õ2ä5œ& 4F<Ýä(äÂ3Ç~”ò²bÌ&r¹¥ƒ#‚Pá_¯+Çl2¡R;`ÐkÑë´5Η ¨\­| zvö6Õ[WÊK`6›Q(ìP:8Zì:mJ•Ú²m4èk§L.ÇAí\ÅfÐk™eœ¢ÙŒ^W^%Fm0™L””–áìäˆÞ`@§Ó`gg‡ÚAeÕ_§×SRRŠ§Ç³¯ÉGù£u!šÍ–ñéuåô:ì•*ô:m[¥Å÷±³Wb¯t°©f¨åVÚÎ 4ïÀ?2vá×Á•ð„u mçK¿5›Wâ×Á•8BâüY±i©ÍïÞº’ÄnscH;fh‡æv>N"±›;÷5·0™Œ,Ÿþ2“_§ ÿ ‰ÝÜ­~§í©âW4›ù`J¾=È&:Ï^Òâ{‹9:æ­½{ì\;þŠ¢U6/¾~»…&2”Ò î"~]/âñ†ϼŽÙ†/ ÍûK»¹3âEg^ë àÓ¥ؼr‹§ôàVÞe’‚ùë;C9œ±Ãj®ÆÆW=!]=œ©ƒ#y½« ˦ Àd4ØNp™40œÄnî ïâÄŽö|µq!kæ#õÝD B÷˜^ H[6‰ïw¤ZsÆh+¿Ÿ¼?Ž ‹ÇY¶OgýÀ–ÕoÕYç™ì_iÙžk¹ù¤,YIÃÈö4ŒlOxkÂÚvåèÉ3è FN˜†oD‚£;ÑîúràPVãVgÎÈX˺ÒÁŽÏW¼ @ÚòÉ,™Ú€9HJbí¼áü¸7Ýj¾Æ÷ ´ò{#çI}9ôýßl¦õQê\.ùä=† IáI-“FNeë?¶`6›ùúû¼½d*Kß^É­¬û¼“4Ÿ¾XÍõ‚|›‰öò $-³•;²¹{+—ýÿüÌÒ&šÍ¤¾›È…Ó‡˜›šA@“¤eòé> IóÓHË,$ºÓŸ,ûi˜=²#Ïüd3Õ‰ŠPqï§Nî áD¶–íß?àj¾žÞãsy¥— ·„³g} Y¥ìù±Ä¦±û §û4Œ™ý1{·¯åVÞeK›æv> “zÚ’éË¿¦s¯!¤e2vΙŒ´ÌB>Þ{£Š¿=ÛÖÐ4º3ë÷\'ûÄŽì·Ù»¡ÏÄÐäÅlú÷]Ia熔߷´]ÿí<ïMŒ§M§Þ$¥¤?8™´ÌB^KZ„»WÒ2 Yš~ÊÊgî¥3”Ý£ðNÅ\>w„°l¦¹M«ܽ|м³Y°qK:SßY@Ö±|³e#yg³hÝ*’ek?F´á{Þ}‡OçÓ}F¿½–Ý[WqçÆ5KÛÝ[¹,LêIH³¦.ÙÉ‹½‡‘–YȳÖ!WØ‘–YȺݹUüøq7óßèŠ^Wn3Õ©UIø(½ãú²dý"®æ]áãóUê·Èd2öø–VM£9è/Œ<–уÇÚL0€(Šèuå”—T9þâMgìàꦅ Gg7ÄÊË¥ƒ#ŽÎnUüéuåôñW²rõüq›j}ˆÙ,R®)-7#`0Šd.Åd‚ÉÞ¸9ËñõTps¸Íc›ŒF´e%+K £¡¢ ))ºÇ¢¤ž”<(ä­å»°³¯(IÝ,%Tõ¹˜òׯ¢-+±*£ž&£mi1F£³Ù„(VüÍ5y,œÐ¥JMòÂ-ÈdrdöòßËA¨qœåeÅÈärâú%²gÛ†&/æJöQb{¶™æ; ›¶l£¸´”«×r˜2~{2ö3tPâ:wà“•Ø,æC̦ÊuQ¹Œ•ÇQñý»,Lꉶ¬˜iK¿ÂÎ^ T¬{•á1ó%Š" 6$eLœÍµ>¤ÎWX³ÆÍeÛš]¨”*ÞZ<™.¯¶£L[ÆmÍm|<}X•¶u¤€:R`õgÚL´¦ ±ñþÌÝ¿€0â'[ÚÎÉ (<Šo¿ø³ÙôTþ<}Ûã›é«‰³—t4ì~‘î£rˆr q€w M¨UNjY§ËZd#´Èfàä<›ÆÞ»}-I A|¹v&ýGÌ QHsò®ü‚£‹;FƒŽ}ýq­|jnç³p|ÚvíOô ñOì/B3Aö ‚àôˆMUikY­oº ýj%ض~ú5æ»ôUüyâœ\<¸xæ0~BÑäñÓÛžÚßÕìc„4!¦Kgd`Ðk¹‘sÿ ˆÚJ{,%¥e9qŠc'OcE®åäc0¹_ôo/ú S@3œšqî׋6‹½{ë*’‚ø[ꎚMƒÀ0r.ÁÅÍ ¶ŒïÚð/¿Ó%µ;á ‚0K„9Õl¯ ‚°©š­ƒ ûê”°L&£gG!W°í¯»Ø÷Å!.]»È/ÎÐ!:–ÿ;ºŸ%3$‰ßÜĵ†lü,x7âËŸ´¤géY¸éG\ܽ-m3>ü;ÉïnæJö1¾ýb…Mã> ÑMUhO4Cw²?lÂÅIFl”E%f+#&RÅÍýáôêäôdgµ¤ïðé¤géÙ|°˜?OZb±û„275ƒ~#f°mÝ\næ>ÝÁP\¤aÑ„—hÜ´5SÞOZ€ý€ñ›©ÒVT­opƒZ2|ÊrÒ6–YH¿¿ßg kѹë2èÞ4iË&Qt¯à©ü]>w„Ðí‘Éätê5„ïÒWãåh¹Am šñyê v~¶ž7ÇfÁòÕ(ärÚDµd÷ûؼn%ÿr£Íb>dÀÈ™¤ÿlàóƒ’ôžÅîÁÜuô6ôfSÅæ±á|åïQò€£Õl÷€ýuJXr¹/oÞ˜=’)‹&0vÎ(¼<¼iÖ‚äa“‰iFd|¦/žÌà‰(-+!¨ap]BÕŒ X.S«ãJPX+â_MfÛúyÜȹ`»¸Ï€LJûª ½[{GFôs¥OR.¯Ï¾Á˜”›üëp ¡¶}ò&—++ì¬ìîÞþ(Uj&ÎÂÕÓ—Ô”DKéüGlx׫Xckæ çøÁ>qQ¯‹¢˜"Š¢ö›¡Ò–[­ïjQ=ydU‘ËVO~¼!—+:±¢¬úäýqV}jâò¹£„VÞ¯êÞ4{·dÓûW9ù×™<+…±Sg±jÝ&:Æ´F–½;›Ó¿œ§m÷æ°‚äsñpwÃÕÅÅf±7_> ±W:ðòè98»y‘º`”Mï=Š(Šßˆ¢øU5Û!Q×W³]Eqü ))Oë\ü}Û¦qºÕ,šÜ9´ŽŒaÕœðr÷F©T1èO¯Ò¸QšÂ»´êÈ’™+éÜöÅZ ä’7EÞ~A„µìhewtv%2¦ ;{"¢^@&W W(hÜ´R?DÆt«rEVßFM¯ùÜC[Ë•Â}õÓèpTˈkçˆÚAF“{b"¬úôs&:BEQ‰‰ vÌOòáAµ{„}ÝÜóf‘nýn Mš·Ã·aˆU›`8ÁÑÈv„F¶*h<,¥Ü<|iÓÕj¿[ùWhÖº3®¾¸¸yÚ/߀šÂo‹ôç×Z æȾÉ8À¯¦6ˆˆê„§O#«¶ÀÐ4 iŽR¥&8¢5&£F›¡r¨¸šõò ¼U¬Õ~%EZÇÆ#vö*<¼ý‰lÛ Wß*ýš8jîÈÑçiÆ NŽjºÄ¶ÇA¥ÂÏÛ ''G<=ÜHèÕƒ9Ó&booŸ¯7/'Äãêẫâú÷~‰Õ‹Sð­,Ÿ†«ež&QVã|„F¶ÇÇ¿qU}€pÁáQ(v4iÞDoÿ`Ý7/?š·ùãã9"êÜ<ºÎëB>/óôHŸ—©5Òçež#Òçe$$$$ž#RÂ’¨7H KBB¢Þ %, ‰zƒ”°$$$ê RÂ’¨7üçÍ0#y‘IEND®B`‚ostinato-1.3.0/client/icons/help.png000066400000000000000000000014221451413623100174010ustar00rootroot00000000000000‰PNG  IHDRóÿagAMA¯È7ŠétEXtSoftwareAdobe ImageReadyqÉe<¤IDAT8Ë¥“ÝO’aÆý[àOh­æjÕjÕÖ8è §æ'ÐÔrµ –³t˜Ó,)53 j꜊o"ˆ&Ê— ðb"_ÊÇDxáU³íê5œËÙZ¿“{ÏõÛ®g÷ çøc #ÙZ÷@ãÜ'Uë4õÅž¢6Šœ±$S¦=ö™µ“æÌ:Ò!Oø;4ÜA*ƒg; G ‘Åhhh!Ì9U@¬¥8rk2µÙ‡ùÛ.¤s~ˆÆœ¨YG±ƒ# WˆÂ'•?ÕElqNdækr5ÜÓXpÆPË„: ü1¾Xc‹ÔH­P®†`÷%Ð:I›Ç]¬¬`t)*\ó§°â‰C8hÇã3ÒG ?~s„ÚA júM˜·G ]‹ Njf}s!ÒÅtí&6QÝcBU÷ üÑ4¦–PÛ¶A3µ-n»­SX½»xÒm$³‚…—v“J,¨üh¯ÓÀ<ÖcÚèÃ.u€]½³.”¶iQùA‡o<±žÎ ZÆ]´+ÄÓ ¸z”¿×¡ìIú;¶"øb-ŠZT¸ÏÀk`÷Ǒߤ>¼ÚHÛVb™ ¼vÊÄó(y;‡â×ê …¯6*PÐ(G½tFOwëäǘ>B…1½#‚ÊC6Kì#²G#O4“¡P$±êƒ„©s«fâøùâVù›ùà’3åŠÜ6 š”h7eøÎo˜À°Æ %€›ÕcÁëGY')O4˹×@¤töm¨­A¼”,¡¤YŽ¢Æi<ïÕCiôBeöãjÅçÔåCœSWùŽPƹýl2ÔG¬ã«; ‹7–aѹƒ.™ ¹V¾°¥tš®…Zš”†Î¢™Æ0ÕÄ4Ñ3É$ýÍÜï½÷ëBH|´=Ë/_>8çUå}Tóæ!su4-ÁWNV­´8 (¡á¦wáÎàÙO¾úo^uðÅ•‘Œµr±#¡Éž¶FÖ®`!r žpzy£ý¹óŸeß¾œÏØHnÚVßÝZÏÔœ¥¸გ¨[C*±ŽÂ³2??\¦†S¹ ‡³+€ÞK÷;EÜС­ñÔö–zrc%œö´5*ŽÍcb†]Éõü3_æ§»…i4|š»vôQ @‚ëëhÐÔŽdÃ…çØÈ"Õ@ïÇIz»SˆlÕ’Ÿ,Ѿ1A×Ö†”FÒ°ÞŸØ×ÞÌXq A‚ÇÇÃd‡'°b±Î²EŒÏ.r`oÆÚ+)ØÈ¶6Õ×P˜)á¼G¼ !Ðw°†G¦ÑCqn‘®ÔfÔG­«ª ŠS°®Jðž”üãyþ­8¼uxñ8q8+¨¼g~jn¹Bs¼£14(Á{^&¤xq‚Xñx²XƘ0µ °îÖ`~’¶Mqœrd;;?l¢n]-ýGö "8:Z ý è­Õ&V¢Û#Ê_§“©ž®Mü–Ÿäû_GùöÞ_¨8TŠòÑö-Œ—y”/L¸ýZ‘ºO—ÑråÆç_îŽwn¬çÁãYfŸ— .°¡±Žm[š/-qï‡_–1µ§rßdߪr÷±ëÔ^LïJ&ìKÓ¼~- <]¨0øû(ùÂŒ1±þÜÀ™ìn ç«+iU ' •4`Œ)7 rçsÿ?¦wÑ ¨?‚{Y¶!¹IEND®B`‚ostinato-1.3.0/client/icons/logo.icns000066400000000000000000003770741451413623100176040ustar00rootroot00000000000000icnsþaËsRGB®Îé9¦IDATxíÜfEõÇ/õWÄD›E±±° »;A1°°ìÄn°»[„U±» ØE,ìÄBxþ¿ïyî÷î¼wŸ÷Ýw{÷|>óÌܹ“çœ9sæÌÌ}ºîL“Édë¸m×´K䥌5Í¿9æÛjslô¸Í!Ú6‰›lµÕVgø.q—Løjq—Š»DܹãÎGšSã~wbÜO⾜¼¿Œ_З×%îtã¶ø› ­‹æ%|™¸Câ¾÷ï¸ÅÂß’ð³qŒÛÅ®&LùÿSÁ¾oò~3ˆú„¯÷®¸1ÑOOÜi½ûoüÖFâ[€Þwm‘0SÙBZÚ§ÍÖ!†Q™ð…㎊kÂBø9„=ãŒ3&º6q&­ÌѾ~ovY o‘"ccø!s}AÂ÷‹ûSœá¢CìÓO‡Ò¶Ú²þ“ç'5õm0n‹¿0"”Èö¸·Ç -±V":ÄýÏþ39õÔS'¿ùÍo&¿ûÝï&ÿøÇ?*Îôg0 e ŸH`Gº˜~6@××k›Å¼£‘#¢w 6>w¹84tÚ_b9ïÐÚó8…åË—w?úѺ/}éKÝ7¿ùÍî¯ýk÷«_ýªû¿ÿû¿nçwîÎyÎsv×¾öµ»«]íjÝe/{Ùî"¹ˆY»0M·õÖƒ´ŸäuAtV û¦ž“Sß6[V ÁÆúˆOñ÷Œûc€X.`„+ÂÁ_ýêW'zÔ£ Új¹'?ùÉ“o}ë[ƒiËí«²Îãó|þ¾M—ð¼9Š!³ ¶>ˆväï™æw¶¸ÿÆÉèÿÉO~Ò½ò•¯ì^þò—çu×÷¼çíÎþóÃ85šÃ5²‘Ûl³MwÚi§uÄm·ÝvÝÏ~ö³îßÿþwå;ðÀ»‡=ìaÝå.‡)ÆêÈãiqÛÅ}>nï”…¾o+˜m ¬K ±¥lÅ¿lÜ_ã€aNv¾!'oyË[†Ñ¾Ûn»M"Ò';í´Ó—vÍ3L.q‰KL.zÑ‹NvÙe—!Ýk_ûÚI˜¢*UÂÔà éóú›¸ÍZ)Ü$%@ºu?º°Ü}%î²qÃÈAjTÿþ÷¿ïžùÌgv/}éK» ^ð‚ÝÙÏ~ö.J^÷ç?ÿ9É»îÎw¾sw£ݨ +=#Ÿ¼8t„¥K—važJËz@Ænûí·/©€$xÆ3žÑë\çjõ˜D¼í™v~ &ˆ¿Åj8`r-Žªøoˆœ‡9ú¤“Nš\õªW­˨ßu×]‡ÑûªW½jrüñÇÏÔô§ÅMÿûßÿN~þóŸOÞþö·—H³K Dœ\úÒ—®òøÀÖ ‚$P}Š®æÕ6qÛÅa£9Ö ÿ£ÙAfÔ[% ‹yÅ~Fï@ì%K–LpÐ!ó÷dÙ²eÓ\ý/D#ߨ5Ĭ”,ŸóœçT9»ØÅÊ¿Ìe.Sþ _øÂ9eZtïßhLªÄÿÏm*q°FÏA\žøŒ¨ïÆ,ÿ†Ñ¡.ùËa˜·%þK^ò’ ú€ie–ŠXàG1É[ßúÖ9L $Èr²’4墧Ľ1î!qSͱé}⶘‘|, ²ý$ ñ§Áˆ‚Œd—x^¥-š¿I†)bˆXd !ì䨣Ž*& ŽwܱÂ÷¾÷½gM)2µÀlD·GÛÙÛW/¦D§Ý*‰}ôäç v¶Ï —-Ãçåoè;¯ö|×þ™ƒƒ¥íÓŸþt÷Ãþ° 1Ê€†èÞûÞ÷Öò/DL·!Xe!kIwÊ)§t¿ýío»ýë_q,ÿð‡?”ÃøC<ùÉw–³œ¥LÅç8Ç9ÊtÝë^·;î¸ãº¿üå/Uæ'?ùÉî÷¸Gw¶³­5Õ»¾ŒS °Ý. ±3‡žðŠø/I²¿$LcYòš.Dú†­uT[±]À¨Ù!¯NŠÛ).eeëþö·¿u·ºÕ­ºÏ~ö³ÝùÎw¾Zë_óš×ìŽ=öØ"FÒvÙੵý¯ýëî?øA÷å/¹Ë(î>ÿùÏóz½lSºvÝmo{ÛîZ׺VwÅ+^±Ú’騃a|OE2T3ãp?OøÐ¼?">%þô<#iþw o÷öÏ›çwÆ¥\)ÎÑÀƒ•Zë«ø½óïœddN“ç>÷¹“½öÚ«Ò®uÙô™\êR—šdÓ§ìæoÓÌ cI[±ŒÓ^éJWš<úÑž|ìc›œxâ‰sôƒ0ÂXç _íôplž¯’2 ®iÐç áo4 ΖŸN&|÷x/Œ»`ÜÑŸç²ïtÐA]–}#Q~÷»ß½Ãþÿo|ƒ$!Xwžóœ§ûç?ÿY#‹ÞOúS_þ npƒÚ ÄÂwÖ³žuˆ'€´azùþ÷¿_„iÁ}F4;‰ìO=¿üåp”°ÊyðƒÜÝúÖ·îöØc’D¦õn$ˆ„àˆÿ§äÝsHÔâ…ç3%ÐI:‡¸£â„at0z”² ´F^DmùA~ùðBºÐd‡v˜D\OÐÚyæ…ñ!yÈäøÀI’ŸIæýZÒa7 üÖaÿçüÒÅ=†l*Mý­X’%"í“M2-U;lõ²JyÍk^SõÙ¹«¡¿I³4îâÉ[LðFœ´a½ŠSÛ¿zÂ?ŽP½KëçAâF¿À.PÄÌh»= ­8Ò´„‡èŸúÔ§êH[e® œz¾öµ¯UÙš™·ÝvÛÉît§¡ Q2íˆd˜d²6—lþË^ö²I$ÅP­å÷í´À §Û'OAÂ*“Fm¾~Û™„ïçºyJäà/]ºt˜Û!xz¿’#^ñ>›7“ˆïÕ©cÊÃÍÇ Æç;ß™`ÿ(Ÿì·ß~UŸËNüLAåݶ‹$„÷ei„¾a^Þ½ímo›üýï¯6QŸuöðçg&}AÂ\/°îuæ·Hø°8A&˜ƒŒe±çg'n@è|Äñid¹G<â“,-·ü…Þ&”l Yž>S˹Ï}îŠßÿýç´éÄ>i[F4ŽífAéuó›ß|½e¨ž66ÀÃtî›L>ðö)§¦üÍÒ E>öýwÇttè¹HÀÿûß? ”}z´øt|Žc„iœaãféÒ¥scyUÓ"~dD5uapbDKø(‹Ÿ¥è$[ÍUb[RƒwäE7@/·‰@ŒÏá•A´e¥ðvJÀ¬|¡äÙ<™ WÙ;WÂÇň:¹| \ 6“G>ò‘… Ž2'²ZEÌåÙžð„šã«ÔüŒiô*}àÕ¯~uÕ9k$K¼Ø"ª<ê"Ÿy9t Qi«ŠaÛnÃ0LÂ3Ò©#XVÿì8VÄË'ýæÅi´#Ÿõ½2ÏN ˆ£³ßþö·‡y­žÑ"ÂZâ;*ßüæ7“µ`M ?ÎRÕ‹„AÁkëÆ–Às¬+ÕÙÖ42ß|LÌ{Šð'>ñ ›2/‰T/ø}µ¹¿UòmºFJüþ^0“øï{ßû„1W¦W3DqÎÇ;Þ1-1¿-ò‡Èµ|îsŸêG«§=,ïÜþîw§»Ôãz[iðãÿxPþrÊh(oÜ7V.±[Ô{±°Fe˘ŽËhÓe‚4®–.ñ9!Ãn0ßÎ0: R)­=F”Ïì†H€«‡µü±]ìøQÓ <IŽs‚«Ë9ùä“'W¾ò•«¬…˜€é‚:¨óq{\Ù¨ÃrúúdÎìš´L›Þê Œ ¿/X‰øÌ—O}êS«Óu>‘O?Y{» ‘/~ñ‹§%¯£_ç^Œ?Ùc¨vIÚ€&¤LïóØ—x¬ý³gPå-4P‡Ì}Ç;Þq‚>XN_¾Lð£<Ÿ+y`‚MËN)úŸ9jôЙ˜Z‡%óê,™¾á@¼£t2–A¬uë DôÑG]õbß§þ;Üá“øÃsÍUß6Y&’@Ë%CûÖúê®lØ·`‰ XN_®þpò$~tÆm? Qã¿]ßX4ýÒöEFÎí¥%öœÿx†Øø­s¾ÉMnRñ, Ucã¯jFê«^}6>ñ‰Oêýë_? §4û°:%Û6ìô 3|k^êA Žy˜@IðÔ”¹i¬ ‚çý]Æœ Ô:_Ä1_|ðÁ…Ä«'zÍ9º]ñœ~©%¿ë]ï*à qäQ4¿ûÝS“‚HžV¹æ¿ìÜþö·¯v<ûÙÏ ¢|û0D®FÀö±kI\fzÃgÚÑêH=”ºï¾ûNrv¡j³œ< K脯ŸüŸ ÒCñ?È¥Ó§ürn¶2×I|:ý¡}¨â "DmëÀeN€+FÏÙ«÷«C Ò‚ÌÖQDJšyæàªkM~lŸvE=¢ßþ°²ñT2R@FyèC:Lw–“6hA=>á³÷L°q¦‚4@ÑÏi` ¾\Ë:â3zµcbe~´Ónä8Ï#ê±» ®@ŽòfœÔ5ùàKè!bÀÃþðÉ›Þô¦JIJý dYÔ+ Ãæ²JµFúà4x—»Ü¥6ŒîsŸûT<øPÚ±ïXN_©x~uÏ~U†(ú/šðÔFÚ‹~‘§}Â3‡»Üãð—;@#žÑ@X+œ7z[kçìÉWZvüf-AÔY•IB½\ e)ù™Ï|f‚ácÔá‡>9á„zü®ðf•·âíâBâÝ…~²ÌTâIèœ^šüéOª÷¤gN…âÃrúZöIú ¿4L#Ôú5ËWŠü–ã!œÄE³žõ¬gUg]ÞérÈ!õžË"ìIݶ,m †´-‚X)`ÝËÙÁɃô Éõ®w½¹àkìJ˜g…ˆä\+nÙ–¶=Ä­Ø6¤ u‹¥"K@€ÝLÞÃè*½,••ˆMœ ¾šôɾa¦‚T$ñ÷¥ÑRúØÀ#<²:«Øä.IJü§Å% ,•j¿–#â°Â‘‡²D $KË8AŒnq×»ÞµÒ“GÇz :.'wÊ]å*WbZüßøÆ5-,°ŸßV½`Øþ°ºéMoZmb @ú9­}å+_©2`@êG_P'zÒ“ž4«|§‚‡&ý†WÓ¢/÷­*n”X9®U@á“X >§r+9¶|Úë(Péáž`9õÐüh@‚" ý@âpòÇu<å“B«x·‡é·-‡›ÏµéS’B#Šª—Ss2y0/– ÄÓòåË«ýn;3êÄ©ä’Èpà³M?_¦QJ2Ý(%Q‚jX 0 u"T¦9½4ýRà‰›EëVHÁÎý{' ”è±—ÚsÏ=«±.mP` ºÀ\KgÓn’ðœë[•IJLßú¾Ãç:8ù°¤ÉÚù6ó´å¬N˜ü|f÷Ýw¯ºe‚ë\ç:CûÛ´³ê±=<5‹?þñ¯iо -í aJâv²ýUÀŽXf‚êb&íº×R 05—õbÇNkñcô;¯kY3ÍÓŸþôêŒáòÆíݦ#ôk%ð=—4é#ŠÏ:¹e¨ùn¬Tè"#¨Û>°gŠ¡ 2Á=ïyÏA'°ã¢•^n‘]ÂyžÑ­»Ýín7GgQ UÅí ¡R`Åm™T²N Qôïœðt-·Bù¨9+' )ÌUÎýíú+×®p…JƒÆ«èà ˆàzýøŽeP:TÄ9“gL äá™Ï¿æ¹F–Å&z?;ì°™e¶ Á¨E“·ÍïyÏ{JBú CÛN LA7»ÙÍ*ŸïQgHNÂë¦L¤@ ZÂk)Håï¾}£J܈Ž:§‚š÷µèaçä|µ¸Xâ1¶óu_öO¢øPNæÁ¶®äq*°~NêrȰs ^ÃËBH|§…cŽ9¦J5m'’ûƒ¶ß«î_øÂôÐÔcûØzÇ4¦iØU‘øcu@~$¬+NQÖ— Ê`}//Ï댔§Â€óM=<ÿùϯÆ!ÖUT°¹WÂ^qk—oÎuîlU™füh"¯Ú3'€5<Èy–ÏÉa¸˜:fT;3J„+ÎUl±=h,2 º¢žö!ùdPvYe*…èFnÁ´Ùv³yFLuêX;GË[§Äß9“)°vÊ` pÓgI§Æi×TÚÐaç%×´,ë¼CbˆÀN¨Âå~´a/M(ºè€Ú© ÷OtÌÎ3R˜¬@ß:¼²e=líZ®iñ%†H¢ß¬H˜w=y¼®—‡Ö£ßÿþ÷/R/ûÄyŽœ ¤òG|ÉL—^La ‰¸ LWHRó{/²Ç+$õS 0Ó4<'2‰JägÔq÷¸Ï'ïáq»Åñ!ƒáã y—Ç|?=ià# ¹ÃwzÒùúÆïS•×'ÛyŽÆ_ßé!q‰7çó*‘ò7s`EÑèòc߯>)sâ3*êó,m]]ï¢dU»²¾®g>í²lÙ²ŠçŸSØjÀµ7z@>è{ï½÷0M-¦¼qù³ž)‡~¯å8à€a‰Æ;EÞó1JÛHY(ÈqÂòž©+D«0: '•°rbÙ%e{W‚= W]M¹ê¯’Ö©je&Hd)|ñ€=3ÏA8K¾üãuÄÊJxÞj·<ëÔ4ÿ¶K³ªqÆ9¤W0ÝsÓ¨é䌜sWhÙ´#§üH‘òïv·» †¢U•7³’&Òün±‡Ï*Çu>J !>P’a SºÏ>û ï4’™v–ï® KGíóè7êoK9iöt>á)ÿÆMâÛ1Ö£X¢‰d…¨*W<·JÏã40Œ‡4ÔÈg)s¶¤yŠ–”g¾ÅŒZÛNyFÔ%)÷ñlC›Þ6,Ö·=šªíg¦È*‚%jË,W=†N¿X"‚[ ÒŠxXü…œ}rÅ@[š¾ÀJò×§h¾‚úˆs$rYµ´_ã÷áòà,÷øYzH ʲÁ1øÂ66q6}‡îºvIÆô 45ª|æÔáèRTïDøœ 3Ú²ßð†7"]MH$mdoÓÏ(nÞ(ÛãrW‰Å3Àû{Ýë^U¿w ²m—:%€CûåþÇÑøüÍ}ï{ß Ói•À2 +íÔÙô&@±•¼ÃÀ'ÌÃSêÕ N2ÃU$Áa™2Œé׫T,E\“²méˆbã˜ïȧèÕX2âm8â1J^÷É]o÷‚^›Ö%¥Äwô`JÚ´ :z)xNAIÉ 8ˆXý€XqÉÌä™C_R2ìÚÿyÚ†ÞÄÞ '¬È¯ÁH¦!Îs•æ©ÌÉÞûxWOº)äÑÿ3b%úí û꤃`n>ðçJ¬³½èEõž5«óûÛæ™^‹‚Û•®síå™^ƒ#IIã¹}Óo1`z|‘GŸ…"qu¥K[¯åçŸÊœ;ŠÊúS&›`ì._¾|%e˜cläuÀ°bùúî}0•) „~Ç`žÁåTð¥”?…tÂËœE|+àzTR”Ó¼«É’ŽÃ‰ûE4‘†ŠÕtXÚ],Ò3…´ÌÀW?ëÖ‡ÜeÓàÁiÓÖÃ",—äy<ú¦¨¥mÈ´í‹,z`zÏôƒÛ,ñù^ô°|Ú†¯¬tH+pxT©ªïæ‘’L& ŸÒdzö»¯Ë©àI[c¾!À«J–_ÉÚ6»P]8®‹ªâ*qlÙß¯ç « 3f'®êΜÛÁø9¡TUÐÞ¶¿‹©Ë#Se^.bä\Dõ5XC‰ x ÔI>@œÎjƒ¸lÓeß¡¬‘‘¨ƒj^­ðš)÷ÚPíâ–9ÅSÿ¨B€œJ-¿DȽmmteèì¤ÆâóÀÙÒ­TX²”:ÔÞ:,#“z·6?2At€úª8eEd—U“6ÁðQÊÖ¨ T+]æù.gê«æ0_–.]Zæ^qÒV¦EU\ÎJÓ¦_J^0_3¤5Ÿ»ÏtÓ&'<;±Â;öoÙ¨ ¦LlÒ˜+£hTü¸!¦ç¥DSV†þÇ|úŒl;—cN]5TÊ,qÊ\*±‘Y…tY ‡Y0=¬â‡vêLJ~âøì|ŒK]ŽCbzÎÜÛEq+;ÄG*8 ƒä¶é-CßvÐ÷‚ŒîØðø¸úÕkVidÓè›Ö‚&Æ™ÆwÆý(›•„x¦ó(õõÜç‡îÀeLw.‚3 É’®Þâ#>P1LÏ;‰Šâc#õcø IídáÇÀSiÌR¨²ÄÈà c4©8vÑdª,KtªHY>e¶@;)[±£˜mÙú3HFàˆ±OѼ‹á–e§9˜º`´\+QMúùê3Þ?«Fqn¤uX_V@Ýð€zŸ%uýù…íêVæÉjbÈopP7qæY”Zêç=õÃÈ@ßV%ÀN0Àñõ¦a:Ϩà¯VwòútƒGçÀèÈ£Ò'bÆÀÆ’ž´ <!yìrâ¥Ë†H—å_)P±(v9ðPİ>æO@dÔC~¬ÄAt¦´ú“:FÄ’%KºYºç=ïy]NÜL¢xFKÏ{ýOùÑb›óï¢ÖÕú¶#öŽŠ–É…¦…Loûï¿E1íøÏ¥–Á %œq Ãú\ç a…Ð'%Ïâ†p¥å>//2¤‘õà7{£ÖgÖ€Z~*®g…€ÃZškL¼ÒˆòC0¦™‰ÄH̼-h-İáÚV#l"?maŒ°ÇÍm¯uQ&_ûÒMZœæRŸñ©S°_~'CUˆ5qŸ@2ŸõbСLò᳇1î£yh³ŸÅ%-kz€xÊ;¸¿4âz+¡ïõMÃîªpYÖÖ&H£=ÂöŒ hZO3ö!C "E(›?Úÿi$Ï`š0 Å`á6 "8´4œó9ÚÇÝ€¡¡"‡S½”MX5ž´7cHÃ{,v¥Ú«QÖýƒC=´Ò’žúØV…i°ÀEU‹œmñŸÀè˜åøG¤¹)‹O·ñÝ@µáH•Jçw=°Ú¦mÓcM¥\Ú‡ïçn`D÷ ´‚zpÔö{î%’ ©;ƒ9ç0 >Oi“†-{iGÓ‘™yæ}RL&Óvý†yDI30÷Õ•”¦Ùá2ŒãYnsTI@¯3²Ø§÷£IäÁBF~¹yl‘Ãm]ZÛûsÂÔ†NYÖEžYŽº”HãÑ ‚=\ Óeê¨2ü Y‰*ž¼!žð±b¦­‡þÇ|íOÚˆy—=}ÏXhÖÕj(>3E {$«#ù±Ì¿ó|¢oM[4 ¿$ù’s2¹Wåì§Â&v?À,F ÄÜ(Ê\Y6u%7W¢ü€PîÏQ£±%faÅ:ï™ãNËDß¶pd`¶fz ?mÁ_ŒS<"5‰ãü̯uŒÛ¾q< ORÁ¾¹Kа¬z˜çÇJG;·«ýn€¹Ï/ˆ ¤.çÓÂtY–’¬ÚÆg[H£4A¿‰BVïmw=Œ~ÚwÞu”§X‡ñSŒ^qóý´i9B®T´.q1î“ʲñêN<ÇÎPç fÔéNàOònº¹’ ‰¨M‚øj2šaN§@œÎˆ•ãаSP9çhŸñ£IçÈe.c5Ð2”Ät^#¯·deõ4t¥N[×BaEf+ný2hK pà³…8nû‰Tø¬óù¼*ßôèÞžfªmغ–)ì‹Ów  Ng–Û×? è<ß0e"ý‹æ„ átp¯>‹z׃4ð½š,â‘9÷0]°¾E 1Š\—2W¹ÎgŽÕF#ÉÕvLÉqpÖÅ-Ð1¿¨Ù2J:10áBaÛH?Ë6BVU'ÃA>'GzVÒMB hû˜UNÛöYa¥ ç©æL‘L9÷»ßýJ×ÙgŸ}*Œ¯Þƒ2Ù¨ diX·<Ï%~*.ðEü½â–Åd>­-Tä´GÎ…œtmñͽT2œbäÓAâìaœÂ)‚)“ˤA’ÀPÔ¹F€±D,KºV¹¢o8ˆÑö“úè£ÿ¨]@)s²Æù*r5~<®æŠFs9©ÝÄXA¢Òw§ O39ú›jó‰ztð6è}„gBªœ;áéUW²OANª':­U ¤8w!ü4Œ®±Ä% s)G—ÒˆZÚµËAâD„Aì‘.ï „ô"…ð,‡"ƒ:­X¦Ó‘|YŒ5´ó«'‡l'š?Æ'À>š±¾õ³þw9©rÇê¦e|êðd1 o»8 êÊÄòúú¥¼cpRÄOؽ¢fCM ýyðµâÞ§NPÛi—K.KÔ–ï€\©¸Gk–¸XÌü*&U‰\IFñeÅ>×QƒˆæeˆÌè ¼Ö± ¹LMŒ*,d8˜ÃË&D®_;1?"Ÿö E`$Ë™q5›f.Ä¡Km”Jñ§ Xi«LÝôE©©5sD|Û€Qâ¢éÇÊs>‘ A2Ö]AÓäù¦qrÕ ò04€”¤+-_äŒ?p€È% NÊ1gòý@##MƒŽADB¶[+¿úFD0NiBùL0Æ—ŸÄ«_€hFÇγk6(s¶]mñeÇÍ%@BÖÃjüH4­ªˆt¥”G¼íûá¹qLè}R™Vš5mPá{PÒ¤IÀ6nÑ~2óÄãúþ•4°^Wb”Ð8 ¬Y˜t85~˜D%Š“´×ŸÒ¨aÅ€UK°ÎÇ“Fà ֯¦ãe†n÷HK="âɼké`(>ô ƒùž%)Ó’€8¥S{Ö¶.Ö·OHmÚ˜½êEy(¢.[‘Žê2n•Aúz%>'½kk0þl…/Y4¤õ‚ûŠŠD¾»hp0V<ùñé]=¦ ˆJqγ­V+¢˜JõšKýò8囎ö1S$éd9§ ŸÑ¤Ûeñú9—7ë¼>ÓuX„tcìq³(OÜù]tñNñæj‹4à×ib¼Q”,*}/Hûûk>ò)@HA¥8Ä?œþ¦”•ìsRàNÖ±Ž4×ÙÉWÛ¸)³æhwÙºÅÆ×óŽÍì©“|Q©Ã»ñ2€z†Èl™€ü˜e™^´Ú)™ðatÖò{ÇÂÈÄ?t “pwO%–rê÷Œ¿ÌË4gÝÓT«÷ c±Ê¡ß0¨’… &¸p‚„POÂÒÊÀú~K¤@J‰_’[:®•ŸÂJ”Ä}PÜ&œŸáR:¤¢ç‡ LÇ'Þ#–©ü¥ŒŠ—[ÁcbB¡8ôÃÍ–=mÖ!m~vü¨Ó|Ø(0A³é¡A2å;â,ß²yç¼í#·£ÚzÚ| …-S»>#_Ãx€1,Wû€uŠW¶­Ó%¨’~ „Îó*µý5åŽ#¨ Pâ%#½8ï–y¼N×f½<C:è ƒ†£O$ä (Åq8uq=üÌèª÷é@ùþPOF@=fä=Ó'-.È©÷žÒ±­ÑAºHú(C¦žº+‰P'v¨—|ÖßÖ&¨ò¢ç”O9–9³!óDRf˜ Ë¡”J%v¸À!ÙHÐ*7ŒÐÅâZi8I9µ ä¼Fù3~¤Ï*çþ…±8*9E´lŸ«EŸè_Ÿ@XÖ¥]DhEg¨³áìº@ÂÅÃ’œùËžÜ?˜…póàÃ0ž·# Œ/T䌟–ˆ¼–ÁzÍFHßér=¾¢¬É’%õ,£µéç ›–£v9G|]–óˆÑº|`Èʱnufô×aÕ(ªuü.ºU–¦M};átþã¸w÷LoŸô³¼Õb€¾±ò²öYdí³Ï>ušÎD¤·âsò7§`* Üí¹8F´£)J^EäýDZ´óz%3í­œo¾gÓãž/­ ÂhÌW¿æ$£o« ãÑtÊTXÅðÅÀ¹DÎD¶Â:Ðõ~A½œþLE]×½:ýÁ|¿mü¹"´I¼N‚©dúýÜã¼æœ…¦ŸŠ†%Xû—'(g~‹ÅÆežµŸHu“á¦ÈƒGùãó•°ÿ±Mε*®±}ßæ‡M£ ÝE d»Ìç›õlYnh1¯ÓžXÃ߯’÷È,ƒéŸs¿š¿"ËJR—~¿L¸æÈøÔuBë9…¤ðš[âß&°õ€²ä‘* k'è¸pL¬qtFi—cXû‰nz}4oòIL4©ç6n]0€™p ŠãbÁþÀà·¸Å-ªì-HX/sRž_ciŒ]Ååq{ðÓò’Ü¥_íñçyÝ,ýæP}ôJŠÃâ̱ xl -?"o1X\^a‚uWe™K+öÚ¦ƒõ,ý&Ö87Œ2åÌISù1Ïš2€mà0ŠßGp7s†nµ3}Û‘1˜AáR˜%]»ñã’ZkŸÒ4S^•mYt±¯ì„øµÇý~ù •(öêàt †:jãYêðÌfŽÀ§b‰ó]”ªzn¿¬aZ}$ŒW¨%'iÝ•k³Ö `Y,Mi'ícú!ì×ÈeÛ7Ë·œèA•‰Er0)s¾R@ÄS¾’¡5:™¦ñkàåù€äAZÿ£ŸŠ€T&¼¹oP‰"•'uÝ<…C>þ/€âŠ:¾zÅP–׆•0L1N/#S$‹pý5‘æÕ†€8v@ gÛ¶jìŒÓPžvÌÎ8úÝþ=¬‚wˆ~vü`ÒÍ`8‰ÿ•¼/Hõëô7•Õ*"•^$nzºI"#]I_¥#p¾÷˜< (Ú³ÿ-^E¨ž`Z1QÜ¢%ÒX°íäóh<}ÐJÇ‘âÚ¶v>°<Ïý1 ®¦iItOûN†óòˆe¥.$®Rw/h’ç„7¤Ò9ñ=J¦B2ˆ_ ¥ò¤9–yO3±r4;%Ìú*X‹t7¡Z&`wÏ£ÔiW¦Ý ePÇw(³AnåéäÁ!†×d¡RƒC| ëWW°Dª8« )-çÙpÏ/‡Øið†'>©\…ðØÂ\¯•J(|wèœ×ì$GÜõòT0ÈiWãK"ÔaÙŒp>žH3ÐDÏØÿÙ¼¼á“ˆõ²ù¡\ˆÅ&e°<£ÍJ-6eëo²®4 ¤—=˜F”V˜‘ˈ¥ Keni–— ¢ÿ¤„ëkñ7œèbæ@*w*¸DÂsþDÚF£øñ#‰og±br¢5…—Rè’¯h©àY^êÂÈõà&yÙ*vû—göü“)‘CØÜYpè„5—˜ä£}n;óìÎ[e\ÅOÛN”^òS·R„…9-ã“N}‰°ó~#ZÑ“¤an8Å gˆ÷7ré Z9¢B£¤(„Hs«€$£\h‘Û†ÙÒ ¢Ñ+DxÚ\Ú;KRÂl¸ ±¹Ä”Aœqïig⸠âñtÚÐÖi›Æ¾i¼g3yž2•L®”Ä’Æcu(„@C|5<<'ålħ!@çªàu´4`c‡N ð'¸”ãÂàR‰4ˆL%Åá‡^ïùÁã0Š”[¿ÖƒØGGà‘ }²™5ìN‡ ƒãaóœ·Ú2Ø6n)S¹_Æöz™Ä·0ˆ=4˜ˆï¼\Ê-HÝOôÛ}›¸ãâ=0gÛ’oÕ2Z5~Ëê«R¢þ"ÏõýøOñ Òf‚Š¿sÜÉqÀJLÐX_1ˆ ŧ]‚!-TŒÔœ)x„¤znãX2â8ž£ÑΕ+¾ÀíêúV‡K–/_>,-¿-¸Y@Óq×€ºp´Û‘žã­"™^]‡ôó_kþµ’nÓý4¨…4Ò¥á OoJ®Ð\‡‘ЍW+W{wDÀ Öh™}À$nˆ@Œväó ´áy¼ ðÍPµNç7~–/áyçFXð1è/„a.¾´®‰WâËü¤i/©4eJ|Š¿xŽ¿ñ•>²ØÈø{Ç© (Æ¢1g{H¤¸JHù…P0¥vßóÉy‰7‹ Rïð~lPÁ" Â)£AüôÅ*~Ûz½´Ác{éßæf”Lf7 ýQÉÕñ5öÔ ßFHc•7ið¸°;ÆŸL‚,D&ŠÊŸ%DŒs2—4è2K>„8l.¼FsNvqljÚ·¨ õH|Œ8þeí×vÏ×R¨×ÍÚ åË|eÅûû–×7 %¾7z6âËéŒLpó„ÿÝwn%€xÿØD1€(§Nò ïã“F&àwkØiix̦÷}ß®EymžeQ6•`Œh Xœ^Æô«]y…P)@û¹Ç0Þ»èЊýÇ€ËÄó%×MGã§Q‹…4\&Ø3áé]ªF1lG-s¾HÌhÇÚR_ÝçoUœ;ÛwTÜH=5B%Öº`Ë¢l H~H‹v9¢ ³SÉ_âiÖÆgئA'Úr7HÈ„ïŽão¾Ä§@:Q°‹Ï‡(–Çè5ǵ"•€Ë$²"0èhÄáø8ÿÞ­vÝ.³ØE<&MYUK~Æ:Àb%@Û.Ëbúðx8Ó”Š)Ë8Ž–³º Ý8¦™•g$B{‚©eþ”¯d/·KzðÆ’zóùt …tDI°SÂÇÆˆ»Aäµ£DóÉ•”QŽ‘ÔZö@®kzüVpÐåËãeÞ½sÕ¡)%"è¨7"LÝoä¬>ÿ‡Ð¶‡û>Ãê-J.tÞ3=° ´Ü¶Ÿé?ƒ@⟔ðՒ⾟i íTÂ/‹DÀ@ ^°Àý@/C @&"5HYɯèå=G¯ØÀñ…b˜©fUs0Z¹ÍĦ•õ¡µKXãdDvú¨_éÄ{¦&–€‚LÐ?·"Ÿ¦’çÌI|:¤“ƒ6›ð=ãþÀ5%ðÐŽ.qS0Ùu )€²èôà;ÔŠ^âQÂ<ÍC,_¾¼LÙ,GQè ¬å9ÿ‡EÐòðÛ‹,mÏOK¾‚„Ï|#ßÎé§“ÃÍã„w‹Ótœà K´ˆƒPØç½£—òŠHŒJŽ=iC0*=t)áI?ÛH”v÷Ð2õ!,"Ý2ÉÏYDæ~ò:úIÀA7{èŒÓ áõ2:"ÿÉ[pí¬ú¼¾ý®\¤Ãœ_¯ë6 ?)>4…‘K ´Ï­æ±Õ´É\¤ˆq¥ãû¼¹ËWáJÐÿ„!ê¶O¶\ë2GDsÝÄ1MFl]\¡‹-!t]p ѺœS¨KiWÉç=áìjg6/·Ÿ2ÝtY’Ö#âI Øf¢âè—#ü5 ?!ïÿ’´ÄqÉfš)6:ÐÉt~›t¼n±$|…D½0n_Þ`˜`&#€+S9#ØåÜ~—¸ú›”ˆw^dT7ˆŒËh® )ÍuKˆK)‘Eð¬ L6¯Ÿ]¤PýŸBÎ Ô×Å%ô"ÿ½ü˜¤ÿ@ü„§wÎæ­qý¼Ø$ Gm”ؽ»T°#€hÙB·"vDpp"›/5*³»×&­«f¤¸sÜ̉x¯?{†)by¬ÿà‰±$ïbíþG¡2å©àzx>Íÿü¸ÃègÚ.Äâ­ž>Ûÿ°Râ ƒñÏ÷È8w¬%ãe‘9= ¬ @2 #¾Ns-_"G¯`ã …myÃ\ÿªƒ)laðá»óÁŒziÄÅ ˆøÃâjò&<(Âÿ`u§8tÿï(Áats{y PN"ó®Ë]¾ú{ûŒØ÷9TñY¯—Oüx:¡œb-ÁÕO$:s çôß÷ž”QÿÈ’¶SáyžÎ9y؋ČÐ#pÈ‘ç ÄÝ7îqœ’«"s9äAô§€!ìçßÍØø”¡GþëVžc¦™øzô±q»üÐH*›NiJ¬öÕ–ðêb È,ÛAü9Íó9ãØhšs[Y€íZ>ðħh¸‹À^† ÷ L—ü‹˜á”¸câøæâ.<îKâ¶›+BƉ6çM¾óá¨G.b±Z“sâ®›çÏGß`˜a:à>Žõ|¬‡y]#tx_S ñµ„™vþ÷‡8¾ Ág9NŽ;!î¤ÔÉ»9ºóÕž9/·<¬? À qî6‘0àÙƒY–¸Yq¦ñb[š:åêe±ù¶¤[!JšÅß>î«qÛªst‚âž÷¤ë5½Ä‡©pìÅCh‰]{ó¼[ÝØRäÚ`b‘?þy⎎P蘻UìÚgÓ¼/ï׫ѯMs¶äÝâ%ü¨¸_Ä-Ëòò¶5áb"ŸÏìþ™RŒAÄF1äòä-âöŠÛ5n‡8¼ã>÷ñ¤=5yÀE‚[̲ÁÃæ4n õh±é*cs}÷ÿ P|ñ”ì/;IEND®B`‚is32«‚ÿýÞýÿ‡ÿ×o‹eáÿ… ÿÿRz†wC©øÿ‚ ÿôk$×ÿŒ‡w%Ìÿ|üf#`òÆúÿo-Ôÿÿ¦h£·0¼À}ÝÊMiÿÿÿ^k÷t‘ÿþ„BQ†Êÿð€;;ðzÿ€¿ÿ’^XX_ýŰf|taè³kㄱ“£LÅý¡/8?ÑL˜ÿYVSñÿ`yE˜@~Côÿa]æÿÿÒXM¾´ÿrÿ¦eyÿ€ ÿŸD•ŸI„ÈßFZõÿ ÿª+n}ó§#pñÿƒÿò²~˜Úÿÿ†ÿðÿÿƒ‚ÿýÞýÿ‡ÿ×o‹eáÿ… ÿÿRz†wC©øÿ‚ ÿôk$×ÿŒ‡w%Ìÿ|üf#`òÆúÿo-Ôÿÿ¦h£·0¼À}ÝÊMiÿÿÿ^k÷t‘ÿþ„BQ†Êÿð€;;ðzÿ€¿ÿ’^XX_ýŰf|taè³kㄱ“£LÅý¡/8?ÑL˜ÿYVSñÿ`yE˜@~Côÿa]æÿÿÒXM¾´ÿrÿ¦eyÿ€ ÿŸD•ŸI„ÈßFZõÿ ÿª+n}ó§#pñÿƒÿò²~˜Úÿÿ†ÿðÿÿƒ‚ÿýÞýÿ‡ÿ×o‹eáÿ… ÿÿRz†wC©øÿ‚ ÿôk$×ÿŒ‡w%Ìÿ|üf#`òÆúÿo-Ôÿÿ¦h£·0¼À}ÝÊMiÿÿÿ^k÷t‘ÿþ„BQ†Êÿð€;;ðzÿ€¿ÿ’^XX_ýŰf|taè³kㄱ“£LÅý¡/8?ÑL˜ÿYVSñÿ`yE˜@~Côÿa]æÿÿÒXM¾´ÿrÿ¦eyÿ€ ÿŸD•ŸI„ÈßFZõÿ ÿª+n}ó§#pñÿƒÿò²~˜Úÿÿ†ÿðÿÿƒs8mk¡Óœçÿÿÿàÿÿÿÿÿþ² ¸ÿÿÿÿÿÿÿÿð7˜ÿÿÿÿÿÿÿÿÿÿë#üÿÿÿÿÿÿÿÿÿÿÿˆ ­ÿÿÿÿÿÿÿÿÿÿÿÿò¸ÿÿÿÿÿÿÿÿÿÿÿÿÿÿŠõÿÿÿÿÿÿÿÿÿÿÿÿÿÿò„ÿÿÿÿÿÿÿÿÿÿÿÿÿÿ¸~ÿÿÿÿÿÿÿÿÿÿÿÿ× éÿÿÿÿÿÿÿÿÿÿÿUSþÿÿÿÿÿÿÿÿÿµhüÿÿÿÿÿÿÿ 7¿þÿÿÿæpDÏj!ic08Uk‰PNG  IHDR\r¨fsRGB®Îé@IDATxí ¼~S¹Ç·© •Œ¹"I2EƒáŸºä"\¥¿LI’1"éâ^Ó%åªP‘Ä•!ÕU¢P·™“(" ®*¥ÒLöý}wÿuì³ÏZ{=kïý¾ç}ÏÙÏç³Î~ÏÞkxÖ³Ö^{­g̲z ôè)ÐS §@OÙGùf_—g^ó<ŽzµšÒŠJ‹ÎKÏžw}\×?(=ªôÛyéºþ`¾ùæû³®=Ìb ô À˜ ¾^ö%…òœyi ]WWâ^*<¡÷)±Ü ô5¥›´(°`ô0K(Ð/c0ÐzéWšoTÚ^iM¥A;…«•®TºH‹ÁúöÐS §À°) —þ™J(}Wi:à¯jôóJÛ*-4ìþ÷íõ˜•ÐËöB¥*=ª4*ðK!r¼Ò²³rPúN÷4ôr­¨t¶ÒcJ£ bç)­2hzôõ÷˜ÐË´˜Ò©J¼\ã Q‚ÌŠAê;ÙS k èå™Oi7¥_(+°h¢ôÌ®éÓ×7 Š›<ìÇ´½0+ õÿVzå˜v¡ŠöýºñvI .¯>èÿm Ì?ÚèÍ,ìôâóÕ?@½ºMi¦¼ü ÒrJ_Rßàa,Âƃý`Hã¤c 5uÒæCjrºš¹[ ¿A»;§ ¾];úÀN«Æ9õò¿B…?£Ä—ràðÈ#d$O}êS³Å[,{Æ3žán úúG5°³/ º¡¾þvè€vô‹–ÖË¿Ÿ2}P©sešßþö·Ù7¿ùÍìÆo̾÷½ïewÞygöÀdùË_¼x-ºè¢Ù _øÂ"­¿þúÙ+^ñŠì¥/}iö´§=Í›¿åÍ¿«üAZ>ܲž¾xOñ£€^ü”NTêî»ï¾ü¤“NÊ7Ø`ƒ|ÈE™VI»‚|»í¶ËÏ>ûì\ J§¸Î«ìˆñ½ãž-( ‰¿°Òºz›þþ÷¿ç—^zi¾Ùf›åú¢¶záÕ­`yƒÝvÛ-¿îºëºBÝÕsh röE{ Œ4ãÑá¿ÞÍü6W^üóÎ;/_y啃/­(3gm´Q~Ùe—åO<ñD›.¸²T²ëøŒbiOÐ$ïìå¿âŠ+òÕW_} /·ºf®w5ÖÈ/ºè"÷"·¹b\´I²öEz Œ>4¹Ùö_Ýæ ¡,güý×5¿ ¢ÌPò¾úÕ¯Îïºë®¶Ý{HôE£?{ S( Iý¥Ö/ÿYg•‹S?”ºÉÂñ”§<%?üðÃsIÚ,_WáRèÛçí)0ÒЄ>£ÍñðÃçÛl³ÍȾø"þ$Ü^þò—ç?ÿùÏÛtù ‘ÐY„\¯Ðr°õ ço,ëþÖ·¾•í°Ã™¶þ­0Yd‘E²¥—^ºPøAéçÙÏÆ% >úhö·¿ý-{ðÁ³{ï½7¨#ÚøsŸûÜìâ‹/.t RË*ÿŸ”Ö”Dã' ÊöEz Œôòo¢ÔØŒ÷ŸøD.-½I_WõÌôÿª«®štÐAù¾ð…üÿþïÿÌ_c) å_üâó#<2Ÿ3gN+]pGRÐ.Qì±è)Ѐšô0ý~Üdò#Zã,­f“’¾ìù»Þõ®ü»ßíÎKØ/~ñ‹üôÓOÏ_ò’—$áâpúÓŸž_}ucödž Hßé)0ýЋz“—ÿ±ÇËwÚi§¤—mñÅÏÿë¿þ+ÿÃþФIs™¯}íkù¦›nš„›F"æ3Ÿ™ûÛß6·SÊxõôdAOD hoª”¬!Ã˯ó¾ù›þùó}öÙgP*º¥÷pòOäþË.»¬O‘/_~ùås M®Èö†R=ô hN?Mé'¶¹ýd.^þ7¾ñæ—ŠðßøÆ“ ù×o~ó›|Ë-·4ã«ÑËçÎÛK¬${è)0Ð ?¤É,ßÿýÍ/Ó&›l’?ô:3Ó ¨"Ã,ÔȘÓ\Š4þ?§t ÒºJ½ŽÀx¼ ³KMNT}VJ‚sÏ=×ü¡ð§?ý)©þAg†ÿ`]8 ´TÂ$‘ámJCñŸ0ûfrßãFЄýÏÿüO]U]<»E•ì¥Ô;!µoå:®I†±ÏJføÎw¾“/¸à‚Ñ—EšÛo¿Ý\ïtg¼ãŽ;rŒ‚DúÚôº×½nX¨²+û˜Ò‹‡4úff4¹vKÍV ¶qƒƒ>¸öågqXh¡…ò_ýêWÃìÚßÕØç•æÌ¶ùÙ÷wÀФº5e&ËQgôá%yÙË^6²L¿ºþþú׿Î^xáh‡p ¡ùM=xÍ€§ÅŒ¨~ÁÑ‹vBi U¿nJ’G³Ë.;õÔS39öŒæmšArüLºþÙïÿûÂ*ðw¿û]†'a,¥˜TTËÿêã”&ô‚gÚê÷ùMÂâ+C®[l±Ea 8¥`é‹_ÿú×—î íçFjé«ê×µº-Zmh-YCý°¤|à 7d2މÖ*‘_&ϾÑ|uðýÿãÿ8ûÑ~T\ùÍ ÿý÷g¿üå/‹$ãº*úìÚkyÿ¦ÜBÀpˆ‚Û§›l¼÷}E˜4kE²M<Þ~ûí³Ïþóÿû~HÇ¿ðáÿâÛøV|­ñù/fa&KÀâÊo9ñU?2÷è'qÖYglíµ×ÎÖZk­"Éxh:pd%$2Ó»µ<4ômŽôò¯¤düùaÀ£nÖ¦m·Ý¶¶NmÕsœ‚¢†‹Z0’‚Xãò\/_áå‚sÎ9'×xˆÔàP¥ÎµŒÙôîÑ­£€&ȳ•’„Ù'žx¢éE­ùàsZ´í­§•“ŽqYÊxbø„‰ô…^˜c„4$@öÚ["Ö½³õ™&ÆÖJ?KˆÚâF€•VZ©ðµÊ/ö÷o{ÛÛr¹×Š–+¿03ù7ºø$øÀ>‹§‘:©ù1éþ¤Òâ³u®÷ý.Q@aéyB—4¿=ÓKü†7¼¡påÕ¿ôõÇ$9Ž n¸aþ±}¬©¯ë >¨Œ[—¦Â¬ùÙ3ç µ&ÀÎúyªÒsšŒ¾\zgúš7):”2ú²fË-·\¶Ì2ËdK,±D‘œh/„€v)¢CmË3yÎä{°2„òò>L¥]˜í¹çž™B¤eZºnŽü ¥CU7Ñ{˜ Ћ‰ïyJ­`Çw4íDÓç“wàÂÍø 'œP8 ½ûî»sôø»Ü’)q~Ægä¸Fªm¬¶ÚjEûüã»èRµŽ{t£ç Ì’—ÿ¥ìVg@“ÿ§#~Ÿ{1¤˜“#Y`«<„sóyþýßÿ}Z×ïç<ç9ùÑG=CªÇÔI¬çŸ ïÁ¬ë£v>%<ÐtbË—È"þs·‹+‘|wÞyçüòË/”é­ÈSï|ç;§uptÄ[ò1Ç3ˆ…mÂefÝ 2“;¬}ŽÒ•õS;í)¾ùÝdôuýõ×/äçRïMCr¹ahÆúûŽw¼#ë[ß:]v'Ÿ|r×~îé6žÉïĬé›r-¥ÎdKøÔßc=þõG4†7a©Öà5ŽWɇ &x`¾×^{_[ }iÆ £ÂŸ½ÿýï/¼ÇÊ´}Ž…Ï}îsñŽÙsp$8pÖ¼(3±£@\ÖvâX¿¸½’aLôh3™9V°Í¿çøRÓø*à…jÒð—±Ñ$Äñzôá8Ùíx“öÑ'è ¢q<ž>}&¾3¶O°”NTBé£5 ¼³úê«7z!R&1!¿÷½ïµÆ·M?ûÙÏò%—\²q_‰6Ÿâ  ')tIÍKø²£Ž:ªK> P–Ÿ±/ÌLꘊð]_ M”ûœ¹%ƒ.”SR'aJ~´ ¯ºêªÔ–W¦¼­^Î÷¼ç=QÜð-°÷Þ{\ýy•UVi¬ÚB#¯?“Þ•× Ð2JߪŽ\“ÿ¿þõ¯çÏþó[½ ±EàYÏzVþÑ~td„p´­dãÝï~·™ÜØBà %F§6ÏéÏ!‡’㬵à8ùº÷âÌ„i`^¨ÔZ¾ó .6*©m&^¬¬œkäl·G dÒÜºÏøć °À ªiÄäÝó5ÖX#Çgcð¸ê <|£B È+”Z;£ÃŸÿ‹^ô¢Ö/謸룄¯ÃÝúlß}÷Mî‹FÓhÅV¼XœN:é¤Â0+Á©NÑ­^iHÄŸVÐ `Å×z—ÚâùVmü’pÖGEwTáÁìdç³Â +4ê"Ò‚8 1}­cƒ÷bøÀ«ŽÁùw›Ö7k ñyùÿÒf ÑsO Ýmhå|˜Zø/ÝþùŸÿ¹õ Hèð6ð™Ï|¦?^¦_׿Y¤°oè>­:zG# ¡‚ˆþF¥VÖ.È‹-ÞC¡‡ÝŸÀÍ7ßlŠm Á.ÿôOÿÔº»ì”šê"ÔáV~ÆŽïôÓOo«*@êô´¡¾³¹1Ÿý0c_™E]48‰Ë¥éo”†¾ò•¯4Æqº žþù­ÔxwÝu×NPG“pã7è1¶ˆ$;°œ¼B0‰È;(ý½Í ;öØc;9ëÆôáÇpv²æšk6zùä!¹³nux—]vi„Gl|ÊÏçÌ™“Ë«r[¼/S½×íA-"î&J~¬òÖ`—åÉÑô72îq…/ùËtvß}÷λŒÿÄýöÛoà‹zòÄÜÿ³5ÿgu½•Õ”~ÓttÐOï‚Á•² òS Ž¦(O[9Â{á`$¥¯ä…»Þ‘·ïh¦â”š3ãªW/2õ7V»=tEÑzY¥ûêi~ŠënA„ÏÐÓ(‹ýBãLœJ+,$;8G‡Pš¸3TÜR󣔄·â–Ð+ ‰ð­Aƒ€nãPºhM§3Îüã-çÑp‹cŒ¤Ðeæ—lÕUW-<ËC=ÔŒŸ&`£¼h‚bÊÜSÙÍZ¿³½Α[º¦“ ‹r»í†Àb|ã§X¿Y ¶ÞzëÕa̤‡ ðöÙgŸ(ž±~Xžã­ m´¢Úé¡ D¼ƒšÿºë®Ë1¶Q»ÓššjÅ5íw›rD'ÂíXˆf˜ óB(a›f:) oE±ƒ¸†úÐäþ»Þõ®68c€Ð‹Eø$ÑÐïoä¿& 858éÖ[om3†V¶ÎÍÙܹs»RŸí¬?hVUiã¼ÿþû·±!ÀõxV h†,­Ôè3ó­o}Ëüò·µøÃC%†ßGÑÙ¤dE!‡ŸÜUÀEÛŠ+®ØxH1†÷ÐÞbÿ³:Ÿ<¿Òך÷ØVñ/¿ÅÁ¥Ã;¹;ì°ED_¡<î>Ž)ƶÙf›)}Qäã6_¾¡tÙ}Ó;T—ÝXÅ®§žzjÓ>Ð`ÕßCD¤FŸd×VS^^~‚\‚J¸$'üñÈ0íµÔ¡ßEþQþƒ÷ár_8ów 7”._tÑE“p/÷£î7 Í/}éKf…ì.¾øâ¦}ºE{Ã! ˆDœ+ý)•ºœ­kxùñÂóÕ¯~µÑ„Á@å‘G™@‘-¨ElÖrû8ÑÞ ¼ò•¯œD‚ŽŒàÁX+9!©ùÁ~`^²¬·æ#½“¶ßÔD[Pé¦&Ž´†A>ú蔲£tcË-·œèÇÚk¯=2®Ê¬4B©ºˆYæ›?üáóïÿû9q,e8j*v¢µr>䦽oÁê‚'¢U¦’õ7朖#®¾¼üZË”ó¾Ú˜û–ó…~Ÿr NdFàk8ÜÙ!#Àjbåùæ7¿¹èî5×\“³Ð;:Ô]Yøj?~_õ¢A·c¥dÛ~\xY}ÉárÛéå¿êU¯2 °›È‡;÷3¥ôïÁ8Á(ç÷ý~Á ^0Ò_UçÞÈ8ƒ•/S#v?ùÉOŠnúÓŸ6Bµœ¨ö{Åáú_Ÿ:á`ú-¿üòÑ—N.øÎÚÜKIl c<Ù]Xê$ºÎ¨QYÈ.» «Öñ|Ëx”ó¸"=ç¨X~V÷ß €£ÀšªwèÐyõ6=öTù§Ö!‘U¦‰-¶Ì2Ëdr?-»ì²E^…ôδÂGË•3|ò“ŸÌää¢|kÊoEòÉ^üâgêÏ”gåÒÏ$ÈôÅ)ßîì·ºLb±LöüÙ<ýô§?ÍĨÌdWŸi§’i”É÷^&¿L¢³L^q2g3‰Â ]qÅÙ™gžY<ë ©i¨HÒ‹ Zÿæ7¿1·= —ÔÇ‹2Œ¹œ£DËkÑ,ÆTZŸÑ¼• ×èÿWÍÓG©<šÿêe!hg²ëm]Ù´¿÷ÜiÍi³Í6Š6ø—ùS½§vš­BC.¬ áA ©§È$‘ˆõ©"Ubžp …ï<ÜwXçI™ï}ï{'ºÉŽG®åç¡ß„$sG̉ l?æªÎÙ ¢ÏÇm4z2—¾l¦ó¶(Z¸€~²dž|ðÁ¦Á¤, ½x˜JVÀ‰†+[w]b‰%&‰­õ“IvÕUW;Ÿ÷¼ç™Ú«ÃÅú )JAçœsNŽÍÀ8<›T~vŠ“" ÿèG?2ǃüЇ>Ô„,ªÐ35³ Ôéõ•þžB1&¿u@9–™v8±0ê4 /ÕûÞ÷¾ôмVüXŒRF))Zkå¾tùõg¥râ4üê¥t½U^äûVF±£Ñÿ7Þ¾Ÿ„K/½ÔäBº4 Lú>µ={@¤…ñGÀÅ$`…•¢ -LÂ2 ³”uyØã§>p5=ïLWÛi-ÚX5¢²lQ6r¸ó P¤' Ô1¨æ'\Y M|N­æÇl°AI°U…ãìuv—ê Åþ‡ oÕ÷¾ä’K&UÇN€ó±¨kN¬úM­²¥-”mB‹ ŽL^ûÚך건5è<,º,²¡þ4¥eå8²€_ 0*+ñ#¬ªæ ÝÀ_"üf>ˆ¨ )ý¨L\ËoR¢N49…Žrœ™-e]žÍ7ß¼\<ù7Û@+³ñßþíß&Õv^pS¬ÔÞ£pEUº…®ü$Ztù$IsÀçÑ™Ýe\qBƒŠxx¹ÆpfƒˆòöTÂX™kË-·œ—¹ö¦7½É<ø¼x¸Åj DÒHF[û믿¾àWà> f›¥œ5ŒL¬q{ _„…ÏÈ,”üf±C-šRêY¹‡×¼æ59R—Q¤)~!^xá¾QpˆR×o÷ 7€¯¨üÌäiJ?M! ~áW^yeÑÑd«¼€”‰M˜°.€v±¦ÓhF_ÍW¿úÕÑ|±ºà`”?¨;Wù ±~ÁÐCÊò¿ÿû¿ùÉ'Ÿœcõ¸øâ‹7Æ ¦+‹Z™ÃaÏ?÷¹Ï%õÅ'®…×a™ðXØÀ¦3v1’M};î8Ó ñuóAŠFÛ;¸Æ]Á\`Â=öb×=_o½õ qgJHî”þñò²#úÏÿüÏ|µÕVkÔŸ­¶Ú*—BNJ³ÉûÿñI} 0ŠˆöT7&îÙºë®Û„!ˆAÜH)ë©?íAZDé!AC÷`üY<îð¥ ù©K™´ ·m!ô‹ûVå QØ4©ÈÇ1ÎvC‘S-¾±‡¸7Cÿ=U¤ºÒJ+™¤±öÛØX9ôsuC½ùÖÝE¡ °ôKQ  9åƒSç@Õ%zÎ%¡ßRù™ê8¶þI=xZh¡èËK8§ :Å<fVý¸·¿, Ä@5j€º, e§Fÿ`²úÎփ¾0EÝïØ•]CH úðÃ7Õã|P$öqŽp› ŽÏMì¼9 d(‚ ­”•¾*÷MÅ·.?¨‰~n«¡ã‰:”:†¦¢Å?"}"sÌ1ãPWaYŒ ÓÓl‡Kì SÔèXv@¨~7صͽïC÷`hYäà/}éKƒfŒ€bëžÃ4|ó›ß4ã^l—Ï;ï¼A£Õyý˜;[E™¡íu×H¡œTåY¤ì}º%Gœ„ºyTwe—8'¢êÄ+;^È«ëˆéžÕÙ­¿ýío7 uÕÕ“Š»/ÿ¯ýë$î38±Ž²ï_?Ý=vU£n̪×ã?ÞØõÊ+¯œ2PV².T˜ i8"¦æ˜PíWõ ‡~8µ§©žñõøÓ)½FädÑ¥G_;$_fËmÙš‰²…S‘Ð/ïP^ÎÈMüÔL¦AMB8wqŸ†`ô£.1Öhé ÊÛ‡ ú p@-n./W”ÑB€ÞE9oèwƒ0c¸_\õ'ùç)áùÄ ¨`ª·ÑTÕ÷/7²ÝFÏ`P@Ü>è-‰HúúS¯õ|ÇÄ×*ܳ|5²Ñ‰³Î:댼Gá:‚a^Ó¡gÜC2÷ººcÏPuöÑXž~Š¢›l²‰÷yµ Ûüº9ÇQ¦ZÆ÷?ñ(G…ã©$ÄïJéì±Çk""~ýëÀjµµÝvÛÕUÓêÙg?ûYS_|“Äw¨=£n{_G0âú¾Äå¾¢bÛ¥³B²•ë/ÿ¾öÚk tåî-˜§œŸß…µkËÇ ›‹º…$PÿFj¼@Áá‡83²ÝU/kŒ›:y8Úq±:Üs^ÒA: ƒˆL<,®ù hBH5büÎëm¹ŠsÈØƒƒ[hˆÕ€ÄÅ͉ºkÕr³Šçî»ïnªG*‰ì3Sý˜^POMéä¹çžk"^Ì“^|Ôóhâ )¥à]Í W¥b¤ª¦Â¼gÀÈ(F›†öôYn¿ýöûˆº6x^†wÞ¹6¿«Ëç(¤\œÏšêi vÆÛ3„Çx€Eó/Iïßæ™è-u€ÿ~Q)šöØcºj?;ꨣ¢mƒŸ .ŠÈ,ÅâŽm4–~ã u_ghƒôº¤_}Œ}bÇ êGs± —_~¹iÌÍÆ,,ñd™ ì8Þ¤zÇDÜ­ËŽý&,Slk¨žç/{ÙËj«âëkUI iwÕ6yH?,çÀêyž(<1&ýw‰8]Z-FºÕùc¤#1ÍÈ­·¶O!, ­^âªÍøXÅÆ ©k”¨÷¼ç=uÕøž]9o?³4Ï?åëAèžõ«cþYµÿðØÒ0¤S¨ Å}‹åêÉåࢮBËÖØ-\a˜¡`4®€h3öÒùü;TûûÐC%£BfãìËô ýÞsÏ=«(Lúeü=†Ê»û8¯IÔ?y\ ý#¸…*Y’OU2›«ÁE7[ªM5fSÎ0VÏ»rúQy䨱¶ÙåÔÅܳžE];x fë;®  $µ;?"?Õ1|é»5*´£G¯ûî›j—†6¨ËSwÅø,,2uu¸guÊE6öVÙÑ!nß»)3^SÔ£hÚb‹-4yòvl[éÚ‰m㞬Ñö‹•Ü"z,‡žòÕŒ’HŒåúஃâeøðĽçE¢`îèrå¸uÐAålýÀ¶šmÇ\1¿,ø4ø}QõŽ6ˆ ŸpDµ\­î–cŽxy,[/ò8?KlÜ5*µ »ðº¯™k‡ÉÛWÛj¿Àµ7ÝW”¿8’UûäþG4ì“ÖpÍ:—¯Éµºxâ~ÝRϧ>UÂÅòÏâ†~•Ïï`͘üIÏŽ£ Bn%s¨/V`Ë Â¸yðÁkè’ç(vˆ*Ñ͹k°|µsZ/Æ–ÅÌõút½«±âÚE¾XˆwïYºë›+ JÎ:ë,S±õm¹å–¦ºªAH.5×mÔßÑ!üt¥$¯?_ÿú×MD‚ÃkàbÝu >K3Τ~!?NÕâcÁ°HF\[|qX8Æ Më¶W\qÒv>âOÀõ½Íµ,Yb÷e© KÇXÝ7pDs¦p!–Uz¿R2KÚRæ^ ¬®¿»¶®³pþY$š6äisBK² —æMpm[&¦’[•p†O¡M(/‹lY¶oñø‹®Ą)G¨Íò}Ƭ̋0ÐñçÊ3ý¶B⟔sûg¥d€û—·LŒÐ﫯¾:Z?N'CåÝ}nR¿Äu ü!&¿Ç}Ýï¢YÊáŒÂáo¹"^Rˆë:´Gò/_úP9Ÿ—Áªñª¯|¿¬\fåIaq«ë±rëXóžO_1!ð ¥Ã”Ì¢¾yHOºXÕ&áÌÆD]¬à–í2.«º|Û•'’ïw[Õ]ô²á«;tEǧkÐeßQ×G>ò‘`?Q°*3ÌR¾†èÄ}ø'ew]q.å,êÊÖÅÄi…&Ðtá0|‚¯Uš*DMÀÜeµno-aº¬‘ƒÊ±ßm®1í3v>vj›Lü”¨6šùœ9s:i;×6ùég&åÙgŸ=©z´ék›ÄâZ¤–úð8«E(c›ŸŽÃ!·ˆÒYJO$"ÌnµÃ¶xQ±zݵ%‚WÜ{ï½Ñ]áÁº”W,Ö’š˜ms¢¶YWè6®çõ¯ýþå¾ð»ª ‚dh…V毖¯þÏ®Ñ7',ÇI c-Åj›¡ÿ}ÊI5Dü…ž ‡ †VQj,¯Ô(9)IJ˜NZüÿY7%£?ù…ÓÝ'‚L—À±Éª¬âp`jʃèwk]0ûîÕ+}¯ªp³§îލÞ =}`ÑÈ\tÑEMtµ.P …¬¦~ Dœå?ê#R›{¨Ã sSbK‹ b¦.!¦Á†~C"wׄ´«Û&ûèÚÀðÄ„Ë 2aÐU·Èùœrà’ëC‰ZòÒ£³Á™û»ßýn}«§_ÜÅÀçÐ7F,:‰ðAåo´ Xвl¨r Všß’?%äÕ¦ìújg •)¶{&#šLÞT2ÅkËämRy™VNúß÷¾¾Ûï‰ \[V¼‹L®6O“‡:³f:[fÛo¿}¦ÆTʼn'ž˜É‚0“«4Sþé̤a&'ª™vO^4®¹æšL²ûIÏd>œ)øF¦Å##qÒ3÷Ï¥—^šÉ?“ów+x•}AðYùŽ™«â–¼þóRR˜Ljá™ÜÖe2 .ã-l±,ÕçéÆbzOߦ£Œm"Tký¯J“|ù)¿ à¶¡Õ¢7/Ü‚;Œ‡ˆã‡ÛlìÆëòºg]¢ ,”«7tµp‰MD dBógš¡ö«÷9ï¢é6€½~÷?NVCòòËŽ)&þÅ3&âGɟæ›nlßáa½¦x†B¬ÜP2YTøtªì ÁdqÜÖõVõÆ4Ö— K¯³x$Žá7—U"ŸsÎ9QFdÏAzß­âÖæÿ:¯ÎØT„ ´ ÷Á=÷ÜS„Aç…és”é8¨ßuž®}ø—î}[¿^í@•l£ôx©âÆ?ñZ³ãŽ;&ŸWÕƒèËÕ$O—4b!Ça ‹ûnÑE(Ó æë¨{BtŠ|¾Œ·ûÍN†3¿B @YzÀ<`7X§zìÚöµ%¯æVÑäÙ¹¨ðÚJ´úʃÁêÁ°™Ä°Ërï½÷®í[Õç\™>ƒøm LáÆ„(5„_e¨Ó 1ðB ±&°ØÄ] ˆ£Ã¨]ÑÝh ר|œÉ¡ŽOzªÒímÇcÓ8Bf ƒ²îºë¶éÚ”²1¿ƒUµÕ) àF l}á£4U5ÌýUÖi?ÂGòAhå—¾<6øBì@rt‰hS˸÷=h¡2KÌw”Q T°&‰ß‚Ï|ôBønÜ=éÇdRθ¶€mUöˆºò“dS"ò±ê „ž‰‰’IΙiKÊÒø¾¶Ñ™ >Šòr¤‘)Dv&ͪìî»ïδ}Íh[ .¦úÅÌ´MÎäÖ”?–IŒÍÚ,Rþ¨}>¨‡ôQÎN2Ù˜›šJ¶ï¾ûf²W0åf&Äi!k¸Ð£¡ß—4&“æ`&)Bñâ*ÒT&)QÆ+Fp&žC& „/IjŠéE]Tˆ¼Í'g$ñmÚù\6ù¶ç?e¼B)0q…Ù¥*Í ‡˜Öü˜÷ÖÒ¶‚uÞdªm¡¼ƒ¢I@«.Ä rí¥8ÿhƒ‹¯,[H"9\,W’ŽÔ©wWr`,†u`ŠÈ­J¼ól¸á†E@<þ ^®†¯–áÿë®»®–t0,­ŠEåúÁG)-àW*»´ê ƒ2lÒ¤dÐV‘QW8›ÝtÓM…+kacšœøŽ·@ªû,<µ´YÐX¦;¬7\t‚OÄðtÏZT®-ãÑUž:ïV=GÂHM,j¼på]¹Ð‰ ‹‰NÁ~ûíWE)cq“x°¸Nyè¹!æfôE’†§äôÞÚe—]2Å·7#±ÿþûO«b—܃™q%#ýÃ,\¼)å824Ø:›¾Ú’LiÏw#e7“(2ó79Šs°.<%\&&? @Òaè‚ .(Õ3ù§ÜbgL X^XW¸»îg«+ €Éc39uÿN\å¶v‡3‘Q?°ñ—è±|kÊoÎÒ£Çw\áGÀ‚[î¹ ,Kþ®óÈÓ©J”pðÀö˜qÁ—@˜w)ÇOWÞ²°îª$6wÕF¯!ÑÒ”~†7/ŠF_zŸÕÜ\S‹à† â]eãÇǼÓP*X¼Yë¬Î·ºrn ÄŒ™‚‡LÉŽ¼æL¹¹QlYê÷°¥Z؇ñJ ¤ì$£Vm!ju R©|fr$:¥~v03ë€Å0ÆUFÿ;e±«koÏp¯&ç™…ÏBûHJäáh¨x넇RÇGAOf˜ïÀ=Ž¥UnFõVôÿ®>B4äÃ5„@yàXW+¤y’8Tí­¬Ü‹’¹^á™Y}§¹ øÊZ!´ë(—oºP_ßÊzä‘Gf¡Å‡UW‘z²[n¹¥ŒÆ”ß”å™RhÈ7ð«Ç6Ò:1¥HS+î}é׫Cœ “¬à]T¡Åwd¹œ…6–<Ôi™Ï®íê{‚¤Ê·a>2†‰°> À„L0VXf‚Yä´!øÌ÷À²½vå,ÜU+ñ]å+N2}Œ8Æœ“}€(ë/ Xù –º•GWÅ—ÔR?ã!Ü,f i©+–G¾úƒYäÉ'Cì\8dõß‘f¥€e~YòÐ&fóV¨¾'2r òØÉ%º,6Ù…2†˜q|}c‘ˆØ¤ìÆ“e‹V·ÈÐ"ƒ¸Uð16áȆ¸²Õòü/ßí‘»‡æ¦ï‹éCNún»íf6ÃöÕa¹W·¬½öÚÑ*ÕúÄÒ|-«_׺Ê,;LKÚHY|8½öµ¯õÝÎî¼óάn—î)´vÒPe²¸ Ù~¤Êj«+›«Ëwµ(ù„p+×W·õB̃Lµ øp’pæXàÛ-TË•ÿÇõyYm¹Š¡üæ ÆŽÿ õ[†›ñAú&ò.¬^NÁgå ˆRþ·ø*:*à]‚eÒ^ù\kß÷ž(Ä|°˜ÆŸy,Ãð;Ïï-ä¬!P(êÐ#ïý”ƒeq)Ëø½ êfÝ@bø¶q0½øZÀ,D3+Øy𲌠§GM: À}¶Æv°ÔWÎëî+]ù•ËU?Á Þï«–çKÜŽ’1 ½'&>µm?›I”:-Îð“`m•k­µVåΓÿ^{íµOþcøeQ¬pÕÔñ\Vߨ1 ¶`´T 4AýˆÊ|²•J¬% „ëK“+»0t-¨àêsåk볯-Yf^xa”ž”åÝi§R·ž¾f§Ü«S:G«{7uŸMzجXÀ²m·|„ø˜…Ë><|ü ŸD‹ò2¹÷Uº·+à™JfÀ»j›’ÖYgsdÔàM©ÃW/÷ô•1Õ­¸¶NÌ(cÐÄc‹Î¢Q_òøš'ˆH—@ÀÉ3Ï<3'2.>|ôÓDɵ¨å:ºžmRÚ×nÇ[§¯wj1H©¾6/.»}í¸{©ÁVµíÎ¥ï0¥NB÷ˆl¬U=c&Zκ9\BW·ŠhH¡IÂULªÂŠD`¡ª'Ý×®"É¥ØI'4©|›ð~ê÷ Ÿ !ÏB:úE«Â•x><“‚Q´܆Åê)?ÇÃvpWÎSþ­cM5{Ýÿ¿b €p¤ -•\OY!%ä^{, oÄ^¼pNbk ðâËHA¦¶mp ¤7!ša£q3¢¿ëgìª-döJ[Ñ(*b^æDÓ‰ÕÉs$m«£uÆ2àˆ·Ø¡6—]vÙF\Ø¡ˆæ­WG´Z´Äõ–+ãh‰¯Pçݨ\—û-ëÅ)xÕyÓϸ`>%œš:ÁêÕêGŸÕ²¡ÿÙb[@2ßÚ:c1˜ åDó áâîéH̽ ´ˆ'gqD)kɉ2Öx9á„j_ ‡WÊ•ÅÄâQâ¥Ú(½å6ñ­‡‹®6@TœrÕßøîk ìR} ¨xHùm·Ý¬¶Î1©ÃOÞªƒåÝÙð×öÍÕÅ<ËsÌÕ!KÖ`R×vÙ,×kÔŽZÊó¤eƒ]gZŠV“˜{9[Ô|ðÁSÊVërÿ‹#«®xNÔWÆw·pÖ²úÍ“4À{Þ £(ã‚ãRË™ÓáÉWµå:ºüÍÑÅ7É\ûîÚ>ûp‘½€+–|ÅÏ"/£¯^îñRX¾´u ïµ×^ÞúáÈîÀ[7b!œ¸ÏñJ WoÙòMËqíð!ª\B1 McYªóªƒºaÛ—JÊžùRhQ-ßÕÿœySÀª*,Ã//ÓSF1¹tÇÃõ‹ÅqÐÀ9¿N¾îp ]- Ïå·P;åûÐÕ¼õÐ' *pº§é„äëÒP«ÑåÙvÛmsˆãËúÑÐC©Ìc[ÂÃzVFâ`YÅCí û~ª*/ JV•nYîMš? ,ú ò4ÕHëoRc ÿ‡²ÉbÌ—˜*Ì|<á²+@ƒ…ŽTÇ0OkŽ E­l¬®ö  OSº%nS²ò%eðÔÊÈ&”r˜ô>Í5^Þî蔋)/ÝPt¤`=¡ú§ë~“¨ÀLrë"'‹eX80`Šõ“ã"FAÃYRæ)áëcýèú9º NœÝ€667VªøJqí› `L 6ëb{Þ%9»²­,)ªÝˆ‰)an±Ò3™ë€ç)¼Ž.û™R/1ZfMÀºufçÍ­RKPØ&øZÊ ¥’™st‘J¡qyÙËë¥ ¾<ÑÍ (óæJj,()ÄŒu„ÕP?¡ªÜuŽã xòu²1P®Š8]}îŠÒQJÛaÑ¡ÜÎÆoìÐM¾"Ùa7U®/ô›E ô¬|?…!™ŒpBQÙÅÂo èSíéQ¢q¨†m•ž´z©V™øÿUW]UX´¥jà kÓı櫊mã-?òÚ`KãRË¥zÁ±µâ7ì|Š8’“³×)Õ¤ö…³´Å>a2ÝÿŸÂ"ÎMí_J~Ĉ; Ç¢=$‘œœ«·M¶X爃‰®ê,±•Åè'VgYs e&«H+T/ ­`ÆiÝY„ÚÄ}vf±£L¬oˆÒøJµÅDßî,Öþ ž×™Ý¶íg]yŠo~ó› ŸV^S¤ÿ¿ÓsûÖ_ÈMUÀq€Š:&Ö~j09±íÄÑܹssŒ‹Ð%ÇÐ=úX}eã®!‹¦šUtÿ«çÈàÙtçŸ~”îuxs<ðÙ¿w[“:,ŠYx¡bXׯÔgU;„&¸—Ê ÒŸAH%ƒ*z‰RcD ©I?Y턌)ñ¥fÂÖ9ÖÀ®ÛRvîìDšˆ}|õ~øá“úåû§Î—‚¯ÎAÞƒÓí³|óám¹ÇbóÊꦸ Z,h5ÎcÑvtJNìZª»*¡ðƒ²›‚ÏæðVR>p¸vóI¥v‚—ÿѼ;P…Ë+]ß!o±:™»0Ÿò2ÇD 0ÙFùÊ–ïñ5ŽqûËùc¿–ÚÛ?w3U<Ö^Óçè= žd·Ô5¤ª»Ò^þ˜•e×xÆê“ÛpÓ‡Á¢ˆÃ.Õ:V[l±E 5ës^þw¨ÝîA/¨„õC'^ a¶¤èð[ ƒ°WÏkæÌ¨0ÇòYŸcÈS(ÃXëT>Tœè×u«xÆY¿y©ŒTœÌ¢Ñ9j`ajÂÏ©%Ó'üM¤Œåi§Ö)þªJÞªv jdS¥{ºÀ½ÂÖ”V_}õh“ï}ï{£u±õ‡{Ÿb”T‡c?6ìÿ»:jÔá{¦HGQÚ¥d€;-÷Ý/&Övõ9ü‡ž‘ÿ–U|«ÿ[Ì“-óÐÕË‚ÒBÆïÈ€Eï&ªs8 ÆÐÄV·¹·KF %ŒÍ Î}ðÜR/Ä)§œbÊ[WbͺÁK5©k«Í³®4ôp5nuâÚž uc<¨g(DY¤5ì¦b€TÃ×wß½¶ÿ_>Ë«îáƒÆ\ï"¥FâBt¥1n榄Îu°%ål«Eò¦ì@|uÖ¹.‡aYççŽúJ`jì«»Ë{MÄ|øM䥷êÿ[ð†ÿ3j€†§wtZê5iK=.O ] ¬äpÄ8Ÿêš^+*¢ôÙúa…ùRK-UްHØ–Ã}f‚+ØeÒ€‰ÒE~^î:E tì]ÞÐ¦šˆØ¤,„¡úB÷yap±àVeÌü¥#|˜[œƒñ™½ù bpÐ9j€"Rˆ~î>Læ˜î„UEš:13oÀ”…÷¥TÇhz®Ò…JfÀºI½0§X$Å–3ÕUاù¯…‰èpýÔ§>UÛÇ=÷Ü3ŠGÙè·bð9\ýƒ¼Â|å,nuóÕ5.øo%¸÷Þ{M¼tOêsðñßÞ{ï]WïÙu³ÞªOƒ5­ Ÿ§„8Â|C¡ÇÔ‘)/DÕibµë±=ýê×ð(™4ûÚ¬Þ#RR bŠ"H#ªÀ±]AŠt¤ŠÛ¸ü#—Q¼"YèVÇð¥/]ó´<ôy§ð}â×yÞb‹ª^™_­þ…0 –]´W ë„—×ø¬.ULØnÇêÁ^p²‚+h S*ÖÎ0Ÿ³pqt°´;K‡hÓõ}ÆÒ"Æd,bêÊ)FD±€5ž~>¦{ˈ¶£B4éÇ,e;“½cR**ESU‰'æãð ƒоü 6ð±ö-V^ÈÊñ‹˜B›X»ƒxNp mòU‡ihi1â(L-øÆ¸õV ”kë#ùHj÷¿ ²ãêÙ"Jñð«%X¿Ú¢@q~­Sä`1ôA$åVõºà‹´ÉÖÜ (-Q¦.YBJ¹ö`6:«+Ý…:¼Rž¡|òÉ'ªÕW¼, Mœ_ŽXŸÄÂßÅ‚›”éŠÓ“²£\#¶RãêÔYÆŽÙÔ¨Þ™Óé§Ÿ^[½•¡‡;(ýøPû©Þië"¹ÐÜõØ1Â×9¤˜4cÈdyÉBýis&ýÃ*¤]hѧoðô‘¤Õ=B·[ŽYŒþýCÀÎ'Eb‚¤*ˆº€Æm|@¿,¥“h—Y< ‹Å‹Ê×b¬VjÔÃD† ïê._9׺]B¨­êý˜h ¸¶@¬AŽè" Ò• ÚoÄ 'ÈfL F¿,ú(bM7 *uè7ý©ƒºy¾:-¯*í£zÆÔ‰ïT:Rû/ +ê¥9Õ#ÙŠÆ\O»¶E†¬ ùÚ¥B̯;Ì¢®!$Õõ5tå3Œ³. WýèG‹ø–HÂÕ¾ GjÇÝo1¹ÚN›ÿ‘¸X”ÆÀ·NôÌV>Æ;r}抃ÛD@î¿‚Êv vVS¼¢O(ËiñlÿÈ!Ùy&‘L¦3¼©È‰'ž˜É×\&Åž)ùeb™)øC&?SžUoHw;“âNõvñ¿Vïýº›:ãÕ=δ]¬}Þä¡v*Áb’²dbxfR:™”G d¦/|&ϵôê ôbuUÕÀê9çœs2)@Eë—Ús¦ãd0ŸŽ2™ŽÁçÕrÃ^½ûÿ+šß÷Å2äs­\ÏR"™`¶©3ætÙe—ë}Õ}õã¬êÂA‹ï÷*(ŒøÚp÷,%Õ:cÿóÕvõW¯MUcm†žÃW©âPý?o1Tw÷9nZý–ƪmÈF;µÚ·Ðÿk­µVÞOúHÔÁüu»|¦ˆ¥ñ³)uJΞô5’òM¼M¬½öÚ™xzŸUoÊO@¦­û¤Û|!åýeÒ=Ë?ÚN×fÓñ¤öù¸?Ô±!Ú1£y•AZœ™Lv£ÕËÕ\¦£U0»K9 >¯>¨Ô»[­æ+ýÏå²ÒÿüÚ0Û÷éú„s­¨Ù[ÞòköLÌ»Lê¿Áü²È >+?<8£í2ȉFù_óoiÖæ•5\íóq¨óu´ MÖh¥† ’¾d-ÀÖ_ŒioVý3ít¼Ï|7ŨÍvÜqGߣº{§ê#úX]†&φº¨w É+R=äCг©µŒŽ ™¸ÓÞìÚŽgR5ö>«Þ”nþ¤—:o5‹éyu­Í'ñSßÚ‚còPb±(¦ÕÅ6Z £ Ÿÿüç3ùi0ÕÆ< ¢÷šyUÔ!ÍϤ9­"|%N§l×0Ô`òö¥Rø‚Öm½ª‘ƒ‰L¡’«·‹ÿ¥”Ywòï–I„7QOÓ€ìÁ'êðýø2“rïÑŒ¸'UèÚ~À]qÅkó ꡂتÞ`ƒ 2YzóÊD&1¦÷™ï&_þøÕÝû˜>ž¿­ËÐôÙÐuä›Böú„Å|Éê8ÛÕºH6^½]ü/óÓ,v.'#¼ièe28*Ê5]“’îIDATA(“éhQGèdê¡Gc?¶¸­¶Újçëa’ŽŒ¨ãÖ37Sø8ÇsLªä‡íìi<›äú0É÷§ ËË'sD:RÁõ懙Ç"`¶ˆR®É$/Äc–2Õ<Ò.ˤ­W½=év3äY'Ê`Stà¡w]—¼ù˜ÚU¬ƒL¡Ã½y¥Ä“}öÙÞg¾›2ñÎäsÂ÷¨îÞùúhÖo£êJâ3}]çS"R‰P¿¬ŠæÔ· ØµN[ø§hõÕ•wÏý…A"šBÌà Š7hóu£"´øw¶X£ØäÆ7v éý£1Ê܈•/?oàýSúѶùW:¶;ƒ‘©Ú˜ô†ÀôA+Uâ6€±‹\}¾ë™gžÙ¦‰IeGe¨ÃÃÑ€Ø Ã„”8†øó ©˜§º?·DŽòÐáRÑif‚:»€RRHTQÅ0ª}‘D­IÏC*Âèô[BˆQúõ¨·¢ºVq+ÿ AMj»u/Þ°¾¸ô%æe:¼¥,ü¸9÷s'ÅÛãŒíD"ðõ_Oeg.¨ƒõ–Š¡‹-Š˜džǙMC|õµ/ùËQœÛî4~£°Xlë¸Ár]ltå(gµÔ#˜GÈJ3¦ÝYŸRCo‚ïù3÷Í/õL”Iò%-fåAÀÎÛ¸¤Ž}¥\=ìêÜ~ûê/ßc2I"P» ’Ú…›ìQX¶Új«Ú¾Bׯ~/ÖÔ—0_>Ày‹›–+FhðœÀ+©þ™êèÆ‰Ä)HZ·ï¢`1`Õpà®M˜<.OìŠ×à6€Î{¬&‡Uœ¦{ÀîBœëÚ¾r¾}a«ýéâÜŽÅprc#®¿·I¶þ©ŽXà4€ —Ù"Ð¥©DJ æÁQÀ·3 ¥èQ;Y5Ï/¿üòTT'òÃPŠyûe’6àO´Áé\ §Å­vG!°&õ;ô†:Ö ¥ðºäƒ”sŸˆ ø:ªí¥gÏÛ¥»\ G‡fàe"®}ùåŒýÞ|óͽ‚ï:ë׆`G˜X¤[Å™ÝM›`™Ó¹H.í>õÛÐ0Fãêsü7Ææ†{ŽSZXúåêàŠó”†‘—TùÙ"z’Û0éÛßþ¶É“¨91ˆÕæƒ}öÙg"O9¿ïwÛm:a£}õ–ï!íøÙÏðþ”Óµ¶=‰>žtÒIéjX‚8VGªì|þ%é—UoÄ!Ò†ð€Ê„9¨ò êøâJ6L¸ó"Š9¡ Ä¿ |‘¬qîØ-Àån ¸ú–\g˜†)C>Ó±<òÈ#&—Ú¼dM< ¹¾¥\ñô#c¬(™?Œ)~ ª€øWÚŠ¦:ÜÊçšoÓÕ±ˆýßë–Ùìû‰€µ B¼HéÏ©³ââ‹/N,BvW £OHFÛFv M·Þ–¶P%Æ%UH5µŒÃ°¾t1Ý×·UVY¥‡r?C¿C½¹¶ë®Œ!P«põÕW'»[‰«u{þÿ7áØƒ£€t´‡HÑ[DšUII¾¦ÔËàÇt÷];|Cb£){nàg0&tmqE§<&!ÆWÜÂðg¶Åè CÆÔ®Ëô$ŠQä",_rÉ%“æü£&>#Õ6Fq §D§(%¤ààZQ¨‰Á%jJ"UH‰Qˆ8²!Ó§hUå5ÖXc§2~¾ßˆ˜˜¸|é|0È€eeõáå»}¯¸â š¹'ïÞÕf1Ðør¯~¡Øž[ƒ[RbÄ6 ‡’É/0(ª¶ƒX~üã')ee‰{؆nå²xµD÷a¼Ðµ¨–eÇW¿›Ö+æÞ áTµÑCˆ"êQM{à 7˜>Ôv1àr:2e+Žû.Oì²<´ö/z,ˆ¾@ØGÈËMŽ›ë®D`(.¡·`•¥;üˆµØ–Vº‘-O«]çþª4Þ aßþÖ+JF ­ÿÓÕN! ˆ@ó+5¸§Æg…›/qP$áãy,áØ£¡h¢Iv)JIUœ0S­ós_gÌðæ›oÎÉC+UùÅá‚ük¯½v¢OƒþÁË‹Tǵ»ú‚»6¡9Al-LYOÿar¯%<{ˆQ@„ZNé×"FoEWõ'%”8ª:D¯µÖƒ~@jü@_G`N² XÛµæÛzë­s¹Â.|„7+8%!ÀÀÛÊø¬k‹s5L´aÂ~ûíg¦»™ª~¾|þ™Ë»¾#Õ@ù©!ì£zz°R@DNöÀÀ0Ð;ì°Còàú”/ÄFmÔŠ)è&•<\Ñ)¹Ã.Ã. ­LŸn…ëÏ ®úЇ̴‘Ò)6)BMÙáÜsÏ=M»ó%œ¿Îú2ÌÖ|"ÚTyaàòZlÓÝàº+Ò„òN€­^,Ò¯+Ëu§vêÄÜ•vO=õÔd4e\ýM̆†/† gÁ9Œu× º3IÑt4Dì[e‡1œòä~ÝYBuõJna¥Û§ÔpfVÊQ¸_2‡1#±üÜóØUÁ$ ØÙ²À ¤>˜k±v‡õú SÄW¦b715Ñ¢*åAW€ãO*Üxãe4R~ÿM™ç¨ÍšR@\A)Ù½ŠÊZh)b=áXL4ÝÊŠ>0 ­ÚoÔ²>§&€!ÑG‘¬¨âúÓöŠ4>~ªgé&ýiR†/0&Å–¾p4)ùpDQ¸9SÙrý,6>}‘ü“£¨ýªÁÑøKá'²¢ï½Þzë%>¢ÀÛoróqï½÷EÂ-Z"'˜ˆ]–nhãm¼ñÆf_|CyøÚuÔQ“îûd©³·UÏ€²;A‹)v•,"mÁ¨_½¼_Dí DÐÝ,“Å—‡s}Ê6^H/9_œo|ãU K1nh$2Ñ^ÝpÇ1µ[‡s›+RøŠyŸßwß}uÍí™â5æîXúo@‘'pCM5jKÙr¾ümì=„Àÿ*-¨:{è’"jc¯øÃ Ñ—ðIš| ÊZ_LH«¢Ð v3|ÞvÙe—`¿ÀFºĨç¥@RCŒ PºcVñjû?*ÈVEŽ)Ÿüä''šD7C~‚4 Í}´ [ÀOT¶gúuù⻺DX”„›âñ¢à®94øu÷Ñþr L.ëÄäåûÀ>Ðb>ÙŠv¥ hkmð¹8ó[·ý|ù99`ë°n<}ϪÇ>W_ÂõwÊ;3£úˆ`#"ð"J7$ ʤ¬pƒs0yr¨ó9þ"LB¶ÌÜ·$”h{P0“¾ÀV†’Ï~ö³d=ùä“ÍbÂò¸1–Ø>´ìûç¨ÎMúYJ·´¬ü}ï{_²®»úUØœ+@dÑ41ØRsß’öØcBo¿ Þ¡²3eÀÁ*êCkÒÅoÄ¢ïo|£iªcõ’—¼¤Uü â¾­To‾„R« sL¶&ª·ðpQÎY} £ ˳²²‘úÐ Œû-O8áó¢Œýƒ“ÏßrË-I»1Íщ…‚¸H ZÀã*;wXó¾o§D~i¥»[ ^!ê«3¤)O–êoŒg8 žKq+Æv³ë™ã¼@¿ÞŒB·j¹,hL6Q”¿‡?üá6S‡²œéÞZš’ýÏaS@ð<¥{•Š>D®¾à–ÿ ÉxôÑG›åóì<º4×û|ì0,´&;(Ä{÷ß’“’rý0û:°\äåï |DØi ÄÊJ­×h‹!ƒk¯%%Ê7€È¡­Á(i‡r|ÛÂ8.0ï¬.Æ Õ[ßúÖ‚‡‚weÄš©ãD~ŽkMÜ®WƇ—ÿ@Õ×èP@ò\¥'U÷*#fýc+Z}Ÿ4 ìê(ÁK¬*©ÿ€”iã´À°ƒ!Z¥_èx.l×q’$¶\ç¾ûî;ÅXšsæï·ý"ìÈf1¥ä ìÕI€ˆÏêó^D˜4‘Ù ì¾ûîÅ5Å’3é±ÇÛXJ0. :ú+¬°Â$šUiXþùå—/‚ºà®»éW€ŽŽ[¨£o?r¿GèI h€ž¡ôÅêKú?J?8ÑàÅ,OHëovØ­£”RN'š„Ðõ€s;8¦¸ã¼¥ébÌX¿µéO ?9Óú_#K ÔJ| T' îÁR~˜tåÄäÅÝǃòýºßì"°^+›%Wñªþ?ª )6)A\Øòã´¯?VÛÿ*=©ãøãŸÐÞ¬Ò+ñÿ•µÑøP@FÄ¡ã•`Ø´äÄ0›NF^h\pY‚ˆÆÅb>¬ñ,rêQ\ˆl”ên›ÅÆhª~G3®´yë­·¶óRay®8.ó¾Ç³B Þ\¥?–´ñOvÖÀ“LÄjB:€ŠÕµ+Ï™™¯hÕŸ}¹#£´ bCmÚáo½.½ôÒfC+_Зc[‡.Êpåõ,µÕÃ8S@ƒ¸ŽR+1¡{Ùð¢‹nW&¸¢«ùE!|Ùi§æ ·5Ý z¨å6Õ§H¡ƒ/ï–[n9Å翳Wv'*Í?Îó¾Ç½D æ²JSc„ëfÀ×ÜvÛmg~y}“¶é=¸Ú)Å\ÖÁt-0÷`t¼lÚŸ6åÐâ¼ä’Kº¸þ^•¼A8õ0Ó( ]P‰•½5_@u€X cÑjZŽN°KÀ‘G‡º¸®)Wäø(ã/Eé)„_“ûHYØî[ø# }»Ky{s^ ÈŒ ò¶J¼W'Ûß /¼0IùG¾°]?k»ð’á=DsÓõÒC¸ûøf€AÕy×…gôğ׹Þ?93I!Éu¹HiÝyti}ó)St¢LNA3Éž[××Uzi3÷Ìd·P$ñ/2I2½LM諞Éág&m»Lª²E’/¾L®¿3®<›NТ“í¹çž™Äƒ™x!]¢òGUöIjÎî²Ò¾®1 €Ž„%ÿ»Rg÷T)¾D®¡íÆ©-lpàÒþ¾o|oÖM>=Ìf hlªô3ß i{­3þ÷REY½  8Å?½å¸°àŸ¢ôäh6¿}ß‹#Ás4!Îm9±‚Åñ²‹Ã fûËê?¢Uü\y啃Œ?£ïýœï)ी&ÇJè „Vƒï|ç;EÄ ]B/Ãl¹’8õüýï‘À SÍH€žæøþfOGM’g*®Ô)o@õMQb4ÄKb4$\Çvñ@Å™HCçž{î„“Õ)„éöÆwTÝKÝøÎök/0ÎMšõ”õ#J/7i•MçÝLH2©Òf×_}&}ö ɸƒy™¢'g›l²I‘N<OdÝú­9Zé#âò?>ŒÇ¡~H%-ÐkW¥ÿRZ2¡h묒¿gò@œÝ|óÍ™|fwÜqGv×]weÜUP0ÍLÆ8™ÂŠerõ•),[¶æškf2¤&ÊìŽÎW:T/þ/†Ùð8´Õ/ FI Áb*v˜Ò;”žÞ ŠNŠHé(“Ã’L:÷…¬¾“J•È–?[b‰%Š$k½LÁQ2që3Yðeò€T$t È7p½ÚæÅçÚƒ‡ýà!Šõ–‚å”÷ß•p 5ÔϚÑ£/¢pq·’®()€f&;™|ðO”U(ôLÑ“ŠÿÙ¶óÕæ‹Îv«\qO÷Ë=«çÇݺ÷zñ?ëyÖßê)Ð-ôò­¡D$Ê3 ÕÆ$ l–zÓ*+q†À÷Õ•¦e1îvV §¶iÝŸ §‹ƒoE_š;”Þ¤–VV:SihL¦.ÎÓÓ¼Mïb€îP%oVZSãpÒôê*wÑ£¾Žñ¥€¾@x%F­øa¥ÚqšðwDÕc¸V¸¿N©?ÊŽïë2s1×Ä\Xio¥Û”â®7^æÎÅIÒXÁo…íJ«ÍÜ™Ó÷lÆQ€ «Ä®à¥N?ù"X£„ªí¼¾ö{)Í 3Ý÷ôú4çSz¹()µü®ºêªÉ ÎIGºq•ÒJËöó§§ÀŒ£€&6.Ë_¡t”ÒuJLúF ¥ ¤(F¸Ï"âˆÁï„>½öTª¢ÕŒ›\}‡ÆšôÏVÚ^‰3n²Iò~ðƒ|5Öˆî¤8T„ÖRÓ Äâ¾Zé(¥W* E'xüfFñ¬£€^†Õ•’ýâ€ä¬³ÎÊ¥o?)vî³6Ûl³œ œ¸-›ÀïN¥ •ØÖ¯¯´Ð¬Øëp/>±)££ä+ú³ò½”ßâ dŠ(ThìÉÏ~& ÔâMó¢oÿ¥{ç]ïÒõN¥»$®ü‡j¡þéa4(Ð/£1^,´`¶z³ÒtG_PzTéJž÷ûWº’Vú¥ÒzÉyÖCOž]P@‹ÀÙJÓ ‡tÑ—¾ŽÑ£ÀtYF"#†‘Þüg ¥Û”Vœ&ÔnT»õêµÓDý7ÛÛ ˜Àm«×‹÷{Õ±ƒÒÛÖÕ üÏUæ ýË߀r}‘ž]R@;m”ठUC/é²}]=z ´ €^H _†a»‹†âËZ Úí)ÐS`Ћ¹žÒ”7©âç÷¾Îž=: €^ÐE•ÐìÒÉŸUÆJCQè€ }=f7ô²®«„î|›…à¯*¨ñù³›š}ï{ Œ)ôò¾HéX%Tm-€Ñ±ðÞ¥Ô[Úé¸wv¯ÐG¨½ÐK 4_¨Ä˽ˆÒJhðýLé‡J7Ï/êg=z ôè)ÐS §@Ož³‹ÿÑ* ªïýIEND®B`‚il32牃ÿ•€ÿùÎËûÿÿ“ ÿÿþ“Žÿÿ’ ÿÿd8Ûÿÿ½‰€ÿ ÿ·ïÕ³¡–_ÞÿŒ€ÿ \Pvˆovê€ÿ‰ÿÿñiqêÿÿðÚO Šþÿÿ‡ÿÿÛ*Šÿ ü!%öÿNðÿÿ…ÿÿÚ 7dÿ×kø€ÿ5òÿÿ„ÿö,`rÇ èò¡z`è€ÿþ>$DXÿÿƒÿÿ9ƒòÿQ2a•£y#…þÿÿº§´ÿÿ‚ ÿì Â"~ÿÿuwÿ ö]mÿÿv+XŒ-ÿÿ ÿÿ1âÞÿÄ©¨ÿ üE³ŠóÀÿ€€ÿ 9{§%ÿÿR•ÿ™ÂÿÑ?^”×Foÿÿÿÿ×\VÿÿÞÿÿ‚Ûÿ Ê6P*û€ÿ Õ,ŒÌ]½þò€ÿl-ô€ÿ/1;ÎA–˜Uüÿÿ¨ÅÔ[JÓ×€ÿ=üJYÿÿþØLÿKŸ¢ÿÿ¢~ÔÂj…y&>ƒÿ¡Bnæ!ÿ¾Cÿƒ°PŸŽªÿÿüf]}Q9gË]€žÇÊ.¹ÿúQ‘` *ØÿÁ"[Y¸k€×ÿ\x€ÿæX 9ßÿÿÿÿsî VÏ6e6vKí€ÿ•3Ú©ÿÿÿÛŸO4†ÿáö}5‡¬ôÖ€ÿ6…iúÿ‚ÿÿ\&º½ÿ4´ÿ­rÿÿêòÿÿ³Ç “ÿÿƒÿälORÿ¦,ï­rÿÿqRÿÿõ'‚9/ûÿ„ÿÿ±ˆ›ÿ[[¯V+èÿÿf>fÙÿÿ…ÿÿ›g±ÿÊN[÷ÿÿ€$R Ãÿÿ‡ ÿÿ³3Œþ­r€ÿõe)(Ñÿÿ‰ÿÿãY2ej÷Õ‹ uôÿÿ‹€ÿ Õx9 K‘æ€ÿŽÿ0êƒÿ’ÿÿéÙ‚ÿ•ÿŒ‰ƒÿ•€ÿùÎËûÿÿ“ ÿÿþ“Žÿÿ’ ÿÿd8Ûÿÿ½‰€ÿ ÿ·ïÕ³¡–_ÞÿŒ€ÿ \Pvˆovê€ÿ‰ÿÿñiqêÿÿðÚO Šþÿÿ‡ÿÿÛ*Šÿ ü!%öÿNðÿÿ…ÿÿÚ 7dÿ×kø€ÿ5òÿÿ„ÿö,`rÇ èò¡z`è€ÿþ>$DXÿÿƒÿÿ9ƒòÿQ2a•£y#…þÿÿº§´ÿÿ‚ ÿì Â"~ÿÿuwÿ ö]mÿÿv+XŒ-ÿÿ ÿÿ1âÞÿÄ©¨ÿ üE³ŠóÀÿ€€ÿ 9{§%ÿÿR•ÿ™ÂÿÑ?^”×Foÿÿÿÿ×\VÿÿÞÿÿ‚Ûÿ Ê6P*û€ÿ Õ,ŒÌ]½þò€ÿl-ô€ÿ/1;ÎA–˜Uüÿÿ¨ÅÔ[JÓ×€ÿ=üJYÿÿþØLÿKŸ¢ÿÿ¢~ÔÂj…y&>ƒÿ¡Bnæ!ÿ¾Cÿƒ°PŸŽªÿÿüf]}Q9gË]€žÇÊ.¹ÿúQ‘` *ØÿÁ"[Y¸k€×ÿ\x€ÿæX 9ßÿÿÿÿsî VÏ6e6vKí€ÿ•3Ú©ÿÿÿÛŸO4†ÿáö}5‡¬ôÖ€ÿ6…iúÿ‚ÿÿ\&º½ÿ4´ÿ­rÿÿêòÿÿ³Ç “ÿÿƒÿälORÿ¦,ï­rÿÿqRÿÿõ'‚9/ûÿ„ÿÿ±ˆ›ÿ[[¯V+èÿÿf>fÙÿÿ…ÿÿ›g±ÿÊN[÷ÿÿ€$R Ãÿÿ‡ ÿÿ³3Œþ­r€ÿõe)(Ñÿÿ‰ÿÿãY2ej÷Õ‹ uôÿÿ‹€ÿ Õx9 K‘æ€ÿŽÿ0êƒÿ’ÿÿéÙ‚ÿ•ÿŒ‰ƒÿ•€ÿùÎËûÿÿ“ ÿÿþ“Žÿÿ’ ÿÿd8Ûÿÿ½‰€ÿ ÿ·ïÕ³¡–_ÞÿŒ€ÿ \Pvˆovê€ÿ‰ÿÿñiqêÿÿðÚO Šþÿÿ‡ÿÿÛ*Šÿ ü!%öÿNðÿÿ…ÿÿÚ 7dÿ×kø€ÿ5òÿÿ„ÿö,`rÇ èò¡z`è€ÿþ>$DXÿÿƒÿÿ9ƒòÿQ2a•£y#…þÿÿº§´ÿÿ‚ ÿì Â"~ÿÿuwÿ ö]mÿÿv+XŒ-ÿÿ ÿÿ1âÞÿÄ©¨ÿ üE³ŠóÀÿ€€ÿ 9{§%ÿÿR•ÿ™ÂÿÑ?^”×Foÿÿÿÿ×\VÿÿÞÿÿ‚Ûÿ Ê6P*û€ÿ Õ,ŒÌ]½þò€ÿl-ô€ÿ/1;ÎA–˜Uüÿÿ¨ÅÔ[JÓ×€ÿ=üJYÿÿþØLÿKŸ¢ÿÿ¢~ÔÂj…y&>ƒÿ¡Bnæ!ÿ¾Cÿƒ°PŸŽªÿÿüf]}Q9gË]€žÇÊ.¹ÿúQ‘` *ØÿÁ"[Y¸k€×ÿ\x€ÿæX 9ßÿÿÿÿsî VÏ6e6vKí€ÿ•3Ú©ÿÿÿÛŸO4†ÿáö}5‡¬ôÖ€ÿ6…iúÿ‚ÿÿ\&º½ÿ4´ÿ­rÿÿêòÿÿ³Ç “ÿÿƒÿälORÿ¦,ï­rÿÿqRÿÿõ'‚9/ûÿ„ÿÿ±ˆ›ÿ[[¯V+èÿÿf>fÙÿÿ…ÿÿ›g±ÿÊN[÷ÿÿ€$R Ãÿÿ‡ ÿÿ³3Œþ­r€ÿõe)(Ñÿÿ‰ÿÿãY2ej÷Õ‹ uôÿÿ‹€ÿ Õx9 K‘æ€ÿŽÿ0êƒÿ’ÿÿéÙ‚ÿ•ÿŒic09](‰PNG  IHDRôxÔúsRGB®Îé@IDATxì¸4E•þÛ¬(¬b\u]LDÅÜ]A‚€’þ 9¸¢’$*É’DQD‚‚ˆ²Q”°‚~P$ƒdø@EP‘$"bèÿû«;oSw¾™{g:L¸·êyjª»§»êÔ[áœ:uª*Ë’K$ „@B !H$ „@B`æ#𴙟ŔÄÀx"çy{ûl¿ï”±<~ø´§=mÒ}ü_ºN$f7½t(³¡”û„@ƒDLž¶hOŠÿ¨ƒy·âz+nâE ¾Žø‰0¹„@B`<HÀx–[¢z ˆ˜½r.&ü©²¢ož©ÿŸ%?ŸüüòÏnóÿÔý_埔ÿ›üãò¶®ÿ¦øù¿£kÑóŒÖŸ¼=IcЭô0!0óHÀÌ+Ó”£B b²]™½Þy‰H~­ükZþß¾Zžç ¶ü¿(D°ð@ˆ‡aüWˆ@ñùå’ÿ£ü}ò÷ÈÿºåïRø[1û'Nr¢Åñ†ø’@0 žt“˜Q$`FgḚ̂h1|ÚÕÓÅ<ÿÞNþ•žý‡üâò‹µü¢ aðƒjh ~'£ü/[þ*…·‹fþ+œèEC]µLI§‹„@B`è ªÃzF &hœÅ?'«ôõœ‘ü òo“ÿ/ù7É¿@¾“c4ï)·MB_wú¦Û3«ò }íÑ}§oVî‘ÿ…üò?•¿Nùaj!¸V‰# -LRgÊt,ãœßD{B 6ÌÅ$‹‘¾ž1b~³üZò«Ë/-ßÎðcFCu;t¨G9 $àiƒn‚Á­zçBùKä/V>Ñ×Ê;ôþSÏã8[o¤ !uÑáŒ:‰¾„@ψñÑf`˜ÌéÃ@ƒÓóet±¡ü:òKÈÇm‹÷ðföñz<&އNè³q .ƒÃž`Žü9òç+ï÷‡§úQÞ±M@(ðð)L$FQìˆF­DÙ¬E ÅøŸ!&ö_+@6–__ìxöe¦ÿ7×±@Ю!À°ðùÓä L‚Ý€0r^“V`J8Ñ8ëHÀ¬¯ €©h1~ úÂÜ|‹É­¡o¶”_[þùÑ÷0}3Á™Ö¶¬ »±và6ÝŸ‚F·ó'N8¡H¶ô“MfZ'5š('ªÆ£/,ùu¿€2ñùíå±â·óH?fŠþo¦†D±f€%…??Qþ a:@¸Ë¤éÝ'—HŒIBH$ŒbX´‰BÕ¯û—ê~;y?Køp07<Ìm¶·!cÁˆßî:]|Mþ[Ø@"¹„À!0Û;¯*ŠDÊ0èÀøaö;Èo#€cÔK›aä›Üd&ÿŠu0þ¤æoQ"°­€§8§à«ò_jÓ$A ¸é“„@U’PÁôýØ! fO½çù×ÓýòìÒ‡KŒ‡º~Û‡ñáò‡Jàà¢`# ë …á>¹„@B y’Ð<Æ)…B@Ì?V÷³aÏä×l‘˜TýÍ–U» p’;@Œ; (H{Fr  €€œ’>­QXϯëçŠ"TýøçÈ3gKsü84ýÛ.\ª÷’ p «|ž©k„±ä H@ƒà¦¨G˜¡èzUQu¤üb-ê`4ž£n=JÁ€h7+r-&Býsýºž_ן‘ß¹•ù4Ïßbæþaø¸»åw‘p67*·¤ ˆä €@MQFb"^ÚǨÿXù×É£~fäi†£ËäFöicDÓ*Ã?'!`J'‘0#HÀŒ,ÖÙ)3 …Ôïƒä™ëÇ¥Qÿ£üÛcpñû%\®²,–lŽ2ñ‰¶„À8!€q*­Dë”´¾ ýÕËßÿOù4ꟹ‘üÓ¶”Ý*ãB³3’T'¢c„@ƨ°©Ýˆƒ®ß«7Q!¿@~ìGýÊOȸÃpÓåGŒ2üã°Ëkãò8¶ 8]D¿_ùz\8$»€q)ÁDçH#€‘.žD\/˜!êý¯ÈoÛúΣÈ^¢ú;0x<îéO¯oEb/‚Á˜ ‚ @Ù^/¿è¿“²VHù&—H”D %KŸf ÿ]1Jü/yu»>.ªÈêt1SîÆìÿþ÷¿à‰'žÈzè¡ìOúSö—¿ü%ã¹ÿƒ&¾Ö³ž•=ûÙÏΞÿüçg/zÑ‹²X 0úg<ã]…‰þóŸá1þ¦¬>Kþò‰æ‹]öºO.!(@J€–>>êü©»âaÿÕuý-ùËì¨ßL¿áÈŸ|òÉì׿þuvÿý÷Fà 7d7ÝtSöÀÅW(k½¹…^8û·û·ì…/|aöš×¼&[j©¥²W¿úÕÙüóÏŸ½îu¯ ÂÁ3Ÿ9yëƒn´õ–âÀÞŠËö=*ûSEwÚ/``ð§„fI˜i%: ò£NC¿`-®ë]•åÃZÙŽÄH ѱ>òÈ#Ùí·ßžÝ{ï½ÙÅ_œ]{íµ!œŠè—¼ä%ÙsŸûÜÌ£za^G€øÇ?þ´h {챮ѼéMoÊ]tÑlÕUWÍ[l±l¡…Ê^õªWe±@Ðæ®‘öØ.€ýޤ>ˆ„\×ó'ƒ¥'¥–[’0¶E7; Wg_Xëšývj!3†¡ƒSŽGú0U˜=£úK.¹$;ï¼óÓ o¾ù²ý× jùǼP÷ÃÜí‰Ç>þaOš0s„<ÓÄKÈôÁo~ó›ø³p½é¦›fk¯½v†p€ö€÷íH GÜ#äþ<½óqÑö9Ñ™„€* DÊx 0R­z< KT uòÁðK!Öýgȯ!ã§óz]†YâcÆÿ‡?ü!»æšk²sÎ9';òHä•§jz3Úæù™øÛßþâxê­ú®,&6ÏyÎs²;ï¼sRz+¯¼r¶É&›dË.»lÐðŽ]»PãçC (sü>öIRa¤d „@c¨s“Ö _%­<îoÁpÅs¼y~ýõ×çŸýìgs1Z†ÐÁ‹Iå¯ýëó—¿üåùóž÷¼\‚BñŸßd=òù—É_ûÚ׿š ˜DÏlŸ~úé¹´ÎZ¥˜”ßIöÐ |ØìI÷i‡ÇÆZaŠ8!H uêfþKéú^yÜ“Áð~aú0C»G}4¿à‚ òÍ7ß|#•á]®ùûÀlݤÿFé^‚\«òÿ÷Ïe48‰ÎÏ|æ3ùÕW_KCá솼ǂOñÇ`/b! œó ä'[8¸¾¦ä „@B Ü™+\Qþ1yÜS\hâ~ ¿íŒÿÁÌÏ8ãŒ|¹å–+˜&£i˜(#lFÚ‚b¬<Ú ´/2<,hßzë­ó /¼0GرÀSRXžoJµmI¨¡ý¥( „ÀPp'®p ù¿Êã†Êüã¿æîƒŠ|ñÅ/$ªô\pèê}XASÕkí1¦´¤°ˆs¥•VÊ¿óïä?üðD©èwÈ‚@,¼ƒ +’’É% qBÀ·Âõäíþî‹A‡±ª[Öôù~ðƒ&(Lƒ‡ñ£>÷ýL Ñd¼øÅ/ZçO{ ägžyæ<‚À Ë§•žëǺ­hDH6‘\B !Ôi{΃VÇNàÎ=zÔüe»ºÿª«®Êµl®`ô¨È1讳ʿà/†ƒÎ÷[ßúÖ\+r„#»Xhò³„Ö]£´žG}Wè%ƒÜ&—H$£ˆ€:k3ÿ5#f1æ«ûûÛßæûí·_Á䱿×Îzްœ•×h=Ð~8ÿïyÏ{òŸÿüçEÑÅ›¿°(»C&’K$£Œ€ø‚™ÿ;tí‘ÜÀ™<êÇêý¬³Î*œvÎ jpáX9B“Ý´®?¡Î»nq\B !HŒî .#ÿgyÜP˜ÿDÒy~×]wåÛm·]`jXÅkãž‚Á ºt݃vsÏ=·Ø3`ÀÚï@qþ7U]a²¡6ŸHI$EǬúÕò^çïœ Æ™A1ZÅÂ]EüÞð†IKáü<…Ý… XXÚ}÷Ý mØP`ò&Õ °¿±Â¡ï™š|B !HuÈÁ@Káså¯”Ç ”ùÇLéw¿û]þÑ~40~«›koþB€Üaz¬ðñh#ð¾&MS¥q$Âïè|‚ü²Ë.›(]ýPp]ú²è(„M®“KÌv’4<ÛkÀó/> 6q‚›®Ï)ëÊûÜ÷P&FTìÝ/ãµp(GðŠqe¿úÕ¯Âþüƒ D8„s´æ>æÃüœüǾý8á<ôr㼄òü¯ýkðœ+ð÷¿ÿ=œ-Àù|7,Ç‘Ä`‰;ᄲ-·Ü2Tcß m` .¥±ºðº@8J5˜nŠ:!H$º!@GÌ –Çy´6q×ð¯Uþú}ýë_/F÷ñ†7ׄ÷¶»/}éKƒñ–ôbN¤…:ž‘8»²ï¿„‹ÆÒꆕwä4,l¢„s4\Ôž ¸YéíkŸûì°ÃËZh¡ì¡‡ §ó5A£{-›Ë´S`8ýOVòó$³Ê*«d¯xÅ+2 A ñÊW¾2h<ÒçD?Fü8ò€ ZFÿŒ´çΛýú׿Î~ÿûßgÚ¶·ãè_ûAS@~9‰8á8nø¶ÛnË6ÜpÃp:"Ç»,NÿU&Ë}\ÿN3EŸi’0ÒÅ33‰SçT° Q!?¿||Æ{cWš!nê­·ÞšiSŸìºë®ËÞøÆ7fwÜqG¡^¯‹˜µ–Èe: (ãhà?ýéOEÔË/¿|ö¶·½-[qÅ3YÏg0{"LeœB„ íÝŸé„ÂìÒK/Ídà8)ZiBzЇ0`Œ&½Tã6OÊ´Â":² Â΄ @ÿ*¿”°Eðt…Û©ÓUB !yÔ鮦ð™ò5ú“Q’.*Ôíœ~'àjõ¨øÅÐçY:¸Å[„ƒƒ®½öÚü±Ç›ÒzQ‘÷ãã<:¯qÈt‡´ù%—\’qĹ„Iù–$gZ#ú1‰ã‹1g¹%Ž|6ì<Åt Ei¥"¹„@B !0Ôéz³Ÿ/¶:{wÊöý1c<ùä“s›o¾ùò—¿üåµ2:?§ÂD…gð믿~~Úi§…}`ÀíÎ >¦±ý²÷±Ñ)Ž?ÿùÏ9ÂÈÑG/³Ì2Í/Ø h„\$Î<óÌ\ëï‹4Ì´Štua,Ii”ã?¾À/¶ä¯WâÐÁ?¶¸8ýð ¾×Ás”.@ZÉ% ºPÇêÑÿ­¾»QÕ¿j攕IçÕs_Ösä­™ÞÖ[oÏ;·•¥FU‘Æ0/Œ+4°j`饗8ÆÂPY\ãïÀØ÷¿ùÍoB–ªl|Š&à?”&BÀ³åÓt`$—H$ª  ÎÔ†‹éúIyœ;Þ‰»Í(´éM¾ÕV[ÕÊüã傌‚=ê‡1:ݳ2’Q‘Oç듟üdÀ˜Q;öª+µxTî¶ÛnEz baôðöz®ôاâòI h'Ý' éPçiàt]ãÜáNÜÕøkƤp kÿºF§¶RW~óŸüä'Õñ¨¸x8 .œo0?î¸ãÓ×öÅùüóÏ_‹Î.»sÏ=7 ê4k†×ÂèCŠwùµå_Ñ©^ëy:“ž% vÔa£*…«ÊãÜÙNÜÕøkæO”ìn'Zr–úVõìŒGK,±D±´ôâ4kÌÊØD3äïÿû#Œ"9l¨*æ|ϾŽCN\C˜·×ˇ•Ô%òŸ—ß@~@Ïž.@ìTHÉ% ލ“¼Tg£«‰»ÍŒ0ȹ­É¹®âm¦óòûî»/Pì´j$l£Šòœ9sÖÌásò_Üý­í-?üð€Qœ^Í ! jH†c /’?Pž ¬}“œža@aš.˜„LºI$ftˆdZáfò¸Æ™?ËðH’Ýøtê^edæ¿ýöÛjs𠤯ì¦lƬ“î/{ÙË&à)—2žmˆYrÈ··ÜrK@fBõÀKCÚ­¶+¨s:H5ŸÆB€1mͯÊ^Z°pÀŒ–LtZÝr«žcH¸œ|Ü.ž¦{„âÙŒëR† Ù‡€:5[þo¤k\£ÌŸ#d…rØ–¶ê:ô—¼ä%!.Bo@“˜ÿD!öók!€ýüwÞy瀩×õSVeê¤`3¬Éo-ÿ’¸‘ë>ib@ÒuB !0>¨óè ]ã:YPOüSá×Ìåì³ÏŒ¡Žyf3 ØpN£™³þScxå•W†rB À|¾jt)VZi¥Â6c „׃NÂÀïõçÑòËÆ­\÷ÉV $]'£€:.ïú÷]ãý³&_ˆoãgý„fþ‡rÈÕé·6Ì :ê¨PVÞT©Ÿòñ»þž“ q2j#¸ùˆ˜& ]´· öÊx¿üüÊopºNÓ#…!v²j ÚÙ1•:h:7F3«ËÓáOZU'ßúÖ·Bt²ØÏþò—¿”ŽZóýÙܹs3ýeÛm·]ˆGy(_ú°3ï}ï{³W\1Óü}¦;¿4ÍSÊEÛ<‡·.½ôÒj™á4_ÜßL‘Ñ.ðT42D¸œü òÌ?}FþjOÿÀëš]kKJ3¹„@B !PwP “Çu²Šžø§ä¯G{,¥ai£B®Ëx ËØ¿žoùË_ªœFIÓg0¦œ¡ÖÅ—)3k{–\rÉü¡‡Ø¾¿±í;ä¤ÑGíZÇ•Ú7åÛ§Ð ¤Õª<ÉÕƒÀ؉Ðõd;ÅRtH­‘ {¦oЊ·ö £=¥•éð™„K¸/›´÷ßvÒI'e‹/¾xˆk G”e³?°ïŒérË-—í¸ãŽA  “K¥¯3 Ùõ×_ŸÝzë­!êÄ pÖ üSyA+ð<ù÷Ê_¦ü/¿–®3ÚB¯\2ä*#€ÊÎúÌì7 ÊÿC¾ÖQŠæ’ÈW_}uv衇fš·Ì;<,ñ£%eÙ]wÝ•­³Î:ÙFmT"†ôI?P~[o½uøLK6afýDÞu=àF[‡geâ Žæý1Ûh#ÕLÌwdÙjºæHDì6‘W–Ÿöwy]¦©a“\’P¼Ùþ©;#Bañ¾ý÷ìÓ ó óg´ŽûóŸÿ<ÍSÿ­k ûî»o6ß|ó…¸Ç‘‘÷ ¹›vïÿ¦F¢¹Iß4‘ ÷ozÓ›²m·Ý6ûõ¯½ô¥/-•¸Tÿá;lƱÜzÈ8m(œ§¡ ;ÓäÙ`ë=òÊúÓøl-„s›\B !hw< ß.ðVç9dþò·êUîÂ}¿¡wû;ðÀk¥³ÎÈÄ8Ã;äÝžgeß{'àŸ|òÉà¥RÏËzÇá8e W 4¿øÅ/Šr”`×w²gƒËüW¿úUHª 6SÑ:bÿµÛ \%úк§kTI0 )ì K™=½œ^JtA`ýÖsF$µÖ)FÿêØ²o~ó›!‰'žx¢ Ó?~æ3Ÿ™=òÈ#áÅ-·Ü2„Ä=ŒQ$éâíȧéqèçbtŨšouPQƈ˜ùq1åLŒ>¼óðÇÿtžAfÿØceà'¦Þ%IJžxq„íxpm¦…kpÔ‰‹ª|¼˜r&½lþùç/¼ŽÿZ¿¾å»ç>÷¹4èà¦0÷ÐÜã}Ï=÷dÚ_`šû‰oŒÞ5s§°¨8KËs*áî Tùœ­U¼‡z`¢Pu“\B µvÖÝIÏgêhÔÇ„eJÏQîÞÝÊáS\¬†,Ã`7ÜpC˜û§³×6½¥cÖè?¡üñc0P6!‚®_æÏ7Æ’ëßÿþ÷áuLqÃÍÌûq êeï¿ä¿/\.T¸¿ò~ YÖ=}{0ä>¹„@'’Ð •ô¬èˆè„Þ!ÿFy¸œ;']Vw0Ü©§žB3ÈpÓç£Ô»µ)ÆM8˜“”iogôñÎèF¯¥ˆÙí·ßŒaò×]w]O$bÏÀh›9#s§ V¤ÕîM£Gý¦—ÄâëöÄc¦ê<Å!éjieHŸozâ4Ñüáh¾¯{4¸‹/¾8[wÝuCZ"pMg§wÆø™5´A$ŸUðšƂFà— “ ÉuE`Bdîúwú#!Ðu4lþƒÊñH½±“Ú1‚ÓÓþÎß# 14ó*´ËÏ*ŸTHýí•ëäF F"…=# ÎDýJX†wb¨º˜‰ÁÇŒ¾gÓ»”õ$vË/¿|VXa…Le/zÑ‹‚ÍšŸNBË”¸ú­qº#t â÷‹®ýåQÞU²¡r RúëG‚äDİPGòtu(˜¥¿U´\!O=¢'®¥>)ÞУ*fés½1W:}96ýaÎ&B¬EwüÓEÂ{øv†Ï3æå™“G Ð ‚A Ãïd£ óì£gÎ8!! ˆø’ëÊaŽip|à‚Ác{LK-µT¶ð g«¬²J)JêT;Ó'žµ?osÄï©Ph¬¹R×Sžæ@·êÏ“}`ÌrWK‡=Ë1œuÙWòu&U¸§2ÿyùxÔQ:a:÷ .¸ [}õÕÃŽ=ûË0J梙#ÇZþnÙ O§žçþ¯ýŒî`øh!~úÓŸfÚÚ6øöÌ?ªi ùð¶¸G]Ÿ\sP^ŒðaêàÏô‰W`PÙîV]uÕlÙe—ÍØ¡ð•¯|e˜fbOˆØMUâ÷FøÚ+l/ð Ñú au/4+iZ`„ o¤YBDZ)1G@*~õùÓ[nÜÊR­‚$Ì!ଳΠѳtθŒcÛYŒêößÿÀü‰façNž4yîÿ`ÜÌÙ3OÙe—esæÌ™‡á¿øÅ/Θ«G8À0=ß@{rƒE€r¤Ìð8¦ap€0v„"B@@  <ñvL¼óïÌÞö¶·e¯}ík³…Z(|ãúÀ{c¨ ½â>¹ÞR~maõiåëËòØð$m€@I.!˜VGÞÐõZò·ÉãèDjsêdC\2þƒãçb°¹Fvášû~¼˜z.Uoø†séq‰›ì„Ñœ÷¾öµ¯å².Ÿ'-˜¿á oÈ%TäœO_–®~òÞí¯Ì{ÁKÂ@®iƒPŽ”§…Š¿}÷»ßê„¿\¶Q-™¸t=šçÑ~ÒÅ9oSžƒÓu Œ&¨cà8Ò0dVø¯ò_—·«•ù©€3Ï<30à׿þõó0bQÖÓ3©þÃ{ë­·^.#»@³ãwd__|ñÅùA”Ëš|R¼R!†ƒH ¿7Ì{-›QzOZ‚P¾”3¦&ÕžiµA.A.íAQG]‡ÆL Í²« ÝÁºàð¡0% ë§ÔcB~íåú;!³ï¾ûfûí·_ØýïþûÙˤœ³à¯Zh¡ xðÁsW†£‡á9„fC^X‰ÀZø~;,²–¬b<`V0.–Òyï~þç9šöh¸õÖ[{J ú#Üyy$‚È8;ðG3€kV\qÅLSXÙJ+­4YfüÆ×÷#˜÷¸Eôí)Zÿ"º‘ìÒ¾#X`UIJ@UgÀ÷4p5tú·R–—_@¾ñQ¿ÒÎÌ…‘#£vÖÙÃ8ªŽRÃàXFÜ0µqs04Þ€ Œ³Ó†CäkñÅ›ÜÀp5‡ëÜ­ „˜ºãå£w3¨kâgŒæÑ X“ÀæF`Ë<„6]b«cöM@P`ß„NAÆ#èNÿË3:øj•Hɿ݇?üálà 7ÌØŒˆ²Ãçh ðüÍò[‹ÎËBwÚ7 fKÀ *Ì~³¢MùÓ¨1BÍÏþá›·â‰G­GÍVÿs(*TÖØ³þf3ÛLͺu˜56 ¶g00z»Ü1"…¹À`‡Åd(O˜;BÁ½÷ÞvLä<o‘ÌaG0Jò:“Ê-e†Í ùe{hÜ»Þõ®ìƒü`†v€- í\ï}?BaÜþ÷S=ÚÚÔOƒ…¢5‘R$”nÜ?‹¥y]cüUù…ä6êWZ…sGˆ‘%q:Þlp0iFãŒÙ»Á§]•V•ò[ßúÖÀì±a`z:&¯²-Fœ1–ýíïgìÚïy¿ý¿Ï»”5;*²;<"ìŒût€ó‡” ‚,Ý-{»#Ž8"[k­µ2ÊÐÎõß÷#ÒxÁŸèú*×;T†<ã@ñS§°‰Œ„ÀÐPÖZ 9!äóòvñF!~6Ðð°Ãƒ»äɆë™êÅôss1‚yò(æ˜ñ‹_ ËÙI€®å ‘vØàˆPŒ¤ë{Ãøzð¦Ñ4°/ƒ¶á ù–à3OþgR™‹Q†‡^«%…š*(òÊÞÒ„L*3p1G…r¿ÀêkÔ€Ê)¹„@B`Pã…á‰^áëå/•ÇÑÈãuÁáá ¤.Î7ÞxãÐAJ]t”ÂvÆ\KåkTØ‘éKEœç;ßÉoºé¦\sêá7#5Fß‘ØM·Ôã¹¶Ý å:Ó€¸þ"°;¥ìRŠ:;:H*ìaÈFP°‰_“÷æAa01ý_¢1!0kPƒµ*É}3ùGäq4ì¡Íd¨—k>;tŒR›¤ m¬¯ÂäZh¡œí„ãüÈH,?ÿüósõ…Qr(‘èF`|¢Çc{é¼°;ì…æÌ'aã3ݵ s6o¢¾€ótïÊÿ²Èeœh7M;ì°Cbæ_@¡£ž°Šâ—ºþÑ4ºnly0i$—H”D@Ó*ÿgêúpy»Xª÷³‡î䮿þúÐÃ$)+»cí5Fz0þ8/;î¸cØI¦o†hй7~6“Bç÷æ›o˜hÝ$µxŒÓt×L¡Äï b§îÀ\ãç£|ÐÂÙÐn:wÙe—05àr3ãægCÝo0/µèN×Å ÃÏR˜H 5HTþfþoÐõĉ8 oå«tzvtl²ï_z饡dN|œFs*Þ¢óæÚj^:vÿ'ëïüë_ÿz.ÃÆy< Ä:øžË¯Ó‹fXäË>~Æ7?ûÙÏ6ŒÞË0lFþ`»Ê*«ä_ýêWs-³+°Æ~?nB$Ó±°ø‰O|"×^ ÄÆ°x0Ü‹xÊðX‘ò,•Ú€4%É%†‰€b¡’ÓõºòËã,½OÜ é—Î,éÎ;7ì±.Ì‚aá¸yY³‡œÐ´ïºë®¹ö4È™óŽÝLcúä­Å;¸‹uyÀxõbGÁ»Ûm·]ˆé#­ É·ß~û{ìH`¨íš‚^ÓÖ{Ž>Å=ôÐ\K)C>ù‰ÛMñp8LX¸Z׋ˆÞ è:­0ŒäƒF@¯Âu}€¼ÝH0ÿ¸ã¨ÕãŽ;®è´±Š^c噎GnÐøá‡‡Ñ[œ×v¡Ç…2îa;ã׎„9*þŸÿüça¤›6ʵ©S®CvO=õÔPƱ껟rGsÀûÇsÌ$øÐ&Ýpà átÇ8>Ê'ÌâÿFõíP´…“O>9çÈj»¸nùÙB÷+*ýõ„'BÚÇb³ä †P£³ÊA]Ÿ+cØeãð`?ý‘6VîçœsN¾ð ‡޹[«ÌÇEýñZÌÀ˜º8餓òßþö·“ॣŽó>éÏ1¿‰ó…Á§ýXeUùPÆ,ûÛrË-óã?>ßh£Â³²+´ODø~Μ9A;CÔ^ù×¾öµIO˜®Q™A`áeùå—Ï/¼ðÂbÚ¬]øbu² Ÿv7§ëd`0R˜h 5´x¾iÝÏ•ÇÑ0'ô®ávð?tRqç¬SùŠå~Â#Œž1˜ãz¾Sù–µþgn_g„ô®¹æši\¿¸6müù»ßý.gôüêW¿º ‘£~;Ñ4ªõ¯½Î±b-‹]œ?BÈ Ã}Íwt=¿ðLv€\B )ÔО.æÜn"ÿ„<.–Ê'ž ø7î˜î»ï¾ü³ŸýlÑ ¿îu¯ Ëã„Kñl”¯ÑLÀÐûì²Æi#.áPš ò[˜s²NSÇ ç:è¦À´}ÔYü1Ã/,Ðé\‚ÒËùŒi¯¡ëÎN;í4I³Ô jêb,ˆ²ŒÓé!´´ïÏàÿF5Dbû¦,H]œ0öÈs¬_ÿŸpD(*Ü'—HT@@ ÊóýÏÕõéò8$ð¡Î÷Ç­÷É?ðmlݬ¬ÏGõšk¬îgŽ›õûv#ÒÙšœ„._v-¤Ü[l±–£·N&}œé™.óí‚6 ûï¿A;)kFµ>Æt¡]A˜ö³=÷Ü3×ÁQ ½âR|Pÿ…5ÄüaÑœ®‹A‹Ÿ¥0!è5"¯»}µ®Y‚ƒê|<êgÙXÊRðtÚeç|Ç Cæ™cÆ¿Ç{ä·ÜrËÊúŒŸÌ{”‰Dy°‘Ï ê˜N°&†}ôqý2º¸žòýÕW_o¾ùæE]E#0ȺV5-¦S^Ïw¿ûÝœmµq#POãÁŽ'ÒŠ•J~–„@B`Ôpbc¿etïÂV¹éÑà]Ü _{íµùk¬:$ Àlݯ¬Ô(_Ã`|ù¸üòË @Û™GñÇ,¹°pðÁ‡²´F‡9pêλßýî®ç%ôZÎ ïcËpî¹ç{P/lF¹ž¶ÓF[³0ÆþwÝuWEÜ>‹‡ƒ»@+é)ÉóuŒUxÉ%úB@ §X[«ëõåŸÇŪ¶‰'ú"Y_ùÊWŠN“Q jJer,<ê_«—¡ù›ßüfþØcH¹-èÖ…&ËëÀf¯µÖZ!ŸÛÛUÐÌô XsöÄ=÷ÜSÀSU`*"*wa!à}¾¼è B€®Ó ÀH.!ÐŽ€Ga4£ë/ÉÛÅók~Öxw–lèÃÆ.¢9øxÞÜÏF9dÉšÚŠüàÅ<7ùrgÙxYö“€±°½wãtùƶ$ÞtÉ´õ“ŸNïÆeŽö‡ÕÎ_<Ïîg£²ôÒu:™â°‹…?`h!€ÁË&¢-­„äí¨ÄÌÿ[­Fo¸1Àvû”‰r°Í¦›n:H˜Á¸Í™2×ï9Óüã9ûØ ¹ƒ4# ÁJõtÒ™öÜÒÛPï°Ã Õ%ð¸üYÉ‚Íù#]¬™ß*iaàÕ ìÙà嫱°ã|0Œ5—ñ BÓ©<'—˜½¨1z™ß|º¾ Õ8‘ž1ª¸s§HÇ¥±J&xæÇeû^h†Öxž”‘‘ˆó8ppÇ$Apúô§?ÊÝ»ñ¹ 2d•› ‘fÝZEÌ 9ÇÀšÒ´ñcUlcMÛ(_qÅÎæ¤©âá`.b æÁÊcpJ: #…³53ÿWèúªV{´ê¬u;˜ î1’b½±J%Ìó“ @C¿ë~wÞìO0Bs£ƒ)Ì ©XH:å”Sîñô~Ë¡Ž÷­ÞfÚ×”ðÇËÁFlý ý¤?nG[sý'œpBq8SœÇ U¤Ì§ f,œ$º‚Ó³Bóég)LÌ Tù½Æÿºž+Ê2¿¸c`ßuwº„ãvÌj::–_y¶J¤èüFùš¹O[oo»í¶“æ‹ÍÐì JèSŸúT(ïG?Œ:ð‚¼ аÊ*«äO<á-1š9®7×_}ÁH-d‹²izI%ß³s'Ç*ãÜþ›Gsž¬é¼Vÿü«èJB $7³Pe7ó_C׺é!ŒüãÎý¼í*£…qÚÔGµ%ðâC^Ø ˆC‰pCìÜBúãücì˜c W\Ú[Xyå•*P~íBò®»îòà•%ƒÆ£lzhRlϱóÎ;«a\ÖC¨¯ôÜ­´Ó†A*Øäf0ªäfþkG-^&=nî2nðì &ȃ7õ&tÇ#›ÿû¿ÿ+@‹óXêǰö~°°Új« l  (×'FÎGuTÀãe/{ÙØÙÐf<%À²A4vñ ÀÏZø­ÒZ”î_a˜å:¹„ÀŒ@@•ÚÌ-]{yßИ?Ë>ÿùÏ‡ŽŒÑÿ¸8©R‡øÐßxã‚u ©#sò3&4Ž>Ìc‹ûAxï6¹Ã;ªëa€l!€´Ï9çœ"ïÖž ‹ºÒˆ…ýïÿûœq‹‡Í_X`ƒ ¡¿T~“KŒ7ªÔfþkêÚKaÎüÝ¡s”èf›m:0FÃ4ðRÉi?×0}Þß`ƒ ò{ï8'iHWóÝãS0¦ž ó…^8Çæ¢Ÿòªò®§N;í´€„ëñ0`‰Óæ”Bç˪ußCˆ`å_úÒ—Šé—ù€ñMB€*Mr3 5"3æü‡ÂüãN몫®*:-w¬‚¼x6ê×X5{D¸÷Þ{çL„R§Òžé?®?7ÝtSqõdËC±G1“ºõÖ[Ô£PÖ¦ý%–_~ùÐ~ÆÑ80ÞF˜¥‚÷ßÿ01Ž…€…a"&i"¹ñC@•×KýVÒµGüõ¨yçΛ”¾÷½ïŒ~ª\•`‘~Ùk:¬ç?ÿù!ž¹sçmÑnÒO#¸=úè£ùé§Ÿž¯½öÚ¡ š6„óÓß™†F2X"R ì°Í6Û<Ø}¯lÝæw^ÐîÜrË- ðæ8Õèß„I!¹ñB@•×#ÿ·èúÑТžZ·Íî m¸4®óý*ýб2"ôæ&—_~yÐùlÍ{Œ3Be‚!Y“ÛCÛð•*¸˜†Q(Óóøãçì¿&0Q××q Ý® ™M ì†(pœð‹EB@Ú1 ’}TYÍüß ë Ú˜?*rïfÆ|ÿ Ô¶*¡F;@²æÌ™#xG)¢fà™–ã”1£ÿ&ë“­Õ=ôБFÓ¸ ‰:äC6ÔÑ&£¦Ú¶5¶ï`S0;çÑ÷½O»yÍG¯¯0 ‘Üè"àJªðåòwÈã¬Öš¸kø×ëmŸnæÎTÈ5Êœ¿G*Ç{l@r#”†Kpô¢7Æ=ôP¾Ì2Ë„:佚(s—1›ÿØÎÃ4Œ:û˜®cŽ9&àƒZýÏxÆØµ7–]Zó‚@3Ä}5, 釃ƒŽ.H”ÍZ\9>O~(û¸“¼ù曋ŽÇ¹‰Žzqz—Â-·Ü2ÿÛß*[©Xg·óÒÑ&ë”™?uˇ8Y¨eôÝö ñ¤“N íÕã¸Ê†©6kÚ>üáç>ø`€~å`!àT3ò4_§0!0Ä•R×g‡Ö2Àƒ}â·¹q||0ŽŸ{h#Àå–[.ì±ÇÔqþ[ا &Üé³Wƒ5÷ã(P¾.ÕW_=gù0nåa)ÿó¢)MBr£ƒ€ÚÄÓä­¢ú2DΕvâ®Á߸Ó9ï¼ó æo j!U<› ×Íjõó 7ÜB§Ô`‰ŽNÔ®[¨áWXa…P8j¹îzDœ,Ö\sÍâäºq,WcF)žzê©+4ãv¤°Ë8ÖöÄ+XKÙ8ÍK¨w]iy @$7|\î)ØR¿¸³ùÆ7¾: y8 TÈÌXï‘I2œ¨pMýº~¹nÕ½wD|ˆõõàƒ.æüÇ‘ù»Œ÷§œrJh‡`7nçl¸‰5‰W\q…³9Èe‚Þ=•´×¦×W˜„€Hnx¸*ÜDÞ.®¬~V{è’ðˆ#Ž(:[ñ •+xƒÃ;¬v\S„˜‰ùX`¬þëPecÏÁ4±0±ì²Ëæ?ý)§ÃN8×mßchü Ý”×ql›±Fñ‚ .(Š$Îgñ°™ kWôo¡×W˜„€Hnð¸ò)\JÞ町’êQsÎ$Ç£ÆëÇqé‘J®oAe߬·Þzɰ¡jæŽý«_ýjÀºÊv·ÔKŽŽãa_ ¶øµ¥?Yqº ek ÑÆyá^êú¸îíœâé7oÍ<à2³võv¥ûBz}…aú•ëäAÀ•Ná‚òwÊã\9'îú5óÿÓŸþ”ï¾ûîcß©¨ÀBú ãèlœw¶ A?k¢5–l ër)³æŸyo¦¤âÑ>ñ1Ïÿío;g=;×kßÏ”ÐX’Ÿ#Lèøã/Š)Îgñ°™ ÛW§ºœ’I+ F ›E ®lºö´®”ÍTùV¬î$Y–³õÖ[‡ÎdœÕŠ*©‚Áôs͈ÒFc—\rI@ÇØ4Z³$rcéã¢ÛøteEù0Mãsx¦¶ŠU*œFiGZdNv aœ¿Ï}îscßnìÜï~øáEùÅùl`÷·ie€WrD@;ìH¥ð‹­JîÊØhw§üûßÿ>_ýõC'ÒoÇ,˜J1ÜQüΆIG}tÀ}€O£å<*‘ÿõ¯Í7ÝtÓP_ú±üGEl êÍ^{í•_xá…ù<0)k”×l*3畽+Àlƽýz:ã ƒ*Žjv>'vý7ñÊ€÷ ËdÉ5‹€ê±·ù}_«N3ç߸џ™?*ÓuÖYgFtUí0$Si<`VÉÕƒ€ëK,Á—yú^w´‹ç÷±è÷Zþ˜²Ù0âó_[U²po|ÞãæYd‘@ó8h!ÀöV4þÿni»`€H®~\¹¾Iþ/ò8W‰»~Ýi0‚Úh£Bc‹×æ*§c×i°Rƒ°*´c‘î]½>ÙX5P ³&J༆½×QªßC;uÝu×MÂk63ýI@èÆuûŠ%–X"´y\¥= ó[k† Øîê—‚Ög$£@U†äjD@•ËýûÙÏBŽ…ÉÎ(ÔöÔS°' —´*’«UU '´ª­+]mµ¸="wXûà…;Zå®è8Ê\cÉkKúº:€^èð6¾¨ŠO<ñÄ|ÑE ù(+ØÈl‡v(:vÓ}ï¸ÃæÐŸ%—\2”Íóž÷¼)ëšµQ,s³sÝõ} çEÀ±±m‡£•˶ƒ^Ú^û;n÷´ÉºÒõt‡Ù¹Nù¾¡Ðƒ±í•ÏdÉÕƒ€*¬çý·iU^W¶†êòSk¡™ÛöR?v•«);äéþ7Óä½AªÙÍsĶÜ_uÕUC^<Ÿ?ííÿ[ˆá9ÖàÔá4VöÃŒØL骫® å2Ýü¿ÒãŽ;® ;á_@1í…±úÎw¾ð¶e}{=oêÞShhåêØä :-°„óéûBÛaÑ7¿Ut${@H®ªL¶ø‹®miÖ輿 á:ŸÌ¥Ü„û²!k²ù–¹Go õh l¼½|ç<°5ªÝW¾ò•@-ú{‰'~º-ýà?Ñš‰9öŽ€±»è¢‹B¹ÀºÕ ¯§ŽÚ¹îú>…S#ãÅr:ê¶ës\Ïë¾öˆ饗Î?ö±…tY½áçUÓëÔÖ§F¢–=0»I±=Oy@HûDrý#àÊ£ðò9Þ7î¼½¯çÖ”ƒÐPˆfþ|ÿóŸÿ<ÿøÇ?^4ü²qöú;5oÝë|²ý+q0’ìÆh¦KÃ;“í²Ë.i@µŠ³€0îÝF¤9¢Áyøá‡C’þ¶Jú³ñ[·´}Ûn»mÀ½é•´5´;”1ÆžÞï ]B«F¬í;÷ÜsC±:Ÿ —±§fQÞ’’+‡€*ª_•/Ç@IDATGÿ[ïïFâDš`þ·Ýv[°Ò*asÂ&½GŠ»í¶[±m¯™{xñXß=¨.mXx÷Ýw²³i¸/|ô®'Ÿ|r¨îÄÛËÃeÊTÎå9xŠgFŠÆ•Ö†±ín;îuÞÏ7ß|!~öx`c1 Øtôºìs*z°ñ ã¿øE((ç³áR³†vчÎ ˆäzGÀ•Fáš­ÊÊ“ç™Zê Ü8¬~¥ó­£!z„¬Üç×_?±€á½ï}ohü/}éKídÜü÷ÿw±Œó z0Û8T¡ÅsÑl/‹‹ÓÒO_|ùË_õ"ÞЇúƒ÷è”ípúJ ½<®³´QpÆ>§ŽÑ¸Ë­ShN rq'tRH{ª©ŸNñt{fMÿ{Oç3$ØÌ€ß+úW*m„€´4 ’›W…/‘ÿ<Εjâ®æ_7Šo¼14@,s=ªÅáY™0n€¨ýq?þñC|nüeâíåFô^Útë­·†´On|ÍÉbÄÇ4AÙiïT·Ùf›åiS u¥Ÿ£Ž:*”‰G£.ïØ^$]V‚¸ãÇnw›‚qyT ÑžùôÐûî»/ÐÄþþÄëyüªix¿·¼å-ùï~÷»†óÙ„zz*àÑl"¹éPeñ’¿3ZuÑ•©žªÙ‹‡Úˆºà«Œ†„­ë/½ôÒ*§n±Å!x5€¿©3ô¼ÿyçÒv>}éxœn™gø–­^}õÕÓsº)쎀ËÄÇ×¶OXÓrÆM£½L»ÇœþéãÏ»6Òm/·•ºBÇì±Ç$sÌ1¡MÖ5é)#–3ÿå/{¨Åy-®÷ÂýöÎÂ*MBrS# úç%[¶ê¢-Kë­š­ØÜØ<Å»üµºDqÁ {½†‘²®˜÷Ù‡Ýî²Ë. Ïšý;~«‰~·Ð‰¶è5Ÿñ{Î/»‘%W3tkeâQ µIlEíÃ|\Ë¥–¾ê„€1e/†VX!´×x/®óu\ÓWXóf-tyUB\ª¤çÁ¾ð…"ÛÎkñ Þ OÙ>®híi«`€Hn^T9<ò•®ÿ kLõW~ÓEµâÆÆúb;!±•qèíäÝY­¼òÊÅ9ïq^M¡Μ9sB\U¦˜/õFC¿ùÍÄÌM·tcÒõS¸õuºª c{ÓM7…vÑ´=€µØØÑvößÿ~]Sއ)ÜÚ§µsèñ•äÓð\'—˜„€*†€ K²f¡µ¸»ˆö4ÉMF@•n ªWzoŠÚ¿‹w^oïÆDz;ï¼shȶ̵ Ö<üïÿþoHÚ Þtt ýοøÅ@O•)/mbÕ×§;þNi§g“0V`‡ÑuL]&>äÇõwò×é®N\œË°ë®»†²°Km·]x&]uaà 7¬Ü6M³—7²LðÞ{ï i4\ŸÒT€ÀO® ªùDõïÊNåIÁÛZÞ÷eB7à~ô£Å(Íi›8½„«LüÓ}ã9x §ëN,´ò.?ÆæBtrõM—^·ÿÃ9çœ3ˆÎ¥K®Æû1e²ÝvÛ…20žœaŸÜ`pÛˆû Osu«ÿeŸûÌ ÿ8| çvÌŠòÕ±}¸Êm¶Ù&òÉ'CZ½ôáÅr?ÖæþPø„©ÂäT §¶ê–+K¹ª6ÅW®äÌÇ»ƒucPQ”fz¬½öÚż»;È¡ó&þ:o':YJdÕý¯~õ«€@œþÑl¾ùæÎ*Ñ ,âXsÍ5'LW$”.¦DÀuKÊÚš€4úŸ¶Æþt;Šíd:µÁ:žYسíuÁ鳘Óð²[ß— ­­dÙ!Îõ®1 Ÿ:½5¤KN¨²Yõ¿^«â5fôGü®ä^kk•9¤”õž;‡ ÛøFë´n¾ùæ7soUFÖSÑç|t[ò×¶kàNæûßÿ~ ÕËͦJsªÿÜ‘ýèG? i:þ®Œé”1žüuó~§×÷ŒÕ-·ÜRÔI„UǪA¶Ïñh|ª6Pæ?/^e•UrN!ŹÞp}ùå—‡:Q×É…6>duÎu/ÜÔÿã¾ýEý¯Â§üqÜ,C@• Xƒ*|üò8W’‰»]¹½Ï•_°m¿×0}Ï©µÝÑpq|pˆßZ‚~Ó˜î}k0>ñ‰O”FË´²ªÓ³FÁ÷ý„^²¶ñÆrÝqéüwû\Ü»þt{·îçh©¶Új«P]tQˆ~Ð4Ô§qÏí=œÍ¬ûi½¼káù‡?üá¤27 ì¶I<¦£—8»½cmÿÈÀÚÝ“”&À¬X˜–=PÚmN…ÿL­ÅÒåýµ»üßåÙ7Z'Óè;ƒËÄ03¦2UþìÑGm£ª¿[¼³;ï¼3Óá-Ùꫯž9å)SÞ2©ã32©Ô3Iô™:ðþ˜æmˆe?þxxKó„™VШBÚÓ|:ÏßþN'f;í´S&{…@ÿð×È?G]éæ(CÊ™PkŃ×>™v@ Ï\Ƽ£MWÂ3YsgšãÍ$Te²øQ“† OÃ3Ê‘:¢Í¡Â=tðîù矟í³Ï>™F~“h"nu Ÿ\ó¸mÿä'?ÉV\qÅLZ²ì®»îª=a Ï¡N½ç=ïÉ´EpQ_Ü>IðÐCÍ´…wæþ§ ô‡Ú-Ûa‡2í=i¥@é>¤G:¨¼4°5Uw9B4ð¿M¯ÍTè>èç¿umgkQßתá†xÔ1ûÞ×aŒgµ;Û¶¶;§É™Ü*¯Â€‡ë:½i¨ã(^Ó,&ÐÈ^UVFxtÑ®ÎlÇj˜÷Ô |'Ç5,š¶ofz…ÍY>ô¡ålƳÒJ+åuïç°ÜrËåk¬±Fþ?ÿó?ùg?ûÙp5ÇÅ2ÿ -ÐÔÉQväÃeØéô¬>öÛo¿ÐFêèG:õÖN¢òÇ¹Žº|ÙÍÏÚ!k;ÅÓë3kúY=T͉å y~£âx¶hDà˜ÑRìŒÎØ£°åh@üÏü·ýôÓCžÓ+›^§ï˜Ÿ÷²¡²Vÿð¶çÃK]´„µRx±†úÙÉK›Ôµ¥Úì-é9_qJê¼Ã±Ì>JˆÊßö¶·uÌ#ö E2JÂŽƒ3ÐnpÖ6 Ô'1Ú€S¿Xñ>ß²A ›Á°ÄŒ¹eVa°¡QÎÜwªд馛æl:%%¡¶k ºå?Æ"]÷Ž€ë’¦gB™P7:•M•gÔ /ëå€2œÓ¯ÝN10¦UIÓÚ,VïĈ!ñúÜ <¡¨Ýðƒîón¼Üø#àBVø yoyÕY«ª83:ú 6Ø 4ï¨&$K5Tân˜ìÖ†‹¦ÓälÍ{‡4ê8U°^«þ›:~—<¡ê&]w í4ôzo‹éãŽ;.àeŒÂMƒ?ä¡=-Œ¸PçqÄ9Síy ~ ¬ÀèYjC†Á÷ËØÛã­rOÚÐ-Є`ãï¤~FˆÐÜqŽ*—cm}f€a¸¾úy ûG½KŸw߬RÎíßÚh#b\{]v9z¹(u¢=Ž~ï-Ìyä‘Ó ëûñÀïLÑ™@˜éNuÇsÿG·ê‘+A}Õª“ £#Ꮋ%¬âÍx»Í•¹Q^rÉ%!Fÿu3/;dä×ÄÉ^Îǃ•ç#Ëâ†Ðd!âöÛo¥ã4ê.tÊÜåî¸aúÉŠžy[oƒê¼°/'ôÁ`«Ž¢ï BF‰h ²\7ã´r°CAXEŽeÐŽUüºîŽ€ë¯¦ŒB¡MV±—‰ËÌ×ÔE8©—W|-CÑZÚ*i»½³u9Îy 7õÿxð·–ÒFhd˜¸“2.\…ÿÕ#«‚¢GÕ/]iQŸ)ÛaÔN‡ÉuYïFYn|cŠY¯]×è¹­6úa„‡s^cê¸&Þ,É ¼=½<ói·Ýv+°«ƒFÇA9ÄeÁêì¾ÈöÆI™N%Ïè\ëî´Æ0BM4žÆÜ´¬¶Újù׿þõ`P×™vìŒi §FÀõ|À¸‰©>3cä4M™ï5ÿŸc@  ¸Üû ½„÷íoû ¶ó¶A xp˜læTh3Ω€CÁ*œÓªÀ.üÖm=#žõÖ[/4Šª ŒNU’³Mg§y(w§zà 7„wa2U…ÒŒ½Õé_úÒ—XÎk=È=‹óRçFV/¢Á9§Ríÿª=gåì¢æ½ÓL†ƒ¬[#ã4F-D¸A«Añ´•iäP¡9:Òç5€ìÉñÄ8§k„ÜX­BüÔ󪂭…¦Ê:¥é´k ­ÞUô£˜lÑÊÃäÆªÂÍ[•¦æOÜn Vý›ñÁIÌ´Ÿ{3^Ôâ87ºpÓúqº,ß"îö‘g?éuz×h0:œÓl‘P{@>aÐSõ#/ \vÙe ÆS–þvüQÅúütcþøÙÄô÷ö†€ÆƒŽ=.G ¿ùÍo;XºN•-—Ú+àˆGèzˆ&˜×Ñ×Äe‡°ê8µŸF@ÃiÆÐøS“|_‡=€û¼+¯¼²kº1 ®= €MØ+D²„™âT¨ù³ãß]­ŠâBoÝÖ¸!X®cû]7À£ž0[èÔ9ú™ Y×=Ÿl:¾÷½ï°œ×z›7ÇÕUW…NÅ£ÕËp_&´J³¬‘‘q6µLñøÌtÓhlêÖ¾8þqЈµ—''XÞu—›ç„pÙŽ·qOádÀ‰é-êFûÔKÕúªâøÀ>0å>PÄ´ «@<À~Ú0-d¸Áú`-ÀW”×d 3Å©æxÙßÇC-jhÙŸ+§vd+µ‰G;³oÆå©¦lQítZy™Ô0|vx{çZ&íøwXx3¿ëDGø£æÒa™ôÔѹٖâꫯ”ZИŠlhˆóËù ŸúÔ§Šò¤œ‰·ÊöÅ1Þ³ášÑ%Ë4©«66#ߟûÜçr4*víØûy 'pýõa=¬$©Sø$.3ó©ì~L‡!”%S@Uê²80ȶ`h†—ÍI ãîT˜>éesaOÜÕôëpæ™g†JßÉ*ZxöÜ )ùô>ÎiÄ$›1! °‹iÄj?iv{×#çYæÙs~}0 •ªsèÞGe’½¬76 5wîܰ;žq ’ùí:;\Ç=›B´U`i—¼cgâíÀ>.î“{ ÷ÚF;´«ÏëªC´áWHÔé=EÁÄ•Ëè / t˜—¥ƒvå>½TpNc"ÅZ­8]ô&ÆÝ©zxôh«ª¸k­9nlŠ#ÌÂ-Ìýr]Ö[€èfë ¸Ać Ue’1Í^ºVå°ÓZ&4¶0èò>¦±ßkkHX¢†s1}Æ•gl"Ä>N¦_‡0âøš©t x„I{?#¬³žÔ‘h‚ÙÄûe`Kc4Ê".î“{ªþb›C9SlUG™û@à¹ÖΜòNm&. OUF,Ä'RN—vLGŸ×®¨¼¦©@W§‚÷è]ÿ¥U\À}Ö‹Þ^ÿøÇ?‰%faWªz$„5¹×OOWé÷Úk¯VÜy–MßßÑ!;>TŒ¸AwÀη¶ ù£ƒ«ªj‡ñy9c»‘é9Mæ49ç€ÍyÀ„©ÊvØŒ èàÑô`ùÍt!4B+FXŒ¾è|­¥@`ÁóBZÞá]¾á[þ£îaÃ@ܤAp}THšÐhM–¶œÍ9ŽòÀÅe¤Ÿ¢]6µ¨Gólüƒsi‡ÞÏY­äoªž\èÁйçž’k°²qøÕu€´$ ÆÑ©ð¼®ó­JÚÈèß•ñâ‹/¥+½0+ÕqÒùyÔÝ~Äo+EàÆæt`P”M?þΣí-Ç)òÕ~aŒme\còè%Sl©Ìf=83®Ù4È+x­AØÆ8OuÍÈ FHÙB »}YÝTßWýa¹ay·¶†|l¿ýöaÊçz1q7»ÝÄ˽ò¥jà{Ç¥35Š Àº!îra×K¾Ë° -ÞÒ›v0€H6_O´Î-À¬“bT1Ÿ¡QÉA?>M…ÊX+Š?ÎñÈ#„ãxµL¯ò1¿bná˜Ï¯}ík™,o©€Å.TÈØù¿o|ãÙûÞ÷¾L -¿Söšã`‰ŸÃ„ªõ[–ç|ªsËÖ]wÝL;ë…Ch´ŽÜ¯” ÅÔÂ;yËD8ŽÕÕ('Ûb‹-½FáÙX„û¦Äô2¼3ÂC¨/åÜž®„ÄLÂK8ÂW£ùLBA8BY£­P_$¬„C $P†Ã\q꜋ㄹ–ÐîÁVsîáxiŽg¥NËP²ã!/Úì%$íTxĈ§)Gž¤å´‘†–fÚ^;qLÝÀ©½‡p6ÿHe-mI¦—j=2˜úC£nheN8ÊéM…ù˜í½÷Þ•÷v[åð3­x˜²_œŠžþóÁpWëÝÿT½Bk¬`â¹¾O¯ ˜Õÿgé×ÈèßR7;œ)Ï•%]¨­¾úêÓ¨9m$þ•W^9¤_Õê–<ØÛð¼á,Õ‡›!ü8ýŸþô§Æ:´ä5V/Þ}÷Ýù&›lâg꣎UƳ[HÇ‚½ªwF÷Ô¥”/Ú4 ~r¦-˜aʼnNy¬µlÀ™8‰‹nöžÀ¨‹zÀ<,;´µçÍ´³ \“û0ìé1F¢:9®¨‰®ŃYxá>-º½eÓ^^eï­yb¯œÓëµÿóÔiz/‘2éÓN˜¢âÛé4£èéó™§¶Rzis €§Â²ê¹V¡72ïï Ó.9ëV«®½·Êµï¸©:5ÿǙݤÃfú€ëªÞ ÕÝL,žp~[˜5ðˆ£ÎÑ*ŒZlðã†Ü€}DêÎÆ«-˜¬jH>éØ˜c„q•-Ãé¾C@ÃÀ#õï¹çžùyçlwr䟲À‹NïÕõ¬—ôN‰ŸsÎ9ap4h¤È+ù«K8uÜ”“-Ä9ûÂK¡yØÔ…qñ8ߌÏ:ë¬Ü†ÁU÷ç7æ„Ö¼?®—¾Áq‡võÂeÇÙϵӟ£­¤{M?¼ØÿùÆV¢/iaÔÊx £Wú+®¸"Tj3LáSt~ý\[uO<6HscîToýé v¥ó~Òn×7K.¹dÎ!8§×‰–A?3ö¶v®:ªpþaÈuŽ/µbØi ÿciå~ô£üþûïïã }ÇÄ»<¤.@—Ë!~ÿ²mìî»ï>©-`LHç_§ ®L=O´av£TgMSÓ¡Ëäì³Ï˜°{(ØXÀ5NUBÏÊF&LM—'Óä½<¨T©6FdiŸ%ÑPY[ p£òø,a†´1ªNäÑÿwZÓR\ë¶zàÊÆÈg£6 Ìs÷Â¥èŒú¹6;ÿüón4ݨõÿÞtÈRq?iv{×ÂÌ·¿ýížhéFcSÏ?Ë#eðö¼p·< ë9BL?ž‡åe òˆ5Ɖruþâç£~ Ý®“¦•|ÜsÏ=9»S¾ûÝï.Új\0‰5 UËÇK:‰çÄO,hi§É´ÍäÜ:è €÷–[n™ÿÏÿüO¸ö’ʪX#tyßë\¼ë´Û•¥Å}Ôt{¤ÔPÎæï­I £êTØý/[CÁwÂJÝóZ¨1{™ç5a¨‰Ý¸-•«lŠŽ¶ÌµGÿK-µT±ùНӅÐeà“™~‰UÏeò^ç7t’09Vˆ›©Œ÷¼•²q$/£ˆ±éë7$/.‹°Ì.’ýèG‹úÉTU؃µ éxó vZLÓL é°!S4‹lßív]ÎÄáéÊc=6À8]ýõÿl¨µÄKÚª$h4œ' ÒN£ærMZøX8¼Gÿg¶*¥·Úê„+ª§ÿüÏÿ¬\‘aZ¶°Eót–ÿGúVÁi¼®NÔ’õ¨Íýw*@—û„ƒCZâ+ã)O ãû!7ä…24ýò6Sž‘Çö|²¡T|r"XYqxÓ&¶zÆ@ç63Spí”ãÌN}6–£sú¥W UÝÔmÂÌ5|¯õ¸ ØX‹xª¶W„Gâé×fªvÓ<3IZ>’Nhæÿº¶Õ¿ÃiÊ·÷¿]‰O9å”PùÌ0J)†Q¦»¡{kZzÊÒàï¼u(ñ¢å{)¬Òÿßÿûÿ:ŒK¯!Fiñª„/ù˹ÖLO"ÛõgÒÃYrÓžw 9cAìê2ÀŒÛå5×\¦¾¸ÎÌDÈo¼L–¹v„"ÚGS.Äë~‡-ÈqN¿¶Æžå¥ØЮª´WhðT›võBC7Ú¦yn-)Ø8T’T Vÿ×*ÌÚ×ý»c°¥<_E¢6Ã%.9Væ ü?kÿ½ÛÒ¸i*ºAkS¡ît zâ†ðÀ4rJùÆTøÇ£N³›;wneær+Îâ Ê,ƃQúÁ\´)°ì´B¿õÚš5¾cÉ¢]œ¶ŸÍ„Ðù²F 5½û'–jz¹¦—øö‹gûûÞ­Thõ ŸÛ«øvŸÓw¯÷¦•X ; ˆ6l¿á:¹!# Âðèÿµº~¬Ujý»uÔQ¡³ò<˜²_t^ý\{”ÒºÝ Èk|¡¡ŠE­éeNÍñô*Œ´pzàr±F$fÆÎ_S¡58Ä¿Í6Û„õñºL›Ÿ¥ð)ÚñÁžƒõÝ.«ªí‹xaÚÃí fj¹0ÿ¯2†äÛÓ*ÔS¶QöJ!?7ÖeB ì¾ÿýâê½<ÌÄËÐÀ7nƒ×^{m Á}d¸©ïÇÓ+Í´FÅ©Œ½QÃÄöT ìúçÛþ`õ_eÞÝÒ;ÞñŽžçÐâºÌN\¿°ຊwg‹•.ÎùÓÕkÓŠV„y_p¨º*c:,1`² ÁÒAæ5½1 ô˜¦QÅl”èŠñÂvŽÖw¿ùÍoåH½¬jàJY™Ih{í"ë3©ŒÌôØ4Šº S6³÷´Ê¾ûî[¬ZеÓÕõnÿ#D¸ °”g: ;\øÔöĽ‚ÝÒšê¹óÇ>Ž»C²u<ò reÑ“´€0l§RõœÌKtíÅÔ.¨: =Äá΢®#iÝpX/ë¥âš,imèTGCF±ÑšU×N+7?ÆÏªÅªkU¯CçÔ)Œç-?ó™Ï„uï†Ètø>…½#cGÿ¾P”ÔNåÑË3˜¡÷ ° eãVÏ»¡iìØœ<¼¬˜kï1²õÖ[‡íœyÛªô‚_·wÌ|ÙĪgÜÙI’¸ãeœÝÒšê¹§Ø—g<ú¡©‡w­øŽhAšg®“*þwo`csÿýÃ,«ŒþÍlYžã%anSUBWêø„­:TyVÁõ3—7ÃúÏøøÄÀº,ËUµ FdÍû\rÉ%EVvñ ]”F Æ’¥ƒLÀÔ¬vŽË¤×kÚÊ /âòé–ÙKÛ+™~n,%÷1\{s«E]4¿è¢‹ò•VZ)¼ãç½â×é=âð轟åxÆÜ;z¢­b¤è}@˜æèg)uŸÅã%áÒÂ# €0,§BðèÿYºö© 6Öè³l»¿îÊêÑUiÕsÿ—^ziH4îðºSñÔ?ûï¿hÀulýKãõh á9´§2ÐÀeärb®Wu2ì¼W‡€D\x«¢9,ÈG’’§Û@¶fm”qy¢ pÇ80^âæ²é'\d‘EByy䑾ã\†¦ƒ›ŒC» dí;%Z@®ªUqZÖ6°/ ®×þÌtõ«_ tW¥ÇZ¯ê‡Žðrï?`«ü§i@–S™ÙòýVù5Æü=ú¯:÷oõ±Žù-æŒ{©{n,lì{{#W9@¯×)`ÀÖ äÜK+½c|¾ÿýï PMzdÒ+Ó½gëi&Ö²ãzíì*enãûÿ÷Eývg?]™uûßBÌÇÎuÈ÷ã#V:_r»àë)G˜´·¯ªÉ4¶îÓvÚi§¾„aã;ÄEf»ÇÝOh-À¶ÛnÛWßÚg9[ ð¾{•èCQ®“ ÞÖÿ¢gž{ަÏ2íþº+i]£w\XñãÜx»S0ñßC…'ˆƒñM{#çy?žï­˜ÓüÁÓe±ôÿ.#òó¯C½Ù KwvüGÇ…sÙ”Î@úpJ\¾¼tÇwä΂¿GžÊ©—g¶£ñq×ħÅý89ŒüÈ·×ÆÇXkÂr9òø±}¬ë»ñw½\ÓÖÜyß‹^qô{ì(HZuiúí_û,gköÍé¨`€¤èfþKêÚ#Kg}–gç×]9=O… ¸ÊÜ¿%T–:•múÓŸ®­ázDûÖ·¾µéC5:\ÃS3_¦STÿ‚õkȉ«›÷Ðf›mŽÆ%®+5d)EÑ—5[ü~ò“Ÿ åƒ[VÓ²qÚi§…TÇ­Mo¼7I'á×¶+›o¾yÈ'jrê7 ×Ì»[}ïå¹qDKƒsY…›)~LÝZ€*}ìäú/ó››õàÙÂ'iaNÀ[ýÿ¥V©X*s!U]9ë–N}bY¿„¹P7´:¬ÿ­ðÈù­ Ü€"0~Þ™ŽŒåOª‡{«T8à€"·ã†_Aø]¸ÌÁú„NåŒÆ§l¹£rö¨Ó›9q€Å´r¸õÞ½ö6À¹ <ãð,œÍàó¼$¹ý›~î=¸Ùm·Ýú†Ýnê° py^}õÕ¡QÍåi!`Cá”laPNiã¿ëú¾VÁ62úgƒ /£é$Y+Ï=1ϵ#³f?ÎØ[|ÒÈ«JíXܺSÀ¾ç†ØmÃzטX;C9xIR¯eRõ½vò8á7¬r«#ÝgÈEYZÍÝo¹²O€™àu×]HtýªƒÞ¦â0l…½Új«…~¨›öËù[o½õŠ•G6–µ0Û/nñûqßè©1Ó7]þýžÛrUM«£™æhÐyºù - ˆA8í¥Ûµ Ø…Q[y»@5¨òÈ#ùrË-*¤µÊ_ÑÙôzmé•gØãœF‹þ®ßƒ¯‡öÜ}¯é·¿ $hp 4®yªò‡ñ`×½øÃ¡,Ì„Ûó9ˆ{Îð¨«×Ó«ä?};×[ï€G™—-XsÆÂ%ÎñONuøw¦‹å¨žßŸêLO‘lµÕV“,ä½¼²¬à·1rìu¿Îù©kw@×ΘÀ¹ßè—®iÞ÷´ó¡Â M:lã¿e¢Â±4=*éÊèc+«2Ï.]8Çß …~—®„k˜ßªº¼Í]¯GyöBë ÞqC¶]†Õð`3,ïùO¦ ¬þt¹ “Ùž†±¾ë®»Š3âÍú­n«{íµWÑN]çFç˜ï§?]~]G÷Øc/Çá~…­’«ö+˜,¶Øb…åtúÁîŸøDhËÅ÷[†¼±¶£§8mð+åo¥‹–DSNÛøïV¥²Öº­'`ž+o壴ßÚXy6Œøpý4 ¿ëƒn¦Ró‘^/ÞÓ'žxbßô„†ôãŽ>^ ÙK~ñŽU–ØxüùÏ+\‡Tœµ&ëºqÏ=÷ä^ã?SìV/¼iÎ(¶÷€wòÉ'‡ön¡¥[~xî~£=Oh$×Zk­5YSÅ3Õî[ú5t&?.Ã+¯¼2ÐÍh+§Jsªÿœg ¸»ð žkŸß#ZÒ’@€h \«þŸ¯ë¹ò8Kaw] ½§<’q•JèÅ2®L%D±ÅnÕFŠdìÑjSœónFôÇ4²\u+g´aM÷e|•r픞©<ðÀÅ2å]|œ.úBÀu„µè6µZ¿SyMõÌL•m·qŽ»/‚j~9®KgŸ}v¨óž½ÔckÊæDû}8¾o}ë[!.çy*\¦ûχ.qb*Îiô'n±Å&â§K·Óÿ^ŠmB™ÁW4[8W4$;@hÊ©@<úß°U8µ2ÿ¸À½©F•¹1,í½}l5”; Y„餾¸/ãÝ >øÁ–Þ‹ Æi×îD8ío5Öù®¢743ŒÜI”Á²Ó7îh¿ñohLû pJi<Ũ1äsùxnÚ÷½„± íÍm܇sœöé§Ÿò†!ol}ß-_ý„âÃrçwÞâ£o¨²?é³¢‚ðï|gXjØ/V¦éüóÏñ ”ô"àtÊ;ßY¨éw›â>èöôó“úf1Ñ‘„@h `Ïÿ·U@–¾ú(¯î¯º³örŒ÷ª43)æéÊ87†ï}ï{¡1ØÊV؆û2¡G©Œ pN£ }ƒúÆåÂ.ä¹*¶ˆ6~U <‡Ã¸Óaª7BgÈñŽ ½ ×åÔKè)„xkY×ÅAA§‡½Ž­ä¡­WáÕsó‹/¾xÎ&J¸8^ðÚqÇCÛŠWCô‚Qû;ô—Ö¾Üxã!-—G¸™æÇtAçK,hò4j{Z½Ü{ÆÁký.¿ž†ÔøoOCï'šÒ4 Ôí„¶™ÿkuýX }K_qa”¾våcô&ú é‘ë~=óaV‡ÅRw¿ÄAÓÎ;ïÒ/Ó‰ÅtÇ£«9ÏýÒ5¨÷ÝyÄ{ Äyê÷Ú%—\2?ï¼órBâðÈ¥ßø:½@á鉛of³°$ ª¾8׸¢Œ°‚w™t*³nÏlÀ|;níÅy ]ú•W^9ä…iÅ~ ö\çÙ/Ž“x}oƒgeGÜÆÐz£gh2Æ6ôõ\¾ãï'$/þžå¢8ç9ÜÔócM4kÓ΀* Ú€õÚÿZeÖÈèŸåvË.»lhhe: e<|ë¥l¼Á®[ý:7–ùX“PE†.¯ýÇÊvœ1¸ï¾û ‹ÞX5k¬{ ÁÏ#wìËÀ÷Vö×tïy„ú™õÖ¸:žq(Æ¡Ñh¼}FŒ³_棵&àúë¯yq¼MeÌõžø©;ŸùÌgB¥Î™™MWÿâÿÍÙ-Óï´¦ÛJ8ŽoºkTÐ*8þp?¦Ïö>´Ù*[°»ÿ,«‰í‘lFß)|Ф%Q— 6œÓ*ZW:ÎxÍ!ôÛYð½UíÌ×áúm¦Ç#ߪ’9y1Mã¶å©—:Y£bŒû ÍäÛ÷+?þøãC¹y´×o¼ÝÞ÷ʶ_õrK—k«§ aÜî¾ùÍo†263ìVfž[h\}õÕ?ûÁõºA›„¯å÷³^ÂX%?6Êí¬ŒÍDL‹*+줊s9ôSÜ|ó‘|$``:N§×k0ðT@[¬iö4ÀÑ¢+ €P—SXý¿ˆ®½®%®˧·×öÜsÏPáh”œ—Ÿüdâ §ÙNKºoã}饗†ºU†Éº^ÔµŒÖ}ƒO5DeïºÚëP¿÷¶Aá1œÓê„®±± TÕih«ìËλ›&÷´å*Æ€î öÛo¿NÔñÌ|‰-__®òBH[DY'­þÿT«„,eµn«®dÞùFƒ´(zKywòH­¸©Ýt”Ûú·ª4n{†q±þç Ÿm¶Ù&àïQW™ò°ÝÀ)§œ vYO‡»5þ¾LÚ¾A°ôÔRÝjäéò”þŸ@ÀíÑ£Ê~µ}^„ð€Êç8'Rèý×õÑ#TûêQ/Ï`”žÊêeÇOç-™¥½¤×þÌßÂRÕý0жx*´Êà,ÖˆxeŽóÝ{©Mû¦©ï&q\äÏZÐ{žeÚ’èç…xƒ ‘YŠùÓè¼ãÖ5×\’w#ï‡Þ¥Ò³›´ô«ªŒé§!š‘¡QÀ•¥)|Üà隣íJÉC¿sœo—{(´ÏûwË‚;°ß~ûí VÆqW¹¶@CGö›ßü&â|w£+=¯—1óÀf*.“^ËÕšƒ£>:æ8û¡ÒeÎ ’¤‹Ê¼.æO|¶CbGÓØAëtôî½÷Þ&zÅ¥ý=/Ÿ<ì°Ãbúº6­ñÄoc¾ö´z½wŸRv‚ˆ÷õTÑ”¦¡¬ضþoDýïÊU×®SfÔ«­¶ZqìofÒ+¦ÉÛÿÒ)T9«;–â”z'å¡ÌóÍ> +¬°BÑ)ªî„ë~Bl&<Òö* w¸ÓÑæ÷°Ä¶bÕŽ°v«f™rò4‘ÓŽ¾ôuŒµw DåÞj ¡ëkÕq®¿½PçwÙŸÞG|;¾öºRöÞs9‰{Áä¬õ·3 ¾'4.ÖH0p°¿ ­¯¹æš¥wá3M·Ýv[èªnW쥔íÇ"Ç8T¼ö4ÀïÏ‚Â-MB'­þÿx«P,]U,£‰Ï]¹¼Ö鲊úß#Ö•ã:5²‰”»ÿš&7B«Ñ„_ߌo¼ Û™:îî©ïÓvÆg„|ZR/“ok<ÊÎÓš3:Ó:· &Oɰì”)\™ú2¼ï”]Æ>Q¯ßúf Sû»T\ÎÖ:ö›v¯m"þýÍÿþïÿÌØt˜fß3PðûâðÓ…Øøû2‚’é"¤ø€ /mœ.ýNÿ30ðÔBÎu!ÜÔóc-õz¢ m elÄúßeí¹vÏ}‰Î¢ôz«ÿÙ~W¥b!µ“všøÞÌл€¹‘;&–üp–8tÛ˜²Wüýží%vÙe—Jø»ì¼6†]e4dúâÐÂ]¬"5£P.3™ãÌq¸ë¯¿~¨sf qu»¦.XèÇžÃõʪÛ uK§ês0}u̓›vØ!§­áŒC\ÖÐè)0 ÊÐF®ãUV™Æ:äÃÂwƒ{x úU¥—@èשBZý¿˜®½ƒ…Õ+q}-uíJ…Ñ‹çª,­ŠÖ¾«ÿ™·§S©â8´‚]Ç Ãj´24!íÚ¢˜%n8w@Uè«û[Óäe˜n ýæ™üzÙK(qŽ» Í®#'tR( /åë—®©Þwœäç4ËЛ¾é× [™Óô#äY8/³ßÇ>ûìê”㘪ŽÔõý›×Ô3šætM\\猉w!ô¢, žî:æ˜cæI+<èáÇ4y5¾§øÊÐå>•)8/çìŒ~^±à.}ô<ј¦¡'à¬þߥ…¼­+û)ˆ®ïºRy§)Ôpý4~åe’`I¾ìÆê†è=°i°UÖ£[(aó‹†*zW|{ýÃyÆ¢Ú£…²£ 'žxbHÞq÷JKû{ñ÷l D™×­®EsdCÃ*£¤vÚÓ}o¸Œ½òÃí¸½}wº§¿ðà¡—“7C•ÝíA'Ê<[d‘EB]>è ƒæÉý¢mÐT±A²aãFmTLuÍ“hØO`³Í6 ´;Þ2ùs ^×]w]HÝùî‘”é^c êÁê;Dãеã¸!Á?Nnµ‰ Þ_©ÃB„sæÌ ¡FíR©DˆKAøö-oyKËÄåo4âôœiî«M|$I9|»Î:ëd&Jç¯4=|èiåV\#ˇ[ô .PƒŒß)êžÑÿúò?’)âg¥¯U÷XÿÛàÇ›fˆ–¾UíéÙ"µ`7­âºâŠ+ µ›dßô8¶ÞÕåÎ/F8ÐŒšQ °ïüZ/æ_ìø× ã:ž»þp¨tۊ߸×zꆭ›{ÝÅ°Ž¼Íæ8\®ì(›™P¶V“÷R¦.³ï}ï{F×ocêømÉ.æSÉÀ·š¦{ÇÓžLaJˆ ¤šNnÈÇ>ô¡€…}·M§ouû*«¬RIÝnL9Kƒ¸Á¼LŸaº\¾¬ˆp;syÕz9àÐÉCÀÆ#~æIž/ÿ!Êuòß‘·Ñâ1]ëè_i)Ê,ÓÚÝ ƒÔÿUFÚH¤H“¨ÿeâ®"IbtƒCâVc ×ýþhn¬È#JœóÝo\M½oŒ0þ³+C£öfŸo¹å–ù.‹™iè5DM¯ã}³Ûo¿=C-Y§cÄ%Á"“•v¦=ëCÔàUŸ:éšÉq/u‡ú´ë®»†¬ŠAô<*t»•APó{4ÝŽuWŸ·=β÷ÐÀŸ©ˆ»ï¾;Dã:FHd›žƒEYGÿŠ–î / }.ñ8~âtŸAÛ@}_U3æ—~V›AeÚ14R†®ÿÏÞ›@]{T…šç:\½wÙ·{­¾íµÛ~n« È ÂOH˜I„ CBˆ&B BI…H@ B˜‚ Q™mEÁ^ÊW»z]ÛVßÛžÞÏþ¾çM}oÎ;¿ç|ß?ÔZç¼SÕ®]»víÚµkWUK¶*{±86àþDàÏèsÖþ¬%ïŸö„…×ÔOÇÿ¯ãw~`ùíø]¿#âG§÷$¶ÕÙ‰eÃtÞ æ <"«qA3žö˜ƒÇÂ/çaα¦Ž}æ÷½ï}«ÎÉÆ3æœé ø0þú×3Ó³HÓéÐ<˜Ú@p~úé‹8}1“KÇ¡°úÆoë86TY\xá…¹zÿ…9Š‚îüóÏOì2ï9ó: k‹¶‘<à‹pKÞrz¯‹FL_á#rýõ×/ð²'¬RFáÙ8š:}Žæœ>êÂoÕwøXoøú Hw”ÊË*Xmï Ñaœ.~[ºú7ë'œ''žxb~f66P&Wô0 $ŒÁ«Gþ8÷l ¨ÅbWúâ]ÉTÂQ+羸ÿÑøýr|cÄôß?:}:ðTkŠÛyƒ•ûOÿôO‹øÃ œ9¹±†”)šÇ˜4—¥€ãØàÜV쩟š7pÄs,Ì9ÓI£80iç³§f¨&åqîÿ™Ï|f ²UwN¼…EÞä…@|ñ‹_¼8å”SÒqLab¼©W–E¢X`݈ãb§‚;œ¾ƒÖ+í'¶ÎÎØááß‘jës˜§+ØÏ!_–mÎ{, ûöíËïc;ÔL¼æ?ñE®¡\3Bf`1&ÐÞ‘m¬®áçÀ?`¡äã°Ip6ÄŽ¨úAàx-ì¦=0€ÕÑãÁÛ x·ñ°+ @sÿGeëÜ÷„(ù—ãwUü°›B:~:ýµãH]}ñ‹_L“}]óÍ=ÿhxŸcFSÃØâ…gnlš¹O1ê¾ÿþlh›ê{’-0eŽùÒL‚“4è !ý½ýc¢ŒU2ÙÚ÷ͯ!]¡õUW]µ_„’q [=êèghˆ‰ÑÓcó˜äYʸ×êst÷`ByèØc]ÐùÓ¾ûv|¬Ü Äf` Ú2°J¾æž·—ι»ExXK£2C\¤J6´ 8Ýfœ!WcIr:Ð  âJOcrrŠ"…å†À€ÐióÈÓÿìðU´pO‡<ÂÚ;×:.ADÍýl‹Èi~ŸŒ8ì–ÁÚ;~ÌüÃM¦c³Í}lpÈÙ<†À”á˜ÓÂ|ÈHr¬bB£v4Ir¯Ëʈ‚ùmh¨>WÌ€„3Ï<3½y;†öCò¬ÇU €Î6K¦ŽJÊ|X®$/sÌ1 <–Í·Œwø~ ÀCðœ’@û*u´»}ûöåfN«LÊòþ”ÕBó”r ò3ö/I+oË6„¢ ¯wÜq™ ïÒÈ-è;ÿ•IXµ´ìŒÑïIüæ°L#x¡L {]a=õè3–ýÛýîøÇeó~"щíÔ.~Œú™çÿWñ{uÀüZü?z[Füíø#¿ 0Ì÷Ñ~4Ÿ™ƒžtŒ‰­vS ²NWu„Œ8ÆŽòèPé\ïyÏ{N²JL*HKbi¤ÆÚ÷¡ÖÙ0ͱ›ÁΘùú/}éK‰ s»Sæ&ëåa4‰…p÷»ß=-æ[{øy:äQG¾(`}F™´Y;·?üÃ?LD¨'ƒ÷qàUúŽ`Ÿ{Úȼú\UTÙ9ÓÎݶLï@‚ÎÒ2”ßûÜ“¹†# þIéÜ'½qÄË 1aJ[£Î”µ*mæaž¯XèXWzÿmX·3Åö‹u_6’aT(®ç{&i˜ç¿ ~äOç¿Säs‡ ³Ñ9²m'£«±&±1è(cCa Gœ‚—æJæíœ#Ÿ™¡‡oG|Ê Ü4eÌèŸÑ4zùË_^mž³›e´L8]~âŸX°©¦ºë@IDAT£Æ9q¼¬ fo, ä«ðÚAäÓ(`{D©;묳’Öv]€T¼÷½ï]hö.å‚÷ú`‚ŸÒuáÓö]_!ø‰Pç%ù— ƒØÙ©Õ³ îªo(ùnt5ÅØÐ:zØÃ–Yõ­›UxñN?°üã9±ÜMñG¼×̼µQLZFÀ˜”dí @T &ÌýŒú¯loŽßÏÆs?ÐÚœûvg€iÌéè´5Kå‹l8ƒÙˆ€Ù0…q98‚œâhcÀlǨÅr'‚»ü'.ßÿþ÷s è($û¢FÃWÉq‰R]põ…5g<ëkÐu×]—|¦_È\ù@7üMPY€eà°0uw§h?n:ÓwI ÖdËW¿úÕ¬' Ê÷ÜÃ'ùI|‡<3Qñçú`æ ™N˜0¯±U&pÇZ&ÀC%bŽM‰¦–©žÞ¯¥ó§uSÛô첬³Ï>;ç-I¯@hJ³©÷àayÎ;ï¼\"ÈŠL¨s¬XÀdõÁe—]VyU›÷œyª°ä)”{¶ø%ôíü4)ß|óÍ&eëêŒ3ÎX\{íµ©,ÒÑ:ª›îÀÆ|ïÆgX“0yßínwkíŒÅS+'p¤ÍPÚÄ‘uŠóµùc•Á׉ÐG9kÃסûÃ(«ÚÒ üF'Dç¿¥QnÐù} žý¢c0²‡y0ùß?Î{*³%>ž$Åö±nßKÆübd—éâ0„mC nº9¶·Œ9ÿÄéáø2L‘CQÙHüèô—û÷ïO•uJW/~Îh#ÒŒ#ÊÌU½”ï̃ºËØ2–ÜõÊ«‰7b0°d ^NïP,vÀ‹Ã·zÑEÃêV¥·lMy7½·LGuÔ2|éÒ1ñzë[ßšxž¯)ÿ¶÷Ê¢p*^"£æÑŸqÜø%ýæôQ#€v#òvþ?÷ߨ.<Ü’|Û/öÊņZÉ$4° ÛèQŒ°3½QÌábq‹ùÀɸÅè8aÄV¦ëÚÛzt•ZÎo}ë[‰ã%,œ¥2m˜þ—1ÊJ\fn¤£ËWOhyã ôe˜zï©BjÏÆ- AhØ«4¿å*9š6ýûv~ʘ®‚y@“˜fX†E ó!¯±W÷«ê¾|£âŒË1¹(†tþåwîc‡ÃeLM-ÃJ8¸ P„KÂt Q‡ßõÌ K«€q€&´<—t"¡í+¶Oœ¤wMßWáfˆûàÀ߸ ‘|-–yá—×Y2 „Ë%~'D¬{r]ÿž˜ë/ í=æœÀ=7Rá›?ø}è¯]œE˜d -a•i,˜6çÖÈ»)€Áeq˜ÅǽþYg >mùŽÍcj:vþ#àXä’©¾05Á>éIOZ¸Ss`_›ŠG½RÿìñŽw¼cÁö²LÏ̽싵Ë!ürçº|àYóý“á'¤5üÁè0Þ¹nÖbã›ÌJ qâí‹ós÷¬ÕF@̸ªÑü|çg€ùbdœcÏžùé”hÙÍg7¯à>αQ 1¡ÜHÅ2ïfÙšò7ê›ú`ééƒô t`dž—£ç øày~ê©§¦€u§¹¹àÊpàYꑳpØÃÎ9þ6ºèð†ÌÁѵËOH^!»>âÃC‡ Ï#ø5Ú>x±“›±7²hß¾}™oÙF(¡”?Mp}¼c@K` á®‡Æés%oùžƒ®X*‰\6N|ÈQŽaf“/” æöyÇXd+q)e§áè£ÎÁéÛèÔ…£ýJÛ 'œPåÑ•nÀw…þ¯-~.ÒõðÿÄ=+èš+x@MQ'uР?Ë "“ߨÎH/ÿ¦|wí}à™W˜†‘'›P åYÑc‘C3§¡²ÙG¹ KlpNaÃËB³g$XW è`dëº C½â-°ÃÜœà@ÒÈ3Ášÿ(›#¢¡–KtÜwa¯•où¨_ê™Nÿ³Ÿýl e„ m®‘üƒ@¦Þñ,’T6ä¯Ux~×ÈøLGNF¬ÈMPtäÅ¶í¨»g宎²éÐÚÒ?mC¥äôÓO¯ö‹7ø€œ.XÄ3>x(/Aëe×ü0àŸÎž½÷‡”!<õqZDAžî  À]îr—T€;E¶;(ùÜç>—–躆€F¿yrü¾üurÔÉßÄuíJÀ¨²b˜ý“âú’ø¶<ë|Ú×`Üjn¨ŽŽsì}y‚XTTë/Æ’yÝæ­N;Ñp–q`Ì2¶¾]2gÅ\4¢ ÅèØª|úÎ1Öq+X¢$l豂x0Ç ÞÐ-WUæzYV=;ïÈ|z×½P¬A8Xß±jUî0-W÷«Ê<ô]˜€—8‘.–Z%~æ;ÙÑ+ È»±_EΡC[ÛZ[ý„PÏóê‰V€Þua}1GNÚèè:–ma¥XÆà$çË…S¤å¦Inòx±lw+”–¯{Ýë'ò `ïâ#ûe È–a½Èrâ°ˆò’9}|¢“ÏxM´ŽcØ—aL\äý¦¸]ïIoÝÆÔZRÌúo!ߨO ±ï€<)0®“êQ¾Æ0 p TŽü_Ð_?+~K=lÌr3Ç飑–.ÚsaÑéäÒ?–ؾkÈç ÑÈrTJ¾hÉš¨°pÏ£^¬,óc.ÎÀc˜ïw¿ûå7Þ3*Ô`¼¾W´U´_Lbj®àµu.Žà£QW›(õÅùMê4V8tšSûÂÜdV@ðS¶RNhUüÁ“©52w¬õTôIu‘ó U¡€d>]´4ýÀ+«0ûÿ·ñûý(Ïc"ŸÏÅu-–€Q @ öCCÕƽÜæÖ¾\w-À*Ç b‡©?ÿó?ÏFzà 7,ÜôG$a$:í9ƒ ¸šý€ïÉR00LK§Œã÷0ØÛßþöüWç:áñ~H Q`d~œ†}¤Ë8ëˆ+*7”Ñú뛟&Q¶$%ÚH|Á“å7¾ñÜ̈ÎsæÔÝѤ?>8YqÄÉk("½,Ón_áUênÿþý‹«¯¾:íúÔ—å¾ð…Åžð„^¹e¥¾äÛÕVh´}øJe@8åU8ÊL®¼ƒWp&eƒËŸôtú NPz˜¶2] {]÷П;;‡ÒfÀ“?òEÂïSñ@"à“õЇ>tÝ2†~™ ç¦ÏD]<.ÊõѸή VD"®gr¯ßV}nÜïZPÁœm8B¦åN¢ 8ÒÐñâ!Jcœ‹I„ßç ¾0?˜¼Á çœ[Ø7þk_ûZ6Ü),4ᄨ·=¥@çÿuJD{ü¡¸)Lqv:ƒJŠ ôˆ5ÑyT_ç²>eG ëcÀn‰wæom;}`Žs;lKå&=´cÞ·æ» ì.íáã¾øø¥+¨cí!”õ,Žà«ÌDÎ0ÂÇBÄÆh87Úñ‘…~¤ò£ Z„啸»À¼¤ñ\x¸59~l·Ý¦PÍ”'LÀõ¦(×¢ž>×Y•€A €™ÇõÄ@êmñ#ÀíÝܘQçý <’ùh 6ú`^4lÃÂ᎑81Kbæñ›ÏØ+e?,üPè¼§024±3õ°BÉ-€‡–ú`=`ºDg,…Ù^,kNÔ|ÀáAx‰³:€g¬8*:]0º¾3RCÆ”yòÉ'/pFc9[Ù9tÁ8ü}‹òšNp˜žû´YzøžAÞíÔ³ðÚè«Ð'®2ÑU2äa=›\¾ÓŸ¬Zºé¦›Ò¿ÌŸv… O¦’úX8Êô˽ƒD•Ðe¨oM A©Üù­E è…_džjj\ˆßÿ?‚›l=mð?:ð*7œ¿Â¬¿|Ñ‹^„2RýbÞ&wºŠFÉ¡Õû2ÎÁ|O¹-Ÿ›|„¨è¶›7âÊÎ2öLHW…ˆ÷Ô§>uÏmp4…¶Ò†]Ù(_˜î[IûЪ‡¶Á»8³bÂ=Ñ-ÛÔüµ´¡¸.ÙezâW§uý9Füé Ìû/ù˽hoÝ çHdžcÑÁ7æ…¼ÛŽrÄÅQ¯b ´¼ñÆ³í”ø…Õ wÄÉðP•›%=¼‡Ö8!òüÇüǽê¬Nó Ïe?ûØÀåcÐà4«B/ ‘™sþÿ&€|8~ÌMhžXwmïÔ`ÑnÑLq¾¹öÚkïz×»2OLT8†1„9‹ø‡jÀ†Q5vìµ%€ÁS9âÁbü£P¦=†¬ŒL0›1£Éc7â2:ƒ>kʾì¬ÿf”Éh­ô'™‚tÇ1”¹]Öˆ³þz¯ùˆL)ß&Ó"‹XŠF Íu¬Zä†Zvà ‚#ø¦¼hZÿ܇¹z6³ÎM‹Ìä‡å‘8|ã÷Ã×­é¬;dΆCi øpðÀâþgq ìkºßâºÅâíQðŸ^ŠiØ!d|‚óþi§–tþ¡íf'BƒÂ¤…È¡ÜùS/ #½€7UWCòÁœFÀ¡hh})D=ýohú!xîVÜOŒp=Œ t€FòõÓžö´\J‡ýô%S¯xX̰.Äæ)¹Æ›|÷¯ô*Ä.GÂi—€B+o·¡¤@}2%Ð7¨ÈMéø®ÜD @^ÒN8wYªâÝ”~/¼ïCÇM⩌b%€S7Cên&\|£ü÷ñûHÔó¿ :á×Ú—7åÝ8’€·Ò*°šqcš¦LƼGÁÀ\c·¾ÅI'”`Ð`aèrYßøs¦Qip̳Óȸ'Àühl˜Öø•Ïëdp>ó{{-H3W(ûâ‰EõòxQ€y°éÄõÌ3ÏLŸŽsÏ=7•–ñÍàOÚ1Ç“ea° ΑÇÁ“:3;[¶½¦rkjg3 èÜwjG€4]ünFª{I^6Ñ„÷ò;>AÐTYjYy¦#æ§L…y†æÆoËcì7ü©ÜKãºë®[<ûÙÏέ™ÍSÇÂŽ>ø?Åï~ñ».~§Æ/ÐÈ>{ž‘pËŽ>®9‚ço=­é?˜¶‚N|ËóÏ?Ÿå6±ë86Øsü¢“‡m‚CȦ§.·1wعµçôÃû8Qu}W=ÐîÃôŸûX„ÒºŒÓ.—îöò—¿|KgwàE žßÔg,Rœ¼àËP MëjÎkxç8þTà‚òõC\G…Hlçÿàmdo·Éo¿˜û"Á a žLVv^¼óƒQé\ f‰Ù¬„1G `WƤL4ܦF]Òå ¬ËØ~ËurÚC!ÒÔÐÊrtÝCG…úK_úÒÌ‹ü¥{‰Ë&î¥IXG¤V–“Âx~á _¸k娭Våa½A¿ÓO?=é#’¼]Òj̽‚8–Ø& ÖÙ*|¿[fGô ¿ð Yt”]4g0¢<¬O°^Kz–ïÂá¹RúPæ_Mx¢\ ƒØüFÙQË œ\pÁòÃþð2–e/é„UèÇ*òݧ?Í`6FžÛ²:øëâE¾£0…ŒRsõYC§ëå]õL=Šsl¹¼´.Á½ OâÌå©rs¼3T$Ní!®ŸØFN cûqÞ‹ žÈËØ5+ s±‡¼£ã¤“§Á°bï€%#GX†À­Ç%½Z`˜¡’8–©‰R0F3ügŸ}vâ…@=ϱÏ2*;çíæ¯ #¦A–ì¨ßËc1ͳDC‹9}dJÅö3ŸùL/¾>¨ ÞQ8”x/}Ú,òÉåØÝ`roáž¶zÅWTr`õMw:üºC{‹©ÄeV¶ /øåmaµ@N•8ƒë©§žšxŽ™Ç‡‡U6>úQξÙI“|±âZ•2µ¥„ÍýÜ¿²Ÿ!?CY¾[ÓÕ~úÜ(V€•Óû|k ‘ÈÑÿ ÛH®ÕHæÑ¬ˆUÛWr?ö‡‰?è¨ÒÇŽjËðÚ¬6è¨ SOU0å #ÖÕ&ÙÆTº ´Þ§<å)‰ã؆ÔD;Xl²¤&Xù°?éƒ xbžjδ³C „¹ô÷LÖ[̽VÛ)³ElSÝ}åH+\ø­d¹ÍsÏa ÿ½øÅ/NÚë$ÖFïRÁŠsvж¤1ÛA³4°¸:}9×èŸN˜Ê…gËðIË&<†’³*€/åç;9`h¡+áõ¹W)ŠÜ2«)íZ|ÝRÛP<ÆÄAyr`Â46rœPÖg¾XÏŸ–úÿ+Àß9ðï5Pר<Âù[—ì„·oç½DŦ³a¸®™µÈ¬I¢Ò‰ŽMœÚ£ÝÅ=ïyÏjëMÞ—»ÏEåðjtÀá'4áÜŸ€} ƆhЕS´Ç©øÕñaßXØB™ÃfÂ!Ϻ¶>êñ×ñ še’àq€biçJWFÇéP 1ŠL§£à¹Ó[ŒBÒ¹”qÈšpNÃÁ•ë#ùÈäòÚ$¯L-æÒÓ~qˆ#P/}iÐÓ+iùáØ†&{Ÿh³8A—646ÐvpžeÀ´¤C°pìc©itü‹;ßùΙ'm³ äkûðåpqJ%ôÝ©„ͽ2'?‚yäÃÀ?ÓR.²”wSh׆ŽÛäŠFn¬…£÷Å_œN’h30ûÀ„WÆïñëd’J¢änBqeOËGÇГ·âöþ—tövþ¡9­ôŒïoTŽä„™©„pÈÈm‚9÷ÜÆH¥“/LêÑŽ6¦>y4Å>ʇ@ÆkJÓç=JaÌ ÝÙ[ž+‚…+‡õÒ¿9â¨ÜØ™)PÐÉwCàèqá?ê ï}ö =ÑùãM=ôl…U´ÀãØ(Õ1ß»xË[Þ’Êô¡JïU4ò¼¨Lð}×ÕvÀ`‚ºä(^¶€&ÄÈ<;hÚè”NÈ'øÏzކÆkž6Ns‹?øÁ‹»Üå.¹Î½Ž?8¯üÕq‘#dò˜£öLïB—1²¡ž¯¢!S)›ô®Ç›ã†A-JÀk^óš¤s,ÛÍÓ7 _éÏÑ><êą̃¯kâÚº*`UÿìmBP[*êö‹9. f‹S#w„šŠÎ?Ìæ‹8]+÷6æ²²ÉOæ”™8Ò‘0tÔ™‰j6;ÿÚçAàIâJIñÝ @="sPJaÿþý¹œ†²@¯M"õ2´œÒ}¬ ÙT7‘õ†Ðùæ7¿™Y¢3Ê›#0òd9Û¿á oHtCël\ö2 G½Ð¦O‡•KXçž÷¼çU?r‘wnAÛæª8ÈH`E‚Îÿ¬³ÎJ«ÊâË^ö²üLÍÝL÷}êSŸJó9ëßé8èHÇÈÅ:M˜ #Qâ8]¾7iÉÁÊÀÎvJ܃¿í‹ç!ÁöˆÜC¹Mg-]‡ä¿*.Š0'"²¿ŠòzU¼¹ß¡¼Å ±ÅÍ7ßœçx`M³ÎW>Á;}ùEÛïU ¶k— tNÄõYñ#¬ööØú6ú?*7Ó²=PÈ¥~\‡þXÖ§Ø/ýÒ/-£«pŠN¥º_uƒçª^Ó.µš_g—ç>÷¹N2«ðXõNúàpExâFƒLŸ¿>÷¡„exù†ò‘¨‰Ë*<§¾³ŽÂ!ó¥.£a *gtF?g¢#Ì©¸èé­·Ø $éÉ º¶ñKôjÉkøŽ¦{YÞô¦7%­c´=ˆæ:ŽAûò¾­.º¾Ñž\Šwùå—ç¾#,5.mF~)ß¹·ý}å+_ɲSŽèlÑ2)“ñˆGôÚ!± WË–Þå±Ç›¸èÔÚE¿9¿+_CáZ†e#Q–^møOø¦C ^ˆ÷ˆ² L­´øRO¸'y!*#5Â`E,—IÍhÌ©fh¬h‡h´ïxÇ;o|ã«ùë(dã(œoæÐ à45¨QâàýX˜âè¾øhïSpdî '®®€kÊ>ðt:R{Ÿ®ôc¿£ Æä#]°þw¤ÀÉ'Ÿ¼¸æškÒ„ÌÈ}Ž@=1uÇètÿþýés°Í¨o†ív?3Jf„Žt;Z/ #q§81A³«!p³íPwcFéõ¼xg¡ÆŽ¶¥]t gÊŸ8!—¢UæL;4-òKgÚüê¯þj5aY‡ÂëŸ?Vö/k‹ÿCÛþÆeù?GÄc¶#«´¥íý‚BxLÛlcJ°î $"2…™š;E-žùÌgf§ C÷efi‰?‡y]“&¦¹‚¦Aœ‚ Ý˜Ã#0;9×ßGoŒW_}uFµµ¥›òÍÆHý ÍKºè,94ý¼÷rZè mb—Äõé|äx£|ë[ð¤'=)êÑNeŽ<T*£ChA]!C˜‹W˜£üt Ê$æÓ ¶3ÛÝùÔaÄ‹Ø'eϘ‘¼ -”Ͼ«ç5äÙ陹åD_x·Ýv[Ní0h¥MZs”­tþtOŒ<Ž<± ÜaZŸŽÞÎþ‘qÏaÖ¨^ísaL¸ñÆs~’Ѧ{À÷…C'Æœšíè£NAû0´„6_, 2X_VÅ£“&¸Ð|VÅíûN¼Æ4 ò@ÓÇÓ—%ìéÎ\?4ï Ð/ò˜ÎXÄF}ˆ0ë‚_ÿnãC3ñ¢œ‡ÃN @WiŠÂÍ©eÌG꡽3öð'”p`á§r饗æ\6mÐ<‡C<ðSPö1 %Gá—ŸûR¶ÓxÈ‚'=6Åã½ø°,gBÂù%¨iÁ˜Š¼érá>}EŸü,/r_«GW:Ú ò56¢[Ä4xFNWÚ‘ß5q_°þ£I:m-'ŒÌ¤5 ƒBb²gD‚†Èý@çO'öÀ>p[;¦³ ŒÜ¡dCÀÜ&³ Á¥W&— æ‚*c…ªÌÎZÞW¼âé¹Íç¢.za®b­7&äoûÛ©\ÍA§:Ýx–vŒVºðª§—F24}ÞÁö =¨7F>¯|å+±Ëf.åS8O-/Ëá'Öª³4p(×´^×Þe]Ac,¡Žî›h>Z'ULJ8ë¼7ß&ÜÚòVÖà¨=—rZ­”9m8ôù†œgÚë –í>S­Àe©.{%„ïZ:~òÎ2s?spÄJàû€ Ã¬šÿY/tôvæZfÅåƒü`ÂcI†Â»O,a¡ó?ꨣ¿ýÛ¿sel¬&7‡y½Ä[&w pùmì½ 1¶ŒZ%P¶hŒÏyÎso{ÛÛrˆ%@œWá‡I]=îq«–¯Ì¡Ø˜Ÿù£„0÷ùÎ8]WyˆçÄ­+ßé;ü/ÑYàßAÀÔ¬0œZF43øëcûX‚“w§Â>ÐÒÚÚ瘿^U~ê‘ÎK(#dFŸm¼ï7=«`ÎýŽvìºû1ò þÑr1×”89(šC \À¡¾™. Ô“øqô œ`ˆlGÇ´fÛNû¤gkc…ØoUZ;ûûÆG¼Å0Ìfþ‡!>xl™;O qrÁ‰OçÊôÁ»Ùq¬"Æ”wC;°¶¼¢À´·Å¯SAX€Áº_–!´¹µ{ß¾} œ4ÙÄÂzƒK[>:æ0r*,¬G,Ò«-¯Cõ›ÂÅÝ X«T§Òkíò¤“NÊ)ó› ÷@K2$̭О©?”]d)û1ÀûmGÐî{2´©d–ÖÐ1ù!cT ´\L•;à¡R1§UtÐ¯Š©h•k:v¬]zÑ'å™ýÀkj9òÕ €/ÀÏDý°LÎ~¿šÿÀvb§`{‡‰dBÖÊ÷mŒ\­,V@p™ï†bbM#"8Ê §ßJ~ýû˜g;´±0ÕrÕz…wúé§/bíìØÝ²³ ?¦ˆÃnV7ÝtSF³¬Mi†¾§¡3MAZ¯*Ô§åšÿ¡_árÿûßqà 7d'‚/Í‹‘Ž˜l¹$¿C­Nh«ŽXçl't(´çr£3ÌÇXD tlMrB N¨duÂÔ=£[Â%Ú)¿lãSð1­› Û÷c®à¨¼B Cf`E`ÀÊsW`«cbìúxíµ×vEŸò=V¶>mЀ#·?ÌvH0&(x>ðHïèðyq÷»ß}RçOž6WÌ¡ÓÆ.ü¾å[g¼:N%ž,Á|ík_›J@—%€Ñó\L` ˜[°Ó(m,C¦Š¤Në¤çÁ;ެM«Îœ+‚Z›žÿüç§ó¼"Lô«—Å2â°†Yž oÖãyfDû¤C¡#ÿЇ>Tmt¶ô%`pÎ=_Ê8”²9;Ó"‹;Ü– ÀÐ6-0e©´õý˜«0Ö¥ˆÚÎ]%Èï«®( ûÂÒzÞyç-äRî5)ÎvøOzüxäÃ>?( –rÚÃ=·4âöãø‹„Mm¿ÿû¿Ÿ^ÃjÈ]PÝ×vœÑ2EWÚ¶ïàd#µq´ÅòMpHšuÄ…NÒ¾l€å{¼üùÁ€mžáŒì.—\rIŽôæìÖ%øiîó]_š(d™®p¤c¹ûÂ8”â•õî¹ç.Î9çœÎúB楂XØ›ƒPæ9Ö¡ÏΦøWL•-8”ÑaÑ>©+¬q(á˜í  wÜq¹òÀ)¿:Ýh¿L³Ýzë­Õ®‚›h#N”ò§Ž[Û³²Ô²¶ÅíûM“ûP9Ó_8Ò ‹iœ4›IÈOÙÙƒ÷N±Ã&¾sÈ×ùѯ³"€ AXíGȾž?Ölü¼‰ZÁÖí´+1κN@TfŸ …h:Ópšt<_Œü£Óp `.X˜¿‘źC2;Ǻ ¡%4~éK_š›ƒàÍ­GñÅ ŒýÞ÷¾·Zh™WÅúŽi!ÂPaAýáDÈÙ*uCó>Ôâ[ÿ'V‡y䑹2Àí`§Ò35¼‚âŽÃî¡l Z¢Plcʧ˜šÁR <¶7g®%vK^ÊUÚ0›ûô¥©ç‡"âœ4í˜ ¾õ¸s< Û©Ç¡mZ”¥Âóý”«J’°§À"­}™ç4P/¼»×½î•S¦Ð[?¶¼°žaéa+hê{€àf¹$àßÇoË›Ï34Ðüã ÑÒ†ÅRÀÁ#SçýëyÁPV˜X3ôY&‹±†æ_  U¨ŒŠiÿú믯’¯ŠëG¦‘`>æ–Û8c®ÀqùL]Y邇`Z€9pêÊ÷@ÿnýcB¶“FáVhO-£U¦àFCðã\mm*nëL¯Ã͘òÒþ° °a G/³\ì”SNIÅxÔ›2Æg¼Ï M«:hâÂê‚0òaMSÌíà,Žâ>šÊ7è8GPƺœŇØË.»,ý6Ú,¬âLÅjÂŽXÒ€³Y¦3àñûßF, ü*à2Û@"Ïèòw÷wÓÌ«£…^uedBæ…õ¨Ge‰º*þ˜wšçÄq Œu§™ÂøÐKë†L_Ç×N€ã?™[Ä*‘¯MºSÈÄ~çÉœsÔ #ƒvI`õ÷ÐH³žSõ8‡ŸWSÀúg‹`¦çàÂ9„#°T²¡ìN¶­–cÚ.«øaAyõ«_Ó(Ô °SMõ¢ßþM£mMÌ8bO|WsÆ´·ÊT•É&œºrGáuÅoû. e¡°ÛÒôù&\ûÒÛ÷L±=àÈþLÙÙZB]QÐwÂ{¬ûXزÖ=~`_üš¶ž&üK—ü1ª/ ÕÚ%-l\‚@Î\fžâ!޾ßKWµË18B¯.€²JWF/xÁ r¾±m+‚S„_üâ“\c„t6­ŽˆMfL㯺Z—ò™eZ÷ð» S¿8þ=ïyÏ‚‘»u±3æð'¬}vN\pAúh˜ßph{;</éK_JDva-¯"1ãœÌ^ Ћ˜ÐÌx%<ß1ÝÂjø_e¸ŒÇ½Ê1Nfâ6F®Ôá¶=O5·[>eD[^}¿Í]fq«ûµ;ßèЯ¸âŠD¯C #,š,—GÎÜ82öñúü3 €»Ì6ÿojv¢Cë* &`·¬Ô´Õ•¦oåxv޾›ëj™ç€7U)Û¢ŒÊÕ󨯶‘8‚ŠÀ^ÖŒ:æê®Ï¥ÌCi¨@CøŽI?G] 0~ñ1—{ι2@§@|GØ-@ýÎݦ÷ýø±K°íuᘆQyÅð1yL&ãm«)ØNh;û÷ïÏh¶£z¦&˜J¥è¥¾×ëÀò͉§ö\0…£e[œ¡¿uDz[œ§Q®ÛW¤?e¯›jñ~æ Søÿ«À9€­Ýb¢}ΕÄ @zDjlƒcF{⟸طo_F•mé†~S°‡¦oŠ?7<ò)™ª)ßú{Ò(„ºæÕ¡/õÄhµþÜë4T‡Ë3#;æ)aP¦vcËm:ëxLî¼+ÂW¥)‘:ü׋ðŠõÀ†$sŸ S ' éàd~½Üã‘, Š(Bž |ɇ–?ÌÂø®Ðé“–=h„>ힸÄ{èCšišÌÌ´ Íßøô…ŸGþõÁ¿ ´tP6´Åíú&.ÖUWü¾ßÅQTOçwæõ ( ]S"(ƒ( ȸ÷™ñ¶ß(åÊ?4w-ðc¼š`<¡‰He.ÎÐ `Ø™ ^eeÅT/&ÞÈ`sÂÕœ'ì!(’FÓxÃ×þñ_|ó›ßL~ZÏsà±ò=mæë_ÿz–³¬£Í(ïXþ…£òrH‡g»Õ‡†viG_/«Ø8d“iëñæ~–>Cášn=ºò ÜawÅïú.eP=¾r‘»©¢ìµ ®HòhN¬]C wkà`ëç-†@: H|Þ¯ 0¤£UFÀ«âOy§ð™ ¾i.x”M˜SÊ =û*Єé‚ç=ïy™eÛ|ël'â6¤<Æ%­2z(M„±êªUt®r þö¾ž¯ïÚ¡tÉdyé}ï{_¶›)´¬ãS{®,*µïã-4E_ÏXˆbÐÇabfëMe¹×›K7t†µý*U]©lh˜!™³bYŠšèª´Î[²« š-éû”Ÿ8âÇj‚+¯¼2Ïp0€g¾Âþx‡rêÁ‹# Nžs€`• Ûp*#¾<³—ëÄvÃ^„>SÄs¤~üñÇ/܃a ßÂû´ñÇ>ö±€MÇ鼩ý¡(»m ¶›ZÔY˲”÷}«tu–}áO@ØCÒ®Š+ýÚ¦|äœ5åW~%•ß®þ Bö²:‹¥ „¹pÞ.‡šæ}¢ ÿ’‡ŸØþ0ëóbìHèÒŠih>jJƒØ.GëÅŠkÔó#°dð9+I˜=ÑXMæ[ùqÅKñÚÓž–_©³&h²ûöíËFuóÍ7¯€vÇW%­®½¯Ù2–ú¦S˜T?ýéOgC™ƒvSð9ÓRßÔË—°°Í,Öž9J%õÒGý,à¾ð…,ŽN©]eSÀ÷fJ'§,»÷½ïY2èj‚Ç‹m½©mg¤™þÄo8Ú¯²¨©,Cá @[9¥µV€.«r×)`¦•3Ë4ûù}ú¿CðEÜÎh0¤B¥ ²Í(Ám$g.t•}[eU‘܈çÜpAa LhoCêSë !ͼ<Ë’Ú¬úà°‚:4á[¾gó™g>ó™iºç|©Á†ÍŽ“N9•ùM…¨¦Ç—]èð‡'æÔ7 ÅÙgŸ½øƒ?øƒä™!<:sÁÇPlP‚1¿;ÕÑ•‡2Ž%˜Œà…Õ•®þÝNœ=¨/¦mšV (ãwð–·¼%56ß:MÏÊÆ¦ïMï0Ú/4ÅòžUK„¹øMÚÑϵiÀy6”‡|S:W[¡¬aaFSü‘ï,Òýœæ€‘0š“‰4„ò¾9öÖâõÛë@þ>…2&áPf7ížð„$Ì­©Óa‡0gdÁf2Ó—q}G™pÖc¾’eIŒ.ç((ŽßøÆ7˜eNs”{]0¨'댭¸ñ ¡ãÞŸ9Ê"¥¾x¿Ã_Cùt<¦ÂF::3—ßäVÏK €þ+ªÇëóLZüwl³MÛzÓ‘h~FùÚ«þÓ¬®0ERNhäà@åbjùÅ©I> ßxX¶µ®zþ‰qêW5œ«á@k Ôà3àßòˆŒ³PrOàZ¤Éh9Œ A\"t±YõÅ¥@íƒÂ‹÷\A¦B»zÞ0º¸Õ¿5=›/#Š‹.º(­NɬJ£0ó›ß¼ò ñ‡æ¿÷{¿—[œ²ÁsÂ]šóªüV½£Œâí”ÓÜu¼*߃ùôƒ®ŒN93bÿþýÙY·Y„úÒ3¹pP.˜ó¤þ6ÑöûâØ'ž<§·öó¿#<·²î“_SévßûÞ7£ÐÑÙqÖÓx*G‚z6}=î”çfyß&8Ùª(õI×^v™¹ók¢µß¹Bâ=üáÏ×]Ó¥s³Î¢CéXæ¿âÞ>ÿߣüÓv_®ˆ?üf.5Ü."Y)h;VÔÌ® 0wç ¶ªÃãðuÞÚSHè¦p“Þ4xlÀExù¢øCxc¸å–[ªÐ¬7®Òé 'œ&H˜[s\jÒ­•ý P,ÌwÐC<1u/0j|ç;ß™ÔÀùÓåkSÈdG}Ó¤:“Ç‘UúÀ8Âì¢ £uøõ˜cŽYìÛ¶ªL)»mz²—¾Tl+¼*0H¡½âäéÎ…–eUüÝxGyP(CÓª†¡xQF4åÛPõøÖøz_ã³4öp úö4Æ'½J[[|ópuÚ?}þ〄QEíÍ=¡eÒ¦Ät¢T8#8 Üw¯½Wp­­åŸ‚§š/°ÇÀSáÃìÖsV쇩Þ9Ëz™`T6–­ È‘ž|Å› `N<ñÄtîä»ñë°¦<#Ø8€ÿ§ÊF4ö¡œÖº¤£²Ã€Î*¨ShƒéœlCí¦Rcøu cÓÊ[l@…ï SPŽZ»`jþ}àXup¶•®´Mß¡u¥ÒN­‚ Þ:ÍrØZÛ_S^ë~¯üA¡ñP¡©y–rÊÁæT˜¦WÖúÜvuäßf¥1½ í®”§~ŸxUøi,[î¡3OÀ€n(Ó%0è蜟r4g#›XÐ;$—ÁV5;Dîñ¢TæÂY'FËâÛ•*ŠL9Ž‘ÊSžò”„Ù4¢à#õÅo{ÛÛª9xqFÈ ”˜¥ã_Gç2§*ð Œ-{&>üWQ€º„–˜˜YÀ~ Ž\«H#oXº‰rÉ&T8G‘× (;<êœâ÷ÅÛNà!yHvÒ}Óµ‘X|8Š–ígÙñ¯iôL{¥þØÀå‹sàPâgÛ¯ò[×½²:¹5î8e>àc‡:WYÅ»ðó;r¥—©Sy%žå½¥øßÌ­´D>*?‰ð¶3žu €NŸƒ*Mž©Ûù¦€±bÁI4ãÌuí"üÐ|¬–e Ãø–YÆgdë;ãt]av€±ø˜§õÇh …`U ¿qâŅݯØY“# ?·Ù¿Ž‹ŠãG?úÑjy=ÎòLBWu¼ËïÜóÛDÀÓœÓêJNïMÉ—Q³#bV“ ¤U8¦À]gZhMûWÎ9 ¸yKW¾´K÷ p ®4}¾ƒx¡LýÒ/ýR&ió/°3Di_‡` ?*{”a}Êß'å$LÁ­ÌG «o#eB©Aù"t•Ï~kÁ\8gÆ;ÿ~@wìÙ¤ˆÇÍš´Ñü¸ýç:ÕO}êS©ñH´2ÎÔ{`:º¶§ÂÔ€€åž S¡HåÁÓY܆â#íUs2#¡É»˜oNÛ Y6v饗æ©V8~/<½Qú…y÷‰_ÆÁº€‰5ækrœ)³›õžïØÐ¡õYù{iÖ¤0ÔÓ}¾¸±¡ SC(…žø7^Ÿ¥k(,yb‰Áò”ñöʽt`ºÀ!æd~NŒÒ]Z9wYñµ!Ð&›,¯(,(æøv¸‡Á\²kj=IASàYW >TÒæ*§¸²Ú¼ºðuj9›â ¹¦üô]SšïÿÍD¢­=TG¤îJ¢Y–y'˜±­Cr„ˆiŠÝ†® èÂÅï2VW¿ëª¦Fçª ˜ênw»ÛâÏþìÏ+0¸JŽf$ðZ^àÐíß¿?IL`¬*£óŸñ±por£§uÑïhÒ”Û•}Ò”qà-ÔðÅÀÉi®£mË<漇Žü 1tµ~(¼„—ÆmQhP0‰C|F?õS?•õÌ4´FÉæ›Áz®ïÇ^OÏk^óšÅûßÿþœ`zHëËXØúph¾'L Èca®+4fU ö®}ü‰œ;ùä“«ùí9ë‡:Gn¾ñoÌçà ö)X¬3d-Ó;Ì·“~|úÒc^ÒY0.äL`j°«Z…OÓ;ñìc(a˜®|·êÞ~…)7¬FsùClçåÀ¡¸ËíÒcFÞYqaœ]Ð2©Ð6€ÎŠøÌ~ûÛß^‹^Z#úVDW±´v¶]ñÛ¾K7˜Jÿ 5Ͷtå7ðQËÔìU~ï{/.GqĂߟþéŸ&íÚ–;¢ec}ÀÔÕ$xVåu´0:BRkЪ¸mïØgžÀÈæq{\*s µ¶|‡|“_ ¯4¦ì(O8•áUÿ» hØŒ(™®yØÃ–m%L%ôsu¦´`¡€p¾qä%¬âCè«°R×Êž£>:Ó®«|ì:‡¥†¶je†ÅJ&VVc°FÝòÑFÝ:  |CƒrrÛÇPÆ·<®ÐPÎø}ÊU늫bÌ« &ñ†F•!ézÆ]ÒéÿEü¨)î‡a V+ŽNm— “ߪø¾ƒ) ,çZG〹¬°¡«8Ö¯VΜKÁSa!-ëù¶=[¶9èå‘–]u(®}…"eÀ1FóÜ­·ÞºxЃ”EÓRÓVÎú7{žã‹°‰=Ïëù·=ÓðЉÑ=&dG`”û¬³Îªö§“e S1ˇ “?ÊÈ7:Iêçÿøœ­€Òsä‘G.žô¤'-®¿þúj*~2ÿ6û|m“s#د¾wŸù>é›â”þ^xaÕy ˜Mðçx/.®ˆ`êBól|”r·½ÖJf{éJÛ÷;ðÀq_Ì3¿öµ¯Må¤Í×+ÚôÓŸþôœÒ±nûæWgyàm‚²±¯íÙÁ µr¬-~Ÿoú] SÆ(%«ò°¬Cäi†Ò„Áà\tXQŽT¾þ÷'½’Ð t*k ˜j˜6@˜i>N[º¾ßÈß¹l5¸¾i›âaÕÀ<ƒ¹Þ½šâö}£¸ônhå#¤s›Å¥.Òž½á (%2þªôäÝW ’áDB`Ô»ÿþ4ÿòÜæÄÄ÷U|µÄà H«âoâ4áÝøaÒcÉä)§œ’Ö±‹/¾8;j::Rê:3ʤ àp‡ †Nþ˜à¦s„Š6,¡ $Pv„ú¾è8qË‚ù[§SÊ. p¼êU¯ÊŽÑäÔ@ù(Ë9Ý{`*̹ÒÛ®PTP®šÑûäuƒÀÔA:æÃÌ(:ã&Y‡|ÐR„s'Ï´Ê:%8âÚÙ‘§ ²a.ú(“ ÃÔ²IáôÁѸ¥/Bm”ûøÙ fÇ–«¼åœ~&~„ÿ¼u™þ‚&Ĩ~æI2\†`Ë+÷M¿(p~‹yÆL/œ© '<_~Ì5âЄ۪÷QQË`Ö„#ƒD3*wºÑ—çž{n …ežÁË5fšQOÂGš…CÑ2æœfh¼ƒðYE3ÞŨ¶‚æîŠ^_ûÚ×ò=|m›Ò7½'M(‘™N¸–£Êd7äYòAÌé/¯¸âŠå‰Nss¶ËÂKê­©LCÞSþ0Ÿ.JA“öföªäSibúöË0Ûg>òÈ|WÅ Å%áÅÔaâ[Ò±*À†oÄ!üK7pì[gÄ Å&ÓÅžk/“u£¬ƒVÑÙw¡|æ÷ð똷P&¼m}¯Ò)¦%&ãbAsò«Ù`|VáM}J³Ï}îsxŠG r2Úe(#­¸Ð†É;v\Æ &ó˜ño«s^.ÿÚ!ùÖy–[sä;=‘RÛbDóìg?;êØ]ˆwÁä(8ÁÐmIz} âez™ÕMúê/ôÔ§>5}¬Û¦4MïÉw¬ ÞÀV¦6åÓç½í^åk.Úè ÝÇ÷¹š&B–Zæòh—!ÂÄ·D㜤S•!ø@óo¬-W^yeÕP ÿІ\Ö©ÂÏ`•¸2¿:­¬§˜G\wÜq‰‡õUÂís¦º%q±@Úò®ãÒö,®×]w]E¯˜ºªîûปqTÉÐ6 –«­ìMßLþIÛîÔ2 ÇIiÂc®÷–ÇÑ›JSßòÀ*‚·Ýv[¢µ)Ü¥ùÅtÚ2ödÉz±M´•Ãv>‚Ú1@¨^nߘ·pËŽÌwmWÛù±Ç;YÙ³î†xÞ·áV~‹½7²Œ±Ô²UÞ‹ƒÖ£˜òìµr$¦~œ½QÉÌ:½'<—}ûC¢\!oŸØZÓ0ó4ÈJŒO|âY8¬$lÓ½ æàªrËpÕ‹ž7h¨û÷ïO¦tl%®*3±9Ê30a,~¦‹uàÕv¨¢¢¦Ûª.Ñ€›ð±NøN¼Ø†9éBÙ0÷ÑVK:ÔïMï¼ Þ—AÄىϔNUËEÌ{—YLº—n1¸ŒãlG:S…}{ùYÚž}öÙKLøé?…HN… °©¬„E¢eLÁ±-­4Àb'Zf­_§A^þò—·eµöoÒ*vóÌrÐuYK3ôÛßþö GaU/¶o|ÏRhêšv‡4¤ÞíøPTæHÍaI¬—A «å®ÓCþ¡áo“t0]^ýY«Qì…‘`…UÏcä³ Àÿé·6¤ˆ§ÎÛª‰`dÍÉ(Lìù±Q× P†%ŠkUÉa a¨X“ùËpõü†>cîT8ÐÈÆâ– ·ÿÂStyüñÇ'žC•r. í“P2jyÐÿßùel÷›y1gM½t ˆ6Q_šn5}‚Cßú2Öò'hÜ–gÛ7GnS÷i/q‹­{—(Xä‹2;…^m¸oâ›Êø›ßüfª©*c> ü“>Xpœ{•¦”%VŸ$­cÛãÉJvŸ"ÙFXÞBú–6 ß1=DfŸüçŽc½h­²ÎÛÊS*ìau‰ÖªrøŽuñÀ>éÛà׿9u'Vy¥ƒøhÒ©žç˜g(Ò•Vé:žæÿ| ãöU„Qš´¬’Ýõ|F<ëH÷­H›ý~iøwñòÛª¦0"ÕIdÂð>®æßc‡¬^LB§¦Â fD.zuŽw| Ã`T  7† Ê40º 'a(^uLQT0‘O_‰SÙpu"¡Ü%N˜ãÙh%–ÜUô‡IçpZ+™˜© ‚£^ΦgyëåR˜ZÆ!W•³Ø#}‰)” ü¦üëïËøðPÑÌz‚Ï^ŒkgÜgÑK^©Ó¢ëÙ´ø‘PV6³š¢ÀI/…(ÖBY']8 ù.\PñÖª&.]WG{Cüp†à84®e¢Ý»É˜ò´«,vž±—D¥|YÇâá³ “ަ]°Ëïv|Ö¯°Ç\-/Ö òè[ÖŸ¦{•'NË.ž>kúG¦ÿj‚W¾wPŠÏ‹9áÎtÕÇï‘o.ûc™°8®?ˆwâC„­µd[÷³üü\>÷¢ 3„sãŠ2Óè@rÙR\ÂÄ^×A Á˘¢sÊíFíòœ2Ÿ1÷A»jÃpš#Ĉ2×+„Ð à­ nËI¹ ÙË<+s+ÚÐPsÙVÌ¿å½P<åµ*r4€jã–šÄgUü¶wn@ÄÚbÊ0&°ektpyä,Ë Шo(éÉ’´?øÁ¹½*˪ØÌç`ÀÄZuÖßÃ+ChTÒ€´!€lê½X¸Œ2ê>æÑ¡p-bÚ%yXy2 XK"y5vÒËeiìIàºï–d;>…Õ.ŸO?ýôäè±›AZEG´`ûiö`y\Ÿ}=Øvš%¼ìÉÀ~ lOMäëÙB€‡†å±g%L¡åE¹|™exsyXÈÌz OhmYÂG¥«÷ò_÷>q ±ô­ç3áy«cX,¾ºFd”Ò5®GÅÏ0»ÀjgÎ9ë%õÒ’Ô.Yr¤·(pÕ¼¸_ügg>jr}óm‹§†9§£ÒÕW_x]zžÎ?º¥2Ú$#þ8p¦¢1#*GÇmeó­žÿªúèz'Ÿ¸…®4ƒ¸fäØ$³–Úð┣ 5õ1¸ìÕ4¶«X¯œ$)ËÞF£UßL Ÿ÷¼ç%Ïi:R~ëOs¶'˜×*<†¾“'´òh‚³–ÅÓN;mò\öPü»â[¾/ùË•lé;: å«’Ž~ÉO˜X¦L¯*o½õÖ,†póaÄVT¬~ÔÝ\íµ´nÖ§Å—eÇ÷¾÷½3_å`þÁÚ¤+9aNÞ\Ýú»_à´3Ä{WÜ´q-¾Šë¥—^š„ÒÌULÖv_6ÌOÚ- ¶˜QøEaóÖ rž ]¯üÚpá›þœsΙì¹)žîamÃè¡ü®GmhìKžÒÔOCÖDY¦™óÞ‡C‚aL°]Z†²B‹§N‰8å æ± ¿ò›Ju¡Yx,{5<‡?U{5”4XE£¶wò1+o,³³>¹:ð–·¼%³Ÿ‚c‰¿ppˆýùŸÿùÄY>‚§²i®éÀÇ9î-ç?øÁ,ãÙ˾ NI^rÉ%Kœ• Ð Ghh5´žcÔ\MóÅ)™ Rþ~ß«å ËÓ2¬P‰ÏÐ)œ¦ú.W68øOq ‹`Å;C^§9ø,Gßr÷ˆçüÿ_FÜ‹2î ñR+Àö™ ìaQ,^ù±ekVÒÐŽ®Ô®Xc»‡UHX!Õ‹¸ñÚ/é§t(A½Ä›ë¥w%N«îų…ŒQTê?ó±%Þ뺷Á! ]bf¯*sÓ;Óà ®å(d îŽrYa@ÖõüÍ—+›¢—ÂdL¾JéÓç“:ÍV=K_±„?…´…ë\Æ”¸ÇVÉYßc,N:<ÿ²\ÞËÛ<ÇÖÆYÖ!VX:Aë¥<ÎÜHЮ2@®öµ*ÈÈ*q°%”x拞ò+F€ÏЦ¹ä¼8>ô¡]Òw•ÁüÈSþ´|]WʯŽÂL[þ§Ú½#±·>LßÜqN5^jøÄvâµX€mEaÊ‹¹²¬¬¡J7eB«-+Æ<ÈO‚b¢q-çÐ%vM)*^­ØüÈ{H0ÝkX1)Ñib:£DÔËKCê^Ê:jBY}é!- ©¸”Z¸ïú^ÁßÝåbo÷•x™'WO6;:hˆ°äÊènî`ÇêH§o­Š§"‹…Ëön½Á[Þt™²ܪ¼ÛÞY¶½àùßFi…uî…/|aÖùÐ2—S“ð‹½±.Úù=ð\†ÿ@¢.ŽmåXõͺŒmäGõ)mõk¿'U§ôAÃ8÷£’O¶¡68õoöaL‹Æ–}=ŠwèŸù£l­àÁ/µ³°œ7(`Íske±~Þõ’CLRwžŽ×N˜ å—|Ì‹%vìâDZGîÂ{-;¼?ú£?ʬͯģϽ•Ï–¹÷¹Ï}Ï¡&µ²}:ì2~Ó=BW3z˜*sS@•Ž7ÜpCÒÂÆÒ„g×{;8s òj–æ^©§8Å/ó;T:è¦ fŽÝ޵϶ő¦˜d]nª¹«®Ú¾Ûa¹4X>iÃeÕ7Ó…“aÖ74P–´å_ÿ¦ûE]´*›=÷Îza;[e¯Ó+õ²5=C«2>Mñ›ÞËsl~c¹ÜGdªÕ°ÄÙ:v«dxÆUeX>TÊ4]÷ÒŒ º¶žÀDöãìøõo'y¼=Ä•€÷mg¨é`ûqÞ‹ K€ÛÏ1IæÙpí–Ë_üâ%&Bç{Å€m{I7§œŠ æN‚å2ß¾WÇ=ÏZŸKQ‘VC®˜§JºÚX»`8Eccé[þz<éÁÈ@ÅmLC+ñU(”ç7˜ù;åp(uþЧ´®8ÇYÒ¥^7}Ÿm žÅQve½ ¹‡/£gî•`>}ñ²lð–þ¨äÁ“·²ä»ßýnf/쾸ìF<é+eªò©¬jÒ­Ïà N[­rLåÎA7–Û1ëŸUÏs̳r#V·,ñ=ÊèØié=ÇܲOàL翵Œƒ‡zðc\&~[ö˜°JÄýÚ‚ŒÈ¨ƒ³§ÜLbŒ&N…kŠ«nºé¦¥ž•o}¾i¦æ~êÏyŸ¶ "šÓCé`¼ /¼0ñrnq*ŽCÓ£ñ*d±¬ ÀㄱÄÉyþ&˜vÒ8 Žu”Ò!ò+’¦ü»Þ;‚¬{½»:E…® ÎÁôŽL SPio]Œ½*ØÝ%Pa:…~ŽÈØ\Lø}ñ+ã¿êU¯J¾RÅI~tëÜv_|v+žõûƒü`y 'Œn_c:~é,ýèX SéÇà)N¨Ì²8oo^S®XzI¯%‹{:cà*cævh]ÁKšÿx¢ÜqþŸ†ˆàî€oSƒX{žW2"W¸¤0Ûù1S¯æª8+މMøc<¸Õvxq¦vk‡sSF~«w˜‚˜Æxö³Ÿù8º¨ç¹®gÌR6Jò`ŽK“0gðNÅ  ÍɘÚÇnÀ³Š³^ñŠWdþådmïéìì@\Òƒ" ÊÁÖ–ï^þM1­K`ºý ƒmf MTän¹å–dåÈ*Þ)ßÙöœZrÊj(.–>_ÇÒÄçuÝK3vQô¡ÁXz ¥_™×­·ÞšEŸ¡åµN=}‹–KëÆàµ*JýË”©Y`F±JÄh¹;âÛù'âýx”©ÙüÏGBDÌù¸þËømmçºÅýZCI€›o¾9tÆvÔ˜eh tfVð€¾H0 mèDå‹)uÔ”Ve›vxÛm·µR ÞÇòÇ;â¯%OùÓ”÷n¼·ó'oË+Þ­…÷Ñþš=Ð&òDhžÿ'B"²S¯ÙÎ_mbûq}‰‚Vÿë¿þëU‡?uÞ3ç:„<žÊ;{Ac–ÄYäï|çò¹Ï}îò¤“NZÞë^÷ªÊ´®î™êÀìÆÓÒÜ J™—÷à C–Ó,̉k²§vØÖ´Kã,«°V]ãšêiÞâ‚2¥“füUù÷}‡%)x£ošƒ1ž£VpAš[S¯ÂÃ\Žå:ªèŽ¥) ¬Ë¯´6)?J|}ÇZkóÔmhÞZP8’`ÙÊ<´{iDYô•b`3–N]tup=™·mÊs97£x€(#ø ¡­§tv Rºp›û» #pc‹åd é½&>±¿¾&ò¤óoŸû'R"S?÷[gq.—]ηƒ-‰ãf"_Ž,]>Áó^ú©Ñ¶ácÒÙÔƒ‘ç&;••²ód·;÷0 JÚól|÷»ßôvÔÕVNÞS…¼±A¼˜š oh7ÇEAÔVžƒý›#á9Ždm«_ë=" i^ꢽ^è¿üË¿¼òð)ó¤³v5I9ë‚_ÿî´ä\Ö­6zmú[ÙNÝò[æ:-¦>·µ½£>:w2ey%+T¬ñgú†ýPX±‚ÿu‹rwÜqÇU²a*ns¥×§«Äw·WŠÈ“kª_Gÿ\ïå6ú'!;pDÜo ¢XÒw[2#fI·5ˆ9u8sÿÐfQb>cLGˆw5–Ñ=}ÓÏðìøKÁ‡uÅýñ!>LYÒÜ ‘YÝ¢ØÑ½°W]µ,°Ùˆ¡ ¾ßû\Å…=ÉÉׯµ ‡Ãïúó·õ…Š4îSSâ`>§Ž¦Zö€¡C Ûƒ[¯L¿éä¦r:†?4-£4Vµ—)4Ù iË2ÑÉÚ©ÒÖ´¡][d#rúò+§çÚÒñºçGº)óó]ù ýNÿ¤|ÂÊ©5yruí@Ý¥ÃFÿQÐ*’Nœ±ðÚë„) Æü´käÉÔL×Å”Àúƒ1áïF'ß„+Œ¨™”8hÔ4l ¾lô¾÷*ýµÄÈÔMùñÞ‘9^ߘôpv4tåg¼®««Ö5:i+ßÁöÍôÃþp’Ý:廙1ßå5œ ´æMñª¦.4ËÇIlÕ&OLXYsæ™gf[žÂ+šÂ?þøunÜ2†¤kI#0eãô´žCakk?ÈNüª°ð1]ÝQÚxÀ§äÕg•’°Wä.--Âì‡âÔª4]K…muôpDÐyÜ蟄†¤%àMÛˆ«al?®ÿ¢Ð 'æ†â8ÛÔøǪa;·ç»Ã×Ûwxƒ8÷°owJº–ga¤ìt4í> M%€4ìvuýõ×WZ0ðÇ6qÆGä~÷»_â´×æü$Þ+A§îf)ßt]­{:U@¦ÐÍο²ÂMÆàÛ±ðæî“Ðæk`¾ËÕz¢<·„_“J£±ô›¹ã %za€ÃyÓG.Í»O:Ñ’—=/ú•´äyMÁ¾ùªÀ·²âs?:¢ÕÖqÿ™mÄ·ŽU[S)šÀ–Däª}èCKhˆÂå!@CÝmFŸÝ¼Ò(ÈŸQÑ·¾åŠÎ-Ê–t¬Óš¶þÇ0+ðоûÒ—FY7¹“,¼ìÐëx´=‹ûúƒiž»Y›Ê[EmŠ0¦NÚê«þMø¬Žñ<©JÓkZ8 Œ¥«ø@ÇR(¡«V/Õÿêå:Ÿ­+ʆՆ]4¡áÔúS{5 ²GŸÿüçï8²¾¤áyÄéyŽjüÉÀcúè „¨࿉ûïnBmcûq3ˆYóÓªüp.Â[}“væ½W®N  ±C¡MpÕéŠ.ìÛÞö¶åQG•tcªEYÀœGAk‡>(%Ž6Á«¬OžûËâæN}|öJÝì%qÏ&„-o›­ûÿ×F®l§ëÙQødV„ šQì¡¢ (ÜÛöÚ¶QãìÞ探 £Cš¡PHË©WLµvÖ¯{Ýë*žÚhŒ_š{KSÜT<öôNÝûÞ÷Þ1-SUÈšo¬?–pAkð)…êPú;5…à<µï†Â²І6èÁ½fŠO|}Üã—õµÊo(”ø˜ù᩺|ÁB*@]åétJ÷† éŸþøgƒž(ý7ý!ÁÀU÷[¶å]VAÖ‰Ï:QÖ–+¢¬intC›±8ëú¡ `µÀ±q ~îPxÎ9ç¬dLéÅÚzõ,BNzŠ Å]ïz×ÌSÇ-;ê³o° ¬f_vÊ0uä'ö«ÖF/„1ôï[OMñ¬?ÍÊ{aTIǦÂ{(9ý5Õïå ”5傞îs;Á§Ìvb9ïyÏ{æ`©ìø¡4j£ã¾9?'ð›ßôÐzˆB¨÷ÿ÷v¡Dd eR¡b*œØØ<⪫®Z>øÁ®:º9ÌÚA› Þ˜{ #kRüzTëŽ(¦óÔ9ê`“ ˳ׯw¹Ë]²Þ_óúØAÆ?Ø.YÑã²^7÷Ù$-=郸ù|(_ñµq:uJý¸\ӺŅ @2ÏÎ òŽyw¦6­(¯Y·ÏCêýHùmƒ÷ZÛÿ1ò¼[БÎüŽ"c•€Ÿ‰û¿ŠaO* Fƒöç wŽBƒ)§^›Ö¤Ó™ã@sþùç/ßþö·/Ù]}ÑÙÏš5ÑuaÃóTm¨'žxbuþ4t ¼ìe/ËòÖ§¦–hz—¢]|ñÅ[ˆMø—†,kÃS·ó4‚ÔΟC•Ü¡LšM ûä¤*o.ëÜ´ÀΟ®a/ÐE\vó*´®aY묩_²‡íÈ଒NÇÀ (XL™zÀO„%ˆÂš[Ap@â¾(Ò`7ëa;oþx|DÐÎ?û`îw%ˆ@\ÿ]ü¶†q[Kÿ9ž÷\Pظ™Ä›¬hMÀ„Ï|<Ç3?À¥£ww½U„7|ÿÈG>’8ÕQúâJCÕîYïÀe3;ƒÝ%k‰aë`vo#ŒipÖ+[C|Æ ª¾ô=Pâ•ÌŒlm¥s&œùÏú/O ÔOaÝ4v:ä oxCU*ñ©^Â7¶«›o¾9ÛÕXyI[t°¥s%þYÈ%–¡r")Š;ÓßU÷c-£Mp™®GwNµü{€ÊÎÿ™Q†QÿìÚBh`˜$~$®ìplàumüž?Âÿ¿Ý1Odöwü <óe4ü¼ÆW‹`ÎEhzwŒ<ðM0P¦ p8\ø\t¢òª¸âóù.P^ctœ×P|ó}¾ìñG™BS^ÄÕE4ºEŒt2Õw¾ó|ùE8ö€´¾(àf¿E(K‹ØÅmŠÉ¨Ì¤]ì/éC[ÄTÏ(Xz"ø ž¦îCÁZÄÁOY¤˜ ZœqÆù þ“ïv»¼ào‡ÅjGh/¢CX„)v áZQCD´ˆ~ÏyÎs2¯½D—µ¾'pÛuB%-¯Cÿb ±gäE(x‹˜šÌä᨜2I¹ÒäƒØ¼l\ŽåÉ‹P1pÊ÷ÜÃÈÃè 1µ¿ªEL—Vòt(^e|qŒC1%‘Ÿ,oîé\ß~´—wŸþp\ÿó.à²:KòKÜ¿$~†)¾Ýå+£qwôHÔÀ?µÜ±×`–Ê±Ï xñ3¢à×7—éGÿzõÅMS[£bÎ"x¸fµ¡ðæŽÍÜUŒŸCµnãýë_Ïòon\xŒ´ê&ôèô—ßøÆ7’¶üÉ_Õ‹=p#N¬XqT>–çûÔ“fl1o¯{€{éAÈSZ9ûиŒãÞ œ:)\®üh¿¾ë[p,V>–y½×¹ño|c_46¯ù¿(ʆ²óCñS!àUï°¶MBÑD,î_1Gñ÷ñcXŒ¦²5ä›ÝgŽ‚N:é¤D%Ìų LœpöÝï~7a¢A=¶ˆO¯8;ïÑNÇGQ1¿YöÁs¯ê"œ}4û1'v)Ëäá°4ÌŸ&üOÒ’uÛm·%¿Å!PiYù­ßú­Å‘G™4‚Nò×^*0x°ZD'‘÷XÖùccäÇV'=ö*]ÖQþ¾0­¬‡ð£â±m‹‘:Ë(ü‡ R.*#Å‹|ù§þó}ø‹dt¬spÐb{ŸûÜ'Aî9‰Ý>ûyA¯WGùyÖ9rpÑ68aŸ`ö,$SŸŠ4Pò3ñC  W£@»`"ÂÝï~÷¼†ZuBùbŸfçð1X&À‘Žñ‘|d&¹ü1`Ò|Zr¦¥!,>ìòôNà”l¤ßþö·¿ñ¿±->͇C` qQ1•ÆÙ ‹[n¹eñw÷w‹86y§&&]¡´Ë뢑õ‡Ž¥  ÚÍ{ßûÞÌ“ñÜs3fÿØ…rþ žö°×è2w¹ÇÀ£NaÍÌ+ü5Fv Ǩ[Bì³×¶?ù”üËŸï™`@CÀü?G@yFÉ ‡…ÓûÌŒ†´ªŸ¸\´ŸÔùC§µ*dˆ¢èð7qÿ°xJr@vÝ8‹œzà˜sÏcGØ ¨øcžŠŽ,vX[ÄúÑü2¦Ñ˜Æ¹(p¶AÙuÞ'3žóxá<“Ï̿ٙé€ÐJA8™ÒúéFüÃa2££LH»>é–8ø<ðë`3L®ù\v°ùbüQ?%^t,±ÉÌâéOúâ±}ì"¦¨Òw¶4gÀ§†‘ÿ›Þô¦E¬°IÐà2„ßæÄg/ò !;b;ñD5œ‡G¡ŒlE¢œÒÁÆÐ\œâ ²„ÁÀÞ™#ÄòÄû¯¤å•‡18ÎKÀ dÐL81ð¸&ÊNß9zäi7éJáˆûûÅïö‰È]ÞB8P ³Ìƒ2Õá9ÜOý9/…?!:¥¼ù?ŽáÜK_Àiìü›éð¬fû XF挽߫ž¼yÈCº"BsÏ2àïÀÚáÝ(Çnç©Kt Õ Ògß­3.í Ä‰“*Ùq/:þªÎX.¿ÎEÓ Õù,Á5”¸øîðu‹ʬ¯~õ«Y7l±=¶m¹¶žÇæ 9~ð†~Sùþ€ï€ó¹Ï}. `ù7ÌtJúÌýeÜ81 A˜%Tò,Ð:€a)п qÿ')Kâ§yÛï®LHN‘õbËÏòʳ£É|1áÏi€›nº)=VÇÀ z%̉Ʋ¼ÇkvL`ÞKþá ¶@Ûµ¸ *ÖÊ9[çc˜…å³ì}3ùÔ§˜mZ,BÁ©¦ú¦=ØâA;ùm(×A ÚUý ^àÄH2œ¸O{ÚÓrNkÙît§äK|gðŸ3І¡vqÞyç-âÄÐ ^{>s–sNXÒ&6 K°˜ñ£S•«|¡ä'Íå‡!À”×X^ýêWçô(«›æ˜ÿá;xðˆ#ŽH–ø=a@\„>ýÇâwTàðÍ(7}'ýå,a£ òN ä²…x~E¼>*~Ÿ‹f pÚø´@àÙ.r¾'6¤ÈiͨùaÂóR8è&ç•5–éix±Wb3Ö€Æã\ìO°~ñ1¯e;Œ|±Á?êÀN_%§/àt&W^yebÍÜà˜À4Äÿƒ1y i¨ë‘:†¿xþË¿üËË49æ˜Å£ýèE¬ý^ÄŠl+Ì¿Ž51wÑ”©.‡Ê…í¿+í¡úú 3n¸á†$ÓtCéA»ržÞåCaž"„E"¯ûöí›ÍüïÔãYg•¼Hä 怘¬/мOŠç ÏÚù|WC(­"Ïψßmñ3`þÀj°‘B)óqk·Î ü¨”I¿Ð&3ý{ÞóžÌü†L3”Ûö²Le¬N³*08=ÀÁ”Ó%QSË<4}Ì fþl.òÿð‰S_:I6\"_è&ï‡àa6qÓ¤!é÷J\§beKn8•ÄÜÐuF}¬ª»pF\þîïþîòÜsÏÝQ7Ôô–þë¤#mæ§ú§3—CÊ?"Ñ•´qY-´+wœâc׿©»O²¤úì³ÏÎzô³©|ÊéÒeÃlÜF°ü¨4|å ÿKÜåAÙ¡ŸÜø`¼7¢p?L!É,®ÿeüXÞÀ™Æ3|âÜÔ=¯VtÙÁ 5õç6¼ÌɆ&ݣѨ±ÔeɹíàäÎyCñCÐ:Ïuë­·fF4È0‰&\¿ …;%~hñ™wyVÁN ´?AŸØÀ%a8Ï8è‚ï€éæT…¹‰ë¦;{ÛN½†Økâ¯ÿú¯ó• .¸ òüÔuŽ2º‰Žß|¹ªïæñÇuZíÕgåÎ5×\“íÂvZҳァ+^~ù壋+¯…)ñ¡óËBÕfûâ²*ž ÊÃö°I;‘,}›sý$}]üþuà—óýq¿ežæÅÁ¢ •sCÜï‹ßoÄok(7 ÒÚ=æÆ«£‚Çv°QO;E³éq¤!ÈÈù0ðÏýí§8¾¸ïþE]TÖØlÈc’7¹1g&ðƒnž­m}t‘ÆxßûÞ÷2=Îc‚š?V¶þTXa‘·z½îÕçRÀ¹Ž¿I«.šò¸þHë¯ £2xè‹_üâ2öaXžzê©;ÚôB¹ÄÂ2v9ͥϟøÄjC¬>49ÔâX×ÈÄýû÷g]ÚI­”<Úé°ÔÆÈ@qz×»Þ•°Tæ†â³*¾°€M0¯|˜ÿ“sÙñszŽú7:¦Ä­…ÆÜQ<î2~ů<ƒqm+<6KIæ²SˆZÈç)W€XŸÅÇ\6w¾²AÁ«ìÔ¾ÿýï'NüqÿˆG<"Ë |b”Æ „ö\V‘gË1fŠDšL9+…A“ÿm·Ý–´`ÅÊxÒaíöAI%½ºî9E88µŽ@GÏèž]á¾ò•¯¤@gꌿn=Rð£¬2ͳéÑþ*ZÁûò1Š#Á¶¿z¨0m[žª‰ELº­¢kÛ;§øˆÛ|'I†ÒÜø($±¼5y|¬BRÇ õëÁòçüeÇÿ÷úüøe¿×‰ß¡1êJX‚ì"X*ÿUTÑEÅz”.{ñªG8ØåÒ:¡‚gð­à(×òk§ÓO |Ç=‡ñppmùÐØ7bùéOzùÖ·¾uôKü îz×»V´)éÀH S1ŠH©h–qvó^¥¦ŒF{°æÅvøú׿>ëÙíqÇÔ 6ËëYÏÊ<è\u"CóM¥Y5 »].™ß<þøã—øÃ—á}Ÿù)§œ²ä˜hžO8ᄌµTuÞ†3S%(Fty”Ò¹„r[¾S¿Y‡Zä¨çÃáv Èÿ TôT,ÇÐ^Ù÷þ÷¿?3þí9ö¿‹]-“7¦ƒO™¦”ÍŸüä'‘!r¨'æöUh?ïŒßÏâþð¨_b¬ºv(ĉwŸ‰aœ:¹•öÿV|¬ÛO&“qÉrêOÓ9çœ3Z ¶á`.ç­ÆàVš¾!€oÇCF‚8H¢!“—ý˜|MãŠÎ~7oóìsµ®n¹å–Ä‰ŽˆÆ,ü>W•,=«ü¤J—Ó ÀÅz2 úà86õKÇŒ’5ÖR„ ʋ_f`GæÑéìç²-ãØt:æ¢ô`9vRÀ¶Åf8Ð93´mY7ð¡Šþ·¾õ­ÌHø;sm~²2E \øn,O‹—W ¦ða!˜_3Fƒ¿ØO]ùftü¯‡¿ëÄBÈ}Nãú¬ø$ìÖÓÄ+^Fƒéçb4˜V“7gÕ†6ãϱ,'ê¥R pÚ"X~îÍ‹{óʤ›by ½§¼ÍqÚÖ+_ùÊÄiÌ…ævk %°6Ä¡1™å@AT‰ày¯ý#€rtØ8Iò£äGGÎÏg®|Ç–r‘c…ÿ:è1>Žf1#³<‘Pò~/jíÀÇÔ'üD:¬KcWAÙc¿ˆ„5çÀLÿ¬ØT(«w |àèŸÕmÿSЂlåôÎóáГA8— þqÿñ#Hà­§™þ/½ôÒd¶¹LMQÄj›á؃<±ÊlÆÇ&6ÌIü¦8ÂØ8cƒ¡Æ9xŸ¾S—Þ8òùË_^YB,×ЪÃË]‡ Ëûþt@ì2ýŸtàþóŸÿüš™sÞêhØ2W¬hX!µÅ%ÂežÌ)¬ß¡Ï·l›Ú§,´ógd³ÁòäCÏ?Óà;@™¹RÄ×ÇC‹Œp›Ð(¿£„}ðƒ¬hΔuqX·]| mëÓ]X'ºÒµ}—Kߦz?”ÞËãøÀ’¾SFÛȬ,ÔÃØÍu”Gò'’p€ܶºíûÍACœ´:ys¢qùú“¯=3ú?µqÿ0;DgŸ ïq{,ÂajK…1Êž%N¬Š•[‘C'Ì`œA°Ï‰Z†hÞºFc¯öõæä5‚å°£“ÌWl :&xöz8.bô¿ˆQ\n [ϯlióö=L”‹˜§ï“´ŠfàÜr6–=æÞß|èÂÅïÐ žˆÕ pelÁ~äá9¿ˆÑÉ"E(U^‡o†SZLj?ëÚÞ[³m5§Æn‚y¶†'· ‡¾¨¶ei™Üºë`JØ&9ö²Xc·þ%q xaIXÜç>÷YÜå.wIxCé¬üùßùL ¥ýÍbUV‚áôɘö¢sî”óE¸#œ9ö“ÛiǤ°ø¿D%9 À>[{ÙÎ<  ç‘§¦Y}D%ŒÖ@u>ãÏå€$n,ÕÒ[›ùݱ8•æÔUŽpAã\"èʃ¡ÎoŒÔ4µ_vÙeÕrC5{à ÒàÚk¯Ír;ÊB}ð!À¡,Ëæ˜Úè ß0EB¯zï÷Ã×Õm uSÖ)ËY†èRQ2]Ê:†–Îk_|ñÅ£¬Pcxf¯§‘ŸçÚÕùu–Ž ¶s§#ð=šjù‘W€ƒågÚ.ÁüÆàÚÆ5ÿoŠ|P0öÌR¿qCFJ±Ëa !ãú÷ §%Ö¢UÅÎx œÄ‚aò~ê£Fˆ×]wÝ"¼b\0^o°jÐÁ¼‹§<å)™Ž‘ÒØ U‚ô1š`‚¾;®Œ®ÃY*ßÅ:Þ¼öùc”Ž‹XJ·ˆ –^xaÒøjö}àÔã@ðøÃ?üÃüä!#õxmÏZu†ÔñÁ2P~á-Ÿ#SF:¯CÛX&Š¿˜~Ì[xÃzóÛØ+£ä§¡Òv ÊÖ±0W¤³Ãߊo‡_¥@Ü$l\a-Ž€ž{Ýi”¹ÚTç¥/})Y jêÌ«ÑR4¸Ñ£LGõìææÜô2.u}·»Ý­Â©Ìcì=¼gûÇ·€0µý¯(“sÿ[ç(²‡ÃLb; ÀZÊ­Å¥Q‡+*aô+Bs^s m:G÷ˆQbâ(Ã÷AظL„vc¬ Œ*Ér±Þ›û¯}ík‰å—nJ‡îÄëúÙÀâѪHâ]½q# ¶ëµãPØtáäw1QfÆnMÚ„:ø‰#q¨£ÁT§—‰ôÁD îsñ•°÷êÁ íQ„êæûûßÿþË8"x‰²Yx°¤'ߨ—áñ|òàØ%©¦cÊRÑ(ó>”îmë:ÿÙ¶Æò’òäŠ+ØðnXçO|ñ¹ù曳žÁþ‹O™ÎU'Ÿ|r5­Dž3Íÿ/Œ¼±ÎcBØ á€ ì0+öè­ƒªÃŠ;]*!”ó>vP[<éIOJ/Ìîs„`îÿ¿½7›¦¨Îþ[ý—ä‹ù>/·M@”UWÔ¨,‚¢,.QÙÜ@ETY\åD‰ˆˆl‚,*Œ@DMD ˆK2ÿûWÏÜM=óÎ<Ó=S=Ï,U×USÝ=ݵܵœS§N*¤¢ÒÚfí(Uî v&?Ò%ß#bÔi€-E}Ú¢! ;+þ¡Ôûýœˆ[ájí¯Øb‹-Â몯$"6âÁ‰Ša"&µ­ Çâò†ü!×y¥Ž´U±Ð6¨â¿øEñ…/|¡Ø`ƒ >šm…³ÖÅ`F›HµÔ4`ö“FyXnó`%¹)XR“ñ©ÖXüÛ¿ý[qÁÛo¿}©évF?ì¬DË«®ºjøÞËu3îvD~|]7ŽiyŸòƒ3ËUÒ« ÅFùÏc'éøß_]Œ‰ƒåIÚ¾gé´náÃ.?^2Eùöã¶ÖåÕA1HAð?½Iÿ³ýb*  Æ(´ÂçÊ?$Kº`.ÔJGÃl‰Q‘æq¯¥#~Ä•ó–Ê(J1žI#Äi½-„ØL'ÿVæë,K|o{üì“g«N,„)~\nöâ“î 3/yˆ±k|öGÙg—\X¾9ì°ÃZÌ|;ñgŒF¡@H½Äÿóµu³½ %;Êa“ÓÎ73o /¡Ôg¥SãVýÚŠ±<ûì³.’§Ó/ô²[Àâe/çe–Bcê>Å2uÙÃ^ÿ{{Æ8jׯ^c¼ŸØîÿ0ù‰óɲ†—6|Z¼Åù\àÚ[Ó¿®´a\ÆnÂ=Vâ@ªëÔ  *Tø …ëû×ÈÃeYñ¢n”˼¯¸Ã3¬Rˆ°7ÝtSؾg:¬#¯0‹½ä’K‚”AåXf¶Ó+sÙl¯“-÷‚m2Ì6Qˆ# ”kP†¹YŠklµBÁüàð°Ëÿ3ÛÓº]!݉p­Î5°¢V—$ÊGÞöÇlÐù+ÿìsr"Ž-™|ߤãÆè¥/}iße—]‚d@K/…t% ðsž Hû½‰YŠ“(¤ý#ÒBÚ³}”EÅà”·þúëm´Q(¯lAlÛŒq1Nñ½®Ág|ë¶¿/bÓ+‰™yN]RZ® e¦½ŸA@@I`Ûæ Ûëœí É#&?qh{HÞ=ôÐBú'!Þ:í.Žkk‹Oç  fºÀ»ù¯AP£ŒŒÂmäqæ¼æîüªc„Xš8&˜žÊÞZgu†R”Iq6ùp~4`·ÄH”豆Íÿ6žÂu7oÓº{î¹g-Ž¿L¨Â…gp©’§nùôö$iëWH1ý+´)—#Žˆ×^{m‹£$íÂù× Øsÿß-ÔÀfr„ÝþŸù]ÒAÏ !fs¬™"ÙAa)3{¯ñÆqøšC‡8NYLiëšk®éj–ò»oÅô»6n6 C]‚%ÌRgY`<1†EýQǃà麧í còÜùáKâ¤~«´_§¿PH¹lÙ•~†szýÚ]ÿ­‹v«¾ \ªÂ±c&^ ŠÆ™}ÿ¦®o•–ýßSÁ´aóTð$Ýï-ÿWòTIJÓ=Ĺ¡hÛHh˜ v4(ì°ŽY;+ë¿ïxÇ;Šõµ^êŽP%nçmã7 ƒ5¼ƒæ Ã(0K—.-vÞyç“ÃÌLq¬»÷*7Ÿ*;&XŸ­S†*åì|‡| ë, 6ž”ßS.<õJˆ–º]•:…Ä”«–•Šƒ>8f˜¾íö½qd@iA‹˜-¯¸¯QŸ@GÍô©Ï§£ý{jnÍ»ç Æ×Ë»"’ ê†™-J[Xµcðf«Lœ1Ä­8âeÖm‚¿ÓëÚ˜FÍŒ î™ïcÂÑëÛnÏÈý-öÀéÈXóÃýú׿D s¶EœgoÏ"ç­[:ùÙÂ;·=Þö³zM¿;@IDAT…¾¤`˜=u.á,ô]Õÿ¨W<Žü8OñuÕ¸†yôÜæœ‡:ñù¤,³èÜ?QØ•9ìÁ°³m¤DŒ2ù]*zç~;?éw½ë]a|M%e%m$FŒ³,MÀàzâÔ/_5ÿ÷¬ä„öw¾¯Mó¯7³0Û|¾{¥` ¿Ð~Á÷½Þ¯ýœÃÌ›m¶ ßÂQ¦rtB¤\Ý Ñ’^G£óÐÀ÷ÙgŸðI§–u•xâwÐÞf™ãˆ#ŽêË-·\Xë%OÝLf†^Êð¬¬jÇÓ­síøMŒê|;ÍïÖòa丧Þêz¾s\1^àS‚wÄÿòºÛ²CÕôÍX!IXìrTÍsÊ÷\·ìøÁT7ãŒã <]Ö—ª:†ÅijËu¸eI±ª¾Kü}·k>»›XNcG®:gÀ†®^/?wrÑÜDT·ãç¦0µÄˆõÏä)ŸŸ%Aß fõÕW/6Ûl³ êb-4•“Ÿºumœ»3/Y²$ÄÁ ÝqüŸÈ3Ï<3Da›ùžuÅñÂÀÄ ÆVÂQ8‹£d\7uÓ5nu¿›”÷M¤ ëz0×Qá3qÞܾY2÷r:Ï©B÷o–ì:↕f2!â4LÖ×ÑÁãp³Àù¡ø]–SÎþ-ÁÔΔ0Qrz di¿Lo¾¤²pÖ9†ê†_#$'¾™* Û€s&¬•/\!àèÿ ”Ît×]w ÐPS9ÖÞáÂ9$HFbB´U8w4mÓ+vÛm· 83lÞP"m V÷Ð1ÀuczÌl v†ÛÆW“Î’f uÓò. K-ê~ßd¹rÜÕ¨Ú?ºÅè6 àþÓí½i|Äq$9ÚÿŒ;ƒÚ1>Þ•ÁÞ& ƒÔ×þÑ!J5ûgœ°2¬%ÎsÂ@YVPþ”v¼IéO;ÎdÁT1€S÷ËS!I90 ÄH4&ÄJ)—¬  ceƒø–ɵ]ÆžŽ÷9£7ì̆è²iÏ}$ˆ—YƒOîqžÃi£¹šûµF/FBêà6"dC5£ÊsshÌfÌ&2UûHŒ’w‚°\V·ýÄñLâ5å¥|ñ‹_ Ù·ôqв@dQþƒù_sÍ5C4UûuÇ»Œ¥²v›¤œýÛØ’U&(NoвöøÎÛüÎTYnRlýË @°y àmàoW_i'âŠI’& •Aõï|gˆÓ[ÉR$€ëtŠm ¶Š#o8Dõ(Ò¡V ãPèÁéÄ´`ñ …L|·—È»™ÿ—:t9騤‡]yæUÓ²`˜ÝUÓÊï¥EÀýÕkÖ~V'¥¸ÍòÝ qÔIo\Þ5Ó„€å¤ßƒôqg£üãY[!¤^RÍþ‹Sq2M>Lû};·Oº(Žë÷â¸ü?°5µ¤" þI8HnBË–©TÎ –ƒt0Ì·^ep‚0ò³ö·¿ýí!;æ B©SÃç—]vYáe€N¦ÇŒf^qUò^àÇ K,yàê2ÞŃ“j° (ù“!À^…÷¬›¨Õ‰ÎmÆm¹É6['_M¿ËxB¿þüç?’B¢7Œs|Ä!3\U,©7úóõ×__ì¿ÿþÁ4¯'!¢!Pdfi㓟üd8ŒŠ|yü2êøsè ³¯ï(îï) ãgø'Î0×SÉxo ¼Jeüf»ÐIE1J#HØ#Jë¾ÏØ.€; VàxXµC9N›Ã!âö@çÿê†du¬g؈ñ¶êÄñzIÀzuó[7O~ßëŽNßÏû…–€Ï¨òÚ/Oùÿz ¿ö:Ò8×gÕ܇y¿nÛ©šÆ8¾gF‰Ù?v>–ÓìP£a.[ÿ# ëZ׃yÀat G½¤êÔ+(œ'.ᦹŸcÛQOmˆLXWpc8Wˆïçž&ø¥¡âdóL2î”h­2ÐÕ•ÀùꤹЬ;h~X+D À qà 7„u:ââxWXxMÝŠyá†ûs˜®®Æƒå uœjài¸Ø3½ëÉú2H‚Lت‚CòòAݶS5q|r#ñ:á„Bö†ýÓ÷-ýÃ÷UëÂïé4Óâ ƒ ÆÇR®ý3²ÄwÈ!‡ˉѡÝxÜNX7L.ÿÿ\~nÿbâÝg ó:/ª©eTÉÆ àßV‰/“§¬è0ãÅ<0nXq{ˆ¤ýƒxÍÜŸþô§’œ¾ö+=¿Ç ¸ ?–`«~•UV šµü˜ÓFñp΃‡M{ªš6KÌ\°¹` aãV5ŽüÞâ"`Æms˜GDÍH ¸ˆÃâÔ‘ºû Œüç>÷¹@=Ñèxµò-ÌÒ@ˆ¿ €yVß/Þƒ?æ˜c«®Ï~ßUù?“È[ƒÎKΟSûù“ƱÞúã0µ @»Vʰ .{’kn¸a0»Š.@JÈÅà¶í¶Û†=±t˜*DÊ£ûî»oè†eN˜ác˜#k‘vÀ±ã¬€"ÎKÆ&X¼K[«Ò/âœ[i›£2\§¿×ôÚ}ªµÊà-Á(×aÄÌŒ W„v¾•ySá‚tÅFÎFipöñ‡ÎpæõÜFâ‰f*<ºÅ3í €gü_Sá±ÌDE5¢ zÏ{Þ0fV™Ê1K5ᶆlÕ¸é` Œ[n¹eø„Î9,Q¶rÎN;í }`o€5X„Áa{Þ UáAƒ?. 'ŽÝvÛm¥YØ*ÉB0lîXÇ †Og•ïó;‹‡€=×Û 3Gë¯Ð¿ß╨ù”MpSÎþѽ`ÒÃ)ô}\•>Dßs8Äó¸”L8u‹»ÿ :Ó˜“Tî{T®‰™ýƒÉT3ªÕG¨‡TÖO·AR€8Ýà×_ýpd+³cÏ,Ûi@ØQb‹ôܙШƒb¹wÜ1ˆ¹‡%Έë°vŠùP3=ÞB„„Vã²P‡ùÏñ#qðàcDUãõ2¡ ápœU¿Ïï-ÌdY;ÆYP'7/»xÜfë|?iï2ÐÆmõoصÊïɧ"ùô¤£60¸sÏ=7x–:M°û}[åfÿØ58î¸ãÂä„ôèÛ‚fl¡ðÖ¿äô¥Jy}gª€6(æÐNÔý­òTXÒJ¢aÑðQ$òŽ€a ­òX:/ƒî3ŸùL¸6q/_êqA¾ܶÛn»ðFŠå Œiì°Ã‹ÿÁ~PZõ@ 6¬õyë‘Å‘=àXæ1Œ LË׿þõ AàN˼œŒ®fý(¤â¼¥³NÍ,ú¨å*LuøÇé]— q;ÊÜa×þÁÙ?Ò7&?¸*D–úcüBùv‹-¶ߥTüC'ÉñqrkƒÎ´…ÙÿDþéÄbêUŒÚ[ü^…ÿL€¤ qºá¯»îº¡Q#ðö´všClUCÔ p饗†¸Ü©ŠØùBSž¥:ì°ù"]¯Ÿ¢H„í0ÂÁ`8Í…ò•ê?N@Ä‘·ªÌïC8¬vÝu×ñ(3…ñýqÓ–/¿üò0 õRNÕ\Ã4Úìí°ý¡jš‹õxÑ'’°+gó×Ãä #\¸

ìñÃìIlj'ž¶,SþÆ$Ïþ9§û“í¬Ì‰5zä+?^$ÔÂ~=…ÿWþ×ò8v $u𥇸ÄeÓZâ²CÈu /=ƒŒµ$Þ i‰àõ-ƒóuÉ%—$Í—”ÿB|W_}uÈñÇîETûæ)Õ .¿Ä‡-íJék7@«b.iMx_ú -c•*ÃÆãòi¦ò(iEK3œ¾ås[Ùd“MZ£¬aË[å{ב–mšöÅ£³-€#Ït|vKË@!Yc]%“ôŽñúæ7¿ʬ‰Dm¼:ñ“>QˆCËo-)ÿVÆÏy¹æškÂ÷Ò:/qÞ$… ñi—RKË’•ó5@}þ¹ýÍ JŸIúeç¦^@ˆû£®PÎøn[«ñÌ“5EÖ뙡 »ÿ>nQ¬i‰©€ÓO?=þkÁks¿ä‹3È—5õü°ÏŸæÚ½¦¸Ë.»„Æ¡ÏçIþvÙй°²cݲ¡À÷lEÊÛ“TK£‘¸}ù°¬AÖ²-þÇd¶m¸-5šùG®q/Ìþ™ñcOç³/†ÉŠgÿX×C@ÌSßY¶óÂìüðÃɲt³P¾­0}衇©D•|-_ÿ`2¼”|tûže7®¨ñY ð]ϱ¬­Vÿé³^®ãÌáJY/p¢/cŒ \ ›¡C‰.[Ú²â‘ö}ÈšÓ\(Ÿ~碋. ߦ˜PK9$Š](ùFÿsÙ¾ûÝï–e«‹¹¶ …oe=äu\fƒÎG–Ì5!ã!-iž‡:ó¬¯Nÿrû?í´Óƪ¾Sw÷“O>9`åþZ«ÎwÅ<…¸¤Ô’AeüœK"´lâéŒÐ{-儸tL{KË•ó5æžý‡uO}?‘³á<[N׆¸æ#íJwE¶oÓ¤>ñ‰O„iQ9I§ðâ¾C<»ï¾{e‘µó$Å©–Œb„ïÝa†É“Åç[mµUK u@§•Íþ±8=Yü*‹(«–ÍËšŽ•HØeË À\;0‘-ŠÐ†µÞÛ‚)®Zϼ' BË}RF BÄŽ·k›œ7Üv¤ð‘‚n¥å£~Xš‰Ð~eìœ+ëÊ}®_zUÿw¾t€Zå| Y›Á©âÈ €*iì**,y(Œ¥ÉuÜØµg>4vˆQ•u[XvŽ~׿žYÅUÀüŽ”C:î0ýÒê÷¿ã9çœs*ç%¼˜ðǘ묂P¶g<㕱¤|H ,ÐÙè‹VŽNH\®ÌÌ!c<ÔPo®³~m4þß„g¥•VjI4"v¼øOò½Ëä‰HÝ>cækëNÈÞ­5vçå£ýh¨73`ŽwØPK­!^mG¬4Q¯ž4~Uyf29Ëè”u*œ*ÌR€÷·+tˆ6±ì§&¶pÉ®e‘#×)¼ÅžÄ[G GNµ×’!Ÿ­ß 禄Y8Î>ÜŒàÇxk_x(ƒ3½:Xk axʆEˆ•1Ž™x¸MiÍ¿õªW½*Ô•öòתcÚƒ‰ÅP¹&íE÷‡XÙ®î²X·¾ãqLÛf$Ng!|üÎøÃPWƒ0mÝòâg”ËÊ„?ûÙÏ*çk¡<÷øÏKÆL_¤ô3“äTqÖø;]ßÖ®èÆ¤ÚjÔ’™àÐðM`„WíA«Û72ÈâÑùÛíbôÜ}§gïÝâ¯óÌZŠt!&\ýs”ö )&¶¶ÞzëP64”ë”w]Ž .¸ dÌx¥ÍeõØŒcf–r]qÅ¡^aò –ž¡ƒ[ì:®Þê½ £ÿÖ·¾5`Åz}ݾÐù¾w–èð³ãÎí³WÎü?;-Ö[o½‡¦ÆA¤UòÔ+¯ž{²x¢°™XÍò>ÓNm)À^íJwÅVhÕ_ñÀš”O. Np+á9Í~¹¤c¾ë]ï ù²Nñ ê™m{f¥s½CòUóÒ/¯Uÿwzˆðclê”ɃƒU­–UóX÷= ™x˜ØXŒÌúºåÝx)Žõhœ1®[7ãú¾û·Išá©‹Uçûާsll½EØãUg܃Þ{I‚1¬ÎvÄêγÿ?êÛç)¿yö“èT–<^×sªôš Ð(*¢-8a°dÐÆ=:m_ ÿÉOåõL2|¾¥ó¤š‘Ø{ï½ePõ`ƒM> eó±†ÿyúÊW¾ê×xU®ì„/ºL³Î‡_ýêW¡^i÷(µuÖ]¿{m ß¼ûÝïNXKã•qB·A‡…²Ê8Wmœ:qôΣm¶Ù¦²²¯ûÍUW]ÒGb“bœ‰óf¦dúGž$†c UãYñO1±Nh)À.íîû—&º±;¤ŽË €KVô†î¾ŒÃbkíÉ Epš •ÇïqÄ!žTJ9^#Ô9!y å%å.×—¿üåP.ó¯~×Ô%7ÝtÓ¢”ظ<™˜›€-]º4Ô«—¿úÕeü?ÄÇí-£¸Q·O×kS¡Û‹Nû 8ylˆqäÚñ°ü‚ë‡›ó¾ÆÆoòb&bô»}ãe BX»rº‰ñõìÿ¿ïs”—<û„IvªHK­ëkÚ ¦)€eS ª‡ríZvùCQªvPo‚èÅâQâÄ["±dÉ’E¡kÄÎKÝ-”ÛÒŒ]wݵ¥¦Ž»ÝVF8ÍYfŒ·ÑVa¢­ýÿ⿸…”ç¸GR™ 'â>ã7†¾Ëݺ[$»õyK-‘bVuÆU¦ÂC^<1èÿ Ï`漫EGœË_55Þóì?X/Òwaò¨|g7ɨ"ƒGáíÆÐ¨ã%:´&tˆ{ð…}I¤Mì0mZÕ¦; ¢nâJÕI=Ëb‚ó`ÐÆ¸ñÀéy«˜óãUåÚâÅÅ\ pY2ÐjÉâdh§ƒÌþ©oG<8cÛxƒaôiìƒP^3±UÚz¯wbÃc0¸~¸y\±­†&Dÿ–H}ôÑ•ò^ìǓ»ôù“…Sžý´8Uª%ßn·F˜w k0{0Ž%öÚ¢ê®wd4z½­*…†.ƒ†×ZÑ3À¹ümŒ \.¤žÙzYŒãrŒ`–Ñ—eV·Ïj#»NëÖ¥·¼V%d]+dL§óÎ;/Œ'f^ë`Ôí]3ÏÇ|(¹Ûc/ü?ZÿœYBœ©';^žã쪓^ù­ðܳÿ½TˆžýÄ´8U¨¥kEÁk>Ñ£á/Ý9Ž<òÈÐ1L°…e2&À–ƒpÂM—ÿŸz§‚€wܱéõ¹.¥z¸Ü:¥0`k\êbmK‡HVloÁ˜uM8ñC·™Ye ç~ûíêÓÚuë÷ýN®s”SºèxÝ–•^‚•¿±ò Ê„U &9/Ö-´ß9ÝBK*G gäÙ?ÆþJyF®³›T©fNl æúÚ·iwŽûî»/œB&øJe3®Sxå!®Ûo¿=d¼*ÁÚÿýC¼æ7l~< 9óÌ3kå#ÚÆéÆlÊå™CÝrYAR<\Àå˜EÀíö / õç¿nýñ¾%nŠÂ9îmm\âðöH·×ApŠ¿qÿ½øâ‹Cûaæÿ¿óï„új‚ø{ÝDŒœ¥Á[—<û„itjÝ6ül]ÏY¸ÐRWÛÄæx‡ÔT¡£u†¶Šõö·¿=Xý£&$ÝÊäÿtJ`ˆ:ƒ(Yuæ±»­ña¹_>ºåm˜gÆšŒ¼ rl¬ËäÁðSŸúT™%ãV>hàÂiÌàºCñÏËR1sëz©ZõÎw¾sê¿qÂX¤"ºÖ¨zÞˆóÁ¤Ãu2h}ùûÎÐJœ< ÿ‹”^žùÂ4;ÝÞø¡ö8Þˆ ¦tPè,žß²ó {mbWõÀoŸKŘ˜cÿÀ>}¤× NØ&S‹‚Á×LË vÆÍ÷©CÇ?K €ËL½í³Ï>¡ÞÌÔRoî_Ó6û7N¬òɈf–ÁÉß ca} Úξ[ûv>Ø-³Ûn» ]_ÎGgè1 =‡~yê–ÏšÏ<\[ù€ÈûþbZ*ØÊ€ÿG×7Ëã¼4w—è×Fçr—ÍëmÂ7 ÀÌÛ¢üŸþô§!çU:16ð_ÿúׇR?s¹þõ_ÿ5`ã™Ä åA iƒ&µmÉS–¦Êãxg…p]a¨‡úáÔzL°€ÅØ’ºÇñ'ï*2c:H›Ž¿1ÓñŒøàÜãô}í|øl$~^ö‹ãæ:Vh´^ÓByrÞ =óç˜Èg(ß0ùÄ?€˜çÊVxŽ<ÎÜàÜ]Â_7âÃ?< v©ÖïTO%`"¾ï¾û–œ³ÓíVÿçcuSÍ*¼PǘH·ü òÌeâp”]vÙ%`céHŒUÕk¤5V(´]tòåtÉc¯oç,0&&×^{m¨#Lýšy¬Z7ñ{n»Ø¡ÇË^XOÊs—Í|›¼äà«+_{¼`€stÃÆÿ±o=‹K΋CK$Pí—§nù¬ùÌãýžJ⟷ýĬ8U¸w<_×j7s…íÛ4;2VÉÖYg0èyÖ.¼K">ìµg¼_úÒ—BÆn·Rø?——¼ä%!624l>,Êå°œnùHýÌiÙ–ñ‰¤•yØSïôVG½e;8 ‹5 …ü>øÁ–礻lÃÂØL+`œ\>êÄLU·v\å™gþð‡‡…¬¾w[@ŸÇíÖKQUpYècVå°×Y“ûýÉ«'ÿþïÿêÁé6T)žä}Aigâ³ìÔÈ,xC»ÁY<”¼ý¹c?ôÐC­·¼å-0 ;ªîBœV™©/œÊKDŸúÔ§–¸ÿºa,ŽÅ´²Óó}Ðuãw€/Ç€õ°uÀò’gU–ºêÔÃb¿k̾ùÍo†vjY·v¾ï-n¸aßÓ;ìﯽöÚ!VþëŒw˜{3Ûm·]‹IÎý ¡zðänWå;+þBvsjtÏ¿·ÝøÌ-&o‹î`­™X«.†&Nqž­bô·Pçò>Ö3ÕÀã³¼9C&ýò^Høc¬¯¿þúÛ ˜ÞvÞœ:‡]—ÍXÖ)†¿™À¸£<æõcÖzÍÆí´îõòË/êòä“O.1¯ƒõ¸¾kÌÜVÁªŠ¨~صp{íwØ•Û!õÆŽ"â6³Õ/:ÿÇ“KÒ\þ†êÇ“ºo+Ÿÿ¬ôÙÍ! á¥€Ú ÐÜb#íÑ qœrPŠÂ¸NåQ ôÌ“ĸ^Ìù!Þ&ÃÎÔ\38h3/†s™™-’'ÄÇ;ƒ„h {Yáo|cëç?ÿyY<§Y>èsaü'p9(.f¡1Ë ¶‘ØÄë xóÛÒûßÿþ’¡ÓìóØþí2p¦Åk^óš€YªY·'¶ê´:ÁˆŸ[w&ÕD ³¾]L‚puûKgÞûÜ[é1Ãó”€lñ ²{5 +Îí_m€?üá­-·Ü2tx‹Ä”£pŸ"„H±Ïš¸ú‰áÝ =Al˜bBÚHãœV¸Á78sÏB†ÁÚƒqœp ¥‚`r:“Êÿ¸N}.C\÷Ã`Ì·Þá‘ÖUO­AÓJš„·üÅmjܬŒ»ë®»ö=/Ämí®¤iÆa˜ô»}ë±Àçm8ݤ@Î̳ÿý”Ÿ,ú„ì–E@mƺ+èzÎ<–$çóÛRº;˜l‹Rn‚O13u\=“ Cûp §ÝY?O½Éìï1yL(#åÅ9­Î<4uïæœsÎ ù€áÆñ%d/»%&¬³bù1._|Ý­|ÎÛ¤1ä;.Û>9ÂØ˜ù~ÐÐÛ‘Þ,ÆÖÒnu–ê™ñ³žD*âo `Þ3çM|Þ§o jJ{¡:v{Øzë­[L~pnû©ðìˆÇ’Ü(_Áéÿ,þ79œ€‡—ön7$sí*Í­;žOùB¤œÚĦJXΞÞüæ7—kñN»WI|ˆQ¬ôF\ƒz¯CްóÏ+Z<а3rÀÅ['-ßQo1V{ì±G몫®š—‡^˜;o“Ât~ô;l‚,À!•„Å ,ñ"ÂõÂqØpãrÜÔ6Œ„¶}Š¥×!¶pN«?·äoRõ â²÷Î îû1$yðÞ“7ÂÕ•nýBv½PC)¹C]§Ýðe<ø³F§œ5&z3qÚo¿ý:?ˆY×Zk­§a¬µQ&{ÏnŽ<òÈ6´£ \6Rå ò…ÀZÒÎç0!ÊnÆšx0‹|õÕWÏ›é8/¾w 3ߨ‘?ÿüó[¯xÅ+–,7y–7 †þ6&þÓ|Êßë^÷º€Ÿ¥H.ÿ ¡Åìý¶×™øCÝÇcÌM¿ó;Ú…—8¯¼òÊÐévƒ#€Ç탔Ÿ,ú„ìú# i3Á,ü¾Ý@ÛàÁŸt>ô¡…À„R¹ ÷©B¯ëaøÇ.NßÏÜ9m¯mþÔú,3àœ–Ón:ŒËëàk1s*¬H½nMœH°ºè.'å7ãÈ€—óç<ò”Âa2Æ ¦'Ué¬*6ë·Iã6ˆ!#Ê3ÆtÐ X¿íu®Sl€¼öµ¯ yˆÛë i÷úÆãÎi§ª'.CõeÑ?Ö…¬ÛUNî”Ïì2½P£ñRÀÎíêÕH{u‡@”ºÑF…ÎhŽY¹,Ù×ÞFõ…/|¡,‹Ó/èÂÄYgÒw'6 Ï2˜ãœV¸Áåu™mÿ€rY«Ø2ú{ö¬ÃXÄu ÑüêW¿Úºí¶ÛÊ<¸È(jò-vª¬ÁZg$µ! ꣳN¸¿ñÆ[´›UW]µl“.çÃå6Œ Ñ´Íü©k·½¯|å+Iû–Íê‚ÿBbv§3jÿMl÷#–FrÈ!næeùËi/,úÿ£¢}òEÿ€]=Ôx¬xz»}Z¤”¶¹¶có€+¦Ø³®R—ƒµ¯Ý)¿öµ9ÛGzÌÚÉþÎñ z?8ƒŽvù{塉ç.÷/ùËÖ +¬pJUƘžNƒ½Öé¸aãìCŒX?¯2›6ᆰï†?Ïî¸ãŽÖyç×Ú}÷Ýçµ#kÅ–~ÁMÛñ¾´aãìm©lÕM¡÷Ã~3N ™úu›g¯¿6›hó´oCf§S•c‡õqÓ{)YôÙÕG@Ñ ÀSt=wÀ½úo¢FÚ5?úÑÂ`ËÚ4ŠAÊ}RÏàêÁ‚5\œ†8c~Fç}Ó›Þò`ã°yòàÀ¥?ýiî@F§ç¡ékcÎ Ħ” i‡w- [Îø{´³!žqü(H±,ƒqÞç=ôøÛn×1€Ùc\Œ!×ö”Ó¾¦¼Ë¬ÉÌ™gžÙÚm·Ýæå|zO?yì–§AŸŸ¥Sì…GB‚sýôÊó$=wYÌèU*&ÊØ!UÀÅíÀÅÏ=ôÐPþnÐzëõ¥|ôs˜HœËïü4šøKù‚ø?ÏuvÚ¨ñx)`³vcm” wf]Êpiü„딞Y¦gâ6ië´Ûe Ÿ1 ;ýT’ Ï<¼'˜ãA*ÎG“×.#[“Ž8âˆPÎxkŸË2„ C·Â3^Ç]‡á3°é¦›–ŠUqbã…ÁtÎ>ûìÖ{ßûÞÖúë¯_Ö³óD=ÁŒÚ¦„Ÿ§ ][Ò¤Oªs½T-Ó8¿çvÍùŽ»xih,݆⃑œž1‰ï9显¥^äÝí[·KþëgÄùKzlÆÖÿ3•6 @˜Äq]F` Ôˆ¬x\»‘šËlß6°ÞªL—3#®Sz^‹z öqÆh+WQWÉ«™ àâª9d—ÙeäÛA ÿ\«”ew˜ù" €X*S5ïõFo+rtìª# Àó Æ eNöx£ôˆÄ€äå¯nÌŒ õƒK*ªæ«Î{ñ²×\ËÖÖd=q™Èµ}S+ýÁTP߸8½Î{ŸFÚÔÌ&Ñíx¡‰EÈhÚëim£ö—Eÿ€Ýð¨Z‹ô¯týãv›m” ˆ;ðÇ?þñ@ôS)á ‘yL„¥<_HÛÚ2¢aÞM5€Ä[„.»ì²¯Ójc=² Æ}Ñ›m¶Y(+³Ón„²ËQß[ #ÀÌ}É’%¥gkÞk¬QiÖ†à#¶e[dS3ýݦIßûPÙq=Œ¬òLÈå1ñu¹c<¹Žûn¯%§MñN:é¤FÇÊàq¡ª~Q"Ø=Vy€øgS¿‘]Ü ®,?wòKÃúî¸Ìè¼›jÖ Tæ1ÞÌs¯Yˆû wöÎøêÞÇë ã`ìÅeG÷ÁvÑ)ø[bR·ŒM½o&`¡øY»gV‘§Î¸†xÀ8°ä0ê2ų~ѬI›w»W?› ç¶tÁ„>—J¢„Á Û¯à¨eœÓ2p1– F!ý¦êÛK >r˜|Äyp¾‡žùÿDñ>Fe kÿ„Ùe’! Æe}€íÚ Ø ¯}›>p‡f¯îÞð†Ð­<§‚Í#âÃÞ[i‡xzæÎÌú1ëż›j@³‘ÅÜàZ4öܳ<‚‰_Ê N©m [w0èt0{=Ïá£oÀ _…Y6/ }£éµ~˜>ˆ¢]Œ·ŸMzè2yé ¦+…¥?êÑý]!œÓ2fî«ÜÛJ#Ø7%ݱT ¢NÛ¡óÔ@èuì ?_m/Ïþ!»fP#³>ÀñíÆlÑSm{.Jwì»îº«õÒ—¾4¡TšøBi3ýfØ,HÍ”˜¹Ùf›mZ(KáF0ˆÌÝå—´>ë«>.ܘMÇæM;±Ì÷·-ˆ½ ¸°¬E{¶3ƾŸ†Ðýö¦¶™_Ê‹ì‡ižiñ‹_ì UŒ§m з`‡I·×·®Û}öÙ'èž©8]3™æ¡'a;(oyݲkµYo D`NîÖà©î#LØ>äíaMÍBc& —N€óƒ¹gî©òãÁäÀ,g5#L ÷2¡ËË,Qì¹çžå@ʲ@¼„¡ÖWþ7ë×hƒ»>Ábûí·/õLÀ2Æ•ûiq.’¬UVY%´Ÿ…1l›ð²Ûa‡VÂ÷øúK_úRH›%—TJ»ù·D‡-´L p.™Áf.æö·Z'(OyæÙ5€Ú²¥/ÐõÜ‘VjóÍ´ñ‡cu§²¹X•´´¨ÇuJ¸ãóî–xpq~âCDRÍp,N<úè£Kâ´Ë‡#¼ˆËOÙ/½ôÒRVº,˜³a1ágù„Óîlïj[ìúlªé¸_°•ÑÛý,ÙrŸ44¦{íµW‰eŒc|}â‰'†> ƒÚÔÌßåzå+_Ùºçž{¤.Sø¶ãµä•Ó¶'f~Ø2Ø{ï½Ëz¡˜6iI/U]W‰Bo¥¾XJÄ·üà[÷‹úÁ'n+‹QOM§—ÏçfX‚UÓ…Þ‰uº[·? ?ùdA+ .ï ÿ¹?Òn½õÖ­óÐ4Ίߓ¬(ÿ™øBv‹‡€¤•ÿV××·;€5SÛ·ÍîtÿñÿQj!R>Kqhß"?¬Ú9ñh{ö©f?¤ír±îŽ‹Ós^3$?ÆÂù〡¥K—Î;9Ïkã0UM)e¥¨ï8fùía¶GRñÿ[o½u +Ž(„Æ<Æ­žâü¥ºŽËø™Ï|&`“ŠÇ ¹èþàâvæëßþö·%Ó™ŠñˆëØ×&þÜshYg~ƒæ~LüOWúÿlç ²[\Ô­°Š®G¦H?óàƒ™W¡|S:ÌüXS$®ùóŸçú£!ç…|üñI˜`b·§ŒÉñpv~ó›ß„ÿlÌÉõAe÷{÷¹oJdë4û…Hm0 „‡ ¾wyÄß²}ê©§†ÓúâÙ>eî,¿q˜Æ0nƒnó©pÜö³Ÿøbl}Í!VV6LÅtÇuíë˜øß¤­8ç!Ü4ûcâ’y¼ò•þ"»ÅG@ÑLÀQ?h\)´<ùxQ¡QnÍã:µ÷·ÿþû—k¼œòek( ¥ÈClŠ©Î醛1û‹òŠT€e”·¼å-Ë`êp˜…ð†È3«7¡G*˜âá-]߯¾úê­÷é–a(ƒ™?CÞYV?Ÿæ0®ÛÔ&~-ñ¢~ò ÜÍoën÷â•VZ)´¥TʆuϽ¥\ßxãËä'ÒzÕ«^UÖ›•.ëÔ’¤ÌÜÙ{Ïú,„5z$ hSg&òk÷qZ|ûÏÿüÏ­8 …•9ÚT¬Ìç2t+—ÿ›ö0nã>° lSHpâ6ÞÉèÆéÆJÀñ΋¸.S\{÷ÁŠ+®¸kþÖø§I½JåÉëþ€Ýø! Êš”uŽ¡ÅÊYt5w×௉ÌÏþó’˜Äb;!V>Oqí3|Ûïg€²wQS31ñbéŒNwÃNlœG”Qð²½vvTe%: ì0H#9ä@ìQ`¼ýöÛƒ_7LicÝž»³Æå?á„BŸbi,…ÒkLü;—ºât‘ĸ§2ÀåøâÐ}|íµ×.u<<ÎŒ ®‘ šx«ò•‰? àJmõ¾ÍŸÔ@@ Vãñ#èœ4Ö³¼NþÏò–oÜi(4 ²@Vè0›B}!mÝFÒÖÌ£á qëèÏBE¸VÙC(,BxÔQG2^RˆY(t>xx6ÌÊB†GB"ZÅZk­ÞáÞiÿ(¾%¿xêËN'º`*E»BböBëëþ«kHY‰C:…Äô…ô4‚—x¿QH Ph-¹Ð:~ð<×ÍBR‚ðœïH_K]ã§=áHgRpíZDÁÚ8hÍ¿Øyç ÿBÒœÂX š”Ûâ¿þë¿Âçbl ™ýuKzî×ü©å†b§v*è<×9!ƒ&¹àw”KâþbóÍ7/Ž=öØÐ^â|,øqš?ÿ¢hh˜ïöRø?i¢Î±dB@ ÕR€Çézno$Ì(_øÂ†™‚-ˆ©Èá>ek*õ«_U‘ç\çl1µ$€Ù’„PlØiòåÄ„žU‰9 åAë>ÞWß«¾lti£6 g'°6Ïv0ó¸v¼U€àÝÎ:«òݬ¼·+kû§ûÇ R-×!gcؾýŽ^íbØçÖóÙa‡ZD†s>FTß6óû•%8¥›'²#‡ã€«=K×ÿ)™N€;+ëóeZaý—° Ϻ³…=ôÐÖƒΘÜIP¬ÈÀYUĽP~Yç†XòÎYg5‡²~ãÁº|8Æίͳ›$ç¼Ã0óÇ9.ãÃá0ñÃì³gà&’ÝúAgñ®«®Â¬ý\ý9Mîcc_^“¯“FÕw‘(8~¤u(ªâ⼄ÍþXgêZ%óÿ”w–AÀuv‰B@×LÀ][¡Å¡5ëÜyéÌZ‡/ o§1º Aô™—`|š ¥t^¸ö)e()ZŒ=hz|Ç൜Î:çú¸ãŽ#‰àâ4ýlCía€ÎýøãXÎIË“Ûbðw¼ã¡}¥šù[bF›½îºë4¤ç4ypÚi§•ý2f†é+ݾe‹­—!â#†Ý.GTo&þX”z¦ò ñﮜŸÙe&7b…›ÉÛŒ ˆ;ñ)§œˆn¬M/Ë&Åu¼û Ö ð¬°GZl_KqŠ`,à¼yψãÕà[è:Ê ÀøÔŒÛÍwÜÑBÇ‚¶jCXÃö3«ôÁx_½ÓIJßÁÒ„ð³Ô5l𽾋sÏ=·¬·ÉòA³^½_ɼ@yÍIJ›Ô¨- Ø>êG#ÓV‹;óE]T&Mîfв-lÄc3_|q™‹ñUãå³A®=CÛc=K)ªâj—®ŸÌTëɷ¨ ×J™‹›hÒ&ão,![²dIØrIYâ>¡%Û…€áÀhSü}Êk—8­@~\~®GàLüYÃZWyÉIJ›.Ô¸­Ð²O»S!)àŽ}íµ×–Ä9Õ¬FµµÌ@ÏÊÙKlQ'å÷lÇG ó},í_Õg.Ӻ뮻ÆKÚÕ[=p½d :fM¼éz nmk-Ûs,ѪÚ»½gætÇwlik(BlU‘%ý]Lœý,eh}Žl¾ùæ›C^âò‡ÍÿÄ’P¶LCüG²eš´²ËŒ 5ìØPÐÁí¾5R&€4MxmÚ$-ƒASʸ\ŸçúÌ3Ïl]{#Ûç 0Ób)€ÿS |ñŒÍæTIx¹²¼½.œ§ÌôB¨ùçî¤Ä²mosõ³º!Œ°QÈeKÎíE]Ÿ"ˆ~N*iX·|bdÊ òûdÞùþû‘º?<.„›ÑüÄÄkå5Ïü!»éE@ýª<ÁJׇ´ûÙ¢1l×ûØÇ>:–R(ä©öÊÁ3¾f½Ókýïÿû[2zŠoñ'»6ÝtÓð­g'ñ÷ƒ\{ ‚o¿ño´á^”Á®L»ÛEfº¡2ºg&þhúsÐíi”·YÒöü Œ-V¹?úè£Kq¿ëæ÷u¯{]øŸå´¿MÒ9X‹x9¼Èåv8:ÄK…h’Ü^ùÉIJ›~Ôàc&à°v§cldˤwz› ep“¦EÙÛîÙ‡˜Ä»È³‘}öÙ' PˆKS¬¢áì21£r ‰A–Ð FçÀÝØ#Û~ûíCÛƒeÖ®Ñh(f½ öÄ[T]B˜R§aÝߧ̈́﷿ýmg¡,ù ù‹xæo¿y«Ÿ*&»A@}¬´j¥ë£Ú}näL€?Ò×åµ%j,CpVŽòÇ„‡ÍÿÄÄ•‘™&þ‘Ýl! †_Z·ÒõÑí¾·(L€47ÜpÃ001¦˜©V»pv X¹j«­¶jAüb¯Ãzß+®ªÏ=¸#fµžciHœ‡Q]ÿÌŒq×7¸33wûI­„Ê;nc3`1?í±ŠÉgç¯nȆuaöÜsÏÖ]wݲâòÇùÁuLüß©²â¯tËÉϲËÌ jü1ð©v'¹NézP`2ÛöT a:×Mx–L”‰ÿk_ûZ©Ež°‹îtSI%Ìt/§¹YËå'ÝQ»ÌŒqp6Öè `î–v@›H±×†ÙšþÛn»m)i¢t²aÒLÅl¸t†ñþ~Ž,¶®Í"µó˜øï®¼fâÙe46ÄLÀ¡í¡}€‘ên<8œzê©%ñÅ™ª±òyªk´žÍh¼ímokÝtÓMmZÁPÊzë­ÒD”逆I›e—i·Ýv›7P›@”Á…ÓÌ€æÀŽÛ6º':-³lS)$]0&ꢲ¦?%ºüòË[/yÉKBz¬õ{ÇË0mx¡oc¦š#žíb ülaLüK±¿ÒÍ3Ubv¸áGºC(|¿¼]Üyü¬ÑÐĈD(½})•b”ª»+¯ÓófP­pï½÷–³5ôRhgwJ0Jd7êÒ˜gÀ5.[×'í ©Û`¬‹âgƒ„±T …Z;öúdz~‹ãI£Ê7l!´2![|o¿ýö0psÞF²¤i·­Êgþ€]F õ’ØNÀ^î5 … ð€q÷Ýw—šù tÞʧü—iÊkÖû­ø‡Á¶IÙxâ‰ešñ ;Lú±¨ôC)-ŽrÐ4Ö™pM§ Mø‰ ýÛ½`¦žBäO»‹gÛ±^É÷¾÷½Ök¬Ú+ËWnÓôՅ¾Û1Ç›yŽ1HƒjåXLü‘dn®¼Cü³m€È.#Ð u˜ Ø1êjîLÑ£æ/=x°~è­‚Êw©XÄui€5—‰Ÿ5Ìx ‘¦ÓŒ_?$ŒgN ÚˆlíŒï›3Õ˜yCÇ#>T‡6“b‰#›=Û~ó›ßÜb!ŽY÷P¶ÑTR†^í^q?¸ä’KB>øEÛ-›áƒ}8Ú÷ÕÊ{&þ€]F ê006üæ¨_- `âD>~úÓŸ–k™ :ÖfV™Ê/å5ºl“"NL–r4*ŽmTozÓ›Âó”kªñ`}ÄG”g fŒ÷)ã΀áQ‰xî´ÓN¡ c’ʦ|”õ'?ùɲmø€+Ú+í²éY?Ò8ëì¼óέÛn»-3@Ã#Z;ÿßéË— ‹Lü!»Œ@Ôy|€Ð+uýûv7tçjߎ&ˆ´™:è 0¨bØ'=ª|áyÊ-Ï´ˆË…>X賟ýl™^ª%,¥yÇÁª«®Úúîw¿[‚ q1±.&¸pœ™LcH H‹N>ùä²mÐ~Rm·‹%SØÎÀ±LååÚhª¶¸P?råŽðõqÐ127Úfü8”VUÞ2ñ„ì2ƒ  Nd&຾EçN6w7Âßxp9ÿüóË–A1Õ+œÊxãkfpñ ýì³Ï"N¤~/¥¢bœöÛo½õÖé‡òá&^™¨"ØÅõqå•W–&¥‘¥šõ£xê#ÿò/ÿÒºóÎ;ƒ¦ÿ§>õ©²ý!³å?·ÉÔ!æºMüÙŸsãPÉ¡¿ðää'Šéé*w&þ€]F`Ô™ÂéX ÿ^þGò8w¶¹»þšX‘ä¯~õ«Ö®»îÀ”"VáUª×„y¾å–[¶®¿þú°ïÝï~wø†e /t~[÷žÁ6–>œrÊ)-n±K5àÓÌÙjaŒ?Ö?ò‘”í†zKaJš6·9$ ØÊÀ Û3~Ìþú¾©0fJY¢"8ÚÛP5ä’¾…’ŸÇ£óuý×*&þ€]F êT–^ÞÎÐ÷# ãÁçÆolm·Ýv`£œ”Ê|¯°ëÊðœ™~<+‚°Ÿ­¿³²–ï Yâ°è•8öÚk¯ }0ØÃ0™0Š ‡Æ‰·h{ßÿþ÷[o¼q¨kêÇ qƒÖqü]lkâàƒisÌ1e»‚e'@üM×±í$6ç;L{[åÊÿÆJÉ«ìÁéëLü F3)P犭¾;êªqgŒ7É@dF€“öbÛê£X¾óD´”¢>ð”ƒsJý”cé' úp!Ðd`6aË€îíµÓ«¯¾ºµûî»—õ‹¸…TÚB ︰ÞwÔQGµ–.]ZšøE4 q?v LüioHìÜß|¿¡EþH ·gŒSXnaæ>»Œ@F ÔѰ…oÿƒ<ÎrînÄ¿ñ „4“¾*~Æk¨BbÌ'^çG"“Ê uˆ(ßf—,YÒÚc=Z›m¶YÙnFÑŽ;w½`I0nWn+ó‘é•ïVªÞæ÷(]gÓ¾jDÙeG€Î&o½€Õtý+yÜ¢2ñL ãAguV9x¦¬rw|Í ÝD1îꫯ^ÎØR¯ÕÂT@¼Ì€A!”Ä0kcâg¡õÌÌ!3“<ð~øáe}#꙯¸þ‡¹vû ­®»îºez¬»[*0Lüý¾…©|Ò“žÒEçà‡?üaÙT:1)ÿݳ}K¯Ôõr*OVö„ì2‹€:¡™€'ëúbyÜ´6îvžIDAT¢*’x°ÂBk¨Â'øÔâZÇÛÆk¸žÕu¾“êéC¬‹À5˶&¸—¹'s¿™˜[:éć³ë±ûàzBôÞôûXRcSçô› Qò‹í Mòn“* dÜ–ºŽõŒNQ%,2ñ„ì2‹‰€:c`ȃ®‰sëÑ£Ñ^Æ:Gû¾æ5¯ ƒ)³é8Ì›&þ¤cÁˆ—!x~ì±Ç† cä;õYfh#.?¡G‚M}oëC?³þ˜8ó¦Â¦w²oÒˆKYr@Úa÷?[„0–*î¯|§|de?ƒ‘ÃŒÀb"wF]ï qçî2ÜÙ²+ 2S…B•ê¦$Ò£¸6#+î·ß~A¤û‡?Xec®ä=ÐÏÊ@',›œsÎ9ó,ê!‡Q%áEû Ê奪µ×^»uá…¶X6ÃuÃ'ü1ú÷+é•ï¬ìÙeÆ uÐR W×/—Ÿ3 >§ÀúÝ¢:92Ö|ç²@ÓvÓU_#eHÏK±a¢M7Ý4NïpÅ`R–ïØ^3½òîeŽM6Ù¤4ë¸Æ-¤þcfü1Ûç| ì8Ä v0†Xð›FÂוƒ®î¿ú:çâ~âg‹Æëý[Am0ˆüu•ý#»ŒÀ8" j½€§èúBy\l°cîÉ"üBâ3¦>¬EX«{£Xs%­Qz–! hñÀOúï{ßû‚Q—{ï½7Ô–yU”ÎÆ™p]w}f¹ˆ¹±Ùà\è‡0+¶2žŸOCH¹(ŸËòá¸uË-·”½0îåÃŹ˜CÌ¥}‚‚¼Þ¯JË.#01¨ÓÆz‡EãˆEzÑ£Ñ_ÆDK‚ïÙpà ËÁ‘uQkÖ ôòù¤_3£…hS¾x™{fÁñòHÝ…qbz|ZÕ ÝÀRã;ì0¯>Ѱ÷NŠi$üÔQLø9µ©‡¸ÅýÁÏ)ôøÀ„ámðt×û F3“€@ÜiuýFùßÈãÜÉçîñ7žõ õ|æ™g¶^ô¢•…º*3aÕGùͤ\#âG*+‘÷:3XÃCœŽkš˜˜ÐSw½Òb–Tã?øAëóŸÿ|ëõ¯ý¼úa¹¢HX…Ñ™”:ó›ïå9K?œ,éuþQÔUhÕ~b‘ÿ/õÉšÊ3"ÿÒæ÷Ùe2„€:0z{WøùËäqc±$0—•ùÛã8n8>ÊUpÍúi”P6f½œm€RXÝf °13ãÊóa¼ãê73|–/8÷Ë_þr0´òÊ+Ï#úØçÇ¢¢ð*ú `3‰Â3u믿~ë¼óÎk=øàƒ%„Ýêªüsô±Èÿ_•üß ÷°ÞO˜]F #0á¨S[/Žþ“Ñ36ÒÏ,7lžŸxâ‰óˆ:3dUÛ¼ïÇý¾®ÜX¼ìe/kÝ~ûí-–S ƽfæÆ¶nH|Žm}t7˜Í"Ògwƒ·wvâK}¡Ð£2ÍDŸr[ªa ^þò—CX¿ûÝïJ¸ÇŒð“/÷˜€=”÷àt].!úY'¬µ9¹u—,çêÔù"Ôõ|Vþ‰ò‘GJ0íDy#…ÖË•¥¢¸ûî» Í Š<°¸õÖ[Ã3‰ I ‡Þ gôG"ôB„¥±-VXa…B–ä ­©—^Ò…Bõ^ˆ¼ ® ß‹È/u§zè¡B3ûBÌE© îe ÇŸÎ ©¾“V{øVË!Îy/MÑ ¸J²QˆÁ)n¾ùæP2Íø‹½÷Þ» ”4 <[Þʼnûßv>èdTæ¶ÊÛª»ÐétíÿÇ$»9ŒÀШƒÇKÏÐ=çwÛÅ¢@?[´Y'ÞŽ5e4ÇW[mµrVo;ÓºŽ¬ /˺Ðõ¨·Ëa†Ëu¬ã³­ ±7K4"•ò»PY&á?Ú›•_ÖøÏ=÷ÜVçŒ?nÃnË‹zÖO6Ž“œÊã–gý1…nlØÎ)Äv"‹Dg×`ÍÌŸŽÿ^>Γgc5(d³œ=1»¼üòË‹“N:©8õÔSÃ"BÅã÷¸âÎ;ï,$³ÐðÇ ý0Ó÷,_ĸòdA¨z.g÷ÌDÁ“o‡TÏ»–pÍ»ÌâÁo逖‚ÔÁuãx¦9ð”âf!ƒV…–BBqµ«¡Ð1Ø…N m‡`Ëûø1rt$$€ôï{åwSþ¾ªz.¥ƒÜg—ÈL9tzù0B)\Cþjy\¬<÷d ~5¨ÎSt“x:h™c>UUUzÍ0¼o³‹ÿŸÅk ôavŽ¤Ï 6ö~Nèwg§ne¦q@O¬ØÇ{üà[?úÑ‚n„»kü´Ó1tñ¬ÿåï*C üº+.…|e—ÈŒ:¿|˜ñ+|¬üQòvñ ág‹v2Ü_ýõÁæ>öâ[ðh3hÿÍßüÍÔn5sYsø0˜ !Ú í‡vä8Ÿÿüç·–.]ں馛æõƒ1&üp#îÇlCÈŠ~ªÌì2 ¥¡]¿VÞ#ÜXmT¾æ9ÞØqnúùçŸßÚ}÷ÝËA[ÅlaS}´æÜy–}Æ n´ -#£D1#É;´'ŒUýæ76¥1×ê:Û_ÜÇà:Öéùžò³’ʬ¿4Î}vÓ@ñL]BEòH‰€¡ªì>T~—vÄcµS §2P~õòBIŠ[n¹¥¸ì²Ë Y×+¤œU¾«Á=¬ÍJ©0¬o³®Ýl" ¢_ ;ñÄ'>1è>x— h¼öµ¯-¶Þzëb­µÖ*$ (×ó;ÛÚ"GgðZÿŸt}€úÅ!äSy/u¸Ïn6È ÀlÔs’Rƃ„®7R¤Ÿ––< šc¥´@×cåœñ ìvleÓ){AqPFj í_÷_A¡ åA˜ÒW Ü–Ýt# 5ý’èSÒ˜è£È·í¶Û묳N±âŠ+–J}¼S ƒi&“gcèb%ÞK•¿·+¿W“Oõ‹¬è7†6Š,e`(OQ,  ;‚4à ºþˆü;ÚEki@;à:°xàâºë®+d¢¶›Bb]¿ör³› omå ûØô³›l¨´÷uÂb!+„¡nµTTj5Ö(¶Új«bÍ5×,VZi¥rï>/ÀLââ6Œß •Ìœ?  òü …”ŸÿÑý\ax˜ÝL!€™ªît…eðÐÀdäºÞ@1%¿J;…x¶‘.Ñbê6{cÆã7²jW|ãß(N?ýôy)cL‡Ù"ÛywÚÚÌ+üß@¬½òïþîïBJio^‰^ýêW›o¾y¡s'ÂLß{x ¢¥Hó>¯ˆºÅýäìù½„Á ܨyÖ3î20ã `˜âk¡ý0 TÄÑ Ø ØOþ±ò >üÿ°Ì]7ãêØñ ¼¢tn–57l èP¢âÇ?þ±ÿ¡”ƒ=¬²¬CõæA´(7jÛ=9Þ@üuÜð¼¼ ÙÑIa]ÿyÏ{^ÅB¾‰]7&1þ ¯é{^ŽûO]ï§6}2ùTϳ~€È. ððH—É ˆ€•r6¡ëç*šÃä7nG7ËEï5èCäµµ00,\zé¥Åµ×^;ïsLî2ÃD: “ ƒþ€åÌ{1ß$C¦Ù=Ý fîÒÄÌ[g"›m¶Y ø¬å/¿üòÁDr<«‡Äw2ƒñŒá},î'{ÇÊ£èwÊ“MùŽa…-v–2°Ø50%ék€¡-iEÒýV >.ÿLîå&fY`.»ÿB ð1‘à_˜–P$Ä> ÁUW]v<üõÜ•L䢄D]®ñªìª!QfùˆxÖïµ…3`ˆ”¦Ó1£éK_Zl¸á†…L­}™ê qÄïöªãø1¾î÷_®¼î+¬.#Ï*[¹\7ÆeÈY[2° Os’lÊ™†®Ù2¸¿ü^ò, tÎPôh²œ E·Ù!„œ%Ö•Q&»æšk‚„Âtå•Wv-èÓŸþô0ce·ö’‡Y'i̪3®&ò²Jˆ5„ž™=K+Ha8ð©›ÓéƒaýÅ=$1²þå!žØ-Tñ{p‹ûïR~Qòã0/?…Æ‚#ý.»ŒÀ2d`Hòƒhð)gº^Qqr¦Àæí¸'J?`!<ú; `°µ ·Ýv[X:ˆ·šqr» ÆÁAˆ)Û Žïq„öHP ÄxžqoBïó H ì鳄ÒË1«G÷âÅ/~q±êª«†(gbŸø;¥5ýê©W:cü<îCx¶ä~D¸†m *o¹,7ÆeÈY[d2°È0ÍÉk¢}Å˯Ñý‡ä_Ò.÷Dê´óÞ50¡áÏN"ä` x?ŒKgœqFqôÑGÃ3HFéÈ'„“õsÄê¸^:ÎÿÛu+'åã<åe©}ˆ|Пõ ™Åcˆ‘=Dž#‘pÌ.F›¸†èæœ?3ÝÞ™ÐgR4´û? rÍTáž•ü&´b#ÛÝ{Ïbä$§9uhPb É/ œ§ûót¿ƒÂäŸ%›F Áâqüo¢…:ç­†ü?Jñ†XÞqÇ#Iv¹å– 3wfüˆõ)?Ë Ï–E=ô$¼®‘‡)A#?Ƭ[&»ûnLI·o'è„ï1›5%?}*÷Ó—²ËTB`´£M¥,å—¦ÁR,©kôvoû'µË<5Œ@•:„pA¨îºë®â¹Ï}nXÛ†à!…ƒavŽ¢Ün»ífÚ̸yN¾Ì¸˜y!„"ž'ÿÜ3 .ϸÇDrAðYæÀóŸßë\ŸïUö˜ÐóßÏ€ƒsDÜo³®RÙ¿@ÙU0׺}ïd—¨…ÀLô Zˆä—E@m.^` ¼üÛåÿZ7Œ€€+®¸"œÏLøÎ;ïÙ®j–6Ø`ƒâ¼óÎ[fÿû\UŒæ¦ogâîÐÏg(ì$üwªìGÊF˜<«d¨¹Ï.#P š­ûQ~?#0(¼ÐJfY 7¬ë»å÷S|«ÊFžEbf;0 0SÝL£CI‡XܳÜQ”“Ù?îꫯ.µêÙÉ@šðD>.£Ú@)5°t€g3èóS1ž~püò+ CåŽÿýG.ÏúLvƒ#`±Òà1ä/3  Á Â…ÌdnTø lG(dÛàŽò“Ç1 ¬N%Ãj;Â`Þ,˜‚7éH—=ôhÜ£ˆˆ3ñm2ÝwWâ5~Ú9ÚüÇÊ3ã¿›/ ü °Ý?ǹñ0»ŒÀLå€:ùÓ# ÁÌG2Àéþ&ùw*H>)¿<m•2»D@ðqÂ>QŒ9ššО™ÉS´s´1? ÿ|õƒåï¦_Èëò‘Ï%p²Kƒ@fÒà˜c lÿ˧ΌÀ ºßCѾ@þÃò Œf0ñSá¬V™GV&Òekf‹ÑÈnd@Äi¿Æ` öÜ,¿¯<¢~öógÂ/0²kÌ4‹o޽&ø:Ûõì@E#ÀÒÀµò ˜ÁÊ™B¤=+B Cûß3òð á3ì±gÛNX7œêLGO;¥½2í—ñ÷ ù·Ê¿@ØBþþ<ãÙÌŒæœH]4v2÷éG¿Pžs¾'oE)BÖ‰’ ¨<ʲŒ!È¢†mn¿ýöÒOø£á ÿàžýìg—¶ NrV£·ÄÊí•™ÿÙò›ª ¼D~©|¬Ü—Eý³ÚRF\îÌŒðœ\=40šð®?ëÙ©òë)¦uä?'ÿy–˜Uá&BW@e™eö½dÉ’pÍN€Q9Dÿ¸W¾ò•` "»dx¶O„–X¡Ø÷)ùÕU÷Ë“?óŒ²[ 2°¨ç4k# ÁÒÊ‚0’×£G\&¿‹"[EžõÓoGl]+X-eƒè"Šßh£BÖ1Ä3 ÇRæyq«¬|Y0€0܉>íγ}b¼Lþmò¬ïï.ÿcÕ;º.aÕ}žñƒRvŒ@F *  Dýî×”?JþùØ¡`ˆÿßøáb_k¿}È‚Ž†x´4+o‰!×Ü7å%uqo»í¶-M¼Ø0LrúT ÛV\Ž[uC;\Ëm“P÷´Ù<ñŠAÉ׌@F`P4 †åø{=ûkùMåO“ÿ­|ì<`3`&àðÃDY:~aâ^~ùåCxÉ%—\d Æ'_/Œ@/¢Ÿ>ûŠü›äÿOG{ [ùâgù:#Èd" ·›Tàéz¾ƒüòhYÇÊ÷gùE£€f~ýë_¢¬]-é4Æèt½÷N;íÔÒ2@ŒE¾îD¿[;ù½žŸ+¿“üS㦬ûeÚbü¾Îd2Ðàk]yâV=ÿ{ùíå¿.ÿùNÇ ?ò¥ÏÀÏ:ë¬@œu8P# Ì…ô BÜ7ÜpC(»ÓîbÆï!øf;%E0‘gÉï"ÿ¬¸ùê>رP8§áÿ™¯3Œ@F`´0Ë#‚ídž¦g[ÉVþ—ò΀°“t¾›ìþC zÅWLÊèÔ¾–EÿçžË¤U…jë $ËüäF|ÀN÷Ÿzðeùm䟷`Ý»}e¢“¯3ŒÀ8! ÖÌÀcõßË䔿DþAùNg† fÀÄø¡‡jí»ï¾ø¯°Â -fíÂp(ÿ”§<¥õÿð!Ž“N:©³\³xßà?$P®”?LþÕòOˆÛ±î3ÑÉ׌@F`’ˆqÛ(³¯ÿž-Ïlïhù+ä!±kœ °R 2ÕzÎsžÓ’ážÚLÌÃ?ýÓ?•ßqjsÎ ‡ï§8¤®b‚#×é˜õß(Šü[å——Ÿ7£×}ï—=$_d2)A€Á^¥-lÌsíÿ ÛÉ/â®Q&€.¸à‚Ö£ýè’€ÿã?þcëIOzRëñ|`´ŸƒÁ³}ð1yL<íiO Lƒ ¾Ýxã[×\s Q7ÅÄßÄ‚þgùnâ|0`ïãuòˆC8…ò…ò™Wñ€×VäS8è|/ßg2ŒÀ”  ß³½nÒöwã 0¸˜@ß}÷Ý­¥K—¶t`OÉæ@èŸùÌg¶dÊ·µÜrË•"~þ³_sÍ5[_ÿú×[>8·¢ÇÛHÆG© ½—e¨‹n3{çè^]ü›ü§å·•_QþÑÍUÏ\ï„™èw”ï§ ÜÀ§ª:saš@ Mè+WÌÌ‘ó~$Ï3ˆmcýH»< è®»î*~üã^xaqÙe——_~¹’^ÖaVxýõ×+OxÂÜòuײ_ÝpÅ™™ñ5X£¿Ñ s¬ðÝ#üÏä9<ê'\«î0=ÏAðõÏwA’2ï…|“˜bzu¢).r.ZF`p "$Ì>ÏT,›ÈsÐË2R‚ÁSXöK¥*Ýr¯}qÇw÷Ýw_ñ—¿pôAQp¸Ï“Ÿüäâ©O}j¡¥€ðŒŸÎïË?š¿0ïLÉÏãkwr?‡ãßÊß(Cä!ü¿V¿W¸ŒÔéd‚¿ :ùÁ¬!€Y«ñ\Þ¡€€ˆ¸ jÞ@],oÂ5T¼U>†ã«ÌŒ¦ÁŒC•4†|,b<†_àj’¿[þvù_Ëß&‹ü­ò7Ëóì÷*i.ã¨+=$ØóB¯wù/»ŒÀ¬!0L5¬ry3K8ÍmcyˆÕ2ʃáå†~`p̈ ¾“ YÑO·ñ„Œ"%#üŸä'8Þþ>]ß+±ýºÿÊôG…=õ¡?-5Èľ'RùŒÀ|ºuØùo仌@F`"8–¼Xü‡<ÄB7«ý ¢ ×Ë¿_"Ñf@þ¿åËs#À±Î0•\‘kû<«¯„`~)#°,³:`-‹D~’¨@Ä©Ïö”‡¨T P#»M¿j€3îÏ®“˜pd ‚yˆÇ"wÂàoyíg9Ìd†C îtÃÅ”¿ÎÌ.ˆ’§¾¡eþùÆÇb3>_Û¶ÖÁØÅ„;¾ÿgÂn˜r˜=™=æ9Å)A@ÄŽmšy•Št¾¼ ܬô+3<(é½PX°×>èGLIçbd¦DoÙe2 Ð&þè\ Ï?&᯼®=@’ãô ÌŽ·?¾µMüaˆâÙÿ8å7ç%#è@ 3€äÛŒ@MÁá{¯¾ƒ@€=êÓîÌè¼Oeÿ–¥!Ó^è\¾ŒÀ4!0+¢Êiª³\–1CÀbo…ÿWYû¡ü?É{m|Ìr›$;08¬õ{Ý?Œ#b¼’$‘IF #Ð,™hßûŒ âï­+¨È—É?Q~™ÿ‹U¾ !úf€tŸ]F #0Aä%€ ª¬œÕñE@„뀬_¯\¾Vž½ï,ÀL‹3ñ¿BÚ´Müa|òºÿ´Ôp.ÇL!€™ªî\Ø&!dGLËÈs(Í4è Ú‡‘Aìÿ}ùW¨Œÿ­²©‡î³Ëd&ÌL`¥å,/€…Àµå.á„€Nâ¹g÷02_“_¢2þ.!‘]F`ÂÈ À„W`Îþø!17(wkÉŸ+EçÆÚóº{ÓÂA¾?¦rm!ÿçLüǾÞr3Œ@F`1€P:}] oåHáquè3°œÃ¸ÏfQ9ò¤Á`ä0#Èd2½ñ, ¦®_&­¼Ý¸10%äÉîl]<‹²)|”|Þ5Ô«¢óóŒ@F #Èt"á”g BúXùƒä’·[lF “ðߦŒmçrè:äÝ÷9Ìd2Œ@F "¤ñ’À ºÿ²|ì`¿ÂAôó[ÔOš÷Ë,A#˜—R‚Q£¨ùÕŒ@F #ÈdbÚDµœQë~5ù/ÉÿAÞÎ3r˜®S9ýXÌOÜwÈLþ™Î«®Ë<úY3éB ¯éMW}æÒL"°af-­ú°ÍN÷XÜJþòÏï(ïàé¯ö¯,së-‡!~ý‹ô!îï<ÿüÉò_U>îSȬÂÿ?º÷÷<Î.#˜Bâa ‹—‹”oÚŒ€èí#ÂöÀ6^W¹~üò/¬| w¿"ÁHÑEòç)Í«©Ò…Ahé™ÿ•ÃŒ@F`JÈ À”Vl.Öd!Ðf)ÌÞûÒéù³u³šü*òÏ•çþäŸ ÏlÂíuzˆ7ßc²+„·Éß(üñŠÿ¿–®Ípä‰H¾ÈÌ™˜ºÎ%Dé“tÑêùÌÙo3 HþVe½ÇËci‘ýŸä/ÏLÿ¿õýC ç¹vü0 ÿ«ÿól:ù�[d`¶ê;—v‚hkú¨}m¢­8 ö|ƒÀ÷ym_@d—ÈÌW Êxd2cŽ@›)p.{1ð%‘ÏßPå0#Èd2Œ@F #Èd2Œ@F #0‹üÿŽÓÊâ8É…IEND®B`‚l8mks¤£uqøÿÿÿÿ÷{Ÿÿÿÿÿÿÿÿÿ}_ÿÿÿÿÿÿÿÿÿ÷^×ÿÿÿÿÿÿÿÿÿÿÿû£" žÿÿÿÿÿÿÿÿÿÿÿÿÿÿú&Ñÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ¶(éÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÎ ×ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿµ’ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ^'úÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿã •ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ`ïÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÄ1Áÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿü8öÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ»Âÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ~÷ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿâäÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿõ~ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ¿©ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿô6Yüÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿä.®ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿw?ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿô´ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ}#òÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ× Tþÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿó*dýÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿôARïÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÜ3 »ÿÿÿÿÿÿÿÿÿÿÿÿüœA²öÿÿÿÿÿÿÿêž)áÿÿûxQ1‘:info bplist00ÔX$versionX$objectsY$archiverT$top† ¥U$nullÓ WNS.keysZNS.objectsV$class¡ €¡€€TnameTiconÒZ$classnameX$classes\NSDictionary¢XNSObject_NSKeyedArchiverÑTroot€#-27=CJR]dfhjlnsx}ˆ‘ž¡ª¼¿ÄÆostinato-1.3.0/client/icons/logo.ico000066400000000000000000001032771451413623100174120ustar00rootroot00000000000000 hV ˆ ¾  ¨F00 ¨%î )B–D(  ÃÃÿÿÿÿÿÿÿÿÿ÷÷÷c×××Öóóó„ÿÿÿ:ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿýýýFÓÓÓ°–––îCCCÿ†††û’’’ºÍñññsÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿñññsŸŸŸîGGGÿrrrÿtttÿÂÂÂÿ¡¡¡ÿGGGÿuuuýÖÖÖ²ÿÿÿ!ÿÿÿÿÿÿÿÿÿ¼¼¼÷÷÷c”””õEEEÿÿ£££ÿ___ÿÿÄÄÄÿÎÎÎÿNNNÿhhhÿ×××®ÿÿÿÿÿÿÿÿÿÿÿÿ#µµµØXXXÿoooÿ­­­ÿ¬¬¬ÿ›››ÿÞÞÞÿŒŒŒÿæææÿ±±±ÿVVVÿ„„„û÷÷÷gäääÿÿÿ òòòƒtttÿaaaÿPPPÿ‡‡‡ÿyyyÿJJJÿŠŠŠÿ```ÿÒÒÒÿóóóÿxxxÿ___ÿÃÃÃÈÿÿÿ$ììì—œœœöEEEÿHHHÿFFFÿoooÿÿÿ¡¡¡ÿZZZÿ¢¢¢ÿòòòÿoooÿJJJÿfffþßßßÀÁÁÁö‡‡‡ÿpppÿrrrÿlllÿyyyÿÏÏÏÿÿwwwÿÌÌÌÿÿŸŸŸÿ‹‹‹ÿÿNNNÿÁÁÁöÜÜÜÄ„„þMMMÿZZZÿ»»»ÿ———ÿøøøÿ“““ÿ°°°ÿÿÿÿÿ¢¢¢ÿRRRÿ```ÿSSSÿnnnüììì ÿÿÿßßߪoooÿnnnÿÓÓÓÿÿˆˆˆÿ¢¢¢ÿþþþÿïïïÿ’’’ÿVVVÿMMMÿhhhÿ±±±Øÿÿÿ$ýýýÿÿÿ@¡¡¡ðZZZÿ§§§ÿ¢¢¢ÿHHHÿ···ÿÄÄÄÿ›››ÿÑÑÑÿ¸¸¸ÿHHHÿ|||ÿãããÿÿÿÿÿÿÿÿÿããã˜mmmÿ666ÿmmmÿÐÐÐÿÀÀÀÿŽŽŽÿÓÓÓÿûûûÿvvvÿ===ÿ¼¼¼Ùÿÿÿ+þþþÿÿÿÿÿÿÿÿÿÙÙÙ®aaaý444ÿÄÄÄÿøøøÿŠŠŠÿxxxÿÿBBBÿ°°°ßýýýLÕÕÕÿÿÿÿÿÿÿÿÿÿÿÿéééž___ÿsssÿžžžÿoooÿQQQÿ™™™îØØØ«ÿÿÿ5ÿÿÿÿÿÿþþþÿÿÿ)ÂÂÂÒÿ™™™ÿ~~~ÿÇÇÇÌÿÿÿ;ÿÿÿÿÿÿÿÿÿÿÿÿïïïÿÿÿ8ÚÚÚµ¿¿¿âÛÛÛ´ÿÿÿ8÷÷÷ÿÿÿøàÀÀ€€€Ààðø?(0 ÃÃÿÿÿÿÿÿÿÿÿ“þþþÉÿÿÿgÿÿÿ ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ ÿÿÿDÿÿÿ ÈÈÈúyyyÿäääïúúúÀþþþ¤ÿÿÿpÿÿÿ+ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ?ûûûµÑÑÑõÿ:::ÿÿUUUÿ\\\ÿmmmÿ©©©þçççäÿÿÿ‡ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿjååå耀€ÿ$$$ÿ***ÿuuuÿgggÿØØØÿ»»»ÿbbbÿÿBBBÿ®®®þøøøÂÿÿÿ3ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿqÕÕÕôUUUÿ,,,ÿTTTÿ×××ÿ½½½ÿVVVÿ»»»ÿëëëÿûûûÿ¦¦¦ÿ222ÿ444ÿ………ÿôôôÒÿÿÿ1ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿLÞÞÞíMMMÿKKKÿQQQÿÖÖÖÿÿVVVÿGGGÿ………ÿdddÿÄÄÄÿÿÿÿÿ¥¥¥ÿCCCÿAAAÿ†††ÿûûû»ÿÿÿÿÿÿÿÿÿÿÿÿøøøÄlllÿZZZÿIIIÿ¼¼¼ÿ§§§ÿ{{{ÿÒÒÒÿ|||ÿýýýÿÆÆÆÿ___ÿïïïÿüüüÿsssÿbbbÿ>>>ÿµµµýÿÿÿvþþþÿÿÿÿÿÿÿÿÿÿÿÿb¼¼¼û@@@ÿ{{{ÿ444ÿÛÛÛÿnnnÿÊÊÊÿÔÔÔÿcccÿÊÊÊÿãããÿ___ÿÎÎÎÿÿÿÿÿÖÖÖÿLLLÿ{{{ÿRRRÿðððÕÿÿÿÿÿÿÿÿÿÿÿÿ+ýýýÅhhhÿvvvÿYYYÿ---ÿ{{{ÿrrrÿhhhÿ444ÿ(((ÿlllÿ^^^ÿ...ÿÎÎÎÿÿÿÿÿÿÿÿÿsssÿ‡‡‡ÿ@@@ÿ¸¸¸üÿÿÿ{ÿÿÿ ÿÿÿIïïïןŸŸÿ"""ÿLLLÿ///ÿ444ÿkkkÿqqqÿÿÿÿÈÈÈÿŸŸŸÿ'''ÿˆˆˆÿûûûÿÿÿÿÿÿ666ÿ333ÿHHHÿÒÒÒöÿÿÿ‡ùùùÓ–––ÿ}}}ÿ444ÿ”””ÿGGGÿKKKÿrrrÿ]]]ÿŽŽŽÿ&&&ÿ+++ÿªªªÿTTTÿ¹¹¹ÿvvvÿÅÅÅÿÎÎÎÿpppÿWWWÿŠŠŠÿ+++ÿsssÿïïïïçççú{{{ÿ´´´ÿCCCÿ¦¦¦ÿaaaÿ‡‡‡ÿPPPÿ’’’ÿÿÿÿÿãããÿÕÕÕÿcccÿ‘‘‘ÿÿÿÿÿ···ÿvvvÿ‘‘‘ÿÅÅÅÿ```ÿ   ÿ777ÿwwwÿìììóûûûÍ———ÿaaaÿ333ÿ{{{ÿ555ÿÎÎÎÿ………ÿªªªÿÿÿÿÿøøøÿ{{{ÿpppÿöööÿÿÿÿÿÎÎÎÿEEEÿLLLÿˆˆˆÿDDDÿwwwÿ&&&ÿºººýÿÿÿœÿÿÿ=öööƳ³³ý...ÿbbbÿPPPÿøøøÿ›››ÿŽŽŽÿþþþÿ………ÿ^^^ÿëëëÿÿÿÿÿÿÿÿÿ´´´ÿhhhÿ;;;ÿTTTÿ;;;ÿGGGÿtttÿÿÿÿ¶ÿÿÿÿÿÿÿÿÿüüüÁjjjÿ’’’ÿNNNÿØØØÿÛÛÛÿ___ÿ………ÿTTTÿãããÿÿÿÿÿÿÿÿÿðððÿvvvÿ”””ÿ...ÿLLLÿ|||ÿbbbÿ±±±þÿÿÿcÿÿÿÿÿÿÿÿÿÿÿÿk³³³ý^^^ÿ^^^ÿ‘‘‘ÿÿÿÿÿˆˆˆÿÿ¢¢¢ÿ÷÷÷ÿöööÿ×××ÿ{{{ÿ¢¢¢ÿþþþÿ£££ÿ***ÿÿ[[[ÿëëëÜÿÿÿ#ÿÿÿÿÿÿÿÿÿôôôÍnnnÿRRRÿ@@@ÿÙÙÙÿcccÿ~~~ÿsssÿlllÿkkkÿuuuÿ¸¸¸ÿûûûÿÿÿÿÿŸŸŸÿ111ÿQQQÿ­­­ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿUÙÙÙñMMMÿÿ@@@ÿ]]]ÿöööÿùùùÿéééÿ¥¥¥ÿ¸¸¸ÿýýýÿÿÿÿÿÕÕÕÿ,,,ÿÿ………ÿøøøÄÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿwÕÕÕôLLLÿÿTTTÿîîîÿÿÿÿÿÿÿÿÿ³³³ÿ!!!ÿÁÁÁÿÏÏÏÿ999ÿÿ‡‡‡ÿóóóÓÿÿÿ7ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿfìììãrrrÿ ÿIIIÿœœœÿËËËÿ­­­ÿ,,,ÿJJJÿ222ÿJJJÿºººýúúú»ÿÿÿ0ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿPÔÔÔóAAAÿ”””ÿÿ†††ÿ€€€ÿ---ÿxxxÿÇÇÇùòòòÓÿÿÿuÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ þþþ¨ŸŸŸÿjjjÿ¼¼¼ÿÆÆÆÿÿbbbÿíííáÿÿÿYÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ#ûûû³½½½ûuuuÿgggÿ‘‘‘ÿäääçÿÿÿ_ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿvýýýÄûûûÒÿÿÿ¤ÿÿÿAÿÿÿÿÿÿÿÿþ?øðààÀÀ€€ÀÀààðüü?þÿÿÿ( @ ÃÃÿÿÿÿÿÿÿÿÿ8ÿÿÿ¥ÿÿÿ²ÿÿÿTÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ2ÿÿÿÎÛÛÛÿÎÎÎÿüüüèÿÿÿ‚ÿÿÿ^ÿÿÿ@ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ ÿÿÿRÿÿÿªÿÿÿãìììþCCCÿÿ½½½ÿáááÿêêêþøøøõÿÿÿÙÿÿÿ›ÿÿÿ@ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ6ÿÿÿ°÷÷÷÷ÄÄÄÿ{{{ÿ>>>ÿÿ ÿ000ÿ///ÿ+++ÿIIIÿ‡‡‡ÿÑÑÑÿûûûïÿÿÿ˜ÿÿÿ#ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ_ÿÿÿáÐÐÐÿ^^^ÿÿÿDDDÿZZZÿdddÿÛÛÛÿÈÈÈÿŒŒŒÿ...ÿÿ ÿtttÿáááÿÿÿÿÏÿÿÿDÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿqûûûò¤¤¤ÿ---ÿ%%%ÿ!!!ÿŽŽŽÿöööÿ¤¤¤ÿtttÿÿÿÿÿÿÿÿÿÿÿÿÿäääÿpppÿÿ%%%ÿ:::ÿ¿¿¿ÿÿÿÿäÿÿÿQÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿaûûûòÿ!!!ÿKKKÿ---ÿ§§§ÿùùùÿ¼¼¼ÿLLLÿ(((ÿdddÿ•••ÿæææÿÿÿÿÿøøøÿ………ÿ///ÿHHHÿ%%%ÿ±±±ÿÿÿÿáÿÿÿ?ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ6ÿÿÿᢢ¢ÿÿkkkÿ...ÿ“““ÿöööÿÿCCCÿOOOÿVVVÿªªªÿaaaÿIIIÿÕÕÕÿÿÿÿÿ÷÷÷ÿmmmÿ>>>ÿ^^^ÿ###ÿÄÄÄÿÿÿÿÆÿÿÿÿÿÿÿÿÿÿÿÿ ÿÿÿ¬ÑÑÑÿ$$$ÿmmmÿPPPÿ]]]ÿùùùÿÿFFFÿàààÿ¤¤¤ÿuuuÿÿÿÿÿùùùÿwwwÿ[[[ÿøøøÿÿÿÿÿæææÿ<<<ÿmmmÿPPPÿ>>>ÿéééÿÿÿÿƒÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿMùùùô___ÿ;;;ÿ———ÿÿ±±±ÿ÷÷÷ÿJJJÿ­­­ÿÿÿÿÿ£££ÿuuuÿÿÿÿÿÿÿÿÿÖÖÖÿ444ÿÖÖÖÿÿÿÿÿÿÿÿÿ«««ÿ)))ÿ¡¡¡ÿ!!!ÿ‹‹‹ÿÿÿÿàÿÿÿ*ÿÿÿÿÿÿÿÿÿÿÿÿ©ÆÆÆÿÿšššÿXXXÿ'''ÿŒŒŒÿæææÿ999ÿËËËÿæææÿsssÿ888ÿ€€€ÿ¡¡¡ÿÁÁÁÿ000ÿÂÂÂÿÿÿÿÿÿÿÿÿòòòÿEEEÿ|||ÿwwwÿ,,,ÿäääÿÿÿÿ|ÿÿÿÿÿÿÿÿÿÿÿÿ ÿÿÿfÿÿÿírrrÿ111ÿÀÀÀÿ(((ÿ>>>ÿÿ´´´ÿNNNÿXXXÿ,,,ÿÿ333ÿvvvÿWWWÿ...ÿÿÍÍÍÿÿÿÿÿÿÿÿÿÿÿÿÿ———ÿ>>>ÿ¼¼¼ÿÿ¡¡¡ÿÿÿÿØÿÿÿ?ÿÿÿÿÿÿÿÿÿ›õõõù±±±ÿ###ÿ'''ÿSSSÿ***ÿFFFÿÿ©©©ÿeeeÿÿÿÿ$$$ÿÊÊÊÿúúúÿZZZÿÿzzzÿùùùÿÿÿÿÿÿÿÿÿÊÊÊÿÿPPPÿÿ===ÿÌÌÌÿþþþáÿÿÿNÿÿÿŽîîîüzzzÿWWWÿ ÿvvvÿOOOÿ ÿ>>>ÿeeeÿ´´´ÿ111ÿMMMÿÿÿÿÿ¾¾¾ÿ+++ÿ£££ÿKKKÿ©©©ÿÿÿÿÿêêêÿnnnÿÿ}}}ÿ___ÿ ÿ@@@ÿÅÅÅÿÿÿÿÌÿÿÿëšššÿ………ÿ¹¹¹ÿÿ½½½ÿlllÿyyyÿtttÿ;;;ÿ;;;ÿ€€€ÿòòòÿ’’’ÿGGGÿqqqÿÈÈÈÿ;;;ÿÿÿÿÿÿ¯¯¯ÿVVVÿöööÿÿ¢¢¢ÿVVVÿ———ÿ•••ÿ ÿ~~~ÿÿÿÿÿúÿÿÿû}}}ÿ£££ÿ»»»ÿÿÇÇÇÿ```ÿrrrÿxxxÿžžžÿ###ÿÅÅÅÿÿÿÿÿþþþÿúúúÿïïïÿXXXÿRRRÿïïïÿÿÿÿÿèèèÿEEEÿ¢¢¢ÿuuuÿòòòÿ[[[ÿ‹‹‹ÿ£££ÿÿfffÿ›››ÿÿÿÿëÿÿÿÏÀÀÀÿPPPÿzzzÿÿ²²²ÿWWWÿ)))ÿ²²²ÿßßßÿ888ÿ×××ÿÿÿÿÿÿÿÿÿ÷÷÷ÿrrrÿ666ÿÚÚÚÿÿÿÿÿÿÿÿÿõõõÿIIIÿ333ÿHHHÿ¿¿¿ÿGGGÿ‚‚‚ÿÿÿ```ÿëëëýÿÿÿ“ÿÿÿSþþþåÂÂÂÿ^^^ÿ ÿ:::ÿ(((ÿUUUÿüüüÿçççÿ:::ÿÊÊÊÿÿÿÿÿùùùÿÿ)))ÿÄÄÄÿÿÿÿÿÿÿÿÿÿÿÿÿìììÿTTTÿÿ333ÿUUUÿÿ222ÿ'''ÿ111ÿãããÿÿÿÿ°ÿÿÿÿÿÿÿÿÿDÿÿÿ¿ñññý???ÿtttÿŽŽŽÿ===ÿóóóÿûûûÿWWWÿÿÿÿÿÿÿ"""ÿµµµÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ¿¿¿ÿSSSÿXXXÿÿxxxÿ(((ÿ§§§ÿKKKÿiiiÿÿÿÿéÿÿÿ0ÿÿÿÿÿÿÿÿÿÿÿÿ$ÿÿÿÜ………ÿHHHÿ···ÿ###ÿÎÎÎÿÿÿÿÿ¯¯¯ÿ>>>ÿÿ###ÿ¡¡¡ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿðððÿ]]]ÿŸŸŸÿ„„„ÿÿgggÿ...ÿÃÃÃÿ...ÿ¯¯¯ÿÿÿÿºÿÿÿ ÿÿÿÿÿÿÿÿÿÿÿÿ—ÔÔÔÿ---ÿ§§§ÿ555ÿ………ÿÿÿÿÿúúúÿlllÿÿkkkÿúúúÿÿÿÿÿÿÿÿÿÿÿÿÿäääÿkkkÿoooÿöööÿùùùÿÿ$$$ÿ[[[ÿŽŽŽÿ>>>ÿíííþÿÿÿlÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ<þþþìxxxÿPPPÿjjjÿ///ÿäääÿøøøÿZZZÿ///ÿ???ÿnnnÿ›››ÿ¡¡¡ÿÿKKKÿÿïïïÿÿÿÿÿÿÿÿÿ¯¯¯ÿ ÿƒƒƒÿ999ÿ¢¢¢ÿÿÿÿÓÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ“ãããÿAAAÿQQQÿÿÿ³³³ÿ---ÿÑÑÑÿÔÔÔÿŽŽŽÿnnnÿ\\\ÿ~~~ÿÐÐÐÿûûûÿÿÿÿÿÿÿÿÿôôôÿUUUÿ###ÿHHHÿ]]]ÿôôôúÿÿÿiÿÿÿÿÿÿÿÿÿÿÿÿ!ÿÿÿÊÅÅÅÿ111ÿ ÿÿ///ÿkkkÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÉÉÉÿƒƒƒÿéééÿÿÿÿÿÿÿÿÿüüüÿ‹‹‹ÿÿÿFFFÿÞÞÞÿÿÿÿ©ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ=ÿÿÿÛÅÅÅÿ555ÿÿÿ„„„ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿáááÿ'''ÿ>>>ÿçççÿùùùÿ’’’ÿÿÿOOOÿÜÜÜÿÿÿÿÂÿÿÿ#ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ=ÿÿÿÎÞÞÞÿfffÿ ÿÿ{{{ÿÝÝÝÿýýýÿÿÿÿÿÝÝÝÿ666ÿÿ®®®ÿdddÿÿÿÿíííýÿÿÿ´ÿÿÿ%ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ%ÿÿÿ¦öööûRRRÿÿ&&&ÿ...ÿ```ÿ………ÿÿ___ÿÿÿ+++ÿpppÿÍÍÍÿüüüíÿÿÿˆÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿʦ¦¦ÿ000ÿÂÂÂÿÁÁÁÿžžžÿŽŽŽÿŽŽŽÿSSSÿ%%%ÿ¹¹¹ÿêêêþýýýêÿÿÿ¨ÿÿÿAÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿjôôôúhhhÿQQQÿÎÎÎÿöööÿóóóÿ¼¼¼ÿ555ÿ‚‚‚ÿÿÿÿìÿÿÿvÿÿÿ/ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ ÿÿÿ›ðððü………ÿ999ÿ>>>ÿ999ÿ222ÿƒƒƒÿòòòüÿÿÿ‡ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿýýýèÝÝÝÿ´´´ÿµµµÿßßßÿýýýèÿÿÿ„ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ<ÿÿÿ‘ÿÿÿ¿ÿÿÿ¾ÿÿÿÿÿÿ;ÿÿÿÿÿÿÿø?ÿÿàÿÿ€ÿþü?øøðààÀ€ÀÀààðøüþ?ÿÿ€ÿÿ€ÿÿÀÿÿàÿ(0` $ÃÃÿÿÿÿÿÿÿÿÿÿÿÿeÿÿÿ”ÿÿÿ}ÿÿÿ/ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ²ÿÿÿúÿÿÿÿÿÿÿþÿÿÿÚÿÿÿMÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ›ÿÿÿÿåååÿ¥¥¥ÿËËËÿþþþÿÿÿÿáÿÿÿ¡ÿÿÿŽÿÿÿrÿÿÿKÿÿÿ#ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿNÿÿÿžÿÿÿØÿÿÿùûûûÿcccÿÿ(((ÿÛÛÛÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿøÿÿÿáÿÿÿ®ÿÿÿ`ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ ÿÿÿQÿÿÿ¼ÿÿÿöÿÿÿÿùùùÿßßßÿ³³³ÿ'''ÿÿÿZZZÿzzzÿ€€€ÿ“““ÿ²²²ÿÖÖÖÿôôôÿÿÿÿÿÿÿÿûÿÿÿÍÿÿÿgÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ%ÿÿÿžÿÿÿôÿÿÿÿæææÿžžžÿRRRÿ!!!ÿÿÿÿÿÿÿÿÿÿÿDDDÿŒŒŒÿÚÚÚÿþþþÿÿÿÿúÿÿÿ¶ÿÿÿ9ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿIÿÿÿÑÿÿÿÿðððÿ™™™ÿ000ÿÿÿÿÿWWWÿÿ‡‡‡ÿÜÜÜÿÒÒÒÿµµµÿwwwÿ)))ÿÿÿÿ"""ÿ‚‚‚ÿåååÿÿÿÿÿÿÿÿãÿÿÿfÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿeÿÿÿéÿÿÿÿÏÏÏÿNNNÿÿÿÿ ÿfffÿÕÕÕÿàààÿ%%%ÿ¢¢¢ÿÿÿÿÿÿÿÿÿÿÿÿÿþþþÿáááÿ~~~ÿÿ ÿÿÿ777ÿ¹¹¹ÿýýýÿÿÿÿõÿÿÿ…ÿÿÿ ÿÿÿÿÿÿÿÿÿÿÿÿlÿÿÿñýýýÿ­­­ÿ$$$ÿ ÿCCCÿ###ÿÿ¢¢¢ÿøøøÿÿÿÿÿâââÿ$$$ÿ¢¢¢ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿýýýÿ»»»ÿ000ÿÿGGGÿÿÿÿöööÿÿÿÿúÿÿÿŽÿÿÿ ÿÿÿÿÿÿÿÿÿÿÿÿ\ÿÿÿïüüüÿ™™™ÿÿÿiiiÿ***ÿ"""ÿºººÿÿÿÿÿÿÿÿÿåååÿ———ÿÿCCCÿ{{{ÿ¤¤¤ÿÝÝÝÿýýýÿÿÿÿÿÿÿÿÿÿÿÿÿÓÓÓÿ888ÿÿjjjÿ***ÿÿxxxÿôôôÿÿÿÿúÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ:ÿÿÿàþþþÿžžžÿÿÿˆˆˆÿ555ÿÿµµµÿÿÿÿÿöööÿšššÿ+++ÿ ÿÿ&&&ÿ000ÿÿ"""ÿ„„„ÿîîîÿÿÿÿÿÿÿÿÿÿÿÿÿÑÑÑÿ---ÿ ÿŠŠŠÿ111ÿÿ{{{ÿøøøÿÿÿÿòÿÿÿZÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿºÿÿÿÿºººÿÿÿ˜˜˜ÿTTTÿÿ———ÿÿÿÿÿöööÿwwwÿ ÿUUUÿ¢¢¢ÿÿœœœÿòòòÿËËËÿjjjÿ ÿaaaÿðððÿÿÿÿÿÿÿÿÿÿÿÿÿ¹¹¹ÿÿ777ÿ¤¤¤ÿ%%%ÿÿ˜˜˜ÿÿÿÿÿÿÿÿ×ÿÿÿ)ÿÿÿÿÿÿÿÿÿÿÿÿtÿÿÿüáááÿ666ÿÿ†††ÿŒŒŒÿÿ```ÿöööÿÿÿÿÿ———ÿÿ~~~ÿöööÿãããÿ$$$ÿ¢¢¢ÿÿÿÿÿÿÿÿÿúúúÿŠŠŠÿ ÿÿÿÿÿÿÿÿÿÿÿÿÿÿþþþÿ‡‡‡ÿÿkkkÿ¤¤¤ÿ ÿÿÈÈÈÿÿÿÿÿÿÿÿšÿÿÿÿÿÿÿÿÿÿÿÿ&ÿÿÿÚýýýÿvvvÿÿOOOÿÇÇÇÿ"""ÿ ÿÓÓÓÿÿÿÿÿäääÿ+++ÿRRRÿôôôÿÿÿÿÿâââÿ$$$ÿ¢¢¢ÿÿÿÿÿÿÿÿÿÿÿÿÿðððÿ???ÿ222ÿìììÿÿÿÿÿÿÿÿÿÿÿÿÿëëëÿ???ÿ ÿµµµÿtttÿÿQQQÿóóóÿÿÿÿîÿÿÿCÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ€ÿÿÿÿÊÊÊÿÿÿÁÁÁÿzzzÿÿ>>>ÿéééÿÿÿÿÿ®®®ÿ ÿ«««ÿÿÿÿÿÿÿÿÿâââÿ$$$ÿ¢¢¢ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ ÿÉÉÉÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ­­­ÿÿSSSÿÕÕÕÿ,,,ÿÿ¨¨¨ÿÿÿÿÿÿÿÿ¦ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÔýýýÿjjjÿÿiiiÿÜÜÜÿ&&&ÿÿ///ÿÍÍÍÿÿÿÿÿŽŽŽÿ ÿÈÈÈÿÿÿÿÿþþþÿÏÏÏÿÿpppÿ···ÿÆÆÆÿäääÿÿÿÿÿ“““ÿÿ³³³ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿòòòÿFFFÿÿÅÅÅÿ’’’ÿÿEEEÿòòòÿÿÿÿëÿÿÿ6ÿÿÿÿÿÿÿÿÿÿÿÿ`ÿÿÿûÖÖÖÿÿÿÇÇÇÿ£££ÿÿdddÿ333ÿdddÿõõõÿšššÿ ÿ¶¶¶ÿÕÕÕÿrrrÿ***ÿÿÿ ÿÿ$$$ÿgggÿUUUÿÿµµµÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ   ÿÿzzzÿâââÿ(((ÿ ÿ¸¸¸ÿÿÿÿÿÿÿÿ†ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿfÿÿÿØÿÿÿÿÿÿNNNÿôôôÿ^^^ÿ ÿOOOÿÿÿ‘‘‘ÿÌÌÌÿÿ<<<ÿ"""ÿÿÿÿnnnÿ´´´ÿ›››ÿeeeÿ ÿÿÿÌÌÌÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿáááÿ###ÿ777ÿðððÿuuuÿÿhhhÿýýýÿÿÿÿçÿÿÿlÿÿÿ ÿÿÿÿÿÿÿÿÿÿÿÿ¨ÿÿÿúþþþÿßßßÿBBBÿÿGGGÿœœœÿ###ÿ111ÿBBBÿÿÿlllÿðððÿ===ÿÿÿÿÿÿ???ÿÔÔÔÿÿÿÿÿÿÿÿÿ›››ÿÿÿqqqÿõõõÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿýýýÿ^^^ÿ ÿtttÿKKKÿÿ$$$ÿÊÊÊÿýýýÿÿÿÿùÿÿÿŠÿÿÿ ÿÿÿÿÿÿªÿÿÿÿêêêÿ‚‚‚ÿ...ÿÿÿ***ÿÿÿ///ÿ˜˜˜ÿ...ÿFFFÿÝÝÝÿ¤¤¤ÿÿÿÿÿÿÿÿqqqÿÿÿÿÿïïïÿIIIÿÿ[[[ÿÿÿþþþÿÿÿÿÿÿÿÿÿÿÿÿÿÞÞÞÿEEEÿÿ!!!ÿCCCÿÿÿÿ„„„ÿôôôÿÿÿÿóÿÿÿcÿÿÿwÿÿÿùèèèÿTTTÿIIIÿ|||ÿÿ!!!ÿÖÖÖÿ™™™ÿÿ&&&ÿ"""ÿÿšššÿãããÿ***ÿ111ÿ“““ÿÿÿÿÿÿpppÿÿÿÿÿŒŒŒÿÿoooÿôôôÿmmmÿÿÏÏÏÿÿÿÿÿÿÿÿÿáááÿJJJÿ888ÿÿ€€€ÿòòòÿAAAÿÿ@@@ÿÿ•••ÿÿÿÿÿÿÿÿÈÿÿÿÕÿÿÿÿ€€€ÿ>>>ÿèèèÿ¬¬¬ÿÿ999ÿõõõÿ›››ÿ ÿ±±±ÿŸŸŸÿ ÿQQQÿwwwÿÿ   ÿÿÿÿÿÁÁÁÿHHHÿÿÿIIIÿÚÚÚÿ¿¿¿ÿÿ555ÿàààÿÿÿÿÿÝÝÝÿ&&&ÿuuuÿÿÿÿÿúúúÿkkkÿlllÿÏÏÏÿÿlllÿÿÿÿÿ___ÿÿ€€€ÿyyyÿDDDÿöööÿÿÿÿ÷ÿÿÿúôôôÿ>>>ÿŠŠŠÿÿÿÿÿžžžÿÿIIIÿüüüÿ‰‰‰ÿ ÿÈÈÈÿ©©©ÿ>>>ÿLLLÿÿ,,,ÿæææÿÿÿÿÿÿÿÿÿòòòÿËËËÿÇÇÇÿñññÿÚÚÚÿ333ÿÿ¹¹¹ÿÿÿÿÿÿÿÿÿÿÿÿÿoooÿ444ÿòòòÿÉÉÉÿAAAÿáááÿäääÿÿ[[[ÿÿÿÿÿsssÿÿvvvÿ‘‘‘ÿ???ÿôôôÿÿÿÿúÿÿÿøöööÿBBBÿvvvÿÿÿÿÿžžžÿÿOOOÿþþþÿ‚‚‚ÿ ÿ¬¬¬ÿLLLÿ¨¨¨ÿ×××ÿÿXXXÿüüüÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿêêêÿNNNÿÿÿþþþÿÿÿÿÿÿÿÿÿÿÿÿÿ§§§ÿÿ£££ÿÿpppÿÿÿÿÿáááÿÿVVVÿÿÿÿÿ{{{ÿÿdddÿNNNÿ{{{ÿÿÿÿÿÿÿÿÖÿÿÿÍÿÿÿÿÿÿ«««ÿ¤¤¤ÿÿKKKÿýýýÿˆˆˆÿÿ000ÿmmmÿøøøÿßßßÿÿoooÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿôôôÿkkkÿÿlllÿöööÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ¾¾¾ÿ ÿÿÿ}}}ÿ÷÷÷ÿÛÛÛÿÿ[[[ÿÿÿÿÿvvvÿÿÿEEEÿáááÿÿÿÿûÿÿÿ|ÿÿÿjÿÿÿõòòòÿvvvÿÿ---ÿÿ***ÿ”””ÿHHHÿÿVVVÿìììÿÿÿÿÿàààÿÿlllÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿøøøÿ~~~ÿÿNNNÿéééÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ»»»ÿÿ˜˜˜ÿ(((ÿÿ???ÿXXXÿÿ???ÿ˜˜˜ÿ777ÿÿFFFÿ×××ÿÿÿÿÿÿÿÿ¸ÿÿÿÿÿÿ ÿÿÿ’ÿÿÿûùùùÿ¿¿¿ÿfffÿÿÿ)))ÿ444ÿÿ„„„ÿÿÿÿÿÿÿÿÿîîîÿ000ÿNNNÿúúúÿÿÿÿÿÿÿÿÿúúúÿ‡‡‡ÿÿ:::ÿÚÚÚÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿžžžÿ%%%ÿÔÔÔÿ???ÿWWWÿzzzÿ111ÿÿÿ(((ÿ ÿ ÿ¼¼¼ÿÿÿÿÿÿÿÿÇÿÿÿ,ÿÿÿÿÿÿÿÿÿÿÿÿ{ÿÿÿåÿÿÿÿõõõÿGGGÿÿ¸¸¸ÿÇÇÇÿ ÿYYYÿüüüÿÿÿÿÿýýýÿ```ÿÿÝÝÝÿÿÿÿÿýýýÿ”””ÿ ÿ///ÿÐÐÐÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿüüüÿ```ÿIIIÿÿ ÿÿ………ÿ€€€ÿÿ”””ÿÑÑÑÿ!!!ÿ%%%ÿäääÿÿÿÿòÿÿÿDÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ0ÿÿÿÁÿÿÿÿˆˆˆÿÿ“““ÿéééÿ&&&ÿ***ÿçççÿÿÿÿÿÿÿÿÿ¯¯¯ÿ ÿ‰‰‰ÿÿÿÿÿ¬¬¬ÿÿ!!!ÿÂÂÂÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÍÍÍÿÿ•••ÿbbbÿÿÿdddÿLLLÿ ÿÍÍÍÿ¼¼¼ÿÿ\\\ÿüüüÿÿÿÿÐÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿhÿÿÿþÌÌÌÿÿJJJÿñññÿUUUÿÿ¹¹¹ÿÿÿÿÿÿÿÿÿòòòÿOOOÿÿ•••ÿ%%%ÿÿ®®®ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿîîîÿQQQÿ<<<ÿëëëÿÇÇÇÿ111ÿÿ¦¦¦ÿÿ///ÿëëëÿsssÿÿ§§§ÿÿÿÿÿÿÿÿ“ÿÿÿÿÿÿÿÿÿÿÿÿ&ÿÿÿßøøøÿVVVÿÿÅÅÅÿ•••ÿÿmmmÿþþþÿÿÿÿÿÿÿÿÿÏÏÏÿ+++ÿÿ ÿ”””ÿþþþÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿëëëÿeeeÿÿ¾¾¾ÿÿÿÿÿÿÿÿÿØØØÿ===ÿNNNÿÿjjjÿÞÞÞÿ%%%ÿ222ÿéééÿÿÿÿóÿÿÿFÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ”ÿÿÿÿ¸¸¸ÿ ÿ___ÿÊÊÊÿÿ!!!ÿÚÚÚÿÿÿÿÿÿÿÿÿúúúÿ]]]ÿÿ ÿ‹‹‹ÿæææÿýýýÿÿÿÿÿÿÿÿÿÿÿÿÿóóóÿ¹¹¹ÿCCCÿ!!!ÿ¯¯¯ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ‰‰‰ÿ ÿÿ¯¯¯ÿˆˆˆÿÿ”””ÿÿÿÿÿÿÿÿ¹ÿÿÿ ÿÿÿÿÿÿÿÿÿ5ÿÿÿçøøøÿ```ÿ ÿ¦¦¦ÿXXXÿÿ~~~ÿÿÿÿÿÿÿÿÿºººÿÿ888ÿSSSÿÿ'''ÿ\\\ÿ}}}ÿ‚‚‚ÿlllÿ;;;ÿÿJJJÿÇÇÇÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ———ÿÿ444ÿ¸¸¸ÿÿ===ÿêêêÿÿÿÿöÿÿÿWÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿŠÿÿÿÿÕÕÕÿ&&&ÿ111ÿ………ÿÿÿËËËÿøøøÿOOOÿÿ½½½ÿ÷÷÷ÿºººÿnnnÿ>>>ÿ,,,ÿ***ÿ333ÿjjjÿ³³³ÿòòòÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿäääÿ666ÿÿqqqÿOOOÿÿ···ÿÿÿÿÿÿÿÿ®ÿÿÿ ÿÿÿÿÿÿÿÿÿÿÿÿËÿÿÿÿ©©©ÿÿ;;;ÿ&&&ÿÿRRRÿ³³³ÿÿZZZÿùùùÿÿÿÿÿÿÿÿÿþþþÿõõõÿìììÿàààÿ‚‚‚ÿëëëÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿùùùÿrrrÿÿÿFFFÿ ÿ………ÿüüüÿÿÿÿäÿÿÿ9ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿJÿÿÿêüüüÿ‘‘‘ÿÿÿÿÿ###ÿÿ©©©ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿøøøÿTTTÿpppÿÜÜÜÿþþþÿÿÿÿÿÿÿÿÿÿÿÿÿþþþÿ™™™ÿ ÿÿÿ ÿoooÿôôôÿÿÿÿ÷ÿÿÿnÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿlÿÿÿôúúúÿ™™™ÿÿÿÿÿÿÑÑÑÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿšššÿÿ$$$ÿ±±±ÿÿÿÿÿÿÿÿÿüüüÿ¡¡¡ÿÿÿÿ ÿ{{{ÿòòòÿÿÿÿüÿÿÿÿÿÿ ÿÿÿÿÿÿÿÿÿÿÿÿsÿÿÿñýýýÿ···ÿ111ÿÿÿÿgggÿãããÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿþþþÿlllÿÿÿhhhÿÿÿÿÿïïïÿ………ÿÿÿÿÿœœœÿùùùÿÿÿÿùÿÿÿ“ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ`ÿÿÿäÿÿÿÿÝÝÝÿjjjÿ ÿÿÿ444ÿ¢¢¢ÿëëëÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿŒŒŒÿ###ÿÿvvvÿ¹¹¹ÿHHHÿÿÿ ÿRRRÿËËËÿþþþÿÿÿÿðÿÿÿ}ÿÿÿ ÿÿÿÿÿÿÿÿÿÿÿÿ>ÿÿÿÂÿÿÿýúúúÿdddÿÿ ÿÿÿ222ÿvvvÿ©©©ÿÃÃÃÿÊÊÊÿÃÃÃÿ‚‚‚ÿ ÿÿ ÿÿÿJJJÿ«««ÿóóóÿÿÿÿÿÿÿÿÖÿÿÿVÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ§ÿÿÿÿ©©©ÿÿVVVÿšššÿ___ÿ555ÿÿÿÿÿÿÿÿÿVVVÿ‹‹‹ÿÈÈÈÿõõõÿÿÿÿÿÿÿÿïÿÿÿšÿÿÿ'ÿÿÿÿÿÿÿÿÿÿÿÿFÿÿÿóëëëÿ<<<ÿÿÐÐÐÿÿÿÿÿñññÿàààÿÐÐÐÿÆÆÆÿÃÃÃÿÆÆÆÿ^^^ÿÿžžžÿÿÿÿÿÿÿÿÿÿÿÿýÿÿÿåÿÿÿ¢ÿÿÿAÿÿÿÿÿÿÿÿÿÿÿÿ ÿÿÿ°ÿÿÿÿ¸¸¸ÿÿ===ÿÑÑÑÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿüüüÿ¬¬¬ÿÿ333ÿåååÿÿÿÿùÿÿÿ®ÿÿÿhÿÿÿ+ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ;ÿÿÿâþþþÿ®®®ÿ"""ÿÿwwwÿ¯¯¯ÿ¹¹¹ÿ¢¢¢ÿ^^^ÿÿ"""ÿºººÿÿÿÿÿÿÿÿ¼ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿYÿÿÿèÿÿÿÿÐÐÐÿ\\\ÿÿÿÿÿÿ]]]ÿÍÍÍÿÿÿÿÿÿÿÿáÿÿÿ@ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿKÿÿÿÑÿÿÿÿøøøÿÐÐÐÿ¡¡¡ÿÿ£££ÿÒÒÒÿùùùÿÿÿÿÿÿÿÿÕÿÿÿKÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ#ÿÿÿŠÿÿÿÝÿÿÿúÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿúÿÿÿÛÿÿÿŒÿÿÿ&ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ)ÿÿÿfÿÿÿ“ÿÿÿ¢ÿÿÿ’ÿÿÿcÿÿÿ'ÿÿÿÿÿÿÿÿð?ÿÿÿÿà?ÿÿÿÿ€ÿÿÿþ?ÿÿøÿÿðÿÿÀÿÿ€ÿÿÿ?þ?üüøøððÀ€€Àðððøøüþ?þ?ÿÿ€ÿÿÀÿÿðÿÿøÿÿø?ÿÿüÿÿÿþÿÿÿþÿÿÿÿ€ÿÿÿÿÀÿÿ‰PNG  IHDRö{`íAðIDATxÚí]g˜ÅÖ~gfó²äœ Š#Áȧˆ™dÀ æ€Å€YÌŠ9 êT@‚ ×ˆ$#9§]6°qÞïÇ ³Ý]§º{fgaÁ}ëáyØîžî §ªN”á_ Ïñ®À±À /< Âÿ¯iºNÚ^ Ä¡"j£6ê¢*! ˆ‘‡lÂ~ìÂVlÅnFáIÜ8éÚMˆA ´ÆhSPå#¶“ÈEv` aVã xÒu‡#Nªð¢ÎÆ…è„zH㇙X‡y˜…Øÿï"ƒ“¤­€$tÄ•¸ ákŽ`5¦ã[¬DÞIÓ5'?‚)¼‚S™Æh`ÇñR¦0@Ve(Í &³g3;*ƒYœÃ¾¬PF¥cx'3+ªƒ9ü—3±ŒJ%‚õù2÷•ÈàE&ÇótzËH ”`¯âúKtøØÎ‡X±l(5 Öá›<| ?€|~Çe$P*@ÐÃs8ï˜Ì}#6°/cN>8Á»âq#F –ûß"‡°éÈD.ÊÆô„5øG‹§ò ®e¡Ógòø4ãÊHà˜ Ø‰Ë—ýWÙ4ÂÁ/ÚñInqúTÿSv(Îã+ìͺδ ƒ ïézú a‘Žã#Q´ ÷stÀ)ÀRt³½äpÿËlTFƒàéºù¿Š§ {ÿíN’û0‘Ïg«|§!׺Á?üŒW³fÙ¦6z9Jî×#$ÌÌ^ÜÕá'Ét^#Úëá½$—Ëù";LÊà[èô±ßr¿¦ü+êÃO’ËØHùVÏHøŒü†×°r¸Ap¸®'»)C’È1%2ü$ù¢¢bªìl“"#‡¿ñvÖ*#G¬Â?å^üŒq \•Ýß/²wÛØZùÞ»‘¤€Ëx?ë”6"ˆ9ÞPpZK—b ò,×j`(’]¼2$ ‡‘ü—ld#yȇ… |ð qˆC‘„d”G¬°¼i!nTûçC¼„ð1¾äîÒ£C,U@¸‰Ò½ïñ‡ríZtÔ¾+‡±;°[±;±‡‰#ÈEü.f¡^áêrlDÄG:|^´Æ«èwð-ÓK”†:„@ f£­z'×`ºåZm|‡v–k8„-X‹¿±[° éȉê’‹h€SÐ-Ð5dZuF.~ÂkøyÇ¿û B ôÀiUŸ‹Ëf¹v Þ/øÿBìÇ,ÆB¬Â6GCâÃDTC´EG´A=W‘i˜€QXSª†àx‚ —]øƒÌA=¤°c)üIòÿæÇ¼­X®˜Á‘•Ödw>Îï¹ÛÙ ÜŠu¼½, ‚‡¿jÁr?ígGA?w€+8нX3JÖÀÅ+IlÍÛø5w†G¹œÂNÿòø}ìÆŸô=÷« ÿïÆ«Y#ŠsÞCOÞÏ–¼‹s V´2õx­Çy"TƸUõO½‚…ŠGÒa>”C%TCuTCeT@2âààA |(D2q{±»°‡Q«Ê¡®Àÿ¡¡x’P€™xKŽûpkôð δwøÌçµQ˜™ y1æXþÎÍLcž­fÏÏîç*Îd¯ˆ¿èe#åv‰Alâ`&þ«¶‚ ì쇵íŠ1ôyæ4nâ‘p–å /&áUåMœkgWlD6?`½ãµûÁëð=7jü•¬![Ö…Oñw¦G0ðGq¯°–„ë„R‰7p>óÝ|ÎÏßyî¿@…LìÌ_ÝXX¤s¤ ›wbèjó&ÎàÁ‡ÝÏ\æ°ÊæãáC|öJpW¸³WÜÁÛ˜pR“ÁXä&ç¾ÈåL^fg{Ù”q…»'þr>ÌKx>ûñžby{¿'¹Ÿx “Ã$‚ú|NwÎ5#›£‹ÞI ‚)|ÊM@ŽUÕÆÌ?…Ïrc1,ƒýœÈ†6_¨ÇõÁ'39—¢H†Aœgp¢›]¯ÓÙâ$$‚`]þ×yz¦ñ-eö9•Z|˜ë‹<ìÖ¶ýÆù¦ ¤œÈ³4ÞIº’ÌëíÍKâ/v?ɸ‚`› ×–üÿà¥aîûåx=…/е ƒÿçðÇ•ßìåkl&©6æûnl‹¶sàI˜Ž ØÝ9ÎG_v˜…êÂß‘“#:äY1Ïq˹QøŽŸ+yS˜A<¯ã ç ¥qØI" èåUÎgþ¿xy˜s?•p[Ÿ$¿p<â•ã4ñ—G8Ž­Â\šòsæ8Ué_; ¢’Œá`îµok? {1mË)îemŽ˜ìb? e`Ör`˜'–dÞÉN•*৬AœÀD@0ž8Écvñž0—Ñ8ñäÑÁß»MÛÒÊÆ=“£cÆöuŽSüÒÏ)lpÂ’ÁDŽpŠè¿€ÝÔ°UäȨG‹ÎåÇïÖçVÛ±šˆ*F©Ç1Î[ÁlvB’ÁD>cß¾|~öÒ߈_¹ &þ`}‡/·st?_ËËÂ$æd>ÈNUû…ÍO8 ˜À'íô >P†‡QZq^ ~SØÊÖÀdˆ‹Ãæ^Þ&˜°Ûûp3 œX«ÁX>l?ü»88Ì®ÁA%+~%Ûj¿\ÉM´Z’™|"lQqGg²þ1 ¦Þhó`/"J) Làz6=’eQ-­¹»`…IÃsø)·[ÞÇÃTƒ^kßÂeP)¥Áÿè™ôB~A\ïDådž¬s!*FÎ$)µœ”ébd'¸Î~˜\ •DÁ®v:¿±PŽl,Uø¢`ò8£; öfÄFevŠ‘ ‹J’ ;ôð;^ €#J]´r‚5ì >¦Š1yë²— 8‘o0‡W+×»Flò©C/ uýnM¹]c…n¢¨´çY"/p·Äã@À~¥Ô€` _Òïþ¿±‰ÐÈÚügj;ÆËû˜CòmåN9Ήòeñ¼à»;r{”ßM’ÓmÌÛSùn€¯³”8>g·-bÃRCÁ+ôGåuì$4°¿âÇ6óÿ’ Áraí¸3Êálv®9ãJ`øI?_¶{_Íå!4–~j÷ÒJ 9Á†\¢«ç^!Òý§Üg3ÿ›„^x„—)wEÙ  ¸Ñô‰rÔÁ"dب›S8›k/h°Žþ!“ýJÅ6@0N/Éå0ÉIäk$?ÓÎÿdíVE_ò 'X±DÎG±–m´$p sù§ìÈ úW®à)Ç‚G«øùLˆïçãCÌešúíh¹ÕdF°Ywpçhá_ÓÇ¾ÎÆÅÂ$1 m`=ü‰äLÑ*©¿VíýãžÅ`mþ¡«ß!ä"ØŸ‡HNÒfø´& ñs¨@Dï1øYÀ#Lçîå^îg³™Or;qF‰?™Ç{´$óI~"cí¸ëtöŽ|ˆ‚C:/žÆ#ò»öc f)WÏÂXÔG6úaŠøÎòƒ«,×þÀeØg¹v¦ºL$Nä`¶a¶bö! ÙÈEˆÄ! PUñú£ª¢\ zêoÁ•X,Þ©Žïp:òñ F¢@¹÷zè^9Wa÷qŠ-@DXˆsÙ_yÎË—ç\×r‡²²œƒ±–åX—gòŽæâbùÛá+mª«ûè'™Æ¾Â½³ô‡S?;n¬ Árœ¬«Ù,Aò—ÌO‚ÄñM74Ó$ ™-쟧Ø$xÌã*¾ÍËX'lY<èeežÍGùK AŽV4Ü8x²YÏÓ„»÷ëe[Øþ¸@p;×ø;îå[wg÷2P¬6 ë!†7x›èpß²?k;¥d Ïák\eíã ‘/=¡D)³„´©œªå˜ã ¬År ùˆ`ðufh!{RÓåÚØÝÌ4‰©Šîy/Góì°•°v«ACå’¨j^Òfç  ªÏ ëVgý6ÆKŽù6@|P79þ'ÐpÕw§&ðK*gÙt[o~ÓÅÐ-éüŒÃ)á¦ÔäÝ\µ•`7»Š_‰ç¤à‡á—‡êk0ƒ)Çžšp\›ƒ‚lÛÃÇCÕ«¤›Ô0 …s²‡0d>àÅaGï§Ôã³Q“=ŒÓ¯ õÀÁK¡:çêgGÿð×_˜Ï›†<€Ëå»á}ø-×ÎÆK(ÈÁ3X%üª&^C]Û¯ÖÀAÌS®®D3¤âY<ŠåabcQõÑq6ºã|ôD7œ3Ð P‰ ò•z!?c>ª£aq:.ˆXŠuÂõý¸5µüh©M¢â¥Æ :¦!û©b×Ì%¶Ô~®äv B–bf.p¨‹%v3Û ¿lĶ _,ë±Gp—r'3;Ÿfps"åEG“‰¥ˆÊ:0Ic/øt艃¼XØ$>Ó½0ŸCŽ@ÐÃåzˆÒ®{ LÔÍëÂgžäذ<ðŽãú0„¼G¸Žcy=ëkˆÊËsõÙD]"—ÃÞ^Jéh°EúI°Ÿì wˆÿƒÕŽhçÿ\¡Ò- º»½W‰ÙHf "!¹$ð,¾Å Úõåsß䙚½º>?/Ö¹`Û°®Hʉ†sMïSî{øŒ¾Ê·“5€ ø¬\‡#‚m}¬Ij?Mlvçð!!¬pA(žÝ9> vC8=DÖ²Ÿ-†ÒŸLe¢ÆHÄè¸NX'éÅ_¿³ê±!€Fº4zß ;[Ã@ˆT)w…uÄúЖÛ÷²=? 3‡¸Òø©ÈcÄ󞈿òA°!“„ú71EÑ{]°™¼O×[yÇ ¥=Að~y½–-%óÃ[ØThrU½:‘òwúi‡¿‡•€óØ6¶6oŽ(_y.¯ €d+ÃÏ Ïjq±îÕsJ\@°Ê_ÿVðŠícZ('ˆ€~a›a~¦1°ªÊÉ%;€,àlvV¤›^Þ lý`%ײ¾&·d6w§®•Y%l,¬Ÿ8^ÒüO1Éö x£ÐÜ$~fÎÓpп”’ð `«0`^Ûop\ð˜ÃWDÅO“ÁÛ~A«RSŸÅð‹Õ L0­éLWštE›EöíÜ0wÒe6>ü`å8µ$É—çï +}u®! ñÛ|Fhƒÿ5ýbŒ°Ü­#ôÝ<­d  «Ñ$GˆìŸlQÕ|%,Ü]²` ¶Ø’JcþV‚$PÀ/Mf<_ãPø³AMþWˆ¡¥˜–ø=æèó%¶ _¿:—•”J^h²fóóVq¹s—›dº‹PN`;CÊ(`ºS8•ã]þv@q$þ1,ÕÜM-.V£„SÈÝ'–³VÉ@M.“¾Yhp­:Zb- ÙÑ*vPL[˜Ú]*WFÄ»Çl…ë²Þšp˜w†2³HŽÚoÙg7 âõºÓN^À¨¨dæÿ52¸Nˆ³Ýž»LÏü(œømÝ>MXe’E¹½—C£@Vï”à|ÇTö›x“i°ks-Éíl)´à^Ëo‡)Oøø¾îC_•H Z¨¼&˜2xw– ¬h1¶’ü£Êñ{]åî‹ú&@¼I&ñÙÂð ‹þm&ÉÖÈŒ)б‡÷—ù©z=çøôÅB[¾x®¸ Ì`5ùWðþ>QðŽå-c¡ðmºUnËEŸâ8Qú–_`}Aëß"|)ìÕíwÍ£ØÇs…pW¨oh´vmÃP2E‚­ã.oȰ³¿‹._à͆£ãýÂý–Ù¶Ið°nlq¡ á;EŸšÊá¶Š+kŤ&Þ庣ßT¨¿–É<*›wk¢ö(Á°r$9ÇbÞrŠÅ¬}«ÆþÏ˾œJ1CØ ¬¡p y³ð–tÕM ¸ˆëÍDA¼c ¬ž!¸?Çé] ,ت ½|Þ¢Úð‘J¿¨‡“0ËcÒf¨™ŸÃ5µÁDö ΩÂI E‰%>IèéËu¦.3™]ˆáÒw$ù¾U”I®ü»IF’|E™Ýç›Ç©‰÷—è‘p‡E_g´mZ¯ñ(*WñI¿0»¡øHlt©5dÁ ¹‹­œÀerÛ jã éò6ÁL³–òèZųhÚ®>¼ c-‹Y9 E5å¹fx „ßâ¼¥øÛEµp7 oÅ„Ðÿÿć_‹ñ<8G05]biw-tVžÙ_äWG'8l® °Z£†¡¾t_jb;e þBžòÔYˆuSÌÂJË• qøä9x•…ë9xŸGsG´àRt7ýýu¨OV;Ø)øiNHz%›þö¡›0hÿ“Ó\Ë4e‚#q>Æ`&n—ÆËÙÈW®ži1].Àrå™ èèªs³0Ñ2{S0Iš§¯ÂãH®§ã|çê{‘ ¼¥Fk1;ø¿#.~½›ÔG åÎì´\é(ÉÝ*#Ñ” @ =ð9&£¿îU»ð»r­:Y®Âzå©8ÅEçáOË•óp–M“nÅ]"áïÆ}Âf-tCWÃ_~|‹lp從‡,É„8€ J¯©d² ä×Ç©ö_¶!‚´Æ{˜„+ƒþ"–a³r­š)UÜ¡<ÕF\¬UÌÂ!Óßqè«ÿá ÞÙ€»„•(:HA?Äþþ3èúÒÕE++¢:à ÄYîäaåŠD&…øUÞÞ’¸ *àLÅ ¤ÚW~®°ÈµBUË•HWž:Í ’†ÿY®´D7‡ß¤b$z‰wþÂ]Øèâ«‘ §iE;€Ÿ‚­¼Òñ—QÐBé7`•2€g;ñBì—_ÝI¡)Ä ´Æ'xId©MÈÄÂÕö¦¹ë<­;&Ð[®\œ/v¨‰×ÑE¼ó+†b—«/‡‹º8ßô÷O8 ön ŽRm4Vîn@¦åJ TQžúGY)BÛv–@bp5¾B7,úfáÃñh£¼Sõ‚«&4V–µ£œ†ÿ·â¼)ì–0 à€«w„‹ Lìç l4À‡è£L‰<¨—p^¨mm”'và åJ-4Rž:¬ãj£©] ˆûñ¡²‰k°B8ÝW .hEÈÆ&å©«ÈWX̦.Wàt¼‹&â x.ß: ¡á¯Ý!~£ >Æ‹&™‡Ñ}ñ:f` aÚ(щ`16c v! y €´¾½P–s$ázTÕ‹„-dI OàÙýTÂ"á”›Ž»Q5ÑMÑ5‘€taÑmŠdï? xŸáŠp8ï`ˆpDòc4Rð„x\,ª¡ƒ¡¾XŠ«ƒÿ¯ˆ¡X÷C÷*a ÎBå´RIÈ2]ÉÀ$À‡DT@ÔCS´Ù²•ØR¥ îÇFНL@ ÏãÍj󳇰¿ã[á^výñ ÕÑ硲À¢´€l±œ¼ÊÓà-Ü&œ@ò1 å0ÌžC >tÆXÃßË‘šKÓr[ ]PÉÔ›k± ± 9–wRÙ®Åù¹ eð¢7ªà,£}ì3‚I|Õik:ÿÇÙÑu2I~‚K;€Ï-¦uil0Qc+Ì×¢ö…$6ÙE´0ióŒ.q=ƒfäÜÊɼ“§³BDÔìô&, Ä¡µ"¦høáøÃiöïÀ4LÄ¢°vP Å ¨ãê·,j:pr0T`û²ðR0(ª1 ªkß‹Ý!&œ¦Í²:â±óðæa³ $¶Žöøƒ°D³ û8éLwòU¶¶MîྴreRðt4” "ÒêòCMLÂaû$Ùã°É‡'ÞÃo ‹wŸçiQgsnȶ@ƒ?´ég 6³Îű<-Jƒ‚¹ ×p„=-¿áÀäóMÍ–Õ\çæ LÎñŽ1Ü3n±Å`§–ºº€ME˜iM<é ?âñ Zé×5¸ ƒ±ØQ¯±HE$#ÖviMp¥œÍRnwaUÄàV’h î©‘Âgª!M§ãîj‰È *¡:ª ñ¶}ºðÌþUá ˜ŒDb‚r¿‹pî7…ø*Ò8=ÎÄHxƒtìÆf¬ÅZlE¦²ð|‡0ÂIU…‹2‚¨Ô-bq72ñ‚°ßÎÇ0¼ïR/á s ‹´ ¯Âƒd¤b¯¥Ž xp9HÇ.lÁz¬ÅF‹~ü˜Š¿ñ®µ;Û܈¿ñ–‰ ˜ªÏ¿’Ë·ÄDO^Öá™b¤ÿ˜ µò¹‡3—ÐËÞ:sÆÖY‚ÂyuN .‘Å¡%îDòž¸e­ã½‚KKÑ÷+³‡ðÎåBž2ŽÓ-ÛL:ßÕnÇåø=3·-`¢X4üൺ 9—/ ¾<±ìÈW¸–?ˆ»êpå-»46±`7‡]k¹ÅÜ2Fç–ìi,Qs*ÿ¯éýýég6?c MIJ¯ã{\ÌCÁ‰“¡xXxK¼_l²“ûx>ŒIÎ`E#$éBÐúùŽbíïa+¾Ìä3A¤B5~÷&mBfð\®³©éËêž}Ÿ&?p~oê•k¸Lt=¬Åë8Ž›,²ˆ|ÁÏéå+Ë4öŽ–‹íV×ü€#ÒQè";{“³J2ïà?¡û¯ŠŸV”ÿfu›ª^h“£k¡Åõ*6* ¶k²ø5 3LŒ/MP—Åoµàë\):Yú…(êª;ö?¬kKà…vÙ»×°i€¼.0È$ ا°×t¥6ÞÄ«uÇnáW>Áväˆb2càˆ¿ÇCHÓ2Ef®—Q1ì¬7Dc´ x@0n 02¼Û4ÖÝq/ZŠÌšG`GUAZ‚­I |ûE]Íp<àE….Ò‚1M|Œ› Rh?ö¿ò *ž¤„þš€×4볨+é‚“vƒæxCP©óð¨`¸ràÆôtŸÍ‘:U¹’­\‰ Àg:è~ƒgõýuš€Mÿ±b£Å†¶ÞÅ…¦ùX(êÔ}&óèr•á­ŠÇðLh­(Ä(|-Ö2Þ"™ö+ÇÂHÑ/‰¿‰xY0q ‡5àCgÑí²!ei µ"Vèg <0XYásÝGà*€ð¢½|þÎdŒXÏX¬]€äÖÁs˜‚‹BïÒn{@yÅLC%IŸ¸}ä¡;Þ6ˆ¢Žàü¥ï€ê€m%»ŸlÌ2Ñq_\«<“kÑ[à´I~eN”ƒ1¸CBkÊßxQ˜IÊl¶ÈàÅm,HÖ21?GüV –>\‚I†j{Ÿ}Ø®}G’R«B¥}¢Ö.Ù¸Ïzí¼¬3Lo®€W¶üÙ††¿jãvA] :#x+3•’à€G ™°¾2ˆ%¡¢åÊVWâhwHÄã¸L¸¾Ãcl·ÈSl*`8>G'x´E…àÕt¬¶©—µU±±G4|'rôÇ צbŽü™x\¯W±Þl0pœ/aù]säjõ’Su<Œdá å\‘º{[Éâ *^½ÝâQAÔê‡-3»ÞÅ¡cV“ЊX¨}Gœ AäˆÅ=hº–‰Äµ@ÔôÊÀfÃìö¡§Hm…"P¨¬º&í_gâ–Ђ·_XžÛ®ËvXަÅES¼&ž&㵈˜Á&"NÆ«&oÊ{¦Z~Ƨô˜Úƒ‰âè1¹>†ÆìWű&ˆúhåE9鎱“+hœK(.Æ~ájŒÒ€¢Êy0(D­~|b²ß;„áÊ<Ùñâ¬Ã™¢7a!ÞÄW¼miå¨j‘7xÐ-Är­Õpø F¹â×l…G'奆½ý°Î%®I7](°†±Êb&>!Sb|‹ÎÄÓø@¤ò?¢$ 2¶ð6Ü&´r#†+®™öȰLsåSÏ w›®}»•iVg©Ì‚q¦¡×ÿÑyC5ñÊg„TÃÿhìÿ<"aø†#A¡`3Ô2ä ^ï˜}£4{ð2Gûð‘€Gq…pý¹­·±·Wžð¢OH†·3Ä·¨+«*õËÎ8&§„NÀaÁ5PÝ+K>ë^”®±—‘O¢~\Qƒuf_âö 0ÿ`(ÞÒJîÄ\D•1RÔŒ K,ô³Iúž*Ð%ô|*zô*$ 2jy"gï1¬U «_'u¨à•™ê¦êÉÕ섲,Š‚„+Qðy5£¹Áo.®Æg6GL?¦;.æú,Ã7x/ã ŒÃoØ¥aŸšàeÁâ8Ïà7—ßÊÄLË%¯¬T M?ñ¥ðDRCU¿’%®L/k’Ýh4ñ^ymhbòsýQ<°ÈÒhççDÅïˆe—‹7èÒ±ÄÐ>tSdóMb*=2ñ†á"ôÄ5¸Ã0q1zàfL[t.žæÚV<æÒ™t±åÌÒUÃ>_ŠÁÿàm¬UîçZzÇ#hÒÅ-Àkèió)M£ òxñ—t¯"zþZ*ŠFã4®]ꢖ 4àˆ²È):±ú0c-ÁW€}˜ '¤a<®Do¼‚?±?´žøq«ñú£7¾º°ŸÈ þŒW\Xîûñ¥iž%(Ú“£¨Ž[B›â¼ èú¬½ã)M$£Ž0Í´kÜðr½:ÞãÆ#x_˜×qš¨û”å;Ѱ¡­P]=!ZH,ã5ÔÂ% ·ñµ­fàjÜ„ÙZ÷•\Ìà ÜmJ›†ábåiâ#$÷7¦™þnn§¨'àcKod+ëcªò†½"‹oXÁÖÆÌ£³¦Îðbþ‘î´Ç¥†¿~R*)Ó%ìSv§8¥é–]|ÞT´ñ¸/¡€s…Å:Œ×vîf܇~øÁ‘OÈÆhܤ,ÀUð¼ ÏФº-ñ™…ÑºÄÆ1¿" ¹¸æàYL5ÝM³ôu‚Ávâ(v‰R˜ÄГ~Ì1H’ÎèáÛ²ÕÐ"Sdßê‚5Þ}¢)RsKˆx’JšˆS ÏdðKvU¬ “ùŒ! ËÃÊWNÍI 9‹§…åc×]0œšÀáɾ¶~7‹-Q«:ä(àpC=r¶áž5,|=ƒÞQÜ#¶¦Èñ/“ÉX3‘èì%§»ñóÓ°4á–'^+Q+ïzÒòLµ R.çðÿ“É8¾`Êšµ˜5•g†*Q«p”Æ Ô®ô7å4!É<ˆ(^7SHæ(¡2û:fAÛeŠ}ÜS‚VÉ…JB¼vJê‹|^!¶å¬ ‘fñÓõº öÏ‚`%]‚C–Ï4²$d+X')„B~`y&ß³óxSņ$ó'Kƒ‡(ÏTæ¦gÒxX9…‹ˆí¥¾»—´À -Ñ æd‹|yΤ3æšfi5¾Å,Z½ Að|ÅgáI¯d>É|¾b œ­‰ðMf±Oà mú©Õ–Ы•ø”!±ñÿĬ—f¸¾³D¸õr¯NŽëlû]˜Û= ¦û8È&ß·}iÏÂàÔž¼Vܶ<¯ ¥Ë”²›'7q73 ç¯uxþa}±%÷Ìå(‹#lWØ™°ŽM¬ [¯àw‹KƒÝ8+¸<¯³]*oY¢ ¶Ó>}‹å B@z/®Hû8°Ζ±â yEÈã“ÀÑÊs9¼ÝòTªiG·CŸ²LV¼Kq·Qûs®ÆÑu÷ó~‹ËHRÂ|%Nw*ó7á>1x•ò²bºXë0u3-ïX)]®ÈÉ$qP1}mÚ&&¢i¡8²ŒVÖÁ›ÃÈ‚šÉû,+—ujxø±ò«OÄ Ë{Ù]鉫'ˆBÞ\äbÝPMXÊs”Ub/¾«Ù‰:˜OÒ¶Wt€Áv.¥dqû ) Ã-UCÌ’Ÿëø*ÏŽ6¶Wv±ìeèmb»†ÆÇš ãÃGMìa$HØípK/âLûsHA@¶‘.¤" rù'‡±u±¢\yƒiåòøÛ,~õ.x¾Ë=Ð]‰“!ð‰ ^ÑîoÆ×|–næ_‰º\¢E˜ÎJû‚`ûðGáç.Náõ.ˆÀÃòl)©Ád&i¸•³ƒ)Š0Í:¦8åÿî(6— Ýuš¾qEjriÉÇ9O‘žX8ºÀ†.¶ ÛaÂ.¶«ÚlÊÎìÃû9šó¸—£”gÚp/3y¾‹9ªJêÒð·æR­œèpÔ<‹k£0ü4IB¥!Ïc=&;²}ñnðíååÆáWì:= 0‡ðºžJ@ut ´ªaš!É¡Ot@y‹Æ?X…*zïÕò° t9€´,öUøp^28Qéq!ÎÒyØè„÷ìc²»Äa!ùÎ Ç.lÃìÀ>¤ãæƒÐ›Q[4@U†á˜§Ø¨ÁFüÂO3NTÂ$)ÌÛN!PÒÓüÁq½å@8®Ø16Sx6ßr¶–|Mû¦NQKM»@‰#§HChXà«Ü ÖÞBŸ(Ò <À?¸ÃÝXCv£þf+¹ìª .¦¿ @cZjFŠ©¢ø4 »`·a&îtŒSÀN¼Ž5÷ÎÄh×ù œ0W1U­®ÄT_-ÆQð ‡sví¸£QhžýZŸ-×q-¾wò‘¬¯Ù+~´¸#Å~¸!ß)­%Æ<ÌwÕ¡v¨æ*[A!6à\‚5 Y໫aሒ 8U™\sÄÀµpŽÓëázŒ³¿mÖ0àÇ\ôÅ:·’bq¡è=¼Rñâ;C±m;€•ì¸ÔÆ™†¿ 1IçîEø±3p.ÀCX¦Ië&µ‚K¬¼;Y êjü »j2£qŸàZüF ‡M9’-™š X/jÍÀ»”äÎí”gÎãnGmÞ&;$õkxeØÇ§W5m)à.ã缕m\(™<|?Jû?ù†òöDÅ®h–(ŠåXýký\ÎLt™2F… ×áQ|ŒëÐͤÉÞçˆáÕfc›)þDtRxþ¥Xg„±?pQз™>:Wá‚bÞÇ+aå[¹HD~fyæ]QT^0!Iúyá4üá®^ëe LuMžÎß°Äôw=!åÓê`>ßl˳@"Äý–ãÍ?Š7O磔Š;1 ¸LF.¼h‹K”;¬Ô'ìÅtåZmËéê0&‹Î §átù¥[1Çyé—r1MÞ/ÏÔ‡ð•‰cq¾òÉ\LF>€ƒ>¢žÄ£Š[éZ‹41Î"k‚‘˜¡¨ÕL@1h‡1(Σúp¬aàW!C'SBàw1ÒW~C€ŸeŸ¯b`C®—Ö›\^#.‘ -Ròu‚¾«&ÿ"¹Ô¤b.ÏwE;ÕG-¿­&Ú¼p ŸçéQ0+òbŽ î|;ØL¹?ªØË¿Ôw^‹)h¾ÅÕãhiÄ òKs6­Ñ'¯.;Ù$xöKC$ƒŽ§h€ÿ€(Úôó&Ë/OÑŽb§r0OPlìaEžÉ'ø›Éô‚/›> -»Xc2v§ÎŽo1«¹þ0åÐ)¸^£‡ÓÄP 0Ðà›ƒKñ•"\ü·¡Ð°·´À¢h³PñR®l#Ñ«‚Kq ö`1æa!–Úg5wI ê %:£3šYüšþ¢øñË' Q:Î5åí&Ɖ±Ù+âÝ.>Ó]<½HY`¡a¼2®Ã›²Óp‹áï³ÑTÙñVb*Ó[“¶œŠ Ù!Ë­µP —"÷`´áz ’‚X½ð!ÉHAeÔF4AÔEE±k<‚Çoqy„ì« èm’}¬ÓÓî¦sAßçBç 2HÇdœ#ÞexO€ZˆÏp…!Ú@m\¬@þ‹'‘Œš¨l¨Rc\@’…}»7ã”ø‹Ä#‰™Î(‘"ëB#¾$í-MÂo`œ§)ýtbô_ì³H!l!–fÈüe}‹Öþ(YB'\®D~Ãß¡ìÚ•ÄØ¯[üHÙœ*8mÐmÑ ÍP5Pq.ˆIn7J%=vc¼Ð†ËL¹Ç6h¢!œ¡ÄN "_¹U—F&Åܤ =yåè@>F›Kp–òL6¾ )4* €O¹“]ÌèþáÂú5¯îæ3„L5ÑÛô÷XQ‹uý´?»Ýš" @|)Æ7CsCÄ?#šB¬$Š™íæ!)xê¯b3«¬¡N2¢6ÒÖPÕ1ÅÊ7xŸ Lä&+€Õš¤4Â7_º¨©c‰l!åÁ1¹s>2P9ߢá€CØ4¬ªaÃØ5°Tù@Xi¬‹«ÉFr±à;KZ(~†cÄýÖ‡tæ,ñ­{Ö4RÈÃXY)ß׉?Xj¢ãjè+TqZ¢€ê6Õjb MuÐ&ñBô‘¯hS]ZI8ˆ1ÂvºþZ¬DPà41²)à[ÛZDD˜+ìôà&‘ >6…Yê#dªHÃô´4±ŠvÊ–Ud(äVÓ¶¶ö˜*ÌÿxÜ`8¯äâQÉ‹ÁºT»1>ÃÈUÙøDÞ~OÅ@ññŽa¿k($¡æ£ zÚžì«*›Ç¯Ç Ü©äi¦ –çŒ=x_ˆ>Ø þú^sþïŒËu¯êÂÌÚ€ À³u1ToÔØÉMÀO†¿ú fL™˜Žgu‡@ ºY6?¢˜ûÛ +a×ÛF,š€Eʵ8ÜlXQöãu‘ÃIÄ¡ sìÇgªÝŸŠcÌ’†Oäš q«(9ˆW LTSôžùëÔ›AœmÙ¶ ªÔ’Â| Ïž‚Ó"|Ó&|$¨U»àÿ }ª ‹}¡žÿŸn“‰&Ú XQg‹°Ï­¨Ä˜Â'lPòdƒ`{{¿Væ)ꤎ!“;ÜkúÍ«.~aÅ.¶Uê™=P!‡ -Oà8Ã3K5––•tNî  Å3g;„ä|UL™A‹P€QXú«1 XŠÑ¶ˆÅÕ+¥˜TLbv‡yŠqøÙ&‰{,À§ÂÕ…4ilûéMÀ¿ÁQ°þs‚å-©íCÈáÍ5ëÕ† E;•k X› mçÏ!žgùE ¾yÅ]²• QI¦²ÎâµB«+˜Þö‘)Ò_Qi.FH%IîbǨëÿ ì¥óF^®É›gŠø™¨­ïëgïSÅüsî4Š0C Þp¦!ºI8'ÆW¼ÑàÓ¿LT+VH$ÂëV·¯cC ú°oj¬tësAè™Ãì-<‘h±¶â»)ûç;´Gñà  Ê¡-Ð&Ñ€¶žÁ¸4WkVÏKõ®ìëyê1þàp†l)L¦ñ2MC.6‡ùUŒïÛ†m»ñk–³ü¢†Ã‚\<xE1hoÉ- ¾bÛËçBOøÅ• XKÓ—,äCá²Ñ#/_ÔÕëwSÄqcƒ Yüòñ™»mý](Æa`3Û°HÅ!€”èd^¾Áð“3XQhë9†sÌã/Ÿ×;g-`­ã2üA°‰øº†š+ðËÐ3ÛÄ€“)œdÛ•«wóÖú9R X!,Ú]llõØÆ.BKS œô&MðMðSøM²Ø÷8Íÿ €71G®›~?kfØõ¾ÃÄ´sØÆ 1µšs–fžDJk„ÀMåÝÆã0!O“aá¾Ðj˜¡±ýëÙåϤã6üA(Ïotµ[.Š{@ðüógž£eˆmü®<>&¸•Öâ‡"5FFKy¦P¯»Âˆ\„É"™w ¾ ø¼f½´eq·ÎÒÇ—ÀÎÜ®«á—š0PÞ j´]…'’ø‘m—R<A0™w ¡ÎÂ'€BÎd+qÇŽ$ Ôj1šReë:Aä@ðfý±¸Ðs\‡?H>¬KK‘Ïáš Gq|.´üÍO ø‡m·ng/‘´ºpše–†KøœX£&5’‘.:ÎøødÈ®.j†¿³.Ó€Xå¸*ëÃäJ…P4{?_ÀžìÖ?¼@óæA\dð. ‡r8“=ÅØG5íãéjPÈ‘bÛ. 1v«„p”RÇ’=Å„}pÅ¥Áµq§®¦«5þÃÆ.M0³Êl¾ÓAÊ·‰—i|‚kðþ\>Ý@g³¯&dkuNÐÅlð˜¥yèð´MCÄ`²¢Â@WOEÕý»Ø$àáƒúÄs´Á›ð×à3DÝ@ßrèö¼Q›0&•ó]®µäõ ‹Ëù&{j²q€õ8)¢á_"²Á•B'‰½¿JÐLJìØÍÙ¬Zj†?Hô§òSmLÐÖ!õÏÿDÑQuGµKGØZö²®…üàg>3¸ƒK8‘°'«Ûdì`·Û`‡ 3Ž#ƒ,Ó!ÒÆ4èÏCúoPJ‚mõй|ŽÔ:jžJ[ô‰8[:FôÌãxÁwWWºòÞÆ›x9ÏbSVvH8Çk"Œš¡ÑŠþ'˜~*wh#$õ°³ŒÈáÝ¥løC$p>Zq‡j»úhÀÅ<>)2LÝœ™s%ûjÔ¨Å)õù†.ïŽrù„Ø–‹‚ÌÒaÞ£íúd/y’\ê†?H±|AŸ{$7k¼3‚«@‡ˆÏ\c—Õ&ˆLþ—í‹™BÊXRx=—F´ó“…|WQY`G® öÅ]Ú¨FÍù»Ý«ÿdÃR9üA¨Â©úºïÍ!ŽnEñ!èå-®fâ6Žd³(A ûp–­,ÒãEÞ¿YP_q€·jgC»^äv/…Ë¿ÀúL„äNqx¥%!InY§X>èhøA’~nâ«<#âíÀËzÄ\¦”1ML=Y—³H’»9P»÷×Ó™YH£ZŠA¼@/ wˆ9ù¥1§ÒOr­’Bã9ÂõœÜÇÉìËZa­1¬ÃKù6×èϳ®0[”ëÕý6²·v#¬k/j*äËŒ/ÕÃ$ÿc·^ïàÕÚ.¨Á1Ì'¹BT&ðé0–å\®âÀ6¶é®|,φìΡϵÅXôâÑ®*ÇÑOr)ÏÖÖ¤¡]¦/’œÈÊ¥~øƒ$ËvRŒ=¼A»¦ðYf’\&’@"Ÿtµ¡€û,ŠÖºìÊóØ‹ýy/_ä8ÎãffEÈìYñ½føÇÒO?¿W"•¦œcÿê_¶â'–ãûv}zwicüÆq0w\.j ø˜ÆÙ÷šÞð,³˜Å<»T‰ÁÏ©¢=u~A?s9Ú&ÙM;γùJÛd/¥« dòiñ ØCzp)ÉÕŠh`y¹]o!ã‚"q qFÇŠ\‡“HàÃÚÖ‚ç9%žØTj?aXÇž¥Íã¬n³$Ndÿ᥿àãua™e–<äp”¨ÑoÂ$ÿæåÚ-ÏÇ«íT¾$¹ËœëçA°ý?ÛµÌÏé Æý<•Ó\îã%KxH¢=ø7ò1­­Xïê *âo.Ej߈H …³ƒl’>4äh¦q”(Y«Â—]1„%G…üQ4çˆá@îà^h“£5g:ð!>>?%@¿Ú·3“/Ù$¤Màõü›ßŠ.Sqìï"ÑaI@_-ùËq·ò gD`m»<¨°Á~øC$pŠ]2{’,àtÅÛXšñþÊžâfц“,uK†þæuâÊUŸŸp./µIa‘ÊÇÏ1{8ð^ü¨Ïod-ëØßf+HàuœÂ[Ef+…wÙrÒÑ'€L~̦BM<<‡“ù¬mzÛVœì,lÞ—ž$r©!ÒzÂklÓa7à;n˜Ö?yÆI7üAHà½ÜïÔþý|Vã$y”Å:“4rµj¼Ÿk”½&ZpkLNbØŒçÛ0}`<¯àgC!§ö–“}¼ÊÁíd!ç³—m>À ¬¡a²’ÜÏû޳«ç1#üÜMÊëµ¼[¹)ÖâœÆýÅ"€,þÉÇØÊÁrX_bÙÓݹ“®bï“àÔïšÊó7*½|þÂ+m :ìK";ò1^6øy˜‹ø:/Š˜A/Ûð}w±„ 9mNê¥_ {s¥»Yø-/(–É·yÖÇ÷ð3—{¸„ã8”g³r1Qzx ŸsˆzÂa¾„[àr2 h§Ð[ÌeÁaÌÂhÌGv¾|º¡:ª < ‘,¤av` ¶bÒ\æ–áEcôÇ4r×Åkñ$¾FÞqŽã‚` ‡¹ öIò0¿åL-ÆJ`^ cÇ&0ž±ôE)íl ÛÚ§[7#“ºâ-zyqkŸ•Í_y:$?>¥<ÏçÇvÑVìàý¹Ñ¿Á|Þ½•W7ð^ªÇ8Y´¾Ä±ïåOáØ*æs»ž$Êžâƒ` /䯺8#òZ°„/ñ|V‹¢3X$Cß”ƒ8‘;Â31ÝÎa¬t¼ç~©â:Õp nUrCÙ"ë1s±ÛåØÕ%Rq ºà<œ†ZJFC[äb^À"ø÷ïï+ àÅéx½”Äá(À.¬Æ",ÄZì@¦˜j=:ˆA*ê£ :ât4Aj¸H¬Ä똄ŒÒÐýÇ¿ ɸ CÑ!¼i@>öcÖ`5Öb+öbTRÊø„TÔ@#4GK4E]¤Fk7>ÃûØ\Zº¾tÔB &`G^Ǿ\l^þÞ2ÁÍ ‚µQ8à+V(þÁ.Üñð/ ¤(à ‚àeúv¶XHìY†=ìå"h‹AIËpƒ x:C}œÇIe‹ÿI„ æðI!¨„ ¼§Ì9ã¤A/;ð]n³Qp_dó¯æî$—kˆAS\€h…jH™÷ù‘]X†9ø›OL)~tð/h9 5ÑõPI ²°[±{N4Ý]ÊP†2DÿOGÓÂñùV0IEND®B`‚ostinato-1.3.0/client/icons/logo.png000066400000000000000000000440431451413623100174170ustar00rootroot00000000000000‰PNG  IHDRÈÄÚšnåsBIT|dˆ pHYs]]IÅX¢tEXtSoftwarewww.inkscape.org›î< IDATxœìi€UÕ†ßs«g&™° 8™éª^’€APÂŽJAÙE@dSDEÅPAQDAAE¶°ˆ(Ù ë!¡—ªžIBXY&Ó]÷ý~tO˜™îZz ÉóÒuëÞ;Ýuênç¼GHb='ÞßFîG@Ih…¸qÝ4€Õ VC¡dŽP/ˆðÿ„|¡cƒ æÏŸ?`”»¿ž²Þ@ìY³ÚÞxí=”èÏòfU½ b¡®Ïöfç’tØÍõTÉz©“m§L™°rüøãI|@Wƒ«ëµÂEÙlöÕ×½ž¬7ÙjêÔI…¶¶S@œB`Óf¶E`…@.¡!e2™eÍlk=ÃYo 5°¬£AüÀ&­lW€7ù±+üµmÛý­l{]e½TÁ Ó´ À€ì=Ê]Y,ï§söÕë×(Íe½„$µNÁ:G»/ƒPð ¼~4ië $˲ÆEˆ?8¼†Û] ,È„~”‚ˆlFp2€É6+ýWÕØÅJ[d¿T*õv÷¯Ç‡õâƒiš[¶Qî `ÇðwqžˆÌðxçªUÿ~þ•WVÝÇ7†ëî%"ŸñqT¿EüŒ´EöI¥R¯UyßzXo Ä{â³Dé;jë–Ë u!üý˶ý\½m[–5#B|‹ÀQŒ·-DÄøX:î«·ýõ¼Ëz©À4ËÚI"x½A€×€3Çù_Ãûaš[iÈù>ò–Þ<õv¹\îÍF÷e]e½Œ ‹MWZ? Èd¿r,€à˜”m?Ñì>%¢ÑÝ ê6[&~—ÎÙ'6»Oë ë d¦in<†À5ïwE>oÛö[­èPÚb¹D2 (E°k+ w] Ö“÷Édr£6ÈßdÄo»sÖ'[i°Àqlc }W!ž (*š¸\D®]ÖãÃz)Á|á&Ûú\šÎÙ'ÍåÜB‹º5Œ…‹.í\½jOÏø•`»˜ižÒ¢n½§Y?ÅŒÆ>OáÍÅþžÉ9ûŽ…“k˲Ìñ´Ÿ˜oLÍ™[Œ–1¿WXçG®®®N‚{QÚ"‡ŒãÛ¶(9 ÅØ’ŠØt‘éìÖÂn½'Yç d|[Çw èö)2 ÀƒÆÚIu*›½ÄŸüÊhÊ~­êÏ{•uÚ@’==q€gø•!qñËŽóR«úT Ôê|ÝY*"ÂÏ´°;ïI"£Ýфʸ@‡O‘\aà¼F¶™L&7ÂÀÀT(µÉNãU'ñTìT¥Ï5ÐZDVè×@?€~V¡ôÿ*‚~’²{å?I˲fض½ ‘úÄ:k ±X,ª€ý} ¾¾xñâ•ÕÔ;½kú休ßJ”îQ"=Ôè ‚n=6†(€Å¥¸Q")ÕD‚Á=)k©TEÅ+ï‘’ô\¯¬Ç›uv+aÆÎø¯ë,H9öV~ul;eÊ„wÚ;?d(½);B°€XÃ;[?ïx‚§H>%‘ÈSë}¶Â±NȬY³ÚÞZúzü\7ˆSÓ9ûÒ¡%“Ét¡ðiPöᎠ¶Fxg±Æ+<ò)w笇×o —³NH2;˜Â›|Ь¤¡Þ—Éd–%“ÉÍw÷'x€½´·¨›­æu€·kª›í^ûÁ±²¥=Ú¬›bÆn%x O‘{@Ü ÁvÇÚ;JÔÊk æ@xs&—ûçºl,ë¤$Lk/ÑS\Àd9ÁåBYA‘å"\!À*’JDÚ5ÑH»@w@dc!¦˜`| :ù*!·Á•ßfú2ÿׂöÆëœLïîîrÈ¢7ë|‰ÀsÖ¬úË‘‘ò“loö¯|?@ëÜ9ˆ;5±ú~üUCÏQmmw·Ê=…Äû|®É¢8%kT‹»‰ð/ñht~²ÎK۶߆ÇZÍ:3‚ˆˆŠõ˜§ˆð<@6hpõïˆà7®È/š-*"ëŽÍRRX™yÃuÝ·:ŒÈRx‡?“É9;$z¬(úL@f5ºO$†V_}/®QÖ ‰ÅbÛ*òJ;4¸ê×ù¥+üU+¨¦¹«@~·2B.Ë8Ù¯þ;ï-ÂoÜ«ÁÝs!¸ŒJ}ï½$úž6îîîñã"‘sI~N'¹T€wö÷ÿ.Œ¬O#HöôÄ©Œy&VsÁ#2ŽsÃÈÏã=ñY"úH€»Mõ,àÌŒã\ó^XŸ¼g ¤ø¦Ô¿ohÅÄÛÜüõì@ÕBÂŒÝ_Ë[?Æ8Žíu=ï+¢/ÐSWËyÜ»¶;J¾çÜݧwMŸœ°¬kDô?Ð@ã`öHçìcZmÓ»¦O¸G-÷æ­ý®gr™»Ç­œð~!.ë| ìbO'ÍØq ¬³å¼§ $}Ám[ýˆ£Xíj@¾×±Á„mS¶ýÏÖ¶ÄQãi¾ß*3Éü婜}¨wðl-íxÐIðʸiÞF[ª„ß(ÞS¬®®®Îñ‘ö« 8¤ÁU;rœF>4U“L&7b¾PóÂWÀ]SŽóx¨²"FÜ4/ñ­ZÛó Wk92Û›}¤Áõ6•µ~I$ÝãÛÚm‚qü}µ[Ø~´R©ÔÛü»Öû L ]–tÓ¶}&‡£œÕ(z”âܤe}m’$Z« $;ÀX-ž—É9Ÿîëë{£õÖ…vå5ß«T¦Ú{2¶}£€»è­µÝ $ÎGͶš:uRëmk­$ÍØqõ €ÍXí*¡ìŸvœï޵¼loö¡py•·ê W®¬Ú@ å8Ï bìA£§EÍG"OÄb±é ®·á¬u2[fG¦y)Á+ÑøØ Ùx³MÿÞà:F*—½™À¡ÕÝÅËê9«I§ÓK&Nš´—eg)õ! ¥ùxÂ4÷ll½e­Z¤wwwoÚaDnм/•zV:—{ºiõ×A2™ì@¾ð!oy12®cç ¼SoÛ"¢bÑè59²ÞºFÈÉ)'ûû×ÛÖš$¾¿ÃˆüÍ4TªŠd9-¦PøAÆñ¬´Eöh„qIÍå¾Áµ¨om¯L˜æE"2æžÇ1סJÄ£ñ})êq´@At5Ù¤ZÇ4ËÚŽ„¯†W‰…Ý3wltÆ)’:ã8Ç€¸¦‘õ‘3â=æœîîîV…fÌ»»ÇMóÓ¥”fm-i°ªtk­ƒÄñð?,|A(ç§{í[›¹Á@R‹È±‰¨© ÓÐÊûw¨Èí3gÎÜoþüù ­»FÆô$aš³¹À¸6«|ûÍ pª1âQ󿨼c—ÊY%ÃhÙ)"*nš·ƒh†¼éÝ9óócAeeÌN±’–µ3€»Ðã ßïtûÀßfíÄzb³QÉ8ˆ3'Nž´U*—½¥Õž³$u¤£ãHÍeý좨sÍXX“Œz*Qšoÿ-D`S yy¨~è[‚<¶úz›‡RºLZTˆŸ¤sö…óæÍËFŸ`Á‚ïÐUû¨6¦?è‡Ç£Ñ߉ˆ¿td“sbYÖ M܇€¸‚ר¸7“Ë}Û(´ÝÀçÁ’™±Xl̬EX®^rOº×9{T:3‚L_&%‚CQ'ð8@.#p>#ÇÅMóõô¯^Æ”X–eÄý6ó-(¸KDnðþðµ3í*JR/\¼p)ˆ{üJ+òKáën.¢ß=%°B+9v,ô§lû^@½†‡ÃÙ ú,Šâ>ÅpjÂŒ5T@¼ÆŒ$“ÉEÜŽ`½ª¹.pˆoVQýJ!÷+ôߪ$ëêê JÝ(\³{%`ÓãÞk!íd¢ñ†] ƒCàèó“ÑØÁõô¯VÆŒ0Ÿ¿H€íJÍ‹ŒëØ_\ÙÀ®¡ë&¾“Êå^úÙ¸ 6ø+¿À§ Ç··>lMEä±âÿp©´µ]4ºñ&}€ÿ†-¯5ÏÊ8Î ¾ŒA±z(¼Â²,³®ÖÀ˜0d4ö@‚’N¾ÔV(ì³`Á‚w”âYaë&ñp¶×¹täçóçÏ äÏ·‰iV^ë{D䂱–éj(¹\îM‚ÇWqË>IÓü`ƶ¯A·òÆqÃl™ÝÒ³»Q?))>ÀÇý™K¥ÐöÁÔ¢Ô¢i–µf¸¨7+”v·MõöVôf÷Äw¥}Ó*k%;e³Ù ÔË5ÓÕÕÕ9~üøIZëM•Ö“t(‘7èºK †ñºã8ËH2aY7K$ò…T*µºY}i‰¨õG¾ª°à–´m qÓú¹§Üp~ÚÉžSw'C2ª""*5ðQŸb$ø™ŒãüQëÏუ䔴“ýµ_‰¤i½äçßDà¹lΙU¯€srjr* w-ÜE€]PIØÁç<.Š»uor? ‹/Ìå²õô§™X–5Ñ æ#œþ±¦¡¶Êd2/Ïš5«mÙk¯?Jo-‚½R¶ýPczëϨH·,n+ä·m…xƨH<ß]D?ÿ‡ý™qLØeÐ/'aY¿CÑ')ˆUðý~r7ƒX–5Î ^`z•!°†Ú:“ÉF×§mî—ÖG/ð—÷䬻Ƃ‹$¢ÖÕ¢è€"ñÔ¢Ô"HFcQx‹ï‚»Ò¶ÝhM¯òfFÃ@Šo×ÕÿñO¿Ìåt ¾·š:uR>Ò¶þI7‹w?ÎälÏôj#‰›æ¹Í¯Œ€w§§bÖX˲Æ)àÇïÄ5ÿŠà÷ÈG®|àF Ó4·Œ@Òð–E]ƒ~²íïþ;µ.‡àD¿{(8)cÛ¿­¿§ÞŒÊ.V~Õê³r“T_:mÈí‡#„qxõÓjú“qœ9ôíd߸i–%ÝIöÄö3ˆÿñ›1`ð>ßc¤àÄÍØ¯ãñ¸ßT¥©8Žó?@. S–Ä1C}¯\…Ó¼àw"η,«*¥Éji¹Äb±¨ˆÿNÁëÒ¹ìðÀáÑaê§à¼Z´a…úkE%tŸ2K“ÉäFǧ%LëoT¼c3q§!àWÄÕ –Ue˜nã !Âÿ¼i«ÇZ£iÛv¿+8ÄÏ…À¦ŠòÝFôÓ‹–O±¦u€Ã|Š,¡¡¦ }ÈãÝñmÄоo `ß`ÂŒZc ¦yiðy /"¯“8 µÅÄÀBƒði¥å5 ÷M%òFAäÍH$ò†ëºãL1È)¦PcŠ(l `K»Ö²àÁ?taø¨Ü*’–u‰Ÿ$nJçìaÆœ4ÍÓ ñÛ 0¨·jÖ®^K dšeí¤‰Çá“Ü›à—2ŽsÕÐÏâfìbOªßK¨9,Ñht“6‘—™\k•û[ 7úÑ<ùD.—{³žúb±Ø¶Jë}ÙÀŽ?XMâÜLήj Z/ÉžØ~Tú²†½zµ[xßP¹¥RFâùðÉoBðÖŒã4Åë¡¥’0­ÁÇEDˆ§Ò½ÎÎC·fgËìH_ÔY¿”ÍEžÉÏ+êúƒ–uˆ,üX¨;¡pEÚ¶ÿѬxd2¹ …Oò…ÐâÖ‚k'Nšt\«\哦ùB6„ÿyWÊ×ҹ쯆~7ÍO änÿûôîé\î_uu´-3©—©•ì<òÔ:}†Â»‚ê-û§z³åë)^Ö,FG`ˆ µÂ¶m¿Roª¡¤hÿS Q|®+8 ÙyMŠ#ÀÒ7YŽâôrK¿òžË8vÙwŸˆZ‡à>÷=™Í9»4úEÔ’Ez2™ì ÐwXàêJ. ·8Ϥûlÿ7LHHjR} ÎsÜCmÉÙ?lµq@&—¹?“sf•dCƒæä³ â±f;.{ýõÝJçASäÚ@€í’¦YfàJxšß&Š;ÅM³Ñò³­1N…ÏA€eŒeˆ%yÊ}ë'~ÕÈøˆbW†>9'`Ü7íØŸ s˜ØLH2cÛ7NœM _†·`ÅGß\úÆåh/ï]aÛ¹?N/Ö¿ ºŸ‚вÀqlüæpR]õ¥©B]&˜ãÌOÖ/ò‡ÚzVN2™Ü†{€šS…<0Öc–½5ÇE‘hî·°€_J˜f͹H€5ÛÍ·V¸Ô ™\îAžÖHNMVìW†®º°Òç""~{Þ%ÞqýçÕ¡Å|áF[ù[,SSœ',+\4Ý(‘vœ¿‘~!®ra<Ü=¬DÒŒ'âþ •ã9ÖŒö"ðwg@í8Šdz3ó<äw¯ÝQ¤iBJÐ"÷‰ÒZFÒ4·EpÚ°»mÛs#ˆõ˜ç•‡ôy%8PÀ/ ÌùqE¬'æûrm29û—½R(}Ãôîî0€™3g¶'Më7ż-STÊ»ÅZß\kåuÅ×·‹ÀAÉdÒ_>*M1˲ÆA2Í7z_ ž^5!1MsKœéÛ’à[/Ûö“iǹ]¶¬K´+ÅÛ“==ÍÑÞ`&Nž|²Oö¨ F›o´ç ɩɩ«–¯x˜þsÿ·WV¯™v•r°øf ÷š9sfÅmþ©½Ñ¿ÁßK¸] u‹n4Å@y`Àâ\» žo­± \îŠü­Æî #B9þ=s2¶½f÷'eÛ?.©:1 ʸ»Ùñ õ0oÞ¼¼D"p*]ðèi¦é7íDÂ4g3RxF€|#n\¼xñ°3 ¡÷3P*±ÁªwV}¤Ò•¹œ[\¬'Ô«ïÛœ)Vðôê!/WŒd2¹‘4¯¤!Ó«x<> â« ˜¡Q~ª«ÚÚŽ˜V™À ƒ¼¥ÕR5ÕJ¥^£«öCQ/w$†¦üØëÞéÑh EñੌÂêX,ö™0xš%Ú{êK_ß>0cÑhˆÙˆ7 7˲fˆ ¢ÕRtý®ŒÎç÷Úfeƒ¦WpÝó}ÚÊ xp¥à«T*µã³rÁÈÞ}=NCÏjM¦/󀇇`ÿD4º[¥K®¨«BÇÝ_SšNÂ4×LQ‹)¶é»M¯ˆŠm@&—{€¯Ï›@ÕµXo¸(Ê—Šäèz"ä½zÀuëž^Åb±r×uB®M9Î3^×Óéô­d¿PYg'&-+@ïitYíæ/P9SŒ2GÓÒÔ+Ø}½¼²ó’¦ù•wÿ)süJS°­×áeÑÿŽ•¶“‡–úd<奄ŸEš0Åâçü¯Ë}~CÔþqÝ$žìëë«;Á½Òü‰Ïå|tàyF6›}A¨G°ëH\·¬`×™Q¢¯¯ï xx\s·¤e SÑ”Ša äƒN…$ÿP¼ãí×_÷)' i–‚ö™¦ÞÜ@b±Øt,¿2Þïw]Ûú_üB™‹}Àl¯ë„\F2R¹ì_B*›‹×Ä¢±…íg«Y90ðKxhë’®fX…3g&aÀ-f¦2Œ!àCr{¯kÙÞì¿øæbŸß:ˆ†ˆ "ÑÚ3)})6ÁW…#Ä'M–©“ !Ôè1”´“½ÄCmS¢çÄ{⳪©¿U,^¼x%([»2kø¹ˆ®kgÉc¡´Æ{Þ·,Åóû*HùG{ÔêÔØ)V°{È;™ÞÞç¼.Fà?zÈ÷ »GÐ;ƤšÑc(ã6œpÀGƒKÊ¢ô=ñîx²Ú6ZAwoô*xœO¸ªíÓƒÿ¯• ˜ûû#C“ ö-¬à9‚”* µÝ<Þ¯"—ÌЦDWWWgÐî‰Çý4nuÀúÀ¼‘{éÕRZ\zÊôháÏj©wþüùF¾ãs!|Œ`3ú^˲‚¼ZNQ•‘7QDôšuG6›]Xe>¡,é×ù5½$ý „ø€Ÿ—±Hˆ“蚦Y 3qmm³ Ä,"žÓ«âõ€õQÿô ÊÏÇ(cÛö‚Zë^¸xáREý„ˆšÀŠÔÙKh­þRésBöšÇ|µ.à‰*«'Á³†n´¨¶6ßç@Dz¥Kgz^œ0áT>Çyáèˆ èôPÊ(e2¡ªúÇ(Gè¹ûBȽõVŸÊåþ#JBˆøkÛ2_¸³Ña¢õb÷Ù¡²ÇøvÃØsð}}}«V»…= üÁ;yyO@pÀHY§R­µ!¢|å°âåÞÞªÏCi7Œ}à„5йz¥¯>o5¤mûޏe%Äu© ˜ˆš¿pr£Ú¯Eu…å‘yÂ}QÉ)QqIe?g^›ÍÙÿðj'nM¨½!ÜÞGO¶&°u± B E"aZoŠøŠS{RÜîm¾tuuuvF:ö…à`vòS(OSp{lOâ,äF>¿^Í#ÙÉÇYýQß7\ dlûƸiŽÈïðë8)µ^Éälßî-£Ý¸ùBÀˆÅ±LÞ5}òÂÅ —†©FF8BNïîîr#‘½Iî-½ jË5%kg„tÓE©*œº*‰÷ÄwÑß×Ö¾/Á ÕÜ‚€oHæ×Ý‚ÆÞ­Èܺë¯@Æq®J˜±N€þX"øAÒ4§§a‘’µ’J¥ÞN˜Ö˨j[Gú·Ê@ú]7·í”)VttáÑ0"{€ÖG& ñ1ªÉj‘žH$º“fì:QúI ÑÆ Y×ú ÞAX.®»~ÒNö× ÌÉš~@./ô¤¢# E‚d`qÇ‘Ÿ­7þ¥´Û³QçPQ'ǺcU)fúÈŒ36L˜±óQp<£øÇ‰Ôo â§ïK–ér5’tξPa¦OmZëÛš­xñð”ep´ç c4L†§n®f7ËsŠ•´¬=HüÁ¢Ñ¡â'nʆl„â—üA'±,B9XDªg§‡~‹6šj ²ís–5Ä×üKÊd¸kææ3w¿d~°·p“ ‡¨ð#H£¹`"˜`"ˆÍÌDH#ð+ñ¨ÅbGg³ÙWƒÊW4D4v<¿FÙ­Rƒx# zV¤sv™´è¬Y³Ú–-]ºÙ©¤Ùä­¢(8!Nš9sæµäÿ(…nz.äiî2HÆqN‹›æ– üåú‰múǯ¸ND>×,eøxÅZ 3xw|‰ 0=…K xˆÀ\P¶Ð{'OpoÚv®)ûXDbSc Jóùd,vT*›½Ï¯`Ù+aš•ö³ýŒ#Á-÷¢ °Câ;8í8 C%› ±ãÁ°Ùšö,ôÌM$›‡,_¬}€>Âåˆ1 K*•ZMCíAˆu¿3Zq$Úc‘Á”¤e}Ô žÁY!C_äb?”Éå6O9ÙƒRŽsY*—{qh!ŠHÕFß鸆~Ãïz ü)išž/ù5ë‰}XÅ;.¸wÆÉ~cx<¹|PEüsÔMÕ¨r{ õR¼¨ïÇ IDAT؆r}|ž¤å'Ø™Lf™Q(|D_PY!®jª»'¯©Ý$æÂ'ëS —à§39gjÚÉž‘rœgý¦‹Ã<|+¨<‚ ¹¿–Ì]Š+fì«/ÅüJéà=­º=O~ í8åg”À€}ÒI$AzezT2«ÅŠŠÂsû  ?ëª1¶F±°¯o±@ïƒàvœ(}G˲E¼v«Æ!ÜŽ¦1àºO„=wÐJ%ûô5%Rkj;ø«‘Q“Àà’Ï ÞùãnÏ䜃<Ãd%Ø@úþá†a<œ¼àqi$Ä© Ëúlpû@D ¾o§ÂÛo74/aXR¹Ü‹¤ÚÀꀢï+Æ–e…Ûõk~;w²P„vi­ ֖ψïß^o …I\=2~]Íœ9³ô<"m‘Ãü¶V5l "¾1²|¹¯ˆÈŠŒã\åa$W‡93èxKD"u«ðÕJ&—yT(Ç$v0œ_£q(ßtiïÂå È÷G^C‡6H$À@´ïbF8¡ü¨˜>zqõŸ‡Ê4©•ï¬ü(Ý ˆë“J¥|ßl"!4‘HßÉU‘ˆ¯²(ºm2òDzbD㦠ÙþÞÞÞeð‘ וdT.{ O ª!–4Íï4½C=‚ÅÛ F¤ª<á‰ht·#ò\16ÂkîîË÷'Mó;~/’´ãü@v×D¥yc-iߺºº:Aœ[éŒÈµÕÔד³®EÑ{(»&zÌŠÏUi⹎r•ò_ƒ„u®ÔzXˆA,‹,Ù>­`ü„ ×C*/€cјÿî+'€IÁ0|;OˆHYç“ÉdGܲ¾­4SürᛕF{%òŒJÚ¦®:EýÔµñ·ÉhÌ3~ZT˜<}€ñ݆Ÿ„‚5""F²Žf¾° ´…ë~{G&ç<2†=•Ëý.Ï¥¾LèˆD|ez’==ñ#ò_G#ÓHŸOš¦g\vQh€Y­H|+‹}äæoÄ£ñŠŽœÉhtk*ãÔ?jx4-?³,k†×õbàZ¹kù„šWvuuù¥¸^ƒŽ œ ]DO¼lÛUIv¥”`¨ÂÈÈo$LkN2™,îr²|¦ð.þ;XÅ€7žX4¾aYÖ­q*OëLår/{àÓŽóS®P¸ à…ñ¨ùÀÈ€’‘Ò’^dkÿë¾ °)ñ¨ù‚7 Š‡R„ðº–Ée%+¸g¿‹!¢¯‹ÇãÃÖ?‰ht{§T!c%jb¼¡ñu¿™œ}¾<5oKôt¶·û¿$MóCo]`ÑÞ© Ã²ª0p,Fœ°—øó…ùÉžØ~ñK*äk +W®Ü*Lض”Îg’Ñèû âAP@ð+`ÄÔ€¤Îäœ#ñš£ï!®žŸ0Ío úd¨)FWüÖ *H`†hÔ×Û•FÅ ™¡D•«×L-âÑøîõ j[ˆW‡øçÕ#©iG"À©‘ÄéÉhÔó»3MsKwÂKr‰¸3Õ›½+D}Y¼xñÊøY¸óÍ©x§@{ºÌ¼@a¸þ¿õˆ\ÒŒGQÿ†÷3ùPÆqî*ÌIêlÎþ2—zܼ! ?{kéÒlÂ4¤‹ÃS˜˜ã鵿‰ Á<‚_&&‰ÊoÙlö)ž9Û€ÀáqÓ<"iš»ˆè{á= i4Ó‚äÒéô¡œâW@›†ª¸~°,k\åïpk.G›Th8Ž ÁðŒËñιN0à D{Ά7J˜^‚ë¯Àça—$™¶íS)8 ž™{d* çhÈ‹ðsQ~—ÎéѨ)"F2}Ü4O˜æ…I˺/aZ¯ J R‘·EpzwÎÜdÅ@# ¾o–¶Bþ¼…r w5Ÿo ,,\¸0Pm>•ËÞï^©äÐ!þ@ÁŽÞ÷É÷Òét êJ5¤lû!¸Æ,TžnùÑï»uM g þ›:šrÄÐÃP í‹ÅbÓ•æÕv Ù?2ºVnÔW€k ‚ï JÅãñiâê2 ÎÊØ¶¯Ã_Ò²ö'ƒ#ùZ ¥ÇsôÊôîî.׈¼ÿÑíµÕnaÆ {KÂ4Ïðr~6“sv¨F ½ººº:ǵµý®R’oX¤Wjd ’!tVDV–² ×#‰úÀF*÷ˆˆŠG­Óþµåip“Vòý‘þR–eM4X.Ì Àó¤þJ:—óõB›æŸªû±š M;NHµB nY' q¹o!ÁiÛ>!nš äx9\jc×LoæßÕô¸ŠzTü9BÈÜ6 ¶¢Þw¤Ò@‰Åb[(òt'a»I|=“³ËÄí’S“SÉŸÈ9•î!ž…¼¶+“ÉäFÌC-›M@Ào¤§’‡Cåò"FÄCc@?ÁwࣃLÁ±Û¾ºšþÖCÂ4wä4:-y0«^´*Ÿÿ±Wöäª:”Íf_MÛö™yê(в)½75%2L]0‹í˜0­9Œ?ãŠÂ.ñ¨—ØA*•zÛì àµÆõ¸ft>`ó`$$)@£¥ˆàûð1ü •ÆZ©åh­qä!¸Ù Þ:í8ßõK-^ÕRv³ˆJ˜æGâøøL…dÅ¡Ök·ëõLÎÙ‚¤ï‰Ï‚ÒU›Ì‡ÀŠˆ[˜¾°¯¯bœÒÛìA„‰µo‚KÓ¶}j-·&ÌØÝ?\²ÄÓ9û˜šî­ƒ¤eE¢Ìõ¤ ,äZáÚ0©€:“x–¶ÂðPÓ•\Ã8Àñ•®§ç±Ò¶Ò¶q+¸'ã8µ¦€@—OÁû%Sù>Á?6ž<©âwÒlH~ʯ»BõàB3 žWùö½S©”Ÿ¸wÓhSêãð=Gã“C¿“…‹.Íôfæeç¶”ãüœ"Ù0íôèH&pIÜöϰmܸ‡‹ûÝÛèˆÎ£ŽW¼xWx2ÝhÞI9öÉ™¾LY¸Aµ$LsÏxÔúg)Ržò{…MéÜèÿ2"ÅÓßLD a"(ÿ›Éå*êxÑPQÀœPIO¡æâÛBž hg¶5ï´h°âÛ¤$–J-~¬0só™$,ëЄi>È"Ÿ!WpéK‹)ª7¢w¸ìãWFI™RãbÑè>Þn2ïBÈíµ¦±k¨¼ì8/!x »O3MçEúO³(jCŽÚ¶™Wöçó¯tAçóÁû T‡œB6˲&º­¿sÅ7œTFc§&t/4ñžž]_Aeé\Îóe)”PRBµN¯€F.Ò×À?¸0\¨ãT m‘HøœkpÏ6WoÚì¹)|~<‚_{î{S¾è³²y2“³w™æ RÜ’œºM_h$-kg!—‰ë¾-&¼½jÕªÈx‘‰.0"CËd(nCm!ØÎ€ˆqXDâ>TGûêØ€×ú\¯ Ó4·Œ>b)šËär•…IBÐðÃik»!„üb2™¬|Ö‰<À/g €cÇù_·cî)à®"ÁYVr[ÆqÊ¢Û`†iZ"~†Æ¿’ÔÛ¾\+™Aðº öÂ!x\C^tÈ¢Bÿê·ÛD½Q€d y†Ä\ o!ñ=öGƒŒÌl–¶o3fÌØP‡ø•‘r!ì5D€cÃĸ¼ž]Á†H*•z R)z¬ŒI,>çQÇjø„Ä‘cDĘ˹…”ã<-‹Ú{¼ <Ò+PAäãðyi¥îüÿl6ûjÆq¾pO´;–1ú;—ly7‡ÂªCƒyµaÜ_ésB¤{îow󿯩ƒ%šr¼OWý*\AxÏ!~Ê#ÐU\¤Ñà}êJùöýü4eÉrÙ—!We³Ù2Aï´ãÌíØ`¶ž ÿo4x”㬂2*Ó,÷FM©Àÿe2™—+]²z¬½°‚ÛÀõnB4Å@JbnarVÏö[](Ü Ÿ| CÞ""ð2×Q0>´•)ð1‘{¼.ÍŸ? å8 bLðìp©›'É9[¥sÙ+ƒúC êÓåz‰Åbð‹C×x]RŠ!u~ùëª:V©­z+ð‚`˜QD`N¨t¡¯¯o•·´ò™!Is*ýÐZËçÂä:ŸßdCI§Ó})ǹ mÛê÷‹à‡ïk‚À‹”`çŒcïœvœÛ§’ ØUT–Ÿž+­ò1² •ª¸Æ3MsK0ÔVüc)Çy¦†î £i²ÉäÉ· X¿ „œäy².*“ˆr9)W¶1@â{ÙÞì#A}(¥€6=[ñŒª¬L*—{1eÛç¦{º€‚àBŒÌeRoCðÀs„zfÚ¶§¥ç[/Ûö“# ék d(ĕaYÖ8ÆÝ¨{½œ #"ßFˆØ ê=€¦ló™7o^>µ~[r¯öD€ nÛêo¡‚«vÚqŽGÍüvn'Íœ9ó!–qÄ›†TRh)g ÚÑéíî ëX_¤çYÏZ–õãJ](z,ÐF M@ò6ŠqÛË@,ƒàM DáùùüÂ\Î {ðe¸îB×ðËISñ׋¡å(}ÓaQqz5£§ç}PF‡ÊW6™4)`öަh…ßÄÙ°xBNN$?K§ÓK†}N2aÆ®Cqì…¹zùòã dÙPçÏe³ÙPoíùKæ/O˜Ö;ðkæCôvÚ±CÉö×¾¾Å Óê‡W˜³jÝRÜÖ§oÜ€7ÑnTŒ±/(uB„k‹àwr¼ljŠmÛ¯Êó¿D€ (èŠÁ>4‚õ` 9›ÜÖDÂ;ì•ð“jé[¶‘”Äþü"µìoã@áOª5…pS¥ÔãEAñP'çùÉ€XX90àZД֧J'ñD&—)s-±,kdžhf@èV•×% -S’ Rg Ô|WŽ«¤ªžvœ»P!Ö6ù*€'Nœx3”•…Õw噇J$ƒÒ¤]|2÷Š<ë§èѶš:u‰Ó‚Ê,Ëž%ÅTTW Äè!‚_¤z{G&ë©›–H6›ÍIñÀ,ÑW”ïÖ@’”àµ!'Ã5N¨v›oñâÅ+â%á)âºu'Ç ü„ç5à”Îu’/æY ÐP“§3ŽSæÎ“ˆZ_ ÊðŠÑÑf†Q5-êZ90ðSa´^·ê_¾²lÁžuœ›úº0ATm³¦òÔ¢"ñ•°¹6Æ Éžž8€¸w ÌX[¦in ÈWƒÊ‰”‰Dbs†Mã-8«±ü•h©,^¼x%T¸;À³ãñø°\ $ÝP£ˆà¤äÔd`(æH6Ùl“{ЧÝ0i|[G˜¹ð˜bøÆ{,4u1 ÎF°Tí éRªaÜKP!ól‚gÇÓ±±^Z-õˆ´mÿ¹èGH‡¸ú·#?Ì:Î5ƒò·wÐ(Ø–QÊÌê³™ Ï´,«A‘„ÍŲ¬qÊ›E8oAooµ[ᡉÅbQA¯[Á™#Ýf’–õ ‡…h†BžZk¼yZn ä©ȱWbv²ŽúIMmœx¿ðK‰htûjûæG(ß+™jA @ÇJãT=^×E‚âëkŸ¿@ÐâZpKÚ¶‡étww×A"Ükn—½­;’Q1”ã< A¸H/âg#§K™ÞÌ¿A\á£D êúj× ¶m;$üÒ%à—ds,°ÕÔ©“J)©½¨<­iI3v€ŠÑ¢Cx'âºeiæ:"‘Ÿ„ †"°B¹yßD«`T $9E Ò &éHáÖ‘»Zyèï XhzúøH{h…ôA´Â…l¯ë„\œ4ÍU[o«(DÚ.‚N²PÎkÖ´$ÞOj0Pa“ÄwGNñ’ÑØÁ ¾¦üÄK_¹‘Œš¤R©×„òËÀN«–¯üÅÐÏr¹Ü› ~Á Éhì3ÕôͶí~¥Å/‰æ8Bî/ ]-fì»ü¨_L÷Ú5Ëàø1kÖ¬6‰èëCŠ?›íu†ÅkL3Í­(ùë×Ü/mÆEµõ²:FÍ@ •Ëþ D(ÏKONFcÃmŠ»¸Cá*9Búö­˜¸Ò3ÔÀ&€ü#aY¾Âg­$}¹”äÈ‚ç{ WÔË[Kßø>‚Ýr´VrâP¥‘3flHÈøä(ÚŒA}P%ßf0ªÒ95Ä®€ÂßÅb±5¹èHR+œ èPp³R¹ª0òí_ P-éqW";ªÚºM2û<„A‹Û?gç†f´?-ûÀo•àw#óýýW…Œ§hùâÂ\.”o#uI¥R«µRy¤I§Òú¶¡¹Ë³Ùì 3Õ>YL÷ž…‹.eÄø8üÛ ¼&aY7ù‹r7‡Ù2;’0Í )¼ >"ÐB<åŠïÔ«fâñøÆZóO~ž^6Æu [X'Ló9(L;"¸°)©«aÔ (º¡<¡©%!Z_;4¥tƶ/B8›%£Ñ­«é[:îÓJ>Ž  â`·mà? Ó Ú½iñx¼§/ê< È7á'1Hôö÷“=ª‡Òy•ç–r‰•ÐÆ0õúâ¨ã»c8”¦Ç/²´)Œ €´ãü `¸½yb¿¸i[´ÓPÇzÇiQ×Ü "›Í.õ'¹3lÈœ¸eÝUúñ›ÂVS§NJ˜æÄÕ/ @׊À ¥°ß`6àFSZXPprº7½ÆÙ4¾_kÎ £ŽàWph³2îúQW ¶FSJéöw W?LÙö¹ƒÿNšæ‡Xô/ ˆ<ãÏÒŽŸ0’XOìÃJé9€„J= Ê%ã6ìüóŠ+:=D–¥;Ôé|"‘èFAŸJðÄ©çò"ø|ʶï Ùߪ˜ašVòüÓOƒ?dœìš€§dOOœÊxÀ–!šq•’=_ÎfkÖ×­‡1e @1ƒÛ¶úÙ0²öExFÚq.üWܲNâ7A7Ü;í8Aªƒe3ênFUŠ„\*P÷<¢ÂEOéêêêìloÿ°&>!ÀÇQ]ÞœRI ¨ˆˆšðÝê&ðœì28½+åt>2K#*83³Ã9-61g ´¬I<Œ°¹³)ǧsÙ+ÿ™0­<ì¿ר5¤¨Ü0fÍšÕöæÒ×*ŽsA??¡l( ÉâÃc¢ø†­>“–஼ÖGçr9/Ç˺ID­Ë!81 ØÛpí¿ãd2¹ò…‡C«9wfzÏ5Ó×*ˆ1i ˆÆŽ‚ðj„['iŽHÛöŸbR™þÎÿtÚ‘¶È.©Tª¦ÔÏŹ\ú³û6мÎLÙö/‚‹ÖNÒ4¿CHPü…+ZÜuŠÇã‹«çž±#§iÈ^™LfY}½­1³HI:—½–àQÂ,ÌÈ? ž˜Ï_2¹´Eö ¡¼Ç@áîZã<ÒŽs»+˜!Ä%]ñj-Û´’šnÑØB?hÛN™2A\}BGqKš{¶qû2FGA’ÑØç)ú†»«)8&cÛ7kÖ ÿBÀ¤€w§s¹ÏÖ³K‹Å¶o x"jOU\-¯¥a\䥄ÞHâÑøÞ"úN}ß]×”w"`­2„Ç¥-²Ïh%ɘ7HZÖþ$nFe €ž“rœ €¢óœúQ1å¼2í8uç Åb[(­Ïä¨à6k¦àM­/ifLÇP¦YÖvº¸. Ÿ=?ídÏ¿{÷@ÂÉ& ·b§æ/™ï¥O…ãwˆIDAT ÐrÖ €¸i~Jй®¥'@€«¦æÌærn¡ôã>„€µ‚—§sÎW±(Fw‘ý@ì_OšÞÐàƒyõ@#2âVC"Ý¢îà+!DÈo2Nö+@qKÜP¼#„šÉ Mè_µï󯼲¢Þþ6’µÆ@ }L ïDpg ÞOÃ8(“É,KD£»AÔ}šþ®Í8α>”Jšæ ñ’ã_p €WêUUñ_âUˆþO:—{®Y†AÄ£ñÝEô_pÖ!À éœs$IÆ-ë0!®F8%D¼µëî×××7Ö’­]IËÚCw‡<(€ÿÐPŸÊd2½Å9´{{ר@nëØ óðùóç7LrÔ²¬‰õ¶šÒ é¼TrÛÔ\ôйœ[H˜æÙ€üa·§‰{]…Ï6Ë ¦^Æì.–)Û~H¨?€ìSCx¿¸úɤeíœÉeîà£È[BðÀþå+îèîî9R½÷HöÄöS¿ xĽ4³vL'’4­«PÌpòìFþ*푦ùˆ5‚µÎ@ ËýKÀÙŸ'} ÉGfìœt.÷¼[L”Ïý“Fä¢tͺEÒ4¿B¥oƒÿ‰ÏHÛö©–eÍ4ˆyZ#n–?ŒÛ ó€VÅuÔÊZ7ÅJ2™ÜŒùÂMf‡¿‹j¥ŽìXž´Ý…àíÇÿ<(í8uiH­ S,˲Æ¿…à‹EW å¨t¯}KÂ4¿NâǽÞÀ(§¤sÙM±ÁZ9‚ ’J¥^ëΙ'PÅá˜ì®4ŸˆD>±Ú-ì`7ù-y(nYAnk5ñx¼Ç <Â8ÞÔZ>–ýH¬Çü;‰Ÿ#üb|‘VòáµÅ8€µÜ@`.ç2Ž}zÉ0¬ÎìÆ¹~œÑvežúK„\P¾MˆË“¦õ‡b–¤÷ ÓÜS\÷i€A2I9îfÜ4¼ ‚WÑÌCˆD¶¯”Þ`,³ÖÈ ǹA+ÙÕOd$hõŒÞJÁ±|÷à ‹|á¹±(ÖP ÛN™2!aY—ò.ü÷À5öÖ"_'qG.ÿäâLÎÙ{dнµ÷Œ@6›}†ú À?Uq›Ib®û(%þù2Š~òHÂ4/ÝvÊ”°[ÍcŽx4¾÷Šqãçƒ8þÏAo ävî¿@Tãmð Á}ÓNöŒÑvjkõ"ݸi(Ào«{Óa% —8…•“Â%'¤²ÙÀ|ˆce‘nYÖÄqqÈ¿/Ê/!ú(@ªÊ‘BðÖöBáÄ—-z½Æ®Ž Þ³–eM1(¿è+â\à#€ì‹0ÊÀžó²ã¼äÓ—Q5d2Ù¡ 'J1O`±¢(ä½?ª‹IyK(§¤rÙëjéçXã=m ƒ$¢±/CôÏCê. åe›!œ‘¸ ®s¾oÛ¶3òâhȬY³ÚÞzíc ú»á¢4Y$‹b°V€cbÙ½÷K¡í˜Ô¢Ô¢Zú:Y' Œçv/Dúx…+rñPCiµ” ã0Ïktý#ø/(gezí?fô_3Xg d¢Ó¢\R휺ðˆüA"‘Û …ÂøVH,ÛQ4ŽðPxß6€~€?›Ðßÿ“±æ…Û(Ö9йïâ¦ùE œ²F½¼IÈ¿TáZ]""’èéÙ"û³ƒ2½önVÓ0nÖ"ßÊf³¹–´7J¬“2ÈÌÍgn°zŠ3@œREÜB£y{ÜÊ ]a‚„DÄ0Ms3ƒü ˆì `;¢µ1ñ’êÜL.óh Û5ÖidÛ)S&¬èè<–ÂÓÂä¦h«¼`)€¥ ÞHà¦L1ÀDÔ¢rR?.·ÑUez3¡t”ß+¬7!ˆˆ7̓@ùf·‹uU„\¡{q+£Çë ăD4º”ú<4€ {´ûÓBÁ£BÞ¤ò7-\¼péhwh4Yo ˆˆ$MsGMøÿíÝÁJ[AÅñÿ™F-¤ ‘î$@Aº­J÷}õ |ߣk7.|fÕ·Ð,lé®b[SðêÆBáÖ™ÏűMÒf’ù~û ³9pïÌó¶È¿e:½ Þ…ëëî$&7Í Ȉª»åa lxÉt¾ Æ¡Ž0ÞÇ@÷¡ÃMçù/Ng1¤´ a Ùšª³•ö´×õƒSAÏŒÃèÑh|¬ûm¾:ð€ŒÙÊòÊó4_®šijNø ¹ãbÀ¥A!T +0)ñÉæÃá,þj^Œ^´;G î¾ïhø‰Y)YLRTR4¥()))&¥,”F¼ˆ!Ífó|œ-,®âɤ*Žkÿbè¯[Å—þé£\nêÂTÝüKÙ[-:i]Å’‰™%UÓ—F¢?·/º)ð€d•&óŒËÅ’Ñ“ß o4÷Ágææö3.ÉÈ’Ñqq|)Ó°³ÙضŸMÔ‹$³“ïߺ`›ü½ø«Ä›“~ß_¯jÆ·y'¤Õj-=m4vÌx ¼+‘><»ºÚ}¬7òfÝ Oe¼ö³‡¢IEND®B`‚ostinato-1.3.0/client/icons/magnifier.png000066400000000000000000000011471451413623100204160ustar00rootroot00000000000000‰PNG  IHDRóÿagAMA¯È7ŠétEXtSoftwareAdobe ImageReadyqÉe<ùIDAT8Ë¥’¿k“aÇ¿ï$F¬©š¤ƒ?èPõ*B EpÈÔ­ ‚8Twݺަƒ‹‹ÒM;TE!"ˆšAÐÆVÐD 1ÑØ &oÞçyîžs°V$y•âg¹›>÷½ãÁÿàÿjרµÖ–ˆ¥ÈÌ9b CÜ âcxfz‰ ̶´3a ÙBÃhw­#Ð2IdS±‚ÒT”À".îN:ø®0 ºZ°ÖcìÝ“„ÒTŒâœç°°ÅO‰t•E<æAkÊEQ‚•È ¬X~W±   !2ÖÔ…ï:“±Yc¾ƒ 4 ­?G -Ô[†.âžÞH’ð '=¼~ׯH÷åðÃËű¥i¦Öì”—>|C̲C>²)qWð¼ÚDçk“¹Ö6¥‚'w.ëû¸z÷½kˆJÆpQçÈ0 ñ§toIO¤^Ì>ŠÆÊ ¼­Üo?uíÍÊ‚(gOĵçÓûMí?2ŽÕåg¨VîµÏ\¯g6Wø'gh¥‚ézõéíåÊ"²ùQh­Ò}+ü‹çFãDjÞ‹%¦‚Îú•ó7×.mIs§33\¸µÞØr‚(~J¸7¡ª2Ë¡IEND®B`‚ostinato-1.3.0/client/icons/name.png000066400000000000000000000053751451413623100174040ustar00rootroot00000000000000‰PNG  IHDR¯Ìï®sBIT|dˆ pHYs  Þ©dètEXtSoftwarewww.inkscape.org›î< zIDATxœÕœ{ðUUÇ?ëò~òB!%ˆÅ@"Í`¤‚)iÌ2EQ(’O|MŽö0² R¥Ç€Q‰:ˆâƒBÀP‡³@Tð…"¦‚ °úcíÃÙ÷pιç>ÎëšÙs÷ÞwíïÚwŸuö^{í½®¨*¥HDÚ£€ã\*¯¸´X¡ªÛJ‚¥Ë)g æÀf`°DUßOé_ç ÅnWÕwN#ÐÃÕïPÕ7cdµÚTC"r<öw«ê‹UbµTÒ/é´­Pôsªz° Y'gêѱÀnL^Êô;T51ýùÀ{€fH€Ñ€¤á&È:X“‚ý pjBÛ)û—Æy8'{õ×$ȃ½¬­Êý1XÛ¬Gk€u/0¿Â¶÷T1~í2à€Àrà`Ì}ÀR *n‚°Fà— À{ÿÿJQê'~e ^gà寗üº€Iu ¼ê·Y=(/Ð×)Å^ k=)/Ð{ÙãÚ¾l¶¸gýþ °øhv‘žÀlÖ h 0ÇÕoSo™‘NÀÀTàWÝX-"TõQ1tÐÅå7Ó€ÇÖÀPà|l©iî‘mªºÂkÿwà’ÜÁ˜²,Ä^ª(mÈп8 Ì.ª°}-é*@€Ø8\Qfû_ÇÔOú¸üÕ@œÙ¶' TD†¿ŽöªÅž÷*U}Ããm†™cocæ›`ϯŸˆŒTÕg‹DÞ’^ÀNBÍÓ5.d|ƒûÿ øÍ™\¢M7`¿ãßtˆáàÇîs@C†þø3ò¸ üå̼AºüHμ@Ooüø 3U•3rIó Òv<Å+gæÕ33Æoxíßúñy Za³^À¼8¶‚ܘëá| KááñÞX{fÐwÉØ—¦PÞƒÀØ#¨¼ó"ýQàÚ#©¼Ølí›”óÆ äø›‡óÐ9ø¾@H·.k€!ZÁXU÷ªêTàWÕ,‘%4éäå_-0TU_)·_9’ EäóM.X¤ 0ÑßÀvíÓE䨦îëS[̼låªfªêšà)J#U} 8›°ÀÌŠÅÁ÷'ð à®n6“쫬û‡èFàA—o\ŸÀ·ÙËLTÕ}ªz Ê~Õ’žrŸ-¥"òÉ&–™“ ðsÌvèˆÙ«G‚®ÃÌO€UÀµÕ€©ê~ÌuLXCDdTð%ÀŸ§æµXrîј­˜=cx:``÷yb åçm6\ üÄ+o:•Ñ¿ŠÍ7nï¸ö»€vØÌ´ÏÕ=O†}A-ÍìEÚAh{g‹ ØÃ½¾l¤ "]1»l#t9oF©9ù¸böEyv·ºb`‘ˆ´ŠòÕ1Í~ïò=ûš¨ÿÓ°ñ˜£ªo«™yw»ºÄŒwÎ48ÆåïRÕ×k¬ªËg\ñSÀÀ0¹Ìæ©SóÒí^~dÏ­.ßX+"½xëŠÜxMÄ\@ƒ€»Ýia.$"­1å³sgy_ÏÄf'€""yõ#†|Se^øs½üÈæàèϵ–¦ªÿÿÜ ¸YIUw§Á±°^DÆÖº?yªîÎÅ–3€¯`>à¼h ¡ïôNUÝáõe¶Üƒã99ö#J.ýWU7怿ÔËŸVÀΗÁ\>UÝMH¡çݧ±^UÝŠ*¬sUm1b–ˆv˜Ro¤ªo_"ô˜\$"—ÕZŽˆ´À6j`öíOcØnòòWÕºq$"mM†r³ûÍ] „Q^UÛÙåA[½|§$&UÝ|øW=X."òéZíHU_Àfºw]ÕÍ"2&¥I%4èêò ܘEû±X銧4‘¯‡—Ïet&Z°:w*`»V°k^´ÛË·LäâŸxp1vr0 X'"ŸÎ©5#UÝœ‡.>àSÒ[e#w„z¥+ÀìÛ$jêÙ×?n ]jYÀœÛQᵦî^>ÓTUoÅ®G×þ*"¯qßjNªº øŽ+6R;ðìª(À"UݜĨª+õ®x¶ˆôMâ­íðòM¡K¯€×ΞÊS ž¼’¤ª«0ßëWÕ¸ßÙWuMª:ó¢€=ÌD¤c•°W{ùÓEäÙ´„¹îÀV€UÊ.EþsíšÈU‰H;ÌŸ ðZxÚ€ÓrØèíŠ[ÜÆ&3©êóÀ 7BŸÅNqêžTõ:ìV„>àŠŽmEäà3^Uì$+-ù3ày"Ò£ÙYÈùô[wpN¾îA^þ‰v‰9 ssø5ìj#À}•¨êà˘G`ê‡açh2ðËŽÊ}À×xù‡°£÷,i­kÓ\^Ür(x¾ÀY9àO‰ÊjMx̸èV«#=wÞñá±Þ*±–xXYŽ{s?ÎØïvߨû…–q<Œy`‚¶«Ë³ØŒ<ߎe´-÷xx˜Ç¿ž "jR°»^X h,¨ê{ÀÍN›"´Óª&™Š-ó`qIU ù/ß'‘«ÎHU߯.ӿ쪦‰È¥e@ø³nYÏGí‚•ÿ|§—Ó¾LY+Õ®8øj áçžÏTÕ÷ƒåëg„6å©::@DNÆn:½-‡mD¤™ˆ\("YÏà›{ù¼|Ò¹Úõ¾³±Uìp¡äND.ÁOªêˆŸOè ¸0"͉ü(Ž;Dä¤jEär,lµš îJ¤›}Ç*Ä,™P…°ØQsླྀAUŸˆð4ÃÂwnæŠH–èßá^~}"W’ª>]^Ùyš§·Š= ­Šªº›p"iO±íXSRÕǸbkl“Ú¯R<9ŸÐgý0^U÷Â’lDÅœà-Ê´M¦b€ÆH°}°Ùø-´IÁŒÅK×+KÚnԉ̓112α6/æ1Biž!c8VÖG°ÃÅf¯’Ï•Ê#)Ä=÷ í»”m‚™8¿ŽŒÓ”"ž˜FSɹ˜TB±Z9žh”èïH ÿša»á€ÿ1฾³(+ÉæR¯Êëp®Ï ¼þÃûz¥²<¼ï{xßÌKy]ÛFìzfÑKŠë&F]c>âïaÿá´ÛL?Œ7`(á¥â íV`ËÏ•.ÍÃ6Q»"¼ÊP°îØ=â íà.Sû²„Ö­òF”ó0åÅâ·‚‹å[¨òb¹Ã<›Õyê8V£¼Æ•ˆ*vjº ªý®{‘bfd4~'pf,vŠÐöÀÈþ‡#Aºè]æ숅º§áî,g?ÊÛ€ùbã”w¶'gJ5r"¸·x¸£óV^‡Ó‹b3"KÚƒyI‹&’8ðDrÿË0»î7Ä)šÁù]ì”np¯ªþ30YN»{ 0€ð`ãE‡‹ª>\&æI„Q"KJõÍE•|ËWjŒkODzcÿ-𠪮)§O1xmQªº0R61×ÐÞjäx¸±ÿEؤª‹RxÇaQ 7UÛw±j$¦K(þ‹©`–]M¨K©¹ÿ'óŽÂîâFIEND®B`‚ostinato-1.3.0/client/icons/neighbor_clear.png000066400000000000000000000014071451413623100214170ustar00rootroot00000000000000‰PNG  IHDRóÿagAMA¯È7ŠétEXtSoftwareAdobe ImageReadyqÉe<™IDAT8Ë¥SMHTQþÞ¼Ñ7?©ù36 S¢ùYµ"Qj!‚m‚ ]`®„–-ƒ¶2jQBDÐv‚¢(0œHÆšJ ç9ÎäŒoœû×¹ÏúZôàp/÷Üï»ß9ç{†R ÿóy·;L$QŒJ)»hí  ­LÒú‘Ö;‰ááaaü©€À=tö  õ†ÃaƒAè{…BkkkXXX@6›}Agçþ"ˆÇãÏ:;;û£Ñ( ÃØV6çÉd©Tê^•À~{rP 5Yæ û}–ß„”œÁðÑrø&Lß®*ÁêêªV›õþ›ñ7ëÞQÛê‚Nî|{΂£ÙLÆ-Cƒc±¨›M”BF‰àBMp/xá <¾>zÅF9ŸÂ?±…Xž…mÛðx<Õž¸˯OÔ’ÔKþ¦Cu)w24› äã0ÂÑÜt^¯Œ1w-•Jn,„€—À}—­ú(îGtJYqâûmÅ!€4PÊrUT(.'ý¡£-Ž=G¥(ÔÚ‰Þ‹‘Aª’UP´g¡bWP‘–«Ä4M—ÀU@õ?*~{ù\qÁh_Ãø»±–Ø)(çÁÖWPò´Á=P^¾êØïQÌ/…FÁûá÷ùI «¾ºÕƒJ¥ò»•%—O|þ°Qâh¸…啸ãTóÚX‘HÚdétzSÁVòóãžÓTBÛb¾ùzZ ŒuÉõ¬-Ërúu ÖVÖ.œ™™É¡†ªNüô°«W !ÛÏÏ'§§§÷‘Äk#Ôñ=wí¾\.‡r¹¬÷©”ËÆ¿~ç©©©:´SPØÜøøøÆ¯ùæ[³Lh-OEIEND®B`‚ostinato-1.3.0/client/icons/neighbor_resolve.png000066400000000000000000000014021451413623100220030ustar00rootroot00000000000000‰PNG  IHDRóÿagAMA¯È7ŠétEXtSoftwareAdobe ImageReadyqÉe<”IDAT8Ë…S]H“Q~¾ùéÒMÍÑ—Ö4+4\¡!Ò…DDE]”±‹2Ê«í¢.ì./ºŠ.½ ¡ Ø &A™†w2”Ôùï7²6 ·Üêû[ï9ñ‰+¥žóÞsÞçý;GÈd2VÒW -†aÔ®&ÐÖøDz‰ô B ¹¹YÇ_"øýþãtÉ#IR]YYl6 Àˆ766L&‡‹Å>’í¦Ûí^Ïbðù|ïeYfá2»‰ªª™`0˜yìíXk÷¶æ0rZN”––âât:‘ÖRNUÑÛí"EæLÓÓÓ°Ûí%%%XI.âükÉehšMÕá,,íšýì-Ïuo_Ë`SÃá`u"‰ ‘÷“Ê9qáàeèA7t¾O¥Ò—®=oZnóV‰f-,²(ŠMC >ƒ}¹$ë~LEƒXù¶ UQQ^TÁÏCáÿÈ}ßE–ÅÌ€P³8!î8²0„D|­{Û𴱠럿jsrh\ù©4v:?«]×9 ýC9Q4EòG’ÛØ<Ū“ó9_ǘA¾é¬ Ìè&É¡ªáí—!~§AhL³þÏLG“PÑ~Qã GR6¹]Q4v6ŒQ?ÎÐ8¹,²HñxôùÒéôfK.ä>äå@Póqçå j¤&­8,ª¯Çìü<ÆÆ'¼‚Çã¹Bìý.—ËVYY «Õ Ap÷Õmãä±Ú­ñ™ã\Z]…ß?:©kú#¥ÛÛÛ{„H: n‹ÅRÌþ+gP~=’ÈÒLhO›õFÿ@ÎOV{£œ`»tww’sÁ ¦†<Ñ/¥î˜êš­Ûñoÿ;¡öAMŽ«½º·óß#@#*£ìIEND®B`‚ostinato-1.3.0/client/icons/paste.png000066400000000000000000000012771451413623100175750ustar00rootroot00000000000000‰PNG  IHDRóÿagAMA¯È7ŠétEXtSoftwareAdobe ImageReadyqÉe<QIDATÁ1ˆ–eðßó¾ï}žœ¦AW¤•`^¡ E¶h 6´\CÐÒ N„m- E) ÕÒRXADÞVáYžò×÷½Ïóÿ÷û•Ìç_~ðü€›§.¬ï(ç^zàQ¼…µ]Ëw{á³¶®ÿ–î:æÃ³¯ÛÞºáÒ© ë`À¥g^9³oùž{ýóÕû2S›mÌtâÈ>KGOÊ̵Ïο÷,.À€û—Wî³ùû1Þ¶þÉ;d"]Ï¥´ñów–W퀲ÎEuû‹Ve¦Ûi-Cpó·-åøÓ^}÷ó¼1[ùõÏõ¿»|nm{€h£¨£gÚì?¤6„7Ÿ[µ1 ‘ilaèøàÊæÃ}é¯>ùÚˆ:­’¡2é»ÎÛéÔui¬áÖvõÔC;?´×Ò¤[ù6ÛOD‹:ׯ¹¨MÔúÂâÀ¤/&}±8é\ÛÜvèN^|b:æbœÉq.kˆ±‘©-¦RXèÉ$ûbqè\ý7Mÿžy|ÿ Cg€¨sQ››¨A¤ÈT}×%E$;z¥+ú¾— Ç&çMÔ&j#€EúÂdè”LE)E&Ô±jc­‰±‘DKCGW:‘ô….0aç@f çUŒM¶&kÈHТb‘•(\ÞžNOìÚ·*kµ‘‰Á¬N±ÌêT‰® xûë/?Ý£píðóºö,Ž2éJQ PÐwwˆ¬ó’™àä™ï¿é»rn…Ü¢ AQJd›îYZØù?¯5I83ý…IEND®B`‚ostinato-1.3.0/client/icons/portgroup_add.png000066400000000000000000000014151451413623100213240ustar00rootroot00000000000000‰PNG  IHDRóÿagAMA¯È7ŠétEXtSoftwareAdobe ImageReadyqÉe<ŸIDAT8Ë}SMhQþv³I´qÍQ¨–@±ŠbÕŠ E¢Iôâ!/ÚÒƒz°ÏR<”¤­-žÔÖ‚¶©H[±”\Œ‰öEi/ ‘Ú¦?Úd»›]gÞ&_˜7»›™ïûfæ=ɲ,üo%‰.Ã0.麮yakkkìIÿH¥R^Jèðù|Ýî2h”Ó„ˆ¦¿ß‡ÁÁPþ–œL&#œ wx<LÏÌ‚ÙMÓ‚i™d¡@`š¦A™ššÒ …‚‹L•d²Ïf³¿KÆÂB–€L„ÏÔ‰o 'WT„°¸¸$Ø-þY¬ÒÆ/ĉ€ß‹þ¾gØW}H°óB£ròŽ…I$ÚíMx¶•åeÜn<*ß%â`µåç.Ø X*³r ?°²,C"“‹ÆÏ `™6cõ#âÙ*ö@fôN2Ç/&=/n3ΩÓè\Œ‰²„½8I–àP”uvá¹òöälÆÎ÷ðcÅ*oÜ.+·•‰úí%!½dØjæ æ¼“¨š"—Ïah¼WLEÈÅ:Ù¾|úó\löN`&7‹ðÉã¨ìÂðÄSÄÇ»‘×6€Ó© ¥¾FŒG× ¬®jB²Ëå¤2,´?@Íž rËÃx5ÙƒÚýÇÐýù9ª«uddèDé ¨ªZÉÓÉd2©sË}…SÚ„º½õBQÓé»èÿÐNìƒF›JRÛÚÚ®ÈùU»+UÕ‡Þ'Mšùýì÷ù+é1)•Es¸ -//cƒÃM’-¹”‹Å¶[,9{* AU=hhhܘÏ磆¥ßŠ¿ƒ‹®3»$£É×tzÌÖõËDÒÈÞöô<>\:ÿÅ{1¿“¾Y{m»Ò==pP)|…¬¾7ÿò/¥Oˆ6ôèIEND®B`‚ostinato-1.3.0/client/icons/portgroup_connect.png000066400000000000000000000013541451413623100222270ustar00rootroot00000000000000‰PNG  IHDRóÿagAMA¯È7ŠétEXtSoftwareAdobe ImageReadyqÉe<~IDAT•ÁKHaðÿÎ|³Î´k("¦Ò)ôС <äŃT`‡Ñ5:EPÑ%¼FêPdtJ‚"è"Ý-²Béhø~츻£ûšùž³_{؃„Bþ~1­5ãчå>JÙD…+Rq%¦µÆAÞ¤v‰”²ƒ‰j'å¢5äâb ú–cip©ðk%O 02å9\¨ ©¢^)T#‰D<†¤m‚ ‰l#à!•·mK÷¶4™àBƒI Ê%¶óâ&°¼U€(Ó ûx5¾Ô]ªæ%ÇQð%vJ!(—¹D£mb9]‚—-~y5E°ÇÈ”çH¡Þ ±zIµL+^šqNÜ@¶ÀA™€³qËe:†‚º×“9Â…úiÝe9'᯻‘èL™Ó³2jmh3“6A.Ï7 @„ÌCº ULÈ®¤cÀ"U8 çÌòï32’rèÏÆr´áÑt„ `a!„¬Ð4j Ô…œu !A¹Ä¦WA<ƒ“è¶l{Øsþ«™õàºE¬-d–üE 5ê*o GÈ$B*°¶íñbhk>K2?šÐ30wqIeWs£*â§ç'ï+ÔÔ•Bv£½ÉBH*”ƒ2 ¯(a[& û13úIEUqjnòÁ<ö ¨¹ùbæ^£c\Ž™B€„mÂ25¬„XߨE.Ý>4ûqpÿ ƒÏ¾ßMØÆ“DÜe–©±ž.agÇõ˜OÁ|þEqöû >åOÛš“Ð:ÂfÖǦ[DÁ †•cç^ÄUZ éÎßQØ)ùÛLh¤·Š(îúýÓo¯Má?Ðc+ùRF…Š*ƾ½¿žÂ!üЇâ‰ë®QIEND®B`‚ostinato-1.3.0/client/icons/portgroup_delete.png000066400000000000000000000014071451413623100220370ustar00rootroot00000000000000‰PNG  IHDRóÿagAMA¯È7ŠétEXtSoftwareAdobe ImageReadyqÉe<™IDAT8Ë}SKHTaþîc8]¼£‰¦Í¢•EVFXô† ,h- j)¸q„‹ˆ©qS Åv¥Y‹Š…dˆ„6£‘R$Zø˜4œÑÉÇõ¾:ç¿3öú/ç?ÿ½÷œïûÎþ_r]ÿñxüeYLÓTɃ¼°µµ5ö]Ò¿††† )¡C×õ³@ J‚ã@DSN8¬£·÷Ô¿%'‰cœ\RRR …0>1fwŽë ›À À:::jضí'A9™ìS©Ôï’1?Ÿ" ‡ßTN./¯@:½ Ø]~\Vé ãâDQ¸/ž?CUõNÁÎÿ„FåäæŽ8…I$š½Ix¶Ìâ"n_Ù-ãïÀjKOžö°TfåÀpQ1dY†D&g× à:cõö±v³{ ³z'Š¢¬39ëyðæ1ãœ6ŽÎtT”%˜ÙöH²EUóìÂs ä½ÎyŒŸÚ±”±„ Ào—ÀŒ‘M¢~oH˜^°Äju 5ƒí¨íŸ…¥cÉì ®H€œ­“mìóŸç¢6ƒÿËGÔ]jB R…•á>Œ¼}²TØðùTܺ¼C´Ç4-,/B²ßï£2\,´6cÛ™FÇbÀ›((Ô±µ¢“c ¨T×X¬¿.wPÈišáî$“IƒÔjÌ ¸9œhÊ«R[J¡?±î.´µµ5È©={÷Ô4OŸávU™Å„@Ê vàŠ- WÓðcm禺_÷«çª¨ƒðÕ¤#E"rÖ¬GÀvxC&åµýåVx4‘>*âÏzcZoggGçÝì`~«²ÜŒGUØL ©ðl pÑöŸúße,iT?³×/‰nE ÀÙÙÙ¾®®®Ž3ÝÏOqLFkcÅVa‘!ý`07‡ÀEuy€ìªËŽ Ci„PÀf|–7”}~~¾c|EVéLæd3 ‚ÑÊšËMûL¤¾ær´9ïh5&¼±ÀД‡›ô·+&]6N?”â¡©ÆðÅ7梳+Ÿh+±õ”‹ eq¼›ç=ð­ÂFAaTëPÉ–>¿ª¯¯7b± Ž×é#mK~Ûq)Lüö,p1®Ðn‡@r‡AiµÇŒÉ¹7ÐâÅ2=44”‘Ï3›žØ.n*²TZpÙ] ¶2¢ºVí ~?͸h¹¶×wkÛ±‹¦ÌôXE™©9~¦Eê@W9‰•mùçaÏÇázÖÅ^Ij™sXÛ°ÁÝñ{28Øñ§…÷ö œžõИçR.¬¯góƒAù²è6§tCäøÏ/¯oð}ÈE—${"ƒÒÎ1-ú0ÐAá?Î/d«ªoîÈ^—IEND®B`‚ostinato-1.3.0/client/icons/portstats_clear.png000066400000000000000000000005571451413623100216720ustar00rootroot00000000000000‰PNG  IHDRóÿa6IDATxÚcüÿÿ?%€‰"ÝÔ0€›àߟO½¿ûïï_†ÿ¿ÿ1üÿ÷áßï¿`ZÚå #A|}ØË/ÍÀÅÅÁÀ´‚O*hÀŸ¿èšqÀ! Ïð÷Û  íXE~}yÂðïï?u¢ÂàÕ ›/ÿÿþføóñ"Ð+?¾¾:ÇðéɾR9¯ w±Àø)šÍ™ÙDN°süfø÷ë7ÃßÏŸ>úöÿÿß¿@atPüÏGÕ˜;âX]T´ó×çç??>{ùóÓË7?YEý¸9deCØÿÿafÿ÷óÏRdÍ`r.üérËÿ{«ôþß^¢2—¬Ñ6èœgçVdøû뛄Jôí—$¥ƒûkµ®ý+"°™‘Àæ¬;+ÔÍñy £Ä9`à3÷èi÷Õ‹IEND®B`‚ostinato-1.3.0/client/icons/portstats_clear_all.png000066400000000000000000000013401451413623100225110ustar00rootroot00000000000000‰PNG  IHDRóÿagAMA¯È7ŠétEXtSoftwareAdobe ImageReadyqÉe<rIDAT¥ÁMlÓuÀáOÿýwݺ5d[ëmѰd‘pÐj€èNœ8 á1f<c49qÇ‹ÅáÀe'g”ñe[3Èëf۵ݠ]ßYÒ&¼ž'á˜–o¯T>wç#u23Ô@ÌP5ÔQCÔ5DGÍ~i1÷S{²9žÃß/îŠiénæŸa­QÆÔ01Le]#~ìÚKÕSˆ ¢Â§û¾&MÇìî’XýÄë”ïÌÐ÷êÇL¾r˜ çf—˜š ­XkD‰MuÌÅŽ¥³£¸Ô!ù2„€H’¿ óXá+"Y'ïÝ\ºú’ ÃöÁ Í Ä.vº³¨!bÄ&zöñýŸú½~hz÷pš•ÂErÃo" Í'EÊ6jÛ?£šØÂ…k«Ä‰U ر¥ÇÀÝqw¾<{ËË+e_<7æfÞ÷¹ïöyeáo›¹¾ìµ'ÁÛæÿ©ø‘S³Ó"f¬]ÿ„tgË<¥ÑSÜ«gáf‘(ÁàÊÜ m[ó¨1-›íBíËýÇ™Ø?Åå?ŠLîÌÑv~ö_>˜ÈÓ¶p·J2‚˜–ªô’?M¶g„‡å:[º¸õh UCÍym°ƒ…»Užeæ–pw6L3wU‚înªuQ‚(*FPCTø?•ÕÆÅ„»ó"þë”zÀßW5IEND®B`‚ostinato-1.3.0/client/icons/portstats_filter.png000066400000000000000000000006601451413623100220640ustar00rootroot00000000000000‰PNG  IHDRµú7êgAMA¯È7ŠétEXtSoftwareAdobe ImageReadyqÉe<BIDAT(Ï}‘ÍJ‚A†½¨¹ŒˆÖÝÀl Z´‰æ ñ£Ÿ ÊMQ‹m "fIRÙø³paþ–Nú™IŠo¯£„ÅáÀpÎ3ïyçL‘ÿãçY•ÑiklJ?©1ädÖÔ¢Ïè †¤¹—ÛA@møä)Ä]c€âfØn"Ë8DÝ!Ú\ dTݵX£øæ°WVò8WHë²oPÈ`™¹H°ƒwœj<Û>ZjXg`>*ÔøÂ‘u@Š@“%³/beÆP#>u›– –Å (ñ=ìF$U Té~»TXe¾ ‡[ø#“ qcZt]FŒ7‹¸àÝßøb¼¨+y4Xüàä*WÕ#âžœXõ™<1I 9; ÏDåÔg‹=µ£·mÌFõ¦Š‰_¿ùW|*Iu¤uRƒIEND®B`‚ostinato-1.3.0/client/icons/preferences.png000066400000000000000000000011101451413623100207440ustar00rootroot00000000000000‰PNG  IHDRóÿagAMA¯È7ŠétEXtSoftwareAdobe ImageReadyqÉe<ÚIDAT8Ë•“»kSQÇó8H)N]uA,¥ƒˆ“w“ÒjÍûazcbb6ÖêÅ&­ Qcn^b4b2e l„ x¥W„ }$çÅçûûžï9ÇX¦µt:­$“Én"‘0âñ¸ñÿÞT8•J)N†Ã!ãñ˜h4ʉîF£ù|žp8|2X,f˜Õ[­ªª–͵`0ˆßïÇçóSÄòï~¿OµZ% } º¦iôz=Ün÷ñ‘Hä‚XÞët:4 ŠÅ"…Br¹L¥RÁétv‡_ÝT'™-±lÔëušÍ&f_«ÕÐuR©„Ãá˜Øl6å ¼u]e× Ù5~lÞ¢Ýn“Ëåðz½{¦e©jØíö®ÕjU†¸iÂwáÛ|}šÂøÉç§Ëåš?Ìíþ$³¬²³&°€ÀûûÐx ËürŸû|TNÿà%•ì*4ŸÃ‡ l‹Ð;'T^ëÙ—G <»¦²½"pFªà­Œwì Ç!yÖÏhÇÝ”…Ôeø²E±üæ6äî ø ܙצ½ ‘‹P—³îJÕì:”C\DWç^ÏòÑ,<<ß4ƒú –A쬜ž Þѹð]‚ËsŸš6ÛÞ‘øéî×—IEND®B`‚ostinato-1.3.0/client/icons/qt.png000066400000000000000000000044161451413623100171030ustar00rootroot00000000000000‰PNG  IHDR@@ªiqÞtEXtSoftwareAdobe ImageReadyqÉe<*iTXtXML:com.adobe.xmp ðÂFzIDATxÚì[{lSe?}·[·¶ëÊÊÆÜ˜N§ X˜øÀ!$ºeŒƒ3ñƒ&þ…:ã†LýCcÔDcLÕ°D™CÄŒ€ FL#‚° öl»±vk×õµ¶×ﻺ¦÷Ñǽ­·]î=M³öì{Üó»ß9çwÎÝdA€˜E" ‹R&“%ýåcw¨Ñ^ônÝ ƒñI‹ñ¬ˆÉxb3ž€ VãIÄl<Ére¼ ½Vëï…5úû VW eJ#è:ˆðGƒ0¾ ƒ[Ðï½ ö³ hËÖðvË“°«b;,S—g4çß_°Ïþüé»–v¬AY + ‰ruþ„c 9@†\€wO¬XQÝu]°®d5ç¹zõ 8¿M9®Åø¼Y÷E×yåEp†§ò[ Èerx ]ãOÎs•°ÝÒšwà%è›Jú©ð4ô¹NÁuÿÌF< ’© \]Í%k`³i#¨å*ÊøÝUÏÂyÏEpäèŽ 6âëN†þ˜ë$|4ö9«žtÿÑqç®7 Jcë5r5ì²vÀ‡£¶¥So4>Fœ¨é:¼?òiÊà4¢ÿëƒoC„ˆRô[Ë6V®Y:' Åø C·ßÙK¶t2rÀ™ÙsÈZâ:lü:ä"g=ýäw|BpJÅR_TÇXãQÓÃàx):÷Â,\ðþ. MúU”ït1<™o~Â}†–µ%«â`ã»j^J:ÿ7ØR+8»ÎõÚñÇù<“»ŸjüÝ,wº c@•f9Cw30Êiù¨ŸÌéÖ-ÈP®*cè&y¤0 @…ÚÿnV™âŸ¯ÍÀ'ã=äç:] ´š7Sãã0x£>Šn:ì­\ËÐy"sœ7ÆNŒpJ ÅÂ0'ß‹LÀ®ŸòÇu fºòÇüœ7ÆR&à\ ÐÉu9Ù˜€ËÚK€0ÎMV $8_öQ5–©9o¬’+Y³CÁàg¹H½¢˜óÆzTJÓ% >Þˆ•q‹Š:g­!"…[úáJbp/a¹¦"íº À(ÊÏô;…û€\äN]-£úãÂ&Ùâ‡`àR3µD©ÖVB­¶:ã56Ö³ÔW“Äœ _Ðä,ç½—º§­Í-Rè ÝòI+¹–¸`f¡ã‚pÜušµ©ñTE;Ii“ n¢î]ù •Šþ×Ù~˜^p'­è·™³~¾Ø>ÄÇ·•oeÔé[PÜý3 #Ÿžù¯'hRȿռ…ÑIŠ" ÷Ù¥,špzLd‰;—µACQ=¸"3 ”)H =è¿¶‰/…Ëgöpi¥¢[ìàÔs¨ímûa88–‚2#÷ð\‚M¦ }£¾!'q7ÅyûÕnp„&yÖ|a?½SGÓŽÅí6ºË1b Ï%+BŽû{Ïÿý2|3ù=c!Ns}ÑyøaúDFcoF k°nø‡’Ž Äüj’lž Ñ+¹æÒµÐX|J‹UdƒCüó_ö8µVh*)súP0}wäcNûàuk´+Èl‚;Ð8ÎŒí¼itÎÈ„üØÞ#Y`¢ìØ —ç®,j¯ †áÈíc ýž;v“™"OrTТ¼Çñ5¸hù»E§uG^ŒÇUP0­]lv&Ê#Æõ(Ÿ+7þTówa¥Ð°Ÿž9 ÛÌ“?N;ä<ç<¿qz®+ã³"BÙÈc6²½žÉHüŸÆç Lžø¨\/hȳ°/’/RO2AéÿE.b–ìXÌ—CSFIEND®B`‚ostinato-1.3.0/client/icons/refresh.png000066400000000000000000000012551451413623100201130ustar00rootroot00000000000000‰PNG  IHDRóÿagAMA¯È7ŠétEXtSoftwareAdobe ImageReadyqÉe<?IDAT8Ë“ÛKÓaÆû3bvÐ’Bº°BÖ9Ëd-Od­6_×tä¡eÎÅšþÜÜ¡ÜIͱaIF%üÀh„eµ ×aù#èÚ;¼ ïž¾]8ýa»xoÞÃó<ïóá+ ùߺñ¼q°eZ!;“”"Ð4U/(Æ/Dvèû¬•Ý]ÔtoUqÍ|ÛúÍ͸U¢!\ýËvt®¸êP¡ÝØñõëÖ¸#i)8:S.¹Î¬žåjÜ[/Uv– åloL´Äë3 kÔò&µü§Ö~jã´å·ýÒ~M™©LµGJfŒ2BÊ”‹ŒÌ˜„Zf—=çEf™QdÖŸî’wLÛ…ÈŒ#³f›d¶&ÊÆ©°uºÉlÕ“¶;îåû‰ïxLHÖŒ ɇ È È Èè~£…+É:Ò#h›hÎ#5=&¤N.eÄÜïhiL˜(B:’* ÕÎÞÆ‘îC‰"j­Æ´ÜÂÇé|ÀÃí®‚,7"îD²;ŒÃ?Ð ÆÖûOŠEû¸ººD2™¤õ»tiAX£ý+.ÓìaÒßs?8ØÎ¶ÿà@m”ÀT9IEND®B`‚ostinato-1.3.0/client/icons/sound_none.png000066400000000000000000000006631451413623100206260ustar00rootroot00000000000000‰PNG  IHDRóÿasRGB®ÎégAMA± üa pHYsÃÃÇo¨dHIDAT8Oí¿NÂPÆ¿[ÛÒ?,‚”R7uÀÄ0:ÊtÔ—ÐÕ˜Èæâ‰î¼®†p„(ƒ•¨a0”¶´ž^¯! &>¿ääœ{ïw¾{îÅ0‘Ñjݾ§iRŽãË`‰0\"¢:ŽWH’,¤iʵŒá£Ý~,gµÄwE‘/eY^ÔëÇ88*• 4MC>Ÿ‡mÛÙ» vÞívW\,Ø0ø%K’ÄCUU]ç5ý‘P¬ùÃ@B.§¢ZµÑhœÀqÑüOƒ( Ñï?c4zÃx<¦üÊMuEŠ-k€o6\j=‰ CŽIEND®B`‚ostinato-1.3.0/client/icons/stream_add.png000066400000000000000000000014561451413623100205630ustar00rootroot00000000000000‰PNG  IHDRóÿagAMA¯È7ŠétEXtSoftwareAdobe ImageReadyqÉe<ÀIDAT8Ëu“ËK[Aƃ€û,º¯‹.ºjéR²pQh5‹J¡`šôAR»P4($bHjJM Ñ Š¯`L(‘ÞFã3>¯Æ4ÅÇÂG/ -–®îýzÎPÓìÀ0 w¾ßùÎ7sut7͵µ5óÒÒRjaa!EÃü¿sE›­­­²R—øüôô''';O$%ñx¼t||¼ìF‰{H¬®®®*étZ?77§âàà$Tb±˜ž@ÊÈȈ:00ÐSÈd2öýý}\\\`oo$¾šžžÖòùÂSg¹öøuùw£Ñ(B$‹. RÅ+=$ŒŽŽ‚îˆލ ŸrAd¾Ið±â‰ÿîÙô>!ŽF£-ÃÃÃÚöö¶óuÑÙ0¨ÆSØ×÷ˆå?€‡/ùþä+ü€ÁÁA;'Ï/@ t¯\.—Æ¡qßíw‘È…ñïˆgƒ øû¦GO]]j±X”††=Av¼¹¥y¥çpK5Bìþ\Sìàz Ìjµ–677—ð|jjŠÿTXîÿ4únãd•yå}!ƒ›fmm­¹ºº:UYY™ªªª2Óáš?ØöŸµƒÏýeyÛ1çB0_IEND®B`‚ostinato-1.3.0/client/icons/stream_delete.png000066400000000000000000000015171451413623100212730ustar00rootroot00000000000000‰PNG  IHDRóÿagAMA¯È7ŠétEXtSoftwareAdobe ImageReadyqÉe<áIDAT8ËuSKHQ½of’˜L$Q“T0f¡%´ÅÒº1³h©t(n„Bµtg© JC (.ÄîÚEèÊ,µ»n´`‰%ŠUÁ`«bCIBlbb¢Ñü43é½ 6pgÂ{÷œsÏyoXµZ…ë~ëëë•J¥_–e¸¸¸ð»\®×õ±Ë¡PÈ€8Ö‚­­­M´¿ººš2 \3"Y‹ÛíÞù‡á.}ÜÆ¦=l2cÉår¨´Z­Lkççç{XÛ~¿ßw…`kkkLÅA›ÍÆI’DJûXæR©ÅbE!Â}Ü· ®±±qpzzzŒ°=•K$ä…‘¢Ùl9Ž#慠Ãá u‘ö"‘*’sW2ƒ&“i”T)¸““ˆF£ ÑhÀn·ƒ *a¡P »“###ê ÚÀq›±D‹Å"åóyÈf³D¸‚ÿ=:Ж·«««›ÄÐ*,--e‘,˜C÷ÚÛÛ¹££#U•“É$ ÏÔÔÔRñù|´¶ˆ@}N§SB›ÒÌÌÌMŽÆ"oH¤ŽN*F£ÐNíx©‡ö1Lµ3š”ì°ÙÙÙt:ÝŒcŠ¥ŽÇF>Wè!0–÷‹w+kŸ ’ˆÁ™¶NÙÓ¶®i:µççç'¬Vë()Q`t ±XLUlIn‚)„—töÛP -ÀÏåE9»»óZ%˜››{ƒ*ã8;>>V' ¢¡±óïúÀùìèÃ_âßL7 -Ø`saù·z0bz½ž.„Ãá÷6'zGº5Ý|icÅ %dÈ>,Èþ¡oB~Ö!G}”ñ~Ú„Ù?d­5_šÕIJ1t9µÆæÔ5÷vïýÝÛ9%/œ1~¿ó<çyžs/§ª*\ôÄb±AY–c IR°··wú¢>î,ÁÚÚš {XÇÎØl¶º_ZZÊ566ZñÌ€dýýý'þ ØÀulJ`“‹Õj5 ÒëõŒÎDQL`­ƒAÿ9‚x<îaÈn·óF£‘&%±,Õj*• (ŠB„I¼·¢Þl6MNNú«£då3™ yáh¢Åbxž'ïÚ—ËEçÝmooC8V‘œ?—A4ojj¥©\¡P€t: uuuàp8@§Ói„år™ìNŒŒŒh 8 hå¶b V«ÕX*•àððñÿËúúz@[cn·û. C«°°°pˆd%Äd¹H$œN'ŸÏç¡X,j766 •Jõù|¾yšâ÷ûühœy[’ ð¨ã5 ² SSS O²È²iÒi 5 ÓõROU¡Óì‚é-/e¤”ìp@`u¿e ÝÝÝFJ×ÄSL_d¢"ƒ$3¾Ýäänuz`å×;ˆï,Õ•Þ]$Ξ†8777ÞÖÖ6JJ(°™­gÐ×õ˜ªS( B¦° F¡·æa5ýAFUzm¡Pè²=onn†££#MI&ðv> ª $&ÁßZn_½ÇRY·œŠT5Ì@Á p ôRÁææf©j­ 2“ÁrÙ†`¦‘ý)¤Ád°Â—O°œøœ+ØO-Œùž`8Y“ÉÔ“t„~×0,Q¡‚j®›oÂk!–ŽBxýãA‰¯´ÿžPkç>¦ááaZÙC+ǸÿŒÇãi¡´gggsßnÌpnÛ}søçûïøªõ Xüïk<ûx½ÞÁ\.7@+æ8.øµëÍ+¦\2WV—P哾ÏCæÂ÷—1;IEND®B`‚ostinato-1.3.0/client/icons/stream_edit.png000066400000000000000000000015411451413623100207530ustar00rootroot00000000000000‰PNG  IHDRóÿagAMA¯È7ŠétEXtSoftwareAdobe ImageReadyqÉe<óIDAT8Ëu“ÝO’aÆ™€[‡vZ9ÿN:0Íb6·üXLMW,õ 5!i~®šffµ4Óðc 4`Ò"ñ­( ~„¸7] ™Jf/WÏËÒÆ¦ïöìݳ=×ï¾îë~ÖQkjjŠk6›' Ãù¸ÇKØ8ÎÔ™™™d"N"âõÕÕU„B!H¥Òu•J•¤T*“e2Yê‘"î&bÚf³Q&“)E¯×S‹‹‹XXXR …"…€¨ááaZ,w'‡0 Ãï÷ƒˆ·µZmÌëõÂívC.—Ljx›8EQP«Õ‰DÂC©ZGz1F0??˜½Ëå°X,1W—ЂÑhleOOOÃjµb||===èïïg´Ûǧð ràW´Âñ: ή´!ÚIõNñù|°ÛíÐh4hjj2ñùüÌúúúÌöövÓÌÄ#PZ>6]£ÀÏUl¸¤Ð‹.ÿf1½¶¶·ËT&Abpp---™î¬Òì¦ïî{Ø[Q‚Ò‰°;÷– ÄEXdÆt0Œ÷ÈØdÄÑÙÙ„Ô§/EÜwcô®» ·¶UⳘ ×›2ð*Ji‰X\éëë‹ÌÎÎÆ!L½½½¦É¡+µ‘9è¨;RDƒEØòÖÂÓ•AßææËË˳†HÆÔJ‚Äää$˜¿^þÔÿŸ¸ѯ…øábþùE´ÕpzîD"©Šy<ž¸Ø0ÚïX è]=¢K\ü ^Ã&qò…ˆíŸÆPUU+//ÿ?Æ!“[¶A«¶\9qœºpˆ<ÑRˆÒx âU’=LÐ(SД—ð•©ØÖÕóÜdŸhÝÛµ³ûìœßuνÝêDÑ‘ý~sxk:TÃT  Ýû*&¼e¨Ý»sî`x~Š×E Ù610³ŽÇ–/è£óñ6ÀÞ«ÖOà_tϸzÖ‚ßIÒ’Õô¢ÁÔÒ84©1¨*Œ0r`0ˆ\.‡µµ5x<d2( Èd²Ë”Ü^ÀAýó‰ Þ~¼(W8…t: «Õ ©T ¥RIÖˆÇãP«Õ2Áàìöp§šÎ¾¥*‰­ÂR©Ä9µ§Òju(‹p¹\Ëå ú_®€—môý!‹Å·ÛB¡½^g²B¤ÕéÍfYB¡TßóOØctÓ9¸ÒŒFÉd ÃÀï÷ˆŽmv:ˆD"‰Dzp…ºoZEíè •ƒSìÔNP¬Ôñu÷Ÿ·a4 €›†[‡k4™LˆF£b±ø%1ä¢bíJýöÄ2n>M¡g$ëJWå1HdÓèííEÔ³Ù •J…ki‚Ÿ<ïyÄn¿­D"ŸÏÇßù_(Y8ÅF*SIEND®B`‚ostinato-1.3.0/client/icons/transmit_on.png000066400000000000000000000013551451413623100210130ustar00rootroot00000000000000‰PNG  IHDRóÿagAMA¯È7ŠétEXtSoftwareAdobe ImageReadyqÉe<IDAT8Ë…’]H“a†’0‚N=Ã0$£È“" ‚R”’¢ƒ@Í2ü!Qt©¥1Q–CS·el*2—?Œk,…%ŠRv…ØAu ëË«{™…?ÕÁÃ/ïu?×ó¼Ÿ 0mªiOŠÊA¸gé.ƒÓ`²m‰[FêS¶Þß W Œ27/ÆUAXƒ§>˜ê‚@Uyåö€iO image/svg+xml ostinato-1.3.0/client/jumpurl.h000066400000000000000000000020641451413623100165020ustar00rootroot00000000000000/* Copyright (C) 2017 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _JUMP_URL_H #define _JUMP_URL_H #include inline QString jumpUrl( QString keyword, QString source="app", QString medium="hint", QString name="help") { return QString("https://jump.ostinato.org/" + keyword + "?" + "utm_source=" + source + "&" + "utm_medium=" + medium + "&" + "utm_campaign=" + name); } #endif ostinato-1.3.0/client/log.h000066400000000000000000000042471451413623100155720ustar00rootroot00000000000000/* Copyright (C) 2018 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _LOG_H #define _LOG_H #include "logsmodel.h" extern LogsModel *appLogs; inline static void logError(QString port, QString message) { appLogs->log(LogsModel::kError, port, message); } inline static void logError(quint32 portGroupId, quint32 portId,QString message) { logError(QString("%1-%2").arg(portGroupId).arg(portId), message); } inline static void logError(quint32 portGroupId, QString message) { logError(QString("%1-*").arg(portGroupId), message); } inline static void logError(QString message) { logError(QString("--"), message); } inline static void logWarn(QString port, QString message) { appLogs->log(LogsModel::kWarning, port, message); } inline static void logWarn(quint32 portGroupId, quint32 portId,QString message) { logWarn(QString("%1-%2").arg(portGroupId).arg(portId), message); } inline static void logWarn(quint32 portGroupId, QString message) { logWarn(QString("%1-*").arg(portGroupId), message); } inline static void logWarn(QString message) { logWarn(QString("--"), message); } inline static void logInfo(QString port, QString message) { appLogs->log(LogsModel::kInfo, port, message); } inline static void logInfo(quint32 portGroupId, quint32 portId,QString message) { logInfo(QString("%1-%2").arg(portGroupId).arg(portId), message); } inline static void logInfo(quint32 portGroupId, QString message) { logInfo(QString("%1-*").arg(portGroupId), message); } inline static void logInfo(QString message) { logInfo(QString("--"), message); } #endif ostinato-1.3.0/client/logsmodel.cpp000066400000000000000000000074771451413623100173410ustar00rootroot00000000000000/* Copyright (C) 2018 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #include "logsmodel.h" #include // XXX: Keep the enum in sync with it's string enum { kTimeStamp, kLogLevel, kPort, kMessage, kFieldCount }; static QStringList columnTitles = QStringList() << "Timestamp" << "Level" << "Port" << "Message"; static QStringList levelTitles = QStringList() << "Info" << "Warning" << "Error"; LogsModel::LogsModel(QObject *parent) : QAbstractTableModel(parent) { } int LogsModel::rowCount(const QModelIndex &/*parent*/) const { return logs_.size(); } int LogsModel::columnCount(const QModelIndex &/*parent*/) const { return kFieldCount; } QVariant LogsModel::headerData( int section, Qt::Orientation orientation, int role) const { if (role != Qt::DisplayRole) return QVariant(); switch (orientation) { case Qt::Horizontal: // Column Header return columnTitles.at(section); case Qt::Vertical: // Row Header return QVariant(); default: break; } return QVariant(); } QVariant LogsModel::data(const QModelIndex &index, int role) const { if (!index.isValid() || (index.row() >= logs_.size())) return QVariant(); if (role == Qt::ForegroundRole) { switch(logs_.at(index.row()).logLevel) { case kError: return QBrush(QColor("darkred")); case kWarning: return QBrush(QColor("orangered")); case kInfo: default: return QVariant(); } } if (role != Qt::DisplayRole) return QVariant(); switch (index.column()) { case kTimeStamp: return logs_.at(index.row()).timeStamp.toString("hh:mm:ss.zzz"); case kLogLevel: return levelTitles.at(logs_.at(index.row()).logLevel); case kPort: return logs_.at(index.row()).port; case kMessage: return logs_.at(index.row()).message; default: break; } return QVariant(); } Qt::DropActions LogsModel::supportedDropActions() const { return Qt::IgnoreAction; // read-only model, doesn't accept any data } // --------------------------------------------- // // Slots // --------------------------------------------- // void LogsModel::clear() { beginResetModel(); logs_.clear(); endResetModel(); } void LogsModel::setLogLevel(int level) { currentLevel_ = static_cast(level % kLevelCount); log(currentLevel_, QString("--"), QString("Log level changed to %1 or higher") .arg(levelTitles.at(currentLevel_))); } void LogsModel::log(int logLevel,QString port, QString message) { if (logLevel < currentLevel_) return; // TODO: discard logs older than some threshold //qDebug("adding log %u %s", logs_.size(), qPrintable(message)); beginInsertRows(QModelIndex(), logs_.size(), logs_.size()); Log l; logs_.append(l); logs_.last().timeStamp = QTime::currentTime(); logs_.last().logLevel = logLevel; logs_.last().port = port; // XXX: QTableView does not honour newline unless we increase the // row height, so we replace newlines with semicolon for now logs_.last().message = message.trimmed().replace("\n", "; "); endInsertRows(); } ostinato-1.3.0/client/logsmodel.h000066400000000000000000000032621451413623100167720ustar00rootroot00000000000000/* Copyright (C) 2018 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _LOGS_MODEL_H #define _LOGS_MODEL_H #include #include class LogsModel: public QAbstractTableModel { Q_OBJECT public: enum LogLevel { // FIXME: use enum class? kInfo, kWarning, kError, kLevelCount }; public: LogsModel(QObject *parent = 0); int rowCount(const QModelIndex &parent=QModelIndex()) const; int columnCount(const QModelIndex &parent=QModelIndex()) const; QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const; QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const; Qt::DropActions supportedDropActions() const; public slots: void clear(); void setLogLevel(int level); void log(int logLevel,QString port, QString message); private: struct Log { QTime timeStamp; int logLevel; QString port; QString message; }; QVector logs_; LogLevel currentLevel_{kInfo}; }; #endif ostinato-1.3.0/client/logswindow.cpp000066400000000000000000000163721451413623100175420ustar00rootroot00000000000000/* Copyright (C) 2018 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #include "logswindow.h" #include "logsmodel.h" #include "modeltest.h" #include #include #include #include #include extern QMainWindow *mainWindow; LogsWindow::LogsWindow(LogsModel *model, QWidget *parent) : QWidget(parent) { setupUi(this); logs->setModel(model); autoScroll->setChecked(true); logs->verticalHeader()->setHighlightSections(false); logs->verticalHeader()->setDefaultSectionSize( logs->verticalHeader()->minimumSectionSize()); logs->setShowGrid(false); logs->setAlternatingRowColors(true); logs->horizontalHeader()->setSectionResizeMode(3, QHeaderView::Stretch); parentDock_ = qobject_cast(parent); windowTitle_ = parentDock_->windowTitle(); warnAnime_ = new QMovie(":/icons/anime_warn.gif", QByteArray(), this); errorAnime_ = new QMovie(":/icons/anime_error.gif", QByteArray(), this); alert_ = new QLabel("ALERT!", this, Qt::FramelessWindowHint|Qt::WindowStaysOnTopHint); alert_->setScaledContents(true); alert_->hide(); alertAnime_ = new QPropertyAnimation(alert_, "geometry", this); alertAnime_->setDuration(2000); alertAnime_->setEasingCurve(QEasingCurve::InOutExpo); connect(level, SIGNAL(currentIndexChanged(int)), model, SLOT(setLogLevel(int))); connect(clear, SIGNAL(clicked()), model, SLOT(clear())); connect(parentDock_, SIGNAL(visibilityChanged(bool)), SLOT(when_visibilityChanged(bool))); connect(model, SIGNAL(rowsInserted(const QModelIndex&, int, int)), SLOT(when_rowsInserted(const QModelIndex&, int, int))); connect(logs->horizontalHeader(), SIGNAL(sectionResized(int, int, int)), logs, SLOT(resizeRowsToContents())); #if defined(QT_NO_DEBUG) || QT_VERSION < 0x050700 logsModelTest_ = nullptr; #else logsModelTest_ = new ModelTest(model); #endif } LogsWindow::~LogsWindow() { delete warnAnime_; delete errorAnime_; delete logsModelTest_; } void LogsWindow::clearCurrentSelection() { logs->selectionModel()->clearCurrentIndex(); logs->clearSelection(); } void LogsWindow::when_visibilityChanged(bool visible) { if (visible) { logs->resizeRowsToContents(); setState(kInfo); } isVisible_ = visible; qDebug("isVisible = %u", isVisible_); } void LogsWindow::when_rowsInserted(const QModelIndex &parent, int first, int last) { if (isVisible_) logs->resizeRowsToContents(); State incrementalState = kInfo; for (int i = first; i <= last; i++) { // FIXME: use a user-role instead, so we don't need to know column and // have to compare strings? QString level = logs->model()->data(logs->model()->index(i, 1, parent)) .toString(); if (level == "Error") { incrementalState = kError; break; // Highest level - no need to look further } else if (level == "Warning") { incrementalState = kWarning; } } alert(incrementalState); if (incrementalState > state()) setState(incrementalState); } void LogsWindow::on_autoScroll_toggled(bool checked) { if (checked) { connect(logs->model(), SIGNAL(rowsInserted(const QModelIndex&, int, int)), logs, SLOT(scrollToBottom())); } else { disconnect(logs->model(), SIGNAL(rowsInserted(const QModelIndex&, int, int)), logs, SLOT(scrollToBottom())); } } LogsWindow::State LogsWindow::state() { return state_; } void LogsWindow::setState(State state) { if (isVisible_) return; state_ = state; notify(); } QLabel* LogsWindow::tabIcon() { QList tabBars = mainWindow->findChildren(); foreach(QTabBar* tabBar, tabBars) { for (int i = 0; i < tabBar->count(); i++) { if (tabBar->tabText(i).startsWith(windowTitle_)) { QLabel *icon = qobject_cast( tabBar->tabButton(i, QTabBar::LeftSide)); if (!icon) { // Lazy create icon = new QLabel(); tabBar->setTabButton(i, QTabBar::LeftSide, icon); } return icon; } } } return nullptr; } //! Popup and animate a big icon void LogsWindow::alert(State state) { if (state == kInfo) return; // start - center of main window QRect start; QWidget *view = mainWindow; if (!view) return; alert_->setParent(view); alert_->raise(); start.setSize(QSize(256, 256).scaled(view->size()/2, Qt::KeepAspectRatio)); start.moveCenter(QPoint(view->size().width()/2, view->size().height()/2)); // end - center of logs window if visible, tab icon otherwise QPoint c; QLabel *icon = tabIcon(); view = isVisible_ ? dynamic_cast(this) : mainWindow; if (icon && !isVisible_) { c = icon->geometry().center(); // in icon's parent (tabBar) coords c = icon->mapFromParent(c); // in icon's own coords c = icon->mapTo(view, c); // in mainWindow's coords } else { c = view->geometry().center(); c = view->mapTo(mainWindow, c); // in mainWindow's coords } QRect end; end.moveCenter(c); switch (state) { case kError: alert_->setPixmap(QPixmap(":/icons/error.svg")); break; case kWarning: alert_->setPixmap(QPixmap(":/icons/warn.svg")); break; default: Q_UNREACHABLE(); break; } alertAnime_->setStartValue(start); alertAnime_->setEndValue(end); alert_->show(); // ensure it's visible before starting animation alertAnime_->start(); } //! Show tab icon void LogsWindow::notify() { QString annotation; QMovie *anime = nullptr; // Stop all animations before we start a new one warnAnime_->stop(); errorAnime_->stop(); switch (state()) { case kError: anime = errorAnime_; annotation = " - Error(s)"; break; case kWarning: anime = warnAnime_; annotation = " - Warning(s)"; break; case kInfo: default: break; } QLabel *icon = tabIcon(); // NOTE: we may not have a icon if not tabified if (icon) { if (anime) { icon->setMovie(anime); anime->start(); } else icon->clear(); icon->adjustSize(); } parentDock_->setWindowTitle(windowTitle_ + annotation); } ostinato-1.3.0/client/logswindow.h000066400000000000000000000035561451413623100172070ustar00rootroot00000000000000/* Copyright (C) 2018 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _LOGS_WINDOW_H #define _LOGS_WINDOW_H #include "ui_logswindow.h" class LogsModel; class QDockWidget; class QShowEvent; class QMovie; class QPropertyAnimation; class LogsWindow: public QWidget, private Ui::LogsWindow { Q_OBJECT public: LogsWindow(LogsModel *model, QWidget *parent = 0); ~LogsWindow(); public slots: void clearCurrentSelection(); private slots: void when_visibilityChanged(bool visible); void when_rowsInserted(const QModelIndex &parent, int first, int last); void on_autoScroll_toggled(bool checked); private: enum State {kInfo, kWarning, kError}; QLabel* tabIcon(); State state(); void setState(State state); void alert(State state); void notify(); State state_{kInfo}; QDockWidget *parentDock_; QMovie *warnAnime_{nullptr}; QMovie *errorAnime_{nullptr}; QLabel *alert_{nullptr}; QPropertyAnimation *alertAnime_{nullptr}; QString windowTitle_; bool isVisible_{false}; // see XXX below QObject *logsModelTest_; // XXX: We cannot use isVisible() instead of isVisible_ since // LogsWindow::isVisible() returns true even when the parent LogsDock // is tabified but not the selected tab }; #endif ostinato-1.3.0/client/logswindow.ui000066400000000000000000000072171451413623100173730ustar00rootroot00000000000000 LogsWindow 0 0 693 300 Form QFrame::Panel QFrame::Raised Level level Log Level Select the desired logging level Info Warning Error Qt::Vertical Auto Scroll Qt::Horizontal 429 20 Clear Logs Clear Logs Clear :/icons/portstats_clear_all.png:/icons/portstats_clear_all.png Ostinato application logs are displayed here QAbstractItemView::SelectRows XTableView QTableView

xtableview.h
level autoScroll clear logs ostinato-1.3.0/client/main.cpp000066400000000000000000000065531451413623100162720ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #include "mainwindow.h" #include "../common/ostprotolib.h" #include "../common/protocolmanager.h" #include "../common/protocolwidgetfactory.h" #include "params.h" #include "preferences.h" #include "settings.h" #include "thememanager.h" #include #include #include #include #include #include extern const char* version; extern const char* revision; extern ProtocolManager *OstProtocolManager; extern ProtocolWidgetFactory *OstProtocolWidgetFactory; Params appParams; QSettings *appSettings; QMainWindow *mainWindow; void NoMsgHandler(QtMsgType type, const QMessageLogContext &context, const QString &msg); int main(int argc, char* argv[]) { QApplication app(argc, argv); int exitCode; app.setApplicationName("Ostinato"); app.setOrganizationName("Ostinato"); app.setProperty("version", version); app.setProperty("revision", revision); appParams.parseCommandLine(argc, argv); #ifndef QT_DEBUG // Release mode if (appParams.optLogsDisabled()) qInstallMessageHandler(NoMsgHandler); #endif OstProtocolManager = new ProtocolManager(); OstProtocolWidgetFactory = new ProtocolWidgetFactory(); Preferences::initDefaults(); /* (Portable Mode) If we have a .ini file in the same directory as the executable, we use that instead of the platform specific location and format for the settings */ QString portableIni = QCoreApplication::applicationDirPath() + "/ostinato.ini"; if (QFile::exists(portableIni)) appSettings = new QSettings(portableIni, QSettings::IniFormat); else appSettings = new QSettings(); qDebug("Settings: %s", qPrintable(appSettings->fileName())); OstProtoLib::setExternalApplicationPaths( appSettings->value(kTsharkPathKey, kTsharkPathDefaultValue).toString(), appSettings->value(kGzipPathKey, kGzipPathDefaultValue).toString(), appSettings->value(kDiffPathKey, kDiffPathDefaultValue).toString(), appSettings->value(kAwkPathKey, kAwkPathDefaultValue).toString()); ThemeManager::instance()->setTheme(appSettings->value(kThemeKey).toString()); qsrand(QDateTime::currentDateTime().toTime_t()); mainWindow = new MainWindow; mainWindow->show(); exitCode = app.exec(); delete mainWindow; delete appSettings; delete OstProtocolManager; google::protobuf::ShutdownProtobufLibrary(); return exitCode; } void NoMsgHandler(QtMsgType type, const QMessageLogContext &/*context*/, const QString &msg) { if (type == QtFatalMsg) { fprintf(stderr, "%s\n", qPrintable(msg)); fflush(stderr); abort(); } } ostinato-1.3.0/client/mainwindow.cpp000066400000000000000000000507531451413623100175230ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #include "mainwindow.h" #if 0 #include "dbgthread.h" #endif #include "clipboardhelper.h" #include "jumpurl.h" #include "logsmodel.h" #include "logswindow.h" #include "params.h" #include "portgrouplist.h" #include "portstatswindow.h" #include "portswindow.h" #include "preferences.h" #include "sessionfileformat.h" #include "settings.h" #include "ui_about.h" #include "updater.h" #include "fileformat.pb.h" #include #include #include #include #include #include #include #include #include #ifdef Q_OS_WIN32 #define WIN32_NO_STATUS #include #undef WIN32_NO_STATUS #include #endif extern const char* version; extern const char* revision; PortGroupList *pgl; LogsModel *appLogs; ClipboardHelper *clipboardHelper; MainWindow::MainWindow(QWidget *parent) : QMainWindow (parent) { Updater *updater = new Updater(); if (appParams.optLocalDrone()) { QString serverApp = QCoreApplication::applicationDirPath(); #ifdef Q_OS_MAC // applicationDirPath() does not return bundle, // but executable inside bundle serverApp.replace("Ostinato.app", "drone.app"); #endif #ifdef Q_OS_WIN32 serverApp.append("/drone.exe"); #else serverApp.append("/drone"); #endif qDebug("staring local server - %s", qPrintable(serverApp)); localServer_ = new QProcess(this); connect(localServer_, SIGNAL(finished(int, QProcess::ExitStatus)), SLOT(onLocalServerFinished(int, QProcess::ExitStatus))); #if QT_VERSION >= 0x050600 connect(localServer_, SIGNAL(errorOccurred(QProcess::ProcessError)), SLOT(onLocalServerError(QProcess::ProcessError))); #else connect(localServer_, SIGNAL(error(QProcess::ProcessError)), SLOT(onLocalServerError(QProcess::ProcessError))); #endif localServer_->setProcessChannelMode(QProcess::ForwardedChannels); localServer_->start(serverApp, QStringList()); QTimer::singleShot(5000, this, SLOT(stopLocalServerMonitor())); } else localServer_ = NULL; pgl = new PortGroupList; appLogs = new LogsModel(this); clipboardHelper = new ClipboardHelper(this); portsWindow = new PortsWindow(pgl, this); statsWindow = new PortStatsWindow(pgl, this); portsDock = new QDockWidget(tr("Ports and Streams"), this); portsDock->setObjectName("portsDock"); portsDock->setFeatures( portsDock->features() & ~QDockWidget::DockWidgetClosable); statsDock = new QDockWidget(tr("Port Statistics"), this); statsDock->setObjectName("statsDock"); statsDock->setFeatures( statsDock->features() & ~QDockWidget::DockWidgetClosable); logsDock_ = new QDockWidget(tr("Logs"), this); logsDock_->setObjectName("logsDock"); logsDock_->setFeatures( logsDock_->features() & ~QDockWidget::DockWidgetClosable); logsWindow_ = new LogsWindow(appLogs, logsDock_); setupUi(this); menuFile->insertActions(menuFile->actions().at(3), portsWindow->portActions()); menuEdit->addActions(clipboardHelper->actions()); menuStreams->addActions(portsWindow->streamActions()); menuDevices->addActions(portsWindow->deviceActions()); statsDock->setWidget(statsWindow); addDockWidget(Qt::BottomDockWidgetArea, statsDock); logsDock_->setWidget(logsWindow_); addDockWidget(Qt::BottomDockWidgetArea, logsDock_); tabifyDockWidget(statsDock, logsDock_); statsDock->show(); statsDock->raise(); portsDock->setWidget(portsWindow); addDockWidget(Qt::TopDockWidgetArea, portsDock); #if QT_VERSION >= 0x050600 // Set top and bottom docks to equal height resizeDocks({portsDock, statsDock}, {height()/2, height()/2}, Qt::Vertical); #endif portsWindow->setFocus(); // Save the default window geometry and layout ... defaultGeometry_ = geometry(); defaultLayout_ = saveState(0); // ... before restoring the last used settings QRect geom = appSettings->value(kApplicationWindowGeometryKey).toRect(); if (!geom.isNull()) setGeometry(geom); QByteArray layout = appSettings->value(kApplicationWindowLayout) .toByteArray(); if (layout.size()) restoreState(layout, 0); connect(actionFileExit, SIGNAL(triggered()), this, SLOT(close())); connect(actionAboutQt, SIGNAL(triggered()), qApp, SLOT(aboutQt())); connect(actionViewShowMyReservedPortsOnly, SIGNAL(toggled(bool)), portsWindow, SLOT(showMyReservedPortsOnly(bool))); connect(actionViewShowMyReservedPortsOnly, SIGNAL(toggled(bool)), statsWindow, SLOT(showMyReservedPortsOnly(bool))); connect(updater, SIGNAL(newVersionAvailable(QString)), this, SLOT(onNewVersion(QString))); updater->checkForNewVersion(); // TODO: If session file specified (and valid?), don't add local drone PG // Add the "Local" Port Group if (appParams.optLocalDrone()) { PortGroup *pg = new PortGroup; pgl->addPortGroup(*pg); } if (appParams.argumentCount()) { QString fileName = appParams.argument(0); if (QFile::exists(fileName)) openSession(fileName); else QMessageBox::information(NULL, qApp->applicationName(), QString("File not found: " + fileName)); } #if 0 { DbgThread *dbg = new DbgThread(pgl); dbg->start(); } #endif } MainWindow::~MainWindow() { stopLocalServerMonitor(); if (localServer_) { #ifdef Q_OS_WIN32 //! \todo - find a way to terminate cleanly localServer_->kill(); #else localServer_->terminate(); #endif } delete pgl; // We don't want to save state for Stream Stats Docks - so delete them QList streamStatsDocks = findChildren("streamStatsDock"); foreach(QDockWidget *dock, streamStatsDocks) delete dock; Q_ASSERT(findChildren("streamStatsDock").size() == 0); QByteArray layout = saveState(0); appSettings->setValue(kApplicationWindowLayout, layout); appSettings->setValue(kApplicationWindowGeometryKey, geometry()); if (localServer_) { localServer_->waitForFinished(); delete localServer_; } } void MainWindow::openSession(QString fileName) { qDebug("Open Session Action (%s)", qPrintable(fileName)); static QString dirName; QStringList fileTypes = SessionFileFormat::supportedFileTypes( SessionFileFormat::kOpenFile); QString fileType; QString errorStr; bool ret; if (!fileName.isEmpty()) goto _skip_prompt; if (portsWindow->portGroupCount()) { if (QMessageBox::question(this, tr("Open Session"), tr("Existing session will be lost. Proceed?"), QMessageBox::Yes | QMessageBox::No, QMessageBox::No) == QMessageBox::No) goto _exit; } if (fileTypes.size()) fileType = fileTypes.at(0); fileName = QFileDialog::getOpenFileName(this, tr("Open Session"), dirName, fileTypes.join(";;"), &fileType); if (fileName.isEmpty()) goto _exit; _skip_prompt: ret = openSession(fileName, errorStr); if (!ret || !errorStr.isEmpty()) { QMessageBox msgBox(this); QStringList str = errorStr.split("\n\n\n\n"); msgBox.setIcon(ret ? QMessageBox::Warning : QMessageBox::Critical); msgBox.setWindowTitle(qApp->applicationName()); msgBox.setText(str.at(0)); if (str.size() > 1) msgBox.setDetailedText(str.at(1)); msgBox.setStandardButtons(QMessageBox::Ok); msgBox.exec(); } dirName = QFileInfo(fileName).absolutePath(); _exit: return; } void MainWindow::on_actionOpenSession_triggered() { openSession(); } void MainWindow::on_actionSaveSession_triggered() { qDebug("Save Session Action"); static QString fileName; QStringList fileTypes = SessionFileFormat::supportedFileTypes( SessionFileFormat::kSaveFile); QString fileType; QString errorStr; QFileDialog::Options options; if (portsWindow->reservedPortCount()) { QString myself = appSettings->value(kUserKey, kUserDefaultValue) .toString(); if (QMessageBox::question(this, tr("Save Session"), QString("Some ports are reserved!\n\nOnly ports reserved by %1 will be saved. Proceed?").arg(myself), QMessageBox::Yes | QMessageBox::No, QMessageBox::No) == QMessageBox::No) goto _exit; } // On Mac OS with Native Dialog, getSaveFileName() ignores fileType. // Although currently there's only one supported file type, we may // have more in the future #if defined(Q_OS_MAC) options |= QFileDialog::DontUseNativeDialog; #endif if (fileTypes.size()) fileType = fileTypes.at(0); _retry: fileName = QFileDialog::getSaveFileName(this, tr("Save Session"), fileName, fileTypes.join(";;"), &fileType, options); if (fileName.isEmpty()) goto _exit; if (QFileInfo(fileName).suffix().isEmpty()) { QString fileExt = fileType.section(QRegExp("[\\*\\)]"), 1, 1); qDebug("Adding extension '%s' to '%s'", qPrintable(fileExt), qPrintable(fileName)); fileName.append(fileExt); if (QFileInfo(fileName).exists()) { if (QMessageBox::warning(this, tr("Overwrite File?"), QString("The file \"%1\" already exists.\n\n" "Do you wish to overwrite it?") .arg(QFileInfo(fileName).fileName()), QMessageBox::Yes|QMessageBox::No, QMessageBox::No) != QMessageBox::Yes) goto _retry; } } if (!saveSession(fileName, fileType, errorStr)) QMessageBox::critical(this, qApp->applicationName(), errorStr); else if (!errorStr.isEmpty()) QMessageBox::warning(this, qApp->applicationName(), errorStr); fileName = QFileInfo(fileName).absolutePath(); _exit: return; } void MainWindow::on_actionPreferences_triggered() { Preferences *preferences = new Preferences(); preferences->exec(); delete preferences; } void MainWindow::on_actionViewRestoreDefaults_triggered() { // Use the saved default geometry/layout, however keep the // window location same defaultGeometry_.moveTo(geometry().topLeft()); setGeometry(defaultGeometry_); restoreState(defaultLayout_, 0); // Add streamStats as tabs QList streamStatsDocks = findChildren("streamStatsDock"); foreach(QDockWidget *dock, streamStatsDocks) { dock->setFloating(false); tabifyDockWidget(statsDock, dock); } statsDock->show(); statsDock->raise(); actionViewShowMyReservedPortsOnly->setChecked(false); portsWindow->clearCurrentSelection(); statsWindow->clearCurrentSelection(); logsWindow_->clearCurrentSelection(); } void MainWindow::on_actionHelpOnline_triggered() { QDesktopServices::openUrl(QUrl(jumpUrl("help", "app", "menu"))); } void MainWindow::on_actionDonate_triggered() { QDesktopServices::openUrl(QUrl(jumpUrl("donate", "app", "menu"))); } void MainWindow::on_actionCheckForUpdates_triggered() { Updater *updater = new Updater(); connect(updater, SIGNAL(latestVersion(QString)), this, SLOT(onLatestVersion(QString))); updater->checkForNewVersion(); } void MainWindow::on_actionHelpAbout_triggered() { QDialog *aboutDialog = new QDialog; Ui::About about; about.setupUi(aboutDialog); about.versionLabel->setText( QString("Version: %1 Revision: %2").arg(version).arg(revision)); aboutDialog->exec(); delete aboutDialog; } void MainWindow::stopLocalServerMonitor() { // We are only interested in startup errors #if QT_VERSION >= 0x050600 disconnect(localServer_, SIGNAL(errorOccurred(QProcess::ProcessError)), this, SLOT(onLocalServerError(QProcess::ProcessError))); #else disconnect(localServer_, SIGNAL(error(QProcess::ProcessError)), this, SLOT(onLocalServerError(QProcess::ProcessError))); #endif disconnect(localServer_, SIGNAL(finished(int, QProcess::ExitStatus)), this, SLOT(onLocalServerFinished(int, QProcess::ExitStatus))); } void MainWindow::onLocalServerFinished(int exitCode, QProcess::ExitStatus /*exitStatus*/) { if (exitCode) reportLocalServerError(); } void MainWindow::onLocalServerError(QProcess::ProcessError /*error*/) { reportLocalServerError(); } void MainWindow::reportLocalServerError() { QMessageBox msgBox(this); msgBox.setIcon(QMessageBox::Warning); msgBox.setTextFormat(Qt::RichText); msgBox.setStyleSheet("messagebox-text-interaction-flags: 5"); // mouse copy QString errorStr = tr("

Failed to start the local drone agent - " "error 0x%1, exit status 0x%2 exit code 0x%3.

") .arg(localServer_->error(), 0, 16) .arg(localServer_->exitStatus(), 0, 16) .arg(localServer_->exitCode(), 0, 16); if (localServer_->error() == QProcess::FailedToStart) errorStr.append(tr("

The drone program does not exist at %1 or you " "don't have sufficient permissions to execute it." "

") .arg(QCoreApplication::applicationDirPath())); if (localServer_->exitCode() == 1) errorStr.append(tr("

The drone program was not able to bind to " "TCP port 7878 - maybe a drone process is already " "running?

")); #ifdef Q_OS_WIN32 if (localServer_->exitCode() == STATUS_DLL_NOT_FOUND) errorStr.append(tr("

This is most likely because Packet.dll " "was not found - make sure you have " "npcap installed and accessible." "

") .arg(jumpUrl("winpcap"))); #endif msgBox.setText(errorStr); msgBox.setInformativeText(tr("Try running drone directly.")); msgBox.exec(); QMessageBox::information(this, QString(), tr("

If you have remote drone agents running, you can still add " "and connect to them.

" "

If you don't want to start the local drone agent at startup, " "provide the -c option to Ostinato on the command line.

" "

Learn about Ostinato's Controller-Agent " "architecture

").arg(jumpUrl("arch"))); } void MainWindow::onNewVersion(QString newVersion) { QDate today = QDate::currentDate(); QDate lastChecked = QDate::fromString( appSettings->value(kLastUpdateCheck).toString(), Qt::ISODate); if (lastChecked.daysTo(today) >= 5) { QMessageBox::information(this, tr("Update check"), tr("

Ostinato version %1 is now available (you have %2). " "See change log.

" "

Visit ostinato.org to download.

") .arg(newVersion) .arg(version) .arg(jumpUrl("changelog", "app", "status", "update")) .arg(jumpUrl("download", "app", "status", "update"))); } else { QLabel *msg = new QLabel(tr("New Ostinato version %1 available. Visit " "ostinato.org to download") .arg(newVersion) .arg(jumpUrl("download", "app", "status", "update"))); msg->setOpenExternalLinks(true); statusBar()->addPermanentWidget(msg); } appSettings->setValue(kLastUpdateCheck, today.toString(Qt::ISODate)); sender()->deleteLater(); } void MainWindow::onLatestVersion(QString latestVersion) { if (version != latestVersion) { QMessageBox::information(this, tr("Update check"), tr("

Ostinato version %1 is now available (you have %2). " "See change log.

" "

Visit ostinato.org to download.

") .arg(latestVersion) .arg(version) .arg(jumpUrl("changelog", "app", "status", "update")) .arg(jumpUrl("download", "app", "status", "update"))); } else { QMessageBox::information(this, tr("Update check"), tr("You are already running the latest Ostinato version - %1") .arg(version)); } sender()->deleteLater(); } //! Returns true on success (or user cancel) and false on failure bool MainWindow::openSession(QString fileName, QString &error) { bool ret = false; QDialog *optDialog; QProgressDialog progress("Opening Session", "Cancel", 0, 0, this); OstProto::SessionContent session; SessionFileFormat *fmt = SessionFileFormat::fileFormatFromFile(fileName); if (fmt == NULL) { error = tr("Unknown session file format"); goto _fail; } if ((optDialog = fmt->openOptionsDialog())) { int ret; optDialog->setParent(this, Qt::Dialog); ret = optDialog->exec(); optDialog->setParent(0, Qt::Dialog); if (ret == QDialog::Rejected) goto _user_opt_cancel; } progress.setAutoReset(false); progress.setAutoClose(false); progress.setMinimumDuration(0); progress.show(); setDisabled(true); progress.setEnabled(true); // to override the mainWindow disable connect(fmt, SIGNAL(status(QString)),&progress,SLOT(setLabelText(QString))); connect(fmt, SIGNAL(target(int)), &progress, SLOT(setMaximum(int))); connect(fmt, SIGNAL(progress(int)), &progress, SLOT(setValue(int))); connect(&progress, SIGNAL(canceled()), fmt, SLOT(cancel())); fmt->openAsync(fileName, session, error); qDebug("after open async"); while (!fmt->isFinished()) qApp->processEvents(); qDebug("wait over for async operation"); if (!fmt->result()) goto _fail; // process any remaining events posted from the thread for (int i = 0; i < 10; i++) qApp->processEvents(); // XXX: user can't cancel operation from here on! progress.close(); portsWindow->openSession(&session, error); _user_opt_cancel: ret = true; _fail: progress.close(); setEnabled(true); return ret; } bool MainWindow::saveSession(QString fileName, QString fileType, QString &error) { bool ret = false; QProgressDialog progress("Saving Session", "Cancel", 0, 0, this); SessionFileFormat *fmt = SessionFileFormat::fileFormatFromType(fileType); OstProto::SessionContent session; if (fmt == NULL) goto _fail; progress.setAutoReset(false); progress.setAutoClose(false); progress.setMinimumDuration(0); progress.show(); setDisabled(true); progress.setEnabled(true); // to override the mainWindow disable // Fill in session ret = portsWindow->saveSession(&session, error, &progress); if (!ret) goto _user_cancel; connect(fmt, SIGNAL(status(QString)),&progress,SLOT(setLabelText(QString))); connect(fmt, SIGNAL(target(int)), &progress, SLOT(setMaximum(int))); connect(fmt, SIGNAL(progress(int)), &progress, SLOT(setValue(int))); connect(&progress, SIGNAL(canceled()), fmt, SLOT(cancel())); fmt->saveAsync(session, fileName, error); qDebug("after save async"); while (!fmt->isFinished()) qApp->processEvents(); qDebug("wait over for async operation"); ret = fmt->result(); goto _exit; _user_cancel: goto _exit; _fail: error = QString("Unsupported File Type - %1").arg(fileType); goto _exit; _exit: progress.close(); setEnabled(true); return ret; } ostinato-1.3.0/client/mainwindow.h000066400000000000000000000042461451413623100171640ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _MAIN_WINDOW_H #define _MAIN_WINDOW_H #include "ui_mainwindow.h" #include #include class LogsWindow; class PortsWindow; class PortStatsWindow; class QDockWidget; class QProcess; class MainWindow : public QMainWindow, private Ui::MainWindow { Q_OBJECT private: void openSession(QString fileName = QString()); bool openSession(QString fileName, QString &error); bool saveSession(QString fileName, QString fileType, QString &error); QProcess *localServer_; PortsWindow *portsWindow; PortStatsWindow *statsWindow; LogsWindow *logsWindow_; QDockWidget *portsDock; QDockWidget *statsDock; QDockWidget *logsDock_; QRect defaultGeometry_; QByteArray defaultLayout_; public: MainWindow(QWidget *parent = 0); ~MainWindow(); public slots: void on_actionOpenSession_triggered(); void on_actionSaveSession_triggered(); void on_actionPreferences_triggered(); void on_actionViewRestoreDefaults_triggered(); void on_actionHelpOnline_triggered(); void on_actionDonate_triggered(); void on_actionCheckForUpdates_triggered(); void on_actionHelpAbout_triggered(); private slots: void stopLocalServerMonitor(); void onLocalServerFinished(int exitCode, QProcess::ExitStatus exitStatus); void onLocalServerError(QProcess::ProcessError error); void reportLocalServerError(); void onNewVersion(QString version); void onLatestVersion(QString version); }; #endif ostinato-1.3.0/client/mainwindow.ui000066400000000000000000000106651451413623100173540ustar00rootroot00000000000000 MainWindow 0 0 1024 700 Ostinato :/icons/about.png &File &Edit &View &Streams &Devices &Help :/icons/exit.png E&xit :/icons/about.png &About :/icons/preferences.png Preferences :/icons/qt.png About &Qt true Show &My Reserved Ports Only Restore &Defaults Open Session ... Save Session ... :/icons/help.png Help (Online) :/icons/donate.png Donate Check for Updates... ostinato-1.3.0/client/mandatoryfieldsgroup.cpp000066400000000000000000000067001451413623100216020ustar00rootroot00000000000000/* Copyright (C) 2021 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #include "mandatoryfieldsgroup.h" // No need for QDateEdit, QSpinBox, etc., since these always return values #include #include #include #include #include void MandatoryFieldsGroup::add(QWidget *widget) { if (!widgets_.contains(widget)) { if (widget->inherits("QCheckBox")) connect(qobject_cast(widget), SIGNAL(clicked()), this, SLOT(changed())); else if (widget->inherits("QComboBox")) connect(qobject_cast(widget), SIGNAL(highlighted(int)), this, SLOT(changed())); else if (widget->inherits("QLineEdit")) connect(qobject_cast(widget), SIGNAL(textChanged(const QString&)), this, SLOT(changed())); else { qWarning("MandatoryFieldsGroup: unsupported class %s", widget->metaObject()->className()); return; } widgets_.append(widget); changed(); } } void MandatoryFieldsGroup::remove(QWidget *widget) { widgets_.removeAll(widget); changed(); } void MandatoryFieldsGroup::setSubmitButton(QPushButton *button) { if (submitButton_ && submitButton_ != button) submitButton_->setEnabled(true); submitButton_ = button; changed(); } void MandatoryFieldsGroup::changed() { if (!submitButton_) return; bool enable = true; for (auto widget : widgets_) { // Invisible mandatory widgets are treated as non-mandatory if (!widget->isVisible()) continue; if (widget->inherits("QCheckBox")) { // Makes sense only for tristate checkbox auto checkBox = qobject_cast(widget); if (checkBox->checkState() == Qt::PartiallyChecked) { enable = false; break; } else continue; } if (widget->inherits("QComboBox")) { auto comboBox = qobject_cast(widget); if (comboBox->currentText().isEmpty()) { enable = false; break; } else continue; } if (widget->inherits("QLineEdit")) { auto lineEdit = qobject_cast(widget); if (lineEdit->text().isEmpty() || !lineEdit->hasAcceptableInput()) { enable = false; break; } else continue; } } submitButton_->setEnabled(enable); } void MandatoryFieldsGroup::clear() { widgets_.clear(); if (submitButton_) { submitButton_->setEnabled(true); submitButton_ = nullptr; } } ostinato-1.3.0/client/mandatoryfieldsgroup.h000066400000000000000000000024661451413623100212540ustar00rootroot00000000000000/* Copyright (C) 2021 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _MANDATORY_FIELDS_GROUP_H #define _MANDATORY_FIELDS_GROUP_H #include #include class QPushButton; class QWidget; // Adapted from https://doc.qt.io/archives/qq/qq11-mandatoryfields.html // and improved class MandatoryFieldsGroup : public QObject { Q_OBJECT public: MandatoryFieldsGroup(QObject *parent) : QObject(parent) { } void add(QWidget *widget); void remove(QWidget *widget); void setSubmitButton(QPushButton *button); public slots: void clear(); private slots: void changed(); private: QList widgets_; QPushButton *submitButton_{nullptr}; }; #endif ostinato-1.3.0/client/ndpstatusmodel.cpp000066400000000000000000000077131451413623100204130ustar00rootroot00000000000000/* Copyright (C) 2016 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #include "ndpstatusmodel.h" #include "port.h" #include "emulproto.pb.h" #include "uint128.h" #include enum { kIp4Address, kMacAddress, kStatus, kFieldCount }; static QStringList columns_ = QStringList() << "IPv6 Address" << "Mac Address" << "Status"; NdpStatusModel::NdpStatusModel(QObject *parent) : QAbstractTableModel(parent) { port_ = NULL; deviceIndex_ = -1; neighbors_ = NULL; } int NdpStatusModel::rowCount(const QModelIndex &parent) const { if (!port_ || deviceIndex_ < 0 || parent.isValid()) return 0; return port_->numNdp(deviceIndex_); } int NdpStatusModel::columnCount(const QModelIndex &parent) const { if (parent.isValid()) return 0; return columns_.size(); } QVariant NdpStatusModel::headerData( int section, Qt::Orientation orientation, int role) const { if (role != Qt::DisplayRole) return QVariant(); switch (orientation) { case Qt::Horizontal: return columns_[section]; case Qt::Vertical: return QString("%1").arg(section + 1); default: Q_ASSERT(false); // Unreachable } return QVariant(); } QVariant NdpStatusModel::data(const QModelIndex &index, int role) const { QString str; if (!port_ || deviceIndex_ < 0 || !index.isValid()) return QVariant(); int ndpIdx = index.row(); int field = index.column(); Q_ASSERT(ndpIdx < port_->numNdp(deviceIndex_)); Q_ASSERT(field < kFieldCount); const OstEmul::NdpEntry &ndp = neighbors_->ndp(ndpIdx); switch (field) { case kIp4Address: switch (role) { case Qt::DisplayRole: return QHostAddress( UInt128(ndp.ip6().hi(), ndp.ip6().lo()).toArray()) .toString(); default: break; } return QVariant(); case kMacAddress: switch (role) { case Qt::DisplayRole: return QString("%1").arg(ndp.mac(), 6*2, 16, QChar('0')) .replace(QRegExp("([0-9a-fA-F]{2}\\B)"), "\\1:") .toUpper(); default: break; } return QVariant(); case kStatus: switch (role) { case Qt::DisplayRole: return ndp.mac() ? QString("Resolved") : QString("Failed"); default: break; } return QVariant(); default: Q_ASSERT(false); // unreachable! break; } qWarning("%s: Unsupported field #%d", __FUNCTION__, field); return QVariant(); } Qt::DropActions NdpStatusModel::supportedDropActions() const { return Qt::IgnoreAction; // read-only model, doesn't accept any data } void NdpStatusModel::setDeviceIndex(Port *port, int deviceIndex) { beginResetModel(); port_ = port; deviceIndex_ = deviceIndex; if (port_) neighbors_ = port_->deviceNeighbors(deviceIndex); endResetModel(); } void NdpStatusModel::updateNdpStatus() { // FIXME: why needed? beginResetModel(); endResetModel(); } ostinato-1.3.0/client/ndpstatusmodel.h000066400000000000000000000027531451413623100200570ustar00rootroot00000000000000/* Copyright (C) 2016 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _NDP_STATUS_MODEL_H #define _NDP_STATUS_MODEL_H #include class Port; namespace OstEmul { class DeviceNeighborList; } class NdpStatusModel: public QAbstractTableModel { Q_OBJECT public: NdpStatusModel(QObject *parent = 0); int rowCount(const QModelIndex &parent = QModelIndex()) const; int columnCount(const QModelIndex &parent = QModelIndex()) const; QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const; QVariant data(const QModelIndex &index, int role) const; Qt::DropActions supportedDropActions() const; void setDeviceIndex(Port *port, int deviceIndex); public slots: void updateNdpStatus(); private: Port *port_; int deviceIndex_; const OstEmul::DeviceNeighborList *neighbors_; }; #endif ostinato-1.3.0/client/ostinato.pro000066400000000000000000000074401451413623100172200ustar00rootroot00000000000000TEMPLATE = app CONFIG += qt ver_info macx: TARGET = Ostinato win32:RC_FILE = ostinato.rc macx:ICON = icons/logo.icns QT += widgets network script xml svg INCLUDEPATH += "../rpc/" "../common/" win32 { QMAKE_LFLAGS += -static CONFIG(debug, debug|release) { LIBS += -L"../common/debug" -lostprotogui -lostproto LIBS += -L"../rpc/debug" -lpbrpc POST_TARGETDEPS += \ "../common/debug/libostprotogui.a" \ "../common/debug/libostproto.a" \ "../rpc/debug/libpbrpc.a" } else { LIBS += -L"../common/release" -lostprotogui -lostproto LIBS += -L"../rpc/release" -lpbrpc POST_TARGETDEPS += \ "../common/release/libostprotogui.a" \ "../common/release/libostproto.a" \ "../rpc/release/libpbrpc.a" } } else { LIBS += -L"../common" -lostprotogui -lostproto LIBS += -L"../rpc" -lpbrpc POST_TARGETDEPS += \ "../common/libostprotogui.a" \ "../common/libostproto.a" \ "../rpc/libpbrpc.a" } LIBS += -lprotobuf LIBS += -L"../extra/qhexedit2/$(OBJECTS_DIR)/" -lqhexedit2 RESOURCES += ostinato.qrc HEADERS += \ arpstatusmodel.h \ clipboardhelper.h \ devicegroupdialog.h \ devicegroupmodel.h \ devicemodel.h \ deviceswidget.h \ dumpview.h \ fieldedit.h \ hexlineedit.h \ logsmodel.h \ logswindow.h \ findreplace.h \ mainwindow.h \ mandatoryfieldsgroup.h \ ndpstatusmodel.h \ packetmodel.h \ port.h \ portconfigdialog.h \ portgroup.h \ portgrouplist.h \ portmodel.h \ portstatsfilterdialog.h \ portstatsmodel.h \ portstatsproxymodel.h \ portstatswindow.h \ portswindow.h \ portwidget.h \ preferences.h \ settings.h \ streamconfigdialog.h \ streamlistdelegate.h \ streammodel.h \ streamstatsfiltermodel.h \ streamstatsmodel.h \ streamstatswindow.h \ streamswidget.h \ variablefieldswidget.h \ xtableview.h FORMS += \ about.ui \ devicegroupdialog.ui \ deviceswidget.ui \ findreplace.ui \ logswindow.ui \ mainwindow.ui \ portconfigdialog.ui \ portstatsfilter.ui \ portstatswindow.ui \ portswindow.ui \ portwidget.ui \ preferences.ui \ streamconfigdialog.ui \ streamstatswindow.ui \ streamswidget.ui \ variablefieldswidget.ui SOURCES += \ arpstatusmodel.cpp \ clipboardhelper.cpp \ devicegroupdialog.cpp \ devicegroupmodel.cpp \ devicemodel.cpp \ deviceswidget.cpp \ dumpview.cpp \ stream.cpp \ hexlineedit.cpp \ logsmodel.cpp \ logswindow.cpp \ fieldedit.cpp \ findreplace.cpp \ main.cpp \ mainwindow.cpp \ mandatoryfieldsgroup.cpp \ ndpstatusmodel.cpp \ packetmodel.cpp \ params.cpp \ port.cpp \ portconfigdialog.cpp \ portgroup.cpp \ portgrouplist.cpp \ portmodel.cpp \ portstatsmodel.cpp \ portstatsfilterdialog.cpp \ portstatswindow.cpp \ portswindow.cpp \ portwidget.cpp \ preferences.cpp \ streamconfigdialog.cpp \ streamlistdelegate.cpp \ streammodel.cpp \ streamstatsmodel.cpp \ streamstatswindow.cpp \ streamswidget.cpp \ thememanager.cpp \ variablefieldswidget.cpp THEMES += \ themes/material-dark.qss \ themes/material-dark.rcc \ themes/material-light.qss \ themes/material-light.rcc \ themes/qds-dark.qss \ themes/qds-dark.rcc \ themes/qds-light.qss \ themes/qds-light.rcc \ QMAKE_DISTCLEAN += object_script.* include(../install.pri) include(../shared.pri) include(../version.pri) include(../options.pri) INCLUDEPATH += "../extra/modeltest" greaterThan(QT_MINOR_VERSION, 6) { CONFIG(debug, debug|release): LIBS += -L"../extra/modeltest/$(OBJECTS_DIR)/" -lmodeltest CONFIG(debug, debug|release): QT += testlib } ostinato-1.3.0/client/ostinato.qrc000066400000000000000000000044651451413623100172110ustar00rootroot00000000000000 icons/find.png icons/info.png icons/about.png icons/add.png icons/anime_error.gif icons/anime_warn.gif icons/arrow_down.png icons/arrow_left.png icons/arrow_right.png icons/arrow_up.png icons/bullet_error.png icons/bullet_green.png icons/bullet_orange.png icons/bullet_red.png icons/bullet_white.png icons/bullet_yellow.png icons/copy.png icons/control_play.png icons/control_stop.png icons/cut.png icons/delete.png icons/devicegroup_add.png icons/devicegroup_delete.png icons/devicegroup_edit.png icons/donate.png icons/error.svg icons/exit.png icons/frag_capture.png icons/frag_exclusive.png icons/frag_link_down.png icons/frag_link_unknown.png icons/frag_link_up.png icons/frag_transmit.png icons/gaps.png icons/help.png icons/logo.png icons/magnifier.png icons/name.png icons/neighbor_clear.png icons/neighbor_resolve.png icons/paste.png icons/portgroup_add.png icons/portgroup_connect.png icons/portgroup_delete.png icons/portgroup_disconnect.png icons/portstats_clear.png icons/portstats_clear_all.png icons/portstats_filter.png icons/preferences.png icons/qt.png icons/refresh.png icons/sound_mute.png icons/sound_none.png icons/stream_add.png icons/stream_delete.png icons/stream_duplicate.png icons/stream_edit.png icons/stream_stats.png icons/transmit_on.png icons/warn.svg ostinato-1.3.0/client/ostinato.rc000066400000000000000000000001021451413623100170100ustar00rootroot00000000000000IDI_ICON1 ICON DISCARDABLE "icons/logo.ico" ostinato-1.3.0/client/packetmodel.cpp000066400000000000000000000144351451413623100176340ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #include #include "packetmodel.h" #include "../common/protocollistiterator.h" #include "../common/abstractprotocol.h" PacketModel::PacketModel(QObject *parent) : QAbstractItemModel(parent) { } void PacketModel::setSelectedProtocols(ProtocolListIterator &iter) { QList currentProtocols; iter.toFront(); while (iter.hasNext()) currentProtocols.append(iter.next()); if (mSelectedProtocols != currentProtocols) { beginResetModel(); mSelectedProtocols = currentProtocols; endResetModel(); } else { emit layoutAboutToBeChanged(); emit layoutChanged(); } } int PacketModel::rowCount(const QModelIndex &parent) const { IndexId parentId; // qDebug("in %s", __FUNCTION__); // Parent == Invalid i.e. Invisible Root. // ==> Children are Protocol (Top Level) Items if (!parent.isValid()) return mSelectedProtocols.size(); // Parent - Valid Item parentId.w = parent.internalId(); switch(parentId.ws.type) { case ITYP_PROTOCOL: return mSelectedProtocols.at(parentId.ws.protocol)->frameFieldCount(); case ITYP_FIELD: return 0; default: qWarning("%s: Unhandled ItemType", __FUNCTION__); } Q_ASSERT(1 == 0); // Unreachable code qWarning("%s: Catch all - need to investigate", __FUNCTION__); return 0; // catch all } int PacketModel::columnCount(const QModelIndex &/*parent*/) const { return 1; } QModelIndex PacketModel::index(int row, int col, const QModelIndex &parent) const { QModelIndex index; IndexId id, parentId; if (!hasIndex(row, col, parent)) goto _exit; // Parent is Invisible Root // Request for a Protocol Item if (!parent.isValid()) { id.w = 0; id.ws.type = ITYP_PROTOCOL; id.ws.protocol = row; index = createIndex(row, col, id.w); goto _exit; } // Parent is a Valid Item parentId.w = parent.internalId(); id.w = parentId.w; switch(parentId.ws.type) { case ITYP_PROTOCOL: id.ws.type = ITYP_FIELD; index = createIndex(row, col, id.w); goto _exit; case ITYP_FIELD: Q_ASSERT(1 == 0); // Unreachable code goto _exit; default: qWarning("%s: Unhandled ItemType", __FUNCTION__); } Q_ASSERT(1 == 0); // Unreachable code _exit: return index; } QModelIndex PacketModel::parent(const QModelIndex &index) const { QModelIndex parentIndex; IndexId id, parentId; if (!index.isValid()) return QModelIndex(); id.w = index.internalId(); parentId.w = id.w; switch(id.ws.type) { case ITYP_PROTOCOL: // return invalid index for invisible root goto _exit; case ITYP_FIELD: parentId.ws.type = ITYP_PROTOCOL; parentIndex = createIndex(id.ws.protocol, 0, parentId.w); goto _exit; default: qWarning("%s: Unhandled ItemType", __FUNCTION__); } Q_ASSERT(1 == 1); // Unreachable code _exit: return parentIndex; } QVariant PacketModel::data(const QModelIndex &index, int role) const { IndexId id; int fieldIdx = 0; if (!index.isValid()) return QVariant(); id.w = index.internalId(); if (id.ws.type == ITYP_FIELD) { const AbstractProtocol *p = mSelectedProtocols.at(id.ws.protocol); int n = index.row() + 1; while (n) { if (p->fieldFlags(fieldIdx).testFlag(AbstractProtocol::FrameField)) n--; fieldIdx++; } fieldIdx--; } // FIXME(HI): Relook at this completely if (role == Qt::UserRole) { switch(id.ws.type) { case ITYP_PROTOCOL: qDebug("*** %d/%d", id.ws.protocol, mSelectedProtocols.size()); return mSelectedProtocols.at(id.ws.protocol)-> protocolFrameValue(); case ITYP_FIELD: return mSelectedProtocols.at(id.ws.protocol)->fieldData( fieldIdx, AbstractProtocol::FieldFrameValue); default: qWarning("%s: Unhandled ItemType", __FUNCTION__); } return QByteArray(); } // FIXME: Use a new enum here instead of UserRole if (role == (Qt::UserRole+1)) { switch(id.ws.type) { case ITYP_PROTOCOL: return mSelectedProtocols.at(id.ws.protocol)-> protocolFrameValue().size(); case ITYP_FIELD: return mSelectedProtocols.at(id.ws.protocol)->fieldData( fieldIdx, AbstractProtocol::FieldBitSize); default: qWarning("%s: Unhandled ItemType", __FUNCTION__); } return QVariant(); } if (role != Qt::DisplayRole) return QVariant(); switch(id.ws.type) { case ITYP_PROTOCOL: return QString("%1 (%2)") .arg(mSelectedProtocols.at(id.ws.protocol)->shortName()) .arg(mSelectedProtocols.at(id.ws.protocol)->name()); case ITYP_FIELD: return mSelectedProtocols.at(id.ws.protocol)->fieldData(fieldIdx, AbstractProtocol::FieldName).toString() + QString(" : ") + mSelectedProtocols.at(id.ws.protocol)->fieldData(fieldIdx, AbstractProtocol::FieldTextValue).toString(); default: qWarning("%s: Unhandled ItemType", __FUNCTION__); } Q_ASSERT(1 == 1); // Unreachable code return QVariant(); } Qt::DropActions PacketModel::supportedDropActions() const { return Qt::IgnoreAction; // read-only model, doesn't accept any data } ostinato-1.3.0/client/packetmodel.h000066400000000000000000000035061451413623100172760ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _PACKET_MODEL_H #define _PACKET_MODEL_H #include class ProtocolListIterator; class AbstractProtocol; class PacketModel: public QAbstractItemModel { public: PacketModel(QObject *parent = 0); void setSelectedProtocols(ProtocolListIterator &iter); int rowCount(const QModelIndex &parent = QModelIndex()) const; int columnCount(const QModelIndex &parent = QModelIndex()) const; QVariant data(const QModelIndex &index, int role) const; QVariant headerData(int /*section*/, Qt::Orientation /*orientation*/, int /*role= Qt::DisplayRole*/) const { return QVariant(); } QModelIndex index (int row, int col, const QModelIndex & parent = QModelIndex() ) const; QModelIndex parent(const QModelIndex &index) const; Qt::DropActions supportedDropActions() const; private: typedef union _IndexId { quint32 w; struct { quint16 type; #define ITYP_PROTOCOL 1 #define ITYP_FIELD 2 quint16 protocol; // protocol is valid for both ITYPs } ws; } IndexId; QList mSelectedProtocols; }; #endif ostinato-1.3.0/client/params.cpp000066400000000000000000000033371451413623100166260ustar00rootroot00000000000000/* Copyright (C) 2016 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #include "params.h" #include extern char *version; extern char *revision; Params::Params() { localDrone_ = true; logsDisabled_ = true; } int Params::parseCommandLine(int argc, char* argv[]) { int c, n = 0; opterr = 0; while ((c = getopt (argc, argv, "cdhv")) != -1) { switch (c) { case 'c': localDrone_ = false; break; case 'd': logsDisabled_ = false; break; case 'v': printf("Ostinato %s rev %s\n", version, revision); exit(0); case 'h': default: printf("usage: %s [-cdhv]\n", argv[0]); exit(1); } n++; } for (int i = optind; i < argc; i++, n++) args_ << argv[i]; return n; } bool Params::optLocalDrone() { return localDrone_; } bool Params::optLogsDisabled() { return logsDisabled_; } int Params::argumentCount() { return args_.size(); } QString Params::argument(int index) { return index < args_.size() ? args_.at(index) : QString(); } ostinato-1.3.0/client/params.h000066400000000000000000000020351451413623100162650ustar00rootroot00000000000000/* Copyright (C) 2016 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _PARAMS_H #define _PARAMS_H #include class Params { public: Params(); int parseCommandLine(int argc, char* argv[]); bool optLocalDrone(); bool optLogsDisabled(); int argumentCount(); QString argument(int index); private: bool localDrone_; bool logsDisabled_; QStringList args_; }; extern Params appParams; #endif ostinato-1.3.0/client/port.cpp000066400000000000000000000644611451413623100163340ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #include "port.h" #include "emulation.h" #include "streamfileformat.h" #include #include #include #include #include #include #include extern QMainWindow *mainWindow; uint Port::mAllocStreamId = 0; uint Port::allocDeviceGroupId_ = 1; static const int kEthOverhead = 20; uint Port::newStreamId() { // We use a monotonically increasing class variable to generate // unique id. To ensure that we take into account the already // existing ids at drone, we update this variable to be greater // than the last id that we fetched // FIXME: In some cases e.g. wrap around or more likely multiple // clients, we will still run into trouble - to fix this we should // check here that the same id does not already exist; but currently // we can only do a linear search - fix this when the lookup/search // is optimized return mAllocStreamId++; } Port::Port(quint32 id, quint32 portGroupId) { mPortId = id; d.mutable_port_id()->set_id(id); stats.mutable_port_id()->set_id(id); mPortGroupId = portGroupId; capFile_ = NULL; dirty_ = false; } Port::~Port() { qDebug("%s", __FUNCTION__); while (!mStreams.isEmpty()) delete mStreams.takeFirst(); } void Port::protoDataCopyInto(OstProto::Port *data) { data->CopyFrom(d); } void Port::updatePortConfig(OstProto::Port *port) { bool recalc = false; if (port->has_transmit_mode() && port->transmit_mode() != d.transmit_mode()) recalc = true; d.MergeFrom(*port); // Setup a user-friendly alias for Win32 ports if (name().startsWith("\\Device\\NPF_")) setAlias(QString("if%1").arg(id())); if (recalc) { recalculateAverageRates(); emit streamListChanged(mPortGroupId, mPortId); // show/hide 'next' col } } void Port::updateStreamOrdinalsFromIndex() { for (int i=0; i < mStreams.size(); i++) mStreams[i]->setOrdinal(i); } void Port::reorderStreamsByOrdinals() { std::sort(mStreams.begin(), mStreams.end(), StreamBase::StreamLessThan); } void Port::setDirty(bool dirty) { if (dirty == dirty_) return; dirty_ = dirty; emit localConfigChanged(mPortGroupId, mPortId, dirty_); } void Port::recalculateAverageRates() { double pps = 0; double bps = 0; int n = 0; foreach (Stream* s, mStreams) { if (!s->isEnabled()) continue; double r = s->averagePacketRate(); pps += r; bps += r * (s->frameLenAvg() + kEthOverhead) * 8; n++; if ((transmitMode() == OstProto::kSequentialTransmit) && (s->nextWhat() == Stream::e_nw_stop)) break; } if (n) { switch (transmitMode()) { case OstProto::kSequentialTransmit: avgPacketsPerSec_ = pps/n; avgBitsPerSec_ = bps/n; break; case OstProto::kInterleavedTransmit: avgPacketsPerSec_ = pps; avgBitsPerSec_ = bps; break; default: Q_ASSERT(false); // Unreachable!! } numActiveStreams_ = n; } else avgPacketsPerSec_ = avgBitsPerSec_ = numActiveStreams_ = 0; qDebug("%s: avgPps = %g avgBps = %g numActive = %d", __FUNCTION__, avgPacketsPerSec_, avgBitsPerSec_, numActiveStreams_); emit portRateChanged(mPortGroupId, mPortId); } void Port::setAveragePacketRate(double packetsPerSec) { double rate = 0; double pps = 0; double bps = 0; int n = 0; qDebug("@%s: packetsPerSec = %g", __FUNCTION__, packetsPerSec); qDebug("@%s: avgPps = %g avgBps = %g numActive = %d", __FUNCTION__, avgPacketsPerSec_, avgBitsPerSec_, numActiveStreams_); foreach (Stream* s, mStreams) { if (!s->isEnabled()) continue; switch (transmitMode()) { case OstProto::kSequentialTransmit: rate = s->averagePacketRate() * (packetsPerSec/avgPacketsPerSec_); break; case OstProto::kInterleavedTransmit: rate = s->averagePacketRate() + ((s->averagePacketRate()/avgPacketsPerSec_) * (packetsPerSec - avgPacketsPerSec_)); break; default: Q_ASSERT(false); // Unreachable!! } // if old avgPps is 0, new rate will be calculated as nan (infinity) // because of divide by 0 (old avgPps) above - fix that if (std::isnan(rate)) rate = packetsPerSec; qDebug("cur stream pps = %g", s->averagePacketRate()); s->setAveragePacketRate(rate); qDebug("new stream pps = %g", s->averagePacketRate()); double r = s->averagePacketRate(); pps += r; bps += r * (s->frameLenAvg() + kEthOverhead) * 8; n++; if ((transmitMode() == OstProto::kSequentialTransmit) && (s->nextWhat() == Stream::e_nw_stop)) break; } if (n) { switch (transmitMode()) { case OstProto::kSequentialTransmit: avgPacketsPerSec_ = pps/n; avgBitsPerSec_ = bps/n; break; case OstProto::kInterleavedTransmit: avgPacketsPerSec_ = pps; avgBitsPerSec_ = bps; break; default: Q_ASSERT(false); // Unreachable!! } numActiveStreams_ = n; setDirty(true); } else avgPacketsPerSec_ = avgBitsPerSec_ = numActiveStreams_ = 0; qDebug("%s: avgPps = %g avgBps = %g numActive = %d", __FUNCTION__, avgPacketsPerSec_, avgBitsPerSec_, numActiveStreams_); emit portRateChanged(mPortGroupId, mPortId); } void Port::setAverageBitRate(double bitsPerSec) { double rate = 0; double pps = 0; double bps = 0; int n = 0; qDebug("@%s: bitsPerSec = %g", __FUNCTION__, bitsPerSec); qDebug("@%s: avgPps = %g avgBps = %g numActive = %d", __FUNCTION__, avgPacketsPerSec_, avgBitsPerSec_, numActiveStreams_); foreach (Stream* s, mStreams) { if (!s->isEnabled()) continue; switch (transmitMode()) { case OstProto::kSequentialTransmit: rate = s->averagePacketRate() * (bitsPerSec/avgBitsPerSec_); qDebug("rate = %g", rate); break; case OstProto::kInterleavedTransmit: rate = s->averagePacketRate() + ((s->averagePacketRate()/avgPacketsPerSec_) * ((bitsPerSec - avgBitsPerSec_) / ((s->frameLenAvg()+kEthOverhead)*8))); break; default: Q_ASSERT(false); // Unreachable!! } // if old avgBps is 0, new rate will be calculated as nan (infinity) // because of divide by 0 (old avgBps) above - fix that if (std::isnan(rate)) rate = bitsPerSec/((s->frameLenAvg()+kEthOverhead)*8); qDebug("cur stream pps = %g", s->averagePacketRate()); s->setAveragePacketRate(rate); qDebug("new stream pps = %g", s->averagePacketRate()); double r = s->averagePacketRate(); pps += r; bps += r * (s->frameLenAvg() + kEthOverhead) * 8; n++; if ((transmitMode() == OstProto::kSequentialTransmit) && (s->nextWhat() == Stream::e_nw_stop)) break; } if (n) { switch (transmitMode()) { case OstProto::kSequentialTransmit: avgPacketsPerSec_ = pps/n; avgBitsPerSec_ = bps/n; break; case OstProto::kInterleavedTransmit: avgPacketsPerSec_ = pps; avgBitsPerSec_ = bps; break; default: Q_ASSERT(false); // Unreachable!! } numActiveStreams_ = n; setDirty(true); } else avgPacketsPerSec_ = avgBitsPerSec_ = numActiveStreams_ = 0; qDebug("%s: avgPps = %g avgBps = %g numActive = %d", __FUNCTION__, avgPacketsPerSec_, avgBitsPerSec_, numActiveStreams_); emit portRateChanged(mPortGroupId, mPortId); } void Port::setAverageLoadRate(double load) { Q_ASSERT(d.speed() > 0); setAverageBitRate(load*d.speed()*1e6); } bool Port::newStreamAt(int index, OstProto::Stream const *stream) { Stream *s = new Stream; if (index > mStreams.size()) return false; if (stream) s->protoDataCopyFrom(*stream); s->setId(newStreamId()); mStreams.insert(index, s); updateStreamOrdinalsFromIndex(); recalculateAverageRates(); setDirty(true); return true; } bool Port::deleteStreamAt(int index) { if (index >= mStreams.size()) return false; delete mStreams.takeAt(index); updateStreamOrdinalsFromIndex(); recalculateAverageRates(); setDirty(true); return true; } bool Port::insertStream(uint streamId) { Stream *s = new Stream; s->setId(streamId); // FIXME(MED): If a stream with id already exists, what do we do? mStreams.append(s); // Update mAllocStreamId to take into account the stream id received // from server if (mAllocStreamId <= streamId) mAllocStreamId = streamId + 1; return true; } bool Port::updateStream(uint streamId, OstProto::Stream *stream) { int i, streamIndex; for (i = 0; i < mStreams.size(); i++) { if (streamId == mStreams[i]->id()) goto _found; } qDebug("%s: Invalid stream id %d", __FUNCTION__, streamId); return false; _found: streamIndex = i; mStreams[streamIndex]->protoDataCopyFrom(*stream); reorderStreamsByOrdinals(); return true; } void Port::getDeletedStreamsSinceLastSync( OstProto::StreamIdList &streamIdList) { streamIdList.clear_stream_id(); for (int i = 0; i < mLastSyncStreamList.size(); i++) { int j; for (j = 0; j < mStreams.size(); j++) { if (mLastSyncStreamList[i] == mStreams[j]->id()) break; } if (j < mStreams.size()) { // stream still exists! continue; } else { // stream has been deleted since last sync OstProto::StreamId *s; s = streamIdList.add_stream_id(); s->set_id(mLastSyncStreamList.at(i)); } } } void Port::getNewStreamsSinceLastSync( OstProto::StreamIdList &streamIdList) { streamIdList.clear_stream_id(); for (int i = 0; i < mStreams.size(); i++) { if (mLastSyncStreamList.contains(mStreams[i]->id())) { // existing stream! continue; } else { // new stream! OstProto::StreamId *s; s = streamIdList.add_stream_id(); s->set_id(mStreams[i]->id()); } } } void Port::getModifiedStreamsSinceLastSync( OstProto::StreamConfigList &streamConfigList) { qDebug("In %s", __FUNCTION__); //streamConfigList.mutable_port_id()->set_id(mPortId); for (int i = 0; i < mStreams.size(); i++) { OstProto::Stream *s; s = streamConfigList.add_stream(); mStreams[i]->protoDataCopyInto(*s); } qDebug("Done %s", __FUNCTION__); } void Port::getDeletedDeviceGroupsSinceLastSync( OstProto::DeviceGroupIdList &deviceGroupIdList) { deviceGroupIdList.clear_device_group_id(); foreach(int id, lastSyncDeviceGroupList_) { if (!deviceGroupById(id)) deviceGroupIdList.add_device_group_id()->set_id(id); } } void Port::getNewDeviceGroupsSinceLastSync( OstProto::DeviceGroupIdList &deviceGroupIdList) { deviceGroupIdList.clear_device_group_id(); foreach(OstProto::DeviceGroup *dg, deviceGroups_) { quint32 dgid = dg->device_group_id().id(); if (!lastSyncDeviceGroupList_.contains(dgid)) deviceGroupIdList.add_device_group_id()->set_id(dgid); } } void Port::getModifiedDeviceGroupsSinceLastSync( OstProto::DeviceGroupConfigList &deviceGroupConfigList) { deviceGroupConfigList.clear_device_group(); foreach(quint32 id, modifiedDeviceGroupList_) deviceGroupConfigList.add_device_group() ->CopyFrom(*deviceGroupById(id)); } /*! * Finds the user modifiable fields in 'config' that are different from * the current configuration on the port and modifes 'config' such that * only those fields are set and returns true. If 'config' is same as * current port config, 'config' is unmodified and false is returned */ bool Port::modifiablePortConfig(OstProto::Port &config) const { bool change = false; OstProto::Port modCfg; if (config.is_exclusive_control() != d.is_exclusive_control()) { modCfg.set_is_exclusive_control(config.is_exclusive_control()); change = true; } if (config.transmit_mode() != d.transmit_mode()) { modCfg.set_transmit_mode(config.transmit_mode()); change = true; } if (config.user_name() != d.user_name()) { modCfg.set_user_name(config.user_name()); change = true; } if (config.is_tracking_stream_stats() != d.is_tracking_stream_stats()) { modCfg.set_is_tracking_stream_stats(config.is_tracking_stream_stats()); change = true; } if (change) { modCfg.mutable_port_id()->set_id(id()); config.CopyFrom(modCfg); return true; } return false; } void Port::when_syncComplete() { //reorderStreamsByOrdinals(); mLastSyncStreamList.clear(); for (int i=0; iid()); lastSyncDeviceGroupList_.clear(); for (int i = 0; i < deviceGroups_.size(); i++) { lastSyncDeviceGroupList_.append( deviceGroups_.at(i)->device_group_id().id()); } modifiedDeviceGroupList_.clear(); setDirty(false); } void Port::updateStats(OstProto::PortStats *portStats) { OstProto::PortState oldState; oldState = stats.state(); stats.MergeFrom(*portStats); if ((oldState.link_state() != stats.state().link_state()) || (oldState.is_transmit_on() != stats.state().is_transmit_on()) || (oldState.is_capture_on() != stats.state().is_capture_on())) { qDebug("portstate changed"); emit portDataChanged(mPortGroupId, mPortId); } } void Port::duplicateStreams(const QList &list, int count) { QList sources; foreach(int index, list) { OstProto::Stream stream; Q_ASSERT(index < mStreams.size()); mStreams.at(index)->protoDataCopyInto(stream); sources.append(stream); } int insertAt = mStreams.size(); for (int i=0; i < count; i++) { for (int j=0; j < sources.size(); j++) { newStreamAt(insertAt, &sources.at(j)); // Rename stream by appending the copy count mStreams.at(insertAt)->setName(QString("%1 (%2)") .arg(mStreams.at(insertAt)->name()) .arg(i+1)); insertAt++; } } setDirty(true); emit streamListChanged(mPortGroupId, mPortId); } bool Port::openStreams(QString fileName, bool append, QString &error) { bool ret = false; QDialog *optDialog; QProgressDialog progress("Opening Streams", "Cancel", 0, 0, mainWindow); OstProto::StreamConfigList streams; StreamFileFormat *fmt = StreamFileFormat::fileFormatFromFile(fileName); if (fmt == NULL) { error = tr("Unknown streams file format"); goto _fail; } if ((optDialog = fmt->openOptionsDialog())) { int ret; optDialog->setParent(mainWindow, Qt::Dialog); ret = optDialog->exec(); if (ret == QDialog::Rejected) goto _user_opt_cancel; } progress.setAutoReset(false); progress.setAutoClose(false); progress.setMinimumDuration(0); progress.show(); mainWindow->setDisabled(true); progress.setEnabled(true); // to override the mainWindow disable connect(fmt, SIGNAL(status(QString)),&progress,SLOT(setLabelText(QString))); connect(fmt, SIGNAL(target(int)), &progress, SLOT(setMaximum(int))); connect(fmt, SIGNAL(progress(int)), &progress, SLOT(setValue(int))); connect(&progress, SIGNAL(canceled()), fmt, SLOT(cancel())); fmt->openAsync(fileName, streams, error); qDebug("after open async"); while (!fmt->isFinished()) qApp->processEvents(); qDebug("wait over for async operation"); if (!fmt->result()) goto _fail; // process any remaining events posted from the thread for (int i = 0; i < 10; i++) qApp->processEvents(); if (!append) { int n = numStreams(); progress.setLabelText("Deleting existing streams..."); progress.setRange(0, n); for (int i = 0; i < n; i++) { if (progress.wasCanceled()) goto _user_cancel; deleteStreamAt(0); progress.setValue(i); if (i % 32 == 0) qApp->processEvents(); } } progress.setLabelText("Constructing new streams..."); progress.setRange(0, streams.stream_size()); for (int i = 0; i < streams.stream_size(); i++) { if (progress.wasCanceled()) goto _user_cancel; newStreamAt(mStreams.size(), &streams.stream(i)); progress.setValue(i); if (i % 32 == 0) qApp->processEvents(); } setDirty(true); _user_cancel: emit streamListChanged(mPortGroupId, mPortId); _user_opt_cancel: ret = true; _fail: progress.close(); mainWindow->setEnabled(true); recalculateAverageRates(); return ret; } bool Port::saveStreams(QString fileName, QString fileType, QString &error) { bool ret = false; QProgressDialog progress("Saving Streams", "Cancel", 0, 0, mainWindow); StreamFileFormat *fmt = StreamFileFormat::fileFormatFromType(fileType); OstProto::StreamConfigList streams; if (fmt == NULL) goto _fail; progress.setAutoReset(false); progress.setAutoClose(false); progress.setMinimumDuration(0); progress.show(); mainWindow->setDisabled(true); progress.setEnabled(true); // to override the mainWindow disable connect(fmt, SIGNAL(status(QString)),&progress,SLOT(setLabelText(QString))); connect(fmt, SIGNAL(target(int)), &progress, SLOT(setMaximum(int))); connect(fmt, SIGNAL(progress(int)), &progress, SLOT(setValue(int))); connect(&progress, SIGNAL(canceled()), fmt, SLOT(cancel())); progress.setLabelText("Preparing Streams..."); progress.setRange(0, mStreams.size()); streams.mutable_port_id()->set_id(0); for (int i = 0; i < mStreams.size(); i++) { OstProto::Stream *s = streams.add_stream(); mStreams[i]->protoDataCopyInto(*s); if (progress.wasCanceled()) goto _user_cancel; progress.setValue(i); if (i % 32 == 0) qApp->processEvents(); } fmt->saveAsync(streams, fileName, error); qDebug("after save async"); while (!fmt->isFinished()) qApp->processEvents(); qDebug("wait over for async operation"); ret = fmt->result(); goto _exit; _user_cancel: goto _exit; _fail: error = QString("Unsupported File Type - %1").arg(fileType); goto _exit; _exit: progress.close(); mainWindow->setEnabled(true); return ret; } // ------------ Device Group ----------- // uint Port::newDeviceGroupId() { // We use a monotonically increasing class variable to generate // unique id. To ensure that we take into account the already // existing ids at drone, we update this variable to be greater // than the last id that we fetched // FIXME: In some cases e.g. wrap around or more likely multiple // clients, we will still run into trouble - to fix this we should // check here that the same id does not already exist; but currently // we can only do a linear search - fix this when the lookup/search // is optimized return allocDeviceGroupId_++; } int Port::numDeviceGroups() const { return deviceGroups_.size(); } const OstProto::DeviceGroup* Port::deviceGroupByIndex(int index) const { if ((index < 0) || (index >= numDeviceGroups())) { qWarning("%s: index %d out of range (0 - %d)", __FUNCTION__, index, numDeviceGroups() - 1); return NULL; } return deviceGroups_.at(index); } OstProto::DeviceGroup* Port::mutableDeviceGroupByIndex(int index) { if ((index < 0) || (index >= numDeviceGroups())) { qWarning("%s: index %d out of range (0 - %d)", __FUNCTION__, index, numDeviceGroups() - 1); return NULL; } OstProto::DeviceGroup *devGrp = deviceGroups_.at(index); // Caller can modify DeviceGroup - assume she will modifiedDeviceGroupList_.insert(devGrp->device_group_id().id()); setDirty(true); return devGrp; } const OstProto::DeviceGroup* Port::deviceGroupById(uint deviceGroupId) const { for (int i = 0; i < deviceGroups_.size(); i++) { OstProto::DeviceGroup *devGrp = deviceGroups_.at(i); if (devGrp->device_group_id().id() == deviceGroupId) return devGrp; } return NULL; } bool Port::newDeviceGroupAt(int index, const OstProto::DeviceGroup *deviceGroup) { if (index < 0 || index > numDeviceGroups()) return false; OstProto::DeviceGroup *devGrp = newDeviceGroup(id()); if (!devGrp) { qWarning("failed allocating a new device group"); return false; } if (deviceGroup) devGrp->CopyFrom(*deviceGroup); devGrp->mutable_device_group_id()->set_id(newDeviceGroupId()); deviceGroups_.insert(index, devGrp); modifiedDeviceGroupList_.insert(devGrp->device_group_id().id()); setDirty(true); return true; } bool Port::deleteDeviceGroupAt(int index) { if (index < 0 || index >= deviceGroups_.size()) return false; OstProto::DeviceGroup *devGrp = deviceGroups_.takeAt(index); modifiedDeviceGroupList_.remove(devGrp->device_group_id().id()); delete devGrp; setDirty(true); return true; } bool Port::insertDeviceGroup(uint deviceGroupId) { OstProto::DeviceGroup *devGrp; if (deviceGroupById(deviceGroupId)) { qDebug("%s: deviceGroup id %u already exists", __FUNCTION__, deviceGroupId); return false; } devGrp = newDeviceGroup(id()); devGrp->mutable_device_group_id()->set_id(deviceGroupId); deviceGroups_.append(devGrp); // Update allocDeviceGroupId_ to take into account the deviceGroup id // received from server - this is required to make sure newly allocated // deviceGroup ids are unique if (allocDeviceGroupId_ <= deviceGroupId) allocDeviceGroupId_ = deviceGroupId + 1; return true; } bool Port::updateDeviceGroup( uint deviceGroupId, OstProto::DeviceGroup *deviceGroup) { using OstProto::DeviceGroup; // XXX: We should not call mutableDeviceGroupById() because that will // implicitly set the port as dirty, so we use a const_cast hack instead DeviceGroup *devGrp = const_cast( deviceGroupById(deviceGroupId)); if (!devGrp) { qDebug("%s: deviceGroup id %u does not exist", __FUNCTION__, deviceGroupId); return false; } devGrp->CopyFrom(*deviceGroup); return true; } // ------------ Devices ----------- // int Port::numDevices() { return devices_.size(); } const OstEmul::Device* Port::deviceByIndex(int index) { if ((index < 0) || (index >= numDevices())) { qWarning("%s: index %d out of range (0 - %d)", __FUNCTION__, index, numDevices() - 1); return NULL; } return devices_.at(index); } void Port::clearDeviceList() { while (devices_.size()) delete devices_.takeFirst(); } void Port::insertDevice(const OstEmul::Device &device) { OstEmul::Device *dev = new OstEmul::Device(device); devices_.append(dev); } // ------------- Device Neighbors (ARP/NDP) ------------- // const OstEmul::DeviceNeighborList* Port::deviceNeighbors(int deviceIndex) { if ((deviceIndex < 0) || (deviceIndex >= numDevices())) { qWarning("%s: index %d out of range (0 - %d)", __FUNCTION__, deviceIndex, numDevices() - 1); return NULL; } return deviceNeighbors_.value(deviceIndex); } int Port::numArp(int deviceIndex) { if (deviceNeighbors_.contains(deviceIndex)) return deviceNeighbors_.value(deviceIndex)->arp_size(); return 0; } int Port::numArpResolved(int deviceIndex) { if (arpResolvedCount_.contains(deviceIndex)) return arpResolvedCount_.value(deviceIndex); return 0; } int Port::numNdp(int deviceIndex) { if (deviceNeighbors_.contains(deviceIndex)) return deviceNeighbors_.value(deviceIndex)->ndp_size(); return 0; } int Port::numNdpResolved(int deviceIndex) { if (ndpResolvedCount_.contains(deviceIndex)) return ndpResolvedCount_.value(deviceIndex); return 0; } void Port::clearDeviceNeighbors() { arpResolvedCount_.clear(); ndpResolvedCount_.clear(); qDeleteAll(deviceNeighbors_); deviceNeighbors_.clear(); } void Port::insertDeviceNeighbors(const OstEmul::DeviceNeighborList &neighList) { int count; OstEmul::DeviceNeighborList *neighbors = new OstEmul::DeviceNeighborList(neighList); deviceNeighbors_.insert(neighList.device_index(), neighbors); count = 0; for (int i = 0; i < neighbors->arp_size(); i++) if (neighbors->arp(i).mac()) count++; arpResolvedCount_.insert(neighbors->device_index(), count); count = 0; for (int i = 0; i < neighbors->ndp_size(); i++) if (neighbors->ndp(i).mac()) count++; ndpResolvedCount_.insert(neighbors->device_index(), count); } void Port::deviceInfoRefreshed() { emit deviceInfoChanged(); } ostinato-1.3.0/client/port.h000066400000000000000000000213441451413623100157720ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _PORT_H #define _PORT_H #include #include #include #include #include #include #include "stream.h" //class StreamModel; namespace OstEmul { class Device; class DeviceNeighborList; } class Port : public QObject { Q_OBJECT static uint mAllocStreamId; static uint allocDeviceGroupId_; OstProto::Port d; OstProto::PortStats stats; QTemporaryFile *capFile_; // FIXME(HI): consider removing mPortId as it is duplicated inside 'd' quint32 mPortId; quint32 mPortGroupId; QString mUserAlias; // user defined bool dirty_; double avgPacketsPerSec_; double avgBitsPerSec_; int numActiveStreams_; QList mLastSyncStreamList; QList mStreams; // sorted by stream's ordinal value QList lastSyncDeviceGroupList_; QSet modifiedDeviceGroupList_; QList deviceGroups_; QList devices_; QHash deviceNeighbors_; QHash arpResolvedCount_; QHash ndpResolvedCount_; uint newStreamId(); void updateStreamOrdinalsFromIndex(); void reorderStreamsByOrdinals(); void setDirty(bool dirty); public: enum AdminStatus { AdminDisable, AdminEnable }; // FIXME(HIGH): default args is a hack for QList operations on Port Port(quint32 id = 0xFFFFFFFF, quint32 pgId = 0xFFFFFFFF); ~Port(); quint32 portGroupId() const { return mPortGroupId; } const QString userAlias() const { return mUserAlias.isEmpty() ? name() : mUserAlias; } quint32 id() const { return d.port_id().id(); } const QString name() const { return QString().fromStdString(d.name()); } const QString systemDescription() const { return QString().fromStdString(d.description()); } const QString userDescription() const { return QString().fromStdString(d.user_description()); } const QString description() const { return userDescription().isEmpty() ? systemDescription() : userDescription(); } const QString notes() const { return QString().fromStdString(d.notes()); } const QString userName() const { return QString().fromStdString(d.user_name()); } AdminStatus adminStatus() const { return (d.is_enabled()?AdminEnable:AdminDisable); } bool hasExclusiveControl() const { return d.is_exclusive_control(); } OstProto::TransmitMode transmitMode() const { return d.transmit_mode(); } bool trackStreamStats() const { return d.is_tracking_stream_stats(); } double speed() const { return d.speed(); } double averageLoadRate() const { return d.speed() ? avgBitsPerSec_/(d.speed()*1e6) : 0; } double averagePacketRate() const { return avgPacketsPerSec_; } double averageBitRate() const { return avgBitsPerSec_; } //void setAdminEnable(AdminStatus status) { mAdminStatus = status; } void setAlias(QString alias) { mUserAlias = alias; } //void setExclusive(bool flag); int numStreams() { return mStreams.size(); } const Stream* streamByIndex(int index) const { Q_ASSERT(index < mStreams.size()); return mStreams[index]; } Stream* mutableStreamByIndex(int index, bool assumeChange = true) { Q_ASSERT(index < mStreams.size()); if (assumeChange) setDirty(true); return mStreams[index]; } void setLocalConfigChanged(bool changed) { setDirty(changed); } OstProto::LinkState linkState() { return stats.state().link_state(); } bool isTransmitting() { return stats.state().is_transmit_on(); } bool isCapturing() { return stats.state().is_capture_on(); } OstProto::PortStats getStats() { return stats; } QTemporaryFile* getCaptureFile() { delete capFile_; capFile_ = new QTemporaryFile(QString(QDir::tempPath()) .append("/") .append(userAlias()) .append(".XXXXXX")); return capFile_; } void protoDataCopyInto(OstProto::Port *data); //! Used when config received from server // FIXME(MED): naming inconsistency - PortConfig/Stream; also retVal void updatePortConfig(OstProto::Port *port); //! Used by StreamModel //@{ bool newStreamAt(int index, OstProto::Stream const *stream = NULL); bool deleteStreamAt(int index); //@} //! Used by MyService::Stub to update from config received from server //@{ bool insertStream(uint streamId); bool updateStream(uint streamId, OstProto::Stream *stream); //@} bool isDirty() { return dirty_; } void getDeletedStreamsSinceLastSync(OstProto::StreamIdList &streamIdList); void getNewStreamsSinceLastSync(OstProto::StreamIdList &streamIdList); void getModifiedStreamsSinceLastSync( OstProto::StreamConfigList &streamConfigList); void getDeletedDeviceGroupsSinceLastSync( OstProto::DeviceGroupIdList &streamIdList); void getNewDeviceGroupsSinceLastSync( OstProto::DeviceGroupIdList &streamIdList); void getModifiedDeviceGroupsSinceLastSync( OstProto::DeviceGroupConfigList &streamConfigList); bool modifiablePortConfig(OstProto::Port &config) const; void when_syncComplete(); void setAveragePacketRate(double packetsPerSec); void setAverageBitRate(double bitsPerSec); void setAverageLoadRate(double loadPercent); // FIXME(MED): Bad Hack! port should not need an external trigger to // recalculate - refactor client side domain objects and model objects void recalculateAverageRates(); void updateStats(OstProto::PortStats *portStats); void duplicateStreams(const QList &list, int count); bool openStreams(QString fileName, bool append, QString &error); bool saveStreams(QString fileName, QString fileType, QString &error); // ------------ Device Group ----------- // uint newDeviceGroupId(); int numDeviceGroups() const; const OstProto::DeviceGroup* deviceGroupByIndex(int index) const; OstProto::DeviceGroup* mutableDeviceGroupByIndex(int index); const OstProto::DeviceGroup* deviceGroupById(uint deviceGroupId) const; //! Used by StreamModel //@{ bool newDeviceGroupAt(int index, const OstProto::DeviceGroup *deviceGroup = NULL); bool deleteDeviceGroupAt(int index); //@} //! Used by MyService::Stub to update from config received from server //@{ bool insertDeviceGroup(uint deviceGroupId); bool updateDeviceGroup(uint deviceGroupId, OstProto::DeviceGroup *deviceGroup); //@} // ------------ Device ----------- // int numDevices(); const OstEmul::Device* deviceByIndex(int index); //! Used by MyService::Stub to update from config received from server void clearDeviceList(); void insertDevice(const OstEmul::Device &device); const OstEmul::DeviceNeighborList* deviceNeighbors(int deviceIndex); int numArp(int deviceIndex); int numArpResolved(int deviceIndex); int numNdp(int deviceIndex); int numNdpResolved(int deviceIndex); //! Used by MyService::Stub to update from config received from server void clearDeviceNeighbors(); void insertDeviceNeighbors(const OstEmul::DeviceNeighborList &neighList); void deviceInfoRefreshed(); signals: //! Used when local config changed and when config received from server void portRateChanged(int portGroupId, int portId); //! Used by MyService::Stub to update from config received from server //@{ void portDataChanged(int portGroupId, int portId); void deviceInfoChanged(); //@} //! Used when local config changed //@{ void streamListChanged(int portGroupId, int portId); void localConfigChanged(int portGroupId, int portId, bool changed); //@} }; #endif ostinato-1.3.0/client/portconfigdialog.cpp000066400000000000000000000110201451413623100206610ustar00rootroot00000000000000/* Copyright (C) 2011 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #include "portconfigdialog.h" #include "settings.h" PortConfigDialog::PortConfigDialog( OstProto::Port &portConfig, const OstProto::PortState &portState, QWidget *parent) : QDialog(parent), portConfig_(portConfig) { QString currentUser(portConfig_.user_name().c_str()); qDebug("In %s", __FUNCTION__); setupUi(this); description->setPlaceholderText(portConfig_.description().c_str()); description->setText(portConfig_.user_description().c_str()); switch(portConfig_.transmit_mode()) { case OstProto::kSequentialTransmit: sequentialStreamsButton->setChecked(true); break; case OstProto::kInterleavedTransmit: interleavedStreamsButton->setChecked(true); break; default: Q_ASSERT(false); // Unreachable!!! break; } // Port Reservation myself_ = appSettings->value(kUserKey, kUserDefaultValue).toString(); // XXX: what if myself_ is empty? if (currentUser.isEmpty()) { reservedBy_ = kNone; reservedBy->setText("Unreserved"); reserveButton->setText("Reserve"); } else if (currentUser == myself_) { reservedBy_ = kSelf; reservedBy->setText("Reserved by: me ("+currentUser+")"); reserveButton->setText("Reserve (uncheck to unreserve)"); reserveButton->setChecked(true); } else { reservedBy_ = kOther; reservedBy->setText("Reserved by: "+currentUser+""); reserveButton->setText("Force reserve"); } qDebug("reservedBy_ = %d", reservedBy_); exclusiveControlButton->setChecked(portConfig_.is_exclusive_control()); streamStatsButton->setChecked(portConfig_.is_tracking_stream_stats()); // Disable UI elements based on portState if (portState.is_transmit_on()) { transmitModeBox->setDisabled(true); streamStatsButton->setDisabled(true); } } void PortConfigDialog::accept() { OstProto::Port pc; pc.set_user_description(description->text().toStdString()); if (sequentialStreamsButton->isChecked()) pc.set_transmit_mode(OstProto::kSequentialTransmit); else if (interleavedStreamsButton->isChecked()) pc.set_transmit_mode(OstProto::kInterleavedTransmit); else Q_ASSERT(false); // Unreachable!!! pc.set_user_name(portConfig_.user_name()); switch (reservedBy_) { case kSelf: if (!reserveButton->isChecked()) pc.set_user_name(""); // unreserve break; case kOther: case kNone: if (reserveButton->isChecked()) pc.set_user_name(myself_.toStdString()); // (force) reserve break; default: qWarning("Unreachable code"); break; } pc.set_is_exclusive_control(exclusiveControlButton->isChecked()); pc.set_is_tracking_stream_stats(streamStatsButton->isChecked()); // Update fields that have changed, clear the rest if (pc.user_description() != portConfig_.user_description()) portConfig_.set_user_description(pc.user_description()); else portConfig_.clear_user_description(); if (pc.transmit_mode() != portConfig_.transmit_mode()) portConfig_.set_transmit_mode(pc.transmit_mode()); else portConfig_.clear_transmit_mode(); if (pc.user_name() != portConfig_.user_name()) portConfig_.set_user_name(pc.user_name()); else portConfig_.clear_user_name(); if (pc.is_exclusive_control() != portConfig_.is_exclusive_control()) portConfig_.set_is_exclusive_control(pc.is_exclusive_control()); else portConfig_.clear_is_exclusive_control(); if (pc.is_tracking_stream_stats() != portConfig_.is_tracking_stream_stats()) portConfig_.set_is_tracking_stream_stats(pc.is_tracking_stream_stats()); else portConfig_.clear_is_tracking_stream_stats(); QDialog::accept(); } ostinato-1.3.0/client/portconfigdialog.h000066400000000000000000000022461451413623100203400ustar00rootroot00000000000000/* Copyright (C) 2011 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _PORT_CONFIG_DIALOG_H #define _PORT_CONFIG_DIALOG_H #include "ui_portconfigdialog.h" #include "protocol.pb.h" #include class PortConfigDialog : public QDialog, public Ui::PortConfigDialog { public: PortConfigDialog(OstProto::Port &portConfig, const OstProto::PortState& portState, QWidget *parent); private: virtual void accept(); OstProto::Port &portConfig_; enum { kNone, kSelf, kOther } reservedBy_; QString myself_; }; #endif ostinato-1.3.0/client/portconfigdialog.ui000066400000000000000000000075361451413623100205350ustar00rootroot00000000000000 PortConfigDialog 0 0 248 292 Port Config Description description Transmit Mode Sequential Streams true Interleaved Streams Reservation Reserved by: Reserve Exclusive Control Stream Statistics Qt::Vertical 226 71 Qt::Horizontal QDialogButtonBox::Cancel|QDialogButtonBox::Ok description sequentialStreamsButton interleavedStreamsButton reserveButton exclusiveControlButton streamStatsButton buttonBox accepted() PortConfigDialog accept() 234 205 157 214 buttonBox rejected() PortConfigDialog reject() 234 205 243 214 ostinato-1.3.0/client/portgroup.cpp000066400000000000000000002062321451413623100174030ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #include "portgroup.h" #include "jumpurl.h" #include "log.h" #include "settings.h" #include "emulproto.pb.h" #include "fileformat.pb.h" #include #include #include #include #include #include #include #include #include #include #include using ::google::protobuf::NewCallback; extern QMainWindow *mainWindow; extern char *version; quint32 PortGroup::mPortGroupAllocId = 0; PortGroup::PortGroup(QString serverName, quint16 port) { // Allocate an id for self mPortGroupId = PortGroup::mPortGroupAllocId++; portIdList_ = new OstProto::PortIdList; portStatsList_ = new OstProto::PortStatsList; statsController = new PbRpcController(portIdList_, portStatsList_); isGetStatsPending_ = false; atConnectConfig_ = NULL; compat = kUnknown; reconnect = false; reconnectAfter = kMinReconnectWaitTime; reconnectTimer = new QTimer(this); reconnectTimer->setSingleShot(true); connect(reconnectTimer, SIGNAL(timeout()), this, SLOT(on_reconnectTimer_timeout())); rpcChannel = new PbRpcChannel(serverName, port, OstProto::Notification::default_instance()); serviceStub = new OstProto::OstService::Stub(rpcChannel); // FIXME(LOW):Can't for my life figure out why this ain't working! //QMetaObject::connectSlotsByName(this); connect(rpcChannel, SIGNAL(stateChanged(QAbstractSocket::SocketState)), this, SLOT(on_rpcChannel_stateChanged(QAbstractSocket::SocketState))); connect(rpcChannel, SIGNAL(connected()), this, SLOT(on_rpcChannel_connected())); connect(rpcChannel, SIGNAL(disconnected()), this, SLOT(on_rpcChannel_disconnected())); connect(rpcChannel, SIGNAL(error(QAbstractSocket::SocketError)), this, SLOT(on_rpcChannel_error(QAbstractSocket::SocketError))); connect(rpcChannel, SIGNAL(notification(int, ::google::protobuf::Message*)), this, SLOT(on_rpcChannel_notification(int, ::google::protobuf::Message*))); connect(this, SIGNAL(portListChanged(quint32)), this, SLOT(when_portListChanged(quint32)), Qt::QueuedConnection); } PortGroup::~PortGroup() { qDebug("PortGroup Destructor"); // Disconnect and free rpc channel etc. PortGroup::disconnectFromHost(); delete serviceStub; delete rpcChannel; delete statsController; delete atConnectConfig_; } void PortGroup::setConfigAtConnect(const OstProto::PortGroupContent *config) { if (!config) { delete atConnectConfig_; atConnectConfig_ = NULL; return; } if (!atConnectConfig_) atConnectConfig_ = new OstProto::PortGroupContent; atConnectConfig_->CopyFrom(*config); } int PortGroup::numReservedPorts() const { int count = 0; for (int i = 0; i < mPorts.size(); i++) { if (!mPorts[i]->userName().isEmpty()) count++; } return count; } const QString PortGroup::serverFullName() const { return serverPort() == DEFAULT_SERVER_PORT ? serverName() : QString("%1:%2").arg(serverName()).arg(serverPort()); } // ------------------------------------------------ // Slots // ------------------------------------------------ void PortGroup::on_reconnectTimer_timeout() { reconnectAfter *= 2; if (reconnectAfter > kMaxReconnectWaitTime) reconnectAfter = kMaxReconnectWaitTime; connectToHost(); } void PortGroup::on_rpcChannel_stateChanged(QAbstractSocket::SocketState state) { qDebug("state changed %d", state); switch (state) { case QAbstractSocket::UnconnectedState: case QAbstractSocket::ClosingState: break; default: emit portGroupDataChanged(mPortGroupId); } } void PortGroup::on_rpcChannel_connected() { OstProto::VersionInfo *verInfo = new OstProto::VersionInfo; OstProto::VersionCompatibility *verCompat = new OstProto::VersionCompatibility; qDebug("connected\n"); logInfo(id(), "PortGroup connected"); emit portGroupDataChanged(mPortGroupId); reconnectAfter = kMinReconnectWaitTime; qDebug("requesting version check ..."); verInfo->set_client_name("ostinato"); verInfo->set_version(version); PbRpcController *controller = new PbRpcController(verInfo, verCompat); serviceStub->checkVersion(controller, verInfo, verCompat, NewCallback(this, &PortGroup::processVersionCompatibility, controller)); } void PortGroup::processVersionCompatibility(PbRpcController *controller) { OstProto::VersionCompatibility *verCompat = static_cast(controller->response()); Q_ASSERT(verCompat != NULL); qDebug("got version result ..."); if (controller->Failed()) { qDebug("%s: rpc failed(%s)", __FUNCTION__, qPrintable(controller->ErrorString())); logError(id(), QString("checkVersion RPC failed: %1") .arg(controller->ErrorString())); goto _error_exit; } if (verCompat->result() == OstProto::VersionCompatibility::kIncompatible) { qWarning("incompatible version %s (%s)", version, qPrintable(QString::fromStdString(verCompat->notes()))); logError(id(), QString("checkVersion failed: %1") .arg(QString::fromStdString(verCompat->notes()))); compat = kIncompatible; reconnect = false; emit portGroupDataChanged(mPortGroupId); QMessageBox msgBox; msgBox.setIcon(QMessageBox::Warning); msgBox.setTextFormat(Qt::RichText); msgBox.setStyleSheet("messagebox-text-interaction-flags: 5"); msgBox.setText(tr("The Drone agent at %1:%2 is incompatible with this " "Ostinato version - %3") .arg(serverName()) .arg(int(serverPort())) .arg(version)); msgBox.setInformativeText(QString::fromStdString(verCompat->notes())); msgBox.exec(); goto _error_exit; } compat = kCompatible; { OstProto::Void *void_ = new OstProto::Void; OstProto::PortIdList *portIdList = new OstProto::PortIdList; qDebug("requesting portlist ..."); PbRpcController *controller = new PbRpcController(void_, portIdList); serviceStub->getPortIdList(controller, void_, portIdList, NewCallback(this, &PortGroup::processPortIdList, controller)); } _error_exit: delete controller; } void PortGroup::on_rpcChannel_disconnected() { qDebug("disconnected %s:%u", qPrintable(rpcChannel->serverName()), rpcChannel->serverPort()); logError(id(), "PortGroup disconnected"); emit portListAboutToBeChanged(mPortGroupId); while (!mPorts.isEmpty()) delete mPorts.takeFirst(); atConnectPortConfig_.clear(); emit portListChanged(mPortGroupId); emit portGroupDataChanged(mPortGroupId); // Disconnected during apply? Restore UI. if (applyTimer_.isValid()) { applyTimer_.invalidate(); emit applyFinished(); mainWindow->setEnabled(true); QApplication::restoreOverrideCursor(); } isGetStatsPending_ = false; if (reconnect) { qDebug("starting reconnect timer for %d ms ...", reconnectAfter); logInfo(id(), QString("Reconnect attempt after %1s") .arg(double(reconnectAfter)/1000.0)); reconnectTimer->start(reconnectAfter); } } void PortGroup::on_rpcChannel_error(QAbstractSocket::SocketError socketError) { qDebug("%s: error %d %s:%u", __FUNCTION__, socketError, qPrintable(rpcChannel->serverName()), rpcChannel->serverPort()); emit portGroupDataChanged(mPortGroupId); switch(socketError) { case QAbstractSocket::SslInvalidUserDataError: // actually abort() logWarn(id(), QString("Bad data received from portgroup, " "aborting connection; " "who is listening on %1:%2 " " - is it drone or some other process?") .arg(rpcChannel->serverName()) .arg(rpcChannel->serverPort())); reconnect = false; break; default: break; } qDebug("%s: state %d", __FUNCTION__, rpcChannel->state()); if ((rpcChannel->state() == QAbstractSocket::UnconnectedState) && reconnect) { qDebug("starting reconnect timer for %d ms...", reconnectAfter); logInfo(id(), QString("Reconnect attempt after %1s") .arg(double(reconnectAfter)/1000.0)); reconnectTimer->start(reconnectAfter); } } void PortGroup::on_rpcChannel_notification(int notifType, ::google::protobuf::Message *notification) { OstProto::Notification *notif = dynamic_cast(notification); if (!notif) { qWarning("unable to dynamic cast notif"); return; } if (notifType != notif->notif_type()) { qWarning("notif type mismatch %d/%d msg = %s", notifType, notif->notif_type(), notification->DebugString().c_str()); return; } switch (notifType) { case OstProto::portConfigChanged: { if (!notif->port_id_list().port_id_size()) { qWarning("notif(portConfigChanged) has an empty port_id_list"); return; } for(int i=0; i < notif->port_id_list().port_id_size(); i++) { logInfo(id(), notif->port_id_list().port_id(i).id(), QString("Port configuration changed notification")); } OstProto::PortIdList *portIdList = new OstProto::PortIdList; OstProto::PortConfigList *portConfigList = new OstProto::PortConfigList; PbRpcController *controller = new PbRpcController(portIdList, portConfigList); portIdList->CopyFrom(notif->port_id_list()); serviceStub->getPortConfig(controller, portIdList, portConfigList, NewCallback(this, &PortGroup::processUpdatedPortConfig, controller)); break; } default: break; } } void PortGroup::when_portListChanged(quint32 /*portGroupId*/) { if (state() == QAbstractSocket::ConnectedState && numPorts() <= 0) { logError(id(), QString("No ports in portlist")); QMessageBox msgBox; msgBox.setIcon(QMessageBox::Warning); msgBox.setTextFormat(Qt::RichText); msgBox.setStyleSheet("messagebox-text-interaction-flags: 5"); QString msg = tr("

The portgroup %1:%2 does not contain any ports!

" "

Packet Transmit/Capture requires special privileges. " "Please ensure that you are running 'drone' - the agent " "component of Ostinato with required privileges.

") .arg(serverName()) .arg(int(serverPort())); msgBox.setText(msg); msgBox.setInformativeText(tr("See the Ostinato FAQ " "for instructions to fix this problem").arg(jumpUrl("noports"))); msgBox.exec(); } } void PortGroup::processPortIdList(PbRpcController *controller) { OstProto::PortIdList *portIdList = static_cast(controller->response()); Q_ASSERT(portIdList != NULL); qDebug("got a portlist ..."); if (controller->Failed()) { qDebug("%s: rpc failed(%s)", __FUNCTION__, qPrintable(controller->ErrorString())); logError(id(), controller->ErrorString()); goto _error_exit; } emit portListAboutToBeChanged(mPortGroupId); for(int i = 0; i < portIdList->port_id_size(); i++) { Port *p; p = new Port(portIdList->port_id(i).id(), mPortGroupId); connect(p, SIGNAL(portDataChanged(int, int)), this, SIGNAL(portGroupDataChanged(int, int))); connect(p, SIGNAL(localConfigChanged(int, int, bool)), this, SIGNAL(portGroupDataChanged(int, int))); qDebug("before port append\n"); mPorts.append(p); atConnectPortConfig_.append(NULL); // will be filled later } emit portListChanged(mPortGroupId); portIdList_->CopyFrom(*portIdList); // Request PortConfigList { qDebug("requesting port config list ..."); OstProto::PortIdList *portIdList2 = new OstProto::PortIdList(); OstProto::PortConfigList *portConfigList = new OstProto::PortConfigList(); PbRpcController *controller2 = new PbRpcController(portIdList2, portConfigList); portIdList2->CopyFrom(*portIdList); serviceStub->getPortConfig(controller, portIdList2, portConfigList, NewCallback(this, &PortGroup::processPortConfigList, controller2)); goto _exit; } _error_exit: _exit: delete controller; } void PortGroup::processPortConfigList(PbRpcController *controller) { OstProto::PortConfigList *portConfigList = static_cast(controller->response()); qDebug("In %s", __FUNCTION__); if (controller->Failed()) { qDebug("%s: rpc failed(%s)", __FUNCTION__, qPrintable(controller->ErrorString())); logError(id(), controller->ErrorString()); goto _error_exit; } //emit portListAboutToBeChanged(mPortGroupId); for(int i = 0; i < portConfigList->port_size(); i++) { uint id; id = portConfigList->port(i).port_id().id(); // FIXME: don't mix port id & index into mPorts[] mPorts[id]->updatePortConfig(portConfigList->mutable_port(i)); } // FIXME: Ideally we should emit portGroupDataChanged since only // port data is changing; but writing the corresponding slot in // PortStatsModel for that signal turned out to be very bug prone // causing assert failures when portgroups are added/deleted or // connected/disconnected in different orders // TODO: Revisit this when we refactor the domain-objects/model/view // design emit portListChanged(mPortGroupId); if (numPorts() > 0) { // XXX: The open session code (atConnectConfig_ related) assumes // the following two RPCs are invoked in the below order // Any change here without coressponding change in that code // will break stuff getDeviceGroupIdList(); getStreamIdList(); } // Now that we have the port details, let's identify which ports // need to be re-configured based on atConnectConfig_ if (atConnectConfig_ && numPorts() > 0) { QString myself = appSettings->value(kUserKey, kUserDefaultValue) .toString(); for (int i = 0; i < atConnectConfig_->ports_size(); i++) { const OstProto::PortContent *pc = &atConnectConfig_->ports(i); for (int j = 0; j < mPorts.size(); j++) { Port *port = mPorts[j]; if (port->name() == pc->port_config().name().c_str()) { if (!port->userName().isEmpty() // rsvd? && port->userName() != myself) // by someone else? { logWarn(id(), j, QString("Port is reserved by %1. " "Skipping reconfiguration") .arg(port->userName())); QString warning = QString("%1 - %2: %3 is reserved by %4.\n\n" "Port will not be reconfigured.") .arg(serverFullName()) .arg(j) .arg(port->userAlias()) .arg(port->userName()); qWarning("%s", qPrintable(warning)); QMessageBox::warning(NULL, tr("Open Session"), warning); continue; } atConnectPortConfig_[j] = pc; qDebug("port %d will be reconfigured", j); break; } } } } _error_exit: delete controller; } void PortGroup::when_configApply(int portIndex) { OstProto::StreamIdList *streamIdList; OstProto::StreamConfigList *streamConfigList; OstProto::BuildConfig *buildConfig; OstProto::Ack *ack; PbRpcController *controller; Q_ASSERT(portIndex < mPorts.size()); if (state() != QAbstractSocket::ConnectedState) return; QApplication::setOverrideCursor(QCursor(Qt::WaitCursor)); mainWindow->setDisabled(true); applyTimer_.start(); // // Update/Sync DeviceGroups // OstProto::DeviceGroupIdList *deviceGroupIdList; OstProto::DeviceGroupConfigList *deviceGroupConfigList; bool refreshReqd = false; qDebug("applying 'deleted deviceGroups' ..."); deviceGroupIdList = new OstProto::DeviceGroupIdList; deviceGroupIdList->mutable_port_id()->set_id(mPorts[portIndex]->id()); mPorts[portIndex]->getDeletedDeviceGroupsSinceLastSync(*deviceGroupIdList); if (deviceGroupIdList->device_group_id_size()) { logInfo(id(), mPorts[portIndex]->id(), QString("Deleting old DeviceGroups")); ack = new OstProto::Ack; controller = new PbRpcController(deviceGroupIdList, ack); serviceStub->deleteDeviceGroup(controller, deviceGroupIdList, ack, NewCallback(this, &PortGroup::processDeleteDeviceGroupAck, controller)); refreshReqd = true; } else delete deviceGroupIdList; qDebug("applying 'new deviceGroups' ..."); deviceGroupIdList = new OstProto::DeviceGroupIdList; deviceGroupIdList->mutable_port_id()->set_id(mPorts[portIndex]->id()); mPorts[portIndex]->getNewDeviceGroupsSinceLastSync(*deviceGroupIdList); if (deviceGroupIdList->device_group_id_size()) { logInfo(id(), mPorts[portIndex]->id(), QString("Creating new DeviceGroups")); ack = new OstProto::Ack; controller = new PbRpcController(deviceGroupIdList, ack); serviceStub->addDeviceGroup(controller, deviceGroupIdList, ack, NewCallback(this, &PortGroup::processAddDeviceGroupAck, controller)); refreshReqd = true; } else delete deviceGroupIdList; qDebug("applying 'modified deviceGroups' ..."); deviceGroupConfigList = new OstProto::DeviceGroupConfigList; deviceGroupConfigList->mutable_port_id()->set_id(mPorts[portIndex]->id()); mPorts[portIndex]->getModifiedDeviceGroupsSinceLastSync( *deviceGroupConfigList); if (deviceGroupConfigList->device_group_size()) { logInfo(id(), mPorts[portIndex]->id(), QString("Modifying changed DeviceGroups")); ack = new OstProto::Ack; controller = new PbRpcController(deviceGroupConfigList, ack); serviceStub->modifyDeviceGroup(controller, deviceGroupConfigList, ack, NewCallback(this, &PortGroup::processModifyDeviceGroupAck, portIndex, controller)); refreshReqd = true; } else delete deviceGroupConfigList; if (refreshReqd) getDeviceInfo(portIndex); // // Update/Sync Streams // qDebug("applying 'deleted streams' ..."); streamIdList = new OstProto::StreamIdList; streamIdList->mutable_port_id()->set_id(mPorts[portIndex]->id()); mPorts[portIndex]->getDeletedStreamsSinceLastSync(*streamIdList); if (streamIdList->stream_id_size()) { logInfo(id(), mPorts[portIndex]->id(), QString("Deleting old Streams")); ack = new OstProto::Ack; controller = new PbRpcController(streamIdList, ack); serviceStub->deleteStream(controller, streamIdList, ack, NewCallback(this, &PortGroup::processDeleteStreamAck, controller)); } else delete streamIdList; qDebug("applying 'new streams' ..."); streamIdList = new OstProto::StreamIdList; streamIdList->mutable_port_id()->set_id(mPorts[portIndex]->id()); mPorts[portIndex]->getNewStreamsSinceLastSync(*streamIdList); if (streamIdList->stream_id_size()) { logInfo(id(), mPorts[portIndex]->id(), QString("Creating new Streams")); ack = new OstProto::Ack; controller = new PbRpcController(streamIdList, ack); serviceStub->addStream(controller, streamIdList, ack, NewCallback(this, &PortGroup::processAddStreamAck, controller)); } else delete streamIdList; qDebug("applying 'modified streams' ..."); streamConfigList = new OstProto::StreamConfigList; streamConfigList->mutable_port_id()->set_id(mPorts[portIndex]->id()); mPorts[portIndex]->getModifiedStreamsSinceLastSync(*streamConfigList); if (streamConfigList->stream_size()) { logInfo(id(), mPorts[portIndex]->id(), QString("Modifying changed Streams")); ack = new OstProto::Ack; controller = new PbRpcController(streamConfigList, ack); serviceStub->modifyStream(controller, streamConfigList, ack, NewCallback(this, &PortGroup::processModifyStreamAck, portIndex, controller)); } else delete streamConfigList; qDebug("resolve neighbors before building ..."); logInfo(id(), mPorts[portIndex]->id(), QString("Resolving device neighbors")); OstProto::PortIdList *portIdList = new OstProto::PortIdList; OstProto::PortId *portId = portIdList->add_port_id(); portId->set_id(mPorts[portIndex]->id()); ack = new OstProto::Ack; controller = new PbRpcController(portIdList, ack); serviceStub->resolveDeviceNeighbors(controller, portIdList, ack, NewCallback(this, &PortGroup::processResolveDeviceNeighborsAck, controller)); qDebug("finish apply by building ..."); logInfo(id(), mPorts[portIndex]->id(), QString("Re-building packets")); buildConfig = new OstProto::BuildConfig; ack = new OstProto::Ack; controller = new PbRpcController(buildConfig, ack); buildConfig->mutable_port_id()->set_id(mPorts[portIndex]->id()); serviceStub->build(controller, buildConfig, ack, NewCallback(this, &PortGroup::processApplyBuildAck, portIndex, controller)); } void PortGroup::processAddDeviceGroupAck(PbRpcController *controller) { qDebug("In %s", __FUNCTION__); OstProto::DeviceGroupIdList *dgidList = static_cast(controller->request()); OstProto::Ack *ack = static_cast(controller->response()); if (controller->Failed()) { qDebug("%s: rpc failed(%s)", __FUNCTION__, qPrintable(controller->ErrorString())); logError(id(), dgidList->port_id().id(), controller->ErrorString()); goto _error_exit; } if (ack->status()) logError(id(), dgidList->port_id().id(), QString::fromStdString(ack->notes())); _error_exit: delete controller; } void PortGroup::processDeleteDeviceGroupAck(PbRpcController *controller) { qDebug("In %s", __FUNCTION__); OstProto::DeviceGroupIdList *dgidList = static_cast(controller->request()); OstProto::Ack *ack = static_cast(controller->response()); if (controller->Failed()) { qDebug("%s: rpc failed(%s)", __FUNCTION__, qPrintable(controller->ErrorString())); logError(id(), dgidList->port_id().id(), controller->ErrorString()); goto _error_exit; } if (ack->status()) logError(id(), dgidList->port_id().id(), QString::fromStdString(ack->notes())); _error_exit: delete controller; } void PortGroup::processModifyDeviceGroupAck(int /*portIndex*/, PbRpcController *controller) { qDebug("In %s", __FUNCTION__); OstProto::DeviceGroupIdList *dgidList = static_cast(controller->request()); OstProto::Ack *ack = static_cast(controller->response()); if (controller->Failed()) { qDebug("%s: rpc failed(%s)", __FUNCTION__, qPrintable(controller->ErrorString())); logError(id(), dgidList->port_id().id(), controller->ErrorString()); goto _error_exit; } if (ack->status()) logError(id(), dgidList->port_id().id(), QString::fromStdString(ack->notes())); _error_exit: delete controller; } void PortGroup::processAddStreamAck(PbRpcController *controller) { qDebug("In %s", __FUNCTION__); OstProto::StreamIdList *streamIdList = static_cast( controller->request()); OstProto::Ack *ack = static_cast(controller->response()); if (controller->Failed()) { qDebug("%s: rpc failed(%s)", __FUNCTION__, qPrintable(controller->ErrorString())); logError(id(), streamIdList->port_id().id(), controller->ErrorString()); goto _error_exit; } if (ack->status()) logError(id(), streamIdList->port_id().id(), QString::fromStdString(ack->notes())); _error_exit: delete controller; } void PortGroup::processDeleteStreamAck(PbRpcController *controller) { qDebug("In %s", __FUNCTION__); OstProto::StreamIdList *streamIdList = static_cast( controller->request()); OstProto::Ack *ack = static_cast(controller->response()); if (controller->Failed()) { qDebug("%s: rpc failed(%s)", __FUNCTION__, qPrintable(controller->ErrorString())); logError(id(), streamIdList->port_id().id(), controller->ErrorString()); goto _error_exit; } if (ack->status()) logError(id(), streamIdList->port_id().id(), QString::fromStdString(ack->notes())); _error_exit: delete controller; } void PortGroup::processModifyStreamAck(int portIndex, PbRpcController *controller) { qDebug("In %s", __FUNCTION__); OstProto::Ack *ack = static_cast(controller->response()); if (controller->Failed()) { qDebug("%s: rpc failed(%s)", __FUNCTION__, qPrintable(controller->ErrorString())); logError(id(), mPorts[portIndex]->id(), controller->ErrorString()); goto _error_exit; } if (ack->status()) logError(id(), mPorts[portIndex]->id(), QString::fromStdString(ack->notes())); _error_exit: delete controller; } void PortGroup::processApplyBuildAck(int portIndex, PbRpcController *controller) { qDebug("In %s", __FUNCTION__); OstProto::Ack *ack = static_cast(controller->response()); qDebug("apply completed"); logInfo(id(), mPorts[portIndex]->id(), QString("All port changes applied - in %1s") .arg(applyTimer_.elapsed()/1e3)); applyTimer_.invalidate(); if (controller->Failed()) { qDebug("%s: rpc failed(%s)", __FUNCTION__, qPrintable(controller->ErrorString())); logError(id(), mPorts[portIndex]->id(), controller->ErrorString()); goto _error_exit; } if (ack->status()) logError(id(), mPorts[portIndex]->id(), QString::fromStdString(ack->notes())); _error_exit: mPorts[portIndex]->when_syncComplete(); emit applyFinished(); mainWindow->setEnabled(true); QApplication::restoreOverrideCursor(); delete controller; } void PortGroup::getDeviceInfo(int portIndex) { OstProto::PortId *portId; OstProto::PortDeviceList *deviceList; OstProto::PortNeighborList *neighList; PbRpcController *controller; Q_ASSERT(portIndex < mPorts.size()); if (state() != QAbstractSocket::ConnectedState) return; portId = new OstProto::PortId; portId->set_id(mPorts[portIndex]->id()); deviceList = new OstProto::PortDeviceList; controller = new PbRpcController(portId, deviceList); serviceStub->getDeviceList(controller, portId, deviceList, NewCallback(this, &PortGroup::processDeviceList, portIndex, controller)); portId = new OstProto::PortId; portId->set_id(mPorts[portIndex]->id()); neighList = new OstProto::PortNeighborList; controller = new PbRpcController(portId, neighList); serviceStub->getDeviceNeighbors(controller, portId, neighList, NewCallback(this, &PortGroup::processDeviceNeighbors, portIndex, controller)); } void PortGroup::processDeviceList(int portIndex, PbRpcController *controller) { OstProto::PortDeviceList *deviceList = static_cast(controller->response()); qDebug("In %s (portIndex = %d)", __FUNCTION__, portIndex); if (controller->Failed()) { qDebug("%s: rpc failed(%s)", __FUNCTION__, qPrintable(controller->ErrorString())); logError(id(), mPorts[portIndex]->id(), controller->ErrorString()); goto _exit; } Q_ASSERT(portIndex < numPorts()); if (deviceList->port_id().id() != mPorts[portIndex]->id()) { qDebug("Invalid portId %d (expected %d) received for portIndex %d", deviceList->port_id().id(), mPorts[portIndex]->id(), portIndex); goto _exit; } mPorts[portIndex]->clearDeviceList(); for(int i = 0; i < deviceList->ExtensionSize(OstEmul::device); i++) { mPorts[portIndex]->insertDevice( deviceList->GetExtension(OstEmul::device, i)); } _exit: delete controller; } void PortGroup::processDeviceNeighbors( int portIndex, PbRpcController *controller) { OstProto::PortNeighborList *neighList = static_cast(controller->response()); qDebug("In %s (portIndex = %d)", __FUNCTION__, portIndex); if (controller->Failed()) { qDebug("%s: rpc failed(%s)", __FUNCTION__, qPrintable(controller->ErrorString())); logError(id(), mPorts[portIndex]->id(),controller->ErrorString()); goto _exit; } Q_ASSERT(portIndex < numPorts()); if (neighList->port_id().id() != mPorts[portIndex]->id()) { qDebug("Invalid portId %d (expected %d) received for portIndex %d", neighList->port_id().id(), mPorts[portIndex]->id(), portIndex); goto _exit; } mPorts[portIndex]->clearDeviceNeighbors(); for(int i=0; i < neighList->ExtensionSize(OstEmul::device_neighbor); i++) { mPorts[portIndex]->insertDeviceNeighbors( neighList->GetExtension(OstEmul::device_neighbor, i)); } mPorts[portIndex]->deviceInfoRefreshed(); _exit: delete controller; } void PortGroup::modifyPort(int portIndex, OstProto::Port portConfig) { OstProto::PortConfigList *portConfigList = new OstProto::PortConfigList; OstProto::Ack *ack = new OstProto::Ack; qDebug("%s: portIndex = %d", __FUNCTION__, portIndex); Q_ASSERT(portIndex < mPorts.size()); QApplication::setOverrideCursor(QCursor(Qt::WaitCursor)); mainWindow->setDisabled(true); logInfo(id(), mPorts[portIndex]->id(), QString("Modifying port configuration")); OstProto::Port *port = portConfigList->add_port(); port->CopyFrom(portConfig); port->mutable_port_id()->set_id(mPorts[portIndex]->id()); PbRpcController *controller = new PbRpcController(portConfigList, ack); serviceStub->modifyPort(controller, portConfigList, ack, NewCallback(this, &PortGroup::processModifyPortAck, controller)); logInfo(id(), mPorts[portIndex]->id(), QString("Re-building packets")); OstProto::BuildConfig *buildConfig = new OstProto::BuildConfig; ack = new OstProto::Ack; controller = new PbRpcController(buildConfig, ack); buildConfig->mutable_port_id()->set_id(mPorts[portIndex]->id()); serviceStub->build(controller, buildConfig, ack, NewCallback(this, &PortGroup::processModifyPortBuildAck, true, controller)); } void PortGroup::processModifyPortAck(PbRpcController *controller) { qDebug("In %s", __FUNCTION__); if (controller->Failed()) { qDebug("%s: rpc failed(%s)", __FUNCTION__, qPrintable(controller->ErrorString())); logError(id(), controller->ErrorString()); } OstProto::Ack *ack = static_cast(controller->response()); if (ack->status()) logError(id(), QString::fromStdString(ack->notes())); delete controller; } void PortGroup::processModifyPortBuildAck(bool restoreUi, PbRpcController *controller) { qDebug("In %s", __FUNCTION__); if (controller->Failed()) { qDebug("%s: rpc failed(%s)", __FUNCTION__, qPrintable(controller->ErrorString())); logError(id(), controller->ErrorString()); } OstProto::Ack *ack = static_cast(controller->response()); if (ack->status()) logError(id(), QString::fromStdString(ack->notes())); if (restoreUi) { mainWindow->setEnabled(true); QApplication::restoreOverrideCursor(); } delete controller; } void PortGroup::processUpdatedPortConfig(PbRpcController *controller) { OstProto::PortConfigList *portConfigList = static_cast(controller->response()); qDebug("In %s", __FUNCTION__); if (controller->Failed()) { qDebug("%s: rpc failed(%s)", __FUNCTION__, qPrintable(controller->ErrorString())); logError(id(), controller->ErrorString()); goto _exit; } for(int i = 0; i < portConfigList->port_size(); i++) { uint id; id = portConfigList->port(i).port_id().id(); // FIXME: don't mix port id & index into mPorts[] mPorts[id]->updatePortConfig(portConfigList->mutable_port(i)); emit portGroupDataChanged(mPortGroupId, id); } _exit: delete controller; } void PortGroup::getStreamIdList() { for (int portIndex = 0; portIndex < numPorts(); portIndex++) { OstProto::PortId *portId = new OstProto::PortId; OstProto::StreamIdList *streamIdList = new OstProto::StreamIdList; PbRpcController *controller = new PbRpcController(portId, streamIdList); portId->set_id(mPorts[portIndex]->id()); serviceStub->getStreamIdList(controller, portId, streamIdList, NewCallback(this, &PortGroup::processStreamIdList, portIndex, controller)); } } void PortGroup::processStreamIdList(int portIndex, PbRpcController *controller) { OstProto::StreamIdList *streamIdList = static_cast(controller->response()); const OstProto::PortContent *newPortContent = atConnectPortConfig_.at(portIndex); qDebug("In %s (portIndex = %d)", __FUNCTION__, portIndex); if (controller->Failed()) { qDebug("%s: rpc failed(%s)", __FUNCTION__, qPrintable(controller->ErrorString())); logError(id(), mPorts[portIndex]->id(), controller->ErrorString()); goto _exit; } Q_ASSERT(portIndex < numPorts()); if (streamIdList->port_id().id() != mPorts[portIndex]->id()) { qDebug("Invalid portId %d (expected %d) received for portIndex %d", streamIdList->port_id().id(), mPorts[portIndex]->id(), portIndex); goto _exit; } if (newPortContent) { // This port needs to configured with new content - to do this // we'll insert the following RPC sequence at this point and once // this sequence is over, return to the regular RPC sequence by // re-requesting getStreamId() // * delete (existing) deviceGroups // (already done by processDeviceIdList) // * delete (existing) streams // * modify port // * add (new) deviceGroup ids // * modify (new) deviceGroups // * add (new) stream ids // * modify (new) streams // * resolve neighbors // * build packets // XXX: This assumes getDeviceGroupIdList() was invoked before // getStreamIdList() - if the order changes this code will break! // XXX: See resolve/build notes below // XXX: same name as input param, but shouldn't cause any problem PbRpcController *controller; quint32 portId = mPorts[portIndex]->id(); QString myself = appSettings->value(kUserKey, kUserDefaultValue) .toString(); // delete all existing streams if (streamIdList->stream_id_size()) { OstProto::StreamIdList *streamIdList2 = new OstProto::StreamIdList; streamIdList2->CopyFrom(*streamIdList); OstProto::Ack *ack = new OstProto::Ack; controller = new PbRpcController(streamIdList2, ack); serviceStub->deleteStream(controller, streamIdList2, ack, NewCallback(this, &PortGroup::processDeleteStreamAck, controller)); } OstProto::Port portCfg = newPortContent->port_config(); if (mPorts[portIndex]->modifiablePortConfig(portCfg)) { OstProto::PortConfigList *portConfigList = new OstProto::PortConfigList; OstProto::Port *port = portConfigList->add_port(); port->CopyFrom(portCfg); if (port->has_user_name()) port->set_user_name(qPrintable(myself)); // overwrite OstProto::Ack *ack = new OstProto::Ack; controller = new PbRpcController(portConfigList, ack); serviceStub->modifyPort(controller, portConfigList, ack, NewCallback(this, &PortGroup::processModifyPortAck, controller)); } // add/modify deviceGroups bool resolve = false; if (newPortContent->device_groups_size()) { OstProto::DeviceGroupIdList *deviceGroupIdList = new OstProto::DeviceGroupIdList; OstProto::DeviceGroupConfigList *deviceGroupConfigList = new OstProto::DeviceGroupConfigList; deviceGroupIdList->mutable_port_id()->set_id(portId); deviceGroupConfigList->mutable_port_id()->set_id(portId); for (int i = 0; i < newPortContent->device_groups_size(); i++) { const OstProto::DeviceGroup &dg = newPortContent->device_groups(i); deviceGroupIdList->add_device_group_id()->set_id( dg.device_group_id().id()); deviceGroupConfigList->add_device_group()->CopyFrom(dg); } OstProto::Ack *ack = new OstProto::Ack; controller = new PbRpcController(deviceGroupIdList, ack); serviceStub->addDeviceGroup(controller, deviceGroupIdList, ack, NewCallback(this, &PortGroup::processAddDeviceGroupAck, controller)); ack = new OstProto::Ack; controller = new PbRpcController(deviceGroupConfigList, ack); serviceStub->modifyDeviceGroup(controller, deviceGroupConfigList, ack, NewCallback(this, &PortGroup::processModifyDeviceGroupAck, portIndex, controller)); resolve = true; } // add/modify streams if (newPortContent->streams_size()) { OstProto::StreamIdList *streamIdList = new OstProto::StreamIdList; OstProto::StreamConfigList *streamConfigList = new OstProto::StreamConfigList; streamIdList->mutable_port_id()->set_id(portId); streamConfigList->mutable_port_id()->set_id(portId); for (int i = 0; i < newPortContent->streams_size(); i++) { const OstProto::Stream &s = newPortContent->streams(i); streamIdList->add_stream_id()->set_id(s.stream_id().id()); streamConfigList->add_stream()->CopyFrom(s); } OstProto::Ack *ack = new OstProto::Ack; controller = new PbRpcController(streamIdList, ack); serviceStub->addStream(controller, streamIdList, ack, NewCallback(this, &PortGroup::processAddStreamAck, controller)); ack = new OstProto::Ack; controller = new PbRpcController(streamConfigList, ack); serviceStub->modifyStream(controller, streamConfigList, ack, NewCallback(this, &PortGroup::processModifyStreamAck, portIndex, controller)); resolve = true; } // XXX: Ideally resolve and build should be called after **all** // ports and portgroups are configured. As of now, any resolve // replied to by ports/portgroups configured later in the open // session sequence will fail. // However, to do that, we may need to rethink the open session // implementation - so going with this for now if (resolve) { OstProto::PortIdList *portIdList = new OstProto::PortIdList; portIdList->add_port_id()->set_id(portId); OstProto::Ack *ack = new OstProto::Ack; controller = new PbRpcController(portIdList, ack); serviceStub->resolveDeviceNeighbors(controller, portIdList, ack, NewCallback(this, &PortGroup::processResolveDeviceNeighborsAck, controller)); resolve = false; } // build packets using the new config OstProto::BuildConfig *buildConfig = new OstProto::BuildConfig; OstProto::Ack *ack = new OstProto::Ack; controller = new PbRpcController(buildConfig, ack); buildConfig->mutable_port_id()->set_id(mPorts[portIndex]->id()); serviceStub->build(controller, buildConfig, ack, NewCallback(this, &PortGroup::processModifyPortBuildAck, false, controller)); // delete newPortConfig atConnectPortConfig_[portIndex] = NULL; // return to normal sequence re-starting from // getDeviceGroupIdList() and getStreamIdList() OstProto::PortId *portId2 = new OstProto::PortId; portId2->set_id(portId); OstProto::DeviceGroupIdList *devGrpIdList = new OstProto::DeviceGroupIdList; controller = new PbRpcController(portId2, devGrpIdList); serviceStub->getDeviceGroupIdList(controller, portId2, devGrpIdList, NewCallback(this, &PortGroup::processDeviceGroupIdList, portIndex, controller)); portId2 = new OstProto::PortId; portId2->set_id(portId); OstProto::StreamIdList *streamIdList = new OstProto::StreamIdList; controller = new PbRpcController(portId2, streamIdList); serviceStub->getStreamIdList(controller, portId2, streamIdList, NewCallback(this, &PortGroup::processStreamIdList, portIndex, controller)); } else { for(int i = 0; i < streamIdList->stream_id_size(); i++) { uint streamId; streamId = streamIdList->stream_id(i).id(); mPorts[portIndex]->insertStream(streamId); } mPorts[portIndex]->when_syncComplete(); getStreamConfigList(portIndex); } _exit: delete controller; } void PortGroup::getStreamConfigList(int portIndex) { if (mPorts[portIndex]->numStreams() == 0) return; qDebug("requesting stream config list (port %d)...", portIndex); OstProto::StreamIdList *streamIdList = new OstProto::StreamIdList; OstProto::StreamConfigList *streamConfigList = new OstProto::StreamConfigList; PbRpcController *controller = new PbRpcController( streamIdList, streamConfigList); streamIdList->mutable_port_id()->set_id(mPorts[portIndex]->id()); for (int j = 0; j < mPorts[portIndex]->numStreams(); j++) { OstProto::StreamId *s = streamIdList->add_stream_id(); s->set_id(mPorts[portIndex]->streamByIndex(j)->id()); } serviceStub->getStreamConfig(controller, streamIdList, streamConfigList, NewCallback(this, &PortGroup::processStreamConfigList, portIndex, controller)); } void PortGroup::processStreamConfigList(int portIndex, PbRpcController *controller) { OstProto::StreamConfigList *streamConfigList = static_cast(controller->response()); qDebug("In %s", __PRETTY_FUNCTION__); Q_ASSERT(portIndex < numPorts()); if (controller->Failed()) { qDebug("%s: rpc failed(%s)", __FUNCTION__, qPrintable(controller->ErrorString())); logError(id(), mPorts[portIndex]->id(), controller->ErrorString()); goto _exit; } Q_ASSERT(portIndex < numPorts()); if (streamConfigList->port_id().id() != mPorts[portIndex]->id()) { qDebug("Invalid portId %d (expected %d) received for portIndex %d", streamConfigList->port_id().id(), mPorts[portIndex]->id(), portIndex); goto _exit; } for(int i = 0; i < streamConfigList->stream_size(); i++) { uint streamId; streamId = streamConfigList->stream(i).stream_id().id(); mPorts[portIndex]->updateStream(streamId, streamConfigList->mutable_stream(i)); } #if 0 // FIXME: incorrect check - will never be true if last port does not have any streams configured // Are we done for all ports? if (portIndex >= (numPorts()-1)) { // FIXME(HI): some way to reset streammodel } #endif _exit: delete controller; } void PortGroup::getDeviceGroupIdList() { using OstProto::PortId; using OstProto::DeviceGroupIdList; for (int portIndex = 0; portIndex < numPorts(); portIndex++) { PortId *portId = new PortId; DeviceGroupIdList *devGrpIdList = new DeviceGroupIdList; PbRpcController *controller = new PbRpcController(portId, devGrpIdList); portId->set_id(mPorts[portIndex]->id()); serviceStub->getDeviceGroupIdList(controller, portId, devGrpIdList, NewCallback(this, &PortGroup::processDeviceGroupIdList, portIndex, controller)); } } void PortGroup::processDeviceGroupIdList( int portIndex, PbRpcController *controller) { using OstProto::DeviceGroupIdList; DeviceGroupIdList *devGrpIdList = static_cast( controller->response()); const OstProto::PortContent *newPortContent = atConnectPortConfig_.at( portIndex); qDebug("In %s (portIndex = %d)", __FUNCTION__, portIndex); if (controller->Failed()) { qDebug("%s: rpc failed(%s)", __FUNCTION__, qPrintable(controller->ErrorString())); logError(id(), mPorts[portIndex]->id(), controller->ErrorString()); goto _exit; } Q_ASSERT(portIndex < numPorts()); if (devGrpIdList->port_id().id() != mPorts[portIndex]->id()) { qDebug("Invalid portId %d (expected %d) received for portIndex %d", devGrpIdList->port_id().id(), mPorts[portIndex]->id(), portIndex); goto _exit; } if (newPortContent) { // We delete all existing deviceGroups // Remaining stuff is done in processStreamIdList() - see notes there if (devGrpIdList->device_group_id_size()) { OstProto::DeviceGroupIdList *devGrpIdList2 = new OstProto::DeviceGroupIdList; devGrpIdList2->CopyFrom(*devGrpIdList); OstProto::Ack *ack = new OstProto::Ack; PbRpcController *controller = new PbRpcController(devGrpIdList2, ack); serviceStub->deleteDeviceGroup(controller, devGrpIdList2, ack, NewCallback(this, &PortGroup::processDeleteDeviceGroupAck, controller)); } } else { for(int i = 0; i < devGrpIdList->device_group_id_size(); i++) { uint devGrpId; devGrpId = devGrpIdList->device_group_id(i).id(); mPorts[portIndex]->insertDeviceGroup(devGrpId); } getDeviceGroupConfigList(portIndex); } _exit: delete controller; } void PortGroup::getDeviceGroupConfigList(int portIndex) { using OstProto::DeviceGroupId; using OstProto::DeviceGroupIdList; using OstProto::DeviceGroupConfigList; if (mPorts[portIndex]->numDeviceGroups() == 0) { // No devGrps but we may still have devices (hostDev) getDeviceInfo(portIndex); return; } qDebug("requesting device group config list (port %d) ...", portIndex); DeviceGroupIdList *devGrpIdList = new DeviceGroupIdList; DeviceGroupConfigList *devGrpCfgList = new DeviceGroupConfigList; PbRpcController *controller = new PbRpcController( devGrpIdList, devGrpCfgList); devGrpIdList->mutable_port_id()->set_id(mPorts[portIndex]->id()); for (int j = 0; j < mPorts[portIndex]->numDeviceGroups(); j++) { DeviceGroupId *dgid = devGrpIdList->add_device_group_id(); dgid->set_id(mPorts[portIndex]->deviceGroupByIndex(j) ->device_group_id().id()); } serviceStub->getDeviceGroupConfig(controller, devGrpIdList, devGrpCfgList, NewCallback(this, &PortGroup::processDeviceGroupConfigList, portIndex, controller)); } void PortGroup::processDeviceGroupConfigList(int portIndex, PbRpcController *controller) { using OstProto::DeviceGroupConfigList; DeviceGroupConfigList *devGrpCfgList = static_cast(controller->response()); qDebug("In %s", __PRETTY_FUNCTION__); Q_ASSERT(portIndex < numPorts()); if (controller->Failed()) { qDebug("%s: rpc failed(%s)", __FUNCTION__, qPrintable(controller->ErrorString())); logError(id(), mPorts[portIndex]->id(), controller->ErrorString()); goto _exit; } Q_ASSERT(portIndex < numPorts()); if (devGrpCfgList->port_id().id() != mPorts[portIndex]->id()) { qDebug("Invalid portId %d (expected %d) received for portIndex %d", devGrpCfgList->port_id().id(), mPorts[portIndex]->id(), portIndex); goto _exit; } for(int i = 0; i < devGrpCfgList->device_group_size(); i++) { uint dgid = devGrpCfgList->device_group(i).device_group_id().id(); mPorts[portIndex]->updateDeviceGroup(dgid, devGrpCfgList->mutable_device_group(i)); } getDeviceInfo(portIndex); #if 0 // FIXME: incorrect check - will never be true if last port does not have any deviceGroups configured // Are we done for all ports? if (portIndex >= (numPorts()-1)) { // FIXME: reset deviceGroupModel? } #endif _exit: delete controller; } void PortGroup::startTx(QList *portList) { qDebug("In %s", __FUNCTION__); if (state() != QAbstractSocket::ConnectedState) goto _exit; if (portList == NULL) goto _exit; { OstProto::PortIdList *portIdList = new OstProto::PortIdList; OstProto::Ack *ack = new OstProto::Ack; PbRpcController *controller = new PbRpcController(portIdList, ack); for (int i = 0; i < portList->size(); i++) { OstProto::PortId *portId = portIdList->add_port_id(); portId->set_id(portList->at(i)); } serviceStub->startTransmit(controller, portIdList, ack, NewCallback(this, &PortGroup::processStartTxAck, controller)); } _exit: return; } void PortGroup::processStartTxAck(PbRpcController *controller) { qDebug("In %s", __FUNCTION__); OstProto::Ack *ack = static_cast(controller->response()); if (controller->Failed()) { qDebug("%s: rpc failed(%s)", __FUNCTION__, qPrintable(controller->ErrorString())); logError(id(), controller->ErrorString()); goto _exit; } if (ack->status()) logError(id(), QString::fromStdString(ack->notes())); _exit: delete controller; } void PortGroup::stopTx(QList *portList) { qDebug("In %s", __FUNCTION__); if (state() != QAbstractSocket::ConnectedState) goto _exit; if ((portList == NULL) || (portList->size() == 0)) goto _exit; { OstProto::PortIdList *portIdList = new OstProto::PortIdList; OstProto::Ack *ack = new OstProto::Ack; PbRpcController *controller = new PbRpcController(portIdList, ack); for (int i = 0; i < portList->size(); i++) { OstProto::PortId *portId = portIdList->add_port_id(); portId->set_id(portList->at(i)); } serviceStub->stopTransmit(controller, portIdList, ack, NewCallback(this, &PortGroup::processStopTxAck, controller)); } _exit: return; } void PortGroup::processStopTxAck(PbRpcController *controller) { qDebug("In %s", __FUNCTION__); OstProto::Ack *ack = static_cast(controller->response()); if (controller->Failed()) { qDebug("%s: rpc failed(%s)", __FUNCTION__, qPrintable(controller->ErrorString())); logError(id(), controller->ErrorString()); goto _exit; } if (ack->status()) logError(id(), QString::fromStdString(ack->notes())); _exit: delete controller; } void PortGroup::startCapture(QList *portList) { qDebug("In %s", __FUNCTION__); if (state() != QAbstractSocket::ConnectedState) return; if ((portList == NULL) || (portList->size() == 0)) goto _exit; { OstProto::PortIdList *portIdList = new OstProto::PortIdList; OstProto::Ack *ack = new OstProto::Ack; PbRpcController *controller = new PbRpcController(portIdList, ack); for (int i = 0; i < portList->size(); i++) { OstProto::PortId *portId = portIdList->add_port_id(); portId->set_id(portList->at(i)); } serviceStub->startCapture(controller, portIdList, ack, NewCallback(this, &PortGroup::processStartCaptureAck, controller)); } _exit: return; } void PortGroup::processStartCaptureAck(PbRpcController *controller) { qDebug("In %s", __FUNCTION__); OstProto::Ack *ack = static_cast(controller->response()); if (controller->Failed()) { qDebug("%s: rpc failed(%s)", __FUNCTION__, qPrintable(controller->ErrorString())); logError(id(), controller->ErrorString()); goto _exit; } if (ack->status()) logError(id(), QString::fromStdString(ack->notes())); _exit: delete controller; } void PortGroup::stopCapture(QList *portList) { qDebug("In %s", __FUNCTION__); if (state() != QAbstractSocket::ConnectedState) return; if ((portList == NULL) || (portList->size() == 0)) goto _exit; { OstProto::PortIdList *portIdList = new OstProto::PortIdList; OstProto::Ack *ack = new OstProto::Ack; PbRpcController *controller = new PbRpcController(portIdList, ack); for (int i = 0; i < portList->size(); i++) { OstProto::PortId *portId = portIdList->add_port_id(); portId->set_id(portList->at(i)); } serviceStub->stopCapture(controller, portIdList, ack, NewCallback(this, &PortGroup::processStopCaptureAck, controller)); } _exit: return; } void PortGroup::processStopCaptureAck(PbRpcController *controller) { qDebug("In %s", __FUNCTION__); OstProto::Ack *ack = static_cast(controller->response()); if (controller->Failed()) { qDebug("%s: rpc failed(%s)", __FUNCTION__, qPrintable(controller->ErrorString())); logError(id(), controller->ErrorString()); goto _exit; } if (ack->status()) logError(id(), QString::fromStdString(ack->notes())); _exit: delete controller; } void PortGroup::viewCapture(QList *portList) { qDebug("In %s", __FUNCTION__); if (state() != QAbstractSocket::ConnectedState) goto _exit; if ((portList == NULL) || (portList->size() != 1)) goto _exit; for (int i = 0; i < portList->size(); i++) { OstProto::PortId *portId = new OstProto::PortId; OstProto::CaptureBuffer *buf = new OstProto::CaptureBuffer; PbRpcController *controller = new PbRpcController(portId, buf); QFile *capFile = mPorts[portList->at(i)]->getCaptureFile(); portId->set_id(portList->at(i)); capFile->open(QIODevice::ReadWrite|QIODevice::Truncate); qDebug("Temp CapFile = %s", qPrintable(capFile->fileName())); controller->setBinaryBlob(capFile); serviceStub->getCaptureBuffer(controller, portId, buf, NewCallback(this, &PortGroup::processViewCaptureAck, controller)); } _exit: return; } void PortGroup::processViewCaptureAck(PbRpcController *controller) { OstProto::PortId *portId = static_cast( controller->request()); QFile *capFile = static_cast(controller->binaryBlob()); QString viewer = appSettings->value(kWiresharkPathKey, kWiresharkPathDefaultValue).toString(); qDebug("In %s", __FUNCTION__); if (controller->Failed()) { qDebug("%s: rpc failed(%s)", __FUNCTION__, qPrintable(controller->ErrorString())); logError(id(), portId->id(), controller->ErrorString()); goto _exit; } capFile->flush(); capFile->close(); if (!QFile::exists(viewer)) { logError(QString("Wireshark does not exist at %1").arg(viewer)); QMessageBox::warning(NULL, "Can't find Wireshark", viewer + QString(" does not exist!\n\nPlease correct the path" " to Wireshark in the Preferences.")); goto _exit; } if (!QProcess::startDetached(viewer, QStringList() << capFile->fileName())) { qDebug("Failed starting Wireshark"); logError(QString("Failed to start %1").arg(viewer)); } _exit: delete controller; } void PortGroup::resolveDeviceNeighbors(QList *portList) { qDebug("In %s", __FUNCTION__); if (state() != QAbstractSocket::ConnectedState) return; if ((portList == NULL) || (portList->size() == 0)) goto _exit; { OstProto::PortIdList *portIdList = new OstProto::PortIdList; OstProto::Ack *ack = new OstProto::Ack; PbRpcController *controller = new PbRpcController(portIdList, ack); for (int i = 0; i < portList->size(); i++) { OstProto::PortId *portId = portIdList->add_port_id(); portId->set_id(portList->at(i)); } serviceStub->resolveDeviceNeighbors(controller, portIdList, ack, NewCallback(this, &PortGroup::processResolveDeviceNeighborsAck, controller)); } _exit: return; } void PortGroup::processResolveDeviceNeighborsAck(PbRpcController *controller) { qDebug("In %s", __FUNCTION__); OstProto::Ack *ack = static_cast(controller->response()); if (controller->Failed()) { qDebug("%s: rpc failed(%s)", __FUNCTION__, qPrintable(controller->ErrorString())); logError(id(), controller->ErrorString()); goto _exit; } if (ack->status()) logError(id(), QString::fromStdString(ack->notes())); _exit: delete controller; } void PortGroup::clearDeviceNeighbors(QList *portList) { qDebug("In %s", __FUNCTION__); if (state() != QAbstractSocket::ConnectedState) return; if ((portList == NULL) || (portList->size() == 0)) goto _exit; { OstProto::PortIdList *portIdList = new OstProto::PortIdList; OstProto::Ack *ack = new OstProto::Ack; PbRpcController *controller = new PbRpcController(portIdList, ack); for (int i = 0; i < portList->size(); i++) { OstProto::PortId *portId = portIdList->add_port_id(); portId->set_id(portList->at(i)); } serviceStub->clearDeviceNeighbors(controller, portIdList, ack, NewCallback(this, &PortGroup::processClearDeviceNeighborsAck, controller)); } _exit: return; } void PortGroup::processClearDeviceNeighborsAck(PbRpcController *controller) { qDebug("In %s", __FUNCTION__); OstProto::Ack *ack = static_cast(controller->response()); if (controller->Failed()) { qDebug("%s: rpc failed(%s)", __FUNCTION__, qPrintable(controller->ErrorString())); logError(id(), controller->ErrorString()); goto _exit; } if (ack->status()) logError(id(), QString::fromStdString(ack->notes())); _exit: delete controller; } void PortGroup::getPortStats() { //qDebug("In %s", __FUNCTION__); if (state() != QAbstractSocket::ConnectedState) goto _exit; if (numPorts() <= 0) goto _exit; if (isGetStatsPending_) goto _exit; statsController->Reset(); isGetStatsPending_ = true; serviceStub->getStats(statsController, static_cast(statsController->request()), static_cast(statsController->response()), NewCallback(this, &PortGroup::processPortStatsList)); _exit: return; } void PortGroup::processPortStatsList() { //qDebug("In %s", __FUNCTION__); if (statsController->Failed()) { qDebug("%s: rpc failed(%s)", __FUNCTION__, qPrintable(statsController->ErrorString())); logError(id(), statsController->ErrorString()); goto _error_exit; } for(int i = 0; i < portStatsList_->port_stats_size(); i++) { uint id = portStatsList_->port_stats(i).port_id().id(); // FIXME: don't mix port id & index into mPorts[] mPorts[id]->updateStats(portStatsList_->mutable_port_stats(i)); } emit statsChanged(mPortGroupId); _error_exit: isGetStatsPending_ = false; } void PortGroup::clearPortStats(QList *portList) { qDebug("In %s", __FUNCTION__); if (state() != QAbstractSocket::ConnectedState) goto _exit; { OstProto::PortIdList *portIdList = new OstProto::PortIdList; OstProto::Ack *ack = new OstProto::Ack; PbRpcController *controller = new PbRpcController(portIdList, ack); if (portList == NULL) portIdList->CopyFrom(*portIdList_); else { for (int i = 0; i < portList->size(); i++) { OstProto::PortId *portId = portIdList->add_port_id(); portId->set_id(portList->at(i)); } } serviceStub->clearStats(controller, portIdList, ack, NewCallback(this, &PortGroup::processClearPortStatsAck, controller)); } _exit: return; } void PortGroup::processClearPortStatsAck(PbRpcController *controller) { qDebug("In %s", __FUNCTION__); OstProto::Ack *ack = static_cast(controller->response()); // Refresh stats immediately after a stats clear/reset getPortStats(); if (controller->Failed()) { qDebug("%s: rpc failed(%s)", __FUNCTION__, qPrintable(controller->ErrorString())); logError(id(), controller->ErrorString()); goto _exit; } if (ack->status()) logError(id(), QString::fromStdString(ack->notes())); _exit: delete controller; } bool PortGroup::clearStreamStats(QList *portList) { qDebug("In %s", __FUNCTION__); if (state() != QAbstractSocket::ConnectedState) return false; OstProto::StreamGuidList *guidList = new OstProto::StreamGuidList; OstProto::Ack *ack = new OstProto::Ack; PbRpcController *controller = new PbRpcController(guidList, ack); if (portList == NULL) guidList->mutable_port_id_list()->CopyFrom(*portIdList_); else for (int i = 0; i < portList->size(); i++) guidList->mutable_port_id_list()->add_port_id() ->set_id(portList->at(i)); serviceStub->clearStreamStats(controller, guidList, ack, NewCallback(this, &PortGroup::processClearStreamStatsAck, controller)); return true; } void PortGroup::processClearStreamStatsAck(PbRpcController *controller) { qDebug("In %s", __FUNCTION__); OstProto::Ack *ack = static_cast(controller->response()); if (controller->Failed()) { qDebug("%s: rpc failed(%s)", __FUNCTION__, qPrintable(controller->ErrorString())); logError(id(), controller->ErrorString()); goto _exit; } if (ack->status()) logError(id(), QString::fromStdString(ack->notes())); _exit: delete controller; } bool PortGroup::getStreamStats(QList *portList) { qDebug("In %s", __FUNCTION__); if (state() != QAbstractSocket::ConnectedState) return false; OstProto::StreamGuidList *guidList = new OstProto::StreamGuidList; OstProto::StreamStatsList *statsList = new OstProto::StreamStatsList; PbRpcController *controller = new PbRpcController(guidList, statsList); if (portList == NULL) guidList->mutable_port_id_list()->CopyFrom(*portIdList_); else for (int i = 0; i < portList->size(); i++) { guidList->mutable_port_id_list()->add_port_id() ->set_id(portList->at(i)); if (mPorts.at(i)->isTransmitting()) logWarn(id(), i, "Port is still transmitting - stream stats may be unavailable or incomplete"); } serviceStub->getStreamStats(controller, guidList, statsList, NewCallback(this, &PortGroup::processStreamStatsList, controller)); return true; } void PortGroup::processStreamStatsList(PbRpcController *controller) { using OstProto::StreamStatsList; qDebug("In %s", __FUNCTION__); StreamStatsList *streamStatsList = static_cast(controller->response()); // XXX: It is required to emit the signal even if the returned // streamStatsList contains no records since the recipient // StreamStatsModel slot needs to disconnect this signal-slot // connection to prevent future stream stats for this portgroup // to be sent to it emit streamStatsReceived(mPortGroupId, streamStatsList); delete controller; } ostinato-1.3.0/client/portgroup.h000066400000000000000000000156641451413623100170570ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _PORT_GROUP_H #define _PORT_GROUP_H #include "port.h" #include #include #include #include "../common/protocol.pb.h" #include "pbrpcchannel.h" /* TODO HIGH MED LOW - Allow hostnames in addition to IP Address as "server address" */ #define DEFAULT_SERVER_PORT 7878 namespace OstProto { class PortContent; class PortGroupContent; class StreamStatsList; } class QFile; class QTimer; class PortGroup : public QObject { Q_OBJECT private: enum { kIncompatible, kCompatible, kUnknown } compat; static quint32 mPortGroupAllocId; quint32 mPortGroupId; QString mUserAlias; // user defined bool reconnect; int reconnectAfter; // time in milliseconds static const int kMinReconnectWaitTime = 2000; // ms static const int kMaxReconnectWaitTime = 60000; // ms QTimer *reconnectTimer; PbRpcChannel *rpcChannel; PbRpcController *statsController; bool isGetStatsPending_; QElapsedTimer applyTimer_; OstProto::OstService::Stub *serviceStub; OstProto::PortIdList *portIdList_; OstProto::PortStatsList *portStatsList_; OstProto::PortGroupContent *atConnectConfig_; QList atConnectPortConfig_; public: // FIXME(HIGH): member access QList mPorts; public: PortGroup(QString serverName = "127.0.0.1", quint16 port = DEFAULT_SERVER_PORT); ~PortGroup(); void connectToHost() { reconnect = true; compat = kUnknown; rpcChannel->establish(); } void connectToHost(QString serverName, quint16 port) { reconnect = true; compat = kUnknown; rpcChannel->establish(serverName, port); } void disconnectFromHost() { reconnect = false; rpcChannel->tearDown(); } void setConfigAtConnect(const OstProto::PortGroupContent *config); int numPorts() const { return mPorts.size(); } int numReservedPorts() const; quint32 id() const { return mPortGroupId; } const QString& userAlias() const { return mUserAlias; } void setUserAlias(QString alias) { mUserAlias = alias; }; const QString serverName() const { return rpcChannel->serverName(); } quint16 serverPort() const { return rpcChannel->serverPort(); } const QString serverFullName() const; QAbstractSocket::SocketState state() const { if (compat == kIncompatible) return QAbstractSocket::SocketState(-1); return rpcChannel->state(); } void processVersionCompatibility(PbRpcController *controller); void processPortIdList(PbRpcController *controller); void processPortConfigList(PbRpcController *controller); void processAddStreamAck(PbRpcController *controller); void processDeleteStreamAck(PbRpcController *controller); void processModifyStreamAck(int portIndex, PbRpcController *controller); void processApplyBuildAck(int portIndex, PbRpcController *controller); void processAddDeviceGroupAck(PbRpcController *controller); void processDeleteDeviceGroupAck(PbRpcController *controller); void processModifyDeviceGroupAck(int portIndex, PbRpcController *controller); void processDeviceList(int portIndex, PbRpcController *controller); void processDeviceNeighbors(int portIndex, PbRpcController *controller); void modifyPort(int portId, OstProto::Port portConfig); void processModifyPortAck(PbRpcController *controller); void processModifyPortBuildAck(bool restoreUi, PbRpcController *controller); void processUpdatedPortConfig(PbRpcController *controller); void getStreamIdList(); void processStreamIdList(int portIndex, PbRpcController *controller); void getStreamConfigList(int portIndex); void processStreamConfigList(int portIndex, PbRpcController *controller); void processModifyStreamAck(OstProto::Ack *ack); void getDeviceGroupIdList(); void processDeviceGroupIdList(int portIndex, PbRpcController *controller); void getDeviceGroupConfigList(int portIndex); void processDeviceGroupConfigList( int portIndex, PbRpcController *controller); void startTx(QList *portList = NULL); void processStartTxAck(PbRpcController *controller); void stopTx(QList *portList = NULL); void processStopTxAck(PbRpcController *controller); void startCapture(QList *portList = NULL); void processStartCaptureAck(PbRpcController *controller); void stopCapture(QList *portList = NULL); void processStopCaptureAck(PbRpcController *controller); void viewCapture(QList *portList = NULL); void processViewCaptureAck(PbRpcController *controller); void resolveDeviceNeighbors(QList *portList = NULL); void processResolveDeviceNeighborsAck(PbRpcController *controller); void clearDeviceNeighbors(QList *portList = NULL); void processClearDeviceNeighborsAck(PbRpcController *controller); void getPortStats(); void processPortStatsList(); void clearPortStats(QList *portList = NULL); void processClearPortStatsAck(PbRpcController *controller); bool clearStreamStats(QList *portList = NULL); void processClearStreamStatsAck(PbRpcController *controller); bool getStreamStats(QList *portList = NULL); void processStreamStatsList(PbRpcController *controller); signals: void applyFinished(); void portGroupDataChanged(int portGroupId, int portId = 0xFFFF); void portListAboutToBeChanged(quint32 portGroupId); void portListChanged(quint32 portGroupId); void statsChanged(quint32 portGroupId); void streamStatsReceived(quint32 portGroupId, const OstProto::StreamStatsList *stats); private slots: void on_reconnectTimer_timeout(); void on_rpcChannel_stateChanged(QAbstractSocket::SocketState state); void on_rpcChannel_connected(); void on_rpcChannel_disconnected(); void on_rpcChannel_error(QAbstractSocket::SocketError socketError); void on_rpcChannel_notification(int notifType, ::google::protobuf::Message *notification); void when_portListChanged(quint32 portGroupId); public slots: void when_configApply(int portIndex); void getDeviceInfo(int portIndex); }; #endif ostinato-1.3.0/client/portgrouplist.cpp000066400000000000000000000111471451413623100202760ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #include "portgrouplist.h" #include "params.h" #include "modeltest.h" PortGroupList::PortGroupList() : mPortGroupListModel(this), mStreamListModel(this), mPortStatsModel(this, this), mDeviceGroupModel(this), mDeviceModel(this) { #if defined(QT_NO_DEBUG) || QT_VERSION < 0x050700 streamModelTester_ = NULL; portModelTester_ = NULL; portStatsModelTester_ = NULL; deviceGroupModelTester_ = NULL; deviceModelTester_ = NULL; #else streamModelTester_ = new ModelTest(getStreamModel()); portModelTester_ = new ModelTest(getPortModel()); portStatsModelTester_ = new ModelTest(getPortStatsModel()); deviceGroupModelTester_ = new ModelTest(getDeviceGroupModel()); deviceModelTester_ = new ModelTest(getDeviceModel()); #endif } PortGroupList::~PortGroupList() { delete portStatsModelTester_; delete portModelTester_; delete streamModelTester_; delete deviceGroupModelTester_; while (!mPortGroups.isEmpty()) delete mPortGroups.takeFirst(); } bool PortGroupList::isPortGroup(const QModelIndex& index) { return mPortGroupListModel.isPortGroup(index); } bool PortGroupList::isPort(const QModelIndex& index) { return mPortGroupListModel.isPort(index); } PortGroup& PortGroupList::portGroup(const QModelIndex& index) { Q_ASSERT(index.isValid()); Q_ASSERT(mPortGroupListModel.isPortGroup(index)); return *(mPortGroups[index.row()]); } Port& PortGroupList::port(const QModelIndex& index) { Q_ASSERT(index.isValid()); Q_ASSERT(index.parent().isValid()); Q_ASSERT(mPortGroupListModel.isPort(index)); return (*mPortGroups.at(index.parent().row())->mPorts[index.row()]); } void PortGroupList::addPortGroup(PortGroup &portGroup) { mPortGroupListModel.portGroupAboutToBeAppended(); connect(&portGroup, SIGNAL(portGroupDataChanged(int, int)), &mPortGroupListModel, SLOT(when_portGroupDataChanged(int, int))); #if 0 connect(&portGroup, SIGNAL(portListAboutToBeChanged(quint32)), &mPortGroupListModel, SLOT(triggerLayoutAboutToBeChanged())); connect(&portGroup, SIGNAL(portListChanged(quint32)), &mPortGroupListModel, SLOT(triggerLayoutChanged())); #endif connect(&portGroup, SIGNAL(portListChanged(quint32)), &mPortGroupListModel, SLOT(when_portListChanged())); connect(&portGroup, SIGNAL(portListChanged(quint32)), &mPortStatsModel, SLOT(when_portListChanged())); connect(&portGroup, SIGNAL(portGroupDataChanged(int, int)), &mPortStatsModel, SLOT(when_portGroupDataChanged(int, int))); connect(&portGroup, SIGNAL(statsChanged(quint32)), &mPortStatsModel, SLOT(when_portGroup_stats_update(quint32))); mPortGroups.append(&portGroup); portGroup.connectToHost(); mPortGroupListModel.portGroupAppended(); mPortStatsModel.when_portListChanged(); } void PortGroupList::removePortGroup(PortGroup &portGroup) { // Disconnect before removing from list portGroup.disconnectFromHost(); mPortGroupListModel.portGroupAboutToBeRemoved(&portGroup); PortGroup* pg = mPortGroups.takeAt(mPortGroups.indexOf(&portGroup)); qDebug("after takeAt()"); mPortGroupListModel.portGroupRemoved(); delete pg; mPortStatsModel.when_portListChanged(); } void PortGroupList::removeAllPortGroups() { if (mPortGroups.isEmpty()) return; do { PortGroup *pg = mPortGroups.at(0); pg->disconnectFromHost(); mPortGroupListModel.portGroupAboutToBeRemoved(pg); mPortGroups.removeFirst(); delete pg; mPortGroupListModel.portGroupRemoved(); } while (!mPortGroups.isEmpty()); mPortGroupListModel.when_portListChanged(); mPortStatsModel.when_portListChanged(); } //.................... // Private Methods //.................... int PortGroupList::indexOfPortGroup(quint32 portGroupId) { for (int i = 0; i < mPortGroups.size(); i++) { if (mPortGroups.value(i)->id() == portGroupId) return i; } return -1; } ostinato-1.3.0/client/portgrouplist.h000066400000000000000000000045441451413623100177460ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _PORT_GROUP_LIST_H #define _PORT_GROUP_LIST_H #include "devicegroupmodel.h" #include "devicemodel.h" #include "portgroup.h" #include "portmodel.h" #include "portstatsmodel.h" #include "streammodel.h" class PortModel; class StreamModel; class PortGroupList : public QObject { Q_OBJECT friend class PortModel; friend class StreamModel; friend class PortStatsModel; QList mPortGroups; PortModel mPortGroupListModel; StreamModel mStreamListModel; PortStatsModel mPortStatsModel; DeviceGroupModel mDeviceGroupModel; DeviceModel mDeviceModel; QObject *streamModelTester_; QObject *portModelTester_; QObject *portStatsModelTester_; QObject *deviceGroupModelTester_; QObject *deviceModelTester_; // Methods public: PortGroupList(); ~PortGroupList(); PortModel* getPortModel() { return &mPortGroupListModel; } PortStatsModel* getPortStatsModel() { return &mPortStatsModel; } StreamModel* getStreamModel() { return &mStreamListModel; } DeviceGroupModel* getDeviceGroupModel() { return &mDeviceGroupModel; } DeviceModel* getDeviceModel() { return &mDeviceModel; } bool isPortGroup(const QModelIndex& index); bool isPort(const QModelIndex& index); PortGroup& portGroup(const QModelIndex& index); Port& port(const QModelIndex& index); int numPortGroups() { return mPortGroups.size(); } PortGroup& portGroupByIndex(int index) { return *(mPortGroups[index]); } void addPortGroup(PortGroup &portGroup); void removePortGroup(PortGroup &portGroup); void removeAllPortGroups(); private: int indexOfPortGroup(quint32 portGroupId); }; #endif ostinato-1.3.0/client/portmodel.cpp000066400000000000000000000243011451413623100173420ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #include "portmodel.h" #include "portgrouplist.h" #include #include #include #if 0 #define DBG0(x) qDebug(x) #define DBG1(x, p1) qDebug(x, (p1)) #else #define DBG0(x) {} #define DBG1(x, p1) {} #endif PortModel::PortModel(PortGroupList *p, QObject *parent) : QAbstractItemModel(parent) { pgl = p; } int PortModel::rowCount(const QModelIndex &parent) const { // qDebug("RowCount Enter\n"); if (!parent.isValid()) { // Top Level Item //qDebug("RowCount (Top) Exit: %d\n", pgl->mPortGroups.size()); return pgl->mPortGroups.size(); } // qDebug("RowCount non top %d, %d, %llx\n", // parent.row(), parent.column(), parent.internalId()); quint16 pg = (parent.internalId() >> 16) & 0xFFFF; quint16 p = parent.internalId() & 0xFFFF; if (p == 0xFFFF) { #if 0 // wrong code? int count = 0; foreach(PortGroup *pg, pgl->mPortGroups) { count += pg->numPorts(); } //qDebug("RowCount (Mid) Exit: %d\n", count); return count; #endif if (parent.column() == 0) return pgl->mPortGroups.value(pgl->indexOfPortGroup(pg))->numPorts(); else return 0; } else { // Leaf Item return 0; } } int PortModel::columnCount(const QModelIndex &/*parent*/) const { return 1; // FIXME: hardcoding } Qt::ItemFlags PortModel::flags(const QModelIndex &index) const { return QAbstractItemModel::flags(index); // FIXME: no need for this func } QVariant PortModel::data(const QModelIndex &index, int role) const { DBG0("Enter PortModel data\n"); // Check for a valid index if (!index.isValid()) return QVariant(); DBG1("PortModel::data(index).row = %d", index.row()); DBG1("PortModel::data(index).column = %0d", index.column()); DBG1("PortModel::data(index).internalId = %08llx", index.internalId()); QModelIndex parent = index.parent(); if (!parent.isValid()) { // Top Level Item - PortGroup if (role == Qt::DisplayRole) { DBG0("Exit PortModel data 1\n"); return QString("Port Group %1: %2 [%3]:%4 (%5)"). arg(pgl->mPortGroups.at(index.row())->id()). arg(pgl->mPortGroups.at(index.row())->userAlias()). arg(pgl->mPortGroups.at(index.row())->serverName()). arg(pgl->mPortGroups.at(index.row())->serverPort()). arg(pgl->mPortGroups.value(index.row())->numPorts()); } else if (role == Qt::DecorationRole) { DBG0("Exit PortModel data 2\n"); switch(pgl->mPortGroups.at(index.row())->state()) { case QAbstractSocket::UnconnectedState: return QIcon(":/icons/bullet_red.png"); case QAbstractSocket::HostLookupState: return QIcon(":/icons/bullet_yellow.png"); case QAbstractSocket::ConnectingState: case QAbstractSocket::ClosingState: return QIcon(":/icons/bullet_orange.png"); case QAbstractSocket::ConnectedState: return QIcon(":/icons/bullet_green.png"); case QAbstractSocket::BoundState: case QAbstractSocket::ListeningState: default: return QIcon(":/icons/bullet_error.png"); } } else { DBG0("Exit PortModel data 3\n"); return QVariant(); } } else { if (pgl->mPortGroups.at(parent.row())->numPorts() == 0) { DBG0("Exit PortModel data 4\n"); return QVariant(); } Port *port = pgl->mPortGroups.at(parent.row())->mPorts[index.row()]; // Non Top Level - Port if (role == Qt::DisplayRole) { QString rsvdBy; if (!port->userName().isEmpty()) rsvdBy = "["+port->userName()+"] "; return QString("Port %1: %2 %3(%4)") .arg(port->id()) .arg(port->userAlias()) .arg(rsvdBy) .arg(port->description()); } else if (role == Qt::DecorationRole) { return statusIcon( port->linkState(), port->hasExclusiveControl(), port->isTransmitting(), port->isCapturing()); } else if (role == Qt::ForegroundRole) { return port->isDirty() ? QBrush(Qt::red) : QVariant(); } else { DBG0("Exit PortModel data 6\n"); return QVariant(); } } return QVariant(); } QVariant PortModel::headerData(int /*section*/, Qt::Orientation orientation, int role) const { if (role != Qt::DisplayRole) return QVariant(); if (orientation == Qt::Horizontal) return QVariant(); else return QString("Name"); } Qt::DropActions PortModel::supportedDropActions() const { return Qt::IgnoreAction; // read-only model, doesn't accept any data } QModelIndex PortModel::index (int row, int col, const QModelIndex & parent) const { if (!hasIndex(row, col, parent)) return QModelIndex(); //qDebug("index: R=%d, C=%d, PR=%d, PC=%d, PID=%llx\n", // row, col, parent.row(), parent.column(), parent.internalId()); if (!parent.isValid()) { // Top Level Item quint16 pg = pgl->mPortGroups.value(row)->id(), p = 0xFFFF; quint32 id = (pg << 16) | p; //qDebug("index (top) dbg: PG=%d, P=%d, ID=%x\n", pg, p, id); return createIndex(row, col, id); } else { quint16 pg = parent.internalId() >> 16; quint16 p = pgl->mPortGroups.value(parent.row())->mPorts.value(row)->id(); quint32 id = (pg << 16) | p; //qDebug("index (nontop) dbg: PG=%d, P=%d, ID=%x\n", pg, p, id); return createIndex(row, col, id); } } QModelIndex PortModel::parent(const QModelIndex &index) const { if (!index.isValid()) return QModelIndex(); //qDebug("parent: R=%d, C=%d ID=%llx\n", // index.row(), index.column(), index.internalId()); quint16 pg = index.internalId() >> 16; quint16 p = index.internalId() & 0x0000FFFF; //qDebug("parent dbg: PG=%d, P=%d\n", pg, p); if (p == 0xFFFF) { //qDebug("parent ret: NULL\n"); // Top Level Item - PG return QModelIndex(); } quint32 id = (pg << 16) | 0xFFFF; //qDebug("parent ret: R=%d, C=%d, ID=%x\n", pg, 0, id); return createIndex(pgl->indexOfPortGroup(pg), 0, id); } bool PortModel::isPortGroup(const QModelIndex& index) { if (index.isValid() && ((index.internalId() & 0xFFFF) == 0xFFFF)) return true; else return false; } bool PortModel::isPort(const QModelIndex& index) { if (index.isValid() && ((index.internalId() & 0xFFFF) != 0xFFFF)) return true; else return false; } quint32 PortModel::portGroupId(const QModelIndex& index) { return (index.internalId()) >> 16 & 0xFFFF; } quint32 PortModel::portId(const QModelIndex& index) { return (index.internalId()) & 0xFFFF; } QPixmap PortModel::statusIcon( int linkState, bool exclusive, bool transmit, bool capture) const { QPixmap pixmap; QString key = QString("$ost:portStatusIcon:%1:%2:%3:%4") .arg(linkState).arg(exclusive).arg(transmit).arg(capture); if (QPixmapCache::find(key, pixmap)) return pixmap; static int sz = QPixmap(":/icons/frag_link_up.png").width(); // All frag_* icons must be of same size and can be overlayed // on top of each other; assume square icons pixmap = QPixmap(sz, sz); pixmap.fill(Qt::transparent); QPainter painter(&pixmap); switch (linkState) { case OstProto::LinkStateUp: painter.drawPixmap(0, 0, QPixmap(":/icons/frag_link_up.png")); break; case OstProto::LinkStateDown: painter.drawPixmap(0, 0, QPixmap(":/icons/frag_link_down.png")); break; case OstProto::LinkStateUnknown: painter.drawPixmap(0, 0, QPixmap(":/icons/frag_link_unknown.png")); break; } if (exclusive) painter.drawPixmap(0, 0, QPixmap(":/icons/frag_exclusive.png")); if (transmit) painter.drawPixmap(0, 0, QPixmap(":/icons/frag_transmit.png")); if (capture) painter.drawPixmap(0, 0, QPixmap(":/icons/frag_capture.png")); QPixmapCache::insert(key, pixmap); return pixmap; } // ---------------------------------------------- // Slots // ---------------------------------------------- void PortModel::when_portGroupDataChanged(int portGroupId, int portId) { QModelIndex index; int row; qDebug("portGroupId = %d, portId = %d", portGroupId, portId); if (portId == 0xFFFF) row = pgl->indexOfPortGroup(portGroupId); else row = portId; index = createIndex(row, 0, (portGroupId << 16) | portId); emit dataChanged(index, index); } void PortModel::portGroupAboutToBeAppended() { int row; row = pgl->mPortGroups.size(); beginInsertRows(QModelIndex(), row, row); } void PortModel::portGroupAppended() { endInsertRows(); } void PortModel::portGroupAboutToBeRemoved(PortGroup *portGroup) { int row; row = pgl->mPortGroups.indexOf(portGroup); beginRemoveRows(QModelIndex(), row, row); } void PortModel::portGroupRemoved() { endRemoveRows(); } void PortModel::when_portListChanged() { // FIXME: why needed? beginResetModel(); endResetModel(); } ostinato-1.3.0/client/portmodel.h000066400000000000000000000044231451413623100170120ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _PORT_MODEL_H #define _PORT_MODEL_H #include #include class PortGroupList; class PortGroup; class PortModel : public QAbstractItemModel { Q_OBJECT friend class PortGroupList; public: PortModel(PortGroupList *p, QObject *parent = 0); int rowCount(const QModelIndex &parent = QModelIndex()) const; int columnCount(const QModelIndex &parent = QModelIndex()) const; Qt::ItemFlags flags(const QModelIndex &index) const; QVariant data(const QModelIndex &index, int role) const; QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const; QModelIndex index (int row, int col, const QModelIndex &parent = QModelIndex()) const; QModelIndex parent(const QModelIndex &index) const; Qt::DropActions supportedDropActions() const; bool isPortGroup(const QModelIndex& index); bool isPort(const QModelIndex& index); quint32 portGroupId(const QModelIndex& index); quint32 portId(const QModelIndex& index); private slots: // FIXME: these are invoked from outside - how come they are "private"? void when_portGroupDataChanged(int portGroupId, int portId); void portGroupAboutToBeAppended(); void portGroupAppended(); void portGroupAboutToBeRemoved(PortGroup *portGroup); void portGroupRemoved(); void when_portListChanged(); #if 0 void triggerLayoutAboutToBeChanged(); void triggerLayoutChanged(); #endif private: QPixmap statusIcon(int linkState, bool exclusive, bool transmit, bool capture) const; PortGroupList *pgl; }; #endif ostinato-1.3.0/client/portstatsfilter.ui000066400000000000000000000105711451413623100204450ustar00rootroot00000000000000 PortStatsFilterDialog 0 0 319 193 Select Ports :/icons/portstats_filter.png false false QAbstractItemView::NoDragDrop QAbstractItemView::ExtendedSelection QListView::Static Qt::Vertical 20 40 > :/icons/arrow_right.png < :/icons/arrow_left.png Qt::Vertical 20 40 true true false QAbstractItemView::InternalMove QAbstractItemView::ExtendedSelection QListView::Free Qt::Horizontal QDialogButtonBox::Cancel|QDialogButtonBox::NoButton|QDialogButtonBox::Ok lvUnselected tbSelectIn tbSelectOut lvSelected buttonBox buttonBox accepted() PortStatsFilterDialog accept() 248 254 157 274 buttonBox rejected() PortStatsFilterDialog reject() 316 260 286 274 ostinato-1.3.0/client/portstatsfilterdialog.cpp000066400000000000000000000075251451413623100217770ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #include "portstatsfilterdialog.h" PortStatsFilterDialog::PortStatsFilterDialog(QWidget *parent) : QDialog(parent) { setupUi(this); mUnselected.setSortRole(kLogicalIndex); mSelected.setSortRole(kVisualIndex); lvUnselected->setModel(&mUnselected); lvSelected->setModel(&mSelected); } QList PortStatsFilterDialog::getItemList(bool* ok, QAbstractItemModel *model, Qt::Orientation orientation, QList initial) { QList ret; uint count = (orientation == Qt::Vertical) ? model->rowCount() : model->columnCount(); *ok = false; mUnselected.clear(); mSelected.clear(); for (uint i = 0; i < count; i++) { QStandardItem *item; item = new QStandardItem(model->headerData(i, orientation) .toString().replace('\n', ' ')); item->setData(i, kLogicalIndex); item->setData(initial.indexOf(i), kVisualIndex); item->setFlags(Qt::ItemIsSelectable | Qt::ItemIsDragEnabled //| Qt::ItemIsDropEnabled | Qt::ItemIsEnabled); if (initial.contains(i)) mSelected.appendRow(item); else mUnselected.appendRow(item); } mSelected.sort(0); // No need to sort right now 'coz we have inserted items in order if (exec() == QDialog::Accepted) { uint count = mSelected.rowCount(); for (uint i = 0; i < count; i++) { QModelIndex index = mSelected.index(i, 0, QModelIndex()); QStandardItem *item = mSelected.itemFromIndex(index); ret.append(item->data(kLogicalIndex).toInt()); } *ok = true; } return ret; } void PortStatsFilterDialog::on_tbSelectIn_clicked() { QList rows; foreach(QModelIndex idx, lvUnselected->selectionModel()->selectedIndexes()) rows.append(idx.row()); std::sort(rows.begin(), rows.end(), qGreater()); QModelIndex idx = lvSelected->selectionModel()->currentIndex(); int insertAt = idx.isValid() ? idx.row() : mSelected.rowCount(); foreach(int row, rows) { QList items = mUnselected.takeRow(row); mSelected.insertRow(insertAt, items); } } void PortStatsFilterDialog::on_tbSelectOut_clicked() { QList rows; foreach(QModelIndex idx, lvSelected->selectionModel()->selectedIndexes()) rows.append(idx.row()); std::sort(rows.begin(), rows.end(), qGreater()); foreach(int row, rows) { QList items = mSelected.takeRow(row); mUnselected.appendRow(items); } mUnselected.sort(0); } void PortStatsFilterDialog::on_lvUnselected_doubleClicked(const QModelIndex &index) { QList items = mUnselected.takeRow(index.row()); QModelIndex idx = lvSelected->selectionModel()->currentIndex(); int insertAt = idx.isValid() ? idx.row() : mSelected.rowCount(); mSelected.insertRow(insertAt, items); } void PortStatsFilterDialog::on_lvSelected_doubleClicked(const QModelIndex &index) { QList items = mSelected.takeRow(index.row()); mUnselected.appendRow(items); mUnselected.sort(0); } ostinato-1.3.0/client/portstatsfilterdialog.h000066400000000000000000000031041451413623100214310ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _PORT_STATS_FILTER_DIALOG_H #define _PORT_STATS_FILTER_DIALOG_H #include #include #include #include "ui_portstatsfilter.h" #include "portgrouplist.h" class PortStatsFilterDialog : public QDialog, public Ui::PortStatsFilterDialog { Q_OBJECT public: PortStatsFilterDialog(QWidget *parent = 0); QList getItemList(bool* ok, QAbstractItemModel *model, Qt::Orientation orientation = Qt::Vertical, QList initial = QList()); private: enum ItemRole { kLogicalIndex = Qt::UserRole + 1, kVisualIndex }; QStandardItemModel mUnselected; QStandardItemModel mSelected; private slots: void on_tbSelectIn_clicked(); void on_tbSelectOut_clicked(); void on_lvUnselected_doubleClicked(const QModelIndex &index); void on_lvSelected_doubleClicked(const QModelIndex &index); }; #endif ostinato-1.3.0/client/portstatsmodel.cpp000066400000000000000000000330401451413623100204210ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #include "portstatsmodel.h" #include "portgrouplist.h" #include #include #include #include #include enum { // XXX: The byte stats don't include FCS so include it in the overhead kPerPacketByteOverhead = 24 // 1(SFD)+7(Preamble)+12(IPG)+4(FCS) }; PortStatsModel::PortStatsModel(PortGroupList *p, QObject *parent) : QAbstractTableModel(parent) { pgl = p; timer = new QTimer(); connect(timer, SIGNAL(timeout()), this, SLOT(updateStats())); timer->start(1000); } PortStatsModel::~PortStatsModel() { timer->stop(); delete timer; } int PortStatsModel::rowCount(const QModelIndex &parent) const { if (parent.isValid()) return 0; if (numPorts.isEmpty()) return 0; if (numPorts.last() == 0) return 0; return (int) e_STAT_MAX; } int PortStatsModel::columnCount(const QModelIndex &parent ) const { if (parent.isValid()) return 0; else if (numPorts.isEmpty()) return 0; else return numPorts.last(); } void PortStatsModel::getDomainIndexes(const QModelIndex &index, uint &portGroupIdx, uint &portIdx) const { int portNum; // TODO(LOW): Optimize using binary search: see qLowerBound() portNum = index.column() + 1; for (portGroupIdx = 0; portGroupIdx < (uint) numPorts.size(); portGroupIdx++) if (portNum <= numPorts.at(portGroupIdx)) break; if (portGroupIdx) { if (numPorts.at(portGroupIdx -1)) portIdx = (portNum - 1) - numPorts.at(portGroupIdx - 1); else portIdx = portNum - 1; } else portIdx = portNum - 1; //qDebug("PSM: %d - %d, %d", index.column(), portGroupIdx, portIdx); } QVariant PortStatsModel::data(const QModelIndex &index, int role) const { // Check for a valid index if (!index.isValid()) return QVariant(); // Check for row/column limits int row = index.row(); if (row >= e_STAT_MAX) return QVariant(); if (numPorts.isEmpty()) return QVariant(); if (index.column() >= (numPorts.last())) return QVariant(); if (role == Qt::TextAlignmentRole) { if (row >= e_STATISTICS_START && row <= e_STATISTICS_END) return Qt::AlignRight; // right-align numbers else return Qt::AlignHCenter; // center-align everything else } uint pgidx, pidx; getDomainIndexes(index, pgidx, pidx); OstProto::PortStats stats = pgl->mPortGroups.at(pgidx) ->mPorts[pidx]->getStats(); // Check role if (role == Qt::DisplayRole) { switch(row) { // Info case e_INFO_USER: return pgl->mPortGroups.at(pgidx)->mPorts[pidx]->userName(); // States case e_COMBO_STATE: return QString("Link %1%2%3") .arg(LinkStateName.at(stats.state().link_state())) .arg(stats.state().is_transmit_on() ? ";Tx On" : "") .arg(stats.state().is_capture_on() ? ";Cap On" : ""); // Statistics case e_STAT_FRAMES_RCVD: return QString("%L1").arg(quint64(stats.rx_pkts())); case e_STAT_FRAMES_SENT: return QString("%L1").arg(quint64(stats.tx_pkts())); case e_STAT_FRAME_SEND_RATE: return QString("%L1").arg(quint64(stats.tx_pps())); case e_STAT_FRAME_RECV_RATE: return QString("%L1").arg(quint64(stats.rx_pps())); case e_STAT_BYTES_RCVD: return QString("%L1").arg(quint64(stats.rx_bytes())); case e_STAT_BYTES_SENT: return QString("%L1").arg(quint64(stats.tx_bytes())); #if 0 case e_STAT_BYTE_SEND_RATE: return QString("%L1").arg(quint64(stats.tx_bps())); case e_STAT_BYTE_RECV_RATE: return QString("%L1").arg(quint64(stats.rx_bps())); #endif case e_STAT_BIT_SEND_RATE: return QString("%L1").arg(quint64( stats.tx_bps() + stats.tx_pps()*kPerPacketByteOverhead)*8); case e_STAT_BIT_RECV_RATE: return QString("%L1").arg(quint64( stats.rx_bps() + stats.rx_pps()*kPerPacketByteOverhead)*8); #if 0 case e_STAT_FRAMES_RCVD_NIC: return stats.rx_pkts_nic(); case e_STAT_FRAMES_SENT_NIC: return stats.tx_pkts_nic(); case e_STAT_BYTES_RCVD_NIC: return stats.rx_bytes_nic(); case e_STAT_BYTES_SENT_NIC: return stats.tx_bytes_nic(); #endif case e_STAT_RX_DROPS: return QString("%L1").arg(quint64(stats.rx_drops())); case e_STAT_RX_ERRORS: return QString("%L1").arg(quint64(stats.rx_errors())); case e_STAT_RX_FIFO_ERRORS: return QString("%L1").arg(quint64(stats.rx_fifo_errors())); case e_STAT_RX_FRAME_ERRORS: return QString("%L1").arg(quint64(stats.rx_frame_errors())); default: qWarning("%s: Unhandled stats id %d\n", __FUNCTION__, index.row()); return 0; } } else if (role == Qt::DecorationRole) { if (row == e_COMBO_STATE) return statusIcons( stats.state().link_state(), stats.state().is_transmit_on(), stats.state().is_capture_on()); else return QVariant(); } else if (role == Qt::ToolTipRole) { if (row == e_COMBO_STATE) { QString linkIcon; switch (stats.state().link_state()) { case OstProto::LinkStateUp: linkIcon = ":/icons/bullet_green.png"; break; case OstProto::LinkStateDown: linkIcon = ":/icons/bullet_red.png"; break; case OstProto::LinkStateUnknown: linkIcon = ":/icons/bullet_white.png"; break; } // FIXME: Ideally, the text should be vertically centered wrt icon // but style='vertical-align:middle for the img tag doesn't work QString tooltip = QString(" Link %2") .arg(linkIcon) .arg(LinkStateName.at( stats.state().link_state())); if (stats.state().is_transmit_on()) tooltip.prepend("" " Transmit On
"); if (stats.state().is_capture_on()) tooltip.append("
" " Capture On"); return tooltip; } else return QVariant(); } else return QVariant(); } QVariant PortStatsModel::headerData(int section, Qt::Orientation orientation, int role) const { if (role == Qt::ToolTipRole) { if (orientation == Qt::Horizontal) { QString notes; uint portGroupIdx, portIdx; if (numPorts.isEmpty() || section >= numPorts.last()) return QVariant(); getDomainIndexes(index(0, section), portGroupIdx, portIdx); notes = pgl->mPortGroups.at(portGroupIdx)->mPorts[portIdx]->notes(); if (!notes.isEmpty()) return notes; else return QVariant(); } else return QVariant(); } if ((role == Qt::BackgroundRole) && (orientation == Qt::Vertical) && qApp->styleSheet().isEmpty()) { QPalette palette = QApplication::palette(); return section & 0x1 ? palette.alternateBase() : palette.base(); } if (role != Qt::DisplayRole) return QVariant(); if (orientation == Qt::Horizontal) { uint portGroupIdx, portIdx; QString portName; if (numPorts.isEmpty() || section >= numPorts.last()) return QVariant(); getDomainIndexes(index(0, section), portGroupIdx, portIdx); PortGroup *portGroup = pgl->mPortGroups.at(portGroupIdx); Port *port = portGroup->mPorts.at(portIdx); portName = QString("Port %1-%2") .arg(portGroup->id()) .arg(port->id()); if (portGroupIdx < (uint) pgl->mPortGroups.size() && portIdx < (uint) portGroup->mPorts.size()) { if (!port->notes().isEmpty()) portName += " *"; portName += "\n"; portName += port->userDescription().isEmpty() ? port->userAlias() : port->userDescription(); } return portName; } else return PortStatName.at(section); } Qt::DropActions PortStatsModel::supportedDropActions() const { return Qt::IgnoreAction; // read-only model, doesn't accept any data } void PortStatsModel::portListFromIndex(QModelIndexList indices, QList &portList) { int i, j; QModelIndexList selectedCols(indices); portList.clear(); //selectedCols = indices.selectedColumns(); for (i = 0; i < selectedCols.size(); i++) { uint portGroupIdx, portIdx; getDomainIndexes(selectedCols.at(i), portGroupIdx, portIdx); for (j = 0; j < portList.size(); j++) { if (portList[j].portGroupId == portGroupIdx) break; } if (j >= portList.size()) { // PortGroup Not found PortGroupAndPortList p; p.portGroupId = portGroupIdx; p.portList.append(portIdx); portList.append(p); } else { // PortGroup found portList[j].portList.append(portIdx); } } } // // Slots // void PortStatsModel::when_portListChanged() { int i, count = 0; beginResetModel(); // recalc numPorts while (numPorts.size()) numPorts.removeFirst(); for (i = 0; i < pgl->mPortGroups.size(); i++) { count += pgl->mPortGroups.at(i)->numPorts(); numPorts.append(count); } endResetModel(); } void PortStatsModel::when_portGroupDataChanged(int /*portGroupId*/, int /*portId*/) { if (!columnCount()) return; // Port (user) description may have changed - update column headers // TODO: update only the changed ports, not all emit headerDataChanged(Qt::Horizontal, 0, columnCount()-1); } // FIXME: unused? if used, the index calculation row/column needs to be swapped #if 0 void PortStatsModel::on_portStatsUpdate(int port, void* /*stats*/) { QModelIndex topLeft = index(port, 0, QModelIndex()); QModelIndex bottomRight = index(port, e_STAT_MAX, QModelIndex()); emit dataChanged(topLeft, bottomRight); } #endif void PortStatsModel::updateStats() { // Request each portgroup to fetch updated stats - the port group // raises a signal once updated stats are available for (int i = 0; i < pgl->mPortGroups.size(); i++) pgl->mPortGroups[i]->getPortStats(); } void PortStatsModel::when_portGroup_stats_update(quint32 /*portGroupId*/) { // FIXME(MED): update only the changed ports, not all QModelIndex topLeft = index(0, 0, QModelIndex()); QModelIndex bottomRight = index(rowCount()-1, columnCount()-1, QModelIndex()); emit dataChanged(topLeft, bottomRight); } QPixmap PortStatsModel::statusIcons( int linkState, bool transmit, bool capture) const { QPixmap pixmap; QString key = QString("$ost:statusList:%1:%2:%3") .arg(linkState).arg(transmit).arg(capture); if (QPixmapCache::find(key, pixmap)) return pixmap; static int sz = QPixmap(":/icons/transmit_on.png").width(); // Assume all icons are of same size and are square QPixmap blank(sz, sz); blank.fill(Qt::transparent); pixmap = QPixmap(sz*3, sz); pixmap.fill(Qt::transparent); QPainter painter(&pixmap); painter.drawPixmap(0, 0, transmit ? QPixmap(":/icons/transmit_on.png") : blank); switch (linkState) { case OstProto::LinkStateUp: painter.drawPixmap(sz, 0, QPixmap(":/icons/bullet_green.png")); break; case OstProto::LinkStateDown: painter.drawPixmap(sz, 0, QPixmap(":/icons/bullet_red.png")); break; case OstProto::LinkStateUnknown: painter.drawPixmap(sz, 0, QPixmap(":/icons/bullet_white.png")); break; default: painter.drawPixmap(sz, 0, blank); } painter.drawPixmap(sz*2, 0, capture ? QPixmap(":/icons/sound_none.png") : blank); QPixmapCache::insert(key, pixmap); return pixmap; } ostinato-1.3.0/client/portstatsmodel.h000066400000000000000000000077331451413623100201000ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _PORT_STATS_MODEL_H #define _PORT_STATS_MODEL_H #include #include class QTimer; typedef enum { // State e_STATE_START = 0, e_COMBO_STATE = e_STATE_START, e_STATE_END = e_COMBO_STATE, // Statistics e_STATISTICS_START, e_STAT_FRAMES_SENT = e_STATISTICS_START, e_STAT_FRAMES_RCVD, e_STAT_BYTES_SENT, e_STAT_BYTES_RCVD, e_STAT_FRAME_SEND_RATE, e_STAT_FRAME_RECV_RATE, #if 0 e_STAT_BYTE_SEND_RATE, e_STAT_BYTE_RECV_RATE, #endif e_STAT_BIT_SEND_RATE, e_STAT_BIT_RECV_RATE, #if 0 e_STAT_FRAMES_RCVD_NIC, e_STAT_FRAMES_SENT_NIC, e_STAT_BYTES_RCVD_NIC, e_STAT_BYTES_SENT_NIC, #endif // Rx Errors e_STAT_RX_DROPS, e_STAT_RX_ERRORS, e_STAT_RX_FIFO_ERRORS, e_STAT_RX_FRAME_ERRORS, e_STATISTICS_END = e_STAT_RX_FRAME_ERRORS, // Info e_INFO_START, // XXX: keep hidden rows at end to avoid having to recalculate rows e_INFO_USER = e_INFO_START, e_INFO_END = e_INFO_USER, e_STAT_MAX } PortStat; static const QStringList PortStatName = (QStringList() << "Status" << "Sent Frames" << "Received Frames" << "Sent Bytes" << "Received Bytes" << "Send Frame Rate (fps)" << "Receive Frame Rate (fps)" #if 0 << "Send Byte Rate (Bps)" << "Receive Byte Rate (Bps)" #endif << "Send Bit Rate (bps)" << "Receive Bit Rate (bps)" #if 0 << "Frames Received (NIC)" << "Frames Sent (NIC)" << "Bytes Received (NIC)" << "Bytes Sent (NIC)" #endif << "Receive Drops" << "Receive Errors" << "Receive Fifo Errors" << "Receive Frame Errors" << "User" ); static QStringList LinkStateName = (QStringList() << "Unknown" << "Down" << "Up" ); class PortGroupList; class PortStatsModel : public QAbstractTableModel { Q_OBJECT public: PortStatsModel(PortGroupList *p, QObject *parent = 0); ~PortStatsModel(); int rowCount(const QModelIndex &parent = QModelIndex()) const; int columnCount(const QModelIndex &parent = QModelIndex()) const; QVariant data(const QModelIndex &index, int role) const; QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const; Qt::DropActions supportedDropActions() const; class PortGroupAndPortList { public: uint portGroupId; QList portList; }; void portListFromIndex(QModelIndexList indices, QList &portList); public slots: void when_portListChanged(); //void on_portStatsUpdate(int port, void*stats); void when_portGroupDataChanged(int portGroupId, int portId); void when_portGroup_stats_update(quint32 portGroupId); private slots: void updateStats(); private: PortGroupList *pgl; // numPorts stores the num of ports per portgroup // in the same order as the portgroups are index in the pgl // Also it stores them as cumulative totals QList numPorts; QTimer *timer; void getDomainIndexes(const QModelIndex &index, uint &portGroupIdx, uint &portIdx) const; QPixmap statusIcons(int linkState, bool transmit, bool capture) const; }; #endif ostinato-1.3.0/client/portstatsproxymodel.h000066400000000000000000000032151451413623100211710ustar00rootroot00000000000000/* Copyright (C) 2015 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _PORT_STATS_PROXY_MODEL_H #define _PORT_STATS_PROXY_MODEL_H #include #include class PortStatsProxyModel : public QSortFilterProxyModel { Q_OBJECT public: PortStatsProxyModel(QSet hiddenRows = QSet(), QObject *parent = 0) : QSortFilterProxyModel(parent), hiddenRows_(hiddenRows) { setFilterRegExp(QRegExp(".*")); } protected: bool filterAcceptsColumn(int sourceColumn, const QModelIndex &sourceParent) const { QModelIndex index = sourceModel()->index(0, sourceColumn, sourceParent); QString user = sourceModel()->data(index).toString(); return filterRegExp().exactMatch(user) ? true : false; } bool filterAcceptsRow(int sourceRow, const QModelIndex &/*sourceParent*/) const { return hiddenRows_.contains(sourceRow) ? false : true; } private: QSet hiddenRows_; }; #endif ostinato-1.3.0/client/portstatswindow.cpp000066400000000000000000000317241451413623100206370ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #include "portstatswindow.h" #include "icononlydelegate.h" #include "portstatsfilterdialog.h" #include "portstatsmodel.h" #include "portstatsproxymodel.h" #include "rowborderdelegate.h" #include "streamstatsmodel.h" #include "streamstatswindow.h" #include "settings.h" #include #include #include extern QMainWindow *mainWindow; PortStatsWindow::PortStatsWindow(PortGroupList *pgl, QWidget *parent) : QWidget(parent), proxyStatsModel(NULL) { setupUi(this); this->pgl = pgl; model = pgl->getPortStatsModel(); // Hide 'user' row proxyStatsModel = new PortStatsProxyModel( QSet({e_INFO_USER}), this); if (proxyStatsModel) { proxyStatsModel->setSourceModel(model); tvPortStats->setModel(proxyStatsModel); } else tvPortStats->setModel(model); tvPortStats->setAlternatingRowColors(true); tvPortStats->verticalHeader()->setHighlightSections(false); tvPortStats->verticalHeader()->setDefaultSectionSize( tvPortStats->verticalHeader()->minimumSectionSize()); // XXX: Set Delegates for port stats view // RowBorderDelegate: Group related stats using a horizontal line // IconOnlyDelegate : For status, show only icons not icons+text tvPortStats->setItemDelegate( new RowBorderDelegate( QSet({ e_STAT_FRAMES_SENT, e_STAT_FRAME_SEND_RATE, e_STAT_RX_DROPS}), this)); statusDelegate = new IconOnlyDelegate(this); #if 0 // XXX: Ideally we should use this, but it doesn't work because in // this constructor, the port model is empty and model->index() returns // an invalid index ... tvPortStats->setItemDelegateForRow( proxyStatsModel ? proxyStatsModel->mapFromSource(model->index(e_COMBO_STATE, 0)) .row() : e_COMBO_STATE, statusDelegate); #else // ... so we use this hard-coded hack tvPortStats->setItemDelegateForRow(e_COMBO_STATE, statusDelegate); #endif connect(tvPortStats->selectionModel(), SIGNAL(selectionChanged( const QItemSelection&, const QItemSelection&)), SLOT(when_tvPortStats_selectionChanged( const QItemSelection&, const QItemSelection&))); when_tvPortStats_selectionChanged(QItemSelection(), QItemSelection()); } PortStatsWindow::~PortStatsWindow() { delete proxyStatsModel; delete statusDelegate; } /* ------------- SLOTS (public) -------------- */ void PortStatsWindow::clearCurrentSelection() { tvPortStats->selectionModel()->clearCurrentIndex(); tvPortStats->clearSelection(); } void PortStatsWindow::showMyReservedPortsOnly(bool enabled) { if (!proxyStatsModel) return; if (enabled) { QString rx(appSettings->value(kUserKey, kUserDefaultValue).toString()); proxyStatsModel->setFilterRegExp(QRegExp::escape(rx)); } else proxyStatsModel->setFilterRegExp(QRegExp(".*")); // match all } /* ------------- SLOTS (private) -------------- */ void PortStatsWindow::when_tvPortStats_selectionChanged( const QItemSelection& /*selected*/, const QItemSelection& /*deselected*/) { QModelIndexList indexList = tvPortStats->selectionModel()->selectedColumns(); if (proxyStatsModel) { selectedColumns.clear(); foreach(QModelIndex index, indexList) selectedColumns.append(proxyStatsModel->mapToSource(index)); } else selectedColumns = indexList; bool isEmpty = selectedColumns.isEmpty(); tbStartTransmit->setDisabled(isEmpty); tbStopTransmit->setDisabled(isEmpty); tbStartCapture->setDisabled(isEmpty); tbStopCapture->setDisabled(isEmpty); tbViewCapture->setDisabled(isEmpty); tbClear->setDisabled(isEmpty); tbGetStreamStats->setDisabled(isEmpty); tbResolveNeighbors->setDisabled(isEmpty); tbClearNeighbors->setDisabled(isEmpty); } void PortStatsWindow::on_tbStartTransmit_clicked() { QList pgpl; // Get selected ports model->portListFromIndex(selectedColumns, pgpl); // Clear selected ports, portgroup by portgroup for (int i = 0; i < pgpl.size(); i++) { pgl->portGroupByIndex(pgpl.at(i).portGroupId). startTx(&pgpl[i].portList); } } void PortStatsWindow::on_tbStopTransmit_clicked() { QList pgpl; // Get selected ports model->portListFromIndex(selectedColumns, pgpl); // Clear selected ports, portgroup by portgroup for (int i = 0; i < pgpl.size(); i++) { pgl->portGroupByIndex(pgpl.at(i).portGroupId). stopTx(&pgpl[i].portList); } } void PortStatsWindow::on_tbStartCapture_clicked() { // TODO(MED) QList pgpl; // Get selected ports model->portListFromIndex(selectedColumns, pgpl); // Clear selected ports, portgroup by portgroup for (int i = 0; i < pgpl.size(); i++) { pgl->portGroupByIndex(pgpl.at(i).portGroupId). startCapture(&pgpl[i].portList); } } void PortStatsWindow::on_tbStopCapture_clicked() { // TODO(MED) QList pgpl; // Get selected ports model->portListFromIndex(selectedColumns, pgpl); // Clear selected ports, portgroup by portgroup for (int i = 0; i < pgpl.size(); i++) { pgl->portGroupByIndex(pgpl.at(i).portGroupId). stopCapture(&pgpl[i].portList); } } void PortStatsWindow::on_tbViewCapture_clicked() { // TODO(MED) QList pgpl; // Get selected ports model->portListFromIndex(selectedColumns, pgpl); // Clear selected ports, portgroup by portgroup for (int i = 0; i < pgpl.size(); i++) { pgl->portGroupByIndex(pgpl.at(i).portGroupId). viewCapture(&pgpl[i].portList); } } void PortStatsWindow::on_tbResolveNeighbors_clicked() { QList portList; // Get selected ports model->portListFromIndex(selectedColumns, portList); // Resolve ARP/ND for selected ports, portgroup by portgroup for (int i = 0; i < portList.size(); i++) { pgl->portGroupByIndex(portList.at(i).portGroupId). resolveDeviceNeighbors(&portList[i].portList); // Update device info for the just processed portgroup for (int j = 0; j < portList[i].portList.size(); j++) { pgl->portGroupByIndex(portList.at(i).portGroupId). getDeviceInfo(portList[i].portList[j]); } } } void PortStatsWindow::on_tbClearNeighbors_clicked() { QList portList; // Get selected ports model->portListFromIndex(selectedColumns, portList); // Clear ARP/ND for ports, portgroup by portgroup for (int i = 0; i < portList.size(); i++) { pgl->portGroupByIndex(portList.at(i).portGroupId). clearDeviceNeighbors(&portList[i].portList); // Update device info for the just processed portgroup for (int j = 0; j < portList[i].portList.size(); j++) { pgl->portGroupByIndex(portList.at(i).portGroupId). getDeviceInfo(portList[i].portList[j]); } } } void PortStatsWindow::on_tbClear_clicked() { QList portList; // Get selected ports model->portListFromIndex(selectedColumns, portList); // Clear selected ports, portgroup by portgroup for (int i = 0; i < portList.size(); i++) { pgl->portGroupByIndex(portList.at(i).portGroupId). clearPortStats(&portList[i].portList); pgl->portGroupByIndex(portList.at(i).portGroupId). clearStreamStats(&portList[i].portList); } } // 'All' => all ports currently visible, not all ports in all portgroups void PortStatsWindow::on_tbClearAll_clicked() { QAbstractItemModel *mdl = tvPortStats->model(); QModelIndexList shownColumns; QList portList; // Find the 'visible' columns for(int vi = 0; vi < mdl->columnCount(); vi++) { int li = tvPortStats->horizontalHeader()->logicalIndex(vi); if (!tvPortStats->isColumnHidden(li)) { shownColumns.append(mdl->index(0, li)); } } if (proxyStatsModel) { for(QModelIndex &index : shownColumns) index = proxyStatsModel->mapToSource(index); } // Get ports corresponding to the shown columns model->portListFromIndex(shownColumns, portList); // Clear shown ports, portgroup by portgroup for (int i = 0; i < portList.size(); i++) { pgl->portGroupByIndex(portList.at(i).portGroupId) .clearPortStats(&portList[i].portList); pgl->portGroupByIndex(portList.at(i).portGroupId) .clearStreamStats(&portList[i].portList); } } void PortStatsWindow::on_tbGetStreamStats_clicked() { QList portList; StreamStatsModel *streamStatsModel; // Get selected ports model->portListFromIndex(selectedColumns, portList); if (portList.size()) { QDockWidget *dock = new QDockWidget(mainWindow); streamStatsModel = new StreamStatsModel(dock); dock->setWidget(new StreamStatsWindow(streamStatsModel, dock)); dock->setWindowTitle(dock->widget()->windowTitle()); dock->setObjectName("streamStatsDock"); dock->setAttribute(Qt::WA_DeleteOnClose); QDockWidget *statsDock = mainWindow->findChild( "statsDock"); mainWindow->addDockWidget(Qt::BottomDockWidgetArea, dock); // Add stream stats tab to the immediate right of port-stats ... mainWindow->tabifyDockWidget(statsDock, dock); mainWindow->splitDockWidget(statsDock, dock, Qt::Horizontal); // ... make it the currently visible tab ... dock->show(); dock->raise(); // ... and set tab remove behaviour // XXX: unfortunately, there's no direct way to get the TabBar QList tabBars = mainWindow->findChildren(); foreach(QTabBar* tabBar, tabBars) { if (tabBar->tabText(tabBar->currentIndex()) == dock->widget()->windowTitle()) tabBar->setSelectionBehaviorOnRemove( QTabBar::SelectPreviousTab); } } // Get stream stats for selected ports, portgroup by portgroup for (int i = 0; i < portList.size(); i++) { PortGroup &pg = pgl->portGroupByIndex(portList.at(i).portGroupId); if (pg.getStreamStats(&portList[i].portList)) { connect(&pg,SIGNAL(streamStatsReceived( quint32, const OstProto::StreamStatsList*)), streamStatsModel, SLOT(appendStreamStatsList( quint32, const OstProto::StreamStatsList*))); } } } void PortStatsWindow::on_tbFilter_clicked() { bool ok; QList currentColumns, newColumns; PortStatsFilterDialog dialog; QAbstractItemModel *mdl = tvPortStats->model(); // create the input list for the filter dialog - // list of logical-indexes ordered by their current visual indexes for(int vi = 0; vi < mdl->columnCount(); vi++) { int li = tvPortStats->horizontalHeader()->logicalIndex(vi); if (!tvPortStats->isColumnHidden(li)) { currentColumns.append(li); } } // return list from the filter dialog - // list of logical-indexes ordered by their new visual indexes newColumns = dialog.getItemList(&ok, mdl, Qt::Horizontal, currentColumns); if (ok) { QHeaderView *hv = tvPortStats->horizontalHeader(); // hide/show sections first ... for(int li = 0; li < mdl->columnCount(); li++) tvPortStats->setColumnHidden(li, !newColumns.contains(li)); // ... then for the 'shown' columns, set the visual index for(int vi = 0; vi < newColumns.size(); vi++) hv->moveSection(hv->visualIndex(newColumns.at(vi)), vi); } } ostinato-1.3.0/client/portstatswindow.h000066400000000000000000000037341451413623100203040ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _PORT_STATS_WINDOW_H #define _PORT_STATS_WINDOW_H #include #include #include "ui_portstatswindow.h" #include "portgrouplist.h" #include "portstatsmodel.h" class QSortFilterProxyModel; class QStyledItemDelegate; class PortStatsWindow : public QWidget, public Ui::PortStatsWindow { Q_OBJECT public: PortStatsWindow(PortGroupList *pgl, QWidget *parent = 0); ~PortStatsWindow(); public slots: void clearCurrentSelection(); void showMyReservedPortsOnly(bool enabled); private slots: void when_tvPortStats_selectionChanged(const QItemSelection &selected, const QItemSelection &deselected); void on_tbStartTransmit_clicked(); void on_tbStopTransmit_clicked(); void on_tbStartCapture_clicked(); void on_tbStopCapture_clicked(); void on_tbViewCapture_clicked(); void on_tbClear_clicked(); void on_tbClearAll_clicked(); void on_tbGetStreamStats_clicked(); void on_tbResolveNeighbors_clicked(); void on_tbClearNeighbors_clicked(); void on_tbFilter_clicked(); private: PortGroupList *pgl; PortStatsModel *model; QSortFilterProxyModel *proxyStatsModel; QStyledItemDelegate *statusDelegate; QModelIndexList selectedColumns; }; #endif ostinato-1.3.0/client/portstatswindow.ui000066400000000000000000000220551451413623100204670ustar00rootroot00000000000000 PortStatsWindow 0 0 502 415 Form QFrame::Panel QFrame::Raised Transmit Start Tx Starts transmit on selected port(s) Start :/icons/control_play.png:/icons/control_play.png Stop Tx Stops transmit on selected port(s) Stop :/icons/control_stop.png:/icons/control_stop.png Qt::Vertical Stats Fetch Selected Port Stream Stats Fetches stream statistics from the selected port(s) Fetch Stream Stats :/icons/stream_stats.png Clear Selected Port Stats Clears statistics of the selected port(s) Clear :/icons/portstats_clear.png:/icons/portstats_clear.png Clear All Ports Stats Clears statistics of all ports Clear All :/icons/portstats_clear_all.png:/icons/portstats_clear_all.png Qt::Vertical Capture Start Capture Captures packets on the selected port(s) Start :/icons/sound_none.png:/icons/sound_none.png Stop Capture End capture on selected port(s) Stop :/icons/sound_mute.png:/icons/sound_mute.png View Capture Buffer View captured packets on selected port(s) View :/icons/magnifier.png:/icons/magnifier.png Qt::Vertical ARP/ND Resolve Neighbors Resolve Device Neighbors on selected port(s) Resolve Neighbors :/icons/neighbor_resolve.png:/icons/neighbor_resolve.png Clear Neighbors Clear Device Neighbors on selected port(s) Clear Neighbors :/icons/neighbor_clear.png:/icons/neighbor_clear.png Qt::Vertical Qt::Horizontal 40 20 Select which ports to view Filter :/icons/portstats_filter.png:/icons/portstats_filter.png QAbstractItemView::SelectColumns XTableView QTableView

xtableview.h
ostinato-1.3.0/client/portswindow.cpp000066400000000000000000000473771451413623100177560ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #include "portswindow.h" #include "applymsg.h" #include "deviceswidget.h" #include "fileformat.pb.h" #include "portconfigdialog.h" #include "portgrouplist.h" #include "portwidget.h" #include "settings.h" #include "streamswidget.h" #include #include #include #include #include extern QMainWindow *mainWindow; PortsWindow::PortsWindow(PortGroupList *pgl, QWidget *parent) : QWidget(parent), proxyPortModel(NULL) { proxyPortModel = new QSortFilterProxyModel(this); plm = pgl; setupUi(this); applyMsg_ = new ApplyMessage(); portWidget->setPortGroupList(plm); streamsWidget->setPortGroupList(plm); devicesWidget->setPortGroupList(plm); tvPortList->header()->hide(); // Populate PortList Context Menu Actions tvPortList->addAction(actionNew_Port_Group); tvPortList->addAction(actionDelete_Port_Group); tvPortList->addAction(actionConnect_Port_Group); tvPortList->addAction(actionDisconnect_Port_Group); tvPortList->addAction(actionExclusive_Control); tvPortList->addAction(actionPort_Configuration); // PortList, StreamList, DeviceWidget actions combined // make this window's actions addActions(tvPortList->actions()); QAction *sep = new QAction(this); sep->setSeparator(true); addAction(sep); addActions(streamsWidget->actions()); sep = new QAction(this); sep->setSeparator(true); addAction(sep); addActions(devicesWidget->actions()); // XXX: It would be ideal if we only needed to do the below to // get the proxy model to do its magic. However, the QModelIndex // used by the source model and the proxy model are different // i.e. the row, column, internalId/internalPtr used by both // will be different. Since our domain objects - PortGroupList, // PortGroup, Port etc. use these attributes, we need to map the // proxy's index to the source's index before invoking any domain // object methods // TODO: research if we can skip the mapping when the domain // objects' design is reviewed if (proxyPortModel) { proxyPortModel->setSourceModel(plm->getPortModel()); tvPortList->setModel(proxyPortModel); } else tvPortList->setModel(plm->getPortModel()); connect( plm->getPortModel(), SIGNAL(dataChanged(const QModelIndex&, const QModelIndex&)), this, SLOT(when_portModel_dataChanged(const QModelIndex&, const QModelIndex&))); connect(plm->getPortModel(), SIGNAL(modelReset()), SLOT(when_portModel_reset())); connect(actionPort_Configuration, SIGNAL(triggered()), SLOT(when_actionPort_Configuration_triggered())); connect(tvPortList, SIGNAL(activated(const QModelIndex&)), SLOT(when_actionPort_Configuration_triggered(const QModelIndex&))); connect(tvPortList->selectionModel(), SIGNAL(currentChanged(const QModelIndex&, const QModelIndex&)), this, SLOT(when_portView_currentChanged(const QModelIndex&, const QModelIndex&))); connect(this, SIGNAL(currentPortChanged(const QModelIndex&, const QModelIndex&)), portWidget, SLOT(setCurrentPortIndex(const QModelIndex&, const QModelIndex&))); connect(this, SIGNAL(currentPortChanged(const QModelIndex&, const QModelIndex&)), streamsWidget, SLOT(setCurrentPortIndex(const QModelIndex&))); connect(this, SIGNAL(currentPortChanged(const QModelIndex&, const QModelIndex&)), devicesWidget, SLOT(setCurrentPortIndex(const QModelIndex&))); // Initially we don't have any ports/streams/devices // - so send signal triggers when_portView_currentChanged(QModelIndex(), QModelIndex()); } PortsWindow::~PortsWindow() { delete proxyPortModel; delete applyMsg_; } int PortsWindow::portGroupCount() { return plm->numPortGroups(); } int PortsWindow::reservedPortCount() { int count = 0; int n = portGroupCount(); for (int i = 0; i < n; i++) count += plm->portGroupByIndex(i).numReservedPorts(); return count; } //! Always return true bool PortsWindow::openSession( const OstProto::SessionContent *session, QString & /*error*/) { QProgressDialog progress("Opening Session", NULL, 0, session->port_groups_size(), mainWindow); progress.show(); progress.setEnabled(true); // since parent (mainWindow) is disabled plm->removeAllPortGroups(); for (int i = 0; i < session->port_groups_size(); i++) { const OstProto::PortGroupContent &pgc = session->port_groups(i); PortGroup *pg = new PortGroup(QString::fromStdString( pgc.server_name()), quint16(pgc.server_port())); pg->setConfigAtConnect(&pgc); plm->addPortGroup(*pg); progress.setValue(i+1); } return true; } /*! * Prepare content to be saved for a session * * If port reservation is in use, saves only 'my' reserved ports * * Returns false, if user cancels op; true, otherwise */ bool PortsWindow::saveSession( OstProto::SessionContent *session, // OUT param QString & /*error*/, QProgressDialog *progress) { int n = portGroupCount(); QString myself; if (progress) { progress->setLabelText("Preparing Ports and PortGroups ..."); progress->setRange(0, n); } if (reservedPortCount()) myself = appSettings->value(kUserKey, kUserDefaultValue).toString(); for (int i = 0; i < n; i++) { PortGroup &pg = plm->portGroupByIndex(i); OstProto::PortGroupContent *pgc = session->add_port_groups(); pgc->set_server_name(pg.serverName().toStdString()); pgc->set_server_port(pg.serverPort()); for (int j = 0; j < pg.numPorts(); j++) { if (myself != pg.mPorts.at(j)->userName()) continue; OstProto::PortContent *pc = pgc->add_ports(); OstProto::Port *p = pc->mutable_port_config(); // XXX: We save the entire OstProto::Port even though some // fields may be ephemeral; while opening we use only relevant // fields pg.mPorts.at(j)->protoDataCopyInto(p); for (int k = 0; k < pg.mPorts.at(j)->numStreams(); k++) { OstProto::Stream *s = pc->add_streams(); pg.mPorts.at(j)->streamByIndex(k)->protoDataCopyInto(*s); } for (int k = 0; k < pg.mPorts.at(j)->numDeviceGroups(); k++) { OstProto::DeviceGroup *dg = pc->add_device_groups(); dg->CopyFrom(*(pg.mPorts.at(j)->deviceGroupByIndex(k))); } } if (progress) { if (progress->wasCanceled()) return false; progress->setValue(i); } if (i % 2 == 0) qApp->processEvents(); } return true; } QList PortsWindow::portActions() { return tvPortList->actions(); } QList PortsWindow::streamActions() { return streamsWidget->actions(); } QList PortsWindow::deviceActions() { return devicesWidget->actions(); } void PortsWindow::clearCurrentSelection() { tvPortList->selectionModel()->clearCurrentIndex(); tvPortList->clearSelection(); } void PortsWindow::showMyReservedPortsOnly(bool enabled) { if (!proxyPortModel) return; if (enabled) { QString rx = "Port Group|\\[" + QRegExp::escape(appSettings->value(kUserKey, kUserDefaultValue).toString()) + "\\]"; qDebug("%s: regexp: <%s>", __FUNCTION__, qPrintable(rx)); proxyPortModel->setFilterRegExp(QRegExp(rx)); } else proxyPortModel->setFilterRegExp(QRegExp("")); } void PortsWindow::when_portView_currentChanged(const QModelIndex& currentIndex, const QModelIndex& previousIndex) { QModelIndex current = currentIndex; QModelIndex previous = previousIndex; if (proxyPortModel) { current = proxyPortModel->mapToSource(current); previous = proxyPortModel->mapToSource(previous); } updatePortViewActions(currentIndex); qDebug("In %s", __FUNCTION__); if (previous.isValid() && plm->isPort(previous)) { disconnect(&(plm->port(previous)), SIGNAL(localConfigChanged(int, int, bool)), this, SLOT(updateApplyHint(int, int, bool))); } if (!current.isValid()) { qDebug("setting stacked widget to welcome page"); swDetail->setCurrentIndex(0); // welcome page } else { if (plm->isPortGroup(current)) { swDetail->setCurrentIndex(1); // portGroup detail page } else if (plm->isPort(current)) { swDetail->setCurrentIndex(2); // port detail page connect(&(plm->port(current)), SIGNAL(localConfigChanged(int, int, bool)), SLOT(updateApplyHint(int, int, bool))); if (plm->port(current).isDirty()) updateApplyHint(plm->port(current).portGroupId(), plm->port(current).id(), true); else if (plm->port(current).numStreams()) applyHint->setText("Click " "to transmit packets"); else applyHint->setText(""); } } emit currentPortChanged(current, previous); } void PortsWindow::when_portModel_dataChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight) { qDebug("In %s %d:(%d, %d) - %d:(%d, %d)", __FUNCTION__, topLeft.parent().isValid(), topLeft.row(), topLeft.column(), bottomRight.parent().isValid(), bottomRight.row(), bottomRight.column()); if (!topLeft.isValid() || !bottomRight.isValid()) return; if (topLeft.parent() != bottomRight.parent()) return; // If a port has changed, expand the port group if (topLeft.parent().isValid()) tvPortList->expand(proxyPortModel ? proxyPortModel->mapFromSource(topLeft.parent()) : topLeft.parent()); #if 0 // not sure why the >= <= operators are not overloaded in QModelIndex if ((tvPortList->currentIndex() >= topLeft) && (tvPortList->currentIndex() <= bottomRight)) #endif if (((topLeft < tvPortList->currentIndex()) || (topLeft == tvPortList->currentIndex())) && (((tvPortList->currentIndex() < bottomRight)) || (tvPortList->currentIndex() == bottomRight))) { // Update UI to reflect potential change in exclusive mode, // transmit mode et al when_portView_currentChanged(tvPortList->currentIndex(), tvPortList->currentIndex()); } } void PortsWindow::when_portModel_reset() { when_portView_currentChanged(QModelIndex(), tvPortList->currentIndex()); } void PortsWindow::updateApplyHint(int /*portGroupId*/, int /*portId*/, bool configChanged) { if (configChanged) applyHint->setText("Configuration has changed - " "click Apply " "to activate the changes"); else if (plm->getStreamModel()->rowCount() > 0) applyHint->setText("Configuration activated - " "click " "to transmit packets"); else applyHint->setText("Configuration activated"); } void PortsWindow::updatePortViewActions(const QModelIndex& currentIndex) { QModelIndex current = currentIndex; if (proxyPortModel) current = proxyPortModel->mapToSource(current); if (!current.isValid()) { qDebug("current is now invalid"); actionDelete_Port_Group->setDisabled(true); actionConnect_Port_Group->setDisabled(true); actionDisconnect_Port_Group->setDisabled(true); actionExclusive_Control->setDisabled(true); actionPort_Configuration->setDisabled(true); goto _EXIT; } qDebug("currentChanged %p", (void*)current.internalId()); if (plm->isPortGroup(current)) { actionDelete_Port_Group->setEnabled(true); actionExclusive_Control->setDisabled(true); actionPort_Configuration->setDisabled(true); switch(plm->portGroup(current).state()) { case QAbstractSocket::UnconnectedState: case QAbstractSocket::ClosingState: qDebug("state = unconnected|closing"); actionConnect_Port_Group->setEnabled(true); actionDisconnect_Port_Group->setDisabled(true); break; case QAbstractSocket::HostLookupState: case QAbstractSocket::ConnectingState: case QAbstractSocket::ConnectedState: qDebug("state = lookup|connecting|connected"); actionConnect_Port_Group->setDisabled(true); actionDisconnect_Port_Group->setEnabled(true); break; case QAbstractSocket::BoundState: case QAbstractSocket::ListeningState: default: // FIXME(LOW): indicate error qDebug("unexpected state"); break; } } else if (plm->isPort(current)) { actionDelete_Port_Group->setDisabled(true); actionConnect_Port_Group->setDisabled(true); actionDisconnect_Port_Group->setDisabled(true); actionExclusive_Control->setEnabled(true); if (plm->port(current).hasExclusiveControl()) actionExclusive_Control->setChecked(true); else actionExclusive_Control->setChecked(false); actionPort_Configuration->setEnabled(true); } _EXIT: return; } void PortsWindow::on_pbApply_clicked() { QModelIndex curPort; QModelIndex curPortGroup; curPort = tvPortList->selectionModel()->currentIndex(); if (proxyPortModel) curPort = proxyPortModel->mapToSource(curPort); if (!curPort.isValid()) { qDebug("%s: curPort is invalid", __FUNCTION__); goto _exit; } if (!plm->isPort(curPort)) { qDebug("%s: curPort is not a port", __FUNCTION__); goto _exit; } if (plm->port(curPort).getStats().state().is_transmit_on()) { QMessageBox::information(0, "Configuration Change", "Please stop transmit on the port before applying any changes"); goto _exit; } curPortGroup = plm->getPortModel()->parent(curPort); if (!curPortGroup.isValid()) { qDebug("%s: curPortGroup is invalid", __FUNCTION__); goto _exit; } if (!plm->isPortGroup(curPortGroup)) { qDebug("%s: curPortGroup is not a portGroup", __FUNCTION__); goto _exit; } disconnect(applyMsg_); connect(&(plm->portGroup(curPortGroup)), SIGNAL(applyFinished()), applyMsg_, SLOT(hide())); applyMsg_->show(); // FIXME(HI): shd this be a signal? //portGroup.when_configApply(port); // FIXME(MED): mixing port id and index!!! plm->portGroup(curPortGroup).when_configApply(plm->port(curPort).id()); _exit: return; #if 0 // TODO (LOW): This block is for testing only QModelIndex current = tvPortList->selectionModel()->currentIndex(); if (current.isValid()) qDebug("current = %llx", current.internalId()); else qDebug("current is invalid"); #endif } void PortsWindow::on_actionNew_Port_Group_triggered() { bool ok; QString text = QInputDialog::getText(this, "Add Port Group", "Port Group Address (HostName[:Port])", QLineEdit::Normal, lastNewPortGroup, &ok); if (ok) { QStringList addr = text.split(":"); quint16 port = DEFAULT_SERVER_PORT; if (addr.size() > 2) { // IPv6 Address // IPv6 addresses with port number SHOULD be specified as // [2001:db8::1]:80 (RFC5952 Sec6) to avoid ambiguity due to ':' addr = text.split("]:"); if (addr.size() > 1) port = addr[1].toUShort(); } else if (addr.size() == 2) // Hostname/IPv4 + Port specified port = addr[1].toUShort(); // Play nice and remove square brackets irrespective of addr type addr[0].remove(QChar('[')); addr[0].remove(QChar(']')); PortGroup *pg = new PortGroup(addr[0], port); plm->addPortGroup(*pg); lastNewPortGroup = text; } } void PortsWindow::on_actionDelete_Port_Group_triggered() { QModelIndex current = tvPortList->selectionModel()->currentIndex(); if (proxyPortModel) current = proxyPortModel->mapToSource(current); if (current.isValid()) plm->removePortGroup(plm->portGroup(current)); } void PortsWindow::on_actionConnect_Port_Group_triggered() { QModelIndex current = tvPortList->selectionModel()->currentIndex(); if (proxyPortModel) current = proxyPortModel->mapToSource(current); if (current.isValid()) plm->portGroup(current).connectToHost(); } void PortsWindow::on_actionDisconnect_Port_Group_triggered() { QModelIndex current = tvPortList->selectionModel()->currentIndex(); if (proxyPortModel) current = proxyPortModel->mapToSource(current); if (current.isValid()) plm->portGroup(current).disconnectFromHost(); } void PortsWindow::on_actionExclusive_Control_triggered(bool checked) { QModelIndex current = tvPortList->selectionModel()->currentIndex(); if (proxyPortModel) current = proxyPortModel->mapToSource(current); if (plm->isPort(current)) { OstProto::Port config; config.set_is_exclusive_control(checked); plm->portGroup(current.parent()).modifyPort(current.row(), config); } } void PortsWindow::when_actionPort_Configuration_triggered( const QModelIndex &portIndex) { QModelIndex current = portIndex.isValid() ? portIndex : tvPortList->selectionModel()->currentIndex(); if (proxyPortModel) current = proxyPortModel->mapToSource(current); if (!plm->isPort(current)) return; Port &port = plm->port(current); OstProto::Port config; // XXX: we don't call Port::protoDataCopyInto() to get config b'coz // we want only the modifiable fields populated to send to Drone // TODO: extend Port::protoDataCopyInto() to accept an optional param // which says copy only modifiable fields //plm->port(current).protoDataCopyInto(&config); config.set_description(port.systemDescription().toStdString()); config.set_user_description(port.userDescription().toStdString()); config.set_transmit_mode(port.transmitMode()); config.set_is_tracking_stream_stats(port.trackStreamStats()); config.set_is_exclusive_control(port.hasExclusiveControl()); config.set_user_name(port.userName().toStdString()); PortConfigDialog dialog(config, port.getStats().state(), this); if (dialog.exec() == QDialog::Accepted) plm->portGroup(current.parent()).modifyPort(current.row(), config); } ostinato-1.3.0/client/portswindow.h000066400000000000000000000051661451413623100174110ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _PORTS_WINDOW_H #define _PORTS_WINDOW_H #include "ui_portswindow.h" #include class ApplyMessage; class PortGroupList; class QProgressDialog; class QSortFilterProxyModel; namespace OstProto { class SessionContent; } class PortsWindow : public QWidget, private Ui::PortsWindow { Q_OBJECT public: PortsWindow(PortGroupList *pgl, QWidget *parent = 0); ~PortsWindow(); int portGroupCount(); int reservedPortCount(); bool openSession(const OstProto::SessionContent *session, QString &error); bool saveSession(OstProto::SessionContent *session, QString &error, QProgressDialog *progress = NULL); QList portActions(); QList streamActions(); QList deviceActions(); signals: void currentPortChanged(const QModelIndex ¤t, const QModelIndex &previous); public slots: void clearCurrentSelection(); void showMyReservedPortsOnly(bool enabled); private slots: void updateApplyHint(int portGroupId, int portId, bool configChanged); void updatePortViewActions(const QModelIndex& currentIndex); void when_portView_currentChanged(const QModelIndex& currentIndex, const QModelIndex& previousIndex); void when_portModel_dataChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight); void when_portModel_reset(); void on_pbApply_clicked(); void on_actionNew_Port_Group_triggered(); void on_actionDelete_Port_Group_triggered(); void on_actionConnect_Port_Group_triggered(); void on_actionDisconnect_Port_Group_triggered(); void on_actionExclusive_Control_triggered(bool checked); void when_actionPort_Configuration_triggered( const QModelIndex &portIndex = QModelIndex()); private: PortGroupList *plm; QString lastNewPortGroup; QSortFilterProxyModel *proxyPortModel; ApplyMessage *applyMsg_; }; #endif ostinato-1.3.0/client/portswindow.ui000066400000000000000000000250771451413623100176020ustar00rootroot00000000000000 PortsWindow 0 0 663 352 Form Qt::Horizontal false 1 0 Qt::ActionsContextMenu QAbstractItemView::SingleSelection 1 0 2 <p><b>Welcome to Ostinato!</b></p> <p>The port list on the left contains all the ports on which you can transmit packets.</p> <p>Ports belong to a port group. Make sure the Port Group has a <img src=":/icons/bullet_green.png"/> next to it, then double click the port group to show or hide the ports in the port group.</p> <p>To generate packets, you need to create and configure packet streams. A stream is a sequence of one or more packets.</p> <p>To create a stream, select the port on which you want to send packets.</p> <hr/> <p>Don't see the port that you want (or any ports at all) inside the port group? <a href="https://jump.ostinato.org/noports">Get Help!</a></p> Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop true true Qt::Vertical 20 40 <p>You have selected a port group in the port list on the left.</p> <p>You can transmit packets on any of the ports within the port group.</p> <p>Make sure the port group has a <img src=":/icons/bullet_green.png"/> next to it and then double click the port group to show or hide the ports in the port group.</p> <p>To generate packets, you need to create and configure packet streams. A stream is a sequence of one or more packets.</p> <p>To create a stream, select the port on which you want to send packets. </p> <hr/> <p>Don't see the port that you want (or any ports at all) inside the port group? <a href="https://jump.ostinato.org/noports">Get Help!</a></p> Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop true true Qt::Vertical 20 177 0 0 0 0 QFrame::Panel QFrame::Raised 3 3 3 3 Qt::Horizontal 40 20 Apply Hint Qt::Horizontal 40 20 Apply 0 Streams Devices :/icons/portgroup_add.png:/icons/portgroup_add.png New Port Group :/icons/portgroup_delete.png:/icons/portgroup_delete.png Delete Port Group :/icons/portgroup_connect.png:/icons/portgroup_connect.png Connect Port Group :/icons/portgroup_disconnect.png:/icons/portgroup_disconnect.png Disconnect Port Group true Exclusive Port Control (EXPERIMENTAL) Port Configuration ... DevicesWidget QWidget
deviceswidget.h
1
XTreeView QTreeView
xtreeview.h
StreamsWidget QWidget
streamswidget.h
1
PortWidget QWidget
portwidget.h
1
ostinato-1.3.0/client/portwidget.cpp000066400000000000000000000124351451413623100175320ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #include "portwidget.h" #include "portgrouplist.h" #include "xqlocale.h" #include PortWidget::PortWidget(QWidget *parent) : QWidget(parent) { setupUi(this); } void PortWidget::setPortGroupList(PortGroupList *portGroups) { plm = portGroups; connect(plm->getStreamModel(), SIGNAL(rowsInserted(QModelIndex, int, int)), SLOT(updatePortActions())); connect(plm->getStreamModel(), SIGNAL(rowsRemoved(QModelIndex, int, int)), SLOT(updatePortActions())); connect(plm->getStreamModel(), SIGNAL(modelReset()), SLOT(updatePortActions())); updatePortActions(); } PortWidget::~PortWidget() { } void PortWidget::setCurrentPortIndex(const QModelIndex ¤tIndex, const QModelIndex &previousIndex) { if (!plm) return; qDebug("In %s", __PRETTY_FUNCTION__); // XXX: We assume indices corresponds to sourceModel, not proxyModel // - caller/sender should ensure this // Disconnect previous port if (plm->isPort(previousIndex)) disconnect(&(plm->port(previousIndex)), SIGNAL(portRateChanged(int, int)), this, SLOT(updatePortRates())); if (!plm->isPort(currentIndex)) { currentPortIndex_ = QModelIndex(); // set to invalid return; } currentPortIndex_ = currentIndex; // Connect current port connect(&(plm->port(currentPortIndex_)), SIGNAL(portRateChanged(int, int)), this, SLOT(updatePortRates())); double speed = plm->port(currentPortIndex_).speed(); portSpeed->setText(QString("Max %L1 Mbps").arg(speed)); rbLoad->setVisible(speed > 0); averageLoadPercent->setVisible(speed > 0); speedSep->setVisible(speed > 0); portSpeed->setVisible(speed > 0); updatePortRates(); updatePortActions(); } void PortWidget::on_startTx_clicked() { Q_ASSERT(plm->isPort(currentPortIndex_)); QModelIndex curPortGroup = plm->getPortModel()->parent(currentPortIndex_); Q_ASSERT(curPortGroup.isValid()); Q_ASSERT(plm->isPortGroup(curPortGroup)); QList portList({plm->port(currentPortIndex_).id()}); plm->portGroup(curPortGroup).startTx(&portList); } void PortWidget::on_stopTx_clicked() { Q_ASSERT(plm->isPort(currentPortIndex_)); QModelIndex curPortGroup = plm->getPortModel()->parent(currentPortIndex_); Q_ASSERT(curPortGroup.isValid()); Q_ASSERT(plm->isPortGroup(curPortGroup)); QList portList({plm->port(currentPortIndex_).id()}); plm->portGroup(curPortGroup).stopTx(&portList); } void PortWidget::on_averageLoadPercent_editingFinished() { Q_ASSERT(plm->isPort(currentPortIndex_)); plm->port(currentPortIndex_).setAverageLoadRate( averageLoadPercent->value()/100); } void PortWidget::on_averagePacketsPerSec_editingFinished() { Q_ASSERT(plm->isPort(currentPortIndex_)); bool isOk; double pps = XLocale().toPacketsPerSecond(averagePacketsPerSec->text(), &isOk); if (isOk) plm->port(currentPortIndex_).setAveragePacketRate(pps); else updatePortRates(); } void PortWidget::on_averageBitsPerSec_editingFinished() { Q_ASSERT(plm->isPort(currentPortIndex_)); bool isOk; double bps = XLocale().toBitsPerSecond(averageBitsPerSec->text(), &isOk); if (isOk) plm->port(currentPortIndex_).setAverageBitRate(bps); else updatePortRates(); } void PortWidget::updatePortRates() { if (!currentPortIndex_.isValid()) return; if (!plm->isPort(currentPortIndex_)) return; // XXX: pps/bps input widget is a LineEdit and not a SpinBox // because we want users to be able to enter values in various // units e.g. "1.5 Mbps", "1000K", "50" (bps) etc. // XXX: It's a considered decision NOT to show frame rate in // higher units of Kpps and Mpps as most users may not be // familiar with those and also we want frame rate to have a // high resolution for input e.g. if user enters 1,488,095.2381 // it should NOT be shown as 1.4881 Mpps averagePacketsPerSec->setText(QString("%L1 pps") .arg(plm->port(currentPortIndex_).averagePacketRate(), 0, 'f', 4)); averageBitsPerSec->setText(XLocale().toBitRateString( plm->port(currentPortIndex_).averageBitRate())); averageLoadPercent->setValue( plm->port(currentPortIndex_).averageLoadRate()*100); } void PortWidget::updatePortActions() { if (!plm->isPort(currentPortIndex_)) return; startTx->setEnabled(plm->port(currentPortIndex_).numStreams() > 0); stopTx->setEnabled(plm->port(currentPortIndex_).numStreams() > 0); } ostinato-1.3.0/client/portwidget.h000066400000000000000000000030331451413623100171710ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _PORT_WIDGET_H #define _PORT_WIDGET_H #include "ui_portwidget.h" #include #include class PortGroupList; class PortWidget : public QWidget, private Ui::PortWidget { Q_OBJECT public: PortWidget(QWidget *parent = 0); ~PortWidget(); void setPortGroupList(PortGroupList *portGroups); public slots: void setCurrentPortIndex(const QModelIndex ¤tIndex, const QModelIndex &previousIndex); private slots: void on_startTx_clicked(); void on_stopTx_clicked(); void on_averageLoadPercent_editingFinished(); void on_averagePacketsPerSec_editingFinished(); void on_averageBitsPerSec_editingFinished(); void updatePortActions(); void updatePortRates(); private: PortGroupList *plm{nullptr}; // FIXME: rename to portGroups_? QModelIndex currentPortIndex_; }; #endif ostinato-1.3.0/client/portwidget.ui000066400000000000000000000126401451413623100173630ustar00rootroot00000000000000 PortWidget 0 0 806 73 Form 0 0 0 0 Start Transmit Start transmit on selected port :/icons/control_play.png:/icons/control_play.png Stop Transmit Stop transmit on selected port :/icons/control_stop.png:/icons/control_stop.png Qt::Horizontal 40 20 Qt::Vertical Frame Rate true Bit Rate false Bit rate on the line including overhead such as Preamble, IPG, FCS etc. Load false QAbstractSpinBox::NoButtons % 4 999.999900000000025 Qt::Vertical Port Speed Max speed radioButton toggled(bool) averagePacketsPerSec setEnabled(bool) 450 44 593 45 radioButton_2 toggled(bool) averageBitsPerSec setEnabled(bool) 661 44 804 45 rbLoad toggled(bool) averageLoadPercent setEnabled(bool) 281 43 308 45 ostinato-1.3.0/client/preferences.cpp000066400000000000000000000124631451413623100176440ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #include "preferences.h" #include "../common/ostprotolib.h" #include "settings.h" #include "thememanager.h" #include #include #include #if defined(Q_OS_WIN32) QString kGzipPathDefaultValue; QString kDiffPathDefaultValue; QString kAwkPathDefaultValue; #endif QString kUserDefaultValue; Preferences::Preferences() { Q_ASSERT(appSettings); setupUi(this); // Program paths wiresharkPathEdit->setText(appSettings->value(kWiresharkPathKey, kWiresharkPathDefaultValue).toString()); tsharkPathEdit->setText(appSettings->value(kTsharkPathKey, kTsharkPathDefaultValue).toString()); gzipPathEdit->setText(appSettings->value(kGzipPathKey, kGzipPathDefaultValue).toString()); diffPathEdit->setText(appSettings->value(kDiffPathKey, kDiffPathDefaultValue).toString()); awkPathEdit->setText(appSettings->value(kAwkPathKey, kAwkPathDefaultValue).toString()); // Theme theme->addItems(ThemeManager::instance()->themeList()); theme->setCurrentText(appSettings->value(kThemeKey).toString()); // TODO(only if required): kUserKey } Preferences::~Preferences() { } void Preferences::initDefaults() { #if defined(Q_OS_WIN32) kGzipPathDefaultValue = QApplication::applicationDirPath() + "/gzip.exe"; kDiffPathDefaultValue = QApplication::applicationDirPath() + "/diff.exe"; kAwkPathDefaultValue = QApplication::applicationDirPath() + "/gawk.exe"; #endif // Read default username from the environment #ifdef Q_OS_WIN32 kUserDefaultValue = QString(qgetenv("USERNAME").constData()); #else kUserDefaultValue = QString(qgetenv("USER").constData()); #endif qDebug("current user <%s>", qPrintable(kUserDefaultValue)); } void Preferences::accept() { // Program paths appSettings->setValue(kWiresharkPathKey, wiresharkPathEdit->text()); appSettings->setValue(kTsharkPathKey, tsharkPathEdit->text()); appSettings->setValue(kGzipPathKey, gzipPathEdit->text()); appSettings->setValue(kDiffPathKey, diffPathEdit->text()); appSettings->setValue(kAwkPathKey, awkPathEdit->text()); OstProtoLib::setExternalApplicationPaths( appSettings->value(kTsharkPathKey, kTsharkPathDefaultValue).toString(), appSettings->value(kGzipPathKey, kGzipPathDefaultValue).toString(), appSettings->value(kDiffPathKey, kDiffPathDefaultValue).toString(), appSettings->value(kAwkPathKey, kAwkPathDefaultValue).toString()); // Theme ThemeManager::instance()->setTheme(theme->currentText()); QDialog::accept(); } void Preferences::on_wiresharkPathButton_clicked() { QString path; path = QFileDialog::getOpenFileName(0, "Locate Wireshark", wiresharkPathEdit->text()); #ifdef Q_OS_MAC // Find executable inside app bundle using Info.plist if (!path.isEmpty() && path.endsWith(".app")) { QFile plist(path+"/Contents/Info.plist"); plist.open(QIODevice::ReadOnly); QXmlStreamReader xml(&plist); while (!xml.atEnd()) { xml.readNext(); if (xml.isStartElement() && (xml.name() == "key") && (xml.readElementText() == "CFBundleExecutable")) { xml.readNext(); // xml.readNext(); // if (xml.isStartElement() && (xml.name() == "string")) path = path+"/Contents/MacOs/"+xml.readElementText(); break; } if (xml.hasError()) qDebug("%lld:%lld Error reading Info.plist: %s", xml.lineNumber(), xml.columnNumber(), qPrintable(xml.errorString())); } } #endif if (!path.isEmpty()) wiresharkPathEdit->setText(path); } void Preferences::on_tsharkPathButton_clicked() { QString path; path = QFileDialog::getOpenFileName(0, "Locate tshark", tsharkPathEdit->text()); if (!path.isEmpty()) tsharkPathEdit->setText(path); } void Preferences::on_gzipPathButton_clicked() { QString path; path = QFileDialog::getOpenFileName(0, "Locate gzip", gzipPathEdit->text()); if (!path.isEmpty()) gzipPathEdit->setText(path); } void Preferences::on_diffPathButton_clicked() { QString path; path = QFileDialog::getOpenFileName(0, "Locate diff", diffPathEdit->text()); if (!path.isEmpty()) diffPathEdit->setText(path); } void Preferences::on_awkPathButton_clicked() { QString path; path = QFileDialog::getOpenFileName(0, "Locate awk", awkPathEdit->text()); if (!path.isEmpty()) awkPathEdit->setText(path); } ostinato-1.3.0/client/preferences.h000066400000000000000000000022361451413623100173060ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _PREFERENCES_H #define _PREFERENCES_H #include "ui_preferences.h" #include class Preferences : public QDialog, private Ui::Preferences { Q_OBJECT public: Preferences(); ~Preferences(); static void initDefaults(); public slots: void accept(); private slots: void on_wiresharkPathButton_clicked(); void on_tsharkPathButton_clicked(); void on_gzipPathButton_clicked(); void on_diffPathButton_clicked(); void on_awkPathButton_clicked(); }; #endif ostinato-1.3.0/client/preferences.ui000066400000000000000000000145231451413623100174760ustar00rootroot00000000000000 Preferences 0 0 400 220 Preferences :/icons/preferences.png:/icons/preferences.png QFrame::Box QFrame::Sunken 'wireshark' Path wiresharkPathEdit true ... 'tshark' Path tsharkPathEdit true ... 'gzip' Path diffPathEdit true ... 'diff' Path diffPathEdit true ... 'awk' Path awkPathEdit true ... Qt::Vertical 21 61 Theme (Experimental) Qt::Horizontal QDialogButtonBox::Cancel|QDialogButtonBox::Ok wiresharkPathEdit wiresharkPathButton tsharkPathEdit tsharkPathButton gzipPathEdit gzipPathButton diffPathEdit diffPathButton awkPathEdit awkPathButton buttonBox buttonBox accepted() Preferences accept() 248 254 157 274 buttonBox rejected() Preferences reject() 316 260 286 274 ostinato-1.3.0/client/rowborderdelegate.h000066400000000000000000000024631451413623100205070ustar00rootroot00000000000000/* Copyright (C) 2023 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _ROW_BORDER_DELEGATE #define _ROW_BORDER_DELEGATE #include #include class RowBorderDelegate : public QStyledItemDelegate { public: RowBorderDelegate(QSet rows, QObject *parent = nullptr) : QStyledItemDelegate(parent), rows_(rows) { } private: void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const { QStyledItemDelegate::paint(painter, option, index); if (rows_.contains(index.row())) { const QRect rect(option.rect); painter->drawLine(rect.topLeft(), rect.topRight()); } } QSet rows_; }; #endif ostinato-1.3.0/client/settings.h000066400000000000000000000050731451413623100166470ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _SETTINGS_H #define _SETTINGS_H #include #include extern QSettings *appSettings; const QString kWiresharkPathKey("WiresharkPath"); #if defined(Q_OS_WIN32) const QString kWiresharkPathDefaultValue( "C:/Program Files/Wireshark/wireshark.exe"); #elif defined(Q_OS_MAC) const QString kWiresharkPathDefaultValue( "/Applications/Wireshark.app/Contents/MacOS/Wireshark"); #else const QString kWiresharkPathDefaultValue("/usr/bin/wireshark"); #endif const QString kTsharkPathKey("TsharkPath"); #if defined(Q_OS_WIN32) const QString kTsharkPathDefaultValue( "C:/Program Files/Wireshark/tshark.exe"); #elif defined(Q_OS_MAC) const QString kTsharkPathDefaultValue( "/Applications/Wireshark.app/Contents/Resources/bin/tshark"); #else const QString kTsharkPathDefaultValue("/usr/bin/tshark"); #endif const QString kGzipPathKey("GzipPath"); #if defined(Q_OS_WIN32) extern QString kGzipPathDefaultValue; #elif defined(Q_OS_MAC) const QString kGzipPathDefaultValue("/usr/bin/gzip"); #else const QString kGzipPathDefaultValue("/usr/bin/gzip"); #endif const QString kDiffPathKey("DiffPath"); #if defined(Q_OS_WIN32) extern QString kDiffPathDefaultValue; #elif defined(Q_OS_MAC) const QString kDiffPathDefaultValue("/usr/bin/diff"); #else const QString kDiffPathDefaultValue("/usr/bin/diff"); #endif const QString kAwkPathKey("AwkPath"); #if defined(Q_OS_WIN32) extern QString kAwkPathDefaultValue; #elif defined(Q_OS_MAC) const QString kAwkPathDefaultValue("/usr/bin/awk"); #else const QString kAwkPathDefaultValue("/usr/bin/awk"); #endif const QString kThemeKey("Theme"); const QString kUserKey("User"); extern QString kUserDefaultValue; // // LastUse Section Keys // const QString kApplicationWindowGeometryKey("LastUse/ApplicationWindowGeometry"); const QString kApplicationWindowLayout("LastUse/ApplicationWindowLayout"); const QString kLastUpdateCheck("LastUse/UpdateCheck"); #endif ostinato-1.3.0/client/stream.cpp000066400000000000000000000026141451413623100166330ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ /*! * \todo Remove this class */ #include #include #include "stream.h" #include "../common/protocollistiterator.h" #include "../common/abstractprotocol.h" Stream::Stream() { //mId = 0xFFFFFFFF; setEnabled(true); } Stream::~Stream() { } void Stream::loadProtocolWidgets() { qWarning("%s: DOES NOTHING", __PRETTY_FUNCTION__); return; } void Stream::storeProtocolWidgets() { qWarning("%s: DOES NOTHING", __PRETTY_FUNCTION__); return; } quint64 getDeviceMacAddress( int /*portId*/, int /*streamId*/, int /*frameIndex*/) { return 0; } quint64 getNeighborMacAddress( int /*portId*/, int /*streamId*/, int /*frameIndex*/) { return 0; } ostinato-1.3.0/client/stream.h000066400000000000000000000020031451413623100162700ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _STREAM_H #define _STREAM_H #include #include #include #include "../common/protocol.pb.h" #include "../common/streambase.h" class Stream : public StreamBase { //quint32 mId; public: Stream(); ~Stream(); void loadProtocolWidgets(); void storeProtocolWidgets(); }; #endif ostinato-1.3.0/client/streamconfigdialog.cpp000066400000000000000000001201061451413623100211760ustar00rootroot00000000000000/* Copyright (C) 2010-2011 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #include #include "streamconfigdialog.h" #include "stream.h" #include "abstractprotocol.h" #include "abstractprotocolconfig.h" #include "protocollistiterator.h" #include "modeltest.h" #include "../common/protocolmanager.h" #include "../common/protocolwidgetfactory.h" #include "xqlocale.h" #include #include extern ProtocolManager *OstProtocolManager; extern ProtocolWidgetFactory *OstProtocolWidgetFactory; QRect StreamConfigDialog::lastGeometry; int StreamConfigDialog::lastTopLevelTabIndex = 0; int StreamConfigDialog::lastProtocolDataIndex = 0; static const uint kEthFrameOverHead = 20; StreamConfigDialog::StreamConfigDialog( QList &streamList, const Port &port, QWidget *parent) : QDialog (parent), _userStreamList(streamList), mPort(port) { mCurrentStreamIndex = 0; Q_ASSERT(_userStreamList.size() > 0); // Create a copy of the user provided stream list // We need a copy because user may edit multiple streams and then // discard the edit - in this case the user provided stream list // should not be modified on return foreach(Stream* stm, _userStreamList) { OstProto::Stream s; stm->protoDataCopyInto(s); _streamList.append(new Stream()); _streamList.last()->protoDataCopyFrom(s); } mpStream = _streamList.at(mCurrentStreamIndex); _iter = mpStream->createProtocolListIterator(); isUpdateInProgress = false; setupUi(this); setupUiExtra(); _windowTitle = windowTitle(); for (int i = ProtoMin; i < ProtoMax; i++) { bgProto[i]->setProperty("ProtocolLevel", i); bgProto[i]->setProperty("ProtocolId", ButtonIdNone); connect(bgProto[i], SIGNAL(buttonClicked(int)), this, SLOT(updateProtocol(int))); } //! \todo causes a crash! #if 0 connect(lePktLen, SIGNAL(textEdited(QString)), this, SLOT(updateContents())); #endif // Time to play match the signals and slots! // If L1/L2(FT)/L3/L4 = None, // force subsequent protocol level(s) also to None connect(rbL1None, SIGNAL(toggled(bool)), SLOT(forceProtocolNone(bool))); connect(rbFtNone, SIGNAL(toggled(bool)), SLOT(forceProtocolNone(bool))); connect(rbL3None, SIGNAL(toggled(bool)), SLOT(forceProtocolNone(bool))); connect(rbL4None, SIGNAL(toggled(bool)), SLOT(forceProtocolNone(bool))); // If L1/L2(FT)/L3/L4/L5 = Other, force subsequent protocol to Other and // disable the subsequent protocol group as well connect(rbL1Other, SIGNAL(toggled(bool)), rbFtOther, SLOT(setChecked(bool))); connect(rbL1Other, SIGNAL(toggled(bool)), gbFrameType, SLOT(setDisabled(bool))); connect(rbFtOther, SIGNAL(toggled(bool)), rbL3Other, SLOT(setChecked(bool))); connect(rbFtOther, SIGNAL(toggled(bool)), gbL3Proto, SLOT(setDisabled(bool))); connect(rbL3Other, SIGNAL(toggled(bool)), rbL4Other, SLOT(setChecked(bool))); connect(rbL3Other, SIGNAL(toggled(bool)), gbL4Proto, SLOT(setDisabled(bool))); connect(rbL4Other, SIGNAL(toggled(bool)), rbL5Other, SLOT(setChecked(bool))); connect(rbL4Other, SIGNAL(toggled(bool)), gbL5Proto, SLOT(setDisabled(bool))); connect(rbL5Other, SIGNAL(toggled(bool)), rbPayloadOther, SLOT(setChecked(bool))); connect(rbL5Other, SIGNAL(toggled(bool)), gbPayloadProto, SLOT(setDisabled(bool))); // Setup valid subsequent protocols for L2 to L4 protocols for (int i = ProtoL2; i <= ProtoL4; i++) { foreach(QAbstractButton *btn1, bgProto[i]->buttons()) { int id1 = bgProto[i]->id(btn1); if (id1 != ButtonIdNone && id1 != ButtonIdOther) { int validProtocolCount = 0; foreach(QAbstractButton *btn2, bgProto[i+1]->buttons()) { int id2 = bgProto[i+1]->id(btn2); if (id2 != ButtonIdNone && id2 != ButtonIdOther) { if (OstProtocolManager->isValidNeighbour(id1, id2)) { connect(btn1, SIGNAL(toggled(bool)), btn2, SLOT(setEnabled(bool))); validProtocolCount++; } else connect(btn1, SIGNAL(toggled(bool)), btn2, SLOT(setDisabled(bool))); } } // If btn1 has no subsequent valid protocols, // force subsequent Protocol to 'None' if (validProtocolCount == 0) connect(btn1, SIGNAL(clicked(bool)), bgProto[i+1]->button(ButtonIdNone), SLOT(click())); // If the id1 protocol doesn't have a payload (e.g. IGMP) // force payload protocol to 'None' if (!OstProtocolManager->protocolHasPayload(id1)) { connect(btn1, SIGNAL(clicked(bool)), bgProto[ProtoPayload]->button(ButtonIdNone), SLOT(click())); } } } } mpAvailableProtocolsModel = new QStringListModel( OstProtocolManager->protocolDatabase(), this); lvAllProtocols->setModel(mpAvailableProtocolsModel); lvAllProtocols->setEditTriggers(QAbstractItemView::NoEditTriggers); mpSelectedProtocolsModel = new QStringListModel(this); lvSelectedProtocols->setModel(mpSelectedProtocolsModel); lvSelectedProtocols->setEditTriggers(QAbstractItemView::NoEditTriggers); connect(lvAllProtocols->selectionModel(), SIGNAL(selectionChanged(const QItemSelection&, const QItemSelection&)), this, SLOT(when_lvAllProtocols_selectionChanged( const QItemSelection&, const QItemSelection&))); connect(lvSelectedProtocols->selectionModel(), SIGNAL(currentChanged(const QModelIndex&, const QModelIndex&)), this, SLOT(when_lvSelectedProtocols_currentChanged(const QModelIndex&, const QModelIndex&))); LoadCurrentStream(); mpPacketModel = new PacketModel(this); tvPacketTree->setModel(mpPacketModel); #if defined(QT_NO_DEBUG) || QT_VERSION < 0x050700 mpPacketModelTester = NULL; #else mpPacketModelTester = new ModelTest(mpPacketModel); #endif tvPacketTree->header()->hide(); vwPacketDump->setModel(mpPacketModel); vwPacketDump->setSelectionModel(tvPacketTree->selectionModel()); pbPrev->setDisabled(mCurrentStreamIndex == 0); pbNext->setDisabled(int(mCurrentStreamIndex) == (_streamList.size()-1)); //! \todo Support Goto Stream Id leStreamId->setHidden(true); disconnect(rbActionGotoStream, SIGNAL(toggled(bool)), leStreamId, SLOT(setEnabled(bool))); switch(mPort.transmitMode()) { case OstProto::kSequentialTransmit: rbModeFixed->setChecked(true); rbModeContinuous->setDisabled(true); break; case OstProto::kInterleavedTransmit: rbModeContinuous->setChecked(true); rbModeFixed->setDisabled(true); nextWhat->setHidden(true); break; default: Q_ASSERT(false); // Unreachable } // Finally, restore the saved last geometry and selected tab for the // various tab widgets if (!lastGeometry.isNull()) setGeometry(lastGeometry); twTopLevel->setCurrentIndex(lastTopLevelTabIndex); } void StreamConfigDialog::setupUiExtra() { QRegExp reHex2B("[0-9,a-f,A-F]{1,4}"); QRegExp reHex4B("[0-9,a-f,A-F]{1,8}"); QRegExp reMac("([0-9,a-f,A-F]{2,2}[:-]){5,5}[0-9,a-f,A-F]{2,2}"); // ---- Setup default stuff that cannot be done in designer ---- bgProto[ProtoL1] = new QButtonGroup(); bgProto[ProtoL1]->addButton(rbL1None, ButtonIdNone); bgProto[ProtoL1]->addButton(rbL1Mac, OstProto::Protocol::kMacFieldNumber); bgProto[ProtoL1]->addButton(rbL1Other, ButtonIdOther); bgProto[ProtoL2] = new QButtonGroup(); #if 0 foreach(QRadioButton *btn, gbFrameType->findChildren()) bgL2Proto->addButton(btn); #else bgProto[ProtoL2]->addButton(rbFtNone, ButtonIdNone); bgProto[ProtoL2]->addButton(rbFtEthernet2, OstProto::Protocol::kEth2FieldNumber); bgProto[ProtoL2]->addButton(rbFt802Dot3Raw, OstProto::Protocol::kDot3FieldNumber); bgProto[ProtoL2]->addButton(rbFt802Dot3Llc, OstProto::Protocol::kDot2LlcFieldNumber); bgProto[ProtoL2]->addButton(rbFtLlcSnap, OstProto::Protocol::kDot2SnapFieldNumber); bgProto[ProtoL2]->addButton(rbFtOther, ButtonIdOther); #endif bgProto[ProtoVlan] = new QButtonGroup(); bgProto[ProtoVlan]->addButton(rbVlanNone, ButtonIdNone); bgProto[ProtoVlan]->addButton(rbVlanSingle, OstProto::Protocol::kVlanFieldNumber); bgProto[ProtoVlan]->addButton(rbVlanDouble, OstProto::Protocol::kVlanStackFieldNumber); bgProto[ProtoL3] = new QButtonGroup(); #if 0 foreach(QRadioButton *btn, gbL3Proto->findChildren()) bgProto[ProtoL3]->addButton(btn); #else bgProto[ProtoL3]->addButton(rbL3None, ButtonIdNone); bgProto[ProtoL3]->addButton(rbL3Arp, OstProto::Protocol::kArpFieldNumber); bgProto[ProtoL3]->addButton(rbL3Ipv4, OstProto::Protocol::kIp4FieldNumber); bgProto[ProtoL3]->addButton(rbL3Ipv6, OstProto::Protocol::kIp6FieldNumber); bgProto[ProtoL3]->addButton(rbL3Ip6over4, OstProto::Protocol::kIp6over4FieldNumber); bgProto[ProtoL3]->addButton(rbL3Ip4over6, OstProto::Protocol::kIp4over6FieldNumber); bgProto[ProtoL3]->addButton(rbL3Ip4over4, OstProto::Protocol::kIp4over4FieldNumber); bgProto[ProtoL3]->addButton(rbL3Ip6over6, OstProto::Protocol::kIp6over6FieldNumber); bgProto[ProtoL3]->addButton(rbL3Other, ButtonIdOther); #endif bgProto[ProtoL4] = new QButtonGroup(); #if 0 foreach(QRadioButton *btn, gbL4Proto->findChildren()) bgProto[ProtoL4]->addButton(btn); #else bgProto[ProtoL4]->addButton(rbL4None, ButtonIdNone); bgProto[ProtoL4]->addButton(rbL4Tcp, OstProto::Protocol::kTcpFieldNumber); bgProto[ProtoL4]->addButton(rbL4Udp, OstProto::Protocol::kUdpFieldNumber); bgProto[ProtoL4]->addButton(rbL4Icmp, OstProto::Protocol::kIcmpFieldNumber); bgProto[ProtoL4]->addButton(rbL4Igmp, OstProto::Protocol::kIgmpFieldNumber); bgProto[ProtoL4]->addButton(rbL4Mld, OstProto::Protocol::kMldFieldNumber); bgProto[ProtoL4]->addButton(rbL4Other, ButtonIdOther); #endif bgProto[ProtoL5] = new QButtonGroup(); #if 0 foreach(QRadioButton *btn, gbL5Proto->findChildren()) bgProto[ProtoL5]->addButton(btn); #else bgProto[ProtoL5]->addButton(rbL5None, ButtonIdNone); bgProto[ProtoL5]->addButton(rbL5Text, OstProto::Protocol::kTextProtocolFieldNumber); bgProto[ProtoL5]->addButton(rbL5Other, ButtonIdOther); #endif bgProto[ProtoPayload] = new QButtonGroup(); #if 0 foreach(QRadioButton *btn, gbPayloadProto->findChildren()) bgProto[ProtoPayload]->addButton(btn); #else bgProto[ProtoPayload]->addButton(rbPayloadNone, ButtonIdNone); bgProto[ProtoPayload]->addButton(rbPayloadPattern, OstProto::Protocol::kPayloadFieldNumber); bgProto[ProtoPayload]->addButton(rbPayloadHexDump, OstProto::Protocol::kHexDumpFieldNumber); bgProto[ProtoPayload]->addButton(rbPayloadOther, ButtonIdOther); #endif // Special bgProto[ProtoSign] = new QButtonGroup(); bgProto[ProtoSign]->addButton(rbSpecialNone, ButtonIdNone); bgProto[ProtoSign]->addButton(rbSignature, OstProto::Protocol::kSignFieldNumber); // Trailer bgProto[ProtoTrailer] = new QButtonGroup(); bgProto[ProtoTrailer]->addButton(rbTrailerNone, ButtonIdNone); bgProto[ProtoTrailer]->addButton(rbTrailerOther, ButtonIdOther); /* ** Setup Validators */ // Meta Data lePktLen->setValidator(new QIntValidator(MIN_PKT_LEN, MAX_PKT_LEN, this)); lePktLenMin->setValidator(new QIntValidator(MIN_PKT_LEN, MAX_PKT_LEN,this)); lePktLenMax->setValidator(new QIntValidator(MIN_PKT_LEN, MAX_PKT_LEN,this)); lePacketsPerBurst->setValidator(new QIntValidator(1, 0x7FFFFFFF,this)); /* ** Setup Connections */ connect(rbSendPackets, SIGNAL(toggled(bool)), this, SLOT(update_NumPacketsAndNumBursts())); connect(rbSendBursts, SIGNAL(toggled(bool)), this, SLOT(update_NumPacketsAndNumBursts())); connect(rbModeFixed, SIGNAL(toggled(bool)), this, SLOT(update_NumPacketsAndNumBursts())); connect(rbModeContinuous, SIGNAL(toggled(bool)), this, SLOT(update_NumPacketsAndNumBursts())); } StreamConfigDialog::~StreamConfigDialog() { delete mpPacketModelTester; delete mpPacketModel; for (int i = ProtoMin; i < ProtoMax; i++) delete bgProto[i]; foreach (AbstractProtocolConfigForm* w, _protocolWidgets) { OstProtocolWidgetFactory->deleteConfigWidget(w); } delete _iter; while (!_streamList.isEmpty()) delete _streamList.takeFirst(); } void StreamConfigDialog::setWindowTitle(const QString &title) { _windowTitle = title; QDialog::setWindowTitle(title); } void StreamConfigDialog::loadProtocolWidgets() { ProtocolListIterator *iter; // NOTE: Protocol Widgets are created on demand. Once created we // store them in _protocolWidgets indexed by the protocol // object's address (to ensure unique widgets for multiple // objects of the same class). Subsequently we check // _protocolWidgets before creating a new widget iter = mpStream->createProtocolListIterator(); while (iter->hasNext()) { AbstractProtocol* p = iter->next(); AbstractProtocolConfigForm *w = _protocolWidgets.value(p); if (!w) { w = OstProtocolWidgetFactory->createConfigWidget( p->protocolNumber()); _protocolWidgets.insert(p, w); } w->loadWidget(p); } delete iter; } void StreamConfigDialog::storeProtocolWidgets() { ProtocolListIterator *iter; // NOTE: After creating a widget, we need to call loadWidget() // to load the protocol's default values iter = mpStream->createProtocolListIterator(); while (iter->hasNext()) { AbstractProtocol* p = iter->next(); AbstractProtocolConfigForm *w = _protocolWidgets.value(p); if (!w) { w = OstProtocolWidgetFactory->createConfigWidget( p->protocolNumber()); w->loadWidget(p); _protocolWidgets.insert(p, w); } w->storeWidget(p); } delete iter; } void StreamConfigDialog::on_cmbPktLenMode_currentIndexChanged(QString mode) { if (mode == "Fixed") { lePktLen->setEnabled(true); lePktLenMin->setDisabled(true); lePktLenMax->setDisabled(true); } else if (mode == "Increment") { lePktLen->setDisabled(true); lePktLenMin->setEnabled(true); lePktLenMax->setEnabled(true); } else if (mode == "Decrement") { lePktLen->setDisabled(true); lePktLenMin->setEnabled(true); lePktLenMax->setEnabled(true); } else if (mode == "Random") { lePktLen->setDisabled(true); lePktLenMin->setEnabled(true); lePktLenMax->setEnabled(true); } else if (mode == "IMIX") { lePktLen->setDisabled(true); lePktLenMin->setDisabled(true); lePktLenMax->setDisabled(true); } else { qWarning("Unhandled/Unknown PktLenMode = %s", qPrintable(mode)); } } void StreamConfigDialog::on_tbSelectProtocols_currentChanged(int index) { qDebug("%s, index = %d", __FUNCTION__, index); switch (index) { case 0: updateSelectProtocolsSimpleWidget(); break; case 1: updateSelectProtocolsAdvancedWidget(); break; default: qFatal("%s: unexpected index = %d", __FUNCTION__, index); } } void StreamConfigDialog::when_lvAllProtocols_selectionChanged( const QItemSelection &/*selected*/, const QItemSelection &/*deselected*/) { int size = lvAllProtocols->selectionModel()->selectedIndexes().size(); qDebug("%s: selected.indexes().size = %d\n", __FUNCTION__, size); tbAdd->setEnabled(size > 0); } void StreamConfigDialog::when_lvSelectedProtocols_currentChanged( const QModelIndex ¤t, const QModelIndex &/*previous*/) { qDebug("%s: currentRow = %d\n", __FUNCTION__, current.row()); tbDelete->setEnabled(current.isValid()); tbUp->setEnabled(current.isValid() && (current.row() != 0)); tbDown->setEnabled(current.isValid() && (current.row() != (current.model()->rowCount() - 1))); } void StreamConfigDialog::on_tbAdd_clicked() { int n = 0; QModelIndex idx2; QModelIndexList selection; selection = lvAllProtocols->selectionModel()->selectedIndexes(); // Validation if (selection.size() == 0) return; idx2 = lvSelectedProtocols->currentIndex(); if (idx2.isValid()) n = idx2.row(); _iter->toFront(); while (n--) { if (!_iter->hasNext()) return; _iter->next(); } foreach(QModelIndex idx, selection) _iter->insert(OstProtocolManager->createProtocol( mpAvailableProtocolsModel->stringList().at(idx.row()), mpStream)); updateSelectProtocolsAdvancedWidget(); lvSelectedProtocols->setCurrentIndex(idx2); } void StreamConfigDialog::on_tbDelete_clicked() { int n; QModelIndex idx; AbstractProtocol *p = NULL; idx = lvSelectedProtocols->currentIndex(); // Validation if (!idx.isValid()) return; n = idx.row() + 1; _iter->toFront(); while (n--) { if (!_iter->hasNext()) return; p = _iter->next(); } Q_CHECK_PTR(p); _iter->remove(); // Free both protocol and associated widget delete _protocolWidgets.take(p); delete p; updateSelectProtocolsAdvancedWidget(); lvSelectedProtocols->setCurrentIndex(idx); } void StreamConfigDialog::on_tbUp_clicked() { int m, n; QModelIndex idx; AbstractProtocol *p = NULL; idx = lvSelectedProtocols->currentIndex(); // Validation if (!idx.isValid() || idx.row() == 0) return; m = n = idx.row() + 1; _iter->toFront(); while (n--) { if (!_iter->hasNext()) return; p = _iter->next(); } Q_CHECK_PTR(p); _iter->remove(); _iter->previous(); _iter->insert(p); updateSelectProtocolsAdvancedWidget(); lvSelectedProtocols->setCurrentIndex(idx.sibling(m-2, 0)); } void StreamConfigDialog::on_tbDown_clicked() { int m, n; QModelIndex idx; AbstractProtocol *p = NULL; idx = lvSelectedProtocols->currentIndex(); // Validation if (!idx.isValid() || idx.row() == idx.model()->rowCount()) return; m = n = idx.row() + 1; _iter->toFront(); while (n--) { if (!_iter->hasNext()) return; p = _iter->next(); } Q_CHECK_PTR(p); _iter->remove(); _iter->next(); _iter->insert(p); updateSelectProtocolsAdvancedWidget(); lvSelectedProtocols->setCurrentIndex(idx.sibling(m,0)); } void StreamConfigDialog::updateSelectProtocolsAdvancedWidget() { QStringList selProtoList; qDebug("%s", __FUNCTION__); _iter->toFront(); while(_iter->hasNext()) { AbstractProtocol* p = _iter->next(); qDebug("%p -- %d", p, p->protocolNumber()); selProtoList.append(p->shortName()); } mpSelectedProtocolsModel->setStringList(selProtoList); } void StreamConfigDialog::on_twTopLevel_currentChanged(int index) { switch (index) { // Protocol Data case 1: { // Hide the ToolBox before modifying it - else we have a crash !!! tbProtocolData->hide(); // Remove all existing protocol widgets while (tbProtocolData->count() > 0) { QWidget* w = tbProtocolData->widget(0); tbProtocolData->removeItem(0); w->setParent(0); } // Repopulate the widgets - create new ones, if required _iter->toFront(); while (_iter->hasNext()) { AbstractProtocol* p = _iter->next(); AbstractProtocolConfigForm *w = _protocolWidgets.value(p); if (!w) { w = OstProtocolWidgetFactory->createConfigWidget( p->protocolNumber()); w->loadWidget(p); _protocolWidgets.insert(p, w); } tbProtocolData->addItem(w, p->name()); } if (lastProtocolDataIndex < tbProtocolData->count()) tbProtocolData->setCurrentIndex(lastProtocolDataIndex); tbProtocolData->show(); break; } // Variable Fields case 2: { StoreCurrentStream(); // Stream protocols may have changed - clear and reload variableFieldsWidget->clear(); variableFieldsWidget->load(); break; } // Stream Control case 3: { StoreCurrentStream(); break; } // Packet View case 4: { StoreCurrentStream(); mpPacketModel->setSelectedProtocols(*_iter); break; } default: break; } lastProtocolDataIndex = tbProtocolData->currentIndex(); } void StreamConfigDialog::update_NumPacketsAndNumBursts() { if (rbSendPackets->isChecked() && rbModeFixed->isChecked()) leNumPackets->setEnabled(true); else leNumPackets->setEnabled(false); if (rbSendBursts->isChecked() && rbModeFixed->isChecked()) leNumBursts->setEnabled(true); else leNumBursts->setEnabled(false); } #if 0 void StreamConfigDialog::on_lePattern_editingFinished() { ulong num = 0; bool isOk; QString str; num = lePattern->text().remove(QChar(' ')).toULong(&isOk, 16); qDebug("editfinished (%s | %x)\n", qPrintable(lePattern->text())); lePattern->setText(uintToHexStr(num, str, 4)); qDebug("editfinished (%s | %x)\n", qPrintable(lePattern->text())); } #endif /*! Skip protocols upto and including the layer specified. */ bool StreamConfigDialog::skipProtocols(int layer) { _iter->toFront(); for (int i = ProtoMin; i <= layer; i++) { if(_iter->hasNext()) { int id; QAbstractButton *btn; id = _iter->peekNext()->protocolNumber(); btn = bgProto[i]->button(id); if (btn) _iter->next(); } } return true; } /*! Protocol choices (except "None" and "Other") for a protocol button group are disabled if checked is true, else they are enabled */ void StreamConfigDialog::disableProtocols(QButtonGroup *protocolGroup, bool checked) { qDebug("%s: btnGrp = %p, chk? = %d", __FUNCTION__, protocolGroup, checked); foreach(QAbstractButton *btn, protocolGroup->buttons()) { int id = protocolGroup->id(btn); if ((id != ButtonIdNone) && (id != ButtonIdOther)) btn->setDisabled(checked); } } void StreamConfigDialog::forceProtocolNone(bool checked) { QObject *btn; btn = sender(); Q_ASSERT(btn != NULL); qDebug("%s: chk? = %d, btn = %p, L1 = %p, L2 = %p, L3 = %p", __FUNCTION__, checked, btn, rbL1None, rbFtNone, rbL3None); if (btn == rbL1None) { if (checked) { bgProto[ProtoVlan]->button(ButtonIdNone)->click(); bgProto[ProtoL2]->button(ButtonIdNone)->click(); bgProto[ProtoPayload]->button(ButtonIdNone)->click(); } disableProtocols(bgProto[ProtoVlan], checked); disableProtocols(bgProto[ProtoL2], checked); disableProtocols(bgProto[ProtoPayload], checked); } else if (btn == rbFtNone) { if (checked) bgProto[ProtoL3]->button(ButtonIdNone)->click(); disableProtocols(bgProto[ProtoL3], checked); } else if (btn == rbL3None) { if (checked) bgProto[ProtoL4]->button(ButtonIdNone)->click(); disableProtocols(bgProto[ProtoL4], checked); } else if (btn == rbL4None) { if (checked) bgProto[ProtoL5]->button(ButtonIdNone)->click(); disableProtocols(bgProto[ProtoL5], checked); } else { Q_ASSERT(1 == 0); // Unreachable code! } } // Button 'newId' has been clicked // - update the protocol list correspondingly void StreamConfigDialog::updateProtocol(int newId) { int level; QButtonGroup *btnGrp; btnGrp = static_cast(sender()); Q_ASSERT(btnGrp != NULL); level = btnGrp->property("ProtocolLevel").toInt(); Q_ASSERT(btnGrp == bgProto[level]); __updateProtocol(level, newId); } // Button 'newId' belonging to layer 'level' has been clicked // - update the protocol list correspondingly void StreamConfigDialog::__updateProtocol(int level, int newId) { int oldId; QButtonGroup *btnGrp; Q_ASSERT((level >= ProtoMin) && (level <= ProtoMax)); btnGrp = bgProto[level]; oldId = btnGrp->property("ProtocolId").toInt(); qDebug("%s: level = %d old id = %d new id = %d upd? = %d", __FUNCTION__, level, oldId, newId, isUpdateInProgress); if (newId == oldId) return; if (!isUpdateInProgress) { int ret; AbstractProtocol *p; ret = skipProtocols(level-1); Q_ASSERT(ret == true); Q_UNUSED(ret); Q_ASSERT(oldId != newId); Q_ASSERT(newId != ButtonIdOther); switch (oldId) { case ButtonIdNone: _iter->insert(OstProtocolManager->createProtocol( newId, mpStream)); break; case ButtonIdOther: default: Q_ASSERT(_iter->hasNext()); p =_iter->next(); if (newId) _iter->setValue(OstProtocolManager->createProtocol( newId, mpStream)); else _iter->remove(); // Free both protocol and associated widget delete _protocolWidgets.take(p); delete p; if (level == ProtoTrailer) { while (_iter->hasNext()) { p = _iter->next(); _iter->remove(); // Free both protocol and associated widget delete _protocolWidgets.take(p); delete p; } } break; } } btnGrp->setProperty("ProtocolId", newId); return; } //! Traverse the ProtocolList and update the SelectProtocols (Simple) widget void StreamConfigDialog::updateSelectProtocolsSimpleWidget() { int i; quint32 id; QAbstractButton *btn; qDebug("%s", __FUNCTION__); isUpdateInProgress = true; // Reset to default state ... for (i = ProtoMin; i < ProtoMax; i++) bgProto[i]->button(ButtonIdNone)->click(); // ... now iterate and update _iter->toFront(); for (i = ProtoMin; i < ProtoMax; i++) { if (!_iter->hasNext()) goto _done; id = _iter->next()->protocolNumber(); btn = bgProto[i]->button(id); if (btn) // we have a button for this protocol { if (btn->isEnabled()) btn->click(); else { btn->setChecked(true); __updateProtocol(i, id); } } else // we don't have a button for this protocol { switch (i) { case ProtoVlan: // No vlan - proto may belong to next layer _iter->previous(); break; case ProtoSign: // No sign - but we have a trailer _iter->previous(); break; case ProtoPayload: goto _other; default: // viz. L1, L2, L3, L4, L5, Trailer // Is this a Payload layer protocol? // (maybe intermediate layers are not present) btn = bgProto[ProtoPayload]->button(id); if (btn && btn->isEnabled()) { btn->click(); i = ProtoPayload; continue; } else goto _other; } } } // If more protocol(s) beyond trailer ... if (_iter->hasNext()) { i = ProtoTrailer; goto _other; } Q_ASSERT(!_iter->hasNext()); // At end of the ProtocolList goto _done; _other: // Set remaining protocols as 'Other' for (int j = i; j < ProtoMax; j++) { // VLAN/Sign doesn't have a "Other" button if ((j == ProtoVlan) || (j == ProtoSign)) continue; bgProto[j]->button(ButtonIdOther)->setChecked(true); __updateProtocol(j, ButtonIdOther); } _done: isUpdateInProgress = false; } void StreamConfigDialog::LoadCurrentStream() { QString str; qDebug("loading mpStream %p", mpStream); variableFieldsWidget->setStream(mpStream); QDialog::setWindowTitle(QString("%1 [%2]").arg(_windowTitle) .arg(mpStream->name().isEmpty() ? tr("") : mpStream->name())); // Meta Data { name->setText(mpStream->name()); enabled->setChecked(mpStream->isEnabled()); cmbPktLenMode->setCurrentIndex(mpStream->lenMode()); lePktLen->setText(str.setNum(mpStream->frameLen())); lePktLenMin->setText(str.setNum(mpStream->frameLenMin())); lePktLenMax->setText(str.setNum(mpStream->frameLenMax())); } // Protocols { updateSelectProtocolsSimpleWidget(); updateSelectProtocolsAdvancedWidget(); loadProtocolWidgets(); } // Variable Fields { variableFieldsWidget->clear(); variableFieldsWidget->load(); } // Stream Control { switch (mpStream->sendUnit()) { case Stream::e_su_packets: rbSendPackets->setChecked(true); break; case Stream::e_su_bursts: rbSendBursts->setChecked(true); break; default: qWarning("Unhandled sendUnit = %d\n", mpStream->sendUnit()); } switch (mpStream->sendMode()) { case Stream::e_sm_fixed: rbModeFixed->setChecked(true); break; case Stream::e_sm_continuous: rbModeContinuous->setChecked(true); break; default: qWarning("Unhandled sendMode = %d\n", mpStream->sendMode()); } switch(mpStream->nextWhat()) { case Stream::e_nw_stop: rbActionStop->setChecked(true); break; case Stream::e_nw_goto_next: rbActionGotoNext->setChecked(true); break; case Stream::e_nw_goto_id: rbActionGotoStream->setChecked(true); break; default: qWarning("Unhandled nextAction = %d\n", mpStream->nextWhat()); } leNumPackets->setText(QString().setNum(mpStream->numPackets())); leNumBursts->setText(QString().setNum(mpStream->numBursts())); lePacketsPerBurst->setText(QString().setNum(mpStream->burstSize())); lePacketsPerSec->setText( QString("%L1").arg(mpStream->packetRate(), 0, 'f', 4)); leBurstsPerSec->setText( QString("%L1").arg(mpStream->burstRate(), 0, 'f', 4)); // TODO(MED): Change this when we support goto to specific stream leStreamId->setText(QString("0")); leGapIsg->setText("0.0"); } qDebug("loading stream done"); } void StreamConfigDialog::StoreCurrentStream() { QString str; bool isOk; Stream *pStream = mpStream; qDebug("storing pStream %p", pStream); // Meta Data pStream->setName(name->text()); pStream->setEnabled(enabled->isChecked()); pStream->setLenMode((Stream::FrameLengthMode) cmbPktLenMode->currentIndex()); pStream->setFrameLen(lePktLen->text().toULong(&isOk)); pStream->setFrameLenMin(lePktLenMin->text().toULong(&isOk)); pStream->setFrameLenMax(lePktLenMax->text().toULong(&isOk)); // Protocols { storeProtocolWidgets(); } // Variable Fields { variableFieldsWidget->store(); } // Stream Control { if (rbSendPackets->isChecked()) pStream->setSendUnit(Stream::e_su_packets); if (rbSendBursts->isChecked()) pStream->setSendUnit(Stream::e_su_bursts); if (rbModeFixed->isChecked()) pStream->setSendMode(Stream::e_sm_fixed); if (rbModeContinuous->isChecked()) pStream->setSendMode(Stream::e_sm_continuous); if (rbActionStop->isChecked()) pStream->setNextWhat(Stream::e_nw_stop); if (rbActionGotoNext->isChecked()) pStream->setNextWhat(Stream::e_nw_goto_next); if (rbActionGotoStream->isChecked()) pStream->setNextWhat(Stream::e_nw_goto_id); pStream->setNumPackets(leNumPackets->text().toULong(&isOk)); pStream->setNumBursts(leNumBursts->text().toULong(&isOk)); pStream->setBurstSize(lePacketsPerBurst->text().toULong(&isOk)); pStream->setPacketRate( XLocale().toDouble(lePacketsPerSec->text(), &isOk)); pStream->setBurstRate( XLocale().toDouble(leBurstsPerSec->text(), &isOk)); } } void StreamConfigDialog::on_tbProtocolData_currentChanged(int /*index*/) { // Refresh protocol widgets in case there is any dependent data between // protocols e.g. TCP/UDP port numbers are dependent on Port/Protocol // selection in TextProtocol #if 0 // FIXME: temp mask to avoid crash till we fix it storeProtocolWidgets(); loadProtocolWidgets(); #endif } void StreamConfigDialog::on_rbPacketsPerSec_toggled(bool checked) { if (checked) on_lePacketsPerSec_textChanged(lePacketsPerSec->text()); } void StreamConfigDialog::on_rbBurstsPerSec_toggled(bool checked) { if (checked) on_leBurstsPerSec_textChanged(leBurstsPerSec->text()); } void StreamConfigDialog::on_lePacketsPerBurst_textChanged(const QString &/*text*/) { if (rbSendBursts->isChecked()) on_leBurstsPerSec_textChanged(leBurstsPerSec->text()); } void StreamConfigDialog::on_lePacketsPerSec_textChanged(const QString &text) { bool isOk; Stream *pStream = mpStream; uint frameLen = pStream->frameLenAvg(); if (rbSendPackets->isChecked()) { double pktsPerSec = XLocale().toDouble(text, &isOk); double bitsPerSec = pktsPerSec * double((frameLen+kEthFrameOverHead)*8); if (rbPacketsPerSec->isChecked()) leBitsPerSec->setText(QString("%L1").arg(bitsPerSec, 0, 'f', 0)); leGapIbg->setText(QString("0.0")); leGapIpg->setText(QString("%L1").arg(1/double(pktsPerSec), 0, 'f', 9)); } } void StreamConfigDialog::on_leBurstsPerSec_textChanged(const QString &text) { bool isOk; Stream *pStream = mpStream; uint burstSize = lePacketsPerBurst->text().toULong(&isOk); uint frameLen = pStream->frameLenAvg(); qDebug("start of %s(%s)", __FUNCTION__, qPrintable(text)); if (rbSendBursts->isChecked()) { double burstsPerSec = XLocale().toDouble(text, &isOk); double bitsPerSec = burstsPerSec * double(burstSize * (frameLen + kEthFrameOverHead) * 8); if (rbBurstsPerSec->isChecked()) leBitsPerSec->setText(QString("%L1").arg(bitsPerSec, 0, 'f', 0)); leGapIbg->setText(QString("%L1").arg(1/double(burstsPerSec), 0, 'f',9)); leGapIpg->setText(QString("0.0")); } qDebug("end of %s", __FUNCTION__); } void StreamConfigDialog::on_leBitsPerSec_textEdited(const QString &text) { bool isOk; Stream *pStream = mpStream; uint burstSize = lePacketsPerBurst->text().toULong(&isOk); uint frameLen = pStream->frameLenAvg(); if (rbSendPackets->isChecked()) { double pktsPerSec = XLocale().toDouble(text, &isOk)/ double((frameLen+kEthFrameOverHead)*8); lePacketsPerSec->setText(QString("%L1").arg(pktsPerSec, 0, 'f', 4)); } else if (rbSendBursts->isChecked()) { double burstsPerSec = XLocale().toDouble(text, &isOk)/ double(burstSize * (frameLen + kEthFrameOverHead) * 8); leBurstsPerSec->setText(QString("%L1").arg(burstsPerSec, 0, 'f', 4)); } } bool StreamConfigDialog::isCurrentStreamValid() { QStringList log; if ((mPort.transmitMode() == OstProto::kInterleavedTransmit) && (mpStream->isFrameVariable())) { log << tr("In 'Interleaved Streams' transmit mode, the count for " "varying fields at transmit time may not be same as configured"); } if (!mPort.trackStreamStats() && mpStream->hasProtocol(OstProto::Protocol::kSignFieldNumber)) { log << tr("Stream contains special signature, but per stream statistics " "will not be available till it is enabled on the port"); } mpStream->preflightCheck(log); if (log.size()) { if (QMessageBox::warning(this, "Preflight Check", tr("

We found possible problems with this stream -

") + "
    " + log.replaceInStrings(QRegExp("(.*)"), "
  • \\1
  • ") .join("\n") + "
" + tr("

Ignore?

"), QMessageBox::Yes | QMessageBox::No, QMessageBox::No) == QMessageBox::No) return false; } return true; } void StreamConfigDialog::on_pbPrev_clicked() { Q_ASSERT(mCurrentStreamIndex > 0); StoreCurrentStream(); if (!isCurrentStreamValid()) return; delete _iter; mpStream = _streamList.at(--mCurrentStreamIndex); _iter = mpStream->createProtocolListIterator(); LoadCurrentStream(); on_twTopLevel_currentChanged(twTopLevel->currentIndex()); pbPrev->setDisabled(mCurrentStreamIndex == 0); pbNext->setDisabled(int(mCurrentStreamIndex) == (_streamList.size()-1)); } void StreamConfigDialog::on_pbNext_clicked() { Q_ASSERT(int(mCurrentStreamIndex) < (_streamList.size()-1)); StoreCurrentStream(); if (!isCurrentStreamValid()) return; delete _iter; mpStream = _streamList.at(++mCurrentStreamIndex); _iter = mpStream->createProtocolListIterator(); LoadCurrentStream(); on_twTopLevel_currentChanged(twTopLevel->currentIndex()); pbPrev->setDisabled(mCurrentStreamIndex == 0); pbNext->setDisabled(int(mCurrentStreamIndex) == (_streamList.size()-1)); } void StreamConfigDialog::on_pbOk_clicked() { // Store dialog contents into current stream StoreCurrentStream(); if (!isCurrentStreamValid()) return; // Copy the working copy of streams to user provided streams Q_ASSERT(_userStreamList.size() == _streamList.size()); for (int i = 0; i < _streamList.size(); i++) { OstProto::Stream s; _streamList.at(i)->protoDataCopyInto(s); _userStreamList[i]->protoDataCopyFrom(s); } qDebug("stream stored"); lastGeometry = geometry(); lastTopLevelTabIndex = twTopLevel->currentIndex(); lastProtocolDataIndex = tbProtocolData->currentIndex(); accept(); } ostinato-1.3.0/client/streamconfigdialog.h000066400000000000000000000105721451413623100206500ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _STREAM_CONFIG_DIALOG_H #define _STREAM_CONFIG_DIALOG_H #include #include "ui_streamconfigdialog.h" #include "port.h" #include "stream.h" #include "packetmodel.h" #include #include #include #define MAX_MAC_ITER_COUNT 256 #define MIN_PKT_LEN 64 #define MAX_PKT_LEN 16384 /* ** TODO ** \todo Improve HexStr handling ** */ class AbstractProtocolConfigForm; class ModelTest; class StreamConfigDialog : public QDialog, public Ui::StreamConfigDialog { Q_OBJECT public: StreamConfigDialog(QList &streamList, const Port &port, QWidget *parent = 0); ~StreamConfigDialog(); void setWindowTitle(const QString &title); private: enum ButtonId { ButtonIdNone = 0, ButtonIdOther = -2 }; enum ProtoButtonGroup { ProtoMin, ProtoL1 = 0, ProtoVlan = 1, ProtoL2 = 2, ProtoL3 = 3, ProtoL4 = 4, ProtoL5 = 5, ProtoPayload = 6, ProtoSign = 7, ProtoTrailer = 8, ProtoMax }; QButtonGroup *bgProto[ProtoMax]; QStringListModel *mpAvailableProtocolsModel; QStringListModel *mpSelectedProtocolsModel; QList _userStreamList; QList _streamList; const Port& mPort; QString _windowTitle; uint mCurrentStreamIndex; Stream *mpStream; ProtocolListIterator *_iter; QHash _protocolWidgets; bool isUpdateInProgress; PacketModel *mpPacketModel; ModelTest *mpPacketModelTester; // The following static variables are used to track the "selected" tab // for the various tab widgets so that it can be restored when the dialog // is opened the next time. We also track the last Dialog geometry. static QRect lastGeometry; static int lastTopLevelTabIndex; static int lastProtocolDataIndex; void setupUiExtra(); bool isCurrentStreamValid(); void LoadCurrentStream(); void StoreCurrentStream(); void loadProtocolWidgets(); void storeProtocolWidgets(); private slots: void on_cmbPktLenMode_currentIndexChanged(QString mode); void update_NumPacketsAndNumBursts(); void on_twTopLevel_currentChanged(int index); void on_tbSelectProtocols_currentChanged(int index); // "Simple" Protocol Selection related bool skipProtocols(int layer); void disableProtocols(QButtonGroup *protocolGroup, bool checked); void forceProtocolNone(bool checked); void updateProtocol(int newId); void __updateProtocol(int level, int newId); void updateSelectProtocolsSimpleWidget(); // "Advanced" Protocol Selection related void when_lvAllProtocols_selectionChanged( const QItemSelection &selected, const QItemSelection &deselected); void when_lvSelectedProtocols_currentChanged( const QModelIndex ¤t, const QModelIndex &previous); void on_tbAdd_clicked(); void on_tbDelete_clicked(); void on_tbUp_clicked(); void on_tbDown_clicked(); void updateSelectProtocolsAdvancedWidget(); // "Protocol Data" related void on_tbProtocolData_currentChanged(int index); // "Stream Control" related void on_rbPacketsPerSec_toggled(bool checked); void on_rbBurstsPerSec_toggled(bool checked); void on_lePacketsPerBurst_textChanged(const QString &text); void on_lePacketsPerSec_textChanged(const QString &text); void on_leBurstsPerSec_textChanged(const QString &text); void on_leBitsPerSec_textEdited(const QString &text); void on_pbPrev_clicked(); void on_pbNext_clicked(); void on_pbOk_clicked(); }; #endif ostinato-1.3.0/client/streamconfigdialog.ui000066400000000000000000001420061451413623100210340ustar00rootroot00000000000000 StreamConfigDialog Qt::ApplicationModal 0 0 647 549 0 0 Edit Stream :/icons/stream_edit.png:/icons/stream_edit.png QLineEdit:enabled[inputMask = "HH; "], QLineEdit:enabled[inputMask = "HH HH; "], QLineEdit:enabled[inputMask = "HH HH HH; "], QLineEdit:enabled[inputMask = "HH HH HH HH; "], QLineEdit:enabled[inputMask = "HH HH HH HH HH HH; "] { background-color: #ccccff } true 0 Protocol Selection Basics Name name Enabled Frame Length (including FCS) Fixed Increment Decrement Random IMIX Min false Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter Max false Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter 0 0 0 605 311 Simple L1 None true Mac false false Other true L2 None true Ethernet II false 802.3 Raw 802.3 LLC false 802.3 LLC SNAP false Other true L3 None true false ARP false IPv4 false false IPv6 false IP 6over4 false false IP 4over6 false false IP 4over4 false false IP 6over6 false false Other true VLAN false false Untagged true Tagged Stacked true L4 None true false ICMP false IGMP false TCP false UDP false Other false MLD true L5 None true false Text false Other true Payload None true Pattern false Hex Dump false Other true Special None true Signature true Trailer None true false Other Qt::Vertical 20 40 0 0 250 135 Advanced Available Protocols true QAbstractItemView::ExtendedSelection QAbstractItemView::SelectRows Qt::Vertical 20 40 false > :/icons/arrow_right.png:/icons/arrow_right.png Qt::Vertical 20 40 Selected Protocols false ^ :/icons/arrow_up.png:/icons/arrow_up.png false v :/icons/arrow_down.png:/icons/arrow_down.png false - :/icons/delete.png:/icons/delete.png Qt::Horizontal 40 20 QAbstractItemView::SelectRows Protocol Data -1 Variable Fields Stream Control Send Packets true Bursts Numbers Number of Packets leNumPackets Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter Number of Bursts leNumBursts false Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter Packets per Burst lePacketsPerBurst false Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter Rate false false Packets/Sec true Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter false Bursts/Sec false Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter Bits/Sec false Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter After this stream Stop Goto Next Stream true Goto First false Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter Qt::Horizontal 20 41 Mode Fixed true Continuous true Gaps (in seconds) false false :/icons/gaps.png ISG leGapIsg false Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter IBG leGapIbg false Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter IPG leGapIpg false Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter Qt::Vertical 153 21 Packet View Qt::Vertical QAbstractItemView::SelectItems QAbstractItemView::ScrollPerPixel true Prev Next Qt::Horizontal 191 20 OK true Cancel DumpView QWidget
dumpview.h
1
VariableFieldsWidget QWidget
variablefieldswidget.h
1
twTopLevel cmbPktLenMode lePktLen lePktLenMin lePktLenMax rbL1None rbL1Mac rbL1Other rbVlanNone rbVlanSingle rbVlanDouble rbFtNone rbFtEthernet2 rbFt802Dot3Raw rbFt802Dot3Llc rbFtLlcSnap rbFtOther rbL3None rbL3Arp rbL3Ipv4 rbL3Ipv6 rbL3Ip6over4 rbL3Ip4over6 rbL3Ip4over4 rbL3Ip6over6 rbL3Other rbL4None rbL4Icmp rbL4Igmp rbL4Mld rbL4Tcp rbL4Udp rbL4Other rbL5None rbL5Text rbL5Other rbPayloadNone rbPayloadPattern rbPayloadHexDump rbPayloadOther lvAllProtocols tbAdd tbUp tbDown tbDelete lvSelectedProtocols rbSendPackets rbSendBursts rbModeFixed rbModeContinuous leNumPackets leNumBursts lePacketsPerBurst lePacketsPerSec leBurstsPerSec rbBitsPerSec leBitsPerSec rbActionStop rbActionGotoNext rbActionGotoStream leStreamId leGapIsg leGapIbg leGapIpg tvPacketTree pbPrev pbNext pbOk pbCancel pbCancel clicked() StreamConfigDialog reject() 623 496 533 466 rbActionGotoStream toggled(bool) leStreamId setEnabled(bool) 463 143 463 177 rbSendPackets toggled(bool) rbPacketsPerSec setEnabled(bool) 30 68 299 82 rbSendBursts toggled(bool) rbBurstsPerSec setEnabled(bool) 30 95 299 132 rbSendBursts toggled(bool) lePacketsPerBurst setEnabled(bool) 30 95 134 189 rbPacketsPerSec toggled(bool) lePacketsPerSec setEnabled(bool) 299 82 299 108 rbBurstsPerSec toggled(bool) leBurstsPerSec setEnabled(bool) 299 132 299 158 rbBitsPerSec toggled(bool) leBitsPerSec setEnabled(bool) 299 182 299 208 rbSendPackets toggled(bool) rbPacketsPerSec setChecked(bool) 95 70 299 82 rbSendBursts toggled(bool) rbBurstsPerSec setChecked(bool) 96 98 299 132 rbModeContinuous toggled(bool) leNumPackets setDisabled(bool) 73 196 164 108 rbModeContinuous toggled(bool) leNumBursts setDisabled(bool) 96 199 226 155
ostinato-1.3.0/client/streamlistdelegate.cpp000066400000000000000000000126041451413623100212220ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #include #include #include #include #include "streammodel.h" #include "streamlistdelegate.h" StreamListDelegate::StreamListDelegate(QObject *parent) : QItemDelegate(parent) { } QWidget *StreamListDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const { QWidget *editor = NULL; switch ((StreamModel::StreamFields) index.column()) { case StreamModel::StreamStatus: { editor = new QCheckBox(parent); goto _handled; } case StreamModel::StreamNextWhat: { editor = new QComboBox(parent); static_cast(editor)->insertItems(0, StreamModel::nextWhatOptionList()); goto _handled; } case StreamModel::StreamIcon: case StreamModel::StreamName: default: break; } editor = QItemDelegate::createEditor(parent, option, index); _handled: return editor; } void StreamListDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const { switch ((StreamModel::StreamFields) index.column()) { case StreamModel::StreamStatus: { QCheckBox *cb = static_cast(editor); cb->setChecked( index.model()->data(index, Qt::EditRole).toBool()); goto _handled; } case StreamModel::StreamNextWhat: { QComboBox *cb = static_cast(editor); cb->setCurrentIndex( index.model()->data(index, Qt::EditRole).toInt()); goto _handled; } case StreamModel::StreamIcon: case StreamModel::StreamName: default: break; } QItemDelegate::setEditorData(editor, index); _handled: return; } void StreamListDelegate::setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const { switch ((StreamModel::StreamFields) index.column()) { case StreamModel::StreamStatus: { QCheckBox *cb = static_cast(editor); model->setData(index, cb->isChecked(), Qt::EditRole); goto _handled; } case StreamModel::StreamNextWhat: { QComboBox *cb = static_cast(editor); model->setData(index, cb->currentIndex(), Qt::EditRole); goto _handled; } case StreamModel::StreamIcon: case StreamModel::StreamName: default: break; } QItemDelegate::setModelData(editor, model, index); _handled: return; } void StreamListDelegate::updateEditorGeometry(QWidget *editor, const QStyleOptionViewItem &option, const QModelIndex &index) const { switch ((StreamModel::StreamFields) index.column()) { case StreamModel::StreamStatus: { /* * extra 'coz QItemDelegate does it - otherwise the editor * placement is incorrect */ int extra = 2 * (qApp->style()->pixelMetric( QStyle::PM_FocusFrameHMargin, 0) + 1); editor->setGeometry(option.rect.translated(extra, 0)); goto _handled; } case StreamModel::StreamIcon: case StreamModel::StreamName: case StreamModel::StreamNextWhat: default: break; } QItemDelegate::updateEditorGeometry(editor, option, index); _handled: return; } bool StreamListDelegate::editorEvent(QEvent *event, QAbstractItemModel *model, const QStyleOptionViewItem &option, const QModelIndex &index) { /* * Special Handling so that user can use the "Stream status" checkbox * without double clicking first. Copied from QItemDelegate::editorEvent() * and modified suitably */ if ((StreamModel::StreamFields)index.column() == StreamModel::StreamStatus) { // make sure that we have the right event type if ((event->type() == QEvent::MouseButtonRelease) || (event->type() == QEvent::MouseButtonDblClick)) { QRect checkRect = doCheck(option, option.rect, Qt::Checked); QRect emptyRect; doLayout(option, &checkRect, &emptyRect, &emptyRect, false); if (!checkRect.contains(static_cast(event)->pos())) return false; Qt::CheckState state = (static_cast(index.data( Qt::CheckStateRole).toInt()) == Qt::Checked ? Qt::Unchecked : Qt::Checked); return model->setData(index, state, Qt::CheckStateRole); } } return QItemDelegate::editorEvent(event, model, option, index); } ostinato-1.3.0/client/streamlistdelegate.h000066400000000000000000000027031451413623100206660ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef STREAM_LIST_DELEGATE_H #define STREAM_LIST_DELEGATE_H #include #include class StreamListDelegate : public QItemDelegate { Q_OBJECT public: StreamListDelegate(QObject *parent = 0); QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const; void setEditorData(QWidget *editor, const QModelIndex &index) const; void setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const; void updateEditorGeometry(QWidget *editor, const QStyleOptionViewItem &option, const QModelIndex &index) const; bool editorEvent(QEvent *event, QAbstractItemModel *model, const QStyleOptionViewItem &option, const QModelIndex &index); }; #endif ostinato-1.3.0/client/streammodel.cpp000066400000000000000000000250711451413623100176560ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #include "stream.h" #include "streammodel.h" #include "portgrouplist.h" #include "qicon.h" #include const QLatin1String kStreamsMimeType("application/vnd.ostinato.streams"); StreamModel::StreamModel(PortGroupList *p, QObject *parent) : QAbstractTableModel(parent) { pgl = p; mCurrentPort = NULL; } int StreamModel::rowCount(const QModelIndex &parent) const { if (parent.isValid()) return 0; if (mCurrentPort) return mCurrentPort->numStreams(); else return 0; } int StreamModel::columnCount(const QModelIndex &/*parent*/) const { int count = StreamMaxFields; if (mCurrentPort && (mCurrentPort->transmitMode() == OstProto::kInterleavedTransmit)) count--; return count; } Qt::ItemFlags StreamModel::flags(const QModelIndex &index) const { Qt::ItemFlags flags = QAbstractTableModel::flags(index); switch (index.column()) { case StreamIcon: break; case StreamName: flags |= Qt::ItemIsEditable; break; case StreamStatus: flags |= Qt::ItemIsUserCheckable; break; case StreamNextWhat: flags |= Qt::ItemIsEditable; break; default: //qFatal("Missed case in switch!"); break; } return flags; } QVariant StreamModel::data(const QModelIndex &index, int role) const { // Check for a valid index if (!index.isValid()) return QVariant(); // Check for row/column limits if (index.row() >= mCurrentPort->numStreams()) return QVariant(); if (index.column() >= StreamMaxFields) return QVariant(); if (mCurrentPort == NULL) return QVariant(); // Return data based on field and role switch(index.column()) { case StreamIcon: { if (role == Qt::DecorationRole) return QIcon(":/icons/stream_edit.png"); else return QVariant(); break; } case StreamName: { if ((role == Qt::DisplayRole) || (role == Qt::EditRole)) return mCurrentPort->streamByIndex(index.row())->name(); else return QVariant(); break; } case StreamStatus: { if (role == Qt::CheckStateRole) { if (mCurrentPort->streamByIndex(index.row())->isEnabled()) return Qt::Checked; else return Qt::Unchecked; } else return QVariant(); break; } case StreamNextWhat: { int val = mCurrentPort->streamByIndex(index.row())->nextWhat(); if (role == Qt::DisplayRole) return nextWhatOptionList().at(val); else if (role == Qt::EditRole) return val; else return QVariant(); break; } default: qFatal("-------------UNHANDLED STREAM FIELD----------------"); } return QVariant(); } bool StreamModel::setData(const QModelIndex &index, const QVariant &value, int role) { if (mCurrentPort == NULL) return false; if (index.isValid()) { switch (index.column()) { // Edit Supported Fields case StreamName: mCurrentPort->mutableStreamByIndex(index.row()) ->setName(value.toString()); emit(dataChanged(index, index)); return true; case StreamStatus: mCurrentPort->mutableStreamByIndex(index.row()) ->setEnabled(value.toBool()); emit(dataChanged(index, index)); return true; case StreamNextWhat: if (role == Qt::EditRole) { mCurrentPort->mutableStreamByIndex(index.row())->setNextWhat( (Stream::NextWhat)value.toInt()); emit(dataChanged(index, index)); return true; } else return false; // Edit Not Supported Fields case StreamIcon: return false; // Unhandled Stream Field default: qDebug("-------------UNHANDLED STREAM FIELD----------------"); break; } } return false; } QVariant StreamModel::headerData(int section, Qt::Orientation orientation, int role) const { if (role != Qt::DisplayRole) return QVariant(); if (orientation == Qt::Horizontal) { switch(section) { case StreamIcon: return QString(""); break; case StreamName: return QString("Name"); break; case StreamStatus: return QString(""); break; case StreamNextWhat: return QString("Goto"); break; default: qDebug("-------------UNHANDLED STREAM FIELD----------------"); break; } } else return QString("%1").arg(section+1); return QVariant(); } QStringList StreamModel::mimeTypes() const { return QStringList() << kStreamsMimeType; } QMimeData* StreamModel::mimeData(const QModelIndexList &indexes) const { using ::google::protobuf::uint8; if (indexes.isEmpty()) return nullptr; // indexes may include multiple columns for a row - but we are only // interested in rows 'coz we have a single data for all columns // XXX: use QMap instead of QSet to keep rows in sorted order QMap rows; foreach(QModelIndex index, indexes) rows.insert(index.row(), index.row()); OstProto::StreamConfigList streams; streams.mutable_port_id()->set_id(mCurrentPort->id()); foreach(int row, rows) { OstProto::Stream *stream = streams.add_stream(); mCurrentPort->streamByIndex(row)->protoDataCopyInto(*stream); } QByteArray data; data.resize(streams.ByteSize()); streams.SerializeWithCachedSizesToArray((uint8*)data.data()); //qDebug("copy %s", streams.DebugString().c_str()); //TODO: copy DebugString as text/plain? QMimeData *mimeData = new QMimeData(); mimeData->setData(kStreamsMimeType, data); return mimeData; // XXX: caller is expected to take ownership and free! } bool StreamModel::dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int /*column*/, const QModelIndex &parent) { if (!data) return false; if (!data->hasFormat(kStreamsMimeType)) return false; if (action != Qt::CopyAction) return false; OstProto::StreamConfigList streamsData; QByteArray ba(data->data(kStreamsMimeType)); streamsData.ParseFromArray((void*)ba.constData(), ba.size()); //qDebug("paste %s", streamsData.DebugString().c_str()); QList streams; for (int i = 0; i < streamsData.stream_size(); i++) { Stream *stream = new Stream; stream->protoDataCopyFrom(streamsData.stream(i)); streams.append(stream); } if ((row < 0) || (row > rowCount(parent))) row = rowCount(parent); // Delete rows that we are going to overwrite if (row < rowCount(parent)) removeRows(row, qMin(rowCount() - row, streams.size())); return insert(row, streams); // callee will free streams after insert } /*! * Inserts streams before the given row * * StreamModel takes ownership of the passed streams; caller should * not try to access them after calling this function */ bool StreamModel::insert(int row, QList &streams) { int count = streams.size(); qDebug("insert row = %d", row); qDebug("insert count = %d", count); beginInsertRows(QModelIndex(), row, row+count-1); for (int i = 0; i < count; i++) { OstProto::Stream s; streams.at(i)->protoDataCopyInto(s); mCurrentPort->newStreamAt(row+i, &s); delete streams.at(i); } streams.clear(); endInsertRows(); return true; } bool StreamModel::insertRows(int row, int count, const QModelIndex &/*parent*/) { qDebug("insertRows() row = %d", row); qDebug("insertRows() count = %d", count); beginInsertRows(QModelIndex(), row, row+count-1); for (int i = 0; i < count; i++) mCurrentPort->newStreamAt(row); endInsertRows(); return true; } bool StreamModel::removeRows(int row, int count, const QModelIndex &/*parent*/) { qDebug("removeRows() row = %d", row); qDebug("removeRows() count = %d", count); beginRemoveRows(QModelIndex(), row, row+count-1); for (int i = 0; i < count; i++) { mCurrentPort->deleteStreamAt(row); } endRemoveRows(); return true; } // --------------------- SLOTS ------------------------ void StreamModel::setCurrentPortIndex(const QModelIndex ¤t) { beginResetModel(); if (!current.isValid() || !pgl->isPort(current)) { qDebug("current is either invalid or not a port"); mCurrentPort = NULL; } else { qDebug("change to valid port"); if (mCurrentPort) { disconnect(mCurrentPort, SIGNAL(streamListChanged(int, int)), this, SLOT(when_mCurrentPort_streamListChanged(int, int))); } quint16 pg = current.internalId() >> 16; // TODO: make mCurrentPort a smart weak pointer mCurrentPort = pgl->mPortGroups[pgl->indexOfPortGroup(pg)]->mPorts[current.row()]; connect(mCurrentPort, SIGNAL(streamListChanged(int, int)), this, SLOT(when_mCurrentPort_streamListChanged(int, int))); } endResetModel(); } void StreamModel::when_mCurrentPort_streamListChanged(int portGroupId, int portId) { qDebug("In %s", __FUNCTION__); if (mCurrentPort) { if ((quint32(portGroupId) == mCurrentPort->portGroupId()) && (quint32(portId) == mCurrentPort->id())) { beginResetModel(); endResetModel(); } } } ostinato-1.3.0/client/streammodel.h000066400000000000000000000051601451413623100173200ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _STREAM_MODEL_H #define _STREAM_MODEL_H #include #include #include "port.h" class PortGroupList; class Stream; class StreamModel : public QAbstractTableModel { Q_OBJECT Port *mCurrentPort; PortGroupList *pgl; public: StreamModel(PortGroupList *p, QObject *parent = 0); int rowCount(const QModelIndex &parent = QModelIndex()) const; int columnCount(const QModelIndex &parent = QModelIndex()) const; Qt::ItemFlags flags(const QModelIndex &index) const; QVariant data(const QModelIndex &index, int role) const; bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole); QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const; QStringList mimeTypes() const; QMimeData* mimeData(const QModelIndexList &indexes) const; bool dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent); bool insert(int row, QList &streams); bool insertRows (int row, int count, const QModelIndex & parent = QModelIndex()); bool removeRows (int row, int count, const QModelIndex & parent = QModelIndex()); #if 0 // CleanedUp! // FIXME(HIGH): This *is* like a kludge QList* currentPortStreamList() { return &mCurrentPort->mStreams; } #endif public: enum StreamFields { StreamIcon = 0, StreamStatus, StreamName, StreamNextWhat, StreamMaxFields }; static QStringList nextWhatOptionList() { return QStringList() << "Stop" << "Next" << "Goto first"; } public slots: void setCurrentPortIndex(const QModelIndex ¤t); private slots: void when_mCurrentPort_streamListChanged(int portGroupId, int portId); }; #endif ostinato-1.3.0/client/streamstatsfiltermodel.h000066400000000000000000000026751451413623100216150ustar00rootroot00000000000000/* Copyright (C) 2017 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _STREAM_STATS_FILTER_MODEL_H #define _STREAM_STATS_FILTER_MODEL_H #include class StreamStatsFilterModel : public QSortFilterProxyModel { Q_OBJECT public: StreamStatsFilterModel(QObject *parent = 0) : QSortFilterProxyModel(parent) { } protected: bool filterAcceptsColumn(int sourceColumn, const QModelIndex &/*sourceParent*/) const { QString title = sourceModel()->headerData(sourceColumn, Qt::Horizontal) .toString(); return filterRegExp().exactMatch(title) ? true : false; } bool filterAcceptsRow(int /*sourceRow*/, const QModelIndex &/*sourceParent*/) const { return true; } }; #endif ostinato-1.3.0/client/streamstatsmodel.cpp000066400000000000000000000246531451413623100207420ustar00rootroot00000000000000/* Copyright (C) 2016 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #include "streamstatsmodel.h" #include "protocol.pb.h" #include "xqlocale.h" #include #include #include #include // XXX: Keep the enum in sync with it's string enum { kTxPkts, kRxPkts, kTxBytes, kRxBytes, kMaxStreamStats }; static QStringList statTitles = QStringList() << "Tx Pkts" << "Rx Pkts" << "Tx Bytes" << "Rx Bytes"; // XXX: Keep the enum in sync with it's string enum { kAggrTxPkts, kAggrRxPkts, kAggrPktLoss, kTxDuration, kAvgTxFrameRate, kAvgRxFrameRate, kAvgTxBitRate, kAvgRxBitRate, kAvgLatency, kAvgJitter, kMaxAggrStreamStats }; static QStringList aggrStatTitles = QStringList() << "Total\nTx Pkts" << "Total\nRx Pkts" << "Total\nPkt Loss" << "Duration\n(secs)" << "Avg\nTx PktRate" << "Avg\nRx PktRate" << "Avg\nTx BitRate" << "Avg\nRx BitRate" << "Avg\nLatency" << "Avg\nJitter"; static const uint kAggrGuid = 0xffffffff; StreamStatsModel::StreamStatsModel(QObject *parent) : QAbstractTableModel(parent) { clearStats(); } int StreamStatsModel::rowCount(const QModelIndex &/*parent*/) const { return guidList_.size(); } int StreamStatsModel::columnCount(const QModelIndex &/*parent*/) const { if (!portList_.size()) return 0; return kMaxAggrStreamStats + portList_.size() * kMaxStreamStats; } QVariant StreamStatsModel::headerData( int section, Qt::Orientation orientation, int role) const { if (role != Qt::DisplayRole) return QVariant(); switch (orientation) { case Qt::Horizontal: // Column Header if (section < kMaxAggrStreamStats) return aggrStatTitles.at(section % kMaxAggrStreamStats); section -= kMaxAggrStreamStats; return QString("Port %1-%2\n%3") .arg(portList_.at(section/kMaxStreamStats).first) .arg(portList_.at(section/kMaxStreamStats).second) .arg(statTitles.at(section % kMaxStreamStats)); case Qt::Vertical: // Row Header if (section == (guidList_.size() - 1)) return QString("GUID Total"); return QString("Stream GUID %1") .arg(guidList_.at(section)); default: break; } return QVariant(); } QVariant StreamStatsModel::data(const QModelIndex &index, int role) const { if (role == Qt::TextAlignmentRole) return Qt::AlignRight; int portColumn = index.column() - kMaxAggrStreamStats; // Stylesheets typically don't use or set palette colors, so if // using one, don't use palette colors if ((role == Qt::BackgroundRole) && qApp->styleSheet().isEmpty()) { QPalette palette = QApplication::palette(); if (index.row() == (guidList_.size() - 1)) // Aggregate Row return palette.dark(); if (portColumn < 0) // Aggregate Column return palette.alternateBase(); if ((portColumn/kMaxStreamStats) & 1) // Color alternate Ports return palette.alternateBase(); } Guid guid = guidList_.at(index.row()); if ((role == Qt::ForegroundRole && qApp->styleSheet().isEmpty())) { QPalette palette = QApplication::palette(); if ((index.column() == kAggrPktLoss) && aggrGuidStats_.value(guid).pktLoss) return palette.link(); if (index.row() == (guidList_.size() - 1)) // Aggregate Row return palette.brightText(); } if (role == Qt::FontRole ) { if (index.row() == (guidList_.size() - 1)) { // Aggregate Row QFont font; font.setBold(true); return font; } } if (role != Qt::DisplayRole) return QVariant(); if (index.column() < kMaxAggrStreamStats) { int stat = index.column() % kMaxAggrStreamStats; switch (stat) { case kAggrRxPkts: return QString("%L1").arg(aggrGuidStats_.value(guid).rxPkts); case kAggrTxPkts: return QString("%L1").arg(aggrGuidStats_.value(guid).txPkts); case kAggrPktLoss: return QString("%L1").arg(aggrGuidStats_.value(guid).pktLoss); case kTxDuration: return QString("%L1").arg(aggrGuidStats_.value(guid).txDuration); case kAvgTxFrameRate: return aggrGuidStats_.value(guid).txDuration <= 0 ? QString("-") : XLocale().toPktRateString( aggrGuidStats_.value(guid).txPkts / aggrGuidStats_.value(guid).txDuration); case kAvgRxFrameRate: return aggrGuidStats_.value(guid).txDuration <= 0 ? QString("-") : XLocale().toPktRateString( aggrGuidStats_.value(guid).rxPkts / aggrGuidStats_.value(guid).txDuration); case kAvgTxBitRate: return aggrGuidStats_.value(guid).txDuration <= 0 ? QString("-") : XLocale().toBitRateString( (aggrGuidStats_.value(guid).txBytes + 24 * aggrGuidStats_.value(guid).txPkts) * 8 / aggrGuidStats_.value(guid).txDuration); case kAvgRxBitRate: return aggrGuidStats_.value(guid).txDuration <= 0 ? QString("-") : XLocale().toBitRateString( (aggrGuidStats_.value(guid).rxBytes + 24 * aggrGuidStats_.value(guid).rxPkts) * 8 / aggrGuidStats_.value(guid).txDuration); case kAvgLatency: return aggrGuidStats_.value(guid).latencyCount <= 0 || aggrGuidStats_.value(guid).latencySum <= 0 ? QString("-") : XLocale().toTimeIntervalString( aggrGuidStats_.value(guid).latencySum / aggrGuidStats_.value(guid).latencyCount); case kAvgJitter: return aggrGuidStats_.value(guid).latencyCount <= 0 || aggrGuidStats_.value(guid).latencySum <= 0 ? QString("-") : XLocale().toTimeIntervalString( aggrGuidStats_.value(guid).jitterSum / aggrGuidStats_.value(guid).latencyCount); default: break; }; return QVariant(); } PortGroupPort pgp = portList_.at(portColumn/kMaxStreamStats); int stat = portColumn % kMaxStreamStats; switch (stat) { case kRxPkts: return QString("%L1").arg(streamStats_.value(guid).value(pgp).rxPkts); case kTxPkts: return QString("%L1").arg(streamStats_.value(guid).value(pgp).txPkts); case kRxBytes: return QString("%L1").arg(streamStats_.value(guid).value(pgp).rxBytes); case kTxBytes: return QString("%L1").arg(streamStats_.value(guid).value(pgp).txBytes); default: break; } return QVariant(); } Qt::DropActions StreamStatsModel::supportedDropActions() const { return Qt::IgnoreAction; // read-only model, doesn't accept any data } // --------------------------------------------- // // Slots // --------------------------------------------- // void StreamStatsModel::clearStats() { #if QT_VERSION >= 0x040600 beginResetModel(); #endif guidList_.clear(); portList_.clear(); streamStats_.clear(); aggrGuidStats_.clear(); #if QT_VERSION >= 0x040600 endResetModel(); #else reset(); #endif } void StreamStatsModel::appendStreamStatsList( quint32 portGroupId, const OstProto::StreamStatsList *stats) { int n = stats->stream_stats_size(); #if QT_VERSION >= 0x040600 beginResetModel(); #endif for (int i = 0; i < n; i++) { const OstProto::StreamStats &s = stats->stream_stats(i); PortGroupPort pgp = PortGroupPort(portGroupId, s.port_id().id()); Guid guid = s.stream_guid().id(); StreamStats &ss = streamStats_[guid][pgp]; StreamStats &aggrPort = streamStats_[kAggrGuid][pgp]; AggrGuidStats &aggrGuid = aggrGuidStats_[guid]; AggrGuidStats &aggrAggr = aggrGuidStats_[kAggrGuid]; ss.rxPkts = s.rx_pkts(); ss.txPkts = s.tx_pkts(); ss.rxBytes = s.rx_bytes(); ss.txBytes = s.tx_bytes(); ss.rxLatency = s.latency(); ss.rxJitter = s.jitter(); aggrPort.rxPkts += ss.rxPkts; aggrPort.txPkts += ss.txPkts; aggrPort.rxBytes += ss.rxBytes; aggrPort.txBytes += ss.txBytes; aggrGuid.rxPkts += ss.rxPkts; aggrGuid.txPkts += ss.txPkts; aggrGuid.pktLoss += ss.txPkts - ss.rxPkts; aggrGuid.rxBytes += ss.rxBytes; aggrGuid.txBytes += ss.txBytes; if (s.tx_duration() > aggrGuid.txDuration) aggrGuid.txDuration = s.tx_duration(); // XXX: use largest or avg? if (ss.rxLatency) { aggrGuid.latencySum += ss.rxLatency; aggrGuid.jitterSum += ss.rxJitter; aggrGuid.latencyCount++; } aggrAggr.rxPkts += ss.rxPkts; aggrAggr.txPkts += ss.txPkts; aggrAggr.pktLoss += ss.txPkts - ss.rxPkts; aggrAggr.rxBytes += ss.rxBytes; aggrAggr.txBytes += ss.txBytes; if (aggrGuid.txDuration > aggrAggr.txDuration) aggrAggr.txDuration = aggrGuid.txDuration; if (ss.rxLatency) { aggrAggr.latencySum += ss.rxLatency; aggrAggr.jitterSum += ss.rxJitter; aggrAggr.latencyCount++; } if (!portList_.contains(pgp)) portList_.append(pgp); if (!guidList_.contains(guid)) guidList_.append(guid); } if (guidList_.size() && !guidList_.contains(kAggrGuid)) guidList_.append(kAggrGuid); std::sort(guidList_.begin(), guidList_.end()); #if QT_VERSION >= 0x040600 endResetModel(); #else reset(); #endif // Prevent receiving any future updates from this sender disconnect(sender(), 0, this, 0); } ostinato-1.3.0/client/streamstatsmodel.h000066400000000000000000000044011451413623100203740ustar00rootroot00000000000000/* Copyright (C) 2016 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _STREAM_STATS_MODEL_H #define _STREAM_STATS_MODEL_H #include #include #include #include #include namespace OstProto { class StreamStatsList; } class StreamStatsModel: public QAbstractTableModel { Q_OBJECT public: StreamStatsModel(QObject *parent = 0); int rowCount(const QModelIndex &parent=QModelIndex()) const; int columnCount(const QModelIndex &parent=QModelIndex()) const; QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const; QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const; Qt::DropActions supportedDropActions() const; public slots: void clearStats(); void appendStreamStatsList(quint32 portGroupId, const OstProto::StreamStatsList *stats); private: typedef QPair PortGroupPort; // Pair = (PortGroupId, PortId) typedef uint Guid; struct StreamStats { quint64 rxPkts; quint64 txPkts; quint64 rxBytes; quint64 txBytes; quint64 rxLatency; quint64 rxJitter; }; struct AggrGuidStats { quint64 rxPkts; quint64 txPkts; quint64 rxBytes; quint64 txBytes; qint64 pktLoss; double txDuration; quint64 latencySum; quint64 jitterSum; uint latencyCount; }; QList guidList_; QList portList_; QHash > streamStats_; QHash aggrGuidStats_; }; #endif ostinato-1.3.0/client/streamstatswindow.cpp000066400000000000000000000037761451413623100211540ustar00rootroot00000000000000/* Copyright (C) 2016 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #include "streamstatswindow.h" #include "streamstatsfiltermodel.h" #include #include static int id; static int count; StreamStatsWindow::StreamStatsWindow(QAbstractItemModel *model, QWidget *parent) : QWidget(parent) { setupUi(this); streamStats->addAction(actionShowDetails); if (id) setWindowTitle(windowTitle() + QString("(%1)").arg(id)); id++; count++; filterModel_ = new StreamStatsFilterModel(this); filterModel_->setFilterRegExp(QRegExp(kDefaultFilter_)); filterModel_->setSourceModel(model); streamStats->setModel(filterModel_); streamStats->verticalHeader()->setHighlightSections(false); streamStats->verticalHeader()->setDefaultSectionSize( streamStats->verticalHeader()->minimumSectionSize()); // Fit all columns in window whenever data changes connect(model, &QAbstractItemModel::modelReset, [=]() { streamStats->resizeColumnsToContents(); }); } StreamStatsWindow::~StreamStatsWindow() { delete filterModel_; count--; if (count == 0) id = 0; } void StreamStatsWindow::on_actionShowDetails_triggered(bool checked) { if (checked) filterModel_->setFilterRegExp(QRegExp(".*")); else filterModel_->setFilterRegExp(QRegExp(kDefaultFilter_)); streamStats->resizeColumnsToContents(); } ostinato-1.3.0/client/streamstatswindow.h000066400000000000000000000022551451413623100206100ustar00rootroot00000000000000/* Copyright (C) 2016 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _STREAM_STATS_WINDOW_H #define _STREAM_STATS_WINDOW_H #include "ui_streamstatswindow.h" class QAbstractItemModel; class QSortFilterProxyModel; class StreamStatsWindow: public QWidget, private Ui::StreamStatsWindow { Q_OBJECT public: StreamStatsWindow(QAbstractItemModel *model, QWidget *parent = 0); ~StreamStatsWindow(); private slots: void on_actionShowDetails_triggered(bool checked); private: QString kDefaultFilter_{"^(?!Port).*"}; QSortFilterProxyModel *filterModel_; }; #endif ostinato-1.3.0/client/streamstatswindow.ui000066400000000000000000000025011451413623100207700ustar00rootroot00000000000000 StreamStatsWindow 0 0 551 452 Stream Statistics Qt::ActionsContextMenu QAbstractItemView::SelectRows Oops! We don't seem to have any stream statistics for the requested port(s) Wait a little bit to see if they appear, otherwise verify your stream stats configuration true Show Details XTableView QTableView
xtableview.h
ostinato-1.3.0/client/streamswidget.cpp000066400000000000000000000412771451413623100202320ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #include "streamswidget.h" #include "clipboardhelper.h" #include "findreplace.h" #include "portgrouplist.h" #include "streamconfigdialog.h" #include "streamfileformat.h" #include "streamlistdelegate.h" #include #include #include extern ClipboardHelper *clipboardHelper; StreamsWidget::StreamsWidget(QWidget *parent) : QWidget(parent) { setupUi(this); delegate = new StreamListDelegate; tvStreamList->setItemDelegate(delegate); tvStreamList->verticalHeader()->setDefaultSectionSize( tvStreamList->verticalHeader()->minimumSectionSize()); // Populate StreamList Context Menu Actions tvStreamList->addAction(actionNew_Stream); tvStreamList->addAction(actionEdit_Stream); tvStreamList->addAction(actionDuplicate_Stream); tvStreamList->addAction(actionDelete_Stream); QAction *sep2 = new QAction(this); sep2->setSeparator(true); tvStreamList->addAction(sep2); tvStreamList->addAction(actionFind_Replace); QAction *sep3 = new QAction(this); sep3->setSeparator(true); tvStreamList->addAction(sep3); tvStreamList->addAction(actionOpen_Streams); tvStreamList->addAction(actionSave_Streams); // StreamWidget's actions is an aggegate of all sub-widget's actions addActions(tvStreamList->actions()); // Add the clipboard actions to the context menu of streamList // but not to StreamsWidget's actions since they are already available // in the global Edit Menu QAction *sep4 = new QAction("Clipboard", this); sep4->setSeparator(true); tvStreamList->insertAction(sep2, sep4); tvStreamList->insertActions(sep2, clipboardHelper->actions()); } void StreamsWidget::setPortGroupList(PortGroupList *portGroups) { plm = portGroups; tvStreamList->setModel(plm->getStreamModel()); connect(plm->getStreamModel(), SIGNAL(rowsInserted(QModelIndex, int, int)), SLOT(updateStreamViewActions())); connect(plm->getStreamModel(), SIGNAL(rowsRemoved(QModelIndex, int, int)), SLOT(updateStreamViewActions())); connect(tvStreamList->selectionModel(), SIGNAL(currentChanged(const QModelIndex&, const QModelIndex&)), SLOT(updateStreamViewActions())); connect(tvStreamList->selectionModel(), SIGNAL(selectionChanged(const QItemSelection&, const QItemSelection&)), SLOT(updateStreamViewActions())); tvStreamList->resizeColumnToContents(StreamModel::StreamIcon); tvStreamList->resizeColumnToContents(StreamModel::StreamStatus); updateStreamViewActions(); connect(plm->getStreamModel(), SIGNAL(dataChanged(const QModelIndex&, const QModelIndex&)), this, SLOT(streamModelDataChanged())); connect(plm->getStreamModel(), SIGNAL(modelReset()), this, SLOT(streamModelDataChanged())); } void StreamsWidget::streamModelDataChanged() { if (plm->isPort(currentPortIndex_)) plm->port(currentPortIndex_).recalculateAverageRates(); } StreamsWidget::~StreamsWidget() { delete delegate; } void StreamsWidget::on_tvStreamList_activated(const QModelIndex & index) { if (!index.isValid()) { qDebug("%s: invalid index", __FUNCTION__); return; } qDebug("stream list activated\n"); Q_ASSERT(plm->isPort(currentPortIndex_)); Port &curPort = plm->port(currentPortIndex_); QList streams; streams.append(curPort.mutableStreamByIndex(index.row(), false)); StreamConfigDialog scd(streams, curPort, this); if (scd.exec() == QDialog::Accepted) { curPort.recalculateAverageRates(); curPort.setLocalConfigChanged(true); } } void StreamsWidget::setCurrentPortIndex(const QModelIndex &portIndex) { if (!plm) return; // XXX: We assume portIndex corresponds to sourceModel, not proxyModel; // caller should ensure this qDebug("In %s", __PRETTY_FUNCTION__); currentPortIndex_ = portIndex; plm->getStreamModel()->setCurrentPortIndex(portIndex); updateStreamViewActions(); } void StreamsWidget::updateStreamViewActions() { // For some reason hasSelection() returns true even if selection size is 0 // so additional check for size introduced if (tvStreamList->selectionModel()->hasSelection() && (tvStreamList->selectionModel()->selection().size() > 0)) { qDebug("Has selection %d", tvStreamList->selectionModel()->selection().size()); // If more than one non-contiguous ranges selected, // disable "New" and "Edit" if (tvStreamList->selectionModel()->selection().size() > 1) { actionNew_Stream->setDisabled(true); actionEdit_Stream->setDisabled(true); } else { actionNew_Stream->setEnabled(true); actionEdit_Stream->setEnabled(true); } // Duplicate/Delete are always enabled as long as we have a selection actionDuplicate_Stream->setEnabled(true); actionDelete_Stream->setEnabled(true); } else { qDebug("No selection"); if (plm->isPort(currentPortIndex_)) actionNew_Stream->setEnabled(true); else actionNew_Stream->setDisabled(true); actionEdit_Stream->setDisabled(true); actionDuplicate_Stream->setDisabled(true); actionDelete_Stream->setDisabled(true); } actionFind_Replace->setEnabled(tvStreamList->model()->rowCount() > 0); actionOpen_Streams->setEnabled(plm->isPort(currentPortIndex_)); actionSave_Streams->setEnabled(tvStreamList->model()->rowCount() > 0); } void StreamsWidget::on_actionNew_Stream_triggered() { qDebug("New Stream Action"); QItemSelectionModel* selectionModel = tvStreamList->selectionModel(); if (selectionModel->selection().size() > 1) { qDebug("%s: Unexpected selection size %d, can't add", __FUNCTION__, selectionModel->selection().size()); return; } // In case nothing is selected, insert 1 row at the end StreamModel *streamModel = plm->getStreamModel(); int row = streamModel->rowCount(), count = 1; // In case we have a single range selected; insert as many rows as // in the singe selected range before the top of the selected range if (selectionModel->selection().size() == 1) { row = selectionModel->selection().at(0).top(); count = selectionModel->selection().at(0).height(); } Q_ASSERT(plm->isPort(currentPortIndex_)); Port &curPort = plm->port(currentPortIndex_); QList streams; for (int i = 0; i < count; i++) streams.append(new Stream); StreamConfigDialog scd(streams, curPort, this); scd.setWindowTitle(tr("Add Stream")); if (scd.exec() == QDialog::Accepted) streamModel->insert(row, streams); } void StreamsWidget::on_actionEdit_Stream_triggered() { qDebug("Edit Stream Action"); QItemSelectionModel* streamModel = tvStreamList->selectionModel(); if (!streamModel->hasSelection()) return; Q_ASSERT(plm->isPort(currentPortIndex_)); Port &curPort = plm->port(currentPortIndex_); QList streams; foreach(QModelIndex index, streamModel->selectedRows()) streams.append(curPort.mutableStreamByIndex(index.row(), false)); StreamConfigDialog scd(streams, curPort, this); if (scd.exec() == QDialog::Accepted) { curPort.recalculateAverageRates(); curPort.setLocalConfigChanged(true); } } void StreamsWidget::on_actionDuplicate_Stream_triggered() { QItemSelectionModel* model = tvStreamList->selectionModel(); qDebug("Duplicate Stream Action"); Q_ASSERT(plm->isPort(currentPortIndex_)); if (model->hasSelection()) { bool isOk; int count = QInputDialog::getInt(this, "Duplicate Streams", "Count", 1, 1, 9999, 1, &isOk); if (!isOk) return; QList list; foreach(QModelIndex index, model->selectedRows()) list.append(index.row()); plm->port(currentPortIndex_).duplicateStreams(list, count); } else qDebug("No selection"); } void StreamsWidget::on_actionDelete_Stream_triggered() { qDebug("Delete Stream Action"); QModelIndex index; if (tvStreamList->selectionModel()->hasSelection()) { qDebug("SelectedIndexes %d", tvStreamList->selectionModel()->selectedRows().size()); while(tvStreamList->selectionModel()->selectedRows().size()) { index = tvStreamList->selectionModel()->selectedRows().at(0); plm->getStreamModel()->removeRows(index.row(), 1); } } else qDebug("No selection"); } void StreamsWidget::on_actionFind_Replace_triggered() { qDebug("Find & Replace Action"); Q_ASSERT(plm->isPort(currentPortIndex_)); QItemSelectionModel* selectionModel = tvStreamList->selectionModel(); FindReplaceDialog::Action action; action.selectedStreamsOnly = selectionModel->selection().size() > 0 ? true : false; FindReplaceDialog findReplace(&action, this); if (findReplace.exec() == QDialog::Accepted) { QProgressDialog progress(this); progress.setLabelText(tr("Replacing %1 ...").arg(action.protocolField)); progress.setWindowModality(Qt::WindowModal); int c, fc = 0, sc = 0; // replace counts Port &port = plm->port(currentPortIndex_); if (action.selectedStreamsOnly) { QModelIndexList selected = selectionModel->selectedRows(); int count = selected.size(); progress.setMaximum(count); for (int i = 0; i < count; i++) { QModelIndex index = selected.at(i); Stream *stream = port.mutableStreamByIndex(index.row(), false); c = stream->protocolFieldReplace(action.protocolNumber, action.fieldIndex, action.fieldBitSize, action.findValue, action.findMask, action.replaceValue, action.replaceMask); if (c) { fc += c; sc++; } progress.setValue(i+1); if (progress.wasCanceled()) break; } } else { int count = tvStreamList->model()->rowCount(); progress.setMaximum(count); for (int i = 0; i < count; i++) { Stream *stream = port.mutableStreamByIndex(i, false); c = stream->protocolFieldReplace(action.protocolNumber, action.fieldIndex, action.fieldBitSize, action.findValue, action.findMask, action.replaceValue, action.replaceMask); if (c) { fc += c; sc++; } progress.setValue(i+1); if (progress.wasCanceled()) break; } } if (fc) port.setLocalConfigChanged(true); QMessageBox::information(this, tr("Find & Replace"), tr("%1 fields replaced in %2 streams").arg(fc).arg(sc)); } } void StreamsWidget::on_actionOpen_Streams_triggered() { qDebug("Open Streams Action"); QStringList fileTypes = StreamFileFormat::supportedFileTypes( StreamFileFormat::kOpenFile); QString fileType; static QString dirName; QString fileName; QString errorStr; bool append = true; bool ret; Q_ASSERT(plm->isPort(currentPortIndex_)); if (fileTypes.size()) fileType = fileTypes.at(0); fileName = QFileDialog::getOpenFileName(this, tr("Open Streams"), dirName, fileTypes.join(";;"), &fileType); if (fileName.isEmpty()) goto _exit; if (tvStreamList->model()->rowCount()) { QMessageBox msgBox(QMessageBox::Question, qApp->applicationName(), tr("Append to existing streams? Or overwrite?"), QMessageBox::NoButton, this); QPushButton *appendBtn = msgBox.addButton(tr("Append"), QMessageBox::ActionRole); QPushButton *overwriteBtn = msgBox.addButton(tr("Overwrite"), QMessageBox::ActionRole); QPushButton *cancelBtn = msgBox.addButton(QMessageBox::Cancel); msgBox.exec(); if (msgBox.clickedButton() == cancelBtn) goto _exit; else if (msgBox.clickedButton() == appendBtn) append = true; else if (msgBox.clickedButton() == overwriteBtn) append = false; else Q_ASSERT(false); } ret = plm->port(currentPortIndex_).openStreams(fileName, append, errorStr); if (!ret || !errorStr.isEmpty()) { QMessageBox msgBox(this); QStringList str = errorStr.split("\n\n\n\n"); msgBox.setIcon(ret ? QMessageBox::Warning : QMessageBox::Critical); msgBox.setWindowTitle(qApp->applicationName()); msgBox.setText(str.at(0)); if (str.size() > 1) msgBox.setDetailedText(str.at(1)); msgBox.setStandardButtons(QMessageBox::Ok); msgBox.exec(); } dirName = QFileInfo(fileName).absolutePath(); updateStreamViewActions(); _exit: return; } void StreamsWidget::on_actionSave_Streams_triggered() { qDebug("Save Streams Action"); static QString fileName; QStringList fileTypes = StreamFileFormat::supportedFileTypes( StreamFileFormat::kSaveFile); QString fileType; QString errorStr; QFileDialog::Options options; // On Mac OS with Native Dialog, getSaveFileName() ignores fileType // which we need #if defined(Q_OS_MAC) options |= QFileDialog::DontUseNativeDialog; #endif if (fileTypes.size()) fileType = fileTypes.at(0); Q_ASSERT(plm->isPort(currentPortIndex_)); _retry: fileName = QFileDialog::getSaveFileName(this, tr("Save Streams"), fileName, fileTypes.join(";;"), &fileType, options); if (fileName.isEmpty()) goto _exit; if (QFileInfo(fileName).suffix().isEmpty()) { QString fileExt = fileType.section(QRegExp("[\\*\\)]"), 1, 1); qDebug("Adding extension '%s' to '%s'", qPrintable(fileExt), qPrintable(fileName)); fileName.append(fileExt); if (QFileInfo(fileName).exists()) { if (QMessageBox::warning(this, tr("Overwrite File?"), QString("The file \"%1\" already exists.\n\n" "Do you wish to overwrite it?") .arg(QFileInfo(fileName).fileName()), QMessageBox::Yes|QMessageBox::No, QMessageBox::No) != QMessageBox::Yes) goto _retry; } } fileType = fileType.remove(QRegExp("\\(.*\\)")).trimmed(); if (!fileType.startsWith("Ostinato") && !fileType.startsWith("Python")) { if (QMessageBox::warning(this, tr("Ostinato"), QString("You have chosen to save in %1 format. All stream " "attributes may not be saved in this format.\n\n" "It is recommended to save in native Ostinato format.\n\n" "Continue to save in %2 format?").arg(fileType).arg(fileType), QMessageBox::Yes|QMessageBox::No, QMessageBox::No) != QMessageBox::Yes) goto _retry; } // TODO: all or selected? if (!plm->port(currentPortIndex_).saveStreams(fileName, fileType, errorStr)) QMessageBox::critical(this, qApp->applicationName(), errorStr); else if (!errorStr.isEmpty()) QMessageBox::warning(this, qApp->applicationName(), errorStr); fileName = QFileInfo(fileName).absolutePath(); _exit: return; } ostinato-1.3.0/client/streamswidget.h000066400000000000000000000033061451413623100176660ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _STREAMS_WIDGET_H #define _STREAMS_WIDGET_H #include "ui_streamswidget.h" #include class PortGroupList; class QAbstractItemDelegate; class StreamsWidget : public QWidget, private Ui::StreamsWidget { Q_OBJECT public: StreamsWidget(QWidget *parent = 0); ~StreamsWidget(); void setPortGroupList(PortGroupList *portGroups); public slots: void setCurrentPortIndex(const QModelIndex &portIndex); private slots: void updateStreamViewActions(); void on_tvStreamList_activated(const QModelIndex & index); void on_actionNew_Stream_triggered(); void on_actionEdit_Stream_triggered(); void on_actionDuplicate_Stream_triggered(); void on_actionDelete_Stream_triggered(); void on_actionFind_Replace_triggered(); void on_actionOpen_Streams_triggered(); void on_actionSave_Streams_triggered(); void streamModelDataChanged(); private: PortGroupList *plm{nullptr}; // FIXME: rename to portGroups_? QModelIndex currentPortIndex_; QAbstractItemDelegate *delegate; }; #endif ostinato-1.3.0/client/streamswidget.ui000066400000000000000000000072311451413623100200550ustar00rootroot00000000000000 StreamsWidget 0 0 602 364 Form 0 9 0 0 0 1 Qt::ActionsContextMenu This is the stream list for the selected port A stream is a sequence of one or more packets Right-click to create a stream QFrame::StyledPanel 1 QAbstractItemView::ExtendedSelection QAbstractItemView::SelectRows :/icons/stream_add.png:/icons/stream_add.png New Stream :/icons/stream_delete.png:/icons/stream_delete.png Delete Stream :/icons/stream_edit.png:/icons/stream_edit.png Edit Stream Open Streams ... Save Streams ... :/icons/stream_duplicate.png:/icons/stream_duplicate.png Duplicate Stream :/icons/find.png:/icons/find.png Find && Replace Find & Replace protocol field values XTableView QTableView
xtableview.h
ostinato-1.3.0/client/thememanager.cpp000066400000000000000000000074321451413623100200000ustar00rootroot00000000000000/* Copyright (C) 2021 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #include "thememanager.h" #include "settings.h" #include #include #include #include #include #include ThemeManager *ThemeManager::instance_{nullptr}; ThemeManager::ThemeManager() { themeDir_ = QCoreApplication::applicationDirPath() + "/themes/"; #if defined(Q_OS_MAC) /* * Executable and Theme directory location inside app bundle - * Ostinato.app/Contents/MacOS/ * Ostinato.app/Contents/SharedSupport/themes/ */ themeDir_.replace("/MacOS/", "/SharedSupport/"); #elif defined(Q_OS_UNIX) /* * Possible (but not comprehensive) locations for Ostinato executable * and theme directory locations * * non-install-dir/ * non-install-dir/themes/ * * /usr/[local]/bin/ * /usr/[local]/share/ostinato-controller/themes/ * * /opt/ostinato/bin/ * /opt/ostinato/share/themes/ */ if (themeDir_.contains(QRegularExpression("^/usr/.*/bin/"))) themeDir_.replace("/bin/", "/share/ostinato-controller/"); else if (themeDir_.contains(QRegularExpression("^/opt/.*/bin/"))) themeDir_.replace("/bin/", "/share/"); #endif qDebug("Themes directory: %s", qPrintable(themeDir_)); } QStringList ThemeManager::themeList() { QDir themeDir(themeDir_); themeDir.setFilter(QDir::Files); themeDir.setNameFilters(QStringList() << "*.qss"); themeDir.setSorting(QDir::Name); QStringList themes = themeDir.entryList(); for (QString& theme : themes) theme.remove(".qss"); themes.prepend("default"); return themes; } void ThemeManager::setTheme(QString theme) { // Remove current theme, if we have one QString oldTheme = appSettings->value(kThemeKey).toString(); if (!oldTheme.isEmpty()) { // Remove stylesheet first so that there are // no references to resources when unregistering 'em qApp->setStyleSheet(""); QString rccFile = themeDir_ + oldTheme + ".rcc"; if (QResource::unregisterResource(rccFile)) { qDebug("Unable to unregister theme rccFile %s", qPrintable(rccFile)); } appSettings->setValue(kThemeKey, QVariant()); } if (theme.isEmpty() || (theme == "default")) return; // Apply new theme QFile qssFile(themeDir_ + theme + ".qss"); if (!qssFile.open(QFile::ReadOnly)) { qDebug("Unable to open theme qssFile %s", qPrintable(qssFile.fileName())); return; } // Register theme resource before applying theme style sheet QString rccFile = themeDir_ + theme + ".rcc"; if (!QResource::registerResource(rccFile)) qDebug("Unable to register theme rccFile %s", qPrintable(rccFile)); #if 0 // FIXME: debug only QDirIterator it(":", QDirIterator::Subdirectories); while (it.hasNext()) { qDebug() << it.next(); } #endif QString styleSheet { qssFile.readAll() }; qApp->setStyleSheet(styleSheet); appSettings->setValue(kThemeKey, theme); } ThemeManager* ThemeManager::instance() { if (!instance_) instance_ = new ThemeManager(); return instance_; } ostinato-1.3.0/client/thememanager.h000066400000000000000000000017261451413623100174450ustar00rootroot00000000000000/* Copyright (C) 2021 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _THEME_MANAGER_H #define _THEME_MANAGER_H #include class ThemeManager { public: ThemeManager(); QStringList themeList(); void setTheme(QString theme); static ThemeManager* instance(); private: QString themeDir_; static ThemeManager *instance_; }; #endif ostinato-1.3.0/client/themes/000077500000000000000000000000001451413623100161165ustar00rootroot00000000000000ostinato-1.3.0/client/themes/material-dark.qss000066400000000000000000000704541451413623100213750ustar00rootroot00000000000000/* ------------------------------------------------------------------------ */ /* QtMaterial - https://github.com/UN-GCPDS/qt-material /* By Yeison Cardona - GCPDS /* ------------------------------------------------------------------------ */ *{ color: #ffffff; font-family: Roboto; font-size: 11px; line-height: 0px; selection-background-color: #ffff74; selection-color: #000000; } *:focus { outline: none; } /* ------------------------------------------------------------------------ */ /* Custom colors */ .danger{ color: #dc3545; background-color: transparent; } .warning{ color: #ffc107; background-color: transparent; } .success{ color: #17a2b8; background-color: transparent; } .danger:disabled{ color: rgba(220, 53, 69, 0.4); border-color: rgba(220, 53, 69, 0.4); } .warning:disabled{ color: rgba(255, 193, 7, 0.4); border-color: rgba(255, 193, 7, 0.4); } .success:disabled{ color: rgba(23, 162, 184, 0.4); border-color: rgba(23, 162, 184, 0.4); } .danger:flat:disabled{ background-color: rgba(220, 53, 69, 0.1); } .warning:flat:disabled{ background-color: rgba(255, 193, 7, 0.1); } .success:flat:disabled{ background-color: rgba(23, 162, 184, 0.1); } /* ------------------------------------------------------------------------ */ /* Basic widgets */ QWidget { background-color: #31363b; } QGroupBox, QFrame { background-color: #31363b; border: 2px solid #4f5b62; border-radius: 4px; } QGroupBox.fill_background, QFrame.fill_background { background-color: #232629; border: 2px solid #232629; border-radius: 4px; } QSplitter { background-color: transparent; border: none } QStatusBar { color: #ffffff; background-color: rgba(79, 91, 98, 0.2); border-radius: 0px; } QScrollArea, QStackedWidget, QWidget > QToolBox, QToolBox > QWidget, QTabWidget > QWidget { border: none; } QTabWidget::pane { border: none; } /* ------------------------------------------------------------------------ */ /* Inputs */ QDateTimeEdit, QSpinBox, QDoubleSpinBox, QTextEdit, QLineEdit, QPushButton { color: #ffd740; background-color: #31363b; border: 2px solid #ffd740; border-radius: 4px; height: 24px; } QDateTimeEdit, QSpinBox, QDoubleSpinBox, QTreeView, QListView, QLineEdit, QComboBox { padding-left: 8px; border-radius: 0px; background-color: #232629; border-width: 0 0 2px 0; border-radius: 0px; border-top-left-radius: 4px; border-top-right-radius: 4px; height: 24px; } QPlainTextEdit { border-radius: 4px; padding: 0px 8px; background-color: #31363b; border: 2px solid #4f5b62; } QTextEdit { padding: 0px 8px; border-radius: 4px; background-color: #232629; } QDateTimeEdit:disabled, QSpinBox:disabled, QDoubleSpinBox:disabled, QTextEdit:disabled, QLineEdit:disabled { color: rgba(255, 215, 64, 0.2); background-color: rgba(35, 38, 41, 0.75); border: 2px solid rgba(255, 215, 64, 0.2); border-width: 0 0 2px 0; padding: 0px 8px; border-radius: 0px; border-top-left-radius: 4px; border-top-right-radius: 4px; height: 24px; } /* ------------------------------------------------------------------------ */ /* QComboBox */ QComboBox { color: #ffd740; border: 1px solid #ffd740; border-width: 0 0 2px 0; background-color: #232629; border-radius: 0px; border-top-left-radius: 4px; border-top-right-radius: 4px; height: 28px; } QComboBox:disabled { color: rgba(255, 215, 64, 0.2); background-color: rgba(35, 38, 41, 0.75); border-bottom: 2px solid rgba(255, 215, 64, 0.2); } QComboBox::drop-down { border: none; color: #ffd740; width: 20px; } QComboBox::down-arrow { image: url(:/ostinato.org/themes/material-dark/primary/downarrow.svg); margin-right: 4px; } QComboBox::down-arrow:disabled { image: url(:/ostinato.org/themes/material-dark/disabled/downarrow.svg); margin-right: 4px; } QComboBox QAbstractItemView { background-color: #232629; border: 2px solid #4f5b62; border-radius: 4px; } QComboBox[frame='false'] { color: #ffd740; background-color: transparent; border: 1px solid transparent; } QComboBox[frame='false']:disabled { color: rgba(255, 215, 64, 0.2); } /* ------------------------------------------------------------------------ */ /* Spin buttons */ QDateTimeEdit::up-button, QDoubleSpinBox::up-button, QSpinBox::up-button { subcontrol-origin: border; subcontrol-position: top right; width: 20px; image: url(:/ostinato.org/themes/material-dark/primary/uparrow.svg); border-width: 0px; margin-right: 5px; } QDateTimeEdit::up-button:disabled, QDoubleSpinBox::up-button:disabled, QSpinBox::up-button:disabled { image: url(:/ostinato.org/themes/material-dark/disabled/uparrow.svg); } QDateTimeEdit::down-button, QDoubleSpinBox::down-button, QSpinBox::down-button { subcontrol-origin: border; subcontrol-position: bottom right; width: 20px; image: url(:/ostinato.org/themes/material-dark/primary/downarrow.svg); border-width: 0px; border-top-width: 0; margin-right: 5px; } QDateTimeEdit::down-button:disabled, QDoubleSpinBox::down-button:disabled, QSpinBox::down-button:disabled { image: url(:/ostinato.org/themes/material-dark/disabled/downarrow.svg); } /* ------------------------------------------------------------------------ */ /* QPushButton */ QPushButton { text-transform: uppercase; margin: 0px; padding: 0px 8px; height: 24px; font-weight: bold; border-radius: 4px; } QPushButton:checked, QPushButton:pressed { color: #31363b; background-color: #ffd740; } QPushButton:flat { margin: 0px; color: #ffd740; border: none; background-color: transparent; } QPushButton:flat:hover { background-color: rgba(255, 215, 64, 0.2); } QPushButton:flat:pressed, QPushButton:flat:checked { background-color: rgba(255, 215, 64, 0.1); } QPushButton:disabled { color: rgba(79, 91, 98, 0.75); background-color: transparent; border-color: #4f5b62; } QPushButton:flat:disabled { color: rgba(79, 91, 98, 0.75); background-color: rgba(79, 91, 98, 0.25); border: none; } QPushButton:disabled { border: 2px solid rgba(79, 91, 98, 0.75); } QPushButton:checked:disabled { color: #232629; background-color: #4f5b62; border-color: #4f5b62; } /* ------------------------------------------------------------------------ */ /* QTabBar */ QTabBar{ text-transform: uppercase; font-weight: bold; } QTabBar::tab { color: #ffffff; border: 0px; } QTabBar::tab:bottom, QTabBar::tab:top{ padding: 0 8px; height: 20px; } QTabBar::tab:left, QTabBar::tab:right{ padding: 8px 0; width: 20px; } QTabBar::tab:top:selected, QTabBar::tab:top:hover { color: #ffd740; border-bottom: 2px solid #ffd740; } QTabBar::tab:bottom:selected, QTabBar::tab:bottom:hover { color: #ffd740; border-top: 2px solid #ffd740; } QTabBar::tab:right:selected, QTabBar::tab:right:hover { color: #ffd740; border-left: 2px solid #ffd740; } QTabBar::tab:left:selected, QTabBar::tab:left:hover { color: #ffd740; border-right: 2px solid #ffd740; } QTabBar QToolButton:hover, QTabBar QToolButton { border: 20px; background-color: #31363b; } QTabBar QToolButton::up-arrow { image: url(:/ostinato.org/themes/material-dark/disabled/uparrow2.svg); } QTabBar QToolButton::up-arrow:hover { image: url(:/ostinato.org/themes/material-dark/primary/uparrow2.svg); } QTabBar QToolButton::down-arrow { image: url(:/ostinato.org/themes/material-dark/disabled/downarrow2.svg); } QTabBar QToolButton::down-arrow:hover { image: url(:/ostinato.org/themes/material-dark/primary/downarrow2.svg); } QTabBar QToolButton::right-arrow { image: url(:/ostinato.org/themes/material-dark/primary/rightarrow2.svg); } QTabBar QToolButton::right-arrow:hover { image: url(:/ostinato.org/themes/material-dark/disabled/rightarrow2.svg); } QTabBar QToolButton::left-arrow { image: url(:/ostinato.org/themes/material-dark/primary/leftarrow2.svg); } QTabBar QToolButton::left-arrow:hover { image: url(:/ostinato.org/themes/material-dark/disabled/leftarrow2.svg); } QTabBar::close-button { image: url(:/ostinato.org/themes/material-dark/disabled/tab_close.svg); } QTabBar::close-button:hover { image: url(:/ostinato.org/themes/material-dark/primary/tab_close.svg); } /* ------------------------------------------------------------------------ */ /* QGroupBox */ QGroupBox { padding: 8px; padding-top: 28px; line-height: ; text-transform: uppercase; font-size: ; } QGroupBox::title { color: rgba(255, 255, 255, 0.4); subcontrol-origin: margin; subcontrol-position: top left; padding: 8px; background-color: #31363b; background-color: transparent; height: 28px; } /* ------------------------------------------------------------------------ */ /* QRadioButton and QCheckBox labels */ QRadioButton, QCheckBox { spacing: 4px; color: #ffffff; line-height: 14px; height: 28px; background-color: transparent; spacing: 5px; } QRadioButton:disabled, QCheckBox:disabled { color: rgba(255, 255, 255, 0.3); } /* ------------------------------------------------------------------------ */ /* General Indicators */ QGroupBox::indicator { width: 16px; height: 16px; border-radius: 3px; } QMenu::indicator, QListView::indicator, QTableWidget::indicator, QRadioButton::indicator, QCheckBox::indicator { width: 20px; height: 20px; border-radius: 4px; } /* ------------------------------------------------------------------------ */ /* QListView Indicator */ QListView::indicator:checked, QListView::indicator:checked:selected, QListView::indicator:checked:focus { image: url(:/ostinato.org/themes/material-dark/primary/checklist.svg); } QListView::indicator:checked:selected:active { image: url(:/ostinato.org/themes/material-dark/primary/checklist_invert.svg); } QListView::indicator:checked:disabled { image: url(:/ostinato.org/themes/material-dark/disabled/checklist.svg); } QListView::indicator:indeterminate, QListView::indicator:indeterminate:selected, QListView::indicator:indeterminate:focus { image: url(:/ostinato.org/themes/material-dark/primary/checklist_indeterminate.svg); } QListView::indicator:indeterminate:selected:active { image: url(:/ostinato.org/themes/material-dark/primary/checklist_indeterminate_invert.svg); } QListView::indicator:indeterminate:disabled { image: url(:/ostinato.org/themes/material-dark/disabled/checklist_indeterminate.svg); } /* ------------------------------------------------------------------------ */ /* QTableView Indicator */ QTableView::indicator:enabled:checked, QTableView::indicator:enabled:checked:selected, QTableView::indicator:enabled:checked:focus { image: url(:/ostinato.org/themes/material-dark/primary/checkbox_checked.svg); } QTableView::indicator:checked:selected:active { image: url(:/ostinato.org/themes/material-dark/primary/checkbox_checked_invert.svg); } QTableView::indicator:disabled:checked, QTableView::indicator:disabled:checked:selected, QTableView::indicator:disabled:checked:focus { image: url(:/ostinato.org/themes/material-dark/disabled/checkbox_checked.svg); } QTableView::indicator:enabled:unchecked, QTableView::indicator:enabled:unchecked:selected, QTableView::indicator:enabled:unchecked:focus { image: url(:/ostinato.org/themes/material-dark/primary/checkbox_unchecked.svg); } QTableView::indicator:unchecked:selected:active { image: url(:/ostinato.org/themes/material-dark/primary/checkbox_unchecked_invert.svg); } QTableView::indicator:disabled:unchecked, QTableView::indicator:disabled:unchecked:selected, QTableView::indicator:disabled:unchecked:focus { image: url(:/ostinato.org/themes/material-dark/disabled/checkbox_unchecked.svg); } QTableView::indicator:enabled:indeterminate, QTableView::indicator:enabled:indeterminate:selected, QTableView::indicator:enabled:indeterminate:focus { image: url(:/ostinato.org/themes/material-dark/primary/checkbox_indeterminate.svg); } QTableView::indicator:indeterminate:selected:active { image: url(:/ostinato.org/themes/material-dark/primary/checkbox_indeterminate_invert.svg); } QTableView::indicator:disabled:indeterminate, QTableView::indicator:disabled:indeterminate:selected, QTableView::indicator:disabled:indeterminate:focus { image: url(:/ostinato.org/themes/material-dark/disabled/checkbox_indeterminate.svg); } /* ------------------------------------------------------------------------ */ /* QCheckBox and QGroupBox Indicator */ QCheckBox::indicator:checked, QGroupBox::indicator:checked { image: url(:/ostinato.org/themes/material-dark/primary/checkbox_checked.svg); } QCheckBox::indicator:unchecked, QGroupBox::indicator:unchecked { image: url(:/ostinato.org/themes/material-dark/primary/checkbox_unchecked.svg); } QCheckBox::indicator:indeterminate, QGroupBox::indicator:indeterminate { image: url(:/ostinato.org/themes/material-dark/primary/checkbox_indeterminate.svg); } QCheckBox::indicator:checked:disabled, QGroupBox::indicator:checked:disabled { image: url(:/ostinato.org/themes/material-dark/disabled/checkbox_checked.svg); } QCheckBox::indicator:unchecked:disabled, QGroupBox::indicator:unchecked:disabled { image: url(:/ostinato.org/themes/material-dark/disabled/checkbox_unchecked.svg); } QCheckBox::indicator:indeterminate:disabled, QGroupBox::indicator:indeterminate:disabled { image: url(:/ostinato.org/themes/material-dark/disabled/checkbox_indeterminate.svg); } /* ------------------------------------------------------------------------ */ /* QRadioButton Indicator */ QRadioButton::indicator:checked { image: url(:/ostinato.org/themes/material-dark/primary/radiobutton_checked.svg); } QRadioButton::indicator:unchecked { image: url(:/ostinato.org/themes/material-dark/primary/radiobutton_unchecked.svg); } QRadioButton::indicator:checked:disabled { image: url(:/ostinato.org/themes/material-dark/disabled/radiobutton_checked.svg); } QRadioButton::indicator:unchecked:disabled { image: url(:/ostinato.org/themes/material-dark/disabled/radiobutton_unchecked.svg); } /* ------------------------------------------------------------------------ */ /* QDockWidget */ QDockWidget { color: #ffffff; text-transform: uppercase; border: 2px solid #232629; titlebar-close-icon: url(:/ostinato.org/themes/material-dark/primary/close.svg); titlebar-normal-icon: url(:/ostinato.org/themes/material-dark/primary/float.svg); border-radius: 4px; } QDockWidget::title { text-align: left; padding-left: 28px; padding: 3px; margin-top: 4px; } /* ------------------------------------------------------------------------ */ /* QComboBox indicator */ QComboBox::indicator:checked { image: url(:/ostinato.org/themes/material-dark/primary/checklist.svg); } QComboBox::indicator:checked:selected { image: url(:/ostinato.org/themes/material-dark/primary/checklist_invert.svg); } /* ------------------------------------------------------------------------ */ /* Menu Items */ QComboBox::item, QCalendarWidget QMenu::item, QMenu::item { height: 20px; border: 8px solid transparent; color: #ffffff; } QCalendarWidget QMenu::item, QMenu::item { padding: 0px 16px 0px 8px; /* pyqt5 */ } QComboBox::item:selected, QCalendarWidget QMenu::item:selected, QMenu::item:selected { color: #000000; background-color: #ffff74; border-radius: 0px; } QComboBox::item:disabled, QCalendarWidget QMenu::item:disabled, QMenu::item:disabled { color: rgba(255, 255, 255, 0.3); } /* ------------------------------------------------------------------------ */ /* QMenu */ QCalendarWidget QMenu, QMenu { background-color: #232629; border: 2px solid #4f5b62; border-radius: 4px; } QMenu::separator { height: 2px; background-color: #4f5b62; margin-left: 2px; margin-right: 2px; } QMenu::right-arrow{ image: url(:/ostinato.org/themes/material-dark/primary/rightarrow.svg); width: 8px; height: 8px; } QMenu::right-arrow:selected{ image: url(:/ostinato.org/themes/material-dark/disabled/rightarrow.svg); } QMenu::indicator:non-exclusive:unchecked { image: url(:/ostinato.org/themes/material-dark/primary/checkbox_unchecked.svg); } QMenu::indicator:non-exclusive:unchecked:selected { image: url(:/ostinato.org/themes/material-dark/primary/checkbox_unchecked_invert.svg); } QMenu::indicator:non-exclusive:checked { image: url(:/ostinato.org/themes/material-dark/primary/checkbox_checked.svg); } QMenu::indicator:non-exclusive:checked:selected { image: url(:/ostinato.org/themes/material-dark/primary/checkbox_checked_invert.svg); } QMenu::indicator:exclusive:unchecked { image: url(:/ostinato.org/themes/material-dark/primary/radiobutton_unchecked.svg); } QMenu::indicator:exclusive:unchecked:selected { image: url(:/ostinato.org/themes/material-dark/primary/radiobutton_unchecked_invert.svg); } QMenu::indicator:exclusive:checked { image: url(:/ostinato.org/themes/material-dark/primary/radiobutton_checked.svg); } QMenu::indicator:exclusive:checked:selected { image: url(:/ostinato.org/themes/material-dark/primary/radiobutton_checked_invert.svg); } /* ------------------------------------------------------------------------ */ /* QMenuBar */ QMenuBar { background-color: #232629; color: #ffffff; } QMenuBar::item { height: 24px; padding: 8px; background-color: transparent; color: #ffffff; } QMenuBar::item:selected, QMenuBar::item:pressed { color: #000000; background-color: #ffff74; } /* ------------------------------------------------------------------------ */ /* QToolBox */ QToolBox::tab { background-color: #232629; color: #ffffff; text-transform: uppercase; border-radius: 4px; padding-left: 15px; } QToolBox::tab:selected, QToolBox::tab:hover { background-color: rgba(255, 215, 64, 0.2); } /* ------------------------------------------------------------------------ */ /* QProgressBar */ QProgressBar { border-radius: 0; background-color: #4f5b62; text-align: center; color: transparent; } QProgressBar::chunk { background-color: #ffd740; } /* ------------------------------------------------------------------------ */ /* QScrollBar */ QScrollBar:horizontal { border: 0; background: #232629; height: 0px; } QScrollBar:vertical { border: 0; background: #232629; width: 0px; } QScrollBar::handle { background: rgba(255, 215, 64, 0.1); } QScrollBar::handle:horizontal { min-width: 16px; } QScrollBar::handle:vertical { min-height: 16px; } QScrollBar::handle:vertical:hover, QScrollBar::handle:horizontal:hover { background: #ffd740; } QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical, QScrollBar::add-line:horizontal, QScrollBar::sub-line:horizontal { border: 0; background: transparent; width: 0px; height: 0px; } /* ------------------------------------------------------------------------ */ /* QScrollBar-Big */ QScrollBar.big:horizontal { border: 0; background: #232629; height: 28px; } QScrollBar.big:vertical { border: 0; background: #232629; width: 28px; } QScrollBar.big::handle, QScrollBar.big::handle:vertical:hover, QScrollBar.big::handle:horizontal:hover { background: #ffd740; } QScrollBar.big::handle:horizontal { min-width: 16px; } QScrollBar.big::handle:vertical { min-height: 16px; } QScrollBar.big::add-line:vertical, QScrollBar.big::sub-line:vertical, QScrollBar.big::add-line:horizontal, QScrollBar.big::sub-line:horizontal { border: 0; background: transparent; width: 0px; height: 0px; } /* ------------------------------------------------------------------------ */ /* QSlider */ QSlider:horizontal { min-height: 16px; max-height: 16px; } QSlider:vertical { min-width: 16px; max-width: 16px; } QSlider::groove:horizontal { height: 4px; background: #393939; margin: 0 4px; } QSlider::groove:vertical { width: 4px; background: #393939; margin: 4px 0; border-radius: 16px; } QSlider::handle:horizontal { image: url(:/ostinato.org/themes/material-dark/primary/slider.svg); width: 16px; height: 16px; margin: -16px -4px; } QSlider::handle:vertical { image: url(:/ostinato.org/themes/material-dark/primary/slider.svg); border-radius: 16px; width: 16px; height: 16px; margin: -4px -16px; } QSlider::add-page { background: #232629; } QSlider::sub-page { background: #ffd740; } /* ------------------------------------------------------------------------ */ /* QLabel */ QLabel { border: none; background: transparent; color: #ffffff } QLabel:disabled { color: rgba(255, 255, 255, 0.2) } /* ------------------------------------------------------------------------ */ /* VLines and HLinex */ QFrame[frameShape="4"] { border-width: 1px 0 0 0; background: none; } QFrame[frameShape="5"] { border-width: 0 1px 0 0; background: none; } QFrame[frameShape="4"], QFrame[frameShape="5"] { border-color: #4f5b62; } /* ------------------------------------------------------------------------ */ /* QToolBar */ QToolBar { background: #31363b; border: 0px solid; } QToolBar:horizontal { border-bottom: 1px solid #4f5b62; } QToolBar:vertical { border-right: 1px solid #4f5b62; } QToolBar::handle:horizontal { image: url(:/ostinato.org/themes/material-dark/primary/toolbar-handle-horizontal.svg); } QToolBar::handle:vertical { image: url(:/ostinato.org/themes/material-dark/primary/toolbar-handle-vertical.svg); } QToolBar::separator:horizontal { border-right: 1px solid #4f5b62; border-left: 1px solid #4f5b62; width: 1px; } QToolBar::separator:vertical { border-top: 1px solid #4f5b62; border-bottom: 1px solid #4f5b62; height: 1px; } /* ------------------------------------------------------------------------ */ /* QToolButton */ QToolButton { background: #31363b; border: 0px; height: 28px; margin: 3px; padding: 3px; border-right: 12px solid #31363b; border-left: 12px solid #31363b; } QToolButton:hover { background: #4f5b62; border-right: 12px solid #4f5b62; border-left: 12px solid #4f5b62; } QToolButton:pressed { background: #232629; border-right: 12px solid #232629; border-left: 12px solid #232629; } QToolButton:checked { background: #4f5b62; border-left: 12px solid #4f5b62; border-right: 12px solid #ffd740; } /* ------------------------------------------------------------------------ */ /* General viewers */ QTableView { background-color: #31363b; border: 1px solid #232629; border-radius: 4px; } QTreeView, QListView { border-radius: 4px; padding: 4px; margin: 0px; border: 0px; } QTableView::item, QTreeView::item, QListView::item { padding: 4px; min-height: 24px; color: #ffffff; selection-color: #ffffff; /* For Windows */ border-color: transparent; /* Fix #34 */ } /* ------------------------------------------------------------------------ */ /* Items Selection */ QTableView::item:selected, QTreeView::item:selected, QListView::item:selected { background-color: rgba(255, 215, 64, 0.2); selection-background-color: rgba(255, 215, 64, 0.2); color: #ffffff; selection-color: #ffffff; /* For Windows */ } QTableView::item:selected:focus, QTreeView::item:selected:focus, QListView::item:selected:focus { background-color: #ffd740; selection-background-color: #ffd740; color: #000000; selection-color: #000000; /* For Windows */ } QTableView { selection-background-color: rgba(255, 215, 64, 0.2); } QTableView:focus { selection-background-color: #ffd740; } QTableView::item:disabled { color: rgba(255, 255, 255, 0.3); selection-color: rgba(255, 255, 255, 0.3); background-color: #232629; selection-background-color: #232629; } /* ------------------------------------------------------------------------ */ /* QTreeView */ QTreeView::branch{ background-color: #232629; } QTreeView::branch:closed:has-children:has-siblings, QTreeView::branch:closed:has-children:!has-siblings { image: url(:/ostinato.org/themes/material-dark/primary/branch-closed.svg); } QTreeView::branch:open:has-children:!has-siblings, QTreeView::branch:open:has-children:has-siblings { image: url(:/ostinato.org/themes/material-dark/primary/branch-open.svg); } QTreeView::branch:has-siblings:!adjoins-item { border-image: url(:/ostinato.org/themes/material-dark/disabled/vline.svg) 0; } QTreeView::branch:has-siblings:adjoins-item { border-image: url(:/ostinato.org/themes/material-dark/disabled/branch-more.svg) 0; } QTreeView::branch:!has-children:!has-siblings:adjoins-item, QTreeView::branch:has-children:!has-siblings:adjoins-item { border-image: url(:/ostinato.org/themes/material-dark/disabled/branch-end.svg) 0; } QTreeView QHeaderView::section { border: none; } /* ------------------------------------------------------------------------ */ /* Custom buttons */ QPushButton.danger { border-color: #dc3545; color: #dc3545; } QPushButton.danger:checked, QPushButton.danger:pressed { color: #31363b; background-color: #dc3545; } QPushButton.warning{ border-color: #ffc107; color: #ffc107; } QPushButton.warning:checked, QPushButton.warning:pressed { color: #31363b; background-color: #ffc107; } QPushButton.success { border-color: #17a2b8; color: #17a2b8; } QPushButton.success:checked, QPushButton.success:pressed { color: #31363b; background-color: #17a2b8; } QPushButton.danger:flat:hover { background-color: rgba(220, 53, 69, 0.2); } QPushButton.danger:flat:pressed, QPushButton.danger:flat:checked { background-color: rgba(220, 53, 69, 0.1); color: #dc3545; } QPushButton.warning:flat:hover { background-color: rgba(255, 193, 7, 0.2); } QPushButton.warning:flat:pressed, QPushButton.warning:flat:checked { background-color: rgba(255, 193, 7, 0.1); color: #ffc107; } QPushButton.success:flat:hover { background-color: rgba(23, 162, 184, 0.2); } QPushButton.success:flat:pressed, QPushButton.success:flat:checked { background-color: rgba(23, 162, 184, 0.1); color: #17a2b8; } /* ------------------------------------------------------------------------ */ /* QTableView */ QTableCornerButton::section { background-color: #232629; border-radius: 0px; border-right: 1px solid; border-bottom: 1px solid; border-color: #31363b; } QTableView { alternate-background-color: rgba(35, 38, 41, 0.7); } QHeaderView { border: none; } QHeaderView::section { color: rgba(255, 255, 255, 0.7); text-transform: uppercase; background-color: #232629; padding: 0 16px; height: 28px; border-radius: 0px; border-right: 1px solid; border-bottom: 1px solid; border-color: #31363b; } QHeaderView::section:vertical { } QHeaderView::section:horizontal { } /* ------------------------------------------------------------------------ */ /* QLCDNumber */ QLCDNumber { color: #ffd740; background-color:rgba(255, 215, 64, 0.1); border: 1px solid rgba(255, 215, 64, 0.3); border-radius: 4px; } /* ------------------------------------------------------------------------ */ /* QCalendarWidget */ #qt_calendar_prevmonth { qproperty-icon: url(:/ostinato.org/themes/material-dark/primary/leftarrow.svg); } #qt_calendar_nextmonth { qproperty-icon: url(:/ostinato.org/themes/material-dark/primary/rightarrow.svg); } /* ------------------------------------------------------------------------ */ /* Inline QLineEdit */ QTreeView QLineEdit, QTableView QLineEdit, QListView QLineEdit { color: #ffffff; background-color: #232629; border: 1px solid unset; border-radius: unset; padding: unset; padding-left: unset; height: unset; border-width: unset; border-top-left-radius: unset; border-top-right-radius: unset; } /* ------------------------------------------------------------------------ */ /* QToolTip */ QToolTip { padding: 4px; border: 1px solid #31363b; border-radius: 4px; color: #ffffff; background-color: #4f5b62; } /* ------------------------------------------------------------------------ */ /* QDialog */ QDialog QToolButton:disabled { background-color: #232629; color: #ffffff } /* ------------------------------------------------------------------------ */ /* Grips */ QMainWindow::separator:vertical, QSplitter::handle:horizontal { image: url(:/ostinato.org/themes/material-dark/primary/splitter-horizontal.svg); } QMainWindow::separator:horizontal, QSplitter::handle:vertical { image: url(:/ostinato.org/themes/material-dark/primary/splitter-vertical.svg); } QSizeGrip { image: url(:/ostinato.org/themes/material-dark/primary/sizegrip.svg); background-color: transparent; } QMenuBar QToolButton:hover, QMenuBar QToolButton:pressed, QMenuBar QToolButton { border-width: 0; border-left: 10px; border-image: url(:/ostinato.org/themes/material-dark/primary/rightarrow2.svg); background-color: transparent; } /* Generated by qt-material 2.8.18 */ ostinato-1.3.0/client/themes/material-dark.rcc000066400000000000000000005074671451413623100213470ustar00rootroot00000000000000qresˆW‹ì!zxœíYYãÆ~7àÿ@Ð/;°Øì“lÊ£1, H^üPdK¢—b dkFÚ_ŸjR¤xI;{,²ÌŒXU}T}uuëñûÓ>wžUYeºX¹a×QE¢Ó¬Ø®Üþüƒ']§2q‘ƹ.ÔÊ-´ûýÓ×_=VÏÛ¯¿r†Õ2MVîΘÃÒ÷Ç2GºÜúiâ«\íUa*Ÿ â»=ùä*Ÿ”*6Ù³Jô~¯‹ªZTßô¥ËtÓ‰¿¼¼ VK‘(Š|L}J=ðªsaâ“7 ûœK1Æ>ðz¢¯[V`œüvò-UúX&j*”ñßþü¶cz¥&íÏ“ïª$>¨Áº-±1C¼WÕ!NTå·ôf‚—,5»•KqóºSÙvg®ïÏ™zù‹>­\ì`G ‘ øõÓEê :i(YºrAYyy»,¹ì{"ζN¨’„mÄ¡˜bGNr¬ŒÞ?4£[½—©N¬+w]ÆE²óT‘¢Î¢ÝêtÐ¥ñ6Y®i§÷Ê?«¬Ò…ÿV=«\¬/ù‡Ì%.Ÿ%ºøWž…ÅùNépŠ‚yî¹å>Yöcª6U-×X¾R×ñf§Ý^j-Ü]ÇÕÇ9Ä[ðå\—+÷›Mý´œµ.SU¶¼ ~†< `gæÜÄa;»i;q'€oT»8Õ/à î{­÷+—…\@„ᄟ€¿Š8åTˆ)×n Ð!çlÂŒïXdépšNp,K+‘Çgêo%‹Z™j§_¶¥µ£)j2ò%+@%ïâó$¢SÍ/"mŒù-·xç;¼}|ÊöÙ{»$«Aßú›8¿:Äm›Ô®²SÉ;U®u\¦£µ]ŽYªª–©Šøà­×6Ògù–åb³»5A-PèW¬à©t«¼}–tV˜‹¿JòÞÊzý›JÌÝÝ×sÀž@2Ô‡¥«½Öf÷a•_µým®×q>”Ød\¥Üf…gô¡çO=F®6fžS6þ;ÇZkclO´v‘{nÐEg©MlêLŽ›”yí: Dàe¤ã˜³­K§³%ºÕ†¨¥D!¿Õþ`kTÝ/È+ùB³á†hÀ…dŒN¸çyn ºªq4زVeë\ µ„ 1PÓ1ÙBta÷œg…‚R’ŸÇrŒžý´ÐÒêtÐæZÆ^™8MÜ+-ItV†þdùÓÛžÚ“dù‹.ßu+:މ×úø»OWúcš,¡£ØÇæ)ÛC¾°ÝÈ·Ð@<úWÆPÚb×›·™¹TMs2Û¦¥É>³£ü˜,Ïÿj—iõîM›™\=ÕË6;]ü‹2­²~_ÛG¿µFóº{g¯Ñßl]p¦©u[êãañºrëÒáöì\º!ú‹ÊZÄ" óب7xáAÓƒB,™xèàØ]ZòðŠ}o°m™Þ@­‡œE l.¯‚- ©‹0¡‚|A%OB1{¸Â×[¨]JºÛRµÁ"räé–Š"PÒì¨y#R,Hˆå\ò…ÇC‚2Âú Â’6I¦ïY»[ÉK”M¬¶$2DBáŽFTæl#ñÒ,ÉwйåËŽõ'¯e`Ä#&iH¢ï*SêwjyiŠ0¾šÂ‚8„f›3ÖÒmĦ–àEÚ'þ™yH÷UeõÙ,yKKc¨ÆeŸ›]õ¨z³©”Yv¸*qˆ!ç{u+¶l˜ŽÕÆ 5ºÐú»Cb‘} % @êxáú‘Ê ”ˆÖôQXK;¿Žg³à[l¤dV×EéŒbtéA?õ›c©‰ëN—ª nl`CKàõ¼+Üûû¶tUn² sÖŠ=š@Œ‡gŒvÏs=ëº&øâ¾µûŽá/·‘aJ0›€Çx äØjÀ£‚!É¥œX hˆ.C&£1¯íË™€Œ$Ãqzëγ¾ æ¼ÇnV†S_ÿHB*å¼üL²2ÉÕK „P éJ{žk#L˜§z$¿Ä!ýS*`BJú¹ÃЫÞu®Þx\<ÌÙ܃l…°Œ‚I€ÔpÁùNà*ëƒ{ ‚Þ²IP}È as|F}T7têÕ” FŸ¸rFxmåäýÖþ׽Góó¦z>;ôÒyÿ¥þnð¥)¾ÓßlçàXrqÆy_¼úú´øD-ðˆðˆN ]Ý ›ÖM[6‘8zM|£ë|Žyx«â Ølñ­{ þ)=€¯üv»Wùi—›n3q×ÇA§ußeŬ»s‰Â àw·§/ŽQÀ›žz6ñåi·á‡Ñm”¿í]>m»k§îÓ ™Ï;œ@/5ôÒÂßG£Eôˆ‚@dz›¾wÃi“$ä@ŠNÉ‚cH’a ™“8Ú8¢ñ–Åÿ#;c£Óð# }Ã2<œµL«y8o˜æëˆpRÜkËÈÐÚ”ÌX&@!˜†÷-3ÍLÍÿG{ƒÿÿ Ùs’ Ÿ image/svg+xml É ¥xœíXÝã¶ÿu^nK")Š¢ïhA4/IŠ} d‰¶•“EA¢×öýõê“²ì½ Ò zèi±»ÒÌpHÎüæƒ\{>äèYVu¦ŠÇqñÉ"QiVìÿüõ{G,P­ã"sUÈÇE¡ß>}ùÅúoŽƒ¾«d¬eŠN™Þ£‹wu—½Ùk]®<ït:¹YGtUµóãÀP\?ï¾ü!sõ*MݘòXålšx2—YèÚ#.ñ–|2Ê'fÙ³LÔá ŠºZÔ_ÙÒUºÄÍ’N~#E¢(ò0õ(u@©/…ŽÏÎÕXXç­±cìÏ}¥ØªË–ð;È÷·VÇ*‘[(ÝBjïí¯o¦ƒÝT§¶žÞ°“y'Ö.⃬Ë8‘µ×Ó[§,ÕûÇÅíç^f»½¿Ÿ3yú»:?.0Â(piD8çl|ë¤FÄ–’¥ ج辺)Wƒ v#ꂞ4æ"J|â/Å$r0qH§´ßî*U‰Y>(̳TVî`ÄA­<—ªÒÎ6Ëe+éíÕAz™ÕªðÞÊg™«ÒÀÇ+3 ”¸Ò^–¨â·<ÓÒ-‹;úÎi ®‰ømî¥ç>ö:•Ûº‘k7o>éy-sØ‹Y^jŒj‰nâºsBe¼øæªz\|µmžž³Ql¾çñæ™òø7Ó—6n{ýý¢âAߨ÷qªNàý÷½R  7Ä,"þŒŸD„ðà0˜s/†+0%‘͸àߣqŽs,2 ±Sžç ŽUe$òø"aûÍ?ÒKÕ{uÚUÆ’º:ÊÙØSVÀ¦œè$¢ó½w"=ø &ó=t2&îñ./ðñ9;dï%¬’ÌdÌlûoã|„Ä}«4`ÙËä¬6*®Ò«]Ž2õËÔE\:› ï›|ÃrÊXïï)h õŠ™î¤sÈÒRe…þ°ø«$_šYm~—‰~qõ˜rˆBZú°t}PJï?¼åW-—«MœO%¶™¨T»¬p´*-eL°¥]–PŸ°{B˜Ò¤½‰zËÚÃLN"Mª4EÎwÉXäû§Ö€]±"ß@7–¯ 8&4oNÏÀ.‹|AC}SëJ½“«®ÑÁ¸#´¥q=3óýžnµ©Mürí” ¨•UW¯XOKc¨¯U_ÚUYTµÝÖR¯†Œ›(cÈâNÓ^­Z&2»AU¡êÖ×Foý„hàú‘y–°K—Ã(r„‹›GH'\RÆ]ÚÐ@vÃFýûZ›q¾ñþŒ5tFª£hU9Ð#=ÇúXÉI¾êœ3d(ˆÏP˜x¦±| wÇþ¹%››±Lž¼iE‹Àéá8“Ìör] >÷ïµû+ðÛ•a*0[ Îg<×V |W0!f…ººœ‰ÐÑ5¯ï´ý2’¯ÓÛp,íø‚ßBY¬çXÿž„TÊØ ø)x2ɪ$—×¾4î‚â‚Î\iÎh}¤‘óÜŒdwƒ8¤I@q?‚~ên°ªw¥´iJ½È ›;­\,"> Æ]lî™»ÀÏ„º<à7x£ËfAõQ\ ›aþ ¹ìuCg«¦Ì|ô‘+g„gñØWNf·æ»é=Ú¿˜µÕó9Ð{ôþs)üoÀàsSüBS|·ƒcIÆÛX±>o>R üBDXDg…®é8ìΟ×MS6Ý€s8zͰ1ô@à|†Yx¯bË#Ÿû7‹oÓùá_Òpàÿð{…#ÿ/ír63¸NÖË n 7äœÍànN_ »Ü÷ýùYÁ °–çÝR„®.¡¼uù´®†·I2¿ pp½Ô¤w¿íÞó°ˆó€üqê­‹M“$!RÌ%K†!I†\ø(AÚ~Î3ëYVuªŠ]d[²ˆU’ûýÏ_p„mÕ:*’(S…ÜØ…²¿úú«ÇúyÿõW–eÁô¢^'ñÆ>h]®=¯ËXå¹*êfjQ3–®’Ý ~:Üm¤p†"!H8õ¥ÐÑٙͅsÞšKBðF¢¯[×`œ~ùžàÖêXÅr¥[Hí½ýõíÀt›èd¼NZ¼«ã¨”“}{bk†(—uŲözz»À)MôacÔ2ÝôuüœÊÓßÔyc# Y¾KBÌ9g×oÔÕ鸥¤ÉÆeE7ê¶\£Ã%Ö)x,$‚peD°ƒ°ƒý•k­ò‡vv¯÷:Q±ÑccÇ¿ËÒZÿ–‰Ô²ÊÓ"ÒÒÌ;l(Ï¥ª´³K3ÙNõ*—ÞE¦µ*¼·òYfª4å•©JTi/Uñ[–Âzeqg½sR‚ÓB~›{é¹O†ý˜È]Ýȵf1Cb[^Ë”3ÇKŒ¹G¢Û¨îÜdYe´‡ÀÎTµ±¿Ù5Ÿž³UU"«žÇ›Ï”§Àó©¾´IÙ¯ßÚ,< ;õ!JÔ âbÁ}¯T¾±qý€#†ü‚'paÀ–\s(â2J.¸àð£qŽs,R YUž— «ÊHdÑE‚ú{Áêƒ:í+cG]åbæ bGœ.pH–šw"}R`„Ø=“"÷x—xytNóô½„S.mg4[e×€¸o“&TLŠÈj«¢*™MlìrLYß±L]D¥³Ýš´¿É7,§ŒôáÞ@¡^±ƒ#“½tò4)UZè‹¿Jò¥ÕöwëO߬{@­Q(W–®s¥ôáÃ*¿êøûLm£l*±K5„JµO G«rO#F&wú6§jã÷k«´6 ¼ Ð&D^ ƒ!;+¥#Ý”uÔ–<¨k×e »™–¥/¦I/†hT“¢†ìJ”yiVÄ•ÜÑLº¹„3_PJÜËmnºÊy6˜W§ÛLNµ„P“9Ù¸¨›aΜ¥…„V’]ær Œžã²ÐÓšrÐ×oÙZF.u”D:µƒžäV°²þåíOýq¼þ·ªÞ ;Z–‰¶êþ·Ÿ®ôÇ$^¼È#ý”æP/ 4ùÐÄ£weL¥ïFë¶+W²E*71[ç©™åýC§Yöw³M¯÷hÙTgò©Ù¶ý:èâuÊôÊzcm½Þíp?Î,ÚJH¢ŸL_°–¥u_©c™C¾nì¦uØ#;7„aŠ®¢¢61†¯ 7hårºPÿapÇ~Ò‚Wß–ÛVéù ôZŸ €Ñp…ÌO7ôé ^ˆ0ñ1g+" =b‚èÃÕ}£ú­|<ñî~â©F#?´'äå‘°†‚Üž¨aá¯pàR˜`+‡bêú„bö0Þ¶4Er²üÈÚÃNN,Ma5-‘º8ðíÙŒZ_L&vxd¿ä–­ ¸/4ßœž\Ra@Ãwµ®Ô;¹î@B¡mì ˆ@ތҞn2µ†(’1ñw¨ÌS*„¯¬2èÏzÍzZA7®ªèÒžjDU»]-õz8ÀU‰2‚šï4PlÝ2-£5zt=7xëg‹ø. ÍgZºt –#\Ô|„t‚aÜ% ýGËGnÐH[ÿ™¯fœo|#]°¥ 0ŠV•xê9ÒÇJN W眡TAޘĆ6ÃgšÔ·CáîÜÿíHWå,S0oZqDó]ÆC7ŒöRä:&t,ð%|›ð»¿¿Í SÙ|(p”q_Ì­<âSW0!mnœ‰€ŠpÎëq9õ¡"‰`^Þ†ËmÇüVô˜ÃÂôSxJ)cƒàçàÉ8­âLÎ}iÜ)ÄY¸ÒÜçúLà 湙Éî&q@>IBqê A>w7ŒºwÀå‡ù·lî@µr‘ù"Awa° FÁÂ]Us5ç>¿Á»ºl‘TÅeP°⟑Ëþ:zÊÂG¹s†h‘}çdcèaÆ öhÿ"ÖvÏgËì!¬÷_ZáŸ_@ñ  ø.œƒkIŒ·cñëKð‘ ð ³,]ƒ8hG—}Ó´M×ç®^‹Ø08Ÿ!ÜÃ@ÌwyH9½Ù| DƒO‚|¸ðŠ¿pؽ‘ÿ—v¹6‹pD ­—CÖ¿îL¸çlîæöÅË)¥Ë»Â(Á±¼DK!z˜½FyûÑãÓ~xv¾MŠùí Ç€¥¦q€{÷½Ñ{ô9÷ñ÷ç°üè…ÓI¨qFðŠ!(’ÔŠ- 0à€ p…Ü“ ´|ßÅHð†B}Î,´BãâÙû8›óCs0éè ª³Uk!G;MwÙ‹ñ­ðÖSYÿšÛ=• C‡²õYóR"½üRÆ'EýÖ¢ÜG(hßÀVŽp ÈP±r0„‚™<~-»|ãÆÀà„0¾¢ààNX »°:°õ/ €â „õ£EM÷2<Wäyж?ë_ÃJ8 çñ·42o{÷ºØ´-s ýÿhÞŠáÿï¥"ZxœíZ[ã¶~ÿ (/;ˆE‘EQÎx‹ Ú—4E€¼²DÛÊJ¢!Ñ3öþúêfÝìm²i]-fÇ:çðrÎwn¤çñûsžYϲ¬RUll‚°mÉ"VIZì7ö?þÁ¶Ué¨H¢LrcÊþþé믫çý×_Y–ËjÄû õqíºÇS™!UîÝ$ve&sYèÊ%ˆ¸ö@>¾ÊÇ¥Œtú,c•窨ê¡EõÍPºLv½øËË zñj)†¡‹©K©Nu)ttv&caŸKc)ÆØÞ@ô•bë Œs„Ÿ^¾# JÊXî` D…ÔîÛŸßöL£D'ÃyÒâ]GG9Z·#6fˆrY£XVnGo&xI}ØØ7¯™îúúþœÊ—¿¨óÆÆ¶|DCÂ9g×O­ÔtÒPÒdcƒ²¢}k—\½ë·©ÄÛù+‹bŠ:8XYñ©Ò*hFwz¯=6ö¶ŒŠøà䪔¨7i¿ˆ<U©]šÉFÜ=¨\º™VªpßÊg™©£q&÷˜j D¥vÓXÿÊR-ѱ¸1ß99P!_æ^:î“a?&rWÕr)Ì+µ-·aö ™í%ÆÄÑmTµÐXÖ1Úƒ3gªÜØßìê§ãlU™È²ãñúó êKˆÝüݦÍĽ¾!P¢D½€/̸ï•Êab¨Ç™±cðyL0ÌÂ9×ì)@ž Â›1â“Æ9©†8:žçãOei$²è"Aù½ðú5ªƒzÙ—ÆŠº<ÉÙÈ—´…œÖåIHçz·"]ŒÙ-·x—;¼<:§yú^Â.ç¦3 m¿‹²«;ܶIí(¿“åVEe2XÛå”&²ºa™ªˆŽÎvk}4°0<çéíjB½b G&{éäirTi¡?,þ*ÉvååÍ«ío2Öw·_O‹@~QHQ–®r¥ôáÃ:¿jÿûLm£l,±K58K¹O G«ãÀ£ŒLîô2§lK£Äá]•Áx`í» n¯2`, x÷$´^ »nG[–¾˜’z¾¢ÝSMz1”0`W¢Ì¦¼Ö­Ž¸’[šIˆræ Ï£3îe™›Jrɦ"Wé6“cMaEÔdJ6š·#ÌžêP³ËTN»¤Å0¥u´:•u•Ë—®†‘K%‘Ž…¬#ù½•¡µZÿôö‡§n…Ç8^ÿ¢ÊwýŠ–eD¢­:çÚOWúc¯¡Ê#ý”æëL#õ-ô>î•1–6Ø æmf.eÓW-v˜Iœ§f”ûfÙ_Í2ÞƒiSɧzÙæc¯‹Û*Ó)ëµ}t;k4¯û©‡fÑVBøÿÍÔ4k^ö¥:sÈ4».{öÀÎ5¡¢¡5ªŒE Âð1‹´|ƒWôk(ÀÂóz8öc—,¸b?˜l[¦ç7Ð%øÌ WØük_}oýhˆ õ g+*(b„bïá ß`¡n)ŸŒÐݪEöC{Džo‰ 0œ’fGÍþÊt”Aû±r<â!Ÿz„= „%MzM?°v¿R›UL9÷ |{2¢Ò‰m'µ&ßAÏ™­ 8ÝÔŸœŽ ¡ß Hø]¥KõN®Ûvã–Ð4% ˆ8'0Ïëè&baSkp€"ƒ”8¦‚ûÊ2ƒÞB¯YGK"è$Ê2º4»PÕnWI½î7pUâA²uê&rÝ0-£Õú‹jj@ëï…~/4Ï ´Dt –#®!`EG´¦ÿhùµ´õët6¾ÁF\›Ã>ЫƒQ´*èŸ#}*å(qµàô© âÆ6ÔžqP/»Âͱ¿oKWåf,“0­8 ùˆñð‚Ñîy®c\—Á_Ü·vß)ü%à61Ly1ì{Œûbj5àQßC‚ 1³(Ðq&O„S^w¦ð|ÈH"˜¦·þ(Þò_ò³YÌ}ý ©”±^ðs@2NË8“S, \BΛS³™£hidÆ<×#ÙÍ èŸPÜó… Ÿ; ƒê]ä‡ùK6w [!,B> .6!8˜Á8ЏÏxWÈfAõI ƒ„Í0ÿŒ û¨nè<¨)3Œ>qå ñ,»ÊɆ­‡y¯{æÌšêùl9Ð{ëý—RøG¸Á—¦øNS|³ƒcIëŒË¾xõõy ð‰Zà;=a!ººâ 7¯›¦l"Ÿs8zÍ|£ï|†Yp«b>â¡Ç½Åâ[÷@^ð§ô@>xÅÿ°Û½ÈÿK»Üt›™»Ž<:­û.ë/º;(àœÍÜÝœ¾FÜó¼ùYa`3_žwK!~˜ÜF¹ûÁåÓ¾¿vê?’ù²À1ôRc? üC4:äAsŸ|<žýôƒN“$!RÌ%+†!I\xVlhó( (W„¡åûˆ`ÁkŠçsfá&ÏãphÎ%ÌÞ¤ÃÛ°û—rÃ[7g€êâÍ›ÃB’•ã3Ä8c!»7æ’ââ¬à¬~ÈB_¬|‚Ž£û­y¸?Ûù[N_¿ÁÇóþ‹GÂqœ\£zR'Ú˜Æp Ø$¾ºî¾Åž„¦éo9AͳLÝ ¸ŒoÞN‚ód€§À` ØÓ«dFeÜênsΩ˹U<ÿtÚΫÔu®Ê•KvY¦*ËËíÊýÛ/?{‘ëÔ&)³¤P¥\¹¥rzùþ»çúuûýwŽãÀð²^féÊÝS-}¿:è)½õ³Ô—…ÜËÒÔ>AÄwòéU>Õ21ù«LÕ~¯ÊºZÖ? ¥u¶éÅÇ#:²FŠÄqìcêSê„WŸK“œ¼ÑXØçÜXŠ1ö7}£Ø²ãTðÛËwT«ƒNåJTJã¿ÿå}Ïô0ÊL6œ'/?ÔiRÉ›u;bk†d/ë*Ieíwôv‚cž™ÝÊ¥¸}ÜÉ|»3×ç×\¯N+;؈Æ$~ýv‘º:´”<[¹ ltyº,¹F¢Î;iâ(ŒÅ”x˜xD,œôPµjGwz/3•Z=Vî¡J´VGÔ›³_@ž*¥·É ÙŠú;µ—þYæµ*ý÷òUª²äW¹J¢Ÿ§ªüg‘‰ªòÎ|§¬'ÅÁ<÷Üq_,û9“›º‘kÍ`©ëø-³WÆn/³æˆ®“úâÇ©’-r¡ôÊýaÓ|:ÎZéLêŽ4Ÿ[žOçæÜ&a7·i;q/€ïÔ»$SGˆƒ ÷£Rû•ËC3OÙ)Ä !ˆ±8 g¸°dŒD8Ž'\ðïÁúÆ;”¹$ªNÓ Z[‰"9KоùG:©z§Ž[m iôANÆótò.Ob:Uý"ÒeÁ˜ß“±9qw~ÀÛ'§|Ÿ”°K2‘± Í¿IŠkDÜ·J+;™~z­6v9䙬ïX¦.“Ê[¯mžÏò-Ë«³»7A#Pª7¬àÉl+½}žU*/ͧÅß$ùheµþU¦æáî›9` (N õéÓÒõ^)³û´ÊoÚþ¶P뤸•ØäBEoóÒ3ªÄÓ€QÈ™çè6~çXkeŒÍài€6!ò( úüÔÊ$¦©ã¸­yPØ®Ó@^F:Ž9Û®t:[¢ÛSmŠZJò+Qî+Û¡´]ÉšM7D."Æè„{žçf «gƒmju¾.ä­–°2j6&[]FØ=y)¡—籜£çå°,t´¦t ÀŸv€–±—&É“ úAG½•,ÿòþç—n…ç4]þ]éýŠŽcE’µ:€ÿÝ—+ý9K—€'ö‰yÉ÷P/,ùÀ‡gÿʸ•¶¾ÌÛάe MfAZ–îs;Êÿ«É‹âv™NïÁ´¹)äK³lûµ×Å¿(Ó)ëµ}ö;k´ÛqtÉZBýɶgZZ·Zª=äë¥k¸;ß¶£“²¶±†¯Ebä;¼ðò À*L<õîØÞ†tÄëïÓ€mu~zÍVPr/°ý¹< ¶HcB ø‚FqB1{ººo°P·” 7ÞÝÞxª!XÄî yº%‚â8 (iwÔ>‘H,Hˆå<â †e„? „%m‘¼™~`í~%/•¶°Ú–È …;Q›³ÍÄ Y’ºËÍ7¯c`ÄcÑÄ?ÖF«ryAE_mcAÔæŒut›±°©%@™ ‰¿Be¾¥BøJ]@6KÞѲº±ÖɹÝÕ€ª6›Zše¿«U5ßk°Ø²e:Vj0ôèzlðÖŸ*‹ígZ¢t Ž!Ü|"é… ÊDúQØH;ÿÏfo}ElÂêq”*Á(FiÕkbZÞ®‹súRycÚX ŸÛ¤ž…»cÿ³-]•›°lÁœµâ€&ãá£=Š\φ.‡ ¾…o¾c÷kðÛÈ0Ì& À1ˆhl5àQÁPÄ£hbQh ! x²(ó:\ÎT¤(—·þ4{áGÁ\ôØÍ™óKxJ)ç½à×àÉ4×i!Ǿ´î‚ ":q¥=Ñu™F&ÌS3’ßMâ~‘„ ˜ˆ"úµ»aн.ßy\<ÍÙ܃j…0œ²' Ò¸‹€M'î?ŠÌð®.›$ÕgqlŽƒ¯Èe¿ =eâ£ÏÜ9c<ÉÇ®sò!ô°Ï öhÿbÞvÏWÇì9¿µÂÿF|Å@ñ]8Ç’K0ÎÇâ5Ö§à3Aàˆð˜N]ƒÐŽMû¦m›H½&±Ñc p>Ç<¼‡¸@AÌ6Û| ÄÂ/‚x£ÿá°{ƒ#ÿ/ír7l&ázq€´‡¬˜ w¡0ø$Üíé‹c0Ʀg…A‚MbyŠ–bü4ºò·ƒË§míÔ»)æóANKÝÆéÜ?ôFçyÐ#A~»?ûé7œ¶HB ¤8à”,8†"sR‡Ì£$¤8^`ÆŽˆà(h(LÜÁ <,žã¡9?U0{“ŽÌÕÎö ¸6‰vǶl-È7bÐG&ömc8Εú_‡DËI²4ö|»qoµo ®Ü›ôjÔíû“9VsÌAÍísLçø¶ÖÇL‡Á¾&öM£Í;Õ9>µ×ÛA@ì dF Ñ[bQ?!ã`n ³xÖ–LÄp6žqð¦HŒUsrÞK4…Èòñ3)3uyµ…G!Ú›gAc†0ƒpt…‚0æöÞ1‚b˜Ó¾W¶%d—/g –a xË ½ŠìM¤¡¿™·yÿlïÅáÿ¿Å£pÑ öxœíXÝã¶ÿP^nQ‹")Š¢œÝ Z‚h_ÚòÈm+'‹‚D¯íûë3Ô·-Û»‡ö‚z>Ü43r¾ÔãÇ]Ž^TUgºxr(&RE¢Ó¬Ø<9ÿúùGW:¨6q‘ƹ.Ô“Shç‡ço¿y¬_6ß~ƒ‚åE½L“'gkL¹ô¼r_åXW/M<•«*LíQL=g"ŸŒòI¥b“½¨Dïvº¨›¥EýÝTºJ׃øápÀ¿‘¢Qy„yŒ¹ áÖ§ÂÄG÷b-œóÚZFñ€7}£Ø²ç”ðwï ¸Öû*QkX¨p¡Œ÷þç÷Ó%85éTOV|¨“¸TgûöÄÖ ñNÕeœ¨Úëé­‚C–ší“ÃHûºUÙfkÆ÷—Lþ¢OA˜ETÁǧNj :m)Yú䀱²{ë¶\‚G sô.Hc!£Ä§þ1B#—P—ò‡vYoð2Õ‰5àÉ©â4Ó«½1ºøu_$[•|Pé¯Yz œ;l§Ž¥®Œ»ÎrÕ®÷¶z§¼“Êj]xïÕ‹ÊuiÓÊ+3PéÅ•ñ²tç™Q¸,nè;¦%„,×¹§žûlÙ©Z×\ëûÊäµÌÁB{¼Ô:{"ºŠë.H•ñÒ:×Õ“óݺùõœ•®RUõ<ÑüÎyâž™S[’½þþÐVñ @nÔÛ8ÕÈŠ÷£Ö; û˜G$òƒ?Ôqç’Ϲ°g„9 )ÅŒ QßÛà¸û"3PSåq®`_UV"O Ìoþ£½T½Õ‡Me=iª½š­=dåv@#6·½é‹‚R?¼%cí”·˜§{Ì]|ÌvÙGç¤3kÃ4ë8“â¶_štiª£Zé¸J/6žÙg©ªoø¦.âÒ]­lá_å[–[Æf{KA#Pè7ìàªt£Ü]––:+Ìëâo’¼·³^ý¦s÷ôغˆBÃz]ºÞim¶¯›ü¦ãor½Šós‰uf UªMV¸F—P«W¹Z›ëœªÍàk¬•†vº›ð†³4)r/ † ­´‰MÛØÛ¶½mT5Ø­DÈœì˜:ž,Ѩ¶H-% ùHT»Òެ>È‘ÜÑìLÂLð@ú>›qO×¹)ت.«ÁN¹:[åêÜJ8@5½$Ûu+ì™ó¬P0NòÓ¥œ§gÅqâÙžfO7Ìo>ZÆN™8M< =)¼ peù÷?>÷;<&Éòߺú0숉WzñwžGúcš,`ìbóœí _Xpò'ÀÞÈ8—¶±›èm5WªÅ*WQ[šì2»Êû§Éòü¯v›Þî‰ÚÌäê¹Ù¶}lñ:czc½©µ^ïöus™y¼RPD³CÍ[ë¦ÒûrõÚÍ gâçóAbª¸¨­Gl„á1zG.` éC86ç)-y8Æ~¢|[eÇw0oFBîG bÿt¯¿ŒÊ*ø‚I€J”ÿa ßd£~«€žEws©F„’ rÎÈó#QER0Úž¨}£2XÐûÍ_¸Ö` û¦=Oµ=Ú&y¦~âía'7Q¶±Ú™ècÎÅŠÚœl%v˜dI¿ô–/ ¸14OnÏ 9|ÉB}_›JPËÒÚÑ‚$ìÍ}¿§ÛŠ…C-!ŠtJü :ó9ÒWU9Ìg³ä=-aWU|jO5¡êõºVf9`4¢Œ¡ç» [¶Ld­AЃaF×—N€hý±û‘ý-ÀJ,À†\‰Ió“Ê Œ ÌúO( 8l¤Ñ/—Úlðml¤ôg¬Iéœbtå¦z‰Í¾Rg« ÎЪ nlaÃKàw^Ô×SáæÚÿìH£q3–m˜W½8¡p ù î6s§ÝË\צ._Ó·IßËðW· ÇTà¶œÏE /½<øXr)g…bÁeèËè’×#s?€Ž$ÃËö6\o;þxɘf=¬ ç¹þ" ­”óAðKˆd’UI®.ciÃ%$$›…ÒÞéúJ£3æ±YÉoqÈþ‚>Ü8Ù—†Éôn¸zçòàášÏ]èV˜ÈHÌ ¤ ŸPÎÂq¦ ‹@\á!›Õg 4lNIJOBCÇÉL™Åè3OΈÌ걟œ| =ì{ƒ=Ú o§ç r{Hôñë(üo¤ÁWP|ß„sp-é’ñz.޹>‡Ÿ ßÁ@”Gl6è $À:>7íØÄpõšåÆ€ øœððâ‘/ü«Ã·Á@~ø‡` .¼ò8íÞÈÿK¿ÜL›Yºže ­û)\Mw.q(Ÿ¥»½}q‚…ïûó»Â¤Àf¹,z³»˜HFƒ/¡PšÏ8\G!€è… %eú38‚F†þbxB¤ù€\H¹ äŽØ¨–Z§œÞ“Aš†QøV¥ãYAÖ†G1 °¼p…] Oí2$â×}•íÞã7ìIV^ûÚëï>¤N^'_iÏòº¹¼0ñÉyݶƒGû¹þÿÆ_q}Z.¹xœíZmã¶þ ÿAU¾Ü¢ERE9Þ Ð‚h¿´) ôK!K´­œ,½¶ï×g¨7Ë’µëEï¶Ýf}¸[kfø6óÌÌCí-~8n3ëQeªò{› l[2U’æë{û¿üèÛ*u”'Q¦ryoçÊþááÛopëÏ…Œ´L¬Cª7ÖÏù§2ŽvÒú°Ñz7wÝÃá€ÒFˆT±vï,Ç¡0¸|\ûeY°v^ΓøÞnÆìöEVÙ&±+3¹•¹.]‚ˆk÷ìã³}lv>ÊXm·*/«¡yù]ߺHV¹ÙÒÁ«¬H†.¦.¥X8å)×ÑÑŒ…}^K1Æ.èz¦7šÍKðìþvö­•j_Är%Ê¥v?þò±S:%:éÏÓ:öbÝ oçÑV–»(–¥ÛÊë i¢7÷6ÅõãF¦ë>??¦òð'u¼·±…-ÑpÎÙù[cuF ©%iroÃaEóÔ,9ï 1 )‚y’ˆ‹0öˆ7³(&¡ƒ‰CšIÛãΛíßÛZ©lΘI&Óie¨ój·Ž<îT¡UšÉz¨»Q[éždZªÜý(e¦vOî.Õ ‰ í¦±Êÿ¥Z¢]>1ß1ÙA¬B~]{jµF½H䪬ìjo˜Gj[n­ìg¶—/÷L—QÙDDzvÑðœ©âÞþnU}ZÍR‰,Z¯>—:Oõ©NävþvÓfâÎO”›(Q€ÃHûY©-Șú¡écÀŒÃßg|¬6»Âˆ Ïóǃ!â{gŸ§²iwO°/ c‘E' ç_ /lmÊ:¬ ãH]ìåhä!ÍáLN|ÒñÑ“6ÆlÊÆ¤Æ”îô„nÓmúYÂ.ÉÈÆœ ïþU”1í“ +’ÅREE2XùeŸ&²œðL™G;g¹4é~UoTÎ.Ò›© *ƒ\ݰ‚#“µt¶i²Si®Ÿ7¿Éò©•ÕòWë'w_Ík@S(SÏ[—[¥ôæù#ß´ýu¦–Qvi±J5@¥X§¹£Õ®‡§ž"“+}]SÔø½¦Z*­MZA¤ƒ‡Úbq¶€äjFY–>™¾s<¡ÝIMöI°³Pnw¦UdBœÅÌd¢œùPèH{º®MàrtÓ¶Êt™ÉK_Âò¤ÉPl¼ßŒ0{ÎÒ\B›ÈNC;þLó~Æ·²*ÓÛÚ{­ØJ%‘Žz¥¾ù—ÌÿöñLJv…EÏÿ©ŠOÝŠ–eL¢¥ÚCh퇳|‘Äs` ÛH?¤[(†müÂÂ=+.­MìzóÖ3²&WiXoS3Êý»N³ìg³L{îÞ´©ÎdOºp›3´gtû‡\¸­êÇõY´”1•ÞËu¡ö»-dà½]5»çÞJÐ ÑE”—Æ&°ð5¾úÏà2(ÀÂóïº(¬/‘,Xpyopi‘?@ûô)˜ΰùÓ<úÞ ¸Zˆ õ g3*€òн»sÔz µKùä"¨ë‹U&û¡}!o‰ 0œ’zGõþŒÈ£Œ 6s€u!Ÿz„Ýõ„%MÙ»˜¾çín%'–¦Tš&ç!øö`D©O&Š1'ßËæ9\ªoN«Àˆ…ž  ¿/u¡>ÉyÃs0nu«C‡fž×ÊM¢Â¦æ€<é …Z{)ÔÊ"ƒŽ«ç¬•%ô×¢ˆNõ®zRµZ•RÏ» œ±‹ Š;»š×J˜Ƃª ]·:¢õW‹úÈ Íg§DÎ@-G \}„t‚eÑJþ“åcTÖÖ¿†³™à›ØáT/R98E«Â†ôé}!/êUœ®BAÞ˜|†ÆÃç2—¯Carì¶¥óáF*S'¯z±'óá6ñÜQÆN{ ¹Ž.ƒ Þá[Áwþâ6pLnó¡ÀyŒûbè5ÐQßC‚ 1ò(ôÍq&O„C]Ë´=*’†å­»¦6zÁ¯¡ÇlVc¬…HB)e¬3| ‘ŒÓ"Îä0–&\B\ÐQ(Í ­Í42R«‘l2‰ú* Åáö(è[C¯{JRâ0à"W|î@µBX„|” U¸àF Fá‚8Š¸Ï¯èÎ!%ÕW l†ù Ù‹ØÐ±×SF1úÊ3Ä£|l;'ëSó\qú_Ìêîùh9À=„õù½~ ¼“â'Hñ$ƒkIÆëXßøxœíX[ã¶~ÿ (/;ˆE‘EQÎx‹ ’—4m¾²DÛÊJ¢ Ñc{}u³.öìÍ.²èz03æ9‡—sÿÈÇïÏyf=˪NU±± ¶%‹X%i±ßØÿüõGØV­£"‰2UÈ](ûû§¯¿z¬Ÿ÷_eYL/êuoìƒÖåÚuËc•!UíÝ$ve&sYèÚ%ˆ¸öH>¾ÊÇ•Œtú,c•窨›©EýÍXºJvƒøétB'¯‘"aº˜º”: áÔ—BGgg6Îyk.Å»À‰¾Rl]ƒqJøä{ªÕ±Šå&JTHí¾ýõíÀt0Jt2^'-ÞÕqTÊɾ=±5C”˺ŒbY»=½]à”&ú°±)n‡™îú:~Nåéoê¼±±…-ÑpÎÙõ['uu:i)i²±AYѺ-×ãè@Ôz#E€E®,Š)q0qˆ¿²âc­UþÐÎîõ^'*6zlìø ãwYZëßÒ"‘ZVyZDZÂ6Ðh°ò°¯<—ªÒÎ.Íd»‚{P¹t/2­UᾕÏ2S¥‰/·L5P¢J»i¬Šß²TKTwÖ;'%ø.ä·¹—žûd؉ÜÕ\k3¤¶å¶ÌAGs¼ÄX}$ºêÎ[–UF{ˆïLUû›]óé9[U%²êy¼ùLy  Õ—67ûõûC›…|G >D‰:Ax,¸ï•Ê76£È8fdÁ!†„)~°äšCQÄ<pÁ¿sœc‘jH®ò¼\àXUF"‹.Ôß >(PÔi_;êê(3OBêäty@BºÔ¼ésƒ`ÌîɘL¹Ç»¼ÀË£sš§ï%œri;£ÁØú»(»Ä}›4¡b2EV[UÉlbc—cšÈúŽeê"*íÖdÿM¾a9e¤÷h õŠ™ì¥“§I©ÒBXüU’/í¬¶¿ËX¿xúf ØJˆBÕú°t+¥VùUÇßgjeS‰]ª!Tª}Z8Z•£x12¹Ó·9U¿·X[¥µIàe€6!òR ÙY)馺ã¶äA]».ØÍ´,}1½ê|1D{ š5”0`W¢ÌKÓ· !®äŽfÒ QÎ|áytÁ½Üæ& «œgƒiuuºÍäTK8@5™“‹ºæÌYZHh%Ùe.§Àèi1. =­)}ýw—  eäRGI¤£Q;èIþ`eÀ,ë_ÞþðÔïðÇë«êݰ£e‘h«ŽàûéJLâ5 Œ<ÒOiõ ”oT<ºWÆTÚøn´n»r%[Àrº%qžšYî?tše7Ûôz–Mu&ŸšmÛ¯ƒ.n§L¯¬;ÖöÑí­Ñ÷óèÌ¢­„$úÉôkYZ÷•:–9äëÆnZ‡=²sC¦è**jcãaøšàxƒW!Æówì§!-Xpõýh°m•žß@¯õ)˜®°ù醾· bB}ÂÙŠ h„bïáê¾ÑFýV>™xw?ñT#B°ÚòòH…¡à”´'jGDø+ 2&ØÊñˆ‡|êö0Þ¶4Er²üÈÚÃNN,Ma5-ÑC$ðíÙŒZ_L&vxdM¾ä–­ ¸64ßœž =a@Ãwµ®Ô;¹î@Æ¡mì ˆàÌózºÉX8Ô HÆÄß¡2O©¾²Ê ?ë5ëiIݸª¢K{ªUívµÔëáW%Êj¾Ó@±uË´Œ6Ô`èÑõÜà­Ÿ-ê#/4Ÿh‰8è@-G Ü|„t‚eцþ£åc4ÒÖæ«çßá-XŠRE«Ê<õéc%'…«sÎPª oLbC‹á3MêÛ¡pwîÿv¤«r –)˜7­8¢ùˆñð £½¹Ž ] | ß&|çî¯Ào3ÃT`6 œÇ¸/æVõ=$˜ ‹6·ÎDà‰pÎëq¹çCEÁ¼¼ wÜŽ/ø­è1‡…›è§ð$”RÆÁÏÁ“qZÅ™œûÒ¸ Rˆ ºp¥¹Ïõ™FÌs3“ÝMâ€~’„âž/ýÜÝ0êÞ —oæ?ܲ¹Õ aòE‚4î"`‚ƒ…»ªæjÎ}~ƒwuÙ"©>ŠË `3Ì?#—ý!4tõ”…>rç ñ"ûÎÉÆÐÃŒìÑþŬížÏ–ØCXï¿´Â?# ¾€â@ñ]8×’.oÇâ5Ö—à#Aà0a!]4ºqÐÎ[öMÓ6‘Ï9\½±1` p>Ã,¸‡˜xèqïfóm0| äÃ…Wü…ÃîŽü¿´ËݰY„ë$âi½²þÍpgœ³E¸›ÛÈ{ž·¼+ŒlËK´â‡Ùk”»=>í‡g§áÛ¤˜ßpp Xj¤wÿؽçAsŸüqË^8M‘„H1g”¬†"páY±EæQP®0  BË÷Á‚7ÏçÌÂ+<.ž½Ã±9?T0“ŽÞ :[µbq´óñt—½ß o=•õ¯¹ÝSÙ0t8 [Ÿ5/e!F„Âèå—2>)ê·ÞÀ°Ç}Œƒö låDÁÔ+‡@xaÑÉãײË7~` N)ã+Ü Ü ë`«“€Xÿ² <B°~´<Ó½ ÃyÞÁ´ÍfýkjØ¿åt_pK£ó¶w¯‹MûØ2Úÿæ­þÿPüé%ƒxœíYYãÆ~7àÿÀÈ/;ˆØì‹Í¦<3’…aÉ‹í @^ŠlIôRl‚l¤ýõ®æ%R¤ffáì&ë,gv%VUUõÕÑ=÷ßö™ó¤Ê*Õù ¼pTë$Í·‹üò½+Ne¢<‰2«‡E®ß=~ýÕýŸ\×ùk©"£瘚ócþ®Š£B9ovÆ+Ï;(m‰H—[ïÎq] ƒ«§í×_9ŽkçÕ*‰í˜âPfµl{*S{•›Ê#ˆx‹||‘íÒ'ëý^çU=4¯¾J—ɦ·[:²ZŠ„aèaêQê‚„[sÜ«±°Ï¹±cìo úJ±U–-à_/ßP¥e¬60P¡\ïí/o{¦‹Qb’áM ææeñWI>·²^ÿªbóìîë9` ÈN êeéj¯µÙ½¬ò«¶¿Íô:ÊÆ›ÔTÊmš»F< ™Ú˜yNÙàw޵֯Øž´†ÈÄýE‚«å8æl+Îél‰‹žj£ÏR€_ˆj_ØêS·òBni6’Ü—ŒÑ ÷<ÏM@ u t[°ªt©±-ayÔäšl­ßް{ÎÒ\A™ÈÎ×rì™æÃˆïhu¤w¹Ý›&÷†±W&J" R}Gò{+Cç±úéí÷Ý ÷q¼ú§.ßõ+:މÖú®]<^è÷I¼‚^a™Çt©Àö†ÖàÞ»0ÆÒÖwƒy›™KÕ´³ XïS;ÊûÙ¤Yö£]¦Ó{0mj25 Þ{­ŽÞPÉ{¯3Bóº½d­„Åßl’w¦Ér[êC±‡lëÀb`Þqa0e”WÖÖ±ð5ƒNõ ^ºÐÅ Kæßõ^ØŽ‘,ypqù`0i™žÞ@ùô)8 —Øþ´¯>[B—bB}"ø’JhvÅìîâµÁBÝR>9u;rP-B°.Fäé– C)(ivÔ¼é/I€å\ò¥ ýò)#ün¸ ,iÓÞhúµû•ÜXÙTi‹C$ðW#*s¶ض+ò-4cÙ*‡CýÍíñIðÛÊ”úZµ}Æ-¡)Õ ˆèž9cÝ*ljÈ“!ñWȵc* V•T\³â-‰ ¾–etnv5 êͦRfÕoà¢DAwëîjÕ0«YªnumðÖßê#Úg Z":PÇ•×Tn°¤\ ZÓp|Œ‚ZÚù×õlÖùÖ7R² «ïŒtF1ºt¡GzŠÌ¡T£|Õ:§ÏP76ž¡0ÅðŒcy 7Çþ¾-]”›°lžœµâ€æÃ9â8Lör] ]|o ßk÷—à·+Ô`6ã—×Võ’\ʉE¡nHp0^óºN›ù‘dpÞújË—b=v³2˜bý#xR)ç½àçàÉ8-ãL]ûÒº BHH:q¥=¢u‘F&ÌS=’ß â€~’€Ì—’~înTïRÛ”¸z‘›»­–ppžu›LÜ~& _Ìð..›ÕGq$lŽÅgä²ê†Nƒš2ñÑG®œ!žÄcW9ù°õ°ïuïÑüyS=Ÿzé¼ÿR ÿ0øÒ?ÓßlçàXÒ‚q‹¬O[€Ô?ÓÒI¡«{ Ú±iÝ´eùBÀÑk‚¾çs̃[=÷‘™`³Å·îXðIz ¼òv¯päÿ¥]nÂf×â Óz²þ,ܹD|w{úâ ÆØô¬0° –§ÝRˆï®.¡¼íàòiÛ_;õßFÉ|ààz©1Hçþ¡7:σ¡>ùpöÓ.6m’„H±à”,9†$ÉœØ!ÐæQP.1  BÇ÷ÁRÔæ îà%&ÏÎÇáМ/%ÌÞ¤“Û06ÈM3·aK—Ø_¼¤aP_sÐË·áÝ×U@ÚÄ4ÝoÔI“IBù¸¥êЇáL8ü;dó´Øgö˜§#xÝ@ßøkA? Ý’ÿ½$0Œ‡k;ÿ´œuiøj< [¸«qj/ì ç;×r˜PÊé§Â3£_ðük9ëÒ×Ãàw๹VxÎ aFÆ—EÎâç®)i>ïíßìàó7N¨ÏEÉ$äxœíYÝoÛÈ?àþBy‰Qq¹ß\*vhƒC \_z) ô¥ È•Ä E äÊ–Rô¿Ù¥HQ$%;mr¸ ‘a›œ™ý˜¯ßÌ®î8lsïQWuV3‚ðÌÓER¦Y±~˜ýýý¾šyµ‰‹4ÎËB?ÌŠröÃÛï¿»¯×ßçy /êEš<Ì6ÆìA°ÛW9*«u&ÎõV¦"Á¬'Ÿœå“JÇ&{ÔI¹Ý–Eí†õ«¾t•®:ñ§§'ôÄœ‰¢(À4 Ô ¿>&>øƒ±°Ï©±c¯'úB±E ÆÙÁo'ßP]î«D¯` F…6Á»÷ï:¦QjÒþŽ|νd_›r{׌nõ^¤ebõx˜-«¸H6~¹ÓEM»UôaWVÆ_e¹näƒM¹ÕÁQguYïô£Î˦`— Ä• ²¤,þ•gF£]qe¾CºOErš{l¹o-û>Õ«ÚÉ5¶°¯´a³6Ç\Ÿ¸žgŽÖsFLÔõ¬£Ûqɾª`£~Ræeå×ɲ ÆóП,ùgGõßÃÞ¿»Ñž²xµ¢öçMKÿOûp¸]|ž-ùìw¹)ùi»ÂX‹Õêö®îëÊæ± Le©M•žÇ—q­Û=îâµvk<Ì^­Ü§å,Ë*ÕUË“îsÉ+!k3slµ¿=;q'€¯Ô›8-Ÿ §GÜe¹º@¡œË?Ä'…\QEÆ\X3BBLØx,$ëÞæ˜¿/2ˆ¸;Œ'89-Ô_+Á[™zS>­+kGSíõhäSV€Jþ ¼HDÇšŸDZ@#ók2Þ®ñŽ7xÛøm³v9¶ŽÕ oýUœŸâºM\¨ltòAWË2®ÒÁ@g—}–êúŠeê"ÞùË¥…ìI¾eù»Øl®MàŠò+ø:]k›¥»2+Ìóâ/’¼µr¹üE'ææîݰԅRó¼t½-K³y^åm—Ë8¿”XeB¥Zg…oÊ]/žzŒ\¯Ì4§jâwе,± <P"·Â ËΪ4±q%wåé< dà~GK¼_K‰B~&êíÎ6®ñSgò‰fÓ QÉ…bŒŽ¸Çin ºêa6Øþ¤Î–¹¾Ô6PÄ@M‡dë¢Ó»ç<+4tùq(W‚ѳ¢ -ÍÁœJÁ¸4Œ­6q›¸WZ’è¬ æâoï~ì*Ô}’,þQVÎeɳ"ñ²ÜƒÿÏ•Ì6ÉZÃmlÞf[À ÛVþ:A¨MãRÚú®7o3s¥›.s²ßN“mfG?›,Ïÿb—iõîM›(‹nÙæñ\1OÊ´Ê}mïƒÖÍëzy¼ÔD?Ùºà¡u]•ûÝòõaæJǬggGè†hkkëaxÌc£_ã¹Ý+ ±bâ®sÇú2¤ϾïM¶­²Ãk¨µ‚â³hŽíÏéU°9tç&TÉçTQÄ Åìîì¾ÞBíR‚\xw}á)'B°ˆfäñ–Š"%)ivÔ¼%æ$DŒr®øÜg„!AáwýaI ’Ó÷¬Ý­ä'Ú«-‰ ‘PÌ#\“ô0;õ# òð|QÀYÏ=ù-#1EC½©MU~ЋSS„ñ‰ÐvÄ!œš8c-Ýf,ljP¤}â/€Ì—T_]åPŸÍ‚·´4†j\Uñ±ÙUZ®Vµ6‹ng%v1`¾ïZ±EÃô¬6`0ÔèzhðÖ_=è¨Xd?sÐIÐz¾BØ}”öÃ9åQGÿ³'0 ´÷ÏálÖùÖ7J±«ë¢ÊŒb í…~ê16ûJ_×É9TAÞØÄ†2–Àç2©§CáêØÿmKgåF, ˜“VìÑâ`<wS6÷­V‘%ˆs›ŽÜU¹ƒ»r‚wvÙ(©¾ˆË°9–_‘Ë>©:ôjÊÈG_¸rFx”måäýÖþ»Þ£ù‹yS==zå}üV ?G|kŠo4ÅWÛ98–œ‚q:ϱ>n¾P |£"<¢£Bçz Ú±qÝ´e )áè5Š®çsÌÃk=HFL²Éâëz þ&=€¯ú‡Ý ùi—«a3 ׋ˆƒNëvÈŠÉpç …RòQ¸ÛÓÇH2ÆÆg…^‚byÜ-Eønp¬{—OëîÚ©{ºóé 'ÐK]ÆiÝß÷FëyÐ#’RO÷g7}ï†Ó‚$` Å’S2ç@2”Šy‰G Í£$¤8šcFžˆ`%… É=<Ç}ðl}õÍù`v&í߆ݾ”ëߺù¢Ñ“Wo¾ˆµas4R^½{nå˵ûô+‹KY8÷yÅš*q÷ÜÍ›ž<§.Þàl Ç\zº T8!VbîKÄ)‹,'”Hc#9¼y[Oäfÿ‹°ëËÚ;>Uƒ¸;G—q+Ø·•VÀAiN9ô$Ÿá`Õihìò¢Éʨ¤Ñõð' '#Ø[KŒÏ’%};9 ‹Fuþ|wg#›h„b=C#‚Z ‡d„„(R||RvXÆ¢R«aÕ™î/ÛSrñНÄRÒ!¶4_ìŸnßVsCˆ‚ÌÓÌ­  'A³¡‚’Lîù—„\÷O‘ë\iµFº‡Ú/Àðÿ”žÇ$ÅNÙÆÔ 1)æwç/yìò÷ö+øÿ+Pœ^"¶!0xœíY[¯ÛÆ~ÿÀÒ/>ˆ¸Ü—¤¢s´FíK“"@^ Š\IŒ).A®Ž$ÿúÎò~‘ŽOÐ:¨˰-ÎÌÎîÜ¿¥6ß]Ž™õ,Ë*Uù£M¶-™Ç*Ióý£ý¯Ÿ¿wÛªt”'Q¦rùhçÊþîéë¯6qëo¥Œ´L¬sªÖùû*Ž i½=h]¬]÷|>£´%"UîÝËq`),®ž÷_eYìWë$~´Û5Å©ÌjÙ$ve&2וKqí‘|<ÈÇæ鳌Õñ¨òª^šWoÆÒe²ëÅ͑ά–"aº˜º”: áT×\Gg¶Îyk-Å»À‰¾Rl]g øÛËwT©SË,”(—Ú}÷ó»žé`”èd¬§sìd߉·óè(«"ŠeåvôFÁ9MôáѦ¸y<ÈtÐÃós*ÏU—G[Øò ‰‚ßZ©!cHCI“GŒ Ú§vËu/ˆQHèI"„1#leQLB‡´J;s׉ŠÍñí]¦"zöZå¥P¥vvi&A÷ ŽÒ½Ê´R¹ûN>ËL&{Ü"Õ@‰Jí¦±Êÿ¥Z¢"¿£ï’™PÜæ^;î“ao¹«j¹ÆvóHmËm˜½)æx‰ñéHtUm,,«ˆö½™*í7»úÓq¶ªLdÙñDý™ò„7Õצl;ýÝ¡â^ߨQ¢Îü÷ƒRG 3ÄC2¾àÇ!„">ó–\ØÓG„QæùbÁ…ðžLpœSžj(â²Tp*K#‘EW æ×ÿ‘Nª:¨ó¾4žÔåI.ÖžÓŒrÚ<'!]ÚÞŠt¹O0^ZØÊ˜J¸Ç»¾À;F—ô˜~pJ²1Œý¿‹²!%î{¥N–ƒŒßËr«¢2™-¬ýrJYÝñL•G…³Ýšê¾É7,§ˆôáž‚Z W¯ØÁ‘É^:Ç4)Tšë‹¿J²Ýybt/¡¶¿ÉX¿xüZ l= D¡-}\º:*¥·ùUçßgjeS‰]ª!WÊ}š;Z£„12¹Ó·9e“À·X[¥µ©áe†Ö92΃§Fb3H@uµ«,K_Íœ¹\ Ñü %ôù@”ÇÂÌœ<¹¥™RBTp/`Œ.¸×ÛÜÌóL7cªJ·™œúG@Mædãýv…9s–æEvË)ðgšK¾£Õ¥ÞuwwÙÞÆQê(‰t4jöÉë½ xcýÏwß?u;lâxý‹*ß÷;Z–‰¶ê¡µŸú&‰×€Ž‘~JÐ ºøÁÆSi»‘ÞFs)°qv%ñ15«ÜŸtše?šm:»GjSÉuã¶6t6ºc#7nç„æq?OÈ,ÚJ(‹¿›.o-»å¾T§âØ{äÞédÐe”WÆ&°ð5|ú¯À.ÈÇóú(ì§™pùH ¸´L/oa€zûœ…+lþ´[6 1¡|E€8„bö0Dm´Q·•G&AÝOT‹ì…ö„¼<AaJš5O$ðVÄGŒrð•( y”þ0Þ¶4mo¢~äí~''–¦Uš)Çñ={¶¢ÒWS€-ÈX“oŽeë® õ7§c`À, > ¿­t©ÞËu‹t0n ͬAìfæŒutS¨p¨5$@žŒ‰¿A¯R!ke™ÁÈÕkÞÑ’lYF׿T#ªÚí*©×ý#Šº¸Sã«uôŒ5tU»ÕÜ ­XÔC,4ŸX‰Ø@-'@¸þÒñW” Dkú–‡‘_K[¿Îµ™à›Ø[°zh¤rpŠV¥ é9Ò§RNúUœ¾CAݘz†ÁÃgZË·SáîÚÿîHƒq –é“7½8¢yp{øî$K§½”¹ŽI] ¾¤o¾óð—·™cJp› Žqás¯z <½Ð/8Ü‚pÎë 6ó #þ¼½õ×Ò–ˆ[ÙcøË\ÿ‘„VÊy/ø9D2NË8“óXšpA ‰€.Bi.i]¥‘óR¯äw‹Ø§HA æýÜÃ0šÞ¥Ò”8°È Ÿ;ЭB±(:\|B°¿WY߯…'nð†-Šê“„ 6Çâ3 ÙïBC—ÑLYÄèOÎ/ê±›œ| =Ìs=š1o¦ç³åö¬_Fáÿ" ¾€â@ñ]8×’6oçâëKð‰ ð ˆð.]XÇ–sÓŒMä W¯Enô‚Ï1÷ïa î!2Ánß1ÿÁ@\xƒÿã´{E ÿ”~¹›6‹td ­—SÖ»™î<@¾|‘îæöÅ1Œ±å]aT`‹\^¢¥?Ì^B¹ûÑ˧}ÿÚ©ÿ6iæ·“–šæéÂ?ŽFy°#Â#¿?ž½úÑ‹MÓ$¡R,8%+Ž¡Iú"`Vl€y”ø‡+Œ|BýÐò·£ü¿š¼(þh—éôL››B¾ÔË6_{]üV™NY¨í³ßY£yÜŽ½³HÖ‚èO¶$8ÓÔºÕêxØC¼¶UÃØù¶Œ”•µˆE¾‰‘ïðƒ¦A·Â‚§Ží­KG<¼b?˜l«óó;(¶Å!gñÛŸö1` hêbLh@_Ј"N(fOWø uKäÝí RµÁAìÞ§["(Ž#AI³£æ‰DÁ‚„ˆQÎ#¾ða( Œð§á‚°¤M’7Ӭݯä¥Ò&V["aàŽFTæb#±mH–äGhÝŠe G„ú›×10â1‹hHâ+£Õ¹l»"Œ[BSØA‡ÐlsÆ:ºXØÔ Ì†Ä_!3ßRÁ}¥. >›%ïhYÕXëäÒìj@U›M%ͲßÀU‰C9ß«{±eÃt¬6ä`¨ÑÕØ€ÖŸ ÛÏ´Dt Ž!\"é… Ê¢5ýN€QXK;ÿÏfÁ·ØD›°ú>J•`£´ÕkbŽZÞ$®œ>UAÜØÀ†2–Âç6¨ç]áîØÿlKWå&,›0g­8 ˆƒñðŒÑy®g]—ÃßÜ·vß1üpFƒÙHpŒ‹ [ x4`(âQ4±(Ð …,ŠÇ¼®/gd¤(§·þ<Ûò#1ç=v³pêüHB*å¼üLsrŒ¥… BHDt¥=Ñu‘F&Ìs=’ß â~‘€,ˆ"úµÃ0¨Þu.ßy×½Góó¦z¾:ô‘óñ[)üo¸Á·¦øAS|·ƒcIëŒó¾xõõi ð™Zà=á1ºº ›ÖM[6Q ½&¾Ñ÷@>Ç<¼×ñ‰˜ 6[|눅_¤ àÀý»Ý€ü¿´Ë]·™¸ëÇA§õØeƒYwç …àw·§/Ž‘`ŒMÏ ƒ›øò´[ŠñÓè6Êß.Ÿ¶ýµSÿí&™Ï;œB/u뤃ˆF‡<è ߎg?ýà†Ó&IÈ NÉ‚cH’¡ˆ˜“:ÚÍll®ó‚`8¤éïæmä?Û›qøÿor~nÖ ž image/svg+xml ¥#ÏxœíÛŽã¶õ=@þվ젖DR%º3 ] м¤) ô%%ÚVV]IÛóõ=¤îÏζ˜´‹Äƒ™Ï…‡ç~(ßs9dèIeªò‹8ØB2U’æ»ëï?}k‡*«(O¢LåòÁÊ•õÍã×_ÝÿÁ¶Ñ_ U2Aç´Ú£ïóe%z¿¯ªãÚuÏ糓6@G;÷Ù6°sù´ûú+„ÈÎËu?X ÏñTd†6‰]™ÉƒÌ«Ò%q­}ÜÓÇú铌Õá òÒ°æå»!u‘l;r}¤³g¨ˆÂÅÔ¥Ô »¼æUt±'¼pÎ%^Š1v7 }%ÙºËá·£oN©NE,·À(\V>tH;I• ÷i ;’;²vdyŒbYº-¼Þàœ&ÕþÁ¢¸^îeºÛWýú)•ç?«Ëƒ…F¾Cᜳþ©¡ê#†Ô4y°@Ù°Y5"×!vu`Ÿ$⡈=â­ÅDؘؤٴUw¨XÿÁŠ÷2þ¸Q—ŸÓ<‘•,içtFíÄÈËQ•½M3Ysº{uîU¦¥ÊÝòIfê¨ÃÉ=¦@¢¢rÓXå?g)ìwÌoìwIŽà*Á—±×û¨Ñ÷‰Ü–†®6†^R ¹5²ÓM/ÑFn¢²qBÇhᜩâÁz·5Ÿ³QE"‹ÇÍgŒSàï´ºÖyÜîßZoÜàå>JÔ¢a†}Vêð`qî0F=ÁgøB†`'q8gŽA¤p<Ÿqæy3,¸û¤}cŸò´‚T:^朊BSdÑU‚öæi©Ê½:ï mȪ8Éï‚Gí&sÕ’6ÆìÎŒ[¸ë ¸CtI鳄S’Ö`hþm”õqÛ*&VtŠÈb£¢"™0»œÒD–D/7Žöf£Ó}Ñte£j_¾D+#b‘ “`Ëd'íCšUšWŸ&åK’ÕæW/žÞì2 F)”©OS—¥ªý§U~Õñw™ÚDÙ˜b›V+Å.ÍíJ5@dr[-cŠ:€—PUU:…çjbd5Å}OÙÕp!T]uß¹\5Ðê :ý4D¬ÊÃQ÷ 3L„=¸éTr(g~èyt†½.cPCN#]·­2ÝdrlK8@4™‚µõ}æ,Í%ô‰ì:¥S`Ï4¦| 3©Þww^ÝkÄAVQUÑ Ö· ¿³2Ìë?|ûØJ¸ãõ?Tñ±“ˆ&‰6ꮵ{ø}¯ab8DÕcz€Z §?€pïöˆ1µöÝ`ßzçBÖÃÇâ–ćTs¹«Ò,û^‹iõl›V™|4bëÇN·Q¦UÖj{ï¶Ö¨—»idfÑFB~üU—{4/›»BŽHŦ#X;[DUDy©-¢= Œïñʆ¡Æ pèùw;v} /1±ú˜ð1[Ù¡p¸ÀXлqÔ¢mo½¡Po']xŽéŸÊs¨Mª°¡>EÕ©£h¬%@B„φpkÈfÜÁ^ÀéÊ ©½#˜'© < +¬Ÿ}­¨;ôbТgfÅ«š7D 9m¨š°1´°Õ=Øy+»8„ÔœOÈHl‹ZŽ–ÐëÎêõ gt@KÛ(¤ËGs]GÌIã~s’º¼!!!&C€~âi47¬Z¨¡ïØF-Ò(ࢱ·ádµ½ [µÛŒA5c€Vr³6z-ÙnO8eèyeuÕE±™ûÖäO0!gëwlëo85 »Ç•U¡>Ê[/ë¹iÛ¥®•™ëò_§¨Cè/ÐïÖP,dÑBÍ"ƒ±§Z³–D0äEt]çp¡BÕv[ʪ—ÔìA'µÍˆ»®‘HAgƒÑ§Ö£i"ý6 0O{ß ¸€2“÷"ô3ÁLà˜„ÐOœ×VCMâ8¡Oñ(ˆã‡¯#ºyz^ª>âÒúÏ Xß<¡’ëV3SÜ~zßÞ»»y‰6SGÈk©hCû+ÒË{¸ë€NóÄ ëŸf雚!0¡>álEÁNŒPìÝ ªòn¢é.ôɨïFÍÔì kž‰8B„б9Q½"¡¿"Ð\(c:ᆠÍ#ìn(pà#cw’ìXê±VßH<‡¾5áXN £œÀA]rÔ—RŒ'éáàC¼yÞ4O GçÉ,MFÐÿAš4©ò¢¾ã ýY–(²C›O(í`E¡‡RÿùpÃ5ÔèŸÓÝ´óµoÂЛ¡^Ÿ /%ÄxîZ…›¼ÿÝ‘zåf(Sp–¬8€ùã᣽¹¶]ü¾Ó6gܯëîÄ0˜Í‡çéiaj5ÀQßsB†3‹Â'€# ¼PLqíkÏ×#V0-oÝ+Åò¥èч ƒy¬¿'¡”2Ö~ žŒÓ"ÎäÔ—Ú]B<¤3Wêjm¦‘òb8ÙÍ$诒PÜóÃ~éntïBUú ÓÕÝ’Ím¨VŸ%ˆq›ÌÜ~&0ìû|×»l–Toâ2(Ø ó/ÈeŸ5 ]=eæ£7îœÏò±íœl8zèµ™=ê¿0Á›î s:Ìáxø~³0øâZáç†ÁïCñ CñÍq®%M0.ÇbëóàFàf Â5:3qÐΛ÷MÝ6Ÿs¸zÍb£›Àù ³àÖ o.<î-6_3yÁ¯2ùpá ÿÃîŽüMÚåfØÌÂuq0i½²þb¸³Ð 8g³p×·/†îyÞü®0H°Y,ϧ%ï&_t/rF/uú§Q1¿ño»a–ºñoèÖó ‡àÜ'ŸïÏnûÁ—PºHB ¤˜3JV C‘ ¸yYK`Ì£$ X¬°äûÁ!7ÏçL¿JÏÖÇbhÎOÌî»'m¶{ýÍüÿ7_«8:¦#âxœíYëã¶ÿ ÿƒ ûr‹XIQ”èîn€â´@ó¥MQ _Y¢mådѕ赽}‡Ôûaß]‹M{H¼Ø]qfø˜™ß<(?~9äÖ‹(«LO6v‘m‰"‘iVìžì¿ÿôƒÙV¥â"sYˆ'»ö÷Ïß~óX½ì¾ýƲ,˜^Të4y²÷Jמw<•¹+Ë—&žÈÅAªò°‹={ ŸôòI)b•½ˆD²¨ÌÔ¢z7”.Óm'~>ŸÝ³o¤0çÜCÄ#Ä §º*¾8“¹pÎ¥¹!äo ú™bë Œs„ßN¾%¸•<•‰ØÂDáBy~úÐ1ä¦*®“«$>ŠÑ¾-±6C|Õ1NDåµôzs–ªý“MP=Ü‹l·Wýø%ç?ÊË“,d.á˜1Fû§Fªw:®)Yúdƒ²Q3j¶\Ñá뽈X…( ùÊ"ˆ`a++9UJêÙ­ÞëT&Z';Ù‹äãF^~ΊT(Q²"VF°¾r;#wÛŠËQ–ÊÙf¹¨ðöò ¼«È*YxÄ‹ÈåQÃË;f (q©¼,‘ÅÏy¦„{,n¬wIà:Ζ¹×–û¬Ù©ØVF®6ŽÛòjf§¢>^ª>ÝÄUã,Ë:Æ;€w.Ë'ûÝÖ|ZÎF–©([3Ÿ1O‚ÿ3u­C³]¿=´^¸@7ª}œÊ3 cÆ}•òtâbâSêÏø @ˆ»aDƒ9KŸ¹ÔÇ(˜/ >?iÏ8§"SXÇË|SYj‰<¾ ÐÝüíTµ—ç]©Í¨Ê“˜Í=‚äÙi¢s2W¼i##DoÉè8¹Å»ÞáâKvÈ^œÏd´Cãoã¼ÇÃm«¤è8åFÆe:™hìrÊRQMý¾E|t6ü‹¦Ó,ç«}uO f‹EnG¤;á²ô(³B}Zü³$ïí,7¿ˆDÝ=½Yö€Œ¢´>-]¤TûO«üYÇßårçc‰m¦+å.+%@ ¹ØªeNYx‰µ‘Jéž#Ô`ä º-¥Š•Iî¨Ny×úe ›™–¥®ºT]®šhwT£šÂCÚÅá¨Ë–i!¢žÜÐt¼¹„Ñ ò}2ã^—¹)è*¦á +]•mr1ÖPÄ@M§dí¢f†>sžJI~ÊI0zV óBK3ù ÍÿÞ¼ÔŒƒPq«xPZRÐYZ–õ_?üðÜîð˜$ëÈòc·£ei‘x#Oàû¹§?¦ÉšŒC¬ž³$ Ý |=Å£×3ÆÒÚwƒuë•KQ÷+‹[š2=Ëû›ÊòüÏz›VïÁ²™Êųٶ~ìtñeZe½¡¶^kz¸›¢37‚è/º&Xóܺ+åéx€xmʆ=°ó¸Ž¨2.*míaxÌ¡áxVôA.40~ðйc×}i ]?à¢+'â.ãqò0ƒzkÇPï+€TW ^hqòõ;âFxïèGº^ëK§, Èn²t ˆ¾ÄêTŠTëã” ‚y@‡t DË¡ÌE~ÈÈÊ ôs‚¹•XПNXHVH?$Â>]Ñ(Q̃õb—ÑPOE«znd5⤑¢Ödµöƒiõ†>l؈ù+§ÝpH©g¾XÎ`Çö¸V;£ô»³ú=éÕ:X Ç(¤sKsdÌI0eAs’:÷€ ÆÂC‚~àâ@i47Sõ¦F¾ÛÀ™LÔ[‚7ö63imo#BWí2cR=¿1@»s³€6z½³Óžp:•[¯C4 kúÆ5þÃ~fàô¼J•ò£X¿£Û`ÃH3¬;¯5j‡:‘2×Õ¿Nq)†Ô_ b®!“ˆ²¥šA“ZÓ––ÆÐ&•e|]pÇRåv[ ÕïÔìC-vL‹¼®™–>ºµš§j˜¬¦ôÛ0À<ì7 áBKMÜó(àÔ€Ã04¡Ÿ«VSMà¸Q@Ð(°Da Ý<½.e¸´ÿ³ÖWVHóºAו´ŸÞ·Þnž¿MKÑÐ^ÊèPËìòîJ SH}¾Bú§&gp„I€]°Åùƒ¬¼›hº‹<ªÎCF.IÜ‘çGÂ.çƒdlNTp¬0TB©gûÐ|L†Î>2v·““Ýë;ïâ0°'3–ÃÂrpë〠ó.8êK-B“ðpQˆo¾?(àE: “õ&M¨üh‘Àõ¹þ¬@K—Är"™O$œpE †Cÿ“ 74ÒÖ?§«içkßD‘?c}~0Ü ˆqS¶ …›sÿ»#õÊÍX&á,Yq@ \ ÆC F»‡\GC—¿ÃwZæŒûuÞ¦³à|Ý-L­<ønD£hfQ¸…ÐpD¡ñ)¯}±âºÅ §é­{EÙð#¶„}Ø(œcý < ©”ÒNðkðd’•I.¦¾Ôî‚b™¹R¿’k# Ϙ3“Þ âü*Åü ŠÈ×î†Aõ6/PÄ{è®–lî@¶rQÄÙ,@Œ»0Ø£pæ.ð3†f?` ¼Þe³ z—A¦ˆ}E.û¢nè2¨)3½qåähmå¤ÃÖCMïQÿ…ÞTOèÓ¡÷ˆÆÍ÷›Áà«+…_ ƒß›â;MñÍv®% —±Øc}Þ¼Q |§”“Y¡3=íüyÝÔeÓ ƒ«× ]Χˆ†·z ¸x3î3±øšÈ•(€ oô »ÏpäoÒ.7a3ƒëqÐi݇l°w¹!ctw}û¢Èe¾ïÏï ƒ›ayÞ-qô0ù6¡{‘3z©Ó?’ùxÛm½ÔxCo´ž=8cþrv˾¡ÒIr AŒ¼¢’dÈÌËZ mÁ!A|…Ü“[Aàb1CñFõ«äaòl}̇æüTÂ쾘Òf{Ô_ Áÿ|<©"GxœíYÛŽãÆ}7àèÐ/;ˆHöͦ<#ñ°øÅqÀ/E¶$z)¶@¶FÒ~}ªy)Jšqâ52Èj°;bUõ­êTÕiÎã7ÇmŽžUYeºX8ÄÃRE¢Ó¬X/œüü+T™¸Hã\jáÚùæéË/ÿäºèÛRÅF¥è™ ú¡øP%ñN¡wcvsß?^Ö =]®ýäº0WÏë/¿@ÁÚE5O“…ÓŽÙí˼¶M_åj« SùÄ#¾3°OÎö‰ÝAö¬½Ýꢪ‡ÕWCë2]õævKV[‘(Š|L}J]°p«Saâ£{1öym,Åû ˜¾Òl^gwð¯·ï^¥÷e¢V0Py…2þûŸß÷J{©I‡ótŽ­;òvoUµ‹Uù¼™à¥f³p(n7*[oÌùù9S‡¿êãÂÁ£À£Bðó·ÖêŒÒH²táÀaeûÔ.9ï ±QæIc!£„6C“ÈÅÄ%í¤Ýqç©NìöN§™^îÑÅ¿’J>¨Ôë=Ú¯¡Ž;]w•åªæoôVù'•Uºðß«g•ëÅ’¿Ë`&?.Ÿ%0ežåíŠóÓÄ)×µ§NûdÕ©ZUµ]ã ûHä7Êþ`v{©õðÀtWmdÚÅkÀr®Ë…óÕªþtš¥.SUv:QÆ: ÁÎÌ©IânþnÓvâÞß0¨6qª…‰ö£ÖÛ…Ã"O]X‘‰¥\LµvCÓå Â{w_d²gwœŽÜ—¥µÈã“‚3׿HgUmôa]Z÷™r¯&cY'q[¨“ˆNwКtð'ó[66néNwtÛø˜m³ vI&6öC§¯âüŒƒÛ^©RgB¹Ôq™^ ¬ý²ÏRUÝðLUÄ;w¹´ ~UoUî.6›[Ô…~Å ®J×ÊÝféNg…yÙüU–÷VÖË_UbîÖ€ª¦P˜^¶®¶Z›ÍËG~Õö×¹^ÆùØb•€J¹Î ×èÝOE®V溦lð{MµÔP8·×ZCdƒ§ÆâñlÉÕŽBÈœl§9ž¬Ðé¥6û¬$ ùY¨¶;Ûujú ÏâVf3É£‚’1:Ñž®kS8†ººmTU¶ÌÕØ—°"iz)¶ÞoGØ=çY¡ 9ä§K; þÌŠaÆw²:Ó»ŠîOKz£Ø*§±‰¾½—qÌzÿÝS·Âc’Ìÿ©ËýŠY“x©÷Zçé,L“9p„mlž²-”Ë/þ ”àÑ?+ÆÖ6vƒy›™KÕЫÄ+M¶™åÿÝdyþƒ]¦;÷`ÚÌäê©^¶ùÚŸÅoÓÖžöÑï¼Ñ<®/‘™ÇKùñ7[íÑ´j®K½ßm!Û†à ü<‹ÊzÄF¾æ@Ußá™ 4Æ ±dÁCŽõÒ’‡çئߖÙñtÏ€â³h†íOû°дÁgTÛ!³‡sø uKdÝõ(Rµ ÁAäŒÄÓ-/Ф ¤ÙQóDd0#¡Ç(ç’Ï\ \^@áÃaI[ÿFӼݯä&ÊÖLÛí˜GÂÀ¹Q™“ÍÄ–aÌÉ×ÀÅòy7†ú›Û)°Ç#&iH¢¯+SêjÞÒŒ[AÓ³Á‡@Ÿ9cÜf,lj(Ò¡ðW(ºc)ÀW•9´^3ç,¡Ñ–e|jv5êÕªRfÞoà|ˆ] åÜ­ÉÕ¼Q"{åÚouéˆÖˆ‹ìg§ôœ"Wz¸þHå†3`I­åߣ{am~¹œÍ߯FJ6QõIà£KÈÒslö¥®68}©‚¼±‰ *Ï8©¯CáæØÿnKçÃMT¶`^õâ@ÀEâ{¸žLv¹®….‡ >÷†ïeøKˆÛ…cJp[ŽqÈK¯ŽÌ“\ʉG¡†žà2d2ºÔu”›P‘dxYÞúj«—âzìfe8Åú'ˆ$”RÎ{÷É$+“\]ÆÒ† RHH: ¥½¥u™F&Êc=’ßLâþ! %X %}ëatïRKJ\\äŠÏ]¨V–‘˜$H.>!8œ„ âL¨'qEwÙ$©>IÈ `s,ÞPÈ~:zÊ$FŸ¸sFx’]çäCêaŸkîÑüyÓ=Ÿ‘ ÜC¢Ÿ[áïƒÏ¤ø)¾IçàZÒ‚ñ:ÏXŸR€ODïp Â#:it5p:6í›¶mzpõš`£ç@|Žyx‹ñÀìjó­9 ÿÀ…WþÃîü¿ôËMØLà:B0­û ®ÂK/‚Oàno_{‚16½+ l‚å)[ŠðÃÅÛ(=xù´î_;õßFÅü: À p©1Hþa4ºÈÃ9"!òÛãÙO?xÃi‹$Ô@ЧdÆ1ÉPH†D€æQRͰF(<‚¥¨%,áÏ.ÆÑÐ/ÌÞ¥¯r_KA_í. Æ’’à-$Jý‡àzÍTô$¼*|²Yÿ áú¬pXà3ËÇë¯w¬aZÞÓ[%R¾v¶ó&Aõ®9ŠQàÕÍ6b³þ[cÄLÜѺ/ªÝ{úZ=ÀáµW§ÝÛýöÕéàqð^v„äúºBÅ‚ä‹û×ïƒeÈ9BÀo‚„Y6nlÀŠê+½×8ðz±¾WFª 'FRÖS¶¶„ÿbëë£ýûüþ7YÒÜuF-¿xœíZÝã¶ÿAU^nQ‹")Š¢ïhA´/mŠ})d‰¶•“EC¢×öýõêË’%{÷½m7]îÖš93¿ù ö?·™õ(‹2Uù½M¶-™Ç*Ióõ½ý_~t„m•:Ê“(S¹¼·seÿððí7‹?8ŽõçBFZ&Ö!ÕëçüSG;i}Øh½›»îáp@iCDªX»w–ãÀRX\>®¿ýƲ,Ø;/çI|o7kvû"«d“Ø•™ÜÊ\—.Aĵ{òñY>6'He¬¶[•—ÕÒ¼ü®/]$«NÜéàUR$ CS—R$œò”ëèè\¬…sN­¥cx=ÑgŠÍKðìþvò-•j_Är %Ê¥v?þò±c:%:éëi;Øwàí<ÚÊrŲt[z­à&zsoS\?ndºÞèóóc*RÇ{[Øò 眿5RgÄš’&÷6+š§fËy'ˆQHèI".ÂØ#ÞÌ¢˜„&i”¶æÎ›ãßÛZ©lΘIg£Šô³‚He¨ók·“<îT¡UšÉz±»Q[éždZªÜý(e¦vQî.Õ@‰ í¦±Êÿ¥Z¢]~Eß1ÙA´B>Í=µÜÃ^$rUVrµ?Ì#µ-·fvæ™ã%ÆÏ=ÑeT6ñ±¬]´Dgª¸·¿[UŸ–³TE"‹–ǫϧ ä©>Õ©ÜêomwøŠ@¹‰u@Œ¸Ÿ•Ú!©Š?Ôxˆ ϧþ˜i΄žç—BÄ÷&6Î>O5dÓî8V°/ #‘E' Ö¯…¶2åFÖ…q£.ör´òæ`‘ÓŸ„tlx#Ò&Á˜]“1©qwºÁÛFÇt›~–pJ2’1ô¿Š²3®û¤BÊFÆŸd±TQ‘\,¬ü²OY^ñL™G;g¹4é>É7,géÍ5•@®ž±ƒ#“µt¶i²Si®Ÿ–ä­ÕòWë›§¯tÀP£@ÊÔÓÒåV)½yÚäg©e” %V©¨ë4w´ÚõðÔcdr¥§9Eß)ÖRimòw Ð "}<Ô‹³$W³Ê²ôÉôãÉíŽj²ÏP€‰r»3=¨&Ä™ÜÐL&!Ê™5ޏ§infÈK ›¶U¦ËL} È# &—dãýf…9s–æšDvº”ƒ~®ëg|K«2½­ì׌­ÔQé¨Wè[’ßyæùß>þøÐî°ˆãù?Uñ©ÛѲŒH´T{­ýp¦/’xÃ6ÒéJ™6þÂÂ=3†Ò&v=½µæBÖÃÇä–ÄÛÔ¬rÿ®Ó,ûÙlÓÚÝS›êLö¨ ·±¡µÑí¹p['ÔëK@fÑRBZüÅTzk\,×…Úï¶÷vÕ ìž{+B·DQ^G˜ÀÂ× æÕxæÀ,ƒ ꮋÂzˆdÁ‚sÈ{jÀ¥EzüÍÓ§8`^8ÃæOóè{3˜ÕBL¨O8›Q#¡Ø»;G­·Q»•OA]T‰ì‡ö€<>Aa(8%õ‰ê'"ü GlæÀÔ…|êv×ß¶4eo ¾çín''–¦Tš&ç!øöÅŠRŸL6Æœ|£X6ÏáÚP}sZF,ô Hø}© õIΛ)ã†P·jÄÌÐÌóZºIT8Ô'}â¯Pk‡T@­,2è¸zÎZZA-ŠèTŸªGU«U)õ¼;ÀÙˆ]UÜ©f«yÍ´Œ5TUèºå¥ Zµ¨¼Ð|f`%â`µpõÒ f”qD+úO–QPI[ÿºÔf‚ob#„7bus‘ÊÁ)ZLH‘ÞrP¯šàt òÆä34¦>Ã\ž†ÂÕµ¿íHgãF,S''½Ø£ùp›ø î(c§ÝB®c Ë@Á;|+ø^†¿€¸]8¦·ùPà<Æ}qé5àQßC‚ 1ò(ôÍq&O„—¼vÒö|¨H"¸,oÝ5µá >…sXŒ±þ" ¥”±Nð-D2N‹8“—±4á‚₎Binhm¦‘óX­dW“8 ¯’Pn‚¾õ0ôºw¡´J³È„ϨV‹¤ ܨ1ÁÁ(\gB÷ùï²QR}•AÁf˜¿¡}Ñ4tìõ”QŒ¾rç ñ(ÛÎÉú£‡y®fú_Ìêîùh90{ëó{+| ¼Å7†â«ã\K0NcñŒõñð•Fà3a!5ºjâ`7m"Ÿs¸z°ÑÍ@|†Ypmb>â¡Ç½Éæ[Í@^ð*3^ñ? »gòÿÒ/Wa3‚ëq0i݆¬? w&PÀ9ÁÝܾFÜó¼ñ]¡—`#,§¥ß]¼„r×½—OëîµS÷mP̧AŽa–‴áïG£<Ørî“/g§¾÷bÓI¨sFÉŒa(’ž[Æ"XðŠâùœYx†ûųqØwçS³séèm˜öÞ`]¤Õo²•¿äô ¼é7¿aüï%Rxç$4’&AΧí3Ûì˜æÍ/å„O±ð-Ä´æ}$ë£úÒÏ•œ‡hè‹gé½}š›†œhüEú†ŸGVN†ôù0ø xfHÏãOâ™#À篆gþŽç7lådHÅËá™BK'” /òNaˆÇ€È'ñ\¿¬yŽÚ—€3Ãïp~ÃVN†”¾œa¾> çv,¹ÔÌç/ŽgöŽç7lådHùKâywƒ±øéò\%¯…gñŽç7låTHý¼^Çs;ŸoäÍ«ŒúçÂüOøù(34S"œxœíÛŽÛ¸õ}ýÂû’A-Š7Q’w< ´Á"º/í^€} d‰¶µ‘EW¢gì|}u¥-{2A›´AãÁÌçBòÜéûŽ»=ªªÎu¹œQLfH•©Îòr³œýòó^4CµIÊ,)t©–³RÏ~xøö›ûúqóí7!`/ëE–.g[cö ßߪëjãg©¯ µS¥©}Š©?sèÓ‘>­TbòG•êÝN—uÃZÖß¹ÔU¶ÈŸžžðo¨hÇ>a>cPxõ©4ÉÑ»à…s^ãe„pé É5(g¿}Àµ>T©Z£Â¥2þëŸ_HàÌdî:yù®N“½:Û·¶jHvªÞ'©ªýÞ.ð”gf»œ1ÒN·*ßlÍ8ÌÕÓŸõq9#ˆ ³˜J)Å8ê¨F£Ó’gËu³n˅똡W*’i’(ŒçˆF=B=ÌQz¨Þݵܽ܋L§VŽå,ݪôÝJßÊf¨²·y k<(xØR÷º2Þ:/TËìoõNù'•׺ô_«GUè½u-Ÿ€$•ñóT—o‹Ü(¼/o¬wÌö`¶X^ÇžzìƒEßgj]7t­bì”Íß"ñìñ2«p‡t•Ô¡Ú'píBWËÙwëæÓcVºÊTÕãdó9Çi°}nNmXöë÷‡¶ äA½M2ýž1Á¾×z·œñ“PROð)¸ÇL‘rŠ…=cLYÄ„,˜ü`ãÊÜ@\íÓUe)Šä¤@üæí©ê­~ÚTV“¦:¨ ïS^‚P^4fSÙ;’>0(!ÓSv46LnáNÏàvÉ1ßåNh¬®þ×I1ºÄm­4ÎÒÄFµÒI•]06z9䙪oh¦.“½·ZÙÐ¿Š·(oŸ˜í­‚R¿`Oeåíòl¯óÒ|˜üE”Ïí¬W¨Ô<{úf ØòBÊú0u½ÓÚl?,ò‹Ž¿)ô*)Î)Ö¹W©6yé½wüÉAjm®cªÖ¯¡VÚÂSm\ä97â³Ò&1Mj'mÒƒÌ6.Øq"dN¶PO8 6D-$ÅT»½-ZMàfà 3 y…s6Áž®c3U]Fƒ­su¾*Ô¹”p€2hv ¶&ê8왋¼TPLŠÓ%¥ç¥›zX“ú àOK@‹Ø)“d‰Iœ‚Ѓ‚AËа,þþúLJ~‡û4]ü¦«wÃŽY’d¥`ÿÙÿÏÒ´»Ä<ä;ȶ=ùt÷þˆ8§¶¶sÖmW®TÛ­\íÛ²t—[.ÿ&/Š¿Úmz¹esS¨‡fÛv8ÈâwÂôÂú®´÷~¯vº¹ôÎ"Y)¢¿Ù’€¦©uSéÃ~ñÚU™£çó2bª¤¬­F¬…aX$F½"sº í îslÎ]:áh{gÐm•_Aµ  çÄþtӀϡˋ e•bÅ Ê¿ÍçlÔoÐ3ënÎ,ÕPij3ðôHÇq$mOÔÎhÌiˆ9ÔèHÌ=N9§âÎݶ´IòlyGÛÃN^ªlbµ%‘c³ ŽÚœl$vÉ‚~½[±(áÎÐŒ¼A°ˆyÄB_›J¿S‹®-"¤´…Iݷ༇ۈ…C-ÀÊÌþ™ù ê³Yˆ–%P«*9µ§r z½®•Y …Ø'ó½¦[´Hd¥Aƒ¡F×—Jký„X€yl?sK!/¤ùDÊ çLHÌø6Ôè÷ËÕ¬ñ­m¢ˆOPC¥KPŠÑ•Õcb•:K\q†TqcÊX Ÿó ¾î 7yÿ½#ÂMP6a^Õ¢ °å‘+J{Îs=ëºøê¾û^š¿»](¦µÁ%!àBÑ¥ÖÇŽ#EB ±QÈ£ø×÷åpýX^¦·á‚Ûá#yÍ{ìaáú9, ©TˆðK°dšWi¡.miÍ!$#61¥½Ñõ‘F'ÈcÃ)nqÈ>K@IDûÒÍàTï¦W¯<Ü]Ó¹Ù “(–“iÌEA'”„s)Ã2Wp£É&AõIL [ù™ì£º¡£SS&6úÄ•3&“xì+§p[;ozö/mõ|Dôzÿµþ'ÜàkSüLS|³ƒkIçŒ×}qôõi ð‰Zàgz *b6)tM$A:>­›¶lâ@J¸zM|cèÀø‚ˆðV$,c.ùÕâÛô@<ü,=PÞèØí^`ÈÿK½Üt›‰»žytZÏ»lpÕÝE„C)ÅÄÝííK,9çÓ»‚`_žvK1¹»xò7ÎãÓfxvFgÉüº€Sè¥Îý€öæw­Ñ[䈥 èÇÛsXÞyá´Ir #R0:’d(#ŽRD¡Íc4d$žRÆ(0%‘l <‘9q“goãØUç‡æ Òóȹ©®.Îgˆ±V’]èì2Jê’JM¤Ñä-LzåÒ}ÕîbþÖð¦ÝQ}X\Ã5M<æ¬ùæuÛ¢ ÷ÊбPÕ<Ûû™]Å1ÈKü—q&Y|ÿ`s4ojÇ1¸;æýqE1Ô[3>f0èЯˆBF!:Ë1§C ‘Ða¦¡³.LÞœ±ô CÑߎ²_Ç,Kèfàçã²0þý„‚¹@[D 7ÁAÀãѹM«’@ €1…ž¨¥˜÷4Èë,Ö³Þ ¶= {ºŽÞŽáˆ Í| rèEsç íÚ[wÿuL÷ÖíL‡ôIôÐ8{Ú”}o¿Ò€ÿÿ‡í8!xœíYYã¸~_`ÿƒ }™F,Š—(ÉÛî‚Á"’—dƒy d‰¶µ#‹E·íùõ)ê²,Éž^$3È ãFw[Uţ꫋ÔóOç}á¼J]åª\¹aבeª²¼Ü®Ü¿ýò³¹Ne’2K UÊ•[*÷§—ï¿{®^·ßç8 /«e–®Ü1‡¥ïŽº@Joý,õe!÷²4•OñÝ|z•OµLLþ*Sµß«²ª‡–ÕCimzñÓé„N¬–"qû˜ú”z áU—Ò$go4ö97–bŒ}à Dß(¶¬À8øíå;ªÔQ§r%*¥ñßÿò¾gze&Γ—ª49È›u;bc†d/«C’ÊÊïèͧ<3»•Kqó¸“ùvg®Ï¯¹<ý^W.v° !¿~k¥® “†’g+”Ú§vÉåÐ;uÞÉH¤Qˆ£0^8Sâaâ‘`á¤ÇʨýS3ºÓ{™©Ôê±rµÝd™h­N¨7i¿ˆ<”6Þ&/d#îïÔ^ú™WªôßËWY¨ƒu&ÿ $ÚøyªÊ¹‘èPޙXÌs/÷ŲŸ3¹©j¹Æö‘ºŽß0{…ìö2kâè:©ZhçlÁ™ ¥Wî›úÓqÖJgRwsåŒñ$»Êɇ­‡}®{æ/æMõ|u<è="çã·RøßpƒoMñƒ¦øn;Ç’Öç}ñêëÓà3µÀz Âc:)tu$@;6­›¶l¢@8zM|£ï|Žyx¯â1l¶øÖ= ¿HÀ7úv»7ùi—»n3q׃Në±Ë³îÎ# Á'înO_#Á›ž6ñåi·ã§Ñm”¿\>mûk§þÛM2Ÿw8…^êÖHÿyÐ#" ¿Ï~úÁ §M’)œ’Ç$C1'u´y”„Ç ŒBBÃØ Dp$j wð“g‡q<4ç§foÒ‘¹ºÄÙ\W&ÑîØ–ù&X úÈd}ãŽc¥ú×1Ñr,µ=ßnÜ[íë7Ü›ôjTÍû“9VÝrCãl¯Ÿ1°^LcF‡ãàŒ€&ö]£õ›Õ9>µÜB»™Hô–ؾ!c1·KYöc*·M+×Å|RÛò:樚Ù^jŒ=ÝÄMï$˪âÀ:WõÚþaÛ>g£êTÖ·Ï%Oß3}êBr˜Ø´™xÀ7š}œª@ÅŒûQ©bm3Ž"AD8c'€€+°0Ì)› Àªb!|̸àðƒq{(3 QUçêÚHäñI‚Ú?djöêeW[êú gc_²Ôrû kß‹ aA0žëÐ˘ ¹Å;Ýáñ1+²vIf2Fƒ©¶q~Åm«´piâި¸N¯¶v9d©lnX¦)ãÊÝlLà/ò Ë­b½¿5A+Pª7¬àÊt'Ý"K+••úuñ7IÞ[Ym~—‰¾»ûvX²ˆBÂz]º)”Òû×U~Óöw¹ÚÄù¥Ä6Ó•z—•®VÕOF.·z™Swø]bmäÑb  -DîÁ`ŒÏZéX·‰wirÛyˆÀ~¤eé“)SÇ“!Ú#Õ„¨¡D!;eQ™’Õ¶âLîi&Üå,¾OgÜÓ27]åu4˜*×d›\^j (c ¦×dã¢~„Ùsž•ÊI~º–S`ô¬œ¦…Ö¦ƒ¡xó"Ð1 ©ã4Öñ¤$ ¤`´2´+«¿½ÿùiXá1IVÿTõ‡qEË2"ñFÀÿöÓ™þ˜&+h0ŠX?eä Óœüú‰Gï̸”6¾›ÌÛÍ\Ë®WYìÚÒ¤ÈÌ(ïï:Ëó?›e½'Óf:—Oí²Ý먋×+3(ëMµ}ôktŸ»ktæñFBýÅ”kžZwµ:TÄk_5ì‰/ˈ®ã²11†×<Öòv\è4/~ð0ºcw iÁ³ï'Ó€mëìø*c@qÈüÈÁæ§ÿ |Šf„  n:TPÄÅþÃÙ}“…†¥ráÝÝ…§Z‚ƒÈ¾ Ï·DP NI·£î‹ˆÀ!!ò)c‚9®O|PŸ°‡é‚°¤I’ÓO¬=®ä&Ò$VS}DÂÀ¾Ñ蓉ľ'Y‘¡{ËW%œÚ7w``Ä g !‰~lt­>ÈUßaܺ‚8„Þ›ùþ@7 ›ZÊtJü2ó%à+ëê³^±–ÆPë:>u»šPÕvÛH½7pV¢Š!ç»m;¶ê˜–ÑÆ‚ 5º¹6xë¯ ™Ç-¨å „ÛGH7t(4^´¥ÿb…­´õÛõlÆùÆ7Bø3ÖØG©Œ¢UíBGõëC-/Wïœ1UAܘÀ†2–ÀsÔËP¸9ö?ÛÒY¹Ë$ÌE+Nhb`<¼`´{Èu tLð ¾-|¯Ý_ƒß® SƒÙHp>㸶ðhà#Á„˜Y hˆ8¡/¢kÞЗûd$^§·ñxÛó_BÙ,B¿„'!•26 ~ žL²:Éåµ/» „¸ 3WšÝidÆ<¶#ÙÍ é (îBÐ¯Ý “êÝ6àòË‚‡%›»­ŸHë.6!8œ¹ üL(â_à]6 ªÏâ2HØ ó¯ÈeŸÔ '5eæ£Ï\9#<‹Ç¡r²iëa¾ÛÞ£ûYW=Ÿ-za}üV ÿ0øÖßiŠo¶sp,éÁ¸ŒÅ3Öç-Àgjïô@„EtVèÚˆƒvþ¼nš²‰Îáè5ÃÆØófá­ˆˆG>÷‹oÛùáé8ðŠÿaؽÁ‘ÿ—v¹ ›\/Ö}È‹pg…œ³ÜÍé‹aÄ}ߟŸ&6Ãò¼[ŠðÃÕm”·›\>íÆk§ñí"™/ƒœ@/u‰2¸êÁó GÄy@>ÝŸãô“N“$!RÌ%Ã$C.|+±´y”„GF!¡ad"Xð–âœYØÁÓä9ø8ššóµ„9šôMæbÛ`Ãé›ÍEaÃXP| Ò^ã˜æ: ¡‰v|„q( ¬?ZEèÃÐwÆ7 ·?Ðà€q!‚%†Å#÷¤CóŸpa¾uÒó^³Œ³b+@¦òÂÖߺawø®û*Û½ÇoÙT.]¤wýýEêäsrK{ëöðBù'ãºKæºþþ7‘qm *7xœíZÝã¶ÿAu^nQ‹")R¢ïhA´/mŠ})d‰¶•“EA¢×öýõêË”eïmšÝ´Û;îÖš93¿ù ÷–ßw¹ó(«:SÅýŒ SlQƒgKø;È÷T«}•È5,”¨Ú{ÿÓûéb”êÔÖÓ;v´ïÈÛE¼“u'²özz«à¥z{?£¸}ÝÊl³Õç÷ÇLþ¤Ž÷3ì`‡#‘ ØùS'uF i)Yz?cE÷Öm¹1Š(=iˆ(ñ‰?w(&‘‹‰K:¥½¹‹T%æø 0û(7UV¢Áƒby,U¥Ýu–ËVÖÛªôN2«Uá½—2W¥Wf(q¥½,QÅ¿óLKT7ôÓ‚×¹§žû`ØËT®ëF®5ß¼Ò™ãµÌÁs¼Ô¸Õ]ÅuÇ)ã 8WÕýì›uóôœ•ªRYõ¼ yÆ<Îô©ÍÜ^h£xÀ7êmœªÄÂý¨ÔFè`HðS6eÂ^ DF|º"»7Aq÷E¦!kÊãTÁ¾ªŒDŸ$˜Ýü ½T½UxPW{9Y{È 0Æí N":µ¹éaO0žÚÐɘ$¸Å;=ÁÛÅÇlÈ…S’‰Œ±Àöû:ÎÏP¸í•$[™|ÕJÅUz±°ñË>Ke}Ã3u—îjeû*ß°Ü2ÖÛ[ B=cW¦éî²´TY¡?-þ,ɧvV«Ÿe¢Ÿ<}£ö€j¢P>-]ï”ÒÛO›ü¬ãorµŠó±Ä:Ó•j“®V¥…'‹‘˵¾Î©Zü^c­”Ö&u§m bÃà¡•Xž% ¹ºUŽ£O¦ÃO†8¨&û % Ù™(w¥é6ÍØ ÎäŽf2 Ñ€qáûtÂ=]ç¦`†¼ºiPu¶Ê娗p€"jzI6ÞïV˜3çY!¡?ä§K9þÌ ;ã{Z“é}Q÷¦U½e줎ÓXÇVïI|ð2L‹¿½ÿþ¡ßa™$‹ªêð£ã‘x¥öÚÙÙ¾L“Ì»X?d;(f®ø#ŒKïÌK›ØYz[Í•lÇŒ«Wšì2³Êû»ÎòüG³Mo·¥6Ó¹´¨K¯³¡·Ñ³\z½Ú×Í% óx%!-þbм3-–›JíËd`×f–{ÇAWqQG˜ÀÂÇ&ÓwxîÂÔ‚B,|~7Da3F²`á9ä–pi•ßAßä‡ÌæØüé^¹?‡©,„r°90ÜŠý»sÔ¬ú­8u3 P#B0f#òôHE‘(iOÔ¾Áç$D>eL°¹ óâÔ'ìÎÞ¶4eo¤Þòö°“›HS*M“ó ùìbE­O&»ÙbA¾…),_pAh>¹=#ù‚†$ú¶Ö•ú Ý€ƒqGh[5â¦eæû=Ý$*j(R›ø3ÔÚ1P+«:®^°ž–ÆÐ_«*>µ§²¨j½®¥^ 8QÆPÅÝf¬Z´LÇXã@U…®[_:¢õW‡räG晃•(¨ã „›GH7œS ÚÐp8Fa#íüëR› ¾‰þ„5LFª§hU¹0#=Æz_ÉQ½ê‚3T(È“ÏИxƹ| 7×þ¶#›°L¼êE‹ÆáÞðÜF¦N{ ¹®._àÛÀ÷2üÄíÂ1¸CóYÀÅ¥×€G¹bâQè›! ˜}]òúIÛçP‘DxYÞ† iÇÁ5ô˜ÃŠpŠõWˆ$”RÆÁ·É$«’\^ÆÒ„ R(tJsGë3L˜Çf%»™Ä!ý]*ð¹ô­‡ÁêÞ•Òf(qÌ"W|îBµBXDÁ$Ašpð Áá$\gBQ—ñ)ï²IR½JÈ `3¼¡ýªièhõ”IŒ^¹sFx’}çdöèaÞ›Ù£ý³¶{>:.ÌÂùø¥¾ ¾ ÅO Å7Ç9¸–t`¼ŽÅ3Ö§#À+ÀOÌ@„EtÒèš(ëüiß4mñ €«×à Ág˜…·f ÆQùµù63þ.3‡ ¯ø†Ý3ùYúå&l&p!&­§!˯ ›ÀÝܾFïûÓ»‚•`,O§¥ß]| åm¬/Ÿ6Ã×NçQ1¿p³Ô¤¿>ò`Gœüúxê­/6M‘„HqÀ(™3 E2 „ï$1’âhŽQHh9œ#‚EÐP|0ϱ]<ûG¶;?U0—Ž3熻ؚ¯úlwˆ®ð)Çü¿—)Sÿû»åý®œö¯5mà·<.˜¥«ùÅõÍcQMWˆ8Š Ä-ÏŽdl}­³àyÞ ž8øÙP«þEô³Ä¬x ÌÂý°ùåŠÿ`–šoßI¯çÀoÅ,Ã_0û&-¼Ê©³ Nñ¨hw˜%0?óÏÓ³ì%<ë#æG\\é`Oz¶‘a0‚0òúÕàK{›^ å‹t°[˜õáÆý,=Ë_¤ÎÞš úLþ烹`-Íÿ;€Ÿ¿°¯Fé³ mxœíYYã6~ÿ U^¦±ER¤D9î°;`÷%Éb} d‰¶•‘EA¢ÛöüúuY—{:H&È`Çî–ªŠd_’7ß\Ž™õ,Ë*Uù£M¶-™Ç*Ióý£ýŸŸ¾u„mU:Ê“(S¹|´seóô囿9ŽõÏRFZ&Ö9Õëûü]G…´Þ´.Ö®{>ŸQÚ‘*÷îƒå8°WÏû/¿°, ÎΫu?ÚíšâTfµl»2“G™ëÊ%ˆ¸ö@>¾ÉÇFƒôYÆêxTyU/Í«¯†Òe²ëÅJg¯–"aº˜º”: áT×\Gg²ô\ZK1Æ.ð¢¯[WàÙ~{ùŽ€*u*c¹ƒ…åR»ozÛ3Œ ÷é;:wäí<:ʪˆbY¹½Ùàœ&úðhSÜÜdº?èÛýs*ÏÿP—G[Øâˆ†Ä÷}v»j¥nˆ! %Mm0V´wí‘ë^£"Ø'‰|ÆñVÅ$t0qH»igî:Q±QÿÑÖÑöç8S•D½ûå¥P¥vvi&a÷ ŽÒ½Ê´R¹ûV>ËLAn‘j D¥vÓXå?g©–¨ÈïìwI ˆNè/s¯÷ɰ7‰ÜUµ\c¿¹¥¶å6ÌÞ£^bü:ÝFUË*¢= 8Så£ýÕ®þtœ­*Yv<¿þŒy Bœêk“ºÝþÒfã^ߨQ¢Î€÷½RG }À<1ãÇ€\Lås.œ) èùœ !>™à8§<Õ>Åe¾Á©,D]%˜_ÿ#TuPç}i<©Ë“œ­=§9å´X'!ÛÞŠtø'˜ÌµleL6Üã]_à£KzLßKÐ’ÌdŒCÿï¢ì‰û^©Árñ;YnUT&“…µ_Ni"« ãvnÎvkR|Ñu†å‘>T/ äª>bQ ?Á‘É^:Ç4)Tšë‹¿Jò¥“Õö뵯÷€3 .(”¦KWG¥ôáÃ&¿Jý}¦¶Q6–Ø¥°RîÓÜѪjÀÈäN/sÊÀK¬­ÒÚäð¡5F†0xj$67 È®v•eé«é5—«!Ú=Õ¤Ÿ¡@y¸å±0}§ ÄÜÒL*!ê3.<θ×enfÈ)ÒM«ªÒm&Ǿò¨É”l¼ß®0:gi.¡Qdשœ¦ù0å;Zê]uwçå½a¥Ž’HGƒbß‘xïe˜9Ö?¼ýö©;aÇëÿªò]¢e‘h«NZûéFß$ñ¦„c¤ŸÒ#Ô3aü†‚{cŒ¥Mìû6;—²8G¯$>¦f•û£N³ì{sLg÷`ÛTgr@ݸ­ îÐÈÛ9¡¹ÝO™E[ iñ/Så­yµÜ—êT!ÛF`Ü;î ºŒòÊ8Â.3˜Qßà•ó °ðøC…ýÉ‚·¶—–éå 4PN1tÂp…ÍO{˽Ìg!&”Ÿ­ "F(önQÔÅÉ(¨ûQ€j‚yhÈs• CáSÒhÔÜÁW$@eL°•“âÔ#ìax iÊÞhû·û“œXšRiºœ‡HÀíÉŠJ_M¶CÆš| ãX¶ÎáQ¡¾r:F,ô Høu¥KõN®ÛIã–ÐôjÄÌÍÌó:ºITPj È“!ñ¨µc* V–´\½f-‰ Á–etm´PÕnWI½î¸QDPÅz¾Z7LËXcAU…¶[MÑú·E9òBóY•Ȩå„ëN°¢ÌG´¦gqŒ‚ZÚúßt7|!¼«TNѪt`HzŽô©”£zÕ§¯P7&Ÿ¡1Åðçò2î®ý}*ÝŒ›±L\ôâ€Æá â;x.™;í%ä:º 6ø ß¾Óð—·‰cJp‡ç1Ÿ‹©×€G¹‡bæQè›ò™<Nyݨíq¨H"˜–·þÑ´å  =FY̱þ" ¥”±^ðSˆdœ–q&§±4á‚ò…Ò<¤u™FfÌK½’ÝMâ€þ) å{\ú©‡aнK¥ÍPâ0˜E|î@µBX„þ,AêpÁ#7&8˜… âL(ò¹¿À»…l–T%dP°ö?¡ý¦iè2è)³}äÎâY>v“ Gs_ÏÍ_Ìšîùl90{ëýçVøGÀàóPüÂP|wœƒÇ’ŒËX¼a}>|¤ø…ˆ°Î]=ù`7m"îûðè5ÃF?AðfÁ½ˆq䇞ï-6ßzò‚?eâæÕï_v¯äÿ¥_îÂf×â`Òz²|îL À÷Ù îæé‹aä{ž7V$Ø Ëói)Ä“—Pî~ðòiß¿vê¯FÅ|àf©1Hþa4ºÈƒ¡ïsòÛãÙo?x±iŠ$7_¡øŒ’ÃP$_xVlóÍ (W„¡Å9"Xø5Åã>³ð ‹gãpèÎÌÞ¥‹îjœÄv|ëÓ>)F·½cxûá4'¶'­g)Q{ís!F¡À!¦„¯h3°)³<äQóv óUsŽ]UÏÃÔÿÝ®zuKêet¢?C 0´WŸ[N«4ôÌVÿATÿêáéß’›dܘwÔðÿW)Àbh¸ [xœíY[ã¶~ÿ (/;ˆE‘EQÎx‹ š—6E¾²DÛÊÊ¢ Ñc{}u³nöNÐnÚEW‹‘Î9¼œó9Ï?^Ž™õ*Ë*UùÆ&Û–Ìc•¤ù~cÿí—Ÿa[•Žò$ÊT.7v®ì_¾ýæ¹zÝûeY0<¯ÖI¼±Zk×-Ne†T¹w“Ø•™<Ê\W.AĵòñM>.e¤ÓW«ãQåU=4¯¾J—É®?ŸÏèìÕR$ CS—R$œêšëèâLÆÂ>—ÆRŒ± ¼èÅÖ§€ÿ½|G@•:•±ÜÁ@‰r©Ý÷¿¼ï™F‰N†ó¤ù‡*Ž 9Z·#6fˆŽ²*¢XVnGo&8§‰>llŠ›ÏƒL÷}û~Måùê²±±…-ÑpÎÙí­•ºNJšllPV´_í’ë¡w b½ó¶1•‚x;eQL±ƒC++>UZŸšÑÞëDÅFýš¥¹D½1ûéå¥P¥vvi&A÷ ŽÒ½Ê´R¹û^¾ÊLÆÜ"Õ@‰Jí¦±Êÿ™¥Z¢"¿3ß%)¢/s¯÷ŰŸ¹«j¹Ææ“Ú–Û0{UÌöcÜè6ªZP,«ˆöàÆ™*7öw»úé8[U&²ìx¼~Æ<8§úÚ„`7·i3q/€ïT‡(Qgð‚÷£RÇí „yèQ6ãÇà*„#Â(„Ôœk6Å„úÁŒ ðž 8Î)O5ÄPq™Op*K#‘EW êï…×/RÔy_;êò$g#Ïi*9­»“Î5oEº ÏõkeL@Üã]ðŽÑ%=¦%ì’ÌdŒCëï¢ìæ÷mR»ÊAÆd¹UQ™LÖv9¥‰¬îX¦Ê£ÂÙnM/ò Ë)"}¸7A-«7¬àÈd/cš*Íõ§Åß$ùheµýUÆúáîë9` ÈL ÉéÓÒÕQ)}ø´ÊoÚþ>SÛ(KìR ®RîÓÜѪøÓ€‘É^æ”ÿ.±¶JkÀs­]ä‘ôÑY*é:‰ã&åA^»MØŽ´,}5%ér5D»§š5”0`7¢<¦<Õ­‚¸‘[š 7D9ó…çÑ÷ºÌM@W9SѪt›É±–°<j2%ˆÚfϦî@)É®S9FOóaZèhu:èò¿;/ ã(u”D:”ƒŽä÷V†Ödý—÷?½t+<ÇñúïªüЯhYF$Úªào¿ÜèÏI¼†fâé—ôùÂ4"ßCïðìÞciƒÝ`ÞfæR6}Éb‡–ÄÇÔŒrÿªÓ,û“Y¦Ó{0mª3ùR/Û¼öº¸­2²îPÛg·³Fó¹Ÿzgm%ÑŸM]°æ©u_ªSq„xÝØué°v® ý]Fye,b†×,Òò^9Ðï  ÏêáØ]Z°à†ý`°m™^ÞA­õ)˜®°ù×~úÞ ú¹CE$œ­¨ ˆн§|ƒ…º¥|2Bw?Bª!Øíy¾%‚ÂPp¨ÔõŽš/"ü Ôv&ØÊñˆ‡|êö4\–4Ir4ýÀÚýJN,Mb5%ÑC$ðíɈJ_M$¶ýÈšü[¶ÎátP¿9#z‚$ü¡Ò¥ú ×mS„qKh ;âúlæyÝD,lj 'C⯙ÇTp_YfPŸõšu´$‚j\–ѵÙÕ€ªv»Jêu¿›E9ß©[±uôŒ6ä`¨ÑÕÔ€ÖÏõ‘šgZ":PË.«~„t‚eÑšþGËÇ(¨¥­Lg3àl„ðf¬¾‹R9E«Ò~ê5Ò§RŽW NŸª nL`C‹áõ²+Üûïmé¦ÜŒeæ¢410^0Ú#ÏuŒë2˜à«ûÖî;…¿Ü&†)Ál>$8q_L­<ê{H0!f… ÎDà‰pÊëúrχŒ$‚izë²-_ð%ï1›ÁÜ×?’Jë¿$ã´Œ39ÅÒÀ!ÄAiÎs]¤‘óRdwƒ8 ¿K@qÏ‚~é0 ªwÝ€ËwóŸ–lî@¶‚#vÈgRÃEÀ&3¸gB÷ùïÙ,¨> d°æ_d¿©º jÊ £Ï\9C<‹Ç®r²aëa¾ëÞ£ù‰YS=_-za}üZ ÿnðµ)~ÐßmçàXÒ:ã²/Þ|}Þ|¦øADXHg…®î8hçÍë¦)›ÈçŽ^3ßè{ ŸaÜë˜xèqo±øÖ=ü.=^ñ?ìvoòÿÒ.wÝfæ®#ƒNë±Ëú‹îÎ 8g3w7§/†÷"XðšâùœYx…‡É³Ã8šóS ³7éð6ìñ¥ÜðÖÍ ºxóæ°ÁÁƒ†dåø 1ÎXÈßy£¤¸8+8«²Ð+Ÿ A„ãè~kžîàÏvþ–Ó7ãoðñþ›q?Ž“[TOêDÓÒÃ?óvOÒÝ_'¡  ¥­în'MÙ xœ‹‡Ñ8¿æ÷³¹§†ßÿ¹°Hì § image/svg+xml ×%KxœíY[ÛÆ~ÿÀ*/^TÎá²»Z#H€ö%IQ /EŽ$Ƈ G+É¿>gx)R»k¤vëÔ2¼+žsæv¾ï\†{ÿÝiŸ9Oª¬R?, Gå±NÒ|û°øÇ/ß»ráT&Ê“(Ó¹zXäzñÝã×_ÝÿÉu¿–*2*qŽ©Ù9?æïª8*”ófgL±ò¼ãñˆÒVˆt¹õî×…¡0¸zÚ~ý•ã8°v^­’øaÑŽ)eVÛ&±§2µW¹©<‚ˆ·ØÇûØî }R±Þïu^ÕCóꛡu™lzs»¥#«­H†¦¥.X¸Õ97Ñɽ ûœK1Æè¦¯4[UàÙþ÷öUúPÆjÊ•ñÞþò¶Wº%&ÎÓ9v´îÈÛy´WUŪò:y3Á1MÌîaAqó¸Sévg.ÏO©:þEŸØÁŽhH„üò­µº0†4’4yXÀaeûÔ.¹ê 1 )‚y’HÈ0f„-ŠIèbâ’vÒDÇvû0a‘¥Æ¨ÒÝé2}¯£ õí×P§B—ÆÝ¤™j†y;½WÞY¥•ν·êIeº°\òŠÔ€$*—Æ:ÿ7L®P‘ߘS(æµçNûhÕ÷‰ÚTµ]ã ûHŽ×(ûƒÙí%ÖÃÓuTµÈ8NmË™.ßlêO§Yë2Qe§õg¬ÓvjÎMwów›¶÷ø†Aµ‹}*L´ïµÞƒœ#‰©ʉ>¾Ä0e˜ÌhaM‰BJN&JÀú`±qyj ŽŠÓtü¡,­Eœ¾þÕOTíôq[ZGšò &cigr[Ò“NÞšt@0æ·llXÜÒŸÑí£SºOß+ØåÔöC÷o¢ìˆÛ^©¹²Sñ;U®uT&Wk¿ÒDU7M ææeóWY>·²^ÿªbóìîë9` ÈO` )êeëj¯µÙ½|äWm›éu”-6©ª”Û4w.|(2µ1óš²áïœj­±<%hM‘! ‹û‹W;ÊqÌÙÖœÓÙ ½ÔFŸ•„¿Õ¾°õ§n$äEÜÊl$!*¸/£íy^›À1Ô5ÑmɪÒu¦Æ¾„ äH“k±õ~;Âî9Kse";_ÛAÅ× #¾“Õ‘ÞåvošÜÅ^™(‰L4HõÈï½ ½Çê§·ß?v+ÜÇñꟺ|ׯè8Ö$Zë@»x¼Èï“xÝÂ>2éRí4þ ÍÁ½wQŒ­-vƒy›™KÕ4³-XïS;ÊûÙ¤Yö£]¦;÷`ÚÔdj ½÷Ú3tgô†‡¼÷:'4ÛkBfÑZAXüÍ&ygš,·¥>{ˆÀ¶,îSFyea…¯ôªoðÒ…>X2ÿ®Ga;f²äÁòÁ4àÒ2=½òéSp.±ý×>úl }Zˆ õ‰àK*¡Ý!³» jƒ…º¥|2u;¨6!Ø#ñtK…¡”4;jžˆô—$@Œr.ùÒ…Ž ù”~7\–´io4ýÀÛýJn¬lª´EŽ!ø‹«•9Ûl[Œùš±l•Õ¡þæv ŒxÈ$ HømeJýN­Ú>ãVДj0ÄôÏœ±Nn6µäÉPø+äÚ±X«Ê *®YñN–DP_Ë2:7»HõfS)³ê7p9DAwëîjÕ({²*TÝêÚ €Ößê#ÚÏN‰œ:®D¸þHåKÊ¢µüÇÇ(¨­]ÏfÁ·ØHÉ&ª¾3Ò98ÅèÒ…é)2‡RòU NŸ¡ nl$8Æ…/¯½:ê3$¹”BÝ à2`2¼Öu6ó!#Éà:½õWÔV/Å{ìfe0åúG@R)ç½áç€dœ–q¦®±´pA I'PÚ+Zid¢<Õ#ùÍ è' (Á|)éçàz—ÚØ¦ÄåÐ‹ÌøÜ…l…°„‹ó,\|Bp0 p& _Ìè.M‚ê£@ ›cñAöAÝÐiPS&}äÊâIå,¤¯§ÁïàsóràE:3„¿òù¨tt:w­EóûÞþå ~ÿ3*ÂâåxœíYYãÆ~7àÿ@Ð/;°Øì›My4‚…‘ñKâ @^ ŠlIôRllÍHûëSÍK¤(ÍŽ‘¬‘E–‹Ù!«ªªúêèžÇOûÂ{ÖU›rå„}O—©Éòr»òÿñËOò½Ú&e–¦Ô+¿4þOß~óX?o¿ýÆó<^ÖË,]ù;kË0<«™jfi¨ ½×¥­C‚HèäÓ‹|ZéÄæÏ:5û½)ëfhY7–®²Í þòò‚^X#Eâ81 ) @"¨Ï¥MNÁÕXØç­±co$úF±e Æ9ÀÏ ßPmŽUª70P£RÛðý/ïf€Qf³ñLeƒM^èvL¸3{žu^›2|¯ŸuaQá!·@I*æ©)-r«Ñ¡¼3ß);€·by›{î¹OŽý˜éMÝȵöpŸÔ÷–9hå¶—9;D×IÝùÇóÉ]˜jå·ižž³6U¦«ž'›gÊ3àòÜžÛhìçï7í&ðz—dæ1ã~4f+DX„©˜ñS‡H$elÎv»’HÅ"ŠÔŒ ®>:ïÇ2·O‡Ó|‚cU9‰"9kÐ 8êeêyÙVζ:êÙÈ—¼‚ú$¦sÕ;‘>ÆüžŒSóïü oŸœò}þQÃ.ÉLÆi06ÿ&).ˆ¸o“+.8tµ6I•] lìrÌ3]ß±L]&‡`½v“ïXÁ!±»{4¥yà ζ:ØçÙÁä¥ý´ø›$_[Ù¬Ó©}u÷Ͱd)…DõiézoŒÝ}Zå7m[˜uRL%6¹¨TÛ¼ ¬9Œð4bzcosª¿·Xkc­‹à9@ˆ¼ƒ!:+cÛ$tÜæš&wl§V<ºø~4 ضÊOï Ø Š#ÎâvÿºOÁÐÛŘPA$_PE'³‡‹ûF õK 2ñîvâ©F„`ûò|Kű’”´;j¿ˆ !F9W|0 Œð‡ñ‚°¤K’“éGÖV Rí«+‰ ‘HøW#j{v‘Ø5$Kò´nŲ„“Bóô ŒxÌHüCm+óA/»®ãŽÐvÄôÜœ±žî"6µ”Ù˜ødæ)à««ê³]òž–%P«*9·»QÍfSk»6pQâ@Κ^lÙ2=§9jt}mðÖψÅîY€–H‚Ô ÂÍ£t-(—ˆ6ô?{£¨‘öþu=›s¾óRlƺ(S‚Q¬©è§ž{¬ô$quÎRÄ l(c)<Ó ¾ …»cÿ³-]”›±\¼iÅM ÆÃ7Œör]|…oßk÷Wà·+ÃT`6 Žq)ÔµÕ€GCŠ+5³(ÐI®"¦âk^ß—3IE×ém8Öv|%o¡Çmvthøœž„TÊù ø%x2Í«´Ð×¾tî‚’ŠÎ\éÎs}¤‘óÔŒäwƒ8¢H@I&”¢_ºFÕ»iÀõ»€‹‡[6 [!¬b9 Æ]lBp4sø™P$…¼Á»¸lTŸÅe°9–_Ë~W7tÕ”™>såŒñ,ûÊÉÇ­‡ûnzöÌÛêùìÐ{(ïã×Røß€Áצø•¦øn;Ç’Œ·±xÁú¼øL-ð+=á1º¦’ ›×MW6‘Ž^3l =8ŸcÝë¸@2f’Ý,¾MÄ¢?¤pàUÿð{ƒ#ÿ/ír63¸NÖë7áÎФä3¸»ÓÇH2Ææg…Q€Í°<ï–büpunG—OÛáÚix›$óÛ §ÐKMq@z÷½Ñ{ôˆ¥ä÷ûs˜~tÃé’$ä@Š%§dÁ1$ÉH*æ¥6’ˆâxQDh{B ‚•l(LHîá'ÏÞÇñØœŸJ˜ƒIGwP­Z ñ4Ù<]e«Æ§Â[WeýmnwU6|:[Á››²#Báëõ›²éYïæí^)Ée¬Ä" ” a$š\xÍ+{skÃÈ‚B4Ã1ìK"BPLÜýQ1:<5SG ÷'3A¢˜5cŠ¥`”z…§:@u›ðÜu $1Yô/×nèÆ¹ ®.y&¦§ŒJ_#Îq€—q,ÌÑßþ~t·Äðû߃»Š`"éxœíÛŽÛ¸õ}ýAyÉ –Ä›(Ê;žŠ`ÑíK»E¾²DÛÚÈ¢+Ñc;_ßCêjɞ̶MÚ`ã 3Ò¹ððÜéyüñ¼/œgYÕ¹*W.ö‘ëÈ2UY^nWîß~þÉ®Së¤Ì’B•rå–Êýñéûïëçí÷ß9Žìe½ÌÒ•»Óú° ‚ñ*|Umƒ, d!÷²Ôu€}¸#út O+™èüY¦j¿WemYËú͘ºÊ6=ùétòOÔRá8ŽDB< ðêK©“³7á…}Þâ%¡p#ÒW’-k0Îþ÷ôÀ¯Õ±Jå¥_J¼ûù]ôŸél¼N^~¨Óä ¯ävÀÆ É^Ö‡$•uÐÁ›Ny¦w+— æu'óíNïϹ<ý^W.rú$Æœs6<µTƒÓqɳ• ÊŠö­¹G‡Oœ·RðTDHDñÂ!ˆ`a‡ '=ÖZíîNïe¦R£ÇÊMw2ý°Vç÷öAfïóVÖ~oÞ^ <T¥½M^Ȇ5Ø©½ .2¯U¼“ϲPXÁ!×I*ä©*ß¹–þ¡¼³Þ9;€Ób~{é°Oý˜ÉMm鳘Wâ:Aƒì•3ÛËŒ¹G¤ë¤nÝä8‡d ]¨jå¾ÙØO‡Y«*“U‡ãösSàù\_š¤ìÖï6mî Ђz—dêq1Ã~Tj¿rò؉ˆÏð)Oì³(BQˆçXùLà˜òùÚàð£qŽw,s Yu8Ï8V•¡(’‹õí¯^L½S§me,©«£œñžò”òÚÀ1™ëÞ’tib÷hL’ÜÃ]^Àí“s¾Ï?JØåÜ>Fƒ±ý7I1„Ä}«Ø`±¹Q­UReFk—cžÉz‚ä–ÉÁ[¯Mæß4Ay‡Dïê—JeEÜ$è%x2ÛJoŸg•—úÓ䯢|I²Zÿ"Sýâîí ÊBÅú4u½WJï>­ò«¶¿-Ô:)®)6¹†X©¶yéiuÔQȾ©š¾…Z+­MÏ#ÔÆÈKaÐ'h¥t¢meGMÕƒÒ6,)Ør:޾˜>u¾ ÛCMŽH±(÷Ó³ìü p 3ùæÎBA)™a/·±è*§é`Ú\¯ y­%l LšMÁÆE-‡Ùs‘—ºIq™Ò)0z^ŽëB³õ kÁ¼4ˆ½ÔI–èdÔ:PØ[æ•å_ÞýôÔIxLÓåßUõ¡—è8†$Y«#øß}àYº„ cŸè§|ÃL'¿ƒâ1×ÔÆw£u›•+Ù +7Ƕ,Ýç†+ø«Î‹âFL§÷hÙ\òÉŠm{]‚V™NÙ`¬ícÐY£yÝN£³HÖ’èO¦'8óÚº­Ôñ°‡|mÛ†;²óuÑURÖÆ"ÆÃðX$Z¾E † ¦>ôîØ^‡´`ÑàûÑ2`Û*?¿…v1/ù×¾†tC^Œ0 1g "ˆÏ0AôapßHP' ÚëØ°Û+OYŒÂؽÏ·„ý8œàfGÍáG>%Œ ¶ð(¦~H(fc ÒÉ«åGÖî%y©4…ÕôDêã(t'µ¾˜LlG’%þ†·bY‘Á>yù,¦‚D8þ¡Ö•ú —í\„P h:;¢†oFi7 ›ZB”Ùø Tæk(„¯¬ hÐzÉ:X–@;®ªäÒìjU›M-õ²ßÀ Ä!šïÙilÙ £5št=5xëÏ }›Ï´ô9è@OøÈ~„ô¢aÜ'þ'D~d©LW3Î7¾‚ÎPý ¥J0ŠV•#Õs¢•¼*\­súRycÚX Ÿë¤¾ wyÿ³- ÊÍP¦`Þ´âú Œ‡ní¥ÈõLè2Xà[øÚ𺿿M SÙB(p”ñPL­8R_0!f=›#‡Ãñ× æ4„Š$¢iyëÏ·-^ð[Ñc6 §Ð/áI(¥Œõ„_ƒ'Ó¼J 9õ¥q¤dæJs¤ë2 ÏgËÉî&qD¾HBq A¾v7Œº·Àå[…·lîAµò‘ˆù,A¬»0Ø£hæ.ð3&>ù Üà²YR}—AÁfˆE.ûUÓÐyÔSf>úÌ3F³|ì:'æÝÎÍOÄšîùìx0{çã·VøßƒoCñ CñÝqŽ%m0ÞŽÅ!Öç#Àg_˜0‹É¬ÑÙˆƒvtÞ7MÛôCÎáè5‹~ç3Ä¢{3 }SNo6_;Ñè‹Ì@!xÅÿqؽ‘¿I»Ü ›Y¸^ELZ/‡lx3Ü™ð#ÎÙ,ÜÍé‹!ŸSJçg…Q‚Íby>-ÅèarlG—OÛþÚ©º*æ·ƒœÂ,u¸sÿØçA˜óÿzöËn8M‘„Hg/‚"qAÔÁ0æ/aÅNú n!4äÌA 4.žã±9?U0{“¾Ê\„Nâ;æzÃ6áš“‰Å¦9Rÿó˜Tr–$ÖŽÿ³$±ŽÀ>%X@†,H QÌÀø!&<"`lx†“)¦€2>ÙGhl0ˆCqvsQhø…Ó2àž8eeÎnÄÚ¦ ¸#4WØ1¤á„Ñn$µÛ¶Óqt„tØñúèìaE8.Ú‹øvæFÞî3¨xÍNš‹z ÄX <˜‡¸U>´Ql´·¬F¨¥ïxF#Ò*FqÜØÝr²Æî–„-ºe®A k€Nr»€1|#Ùëv8eoeŽCöï¤ÑUú¥Qg; ÝŸ-€Ì×Ý£kÍþ/,*£ºu6_€‡Âž±¨»b ‘ Þ ÕõW4¦<š/Hà÷¿2¥ùeŒ#±xœíYYÛÈ~_`ÿ!¿x±Ù7›òÈ $Æb$/YòPdKâšb dkFr°ÿ}«Iñ)ÍŒ{#Ö`fȪ꣪¾:ºuÿÃq—{º¬2S,gá™§‹Ä¤Y±YÎþþþG_Í¼ÊÆEç¦ÐËYaf?¼ýþ»ûêaóýwžçÁð¢Z¤Ér¶µv¿‚ý¡Ì‘)7Aš:×;]Ø* ˆ³|ÒË'¥Žmö ³Û™¢ª‡Õ«¡t™®;ñÇÇGôÈj)EQ€i@©~u*l|ôGcaŸ×ÆRŒq¼è Åg¿|K@•9”‰^Ã@ mƒwïßuL£Ô¦Ãy²âC•Ä{}±nKlÌïtµ]-½™à1Kív9£¸yÝêl³µýûC¦ÿhŽËö°'ˆ”’÷Og©Þ餡dérʪóÛyÉňx¯Ù*¡Z¶sbŠ}ù8œ{É¡²fw׌nõ^¤&qz,g«2.’­Ÿä¦Ò)êŒÚ-£{SZåºlÍN'U¦Þ雽ƒS°Ï,PâÒYbŠå™Õh_ܘï˜îÁU‘¼Î=µÜ·Ž}ŸêuUË5Æp¯´a³²§\Ÿ¹žgOÎuVmTÕ¬£»qÉ¡,a£~brSúU²…0h§ñ<ô'Gþ¹¦úïaïßÝh¯²xµ¦îçMKÿµ}¸ê]|ž-ùìóoê>pFk; 8¦•Û®âJ·[ÜÇ]¯±œ½Zן–³2eªË–'ëÏ%Ï@€döÔä®vþÖËnâNߨ¶qj!|&ÜÆì€.P(çrÂO ÆE!WT‘)ÖŒP6 qqphöEf!ùìÓ Î>Ëã“õ7JuÓT[ó¸)myГ‘Y*ùçf»ì£†]N­ã4Zç= nÛ¤†ÊV't¹2q™ŽÖv9d©®nX¦*⽿Z¹ìx•ïXþ>¶Û[Ô…yÁ ¾N7ÚßeéÞd…}^üE’O­lV¿èÄ>¹ûzXR:ˆBV^ºÚc·Ï«ü¢íor³ŠóK‰uf*å&+|kö< ¹^Û뜲Áï5ÖÊXëx Ð"OÁ ‹ÎÒØØÖÕw… Ÿ"p”}'G¼Ì½Ž…¼'êÝÞÕõºÇR=ùLsᆨäB1F'ÜÓun ºêq4¸V ÊV¹¾Ô6PÄ@MÇdç¢ó·ç<+4ÔÞü4–3`ô¬¦…–V§/8—‚ih;mã4¶ñ ´$ÑYzºÅßÞýØU¨û$YüÔú²ä9‘xeàÿ¾’¹2ž,  ÛÅöm¶ƒ|á:¸?@Óµ©c\J;ß æmf.uÓÐ]mmÓd—¹QÁÏ6Ëó?»eZ½ÓfÊb½lóØW̳2­²ÁPÛû µFóº£3W‚è/®.xÓÔº)Ía¿ƒx]ÎêÒ1ع&tC,ôd•³ˆó0<æ±Õ¯ñ܇F…X1q×¹cs iÅÃÞ÷ƒiÀ¶ev| µVPrͱû9¿ 6‡F8„ "ùœ*Š8¡˜Ýõî,Ô.%È…w7žªEÑì‚<ÝAQ¤$%ÍŽš7¢Äœ„ˆQΟûŒ0$(#ün¸ ,é’äÅôkw+ù‰v‰Õ•D†H(f£u“´œû‘y­n¾(àXU?ù-#1EC½©li>èŹ)ÂøLh ;â(œ±–î"6µéø dæK*ÀW—9Ôg»à--¡—e|jv5 šõºÒvÑm WbCÎ÷ëVlÑ0=§9jt56xë¯tT,rŸ9h‰$è@=_!\”öÃ9åÑšþ“'0 kiïŸãÙœóo”bV×E™Œb¡ë…~ê!¶‡R_$®³sºTqãÊXŸË ¾…›cÿ»-õÊMX.a^µâ€&ãá+F{ ¹¾ƒ.‡ ¾Á·†ïØý%ømd˜Ì& Á1.…[íäÎ )®ÔÄ¢P@C$¹ ™ŠÆ¼¶/g2’ Çé­»8óû“Á=n³*œbý xR)çà×àÉ$+“\}Y÷X$áD76›;ϵ‘F&Ìc=’ß âþ.%™PŠ~ínTïºׯ}.î®Ù܇l…°Šä$@jw° ÁáÄ]e}p—B^áõ.›Õq$lŽåWä²Oꆎƒš2ñÑ®œžÄc[9ù°õpïuïÑüż©žž½‡ò>~+…Ÿßšâ'šâ›íKÎ`¼ŽÅëÓà µÀOô@„GtRèêH‚vlZ7]ÙDBJ8zM°Ñõ@à|Žyx«âɈIvµøÖ= —HÀWýÃîŽü¿´ËMØLàz8è´ž†¬¸ w®P(%ŸÀݾ8F’16=+ l‚åi·á»ÑmT°\>mºk§îé"™_88^ê¤uÿЭçAHJA>ÝŸÝôƒN—$!R,9%sŽ!I†R1/ñ´y”„GsŒBBÃȬdMaBrÏñ0y¶>ކæ|.av&Þ†=})7¼uóÅ E_½zóE„œÍ›û Š¨ðæåØs+_®=¤ßX\¢ŒÈ¹Ï#(ÖT‰»çnÞÔøäyíâ ÎÆpÌ¥ç«@…Cb%æ¾Dœ²ÈqB‰86’ã›·Û)ªÁ$eTÒè6 ,ÁÉ$ù¬ÖNrO ÏÏ‚ÕKŒ©hRkÛÓ;œN Ü£ÛyEŒ6)íî‹G¢Hñé9ÕÝ?F!À«Ý^ Ì9PkñНÅJÒqL7_=ìŸnÑ›vª#›xÇ„ÏÝæA-N=‚)¡šC¤3E¸ç_&I²?¹MÖ¸ÑÜ|b–üÏÂþù¨WPN”ký|Á“Ra~×â–¿w_bÀÿßÌ îxœíY[£È~_iÿb_¦SÔ¼í^)­)yI6Š”—CÙfSNQnÛóësŠ›1`O¯’e”q«»Í9§.çþUñüÓy_8¯RW¹*W.AØud™ª,/·+÷o¿üìE®S™¤Ì’B•rå–Êýéåûïž«×í÷ß9ŽÃËj™¥+wgÌaéû‡£.Ò[?K}YȽ,MåD|w Ÿ^åS-“¿ÊTí÷ª¬ê¡eõÃPZg›^üt:¡«¥HÇ>¦>¥HxÕ¥4ÉÙ…}Î¥cxÑ7Š-+0Î~{ùŽ€*uÔ©ÜÀ@‰Jiü÷¿¼ï™F™É†óäå‡*Mòfݎؘ!ÙËꤲò;z3Á)ÏÌnåRÜ<îd¾Ý™ëók.O¿Wç•‹ìˆÆDÁ¯ßZ©«ÓICɳ• ÊFíS»ärˆ:ïd$Ò(ÄQ/Š)ñ0ñH°pÒceÔþ©Ýé½ÌTjõX¹™:•‰Öê„zƒöKÈóAiãmòB6ÂþNí¥‘y¥Jÿ½|•…:ØPò¹J¢Ÿ§ªüg‘‰åùÎÙÜ‹yî¥ã¾Xös&7U-×Â>R×ñf¯ŽÝ^f <]'UëÇ9$[åBé•ûæþtœµÒ™ÔOÔŸ[ž_çæÒ¤a7·i;q/€ïT»Ì ‘0á~Tj¿ryˆbFã);…h!1Gá –ŒQÅÇñ„ >ZßxÇ27F‡ót‚£ÖV¢H.´¯ÿ‘NªÚ©ÓV[C}”“±§¼¼6æIL§ª·"]Œù=›÷x—¼}rÎ÷ùG »$«ÁÐü›¤¸FÄ}«Ô±²“é©×*ÑÙh`m—cžÉêŽeª29xëµÍôY¾ey‡ÄìîMP ”ê +x2ÛJoŸg•—æÓâo’|´²Zÿ*Sóp÷õ°”'… õiéj¯”Ù}Zå7m[¨uRÜJlr¡¢·yéuÄÓ€QÈ™çè&~çXkeŒÍài€Ö!ò( úüÔÊ$¦®ä¸©yPØ®Ó@¶#Ç\l_:_,Ñí©6E-%ù•(÷Û£j¼]É-ͦ¢‚ct½Ìs3ÐU޳Á¶µ*_òVKØ@™5“­‹ÚvÏE^Jè%Åe,§Àèy9, ­.]ð§ aì¥I²Ä$ƒ~Б‚ÞÊ€O–yÿóK·Âsš.ÿ®ô‡~EDZ"ÉZÁÿîË•þœ¥K@ûļä{¨üijeÜJ[ß æmfÖ²'³0-K÷¹åÿÕäEñG»L§÷`ÚÜò¥^¶ùÚëâ·ÊtÊúCmŸýÎÍãvE²–D²-Á™–Ö­VÇÃòµíîÀηmÄ褬¬E¬‡ák‘ù/<=Ð žzwloC:âáÕ÷ƒiÀ¶:?¿ƒfPr/°ýi¶PcB"ø‚FqB1{ººo°P·T@n¼»½ñT-Bp»7äé–ŠãHPÒì¨y"Q° !b”óˆ/·I= wÇþg[º*7aÙ‚9kÅ-@Œ‡gŒö(r=º&ø¾uøŽÝ¯Áo#Ãh0[ŽqDc« E<Š&…"Á£Eñ˜×ár@EŠÂqyëϳ-?sÑc7 §Î/áI(¥œ÷‚_ƒ'Ó\§…ûÒº RHDtâJ{¢ë2L˜çz$¿›Ä!ý" %XEôkwà{×\¾óxð4gsªÂpÊž$Hí.6!8œ¸ üL(˜á]]6IªÏâ2(Ø‹¯Èe¿ =eâ£ÏÜ9c<ÉÇ®sò!ô°Ï5öhþbÞtÏWÇì9¿µÂÿF|Å@ñ]8Ç’6çcñëSð™ ð DxL'®Æ@´cÓ¾iÛ& „€£×$6z Î瘇÷0ˆ™`³Í·Æ@,ü"(€oô?vopäÿ¥]î†Í$\o"Öã fÃG(‚OÂÝž¾8F‚16=+ lËS´ã§Ñm”¿\>mûk§þÛM1Ÿpp Xê6Hçþ¡7:σ±ùíþì§ÜpÚ" 5bÁ)Yp E2sR‡Ì£$¤8^`ÆN ‚#QSX ¸ƒxX<;ÇCs~ª`ö&™«+œÍpeíŽmÙXo‚µ LØ÷á8Wª-'ÉRÛóíÆ½Õ¾†¸ÁàÞ¤W£jޟ̱jÈ ÀÙ^?c:'`£˜ÆŒÇÁMì»:Fë÷ªs|j/¸… v 2#è-±¸!c1·KY image/svg+xml – txœíYYãÆ~7àÿÀp_v±Ù›lÊš1, H^lòPdK¢—bdk$í¯w5ïCš#YÇ‹¬;#VU_õÕñ5góíå˜Yϲ¬R•?ÚaÛ’y¬’4ß?Úÿøù;GØV¥£<‰2•ËG;Wö·O_µù“ãX-e¤ebS}°~ÈßWqTHëíAëbíºçó¥­©rï>XŽCapõ¼ÿú+˲`í¼Z'ñ£ÝŽ)NeVÛ&±+3y”¹®\‚ˆkìãÁ>6;HŸe¬ŽG•Wõмz3¶.“]on¶tfµ ÃÐÅÔ¥Ô §ºæ:º8³±°Ï[c)ÆØÝÈô•fë <[ÀÿÞ¾ JÊXî` D¹ÔßõJ£D'ãy:ÇNÖx;޲*¢XVn'o&8§‰><Ú7™îzx~Nåù/êòhc [Ñø¾ï ßZ«!bH#I“G+Ú§vÉuoˆQHÌ“D¾cFØÊ¢˜„&i'펻NTl¶ÿhÇ™ª$ê}ØÏ*/…*µ³K3Ùºu”îU¦•ÊÝwòYfª0Ñã©ITj7Uþï,Õùù.IÈ„þmíµÓ>õ&‘»ª¶kÎn©m¹²?ŠÙ^b|:2ÝFU‹…eÑ¢7Så£ýfW:ÍV•‰,;_¦:ð¦úÚ¤m7·i3qo€ïT‡(Qg¡ý ÔÑŒó<Á‰Gù †q<ð瘑¥Võ¢þrvødàqNyª!yŠËr‚SY‹,ºJp@ý«_¦:¨ó¾4¾ÔåI.ÆžÓŽå´‘NBº<}kÒE?ÁØ»gcrážîú‚î]ÒcúAÂ.—þ1'#°‹²!(î{¥—ƒŒßËr«¢2™ ¬ýrJYÍúyT8Û­Ið›®3*§ˆô¡zÉ Wõ7 ú™ì¥sL“B¥¹þ¸ù«,_ZYm‘±~q÷õ°T%0…Âôqëꨔ>|üȯÚþ>SÛ(›ZìR ±RîÓÜѪÔH‘ɾ­)›¾¥Ú*­ë,^쥎‘q<5›Á²«eYúj:Íåj„v/5ég$aà By,Lשéƒĭ̤¢¾Çct¡½ÞÖ&p 9tÓ¨ªt›É©/ayÒd.6ÞoG˜=gi.¡Ud×¹¦ù8å;Yê]}w—¾Q¥Ž’HG£r߉xïe`ëß}÷Ô­°‰ãõ?Uù¾_ѲŒI´U'€Ö~ä›$^G8Fú)=B-0üâÏ@ 6î ˜ZìFó63—²¡7‰WS3ÊýI§YöƒY¦;÷hÚTgr$ݸíº3ºãCnÜÎ Íã~Y´•3UÞZVË}©NÅ2°möȽÓΠË(¯Œ# °ð5†ú¯`/(À‚ñ‡…ý4’… ¦—–éå-´PNqà±p…Í¿ö‘³°³ʉﭨ’C(fj£…º¥8™€ºŸT›ÌC{"^n‰ 0>%ÍŽš'"øŠˆQhÜÞÊž…8eÄ{/Kš²7™~äí~%'–¦Tš.Ç ¸=Qé«IÀ–f¬É7@Ȳu…ú›Ó)€F„LЀ„ßTºTïåºå:·‚¦Wƒ!€5{Œur“¨°©5@žŒ…¿@­J!je™AËÕk¯“%4زŒ®Í®FRµÛUR¯û ‡("¨âNͰÖÒ2§± ªBÛ­æN´þnQŽXh>+8%òá ÔrÂõGH'XQÏG´–oqŒ‚ÚÚú×|6¾ÁF¶PõÔHåà­JHÒs¤O¥œÔ«œ¾BAÞ˜|†ÆÃgšË·CáîØÿlKÃá*S'ozq$ãpøn%K§½¹Ž ]&ø¾uøÎá/·™cJp‡Ç<Ÿ‹¹×@G9CÂbáQè›ò=0ÎuÕf*’æå­¿˜¶záߊ³Y,cý ¥ÔózÃÏÉ8-ãLα4pA ù‚. 4—´.ÓÈBy©Gzw“8 ¿KBùŒ A?wFÝ»TÚÇ.rÃçT+„Eè/¤†‹€Op΄"Ÿû7td‹¤ú$AÁö°ÿAö›ØÐeÔS}âÎâE>vÓSó\sæ'öšîùl9À=„õáK+üo„ÁRü)¾KçàZÒãíXb}I>~/¤‹FWs NÇ–}Ó´MÄ}®^‹Øè9y±Š½àò8òC泛ͷæ@,ø]8‡ ¯ø‡Ý+€ü¿ôËݰY„ë$â€i½²üf¸{¾ï-ÂÝܾ<Œ|ÆØò®0J°E,/ÙRˆf/¡ÜýèåÓ¾íÔ›ó6jèßx;¾õiòä±Ç˜·¼=i½À¸†õ·cÜã9zÙi 'AÌüÑ‚C×Â4àÜ2ÜŽQÎùªù6òYOïè¨`~¬TöΜ:êÕ¶_Ô£³Ý3Ô (ëwï!i6-h»ýQ8üѱ<5cö¯­,äãÇ¡Lÿ»rr+”F­©¾ ¢ú½<ÙÖבÐCŒÃ5y› 'b‚Pâ¯ËMVnÌËjøý+ ik"ñxœíÛŽã¶õ=@þAU^vPK")J"ñhA4/iŠ} d‰¶••EU¢Çö~}©»dÏNÚnÚEÖ‹‘Î…çðÜIÏã7—cn=‹ªÎd±±±‹lK‰L³b¿±ÿöÓ·³­ZÅEç²»ö7O_~ñøDZþ\‰X‰Ô:gê`}_¼«“¸Ö›ƒRåÚóÎç³›µ@WV{ïÁr`æúyÿå–eì¢^§ÉÆnyÊS•Ú4ñD.Ž¢Pµ‡]ìÙ#úd O´Ù³Häñ(‹Ú°õWcê*ÝõäZ¥³o¨0çÜCÄ#Ä §¾*¾83^Ðó/Ay€‘¾’l]ƒeKøßÓw·–§*;`n!”÷ö§·=ÒAnªÒñ:a'r'Ö.⣨Ë8µ×Á›ÎYª› æõ ²ýA ïÏ™8ÿI^66²¸„ã0 éðÔR ƒH–nlØ,kßZ‘ëž¹œ¸°N‡Œ'>öWA˜;;¸]´Ûî:•‰Vc'‘¼ÛÊËÏæA¤noÎ^€¸”²RÎ.ËEÃãäQxW‘Õ²ðÞŠg‘ËR’Wf q¥¼,‘ÅÏy¦„[wÖ»¤%8‰‡·±×û¤Ñ©ØÕ†®1ƒ~%¶å5È~WZ½T›wDºëÖ-–UÆ{ä\Vû«ùt˜­¬RQu¸Ð|¦8 žÎÔµÉànýNi½pO€îÔ‡8•gˆƒö½”ÇM‘pÊ¢pO X¸K£E^bAfäR†¹.×OŸ´sœS‘)È¢ò²\àTUš"¯¶o~õbêƒ<ï+mIUÄ‚÷œ°)§ yÌÉrï-I—!zF'Å=ÜõÜ1¾dÇì½-—öÑ;ÛçCHÜ·Š “ÕVÆU:c4v9e©¨gˆAn—Îv«3ý¦é4Ê)cu¨_"(¤q“ —àˆt/œc––2+Ô‡É_Eù’d¹ýE$êEíÍ ÊB…ú0u}”R>¼åW©¿Ïå6Χ»LA¬Tû¬p”,G5Bäb§ncª&€o¡¶R)ÃË512ƒ§†âq €ìj¹,K]u˹\5Ðî¡:ý4„GtŠc©Û™#Øna:•\Ò€ù>Y`¯·±)lCÌ#]w¬:ÛæbjKP ˆšÎÁÚú-‡Ö9Ï "¿Îé$Ø3+Æ)ßÁLªwÕÝ[–÷q*NcŠ} z+Ãè±þñí·O„Ç$Yÿ]Vïz‰–¥Iâ­¦c R׿Éò#k÷’œDèš©Ûïâ(°gµºêLl§5þæ²|]ÀÑÁ<9¹”ûŒD˜]«J¾ëväA¨4MQs4õý®3”ZCéø Ý)ÂWT9ô^µ¦,¡ÓVU|m´AånW µî6QÆPÎ3h­¤¥wcAy…þ[ÏÞúÁ"ësýYÁ.Ýö@,‡¹È|˜p¢¡¡K ü;+@nd¨­ÌWÓÎ×¾aÌ_ úI`%+¦¥çX*1)\­súRy£:TŸiRß…»¼ÿ™JÃæ(]0oZq àDñœS–F{)rºø¾&|çî¯Ào3ÃT`¶ œOÀͭ8ø.£Œ-,zÑÓçŸñ9®›¹ý*‹æå­?ª¶xÞŠ­,‹–±þ< ¥”ÒžðSðd’UI.æ¾Ôî‚ Y¸RŸÖºLà äÅpÒ»I‘ß$¡B?`Œ|ênuïJ*=”8f‘6w Z¹ˆñp‘ Æ]l‚Q´pø7 ¸Áe‹¤ú(.ƒ‚MQø ¹ìWMC—QOYøè#wNŽùØuN:=ô»™=šŸˆ6ÝóÙr`ö`Öûϭ𿟇â†â»ãKÚ`¼‹C¬/G€4¿0aÊɢљ(„Ýù˾©Û¦„!½±ÑÏ@à|Šhto¢r?ôo6_3ùÑo2pàeÿÇa÷ Gþ.ír7lá:‰8˜´^Ùàf¸SæFaHá®O_¹¡ïû˳Â(Á±¼œ–8z˜ÝFyûÑåÓ¾¿vêŸ&Åüv€ƒ˜¥¦q€;÷½ÑyöÁÃ0À¿ÞŸýò£N]$¡R‚WA‘ŒBæ[‰…aÌ#8"ˆ¯aq+\ŒXh ~R ­Ð¸xv>æcs~¨`ö&}•¹è.؆䎹:ìÔbó©ÿyŠ+±HcÇÿY’G`×'˜A†¬‡Ž|LÁø“0"`lx†“)ö> ~@ÃÌ0Q¬woXµPCß pfŒZ¤Ù@qÞØÝpÒÆî†„®ºe¦ †¿5@'¹]@¾‘ìtÎYù­,Â< ÿNMÐï,:Û0øíþÜ ¿É]köŒ`PÕ­‹þn;`æŒ5@õØÅ}ˆlðÞ­¦_Ñè𨿠ßÿ_ïûZU&xœíXm£Fþ)ÿ#_vÓt7MÓ8ž‰t·Šéò%Éé¤ûahÛd1 =¶÷×_5o·=;Q²Ñ­nÍ TU¿U=UõÀêÛÓ¾pžeÝäª|t ®#ËTey¹}tÿõËwžpF'e–ª”n©ÜoŸ¾übõ7ÏsþQËDËÌ9æzçüP¾kÒ¤’ΛÖÕÒ÷Ç#Ê{!RõÖp<†ÂàæyûåŽãÀÚe³ÌÒG·SꢵÍR_r/KÝøߨ§ûÔì –©ÚïUÙ´CËæ«©umFs³¥cÐZ‘8Ž}L}J=°ðšs©““w5öyk,Åû ›˜¾ÒlÙ€g+øíjÔ¡NåJTJí¿ýåí¨ô0Êt6gpìlÝ™·Ëd/›*Ieãòn‚cžéÝ£Kq÷¸“ùv§/ÏϹ<þ]]ì`'D4&œsv¹ë­.ˆ!$Ï]8¬èŸú%—£!F1E0O–p§ Å$ö0ñH?épÜe¦R³ýG7ÝÉôÝZ~mod†FwŽ ÈS¥jímòBvcüÚKÿ,óF•þ[ù, U ùU®A’ÔÚÏSUþZäZ¢ª¼3ß)« H1¿­=Ú'£^erÓ´vÌ#u¿Sާ2ÛËŒ{'¦ë¤éÃâ8U² ª~t¿Ú´× Y«:“õ ãí5×)ˆt®Ï]ó›6øŽA³K2uXÚ÷Jíaâ…À"´ô)€…#Ž·µ°fŒ0HÀ"K ‘>˜àx‡2×EÕÉžàPׯ¢HÎŽßþ#ƒU³SÇmm<©ëƒ´Æóåõ'1µÏÞ› i@0±OØÛ˜¤¸§;¿ Û'§|Ÿ¿—°KbÙ˜Lý¿IŠ $î{¥K›õZ%uv5°õË!ÏdsÇ3M™TÞzmý¦Þ¨¼*Ñ»{´¥zÅ žÌ¶ÒÛçY¥òRØüU–/­¬Ö¿ÉT¿¸ûvXª˜Bú°u³WJï>|äWm[¨uRÌ-6¹¨ÔÛ¼ô´ª&xš( ¹Ñ·5u‡ß[ªµÒÚ¤° Ð"Swß®îb/u’%:™ÔúAŽ^æ±üéíwOà «4]þ[ÕïÆǘ$ku€ÐºOù*K—Àö‰~Ê÷P Ïø¨ÁÊ¿(æÖ&v“y»™kÙÑŽ›,K÷¹åÿ¬ó¢øÁ,3œ{2m® ùÔ.ÛÝŽgñûà ‡õ§§]ùƒ7ºÇí52‹d-!?þiª½cWÍm­ÕR±oîÄÏó¡ë¤lŒGL„á¶Êú/< 3(‚Ž>ŒáØÎ!-.ýj6 ø¶ÎOo ‘†G,ˆØüôa°ºcBCÂÙ‚ `=„âàá¾ÉBÃR!™Ew;‹TkBp»3±½%‚âXpJºuOD„ èÝeL°…Ä …4 ìaº ,iêßlú‰·Ç•¼Tššiº]€HºW#}6™Ø“%ùhY±,áÍ¡½óF,p‚ø›F×ê\öŒã^Ðõl0ÄÐhƒÜd,lj (³©ð7(ºs)ÀWÖ´^½dƒ,K ÑÖurîv5‘ªÍ¦‘z9nàrˆ*rîµ8c©‚¼1‰ *…kžÔ·¡pwìÛÒåp–ÊÌ›^œÈBx¡ø^Sl§½„\Ï@—ÁŸáÛÂ÷:ü5ÄíÊ15¸-„0Šk¯Ž†LË£Ð@#Ä™ˆ_ëÊ„P‘Dt]ÞÆ7Õ^/ø-ô˜ÍŠÈÆúGˆ$”RÆFÃO!’i^§…¼Ž¥ ¤Ô ¥yY2XÊS;’ÝMâˆþ% ŃPú©‡aÒ½k¥ )ñp‘>÷ Z!,bn%H.>!8²Âq&ñßÐ]Bf%ÕG l†ù'²ßņN“žbÅè#wÎ[ù8tN6¥æ¹åÝ_̺îùìxÀ=„óþs+ü3`ð™¿@ŠïÒ9x-éÁx‹¬Ûà#Qà8a1µ]Ë8œ.°û¦i›(ä^½,lŒ‚Ï0‹îq "<¸Ù|[D  á…WüÃîü¿ôË]ØXp!˜ÖË o qÎ,¸›·/†‚À~W˜$˜…e›-Åøáêk”¿||ÚŽŸÆ»Y1¿ p \jŽ2„!òpŽ˜óüþxŽÓO¾pš" 5bÎ(Y0 E2â"pR‡Í£$¢8^`ÅN"‚o%AÈ™ƒxZ<‡ÇSw~¨`Ž6ÛVæ³"üÿ/×+µL "KxœíY[ã¶~ÿÀ*/;ˆ%ñ&Šr<´] о¤) ä%%ÚVV ‰Ûûë{¨›eKöLÚlÐA׃™‘Î9$Ïõ#½øö¸Íѳ*«Lñ°ƒT‘è4+ÖÎ?úΕªL\¤q® õèÚùöéË/r]ô×RÅF¥è™ ú¡øP%ñN¡wcvsß?^Ö=]®ýäº0WÏë/¿@ÁÚE5O“G§³Û—y-›&¾ÊÕV¦ò‰G|g Ÿœå«Aö¬½Ýꢪ‡ÕWCé2]õâV¥«¥HE>¦>¥.H¸Õ©0ñѽ zN¥cxÑWŠÍ+°ì~{ùŽàUz_&j•W(ã¿ÿé}Ït±—št8OgØ‹u/¬]Ä[UíâDU~Go&8d©Ù<:7¯•­7æüþœ©Ã_ôñÑÁ£À£BðóS+uŽÒP²ôÑÍÊö­]rÞ b/¢Gï‚42Ja3D1‰\L\ša݆ç©Nì2N3½Ü£‹_’J>¨ô—¬€Y×›¶_Lwº4î*ËU3Úßè­òO*«tá¿WÏ*×;Tþ.ƒ ý¸4~–ÀÌyf”·+nÌwLwà°HLsO÷ɲ©ZUµ\cûJä7Ì~V½Ôšz ºŒ«ÖEíâ5u®ËGç«Uýé8K]¦ªìx¢þ\ò4x=3§&›»ù;¥íĽ¾!PmâT &FÜZoy2ñaÉOp­BãåÀÑ{ëw_dÒhwÜ—¥•Èã“‚=×ÿH'Umôa]Zó™r¯FcY;qÛ˜'kЊty@0 oÉídl•– ;ã·Ìm|̶ÙGš’‘ŒÝÅÐð«8?ÇÂmËÔQR'E¹Ôq™^ ¬m³ÏRUݰNUÄ;w¹´Ù>É·,w›Í­ jB¿bW¥kån³t§³Â¼,þ*É{+ëå¯*1wµ¯ç€5 D(T©—¥«­Öfóò–_¥þ:×Ë8¿”XeB¥\g…kôRt‚‘«•™æ”M O±–jèvÀëu©CdOÄâ, ÖŽBÈœlÛ9ž,Ñé©6-% ù™¨¶;Û‚j,!Ïä–f{ŒG$ctÄ=MsS؆ºtÛµªl™«K[‚E Ôôšl­ßް:çY¡ Aä§k9 öÌŠãÀhÍj×Wu\ÖÆV™8M<(ò)è­ ðcþãûI2ÿ—.?ô+"dEâ¥Þƒk§3}‘&s ÛØ­ûk§þ题O88,u¤sÿÐça‘ùíþì§\lÚ" 5bÁ)™q E2’¡€y”„G3ì…„† `)j Gx†‡Å³óq44çK³7é«ÌE4zµ¹((Œ%%Á[H”ú‡àxÍTôg$¼*|²Yÿ„pýR8€Xà3‹ÇëÇ;Ò0- ïñ-)_;ÛYÉŸQ­5G1 ¼ºÙFlÖ?5òHÌÄ®û"۽ǯك8œº:í.õÛ«ÓÁëà^ö"’ëã ÿI$_¿~ŸX†œ#!ôü&@˜µ`cɨ¨>Ò{ñ׋õ¹2âPU8 ’²þ:ÙÊvþâÃÖ×…ýÚþÿ£½ÜfL"”xœíÛŽã¶õ=@þp^v‹âM”äŒ'@»¶@òÒ¦)—@–h[YYt%zÆÞ¯ï¡®´eÏ΢ÝM]f†<’ç~HßÜèQUu®ËåŒb2CªLu–—›åìŸ?ÿàE3T›¤Ì’B—j9+õìû‡¯¿º¯7_…ö²^dér¶5f¿ðýý¡*°®6~–úªP;UšÚ§˜ú3‡>éÓJ%&T©ÞítY7¬eýK]eëüéé ?ñ†ŠÆqìæ3æ…WŸJ“½ ^8ç5^Fñç¾lQƒröð;Ð÷\ëC•ª50*\*ã¿þùõ€ôÎL殓—oë4Ù«³}{`«†d§ê}’ªÚïáíOyf¶Ë#ít«òÍÖŒóÇ\=ýE—3‚ 0‹©”RŒ£Žj4:m!y¶œ°Q7ë¶\¸Þz¥"™F!‰ÂxŽaÔ#Ô£Á¥‡ÚèÝ]ËÝ˽ÈtjåXÎÒ­Jß®ôñ·CÙ U†Í{©ã^WÆ[ç…j¹ü­Þ)ÿ¤òZ—þkõ¨ ½·>åïs¤2~žêò·"7 ïËë³=Ø+–×±§û`Ñ÷™Z× ]«;e3ä·ÈA.{¼ÌjÚ!]%ug!„öÉ|ºÐÕröͺùô˜•®2Uõ8Ù|ÎqŒž›Sýúý¡í¹APo“L?KL°ï´Þ-g<À$”TÆ| ~Ã0‹X„|Š…=#L™$¡˜ ÁÔkïPæâiœòªÊRÉIôÍ?ÚSÕ[ý´©¬"MuPÞ§¼™¼Îùi̦¢w$}@PB¦§ìhlxÜžÁí’c¾Ëß)8%ÐX \õ¯“bôˆÛZi|¥‰‰j¥“*»`lôrÈ3UßÐL]&{oµ²!oQÞ>1Û[ 4¥~ÁžÊ6ÊÛåÙ^ç¥y?ù‹(ŸÛY¯~W©yöôͰä) …Tõ~êz§µÙ¾_äSèURœS¬s®RmòÒ3zïø“ƒ(ÔÚ\ÇT­ÿ^C­´16‚§Ú¸Èsn0Äg¥Mbš”NÚœ‰m\"°ãDÈœl:ž,p6@mˆZH<&„Ôno‹UÓ8D#¸ƒÙpÃLŠ âœM°§ëØ dU—Ñ`ë[¯ u.% Lš]‚­‰:{æ"/Ô’âtI§Aéy馅Ö¤ƒ¾øÓ Ð"vÊ$Yb§ô `Ð24*‹¿¿þá¡ßá>MÿÒÕÛaG„,I²Ò°ÿìa„ßgéZ‹]bòä Û–| Ľ?"Ω­íœuÛ•+Õv)Wûµ,Ýå–Ëÿ‡É‹âov›^ngÙÜê¡Ù¶²ø0½°¾+í½ßk£n.½³HV ‚èG[Ð4µn*}Øï ^»ª1sô|^FL•”µÕˆµ0 ‹Ä¨WdîA÷ƒ¡máÁÝ`ŽÍ¹KG"mï,º­òã+(¶ƒÂÇã9±?Ý4àsèîbBY@¥˜CéÄ‚2ÂïFó9õ[ô̺›3K5$”ñì <=ÅqIFÛµ3sb΄ˆÄÜã”ã€q*îÜ aK›$Ï–w´=ìä¥Ê&V[9¦a0»à¨ÍÉFb×,èwк‹î ÍÈ님CSAãïjSé·jÑuE„t€¶°! ¡ëœ÷p±p¨8@™¹Àß!3ŸCÁ}UU@}6 ÑòªqU%§öTT¯×µ2‹á£ûr¾×ôb‹‰¬4r0ÔèúR `­Ÿ 0ígRb 20äE˜4ŸHyᜠ‰Yƒ‚Æýz¹š5¾µMñ jè£t J1ºò £zLÌ¡Rg‰«3Ϊ nl`CKásÔ×]á&ïw¤Q¸ Ê&Ì«Zt` Òèyl8ÅÍ Ù' (Ƀ(bŸ»œêÝ4àê•'‚»k:÷ [aÅr ¹(è„’pb.°3eXò n4Ù$¨>ŠÉ a "?#“}P7ttjÊÄF¹rÆd}ånëaçMïÑþ%¢­žÈƒÞ#Bï¾”Âÿ…|iŠŸiŠo¶sp-éœñº/޾>m>R üLDEÌ&…®é$Hǧu³yB ¤„«×Ä7†Œ/ˆoõ@"À2æ’_-¾MÄÃOÒpáþÄn÷Cþ_êå¦ÛLÜõÌã ÓzÞeƒ«î."J)&îno_‚`É9ŸÞœ›øò´[ŠÉÝÅk”¿qŸ6óÓ0:Kæ× œB/uî´7¿kÞò G,e@?ÜžÃòÎ §M’‘‚ѹ $Cq”" m£!#ñœà²0FA€)‰dáˆÌ‰›<{Ç®:ß—0•žGÎMuuñp8C¼ˆu°’ìBg—QRÿûTj&&ÿ°0é•H÷U»‹ ûZÛvoDõaq ×4ñ˜³æ×l‹F,0Ü+CÇBUóümïgvÇ /ñßNá×ý÷36Gó¦&pƒ»óhÎÑ_ÇQC½0ãóaãý‚(d¡s°s*1 f:ëÂäÍKÏ`14ùíx ûe\Á²„Îa~>. ã_ÑO(˜ ´E”@p<!Û´* ¤Sè‰ZŠyOƒ¼ŽÀb=ûç `[ѳÁ!‘°§ëèíŽØÐÌ*‡^4GqÞЮ½u÷_ÇtoÝÎÔyHŸDñÁ¹§MÙ÷ö+ øÿ®³êjÜÜxœíYëã¶ÿ ÿƒ |¹E,ФHŠrÖ 8)Ð~ISè—B–h[9Y4$z×¾¿¾C½¬—÷6h/è¡§ÃÞJ3ÃÇÌüæAîã—cî<«²Êt±q ®£ŠD§Y±ß¸ÿõ'OºNeâ"s]¨[h÷ǧo¿y¬ž÷ß~ã8 /ªušl܃1§µïŸÎeŽt¹÷ÓÄW¹:ªÂT>AÄwòÉM>)Ul²g•èãQU=´¨¾J—é®yyA/A-E¢(ò1õ)õ@«®…‰/Þd,ìsi,ÅûÀˆ¾Ql]qNðÓËwTés™¨ T¨PÆÿëûžéa”št8OV|¨’ø¤FëvÄÆ ñQU§8Q•ßÑ› ^²Ô6.ÅÍçAeûƒ¹}?gêåOú²q±ƒŽhD„ìöÖJÝœNJ–n\PV¶_í’ë!:uÞ))bF+‡bJ>4£;½×©N¬79¨äCžUõí—P—“.·ËrÕû}TþUe•.ü÷êYåúd¡äŸ2”¸4~–èâ_yf:w滤'pS$–¹×ŽûdÙ©ÚUµ\cûI]Ço˜½:v{©5ð@tW­cçïʹ.7îw»úé8[]¦ªìx¢~Æ< ¾Î̵ ÃnþnÓvâ^ߨqª_ 3îG­0±D$1å3~bá‚"@‡ A0gÛ] $#†rÆŸ­w¼s‘¤Óe>Á¹,­D_è¿u2ÕA¿ìKkHSžÕläKV€N^‹yѹê­HcvOƪyw}…wŒ/Ù1û¨`—d&c5šç7DÜ·IªÜê¸L'k»œ³TUw,SñÉÛnm¤/ò-Ë;Åæpo‚Z ÐoXÁSé^yÇ,=é¬0Ÿ“äk+ëío*1¯î¾žÖ€ô¢¡>-]µ6‡O«ü¦íïs½ó±Ä.3•rŸžÑ§žŒ\íÌ2§lð»ÄÚjclÏZCä5ôÑYj›:“ã&çAb»MØŽtsµuérµD·§Úµ”(d7¢:žlªûy#·´:«PÁ¸ :ã^—¹)誦Ñ`ËZ•ms5Ö6PÄ@M§dë¢v„Ýsž jI~Êi0zV ÓBG«ÓAWüyhGeâ46ñ t$Þ[ú“õ/ïzêVxL’õ?tù¡_Ñq¬H¼Õgð¿ût£?¦É:Šclž²#ä Û| Ä£cŒ¥­ïó63—ªiNÛ´49fv”ÿ7“åùŸí2Þƒi3“«§zÙæµ×Åo•é”õ‡Ú>ú5šÏýy¼UD±uÁ™§Ö}©Ï§#ÄëÆ­K‡;°sM臘2.*këaxÍc£Þá•M‚n%à½;öcHKÞ|?˜l[f—wPl9Å! ¢¶ÿÚO¬ ©‹0¡œ¶¢’"F(nî,Ô-ÅÉÈ»û‘§j‚yäŽÈó-ERPÒì¨ù"’¯Hˆʘd+/ â4 ìa¸ ,i“ähúµû•¼DÙÄjKb€HÈÝɈÊ\m$¶ Éšü­[¾.àˆP¿y#’†$ú¡2¥þ ÖmW„qKh ;âšmÝF,lj (Ò!ñ7ÈÌc*ÀW•9Ôg³f-¡—e|mv5 êÝ®RfÝoà¦Ä)†œïսغa:Vr0ÔèjjðÖ_ÊQÙgZ":PÇ“×T^¸¢L ZÓv8Fa-íüs:›u¾õ”ÁŒÕwQº£]zÐO=Çæ\ªQâjÓ§*ˆØPÆxÆA½ …»cÿ³-Ý”›±lÂ\´â€Æã᣽†\ÏB—Á_á[Ãwêþü61L fãà&¸œZ x”H2)g…"ÁdÈhÊëúò€CF’á4½õçÙ–/Åzìf‡†ÏéIH¥Œõ‚_‚'“¬Lr5õ¥u„tæJ{žë"̘—z$»Ä!ýCJ\Jú¥»aP½ë\½óX²¹Ù a‰Y€Ôî"`‚Ù»ÀÏ„"ÁÅïæ²YP}—AÂfX|A.û]ÝÐePSf>úÌ•3³xì*'¶ö»î=šÿ1kªç³ãAï!_Ká_›âWšâ»íKZ0.cñ†õy ð™ZàWz Â":+tu$@»`^7mÙD\8zͰÑ÷@à|†Yx¯b‰(Ábñ­{  üCz ^ù? »78òÿÒ.wa3ƒëqÐi½Y¾w&Q(›ÁÝž¾F"‚ùYa`3,Ï»¥?Ln£üýàòiß_;õo£d¾ pp½Ô¤sÿÐçAHN~¿?ûé7œ6IB¤X0JV C’ … œÄ!ÐæQR­0 #‡sD°5%à‚9x…‡É³óq44ç§foÒÁTk«ÆB,‰wWÙËá©p骬»Ím¯ÊúOO@gËY}SaD(|½~S6>ë-Þîy0‘LD’¯<I™€„£ ¯ye¯om²¢ÍpÌûR‰ç{Fd‡‡†õÔáÊþÉŒ“0 ê1E‚”:¹#[O¶›pìu  Yu/Ó ×wãlR߯†ßñ­ SüÁ)Ð2Œ„9ö›ßöŽ~ÿþ©”ê!zxœíYYãÆ~7àÿ@Ð/;°Øì“lÊ£1, H^üPdK¢—b dkFÚ_ŸjR¤xI;{,²ÌŒXU}T}uuëñûÓ>wžUYeºX¹a×QE¢Ó¬Ø®Üþüƒ']§2q‘ƹ.ÔÊ-´ûýÓ×_=VÏÛ¯¿r†Õ2MVîΘÃÒ÷Ç2GºÜúiâ«\íUa*Ÿ â»=ùä*Ÿ”*6Ù³Jô~¯‹ªZTßô¥ËtÓ‰¿¼¼ VK‘(Š|L}J=ðªsaâ“7 ûœK1Æ>ðz¢¯[V`œüvò-UúX&j*”ñßþü¶cz¥&íÏ“ïª$>¨Áº-±1C¼WÕ!NTå·ôf‚—,5»•KqóºSÙvg®ïÏ™zù‹>­\ì`G ‘ øõÓEê :i(YºrAYyy»,¹ì{"ζN¨’„mÄ¡˜bGNr¬ŒÞ?4£[½—©N¬+w]ÆE²óT‘¢Î¢ÝêtÐ¥ñ6Y®i§÷Ê?«¬Ò…ÿV=«\¬/ù‡Ì%.Ÿ%ºøWž…ÅùNépŠ‚yî¹å>Yöcª6U-×X¾R×ñf§Ý^j-Ü]ÇÕÇ9Ä[ðå\—+÷›Mý´œµ.SU¶¼ ~†< `gæÜÄa;»i;q'€oT»8Õ/à î{­÷+—…\@„ᄟ€¿Š8åTˆ)×n Ð!çlÂŒïXdépšNp,K+‘Çgêo%‹Z™j§_¶¥µ£)j2ò%+@%ïâó$¢SÍ/"mŒù-·xç;¼}|ÊöÙ{»$«Aßú›8¿:Äm›Ô®²SÉ;U®u\¦£µ]ŽYªª–©Šøà­×6Ògù–åb³»5A-PèW¬à©t«¼}–tV˜‹¿JòÞÊzý›JÌÝÝ×sÀž@2Ô‡¥«½Öf÷a•_µým®×q>”Ød\¥Üf…gô¡çO=F®6fžS6þ;ÇZkclO´v‘{nÐEg©MlêLŽ›”yí: Dàe¤ã˜³­K§³%ºÕ†¨¥D!¿Õþ`kTÝ/È+ùB³á†hÀ…dŒN¸çyn ºªq4زVeë\ µ„ 1PÓ1ÙBta÷œg…‚R’ŸÇrŒžý´ÐÒêtÐæZÆ^™8MÜ+-ItV†þdùÓÛžÚ“dù‹.ßu+:މ×úø»OWúcš,¡£ØÇæ)ÛC¾°ÝÈ·Ð@<úWÆPÚb×›·™¹TMs2Û¦¥É>³£ü˜,Ïÿj—iõîM›™\=ÕË6;]ü‹2­²~_ÛG¿µFóº{g¯Ñßl]p¦©u[êãañºrëÒáöì\º!ú‹ÊZÄ" óب7xáAÓƒB,™xèàØ]ZòðŠ}o°m™Þ@­‡œE l.¯‚- ©‹0¡‚|A%OB1{¸Â×[¨]JºÛRµÁ"räé–Š"PÒì¨y#R,Hˆå\ò…ÇC‚2Âú Â’6I¦ïY»[ÉK”M¬¶$2DBáŽFTæl#ñÒ,ÉwйåËŽõ'¯e`Ä#&iH¢ï*SêwjyiŠ0¾šÂ‚8„f›3ÖÒmĦ–àEÚ'þ™yH÷UeõÙ,yKKc¨ÆeŸ›]õ¨z³©”Yv¸*qˆ!ç{u+¶l˜ŽÕÆ 5ºÐú»Cb‘} % @êxáú‘Ê ”ˆÖôQXK;¿Žg³à[l¤dV×EéŒbtéA?õ›c©‰ëN—ª nl`CKàõ¼+Üûû¶tUn² sÖŠ=š@Œ‡gŒvÏs=ëº&øâ¾µûŽá/·‘aJ0›€Çx äØjÀ£‚!É¥œX hˆ.C&£1¯íË™€Œ$Ãqzëγ¾ æ¼ÇnV†S_ÿHB*å¼üL²2ÉÕK „P éJ{žk#L˜§z$¿Ä!ýS*`BJú¹ÃЫÞu®Þx\<ÌÙ܃l…°Œ‚I€ÔpÁùNà*ëƒ{ ‚Þ²IP}È as|F}T7têÕ” FŸ¸rFxmåäýÖþ׽Góó¦z>;ôÒyÿ¥þnð¥)¾ÓßlçàXrqÆy_¼úú´øD-ðˆðˆN ]Ý ›ÖM[6‘8zM|£ë|Žyx«â Ølñ­{ þ)=€¯üv»Wùi—›n3q×ÇA§ußeŬ»s‰Â àw·§/ŽQÀ›žz6ñåi·á‡Ñm”¿í]>m»k§îÓ ™Ï;œ@/5ôÒÂßG£Eôˆ‚@dz›¾wÃi“$ä@ŠNÉ‚cH’a ™“8Ú image/svg+xml Ç ¥xœíXÝã¶ÿu^nK")Š¢ïhA4/IŠ} d‰¶•“EA¢×öýõê“²ì½ Ò zèi±»ÒÌpHÎüæƒ\{>äèYVu¦ŠÇqñÉ"QiVìÿüõ{G,P­ã"sUÈÇE¡ß>}ùÅúoŽƒ¾«d¬eŠN™Þ£‹wu—½Ùk]®<ït:¹YGtUµóãÀP\?ï¾ü!sõ*MݘòXålšx2—YèÚ#.ñ–|2Ê'fÙ³LÔá ŠºZÔ_ÙÒUºÄÍ’N~#E¢(ò0õ(u@©/…ŽÏÎÕXXç­±cìÏ}¥ØªË–ð;È÷·VÇ*‘[(ÝBjïí¯o¦ƒÝT§¶žÞ°“y'Ö.⃬Ë8‘µ×Ó[§,ÕûÇÅíç^f»½¿Ÿ3yú»:?.0Â(piD8çl|ë¤FÄ–’¥ ج辺)Wƒ v#ꂞ4æ"J|â/Å$r0qH§´ßî*U‰Y>(̳TVî`ÄA­<—ªÒÎ6Ëe+éíÕAz™ÕªðÞÊg™«ÒÀÇ+3 ”¸Ò^–¨â·<ÓÒ-‹;úÎi ®‰ømî¥ç>ö:•Ûº‘k7o>éy-sØ‹Y^jŒj‰nâºsBe¼øæªz\|µmžž³Ql¾çñæ™òø7Ó—6n{ýý¢âAߨ÷qªNàý÷½R  7Ä,"þŒŸD„ðà0˜s/†+0%‘͸àߣqŽs,2 ±Sžç ŽUe$òø"aûÍ?ÒKÕ{uÚUÆ’º:ÊÙØSVÀ¦œè$¢ó½w"=ø &ó=t2&îñ./ðñ9;dï%¬’ÌdÌlûoã|„Ä}«4`ÙËä¬6*®Ò«]Ž2õËÔE\:› ï›|ÃrÊXïï)h õŠ™î¤sÈÒRe…þ°ø«$_šYm~—‰~qõ˜rˆBZú°t}PJï?¼åW-—«MœO%¶™¨T»¬p´*-eL°¥]–PŸ°{B˜Ò¤½‰zËÚÃLN"Mª4EÎwÉXäû§Ö€]±"ß@7–¯ 8&4oNÏÀ.‹|AC}SëJ½“«®ÑÁ¸#´¥q=3óýžnµ©Mürí” ¨•UW¯XOKc¨¯U_ÚUYTµÝÖR¯†Œ›(cÈâNÓ^­Z&2»AU¡êÖ×Foý„hàú‘y–°K—Ã(r„‹›GH'\RÆ]ÚÐ@vÃFýûZ›q¾ñþŒ5tFª£hU9Ð#=ÇúXÉI¾êœ3d(ˆÏP˜x¦±| wÇþ¹%››±Lž¼iE‹Àéá8“Ìör] >÷ïµû+ðÛ•a*0[ Îg<×V |W0!f…ººœ‰ÐÑ5¯ï´ý2’¯ÓÛp,íø‚ßBY¬çXÿž„TÊØ ø)x2ɪ$—×¾4î‚â‚Î\iÎh}¤‘óÜŒdwƒ8¤I@q?‚~ên°ªw¥´iJ½È ›;­\,"> Æ]lî™»ÀÏ„º<à7x£ËfAõQ\ ›aþ ¹ìuCg«¦Ì|ô‘+g„gñØWNf·æ»é=Ú¿˜µÕó9Ð{ôþs)üoÀàsSüBS|·ƒcIÆÛX±>o>R üBDXDg…®é8ìΟ×MS6Ý€s8zͰ1ô@à|†Yx¯bË#Ÿû7‹oÓùá_Òpàÿð{…#ÿ/ír63¸NÖË n 7äœÍànN_ »Ü÷ýùYÁ °–çÝR„®.¡¼uù´®†·I2¿ pp½Ô¤w¿íÞó°ˆó€üqê­‹M“$!RÌ%K†!I†\ø(AÚ‰è;Ä\&‡¯#cŸùh|]|4[<ˆK§Xø4èu)r#Áá$f¦ë>¨@ƒ¤-пYz†f¢Q WoéVqµ/ëÞäÖýfóÞÝoZŸÖåénÍ™‚ò×ÃmØÊœŒ7ì&׿~þÿâúbãÛñxœíXYã6~ÿ (/Óˆ%ñE9í°Y yÉfw} d‰¶•‘DA¢Ûöüú-ê²»§ƒÍ 2Øq£»ÍªâQ÷G>~Î3ëYVuªŠ]d[²ˆU’ûýÏ_p„mÕ:*’(S…ÜØ…²¿úú«ÇúyÿõW–eÁô¢^'ñÆ>h]®=¯ËXå¹*êfjQ3–®’Ý ~:Üm¤p†"!H8õ¥ÐÑٙͅsÞšKBðF¢¯[×`œ~ùžàÖêXÅr¥[Hí½ýõíÀt›èd¼NZ¼«ã¨”“}{bk†(—uŲözz»À)MôacÔ2ÝôuüœÊÓßÔyc# Y¾KBÌ9g×oÔÕ鸥¤ÉÆeE7ê¶\£Ã%Ö)x,$‚peD°ƒ°ƒý•k­ò‡vv¯÷:Q±ÑccÇ¿ËÒZÿ–‰Ô²ÊÓ"ÒÒÌ;l(Ï¥ª´³K3ÙNõ*—ÞE¦µ*¼·òYfª4å•©JTi/Uñ[–Âzeqg½sR‚ÓB~›{é¹O†ý˜È]Ýȵf1Cb[^Ë”3ÇKŒ¹G¢Û¨îÜdYe´‡ÀÎTµ±¿Ù5Ÿž³UU"«žÇ›Ï”§Àó©¾´IÙ¯ßÚ,< ;õ!JÔ âbÁ}¯T¾±qý€#†ü‚'paÀ–\s(â2J.¸àð£qŽs,R YUž— «ÊHdÑE‚ú{Áêƒ:í+cG]åbæ bGœ.pH–šw"}R`„Ø=“"÷x—xytNóô½„S.mg4[e×€¸o“&TLŠÈj«¢*™MlìrLYß±L]D¥³Ýš´¿É7,§ŒôáÞ@¡^±ƒ#“½tò4)UZè‹¿Jò¥ÕöwëO߬{@­Q(W–®s¥ôáÃ*¿êøûLm£l*±K5„JµO G«rO#F&wú6§jã÷k«´6 ¼ Ð&D^ ƒ!;+¥#Ý”uÔ–<¨k×e »™–¥/¦I/†hT“¢†ìJ”yiVÄ•ÜÑLº¹„3_PJÜËmnºÊy6˜W§ÛLNµ„P“9Ù¸¨›aΜ¥…„V’]ær Œžã²ÐÓšrÐ×oÙZF.u”D:µƒžäV°²þåíOýq¼þ·ªÞ ;Z–‰¶êþ·Ÿ®ôÇ$^¼È#ý”æP/ 4ùÐÄ£weL¥ïFë¶+W²E*71[ç©™åýC§Yöw³M¯÷hÙTgò©Ù¶ý:èâuÊôÊzcm½Þíp?Î,ÚJH¢ŸL_°–¥u_©c™C¾nì¦uØ#;7„aŠ®¢¢61†¯ 7hårºPÿapÇ~Ò‚Wß–ÛVéù ôZŸ €Ñp…ÌO7ôé ^ˆ0ñ1g+" =b‚èÃÕ}£ú­|<ñî~â©F#?´'äå‘°†‚Üž¨aá¯pàR˜`+‡bêú„bö0Þ¶4Er²üÈÚÃNN,Ma5-‘º8ðíÙŒZ_L&vxd¿ä–­ ¸/4ßœž\Ra@Ãwµ®Ô;¹î@B¡mì ˆ@ތҞn2µ†(’1ñw¨ÌS*„¯¬2èÏzÍzZA7®ªèÒžjDU»]-õz8ÀU‰2‚šï4PlÝ2-£5zt=7xëg‹ø. ÍgZºt –#\Ô|„t‚aÜ% ýGËGnÐH[ÿ™¯fœo|#]°¥ 0ŠV•xê9ÒÇJN W眡TAޘĆ6ÃgšÔ·CáîÜÿíHWå,S0oZqDó]ÆC7ŒöRä:&t,ð%|›ð»¿¿Í SÙ|(p”q_Ì­<âSW0!mnœ‰€ŠpÎëq9õ¡"‰`^Þ†ËmÇüVô˜ÃÂôSxJ)cƒàçàÉ8­âLÎ}iÜ)ÄY¸ÒÜçúLà 湙Éî&q@>IBqê A>w7ŒºwÀå‡ù·lî@µr‘ù"Awa° FÁÂ]Us5ç>¿Á»ºl‘TÅeP°⟑Ëþ:zÊÂG¹s†h‘}çdcèaÆ öhÿ"ÖvÏgËì!¬÷_ZáŸ_@ñ  ø.œƒkIŒ·cñëKð‘ ð ³,]ƒ8hG—}Ó´M×ç®^‹Ø08Ÿ!ÜÃ@ÌwyH9½Ù| DƒO‚|¸ðŠ¿pؽ‘ÿ—v¹6‹pD ­—CÖ¿îL¸çlîæöÅË)¥Ë»Â(Á±¼DK!z˜½FyûÑãÓ~xv¾MŠùí Ç€¥¦q€{÷½Ñ{ô9÷ñ÷ç°üè…ÓI¨qFðŠ!(’ÔŠ- 0à€ p…Ü“ ´|ßÅHð†B}Î,´BãâÙû8›óCs0éè ª³Uk!G;MwÙ‹ñ­ðÖSYÿšÛ=• C‡²õYóR"½üRÆ'EýÖ¢ÜG(hßÀVŽp ÈP±r0„‚™<~-»|ãÆÀà„0¾¢ààNX »°:°õ/ €â „õ£EM÷2<Wäyж?ë_ÃJ8 çñ·42o{÷ºØ´-s ýÿhÞŠáÿï¥"ZxœíZ[¯ã¶~ÿ (/{‹")J¢û(A ´/iŠy)d‰¶••DA¢íýõêfÝì=ÛdÓ.ºZœ=ÖÌð2óÍôÙ|ÉRãE”U"ó­I6 ‘G2NòÃÖüçÏ?XÜ4*æq˜Ê\lÍ\šß?ýÕ¦z9|ý•a0<¯Öq´5JkÛ.NeŠdy°ãÈ©ÈD®*› b›ùè&•"TÉ‹ˆd–ɼª‡æÕ7Cé2Þ÷âçóZŠA`cjSj„U]s^¬ÉXØçÒXŠ1¶7}¥ØºãðÓËwTÉS‰= (Ê~ûóÛžia«x8O’¿«¢°£u;bc†0UF¢²;z3Á9‰ÕqkRܼEr8ªÛûK"Α—­‰ l¸ˆÄóàeá%É’÷v97Ö`hû}˜ÞÜá¾MjG9Šè(w2,ãÉÀÚ.§$ÕËTyXX»ôÑÀ^@ó¬"TÇ{3Ô¹|Å–ˆÂÊ’¸I®>,þ*ÉvååÍËÝo"R·_O‹@~QHQ–®2)ÕñÃ:¿jÿ‡TîÂt,±O8KyHrKÉbàQF*öj™S6¼ÄÚI¥tÏ]´v’Ö7ÛÇg)U¨êT>Ÿ¥Ñâð¡Ê`<°öC‰·W0¼GJ\”¥³kšèî£|n$77À®Ûц¡®º¤^®šhöT^4%ðÙ(²B—׺Õá7rKÓ©Q¹ÜqèŒ{]æÆ€’˜F²®ÈU²KÅXSØ@5ž’µæí½g­:Áô:•“à.I>Li­Ne]å²ç¥«adB…q¨ÂA!ëHnoeh­Ö?½ýá¹[aEë_dù®_Ñ0´H¸“'ð\óùFßÄÑš¡,TÏI¹N7RßBï³±oŒ±´Æn0o3s)š¾j±ÃŒ£,Ñ£ì¨$Mÿª—éôL›¨T<×Ë6{]ìV™NY{¨íÆî¬Ñ¼¦š†;áÿ7]ÓŒyY8”òTdi¶f]öÌkB?DAkTi‹h„ác*ñ¯,è×¹ã>õpÆ.Í™Ã~0 ضL.o Kp)ö™¬°þ×¾ºÎ úÑê­(§ˆЧ|ƒ…º¥\2B÷0Bª!Ø Ìy¾%‚‚€{”4;jÞwWº»  Ú•å¹Ô!ìi¸ ,©Óûhúµû•Ú¬¢Ë¹ƒˆïš“•ºêHl;©5ùzÎtÃé¦þdu ŒXýŽO‚ï*UÊwbݶs·„¦)AìÃ99NG× ›Zƒäñø¤Ä1ÜW”)ôjÍ:ZB'Q–áµÙÕ€*÷ûJ¨u¿›EÉÖª›ÈuÃ4´6Tè/ª©­¿ú½@?+Ðy 5,Žpýpaù+Ê:ˆ7sŸ–lnA¶B˜Þ,@j¸Ø„`àL(ò\owƒlTŸ2HØ {ŸdÕ ]5e†Ñ'®œžÅcW9Ù°õÐïuïÑüYS=_ zn¼ÿR ÿ7øÒ?hŠï¶sp,iqÙo¾>o>Q ü ", ³BW÷@hçÌë¦.›Èõ<8zÍ|£ï|†™¯b.òÇs‹oÝ9þŸÒ¹pàåÿÃn÷ ÿ/ír×mfî:ò8è´»¬»èîŒ#ßóØÌÝõé‹aä9Ž3?+ læËón)ÀO“Û(û0¸|:ô×Ný§Q2_v8‚^j줃ˆF‡<èxžK>Ï~úÁ §N’)ö%+†!IúwŒÈ ÐæQâS¬0ò õÃuÁÜ«)Žë1¯ð0yvCs~(aö&Þ†=¾”ÞºYToÞ, 8xЀ¬,—!æ1°ÇwcÎ().Î Îê,pùÊ%ˆá8ºßš§»øÇ>ïÆ_ãã8ÿÅ#á8NnQ=©mLc8HÄóÙ$¾ºî¾Åž„¦îo=Äýš7féº0p?8ß½çIy²"Ó½ËFDõ?;æ›Ú§û›Ô|'qÇzÍwþ¬óuÖ# ÖóæcCËÌÓWó{£¯ùá÷¿{Øï ÎxœíY[£È~_iÿb_¦SÔKyÛ½R4Z%Rò’l)/†²Í¦HQnÛóës Æ€=½Jf”QÆ­î6眺œûWÅóO§}á¼J]çª\¹aבeª²¼Ü®Ü¿ýò³»Nm’2K UÊ•[*÷§—ï¿{®_·ßç8 /ëe–®Ü1ÕÒ÷«ƒ.Ò[?K}YȽ,MíD|w Ÿ^åS-“¿ÊTí÷ª¬›¡eýÃPZg›^üx<¢#k¤ˆÂÇÔ§Ô ¯>—&9y£±°Ï¹±cìo úF±e Ʃ෗ï¨VÊ ”¨”ÆÿËûžéa”™l8O^~¨Ó¤’7ëvÄÖ É^ÖU’ÊÚïèíÇ<3»•Kqû¸“ùvg®Ï¯¹<þ^V.v° *H†üúí"uu:i)y¶rAÙøòtYr9ŒDw2Ó8Âq$Å”x˜x$X8é¡6jÿÔŽîô^f*µz¬ÜC•h­Ž¨7g¿€ÿ(a—d"c5š“׈¸o•&Vv2ý õZ%: lìrÈ3Yß±L]&•·^Û<Ÿå[–W%fwo‚F ToXÁ“ÙVzû<«T^šO‹¿IòÑÊjý«LÍÃÝ7sÀPœ@êÓ§¥ë½Rf÷i•ß´ým¡ÖIq+±É „ŠÞæ¥gT5ˆ§£3ÏÑmüαÖÊ›ÁÓmBäQôù©•ILSÇq[ó °]§ ¼Œts¶]ét¶D·§Úµñ+Qî+Û¡´_ÉšM7DCÄŒÑ ÷<ÏÍ@W9ÎÛÔê|]È[-aeÔlL¶.ºŒ°{.òRB/)Îc9FÏËaYèhM9è€?í-c/M’%&ôƒŽôVt²üËûŸ_ºžÓtùw¥?ô+:ŽIÖêþw_®ôç,]žØ'æ%ßC½°Xäwžý+ãVÚún0o;³–-4™iYºÏí(ÿ¯&/Š?Úe:½Óæ¦/Ͳí×^ÿ¢L§¬?ÔöÙï¬Ñ>nÇÑY$k Iô'ÛœiiÝju¨ö¯—®áì|ÛFŒNÊÚZÄz¾‰‘ïðÂȃ«°à©wÇö6¤c]}?˜l«óÓ;h¶ÅgbíÏå1` €t/hL'³§«û uK䯻ÛO5"½!O·DqHI»£ö‰ÄÁ‚DˆQÎc¾ða( Œð§á‚°¤-’7Ӭݯä¥ÒVÛ"QàŽFÔæl3ñH–äG€nŲ„BóÍëqÁbñcm´ú —T„ñ…Ð6vÄ@mÎXG· ›ZB”Ùø+Tæ[*„¯Ôôg³ä-K kœÛ] ¨j³©¥Yö¸*Q%Pó½‹-[¦cµq C®ÇFoýÙ¡bÂ~ % Aêx1ÂÍ'–^´ úÌSàI>v“¡‡}n°Gûó¶{¾:`Øùø­þ7Âà(~ŠïÂ98–\‚q>¯±>…Ÿ ?À@„ :it  A;6í›¶m¢ áè5‰ó9æÑ= Ä ²ÙæÛ` } À7þ»78òÿÒ.wÃf®7HëqȳáÎc…!Ÿ„»=}qŒBÆØô¬0H°I,OÑ’ÀO£Û(;¸|Úö×Ný·›b>àà°ÔmÎýCotž=Dä·û³Ÿ~pÃi‹$Ô@ŠCNÉ‚c(’Q3'uÀÜ43r¾ÔãÇ]Ž^TUgºxr(&RE¢Ó¬Ø<9ÿúùGW:¨6q‘ƹ.Ô“Shç‡ço¿y¬_6ß~ƒ‚åE½L“'gkL¹ô¼r_åXW/M<•«*LíQL=g"ŸŒòI¥b“½¨Dïvº¨›¥EýÝTºJ׃øápÀ¿‘¢Qy„yŒ¹ áÖ§ÂÄG÷b-œóÚZFñ€7}£Ø²ç”ðwï ¸Öû*QkX¨p¡Œ÷þç÷Ó%85éTOV|¨“¸TgûöÄÖ ñNÕeœ¨Úëé­‚C–ší“ÃHûºUÙfkÆ÷—Lþ¢OA˜ETÁǧNj :m)Yú䀱²{ë¶\‚G sô.Hc!£Ä§þ1B#—P—ò‡vYoð2Õ‰5àÉ©â4Ó«½1ºøu_$[•|Pé¯Yz œ;l§Ž¥®Œ»ÎrÕ®÷¶z§¼“Êj]xïÕ‹ÊuiÓÊ+3PéÅ•ñ²tç™Q¸,nè;¦%„,×¹§žûlÙ©Z×\ëûÊäµÌÁB{¼Ô:{"ºŠë.H•ñÒ:×Õ“óݺùõœ•®RUõ<ÑüÎyâž™S[’½þþÐVñ @nÔÛ8ÕÈŠ÷£Ö; û˜G$òƒ?Ôqç’Ϲ°g„9 )ÅŒ QßÛà¸û"3PSåq®`_UV"O Ìoþ£½T½Õ‡Me=iª½š­=dåv@#6·½é‹‚R?¼%cí”·˜§{Ì]|ÌvÙGç¤3kÃ4ë8“â¶_štiª£Zé¸J/6žÙg©ªoø¦.âÒ]­lá_å[–[Æf{KA#Pè7ìàªt£Ü]––:+Ìëâo’¼·³^ý¦s÷ôغˆBÃz]ºÞim¶¯›ü¦ãor½Šós‰uf UªMV¸F—P«W¹Z›ëœªÍàk¬•†vº›ð†³4)r/ † ­´‰MÛØÛ¶½mT5Ø­DÈœì˜:ž,Ѩ¶H-% ùHT»Òެ>È‘ÜÑìLÂLð@ú>›qO×¹)ت.«ÁN¹:[åêÜJ8@5½$Ûu+ì™ó¬P0NòÓ¥œ§gÅqâÙžfO7Ìo>ZÆN™8M< =)¼ peù÷?>÷;<&Éòߺú0숉WzñwžGúcš,`ìbóœí _Xpò'ÀÞÈ8—¶±›èm5WªÅ*WQ[šì2»Êû§Éòü¯v›Þî‰ÚÌäê¹Ù¶}lñ:czc½©µ^ïöus™y¼RPD³CÍ[ë¦ÒûrõÚÍ gâçóAbª¸¨­Gl„á1zG.` éC86ç)-y8Æ~¢|[eÇw0oFBîG bÿt¯¿ŒÊ*ø‚I€J”ÿa ßd£~«€žEws©F„’ rÎÈó#QER0Úž¨}£2XÐûÍ_¸Ö` û¦=Oµ=Ú&y¦~âía'7Q¶±Ú™ècÎÅŠÚœl%v˜dI¿ô–/ ¸14OnÏ 9|ÉB}_›JPËÒÚÑ‚$ìÍ}¿§ÛŠ…C-!ŠtJü :ó9ÒWU9Ìg³ä=-aWU|jO5¡êõºVf9`4¢Œ¡ç» [¶Ld­AЃaF×—N€hý±û‘ý-ÀJ,À†\‰Ió“Ê Œ ÌúO( 8l¤Ñ/—Úlðml¤ôg¬Iéœbtå¦z‰Í¾Rg« ÎЪ nlaÃKàw^Ô×SáæÚÿìH£q3–m˜W½8¡p ù î6s§ÝË\צ._Ó·IßËðW· ÇTà¶œÏE /½<øXr)g…bÁeèËè’×#s?€Ž$ÃËö6\o;þxɘf=¬ ç¹þ" ­”óAðKˆd’UI®.ciÃ%$$›…ÒÞéúJ£3æ±YÉoqÈþ‚>Ü8Ù—†Éôn¸zçòàášÏ]èV˜ÈHÌ ¤ ŸPÎÂq¦ ‹@\á!›Õg 4lNIJOBCÇÉL™Åè3OΈÌ걟œ| =ì{ƒ=Ú o§ç r{Hôñë(üo¤ÁWP|ß„sp-é’ñz.޹>‡Ÿ ßÁ@”Gl6è $À:>7íØÄpõšåÆ€ øœððâ‘/ü«Ã·Á@~ø‡` .¼ò8íÞÈÿK¿ÜL›Yºže ­û)\Mw.q(Ÿ¥»½}q‚…ïûó»Â¤Àf¹,z³»˜HFƒ/¡PšÏ8\G!€è… %eú38‚F†þbxB¤ù€\H¹ äŽØ¨–Z§œÞ“Aš†QøV¥ãYAÖ†G1 °¼p…] Oí2$â×}•íÞã7ìIV^ûÚëï>¤N^'_iÏòº¹¼0ñÉyݶƒGû¹þÿÆ_q}X.¹xœíZmã¶þ ÿAU¾Ü¢ERE9Þ Ð‚h¿´) ôK!K´­œ,½¶ï×g¨7Ë’µëEï¶Ýf}¸[kfø6óÌÌCí-~8n3ëQeªò{› l[2U’æë{û¿üèÛ*u”'Q¦ryoçÊþááÛopëÏ…Œ´L¬Cª7ÖÏù§2ŽvÒú°Ñz7wÝÃá€ÒFˆT±vï,Ç¡0¸|\ûeY°v^ΓøÞnÆìöEVÙ&±+3¹•¹.]‚ˆk÷ìã³}lv>ÊXm·*/«¡yù]ߺHV¹ÙÒÁ«¬H†.¦.¥X8å)×ÑÑŒ…}^K1Æ.èz¦7šÍKðìþvö­•j_Är%Ê¥v?þò±S:%:éÏÓ:öbÝ oçÑV–»(–¥ÛÊë i¢7÷6ÅõãF¦ë>??¦òð'u¼·±…-ÑpÎÙù[cuF ©%iroÃaEóÔ,9ï 1 )‚y’ˆ‹0öˆ7³(&¡ƒ‰CšIÛãΛíßÛZ©lΘI&Óie¨ój·Ž<îT¡UšÉz¨»Q[éždZªÜý(e¦vOî.Õ ‰ í¦±Êÿ¥Z¢]>1ß1ÙA¬B~]{jµF½H䪬ìjo˜Gj[n­ìg¶—/÷L—QÙDDzvÑðœ©âÞþnU}ZÍR‰,Z¯>—:Oõ©NävþvÓfâÎO”›(Q€ÃHûY©-Șú¡écÀŒÃßg|¬6»Âˆ Ïóǃ!â{gŸ§²iwO°/ c‘E' ç_ /lmÊ:¬ ãH]ìåhä!ÍáLN|ÒñÑ“6ÆlÊÆ¤Æ”îô„nÓmúYÂ.ÉÈÆœ ïþU”1í“ +’ÅREE2XùeŸ&²œðL™G;g¹4é~UoTÎ.Ò›© *ƒ\ݰ‚#“µt¶i²Si®Ÿ7¿Éò©•ÕòWë'w_Ík@S(SÏ[—[¥ôæù#ß´ýu¦–Qvi±J5@¥X§¹£Õ®‡§ž"“+}]SÔø½¦Z*­MZA¤ƒ‡Úbq¶€äjFY–>™¾s<¡ÝIMöI°³Pnw¦UdBœÅÌd¢œùPèH{º®MàrtÓ¶Êt™ÉK_Âò¤ÉPl¼ßŒ0{ÎÒ\B›ÈNC;þLó~Æ·²*ÓÛÚ{­ØJ%‘Žz¥¾ù—ÌÿöñLJv…EÏÿ©ŠOÝŠ–eL¢¥ÚCh퇳|‘Äs` ÛH?¤[(†müÂÂ=+.­MìzóÖ3²&WiXoS3Êý»N³ìg³L{îÞ´©ÎdOºp›3´gtû‡\¸­êÇõY´”1•ÞËu¡ö»-dà½]5»çÞJÐ ÑE”—Æ&°ð5¾úÏà2(ÀÂóïº(¬/‘,Xpyopi‘?@ûô)˜ΰùÓ<úÞ ¸Zˆ õ g3*€òн»sÔz µKùä"¨ë‹U&û¡}!o‰ 0œ’zGõþŒÈ£Œ 6s€u!Ÿz„Ýõ„%MÙ»˜¾çín%'–¦Tš&ç!øö`D©O&Š1'ßËæ9\ªoN«Àˆ…ž  ¿/u¡>ÉyÃs0nu«C‡fž×ÊM¢Â¦æ€<é …Z{)ÔÊ"ƒŽ«ç¬•%ô×¢ˆNõ®zRµZ•RÏ» œ±‹ Š;»š×J˜Ƃª ]·:¢õW‹úÈ Íg§DÎ@-G \}„t‚eÑJþ“åcTÖÖ¿†³™à›ØáT/R98E«Â†ôé}!/êUœ®BAÞ˜|†ÆÃç2—¯Carì¶¥óáF*S'¯z±'óá6ñÜQÆN{ ¹Ž.ƒ Þá[Áwþâ6pLnó¡ÀyŒûbè5ÐQßC‚ 1ò(ôÍq&O„C]Ë´=*’†å­»¦6zÁ¯¡ÇlVc¬…HB)e¬3| ‘ŒÓ"Îä0–&\B\ÐQ(Í ­Í42R«‘l2‰ú* Åáö(è[C¯{JRâ0à"W|î@µBX„|” U¸àF Fá‚8Š¸Ï¯èÎ!%ÕW l†ù Ù‹ØÐ±×SF1úÊ3Ä£|l;'ëSó\qú_Ìêîùh9À=„õù½~ ¼“â'Hñ$ƒkIÆëX¾ÊÇ•Œtú,c•窨›©EýÍXºJvƒøétB'¯‘"aº˜º”: áÔ—BGgg6Îyk.Å»À‰¾Rl]ƒqJøä{ªÕ±Šå&JTHí¾ýõíÀt0Jt2^'-ÞÕqTÊɾ=±5C”˺ŒbY»=½]à”&ú°±)n‡™îú:~Nåéoê¼±±…-ÑpÎÙõ['uu:i)i²±AYѺ-×ãè@Ôz#E€E®,Š)q0qˆ¿²âc­UþÐÎîõ^'*6zlìø ãwYZëßÒ"‘ZVyZDZÂ6Ðh°ò°¯<—ªÒÎ.Íd»‚{P¹t/2­UᾕÏ2S¥‰/·L5P¢J»i¬Šß²TKTwÖ;'%ø.ä·¹—žûd؉ÜÕ\k3¤¶å¶ÌAGs¼ÄX}$ºêÎ[–UF{ˆïLUû›]óé9[U%²êy¼ùLy  Õ—67ûõûC›…|G >D‰:Ax,¸ï•Ê76£È8fdÁ!†„)~°äšCQÄ<pÁ¿sœc‘jH®ò¼\àXUF"‹.Ôß >(PÔi_;êê(3OBêäty@BºÔ¼ésƒ`ÌîɘL¹Ç»¼ÀË£sš§ï%œri;£ÁØú»(»Ä}›4¡b2EV[UÉlbc—cšÈúŽeê"*íÖdÿM¾a9e¤÷h õŠ™ì¥“§I©ÒBXüU’/í¬¶¿ËX¿xúf ØJˆBÕú°t+¥VùUÇßgjeS‰]ª!Tª}Z8Z•£x12¹Ó·9U¿·X[¥µIàe€6!òR ÙY)馺ã¶äA]».ØÍ´,}1½ê|1D{ š5”0`W¢ÌKÓ· !®äŽfÒ QÎ|áytÁ½Üæ& «œgƒiuuºÍäTK8@5™“‹ºæÌYZHh%Ùe.§Àèi1. =­)}ýw—  eäRGI¤£Q;èIþ`eÀ,ë_ÞþðÔïðÇë«êݰ£e‘h«ŽàûéJLâ5 Œ<ÒOiõ ”oT<ºWÆTÚøn´n»r%[Àrº%qžšYî?tše7Ûôz–Mu&ŸšmÛ¯ƒ.n§L¯¬;ÖöÑí­Ñ÷óèÌ¢­„$úÉôkYZ÷•:–9äëÆnZ‡=²sC¦è**jcãaøšàxƒW!Æówì§!-Xpõýh°m•žß@¯õ)˜®°ù醾· bB}ÂÙŠ h„bïáê¾ÑFýV>™xw?ñT#B°ÚòòH…¡à”´'jGDø+ 2&ØÊñˆ‡|êö0Þ¶4Er²üÈÚÃNN,Ma5-ÑC$ðíÙŒZ_L&vxdM¾ä–­ ¸64ßœž =a@Ãwµ®Ô;¹î@Æ¡mì ˆàÌózºÉX8Ô HÆÄß¡2O©¾²Ê ?ë5ëiIݸª¢K{ªUívµÔëáW%Êj¾Ó@±uË´Œ6Ô`èÑõÜà­Ÿ-ê#/4Ÿh‰8è@-G Ü|„t‚eцþ£åc4ÒÖæ«çßá-XŠRE«Ê<õéc%'…«sÎPª oLbC‹á3MêÛ¡pwîÿv¤«r –)˜7­8¢ùˆñð £½¹Ž ] | ß&|çî¯Ào3ÃT`6 œÇ¸/æVõ=$˜ ‹6·ÎDà‰pÎëq¹çCEÁ¼¼ wÜŽ/ø­è1‡…›è§ð$”RÆÁÏÁ“qZÅ™œûÒ¸ Rˆ ºp¥¹Ïõ™FÌs3“ÝMâ€~’„âž/ýÜÝ0êÞ —oæ?ܲ¹Õ aòE‚4î"`‚ƒ…»ªæjÎ}~ƒwuÙ"©>ŠË `3Ì?#—ý!4tõ”…>rç ñ"ûÎÉÆÐÃŒìÑþŬížÏ–ØCXï¿´Â?# ¾€â@ñ]8×’.oÇâ5Ö—à#Aà0a!]4ºqÐÎ[öMÓ6‘Ï9\½±1` p>Ã,¸‡˜xèqïfóm0| äÃ…Wü…ÃîŽü¿´ËݰY„ë$âi½²þÍpgœ³E¸›ÛÈ{ž·¼+ŒlËK´â‡Ùk”»=>í‡g§áÛ¤˜ßpp Xj¤wÿؽçAsŸüqË^8M‘„H1g”¬†"páY±EæQP®0  BË÷Á‚7ÏçÌÂ+<.ž½Ã±9?T0“ŽÞ :[µbq´óñt—½ß o=•õ¯¹ÝSÙ0t8 [Ÿ5/e!F„Âèå—2>)ê·ÞÀ°Ç}Œƒö låDÁÔ+‡@xaÑÉãײË7~` N)ã+Ü Ü ë`«“€Xÿ² <B°~´<Ó½ ÃyÞÁ´ÍfýkbØÝ. Ø¢²À-ÎÛÞ½.6ícËhÿ?š·bøÿ_e.ç%ƒxœíYë£Fÿ)ÿG¾ìèLÓ/ q<én%ÒÝ—$§“îË CÛ&‹ií±½}ªy ž™U2›l²ÌìÚTU?ªêWîY}sÚgÖ£,«Tå÷6Aضd«$Í·÷ö~úÖ¶Ué(O¢LåòÞΕýÍ×_¬þæ8Ö?Ki™XÇTï¬ïówUÒz³ÓºXºîñxDiKDªÜºw–ãÀP\=n¿ü²,X;¯–I|o·cŠC™Õ²IìÊLîe®+— âÚùø"›¤2Vû½Ê«zh^}5”.“M/n¶tdµ ÃÐÅÔ¥Ô §:ç::9WcaŸsc)ÆØÞ@ô…bË ,[À¿^¾# JÊXn` D¹ÔîÛŸÞöL£D'Ãy:ÃŽÖY;ö²*¢XVnGo&8¦‰ÞÝÛ7¯;™nwúòþ˜Êã?ÔéÞÆ¶¿|k¥.ˆ! %MîmPV´oí’Ë^£"˜'‰|ÆŒ°…E1 LÒNÚ©»LTl¶Yªµ,˜F§q”¡Þžý òT¨R;›4“Í w§öÒ=Ë´R¹ûV>ÊLIn‘j D¥vÓXåÿ‡©%*òó’¼úóÜsÇ}0ìU"7U-רÁ¼RÛrf¯–Ù^bì;]GUëË*¢- 9Så½ýÕ¦~:ÎZ•‰,;ž_?cžW§úÜ„p7·i3q/€oT»(QG„û^©=Ð9˜z¡˜ðc@‹¦ “.¬)P臂“ <}0¾qyª!ŠŠÓtü¡,D%h_ôU;uܖƺ<ÈÉØcšƒNN yÒ©ê­Hc~KÆÅ-Þù Þ>:¥ûô½„]N-`4šeDܶJ•ŒßÉr­¢2¹XÛå&²ºa™* g½6>Ë7,§ˆôîÖµ@®^°‚#“­töiR¨4×Ï‹¿Hò©•Õúgë'w_Ïk@vQHPÏKW{¥ôîy•_´ým¦ÖQ6–ؤ RnÓÜѪàiÀÈäFÏsÊ¿s¬µÒÚDð 5D†0xh$V ®v”e鳩8§³!Ú=ÕDŸ¡„¿å¾0Õ§n#Ä…ÜÒL$!êsO0F'Üó<75ä5ÐMÁªÒu&Ƕ„ äP“k²±~;Âì9Ks e";_Ë)°gš#¾£Õ‘ÞåvwšÜÆ^ê(‰t4HõÉë­ Çò‡·ß>t+¬âxù_U¾ëW´,#­Õ\k?\è«$^B¯°ôCº‡T`úŒ¿Ck°r/Œ±´ñÝ`ÞfæR6mÇl–ÄûÔŒrÔi–}o–éôL›êL¨+·Õ¡ÓÑ*¹r;#4¯Ûk@fÑZBXüË$ykš,·¥:{ˆÀ¶ØóŽ ƒ.£¼2†0Ž…¯tªoðÂ.X0ï®÷ÂvŒdÁƒ‹ËÓ€IËôôʧGqÀY¸Àæ§}õغ´êŸ/¨€f‡PÌî.^,Ô-å‘‘S·#Õ"{¡="O·DP Ÿ’fGÍÞ‚ˆQÎ_8Ðo!2Âï† Â’&í¦X»_ɉ¥I•¦È1DϾQé³ À¶ÅX’¯¡Ë–9êoNÇÀˆ‡LЀ„_WºTïä²ís0n M©A@÷Ìëè&PaSK@ž ‰?C®Sµ²Ì âê%ïhIõµ,£s³«Um6•ÔË~%в¸SwWˆim,ȪPu«k#€·þmQ±Ð< Ðù µpýé Ê}Dkúw–‡QPK[ÿ»žÍ8ßøF6aõ‘ÊÁ(Z•ôH‘>”r”¯Zçô âÆÄ3¦žq,ÏCáæØ_·¥‹r–É“³VÐ<8G|§“©ÑžB®c Ëa‚Ïð­á{íþüve˜ÌæA‚cÜ÷ĵՀG=†bbQ¨›ò¹˜¯y]§Í<ÈH"¸Noýµå =f³"˜bý< ©”ó^ðSðdœ–q&¯}iÜ!ä :q¥9¢u‘F&ÌS=’ß â€~”€ò™'ýÔÝ0¨Þ¥Ò¦)q8ô"36w [!,àà<ë.6!8˜¸ üL(ò=†wqÙ$¨^Åe°9ö?!—}P7tÔ”‰^¹r†x]åäÃÖü׽Gó?æMõ|´è=„õþs)ü-`ð¹)~¢)¾ÙÎÁ±¤ã</XŸ¶¯Ô?ÑÒI¡«{ ´cÓºiÊ&ò|Ž^lô=8ŸcÜ긇üùl¶øÖ= >JäÁWüa÷Gþ%ír6¸ŽÖÓõfáÎ |ŸOànN_#Ÿ16=+ l‚åi·⻫K(w;¸|Úö×Ný·Q2Ÿ88†^jŒÒ¹èÎó GèûùpöÓ.6M’„H±Ï)Yp I2ð³b‹@›GI@q¸À( 4-ÏC ¿¦0Ïç^àaòì|Íù\ÂìM:¹ cƒÜ4s¶pˆùÅ õ5½|Þ}]¤Iœ!Aa@ÃÑýF4™ ”[ª}΄ÿC6O‹Ýyfy:‚×M$$Ç€„nÉß/ ãáÚÎ"-g]¾O#èîjœš ;Ðó„k9L(åôcá™ÑÏxþ„µœuéËað+ðÜ\+< g†0#ãË¢W…³ÿg‡s×”4Ÿ+ó7;øüÐ?Ç$äxœíYÝoÛÈ?àþBy‰Qq¹ß\*vhƒC \_z) ô¥ È•Ä E äÊ–Rô¿Ù¥HQ$%;mr¸ ‘a›œ™ý˜¯ßÌ®î8lsïQWuV3‚ðÌÓER¦Y±~˜ýýý¾šyµ‰‹4ÎËB?ÌŠröÃÛï¿»¯×ßçy /êEš<Ì6ÆìA°ÛW9*«u&ÎõV¦"Á¬'Ÿœå“JÇ&{ÔI¹Ý–Eí†õ«¾t•®:ñ§§'ôÄœ‰¢(À4 Ô ¿>&>øƒ±°Ï©±c¯'úB±E ÆÙÁo'ßP]î«D¯` F…6Á»÷ï:¦QjÒþŽ|νd_›r{׌nõ^¤ebõx˜-«¸H6~¹ÓEM»UôaWVÆ_e¹näƒM¹ÕÁQguYïô£Î˦`— Ä• ²¤,þ•gF£]qe¾CºOErš{l¹o-û>Õ«ÚÉ5¶°¯´a³6Ç\Ÿ¸žgŽÖsFLÔõ¬£Ûqɾª`£~Ræeå×ɲ ÆóП,ùgGõßÃÞ¿»Ñž²xµ¢öçMKÿOûp¸]|ž-ùìw¹)ùi»ÂX‹Õêö®îëÊæ± Le©M•žÇ—q­Û=îâµvk<Ì^­Ü§å,Ë*ÕUË“îsÉ+!k3slµ¿=;q'€¯Ô›8-Ÿ §GÜe¹º@¡œË?Ä'…\QEÆ\X3BBLØx,$ëÞæ˜¿/2ˆ¸;Œ'89-Ô_+Á[™zS>­+kGSíõhäSV€Jþ ¼HDÇšŸDZ@#ók2Þ®ñŽ7xÛøm³v9¶ŽÕ oýUœŸâºM\¨ltòAWË2®ÒÁ@g—}–êúŠeê"ÞùË¥…ìI¾eù»Øl®MàŠò+ø:]k›¥»2+Ìóâ/’¼µr¹üE'ææîݰԅRó¼t½-K³y^åm—Ë8¿”XeB¥Zg…oÊ]/žzŒ\¯Ì4§jâwе,± <P"·Â ËΪ4±q%wåé< dà~GK¼_K‰B~&êíÎ6®ñSgò‰fÓ QÉ…bŒŽ¸Çin ºêa6Øþ¤Î–¹¾Ô6PÄ@M‡dë¢Ó»ç<+4tùq(W‚ѳ¢ -ÍÁœJÁ¸4Œ­6q›¸WZ’è¬ æâoï~ì*Ô}’,þQVÎeɳ"ñ²ÜƒÿÏ•Ì6ÉZÃmlÞf[À ÛVþ:A¨MãRÚú®7o3s¥›.s²ßN“mfG?›,Ïÿb—iõîM›(‹nÙæñ\1OÊ´Ê}mïƒÖÍëzy¼ÔD?Ùºà¡u]•ûÝòõaæJǬggGè†hkkëaxÌc£_ã¹Ý+ ±bâ®sÇú2¤ϾïM¶­²Ãk¨µ‚â³hŽíÏéU°9tç&TÉçTQÄ Åìîì¾ÞBíR‚\xw}á)'B°ˆfäñ–Š"%)ivÔ¼%æ$DŒr®øÜg„!AáwýaI ’Ó÷¬Ý­ä'Ú«-‰ ‘PÌ#\“ô0;õ# òð|QÀYÏ=ù-#1EC½©MU~ЋSS„ñ‰ÐvÄ!œš8c-Ýf,ljP¤}â/€Ì—T_]åPŸÍ‚·´4†j\Uñ±ÙUZ®Vµ6‹ng%v1`¾ïZ±EÃô¬6`0ÔèzhðÖ_=è¨Xd?sÐIÐz¾BØ}”öÃ9åQGÿ³'0 ´÷ÏálÖùÖ7J±«ë¢ÊŒb í…~ê16ûJ_×É9TAÞØÄ†2–Àç2©§CáêØÿmKgåF, ˜“VìÑâ`<wS6÷­V‘%ˆs›ŽÜU¹ƒ»r‚wvÙ(©¾ˆË°9–_‘Ë>©:ôjÊÈG_¸rFx”måäýÖþ»Þ£ù‹yS==zå}üV ?G|kŠo4ÅWÛ98–œ‚q:ϱ>n¾P |£"<¢£Bçz Ú±qÝ´e )áè5Š®çsÌÃk=HFL²Éâëz þ&=€¯ú‡Ý ùi—«a3 ׋ˆƒNëvÈŠÉpç …RòQ¸ÛÓÇH2ÆÆg…^‚byÜ-Eønp¬{—OëîÚ©{ºóé 'ÐK]ÆiÝß÷FëyÐ#’RO÷g7}ï†Ó‚$` Å’S2ç@2”Šy‰G Í£$¤8šcFžˆ`%… É=<Ç}ðl}õÍù`v&í߆ݾ”ëߺù¢Ñ“Wo¾ˆµas4R^½{nå˵ûô+‹KY8÷yÅš*q÷ÜÍ›ž<§.Þàl Ç\zº T8!VbîKÄ)‹,'”Hc#9¼y[Oäfÿ‹°ëËÚ;>Uƒ¸;G—q+Ø·•VÀAiN9ô$Ÿá`Õihìò¢Éʨ¤Ñõð' '#Ø[KŒÏ’%};9 ‹Fuþ|wg#›h„b=C#‚Z ‡d„„(R||RvXÆ¢R«aÕ™î/ÛSÒÂNòQUi¾Ù>ݾ7¬æ0†™¦™[@=N<‚ fC!%™"Üó/ #¸îŸ"'Ö¹Òj tµ_€áÿ(=I в©/bR*ÌïÎ_òØåïíW,ðÿW¤7^T´!0xœíY[¯Û6~/Ðÿ U_rP‹âM”äúœ»AÑÛ—¶‹ö¥%ÚV#‹†DÛùõ;Ô•²ì“StSlÐ8HbÍ ‡œûGyõíy_8ϲªsU>ºaבeª²¼Ü>ºÿúå;/rZ'e–ª”n©ÜoŸ¾übõ7ÏsþQÉDËÌ9åzçüP¾«Óä 7;­Kß?N(ïˆHU[ÿÁñ¦>¥Hxõ¥ÔÉÙ»Z ç¼µ–bŒ}àY¢¯[ÖàÙüä{ªÕ±JåJTJí¿ýåíÀô0Êtfëé;Ùwâí2ÙËú¤²ö{z«à”gz÷èRÜ>îd¾Ýéñù9—§¿«ó£‹ìˆÆDÁÇoÔ˜1¤¥äÙ£ ÆFÝS·årÄ(¦ôd‰ˆâ”¶p(&±‡‰G:¥½¹ËL¥æøî¦P‰Fƒ­ò|P•ö6y![A§öҿȼV¥ÿV>ËBLöø‡\%©´Ÿ§ªüµÈµD‡ò޾sv€ÈÄâ6÷ÒsŸ {•ÉMÝȵ¶›Gê:~ËL1ÇËŒO-ÑuRw±pœC²…ì-Tõè~µi>=g­ªLV=O4Ÿ)OAxs}i˶×ßÚ(ðz—dêÁŸqß+µ:C<Æ1ã3~ B(bQ²`Î…=CDeA(f\ïÑÇ;–¹†Ò9œç ŽUe$Šä"Áüæ?ÒKÕ;uÚVÆ“º:ÊÙÚS^‚Q^—ç$¦sÛ;‘>÷ Æs ;S ÷x—xûäœïó÷NIf2ÆÛÿ›¤Sâ¾WšdÙÉô¬Ö*©²«…_Žy&ë;ž©Ëäà­×¦ºoò Ë;$zwOA#PªWìàÉl+½}žT^ê‹¿J²Ûybô ¡Ö¿ÉT¿xüF l= D¡-}XºÞ+¥w¶ùUçßjS‰M®!Wªm^zZ¬„²…ÜèÛœªMà[¬µÒÚÔðåþ`fN¢‘ÜÑL)!*x1FgÜËmnfÈëL7cªÎ×…œúP&@Í®ÉÆûÝ sæ"/% Šâr-§ÀŸyi—|OkJ½ïîþ¼½·Œ½ÔI–èÄjö=)¼ xcùÓÛïžúViºü·ªÞ ;:ŽIÖê¡uŸFú*K—€ö‰~Ê÷Ð ºøÁÊSi;Ko«¹’-ظ »²tŸ›UþÏ:/ŠÌ6½Ý–Ú\Ò¢®üΆÞFß6rå÷Nh·× Y$k eñOÓåy·ÜVêxØCvƒÀµÜ; ºJÊÚ8¾€Oßà…Ø…8bÁÃ…í4“#Ž!·Ô€K«üüh@qÈY¼ÀæO÷°`³Á4ˆC(fcÔ¬ú­2 êv F„à v'äù‘ŠãHPÒž¨}"Q° !b”óˆ/<@Y( Œð{CØÒ´½‰zËÛÃN^*M«4SŽ!îÕŠZ_Lv cI¾8V,K¸&4ß¼žC°ˆ†$þ¦Ö•z'—ÒÁ¸#´³q˜™3ÖÓM¡Â¡–efƒ^;¥BÖʪ€‘«—¼§e تJ.í©,ªÚlj©—ÃF# tq¯ÁWË–ékèª0vëk'@´~th€Xl> ° °:^„pó‰¤.(ˆ6ô‘vþs­ÍßÄ&ŠØŒ5@#U‚S´ª<Iω>VrÒ¯ºà  êÆÔ3 ¦>ÓZ¾ w×þ±#ÆÍX¦OÞô¢E àöð=ÜIæN{)s=“º|Nß&}¯Ã_AÜ®SÛhpŒ‹ ºöðhÀPÄ£hæÑ³ý‚Ã Š¯y=Ôft¤(¼noõ´ãGâVö˜ÃFá<×?B$¡•r>~ ‘Ló*-äu,M¸ „DDg¡4—´¾ÒÈŒynVò»EÒ?¥  ¢ˆ~êa°¦w¥´%,rÃçt+„£XÌ ¤ ŸÎÂU5·qˆ¼1d³¢ú(!ƒ†Í±ø„Bö»ÐÐÙš)³}äÉãY=ö““ÛÐÃ<7Ø£ýóvz>;`Èyÿyþ/Òà3(~ß…sp-é’ñv.޹>‡ ¿€élÐ5H€ul>7ÍØDpõšåÆ€ øóðâ1ìæðm0 ÿ À…7ú?N»Wò/é—»i3K×IÆÒz9eƒ›éÎ# Ágénn_#Á›ß¬›åò-Åøáê%”¿µ^>m‡×N÷I3¿à°Ô4H~;}äÁŽXˆ€üþxê­›¦IB¤XpJC“ EÄœÔ!ó( )Ž…„†±ˆàH4îà¶›gãØvç‡æàÒiå¼¶fˆU*YÈg…t?~ •2ÔµºrÿS)ëˆDÄï¦kä4/¹E@)Do¤š‘3$Ø[²UûÎ;d•¸Ž+©©NŠ˜°'¥ùYÅpÖIAÞ?î}Ç®aÙñJáe¿é)+óªþÿ/+5› ÒxœíYYã¸~_`ÿƒ }™F,Š—(ÉÛî‚Á"’—dƒy d‰¶µ#‹E·íùõ)ê²,Éž^$3È ãFw[Uţ꫋ÔóOç}á¼J]åª\¹aבeª²¼Ü®Ü¿ýò³¹Ne’2K UÊ•[*÷§—ï¿{®^·ßç8 /«e–®Ü1‡¥ïŽº@Joý,õe!÷²4•OñÝ|z•OµLLþ*Sµß«²ª‡–ÕCimzñÓé„N¬–"qû˜ú”z áU—Ò$go4ö97–bŒ}à Dß(¶¬À8øíå;ªÔQ§r%*¥ñßÿò¾gze&Γ—ª49È›u;bc†d/«C’ÊÊïèͧ<3»•Kqó¸“ùvg®Ï¯¹<ý^W.v° !¿~k¥® “†’g+”Ú§vÉåÐ;uÞÉH¤Qˆ£0^8Sâaâ‘`á¤ÇʨýS3ºÓ{™©Ôê±r ¹1‰Öê„zƒöKÈóAiãmòB6ÂþNí¥‘y¥Jÿ½|•…:XWò¹J¢Ÿ§ªüg‘‰åùÎÙ`ŠÅ<÷Òq_,û9“›ª–k a©ëø ³WÇn/³ˆ®“ªÆqÉ\¹Pzåþ°©?g­t&uÇõç–§ëÜ\š0ìæï6m'îðj—dêž0á~Tj¿ryˆbFã);o!1Gá –ŒQÅÇñ„ -6Þ±Ì „Ñá<ਵ•(’‹í뤓ªvê´ÕÖFådì)/A'¯õyÓ©ê­Hc~OÆFÅ=ÞåoŸœó}þQÂ.ÉDÆj04ÿ&)®qß*µ¯ìdúAêµJt6XÛå˜g²ºc™ªLÞzm#}–oYÞ!1»{Ô¥zà žÌ¶ÒÛçÙAå¥ù´ø›$­¬Ö¿ÊÔ<Ü}=¬é D!C}ZºÚ+evŸVùMÛßj·›Ü€«èm^zFþ4`ØD8ÏÑÿαÖÊÁS­]ä‘ôñ©•ILÉq“ó ±]§lG:Ž¹Øºt¾X¢ÛSmˆZJò+Qî¶FÕýBt%·4nˆ DŒÑ ÷2ÏÍ@W9Ž[Öª|]È[-aeÔlL¶µ#ìž‹¼”PKŠËXNÑór˜:ZºàO+@ÃØK“d‰Iõ #½•¡?YþåýÏ/Ý Ïiºü»ÒúÇŠ$kuüÝ—+ý9K—ÐQìó’ï!_ØnäwÐ@<ûWÆ­´Ån0o3³–Ms2Û¦eé>·£ü¿š¼(þh—éôL››B¾ÔË6_{]üV™NY¨í³ßY£yÜŽ½³HÖ‚èO¶$8ÓÔºÕêxØC¼¶UÃØù¶Œ”•µˆE¾‰‘ïðƒ¦A·Â‚§Ží­KG<¼b?˜l«óó;(¶Å!gñÛŸö1` hêbLh@_Ј"N(fOWø uKäÝí RµÁAìÞ§["(Ž#AI³£æ‰DÁ‚„ˆQÎ#¾ða( Œð§á‚°¤M’7Ӭݯä¥Ò&V["aàŽFTæb#±mH–äGhÝŠe G„ú›×10â1‹hHâ+£Õ¹l»"Œ[BSØA‡ÐlsÆ:ºXØÔ Ì†Ä_!3ßRÁ}¥. >›%ïhYÕXëäÒìj@U›M%ͲßÀU‰C9ß«{±eÃt¬6ä`¨ÑÕØ€ÖŸ ÛÏ´Dt Ž!\"é… Ê¢5ýN€QXK;ÿÏfÁ·ØD›°ú>J•`£´ÕkbŽZÞ$®œ>UAÜØÀ†2–Âç6¨ç]áîØÿlKWå&,›0g­8 ˆƒñðŒÑy®g]—ÃßÜ·vß1üpFƒÙHpŒ‹ [ x4`(âQ4±(Ð …,ŠÇ¼®/gd¤(§·þ<Ûò#1ç=v³pêüHB*å¼üLsrŒ¥… BHDt¥=Ñu‘F&Ìs=’ß â~‘€,ˆ"úµÃ0¨Þu.ßy×½Góó¦z¾:ô‘óñ[)üo¸Á·¦øAS|·ƒcIëŒó¾xõõi ð™Zà=á1ºº ›ÖM[6Q ½&¾Ñ÷@>Ç<¼×ñ‰˜ 6[|눅_¤ àÀý»Ý€ü¿´Ë]·™¸ëÇA§õØeƒYwç …àw·§/Ž‘`ŒMÏ ƒ›øò´[ŠñÓè6Êß.Ÿ¶ýµSÿí&™Ï;œB/u뤃ˆF‡<è ߎg?ýà†Ó&IÈ NÉ‚cH’¡ˆ˜“:Ú image/svg+xml  #ÏxœíÛŽã¶õ=@þU^vPK")Š]ÏhA´/mŠ} d‰¶••EW’Çö|}©ûų»-&í"ÑbwÅsáá¹Ê›o¯Ç =Ë¢LUþh[Hæ±JÒ|ÿhýýÇïìÐBeåI”©\>Z¹²¾}úú«Íïlý©Q%tI«ú!ÿPÆÑI¢w‡ª:­]÷r¹8itT±wm+0—Ïû¯¿Bì¼\'ñ£ÕðœÎEfh“Ø•™<ʼ*]â×ÐÇ=}¬O>ËX*/ k^~3¤.’]G®tñ B¸˜º”Ú@a—·¼Š®ö„ιÄK1Æ.टH¶.Á²'øÛÑ·§Tç"–;`”N.+÷ýï;¤¤J†û´†ÉY;޲ÍíJ5@drW-cŠ:€—P[UU:…çjbdO5Ŧ§€ìj¸ªnºï\ohuP~"Öåñ¤{&ÂÜÀt*9”3?ô<:ÃÞ–± ¨!§‘®ÛV™n39¶% šLÁÚú ‡>s–æúDv›Ò)°gšS¾…™To‹»;¯î5â(«(‰ªhPë[ßYæõ_ß÷ÔJØÄñúªøÐIDH“D[u×ZO=|“Äk˜ŽQõ”¡èiã÷0 lÜ1¦Ö¾ì[ï\ÈzøXÃ’ø˜j.÷oUše?h1­ÞƒmÓ*“OFlýÚéâ6ʴʺCm7nkz¹ŸFfm%äÇŸu¹Gó²¹/Ôùt„Tl:‚5°ó¸ETE”—Ú"ÚÃðšÁñ¯ljœ‡žÿйcßú   ³• ‡ Œ}§A-ÚöÐ; õvÒ…gà˜¾ñ©<‡Ú¤ ZàsT 9ŠÆZb$Døl×±†lÆìœ®¼:Ðûˆ@1‚y’ ʺÂúݧ!ñØ €ºC æ=#êphV¼ªyCÔӆС C‡[-Ð ™·²[CHÍùŒìÄö¸¨åh ½î¬^zAGD°´Bº|4çÐuÄœ„0î7'©Ë b2èÑ(Fsê…úN€=aÔ"~ {NVÛÛ°U»ÍTó7h%7h£×’íö„SV^†APV7]›¹oMþr¶†Á2 6 »Ç•U¡>È[/ë¹iÝ-u­„È\—ÿ:G…B†~·†b!‹jŒ=Õšµ°$‚!§(¢Û:‡ åªv»RV½¤þ`§:©mFÜuDúè:Œ>å°Mé×a€yÚûNÀ”™¼¡/˜ fw¸À$„~ã¼´jÇ }ŠG@? |ÑÍÛËRõ¡—ÖVÀúæ •\·˜™âöé}»q÷óm¦ŽÖRцöW¤×wp׿‰Öš¥oj†À„ú„³;1B±÷0¨Êû‰¦ûÐ'£¼5SCB°/¬x~$âr(ÆæDõŠ„þŠ@s¡Œét†24°‡¡Ày€ŒÝI²c©ÇZ}#ñøÖ„c9-L@Žr;L@D ’C?xš0Ä›çMóztžÌÒdý¤I“*AÔw<¡ŸhépÐ";t°yBi+ =”ø÷ȇ®¡Fÿœî¦¯}†Þ õéÉðZBŒç®åP¸Ëûß©Wn†2gÉŠ˜ï00^0Úk‘këÐe°Áoá;msÆýºîN S€Ù|(pžž¦Võ='da8³(Üq8ÂÀ Å×~ñ|=bÓòÖ}Rlð!_Š}Ø0˜ÇúxJ)cá—àÉ8-âLN}©Ý)ÄC:s¥þ Öf™!¯†“ÝMâ€þ" Å=? é—î†A÷.T¥¯€0]=,Ù܆jåàPðY‚w° ÁÁÌ]àgþÏp½ËfIõ&.ƒ‚Í0ÿ‚\öYÓÐuÐSf>zãÎ)ð,ÛÎɆ£‡^›Ù£þ&xÓ=aN‡Ù#ßo_\+üÜ0øm(~e(¾;ÎÁµ¤ ÆåXìc}>¼ÑüÊ D˜ ³Fgf Úyó¾©Û¦ãsW¯Ylt38ŸaÜ›àâÍ…Ç½Åækf /øEf .¼áÿqØ}‚#•v¹6³pELZ¯‡¬¿î,tÎÙ,Üõí‹a‡{ž7¿+ lËóiIà‡É݇œÑGþmTÌï~À‹a–ºóoèÖó ‡àÜ'ŸïÏnûÁPºHB ¤˜3JV C‘ ¸ùXK`Ì£$ X¬°äûÁ!7ÏçLJÏÖÇbhÎÌî·'m¶þåþÿ7"9¡#âxœíYYÛÈ~_`ÿA¿x°"ÙVffÀX$@ö%Ù @^Ù’¸¦Ø ÙIóëSݼÉv‚ÙÄØ¥a›]U}TÕWGSß_¹õ"Ê*“Å“]d[¢Hdš»'ûï?ýàD¶U©¸Hã\âÉ.¤ýýó·ßtL¹©J‡ëdÅÇ*‰b´oK¬ÍDuŒQy-½^àœ¥jÿdT÷"ÛíU?~ÉÄùòòd# Y¾K8‚€õoTït\S²ôÉe£fÔl¹¢Ã%Ö{I¢(ä+‹ ‚„쯬äT)yx¨g·z¯S™h=žìd/’yù9+R¡DyÈŠX ÁúÊíŒÜm+.GY*g›å¢^ÀÛ˃ð®"«dá}/"—G /ï˜) Ä¥ò²D?ç™±Þ%=‚ëx°Ì½¶ÜgÍ~LŶ2rµqôØ–W3;õñRmôè&®gYÖ1Þ¼sY>Ùï¶æi9Y¦¢lyyÆ< þÏÔµÍvýöÐzáNݨöq*Ï€Ž÷UÊЉ‹ eŒÎø @ˆ»aÄü9KŸ¹ŒbäÏŸŸ´gœS‘)¬ãe¾À©,µD_ènþíTµ—ç]©Í¨Ê“˜Í=‚äÙi¢s2W¼i##ÄnÉè8¹Å»ÞáâKvÈ^œÏd´Cãoã¼ÇÃm«¤è8åFÆe:™hìrÊRQMý¾E|t6ü‹¦Ó,ç«}uO f‹EnG¤;á²ô(³B}Zü³$ïí,7¿ˆDÝ=½Yö€Œ¢´>-]¤TûO«üYÇßårçc‰m¦+å.+%@ ¹ØªeNYx‰µ‘Jéž#Ô`ä º-¥Š•Iî¨Ny×úe ›™–¥®ºT]®šhwT£šÂCÖÅá¨Ë–i!¢žÜÐt¼¹$`~D)™q¯ËÜtÓpЕ®Ê6¹k (b ¦S²vQ3CŸ9Ï ¥$¿Nå$=+†y¡¥™|Ðæo^jÆA¨8U<(-Éï¬ -Ëú¯~xnwxL’õ?dù±ÛѲ´H¼‘'ð¿ýÜÓÓd MÆ!VÏÙ†nP¾ƒžâÑëcií»ÁºõÊ¥¨û•ÅÎ-M™žåýMeyþg½M«÷`ÙLåâÙl[¿vºx2­²ÞPÛG¯µF=ÜMÑ™ÇAô]¬ynÝ•òt<@¼6eÃØy\GT•¶ˆö0¼æÐp¼G+ú ê?tîØõ@_šÄB—úÜGlåDÜ 8Bœ<ŒÃ ÞÚ¡ê}…êªÁ -N¾~G( ïý¨S×Àk}é”EÙM–Ñ—XJ1‚j}œD0÷Ù®h9,p ²¢”~N0· úSÂI’Òï>‰0e+ RŸ#Šy±^,â,ÔSѪžY8i¤˜5™Æ¬ý`Z½!… 1ºrÚ ‡”zæ‹å vlkµ3ZAÚ•ö¤Wë`a$£Î-Í9t’1'Á,ð›“Ô¹1Žô oJ£¹™ª75òÝÎd¢ÞÒ(à‡¼±·™Éj{¶j—“êùÚ›´Ñëö„Ó©Üz‚ ]Ó7®ñ†ð3§çUª”ÅÚÖ4d¨Ö׺êD È\Wÿ:Å¥Rй†L"Ê–j94NjÍZZC›T–ñu]ÀsH•Ûm%T¿S°c µØ1-òºfZúèÔFhžªa²šÒoÃó°÷Ý0„ -3qÏ#Ÿ3f wÂЄ~ ‚h5ÕŽù"»~úÑÍÛëRö!€Kû?K`}e…4¯ët]Iûô¾}ôvóümZ’ˆ…öRF‡ÚXf—÷pWBFù é?ÍÐ79ƒ#L|°;1L}dåÝDÓ]äãQu 4"pIâöˆu#E3‹Â(„†# iħ¼öà õu‹NÓ[÷‰²áGÁzôa£pŽõ7ð$¤RÆ:Á¯Á“IV&¹˜úR» B(ˆÈÌ•ú“\ixƼ˜™ìf‡äW ¨€úQD¾v7 ª·ù€"ÞCwõ°ds²•‹"Ìĸ ƒM0 gî?chöý`×»lToâ2HØ _‘˾¨º jÊÌGo\99šÅc[9Ù°õÐcÓ{ÔÿBoª'ôéÐ{DãæûÍ`ðÕ•Â/…ÁïMñ¦øf;×’ŒËXì±>oÞ¨¾ÓaÆÉ¬Ð™(íè¼nê²éúAW¯6ºœÏ oõ@pñ8 èbñ5= •ȇ oô »ÏpäoÒ.7a3ƒëqÐi݇¬¿w¹a°Üõí‹!7 ”Îï ƒ›ayÞ-qô0ù5¡û3ú¨Ó¿’ùxÛm½ÔxCo´ž=xøøËýÙ-?ø…J'IÈŒàC$ÃÀ|¬ÅÐæÄWÈ 1 ¹åû.FQ`(Ô˜þ”¨Ôë=Ú¯¡Ž;]w•åªæoôVù'•Uºðß«g•ëÅ’¿Ë`&?.Ÿ%0ežåíŠóÓÄ)×µ§NûdÕ©ZUµ]ã ûHä7Êþ`v{©õðÀtWmdÚÅkÀr®Ë…óÕªþtš¥.SUv:QÆ: ÁÎÌ©IânþnÓvâÞß0¨6qª…‰ö£ÖÛ…Ã"O]X‘‰¥\LµvCÓå Â{w_d²gwœŽÜ—¥µÈã“‚3׿HgUmôa]Z÷™r¯&cY'q[¨“ˆNwКtð'ó[66néNwtÛø˜m³ vI&6öC§¯âüŒƒÛ^©RgB¹Ôq™^ ¬ý²ÏRUÝðLUÄ;w¹´ ~UoUî.6›[Ô…~Å ®J×ÊÝféNg…yÙüU–÷VÖË_UbîÖ€ª¦P˜^¶®¶Z›ÍËG~Õö×¹^ÆùØb•€J¹Î ×èÝOE®V溦lð{MµÔP8·×ZCdƒ§ÆâñlÉÕŽBÈœl§9ž¬Ðé¥6û¬$ ùY¨¶;Ûujú ÏâVf3É£‚’1:Ñž®kS8†ººmTU¶ÌÕØ—°"iz)¶ÞoGØ=çY¡ 9ä§K; þÌŠaÆw²:Ó»ŠîOKz£Ø*§±‰¾½—qÌzÿÝS·Âc’Ìÿ©ËýŠY“x©÷Zçé,L“9p„mlž²-”Ë/þ ”àÑ?+ÆÖ6vƒy›™KÕЫÄ+M¶™åÿÝdyþƒ]¦;÷`ÚÌäê©^¶ùÚŸÅoÓÖžöÑï¼Ñ<®/‘™ÇKùñ7[íÑ´j®K½ßm!Û†à ü<‹ÊzÄF¾æ@Ußá™ 4Æ ±dÁCŽõÒ’‡çئߖÙñtÏ€â³h†íOû°дÁgTÛ!³‡sø uKdÝõ(Rµ ÁAäŒÄÓ-/Ф ¤ÙQóDd0#¡Ç(ç’Ï\ \^@áÃaI[ÿFӼݯä&ÊÖLÛí˜GÂÀ¹Q™“ÍÄ–aÌÉ×ÀÅòy7†ú›Û)°Ç#&iH¢¯+SêjÞÒŒ[AÓ³Á‡@Ÿ9cÜf,lj(Ò¡ðW(ºc)ÀW•9´^3ç,¡Ñ–e|jv5êÕªRfÞoà|ˆ] åÜ­ÉÕ¼Q"{åÚouéˆÖˆ‹ìg§ôœ"Wz¸þHå†3`I­åߣ{am~¹œÍ߯FJ6QõIà£KÈÒslö¥®68}©‚¼±‰ *Ï8©¯CáæØÿnKçÃMT¶`^õâ@ÀEâ{¸žLv¹®….‡ >÷†ïeøKˆÛ…cJp[ŽqÈK¯ŽÌ“\ʉG¡†žà2d2ºÔu”›P‘dxYÞúj«—âzìfe8Åú'ˆ$”RÎ{÷É$+“\]ÆÒ† RHH: ¥½¥u™F&Êc=’ßLâþ! %X %}ëatïRKJ\\äŠÏ]¨V–‘˜$H.>!8œ„ âL¨'qEwÙ$©>IÈ `s,ÞPÈ~:zÊ$FŸ¸sFx’]çäCêaŸkîÑüyÓ=Ÿ‘ ÜC¢Ÿ[áïƒÏ¤ø)¾IçàZÒ‚ñ:ÏXŸR€ODïp Â#:it5p:6í›¶mzpõš`£ç@|Žyx‹ñÀìjó­9 ÿÀ…WþÃîü¿ôËMØLà:B0­û ®ÂK/‚Oàno_{‚16½+ l‚å)[ŠðÃÅÛ(=xù´î_;õßFÅü: À p©1Hþa4ºÈÃ9"!òÛãÙO?xÃi‹$Ô@ЧdÆ1ÉPH†D€æQRͰF(<‚¥¨%,áÏ.ÆÑÐ/ÌÞ¥¯tWrüjwQØ0–”o!Qê×8ÏÐk&   ¿ áEPáÃÍúo×?`…ÀŸY>^½c Ó’ðžÞ (‘òµ³7ù ªwÍQŒ¯n¶›õß{$fâŽÖ}QíÞÓ×꯽:íÞî·¯Nƒ÷²#$××*þ$_Ü¿~,CΑú~$Ìz°qƒdVT_齯ùƒ×‹õ½2âPU80’²þ›²µ%ìü[_íßà÷¿d³ÜÙE-¿xœíZÝã¶ÿAU^nQ‹")Š¢ïhA´/mŠ})d‰¶•“EC¢×öýõêË’%{÷½m7]îÖš93¿ù ö?·™õ(‹2Uù½M¶-™Ç*Ióõ½ý_~t„m•:Ê“(S¹¼·seÿððí7‹?8ŽõçBFZ&Ö!ÕëçüSG;i}Øh½›»îáp@iCDªX»w–ãÀRX\>®¿ýƲ,Ø;/çI|o7kvû"«d“Ø•™ÜÊ\—.Aĵ{òñY>6'He¬¶[•—ÕÒ¼ü®/]$«NÜéàUR$ CS—R$œò”ëèè\¬…sN­¥cx=ÑgŠÍKðìþvò-•j_Är %Ê¥v?þò±c:%:éëi;Øwàí<ÚÊrŲt[z­à&zsoS\?ndºÞèóóc*RÇ{[Øò 眿5RgÄš’&÷6+š§fËy'ˆQHèI".ÂØ#ÞÌ¢˜„&i”¶æÎ›ãßÛZ©lΘIg£Šô³‚He¨ók·“<îT¡UšÉz±»Q[éždZªÜý(e¦vQî.Õ@‰ í¦±Êÿ¥Z¢]~Eß1ÙA´B>Í=µÜÃ^$rUVrµ?Ì#µ-·fvæ™ã%ÆÏ=ÑeT6ñ±¬]´Dgª¸·¿[UŸ–³TE"‹–ǫϧ ä©>Õ©ÜêomwøŠ@¹‰u@Œ¸Ÿ•Ú!©Š?Ôxˆ ϧþ˜i΄žç—BÄ÷&6Î>O5dÓî8V°/ #‘E' Ö¯…¶2åFÖ…q£.ör´òæ`‘ÓŸ„tlx#Ò&Á˜]“1©qwºÁÛFÇt›~–pJ2’1ô¿Š²3®û¤BÊFÆŸd±TQ‘\,¬ü²OY^ñL™G;g¹4é>É7,géÍ5•@®ž±ƒ#“µt¶i²Si®Ÿ–ä­ÕòWë›§¯tÀP£@ÊÔÓÒåV)½yÚäg©e” %V©¨ë4w´ÚõðÔcdr¥§9Eß)ÖRimòw Ð "}<Ô‹³$W³Ê²ôÉôãÉíŽj²ÏP€‰r»3=¨&Ä™ÜÐL&!Ê™5ޏ§infÈK ›¶U¦ËL} È# &—dãýf…9s–æšDvº”ƒ~®ëg|K«2½­ì׌­ÔQé¨Wè[’ßyæùß>þøÐî°ˆãù?Uñ©ÛѲŒH´T{­ýp¦/’xÃ6ÒéJ™6þÂÂ=3†Ò&v=½µæBÖÃÇä–ÄÛÔ¬rÿ®Ó,ûÙlÓÚÝS›êLö¨ ·±¡µÑí¹p['ÔëK@fÑRBZüÅTzk\,×…Úï¶÷vÕ ìž{+B·DQ^G˜ÀÂ× æÕxæÀ,ƒ ꮋÂzˆdÁ‚sÈ{jÀ¥EzüÍÓ§8`^8ÃæOóè{3˜ÕBL¨O8›Q#¡Ø»;G­·Q»•OA]T‰ì‡ö€<>Aa(8%õ‰ê'"ü GlæÀÔ…|êv×ß¶4eo ¾çín''–¦Tš&ç!øöÅŠRŸL6Æœ|£X6ÏáÚP}sZF,ô Hø}© õIΛ)ã†P·jÄÌÐÌóZºIT8Ô'}â¯Pk‡T@­,2è¸zÎZZA-ŠèTŸªGU«U)õ¼;ÀÙˆ]UÜ©f«yÍ´Œ5TUèºå¥ Zµ¨¼Ð|f`%â`µpõÒ f”qD+úO–QPI[ÿºÔf‚ob#„7bus‘ÊÁ)ZLH‘ÞrP¯šàt òÆä34¦>Ã\ž†ÂÕµ¿íHgãF,S''½Ø£ùp›ø î(c§ÝB®c Ë@Á;|+ø^†¿€¸]8¦·ùPà<Æ}qé5àQßC‚ 1ò(ôÍq&O„—¼vÒö|¨H"¸,oÝ5µá >…sXŒ±þ" ¥”±Nð-D2N‹8“—±4á‚₎Binhm¦‘óX­dW“8 ¯’Pn‚¾õ0ôºw¡´J³È„ϨV‹¤ ܨ1ÁÁ(\gB÷ùï²QR}•AÁf˜¿¡}Ñ4tìõ”QŒ¾rç ñ(ÛÎÉú£‡y®fú_Ìêîùh90{ëó{+| ¼Å7†â«ã\K0NcñŒõñð•Fà3a!5ºjâ`7m"Ÿs¸z°ÑÍ@|†Ypmb>â¡Ç½Éæ[Í@^ð*3^ñ? »gòÿÒ/Wa3‚ëq0i݆¬? w&PÀ9ÁÝܾFÜó¼ñ]¡—`#,§¥ß]¼„r×½—OëîµS÷mP̧AŽa–‴áïG£<Ørî“/g§¾÷bÓI¨sFÉŒa(’ž[Æ"XðŠâùœYx†ûųqØwçS³séèm˜öÞ`]¤ÕUo&Ã_àM¿ù ã/‘úÀ;'É ‘4 r>mŸÙfÇ4÷h~)'|Š…7h!¦}4ï#YÕ—~®ä5<ów<¿a+'C*^ÏZ:¡lx‘¯p C<D>‰çúeÍsÔ¾œ~‡ó¶r2¤ô5à ó}àðI8·cÉ¥Þ`F8q<³w<¿a+'CÊ_ϸŒÅO—çz,y-<‹w<¿a+§Bê¿àuð:žÛ±ø|#o^eÔ?æúÀÏÿ£Y6¥Q"œxœíÛŽÛ¸õ}ýÂû’A-Š7Q’w< ´Á"º/í^€} d‰¶µ‘EW¢gì|}u¥-{2A›´AãÁÌçBòÜéûŽ»=ªªÎu¹œQLfH•©Îòr³œýòó^4CµIÊ,)t©–³RÏ~xøö›ûúqóí7!`/ëE–.g[cö ßߪëjãg©¯ µS¥©}Š©?sèÓ‘>­TbòG•êÝN—uÃZÖß¹ÔU¶ÈŸžžðo¨hÇ>a>cPxõ©4ÉÑ»à…s^ãe„pé É5(g¿}Àµ>T©Z£Â¥2þëŸ_HàÌdî:yù®N“½:Û·¶jHvªÞ'©ªýÞ.ð”gf»œ1ÒN·*ßlÍ8ÌÕÓŸõq9#ˆ ³˜J)Å8ê¨F£Ó’gËu³n˅똡W*’i’(ŒçˆF=B=ÌQz¨Þݵܽ܋L§VŽå,ݪôÝJßÊf¨²·y k<(xØR÷º2Þ:/TËìoõNù'•׺ô_«GUè½u-Ÿ€$•ñóT—o‹Ü(¼/o¬wÌö`¶X^ÇžzìƒEßgj]7t­bì”Íß"ñìñ2«p‡t•Ô¡Ú'píBWËÙwëæÓcVºÊTÕãdó9Çi°}nNmXöë÷‡¶ äA½M2ýž1Á¾×z·œñ“PROð)¸ÇL‘rŠ…=cLYÄ„,˜ü`ãÊÜ@\íÓUe)Šä¤@üæí©ê­~ÚTV“¦:¨ ïS^‚P^4fSÙ;’>0(!ÓSv46LnáNÏàvÉ1ßåNh¬®þ×I1ºÄm­4ÎÒÄFµÒI•]06z9䙪oh¦.“½·ZÙÐ¿Š·(oŸ˜í­‚R¿`Oeåíòl¯óÒ|˜üE”Ïí¬W¨Ô<{úf ØòBÊú0u½ÓÚl?,ò‹Ž¿)ô*)Î)Ö¹W©6yé½wüÉAjm®cªÖ¯¡VÚÂSm\ä97â³Ò&1Mj'mÒƒÌ6.Øq"dN¶PO8 6D-$ÅT»½-ZMàfà 3 y…s6Áž®c3U]Fƒ­su¾*Ô¹”p€2hv ¶&ê8왋¼TPLŠÓ%¥ç¥›zX“ú àOK@‹Ø)“d‰Iœ‚Ѓ‚AËа,þþúLJ~‡û4]ü¦«wÃŽY’d¥`ÿÙÿÏÒ´»Ä<ä;ȶ=ùt÷þˆ8§¶¶sÖmW®TÛ­\íÛ²t—[.ÿ&/Š¿Úmz¹esS¨‡fÛv8ÈâwÂôÂú®´÷~¯vº¹ôÎ"Y)¢¿Ù’€¦©uSéÃ~ñÚU™£çó2bª¤¬­F¬…aX$F½"sº í îslÎ]:áh{gÐm•_Aµ  çÄþtӀϡˋ e•bÅ Ê¿ÍçlÔoÐ3ënÎ,ÕPij3ðôHÇq$mOÔÎhÌiˆ9ÔèHÌ=N9§âÎݶ´IòlyGÛÃN^ªlbµ%‘c³ ŽÚœl$vÉ‚~½[±(áÎÐŒ¼A°ˆyÄB_›J¿S‹®-"¤´…Iݷ༇ۈ…C-ÀÊÌþ™ù ê³Yˆ–%P«*9µ§r z½®•Y …Ø'ó½¦[´Hd¥Aƒ¡F×—Jký„X€yl?sK!/¤ùDÊ çLHÌø6Ôè÷ËÕ¬ñ­m¢ˆOPC¥KPŠÑ•Õcb•:K\q†TqcÊX Ÿó ¾î 7yÿ½#ÂMP6a^Õ¢ °å‘+J{Îs=ëºøê¾û^š¿»](¦µÁ%!àBÑ¥ÖÇŽ#EB ±QÈ£ø×÷åpýX^¦·á‚Ûá#yÍ{ìaáú9, ©TˆðK°dšWi¡.miÍ!$#61¥½Ñõ‘F'ÈcÃ)nqÈ>K@IDûÒÍàTï¦W¯<Ü]Ó¹Ù “(–“iÌEA'”„s)Ã2Wp£É&AõIL [ù™ì£º¡£SS&6úÄ•3&“xì+§p[;ozö/mõ|Dôzÿµþ'ÜàkSüLS|³ƒkIçŒ×}qôõi ð‰Zàgz *b6)tM$A:>­›¶lâ@J¸zM|cèÀø‚ˆðV$,c.ùÕâÛô@<ü,=PÞèØí^`ÈÿK½Üt›‰»žytZÏ»lpÕÝE„C)ÅÄÝííK,9çÓ»‚`_žvK1¹»xò7ÎãÓfxvFgÉüº€Sè¥Îý€öæw­Ñ[䈥 èÇÛsXÞyá´Ir #R0:’d(#ŽRD¡Íc4d$žRÆ(0%‘l <‘9q“goãØUç‡æ Òóȹ©®.Îlj—,—Ñr%õ?I¥&aÒhò¿&½òé¾jw±a_kxÓî¨>,®áš&sÖ|ó:‚mш†{eèX¨jž¿íýÌ®âä%þË8“,¾á¿_°9š75ãÜGsŽþ‚8Ž¢ê­€Ÿ3tèWD!£ƒå˜S‰!Hè0ÓÐY&oÎXz‹¡áÈoÇٯ㠖%t3ðóqYÿŽ~BÁ\ -¢‚›à àqŒèܦUI À˜BOÔRÌ{äuëÙ?oPÛŠž ‰„=]GoÇpĆf>P9ô¢9Šó†ví­»ÿ:¦{ëv¦ÎCú$zhˆÎ=mʾ·_iÀÿw>íœ!xœíYYã¸~_`ÿƒ }™F,Š—(ÉÛî‚Á"’—dƒy d‰¶µ#‹E·íùõ)ê²,Éž^$3È ãFw[Uţ꫋ÔóOç}á¼J]åª\¹aבeª²¼Ü®Ü¿ýò³¹Ne’2K UÊ•[*÷§—ï¿{®^·ßç8 /«e–®Ü1‡¥ïŽº@Joý,õe!÷²4•OñÝ|z•OµLLþ*Sµß«²ª‡–ÕCimzñÓé„N¬–"qû˜ú”z áU—Ò$go4ö97–bŒ}à Dß(¶¬À8øíå;ªÔQ§r%*¥ñßÿò¾gze&Γ—ª49È›u;bc†d/«C’ÊÊïèͧ<3»•Kqó¸“ùvg®Ï¯¹<ý^W.v° !¿~k¥® “†’g+”Ú§vÉåÐ;uÞÉH¤Qˆ£0^8Sâaâ‘`á¤ÇʨýS3ºÓ{™©Ôê±rµÝd™h­N¨7i¿ˆ<”6Þ&/d#îïÔ^ú™WªôßËWY¨ƒu&ÿ $ÚøyªÊ¹‘èPޙXÌs/÷ŲŸ3¹©j¹Æö‘ºŽß0{…ìö2kâè:©ZhçlÁ™ ¥Wî›úÓqÖJgRwsåŒñ$»Êɇ­‡}®{æ/æMõ|u<è="çã·RøßpƒoMñƒ¦øn;Ç’Öç}ñêëÓà3µÀz Âc:)tu$@;6­›¶l¢@8zM|£ï|Žyx¯â1l¶øÖ= ¿HÀ7úv»7ùi—»n3q׃Në±Ë³îÎ# Á'înO_#Á›ž6ñåi·ã§Ñm”¿\>mûk§þÛM2Ÿw8…^êÖHÿyÐ#" ¿Ï~úÁ §M’)œ’Ç$C1'u´y”„Ç ŒBBÃØ Dp$j wð“g‡q<4ç§foÒ‘¹ºÄÙ\W&ÑîØ–³ãG& ìÇp+Õ¿Ž‰–“`©íùvãÞj_·¸ÁàÞ¤W£jޟ̱ê–g{ýŒéœ€õbs0:g4±ïê­ß¬Îñ©½à‚Ø%ÈŒ@¢·Äöý(q‹¹]‚ÌâYk² †Óñ Ä›"1VÑÉ%x/Q§"ËÇs̤ÌTûr œ´7±7©*Â8F"Œ™½zŒ8 ­Áf‘‘k>ºŸz›0˜ô†.,®8,Ú1úô–uεñÃ!„\ïóm¶x¶·éðÿßNƒ~ÕÏ öxœíYÝ£È?éþĽì(Ðt7MÓøÆsJ´:]¤ä%¹(Ò½DÚ6·@#hÏØû×§š/cƒ=³Jv•U–ÑÎ@UõGUýê£{:¹õ,ë&SåÚ&Û–,•fånmÿãן]a[ŽË4ÎU)×v©ìŸž¾ÿî±yÞ}ÿeY0¼lVi²¶÷ZW+Ï«uŽT½óÒÄ“¹,d© âÙùä,ŸÔ2ÖÙ³LTQ¨²i‡–ÍSé:ÝŽâ///èÅo¥HE¦¥.H¸Í©Ôñѽ û\K1Æð&¢o[5`œ þò5êP'r %*¥öÞÿú~dº¥:Γ•š$®äź±3C\ȦŠÙx½›à%Kõ~mSÜ}îe¶Ûëó÷s&_þ¤Žk[Ø çœßz©³ÓIGÉÒµ ÊŠþ«_r5E¢Ö;)x"B,Âȱ(¦ÄÅÄ%c%‡F«â¡=è½JUbôXÛuœfjsÐZ•ÿ:”É^&dŠFãŽËÉc¥jín³\v½½*¤w’Y£Jï½|–¹ª ¬¼*ƒ¹¼¸Ö^–À¤y¦%ªÊóÓ \ñeîià>öc*·M+×Å|RÛò:樚Ù^jŒ=ÝÄMï$˪âÀ:WõÚþaÛ>g£êTÖ·Ï%Oß3}êBr˜Ø´™xÀ7š}œª@ÅŒûQ©bm3Ž"AD8c'€€+°0Ì)› Àªb!|̸àðƒq{(3 QUçêÚHäñI‚Ú?djöêeW[êú gc_²Ôrû kß‹ aA0žëÐ˘ ¹Å;Ýáñ1+²vIf2Fƒ©¶q~Åm«´piâި¸N¯¶v9d©lnX¦)ãÊÝlLà/ò Ë­b½¿5A+Pª7¬àÊt'Ý"K+••úuñ7IÞ[Ym~—‰¾»ûvX²ˆBÂz]º)”Òû×U~Óöw¹ÚÄù¥Ä6Ó•z—•®VÕOF.·z™Swø]bmäÑb  -DîÁ`ŒÏZéX·‰wirÛyˆÀ~¤eé“)SÇ“!Ú#Õ„¨¡D!;eQ™’Õ¶âLîi&Üå,¾OgÜÓ27]åu4˜*×d›\^j (c ¦×dã¢~„Ùsž•ÊI~º–S`ô¬œ¦…Ö¦ƒ¡xó"Ð1 ©ã4Öñ¤$ ¤`´2´+«¿½ÿùiXá1IVÿTõ‡qEË2"ñFÀÿöÓ™þ˜&+h0ŠX?eä Óœüú‰Gï̸”6¾›ÌÛÍ\Ë®WYìÚÒ¤ÈÌ(ïï:Ëó?›e½'Óf:—Oí²Ý먋×+3(ëMµ}ôktŸ»ktæñFBýÅ”kžZwµ:TÄk_5ì‰/ˈ®ã²11†×<Öòv\è4/~ð0ºcw iÁ³ï'Ó€mëìø*c@qÈüÈÁæ§ÿ |Šf„  n:TPÄÅþÃÙ}“…†¥ráÝÝ…§Z‚ƒÈ¾ Ï·DP NI·£î‹ˆÀ!!ò)c‚9®O|PŸ°‡é‚°¤I’ÓO¬=®ä&Ò$VS}DÂÀ¾Ñ蓉ľ'Y‘¡{ËW%œÚ7w``Ä g !‰~lt­>ÈUßaܺ‚8„Þ›ùþ@7 ›ZÊtJü2ó%à+ëê³^±–ÆPë:>u»šPÕvÛH½7pV¢Š!ç»m;¶ê˜–ÑÆ‚ 5º¹6xë¯ ™Ç-¨å „ÛGH7t(4^´¥ÿb…­´õÛõlÆùÆ7Bø3ÖØG©Œ¢UíBGõëC-/Wïœ1UAܘÀ†2–ÀsÔËP¸9ö?ÛÒY¹Ë$ÌE+Nhb`<¼`´{Èu tLð ¾-|¯Ý_ƒß® SƒÙHp>㸶ðhà#Á„˜Y hˆ8¡/¢kÞЗûd$^§·ñxÛó_BÙ,B¿„'!•26 ~ žL²:Éåµ/» „¸ 3WšÝidÆ<¶#ÙÍ é (îBÐ¯Ý “êÝ6àòË‚‡%›»­ŸHë.6!8œ¹ üL(â_à]6 ªÏâ2HØ ó¯ÈeŸÔ '5eæ£Ï\9#<‹Ç¡r²iëa¾ÛÞ£ûYW=Ÿ-za}üV ÿ0øÖßiŠo¶sp,éÁ¸ŒÅ3Öç-Àgjïô@„EtVèÚˆƒvþ¼nš²‰Îáè5ÃÆØófá­ˆˆG>÷‹oÛùáé8ðŠÿaؽÁ‘ÿ—v¹ ›\/Ö}È‹pg…œ³ÜÍé‹aÄ}ߟŸ&6Ãò¼[ŠðÃÕm”·›\>íÆk§ñí"™/ƒœ@/u‰2¸êÁó GÄy@>ÝŸãô“N“$!RÌ%Ã$C.|+±´y”„GF!¡ad"Xð–âœYØÁÓä9ø8ššóµ„9šôæJC†ßl. Æ‚’àk”öÇ4×QM´ã#ŒCAh`ýÑâ(‚D†¾3¾Y¸ýŒ á|Gì<-1,F¹'šÿ„ £ð­“ž÷ú›etp˜[2•Ž°ÎøÖ ³¸ÃïpÝWÙî=~Ëž ré"u¸ëï/R'Ÿ“[Ú \·‡Ê?×]:x4×åð÷ßÁqŸ *7xœíZÝã¶ÿAu^nQ‹")R¢ïhA´/mŠ})d‰¶•“EA¢×öýõêË”eïmšÝ´Û;îÖš93¿ù ÷–ßw¹ó(«:SÅýŒ SlQƒgKø;È÷T«}•È5,”¨Ú{ÿÓûéb”êÔÖÓ;v´ïÈÛE¼“u'²özz«à¥z{?£¸}ÝÊl³Õç÷ÇLþ¤Ž÷3ì`‡#‘ ØùS'uF i)Yz?cE÷Öm¹1Š(=iˆ(ñ‰?w(&‘‹‰K:¥½¹‹T%æø 0û(7UV¢Áƒby,U¥Ýu–ËVÖÛªôN2«Uá½—2W¥Wf(q¥½,QÅ¿óLKT7ôÓ‚×¹§žû`ØËT®ëF®5ß¼Ò™ãµÌÁs¼Ô¸Õ]ÅuÇ)ã 8WÕýì›uóôœ•ªRYõ¼ yÆ<Îô©ÍÜ^h£xÀ7êmœªÄÂý¨ÔFè`HðS6eÂ^ DF|º"»7Aq÷E¦!kÊãTÁ¾ªŒDŸ$˜Ýü ½T½UxPW{9Y{È 0Æí N":µ¹éaO0žÚÐɘ$¸Å;=ÁÛÅÇlÈ…S’‰Œ±Àöû:ÎÏP¸í•$[™|ÕJÅUz±°ñË>Ke}Ã3u—îjeû*ß°Ü2ÖÛ[ B=cW¦éî²´TY¡?-þ,ɧvV«Ÿe¢Ÿ<}£ö€j¢P>-]ï”ÒÛO›ü¬ãorµŠó±Ä:Ó•j“®V¥…'‹‘˵¾Î©Zü^c­”Ö&u§m bÃà¡•Xž% ¹ºUŽ£O¦ÃO†8¨&û % Ù™(w¥é6ÍØ ÎäŽf2 Ñ€qáûtÂ=]ç¦`†¼ºiPu¶Ê娗p€"jzI6ÞïV˜3çY!¡?ä§K9þÌ ;ã{Z“é}Q÷¦U½e줎ÓXÇVïI|ð2L‹¿½ÿþ¡ßa™$‹ªêð£ã‘x¥öÚÙÙ¾L“Ì»X?d;(f®ø#ŒKïÌK›ØYz[Í•lÇŒ«Wšì2³Êû»ÎòüG³Mo·¥6Ó¹´¨K¯³¡·Ñ³\z½Ú×Í% óx%!-þbм3-–›JíËd`×f–{ÇAWqQG˜ÀÂÇ&ÓwxîÂÔ‚B,|~7Da3F²`á9ä–pi•ßAßä‡ÌæØüé^¹?‡©,„r°90ÜŠý»sÔ¬ú­8u3 P#B0f#òôHE‘(iOÔ¾Áç$D>eL°¹ óâÔ'ìÎÞ¶4eo¤Þòö°“›HS*M“ó ùìbE­O&»ÙbA¾…),_pAh>¹=#ù‚†$ú¶Ö•ú Ý€ƒqGh[5â¦eæû=Ý$*j(R›ø3ÔÚ1P+«:®^°ž–ÆÐ_«*>µ§²¨j½®¥^ 8QÆPÅÝf¬Z´LÇXã@U…®[_:¢õW‡räG晃•(¨ã „›GH7œS ÚÐp8Fa#íüëR› ¾‰þ„5LFª§hU¹0#=Æz_ÉQ½ê‚3T(È“ÏИxƹ| 7×þ¶#›°L¼êE‹ÆáÞðÜF¦N{ ¹®._àÛÀ÷2üÄíÂ1¸CóYÀÅ¥×€G¹bâQè›! ˜}]òúIÛçP‘DxYÞ† iÇÁ5ô˜ÃŠpŠõWˆ$”RÆÁ·É$«’\^ÆÒ„ R(tJsGë3L˜Çf%»™Ä!ý]*ð¹ô­‡ÁêÞ•Òf(qÌ"W|îBµBXDÁ$Ašpð Áá$\gBQ—ñ)ï²IR½JÈ `3¼¡ýªièhõ”IŒ^¹sFx’}çdöèaÞ›Ù£ý³¶{>:.ÌÂùø¥¾ ¾ ÅO Å7Ç9¸–t`¼ŽÅ3Ö§#À+ÀOÌ@„EtÒèš(ëüiß4mñ €«×à Ág˜…·f ÆQùµù63þ.3‡ ¯ø†Ý3ùYúå&l&p!&­§!˯ ›ÀÝܾFïûÓ»‚•`,O§¥ß]| åm¬/Ÿ6Ã×NçQ1¿p³Ô¤¿>ò`Gœüúxê­/6M‘„HqÀ(™3 E2 „ï$1’âhŽQHh9œ#‚EÐP|0ϱ]<ûG¶;?U0—Ž3离Òág»‹@t…O9æÿ½L™úGØß-wèÇpå´­i¿åqÁ,]Í/¾¨o‹jºBÄQÄH nyv$cëkÏóNðÄÁφZýóÿ(¢Ÿ%fÅK`î‡Í/Wüÿ³Ô”xûNz=~+fþ‚Ù7iá•P¾H%HpŠGE»Ã,ù™žže/áY1?ââJ{Ò³ ƒ„‘ׯ_:ØÛ´ðJ(_¤ƒÝ¬7nÌègéYþ"uöÖlÐgúð?Ìkiþßüü"H«² mxœíYYã6~ÿ U^¦±ER¤D9î°;`÷%Éb} d‰¶•‘EA¢ÛöüúuY—{:H&È`Çî–ªŠd_’7ß\Ž™õ,Ë*Uù£M¶-™Ç*Ióý£ýŸŸ¾u„mU:Ê“(S¹|´seóô囿9ŽõÏRFZ&Ö9Õëûü]G…´Þ´.Ö®{>ŸQÚ‘*÷îƒå8°WÏû/¿°, ÎΫu?ÚíšâTfµl»2“G™ëÊ%ˆ¸ö@>¾ÉÇFƒôYÆêxTyU/Í«¯†Òe²ëÅJg¯–"aº˜º”: áT×\Gg²ô\ZK1Æ.ð¢¯[WàÙ~{ùŽ€*u*c¹ƒ…åR»ozÛ3Œ ÷é;:wäí<:ʪˆbY¹½Ùàœ&úðhSÜÜdº?èÛýs*ÏÿP—G[Øâˆ†Ä÷}v»j¥nˆ! %Mm0V´wí‘ë^£"Ø'‰|ÆñVÅ$t0qH»igî:Q±QÿÑÖÑöç8S•D½ûå¥P¥vvi&a÷ ŽÒ½Ê´R¹ûV>ËLAn‘j D¥vÓXå?g©–¨ÈïìwI ˆNè/s¯÷ɰ7‰ÜUµ\c¿¹¥¶å6ÌÞ£^bü:ÝFUË*¢= 8Så£ýÕ®þtœ­*Yv<¿þŒy Bœêk“ºÝþÒfã^ߨQ¢Î€÷½RG }À<1ãÇ€\Lås.œ) èùœ !>™à8§<Õ>Åe¾Á©,D]%˜_ÿ#TuPç}i<©Ë“œ­=§9å´X'!ÛÞŠtø'˜ÌµleL6Üã]_à£KzLßKÐ’ÌdŒCÿï¢ì‰û^©Árñ;YnUT&“…µ_Ni"« ãvnÎvkR|Ñu†å‘>T/ äª>bQ ?Á‘É^:Ç4)Tšë‹¿Jò¥“Õö뵯÷€3 .(”¦KWG¥ôáÃ&¿Jý}¦¶Q6–Ø¥°RîÓÜѪjÀÈäN/sÊÀK¬­ÒÚäð¡5F†0xj$67 È®v•eé«é5—«!Ú=Õ¤Ÿ¡@y¸å±0}§ ÄÜÒL*!ê3.<θ×enfÈ)ÒM«ªÒm&Ǿò¨É”l¼ß®0:gi.¡Qdשœ¦ù0å;Zê]uwçå½a¥Ž’HGƒbß‘xïe˜9Ö?¼ýö©;aÇëÿªò]¢e‘h«NZûéFß$ñ¦„c¤ŸÒ#Ô3aü†‚{cŒ¥Mìû6;—²8G¯$>¦f•û£N³ì{sLg÷`ÛTgr@ݸ­ îÐÈÛ9¡¹ÝO™E[ iñ/Så­yµÜ—êT!ÛF`Ü;î ºŒòÊ8Â.3˜Qßà•ó °ðøC…ýÉ‚·¶—–éå 4PN1tÂp…ÍO{˽Ìg!&”Ÿ­ "F(önQÔÅÉ(¨ûQ€j‚yhÈs• CáSÒhÔÜÁW$@eL°•“âÔ#ìax iÊÞhû·û“œXšRiºœ‡HÀíÉŠJ_M¶CÆš| ãX¶ÎáQ¡¾r:F,ô Høu¥KõN®ÛIã–ÐôjÄÌÍÌó:ºITPj È“!ñ¨µc* V–´\½f-‰ Á–etm´PÕnWI½î¸QDPÅz¾Z7LËXcAU…¶[MÑú·E9òBóY•Ȩå„ëN°¢ÌG´¦gqŒ‚ZÚúßt7|!¼«TNѪt`HzŽô©”£zÕ§¯P7&Ÿ¡1Åðçò2î®ý}*ÝŒ›±L\ôâ€Æá â;x.™;í%ä:º 6ø ß¾Óð—·‰cJp‡ç1Ÿ‹©×€G¹‡bæQè›ò™<Nyݨíq¨H"˜–·þÑ´å  =FY̱þ" ¥”±^ðSˆdœ–q&§±4á‚ò…Ò<¤u™FfÌK½’ÝMâ€þ) å{\ú©‡aнK¥ÍPâ0˜E|î@µBX„þ,AêpÁ#7&8˜… âL(ò¹¿À»…l–T%dP°ö?¡ý¦iè2è)³}äÎâY>v“ Gs_ÏÍ_Ìšîùl90{ëýçVøGÀàóPüÂP|wœƒÇ’ŒËX¼a}>|¤ø…ˆ°Î]=ù`7m"îûðè5ÃF?AðfÁ½ˆq䇞ï-6ßzò‚?eâæÕï_v¯äÿ¥_îÂf×â`Òz²|îL À÷Ù îæé‹aä{ž7V$Ø Ëói)Ä“—Pî~ðòiß¿vê¯FÅ|àf©1Hþa4ºÈƒ¡ïsòÛãÙo?x±iŠ$7_¡øŒ’ÃP$_xVlóÍ (W„¡Å9"Xø5Åã>³ð ‹gãpèÎÌÞ¥‹î꜔ ’bpÛ;†·_Nsb{Òz–µ×þ8b bJøŠ†0‹2ËC5oÇ0_5—áØUõŸÑ™ÕR$ CS—R$œêšëèâLÆÂ>—ÆRŒ± ¼èÅÖ§€ÿ½|G@•:•±ÜÁ@‰r©Ý÷¿¼ï™F‰N†ó¤ù‡*Ž 9Z·#6fˆŽ²*¢XVnGo&8§‰>llŠ›ÏƒL÷}û~Måùê²±±…-Ñø¾Ïoo­Ô tÒPÒdcƒ²¢ýj—\½ëÛÆT ÂvÞÊ¢˜b‡VV|ª´:>5£;½×‰Šû5Ks‰zcöÓËK¡JíìÒL6‚îA¥{•i¥r÷½|•™*Œ¹Eª•ÚMc•ÿ3KµDE~g¾KRD¡¿Ì½vÜÃ~N䮪å#˜Oj[nÃìU1ÛKŒq¢Û¨jA±¬"ÚƒgªÜØßíê§ãlU™È²ãùõ3æ)À9Õ×&»ù»M›‰{|G :D‰:ƒ̸•:nl&öCFùŒƒ«N!¤æ\³)ް Ô f\€÷dÀqNyª!†ŠË|‚SY‰,ºJP/X¿HuPç}iì¨Ë“œ<§9¨ä´îNB:×¼éB€`<ׯ•1qw}À;F—ô˜~”°K2“1 ­¿‹²›CÜ·Ií*åVEe2XÛå”&²ºc™* g»5A¾È7,§ˆôáÞµ@®Þ°‚#“½tŽiR¨4ן“䣕ÕöW뇻¯ç€5 3($§OKWG¥ôáÓ*¿iûûLm£l,±K5¸J¹OsG«bàOF&wz™S6þ»ÄÚ*­MÏ´v‘GnÐGg©t¤ë$Ž›”yí6 D`;Ò²ôÕ”¤ËÕížjBÔP€߈òX˜òT· âFni&Üõ¹'£3îu™›€®r ¦¢Ué6“c-ayÔdJ6µ#ÌžMÝR’]§r ŒžæÃ´ÐÑêtÐåw^ÆQê(‰t4(Éë­ ­Éú/ïzéVxŽãõßUù¡_ѲŒH´U'Àß~¹ÑŸ“x ÍÄ1Ò/éò…iD¾‡ÞáÙ½1ÆÒ»Á¼ÍÌ¥lú’Å-‰©åþU§Yö'³L§÷`ÚTgò¥^¶yíuq[e:eÝ¡¶Ïngæs?õÎ,ÚJ¢?›º`ÍSë¾T§âñº±ëÒaì\ú!ºŒòÊXÄ ¯Y¤å;¼r ßAÌ{êáØ]Zðà†ý`°m™^ÞA­õ(8 WØük?=¶‚~.ÄP‰ÏWTPÄ Åìéß`¡n)ŒÐݪEöB{Džo‰ 0>TêzGÍÞŠj;|å0ÂGáOÃaI“$GӬݯäÄÒ$VS"gOFTúj"±íGÖäèܲu§ƒúÍéñ ð‡J—êƒ\·MÆ-¡)ì ˆè³9cÝD,lj 'C⯙ÇTp_YfPŸõšw´$‚j\–ѵÙÕ€ªv»Jêu¿›E9ß©[±uôŒ6ä`¨ÑÕÔ€ÖÏõ ͳ-‘:PË.«~„t‚å>¢5ý–‡QPK[ÿ˜ÎfÀ7ØÁf¬¾‹R9E«Ò~ê5Ò§RŽW NŸª nL`C‹áõ²+Üûïmé¦ÜŒeæ¢4q0^0Ú#ÏuŒër˜à«ûÖî;…¿Ü&†)Ál$8Æ}OL­<ê1$¸3‹B ÏEÀD8åu}9ó #‰`šÞú£lËþ’÷˜ÍŠ`îëŸIH¥œ÷‚_’qZÆ™œbià‚òAiÎs]¤‘óRäwƒ8 ¿K@ùÌ‚~é0 ªwÝ€Ëw÷ž–lî@¶‚#vèϤ†‹€Mfp΄"ßóx7ÈfAõY ƒ„ͱÿAö›º¡Ë ¦Ì0úÌ•3ijxì*'¶æ»î=šŸ˜7ÕóÕr ÷Öǯ¥ð?á_›âMñÝvŽ%­3.ûâÍ×ç-Àgjô@„‡tVèêÈíØ¼nš²‰<߇£×Ì7úÀç˜÷z î!?d>[,¾uĂߥòàÀ+þ‡Ýî @þ_Úå®ÛÌÜuäqÐi=vYoÑݹ@ïó™»›ÓÇÈgŒÍÏ ƒ›ùò¼[ ñÓä6ÊÝ.ŸöýµSÿ6JæËNÇÐKý€tðÑè=Bß÷ÈodzŸ~pÃi’$ä@Š}NÉŠcH’/˜[Ú·ð “g‡q84ç§foÒámØãK¹á­›3@uñæÍá!‚ƒ ÉÊñ8â>ç!|7ÆFIqqVpV/ä¡'VA‚ÇÑýÖ< ÜÅ? 8~3þöߌûqœÜ¢zR'Ú˜Æpþ™·{ºîþ < Mˆh(muw;hÊ^ÈÁûà\<ŒÆyü5¿ŸÍ=5üþI § image/svg+xml Õ%KxœíY[£F~”ÿÀ’—i­)ꎻ#펢DÚ}I²Zi_VÊ6L!(·íùõ9ÅÍ`pw’žd’ñhºÍ9§nçûÎ¥èÕ7§}f=ʲJU~o„mKæ±JÒ|{oÿç§oa[•Žò$ÊT.ïí\Ùß<|ùÅêoŽcý³”‘–‰uLõÎú>WÅQ!­7;­‹¥ëG”¶B¤Ê­{g9 …ÁÕãöË/,Ë‚µój™Ä÷v;¦8”Ym›Ä®Ìä^æºr "®=°/ö±ÙAú(cµß«¼ª‡æÕWCë2ÙôæfKGV[‘0 ]L]J°pªs®£“s5ö97–bŒ]Ð L_h¶¬À³üïí;ªÔ¡ŒåJ”Kí¾ýém¯t0Jt2œ§sìhÝ‘·óh/«"Šeåvòf‚cšèݽMqó¸“év§/Ï©<þCîmlaËC4$¾ïóË·ÖêÂÒHÒäކÊö©]rÙbRó$‘/˜¶°(&¡ƒ‰CÚI»ã.›íÄE–j-Kg§Êô½Œ2Ô{´_Cž Ujg“f²æîÔ^ºg™V*wßÊG™©ÂpÉ-R ’¨Ôn«üÿ0¹DE~c¾SRN¡?¯=wÚ£^%rSÕv'Ì#µ-·Qö3ÛKŒ‡¦ë¨j‘±¬"Ú—3UÞÛ_mêO§Y«2‘e§óëÏX§ìTŸ› îæï6m&î ð ƒj%êT˜hß+µ9GS/} |ñÔa2£…5 ýPp2QÖƒsÈS qTœ¦ãei,²è,áôõ¯~¢j§ŽÛÒ8R—9{Ls8“Ó’ž„tzôÖ¤ ‚1¿ecÂâ–îü„nÒ}ú^Â.§0'ºeFÜöJÍ•ŒßÉr­¢2¹Xûå&²ºá™* g½6¡>«7*§ˆôîÖµA®^°‚#“­töiR¨4×Ï›¿Èò©•Õúgë'w_Ïk@~SHQÏ[W{¥ôîù#¿hûÛL­£ll±I5P¥Ü¦¹£U1àÓ@‘Éž×” çTk¥µ‰à)AkŠ iðÐX¬.\í(ËÒgSsNg#´{©‰># ~Ê}aêOÝHˆ‹¸•™HBÔçž`ŒN´çymÇ×D7%«J×™û6G M®ÅÆûí³ç,Í%”‰ì|m\7ŒøNVGz—ÛÝiro{©£$ÒÑ Õw"¯÷2ôËÞ~ûЭ°ŠãåUù®_ѲŒI´V€Ö~¸ÈWI¼„naé‡t©Àt‡æ`å^ckƒÝ`ÞfæR6Çl –ÄûÔŒrÔi–}o–éÎ=˜6Õ™HWn{†îŒîð+·sBó¸½&d­%„Å¿L’·¦Ér[ªC±‡lë€=pï¸0è2Ê+ã,|Í W}ƒô1(À‚yw= Û1“.¦—–éé ”O″pÍ¿öÑc èÓBL¨G|¾ ÚB1»» 6X¨[Ê##P·#€j‚½Ð‰§["( …OI³£æ‰oAÄ(ç‚/è¸GáwÃaI“öFӼݯäÄÒ¤JSä"g_¨ôÙ`Ûb,É×ÐŒeË® õ7§S`ÄC&h@¯+]ªwrÙö9·‚¦Tƒ! æŒur¨°©% O†ÂŸ!׎¥ÀZYfPqõ’w²$‚úZ–ѹÙÕ@ª6›Jêe¿Ë!в¸SwWËFi™ÓXU¡êV×N´þmQ±Ð|pJäèå„ëN° ÜG´–gyµµõ¿ëÙ ø!ØDÕwF*§hU:Ð#=FúPÊQ¾jÁé3ĉg(L1|Ʊl=ÌsÝ{4?1oªç£å@ï!¬÷ŸKáoAƒÏMñMñÍv®%-ç¹xáú´x¥ø‰ˆðN ]Ýùp:6­›¦l"Ï÷áê5áFßøóàVÄ=ä‡Ìg³Å·îXðQz .¼âL»ù—ôËMÚLè:btZOSÖ›¥;(ð}>¡»¹}qŒ|ÆØô®0° —§ÝRˆï®^B¹ÛÁ˧mÿÚ©ÿ6Jæó$€cè¥Æ< üC4:äá¡ï{äÃñì§¼Ø4Ir Å>§dÁ1$ÉÀÌŠ-m%ÅᣀР´<,üZÂ<Ÿ[x‡É³Ã8ºó¹„Ù»tò6Œ…ƒ7XWaeÒ_HPÐpô–¢N}LÊÇQÇ! 7»áßG œWöÌ¥#’ÜÄ3 8þ<»%¿P²úÚÏ¢SÎB¾˜O3èïjžš×n>°ç ?×v^PéÇâ3£Ÿùü ŸrÒ—ÓàWð¹y9ð,ÂŒŒ_ù¼*ý?;»Ö¢ù½2yƒß¿óŽÃyâåxœíYYãÆ~7àÿ@Ð/;°Øì›My4‚…‘ñKâ @^ ŠlIôRllÍHûëSÍK¤(ÍŽ‘¬‘E–‹Ù!«ªªúêèžÇOûÂ{ÖU›rå„}O—©Éòr»òÿñËOò½Ú&e–¦Ô+¿4þOß~óX?o¿ýÆó<^ÖË,]ù;kË0<«™jfi¨ ½×¥­C‚HèäÓ‹|ZéÄæÏ:5û½)ëfhY7–®²Í þòò‚^X#Eâ81 ) @"¨Ï¥MNÁÕXØç­±co$úF±e Æ9ÀÏ ßPmŽUª70P£RÛðý/ïf€Qf³ñLeƒM^èvL¸3{žu^›2|¯ŸuaQá!·@I*æ©)-r«Ñ¡¼3ß);€·by›{î¹OŽý˜éMÝȵöpŸÔ÷–9hå¶—9;D×IÝùÇóÉ]˜jå·ižž³6U¦«ž'›gÊ3àòÜžÛhìçï7í&ðz—dæ1ã~4f+DX„©˜ñS‡H$elÎv»’HÅ"ŠÔŒ ®>:ïÇ2·O‡Ó|‚cU9‰"9kÐ 8êeêyÙVζ:êÙÈ—¼‚ú$¦sÕ;‘>ÆüžŒSóïü oŸœò}þQÃ.ÉLÆi06ÿ&).ˆ¸o“+.8tµ6I•] lìrÌ3]ß±L]&‡`½v“ïXÁ!±»{4¥yà ζ:ØçÙÁä¥ý´ø›$_[Ù¬Ó©}u÷Ͱd)…DõiézoŒÝ}Zå7m[˜uRL%6¹¨TÛ¼ ¬9Œð4bzcosª¿·Xkc­‹à9@ˆ¼ƒ!:+cÛ$tÜæš&wl§V<ºø~4 ضÊOï Ø Š#ÎâvÿºOÁÐÛŘPA$_PE'³‡‹ûF õK 2ñîvâ©F„`ûò|Kű’”´;j¿ˆ !F9W|0 Œð‡ñ‚°¤K’“éGÖV Rí«+‰ ‘HøW#j{v‘Ø5$Kò´nŲ„“Bóô ŒxÌHüCm+óA/»®ãŽÐvÄôÜœ±žî"6µ”Ù˜ødæ)à««ê³]òž–%P«*9·»QÍfSk»6pQâ@Κ^lÙ2=§9jt}mðÖψÅîY€–H‚Ô ÂÍ£t-(—ˆ6ô?{£¨‘öþu=›s¾óRlƺ(S‚Q¬©è§ž{¬ô$quÎRÄ l(c)<Ó ¾ …»cÿ³-]”›±\¼iÅM ÆÃ7Œör]|…oßk÷Wà·+ÃT`6 Žq)ÔµÕ€GCŠ+5³(ÐI®"¦âk^ß—3IE×ém8Öv|%o¡Çmvthøœž„TÊù ø%x2Í«´Ð×¾tî‚’ŠÎ\éÎs}¤‘óÔŒäwƒ8¢H@I&”¢_ºFÕ»iÀõ»€‹‡[6 [!¬b9 Æ]lBp4sø™P$…¼Á»¸lTŸÅe°9–_Ë~W7tÕ”™>såŒñ,ûÊÉÇ­‡ûnzöÌÛêùìÐ{(ïã×Røß€Áצø•¦øn;Ç’Œ·±xÁú¼øL-ð+=á1º¦’ ›×MW6‘Ž^3l =8ŸcÝë¸@2f’Ý,¾MÄ¢?¤pàUÿð{ƒ#ÿ/ír63¸NÖë7áÎФä3¸»ÓÇH2Ææg…Q€Í°<ï–büpunG—OÛáÚix›$óÛ §ÐKMq@z÷½Ñ{ôˆ¥ä÷ûs˜~tÃé’$ä@Š%§dÁ1$ÉH*æ¥6’ˆâxQDh{B ‚•l(LHîá'ÏÞÇñØœŸJ˜ƒIGwP­Z ñ4Ù<]e«Æ§Â[WeýmnwU6|:[Á››²#Báëõ›²éYïæí^)Ée¬Ä" ” a$š\xÍ+{skÃÈ‚B4Ã1ìK"BPLÜýQ1:<5SG ÷'3A¢˜5cŠ¥`”z…§:@u›ðÜu $1Yô/×nèÆ¹ ®.y&¦§ŒJ_#Îq€—q,ÌÑßþ~t·Äðû߃»Š_"éxœíÛŽã¶õ=@þAP^vKâMåŒ'@±Z yiSèËB–h[YYt%zlï×÷ºZ²g'mwÛEÖ‹‘Î…‡ç~HÏãç}á<˪ÎU¹r±\G–©Êòr»rÿöËOžpZ'e–ª”+·TîOß~óX?o¿ýÆq`/ëe–®ÜÖ‡eŽUá«jdi ¹—¥®ìãÀѧ}ZÉDçÏ2Uû½*kËZÖß©«lÓ“ŸN'ÿD-Žã8@$ Ä ¯¾”:9{^Øç-^‚ 7"}%Ù²ãàOßüZ«Tn€Qú¥ÔÁÛ_ÞöHù™ÎÆëäåû:MòJnlÌìe}HRY¼Yà”gz·r j^w2ßîôðþœËÓÔyå"9¡ObÌ9gÃSK587<[¹ ¬hßZ‘ËqtøÄy#OE„D/‚ööp¸pÒc­Õþ¡áîô^f*5z¬Üt'Ó÷ku~gdö./aeí÷æíÊóAUÚÛä…lXƒÚËà"óZ•Á[ù, u0r ¤ÒAžªò]‘kéÊ;ë³8-æ·±—ûdЙÜÔ–®1‹y%®4È^9³½Ì˜{DºNêÖMŽsH¶Ø…ªVîwûé0kUe²êpÜ~®q <ŸëK“”ÝúݦÍÂ=ºCPï’L .fØJíW.C~3ñ>…à‰}E( ñ 2#Ÿ S>_~4ÎñŽe®!«çùǪ2Er‘ ¾ýÕ‹©wê´­Œ%uu”3ÞS^‚R^›8&sÝ[’.-0BìI’{¸Ë ¸}rÎ÷ù »œÛÇh0¶ÿ&)†¸o,67ªµJªlÂhírÌ3YOƒÜ29xëµÉü›¦3(ïè]ýA©¬ˆ›½Of[éíóì òRœüU”/IVë_eª_ܽ]d@¹R¨X§®÷JéÝÇU~Õö·…Z'Å5Å&×+Õ6/=­£€! ¹Ñ·1UÀ·Pk¥µÉáy„Úy) ú­”N´­ì¨©zPÚ†e [NÇÑÓ§Ît{¨ÉQ‰#6åþ`z–Äna&ß|ÂY((%3ìå66]å4L›«óu!¯µ„ ” @³)ظ¨å0{.òRB7).S:FÏËq]è`¶t- ˜÷€±—:ÉŒ:B {+ü²üËÛŸž: iºü»ªÞ÷Ç$kuÿ»Oü1K—0aìý”ï¡`˜éä{(ƒqMm|7Z·Y¹’ͰrslËÒ}n¸‚¿ê¼(þdÄtz–Íu!Ÿ¬Øæ±×%h•é” ÆÚ>5š×í4:‹d-!‰þlz‚3¯­ÛJ{È×¶m¸#;_÷]%em,b< E¢å´ð`òaz¡áCïŽíuH  ¾-¶­òóh·!A£ñ™íkH0äÅ“s¶ ‚ø D÷u¢ ½Ž »½ò”%Á(ŒÝ+ð|KØcÁ nvÔ¼a.päS˜` bꇄbö0"M‘¼Z~dí^’—JSXMO¤>ŽBwÂQë‹ÉÄv$Yâ`x+–%ì“×!Ïb*H„ãj]©÷rÙÎEµ€¦³!Š`øf”vp“±°©%@™¿Be¾†BøÊª€­—¬ƒe ´ãªJ.Í®FPµÙÔR/û J¨ùžÆ– Ò1Ú8Pƒ¡I×S#€·~vHèÓØ| ¥ÏAâxÂGö#¤-ã>±ð?:!ò#Kíücºšq¾ñt†ê)U‚Q´ª<©ž}¬äUájÓ—*È“ØÐÆRø\'õíP¸ËûŸmiPn†2ó¦G°Ðg`>Ñü „YLfÎÎ@´£ó¾iÚ¦rG¯Ylô38Ÿ!Ý›Xèó˜rz³ùÚˆFŸe áÀ+þÃîŽü]ÚånØÌÂõ*â`Òz9dÛá΄qÎfánN_ ùœR:?+ŒlËói)F“Û¨`;º|Úö×NýÓU1¿ààf©ë8ÀûÇÞè<zÄœ‡ø·û³_~tÃiŠ$Ô@‚8#xÁɈ ꤆1àˆ xü“(vÂÐÇHp ¡!gZ qñì|Íù±‚Ù›ôUæ"”pß13‹Ø4W¦9Rÿó˜Tr–$ÖŽÿ³$±ŽÀ>%X@†,H QÌÀø!&<"`lx†“)¦€2>ÙGhl0ˆCqvsQhø…Ó2àž8eeÎnÄÚ¦ ¸#4WØ1¤á„Ñn$µÛ¶Óqt„tØñúàìaE8.Ú‹øvæFÞî3¨xÍNš‹z ÄX <˜‡¸U>´Ql´·¬F¨¥ïxF#Ò*FqÜØÝr²Æî–„-ºe®A k€Nr»€1|#Ùëv8eoeŽCöï¤ÑUú¥Qg; ÝŸ-€Ì×Ý£kÍþ/,*£ºu6_€‡Âž±¨»b ‘ Þ ÕõW4¦<š/Hà÷¿X¬ùɉ#±xœíYYÛÈ~_`ÿ!¿x±Ù›Myäc±’—¬ƒy (²%qM± ²5#9Øÿ¾Õ¤xK3ãÄ^Ĉ5˜²ªú¨ª¯ŽnÝÿp:d΃*«TçëAxá¨<ÖIšïÖ‹¿¿ÿÑ• §2QžD™ÎÕz‘ëÅo¿ÿî¾zØ}ÿã80<¯VI¼^ì)VžWË érç%±§2uP¹©<‚ˆ·Èǽ|\ªÈ¤*Ö‡ƒÎ«zh^½J—ɶ||D¬–"az˜z”º áVçÜD'w2öym,Å{Àˆ¾PlUq øíä[ªô±ŒÕ*”+ã½{ÿ®cº%&Γæª8*Ôhݖؘ!:¨ªˆbUy-½™à1MÌ~½ ¸yÝ«t·7ýûCªÿ¨Oëv°ã#!ïŸ.R½ÓICI“õ”•—·Ë’«!:q^³ML•$lë/Š)vqèâ`éÄÇÊèÃ]3ºÕ{•èØê±^lÊ(÷nœéJ%¨3j·Œ:º4î6ÍT3ÀÛëƒòÎ*­tî½S*Ó……“W¤(Qi¼4Öù¿²Ô(Tä7æ;%¸*×¹ç–ûֲﵭj¹Æö•6,`V朩 ×qÌٺΨ“ñâªZtt;.>–%lÔu¦K·Š÷í4ŽƒþdÉ?×T÷=Làü»íÔCV¯¶Ôþ¼ié¿¶÷^½‹Ï³%—}þMÝ{ÖhÍc‡ëÏÄ¢r`ÛMT©v‹E´SõëÅ«mýi9]&ªly¢þŒy$5ç&wµó·^¶wø†@µýá3ã~ÔútÂç\Ìø1Ä¡(à’J2çš!ò%Á„ÍÇB\-šÝcžH>Åi>ÁÅgYtV þNÊnšj¯w¥µ£)j6ò1ÍA%÷’'HHçš_DÚÜA0æ·dl&¹Å;?Á;D§ô~T°Ë¹u¬Cëo£¬Äm›ÔPÙ«øƒ*7:*“ÉÀÚ.Ç4QÕ ËTyT¸›ÍŽWù–å‘Ùßš Èõ VpU²Sî!M ææyñI>µ²Þü¢bóäîë9` Hé Yýyéê µÙ?¯ò‹¶¿Ëô&ÊÆÛÔTÊ]š»F< ™Úšëœ²Áï5ÖFcxÐ"OÁ ‹ÎR›ÈÔÕw… Ÿ"p’}OgKç^K ÞÕ¡°u½î±dO¾Ðl¸!*¸/£3îù:7]Õ4l+P¥›Lµ„ äP“)Ùºè2Âî9Ksµ7;Oå4=͇i¡¥ÕéÀñ.¥`^ÆA™(‰L4(-Éï¬ =Ýêoï~ì*Ô}¯þ¡Ë}Yr¬H´ÑGð_ÉlWÐ…"ó6=@¾°Ü é‚ÚÔ1ÆÒÖwƒy›™KÕ4tW[Û$>¤v”÷³I³ìÏv™VïÁ´©²X/Û<öó¢L«¬7ÔöÞk­Ñ¼î¦èÌ¢‚ ú‹­ Î<µîJ},¯ëE]:;ׄnˆž¬²±†Ç,2ê5^ºÐ(¢KæßuîØ!-yÐû~0 ضLO¯¡Öúœ…Kl.¯>[B#bB}"ø’JŠ8¡˜Ýõî,Ô.å“‘ww#OÕ"ûábDžo‰ 0”‚’fGÍ‘þ’ˆQÎ%_ºŒ0äSFøÝpAXÒ&ÉÑôkw+¹±²‰Õ–D†Hà/&#ê&i½¸ô#+òZÝl•ñª~r[Fú•3ijxl+'¶ö½î=š¿˜7ÕóÁq¡÷ÎÇo¥ðsÀà[SüDS|³ƒcɌױØc}Þ|¡ø‰ˆðÎ ]Ý ÐŽÍë¦-›ÈŽ^3lt=8ŸcÜê¸DÈ»Z|눿KäÃWþÃîŽü¿´ËMØÌà:BtZOCÖ¿ w.Q ŸÁÝž¾8F‚16?+ l†åy·â»Ém”·\>íºk§îi”̯ƒC/5ÆiÝ?ôFëyÐ#Â'ŸîÏnúÁ §M’)œ’%Ç$!™;Ú"XŠšÂ|Á¼ÄÃäÙú8šó¹„Ù™txöô¥ÜðÖÍõ)úêÕ›ë‡ÈÚŒ°¥ øBܼ{nåñÚCúÅ Áˆ,Xº<„bM¥÷ÜÍ›œž<¯]¼ÁÙ޹ôr(qàXúKW NYh9@ŠéÍÛíÕ`’2*hx„–àd–|6Gcf¹§†çgÁêc2œÕÚöô§÷ðvA!#>›•vûÅ# P(ùüœjïÃàå‹n¯æ¨eÃ= ø,›7_=§O·èM;Õ‘Mˆ¼c—vó §A€”@.!Ò™$ÜqÇ„Y’ìOn³5n47Ÿ˜%ÿ³°>ê%”i[?×gˆ !1¿ë¿F±ËßÛ/1àÿoÑÏ îxœíY[£È~_iÿb_¦SÔ¼í^)­)yI6Š”—CÙfSNQnÛóësŠ›1`O¯’e”q«»Í9§.çþUñüÓy_8¯RW¹*W.AØud™ª,/·+÷o¿üìE®S™¤Ì’B•rå–Êýéåûïž«×í÷ß9ŽÃËj™¥+wgÌaéû‡£.Ò[?K}YȽ,MåD|w Ÿ^åS-“¿ÊTí÷ª¬ê¡eõÃPZg›^üt:¡«¥HÇ>¦>¥HxÕ¥4ÉÙ…}Î¥cxÑ7Š-+0Î~{ùŽ€*uÔ©ÜÀ@‰Jiü÷¿¼ï™F™É†óäå‡*Mòfݎؘ!ÙËꤲò;z3Á)ÏÌnåRÜ<îd¾Ý™ëók.O¿Wç•‹ìˆÆDÁ¯ßZ©«ÓICɳ• ÊFíS»ärˆ:ïd$Ò(ÄQ/Š)ñ0ñH°pÒceÔþ©Ýé½ÌTjõX¹™:•‰Öê„zƒöKÈóAiãmòB6ÂþNí¥‘y¥Jÿ½|•…:ØPò¹J¢Ÿ§ªüg‘‰åùÎÙÜ‹yî¥ã¾Xös&7U-×Â>R×ñf¯ŽÝ^f <]'UëÇ9$[åBé•ûæþtœµÒ™ÔOÔŸ[ž_çæÒ¤a7·i;q/€ïT»Ì ‘0á~Tj¿ryˆbFã);…h!1Gá –ŒQÅÇñ„ >ZßxÇ27F‡ót‚£ÖV¢H.´¯ÿ‘NªÚ©ÓV[C}”“±§¼¼6æIL§ª·"]Œù=›÷x—¼}rÎ÷ùG »$«ÁÐü›¤¸FÄ}«Ô±²“é©×*ÑÙh`m—cžÉêŽeª29xëµÍôY¾ey‡ÄìîMP ”ê +x2ÛJoŸg•—æÓâo’|´²Zÿ*Sóp÷õ°”'… õiéj¯”Ù}Zå7m[¨uRÜJlr¡¢·yéuÄÓ€QÈ™çè&~çXkeŒÍài€Ö!ò( úüÔÊ$¦®ä¸©yPØ®Ó@¶#Ç\l_:_,Ñí©6E-%ù•(÷Û£j¼]É-ͦ¢‚ct½Ìs3ÐU޳Á¶µ*_òVKØ@™5“­‹ÚvÏE^Jè%Åe,§Àèy9, ­.]ð§ aì¥I²Ä$ƒ~Б‚ÞÊ€O–yÿóK·Âsš.ÿ®ô‡~EDZ"ÉZÁÿîË•þœ¥K@ûļä{¨üijeÜJ[ß æmfÖ²'³0-K÷¹åÿÕäEñG»L§÷`ÚÜò¥^¶ùÚëâ·ÊtÊúCmŸýÎÍãvE²–D²-Á™–Ö­VÇÃòµíîÀηmÄ褬¬E¬‡ák‘ù/<=Ð žzwloC:âáÕ÷ƒiÀ¶:?¿ƒfPr/°ýi¶PcB"ø‚FqB1{ººo°P·T@n¼»½ñT-Bp»7äé–ŠãHPÒì¨y"Q° !b”óˆ/·I= wÇþg[º*7aÙ‚9kÅ-@Œ‡gŒö(r=º&ø¾uøŽÝ¯Áo#Ãh0[ŽqDc« E<Š&…"Á£Eñ˜×ár@EŠÂqyëϳ-?sÑc7 §Î/áI(¥œ÷‚_ƒ'Ó\§…ûÒº RHDtâJ{¢ë2L˜çz$¿›Ä!ý" %XEôkwà{×\¾óxð4gsªÂpÊž$Hí.6!8œ¸ üL(˜á]]6IªÏâ2(Ø‹¯Èe¿ =eâ£ÏÜ9c<ÉÇ®sò!ô°Ï5öhþbÞtÏWÇì9¿µÂÿF|Å@ñ]8Ç’6çcñëSð™ ð DxL'®Æ@´cÓ¾iÛ& „€£×$6z Î瘇÷0ˆ™`³Í·Æ@,ü"(€oô?vopäÿ¥]î†Í$\o"Öã fÃG(‚OÂÝž¾8F‚16=+ lËS´ã§Ñm”¿\>mûk§þÛM1Ÿpp Xê6Hçþ¡7:σ±ùíþì§ÜpÚ" 5bÁ)Yp E2sR‡Ì£$¤8^`ÆN ‚#QSX ¸ƒxX<;ÇCs~ª`ö&™«+œÍpeíŽmÙY0 9~d²À¾o ǹRýë˜h9I–Úžo7î­ö5Ä ÷&½UóþdŽUCnÎöúÓ9Å4æ`t8ÎhbßÕ1Z¿WãS{Á-±KDo‰Åý(q‹¹]‚ÌâYk² †ÓñŒ‹7Eb¬¢“Kð^¢.E–ç˜I™©öåio boR¡T„!qD3{õqZ10‚­"£Ð|t?ÑÌphÇÍ^¿ÂÀKäé-ÓÕ˜ ‹0 Tzú{{[ží­9üÿ7(ƒxë ª image/svg+xml ” txœíYYãÆ~7àÿÀÐ/;ˆØì‹dSÖŒdaØ@ò’8—€"[½› [#i½«yÒÌÉ:^dµØ±ªúª¯Ž¯9›ï.ÇÌz–e•ªüÑ&Û–Ìc•¤ùþÑþÇOß;¶*åI”©\>Ú¹²¿{úú«ÍÇús)#-ëœêƒõcþ¡Š£BZïZk×=ŸÏ(m…H•{÷Ár ƒ«çý×_Y–kçÕ:‰ívLq*³Ú6‰]™É£ÌuåD\{dö±ÙAú,cu<ª¼ª‡æÕ7cë2ÙõæfKgV[‘0 ]L]J°pªk®£‹3 û¼5–bŒ]ÐLßh¶®À³üïí;ªÔ©ŒåJ”Kí¾ÿé}¯t0Jt2ž§sìd݉·óè(«"Šeåvòf‚sšèãMqóxéþ ‡ççTžÿ¤.6¶°å!ß÷ùð­µ"†4’4y´á°¢}j—\÷†…ÁÚßìêO§Ùª2‘e§óëÏT§ÞT_›´íæï6m&î ðƒê%ê à/´•:šqœ pê- b‡û€¿‡YªaU_ *pà/g€Oç”§’§¸,'8•¥±È¢«Ô¿úeªƒ:ïKãK]žäbì9ÍáXNé$¤ËÓ·&]ôŒù=“ ÷t×tÇè’Óv¹ô9Á]” Aqß+u¸düA–[•Él`í—SšÈj¦ÖÍ£ÂÙnM‚ßtQ9E¤ÕK¹ª—¸iЯàÈd/cš*Íõëæo²|ieµýYÆúÅÝ×sÀP•À ÓëÖÕQ)}xýÈoÚþ>SÛ(›ZìR ±RîÓÜѪÔH‘ɾ­)›¾¥Ú*­ë,^쥎‘q<5›Á²«eYúj:Íåj„v/5ég$aÀ¡<¦ëÔôA âVfR QŸ{‚1ºÐ^ok8†œGºiTUºÍäÔ—°<i2ï·#Ìž³4—Ð*²ëÜN?Ó|œò¬Nõ®¾»Ëß(ŽRGI¤£Q¹ïD^ïe`뿽ÿþ©[aÇëªòC¿¢e“h«N­ý4È7I¼ŽpŒôSz„Z`øÅlÜA1µ6Øæmf.eC7n¯$>¦f”ûwfÙf™îÜ£iSÉ‘tã¶gèÎ莹q;'4ûy@fÑVBZüÅTykY-÷¥:GÈÀ¶Ø#÷N;ƒ.£¼2Ž0ÀÂ× ê;¼r€½  æ=ô(ì§‘,x0@>š\Z¦—wÐB=ŠÎÂ6ÿÚG­€…˜Pø|EB1{P-Ô-å‘ ¨û @µ Á^hOÄË-†Â§¤ÙQóD„·"b7_9À³GáãaISö&ӼݯäÄÒ”JÓå"gÏFTúj°¥kò-²lÃE¡þæt  !4 á·•.Õ¹n¹Æ­ éÕ`ˆ`Íœ±Nn6µ†È“±ðg¨µS)D­,3h¹zÍ;YAƒ-ËèÚìj$U»]%õºßÀpˆ"‚*îÔ kÝ(-s ª*´Ýjî@ë¯õ Íg§D>œZŽ@¸þé+Ê}Dkù–‡QP[[ÿšÏfÀ7ØÁªž©œ¢UéIzŽô©”“zÕ‚ÓW(È“ÏИbøLsùv(ÜûŸmi8ÜBeêäM/ŽdÜ~€[ÉÒi/E®cB—Ã_·ß9ü%à6sL nó À1î{bî5ÐQ!Á…Xxúf€|.&¹®£Ú̃Š$‚yyë/¦­^ø·¢ÇlVËXÿHB)å¼7üŒÓ2ÎäK¤/èJsIë2,”—z$¿›ÄýMÊgžôs‡aÔ½K¥ )q8p‘>w Z!,B‘ 5\|Bp°€ p&ùžC7@¶HªOlŽýϲ_ņ.£ž²ÀèwÎ/ò±ëœ|L=ÌsÍ=šŸ˜7ÝóÙr€{ëã—Vøßƒ/¤øR|—ÎÁµ¤ ÆÛ±8Äú’|" ü"<¤‹FWs NÇ–}Ó´Mäù>\½±Ñs óbóàâòC泛ͷæ@,øM8^ñ;»7ùé—»a³×IÄÓz9d½›áÎ |Ÿ/ÂÝܾ8F>clyW%Ø"–—l)ij—Pî~ôòiß¿vê¿MŠy5ôkpöWA˜;;¸]´Ûî:•‰Vc'‘¼ÛÊËÏæA¤noÎ^€¸”²RÎ.ËEÃãäQxW‘Õ²ðÞŠg‘ËR’Wf q¥¼,‘ÅÏy¦„[wÖ»¤%8‰‡·±×û¤Ñ©ØÕ†®1ƒ~%¶å5È~WZ½T›wDºëÖ-–UÆ{ä\Vû«ùt˜­¬RQu¸Ð|¦8 žÎÔµÉànýNi½pO€îÔ‡8•gˆƒö½”ÇM‘pÊ¢pO X¸K£E^bAfäR†¹.×OŸ´sœS‘)È¢ò²\àTUš"¯¶o~õbêƒ<ï+mIUÄ‚÷œ°)§ yÌÉrï-I—!zF'Å=ÜõÜ1¾dÇì½-—öÑ;ÛçCHÜ·Š “ÕVÆU:c4v9e©¨gˆAn—Îv«3ý¦é4Ê)cu¨_"(¤q“ —àˆt/œc––2+Ô‡É_Eù’d¹ýE$êEíÍ ÊB…ú0u}”R>¼åW©¿Ïå6Χ»LA¬Tû¬p”,G5Bäb§ncª&€o¡¶R)ÃË512ƒ§†âq €ìj¹,K]u˹\5Ðî¡:ý4„GtŠc©Û™#Øna:•\Ò€ù>Y`¯·±)lCÌ#]w¬:ÛæbjKP ˆšÎÁÚú-‡Ö9Ï "¿Îé$Ø3+Æ)ßÁLªwÕÝ[–÷q*NcŠ} z+Ãè±þñí·O„Ç$Yÿ]Vïz‰–¥Iâ­¦c R׿Éò#k÷’œDèš©Ûïâ(°gµºêLl§5þæ²|]ÀÑÁ<9¹”ûŒD˜]«J¾ëväA¨4MQs4õý®3”ZCéø Ý)ÂWT9ô^µ¦,¡ÓVU|m´AånW µî6QÆPÎ3h­¤¥wcAy…þ[ÏÞúÁ"ësýYÁ.Ýö@,‡¹È|˜p¢¡¡K ü;+@nd¨­ÌWÓÎ×¾aÌ_ úI`%+¦¥çX*1)\­súRy£:TŸiRß…»¼ÿ™JÃæ(]0oZq àDñœS–F{)rºø¾&|çî¯Ào3ÃT`¶ œOÀͭ8ø.£Œ-,zÑÓçŸñ9®›¹ý*‹æå­?ª¶xÞŠ­,‹–±þ< ¥”ÒžðSðd’UI.æ¾Ôî‚ Y¸RŸÖºLà äÅpÒ»I‘ß$¡B?`Œ|ênuïJ*=”8f‘6w Z¹ˆñp‘ Æ]l‚Q´pø7 ¸Áe‹¤ú(.ƒ‚MQø ¹ìWMC—QOYøè#wNŽùØuN:=ô»™=šŸˆ6ÝóÙr`ö`Öûϭ𿟇â†â»ãKÚ`¼‹C¬/G€4¿0aÊɢљ(„Ýù˾©Û¦„!½±ÑÏ@à|Šhto¢r?ôo6_3ùÑo2pàeÿÇa÷ Gþ.ír7lá:‰8˜´^Ùàf¸SæFaHá®O_¹¡ïû˳Â(Á±¼œ–8z˜ÝFyûÑåÓ¾¿vêŸ&Åüv€ƒ˜¥¦q€;÷½ÑyöÁÃ0À¿ÞŸýò£N]$¡R‚WA‘ŒBæ[‰…aÌ#8"ˆ¯aq+\ŒXh ~R ­Ð¸xv>æcs~¨`ö&}¥¹Òˆ¢;æê°S‹Ís¤þç)®Ä"IŒÿgIb]Ÿ`²" 8ò1G8àNˆ€±áN¦Ø ø€ø g0ó q(îÀ®/ 5?³ZÜÓç¬Ô:ŒXÁ>îõUc#v i8a´IíÔ¶:ŽŽÐ4@ï­#¬gÀU{ßê¡oä&˜BÅk4i.êc†ð x»ùÀD±Þ½aÕB }/À™1j‘fAÄycwÃI»ºê–™‚þÖävmøF²Ói8gå·²ó€þ;i4i@¿³4êlGÀà´û pƒþ&{t­Ùÿ1‚AElT·.ú»í€™3ÖÕc÷!²Á{´š~E£À£þ‚~ÿ ·µü"U&xœíXm£Fþ)ÿ#_vÓt7MÓ8ž‰t·Šéò%Éé¤ûahÛd1 =¶÷×_5o·=;Q²Ñ­nÍ TU¿U=UõÀêÛÓ¾pžeÝäª|t ®#ËTey¹}tÿõËwžpF'e–ª”n©ÜoŸ¾übõ7ÏsþQËDËÌ9æzçüP¾kÒ¤’ΛÖÕÒ÷Ç#Ê{!RõÖp<†ÂàæyûåŽãÀÚe³ÌÒG·SꢵÍR_r/KÝøߨ§ûÔì –©ÚïUÙ´CËæ«©umFs³¥cÐZ‘8Ž}L}J=°ðšs©““w5öyk,Åû ›˜¾ÒlÙ€g+øíjÔ¡NåJTJí¿ýåí¨ô0Êt6gpìlÝ™·Ëd/›*Ieãòn‚cžéÝ£Kq÷¸“ùv§/ÏϹ<þ]]ì`'D4&œsv¹ë­.ˆ!$Ï]8¬èŸú%—£!F1E0O–p§ Å$ö0ñH?épÜe¦R³ýG7ÝÉôÝZ~mod†FwŽ ÈS¥jímòBvcüÚKÿ,óF•þ[ù, U ùU®A’ÔÚÏSUþZäZ¢ª¼3ß)« H1¿­=Ú'£^erÓ´vÌ#u¿Sާ2ÛËŒ{'¦ë¤éÃâ8U² ª~t¿Ú´× Y«:“õ ãí5×)ˆt®Ï]ó›6øŽA³K2uXÚ÷Jíaâ…À"´ô)€…#Ž·µ°fŒ0HÀ"K ‘>˜àx‡2×EÕÉžàPׯ¢HÎŽßþ#ƒU³SÇmm<©ëƒ´Æóåõ'1µÏÞ› i@0±OØÛ˜¤¸§;¿ Û'§|Ÿ¿—°KbÙ˜Lý¿IŠ $î{¥K›õZ%uv5°õË!ÏdsÇ3M™TÞzmý¦Þ¨¼*Ñ»{´¥zÅ žÌ¶ÒÛçY¥òRØüU–/­¬Ö¿ÉT¿¸ûvXª˜Bú°u³WJï>|äWm[¨uRÌ-6¹¨ÔÛ¼ô´ª&xš( ¹Ñ·5u‡ß[ªµÒÚ¤° Ð"Swß®îb/u’%:™ÔúAŽ^æ±üéíwOà «4]þ[ÕïÆǘ$ku€ÐºOù*K—Àö‰~Ê÷P Ïø¨ÁÊ¿(æÖ&v“y»™kÙÑŽ›,K÷¹åÿ¬ó¢øÁ,3œ{2m® ùÔ.ÛÝŽgñûà ‡õ§§]ùƒ7ºÇí52‹d-!?þiª½cWÍm­ÕR±oîÄÏó¡ë¤lŒGL„á¶Êú/< 3(‚Ž>ŒáØÎ!-.ýj6 ø¶ÎOo ‘†G,ˆØüôa°ºcBCÂÙ‚ `=„âàá¾ÉBÃR!™Ew;‹TkBp»3±½%‚âXpJºuOD„ èÝeL°…Ä …4 ìaº ,iêßlú‰·Ç•¼Tššiº]€HºW#}6™Ø“%ùhY±,áÍ¡½óF,p‚ø›F×ê\öŒã^Ðõl0ÄÐhƒÜd,lj (³©ð7(ºs)ÀWÖ´^½dƒ,K ÑÖurîv5‘ªÍ¦‘z9nàrˆ*rîµ8c©‚¼1‰ *…kžÔ·¡pwìÛÒåp–ÊÌ›^œÈBx¡ø^Sl§½„\Ï@—ÁŸáÛÂ÷:ü5ÄíÊ15¸-„0Šk¯Ž†LË£Ð@#Ä™ˆ_ëÊ„P‘Dt]ÞÆ7Õ^/ø-ô˜ÍŠÈÆúGˆ$”RÆFÃO!’i^§…¼Ž¥ ¤Ô ¥yY2XÊS;’ÝMâˆþ% ŃPú©‡aÒ½k¥ )ñp‘>÷ Z!,bn%H.>!8²Âq&ñßÐ]Bf%ÕG l†ù'²ßņN“žbÅè#wÎ[ù8tN6¥æ¹åÝ_̺îùìxÀ=„óþs+ü3`ð™¿@ŠïÒ9x-éÁx‹¬Ûà#Qà8a1µ]Ë8œ.°û¦i›(ä^½,lŒ‚Ï0‹îq "<¸Ù|[D  á…WüÃîü¿ôË]ØXp!˜ÖË o qÎ,¸›·/†‚À~W˜$˜…e›-Åøáêk”¿||ÚŽŸÆ»Y1¿ p \jŽ2„!òpŽ˜óüþxŽÓO¾pš" 5bÎ(Y0 E2â"pR‡Í£$¢8^`ÅN"‚o%AÈ™ƒxZ<‡ÇSw~¨`Ž6ÛVæ³"üÿ/×+µL "KxœíY[ã¶~ÿÀ*/;ˆ%ñ&Šr<´] о¤) ä%%ÚVV ‰Ûûë{¨›eKöLÚlÐA׃™‘Î9$Ïõ#½øö¸Íѳ*«Lñ°ƒT‘è4+ÖÎ?úΕªL\¤q® õèÚùöéË/r]ô×RÅF¥è™ ú¡øP%ñN¡wcvsß?^Ö=]®ýäº0WÏë/¿@ÁÚE5O“G§³Û—y-›&¾ÊÕV¦ò‰G|g Ÿœå«Aö¬½Ýꢪ‡ÕWCé2]õâV¥«¥HE>¦>¥.H¸Õ©0ñѽ zN¥cxÑWŠÍ+°ì~{ùŽàUz_&j•W(ã¿ÿé}Ït±—št8OgØ‹u/¬]Ä[UíâDU~Go&8d©Ù<:7¯•­7æüþœ©Ã_ôñÑÁ£À£BðóS+uŽÒP²ôÑÍÊö­]rÞ b/¢Gï‚42Ja3D1‰\L\ša݆ç©Nì2N3½Ü£‹_’J>¨ô—¬€Y×›¶_Lwº4î*ËU3Úßè­òO*«tá¿WÏ*×;Tþ.ƒ ý¸4~–ÀÌyf”·+nÌwLwà°HLsO÷ɲ©ZUµ\cûJä7Ì~V½Ôšz ºŒ«ÖEíâ5u®ËGç«Uýé8K]¦ªìx¢þ\ò4x=3§&›»ù;¥íĽ¾!PmâT &FÜZoy2ñaÉOp­BãåÀÑ{ëw_dÒhwÜ—¥•Èã“‚=×ÿH'Umôa]Zó™r¯FcY;qÛ˜'kЊty@0 oÉídl•– ;ã·Ìm|̶ÙGš’‘ŒÝÅÐð«8?ÇÂmËÔQR'E¹Ôq™^ ¬m³ÏRUݰNUÄ;w¹´Ù>É·,w›Í­ jB¿bW¥kån³t§³Â¼,þ*É{+ëå¯*1wµ¯ç€5 D(T©—¥«­Öfóò–_¥þ:×Ë8¿”XeB¥\g…kôRt‚‘«•™æ”M O±–jèvÀëu©CdOÄâ, ÖŽBÈœlÛ9ž,Ñé©6-% ù™¨¶;Û‚j,!Ïä–f{ŒG$ctÄ=MsS؆ºtÛµªl™«K[‚E Ôôšl­ßް:çY¡ Aä§k9 öÌŠãÀhÍj×Wu\ÖÆV™8M<(ò)è­ ðcþãûI2ÿ—.?ô+"dEâ¥Þƒk§3}‘&s ÛØ­ûk§þ题O88,u¤sÿÐça‘ùíþì§\lÚ" 5bÁ)™q E2’¡€y”„G3ì…„† `)j Gx†‡Å³óq44çK³7é«ÌE4zµ¹((Œ%%Á[H”ú‡àxÍTôg$¼*|²Yÿ„pýR8€Xà3‹ÇëÇ;Ò0- ïñ-)_;ÛYÉŸQ­5G1 ¼ºÙFlÖ?5òHÌÄ®û"۽ǯك8œº:í.õÛ«ÓÁëà^ö"’ëã ÿI$_¿~ŸX†œ#!ôü&@˜µ`cɨ¨>Ò{ñ׋õ¹2âPU8 ’²þ:ÙÊvþâÃÖ×…ýÚþÿ£½ÜfJ"”xœíÛŽã¶õ=@þP^v‰"EêæŒ'@»¶@òÒ¦)—@–h[YYt%zÆÞ¯ï!u£-{vín²èz03ä¹<÷Cúþûã®B¢iKY/ЉƒDË¢¬7KçŸ?ÿà%jUVY%k±tjé|ÿðõW÷íãæë¯BÀ^·‹"_:[¥ö ßßš Ëfã¹/*±µj}Š©ïXôùDŸ7"Så£Èån'ëÖ°Öí76uS¬Gò§§'üÄ MÓÔ'Pxí©VÙÑ»à…s^ã !>à,Ò’-ZPÎ~Gú€[yhr±Fk¡ü×?¿‘Á…*ìuÊúm›g{q¶ïìÔíD»ÏrÑú¼[à©,Ôv餛nE¹ÙªiþXЧ¿ÈãÒ!ˆ )¢ˆO£žj2:í e±t@ؤŸõ[.lïÀz%’(Ob’Ä©‹PP†.Ê­’»»Ž{{QÈ\˱tò­Èß®äñ·Cm†¢À£fǽÄq/å­ËJt\þVî„e+kÿµx•ÜkŸò÷¥HÖ(¿Ìeý[U*÷õõŽÅì•F×±§û Ñ÷…X·†®Óˆžò;ä(—>^¡5m‘®²¶·Bûl>]Éfé|³6Ÿ³’M!š™Ï9N‚ÑKuêâqX8´^x$ 7ÚmVÈ'p‰ö”»¥ÃBLâˆFé Ÿƒß8H‚8ŒÙ {&˜‰ù ¦>hÛx‡ºTOûãœÿÐ4š¢ÊN¤7ÿè@ÕnåӦъTÍAÌxŸÊdòzç§i0½'‚2?eO£Ããîô n—Ë]ùNÀ)éŒFK`«U“GÜÖŠñÍJfMqÁhôr( ÑÞÐL[g{oµÒ!¯QÞ>SÛ[ ‚Z¾`OáíÊb/ËZ½ŸüE”Ïí,W¿‹\={z³ìy H!U½ŸºÝI©¶ïùEÇßTr•UçëR«4›²ö”Ü[þd!*±V×1Mç¿×P+©”Žà¹ƒyÎ Æøl¤Ê”Ié¤Ëyئe {N„ÔI¨ãIªCTCÒ) $v{]¬LãLà¦Ã Æ‚öt[€¬â2t}kËU%Î¥„Ô@‹K°6QÏ¡Ï\•µ€ZR.é$(½¬í´0ÀL: €?¯b'TVd*³êÁ G-C£²øûë†îó|ñ/Ù¼wDH“d+yû;ü¾ÈÐZì2õPî _è¶ä[è$îý qN­mg­Û­Üˆ®K¹Ú¯ù®Ô\þ?TYUÓÛ r[Ë–ªfÛn8Êâ÷ Âú¶´÷þ nº¹ôÎ*[ ¢uI@óÔºiäa¿ƒxí«†céù¼Œ¨&«[­maV™¯ˆëA÷ƒ¡maáÝhŽÍ¹K'<žlo-ºmÊã+(¶a…¥.Ñ?ý4d.tw)¡AH#îBéÄœ„ÝMæ³6¶ é™u7g–2$”„©sž‰â4M¢€v'êf4 ]cpžp×c”á0`”ßÙ–:Iž-oi{ÜÉË…N¬º$2LãйàhÕIGbß,èwкU‹î fä ‚yÊ © éw­jä[±è»"Bz@WØÄÐusƸŽX8Ô .làï™Ï¡à¾¢© >«`EÕ¸i²Sw* *×ëV¨Åx€Iˆ}9ß3½Ø¢C"- ‚ 5º½TXë'„˜¥úã‚”8ä%˜˜O"¼Ø x„ƒB‚cC~½\M_Û&IØ 5öQ²¥(ÙxÐQ=fêЈ³ÄÕgLU7:°¡Œåð9êë®p“÷¿;Ò$Ü ¥æU-Z°sP¹¢´ç<×Ó®Ëa/îkÜ÷Òü ØíB1 ¨ î!ãQ˜\j pAÈp“d¦Q( 1Žx³$½Ä }9Ü> ‰/ÓÛx±íñItÍ{ôaáúù), ©”ó‘ðs°d^6y%.m©Í!%ÁÌ”úB7D!†“ß â8ø$±0I‚ÏÝ Võ6 ¸xåñðîšÎ=ÈV˜$i4 c. :¡$ž™ ìL…ÑÜd²YP}“AÂæ$úŒLöAÝÐѪ)3}äÊ™’Y<•“Û­‡ž›Þ£ûKxW=‘½G‚Þ})…ÿ 7øÒ?ÓßlçàZÒ;ãu_œ|}Þ|¤ø™ˆò4˜:ÓE ›×Mó„F\½f¾1ö@`|Nx|«â!ŽR±«Å×ô@,þ$=PÞäOìv/0äÿ¥^nºÍÌ]Ï<:­ç]6¼êî<Áqñ™»ëÛ'8bŒÍï V€Í|yÞ-¥äîâ5ÊßXO›ñÙi%óëNΡ—:÷:˜ß¶Æ`y#¢~¸=Çå­N$!$âu9$G C9¢Ðæ4HêÓ NQbJ’È@XqD\b'ÏÁÆ©­Î÷%ÌQ¥ç‘sS]}<œŽ/EÌ/£å2JÚ²FÌÂÄhò “Ayad¿j÷±¡_k˜i÷&Ô×p¦‰Ç,0߸N`]4RŽá^[jÌ󷾟éU,ƒ¼Ì¯ûïgló¦Æqš‚»³Äe诈á$I¡Þr˜1wœÁx¤C¿ …0è4‡K# Db‹™ÆÖº0ysÆ20h '~=É~™VÐ,±u˜‘ŸMËÂøWô ]޶ˆn‚Ã¥12¢®N«c =QGá4Èë 4ÖÓÞ ¶åq}ºž^ሆÆ©,znŽb½¡]{뾎éߺ­©õ>‹š†üƒsO—²ïõWðÿ?˜²ëÝÜxœíYÝã¶ÿAP^n‹")’¢œõ(A ´/iŠ})d‰¶•“EA¢wíûë;Ô—%KÞÛ ½ ‡ž{+Í ?fæ7ä>þx>æÎ³ªêL— ì:ªHtšûû÷_ò¤ëÔ&.Ò8×…Ú¸…v|úö›Çúyÿí7ŽãÀð¢^§ÉÆ=S®}¿{7caŸKc)ÆØÞHôbëŒSÂÏ ßP­OU¢v0P¡Bÿý¯ï¦‡QjÒñ>´£{½×©N¬79¨äCžÕ –PçRWÆÛe¹j…ýƒ>*ÿ¢²Zþ{õ¬r]Z(ùef€WÆÏ]ü+ÏŒBeqg¾sZ‚›"±Ì½ôÜ'Ë~LÕ®näZCØOê:~ËÔ±ÛK­G¢Û¸îã8e¼(çºÚ¸ßíš§çlu•ªªç‰æ™ò4ø:3—6 ûùûMÛ‰|G >Ä©~$̸µ>ÂÄ‘ Ä”Ïø‰… Š‚Áœmw%ŒxÊ||²ÞñNEf Êó|‚SUY‰<¾(Ðêeêƒ~ÙWÖ¦:©ÙÈ—¬¼ó$¢sÕ;‘>ÆìžŒUóïò cöQÁ.ÉLÆj06ÿ.ίˆ¸o“+6*TµÕq•Þ lìrÊRUß±L]Ä¥·ÝÚH_ä[–WÆæpo‚F ÐoXÁSé^yÇ,-uV˜O‹¿Iòµ•õö7•˜WwßÌk@zQÈPŸ–®Z›Ã§U~Óö÷¹ÞÆùTb—€JµÏ Ïèr„§#W;³Ì©Zü.±¶ÚÁs€6y CtVÚĦÉä¸ÍyØ®Ó@v#Ç\l]:_,Ѩ6D-% Ù•¨Ž¥­QM¿ ¯äŽÖd*—A@gÜË27]Õm4زVgÛ\Mµ„ 1PÓ[²uQ7Âî9Ï µ$¿ÜÊi0zVŒÓBOkÒA_üyhGeâ46ñ¨ô$>Xú“õ/ïzêWxL’õ?tõaXÑq¬H¼Õ'ð¿ût¥?¦É:Šclž²#ä Û| Ä£eL¥­ïFó¶3WªmNÛ´49fv”ÿ7“åùŸí2½Þ£i3“«§fÙöuÐÅï”é•õÇÚ>ú½5ÚÏý-:óx« ˆþbë‚3O­ûJŸÊ#ÄëÆmJ‡;²sC†˜*.jkëaxÍc£Þá•M‚n%àƒ;öSHK^}?šl[eçwPl9Å! ¢¶ÿºO¬ ©‹0¡œ¶¢’"F(®î-Ô/ÅÉÄ»û‰§‚yäNÈó-ERPÒî¨ý"’¯Hˆʘd+/ â4 ìa¼ ,i“ädú‘µ‡•¼DÙÄjKb€HÈÝ›µ¹ØHì’5ùZ·|]À¡yózF, $ IôCm*ýA­»®ãŽÐvÄ!4Û,zºXØÔP¤câo™§T€¯ªr¨ÏfÍzZC5®ªøÒîjDÕ»]­ÌzØÀU‰2†œï5½Øºe:Vr0ÔèúÖà­¿:”£ ²Ï ´Dt Ž'n©¼pE™@´¡ÿìpŒÂFÚùçílÖùÖ7R3ÖÐEéŒbtåA?õ›S¥&‰«sΪ nl`CKà™õ2îŽý϶tUnƲ sÑŠ#G Œ‡Œör= ]|…oß[÷Wà·ÃT`6 .`‚Ë[«òI&åÌ¢P@C$˜ Ýòú¾<à‘dx›Þ†ólÇ—b =v³£CÃçô$¤RÆÁ/Á“IV%¹ºõ¥u„tæJ{žë#̘çf$»Ä!ýCJ\Jú¥»aT½›\½óX²¹Ù a‰Y€4î"`‚Ù»ÀÏ„"ÁÅïê²YP}—AÂfX|A.û]ÝÐyTSf>úÌ•3³xì+'·ö»é=Úÿ1k«ç³ãAï!_Ká_›âWšâ»íK:0.cñŠõy ð™ZàWz Â":+tM$@»`^7mÙD\8zͰ1ô@à|†Yx¯b‰(Ábñmz  üCz ^ù? »78òÿÒ.wa3ƒëqÐi½Y¾w&Q(›ÁÝž¾F"‚ùYa`3,Ï»¥?ÜÜFùûÑåÓ~¸vÞ&É|ààz©)Hïþ±7zσ‘œü~Ón8m’„H±`”¬†$ 8‰C Í£$¤8ZaFçˆ`)JÀsð “gïãhlÎO%ÌÁ¤£;¨ÎV­…Xï8ž®²—ãSáÒUY›Û]• Ÿž€Î–³æ¦,ˆPøzý¦lzÖ[¼Ýó`")˜ˆ$_y’2! '^óÊÞÜÚdE!šá˜ ö¥Î)&öþŒÈ ›©Ã•ý“'a4cŠ(urGvžì6áØë@A"²ê_n+Üг›ú61ün—†l–yàhGÂûíïG{G ¿ÿ ýÆ g6ostinato.org®ÃÃthemes æ8«material-dark8yprimary—ŸÔdisabled NvÇbranch-end.svg lS'downarrow2.svg Ê'slider.svg"u'checklist_indeterminate_invert.svgœgbranch-more.svg ’‹çuparrow.svg “Gradiobutton_unchecked_invert.svg ·9§toolbar-handle-vertical.svg Dgchecklist_indeterminate.svg Ë çsplitter-vertical.svgSœ'branch-open.svg ‡€§float.svg  'leftarrow.svg (Içuparrow2.svg‡£çcheckbox_indeterminate.svg! ‚u'checkbox_indeterminate_invert.svgµ¸'radiobutton_checked.svg ãtoolbar-handle-horizontal.svg©checkbox_unchecked_invert.svgÙÇrightarrow.svg$Çradiobutton_unchecked.svg AMsizegrip.svg hïGtab_close.svg H §vline.svgUGleftarrow2.svg >Yçsplitter-horizontal.svg ´é§checklist_invert.svg,§checkbox_checked_invert.svg v='branch-closed.svg ¶ÊGdownarrow.svg Qªrightarrow2.svg ˜Ž§close.svgìÜGcheckbox_checked.svg˜WÇbase.svg Œgradiobutton_checked_invert.svguÁcheckbox_unchecked.svg pchecklist.svg0d%+P%4+*~¤“ú ˜~¤“ú8° À~¤“úzqH~¤“úʼk~¤“ú xþQ~¤“ú0ÀÞ~¤“ú¬y–~¤“úLÍ´~¤“ú!Ø^2~¤“útŸ~¤“ú€Õ¹~¤“ú2"f~¤“úZöB~¤“ú*P–9~¤“úÜQÚ~¤“úæW~¤“ú'ôYr~¤“ú"îo~¤“ú$bv:~¤“ú%"aß~¤“ùÿ¬®É~¤“ú6ä†k~¤“úFn1~¤“ú5¨~~¤“ú4Òk~¤“ú-¾Vg~¤“ú+8i8~¤“ú"œHÄ~¤“úp3~¤“úŒ¦¼~¤“ú#D~¤“ú ŽN~¤“ú/z@Ö~¤“ú¸D,~¤“ú(Å~¤“ú ˜<~¤“ú4ê~~¤“ú ˜ÅV~¤“ú7°Í~¤“úz0¦~¤“úÊ{¥~¤“ú x½Ÿ~¤“ú0ÀH~¤“ú¬8ö~¤“úLŒø~¤“ú Ø`~¤“út^;~¤“ú€”þ~¤“ú1"%?~¤“úZµŽ~¤“ú)PUn~¤“úÜ4~¤“ú¥Ÿ~¤“ú&ôÎ~¤“ú"­¹~¤“ú#b5k~¤“ú%"!=~¤“ùþ¬n~¤“ú6äEž~¤“úF-`~¤“ú4¨=@~¤“ú3ÒÚ½~¤“ú,¾“~¤“ú*8(–~¤“ú"œð~¤“úpòâ~¤“úŒeõ~¤“úâ˜~¤“ú M~¤“ú.z~¤“ú¸†~¤“ú'„N~¤“ú ˜ûr~¤“úostinato-1.3.0/client/themes/material-light.qss000066400000000000000000000705651451413623100215660ustar00rootroot00000000000000/* ------------------------------------------------------------------------ */ /* QtMaterial - https://github.com/UN-GCPDS/qt-material /* By Yeison Cardona - GCPDS /* ------------------------------------------------------------------------ */ *{ color: #555555; font-family: Roboto; font-size: 11px; line-height: 0px; selection-background-color: #75a7ff; selection-color: #3c3c3c; } *:focus { outline: none; } /* ------------------------------------------------------------------------ */ /* Custom colors */ .danger{ color: #dc3545; background-color: transparent; } .warning{ color: #ffc107; background-color: transparent; } .success{ color: #17a2b8; background-color: transparent; } .danger:disabled{ color: rgba(220, 53, 69, 0.4); border-color: rgba(220, 53, 69, 0.4); } .warning:disabled{ color: rgba(255, 193, 7, 0.4); border-color: rgba(255, 193, 7, 0.4); } .success:disabled{ color: rgba(23, 162, 184, 0.4); border-color: rgba(23, 162, 184, 0.4); } .danger:flat:disabled{ background-color: rgba(220, 53, 69, 0.1); } .warning:flat:disabled{ background-color: rgba(255, 193, 7, 0.1); } .success:flat:disabled{ background-color: rgba(23, 162, 184, 0.1); } /* ------------------------------------------------------------------------ */ /* Basic widgets */ QWidget { background-color: #ffffff; } QGroupBox, QFrame { background-color: #ffffff; border: 2px solid #e6e6e6; border-radius: 4px; } QGroupBox.fill_background, QFrame.fill_background { background-color: #f5f5f5; border: 2px solid #f5f5f5; border-radius: 4px; } QSplitter { background-color: transparent; border: none } QStatusBar { color: #555555; background-color: rgba(230, 230, 230, 0.2); border-radius: 0px; } QScrollArea, QStackedWidget, QWidget > QToolBox, QToolBox > QWidget, QTabWidget > QWidget { border: none; } QTabWidget::pane { border: none; } /* ------------------------------------------------------------------------ */ /* Inputs */ QDateTimeEdit, QSpinBox, QDoubleSpinBox, QTextEdit, QLineEdit, QPushButton { color: #2979ff; background-color: #ffffff; border: 2px solid #2979ff; border-radius: 4px; height: 24px; } QDateTimeEdit, QSpinBox, QDoubleSpinBox, QTreeView, QListView, QLineEdit, QComboBox { padding-left: 8px; border-radius: 0px; background-color: #f5f5f5; border-width: 0 0 2px 0; border-radius: 0px; border-top-left-radius: 4px; border-top-right-radius: 4px; height: 24px; } QPlainTextEdit { border-radius: 4px; padding: 0px 8px; background-color: #ffffff; border: 2px solid #e6e6e6; } QTextEdit { padding: 0px 8px; border-radius: 4px; background-color: #f5f5f5; } QDateTimeEdit:disabled, QSpinBox:disabled, QDoubleSpinBox:disabled, QTextEdit:disabled, QLineEdit:disabled { color: rgba(41, 121, 255, 0.2); background-color: rgba(245, 245, 245, 0.75); border: 2px solid rgba(41, 121, 255, 0.2); border-width: 0 0 2px 0; padding: 0px 8px; border-radius: 0px; border-top-left-radius: 4px; border-top-right-radius: 4px; height: 24px; } /* ------------------------------------------------------------------------ */ /* QComboBox */ QComboBox { color: #2979ff; border: 1px solid #2979ff; border-width: 0 0 2px 0; background-color: #f5f5f5; border-radius: 0px; border-top-left-radius: 4px; border-top-right-radius: 4px; height: 28px; } QComboBox:disabled { color: rgba(41, 121, 255, 0.2); background-color: rgba(245, 245, 245, 0.75); border-bottom: 2px solid rgba(41, 121, 255, 0.2); } QComboBox::drop-down { border: none; color: #2979ff; width: 20px; } QComboBox::down-arrow { image: url(:/ostinato.org/themes/material-light/primary/downarrow.svg); margin-right: 4px; } QComboBox::down-arrow:disabled { image: url(:/ostinato.org/themes/material-light/disabled/downarrow.svg); margin-right: 4px; } QComboBox QAbstractItemView { background-color: #f5f5f5; border: 2px solid #e6e6e6; border-radius: 4px; } QComboBox[frame='false'] { color: #2979ff; background-color: transparent; border: 1px solid transparent; } QComboBox[frame='false']:disabled { color: rgba(41, 121, 255, 0.2); } /* ------------------------------------------------------------------------ */ /* Spin buttons */ QDateTimeEdit::up-button, QDoubleSpinBox::up-button, QSpinBox::up-button { subcontrol-origin: border; subcontrol-position: top right; width: 20px; image: url(:/ostinato.org/themes/material-light/primary/uparrow.svg); border-width: 0px; margin-right: 5px; } QDateTimeEdit::up-button:disabled, QDoubleSpinBox::up-button:disabled, QSpinBox::up-button:disabled { image: url(:/ostinato.org/themes/material-light/disabled/uparrow.svg); } QDateTimeEdit::down-button, QDoubleSpinBox::down-button, QSpinBox::down-button { subcontrol-origin: border; subcontrol-position: bottom right; width: 20px; image: url(:/ostinato.org/themes/material-light/primary/downarrow.svg); border-width: 0px; border-top-width: 0; margin-right: 5px; } QDateTimeEdit::down-button:disabled, QDoubleSpinBox::down-button:disabled, QSpinBox::down-button:disabled { image: url(:/ostinato.org/themes/material-light/disabled/downarrow.svg); } /* ------------------------------------------------------------------------ */ /* QPushButton */ QPushButton { text-transform: uppercase; margin: 0px; padding: 0px 8px; height: 24px; font-weight: bold; border-radius: 4px; } QPushButton:checked, QPushButton:pressed { color: #ffffff; background-color: #2979ff; } QPushButton:flat { margin: 0px; color: #2979ff; border: none; background-color: transparent; } QPushButton:flat:hover { background-color: rgba(41, 121, 255, 0.2); } QPushButton:flat:pressed, QPushButton:flat:checked { background-color: rgba(41, 121, 255, 0.1); } QPushButton:disabled { color: rgba(230, 230, 230, 0.75); background-color: transparent; border-color: #e6e6e6; } QPushButton:flat:disabled { color: rgba(230, 230, 230, 0.75); background-color: rgba(230, 230, 230, 0.25); border: none; } QPushButton:disabled { border: 2px solid rgba(230, 230, 230, 0.75); } QPushButton:checked:disabled { color: #f5f5f5; background-color: #e6e6e6; border-color: #e6e6e6; } /* ------------------------------------------------------------------------ */ /* QTabBar */ QTabBar{ text-transform: uppercase; font-weight: bold; } QTabBar::tab { color: #555555; border: 0px; } QTabBar::tab:bottom, QTabBar::tab:top{ padding: 0 8px; height: 20px; } QTabBar::tab:left, QTabBar::tab:right{ padding: 8px 0; width: 20px; } QTabBar::tab:top:selected, QTabBar::tab:top:hover { color: #2979ff; border-bottom: 2px solid #2979ff; } QTabBar::tab:bottom:selected, QTabBar::tab:bottom:hover { color: #2979ff; border-top: 2px solid #2979ff; } QTabBar::tab:right:selected, QTabBar::tab:right:hover { color: #2979ff; border-left: 2px solid #2979ff; } QTabBar::tab:left:selected, QTabBar::tab:left:hover { color: #2979ff; border-right: 2px solid #2979ff; } QTabBar QToolButton:hover, QTabBar QToolButton { border: 20px; background-color: #ffffff; } QTabBar QToolButton::up-arrow { image: url(:/ostinato.org/themes/material-light/disabled/uparrow2.svg); } QTabBar QToolButton::up-arrow:hover { image: url(:/ostinato.org/themes/material-light/primary/uparrow2.svg); } QTabBar QToolButton::down-arrow { image: url(:/ostinato.org/themes/material-light/disabled/downarrow2.svg); } QTabBar QToolButton::down-arrow:hover { image: url(:/ostinato.org/themes/material-light/primary/downarrow2.svg); } QTabBar QToolButton::right-arrow { image: url(:/ostinato.org/themes/material-light/primary/rightarrow2.svg); } QTabBar QToolButton::right-arrow:hover { image: url(:/ostinato.org/themes/material-light/disabled/rightarrow2.svg); } QTabBar QToolButton::left-arrow { image: url(:/ostinato.org/themes/material-light/primary/leftarrow2.svg); } QTabBar QToolButton::left-arrow:hover { image: url(:/ostinato.org/themes/material-light/disabled/leftarrow2.svg); } QTabBar::close-button { image: url(:/ostinato.org/themes/material-light/disabled/tab_close.svg); } QTabBar::close-button:hover { image: url(:/ostinato.org/themes/material-light/primary/tab_close.svg); } /* ------------------------------------------------------------------------ */ /* QGroupBox */ QGroupBox { padding: 8px; padding-top: 28px; line-height: ; text-transform: uppercase; font-size: ; } QGroupBox::title { color: rgba(85, 85, 85, 0.4); subcontrol-origin: margin; subcontrol-position: top left; padding: 8px; background-color: #ffffff; background-color: transparent; height: 28px; } /* ------------------------------------------------------------------------ */ /* QRadioButton and QCheckBox labels */ QRadioButton, QCheckBox { spacing: 4px; color: #555555; line-height: 14px; height: 28px; background-color: transparent; spacing: 5px; } QRadioButton:disabled, QCheckBox:disabled { color: rgba(85, 85, 85, 0.3); } /* ------------------------------------------------------------------------ */ /* General Indicators */ QGroupBox::indicator { width: 16px; height: 16px; border-radius: 3px; } QMenu::indicator, QListView::indicator, QTableWidget::indicator, QRadioButton::indicator, QCheckBox::indicator { width: 20px; height: 20px; border-radius: 4px; } /* ------------------------------------------------------------------------ */ /* QListView Indicator */ QListView::indicator:checked, QListView::indicator:checked:selected, QListView::indicator:checked:focus { image: url(:/ostinato.org/themes/material-light/primary/checklist.svg); } QListView::indicator:checked:selected:active { image: url(:/ostinato.org/themes/material-light/primary/checklist_invert.svg); } QListView::indicator:checked:disabled { image: url(:/ostinato.org/themes/material-light/disabled/checklist.svg); } QListView::indicator:indeterminate, QListView::indicator:indeterminate:selected, QListView::indicator:indeterminate:focus { image: url(:/ostinato.org/themes/material-light/primary/checklist_indeterminate.svg); } QListView::indicator:indeterminate:selected:active { image: url(:/ostinato.org/themes/material-light/primary/checklist_indeterminate_invert.svg); } QListView::indicator:indeterminate:disabled { image: url(:/ostinato.org/themes/material-light/disabled/checklist_indeterminate.svg); } /* ------------------------------------------------------------------------ */ /* QTableView Indicator */ QTableView::indicator:enabled:checked, QTableView::indicator:enabled:checked:selected, QTableView::indicator:enabled:checked:focus { image: url(:/ostinato.org/themes/material-light/primary/checkbox_checked.svg); } QTableView::indicator:checked:selected:active { image: url(:/ostinato.org/themes/material-light/primary/checkbox_checked_invert.svg); } QTableView::indicator:disabled:checked, QTableView::indicator:disabled:checked:selected, QTableView::indicator:disabled:checked:focus { image: url(:/ostinato.org/themes/material-light/disabled/checkbox_checked.svg); } QTableView::indicator:enabled:unchecked, QTableView::indicator:enabled:unchecked:selected, QTableView::indicator:enabled:unchecked:focus { image: url(:/ostinato.org/themes/material-light/primary/checkbox_unchecked.svg); } QTableView::indicator:unchecked:selected:active { image: url(:/ostinato.org/themes/material-light/primary/checkbox_unchecked_invert.svg); } QTableView::indicator:disabled:unchecked, QTableView::indicator:disabled:unchecked:selected, QTableView::indicator:disabled:unchecked:focus { image: url(:/ostinato.org/themes/material-light/disabled/checkbox_unchecked.svg); } QTableView::indicator:enabled:indeterminate, QTableView::indicator:enabled:indeterminate:selected, QTableView::indicator:enabled:indeterminate:focus { image: url(:/ostinato.org/themes/material-light/primary/checkbox_indeterminate.svg); } QTableView::indicator:indeterminate:selected:active { image: url(:/ostinato.org/themes/material-light/primary/checkbox_indeterminate_invert.svg); } QTableView::indicator:disabled:indeterminate, QTableView::indicator:disabled:indeterminate:selected, QTableView::indicator:disabled:indeterminate:focus { image: url(:/ostinato.org/themes/material-light/disabled/checkbox_indeterminate.svg); } /* ------------------------------------------------------------------------ */ /* QCheckBox and QGroupBox Indicator */ QCheckBox::indicator:checked, QGroupBox::indicator:checked { image: url(:/ostinato.org/themes/material-light/primary/checkbox_checked.svg); } QCheckBox::indicator:unchecked, QGroupBox::indicator:unchecked { image: url(:/ostinato.org/themes/material-light/primary/checkbox_unchecked.svg); } QCheckBox::indicator:indeterminate, QGroupBox::indicator:indeterminate { image: url(:/ostinato.org/themes/material-light/primary/checkbox_indeterminate.svg); } QCheckBox::indicator:checked:disabled, QGroupBox::indicator:checked:disabled { image: url(:/ostinato.org/themes/material-light/disabled/checkbox_checked.svg); } QCheckBox::indicator:unchecked:disabled, QGroupBox::indicator:unchecked:disabled { image: url(:/ostinato.org/themes/material-light/disabled/checkbox_unchecked.svg); } QCheckBox::indicator:indeterminate:disabled, QGroupBox::indicator:indeterminate:disabled { image: url(:/ostinato.org/themes/material-light/disabled/checkbox_indeterminate.svg); } /* ------------------------------------------------------------------------ */ /* QRadioButton Indicator */ QRadioButton::indicator:checked { image: url(:/ostinato.org/themes/material-light/primary/radiobutton_checked.svg); } QRadioButton::indicator:unchecked { image: url(:/ostinato.org/themes/material-light/primary/radiobutton_unchecked.svg); } QRadioButton::indicator:checked:disabled { image: url(:/ostinato.org/themes/material-light/disabled/radiobutton_checked.svg); } QRadioButton::indicator:unchecked:disabled { image: url(:/ostinato.org/themes/material-light/disabled/radiobutton_unchecked.svg); } /* ------------------------------------------------------------------------ */ /* QDockWidget */ QDockWidget { color: #555555; text-transform: uppercase; border: 2px solid #f5f5f5; titlebar-close-icon: url(:/ostinato.org/themes/material-light/primary/close.svg); titlebar-normal-icon: url(:/ostinato.org/themes/material-light/primary/float.svg); border-radius: 4px; } QDockWidget::title { text-align: left; padding-left: 28px; padding: 3px; margin-top: 4px; } /* ------------------------------------------------------------------------ */ /* QComboBox indicator */ QComboBox::indicator:checked { image: url(:/ostinato.org/themes/material-light/primary/checklist.svg); } QComboBox::indicator:checked:selected { image: url(:/ostinato.org/themes/material-light/primary/checklist_invert.svg); } /* ------------------------------------------------------------------------ */ /* Menu Items */ QComboBox::item, QCalendarWidget QMenu::item, QMenu::item { height: 20px; border: 8px solid transparent; color: #555555; } QCalendarWidget QMenu::item, QMenu::item { padding: 0px 16px 0px 8px; /* pyqt5 */ } QComboBox::item:selected, QCalendarWidget QMenu::item:selected, QMenu::item:selected { color: #3c3c3c; background-color: #75a7ff; border-radius: 0px; } QComboBox::item:disabled, QCalendarWidget QMenu::item:disabled, QMenu::item:disabled { color: rgba(85, 85, 85, 0.3); } /* ------------------------------------------------------------------------ */ /* QMenu */ QCalendarWidget QMenu, QMenu { background-color: #f5f5f5; border: 2px solid #e6e6e6; border-radius: 4px; } QMenu::separator { height: 2px; background-color: #e6e6e6; margin-left: 2px; margin-right: 2px; } QMenu::right-arrow{ image: url(:/ostinato.org/themes/material-light/primary/rightarrow.svg); width: 8px; height: 8px; } QMenu::right-arrow:selected{ image: url(:/ostinato.org/themes/material-light/disabled/rightarrow.svg); } QMenu::indicator:non-exclusive:unchecked { image: url(:/ostinato.org/themes/material-light/primary/checkbox_unchecked.svg); } QMenu::indicator:non-exclusive:unchecked:selected { image: url(:/ostinato.org/themes/material-light/primary/checkbox_unchecked_invert.svg); } QMenu::indicator:non-exclusive:checked { image: url(:/ostinato.org/themes/material-light/primary/checkbox_checked.svg); } QMenu::indicator:non-exclusive:checked:selected { image: url(:/ostinato.org/themes/material-light/primary/checkbox_checked_invert.svg); } QMenu::indicator:exclusive:unchecked { image: url(:/ostinato.org/themes/material-light/primary/radiobutton_unchecked.svg); } QMenu::indicator:exclusive:unchecked:selected { image: url(:/ostinato.org/themes/material-light/primary/radiobutton_unchecked_invert.svg); } QMenu::indicator:exclusive:checked { image: url(:/ostinato.org/themes/material-light/primary/radiobutton_checked.svg); } QMenu::indicator:exclusive:checked:selected { image: url(:/ostinato.org/themes/material-light/primary/radiobutton_checked_invert.svg); } /* ------------------------------------------------------------------------ */ /* QMenuBar */ QMenuBar { background-color: #f5f5f5; color: #555555; } QMenuBar::item { height: 24px; padding: 8px; background-color: transparent; color: #555555; } QMenuBar::item:selected, QMenuBar::item:pressed { color: #3c3c3c; background-color: #75a7ff; } /* ------------------------------------------------------------------------ */ /* QToolBox */ QToolBox::tab { background-color: #f5f5f5; color: #555555; text-transform: uppercase; border-radius: 4px; padding-left: 15px; } QToolBox::tab:selected, QToolBox::tab:hover { background-color: rgba(41, 121, 255, 0.2); } /* ------------------------------------------------------------------------ */ /* QProgressBar */ QProgressBar { border-radius: 0; background-color: #e6e6e6; text-align: center; color: transparent; } QProgressBar::chunk { background-color: #2979ff; } /* ------------------------------------------------------------------------ */ /* QScrollBar */ QScrollBar:horizontal { border: 0; background: #f5f5f5; height: 0px; } QScrollBar:vertical { border: 0; background: #f5f5f5; width: 0px; } QScrollBar::handle { background: rgba(41, 121, 255, 0.1); } QScrollBar::handle:horizontal { min-width: 16px; } QScrollBar::handle:vertical { min-height: 16px; } QScrollBar::handle:vertical:hover, QScrollBar::handle:horizontal:hover { background: #2979ff; } QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical, QScrollBar::add-line:horizontal, QScrollBar::sub-line:horizontal { border: 0; background: transparent; width: 0px; height: 0px; } /* ------------------------------------------------------------------------ */ /* QScrollBar-Big */ QScrollBar.big:horizontal { border: 0; background: #f5f5f5; height: 28px; } QScrollBar.big:vertical { border: 0; background: #f5f5f5; width: 28px; } QScrollBar.big::handle, QScrollBar.big::handle:vertical:hover, QScrollBar.big::handle:horizontal:hover { background: #2979ff; } QScrollBar.big::handle:horizontal { min-width: 16px; } QScrollBar.big::handle:vertical { min-height: 16px; } QScrollBar.big::add-line:vertical, QScrollBar.big::sub-line:vertical, QScrollBar.big::add-line:horizontal, QScrollBar.big::sub-line:horizontal { border: 0; background: transparent; width: 0px; height: 0px; } /* ------------------------------------------------------------------------ */ /* QSlider */ QSlider:horizontal { min-height: 16px; max-height: 16px; } QSlider:vertical { min-width: 16px; max-width: 16px; } QSlider::groove:horizontal { height: 4px; background: #393939; margin: 0 4px; } QSlider::groove:vertical { width: 4px; background: #393939; margin: 4px 0; border-radius: 16px; } QSlider::handle:horizontal { image: url(:/ostinato.org/themes/material-light/primary/slider.svg); width: 16px; height: 16px; margin: -16px -4px; } QSlider::handle:vertical { image: url(:/ostinato.org/themes/material-light/primary/slider.svg); border-radius: 16px; width: 16px; height: 16px; margin: -4px -16px; } QSlider::add-page { background: #f5f5f5; } QSlider::sub-page { background: #2979ff; } /* ------------------------------------------------------------------------ */ /* QLabel */ QLabel { border: none; background: transparent; color: #555555 } QLabel:disabled { color: rgba(85, 85, 85, 0.2) } /* ------------------------------------------------------------------------ */ /* VLines and HLinex */ QFrame[frameShape="4"] { border-width: 1px 0 0 0; background: none; } QFrame[frameShape="5"] { border-width: 0 1px 0 0; background: none; } QFrame[frameShape="4"], QFrame[frameShape="5"] { border-color: #e6e6e6; } /* ------------------------------------------------------------------------ */ /* QToolBar */ QToolBar { background: #ffffff; border: 0px solid; } QToolBar:horizontal { border-bottom: 1px solid #e6e6e6; } QToolBar:vertical { border-right: 1px solid #e6e6e6; } QToolBar::handle:horizontal { image: url(:/ostinato.org/themes/material-light/primary/toolbar-handle-horizontal.svg); } QToolBar::handle:vertical { image: url(:/ostinato.org/themes/material-light/primary/toolbar-handle-vertical.svg); } QToolBar::separator:horizontal { border-right: 1px solid #e6e6e6; border-left: 1px solid #e6e6e6; width: 1px; } QToolBar::separator:vertical { border-top: 1px solid #e6e6e6; border-bottom: 1px solid #e6e6e6; height: 1px; } /* ------------------------------------------------------------------------ */ /* QToolButton */ QToolButton { background: #ffffff; border: 0px; height: 28px; margin: 3px; padding: 3px; border-right: 12px solid #ffffff; border-left: 12px solid #ffffff; } QToolButton:hover { background: #e6e6e6; border-right: 12px solid #e6e6e6; border-left: 12px solid #e6e6e6; } QToolButton:pressed { background: #f5f5f5; border-right: 12px solid #f5f5f5; border-left: 12px solid #f5f5f5; } QToolButton:checked { background: #e6e6e6; border-left: 12px solid #e6e6e6; border-right: 12px solid #2979ff; } /* ------------------------------------------------------------------------ */ /* General viewers */ QTableView { background-color: #ffffff; border: 1px solid #f5f5f5; border-radius: 4px; } QTreeView, QListView { border-radius: 4px; padding: 4px; margin: 0px; border: 0px; } QTableView::item, QTreeView::item, QListView::item { padding: 4px; min-height: 24px; color: #555555; selection-color: #555555; /* For Windows */ border-color: transparent; /* Fix #34 */ } /* ------------------------------------------------------------------------ */ /* Items Selection */ QTableView::item:selected, QTreeView::item:selected, QListView::item:selected { background-color: rgba(41, 121, 255, 0.2); selection-background-color: rgba(41, 121, 255, 0.2); color: #555555; selection-color: #555555; /* For Windows */ } QTableView::item:selected:focus, QTreeView::item:selected:focus, QListView::item:selected:focus { background-color: #2979ff; selection-background-color: #2979ff; color: #3c3c3c; selection-color: #3c3c3c; /* For Windows */ } QTableView { selection-background-color: rgba(41, 121, 255, 0.2); } QTableView:focus { selection-background-color: #2979ff; } QTableView::item:disabled { color: rgba(85, 85, 85, 0.3); selection-color: rgba(85, 85, 85, 0.3); background-color: #f5f5f5; selection-background-color: #f5f5f5; } /* ------------------------------------------------------------------------ */ /* QTreeView */ QTreeView::branch{ background-color: #f5f5f5; } QTreeView::branch:closed:has-children:has-siblings, QTreeView::branch:closed:has-children:!has-siblings { image: url(:/ostinato.org/themes/material-light/primary/branch-closed.svg); } QTreeView::branch:open:has-children:!has-siblings, QTreeView::branch:open:has-children:has-siblings { image: url(:/ostinato.org/themes/material-light/primary/branch-open.svg); } QTreeView::branch:has-siblings:!adjoins-item { border-image: url(:/ostinato.org/themes/material-light/disabled/vline.svg) 0; } QTreeView::branch:has-siblings:adjoins-item { border-image: url(:/ostinato.org/themes/material-light/disabled/branch-more.svg) 0; } QTreeView::branch:!has-children:!has-siblings:adjoins-item, QTreeView::branch:has-children:!has-siblings:adjoins-item { border-image: url(:/ostinato.org/themes/material-light/disabled/branch-end.svg) 0; } QTreeView QHeaderView::section { border: none; } /* ------------------------------------------------------------------------ */ /* Custom buttons */ QPushButton.danger { border-color: #dc3545; color: #dc3545; } QPushButton.danger:checked, QPushButton.danger:pressed { color: #ffffff; background-color: #dc3545; } QPushButton.warning{ border-color: #ffc107; color: #ffc107; } QPushButton.warning:checked, QPushButton.warning:pressed { color: #ffffff; background-color: #ffc107; } QPushButton.success { border-color: #17a2b8; color: #17a2b8; } QPushButton.success:checked, QPushButton.success:pressed { color: #ffffff; background-color: #17a2b8; } QPushButton.danger:flat:hover { background-color: rgba(220, 53, 69, 0.2); } QPushButton.danger:flat:pressed, QPushButton.danger:flat:checked { background-color: rgba(220, 53, 69, 0.1); color: #dc3545; } QPushButton.warning:flat:hover { background-color: rgba(255, 193, 7, 0.2); } QPushButton.warning:flat:pressed, QPushButton.warning:flat:checked { background-color: rgba(255, 193, 7, 0.1); color: #ffc107; } QPushButton.success:flat:hover { background-color: rgba(23, 162, 184, 0.2); } QPushButton.success:flat:pressed, QPushButton.success:flat:checked { background-color: rgba(23, 162, 184, 0.1); color: #17a2b8; } /* ------------------------------------------------------------------------ */ /* QTableView */ QTableCornerButton::section { background-color: #f5f5f5; border-radius: 0px; border-right: 1px solid; border-bottom: 1px solid; border-color: #ffffff; } QTableView { alternate-background-color: rgba(245, 245, 245, 0.7); } QHeaderView { border: none; } QHeaderView::section { color: rgba(85, 85, 85, 0.7); text-transform: uppercase; background-color: #f5f5f5; padding: 0 16px; height: 28px; border-radius: 0px; border-right: 1px solid; border-bottom: 1px solid; border-color: #ffffff; } QHeaderView::section:vertical { } QHeaderView::section:horizontal { } /* ------------------------------------------------------------------------ */ /* QLCDNumber */ QLCDNumber { color: #2979ff; background-color:rgba(41, 121, 255, 0.1); border: 1px solid rgba(41, 121, 255, 0.3); border-radius: 4px; } /* ------------------------------------------------------------------------ */ /* QCalendarWidget */ #qt_calendar_prevmonth { qproperty-icon: url(:/ostinato.org/themes/material-light/primary/leftarrow.svg); } #qt_calendar_nextmonth { qproperty-icon: url(:/ostinato.org/themes/material-light/primary/rightarrow.svg); } /* ------------------------------------------------------------------------ */ /* Inline QLineEdit */ QTreeView QLineEdit, QTableView QLineEdit, QListView QLineEdit { color: #555555; background-color: #f5f5f5; border: 1px solid unset; border-radius: unset; padding: unset; padding-left: unset; height: unset; border-width: unset; border-top-left-radius: unset; border-top-right-radius: unset; } /* ------------------------------------------------------------------------ */ /* QToolTip */ QToolTip { padding: 4px; border: 1px solid #ffffff; border-radius: 4px; color: #555555; background-color: #e6e6e6; } /* ------------------------------------------------------------------------ */ /* QDialog */ QDialog QToolButton:disabled { background-color: #f5f5f5; color: #555555 } /* ------------------------------------------------------------------------ */ /* Grips */ QMainWindow::separator:vertical, QSplitter::handle:horizontal { image: url(:/ostinato.org/themes/material-light/primary/splitter-horizontal.svg); } QMainWindow::separator:horizontal, QSplitter::handle:vertical { image: url(:/ostinato.org/themes/material-light/primary/splitter-vertical.svg); } QSizeGrip { image: url(:/ostinato.org/themes/material-light/primary/sizegrip.svg); background-color: transparent; } QMenuBar QToolButton:hover, QMenuBar QToolButton:pressed, QMenuBar QToolButton { border-width: 0; border-left: 10px; border-image: url(:/ostinato.org/themes/material-light/primary/rightarrow2.svg); background-color: transparent; } /* Generated by qt-material 2.8.18 */ ostinato-1.3.0/client/themes/material-light.rcc000066400000000000000000005074671451413623100215350ustar00rootroot00000000000000qresˆW‰ë!zxœíYYãÆ~7àÿ@Ð/;°Øì“lÊ£1, H^üPdK¢—b dkFÚ_ŸjR¤xI;{,²ÌŒXU}T}uuëñûÓ>wžUYeºX¹a×QE¢Ó¬Ø®Üþüƒ']§2q‘ƹ.ÔÊ-´ûýÓ×_=VÏÛ¯¿r†Õ2MVîΘÃÒ÷Ç2GºÜúiâ«\íUa*Ÿ â»=ùä*Ÿ”*6Ù³Jô~¯‹ªZTßô¥ËtÓ‰¿¼¼ VK‘(Š|L}J=ðªsaâ“7 ûœK1Æ>ðz¢¯[V`œüvò-UúX&j*”ñßþü¶cz¥&íÏ“ïª$>¨Áº-±1C¼WÕ!NTå·ôf‚—,5»•KqóºSÙvg®ïÏ™zù‹>­\ì`G ‘ øõÓEê :i(YºrAYyy»,¹ì{"ζN¨’„mÄ¡˜bGNr¬ŒÞ?4£[½—©N¬+w]ÆE²óT‘¢Î¢ÝêtÐ¥ñ6Y®i§÷Ê?«¬Ò…ÿV=«\¬/ù‡Ì%.Ÿ%ºøWž…ÅùNépŠ‚yî¹å>Yöcª6U-×X¾R×ñf§Ý^j-Ü]ÇÕÇ9Ä[ðå\—+÷›Mý´œµ.SU¶¼ ~†< `gæÜÄa;»i;q'€oT»8Õ/à î{­÷+—…\@„ᄟ€¿Š8åTˆ)×n Ð!çlÂŒïXdépšNp,K+‘Çgêo%‹Z™j§_¶¥µ£)j2ò%+@%ïâó$¢SÍ/"mŒù-·xç;¼}|ÊöÙ{»$«Aßú›8¿:Äm›Ô®²SÉ;U®u\¦£µ]ŽYªª–©Šøà­×6Ògù–åb³»5A-PèW¬à©t«¼}–tV˜‹¿JòÞÊzý›JÌÝÝ×sÀž@2Ô‡¥«½Öf÷a•_µým®×q>”Ød\¥Üf…gô¡çO=F®6fžS6þ;ÇZkclO´v‘{nÐEg©MlêLŽ›”yí: Dàe¤ã˜³­K§³%ºÕ†¨¥D!¿Õþ`kTÝ/È+ùB³á†hÀ…dŒN¸çyn ºªq4زVeë\ µ„ 1PÓ1ÙBta÷œg…‚R’ŸÇrŒžý´ÐÒêtÐæZÆ^™8MÜ+-ItV†þdùÓÛžÚ“dù‹.ßu+:މ×úø»OWúcš,¡£ØÇæ)ÛC¾°ÝÈ·Ð@<úWÆPÚb×›·™¹TMs2Û¦¥É>³£ü˜,Ïÿj—iõîM›™\=ÕË6;]ü‹2­²~_ÛG¿µFóº{g¯Ñßl]p¦©u[êãañºrëÒáöì\º!ú‹ÊZÄ" óب7xáAÓƒB,™xèàØ]ZòðŠ}o°m™Þ@­‡œE l.¯‚- ©‹0¡‚|A%OB1{¸Â×[¨]JºÛRµÁ"räé–Š"PÒì¨y#R,Hˆå\ò…ÇC‚2Âú Â’6I¦ïY»[ÉK”M¬¶$2DBáŽFTæl#ñÒ,ÉwйåËŽõ'¯e`Ä#&iH¢ï*SêwjyiŠ0¾šÂ‚8„f›3ÖÒmĦ–àEÚ'þ™yH÷UeõÙ,yKKc¨ÆeŸ›]õ¨z³©”Yv¸*qˆ!ç{u+¶l˜ŽÕÆ 5ºÐú»Cb‘} % @êxáú‘Ê ”ˆÖôQXK;¿Žg³à[l¤dV×EéŒbtéA?õ›c©‰ëN—ª nl`CKàõ¼+Üûû¶tUn² sÖŠ=š@Œ‡gŒvÏs=ëº&øâ¾µûŽá/·‘aJ0›€Çx äØjÀ£‚!É¥œX hˆ.C&£1¯íË™€Œ$Ãqzëγ¾ æ¼ÇnV†S_ÿHB*å¼üL²2ÉÕK „P éJ{žk#L˜§z$¿Ä!ýS*`BJú¹ÃЫÞu®Þx\<ÌÙ܃l…°Œ‚I€ÔpÁùNà*ëƒ{ ‚Þ²IP}È as|F}T7têÕ” FŸ¸rFxmåäýÖþ׽Góó¦z>;ôÒyÿ¥þnð¥)¾ÓßlçàXrqÆy_¼úú´øD-ðˆðˆN ]Ý ›ÖM[6‘8zM|£ë|Žyx«â Ølñ­{ þ)=€¯üv»Wùi—›n3q×ÇA§ußeŬ»s‰Â àw·§/ŽQÀ›žz6ñåi·á‡Ñm”¿í]>m»k§îÓ ™Ï;œ@/5ôÒÂßG£Eôˆ‚@dz›¾wÃi“$ä@ŠNÉ‚cH’a ™“8ÚÝ7Ũ¹tÝ4ADCi«»ÛÑ@[ö"ÞçâQ4Þ²ødgŒ"pt~„¡oX†‡³–i5ç Ó|NŠ{mZ›’Ë(Óð¾e¦™©ùÿhoðáÿ¿¢:’q Ÿ image/svg+xml Ç ¥xœíXÝã¶ÿu^nK")Š¢ïhA4/IŠ} d‰¶•“EA¢×öýõê“²ì½ Ò zèi±»ÒÌpHÎüæƒ\{>äèYVu¦ŠÇqñÉ"QiVìÿüõ{G,P­ã"sUÈÇE¡ß>}ùÅúoŽƒ¾«d¬eŠN™Þ£‹wu—½Ùk]®<ït:¹YGtUµóãÀP\?ï¾ü!sõ*MݘòXålšx2—YèÚ#.ñ–|2Ê'fÙ³LÔá ŠºZÔ_ÙÒUºÄÍ’N~#E¢(ò0õ(u@©/…ŽÏÎÕXXç­±cìÏ}¥ØªË–ð;È÷·VÇ*‘[(ÝBjïí¯o¦ƒÝT§¶žÞ°“y'Ö.⃬Ë8‘µ×Ó[§,ÕûÇÅíç^f»½¿Ÿ3yú»:?.0Â(piD8çl|ë¤FÄ–’¥ ج辺)Wƒ v#ꂞ4æ"J|â/Å$r0qH§´ßî*U‰Y>(̳TVî`ÄA­<—ªÒÎ6Ëe+éíÕAz™ÕªðÞÊg™«ÒÀÇ+3 ”¸Ò^–¨â·<ÓÒ-‹;úÎi ®‰ømî¥ç>ö:•Ûº‘k7o>éy-sØ‹Y^jŒj‰nâºsBe¼øæªz\|µmžž³Ql¾çñæ™òø7Ó—6n{ýý¢âAߨ÷qªNàý÷½R  7Ä,"þŒŸD„ðà0˜s/†+0%‘͸àߣqŽs,2 ±Sžç ŽUe$òø"aûÍ?ÒKÕ{uÚUÆ’º:ÊÙØSVÀ¦œè$¢ó½w"=ø &ó=t2&îñ./ðñ9;dï%¬’ÌdÌlûoã|„Ä}«4`ÙËä¬6*®Ò«]Ž2õËÔE\:› ï›|ÃrÊXïï)h õŠ™î¤sÈÒRe…þ°ø«$_šYm~—‰~qõ˜rˆBZú°t}PJï?¼åW-—«MœO%¶™¨T»¬p´*-eL°¥]–PŸ°{B˜Ò¤½‰zËÚÃLN"Mª4EÎwÉXäû§Ö€]±"ß@7–¯ 8&4oNÏÀ.‹|AC}SëJ½“«®ÑÁ¸#´¥q=3óýžnµ©Mürí” ¨•UW¯XOKc¨¯U_ÚUYTµÝÖR¯†Œ›(cÈâNÓ^­Z&2»AU¡êÖ×Foý„hàú‘y–°K—Ã(r„‹›GH'\RÆ]ÚÐ@vÃFýûZ›q¾ñþŒ5tFª£hU9Ð#=ÇúXÉI¾êœ3d(ˆÏP˜x¦±| wÇþ¹%››±Lž¼iE‹Àéá8“Ìör] >÷ïµû+ðÛ•a*0[ Îg<×V |W0!f…ººœ‰ÐÑ5¯ï´ý2’¯ÓÛp,íø‚ßBY¬çXÿž„TÊØ ø)x2ɪ$—×¾4î‚â‚Î\iÎh}¤‘óÜŒdwƒ8¤I@q?‚~ên°ªw¥´iJ½È ›;­\,"> Æ]lî™»ÀÏ„º<à7x£ËfAõQ\ ›aþ ¹ìuCg«¦Ì|ô‘+g„gñØWNf·æ»é=Ú¿˜µÕó9Ð{ôþs)üoÀàsSüBS|·ƒcIÆÛX±>o>R üBDXDg…®é8ìΟ×MS6Ý€s8zͰ1ô@à|†Yx¯bË#Ÿû7‹oÓùá_Òpàÿð{…#ÿ/ír63¸NÖË n 7äœÍànN_ »Ü÷ýùYÁ °–çÝR„®.¡¼uù´®†·I2¿ pp½Ô¤w¿íÞó°ˆó€üqê­‹M“$!RÌ%K†!I†\ø(AÚóÑøºøh¶$x –N±$ðiÐëRäF‚ÃIÌL×}PI[ ³ô #ÌD£@¯ÞÒ3¬âj_ֽɭûÍþ潻ߴ>­ËÓ Üš3寇Û°;•9oØM ¯Íý6üÿñbéÛñxœíX[ã¶~ÿ@(/;ˆ%‘EQÎx‹ ’—4m¾²DÛÊJ¢ Ñc{}u³.öìÍ.²èz03æ9‡—sÿÈÇïÏy†žeU§ªØXÄÁ’E¬’´Øo¬þúƒ-,Të¨H¢LrcÊúþéë¯ëçý×_!„`zQ¯“xc´.×®[«ÌQÕÞMbWf2—…®]â×ÉÇWù¸’‘NŸe¬ò\u3µ¨¿KWÉn?NÎÉk¤H†.¦.¥6HØõ¥ÐÑٞͅsÞšK1Æ.ðF¢¯[×`œ~ùžàÔêXÅr¥SHí¾ýõíÀ´±“èd¼NZ¼«ã¨”“}{bk†(—uŲv{z»À)MôacQÜ2ÝôuüœÊÓßÔyca„‘ïÐpÎÙõ['uu:i)i²±@YѺ-×ãèp(z#E€E®ŔؘØÄ_¡øXk•?´³{½×‰Š+>Èø]–Öú·´H¤–Už‘–Î`ÞaCy.U¥í]šÉvª{P¹t/2­UᾕÏ2S¥ ,·L5P¢J»i¬Šß²Ö+‹;드òÛÜKÏ}2ìÇDîêF®5‹R ¹-sPÎ/1æ‰n£ºsBe´‡ÀÎTµ±¾Ù5Ÿž³UU"«žÇ›Ï”§Àó©¾´IÙ¯ßÚ,<à;õ!JÔ âbÁ}¯T¾±uü€cFü‚'p0eÀ–\s(ê0$\pÁáGãûX¤²ª,]çJéÇU~Õñ÷™ÚFÙTb—j•jŸ¶Vå(žFŒLîômNÕÆï-ÖVimx Mˆ¼CvVJGº)ë¸-yP×®Ë@v3ÒÓ¤ÎC´ªIQC v%ʼ4 «âJîh&ÝÊ™/<.¸—ÛÜt•ól0=®N·™œj (" &s²qQ7Ü9K ­$»Ìå=-Æe¡§5å ¯ÿî²´Œ\ê(‰t4j=ɬ `eýËÛžúãxýoU½vDȈD[uÿ[OWúc¯^ä‘~Js¨š| hâѽ2¦ÒÆw£uÛ•+Ù"•›˜-‰óÔÌrÿ¡Ó,û»Ù¦×{´lª3ùÔlÛ~tq;ezeݱ¶nov¸ŸGgm%$ÑO¦/ eiÝWêX毫iÖÈÎ a˜¢«¨¨EŒ‡ákHã ^Ù€€€.žÿ0¸c? iÁ‚«ïGË€m«ôüz­OqÀ¼p…ÍO7ô½ ¼êÎVT@{${W÷6ê·òÉÄ»û‰§‚ýК—G"N NI{¢vD„¿"ãQÆ[ÙñŸz„=Œ7„-M‘œ,?²ö°“KSXMKôøÖlF­/&;<²&ßrËÖÜšovÏÀ =a@Ãwµ®Ô;¹î@Æ¡mì ˆ@ÞÌózºÉX8Ô HÆÄß¡2O©¾²Ê ?ë5ëiIݸª¢K{ªUívµÔëáW%Êj¾Ý@±uËDF5zt=7xëgD}Ç ÍgZ:t Èn>BÚÁŠ2îІþ#ò±4Òè?óÕŒóo„ð¬E©Œ¢UežzŽô±’“ÂÕ9g(U7&±¡Åð™&õíP¸;÷;ÒU¹ËÌ›VÑ|‡ñð £½¹¶ ] | ß&|çî¯Ào3ÃT`6 œÇ¸/æVõ=G0!mnœ‰Àáœ×ãrχŠ$‚yy.·_ð[Ñc WÐOáI(¥Œ ‚Ÿƒ'ã´Š39÷¥q¤táJsŸë3,˜çf&»›Äý$ Å=_ú¹»aÔ½.ߨ̸esª•ƒEÈ Ò¸‹€M«9÷ù ÞÕe‹¤ú(.ƒ‚Í0ÿŒ\ö‡ÐÐyÔS>úÈ3Ä‹|ì;'C3n°Gû³¶{>#°‡@ï¿´Â?# ¾€â@ñ]8×’.oÇâ5Ö—à#Aà0a!]4ºqÐÎ[öMÓ6Ÿs¸z-bcÀ@à|†Yp1ßá¡Ç½›Í·Á@^ðI0^ñ»W8òÿÒ.wÃf®“ˆ¤õrÈú7à 'àœ-ÂÝܾv¸çyË»Â(Á±¼DK!~˜½F¹ûÑãÓ~xv¾MŠùí Ç€¥¦q@z÷½Ñ{ô9÷É÷ç°üè…ÓI¨sFÉŠa(’Š˜GI@q¸ÂN@h"ßw¼¡x>g¯ð¸xö>ÇæüPÁL:zƒêlÕZˆÅÑÎÇÓ]öb|+¼õTÖ¿ævOeÃÐæ€l}Ö¼”…Ø!F/¿”ñIQ¿õ†=îc´o`+[8”L=±² „†<~-»|ãÆÀà”2¾òÀ À@DXý Ax$„~Džé^†‡áŠ<ï`ÚögýkbØo~æñ·4:o{÷ºØ´-s ýÿhÞŠáÿa¼>"ZxœíZ[ã¶~ÿ (/;ˆE‘EQÎx‹ Ú—4E€¼²DÛÊJ¢!Ñ3öþúêfÝìm²i] vÆ:çðrÎwn¤÷ñûsžYϲ¬RUll‚°mÉ"VIZì7ö?þÁ¶Ué¨H¢LrcÊþþé믫çý×_Y–ËjÄû õqíºÇS™!UîÝ$ve&sYèÊ%ˆ¸ö@>¾ÊÇ¥Œtú,c•窨ê¡EõÍPºLv½øËË zñj)†¡‹©K©Nu)ttv&caŸKc)ÆØÞ@ô•bë Œs„½|G@•:•±ÜÁ@‰ ©Ý·?¿í™F‰N†ó¤Å»*ŽŽr´nGlÌå²:F±¬ÜŽÞLð’&ú°±)n^2Ýôõý9•/Qç-lùˆ†„sήŸZ©+褡¤ÉÆeEûÖ.¹z"ÖoS)ˆ·óWÅ;8tp°²âS¥UþÐŒîô^'*6zlìmñÁÉU)QoÒ~y>ªR;»4“¸{P¹t/2­TᾕÏ2SGãLî1Õ@‰Jí¦±*þ•¥Z¢cqc¾sr B¾Ì½tÜ'Ã~L䮪åS˜Wj[nÃì2ÛKŒ‰¢Û¨j¡±¬c´gÎT¹±¿ÙÕOÇÙª2‘eÇãõ3æ)@;Õ—&»ù»M›‰{|C :D‰z_˜qß+•ÃÄQ2cÇà/>ò˜`˜…s®ÙS€,]åJéÇu~Õþ÷™ÚFÙXb—jp–rŸŽVÇG ™ÜéeNÙxðk«´6ЫƒQ´*èŸ#}*å(qµàô© âÆ6ÔžqP/»Âͱ¿oKWåf,“0­8 ùˆñð‚Ñîy®c\—Á_Ü·vß)ü%à61Ly1ì{Œûbj5àQßC‚ 1³(Ðq&O„S^w¦ð|ÈH"˜¦·þ(Þò_ò³YÌ}ý ©”±^ðs@2NË8“S, \BΛS³™£hidÆ<×#ÙÍ èŸPÜó… Ÿ; ƒê]ä‡ùK6w [!,B> .6!8˜Á8ЏÏxWÈfAõI ƒ„Í0ÿŒ û¨nè<¨)3Œ>qå ñ,»ÊɆ­‡y¯{æ7fMõ|¶è=„õþK)ü#ÜàKS|§)¾ÙÎÁ±¤uÆe_¼úú¼øD-ðˆ°Î ]ÝqÐΛ×MS6‘Ï9½f¾Ñ÷@>Ã,¸Õ1ñÐãÞbñ­{ /øSz ¼âØí^äÿ¥]nºÍÌ]GÖ}—õÝ pÎfînN_ #îyÞü¬0°™/Ï»¥?Ln£Üýàòiß_;õŸFÉ|Ù àz©±þ!ò GȹO>Ï~úÁ §I’)挒Ã$.<+¶´y”‡+ŒBƒÐò}D°à5Åó9³ð “g‡q84ç‡foÒámØýK¹á­›3@uñæÍa!‚ƒ ÉÊñbœ±Ý¿óFIqqVpV?d¡/V>A‚ÇÑýÖ< ÜÀ_róójü >ž÷_<ŽãäÕ“:ÑÆ4†ƒtHxÀ&ñÕ…t÷-ö$4MË‘jÞ˜eê^ÈÀýà`|óvœ'äÉŠLCö,ÿ… 4ø4n˜ojŸîÿ æ;‰Ök¾³f˜¯³Y°G˜ -3O_ÍßGsÍÿ +Øû ÎxœíY[£È~_iÿb_¦SÔKyÛ½R4Z%Rò’l)/†²Í¦HQnÛóës Æ€=½Jf”QÆ­î6眺œûWÅóO§}á¼J]çª\¹aבeª²¼Ü®Ü¿ýò³»Nm’2K UÊ•[*÷§—ï¿{®_·ßç8 /ëe–®Ü1ÕÒ÷«ƒ.Ò[?K}YȽ,MíD|w Ÿ^åS-“¿ÊTí÷ª¬›¡eýÃPZg›^üx<¢#k¤ˆÂÇÔ§Ô ¯>—&9y£±°Ï¹±cìo úF±e Ʃ෗ï¨VÊ ”¨”ÆÿËûžéa”™l8O^~¨Ó¤’7ëvÄÖ É^ÖU’ÊÚïèíÇ<3»•Kqû¸“ùvg®Ï¯¹<þ^V.v° *H†üúí"uu:i)y¶rAÙøòtYr9ŒDw2Ó8Âq$Å”x˜x$X8é¡6jÿÔŽîô^f*µz¬ÜC•h­Ž¨7g¿€ÿ(a—d"c5š“׈¸o•&Vv2ý õZ%: lìrÈ3Yß±L]&•·^Û<Ÿå[–W%fwo‚F ToXÁ“ÙVzû<«T^šO‹¿IòÑÊjý«LÍÃÝ7sÀPœ@êÓ§¥ë½Rf÷i•ß´ým¡ÖIq+±É „ŠÞæ¥gT5ˆ§£3ÏÑmüαÖÊ›ÁÓmBäQôù©•ILSÇq[ó °]§ ¼Œts¶]ét¶D·§Úµñ+Qî+Û¡´_ÉšM7DCÄŒÑ ÷<ÏÍ@W9ÎÛÔê|]È[-aeÔlL¶.ºŒ°{.òRB/)Îc9FÏËaYèhM9è€?í-c/M’%&ôƒŽôVt²üËûŸ_ºžÓtùw¥?ô+:ŽIÖêþw_®ôç,]žØ'æ%ßC½°Xäwžý+ãVÚún0o;³–-4™iYºÏí(ÿ¯&/Š?Úe:½Óæ¦/Ͳí×^ÿ¢L§¬?ÔöÙï¬Ñ>nÇÑY$k Iô'ÛœiiÝju¨ö¯—®áì|ÛFŒNÊÚZÄz¾‰‘ïðÂȃ«°à©wÇö6¤c]}?˜l«óÓ;h¶ÅgbíÏå1` €t/hL'³§«û uK䯻ÛO5"½!O·DqHI»£ö‰ÄÁ‚DˆQÎc¾ða( Œð§á‚°¤-’7Ӭݯä¥ÒVÛ"QàŽFÔæl3ñH–äG€nŲ„BóÍëqÁbñcm´ú —T„ñ…Ð6vÄ@mÎXG· ›ZB”Ùø+Tæ[*„¯Ôôg³ä-K kœÛ] ¨j³©¥Yö¸*Q%Pó½‹-[¦cµq C®ÇFoýÙ¡bÂ~ % Aêx1ÂÍ'–^´ úÌSàI>v“¡‡}n°Gûó¶{¾:`Øùø­þ7Âà(~ŠïÂ98–\‚q>¯±>…Ÿ ?À@„ :it  A;6í›¶m¢ áè5‰ó9æÑ= Ä ²ÙæÛ` } À7þ»78òÿÒ.wÃf®7HëqȳáÎc…!Ÿ„»=}qŒBÆØô¬0H°I,OÑ’ÀO£Û(;¸|Úö×Ný·›b>àà°ÔmÎýCotž=Dä·û³Ÿ~pÃi‹$Ô@ŠCNÉ‚c(’Q3'uÀ©Tl²•èÝNu³´¨¿›JWéz?è4R$Š"SŸR$¼úT˜øè]¬…s^[K1Æ>ð&¢o[Öàœþ ò=Õz_%j *”ñßÿü~`z¥&êÉŠu—êlߞغ!Þ©ºŒUû=½UpÈR³}r)n·*ÛlÍøü’©Ã_ôñÉÅv8¢B°ñW'5´”,}rÁXÙ=u[.AŒ"Š˜óŽ§±Q`áPL"°‡vYoð2Õ‰5àÉ­â4Ó«½1ºøu_$[•|Pé¯Yz œ;l§Ž¥®Œ·ÎrÕ®÷·z§ü“Êj]øïÕ‹ÊuiÓÊ/3PéÇ•ñ³tç™Q¨,nè;¦%„,×¹§žûlÙ©Z×\ëûH]Ço™ƒ…öx©uöDt×]§Œ7Ö¹®žÜïÖͧç¬t•ªªç‰æsÎÓ÷̜ڒìõ÷‡¶Š|C ÞÆ©>@V̸µÞ=@,ÂQÀgüRÇ£ˆSÆ$›saÏ1¹”bÆ…¨ïmp¼}‘¨©ò8W°¯*+‘Ç'æ7H/UoõaSYOšj¯fkYFy]ˆÎmïDú¢ $oÉX;å-æés³]öQÁ9ÉLÆÚ0À:ÎǤ¸í—&]šê¨V:®Ò‹…göYªê¾©‹¸ôV+[øWù–啱ÙÞRÐú ;x*Ý(o—¥¥Î óºø›$ïí¬W¿©ÄÜ=}£ö€n¢Ð°^—®wZ›íë&¿éø›\¯âü\bH•j“žÑ%ÔêF®Öæ:§j3øk¥¡î&¼á,MŠÜKƒ¡B+mbÓ6ö¶íAoÕ@ v+Çœì˜:ž,Ѩ¶H-% ÙHT»Òެ>È‘ÜÑìLBT0.ƒ€Î¸§ëÜlU—Õ`§\­run% ˆš^’mˆºöÌyV('ùéRNƒÓ³â8ñlO³§f€?-c§LœÆ&žŒ„žÄ/\YþãýÏýI²ü·®> ;:މWzñwŸGúcš,`ìbóœí _Xpò'ÀþÈ8—¶±›èm5WªÅ*WQ[šì2»Êÿ§Éòü¯v›Þî‰ÚÌäê¹Ù¶ý9ØâwÆôÆúSkýÞíãæ2;óx¥ ˆþf‡‚3o­›JïËÔk77܉ŸÏ‰©â¢¶±†ŸylÔ;¼ð¡Ë€? áØœ§´dáû‰ðm•ßÁ¼å‡,ˆØ~»G,ãE˜PN[P P‰P<Œá›lÔoÅÉYt7g‘jDæ‘{Fž‰ (’‚’öDí‘|AB4C|áXƒX˜ö<Õöh›ä™ú‰·‡¼DÙÆjgb€HÈÝ‹µ9ÙJì0É’|è-_pch~y=ä$ Iô}m*ýA-;`„qGhG;â°7 ‚žn+µ„(Ò)ñ7èÌçTH_Uå0ŸÍ’õ´4†i\Uñ©=Õ„ª×ëZ™åp€Ñˆ2†žï5plÙ2k=ft}éˆÖßÊQÙϬDl Ž'n>Ryá‚2hCÿÉá…´óË¥6|)ƒk@Rº§]y€©^b³¯ÔYãê‚3´*¨[Ø0ÆøœõõT¸¹ö?;ÒhÜŒeæU/Nhn!?ÁÝfî´{™ëÙÔe àkú6é{þ âvá˜ ÜÆ¡ÁLpyé5àQ ɤœyhˆ“a £K^ÌI†—ím¸Þvüñ’1Í{XÎsý3DZ)cƒà—É$«’\]ÆÒ† JHH: ¥½Óõ•FfÌc³’Ý,âþ!%¸qÒ/= “éÝpõÎcüášÏ=èVËHÌ ¤ ŸÎÂq& .®ðÆÍŠê³„ 6Ãâ Ù'¡¡ãd¦Ìbô™'g„gõØON6…ö¹Áíÿ˜µÓóÅñ{Hçã×QøßHƒ¯ ø(¾ çàZÒ%ãõ\s}>¾ƒ‹èlÐ5H€uÁ|nÚ±‰¸põšåÆ€ ø ³ðb‰(ÁÕáÛ`  üC0‡ ¯üN»7òÿÒ/7Óf–®gHë~Êò«éÎ$ …`³t··/†‘‚`~W˜Ø,—çh)Âo£üÍäåÓfxí4ü:kæ×“œ–:ÏÒ‡>ò`G$'ŸÏAýä §m’Ð)Œ’ÃÐ$C!'qÀÊXm·*/«¡yù]ߺHV¹ÙÒÁ«¬H†.¦.¥X8å)×ÑÑŒ…}^K1Æ.èz¦7šÍKðìþtö­•j_Är%Ê¥v?þò±S:%:éÏÓ:öbÝ oçÑV–»(–¥ÛÊë i¢7÷6ÅõãF¦ë>??¦òð'u¼·±…-ÑpÎÙù[cuF ©%iroÃaEóÔ,9ï 1 )‚y’ˆ‹0öˆ7³(&¡ƒ‰CšIÛãΛíßÛZ©lΘI&Óie¨ój·Ž<îT¡UšÉz¨»Q[éždZªÜý(e¦vOî.Õ ‰ í¦±Êÿ¥Z¢]>1ß1ÙA¬B~]{jµF½H䪬ìjo˜Gj[n­ìg¶—/÷L—QÙDDzvÑðœ©âÞþnU}ZÍR‰,Z¯>—:Oõ©NävþvÓfâÎO”›(Q€ÃHûY©-Șú¡écÀŒÃßg|¬6»Âˆ Ïóǃ!â{gŸ§²iwO°/ c‘E' ç_ /lmÊ:¬ ãH]ìåhä!ÍáLN|ÒñÑ“6ÆlÊÆ¤Æ”îô„nÓmúYÂ.ÉÈÆœ ïþU”1í“ +’ÅREE2XùeŸ&²œðL™G;g¹4é~UoTÎ.Ò›© *ƒ\ݰ‚#“µt¶i²Si®Ÿ7¿Éò©•ÕòWë'w_Ík@S(SÏ[—[¥ôæù#ß´ýu¦–Qvi±J5@¥X§¹£Õ®‡§ž"“+}]SÔø½¦Z*­MZA¤ƒ‡Úbq¶€äjFY–>™¾s<¡ÝIMöI°³Pnw¦UdBœÅÌd¢œùPèH{º®MàrtÓ¶Êt™ÉK_Âò¤ÉPl¼ßŒ0{ÎÒ\B›ÈNC;þLó~Æ·²*ÓÛÚ{­ØJ%‘Žz¥¾ù—ÌÿöñLJv…EÏÿ©ŠOÝŠ–eL¢¥ÚCh퇳|‘Äs` ÛH?¤[(†müÂÂ=+.­MìzóÖ3²&WiXoS3Êý»N³ìg³L{îÞ´©ÎdOºp›3´gtû‡\¸­êÇõY´”1•ÞËu¡ö»-dà½]5»çÞJÐ ÑE”—Æ&°ð5¾úÏà2(ÀÂóïº(¬/‘,Xpyopi‘?@ûô)˜ΰùi}o\-Ä„ú„³@yÅÞÝ9j½…Ú¥|rÔõE€*‚ýо·DP NI½£ú‰FäQÆ›9ÀºO=Âîú Â’¦ì]Lßóv·’KS*M“ó |{0¢Ô'“€ Ř“ïŒeó® Õ7§U`ÄBOЀ„ß—ºPŸä¼á97‚ºUƒ!€C3Ïkå&QaSs@žô…¿B­½”je‘AÇÕsÖÊ’úkQD§zW=©Z­J©çÝ·ØEPÅŠ]Ík¥eNcAU…®[Ñú«E}ä…æ3ƒS"g –#®>B:ÁŒ2Žh%ÿÉò1 *kë_ÃÙLðMl„ðFªŽ©œ¢UáCzŒô¾õª NW¡ oL>CcŠás™Ëס09ö?ÛÒùp#•©“W½Ø“ùp›ø î(c§=…\Ç@—Áïð­à; q8¦·ùPà<Æ}1ôè¨ï!Á„yúf€8'¡®eÚžIÃòÖ]S½à×Ðc6+‚1Ö¿B$¡”2Ö¾…HÆigrK.H!.è(”æ†Öf)ÕH6™Ä}•„âp{ô­‡¡×½ ¥ )qp‘+>w Z!,B>J*\p£Æ£pAœ EÜçWtç’ê«„ 6Ãü …ìElèØë)£}åÎâQ>¶“õ©‡y®¸Gý7fu÷|´àÂúüÞ ¿ ÞIñ¤x’ÎÁµ¤ãu,ž±>¦_‰?Á é¨ÑUˆÃé¼qß4mùœÃÕk„ŽAðfÁb>â¡Ç½«Í·â@^ð*ȇ ¯ø†Ý ü]úe6#¸^ ˜ÖÓõ¯Â pÎFp7·/†÷gžá~ñlcöÝù\Áì\:zæ…O¾Áb¥ëZ¼fÀFëW¹32Ø2#ââ-Ô8YMYmÞÓ *ßÑü—ð)hŸÅ'†[cÿß+ÛOƒï)õ93Ýc1’›Ÿ ¦]ö¿W,y3òúÿÑI'BK_‰ç5Å ¿¢¡/Øs^¯l üÕ±ÎÞ±þæO:ZþšXçˆA±æ7aH1†yFóShª„²áUú‹a]¼cýÍŸôzhþÒXŸÀbS«ÍK¹› \:pøÚPgôêoþ¤¡}$n‚zH0#œ_zKÁoÁzKwnœÿ‹a¿cýÍŸt"´â5±ÞRð[°ÞÒWƺÿ»¸šŽ_âÔ¿æÿ8ÁïßKª]¦ÞøxœíX[ã¶~ÿ (/;ˆE‘EQÎx‹ ’—4m¾²DÛÊJ¢ Ñc{}u³.öìÍ.²èz03æ9‡—sÿÈÇïÏyf=˪NU±± ¶%‹X%i±ßØÿüõGØV­£"‰2UÈ](ûû§¯¿z¬Ÿ÷_eYL/êuoìƒÖåÚuËc•!UíÝ$ve&sYèÚ%ˆ¸öH>¾ÊÇ•Œtú,c•窨›©EýÍXºJvƒøétB'¯‘"aº˜º”: áÔ—BGgg6Îyk.Å»À‰¾Rl]ƒqJøä{ªÕ±Šå&JTHí¾ýõíÀt0Jt2^'-ÞÕqTÊɾ=±5C”˺ŒbY»=½]à”&ú°±)n‡™îú:~Nåéoê¼±±…-ÑpÎÙõ['uu:i)i²±AYѺ-×ãè@Ôz#E€E®,Š)q0qˆ¿²âc­UþÐÎîõ^'*6zlìø ãwYZëßÒ"‘ZVyZDZÂ6Ðh°ò°¯<—ªÒÎ.Íd»‚{P¹t/2­UᾕÏ2S¥‰/·L5P¢J»i¬Šß²TKTwÖ;'%ø.ä·¹—žûd؉ÜÕ\k3¤¶å¶ÌAGs¼ÄX}$ºêÎ[–UF{ˆïLUû›]óé9[U%²êy¼ùLy  Õ—67ûõûC›…|G >D‰:Ax,¸ï•Ê76£È8fdÁ!†„)~°äšCQÄ<pÁ¿sœc‘jH®ò¼\àXUF"‹.Ôß >(PÔi_;êê(3OBêäty@BºÔ¼ésƒ`ÌîɘL¹Ç»¼ÀË£sš§ï%œri;£ÁØú»(»Ä}›4¡b2EV[UÉlbc—cšÈúŽeê"*íÖdÿM¾a9e¤÷h õŠ™ì¥“§I©ÒBXüU’/í¬¶¿ËX¿xúf ØJˆBÕú°t+¥VùUÇßgjeS‰]ª!Tª}Z8Z•£x12¹Ó·9U¿·X[¥µIàe€6!òR ÙY)馺ã¶äA]».ØÍ´,}1½ê|1D{ š5”0`W¢ÌKÓ· !®äŽfÒ QÎ|áytÁ½Üæ& «œgƒiuuºÍäTK8@5™“‹ºæÌYZHh%Ùe.§Àèi1. =­)}ýw—  eäRGI¤£Q;èIþ`eÀ,ë_ÞþðÔïðÇë«êݰ£e‘h«ŽàûéJLâ5 Œ<ÒOiõ ”oT<ºWÆTÚøn´n»r%[Àrº%qžšYî?tše7Ûôz–Mu&ŸšmÛ¯ƒ.n§L¯¬;ÖöÑí­Ñ÷óèÌ¢­„$úÉôkYZ÷•:–9äëÆnZ‡=²sC¦è**jcãaøšàxƒW!Æówì§!-Xpõýh°m•žß@¯õ)˜®°ù醾· bB}ÂÙŠ h„bïáê¾ÑFýV>™xw?ñT#B°ÚòòH…¡à”´'jGDø+ 2&ØÊñˆ‡|êö0Þ¶4Er²üÈÚÃNN,Ma5-ÑC$ðíÙŒZ_L&vxdM¾ä–­ ¸64ßœž =a@Ãwµ®Ô;¹î@Æ¡mì ˆàÌózºÉX8Ô HÆÄß¡2O©¾²Ê ?ë5ëiIݸª¢K{ªUívµÔëáW%Êj¾Ó@±uË´Œ6Ô`èÑõÜà­Ÿ-ê#/4Ÿh‰8è@-G Ü|„t‚eцþ£åc4ÒÖæ«çßá-XŠRE«Ê<õéc%'…«sÎPª oLbC‹á3MêÛ¡pwîÿv¤«r –)˜7­8¢ùˆñð £½¹Ž ] | ß&|çî¯Ào3ÃT`6 œÇ¸/æVõ=$˜ ‹6·ÎDà‰pÎëq¹çCEÁ¼¼ wÜŽ/ø­è1‡…›è§ð$”RÆÁÏÁ“qZÅ™œûÒ¸ Rˆ ºp¥¹Ïõ™FÌs3“ÝMâ€~’„âž/ýÜÝ0êÞ —oæ?ܲ¹Õ aòE‚4î"`‚ƒ…»ªæjÎ}~ƒwuÙ"©>ŠË `3Ì?#—ý!4tõ”…>rç ñ"ûÎÉÆÐÃŒìÑþŬížÏ–ØCXï¿´Â?# ¾€â@ñ]8×’.oÇâ5Ö—à#Aà0a!]4ºqÐÎ[öMÓ6‘Ï9\½±1` p>Ã,¸‡˜xèqïfóm0| äÃ…Wü…ÃîŽü¿´ËݰY„ë$âi½²þÍpgœ³E¸›ÛÈ{ž·¼+ŒlËK´â‡Ùk”»=>í‡g§áÛ¤˜ßpp Xj¤wÿؽçAsŸüqË^8M‘„H1g”¬†"páY±EæQP®0  BË÷Á‚7ÏçÌÂ+<.ž½Ã±9?T0“ŽÞ :[µbq´óñt—½ß o=•õ¯¹ÝSÙ0t8 [Ÿ5/e!F„Âèå—2>)ê·ÞÀ°Ç}Œƒö låDÁÔ+‡@xaÑÉãײË7~` N)ã+Ü Ü ë`«“€Xÿ² <B°~´<Ó½ ÃyÞÁ´ÍfýkbXÉÍÏ<¾à–Fçmï^›ö±e´ÿÍ[1üÿ/T4è%ƒxœíYYãÆ~7àÿÀÈ/;ˆØì‹Í¦<3’…aÉ‹í @^ŠlIôRl‚l¤ýõ®æ%R¤ffáì&ë,gg%VUUõÕÑ=÷ßö™ó¤Ê*Õù ¼pTë$Í·‹üò½+Ne¢<‰2«‡E®ß=~ýÕýŸ\×ùk©"£瘚ócþ®Š£B9ovÆ+Ï;(m‰H—[ïÎq] ƒ«§í×_9ŽkçÕ*‰í˜âPfµl{*S{•›Ê#ˆx‹||‘íÒ'ëý^çU=4¯¾J—ɦ·[:²ZŠ„aèaêQê‚„[sÜ«±°Ï¹±cìo úJ±U–-à·—ï¨Ò‡2V¨P®Œ÷ö—·=ÓÅ(1ÉpžÎ°£uGÖΣ½ªŠ(V•×Ñ› Žibv Š›×J·;syJÕñ/úô°Àv|DC"„à—o­Ô1¤¡¤ÉÔ•í[»äªÄ(¤æI"!ضt(&¡‹‰KÚI;uW‰ŽíöaÂ"KQ¥ Ó˜4Ž2ÔÛ³_A ]w“fªäíô^yg•V:÷Þª'•éÂ"É+R”¨4^ëüß0µBE~c¾SR€—B1Ï=wÜG˾OÔ¦ªå;ØWºp¼†Ù«e·—XûD×QÕúÅqŠh HÎtù°øfS?g­ËD•OÔϘ§ÁÕ©97!ÜÍßmÚNÜ àÕ.Jô€0á¾×ztŽ$¦~('üÐ"Ôa2Ã…5% E(9™0ÁÓë÷§¢¨8MÇÊÒJdÑYöõG?QµÓÇmi iʃšŒ=¦9èä¶'!ªÞŠta@0æ·dlPÜâŸáí£SºOß+ØåÔVƒ¡ù7QvAÄm«ÔXÙ©ø*×:*“«µ]i¢ª–©ò¨p×kè³|Ër‹ÈìnMP äú+¸*Ù*wŸ&…Nsó²ø«$Ÿ[Y¯U±yv÷õ°d'…õ²tµ×Úì^VùUÛßfzec‰Mj*å6Í]£‹žŒLmÌ<§lð;ÇZkclOZCdƒÇFâþ"ÁÕŽrs¶çt¶ÄEOµÑg)aÀ/Dµ/lõ©Ûy!·4Iˆ îKÆè„{žç& †ºº-XUºÎÔØ–°<jrM¶ÖoGØ=gi® Ldçk9 öLóaÄw´:Ò»ÜîM“{ÃØ+%‘‰©¾#ù½•¡óXýôöûÇn…û8^ýS—ïúÇŠDk}×./ôû$^A¯°Ìcº‡T`ûŒ?Ckpï]cië»Á¼ÍÌ¥jÚŽÙ,‰÷©åýlÒ,ûÑ.Óé=˜65™Pï½V‡NGo¨ä½×¡yÝ^2‹Ö Ââo6É;Ód¹-õ¡ØC¶u`10ï¸0˜2Ê+këXøšA§ú/]èbP€%óïz/lÇH–<¸¸|0 ˜´LOo |úœ…KlÚWŸ-¡K 1¡>|I%4;„bvwñÚ`¡n)ŸŒœº9¨!Ø#òtK…¡”4;jÞˆô—$@Œr.ùÒ…~ ù”~7\–´io4ýÀÚýJn¬lª´EŽ!ø‹«•9Ûl[Œùš±l•áþæv ŒxÈ$ HømeJýN­Ú>ã–ДjÄtÏœ±Žn6µäÉø+äÚ1P«Ê *®YñŽ–DP_Ë2:7»PõfS)³ê7pQ¢ˆ ‹»uwµj˜ŽÕƬ U·º6xëïõ í³-‘¨ãJ„ëG*7XR.­é?8>FA-íüëz6ë|ë)Ù„ÕwF:£]ºÐ#=EæPªQ¾jÓg(ˆÏP˜bxƱ<…›cß–.ÊMX6OÎZq@óáñœN¦F{¹®….‡ ¾À·†ïµûKðÛ•aJ0› ŽqáËk«ú I.åÄ¢P7$¸ ˜ ¯y]§Í|ÈH2¸NoýµåK1‡»YL±þ< ©”ó^ðsðdœ–q¦®}iÝ!$$¸ÒѺH#æ©Éoq@?I@ æKI?w7 ªw©mJ\½ÈŒÍ]ÈVK88Ϻ‹€M&î?Š„/fx—M‚꣸ 6Çâ3rÙuC§AM™øè#WÎOⱫœ|ØzØ÷º÷hþǼ©žOŽ ½‡tÞ)…ÿ |iŠŸiŠo¶sp,iÁ8Å Ö§-ÀGjŸéé¤ÐÕ=íØ´nÚ²‰|!àè5ÁFßó9æÁ­ˆûH„L°Ùâ[÷@,ø$=^ù? »W8òÿÒ.7a3ëqÐi=Yî\¢@>»=}qŒclzVØËÓn)ÄwW—PÞvpù´í¯úo£d>pp ½Ô¤sÿÐçAPŸ|¸?ûé›6IB¤XpJ–C’ „dNìhó( (—„¡ãûˆ`)j ówð“gçãphΗfoÒÉm䦙۰¥Kì?¼¤aP_sÐË·áÝ×U@ÚÄ4ÝoÔI“IBù¸¥êЇáL8ü;dó´Øgö˜§#xÝ@‚öçÐ-ùßKÃx¸¶óHËY—†¯†Áóº…»§öÂNzž±s- ¥œ~*<3úÏŸ±–³.}= ~ž›k…áÌfd|YôQá,þèpîš’æóÞþÍ>~ÀÐ]Æ$äxœíY[Û¸~_`ÿƒà¼dP‹"Å‹(g& ´Á¢¶/ÝúRÈmk#‹†DÏØ)úß÷²dY’=“6YlÐØÈD:çðrîéûÛÂ{TUëòaFžyªLu–—ë‡ÙßßÿèË™W›¤Ì’B—êaVêÙo¿ÿî¾~\ÿçy0¼¬Yú0Û³[Án_HWë KU¨­*MD‚YO>=˧•JLþ¨R½Ýê²vCËúU_ºÊVøÓÓz¢NŠÄqà0C$üúXšäàÆÂ>§Æ†ãx=ÑŠ-j0Îþuò-Õz_¥j*• Þ½×1}Œ2“õçÉËušìÔź-±1C²Uõ.IU´ôf‚§<3›‡Yˆ›×Ê×s~ÌÕÓõáa†=ìqÆDÁÎO'©³ÓICɳ‡(+Oo§%ýè@Ä{M—i¨$¡+>÷BbÇ>Žæ^º¯ÞÞ5£[½™N­³e•”éÆ×;U†¨³i·Š:ìteüU^¨F>Øè­ Ž*¯u¼SªÐ;MÁ.7@I*ä©.ÿUäF¡]ye¾C¶OÅbš{l¹o-û>S«ÚÉ5¶°¯aÃfmŽ…:q=Ï­çŒ:˜ ­ëYG·ãÒ}UÁFýTºòëtYÐNãyèO–ü³£úïaïßÝhÏ Y¼Z…öû¦¥ÿ§}¸Ü.>Ï–|ú»Ü”ø´]a¬øju{W÷ueóØ¦²Ì¦JÏãˤVíwÉZ¹5f¯VîÓr–ºÊTÕò„û\ò4dmnŽMAmçocÏNÜ à+õ&ÉôäôˆûQë-Ð9ŠgLŒø)$> QÄd(ɘ kƈK‚ …dÝÛó÷en "îã NN+’£õ×’³V¦Þè§ueíhª½|ÊKPÉ?/‡cÍO"mA#³k2¶¼]ãoð¶É!ßæìrl«Aßú«¤8Äu›¸PÙ¨ôƒª–:©²Á@g—}ž©úŠeê2ÙùË¥-Ù“|Ëòw‰Ù\›À ”ú+ø*[+›g;—æyñIÞZY/Q©¹¹{7¬}D¡Õ'¢!c’Í}J(â!%쮿 ,i‹äÅô=kw+ù©²…Õ¶DŠHÄgƒ$=ÌNxdAÞ/%œõÜ“ß20b1•aDâ7µ©ôµ8"ŒO„¦±ƒ ŽàÔÄ(mé6caS €2ëÊ|I…ðUUýÙ,XKËèÆU•›]õ¨zµª•Yt8+±K æûŠ-¦gµñ C®‡FoýÕDEcû™ƒ–H€¡çK„ÝG*?š‡L ÐÑÿìqŒ"'íýs8›u¾õ”tÄêP”.Á(`/à©ÇÄì+uQ¸NÎéJäMlhc)|.“z:®Žýß¶tVnIJsÒŠ=G Œ‡'Œv+r}º &ø¾.|‡î¯ÀoÃT`6Ž2ÁåÐjG{~ H2)G…!ÁdDe<䵸œr¨H2–·îbâÄ—b*zìfe4Žõ/àI(¥Œu‚_ƒ'Ó¼J 5ô¥;îÑXÀ‰nh6{žk3Œ˜7’]Mâ(üMJP.eøµ»¡×½W¯}Æï¦lîCµBXÆb” Î]lBp4rWåî‚‹ ÞÙe£¤ú".ƒ‚ͰøŠ\öIhèÐë)#}áÎãQ>¶“õ¡‡}wØ£ù‹YÓ==°‡ô>~k…Ÿ# ¾â ø*œƒcÉ)§cñëcð… ð DXŽÃ@´£ã¾iÛ&âBÀÑkç3Ì¢kˆq$b*èdóuˆF¿ âpà•¿ã°{#ÿ/ír5lFázq€´n‡,Ÿ w&Q$…»=}1Œ¥t|Vè%Ø(–Çh)ÆwƒÛ¨`Ý»|Zw×NÝÓE1Ÿpp Xê2Hëþ¾7Zσ±œ|º?»é{7œ¶HB ±`!™3 E2’z©Gæ…$ q<Ç("a{œ#‚¥pÊóð÷‹gëã¸oÎç fgÒþmØíK¹þ­›Ï{%zòêÍç1²6#tîƒ\H"£«—cÏ­|¹vŸ~eqb0"æ>‹¡Y‡’ß=wó&‡'Ï©‹78Ã17<]JñK>÷b!-'H‚cc1¼y[Oäfÿ‡°ëËÚ;> ]ƒ¸;G—q!–°f;-‡ƒÒ£ÁªÓ¥±Ë‹S6pû½þ”cdTö–{cFUÏ%ÆgÉ’¾\ ‹G}þ|wg#(6ñ¨ŠõêŠ)át-\%#$B±d㓲«e4Ž ±viäxiÜž’‹WJØï°¶4?ìŸnßVs5† ™¦™[@=F<‚ f# !%¨$Ìó/ £rÝ?EN¬sjt¯j¿ †ÿwEéùš$¡ÙI L}NBbvwþ‘Ç.obÿƒw^ó¶!0xœíY[ã¶~ÿ j_vKâE¢(Ç3ÚEíK“"@^ Y¢meeQè±½¿¾‡ºR’=;A»AYvgtÎá!Ïý£¼ùîrÌ­gQÕ™,mì"ÛE"Ó¬Ø?Úÿúù{‡ÛV­â"sYˆG»öwO_µù‹ãX«D¬Dj3u°~,Þ×I\ ëíA©ríyçóÙÍ:¢+«½÷`9,…Åõóþë¯,Ë‚½‹z&v·¦´ŠK)+åì²\´‚ÞA…wY- ïx¹,uöxe¦€WÊËYü;Ï”pË⎾KZBd"v›{í¹Oš½IÅ®näZÛõ#±-¯e¦èã¥Ú§†è6®»XXVï!{sY=Úovͧçle•Šªç±æ3åIo¦®mÙöúûCkŃº#PâTž!ø î)@§®¡ˆú ~‚‰K9i°äž¡‹)¡AÈ\ïIÇ9™‚Ò)/K§ªÒy|`~ó ÷RõAž÷•ö¤ªNb±öœ`”Óå9ŽÈÒöN¤Ï}ŒÐÒÂNFWÂ=ÞõÞ1¾dÇ샀Sâ…Œ¶Àôÿ.ÎÇ”¸ï•&Y"y/ª­Œ«t¶°ñË)KE}Ç3u—Îv««û&_³œ2V‡{ B¾bG¤{á³´”Y¡>.þ*Énç‰Ñƒ„Üþ&õâñ%° ô$…¶ôqéú(¥:|ÜæWŸËmœO%v™‚\©öYá(Y e0r±S·9U›À·X[©”®áe†69bæÁS+±% ººU–¥®zÎ\®šhT]~š…þHÇRÏœ<ð‘ÜÑt)¹„ù§”,¸×ÛÜÌóL×cªÎ¶¹˜úPÄ@Mçdíýn…>sžE~ËIðgV˜%ßÓšRï»»·lï-ã(TœÆ*6š}O /ÞXÿóÝ÷Oý›$Yÿ"«÷ÃŽ–¥Eâ­÷W ,7 û憰¥n{õ†·‡œDèV©§uqسµºêì@Æ p,_pMhþrz A9 qôm­*ù^¬;¤ƒPGhg5¢0³OiO×… ‡ZC©Iü zí” Y+ªF®Zû=-aÀVU|mOePånW µ0QÆÐÅ_­[¦¥­± «ÂØ­çN€hýÃ"K#ýY•.ˆåp5.œpE|æ’†þƒ 7l¤­_çÚtðul8§ ÖdNQ²r$=ÇêT‰I¿ê‚3t(¨]Ï0˜øLkùv*Ü]ûßi4nÁÒ}ò¦ Z·‡àN²tÚK™ëèÔõAÁ—ômÒwþ â6sLn  ÁQŸ|î5à‘€ºÜç|áÑ‹ý̇æ¼jÓ:çím¸–v|Îne>,—¹þ " ­Ô÷ÁÏ!’IV%¹˜ÇR‡ Jˆq²¥¾¤õ•†ÌK³Ò¿[Ä!ùC ŠÑ€sò¹‡Á˜Þ•T”8>`‘>w [¹ˆGlQ M¸0ø£p®ª¹³€Ýà![Õ' 4l±Ï(d¿ ]Œ™²ˆÑ'žœZÔc?9}zèç{´ÿ#¿žÏ–؃[¾ŒÂÿE|Å/€â»p®%]2ÞÎÅ1×—àAà0ö#²t b`]ÎM=6Ý€1¸z-rcÀ@|ùá= ä.‹(£7‡oƒhø‡` .¼üÿ8í^È?¥_î¦Í"]'Hëå” n¦»ÏÝ1‘îúöå#—QJ—w£À¹¼DKz˜½„òöÆË§ýðÚiøkÒÌo'8,5Í܇ߌFy°#b,À¿?žƒzãŦn’Ð b>Á+A“ §Vba€y‡E+䆘„‘.Fœ50ßB+d6Ï>Æ‘éÎ5ÌÁ¥ÓÊymÍ ¾z#˜þ™»+è¾2ü*e¨btåþ«RÖsl ‚á[ÓŽ5rš—Ü, ¢7RõȈ¨ËزUûÎ; (d»Ž™ÔT'q)3'¥þZÎଓ‚¼Üû&Ž]ðãO”ÃË~ÝS6úU;üþ@›' ÒxœíYYã¸~_`ÿƒ }™F,Š—(ÉÛî‚Á"’—dƒy d‰¶µ#‹E·íùõ)ê²,Éž^$3È ãFw[Uţ꫋ÔóOç}á¼J]åª\¹aבeª²¼Ü®Ü¿ýò³¹Ne’2K UÊ•[*÷§—ï¿{®^·ßç8 /«e–®Ü1‡¥ïŽº@Joý,õe!÷²4•OñÝ|z•OµLLþ*Sµß«²ª‡–ÕCimzñÓé„N¬–"qû˜ú”z áU—Ò$go4ö97–bŒ}à Dß(¶¬À8øíå;ªÔQ§r%*¥ñßÿò¾gze&Γ—ª49È›u;bc†d/«C’ÊÊïèͧ<3»•Kqó¸“ùvg®Ï¯¹<ý^W.v° !¿~k¥® “†’g+”Ú§vÉåÐ;uÞÉH¤Qˆ£0^8Sâaâ‘`á¤ÇʨýS3ºÓ{™©Ôê±r ¹1‰Öê„zƒöKÈóAiãmòB6ÂþNí¥‘y¥Jÿ½|•…:XWò¹J¢Ÿ§ªüg‘‰åùÎÙ`ŠÅ<÷Òq_,û9“›ª–k a©ëø ³WÇn/³ˆ®“ªÆqÉ\¹Pzåþ°©?g­t&uÇõç–§ëÜ\š0ìæï6m'îðj—dêž0á~Tj¿ryˆbFã);o!1Gá –ŒQÅÇñ„ -6Þ±Ì „Ñá<ਵ•(’‹í뤓ªvê´ÕÖFådì)/A'¯õyÓ©ê­Hc~OÆFÅ=ÞåoŸœó}þQÂ.ÉDÆj04ÿ&)®qß*µ¯ìdúAêµJt6XÛå˜g²ºc™ªLÞzm#}–oYÞ!1»{Ô¥zà žÌ¶ÒÛçÙAå¥ù´ø›$­¬Ö¿ÊÔ<Ü}=¬é D!C}ZºÚ+evŸVùMÛßj·›Ü€«èm^zFþ4`ØD8ÏÑÿαÖÊÁS­]ä‘ôñ©•ILÉq“ó ±]§lG:Ž¹Øºt¾X¢ÛSmˆZJò+Qî¶FÕýBt%·4nˆ DŒÑ ÷2ÏÍ@W9Ž[Öª|]È[-aeÔlL¶µ#ìž‹¼”PKŠËXNÑór˜:ZºàO+@ÃØK“d‰Iõ #½•¡?YþåýÏ/Ý Ïiºü»ÒúÇŠ$kuüÝ—+ý9K—ÐQìó’ï!_ØnäwÐ@<ûWÆ­´Ån0o3³–Ms2Û¦eé>·£ü¿š¼(þh—éôL››B¾ÔË6_{]üV™NY¨í³ßY£yÜŽ½³HÖ‚èO¶$8ÓÔºÕêxØC¼¶UÃØù¶Œ”•µˆE¾‰‘ïðƒ¦A·Â‚§Ží­KG<¼b?˜l«óó;(¶Å!gñÛŸö1` hêbLh@_Ј"N(fOWø uKäÝí RµÁAìÞ§["(Ž#AI³£æ‰DÁ‚„ˆQÎ#¾ða( Œð§á‚°¤M’7Ӭݯä¥Ò&V["aàŽFTæb#±mH–äGhÝŠe G„ú›×10â1‹hHâ+£Õ¹l»"Œ[BSØA‡ÐlsÆ:ºXØÔ Ì†Ä_!3ßRÁ}¥. >›%ïhYÕXëäÒìj@U›M%ͲßÀU‰C9ß«{±eÃt¬6ä`¨ÑÕØ€ÖŸ ÛÏ´Dt Ž!\"é… Ê¢5ýN€QXK;ÿÏfÁ·ØD›°ú>J•`£´ÕkbŽZÞ$®œ>UAÜØÀ†2–Âç6¨ç]áîØÿlKWå&,›0g­8 ˆƒñðŒÑy®g]—ÃßÜ·vß1üpFƒÙHpŒ‹ [ x4`(âQ4±(Ð …,ŠÇ¼®/gd¤(§·þ<Ûò#1ç=v³pêüHB*å¼üLsrŒ¥… BHDt¥=Ñu‘F&Ìs=’ß â~‘€,ˆ"úµÃ0¨Þu.ßy×½Góó¦z¾:ô‘óñ[)üo¸Á·¦øAS|·ƒcIëŒó¾xõõi ð™Zà=á1ºº ›ÖM[6Q ½&¾Ñ÷@>Ç<¼×ñ‰˜ 6[|눅_¤ àÀý»Ý€ü¿´Ë]·™¸ëÇA§õØeƒYwç …àw·§/Ž‘`ŒMÏ ƒ›øò´[ŠñÓè6Êß.Ÿ¶ýµSÿí&™Ï;œB/u뤃ˆF‡<è ߎg?ýà†Ó&IÈ NÉ‚cH’¡ˆ˜“:Ú image/svg+xml ¤#ÏxœíÛŽã¶õ=@þվ젖DR%º3 ] м¤) ô%%ÚVV]IÛóõ=¤îÏζ˜´‹Äƒ™Ï…‡ç~(ßs9dèIeªò‹8ØB2U’æ»ëï?}k‡*«(O¢LåòÁÊ•õÍã×_ÝÿÁ¶Ñ_ U2Aç´Ú£ïóe%z¿¯ªãÚuÏ糓6@G;÷Ù6°sù´ûú+„ÈÎËu?X ÏñTd†6‰]™ÉƒÌ«Ò%q­}ÜÓÇú铌Õá òÒ°æå»!u‘l;r}¤³g¨ˆÂÅÔ¥Ô »¼æUt±'¼pÎ%^Š1v7 }%ÙºËá·£oN©NE,·À(\V>tH;I• ÷i ;’;²vdyŒbYº-¼Þàœ&ÕþÁ¢¸^îeºÛWýú)•ç?«Ëƒ…F¾Cᜳþ©¡ê#†Ô4y°@Ù°Y5"×!vu`Ÿ$⡈=â­ÅDؘؤٴUw¨XÿÁŠ÷2þ¸Q—ŸÓ<‘•,içtFíÄÈËQ•½M3Ysº{uîU¦¥ÊÝòIfê¨ÃÉ=¦@¢¢rÓXå?g)ìwÌoìwIŽà*Á—±×û¨Ñ÷‰Ü–†®6†^R ¹5²ÓM/ÑFn¢²qBÇhᜩâÁz·5Ÿ³QE"‹ÇÍgŒSàï´ºÖyÜîßZoÜàå>JÔ¢a†}Vêð`qî0F=ÁgøB†`'q8gŽA¤p<Ÿqæy3,¸û¤}cŸò´‚T:^朊BSdÑU‚öæi©Ê½:ï mȪ8Éï‚Gí&sÕ’6ÆìÎŒ[¸ë ¸CtI鳄S’Ö`hþm”õqÛ*&VtŠÈb£¢"™0»œÒD–D/7Žöf£Ó}Ñte£j_¾D+#b‘ “`Ëd'íCšUšWŸ&åK’ÕæW/žÞì2 F)”©OS—¥ªý§U~Õñw™ÚDÙ˜b›V+Å.ÍíJ5@dr[-cŠ:€—PUU:…çjbd5Å}OÙÕp!T]uß¹\5Ðê :ý4D¬ÊÃQ÷ 3L„=¸éTr(g~èyt†½.cPCN#]·­2ÝdrlK8@4™‚µõ}æ,Í%ô‰ì:¥S`Ï4¦| 3©Þww^ÝkÄAVQUÑ Ö· ¿³2Ìë?|ûØJ¸ãõ?Tñ±“ˆ&‰6ꮵ{ø}¯ab8DÕcz€Z §?€pïöˆ1µöÝ`ßzçBÖÃÇâ–ćTs¹«Ò,û^‹iõl›V™|4bëÇN·Q¦UÖj{ï¶Ö¨—»idfÑFB~üU—{4/›»BŽHŦ#X;[DUDy©-¢= Œïñʆ¡Æ pèùw;v} /1±ú˜ð1[Ù¡p¸ÀXлqÔ¢mo½¡Po']xŽéŸÊs¨Mª°¡>EÕ©£h¬%@B„φpkÈfÜÁ^ÀéÊ ©½#˜'© < +¬Ÿ}­¨;ôbТgfÅ«š7D 9m¨š°1´°Õ=Øy+»8„ÔœOÈHl‹ZŽ–ÐëÎêõ gt@KÛ(¤ËGs]GÌIã~s’º¼!!!&C€~âi47¬Z¨¡ïØF-Ò(ࢱ·ádµ½ [µÛŒA5c€Vr³6z-ÙnO8eèyeuÕE±™ûÖäO0!gëw’ë³°{\Yê£ì°õ²ž›Ö¸]êZ ‘¹.ÿuŠ 9„þýn ÅB-Ô,2{ª5kaICNQD×uÊ!Tm·¥¬zIýÁŽtRÛŒ¸ë‰ôÑt6}Êa=š&ÒoÃó´÷ € (3y/B_0ÌîpIýÄyh5Ô$Žú2€8~ø:¢›§ç¥êC!.­ÿ¬€õÍ*¹n503Åí§÷í½»›—h3u„,°–Š6´¿"½¼‡»è0O¬°þi–¾©êÎVìÄÅÞÝ *ï&šîBŸŒðnÔL Á¾°Fàù‘ˆ#DÈ¡›Õ+ú+Í…2¦ÓnÈPÐ<Âî†ç>2v'ÉŽ¥kõÄsHà[Žå´09Ê ì0Ñ%G})Åx’0Ä›çMóztžÌÒdý¤I“*? ê;žÐŸhépÐ";t°ù„ÒVz(5ðï7\Cþ9ÝM;_û& ½êõÉðRBŒç®åP¸Éûß©Wn†2gÉŠ˜ï00^0ÚK‘këÐe°Áïá;msÆýºîN S€Ù|(pžž¦Võ='da8³(Üq8ÂÀ Å×¾ñ|=bÓòÖ½Rlð!_Š}Ø0˜ÇúxJ)cá—àÉ8-âLN}©Ý)ÄC:s¥~¡Öf™!/†“ÝLâ€þ* Å=? é—î†A÷.T¥¯€0]Ý-Ù܆jåàPðY‚w° ÁÁÌ]àgþÏp½ËfIõ&.ƒ‚Í0ÿ‚\öYÓÐeÐSf>zãÎ)ð,ÛÎɆ£‡^›Ù£þ ¼éž0§Ãìއï7 ƒ/®~nü>¿0ßçàZÒãr,ö±>Þh~a"LÐY£33í¼yßÔmÓñ9‡«×,6ºœÏ0 nÍ@pñæÂãÞbó53ü*3Þðÿ8ì^áÈߤ]n†Í,\G“ÖË!ë/†; €s6 w}ûbØážçÍï ƒ›Åò|Zønò…A÷"gôR§ó/ð¶Ûf©/ð†Þh=zÎ}òùþì¶| ¥‹$Ô@Š9£dÅ0É€›—µÆ¥þŒŸ„"7)›³ô‰K}ŒØ|aðùI{Æ9™‚À:^æ œÊRKäñU€îæn¥ª½<ïJmFUžÄlî$ÏN8"sÅ‘620Bô–ŒŽ“[¼ëÞ!¾d‡ìUÀ)ñLFk04þ6Î{<ܶŠAŠŽQnd\¦“‰Æ.§,Õ„Ñï[ÄGg³ÑÁ¿h:ÍrޱÚW÷ i¶XèvpDºÎ!K2+Ô§Å?KòÞÎró‹HÔÝÓ›5`ÈX IëÓÒÕAJµÿ´ÊŸuü].7q>–Øf °Rî²ÂQò8Ô€‘‹­Zæ”5€—X©”à9B FîÁ  ÐRªX™äŽê”y­_B°™iYêªKÕ媉vGÕ1ª)Q@{¢8uÙ2-DØ“šŽ7—pÊBß'3îu™›‚®bºÒUÙ&c-áE ÔtJÖ.jfè3çY! ”äשœ£gÅ0/´4“ÚüïÍ @Í8§±Šå %±ÎÊв¬ÿúá‡çv‡Ç$YÿC–»-K‹ÄyÿÛÏ=ý1MÖÐdbõœ aèå;è)½ž1–Ö¾¬[¯\Šº_YìÜÒäéYÞßT–çÖÛ´z–ÍT.žÍ¶õc§‹×(Ó*ë µ}ôZkÔÃÝy¼DÑ5ÁšçÖ])OÇÄkS6ìÇuD•qQi‹hÃc Ç{´r r¡ñÙCçŽ]ô¥I4p}1DWN¹¦à ç»ÛÉI„nŒõÆwqÀìÉŒå°0€ÅÜú"@P€£.8êK-B“ðpQ€o¾?(àE: “õ&M¨ühæú‘þ¬@K—ƒÄrB™O(œ`E †Cÿ“ÅiëŸÓÕ´óµoÂП±>?îĸ)[†Â͹ÿÝ‘zåf,“p–¬8 1—‚ñЂÑî!×ÑÐ¥°Àïð–9ã~w'†)Ál œ¯»…©Õ€G˜ï†4 g… P Gøa4åµ/V|¦[¬`šÞºW” ?äKèч ƒ9ÖßÀ“J)í¿O&Y™äbêKí.!’™+õ+¹6ÒðŒy13éÍ ȯPÜgaH¾v7 ª·y"ÞCwõ°ds²•‹ÂˆÏĸ ƒM0 fî?chö_àõ.›Õ›¸ 6Eü+rÙuC—AM™ùè+g„fñØVN:l=ôØôõÿÐÁ›ê }:ôá¸ù~3|u¥ðKað{S|§)¾ÙÎÁµ¤ã2{¬Ï[€7jïô@˜FdVèLÄA;^7uÙtçpõša£ëÀùÑàVoùÜ_,¾¦òƒ_¥bpá ÿa÷ŽüMÚå&lfp!:­ûe‹p§¡pNgp×·/Š\îûþü®0°–çÝR„&¿&t/rF/uú§Q2¿õo›@/uãÞЭçAˆs†¿ÜŸÝòƒ_¨t’„H§¯(‚$pó²C›Gp@P´Bn€IYŒ¹…ÜP|Æ©~•ä"GxœíYÝã¶ÿU^nQKâ—(ÊYoÐæ$@ó’¦(—B–h[9Y4$zmß_ß¡¾,Y¶wÓæ‚.z6îVš’Ùß|~üæ¸Íѳ*«L ‡xØAªHtšë…óŸ¿s¥ƒ*iœëB-œB;ß<}ùÅãŸ\}[ªØ¨2³A?ª$Þ)ôncÌnîû‡ÃÁËZ¢§Ëµÿ€\†Âàêyýå!X»¨æi²pÚ1»}™×²iâ«\mUa*ŸxÄwòÉY>±dÏ*ÑÛ­.ªzhQ}5”.ÓU/nU:°ZŠDQäcêSê‚„[ Ý‹± çµ±cìo úJ±y–ÝÁ¿^¾#x•Þ—‰ZÁ@åÊøï~ß3]ì¥&ÎÓv´îÈÚE¼UÕ.NTåwôf‚C–šÍ¡¸yݨl½1ç÷çLþª #ŒFDÁÏO­Ô1¤¡déÂÍÊö­]rÞ b/¢Ì“ÆBF #l†(&‘‹‰KÚI»íÎSXõN§™^îÑÅ¿’J>¨Ôë-Ú¯¡Ž;]w•åªæoôVù'•Uºðß«g•ëÅ’¿Ë`&?.Ÿ%0ežåíŠóÓø)×¹§ŽûdÙ©ZUµ\c ûJä7Ì~cV½ÔZx ºŒ«Ö3íâ5`9×åÂùjU:ÎR—©*;ž¨?cžggæÔq7§´¸À7ªMœê@aÂý¨õvá°È“Á„—V¤G"F)S®Uhºxxo=âî‹Ì@ôìŽÓ‘û²´y|R°çú餪>¬Kk>SîÕdì!+`'n uÑ©­H‚1¿%cƒáït‡·Ù6û¨@K2‘±;}çgܶJ:Ê¥ŽËôb`m—}–ªê†eª"޹˥ ð«|Ërw±ÙÜš (ô+VpUºVî6Kw:+ÌË⯒¼·²^þªsWûzX²ˆBbzYºÚjm6/oùUê¯s½Œó±Ä*3•r®Ñ»žŒ\­ÌuNÙà÷k©!qn¯´†ÈOÄãY‚«…9ÙJsú5š×õ%2óx© >þf³=šfÍu©÷»-„b[œÇ”qQY‹XÃc­ê;sÆ:ºXPj(Ò!ñWHºc*ÀW•9”^3ç-¡Ð–e|j´PõjU)3ï8obC:wëæjÞ0‘Ý ‚ô å·º4xëGDEö3ƒ]zö@‘+=\¤rÃtI­éߣ{a-~¹œÍ:ßúFJ6aõ-’.À(F—.4Kϱٗj”¸Zçô© âÆ6T¨>ã ¾…›cÿ;•Λ›°l¼jÅ-€ƒÄ÷p<™ír] ]|†o ßK÷—෠Ô`¶ã"—V ˜'¹”‹B =ÁeÈdtÉëZn@F’áezëO¨-_Škè±ÊÊpŠõOàIH¥œ÷‚oÁ“IV&¹ºô¥u„tâJ{Jë"L˜Çz$¿Ä!ýCJ°@JúÖÝ0¨Þ¥6¶)q9ô"WlîB¶ò°ŒÄ$@jw° ÁáÄ]àgB=ˆ+¼³Ë&AõI\ ›cñ†\ö›º¡ã ¦L|ô‰+g„'ñØUN>l=ì{Ý{4ÿcÞTÏgäBï!ÑÇÏ¥ð÷€Áç¦øNS|³ƒcI ÆëXÇ<¼ÕñÀìjñ­{ þ!=P^ù? »W8òÿÒ.7a3ëqÐi݇lpî\z¡|w{úâØŒ±éYa`,O»¥?\ÜFùëÁåÓº¿vêŸFÉü:ÀÁ ôRcÎýCotž‡}DBä·û³Ÿ~pÃi“$ä@ЧdÆ1$ÉPH†D Í£$¤8ša/$4ŒPxKQSX 8Â3Ò{ñ׋õ¹2âU8t$eý›²•%ìü ˆÍ¯ö÷øûoƒIÜåE-¿xœíZÝã¶ÿAU^nQ‹")Š¢ïhA´/mŠ})d‰¶•“EC¢×öýõêË’%{÷½m7]/îÖš93¿ù î?·™õ(‹2Uù½M¶-™Ç*Ióõ½ý_~t„m•:Ê“(S¹¼·seÿððí7‹?8ŽõçBFZ&Ö!ÕëçüSG;i}Øh½›»îáp@iCDªX»w–ãÀRX\>®¿ýƲ,Ø;/çI|o7kvû"«d“Ø•™ÜÊ\—.Aĵ{òñY>6'He¬¶[•—ÕÒ¼ü®/]$«NÜéàUR$ CS—R$œò”ëèè\¬…sN­¥cx=ÑgŠÍKðìþtò-•j_Är %Ê¥v?þò±c:%:éëi;Øwàí<ÚÊrŲt[z­à&zsoS\?ndºÞèóóc*RÇ{[Øò 眿5RgÄš’&÷6+š§fËy'ˆQHèI".ÂØ#ÞÌ¢˜„&i”¶æÎ›ãßÛZ©lΘIg£Šô³‚He¨ók·“<îT¡UšÉz±»Q[éždZªÜý(e¦vQî.Õ@‰ í¦±Êÿ¥Z¢]~Eß1ÙA´B>Í=µÜÃ^$rUVrµ?Ì#µ-·fvæ™ã%ÆÏ=ÑeT6ñ±¬]´Dgª¸·¿[UŸ–³TE"‹–ǫϧ ä©>Õ©ÜêomwøŠ@¹‰u@Œ¸Ÿ•Ú!©Š?Ôxˆ ϧþ˜i΄žç—BÄ÷&6Î>O5dÓî8V°/ #‘E' Ö¯…¶2åFÖ…q£.ör´òæ`‘ÓŸ„tlx#Ò&Á˜]“1©qwºÁÛFÇt›~–pJ2’1ô¿Š²3®û¤BÊFÆŸd±TQ‘\,¬ü²OY^ñL™G;g¹4é>É7,géÍ5•@®ž±ƒ#“µt¶i²Si®Ÿ–ä­ÕòWë›§¯tÀP£@ÊÔÓÒåV)½yÚäg©e” %V©¨ë4w´ÚõðÔcdr¥§9Eß)ÖRimòw Ð "}<Ô‹³$W³Ê²ôÉôãÉíŽj²ÏP€‰r»3=¨&Ä™ÜÐL&!Ê™5ޏ§infÈK ›¶U¦ËL} È# &—dãýf…9s–æšDvº”ƒ~®ëg|K«2½­ì׌­ÔQé¨Wè[’ßyæùß>þøÐî°ˆãù?Uñ©ÛѲŒH´T{­ýp¦/’xÃ6ÒéJ™6þÂÂ=3†Ò&v=½µæBÖÃÇä–ÄÛÔ¬rÿ®Ó,ûÙlÓÚÝS›êLö¨ ·±¡µÑí¹p['ÔëK@fÑRBZüÅTzk\,×…Úï¶÷vÕ ìž{+B·DQ^G˜ÀÂ× æÕxæÀ,ƒ ꮋÂzˆdÁ‚sÈ{jÀ¥EzüÍÓ§8`^8Ãæ§yô½Ìj!&Ô'œÍ¨€‘‡PìÝ£ÖÛ¨ÝÊ'ƒ ®ªDöC{@‰ 0œ’úDõþŒÈ£Œ 6s`êB>õ»ëo[š²7Pßóv·“KS*M“ó |ûbE©O&›cN¾‡Q,›çpm¨¾9-#z‚$ü¾Ô…ú$çÍ”ƒqC¨[5âfhæy-Ý$*jÈ“>ñW¨µC* Vt\=g--‰ ¿EtªOÕ£ªÕª”zÞàlÄ.‚*îT³Õ¼fZÆ ª*tÝòÒ ­¿ZÔG^h>3°q°ZŽ@¸úé3Ê8¢ý'ËÇ(¨¤­]j3Á7±Â±º¹Håà­ &¤ÇHï 9¨WMpº ycòS Ÿa.OCáêÚßv¤³q#–©““^ìÑ|¸Müw”±Ón!×1Ðe à¾|/Ã_@Ü.S€Û|(p㾸ôð¨ï!Á„yúf€8'ÂK^;i{>T$\–·îšÚðŸB9¬ÆXÿ ‘„RÊX'ø"§EœÉËXšpA qAG¡47´6ÓȈy¬V²«IÐWI(·GAßzzÝ»PÚ %ƒYdÂçT+„EÈG R… nÔ˜à`.ˆ3¡ˆû|‚wÙ(©¾JÈ `3ÌßPȾh:özÊ(F_¹s†x”mçdýÑÃÃ,¸61ñÐãÞdó­f /x•ȇ ¯ø†Ý3ùé—«°Áu€8˜´nCÖŸ„;(àœànn_ #îyÞø®ÐK°–ÇÓRˆï.^B¹ëÞ˧u÷Ú©û6(æÓ €Ç0K q@Úð÷£ÑFì9÷ɗdzSß{±iŠ$Ô@Š9£dÆ0É€ ÏŠ-c%Åá £€Ð ´|,xEñ|Î,<ÃýâÙÆ8ì»ó©‚Ù¹tô6Ì {o°.ÒêŠ7%7?_àM¿ùÆÿ^"õwN’A#iä|Ú>³ÍŽiîÑü£œð)Þ …˜öѼd}T_ú¹’ó }ñ,½·OsÓ³ñ¿£HßðóïÈÊÉ>¿Ï áyüIà,Ò’-PÎ~Fú€u¨3¹F‰+©ý×?¿‘Á¹ÎíuŠê]“¥{y¶ïìÔîd³O3Ùø¼[à©Èõv餛ne±ÙêiþXȧ?«ãÒ!ˆ  BðiÔSMF§¤È—÷³~Ë…í8@¯d,²8"q”¸( õõhè¢ìÐhµ»ë¸¹¹ÊŒK'ÛÊìÝJߪv(ó·Ekk<*xÜR÷ªÖÞº(eÇìoÕNú'Y4ªò_ËGYª½q-_h€¤µö‹LUoËBK¼¯n¬wÌ÷`¶D\ÇžìƒAßçrÝ´tbÌ4pß!GñÌñr£p‹t•6½¡Ú§píRÕKç»uû0+Uç²p¢ýœãؾЧ.,‡õ‡C›…Grƒ Ù¦¹zϘaß+µ[:,Ä$T$3|îÃpÀÃXp1Çž ¦ApÎgX0ùÁÇ;T…†¸Úç êÚP”éI‚øí:P5[õ´©&u}3Þ§¢¡¼>hÌeïI†À „ÌOÙÓ˜0¹…;=ƒÛ¥ÇbW¼—pJ:£1Øú_§åä·µÒ:KõJ¥u~ÁØêåPä²¹¡™¦J÷ÞjeBÿ*Þ ¼}ª··h *õ‚<™o¤·+ò½**ýaòQ>·³Zý!3ýìéÛ5`ÈW@ )ëÃÔÍN)½ý°È/:þ¦T«´<§X\¥Þ•§ÕÞò' Qʵ¾Ž©;ÿ½†Z)­MÏ´u‘çÜ`ŒÏZéT·©tI2Û´ D`ω>™Bu< 3BMˆHñ (w{S´Ú"žÀ=Ì„äÆ‚öt›ƒ¬ò2LkŠU)Ï¥„T)@óK°1QÏaÎ\•„bRž.é(½¨ì´0ÀÚt0T^:ÄNê4Ouj„ŽZ††eñ÷×?> ;ÜgÙâ7U¿wDȤ+uû;ü>ÏÐbìRýPì _˜öäOÐQÜûâœÚØÎZ·[¹–]·rµo˳]a¸üè¢,ÿj¶ä¶–-t)Úm»á(‹ß 3ëÛÒÞûƒ6ºéæÒ;Ët%!ˆþfJš§ÖM­ûÄk_5KÏçeD×iÕ ðLµ|E\º í ïFslÎ]:æÑd{kÐm]_Aµ q–¸Ä|ûiÈ\èòBƒ îBqÄœ„ÝMæ³6¶ é™u7g–jI( ç ‡‚ûʺ„ú¬|€å)TãºNOÝ©,¨Z¯©ã&!ö)ä|¯mÆiä`¨ÑÍ¥ÀZ?¡ Ä,1¤ÄdcÒ~béEnÀZøµÔè÷ËÕŒñmâ˜ÍPc¥*PŠVµÕcªµqåLÈ,‡ÊÉíÖÃÌÛÞ£ûMxW=‘½GŒÞ-…ÿ 7øÚ?ÓßlçàZÒ;ãu_œ|}Þ|¢ø™ˆò$˜º¶ ›×MS6q(\½f¾1ö@`|Nxt«â! ìjñm{ }–(„ oü?ìv/0äÿ¥^nºÍÌ]Ï<:­ç]6¼êî<Æ‘|æîæöÅ Œ±ù]Á °™/Ï»¥„Ü]¼Fùëñi3>;£³d~Ý ÀÀôRç~@óÛÖ,r$B„ôãí9.o½pš$ 90 ‚Ôå’d$b†2D¡Í hÄ%8¢A” 0ĔĢ…°PpD\b'ÏÁƉ­Î%ÌQ¥ç‘sS]}<œÎ/R˜ï…Î.£¤ùç!­å,LZMþ×ÂdP^(ìWí>6Ìk kÛ½ 5„Å5\ÛÄc´ÿyÀ¦h$ý2²,T·Ïßæ~fV± ò"ÿ Í÷†ÿ~ÁæhßÔ8Npw» ý1Ç Ô[3æŽ3tèWD!£ƒáp©ÀH$²˜id­ “7g,ƒÁÐhâ7ã‘ì×iÃY‡ùÙ´,ŒG?¡Ðåh‹(à&8 YA #êš´*¤Sè‰: w A^O`°žùõ…°-Øàˆ›Óõôf GliܑʢçíQ¬7´koÝÿcú·nkj=¤Ï¢‡&!ÿèÜÓ¥ì{ó/ øû/N#îA!xœíYYã¸~_`ÿƒ }™F,Š—(ÉÛî‚Á"’—dƒy d‰¶µ#‹E·íùõ)ê²,Éž^$3È ãFw[Uţ꫋ÔóOç}á¼J]åª\¹aבeª²¼Ü®Ü¿ýò³¹Ne’2K UÊ•[*÷§—ï¿{®^·ßç8 /«e–®Ü1‡¥ïŽº@Joý,õe!÷²4•OñÝ|z•OµLLþ*Sµß«²ª‡–ÕCimzñÓé„N¬–"qû˜ú”z áU—Ò$go4ö97–bŒ}à Dß(¶¬À8øíå;ªÔQ§r%*¥ñßÿò¾gze&Γ—ª49È›u;bc†d/«C’ÊÊïèͧ<3»•Kqó¸“ùvg®Ï¯¹<ý^W.v° !¿~k¥® “†’g+”Ú§vÉåÐ;uÞÉH¤Qˆ£0^8Sâaâ‘`á¤ÇʨýS3ºÓ{™©Ôê±rµÝd™h­N¨7i¿ˆ<”6Þ&/d#îïÔ^ú™WªôßËWY¨ƒu&ÿ $ÚøyªÊ¹‘èPޙXÌs/÷ŲŸ3¹©j¹Æö‘ºŽß0{…ìö2kâè:©ZhçlÁ™ ¥Wî›úÓqÖJgRwsåŒñ$»Êɇ­‡}®{æ/æMõ|u<è="çã·RøßpƒoMñƒ¦øn;Ç’Öç}ñêëÓà3µÀz Âc:)tu$@;6­›¶l¢@8zM|£ï|Žyx¯â1l¶øÖ= ¿HÀ7úv»7ùi—»n3q׃Në±Ë³îÎ# Á'înO_#Á›ž6ñåi·ã§Ñm”¿\>mûk§þÛM2Ÿw8…^êÖHÿyÐ#" ¿Ï~úÁ §M’)œ’Ç$C1'u´y”„Ç ŒBBÃØ Dp$j wð“g‡q<4ç§foÒ‘¹ºÄÙ\W&ÑîØ–¥°?LØ7Žá8Vª-'ÁRÛóíÆ½Õ¾nqƒÁ½I¯FÕ¼?™cÕ-74ÎöúÓ9ëÅ4æ`t8ÎhbßÕ1Z¿YãS{Á-±KDo‰íûQâ0s»˜Å³ÖdA §ãˆ7Eb¬¢“Kð^¢NE–ç˜I™©öå9io boR!U„!qD3{õqZ10‚Í"#×|t?#ô6a0é þ\ X\qX´;bôé-ëœkã1†C*¹ÞçÛlñloÓáÿ¿^”~ÛÐ öxœíYÝoã¸?àþA÷²A%Ф(ŠòÅ9´X®@ûÒ^Qà^ Y¢mÝJ¢ щ½}‡ú²lÉNí.ºèÚH"Í ?fæ7d:¹õ,ë&SåÚ&Û–,•fånmÿãן]a[ŽË4ÎU)×v©ìŸž¾ÿî±yÞ}ÿeY0¼lVi²¶÷ZW+Ï«uŽT½óÒÄ“¹,d© âÙùä,ŸÔ2ÖÙ³LTQ¨²i‡–ÍSé:ÝŽâ///èÅo¥HE¦¥.H¸Í©Ôñѽ û\K1Æð&¢o[5`œ ~Fù€u¨¹…•R{ï}?2]ŒRNçÉÊMWòbÝØ™!.dSʼnl¼ÞMð’¥z¿¶)î^÷2Ûíõùý9“/Rǵ-lˆF„sÎÎO½ÔÙ餣déÚeEÿÖ/¹š¢Që<!aäXSâbâ’À±’C£UñÐô^¥*1z¬í:N3µ9h­ÊÊd/“2E£qÇåä±Rµv·Y.»Þ^Ò;ɬQ¥÷^>Ë\UV^•Á\^\k/K`Ò<ÓUåùŽi.‹ø2÷4pŸ û1•Û¦•ëŒb^©mysTÍl/5Æžˆnâ¦w’eUñ`«zmÿ°m?g£êTÖ·ŸKž¿gúÔ…ä0ÿ°i3ñ(€o4û8U/€Š÷£RÅÚfE‚ˆpÆN9W `a˜S6€U#ÄBù"˜qÁáã÷Pf¢ª:Î'8Ôµ‘Èã“´È ÕìÕË®6¶ÔõAÎÆ¾d%¨åö!@":×¾‚`<ס—1Ar‹wºÃ+âcVd%ì’ÌdŒSlãü ŠÛViáÒ†E½Qq^ lírÈRÙܰLSÆ•»Ù˜À_ä–[Åzk‚V ToXÁ•éNºE–V*+õëâo’¼·²Úü.}w÷í°d+…„õºtS(¥÷¯«ü¦íïrµ‰óK‰m¦*õ.+]­ª ž&Œ\nõ2§îð»ÄÚ(È£Å@[ˆÜƒÁŸµÒ±n;îÒä¶ó4ýHËÒ'S¦Ž'C´Gª QC‰Bv&Ê¢2%«mÄ™ÜÓL¸!ÊY |ŸÎ¸§en ºÊëh0U®É6¹¼Ô6PÆ@M¯ÉÆEý³ç<+%”“üt-§ÀèY9M ­MC ðæE cRÇi¬ãIIHÁhehWV{ÿóÓ°Âc’¬þ©ê㊖eDâ:€ÿí§3ý1MVÐ`±~Ê È¦9ùôÞ™q)m|7™·›¹–]¯²Øµ¥I‘™QÞßu–ç6Ë zO¦Ít.ŸÚe»ÇQ¯WfPÖ›jûè Öè^w×èÌã„ ú‹) Ö<µîju¨ ˆ×¾jØ;_–]Çec,b< y¬å;ì¸Ð!h^üàatÇîÒ‚…gßO¦ÛÖÙñTÆ€âù‘ƒÍ· |Šf„  n:TPÄÅþÃÙ}“…†¥ráÝÝ…§Z‚ƒÈ¾ Ï·DP NI·£îˆÀ!!ò)c‚9®O|PŸ°‡é‚°¤I’ÓO¬=®ä&Ò$VS}DÂÀ¾Ñ蓉ľ'Y‘¡{ËW%œÚ'w``Ä g !‰~lt­>ÈUßaܺ‚8„Þ›ùþ@7 ›ZÊtJü2ó%à+ëê³^±–ÆPë:>u»šPÕvÛH½7pV¢Š!ç»m;¶ê˜–ÑÆ‚ 5º¹6xë¯ ™Z":PË·!ÝСÐxÑ–þ‹`¶ÒÖo׳çßáÏXc¥J0ŠVµ Õs¬µ¼H\½sÆTqcÊXŸË ^†ÂͱÿÙ–ÎÊÍX&a.ZqB ãá£ÝC®k Ë`‚oðmá{íþüve˜Ì@‚óĵՀG &ÄÌ¢P@CÄ™}]ó†¾Ü #‰ð:½ÇÛž/øzÌfáú%< ©”±Qðkðd’ÕI.¯}iÜ!ĹҜè†H#3æ±ÉnqH¿H@q?‚~ín˜Tï¶—ï\<,ÙÜ…l…°ˆø,@Zw° ÁáÌ]àgBøïì²YP}—AÂf˜E.û¤nè8©)3}æÊáY<•“M[óÞöÝo̺êùl¹Ð{ëã·Røß€Á·¦øNS|³ƒcIÆe,ž±>o>S |§",¢³B×ö@´óçuÓ”MpG¯6ÆœÏ0 oõ@,@<ò¹¿X|ÛÈ¿HÀWüÃî Žü¿´ËMØÌàz8è´îC6X„;(äœÍànN_ #îûþü¬0 °–çÝR„®n£¼Ýäòi7^;OÉ|ààz©KÁýSo ž="Îòéþ§ŸÜpš$ 9bÎ(q†$rá[‰E Í£$¤8r0 #+Á‚·?àÌž&ÏÁÇÑÔœ¯%ÌѤo2—äæûfsQØ0”_C ´×8¦¹ŽBh¢a BëG$ú0ôñÉÂí0.„Cð±ó´Ä°aäžthþ Fá['=ïõ7Ëèà0+¶d*/añ©fq‡ßắ²Ý{ü–=AåÒEêp×ß_¤N^'·´¸n/”2®»tðh®Ëáï¿ÐÝq¥ *7xœíZÝã¶ÿAu^nQ‹")R¢ïhA´/mŠ})d‰¶•“EA¢×öýõêË”eïmšÝ´Û;/îÖš93¿ù ½ËÜy”U©â~Fž9²HTš›ûÙ?~úÞ3§Öq‘ƹ*äý¬P³ï¾þjù×uþ\ÉXËÔ9dzëüX|¨“¸”λ­ÖåÂó‡Ê:"RÕÆ»s\–ÂâúqóõWŽãÀÞE½H“ûY·¦ÜWy#›&žÌåNºö"ÞÌ’OÎò‰9Aö(µÛ©¢n–õ7¶t•®qs¤ƒßH‘(Šne¶Ùêóóc&RÇûv°ÃHìü®“:#†´”,½Ÿ±¢{ê¶\ ‚Ež4D”øÄŸ;“ÈÅÄ%ÒÞÜEªs|P˜}”›*+ÑàÆA±<–ªÒî:Ëe+ëmÕNz'™ÕªðÞËG™«ÒÈ+3 ”¸Ò^–¨âßy¦%*‹úŽi Á‰‚ëÜSÏ}0ìe*×u#ךoéÌñZæ`9^jÜj‰®âº ‡ã”ñœ«ê~öͺyõœ•ªRYõ¼ yy "œéS›¹½þþÐFñ €oÔÛ8Uˆÿ„ûQ©(&ŒÐÁà¦lÊ„½"ˆ0Œøt)Dvo‚âî‹LC֔ǩ‚}U‰<>I0»ùEz©z«ð ®ör²ö`ŒÛAœDtjs'ÒÞ`<µ¡“1Ip‹wz‚·‹Ù §$cí÷uœŸ¡pÛ+ H¶2ù «•Š«ôbaã—}–Êú†gê".ÝÕÊ$öU¾a¹e¬··4…zÆ®L7ÒÝei©²BZüY’Oí¬V?ËD?yúFìÕD¡ }ZºÞ)¥·Ÿ6ùYÇßäjçc‰u¦*Õ&+\­J O#—k}Sµø½ÆZ)­MêNÚ@ĆÁC+±Áç$D>eL°¹ óâÔ'ìÎÞ¶4eo¤Þòö°“›HS*M“ó ùìbE­O&»ÙbA¾…),_pAhÞ¹=#ù‚†$ú¶Ö•ú Ý€ƒqGh[5â¦eæû=Ý$*j(R›ø3ÔÚ1P+«:®^°ž–ÆÐ_«*>µ§²¨j½®¥^ 8QÆPÅÝf¬Z´LÇXã@U…®[_:¢õW‡räGæ5+Q6PÇ7/!ÝpNY€hCÿÁá…´ó¯Km&ø&6BøÖ0©œ¢UåÂŒôë}%Gõª ÎP¡ oL>CcJà5ÎåëP¸¹ö·élÜ„eêäU/Z4÷†à62uÚSÈu t(øß¾—ᯠnŽ©Àm œÏ..½<Ê}$˜Bß QÀDè‹è’×OÚ>‡Š$ÂËò6\H;¾®¡ÇV„S¬¿B$¡”26¾…H&Y•äò2–&\B “Pš;ZŸidÂ<6+ÙÍ$éï’PÏ… o= V÷®”6C‰Ë`¹âsªÂ" & Ò„‹€O'á‚8ЏŒOyçM’êUB›áà …ìWMCG«§LbôÊ3“|ì;'³GóÜÌíÿ˜µÝóÑqaöÎÇ/­ð%`ðe(~b(¾9ÎÁµ¤ãu,ž±>^i~b",¢“F×Ì@XçOû¦i›ˆ\½&Øf >Ã,¼51Ž‚Èü«Í·™üðw™8\xÅÿ0ìžÈÏÒ/7a3ëq0i= Y~îL 0ØîæöÅ0 |ߟÞ¬›`y:-EøîâC(oc}ø´>vÞŠùu@€˜¥Æ8 }øíhô‘;¢ àä×ÇsPo}°iŠ$Ô@ŠFÉœa(’a |'qŒy”„GsŒBBÃÈá,‚†âó€9xŽíâÙÇ8²Ýù©‚9¸tœ97Ü%óólwˆ®ð)Çü¿—)Sÿû³åý®œöך6ð[ÌÒÕ|ñE}󲨦+DEŒâ–gG2¶¾ÖYðzÞ ž8øÙP«þEô³Ä¬x ÌÂý°ùrÅÿ0KM‰·ï¤×sà·b–á/˜}“^ å‹ÔY‚§xT´;̘ŸùçéYöžõó#.®t°'=ÛÈ0Ayýj𥃽M ¯„òE:Ø-ÌúpãÆŒ~–žå/RgoÍ}¦ù`.XKówðû{Há² mxœíYKã6¾ÈÐ*—i¬E‘)QŽ»ì‚ؽ$Y,°—@–h[Y$ºmϯOQ/ëåž’ 2Øq£»¥ª"YU_½$o¾¹3ëY–UªòG› l[2U’æûGû??}ëÛªt”'Q¦rùhçÊþæéË/6s럥Œ´L¬sªÖ÷ù»*Ž i½9h]¬]÷|>£´%"UîÝËq`),®ž÷_~aYœWë$~´Û5Å©ÌjÙ$ve&2וKqí||“鳌Õñ¨òª^šW_ ¥Ëd׋•Î^-EÂ0t1u)u@©®¹Ž.Îd-è¹´–bŒ]à D_)¶®À³üöòUêTÆr %Ê¥vßþô¶g:%:îÓ9vtîÈÛyt”UŲr;z³Á9MôáѦ¸¹=ÈtзûçTžÿ¡.6¶°Å ‰ïûìvÕJÝ"†4”4y´ÁXÑÞµG®{AŒBŠ`Ÿ$òE{Ä[Y“ÐÁÄ!í¦¹ëDÅFýG[GÛŸãLUõ~ìw–—B•ÚÙ¥™l„݃:J÷*ÓJåî[ù,3U˜r‹T%*µ›Æ*ÿ9KµDE~g¿KR:¡¿Ì½vÜ'ÃÞ$rWÕrýæ–Ú–Û0{sŒz‰ñë@tU-–UD{ˆàL•öW»úÓq¶ªLdÙñüú3æ)€8Õ×&u»ý;¥Íƽ¾#P¢D!fÜ÷JM`ôóÄŒC”àb*(ŸsáL {<äs.@|2à8§<Õ>Åe¾Á©,D]%˜_ÿ#TuPç}i<©Ë“œ­=§9å´±NB:·½éâŸ`2ײ•1Ùpw}wŒ.é1}/AK2“1 ý¿‹²[HÜ÷J,¿“åVEe2YXûå”&²š0nçæQál·&Å]gXNéCõ’@®ê#ú™ì¥sL“B¥¹þ°ø«$_:Ym‘±~Qûz8êˆBiú°tuTJ>lò«Ôßgjec‰]ª!VÊ}š;Zƒ€02¹ÓËœ² à%ÖVimrx¡uŒ Ãà©‘ØÜ$ »ÚU–¥¯¦×\®†h÷T“~†åáF”ÇÂôz€7rK3©„¨Ï¸ð<:ã^—¹ ˜!§‘nZU•n39ö%(G@M¦dãýv…Ñ9Ks "»Nåø3͇)ßÑêT請;/ï ã(u”D:ûŽÄ{/Ã̱þáí·OÝ ›8^ÿW•ïú-ˈD[uhí§}“Äk˜Ž‘~JP Ì„ñw 6î1–6Ø ömv.e3p,Ž^I|LÍ*÷GfÙ÷æ˜ÎîÁ¶©Î䀺q[:Ý¡‘·sBs»Ÿdm%¤Å¿L•·æÕr_ªSq„ l=pï¸3è2Ê+ã,\f0£¾Á+æ`áñ‡…ý8’ n¶—–éå 4PN1tÂp…ÍO{˽Ìg!&”Ÿ­ "F(ön¨ êŽâdê~P-B0íy®Aa(|Jš;"øŠÈ£Œ ¶r`ÒBœz„= „#MÙm?ðv’KS*M—ó ¸=YQé«IÀvÈX“¯aËÖ9<*ÔWNÇÀˆ…ž  ¿®t©ÞÉu;é`Üš^ ‚8€¹™y^G7‰ J­!òdHüjí˜ Q+Ë Z®^³Ž–DÐ`Ë2º6Z ¨j·«¤^÷ ÜŒ("¨âN=_­¦e¬± ªBÛ­¦N´þmQ޼Ð|V`%òÁj9áú#¤¬(ó­éßY£ –¶þ7ÝÍ€o°›±úÑHåà­J†¤çHŸJ9ªW-8}…‚¼1ù )†Ï8——CáîÚß§Ò͸ËÔÉE/hž ¾ƒç’¹Ó^Š\Ç„.ƒ >‡o¾SøKÀmâ˜ÜÆ¡ÀyÌçbê5àQî!Á„˜yúf€|&O„S^7j{*’¦å­4mùÂ_Š£¬æ±þ„RÊX/ø) §eœÉ)–.H!_Дæ!­Ë42c^ê•ìnôOI(ßãBÐO†A÷.•6C‰Ã`Yð¹Õ aú³©á‚GnLp0ƒ p&ùÜ_àÝ ›%ÕG 6Ãþ'Ùoš†.ƒž2Ãè#wÎÏò±ëœl8z˜ûzöhþbÖtÏgËÙCXï?·Â?" >Å/ ÅwÇ9x,iƒq9o±>>Òü DXHg®ž|°Î›÷MÓ6÷}xôšÅF?ø ³àÞ Ä8òCÏ÷›o=yÁŸ2qóê÷/v¯òÿÒ/wÃf®£ˆƒIëåå‹áÎ |ŸÍÂÝ<}1Œ|ÏóæÏ ƒ›Åò|Z ñÃä%”»¼|Ú÷¯ú«Q1_8†Yj¤ƒˆF‡<Øú>'¿Ï~ûÁ‹MS$¹ù Åg”¬†"øÂ³b‹˜oVH@q¸Â( 4-ÎÁ¯)÷™…WxX<;ŒÃ¡;?T0{—.º«q’ôÍOŸ£ÛÞ1¼ý‚pšÛ“Ö³”¨½öǹ£PàSÂW4„X„”Yò¨y;†ùª¹ Ç®ªçaêÿnW½º%õ‡2:ÑŸ!¢0´WŸ[N«4ôÌVÿªuxú·ä&7æ5üÿißcH¶ [xœíY[ã¶~ÿ (/;ˆE‘EQÎx‹ š—6E¾²DÛÊÊ¢ Ñc{}u³nöNÐnÚEWƒÝ‘Î9¼œó9Ï?^Ž™õ*Ë*UùÆ&Û–Ìc•¤ù~cÿí—Ÿa[•Žò$ÊT.7v®ì_¾ýæ¹zÝûeY0<¯ÖI¼±Zk×-Ne†T¹w“Ø•™<Ê\W.AĵòñM>.e¤ÓW«ãQåU=4¯¾J—É®?ŸÏèìÕR$ CS—R$œêšëèâLÆÂ>—ÆRŒ± ¼èÅÖ§€½|G@•:•±ÜÁ@‰r©Ý÷¿¼ï™F‰N†ó¤ù‡*Ž 9Z·#6fˆŽ²*¢XVnGo&8§‰>llŠ›ÏƒL÷}û~Måùê²±±…-ÑpÎÙí­•ºNJšllPV´_í’ë¡w b½ó¶1•‚x;eQL±ƒC++>UZŸšÑÞëDÅFýš¥¹D½1ûéå¥P¥vvi&A÷ ŽÒ½Ê´R¹û^¾ÊLÆÜ"Õ@‰Jí¦±Êÿ™¥Z¢"¿3ß%)¢/s¯÷ŰŸ¹«j¹Ææ“Ú–Û0{UÌöcÜè6ªZP,«ˆöàÆ™*7öw»úé8[U&²ìx¼~Æ<8§úÚ„`7·i3q/€ïT‡(Qgð‚÷£RÇí „yèQ6ãÇà*„#Â(„Ôœk6Å„úÁŒ ðž 8Î)O5ÄPq™Op*K#‘EW êï…×/RÔy_;êò$g#Ïi*9­»“Î5oEº ÏõkeL@Üã]ðŽÑ%=¦%ì’ÌdŒCëï¢ìæ÷mR»ÊAÆd¹UQ™LÖv9¥‰¬îX¦Ê£ÂÙnM/ò Ë)"}¸7A-«7¬àÈd/cš*Íõ§Åß$ùheµýUÆúáîë9` ÈL ÉéÓÒÕQ)}ø´ÊoÚþ>SÛ(KìR ®RîÓÜѪøÓ€‘É^æ”ÿ.±¶JkÀs­]ä‘ôÑY*é:‰ã&åA^»MØŽ´,}5%ér5D»§š5”0`7¢<¦<Õ­‚¸‘[š 7D9ó…çÑ÷ºÌM@W9SѪt›É±–°<j2%ˆÚfϦî@)É®S9FOóaZèhu:èò¿;/ ã(u”D:”ƒŽä÷V†Ödý—÷?½t+<ÇñúïªüЯhYF$Úªào¿ÜèÏI¼†fâé—ôùÂ4"ßCïðìÞciƒÝ`ÞfæR6}Éb‡–ÄÇÔŒrÿªÓ,û“Y¦Ó{0mª3ùR/Û¼öº¸­2²îPÛg·³Fó¹Ÿzgm%ÑŸM]°æ©u_ªSq„xÝØué°v® ý]Fye,b†×,Òò^9Ðï  ÏêáØ]Z°à†ý`°m™^ÞA­õ)˜®°ùi?}oý\ˆ¡"ÎVTPÄÅÞÓ ¾ÁBÝR>¡»!U‹ì‡öˆ<ßAa(8TêzGÍþŠj;låxÄC>õ{.Kš$9š~`í~%'–&±š’è!øödD¥¯&Û~dM~€Î-[çp:¨ßœŽ =AþPéR}ë¶)¸%4…q}6ó¼Žn"6µÈ“!ñWÈÌc*¸¯,3¨ÏzÍ:ZA5.ËèÚìj@U»]%õºßÀM‰"‚œïԭغaZF r0Ôèjj@ëg‹úÈ Í³-¨å@—U?B:ÁŠ2ŽhMÿ£åcÔÒÖ?¦³ð 6Bx3VßE©Œ¢Ué@?õéS)G‰«§OU7&°¡ŒÅðŒƒzÙîŽý÷¶tSnÆ2 sÑŠš/í‘ç:ÆuLðÕ}k÷Â_nÔ`6œÇ¸/¦Võ=$˜3‹B g"ðD8åu}¹çCFÁ4½õGÙ–/ø’÷˜ÍŠ`îëŸIH¥Œõ‚_’qZÆ™œbià‚â‚Π4ç¹.ÒÈŒy©G²»AÐß% ¸ç A¿tÕ»nÀå;‡ùOK6w [Á;䳩á"`‚ƒ\€3¡ˆû|wƒlTŸ2HØ ó/²ßÔ ]5e†Ñg®œ!žÅcW9Ù°õ0ßuïÑüYS=_-za}üZ ÿnðµ)~ÐßmçàXÒ:ã²/Þ|}Þ|¦øADXHg…®î8hçÍë¦)›ÈçŽ^3ßè{ ŸaÜë˜xèqo±øÖ=ü.=^ñ?ìvoòÿÒ.wÝfæ®#ƒNë±Ëú‹îÎ 8g3w7§/†÷"XðšâùœYx…‡É³Ã8šóS ³7éð6ìñ¥ÜðÖÍ ºxóæ°ÁÁƒ†dåø 1ÎXÈßy£¤¸8+8«²Ð+Ÿ A„ãè~kžîà/¹ùy3þï¿÷ã8¹Eõ¤N´1á =ü3o÷t!ÝýxšÑPÚêîv2Д½÷Á¹xóøk~?›{jøý/ËI$ § image/svg+xml Õ%KxœíY[ÛÆ~ÿÀ*/^TÎá²»Z#H€ö%IQ /EŽ$Ƈ G+É¿>gx)R»k¤vëÔZØ+žsæv¾ï\†{ÿÝiŸ9Oª¬R?, Gå±NÒ|û°øÇ/ß»ráT&Ê“(Ó¹zXäzñÝã×_ÝÿÉu¿–*2*qŽ©Ù9?æïª8*”ófgL±ò¼ãñˆÒVˆt¹õî×…¡0¸zÚ~ý•ã8°v^­’øaÑŽ)eVÛ&±§2µW¹©<‚ˆ·ØÇûØî }R±Þïu^ÕCóꛡu™lzs»¥#«­H†¦¥.X¸Õ97Ñɽ ûœK1Æè¦¯4[UàÙþõöUúPÆjÊ•ñÞþò¶Wº%&ÎÓ9v´îÈÛy´WUŪò:y3Á1MÌîaAqó¸Sévg.ÏO©:þEŸØÁŽhH„üò­µº0†4’4yXÀaeûÔ.¹ê 1 )‚y’HÈ0f„-ŠIèbâ’vÒDÇvû0a‘¥Æ¨ÒÝé2}¯£ õí×P§B—ÆÝ¤™j†y;½WÞY¥•ν·êIeº°\òŠÔ€$*—Æ:ÿ7L®P‘ߘS(æµçNûhÕ÷‰ÚTµ]ã ûHŽ×(ûƒÙí%ÖÃÓuTµÈ8NmË™.ßlêO§Yë2Qe§õg¬ÓvjÎMwów›¶÷ø†Aµ‹}*L´ïµÞƒœ#‰©ʉ>¾Ä0e˜ÌhaM‰BJN&JÀú`±qyj ŽŠÓtü¡,­Eœ¾þÕOTíôq[ZGšò &cigr[Ò“NÞšt@0æ·llXÜÒŸÑí£SºOß+ØåÔöC÷o¢ìˆÛ^©¹²Sñ;U®uT&Wk¿ÒDU7M ææeóWY>·²^ÿªbóìîë9` ÈO` )êeëj¯µÙ½|äWm›éu”-6©ª”Û4w.|(2µ1óš²áïœj­±<%hM‘! ‹û‹W;ÊqÌÙÖœÓÙ ½ÔFŸ•„¿Õ¾°õ§n$äEÜÊl$!*¸/£íy^›À1Ô5ÑmɪÒu¦Æ¾„ äH“k±õ~;Âî9Kse";_ÛAÅ× #¾“Õ‘ÞåvošÜÅ^™(‰L4HõÈï½ ½Çê§·ß?v+ÜÇñꟺ|ׯè8Ö$Zë@»x¼Èï“xÝÂ>2éRí4þ ÍÁ½wQŒ­-vƒy›™KÕ4³-XïS;ÊûÙ¤Yö£]¦;÷`ÚÔdj ½÷Ú3tgô†‡¼÷:'4ÛkBfÑZAXüÍ&ygš,·¥>{ˆÀ¶,îSFyea…¯ôªoðÒ…>X2ÿ®Ga;f²äÁòÁ4àÒ2=½òéSp.±ýi}¶„>-Ä„úDð%•ÐîŠÙݵÁBÝR>ºT›쇋‘xº%‚ÂP Jš5ODúK F9—|éBÇ…|Ê¿.KÚ´7š~àí~%7V6UÚ"Ç üÅՈʜm¶-ÆŠ| ÍX¶ÊáÊPs;F!8˜À8Š„/ftÈ&AõQ ƒ„ͱøŒ û nè4¨)Œ>rå ñ$»Êɇ­‡}®{æÌ›êùä¸Ð{Hçý—RøŸ Á—¦ø™¦øf;×’–Œó\¼p}Ú|¤ø™ˆðN ]Ý 8›ÖM[6‘/\½&Üè{ ŸcÜê¸DÈ›-¾uÄ‚OÒùpá•ÿô{ÿ—~¹I› ]GŒƒNëyÊú³tçBð Ýíí‹c$cӻ À&\žvK!¾»z åm/Ÿ¶ýk§þÛ(™Ï“Ž¡—ó€tðÑè‡s„BøäÃñ짼شIr Å‚S²ä’d $sb‡@›GI@q¸Ä( 4ßGKQK˜/¸ƒ—x˜<;ŒÃ¡;_J˜½K'oÃX8xƒuV6ý…… Go)êÔÇ$¡|ÜuÂp³þ=qÄÀyeÏ\:"É <•°?€g·ä/”‡¬¾öó蔳†¯¦Áó ºÅ»š§öµ›ö<ãçÚ 2ýT|fô Ÿ?ãSÎBúzü>7/^¤3C˜‘ñ+ŸJgñG§s×Z4¿ïí_Þà÷ocB×ãåxœíYëã¶ÿ ÿƒ |¹E,ФHŠrÖ 8-Ð|iSè—@–h[9Y4$z×¾¿¾C½¬—÷6h/è¡§ÅíJ3ÃÇÌüæAÞã—cî<«²Êt±q ®£ŠD§Y±ß¸ÿøå'OºNeâ"s]¨[h÷ǧo¿y¬ž÷ß~ã8 /ªušl܃1§µïŸÎeŽt¹÷ÓÄW¹:ªÂT>AÄwòÉM>)Ul²g•èãQU=´¨¾J—é®yyA/A-E¢(ò1õ)õ@«®…‰/Þd,ìsi,ÅûÀˆ¾Ql]qNð¯—ï¨Òç2Q;¨P¡Œÿþ—÷=ÓÃ(5épž¬øP%ñIÖíˆ⣪Nq¢*¿£7¼d©9l\Š›ÏƒÊösû~ÎÔËŸôeãb;ш!Øí­•º94”,ݸ ¬l¿Ú%×Ct ê¼SR$2Ä2ŒVÅ”x˜x„¯œä\}|hFwz¯SX=6nrPɇ<«Ì¯YSÔÛµ_I]Nº4Þ.ËU3Æ?è£ò¯*«tá¿WÏ*×'‹(ÿ” Ä¥ñ³D¿æ™QèTܙÀ[‘Xæ^;î“e?¦jWÕr=ì'u¿aöZÙí¥ÖÎÑm\µþqœS¼DçºÜ¸ßíê§ãlu™ª²ã‰úó4¸<3×&»ù»MÛ‰{|G :Ä©~@̸µ>ÂÄ‘ Ä”Ïø‰E Š$‚Áœmw%ŒxÊ\}¶ÞñÎEf žN—ùç²´y|U ÿpÔÉTý²/­!MyV³‘/Y:y-ôID窷"]8ŒÙ=«æ=ÞõÞ1¾dÇ죂]’™ŒÕ`hþ]œßqß&5Vlp¨r«ã2 ¬írÎRUݱLUÄ'o»µ¿È·,ï›Ã½ jB¿aO¥{å³ô¤³Â|ZüM’¯­¬·¿©Ä¼ºûzX²ˆB¢ú´tuÔÚ>­ò›¶¿Ïõ6ÎÇ»ÌTÊ}VxFŸx0rµ3Ëœ²Áïk«±<h ‘×`ÐGg©Mlꄎ›œ‰í6 D`;ÒqÌÕ–§ËÕÝžjCÔR¢Ýˆêx²¥ªnäÜÒê¬Bã2èŒ{]榠«šFƒ­nU¶ÍÕXKØ@5’­‹ÚvÏyV(¨%ùu*§ÁèY1L ­N]ðç a•‰ÓØÄƒzБxoehSÖ{ÿÓS·Âc’¬ÿ©ËýŠŽcEâ­>ƒÿݧý1MÖÐXcó”!_ئä{è#ýc,m}7˜·™¹TM²Ø­¥É1³£ü¿›,Ïÿb—éôL›™\=ÕË6¯½.~«L§¬?ÔöÑï¬Ñ|î§èÌã­‚ ú«­ Î<µîK}>!^7n]:ÜkB?Ä”qQY‹XÃkõ¯<è}4-èݱCZ²ðæûÁ4`Û2»¼ƒbË)Y­°ýi?y°‚Þ.„r"ØŠJŠ¡8x¸¹o°P·'#ïîGžªEæ‘;"Ï·DPIAI³£æ‹H¾"! (c’­¼€ˆÓ€°‡á‚°¤M’£éÖîWòe«-‰"!w'#*sµ‘Ø6$kò´nùº€“Býæu ŒXH’è‡Ê”úƒZ·]Æ-¡)ì ˆCè¹Ytt±°©5 H‡Äß 3©_UæPŸÍšu´4†j\–ñµÙÕ€ªw»J™u¿›§r¾W÷bë†éXmÈÁP£«©À[?;”£ ²Ï ´Dt Ž'®©¼pE™@´¦ÿÙá…µ´ó¯élÖùÖ7R3VßEéŒbtéA?õ›s©F‰«uNŸª nl`CKàõ2îŽý϶tSnƲ sÑŠG Œ‡Œör= ]|…o ß©ûKðÛÄ0%˜C‚ ˜àrj5àQ ɤœY hˆ“a £)¯ëËI†ÓôÖk[¾Kè±›>§'!•2Ö ~ žL²2ÉÕÔ—Ö]BBÒ™+íy®‹42c^ê‘ìn‡ô (p)é—î†Aõ®põÎcüaÉæd+„e$fR»‹€Mgî?Š ¼›ËfAõY\ ›añ¹ìwuC—AM™ùè3WÎÏⱫœlØzØïº÷h~cÖTÏgǃÞC:¿–Âÿ ¾6ů4ÅwÛ98–´`\Æâ ëóà3µÀ¯ô@„EtVèêH€vÁ¼nÚ²‰¸pôša£ïÀù ³ð^Ä8Q ‚Åâ[÷@Aø‡ô@¼òvopäÿ¥]îÂf×â Óz²|îL¢P6ƒ»=}1ŒDó³Â ÀfXžwK~˜ÜFùûÁåÓ¾¿vêßFÉ|ààz©1Hçþ¡7:σ‘œü~öÓn8m’„H±`”¬†$ 8‰C Í£$¤8ZaFçˆ`)jJÀsð “gçãhhÎO%ÌÞ¤ƒ;¨ÖV…Xï8¯²—ÃSáÒUYw›Û^•õŸž€Î–³ú¦,ˆPøzý¦l|Ö[¼Ýó`")˜ˆ$_y’2! G^óÊ^ßÚdE!šá˜ ö¥Î)&öþŒÈ ë©Ã•ý/3NÂ(¨Ç PêäŽl<Ùn±ׂDdÕ½L+\ß3áM.yF¦ßqû3E œã/ÃX˜£¿ùûho‰áï¿­ #]"éxœíÛŽ£Èõ}¥ýļL+êFQxÛ½R4Z%Rò’l)/# e›L9PnÛóõ9U\ vOo’™d´ƒÕÝp.uêÜOá~üñ¼/œgYÕ¹*W.ö‘ëÈ2UY^nWîß~þÉ®Së¤Ì’B•rå–Êýñéûïëçí÷ß9Žìe½ÌÒ•»Óú° ‚ñ*|Umƒ, d!÷²Ôu€}¸#út O+™èüY¦j¿WemYËú͘ºÊ6=ùétòOÔRá8ŽDB< ðêK©“³7á…}Þâ%¡p#ÒW’-k0Î~zúà×êX¥rŒÒ/¥Þýü®GzÈÏt6^'/?ÔirWr;`c†d/ëC’Ê:èàͧ<Ó»•KPó¸“ùv§‡çç\ž~¯Î+9È }cÎ9îZªÁé¸äÙÊeEûÔŠ\Ž£Ã'Î[)x*"$¢xáD°‡°‡Ã…“k­ö w§÷2S©Ñcå¦;™~X«ó{{#³÷y +k¿7o/PžªÒÞ&/dÃìÔ^™×ª ÞÉgY¨ƒ ¬àk€$•òT•ï‹\KÿPÞYïœÀi1¿½tØ'ƒ~Ì䦶tYÌ#q AöÊ™íeÆÜ#ÒuR·nrœC²…À.TµrßlìÕaÖªÊdÕḽ®q <ŸëK“”ÝúݦÍÂ=ºCPï’L .fØJíW.C~3ñ>…à‰}E( ñ 2#Ÿ S>_~4ÎñŽe®!«çùǪ2Er‘ ¾ýÓ‹©wê´­Œ%uu”3ÞS^‚R^›8&sÝ[’.-0BìI’{¸Ë ¸}rÎ÷ùG »œÛÇh0¶ÿ&)†¸o,67ªµJªlÂhírÌ3YOƒÜ29xëµÉü›¦3(ïè]ýA©¬ˆ›½Of[éíóì òRšüU”/IVë_dª_ܽ]d@¹R¨XŸ¦®÷JéݧU~Õö·…Z'Å5Å&×+Õ6/=­£€! ¹Ñ·1UÀ·Pk¥µÉáy„Úy) ú­”N´­ì¨©zPÚ†e [NÇÑÓ§Ît{¨ÉQ‰#6åþ`z–Äna&ß|ÂY((%3ìå66]å4L›«óu!¯µ„ ” @³)ظ¨å0{.òRB7).S:FÏËq]è`¶t- ˜÷€±—:ÉŒ:B {+ü²üË»Ÿž: iºü»ª>ôÇ$kuÿ»Oü1K—0aìý”ï¡`˜éäw0P<âšÚøn´n³r%›a忨–¥ûÜpÕyQüшéô-›ëB>Y±Ím¯KÐ*Ó)Œµ} :k4ÛitÉZBýÉôg^[·•:ö¯mÛpGv¾î#ºJÊÚXÄxn‹DË·háÁäÃôBÇÞÛë,|?Zl[åç·ÐnC‚"Fã2Ÿö1¤ òb„Iˆ9[A|† ¢ƒûF‚:QÐ^džÝ^yÊ’`Æîx¾%ìDZà7;jž°8ò)aL°…G1õCB1{ ‘¦H^-?²v/ÉK¥)¬¦'RG¡;á¨õÅdb;’,ñ0¼ËŽ öÎëÈg1$Âñµ®Ô¹lç"„Z@ÓÙE0|3J;¸ÉXØÔ ÌÆÀ_ 2_C!|eU@ƒÖKÖÁ²ÚqU%—fW#¨Úlj©—ý% Ô|ÏNcËém¨ÁФë©À[vHèÓØ\ ÐÒç q<á#{ éE ¸O,üNˆüÈR;ÿ˜®fœo|#¡úAJ•`­*FªçD+yU¸Zçô¥ òÆ$6´±®ë¤¾ wyÿ³- ÊÍP¦`Þ´âú Œ‡ní¥ÈõLè2Xà[øÚ𺿿M SÙB(p”ñPL­8R_0!f=›#‡Ãñ× æ4„Š$¢iyëÏ·-^ð[Ñc6 §Ð/áI(¥Œõ„_ƒ'Ó¼J 9õ¥q¤dæJs¤ë2 ÏgËÉî&qD¾HBq A¾v7Œº·Àå[…·lîAµò‘ˆù,A¬»0Ø£hæ.ð3&>ù Üà²YR}—AÁfˆE.ûUÓÐyÔSf>úÌ3F³|ì:'æÙÎÍoÄšîùìx0{çã·VøßƒoCñ CñÝqŽ%m0ÞŽÅ!Öç#Àg_˜0‹É¬ÑÙˆƒvtÞ7MÛôCÎáè5‹~ç3Ä¢{3 }SNo6_;Ñè‹Ì@!xÅÿqؽ‘¿I»Ü ›Y¸^ELZ/‡lx3Ü™ð#ÎÙ,ÜÍé‹!ŸSJçg…Q‚Íby>-Åèaò6*ØŽ^>mû×NýÝU1¿ààf©ë8ÀûÇÞè<zÄœ‡ø×û³_~ô†ÓI¨qFð‚!(’ÔI cÁAñù&Qì„¡‘àBCδ@ãâÙù8›óS³7éëÌšÏs½‘Ü|&›æHýÏcRÉY’X;þÏ’Ä:û”`² 1pD1Gxà‡˜ðˆ€±áN¦˜|@hÈøfo¡±Á ÅØÍ‹BÃ/œ–÷tàÀ)+sv#ÖF0Á¡yÕØˆCNíFR»m;GGH‡ ÎV„3à¢}ßîü‘·;Á *^³“æE=b,ÌMÜ*Ú(6Ú[V#ÔÒ÷¼ £i£8nìn9YcwKÂÝ2× †¿5@'¹]À¾‘ìu;œ²Æ·²Ç!ûwÒèªýÆÒ¨³ƒÐîßÀ æëîÑkÍþ?,*£ºu6_€‡Âž±¨»b ‘ Þ ÕõW4¦<š/Hàï¿ûˆ#±xœíYYÛÈ~_`ÿ!¿x±ÙÍ>Ø”G^ 1 yÉ:—€"[×[ [3’ƒýï[ÍûfƉ½ˆK𘬪>ªê«£[÷?œ™ó Š2ÕùzA^8*u’æ»õâïïtåÂ)M”'Q¦sµ^äzñÃÛï¿»/vßç8 ÏËU¯{cŽ+Ï;žŠ ébç%±§2uP¹)=‚ˆ·Èǽ|\¨È¤*Ö‡ƒÎËjh^¾Jɶ||D´’"azØ÷|ß ·¼ä&:»“±°Ïkc}Œ±¼è ÅV%çÿ:ù–€J}*bµ… åÊxïÞ¿ë˜.F‰I†ó¤ù‡2ŽŽj´nK¬ÍTyŒbUz-½žà1MÌ~½ðqýºWénoú÷‡T=þQŸ× ì`‡#?$BÖ?5R½ÓIMI“õ”•Í[³äjˆDœ×tûJºåKÇÇ>vqèâ`éħÒèÃ]=ºÕ{•èØê±^lŠ(÷nœéR%¨3j·Œ:uaÜmš©z€·×å]TZêÜ{§T¦NÞ15@‰ 㥱Îÿ•¥F¡c~c¾srW…â:÷ÒrßZö}¢¶e%Wþú5 ˜¥¹dªá:޹X×u6^\–‹ŽnÇŧ¢€º±Îtá–ñ ÆqП,ùçŠê¾‡ œw£jÈêÕÖ·ß7-ý×öáÞ«vñy¶äÒÏ¿©{Ï­~ì0`ý™XTl»‰JÕnñíTµÆzñj[}ZÎF‰*Zž¨>cž†IÍ¥Î]íü­—íľ!Pî£D?BøÌ¸µ>£@pÆÄŒCŒLú’̹°fˆ¸$˜ÐùXˆ‹“E³{ÊSÉçxžOÐø,‹. ÔßIÙMSîõã®°v4ÅIÍF>¦9¨ä6y‚„þ\óF¤ÍcvKÆf’[¼Ë¼CtNéG»œ[Çj0´þ6Êz@ܶI•½Š?¨b££"™ ¬ìrJUÞ°L™GGw³±Ùñ*ß²Ücdö·&¨rý‚\•ì”{H“£Nsó¼ø‹$ŸZYo~Q±yr÷Õ°¤t…¬þ¼tyÐÚìŸWùEÛßezec‰mj*Å.Í]£< ™Úšëœ¢Æï5ÖFcxÐ "OÁ ‹ÎB›ÈTÕw… Ÿ"p’}ÏKç^K ÖÕáhëzÕcÉžÜÐl¸!_0.)õgÜËunºªi4ØV L7™k È# &S²uQ3Âî9Ksµ7»Lå4=͇i¡¥UéÀñšR0/5ã L”D&”ƒ–Ä;+CO·úÛ»» uÇ«èâC_–+mô üßW2[Æãta‡È¼M/l÷hº 6uŒ±´õÝ`ÞzæBÕ ÝÕÖ6‰©åýlÒ,û³]¦Õ{0mj ,VËÖ}Ål”i•õ†ÚÞ{­5ê×ÝY´QD±uÁ™§Ö]¡OÇÄëzQ•ŽÅÀΡb '+­E¬‡á1‹Œz—.4Š(À’ò»Î»1¤% zߦÛéù5ÔZîã€Ñp‰í·yåt pˆ‰Ï‰`K_úˆÓ»Þ}ƒ…Ú¥8yw7òT%B0#ò|K…¡>©wT¿É—$@ÔgL²¥K Eܧ„Ý „%m’M?°v·’+›XmI¤ˆ|1Q5IëEÓ¬Èhu³UǪêÉm±J? á›ÒúƒZ5MÆ ¡.ì ˆ8 0J[ºXØÔ 'Câ/™ÇT€¯*2¨ÏfÅZZA5.ŠèRïj@ÕÛm©ÌªÛ@¯Ä1‚œïV­Øªf:Vr0ÔèrjðÖ_è¨hh?KÐ ÐÁw\‰põ‘Ê –>ȯè?9£ ’vþ9Í:ßúFJ:cu]”ÎÁ(º^è§"s*Ô(q5ÎéRÄ l(c1|ÆA} 7Çþw[ê•›±l¼jÅ#ÆÃWŒör] ]|ƒoß©û ðÛÄ0˜C‚£Lp9µÚÅž(’LÊ™E¡€H0PNym_N9d$LÓ[wÐðû“Á=v³2˜cý xR)cà×àÉ8-âLM}Y÷h(àD75›=ϵ‘FfÌs5’Ý âÀÿ]JP.¥ÿµ»aP½«\½v¿»fs²Â2³©ÜEÀ&3wÕÁ]pq…×»lT_Äe°_‘Ë>©:jÊÌG_¸r†xmådÃÖþW½Gý³ºz>8.ôÒùø­~|kŠŸhŠo¶sp,iÀx‹=Öç-ÀjŸè ýY¡«z ÚÑyÝ´eq!àè5ÃF×ófÁ­ˆq$B*èÕâ[õ@4ø]z ^ù? »8òÿÒ.7a3ƒëqÐi= Y~îL¢@6ƒ»=}1Œ¥t~VØ Ëón)Äw“Û(o7¸|Úu×NÝÓ(™_88†^jŒÒºèÖó G('ŸîÏnúÁ §M’},˜O– C’ „¤Nìhó|ø8\b?ÎÁRTÊsð“gëãphÎçfgÒámØÓ—rÃ[7—RôÕ«7—‡ÈڌХ p!‰ n^Ž=·òxí!ýÆâ…`D,]B±ö%¿{îæMNOž×.Þàl Ç\¿¹ ”8à–|é Ä|ZN džbzóv;E5˜äö{„–`d–|6'cf¹§‚çgÁêc2œÕÚöô§÷ðvA!%œÎJ»ýá‘(”l~Nµ÷aðÇòE·WsÔZ½RÂ~§1]ÿôp<ºEoÚ©ŠlB$à¶´›µ˜ïH ä"JÂwL˜%Éþä6[ãFsó‰Yò? ûç£^B9‘¶õs9ET‰Ù]ÿ3Š]þÞþˆÿÿšSn îxœíY[£È~_iÿb_¦SÔ¼í^)­)yI6Š”—CÙfSNQnÛóësŠ›1`O¯’e”q«»Í9§.çþUñüÓy_8¯RW¹*W.AØud™ª,/·+÷o¿üìE®S™¤Ì’B•rå–Êýéåûïž«×í÷ß9ŽÃËj™¥+wgÌaéû‡£.Ò[?K}YȽ,MåD|w Ÿ^åS-“¿ÊTí÷ª¬ê¡eõÃPZg›^üt:¡«¥HÇ>¦>¥HxÕ¥4ÉÙ…}Î¥cxÑ7Š-+0Î~{ùŽ€*uÔ©ÜÀ@‰Jiü÷¿¼ï™F™É†óäå‡*Mòfݎؘ!ÙËꤲò;z3Á)ÏÌnåRÜ<îd¾Ý™ëók.O¿Wç•‹ìˆÆDÁ¯ßZ©«ÓICɳ• ÊFíS»ärˆ:ïd$Ò(ÄQ/Š)ñ0ñH°pÒceÔþ©Ýé½ÌTjõX¹™:•‰Öê„zƒöKÈóAiãmòB6ÂþNí¥‘y¥Jÿ½|•…:ØPò¹J¢Ÿ§ªüg‘‰åùÎÙÜ‹yî¥ã¾Xös&7U-×Â>R×ñf¯ŽÝ^f <]'UëÇ9$[åBé•ûæþtœµÒ™ÔOÔŸ[ž_çæÒ¤a7·i;q/€ïT»Ì ‘0á~Tj¿ryˆbFã);…h!1Gá –ŒQÅÇñ„ >ZßxÇ27F‡ót‚£ÖV¢H.´¯ÿ‘NªÚ©ÓV[C}”“±§¼¼6æIL§ª·"]Œù=›÷x—¼}rÎ÷ùG »$«ÁÐü›¤¸FÄ}«Ô±²“é©×*ÑÙh`m—cžÉêŽeª29xëµÍôY¾ey‡ÄìîMP ”ê +x2ÛJoŸg•—æÓâo’|´²Zÿ*Sóp÷õ°”'… õiéj¯”Ù}Zå7m[¨uRÜJlr¡¢·yéuÄÓ€QÈ™çè&~çXkeŒÍài€Ö!ò( úüÔÊ$¦®ä¸©yPØ®Ó@¶#Ç\l_:_,Ñí©6E-%ù•(÷Û£j¼]É-ͦ¢‚ct½Ìs3ÐU޳Á¶µ*_òVKØ@™5“­‹ÚvÏE^Jè%Åe,§Àèy9, ­.]ð§ aì¥I²Ä$ƒ~Б‚ÞÊ€O–yÿóK·Âsš.ÿ®ô‡~EDZ"ÉZÁÿîË•þœ¥K@ûļä{¨üijeÜJ[ß æmfÖ²'³0-K÷¹åÿÕäEñG»L§÷`ÚÜò¥^¶ùÚëâ·ÊtÊúCmŸýÎÍãvE²–D²-Á™–Ö­VÇÃòµíîÀηmÄ褬¬E¬‡ák‘ù/<=Ð žzwloC:âáÕ÷ƒiÀ¶:?¿ƒfPr/°ýi¶PcB"ø‚FqB1{ººo°P·T@n¼»½ñT-Bp»7äé–ŠãHPÒì¨y"Q° !b”óˆ/·I= wÇþg[º*7aÙ‚9kÅ-@Œ‡gŒö(r=º&ø¾uøŽÝ¯Áo#Ãh0[ŽqDc« E<Š&…"Á£Eñ˜×ár@EŠÂqyëϳ-?sÑc7 §Î/áI(¥œ÷‚_ƒ'Ó\§…ûÒº RHDtâJ{¢ë2L˜çz$¿›Ä!ý" %XEôkwà{×\¾óxð4gsªÂpÊž$Hí.6!8œ¸ üL(˜á]]6IªÏâ2(Ø‹¯Èe¿ =eâ£ÏÜ9c<ÉÇ®sò!ô°Ï5öhþbÞtÏWÇì9¿µÂÿF|Å@ñ]8Ç’6çcñëSð™ ð DxL'®Æ@´cÓ¾iÛ& „€£×$6z Î瘇÷0ˆ™`³Í·Æ@,ü"(€oô?vopäÿ¥]î†Í$\o"Öã fÃG(‚OÂÝž¾8F‚16=+ lËS´ã§Ñm”¿\>mûk§þÛM1Ÿpp Xê6Hçþ¡7:σ±ùíþì§ÜpÚ" 5bÁ)Yp E2sR‡Ì£$¤8^`ÆN ‚#QSX ¸ƒxX<;ÇCs~ª`ö&™«+œÍpeíŽmÙXP ûóÈd}ߎs¥ú×1Ñr’,µ=ßnÜ[íkˆ îMz5ªæýÉ«†Üœíõ3¦s6ŠiÌÁèpœÐľ«c´~¯:ǧö‚[b— 3‰Þ‹ûQâ0s»˜Å³ÖdA §ãoŠÄXE'—à½D]Š,Ï1“2SíË-< ÒÞ@ÄÞ¤B©Câ‰0föê1â,´b`[EF¡ùè~¢™áÐŽ›½~…–ÈÓ[¦«1a@©ô>ô÷ö¶*<Û[søÿo8xñ ª image/svg+xml ” txœíYYãÆ~7àÿÀp_v±Ùɦ¬ɰäÅv /E¶$z)6A¶FÒþzWó>¤Ù1’u¼Èj°;bUõU__s6ß^Ž™õ,Ë*Uù£M¶-™Ç*Ióý£ýŸ¿s„mU:Ê“(S¹|´seûôõW›?9Žõ×RFZ&Ö9Õë‡ü}G…´Þ´.Ö®{>ŸQÚ ‘*÷îƒå80WÏû¯¿², ÖΫu?Úí˜âTfµm»2“G™ëÊ%ˆ¸öÈ>ìc³ƒôYÆêxTyUÍ«7cë2ÙõæfKgV[‘0 ]L]J°pªk®£‹3 û¼5–bŒ]ÐL_i¶®À³üëí;ªÔ©ŒåJ”Kí¾ûù]¯t0Jt2ž§sìd݉·óè(«"Šeåvòf‚sšèãMqóxéþ ‡ççTžÿ¢.6¶°å!ß÷ùð­µ"†4’4y´á°¢}j—\÷†…ÁÚovõ§ÓlU™È²ÓùõgªSoª¯MÚvów›6÷øŽAuˆuðÚJÍ8Î…G8õ1„ˆÃ}ÀßÃŒ,Õ°ª/8ð—³À'sÊS ÉS\–œÊÒXdÑU‚ê_ý2ÕA÷¥ñ¥.Or1öœæp,§tÒåé[“.ú ÆüžÉ…{ºë ºctIé »\úÇœ`ŒÀ.ʆ ¸ï•:\2~/Ë­ŠÊd6°öË)Md5S ëæQál·&ÁoºÎ¨œ"Ò‡ê%ƒ\ÕKÜ4èWpd²—Î1M •æúãæ¯²|ieµýEÆúÅÝ×sÀP•À ÓÇ­«£Rúðñ#¿jûûLm£lj±K5ÄJ¹OsG«bP#E&wú¶¦lø–j«´®³x±—:FÆaðÔXl È®v”eé«é4—«Ú½Ô¤Ÿ‘„„òX˜®SÓ1ˆ[™I%D}î ÆèB{½­Màré¦QUé6“S_Âò¤É\l¼ßŽ0{ÎÒ\B«È®s;þLóqÊw²:Õ»úî. |£8J%‘ŽFå¾y½—q¬|÷ÝS·Â&Ž×ÿTåû~EË2&ÑVZûio’x áé§ôµÀð‹?%ظƒbjm°ÍÛÌ\ʆnÜ$^I|LÍ(÷'fÙf™îÜ£iSÉ‘tã¶gèÎ莹q;'4ûy@fÑVBZüÍTykY-÷¥:GÈÀ¶Ø#÷N;ƒ.£¼2Ž0ÀÂ× ê[¼r€½  æ=ô(ì§‘,x0@>š\Z¦—·ÐB=ŠÎÂ6?í£ÇVÀÎBL¨G|¾¢H¡˜= ¨ê–òÈÔý Ú„`/´'âå– CáSÒì¨y"Â[‘1 ›¯àYÈ£Œð‡ñ‚°¤){“éGÞîWrbiJ¥ér ‘À³g#*}5 ØÒŒ5ùY¶Îá¢Ps:Ј ð›J—ê½\·\ãVÐôj0İfÎX'7‰ ›ZCäÉXø ÔÚ©¢V–´\½æ,‰ Á–etmv5’ªÝ®’zÝo`8DAwj†µn”–9UÚn5w õw‹zˆ…泂S"Î@-G \„t‚å>¢µü{ËÃ(¨­­Íg3àl„` UOTNѪt€$=GúTÊI½jÁé+äÉghL1|¦¹|;îŽý϶4n¡2uò¦G2îßídé´—"×1¡Ëa‚/á[‡ïþp›9¦·yPà÷=1÷è¨ÇàB,< }3@>á\×QmæAEÁ¼¼õÓV/ü[Ñc6+‚e¬$¡”rÞ~HÆigrŽ¥ RÈt¥¹¤u™FÊK=’ßMâ€þ. å3Oú¹Ã0êÞ¥Ò†”8¸È Ÿ;P­¡¿H.>!8XÀ8Š|Ï¿¡ [$Õ' 6ÇþgÙobC—QOY`ô‰;gˆùØuN>¦æ¹æÍÿ˜7ÝóÙr€{ë×Vøßƒ/¤øR|—ÎÁµ¤ ÆÛ±8Äú’|" ü"<¤‹FWs NÇ–}Ó´Mäù>\½±Ñs óbóàâòC泛ͷæ@,ø]8^ñ»Wùé—»a³×IÄÓz9d½›áÎ |Ÿ/ÂÝܾ8F>clyW%Ø"–—l)ij—Pî~ôòiß¿vê¿MŠy5ôo¤o~z”'=Æ^û÷®9ÈÛ“Ö ŒkX;Æ=ž£—¦pÄÌ-<èÚ!C˜žgnǨçy«æÛÈg=½££‚ù±RÙ;sê¨WWØ~QNg»g( ((ëwï!i6-h»ýQ8üѱ<5cö¯­,äãÇ¡Lÿ»rr+”F­©¾ ¢ú½<ÙÖב#æÁ5y› 'b‚Pâ¯ËMVnÌËjøý+˜0j7i"ñxœíÛŽã¶õ=@þAU^vPK")J"ñhA4/iŠ} d‰¶••EU¢Çö~}©»dÏNÚnÚEփݑ΅çðÜIÏã7—cn=‹ªÎd±±±‹lK‰L³b¿±ÿöÓ·³­ZÅEç²»ö7O_~ñøDZþ\‰X‰Ô:gê`}_¼«“¸Ö›ƒRåÚóÎç³›µ@WV{ïÁr`æúyÿå–eì¢^§ÉÆnyÊS•Ú4ñD.Ž¢Pµ‡]ìÙ#úd O´Ù³Häñ(‹Ú°õWcê*ÝõäZ¥³o¨0çÜCÄ#Ä §¾*¾83^Ðó/Ay€‘¾’l]ƒeKø×Ów·–§*;`n!”÷ö§·=ÒAnªÒñ:a'r'Ö.⣨Ë8µ×Á›ÎYª› æõ ²ýA ïÏ™8ÿI^66²¸„ã0 éðÔR ƒH–nlØ,kßZ‘ëž¹œ¸°N‡Œ'>öWA˜;;¸]´Ûî:•‰Vc'‘¼ÛÊËÏæA¤noÎ^€¸”²RÎ.ËEÃãäQxW‘Õ²ðÞŠg‘ËR’Wf q¥¼,‘ÅÏy¦„[wÖ»¤%8‰‡·±×û¤Ñ©ØÕ†®1ƒ~%¶å5È~WZ½T›wDºëÖ-–UÆ{ä\Vû«ùt˜­¬RQu¸Ð|¦8 žÎÔµÉànýNi½pO€îÔ‡8•gˆƒö½”ÇM‘pÊ¢pO X¸K£E^bAfäR†¹.×OŸ´sœS‘)È¢ò²\àTUš"¯¶o~õbêƒ<ï+mIUÄ‚÷œ°)§ yÌÉrï-I—!zF'Å=ÜõÜ1¾dÇì½-—öÑ;ÛçCHÜ·Š “ÕVÆU:c4v9e©¨gˆAn—Îv«3ý¦é4Ê)cu¨_"(¤q“ —àˆt/œc––2+Ô‡É_Eù’d¹ýE$êEíÍ ÊB…ú0u}”R>¼åW©¿Ïå6Χ»LA¬Tû¬p”,G5Bäb§ncª&€o¡¶R)ÃË512ƒ§†âq €ìj¹,K]u˹\5Ðî¡:ý4„GtŠc©Û™#Øna:•\Ò€ù>Y`¯·±)lCÌ#]w¬:ÛæbjKP ˆšÎÁÚú-‡Ö9Ï "¿Îé$Ø3+Æ)ßÁLªwÕÝ[–÷q*NcŠ} z+Ãè±þñí·O„Ç$Yÿ]Vïz‰–¥Iâ­¡”Ñ•“—Ó‡±@©ëßdù‘µ{IN"tÍÔíÎwqØ3ŽZ]u&¶ÓÆ sY¾.àè`žœ\Ê}F"Ì¿®U%߉u;ò Ô𦠄(‚9šú~× J­!Št üŠî á+ªz¯ZÓ–ÆÐi«*¾6Z r·«…Z÷ ›(c(çŽ´Ö ÒÒ»± ¼Bÿ­çFoý`‘Àõ¹þ¬`—n{ –Ã\d>L8ÑŠÐÐ%þ 72ÔÖ?æ«içkß0æ/PýŒ$ 0Š’•ÓÒs¬N•˜®Ö9}©‚¼Ñ‰ *Ï4©o‡Â]ÞÿL¥as ”.˜7­8‚p¢øÎ)K£½¹Ž] |_¾s÷Wà·™a*0[ΧaÀæV |—Qƽèé?„sƒÏø×ÍÜ~‰EóòÖU[< oEV–EËXÿž„RJiOø)x2ɪ$s_jwA …Œ,\©Ok]¦áòb8éÝ$ŽÈo’P¡0F>u7Œºw%•J ³È ›;P­\Äx¸Hã. 6Á(Z¸ üŒ‰á Üà²ER}—AÁ¦(ü„\ö«¦¡Ë¨§,|ô‘;'G‹|ì:'úÝÌÍÿˆ6ÝóÙr`ö`Öûϭ𿟇â†â»ãKÚ`¼‹C¬/G€4¿0aÊɢљ(„Ýù˾©Û¦„!½±ÑÏ@à|Šhto¢r?ôo6_3ùÑo2pàeÿÇa÷ Gþ.ír7lá:‰8˜´^Ùàf¸SæFaHá®O_¹¡ïû˳Â(Á±¼œ–8z˜ÝFyûÑåÓ¾¿vêŸ&Åüv€ƒ˜¥¦q€;÷½ÑyöÁÃ0À¿ÞŸýò£N]$¡R‚WA‘ŒBæ[‰…aÌ#8"ˆ¯aq+\ŒXh ~R ­Ð¸xv>æcs~¨`ö&}•¹D¨ÃN-6Ï‘úŸ§¸‹$1vüŸ%‰qv}‚dÈŠp(àÈÇá€8 #Ɔg8™b0àâ4œÁÌ#46Ä¡¸»¾(ÔüÌjpOœ³Rë0bmû ¸#ÔWØ1¤á„Ñn$µSÛê8:BÐx½·Ž°"œWíE|«‡¾‘7š` ¯Ñ¤¹¨BŒÂc€~àíæÅz÷†U 5ô½gƨEš çÝ 'mìnHèª[f jø[t’Û´áÉN§áœ•ßÊ"Ìúï¤Ñ¤ýÎÒ¨³ƒÐî/À ú›ìѵfÿDZQݺèï¶fÎXT]܇Èï Ðjúnú øý/üÍü:U&xœíXm£Fþ)ÿ#_vÓt7MÓ8ž‰t·Šéò%Éé¤ûahÛd1 =¶÷×_5o·=;Q²Ñ­nÍ TU¿U=UõÀêÛÓ¾pžeÝäª|t ®#ËTey¹}tÿõËwžpF'e–ª”n©ÜoŸ¾übõ7ÏsþQËDËÌ9æzçüP¾kÒ¤’ΛÖÕÒ÷Ç#Ê{!RõÖp<†ÂàæyûåŽãÀÚe³ÌÒG·SꢵÍR_r/KÝøߨ§ûÔì –©ÚïUÙ´CËæ«©umFs³¥cÐZ‘8Ž}L}J=°ðšs©““w5öyk,Åû ›˜¾ÒlÙ€g+øíjÔ¡NåJTJí¿ýåí¨ô0Êt6gpìlÝ™·Ëd/›*Ieãòn‚cžéÝ£Kq÷¸“ùv§/ÏϹ<þ]]ì`'D4&œsv¹ë­.ˆ!$Ï]8¬èŸú%—£!F1E0O–p§ Å$ö0ñH?épÜe¦R³ýG7ÝÉôÝZ~mod†FwŽ ÈS¥jímòBvcüÚKÿ,óF•þ[ù, U ùU®A’ÔÚÏSUþZäZ¢ª¼3ß)« H1¿­=Ú'£^erÓ´vÌ#u¿Sާ2ÛËŒ{'¦ë¤éÃâ8U² ª~t¿Ú´× Y«:“õ ãí5×)ˆt®Ï]ó›6øŽA³K2uXÚ÷Jíaâ…À"´ô)€…#Ž·µ°fŒ0HÀ"K ‘>˜àx‡2×EÕÉžàPׯ¢HÎŽßþ#ƒU³SÇmm<©ëƒ´Æóåõ'1µÏÞ› i@0±OØÛ˜¤¸§;¿ Û'§|Ÿ¿—°KbÙ˜Lý¿IŠ $î{¥K›õZ%uv5°õË!ÏdsÇ3M™TÞzmý¦Þ¨¼*Ñ»{´¥zÅ žÌ¶ÒÛçY¥òRØüU–/­¬Ö¿ÉT¿¸ûvXª˜Bú°u³WJï>|äWm[¨uRÌ-6¹¨ÔÛ¼ô´ª&xš( ¹Ñ·5u‡ß[ªµÒÚ¤° Ð"Swß®îb/u’%:™ÔúAŽ^æ±üéíwOà «4]þ[ÕïÆǘ$ku€ÐºOù*K—Àö‰~Ê÷P Ïø¨ÁÊ¿(æÖ&v“y»™kÙÑŽ›,K÷¹åÿ¬ó¢øÁ,3œ{2m® ùÔ.ÛÝŽgñûà ‡õ§§]ùƒ7ºÇí52‹d-!?þiª½cWÍm­ÕR±oîÄÏó¡ë¤lŒGL„á¶Êú/< 3(‚Ž>ŒáØÎ!-.ýj6 ø¶ÎOo ‘†G,ˆØüôa°ºcBCÂÙ‚ `=„âàá¾ÉBÃR!™Ew;‹TkBp»3±½%‚âXpJºuOD„ èÝeL°…Ä …4 ìaº ,iêßlú‰·Ç•¼Tššiº]€HºW#}6™Ø“%ùhY±,áÍ¡½óF,p‚ø›F×ê\öŒã^Ðõl0ÄÐhƒÜd,lj (³©ð7(ºs)ÀWÖ´^½dƒ,K ÑÖurîv5‘ªÍ¦‘z9nàrˆ*rîµ8c©‚¼1‰ *…kžÔ·¡pwìÛÒåp–ÊÌ›^œÈBx¡ø^Sl§½„\Ï@—ÁŸáÛÂ÷:ü5ÄíÊ15¸-„0Šk¯Ž†LË£Ð@#Ä™ˆ_ëÊ„P‘Dt]ÞÆ7Õ^/ø-ô˜ÍŠÈÆúGˆ$”RÆFÃO!’i^§…¼Ž¥ ¤Ô ¥yY2XÊS;’ÝMâˆþ% ŃPú©‡aÒ½k¥ )ñp‘>÷ Z!,bn%H.>!8²Âq&ñßÐ]Bf%ÕG l†ù'²ßņN“žbÅè#wÎ[ù8tN6¥æ¹åÝ_̺îùìxÀ=„óþs+ü3`ð™¿@ŠïÒ9x-éÁx‹¬Ûà#Qà8a1µ]Ë8œ.°û¦i›(ä^½,lŒ‚Ï0‹îq "<¸Ù|[D  á…WüÃîü¿ôË]ØXp!˜ÖË o qÎ,¸›·/†‚À~W˜$˜…e›-Åøáêk”¿||ÚŽŸÆ»Y1¿ p \jŽ2„!òpŽ˜óüþxŽÓO¾pš" 5bÎ(Y0 E2â"pR‡Í£$¢8^`ÅN"‚o%AÈ™ƒxZ<‡ÇSw~¨`Ž6ÛVæ³"üÿ/×+µL "KxœíY[ã¶~ÿÀ*/;ˆ%ñ&Šr<´] о¤) ä%%ÚVV ‰Ûûë{¨›eKöLÚlÐAׯîHç’‡çú‘^|{ÜæèY•U¦‹G‡xØAªHtšëGçŸ?}çJU&.Ò8×…zt í|ûôå‹?¹.úk©b£RtÈÌýP|¨’x§Ð»1»¹ï/k‰ž.×þr] ƒ«çõ—_ „`í¢š§É£ÓŽÙí˼–M_åj« SùÄ#¾3OÎò‰Õ {V‰ÞnuQÕC‹ê«¡t™®zq«ÒÕR$Š"SŸR$ÜêT˜øè^=§ÆRŒ±¼è+ÅæXvÿzùŽàUz_&j•W(ã¿ÿé}Ït±—št8OgØ‹u/¬]Ä[UíâDU~Go&8d©Ù<:7¯•­7æüþœ©Ã_ôñÑÁ£À£BðóS+uŽÒP²ôÑÍÊö­]rÞ b/¢Gï‚42Ja3D1‰\L\ša݆ç©Nì2N3½Ü£‹_’J>¨ô—¬€Y×›¶_Lwº4î*ËU3Úßè­òO*«tá¿WÏ*×;Tþ.ƒ ý¸4~–ÀÌyf”·+nÌwLwà°HLsO÷ɲ©ZUµ\cûJä7Ì~V½Ôšz ºŒ«ÖEíâ5u®ËGç«Uýé8K]¦ªìx¢þ\ò4x=3§&›»ù;¥íĽ¾!PmâT &FÜZoy2ñaÉOp­BãåÀÑ{ëw_dÒhwÜ—¥•Èã“‚=×H'Umôa]Zó™r¯FcY;qÛ˜'kЊty@0 oÉídl•– ;ã·Ìm|̶ÙGš’‘ŒÝÅÐð«8?ÇÂmËÔQR'E¹Ôq™^ ¬m³ÏRUݰNUÄ;w¹´Ù>É·,w›Í­ jB¿bW¥kån³t§³Â¼,þ*É{+ëå¯*1wµ¯ç€5 D(T©—¥«­Öfóò–_¥þ:×Ë8¿”XeB¥\g…kôRt‚‘«•™æ”M O±–jèvÀëu©CdOÄâ, ÖŽBÈœlÛ9ž,Ñé©6-% ù™¨¶;Û‚j,!Ïä–f{ŒG$ctÄ=MsS؆ºtÛµªl™«K[‚E Ôôšl­ßް:çY¡ Aä§k9 öÌŠãÀhÍj×Wu\ÖÆV™8M<(ò)è­ ðcþãûI2ÿ—.?ô+"dEâ¥Þƒk§3}‘&s ÛØ‡o¾×î/ÁoW†)Ál8ÆE ¯­<0Or)G…¾z‚ËÉèš×¡m@E’áuyëO©-_Š©è±ÊÊpëŸÀ“PJ9ïß‚'“¬LruíKë.H!!éÈ•ö€Öe1õH~3‰Cú‡$”`”ô­»aнKm,(qyð0esª•‡e$F R»‹€MGî?ê‰@LðÎ.%Õ'qlŽÅrÙoBCÇAOùèwÎò±ëœ|=ì{=šÿ1oºç3r{Hôñs+ü=Âà3(¾ŠoÂ98–´Á8‹çXC€Oï` Â#:jt5°;6î›¶mzpôÅFÀùóðâ'"&Ødó­1 ÿ ÀWþ‡Ý+ùi—›a3 ׋ˆ¤u?dƒÉpçÒ …à£p·§/Ž=ÁŸ 6Šå1ZŠðÃÕ%”¿\>­ûk§þ题O88,u¤sÿÐça‘ùíþì§\lÚ" 5bÁ)™q E2’¡€y”„G3ì…„† `)j Gx†‡Å³óq44çK³7éëÌØï«ÍEAa,) ÞB¢Ô×8ÏÀk&   ?#áEPáÃÍú'„ë/HábÏ,¯ïHô$¼Ç·J¤|ílg%FµÖÅ(ðêf±YÿÔÈ#1w¸î‹l÷¿fâpêê´»Ôo¯N¯ƒ{Ù‹H®+Tü'‘|uþú}brŽ„Ðð›aÖ‚$ ¢úHï5Æ\/ÖçʈCUád€HÊúçd+KØù‡[_ögøûoÐØÝ˜K"”xœíÛŽã¶õ=@þp^v‹"EŠ’œñhÁH^Ú4òÈm++‹®DÏØûõ=Ô•²äÙY´»É¢ëÁÌçBòÜéûïχ=ʲÊT±^PLH‰J³b·^üóçœp*iœ«B®…Z|ÿðõW÷Õãîë¯BÀ^T«4Y/öZW®{<•9VåÎMWæò ]¹SwaÑ'}RÊXg2Q‡ƒ*ªšµ¨¾±©ËtÛ“?==á'VSÑ(Š\⹞ç…S] Ÿ+^8ç¯Gqg‘¾lUrŽðÛÓw\©S™È-0J\Hí¾þùutNuj¯“o«$>ÊѾ°QC|Õ1Ndåvðf§,ÕûõÂ#Ít/³Ý^óÇL>ýE× ‚ò±Q!F-Õ`tÚ@²t½aÃvÖn¹²½{è• E$ ¢%òˆGBê/Qrª´:Ü5ÜÜ«T%FŽõ"ÙËäíF;õP¦¸×l¿—<U©m–ˆËÝ«ƒt/2«Tá¾–2WGãSî1Ó‰Kíf‰*~Ë3-ñ±¸±Þ9=‚½"1½t؃¾O嶪阩·@nƒìå2ÇK¦-ÒM\µBèïÀ§sU®ßlëO‡Ù¨2•e‡õgŒS`ôL_šxìÖïmî È ‚j§ê \b‚}§Ôa½`>& "šàð{¡ø›baÏSO€O`ꓱs*2 ñt| ¥sêv7˜ÏÚ¨Ûʧ#ëîF–ªI(ñ£Å<=ÅQ 6'jf4ô—4ÀÌã<äK‡Q†}Q~go[š$9ZÞÒv¿““H“XMId˜þ⊣Ò‰mC²¢ßAë–¯ ¸+Ô#§CÌ#M¾«t©ÞÊUÛҚ„$€®›3ÖÁMÄ¡VàEj‡Ì<†‚ûÊ2‡ú¬W¼ƒ¥1T㲌/Í©,¨Ún+©Wý!Ž1ä|§îÅV iä`¨ÑÕµÀZ?!ÏÇ,2Ÿ%H‰Èà!'Ĥþ„Ò –Ø«áoOpPS£_¯W3Æ7¶ C6Aõ}”*@)Z•tT±>•r”¸Zãô© âÆ6”±>ã žw…›¼ÿÝ‘á&(“0gµhÁ|ÌAydFiÏy®c\—Ã_Ü·vßkó—`·+Å” 6¸#øŒ ?¼Öà<Ÿá‡áD£P@,x°0ºÆu}9Ü>|/ ®Ó[±mñ¡˜ósX¸~~ KB*å¼'ü,™de’Ëk[sA‰Ð›˜Ò\èºH£乿ä7ƒ8ð>I@ 懡÷¹›ÁªÞu._9Ü¿›Ó¹Ù “0“©ÍEA'”s©‡…/fpƒÉ&AõQL ›ñ™ìƒº¡³US&6úÈ•3"“xì*'·[3¯{æ/áMõ|Dô!z÷¥þ/ÜàKSüLS|³ƒkIëŒó¾8øú´øH-ð3=å‘7)tu$@:6­›õª/\½&¾Ñ÷@`|Nxp«â>l¶øÖ= >IäÃ…7ü»Ý ù©—›n3qבÇA§õ¼Ëú³îÎCÁ'înn_œ`Á›Þ¬›øò´[ŠÈÝÕk”»³Ÿvý³S?%óy''ÐKý€væ·­ÑY䈄ðé‡Û³_Þzá4Ir G÷è’H’J…6Ï£G¢%Áõ‚ù>¦$5„ù‚#²$vòìlÙê|_ÂìU:Žœ›êjãa8}¼Ha~®tv%Õ¿Oq)'aRkò “Ny¾°_µÛØ0¯5¬n÷Ts¸º‰ÇÌ«¿qÀ¦hDý2°,TÖÏßæ~fV± òÿm>￟±9ê75ޣ܅K†þŠÃê-‡[ö3÷tèD!£ƒáXR!H`1ÓÀZ&oF,ƒÁÐ`à7ãžì—aÃX‡éùÙ°,ŒE?!ÉÑQÁM°ï³(€@FtiÒª `L¡'j(– rZƒuÌŸ7ȇmyLJDÜœ®¥7c8bM³ì©,z^ÅzC›{ëiߺ­©õ>‰ùüƒsO“²ïÍWðÿ?ÑUëÛÜxœíYëã¶ÿ ÿƒ |¹E,ФHŠrÖ 8)Ð~ISè—B–h[9Y4$z×¾¿¾C½¬—÷6h/è¡§ÅíJ3ÃÇÌüæAÞã—cî<«²Êt±q ®£ŠD§Y±ß¸ÿõ'OºNeâ"s]¨[h÷ǧo¿y¬ž÷ß~ã8 /ªušl܃1§µïŸÎeŽt¹÷ÓÄW¹:ªÂT>AÄwòÉM>)Ul²g•èãQU=´¨¾J—é®yyA/A-E¢(ò1õ)õ@«®…‰/Þd,ìsi,ÅûÀˆ¾Ql]qNð¯—ï¨Òç2Q;¨P¡Œÿþ×÷=ÓÃ(5épž¬øP%ñIÖíˆ⣪Nq¢*¿£7¼d©9l\Š›ÏƒÊösû~ÎÔËŸôeãb;ш!Øí­•º94”,ݸ ¬l¿Ú%×Ct ê¼SR$2Ä2ŒVÅ”x˜x„¯œä\}|hFwz¯SX=6nrPɇ<« ê Ú/¡.']o—åªöú¨ü«Ê*]øïÕ³ÊõÉBÉ?e(qiü,ÑÅ¿òÌ(t*îÌwIOà¦H,s¯÷ɲSµ«j¹Æö“ºŽß0{uìöRkàè6®ZÇ8Î)Þ”s]nÜïvõÓq¶ºLUÙñDýŒy|™k†ÝüݦíĽ¾#PâT¿fÜZab‰HbÊgüÄÂE€Aƒ`ζ»HF< åŒ >>[ïxç"3H§Ë|‚sYZ‰<¾*Ðêdªƒ~Ù—Ö¦<«ÙÈ—¬¼ó$¢sÕ[‘.ÆìžŒUóïú ï_²cöQÁ.ÉLÆj04ÿ.Îoˆ¸o“+6*T¹Õq™NÖv9g©ªîX¦*â“·ÝÚH_ä[–wŠÍáÞµ@¡ß°‚§Ò½òŽYzÒYa>-þ&É×VÖÛßTb^Ý}=¬é D!C}Zº:jmŸVùMÛßçzçc‰]f*å>+<£O< ¹Ú™eNÙàw‰µÕÆØž´†Èk0裳Ô&6u&ÇM΃Äv›"°é8æjëÒåj‰nOµ!j)QÈnDu<ÙU÷ òFniuV¡‚qtƽ.sSÐUM£Á–µ*Ûæj¬%l ˆšNÉÖEí»ç<+Ô’ü:•Ó`ô¬¦…ŽV§ƒ®øó Ð0ŽÊÄilâA=èH¼·2ô'ë_ÞÿôÔ­ð˜$ëèòC¿¢ãX‘x«Ïà÷éFL“5tÇØúÌ•3³xì*'¶ö»î=šß˜5ÕóÙñ ÷Îǯ¥ð¿ƒ¯Mñ+MñÝvŽ%-—±xÃú¼øL-ð+=aºº ]0¯›¶l".½fØè{ p>Ã,¼×1ŽDˆ`±øÖ=Pþ!=‡¯ü†Ýùi—»°™Áu„8è´^‡,_„;“(‚ÍànO_ #Áü¬0°–çÝR„&·Qþ~pù´ï¯ú·Q2_88^jŒÒ¹èÎó G$'¿ßŸýôƒN›$!R,%+†!I†BNâhó( )ŽV…„†‘Ã9"XŠšpÁ¼ÂÃäÙù8šóS ³7éવUc!–Ä;ŽÇ«ìåðT¸tUÖÝæ¶Weý§' ³å¬¾)‹0"¾^¿)Ÿõo÷<˜H &"ÉW¤LH@ÂÑ…×¼²×·6YQˆf8æ‚}©D„sЉ½?#2‚C‡CÃzêpeÿËŒ“0 ê1E‚”:¹#[O¶›pìu  Yu/Ó ×wãlRßF†WÂþLñ§8@Ë0æØoþ>Ú;bøûo üÌì!zxœíYYãÆ~7àÿ@Ð/;°Øì“lÊ£1, H^üPdK¢—b dkFÚ_ŸjR¤xI;{,²ÌŒXU}T}uuëñûÓ>wžUYeºX¹a×QE¢Ó¬Ø®Üþüƒ']§2q‘ƹ.ÔÊ-´ûýÓ×_=VÏÛ¯¿r†Õ2MVîΘÃÒ÷Ç2GºÜúiâ«\íUa*Ÿ â»=ùä*Ÿ”*6Ù³Jô~¯‹ªZTßô¥ËtÓ‰¿¼¼ VK‘(Š|L}J=ðªsaâ“7 ûœK1Æ>ðz¢¯[V`œüvò-UúX&j*”ñßþü¶cz¥&íÏ“ïª$>¨Áº-±1C¼WÕ!NTå·ôf‚—,5»•KqóºSÙvg®ïÏ™zù‹>­\ì`G ‘ øõÓEê :i(YºrAYyy»,¹ì{"ζN¨’„mÄ¡˜bGNr¬ŒÞ?4£[½—©N¬+w]ÆE²óT‘¢Î¢ÝêtÐ¥ñ6Y®i§÷Ê?«¬Ò…ÿV=«\¬/ù‡Ì%.Ÿ%ºøWž…ÅùNépŠ‚yî¹å>Yöcª6U-×X¾R×ñf§Ý^j-Ü]ÇÕÇ9Ä[ðå\—+÷›Mý´œµ.SU¶¼ ~†< `gæÜÄa;»i;q'€oT»8Õ/à î{­÷+—…\@„ᄟ€¿Š8åTˆ)×n Ð!çlÂŒïXdépšNp,K+‘Çgêo%‹Z™j§_¶¥µ£)j2ò%+@%ïâó$¢SÍ/"mŒù-·xç;¼}|ÊöÙ{»$«Aßú›8¿:Äm›Ô®²SÉ;U®u\¦£µ]ŽYªª–©Šøà­×6Ògù–åb³»5A-PèW¬à©t«¼}–tV˜‹¿JòÞÊzý›JÌÝÝ×sÀž@2Ô‡¥«½Öf÷a•_µým®×q>”Ød\¥Üf…gô¡çO=F®6fžS6þ;ÇZkclO´v‘{nÐEg©MlêLŽ›”yí: Dàe¤ã˜³­K§³%ºÕ†¨¥D!¿Õþ`kTÝ/È+ùB³á†hÀ…dŒN¸çyn ºªq4زVeë\ µ„ 1PÓ1ÙBta÷œg…‚R’ŸÇrŒžý´ÐÒêtÐæZÆ^™8MÜ+-ItV†þdùÓÛžÚ“dù‹.ßu+:މ×úø»OWúcš,¡£ØÇæ)ÛC¾°ÝÈ·Ð@<úWÆPÚb×›·™¹TMs2Û¦¥É>³£ü˜,Ïÿj—iõîM›™\=ÕË6;]ü‹2­²~_ÛG¿µFóº{g¯Ñßl]p¦©u[êãañºrëÒáöì\º!ú‹ÊZÄ" óب7xáAÓƒB,™xèàØ]ZòðŠ}o°m™Þ@­‡œE l.¯‚- ©‹0¡‚|A%OB1{¸Â×[¨]JºÛRµÁ"räé–Š"PÒì¨y#R,Hˆå\ò…ÇC‚2Âú Â’6I¦ïY»[ÉK”M¬¶$2DBáŽFTæl#ñÒ,ÉwйåËŽõ'¯e`Ä#&iH¢ï*SêwjyiŠ0¾šÂ‚8„f›3ÖÒmĦ–àEÚ'þ™yH÷UeõÙ,yKKc¨ÆeŸ›]õ¨z³©”Yv¸*qˆ!ç{u+¶l˜ŽÕÆ 5ºÐú»Cb‘} % @êxáú‘Ê ”ˆÖôQXK;¿Žg³à[l¤dV×EéŒbtéA?õ›c©‰ëN—ª nl`CKàõ¼+Üûû¶tUn² sÖŠ=š@Œ‡gŒvÏs=ëº&øâ¾µûŽá/·‘aJ0›€Çx äØjÀ£‚!É¥œX hˆ.C&£1¯íË™€Œ$Ãqzëγ¾ æ¼ÇnV†S_ÿHB*å¼üL²2ÉÕK „P éJ{žk#L˜§z$¿Ä!ýS*`BJú¹ÃЫÞu®Þx\<ÌÙ܃l…°Œ‚I€ÔpÁùNà*ëƒ{ ‚Þ²IP}È as|F}T7têÕ” FŸ¸rFxmåäýÖþ׽Góó¦z>;ôÒyÿ¥þnð¥)¾ÓßlçàXrqÆy_¼úú´øD-ðˆðˆN ]Ý ›ÖM[6‘8zM|£ë|Žyx«â Ølñ­{ þ)=€¯üv»Wùi—›n3q×ÇA§ußeŬ»s‰Â àw·§/ŽQÀ›žz6ñåi·á‡Ñm”¿í]>m»k§îÓ ™Ï;œ@/5ôÒÂßG£Eôˆ‚@dz›¾wÃi“$ä@ŠNÉ‚cH’a ™“8Úüÿ7 ®’ Ÿ image/svg+xml É ¥xœíXÝã¶ÿu^nK")Š¢ïhA4/IŠ} d‰¶•“EA¢×öýõê“²ì½ Ò zèi±»ÒÌpHÎüæƒ\{>äèYVu¦ŠÇqñÉ"QiVìÿüõ{G,P­ã"sUÈÇE¡ß>}ùÅúoŽƒ¾«d¬eŠN™Þ£‹wu—½Ùk]®<ït:¹YGtUµóãÀP\?ï¾ü!sõ*MݘòXålšx2—YèÚ#.ñ–|2Ê'fÙ³LÔá ŠºZÔ_ÙÒUºÄÍ’N~#E¢(ò0õ(u@©/…ŽÏÎÕXXç­±cìÏ}¥ØªË–ð;È÷·VÇ*‘[(ÝBjïí¯o¦ƒÝT§¶žÞ°“y'Ö.⃬Ë8‘µ×Ó[§,ÕûÇÅíç^f»½¿Ÿ3yú»:?.0Â(piD8çl|ë¤FÄ–’¥ ج辺)Wƒ v#ꂞ4æ"J|â/Å$r0qH§´ßî*U‰Y>(̳TVî`ÄA­<—ªÒÎ6Ëe+éíÕAz™ÕªðÞÊg™«ÒÀÇ+3 ”¸Ò^–¨â·<ÓÒ-‹;úÎi ®‰ømî¥ç>ö:•Ûº‘k7o>éy-sØ‹Y^jŒj‰nâºsBe¼øæªz\|µmžž³Ql¾çñæ™òø7Ó—6n{ýý¢âAߨ÷qªNàý÷½R  7Ä,"þŒŸD„ðà0˜s/†+0%‘͸àߣqŽs,2 ±Sžç ŽUe$òø"aûÍ?ÒKÕ{uÚUÆ’º:ÊÙØSVÀ¦œè$¢ó½w"=ø &ó=t2&îñ./ðñ9;dï%¬’ÌdÌlûoã|„Ä}«4`ÙËä¬6*®Ò«]Ž2õËÔE\:› ï›|ÃrÊXïï)h õŠ™î¤sÈÒRe…þ°ø«$_šYm~—‰~qõ˜rˆBZú°t}PJï?¼åW-—«MœO%¶™¨T»¬p´*-eL°¥]–PŸ°{B˜Ò¤½‰zËÚÃLN"Mª4EÎwÉXäû§Ö€]±"ß@7–¯ 8&4oNÏÀ.‹|AC}SëJ½“«®ÑÁ¸#´¥q=3óýžnµ©Mürí” ¨•UW¯XOKc¨¯U_ÚUYTµÝÖR¯†Œ›(cÈâNÓ^­Z&2»AU¡êÖ×Foý„hàú‘y–°K—Ã(r„‹›GH'\RÆ]ÚÐ@vÃFýûZ›q¾ñþŒ5tFª£hU9Ð#=ÇúXÉI¾êœ3d(ˆÏP˜x¦±| wÇþ¹%››±Lž¼iE‹Àéá8“Ìör] >÷ïµû+ðÛ•a*0[ Îg<×V |W0!f…ººœ‰ÐÑ5¯ï´ý2’¯ÓÛp,íø‚ßBY¬çXÿž„TÊØ ø)x2ɪ$—×¾4î‚â‚Î\iÎh}¤‘óÜŒdwƒ8¤I@q?‚~ên°ªw¥´iJ½È ›;­\,"> Æ]lî™»ÀÏ„º<à7x£ËfAõQ\ ›aþ ¹ìuCg«¦Ì|ô‘+g„gñØWNf·æ»é=Ú¿˜µÕó9Ð{ôþs)üoÀàsSüBS|·ƒcIÆÛX±>o>R üBDXDg…®é8ìΟ×MS6Ý€s8zͰ1ô@à|†Yx¯bË#Ÿû7‹oÓùá_Òpàÿð{…#ÿ/ír63¸NÖË n 7äœÍànN_ »Ü÷ýùYÁ °–çÝR„®.¡¼uù´®†·I2¿ pp½Ô¤w¿íÞó°ˆó€üqê­‹M“$!RÌ%K†!I†\ø(AÚ‡- D‚%S, |ôº€¹‘àp3ÓuT AÒèß,=Ã3Ñ(Ы·ô «¸Ú—uorë~³¿yïî7­Oëòt·æLAùëá6ìNeNÆvÈks¿ ÿÿˆ¡b¿ÛñxœíX[ã¶~ÿ@(/;ˆ%‘EQÎx‹ ’—4m¾²DÛÊJ¢ Ñc{}u³.öìÍ.²èz03æ9‡—sÿÈÇïÏy†žeU§ªØXÄÁ’E¬’´Øo¬þúƒ-,Të¨H¢LrcÊúþéë¯ëçý×_!„`zQ¯“xc´.×®[«ÌQÕÞMbWf2—…®]â×ÉÇWù¸’‘NŸe¬ò\u3µ¨¿KWÉn?NÎÉk¤H†.¦.¥6HØõ¥ÐÑٞͅsÞšK1Æ.ðF¢¯[×`œ~ùžàÔêXÅr¥SHí¾ýõíÀ´±“èd¼NZ¼«ã¨”“}{bk†(—uŲv{z»À)MôacQÜ2ÝôuüœÊÓßÔyca„‘ïÐpÎÙõ['uu:i)i²±@YѺ-×ãèp(z#E€E®ŔؘØÄ_¡øXk•?´³{½×‰Š+>Èø]–Öú·´H¤–Už‘–Î`ÞaCy.U¥í]šÉvª{P¹t/2­UᾕÏ2S¥ ,·L5P¢J»i¬Šß²Ö+‹;드òÛÜKÏ}2ìÇDîêF®5‹R ¹-sPÎ/1æ‰n£ºsBe´‡ÀÎTµ±¾Ù5Ÿž³UU"«žÇ›Ï”§Àó©¾´IÙ¯ßÚ,<à;õ!JÔ âbÁ}¯T¾±uü€cFü‚'p0eÀ–\s(ê0$\pÁáGãûX¤²ª,]çJéÇU~Õñ÷™ÚFÙTb—j•jŸ¶Vå(žFŒLîômNÕÆï-ÖVimx Mˆ¼CvVJGº)ë¸-yP×®Ë@v3ÒÓ¤ÎC´ªIQC v%ʼ4 «âJîh&ÝÊ™/<.¸—ÛÜt•ól0=®N·™œj (" &s²qQ7Ü9K ­$»Ìå=-Æe¡§5å ¯ÿî²´Œ\ê(‰t4j=ɬ `eýËÛžúãxýoU½vDȈD[uÿ[OWúc¯^ä‘~Js¨š| hâѽ2¦ÒÆw£uÛ•+Ù"•›˜-‰óÔÌrÿ¡Ó,û»Ù¦×{´lª3ùÔlÛ~tq;ezeݱ¶nov¸ŸGgm%$ÑO¦/ eiÝWêX毫iÖÈÎ a˜¢«¨¨EŒ‡ákHã ^Ù€€€.žÿ0¸c? iÁ‚«ïGË€m«ôüz­OqÀ¼p…ÍO7ô½ ¼êÎVT@{${W÷6ê·òÉÄ»û‰§‚ýК—G"N NI{¢vD„¿"ãQÆ[ÙñŸz„=Œ7„-M‘œ,?²ö°“KSXMKôøÖlF­/&;<²&ßrËÖÜšovÏÀ =a@Ãwµ®Ô;¹î@Æ¡mì ˆ@ÞÌózºÉX8Ô HÆÄß¡2O©¾²Ê ?ë5ëiIݸª¢K{ªUívµÔëáW%Êj¾Ý@±uËDF5zt=7xëgD}Ç ÍgZ:t Èn>BÚÁŠ2îІþ#ò±4Òè?óÕŒóo„ð¬E©Œ¢UežzŽô±’“ÂÕ9g(U7&±¡Åð™&õíP¸;÷;ÒU¹ËÌ›VÑ|‡ñð £½¹¶ ] | ß&|çî¯Ào3ÃT`6 œÇ¸/æVõ=G0!mnœ‰Àáœ×ãrχŠ$‚yy.·_ð[Ñc WÐOáI(¥Œ ‚Ÿƒ'ã´Š39÷¥q¤táJsŸë3,˜çf&»›Äý$ Å=_ú¹»aÔ½.ߨ̸esª•ƒEÈ Ò¸‹€M«9÷ù ÞÕe‹¤ú(.ƒ‚Í0ÿŒ\ö‡ÐÐyÔS>úÈ3Ä‹|ì;'C3n°Gû³¶{>#°‡@ï¿´Â?# ¾€â@ñ]8×’.oÇâ5Ö—à#Aà0a!]4ºqÐÎ[öMÓ6Ÿs¸z-bcÀ@à|†Yp1ßá¡Ç½›Í·Á@^ðI0^ñ»W8òÿÒ.wÃf®“ˆ¤õrÈú7à 'àœ-ÂÝܾv¸çyË»Â(Á±¼DK!~˜½F¹ûÑãÓ~xv¾MŠùí Ç€¥¦q@z÷½Ñ{ô9÷É÷ç°üè…ÓI¨sFÉŠa(’Š˜GI@q¸ÂN@h"ßw¼¡x>g¯ð¸xö>ÇæüPÁL:zƒêlÕZˆÅÑÎÇÓ]öb|+¼õTÖ¿ævOeÃÐæ€l}Ö¼”…Ø!F/¿”ñIQ¿õ†=îc´o`+[8”L=±² „†<~-»|ãÆÀà”2¾òÀ À@DXý Ax$„~Džé^†‡áŠ<ï`ÚögýkbØo~æñ·4:o{÷ºØ´-s ýÿhÞŠáÿa¼>"ZxœíZ[¯ã¶~ÿ (/{‹")J¢û(A ´/iŠy)d‰¶••DA¢íýõêfÝì=ÛdÓ.ºZœ=ÖÌð2óÍôÙ|ÉRãE”U"ó­I6 ‘G2NòÃÖüçÏ?XÜ4*æq˜Ê\lÍ\šß?ýÕ¦z9|ý•a0<¯Öq´5JkÛ.NeŠdy°ãÈ©ÈD®*› b›ùè&•"TÉ‹ˆd–ɼª‡æÕ7Cé2Þ÷âçóZŠA`cjSj„U]s^¬ÉXØçÒXŠ1¶7}¥ØºãðÓËwTÉS‰= (Ê~ûóÛžia«x8O’¿«¢°£u;bc†0UF¢²;z3Á9‰ÕqkRܼEr8ªÛûK"Α—­‰ l¸ˆÄóàeá%É’÷v97Ö`hû}˜ÞÜá¾MjG9Šè(w2,ãÉÀÚ.§$ÕËTyXX»ôÑÀ^@ó¬"TÇ{3Ô¹|Å–ˆÂÊ’¸I®>,þ*ÉvååÍËÝo"R·_O‹@~QHQ–®2)ÕñÃ:¿jÿ‡TîÂt,±O8KyHrKÉbàQF*öj™S6¼ÄÚI¥tÏ]´v’Ö7ÛÇg)U¨êT>Ÿ¥Ñâð¡Ê`<°öC‰·W0¼GJ\”¥³kšèî£|n$77À®Ûц¡®º¤^®šhöT^4%ðÙ(²B—׺Õá7rKÓ©Q¹ÜqèŒ{]æÆ€’˜F²®ÈU²KÅXSØ@5ž’µæí½g­:Áô:•“à.I>Li­Ne]å²ç¥«adB…q¨ÂA!ëHnoeh­Ö?½ýá¹[aEë_dù®_Ñ0´H¸“'ð\óùFßÄÑš¡,TÏI¹N7RßBï³±oŒ±´Æn0o3s)š¾j±ÃŒ£,Ñ£ì¨$Mÿª—éôL›¨T<×Ë6{]ìV™NY{¨íÆî¬Ñ¼¦š†;áÿ7]ÓŒyY8”òTdi¶f]öÌkB?DAkTi‹h„ác*ñ¯,è×¹ã>õpÆ.Í™Ã~0 ضL.o Kp)ö™¬°þ×¾ºÎ úÑê­(§ˆЧ|ƒ…º¥\2B÷0Bª!Ø Ìy¾%‚‚€{”4;jÞwWº»  Ú•å¹Ô!ìi¸ ,©Óûhúµû•Ú¬¢Ë¹ƒˆïš“•ºêHl;©5ùzÎtÃé¦þdu ŒXýŽO‚ï*UÊwbݶs·„¦)AìÃ99NG× ›Zƒäñø¤Ä1ÜW”)ôjÍ:ZB'Q–áµÙÕ€*÷ûJ¨u¿›EÉÖª›ÈuÃ4´6Tè/ª©­¿ú½@?+Ðy 5,Žpýpaù+Ê:ˆ7sŸ–lnA¶B˜Þ,@j¸Ø„`àL(ò\owƒlTŸ2HØ {ŸdÕ ]5e†Ñ'®œžÅcW9Ù°õÐïuïÑüYS=_ zn¼ÿR ÿ7øÒ?hŠï¶sp,iqÙo¾>o>Q ü ", ³BW÷@hçÌë¦.›Èõ<8zÍ|£ï|†™¯b.òÇs‹oÝ9þŸÒ¹pàåÿÃn÷ ÿ/ír×mfî:ò8è´»¬»èîŒ#ßóØÌÝõé‹aä9Ž3?+ læËón)ÀO“Û(û0¸|:ô×Ný§Q2_v8‚^j줃ˆF‡<èxžK>Ï~úÁ §N’)ö%+†!IúwŒÈ ÐæQâS¬0ò õÃuÁÜ«)Žë1¯ð0yvCs~(aö&Þ†=¾”ÞºYToÞ, 8xЀ¬,—!æ1°ÇwcÎ().Î Îê,pùÊ%ˆá8ºßš§;øÓÀöûWã¯ñqœÿâ‘p'·¨žÔ‰6¦1¤âùl_]HwßbOBS÷·â~ͳtÝ ¸ŒïÞN‚󤀗&9y£±°Ï¹±cìo úF±e Ʃ෗ï¨VÊ ”¨”ÆÿËûžéa”™l8O^~¨Ó¤’7ëvÄÖ É^ÖU’ÊÚïèíÇ<3»•Kqû¸“ùvg®Ï¯¹<þ^V.v° *H†üúí"uu:i)y¶rAÙøòtYr9ŒDw2Ó8Âq$Å”x˜x$X8é¡6jÿÔŽîô^f*µz¬ÜC•h­Ž¨7g¿€ÿ(a—d"c5š“׈¸o•&Vv2ý õZ%: lìrÈ3Yß±L]&•·^Û<Ÿå[–W%fwo‚F ToXÁ“ÙVzû<«T^šO‹¿IòÑÊjý«LÍÃÝ7sÀPœ@êÓ§¥ë½Rf÷i•ß´ým¡ÖIq+±É „ŠÞæ¥gT5ˆ§£3ÏÑmüαÖÊ›ÁÓmBäQôù©•ILSÇq[ó °]§ ¼Œts¶]ét¶D·§Úµñ+Qî+Û¡´_ÉšM7DCÄŒÑ ÷<ÏÍ@W9ÎÛÔê|]È[-aeÔlL¶.ºŒ°{.òRB/)Îc9FÏËaYèhM9è€?í-c/M’%&ôƒŽôVt²üËûŸ_ºžÓtùw¥?ô+:ŽIÖêþw_®ôç,]žØ'æ%ßC½°Xäwžý+ãVÚún0o;³–-4™iYºÏí(ÿ¯&/Š?Úe:½Óæ¦/Ͳí×^ÿ¢L§¬?ÔöÙï¬Ñ>nÇÑY$k Iô'ÛœiiÝju¨ö¯—®áì|ÛFŒNÊÚZÄz¾‰‘ïðÂȃ«°à©wÇö6¤c]}?˜l«óÓ;h¶ÅgbíÏå1` €t/hL'³§«û uK䯻ÛO5"½!O·DqHI»£ö‰ÄÁ‚DˆQÎc¾ða( Œð§á‚°¤-’7Ӭݯä¥ÒVÛ"QàŽFÔæl3ñH–äG€nŲ„BóÍëqÁbñcm´ú —T„ñ…Ð6vÄ@mÎXG· ›ZB”Ùø+Tæ[*„¯Ôôg³ä-K kœÛ] ¨j³©¥Yö¸*Q%Pó½‹-[¦cµq C®ÇFoýÙ¡bÂ~ % Aêx1ÂÍ'–^´ úÌSàI>v“¡‡}n°Gûó¶{¾:`Øùø­þ7Âà(~ŠïÂ98–\‚q>¯±>…Ÿ ?À@„ :it  A;6í›¶m¢ áè5‰ó9æÑ= Ä ²ÙæÛ` } À7þ»78òÿÒ.wÃf®7HëqȳáÎc…!Ÿ„»=}qŒBÆØô¬0H°I,OÑ’ÀO£Û(;¸|Úö×Ný·›b>àà°ÔmÎýCotž=Dä·û³Ÿ~pÃi‹$Ô@ŠCNÉ‚c(’Q3'uÀ¼œXf8‚Á[FXè 0ÀÞDzú›y›÷Ïö^þÿè©pÏ öxœíXÝã¶ÿAP^nQ‹")’¢œÝ Z‚h_ÚòÈm+'‹‚D¯íûë3Ô·-Û»‡ö‚z6îÖš9ß?êñ‡ã.w^TUgºxr ®£ŠD§Y±yrÿõóžtÚÄEçºPOn¡Ýž¿ýæ±~Ù|ûã8°¼¨—iòän)—¾_î«éj㧉¯rµS…©}‚ˆïNä“Q>©Tl²•èÝNu³´¨¿›JWéz?è4R$Š"SŸR$¼úT˜øè]¬…s^[K1Æ>ð&¢o[Öàœþ ò=Õz_%j *”ñßÿü~`z¥&êÉŠu—êlߞغ!Þ©ºŒUû=½UpÈR³}r)n·*ÛlÍøü’©Ã_ôñÉÅv8¢B°ñW'5´”,}rÁXÙ=u[.AŒ"Š˜óŽ§±Q`áPL"°‡vYoð2Õ‰5àÉ­â4Ó«½1ºøu_$[•|Pé¯Yz œ;l§Ž¥®Œ·ÎrÕ®÷·z§ü“Êj]øïÕ‹ÊuiÓÊ/3PéÇ•ñ³tç™Q¨,nè;¦%„,×¹§žûlÙ©Z×\ëûH]Ço™ƒ…öx©uöDt×]§Œ7Ö¹®žÜïÖͧç¬t•ªªç‰æsÎÓ÷̜ڒìõ÷‡¶Š|C ÞÆ©>@V̸µÞ=@,ÂQÀgüRÇ£ˆSÆ$›saÏ1¹”bÆ…¨ïmp¼}‘¨©ò8W°¯*+‘Ç'æ7H/UoõaSYOšj¯fkYFy]ˆÎmïDú¢ $oÉX;å-æés³]öQÁ9ÉLÆÚ0À:ÎǤ¸í—&]šê¨V:®Ò‹…göYªê¾©‹¸ôV+[øWù–啱ÙÞRÐú ;x*Ý(o—¥¥Î óºø›$ïí¬W¿©ÄÜ=}£ö€n¢Ð°^—®wZ›íë&¿éø›\¯âü\bH•j“žÑ%ÔêF®Öæ:§j3øk¥¡î&¼á,MŠÜKƒ¡B+mbÓ6ö¶íAoÕ@ v+Çœì˜:ž,Ѩ¶H-% ÙHT»Òެ>È‘ÜÑìLBT0.ƒ€Î¸§ëÜlU—Õ`§\­run% ˆš^’mˆºöÌyV('ùéRNƒÓ³â8ñlO³§f€?-c§LœÆ&žŒ„žÄ/\YþãýÏýI²ü·®> ;:މWzñwŸGúcš,`ìbóœí _Xpò'ÀþÈ8—¶±›èm5WªÅ*WQ[šì2»Êÿ§Éòü¯v›Þî‰ÚÌäê¹Ù¶ý9ØâwÆôÆúSkýÞíãæ2;óx¥ ˆþf‡‚3o­›JïËÔk77܉ŸÏ‰©â¢¶±†ŸylÔ;¼ð¡Ë€? áØœ§´dáû‰ðm•ßÁ¼å‡,ˆØ~»G,ãE˜PN[P P‰P<Œá›lÔoÅÉYt7g‘jDæ‘{Fž‰ (’‚’öDí‘|AB4C|áXƒX˜ö<Õöh›ä™ú‰·‡¼DÙÆjgb€HÈÝ‹µ9ÙJì0É’|è-_pch~y=ä$ Iô}m*ýA-;`„qGhG;â°7 ‚žn+µ„(Ò)ñ7èÌçTH_Uå0ŸÍ’õ´4†i\Uñ©=Õ„ª×ëZ™åp€Ñˆ2†žï5plÙ2k=ft}éˆÖßÊQÙϬDl Ž'n>Ryá‚2hCÿÉá…´óË¥6|)ƒk@Rº§]y€©^b³¯ÔYãê‚3´*¨[Ø0ÆøœõõT¸¹ö?;ÒhÜŒeæU/Nhn!?ÁÝfî´{™ëÙÔe àkú6é{þ âvá˜ ÜÆ¡ÁLpyé5àQ ɤœyhˆ“a £K^ÌI†—ím¸Þvüñ’1Í{XÎsý3DZ)cƒà—É$«’\]ÆÒ† JHH: ¥½Óõ•FfÌc³’Ý,âþ!%¸qÒ/= “éÝpõÎcüášÏ=èVËHÌ ¤ ŸÎÂq& .®ðÆÍŠê³„ 6Ãâ Ù'¡¡ãd¦Ìbô™'g„gõØON6…ö¹Áíÿ˜µÓóÅñ{Hçã×QøßHƒ¯ ø(¾ çàZÒ%ãõ\s}>¾ƒ‹èlÐ5H€uÁ|nÚ±‰¸põšåÆ€ ø ³ðb‰(ÁÕáÛ`  üC0‡ ¯üN»7òÿÒ/7Óf–®gHë~Êò«éÎ$ …`³t··/†‘‚`~W˜Ø,—çh)Âo£üÍäåÓfxí4ü:kæ×“œ–:ÏÒ‡>ò`G$'ŸÏAýä §m’Ð)Œ’ÃÐ$C!'qÀÊXm·*/«¡yù]ߺHV¹ÙÒÁ«¬H†.¦.¥X8å)×ÑÑŒ…}^K1Æ.èz¦7šÍKðìþvö­•j_Är%Ê¥v?þò±S:%:éÏÓ:öbÝ oçÑV–»(–¥ÛÊë i¢7÷6ÅõãF¦ë>??¦òð'u¼·±…-ÑpÎÙù[cuF ©%iroÃaEóÔ,9ï 1 )‚y’ˆ‹0öˆ7³(&¡ƒ‰CšIÛãΛíßÛZ©lΘI&Óie¨ój·Ž<îT¡UšÉz¨»Q[éždZªÜý(e¦vOî.Õ ‰ í¦±Êÿ¥Z¢]>1ß1ÙA¬B~]{jµF½H䪬ìjo˜Gj[n­ìg¶—/÷L—QÙDDzvÑðœ©âÞþnU}ZÍR‰,Z¯>—:Oõ©NävþvÓfâÎO”›(Q€ÃHûY©-Șú¡écÀŒÃßg|¬6»Âˆ Ïóǃ!â{gŸ§²iwO°/ c‘E' ç_ /lmÊ:¬ ãH]ìåhä!ÍáLN|ÒñÑ“6ÆlÊÆ¤Æ”îô„nÓmúYÂ.ÉÈÆœ ïþU”1í“ +’ÅREE2XùeŸ&²œðL™G;g¹4é~UoTÎ.Ò›© *ƒ\ݰ‚#“µt¶i²Si®Ÿ7¿Éò©•ÕòWë'w_Ík@S(SÏ[—[¥ôæù#ß´ýu¦–Qvi±J5@¥X§¹£Õ®‡§ž"“+}]SÔø½¦Z*­MZA¤ƒ‡Úbq¶€äjFY–>™¾s<¡ÝIMöI°³Pnw¦UdBœÅÌd¢œùPèH{º®MàrtÓ¶Êt™ÉK_Âò¤ÉPl¼ßŒ0{ÎÒ\B›ÈNC;þLó~Æ·²*ÓÛÚ{­ØJ%‘Žz¥¾ù—ÌÿöñLJv…EÏÿ©ŠOÝŠ–eL¢¥ÚCh퇳|‘Äs` ÛH?¤[(†müÂÂ=+.­MìzóÖ3²&WiXoS3Êý»N³ìg³L{îÞ´©ÎdOºp›3´gtû‡\¸­êÇõY´”1•ÞËu¡ö»-dà½]5»çÞJÐ ÑE”—Æ&°ð5¾úÏà2(ÀÂóïº(¬/‘,Xpyopi‘?@ûô)˜ΰùÓ<úÞ ¸Zˆ õ g3*€òн»sÔz µKùä"¨ë‹U&û¡}!o‰ 0œ’zGõþŒÈ£Œ 6s€u!Ÿz„Ýõ„%MÙ»˜¾çín%'–¦Tš&ç!øö`D©O&Š1'ßËæ9\ªoN«Àˆ…ž  ¿/u¡>ÉyÃs0nu«C‡fž×ÊM¢Â¦æ€<é …Z{)ÔÊ"ƒŽ«ç¬•%ô×¢ˆNõ®zRµZ•RÏ» œ±‹ Š;»š×J˜Ƃª ]·:¢õW‹úÈ Íg§DÎ@-G \}„t‚eÑJþ“åcTÖÖ¿†³™à›ØáT/R98E«Â†ôé}!/êUœ®BAÞ˜|†ÆÃç2—¯Carì¶¥óáF*S'¯z±'óá6ñÜQÆN{ ¹Ž.ƒ Þá[Áwþâ6pLnó¡ÀyŒûbè5ÐQßC‚ 1ò(ôÍq&O„C]Ë´=*’†å­»¦6zÁ¯¡ÇlVc¬…HB)e¬3| ‘ŒÓ"Îä0–&\B\ÐQ(Í ­Í42R«‘l2‰ú* Åáö(è[C¯{JRâ0à"W|î@µBX„|” U¸àF Fá‚8Š¸Ï¯èÎ!%ÕW l†ù Ù‹ØÐ±×SF1úÊ3Ä£|l;'ëSó\qú_Ìêîùh9À=„õù½~ ¼“â'Hñ$ƒkIÆëX¾ÊÇ•Œtú,c•窨›©EýÍXºJvƒøétB'¯‘"aº˜º”: áÔ—BGgg6Îyk.Å»À‰¾Rl]ƒqJøä{ªÕ±Šå&JTHí¾ýõíÀt0Jt2^'-ÞÕqTÊɾ=±5C”˺ŒbY»=½]à”&ú°±)n‡™îú:~Nåéoê¼±±…-ÑpÎÙõ['uu:i)i²±AYѺ-×ãè@Ôz#E€E®,Š)q0qˆ¿²âc­UþÐÎîõ^'*6zlìø ãwYZëßÒ"‘ZVyZDZÂ6Ðh°ò°¯<—ªÒÎ.Íd»‚{P¹t/2­UᾕÏ2S¥‰/·L5P¢J»i¬Šß²TKTwÖ;'%ø.ä·¹—žûd؉ÜÕ\k3¤¶å¶ÌAGs¼ÄX}$ºêÎ[–UF{ˆïLUû›]óé9[U%²êy¼ùLy  Õ—67ûõûC›…|G >D‰:Ax,¸ï•Ê76£È8fdÁ!†„)~°äšCQÄ<pÁ¿sœc‘jH®ò¼\àXUF"‹.Ôß >(PÔi_;êê(3OBêäty@BºÔ¼ésƒ`ÌîɘL¹Ç»¼ÀË£sš§ï%œri;£ÁØú»(»Ä}›4¡b2EV[UÉlbc—cšÈúŽeê"*íÖdÿM¾a9e¤÷h õŠ™ì¥“§I©ÒBXüU’/í¬¶¿ËX¿xúf ØJˆBÕú°t+¥VùUÇßgjeS‰]ª!Tª}Z8Z•£x12¹Ó·9U¿·X[¥µIàe€6!òR ÙY)馺ã¶äA]».ØÍ´,}1½ê|1D{ š5”0`W¢ÌKÓ· !®äŽfÒ QÎ|áytÁ½Üæ& «œgƒiuuºÍäTK8@5™“‹ºæÌYZHh%Ùe.§Àèi1. =­)}ýw—  eäRGI¤£Q;èIþ`eÀ,ë_ÞþðÔïðÇë«êݰ£e‘h«ŽàûéJLâ5 Œ<ÒOiõ ”oT<ºWÆTÚøn´n»r%[Àrº%qžšYî?tše7Ûôz–Mu&ŸšmÛ¯ƒ.n§L¯¬;ÖöÑí­Ñ÷óèÌ¢­„$úÉôkYZ÷•:–9äëÆnZ‡=²sC¦è**jcãaøšàxƒW!Æówì§!-Xpõýh°m•žß@¯õ)˜®°ù醾· bB}ÂÙŠ h„bïáê¾ÑFýV>™xw?ñT#B°ÚòòH…¡à”´'jGDø+ 2&ØÊñˆ‡|êö0Þ¶4Er²üÈÚÃNN,Ma5-ÑC$ðíÙŒZ_L&vxdM¾ä–­ ¸64ßœž =a@Ãwµ®Ô;¹î@Æ¡mì ˆàÌózºÉX8Ô HÆÄß¡2O©¾²Ê ?ë5ëiIݸª¢K{ªUívµÔëáW%Êj¾Ó@±uË´Œ6Ô`èÑõÜà­Ÿ-ê#/4Ÿh‰8è@-G Ü|„t‚eцþ£åc4ÒÖæ«çßá-XŠRE«Ê<õéc%'…«sÎPª oLbC‹á3MêÛ¡pwîÿv¤«r –)˜7­8¢ùˆñð £½¹Ž ] | ß&|çî¯Ào3ÃT`6 œÇ¸/æVõ=$˜ ‹6·ÎDà‰pÎëq¹çCEÁ¼¼ wÜŽ/ø­è1‡…›è§ð$”RÆÁÏÁ“qZÅ™œûÒ¸ Rˆ ºp¥¹Ïõ™FÌs3“ÝMâ€~’„âž/ýÜÝ0êÞ —oæ?ܲ¹Õ aòE‚4î"`‚ƒ…»ªæjÎ}~ƒwuÙ"©>ŠË `3Ì?#—ý!4tõ”…>rç ñ"ûÎÉÆÐÃŒìÑþŬížÏ–ØCXï¿´Â?# ¾€â@ñ]8×’.oÇâ5Ö—à#Aà0a!]4ºqÐÎ[öMÓ6‘Ï9\½±1` p>Ã,¸‡˜xèqïfóm0| äÃ…Wü…ÃîŽü¿´ËݰY„ë$âi½²þÍpgœ³E¸›ÛÈ{ž·¼+ŒlËK´â‡Ùk”»=>í‡g§áÛ¤˜ßpp Xj¤wÿؽçAsŸüqË^8M‘„H1g”¬†"páY±EæQP®0  BË÷Á‚7ÏçÌÂ+<.ž½Ã±9?T0“ŽÞ :[µbq´óñt—½ß o=•õ¯¹ÝSÙ0t8 [Ÿ5/e!F„Âèå—2>)ê·ÞÀ°Ç}Œƒö låDÁÔ+‡@xaÑÉãײË7~` N)ã+Ü Ü ë`«“€Xÿ² <B°~´<Ó½ ÃyÞÁ´ÍfýkbXán7/¸¥ÑyÛ»×Ŧ}l™íÿGóV ÿÿ P é%ƒxœíYë£Fÿ)ÿG¾ìèLÓ/ q<én%ÒÝ—$§“îË CÛ&‹ií±½}ªy ž™U2›l²ÌìÚTU?ªêWîY}sÚgÖ£,«Tå÷6Aضd«$Í·÷ö~úÖ¶Ué(O¢LåòÞΕýÍ×_¬þæ8Ö?Ki™XÇTï¬ïówUÒz³ÓºXºîñxDiKDªÜºw–ãÀP\=n¿ü²,X;¯–I|o·cŠC™Õ²IìÊLîe®+— âÚùø"›¤2Vû½Ê«zh^}5”.“M/n¶tdµ ÃÐÅÔ¥Ô §:ç::9WcaŸsc)ÆØÞ@ô…bË ,[À¿^¾# JÊXn` D¹ÔîÛŸÞöL£D'Ãy:ÃŽÖY;ö²*¢XVnGo&8¦‰ÞÝÛ7¯;™nwúòþ˜Êã?ÔéÞÆ¶¿|k¥.ˆ! %MîmPV´oí’Ë^£"˜'‰|ÆŒ°…E1 LÒNÚ©»LTl¶Yªµ,˜F§q”¡Þžý òT¨R;›4“Í w§öÒ=Ë´R¹ûV>ÊLIn‘j D¥vÓXåÿ‡©%*òó’¼úóÜsÇ}0ìU"7U-רÁ¼RÛrf¯–Ù^bì;]GUëË*¢- 9Så½ýÕ¦~:ÎZ•‰,;ž_?cžW§úÜ„p7·i3q/€oT»(QG„û^©=Ð9˜z¡˜ðc@‹¦ “.¬)P臂“ <}0¾qyª!ŠŠÓtü¡,D%h_ôU;uܖƺ<ÈÉØcšƒNN yÒ©ê­Hc~KÆÅ-Þù Þ>:¥ûô½„]N-`4šeDܶJ•ŒßÉr­¢2¹XÛå&²ºa™* g½6>Ë7,§ˆôîÖµ@®^°‚#“­töiR¨4×Ï‹¿Hò©•Õúgë'w_Ïk@vQHPÏKW{¥ôîy•_´ým¦ÖQ6–ؤ RnÓÜѪàiÀÈäFÏsÊ¿s¬µÒÚDð 5D†0xh$V ®v”e鳩8§³!Ú=ÕDŸ¡„¿å¾0Õ§n#Ä…ÜÒL$!êsO0F'Üó<75ä5ÐMÁªÒu&Ƕ„ äP“k²±~;Âì9Ks e";_Ë)°gš#¾£Õ‘ÞåvwšÜÆ^ê(‰t4HõÉë­ Çò‡·ß>t+¬âxù_U¾ëW´,#­Õ\k?\è«$^B¯°ôCº‡T`úŒ¿Ck°r/Œ±´ñÝ`ÞfæR6mÇl–ÄûÔŒrÔi–}o–éôL›êL¨+·Õ¡ÓÑ*¹r;#4¯Ûk@fÑZBXüË$ykš,·¥:{ˆÀ¶ØóŽ ƒ.£¼2†0Ž…¯tªoðÂ.X0ï®÷ÂvŒdÁƒ‹ËÓ€IËôôʧGqÀY¸Àæ§}õغ´êŸ/¨€f‡PÌî.^,Ô-å‘‘S·#Õ"{¡="O·DP Ÿ’fGÍÞ‚ˆQÎ_8Ðo!2Âï† Â’&í¦X»_ɉ¥I•¦È1DϾQé³ À¶ÅX’¯¡Ë–9êoNÇÀˆ‡LЀ„_WºTïä²ís0n M©A@÷Ìëè&PaSK@ž ‰?C®Sµ²Ì âê%ïhIõµ,£s³«Um6•ÔË~%в¸SwWˆim,ȪPu«k#€·þmQ±Ð< Ðù µpýé Ê}Dkúw–‡QPK[ÿ»žÍ8ßøF6aõ‘ÊÁ(Z•ôH‘>”r”¯Zçô âÆÄ3¦žq,ÏCáæØ_·¥‹r–É“³VÐ<8G|§“©ÑžB®c Ëa‚Ïð­á{íþüve˜ÌæA‚cÜ÷ĵՀG=†bbQ¨›ò¹˜¯y]§Í<ÈH"¸Noýµå =f³"˜bý< ©”ó^ðSðdœ–q&¯}iÜ!ä :q¥9¢u‘F&ÌS=’ß â€~”€ò™'ýÔÝ0¨Þ¥Ò¦)q8ô"36w [!,àà<ë.6!8˜¸ üL(ò=†wqÙ$¨^Åe°9ö?!—}P7tÔ”‰^¹r†x]åäÃÖü׽Gó?æMõ|´è=„õþs)ü-`ð¹)~¢)¾ÙÎÁ±¤ã</XŸ¶¯Ô?ÑÒI¡«{ ´cÓºiÊ&ò|Ž^lô=8ŸcÜ긇üùl¶øÖ= >JäÁWüa÷Gþ%ír6¸ŽÖÓõfáÎ |ŸOànN_#Ÿ16=+ l‚åi·⻫K(w;¸|Úö×Ný·Q2Ÿ88†^jŒÒ¹èÎó GèûùpöÓ.6M’„H±Ï)Yp I2ð³b‹@›GI@q¸À( 4-ÏC ¿¦0Ïç^àaòì|Íù\ÂìM:¹ cƒÜ4s¶pˆùÅ õ5½|Þ}]¤Iœ!Aa@ÃÑýF4™ ”[ª}΄ÿC6O‹Ýyfy:‚× $ÀæÃÍæÐ-ùû%a<\ÛùO¤å¬KÃÃàiÝÂ]Ssaçzž°s- ¥œ~,<3úÏŸ°–³.}9 ~ž›k…gáÌfd|Yôªpöÿìpîš’æseþfŸ¿U©Ï‹È$äxœíYKÛȾ/°ÿ/Dlö›MyÆ $Æ"6—¬ƒ¹Ù’¸¦HlÍHòß·º)RIiƉ½X#–à1YUý¨×WÕ­ûÛÜ{ÔU•ÅÃŒ <ót‘”iV¬fÿ£¯f^mâ"ó²Ð³¢œýðöûïîëÇõ÷ßyžËz‘&³1»EìöUŽÊj¤I s½Õ…©‚H0ëÉ'gù¤Ò±ÉuRn·eQ»¡Eýª/]¥«Nüéé =1'E¢( 0 (õA¯…‰þ`,ìsj,ÅÀ뉾PlQƒqvð¯“o ¨.÷U¢W0P£B›àÝûwÓÇ(5iž¬øP'ñN_¬Û3Ä[]ïâD×AKo&xÊR³y˜Qܼnt¶Þ˜óûc¦ŸþXfØÃž@4"RJ~~:INJ–>Ì@Yuz;-¹èG"Þk¶L¨V„­ÄÜ£˜bG>ç^²¯M¹½kF·z/Ò2±z<Ì–U\$¿Ü邢ΦÝ*ú°++㯲\7òÁ¦Üêਲ਼º,‚wúQçåÎFS°Ë PâÊYRÿÊ3£Ñ®¸2ß!ݧ"9Í=¶Ü·–}ŸêUíä[ØWÚ°€Y›c®O\Ï3Gë9£&HêzÖÑí¸d_U°Q?)ó²òëdYÐNãyèO–ü³£úïaïßÝhÏ Y¼ZQû}ÓÒÿÓ>ÜnŸgK>û]nJ~Ú®0Öbµº½«ûÀº²yìÓFYjS¥çñe\ëv»x­Ý³W+÷i9˲JuÕò¤û\òJÈÚÌ@mçocÏNÜ à+õ&NË'Èé÷cYn.P(çrÄO ñ E!WT‘1ÖŒP6 ɺ·9æï‹Ì"îã NNËã£õ×JðV¦Þ”OëÊÚÑT{=ù” ’/ѱæ'‘ÐÆüšŒ…·k¼ã Þ6>dÛ죆]Ž­c5è[ç瀸n*|ÐÕ²Œ«t0ÐÙeŸ¥º¾b™ºˆwþri!{’oYþ.6›k8¢|Á ¾N×Úßfé®Ì ó¼ø‹$o­\.щ¹¹{7¬uD¡ÔšÂ‚8„Sg¬¥ÛŒ…M- Š´Oüù’ á««ê³Yð––ÆP«*>6»êQËÕªÖfÑmà¬Ä.Ì÷]+¶h˜žÕÆ †]Þú«‹ìgZ" :PÏW»Ò~8§\"êèöF¡“öþ9œÍ:ßúF)6bu]TY€Q ´½ÐO=Æf_é à:9§ƒ*È›ØPÆø\&õt(\û¿mé¬ÜˆesÒŠ=š@Œ‡'Œv+r}º&ø¾.|‡î¯ÀoÃT`6Ǹjhµ£=?0¤¸R#‹B ‘ä*d*òÚ¾œ @$á­»˜8ñ•œŠ»YŽcý x ”óNðkðd’UI®‡¾tÇ=I8Ñ ÍfÏsm¦‘óàFò«IÒß$¡$JÑ¯Ý ½êípýÚçânÊæ> Â*’£qî"`‚Ñ»*wp—BNðÎ.%Õq6Çò+rÙ'uC‡^Mùè WÎò±­œ¼ßzØw×{41oªç£çCßJáçƒoMñ¦øj;Ç’S0NÇâ9ÖÇ-Àjoô@„GtTè\$A;6®›¶l"!%½F±Ñõ@à|Žyx­âɈI6Y|]ÄÂߤpàU¿ã°{#ÿ/ír5lFázqÐiÝY1î\¡PJ> w{úâIÆØø¬ÐK°Q,»¥ß n£‚uïòiÝ];uO`>ààz©Ë8 ­ûûÞh=zDR òéþì¦ïÝpZ ¤XrJæH†R1/ñ´y”„GsŒBBÃȬ¤£0!¹‡ç¸ž­£¾9ŸÌΤýÛ°Û—rý[7_ô zòêͲ6#lîƒB*¢Â«—cÏ­|¹vŸ~eq‰"0" ç> XS%îž»ySÓçÔÅœá˜KOW ‡"ÄJÌ}‰8e‘å„)pl$‡7oë‰Üìÿv}Y{ÇÇ jwçè2Žb;à¶Ò 8(Í)‡žâ3¬: ]^œ²AØïõð' '#Ø[KŒÏ’%};9 ‹Fuþ|wg#›h„b=C#‚Z ‡d„„(R||RvXÆ¢R«aÕ™î/ÛSrñŠFa´Z ±¥ù dwøtûÞ°šÃBd˜fnUõ8ñ‚˜ „”dŠpÏ¿$ŒàºŠœXçJ«5*Ð=Ô~†ÿw ô<&)(vÊ6¦¾`ˆI©0¿;ÿÈc—¿·?±Àÿ¿<Å^ɸ!0xœíY[¯Û6~/Ðÿ U^rP‹âE¢$×çØ ŠØ}ÙvQ / Y¢m5²(Hô±_¿C])É>9ÅnŠ I¬™ásÿ(o¾»sëYTu&‹G› l[¢HdšûGû_?ï„¶U«¸Hã\âÑ.¤ýÝÓ×_mþâ8Öß*+‘ZçL¬‹÷u—Âz{Pª\»îù|FYGD²Ú»–ãÀRX\?ï¿þʲ,Ø»¨×iòhwkÊS•7²iâŠ\E¡j— âÚ†|2Ê'úÙ³Häñ(‹ºYZÔoLé*Ý âúHgÖH‘(Š\L]Jpêk¡â‹3[ ç¼µ–bŒ]ࢯ[×àÙþò=ÕòT%b *„rßýün`:¥*5õôŽì;ñvE]Ɖ¨ÝžÞ*8g©:<Ú·‘íj|~ÎÄù¯òòhc [>¢áœ{ã·NjÌÒR²ôÑcÃî©Ûr=bQzÒ˜‡QÂ[Y“ÈÁÄ!ÒÞÜu*}üG{—ËX¡Á‡ƒVq)e¥œ]–‹VÐ=È£p¯"«eá¾Ï"—¥Î·ÌPâJ¹Y"‹ç™¨,î軤%D&ⷹמû¤Ù›TìêF®µ]?RÛr[æ`Š>^ª}jˆn㺋…e•ñ²7—Õ£ýf×|zÎVV©¨zo>Sž„ðfêÚ–m¯¿?´V<à;õ!Nå‚¿à~òt†¼GÌ[ðÈB ÀùK.ì Â(ó¾àBxO:8ΩÈ”NyY*8U•–Èã«ó›ÿH/Uäy_iOªê$kÏYF9]ž“ˆ.mïDúÜ'/-ìdt%Üã]_àãKvÌ>8%YÈh Lÿïâ|L‰û^i’å ’÷¢ÚʸJg ¿œ²TÔwÅÇ¢ÖºGŸ­›E˜PŸpoEC€8„bö0FÍØ¨ßÊ'“ î'jDö#{B^‰ ( 9%í‰Ú'ú+ F=/ôV ,äSF¼sCØR·½‰zÃÛÃNN"t«ÔSŽ!øölE­®º;±&ßË×\šoNÏÀ€!XH}[«J¾ëé`ÜÚY ‚8Ìì1ÖÓu¡Â¡ÖEjƒ^;¥BÖŠ*‡‘«Ö^OKc°U_ÛST¹ÛÕB­‡ŒF”1tq§ÁWë–iik,èª0vë¹ Zÿ°¨X¤?+°q°ZNˆpó …¬¨Çmè?X>FA#mý:צƒ¯c†lÁ ‘,À)JV€¤çX*1éW]p†u£ëSŸi-ßN…»kÿ»#Æ-XºOÞô¢AóáöðÜI–N{)sº(ø’¾MúÎÃ_AÜfŽ©Àm>48æq?œ{ xÔg(ôÂpáÑ‹ý܃AÍy=Ôf>t¤0˜··áZÚñC~+{ôaÃ`™ëŸ ’ÐJ=oü"™dU’‹y,u¸ „xH¡Ô—´¾ÒÈ‚yiVzw‹8 HAqæ‡!ýÜÃ`LïJ* J°È Ÿ;Э#¾(&\|Bp°WÕÜÆ¹ÏoðÆ-Šê“„ ¶‡ùg²ß…†.ÆLYÄèOÎ/걟œž =ôsƒ=Ú±×NÏgËìZ¾ŒÂÿE|Å/€â»p®%]2ÞÎÅ1×—àAà0ñ"ºt â`[ÎM=6‘Ï9\½¹1` ¾‡½àò|Ä#ÆÙÍáÛ` ü!ȇ oøœv¯äŸÒ/wÓf‘®“Œ¤õrÊú7ÓÝ QÀ¹·Hw}ûò0⌱å]Á(°E./ÑR„f/¡Ü½ñòi?¼v¾Mšùí$€'€¥¦y@úð›Ñè#vDœûä÷ÇsPo¼ØÔMz ÅÜ£dåah’™•X`%ÅÑ £€Ð ²|ò†Â|îYx…ÍæÙÇ82Ýù±†9¸tZ9¯­™ÁWohD»ÝÜ]~÷“áçP)CP£+÷?•°IHŒA0üjÚ±FNó’›û”BôFªC€½![µï¼#ŸAVñ{á˜IMuRĸ9)õÏr(‚³N òþqï›8v ÃŽ?Qj /ûuOÙèWíðÿ¢ÀšÓ ÒxœíYÝ£È?éþĽì(¦é/Ú73'E«S"%/ÉE‘òahÛÜbÚiÚc{ÿúTƒÁð윒]e•õhfLUõGÕ¯¾ºyüé´+½eêBWO>AØ÷T•鼨6Oþß~ù9H|¯¶i•§¥®Ô“_iÿ§çï¿{¬_6ßçy ¯êež=ù[k÷Ë0ÜL‰´Ù„yªRíTeë úùì*Ÿ•ÚâEez·ÓUÝ ­ê†Ò&_÷âÇãY#E¤”!¦!¥Hõ¹²é)…}Î¥ãxÑ7Š-k0Î~{ùŽ€j}0™ZÃ@…*eÃ÷¿¼ï™F¹Í‡óÕ‡:K÷êfÝŽØš!Ý©zŸfª;z;Á±Èíöɧ¸}ܪb³µ×ç—B¯OO>ö°!*‰‚_¿]¤® “–RäO>(›\ž.K.‡Þ¨÷N%"KbœÄráQLI€I@¢…—j«wíèNïe®3§Ç“_ªµMÑGÔ´_BöÚØ`]”ª·z§Â³*j]…ïÕ‹*õÞ¹R¸/,PRcÃ"ÓÕ?ËÂ*´¯îÌwÊ÷“óÜsÇ}vìÇ\­ëF®5„{¤¾¶Ì^·½Üx ºJë 0ž·O7àÊ¥6OþëæÓqVÚäÊt<Ñ|ny°.ì¹ ÃnþnÓnâ^ߨ·i®à îG­wO>‘dTNÙx !ˆ1™Ä3\XR¢(‘K9á‡Mp¨ a´?M'8ã$Êô¬@ûæé¤ê­>nŒ3¤55{,*Ð)¸ø<‘tªúE¤‹‚1¿'ã¢âïü o—žŠ]ñQÁ.ÉDÆi04ÿ:-¯qß*¯lUöA™•NM>ØØåP䪾c™ºJ÷Ájå"}–ïXÁ>µÛ{4•~à Ê7*Øù^•ý´ø›$_[Y¯~U™}u÷Ͱ¤'… õiéz§µÝ~Zå7mSêUZÞJ¬ ®b6EX½øÓ€áá<Ç´þ;ÇZik]O´q‘×Ü O£mj›LŽÛœ‰í: Dàe¤çÙ³«K§³#ú=Õ…¨£È˜_‰j·w5ªé’+ùBsᆨàQÂpÏóÜtUãhpe­.V¥ºÕ6P¥@ÍÇdÑe„ÛsYT jIyËi0zQ ÓBGkÒAWÂih;eÓ<µé t¤¨·2ô'Ë¿¼ÿù¹[á1Ë–׿C¿¢ç9‘t¥€¿ÿ|¥?æÙ:Š]jŸ‹ä ×üˆÇðʸ•vØ æmg6ªmNfÛ´<ÛnTøW[”åÝ2Þƒi [ªçfÙök¯KxQ¦S6jûvÖh7cï,Ó•‚ ú“+ Þ4µnŒ>ìw¯—ªáì|[F¬I«ÚYÄ! _ËÔªwx@Óƒ [aÑCÇæÖ¥_±L¶5ÅéÛˆâ˜3¹Àîçò±4uÁ4¡ˆŠÙþÁBÝR¹AwsƒT#Bp$ýòtKI™JÚµO$‰$FŒržðEÀCe„? „%]’¼™~`í~¥ S.±º’ȉ#4¢¶g‰—†dI~„Ö­\VpDh¾#.YBc"¬­ÑÔòÒa|!´…q Í6g¬£»ˆ…M-Áª|Hü2ó-ÜW™ê³]òŽ–§PIÏí®T½^×Ê.û \•اóƒ¦[¶LÏiãA†]hýÙ£bÒ} % õ‚áæ“¨ ^P.mèð"ŒâFÚûÇx6¾Ã&IØ„Õ÷Qº£Xmè¨^R{0ê&q]ÀéSÄ l(c|nƒzÞîŽý϶tUnÂr sÖŠZ„8Ïí5Ï œër˜à›û6î;†ßn#Ã0[ Žq%c«F %l=ÜsÓ{´1o«ç‹@ï‘x¿•Âÿ†|kŠ_iŠï¶sp,¹8ã¼/^}}Ú|¦ø•ˆpI'…®éhǦuÓ•M G¯‰oô=€Ï1ïõ@uÜB·™H͆¸¾E1Ž¥˜Û%ˆÀ,³&‹$œŽg ^—©uŠN.Á{‰&9>žc¦U®//·ðÈI{w“ ©"މGh$bÉÜÕcÂYìÄÀ.‹ÜºæÌáGâEoÓE7=£3›ë¼ Ž©@ú»yùîfþÿ• nä ž image/svg+xml ¥#ÏxœíÛŽã¶õ=@þվ젖DR%º3 ] м¤) ô%%ÚVV]IÛóõ=¤îÏζ˜´‹Äƒ™Ï…‡ç~(ßs9dèIeªò‹8ØB2U’æ»ëï?}k‡*«(O¢LåòÁÊ•õÍã×_ÝÿÁ¶Ñ_ U2Aç´Ú£ïóe%z¿¯ªãÚuÏ糓6@G;÷Ù6°sù´ûú+„ÈÎËu?X ÏñTd†6‰]™ÉƒÌ«Ò%q­}ÜÓÇú铌Õá òÒ°æå»!u‘l;r}¤³g¨ˆÂÅÔ¥Ô »¼æUt±'¼pÎ%^Š1v7 }%ÙºËá·£oN©NE,·À(\V>tH;I• ÷i ;’;²vdyŒbYº-¼Þàœ&ÕþÁ¢¸^îeºÛWýú)•ç?«Ëƒ…F¾Cᜳþ©¡ê#†Ô4y°@Ù°Y5"×!vu`Ÿ$⡈=â­ÅDؘؤٴUw¨XÿÁŠ÷2þ¸Q—ŸÓ<‘•,içtFíÄÈËQ•½M3Ysº{uîU¦¥ÊÝòIfê¨ÃÉ=¦@¢¢rÓXå?g)ìwÌoìwIŽà*Á—±×û¨Ñ÷‰Ü–†®6†^R ¹5²ÓM/ÑFn¢²qBÇhᜩâÁz·5Ÿ³QE"‹ÇÍgŒSàï´ºÖyÜîßZoÜàå>JÔ¢a†}Vêð`qî0F=ÁgøB†`'q8gŽA¤p<Ÿqæy3,¸û¤}cŸò´‚T:^朊BSdÑU‚öæi©Ê½:ï mȪ8Éï‚Gí&sÕ’6ÆìÎŒ[¸ë ¸CtI鳄S’Ö`hþm”õqÛ*&VtŠÈb£¢"™0»œÒD–D/7Žöf£Ó}Ñte£j_¾D+#b‘ “`Ëd'íCšUšWŸ&åK’ÕæW/žÞì2 F)”©OS—¥ªý§U~Õñw™ÚDÙ˜b›V+Å.ÍíJ5@dr[-cŠ:€—PUU:…çjbd5Å}OÙÕp!T]uß¹\5Ðê :ý4D¬ÊÃQ÷ 3L„=¸éTr(g~èyt†½.cPCN#]·­2ÝdrlK8@4™‚µõ}æ,Í%ô‰ì:¥S`Ï4¦| 3©Þww^ÝkÄAVQUÑ Ö· ¿³2Ìë?|ûØJ¸ãõ?Tñ±“ˆ&‰6ꮵ{ø}¯ab8DÕcz€Z §?€pïöˆ1µöÝ`ßzçBÖÃÇâ–ćTs¹«Ò,û^‹iõl›V™|4bëÇN·Q¦UÖj{ï¶Ö¨—»idfÑFB~üU—{4/›»BŽHŦ#X;[DUDy©-¢= Œïñʆ¡Æ pèùw;v} /1±ú˜ð1[Ù¡p¸ÀXлqÔ¢mo½¡Po']xŽéŸÊs¨Mª°¡>EÕ©£h¬%@B„φpkÈfÜÁ^ÀéÊ ©½#˜'© < +¬Ÿ}­¨;ôbТgfÅ«š7D 9m¨š°1´°Õ=Øy+»8„ÔœOÈHl‹ZŽ–ÐëÎêõ gt@KÛ(¤ËGs]GÌIã~s’º¼!!!&C€~âi47¬Z¨¡ïØF-Ò(ࢱ·ádµ½ [µÛŒA5c€Vr³6z-ÙnO8eèyeuÕE±™ûÖäO0!gëwTb»5 »Ç•U¡>Ê[/ë¹iÛ¥®•™ëò_§¨Cè/ÐïÖP,dÑBÍ"ƒ±§Z³–D0äEt]çp¡BÕv[ʪ—ÔìA'µÍˆ»®‘HAgƒÑ§Ö£i"ý6 0O{ß ¸€2“÷"ô3ÁLà˜„ÐOœ×VCMâ8¡Oñ(ˆã‡¯#ºyz^ª>âÒúÏ Xß<¡’ëV3SÜ~zßÞ»»y‰6SGÈk©hCû+ÒË{¸ë€NóÄ ëŸf雚!0¡>álEÁNŒPìÝ ªòn¢é.ôɨïFÍÔì kž‰8B„б9Q½"¡¿"Ð\(c:ᆠÍ#ìn(pà#cw’ìXê±VßH<‡¾5áXN £œÀA]rÔ—RŒ'éáàC¼yÞ4O GçÉ,MFÐÿAš4©ò¢¾ã ýY–(²C›O(í`E¡‡RÿùpÃ5ÔèŸÓÝ´óµoÂЛ¡^Ÿ /%ÄxîZ…›¼ÿÝ‘zåf(Sp–¬8€ùã᣽¹¶]ü¾Ó6gܯëîÄ0˜Í‡çéiaj5ÀQßsB†3‹Â'€# ¼PLqíkÏ×#V0-oÝ+Åò¥èч ƒy¬¿'¡”2Ö~ žŒÓ"ÎäÔ—Ú]B<¤3Wêjm¦‘òb8ÙÍ$诒PÜóÃ~éntïBUú ÓÕÝ’Ím¨VŸ%ˆq›ÌÜ~&0ìû|×»l–Toâ2(Ø ó/ÈeŸ5 ]=eæ£7îœÏò±íœl8zèµ™=ê¿0Á›î s:Ìáxø~³0øâZáç†ÁïCñ CñÍq®%M0.ÇbëóàFàf Â5:3qÐΛ÷MÝ6Ÿs¸zÍb£›Àù ³àÖ o.<î-6_3yÁ¯2ùpá ÿÃîŽüMÚåfØÌÂuq0i½²þb¸³Ð 8g³p×·/†îyÞü®0H°Y,ϧ%ï&_t/rF/uú§Q1¿ño»a–ºñoèÖó ‡àÜ'ŸïÏnûÁ—PºHB ¤˜3JV C‘ ¸yYK`Ì£$ X¬°äûÁ!7ÏçL¿JÏÖÇbhÎOÌî»'m¶{ýÍüÿ7R8r¤#âxœíYëã¶ÿ ÿƒ ûr‹XI‘’èîn€â´@ó¥MQ _Y¢mådѕ赽}‡Ôûaß]‹M{H¼Ø]qfø˜™ß<(?~9äÖ‹(«LO6v‘m‰"‘iVìžì¿ÿôƒÙV¥â"sYˆ'»ö÷Ïß~óX½ì¾ýƲ,˜^Të4y²÷Jמw<•¹+Ë—&žÈÅAªò°‹={ ŸôòI)b•½ˆD²¨ÌÔ¢z7”.Óm'~>ŸÝ³o¤0çÜCÄ#Ä §º*¾8“¹pÎ¥¹!äo ú™bë Œs„ßN¾%¸•<•‰ØÂDáBy~úÐ1ä¦*®“«$>ŠÑ¾-±6C|Õ1NDåµôzs–ªý“MP=Ü‹l·Wýø%ç?ÊË“,d1—pퟩÞ鸦dé“ ÊFͨÙr=D‡K¬÷" ’(DQÈWA;;˜­¬äT)yx¨g·z¯S™h=žìd/’yù9+R¡DyÈŠX ÁúÊíŒÜm+.GY*g›å¢^ÀÛ˃ð®"«dá}/"—G /ï˜) Ä¥ò²D?ç™±Þ%=‚ëx°Ì½¶ÜgÍ~LŶ2rµqôØ–W3;õñRmôè&®gYÖ1Þ¼sY>Ùï¶æÓr6²LEÙòóó$ø?S×:4ÛõÛCë…;tC ÚÇ©<:fÜW)@'.&>¥þŒŸ„¸F”ÍYúDÈ¥>Fl¾0øü¤=㜊LA`/óNe©%òø*@wó·RÕ^žw¥6£*Ob6÷ ’g§‰ÌÉ\ñF¤ Œ½%£ãäïz‡wˆ/Ù!{pJ<“Ñ ¿ó·­b¢ãD”—éd¢±Ë)KE5aôûñÑÙltð/šN³œc¬öÕ=Bš-º‘î„sÈÒ£Ì õiñÏ’¼·³Üü"u÷ôf Ø2ˆBÒú´tuRí?­òg—ËMœ%¶™¬”»¬p”<5`äb«–9e à%ÖF*¥xŽPƒ‘{0è´”*V&¹£:åA^ë—lfZ–ºêRu¹j¢ÝQuŒj iO‡£.[¦…ˆzrCÓñæ’€²È÷ÉŒ{]榠«˜†ƒ®tU¶ÉÅXK8@5’µ‹šúÌyV(%ùu*'ÁèY1Ì -Íäƒ6ÿ{óP3BÅi¬âA9hI¬³2´,ë¿~øá¹Ýá1IÖÿåÇnGËÒ"ñFžÀÿösOL“54‡X=gHºAùzŠG¯gŒ¥µïëÖ+—¢îW;·49dz–÷7•åùŸõ6­Þƒe3•‹g³mýØéâ5Ê´ÊzCm½Öõp7EgoÑ_tM°æ¹uWÊÓññÚ” {`çqQe\TÚ"ÚÃð˜CÃñ­èƒ\h`|öйc×}i ]Ÿq†èʉ¸p„8y‡A½µã¨÷@ª«/´8ùúÝ–éŸÞÑ:u ¼Ö—NYÝdé@}‰Õ©#¨ÖÇ)AsF‡t DË¡‹ü0 +?‚ÒÏ æVbAJ8 B²Bú™‘ûtDŸÑ`D1Ö‹EÜ€†z*ZÕs#«'µ&Ó¨µL«7ôaÃFÌ_9í†CJ=óÅr;¶ÇµÚ­ ßÕïI¯ÖÁÂH8F![šsè$cN‚iÀš“Ô¹1ŽôoJ£¹™ª75òÝÎd¢ÞÒ(ÀBÞØṲ̂µ½]µËŒIõüÆíÎÍÚèõÎN{ÂéTn½AЀ®é×øCø™Óó*UÊbýŽðo·Í°î¼Ö¨êD È\Wÿ:Å¥Rй†L"Ê–j94NjM[ZC›T–ñu]ÀsH•Ûm%T¿S°c µØ1-òºfZúèÔFhžªa²šÒoÃó°gnÂ…–š¸çãÔ€Ã04¡Ÿ‚ ZM5ãFŒ Q`—E!Óˆnž^—²\ÚÿYë++¤y]‡ ëJÚOïÛGo7Ïߦ%‰hh/et¨evyw%Ð)¤>_!ýÓ ™ÉaÂp@WìD1Aþà +ï&šî"†GÕy(ЈÀ%‰Û#òüHØå< ›Õ#±†ÊC(ÕáìcšéÃpÃ9ÀGÆîvr¡c}§ñ]2{2c9, G1·> 1ï‚£¾Ô"4 …ðæûÓ8^¤³0QÿaÒ„Êa®ÏõgZºè@,'r‘ùD Wj(1ô?Y ¹¡‘¶þ9]M;_û&Šüëóƒá^@Œ›²e(Üœûß©WnÆ2 gÉŠs)-ír ] üßi™3î×ywb˜ÌÆ Áùº[˜Z x„ùnD£hfQ¸…ÐpD¡ñ)¯}±â3Ýb…ÓôÖ½¢løQ°„}Ø(œcý < ©”ÒNðkðd’•I.¦¾Ôî‚ "2s¥~%×Fž1/f&½Ä!ùU*ðY‘¯Ý ƒêm^ ˆ÷Ð]=,ÙÜl墈³1îÂ`ŒÂ™»ÀÏš},ðz—Í‚êM\ ›¢à+rÙuC—AM™ùè+'G³xl+'¶zlzú/tð¦zBŸ½G4n¾ß _])üRüÞßiŠo¶sp-iÀ¸ŒÅëóàZà;=¦œÌ éÐΟ×M]6]põša£ëÀùÑðVï€û¿X|M䇿JÄàÂýÃî3ù›´ËMØÌà:BtZ÷!ËáN#7 :ƒ»¾}Qä¾ïÏï ƒ›ayÞ-qô0ù6¡{‘3z©Ó?’ù­xÛz©/ð†Þh=zð `øËýÙ-?ø†J'IÈ”àE$ÃÀ¼¬ÅÐæÄWÈ 1 ¹Å˜‹QŠÏª_%“gëc>4ç§f÷Å”6Û£þZþÿÙÂ>"GxœíYÛŽãÆ}7àèÐ/;ˆHöͦ<#ñ°øÅqÀ/E¶$z)¶@¶FÒ~}ªy)Jšqâ52Èj°;bUõ­êTÕiÎã7ÇmŽžUYeºX8ÄÃRE¢Ó¬X/œüü+T™¸Hã\jáÚùæéË/ÿäºèÛRÅF¥è™ ú¡øP%ñN¡wcvsß?^Ö =]®ýäº0WÏë/¿@ÁÚE5O“…ÓŽÙí˼¶M_åj« SùÄ#¾3°OÎö‰ÝAö¬½Ýꢪ‡ÕWCë2]õævKV[‘(Š|L}J]°p«Saâ£{1öym,Åû ˜¾Òl^gwð¯·ï^¥÷e¢V0Py…2þûŸß÷J{©I‡ótŽ­;òvoUµ‹Uù¼™à¥f³p(n7*[oÌùù9S‡¿êãÂÁ£À£Bðó·ÖêŒÒH²táÀaeûÔ.9ï ±QæIc!£„6C“ÈÅÄ%í¤Ýqç©NìöN§™^îÑÅ¿’J>¨Ôë=Ú¯¡Ž;]w•åªæoôVù'•Uºðß«g•ëÅ’¿Ë`&?.Ÿ%0ežåíŠóÓÄ)×µ§NûdÕ©ZUµ]ã ûHä7Êþ`v{©õðÀtWmdÚÅkÀr®Ë…óÕªþtš¥.SUv:QÆ: ÁÎÌ©IânþnÓvâÞß0¨6qª…‰ö£ÖÛ…Ã"O]X‘‰¥\LµvCÓå Â{w_d²gwœŽÜ—¥µÈã“‚3׿HgUmôa]Z÷™r¯&cY'q[¨“ˆNwКtð'ó[66néNwtÛø˜m³ vI&6öC§¯âüŒƒÛ^©RgB¹Ôq™^ ¬ý²ÏRUÝðLUÄ;w¹´ ~UoUî.6›[Ô…~Å ®J×ÊÝféNg…yÙüU–÷VÖË_UbîÖ€ª¦P˜^¶®¶Z›ÍËG~Õö×¹^ÆùØb•€J¹Î ×èÝOE®V溦lð{MµÔP8·×ZCdƒ§ÆâñlÉÕŽBÈœl§9ž¬Ðé¥6û¬$ ùY¨¶;Ûujú ÏâVf3É£‚’1:Ñž®kS8†ººmTU¶ÌÕØ—°"iz)¶ÞoGØ=çY¡ 9ä§K; þÌŠaÆw²:Ó»ŠîOKz£Ø*§±‰¾½—qÌzÿÝS·Âc’Ìÿ©ËýŠY“x©÷Zçé,L“9p„mlž²-”Ë/þ ”àÑ?+ÆÖ6vƒy›™KÕЫÄ+M¶™åÿÝdyþƒ]¦;÷`ÚÌäê©^¶ùÚŸÅoÓÖžöÑï¼Ñ<®/‘™ÇKùñ7[íÑ´j®K½ßm!Û†à ü<‹ÊzÄF¾æ@Ußá™ 4Æ ±dÁCŽõÒ’‡çئߖÙñtÏ€â³h†íOû°дÁgTÛ!³‡sø uKdÝõ(Rµ ÁAäŒÄÓ-/Ф ¤ÙQóDd0#¡Ç(ç’Ï\ \^@áÃaI[ÿFӼݯä&ÊÖLÛí˜GÂÀ¹Q™“ÍÄ–aÌÉ×ÀÅòy7†ú›Û)°Ç#&iH¢¯+SêjÞÒŒ[AÓ³Á‡@Ÿ9cÜf,lj(Ò¡ðW(ºc)ÀW•9´^3ç,¡Ñ–e|jv5êÕªRfÞoà|ˆ] åÜ­ÉÕ¼Q"{åÚouéˆÖˆ‹ìg§ôœ"Wz¸þHå†3`I­åߣ{am~¹œÍ߯FJ6QõIà£KÈÒslö¥®68}©‚¼±‰ *Ï8©¯CáæØÿnKçÃMT¶`^õâ@ÀEâ{¸žLv¹®….‡ >÷†ïeøKˆÛ…cJp[ŽqÈK¯ŽÌ“\ʉG¡†žà2d2ºÔu”›P‘dxYÞúj«—âzìfe8Åú'ˆ$”RÎ{÷É$+“\]ÆÒ† RHH: ¥½¥u™F&Êc=’ßLâþ! %X %}ëatïRKJ\\äŠÏ]¨V–‘˜$H.>!8œ„ âL¨'qEwÙ$©>IÈ `s,ÞPÈ~:zÊ$FŸ¸sFx’]çäCêaŸkîÑüyÓ=Ÿ‘ ÜC¢Ÿ[áïƒÏ¤ø)¾IçàZÒ‚ñ:ÏXŸR€ODïp Â#:it5p:6í›¶mzpõš`£ç@|Žyx‹ñÀìjó­9 ÿÀ…WþÃîü¿ôËMØLà:B0­û ®ÂK/‚Oàno_{‚16½+ l‚å)[ŠðÃÅÛ(=xù´î_;õßFÅü: À p©1Hþa4ºÈÃ9"!òÛãÙO?xÃi‹$Ô@ЧdÆ1ÉPH†D€æQRͰF(<‚¥¨%,áÏ.ÆÑÐ/ÌÞ¥¯rÂhµzµ»(lKJ‚·(õk‚g è5PÐ_ð"¨ðaÈfý7„ë°Â`Ï,¯¿Þ±†iIxOo”HùÚÙΛüÕ»æ(FW7ÛˆÍúo=3qG뾨vïékõ‡×^vo÷ÛW§ƒÇÁ{Ù’ëë ÿ ’/î_¿–!çHý¿ f=ظA²+ª¯ô^ãüÁëÅú^q¨*œ IYÿMÙÚvþ ˆ­¯öïðûߢ*Ü‘G-¿xœíZÝã¶ÿAU^nQ‹")Š¢ïhA´/mŠ})d‰¶•“EC¢×öýõêË’%{÷½m7]îÖš93¿ù ö?·™õ(‹2Uù½M¶-™Ç*Ióõ½ý_~t„m•:Ê“(S¹¼·seÿððí7‹?8ŽõçBFZ&Ö!ÕëçüSG;i}Øh½›»îáp@iCDªX»w–ãÀRX\>®¿ýƲ,Ø;/çI|o7kvû"«d“Ø•™ÜÊ\—.Aĵ{òñY>6'He¬¶[•—ÕÒ¼ü®/]$«NÜéàUR$ CS—R$œò”ëèè\¬…sN­¥cx=ÑgŠÍKðìþvò-•j_Är %Ê¥v?þò±c:%:éëi;Øwàí<ÚÊrŲt[z­à&zsoS\?ndºÞèóóc*RÇ{[Øò 眿5RgÄš’&÷6+š§fËy'ˆQHèI".ÂØ#ÞÌ¢˜„&i”¶æÎ›ãßÛZ©lΘIg£Šô³‚He¨ók·“<îT¡UšÉz±»Q[éždZªÜý(e¦vQî.Õ@‰ í¦±Êÿ¥Z¢]~Eß1ÙA´B>Í=µÜÃ^$rUVrµ?Ì#µ-·fvæ™ã%ÆÏ=ÑeT6ñ±¬]´Dgª¸·¿[UŸ–³TE"‹–ǫϧ ä©>Õ©ÜêomwøŠ@¹‰u@Œ¸Ÿ•Ú!©Š?Ôxˆ ϧþ˜i΄žç—BÄ÷&6Î>O5dÓî8V°/ #‘E' Ö¯…¶2åFÖ…q£.ör´òæ`‘ÓŸ„tlx#Ò&Á˜]“1©qwºÁÛFÇt›~–pJ2’1ô¿Š²3®û¤BÊFÆŸd±TQ‘\,¬ü²OY^ñL™G;g¹4é>É7,géÍ5•@®ž±ƒ#“µt¶i²Si®Ÿ–ä­ÕòWë›§¯tÀP£@ÊÔÓÒåV)½yÚäg©e” %V©¨ë4w´ÚõðÔcdr¥§9Eß)ÖRimòw Ð "}<Ô‹³$W³Ê²ôÉôãÉíŽj²ÏP€‰r»3=¨&Ä™ÜÐL&!Ê™5ޏ§infÈK ›¶U¦ËL} È# &—dãýf…9s–æšDvº”ƒ~®ëg|K«2½­ì׌­ÔQé¨Wè[’ßyæùß>þøÐî°ˆãù?Uñ©ÛѲŒH´T{­ýp¦/’xÃ6ÒéJ™6þÂÂ=3†Ò&v=½µæBÖÃÇä–ÄÛÔ¬rÿ®Ó,ûÙlÓÚÝS›êLö¨ ·±¡µÑí¹p['ÔëK@fÑRBZüÅTzk\,×…Úï¶÷vÕ ìž{+B·DQ^G˜ÀÂ× æÕxæÀ,ƒ ꮋÂzˆdÁ‚sÈ{jÀ¥EzüÍÓ§8`^8ÃæOóè{3˜ÕBL¨O8›Q#¡Ø»;G­·Q»•OA]T‰ì‡ö€<>Aa(8%õ‰ê'"ü GlæÀÔ…|êv×ß¶4eo ¾çín''–¦Tš&ç!øöÅŠRŸL6Æœ|£X6ÏáÚP}sZF,ô Hø}© õIΛ)ã†P·jÄÌÐÌóZºIT8Ô'}â¯Pk‡T@­,2è¸zÎZZA-ŠèTŸªGU«U)õ¼;ÀÙˆ]UÜ©f«yÍ´Œ5TUèºå¥ Zµ¨¼Ð|f`%â`µpõÒ f”qD+úO–QPI[ÿºÔf‚ob#„7bus‘ÊÁ)ZLH‘ÞrP¯šàt òÆä34¦>Ã\ž†ÂÕµ¿íHgãF,S''½Ø£ùp›ø î(c§ÝB®c Ë@Á;|+ø^†¿€¸]8¦·ùPà<Æ}qé5àQßC‚ 1ò(ôÍq&O„—¼vÒö|¨H"¸,oÝ5µá >…sXŒ±þ" ¥”±Nð-D2N‹8“—±4á‚₎Binhm¦‘óX­dW“8 ¯’Pn‚¾õ0ôºw¡´J³È„ϨV‹¤ ܨ1ÁÁ(\gB÷ùï²QR}•AÁf˜¿¡}Ñ4tìõ”QŒ¾rç ñ(ÛÎÉú£‡y®fú_Ìêîùh90{ëó{+| ¼Å7†â«ã\K0NcñŒõñð•Fà3a!5ºjâ`7m"Ÿs¸z°ÑÍ@|†Ypmb>â¡Ç½Éæ[Í@^ð*3^ñ? »gòÿÒ/Wa3‚ëq0i݆¬? w&PÀ9ÁÝܾFÜó¼ñ]¡—`#,§¥ß]¼„r×½—OëîµS÷mP̧AŽa–‴áïG£<Ørî“/g§¾÷bÓI¨sFÉŒa(’ž[Æ"XðŠâùœYx†ûųqØwçS³séèm˜öÞ`]¤ÕoÒ0W«/ð¦ßü†ñ¿—H}à“dÐHš9Ÿ¶Ïl³cš{4¿”>Å´Ó>š÷‘¬êK?Wr¢¡/ž¥÷öinr6~ ñwé~þY9ÒçÃà7à™!!pø$œÛ±äRoH0#œ¿8žÙ;žß°•“!å/‰ç Ü Æâ§Ës=–¼žÅ;žß°•S!õ_ð:xÏíX|¾‘7¯2êŸ ó?}àç>5R"œxœíÛŽÛ¸õ}ýBû’A%Š)JòŽg6X¤@÷¥Ý °/,Ѷ6²èJôŒ¯ï¡®´eO&h“6h<˜ò\Hžû!}ÿÃqW¢GY7…ª–ÅÄA²ÊT^T›¥óËÏ?z±ƒVyZªJ.J9?<|ûÍ}ó¸ùö„°WÍ"Ï–ÎVëýÂ÷÷‡ºÄªÞøyæËRîd¥Ÿbê;}6ÑgµLuñ(3µÛ©ªiY«æ;›ºÎ×#ùÓÓ~b-M’Ä'PxÍ©ÒéÑ»à…s^ã !>à,Ò’-PÎ~Gú€u¨3¹F‰+©ý×?¿‘Á¹ÎíuŠê]“¥{y¶ïìÔîd³O3Ùø¼[à©Èõv餛ne±ÙêiþXȧ?«ãÒ!ˆ  BðiÔSMF§¤È—÷³~Ë…í8@¯d,²8"q”¸( õõhè¢ìÐhµ»ë¸¹¹ÊŒK'ÛÊìÝJߪv(ó·Ekk<*xÜR÷ªÖÞº(eÇìoÕNú'Y4ªò_ËGYª½q-_h€¤µö‹LUoËBK¼¯n¬wÌ÷`¶D\ÇžìƒAßçrÝ´tbÌ4pß!GñÌñr£p‹t•6½¡Ú§píRÕKç»uû0+Uç²p¢ýœãؾЧ.,‡õ‡C›…Grƒ Ù¦¹zϘaß+µ[:,Ä$T$3|îÃpÀÃXp1Çž ¦ApÎgX0ùÁÇ;T…†¸Úç êÚP”éI‚øí?:P5[õ´©&u}3Þ§¢¡¼>hÌeïI†À „ÌOÙÓ˜0¹…;=ƒÛ¥ÇbW¼—pJ:£1Øú_§åä·µÒ:KõJ¥u~ÁØêåPä²¹¡™¦J÷ÞjeBÿ*Þ ¼}ª··h *õ‚<™o¤·+ò½**ýaòQ>·³Zý!3ýìéÛ5`ÈW@ )ëÃÔÍN)½ý°È/:þ¦T«´<§X\¥Þ•§ÕÞò' Qʵ¾Ž©;ÿ½†Z)­MÏ´u‘çÜ`ŒÏZéT·©tI2Û´ D`ω>™Bu< 3BMˆHñ (w{S´Ú"žÀ=Ì„äÆ‚öt›ƒ¬ò2LkŠU)Ï¥„T)@óK°1QÏaÎ\•„bRž.é(½¨ì´0ÀÚt0T^:ÄNê4Ouj„ŽZ††eñ÷×?> ;ÜgÙâ7U¿wDȤ+uû;ü>ÏÐbìRýPì _˜öäOÐQÜûâœÚØÎZ·[¹–]·rµo˳]a¸üè¢,ÿj¶ä¶–-t)Úm»á(‹ß 3ëÛÒÞûƒ6ºéæÒ;Ët%!ˆþfJš§ÖM­ûÄk_5KÏçeD×iÕ ðLµ|E\º í ïFslÎ]:æÑd{kÐm]_Aµ q–¸ÄüôÓ¹Ðå%„!Ü…âˆ9 »›Ìgm4lÒ3ënÎ,Õ’P&Îx~$Š“$íNÔÍhº4 jtÌ]Q†Ã€Q~go[š$y¶¼¥íq'/“&±š’È0Bç‚£Ñ'‰}G² ßCïV.*¸3´#o@ÌM¾ot­ÞÉEßҺ„$‚î›36ÀMÄ¡àUnÿ€Ì|÷•u õY/øËS¨ÆužºSYPµ^7R/ÆLBìSÈù^ÛŒ-:$2Ò ÈÁP£›K%€µ~BAˆYb>.H‰È /ƤýÄÒ‹Ü€ ´ð7($8j©Ñï—«ãÛÄ1›¡Æ>JU ­j:ªÇTjy–¸z㌩ âÆ6”± >çA}Ýnòþ{Gš„›¡L¼ªE bÊ#W”öœçzÆu9,ðÕ}[÷½4 v»PL jƒKBȸãK­.ŽyÏ4 4‚Ç‹“KÜЗÃõ# âè2½Ü‹kÞc ×ÐÏaIH¥œ„_‚%³¢ÎJyiKc.!3SšÝit†<¶œüfGÁg (ÁÂ8¾t3XÕ»mÀå+‡w×tîA¶Â$NÄ,@ZsQÐ %ÑÌ\`g`Š+¸Éd³ ú$&ƒ„͉ø‚LöQÝÐѪ)3}âÊ™Y<•“Û­‡™·½G÷—ð®z>"z½ÿZ ÿnðµ)~¦)¾ÙÎÁµ¤wÆë¾8ùú¼øD-ð3=åI0+tm$@:6¯›¦lâP¸zÍ|cìÀøœðèVÄC,&ØÕâÛö@,ú,=PÞøØí^`ÈÿK½Üt›™»žytZÏ»lxÕÝyŒ#!øÌÝÍí‹,có»‚`3_žwK ¹»xò7ÖãÓf|vGgÉüº€3è¥Îý€æ·­1XäH„éÇÛs\Þzá4Ir`@¨Ë $ÉHÄ eˆB›Ð( ‰KpDƒ(Aaˆ)‰E a¡àˆ¸ÄNžƒ[J˜£JÏ#禺úx8œ1^‚$JÖë ]FIóÏCZËY˜´šü¯…É ¼PدÚ}l˜×Ö¶{j‹k¸¶‰Ç,h¿yÀ¦h$ý2²,T·Ïßæ~fV± ò"ÿ ÍÏ ÿý‚ÍѾ©qœ$àî,vú b8ލ·fÌg0éЯˆBF! :ÃáR!Hd1ÓÈZ&oÎXƒ¡ÑÄoÆ#Ù¯Ó †%²3ò³iYÿŽ~B¡ËÑQÁMp2° Ä/uMZRŒ)ôD…;Ð ¯'0XÏüyƒBØ–lpHÄÍézz3†#¶4îHeÑóö(ÖÚµ·îáë˜þ­ÛšZé³è¡IÈ?:÷t)ûÞ|¥ÿÿ(Fíí!xœíYYã¸~_`ÿƒ }™F,Š—(ÉÛî‚Á"’—dƒy d‰¶µ#‹E·íùõ)ê²,Éž^$3È ãFw[Uţ꫋ÔóOç}á¼J]åª\¹aבeª²¼Ü®Ü¿ýò³¹Ne’2K UÊ•[*÷§—ï¿{®^·ßç8 /«e–®Ü1‡¥ïŽº@Joý,õe!÷²4•OñÝ|z•OµLLþ*Sµß«²ª‡–ÕCimzñÓé„N¬–"qû˜ú”z áU—Ò$go4ö97–bŒ}à Dß(¶¬À8øíå;ªÔQ§r%*¥ñßÿò¾gze&Γ—ª49È›u;bc†d/«C’ÊÊïèͧ<3»•Kqó¸“ùvg®Ï¯¹<ý^W.v° !¿~k¥® “†’g+”Ú§vÉåÐ;uÞÉH¤Qˆ£0^8Sâaâ‘`á¤ÇʨýS3ºÓ{™©Ôê±rµÝd™h­N¨7i¿ˆ<”6Þ&/d#îïÔ^ú™WªôßËWY¨ƒu&ÿ $ÚøyªÊ¹‘èPޙXÌs/÷ŲŸ3¹©j¹Æö‘ºŽß0{…ìö2kâè:©ZhçlÁ™ ¥Wî›úÓqÖJgRwsåŒñ$»Êɇ­‡}®{æ/æMõ|u<è="çã·RøßpƒoMñƒ¦øn;Ç’Öç}ñêëÓà3µÀz Âc:)tu$@;6­›¶l¢@8zM|£ï|Žyx¯â1l¶øÖ= ¿HÀ7úv»7ùi—»n3q׃Në±Ë³îÎ# Á'înO_#Á›ž6ñåi·ã§Ñm”¿\>mûk§þÛM2Ÿw8…^êÖHÿyÐ#" ¿Ï~úÁ §M’)œ’Ç$C1'u´y”„Ç ŒBBÃØ Dp$j wð“g‡q<4ç§foÒ‘¹ºÄÙ\W&ÑîØ–iÆ›Í#“öc8Ž•ê_ÇDËI°Ôö|»qoµ¯[Ü`poÒ«Q5ïOæXuË ³½~ÆtNÀz19Žƒ3šØwuŒÖoVçøÔ^p AìdF Ñ[bû~„8ŒÅÜ.Afñ¬5YÃéxâM‘«èä¼—¨S‘åã9fRfª}¹…GNڈ؛THaH‚@#ÆÌ^=Fœ…V Œ`³ÈÈ5ÝϽMLzÂ?—WíŽ}zË:çÚxŒá B®÷ù6[<ÛÛtøÿoè“~±Ñ öxœíYÝ£È?éþĽì(Ðt7MÓøÆsJ´:]¤ä%¹(Ò½DÚ6·@#hÏØû×§š/cƒ=³Jv•U–ÑÎ@UõGUýê£{:¹õ,ë&SåÚ&Û–,•fånmÿãן]a[ŽË4ÎU)×v©ìŸž¾ÿî±yÞ}ÿeY0¼lVi²¶÷ZW+Ï«uŽT½óÒÄ“¹,d© âÙùä,ŸÔ2ÖÙ³LTQ¨²i‡–ÍSé:ÝŽâ///èÅo¥HE¦¥.H¸Í©Ôñѽ û\K1Æð&¢o[5`œ þò5êP'r %*¥öÞÿú~dº¥:Γ•š$®äź±3C\ȦŠÙx½›à%Kõ~mSÜ}îe¶Ûëó÷s&_þ¤Žk[Ø çœßz©³ÓIGÉÒµ ÊŠþ«_r5E¢Ö;)x"B,Âȱ(¦ÄÅÄ%c%‡F«â¡=è½JUbôXÛuœfjsÐZ•ÿ:”É^&dŠFãŽËÉc¥jín³\v½½*¤w’Y£Jï½|–¹ª ¬¼*ƒ¹¼¸Ö^–À¤y¦%ªÊóÓ \ñeîià>öc*·M+×Å|RÛò:樚Ù^jŒ=ÝÄMï$˪âÀ:WõÚþaÛ>g£êTÖ·Ï%Oß3}êBr˜Ø´™xÀ7š}œª@ÅŒûQ©bm3Ž"AD8c'€€+°0Ì)› Àªb!|̸àðƒq{(3 QUçêÚHäñI‚Ú?djöêeW[êú gc_²Ôrû kß‹ aA0žëÐ˘ ¹Å;Ýáñ1+²vIf2Fƒ©¶q~Åm«´piâި¸N¯¶v9d©lnX¦)ãÊÝlLà/ò Ë­b½¿5A+Pª7¬àÊt'Ý"K+••úuñ7IÞ[Ym~—‰¾»ûvX²ˆBÂz]º)”Òû×U~Óöw¹ÚÄù¥Ä6Ó•z—•®VÕOF.·z™Swø]bmäÑb  -DîÁ`ŒÏZéX·‰wirÛyˆÀ~¤eé“)SÇ“!Ú#Õ„¨¡D!;eQ™’Õ¶âLîi&Üå,¾OgÜÓ27]åu4˜*×d›\^j (c ¦×dã¢~„Ùsž•ÊI~º–S`ô¬œ¦…Ö¦ƒ¡xó"Ð1 ©ã4Öñ¤$ ¤`´2´+«¿½ÿùiXá1IVÿTõ‡qEË2"ñFÀÿöÓ™þ˜&+h0ŠX?eä Óœüú‰Gï̸”6¾›ÌÛÍ\Ë®WYìÚÒ¤ÈÌ(ïï:Ëó?›e½'Óf:—Oí²Ý먋×+3(ëMµ}ôktŸ»ktæñFBýÅ”kžZwµ:TÄk_5ì‰/ˈ®ã²11†×<Öòv\è4/~ð0ºcw iÁ³ï'Ó€mëìø*c@qÈüÈÁæ§ÿ |Šf„  n:TPÄÅþÃÙ}“…†¥ráÝÝ…§Z‚ƒÈ¾ Ï·DP NI·£î‹ˆÀ!!ò)c‚9®O|PŸ°‡é‚°¤I’ÓO¬=®ä&Ò$VS}DÂÀ¾Ñ蓉ľ'Y‘¡{ËW%œÚ7w``Ä g !‰~lt­>ÈUßaܺ‚8„Þ›ùþ@7 ›ZÊtJü2ó%à+ëê³^±–ÆPë:>u»šPÕvÛH½7pV¢Š!ç»m;¶ê˜–ÑÆ‚ 5º¹6xë¯ ™Ç-¨å „ÛGH7t(4^´¥ÿb…­´õÛõlÆùÆ7Bø3ÖØG©Œ¢UíBGõëC-/Wïœ1UAܘÀ†2–ÀsÔËP¸9ö?ÛÒY¹Ë$ÌE+Nhb`<¼`´{Èu tLð ¾-|¯Ý_ƒß® SƒÙHp>㸶ðhà#Á„˜Y hˆ8¡/¢kÞЗûd$^§·ñxÛó_BÙ,B¿„'!•26 ~ žL²:Éåµ/» „¸ 3WšÝidÆ<¶#ÙÍ é (îBÐ¯Ý “êÝ6àòË‚‡%›»­ŸHë.6!8œ¹ üL(â_à]6 ªÏâ2HØ ó¯ÈeŸÔ '5eæ£Ï\9#<‹Ç¡r²iëa¾ÛÞ£ûYW=Ÿ-za}üV ÿ0øÖßiŠo¶sp,éÁ¸ŒÅ3Öç-Àgjïô@„EtVèÚˆƒvþ¼nš²‰Îáè5ÃÆØófá­ˆˆG>÷‹oÛùáé8ðŠÿaؽÁ‘ÿ—v¹ ›\/Ö}È‹pg…œ³ÜÍé‹aÄ}ߟŸ&6Ãò¼[ŠðÃÕm”·›\>íÆk§ñí"™/ƒœ@/u‰2¸êÁó GÄy@>ÝŸãô“N“$!RÌ%Ã$C.|+±´y”„GF!¡ad"Xð–âœYØÁÓä9ø8ššóµ„9šôMæ¢Qm·o6… cAIð5J{cšë(„&ÚñÆ¡ 4°þhqA¢Cßß,Üþ@€Æ…p¾#vž–#ŒÜ“ÍÂ…QøÖIÏ{ýÍ2:8ÌŠ­™Ê GXg|ë†YÜáw¸î«l÷¿eOP¹t‘:Üõ÷©“ÏÉ-í®Ûà 埌ë.<šërøûo\ãq{ *7xœíZÝã¶ÿAu^nQ‹")R¢ïhA´/mŠ})d‰¶•“EA¢×öýõêË”eïmšÝ´Û;îÖš93¿ù ÷–ßw¹ó(«:SÅýŒ SlQƒgKø;È÷T«}•È5,”¨Ú{ÿÓûéb”êÔÖÓ;v´ïÈÛE¼“u'²özz«à¥z{?£¸}ÝÊl³Õç÷ÇLþ¤Ž÷3ì`‡#‘ ØùS'uF i)Yz?cE÷Öm¹1Š(=iˆ(ñ‰?w(&‘‹‰K:¥½¹‹T%æø 0û(7UV¢Áƒby,U¥Ýu–ËVÖÛªôN2«Uá½—2W¥Wf(q¥½,QÅ¿óLKT7ôÓ‚×¹§žû`ØËT®ëF®5ß¼Ò™ãµÌÁs¼Ô¸Õ]ÅuÇ)ã 8WÕýì›uóôœ•ªRYõ¼ yÆ<Îô©ÍÜ^h£xÀ7êmœªÄÂý¨ÔFè`HðS6eÂ^ DF|º"»7Aq÷E¦!kÊãTÁ¾ªŒDŸ$˜Ýü ½T½UxPW{9Y{È 0Æí N":µ¹éaO0žÚÐɘ$¸Å;=ÁÛÅÇlÈ…S’‰Œ±Àöû:ÎÏP¸í•$[™|ÕJÅUz±°ñË>Ke}Ã3u—îjeû*ß°Ü2ÖÛ[ B=cW¦éî²´TY¡?-þ,ɧvV«Ÿe¢Ÿ<}£ö€j¢P>-]ï”ÒÛO›ü¬ãorµŠó±Ä:Ó•j“®V¥…'‹‘˵¾Î©Zü^c­”Ö&u§m bÃà¡•Xž% ¹ºUŽ£O¦ÃO†8¨&û % Ù™(w¥é6ÍØ ÎäŽf2 Ñ€qáûtÂ=]ç¦`†¼ºiPu¶Ê娗p€"jzI6ÞïV˜3çY!¡?ä§K9þÌ ;ã{Z“é}Q÷¦U½e줎ÓXÇVïI|ð2L‹¿½ÿþ¡ßa™$‹ªêð£ã‘x¥öÚÙÙ¾L“Ì»X?d;(f®ø#ŒKïÌK›ØYz[Í•lÇŒ«Wšì2³Êû»ÎòüG³Mo·¥6Ó¹´¨K¯³¡·Ñ³\z½Ú×Í% óx%!-þbм3-–›JíËd`×f–{ÇAWqQG˜ÀÂÇ&ÓwxîÂÔ‚B,|~7Da3F²`á9ä–pi•ßAßä‡ÌæØüé^¹?‡©,„r°90ÜŠý»sÔ¬ú­8u3 P#B0f#òôHE‘(iOÔ¾Áç$D>eL°¹ óâÔ'ìÎÞ¶4eo¤Þòö°“›HS*M“ó ùìbE­O&»ÙbA¾…),_pAh>¹=#ù‚†$ú¶Ö•ú Ý€ƒqGh[5â¦eæû=Ý$*j(R›ø3ÔÚ1P+«:®^°ž–ÆÐ_«*>µ§²¨j½®¥^ 8QÆPÅÝf¬Z´LÇXã@U…®[_:¢õW‡räG晃•(¨ã „›GH7œS ÚÐp8Fa#íüëR› ¾‰þ„5LFª§hU¹0#=Æz_ÉQ½ê‚3T(È“ÏИxƹ| 7×þ¶#›°L¼êE‹ÆáÞðÜF¦N{ ¹®._àÛÀ÷2üÄíÂ1¸CóYÀÅ¥×€G¹bâQè›! ˜}]òúIÛçP‘DxYÞ† iÇÁ5ô˜ÃŠpŠõWˆ$”RÆÁ·É$«’\^ÆÒ„ R(tJsGë3L˜Çf%»™Ä!ý]*ð¹ô­‡ÁêÞ•Òf(qÌ"W|îBµBXDÁ$Ašpð Áá$\gBQ—ñ)ï²IR½JÈ `3¼¡ýªièhõ”IŒ^¹sFx’}çdöèaÞ›Ù£ý³¶{>:.ÌÂùø¥¾ ¾ ÅO Å7Ç9¸–t`¼ŽÅ3Ö§#À+ÀOÌ@„EtÒèš(ëüiß4mñ €«×à Ág˜…·f ÆQùµù63þ.3‡ ¯ø†Ý3ùYúå&l&p!&­§!˯ ›ÀÝܾFïûÓ»‚•`,O§¥ß]| åm¬/Ÿ6Ã×NçQ1¿p³Ô¤¿>ò`Gœüúxê­/6M‘„HqÀ(™3 E2 „ï$1’âhŽQHh9œ#‚EÐP|0ϱ]<ûG¶;?U0—Ž3熻hFëõ³ÝE ºÂ§óÿ^¦Lý#ìï–;ôc¸rÚ¿Ö´ßò¸`–®æ_Ô7E5]!â(b$·<;’±õµÎ‚çy'xâàgC­þùÑϳâ%0 ÷Ãæ—+þ€YjJ¼}'½ž¿³ Áì›´ðJ(_¤Î$8Å£¢Ýa–ÀüÌ?Oϲ—ð¬˜qq¥ƒ=éÙF†ÁÂÈëWƒ/ìmZx%”/ÒÁnaÖ‡7fô³ô,‘:{k6è3}øŸ悵4ÿï~þÚ:Gg´ mxœíYYã6~ÿ U^¦±ER¤D9î°;`÷%Éb} d‰¶•‘EA¢ÛöüúuY—{:H&È`Çî–ªŠd_’7ß\Ž™õ,Ë*Uù£M¶-™Ç*Ióý£ýŸŸ¾u„mU:Ê“(S¹|´seóô囿9ŽõÏRFZ&Ö9Õëûü]G…´Þ´.Ö®{>ŸQÚ‘*÷îƒå8°WÏû/¿°, ÎΫu?ÚíšâTfµl»2“G™ëÊ%ˆ¸ö@>¾ÉÇFƒôYÆêxTyU/Í«¯†Òe²ëÅJg¯–"aº˜º”: áT×\Gg²ô\ZK1Æ.ð¢¯[WàÙ~{ùŽ€*u*c¹ƒ…åR»ozÛ3Œ ÷é;:wäí<:ʪˆbY¹½Ùàœ&úðhSÜÜdº?èÛýs*ÏÿP—G[Øâˆ†Ä÷}v»j¥nˆ! %Mm0V´wí‘ë^£"Ø'‰|ÆñVÅ$t0qH»igî:Q±QÿÑÖÑöç8S•D½ûå¥P¥vvi&a÷ ŽÒ½Ê´R¹ûV>ËLAn‘j D¥vÓXå?g©–¨ÈïìwI ˆNè/s¯÷ɰ7‰ÜUµ\c¿¹¥¶å6ÌÞ£^bü:ÝFUË*¢= 8Så£ýÕ®þtœ­*Yv<¿þŒy Bœêk“ºÝþÒfã^ߨQ¢Î€÷½RG }À<1ãÇ€\Lås.œ) èùœ !>™à8§<Õ>Åe¾Á©,D]%˜_ÿ#TuPç}i<©Ë“œ­=§9å´X'!ÛÞŠtø'˜ÌµleL6Üã]_à£KzLßKÐ’ÌdŒCÿï¢ì‰û^©Árñ;YnUT&“…µ_Ni"« ãvnÎvkR|Ñu†å‘>T/ äª>bQ ?Á‘É^:Ç4)Tšë‹¿Jò¥“Õö뵯÷€3 .(”¦KWG¥ôáÃ&¿Jý}¦¶Q6–Ø¥°RîÓÜѪjÀÈäN/sÊÀK¬­ÒÚäð¡5F†0xj$67 È®v•eé«é5—«!Ú=Õ¤Ÿ¡@y¸å±0}§ ÄÜÒL*!ê3.<θ×enfÈ)ÒM«ªÒm&Ǿò¨É”l¼ß®0:gi.¡Qdשœ¦ù0å;Zê]uwçå½a¥Ž’HGƒbß‘xïe˜9Ö?¼ýö©;aÇëÿªò]¢e‘h«NZûéFß$ñ¦„c¤ŸÒ#Ô3aü†‚{cŒ¥Mìû6;—²8G¯$>¦f•û£N³ì{sLg÷`ÛTgr@ݸ­ îÐÈÛ9¡¹ÝO™E[ iñ/Så­yµÜ—êT!ÛF`Ü;î ºŒòÊ8Â.3˜Qßà•ó °ðøC…ýÉ‚·¶—–éå 4PN1tÂp…ÍO{˽Ìg!&”Ÿ­ "F(önQÔÅÉ(¨ûQ€j‚yhÈs• CáSÒhÔÜÁW$@eL°•“âÔ#ìax iÊÞhû·û“œXšRiºœ‡HÀíÉŠJ_M¶CÆš| ãX¶ÎáQ¡¾r:F,ô Høu¥KõN®ÛIã–ÐôjÄÌÍÌó:ºITPj È“!ñ¨µc* V–´\½f-‰ Á–etm´PÕnWI½î¸QDPÅz¾Z7LËXcAU…¶[MÑú·E9òBóY•Ȩå„ëN°¢ÌG´¦gqŒ‚ZÚúßt7|!¼«TNѪt`HzŽô©”£zÕ§¯P7&Ÿ¡1Åðçò2î®ý}*ÝŒ›±L\ôâ€Æá â;x.™;í%ä:º 6ø ß¾Óð—·‰cJp‡ç1Ÿ‹©×€G¹‡bæQè›ò™<Nyݨíq¨H"˜–·þÑ´å  =FY̱þ" ¥”±^ðSˆdœ–q&§±4á‚ò…Ò<¤u™FfÌK½’ÝMâ€þ) å{\ú©‡aнK¥ÍPâ0˜E|î@µBX„þ,AêpÁ#7&8˜… âL(ò¹¿À»…l–T%dP°ö?¡ý¦iè2è)³}äÎâY>v“ Gs_ÏÍ_Ìšîùl90{ëýçVøGÀàóPüÂP|wœƒÇ’ŒËX¼a}>|¤ø…ˆ°Î]=ù`7m"îûðè5ÃF?AðfÁ½ˆq䇞ï-6ßzò‚?eâæÕï_v¯äÿ¥_îÂf×â`Òz²|îL À÷Ù îæé‹aä{ž7V$Ø Ëói)Ä“—Pî~ðòiß¿vê¯FÅ|àf©1Hþa4ºÈƒ¡ïsòÛãÙo?x±iŠ$7_¡øŒ’ÃP$_xVlóÍ (W„¡Å9"Xø5Åã>³ð ‹gãpèÎÌÞ¥‹îjœDà Üíú¤ÝöŽáí„Ӝ؞´ž¥Díµ?Î……‡˜¾¢!ÌÀ"¤ÌòGÍÛ1ÌWÍe8vU=Sÿw»êÕ-©?”щþ y€Â@Ð^}n9­ÒÐ3[ýQý«‡§Kn’qcÞQÃÿ_uÀb · [xœíYYãÆ~7àÿ@Ð/;°Øì‹dS`a$@ü’8—€"[½› [#i}ªy‰—´c$ëd‘åbgȪê£ê««{ž¼3ëU–UªòM¶-™Ç*IóýÆþÛ/?9¶*åI”©\nì\Ù?¾|ûÍsõºÿö˲`x^­“xc´.Ö®[œÊ ©rï&±+3y”¹®\‚ˆkäã›|\ÊH§¯2Vǣʫzh^}7”.“]/~>ŸÑ™ÕR$ CS—R$œêšëèâLÆÂ>—ÆRŒ± ¼èÅÖ§€ÿ½|G@•:•±ÜÁ@‰r©Ý÷¿¼ï™F‰N†ó¤ù‡*Ž 9Z·#6fˆŽ²*¢XVnGo&8§‰>llŠ›ÏƒL÷}û~Måùê²±±…-Ñø¾Ïoo­Ô tÒPÒdcƒ²¢ýj—\½ëÛÆT ÂvÞÊ¢˜b‡VV|ª´:>5£;½×‰Šû5Ks‰zcöÓËK¡JíìÒL6‚îA¥{•i¥r÷½|•™*Œ¹Eª•ÚMc•ÿ3KµDE~g¾KRD¡¿Ì½vÜÃ~N䮪å#˜Oj[nÃìU1ÛKŒq¢Û¨jA±¬"ÚƒgªÜØßíê§ãlU™È²ãùõ3æ)À9Õ×&»ù»M›‰{|G :D‰:ƒ̸•:nl&öCFùŒƒ«N!¤æ\³)ް Ô f\€÷dÀqNyª!†ŠË|‚SY‰,ºJP/X¿HuPç}iì¨Ë“œ<§9¨ä´îNB:×¼éB€`<ׯ•1qw}À;F—ô˜~”°K2“1 ­¿‹²›CÜ·Ií*åVEe2XÛå”&²ºc™* g»5A¾È7,§ˆôáÞµ@®Þ°‚#“½tŽiR¨4ן“䣕ÕöW뇻¯ç€5 3($§OKWG¥ôáÓ*¿iûûLm£l,±K5¸J¹OsG«bàOF&wz™S6þ»ÄÚ*­MÏ´v‘GnÐGg©t¤ë$Ž›”yí6 D`;Ò²ôÕ”¤ËÕížjBÔP€߈òX˜òT· âFni&Üõ¹'£3îu™›€®r ¦¢Ué6“c-ayÔdJ6µ#ÌžMÝR’]§r ŒžæÃ´ÐÑêtÐåw^ÆQê(‰t4(Éë­ ­Éú/ïzéVxŽãõßUù¡_ѲŒH´U'Àß~¹ÑŸ“x ÍÄ1Ò/éò…iD¾‡ÞáÙ½1ÆÒ»Á¼ÍÌ¥lú’Å-‰©åþU§Yö'³L§÷`ÚTgò¥^¶yíuq[e:eÝ¡¶Ïngæs?õÎ,ÚJ¢?›º`ÍSë¾T§âñº±ëÒaì\ú!ºŒòÊXÄ ¯Y¤å;¼r ßAÌ{êáØ]Zðà†ý`°m™^ÞA­õ(8 WØük?=¶‚~.ÄP‰ÏWTPÄ Åìéß`¡n)ŒÐݪEöB{Džo‰ 0>TêzGÍÞŠj;|å0ÂGáOÃaI“$GӬݯäÄÒ$VS"gOFTúj"±íGÖäèܲu§ƒúÍéñ ð‡J—êƒ\·MÆ-¡)ì ˆè³9cÝD,lj 'C⯙ÇTp_YfPŸõšw´$‚j\–ѵÙÕ€ªv»Jêu¿›E9ß©[±uôŒ6ä`¨ÑÕÔ€ÖÏõ ͳ-‘:PË.«~„t‚å>¢5ý–‡QPK[ÿ˜ÎfÀ7ØÁf¬¾‹R9E«Ò~ê5Ò§RŽW NŸª nL`C‹áõ²+Üûïmé¦ÜŒeæ¢4q0^0Ú#ÏuŒër˜à«ûÖî;…¿Ü&†)Ál$8Æ}OL­<ê1$¸3‹B ÏEÀD8åu}9ó #‰`šÞú£lËþ’÷˜ÍŠ`îëŸIH¥œ÷‚_’qZÆ™œbià‚òAiÎs]¤‘óRäwƒ8 ¿K@ùÌ‚~é0 ªwÝ€Ëw÷ž–lî@¶‚#vèϤ†‹€Mfp΄"ßóx7ÈfAõY ƒ„ͱÿAö›º¡Ë ¦Ì0úÌ•3ijxì*'¶æ»î=šŸ˜7ÕóÕr ÷Öǯ¥ð?á_›âMñÝvŽ%­3.ûâÍ×ç-Àgjô@„‡tVèêÈíØ¼nš²‰<߇£×Ì7úÀç˜÷z î!?d>[,¾uĂߥòàÀ+þ‡Ýî @þ_Úå®ÛÌÜuäqÐi=vYoÑݹ@ïó™»›ÓÇÈgŒÍÏ ƒ›ùò¼[ ñÓä6ÊÝ.ŸöýµSÿ6JæËNÇÐKý€tðÑè=Bß÷ÈodzŸ~pÃi’$ä@Š}NÉŠcH’/˜[Ú·ð “g‡q84ç§foÒámØãK¹á­›3@uñæÍá!‚ƒ ÉÊñ8â>ç!|7ÆFIqqVpV/ä¡'VA‚ÇÑýÖ< ÜÁŸ†A¸Û½ƒûoÆý8NnQ=©mLc8HÿÌÛ=]Hwž„&D4”¶º» 4e/äà}p.Fã<þšßÏæž~ÿ Ì2Hú § image/svg+xml ×%KxœíY[£F~”ÿÀ’—i­)ꎻ#펢DÚ}I²Zi_VÊ6L!(·íùõ9ÅÍ`pw’žd’ñhºÍ9§nçûÎ¥èÕ7§}f=ʲJU~o„mKæ±JÒ|{oÿç§oa[•Žò$ÊT.ïí\Ùß<|ùÅêoŽcý³”‘–‰uLõÎú>WÅQ!­7;­‹¥ëG”¶B¤Ê­{g9 …ÁÕãöË/,Ë‚µój™Ä÷v;¦8”Ym›Ä®Ìä^æºr "®=°/ö±ÙAú(cµß«¼ª‡æÕWCë2ÙôæfKGV[‘0 ]L]J°pªs®£“s5ö97–bŒ]Ð L_h¶¬À³üïí;ªÔ¡ŒåJ”Kí¾ýém¯t0Jt2œ§sìhÝ‘·óh/«"Šeåvòf‚cšèݽMqó¸“év§/Ï©<þCîmlaËC4$¾ïóË·ÖêÂÒHÒäކÊö©]rÙbRó$‘/˜¶°(&¡ƒ‰CÚI»ã.›íÄE–j-Kg§Êô½Œ2Ô{´_Cž Ujg“f²æîÔ^ºg™V*wßÊG™©ÂpÉ-R ’¨Ôn«üÿ0¹DE~c¾SRN¡?¯=wÚ£^%rSÕv'Ì#µ-·Qö3ÛKŒ‡¦ë¨j‘±¬"Ú—3UÞÛ_mêO§Y«2‘e§óëÏX§ìTŸ› îæï6m&î ð ƒj%êT˜hß+µ9GS/} |ñÔa2£…5 ýPp2QÖƒsÈS qTœ¦ãei,²è,áôõ¯~¢j§ŽÛÒ8R—9{Ls8“Ó’ž„tzôÖ¤ ‚1¿ecÂâ–îü„nÒ}ú^Â.§0'ºeFÜöJÍ•ŒßÉr­¢2¹Xûå&²ºá™* g½6¡>«7*§ˆôîÖµA®^°‚#“­töiR¨4×Ï›¿Èò©•Õúgë'w_Ïk@~SHQÏ[W{¥ôîù#¿hûÛL­£ll±I5P¥Ü¦¹£U1àÓ@‘Éž×” çTk¥µ‰à)AkŠ iðÐX¬.\í(ËÒgSsNg#´{©‰># ~Ê}aêOÝHˆ‹¸•™HBÔçž`ŒN´çymÇ×D7%«J×™û6G M®ÅÆûí³ç,Í%”‰ì|m\7ŒøNVGz—ÛÝiro{©£$ÒÑ Õw"¯÷2ôËÞ~ûЭ°ŠãåUù®_ѲŒI´V€Ö~¸ÈWI¼„naé‡t©Àt‡æ`å^ckƒÝ`ÞfæR6Çl –ÄûÔŒrÔi–}o–éÎ=˜6Õ™HWn{†îŒîð+·sBó¸½&d­%„Å¿L’·¦Ér[ªC±‡lë€=pï¸0è2Ê+ã,|Í W}ƒô1(À‚yw= Û1“.¦—–éé ”O″pÍ¿öÑc èÓBL¨G|¾ ÚB1»» 6X¨[Ê##P·#€j‚½Ð‰§["( …OI³£æ‰oAÄ(ç‚/è¸GáwÃaI“öFӼݯäÄÒ¤JSä"g_¨ôÙ`Ûb,É×ÐŒeË® õ7§S`ÄC&h@¯+]ªwrÙö9·‚¦Tƒ! æŒur¨°©% O†ÂŸ!׎¥ÀZYfPqõ’w²$‚úZ–ѹÙÕ@ª6›Jêe¿Ë!в¸SwWËFi™ÓXU¡êV×N´þmQ±Ð|pJäèå„ëN° ÜG´–gyµµõ¿ëÙ ø!ØDÕwF*§hU:Ð#=FúPÊQ¾jÁé3ĉg(L1|Ʊl=ÌsÝ{4?1oªç£å@ï!¬÷ŸKáoAƒÏMñMñÍv®%-ç¹xáú´x¥ø‰ˆðN ]Ýùp:6­›¦l"Ï÷áê5áFßøóàVÄ=ä‡Ìg³Å·îXðQz .¼âL»ù—ôËMÚLè:btZOSÖ›¥;(ð}>¡»¹}qŒ|ÆØô®0° —§ÝRˆï®^B¹ÛÁ˧mÿÚ©ÿ6Jæó$€cè¥Æ< üC4:äá¡ï{äÃñì§¼Ø4Ir Å>§dÁ1$ÉÀÌŠ-m%ÅᣀР´<,üZÂ<Ÿ[x‡É³Ã8ºó¹„Ù»tò6Œ…ƒ7XWaeÒ_HPÐpô–¢N}LÊÇQÇ! 7»áßG œWöÌ¥#’ÜÀ“†A¸Ù|žÝ’¿_(Y}íç?Ñ)g! _Lƒ§t‹w5OÍk7Øó„Ÿk;/(Èôcñ™ÑÏ|þ„O9 éËið+øÜ¼x–Î aFƯ|^•ÎþŸÎ]kÑü^™¿¼Áï_:+ÂÅãåxœíYëã¶ÿ ÿƒ |¹E,ФHŠrÖ 8-Ð|iSè—@–h[9Y4$z×¾¿¾C½¬—÷6h/è¡§ÅíJ3ÃÇÌüæAÞã—cî<«²Êt±q ®£ŠD§Y±ß¸ÿøå'OºNeâ"s]¨[h÷ǧo¿y¬ž÷ß~ã8 /ªušl܃1§µïŸÎeŽt¹÷ÓÄW¹:ªÂT>AÄwòÉM>)Ul²g•èãQU=´¨¾J—é®yyA/A-E¢(ò1õ)õ@«®…‰/Þd,ìsi,ÅûÀˆ¾Ql]qNð¯—ï¨Òç2Q;¨P¡Œÿþ—÷=ÓÃ(5épž¬øP%ñIÖíˆ⣪Nq¢*¿£7¼d©9l\Š›ÏƒÊösû~ÎÔËŸôeãb;ш!Øí­•º94”,ݸ ¬l¿Ú%×Ct ê¼SR$2Ä2ŒVÅ”x˜x„¯œä\}|hFwz¯SX=6nrPɇ<«Ì¯YSÔÛµ_I]Nº4Þ.ËU3Æ?è£ò¯*«tá¿WÏ*×'‹(ÿ” Ä¥ñ³D¿æ™QèTܙÀ[‘Xæ^;î“e?¦jWÕr=ì'u¿aöZÙí¥ÖÎÑm\µþqœS¼DçºÜ¸ßíê§ãlu™ª²ã‰úó4¸<3×&»ù»MÛ‰{|G :Ä©~@̸µ>ÂÄ‘ Ä”Ïø‰E Š$‚Áœmw%ŒxÊ\}¶ÞñÎEf žN—ùç²´y|U ÿpÔÉTý²/­!MyV³‘/Y:y-ôID窷"]8ŒÙ=«æ=ÞõÞ1¾dÇ죂]’™ŒÕ`hþ]œßqß&5Vlp¨r«ã2 ¬írÎRUݱLUÄ'o»µ¿È·,ï›Ã½ jB¿aO¥{å³ô¤³Â|ZüM’¯­¬·¿©Ä¼ºûzX²ˆB¢ú´tuÔÚ>­ò›¶¿Ïõ6ÎÇ»ÌTÊ}VxFŸx0rµ3Ëœ²Áïk«±<h ‘×`ÐGg©Mlꄎ›œ‰í6 D`;ÒqÌÕ–§ËÕÝžjCÔR¢Ýˆêx²¥ªnäÜÒê¬Bã2èŒ{]榠«šFƒ­nU¶ÍÕXKØ@5’­‹ÚvÏyV(¨%ùu*§ÁèY1L ­N]ðç a•‰ÓØÄƒzБxoehSÖ{ÿÓS·Âc’¬ÿ©ËýŠŽcEâ­>ƒÿݧý1MÖÐXcó”!_ئä{è#ýc,m}7˜·™¹TM²Ø­¥É1³£ü¿›,Ïÿb—éôL›™\=ÕË6¯½.~«L§¬?ÔöÑï¬Ñ|î§èÌã­‚ ú«­ Î<µîK}>!^7n]:ÜkB?Ä”qQY‹XÃkõ¯<è}4-èݱCZ²ðæûÁ4`Û2»¼ƒbË)Y­°ýi?y°‚Þ.„r"ØŠJŠ¡8x¸¹o°P·'#ïîGžªEæ‘;"Ï·DPIAI³£æ‹H¾"! (c’­¼€ˆÓ€°‡á‚°¤M’£éÖîWòe«-‰"!w'#*sµ‘Ø6$kò´nùº€“Býæu ŒXH’è‡Ê”úƒZ·]Æ-¡)ì ˆCè¹Ytt±°©5 H‡Äß 3©_UæPŸÍšu´4†j\–ñµÙÕ€ªw»J™u¿›§r¾W÷bë†éXmÈÁP£«©À[?;”£ ²Ï ´Dt Ž'®©¼pE™@´¦ÿÙá…µ´ó¯élÖùÖ7R3VßEéŒbtéA?õ›s©F‰«uNŸª nl`CKàõ2îŽý϶tSnƲ sÑŠG Œ‡Œör= ]|…o ß©ûKðÛÄ0%˜C‚ ˜àrj5àQ ɤœY hˆ“a £)¯ëËI†ÓôÖk[¾Kè±›>§'!•2Ö ~ žL²2ÉÕÔ—Ö]BBÒ™+íy®‹42c^ê‘ìn‡ô (p)é—î†Aõ®põÎcüaÉæd+„e$fR»‹€Mgî?Š ¼›ËfAõY\ ›añ¹ìwuC—AM™ùè3WÎÏⱫœlØzØïº÷h~cÖTÏgǃÞC:¿–Âÿ ¾6ů4ÅwÛ98–´`\Æâ ëóà3µÀ¯ô@„EtVèêH€vÁ¼nÚ²‰¸pôša£ïÀù ³ð^Ä8Q ‚Åâ[÷@Aø‡ô@¼òvopäÿ¥]îÂf×â Óz²|îL¢P6ƒ»=}1ŒDó³Â ÀfXžwK~˜ÜFùûÁåÓ¾¿vêßFÉ|ààz©1Hçþ¡7:σ‘œü~öÓn8m’„H±`”¬†$ 8‰C Í£$¤8ZaFçˆ`)jJÀsð “gçãhhÎO%ÌÞ¤ƒ;¨ÖV…Xï8¯²—ÃSáÒUYw›Û^•õŸž€Î–³ú¦,ˆPøzý¦l|Ö[¼Ýó`")˜ˆ$_y’2! G^óÊ^ßÚdE!šá˜ ö¥Î)&öþŒÈ ë©Ã•ý/3NÂ(¨Ç PêäŽl<Ùn±ׂDdÕ½L+\ß3áM.yF¦ßqû3E œã/ÃX˜£¿ùûho‰áï¿­ #^"éxœíÛŽÛ¸õ}ýAyÉ –Ä›$Ò;žŠ`ÑíK»E¾²DÛÚÈ¢+Ñc;_ßCêjɞ̶MÚ`ãA2Ò¹ððÜéyüñ¼/œgYÕ¹*W.ö‘ëÈ2UY^nWîß~þÉã®Së¤Ì’B•rå–Êýñéûïëçí÷ß9Žìe½ÌÒ•»Óú° ‚ñ*|Umƒ, d!÷²Ôu€}¸#út O+™èüY¦j¿WemYËú͘ºÊ6=ùétòOÔRa!D€H@ˆ^})urö&¼°Ï[¼!nDúJ²e Æ9À¿ž¾øµ:V©Ü£ôK©ƒw?¿ë‘ò3×ÉËušä•Üؘ!ÙËú¤²:x³À)ÏônåÔ¼îd¾Ýéáý9—§ß«óÊErBŸEžZªÁé¸äÙÊeyûÖŠ\Ž£Ã'Î[É£”LjÇbáD°‡°‡Ã…“k­ö w§÷2S©Ñcå¦;™~X«ó{û ³÷y +k¿7o/PžªÒÞ&/dÃìÔ^™×ª ÞÉgY¨ƒ ¬àk€$•òT•ï‹\KÿPÞYïœÀi"º½tØ'ƒ~Ì䦶tYÌ+q AöÊ™íeÆÜ#ÒuR·nrœC²…À.Tµrßlì§Ã¬U•ɪÃEösSàù\_š¤ìÖï6mî Ђz—dêq1Ã~Tj¿ròCÁxÍð)ðY£8Äs,ÈŒ}Ʊ Ñ|mpøÑ8Ç;–¹†¬:œç «ÊPÉE‚úöW/¦Þ©Ó¶2–ÔÕQÎxOy Jym `A溷$]Z`„Ø=“$÷p—pûäœïóv9·Ñ`lÿMR !qß*6XlnTk•TÙ„ÑÚå˜g²ž ¹erðÖk“ù7MgPÞ!Ñ»ú%‚RY7 z žÌ¶ÒÛçÙAå¥þ4ù«(_’¬Ö¿ÈT¿¸{»È€r¤P±>M]ï•Ò»O«üªío µNŠkŠM®!Vªm^zZF5Br£ocª&€o¡ÖJk“Ãóµ1òRô Z)h[ÙQSõ ´ Ë@ ¶œŽ£/¦O/èöP“£"b6åþ`z–øna&ß|±SJfØËmlºÊi:˜6WçëB^k (€fS°qQËaö\䥄nR\¦t Œž—ãºÐÁl=èZ@0ï b/u’%:u„öV†yeù—w?=uÓtùwU}è%:Ž!IÖêþwŸøc–.aÂØ'ú)ßCÁ0ÓÉï` x Ä5µñÝhÝfåJ6ÃÊͱ-K÷¹á þªó¢ø£Óé=Z6×…|²b›Ç^— U¦S6kûtÖh^·Óè,’µ„$ú“é μ¶n+u<ì!_Û¶áŽì|ÝGt•”µ±ˆñ0<‰–oу!ȇ酆½;¶×!ÍY<ø~´ ضÊÏo¡Ý†ÅŒŠ2?íkH0ä „Iˆ#¶ œø D÷u¢ ½Ž »½ò”%Á(îx¾%ì Á#‚›5o˜‡ û”0ÆÙ£˜ú!¡˜=Œ‚HS$¯–Y»—ä¥ÒVÓ©ãÐpÔúb2±I–øÞŠe Gûäuä3A9‰±ø¡Ö•ú —í\„P h:;¢†oFi7 ›ZB”Ùø Tæk(„¯¬ hÐzÉ:X–@;®ªäÒìjU›M-õ²ßÀ Ä!šïÙilÙ £5št=5xëÏ }*ÌgZúè@ûÈ~¸ôâa‘O,üNˆüØR;ÿ˜®fœo|Ã9¡úAJ•`­*FªçD+yU¸Zçô¥ òÆ$6´±>×I};îòþg[”›¡LÁ¼iÅ,ôÝ0ÚK‘ë™Ðe°À·ðµá;u~›¦³…Pà(‹B>µàHH}Î8ŸYôlŽ.(S\7˜Ó*§å­?ß¶xÝŠ³Y8…~ OB)e¬'ü<™æUZÈ©/» …"Nf®4Gº.Óð y¶œìnÇä‹$TDCÎÉ×î†Q÷¶¸|ë±ðá–Í=¨V>â"š%ˆu›`ÏÜ~ÆÄÂènpÙ,©>‹Ë `3}E.ûUÓÐyÔSf>úÌS Y>v“GóngæÄšîùìx0{pçã·VøßƒoCñ CñÝqŽ%m0ÞŽÅ!Öç#Àg_˜0dÖèì vtÞ7MÛôÃ(‚£×,6úœÏ‹ïÍ@,ô#A#z³ùÚˆÆ_d áÀËÿÃîŽüMÚånØÌÂõ*â`Òz9dÛáθG›…»9}1äG”ÒùYa”`³XžOK=Ln£‚íèòiÛ_;õOWÅüv€ƒS˜¥®ãwî{£ó<è!¢(Ŀޟýò£NS$¡1‚ A‘Œ#NÔÁ0æ$È1‰…†>F<²FÌA 4.žÅØœŸ*˜½I_g®ÐüÜ1×"b±ÙL,6Í‘úŸÇ¤’³$±vüŸ%‰uö)Á2dApD1GxàA¢˜€±áN¦˜|@hÈ¢ Ì>BcƒAŠ;°›‹BÃÏ–÷tàÀ)+sv#ÖF0Á¡¹jlÄŽ! 'Œv#©Ý¶Ž£#¤ÃŽÐGg+ÂpÑ^Ä·û07òv'˜AÅkvÒ\Ô!Æá1À<ˆVùÐF±ÑÞ²¡–¾àMH«@ ÑØÝr²Æî–„-ºe®A k€Nr»€1|#Ùëv8e·²‹ý;itÕ€~ciÔÙŽ€Áh÷g àóu÷èZ³ÿ ‹Šù¨nÍà!·g¬jÆ.A!²Á{´ºþŠÆ4€Gó üþ&°ú³Š#±xœíYYÛÈ~_`ÿ!¿x±Ù7›òÈ $Æb$/YòPdKâšb dkFr°ÿ}«y‰"¥™qb/bÄ<&«ªªúêèÖýÇ]î=è²ÊL±œ„gž.“fÅf9ûûû}5ó*iœ›B/g…™ýðöûïÍ÷ßyžËj‘&ËÙÖÚý"ö‡2G¦Üiè\ïta«€ ÌòÉY>)ul³˜ÝÎU=´¨^ ¥ËtÝ‹?>>¢GVK‘(ŠLJ}ð«Saã£? û¼6–bŒà D_(¶¨À8{ø×ËwT™C™è5 Ô¨Ð6x÷þ]Ïô1Jm:œ'+>TI¼×ëvÄÆ ñNWû8ÑUÐÑ› ³Ôn—3Š›×­Î6[{~ÈôãÍq9Ãö¢‘RòóS+uv:i(Yºœ²ª}k—\ ш÷š­ªak1÷(¦ØÇ‘ù—*kvwÍèNïEj§Çr¶*ã"ÙúIn*¢Þ¨ý2ú¸7¥õ×Y®›ÁÖìtpÒYeŠà~йÙ;8ûÌ%.m%¦øWžYöÅùŽé\ÉëÜSÇ}ëØ÷©^Wµ\c ÷J0+{ÊuËõ<{r®³úhƒ¤ªf=ÝKe õ“›Ò¯’-„A7ç¡?9òÏ5ÕxÿîG{õÅ«5uß7ý×îá>¨wñy¶ä³Ï¿©ûÀ­yì1àü™:Tl»Š+Ýmqot½Æröj]:ÎÊ”©.;ž¬?—<’ÙS“»ºù;/»‰{|C ÚÆ©y„ð™p?³º@¡œË ?#…\QE¦\X3BBLØt,ÄÅÁ¡Ù?™…ä³?N'h}–Ç' êo”ê§©¶æqS:;Úò '#³TòÛ¶Û[Ô…yÁ ¾N7ÚßeéÞd…}^üE’O­lV¿èÄ>¹ûzXR:ˆBV^ºÚc·Ï«ü¢íor³ŠóK‰uf*å&+|kö< ¹^Û뜲Áï5ÖÊXëx Ð"OÁ ÎÒØØÖÕ÷…à< Dà(ûOŽx™{% ù™¨w{W×ëKÉ-Í…¢’ ÅpO×¹)èªÇÑàZ*[åúRKØ@5“‹ÚnÏyVh¨½ùi,gÀèY1L ­N^Ж‚ih;mã4¶ñ t$Ñ[zºÅßÞýØW¨û$YüÔÎeÉs"ñÊÀÿçJæÊx²€.lÛ·Ùò…ëàþMÔ¦žq)í|7˜·™¹ÔMCwµµM“]æF?Û,Ïÿì–éôL›Y(‹õ²Íã¹b¶ÊtÊCmïƒÎÍëfŒÎ<^i¢¿¸ºàMSë¦4‡ýâu9«KÇl`çšÐ±Ð“UÎ"ÎÃð˜ÇV¿ÆsEbÅÄ]ïŽÍ%¤ϾL¶-³ãk¨µ‚â³hŽÝ·}lp„ Dò9UqB1»;»o°P·” ÞÝ\xª!XD³ òtKE‘’”4;jÞˆs"F9W|î3 Œð»á‚°¤K’Ӭݯä'Ú%VW"¡˜FÔMÒrÖö# òZÝ|QÀ±ª~ò;F5»PÍz]i»è7pVbCÎ÷ëVlÑ0=§9jt56xë¯tT,rŸ9h‰$è@=_!\”öÃ9åÑšþ“'0 kiïŸãÙœóo”bVßE™Œb¡ë…~ê!¶‡R_$®Ö9}ª‚¸q e,ÏeP_‡ÂͱÿÝ–ÎÊMX.a^µâ€&ãá+F{ ¹¾ƒ.‡ ¾Á·†ïØý%ømd˜Ì& Á1.…[íäÎ )®ÔÄ¢P@C$¹ ™ŠÆ¼®/g2’ Çé­¿hùç“Á=n³*œbý xR)ç½à×àÉ$+“\}Y÷X$áD76›;Ïu‘F&Ìc=’ß âþ.%™PŠ~ínTïºׯ}.î®Ù܇l…°Šä$@jw° ÁáÄ]e}p—B^á]6 ª/â2HØ˯ÈeŸÔ 5eâ£/\9#<‰Ç®ròaëáÞëÞ£ù‹yS=<zå}üV ? ¾5ÅO4Å7Û98–´`¼ŽÅ3Ö§-ÀjŸèè¤ÐÕ=íØ´nº²‰„”pôš`£ïÀùóðVÄ’“ìjñ­{ þ.=€¯ú†Ý ùi—›°™ÀõqÐi= Yqî\¡PJ>»;}qŒ$clzVØËÓn)Âw£Û¨`3¸|Úô×NýÓE2¿pp½Ô%Hçþ¡7:σ‘”‚|º?ûé7œ.IB¤XrJæC’ ¥b^âhó( )Žæ…„†‘'"Xɚ„äžãaòì| Íù\ÂìM:¼ {úRnxëæ‹Aоzõæ‹9›6÷A!QáÍ˱çV¾\{H¿±¸D‘…sŸGP¬©wÏݼ©ñÉóÚÅœá˜KÛ«@…Cb%æ¾Dœ²ÈqB‰86’ã›·Û)ªÅ¤pßÛ $°'“ä³:X;É=5ê”åZ?_0ĤT˜ßFqËß»1àÿßW=D îxœíY[£È~_iÿb_¦SÔ¼í^)­)yI6Š”—CÙfSNQnÛóësŠ›1`O¯’e”q«»Í9§.çþUñüÓy_8¯RW¹*W.AØud™ª,/·+÷o¿üìE®S™¤Ì’B•rå–Êýéåûïž«×í÷ß9ŽÃËj™¥+wgÌaéû‡£.Ò[?K}YȽ,MåD|w Ÿ^åS-“¿ÊTí÷ª¬ê¡eõÃPZg›^üt:¡«¥HÇ>¦>¥HxÕ¥4ÉÙ…}Î¥cxÑ7Š-+0Î~{ùŽ€*uÔ©ÜÀ@‰Jiü÷¿¼ï™F™É†óäå‡*Mòfݎؘ!ÙËꤲò;z3Á)ÏÌnåRÜ<îd¾Ý™ëók.O¿Wç•‹ìˆÆDÁ¯ßZ©«ÓICɳ• ÊFíS»ärˆ:ïd$Ò(ÄQ/Š)ñ0ñH°pÒceÔþ©Ýé½ÌTjõX¹™:•‰Öê„zƒöKÈóAiãmòB6ÂþNí¥‘y¥Jÿ½|•…:ØPò¹J¢Ÿ§ªüg‘‰åùÎÙÜ‹yî¥ã¾Xös&7U-×Â>R×ñf¯ŽÝ^f <]'UëÇ9$[åBé•ûæþtœµÒ™ÔOÔŸ[ž_çæÒ¤a7·i;q/€ïT»Ì ‘0á~Tj¿ryˆbFã);…h!1Gá –ŒQÅÇñ„ >ZßxÇ27F‡ót‚£ÖV¢H.´¯ÿ‘NªÚ©ÓV[C}”“±§¼¼6æIL§ª·"]Œù=›÷x—¼}rÎ÷ùG »$«ÁÐü›¤¸FÄ}«Ô±²“é©×*ÑÙh`m—cžÉêŽeª29xëµÍôY¾ey‡ÄìîMP ”ê +x2ÛJoŸg•—æÓâo’|´²Zÿ*Sóp÷õ°”'… õiéj¯”Ù}Zå7m[¨uRÜJlr¡¢·yéuÄÓ€QÈ™çè&~çXkeŒÍài€Ö!ò( úüÔÊ$¦®ä¸©yPØ®Ó@¶#Ç\l_:_,Ñí©6E-%ù•(÷Û£j¼]É-ͦ¢‚ct½Ìs3ÐU޳Á¶µ*_òVKØ@™5“­‹ÚvÏE^Jè%Åe,§Àèy9, ­.]ð§ aì¥I²Ä$ƒ~Б‚ÞÊ€O–yÿóK·Âsš.ÿ®ô‡~EDZ"ÉZÁÿîË•þœ¥K@ûļä{¨üijeÜJ[ß æmfÖ²'³0-K÷¹åÿÕäEñG»L§÷`ÚÜò¥^¶ùÚëâ·ÊtÊúCmŸýÎÍãvE²–D²-Á™–Ö­VÇÃòµíîÀηmÄ褬¬E¬‡ák‘ù/<=Ð žzwloC:âáÕ÷ƒiÀ¶:?¿ƒfPr/°ýi¶PcB"ø‚FqB1{ººo°P·T@n¼»½ñT-Bp»7äé–ŠãHPÒì¨y"Q° !b”óˆ/·I= wÇþg[º*7aÙ‚9kÅ-@Œ‡gŒö(r=º&ø¾uøŽÝ¯Áo#Ãh0[ŽqDc« E<Š&…"Á£Eñ˜×ár@EŠÂqyëϳ-?sÑc7 §Î/áI(¥œ÷‚_ƒ'Ó\§…ûÒº RHDtâJ{¢ë2L˜çz$¿›Ä!ý" %XEôkwà{×\¾óxð4gsªÂpÊž$Hí.6!8œ¸ üL(˜á]]6IªÏâ2(Ø‹¯Èe¿ =eâ£ÏÜ9c<ÉÇ®sò!ô°Ï5öhþbÞtÏWÇì9¿µÂÿF|Å@ñ]8Ç’6çcñëSð™ ð DxL'®Æ@´cÓ¾iÛ& „€£×$6z Î瘇÷0ˆ™`³Í·Æ@,ü"(€oô?vopäÿ¥]î†Í$\o"Öã fÃG(‚OÂÝž¾8F‚16=+ lËS´ã§Ñm”¿\>mûk§þÛM1Ÿpp Xê6Hçþ¡7:σ±ùíþì§ÜpÚ" 5bÁ)Yp E2sR‡Ì£$¤8^`ÆN ‚#QSX ¸ƒxX<;ÇCs~ª`ö&™«+œÍpeíŽmÙXÆa¼Ù<2Y`ß7†ã\©þuL´œ$KmÏ·÷Vûâƒ{“^ªy2Ǫ!7g{ýŒéœ€bs0:g4±ïê­ß«Îñ©½à‚Ø%ÈŒ@¢·Äâ~„8ŒÅÜ.Afñ¬5YÃéxÆÅ›"1VÑÉ%x/Q—"ËÇs̤ÌTûr ‚´7±7©P*Â8F"Œ™½zŒ8 ­ÁV‘Qh>ºŸhf8´ãf¯_a`‡%òô–éjL†EP*=€ý½½­ ÏöÖþÿÅóxÇ ª image/svg+xml – txœíYYãÆ~7àÿÀp_v±Ù›lÊš1, H^lòPdK¢—bdk$í¯w5ïCš#YÇ‹¬;#VU_õÕñ5góíå˜Yϲ¬R•?ÚaÛ’y¬’4ß?Úÿøù;GØV¥£<‰2•ËG;Wö·O_µù“ãX-e¤ebS}°~ÈßWqTHëíAëbíºçó¥­©rï>XŽCapõ¼ÿú+˲`í¼Z'ñ£ÝŽ)NeVÛ&±+3y”¹®\‚ˆkìãÁ>6;HŸe¬ŽG•Wõмz3¶.“]on¶tfµ ÃÐÅÔ¥Ô §ºæ:º8³±°Ï[c)ÆØÝÈô•fë <[ÀÿÞ¾ JÊXî` D¹ÔßõJ£D'ãy:ÇNÖx;޲*¢XVn'o&8§‰><Ú7™îzx~Nåù/êòhc [Ñø¾ï ßZ«!bH#I“G+Ú§vÉuoˆQHÌ“D¾cFØÊ¢˜„&i'펻NTl¶ÿhÇ™ª$ê}ØÏ*/…*µ³K3Ùºu”îU¦•ÊÝwòYfª0Ñã©ITj7Uþï,Õùù.IÈ„þmíµÓ>õ&‘»ª¶kÎn©m¹²?ŠÙ^b|:2ÝFU‹…eÑ¢7Så£ýfW:ÍV•‰,;_¦:ð¦úÚ¤m7·i3qo€ïT‡(Qg¡ý ÔÑŒó<Á‰Gù †q<ð瘑¥Võ¢þrvødàqNyª!yŠËr‚SY‹,ºJp@ý«_¦:¨ó¾4¾ÔåI.ÆžÓŽå´‘NBº<}kÒE?ÁØ»gcrážîú‚î]ÒcúAÂ.—þ1'#°‹²!(î{¥—ƒŒßËr«¢2™ ¬ýrJYÍúyT8Û­Ið›®3*§ˆô¡zÉ Wõ7 ú™ì¥sL“B¥¹þ¸ù«,_ZYm‘±~q÷õ°T%0…Âôqëꨔ>|üȯÚþ>SÛ(›ZìR ±RîÓÜѪÔH‘ɾ­)›¾¥Ú*­ë,^쥎‘q<5›Á²«eYúj:Íåj„v/5ég$aà By,Lשéƒĭ̤¢¾Çct¡½ÞÖ&p 9tÓ¨ªt›É©/ayÒd.6ÞoG˜=gi.¡Ud×¹¦ù8å;Yê]}w—¾Q¥Ž’HG£r߉xïe`ëß}÷Ô­°‰ãõ?Uù¾_ѲŒI´U'€Ö~ä›$^G8Fú)=B-0üâÏ@ 6î ˜ZìFó63—²¡7‰WS3ÊýI§YöƒY¦;÷hÚTgr$ݸíº3ºãCnÜÎ Íã~Y´•3UÞZVË}©NÅ2°möȽÓΠË(¯Œ# °ð5†ú¯`/(À‚ñ‡…ý4’… ¦—–éå-´PNqà±p…Í¿ö‘³°³ʉﭨ’C(fj£…º¥8™€ºŸT›ÌC{"^n‰ 0>%ÍŽš'"øŠˆQhÜÞÊž…8eÄ{/Kš²7™~äí~%'–¦Tš.Ç ¸=Qé«IÀ–f¬É7@Ȳu…ú›Ó)€F„LЀ„ßTºTïåºå:·‚¦Wƒ!€5{Œur“¨°©5@žŒ…¿@­J!je™AËÕk¯“%4زŒ®Í®FRµÛUR¯û ‡("¨âNͰÖÒ2§± ªBÛ­æN´þnQŽXh>+8%òá ÔrÂõGH'XQÏG´–oqŒ‚ÚÚú×|6¾ÁF¶PõÔHåà­JHÒs¤O¥œÔ«œ¾BAÞ˜|†ÆÃgšË·CáîØÿlKÃá*S'ozq$ãpøn%K§½¹Ž ]&ø¾uøÎá/·™cJp‡Ç<Ÿ‹¹×@G9CÂbáQè›ò=0ÎuÕf*’æå­¿˜¶záߊ³Y,cý ¥ÔózÃÏÉ8-ãLα4pA ù‚. 4—´.ÓÈBy©Gzw“8 ¿KBùŒ A?wFÝ»TÚÇ.rÃçT+„Eè/¤†‹€Op΄"Ÿû7td‹¤ú$AÁö°ÿAö›ØÐeÔS}âÎâE>vÓSó\sæ'öšîùl9À=„õáK+üo„ÁRü)¾KçàZÒãíXb}I>~/¤‹FWs NÇ–}Ó´MÄ}®^‹Øè9y±Š½àò8òC泛ͷæ@,ø]8‡ ¯ø‡Ý+€ü¿ôËݰY„ë$â€i½²üf¸{¾ï-ÂÝܾ<Œ|ÆØò®0J°E,/ÙRˆf/¡ÜýèåÓ¾íÔ›ó6jèßÐ0w»åÉc1oÿÞ5y{Òzq ëoǸÇsô²ÓN‚˜ù£‡®2„iÀ¹e¸£œóUóm䳞ÞÑQÁüX©ì9uÔ«+l¿¨Gg»g( ¨PÖïÞBÒlZÐvû£pø£c3xjÆì_[YÈÇC™þwåäV(ZS}Dõ{y2²­¯#¡‡‡kò 6NÄ¡Ä^—›¬Ü˜—ÕðûWªÐiek"ñxœíÛŽã¶õ=@þAU^vPK")J"ñhA4/iŠ} d‰¶••EU¢Çö~}©»dÏNÚnÚEÖ‹‘Î…çðÜIÏã7—cn=‹ªÎd±±±‹lK‰L³b¿±ÿöÓ·³­ZÅEç²»ö7O_~ñøDZþ\‰X‰Ô:gê`}_¼«“¸Ö›ƒRåÚóÎç³›µ@WV{ïÁr`æúyÿå–eì¢^§ÉÆnyÊS•Ú4ñD.Ž¢Pµ‡]ìÙ#úd O´Ù³Häñ(‹Ú°õWcê*ÝõäZ¥³o¨0çÜCÄ#Ä §¾*¾83^Ðó/Ay€‘¾’l]ƒeKøßÓw·–§*;`n!”÷ö§·=ÒAnªÒñ:a'r'Ö.⣨Ë8µ×Á›ÎYª› æõ ²ýA ïÏ™8ÿI^66²¸„ã0 éðÔR ƒH–nlØ,kßZ‘ëž¹œ¸°N‡Œ'>öWA˜;;¸]´Ûî:•‰Vc'‘¼ÛÊËÏæA¤noÎ^€¸”²RÎ.ËEÃãäQxW‘Õ²ðÞŠg‘ËR’Wf q¥¼,‘ÅÏy¦„[wÖ»¤%8‰‡·±×û¤Ñ©ØÕ†®1ƒ~%¶å5È~WZ½T›wDºëÖ-–UÆ{ä\Vû«ùt˜­¬RQu¸Ð|¦8 žÎÔµÉànýNi½pO€îÔ‡8•gˆƒö½”ÇM‘pÊ¢pO X¸K£E^bAfäR†¹.×OŸ´sœS‘)È¢ò²\àTUš"¯¶o~õbêƒ<ï+mIUÄ‚÷œ°)§ yÌÉrï-I—!zF'Å=ÜõÜ1¾dÇì½-—öÑ;ÛçCHÜ·Š “ÕVÆU:c4v9e©¨gˆAn—Îv«3ý¦é4Ê)cu¨_"(¤q“ —àˆt/œc––2+Ô‡É_Eù’d¹ýE$êEíÍ ÊB…ú0u}”R>¼åW©¿Ïå6Χ»LA¬Tû¬p”,G5Bäb§ncª&€o¡¶R)ÃË512ƒ§†âq €ìj¹,K]u˹\5Ðî¡:ý4„GtŠc©Û™#Øna:•\Ò€ù>Y`¯·±)lCÌ#]w¬:ÛæbjKP ˆšÎÁÚú-‡Ö9Ï "¿Îé$Ø3+Æ)ßÁLªwÕÝ[–÷q*NcŠ} z+Ãè±þñí·O„Ç$Yÿ]Vïz‰–¥Iâ­¦c R׿Éò#k÷’œDèš©Ûïâ(°gµºêLl§5þæ²|]ÀÑÁ<9¹”ûŒD˜]«J¾ëväA¨4MQs4õý®3”ZCéø Ý)ÂWT9ô^µ¦,¡ÓVU|m´AånW µî6QÆPÎ3h­¤¥wcAy…þ[ÏÞúÁ"ësýYÁ.Ýö@,‡¹È|˜p¢¡¡K ü;+@nd¨­ÌWÓÎ×¾aÌ_ úI`%+¦¥çX*1)\­súRy£:TŸiRß…»¼ÿ™JÃæ(]0oZq àDñœS–F{)rºø¾&|çî¯Ào3ÃT`¶ œOÀͭ8ø.£Œ-,zÑÓçŸñ9®›¹ý*‹æå­?ª¶xÞŠ­,‹–±þ< ¥”ÒžðSðd’UI.æ¾Ôî‚ Y¸RŸÖºLà äÅpÒ»I‘ß$¡B?`Œ|ênuïJ*=”8f‘6w Z¹ˆñp‘ Æ]l‚Q´pø7 ¸Áe‹¤ú(.ƒ‚MQø ¹ìWMC—QOYøè#wNŽùØuN:=ô»™=šŸˆ6ÝóÙr`ö`Öûϭ𿟇â†â»ãKÚ`¼‹C¬/G€4¿0aÊɢљ(„Ýù˾©Û¦„!½±ÑÏ@à|Šhto¢r?ôo6_3ùÑo2pàeÿÇa÷ Gþ.ír7lá:‰8˜´^Ùàf¸SæFaHá®O_¹¡ïû˳Â(Á±¼œ–8z˜ÝFyûÑåÓ¾¿vêŸ&Åüv€ƒ˜¥¦q€;÷½ÑyöÁÃ0À¿ÞŸýò£N]$¡R‚WA‘ŒBæ[‰…aÌ#8"ˆ¯aq+\ŒXh ~R ­Ð¸xv>æcs~¨`ö&}•¹ønwÇ\vj±yŽÔÿ<Å•X$‰±ãÿ,IŒ#°ëÌ CV„CG>¦àüÀI06<ÃÉû€? á f¡±Á ÅØõE¡ægVË€{:pàœ•Z‡k#ØÁ¡¾jlÄŽ! 'Œv#©ÚVÇÑúƒÆè½u„á ¸j/â[=ô¼ÑS¨x&ÍE=bÌôo7˜(Ö»7¬Z¨¡ï83F-Òl ˆ8oìn8icwCBWÝ2SPÃß “Ü.  ßHv: ç¬üVaÐ'& èw–Fí|€vnÐßd®5û?F0¨ˆêÖE·0sÆ zìâ>D6xo€VÓ¯htxÔ_Àï*û’U&xœíXm£Fþ)ÿ#_vÓt7MÓ8ž‰t·Šéò%Éé¤ûahÛd1 =¶÷×_5o·=;Q²Ñ­nÍ TU¿U=UõÀêÛÓ¾pžeÝäª|t ®#ËTey¹}tÿõËwžpF'e–ª”n©ÜoŸ¾übõ7ÏsþQËDËÌ9æzçüP¾kÒ¤’ΛÖÕÒ÷Ç#Ê{!RõÖp<†ÂàæyûåŽãÀÚe³ÌÒG·SꢵÍR_r/KÝøߨ§ûÔì –©ÚïUÙ´CËæ«©umFs³¥cÐZ‘8Ž}L}J=°ðšs©““w5öyk,Åû ›˜¾ÒlÙ€g+øíjÔ¡NåJTJí¿ýåí¨ô0Êt6gpìlÝ™·Ëd/›*Ieãòn‚cžéÝ£Kq÷¸“ùv§/ÏϹ<þ]]ì`'D4&œsv¹ë­.ˆ!$Ï]8¬èŸú%—£!F1E0O–p§ Å$ö0ñH?épÜe¦R³ýG7ÝÉôÝZ~mod†FwŽ ÈS¥jímòBvcüÚKÿ,óF•þ[ù, U ùU®A’ÔÚÏSUþZäZ¢ª¼3ß)« H1¿­=Ú'£^erÓ´vÌ#u¿Sާ2ÛËŒ{'¦ë¤éÃâ8U² ª~t¿Ú´× Y«:“õ ãí5×)ˆt®Ï]ó›6øŽA³K2uXÚ÷Jíaâ…À"´ô)€…#Ž·µ°fŒ0HÀ"K ‘>˜àx‡2×EÕÉžàPׯ¢HÎŽßþ#ƒU³SÇmm<©ëƒ´Æóåõ'1µÏÞ› i@0±OØÛ˜¤¸§;¿ Û'§|Ÿ¿—°KbÙ˜Lý¿IŠ $î{¥K›õZ%uv5°õË!ÏdsÇ3M™TÞzmý¦Þ¨¼*Ñ»{´¥zÅ žÌ¶ÒÛçY¥òRØüU–/­¬Ö¿ÉT¿¸ûvXª˜Bú°u³WJï>|äWm[¨uRÌ-6¹¨ÔÛ¼ô´ª&xš( ¹Ñ·5u‡ß[ªµÒÚ¤° Ð"Swß®îb/u’%:™ÔúAŽ^æ±üéíwOà «4]þ[ÕïÆǘ$ku€ÐºOù*K—Àö‰~Ê÷P Ïø¨ÁÊ¿(æÖ&v“y»™kÙÑŽ›,K÷¹åÿ¬ó¢øÁ,3œ{2m® ùÔ.ÛÝŽgñûà ‡õ§§]ùƒ7ºÇí52‹d-!?þiª½cWÍm­ÕR±oîÄÏó¡ë¤lŒGL„á¶Êú/< 3(‚Ž>ŒáØÎ!-.ýj6 ø¶ÎOo ‘†G,ˆØüôa°ºcBCÂÙ‚ `=„âàá¾ÉBÃR!™Ew;‹TkBp»3±½%‚âXpJºuOD„ èÝeL°…Ä …4 ìaº ,iêßlú‰·Ç•¼Tššiº]€HºW#}6™Ø“%ùhY±,áÍ¡½óF,p‚ø›F×ê\öŒã^Ðõl0ÄÐhƒÜd,lj (³©ð7(ºs)ÀWÖ´^½dƒ,K ÑÖurîv5‘ªÍ¦‘z9nàrˆ*rîµ8c©‚¼1‰ *…kžÔ·¡pwìÛÒåp–ÊÌ›^œÈBx¡ø^Sl§½„\Ï@—ÁŸáÛÂ÷:ü5ÄíÊ15¸-„0Šk¯Ž†LË£Ð@#Ä™ˆ_ëÊ„P‘Dt]ÞÆ7Õ^/ø-ô˜ÍŠÈÆúGˆ$”RÆFÃO!’i^§…¼Ž¥ ¤Ô ¥yY2XÊS;’ÝMâˆþ% ŃPú©‡aÒ½k¥ )ñp‘>÷ Z!,bn%H.>!8²Âq&ñßÐ]Bf%ÕG l†ù'²ßņN“žbÅè#wÎ[ù8tN6¥æ¹åÝ_̺îùìxÀ=„óþs+ü3`ð™¿@ŠïÒ9x-éÁx‹¬Ûà#Qà8a1µ]Ë8œ.°û¦i›(ä^½,lŒ‚Ï0‹îq "<¸Ù|[D  á…WüÃîü¿ôË]ØXp!˜ÖË o qÎ,¸›·/†‚À~W˜$˜…e›-Åøáêk”¿||ÚŽŸÆ»Y1¿ p \jŽ2„!òpŽ˜óüþxŽÓO¾pš" 5bÎ(Y0 E2â"pR‡Í£$¢8^`ÅN"‚o%AÈ™ƒxZ<‡ÇSw~¨`Ž6ÛVæ³"üÿ/×+µL "KxœíY[ã¶~ÿÀ*/;ˆ%ñ&Šr<´] о¤) ä%%ÚVV ‰Ûûë{¨›eKöLÚlÐAׯîHç’‡çú‘^|{ÜæèY•U¦‹G‡xØAªHtšëGçŸ?}çJU&.Ò8×…zt í|ûôå‹?¹.úk©b£RtÈÌýP|¨’x§Ð»1»¹ï/k‰ž.×þr] ƒ«çõ—_ „`í¢š§É£ÓŽÙí˼–M_åj« SùÄ#¾3OÎò‰Õ {V‰ÞnuQÕC‹ê«¡t™®zq«ÒÕR$Š"SŸR$ÜêT˜øè^=§ÆRŒ±¼è+ÅæXvÿzùŽàUz_&j•W(ã¿ÿé}Ït±—št8OgØ‹u/¬]Ä[UíâDU~Go&8d©Ù<:7¯•­7æüþœ©Ã_ôñÑÁ£À£BðóS+uŽÒP²ôÑÍÊö­]rÞ b/¢Gï‚42Ja3D1‰\L\ša݆ç©Nì2N3½Ü£‹_’J>¨ô—¬€Y×›¶_Lwº4î*ËU3Úßè­òO*«tá¿WÏ*×;Tþ.ƒ ý¸4~–ÀÌyf”·+nÌwLwà°HLsO÷ɲ©ZUµ\cûJä7Ì~V½Ôšz ºŒ«ÖEíâ5u®ËGç«Uýé8K]¦ªìx¢þ\ò4x=3§&›»ù;¥íĽ¾!PmâT &FÜZoy2ñaÉOp­BãåÀÑ{ëw_dÒhwÜ—¥•Èã“‚=×H'Umôa]Zó™r¯FcY;qÛ˜'kЊty@0 oÉídl•– ;ã·Ìm|̶ÙGš’‘ŒÝÅÐð«8?ÇÂmËÔQR'E¹Ôq™^ ¬m³ÏRUݰNUÄ;w¹´Ù>É·,w›Í­ jB¿bW¥kån³t§³Â¼,þ*É{+ëå¯*1wµ¯ç€5 D(T©—¥«­Öfóò–_¥þ:×Ë8¿”XeB¥\g…kôRt‚‘«•™æ”M O±–jèvÀëu©CdOÄâ, ÖŽBÈœlÛ9ž,Ñé©6-% ù™¨¶;Û‚j,!Ïä–f{ŒG$ctÄ=MsS؆ºtÛµªl™«K[‚E Ôôšl­ßް:çY¡ Aä§k9 öÌŠãÀhÍj×Wu\ÖÆV™8M<(ò)è­ ðcþãûI2ÿ—.?ô+"dEâ¥Þƒk§3}‘&s ÛØ‡o¾×î/ÁoW†)Ál8ÆE ¯­<0Or)G…¾z‚ËÉèš×¡m@E’áuyëO©-_Š©è±ÊÊpëŸÀ“PJ9ïß‚'“¬LruíKë.H!!éÈ•ö€Öe1õH~3‰Cú‡$”`”ô­»aнKm,(qyð0esª•‡e$F R»‹€MGî?ê‰@LðÎ.%Õ'qlŽÅrÙoBCÇAOùèwÎò±ëœ|=ì{=šÿ1oºç3r{Hôñs+ü=Âà3(¾ŠoÂ98–´Á8‹çXC€Oï` Â#:jt5°;6î›¶mzpôÅFÀùóðâ'"&Ødó­1 ÿ ÀWþ‡Ý+ùi—›a3 ׋ˆ¤u?dƒÉpçÒ …à£p·§/Ž=ÁŸ 6Šå1ZŠðÃÕ%”¿\>­ûk§þ题O88,u¤sÿÐça‘ùíþì§\lÚ" 5bÁ)™q E2’¡€y”„G3ì…„† `)j Gx†‡Å³óq44çK³7éëÌØï«ÍEAa,) ÞB¢Ô×8ÏÀk&   ?#áEPáÃÍú'„ë/HábÏ,¯ïHô$¼Ç·J¤|ílg%FµÖÅ(ðêf±YÿÔÈ#1w¸î‹l÷¿fâpêê´»Ôo¯N¯ƒ{Ù‹H®+Tü'‘|uþú}brŽ„Ðð›aÖ‚$ ¢úHï5Æ\/ÖçʈCUád€HÊúçd+KØù‡[_ögøûoÐØÝ˜L"”xœíÛŽã¶õ=@þp^v‹âM7g<ÚE°’—6M¼²DÛÊÊ¢+Ñ3ö~}u¥,yvín²èz03ä¹<÷Cúþûó!G²¬2U¬“’E¢Ò¬Ø­ÿüù'\ JÇEçªëE¡ß?|ýÕ}õ¸ûú+„°Õ*MÖ‹½ÖÇ•ëOeŽU¹sÓÄ•¹<ÈBW.ÅÔ]XôÉ@Ÿ”2ÖÙ£LÔá Šªf-ªolê2ÝöäOOOø‰×T4Š"—0—1(œêRèøì\ñÂ9çx!ÄœEúB²UÊ9ÂoOßp¥Ne"·À(q!µûúç×=Ò!8Õ©½NV¼­’ø(GûvÀF ñAVÇ8‘•ÛÁ›ž²Tï× Fšé^f»½æ™|ú‹:¯äaQß÷Å0j©£Ó’¥ë¶³v˕혡W2ô“0 a-#Œ:„:Ô[¢äTiu¸k¸;¹W©JŒëE²—ÉÛ:ÿv*ê¡Lq¯Ù~/y>ªR;Û,— —»Wé^dV©Â}-e®ŽÆ§Üc¦—ÚÍUü–gZâcqc½sz{Eþ<öÒa ú>•Ûª¦k4b¦lÜÙËeŽ—M[¤›¸j-„Ð1ÞOçª\/¾ÙÖŸ³Qe*Ëçן1NÑ3}iâ±[¿;´Y¸' 7ª}œª'p‰ öR‡õ‚{˜>õ£ >¿a˜…,ð>Åž!¦Ì'˜ ÁÔ'cçTdâéxžòŸÊÒPäñE‚ôõ?ÚQU{õ´+"uy’Þ§¬™œÖùiĦ¢·$]@PB¦§liLxÜÂ]žÁâsvÈÞI8%Ð lõoã|ðˆÛZ©}¥Ž‰r£â2½b¬õrÊRYÝÐLUÄGg³1!?‹7(çëý­j‚B½`G¦;é²ô¨²B¿ŸüE”Ïí¬6¿ËD?{úz ØòBªz?uuPJïß/ò‹Ž¿ËÕ&ÎÇÛLƒ«”»¬p´:Zþd!r¹Õó˜²ñß9ÔFim"xê µ‹<ç}|–JǺNé¤Éy؆e [N„ôŨóÅ=Ô„¨DC@HަXÕC8€[˜ 7Ì|á…œ³ ö2MAVy ¦¾UÙ&—c)áE ÐôlLÔr˜3çY!¡–ä—k:JÏ ;-t°:tÀV€q:Nc[õ y½–¡QYýýõÝ÷I²ú—*ßö;"dHâ:ýü>MVÐZbý _˜¶ä[è$îÝ1¦6¶³ÖmV.eÓ¥ÌökirÈ —ûåùßÌ6ÜÖ²™ÎåC½m3ìeq[a:a][Ú{·ÓF3Ý]{go$Ѧ$ ijÝ•êt<@¼¶Ucaéy\Ft•ш±0 óXËWdé@÷ƒ¡máÞ]oŽÝØ¥C ¶·–Ý–Ùù[AáãÑ’˜Ÿvêñ%tw¡Ì£¾XBéÄ‚2ÂïóYu[ytdÝÝÈR5 %^´§G¢8ŠBŸÑæDÍŒ†Þ’˜3!B±t8åØcœŠ;{CØÒ$ÉÑò–¶ûœDšÄjJ"Ç4ðW•¾˜Hl’ýZ·|UÀ]¡9‚`qh*hô]¥KõV®Ú®ˆÐv $tÝ‚ón"µ(Rø;dæ1ÜW–9Ôg½,¡—e|iNeAÕv[I½ê0qŒ!ç;u/¶jÈHƒ C®®•Öú 1óÈ|– %öA†œ“úJ'X2ácVÃß à ¦F¿^¯fŒol†|‚êû(U€R´*è¨c}*å(qµÆéSÄ l(c |ÆA=ï 7yÿ»# ÂMP&aÎjÑ‚yX€òȌҞó\Ǹ®€¾¸oí¾×æ/ÁnWŠ)AmpGð¸ð½ðZk€cÇ¡ÉF¡€ØaÀÃè×õåpûðX\§·þbÛâCÎ{Ìaáúù), ©Tˆžðs°d’•I.¯miÌ!ä‡lbJs¡ë"NçšSÜ â€}’€ò¹†ìs7ƒU½ë\¾r„w7§s²&aäO¤6PLÌv¦ ûž?ƒL6 ªb2HØ‚øŸ‘É>¨:[5eb£\9#2‰Ç®r »õ0óº÷hþÑTÏGä@ï¢w_Jáÿ ¾4ÅÏ4Å7Û9¸–´Î8¯O[€Ô?ÓQ±I¡«{ ¤ãÓºY?¡z¾W¯‰oô=_Üꄇýˆû|¶øÖ=>IäÁ…7ü»Ý ù©—›n3qבÇA§õ¼Ëz³î.Bø¾˜¸»¹} ‚}Îùô®`ØÄ—§ÝRDî®^£Üõø´ëŸúÑ(™Ï;8^jì´3¿mÎò Gäûýp{öË[/œ&IBdÄŒ.$ø!G ¢Ðæ10- ( "äy˜’Я!Üó"Kb'ÏÎÆ‘­Î÷%Ì^¥ãȹ©®6ÆÓÇ ‹‚h»½ÒÙu”Tÿ>Å¥œ„I­É?,L:åy¾ýªÝƆy­áu»7 º°˜ÃÕM<æ¬þÆu›¢ ÷ÊÀ²PY?›û™YÅ2ÈKü·Uø¼ÿ~Ææ¨ßÔŽ"pw.9ú+â8 #¨·f|ÙÏ`ÜÓ¡_…ŒB8t†cI} D‹™Öº0y3bé †¿÷d¿ +–À:LÏχeaü+ú yKöˆn‚=ƒm ~éÒ¤UŸ@ €1…ž¨¡Xv4Èi Ö1Þ ¶ sº–ÞŒáˆ5Ͳ§²èE}ë mî­»û:¦}붦ÖCú$zhä‰Î=Mʾ7_iÀÿÿ4`ê”ÝÜxœíYëã¶ÿ ÿƒ |¹E,ФHŠrÖ 8)Ð~ISè—B–h[9Y4$z×¾¿¾C½¬—÷6h/è¡§ÃÞJ3ÃÇÌüæAîã—cî<«²Êt±q ®£ŠD§Y±ß¸ÿõ'OºNeâ"s]¨[h÷ǧo¿y¬ž÷ß~ã8 /ªušl܃1§µïŸÎeŽt¹÷ÓÄW¹:ªÂT>AÄwòÉM>)Ul²g•èãQU=´¨¾J—é®yyA/A-E¢(ò1õ)õ@«®…‰/Þd,ìsi,ÅûÀˆ¾Ql]qNðÓËwTés™¨ T¨PÆÿëûžéa”št8OV|¨’ø¤FëvÄÆ ñQU§8Q•ßÑ› ^²Ô6.ÅÍçAeûƒ¹}?gêåOú²q±ƒŽhD„ìöÖJÝœNJ–n\PV¶_í’ë!:uÞ))bF+‡bJ>4£;½×©N¬79¨äCžUõí—P—“.·ËrÕû}TþUe•.ü÷êYåúd¡äŸ2”¸4~–èâ_yf:w滤'pS$–¹×ŽûdÙ©ÚUµ\cûI]Ço˜½:v{©5ð@tW­cçïʹ.7îw»úé8[]¦ªìx¢~Æ< ¾Î̵ ÃnþnÓvâ^ߨqª_ 3îG­0±D$1å3~bá‚"@‡ A0gÛ] $#†rÆŸ­w¼s‘¤Óe>Á¹,­D_è¿u2ÕA¿ìKkHSžÕläKV€N^‹yѹê­HcvOƪyw}…wŒ/Ù1û¨`—d&c5šç7DÜ·IªÜê¸L'k»œ³TUw,SñÉÛnm¤/ò-Ë;Åæpo‚Z ÐoXÁSé^yÇ,=é¬0Ÿ“äk+ëío*1¯î¾žÖ€ô¢¡>-]µ6‡O«ü¦íïs½ó±Ä.3•rŸžÑ§žŒ\íÌ2§lð»ÄÚjclÏZCä5ôÑYj›:“ã&çAb»MØŽtsµuérµD·§Úµ”(d7¢:žlªûy#·´:«PÁ¸ :ã^—¹)誦Ñ`ËZ•ms5Ö6PÄ@M§dë¢v„Ýsž jI~Êi0zV ÓBG«ÓAWüyhGeâ46ñ t$Þ[ú“õ/ïzêVxL’õ?tù¡_Ñq¬H¼Õgð¿ût£?¦É:Šclž²#ä Û| Ä£cŒ¥­ïó63—ªiNÛ´49fv”ÿ7“åùŸí2Þƒi3“«§zÙæµ×Åo•é”õ‡Ú>ú5šÏýy¼UD±uÁ™§Ö}©Ï§#ÄëÆ­K‡;°sM臘2.*këaxÍc£Þá•M‚n%à½;öcHKÞ|?˜l[f—wPl9Å! ¢¶ÿÚO¬ ©‹0¡œ¶¢’"F(nî,Ô-ÅÉÈ»û‘§j‚yäŽÈó-ERPÒì¨ù"’¯Hˆʘd+/ â4 ìa¸ ,i“ähúµû•¼DÙÄjKb€HÈÝɈÊ\m$¶ Éšü­[¾.àˆP¿y#’†$ú¡2¥þ ÖmW„qKh ;âšmÝF,lj (Ò!ñ7ÈÌc*ÀW•9Ôg³f-¡—e|mv5 êÝ®RfÝoà¦Ä)†œïսغa:Vr0ÔèjjðÖ_ÊQÙgZ":PÇ“×T^¸¢L ZÓv8Fa-íüs:›u¾õ”ÁŒÕwQº£]zÐO=Çæ\ªQâjÓ§*ˆØPÆxÆA½ …»cÿ³-Ý”›±lÂ\´â€Æã᣽†\ÏB—Á_á[Ãwêþü61L fãà&¸œZ x”H2)g…"ÁdÈhÊëúò€CF’á4½õçÙ–/Åzìf‡†ÏéIH¥Œõ‚_‚'“¬Lr5õ¥u„tæJ{žë"̘—z$»Ä!ýCJ\Jú¥»aP½ë\½óX²¹Ù a‰Y€Ôî"`‚Ù»ÀÏ„"ÁÅïæ²YP}—AÂfX|A.û]ÝÐePSf>úÌ•3³xì*'¶ö»î=šÿ1kªç³ãAï!_Ká_›âWšâ»íKZ0.cñ†õy ð™ZàWz Â":+tu$@»`^7mÙD\8zͰÑ÷@à|†Yx¯b‰(Ábñ­{  üCz ^ù? »78òÿÒ.wa3ƒëqÐi½Y¾w&Q(›ÁÝž¾F"‚ùYa`3,Ï»¥?Ln£üýàòiß_;õo£d¾ pp½Ô¤sÿÐçAHN~¿?ûé7œ6IB¤X0JV C’ … œÄ!ÐæQR­0 #‡sD°5%à‚9x…‡É³óq44ç§foÒÁTk«ÆB,‰wWÙËá©p骬»Ím¯ÊúOO@gËY}SaD(|½~S6>ë-Þîy0‘LD’¯<I™€„£ ¯ye¯om²¢ÍpÌûR‰ç{Fd‡‡†õÔáÊþÉŒ“0 ê1E‚”:¹#[O¶›pìu  Yu/Ó ×wãlRßF†§QívSüÁ)Ð2Œ„9ö›ßöŽ~ÿˆ¢ g6ostinato.org®ÃÃthemes›ÿômaterial-light8yprimary—ŸÔdisabled NvÇbranch-end.svg lS'downarrow2.svg Ê'slider.svg"u'checklist_indeterminate_invert.svgœgbranch-more.svg ’‹çuparrow.svg “Gradiobutton_unchecked_invert.svg ·9§toolbar-handle-vertical.svg Dgchecklist_indeterminate.svg Ë çsplitter-vertical.svgSœ'branch-open.svg ‡€§float.svg  'leftarrow.svg (Içuparrow2.svg‡£çcheckbox_indeterminate.svg! ‚u'checkbox_indeterminate_invert.svgµ¸'radiobutton_checked.svg ãtoolbar-handle-horizontal.svg©checkbox_unchecked_invert.svgÙÇrightarrow.svg$Çradiobutton_unchecked.svg AMsizegrip.svg hïGtab_close.svg H §vline.svgUGleftarrow2.svg >Yçsplitter-horizontal.svg ´é§checklist_invert.svg,§checkbox_checked_invert.svg v='branch-closed.svg ¶ÊGdownarrow.svg Qªrightarrow2.svg ˜Ž§close.svgìÜGcheckbox_checked.svg˜WÇbase.svg Œgradiobutton_checked_invert.svguÁcheckbox_unchecked.svg pchecklist.svg0f%+R%6+"~¤“øèšú~¤“ù² µ~¤“øý|qD~¤“øì̼J~¤“øézþB~¤“ùÂÝì~¤“øî®y”~¤“øïNÍ›~¤“øÿÚ^~¤“øòvžÝ~¤“øú‚Õ¡~¤“ù$eã~¤“øã\ö1~¤“ù R–~¤“øäÞQÔ~¤“øõæB~¤“ùöYn~¤“øæ$î\~¤“ùdv~¤“ù$aÝ~¤“øÝ®®¨~¤“ùæ†@~¤“øñHn~¤“ùª}â~¤“ùÔ`~¤“ù ÀV7~¤“ù :i6~¤“ùžH”~¤“øør3„~¤“øßަ™~¤“øü#;~¤“øô"Ž$~¤“ù|@¤~¤“øáºD&~¤“ùÄó~¤“øëš<~¤“ø÷6ê]~¤“øçšÅ8~¤“ù²Ìò~¤“øü|0v~¤“øìÌ{—~¤“øéz½‚~¤“ùÂ2~¤“øí®8Å~¤“øîNŒå~¤“øþÚ]~¤“øòv^.~¤“øù‚”é~¤“ù$%<~¤“øâ\µs~¤“ù RUd~¤“øãÞ ~¤“øõ¥†~¤“ùö¢~¤“øå$­Ÿ~¤“ùd5f~¤“ù$!~¤“øÝ®mõ~¤“ùæE–~¤“øðH-\~¤“ùª=9~¤“ùÔÚ~¤“ù À’~¤“ù :(h~¤“ùžï~¤“øørò¾~¤“øÞŽeè~¤“øûâv~¤“øó"Mx~¤“ù|~¤“øàº\~¤“ù„?~¤“øêšûJ~¤“øöostinato-1.3.0/client/themes/qds-dark.qss000066400000000000000000001543661451413623100203730ustar00rootroot00000000000000/* --------------------------------------------------------------------------- WARNING! File created programmatically. All changes made in this file will be lost! Created by the qtsass compiler v0.3.0 The definitions are in the "qdarkstyle.qss._styles.scss" module --------------------------------------------------------------------------- */ /* Light Style - QDarkStyleSheet ------------------------------------------ */ /* See Qt documentation: - https://doc.qt.io/qt-5/stylesheet.html - https://doc.qt.io/qt-5/stylesheet-reference.html - https://doc.qt.io/qt-5/stylesheet-examples.html --------------------------------------------------------------------------- */ /* Reset elements ------------------------------------------------------------ Resetting everything helps to unify styles across different operating systems --------------------------------------------------------------------------- */ * { padding: 0px; margin: 0px; border: 0px; border-style: none; border-image: none; outline: 0; } /* specific reset for elements inside QToolBar */ QToolBar * { margin: 0px; padding: 0px; } /* QWidget ---------------------------------------------------------------- --------------------------------------------------------------------------- */ QWidget { background-color: #19232D; border: 0px solid #455364; padding: 0px; color: #E0E1E3; selection-background-color: #346792; selection-color: #E0E1E3; } QWidget:disabled { background-color: #19232D; color: #9DA9B5; selection-background-color: #26486B; selection-color: #9DA9B5; } QWidget::item:selected { background-color: #346792; } QWidget::item:hover:!selected { background-color: #1A72BB; } /* QMainWindow ------------------------------------------------------------ This adjusts the splitter in the dock widget, not qsplitter https://doc.qt.io/qt-5/stylesheet-examples.html#customizing-qmainwindow --------------------------------------------------------------------------- */ QMainWindow::separator { background-color: #455364; border: 0px solid #19232D; spacing: 0px; padding: 2px; } QMainWindow::separator:hover { background-color: #60798B; border: 0px solid #1A72BB; } QMainWindow::separator:horizontal { width: 5px; margin-top: 2px; margin-bottom: 2px; image: url(":/ostinato.org/themes/qds-dark/rc/toolbar_separator_vertical.png"); } QMainWindow::separator:vertical { height: 5px; margin-left: 2px; margin-right: 2px; image: url(":/ostinato.org/themes/qds-dark/rc/toolbar_separator_horizontal.png"); } /* QToolTip --------------------------------------------------------------- https://doc.qt.io/qt-5/stylesheet-examples.html#customizing-qtooltip --------------------------------------------------------------------------- */ QToolTip { background-color: #346792; color: #E0E1E3; /* If you remove the border property, background stops working on Windows */ border: none; /* Remove padding, for fix combo box tooltip */ padding: 0px; /* Remove opacity, fix #174 - may need to use RGBA */ } /* QStatusBar ------------------------------------------------------------- https://doc.qt.io/qt-5/stylesheet-examples.html#customizing-qstatusbar --------------------------------------------------------------------------- */ QStatusBar { border: 1px solid #455364; /* Fixes Spyder #9120, #9121 */ background: #455364; /* Fixes #205, white vertical borders separating items */ } QStatusBar::item { border: none; } QStatusBar QToolTip { background-color: #1A72BB; border: 1px solid #19232D; color: #19232D; /* Remove padding, for fix combo box tooltip */ padding: 0px; /* Reducing transparency to read better */ opacity: 230; } QStatusBar QLabel { /* Fixes Spyder #9120, #9121 */ background: transparent; } /* QCheckBox -------------------------------------------------------------- https://doc.qt.io/qt-5/stylesheet-examples.html#customizing-qcheckbox --------------------------------------------------------------------------- */ QCheckBox { background-color: #19232D; color: #E0E1E3; spacing: 4px; outline: none; padding-top: 4px; padding-bottom: 4px; } QCheckBox:focus { border: none; } QCheckBox QWidget:disabled { background-color: #19232D; color: #9DA9B5; } QCheckBox::indicator { margin-left: 2px; height: 14px; width: 14px; } QCheckBox::indicator:unchecked { image: url(":/ostinato.org/themes/qds-dark/rc/checkbox_unchecked.png"); } QCheckBox::indicator:unchecked:hover, QCheckBox::indicator:unchecked:focus, QCheckBox::indicator:unchecked:pressed { border: none; image: url(":/ostinato.org/themes/qds-dark/rc/checkbox_unchecked_focus.png"); } QCheckBox::indicator:unchecked:disabled { image: url(":/ostinato.org/themes/qds-dark/rc/checkbox_unchecked_disabled.png"); } QCheckBox::indicator:checked { image: url(":/ostinato.org/themes/qds-dark/rc/checkbox_checked.png"); } QCheckBox::indicator:checked:hover, QCheckBox::indicator:checked:focus, QCheckBox::indicator:checked:pressed { border: none; image: url(":/ostinato.org/themes/qds-dark/rc/checkbox_checked_focus.png"); } QCheckBox::indicator:checked:disabled { image: url(":/ostinato.org/themes/qds-dark/rc/checkbox_checked_disabled.png"); } QCheckBox::indicator:indeterminate { image: url(":/ostinato.org/themes/qds-dark/rc/checkbox_indeterminate.png"); } QCheckBox::indicator:indeterminate:disabled { image: url(":/ostinato.org/themes/qds-dark/rc/checkbox_indeterminate_disabled.png"); } QCheckBox::indicator:indeterminate:focus, QCheckBox::indicator:indeterminate:hover, QCheckBox::indicator:indeterminate:pressed { image: url(":/ostinato.org/themes/qds-dark/rc/checkbox_indeterminate_focus.png"); } /* QGroupBox -------------------------------------------------------------- https://doc.qt.io/qt-5/stylesheet-examples.html#customizing-qgroupbox --------------------------------------------------------------------------- */ QGroupBox { font-weight: bold; border: 1px solid #455364; border-radius: 4px; padding: 2px; margin-top: 6px; margin-bottom: 4px; } QGroupBox::title { subcontrol-origin: margin; subcontrol-position: top left; left: 4px; padding-left: 2px; padding-right: 4px; padding-top: -4px; } QGroupBox::indicator { margin-left: 2px; margin-top: 2px; padding: 0; height: 14px; width: 14px; } QGroupBox::indicator:unchecked { border: none; image: url(":/ostinato.org/themes/qds-dark/rc/checkbox_unchecked.png"); } QGroupBox::indicator:unchecked:hover, QGroupBox::indicator:unchecked:focus, QGroupBox::indicator:unchecked:pressed { border: none; image: url(":/ostinato.org/themes/qds-dark/rc/checkbox_unchecked_focus.png"); } QGroupBox::indicator:unchecked:disabled { image: url(":/ostinato.org/themes/qds-dark/rc/checkbox_unchecked_disabled.png"); } QGroupBox::indicator:checked { border: none; image: url(":/ostinato.org/themes/qds-dark/rc/checkbox_checked.png"); } QGroupBox::indicator:checked:hover, QGroupBox::indicator:checked:focus, QGroupBox::indicator:checked:pressed { border: none; image: url(":/ostinato.org/themes/qds-dark/rc/checkbox_checked_focus.png"); } QGroupBox::indicator:checked:disabled { image: url(":/ostinato.org/themes/qds-dark/rc/checkbox_checked_disabled.png"); } /* QRadioButton ----------------------------------------------------------- https://doc.qt.io/qt-5/stylesheet-examples.html#customizing-qradiobutton --------------------------------------------------------------------------- */ QRadioButton { background-color: #19232D; color: #E0E1E3; spacing: 4px; padding-top: 4px; padding-bottom: 4px; border: none; outline: none; } QRadioButton:focus { border: none; } QRadioButton:disabled { background-color: #19232D; color: #9DA9B5; border: none; outline: none; } QRadioButton QWidget { background-color: #19232D; color: #E0E1E3; spacing: 0px; padding: 0px; outline: none; border: none; } QRadioButton::indicator { border: none; outline: none; margin-left: 2px; height: 14px; width: 14px; } QRadioButton::indicator:unchecked { image: url(":/ostinato.org/themes/qds-dark/rc/radio_unchecked.png"); } QRadioButton::indicator:unchecked:hover, QRadioButton::indicator:unchecked:focus, QRadioButton::indicator:unchecked:pressed { border: none; outline: none; image: url(":/ostinato.org/themes/qds-dark/rc/radio_unchecked_focus.png"); } QRadioButton::indicator:unchecked:disabled { image: url(":/ostinato.org/themes/qds-dark/rc/radio_unchecked_disabled.png"); } QRadioButton::indicator:checked { border: none; outline: none; image: url(":/ostinato.org/themes/qds-dark/rc/radio_checked.png"); } QRadioButton::indicator:checked:hover, QRadioButton::indicator:checked:focus, QRadioButton::indicator:checked:pressed { border: none; outline: none; image: url(":/ostinato.org/themes/qds-dark/rc/radio_checked_focus.png"); } QRadioButton::indicator:checked:disabled { outline: none; image: url(":/ostinato.org/themes/qds-dark/rc/radio_checked_disabled.png"); } /* QMenuBar --------------------------------------------------------------- https://doc.qt.io/qt-5/stylesheet-examples.html#customizing-qmenubar --------------------------------------------------------------------------- */ QMenuBar { background-color: #455364; padding: 2px; border: 1px solid #19232D; color: #E0E1E3; selection-background-color: #1A72BB; } QMenuBar:focus { border: 1px solid #346792; } QMenuBar::item { background: transparent; padding: 4px; } QMenuBar::item:selected { padding: 4px; background: transparent; border: 0px solid #455364; background-color: #1A72BB; } QMenuBar::item:pressed { padding: 4px; border: 0px solid #455364; background-color: #1A72BB; color: #E0E1E3; margin-bottom: 0px; padding-bottom: 0px; } /* QMenu ------------------------------------------------------------------ https://doc.qt.io/qt-5/stylesheet-examples.html#customizing-qmenu --------------------------------------------------------------------------- */ QMenu { border: 0px solid #455364; color: #E0E1E3; margin: 0px; background-color: #37414F; selection-background-color: #1A72BB; } QMenu::separator { height: 1px; background-color: #60798B; color: #E0E1E3; } QMenu::item { background-color: #37414F; padding: 4px 24px 4px 28px; /* Reserve space for selection border */ border: 1px transparent #455364; } QMenu::item:selected { color: #E0E1E3; background-color: #1A72BB; } QMenu::item:pressed { background-color: #1A72BB; } QMenu::icon { padding-left: 10px; width: 14px; height: 14px; } QMenu::indicator { padding-left: 8px; width: 12px; height: 12px; /* non-exclusive indicator = check box style indicator (see QActionGroup::setExclusive) */ /* exclusive indicator = radio button style indicator (see QActionGroup::setExclusive) */ } QMenu::indicator:non-exclusive:unchecked { image: url(":/ostinato.org/themes/qds-dark/rc/checkbox_unchecked.png"); } QMenu::indicator:non-exclusive:unchecked:hover, QMenu::indicator:non-exclusive:unchecked:focus, QMenu::indicator:non-exclusive:unchecked:pressed { border: none; image: url(":/ostinato.org/themes/qds-dark/rc/checkbox_unchecked_focus.png"); } QMenu::indicator:non-exclusive:unchecked:disabled { image: url(":/ostinato.org/themes/qds-dark/rc/checkbox_unchecked_disabled.png"); } QMenu::indicator:non-exclusive:checked { image: url(":/ostinato.org/themes/qds-dark/rc/checkbox_checked.png"); } QMenu::indicator:non-exclusive:checked:hover, QMenu::indicator:non-exclusive:checked:focus, QMenu::indicator:non-exclusive:checked:pressed { border: none; image: url(":/ostinato.org/themes/qds-dark/rc/checkbox_checked_focus.png"); } QMenu::indicator:non-exclusive:checked:disabled { image: url(":/ostinato.org/themes/qds-dark/rc/checkbox_checked_disabled.png"); } QMenu::indicator:non-exclusive:indeterminate { image: url(":/ostinato.org/themes/qds-dark/rc/checkbox_indeterminate.png"); } QMenu::indicator:non-exclusive:indeterminate:disabled { image: url(":/ostinato.org/themes/qds-dark/rc/checkbox_indeterminate_disabled.png"); } QMenu::indicator:non-exclusive:indeterminate:focus, QMenu::indicator:non-exclusive:indeterminate:hover, QMenu::indicator:non-exclusive:indeterminate:pressed { image: url(":/ostinato.org/themes/qds-dark/rc/checkbox_indeterminate_focus.png"); } QMenu::indicator:exclusive:unchecked { image: url(":/ostinato.org/themes/qds-dark/rc/radio_unchecked.png"); } QMenu::indicator:exclusive:unchecked:hover, QMenu::indicator:exclusive:unchecked:focus, QMenu::indicator:exclusive:unchecked:pressed { border: none; outline: none; image: url(":/ostinato.org/themes/qds-dark/rc/radio_unchecked_focus.png"); } QMenu::indicator:exclusive:unchecked:disabled { image: url(":/ostinato.org/themes/qds-dark/rc/radio_unchecked_disabled.png"); } QMenu::indicator:exclusive:checked { border: none; outline: none; image: url(":/ostinato.org/themes/qds-dark/rc/radio_checked.png"); } QMenu::indicator:exclusive:checked:hover, QMenu::indicator:exclusive:checked:focus, QMenu::indicator:exclusive:checked:pressed { border: none; outline: none; image: url(":/ostinato.org/themes/qds-dark/rc/radio_checked_focus.png"); } QMenu::indicator:exclusive:checked:disabled { outline: none; image: url(":/ostinato.org/themes/qds-dark/rc/radio_checked_disabled.png"); } QMenu::right-arrow { margin: 5px; padding-left: 12px; image: url(":/ostinato.org/themes/qds-dark/rc/arrow_right.png"); height: 12px; width: 12px; } /* QAbstractItemView ------------------------------------------------------ https://doc.qt.io/qt-5/stylesheet-examples.html#customizing-qcombobox --------------------------------------------------------------------------- */ QAbstractItemView { alternate-background-color: #19232D; color: #E0E1E3; border: 1px solid #455364; border-radius: 4px; } QAbstractItemView QLineEdit { padding: 2px; } /* QAbstractScrollArea ---------------------------------------------------- https://doc.qt.io/qt-5/stylesheet-examples.html#customizing-qabstractscrollarea --------------------------------------------------------------------------- */ QAbstractScrollArea { background-color: #19232D; border: 1px solid #455364; border-radius: 4px; /* fix #159 */ padding: 2px; /* remove min-height to fix #244 */ color: #E0E1E3; } QAbstractScrollArea:disabled { color: #9DA9B5; } /* QScrollArea ------------------------------------------------------------ --------------------------------------------------------------------------- */ QScrollArea QWidget QWidget:disabled { background-color: #19232D; } /* QScrollBar ------------------------------------------------------------- https://doc.qt.io/qt-5/stylesheet-examples.html#customizing-qscrollbar --------------------------------------------------------------------------- */ QScrollBar:horizontal { height: 16px; margin: 2px 16px 2px 16px; border: 1px solid #455364; border-radius: 4px; background-color: #19232D; } QScrollBar:vertical { background-color: #19232D; width: 16px; margin: 16px 2px 16px 2px; border: 1px solid #455364; border-radius: 4px; } QScrollBar::handle:horizontal { background-color: #60798B; border: 1px solid #455364; border-radius: 4px; min-width: 8px; } QScrollBar::handle:horizontal:hover { background-color: #346792; border: #346792; border-radius: 4px; min-width: 8px; } QScrollBar::handle:horizontal:focus { border: 1px solid #1A72BB; } QScrollBar::handle:vertical { background-color: #60798B; border: 1px solid #455364; min-height: 8px; border-radius: 4px; } QScrollBar::handle:vertical:hover { background-color: #346792; border: #346792; border-radius: 4px; min-height: 8px; } QScrollBar::handle:vertical:focus { border: 1px solid #1A72BB; } QScrollBar::add-line:horizontal { margin: 0px 0px 0px 0px; border-image: url(":/ostinato.org/themes/qds-dark/rc/arrow_right_disabled.png"); height: 12px; width: 12px; subcontrol-position: right; subcontrol-origin: margin; } QScrollBar::add-line:horizontal:hover, QScrollBar::add-line:horizontal:on { border-image: url(":/ostinato.org/themes/qds-dark/rc/arrow_right.png"); height: 12px; width: 12px; subcontrol-position: right; subcontrol-origin: margin; } QScrollBar::add-line:vertical { margin: 3px 0px 3px 0px; border-image: url(":/ostinato.org/themes/qds-dark/rc/arrow_down_disabled.png"); height: 12px; width: 12px; subcontrol-position: bottom; subcontrol-origin: margin; } QScrollBar::add-line:vertical:hover, QScrollBar::add-line:vertical:on { border-image: url(":/ostinato.org/themes/qds-dark/rc/arrow_down.png"); height: 12px; width: 12px; subcontrol-position: bottom; subcontrol-origin: margin; } QScrollBar::sub-line:horizontal { margin: 0px 3px 0px 3px; border-image: url(":/ostinato.org/themes/qds-dark/rc/arrow_left_disabled.png"); height: 12px; width: 12px; subcontrol-position: left; subcontrol-origin: margin; } QScrollBar::sub-line:horizontal:hover, QScrollBar::sub-line:horizontal:on { border-image: url(":/ostinato.org/themes/qds-dark/rc/arrow_left.png"); height: 12px; width: 12px; subcontrol-position: left; subcontrol-origin: margin; } QScrollBar::sub-line:vertical { margin: 3px 0px 3px 0px; border-image: url(":/ostinato.org/themes/qds-dark/rc/arrow_up_disabled.png"); height: 12px; width: 12px; subcontrol-position: top; subcontrol-origin: margin; } QScrollBar::sub-line:vertical:hover, QScrollBar::sub-line:vertical:on { border-image: url(":/ostinato.org/themes/qds-dark/rc/arrow_up.png"); height: 12px; width: 12px; subcontrol-position: top; subcontrol-origin: margin; } QScrollBar::up-arrow:horizontal, QScrollBar::down-arrow:horizontal { background: none; } QScrollBar::up-arrow:vertical, QScrollBar::down-arrow:vertical { background: none; } QScrollBar::add-page:horizontal, QScrollBar::sub-page:horizontal { background: none; } QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical { background: none; } /* QTextEdit -------------------------------------------------------------- https://doc.qt.io/qt-5/stylesheet-examples.html#customizing-specific-widgets --------------------------------------------------------------------------- */ QTextEdit { background-color: #19232D; color: #E0E1E3; border-radius: 4px; border: 1px solid #455364; } QTextEdit:focus { border: 1px solid #1A72BB; } QTextEdit:selected { background: #346792; color: #455364; } /* QPlainTextEdit --------------------------------------------------------- --------------------------------------------------------------------------- */ QPlainTextEdit { background-color: #19232D; color: #E0E1E3; border-radius: 4px; border: 1px solid #455364; } QPlainTextEdit:focus { border: 1px solid #1A72BB; } QPlainTextEdit:selected { background: #346792; color: #455364; } /* QSizeGrip -------------------------------------------------------------- https://doc.qt.io/qt-5/stylesheet-examples.html#customizing-qsizegrip --------------------------------------------------------------------------- */ QSizeGrip { background: transparent; width: 12px; height: 12px; image: url(":/ostinato.org/themes/qds-dark/rc/window_grip.png"); } /* QStackedWidget --------------------------------------------------------- --------------------------------------------------------------------------- */ QStackedWidget { padding: 2px; border: 1px solid #455364; border: 1px solid #19232D; } /* QToolBar --------------------------------------------------------------- https://doc.qt.io/qt-5/stylesheet-examples.html#customizing-qtoolbar --------------------------------------------------------------------------- */ QToolBar { background-color: #455364; border-bottom: 1px solid #19232D; padding: 1px; font-weight: bold; spacing: 2px; } QToolBar:disabled { /* Fixes #272 */ background-color: #455364; } QToolBar::handle:horizontal { width: 16px; image: url(":/ostinato.org/themes/qds-dark/rc/toolbar_move_horizontal.png"); } QToolBar::handle:vertical { height: 16px; image: url(":/ostinato.org/themes/qds-dark/rc/toolbar_move_vertical.png"); } QToolBar::separator:horizontal { width: 16px; image: url(":/ostinato.org/themes/qds-dark/rc/toolbar_separator_horizontal.png"); } QToolBar::separator:vertical { height: 16px; image: url(":/ostinato.org/themes/qds-dark/rc/toolbar_separator_vertical.png"); } QToolButton#qt_toolbar_ext_button { background: #455364; border: 0px; color: #E0E1E3; image: url(":/ostinato.org/themes/qds-dark/rc/arrow_right.png"); } /* QAbstractSpinBox ------------------------------------------------------- --------------------------------------------------------------------------- */ QAbstractSpinBox { background-color: #19232D; border: 1px solid #455364; color: #E0E1E3; /* This fixes 103, 111 */ padding-top: 2px; /* This fixes 103, 111 */ padding-bottom: 2px; padding-left: 4px; padding-right: 4px; border-radius: 4px; /* min-width: 5px; removed to fix 109 */ } QAbstractSpinBox:up-button { background-color: transparent #19232D; subcontrol-origin: border; subcontrol-position: top right; border-left: 1px solid #455364; border-bottom: 1px solid #455364; border-top-left-radius: 0; border-bottom-left-radius: 0; margin: 1px; width: 12px; margin-bottom: -1px; } QAbstractSpinBox::up-arrow, QAbstractSpinBox::up-arrow:disabled, QAbstractSpinBox::up-arrow:off { image: url(":/ostinato.org/themes/qds-dark/rc/arrow_up_disabled.png"); height: 8px; width: 8px; } QAbstractSpinBox::up-arrow:hover { image: url(":/ostinato.org/themes/qds-dark/rc/arrow_up.png"); } QAbstractSpinBox:down-button { background-color: transparent #19232D; subcontrol-origin: border; subcontrol-position: bottom right; border-left: 1px solid #455364; border-top: 1px solid #455364; border-top-left-radius: 0; border-bottom-left-radius: 0; margin: 1px; width: 12px; margin-top: -1px; } QAbstractSpinBox::down-arrow, QAbstractSpinBox::down-arrow:disabled, QAbstractSpinBox::down-arrow:off { image: url(":/ostinato.org/themes/qds-dark/rc/arrow_down_disabled.png"); height: 8px; width: 8px; } QAbstractSpinBox::down-arrow:hover { image: url(":/ostinato.org/themes/qds-dark/rc/arrow_down.png"); } QAbstractSpinBox:hover { border: 1px solid #346792; color: #E0E1E3; } QAbstractSpinBox:focus { border: 1px solid #1A72BB; } QAbstractSpinBox:selected { background: #346792; color: #455364; } /* ------------------------------------------------------------------------ */ /* DISPLAYS --------------------------------------------------------------- */ /* ------------------------------------------------------------------------ */ /* QLabel ----------------------------------------------------------------- https://doc.qt.io/qt-5/stylesheet-examples.html#customizing-qframe --------------------------------------------------------------------------- */ QLabel { background-color: #19232D; border: 0px solid #455364; padding: 2px; margin: 0px; color: #E0E1E3; } QLabel:disabled { background-color: #19232D; border: 0px solid #455364; color: #9DA9B5; } /* QTextBrowser ----------------------------------------------------------- https://doc.qt.io/qt-5/stylesheet-examples.html#customizing-qabstractscrollarea --------------------------------------------------------------------------- */ QTextBrowser { background-color: #19232D; border: 1px solid #455364; color: #E0E1E3; border-radius: 4px; } QTextBrowser:disabled { background-color: #19232D; border: 1px solid #455364; color: #9DA9B5; border-radius: 4px; } QTextBrowser:hover, QTextBrowser:!hover, QTextBrowser:selected, QTextBrowser:pressed { border: 1px solid #455364; } /* QGraphicsView ---------------------------------------------------------- --------------------------------------------------------------------------- */ QGraphicsView { background-color: #19232D; border: 1px solid #455364; color: #E0E1E3; border-radius: 4px; } QGraphicsView:disabled { background-color: #19232D; border: 1px solid #455364; color: #9DA9B5; border-radius: 4px; } QGraphicsView:hover, QGraphicsView:!hover, QGraphicsView:selected, QGraphicsView:pressed { border: 1px solid #455364; } /* QCalendarWidget -------------------------------------------------------- --------------------------------------------------------------------------- */ QCalendarWidget { border: 1px solid #455364; border-radius: 4px; } QCalendarWidget:disabled { background-color: #19232D; color: #9DA9B5; } /* QLCDNumber ------------------------------------------------------------- --------------------------------------------------------------------------- */ QLCDNumber { background-color: #19232D; color: #E0E1E3; } QLCDNumber:disabled { background-color: #19232D; color: #9DA9B5; } /* QProgressBar ----------------------------------------------------------- https://doc.qt.io/qt-5/stylesheet-examples.html#customizing-qprogressbar --------------------------------------------------------------------------- */ QProgressBar { background-color: #19232D; border: 1px solid #455364; color: #E0E1E3; border-radius: 4px; text-align: center; } QProgressBar:disabled { background-color: #19232D; border: 1px solid #455364; color: #9DA9B5; border-radius: 4px; text-align: center; } QProgressBar::chunk { background-color: #346792; color: #19232D; border-radius: 4px; } QProgressBar::chunk:disabled { background-color: #26486B; color: #9DA9B5; border-radius: 4px; } /* ------------------------------------------------------------------------ */ /* BUTTONS ---------------------------------------------------------------- */ /* ------------------------------------------------------------------------ */ /* QPushButton ------------------------------------------------------------ https://doc.qt.io/qt-5/stylesheet-examples.html#customizing-qpushbutton --------------------------------------------------------------------------- */ QPushButton { background-color: #455364; color: #E0E1E3; border-radius: 4px; padding: 2px; outline: none; border: none; } QPushButton:disabled { background-color: #455364; color: #9DA9B5; border-radius: 4px; padding: 2px; } QPushButton:checked { background-color: #60798B; border-radius: 4px; padding: 2px; outline: none; } QPushButton:checked:disabled { background-color: #60798B; color: #9DA9B5; border-radius: 4px; padding: 2px; outline: none; } QPushButton:checked:selected { background: #60798B; } QPushButton:hover { background-color: #54687A; color: #E0E1E3; } QPushButton:pressed { background-color: #60798B; } QPushButton:selected { background: #60798B; color: #E0E1E3; } QPushButton::menu-indicator { subcontrol-origin: padding; subcontrol-position: bottom right; bottom: 4px; } QDialogButtonBox QPushButton { /* Issue #194 #248 - Special case of QPushButton inside dialogs, for better UI */ min-width: 80px; } /* QToolButton ------------------------------------------------------------ https://doc.qt.io/qt-5/stylesheet-examples.html#customizing-qtoolbutton --------------------------------------------------------------------------- */ QToolButton { background-color: #455364; color: #E0E1E3; border-radius: 4px; padding: 2px; outline: none; border: none; /* The subcontrols below are used only in the DelayedPopup mode */ /* The subcontrols below are used only in the MenuButtonPopup mode */ /* The subcontrol below is used only in the InstantPopup or DelayedPopup mode */ } QToolButton:disabled { background-color: #455364; color: #9DA9B5; border-radius: 4px; padding: 2px; } QToolButton:checked { background-color: #60798B; border-radius: 4px; padding: 2px; outline: none; } QToolButton:checked:disabled { background-color: #60798B; color: #9DA9B5; border-radius: 4px; padding: 2px; outline: none; } QToolButton:checked:hover { background-color: #54687A; color: #E0E1E3; } QToolButton:checked:pressed { background-color: #60798B; } QToolButton:checked:selected { background: #60798B; color: #E0E1E3; } QToolButton:hover { background-color: #54687A; color: #E0E1E3; } QToolButton:pressed { background-color: #60798B; } QToolButton:selected { background: #60798B; color: #E0E1E3; } QToolButton[popupMode="0"] { /* Only for DelayedPopup */ padding-right: 2px; } QToolButton[popupMode="1"] { /* Only for MenuButtonPopup */ padding-right: 20px; } QToolButton[popupMode="1"]::menu-button { border: none; } QToolButton[popupMode="1"]::menu-button:hover { border: none; border-left: 1px solid #455364; border-radius: 0; } QToolButton[popupMode="2"] { /* Only for InstantPopup */ padding-right: 2px; } QToolButton::menu-button { padding: 2px; border-radius: 4px; width: 12px; border: none; outline: none; } QToolButton::menu-button:hover { border: 1px solid #346792; } QToolButton::menu-button:checked:hover { border: 1px solid #346792; } QToolButton::menu-indicator { image: url(":/ostinato.org/themes/qds-dark/rc/arrow_down.png"); height: 8px; width: 8px; top: 0; /* Exclude a shift for better image */ left: -2px; /* Shift it a bit */ } QToolButton::menu-arrow { image: url(":/ostinato.org/themes/qds-dark/rc/arrow_down.png"); height: 8px; width: 8px; } QToolButton::menu-arrow:hover { image: url(":/ostinato.org/themes/qds-dark/rc/arrow_down_focus.png"); } /* QCommandLinkButton ----------------------------------------------------- --------------------------------------------------------------------------- */ QCommandLinkButton { background-color: transparent; border: 1px solid #455364; color: #E0E1E3; border-radius: 4px; padding: 0px; margin: 0px; } QCommandLinkButton:disabled { background-color: transparent; color: #9DA9B5; } /* ------------------------------------------------------------------------ */ /* INPUTS - NO FIELDS ----------------------------------------------------- */ /* ------------------------------------------------------------------------ */ /* QComboBox -------------------------------------------------------------- https://doc.qt.io/qt-5/stylesheet-examples.html#customizing-qcombobox --------------------------------------------------------------------------- */ QComboBox { border: 1px solid #455364; border-radius: 4px; selection-background-color: #346792; padding-left: 4px; padding-right: 4px; /* padding-right = 36; 4 + 16*2 See scrollbar size */ /* changed to 4px to fix #239 */ /* Fixes #103, #111 */ min-height: 1.5em; /* padding-top: 2px; removed to fix #132 */ /* padding-bottom: 2px; removed to fix #132 */ /* min-width: 75px; removed to fix #109 */ /* Needed to remove indicator - fix #132 */ } QComboBox QAbstractItemView { border: 1px solid #455364; border-radius: 0; background-color: #19232D; selection-background-color: #346792; } QComboBox QAbstractItemView:hover { background-color: #19232D; color: #E0E1E3; } QComboBox QAbstractItemView:selected { background: #346792; color: #455364; } QComboBox QAbstractItemView:alternate { background: #19232D; } QComboBox:disabled { background-color: #19232D; color: #9DA9B5; } QComboBox:hover { border: 1px solid #346792; } QComboBox:focus { border: 1px solid #1A72BB; } QComboBox:on { selection-background-color: #346792; } QComboBox::indicator { border: none; border-radius: 0; background-color: transparent; selection-background-color: transparent; color: transparent; selection-color: transparent; /* Needed to remove indicator - fix #132 */ } QComboBox::indicator:alternate { background: #19232D; } QComboBox::item:alternate { background: #19232D; } QComboBox::item:checked { font-weight: bold; } QComboBox::item:selected { border: 0px solid transparent; } QComboBox::drop-down { subcontrol-origin: padding; subcontrol-position: top right; width: 12px; border-left: 1px solid #455364; } QComboBox::down-arrow { image: url(":/ostinato.org/themes/qds-dark/rc/arrow_down_disabled.png"); height: 8px; width: 8px; } QComboBox::down-arrow:on, QComboBox::down-arrow:hover, QComboBox::down-arrow:focus { image: url(":/ostinato.org/themes/qds-dark/rc/arrow_down.png"); } /* QSlider ---------------------------------------------------------------- https://doc.qt.io/qt-5/stylesheet-examples.html#customizing-qslider --------------------------------------------------------------------------- */ QSlider:disabled { background: #19232D; } QSlider:focus { border: none; } QSlider::groove:horizontal { background: #455364; border: 1px solid #455364; height: 4px; margin: 0px; border-radius: 4px; } QSlider::groove:vertical { background: #455364; border: 1px solid #455364; width: 4px; margin: 0px; border-radius: 4px; } QSlider::add-page:vertical { background: #346792; border: 1px solid #455364; width: 4px; margin: 0px; border-radius: 4px; } QSlider::add-page:vertical :disabled { background: #26486B; } QSlider::sub-page:horizontal { background: #346792; border: 1px solid #455364; height: 4px; margin: 0px; border-radius: 4px; } QSlider::sub-page:horizontal:disabled { background: #26486B; } QSlider::handle:horizontal { background: #9DA9B5; border: 1px solid #455364; width: 8px; height: 8px; margin: -8px 0px; border-radius: 4px; } QSlider::handle:horizontal:hover { background: #346792; border: 1px solid #346792; } QSlider::handle:horizontal:focus { border: 1px solid #1A72BB; } QSlider::handle:vertical { background: #9DA9B5; border: 1px solid #455364; width: 8px; height: 8px; margin: 0 -8px; border-radius: 4px; } QSlider::handle:vertical:hover { background: #346792; border: 1px solid #346792; } QSlider::handle:vertical:focus { border: 1px solid #1A72BB; } /* QLineEdit -------------------------------------------------------------- https://doc.qt.io/qt-5/stylesheet-examples.html#customizing-qlineedit --------------------------------------------------------------------------- */ QLineEdit { background-color: #19232D; padding-top: 2px; /* This QLineEdit fix 103, 111 */ padding-bottom: 2px; /* This QLineEdit fix 103, 111 */ padding-left: 4px; padding-right: 4px; border-style: solid; border: 1px solid #455364; border-radius: 4px; color: #E0E1E3; } QLineEdit:disabled { background-color: #19232D; color: #9DA9B5; } QLineEdit:hover { border: 1px solid #346792; color: #E0E1E3; } QLineEdit:focus { border: 1px solid #1A72BB; } QLineEdit:selected { background-color: #346792; color: #455364; } /* QTabWiget -------------------------------------------------------------- https://doc.qt.io/qt-5/stylesheet-examples.html#customizing-qtabwidget-and-qtabbar --------------------------------------------------------------------------- */ QTabWidget { padding: 2px; selection-background-color: #455364; } QTabWidget QWidget { /* Fixes #189 */ border-radius: 4px; } QTabWidget::pane { border: 1px solid #455364; border-radius: 4px; margin: 0px; /* Fixes double border inside pane with pyqt5 */ padding: 0px; } QTabWidget::pane:selected { background-color: #455364; border: 1px solid #346792; } /* QTabBar ---------------------------------------------------------------- https://doc.qt.io/qt-5/stylesheet-examples.html#customizing-qtabwidget-and-qtabbar --------------------------------------------------------------------------- */ QTabBar, QDockWidget QTabBar { qproperty-drawBase: 0; border-radius: 4px; margin: 0px; padding: 2px; border: 0; /* left: 5px; move to the right by 5px - removed for fix */ } QTabBar::close-button, QDockWidget QTabBar::close-button { border: 0; margin: 0; padding: 4px; image: url(":/ostinato.org/themes/qds-dark/rc/window_close.png"); } QTabBar::close-button:hover, QDockWidget QTabBar::close-button:hover { image: url(":/ostinato.org/themes/qds-dark/rc/window_close_focus.png"); } QTabBar::close-button:pressed, QDockWidget QTabBar::close-button:pressed { image: url(":/ostinato.org/themes/qds-dark/rc/window_close_pressed.png"); } QTabBar::tab, QDockWidget QTabBar::tab { /* !selected and disabled ----------------------------------------- */ /* selected ------------------------------------------------------- */ } QTabBar::tab:top:selected:disabled, QDockWidget QTabBar::tab:top:selected:disabled { border-bottom: 3px solid #26486B; color: #9DA9B5; background-color: #455364; } QTabBar::tab:bottom:selected:disabled, QDockWidget QTabBar::tab:bottom:selected:disabled { border-top: 3px solid #26486B; color: #9DA9B5; background-color: #455364; } QTabBar::tab:left:selected:disabled, QDockWidget QTabBar::tab:left:selected:disabled { border-right: 3px solid #26486B; color: #9DA9B5; background-color: #455364; } QTabBar::tab:right:selected:disabled, QDockWidget QTabBar::tab:right:selected:disabled { border-left: 3px solid #26486B; color: #9DA9B5; background-color: #455364; } QTabBar::tab:top:!selected:disabled, QDockWidget QTabBar::tab:top:!selected:disabled { border-bottom: 3px solid #19232D; color: #9DA9B5; background-color: #19232D; } QTabBar::tab:bottom:!selected:disabled, QDockWidget QTabBar::tab:bottom:!selected:disabled { border-top: 3px solid #19232D; color: #9DA9B5; background-color: #19232D; } QTabBar::tab:left:!selected:disabled, QDockWidget QTabBar::tab:left:!selected:disabled { border-right: 3px solid #19232D; color: #9DA9B5; background-color: #19232D; } QTabBar::tab:right:!selected:disabled, QDockWidget QTabBar::tab:right:!selected:disabled { border-left: 3px solid #19232D; color: #9DA9B5; background-color: #19232D; } QTabBar::tab:top:!selected, QDockWidget QTabBar::tab:top:!selected { border-bottom: 2px solid #19232D; margin-top: 2px; } QTabBar::tab:bottom:!selected, QDockWidget QTabBar::tab:bottom:!selected { border-top: 2px solid #19232D; margin-bottom: 2px; } QTabBar::tab:left:!selected, QDockWidget QTabBar::tab:left:!selected { border-left: 2px solid #19232D; margin-right: 2px; } QTabBar::tab:right:!selected, QDockWidget QTabBar::tab:right:!selected { border-right: 2px solid #19232D; margin-left: 2px; } QTabBar::tab:top, QDockWidget QTabBar::tab:top { background-color: #455364; margin-left: 2px; padding-left: 4px; padding-right: 4px; padding-top: 2px; padding-bottom: 2px; min-width: 5px; border-bottom: 3px solid #455364; border-top-left-radius: 4px; border-top-right-radius: 4px; } QTabBar::tab:top:selected, QDockWidget QTabBar::tab:top:selected { background-color: #54687A; border-bottom: 3px solid #259AE9; border-top-left-radius: 4px; border-top-right-radius: 4px; } QTabBar::tab:top:!selected:hover, QDockWidget QTabBar::tab:top:!selected:hover { border: 1px solid #1A72BB; border-bottom: 3px solid #1A72BB; /* Fixes spyder-ide/spyder#9766 and #243 */ padding-left: 3px; padding-right: 3px; } QTabBar::tab:bottom, QDockWidget QTabBar::tab:bottom { border-top: 3px solid #455364; background-color: #455364; margin-left: 2px; padding-left: 4px; padding-right: 4px; padding-top: 2px; padding-bottom: 2px; border-bottom-left-radius: 4px; border-bottom-right-radius: 4px; min-width: 5px; } QTabBar::tab:bottom:selected, QDockWidget QTabBar::tab:bottom:selected { background-color: #54687A; border-top: 3px solid #259AE9; border-bottom-left-radius: 4px; border-bottom-right-radius: 4px; } QTabBar::tab:bottom:!selected:hover, QDockWidget QTabBar::tab:bottom:!selected:hover { border: 1px solid #1A72BB; border-top: 3px solid #1A72BB; /* Fixes spyder-ide/spyder#9766 and #243 */ padding-left: 3px; padding-right: 3px; } QTabBar::tab:left, QDockWidget QTabBar::tab:left { background-color: #455364; margin-top: 2px; padding-left: 2px; padding-right: 2px; padding-top: 4px; padding-bottom: 4px; border-top-left-radius: 4px; border-bottom-left-radius: 4px; min-height: 5px; } QTabBar::tab:left:selected, QDockWidget QTabBar::tab:left:selected { background-color: #54687A; border-right: 3px solid #259AE9; } QTabBar::tab:left:!selected:hover, QDockWidget QTabBar::tab:left:!selected:hover { border: 1px solid #1A72BB; border-right: 3px solid #1A72BB; /* Fixes different behavior #271 */ margin-right: 0px; padding-right: -1px; } QTabBar::tab:right, QDockWidget QTabBar::tab:right { background-color: #455364; margin-top: 2px; padding-left: 2px; padding-right: 2px; padding-top: 4px; padding-bottom: 4px; border-top-right-radius: 4px; border-bottom-right-radius: 4px; min-height: 5px; } QTabBar::tab:right:selected, QDockWidget QTabBar::tab:right:selected { background-color: #54687A; border-left: 3px solid #259AE9; } QTabBar::tab:right:!selected:hover, QDockWidget QTabBar::tab:right:!selected:hover { border: 1px solid #1A72BB; border-left: 3px solid #1A72BB; /* Fixes different behavior #271 */ margin-left: 0px; padding-left: 0px; } QTabBar QToolButton, QDockWidget QTabBar QToolButton { /* Fixes #136 */ background-color: #455364; height: 12px; width: 12px; } QTabBar QToolButton:pressed, QDockWidget QTabBar QToolButton:pressed { background-color: #455364; } QTabBar QToolButton:pressed:hover, QDockWidget QTabBar QToolButton:pressed:hover { border: 1px solid #346792; } QTabBar QToolButton::left-arrow:enabled, QDockWidget QTabBar QToolButton::left-arrow:enabled { image: url(":/ostinato.org/themes/qds-dark/rc/arrow_left.png"); } QTabBar QToolButton::left-arrow:disabled, QDockWidget QTabBar QToolButton::left-arrow:disabled { image: url(":/ostinato.org/themes/qds-dark/rc/arrow_left_disabled.png"); } QTabBar QToolButton::right-arrow:enabled, QDockWidget QTabBar QToolButton::right-arrow:enabled { image: url(":/ostinato.org/themes/qds-dark/rc/arrow_right.png"); } QTabBar QToolButton::right-arrow:disabled, QDockWidget QTabBar QToolButton::right-arrow:disabled { image: url(":/ostinato.org/themes/qds-dark/rc/arrow_right_disabled.png"); } /* QDockWiget ------------------------------------------------------------- --------------------------------------------------------------------------- */ QDockWidget { outline: 1px solid #455364; background-color: #19232D; border: 1px solid #455364; border-radius: 4px; titlebar-close-icon: url(":/ostinato.org/themes/qds-dark/rc/transparent.png"); titlebar-normal-icon: url(":/ostinato.org/themes/qds-dark/rc/transparent.png"); } QDockWidget::title { /* Better size for title bar */ padding: 3px; spacing: 4px; border: none; background-color: #455364; } QDockWidget::close-button { icon-size: 12px; border: none; background: transparent; background-image: transparent; border: 0; margin: 0; padding: 0; image: url(":/ostinato.org/themes/qds-dark/rc/window_close.png"); } QDockWidget::close-button:hover { image: url(":/ostinato.org/themes/qds-dark/rc/window_close_focus.png"); } QDockWidget::close-button:pressed { image: url(":/ostinato.org/themes/qds-dark/rc/window_close_pressed.png"); } QDockWidget::float-button { icon-size: 12px; border: none; background: transparent; background-image: transparent; border: 0; margin: 0; padding: 0; image: url(":/ostinato.org/themes/qds-dark/rc/window_undock.png"); } QDockWidget::float-button:hover { image: url(":/ostinato.org/themes/qds-dark/rc/window_undock_focus.png"); } QDockWidget::float-button:pressed { image: url(":/ostinato.org/themes/qds-dark/rc/window_undock_pressed.png"); } /* QTreeView QListView QTableView ----------------------------------------- https://doc.qt.io/qt-5/stylesheet-examples.html#customizing-qtreeview https://doc.qt.io/qt-5/stylesheet-examples.html#customizing-qlistview https://doc.qt.io/qt-5/stylesheet-examples.html#customizing-qtableview --------------------------------------------------------------------------- */ QTreeView:branch:selected, QTreeView:branch:hover { background: url(":/ostinato.org/themes/qds-dark/rc/transparent.png"); } QTreeView:branch:has-siblings:!adjoins-item { border-image: url(":/ostinato.org/themes/qds-dark/rc/branch_line.png") 0; } QTreeView:branch:has-siblings:adjoins-item { border-image: url(":/ostinato.org/themes/qds-dark/rc/branch_more.png") 0; } QTreeView:branch:!has-children:!has-siblings:adjoins-item { border-image: url(":/ostinato.org/themes/qds-dark/rc/branch_end.png") 0; } QTreeView:branch:has-children:!has-siblings:closed, QTreeView:branch:closed:has-children:has-siblings { border-image: none; image: url(":/ostinato.org/themes/qds-dark/rc/branch_closed.png"); } QTreeView:branch:open:has-children:!has-siblings, QTreeView:branch:open:has-children:has-siblings { border-image: none; image: url(":/ostinato.org/themes/qds-dark/rc/branch_open.png"); } QTreeView:branch:has-children:!has-siblings:closed:hover, QTreeView:branch:closed:has-children:has-siblings:hover { image: url(":/ostinato.org/themes/qds-dark/rc/branch_closed_focus.png"); } QTreeView:branch:open:has-children:!has-siblings:hover, QTreeView:branch:open:has-children:has-siblings:hover { image: url(":/ostinato.org/themes/qds-dark/rc/branch_open_focus.png"); } QTreeView::indicator:checked, QListView::indicator:checked, QTableView::indicator:checked, QColumnView::indicator:checked { image: url(":/ostinato.org/themes/qds-dark/rc/checkbox_checked.png"); } QTreeView::indicator:checked:hover, QTreeView::indicator:checked:focus, QTreeView::indicator:checked:pressed, QListView::indicator:checked:hover, QListView::indicator:checked:focus, QListView::indicator:checked:pressed, QTableView::indicator:checked:hover, QTableView::indicator:checked:focus, QTableView::indicator:checked:pressed, QColumnView::indicator:checked:hover, QColumnView::indicator:checked:focus, QColumnView::indicator:checked:pressed { image: url(":/ostinato.org/themes/qds-dark/rc/checkbox_checked_focus.png"); } QTreeView::indicator:unchecked, QListView::indicator:unchecked, QTableView::indicator:unchecked, QColumnView::indicator:unchecked { image: url(":/ostinato.org/themes/qds-dark/rc/checkbox_unchecked.png"); } QTreeView::indicator:unchecked:hover, QTreeView::indicator:unchecked:focus, QTreeView::indicator:unchecked:pressed, QListView::indicator:unchecked:hover, QListView::indicator:unchecked:focus, QListView::indicator:unchecked:pressed, QTableView::indicator:unchecked:hover, QTableView::indicator:unchecked:focus, QTableView::indicator:unchecked:pressed, QColumnView::indicator:unchecked:hover, QColumnView::indicator:unchecked:focus, QColumnView::indicator:unchecked:pressed { image: url(":/ostinato.org/themes/qds-dark/rc/checkbox_unchecked_focus.png"); } QTreeView::indicator:indeterminate, QListView::indicator:indeterminate, QTableView::indicator:indeterminate, QColumnView::indicator:indeterminate { image: url(":/ostinato.org/themes/qds-dark/rc/checkbox_indeterminate.png"); } QTreeView::indicator:indeterminate:hover, QTreeView::indicator:indeterminate:focus, QTreeView::indicator:indeterminate:pressed, QListView::indicator:indeterminate:hover, QListView::indicator:indeterminate:focus, QListView::indicator:indeterminate:pressed, QTableView::indicator:indeterminate:hover, QTableView::indicator:indeterminate:focus, QTableView::indicator:indeterminate:pressed, QColumnView::indicator:indeterminate:hover, QColumnView::indicator:indeterminate:focus, QColumnView::indicator:indeterminate:pressed { image: url(":/ostinato.org/themes/qds-dark/rc/checkbox_indeterminate_focus.png"); } QTreeView, QListView, QTableView, QColumnView { background-color: #19232D; border: 1px solid #455364; color: #E0E1E3; gridline-color: #455364; border-radius: 4px; } QTreeView:disabled, QListView:disabled, QTableView:disabled, QColumnView:disabled { background-color: #19232D; color: #9DA9B5; } QTreeView:selected, QListView:selected, QTableView:selected, QColumnView:selected { background-color: #346792; color: #455364; } QTreeView:focus, QListView:focus, QTableView:focus, QColumnView:focus { border: 1px solid #1A72BB; } QTreeView::item:pressed, QListView::item:pressed, QTableView::item:pressed, QColumnView::item:pressed { background-color: #346792; } QTreeView::item:selected:active, QListView::item:selected:active, QTableView::item:selected:active, QColumnView::item:selected:active { background-color: #346792; } QTreeView::item:selected:!active, QListView::item:selected:!active, QTableView::item:selected:!active, QColumnView::item:selected:!active { color: #E0E1E3; background-color: #37414F; } QTreeView::item:!selected:hover, QListView::item:!selected:hover, QTableView::item:!selected:hover, QColumnView::item:!selected:hover { outline: 0; color: #E0E1E3; background-color: #37414F; } QTableCornerButton::section { background-color: #19232D; border: 1px transparent #455364; border-radius: 0px; } /* QHeaderView ------------------------------------------------------------ https://doc.qt.io/qt-5/stylesheet-examples.html#customizing-qheaderview --------------------------------------------------------------------------- */ QHeaderView { background-color: #455364; border: 0px transparent #455364; padding: 0; margin: 0; border-radius: 0; } QHeaderView:disabled { background-color: #455364; border: 1px transparent #455364; } QHeaderView::section { background-color: #455364; color: #E0E1E3; border-radius: 0; text-align: left; font-size: 13px; } QHeaderView::section::horizontal { padding-top: 0; padding-bottom: 0; padding-left: 4px; padding-right: 4px; border-left: 1px solid #19232D; } QHeaderView::section::horizontal::first, QHeaderView::section::horizontal::only-one { border-left: 1px solid #455364; } QHeaderView::section::horizontal:disabled { color: #9DA9B5; } QHeaderView::section::vertical { padding-top: 0; padding-bottom: 0; padding-left: 4px; padding-right: 4px; border-top: 1px solid #19232D; } QHeaderView::section::vertical::first, QHeaderView::section::vertical::only-one { border-top: 1px solid #455364; } QHeaderView::section::vertical:disabled { color: #9DA9B5; } QHeaderView::down-arrow { /* Those settings (border/width/height/background-color) solve bug */ /* transparent arrow background and size */ background-color: #455364; border: none; height: 12px; width: 12px; padding-left: 2px; padding-right: 2px; image: url(":/ostinato.org/themes/qds-dark/rc/arrow_down.png"); } QHeaderView::up-arrow { background-color: #455364; border: none; height: 12px; width: 12px; padding-left: 2px; padding-right: 2px; image: url(":/ostinato.org/themes/qds-dark/rc/arrow_up.png"); } /* QToolBox -------------------------------------------------------------- https://doc.qt.io/qt-5/stylesheet-examples.html#customizing-qtoolbox --------------------------------------------------------------------------- */ QToolBox { padding: 0px; border: 0px; border: 1px solid #455364; } QToolBox:selected { padding: 0px; border: 2px solid #346792; } QToolBox::tab { background-color: #19232D; border: 1px solid #455364; color: #E0E1E3; border-top-left-radius: 4px; border-top-right-radius: 4px; } QToolBox::tab:disabled { color: #9DA9B5; } QToolBox::tab:selected { background-color: #60798B; border-bottom: 2px solid #346792; } QToolBox::tab:selected:disabled { background-color: #455364; border-bottom: 2px solid #26486B; } QToolBox::tab:!selected { background-color: #455364; border-bottom: 2px solid #455364; } QToolBox::tab:!selected:disabled { background-color: #19232D; } QToolBox::tab:hover { border-color: #1A72BB; border-bottom: 2px solid #1A72BB; } QToolBox QScrollArea QWidget QWidget { padding: 0px; border: 0px; background-color: #19232D; } /* QFrame ----------------------------------------------------------------- https://doc.qt.io/qt-5/stylesheet-examples.html#customizing-qframe https://doc.qt.io/qt-5/qframe.html#-prop https://doc.qt.io/qt-5/qframe.html#details https://stackoverflow.com/questions/14581498/qt-stylesheet-for-hline-vline-color --------------------------------------------------------------------------- */ /* (dot) .QFrame fix #141, #126, #123 */ .QFrame { border-radius: 4px; border: 1px solid #455364; /* No frame */ /* HLine */ /* HLine */ } .QFrame[frameShape="0"] { border-radius: 4px; border: 1px transparent #455364; } .QFrame[frameShape="4"] { max-height: 2px; border: none; background-color: #455364; } .QFrame[frameShape="5"] { max-width: 2px; border: none; background-color: #455364; } /* QSplitter -------------------------------------------------------------- https://doc.qt.io/qt-5/stylesheet-examples.html#customizing-qsplitter --------------------------------------------------------------------------- */ QSplitter { background-color: #455364; spacing: 0px; padding: 0px; margin: 0px; } QSplitter::handle { background-color: #455364; border: 0px solid #19232D; spacing: 0px; padding: 1px; margin: 0px; } QSplitter::handle:hover { background-color: #9DA9B5; } QSplitter::handle:horizontal { width: 5px; image: url(":/ostinato.org/themes/qds-dark/rc/line_vertical.png"); } QSplitter::handle:vertical { height: 5px; image: url(":/ostinato.org/themes/qds-dark/rc/line_horizontal.png"); } /* QDateEdit, QDateTimeEdit ----------------------------------------------- --------------------------------------------------------------------------- */ QDateEdit, QDateTimeEdit { selection-background-color: #346792; border-style: solid; border: 1px solid #455364; border-radius: 4px; /* This fixes 103, 111 */ padding-top: 2px; /* This fixes 103, 111 */ padding-bottom: 2px; padding-left: 4px; padding-right: 4px; min-width: 10px; } QDateEdit:on, QDateTimeEdit:on { selection-background-color: #346792; } QDateEdit::drop-down, QDateTimeEdit::drop-down { subcontrol-origin: padding; subcontrol-position: top right; width: 12px; border-left: 1px solid #455364; } QDateEdit::down-arrow, QDateTimeEdit::down-arrow { image: url(":/ostinato.org/themes/qds-dark/rc/arrow_down_disabled.png"); height: 8px; width: 8px; } QDateEdit::down-arrow:on, QDateEdit::down-arrow:hover, QDateEdit::down-arrow:focus, QDateTimeEdit::down-arrow:on, QDateTimeEdit::down-arrow:hover, QDateTimeEdit::down-arrow:focus { image: url(":/ostinato.org/themes/qds-dark/rc/arrow_down.png"); } QDateEdit QAbstractItemView, QDateTimeEdit QAbstractItemView { background-color: #19232D; border-radius: 4px; border: 1px solid #455364; selection-background-color: #346792; } /* QAbstractView ---------------------------------------------------------- --------------------------------------------------------------------------- */ QAbstractView:hover { border: 1px solid #346792; color: #E0E1E3; } QAbstractView:selected { background: #346792; color: #455364; } /* PlotWidget ------------------------------------------------------------- --------------------------------------------------------------------------- */ PlotWidget { /* Fix cut labels in plots #134 */ padding: 0px; } /* QDarkStyleSheet v3.0.2 */ ostinato-1.3.0/client/themes/qds-dark.rcc000066400000000000000000004613751451413623100203350ustar00rootroot00000000000000qresP™!‘h‰PNG  IHDR szzô pHYsMMgŒàIDATX…íÁ‚ ÿ¯nH@ï G ˆIEND®B`‚x‰PNG  IHDR szzô pHYsMMgŒà*IDATX…íÎA İ Mxš ƒŒã‘h«€°Þsnr`%ãÀ¾0í¹[–IEND®B`‚x‰PNG  IHDR szzô pHYsMMgŒà*IDATX…íÎA ±ÿ"é dÄÀnÖ{ÎM¬døÂH®§Ôí GIEND®B`‚u‰PNG  IHDR@@ªiqÞ pHYsMMgŒà'IDATxœí›Oh\UÆçÍ´ZŒ¶º0¨+QÔP„"Hl'‰A vÆÑ•Øü™P¢EðãЊ"¶I¦ÆEŠš& ¡;Id Š (Å… â¢A­m “¹ÇEyófÀß½ïbæ[~ÜsîwÎý÷˜:è`3C’N`£ÈdG‹"ò2QÕCËsÇ^cÜ ŽAl£?;üB4 ’ ^‰kìt\ÙBonx§!Xæn­p^w@_nì4Xº"’‰+†·dòc·(¦‚°£¬qÅñÒ€üÐ Sn¶Ë;‡¯]3©OnwÏ+zöí»úÒÖ`Ø‘bkù(¼1 §§˜ÞòGú8ÊĸéEá‹’Þ¾zeo«juòà‰™Üè‚GhEÄêäÁz-¼ ð|Éúä!a2¹BÕÃm¤º«3 “+ä=ÒD®ïõN*ß@"ìÉ2‚o‰`°x䵃súó#÷¢óÀÖˆdP·“ÇìÉÝiŒ,ׄyÁ}åpöÞ¹50R®HFš<8ꀾ½ûoL K@w˜$±Ê7`½2ù±ëÔ¬} r[³¢šdå°Ú»ò¶‰1 À=IÁþ-o#°f@OO1Ý¥>îHÖï÷W+‹Å`Ëös“¨ F$o*߀ 䋳«oO6±Ú¨|â˾ ±ɾ(èAüiû0b5 7WØ/p¨Ug›+ElôfGƒ¾åÅö#¾‰~Ð$Ñ[ÞF›Òö ïõÜ 0È3´Î8ñ/Nÿ†Ø\žøxºUÑT\1l Ö -ÍN¾ZlfÀ[boÑ¥ÙòAÞŽòŠz¹l$¥»îê~V•é0)ˆXŠ÷Ÿ`%¡R©dÎï†ù8" žu‚µd¾¯]uYCõófED=꫉œ:5~±fêƒ|æÄ—=ÁzÕù©ßkAíA¼ "üHËIª3S?Ћ°æ‘dMpÖ†•™É¥®ý(çü*ë'D"pº+'ËgMÊ<\lV4 ¡åà|#Z™9ö¥"Y Ö&ç&$²/ÏNœå ¢'%p½1&vUæ&?Dô©&RÖ7F—$z/(Ex©Uq÷‚Lü2²tbòuEÞj#9É-q½ïÞðN®Ì>@©T2µ_»G]óoˆVsô€jµ´Vëª?Žr&"Y5ÁªSS—ÓÛ. *|‘¬ ^°8=}!0éà{ñ¼3 ròÈ/uÕ>àœíX^ðÙ\ù'Ôû€ßÚȱ- o X™yï» Ðàψ´9þ2pz¦ü•QyôŸwƒl†¿Ì„±27± ­#ÔEx-éœ:è ƒÿþì5-Œ›ãmIEND®B`‚¤‰PNG  IHDR szzô pHYsMMgŒàVIDATX…c` X¬ýÛ`±öo%f0Q¢™`Ô£uÀ¨F0ê€w åFüw ¤J!ÀxàD0s¹º<F0ê€QŒ:`Ô£p0Ë ‰(GGIEND®B`‚ ,‰PNG  IHDR@@ªiqÞ pHYsMMgŒàÞIDATxœí˜Ïk\UÇ?ç¾44‘Š`q“¶.²Ð"ÆB­YĦi Z51 "Mð@‘ ‚ºP .4i‰3t„š23)#âª6éª1]m+ˆ¦ ›Ò@fæIcß›7ÉdfÞ4ÚûY¾óîyßsï¹ç¾{Àb±X,‹Åb±X,‹Åò`!þ=ýï‰1ÇQÀ´ö²ªŠ!¯®û~:~jè^c]ÁÛ²<€©…š 8"æ8à™€ÿO€eR0ªî ÿõ´@u96/5àPOG^LB`›Ç¨,×…Í‹ªAė‘t,’ñ¿8]}{4Ïy„íþ/€q7_m@ þ˜”yqèNŽE.U”î¾þ'ò®I;½NUÙl™P<\sŒÛu~ìÔl±AkN@׋Gwj“žô™”ͳ‚‚ÿErù®dbäúz×$™¹ž­7íÀÏ>“€:Acj‡°¢ÁüÅl½i_/x(ñÌœžoȺÏ!\(" æ¸N@O6dÝ™ÓÃó¥ø)YüììôÒ®½O}ëdM+Ð⑦„ÝTEDŠ|3^w§¡÷ܹ/ï”êiC«÷ëå˹]½pÆi\hž R6r÷×¶è#æï׉‘ìý•§£³wàc7ºÖ)¼È'©ØÉ·)ã»eïß¹™©TsKÛ""ýrÂÈ…¢+¯úVêLd¨pDiTTÀæf¦jnmûäy¼ÙTÕ- Þe0~^¡ïÊéììô Þgªø_AQRK*¼šŽEâ•ø†*®ÔžÁN#zxÈkQ…òþŒj¼èK©Xt²<¥^ªšª‡úŽíu]ùxÔcPÙ`&()ÐwÓU9<?y±2¥ÿRõjÕùò@‹R@“ÏTúv¸Ñ7Ô¥+=™©‚ÌUªÞIGfòªû«>SðæC þª³¿ÚÁCH¡ ñèoâÖµ+Ly Ë)½ÖÉãø žÂTÉ?›¾‚ÔðZbÉñ/þܲu±å¿M'!ðN‘Ù²u±cblä¯$!÷'FGoeÎu#úݽÏW–ØAd5+¼Y/ÈnËž½¦ÆÐorË÷‡#1§qáqàiŸY,ÄúUöæŽ×2gO,…­¯&]áLf(·owS¿"Ÿ­ÿ¶~ºo÷Žc™ÌP.|eµ½ÃÈÁWÞAù0Шún2ýˆ6kÞ̘»2õcskÛ+÷‡»¨(o$ãѵÖs_º9sW¦/5·îid¹Ïx[”£Éxäëû¡Åb±X,‹Åb±X,‹Åò ñdv7oÔaIEND®B`‚‰PNG  IHDR szzô pHYsMMgŒàAIDATX…íÓ¡À Ñ…š(“ZpƒJ1™¡…Â"ùfŸ:·ê@’,íQÚ˜@½Týþ·?ùJP:ð’$…[ Ô é IEND®B`‚ñ‰PNG  IHDR@@ªiqÞ pHYsMMgŒà£IDATxœíšÍs[g‡ŸóÊí$„:M¿™®Ø6mÚêÃ!”ÆIm'ãþ…EùcjK†UYÀ‚Ɇږ§¶lfØõ  ‰¿B6±%«@7N†J qš&Jð{XÈ7ˆ+Ùz¯tom?Ë÷ž{ÞóûéœWÒ•`—]vùFê-úöÏöݱwÏ÷Åh/ÊKfŸ¬üräÃ/ºÀfèønî«6ÆK Gn`åìçmæçï½òâgþØžzá§w•ïØû:ð¤ïÒUÛ½:3ºUáaJ¥Ô˜y`õºÂ;mwòÍå\f­zÝø”ïÜûµâö‹˜¹DÿØáP+‘ÍÄÿZ­°·Á^í"f.98ѰÆÈèÈ%‰@t©žo8ìÙŽ2¿LèÈ%-º@#ñh¶bèàŠÃÞÛnB0ñ¬ÅàÇþÅ–óÙ÷ºp7a[ÆaC|㶯°f ÝËùìûþ õF€âtfeĵz×}ìG™K¥ÇR±¡Olˆ¿Û!üª5t—¦2—ë]¬ûAÈ#98Ñ2´;ltE¬}¦0;ZpˆmšøÐxÂXYÀ]|×fâ¡°³L[<8è;,bæp4Á ÇWò#E—Ü®$ú'ã"v8à~Õ`ºWò×:P)`ûLˆJ4v(h½~Z2 98þ(ˆóÌ[‘S¥é‘¥ÍŠÓ#¿ hÂb«&4=·Å+÷;„_´·Ï.ºåÎ=ê9ÿ]àïªÒ¹:3òG—Ü~šê€Tzò`ñ7‚ˆ‡Ûp¸î~Ÿˆ.%ús¹æ¯&p¤Ò“5f/*ˆøjýãGEä,vB RéɃ»¤ð€CxKâ=‚š€èÑâtöO®ù *Koq6³àš+⃹N£ú˜àdÀá‰G,\ÜñQ™Ðð ,s:lñ¥é‘%A{q<A–*ïT[³e4%>?<ïÛ4©ñcмÔüë³ácÐέ:aÓ8<0ñÈ:8ϼ*}Q‹(䳋‚žn4 VîYª¼m×§nxâj*«rzu&3çA;AÖÍÑÂìðŸk/ù82ø‹‡oi[xÈ¡Žmï‘LOÇðkLø±X²ðÊ‹U¯×ŒÀ-ÛöCþÄg3 Xzq…°ë?ò¯×žÂÓ{—­HßvŠ÷¨¼ã˜Ó¸˜ õ¯Õ °Þ OÙŠô•¦G.¸—-•Ã×É„m5lõª–}n'‰÷(æ‡çUéc D8ï_«1À´™3Àuî/ ú\!Ÿ­I²SXÉÌma‡ÿ*sÆ¿ó/ü¥pîúWÇ^‰íCyP„›(‹ŠùNq&º_iÃ⯫¿ùàáx×YÄ<@ùDE_µ7åù·~•ùx»ëÛe—]vÿ^ì ö^&­IEND®B`‚‰PNG  IHDR szzô pHYsMMgŒàÆIDATX…í”±jQ†¿ÿî AÐh‹´Úج înÔÔ"E±K QAó@PBv³‘ˆ˜.à‚õ´ÄÆJX³s‹™„$»Ñì°j3_u‡{îùÿsν%%%%ÿ=85ïɘø%PM*šì\Ö—"yB‘Cõ‰ßU€^êÎÔ’É5tN/úd°_Øy|5:ÿîª> “o¨ÔÚžö*00A)A)`à¨ðj­íé¿b ÖòŒ¢W€CÆDœKCt¶WôJ­é‹#5P_ôMá60¶LÜ#ˆd3“¼ÔhùÆH 4Z¾‡ý¨Ô'¾ÅFÌb¨?k4}·¸[õV:kü(ÿŒú­xnEçq–çÍô!ö®—}ðƇz5Îî¢6¦½Gœ½ ûñÛá>³ýô81ï}“øtÝY¦( '¾iÂ2?ï} ·Önk}W§^ø@Òóp!·"CV>C.õª—èÚÚ%ýè3pnÙG~v½,qÆ€L¤`å;ÙÞ :]iæý}H6‚Ö»þ$1¾éJÅ~ÓƒÐöAŸÝoÖWàÑ»'ì§URRRòG~Qãºr”/rÅIEND®B`‚&‰PNG  IHDR@@ªiqÞ pHYsMMgŒàØIDATxœíÒ±MÃPFцð&,’R †¡‰²Da‹Tla ,¥ˆh}"åÉÒsõ>_y&É3{Q¿½fæ´½~|Ÿ?/bÇ«¸tsZg–uf™[ˆÝ±Û‡ß÷&ÿ€‡P=@+€ @Ð  hдèZô­z€V=@+€ @Ð  hдèZô­z€V=@+€ @Ð  hдèZô­z€V=@“~þ9ï X3sý{Ö£Û‘ä™ýÉnJ" IEND®B`‚è‰PNG  IHDR szzô pHYsMMgŒàšIDATX…å–»rÓ@†¿ã¤ž#%T±Rð „Ä!dr#Dj,7™á(e5’' Ø!ÎÅÀ0Ã3PY~èà €ûPÄJd]lËqÃp*iµûŸV«Ù…ÿ½$¼0^:µ Ê]^µ}ûã4A†Y]Rå5ÂoD¬À+(„´ GÀ„9…–aº¥iÁ‹–³¦ÐB˜î£z>»€;‘1E›Ó(ZÎ*§Qp/! "@oš†é–Rà=U9H^ù°‘"q:‰„aº%E›q¸ [Z¹•|û,Eb&¯DžxsA¶Ú~ù]´¯RóûžÄº"ºÞö*—Cá–³ª*M`fPZ¹)Â&ñÏ¡Ò4,g5Ex%/†Ì@X†U}®J#&ÛU•gábšßwWDô,/|,Ãt7­§I¤ÁÝ ¼Êɨì±²$´ÿydPllx.ˆDcȸ\ðÜ74@ãcØ|»‘'/ó/È*~)ª‰ö+Ÿyór -wÕsI×o;/šÕ§y2s,Âê’Â0ig"šÓE¤Ôß[FÖX3WÕ`;"0ƒêEÑr—§"°`9‹YðN­R|»Ñ™Hb¨À‚å,öT.ãpv;µJ=lèÔ*õ ‰óQ™ÃàmÏ~ïß©Uê"ìÆ$fGIdmÇOD´5.…dàÒITv t–³_Ù7 ™k_&ÎV>ÐÊ…Æc=§ÊÛ,¢×àýó»~WÏ[ ßÿl=ëÅtM%¿þËÄøé©×VÕ~=©ó–¯Üã´“ÓÕ±Ôð7=+Õ«"@ð3ÓMùU:MúnÈ‚?ú{oV²Lž¥Î¯}©~)ÂË€CuÏÚUKw}Ù¹;ÔÿúGN"ÑQ•hi™÷°{cŒ1ÆcŒ1ÆcŒ1‰vCô=PÃpIEND®B`‚‰‰PNG  IHDR@@ªiqÞ pHYsMMgŒà;IDATxœíÐ Á×R¶6¢~ 9™ËVð¿±ö¹éˆ¤™H3 ï5†jêÑC3bIEND®B`‚‰‰PNG  IHDR szzô pHYsMMgŒà;IDATX…í—1NA†¿·¥tiV C4Ùf/à LäœkµNÁŒ‰'àÛlÀ€Å†,YÈ> g ,b¡»±pþj23É÷e¦y?ü÷Hqc:›];*7 —€Wg%0ÎDíöÓQ—ÙkÕ[` D(o¥à… ˆôÏÛg÷Óù¼# ÏÛÛ ‚Ö¢¸I'~­žŽ@®T´“¿„“_p2zÀº 8@´ÛÛRG¥÷ÉÍæÏ£*໑aí ^iþ]2V@ó+?‰°VÀ X+`¬À®ÀÊL¯U=`y 0Â8NüªØqœø(¡aí d¢ Q«§£*$&“ÉéÇXŽ›‰óýb1y@õH"3@þ>J¸G‹ISÍzftnϘ¥©fÃb5³y%¿n,: BçIEND®B`‚ `‰PNG  IHDR@@ªiqÞ pHYsMMgŒà IDATxœí[kÕ=çÎÌ®ƒ,ˆøHJ(ƒ1–®Lp™]‚+˜¤T‚,»‹¥â#*&ÆÄ$&Z¥‹FÆ–Uµ‚¥"•d`w-Q«ÄµÙ]^jùÀ2QB4*ÊCyÌô=ù± Ý=3Ý3;3”•pªæG÷ÜïžûM÷íûø8€8€ÿgp4rJR‡ï‰¡‘ÖŽ”Áñp€A"¶íý½'è À¼Ž(–÷Ná;•ÖV±ŒK雎µÓAN0ª_N„w`´ÔZó𪬩òª,w$6¤pº…®0±œ® ¼*pVæC<¶æ ¦Ëè·V74c;5”ÐËŽr™{ã[yj×ÅÜUŒÐúN`…f; â0Ã86‘Ú$kÖ*†Î¢×);àÅ.ëŠ Ý­\T54‰Ng.ÀK\¦ÍÑëV4qs!Új“ªª‰a¤+_Hë xïð4æ-h£SH3ŸTõÇ;µŒÄ8—ym|]™ÉW/0‰N´Ü­ ⸞Vv"jlJSiuˆc áçÀKÕÛÂe…‹u$½ à°}6‚×t·ðÞ|uò‚ýÁgœ_HçÇ< X"åÜG)UBçà$BÏÖw83Ѯ막@¼Í£ºqÔÓ:8_¼NRh0ÞeJ;Ö´‡‰ÿ„†Ä×RˆW…q …ÈöÄhý}Ì ãÆ·á~ï¹L‡ ø Wäãç €…~㾦ôÀêi|;¨ñÚ¤ªÒ{´HÀ÷ÄöÓ¢i=v'ô ÌôüQ‚®Í77Èé¬.©¯8ÃeJ+jn ”'q`ÔÞ 1W ˆÖÄhzOc€·\¦£6ü 7g"Qœ‡¾­ª} /ëiâA&á|‚—…‰+¼©~¡rvf´Ñ!´ÀSËØ‹rqs@ÐYƒå’ Ç,ÑHwqÊ Í îãÓÌI¹êd`ïˆé~—"ãà‰ ¶b®…w¢TiŒÜÃô B÷:¬à¾koŠaŒŸ—€êÏQ ÊeziMßóóöáì¤"Vúy¨ä2ƒÒ5„›i)yþ8){Ÿ2+ø¶§=¡,lŒá‡Š© F&ê[AKãÑ.Úýœì1@öÁš·²8nºì´@™•„AkPqÄÀ§'ø9Ù ¿á¾´@à»b¥wƒÏz¦ÝÈìñiFø9YEði î×ÁÏaA…–¥½ÆÏÉ é%Y"ÿ[߬,×níþB`º¦c7÷J°ª6)÷Ÿ#¾"-ònxL˜“ËÇ~D¬TÙ°Ý}í¿#ÜØ»Îœ!V»ÏF5€¨Ë´çõ6îqsr ‚ðì¨Ú¾SÜ ü+¤¼’ ÀÎC1ØgÚîçdÀjƒ‘5rºAèÕ òJB!mËÁ1>Ó»~NŽç׼ᾢlàd2å„ oÛ£]Ðú,YUßa#Çgq\8hžÞ•Æî¬ "v¼÷ÚdÝ1Yˆg°€ûüý»‰å}Ýt]Ì]”•[fH|0pK¾]àdo%tùiYØ{¶¶ÒS˜ìç¹sÌ-¶*./6FÌ"ÔÂxç(Ûâ‡bµŸ—g?€žU¡)A-o㇃wŒÊ ò†•mÜD1°Í‚žÊµ=ž3±æ£/©aoeœvJR+¾øÜâé@áå5¯g*ærÚe@zi´æ‘\ÔœXÑÄÍžù¢2PŽZÿq¸]™ÙÝwpúf ¸ÒÐÿÔ\vW?çÂ{ó~|è—ýq#ï4VäŸ<×àOÇuèè †_jæ§Žå$T"ÄjD85ì8®6©*I¿wÛDÎÎw:”7«¦b¼ƒa•;3Lçêi|{7YäŽxÿ ¿™4Ã6f`` —ƒžÉÛ–˜Áœ|üü R$oðÙ.L¤‚×à@ßÂürL?‹ÀGgô4›óÂ=³D‡QòáC¼}Eój\Éu7³ Ô<_J%ëÈ01]™énáðXIs|VÇ…­€îÈdxlo ï/ä¾6©ªHF áÍ!z%óòž œëÔô €/:-áÅ¡qž¶tw‡Õ߇1K4 –Á m•8}»È‡€€ l´–4 ¶¥ñ¬Õ†úNgŽÀ+]¦´!LJe®” ‘èP½¨.oF˜êi6—•’ ÑTü°Å2¾«@Ý綉¼¬·™sÃêœ$•èР¼ÒƒÛsu±ÿVÙ ±>…«͆ûq¦îéiŽü²Ee‰Õw8wŠ¼Î£xÞ‰rÚš)ü¨_¥¢6©ªAûg‘—z ˆ§ã‡prPR„Emg wÌ RÞöpj4£U ‹4²_¥`\§Ž¨‰ê٬ίíÏ)´ó@?%ÏN*²1jïè¿ÅÒ€æD`n{±…ÿ)Öo!hH*îDñ3B×â+^šÉðÜ5m,jQÖÿTÙ”.eßëÍ¿1¹ƒÒÝŠ›YÅ$,aÂsŠîÚ‚é¢f"Ǥ YGgÌu…æ¹QR²t]‡ Õ `hŽâO=™ÇÇ3Å£6©ªA2ö,€MrMÃÓ"g2ÚçCÉéòu‹4ÜXÝ %€–ñ¼,Ÿ£Á›þ!ƒ-ÕQìH8fjlËÁǶàéèû¤&ŸðU"gô4sM)úËöÁD}JgJº@¥ÃÍo9ÚÁ_úsËûQÞOfÚeÆŽB“¡®PWVßÀÛgÅ·bn± šA¨ØGSu u¢1vúÞŽë§›÷=ieYõ ^èÏ÷aØ/ŸÍÕ-ÒpZL4²'Š<Àƒ% BßÉÍVô}6·ÐzÒ¬ñ|OÞø*ä"ÀÀÿ.þ ‘ZÀ‹yåmIEND®B`‚'‰PNG  IHDR@@ªiqÞ pHYsMMgŒàÙIDATxœí›OhWÇ¿¿™ÑBÓ"v…Ý¢‡ÆCE°$[öàe¡µm.mjKU<„¬6æÒ‚f²bk/Ù`ĵZ4ö"”´‡ ½,Ùxº½Øƒ!Ö¬E0HÂÎüzH^;»;»;›ù›f?°‡yó›y¿ùî÷7ófxhРÁV†¬ÅUUY^ uèÀQEnð‚˹ÕË @œ•€«M‘¥É™d²Pë šD9â×ÉÓȤ¾lú̯Õâ¤*§ h÷H? Oo¶‹öúô›‰Ô9€+þÑr¥ÑîP?_º“ž—Ð[-ÑÌÊßs¿ünº×¬1š¸xЧKšŸ€0 èÆž§áù‰‰4ÇsµAgç5ùþŽün]áÃ` ØY!½cVeÄUUyöp×"Û~“ ú±ÌX_ÞùÔ'Ö5ÖiŒ·E¹—#KJoŒe÷€åÅPGIÍ?^Ŷ7ËÅ@f¬//ôcžˆ6ö-/†:JcËУE õÎåO¹‘¨›dÆúò Vme×Öžó†€Ýp>=o5iʸM@{iŒÉc[Œ[{ž†çNÌ+Lrµ4ÆlP4 ÚݾLr/½Vm ¿°B[Ïh«TÐ@xo½é:3†æ®ôþe÷Üw@,‘Ú+iÚMŽxqýw‚·ÚzF[íž?ÐÄUUј~Ðl²»YÒ´/ìöhž= }*txßn z*µ„A·û ¤qUU Ó÷¶U‹#¢ »}R Ö€U‰ù‚ݾ'€Uëx0“îÍÙí/PXµ>€lSäñ7Nô(¬ZŸt餕žVŒõXöÛ3wê7øa}#ïíÝ#¯ab`ÂøYWä¡Û—Nß³r¼ÖØv@[Ïh+n81V'—4íf,‘Ú[ëx¿¬/°-€TÐPa¬®~Œ«jE—ùi}ý{À¯¨fD—_ù¬ÒN?­/pý&È Áè©ÔþÒv¿­/pB€ë5öoÓwÆR‚õ¶¯¬Ö+*… X_`[€Lº7F²Vœ(… X_àÈ=à¥æ¥ `ÌÕÛ–~`¦qÀúG˜I& ø$j–¿AÀ§óÄúÇžÙËgÿ°R µðÊúGƒK¡žY_à¨ÖKÁO­/p| ´ÑRðÚúWF‚(Ï­/pE€:KÁë \{°Z ~Y_àêË…RðÍúW˜I& º"Àl’ż.Ëùe}ë¯Ã·/¾'k8Hà)Ï<'ð”¬á ÕOfnâÉü€ÌXï€w½è«^ñUØOÌX1ntv^«86è˜ä¾Rc"=0nÝß‘ßíhVb’ûBiL™ ηu…;œ—gh²~ĸÍ($— W‹C±®á°ãÙ¹L¬k8L ¢XÙµÁD€¦ÈÒ$˜þ44íÔi|3‰ðïdiÃŒqrM‘¥ÉÒØº¦Ë3X•5i*ÈÓå5Y?²þÏolº¼ Ú=ò?Y0¸6}ö¼Ù¾Šã€ì•Oϸ߽´<™éÜlº÷«J–M1ôáµ58›«‹¦6°líX›uÀesX``®žes 4ØÚüø­òûI6ÖIEND®B`‚è‰PNG  IHDR szzô pHYsMMgŒàšIDATX…Å—ÙvâF†¿f`PœÄžñ$9YÞÿr1žIÏl#$¨\Ôߨ‘qr—Ô9‰V×ö×Ö X øÏÉX¤| ¬!X÷‘K –%ï“DJ\³dm›¬íô~zò—À:ôÀ0ªDYØ !ŒqÏ¥¬¬•Q¹ Kþ#0–òàÞ²ûDù‘q÷èÌLÊô˾¦€Ùx|”ŽAûNÿ+ص¾ý ÌÕ J\p¨Ï`s Åó¥Òó€'ôxÂãÿ °‚°%Üc)Ïåݸ’Å9”O‚qf$åÀ³”•B8îððdž;(ÜR›À˵ƒ \Hh¤9†¹øú×"mÄ»“¬Û÷JÌcîżsA0ýŒÇ²–õ#ÁVCu Óµ#uŽl!™50•SºÒIöÁ— ´/Imеá–]ÃtåH½I±Î¿ ‰X¶-<6xèú!hJXäxÒü%Á…úAîIg³î?ž7_as)Þ<‘ ,žIB ȼ‹‘ ÆR´b\á‰ö Hs=ƒ º ¦®œfÚ×â=åï1ChLûc @P× Þ¹‚2Û ‚ú»56î{½ýÊŽ|´`{†Ø3^†º$?©‚aRßI“©Ó=Úǽ{cé0»KŠ”NYÉ UçlÇ0‚ñ!ñ†ä½M„Œ€Æ=·5êLxF[ÒRJx5=És†¹O‡žÀ>kÏ–¼¿Ú…í QW Ömõ°gÀ0}¥ß2™'}ƒOFnߘ”¡U)õ¬-úußâ §ybÛ¾¦;D ¯ß­;î‹9`ÊpSÎ4XÀK³”âo¹j6–BÜßJ¸I¶!; 2vù¢;C•ÛÖ$4þßÀ3#!Ÿÿaî8žvÖ%,ï:yÀK8îÙÁIÆuâõ5 Q/?Éós™~´@~€åS·¼*¤7l¼‰_ ãê²c¢ö†ù->ÝúgÁ”"Ôàí6ø³L†W6“1'!Øûæâì†nàøIæw<ž{ t¤êA— õPÕ!{Æ»ŸŽw,¡¸•Ìmjn@xÐÅ|$o6`-ð áå<6w ƒëg|–l$såùÓE1 Aã“-te}r1ùíÌÅäØÛ÷‚m +3þo2y0„qz÷Ëu0n}ÔŽj¼gìÅÛÐ ¦5ÉÕìËiôâ˜pÃä}¢÷ l5;&•”ÝåT¹°óžÃkøŸ¯çˆ|Ó;í2OßIEND®B`‚š‰PNG  IHDR szzô pHYsMMgŒàLIDATX…í×Á Á fá‚­HRHì@=ñ¹û<Ì7f$6=Æô·û©* Òš´*/G?ý3²ìÚNû¾{¿0â“çIEND®B`‚4‰PNG  IHDR szzô pHYsMMgŒàæIDATX…å–ËnÓ@@Ï ,€ÏH³i ‹$;Öi6H´µÃKB-[ðH|KvH$@åÕ&ÐvË6)Ú¼b^ü°i.‹ÔÁŽíÄ)Ù îjlÏÜsæÎèÊð¿‡øƒJÕ>©ª·£*ÜØÞ¨·¦ *W-C”›À/T¯mµÏ2þUm'€œ(ëÃ:7-x¥jŸeÈÇ©ûß2¦é‘ÀšŒ ¦!Q©ÚçUu-ÌâXD@É\zÓ”¨Ö¹xÑëþÃ!à¹mw&_|,ñçnÂRn¶ø¶ë¶w'† "pXÙj6žDö%^ÇHd&•؇¯ å߇×ç†|‰ìl჈D$²ùÒoŒDÙ´ÏÒßùXx¬@_¢ó*NBÀ%Q6í³‚Ã5 ž(àKäæJ]`1Ä|µvFàq |9 >R ë´%fæ ®çtŽÊÄpIGƼi]V ßè=D/ôQ1pÕ•­VãÁ¸Ü©’$zÚÅRÃaÌÃsÛ/gòEá>ÞÄDð‰Ý!‰\…KÛÍôp—.U(ò“þ ‡ˆfzü˜4ßD(›¶)è“„u‚`æf‹N×m;S(W-Cà)p8ðÚ¯D°cšÙ|i×sÛnš¼©Ž`ÿg"ô² —É!AŸ–MÛL“{læ {IàY|³ÙXõœö‹\¾ð dp³JU‰‘󆽄hŽÊ•­V}ÕÑu;IF6_rFI$ Œ†ß¹?<¿ëv^ÌÌ–>#L$+P1k‹ëiá~xn{'Qb®¸ë9Q‰ˆ@Ŭ-*«PÛnÖáA‰ì\ñ‹Ài†ïDŒDH \µ@6báõ{ãà '½Ä@ lÔN Òú[xP"—/|‰Häò…®Ûy>¹5 Ô:ÜÍfã® á>qX‘[ÉUPk³Ù¸{Pø‰½ˆ@Fõ*àßQ]™<(,#|>J«ÓÊýïÇoÏ †Áýß¹IEND®B`‚î‰PNG  IHDR@@ªiqÞ pHYsMMgŒà IDATxœíÐA 1‚‘„€Œò¸Øec@kŸ»ö¹²aÊñt€Ð:@h ´ÐZè­t€Ö:@ë u€Ð:@h ´ÐZè­t€Ö:@ë u€Ð:@h ´ÐZè­t€Ö:@ë u€Ð:@h ´ÐZè­t€Ö:@{ÖÅë£IEND®B`‚ЉPNG  IHDR@@ªiqÞ pHYsMMgŒà‚IDATxœí×1‚PÐCÎä¨87ñ<$6ÄCˆ­áÛ"†ÿ^9Ùbv»M¨G³ æùqK“)Iw@Ÿ=-Y3ôýõþ¶ÅØ9—O’KÚLÛ°<@eʬ“<_ewK^.ü¿@1vÎå¿Àw~*ñtêÍBIEND®B`‚ЉPNG  IHDR@@ªiqÞ pHYsMMgŒàa YÀþ¡GÞZaSÛ @…t&·Í•~}ÏæåÉ’a‚àžÇú÷ Ñ°«_€ˆbêIòáðèÊ—lÜ ý3„€ Ô^1>–›O™VóЀED$ÑÊ¢`{X*&u¼5€2ºE€˜ +Ñý¦t¼5$»eMS^àLuÏØcÑ›—“Y,-=k“Þ4¥á¯(¢Þlj·jo˜’ñÔImñ¤<7›[<ào ´Dxó6ÓBf€zåãÛí+~Ü’ñá¼ñ+mñ~H®Xñq½'0€GHAjñ8T¼0y Wl©úqˆrúÎøœ\~÷§;ë¿Ù”ö!$½ó ¾.&êÎN\ùÙ¶x®p±× RƒßƒdÍg{ÆŽ¹ˆ!Ï# ¢oÀOÂ`íá]õo\‘KP2v8¡ÜÕÞ½ù —±äa€5Ýó“õîƒqüq”Y;O>Ô‰ëS.cù §5@ ºádµÝ}ße§â2 韞‰Gc‡1h81€Y;¼ÒnÕsÿ™Œƒ# @ëòövZµ'm\nºÅvdÜìðAß•Ç7ø°xÀb$ÝâÓÿÑ÷àôxý¤-Ýn±ùCÉôâg %Ü;½÷Ñßmiž ®ƒ_Í˲uGš#¿8Ò;k\ð©Öiüà@«kLþa"ëþqUHÖvâ±oMé˜ÆXdÜìæD©u³­-_šÒ°Á øç!ð¾öî±¶©ùmað_cÁv!@€/´[£Ÿ›»G=,ñ'¯C¯^ÎÃIEND®B`‚%‰PNG  IHDR szzô pHYsMMgŒà×IDATX…í•=hQÀÿwýÂÁ(-:t)Úê±›¥PÄ$”ÆÁæNð Á&Ö.Ýœƒ›{· ÒÍÅ¡JD¬‹é"ØQ'?&ÇÒI8Ï{‡\lBªM«î·w÷ÿø½{ïÞƒ””””ÿŒ$M¼äÝulô4KhG_?ü5I“$)WZœplØÍÐ/oaüŸä¼Êyˆ c (2fU…«w³*PpËÓX|à ‘@(ÊIkŒ_pËÓ"w+E«¬ƒÕæÀmÛk ª@Æ*ë—çæþª@®T¾¥ªkÀª*ÍcĪ¢Àˆ<Ë»•›½Ôvö È—ÊË@ 0ŠªˆìÒ}Ø|›T@r¥ÊCàQ|k¥µæþŒŠÄ>Âìø¹ òñý¦¿/jµjF&V€q©Ö÷Œì\fNŸ¾}íÊ+ß÷»jtmDžç lÛÌ*p=Ùwó¶òÚZgO¶†ÍwµZø[bñÞ‘`0z 2‹F,š´ù/QUÓl¤/Ç­×kߺæn,ÿ„uàbüÈÒÛœï퀈î|‰0úQ|³¶º Ð× ‚ð³@¦-/Ñ6½Ú9Ž©~§ï p¬£IâS)%%%å°óÿšXð7Ÿ IEND®B`‚ç‰PNG  IHDR@@ªiqÞ pHYsMMgŒà™IDATxœí›½oEÆŸw÷¤t©LtcN‘(R’.m  P`¤€D¢üi@ØN°¡¿$,PDB9¡JZ§A`7áØ»"P"‡Û}(Î#ön÷¼{ÞOãûI.væ½›w?ïììi˜2eÊaF’‘¬Ùv÷ / ä€:€òMmbvôn å®R"ÒûP¬¶m¿BÈ—€œÌ$ÍâØø×•R?îdŒë )–Ó]&ŒxópŠ06~·»K$Çþ£Ç `»½%·òÉ­8üÄrzŽï``{cc¤ù/+€÷P)刈—i¦)!iÚ¶ÝÌ‹¬8ìø¯F•CH’5Ûq¶=ò½þ»ívûY¹gN§Ó™5ÌÚ Í[ªÕ83:1†°,÷Mîšþìÿ3sraáÅ?òJ8"Ìl#èbqn®ùC0.4Pxi¸7ÚÍ@»Ý~&À`[èÞ!Àîs>€÷0ë䊂ôÖƒ×9;õ¨/”RNÆyFDîј(†VxU›í'!"÷Ðêuì:à°P+;$¸®»àùX°ÕV«õ[Ú﮼\×=áùxà2€£Ž äŠOyìºîBÚﯴ$kžïè>Þ÷ùQÚ1*-€íö>ztiòVÚ1*+€ã8§AÞÌ{œJ @²æS¾0³Wœ€ß§«’ÄY—ç狼r$µ>!7•R[iÇ«”I­`s®Uÿ<‹1+%@Rë ü«I~ðLBe˜Ðú¿f5n%(ÃúšLÞÇyÉ#V “û¦Õf³ù4Éç˰¾&µ\×]ð)r»ku—=\×=÷ù²¬¯I-Àî[ZäZÝóñ-ɱ.+Óúš,æ€Å=úÎYNïýqeZ_“û$(àMÇqN¶—m}MjÞ‹ 9âS¾ –B¬¯I-€~àyLØP)TÁúšÔ(¥¶ òq\œ.…ªX_“É šõ;žÄ„ñ)ßø5TÀúšL‘¾!¼ŠøRxÄ™˜˜B¬¯Éì)Ðjµ~IR qe}M¦Á„¥°…Y_“©”B…Z_“ùBh¿¥P´õ5¹¬÷Q …[_“‹–B)Ö×äö.´ʲ¾&×—¡¥Pšõ5¹ "}ÓÀÛ„7YÓÀ;eY_“ûëp³Ù|Z3ä<€uïþ­× 9Ÿô'³<)d@£Ñèx½ˆ±&¥¿ —I”;Á ’fA¹dNDî;£1Qô‚ƒí§“ˆÜ»£1á’àæp‹y1Ó¬ DÄ|-xM0ôHo”¤ÜnÀj§Ó™Í<»œét:³†b¡{C„J5Ü43ÌÚÚAá¿ÍÒC;Æ·÷6LØ"}ßi½`˜3Û¶í^³,k¾Š#IÓ²¬yÛv¯ 6Ií‡À¿µè{’ÂrºËÿ‡@Èò¼jÜŽê»PÍúmB–óK«H`i®Uÿt\@ÂCSÆNešZþ$:4µŸcsg1Øu]Åcs]‚O&967eÊ”ÃÍ¿[?ö< ÿœ\IEND®B`‚‹‰PNG  IHDR szzô pHYsMMgŒà=IDATX…íÓ!ÑåRªâ.êfd—2’*Rö¥ß6}$}ÎHm ?ªöYcˆO‚Ò…/$é» »X ƒ2eIEND®B`‚‰PNG  IHDR szzô pHYsMMgŒàÇIDATX…å—QH]eÀÿïê]óÁm!ÌJÙ&+¶¨Z V+õš §3B6mD/Aàèá Ez*ÆbN-"Ÿ®WKk¶{u\_ÃBF+„ÈF]µˆ Zê½÷û÷p¯v½×9/^{Ùÿéœïü¿ÿï÷Î9ßÿÀ’M²¯öx©õäÕ‰±¨”%€˜Rˆ :Šäõ‡gÌ©@•Ó|0nM» O¯³î¢'C½]#¨pZ¶‰µ/$‡f :G&½&>³‹Û<³¥8®v·ˆø€Z`W2ÿ¼Ò42Ðù{ÖUNó^keØŸ‹»~ïGáp[l-i×uÍ¥«SÏïeÀ/ÆÚ£ú»¯¬[ ¼þx™Ás¡HàsÏ]ÿ¼8ÔÓóçZàô¨®~}K´`þŒ MÀߊtßV º¡¡06_ð5胠ï?ñPÉmmm6xj}_}s«Š¼ DŒšƒú:fRLúŒØ|ÁéœÁ Â4Ø×ÕŽÒ ”Xc?%mÑ+N*Ÿkz Ìð‡sÿ°¿cnðåpÇ;k·]öGBÎóK×VÜÁœP•·sðûý‹*¼•Ä\ÌXÔÁ¤Ý‘--ù›ÀEŽ&©ý¡îi„‹E;®Û—sM¯¬?Q“Üä¾ùÏ-÷+Þk¥@·º¡¡0WpÇq¼§= èª#}çÆTéAÙŸßú‰ëº¹xMe6¾ýƒäêÃÁ@ש3ó0¡P{éjä½ JHe}󛈾‚0MÔ6²z¸EK–ì/w}Qãm ûÏü• Ùqïœ-<­È à&اBîoÒó<«Mþé‡ñ{öø Á'pÈ£ñƽû+ÝYóݵk£kvH®ëšü¢œõö‚T’hÅ|Á¾ÎoWË_³-?\÷Òö|“×à$‡"ƒ C‚™ÄØ™…hÜz=Þb‹îª­%Ñ ŒšhcØÿño·b¬ëǤÜizÜXÓ<¹ž|`±­¡Þîàí³ú5{¶îÕÝ1«#¢Zª‰mÕD€ˆ¨Ž"žþ` c"›ºwvü ë2dÔTÂCIEND®B`‚܉PNG  IHDR szzô pHYsMMgŒàŽIDATX…í—AJÃP†¿yq« E]ØÖ’K((^ ^A èJÔ#XÁ…<ˆÐ ‚^"æ%Ï…Š‚tŒ ‘6F¬© æ_ ó†73Ãã üwÉ ÃÚÛ•”d_T–fKÉ¢<¨è•Ák/..\~†Ñ."Gy`%)Eu¯ÙlXkWs< ²cŒvëõúSYã8žNSi)Ú¦³6X nltÚX­ue$Í“µn3´±ÞØè<ó™Ì•ec´;.Ï£ ÈÒ@6pe•=OµZí±oÎ ü’*€ à×&> £h •C`ò‹w÷=h6gEA…‚`•Ó’L¢Ò ‚`~d€ŸP!€ïûwˆn½îî!ºãûþ]QЧ3Ðïaa¿£¿Ý‚  øYå^?ãJ望é›÷C*z¦Ò@’ÐPô:ó½=D¯­¤ëŠv¬uâytßýá¾%çÜL’ÐRôH ^;;Ë[LÚŒo6>^L2½­f¯_ç¹Áóu¯èuÞjVérߓޯŸ“LIEND®B`‚µ‰PNG  IHDR@@ªiqÞ pHYsMMgŒàgIDATxœí›Mp[WÇç*…”ФM?g »4I“6´h¬/§íÚŠvmgbÙÖ¬ÊLlØ2S[vWíF¶4 M?éÉ壄å£-0ÌtJÝQ Äij;ÝÃÂ’1ú¼OzÏ6ƒÿË{Ï;çü¾÷>=éöµ¯}ý?KÚ ¦ž}ö®}ö;(#ÀA…«ÆðC¯ÿ`‡ûëK©ìÔ—¬åyÇuU.ߊ}æG¯~üIsl €ÑÑÉ;Ön3?N7M]LÚ[š}3¢¾CQ|bú+F´ ùïyûÀÁO¿ùÚK/­n5Í Ö˜çi5pDQ/•™y,Ä~CUgóúPmýöï6¶@í\BïUÝÍoJa¬y¬€ÀíÝKmBHgsöÑg$Jf&Ïô2_×ÁæÖ ¼Ñ»¤¶Ö”÷„dfò ˜ ½Í­ÞZ¨Ä.×zçÚ}ÁÌË*Ä~Ð<Ú ¼ôÂ{Æh g²+Û¡nÞeÙ²ªªéòÒ ïµÌtº$Í=j­)ƒvèçšUIúŹß8ĬTvêµT€;{Å*\GIUŠù_·›ïö&„0ÍC°· „m¤23)ê¹B›(/-¼å’ÛU鉙‡-¶‚pW¯X…ëjlÚ/,\íëvBTæ¡Ýç€ò–fßTÕôæí¤§Ž€©lžÔƒ)¨y#2ìj¬€†S_EðîpÿØd¿+!ˆyàI{‹s¿ R#0A ¯ÿm;aúÑBHdr§EÅÚ< ž|\¬YBPó¨—‹óÏ/í5ÂN›‡wNò WȰÂu‡ð;µF9=1ópóD"“;-ˆóžWcÎ jBX ¥.LMU—Ï÷ Vþi0‰åâìï’ÙéSXõ£¥n¨1ÕÂì/ëxS¡€þ ؘÚÝ2!€€DWQqùtºyˆ@r"÷uD®àÁM7Œpny1ÿ‹òm)*„ÈÌCwN*çßPcÎ-?FÐ Q}2*óá h(‘ù†X{8ðÒ¢ú¤Wœÿy}59¨C¨Yéõ•{]ºXMGm"ÜÛ¥Ö®"rÓý½©1ãòØ=°"ÏL4PuùÞ¾.9ŒU?™>]gõJQ&ÿyîî3Å?0/æþf_ÛÙ ˆg¦NŧóG±ê§Ç' «¯fE ‘9aážÒµÆT£‚:€Dvæ„ÔlÕżÀ†Â†CÚ£Ö˜HVB¨‚˜Ö¬0lTSÀ§ñwG!¼ÇáñÜqYq5oÔ>µ\\XHf§¿…ÕWÏ9\û±…!)ÿÎ@ × €AÌ7´[Ô¼XÎ{¥|µÝdâÂÔ¢\f! t¤ÆsÇÕˆóžïf ²˜]…§p<ŒâÇ3S'n£¾l™‡{Â×éf¾¡mÖzfî1ŠŸÈΜpè¡CŠ>”ÎL?hÑÜ̯#z¾¼8ï©Q߯Òó%@ù»ÆÌP¥0ûnЀ0¿Ukbò¬s™!P7_îsÈüVÍ€Dõ¬Wšÿ£k~gAÍ[•¿8WqÍßM©ñ©!5¼BœÁxöâ±Ý2à•òU±œÇñ`T‘•Ôxî¸KîžâÙ‹ÇŒ­àh^,£ašoÈ+嫈Žà ÁHÕB×-Ðy¯”/;Äö­ä…\•WhóÖg}$V‡ºm‡Ž+ ž½xÌh,ÀgÇ¢6P^œ÷=¬;„ß«FªéÌôƒÚ®€-óÊýE6;Z^\ðbCSЕ`³ËKsjžh0”ýöØ[o)<àxWÌ7Ÿ˜NÑŸâáor«vÆûÉ‹nlÙ±Zí{ÿ æüâ\ŪŒà¶îÓXìû̓m^–Ö'’m(2¶›æò‹s±ŒâA„³Ícmþ_@j=òl(2VYš[vo3Zy¥|Ù‚B‹·V¢ÝþªÖêÓ{É|C^)_FìÝ!\ihP«Ý¼$ð~›‹7¬Õ§ýÒ|K’½¢òâ‚×ÂLíRó`¬%ê/o¯}ñÔ™—c5=ÜÜDÔ7{®\ŠîWÚ°ô×?ÿþý/䲪< ›ï}¬ª/߳ϼVxñ£Ýîo_ûÚ×ÞÒ¿îƒDRôœš;IEND®B`‚ë‰PNG  IHDR@@ªiqÞ pHYsMMgŒàIDATxœíš½nAFÏu¡ ]Þ€7°]Ñð4X6 { lWt´<€ml7üæGP Þ€pÅs !Ñ@$|)"$3^DZwfvfã[îÞý4çhvv5ØÔ¦6u™Kò€ë*7·}*ÂJ2·¿ÍÞ¿’×À|T%éÕQ=J(HI‡ÀÍÙžR>Cs_gðrÿ¯”ëf_!¤ÂÃÐ'foá^EðªÒ˜Œ;ÍþB-‚ç÷ߧ=SëÀCA¬ ""`"` "`"`"à"à "àààà `¾ÜÜ‘’>(¡­ÏÃî‡ÙûAíÙ†¯$½ºˆž ì¡ìM‘‘ÙŒð)ys„OðSA™½¹ ð/Üÿ2ì¾3ûs]ýÂwÓžÉM@ð“€Pà!!Áƒg¡ÁƒG!ƒ'¾àUåÞdÜ>Z%˹€áÁ±€ÐáÁ¡€àÁ‘€XàÁ€˜àÁ²€ØàÁ¢€áÁ’€XáÁÂ!_ðÀÁdÜ>^5¯šôjŠ<{‚>῎:«Ã·ªrìdO0 xôK×<|ÒßO·³'¼òÚ̳²'7|Æ=ÁÐáËÍÁ]}cæ-ût^H@Qáák€7xÑ}ßð°dx…vOVÍË çü †_iõ0ÏŠŒ©3 xÞÎå­±†l™ªI¯†Ê–àä­ _Mz5HÉ[sýo”›£m‘ßßÝ™Ë'¤æexçíæñØ=½ºìÌ\Êt,%%/Ó`mç!àÓˇ¿Ty œ?z–39fžˆ6² ÖvÞºÑîï”›£íPón=xqÍfÞ¦6u‰ë/W>ž4a¼´IEND®B`‚ ‰PNG  IHDR@@ªiqÞ pHYsMMgŒàÒIDATxœíб‚PDÑ]‹ ë "+² ê°"«4Ó”ë9Ì~ög_UDœY«¯ó6nµÝ÷}{Œ=‹ñiUÕûø¡ª†O¨ýøoïCÉ!ô-ô-ô-ô-ô-ô-ô-ô-ô-ô-ô-ô-ô-ô-ô-ô-ô-ô-ô-ô-ô-ô-ô-ô-ôMxþxÊX{ꮥ»–Z{b;"âÔ^hN;FäÒIEND®B`‚ª‰PNG  IHDR szzô pHYsMMgŒà\IDATX…í”?KBQ‡ŸW­M¬!ŠZ ·lrtp mhºÚAß IÎÐÒ'¨Tp 2†Z¤¾€Ðä”ÔÑSCê=MZ÷\¸K÷Ù^8ç÷>¼çøøøüwÄéú´U°£áìýMµïV àdq±XœOC¹\.è©€RÊFôö¸îÛ‘¡§­zåJàx\g¬Â££>u;íX<‘–ùØJb¡×íÜ™d9½„H[= Ò4¯ËçN3Áo’«K“ já,³•Oy* ”²éð„ H—?S¬nˆAßK~æÏƒ¤(.ùBÁ5•ÿÉ)}v¢‘V?9ôç+ ð]vÕ]„@×~˜— g€µkœÖ¬qX °Æ`-Àg€µkœÖ¬qX °Æ`-Àg€µkœÖ¬qX °Æ`-Àg€µkœÖ¬qX °Æ`-Àg€µk 8,H6½Aƒóaer¹òPbW4²êG ‡­à|X™ÜêÉ‘TSOA#"‰¢F|¡µàš D-–dž¼lv<"}‘ãe³÷NUŽS Á5Õ‹¥…ÂÑÑÁPSM®¯¯ÇÉÖ±R¨¶béx<¾Gpæ$@ÄZZÚÒž—h†ã ‰ž—$‚³a-4¡ Ib&“}ì)5L,‘Úi´*ÒRÃD—¨ÿ¶Bs‰Ä­'57LÿUËÌ\¡°;}VËLÕ¦©Lfë —h¾¦©ÁÙ 7Mù)µÍ•êíp\uÝxmsÇ÷üš¨…Žöö…ZÚæÇåæ7Iš‚(nÈnIEND®B`‚‰PNG  IHDR szzô pHYsMMgŒàÊIDATX…í–?HÃ@Æ¿— )Õ]©‹¸vA\Œ®ÒXÄ!.­.ºD\œ:8.Å¥V\”’‚ÐE³"8ÚÑÁ¡8¸þ …æž‹©55iÒ¿ƒý¦wÉË}¿¼»{ðßEn/4mX”ƒ)GL´éUX—+½B!Sn ° iÃ\ÞiÓØ&*Êš± $§4Q¦:o©ÈfÊ9|—½K"ÕŠ†\²~­¹¡g]÷K3)j‚ÿšÛ±½Ò aÌÇ6Âdšs ÏÕø–Ó$I‚…(Ž>ïs¹œÙ2Àür|›„ØQÒN“03@„71r»ÝX¹Êg^¼Ô–@Q«Ät`‡ò#fM±X,àÀN«Ævˆw34í5¿þoí]ïȇ„$EÜùøÕh =ë¸éìRÔlZcÁÂóéêû1 úÐr×sÒŠšplÝJVЧ °nE]©€»¨(W¤=kÔË ”>¬¿uíWQ×?uÄŠyÍÈŸœyI­U€ ŒŽ˜U2Å×äŸ%¨Š$¯mÛ3v¯/OŸ½¦×.OŸ“S‘s’( Hãd_¶@‘ˆ’†ž=öñÝ@øgÛ€¥ÉrZÉIEND®B`‚ ô‰PNG  IHDR@@ªiqÞ pHYsMMgŒà ¦IDATxœí[{pTÕÿ}÷în`“dƒÒ‘Ø–ÑX˜!» R5"ÚÑZÅ®õ1(QØM€,ÂT[©K¬v´j»`ÜA¦­™Q§:ŠLµ¡Ô"d–úÀñ4JÂ#²ÙÝóõ Ü{÷î½w“]¦Óòûo¿ó;ßùïž=÷<¾ œÆiœÆÿ3èT42µ*|¦\@•€˜ ¦óA8£ 8€ ÐGÀüøPJÑÖkj;ò­-opûÃß—ó˜1„)CtÓÁà7$ÁÍmÏ.‰Ä9‰œ€É_Å û\ž[ßxŸ@õ‚/´7¹rš³¸ý+/'¢ßðäʧøÌËÛ›‚ës1"†€éóÎJÊÒSÜa@¼âV!¤=@ê3Qm½Éc©”<"Y ¶"F™`šDà üÀHá[!¤š¶g0ýà €§:< ŒWŒ×)îð:^%YÚ´cõâîl|ÏXZ?rà¨m×0;CýîŽEƒ¿Ï^ý †·?r;¯P&Š9—íý³qáÁ¡úW¢Ür8Ç”, ¢‡—F`z46þÀC¨«ÙúÎ>¡äÞ?ö? )DX'%Q·}MpoÖ~-àâ»/ŽÛœË@ü EšâWúdyî‡O/ú6ŸÙ ’<ûÆnñ­*;£nŒ=Ü‘•¿!búüð„”Œ—‘>áî’˜¹=7j¼ÜrˆÔ+®0:ÜLè_PÈðoÛÚ\ÕO̓’–eZè:ó.\] àj…)! ñˆ±>¦B—k5À•Ƽa€hŽ{¿ËtNì.ÝÀÀ''ªÏùÌÕu¥W7"•¼ŠEoo_so—Q£ÞÀª;˜1ßLÜpAÀCžaÝÎ|‡––[RDhQÚ$æ»ô¸º ÐuËŸŒtû£Nþ`å¨ÕáÚïÖòÒPàt\ÀqÒBïµ7Ýû•–÷|¾e"Í5ç¼Ä°¸®N¬zpÌéç”:éGÃV£v:\]—‚q¦¡˜ü`òôÀʘÕÚ‰q¡–“f¾@õâ-GSáfÃò<"ÉÒ£r‰¡ÖN¸ £5a¢ÊÀdøî(¯§À†-§ý§U°Û´ÚË´”ô@(V5"‘É^ÿ”N~*0Á¨¼°Ï©Õ^¬å¤Ï¬&IÌ™ÏØWez§µ§l€ÖæyqÊ £Ür(9zëÕ))‰Œ3ëgu¬–c؇ë@O|¯ŠÀrÚ°ùÇ÷Ù†+ļ‚axú ªU<ÚDÄ¿L…æ d€c¶Ã£4¦^-'}}©jCJŸ9Õåô¾Qy^ÁÆm³l;OmÀZŽÎ$È)`¼Ø ¼dTžO0Ȱm¡Ò΄=ZNzHR]62«EÒPt¬h3€¬ncrÆ—íM‹vS4ÚuFLZúdÚ@yÿ^1}~8ã릵¹ªŒf3½y@“á‘|($p­ÒÄ2·jii¼[£mJ[R¦kµ<%R < à°‰à\âk{aR{ ‚·sŒê5Ê‘3ÆuÇ´<ýw8«wQD<Û¨±]Ï¿!°É‰Qî@ ·­XvÌ$I*Í lÒ;× @ÊAœEð à$©»þ+•šØð'cr´ýg•½‚-€²˜ìa_H¥c²¼jk›¨Ià3FV>ˆÒFÝ+z¥)`GFÆY ’L6[z­ià»PˆdƒÍY½TPè Të·®R—µ†u@ƒ.T†¨ÜÂ‘æ‡>®Që»-ÝÃ^PEˆÚCÔ wl? Q•$Ÿî‡[ÈÓ6#Ë.?n«ÿu/ÌÏS—"Û‹ö¶­Ýo£š6þ ÀÖ”za_—wl¾äôþwõ‹Ê­IsBïIEND®B`‚r‰PNG  IHDR@@ªiqÞ pHYsMMgŒà$IDATxœí›MˆU…¿[Ý ŠŠ *bOWu wÂàRP4ÄàèJDQÐ…At!Š"p㠮€FG]‚àBD‚f¦çuÍ$шŠ`ìîw]̈U5C˜!ï½zš>Ës î}§î}?õL0Áÿ³²ß Šá’)FÆ/¸º®¸ºO,--ß„èçü›¯íeÝ–‹k'..âƒÁ`Ñ9J7KUåµýþʵVå(pq™ë*F´,,¬¤IbçË7‘ÕUœ( øñäÉ+Zm;¯Ðõ+:Ž;}ÉŽ¿Fׄˆ×d«è÷ûH²z˜­IЧ+š PÕ¶´Ú‡›ëàlÒ«# TUL±|åîª"^‘`˯ <\æDPP¯ƒ‡æcŠgž)sº¶Èy<4\Æ*¬ó"ŒCåИKKŽ omTü—}`ÌÊ-‡7ÄWìúÄ Á X,Š{ØYæ,ân‹»U1שå(pQM²âp¿«€ÅÅ™’Ì—UÁÒÐà!ÇŸºRZãON™°hsƒ‡-°°°pi«=ü˜©*ªxÂÛ ^ (ŠâBk™S¸¾&)¸{¨q.ðÖªÚYyOáÆºD ]ÞVàÅUMƒâmA÷–y‰lðàÁUcŠWy°Ì‹ ÙàÁÃ`Šâ9Dž*s!7Û…Ó è–CåÅ:òp³]83 oŠƒ¢úæF%ìáf»pf€ÔhÀúþ>‚µþlp×›”yg›mÃ] ORÛÓ+Å#·³ÁY‚Y6õªO”9Z1¿ƒuz‡z½ô ýeNÔFk‚óMÓ©ÀëRÄÝ]—pž”ˆh–NíÞ­Š q<†/ÃKB"b9ýÓCÀ‡AD"êowdvvv¸£ÜòeUQqùùÂk"NçŒÚáßV4ŽvðžDžç¿ëxt;°PAšo† w!ÏóSèøVàDEPMÖ–ˆæ¬ {½^?½ ø­ªhBƒ›„ }˜¦éw¨Üœ©ç¡ ™|"êõ¦¾RÑ{€a™HTÛÐÈLœ§é'*úµÃ“ hP[Šò4}_‘Ç7*aO®Åy6u‘çë¼*N>ƒÝ ߌdÝÎ˨¾VæDþiÿhÜÑ,ë>-è;e^×Ï ¾ã7n¬žÒ´ûˆ"sUEÏ9Fa€ˆŒ°ÃûQ¾¨KxÌ3ò<_W÷‚~S“¼-Q033óÇh¸sÊ!âEgÀ®]Wýlmk7°â;V”LO_m»øuÙYKDk@–eßÛ„=ÀŸeþ¼ùe`ºÛýZHî*¿b;/~™)#Ë:Ÿ rƒŒ‘—šÎi‚ &øào>PN†ùp£IEND®B`‚ʼnPNG  IHDR@@ªiqÞ pHYsMMgŒàwIDATxœíб Â@À=èQ u : rEî‚èÄ¢xRôŽ%<®.ØÛØ‚êƒÓ­[Ú˜ä°BŸ%My×õq©ûw¸ë¯þôù$9Ö¾}8`kfTjHò\¡ËÒ¦öªaíÀï}Nþ*?çIEND®B`‚Ú‰PNG  IHDR@@ªiqÞ pHYsMMgŒàŒIDATxœíš;oAFϵBºü’H¶%Zº „d6ë8Þ™Yû–³w?Í9Ú][WËZÖ²¹¬î ø®N¦-“žHüh·íÁç-û6y}¥®…¨îP}¤# e㱞×'{ZõlÍu‡êÃ_øk-ãZ¾¯‘Šà±°ÇùÞÆ½ÀïvìC¾¿QÁðïŠîiŒ€2ðÐeᡪÀCâªÂCÂ\ÀC¢\ÁC‚\ÂCb\ÃCB|ÀC"|ÁC|ÂCä|ÃCÄBÀC¤BÁC„\Ãw†ºz€ìþhײÉëQM„\Ãw‡ê:5Ø0ØÀ4È÷D#À|AÞ¹ŠB@ ø³–ÙÃ|oíBÁv÷˶½Í÷×ú ¿cGE÷Ô& x¨I@,ðPƒ€˜à!°€Øà! €á!€Pð’ÝíÚñÏÉL0iøª3ÁØá;ïuÛL¯ÏåÍøé¼”€¦ÂÃ%Î ‡‚;mÛé¼yUàaÆþëNxx¸@@ôð™öMz“Ï›÷5*°(ðíüB/ÓèGðSòJÃ÷2í¡óye? ÿ=›­®lè;°>±\~J^ix×yû'¸~…6°6±TéXJA^¥ÍºÎƒœ€O÷ì—a€ßÀOdý*gròyf¶_e³®ó¦ÖÍZÛh5Ö¼/tÕeÞ²–µÀõÖAL+lÞIEND®B`‚÷‰PNG  IHDR szzô pHYsMMgŒà©IDATX…í–±JÃP†ÿ“d±_@Ô7¨`¡A|) *]|]—ND—â8YI:A †R°P;¸¸(Ô©¹Ç¡&6µ·MLk†öŸn’sÏÿåÜ{˜uѨ©*'QdPÀjD¯ƒMGS ú ªrr¡Ã5kÕìj´éB(²¨DG§`kjWÝ)ÀwÙ§"îXç[s;§ŒÜ/ã¤[‚‡å–Và¿4øµÒe^ÑTlì{Ÿ©ð‘, šËžîöÈù3€nñ ÀçÃÀˆ¹$ÍB=Ü i“÷½ð– Sá<ÀÃÌChK#¾Ý-³€˜O#û Ú*6‚÷ÿí@×ãëàž”°äÍì媅ð5;§J7Ý tË@‡€ütÅ~ çs€Ø¢t=©ˆ¹¤[BÞº–;ˆ¥ 6ãh:šRˆ Åà«þ1Ð×~uK¼XœˆÓmÐMП 0î'bt…Ї Á€PéÀ[d{¦³z–Ú¡êYj …ÖÁ0|„µð PÞ6è2äÜב{2båkIEND®B`‚ʉPNG  IHDR szzô pHYsMMgŒà|IDATX…í—KrÓ@EÏs2¶Y-Í` lŠóq, U¬af Äù;!U¬5À·$;ÄAä ëcI.3‚Y¯ºï9~j[-ø×‡ ?Xûå¦Êà…ÀU”gž×|;MµÑ<Âsà§ÐxjÌ܇Q ú\/*ºä»nwð^>• —>y¦yƒD+‰5 Q9ì…áâ_€ƒpítYTÝÓ”è…ábQÙÌx^óLE—s$&‘ˆá‡8´ŒqN3¾ëåHÌÔ•(üæÐ2¦¹Ÿœ+ä k£%„½TÀ¹ KÆ8'ãàAРè!0S/˜T¢®+yð±±Äc„Ý*Aп¯èQx©@‰Ä£áfš^Ià-+tò$òáÒ2ÆÙ+Ë®$0FbøkIÖ*Ãk $$vǬ«‡Ôÿ@Ù0¦¹/°h|µ¼¶€*?òä¢ö½n^-k£„ã¼u Eƒ ¯Nfå=?Ï»ÀìWT“9ç(‹ž×<«’[©EpEÖDYeô–Ì t­¦"`mt·î§cŒ³«ÈÚ¤cbøI.ºî§3,øÆéH—I Œ…»îNz¾oœŽŠ®§$fË$r7a/ ïˆÊiUxjíª¨l§²Å3sÐÍt` |£ à»îŽŠníD×Úh>=¤½0¼-*o à¯Ë੬5QyEI'ïý[ˆ¾›üOf¸ŽÈË´„И7fî=Œ•u+ Gµ=)ÀóÜmTÛ¤n‡2Ø^\ èèATQm{ž»=)¼Dâ<# *O€ÏÀ7AZÓ€'%â§èW Çëÿà7“bX¢9J9IEND®B`‚‰‰PNG  IHDR@@ªiqÞ pHYsMMgŒà;IDATxœíÐ Á×þ!ƒø)ä@f,[ÀÿÆÚç¦#’f: Í€t¼×BÔ«ÄyÍ…IEND®B`‚Ö‰PNG  IHDR@@ªiqÞ pHYsMMgŒàˆIDATxœí×± Â@À}Š@Dˆ qENh”Ä Y4ñ¤È/Bë‘=®.Ø»ìö£,ƒóõ~)5c’c‡>«)É\“Ûô|¼¾óC3¸Á哤&§$ã2o°7ÍjÉäݡ˪J2'z÷þ‡_ Üàò‰_à'¿;ñ×Sü¯IEND®B`‚ö‰PNG  IHDR@@ªiqÞ pHYsMMgŒà¨IDATxœíÐ1€0ÄPZSõP|` ºâà3$ÛM——q Öõ¼ß}Ÿs)NÿD´€¦Z@S- )€Ð@ h  4КhM´€¦Z@S- )€Ð@ h  4КhM´€¦Z@S- )€Ð@ h  4КhM´€¦Z@S- )€Ð@ h  4К dA€Éä’­IEND®B`‚g‰PNG  IHDR@@ªiqÞ pHYsMMgŒàIDATxœí›MˆE†ß·g'Y‘x1ˆ0;ÝÓ;‰?W]¼ ÁD"øÃ"âAüAÁ“"=¨ Eð à DÑS bdƒð"‚18Ël×ôt0FEqÍôÔça&ÆíjÍŽ©ªîÃ<·m–z¿z¦º»¾î`Μ9s, TúL¢Ò\ Ò‘RÙÓU׳]hk D¥cÁôO}qÜ>ak|Wþ—í!"ÿ‹ pT)u½­ñ]aMI]8´K|ÚïgWÛÊp5¤äØîFC_ϲ¶Å«ØPŠí…\¯eY¶ÛuÖÿÁ¹€)׌r}ìäÉ3»<åm—ЧÄÊâ%›Gz½ÞN‡™3ãR€†)a_sçâ{"²à0w&\ŸàV ‚U•_k{‹ÁÃ5@4‹+Ap2>ï>ûÂx¹  ¥p28¤TzÈGþáë.ãâ1^H’ô_5”áMÀ)îâ$IWýÖqÏ(% ÞW*»Éo-çÂ}C`HØ!ÐGúizƒïrü ˜P&áÒ@ã“$I®õYHU@@CÂå`ãx¿*òUD•Ð\ ­ ¯­¯…ª@‘âF‰¸j¡yöX¯×»Ìu~倥}¯k6nll,ºÌ®…€)¦âFÍ\6Ou”4O„Ü6¤ož9Z£nˆ&·®ïU*{ÑEYC€ˆÙ<ò˜J³'lgÕRPÞd3§¶&˜Í!¯)5¼ÓVBÍPÄlž(Wl%Ô\þíåµÛbmN–1»ñ! ð¶­Œ 4¤a,Ê#q¾j+¥¦§!R2y‘§:'ÔU%`aö¼EíçlGÕNb\úÞí„K’,{{QÔFÀäg æuÿãÏœ¾¯äõ»j#`ÒìlÝë ðy#À]+++#W¹õ œ[C|­ó³·¶Ûí?\FW$ sÙ¯ë|ts·ÛýÅu|Å+€„ÙçŸï_^^>í£‚*Wcò?ú@'Ž_ET²¦Wúböï¢yKEßø¬Å¿iæŽ4e5Ž—¾ð]Žï—£L!xÏr®ù­e‚ï—£F‡£héC¿uœÇ›4Œcà“QÔ~ÓW ex¹ Ð\ö€ð¥NÔªük2V)47߉¢Öã.š›Yq-ÀØßø( [Öaò€[Š[\â„ÖùÝ$s‡¹3áR@ñ‘ÆW£?7oãxÓaæÌøÙ ¾Ëó÷î õ’7>nƒÃñ8Ø¿gÏ•?xÈš›Êžàÿèv[‹9Vqõ“ø- ìt:ßÚÊp£ŸÌPކᗶÆw…5yàÀ˜à³Q´ô™­±çÌ™3ÇIKQiÕRIEND®B`‚è‰PNG  IHDR szzô pHYsMMgŒàšIDATX…Å—ÙvâF†¿f`PœÄžñ$9YÞÿr1žIÏl#$¨\Ôߨ‘qr—Ô9‰V×ö×Ö X øÏÉX¤| ¬!X÷‘K –%ï“DJ\³dm›¬íô~zò—À:ôÀ0ªDYØ !ŒqÏ¥¬¬•Q¹ Kþ#0–òàÞ²ûDù‘q÷èÌLÊô˾¦€Ùx|”ŽAûNÿ+ص¾ý ÌÕ J\p¨Ï`s Åó¥Òó€'ôxÂãÿ °‚°%Üc)Ïåݸ’Å9”O‚qf$åÀ³”•B8îððdž;(ÜR›À˵ƒ \Hh¤9†¹øú×"mÄ»“¬Û÷JÌcîżsA0ýŒÇ²–õ#ÁVCu Óµ#uŽl!™50•SºÒIöÁ— ´/Imеá–]ÃtåH½I±Î¿ ‰X¶-<6xèú!hJXäxÒü%Á…úAîIg³î?ž7_as)Þ<‘ ,žIB ȼ‹‘ ÆR´b\á‰ö Hs=ƒ º ¦®œfÚ×â=åï1ChLûc @P× Þ¹‚2Û ‚ú»56î{½ýÊŽ|´`{†Ø3^†º$?©‚aRßI“©Ó=Úǽ{cé0»KŠ”NYÉ UçlÇ0‚ñ!ñ†ä½M„Œ€Æ=·5êLxF[ÒRJx5=És†¹O‡žÀ>kÏ–¼¿Ú…í QW Ömõ°gÀ0}¥ß2™'}ƒOFnߘ”¡U)õ¬-úußâ §ybÛ¾¦;D ¯ß­;î‹9`ÊpSÎ4XÀK³”âo¹j6–BÜßJ¸I¶!; 2vù¢;C•ÛÖ$4þßÀ3#!Ÿÿaî8žvÖ%,ï:yÀK8îÙÁIÆuâõ5 Q/?Éós™~´@~€åS·¼*¤7l¼‰_ ãê²c¢ö†ù->ÝúgÁ”"Ôàí6ø³L†W6“1'!Øûæâì†nàøIæw<ž{ t¤êA— õPÕ!{Æ»ŸŽw,¡¸•Ìmjn@xÐÅ|$o6`-ð áå<6w ƒëg|–l$såùÓE1 Aã“-te}r1ùíÌÅäØÛ÷‚m +3þo2y0„qz÷Ëu0n}ÔŽj¼gìÅÛÐ ¦5ÉÕìËiôâ˜pÃä}¢÷ l5;&•”ÝåT¹°óžÃkøŸ¯çˆ|Ó;í2OßIEND®B`‚†‰PNG  IHDR szzô pHYsMMgŒà8IDATX…íÎA @Arª‡!$’œxÎþÛLÄ¢ì©ì©ÍÇÛŒ/ø¬yßV1ÉIEND®B`‚˜‰PNG  IHDR szzô pHYsMMgŒàJIDATX…åÖ?JÄ@ÇñïK½[x×Ê*`·ñ ñ@z"!ØÂn'¤^ÿœÀB+°áY„(ÄÄ™—‰…SÎ<øü˜y<þû’)±¬Ô9;-^«"¹H&ÅkÝ d(Çíþ$¾p8^˜Éª=‹þ?ðT–ÕJÞ& à£ðÁ£ðÅ£°àÁXñ ÆàÁøâç7z´½å£*’ " ^£kQ΀ƒv6%œ <&HÑž~‚=ð‹‡BÞ÷  Àˆo€ð”"y7›ÄÀÒxVê\Õ°àÔº¶àÎ#ñçYöá¦I÷ž„‘ð °pNB#ÞÖ â®Þžb3N*ùØÆ”NqhÜyCM^iB­÷.U‘Zo]x·nÿЬC`ûëOæѦƒ·ƒxOÏuÙ<ùdˆH³ÑAÒøÈØ¹J±Xt’«W ê ÁšðÔ·§Ï¾ ¤g\Á¼2ƒ^Hå /ÙòÕd6OŒ£©\þ›*À ÍÀô~:[ÈÙRñؽ¢Ü»¶|7Rö˜aíXôÞæ°¹j+¾7ÿ ‡cÁ“ˆžmÔ*ïÙRðx˜“áµÆ\ÙÚä P!wžã͹ÊB>A³‚9û¸wÛóûvƒxV´]]÷øò˜š,•J!·ÃãÍ àÞ²Nþ›Kjô±gfV\éza3™ïg(µç»Ïµ]jG¿ˆˆÀÁ=ÿ›^áÝK å\ËG»ˆhµèõáÏX7žZZ¨ü%‘BdôR&ϸ ˜Ò§æ§—Ê#*ˆMíK <òu­|F2y¢»ÒÙV½ò½t:Ò´¡É z²9÷aC8ò(ã°cjÕÊŸ çqÉc0 æ—õêŒ`R+ ¬§§Q¯FþLƽ¡Í Ï6kÕá ¹×Í _¬œ¿} LpûP2¬­]¾<Úy|y¹Ôq¥;(.J;»Ÿâ›Û{—ggÿs¥y=HÁ_•ŽgOœøWHoÝHp–•J5NNÿ- 506ïa?\žgt«6ó‡E«8ùdf•‹š)Ó:YùÅ¢†uì}4…þW_Ì ~t©^þÁV|WXüjŒ‚и àæ\uÉVì!C† qÅÿ¡2½)ÒIEND®B`‚‰PNG  IHDR@@ªiqÞ pHYsMMgŒà¸IDATxœíÙÁ ƒPÄÐ ¢$zâF1Üè)=‘2^¤o7°–5·çýçýJ‡Mÿ  4КhM´€¦Z@S- )€Ð@ h  4КhM´€¦Z@S- Y>ÀGׯñ™0»˜™ù>[âò (€Ð@ h  4КhM´€¦Z@S- )€Ð@ h  4КhM´€fù?úªØçj,äIEND®B`‚Œ‰PNG  IHDR szzô pHYsMMgŒà>IDATX…íÓ±Á£2‰ ´ ,-hÀ˜ÑšH*$¹>»èA’ôY8#—º€ô(:Go ¾J7¾@’¤ï6ñ) {¾iÌIEND®B`‚ ‰PNG  IHDR szzô pHYsMMgŒà¿IDATX…í”±jTA†¿6Jàj‹‘­c‘JI-bJQðD}}ƒ (ØX((bº€yaûÝÙÙ)¬lÂßâî&1»÷ºbs¿â2pîœÿ›s‡ 5555ÿUݘRjæf-È­åååïUú„Šá׊¬¯˜5€ì°›RºZ¥×Ôètú×üXk£½\pkuuéÛ4ý¦š@¯×ÛPð°(0¨åÚ—BÃ;½^oãŸÄØßÌÖ6p°!jõf¶¶»Ýþ™ Ę¿æËì£ðcäamùC·›îÏD ÆôÄðh¸û¤ð!Ê Ä›ÓãʶՉé¹áŨ¹&Ÿü„•’†­NLÏlŸzÙ'l‡˜ú[˜G¶ RöÑ•ÿC$;I ^^¹¼ôTŸÞ˜@»Ý>»°Ð|…¸7ÔÉ )Ã"P9eóvïáúúúÁ©ƒÁàÜA‘ßan—{Èšúä'@Ââó™F¸Ûjµ~Ž Ä/šð¸Q^feôwáÇ,H€w‹Í•••s£w²CG¢Yj *þ¦'¢Ã › s]àÂï!U?s<£ÁÖÔÔÔÌ„_0}¹›K²IEND®B`‚ƒ‰PNG  IHDR@@ªiqÞ pHYsMMgŒà5IDATxœí›KˆU†¿SÝ“1$Aˆñ…¨+Q\C·d1ø]‰N0Ó’žv@|Ð6‰˜€øè6¦{ u4:ºH@Ìtâq!ˆ¸Ð¨„(š ffº~m ªƒÑz\LÿËÿ@Ýs¿:}ï=UÕÐW_}Ͳ¬ø§*Ž6kfzJ`H[;Õg⸮ÇE’Vaôå›0j‚<Ãì鸮í<€Õ¥fÑÌ&AÁj-o§F›Wùh°<è ü¸Æp@aô¥ËÌ4¬ŠÆ ×8ù¸.§†Fç›1\šôXÎUÀ ë·¯È-á=àÊ4Æs ÀðC¯Ÿ37°t/P úбä£rÀp­–ÿ}ðøÁÍ‘,ÆE/*GÈŽ9¯mèî°o"ÁɃ#Š¥ævŒõAO˜@‰NP(5‹ú–Âä!c×– ¶-ꦕCfŠåæ½­¨¯”îü)e ¸¡q ÒžEÆ÷­·ð¥¦ÔËÕxì–}> î÷§T\Wj\Ø,‹„|Sú“‡ mzåòl Xôÿêì2™<¤àšòÎ ¼ÿCC—}gg÷o”x7XmŸk:ù>pEÐWéä!á ¸~ü…¥fs“ÀP( Kô|&J Àp­–ŸŸÍ¿º1è«wÊubò€ZÍ›ýiÕ.ŒuA[¸sçO)²â‘•ÏKz0ì»7yH`,–O ™˜{“‡˜+ Pj–‘m ™3K­¹9SÅ Xjl3´3êËð3ßëN£ø* ò@z {ýé¥×Ãǩؘ©Jänæ¹þú563­ê;H•¨/‘sB¬»@g¢ºÃ ô @ʹú&>öƒÐL»²¬v Pæ`SI™:}Dðf4ÌxÿMÉ$T¯ûhpx71ä„Ä’94Qš—ï> sB¢‰š(X8Ù½Sðy8"g~‰'ñÙîñ_½\îvà›HÈzÕ­R¹ 3¯núÑëÚ­ˆBá)c©•áô®Ê·xº ø%è›ðz ²Qª¿ÃN«ú¥‡wp"y"©/DÓíÍŸøf÷óAß0OTB&+ñáVåà Ú<É<¥|fÎl+šiWÞÆØõMxiÈt/î´ÆÚˆ'C¦¤\Z9d~éLTžCz1dš¡”N‹™SçâcšÙ!¥Ò78¨×ýe}1ò-ùÒ Àþz}aùÜŠû1DB¦ótÀþÝ#äXú–àÖè€ƒÍ±ßæl`-ðuã9à‹ÖÆŸ»yo°ï“ËIŸîØü]­ŽEcqžp°=öÆZ`6èÛÙò—€NklŸ»ø~0Î)Ðymì#உ.æ=›uN}õÕ×ÿC—v>ƒb‰ŽƒIEND®B`‚ЉPNG  IHDR szzô pHYsMMgŒàëÛú¾£ 4Šï?÷4Tý÷%p—§P9òmS¨ ŸÿÄÿŸòmh§¾})è{îq1o…R’- @V1k Fûȼÿ=rä®ÿ̀ܲ#¤îÇ,¨àå®Ç1áûç@6óGUší ¿)_óˆËÈŸõÁ\ß6HÅhÛ?·â'[ŽðÝsr<ã%ÿÛyF¶ uKÛå-ƒÙ2ÈOŒö ?Ú¼ð‘0’š¬0Ƕí–íkLN`¢âöYN{IçËõ—³Ï¥»¹ò«q'ßlûPcÊÈmcü*Ⱥ}M|ó^y^dV·¼Ù7òdÑèkúg#Ë&›úÉ‚q&äà> þ'Ê£ì!£«py *_ád xZЗÚá< ðºO ö*hë(]”É)¼˜7˜0ÆÄ4Pæ Z½íÇl/T¡ñÄ,ùÿ‰éJ4xØFÐö %† ‚2¼çŸ]~|Ì "œÙŒ ÔÌ$ÿ7€6¸]?Q5]›À×à®ô|¹r ΛB}oG˜ŸøÙE`5©{ö³Ò†ú ÔŸX~€Ï€ç€·<ìøç{ªÛÜUÑÄ[ó&[YÍ)^|)FËL.¡¦rF?7YõÊx=kUÒþ™~‘ ßx+¶ŠQ@ɘµ¯² ®]þÜ „¬ÌaSçÜSß7檛àŸuO€Å}ØÌ÷‹Ö¿÷²sºžò"ëGÀÀp1zæ¿ó[é1Y! ¹ÙWÆÝåø<Ž îRq7ûÑn=¼ð=záê)NÇX-%8çW/ÙF•L)0wÀ}àÒ›«Wèî MžŸÐÜ%èúãV;Äö÷§.Hxfû:VîGèÊ­c ¨/’Ñgîä§ñcXy .*PFÓ)ƒ­½ üøKt桞?>N¨ùÎyÔRøÕ9‡úád]Ô<þø$ Ù+Ïz ¸ r-fn%bÄÑá]`&ªtûÙá²ü pŒ C€îm¨M@ý%¸NCGÙ¿Rà¢Õ“¼ÖN}“¿kàö‚Î6È ºcßöžkB[ŸèØÇ:  œû î¡«ò…M³,¡Û¾‡­¦U¨µU‰2o€ëè3Õ¶2«0ª£‡ú+ˆ¾ñcîùç:d¹"+Àð õŒ|غພàH1ÍC»†®ÂùX<Ûp<…ö$/`‡´]àLçŒÁ¡‹úƒç)Ç‹%€ê=-æ·|Í)p­l["„Ë÷a®³¯ÌGo³¯`öRq[G͵`® a5ß-ÔÂeÀÀ4ª= ”š,£nªÇM¨|Ì3~ì=ˆ òÎaÂsÛYr—@Ü1º¸° ¨iÙ) d¶ó2¥d"¬`ëÚnû”J^¿Äî”6±x:Ô#4Ú ”ƒL¿}Õ²á*Ì‚t˜ÙúZÀ»ÀÒŒN’Ô¨OaåçÐ@çý- ;sä?#Á¦}dѸcï'Y 1a5rIH™GƒeOd„œtf¡~ÄAÆ#OÐ×Áä+@:gPßtoA-É/>óßÎÏùš+ átf ž-œ·‰*¾¾î†TW%yËLÂÆZ™EàKª'à%¸C4Ós–ü¤õ2¸çyT‚g~8FWìcwµýø2ºâžŽÿh_.z;ñ¦ÏgCšd˜šlOà¤)²á±©J¨oƒÛ ì÷-lå7 Gih¥ÍônO‡‡Ä©/ g‡R'¾½_Ÿ¹r'¨·ºôHe H .åã( ¾óGF{e%´òœ³”›:QxŸaß?„´ 7N|j|Íêµ°Xöߺ„V$4 1œ“ó.ŒÔ{Š„iÑú C`GàZáì(Áô;§0”6<<×…ê¨|Û5r î Uæˆ0AqTw›^ƒndÞb! ½±¹©Qô\®=¶ËpeióÀ?È´—ìöšP{fÐ! Ñ$ À£ý2b…»}4sÐÃÎÖŽŠz˜¸ÎöOìuôGô¿&ÔÅGp¬y#”Ÿ¡ö7ÆÕ³ŽÌòš¤ÒmÿÈFX‡B(@Û³ÌÍë€.Q¼@³Ã¿ÊÏŸùQÖa([9ÆHrç9my"è“Þ')ƒóIbt|Û2zÑ Ž<½ìg·± ¹CYÏæÓs÷Opí`Ž‚<£š8Ùæ\Ó`-a< ‡Å¡»ú8Û ¶2Ùéø„W×ì¡ÁÏÈVàoe_ûn0¶+æ$“ÀË„ä:>£8‹”à…m ]_‰²êmÜÐòg0ÉÛ¿ó BhÝF$VN²Ð÷-£Öh,%ØFí¥?ûmnd_8ƒÆ2œœá%^œ(Ιew1,S|¼’…œYµp†*—rA†8î»÷O·ýWÐ8Ósúc¬*N¾Âô¥­®-t™ƒ³.ÊSNxEù€Äf¾mÔ«8’êOÓmžfa.NÖ&°SÓOˆpAš4pÞtɺ×í+rJØîí°õKU\²8ž¯/Î]I#<ó¡(«vúÔ4_TÀ}ëêDV 6ƒñ•Ó'h9À2Z'ÔÆ]¿E¯Ç— —¤ NÑøü=/„a‰!­5 ºÑVMÏ%ð{àS—£‰LÊk'õf;…ÃÝgÙB ¶†÷7¢ö¤oÍVCܨF0}~óREªh íÈ»¶N÷óÖJ>dŒ¡C #ëNफõ€)¢ðŠºîê%¾³ã‚Ì )ûïÑÌÏyŸ¦àà žän"–º@•LIìÌ ¨LzÉ…´ûЪªÎ¸V]î˜ eÅݪ¯úc{º2_*¨nËÕ(X±@ Üžnyy( §¨Fîÿ ˜$?wà”„¿¡ |H¼¿U”ѧ0'Q¿9ÆûÐh‘¹¾“ŠÒãv@îÌfà%i.Àí¢u6ÉÖTñTÈf"NT€ì£µ?÷Æ— hÝF›%à¹î°œŸ&]Õã&ºêeTyvÐÚæ%eÐËØŒ3íwš WÝZÿFfŸ£Bxç'P=R" Í[_çh$›I€ƒ ¿»ªÉ !{f¼©3ËeÅ/Ko#|<À½ Æ—ã{FëÄÌt=!Ñ‚EÐ+ïeß¾´ b÷‹ƒê¤g°SàÅ­À}LÁÑ÷Ùë9qž¶øÌ“B“;ç'S._ÏZðfè}ìDI2æÿP.ºòó¼ù•¦ZŠ\¹ü̲^ùÇØ/L¬’{aBú‚…ñC:f{̉CœψCßù¥ÑWÎú+©ªŒÿÂDŠèONÈF^òúÊJî¥^ë•™´­äŸ·@Þ1œŸ²¥å=óÈã+ž<‘ZY¥{€àw‚q}-~kÞÓO+tž*Œ~ijÊ /+ ^¾*z9j3f‘-ÏC)¿›bD†“#›¨¶õ5º´üïjæzrÁ൹@ 7 åÄ)JJd{Áÿäö&ym.(—Méëúq5a§þ÷$ªÄ}T¡ÎxöN“íÀ¸¢ŽA&0JÍ<ž:y-¿8YB¯Ê’#Teà_à™8$kó¬'…Aâ3£´6}¬ˆÇ7÷ÕY«á € Ïoó ¨þ´ ý~Ï"[ÂIEND®B`‚‡‰PNG  IHDR szzô pHYsMMgŒà9IDATX…íÍ¡Ñc)U1€EMà=ÙRž¤ŠŠûéÚ$I¿ 'R[È®}ÖXâ“¡$IÒÅe¼É9ŽøIEND®B`‚˜‰PNG  IHDR szzô pHYsMMgŒàJIDATX…íÎI 1 Á\ Ê,6L%ür€˜•üéúϨK˜Ç4©|4eü €HòíŸyäÜÓWm[¹ùò# Ýd… IEND®B`‚¢‰PNG  IHDR@@ªiqÞ pHYsMMgŒàTIDATxœí›½OSQ‡Ÿ·…X$:Ë $’6n˜( ‘&È ÿqS$šø'?þ1‚Dœ4%˜ Îp°ÄŤ¯ƒsïí­Òž{{@γ÷žžþÞß=_iχñ“‘ÍTêžÕ–r‰3*ƒ‚t)ìØ•´¸zPø!ðYÑù©‡™,O_—ŸÿúÜ? ÈOêIлÀ¡X”6E‹Å‚¼ü[¥TÍ'ª’{¬£ 3l¿ä:@gòS:‚jÍ]Ó€Ü$#"z=mMDõFþ —k=ŽtÆëö3¡pIE®æùÁ2KÎÉz¬B ˜Ðô§ X用^²ÁÒ5ª èžÕ–ï%ý€¿Û ¯HÉP±OVbWžùim£¢PNøÂ‹»³r$<1V r‰3Çü×´Êùí’<@±OVHÉPò…;¼ÜTP¡2è/«ÈØ›‚|‰_f²ûdETÆü±pna€ ]@šç±«k•Vž#r4\§Ê…ýþòÁ2Kq kÚÛÃu"z@p‡·ÕfûzkÚ½ÖÞíœ¶ØÆ`[€mœ¶ØÆ`[€mœ¶ØÆ`[€mZâh$?¥=¨Þ³Éÿ Pà="—ŠýþݲnŒ{@nR‡Q}!ùäñ¾£Õ¹I6mÌÈ€Î)Ý'è}S"è½Î)ÝgÒ†‘8ì1iýž††Ùñ“ ‘ex |‹IK#|ó44Œ‘ ý²ªÈ“6LPäÂB¿¬š´a<æ 2ŽH/°Àï%*i6–ÁÞ¹‚Œ›6Ë>À[×d¸IжÛ8l °3À¶Û8l °3À¶Û8l °3 Pøá/Lhºyrâ%¬=œD’úì/Êp ~iÍ!Bûr¸NDÐù@`SñÊj©5N#ú®ªNu õÐ_Õkùim‹[\Òä§µME¯úcáÜ~ÇBd²<>úBY*ú`;™ðç°tðÄø¢—[€ºŽË‹ÊX¥•g[ù¸|jÓÞ›oì¸ü¹Ç:ú_\˜-öËͨG5÷sn"2šœª¦  #ųܪUa³—¦î±JKžM]šªûÚœwä¼}+^›–AßÕsmÎápìl~C‡»æ?šIEND®B`‚ωPNG  IHDR szzô pHYsMMgŒàIDATX…íÒ1‚P„áŸw á6¯àöZp Â-(°µµµ1á6&Ü– ˆ˜Øñhæëv³ÉL± ""r0ÛÎ1V§=»®ÿ)PœoÑœïYèÇÑ/ïçý–µÓ$ÈB°v¿ËÖF |döŽ]רoÉŸPDDäpjYÎû>üIEND®B`‚‰PNG  IHDR szzô pHYsMMgŒàÍIDATX…í–±J#Q†¿“dT°p‹-Å0 #,‚ì+,ŠŠ…–މ"6V)¶»«#+›´V*yAÁô T,TÐB AP…NL¢3Î$Ñæ¯î9sÿoÎ=sæÂg—¸ÝìM6… ~„öŠœ”#„Œjd>—6¯ßèM6}iŒl D+2~ÁÁ>éµ!Ná†p¢ÚæQ‘›„=w鯶y^Ê€= 9ûïy6w­—·d˜–¾¶¶K>Fu€5Ð=¾Ð&ù_7&¬I§EDå^…ýŽ‹o;ëëCweô˜‹3ŠüB_)NeÉiEAá°åt«{üïp.=}â ¿†i(’,…ò#…¾€Wׂ¾@fË5.…8øzöÝk|ÁÛji×ûçÝT~ ÚjÏE‰Û>Šÿ ÙTܱèJe˜@,$êùëªùgX¨Ô ì®ç*eÉ0-ÇÖrdk“!cß'.z:”ÎÛóËÀcÚÿžˆ¡ ý¦u 4WÅLt,ûjÅKèsT7ªb·mz ÎH(4 œWê®ÊÜîrìØ7ÀîrìX‚Á.T3À•__=DGréøoŸÏ~r='™y‘öwYÙIEND®B`‚ މPNG  IHDR@@ªiqÞ pHYsMMgŒà @IDATxœí[}Œeÿ=³·×»r×)õvvv¯=¡%*ÔP( $ò¤" ˆòeÐ!ò!…Ô± P1(1|4¡”T…@-ˆ‹”\½ÝùØëQÊG?´×ÝÙ÷çwmgfggvïö.Fûûož÷÷|Ì3ïμïó¼ ìÇ~ìÇÿ3d*œ lÞ|Hr·ÛOÁ< r„{(¶ØNˆð= òÒ]—Édò“Û¤%À²¬Ù Ú"(.„àèqšÉƒò‚¦©Gu]_/"ljhrHJÁqN…›œÜLÛr.ùhË–'æÏŸ_n–Ѧ% PpN†ðó›e3R °8“îy¼3b F(¼4›Nÿf<ñH@¡`_Á Óª‚Ô î2 ã“ñÚ÷"—˵vtÌøDnÐ]E Üi=·ŠˆjÔvà ©Y–}!7†€- ùY*•rµ[6nü°³½}ä:7èð†g§OŸvqww÷ÎFl6”’šiWAx~`hHi8§W×ßhÄÞxá8NÊ­ðT¿pß*—FNêëëÛ^¯-­Ǧ]\\uó‚×ÁÊü©ºyH¥RNBC?€U¡Ï'[ÛV‘LÔk«î·¬o åI¿2Sªre6›©×N3AR,˹‘À]ðÜ {²†þãzlԕ˲æ+Ê:mñF:õ­ÉX5мéüPÀû½2\b©Çãtcà8ÎÁn…ÐãQzC)÷Ë>ùB¡p$´–s„8šP)@R*88Pܨ§ÝÓ,.‡ðÛq LœÉ|vC”nl –½Ä¥Ñ&<6NÕ\.—kíì캊À•ލGÀ[>˜Ñõ•"R©Ga```Z²µm €÷I¹ÁHë_·–^dLÓ\@hë<"RɉÙlêõz‚Ê[ÖÙB¹Àœzø!x”ë3™ÔšzÈc«Ò€™{d\kúƒµtj~H ‘øE@¼ªž›_¿~}²Pp– åŒÿæàs¾œ7íÅ$c¿X½½½r§WFà–ááájéÔ4Zpœ~€ <¢²@Ý„išÍ<äÐ ¼:Ž[/¸Í´œß‹Åé±dU~Ë#™9R*}·½fDáÆ@†1å;—˵Ú³¾hã8¯ìªÇãfB6›!èP”ëj­ BåóùÜæ••[¾#Ê1IéèìZJ ?Š7Aœ[°œØY˜Ñõ•<¢ž‚m5ŒšI$.°7c¬éííý Ê©e9¸<.¸‰B€[M³z3{9"!ŸòÉ”\Æ­1äLï…ˆrX,§­Æ¦ jIÜr— ñÇ,8=L§*ÃÃÀÞo)P)'þå¬\®\ÏBi²!À<ÓtEq2©Ô›€xgmWÞqŽ òª°{·{<€VèíÙ³{¬ oF³*?ˆ»¹ àÚ¨qQú\BU×)C~œ¬«æìC¡à| ‚C¢8“æÙ¶ÝÅáèþeß5pT–€#Žª9žñÎ èGÑ› Wáܨq¡c?2È I€d¼W¤D~û¡‚E‰)éµìñTõ›ö‚,cÏ9U Ðés"òiLSöò ñŠacï bP©°fmlUV­0:™Lf7ïN°utµºU àN2êG­…Ù˜:Hr¢‚ßá½H(­jÚìu?ºÏŽ\!N.Y}. Ó´xD¥¹sç–¼œ°૨’<0&ŠbÌø¤A€Èì ˆv9U P€0SõæôÁ\Ôød‚”HßšÖÚª8ÕjòžÏ‰ r±!‚§£Æ'Z¤o©Ž}c•…jAÅ×lQ¤ •Je5€†º1M‚i³ÞŠ"0;ªS•€ööö×xúïòÇqj~n²ÙìÆÇÛdˆ<U’ûDŸá*YäU% »»{§¯yee0@9Ùr;€mÑ7Ø ñ÷‚°mûø×(Û3™žõA^è7œ„o%àÂ(g}³f}2²bÔdüD×õ]QÍ3ÁÃÊã¡ Ð4®Âh·wö)›7Gîø C@€—¢8#t†¯L§SÁž _‹Ô@ú6iBy,Œš€t:=bµG4-¹»l‡û "®RîùÞâ5 ^¾Nå^׎3Mçø›0Æ‘ZÆ­]ùe@ð½ÁÁ!#Êq6›ýT NGS“°ë•[>;®—ËåZ!ø¹W&ÀýµºC5N÷¬ÿeØšH¨ÅqQ†1Hå/@hÆÇÊ“ ýq…YèèèºþmïÇ»vµ-«ÅܼçóöI¢áH Ôq†aü56f²Å²œë ü!ÛÐ:±•"·dôž‡ëéB‹Å™eW½ ï1ò†L&}_-˜&ƒ¾ÀJ?_{fppðи`DÄ5 ýn·œœÊ2ÿŽÓñ`›wUÜÒœl:õP=7ŸËåZËeõ;ønïlݺ¥f_¨£|³iÓpwKÒ} ÷¦ÿR.œÒ××·;NŠÅâôrY ç’˜'£UäÏŒú!4 AÉ€Oíܹíåà®-¦i-#äJ¨¬4,ˆ;¹RWý*ŸwŽká=&Xaè©Ë'r@¶íö‘‘ÕH"ÃP°¬«AYê—g2úò8ݺ x¦é\LÐwâ‚À#ÿÚ±íšFŸV³0vDæ÷ÃósðÃHÿ¨ U0ó¦}·7ù£À«É¤v^OOÏÖFlM¹\®µcFׯ@\æ• ðR::#êPD€_?H&LÛy Ä93…„Æ…º®ÿ½{ãÅØ{é÷¨Þ©¾Kå.Èf³Ñ…\ÆsP2ašö½ N±2e•ròÎ9sÛÒ¨Ýz0z|߇àfù/TÊ¥ fÏžÝЦl"Ge/ƒ`€`ar'ûÜÒÈ’F,Fd‹i:‹ XŒ°$e‰aôÜTïy"/&vXÚ²ú5ÊÓþ„às´çJ¥]«MÆè᪃úÔ™œ l^qU=oûZ˜pgS±¨·TÔ ¾A+|¢½Å÷•’Mjš|œtÝ¥R©ÒÖÖÖ©”ê"Yj8T'äT3j$ðæØqùØUišø‡ ûëÜ-À¼fÙ¬!BnϤ{~=ž)D³ÿ2£™¦s7 pl3m¹„¬,oæÑÜIëdš¦yE[……>N3Ã<¯ÀDzºþ§ñü SÒÊÝT,êI—'+ð(@Ž˜AˆíÜl¸] ¶6B¸Q#_M§Óïý7œEÞýØÿ]ü»þÔÄ2ÙIEND®B`‚5‰PNG  IHDR@@ªiqÞ pHYsMMgŒàçIDATxœí›±Â0Åž"1ÇÐ0 #pLÀ4©²…)]:Ûˆ;¤êÿ}õNDäŸ)£°?ž¥æš$µäò¼ß=÷[Ùüx’¬ÇLI¦Ïa=÷[ ïc¶æ^ûM|#ÀOcZ€Æ´hÐ4 h @ Ѐ 1-@cZ€Æ´hÐ4 h @ Ѐ 1-@cZ€Æ´hÐ4 h @ Ѐ 1-@cZ€fü£©dÙš{í·2<@MNIæ$ó:wÝiá,'"0žBóIEND®B`‚^‰PNG  IHDR@@ªiqÞ pHYsMMgŒàIDATxœí™ÑKSQÇ¿çl:7Ê¢ˆª‡F$=T‘ Ñœ1‘2/#zJéjH@Ðê-¢Þй-Ò‚µ‹,·R °¨Ì´Ê‚È2WŽœ;§'i^¯ Ás®Ðù<~°ï÷~ïÙιw€B¡P( …B¡P(Šÿ "ÚÀ«éŽ8iLÅ[ÛD{.¡hh()JÛ3ùåì`w<Ò+Òw1P¡ŸþËe7JŒÐO}£[¨ï"ñàfº3ËJ‰HZ´!Ä®«éy«2Et2÷/„ðÑh4g/ɬ2›õŽåDûBʸßÞ>I)ßj6ójúcBÚ쎆G@x•ɨ¢Zkº(+‡›L³wCÏß»·ï' 5†Ñ¾-e»GF‡ž½’™° ˜áÕô6'Œ:¥¼¼;~*3‹%€WÓGÌ?dÙ†T"òIVË >#ü¤+]O¢—3f³¥ÆÒ}øÛZZl¦¯`é)Hº9–0 f³ÅtÙÌ«éßed°ü$Öw3ø•0¾Ãd´ªJÓo‰ö·¼Hv†9AQ'À1ÑÞË¢ œ[âk…©Ïa}=ë2õ‹ö¶¼€ÚÚfµ#`óœÇùT,T!ÚßÒü~¿mÚÁ:@°'_'à­©x( #ƒ¥L°ÒK8’¯‚ÓãO0=$-5…›ÎüÊ\•¼pfs•2ßYó0Tßt„Ç þsÔ¾·'zmLféTkÍ嬀sVã@šûÅB/eç‘úà©ots°»È»x3”A³ââ‰òŸ\Cˆ­ Àœ³??•ì ¥då0"¥Ÿ¯Å‘cä¶åëœð ÉX8"#ÃB/ ÐWæ:* £o‡Ï‰ö/„ðú_Åìƒé³O9uHÚëÿ…Ð]Àçkq̸2Jòä7ÅŽ¢Š{W¥<ïBè ø²é7ã@6_²åì5ËåâÁ¯Å? 0wÙ®aÂáá%uÉxð­HO…B¡P( …B¡P(ŠBüêÝSçIòIEND®B`‚…‰PNG  IHDR szzô pHYsMMgŒà7IDATX…íÎÁ 01Ôý‡1ž¾"G,ÊêÉêÙ|¼Íø"€~…¡¿¬éIEND®B`‚‡‰PNG  IHDR szzô pHYsMMgŒà9IDATX…íÎ10A&âP€„ =" Üëÿg#eõdõl>Þf|Àe#;X{¯ IEND®B`‚‰‰PNG  IHDR szzô pHYsMMgŒà;IDATX…íͱÑO!(E¢k3Z1r"¡ôû¢ÍVàwáÆ\{ÈT}¶ÖKNM’¢ÏàíZ6—Á"IEND®B`‚§‰PNG  IHDR szzô pHYsMMgŒàYIDATX…c` ¸…¤4¸…¤4Pb%š©F0ê€QŒ:`Ô£p°PjÀJªä¡Œ v­™Ó@®þQŒ:`Ô£uÀ¨Üž ?¶mĽIEND®B`‚0‰PNG  IHDR@@ªiqÞ pHYsMMgŒàâIDATxœíб Â0ág†H‡”™X!±3E¡a S€¨ ²Câ¾*Å/x¾DÒ?+£ÿ`]o§”\’$5çy>^{Þ·:Œüñ$y=fJ2½Öó¾ÑøÏÇ|úîußd?Íôšè4Ðh Ð @ €@3=€fzÍôšè4Ðh Ð @ €@3=€fzÍôšè4Ðh Ð @ €@3=€fzÍôÚî_¾{Ý7 fIÉ–’-5K÷{IjðÅ=4á£÷oIEND®B`‚œ‰PNG  IHDR@@ªiqÞ pHYsMMgŒàNIDATxœí›ÍkAÆŸwÓ¨…R""ö`m<¨ѓފ6‰›”ÒTûñÿëQiý"í¥Ôƒ1UZM*´àÁ‹‡"=XÌ¥Š‡–Š©If<³›M³»“1Íü޳Ãó>ûd23ìÎ …¢–!»m¡7Ï ‚ñ­¶:àËëàX€F± ¹Gçb·×Ì:Û À×ñr ãšìè$E áÄãh²X—Ue_°'̉ØnU£4‚еïÐÑ/Ÿ>¼}oÔÁÒðžûÀµš-{•#G F#¡ìÚB nökÀG¬UŽTFÛr pN(ûtóÌ ªïæ )ïýÊÂŒ‡±#ïåðg©«N ¼×Y1]ç“ñ¨í½…¼Á7¹¬ó^-³¸0T² ÈF Û€lT² Ȧæ(¹ò®ìf`ý œ$`—Ù.|ÁÈG«f8À¤˜vmûyofrrÕªÖF1 Àè=ˈØ‘7X´Øôäá@[6]ß߈tÍNE_ÛÔ3¥è_Àèñ§Èß¼š‰ðÊìíYÄ0_ rDC" oÀGÚ‘c¢ è8u®o/'º/ª \DxÐÑÝÝ(B\@Ö•½p!ÅlМK×_!¬ €€Ó" Ù… òe4ü¯¸…ø*ûˆ¦ñý"Œd³ðhÍ»N¼Ä1¢ì^ÄÆ,otÌð‡zÀ˜esj~+¬m@6*Ùd£m@6*Ùd£m@6*Ùdcå|€8ÐRâÝÿFX/l¨­À±PØT[hÓ5Éð!‰T†Ü£…U3Ø$G ṸþÜp-ã„Þd‘ó›=€1„‹Ý<°9ø{\žêÓ#‰‰‰fušXk…@ÀR"õ8­k´ |sºˆp`Y„®Q³" Ù†ã¥Y]c7dD³Áw Úº/F–ß-{YÈ/¢ übb*úF„¶áNðxkÓM‰(X. 'âcÏDé~34??Ï/…ÎÄS_WW:Q¬Ÿ`V8èr2½%²HÉ7®þÎða¦iCÚìi&OŠÓu¹ºëÏŸÜý\z …¢†ù )ÐôÆlëIEND®B`‚7‰PNG  IHDR szzô pHYsMMgŒàéIDATX…í”1haÇïŽP uPŠ“)â$¢¥qÜD‘ŽŠSA‡Ú IŠƒ“êê`M[këà  XÄA¥SqP¨ZÄÉÁE¡ˆVEKµ¹û;$gÏ`›»Xìr?8øŽ{ïÿþïÞ÷}°ÊX°è<^šÅhý/UÅçg£Å NÈŠ³dÂJªõ{‘ªT2Oƒw!ðVâø¡òOR•JfÑKˆÝ×µ¤[î ö#Àäƒé_š2ÂFþœûqhúÆéïÁw7<3=¾ÐÚ¾ûNjmzÆöªÁ?<Æ+n,—Ýž›ýxäÕ­3óá·>éÃë oOû±{_Òß6‚e“ ‹iD`V±ÙOm=cùJ}Ü2ª²l®t^²³µHm†¡jqÁ…©‘¹¥FÙ°­lï@ŸP?€ªõ¤8®DßÔh±´¼×tæ.w#»¸7!áXuVžaG'G 7iGlGn°Ë‘?¬D½ ™ƒÉ€yßœÃå«ùQtcí¬loÿ^áÜÖ×™~ûWß¡«<\|U3öËö\Ú‰ã>lTÛí3æ{&¯zG¯©CÞqbh«yÞ¸ÁÁ[¹î¾ò•“oâj5}ËìÊlvôNðÒOÙÁ烅÷Íj%$$$¬*¿¯8¬‚âQIEND®B`‚#‰PNG  IHDR@@ªiqÞ pHYsMMgŒàÕIDATxœíÛ1Â@Ñ "¢  A  &.BAèRg™a_uÝýÛþ É?¬‹÷Çóa˜¹Ì—çýö0vìŒK–ÇÀø aÐðyüÚySf€ŸP{€­ö[ì¶Øl°Ø `°À`+€=ÀV{€­ö[ì¶Øl°Ø `°À`+€=ÀV{€­ö[ì¶Øl°Ø `°À`+€=ÀV{€Íû4¯µóÖ´3œ€ ˜–s’lî »“Pçç·yIEND®B`‚‰PNG  IHDR@@ªiqÞ pHYsMMgŒà¹IDATxœíÙQ ƒPÑ¥Á °€¬Z¨ÄŒÓäÍØÉäþí ä8¯ç8¯G:|äñ Z@S- )€Ð@ h  4КhM´€¦Z@S- )€Ð@ h  4Кålò¸~Ï´€ÙµÀÌÌýû²%.¿€hM´€¦Z@S- )€Ð@ h  4КhM´€¦Z@S- )€Ð@ h–ð.VvbñIEND®B`‚…‰PNG  IHDR szzô pHYsMMgŒà7IDATX…íα01.K±5#B†€RßÛ§ˆEY=Y=›·_ð‹i‡|áÍIEND®B`‚ ‰PNG  IHDR@@ªiqÞ pHYsMMgŒàÒIDATxœíб Â@DÁ5E8CrM´àˆŠè‚š,“Ð… @Dz؉.»ý/©ª6¨—åvÊK’dËyšŽW±ã >M’×ñc’ñpžÇzïJø  h  h  h  h  h  h  h  h  h  h  h  h  h  h  h  h  h  h  h  h  h  h  h  h  h2ÀýË{W.À–9CÖ Y³ef;ªê¯=-R)ló`IEND®B`‚_‰PNG  IHDR@@ªiqÞ pHYsMMgŒàIDATxœí›MhUÅgò•¢¨QW¢¸iÉGEIE«U Í{¥®D*Šv!*(‚ ±´bÁ…ˆqaÑ/ZJ+H›]¯Ï7zzò¸v”ÇEŠT߸×#ÏüÝy)·ºƒ08î«y?p^Ò7Äyµ,€uc¾<Ž< \”ÍΫ Ü0æ‹k5Ï—ÝV­èV«ëßñùÆ~sUí5†^wïRËÓ˜õIß9Nù¬‚0tÀµÅµÞÜ” „•㢗Ulý¾ï5Œd\\ç!ýSñó {S¦0Žü²*00é'@'=·ïúÂ;˜ôýÆ{²¾µÊª¡2ýSn¿š2 61.lÑïP%Æ}3ö¾lû±TÜ#ïL*Àà”û,Oç$}›8Ï-îJU*€þi_Ûû禗?òË* À† _AìYàÂt¢˜Š:%6Lú’þ¸4(¦Ìï *|¬óÚ%ù}àÊT »êÎCÁ3`pÌkâšg0×¥a¬R6:ÿ¤ÂfÀÐ×âšßnLú.a¿`ÔÑâBü0œò >ÙýåÀÖÀµñ F÷d“F~Y¹˜âI£GSf{­ ®ó3€¾I?h¼»#J;ܬV¹è›lí~%ë;ÀiŸTŽ3 óBƒöûûª¶¸+UnÓ<è®·•éa2]–ˆBÿü˜€¹êLœËGÌ¢”ëS`¾®—A£I¸rß̰ËòKiW ¢ï†üw‚’›Ç¢GŒßLùF¸ú·ÐYSÐNÅ­£íÀ»™D(,…säZªiði*hÿÐ!…rdX'ÿîŽg"¡ð"ŽèçxI·¾JúB2ª|Y,e>Û¦ï©é໤/U ¡´iØÖ×-t+°ôÕ¾*ƒPê}øy]_ÝœÌÖአ”¾5ë:ìXuàTÒDvù*Y‰›[õ¬»Éž*€PÙ£h®¡·míH™Z>A–§JŸÅ͆öÚzª#py‡§Ê7#Í:Ï ¿˜2uúðT‚*€ä¹ãÑcÂo¤ü’¶ÌÕØ©¸÷‚è>`&“~x pp£–Öü¢» Ÿ¤#Xg0n×¢z5 Mú*p“€ùÛõkÚ œ(£½àªë‡kðmÑm àPCßiðS6ËóÜ,€ù-ú2’6c~Kúʱî QSÖ–Ì©Îþ¿Ì$5·Uaí¢ýù­…ôlÕ5uÕUWg‡þ°QVìÍIEND®B`‚u‰PNG  IHDR@@ªiqÞ pHYsMMgŒà'IDATxœíÁ  ÷Om7 €w@@òÉQIEND®B`‚‰PNG  IHDR@@ªiqÞ pHYsMMgŒàÉIDATxœíÖMHTQà÷½^K² ‚ ŒVAATÒ8„E&)&Ì"Ê¢ŸE‹¢‚ E`n‚(ˆjø3W¤¿Môæ80›­¢¬° 5¯÷m#ä\+̹3A}Ïò|ç¼|ç»fcŒ1ÆcŒ1ÆcŒ1Æü/˜k@E·V!Ð Ë^L7àû"üøL‚o|Ÿ <˜ÁöA§ˆ•…¸·ºþ«=žú.«úêù²P}ä d‘O-"[B•NÇgS_‚#…l§ð˜KN\!Ø @‚.¤œS¿ûðOª~(·úšJþvÆcŒ1ÆcŒ1ÆcÌâ;_zòê±nМIEND®B`‚ȉPNG  IHDR@@ªiqÞ pHYsMMgŒàzIDATxœíбmÂ`…Ñïgˆˆ2bƒÐx"š¬6 Y,á´‘]ƒ%8§¼zÅ}·€w0ÖÁçùrK×êc‡>3j^êëöóýû??l_ðùª¥ŽÕuox7›–ÑTÝwèòP£æjÚ»ð| Ñ Ÿ[ÇIEND®B`‚à‰PNG  IHDR@@ªiqÞ pHYsMMgŒà’IDATxœí˜Mh\U†Ÿ÷ÞPE¡ÚèV\·d"©hTJ›BÒI àJ´þkAD¬`ºÐ­ŠW®šŒÉ¢J5›ÊLÒtÓŸÅeDÒ¸ˆ:s_¡2¹÷&™™Ü™Dzžåùî9ç}¿ósÏ9@ @ ¸·Pº`pÆJþˆ ‰À{ «0 D@ÃÖÇ‹“ú¨9žI@i6©q¯ö˜Fm"êk.ˆöJÉ~!“[gÐÿ{Úç"lëL¶8‡ÒŒGç€R!IäIDÖך¤±jY ésPªø0ö÷À¡T ãý™C¤¬§¤ãµ²–óêl™€#s~´£ÇÝõÈ|iâ+`¬-ªÚr äóÝúmã€#5 œëÚ|#{elìï„¥%ÀÀ+ç¾µöægv‡k1AÀŠÖøåú;]{¯|<òW’ú à™×‹üE\°=fer_…ì°—SwvÏœ;|3)‰ðôHñQ{ &A8ÁŠ «DÄŽ‘4ç/bè»ó£¿'¡3‘œÌÉ-¤5`k¨$Lu9bÅèºNÛÞ1ûþk?µR'söš¥ïà{O‘Ö ê6¿ô€*lߟq2Þ¶Vé\¢¥¤œL[¾}ÀÁ k0_‰Â!ô’r­?ë¹MÊ Ð²ÒÙ bY—lª|.¤óe»6Èt_Î{¾ ©Z@:[Ü#àîµµ•YÏ7&v'tY"“ý¹ÂpãóÞ§éܬ·_€q©@ážñ&ÌW'e„Ÿ»¹â¾f§oªv3Å·@xˆÜÚ·}›B0¸¼ô¸»óö|éâ×ÎÛ`B7³ñÈw"À°Åæ²s †Âæ`»kÝ|iâR#ïu÷å¾þ4Àýáš@1Xƒ %ztIœ}òÆc¹ññ—£MVê  Ü× `OT˜J²æ+Ö"…q÷צ°÷oôN­sÕÀ³ûŽ­_\³î‚ÁŸ „0Dä¶Nˆ áÒÚ» õ¾?Ô€“9ÑM¦&›ôÛê<Œˆ½0+fq¨tæÐ+ _1€ôÁS½âûSBm(kp@@‹ á÷¶‘ÁoÎŽþ\mlÕœŒ·”)½¡¤ió®_Z‡üË Î}ðæõåF-Ûõg=—”«ˆ5Uf(ïÆ°¨-0Öµ¾W NÜ`™œLa»Lè,%ó«#Ѻ-Ãi7ç Ä ˆt®pDÄ‹ÙNÿq(¤ÉÏž~ãÝʧÑÿâ óžy Ü©äÃO£Gà´þ/1Þ¢;€<*­x‹[e`„<ÚiŠ¢(Š¢(Š¢(Š¢(J§ùîs?õ¢WIEND®B`‚2‰PNG  IHDR@@ªiqÞ pHYsMMgŒàäIDATxœí™Ëo[UÆ¿9¶K¡B$l© AjÓvã´¬Kì‹ÚEZ?²€,ÈÃüˆUØTB¬BB¡$,²0¶ Œ¬¶‰ha(ÞÕT*F¢Í XTUÕ¨±Ï°H×÷;~Ük1?É›™ó˜ù·o`V?»\öÆÆþŠ€h,VÕ{‡Ée©»³uòØ”°~ϲ4žJ¥L¥ùa¥U]Âú‘ÜìÔf5Pч¼hVÕ™h,.ˆ;‚õ.ünÃc$“L&#+ëÛ5[_µ…1 ÷]Ð+ËëÛ·-áÖ»¨Ø‚îÎÖIE–¼1 U}x˜ÜŸõU H¥RƲ4ì•Ióm}Õf§6Ed´Ô¸ÂÈ‚Oë} ¸p¦åQq+Èk³Þ·€­¨Ùzßà ƨ ººeŒÚµZïÂ÷ïxq.óWL¬^váàTñbôô4:;°`òô— Õ»µ?¸±ß-PM#Ò‡ÈxÄNˆæ÷ç=g¸#¡§ QBõ½¢< ªé‚(obÄŽ‹Jš*E0(£¹ìÔ}7pèf´ñåÓJ[ÇÙEDš€“w;ÚÞ¡šÈe3/ªù௠¨ããpáIEND®B`‚6‰PNG  IHDR@@ªiqÞ pHYsMMgŒàèIDATxœí›OLUÇ¿¿™YLz!URizq›ˆ!©Ho½l\C+‘ÀFÐ4Ù½z±Y 1¦õäÍè%šf[0„Æb6-z¢’].ÅaléYØ™ŸöÉìììÎ,ógÙÏíÍüfÞo¾ùþÞ{3y4hÐà$Cv‚"‘¤¢´äúˆñ.€KÚ¼àifµ“°Ê„»…¶¹Lf¼`u‘¥=ý‰n–ð€v’ôÊ’Æ-Φ–ªEÉÕîÐ=˜á€3®ææg@·wîm¬¯ýR)¨¢Ýƒ‰0>õ&7!ІÛ;óëk+¦§Ím¿XrñŒ‰G%‰4cw3N«¤{db±˜¼‹æóšÆWˆi„Óúó¤¡Ç¬ÊˆD’Jè¥ÜïÐÕ<?©ûZžKm{’½ËDû­rÓ ¼ux”²ûÿ¼|Ñ80JÆ‹•–\J¼§Ð”ŽËÃÀò\j[ÝÇÏòkÅg+¡L€âTwx8¹8ûåß^$ê%Ës©m%õnjϘ€ƒyþ0@¢.çæ’Ìóú6]e1&×µéÍØÝt9/ß0æÎÀ9cŒ™%+¼ öµ`’{ÙêÕL€…RïìЋ‡5Mxˆq¯ I?ÿðÕ†Ó{ÞÝ7.h=xÀ)§˜pM–µG½±xØéý-@$’TÀê÷Κœ>Ë*}â´@ jÉÝ•O]& :í#°ôôÇ;ÀóºŸ@ ‰$&º dšvÚW °²~‘=MRo;í+pص>1-§§²Nû ”5Xuïé¹ÏÝè3Pص¾ºn烧³ìéw°Më/̤þt«ß@8 Ö¸â€7ßùðEÒF&t_’´‰…ôä;ׇZr7ÁþZ_àØ½±xX–µGL¸†âZàaM£‡Ý7.X]_˨¿pÿk׬/p,ÀÁ[šùZP¿‹D’]VOë \^Q+p©éÅ­+¬Ç¨oÄóA‰ÆzúãÆãõ¶¾À±Ä¸gÒÄMéK!Ö8@•Õ[ö,ÂJJ!Ö8`9=•aÜ*N”BP¬/pe Øßi» Æc‹°&–è[–¤iÀúWÈdÆ Ä|Ö¥ð:À-b|±¾ÀµY`qvò;¥`…_Ö¸: Ú,…jøf}«ÔP føj}ë ¡£–‚ßÖx²Q—˜ðÀsæÆe»ŸÌ¼Ä—¯Â?Τ¶¼íG_µˆ¯ÂõÄL€¼¾‹Åªí'4&¹ç1fäô]4Ÿw3)?1æNÀ–1ÆL€U}CÓøŠËyù†¦ÒU}›Q>%— À„»ú61MDû­î§ç-ѾD+KbÆgL(ì´ÍXÿïá´ÂôqAl–.Ý1NÙⳕPÛvyPR’y>ÐÛåUºJàñ#o—üo~˜@Ì#‹3“Ÿ™«8Åý•ým%ÜÞ™QÔ»Ô<‡Yš™¼U) ê¿±¾¶~õ_AÔ…c÷ßeIÃÐÒLꛪQvn¥ÿmŽ€®â®ëÀý6GÀkùm®Aƒ'› ™ž‚ß’IEND®B`‚ö‰PNG  IHDR@@ªiqÞ pHYsMMgŒà¨IDATxœíб €0Ä@Âì?ˆABËGaw_½å±!®ûYß}s]œþ‰hM´€¦Z@S- )€Ð@ h  4КhM´€¦Z@S- )€Ð@ h  4КhM´€¦Z@S- )€Ð@ h  4КhM´€¦Z@S- )€Ð@ h  4/pQ€fa™¨IEND®B`‚e‰PNG  IHDR@@ªiqÞ pHYsMMgŒàIDATxœíšÉs\Õ‡¿s[12ƒLlPõZo°1“Á˜¤,$ƒmåoa‹d‘ò‚ Ëäï ˜ÊÖFC…¤B€*ª(Ôo)Ç–q,ëR;âu«û¾î÷$Qè[Þwú¾óûúܤ†m¶Ùæ§Œ´Z<þüm‹‹×ŸW‘@?pVtù¥0 ëÛ^wDQTS©¼ ¼±sç-/~›­mðùçîèß¹øðpæÒe5ŒÖ<ïÃrÚ.†$I³*“À®\>þniñ7{÷î]X»l²ô÷_{æð»Ä2QOÓCE6\$ë†Pܱ£ÿÅìr“DN¶¹ÇÀV•Ð6ü*ŠŽgךÀÎ÷ËÄ\š>ž³ÇÒH’ä`§ð+Hv¥I€¢ï9ÜsÀX&·‚„ÕðSt @S¶&}FN—6Ût 9Ã/T Ê.6 ð<ï kxw ›rÜÇ€µ2êyÞÙ -?Ì¥éãÆ2 8Üà²ýïûs¨í™8ŽUÌp§CùµòL­Vý ÕÅuÀÖ”Pdxè ¶–„¢Ãƒƒ€zšËîŽú¾ÿ‘ËÞ®DÑW ËSÀÏʯ 2†Õ³ ÀæJ(+<´þ Ô’šç}¨VF…ŽÅ°ËªL%IrÐuÿõÈÞŒ¹†‡Р^ŸBŒNw8”³úšÐÕ$ä ÿ-jFÃpèý<÷È-òKìÑ þžçºåJˆ¢s#vš’ÃC¢hþ0¢g(PBÞð‚ ‚!—ï/-éI+a£ÃCŽwõÃêYÔŒWÊïTÌd}õHöÂjxç3/Øc½†‡& A{±g€ÛÊÿ‹Vކá}ÿ¨×ç£ÓÀn‡Ç^ìXïöÒoƒÂ@wT­Ý¬ðP°È)AYPqùtYxx(A@Ÿû•bOã6 .\5¢Ç|ß§ ýnRŠ(TBiá¡€wõ‚¡÷{ húgD®ZÑgË %N@ƒ8Ž­˜ÓÀm9zÕŠ>;ìûo—ÑWƒÒÀª5HÇ?¹7X´¢£e‡‡ÀZ–Y@¸îZ¯p]–Ë×îž)]À\š0–Üþz €À€®×ç*±µÆ½ÊcMø=]nqI­<]«UÿYd_k)mVÃOÓ}x€ÝbtznnþÁ¢úÊRʤiúÀòÊ3WA[^²Ë22<\ý¸ ýnRøä ¿¤ªKu»M¥œI(T@Îð×Ô2¦+ÿ†ûŸCýž2$v’$ÙoUfq ÊsaX¨§éoÅò&p«Ãc/ZÃȰçý«—~" —ð 6KBÏG ‹ðdzájž÷–ZžÃõ8XfæÒô@Þ~³ô$ I’ýÖŠó™_ ?³^A­æý5§„é^%t}n†îv(¿&ȉ ¨N»ì]¯§O‰áM:ÿ\à?ÈçyŸ¸ì¥+qïS5³Žá9î¾ÁFIÈ-`#Â7ˆ¢ù#ˆ¾A‰r ˆãxŸbf€A‡òžÂ7È+Áˆñ}ÿS×ý_ó‡7Îg¾aXEå8pÍ¡ü.«2›$É~×ý& Žãû3K®ðCS®M¸Eó#ˆ¾NÁ“Ðqò‡·'‹†ÕANà: Vf\&¡ít>˜t¨íš8žZÑ×Yù{{” ÆèH»IXwVÂWœÏ¼/;<@T§9,v,î¶Vfâ8Þ·~I þ^áÐÓ’=9ìûµ…‘wDì‘ >Ë^j¦éвå#à‡>6%|ƒ8>wT±¯á"¾¾Ñgþrh(]»Øt¬Õ?ð#CS‚9Ëq€Á¾öÙÅ¿—§6[RÑñÍ ß`E‚=‰›„#Ù…V/‚Ë6YRÑñšïŸqip#‚`ÒI‚6gk h»guI ¿ßJáA0iEÇi#A…ÓÙµ&•Š9|Ùâñ+á=¯i“­Â°ïO´‘P¿±£ïTv±I@µZ½ø³>óŸøZ‘WÑÊ“[9|ƒaߟP+‡Q^ù7£ò—ïvôÚ{ï½6»¿m¶Ùfkñ=d)#ÇǺËIEND®B`‚!‰PNG  IHDR szzô pHYsMMgŒàÓIDATX…íÕ;‹QÆñÿ3ñ†‚ÙNP [ÑVÄr6Íjav°qQ4),d?H𠈂٠îB6.È"ÎÆÎÂÚÞÂJ°q‰ x!籘 Ø™Ë0)ô­Nñ^~œsæ üë¡q’âzcPùdÚ]û\$ 3o¨z¨íÅ••Có`qÎßö÷’$Ù7@µÝP}š$I¥t€P0Xpeªó0¶$gknÅ—­R™ÂĸÞ\-+!_ßë«%À`i„àÉârs©T€3 ÛÝÚrãB©€åûÌûÛç}f_Œ1ÆcŒ1ÆcŒ1Æóº²iš¾­xÇ:ÊwQ|+"ZAo r.߆pe_‡gËäU0€ü/…÷z–NÜ¿7³{lll®lv/U•,k}©p¨gyfúö­7ÆÇÇÛ‹ÍõJ7†3v.Yþg’$+Êfÿÿªµ,˼ÀÒY–uUÎoJ—»ËêÕ«Ò2ùSSSË<|tØZ(=¼íQTÿ­L~é8çˆXS(ÝDý­q¼êÒ"sW"Þu…Ò]Áû(Šê‹j¸Gé+Çqâ{l8_(½…tÎ;×*¾½çJÓt-žaÃ_ïø²¾ŠÃCEÃðîÜÜì(?JKý5ɲ½/šå\ë}Å›@WP.¶‡kï6ƒàJ=CEW —ªzY–£ÈW ”G£Á~é>mš¶v)z šWNùÞ'õzýA•ýV>€ÿ¤i¾Gá(à÷®+ú3ÝΧFcvÞú“ÏÜ…¯‹Y GâÑà éTÝgßà\¾á`éüŠNÔ|ïã îLNNŒ¬ø^ÑÏ *ðy…GúÕc_àÜwîïÀÊÞu…«¾èæv»=í׆Nl*lEÙÇáé~ö×÷\»vcÔ¯uÿÖ¾à–iíʇFp¡Ÿ}A…_gi6ëYçñÜFàÜóžU¸ê{¬Äáa@h6›3÷ïÍlôÇg<ö÷pÍÛ†á?ƒêk W —ªŠËZJ§|]a>tO/…Kó\š?viÞNÒüªìßøÊPÕZ’$K^vÆcŒ1ÆcŒ1ÆcÌkâ_Ñ€ôÒ‘ç8IEND®B`‚R‰PNG  IHDR@@ªiqÞ pHYsMMgŒàIDATxœí›½OQÅÏ™¬l©¾zYâb!55ŸF±“hZÂ_ 5˜¥šHÂg,©± $4,TÖ,´Ü¥`cá²;Ì "»w7¼_µ÷Ý;Éy'ïÍlq/àp8n3¼JQ׺’MyŒ€"Ð)¡@¢ÊÚþ•"‰œ€]ˆËÇI,îô1ÙC 1³†1Bo´Ý”Òq pj«ŸAê¼¢s èž×DÊŸ8^}µBóÅ#ob{œ'qÙøc,1±ê¿ø"”ÉJúFz?Hø7-õàI~+ÉÇÒ• Ç)^Æ„ØYÕ¡¥_'·°pÑqª $fVñ”Ô €»åeðùÖ?…Ë#ü}áé;*wþgQìÚäAÕDWîµ%¨÷JK¹ã$ï‡_Œ^øÁ¦ÌšÛì÷^Åõ=ÇOŒ*Îk"‘ò2! 1 ªÇê˜JhîäÈ›<¯éû*CSϽCã MåN_{h*H׺’Í .µœw ÇæìƒØ#¸ThÆÒUÆæÇíæý[ö<ÿÚGIEND®B`‚ù‰PNG  IHDR@@ªiqÞ pHYsMMgŒà«IDATxœíÐ1€0ÄPŠ·*À²°€Ä•Ÿ!Ùnº¼Œ 1s}÷s_CxìâôO@ h  4КhM´€¦Z@S- )€Ð@ h  4КhM´€¦Z@S- )€Ð@ h  4КhM´€¦Z@S- )€Ð@ h  4КhM´€¦Z@S- yUÑ€;ºêIEND®B`‚¦‰PNG  IHDR szzô pHYsMMgŒàXIDATX…c` Üø¸áþÃÇ ”˜ÁD‰fj€QŒ:`Ô£uÀ¨Ü,›ðŸÁ’*y„#ÃEyÙrµxŒ:`Ô£uÀ¨F0àæ÷ ‰pwvIEND®B`‚u‰PNG  IHDR@@ªiqÞ pHYsMMgŒà'IDATxœíÁ  ÷Om7 €w@@òÉQIEND®B`‚‰PNG  IHDR szzô pHYsMMgŒà?IDATX…íÓ±DÑ¥”"ѵ­ 9‘Pê’ÿ¢Í~´ÀY¸c®=dª²ÖKNM’⟠ðÆ pw¤LÿY¹IEND®B`‚w‰PNG  IHDR szzô pHYsMMgŒà)IDATX…íÎA İ S¸žD@ÆñH ´U@Xï979°’qà Ó²æj„F˜IEND®B`‚ΉPNG  IHDR szzô pHYsMMgŒà€IDATX…íÒ!Â@„á–;päž 0 ÆÖ"ë8 Iï@Õ& ®[3Ÿ{o7™"""6¦Åd«öìÖ ì+$ÿ8v®¶ÀaÍÀPÐùuÒ L[Û·áûß§¡üûÙÂ\@Òx7È<ê2ç.ž68ˆˆˆÍ}^“ 7ä™ìIEND®B`‚»‰PNG  IHDR@@ªiqÞ pHYsMMgŒàmIDATxœí›½o1ÆŸ÷.¦"5Ľˆ@‚…Êb€¡ ´$>ÄÄÇ_€ÄÒ…$ÊÐnwç$`ÄV‘äaH$.wh㻸¥þm~ýÆyüÄg[9p8ÛYOÉŠÖ­³Î ä€);Ê•¶aÖ¬ü ”¥ª/E¤ý¯ýÓ­õIBr ™ãcYнª”zû·$oXI‰­yÂ[Ü‚€ƒ„·éÖÉ¡?ôPtsuär´ïÆÕëÃësè {o1þâÐy­”jˆH§P¥†ôµÖ5À? Ám{’õ‚îLÞã1€dE7šŸ‡=ßu;í õzýK Ú ' ÃIϯ<äD"¼¬jÕÃé‰1c@7ÏAð<úÖþ5q`ÿþ½_Ë\=&V Äùéé}/’y™9€ÂÙÁnnµÎ@½^ÿ"ÀÍd,Ó7äÐ_çt^-n\Wɲ@ަsòV©dA)Õ(XרÈÑ^Mçä0°ÃÛl³ýFÈÑžÙ½Ýlœ¶ØÆ`[€mœ¶ØÆ`[€mœ¶ØÆ`[€m*E463äˆCXç»`©+¼V¯ÕÒÿ[nãéÖEòÄa”ßyô¿ãˆGyéÖEÓÆŒ ˆ¢h·€OLEŒŠ€£(ÚmÒ†‘"•ãvš´aÈ®¾†‘Ùö“ ‘dû=€Ÿi…Ÿ} #cd@?¹lÒ† „\‚à‡IÆ@ ªO»ÂS>¡·D• !Xê OªúÔ´±BöýõØxM¶›m °3À¶Û8l °3À¶Û8l °3À¶Û8rbkÉILZ 'GûZ:'Ï€Õd¡wütk’£½•ÎÉ”? FüÓ…ª#"þ™d™àÇtNö $ea0€ÛaN®®dÂ0œ$p+Ëô 9(U} p%Úãù•g[É„?‡¥NŒ/÷ú6Hvˆ´¼’Šžðü‰­›—â86ãÄHÒã8кy©wHzà¤8Ý«yWh†¾ÊŠ­ùÿáÂ2¨ê½¼º¡ûµoê!óåÉ $07]›º?,a—¦¼‡*­|Öuij”ksGÑ;u½¯Íµ~Üȵ9‡Ã±½ù œ¾ ·õö›IEND®B`‚̉PNG  IHDR@@ªiqÞ pHYsMMgŒà~IDATxœíÙA À AZMxÂOeÕ•±M˜1ð_6Ü#4׳çzv¹á.ÿõ€šõ€šõ€šõ€šõ€šõ€šõ€šõ€šõ€šõ€šõ€šõ€šõ€šõ€Úñ®òxý5>FÿÞø>œàÚ¯Ž ¯IEND®B`‚*‰PNG  IHDR szzô pHYsMMgŒàÜIDATX…í•¿kSQÇ?ß›hJ«*µ«.‚T’Qél‹DQp2?+þ!›“Å…fêæâ  T»ìRÔ¿@§ªƒâ$´Mîqȳ©iªÍkÄå}à ïÞ{Îùpî»ïBDDDÄFaÏäï®[ü¥`¬¹O£o+·WÃäqa‚Nç_·øÁ€Û°¥ÔÍGÇÂäê¹™³§p±×ÃS_œ_ž›yßK¾ž:*”'ÌÅjÃ4[ 8â¡–*”'þ‰@*_™vžEà€!|{Ö¼™ ržÅt¶|¡¯™ÜÃkÎü3`0±µx É|Е‰ç™|ùênrÇþ¶ -Ï ª¬Ñ¥xÛÃÂGÆ'¿}ª¿Z)`Jç’%‰{Á€þŒ0$4u43©Õ•…”vXÞbÑN–[´ªzí¦øV}LB.x©¬Œ|½C©´}ë:N\.î<œœ®˜ð²ÞŠoJÉ6¿³'f‰ëõjncGtvnPní)Æ&ój±ÐüÖ ±`>q©^ÍýØ&p²ðøPÂ/0Î{.Þ–@jŸ¸¥ÆZsúÝüÝïñ_‹¾ñj›)Ôoºû|.žˆBÈ»`ïô¥±ýá'h꘠^nCÞIEND®B`‚"‰PNG  IHDR szzô pHYsMMgŒàÔIDATX…íÖ1kAÆñÿ3 ÑtÞí­–‚¶–’&V"6‚¶’O "~Q°±ÄîÅÒÂZ¸RHv7‘³QŒróXì]Zoï–M¡Óì.Ì;Ïoß…}h–I[Yþ ¤V Ÿ$É×:a¶iZNŽ#ƒÑhtüãâþþïWÃáðèál k++«Ïl·š (aãëY¶óØöL{¨>€p4.ï};+vî7 GÛ`ßͲb£QÀ„a)DÃÃ,+n4 °‘¦ˆ§Y¶½Þ0°M¹'ZÆ/ó<¿Ô,@D°cÑlnn_hP*"`à„oŠ¢8Û0€89!§ÆQïò Èò ƒzd1p,ðB$Ò@4Œ Dœ ¦Éá2°ÍZÚX·vßHüQfKe7+ mp…¶cÞD[·Z7m8î\iYò¨@ÿL‚˜ â"=NXÜ\ç~}X7€ ßQ¶€ˆÖH qŠè9›ÍúÌ_^[Ó;\ýjH’dëSÒÊ<àRm;OçNÎx¼²²R‰VwÔp¹\bû¾î§Àü˜¦Ic¨l¨wŽV¯-N9aáç.T·1h³_$,ôz^8Ψàr¹Äöׂèn–nA\Üè©~?}ÃE¡Ã9ÖO؄Рw·5éäô†ÚÚ~³º,Ѷ¾®DiÄï ³ëÜ£Ñ5|¼¿½ÿ²É·½š ø®! Zc\ð'dçLÎüsGG‡©/„éØÎ‚°Z#~YNñK­¯W÷™Õ+îxÏß¹¿}Sæ¤O€(§G3N!9©³£½ÅŒS¯@AÉâ©€Ø I%~µ¹Î}â°:‹ùçψ°R-cÂý-Ý"õ€YÅ÷Âögµã}9Õ›·¦æTTŽ;'‘Å̸ž€±ü&à03ÚÉ‚úaì¨Àá¬a‘Jæ,rë«Ú ;Ftº¤¼šÀ‹U=ºáSnjÞ²®ÛŒgƒŸ°ô¥/0ÑL»Áô\º¥ïÇ0Ó¡°ðáDÿ'ßpëŒvÿ‘Œïz½•þpý„‘Ò|©bZÐÍÌ JÌÞ¼½¤ì®>%mÀ«`þæàF¯ïSÒvÚ‹3ÍthhX= Èp€ñõŒ€)¶Ñ_’µ¾Órô¾)AF€{}:—“_0c ÛG…øž¢¥D €`›c;½*ÓËÖž+À[S3@½´I’¤žàu´Eö vÒÖFǹ!a¤*Bœ'Ʊ sȰ‚×[é'Àp…W(0Ì>O/-M`U‰|ǧ愮OjÏ^éFF˜ñEdOã"ãXz¬iÑ1-'4À§ÁVBgNMû^ÃöøÂØv¢rmÐ5£KKÑ[ìW_Èp±¦zÃö8‚XÚ&A¾3p@Ë €`620MËQCN•›Du#|ÚX_µÛˆ ˜ƒ}×­!ðYm;œI0¥Ðá û¹Ì S ‡c B Rò.—K0hNáÕòBpúlm‡Zæ'" ¬Âÿ$€£†ÇŸ§”•F„w>ìÎAÐ…úý=;µ<Ýo8ší¯Rdd¬Á³þ+¦ˆ£XbùÏÊ“FZŸù-½ô¸nXVj¨Žšif¡´ÈpÇçÿzì*FœØ€^i®s×1\.— &í&íe=®nš·¬ë&B“J”(+Víqx¼ÞJ¿ðßÍ@<Iß“Sä D8ŽÛ±¯û¨Î!øR>’Ñ¤Ç »ŒeðÔ×~h¦£ìj#ÃÞÍ5}Ì;ℊŒ»"ÇI’dcæ_«e̼2ÜéPØ4o¬~›8h2´YODò²µ~}§?à¿ ݈ Ì<.Rò"%f WI­@ð¶·'ÙÏkÂñ62¬,Ñ}vÉi¸G‚|ä[w‚è—¬³ü4 Æ×-m®¯^iÒ€é÷TŒ!Я4:~³e˺°>fV;;Ú»2¿“ à;§EFá¸ë¦¼vè`û7F}»ºÚ”ÎŽöwÇO¼¹šH¹@`z÷v”€Ul?hÙ¸ö]3$I²ÉrâVÐÙ´{zƈ²îÚ¶v(âé°½xÉ¥lñïã2•x»õDò̆†ÕfœSç§’”Û£„l f‘‡Î ¿¨Ìí$È“F}­Ú][$”8×X¢ÉBð´He;¦ $òÎ[ˆà…ª"ŒAëZêÖ:1‚‰\iYrúq¿M õ`Ÿ_þ 3?¯–ÃÙTﮎÔ×t‘T£l!ˆ‚*.¨ºH}8Ú§C½Äù0+¡šÏˆ°ªi£{™¦k„:÷ïÞ“™•s€ï1äœR’ò®ºaê]{wˆÆó‘B’$[Ƥ[_°Á²Q>’±¨««ÍTÍ`Té¬tê[ŽÁò´³ Ü–à |/UdG£k$°/¹´/Ú  LÓ´Oøï6ªÑ"êBII’,½Júï ¬b2€5¤XŸnÚôâ¿£Õk¹Ò²ä娙ñ·r Ë=-žª¨6eÃ.•µ;œeLXƒÐOÛq"Ùm0$I²õÒóñ\óè-Ãeb,53ۇÈËåíó]ÉVË*šL@þÊL-JàcV¸'aTÒq…ú¾”Â@Ä8A˜ÀL¹ š pªÎH`i“ǽk$þÇìöùå…Ìü[ .râB7ÀO¦S¿{8C>T] 1XLÝ=ĸ)–ºtX!§ø«£-Ð4BÜþ45»¤<‹R˜0|©€Þ¬¼œ›=öáüÀ„øÃ>oÑ•J‚e²e"˜ÆHŠÁ“›£úúà `hkõ¬ßÿ‚Zäó8óøßÅ€ꀲ·úIEND®B`‚¥‰PNG  IHDR szzô pHYsMMgŒàWIDATX…íÒÍ+DQÆñ﹓Ò()åeÇ ÷’²³š0;e5Vš;ˆݬd¥¦¹w3#y©YXX)%E"®¢¬m”lˆ©;ó³0 Ô¸3c%ç³<===§s@Ó4MûïTPÀ²3+`˜¨bÌËÎ\URÚ—HwøʃzòrSà¤\Ölš@ºuÔp‚âÝÓ—Ð Ð4åÔ5bÀÐhˆìZ¶3V.kÆ!£¨€V`ßó#?ݾ¢Çé¹W¯í~TPK@°Ñc;)/ÏgÚnB)¶0Hþåá>r±>ûÔø>³â™I”r>†ËšH½}Þ~[°îšùRáÂYn:tóšôØNT`+8Ô3È à£°½lrµš¾ªôÚ®%ÈŽ@KéèY„‘óåä^µ]5 èw;‹!¹n ˆžæ’—µvý‚¨ïŸQÓ4MûsÞW+iEÄtL…IEND®B`‚y‰PNG  IHDR szzô pHYsMMgŒà+IDATX…íÎA İ o(˜ü[Ç#1ÐVa½çÜäÀJÆ€/<èÿ÷Ík%IEND®B`‚؉PNG  IHDR szzô pHYsMMgŒàŠIDATX…å–=rÓ@†ß×IœƒZKp ¢5Ä!$qþfÂ2Ã(ÓQ$;ä×HÎ g`ÆÖ! ƒÖKá(È+É’w%Ïÿ{1¾xt­ÇQ¤·î’|=4 까¤7$~A|5lñ34â†(Ò€‡ î 껾ÚuÁ@+‚ú“l<u?k$úî$®¢Îêp­:M²HÜK û¢:%&s§á¢HÜO Œ<ú"×2$Nç‘p}µEÙpаŸ€Ðãy†ÄBU‰xê—ƒìŒ<~Hö6`Uèñ\âz¦Ä@Ë…ð–oà Ep ñÚÕôµJêÄ’“\z¼Ê…Kg\ 7²à3À ôRP¯ŒD3ÐsBçUà…³$¾ˆ_¦yá¥Àh R7Kb’;£OвK äIHˆ@€Ób¥á•½ó*Á+ ÜJ=Hö\ Ü {UòRÿ%ê§$ÙƒDáGÕ°JÎ@-H̘G êÂñõ¬Jfé-pA—à k;Æ Û#~™ÜR+¸%a@rK ]:µjhúòòà¡a74ì ÜšWb¦@Ó—Gêʆ“Ü »ñ@hØÍ‘¸(’Șz|o÷‡†]’Û–Äb‘DæKØh‰R¿,2|W·²¶¡`%n6}=!u]ßò®Š¼ŒìcÅÿ¼W[^ü©›Äqß-;欖×ÄæL\¾0»TegwòS®Me_ä£ö¤®Íßç^\ èPa{tXèžëžµé1…eÀÍqž[ûݨ§.´ìÖß[S?‰Ùœºvã[n÷ñ²âJà07}Mî“=(veD™1ÿ²¬œdÀàŽ­®ý0uUî¤8i"Uê1kz –¨Ú£nw ‘Ól¸6Õ=ö„öä(v|t7¿ß/Å `ž°Ýàr dBcž‚‚+põ>\sÚºÅÖÖØçÞ¿ˆ0=B¨a¸¦ñï& ÉQKÌwO€¡.ú”î~),á/€áô‚ñP§GLbNöСÕQu+æoÚÔUv'šDø$:7 ÅBTÙU(vW”(²*ÊZÙC@°ª´á$„q7l¾ôÆxóP3˜ƒH"*‹çNÞéö§fm\mr?_vÝ âv¸µ²@FvvbÌßPTÔyóa“ º‡þ?Ge×XÏíuî.ê8ŽawD nÕ˸2¹|ñ§ÖäYóf ,ÔPÂìõ¯¬Hz5Çq|·mâ^E6wÀ>,/ý±{\¯'Õ¯M7§§Ìÿ!M`‰GÃfÌšw;éÑ•ç¯\©ï·$ý~¿I˜ð„Ó®¾2T"W±´ÚŠâ†ÞâûÝ×K3_›`¼ENÔÕ"P zD0ÍÛÖÞ¶>o¢E§é‚®¦«Hm§éÜPøê÷¾8õc²ÌÉ]h¬)žL<ЈحÁ²ÒÚ‡ôköbææi!Ê#ËE5I#mÕ´-¢Zx*kË‹. ÷Á¶ÿ÷€Ú°€ÝBIEND®B`‚‰PNG  IHDR@@ªiqÞ pHYsMMgŒà¶IDATxœíÙÑ ƒP ÅÐu öŸ Ä tŒƒôìbY÷/3óºŸóºé°Éão Z@S- )€Ð@ h  4КhM´€¦Z@S- )€Ð@ h  4Кå|äqýŸióÕ33ÇþcK\~КhM´€¦Z@S- )€Ð@ h  4КhM´€¦Z@S- )€Ð,àǨ Æ$ZÇIEND®B`‚”‰PNG  IHDR szzô pHYsMMgŒàFIDATX…íÑ!KCQÆñÿ{ÇŠAAËvÝö m6“¨k‚I«‚ßÁb1ød  &A‹ †Áì}Ï=×î˜`q{-¶»»$x~ñððð^‚ þ;É hê1"±­8ŽŸF)M’¤&Qé¤3WÖEÄe£Ü6c ˜ï›Ü«fËyqçÜ‚Då%Œé¼|î€RÄpL"vå\¶=(«š­Ñ-Ø,pcö±2ì÷# ¨V«ïsqeá(vâœß7³çKÒl± `8}ëvVêõúk^!ªéž:ßSçM5kµÛí²™E‰ó‡ê¼©ó¦ivð{Ü0#¿9÷Ò4úgÀ„Á@X>0Û©ÕâV‘¾ÂÒ4]ì›\3_OݾØF#ޝ‹v5@UëHéYÀ÷zÒl4*ãvÍ̤Ƚƒ ‚?éúʇI߃:5IEND®B`‚d‰PNG  IHDR@@ªiqÞ pHYsMMgŒàIDATxœí›¿OSQÇ¿ßNò#î.XWML¬us¨#IѤ¥DÐþÁD¬8hM ²ƒl¸ºP˜œ)’¶ DòŽ}MsûjiOîgº=÷¾ö{¾½÷ÝåÀb±\dxšEáX,àtŒPp À €¶º*ûwŽäl ‘qºŽWÓéB­‡jÀðƒø˜€/ œ‡Ê²#ÄôZf! @ª-ªjÀd²½;/s LÔE^£¾Ýïåä÷Tê·ßtµmÌK{îkI#ž%ðM ?)Ž{¾Jÿ¡ëìà&€`i‚2Ñ<…ÏNðÝ¡hbŒ‚e¡=™Z[~ÿÙïKš †"ñG$gôxA!¯e>V,6áX, ¿:~À;ó‚¼çúÊrj§Ž¢Ï{‘ä€ wD/€È±ëèªùbtÌ݃Ž”½ðò¼Õ’€•åÔ¾(ƒRè6×UP¼ê<²Åmß’¬fÞ}-(µ ÀÉ=‘u4ÿ™ÿRÌÁcÈ\àgÀ 7 ±[UÄÈáŠ9ïg@éjl¶«î,9T\û~\(¬Ú´±h ÐÆ -@k€¶m¬Ú´±h ÐÆ -@k€¶m¬Ú´±h ÐÆ -@k€¶m¬Ú´±h ÐÆ -@k€¶m¬Ú´ñ3àØÝ–7ÈÈáØœ÷K0WzXp¹¢‰‘ö9ïgÀ¦7 y §ì)hRXÌÁcË\Pa€™²Áp$1Re ˆòÊqá’¹¦Â§ëh@©6Xˆ¹;÷-wn?é‘ÙR€È1pXÛ€Õtº ÄtY¨ÏiG6‰¢5ŽC‘øh»Û¶ Ï 0ã×BS-!ÞŒ¿ñéÉŠÈ:‰Ýf«"ºŽ.Ï|Иžÿúeá|êž«uŒÈ~/'»ó€aBdðä›­†š ÿß9¿ßãL¡JÑwͦ©P4£àZ­iŠÈ 0s榩r±X@ ÃÅzû!œT]7cÛÜ6€-—8\:MÛœÅb¹ØüßsáµÏ3¦ÑIEND®B`‚ Ö‰PNG  IHDR@@ªiqÞ pHYsMMgŒà ˆIDATxœå›[o[Ç€¿%EŠ”(‰’­»dÉNšÂAà&EÑ H ô©}é{ý£}*P´A6NR i’Æ–lÉ7]HI¤.äôaöîÙ3‡¤äôÉ$w÷ÌÎÌîÎmç²âxã@œòžþx“„ãùMB–W—íàðœ<Ün%`2‹/m¯ ¡fàbȘs g´w¾ÑþÜ©*Ï£5d¸í =^ø‰§€9ß.ÀYÐ>ëÛú¾£ 4Šï?÷4Tý÷%p—§P9òmS¨ ŸÿÄÿŸòmh§¾})è{îq1o…R’- @V1k Fûȼÿ=rä®ÿ̀ܲ#¤îÇ,¨àå®Ç1áûç@6óGUší ¿)_óˆËÈŸõÁ\ß6HÅhÛ?·â'[ŽðÝsr<ã%ÿÛyF¶ uKÛå-ƒÙ2ÈOŒö ?Ú¼ð‘0’š¬0Ƕí–íkLN`¢âöYN{IçËõ—³Ï¥»¹ò«q'ßlûPcÊÈmcü*Ⱥ}M|ó^y^dV·¼Ù7òdÑèkúg#Ë&›úÉ‚q&äà> þ'Ê£ì!£«py *_ád xZЗÚá< ðºO ö*hë(]”É)¼˜7˜0ÆÄ4Pæ Z½íÇl/T¡ñÄ,ùÿ‰éJ4xØFÐö %† ‚2¼çŸ]~|Ì "œÙŒ ÔÌ$ÿ7€6¸]?Q5]›À×à®ô|¹r ΛB}oG˜ŸøÙE`5©{ö³Ò†ú ÔŸX~€Ï€ç€·<ìøç{ªÛÜUÑÄ[ó&[YÍ)^|)FËL.¡¦rF?7YõÊx=kUÒþ™~‘ ßx+¶ŠQ@ɘµ¯² ®]þÜ „¬ÌaSçÜSß7檛àŸuO€Å}ØÌ÷‹Ö¿÷²sºžò"ëGÀÀp1zæ¿ó[é1Y! ¹ÙWÆÝåø<Ž îRq7ûÑn=¼ð=záê)NÇX-%8çW/ÙF•L)0wÀ}àÒ›«Wèî MžŸÐÜ%èúãV;Äö÷§.Hxfû:VîGèÊ­c ¨/’Ñgîä§ñcXy .*PFÓ)ƒ­½ üøKt桞?>N¨ùÎyÔRøÕ9‡úád]Ô<þø$ Ù+Ïz ¸ r-fn%bÄÑá]`&ªtûÙá²ü pŒ C€îm¨M@ý%¸NCGÙ¿Rà¢Õ“¼ÖN}“¿kàö‚Î6È ºcßöžkB[ŸèØÇ:  œû î¡«ò…M³,¡Û¾‡­¦U¨µU‰2o€ëè3Õ¶2«0ª£‡ú+ˆ¾ñcîùç:d¹"+Àð õŒ|غພàH1ÍC»†®ÂùX<Ûp<…ö$/`‡´]àLçŒÁ¡‹úƒç)Ç‹%€ê=-æ·|Í)p­l["„Ë÷a®³¯ÌGo³¯`öRq[G͵`® a5ß-ÔÂeÀÀ4ª= ”š,£nªÇM¨|Ì3~ì=ˆ òÎaÂsÛYr—@Ü1º¸° ¨iÙ) d¶ó2¥d"¬`ëÚnû”J^¿Äî”6±x:Ô#4Ú ”ƒL¿}Õ²á*Ì‚t˜ÙúZÀ»ÀÒŒN’Ô¨OaåçÐ@çý- ;sä?#Á¦}dѸcï'Y 1a5rIH™GƒeOd„œtf¡~ÄAÆ#OÐ×Áä+@:gPßtoA-É/>óßÎÏùš+ átf ž-œ·‰*¾¾î†TW%yËLÂÆZ™EàKª'à%¸C4Ós–ü¤õ2¸çyT‚g~8FWìcwµýø2ºâžŽÿh_.z;ñ¦ÏgCšd˜šlOà¤)²á±©J¨oƒÛ ì÷-lå7 Gih¥ÍônO‡‡Ä©/ g‡R'¾½_Ÿ¹r'¨·ºôHe H .åã( ¾óGF{e%´òœ³”›:QxŸaß?„´ 7N|j|Íêµ°Xöߺ„V$4 1œ“ó.ŒÔ{Š„iÑú C`GàZáì(Áô;§0”6<<×…ê¨|Û5r î Uæˆ0AqTw›^ƒndÞb! ½±¹©Qô\®=¶ËpeióÀ?È´—ìöšP{fÐ! Ñ$ À£ý2b…»}4sÐÃÎÖŽŠz˜¸ÎöOìuôGô¿&ÔÅGp¬y#”Ÿ¡ö7ÆÕ³ŽÌòš¤ÒmÿÈFX‡B(@Û³ÌÍë€.Q¼@³Ã¿ÊÏŸùQÖa([9ÆHrç9my"è“Þ')ƒóIbt|Û2zÑ Ž<½ìg·± ¹CYÏæÓs÷Opí`Ž‚<£š8Ùæ\Ó`-a< ‡Å¡»ú8Û ¶2Ùéø„W×ì¡ÁÏÈVàoe_ûn0¶+æ$“ÀË„ä:>£8‹”à…m ]_‰²êmÜÐòg0ÉÛ¿ó BhÝF$VN²Ð÷-£Öh,%ØFí¥?ûmnd_8ƒÆ2œœá%^œ(Ιew1,S|¼’…œYµp†*—rA†8î»÷O·ýWÐ8Ósúc¬*N¾Âô¥­®-t™ƒ³.ÊSNxEù€Äf¾mÔ«8’êOÓmžfa.NÖ&°SÓOˆpAš4pÞtɺ×í+rJØîí°õKU\²8ž¯/Î]I#<ó¡(«vúÔ4_TÀ}ëêDV 6ƒñ•Ó'h9À2Z'ÔÆ]¿E¯Ç— —¤ NÑøü=/„a‰!­5 ºÑVMÏ%ð{àS—£‰LÊk'õf;…ÃÝgÙB ¶†÷7¢ö¤oÍVCܨF0}~óREªh íÈ»¶N÷óÖJ>dŒ¡C #ëNफõ€)¢ðŠºîê%¾³ã‚Ì )ûïÑÌÏyŸ¦àà žän"–º@•LIìÌ ¨LzÉ…´ûЪªÎ¸V]î˜ eÅݪ¯úc{º2_*¨nËÕ(X±@ Üžnyy( §¨Fîÿ ˜$?wà”„¿¡ |H¼¿U”ѧ0'Q¿9ÆûÐh‘¹¾“ŠÒãv@îÌfà%i.Àí¢u6ÉÖTñTÈf"NT€ì£µ?÷Æ— hÝF›%à¹î°œŸ&]Õã&ºêeTyvÐÚæ%eÐËØŒ3íwš WÝZÿFfŸ£Bxç'P=R" Í[_çh$›I€ƒ ¿»ªÉ !{f¼©3ËeÅ/Ko#|<À½ Æ—ã{FëÄÌt=!Ñ‚EÐ+ïeß¾´ b÷‹ƒê¤g°SàÅ­À}LÁÑ÷Ùë9qž¶øÌ“B“;ç'S._ÏZðfè}ìDI2æÿP.ºòó¼ù•¦ZŠ\¹ü̲^ùÇØ/L¬’{aBú‚…ñC:f{̉CœψCßù¥ÑWÎú+©ªŒÿÂDŠèONÈF^òúÊJî¥^ë•™´­äŸ·@Þ1œŸ²¥å=óÈã+ž<‘ZY¥{€àw‚q}-~kÞÓO+tž*Œ~ijÊ /+ ^¾*z9j3f‘-ÏC)¿›bD†“#›¨¶õ5º´üïjæzrÁ൹@ 7 åÄ)JJd{Áÿäö&ym.(—Méëúq5a§þ÷$ªÄ}T¡ÎxöN“íÀ¸¢ŽA&0JÍ<ž:y-¿8YB¯Ê’#Teà_à™8$kó¬'…Aâ3£´6}¬ˆÇ7÷ÕY«á € Ïoó ¨þ´ ý~Ï"[ÂIEND®B`‚“‰PNG  IHDR szzô pHYsMMgŒàEIDATX…í×±NÂPÆñÿ)¡Œ–Ÿ¡  ø&ð¬&”ÁœÕAvè ð ÆÄ'¨‰Kß)ã5XÀAÛ8x¿ñ´É÷Ko‡{à¿G’ƒŠ7¬[ª×@ pRê™ÁZ¤÷æ_>T[]…`"úžJ½Ê ¨ D´ûê_Ýíª­AC‘'„—ˆ|sâ·§©”oSöF%›åå|-Òˆ¿„õ‰D:À"‹r€‰ßžFä›@”SíÄskçH˜Eù.B!ÔÍÿµpR;óo"Â(üI À À À ÀvóÍí5Û¨â³C€Ô-{£RVåeoTp‚=ÀZ¤l–ã,g§6Ë1`¯Dúñüëbâ îUåˆÂíò×QÅpûèb§â ë9ÕÎöê\L>ÿafÁJ¤Ÿ\ÍL>§|n2Û½dEIEND®B`‚{‰PNG  IHDR@@ªiqÞ pHYsMMgŒà-IDATxœí›És\WÆßµI9 ‘ƒ3@+¶xHœH=$ÄŠcÉ“,u«ò‡° XP^°ÉþˆT«%’8ek4   EV)*•bE&°lc)F÷°ènèt·ôîë~Oêú–÷ž>÷|¿:çõë§'8Ðôÿ,õZèÜèyŸ·Œ7[F¸íM¯Ô‹ªÄ¬ì¼=Ù2ðx@øLçªEUzmî†B’æ! „¤ÍC€Ü‚eÍÛ"ÌëlmV·Br‡*7gÏ™løF@øCµ‚Ö£ƒÀþBHË<ô¼ì­Ê´ª˜&€È`8*gË™’†æßIqÍ;§ÉPó£ZÊÎYÙ"ðX@ø?Íë•~;!¦ù»Îib}Z¿sFlÒÙêŒ~猽01F ]Õ¢*˜ÎwÂÇl9;oχæÏ—ìô^˜‡>@‚¡ †/Ùiïl…@ó2MökúveÊ–vƒÆ¡ó•¢B~¼í¨¾; ¥ZAëÎi’ðNXÊÍÙsMóÁm:?¨yH ZÊ/Ø ÞÛ àëáÿél¥¨ßŒÍÛ³Îl8ùIãÒdµ _VqC‰€þ l;ü~™‡„@<&6d@ÈÝe æ!¹9{Ñd× ë„h÷ìÎצõ^"ùÚ” HBŠæ!oÔ¸Bë<ÐõLj`÷<º–yH±ZÊ–í{˜]G<ëƒMóõ¢ÞM©4`À!,¢ÈGî ›M¤mR/ɳø"8Þé \ÐÏî•:€lÉNál•°§· ™8³•±y{6½ÊJuÚÌ?ÑgŠÏ½ôr}FH²®v¥ i~xrÀTŸ¯ô~uu*•È/ØÉ„̶š)Û3 äêRâò vÒ{[%À¼Á–`+ í1a+i@H@óÀ}'Mn7ž,ý+ þ‰4 $`´l'â˜Çt±2£›õ¢Þuè1 dKvj°jÿ§D.‚£e;q[#†ùjQkí‹ù²ýÀcïäø ¯ñê¬þØO½í@\ó†.Õ Zíµ™›·—Ìì{a -Û GxÛïf 2£›’.8¸ÁÇ¡ïh™<~ßK—ë3Z ÉÝì„wˆ~]àSç4¾>­?…äîT_ÆJv\ÎÖÍozéR¨ù–ö BìØ óÐLûáOzo«ù;÷œX0V²ã®qoÿt@xßæÛ•³3騯 ÛèÌ­‚þš?¸âš—ŸùÝT-jÍÐ%;á¶6Z¶¡ùƒ: »`ßÅÛ1ÌWfµZDˆ2eö6 wB$€¸æASÕ‚–bcklÞ^vfoÁàÆ£ ì `˜Ì·Ô„ð6t¿õÙ©;^šæƒgéJÚæê3ZñÒ%`3*Vð”ÃVÇJv|—˜nµ™ÿfÔ![’¦ª3ZŒŠMRq;Á¼ÎÔgõ—ν.ù7ìÛþ°Ý¾x_Ì·”+ÙYsö€½Óh}Zk_ì~[ü°ÿ1_ó•Y-Ëë2ã<-ïÒ¹Øëÿ^ŠÊd°…וý4ßRãëVSAЙΕ^ÁíÝR´Ì×fu#´È´Õ¸øFCPoÝoõ”ÁÖ!4=Læ[ª´„t…]!ØõΕ.fîªÁ_»Ö›æ× êJ2,ªÎhqGƇýÛ]í\îP+è³íÃÊýøøxÓ™^fó-Ug´è¥<Æðwà#d¿xh[Ù÷^Õ'û]ßt áÒrÒI0.é­:IEND®B`‚Q‰PNG  IHDR@@ªiqÞ pHYsMMgŒàIDATxœíÖ!NÄP…ásûš  < !‹ƒ°ÜLHXÀPÏ@¢ðHpØQ„ HÈ8™LÚ^,yp}“Ðÿ“çVœÓJÀ Øßg·ƒÉyÈS¥OR§ºî~»ÿ8ÀîÉņY5•t,i­§n¹,ä~ßVÅøùzô“öG—ëÍR/&me©—‹išn{vsúþ=.âçÚ¥ê÷ò’äÚlB1ãd¹f)´&ÅY:ÀÀ¤˜?® G.=ÄY2@¨4qé5K£œLó²íÎâ8`v5þe±'éVÒgŽn=[Èý®-m'þHüCñ­ñ<56PŽiIEND®B`‚‰PNG  IHDR szzô pHYsMMgŒà?IDATX…í’±JQEÏM¢‚­¬ÙMZÁ_°°´°³í,üA°²ñIa«bg-èøºo³ ¦ÑÊB“7V b·oa÷t3wï ÔÔÔüwT¤ÙÌä²Ü˜/õz½²‚ý3a5ZïfÖ¬T@’ÚžÖ.ËÇ• ÄqtƒédZ§éð©R€$‰Ž€{ÄšsÙY¨@¡#üKê†ö+h?ŽW.*0³†ËòÉ´ö²õ~·ûX$#è ¦Hò ósíY˜é¡hF)€ÑhôJCçK ˜™ÛXzó“fR©@šåÇÀð)&›ýþ²+š|„/.ßv x¡­8ŽîBr‚^À¹|CØÀ¤ƒÐåAιUÃn–Ái¯‡./,`f2×@Gè*éF‡e–´f^gï¿÷$ù²5555?n9fȦ¼VIEND®B`‚Þ‰PNG  IHDR@@ªiqÞ pHYsMMgŒàIDATxœí˜ÏoTUÅϹóÒÔ1J݉.uÛ™1Ædª˜.´ÓìýTâ‚bBü±6!P`EL ó¦¸`’&2$ØÈˆ¬Õ5î$ã¢UÚ{\Ì€Ówo;¿Þ›©x?Ëûîý~Ï÷¼ïÜwï@ @ ÿUâ|EeR_0.4r§G¡c$ct€y,D”s$?Œ=äìí£ü5 ™0yU/F‘VHd»ãîFÈÑõ³5<|wŽ÷S–š¾ÅX‡­x!ñH ,:íí$ºÚîË#߽͟ÒS›ò9`º¢iA·á+]ßšIÀ‚ÎìýÖèÛü’¦RÛ"5¦«zÝP7ìkÔ,¾W 8&ì£Q=ëµ¾…&HÅ€b¬y#ݰ§}\ö^ü¿X4 lg¡Z¾¢òq3°ùªNº`¬}\ °ø´r㌑Z*ÄzgÐèõ¥ÅdœfñΛëBò˜`],Äêp´î»$cû©Hçò"ÀÒýí¦áyi‚>û¾l>êçþг¥º¢õßì9‘'\!™ßÌ!ôš°øÒ†yïÚÑíY~z2 tYãkÏè €ùDöfñî§+$€ñ¨Ÿç±Ú,ÿê6V×¼r]Oolj€ó j]j†R|[No'ø&—c¹ÛûCWL~­‰hS5[!ÀÞZ.UHÀÚ¸µ wFœ½÷&t ÑiÂô²ö«ú;àdÿþð#Ä#þ²ÓÒ?ƒ//é ±ZÅn.ØîÔxˆF«S×uÀ·äÛv@~IS4ª˜pÒí¦âÛñwÂo4ʼç[âí€BE34ª#Q¼vsñмj';aR½XUÉ·Äé€BE§atÆãäp·ùqÄ‚åÇ~²ã¼bl7øþžúÏC`óμ‰ÚÇ<'ª'_mî@žmíªO‚yvÔ2@ @ £æ t‰œòÀµ/OUU%GœR0h¼Ýo€ì-.Xu&Ryv̼›öbsZ€iÀ'—·[j+ÿ‘xæByS{©CX\Gx6v°²mT‹¡û’ ~(R´&îéyª*;òÛPñùC;Ù t¨¦JâÑw»Ü9CR½„E(š‡"OŸ„Ôå³,ÈEUŽA²¡?hkD«}åµ…ªl0â]¢C®@‰¿ÖkÑV„?ÔæÍGýfƒAãëÎÿ@‘»†™òuU>ŒGµn’¢Ò`îäü‚³ÀбÈÖã™1㎶¢Ÿ¨êîäÞîüC}K99Àj¼þPhvríU úæ\íË (.Û?å% çfOOÄÕ×]° ä­ˆ]P@Ê}þp¥ÛÚ 4ç€ÇKüµO  ˜•€ ko¬JdìÞMû v<)-C?}jKíƒ.»¢zÀо2H"/¤åÛ~é4§˜4vYLqlt¬1ߥsÊâÁ`&€#rÁ¤ÊŠ §!ú²û3—äy…ƒdŒFn¹Ž‘ P8q™+9«“œLî‡2Ñ r#ø‰Œê3g”ÜîMÈe€„8ž¬Súº8aÈ%÷×½ÖxÒºèÌ\p·g§ò;€±Ìä8…h¿Xs+U БµÝÕS"²Ìä ‰Ô‚„IJC?†¥ ? Ê‘fU]Q\É»–He;7½ÊÞ3 nƒUY`lªi€¶p SÒʦAï†~¹4ïýñý mŒM¿ºÛmñn ¯(~iýb[¶7è¿Ã­Ý BpQ t_Æúo©æ®Q´†‘GaïÜžékÝU´¨4˜«6]gv[ Šý¡ÃoGbžk¯,ÇÞòšÇP)–³H_þýFŒÖÇê·ý:`5Ä[V[àNÅ"Å# H÷? €}1ϵíÃ5$¥¥_9¯§†^oyx;ªŸ£t¦rÅ×tº=†lÉV˜gR© øú†ã¬k¯Ûü÷Ð$C£¨4˜{w~AX`#pà ϟ‰bý†mJK6†¶ÆQä²@ðÏÕCciJ}Wò_³*{æªfu<ºåç¡ÜGlËŸ|gÿÔœ<)í3uˆÐŒrkÎçØD×Í”X'7Ç£†ÙÆÚeˆYÝ× œÇY×Z·¹{8Ž1=LJüág,Z úÜXü6UvÄ£•'Gs×Óìéòýs’ÖYƒ°í{št¤¯W=mÕ4Å£sãÉ{gã_^ï×= ²_BIEND®B`‚͉PNG  IHDR@@ªiqÞ pHYsMMgŒàIDATxœíÙQ €0ÁoU€ÔbªÈXf Ü˦ ­ëÞëºw¹á,õ€šõ€šõ€šõ€šõ€šõ€šõ€šõ€šõ€šõ€šõ€šõ€šõ€šõ€Úïåñúk|¦~{žô>üà ·&ú*dIEND®B`‚å‰PNG  IHDR@@ªiqÞ pHYsMMgŒà—IDATxœí[mlÇ~Þ¹Û¡þh!¡ÈЬ (¦*˜V5E`Àœëª˜ø`Õ bㆤ­J”F)9܆~¤ ‰œ!ÐH=l#“Æál§F¤€B U6QR‡Pj%Q±í»›·?øÚÝ[Ÿï|»®iýü›gÞ}÷Ùçfgfgæ€1Œa ÿÏ ‘¸I¡RzwPÚ âÙ’1ƒ€ig€( 6 ¾‰Ô.Gý>ïÇVk³Ì€%÷Wf {¸D01ðÍa¦ù˜™›¢¶µ~×;ØL€ùÐÒâ²â /69÷&líùŠØwjçΠYIM3 À½n±$ñ €yfå4Ÿ'`³¿®z/Lh °´¨|"Ùñ,~%L2ð7b´1øœ Û?Ôý—S@á0R‰D:ˆ¦‚1ˆsÁ´ @Ê` 8j­?\·ël"ú2 @)ûŽ”tÀ$}—¼.ˆ…Ãýo´6ì½Oî\eCʸp` A,q‘Ñ=ôX×\ç}uX€ Èw—=HD5’"D½àtÚ÷—?mïn~5EqöÊô O¸G_OÀ–Üì̧ªªªd¼¹ã6Àãñˆcg»žóF]•£ÆT5Õ{/Æ›7­K½jáq_R×1è`H8Ö´ù^ºOθ ðx<âØû÷ƒè]–.A\|ØWýv<ù†‹Bwùä¡‘î»öäkyMû÷bÍe‹çÆÎñ÷UèQ}B°Xz¸ÎûA<¹ÁG§³ísÈ{‰4sŒIácvNvÖŸÛÛÛc!b6Àå.ÿ/êè=ÁÔÒújuo¬yÌÂÅö¡ÎŽÓ Y3ç\Q>n´f¦÷!%¹³ýtK,ybz–­\7G$«è}ÍuÞµ°`v/òÝå¿ ÂsjŽ µðîêZ1TÀÒâµãqê‡g¼L U`<<´Ô{·±[ÍcW»rîP×i‰¤gdÞ&Ð…,n«­íŽX‹Àök)ëSqÎ0É]yy{´ £¯T. ð:õXbescMWb-ASÓ‹ý27ÿ¾É0×9þ_D».šDáðouÜþ–zï‰D„Z‰ÖFïg lQsÌxÒµf͸Á®Ô€eJÅB-PQAÉa :-E(-´À…[a_MþÉ`ñƒ·Æ/µ½ÜZ¿»3a…£­¶¶LºŠSÅpÈ74 O)ù*˜¿¯¢‚2ÈO›¦ÒbdØz_øC•Ù#3òb pHǃÐN’Þlmô~f¢FKáóùÂ`ò©9?d;È+ÀË55š%n¤@$ôš`ôDp£Çœ¯æ$ÑkæÊ³¹Ù“N‚ nµé¤åèã" àkÎïpª¨¿·øv^ÐÇvTUUI04?œd±Nù °ÈV‰pÔtu#kµ3féc ÀLuQJú0"æN vž©‰4€ø^-!GýØ?(¨×>Uc4 ¤ª Œø·¾i¡=U2¤‚9®5¶Ñ„¶ÚÚ~!åTEÝÁ Y$ 1*¾ù­B„ĸ¬ `Žh6w òJJ’¨×|>߀:&rp}§övȰFžõ°uÛÓuÔe}L¤À'‚"{Î;Irš¦Ì8¯1št¨‹ºÏdY#b¡ÑÎÀ9}L„‚¡Ùld`>æN`Öj'œ‰ˆÑvçq·öß ˜[è.Ÿl…@+áñxƒ~¨!mú¸nì­Ws!Ò%ºðÖû]9Ðì(S ÔùŽ>Îp=€Ýç¯,2UÝ€„^3¿ÑÖVÒÇÀA¹€j«™–*¥w›ªÐBx<AL«tô£XCškºˆàWQIAi×o‡Z?ÛµÀŒ›e> ^Êôź*Ìà?ªË~t‰»ì릩´Š¢8™ù×jŽ™Ÿ3jþ@šT¿I¬é 6ÂfsdZ‡™V ígowJˆ·mgˆ¥À&-Ek]JyĺÚhAÞêÊ zRC2~ÓØX1¾‰¨{ƒ-¼m½¢Žg‰†¥EåRjEq:úå¨Îð^÷ñB´ë†Þ–¶Çu««S„õ……?ÓŽú¯¢W¦oa‘Š ’àŠ¡Ui€¿aÇç,q?€~=?xWßK¡³ÆCÁµªâ«9b¬åÌRLGd:;N_Ìš9çߺ0gÚ7æNÊÉÎò···‡ãVmȵ²üç lƒêÇ Âóþ:¯~gÛ1Ÿêìx÷½¬Y9wøÞ­9}2yá×¾5ïµógN]Gy¢PÅ™9sþË6AÛ/e–ž?$¦3ƒC¾jdPï&\?žv„EŽðÉ|¥rv<¹«øá{zÃi­ÊtUgƒáЃùFˆûVÅÖ#3þ@à ºª €í$í[ü ;>7o,ÈU6¤¤ÊË?eÆF¾¬­å&¶Õ-¾_Ä“sؘË]^Æ„íºª+DxÖ–tmk<£!/ÏcwL¸X¦ÍPŸWºÞš!Oø|¾¸û¢„zq—»l!Õ© =pÈ–rÍ¯Š¢8»Ã ñr+MÃÄXï¯÷VG?`Â0æZQ:…í¶ç¸£„ 8„¿2Ó6þˆ%w;Æ%_‘÷¤¦úÓ),¦ ÂtfÊQÀiQrž$õ~Ÿ÷T"úMÇ]«* ™ù÷¬í ]ÿ*ƒÞá4ùÈt&âúaê® ÞàÛfæÐ `k05TmæEËfr++f1PÂà"Ó‡“ƒ€O%èuÁrOîìÉo çÿ1ÜÃz¸V”N‘ÛbÍä 0M ×wn¾蟟cà‹ð‘Vßã¸cÃþ7ñ8ñàç^bIEND®B`‚O‰PNG  IHDR@@ªiqÞ pHYsMMgŒàIDATxœí[}pTÕ=çî&a« ~´£ŠN [ lBÁiÇÊqLÕJu¨ÅÖ™Ö±tpF­Øj+¨H[?**Ó™…Íé€:S ƒ|lBÆÖQ¡cë0©Z åÃØÝ{úGPß{»›ìf߯¤Í™Ù?Þïžû{çž÷ö¾ûî½Æ0†ñÿ ÄI®ˆê¼Óe˜Ik/“ÁŠ 8ÀHÇÎüö Ú ˜7Äööù|§ÔÚJfÀô¸.I[»ä|“ú•DxF[¬5Ow4`Hù«Òo$ÖÅqµ…–˜ígj¯ \•ú'žë¼IóúƒpL³aô „©~åÌw.oà? oÔJÛ‡~§š°T›¬Ùgø»R8l-ŽÒ' *Ë€Q2'`< :ßê%çvk¹¤cß(FQÔÅ5ÍJ­¾˜¥ø8¨Í”ÙdÚø¯‚rGJpi真ãÝ"oiðýÑaÀ´˜nõ ¯(J+Ì/_™Ë#ýÍïDuTå•A,t/€ó½å‚V¶ÿÅÜ‹´…æ.Ü€f™ð${Èež+è)ʬH4ò`ÁyóÀ×7ª2iq'¥»œí*ZCi.lkâ‰Brf@³Lør»àõž’.CFvEØ^P¾~"ÓE0Š{;\¯²‚³sx,ß\¦ Õ.ÏÒøÝ)qê@5 ìƒ.t„º‚FÚ¾ËîÂ¥–¤F0K$ìtDËE=>k«‚½UíÕ€p‹f¼ÅÄÆõì*Fo)°eO1ȇáÉÝGp{oõr Ð/<Áu‰Fî.BgI‘¨çW:c‚î™ô¢ÎÊU'§uqÌ0ÃJ¦­i.^fi:†ßØïû¹p[.~N,ôç1¥ßîYÀ·}ÐXRôôMt](Awæd5 &ª/ø–#”TÐÜçŸÌÒ¢*‰g¼å]øžÁ7²q³âFôLUõ€x9QÏüYJ¬obšÐzgŒÆÞœ›ÕAó\Ëç}S7P0Æ£™s²ý 2 8ÓcNwÆRiüÑgy%ÇîWÑÀy׎:X†)^^†ÿA-€rGèÏMÜïå z¬ ¥äºpRæ=^^ŽùºÞ¢Í÷W^éa`]š½Ð6›©L^”°=›ÎTÆUWDužï*K…f œ!Z³65«;êÙ%à¥O*É õ.‡ZÔ^ŽLp„Þù´=Näœùk×1øƒé1õGbéPU¹¤Ÿ;c"Wg»ý^ è¸/ÃÝ–§a—û¢²„8» ß]ƒ·Ãekrñs¯ ‘"y·'vS8®ŒyµÁ‚)Ïë\J®%|ˆ÷ï¨gÆøcôº6¸;Â6PϺøR<¼Q'ÕTGUHiÜ{ˆ^K£½Õësu8 sܳ«UJ©åšÍònŽúL12h!p¥#”4äâ¾6UöiÀÎ~ñZ§>Ž‘˜~¸Û>Ö³€úÙ#Óí¿ïŒ‰\’϶¼Ži!(÷Ž éwÇÓæï;ö€AbmwZ çŤND?Î'EAW°6–~@äR—`[:Èóy(W½R :ªò‘û˜È[]Ä‹¡s87×cÏ‹‚v‰U¥ÍÝâîóáÊ`Ju­º¬\Å`z‹Î¯ êOÞ8^Ÿoã~l”¼.ªÀ ý•@ï-–´&³rg?,4o>¨‹*”⇄–ø¼§xK*Å:›x´œýß*×­”Ö(ó ôBfU!{ì­ vÆ"QËáÞ¯´jlÊ,]ßÄt¡¹‹êÅkbši¨c²´ 2›ÂK…šQUùHƒ™2vÀzÙ†áI‘KÚ#|²?ú¶Ë×´ªÊX=  ¡ZÄ6Yn¥Á_%üM‡+‚8‘4H›¨´å¥4ÆOØ:€W£ç“š\Â;D.IDØYŒ~ßžãµq]#é¥î »$þllOôç–÷ÂßL³Ì´I¨7Ô25¾æÞ¸*tOú¹G±d#¹š šhŒ]tæc‡ñýLó> ÍVfmÇkx¥?ßô…ÊÖ´ªŠ³ìD‘Œ#1JÂHô¬ÜEÏgsí#Í>ÛõØ;X¶ãcÃøßÄÔS’/ˆIEND®B`‚ Ö‰PNG  IHDR@@ªiqÞ pHYsMMgŒà ˆIDATxœå›[o[Ç€¿%EŠ”(‰’­»dÉNšÂAà&EÑ H ô©}é{ý£}*P´A6NR i’Æ–lÉ7]HI¤.äôaöîÙ3‡¤äôÉ$w÷ÌÎÌîÎmç²âxã@œòžþx“„ãùMB–W—íàðœ<Ün%`2‹/m¯ ¡fàbȘs g´w¾ÑþÜ©*Ï£5d¸í =^ø‰§€9ß.ÀYÐ>ëÛú¾£ 4Šï?÷4Tý÷%p—§P9òmS¨ ŸÿÄÿŸòmh§¾})è{îq1o…R’- @V1k Fûȼÿ=rä®ÿ̀ܲ#¤îÇ,¨àå®Ç1áûç@6óGUší ¿)_óˆËÈŸõÁ\ß6HÅhÛ?·â'[ŽðÝsr<ã%ÿÛyF¶ uKÛå-ƒÙ2ÈOŒö ?Ú¼ð‘0’š¬0Ƕí–íkLN`¢âöYN{IçËõ—³Ï¥»¹ò«q'ßlûPcÊÈmcü*Ⱥ}M|ó^y^dV·¼Ù7òdÑèkúg#Ë&›úÉ‚q&äà> þ'Ê£ì!£«py *_ád xZЗÚá< ðºO ö*hë(]”É)¼˜7˜0ÆÄ4Pæ Z½íÇl/T¡ñÄ,ùÿ‰éJ4xØFÐö %† ‚2¼çŸ]~|Ì "œÙŒ ÔÌ$ÿ7€6¸]?Q5]›À×à®ô|¹r ΛB}oG˜ŸøÙE`5©{ö³Ò†ú ÔŸX~€Ï€ç€·<ìøç{ªÛÜUÑÄ[ó&[YÍ)^|)FËL.¡¦rF?7YõÊx=kUÒþ™~‘ ßx+¶ŠQ@ɘµ¯² ®]þÜ „¬ÌaSçÜSß7檛àŸuO€Å}ØÌ÷‹Ö¿÷²sºžò"ëGÀÀp1zæ¿ó[é1Y! ¹ÙWÆÝåø<Ž îRq7ûÑn=¼ð=záê)NÇX-%8çW/ÙF•L)0wÀ}àÒ›«Wèî MžŸÐÜ%èúãV;Äö÷§.Hxfû:VîGèÊ­c ¨/’Ñgîä§ñcXy .*PFÓ)ƒ­½ üøKt桞?>N¨ùÎyÔRøÕ9‡úád]Ô<þø$ Ù+Ïz ¸ r-fn%bÄÑá]`&ªtûÙá²ü pŒ C€îm¨M@ý%¸NCGÙ¿Rà¢Õ“¼ÖN}“¿kàö‚Î6È ºcßöžkB[ŸèØÇ:  œû î¡«ò…M³,¡Û¾‡­¦U¨µU‰2o€ëè3Õ¶2«0ª£‡ú+ˆ¾ñcîùç:d¹"+Àð õŒ|غພàH1ÍC»†®ÂùX<Ûp<…ö$/`‡´]àLçŒÁ¡‹úƒç)Ç‹%€ê=-æ·|Í)p­l["„Ë÷a®³¯ÌGo³¯`öRq[G͵`® a5ß-ÔÂeÀÀ4ª= ”š,£nªÇM¨|Ì3~ì=ˆ òÎaÂsÛYr—@Ü1º¸° ¨iÙ) d¶ó2¥d"¬`ëÚnû”J^¿Äî”6±x:Ô#4Ú ”ƒL¿}Õ²á*Ì‚t˜ÙúZÀ»ÀÒŒN’Ô¨OaåçÐ@çý- ;sä?#Á¦}dѸcï'Y 1a5rIH™GƒeOd„œtf¡~ÄAÆ#OÐ×Áä+@:gPßtoA-É/>óßÎÏùš+ átf ž-œ·‰*¾¾î†TW%yËLÂÆZ™EàKª'à%¸C4Ós–ü¤õ2¸çyT‚g~8FWìcwµýø2ºâžŽÿh_.z;ñ¦ÏgCšd˜šlOà¤)²á±©J¨oƒÛ ì÷-lå7 Gih¥ÍônO‡‡Ä©/ g‡R'¾½_Ÿ¹r'¨·ºôHe H .åã( ¾óGF{e%´òœ³”›:QxŸaß?„´ 7N|j|Íêµ°Xöߺ„V$4 1œ“ó.ŒÔ{Š„iÑú C`GàZáì(Áô;§0”6<<×…ê¨|Û5r î Uæˆ0AqTw›^ƒndÞb! ½±¹©Qô\®=¶ËpeióÀ?È´—ìöšP{fÐ! Ñ$ À£ý2b…»}4sÐÃÎÖŽŠz˜¸ÎöOìuôGô¿&ÔÅGp¬y#”Ÿ¡ö7ÆÕ³ŽÌòš¤ÒmÿÈFX‡B(@Û³ÌÍë€.Q¼@³Ã¿ÊÏŸùQÖa([9ÆHrç9my"è“Þ')ƒóIbt|Û2zÑ Ž<½ìg·± ¹CYÏæÓs÷Opí`Ž‚<£š8Ùæ\Ó`-a< ‡Å¡»ú8Û ¶2Ùéø„W×ì¡ÁÏÈVàoe_ûn0¶+æ$“ÀË„ä:>£8‹”à…m ]_‰²êmÜÐòg0ÉÛ¿ó BhÝF$VN²Ð÷-£Öh,%ØFí¥?ûmnd_8ƒÆ2œœá%^œ(Ιew1,S|¼’…œYµp†*—rA†8î»÷O·ýWÐ8Ósúc¬*N¾Âô¥­®-t™ƒ³.ÊSNxEù€Äf¾mÔ«8’êOÓmžfa.NÖ&°SÓOˆpAš4pÞtɺ×í+rJØîí°õKU\²8ž¯/Î]I#<ó¡(«vúÔ4_TÀ}ëêDV 6ƒñ•Ó'h9À2Z'ÔÆ]¿E¯Ç— —¤ NÑøü=/„a‰!­5 ºÑVMÏ%ð{àS—£‰LÊk'õf;…ÃÝgÙB ¶†÷7¢ö¤oÍVCܨF0}~óREªh íÈ»¶N÷óÖJ>dŒ¡C #ëNफõ€)¢ðŠºîê%¾³ã‚Ì )ûïÑÌÏyŸ¦àà žän"–º@•LIìÌ ¨LzÉ…´ûЪªÎ¸V]î˜ eÅݪ¯úc{º2_*¨nËÕ(X±@ Üžnyy( §¨Fîÿ ˜$?wà”„¿¡ |H¼¿U”ѧ0'Q¿9ÆûÐh‘¹¾“ŠÒãv@îÌfà%i.Àí¢u6ÉÖTñTÈf"NT€ì£µ?÷Æ— hÝF›%à¹î°œŸ&]Õã&ºêeTyvÐÚæ%eÐËØŒ3íwš WÝZÿFfŸ£Bxç'P=R" Í[_çh$›I€ƒ ¿»ªÉ !{f¼©3ËeÅ/Ko#|<À½ Æ—ã{FëÄÌt=!Ñ‚EÐ+ïeß¾´ b÷‹ƒê¤g°SàÅ­À}LÁÑ÷Ùë9qž¶øÌ“B“;ç'S._ÏZðfè}ìDI2æÿP.ºòó¼ù•¦ZŠ\¹ü̲^ùÇØ/L¬’{aBú‚…ñC:f{̉CœψCßù¥ÑWÎú+©ªŒÿÂDŠèONÈF^òúÊJî¥^ë•™´­äŸ·@Þ1œŸ²¥å=óÈã+ž<‘ZY¥{€àw‚q}-~kÞÓO+tž*Œ~ijÊ /+ ^¾*z9j3f‘-ÏC)¿›bD†“#›¨¶õ5º´üïjæzrÁ൹@ 7 åÄ)JJd{Áÿäö&ym.(—Méëúq5a§þ÷$ªÄ}T¡ÎxöN“íÀ¸¢ŽA&0JÍ<ž:y-¿8YB¯Ê’#Teà_à™8$kó¬'…Aâ3£´6}¬ˆÇ7÷ÕY«á € Ïoó ¨þ´ ý~Ï"[ÂIEND®B`‚щPNG  IHDR@@ªiqÞ pHYsMMgŒàƒIDATxœí×± Â@À}Š@4B#´@€Ñ ‰E#TáB,šxRd}jŒü3áꂽË. eœoK’±&Ç ú¬iNêuzÞ_ßá¡1¸Çå“ä””q¶ЕÖ†’¼Þd}sR‡­KÿÃ/ÐÜãò‰_ Í/@'>ìý:W˜ÉIEND®B`‚ЉPNG  IHDR szzô pHYsMMgŒàî;€Åb±X,‹Åb±X,–ÿ ê6ðraÀS@òÜD†½º=WƒÖöHr¡AJ†ÂC~Guú®G맯‡[1£Œì{ Ûµú®¯€’¨¹›à†ñ4çuûWCï °5`å*„2‡nÑî_ íîw2D’ Qš·GBÝþÕ0òü6Î9 îˆÒ¼œ3‘a%Œ-ÁBšoy8Björá S9Ê1ú>Ïð‰+^Nåä´É,œã0MåÂ^Ï”ÏÒ+d8i2K,€—SïTœÜ·Œ§ùÅTŽØ V>#8ë \ˆÒÖšX÷áàk¢æÊ•Ÿ1òpb-`ª‹KËMQš7(ßMdˆý$6uŒ3!¸+BjðÃ{ºýc/^dù`{… <©ÛûŸ(…éX|ã0-Ç{(›%_>Á3ÝÞ±°wXêÊ0ˆm%åÚd‡Ó¬Û?ÖNôK ¤@S©"·ýv§ÛD†X øàª›ÒÅ3Õmt.€Œ<$­5ñ…åDn•_º ¶˜¼)Š¥€¦É2PæÿÉ ˜*tò³É,Æ H ‰'JFÔç¡xÀ?ÎW¦ó$Lšý¾ –ÅWdÀ´ßÁ‚É,ËD^Xê`¿4*GòÊÎþìò³|l*G9FvÖ¼Ô*W†ì,D®ûYÞ1‘a%ôÐ-Îì¢êÐR*È]?ë\Õî_íx»qeù¿Á"ž6&ó¦öú¿¡uhÍKíì¢ü,¿YWÃæ±£4ò{¿ZWÀ×P–ŠG$Ûþ•/h.`ª‹KϘ0­È#¾×éi±X,‹Åb±X,‹ÅR_D”̾Äù «IEND®B`‚G‰PNG  IHDR@@ªiqÞ pHYsMMgŒàùIDATxœíÖ!NÄP…ás[¦ lCšT’ Ø,€u`Xa³ <’dXºS2A†ó.–¼¸¾IèÿÉs+Ni%`쯣»›¤2S—±3 ¿ ïûÃás37Ù¥¤ýѪ屒ôP˜_Õuý“º®;˜U{O’çh—kÂú¤iš×ïq?WU»·ÿîå%ÉtT”³y'¸tž§ÑV\ÄA2ÀÔ$˜ô¸"™,â `>n${ÎR''×2lÖ×qœ жíÛN©3—ßIzÏRn\+I÷Eá§ñ@âG˜Š/æO8jt­—/IEND®B`‚ȉPNG  IHDR szzô pHYsMMgŒàzIDATX…íÒ± ƒ@DÑ¿K–Ü ¸$Ç®n Ñ·ŽYB"ã.™—´ÒL° ""R™ý‡ˆ0 ¹¹s7³8 X–ooÎho°…3<»nðc‰3(xXâƒ_]–p ˆÄ X tna1æPý EDDªûf¯Á¨¬IEND®B`‚h‰PNG  IHDR szzô pHYsMMgŒàIDATX…íÁ‚ ÿ¯nH@ï G ˆIEND®B`‚ĉPNG  IHDR@@ªiqÞ pHYsMMgŒàvIDATxœíš=oÓ@‡ŸK¨ÊKxYjçü9ø,T -¬t`ecå€ÄÊK ‚ñ=¼1fpífCHH,¨R{,2‡6öÝùÎño³ýÏO÷<Šèd2dÈ&Gt½ÛÉŠâ¾Pâðs<â0Š¢oåë×:Z—“dE±'”8Fç¼î•gF],ÌEtøenês½P®„z®Ïöˆ™Œ£¯ú|¯‚«à§ÓÏUŸé€&ðÐMá¡ÚÀCàÚÂCÀLÀC LÁC€LÂC`LÃC@lÀC lÁClƒçlÃÇ\Àƒ§\Áƒ‡LÃçùâRêÕòð‰”Ñ—òu¯˜†ÿ¯Oñ]ÊèNyÆ›-1ëð5ñB€#ø !ÄS}¶s®àQ<žNw>éó>\ÂKU}¦3>ÀCG|‡øŽøøŽ8„$et¼N—u>Ãe¾ÃƒE!Àƒ%¡Àƒ!Áƒa¡ÁƒA!ƒ!¡Âƒ®à•PIX·/Ï»J©—ËC³{‚þß(xó·Ïäž`pð5i$Àwø““Óý x3{‚!À#x«÷Ù ¾õž`ð¼Óû.ûé¼’€¾ÂÃ8„ßOâøãº}màá}‡‡|‡ÏŠb&”x_Ñ·ÖmT)`SàÆú‰<_ìÇ‚¯ék ¿¢¯ÑôŸo@š¦[·nßýÜ(n _Ó×Þthÿ'“ÉØ.jõZJE_«ÅšîM@’$¿<΀_±×朊¾Y›Åšî«Í|>ßNÓtË×¾,Ë®›ì2dƒó¨oY` ›IEND®B`‚ã‰PNG  IHDR@@ªiqÞ pHYsMMgŒà•IDATxœí›=H[QÇÿçåI±*"Xt®C’"Ý„V7ëPA‰Z?¦R‡…béâGª8(¸¨›(TÛ¡C¡ˆÔÁΊ¶¦úAl“w:¨åååE“Ü—\­÷·ÝóNNþ÷ÿÞ»÷’Ü (Šë %’Té÷ëG»õÐH /ÀEn¤Y[²´Ãà ˜Ê. Í~êé _ô¡ ð¶ WyÄwœÑ™Ø h+#O?œ—§S‚¼¾¡nÀX¼jJcñ^Ûà €ãÞhW¼ ^_A7¯Ò#/“Ѓ"ïÒñ·Àû϶Wí‚Þ¶á*ÀX´„÷@x©…i¾äÀ½53ó(â¸V¦]›¹ÁbCçZ0zäGghÕv¯CŒ•~¿þëû­õ¨ÇžðÑ6š–Æ:ƒÎKwžŠÖwD×&Áxxc`#§0TfcÆ€£Ý‚zË;ÿó7²_•ÎÀÒXgÐ6šìÅ(=Ú-¨·æÆ`Q‚ýÝ“éšN–Æ:ƒ ö›c1}ƒ'ó¼)!LóÎËË ®ˆ6gnà±æØLƒ\dn•¸·œ–)l´ß¶æØ­¢Vx—m´Oí1«×sB×e€l²QÈ e€l²QÈ e€l²QÈ ݉"ßP5o¸‹ÿk€ZcæçÑvëï–I#üx|ƒÍDX   éï–òàÞ²ûDù‘q÷èÌLÊô˾¦€Ùx|”ŽAûNÿ+ص¾ý ÌÕ J\p¨Ï`s Åó¥Òó€'ôxÂãÿ °‚°%Üc)Ïåݸ’Å9”O‚qf$åÀ³”•B8îððdž;(ÜR›À˵ƒ \Hh¤9†¹øú×"mÄ»“¬Û÷JÌcîżsA0ýŒÇ²–õ#ÁVCu Óµ#uŽl!™50•SºÒIöÁ— ´/Imеá–]ÃtåH½I±Î¿ ‰X¶-<6xèú!hJXäxÒü%Á…úAîIg³î?ž7_as)Þ<‘ ,žIB ȼ‹‘ ÆR´b\á‰ö Hs=ƒ º ¦®œfÚ×â=åï1ChLûc @P× Þ¹‚2Û ‚ú»56î{½ýÊŽ|´`{†Ø3^†º$?©‚aRßI“©Ó=Úǽ{cé0»KŠ”NYÉ UçlÇ0‚ñ!ñ†ä½M„Œ€Æ=·5êLxF[ÒRJx5=És†¹O‡žÀ>kÏ–¼¿Ú…í QW Ömõ°gÀ0}¥ß2™'}ƒOFnߘ”¡U)õ¬-úußâ §ybÛ¾¦;D ¯ß­;î‹9`ÊpSÎ4XÀK³”âo¹j6–BÜßJ¸I¶!; 2vù¢;C•ÛÖ$4þßÀ3#!Ÿÿaî8žvÖ%,ï:yÀK8îÙÁIÆuâõ5 Q/?Éós™~´@~€åS·¼*¤7l¼‰_ ãê²c¢ö†ù->ÝúgÁ”"Ôàí6ø³L†W6“1'!Øûæâì†nàøIæw<ž{ t¤êA— õPÕ!{Æ»ŸŽw,¡¸•Ìmjn@xÐÅ|$o6`-ð áå<6w ƒëg|–l$såùÓE1 Aã“-te}r1ùíÌÅäØÛ÷‚m +3þo2y0„qz÷Ëu0n}ÔŽj¼gìÅÛÐ ¦5ÉÕìËiôâ˜pÃä}¢÷ l5;&•”ÝåT¹°óžÃkøŸ¯çˆ|Ó;í2OßIEND®B`‚À‰PNG  IHDR szzô pHYsMMgŒàrIDATX…å—ÏNaÅ÷k»"1QÀ»¾‚ЊÑL\ qÅÐÐ&@IŒÂ²iX•D#-BÀİÁ½MØ(‰‰Š@}X)1qÁªv®‹ÎÔ±Ð?Sz–çÞ™sæÜo2wà‡Ôý‰Ü-TgPº}R9BÙF$³—Ÿ|W×@$>ÿH‘¹³ŒùK•éâJòÉ)}cÏn‹˜·ÀD“¡R¹ðaíáw?ToŽ>½R †PÉ—¹ã$¬:3 ¢É½üÔºÂìYïgxê ðÀT»„€P©\ðSÜ 4…Š7ª\µj8¿b? ;‹Ç)®:\°~{{¸žX6j­XbÆ>å'^5ê÷Õ@4– [jm¶‘ÞFטFEOH¥ŒÐçŽx«ðÍ@ä°+ º9UfÿŠh,Vt®†Þ*ö/_¼³£?1e%¶š]Þô6;Ցî„¢ƒnN•Ç;«“û­øoš€-Þôµ6¢±\Ø©'z^_Ã+ k¤R÷¬Ãö£oÙ€*³",¹¨»}_:ãFD탦#°ãÜrs",)ºXÓê)ú– N[¦,£ÀIƒ.ÏÑ·nØYÜdº^½è=ؽö-OÍ(l´½guFÑvôÞ àŒBGi7zž?Ç»ËS›@¸ic‹p¯dGPY ýºy-¢ã )¾ž6 l”B¡‹2`ý´†*R|t¸ß#É :ŒJ¶?ž4g‡;/¢ã ¶ø<`!’©Êºí“ ~nJâÔIÀ]ý\Ü|ß¹ÿ踌Çõª왿A$V\N¾ôãžÿ~)r/NÀ¢ÊIEND®B`‚ ‰PNG  IHDR szzô pHYsMMgŒà½IDATX…í”=kQ…Ÿs×B ˆ6AÁZl’E;Y‘຋¦QÁ_ ¨¿@lRÚX(²‰A CJ‘$k£)RYØ(X%¨‘ݽÇbfÝe£›™õ#Í<0ð2Ü9çÜû¾w    `ŸQ·˜œ‹‚#ÿÃÔ°Ù¬‡£ágõêM¿W¯hé$°’&Îßxl¢{þË©W¦?ÙéEúQ”0†À„áåö˜®®U´µëÖ*ÚúÚÒeä'HHƒ!ó`#CH$üø[[Õ~ó]Ö§µ3Þ ×mϦÞÁΠ‰ Àöìx;ÜXŸÖÎàºß Û*?‹w±n'‚ŠÂ™Úa$áts¾·Z w~ùíž;+Ï{û~7(7wR"®™•º [ŸéhË ßD~”†…è3ï ÝZ­éÑ^Ú™{;9ç)á§ÀA’›:¢;°ÛFךu½È¢›k¸&>äçÀaƒ•†0%Z_4Õ¼¢WY5sO÷ļÏ{8†0ÑÝ–ŽÒÅ75½Í£7Ò?»àS±ã%àDúêC(éÂrUïójü“9·àãíŽ?ï”téuUŸFÕ*(((ØW~¤ø±x‹Ç¸IEND®B`‚ê‰PNG  IHDR szzô pHYsMMgŒàœIDATX…å—]lUÇg¦[ªj”4hwf§‹MTŸ* ø‘IÔ$‘øàƒ JäÅúb‚PCbLx3± (‰žÐ„TYÚÝÙ¥b "¨íÎ=>ì³Ýn»­¾q^væÜÿ9ÿÿ½wî=gáN7™ øJ>ïÄ f½AVê Ä,”Ï¿)"f&äeSUÉøù‚îrh¸Äó¼‘(¦ibP¬¹e‘\û=ש!¿’Ï;vÁlxpƒ’=æP”@DTU{³~. lAìêºBDtÒð}¿Ç¨œG ñ®… ^ÌÆÊøù·}h©3é›ï¸nüÓ(Éàà`ó=sÛ.‹PV{žs¼áºîÀDܤ·œã8WB[–—Q6ÜúëŸK™LðªªÖtPMU­l6÷BëܶÁyµ—MFÓ´åétú^,û Až/¹r¨ô‹È ( [–5R( 4/ÀbX…È:JõCà¤1…M¿Ôãh¨Âg2¹¥"Ú«ðx#xà‚Ý‘tÝ“Ógô×,N{X±õ‚®@q†är9ƒ‘¾ÎÎŽK3É{gÛ¿!¹æÎ®êöÖIEND®B`‚˜‰PNG  IHDR szzô pHYsMMgŒàJIDATX…íÎÁ À0A‚\P}˜šR ".ŸÿÖLYY­|¸2þ@Œ,õà^Û‘5ðÍræ} ¶ O²qÖ IEND®B`‚›‰PNG  IHDR szzô pHYsMMgŒàMIDATX…í×Á Fa‹Fr€¶qŸæ \­ 2¥ã{Ç„ïª%S[®¶üuÃLî×zåègÿŒšìÔNA9µ å9­ NIEND®B`‚Ó‰PNG  IHDR szzô pHYsMMgŒà…IDATX…å—MhœU†Ÿ÷fÒ¡µ•¢)6´­šÔI²ð§¥´Ti“š©…¢¸p').¢(p¥EDÑMh3i¢©,éBÚi¬¥ CAˆ “F”jµ?Ìw_3I“N’&dâ¦gs?Îw¾ó>÷~÷ç\¸ÙMó ~°×Õ -–·ZTcÖ 0ŠÈWB&Û¦Ë Ð”q:Ú]Àæ9&J¤ýC»õÍ‚ê»}k*ß=]ta÷;„#JY»’":R%±AŠÛ@ÍÀúbö‚öe›õÛ¼šû¸ê\¶š7+?k—:Òu<%û  Fâçhí:õ„ÎÎ`ÓA×Tg5Àa–éÙìcúkVáëlÇ€—þq9¾Ú‡ù'XhÓ™4 x%—}¨¿“=^â5ÅùˆO˜­†^:°ßry+}ºUc“CBÉGWâ Ö¦Aâ’³-t¬KÉŸaOéô€ôAoÂÚcø=I´gAâ“ .æÃ‹À9`KÃ!vÌ  ¿¬×O·ë‚ŋ6Ü®«B¯Ý5y&êymJþü;¯µÃíºZ. 02î3ª_# ì„üeÙÅ$ËÇÝ×~㣅&|Uvñ¢E…¯,m)@Õ…†Ÿ ˆ̺i Î ÆX$[½´˜[Ü9>K÷ÿË^¥`À‘ªÅÒ¼pi"÷ùñ=f€pà®ÅH ¦ø˜÷MØa 8n_,·ÕŽ•T¦è0z¼þW–]Üh@TÈ”|Û¬óÀ1`Måíà^`ÔRç-«ød.Eicm.¥÷9YÍ'[õýtá³–åu¯Zêø!èÉ¢+g»?T„#Iž‘TdìR%qyBUëCˆÛ](ËÇ7œ£1¯½Cíúu&9]LÒ=nîžK¼Å¡Žìn½Qì¼®fop¤y+PMá­rˆèxHÈœhÓ¹ùä½¹í?‰Gc¦‘&ÐIEND®B`‚b‰PNG  IHDR@@ªiqÞ pHYsMMgŒàIDATxœíš;hQ…Ï™]0b°1…ÁX awí¾0… 1`—€¨¤³ )­Œ¥ ZXZ$î† $‚ ‚Ù€E$……¢Å‚ˆX(>ÜcDwfw33{÷&Î|å›óŸ9™Ç¿w.gU oZ«iŒ ÐK`[+Œ…EÀ‚3L=»ÀoæG [Ô @÷tEÑ1HàH)ÏÇõ&8a•³Ez„Í{òÐh![Ôp½ ¡®€ì¬NBZ@„ÛLài¿+!p}Óêü™ÖkûZb­}”·Wx¨ö™ø?¸šÆ(¶ÞÉ@׺÷€ ÁÖøi?~ÞC€ÞÖØi?~ÞÓAEš½çKy'ro…lÑU½c~Þ·ÊSÜI¶ Ø& À¶Û$Ø6`›Øд:RÐÞ´ã^•Ëc ö4›Ÿ)ºoš¡ eóìÀR?¿†ÕÚpÍFsuÀ.ÓF<ï^, p1ÈŸ5êo§Z÷È«¢æ`ãä€èõ5n²HÓ/®™¢î˜Àãv›4³NÒ<çÆây¾oC½„„„óé ÑìË IEND®B`‚‰PNG  IHDR szzô pHYsMMgŒàµIDATX…å—]h›UÇÏyûSçl‹Ø²YªbA‘5Íô¯96D¶N°ˆÈœ ×t­éª txƒC ‚Å4­&ˆ¢W‹ít:Pº+é²X¼°„ š6®Åm6Ë{/’~fíZšz³ÿåó>çüÏá=ç<nvÉj’ëš»+­›Ù/*»**P Ê"I„sŽÚÞÁèÑ_ àm ûP:€§V8k\TÅ£G¿_@mcô‘é€s¡H¿*gÔqGõ_“*Þ`¬¦ñX±ÛŒa·Zê¶æ¦ÿÆqÝCƒ'Û.®`{swµÉØ>DR"oÝ2ùÉ@(”Y¶¤`Ðx'Ê_9Zün`ßùhëÏ+ðù  [4l¾rJxy°«õïe©:.Ý4MÂ!à2Âc>nº!ÀŽ@xc&Í5Š~˜ðL½A(dWc>'•:¸]‘w¤ªëKÄ^OÍÏ0‹‡¸ÓDj€þµ™ˆÆ£­"œ*Œ8Ÿƒ.(z€ÏßåE8€ð§jéµ™ÏA\žœlFvzýág–°¢ï¨ê;‰˜ÿ¯µ›g5|*”x+Ô1fj;ïBy˜º:5-”ùŒâÑ@/0<èów=” ˜½€ |=|*”.4ˆ¢Ú`EŸË@äIE¾-¼yVÖ˜ï²(²3*‘ßÖ  „Ì(€BEÀLÐȵTþÐÂèR±33÷Ý3?bÞ9ð¿)øöB1€´8žõò¼Ý€ÂøÌ3·*Ë=ë`®¹UÉÙØÜW‘=ë`Ø  ðC€[$ýªúlmc´¸ðö*Ve€±noÀP$0.Y²Í0ýj¡í½G"{s—Ü/ñm³½ÁÂ]`m;€ÁðÆB™×4KÔfïc z]€ø‰¶¸ÂÀ7Ígƒئ*î,窈G§çÍ7ÐÒ²—F½w¢üýµA¨x›"o"øQÆÝbyi~õ°DK¶½¹»Ú¸î P|yÅq÷´ü³뚆`É-eåÃÀƒ¸°8oɦÔw8rŸ5¶¸_‘1àmžKŸ®¤)­»Xö¼U9.p/T5õ‰Øk?]/}Ù¶üáW:7•šHC.”¡å ÖŒÙtêª+Ö))ò¨a«±vbêsÝ0ÀYqœƒñž–‰¥“CŸÀE‚£}€b>P3¸è ¡i{lB›4éçÐþ§ûqîýÿrî¹—Ë.Jšt'çºí*ˆö&ô2ÁTWUª!~F8Ž“sÛ^ŒÒ„Æašª,mûR\œë¶«S7FÉó¸êwcúiŸˆü¦’:s½O¬—Q2L›‡íŸ9ið¯Z­7MQx—Á¡qðObw‘¸G,75mý™ˆºYB…eÖˆ.‡¥OÄÝC]×?¢s‘" Š:8‚WË*ƒèj\s `‡I¾gf9횀˜ÎÇ5ŽBXÖûVÚøÁ¯ ¿z„ÛÔ®Œ="èR· ‘ Tº–/ºˆ ËÇÁ@Rß®…_ÃÀ `ác¿z‰"Ô Ó®%D˜~c1`ªûÍÙd I„¦ªPÅïÎ3&˜nÿˆÿxú2Lû ÀÚ4œtT(lÜ¥‰ÌÀÃ4Ìt<…ÓE:ð9±=óEQ;3@Q»£H›ê¾³Úxaâ²®k××.¹~ùª{Å#•sIEND®B`‚²‰PNG  IHDR szzô pHYsMMgŒàdIDATX…åÖ1NÃ0Æñÿs“r:p‚–‰‰¹ÄX\¡·ð5è*,ˆB{ œ€&öÒ˜†¢¦!vâ„ö“Ÿlç)ð߇ԉEýAK…æäe<ž¨zq¦ û`v’ùZ¬pÚÀs<—Ãd­ò+Hãt﯆¯µÈÃ+ `ƒWÀ¯$€ î=€+î5@Ü[[üàètÛ¨ƒ¼ßÎ{à¡9â`O0Íd¾T€ø.ð(qpœ¬¾‚xt{qöV*€/¼P7|1Óž$zë¸s߸S€*p°ü lñ¨?h¹à`q.¸ eâ‚ç(†ËLâFwîÔ +ÂS0¨_Lk°Qg¹i6õ- Ìχ™Ý àmY<7½¢S6oê _ÕeÐZ+2ÎÃQ!×yøz]þ`9¶€‡ßþd´ÖÒüªËÄSu6ý­µÂ®5‹çº¿Ÿù¾¦•…ÿ¼ŽIEND®B`‚\‰PNG  IHDR@@ªiqÞ pHYsMMgŒàIDATxœíÕKlTuÇñï¹S¦ÚÐИPMJdcR6ÀLÇG¢©Ï¤$,pAK3í€bâÂÄLº3ñQ;¥‰ )1`ݘX’RÚY²¢ƒ„Ð IƒÚÎqQ£Ók%Ø{gºà|–ÿsïo~÷Ì Œ1ÆcŒ1ÆcŒ1Æó¨¯ÏÆÓõ³Êq„§P¾íM~¢~”[H4ѹµ ò)IŽö%½äy^@$ž}éïåÄý߯w_íoŸöš=ŸJ8žùPà“¢Ã{ª•OŒõÆÿXlªãC±ùÂŽªU5߯oé¨öž=§1•ªˆÄÓ]®‡Ÿ{5¼/@t¯Àm×éËÊÀð†ÖCk¼Æ×ï;¼|òVÍY„k”xÇË»>l º;ót! ƒÀ3®Ñ-U§y¬·-·˜Üp¬£V$pØèM òÖhwrx1¹Å|ø @ö«äÏËff^¹à­)\ÇÒÍÿ7³aÏ¡u"Küëáå'§à<ïÇÃO 9zpânPßNºF!¾ ÇÒ{6kc"óŠ:…a nþD.ÏNësÙ¾¶kž ÿ%àWÀDvpöæ¦è@íd((ðbÑHDh~2Òº¹)úCCÿù7‰§w œªŠÏ@ƒ[rG[ïùÙٗ߀…Db1Dºp-YáÔŠüŠw<¶kjþ* ñÌÇ íî,E;×NÔ~Ðßÿö¬ß=K¶€ÈžLŽö!×hØ©p¶d·ÔoK_UÓ#ÐâºNyÿJO²³TKº€H"½å4QÒtwkÛˆ"A¥‹.‚JKÚ¢2» ¬ÐŠ ¯ÌvVÍí ½]Í6HŠÀÐV· %Ólæ=]Ìì8ãή;Öçfø?ÿç=ç¼_ÿç¸Ô¡j’§·»&‘£Ñò‹ÌAŽ zŒv)G:Ó¬ŸÿW3ÓNE{-0kˆ¤]9iUW“¾úO¦nö•ÉdÜz¬êÅÞî>SŽƒÃ#½'‰ŽŒ—¸AŠsA Àõö‚–dô{ÕfnõM1çmÀm@¯Pëȱ|´s–²ƒN©Õ!5…Ge¿ÔJŽÖ¢Î‡Ô=dÓ¶¸6œÆ[©'3óurPáóð@‡G?׃–`NëîÝÍÚAõ¾‚3Þ Ô×eºÃ3¼¢Xx¶êÛY‰½èÉZ©}«·4%ÙoÐßñ]PÍöÎïû‹OowMˆq)Ò|ÌD bvdáÃ2É{m*íZ¡¥Iyö$W\ÔOSp—á\V“öµèDñe«CýžÇ~ YyÆœ’ôž&Þ)©Ûìá—'Ý ÜJÔ‚L³:úÞ…ÒñJø €`½ÚO|rÜXXÊÊâùéŒ6^7#×c'w Eg…^Ì+zm黢©ŸøÌýÀñ“9Þ/å­ŸÌjИ!ÁhE};O—Æö4‘~îH¥¹³Ÿd`! äZt¶/>m‹kÁ« 3:¢_¿k«¯->K–½ 86õ3€ã}ùŸðy)O"Ÿ†U!]dô?‘eež¾°4»¿TSøK‘µ jñ¢?XúÄÁ<'*ȃ8wò‡¥öb Ø…OrcG¸Åu}1Tw/£ æ AâÜeäG_Œ¼¢h8t±€Ã¥'N¹ö}àJ øWn,£;¸Hˆò±¹sÛÙÓ+°ÃN€à8¯ŒDá=àbjÁÙ˜ –;Šs j_÷30,Év£SßwñÚeuùíjÕ-­é\¤s7Êh@TH÷3ðMƒŽ_ã†]ÍÒR²ì±ð¢Š­ðÇßñjidF :à‡®FнAÙ ÖʼY·ÖwøŠ¾ø¾åú'{Làu ¾Yá5³á±Ò*Z·Ùí|QЪ«!@ª-·Ièq`k¦[_ާµùöqÖ|ÄDÀ†CŸæaÃÞýT¾V}:n-vfš4{Pù>Ð{€[ßêìÏÔ<²Ù €[”«¸¶Rí<+ûMàh2¡é…­.¢by)ôƒà* mTV‹w¶èÏŠ" ßÄwAË€¿uog³öžŸ7`}KµùfámÀ-ÀK­—aãPšÒShv¾)ôÈjØó°¾­”>h’ö˜Ž€)„zlo‰ðY.ËÁd¤÷ô0â¨ãsp}qžómyßç˘Õâ®ý6Æ*|ªÍ3…×÷ %ßb¿ÐÊL“¾¼PnUÍf¤}ƒ#Ès€ò4ô z@»BŽôîfýX ï¥SëÛx¸øà6IEND®B`‚‰PNG  IHDR szzô pHYsMMgŒàAIDATX…í—1kQ…¿3Y«€…b vùbµ»¢ˆ‚]BŠ$øj•Ê$e©V°0`PAAÒÁ-‚ vA²»•ø’:AÁ"•É‹·¬›ÙìNv´Ñ¯zÜwgî™sß̼ÿ:ê”ßûfŒ—eÁ•<Š1õUê³úÜS@©êÇà'iÂr"ÆZjÌêé)¥ªo??°¢j»÷õ=ª7ÞùR<Â$ò:pQÖíàD!$Ù^’ÖBcV›y$²Y¬ZÂoc¼ LDí,QˆN¨åY¼‹€D)Ú‚ËËö4š3ú– ÇB¬Ð#÷Ü«ž~`ô 9£gåç* ¸í ýôb rõ¬k¢³&3±âHÇ~ŠJnJ×xÜéŒIZû+ŠÛžH>`ìÔ¿ðòÏ H³Þ¹ yV÷»¼ï"ì·ªS­´Ø˜ÖÞ úû:ÆeoµìNÄ a} Ûk(Fuì׬ø@ªõ³~`’Ölot„î–¯óÐ1bë}[عÓ³½ü¼+5“õ `U± šÇõÌÉ°ê³ šÓÚ-õšW¤ÅfFë3 h|å]­H8—õ™¤¶bë³  ÕŠHšöýHš;¯õÌ¿ãÝ}&ú&HÛÃ!´6yݼ›bÕ—“áÁ)˜:@<Â䟭{Û4B Ý‚UŒ§×‹U ¨uìá†"yòIág@¡J˜K;˜TÈs§ô;½&p4K¶ÎcÝóçäÀ¦‘v4ûÏ/¦Në­¹mËIEND®B`‚‰‰PNG  IHDR@@ªiqÞ pHYsMMgŒà;IDATxœíš½kAÆŸÙ „ Øˆc Cb¡BrÑ”â'± Šx`6—ÓS þÆ*¸—Kr)Ò6ÑB„œ ‹H.)$ˆ…‚Å)µˆäƒ3™}-"·{{·s“sçWÎ ÏûìsóÅî …"ʰ G3»›8O2BŽh ÁWŠ ,Ãô†®g>Œ%WÝ: fŒž¬)­AtR´x>{óU¹š_娀¬—ع­€•ë6Ò×Ëuð5ºúÍsŒ!‡ÖhœF‚ç¶ç¼õ‘„ã­fÖuýpéšàùlâqðÞàCÆuŸÏgSÏAˆ&¹üló^/«¸0T² ÈF Û€lT² È&òT<uõì×4-A`§Ah®Ô?f˜Ÿ|»! ÌèȾK§~ùÖª×b‰ô%X4E„½4Û}»Ù>Cv€á$ÿDg¼º8žšó­We§@Ì0‡aÑsÀÓÇI›fám—‘YÄ1€n#}ÀÈÂU¢3ÐhgÂìUÀ@ÏÀÈAMŠ*è]³ðäÄ-sq[[Ôp€bhãE"„¦õŠ(!¾œÖ€ù–—ÄøòüBÄÒõC"Œè|«ƒÀre;°àqœðÀâXÒÿAÇ…ÎÁ 4ÎEH»ù£° @¶Ù¨d @¶Ù¨d @¶Ù¨dŸû²h¯ðí¿Š¥ Ñ„¥Ò¦H@ Ó¥mQ  °¡ë™ÒƨÀ-îto8 pv£Ü}ázÚüP´ø¼Ëeéÿ1€×å7׋£ïß[sël{ÕÂ^+ŠÏùlª#lQûÀð=ì"¡À°,BÖᵈBAa 7"tmhÀC›"Šà§eYã"„õÒ†¯ ¹å–îÞUçEô®ä'ï,ˆÐv<ä›<ðTDA¯0FÃóÙÔ Qú¶˜¥o?kYÛµàTÙ~bYaŒ®ÍOÜ6E©øÅõ¸a³€!0œaŸH3)l¦m=˜›¸û¥õ E„ù:ÞÐ1K4jIEND®B`‚‡‰PNG  IHDR szzô pHYsMMgŒà9IDATX…íÎ10A&ÞP€ü[ " Üëÿg#eõdõl>Þf|ÀäÆ5‰•.IEND®B`‚ƒ‰PNG  IHDR szzô pHYsMMgŒà5IDATX…í—1NQEÏe(‘×0"µ ´;VµZH/« SŒ•1±6q6ÀhÀR“™k“ ‚…™‰…ÿ–ï¿ü{òþ/Þ…ÿ.m’[÷Tó9%ù,Ô¹nÒ3=íèÜe#¤ û`fx+Ã]Ð4Ä‚òèõ$ºúp4u?Ljçȼœj^†y¡îÔíLž`Ž«_L¢V4äxhx¯ÂàåTóÈŠ<,굞D0«Â|˜a’]e½ùOòêC¶vü‰@ €6–‚fÕ†Z­ú‹]©!îNÝ®Ê|}wŒH¿8× ‘É“* :>Ìä Pw¦qQÿLî³k¬KàƒU0Y–a¾{ Ô÷“BÉ­{Š<\¯Î­íó_jHi¼Í‚>h±o/|A'>IEND®B`‚‹‰PNG  IHDR szzô pHYsMMgŒà=IDATX…í“?/Ca‡Ÿs[ÁЈoÐ]b»ÚÉb4H+lÂfð $:X|ÒÁªôv1K,¢Ô,¢“„Ôû3PMl÷½É]Üg;É{~çÉÉy!##ã¿c±^KVjɼ›MÞT¬—T ˆõzg(<*½.5”KW fNØò |Ì«Ÿ®Ð©Ú©Ðî .E®›ªÀ·Dn ¸ø)§ËÑ羯@¼#üC)r&Ùz»bGq3¼60 ÝµáJ‡aS³© P37Þ·Âo˜é2]à>€{ßþd’F\(Ï9Y1Urä¶‘­o˜Í_-ÚCÜ ï_0iÕÐ1à„-tªvî“ãµ°¥9Cua¾Ã½Â3MRÈ›´×©Úïðø’À¦“ëÛ`3Ép€|܃'àn¬¬Q3—T ###ã Fpb¡›SjIEND®B`‚‰PNG  IHDR@@ªiqÞ pHYsMMgŒà³IDATxœí™Oh\EÇ¿ßÙMˆ'[±¼¼²gOí!þ£Z(‚= ÆXÁ£¢ ˆ”zi½V±àI¤Ú‹•B¥^-õ,’}yY§M6ûæëa7t÷½·d“}»é|NËüfæ÷ýΛyofðx<Çãñx<ÇãñÜ[0]ED}(çJ$M@W‘P’¡1 ÅóÖçz‚éÚõ(n(MÞxIª6,w˜I)9,d€ÀÇøÿOû<ÔñÖCf @½¾¾ê*€ûS}ˆ¤Ó!†Y_›r837ÞL×ψ¢èqÁüàHª‰¹´Žƒ¬§ ²ÖÞî× kím—˜âÞˆÈCøî`žybÍ%æD?óÈ4Èa¹ÑË-w@-ÕRÝä_(e¼üÖ*›çæƒ Îmv·õÞ4#;-w ÀÝå@2™ä H(1åBÀ­é²y!‚½Ú4•ƒ ØØº3󀟺ËI@Pi Q,QÈšpcûÎÌÓƒ˜ö±–kµ£›r­Ó ¾ë.'% 8›Š ‹&“¸²ÓÜ:]«Ý´¯}‹–TŽâõKs¢®ý•%"Àìƒ#.Ûpöl{IÎ¾ßæ$[6œ]ðIN4ï3T$¹æ\´áìÒ~Íw:<8Q¿+àBN¨ðCÛS+oçúžµáÅ!úŽz=~ÄçH‹#TО‘$¤´yáj5¼Öšœó¹RvÉë×é©S;ö÷(tŽìX;?ÿÐ_;Íí?÷HHê¹slo§•5OÜÜin/ŒÊünºº:3õ ¡3Ýåçï:"ò68Wk½<77·5J}c9ÀH*¯­Å_ |uú¿ªT‚%’­QkËÍÉV¥¾é³=+‹ŸV*Áâ8Ìc¼Ú"é¬ ßùA¿:ß·6x§ó‡Ìxt+Q7«ÑúYB—ºŠDàMkÃ/Æ­e2—9V£øE Á·¬ývRZ<Çãñx<Çãñx<÷ÿƒÁ*‹¶IÊIEND®B`‚Ó‰PNG  IHDR@@ªiqÞ pHYsMMgŒà…IDATxœí×± Â@À}èQ u : rEî‚èÄ¢xRô&µÙ3áꂽË.`;Jœnõ\SÇ$‡}–4å]®K¹‡»vj¥Ë'ɱìëØ†³lÍì%eHòìÐeiS}•¡w àøÚ©•.Ÿø~ó °]¼$lŸƒAIEND®B`‚•‰PNG  IHDR szzô pHYsMMgŒàGIDATX…í×± AƒØ)-g¤l1„éøïc]‰Œ"»"»œe¶y¯é€›÷ GŽ»²Ó|c…~IEND®B`‚ü‰PNG  IHDR@@ªiqÞ pHYsMMgŒà®IDATxœíš?nA‡¿Ù…&H¹%)iˆ-¥CØ „$ɱ¡€ÐrÇ‚±h³>D$.À"! ŠühpÆÞïÎÌÎlüÊõÛŸæû´ž=-ÌjV³:Ï¥ò^€í*Wj·„èÈw‰¢Gýƒ×_‡¿×Â\T¹Z_‘=@ /ëÃ=Q.+sPÿàO1^Òû ) þ·(žê½…û $Á£d½ßi}Ñû µ žwZãî)Œ€4ðPiᡲÀCà²ÂCÀLÀC LÁC€LÂC`LÃC@lÀC lÁClƒçlÃÇ\Àƒ§\Áƒ‡Lתµ;ˆz€¨‡q¯ùyøw¯&B¦áËÕú¢ö%` %¯ôoØ€“7R^p?Ôc½7w®à•R÷ûÝæ'½?×MÐ%üa§¹7îžÜø9 ðrà<8à<8à#<8àì=/²Õïµ>L“e]€Ïð`Y€ïð`Q@ð`I@(ð`A@Hð`X@hð`P@ˆð`H@¨ð`à!WðÀf¿×jO›·R©ßUÈsÀüLÐ%|ÜݾTÙÙTH3Áà7L`L%Àwørµ¾Á(¼™™`ð"òVÏ32 >óLÐwø•Ûµ{J©wzÞ¤Wç (*<À\ÊÅÚ˜älÄÝÝýió²ÀÄ'À%üa§éÎ8 ú_ªì¬#ðLybûÿ^Ï#Å2"àïÙ¹!ø„¼ô}B)7ÐS–ùËǃc‹C—SÃ'䥆7ÚIpñädNÁÂÐ¥LŸ¥ŒÉË´XÓy ½¿ýºrõÚà¦ÀÏHØŠ»­NÚp=Ôƒ¸ÛüèK^b­®>YXn4æ}Í»±½}ÑdÞ¬fuŽë}ê¿ YIEND®B`‚ô‰PNG  IHDR szzô pHYsMMgŒà¦IDATX…å—OhUÀß›ì&Z“ˆ–˜šÝÉîÆ€†=xBõO)-"mª "R UñЛ ¤xèAÑ€'¥(ˆ ˆ^ 6jµ`iOR(Â*‘m²f¶«xØX5™yŸ‡]“lÒdÙ—~—ùøæ›÷û½aÞ̸ÕCZi¾Z©$c=b‘ý‚&Q¥‚à \4†©d2™ï¨À¼çí6–Ià±­ô+\6ÈÉááÄwm ÌÍÍõ;Nü„ç¢RUaѳ*2vuUo C†á ª“RÑãÀpÔÿµ —g2™_[ð<ï¾ÐꀪÀ)×M|,"ÁͤUÕ=ïYQyÈ ”m ‡3™Ä[(•JÅ\v |±´ô÷ £££Ü ¼6òù|w¬»ç}”ãÀŸ¨Ù›J ]ÙT ŸÏ÷Åâ=ßc ï¦ÜÄ«"b[×CU¥X®Lúà£áîT*U]ÙcÖ^‹÷œ®Áuº8€ˆhÊš>ˆó©ª®šô*r¹üpå7,m¾Rbñú `ØW*ùo(`•·kñÆÈÈÈB»ðzd³Ù%”×T˜\yI±XÜ…8à÷Åë »²ÙìR§ ö<”ÊþOÀý6”ë«¢qDœC‘ÐW†×ÆU8 Ž>Y¯7Ty´–ðM§á •o£t_“"É()l—€êò<€@¢Y *ª.WÙ¦‚ >öPýAlzü±VÀVjÇØàvgG4¶\«¿cþPãÕºlz»Œ±™(õµ_¸`”ƒÛ% ¢=ß$àˆ®”'fffb†«ª(ÀÊT“€ëº×€óÀλ^ê´€çy‡€1àÇTj¨±7Xµ Œè€¨9•Ïçû:Ïårq«RûÎ`NŠˆ®+àºîeà3Ð{âñžOTµíeªªÒÛÛ÷0†pÁuïýråù&@,fÆK¥Ê;íH¨ª”Ëþkм\3èó+glÉjûA.w!|~{O÷±ÅVà¹\.~Goÿiàeà†}ÄuÝ™µ}ëÎ.™L^ ÙüŒòÔ¿þ™-½Uµk3°ªšRÉfGo.‚û¨³w=8l²-/ wbœy:*ù¨L‹ÈYæ1Õ ,ÄqÂa±DdÈDƒŸ³68–N§Ùˆ±¥“bÑß#¢“ o¥¸bE'2®{n³Æ–~Í …B ;"è~”$Bp_À·ÈE¬L¥ÓC³­Œ{kÇ¿ÁlÜ©ÜIEND®B`‚^‰PNG  IHDR@@ªiqÞ pHYsMMgŒàIDATxœí›MOQ†ßSÑ!t©„÷n°Ý´Äþ ÁHRܘV‰­²0_  M[#íNI4þî¤ +×It 2 }]´”afJ+2=m¸Ïj:çNòÞ'÷önî ÃeFÚM,„­þI÷Þ0  /ØhÿÌ!€ ›¬ÚûËK/÷Z}ÔB%–ÌN‹È+#“³cì˜[/¤—a³AMÄ’…«;A*x‚(VºTLýñ+7YÆ‘|@ÒU(‹à ˆïU z±Iÿ‚`˜Ä]‘FAÚøÔo%ø®€x*; àƒãÕOR2¥â³Ïg-§î€KæŠ0 ຣðh½ùèíM,„¯ Xßp²çGÑRqv'˜ÀÁK¾¹²à€¨üþeßrÿ1†ÜZý“püá‘ò¼×&¥âìŽ^4^F­°5áçP;ê”k˾7ùº˜þ |ü›”Öêç|í \ëþ=ÂÚêcî>0zR”Ý`‚uŽSsÜôÖ½4ŽÆn;ê΃kžcßOÀ¥ÂР @#@;€6F€vmŒíÚÚ´1´hchÐÆÐ  @#@;€6F€vmŒíÚÚ´1´hchÐÆÐ Ÿ€CG±ç¹æpèS÷P9~¨‚CA„ê$§æ@l»ë>d³ñ¹°­ž‚î„R›C‘-÷V?#ã©üd0á‚'þ$ûŽ›ã"\qñ°÷–Qk6P˜»óømÏm…ñ™wÀd/ˆŠ½g·P¿M=çøpð¨/TŽ%sS½±(±dnŠÕ£ ƒ×‚y¿š&¢Ä“¹÷>Ý"e‚k!Èn·Ý" ¡*8Tßó‘ÓU.®23~÷ž›tŒ‰BZhÃ%!"Ñf·U©eòKÅE²?ÓìÒw˦©x*—ð½Ö4ET ˜?wÓ”“hb!l…­ R&@ŽÕo]w_Û± ‘-®Ø{öJ;msƒáróô°å&aEIEND®B`‚²‰PNG  IHDR szzô pHYsMMgŒàdIDATX…åÖ±J1Çñor—úVj)]ª““sëú¤âRÚ‡PëRnÔÉý®÷wÚ^½$MÎÁŒÉ>?’?ÿ}©6±,˺Z›;4¯ý“Þ-€nOÒ9ŠKJŽ«ý´U5y)Ëâº:‹þ[øªƒ÷V4áQØàÑØâQ¸àÁ¸âAøàÁØâËåÛajò)ðÙ?íM À$tÄgÀH§Úß+€~<yç¦:ó~Ü\ ‡G{…{pÄçÀð\äf²‰;;ˆ$!ñ,˺IŠ57à‚ëÄÌ\ðÆžø¢È͸wš„‘ð­IXû'Œ„W½±6 ·šÐ Oç ~ÅksméÍbG|,ÊU^Ûp6©Ö‹ýp›º]7¤DD«Ä<4á"¢Tbî›ðͺ]øO%<6üd”Rt¾ëêñº:›y€ˆhiL"¢BÖýùúSBèˆaNIEND®B`‚À‰PNG  IHDR@@ªiqÞ pHYsMMgŒàrIDATxœíб ƒ`àÓ!Ä&àLYÁʉÜ$óiÄ!4­ø×*Äï+WÜ»x‚êLÓ÷*c’æ†>gš³¥ïº×gÖÅÙ>Ÿ$mêŒÇ°àaʶ I–뫜nΚáîÀõ~Z• ò[­IEND®B`‚‰PNG  IHDR szzô pHYsMMgŒàÏIDATX…íÖ=‹SAÅñÿ™Dñ_:A+ µ1aKI³²Ye×FÐÖBöˆ,~Q°±Í.q±‰I¥Vk#Ø(‹ø‚&s,æF,s“0ú—[ÌÌùÍÜgà¿^eP­?É(ôu¨³¨Ó„QÉìöĪÛG×½3;àšÙþÕ¬xë_8=»¶Ä» +®d`"ÂXçßUâ-ì‘zhj clƒ¤ËõÕ¸œP”˜ÞtµÞòRnöo„oÔ[¾ °Ñq§öȳY a¨?¨­úTV@B(’ÛÝ>Ñòñ¬€‚šsw°ŸÌ¬ùpf(5¥}qà§'×¼?+ PÄô58Øøq~@B ëX~€ …`£ZѬ‹@Jÿ*:ý|Nï3dl5:sz;êÌêÄÑ8 øî 3½¦^•™?Ñ ØR>0Zè5õ¬ìcŸ€$§ H—zój³Î¸' ¥¦Ch©;¯{c®S`#îÜ×_œÕÍqÃKœ~Ò]—ow›áÚ$á¥Â!õœïw_†+HÎ H;gýs?\d¹ø™°Êނη:÷¦¡Ó/ØDVøf_7ôeZáÿ àž¤@—ñœêIEND®B`‚1‰PNG  IHDR@@ªiqÞ pHYsMMgŒàãIDATxœíб‚@á÷[µX-Y‘]P‡uY ™Fw°Î¸_tÁ ¼ÛDÒ?«½pžÖqÍzÛ~V×ûXSÏû­N{~ëÛú¾£ 4Šï?÷4Tý÷%p—§P9òmS¨ ŸÿÄÿŸòmh§¾})è{îq1o…R’- @V1k Fûȼÿ=rä®ÿ̀ܲ#¤îÇ,¨àå®Ç1áûç@6óGUší ¿)_óˆËÈŸõÁ\ß6HÅhÛ?·â'[ŽðÝsr<ã%ÿÛyF¶ uKÛå-ƒÙ2ÈOŒö ?Ú¼ð‘0’š¬0Ƕí–íkLN`¢âöYN{IçËõ—³Ï¥»¹ò«q'ßlûPcÊÈmcü*Ⱥ}M|ó^y^dV·¼Ù7òdÑèkúg#Ë&›úÉ‚q&äà> þ'Ê£ì!£«py *_ád xZЗÚá< ðºO ö*hë(]”É)¼˜7˜0ÆÄ4Pæ Z½íÇl/T¡ñÄ,ùÿ‰éJ4xØFÐö %† ‚2¼çŸ]~|Ì "œÙŒ ÔÌ$ÿ7€6¸]?Q5]›À×à®ô|¹r ΛB}oG˜ŸøÙE`5©{ö³Ò†ú ÔŸX~€Ï€ç€·<ìøç{ªÛÜUÑÄ[ó&[YÍ)^|)FËL.¡¦rF?7YõÊx=kUÒþ™~‘ ßx+¶ŠQ@ɘµ¯² ®]þÜ „¬ÌaSçÜSß7檛àŸuO€Å}ØÌ÷‹Ö¿÷²sºžò"ëGÀÀp1zæ¿ó[é1Y! ¹ÙWÆÝåø<Ž îRq7ûÑn=¼ð=záê)NÇX-%8çW/ÙF•L)0wÀ}àÒ›«Wèî MžŸÐÜ%èúãV;Äö÷§.Hxfû:VîGèÊ­c ¨/’Ñgîä§ñcXy .*PFÓ)ƒ­½ üøKt桞?>N¨ùÎyÔRøÕ9‡úád]Ô<þø$ Ù+Ïz ¸ r-fn%bÄÑá]`&ªtûÙá²ü pŒ C€îm¨M@ý%¸NCGÙ¿Rà¢Õ“¼ÖN}“¿kàö‚Î6È ºcßöžkB[ŸèØÇ:  œû î¡«ò…M³,¡Û¾‡­¦U¨µU‰2o€ëè3Õ¶2«0ª£‡ú+ˆ¾ñcîùç:d¹"+Àð õŒ|غພàH1ÍC»†®ÂùX<Ûp<…ö$/`‡´]àLçŒÁ¡‹úƒç)Ç‹%€ê=-æ·|Í)p­l["„Ë÷a®³¯ÌGo³¯`öRq[G͵`® a5ß-ÔÂeÀÀ4ª= ”š,£nªÇM¨|Ì3~ì=ˆ òÎaÂsÛYr—@Ü1º¸° ¨iÙ) d¶ó2¥d"¬`ëÚnû”J^¿Äî”6±x:Ô#4Ú ”ƒL¿}Õ²á*Ì‚t˜ÙúZÀ»ÀÒŒN’Ô¨OaåçÐ@çý- ;sä?#Á¦}dѸcï'Y 1a5rIH™GƒeOd„œtf¡~ÄAÆ#OÐ×Áä+@:gPßtoA-É/>óßÎÏùš+ átf ž-œ·‰*¾¾î†TW%yËLÂÆZ™EàKª'à%¸C4Ós–ü¤õ2¸çyT‚g~8FWìcwµýø2ºâžŽÿh_.z;ñ¦ÏgCšd˜šlOà¤)²á±©J¨oƒÛ ì÷-lå7 Gih¥ÍônO‡‡Ä©/ g‡R'¾½_Ÿ¹r'¨·ºôHe H .åã( ¾óGF{e%´òœ³”›:QxŸaß?„´ 7N|j|Íêµ°Xöߺ„V$4 1œ“ó.ŒÔ{Š„iÑú C`GàZáì(Áô;§0”6<<×…ê¨|Û5r î Uæˆ0AqTw›^ƒndÞb! ½±¹©Qô\®=¶ËpeióÀ?È´—ìöšP{fÐ! Ñ$ À£ý2b…»}4sÐÃÎÖŽŠz˜¸ÎöOìuôGô¿&ÔÅGp¬y#”Ÿ¡ö7ÆÕ³ŽÌòš¤ÒmÿÈFX‡B(@Û³ÌÍë€.Q¼@³Ã¿ÊÏŸùQÖa([9ÆHrç9my"è“Þ')ƒóIbt|Û2zÑ Ž<½ìg·± ¹CYÏæÓs÷Opí`Ž‚<£š8Ùæ\Ó`-a< ‡Å¡»ú8Û ¶2Ùéø„W×ì¡ÁÏÈVàoe_ûn0¶+æ$“ÀË„ä:>£8‹”à…m ]_‰²êmÜÐòg0ÉÛ¿ó BhÝF$VN²Ð÷-£Öh,%ØFí¥?ûmnd_8ƒÆ2œœá%^œ(Ιew1,S|¼’…œYµp†*—rA†8î»÷O·ýWÐ8Ósúc¬*N¾Âô¥­®-t™ƒ³.ÊSNxEù€Äf¾mÔ«8’êOÓmžfa.NÖ&°SÓOˆpAš4pÞtɺ×í+rJØîí°õKU\²8ž¯/Î]I#<ó¡(«vúÔ4_TÀ}ëêDV 6ƒñ•Ó'h9À2Z'ÔÆ]¿E¯Ç— —¤ NÑøü=/„a‰!­5 ºÑVMÏ%ð{àS—£‰LÊk'õf;…ÃÝgÙB ¶†÷7¢ö¤oÍVCܨF0}~óREªh íÈ»¶N÷óÖJ>dŒ¡C #ëNफõ€)¢ðŠºîê%¾³ã‚Ì )ûïÑÌÏyŸ¦àà žän"–º@•LIìÌ ¨LzÉ…´ûЪªÎ¸V]î˜ eÅݪ¯úc{º2_*¨nËÕ(X±@ Üžnyy( §¨Fîÿ ˜$?wà”„¿¡ |H¼¿U”ѧ0'Q¿9ÆûÐh‘¹¾“ŠÒãv@îÌfà%i.Àí¢u6ÉÖTñTÈf"NT€ì£µ?÷Æ— hÝF›%à¹î°œŸ&]Õã&ºêeTyvÐÚæ%eÐËØŒ3íwš WÝZÿFfŸ£Bxç'P=R" Í[_çh$›I€ƒ ¿»ªÉ !{f¼©3ËeÅ/Ko#|<À½ Æ—ã{FëÄÌt=!Ñ‚EÐ+ïeß¾´ b÷‹ƒê¤g°SàÅ­À}LÁÑ÷Ùë9qž¶øÌ“B“;ç'S._ÏZðfè}ìDI2æÿP.ºòó¼ù•¦ZŠ\¹ü̲^ùÇØ/L¬’{aBú‚…ñC:f{̉CœψCßù¥ÑWÎú+©ªŒÿÂDŠèONÈF^òúÊJî¥^ë•™´­äŸ·@Þ1œŸ²¥å=óÈã+ž<‘ZY¥{€àw‚q}-~kÞÓO+tž*Œ~ijÊ /+ ^¾*z9j3f‘-ÏC)¿›bD†“#›¨¶õ5º´üïjæzrÁ൹@ 7 åÄ)JJd{Áÿäö&ym.(—Méëúq5a§þ÷$ªÄ}T¡ÎxöN“íÀ¸¢ŽA&0JÍ<ž:y-¿8YB¯Ê’#Teà_à™8$kó¬'…Aâ3£´6}¬ˆÇ7÷ÕY«á € Ïoó ¨þ´ ý~Ï"[ÂIEND®B`‚ljPNG  IHDR szzô pHYsMMgŒàyIDATX…åÖ1NÃ0Æñÿ³›r:p‚‚„ÄÔ¹åHp…°TÜ ¡T∠©™‘:Q('`€‰¹!5C¡¶ic§Nðh?ù÷ɶž ÿ}H•X«Ûo$Zßy zgªR\éL d/¯UŠ‹ibäMÏ’“t­ô+ÈÀÛO·—•ÈÃK `ƒ—À/%€ î=€+î5@Ü[[üðüz·nâàk4èuÀC'tćÀPOç· P?^¦œ¦k…¯` üøùæâs«¾ðB\ðÀÄ‘À>ð:• ³Œ;ð;(Ð>ñV·ß1Ö8Xœ€ þ­ÕÐÏ P70‰%hgáN°$|¥fþ ËÀG̯g¡®–òàÞ²ûDù‘q÷èÌLÊô˾¦€Ùx|”ŽAûNÿ+ص¾ý ÌÕ J\p¨Ï`s Åó¥Òó€'ôxÂãÿ °‚°%Üc)Ïåݸ’Å9”O‚qf$åÀ³”•B8îððdž;(ÜR›À˵ƒ \Hh¤9†¹øú×"mÄ»“¬Û÷JÌcîżsA0ýŒÇ²–õ#ÁVCu Óµ#uŽl!™50•SºÒIöÁ— ´/Imеá–]ÃtåH½I±Î¿ ‰X¶-<6xèú!hJXäxÒü%Á…úAîIg³î?ž7_as)Þ<‘ ,žIB ȼ‹‘ ÆR´b\á‰ö Hs=ƒ º ¦®œfÚ×â=åï1ChLûc @P× Þ¹‚2Û ‚ú»56î{½ýÊŽ|´`{†Ø3^†º$?©‚aRßI“©Ó=Úǽ{cé0»KŠ”NYÉ UçlÇ0‚ñ!ñ†ä½M„Œ€Æ=·5êLxF[ÒRJx5=És†¹O‡žÀ>kÏ–¼¿Ú…í QW Ömõ°gÀ0}¥ß2™'}ƒOFnߘ”¡U)õ¬-úußâ §ybÛ¾¦;D ¯ß­;î‹9`ÊpSÎ4XÀK³”âo¹j6–BÜßJ¸I¶!; 2vù¢;C•ÛÖ$4þßÀ3#!Ÿÿaî8žvÖ%,ï:yÀK8îÙÁIÆuâõ5 Q/?Éós™~´@~€åS·¼*¤7l¼‰_ ãê²c¢ö†ù->ÝúgÁ”"Ôàí6ø³L†W6“1'!Øûæâì†nàøIæw<ž{ t¤êA— õPÕ!{Æ»ŸŽw,¡¸•Ìmjn@xÐÅ|$o6`-ð áå<6w ƒëg|–l$såùÓE1 Aã“-te}r1ùíÌÅäØÛ÷‚m +3þo2y0„qz÷Ëu0n}ÔŽj¼gìÅÛÐ ¦5ÉÕìËiôâ˜pÃä}¢÷ l5;&•”ÝåT¹°óžÃkøŸ¯çˆ|Ó;í2OßIEND®B`‚u‰PNG  IHDR@@ªiqÞ pHYsMMgŒà'IDATxœíÁ  ÷Om7 €w@@òÉQIEND®B`‚ð‰PNG  IHDR@@ªiqÞ pHYsMMgŒà¢IDATxœí—½oEÆŸgn±Œ”ÐÄ)¢ûް05¡‡ ,” §LQQD!!>j¤(1éhˆ%Kà: Æ‘‰vv÷N)œŽßÍCq2ºÛÝó}øöœù•óî;ó¼Ï¾»3x<Çãñx<Çãñxž,x\ [›¬ø q³Q«\;Çb@%W\`þ®4Õõyk1£™-Q”\p#³6q3Š’÷æ­gn ‰QÒú Â;#ý¼^«\%©yèš‹’‚(jßõÆX Ä­zµòÉNÁÒŠ7 ÃE2øÄëýã@È©'Ád„߫۹Ôl6÷ŠÔWè?`ggçšàv¦xºE@º™~Öh‚··wO©±°عwïôSÿv6¼0šn¯RRäJ`FÒïû Á…å3gv‹ÐYˆwï¶k¥Àmx.µš:hØÿe Œ®;À¼úl¹œÌZëÌ °Ö>–6TR+ ‚S”QJÄ9³Úl–·g§vÆÿ€8Ž_K¿ ]<Æ/¾÷00¸ ¨Ò¸_ã8>7 ­ÌÌ€(j½âÄŸœJ…DŽ_ü¤²ßÊ)'nYÛ:?µÐ31ÀÚ䢠ÛN pC?ùCèåÐA™ì 6¬MÖ¦S;È‘ °6¹ â ÒaŠ7Ÿt Óó,€øÖÚøÍ£N$¬MÞ±žž‡@Þ››©·{ b@~im<êh}(Sí’hãÖ'ò./ÙM~V9/MÀ§Zåýiî )ˆ’Öu—s„8WüÁd~ç®×k•·Iv'™o"Â0\d)ø ÂÅŒ0Á‘Å߷Ɉïöì]Z^^~0î\c°½½{rñé½älAÊìÛÅ#Ì넟öþY\[Y9ý÷8³Œe@»Ý^Úï¸ ‡õúq¢–›5Jé냀ßs¡\.ß•?Ò€¿ÚíjÐq›V i‚Ó]‘0ÊÜôgP2«•J¥uhîaÁ0l¯ã6TSiêµýÃMï³èBì:fõìÙò¡YÃqŸs‥THÀÃñæs0ÈÖtŸp¯Õëõ?†%d°¶õ²·ðhäŸA–³†ÉKy ™°q| â‡y±Gú Q«}Ô?˜)2Š’Ž€ÒütÍÝz½ôå+[òjË`Ès.t½Ú<Çãñx<Çãñx–Ûå?|yIÚ·uâ0À]ý<}ò3Óqœ%´M2€™æQ$wÝÅY2@gó}(ùS–Fy-|¥Ë8L¸N_C¨Ìu%é-KµfU&¿Vºñ@âGh‹OÆåB‰±í—†IEND®B`‚!‰PNG  IHDR szzô pHYsMMgŒàÓIDATX…í—±kSQÆ_^é"´ÄþJG],(.yê T^×"äé ˜©Tÿ38ÁÅdÐE”… â ¨uÑM·îmQàRÈ=¾ŸR^‰}iEóM÷{8ßïÞsyÜ ÿ»ô{àüåò¤›ÀÏÈgXǬÚn6^''¼ä‡”—„VàHFæDµf®LÏžú¶ñéýÛx¢¿þµ³rîð³JΣõüIãKîÊÇ]"R 8ŠÙ¹x'Æú$Î-¬Òn6V²0Ž-dÅBÉxµ¸KäÍä€ììKØ-ж)†¾iAlAï1Xä^µÍYpþ¿x΄ï~áΕ€A°?·îvܪÊÒ‹¶UÒÌÒoû 8¸öQóžæ’JÚê-]ŒN®EÓØÙýÔVù²(àèÆ·Õùƒ¤ÝˆtQ\z¶Úö–S{ú>¯Š/ßÒLÿìð’dÒŽ¤y>/ p; I´&ç³²€) 0¿[CŽ.¿Íge›Õ…¤Ç€<¡¾.Áê<Ÿ,'öâ²CÉ/%½†¤ëW'×ÕFmûù@â! Å'¶;Á |PŸIEND®B`‚a‰PNG  IHDR@@ªiqÞ pHYsMMgŒàIDATxœíš¿kAÇ¿o“pž1Fr›Ý¹BØÚû´°“€]D%MÈ_`,…Ñ"¥MlDHµ³°RÑìÞÞ‚xb¡¨ç‘ÜŒMx»w—ݹ—ɹó)çæ¾ï»ßÛ}Ì `0ò © ÔëõÑF£9/fÔm)ѰIÀZ±XXûÙm²RÕjõ²„µ  ¤¢ÃHDÇq^uš`eU‚°"a½Äѽx(IX~ÎušéØÃ+–¤ (xÈ´âjÒ:€z½>ú»Ñü`¢/Öèx±p®}MHý 6Íy ÞÅ@iÏû?¤`oµH’¼gy†gúàE1ïÃDº¾ç]ÇVî-Tª5Ùåã˜÷AYÅÙ0è6 €nº1è6 ›Üг ‚à ¬á»ò"€Ó=çWküH"¬ïìüy\.—(hˆ®][†×…¤U'¹$àKA7=¯ô.Í—zt‚±Nµã#àWkKBÒsè¹xðÈ’oý0ºÃY$1? g Xà,|@†HÊeß.pˆàû¾K’žpÌÀYòéÖÖÖ ñø` /`)¦€72rì6‡p,®qRÆâñ•´Í]^Éã+õ†È…2‡‘]`š6ºLaÙhI€mÛ*NGjµZÂ=È}+lÐm@7&ÝtcÐm@7&ÝtcÐm@7&Ýt“å|€.ÎöÚñ=Íö¼Ý›í¹ €€µö±<‹…•öÁ¼Ð"ˆJÒ¹á<Ð’$ou:/Zzßã¶rÛ=ïÜÃw¿¼wïM¸P(í ­%)Hèú®B„gà°ÕUeÍSP0Ë„ÇK_ýélvxiµN«> ±†ÛºÙB(G_ÎL$§eyU Äâ ŒØí¨¶Ö°„s]Ý=¿çós/ë%Õ5 4‹ƒqÝm-„(ØÕÝSšÏÏÍØ>¶ V^ûLUñƒ‰¯j=ó¡øitt´ì‚ÜuF=EøöŸ$¦k t˜Ÿ“°ÝçPc@ н; `úæ xQ^Äàt:¹àŠz‡ Fb/1p|%J¹Åo{Y'FÍÚYßUˆ zÂûC?»YÓéäByƒ`üX‰òÊØª¨1 ²Ô­t'2÷¾¸!ÔM¦ÓÉ%Ì1ëØ°¼Î¯$hôÌam-CóðSs›€Þš›~~sÇâ'‡uµ «vöZsì ¨Úám´Ù¾l´×ì^í h+”²ÈF [€l”²ÈF [€l”²ÈF [€lt'Š„.„Áž[Äÿk€¼W¦žÜϬš½ Âo@¸ÿây°öàCpð¨üÆa°ö<Üñ¼h1!}C; Ð]Që…¡Ý ô í©!dÀÝ{Œ€m"5Äàí[tï1‘ m? ð{iñ5@¿œÓ<ôkYÃú2 ;9ò“`\©!Á¸”ù)RCøÈŒ¥€ŒÞay‰rè=È8‘K=-æÈ> ² ¯É2P“ l²QÈ e€l²QÈ e€l²QÈ e€M¬dnD£ÑFç‰746ÚKÖ; æF¾}NŠj%Ví|¶æØ0knŸtXWË0ÊtÊÜfà5§Æ&<6·‰éZ0ët^ž»#±N›cÖ±6,}õ§äÿ/m&þ–®>1N¹Êتhî¸<(¡yøé†>._¦S^÷qù¿ü7&s<3žºa÷¬î÷1÷v¦«»§¢ {Ò\‡ˆO§nÖKh¸ÆÏççfºöy¢^lº{C”#ƒSãɇ ³ÖRÊ|mŽ€ÞÊ©ë wmŽ€Ï ¼iæÚœB¡hoþAXIEND®B`‚ï‰PNG  IHDR@@ªiqÞ pHYsMMgŒà¡IDATxœíÐA Ã0Mx†AFx4ÖÛÐÚç®}®l˜rü ´ÐZè­t€Ö:@ë u€Ð:@h ´ÐZè­t€Ö:@ë u€Ð:@h ´ÐZè­t€Ö:@ë u€Ð:@h ´ÐZè­t€Ö:@ë u€Ðs×(˜ÍgIEND®B`‚î‰PNG  IHDR@@ªiqÞ pHYsMMgŒà IDATxœíÐA Ã0S¸F" #<ëm hís×>W6L9þƒÐZè­t€Ö:@ë u€Ð:@h ´ÐZè­t€Ö:@ë u€Ð:@h ´ÐZè­t€Ö:@ë u€Ð:@h ´ÐZè­t€Ö:@ë u€Ð:@h¯çC—’Y¼IEND®B`‚‡‰PNG  IHDR szzô pHYsMMgŒà9IDATX…íÎ10A&š… ,à8" Üëÿg#eMgMo>Þf|À9?™9;&IEND®B`‚Ò‰PNG  IHDR szzô pHYsMMgŒà„IDATX…íÒ± ‚PÄñÿ÷ÜÄ=€M,lèA˜kg°´wvгR!$všûu—÷%wÅ33³•Å4*ªc³ÉYxk›'„fʺ«.À6瀀´»ŸW€ô}‘úÜå‚âE:rúw¼„߀ˆxä. ìGylùOhff¶º7•y À²g/IEND®B`‚l‰PNG  IHDR@@ªiqÞ pHYsMMgŒàIDATxœí[}Œe~~3·{wpèQ³³{×kJZL,§ÒTK4¨¡‡E4*EI”@MMÀªT¡J üAU-ÐHBc `ÿÀ6U!€ÑèbÏ\ÝÛÙ»µûaz·³û>þÑ"óµw»·3ÇUïùož÷÷þæ™gÞ™}ç} Ìcóø†ÌÆI†Z›¬¬¤à ²D½ÎH@ÀqÇ )< AþFVög2™‘¨µEf@±XìSÐÖBq‚Î0Í(/jšúy*•zCDªH„lI)XÖ•¢°Àª0sÈQ¸õíÇŸ°ÃJš…‚µ Â-ÂÊ )Ø”I'žcD4m@>Ÿ¿HÓcøâaŠÀïEd/‡¨ã-ÀÑJ¥r¢ÒÖV—ËdK7 ²,ä2?  ½VBû5¨õ†a¼ÙŒþ¦ È—JÑvø@@ó ¼@rwK‹¼”L&ßn$w©Tj¯Vå µšÁ瘠ðæl:ý‹™èš0 P(]ÁãZ}¢€‡5¨û Ãxg¦ùÈårñŽŽ®u¹@/€²Ù0÷ˆˆj4wÃÔŠÅÒ½„lô4)·èòíd2i5š· ý³³½}âwèp îsÎi½¡§§çd#92€¤fš£;!ü‚§iLiXÓ›J½ÖH¾™Â²¬d¥ÊçááþÉ.O|¼¿¿ÿx½¹´FNl–F7ù.^ð*X˜­‹€d2iéVØéiúP,Þ¶“¤^o®ºGÀH±øy¡<íîÌJUoÍf³õæ $¥X´î"p×B`KÖH}£žuP,e?€6ý”‘NÞÅì¬QŒ˜Ö×Üæär“a$Ÿ˜®ï´€eY(Ê.8.^€×¨*ëæÂÅ@&x”Ÿ99‚ c˧ë;­Å-jL„kÞ¯a¡mŸZà:©h€èúõþë˜/÷öö¾®Ìè "U!ŸqqJn Š­ñÈjç…¿ KÜlº¸5 >ôø ?ÄåN®jë¿]aÄÈ$“¯âµÝ#–u©7ÎgÀädå£âêÏ}}‰¢7n®CD„®§+ÿ:eÀ#Àeb¨Êf<ýýòÞ1°ÔdÀÅÎ#q¿MÏ*Å«ýboL€’q‘2çûk,{µg½1>t:Eä_!ëšMxµwz¦5 ZeCkls ™Lf@ÅAÅs¹œóï7@¼‹$1Ήoþ¨4:á<Еæ6g …B+çz@yÙ²eegLÐp­¨’thAØ"£I äµNN(;‚b H§Óc ö8¨ÖؤíÝŸ³0Më:KÔ¸a$÷ÅÖ^ù¡‡øJ>?f„¢0Bär¹8ßurl þÀ¤Ó‰—á~Æu]m Ee„èèèþÜŸ½GOjÛ^+~ª@*|ÓɼÑ4MߺÚ\Áèèè…Üí"Éï-Y²À7~Sî f³©½žtÇkÏçóù‹šr¹\ܶճpÖ9räðÃSõ›~wØŽÝé\]%ÒôØsÃÃÃÞâ¨÷]Að1e+ë¦+ªœÖ€E‹¦ÂgL:èËc­mœ•ZãéP(o#äVI¬¯§l§î 0Më‚®Š ?ù÷‰c·{¿±g gJdn'° Ž›)àƒ†‘þz=9ºƒ#fé~6¸U`_,¦]›H$ŽÔè r¹\¼£«û·8y~“N'¯ªõ³çE£erºY²ž±Æ“¦ kL¥Rm$ßLqðàxOK¬òKø¿Tߤª¬Èf³u/äΤPR7ÍÒ âb6íU;¶yÑ¢…‡Í[N—Ïâ«lp¾«QðbÕ._×××w¬‘œÍ”ÊÞÁv1OÓITÊ[)Xœ $[LÓZ Á&¸ë•ÎÈVÃHl‘j£¹›+–.Wj”ç\ÐüÁÝ´Ýåò©=š‘Ëåâç¯TP«¸@Ð4ܱ>“I=6ý@åòGGS-Uõ ˆk¦³îƒh¿…âß•’ƒªUŽÆ*•“år¹ÚÖÖÖ©”ê&õ,5,Õe¹@W­„^?S.ÿÇfô‡ø‡‰Ò§)¸_€KÂÊYc„|'“Nüt&CÞ‹°ÿ2£™¦u5øp˜¹äAn%«…Y£ÙLÎ4Í¥m-!X<Ã4ã¼ ÀÙTêw3ù?Àt˜•©ìÁÑÑT¬ÂU \ È€YtƒèÂé›c ¤¤€!‡4r_:>0WÊqç1yüoâ?!{ ) ¾IEND®B`‚Ò‰PNG  IHDR szzô pHYsMMgŒà„IDATX…í—ÍJ#A…¿ÓºÕ…2ú&é Š/a>„`Vbæ&‚ #¸Ü Øà"/0 ¦}$kûÌ® Il1vtÀ>«âÖ¥ÎW?÷Âg—Fõ3¯e¸¨ ¾”ab¸Åô#Ôé7uñ,@œx|0 ¬$eXûiS‡cqâuðà«=лÜÖß2\¿þöR6G¹ ,ÊÚ'1’lïK«•6uZ†qP¾‘ÓZb ÿÊpø ³D z W¦ùˆzq ƒ+ëØ'éjKwùpe à£TT0ÿRB|æäŸÀÂ+×`ýH›:)J*<øÜ«ÈÇS˜, wãs¯N ð*H7uµ ¦X{€ÕJ7u]”ôâÈï°ðߢÿû *€ à] ·ðX@Îʬ–x9ÞŒ`úÙYÀãÚ6i ?¢uŒ¿!wk‰ôžÔpoR¾ó†ðE¨æ&5&f÷6žoL‚Bk–—Î+£óSêÆ&ÔšUúñ䄆ê WùIEND®B`‚c‰PNG  IHDR@@ªiqÞ pHYsMMgŒàIDATxœí™_HSaÆŸïœÍé +’¼RŒ®ºÚ™V&D¡7aÑUFâQk³$è"hu™m‘szI&ÙEeåf”Aöç¢4DK4ûûvc1·cCðûŽÐ÷»Ûûžç{λwg€D"‘H$‰D"‘H$’ÿ Æ[@Ó½½Žc81ìw÷ðÖ\ \ØS×ûË6N­ÑÞP e§îJPx¾ù”}Á’^cŒ=ÑšÚJyê®2ªÛbáõÏ»ÎÏóÖÏ× €’ESkÞ<îúÙàn ¯ïHBÍA¾QOû¾!Á[?B®À+Ÿ{.©ª[zšî}&ÂÃrÁ‘›§ÆhŸA«Ò¡û®ˆò‘Ž*Rl"4ðy‹³z@Mj»ŠÕãÁG£"ý,j‹ÇÙèë!¢ã †Š ßýZ¤SM÷}(cHXYñ›® Q>L X~G°Úãö¡k­a£Þjcê}˜È–cT-XrqL ÐcDÑMF=M÷ýáÁôM,87 FÛ Zùšî½Ã[ßô èoyÐAƒÖQÞÚk" RÆÌÐ]TÔ·ofŒú3 /xk›€£¡ÓžP•JRëŒÑ¥ ß]É[ßÔjk戴Ez”§Ö ¸5ìw{Dx05€O…“WH+¬+ši˜á’´Ú˜¶ :u_ ÚSk¼Í…«D>)2%€òÆë‡’Äî¥éµ²øŽ!ë7‘^„àhh¯`L—RžO*ÉÝ#gÞ‰öcø¼ŽZS[)HyZrø8 ép ?< ðKpçɶB$-ý ,Ýýô`çéÇ¢|¤#dÊ\^[<Šû¶¥ÖpyØïîáa9øO€Ç£DÑM@ÕÒÝît]䮟î8&7^Àâƒ)<Íaõ¢îõÿ‚ë] ÌåµD1 ÷o‘؇ˆªVŽv4 ù½Ÿ ®±%Äþ¼fÀ”E‰×¬•ÜôêLC2±ÿ¥ÿìžš‰D"‘H$‰D"‘H$Ùø Y›Ú ñ"OIEND®B`‚.‰PNG  IHDR szzô pHYsMMgŒààIDATX…å–½kQÅgF ñ«…ÍìÁ.…¥•e%…V v6Bþ±{KA+-l‚…¦³²ÙNÈ:;»°Xi‚vŽÅ¬µûf z«yüs~÷¾ûÞøßC±˲<=­ØûlÞ;õ»ïÅ4ŸL&Çv÷Þ'æÍ-@¿ß?²·wð¸2/‰an;]\<ùܰŠ4·5€m ‡åãõU (ÇŒî*DP Z ŠÑ:ö=Û€+fÞ  (F7…$R 6o Pã5ãgõH•dÞ`k8¼dªW@Z¶}€Á`t!±6€£` ‚:¾@Y–ç•ø-ppèvkP–ewZ± œ™uzs˜ó(žVŒê'ãH™ÿŠù–À|Ši ¦\}¡H÷G@–eã4ñ*ðÕõ= bn¡,Ë>»Òe`Ž$²¼¼ô±’×€]„¡Áçz½÷"¹LRËߺFäywCè6À¬ !—0Ï—^Öl7†hµ†yž=Fz(i¦å`ˆÖM”gÝûXOë‘;¬­$9Ï»w…^Öã0Í({YÒtgçÛ-Á»Ð¹ÑN´•••ý……Ã×þ @§Óù‘Š«¶·Áßcjÿ»ñ¶ ¡ažÍKIEND®B`‚ĉPNG  IHDR@@ªiqÞ pHYsMMgŒàvIDATxœíÐÁ ‚@ÐY‹06b#¶ÀÁp²/„F¨‚BˆM¬WCö $òÞqòó'Π¬ƒûóýH2Ôäz@Ÿ--Iíæñ5ý†—Æá?>Ÿ$·¤ ë°5À©´èKòÙ½Éö–¤öG—ö÷¦p„™IEND®B`‚‰PNG  IHDR@@ªiqÞ pHYsMMgŒà¶IDATxœíÙA Ã@ÅÐiNáQ áR›Âx•Ö&0–õo3¹îõ^÷z¥Ã!ÿКhM´€¦Z@S- )€Ð@ h  4КhM´€¦Z@S- )€Ðlà#ë×øL ˜S ÌÌ<߃-qû@ h  4КhM´€¦Z@S- )€Ð@ h  4КhM´€¦Z@³}€g DøîëLIEND®B`‚ЉPNG  IHDR@@ªiqÞ pHYsMMgŒà‰PNG  IHDR szzô pHYsMMgŒàðIDATX…íÖ?haÇñïó^b¥‚Š]DœüÌàà(]ê$ÁEÑEÒÚ4ÁQJptP’Zk@u‹[ÅÑ¡ mÄÅÉ¡“(HK‡Š5Éýî²8Ý]ŽË ÜvÏûûðÜûÞüëeQn:]®o€Y¾Û9²üèÆzšñ¾½ =\®uüò]ÃôëÌÈèÈËc¥¹Ã Ý7ö¤Tzáeø`.®}k€"í¡Ô¾$¦Š“õZ¦€a’á»U,7f3˜P€@÷ŠåÆ¥Lð×$ÐãÂäüD¦&à9ùK…©úÙL?Dìt>­âµ»'³„Éì–ó^®ß?š-0Sÿ±ßz½·§*ƒ™‚ N†Áa×Ñ«!Dˆ81€90k½¼ŠÒ‘K+Zà,ø6|—çû8?ý5J_ZpüÜl:o/L‰Ü˜F8Aø/ßqþýÃê§8̓> ¯žo®Ô~PywÄÉúý†]m/VZIÖI:3™˜]iÎ,Vç 0p {¾z`}&|÷T1÷€aðfkãÇš5Ðpˆ¿–·n_ø¼TûF8Dœ€`ÓÀòÝîÄÊÓ›[i…ÿ/€?”¢cÅû‰IEND®B`‚.‰PNG  IHDR@@ªiqÞ pHYsMMgŒààIDATxœí˜Mh\UÇç½2©š¸±”´RÄÖ`2º(4~„ˆR”JQWÍ¢é¤i&ê' V°]¨ÍÔ|Œ”D„‚›b]i©L®æM nüÀEi5 ›H!¦SçÉhæ½—Nfæ½IJïoyÏ»çþϹïÞ{îƒÁ`0 ƒÁ`0 ý…xâ]©PÁVpt=„……‚X¢ùÐI÷®´oòõP°¬:錌⠫`ƒ% ¸ë¬T+rWÿöA(¢¨÷¶ûö€Ö®Sm"rhò|® n$ ÃÃÂ×<"ûœÑÞŒ÷ãÀ´tŸjµ\ùØRbPaC&AÁL³®¥Ó#}¹ >«& ÞýÙã¸ÖE`‡§›îÆ9 d9xõÆsËmwFú]½gž>òù-.;=]7HÀÂü/bÛíÙážkwê]öÈ÷\SÍïœR‹Š¢vùF‡ŠÚ¾à…¬j~O¹àaÇ`.}l¶áöÍg~ð PìõÈ‚¸Ž}©!óÙ\úØìZüT¤}ïÁñÍó g}Å/H]©Óñ©¨4yçnÄxã÷TòÖZ}UTe&:›šg€žñÚ±´Â„VÃRi¼žyôïæ•U V‰NÞ Ùý¡X×û zÒI'ߥŠ?°¦‹'Ro£z"À«‹†‚f^äg´÷dµ^íZ$Í8“?nkíøSD^¢4™¢„¸„ à]CÎXïéÚ\‡@<‘ÚêY æ1-× 5TÚæ^ÏŽ%ÏÕè;¼IŠz‹o€V¶ëÒ–]e$¨ÀYôåìXߥê|zFÃI‘xbhÊwÀCžaª¸D?'®ûBö‹þl-:KF ËQ‘–#Ÿ>!…M}Øcª`9ÿ‡kÚ§‡ßü9™ÿ¦³"Oõœ~ÄþǽîŽÂa¾‘IEND®B`‚8‰PNG  IHDR@@ªiqÞ pHYsMMgŒàêIDATxœí˜ÁkAÅß·iÚ´E*¢õL6 OÞŠª/*‚"ìAéÅ› ½‰ ¤xD¡¢õ ¼)‚-±ÍN6´Mšd?Ob“lM ™Ù¾ßñ}°ïÍÛYfX@AAA¡Çxž?íiŸ=ís±TšŒ:O;dòáÅb1ANßj‹Èt$“IÍšôÝ ŽÉ‡÷uˆÄ¯´Ö9“¾[Áh£££¿Ât†³07·´Ã¤÷f1Z¸éTç.¬­0³qÿn@DÍúZm$l¦Kå¦iÿnXyù|~%æ 6óté am *¥>èhç„&¼’ÛVŽv¬~ƒ®›z Æ•Žãªçùçmfù‹Ñ{ÀFh]zÀ  ízà`<§Ô;›Y")<í/è¸8ÄÉt:ýÅVŽÈ Oû¦Ç )¥VÃf½&Òsxyék˜Þ ð›™­¼œHwT*•ÝõF°2ªf\µÓ´ä7±d2¹ìïh]~dÚ?ò Ndâ“í:ƒÏšöÞš±ù(l·E‹‹‹{É fBFoM{G^@¥RŠÅâOd[D73®š0íiÌk4‚i´îº*yÃF†H ðJå; œhÏÝtê2…^’zMdhíO0Õ"Þ×j‰3DÔ°•#’‹çù§@xÜæ_Ž98¨”ªØÌb½€¢ïS€Yƒë䟃C9¥>ØÎcõÐZ稉gh]|ƒœŽbñ€Å|ßßÅpf@ØÓ2`¾äºî [9Ú ýcÛk …Â@#Àöµ ˆne\uÏF†0¾˜Ù‰Ç÷ 8Ü6zèªäuÓþÝ0^€Öþ5εˆ„×õµÚE[gýÿ0z  …xâ€Ä?•?xÂuÝï&½7‹ÑP­VõuÒ7šÇ·ËâÃŒÕÁ˜° `ž:–Íf=“ž‚ ‚ ‚ ‚ ÝøjeÜE1PÄ}IEND®B`‚͉PNG  IHDR@@ªiqÞ pHYsMMgŒàIDATxœíÙQ €0ÁÿÊPAƒ±$1p/›þu&t¯ç½×ó–ÎòøP¨ P¨ P¨ P¨ P¨ P¨ P¨ P¨ P¨ P¨ P¨ P¨ P¨ P¨mà(×_ã3ñ 8f®ò>lâœm ô”;KIEND®B`‚-‰PNG  IHDR szzô pHYsMMgŒàßIDATX…å–±kQ‡¿ß»C‚‚G,l¬í-­,$·%ÍÅÕdÓùÄBìETâ!x•®wM°ð6XXYŠ­ ÚØè Aro,VÅJ÷í>"èt;3ßüvæ1ð¿›b'\L³#Þë-`ÅèþüŸ¾oÇ,žôû‡üŽžª1.Vñ4MØÎÜ8 MÓÖ'ßy–„ÆÆÐgß¹-8à÷`á\vXÃ'ß1*Z£!\è]\—qÉcT(Ðíe}‰›?‹^¼6@²œ-!=(½z×HzÙ)C–aV·óZÝôÒ “Ûæ03¡ ‰oÐ]¾|oÏÀXùß›[%€ÓéÚ1˜ÀQÀ¸ë¿³JkØò{ïq£‡Š ^Cà `ϵπ½S@LT€çùÝÐN€€,"DåDÅèÞœAÛ*™(AIŠ|ã•Ì/»€ÌšC'˜Œ/„­3 uµ:˜Œ›˜](=s4¸-kKXŒCÐú<¥ûPŒ6n™ì€yœj(Ñxˆ¶ž ® î 0èDŒU²Ž›^1x„€À͈²ËyžÏæÝt4 ö¢åyþUw{ÀË¿0¿8gg‘¶iÌÜÿ®}hG• ˆIàIEND®B`‚h‰PNG  IHDR szzô pHYsMMgŒàIDATX…íÁ‚ ÿ¯nH@ï G ˆIEND®B`‚¦‰PNG  IHDR szzô pHYsMMgŒàXIDATX…íÒ1/CQÆñÿ9‰Bº›‰°‰E´iÐDµµÕÀÀfÀ¬µ4bh™­DBH7ÂÖE'ƒDÊäÞûZ8Ú.ç7?yÎó&,˲¬ÿN™¡¸³ +_R…ƒôÝoJ#3+½^ÀÍ/Åüö4 ßeµy¡´ƒ¢õe8挛òá¤3ì5¹%`¤Ó”7¨ê¶ŠH›hŽ'ÎüwÙ‰ÙÅ)ñ9GèÔÙ»çEøáz€€i@å¾ä¦’Ñ|åéµSë’òÃõùç\(¾´¢Y Èvè—¹£ýÝ7S¿ñ|}ÄY¶°óÜ¥—§ƒAïê¶²‰Rk¢dã4—^Çpy]ÂñŨ ö€ U`p²\ȧ3µôÕ< _}tT•òÅ\¦PkW]ÂI§O|ÊÀ£öýèÉAæ¦Þ®F(8²,Ëú>w…k0Ô ågIEND®B`‚Œ‰PNG  IHDR szzô pHYsMMgŒà>IDATX…íÒ¿+…QÆñïóÊbR¬ü½÷–Í$?Š›˜X)€Í$“É,R/)%‹"ñŠ¢Ìe±Ð¤î½ÅëÞMÎg<===§s ‚ øï”Èo•—-u•ÑÄEA7¿)Ío»Íö:â)Ö ’¿ËFYeFM@g>‰÷fåsÛî²}ôP¡9+Ÿ9 *kØ…÷ãÄ“ßeã-`­ÀáK¤¾Ÿnÿ«§czN¯5^ê…×r‰ç°?=_œxZò.Ð^/–Ôw5¢Ç¬þÌ?ðQ.ñ xém¸WKÑÔå=帣² 4 €=Ÿ¢¹¬›×4 ¿ã!W¼4Lj"¦(¦Ò‚Vªé«z@¼énEÞZÞŠH£éˆªíªi@¼ëv•| Ü t]kWíl}ýŒAÁŸó +rôï‚IEND®B`‚"‰PNG  IHDR szzô pHYsMMgŒàÔIDATX…í”±kQÇ?ß„  qÐÁI‰J%¸:yµHéP Ž‚mfÿAÝü ’¸V¼¡uðzCuí-. .µd±¡æ~—3§mc’»ÜÞÝû¾ï÷û~ï÷2dÈအdà]Ÿûþ“ïFجpý(r»Ò÷)¯þ@ÑiàuŠÝýx„¢ßÞÆ«ž×Ÿ¿±®Ã+@€áHÑv«sñéxßXO¦siî§ÕÕ­r©øô‡åÏJ”0´·˜€Ù“‚kß\šŸßL3r/iµZÝr©ø¬cùãˆKŒ[ÅæÄ ¹vÕ÷ýŸÛhƒ$¼™¹ûˆ»½×̆uOÌAÂfí°ãÚmHcíÃÛ•âùò0Ù «Ý„˜á$’ß ›õ‡ƒøôB¼)ž»¸†4 ¸^_ìÂÌœ$wÿ­°Y¯ýKèó¸Q23ÈW!JϧÌ7%U–jχÑ©Á&ff/›ÜØQ33)¾ß}sµl*X¨¿Vsä÷*Õ D' L‰†ø†4úµw£èuÅ®VfÏD!èTüÅ>;‡ø£jý›ñ¦oŸäûzÏV÷Z¸øøë¸Z2dÈp ø¤ñ¤ŽDÆQ^IEND®B`‚׉PNG  IHDR@@ªiqÞ pHYsMMgŒà‰IDATxœí[mpTÕ~Þsw7°$|dÔÑ e0Tf`³PÔF°v¤Áb»Ví¨€$!@V`¬œÑKZ©õ+&»` *ÓÓ¦8€LkÃP !íP:~€”Iˆ•Ï`$ÙÝóöGîÇîfor7†6Ï¿ûœç¼ûÜwÏ={î9ïÀþŸA}ñ!ç¯R2¨·€ic˜1œ€¡€Î8æc pPÄhÏþÚ²#éö–¶x‹ƒ7 ÂfÌaBÃað!ycãºÇñ­&a{˜|%Á»ô$€éöÆÆ‡ªìz£imIÄ® ¶%À[\5ˆ^P`WÌx`à(˜W4­ ¼fLjèu¦Ì¯¼:ªˆ— øE™$à=×K)±OD§¤âh‹~‹)ƒ¢Y`Ç0bäI¦±ž  Nb|¤(m\·ø£ÞøïU 'ƒ±@nœæ6Û‰°±sÿêÅ'­Äžº´bpçWŽ; ˜ ¬ŸqÀ¼pMàÖÝw¡Ç ð‡"â 2L¦˜ƒŠówÿ¬^xº§ñµÈ÷«.÷ˆì""z†Q&ÓÊpî—Ï ¼\Zm=ª*¼'F>Kàå†I„ "Šò†ÚÀqËqSÀmóžÏêp¸—ø ™†æ­íŠòðÁW·ÓZTU´ŒÜ ât<£³Ãkû-Åë!¦Ì^S°æ ÷Å…i ¡À¹Tc +ìkÍ^a¼yö1b}uóÐP8îtG Ølhš(;y³ßÿ¦’j¬”…¾’ÐÏi9"Ú4¤#Ë¿w}é™TãØ…ãûÞ‰¶4mß2Úרà.\Í4öÌ ¶AÍM;ÿœJœ”É%¡ Þ`Ðe–ß×IÇêÌ* ŠƒK@xÅ@?® ¼Ö]ßnÉ‹VybÀVènû3;†õ‡›€ðÚ²*€o ×y‹WMê®o· ù¯¹D0šcNš]¿qîëVÓâ3.*áïÒE$×MSUG²žIà-®¼„yŠ¥‚Ÿ~°ª¬¹7vÓOB%*ï𥆞ÔÖâY˜¬_’0‰ç Üæ÷«ûzn3½h¨]Ò ÆJ-G„§'<üâD}&ÀW*p»†Š@ÄÔÞÛL/2;³Ö8¦¡F:Ý®’Dú„ `_ê Ô„«—}Ök‡iFýƹˆ`ø¢Ä²Dkƒ¸ ð-\`††Š(R>k—Ét㆓9¯3ðñ7×¾æSOë]ñ´q cч Y$ðnCí’VÛ¦ uu÷LjP§åó£ñ´q@ ™æOv™ë+°”FÏ?Ž÷˜pqƼUËI޽m¯½ô£)÷t#í¨vÄsÂkÔ™áv}€ë2CÿhZ»ä˜Q×ïQ^.Ö}qÌæ}Ê8€ø®Øc«±>³Þ;1Æ5¦0óͺkÈš+‚¡÷N¸Ù¤1D¸AG0õûßþ„p:ŒÞóŒó di¯IPŸ¿ëÛ…!ín£÷,£Æ<°^$˜-í±õ'ÔoœÓ ª¡\ù~Õ¥ÕÄ[è6I¢Bö‹wþt!^ÚtVLÃæJÁ´93h÷:Ö•wj5æIÐí¨²äáé±—~|í8;Ì@µ5æIô¹öš„yæ¼RÀŠcŒžÀQ£&Î$ȇ´—|Çf_} ©ó΄ÃF9$t‡ÌºM‘+ ƒw¦SÚÚ @{þ>iÊü൶»K7TUp–b…ë2SºÎÖh¯–‹*tQ×ßáká…þDùÜÐQ'ÃF]ü-1Ö¿Eñ,[Ýõ„Ðyf`g}yyÔ$‹×7æ¢Í.53îœ87x•ÝÓU üLKxS `€mŠ »¬&#߯ºÜÃ=…¤`&KÜ B¼ex ÒpMÙúØ`C¹¼oáêë8«p_Y„Ýø+ýK0ŠrêÓy·#‘h–dǰ(#˩ݮ¿Ô$rÞ(‰K߯~¼©7þmû/( ý‚ŸÞɰ«.ù×y'sj{2ä°w!£ªbrkö½´ Ÿ­±AŸeEfçÐõvÖ(¦m%ç+Z5^ žCàYÆö0Ì 0¶ÉM9§ÿÖ“ÿt‡>YÊ^œ'¦ƒy<@ã@È0 ]ϸƒAg‰qÄÿ&Âa)qØAØÝPSv¨¿”ã`øßÄ3AÇÎavêIEND®B`‚—‰PNG  IHDR szzô pHYsMMgŒàIIDATX…íÎÁ À0 AÉIßqMnHÕì"ΠÏÎÿŽ5DÖŒ¬©| e|@´¼ê›}‘ÕàûYÛåô;+š @Mà‰IEND®B`‚‡‰PNG  IHDR szzô pHYsMMgŒà9IDATX…í׿NÂPÇñï¹!0Z|†¾@_€ŠL&¨#˜°ÖYd‡•DWFe4"OÐè;°PGá8”*]lãàý77ý}Ò»œÿ=²}à_^Ÿ Ü€z€“QO *tÇÏoþU³#*·À ˆP>2©Ž(¡tÞ_žîw•z³fŒ¼ ŒY£a’Iù*Õz»ŒY |EjéŸ0écLÌò( û–…04øêý¾¢åQ¾@"o'³7ÿ)ª1Âñ>ÀŸÄ,À,À,À,`¯¦×|#â L÷$Üj½]Ϋ;ù¶ºá@¡ ”0‹AˆÊyë³EEzéùÆbrzÑz@¸æÉôªq&í"¨ .&i’ÕLo}zýU”)B¨Ho{5³ùÃ%f/å+=IEND®B`‚õ‰PNG  IHDR@@ªiqÞ pHYsMMgŒà§IDATxœí›ÏoUÇ¿g¦…ÂÊç¦!XX6M0ñ¥/•M›¨–¢‚‰mb[›(Šý 4±Ñmñ‘²©&m <-£uƒ 𲍋Ö-й_}SfæÍtæ1w~Ô¾Oòsîyïž{Þ÷ܹ3¹hРÁNF¢8uϰim'ÔY ì`wÒÁÕ'<"8gÀ¸ÚRÀÍÙYû^hJ“| à7^ÕizÌòa¥_~ÞÊÉl!¥s‚ƒ§±ýí§K782ðL@ç$DøY2±¥ùyé'|Ôì›™ªì§=æŠ ÁÄ­CkX¸þ®XZÉ™k4¶à ,r@Áí!oû•CMºgØ´ºÂßᔽàr®Ò+KÚ#O€R™­Pñ¦Ã<¿§ G¼cM ¬­àÜ5¿lR>Ø.ƒ€J¯,ÁsVæöêØ\Ô$@Au^Sdøn¿ü£?Ìd©ôÊ’P†6ïØŸ¤è2˜¸¥=º”P͘r[äu¯OMìw^ZÂîÀÒÂ'ö^¸Wxy›íëÁ»ßê5x!´ChÊ:€(t•Ù¦”åÔ†…㦣wOÊßq;÷ (ó°²x”óön|ä‚EÞé*³-îïç:Ý3l‚ÁìóiÞ§,õIÜ>r€Ç˸ æÖõ9·ÜÎÅIv8’t?¹T@÷ ›Lð;Í[ù x=n_¹L@¸ôOiWâö•»ˆ,}‘‘JŸÌÇí/W ˆ*}æö¼„¯tô™«D•¾²äb”žQÈM Ô#ýßNËŸºúÍ…²¾¼qƒ¯XTC› á„a£÷zåA”ï?^Æ%‘t¥o[]e¶Yä@.À^«SÎ+‹·Kã<öýâ$;DÒ—¾Mì(¥†°V‡Éºg¨²,¥oØ|DõkCqõ_|ԜŬï%ùI)N²ÃkÎZú6Àñ‡]¦ð[g)äAú6ñ`_xº¥§ò }›Ø ¨ôÉ<)Ÿ†:VK!/Ò·Ñ2ì}WÜqÛe‚ß›àr }- ˜í‘u ra¥¼àHˆO*Ò·Ñv˜ë—?"•B)IßFëm0b)“¢ôm´& ŽRð#UéÛh_½p)¤,}›DV‚u—BÒ·I$u–B&Ò·IìY r)d$}›D†BK!CéÛ$š€ÙY7LyO¤v“… CÞÏJú6‰?ßë•TrT€)«V˜¢’£Q_™%I*o…+§dÀ;iôU/¹x+œ%~›¤ž8¯Ï\£™^8zñÆîà¿Iê‘óúa ê-|b_ôúø(€s.ƒ…czÃJ㎻-¬¹%×$À€qÕy-äh©ÌVÝÁ%M©ÌV ] 1ïØ6lZ ¸ à/‡©Å±í”„ÍÍÒîãóÕ±¹¨k»¼P†U3¦ò¼]Þx†ãÕþŶËÛtNpðq`D+'å²_Sà:à×~\†È`rQ¥¨ôá‹ ‡¨‡¦¾Ю5´ä‰thªîcsÕ-çòxlÀ"Àûõ›kРÁÎæ?·7é‹ù]VIEND®B`‚>‰PNG  IHDR szzô pHYsMMgŒàðIDATX…å–1hSQ†¿?}­RA1‚...âР]+Tº©SM–´©à&Šâ&´M+؇ƒ`'p)êæ¢ò:HAA¨`¡EÛ䇤N‚ïÞ\"èÙÜóÿß=ïÜs/üï¡Ð‚ƒ¦ò[Qï;0[ŽÇ÷þi}ÒüèØí](z ¶'kN.”ù‘âõ¾¾þ N¸ä({úóûC®¹LïóŸïg10,í*@¡4wTÃd©u `àÒlÕà*€IÎæ ”kcˆiƒT†³¹7@¡4;‚é~û3~æ^…ÒÌIƒE ‡–±·¹3@áâô1#·ìl;uüï"ó$<^ªNÑ`·)€9d¬À`yê`ÓôÌ`?Î2V`Ë¢¿®-…3‡¬=`¬„4uèÍ5† >´a‚]`™^Ì_þaC‚/´Æ]0ˆÌB¯â‰·¤ÍSÀšZ™ N"ɽÉ×"¾ÂY ‰«Ï£@êìY絃$_BvØnJoï.ÏO,U·u̳ýÃ$®Ô·dæÑq%qåšÄ]Jɵ&u@vhõ@xˆ@ÈI3ÈY®×G›_WÏO]sƒM´7õ››?Î^þ€•…+ëQ£qX} ©ýïÆO2” ok ÞIEND®B`‚ ‰PNG  IHDR szzô pHYsMMgŒà¼IDATX…í”=ka…Ÿón„…€h#ƒ›7 þ(´ÑRH+Aòôˆ–6ŠXi¡XZh©½˜ÌìFÖJc0ÈαØ–|íÎú‘f¸3\Î9Üyï ÇŒ†Åz–ú?¶þ¾ÓÓaôi¬þçöc^£b.¨…xßï0˜èý•ÇØC«wsA­áËèt»ÝùÝÝ_Ï —I ù  0¯êõד$ÙÙ7€$Iv¶¶6¯ =¤ {C–´W_C`=ÙÞÞ¼6nÎaâ¶kY¶ñù–m$”œ„@…$õ0ÆÆmI½½}« ‰ô.˜h¨¼$„­½B3“óº<–æÅÍtêÂŽÓ]½¿ýsÇÒ”¸K@°p‹c½{Ú¾)ØäRHQœ™ ò¡jË>Ÿ@û?IS«rI T¦u ‰þ¿ji çšôBµ©‡SÅà#™|§Eˆz£ÚR=Y%Ò€þiÑc‚ôAõcÝ•2–?ÅS Ðêîðw.5sÛ´*©ãIn€ž™@\a¥ùjK7¥ e8`­yby&\#èø¶¦®KÇÐ è#Ÿ ×Ô¶–6¥ˆ`È}T>-^_@Ç&æuelõ Z§ynQWsÛßѺ˜Ê™°Ú<•L pǹ«ôaÌæ)Vñ5O÷vÏØ·ðŒ¢Ä𛀝y¨Ül_ŒÑAæh@¿.ÕE‚OTZ8Z*O€t›'êh¥©ý!eò5€[®¤^›hj_(¬  ïÈ Ô+¡4²6è/{Á¶Å¼ ègïÄ(èíPÙ<õ"¹×øØrüJ"ãàMþévƒÁ’r4€€ã¾¹×Ëí:Ž„–ËÎ †*g¯÷Ú'Ìã }/`/‰¼ K·žÀ'½SæAvž!' œ;/ØãÔÊ~ž%šÉ. ãÙñOôz¼geŠçb*Ý,O{ßaœw¯Ôy6¶þP—@?y§Ïÿ‘†;Û{øSІg€àKþŒµÜµTãÉTa Å KÚÂïwwöñë”±$¯ÈròÀyÖÛ5.¥Ž'é @ÊÑÉûÛ5.¤Œet3€„gÍä£K5~”,ŽÉf€dÝæ†<Ô®ñÍT1øHcaèlõz©½CÿL&…îWzw¹fžŒÑÜ J\úÕ¾|Ä]µy$‡ä¸Júš›ÅõgyßâìÅÒ”h»€s¾¾Â:îY¬³KóbHµ |;N¶wó×DzL ~èõ¸óË:N 50!˜ð=Áÿ…க)~J'4Áj]3³âd§ÁoBiÄ äøë™!Kîí4Ø 8~Bþ4u@ €ø\§ÆÏ‚=bĈ‘ønþN~àÆ%SIEND®B`‚ g6ostinato.org®ÃÃthemes c¦kqds-darkƒrc âhgtransparent.pngy£Çline_horizontal_pressed.png NW§line_horizontal.png¶µ§arrow_left_disabled@2x.pngø§branch_more_focus.png5À.keep .©Garrow_down_disabled@2x.png!ñ7gtoolbar_move_vertical_pressed.png ôçwindow_close_pressed@2x.png x¶‡arrow_up_focus.png+ÒºÇtoolbar_separator_horizontal_pressed@2x.pngÏgwindow_close_pressed.png!§Çbranch_open_disabled@2x.png.Ógline_horizontal_focus@2x.pngu̇checkbox_unchecked.pngZ´§radio_checked_focus@2x.png÷checkbox_checked_pressed@2x.pngœ¤§base_icon_disabled.png¬9§toolbar_move_horizontal.png~oçwindow_close_disabled.png³L'branch_line@2x.pngМgtoolbar_move_vertical@2x.pngÇ%Garrow_right_pressed@2x.png `Ž'arrow_up_disabled.png ó«'checkbox_checked@2x.pngud‡toolbar_move_vertical_focus.pngàJradio_unchecked_disabled.png‡®gcheckbox_indeterminate.png¹gwindow_close_disabled@2x.png íGwindow_grip_pressed@2x.png) Ógtoolbar_separator_horizontal_focus@2x.png(§branch_closed_disabled.pngÖ%Çbranch_line.png"Š'checkbox_indeterminate_pressed.png“‡transparent_focus.png NZÇtoolbar_separator_horizontal.pngD Gbranch_end_focus.png 'xcheckbox_unchecked@2x.png  Gwindow_undock_disabled.pngµwgradio_checked_pressed@2x.png ë_gbranch_end_focus@2x.png!p0'toolbar_move_horizontal_focus.png -arrow_left_focus.pngüIçarrow_left@2x.png' ëågtoolbar_separator_vertical_focus@2x.pngH'window_grip_focus@2x.png /Ú‡window_undock_focus.pngUbwindow_close.png”9gline_horizontal@2x.png%~‡toolbar_move_vertical_disabled@2x.png ÕZgline_vertical_focus@2x.png¢³'arrow_right@2x.png¬ãÇbase_icon_pressed.png xçbranch_line_focus.png Jgwindow_grip_focus.png ý¢arrow_right_disabled@2x.pngˆ%branch_more_pressed@2x.png"Aî‡toolbar_move_vertical_disabled.png Ðzçarrow_up.png 3”'arrow_left_pressed@2x.pngìÑÇcheckbox_checked.png$ 'toolbar_move_horizontal_disabled.png6Gline_vertical_pressed.pngÒ£'base_icon_pressed@2x.png$” Gtoolbar_separator_vertical_focus.png5ûbranch_end_pressed.png#m‡checkbox_indeterminate_focus@2x.pngó&'window_minimize_disabled.pngO)Çwindow_undock_pressed.png. radio_checked@2x.png'=×Çtoolbar_move_horizontal_disabled@2x.png5ó§branch_closed_disabled@2x.png*pgline_vertical.png†N'branch_line_disabled.png–q‡toolbar_separator_vertical.pngƒþ'branch_more_disabled.pngu­'toolbar_move_horizontal@2x.pngº§window_undock_disabled@2x.png ÖGarrow_down_pressed.png, ¤ZGtoolbar_separator_horizontal_disabled@2x.png ¦Çbranch_more_disabled@2x.pngl£§line_vertical_focus.png#C¹gtoolbar_separator_horizontal@2x.pngþ2Garrow_left_focus@2x.png×4gtransparent_focus@2x.pngm›'branch_open_focus@2x.png*Ä÷toolbar_separator_vertical_disabled@2x.png‹Çarrow_down_focus@2x.pngø2Çarrow_up_pressed@2x.png Ú§arrow_up_disabled@2x.png checkbox_checked_disabled.png þvGcheckbox_checked_disabled@2x.pngéýçline_vertical@2x.pngÏ\Çwindow_close@2x.png¥±'arrow_right_disabled.pngµGbranch_open@2x.png HP‡checkbox_unchecked_focus@2x.png4ðÇline_vertical_disabled@2x.png%çbranch_more.png5¶gtransparent_pressed@2x.png&“çtoolbar_move_vertical.png $ìline_horizontal_focus.png8§window_minimize_focus.png fcheckbox_indeterminate@2x.png ^°branch_end_pressed@2x.png #!garrow_up_pressed.png,$Çarrow_right.png Ø'radio_checked_disabled@2x.png —Gbranch_open_pressed.png Ȥ§line_horizontal_disabled.png z‡gwindow_close_focus.png¼Ãgradio_checked_disabled.png±”'branch_more@2x.pngS%§branch_open.png"šgcheckbox_unchecked_disabled@2x.pngzÓgbase_icon_focus@2x.pngOËçcheckbox_unchecked_pressed.png˜'window_close_focus@2x.pngP+çwindow_minimize_pressed@2x.png Ú0§branch_closed.png †ùçarrow_up_focus@2x.pngå§radio_checked_pressed.pngd÷branch_end_disabled@2x.pngOj§radio_unchecked_disabled@2x.png=Çradio_unchecked_focus@2x.png>9gbase_icon@2x.png$˜ˆ‡toolbar_move_vertical_pressed@2x.png&5ð§toolbar_separator_vertical_pressed.png)g¤§toolbar_separator_horizontal_disabled.png ¦gbranch_line_disabled@2x.png·¡Gbranch_closed_pressed.pngÎúgbranch_closed_focus@2x.png ø'window_minimize@2x.pngvgwindow_minimize.png=§transparent_pressed.png§ZGwindow_grip@2x.png%lGçcheckbox_indeterminate_pressed@2x.png $9Gbase_icon_focus.pnguQ‡checkbox_checked_pressed.pngégarrow_down_focus.png ålradio_checked.png (§branch_end_disabled.png#¥‘Gtoolbar_move_horizontal_pressed.png Yn‡radio_unchecked_focus.png Fr‡window_undock_focus@2x.png û-'radio_unchecked_pressed.png' ’§toolbar_separator_vertical_disabled.png ËÌçwindow_undock.png®bçwindow_grip_disabled.pngšåbranch_open_pressed@2x.png›Gradio_checked_focus.pngàçcheckbox_checked_focus.pngs_Gwindow_undock_pressed@2x.png 3-§line_vertical_disabled.png?Úgcheckbox_unchecked_focus.png l"branch_closed_focus.png–§arrow_down@2x.png"§™Gtoolbar_move_vertical_focus@2x.png&$öçtoolbar_separator_horizontal_focus.pngÁ#gwindow_grip_disabled@2x.pngÈ–çradio_unchecked.png! Q_Gcheckbox_unchecked_pressed@2x.pngo‡window_grip.png!ö¢gtoolbar_separator_vertical@2x.png Çarrow_right_focus.png$íúçtoolbar_move_horizontal_focus@2x.pngÔ‡transparent_disabled@2x.png ¥Å'base_icon_disabled@2x.png ßϧwindow_grip_pressed.png r=base_icon.pnghÊçtransparent@2x.png@-§arrow_up@2x.png[°gline_vertical_pressed@2x.png ¤o§line_horizontal_disabled@2x.pngHÇwindow_minimize_disabled@2x.png#òGcheckbox_indeterminate_disabled.pngQçwindow_minimize_focus@2x.png nwindow_undock@2x.png&Kˆçcheckbox_indeterminate_disabled@2x.png8%branch_line_pressed@2x.png`9çbranch_line_focus@2x.png Mgbranch_line_pressed.png êçwindow_minimize_pressed.png€O§radio_unchecked@2x.png ×§checkbox_indeterminate_focus.png û Gbranch_closed_pressed@2x.pngúá'arrow_left.png) ä°toolbar_separator_vertical_pressed@2x.pnge‰çbranch_more_focus@2x.png Ò¹‡line_horizontal_pressed@2x.png gbranch_more_pressed.pngŸr‡arrow_right_pressed.png Šçarrow_down_pressed@2x.png(cSÇtoolbar_separator_horizontal_pressed.pngä-çbranch_closed@2x.pngöÿgbranch_end@2x.pngE‡arrow_left_disabled.png1C§transparent_disabled.png‹ìçbranch_open_disabled.pngb'branch_open_focus.pngÿüarrow_down_disabled.png< Çradio_unchecked_pressed@2x.png Nq‡branch_end.png ®'Gcheckbox_unchecked_disabled.pngáöÇcheckbox_checked_focus@2x.png­Çarrow_left_pressed.png æarrow_down.png&û"toolbar_move_horizontal_pressed@2x.pngCÆçarrow_right_focus@2x.png0FÑàK~•>ðbJ8~•>œ®¤~•>æF…~•<´,û]~•>PZL~•>¾d¬…~•>-m~•>Ü ¾xû~•>&NÌH~•>‚É^~•>æ$œ²n~•>#P¦¤~•>ú#ì¨~•>Né~•>lV²~•>Æ&«²~•>*î'®Õ¹~•>&xÌÁ~•>´ Ð,~•>øJj~•>Ü(HX~•>Ò8~•>´êD?~•>ð è"~•>æ$À´$~•>ú,þÑ~•>¾<b<~•>Æ(lÞ^~•>È Tt"~•>œB(~•>ð"v~•>ÒÜd~•>¨%†¸>~•>ÚYE~•>¶Éç~•>ú ä{É~•>Ü+(ô†~•>²ê€|~•>ð!`Žô~•>>[~•>ú¶([~•>Ò ž˜â~•>ð#š§{~•>úø1~•>æ-DŸ~•>ðbAW~•>¾">›q~•>'Ò@~•>%´è~•>²š+~•>Üú-†~•>´  ~*~•>¨L‹~•>È 6H~•>ưñª~•>ȰdÓ~•>¾0ײ~•>´"˜Þ~•>Òš…~•> 2U~•>¾^½Ñ~•>ä‚ùØ~•>Sd~•>´ bl~•>ð4À~•>ðiL~•>ä “~•>´Z¦å~•>D`>~•>ú+êû•~•>¼%8· ~•>ð.^Û~•>Ÿ.,™~•>¨^ù.~•>È.Ò!~•>²v“~•>È(¦ßQ~•>È,àé~•>Ð*|òK~•>ȦmL~•>ÚlÏç~•>ЦC°~•>ú|j„~•>vÁQ~•>`xã~•>¨'bÓ”~•>ÒÝ~•>ÈHj~•>êh~•>Ú¨ ~•>Ú ì™~•>æ(Úo~•>Ò Y±~•>ú5ƒ~•>&œе~•>æþ¿r~•>Ȇ¾Z~•>Æ –Ž"~•>È,ª?~•>Èzï…~•>¨,2r~•>ðxc0~•>¾ p–T~•>Ò |V~•>øÐq~•>Ð Ý~•>ä"¶Ÿ‰~•>Üäa¡~•>úW;~•>Ú#(¢Ÿ~•>Ÿð`S~•>î$(«~•>î$š ~•>¼-ì ~•>Ò*ïQ~•>¨ Ønã~•>¨šà ~•>ÒFÈS~•>ÈV‹~•>¨ Šw~•>¤ÿî~•>¾ÚSà~•>ú¢ö~•>Ü!8Œë~•>)~ê~•>Ò*$ñƒ~•> *ˆ ~•> Pñ~•>Ø N~•>¶Äñ~•>¨'ì× ~•>~Zå~•>Ü$T¯ ~•>Üàs~•> ¶^7~•>-¨~•>Ü.~•>æŽzò~•>æ ¨ˆX~•>ðܾ~•>´ ªl¸~•>¨ n„ò~•>ÚôÛ~•>ܰè~•>Ü 6~•>ð&.Ç\~•>´ ú‡~•>È+\öÈ~•>¨*ôóá~•>È(ÜàC~•>È ¼~•>´Ò6Ø~•>¼)àÎ~•>r>Å~•>Òdü/~•>Òöq3~•>´ôúâ~•>Ü 8‘Í~•>¨B<œ~•>´èÇ,~•>ú&Úѱ~•>Ü%¸·~•>´*²óS~•>ä f}0~•>æP~•> .kK~•>¾ sY~•>)Äëê~•>¾¶à~•>¾ äŒ_~•>ú"|žþ~•>æ-†z~•>¾"ô¡~•>¾C~•>´ú8i~•>´nM~•>Ò%úÅ‘~•> Z‰§~•>¨dÛÓ~•>´üÓÎ~•>¨¼úQ~•>ø,tÓ~•>ºo~•>¾Ê¹6~•>ðX/~•> O~•>æ »~•>úË ~•>¨Úâè~•>Ò,@ÿ¢~•>¨!Д ~•>æ ì~•>¨’a~•>¨§p~•>¾|÷~•>æ+”úú~•>útl~•>äh3~•>)Lá¤~•>ð¼¾å~•>ú!–ª~•>Ò Ê‚•~•>¼¢Ó~•>Ê7:~•>ø6Ïn~•>.€é~•>ð-y~•>Ÿostinato-1.3.0/client/themes/qds-light.qss000066400000000000000000001545261451413623100205570ustar00rootroot00000000000000/* --------------------------------------------------------------------------- WARNING! File created programmatically. All changes made in this file will be lost! Created by the qtsass compiler v0.3.0 The definitions are in the "qdarkstyle.qss._styles.scss" module --------------------------------------------------------------------------- */ /* Dark Style - QDarkStyleSheet ------------------------------------------ */ /* See Qt documentation: - https://doc.qt.io/qt-5/stylesheet.html - https://doc.qt.io/qt-5/stylesheet-reference.html - https://doc.qt.io/qt-5/stylesheet-examples.html --------------------------------------------------------------------------- */ /* Reset elements ------------------------------------------------------------ Resetting everything helps to unify styles across different operating systems --------------------------------------------------------------------------- */ * { padding: 0px; margin: 0px; border: 0px; border-style: none; border-image: none; outline: 0; } /* specific reset for elements inside QToolBar */ QToolBar * { margin: 0px; padding: 0px; } /* QWidget ---------------------------------------------------------------- --------------------------------------------------------------------------- */ QWidget { background-color: #FAFAFA; border: 0px solid #C9CDD0; padding: 0px; color: #19232D; selection-background-color: #9FCBFF; selection-color: #19232D; } QWidget:disabled { background-color: #FAFAFA; color: #788D9C; selection-background-color: #DAEDFF; selection-color: #788D9C; } QWidget::item:selected { background-color: #9FCBFF; } QWidget::item:hover:!selected { background-color: #73C7FF; } /* QMainWindow ------------------------------------------------------------ This adjusts the splitter in the dock widget, not qsplitter https://doc.qt.io/qt-5/stylesheet-examples.html#customizing-qmainwindow --------------------------------------------------------------------------- */ QMainWindow::separator { background-color: #C9CDD0; border: 0px solid #FAFAFA; spacing: 0px; padding: 2px; } QMainWindow::separator:hover { background-color: #ACB1B6; border: 0px solid #73C7FF; } QMainWindow::separator:horizontal { width: 5px; margin-top: 2px; margin-bottom: 2px; image: url(":/ostinato.org/themes/qds-light/rc/toolbar_separator_vertical.png"); } QMainWindow::separator:vertical { height: 5px; margin-left: 2px; margin-right: 2px; image: url(":/ostinato.org/themes/qds-light/rc/toolbar_separator_horizontal.png"); } /* QToolTip --------------------------------------------------------------- https://doc.qt.io/qt-5/stylesheet-examples.html#customizing-qtooltip --------------------------------------------------------------------------- */ QToolTip { background-color: #9FCBFF; color: #19232D; /* If you remove the border property, background stops working on Windows */ border: none; /* Remove padding, for fix combo box tooltip */ padding: 0px; /* Remove opacity, fix #174 - may need to use RGBA */ } /* QStatusBar ------------------------------------------------------------- https://doc.qt.io/qt-5/stylesheet-examples.html#customizing-qstatusbar --------------------------------------------------------------------------- */ QStatusBar { border: 1px solid #C9CDD0; /* Fixes Spyder #9120, #9121 */ background: #C9CDD0; /* Fixes #205, white vertical borders separating items */ } QStatusBar::item { border: none; } QStatusBar QToolTip { background-color: #73C7FF; border: 1px solid #FAFAFA; color: #FAFAFA; /* Remove padding, for fix combo box tooltip */ padding: 0px; /* Reducing transparency to read better */ opacity: 230; } QStatusBar QLabel { /* Fixes Spyder #9120, #9121 */ background: transparent; } /* QCheckBox -------------------------------------------------------------- https://doc.qt.io/qt-5/stylesheet-examples.html#customizing-qcheckbox --------------------------------------------------------------------------- */ QCheckBox { background-color: #FAFAFA; color: #19232D; spacing: 4px; outline: none; padding-top: 4px; padding-bottom: 4px; } QCheckBox:focus { border: none; } QCheckBox QWidget:disabled { background-color: #FAFAFA; color: #788D9C; } QCheckBox::indicator { margin-left: 2px; height: 14px; width: 14px; } QCheckBox::indicator:unchecked { image: url(":/ostinato.org/themes/qds-light/rc/checkbox_unchecked.png"); } QCheckBox::indicator:unchecked:hover, QCheckBox::indicator:unchecked:focus, QCheckBox::indicator:unchecked:pressed { border: none; image: url(":/ostinato.org/themes/qds-light/rc/checkbox_unchecked_focus.png"); } QCheckBox::indicator:unchecked:disabled { image: url(":/ostinato.org/themes/qds-light/rc/checkbox_unchecked_disabled.png"); } QCheckBox::indicator:checked { image: url(":/ostinato.org/themes/qds-light/rc/checkbox_checked.png"); } QCheckBox::indicator:checked:hover, QCheckBox::indicator:checked:focus, QCheckBox::indicator:checked:pressed { border: none; image: url(":/ostinato.org/themes/qds-light/rc/checkbox_checked_focus.png"); } QCheckBox::indicator:checked:disabled { image: url(":/ostinato.org/themes/qds-light/rc/checkbox_checked_disabled.png"); } QCheckBox::indicator:indeterminate { image: url(":/ostinato.org/themes/qds-light/rc/checkbox_indeterminate.png"); } QCheckBox::indicator:indeterminate:disabled { image: url(":/ostinato.org/themes/qds-light/rc/checkbox_indeterminate_disabled.png"); } QCheckBox::indicator:indeterminate:focus, QCheckBox::indicator:indeterminate:hover, QCheckBox::indicator:indeterminate:pressed { image: url(":/ostinato.org/themes/qds-light/rc/checkbox_indeterminate_focus.png"); } /* QGroupBox -------------------------------------------------------------- https://doc.qt.io/qt-5/stylesheet-examples.html#customizing-qgroupbox --------------------------------------------------------------------------- */ QGroupBox { font-weight: bold; border: 1px solid #C9CDD0; border-radius: 4px; padding: 2px; margin-top: 6px; margin-bottom: 4px; } QGroupBox::title { subcontrol-origin: margin; subcontrol-position: top left; left: 4px; padding-left: 2px; padding-right: 4px; padding-top: -4px; } QGroupBox::indicator { margin-left: 2px; margin-top: 2px; padding: 0; height: 14px; width: 14px; } QGroupBox::indicator:unchecked { border: none; image: url(":/ostinato.org/themes/qds-light/rc/checkbox_unchecked.png"); } QGroupBox::indicator:unchecked:hover, QGroupBox::indicator:unchecked:focus, QGroupBox::indicator:unchecked:pressed { border: none; image: url(":/ostinato.org/themes/qds-light/rc/checkbox_unchecked_focus.png"); } QGroupBox::indicator:unchecked:disabled { image: url(":/ostinato.org/themes/qds-light/rc/checkbox_unchecked_disabled.png"); } QGroupBox::indicator:checked { border: none; image: url(":/ostinato.org/themes/qds-light/rc/checkbox_checked.png"); } QGroupBox::indicator:checked:hover, QGroupBox::indicator:checked:focus, QGroupBox::indicator:checked:pressed { border: none; image: url(":/ostinato.org/themes/qds-light/rc/checkbox_checked_focus.png"); } QGroupBox::indicator:checked:disabled { image: url(":/ostinato.org/themes/qds-light/rc/checkbox_checked_disabled.png"); } /* QRadioButton ----------------------------------------------------------- https://doc.qt.io/qt-5/stylesheet-examples.html#customizing-qradiobutton --------------------------------------------------------------------------- */ QRadioButton { background-color: #FAFAFA; color: #19232D; spacing: 4px; padding-top: 4px; padding-bottom: 4px; border: none; outline: none; } QRadioButton:focus { border: none; } QRadioButton:disabled { background-color: #FAFAFA; color: #788D9C; border: none; outline: none; } QRadioButton QWidget { background-color: #FAFAFA; color: #19232D; spacing: 0px; padding: 0px; outline: none; border: none; } QRadioButton::indicator { border: none; outline: none; margin-left: 2px; height: 14px; width: 14px; } QRadioButton::indicator:unchecked { image: url(":/ostinato.org/themes/qds-light/rc/radio_unchecked.png"); } QRadioButton::indicator:unchecked:hover, QRadioButton::indicator:unchecked:focus, QRadioButton::indicator:unchecked:pressed { border: none; outline: none; image: url(":/ostinato.org/themes/qds-light/rc/radio_unchecked_focus.png"); } QRadioButton::indicator:unchecked:disabled { image: url(":/ostinato.org/themes/qds-light/rc/radio_unchecked_disabled.png"); } QRadioButton::indicator:checked { border: none; outline: none; image: url(":/ostinato.org/themes/qds-light/rc/radio_checked.png"); } QRadioButton::indicator:checked:hover, QRadioButton::indicator:checked:focus, QRadioButton::indicator:checked:pressed { border: none; outline: none; image: url(":/ostinato.org/themes/qds-light/rc/radio_checked_focus.png"); } QRadioButton::indicator:checked:disabled { outline: none; image: url(":/ostinato.org/themes/qds-light/rc/radio_checked_disabled.png"); } /* QMenuBar --------------------------------------------------------------- https://doc.qt.io/qt-5/stylesheet-examples.html#customizing-qmenubar --------------------------------------------------------------------------- */ QMenuBar { background-color: #C9CDD0; padding: 2px; border: 1px solid #FAFAFA; color: #19232D; selection-background-color: #73C7FF; } QMenuBar:focus { border: 1px solid #9FCBFF; } QMenuBar::item { background: transparent; padding: 4px; } QMenuBar::item:selected { padding: 4px; background: transparent; border: 0px solid #C9CDD0; background-color: #73C7FF; } QMenuBar::item:pressed { padding: 4px; border: 0px solid #C9CDD0; background-color: #73C7FF; color: #19232D; margin-bottom: 0px; padding-bottom: 0px; } /* QMenu ------------------------------------------------------------------ https://doc.qt.io/qt-5/stylesheet-examples.html#customizing-qmenu --------------------------------------------------------------------------- */ QMenu { border: 0px solid #C9CDD0; color: #19232D; margin: 0px; background-color: #CED1D4; selection-background-color: #73C7FF; } QMenu::separator { height: 1px; background-color: #ACB1B6; color: #19232D; } QMenu::item { background-color: #CED1D4; padding: 4px 24px 4px 28px; /* Reserve space for selection border */ border: 1px transparent #C9CDD0; } QMenu::item:selected { color: #19232D; background-color: #73C7FF; } QMenu::item:pressed { background-color: #73C7FF; } QMenu::icon { padding-left: 10px; width: 14px; height: 14px; } QMenu::indicator { padding-left: 8px; width: 12px; height: 12px; /* non-exclusive indicator = check box style indicator (see QActionGroup::setExclusive) */ /* exclusive indicator = radio button style indicator (see QActionGroup::setExclusive) */ } QMenu::indicator:non-exclusive:unchecked { image: url(":/ostinato.org/themes/qds-light/rc/checkbox_unchecked.png"); } QMenu::indicator:non-exclusive:unchecked:hover, QMenu::indicator:non-exclusive:unchecked:focus, QMenu::indicator:non-exclusive:unchecked:pressed { border: none; image: url(":/ostinato.org/themes/qds-light/rc/checkbox_unchecked_focus.png"); } QMenu::indicator:non-exclusive:unchecked:disabled { image: url(":/ostinato.org/themes/qds-light/rc/checkbox_unchecked_disabled.png"); } QMenu::indicator:non-exclusive:checked { image: url(":/ostinato.org/themes/qds-light/rc/checkbox_checked.png"); } QMenu::indicator:non-exclusive:checked:hover, QMenu::indicator:non-exclusive:checked:focus, QMenu::indicator:non-exclusive:checked:pressed { border: none; image: url(":/ostinato.org/themes/qds-light/rc/checkbox_checked_focus.png"); } QMenu::indicator:non-exclusive:checked:disabled { image: url(":/ostinato.org/themes/qds-light/rc/checkbox_checked_disabled.png"); } QMenu::indicator:non-exclusive:indeterminate { image: url(":/ostinato.org/themes/qds-light/rc/checkbox_indeterminate.png"); } QMenu::indicator:non-exclusive:indeterminate:disabled { image: url(":/ostinato.org/themes/qds-light/rc/checkbox_indeterminate_disabled.png"); } QMenu::indicator:non-exclusive:indeterminate:focus, QMenu::indicator:non-exclusive:indeterminate:hover, QMenu::indicator:non-exclusive:indeterminate:pressed { image: url(":/ostinato.org/themes/qds-light/rc/checkbox_indeterminate_focus.png"); } QMenu::indicator:exclusive:unchecked { image: url(":/ostinato.org/themes/qds-light/rc/radio_unchecked.png"); } QMenu::indicator:exclusive:unchecked:hover, QMenu::indicator:exclusive:unchecked:focus, QMenu::indicator:exclusive:unchecked:pressed { border: none; outline: none; image: url(":/ostinato.org/themes/qds-light/rc/radio_unchecked_focus.png"); } QMenu::indicator:exclusive:unchecked:disabled { image: url(":/ostinato.org/themes/qds-light/rc/radio_unchecked_disabled.png"); } QMenu::indicator:exclusive:checked { border: none; outline: none; image: url(":/ostinato.org/themes/qds-light/rc/radio_checked.png"); } QMenu::indicator:exclusive:checked:hover, QMenu::indicator:exclusive:checked:focus, QMenu::indicator:exclusive:checked:pressed { border: none; outline: none; image: url(":/ostinato.org/themes/qds-light/rc/radio_checked_focus.png"); } QMenu::indicator:exclusive:checked:disabled { outline: none; image: url(":/ostinato.org/themes/qds-light/rc/radio_checked_disabled.png"); } QMenu::right-arrow { margin: 5px; padding-left: 12px; image: url(":/ostinato.org/themes/qds-light/rc/arrow_right.png"); height: 12px; width: 12px; } /* QAbstractItemView ------------------------------------------------------ https://doc.qt.io/qt-5/stylesheet-examples.html#customizing-qcombobox --------------------------------------------------------------------------- */ QAbstractItemView { alternate-background-color: #FAFAFA; color: #19232D; border: 1px solid #C9CDD0; border-radius: 4px; } QAbstractItemView QLineEdit { padding: 2px; } /* QAbstractScrollArea ---------------------------------------------------- https://doc.qt.io/qt-5/stylesheet-examples.html#customizing-qabstractscrollarea --------------------------------------------------------------------------- */ QAbstractScrollArea { background-color: #FAFAFA; border: 1px solid #C9CDD0; border-radius: 4px; /* fix #159 */ padding: 2px; /* remove min-height to fix #244 */ color: #19232D; } QAbstractScrollArea:disabled { color: #788D9C; } /* QScrollArea ------------------------------------------------------------ --------------------------------------------------------------------------- */ QScrollArea QWidget QWidget:disabled { background-color: #FAFAFA; } /* QScrollBar ------------------------------------------------------------- https://doc.qt.io/qt-5/stylesheet-examples.html#customizing-qscrollbar --------------------------------------------------------------------------- */ QScrollBar:horizontal { height: 16px; margin: 2px 16px 2px 16px; border: 1px solid #C9CDD0; border-radius: 4px; background-color: #FAFAFA; } QScrollBar:vertical { background-color: #FAFAFA; width: 16px; margin: 16px 2px 16px 2px; border: 1px solid #C9CDD0; border-radius: 4px; } QScrollBar::handle:horizontal { background-color: #ACB1B6; border: 1px solid #C9CDD0; border-radius: 4px; min-width: 8px; } QScrollBar::handle:horizontal:hover { background-color: #9FCBFF; border: #9FCBFF; border-radius: 4px; min-width: 8px; } QScrollBar::handle:horizontal:focus { border: 1px solid #73C7FF; } QScrollBar::handle:vertical { background-color: #ACB1B6; border: 1px solid #C9CDD0; min-height: 8px; border-radius: 4px; } QScrollBar::handle:vertical:hover { background-color: #9FCBFF; border: #9FCBFF; border-radius: 4px; min-height: 8px; } QScrollBar::handle:vertical:focus { border: 1px solid #73C7FF; } QScrollBar::add-line:horizontal { margin: 0px 0px 0px 0px; border-image: url(":/ostinato.org/themes/qds-light/rc/arrow_right_disabled.png"); height: 12px; width: 12px; subcontrol-position: right; subcontrol-origin: margin; } QScrollBar::add-line:horizontal:hover, QScrollBar::add-line:horizontal:on { border-image: url(":/ostinato.org/themes/qds-light/rc/arrow_right.png"); height: 12px; width: 12px; subcontrol-position: right; subcontrol-origin: margin; } QScrollBar::add-line:vertical { margin: 3px 0px 3px 0px; border-image: url(":/ostinato.org/themes/qds-light/rc/arrow_down_disabled.png"); height: 12px; width: 12px; subcontrol-position: bottom; subcontrol-origin: margin; } QScrollBar::add-line:vertical:hover, QScrollBar::add-line:vertical:on { border-image: url(":/ostinato.org/themes/qds-light/rc/arrow_down.png"); height: 12px; width: 12px; subcontrol-position: bottom; subcontrol-origin: margin; } QScrollBar::sub-line:horizontal { margin: 0px 3px 0px 3px; border-image: url(":/ostinato.org/themes/qds-light/rc/arrow_left_disabled.png"); height: 12px; width: 12px; subcontrol-position: left; subcontrol-origin: margin; } QScrollBar::sub-line:horizontal:hover, QScrollBar::sub-line:horizontal:on { border-image: url(":/ostinato.org/themes/qds-light/rc/arrow_left.png"); height: 12px; width: 12px; subcontrol-position: left; subcontrol-origin: margin; } QScrollBar::sub-line:vertical { margin: 3px 0px 3px 0px; border-image: url(":/ostinato.org/themes/qds-light/rc/arrow_up_disabled.png"); height: 12px; width: 12px; subcontrol-position: top; subcontrol-origin: margin; } QScrollBar::sub-line:vertical:hover, QScrollBar::sub-line:vertical:on { border-image: url(":/ostinato.org/themes/qds-light/rc/arrow_up.png"); height: 12px; width: 12px; subcontrol-position: top; subcontrol-origin: margin; } QScrollBar::up-arrow:horizontal, QScrollBar::down-arrow:horizontal { background: none; } QScrollBar::up-arrow:vertical, QScrollBar::down-arrow:vertical { background: none; } QScrollBar::add-page:horizontal, QScrollBar::sub-page:horizontal { background: none; } QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical { background: none; } /* QTextEdit -------------------------------------------------------------- https://doc.qt.io/qt-5/stylesheet-examples.html#customizing-specific-widgets --------------------------------------------------------------------------- */ QTextEdit { background-color: #FAFAFA; color: #19232D; border-radius: 4px; border: 1px solid #C9CDD0; } QTextEdit:focus { border: 1px solid #73C7FF; } QTextEdit:selected { background: #9FCBFF; color: #C9CDD0; } /* QPlainTextEdit --------------------------------------------------------- --------------------------------------------------------------------------- */ QPlainTextEdit { background-color: #FAFAFA; color: #19232D; border-radius: 4px; border: 1px solid #C9CDD0; } QPlainTextEdit:focus { border: 1px solid #73C7FF; } QPlainTextEdit:selected { background: #9FCBFF; color: #C9CDD0; } /* QSizeGrip -------------------------------------------------------------- https://doc.qt.io/qt-5/stylesheet-examples.html#customizing-qsizegrip --------------------------------------------------------------------------- */ QSizeGrip { background: transparent; width: 12px; height: 12px; image: url(":/ostinato.org/themes/qds-light/rc/window_grip.png"); } /* QStackedWidget --------------------------------------------------------- --------------------------------------------------------------------------- */ QStackedWidget { padding: 2px; border: 1px solid #C9CDD0; border: 1px solid #FAFAFA; } /* QToolBar --------------------------------------------------------------- https://doc.qt.io/qt-5/stylesheet-examples.html#customizing-qtoolbar --------------------------------------------------------------------------- */ QToolBar { background-color: #C9CDD0; border-bottom: 1px solid #FAFAFA; padding: 1px; font-weight: bold; spacing: 2px; } QToolBar:disabled { /* Fixes #272 */ background-color: #C9CDD0; } QToolBar::handle:horizontal { width: 16px; image: url(":/ostinato.org/themes/qds-light/rc/toolbar_move_horizontal.png"); } QToolBar::handle:vertical { height: 16px; image: url(":/ostinato.org/themes/qds-light/rc/toolbar_move_vertical.png"); } QToolBar::separator:horizontal { width: 16px; image: url(":/ostinato.org/themes/qds-light/rc/toolbar_separator_horizontal.png"); } QToolBar::separator:vertical { height: 16px; image: url(":/ostinato.org/themes/qds-light/rc/toolbar_separator_vertical.png"); } QToolButton#qt_toolbar_ext_button { background: #C9CDD0; border: 0px; color: #19232D; image: url(":/ostinato.org/themes/qds-light/rc/arrow_right.png"); } /* QAbstractSpinBox ------------------------------------------------------- --------------------------------------------------------------------------- */ QAbstractSpinBox { background-color: #FAFAFA; border: 1px solid #C9CDD0; color: #19232D; /* This fixes 103, 111 */ padding-top: 2px; /* This fixes 103, 111 */ padding-bottom: 2px; padding-left: 4px; padding-right: 4px; border-radius: 4px; /* min-width: 5px; removed to fix 109 */ } QAbstractSpinBox:up-button { background-color: transparent #FAFAFA; subcontrol-origin: border; subcontrol-position: top right; border-left: 1px solid #C9CDD0; border-bottom: 1px solid #C9CDD0; border-top-left-radius: 0; border-bottom-left-radius: 0; margin: 1px; width: 12px; margin-bottom: -1px; } QAbstractSpinBox::up-arrow, QAbstractSpinBox::up-arrow:disabled, QAbstractSpinBox::up-arrow:off { image: url(":/ostinato.org/themes/qds-light/rc/arrow_up_disabled.png"); height: 8px; width: 8px; } QAbstractSpinBox::up-arrow:hover { image: url(":/ostinato.org/themes/qds-light/rc/arrow_up.png"); } QAbstractSpinBox:down-button { background-color: transparent #FAFAFA; subcontrol-origin: border; subcontrol-position: bottom right; border-left: 1px solid #C9CDD0; border-top: 1px solid #C9CDD0; border-top-left-radius: 0; border-bottom-left-radius: 0; margin: 1px; width: 12px; margin-top: -1px; } QAbstractSpinBox::down-arrow, QAbstractSpinBox::down-arrow:disabled, QAbstractSpinBox::down-arrow:off { image: url(":/ostinato.org/themes/qds-light/rc/arrow_down_disabled.png"); height: 8px; width: 8px; } QAbstractSpinBox::down-arrow:hover { image: url(":/ostinato.org/themes/qds-light/rc/arrow_down.png"); } QAbstractSpinBox:hover { border: 1px solid #9FCBFF; color: #19232D; } QAbstractSpinBox:focus { border: 1px solid #73C7FF; } QAbstractSpinBox:selected { background: #9FCBFF; color: #C9CDD0; } /* ------------------------------------------------------------------------ */ /* DISPLAYS --------------------------------------------------------------- */ /* ------------------------------------------------------------------------ */ /* QLabel ----------------------------------------------------------------- https://doc.qt.io/qt-5/stylesheet-examples.html#customizing-qframe --------------------------------------------------------------------------- */ QLabel { background-color: #FAFAFA; border: 0px solid #C9CDD0; padding: 2px; margin: 0px; color: #19232D; } QLabel:disabled { background-color: #FAFAFA; border: 0px solid #C9CDD0; color: #788D9C; } /* QTextBrowser ----------------------------------------------------------- https://doc.qt.io/qt-5/stylesheet-examples.html#customizing-qabstractscrollarea --------------------------------------------------------------------------- */ QTextBrowser { background-color: #FAFAFA; border: 1px solid #C9CDD0; color: #19232D; border-radius: 4px; } QTextBrowser:disabled { background-color: #FAFAFA; border: 1px solid #C9CDD0; color: #788D9C; border-radius: 4px; } QTextBrowser:hover, QTextBrowser:!hover, QTextBrowser:selected, QTextBrowser:pressed { border: 1px solid #C9CDD0; } /* QGraphicsView ---------------------------------------------------------- --------------------------------------------------------------------------- */ QGraphicsView { background-color: #FAFAFA; border: 1px solid #C9CDD0; color: #19232D; border-radius: 4px; } QGraphicsView:disabled { background-color: #FAFAFA; border: 1px solid #C9CDD0; color: #788D9C; border-radius: 4px; } QGraphicsView:hover, QGraphicsView:!hover, QGraphicsView:selected, QGraphicsView:pressed { border: 1px solid #C9CDD0; } /* QCalendarWidget -------------------------------------------------------- --------------------------------------------------------------------------- */ QCalendarWidget { border: 1px solid #C9CDD0; border-radius: 4px; } QCalendarWidget:disabled { background-color: #FAFAFA; color: #788D9C; } /* QLCDNumber ------------------------------------------------------------- --------------------------------------------------------------------------- */ QLCDNumber { background-color: #FAFAFA; color: #19232D; } QLCDNumber:disabled { background-color: #FAFAFA; color: #788D9C; } /* QProgressBar ----------------------------------------------------------- https://doc.qt.io/qt-5/stylesheet-examples.html#customizing-qprogressbar --------------------------------------------------------------------------- */ QProgressBar { background-color: #FAFAFA; border: 1px solid #C9CDD0; color: #19232D; border-radius: 4px; text-align: center; } QProgressBar:disabled { background-color: #FAFAFA; border: 1px solid #C9CDD0; color: #788D9C; border-radius: 4px; text-align: center; } QProgressBar::chunk { background-color: #9FCBFF; color: #FAFAFA; border-radius: 4px; } QProgressBar::chunk:disabled { background-color: #DAEDFF; color: #788D9C; border-radius: 4px; } /* ------------------------------------------------------------------------ */ /* BUTTONS ---------------------------------------------------------------- */ /* ------------------------------------------------------------------------ */ /* QPushButton ------------------------------------------------------------ https://doc.qt.io/qt-5/stylesheet-examples.html#customizing-qpushbutton --------------------------------------------------------------------------- */ QPushButton { background-color: #C9CDD0; color: #19232D; border-radius: 4px; padding: 2px; outline: none; border: none; } QPushButton:disabled { background-color: #C9CDD0; color: #788D9C; border-radius: 4px; padding: 2px; } QPushButton:checked { background-color: #ACB1B6; border-radius: 4px; padding: 2px; outline: none; } QPushButton:checked:disabled { background-color: #ACB1B6; color: #788D9C; border-radius: 4px; padding: 2px; outline: none; } QPushButton:checked:selected { background: #ACB1B6; } QPushButton:hover { background-color: #B9BDC1; color: #19232D; } QPushButton:pressed { background-color: #ACB1B6; } QPushButton:selected { background: #ACB1B6; color: #19232D; } QPushButton::menu-indicator { subcontrol-origin: padding; subcontrol-position: bottom right; bottom: 4px; } QDialogButtonBox QPushButton { /* Issue #194 #248 - Special case of QPushButton inside dialogs, for better UI */ min-width: 80px; } /* QToolButton ------------------------------------------------------------ https://doc.qt.io/qt-5/stylesheet-examples.html#customizing-qtoolbutton --------------------------------------------------------------------------- */ QToolButton { background-color: #C9CDD0; color: #19232D; border-radius: 4px; padding: 2px; outline: none; border: none; /* The subcontrols below are used only in the DelayedPopup mode */ /* The subcontrols below are used only in the MenuButtonPopup mode */ /* The subcontrol below is used only in the InstantPopup or DelayedPopup mode */ } QToolButton:disabled { background-color: #C9CDD0; color: #788D9C; border-radius: 4px; padding: 2px; } QToolButton:checked { background-color: #ACB1B6; border-radius: 4px; padding: 2px; outline: none; } QToolButton:checked:disabled { background-color: #ACB1B6; color: #788D9C; border-radius: 4px; padding: 2px; outline: none; } QToolButton:checked:hover { background-color: #B9BDC1; color: #19232D; } QToolButton:checked:pressed { background-color: #ACB1B6; } QToolButton:checked:selected { background: #ACB1B6; color: #19232D; } QToolButton:hover { background-color: #B9BDC1; color: #19232D; } QToolButton:pressed { background-color: #ACB1B6; } QToolButton:selected { background: #ACB1B6; color: #19232D; } QToolButton[popupMode="0"] { /* Only for DelayedPopup */ padding-right: 2px; } QToolButton[popupMode="1"] { /* Only for MenuButtonPopup */ padding-right: 20px; } QToolButton[popupMode="1"]::menu-button { border: none; } QToolButton[popupMode="1"]::menu-button:hover { border: none; border-left: 1px solid #C9CDD0; border-radius: 0; } QToolButton[popupMode="2"] { /* Only for InstantPopup */ padding-right: 2px; } QToolButton::menu-button { padding: 2px; border-radius: 4px; width: 12px; border: none; outline: none; } QToolButton::menu-button:hover { border: 1px solid #9FCBFF; } QToolButton::menu-button:checked:hover { border: 1px solid #9FCBFF; } QToolButton::menu-indicator { image: url(":/ostinato.org/themes/qds-light/rc/arrow_down.png"); height: 8px; width: 8px; top: 0; /* Exclude a shift for better image */ left: -2px; /* Shift it a bit */ } QToolButton::menu-arrow { image: url(":/ostinato.org/themes/qds-light/rc/arrow_down.png"); height: 8px; width: 8px; } QToolButton::menu-arrow:hover { image: url(":/ostinato.org/themes/qds-light/rc/arrow_down_focus.png"); } /* QCommandLinkButton ----------------------------------------------------- --------------------------------------------------------------------------- */ QCommandLinkButton { background-color: transparent; border: 1px solid #C9CDD0; color: #19232D; border-radius: 4px; padding: 0px; margin: 0px; } QCommandLinkButton:disabled { background-color: transparent; color: #788D9C; } /* ------------------------------------------------------------------------ */ /* INPUTS - NO FIELDS ----------------------------------------------------- */ /* ------------------------------------------------------------------------ */ /* QComboBox -------------------------------------------------------------- https://doc.qt.io/qt-5/stylesheet-examples.html#customizing-qcombobox --------------------------------------------------------------------------- */ QComboBox { border: 1px solid #C9CDD0; border-radius: 4px; selection-background-color: #9FCBFF; padding-left: 4px; padding-right: 4px; /* padding-right = 36; 4 + 16*2 See scrollbar size */ /* changed to 4px to fix #239 */ /* Fixes #103, #111 */ min-height: 1.5em; /* padding-top: 2px; removed to fix #132 */ /* padding-bottom: 2px; removed to fix #132 */ /* min-width: 75px; removed to fix #109 */ /* Needed to remove indicator - fix #132 */ } QComboBox QAbstractItemView { border: 1px solid #C9CDD0; border-radius: 0; background-color: #FAFAFA; selection-background-color: #9FCBFF; } QComboBox QAbstractItemView:hover { background-color: #FAFAFA; color: #19232D; } QComboBox QAbstractItemView:selected { background: #9FCBFF; color: #C9CDD0; } QComboBox QAbstractItemView:alternate { background: #FAFAFA; } QComboBox:disabled { background-color: #FAFAFA; color: #788D9C; } QComboBox:hover { border: 1px solid #9FCBFF; } QComboBox:focus { border: 1px solid #73C7FF; } QComboBox:on { selection-background-color: #9FCBFF; } QComboBox::indicator { border: none; border-radius: 0; background-color: transparent; selection-background-color: transparent; color: transparent; selection-color: transparent; /* Needed to remove indicator - fix #132 */ } QComboBox::indicator:alternate { background: #FAFAFA; } QComboBox::item:alternate { background: #FAFAFA; } QComboBox::item:checked { font-weight: bold; } QComboBox::item:selected { border: 0px solid transparent; } QComboBox::drop-down { subcontrol-origin: padding; subcontrol-position: top right; width: 12px; border-left: 1px solid #C9CDD0; } QComboBox::down-arrow { image: url(":/ostinato.org/themes/qds-light/rc/arrow_down_disabled.png"); height: 8px; width: 8px; } QComboBox::down-arrow:on, QComboBox::down-arrow:hover, QComboBox::down-arrow:focus { image: url(":/ostinato.org/themes/qds-light/rc/arrow_down.png"); } /* QSlider ---------------------------------------------------------------- https://doc.qt.io/qt-5/stylesheet-examples.html#customizing-qslider --------------------------------------------------------------------------- */ QSlider:disabled { background: #FAFAFA; } QSlider:focus { border: none; } QSlider::groove:horizontal { background: #C9CDD0; border: 1px solid #C9CDD0; height: 4px; margin: 0px; border-radius: 4px; } QSlider::groove:vertical { background: #C9CDD0; border: 1px solid #C9CDD0; width: 4px; margin: 0px; border-radius: 4px; } QSlider::add-page:vertical { background: #9FCBFF; border: 1px solid #C9CDD0; width: 4px; margin: 0px; border-radius: 4px; } QSlider::add-page:vertical :disabled { background: #DAEDFF; } QSlider::sub-page:horizontal { background: #9FCBFF; border: 1px solid #C9CDD0; height: 4px; margin: 0px; border-radius: 4px; } QSlider::sub-page:horizontal:disabled { background: #DAEDFF; } QSlider::handle:horizontal { background: #788D9C; border: 1px solid #C9CDD0; width: 8px; height: 8px; margin: -8px 0px; border-radius: 4px; } QSlider::handle:horizontal:hover { background: #9FCBFF; border: 1px solid #9FCBFF; } QSlider::handle:horizontal:focus { border: 1px solid #73C7FF; } QSlider::handle:vertical { background: #788D9C; border: 1px solid #C9CDD0; width: 8px; height: 8px; margin: 0 -8px; border-radius: 4px; } QSlider::handle:vertical:hover { background: #9FCBFF; border: 1px solid #9FCBFF; } QSlider::handle:vertical:focus { border: 1px solid #73C7FF; } /* QLineEdit -------------------------------------------------------------- https://doc.qt.io/qt-5/stylesheet-examples.html#customizing-qlineedit --------------------------------------------------------------------------- */ QLineEdit { background-color: #FAFAFA; padding-top: 2px; /* This QLineEdit fix 103, 111 */ padding-bottom: 2px; /* This QLineEdit fix 103, 111 */ padding-left: 4px; padding-right: 4px; border-style: solid; border: 1px solid #C9CDD0; border-radius: 4px; color: #19232D; } QLineEdit:disabled { background-color: #FAFAFA; color: #788D9C; } QLineEdit:hover { border: 1px solid #9FCBFF; color: #19232D; } QLineEdit:focus { border: 1px solid #73C7FF; } QLineEdit:selected { background-color: #9FCBFF; color: #C9CDD0; } /* QTabWiget -------------------------------------------------------------- https://doc.qt.io/qt-5/stylesheet-examples.html#customizing-qtabwidget-and-qtabbar --------------------------------------------------------------------------- */ QTabWidget { padding: 2px; selection-background-color: #C9CDD0; } QTabWidget QWidget { /* Fixes #189 */ border-radius: 4px; } QTabWidget::pane { border: 1px solid #C9CDD0; border-radius: 4px; margin: 0px; /* Fixes double border inside pane with pyqt5 */ padding: 0px; } QTabWidget::pane:selected { background-color: #C9CDD0; border: 1px solid #9FCBFF; } /* QTabBar ---------------------------------------------------------------- https://doc.qt.io/qt-5/stylesheet-examples.html#customizing-qtabwidget-and-qtabbar --------------------------------------------------------------------------- */ QTabBar, QDockWidget QTabBar { qproperty-drawBase: 0; border-radius: 4px; margin: 0px; padding: 2px; border: 0; /* left: 5px; move to the right by 5px - removed for fix */ } QTabBar::close-button, QDockWidget QTabBar::close-button { border: 0; margin: 0; padding: 4px; image: url(":/ostinato.org/themes/qds-light/rc/window_close.png"); } QTabBar::close-button:hover, QDockWidget QTabBar::close-button:hover { image: url(":/ostinato.org/themes/qds-light/rc/window_close_focus.png"); } QTabBar::close-button:pressed, QDockWidget QTabBar::close-button:pressed { image: url(":/ostinato.org/themes/qds-light/rc/window_close_pressed.png"); } QTabBar::tab, QDockWidget QTabBar::tab { /* !selected and disabled ----------------------------------------- */ /* selected ------------------------------------------------------- */ } QTabBar::tab:top:selected:disabled, QDockWidget QTabBar::tab:top:selected:disabled { border-bottom: 3px solid #DAEDFF; color: #788D9C; background-color: #C9CDD0; } QTabBar::tab:bottom:selected:disabled, QDockWidget QTabBar::tab:bottom:selected:disabled { border-top: 3px solid #DAEDFF; color: #788D9C; background-color: #C9CDD0; } QTabBar::tab:left:selected:disabled, QDockWidget QTabBar::tab:left:selected:disabled { border-right: 3px solid #DAEDFF; color: #788D9C; background-color: #C9CDD0; } QTabBar::tab:right:selected:disabled, QDockWidget QTabBar::tab:right:selected:disabled { border-left: 3px solid #DAEDFF; color: #788D9C; background-color: #C9CDD0; } QTabBar::tab:top:!selected:disabled, QDockWidget QTabBar::tab:top:!selected:disabled { border-bottom: 3px solid #FAFAFA; color: #788D9C; background-color: #FAFAFA; } QTabBar::tab:bottom:!selected:disabled, QDockWidget QTabBar::tab:bottom:!selected:disabled { border-top: 3px solid #FAFAFA; color: #788D9C; background-color: #FAFAFA; } QTabBar::tab:left:!selected:disabled, QDockWidget QTabBar::tab:left:!selected:disabled { border-right: 3px solid #FAFAFA; color: #788D9C; background-color: #FAFAFA; } QTabBar::tab:right:!selected:disabled, QDockWidget QTabBar::tab:right:!selected:disabled { border-left: 3px solid #FAFAFA; color: #788D9C; background-color: #FAFAFA; } QTabBar::tab:top:!selected, QDockWidget QTabBar::tab:top:!selected { border-bottom: 2px solid #FAFAFA; margin-top: 2px; } QTabBar::tab:bottom:!selected, QDockWidget QTabBar::tab:bottom:!selected { border-top: 2px solid #FAFAFA; margin-bottom: 2px; } QTabBar::tab:left:!selected, QDockWidget QTabBar::tab:left:!selected { border-left: 2px solid #FAFAFA; margin-right: 2px; } QTabBar::tab:right:!selected, QDockWidget QTabBar::tab:right:!selected { border-right: 2px solid #FAFAFA; margin-left: 2px; } QTabBar::tab:top, QDockWidget QTabBar::tab:top { background-color: #C9CDD0; margin-left: 2px; padding-left: 4px; padding-right: 4px; padding-top: 2px; padding-bottom: 2px; min-width: 5px; border-bottom: 3px solid #C9CDD0; border-top-left-radius: 4px; border-top-right-radius: 4px; } QTabBar::tab:top:selected, QDockWidget QTabBar::tab:top:selected { background-color: #B9BDC1; border-bottom: 3px solid #37AEFE; border-top-left-radius: 4px; border-top-right-radius: 4px; } QTabBar::tab:top:!selected:hover, QDockWidget QTabBar::tab:top:!selected:hover { border: 1px solid #73C7FF; border-bottom: 3px solid #73C7FF; /* Fixes spyder-ide/spyder#9766 and #243 */ padding-left: 3px; padding-right: 3px; } QTabBar::tab:bottom, QDockWidget QTabBar::tab:bottom { border-top: 3px solid #C9CDD0; background-color: #C9CDD0; margin-left: 2px; padding-left: 4px; padding-right: 4px; padding-top: 2px; padding-bottom: 2px; border-bottom-left-radius: 4px; border-bottom-right-radius: 4px; min-width: 5px; } QTabBar::tab:bottom:selected, QDockWidget QTabBar::tab:bottom:selected { background-color: #B9BDC1; border-top: 3px solid #37AEFE; border-bottom-left-radius: 4px; border-bottom-right-radius: 4px; } QTabBar::tab:bottom:!selected:hover, QDockWidget QTabBar::tab:bottom:!selected:hover { border: 1px solid #73C7FF; border-top: 3px solid #73C7FF; /* Fixes spyder-ide/spyder#9766 and #243 */ padding-left: 3px; padding-right: 3px; } QTabBar::tab:left, QDockWidget QTabBar::tab:left { background-color: #C9CDD0; margin-top: 2px; padding-left: 2px; padding-right: 2px; padding-top: 4px; padding-bottom: 4px; border-top-left-radius: 4px; border-bottom-left-radius: 4px; min-height: 5px; } QTabBar::tab:left:selected, QDockWidget QTabBar::tab:left:selected { background-color: #B9BDC1; border-right: 3px solid #37AEFE; } QTabBar::tab:left:!selected:hover, QDockWidget QTabBar::tab:left:!selected:hover { border: 1px solid #73C7FF; border-right: 3px solid #73C7FF; /* Fixes different behavior #271 */ margin-right: 0px; padding-right: -1px; } QTabBar::tab:right, QDockWidget QTabBar::tab:right { background-color: #C9CDD0; margin-top: 2px; padding-left: 2px; padding-right: 2px; padding-top: 4px; padding-bottom: 4px; border-top-right-radius: 4px; border-bottom-right-radius: 4px; min-height: 5px; } QTabBar::tab:right:selected, QDockWidget QTabBar::tab:right:selected { background-color: #B9BDC1; border-left: 3px solid #37AEFE; } QTabBar::tab:right:!selected:hover, QDockWidget QTabBar::tab:right:!selected:hover { border: 1px solid #73C7FF; border-left: 3px solid #73C7FF; /* Fixes different behavior #271 */ margin-left: 0px; padding-left: 0px; } QTabBar QToolButton, QDockWidget QTabBar QToolButton { /* Fixes #136 */ background-color: #C9CDD0; height: 12px; width: 12px; } QTabBar QToolButton:pressed, QDockWidget QTabBar QToolButton:pressed { background-color: #C9CDD0; } QTabBar QToolButton:pressed:hover, QDockWidget QTabBar QToolButton:pressed:hover { border: 1px solid #9FCBFF; } QTabBar QToolButton::left-arrow:enabled, QDockWidget QTabBar QToolButton::left-arrow:enabled { image: url(":/ostinato.org/themes/qds-light/rc/arrow_left.png"); } QTabBar QToolButton::left-arrow:disabled, QDockWidget QTabBar QToolButton::left-arrow:disabled { image: url(":/ostinato.org/themes/qds-light/rc/arrow_left_disabled.png"); } QTabBar QToolButton::right-arrow:enabled, QDockWidget QTabBar QToolButton::right-arrow:enabled { image: url(":/ostinato.org/themes/qds-light/rc/arrow_right.png"); } QTabBar QToolButton::right-arrow:disabled, QDockWidget QTabBar QToolButton::right-arrow:disabled { image: url(":/ostinato.org/themes/qds-light/rc/arrow_right_disabled.png"); } /* QDockWiget ------------------------------------------------------------- --------------------------------------------------------------------------- */ QDockWidget { outline: 1px solid #C9CDD0; background-color: #FAFAFA; border: 1px solid #C9CDD0; border-radius: 4px; titlebar-close-icon: url(":/ostinato.org/themes/qds-light/rc/transparent.png"); titlebar-normal-icon: url(":/ostinato.org/themes/qds-light/rc/transparent.png"); } QDockWidget::title { /* Better size for title bar */ padding: 3px; spacing: 4px; border: none; background-color: #C9CDD0; } QDockWidget::close-button { icon-size: 12px; border: none; background: transparent; background-image: transparent; border: 0; margin: 0; padding: 0; image: url(":/ostinato.org/themes/qds-light/rc/window_close.png"); } QDockWidget::close-button:hover { image: url(":/ostinato.org/themes/qds-light/rc/window_close_focus.png"); } QDockWidget::close-button:pressed { image: url(":/ostinato.org/themes/qds-light/rc/window_close_pressed.png"); } QDockWidget::float-button { icon-size: 12px; border: none; background: transparent; background-image: transparent; border: 0; margin: 0; padding: 0; image: url(":/ostinato.org/themes/qds-light/rc/window_undock.png"); } QDockWidget::float-button:hover { image: url(":/ostinato.org/themes/qds-light/rc/window_undock_focus.png"); } QDockWidget::float-button:pressed { image: url(":/ostinato.org/themes/qds-light/rc/window_undock_pressed.png"); } /* QTreeView QListView QTableView ----------------------------------------- https://doc.qt.io/qt-5/stylesheet-examples.html#customizing-qtreeview https://doc.qt.io/qt-5/stylesheet-examples.html#customizing-qlistview https://doc.qt.io/qt-5/stylesheet-examples.html#customizing-qtableview --------------------------------------------------------------------------- */ QTreeView:branch:selected, QTreeView:branch:hover { background: url(":/ostinato.org/themes/qds-light/rc/transparent.png"); } QTreeView:branch:has-siblings:!adjoins-item { border-image: url(":/ostinato.org/themes/qds-light/rc/branch_line.png") 0; } QTreeView:branch:has-siblings:adjoins-item { border-image: url(":/ostinato.org/themes/qds-light/rc/branch_more.png") 0; } QTreeView:branch:!has-children:!has-siblings:adjoins-item { border-image: url(":/ostinato.org/themes/qds-light/rc/branch_end.png") 0; } QTreeView:branch:has-children:!has-siblings:closed, QTreeView:branch:closed:has-children:has-siblings { border-image: none; image: url(":/ostinato.org/themes/qds-light/rc/branch_closed.png"); } QTreeView:branch:open:has-children:!has-siblings, QTreeView:branch:open:has-children:has-siblings { border-image: none; image: url(":/ostinato.org/themes/qds-light/rc/branch_open.png"); } QTreeView:branch:has-children:!has-siblings:closed:hover, QTreeView:branch:closed:has-children:has-siblings:hover { image: url(":/ostinato.org/themes/qds-light/rc/branch_closed_focus.png"); } QTreeView:branch:open:has-children:!has-siblings:hover, QTreeView:branch:open:has-children:has-siblings:hover { image: url(":/ostinato.org/themes/qds-light/rc/branch_open_focus.png"); } QTreeView::indicator:checked, QListView::indicator:checked, QTableView::indicator:checked, QColumnView::indicator:checked { image: url(":/ostinato.org/themes/qds-light/rc/checkbox_checked.png"); } QTreeView::indicator:checked:hover, QTreeView::indicator:checked:focus, QTreeView::indicator:checked:pressed, QListView::indicator:checked:hover, QListView::indicator:checked:focus, QListView::indicator:checked:pressed, QTableView::indicator:checked:hover, QTableView::indicator:checked:focus, QTableView::indicator:checked:pressed, QColumnView::indicator:checked:hover, QColumnView::indicator:checked:focus, QColumnView::indicator:checked:pressed { image: url(":/ostinato.org/themes/qds-light/rc/checkbox_checked_focus.png"); } QTreeView::indicator:unchecked, QListView::indicator:unchecked, QTableView::indicator:unchecked, QColumnView::indicator:unchecked { image: url(":/ostinato.org/themes/qds-light/rc/checkbox_unchecked.png"); } QTreeView::indicator:unchecked:hover, QTreeView::indicator:unchecked:focus, QTreeView::indicator:unchecked:pressed, QListView::indicator:unchecked:hover, QListView::indicator:unchecked:focus, QListView::indicator:unchecked:pressed, QTableView::indicator:unchecked:hover, QTableView::indicator:unchecked:focus, QTableView::indicator:unchecked:pressed, QColumnView::indicator:unchecked:hover, QColumnView::indicator:unchecked:focus, QColumnView::indicator:unchecked:pressed { image: url(":/ostinato.org/themes/qds-light/rc/checkbox_unchecked_focus.png"); } QTreeView::indicator:indeterminate, QListView::indicator:indeterminate, QTableView::indicator:indeterminate, QColumnView::indicator:indeterminate { image: url(":/ostinato.org/themes/qds-light/rc/checkbox_indeterminate.png"); } QTreeView::indicator:indeterminate:hover, QTreeView::indicator:indeterminate:focus, QTreeView::indicator:indeterminate:pressed, QListView::indicator:indeterminate:hover, QListView::indicator:indeterminate:focus, QListView::indicator:indeterminate:pressed, QTableView::indicator:indeterminate:hover, QTableView::indicator:indeterminate:focus, QTableView::indicator:indeterminate:pressed, QColumnView::indicator:indeterminate:hover, QColumnView::indicator:indeterminate:focus, QColumnView::indicator:indeterminate:pressed { image: url(":/ostinato.org/themes/qds-light/rc/checkbox_indeterminate_focus.png"); } QTreeView, QListView, QTableView, QColumnView { background-color: #FAFAFA; border: 1px solid #C9CDD0; color: #19232D; gridline-color: #C9CDD0; border-radius: 4px; } QTreeView:disabled, QListView:disabled, QTableView:disabled, QColumnView:disabled { background-color: #FAFAFA; color: #788D9C; } QTreeView:selected, QListView:selected, QTableView:selected, QColumnView:selected { background-color: #9FCBFF; color: #C9CDD0; } QTreeView:focus, QListView:focus, QTableView:focus, QColumnView:focus { border: 1px solid #73C7FF; } QTreeView::item:pressed, QListView::item:pressed, QTableView::item:pressed, QColumnView::item:pressed { background-color: #9FCBFF; } QTreeView::item:selected:active, QListView::item:selected:active, QTableView::item:selected:active, QColumnView::item:selected:active { background-color: #9FCBFF; } QTreeView::item:selected:!active, QListView::item:selected:!active, QTableView::item:selected:!active, QColumnView::item:selected:!active { color: #19232D; background-color: #CED1D4; } QTreeView::item:!selected:hover, QListView::item:!selected:hover, QTableView::item:!selected:hover, QColumnView::item:!selected:hover { outline: 0; color: #19232D; background-color: #CED1D4; } QTableCornerButton::section { background-color: #FAFAFA; border: 1px transparent #C9CDD0; border-radius: 0px; } /* QHeaderView ------------------------------------------------------------ https://doc.qt.io/qt-5/stylesheet-examples.html#customizing-qheaderview --------------------------------------------------------------------------- */ QHeaderView { background-color: #C9CDD0; border: 0px transparent #C9CDD0; padding: 0; margin: 0; border-radius: 0; } QHeaderView:disabled { background-color: #C9CDD0; border: 1px transparent #C9CDD0; } QHeaderView::section { background-color: #C9CDD0; color: #19232D; border-radius: 0; text-align: left; font-size: 13px; } QHeaderView::section::horizontal { padding-top: 0; padding-bottom: 0; padding-left: 4px; padding-right: 4px; border-left: 1px solid #FAFAFA; } QHeaderView::section::horizontal::first, QHeaderView::section::horizontal::only-one { border-left: 1px solid #C9CDD0; } QHeaderView::section::horizontal:disabled { color: #788D9C; } QHeaderView::section::vertical { padding-top: 0; padding-bottom: 0; padding-left: 4px; padding-right: 4px; border-top: 1px solid #FAFAFA; } QHeaderView::section::vertical::first, QHeaderView::section::vertical::only-one { border-top: 1px solid #C9CDD0; } QHeaderView::section::vertical:disabled { color: #788D9C; } QHeaderView::down-arrow { /* Those settings (border/width/height/background-color) solve bug */ /* transparent arrow background and size */ background-color: #C9CDD0; border: none; height: 12px; width: 12px; padding-left: 2px; padding-right: 2px; image: url(":/ostinato.org/themes/qds-light/rc/arrow_down.png"); } QHeaderView::up-arrow { background-color: #C9CDD0; border: none; height: 12px; width: 12px; padding-left: 2px; padding-right: 2px; image: url(":/ostinato.org/themes/qds-light/rc/arrow_up.png"); } /* QToolBox -------------------------------------------------------------- https://doc.qt.io/qt-5/stylesheet-examples.html#customizing-qtoolbox --------------------------------------------------------------------------- */ QToolBox { padding: 0px; border: 0px; border: 1px solid #C9CDD0; } QToolBox:selected { padding: 0px; border: 2px solid #9FCBFF; } QToolBox::tab { background-color: #FAFAFA; border: 1px solid #C9CDD0; color: #19232D; border-top-left-radius: 4px; border-top-right-radius: 4px; } QToolBox::tab:disabled { color: #788D9C; } QToolBox::tab:selected { background-color: #ACB1B6; border-bottom: 2px solid #9FCBFF; } QToolBox::tab:selected:disabled { background-color: #C9CDD0; border-bottom: 2px solid #DAEDFF; } QToolBox::tab:!selected { background-color: #C9CDD0; border-bottom: 2px solid #C9CDD0; } QToolBox::tab:!selected:disabled { background-color: #FAFAFA; } QToolBox::tab:hover { border-color: #73C7FF; border-bottom: 2px solid #73C7FF; } QToolBox QScrollArea QWidget QWidget { padding: 0px; border: 0px; background-color: #FAFAFA; } /* QFrame ----------------------------------------------------------------- https://doc.qt.io/qt-5/stylesheet-examples.html#customizing-qframe https://doc.qt.io/qt-5/qframe.html#-prop https://doc.qt.io/qt-5/qframe.html#details https://stackoverflow.com/questions/14581498/qt-stylesheet-for-hline-vline-color --------------------------------------------------------------------------- */ /* (dot) .QFrame fix #141, #126, #123 */ .QFrame { border-radius: 4px; border: 1px solid #C9CDD0; /* No frame */ /* HLine */ /* HLine */ } .QFrame[frameShape="0"] { border-radius: 4px; border: 1px transparent #C9CDD0; } .QFrame[frameShape="4"] { max-height: 2px; border: none; background-color: #C9CDD0; } .QFrame[frameShape="5"] { max-width: 2px; border: none; background-color: #C9CDD0; } /* QSplitter -------------------------------------------------------------- https://doc.qt.io/qt-5/stylesheet-examples.html#customizing-qsplitter --------------------------------------------------------------------------- */ QSplitter { background-color: #C9CDD0; spacing: 0px; padding: 0px; margin: 0px; } QSplitter::handle { background-color: #C9CDD0; border: 0px solid #FAFAFA; spacing: 0px; padding: 1px; margin: 0px; } QSplitter::handle:hover { background-color: #788D9C; } QSplitter::handle:horizontal { width: 5px; image: url(":/ostinato.org/themes/qds-light/rc/line_vertical.png"); } QSplitter::handle:vertical { height: 5px; image: url(":/ostinato.org/themes/qds-light/rc/line_horizontal.png"); } /* QDateEdit, QDateTimeEdit ----------------------------------------------- --------------------------------------------------------------------------- */ QDateEdit, QDateTimeEdit { selection-background-color: #9FCBFF; border-style: solid; border: 1px solid #C9CDD0; border-radius: 4px; /* This fixes 103, 111 */ padding-top: 2px; /* This fixes 103, 111 */ padding-bottom: 2px; padding-left: 4px; padding-right: 4px; min-width: 10px; } QDateEdit:on, QDateTimeEdit:on { selection-background-color: #9FCBFF; } QDateEdit::drop-down, QDateTimeEdit::drop-down { subcontrol-origin: padding; subcontrol-position: top right; width: 12px; border-left: 1px solid #C9CDD0; } QDateEdit::down-arrow, QDateTimeEdit::down-arrow { image: url(":/ostinato.org/themes/qds-light/rc/arrow_down_disabled.png"); height: 8px; width: 8px; } QDateEdit::down-arrow:on, QDateEdit::down-arrow:hover, QDateEdit::down-arrow:focus, QDateTimeEdit::down-arrow:on, QDateTimeEdit::down-arrow:hover, QDateTimeEdit::down-arrow:focus { image: url(":/ostinato.org/themes/qds-light/rc/arrow_down.png"); } QDateEdit QAbstractItemView, QDateTimeEdit QAbstractItemView { background-color: #FAFAFA; border-radius: 4px; border: 1px solid #C9CDD0; selection-background-color: #9FCBFF; } /* QAbstractView ---------------------------------------------------------- --------------------------------------------------------------------------- */ QAbstractView:hover { border: 1px solid #9FCBFF; color: #19232D; } QAbstractView:selected { background: #9FCBFF; color: #C9CDD0; } /* PlotWidget ------------------------------------------------------------- --------------------------------------------------------------------------- */ PlotWidget { /* Fix cut labels in plots #134 */ padding: 0px; } /* QDarkStyleSheet v3.0.2 */ ostinato-1.3.0/client/themes/qds-light.rcc000066400000000000000000004572441451413623100205230ustar00rootroot00000000000000qresL@6h‰PNG  IHDR szzô pHYsMMgŒàIDATX…íÁ‚ ÿ¯nH@ï G ˆIEND®B`‚x‰PNG  IHDR szzô pHYsMMgŒà*IDATX…íÎA0Ä0˜4QLÆñH ´U@Xß7“8É8°ÂO>l~奼IEND®B`‚y‰PNG  IHDR szzô pHYsMMgŒà+IDATX…íÎ1 ÄÀÌñ/d<ä ë=ç&V2|á0‹lëH¶IEND®B`‚†‰PNG  IHDR@@ªiqÞ pHYsMMgŒà8IDATxœí›OˆUÆ_ïÝU êET<ˆ"âüÝ™€0¢Ñ ÿ€žD zA…‰Að ¢/ ê),htUa³îN&Ù " ñM k`wº<ì†ôôîÊ{Ýmœïø¼ª÷Uuõ«×30Â#üŸ¡¬X+æÙ)ôJdHf»“åW]¬ûŸ`®Ûkâk;oÔ¨–Æ\¬]p±ˆȮa cÊ“¸ZßÙB>Ðí½ —ÄyI‘+¹ Ûí^ÛW4 \‘´™™¹ò“KÂ0¼²¯Â4po_¹`fæ‡K—lì3àÆ4üåJ€vûøEŒŸÙVK˜œ•|¹ Ýn&6žÜÜç%à¬é%‘ ÌL/ß Üç%Ì̼mr"@'<ú°#Î æ/óg‘¹óaïE°âœaDÞü2 søÈ{’¼¤¾Ç¾7€Ì˜ 4ô^’D)íÈH€NÞ!³}Iÿ‘¥•úU¤.À¡C½ºY°Øç…"³t7)Oƒsaxs0.Žó’"—çûõ 5†áu²`ØçW2ŸÍæ!¥GààÂÂU ¾®Žó’"#»ÍC Ðív7õ—ísà†„É²ÌüYx­€ÙÙÙñ¾Æ¦€rœ×J§÷~Ê[ ¼ Ðn· … Ç?Ý–0™ådóàI3 &6m~ßÐö¤‰m<ô3S'\xxtÐ"¿“Ý¿ó 脽—€ç†-ùÛ<8`>ì= Ú=d}—~\™óÝÞŒw‡ Jw¸Y/ÜU€/4VɈ Î÷ëËG`¨ÌW®óò gž!1ÊšYæ7NÿgÖ«¥O {zN>bú‚Ó 5«åwÌØ9@Š\‹à¼DÕâ.Œ·H”ýìßÁyP’¬^-> |çͤ o?Ið’Iј-?|çÍL"_Ñ[0µZmiÌ–fâ¼r#‚×@jµÚâ†Àî fòí{­ðD¹\þCýÂÝ`?&L’”yCH% õú-¿RîüçÍ,ÈZ„ÔʰQ,W¤»€ßã|Ö"¤úNN¿CÑ=Àbœ_=2g"Bê¨Q©| <, Ç’~%dÒ‰ÕÒ‚GúhJ·2{Õ«¥§† F¦™¾‹ÕÒ^ÐËC†‡§Ì#õÊ­¯o%y¡ BÈ\IV¯ŸGú Î&R˜ 3V†§Å“'6çÏMþ Z­ÖòéS—= ú&Λç¹!7´Zן±¥?· „É[;È•ÍfóÔRAÛ0Ž¥á/wl)[¢­ÀϾ}åR€-•ÊO¦h+âDÒærxÊ­ÍJåû Ï6àtœwù½!×LN–:RtŸç†óÿ/3qÔ+•¯»0ú@_⵬ca„ÎüîPÚ9+‘IEND®B`‚¨‰PNG  IHDR szzô pHYsMMgŒàZIDATX…c` ¨Í|Ù 6óe%f0Q¢™`Ô£uÀ¨F0ê€w ¥ügøï@I•<ôC€‘ñÀ­tñrõxŒ:`Ô£uÀ¨F0àè “ ‹pøIEND®B`‚ ‰PNG  IHDR@@ªiqÞ pHYsMMgŒàÄIDATxœí˜Mh\UÇÿ7Q‹Å¸¨¸É"ºª¯óõ2R³¿H7¢T ºUTp%ZEDì&E\«PÐMWR¬ ¥¶. ™Lf’éª-ˆêF£µ©M£ÉÜã")̼y‰™dÞ$Òû[Þsï¹çÜsϹàñx<Çãñx<Çã¹¹P¼¡R›~ô‘äÌ̶Á®ž!IfM°÷GòÙ÷Úäñ•©ée¤Lß,ì'fÍ‘Bv µ)Ø.[v `ü¯}Ã8’ÐÞÉÄÔLYâ$pG{of.{F@§_W³',fÏÄ;'.ÀDí\^¸o€»bÌ`§.B’ósF0VÊ?PK°æT«û]`ß÷Ć츓à¼fÇãÅbxa½AkR,†ÔÌì78ß.1!휺bK›óçÕ ZÏyØÀ+E{/ Øò(hªM`&Œí}.Ìâ'¹:`Ë£Q´÷Ò©ØÐ. …¹¥ë·= |ŸhÀº)%„p‰sŸZº¾ë‘B¡0·15]púôO»n¼rx*A•[‰ý@ëØ<ƒ{w?;<<ü÷F5uuËå¡Å…ùˇ0ŽuJ-)§A¢óÇf¼x¨çW•u™i²~nìp§F9Rª´òü$mÚÑ(¾)©ëy·´c•Úôë ñ¥Â™õ8›”„%ì<öÆH>{tÓj·b@eªñ<²‰íŒ³Å„–Š®‡ôÂH.L¸Ž]èÞÊàLÔev¸5¦Ý°-gI±åÁ3Q~߉-êî]К¬×5 ¾vǦØ|Ö( oü_’žŒrá©MéŒOÑ %7¨Vg"ØW =í“hµ~Øè‚Àˆ;o¿+à@”ÍV{droÿŠÅ}“7 üÒÚn˜¤®æêtÞøÙáF{é<¤ônŸ­×ïpÁ·ˆûÚ&[ ë‰ÀŒ¸ó3,?V(f{mkj‰ËÙFãî[–íkƒ\›À@¢™¸ iµ > 7–Ëå~KÃÎÔ*ºýaø«[Z,ƒ~h¬|(dâm‰Î‹3ni±œ–óòŸ`©Tš_˜öe›@–ÑJR¯Ä‚ÊtòÚ•;”J¥ù4mL½¦/—‡¯Íÿyé³v‰0’R[ƒO®þñt¹<´˜¶}}+dÍ,˜¬7>^Y¿#Fù𰤾ü8õíWG’‹rá« ·×êc¦·¢|øZ¿œ‡>ž€V*µ™ZšÌ°—Jùì'ý¶e[ R›{Ô4éåR.ü|»lñx<Çãñx<ÇãñÜLü 3xB²3è?9IEND®B`‚‹‰PNG  IHDR szzô pHYsMMgŒà=IDATX…íÓ¡EÑÇNºb›YÄ90I)ï¦ß^ú Iú,œÑæ@~í5…_¥_ IÒw0§ ùFƪIEND®B`‚l‰PNG  IHDR@@ªiqÞ pHYsMMgŒàIDATxœí›ËsSuÇ?çG@¤ :ãÊ-T^yt”w ”ÂàØ$uçÎ.tá°pãRWôþG…>y´I눕ã0Œ+EPÊ«ò;.štJ’öþnroÇ~—¿{î9çûéùýn’&°¤%-éÿ,©´x憮Î?åC”cÀJ…ë –ÏÞKÈÍî¯*ÕןN ì&³VñùMò 4¶ @×]#+¸$°£äÒ=ZÒQ ©ï@Ô“ÕB?°ö¹ ÂåÏxûÝf™˜½lJ˜œ¬``­(}Ýcš´ã5§yeÛSÃG¥ËeÚç©ÑX¯æ5_ ÇK×Ê(¬ò¨Õ(J_&§qÿm†£®Qz™XYºV ÀU‡šúëBרFaóRÁ[ùÈs ¸çP{Ñ!ø1LhžOKˤšåW‡p‡°(Û¡`Þsì šÀÐ’j–_K/T|ÉiÜB?ÐèPàÊÁT\r±5«w\#jÖ9„ßÇp(‘‘Jçõ !Hóàê BÐæ¡ÂPªŽ˜dU8LxÅkú»F5êëKÝcºSóôãhÞXZ¼ÌƒÃÌj !JŽ“`-:2æšß£öN± ¼ä~ßXZ:rÝ%·ç•ŽÊ(†'Á‚˜¿æUiu5>& ¨žqmÆÒ¬qÿÇZV; ~Ì <°JK:.×üÔð üCÃdDÆýÔXó…{«S˜29Ýa•Á°Íƒ3 T©ˆŒK pß!|ZzÇ5âè×<ÐZ­ùBŽÚ”Õ]Öp&¡óɘ¸¼y›SUO@Q ¹®J+®“§¿{Lw–^Èät‡Å}Ïç…õš/ä FÝYÝm„ /z+«á@:*ßôdu; °Þ¡ÔC+´vFåJ-€ê ˆÅ.–yø†0QèÀåÕeàæ!½9}8ïÁM1NE什òÍ((„ÐÌCO¹”ŒÉÕ¼p¸ð¸ªVE9–yqŠêÓ·ŒrXíóÖ‡¢IÆår}:(@°ô!ž¹OK™h Û<„¸f«A˜ÀðÄ5^„'êö¶»f… 3®[­euúô…F„Áž¬n³7y ̘‡ U¦¸‹²?—‚ìk¶B›€Ì¸nUË Õ›X0Ø5ªÛ‚ê«T¡L@Ϙ¾!ÊÂÆ€RÞµ–} ¹P¾>~Ì«0L9¤]oL8“(ŸùÇ¢´Šrxä¿! 8Ó&?æp4“‹É¸\Vå> dÆukíÎ(3àtN›`Øùލ Ï^ìÎê;"œ^pÈqÇöuDäǪž¥š'À¯y5´•šHÇåpÇI°–¡ &¡&§sÚ´ ÷±WC[:"Cs¤brÔÖ¾ªÞEóÀËáE9–ŒË KîžœîÎáýuþRa_**?¹ä®p¿õfu‹ ø™Ÿ¥ÍÕ|Q Á7€…0_TfL÷Zå,!Bðuôfu‹º}Mæ:¢2l„£Àc¯X…¢ Îi“ŸÎfÌ ›Â'­=?Ÿ:¢2¬†6!4À°NºGt³_óqpmÂKéˆ …Áó èÑÍbv5´§bÒïRܯz³º_…op8€ÛyØ÷~L~ž/h^õd¾¨„o©ð­Ï ò„0'€îÝ,Ë^q(4)ÂñdTúbk–_¢ìMÆå—J+ði~J„ö…2_TPÊd®ékv9cÀ«‰Å|Q]Y=`¦ÏoÊŸ Ñt\~Ÿ½\ö°Ëù˜ÿ€y€Î¸ Øé_µLz › |Rº\é1¸Ç¡ö”š…Ûóó©ð¸mÇ‚ {K×Êy1TÈ´jNd¶? Û´ý¦÷ÅöÜ^è©ý\Nv·IJJJÊqå&Ñ—°4Ñ IEND®B`‚"‰PNG  IHDR@@ªiqÞ pHYsMMgŒàÔIDATxœíÒ±mÂPFÑß Á(„ +&¢Ì„1¨Ø‚±D¥õ‰Ä=’¥çê}¾òL’w¶¨‹?oÏÓÌ\·×Ëùcù;âÒÍu–9Î2Çy…Ø ðóá¿Ï;“À¿P=@+€ @Ð  hдèZô­z€V=@+€ @Ð  hдèZô­z€V=@+€ @Ð  hдèZô­z€V=@“œw%¬3sßžîHòƾ³Þ T {IEND®B`‚ʉPNG  IHDR szzô pHYsMMgŒà|IDATX…å—;rÓP†¿#SëH ­‚-P@Â#!dbIÉLX@fXe: í<fXEä¬:ØPÿ–3zY²S0œÊ²î=ß§s¯|á›|#Ý3ãµÁ-‰—¾k êDZ1ãð xáµì3€seb¼î –0a¤Õ…Á‡ZÖwo&÷®0n&æ8f/B¢3Ôp”bÁ휀‰]`´H‰xn>ÂØÍ x-{/Ø(8šG"Œ´jÆq.Ñö›6È -;)hÔ•ˆá¹'—h®&ÇѹÐ:â “àÒĺçÚY¼驯OÞ¨‚O˜Wb \›EðR8ásû³Ht.ôqR^)P&ñl²™æ…Ï$FÚ0£W$1Fåá@ÛoÙAUIÈÙ—›^K !±_2¯2¿U¸v(±icPnàÕ×pŒŸ²C#øQ;_ÁÝ¡ýÌšC8ýp¨ÇE iè n¤°éj4 ú±èââfâ4 —á›ðHàtV‰JÞ¹–§Áƒ¦õ¼–íËðç•(èkYg¹²‹ hZoòEд^‘3,ÇT2¸ïZ7;>hZ$%â¹¥ÓŽã‡&³ÂSs#ya2·Áo‰Õ¢F7Wip‰­*8€ïZWb‹L%Ì8ߤT¤*õÀwEðÀµN<•+’wÚ¥•¸ºÙ½Ð}ÄÇEÀ'Ñ*`Ü‚g%V|×>Aòd{¹5‡íyá~ËB`›ìÆ4ö&×ÉÿÉFTÀvœàZQ$ap™p`ø|g|¤^ž”ؾ_G;‹ÊýïÇiàeÕæ÷IEND®B`‚0‰PNG  IHDR@@ªiqÞ pHYsMMgŒàâIDATxœí×OHqðïÛ]52²Až‚W×ÑÕ £‹ˆI¡0-ˆ(¨CQAHÐ!°/F ¨[Iv¨ìC†ëîÎìÆÞ–2:E™`øouf^‡ÚØF-sf7È÷9þÞÌÛïïÍ ³!„B!„B!„b­ § F4m1ÝhÀ75׈ˆÝ·”HA7qèbg 8Bb ¤Ú8x¿ñ,ßï¶Ë9ðß#ùA«Ýé’e·à”Ô³B‰1f4{}> hù×CàX£’ úQJ½JÑhÃYúöP´ü°ú¼dHžN¥”ïâù¡kÐ pƒ˜ÞþK˜ƒRÀºŠr€y:]dHØ Ÿƒýü"T’*Ê HT€SÚ?ÿ-Ê ¸ø ð'± ° ° ° 8¬PiVÞ(8À²PbDÏݪº=?tQи0f4 :©á^^y»µ¼ŽÔÆûyþ0yî B²[ Ïàl_NS‡É7¢Ýén÷v‰8Ú^ÏÌ4Fjãüifó‚f1§ ×IEND®B`‚ Ô‰PNG  IHDR@@ªiqÞ pHYsMMgŒà †IDATxœí[kpÕþÎê‘8äáHvic,‰Ó–Г†GÛRPÊc€G6áÙ¦…fÀ  8L¦Á±œx )5S`J‡GfHä- @© ±%“ØŽ$óð#$Ž%íמÝÕjW²¤L§Í÷Oç~çܳŸ®îÞ{îpGqÿÏ#щ¿uŸGRÊB!æ©9"8`9€© !°àÇ"²CÀm¡Êݥέdø›ã5"²à"§Ž3Ìn^Ô';C•ïB„EL@± ¥¦¹ï|‡‚»ž[ÜØh‡"SUÏ3ïÕK²Xa‹&@Mó¾sE‘G„2¿X1³ K(+:ë<‹1"  º%^á$p­Mð¶BåNŸ¤©|¡¦1ä,K§…2…T¦ YMU‚"<À”YÄÜF¨Ë¢u³>*$ÿ‚®ß÷]ªÊ‹f™4‘ò2„/9G’¯î¼µêó|bW5î-+;fâyõb‹²ô1 á‘PåÇ‘>€4Ç®H+€ Ik¥&¬Ússù—ã¯ÅܶvwjÀ"yoAäÁH·ç>¬5ߨù Ð@Åï‹? » -*EZÇÊhhFwÞqsÀ‰ú¦¤Uu¹¿0YÛFàEwR®Ûq‹w>1ó JÀ×· àU†–^QÔË:—Îz;¯xã„¿åó* ùBÆ„K¼ÏCÎs¢·ÏÌ5–’OÇªÄ “‡ËIeþ‘zxˆ†ft%ؤk|[&¦7¡Ž\cå<‚-±Ÿ‘ò¬Áû©¤óÀM]Kª‡sST'î„`ôÏòH¤®â×¹„ÈI€×õÍWu€‰cŽÄ3uÞëK±:Ëþ–ØÏ…²ZkÁ ¡Šv¾¶ÌùC÷Œ´Ûõ/>ùí¤ëÀÙù~ó–Þ“Êeªªœ a•T‘HCA7€nQ±]>Ÿ÷€”@8¾"K4ÖR93ZïÙnåj+@ ÛÈS¯âPNï¨õôæ’Ûܶvw²æ272'ïCdM¤Üó4®”t..þ5‘ ÊÄ©[œ¥1o÷õx¿·u¥¤²ùY lN, p›65 ÏŠ†*ßÊ%©`süR àÏ…o‚á/;B•[r!^•¶˜9f¤Ü©÷®Éæ“ý-@ …¿Óš„Ø”ËßÖLW0_KÁ ÿÃÀ·TÊëÁæø 4Ðöµ;T§ðAQxï©cÇdóÉÔßòÙB 4¦$l°KâOôOT¯¸ÙŽ›+(høšÕÜ3ÉŽ›r\`Æ4sxXê³ñ³ Hß©KlŽÔVî²ê|n[»{‚ëЋ ~`—è8pÅdqn´ £³è¾(˳­ LƒÍ^›¨ä)™yÀ2=RF¼kA,´ä†Ëƒ¾„í(Œ”{žј|þþÄ͸¦¸]¼€V±-»Cq«Nýë× ¹Ô.¹BAà¾@8fú0c¸RÒ¤<§5 pƒÕTB.6tú«þf5÷Lb•eRE!vË]jÌùB3Ÿ FgLjߥP™ú«Ug“űú…RI!À<ÿ@|±'Ò[ñí¨Œfäe0|gpkLìª÷í1òÆÐF ·Û%]tPî°l_)ª@t_œª:2ê”PÁ)Ë6#G ÿ@ìû<–É”Ì;a]"`I"t¹+àÉFŠÉ '(‘LŽªr…e{ ¡.·jO u¹«Â“Œ³Ip¶îÅòÝ/‚RW-úfÆoZ‹tRŸ»@ªœ ˜¢#8Ô~›<ŽØäg‚*ËÖI_és§þÙTƒL!{mtUfV­=R° kñìC´;A÷ܶvío2¨ß!ŠY çŒúçUV+2\…ÈL^Áî³d›¯±u¥¤D`¹B,-hY}žýd×NidÇ•§Œh9™ºŠªšVÊ-S {ló,b)€¤&M38 9f|ª÷aÆÌ©*í–í%múv¤q‚Þ]FŽÉBˆë?«–‹ ’Ï[µ—TŲoÅCî²3ƒc4DØHY`äh‘rÜ X¼)J>ÖÍ|ߊ£RWЙ9b2pÈ›´çïß=‰1G×’êa‚Of¶”¸Z. [–ä¨p‘Φ¨[´ vÜâÝÁ›ú¾Ry:8y?€C†–.Ü{`(¹ÚŠôÅNƒ~2XÕ]ñ®‘—í®ÛEqôx:+¢7Îê`]1*"¨È=ÝË?hÉC—³€¯š•ÇMPeF/5vÆyþÖ}–;>_÷q¯YqŠžŽ.õn²$5PTý&M”§Ì¨¦tÔzzE°Ycš ¤ãq¸[WJ*ér_¢Ã29[XÎo¸ÔÙÇù}‰«µ‡0"ˆ×íÙlÆÍ¾ŒMó÷†´n©^ßûM«Ž»–L&‚ùÜAá»)Á¥vÇqsÛÚÝüVç«bu¶Ó¡¬tÖWl1L†n§êXaÕ9Dj+w%Ýî3˜*>>ȳÃCÉ…v…YH}é­ ]¼}¡8•¦lüì#@„„zÁz}MKŸå ¾ïOù ¹üÌŸX y®±›ô ØÜ3“Â{µ6!ê¨õdÍÁö] Ç7¸nÌ@îM)rz.ßÔ4żŠSV€¸€íÉÎa ˆ°)¥ºWíª?vÀž>v»³5æ§Ò;ßê^¡­5M1¯âThœþ©ž½=p(—ä€ÑÒùdÅy€ËIÌ“ÑBʱ‡›ûô‚Ü.¢<ç,O¼nܵÙ!Ž7¸IcJŠ¢.°»¹’ÓjÅß;C([¡»ÆÖH¨bi!$ª÷–Mt«ùiš_8~³kµ6 —FC•ì|s^®ùÃñëÐ߸ ®é}·åûm ¤Â}·A¸ÚùŒòx¤Þû‹\Bäµ^ „ã¸ËÅÂôõ¾Ïò‰U(æ¶µ»SýÞ'Öš^óõx/²º¡E~ ö6:ý‰ç\fhér‹vÖWþ;¯xãÄáyéÏÐßÀGI—{A×’év…Ü1ä¿ci£#ØŸx”€qˆ%!lRSxð“e•‰¼ã怪ƽeeSÜ· y7!Óµm^Qéº:×·Æ×÷–Íß«J2 “û…xL=älÌç¢Îi ³÷¸øbЬ€Iž@c´Ü{W®÷‰´(hÏX—XŸ1#30¿T!/Aä%tlÎWŒÃïõ…\,”K˜-Ó.Ëe¶Ï†‚7í5­=Ç+IçãüÔ‚–ð†ÿFJ‡¨ˆ¦Ý©/’ÃÇìw§ÒiÅÅ)ª$§‰8«‰tPgr>FÿR“-õwT‘eŸ„<ï’ѪÁpâÇ*ø°óŠ3 zAÜ™î]?ž!oDqË6 T‚¾Ä%„Ü ðô¢Æ&v‰"#ί6ójnéþ4Þw²@YL`‘ÁñÄAŒäËP•§"û<Ïÿlû(v@3Ô´öïH;Ï%q2€9ª˜†Ñ߸À ƒ÷B¸“ªìÕ7¢õ•ÿ7ÜE>Š£8Šÿ]ü0úÝk¨–æIEND®B`‚æ‰PNG  IHDR@@ªiqÞ pHYsMMgŒà˜IDATxœí›MLUÇgæMaÓij ‰tac‚ ÐtEbÝ´²©i5¶„6°wc#$å‰VëF׉DS¬B7hBÒnˆÀÆ(nj„.paJZÚ7s\øßÇ oóIx¿Ý½÷Ì»gÎûŸ¹g&÷B:uŽ2âÇ(»¨™¶&úÔàª(]À)à•h]«™]à‰ +bso}‡¹ñ^ÉW»¨jrËzAá+×Cq3.„5”otËû™^ª*¹eÝÍ(g…¯ÖaUõü£=0½Â0ðI¾Å‰Ÿæ–ùØsÜ­3·¬€…’Nå© £›Ç6ؼrE¬p] Æì¬šÏ[97¸(ʉ2“·ÝÒ¡"ÙEÍ´6óK±ì~ziqm¨G¶#ð=t&—´¥ÁdFá­½Namýo:ÊŒ)ÐÖD_YÎÿe4òÁa¹y€¡Ù~iq åé^§r¶­‰¾rÛŠ¨ÁÕ’6dû;äÏH<¡ÙÆ [ÜW~oà€Â:¿GÆæAøîŃi1_Ü¥³ÜÆm8UÜ8¶ÁfÈ~ņ‹ï¯–Û¸ ¤ÂKÛÓ¾\|¯¨^=뀣B&iü0µªí¦Í(p@á¾!L tÉA;õ È-éÓæ!p8èWåÑÔª¶ýýT »¨ ¾Nº Ÿ4ln#ÕhkæR¹t9¼tŽÔ`jY߯¢ž'•È.jÆT¦†} •ïƒÎ•ÊU ­™[à-ý/ÔænйR§¿ÒWeìf¬/Uð+}•¾cÎT¥€_é£ øùàé‡Ô( éœ“ßš7HBú¡¤Àôоf+£E…É–ÁÄ`§<ös}Òw¬€©UmWå‘@?…Z¸nÚ<Ì-陪×'$}‡À(¼¥¹ÖêbòmvQ=U–¤ôÂx\öPèjmâ#¯ñjµ~H¤ïùCP„±‚ÌKHZú p¿ŠIcrÅ©é;W€ÅçÀ‹ýLÊS! Òw€B=>^ÍÎI…´Hß!”gÀú?ÜEY­bÖhÂ7¦0C ¤ïJÆ{%o TIàM”Ž*6±Hß!´U`°[~ÅG*T#.é;„º úLO┾C¨¨!܈Uú¡BM…¸¥ïI%Xk*$!}‡HPc*$"}‡ÈÞü¦BRÒwˆôe¨Z*$)}‡H0Þ+yËä=pÝd±™7x?)é;Dþ:<Ø)óçE™žÏD™Ï[œ÷ûÉ,Jbù,>Ô#[À;qÌU+©ø*œ$nØ-nÌΪ“/¡ãâûn¹[ž7ž·r:L§âÄÅ÷­r›Ê’ÂJq;op1d¿bÃ2¹TÜV©\’+7JÚÜ+i+“KÚ¾{Ñ2¹¤-Ø¥…Xù½KÖw˜Søýÿ«8Ñ`2s˜‚àl–.Ù1.¬­ï0Wn[Óvy ²¦Å|š·Ë[&—°?ðvy‡Âi‘C`@•‘›çäŽÛ˜g0ÐÅUF¢s+aøF7Ÿyø:4…ðeá ÎáÁç¡©ƒ›ëä¿]×i<6·¥Âj-ÇæêÔ©s´ùWÄÔðÿPIEND®B`‚è‰PNG  IHDR szzô pHYsMMgŒàšIDATX…Å—ÙvâF†¿f`PœÄžñ$9YÞÿr1žIÏl#$¨\Ôߨ‘qr—Ô9‰V×ö×Ö X øÏÉX¤| ¬!X÷‘K –%ï“DJ\³dm›¬íô~zò—À:ôÀ0ªDYØ !ŒqÏ¥¬¬•Q¹ Kþ#0–òàÞ²ûDù‘q÷èÌLÊô˾¦€Ùx|”ŽAûNÿ+ص¾ý ÌÕ J\p¨Ï`s Åó¥Òó€'ôxÂãÿ °‚°%Üc)Ïåݸ’Å9”O‚qf$åÀ³”•B8îððdž;(ÜR›À˵ƒ \Hh¤9†¹øú×"mÄ»“¬Û÷JÌcîżsA0ýŒÇ²–õ#ÁVCu Óµ#uŽl!™50•SºÒIöÁ— ´/Imеá–]ÃtåH½I±Î¿ ‰X¶-<6xèú!hJXäxÒü%Á…úAîIg³î?ž7_as)Þ<‘ ,žIB ȼ‹‘ ÆR´b\á‰ö Hs=ƒ º ¦®œfÚ×â=åï1ChLûc @P× Þ¹‚2Û ‚ú»56î{½ýÊŽ|´`{†Ø3^†º$?©‚aRßI“©Ó=Úǽ{cé0»KŠ”NYÉ UçlÇ0‚ñ!ñ†ä½M„Œ€Æ=·5êLxF[ÒRJx5=És†¹O‡žÀ>kÏ–¼¿Ú…í QW Ömõ°gÀ0}¥ß2™'}ƒOFnߘ”¡U)õ¬-úußâ §ybÛ¾¦;D ¯ß­;î‹9`ÊpSÎ4XÀK³”âo¹j6–BÜßJ¸I¶!; 2vù¢;C•ÛÖ$4þßÀ3#!Ÿÿaî8žvÖ%,ï:yÀK8îÙÁIÆuâõ5 Q/?Éós™~´@~€åS·¼*¤7l¼‰_ ãê²c¢ö†ù->ÝúgÁ”"Ôàí6ø³L†W6“1'!Øûæâì†nàøIæw<ž{ t¤êA— õPÕ!{Æ»ŸŽw,¡¸•Ìmjn@xÐÅ|$o6`-ð áå<6w ƒëg|–l$såùÓE1 Aã“-te}r1ùíÌÅäØÛ÷‚m +3þo2y0„qz÷Ëu0n}ÔŽj¼gìÅÛÐ ¦5ÉÕìËiôâ˜pÃä}¢÷ l5;&•”ÝåT¹°óžÃkøŸ¯çˆ|Ó;í2OßIEND®B`‚™‰PNG  IHDR szzô pHYsMMgŒàKIDATX…í×± 0Q YÂÚÚ2xÀÕ²‰’ò®ü ¼V*¦æ¡æñºßš Ã*îi£sô3è|F»¸SÚ­ÁÓsƒo€IEND®B`‚é‰PNG  IHDR szzô pHYsMMgŒà›IDATX…å–KnÓP@ÏM;ÖÑ!(ŽÝ&[`m)¥4ýIe•XÃΔþ?´¡‰5 %Q;€ì_‰+Ç¿Äi&ˆ;²åûî9~÷=½ÿ{ˆÿÐtœ‡xòVá®Àë¢e|œ$¨i» o€ß9O_Í;äüÏ“w €…ZÓv+“‚×Û%…0ÜïædÏÿv# p'0&§p> ‰z»³$ªgƒ,¹P‘À›¤DÓv+a8à¡ÞND læ¯Y‰‘8G¢i»…ó0\ Zš-Ô"%3#1•U¢ü¹@µh§ÁÜ¡(™ù T_&H<þ¬Ÿ‡À6 G£å,#r’ì ,-ã*~‚«Àj÷Ó¸ð‘zîŠÂQœD¯J®Bµl'Ãj$(!â  ªA±‘á™Ç)ã2Á!f¦EÑ2NVÑ88"kYà™<‘_Q…ŸYëejA½ÝYÕ÷ÀtBJWE–ÊfþÃÄúçùeîÏD°NWE*e3=JÝ‘ZW]GdÁ–L‰êe½ÝYœˆ@Ãvç“à¥ÙÂQÉÌ£º>®Dj ¶;\…á‚n­Âá@nË©"rªù§¿&Û‘|e€ûÑ´5Eö³HÄ 4ZÎDjYàÃ$*qÝÈH‚+lƒ­Â¡Â&ƒkbZá²i» ©uÇyœ/[ÆÁ0¸eË8Uâfš-ç"Ÿn F½ínˆ²G¤²P´òŸaàdc7¶Æ…”Mc_…-Â3¡ºë¿Z Á;`nûãÂS%„nD@•mà+ð£¤Þ”XøŽò d{Rµÿýø -Âc<‚&i}IEND®B`‚ð‰PNG  IHDR@@ªiqÞ pHYsMMgŒà¢IDATxœíÐ1 Ä@‚ ffüËÇÐ3ðÍ­}îÚçʆ)ÇÐ:@ë u€Ð:@h ´ÐZè­t€Ö:@ë u€Ð:@h ´ÐZè­t€Ö:@ë u€Ð:@h ´ÐZè­t€Ö:@ë u€Ð:@h ´ÐZèícIO7}LIEND®B`‚щPNG  IHDR@@ªiqÞ pHYsMMgŒàƒIDATxœí×± Â@À}Š@S -¹"wB#äÄ/7ñ¤ÈOjŒì™puÁÞe—°eœ/×[J“W賤ÚZîõõ||†‡nl›Ë'É©”Œó°?ÀÎôhmH2ý¾ÊâjkÖ.ü¿@7¶Íå¿Àw~vâ ) fšŒ¥IEND®B`‚J‰PNG  IHDR@@ªiqÞ pHYsMMgŒàüIDATxœí›Oh\EÇ?ß·šŠm¼X¼H1§ª§‚›,Ú‹´ØJ­)B|«– ž)(b ¶ *¨•fKiÑS-b%EZÐCÉn =ˆ ªˆ  þ«¨Í¾ýyHKͼHÒ™yÜÏñæû›oæÍÌÎBŸ>}þÏÈWCSm{ãE ³„½Íº&|µoZmË €„™±¹9¢Ó¾ÚEâ«!ý§-3Ÿ>ku_í‡Â›DÏy2HO&Ûv»·Œø`Xþk8ÙšµuÞr<ãOÀ⬓˜9бµ²–M w¬2NL~nƒ‘ò–L8Z8$ †“ŽíÿÚVË\á=pæ±yðWŽì9e×Ë]&¡‡@O®ZÃ;fæm r-ŸŒ‚7ž˜êðjèì¥kt×vO·mw¤üE‰%A澯µ:öd¬Šˆ&À hµˆŒS³6«—h4¿Zt%$ˆ£“³v_ÌZ®†Ç§HÂ@"޽׶FìbÊ`Ãau>>tÖîŒYHYÀ0ËK¸93N¨ÒàÞX1>»±ËŽt£æB†–ŽŠ:/Î_JxpÇFý2»|BFnÌÓírÿ®aý:¾\BX®†¬Ë–wëÇ%”6Hnç_[Ó{t!V¥0tþ¤Æi]_Ĭ¥ EŸ“Më:»˜¨s€,ßy“1–k&f-Wˆùó8@"çTOÆSiCĪÃ%æÏã5÷Dӌ҆ÞUCQH…9o4GÊ¿&|TÁú1Öy.Äæf¹„~w•|xávV¡óö¢d~}§¹È£›Ô –»L ÷µ‡s×glÛ¤¿ƒe®€X ¡¯jlÛ ß#å-™“à÷[Æ7è§ÐY+ÁŸ¹×£ø9IØ:^×wÞr<ãó Ú\ÄØ6^×—Þ2àó+põÌNÆÃ͆ÚÛ‚Ïÿ í2 ¼’6ô©·¶ûôéÓ'ÿ "óv~«QIEND®B`‚ ‰PNG  IHDR szzô pHYsMMgŒà»IDATX…í”±jQFÏ'®ÁÂh‹´Z¯; jj ¦´@ÔWÐG0ø‚‚…‚"¦ šàŒ3;3a{­;Eç~f7‹n6ìµ™ÓÞÿ~ß¹÷----ÿkºq«(–‚š@·2¿t! ?6ÉqM6½ÍóSAÍ  pHns+ËN6Éšù’rÀ$90„^Ω¾EÑ×ß^ooïTþ9pN€/þ¬|\CÈ có°iµ×ë}˜Žt*ÿXZ©áoz2ÔÄùJö8c% _¹þvµ´´´ìË­»˜[§«ÅIEND®B`‚1‰PNG  IHDR@@ªiqÞ pHYsMMgŒàãIDATxœí›MoU†ß33©RBÚ$6‘ØUD°ÒH鮲T`S袂šT•øl`‰4DB¨eÅ/ ‚T5lªt,¬6°@©‘1 ²•*8¸6©»hÚ*‘g‹xÒñÌØ3î|†ø‘¼˜{ÏÌ=÷è=÷úèСÃA†lY¥ÓRl}ó eà€<ë©gí³ `ƒ€ef¾Z軎L¦fõ’e^L½Á} à7¼ô‘)üá?kÙ[‰-ê(–á €~W]ó‡~MuõFv¶ª•Ÿ›5 @,9: æÏ¼ñÍOèTWod{«ZY2­5+¬Ëþ]ñ}fd‰n”W‡×k²ë¾:bBŒ¯ˆ5>ÍÀ€#ÚZRøM³t0 –bß[AcÎÿ$+4vÌêý ¤¯â82¤™¦ku’¿E:Ý\eJ_Å…1@Ý¢šr"VÜü¨YeÒWñ~dš&FSúâ ¥¯â†,  à¯R!ÒWq&å€ ³†TƒôUàn>—ѧ–†õT‹ôU\Jñ£—ܶ0;$0¾ˆçà }wÁL¦¦€.À*ˆÇÀ8nñ5_¤¯âÚ,P.¬dm¥‚>I_ÅÕiÐf*´Â7髸»°› æø*}×BO >K_Å“•àS¤‚ïÒWñf)Ü^*"}Ïö¶S! é«xº²‘ I_ÅÛÝ`&S!¿Àìźùý ¤¯âùv¸XÈ墓D´à1€ÇD´(´û—™—ør>`#¿Rð¶mµK8þ³l7>N´:Or ¾oë-̰¡}ˆ¯¸é’Ÿè}' ¨·1”–µÏbO»ïš?ˆ5å-]‘aJ6”d¾Úð ÌESQ—}óœÈ`*Êh\ˆéû˜ 4ÐwÀŸš¢#¢@óû)šÃÒÚã¹zß0pwî(Ý=‘¿@4¥)ˆ>è>yt8òü¿ï½öȱû®;aBŒK/?×Ó?ED ^ÕÖ’Âçüö«aÝÑôÊÌÿçÂâéR>û¹YUÓ)n«Z^êêltÊ;Ï<‡Á˜.²—š´œã·ª•¥îžÈ/ Çþ»7”#…'KkÙ¯[µ}mÀxýÔuè®ÍÕçùÛí\›ëСÃÁæ?0 òr.Ê™ÑIEND®B`‚‰PNG  IHDR szzô pHYsMMgŒàBIDATX…c`£`Œ‚Q0À€ÆPõj?Ã:Ù»çvš¸+,£'Í£`Œ‚Q0 FÁ€ŽÆ ºµIEND®B`‚þ‰PNG  IHDR szzô pHYsMMgŒà°IDATX…å—]h[eÇÏ9i»²1E VlÙ,S±dýˆIš_c"²u‚""s0/¼„/záp¯”¡(‚(zk±U§Çv³%iš´5B¡PAÓÖ®[;Ö$çïEš’.k×Ô›ýoÎËû>çùÿÞÃ9Ïy^¸Óe•Çã¿´Ê—?bÒa­@ Èf1'ƒ¼ rŠtwO×`tt<ì9 ½›Ì;jf'ÂΟ«H$wåq?Æìå•©yƒIgd¾™zËÎg³YÏš•Ón9vÐD°«Ý~вs<ÙûWÅÇÆö¸ø†AŒ5°¸ðïç½½½¹ %9ñÔäKH'6°ß±üáž@`bÓñød›\/4!ûV¹ë¯F"‘…ŒoÖôôtÃå…Å€ãÀ¢ãñx(Ô•º-@4ÝiuÛ.í†>ºÞ23¯ó¢$Yll²ßLï!2.¹p0œ/qʈê¶Ú‘jÌÌL=v bú £%o¾¯$­Ùô€X2Žÿ¸Ê­Æ¼b{}Ý›‚)`,1þ캒{ªpïƒÁ+Õšå÷û—xÀql°ô)¬$‰û =qy{½ï“Z™ t ¦{ã©TG@ÎÜC€!¾÷ûý˵03  {¾ ÀÄS…EýXkóŒŸV®ûË0k-Ìè·­²÷oÑR@aÒ3›g‹Ô´cG!·qñE,«ÿ£ÖȘ0ÏiÞ*Ç¿¯^-æž+Ö˜UGü`Æ[àzNÛÊ0³ê[ί ŸÙ*˜q® kÞHDÏ%‰ºZ{K2‡0 •<Ìç€&º×j MMÚ¿†ººV{ƒ5_9ê£h4º³VæétºÞ‘N˜Ù 3Ó-ÂÝÝ£À׈{Í×ø¥¤ª?SI¶x#÷!ÐnÆùP ã»Òõ2W¹Â¯ÓÔ›x¿IOŽ¿ ¼a0·Œ÷Jéîa–ìâØØ×ÜâÁ7×|Çzýþk•˜§Óéúk7ò§ ½,aÞ“=@âæ¸u›ÒØøøCäf1Xºrù‹Í4¥£É‰'ëÛìLÞ*~ö<•JݽìÙ§À‹…ìdp4brÎàj&·ä›olÌzù¼Ûœ7o…Ò ÎYËûŽ…Ãþ?×óØÔÁ$–LîC6öÄfâ”D$ØuövÍ.¥R»Íãˆa@­˜µ \D##Ù6 vLU’÷ÎÖcrEÿOIEND®B`‚ ‰PNG  IHDR szzô pHYsMMgŒà¾IDATX…í—¿KqÆŸ÷®"°ŒõÌ–¼©³Æ‚¢¡Õ¹± p jŠêO¨ !ƒþ‡VÁ±† ¨5,¡,õìqAQxoC'D^ÙiQ>ÓË{/ïóá}ßá¾À½OtÉý#:Ó" ðØäSdæxå2}¼÷!€$+óZ5³I:ƒòéäZ€?88 ÒwÜxN|ÔÙìÉ®ÝÝ}®R«S €“ ½Ÿ¤ ÷Ëaö÷*“v˜šI’¦ür˜¥ /ç„r`ì⣞¨€Øü”0¼†*`œ]c7ÓE*um„^3€Q ðãMŸH½3ļ  ý‹½5&ZÊŸmZYNÀÛ£øˆy£ sh'昷GñU PY¨™d‰fhUôÖ˜hNÍ$ VEŸÞ€±CË=~G¿{ €@½ŠÀëd­Ì¡ÛÕ f>€R«©@é¹%bx–sb9pº¿©r¹°jU.›q¹À9µR[‰¢’Cn"-7Û¾`k8šGì\œyp˜€= Tè<Ž“€Ø‹UßoºÖÙÛ7¢ ÀwÒï#v®êûË.µ4ÖÚ ·8DIÍ‹ÑËÏ»ãZ?xaxÂ!ý߈}qT Í!Fç’˜‡@rй§Ö’ìqæaD-„0lyÖ°HÆæÁá)°Ÿ‚b¾Ø9à¾Cú“‚]h¬m㓚76Ù™ï×ÈÐÓJM£æ)LÂ(æËeÏå·}5òôTõýe1z÷I¨-7Û~ÿB¶<›ÃùÌ«òÒ¸æ!… èiu}ý‚Z¹|Ç!ý_Våât)í^[P•E”§®} Êå T¸=VûJ Œ!—³ö¨ÌCÊ A7µû“Ë‹Uêæ»=d 0\ÿ‰5r·IpÑ—ªÅüSª÷H™€T!df2Ðh¶~*ÂÂórÅŸú ;ö*SðÂMàñnD]·ÍÜ<®Œ«.κl©È–™ËÚ<¤ð"ä$ÉmŠÈW®¼èWHÇ嘱•9€ÆÚÝ)a§®êôé-ªL¨Êâêúz!ËÞ ë›à®y§G* |.¢/T<¯•rk{¶ÈH]óEà™± Ÿƒ­ú~;ξ©LŽÀæÆùTÌÝWdS_‰¢üø *uwšçsbë8˜‘m`;¶¨ò*‹Y@H@óÀÃNÇ^1—€/ãÓåé, ¤ Û“IÌ«òÊtÙ{¯âO} b_&„ÆÚÝ©1ÛýoÅ4Š„a{Ò]"ù TXÚ\¢Ÿ«šwoÇ—ÐÏ”S³AñüÆH ïÑØÔ<È•~óßßZ}ÇIvêiLÂXvÍ;=È•j1_ß/aºì½— Bgìã0òØcþ{éEäjÅÏ/ºÔ^ן7FÞâ¾®ðÏŽšÙ ¥©».µû5€ÕVëÇÚa 7ó["rÅÕ|O‡!1€Ã0ßS£Ùšá2„À®ù:ð¬CúXæ{J ÁX™)—óv­ï|LnÞ:ŸùƒÔ}bÈà¡Cú3ÖèR¶']ë;M@#Šž5K$2ï/¸6ᢕµö,èÛ¤< ±Ô¼Z½–¶y€j1_‘«¸OBÝeœ€QÌe¯æ;²V£ö ªú6Ýo±ÇéSceö IØ@#ŠžLåûm©r=(ærÇVR’c¦R(üeØâP Ío«rí°Ì÷”„wZ­æ:48>ó=­FÑEUón>‘N®T©œÿûÞàÀM0×áuþÌT|AÄ^¶ÒŸÕS;oô‡=žw(¶ ‡wæRÅ÷Ôê5\ (3ý¡a:1e¶ëÕbá–S‡‡  ìÕœ ¨ x ÈA¿ÕmA^=Næ{ Ê^M•ëA 7ûcƒÿ/°³uôoC®ßäÕJ1?Pä¸((æ÷… |ôöFx@µZý,§@à·ÀÇÀ'‚¾iU.gó=¥Â¼~ðàcE~wÚØªïûŸu':Ñ‰Ž—þ0Ò1dºoQIEND®B`‚ljPNG  IHDR@@ªiqÞ pHYsMMgŒàyIDATxœíš½nÔ@FÏ]HB]ÞÄ‚Š7 í’ðã4PÐÒÑò$¶‰ÃOØ ÄÐﺣäÒ $öR@a&v6¶gÆ3^ßÒ¾þ4çH® C 5Ô:—t½×•åzx†òáQšÈ×âý‹­ËKe £#”WÀõbϨƒuy©l¡c(ÀW̾^ (ƒ~/á©ÙÛ»W  a²ŸÈg³¿W›àYði"Ëžé€&ðÐMá¡ÚÀCäÚÂCÄlÀC¤lÁC„lÂCdlÃCD\ÀC$\ÁC\ÂCà\ÃCÀ|ÀC |ÁC€lÃg¹ÞBy 0Zòðþ5ùT¼ÔDÈ:üß™à1° l/GLÍž`8?wª‚à ~)Êc³·sáï=¸*ÌþN7AÏðGeÏt& xèH@(ðЀàÁ³€ÐàÁ£€áÁ“_ðwÓDÞ×Ér. dxp, txp( xp$ xp &x°, 6x°( Fx°$ Vx°ð/x…½ýDfuóçz[…à`&è~§>üÁ\÷T˜áb&¼¯YÁØH@èð‡sÝ-·3Œ^…7fž•™`äðíf‚¡Ãg¹ÞAykæ­útžK@_áá¼r`7Ý‘ãÚy-àa…€¾ÃÃB‡?XèDà™W÷5*°.ðP"àßÙy†%øŠ¼ÆðUyM7ÐÿLsÝØT¾— —ÃWä5†·ÆIpó„ ÀVáR«ßRJòZ-ÖvÒòS„'À/àDaÜæŸ3O”I›ÅÚΫ¬çßtkšëF¨yÙ½d3o¨¡Ö¸þÿ0¬ôwVj÷IEND®B`‚"‰PNG  IHDR@@ªiqÞ pHYsMMgŒàÔIDATxœíÒ± Â0DÑ3CÀ,Ä ˆ‚‰(Ø…è¨` (Q¥ÎCâžÉÏ_IªêŸ uñþú:ä˼â|?ínbÇF\š$óã·I¶ß€ 0=~é¼*à'4€ 5€ 5€ 5€ 5€ 5€ 5€ 5€ 5€ 5€ 5€ 5€ 5€ 5€ 5€ 5€ 5€ 5€ 5€ 5€ 5€ 5€ 5€ 5€ 5€ ±cä¹t^üÆ1ÉcúÆÑí¨ªö¦øTºIEND®B`‚Ÿ‰PNG  IHDR szzô pHYsMMgŒàQIDATX…í”±JÃP†¿‹NRœ-.BÛ$ÒŽnn¢›à‚“‹/ ˆƒ“`ÄÍYÐ-¡­É ˆNâ¤V,mŽCi n¹,æ›îsÿó]îåBAAÁGÒ4«ª„í8˜´tƶí¬VÊþ‘p7‘wß÷'r‘ÕÍa=7¿ÐËUÀ[²¯AކuÐzŒsðÜÚp7¨¤4£STð/A+ÒáZÑÝe×>ËU@U­°÷Gab­6œêCš £+”¤3Uš %÷i32 ÌBx6ÝŸI@Uåë§wTÞz’TrÛÑ!°tT­õÇyI›aüƒæã6"@lxnýÖ$Çè+ ÛñàžçÖ/MrÀà Â(ZTÕ {NýÔtxjUíãe…«†SÛÏ2§HË+ðôýYÞ‘$«@AAAÁ/çJe· Ê„!IEND®B`‚†‰PNG  IHDR szzô pHYsMMgŒà8IDATX…íα01.cPS3#£g(õ½}ŠX”Õ“Õ³ùx›ñEH©Ó+/˜IEND®B`‚̉PNG  IHDR szzô pHYsMMgŒà~IDATX…í—ÍJQ€¿3ÙÒ‰ù"™RÑ ½B ¹ rÖ#dÐ"ƒ6ö£ø ¾@PøÓ‹¨„KqN gl(Ig4h¾ååpÎ7÷\†sà¿#ßÊ-ÝQ8’@Ô£:] .PHÇåy¢ÀCSOE¸'æ¦ùô¦\ý¸Õ]Cy>€Ü Oíh[Þ½¨z÷¢ë+«¤€"°&°gßDÈ2”¼%”ËÄ¥êEaëCªå–ŠBÅjñ€áˆK úÔ¼,îDL+·’°ÏœQ‡­/&ämhÂÆ8¥KM xlê1ÂþeîÊyfKnÝ‚\o T×ÂÍ ÅÂÅR]c3 ,WlRÚ('@o†Ü=”\6)m· ©oÀê¡kçáo· -Ð…áéW±JC#(qukzõ5¬ÜBÃ>ýˆ û@±ÜR“Úh†›“JC#jR¸L‚£îÖbRÀ¿·1y1±­fJÂ9½Î…ÒAhŒ[Í>Dr ´PéIEND®B`‚h‰PNG  IHDR szzô pHYsMMgŒàIDATX…íÁ‚ ÿ¯nH@ï G ˆIEND®B`‚”‰PNG  IHDR szzô pHYsMMgŒàFIDATX…í×± Qb\‚šÚÜ„Õâì¼ëùy-ÊZµšlLhØð> @€ @€ýŒÎÅ×]Èȇ̜ÚIEND®B`‚˜‰PNG  IHDR szzô pHYsMMgŒàJIDATX…íÎA ! DÑÒ ¥6µ«šDÌ&½üwŸÉ7DÖŠ¬¥|¸2þ@´Lõàû"«'à˜o¿G¹ý$˜ Mòo;¯IEND®B`‚W‰PNG  IHDR@@ªiqÞ pHYsMMgŒà IDATxœí›±NÛP†ÿcK"šØÚ©R öÂÚ¾E¡*RéVÔJÐŽðí •JÇ©•€· +‹ b¨@b &R*e K9°Á±’R’ãˆûM¾÷\[ÿùsoîr P(n3ÔÉ¢œmgôžfÂsL]UöïÔìcól˜ÖKŽSm÷R;(_°g5ðGŒ‘Ù#ð Åã}g _±®““ƒF¥¶`® úzÉW/;4ííÓ¤`«mLF¥öÀ›È¼ â_Ü c"4nTæ 4΃é)+š3*5xÀ[$ì„Ä/سþšúCŒ…âû3é#)ƒÌ1ë%–Ü &ôúxßù[ÈÙvF¯òïË3ÏåÓú™]>Üóº(úƹÿØÐ€F€"gèQôQ‹¾¨Ÿðtø˜Þ÷[òP>ÜóÀô!`R•§¢ëbøW]€ëoû¾Ä;ppƒ1í Àù=þóÒ毂ý|h<º É3xh€J]‘ÕCšsà{Ñx’WcÚ®ºëÉ!ví'p«PH F -@e€´i”Ò¤QH F -@e€´i”Ò¤QH F -@e€´i”Ò¤QH F -@e€´i”Ò¤QH &É€zðÀÜÿEr¨GãI /ƒœë†¨^ÒœÅãqv‚&z†{ R ù9øðntAÌbl††–Y˜˜îŠ´`ìUŽ3°]3àl˜Ö ¸¨ fÐÊ݇}wF<É3x9ûÅÒí (9NµZ K×È5ǬôÇq sÌšÐuÀhhz)©…¦UBd¬/ˆw‹¸Ä¼Õ•ÒVEÊ Mçü3o5GiÕÛwÞ!¡î¹UÇ{Ù¡y£RšM°˜ÈJã6 8ñ÷¤U/;¸€Eß4M½ÒÀŸú­iŠ"@K×oš ‘³í Uyê¼ÞžÆýªë¶ÍÑÀ» lp†6:i›S(·›¿ÄÅàºC–8eIEND®B`‚ù‰PNG  IHDR szzô pHYsMMgŒà«IDATX…íV±JÃP=·É&ø‚ÒÁ±4´!AüQ¨tñtD\:up]Š_àd¡_à"H¡‘D’ÙAJÄ!éuh“4i“&¤š¡=Ó{y÷ÝsÞ}ç],:(jÑ4Í•‹öl¤äêÑ`ÕeYþž)`DÞPLI<‘Z`kÛ‘ ‹|ÞäP´!6œI¨ Ëþ7 8C1"Ìwç[e)Ò/³ êOËUÁRÀ„4M[·râ.Øÿ]Õ“°$Ä< †ùööÚ­V«v>cu5óŒˆ/§ ‹‰'­CY–߃ º¦v¯@}1kD|•‚vlï[­–wƒçŸ§ ö‰Èç7+qƒ½Ó’¿ëp7 {ÖÜù0W'™€€”²jº žu {(öëÊü.,d. M׋BSÕfÄzÏdSF;K¦«ž…€7ãÄÀXûUuãÀê<˜˜pT)IwqbÝ ð0rVÎã{W` §>Ò²ãBQ ýÄ¥Ð'[(Ðð•—Á0˜¨¦ÈÒu½ Ž_ø÷|Ê õóŠIEND®B`‚ ‰PNG  IHDR@@ªiqÞ pHYsMMgŒà ÆIDATxœí›}pTÕÆïÍfc€ˆVÄvÔ‘hetÍ.D†FcÛÑZ?‹“*²QѪ­mmu¦¬:V+RfŠ˜O`Ú®3ÊÔNQfD,cvʼnÇV‚Rj«õ6‰d³÷íðÞ»wï½›ì2Ê3“?î{žóžç¾{r>Þs.ÆaÆr(iíÕc u 3€Ó€“ŽB9R La/°Wá]QÞᯆÉÖ³dg©µ•,íI=“˜ —gÑÍN„˜´Ç¢$ED‹©ŠU•ö©p§@}1} ô™Êò´Áº#’.¢ßâ #©õª<¤)–OWý K›jè,FwÖtéq¡ \çA3QþlÁ`‡Âße„0ا!2:BUH˜l*ÕLWåa30ÛbîíO1kY½Œä«ç€–¤Î1”­6m³›j¤+ˆ¨¶¤^)Êà _ ÂwÁkjð£…5²9yM—W^FŸÂ”6nEde¾:y0Ú­þ¬0ÇB^‹Ê|?!«“Z†(7Ëú#Üd¡ã>4ÂL[p¶ ¸ñóÎm ê¬/¤3J³Ÿ€õ¯ëÑaec_ ¹:ÉïW'u‚/3Åo€w<*Lɤ¹1=oDø‰íV/š)ïxµïÓðð~6_÷Z ®+ͪžSwS½|FîuG¾µ«³¶ný2p±Å”Ng¸ÏS ª òPçÅ'æžÔãß 'ö³áíÏÅq|ê¾áÆu¦Á<àóˆ)›×Ê¿¼mKp ×û‰/D¹§¥[]_æ$ƒò¤Õf˜4ºqÝÝ‚Zñ¯W'u‚<èÅ)& a¹ßrWÅ®Yá·:9èÜ®±Ï¥ôj¬îp,”JƒÕļ»jHÖ^;y¨š'/'æ~j°ÅôZcDÞuò ×2Qnó•\d(ÜîU>:eÚ~8Sró”9P8ÓaÚêäX1PÍ×€c½8%ÂŒ¶.=Õ‹ âЮœáääŽÂéŽç·s8v\íS^:„˜ëYnÚµ ŽwÃ-Ê4G%ϹŸRg= šû?m+whW¨vrÜz@•íYùħ‘C9øÙ Ê ž„íUNŠÛ4h#eLòæØšU uÏÖˆx vû¬;Áp¼O­|nıA …ÈŸðØ‚áæãPA¡|¼>r§Ál>ÿóFÌÜns£ûlÏbI¡xfŸÛ·P¡²˜†Δa+'·(öŒªp”ˆø -Ä;#•Lv˜ö99¹ëa—õÙÔÜ‘Ó>Ÿò’A}Ú ';*ô;9ncÀ›ŽJž‹ žò*/)|ÚVáÝ`‡“ã6€ÙElI‘ÜFRlòÏ%Ä®¦^õ"h®öœ“€ÁJ^¬çïç®éÒ¼ÓÍhf¸ÝSjið¸WJ¾YÕ@¹Ôf¶8y9=[{Ùj …Ž0˸øÔ[oQñÞÄ´ýÀ‰i j°¯Qööï%éäå›Ãm»(U.÷jlá¹òªwƨ˜Påî†Ù2äÅìšUxÖ-=õÀÁ ¬À…­½ê¹ãÛ5À ”ç<•k›¢ö3A'šU û&M”7®kF=6YLF&ç8܆eõ2B9×oyñÆ¥‹7øÇU'¸û!Ìûý)ÛûDþ¬°ò+{ÛÜòD¯žäÕpÓ9ò‰©\B ‚ 1¹Òï8.Þ§a~á¨ûh¾Ó¡¼hŒ²Yíƒa¸Ìd©ŸÐE3åBÔ‚{ÄLjßMHSç—˜H qömïG™ý¬ÊÇ÷8¸ÛfT´÷¨ç²=¡?Å·~†Ëò3(>DX‹0ÏoÐXÔ)¢ö#|X4Gòjr8Ú XÃÞÉ òktnשæ~–ªÐøŸìdñ)ʪaƒoŒH é5Þ§á!6ç[̯ ¯{…¾èÜ®S3üg1oÛwÞvªì"²©ó°ÉÅÀ\„d)_-þa(½OVVò¼s׿‡¶„®n²˜ÒÌñ»¹è‚D[Öb²û°ÖX„ëÇsA"¾M+ß?³@ºêëÖ›³Ú®E¥Å¯nàKRm Øn\(<>©’[ ýµŠU•ŽnUåQìãÙŠ¦¨ü0ˆÀÙœ¦¨¬UxÈj¸a`ˆM«“:%_½R!Þ§áö$kTù5Ö÷PžëOÙv½PP:kÒNîVái‡ùü0$Ú’:£_ãAçv:8ÄóÀ"GÑ”s×' ¾(kÙ`5+8»XZ•U¡ î_p¶ü»P¿ÚÞ¦•å|å.„£­e ÓµAgóUÙö„.RX…#1)Rx¤<ÃòB.,z¡ù TEL`iž3ÈåwrgÐûDVŒë²t{·Öi6+sLN¡ò± ÏÏ„3l*4ñ> ï¤Nà2®Ü–ái%AFû|÷uùÖn=Q„Àwª8‚”’ɘTe &!ªMeº(ç!\éá3!Kb5Ò3ýÅû`"¡ß2á—d¿ *%ö(Ü;i'OŒ¥Ë;QÔOfšU“»¹Â4¸ ˆÓ7ðŽÀrMÑRèM/”n=ƒØh6iúݼOö£‹ŽQ^Ë÷~84ŸÍu뉆PoÂÀi’Ý®NŽ©ð©*{ÞSe;áŦY¼ù¿pù0ã0þñ_³tæ9Ê=÷IEND®B`‚̉PNG  IHDR@@ªiqÞ pHYsMMgŒà~IDATxœíÙA À AZ/˜FRT•±M˜1ð_6Ü#4×»çzw¹á.ÿõ€šõ€šõ€šõ€šõ€šõ€šõ€šõ€šõ€šõ€šõ€šõ€šõ€šõ€Úñ®òxý5>Fÿžø>œàÎÑÿ.ÚOhIEND®B`‚›‰PNG  IHDR szzô pHYsMMgŒàMIDATX…í×Á Fa‰Vqæt– }\¢6°”Žï¾«"”LÍ—š¯×ýVÏöÈíq­rô3(|F2“;…¦J ÑLZ´IEND®B`‚0‰PNG  IHDR szzô pHYsMMgŒàâIDATX…å–±kA‡¿·§GD0œèæÀNn-‚ˆ¥•eC‹³Ê ÇY÷ˆ…Ø‹ $$!V^a,’tV–‚Eö‚ ç*¨rÇÉî³ØU¯œ™"è¯ÚûÞï›7o†ÿ]â»àìÚ›ãß´¼¯*Úm†•?ýȧù¹õÞÑÁPžÓ‚å¾Ìg;»åÁPžlòüt´4ú®s† ÷ *Ñç䡈^!;P€hõÃ] •ÃHv=˜ ¶ÚkƒÞ.<3L'oLΧ ¶ò~Q”ûyd¿òrê@m­· ð¨ÍÁ¡Ñrr‘L;@©0v6·ˆV’ó ÀTal5ñ¿“ñœ^N"ÐMà˜/s0ìÀ™¥§Òm…Ч¹1€–Ò·ÅÅÄ{“éìüô÷,#IK—׈çxØ»yâ]šÉ¿!¼AÚo…1È% âªHÜ _’É0Dý@Xˆ[ás ´¤(„Ï:§t¯W7®Õp†pna·9ó´ý«Ž:AL´‡q³úä^‰S'&¢øÆÉ;Ky¤A1€ˆÆÓá-Uy‚íÉðs¡Ô%-W’°e›êíF{U?;:2¥W€`§Qýz8ͫҾø¬ýïê;j·û29KIEND®B`‚9‰PNG  IHDR@@ªiqÞ pHYsMMgŒàëIDATxœí›Mh\UÇg2¤-E[u^l5‰DãÌH±n$ŠV‹X´‚®Š( ºAªXAp!¢7.,ꪴ*‚1™Qf’mÞDi?¢53ï¸ òî šâ½÷]uþËó‡{ÎýÝwÞ¼úꫯ¾þ*Ëû£ÑêjTª¶ Åò“¶Æ[¹T¡tÙ5Bîþ¬7‰µcçm âRQ±¼d’ÔbiÎÖøÖr¡h¤r ÂQà,ÃJlåvDÅñ ¦@¶t»¢¶ò¹ £;ÎE¦€m®s`ËØØFÑöÛÀ˜|Alv–µ-o*ùS+\k8ŠÅKÏT(¤ð͉CÀ­é°:<`¸T~V”»°‚8<ð.VSx´‡å|ò1€¨T¹Wá`«ã«†ÌŽ@T¬Þ¼Üí¨—•_S&†‹•ë=Ü_›]Þzä@aûåW* ËûäÁóP’£(g¦ÿ+¿&o¢í—^„LçVf“OÎ/Uσä=àÃJpØæ®GÎ‹WlÊ¡ï€\œvTÉxòàÀÖ­W1Äï“ÀÃòÒå­GîLLäÛ~~áê´á¾¿?¹ºrÑ×'^öñ`V~M.v€DÅÊsÀF<¨•_“uQ©ú8ÂÃ=¬à&–DÅò} O¥£ _nNWÖF+y©Û ëÌ›² Ç@£ów²yzló çX ¢Ò5c â'·¿’µ[õ¹7Ux ‡eå#¦+Y]¡V½ö¢*û{XÁB°¾E[ µ /øÈeC.ŠÒV£öÂkF\åûGrUPo¼Kà-#.6¿íÛ»b¦§WueðvÐÒ†Ái!q<½²¡“¿a&íH0ÇÁyÍæg?´Û†% ™ÿ’—Uø®9ßêäåz…¥´#¹¬!xÛ†Ë_Ì.*rð}ÚÉ‚×s¸Ü˜Ë 7+i';Þ/¢ãõÚÇHn¯ÀjÚÉB&7q\Ÿyt½_ž¼BÈìQ´Ô˜{¸¿ÛñÛ#dú,޵Cˆ<‘Ž x|yʼ‰ë³Ï <ßÃòR[æj¯q/Ýb’xÛæ{&¸s¡€cÇÚCŸî>4§Â4›ÍßNɯ{€O ËÙ£1('ëõd7ð¥|ÁXnÌ~KÒÞw+Hñâç_©tv!œìvíµÌÁhÕç絓Ûò‹aý?þe µ8ó‰À-¤Þì}o ÀÒBí}¨ÒQ¥òk ¯± IEND®B`‚ljPNG  IHDR@@ªiqÞ pHYsMMgŒàyIDATxœíб Â@À=аh€†h¸"ôA1T`hâI‘?¶-á™puÁÞ&ÀÔ2¸<Þ×´LI†ú¬¦*sR·×}xþæ§îòŸO’Örn­M˼à`ú*c’ÏöUÖU•¹ªÆ½{Ûû-fÏ5ÔIEND®B`‚è‰PNG  IHDR@@ªiqÞ pHYsMMgŒàšIDATxœíš½nAFÏu¡.‰KŒ_ %/ACd“€D›‚–Ž–@ ±NøqAxG"¥m)‚!‘K‘fY¯cïîÌìÌÚ·œ½ûiÎÑìßhaYËZÖ"—”=ÛÕŒFwUx.ð£¦úø[·þ5~üJYsQÍÞ°¥Ðj œ‹¼îÄ{j¥ÌÌA]ÀKŸ£*7’}•üÕš>KöVNÀTx•öÉ^ýK²¿R7ÁKởÒΩŒ€<ðPyᡊÀCàŠÂCÀLÀC LÁC€LÂC`LÃC@lÀC lÁClƒçlÃÇ\Àƒ§\Áƒ‡LÃ7¢ñ=} Ð=îl~Ž÷jCÄ4|³7l‰è°¬+DÉoØ€OÉ›(/8‚?WäI²·tîàyxÒÙø˜ì/õ&è~³ŸvNi|€‡’ø%ð  ð  ð=]Á£ú <8X.áÝúAÖ<«|‡‹B€KB B‚ÃBƒƒB„CB…œÁ‹îöê‡Yó½ñ¶ /Áž ÷ðû£]A±±'¼ò–Œ¹xßí¤À›Ù Þ%óŒì † Ÿþ•8÷ ðþv4¼Ÿ?ëy®<"ï'òæxo˜¹\Á ºãf¬—ðÇúQÖ¼¢ðp‰ïá÷‡mT>$ó²^F©`%9Ðè· ÁOÉË ñn?™—÷úß ØŠtõ§ŒÏ€ë±áÜðSòrÛ΃ÄSàìêé °*ô[JJ^¡ÉšÎƒ„€ÓG·~£òøüR‘V‘r&òv‘ɚΛZƒµ­HW}Í»ùæû5“yËZÖ×?ø¢‰<$ã……IEND®B`‚‰PNG  IHDR szzô pHYsMMgŒàÅIDATX…í–±KQÇ?¿ä¶@6›!P¡ôì–@'Jÿ…¢`qqp0ƒ.…".NŠ.âtK'…ü.B tl&iÕ!ˆ˜K;´¢6Ãù~z’¤¹x׋É|§÷Þýî÷ý¼÷~÷ã`Ø%ÝfеD£!ëÀ´ÂxD£ªÒ¥ºkg¹ôÕƒ™b-ñ·!e Ÿƒ*êNy1¿¨»÷Ú ›kÝ›øÓ`î™ÎxcË/¨ýÎS]ëå!ÙG;åîv}ÑàŸx–?}ëuûúD¾¾ì—ĈšxŒÊ÷䓯ÌÊõØç=𡘊nù%À°׿XùŸorcgAî¯ÀþT›>v2¡W®˜v5••Æ-/Îë“Aƒ›wÛÒõTÙšD„7@Ú›_²@9,@K£9Ê¥|‹®]Ïó",yó˜Jà¯kàŸá`0p€(]ÏW*ºeœn­»êrJƒ¨\ª»ÖwªÂfóñÝú­ì‚óHöÂLaþh1õ9Hló ìõÂpÕr÷ƒßË}üŠê.ÊêñBú$4ÀñBúÄXîKn+ô<¤¯ßû‘Km„|wÈu´Ô‚Ô¹ÓËIEND®B`‚.‰PNG  IHDR szzô pHYsMMgŒààIDATX…åÖOoÚvÇñ÷‡ä°UI$ Gàèäì)LZ› &©T%é){•övìm‡­[´õß2õYôž&$i·@ÛM bm»”ïÔÄ`cìŽË´ßÉØæ÷~ùg0Àÿ}Èß(–ÝO }cð±Ì¾j6jϦ*”ÜU“¾ü-z_ž×ŸdüLúXTö‹ew}Zñbyyi_P\Sæ[ÿØ€ñQà=C¦(–—7 {8ÒºÐ= 7MD±ì®‡âÐ3qÏ1ãoxÝÖéBÖù ±ÂÕgC •…ë¹W—Ýöqú¸Æ‘m]œÕž„—ÝV-‘I‹x82ÙVó¬öSðÜFF‘?‹BÌ-æ^zx„Sq×è_ùÄx$ hE!„VãNÅ]“…â6.>à#æ³ù:âf„SYº%Óãˆøæ¸x,À‹EäO½Nûä*Nê8IcG¡âÞÁ´Çð'ú‰Ûcâ[ͳړæNˆAôÀ÷%ŽÃ„[^§}8¿˜k€FŸÁ‹HO ê#ˆAÜÐöEý(q†—3Ùèeþ³ˆ#†ø3ít©V PZ®"{ŠzŸÕ¹EçÄë´N¦(”ÜUÄS`6°Û_‰ÀW”ê|Ö9öº­Ó$ó&ºý¸BqCw m 3ÈžJËÕ$sO\BÙ]Aú9*~Q?Üóº­s×7‚;”l%b…²»Qq>¿¨íù;b«óYç$10!þÃèù}DþmZD$ P^º ÚO¿B´Æ#rÇ^·B„ý8¡¸Œ/šññ b!›ÿñÙ0BÕ(Ä XYºü?o}?)îˈÀ)¹Ÿ‚žýÛø0"÷R±°è\vZ¿öwøG¤ûá¸í|HÜçÚ™í0üœ˜µžÝt5,ø—ü}¼öàCãq@CÐD¶5xlÓà㵡ÝiÍýßÿ$±‹¥N~5íIEND®B`‚‹‰PNG  IHDR@@ªiqÞ pHYsMMgŒà=IDATxœíÐA ±¼yã_§‚lBZ“©àcísÓI3f@:Þk ïpKDQsIEND®B`‚Ö‰PNG  IHDR@@ªiqÞ pHYsMMgŒàˆIDATxœí×± Â@Àµ‹@´A€D´@äŠÜ E !Q†E~Rô&µŒü3áꂽË. ]<ž¯kIÆ$‡ ú¬i*s¹]ΧûwØ×S;]>IŽ]ßu¸8@kè’!É{ƒ.k›Ê<[—þ‡_ žÚéò‰_à7¿øè$)kûKIEND®B`‚ù‰PNG  IHDR@@ªiqÞ pHYsMMgŒà«IDATxœíÐ1€0ÄPЉ*À¢°†T”Ÿ!Ùnº¼Œ q\ÏúîûœCxìâôO@ h  4КhM´€¦Z@S- )€Ð@ h  4КhM´€¦Z@S- )€Ð@ h  4КhM´€¦Z@S- )€Ð@ h  4КhM´€¦Z@S- y`‘€¢fA$IEND®B`‚K‰PNG  IHDR@@ªiqÞ pHYsMMgŒàýIDATxœí›_hWÆ¿3‘M[IA íÎJh²;š »+hékEªEÁVBA-|j)‚}PAK¡-ô-ôAZôÉJQ‰>(HKM6 :YƒµîDÐ`UL›ÌTÔ¹7˜Å{Ï\ao;„û}÷›sÏÜ;›Z´hÑÂùby·_ªNùAu2_,ïÊÚÏL!Sù¥J À{ð‘™xyÖLo ïé2Søñ±ˆ˜Žø¥ÊRsãÛÁ`HRŸ;>^è©öÔ0ŽÁˆ5×:Ùã…R¹ËœŽYLVÀtt1Q¿¿pi§€VÓH0^Güÿ±ÎÞÞ½&°@zI¼‘›Ê‚ Ý¢fÓØ A*–ßá—~Ʋe³,ê6…í%œªîó/ßø÷ Ï‚@ ¥@¼9_ªì·¯ýtdš ºGÛ ¥Êv!ýi‘ âô>+Õ-‚$ÀJ%0ów~±Ü'ëãÂkBð@ôK¡Xy[ÖËCqqˆ¡ö„‡ó=‹ß”v“Aî?ž 0›¼ä¨¿ ¼HÒHVº€yHè„ß³è5)Yºž0Þ¬þWKÕW$ d4Æô¹a¡>67^¶­î@€v·,içtww¿`SÙ‘hOÞšhë8hóðäR€&Þõÿ¾þ,yu-@_ ýbåsX8Aº 9<ð±Tw˜r5ˆ•B`Þç—*šq9@W À7ù ú¾)ÇОˆ˜¿2¥àx€¾ï±±Çâs€ê‘‰~45¸3og§¡-}‰¶Eáàצ®V'ì49yÀÙØS×>} í5­äâðzröŒúàGPwˆ&Ä\‚=¤n=ýÖ˜“Ûýžà™q¨X½óÀIobö:„§&m©:€fò„³InÍø•Swm*»¥'Ï@˜ÄüÎø¥?nÚϸ0¥=0pqÛŠk—jc2 €  ´þ8È[þ5*å"«ÔÉ3î$D«£ðì°¤‘ PËž€IxÜ7ž–v#ܵeÏ ñ†(¬õËz¹ô—£:½­QX;$ëã’(‡Úú^Ѓ‚ÔPƒf|Ñ©eþo2 Ùß?5êCŸÀÂá¦Yl ÛßÿzµkÞp`ò€Ý% Üyíñ­õš²¨Û6HOþÏÿèî{ÑÑ ‹šM#õ¸€VÝÃ…ôfŒÄSà 8^1V?wM@«iLþbD÷ÿ:<^Ù¨Ÿ»lNÇ,&+ æmædUtqø¼A ã˜üÅÈãïìØcZՇϘß&›à>fÄ̈‰°çŸúàïÇnÑ¢E +ÜéØälIEND®B`‚è‰PNG  IHDR szzô pHYsMMgŒàšIDATX…Å—ÙvâF†¿f`PœÄžñ$9YÞÿr1žIÏl#$¨\Ôߨ‘qr—Ô9‰V×ö×Ö X øÏÉX¤| ¬!X÷‘K –%ï“DJ\³dm›¬íô~zò—À:ôÀ0ªDYØ !ŒqÏ¥¬¬•Q¹ Kþ#0–òàÞ²ûDù‘q÷èÌLÊô˾¦€Ùx|”ŽAûNÿ+ص¾ý ÌÕ J\p¨Ï`s Åó¥Òó€'ôxÂãÿ °‚°%Üc)Ïåݸ’Å9”O‚qf$åÀ³”•B8îððdž;(ÜR›À˵ƒ \Hh¤9†¹øú×"mÄ»“¬Û÷JÌcîżsA0ýŒÇ²–õ#ÁVCu Óµ#uŽl!™50•SºÒIöÁ— ´/Imеá–]ÃtåH½I±Î¿ ‰X¶-<6xèú!hJXäxÒü%Á…úAîIg³î?ž7_as)Þ<‘ ,žIB ȼ‹‘ ÆR´b\á‰ö Hs=ƒ º ¦®œfÚ×â=åï1ChLûc @P× Þ¹‚2Û ‚ú»56î{½ýÊŽ|´`{†Ø3^†º$?©‚aRßI“©Ó=Úǽ{cé0»KŠ”NYÉ UçlÇ0‚ñ!ñ†ä½M„Œ€Æ=·5êLxF[ÒRJx5=És†¹O‡žÀ>kÏ–¼¿Ú…í QW Ömõ°gÀ0}¥ß2™'}ƒOFnߘ”¡U)õ¬-úußâ §ybÛ¾¦;D ¯ß­;î‹9`ÊpSÎ4XÀK³”âo¹j6–BÜßJ¸I¶!; 2vù¢;C•ÛÖ$4þßÀ3#!Ÿÿaî8žvÖ%,ï:yÀK8îÙÁIÆuâõ5 Q/?Éós™~´@~€åS·¼*¤7l¼‰_ ãê²c¢ö†ù->ÝúgÁ”"Ôàí6ø³L†W6“1'!Øûæâì†nàøIæw<ž{ t¤êA— õPÕ!{Æ»ŸŽw,¡¸•Ìmjn@xÐÅ|$o6`-ð áå<6w ƒëg|–l$såùÓE1 Aã“-te}r1ùíÌÅäØÛ÷‚m +3þo2y0„qz÷Ëu0n}ÔŽj¼gìÅÛÐ ¦5ÉÕìËiôâ˜pÃä}¢÷ l5;&•”ÝåT¹°óžÃkøŸ¯çˆ|Ó;í2OßIEND®B`‚‡‰PNG  IHDR szzô pHYsMMgŒà9IDATX…íÎ10A&bЉ $â"" Üëÿg#eOeOm>Þf|Àz”ä¢?IEND®B`‚«‰PNG  IHDR szzô pHYsMMgŒà]IDATX…åÖOJÃ@ðïMC½A#¸*®ÔUW® èTx°¶øîõGO)¹‘¯çó^DNxGf 4Ø-÷“VqàÄ=5OÊoÞWà‹kÍ£×ËíÏ_…{pÄçö<è%³*î 4î Xþ¶xœ§Š˜Ùâ€Å ¸à‰#n à‰?ê%‡ëp§I·„‘ð¢1+“°À /ëšñZcnš„¾øBù6¦ü,›O¨hÂU‡˜¤$”^­kÂWеEànãKæ °[Ô5àkêlæ0¢iͤ­ûëõ ³:õ†¡ùDIEND®B`‚‰PNG  IHDR@@ªiqÞ pHYsMMgŒàBIDATxœí›Kˆ[UÆ¿ïd¢¶eFÔâFЏñ±Ð$½·I¦]È[©àƒAÄ… ø@¡+E Šˆb ŠàBÁ .DÑU-beФ`‹™d’N*¢8‚"RP(µCÇŽNr?“‚½÷–šöœ“ æ·Ëe8ßÿürNÎ9÷ÞFŒñ†¶šmy^䳆 Ý• ô‚­¶]bO@k¾Àô?*Š´urSé ­ö]aÎÿ'Ôá¾zëh`±}'X@2Š]'¢/šÍÎ ¶2\`M€$¥\^h4¾Ù`+Ç66§À¹Øåz3sssë=d Œ pcùý‡}?î#o `lJ(̯YÞ»°°p©»ÌÁq(@—€­Ç—>¬ÕjcîrÃé‘8KéµãW¼%ÉÚäbpÿ $$€x¸Ñî¼ä<û?à\€HŒ„Ä*¹k¶ud—ëüóáe’½äu¾<;×yÄO éx€Õ¡F‰q@½ÝhÍO{«#†?Éñ-³ðQ£Ý¾Ío-ýpß”rn¸D2{›Íù²ïz¼ ú熤„u‘Ñç_·Z7ù¬e(’ˆ¸^e;p¸Ý¾ÖWÃ@H“€kÆ"3s¸Ó¹ÚG Cœ‘Ø(]Ÿïj½^Ÿp?t BL‚€Ì¯ÙW«ýt™ËìLè!¾e†n]7ñÇÇ.OYznx×Ú˯|W’“Z³%€„ˆñ{ ÒƒVç'ÈÌ Añ• žl´>m;+“DÊáI{f[óٌɮDâÜoÖÛ{mÅdW­jˆK ¥×medZ€äÊ¸Šµe1ÛV·ˆÉ…÷lEdæîl*rñÇ·”v–ÃÒ¶"²;”ÖyTJ…'R´\4™@À‰íîgcê>”rÍ Ù@%ßXùª÷ן÷…a¸â*6+«@bØèäÔ½³²yói·ÁC†$‘üæÔJîö0 OºÎª’ŒŸó ‹Œ¶U«7ÿ棆¡M´Î8¶OK?ûªc(#€Híü’À;ÊÅâ·>kñ.€…DçW$LWƒBÝw=^§J é"ð@%,Îø¬å þFÀ9Nv‚/ÅO¼ÕÓBR.~Uâ3Õ ôŽŸÒq>ú ¼I,õ«•à–¡¿&ã~FHnÞ/…§\nÅ©’FŠ}õâ§§O<š…ÎN§MüA‰ƒK‹÷OMºîrÃå‹’gwhG/ß=5uݲ»ÌÁñ³~XÉsÇ– ºè%o|¼(ùkÝm[ …ßg]6ÿa"åÁ¥ŽGìmÃð[9¶±6R7§h°c²|g+Ãö¦À¿Þÿ# ’÷”K¥¦µöao {ô ô@¼XÞXøÒVÛ#FŒáŠÃ!bó$#äIEND®B`‚‰PNG  IHDR@@ªiqÞ pHYsMMgŒàµIDATxœíÙA Ã@ÅÐIJTd´ 60^¥µ ŒeýÛÌ@žµ÷³ö–yü(€Ð@ h  4КhM´€¦Z@S- )€Ð@ h  4КhM´€æø—<®_ã3-`n-03óû^l‰Ç/ Z@S- )€Ð@ h  4КhM´€¦Z@S- )€Ð@ h  4Кã¼ê^ P\û•IEND®B`‚މPNG  IHDR szzô pHYsMMgŒà@IDATX…íÓ¡DÑ}~HPDÅ_›Ñãž¤Š”{Ò¶›V|fgŒ¹º»Ê‹¨ËZN±JRxnxßm}*º¯-IEND®B`‚‰PNG  IHDR szzô pHYsMMgŒàÃIDATX…í•ÏkA€¿7›D(šþ‚¦›”Ò¤©‡mÒ¼Y ^‹=‰ ‚ààÍ?À›wožz«Š´·@z‚‚'¥ÁС¥¨ÉÌxÈDVkº¡ö²ìagß¼÷ÍÛabbbb®‰:qîf%§y‹¥’H$r_>ø%ŠT¼°º¤µ©a©t»Ý½¹Å ø_rKÁª]Bà ¯–Ë/W.UÀϯ¬ãU°Ðî±@Æ(Uõó+ë—")”6QvH÷ Š }6Nbewg ¥»Ãæõ† ò‹¥G‚¼R®fém꤈<¸1™Ù?i·>,],=y ¨^ÛePñ„…p/=5stÜn½* Ùbù™Ež»wbÿ%Šôô¬V/* ²…ò O]gûÿø‚Xî\ŸÊLŸ´[;ƒrœ9ˆ‚ Hµx[ÀC7±øïüý¾ÝœH=¦^ïüUÀ÷o1öó5°áV0dÛÏÃí‰ï8MÝo6ë§gæçË“$o€Ûnh”•Ÿ#!{×´Úl4>~HôC:I€ñЬHÇô`¶kß=Ý&þ(b£ßK# ÅÄÄÄ\1¿vR~Ì*[Ü6IEND®B`‚S‰PNG  IHDR@@ªiqÞ pHYsMMgŒàIDATxœí›Mh\U†Ÿw& J[Q7¢Ö…(n¤:“v%D´ZÄâèF›dl©.DEP!–V¬ ¸Ñ@Ói‹vUZJŠ?é(ˆ AZЏP‘¶H𹝋1pïM…Dν÷¨ó.ßîùÎsÎ=ç;÷Ì@_}õõ–ªN`¥jåq%¯hŒÏzÀë9„¸%²nÑË+ ¶µa2÷¤}¡¸ÎC$vxh¥= »àÎCÚ?<÷‹œöiU  =ç˜}iÏ€ [V•8xÜ÷!ÞL{î­õIaKþyTI)<9ç[ ‡È €z%n™ý/L÷PMÓ¾+è<” àÀ¾>£ÀÚ´oH°¾_J°ÞWi€àâ´o‘¨‚‘_R)Þ:áËê a.ÏTÝÈ/©p_´¸À˜kr!‡<ÖþS àðg¾pÐL 6æB…—¸+UaÆg=pz ï7gЧóP€q»¶a-ûÛ2aJ*qWªàlëê¯ FÒ¾"ì<P NuxÖðdÆto¯ÝVíŽ1ì͘½ÓMi‡›Õ*€öœ÷aÞXø »…›Ê~ЀêêûÕ(ä+°|šGÝõž‚yœ\—5E~û Àè Ëû6õ˜¯`ƒî£M½.3ž÷c†¼i²x-í «ÿ{>OJ’O5xñvÚÿëz;:…$´[J` x/ŠBaÉìjèÜ‚¸_p,Š B¡‰ìj謸 q"ŠBáIŒmÔo$Ü|— U¿7”2 cCú©žpðc&`jUC(mnߤ“]¸ók&ÐÛ+ƒPê{¸£©¯kæNàì²<*š ¥/D#›ô¹kÜ œË*z*Y‰[7éC›‡Èžœ”ÿ:T¶µ†ô®áÑ´'±´&”¦J÷âVSÀs³!ÈÏ`W¢Ê‹‘Ñ/ ^͘©œÜ* É'<…™Jû6 ù›à¿Så wx:u††é´_Æ 2 »‡µ¨Ó<|š ©È×!cÃúcM—mÀ|Ú_úŸ@Š Àƒ›õ{}­À·e´€í7èçn-ˆŠn+J;nÔ÷^d ð˲`À’9Z­Íú¦[3™@Àj1j#MÍ%ænR?™yÓ=€‡‡ô±`½ë·®Ä‹UçÔW_}ý7ô'-ÙR¾OTIEND®B`‚ωPNG  IHDR szzô pHYsMMgŒàIDATX…å—ÍkAÆŸw6–B$¥)1›´—|@‚¦ëQA‘ê5ëQAZ´'OZÿ€žZð `Ñ‚žëµð¢A©×ÐJ¦)ÍWµH-ÛÌë!»šïÍ&Ûô9 ï¼3ÏgvøßEõèÔ9Ét—ˆÎðØäSbæA<û%½ö¦%€ŠÝ&Ð\30›$o×o+€7¢Ä>/2àÄßnh7iQBðSPżãEv¹«am ÀXuM2Í €7¢Hò\m•–óÉÇGÐ=cW:0@š-6½„f·Z kS`«®1èN!•ÌtDoÖ -0àcÀ'@‹Þˆø×CôÔî §8À}]OÑ2=É4CÄó+|Q j7!@½DoÈ4Jœ´\]#âyb~XÛi-úŽHéÀ»-;ºˆÞ ©d†M·ö·½%ÈH>ª?ŠŠº‹Þ2šEÑw€B*™Ñ8€M› ï6zC–?ǹtò€€ic‡ªN Tvm^¯áhtH˜yÊý"~Xåý¾¸îõΨ)ÆÀåö|è:@—Ž»ÕÜ€w0»³µõÓãáhtÈ9à¿BàûúñäÎvéÐüÇdö¾”ªÕðc¢TÏþØ.¾u¹=/™á!¢AÀÚóªŠÌüJOæÒkÏlÚóÑo^]þJPÖU÷IEND®B`‚›‰PNG  IHDR szzô pHYsMMgŒàMIDATX…í×± DÑ(æàBz q£8B<±ü¿<¼6f$6×ö¹¶ßîYM=ˆ°®ìYõåèg FV†²SÖ=|Ë>ƒžŽIEND®B`‚…‰PNG  IHDR szzô pHYsMMgŒà7IDATX…íÎA@1æü{@¢z"à™ý·“ªE=IO²ùx›ñE+¶ÜuqÍIEND®B`‚ Ö‰PNG  IHDR@@ªiqÞ pHYsMMgŒà ˆIDATxœå›[o[Ç€¿%EŠ”(‰’­»dÉNšÂAà&EÑ H ô©}é{ý£}*P´A6NR i’Æ–lÉ7]HI¤.äôaöîÙ3‡¤äôÉ$w÷ÌÎÌîÎmç²âxã@œòžþx“„ãùMB–W—íàðœ<Ün%`2‹/m¯ ¡fàbȘs g´w¾ÑþÜ©*Ï£5d¸í =^ø‰§€9ß.ÀYÐ>ëÛú¾£ 4Šï?÷4Tý÷%p—§P9òmS¨ ŸÿÄÿŸòmh§¾})è{îq1o…R’- @V1k Fûȼÿ=rä®ÿ̀ܲ#¤îÇ,¨àå®Ç1áûç@6óGUší ¿)_óˆËÈŸõÁ\ß6HÅhÛ?·â'[ŽðÝsr<ã%ÿÛyF¶ uKÛå-ƒÙ2ÈOŒö ?Ú¼ð‘0’š¬0Ƕí–íkLN`¢âöYN{IçËõ—³Ï¥»¹ò«q'ßlûPcÊÈmcü*Ⱥ}M|ó^y^dV·¼Ù7òdÑèkúg#Ë&›úÉ‚q&äà> þ'Ê£ì!£«py *_ád xZЗÚá< ðºO ö*hë(]”É)¼˜7˜0ÆÄ4Pæ Z½íÇl/T¡ñÄ,ùÿ‰éJ4xØFÐö %† ‚2¼çŸ]~|Ì "œÙŒ ÔÌ$ÿ7€6¸]?Q5]›À×à®ô|¹r ΛB}oG˜ŸøÙE`5©{ö³Ò†ú ÔŸX~€Ï€ç€·<ìøç{ªÛÜUÑÄ[ó&[YÍ)^|)FËL.¡¦rF?7YõÊx=kUÒþ™~‘ ßx+¶ŠQ@ɘµ¯² ®]þÜ „¬ÌaSçÜSß7檛àŸuO€Å}ØÌ÷‹Ö¿÷²sºžò"ëGÀÀp1zæ¿ó[é1Y! ¹ÙWÆÝåø<Ž îRq7ûÑn=¼ð=záê)NÇX-%8çW/ÙF•L)0wÀ}àÒ›«Wèî MžŸÐÜ%èúãV;Äö÷§.Hxfû:VîGèÊ­c ¨/’Ñgîä§ñcXy .*PFÓ)ƒ­½ üøKt桞?>N¨ùÎyÔRøÕ9‡úád]Ô<þø$ Ù+Ïz ¸ r-fn%bÄÑá]`&ªtûÙá²ü pŒ C€îm¨M@ý%¸NCGÙ¿Rà¢Õ“¼ÖN}“¿kàö‚Î6È ºcßöžkB[ŸèØÇ:  œû î¡«ò…M³,¡Û¾‡­¦U¨µU‰2o€ëè3Õ¶2«0ª£‡ú+ˆ¾ñcîùç:d¹"+Àð õŒ|غພàH1ÍC»†®ÂùX<Ûp<…ö$/`‡´]àLçŒÁ¡‹úƒç)Ç‹%€ê=-æ·|Í)p­l["„Ë÷a®³¯ÌGo³¯`öRq[G͵`® a5ß-ÔÂeÀÀ4ª= ”š,£nªÇM¨|Ì3~ì=ˆ òÎaÂsÛYr—@Ü1º¸° ¨iÙ) d¶ó2¥d"¬`ëÚnû”J^¿Äî”6±x:Ô#4Ú ”ƒL¿}Õ²á*Ì‚t˜ÙúZÀ»ÀÒŒN’Ô¨OaåçÐ@çý- ;sä?#Á¦}dѸcï'Y 1a5rIH™GƒeOd„œtf¡~ÄAÆ#OÐ×Áä+@:gPßtoA-É/>óßÎÏùš+ átf ž-œ·‰*¾¾î†TW%yËLÂÆZ™EàKª'à%¸C4Ós–ü¤õ2¸çyT‚g~8FWìcwµýø2ºâžŽÿh_.z;ñ¦ÏgCšd˜šlOà¤)²á±©J¨oƒÛ ì÷-lå7 Gih¥ÍônO‡‡Ä©/ g‡R'¾½_Ÿ¹r'¨·ºôHe H .åã( ¾óGF{e%´òœ³”›:QxŸaß?„´ 7N|j|Íêµ°Xöߺ„V$4 1œ“ó.ŒÔ{Š„iÑú C`GàZáì(Áô;§0”6<<×…ê¨|Û5r î Uæˆ0AqTw›^ƒndÞb! ½±¹©Qô\®=¶ËpeióÀ?È´—ìöšP{fÐ! Ñ$ À£ý2b…»}4sÐÃÎÖŽŠz˜¸ÎöOìuôGô¿&ÔÅGp¬y#”Ÿ¡ö7ÆÕ³ŽÌòš¤ÒmÿÈFX‡B(@Û³ÌÍë€.Q¼@³Ã¿ÊÏŸùQÖa([9ÆHrç9my"è“Þ')ƒóIbt|Û2zÑ Ž<½ìg·± ¹CYÏæÓs÷Opí`Ž‚<£š8Ùæ\Ó`-a< ‡Å¡»ú8Û ¶2Ùéø„W×ì¡ÁÏÈVàoe_ûn0¶+æ$“ÀË„ä:>£8‹”à…m ]_‰²êmÜÐòg0ÉÛ¿ó BhÝF$VN²Ð÷-£Öh,%ØFí¥?ûmnd_8ƒÆ2œœá%^œ(Ιew1,S|¼’…œYµp†*—rA†8î»÷O·ýWÐ8Ósúc¬*N¾Âô¥­®-t™ƒ³.ÊSNxEù€Äf¾mÔ«8’êOÓmžfa.NÖ&°SÓOˆpAš4pÞtɺ×í+rJØîí°õKU\²8ž¯/Î]I#<ó¡(«vúÔ4_TÀ}ëêDV 6ƒñ•Ó'h9À2Z'ÔÆ]¿E¯Ç— —¤ NÑøü=/„a‰!­5 ºÑVMÏ%ð{àS—£‰LÊk'õf;…ÃÝgÙB ¶†÷7¢ö¤oÍVCܨF0}~óREªh íÈ»¶N÷óÖJ>dŒ¡C #ëNफõ€)¢ðŠºîê%¾³ã‚Ì )ûïÑÌÏyŸ¦àà žän"–º@•LIìÌ ¨LzÉ…´ûЪªÎ¸V]î˜ eÅݪ¯úc{º2_*¨nËÕ(X±@ Üžnyy( §¨Fîÿ ˜$?wà”„¿¡ |H¼¿U”ѧ0'Q¿9ÆûÐh‘¹¾“ŠÒãv@îÌfà%i.Àí¢u6ÉÖTñTÈf"NT€ì£µ?÷Æ— hÝF›%à¹î°œŸ&]Õã&ºêeTyvÐÚæ%eÐËØŒ3íwš WÝZÿFfŸ£Bxç'P=R" Í[_çh$›I€ƒ ¿»ªÉ !{f¼©3ËeÅ/Ko#|<À½ Æ—ã{FëÄÌt=!Ñ‚EÐ+ïeß¾´ b÷‹ƒê¤g°SàÅ­À}LÁÑ÷Ùë9qž¶øÌ“B“;ç'S._ÏZðfè}ìDI2æÿP.ºòó¼ù•¦ZŠ\¹ü̲^ùÇØ/L¬’{aBú‚…ñC:f{̉CœψCßù¥ÑWÎú+©ªŒÿÂDŠèONÈF^òúÊJî¥^ë•™´­äŸ·@Þ1œŸ²¥å=óÈã+ž<‘ZY¥{€àw‚q}-~kÞÓO+tž*Œ~ijÊ /+ ^¾*z9j3f‘-ÏC)¿›bD†“#›¨¶õ5º´üïjæzrÁ൹@ 7 åÄ)JJd{Áÿäö&ym.(—Méëúq5a§þ÷$ªÄ}T¡ÎxöN“íÀ¸¢ŽA&0JÍ<ž:y-¿8YB¯Ê’#Teà_à™8$kó¬'…Aâ3£´6}¬ˆÇ7÷ÕY«á € Ïoó ¨þ´ ý~Ï"[ÂIEND®B`‚‰PNG  IHDR szzô pHYsMMgŒà?IDATX…c`£`Œ‚Q0 F:`„1Tg½ÚÏÀðßNöî¹&îÊÀÀÀÀD' GÁ(£`Œ‚Q0 pOsÐÃïÉIEND®B`‚“‰PNG  IHDR szzô pHYsMMgŒàEIDATX…íÎA Qpì-Mè` BaaFûî|V¤À#Í#­²1*Ç € àyÀlØXù& U¶Þÿ8A­ ‘uËwIEND®B`‚‰PNG  IHDR@@ªiqÞ pHYsMMgŒàtIDATxœí›=LAÇÿo9´R¸Â[“#="…˜HcG¡P@(¥„xb§\áÅXùQá™( œv6¨†S!\‰|t쳸ƒÜ~œp7»7 óëfæÍÜÿýÙÝ™„7€B¡8ËÐq‚ZâH×d:‰¹‹™šA8ï³¶bÙ#MÄsL4^ M}¤½£&iÀÕ„qL¯¾æÎ²±Â£Qýÿ‚´‚#ÌI¬ÅÀHžÂä Œdä]fÌÿÐ ˆ¼7˜é™?ÚʃŸGëO »:“}ì‘´òX{Jš9³X¥¯à.í{-VˆI®hØ2jÙÔÚA惂–qB«Ûëà0 %Îtxý›í±ÿ¸Gè^Šê†÷ʽ§>aèÆ8€ÛyÝ át¨Éþat¼éšL§-ùßæ>ß;-ÉÀRT7öÝÞÈënÌæfÅa1wYÛÿõðrƾ²Õ †Ïï³ç¸ÀLÍ–Iš9ã½¼ò ‘9ßfÐMGŒcVösÈb•¾â¹²2ᢽÆã¶ ZOx'ík_ NíŽÓkáƒÐA [€l”²ÈF [€l”²ÈF [€l”²È&àÅ"WFŒV"¼pÇü_ƒ à+3ÿìÓ“GFðÉôa@üO¹ß¸A„ÙÈH¦Gt1!êF7ª™ø­¨ˆRaâ7u£Õ"kpnw÷€ "kr1§¡dÎüGPÈ€?••Ÿl{¤¥¶sJFÈ€åÞà&1õ‹¬!1õ/÷7EÖ~R}¡1f´ø‚ìå7Û`[ª/4&º˜'ç€Ü~,¼'Ë@}e 2@¶Ù(d 2@¶Ù(d 2@¶Ù(\úv,­I®(pjß±‡8 `¤ó› [F­·ªÊ‡‹öU{Œ³P’x.¿Í¦Öî±®²a²Ö‘ß&ð¼=ÆY(I4aé s¨>aèž«ó™ú„¡ÌÁü>Gnp1 ¼šèûá$P0À?M&KÛ*Ʋ¹Y)ª\ž¡Å52§Or¹¼ÉZÁ,¹\þ€Hb-ö?\˜ÅRBÃncÏ©ûú0bþÉ* ÌL©è¥…Žyi ¯4z*ÍŽuiªøksÙ’óœÄksÀ*狹6§P(Î6Ð~(MS‹%IEND®B`‚ΉPNG  IHDR szzô pHYsMMgŒà€IDATX…íÒ¡ AEÑû>=P‚= 0”„@SrA$ÛûQ»!!ÁíŒyÇý1÷‰33³Îô}d¦j­«%ƒ¥”·¤üp›Èßë¶*û¿ ä|WPàmŒ›³¢.­$ä),@u ò†.‹p Á'#XÂLÅæüâB{#ì4mìxg*lxíßz»ÞqÓ9”awªN½ P×hÒIñ-:¯ †lT$üéêø1ìt:Ð|× @®`¨ëvî‡@ç2péZ’@9—Ò=wÚÎ ”€£Ú±Ãó­‚¡/ÀPLf«é¤œ… ¬ÍÀUÎU®ÃÆWTÙxŽ`7“’rd€LJʶ2‰S¡¯mx–×rñÛ®/!`t×vzôIEND®B`‚ è‰PNG  IHDR@@ªiqÞ pHYsMMgŒà šIDATxœí[kpÕþΕ,ůν Ì`3ø"ƒNG#.ï9óI&›H õ®x.@³¶4¦!&>Â>0µ±;z÷‡æ[[Þ°Àë÷ؘÖ0°Š€`VN ü¦?íüø#œ[•¹¹=ÁKÜð…9öÝJŒÝó/`çÎx®œæ,.ÏÒ â÷–åÊgD´>ÚÚŒôˆi`A­¿Òf£‡Á¸Á€&¼  ™íIu$Eÿ˜¶ÙII‰R;‹9,“µ-aB#ßPhàs“X ïýx:ú§WíÙ_B¾Â€[§x˜AoókÂëH{ûg™ø®®n,L8†/"¢+X•¦Qbº¥»3ô×ìÞ`¨ª \Ï„g8µ¢xÌdžC‡Z²õ¯„ÏçsôÛ~HŒû,Heðoz:ZïÇDOËÙ@¸ë‚ø^]üŒ ñ«#áPW~MQq晥öxÁÝDô3%šâWÎäGÛÚb™øÌ4Âåño!ÐuJ#ÝâêèÁ½dè/+,ô«%óËHpwщ•ýáðU_"“†ÝžÀzíËð~"‘Xvº^Ž„C]¶±’&¶hŠÎ™…¢-Àj›U_–‰Uÿwz\c~Ι^ÝýixЪŸ\ah¨+ëï{yvY個1Õ›—””›èû§?–à®ó/ѫ짌Œz:[Ö &2ÔžS ô½[:Ïõ9—ž´p~I™ë`l w¯Y}Ó1`a}}¹Œì°pªàLÄ.ˆD"£™ˆuŸá? ,®fF€jÕ'Áè‚@3ïb»x)‹=¹¼§‰q³Â6NDÝáÐ.Êfž'ßrò™î»}ùáý»»­(óù|ŽQû:¾ Œz+uìfà±hGýóÀÖ¤• ^¯×yœ ·8OaÞÕSSþ547§í¥†p×½$w(L,‰Îë ‡Þ·"Êí \ÅÀðZá§€i$ü´·#´Ý }A­¿Ò&Ð PÅIïìîh},]£1€JËæohÑ” l‰v´hÂT44T–?¢‡ (³"^_\Ü4»ÜEÃý½ÿÉÚÿø`ßñÒy®¸r<°¼¸Æõ§ã½½º¨´Ó ËhhŔē6<`¦yÑ¢À<×àø›Lt»×*˜ùw]àE·»¡ÈŒë”CphÊBb„”ŽŸ6‚ñs峞ì;ÐÒiÔ¸ÏçsÄíx…€o˜ Í„kQ<¶&k—H$2ÊÐüPŒ»Ó­ tÍ_ìs1TÓJ\J~ÈLâÀ˜x„&^ö`º¦Ê0í…ƒ'P˜º½Ÿ\¬ÇÕ €Ýn»ŠñÁÛû¶ö5êöúoh­™¸é‚÷WÕt_f [“¶ª+Ê›ô˜ú݉q…ê‘Ä«FÍ¹Ý E`Ú`,*w`ÂF³å®)š/Ó«“€Ê`°ê¹$¯**¿Ê…Rþ¨òî[cDè=ú€²×ÎqÕ~Ò å¥Àù§ L{z:ÛiySXmð3Ź3ÝiB‘`Rýp‚dJž2%ð«"Þ¡å(áòîû:€ù&bò@uÝ—Ï0"0a‡ú™–j9©cñYj‚j4Mt­¡Ì<"‰ä5FåR£]ýnœðbå“dÎýÈØ)ÿi%âI©Õ^«åè€JUbý>ÓiüT BµQy1F´ÚKµ½iPE" £›€~¶ö´€a€H$2@¹tø|>‡’£R?Pú ÈÊ•"Ó…‚é:HÏÀ°òY Né6§0±Ï6\!æ†ÙçÅ‹;¡ÌbãmmmãJNJ¬Ê¨J¦¹&"Ž˜”熡’9Ó°–£7~ª"¥Œœ´š”çlØvê4¦ˆ–“¦}ªGábƒˆ^2*Ï'ˆ„aÛIÖh'´k9©–šÃF^¡å(áH m gŠ<>í‡v2´ÚejoM @¼P¾G€2}ô•…Þ`Úéf2ñ¬©Þ\ƒðŒSd‚€Ë•)¨9…¤5mk‹1ã=UE)/×òÔbìøÜ“[¶?bDpÕ-mМ(õV—}¤åéÎáLê]ˆV5 ï>Ê0ÍåÄ÷uu½wÂBB¥™€·ôÒãº(°Û¶@}Ô|‘Ë{ŽáŽ/ZSñ(Àoqrz¾'ܪ=ÔB€ Ú¤IÆsúDLzlS˜œ$Úãp5š›Τý:ûMÄe Þw&‡n…IzÜ]øžæ&]T¾Mk¦?jZÿ±»ö¬/5‰ìL \†üᣄä«ÌŽã&×ú¿VÚx$ÝéPÚé mg¨C ûz3•}Z:IÛ¹`èF<0øo¶±’&³Ä, ŽÙn)¶½„þqûø¦t|£ ƒø>µßw{†{p`¢'ô,*ÿ61~å§uð10¯‹v´^o6è€{IC¿Ty`þí±ýûÓj0=u{‚›¾Qa:œ”¼Üʯ•žàAr=˜n`z²3‰ÏAØ4*:wZš^}>Ÿc`̶ À'm „¢sˌÒ\ À!• óÿŠéÄEápxÌŠ8`2u^8v)]F„…àSç†GAèf`[ËœÉiwm¦þ=ÁMßvòyâ0G¬0»¹béŽP¥7x®`n†âFž‰†[Öb—«« gÍ:*3 ¤\ÞàíÄü„ÒFÄk»Ã­O›Õµ|IÊíõߦÍóSóœÉ;2ýµrªòøï`Ð#PŽg„G{Â-wYq`ùŽP¬¿/TRVYDÀù sÃhR4•ίy=öYψeÙ9€ÏçsØKÝO‚pT?$¿ÝSSq3"Kw3¼&·Úæö´opµ¦ "«z;[Z2ó—&Æ%ùåñý$>v&m+"‘=–/mYîhãØÀ/w¬„FEÁ\AX[Ræª(.«Üu| ÷xf~­¡ºº±°h~Ù]ü"@gjŠßeÇeу»3Ú”eUÖëÿ˜6qjb2FDbdc& ±r¥½ª« 3¯‡Þ$ccOgý=Vï)1­ËÒî3Mx @¹NñÀ¯1‰×Æ1²-Ó`ø|>Çਭ‰‰®p%À)Ëpâ ^ge´O‡i_—¯òøkâQ€¿cÐHœw@ôoHìœ ³ ~{œb1ljd‘(.MÈøHQK‚–@¢„KÌ6húCëz:ZvNG?˜|‹€ßäʧè&àÁžŽú¿dÓåµÈõ'3Âíñ_ н–çØw'6ÎJ ?éM#äí£)—÷쥂y ƒWX’¥›(Þ Âs=Zþ‹,¾0Ãiùl®Ê㯑¼@=MœÒÎÁÄÜŽ‰|â‡AÔÎŒvˆÄ;ÑpÛ>äáK±Ì`38‰ÿÐðÁÑëpkIEND®B`‚6‰PNG  IHDR@@ªiqÞ pHYsMMgŒàèIDATxœí›± Â@ÅÞ1Dæ@BbV b"¶`$$fHÅ„"¡ƒêîb$ìê_9÷IDäŸ)½¸Þî‡)9/ö»í¥å~-›žO’å˜!Éð>¬å~-Ýd>æÓÜj¿Š5ü4 h @ Ѐ 1-@cZ€Æ´hÐ4 h @ Ѐ 1-@cZ€Æ´hÐ4 h @ Ѐ 1-@cZ€Æ´hÐ4 hÖðø2·Ú¯¢ÿ_cÏ阔1)ã<·Ý©áëD( ÚGè¹IEND®B`‚O‰PNG  IHDR@@ªiqÞ pHYsMMgŒàIDATxœí™MhQ…Ï4&X¡P\¸ÑJÑM›¿IÓt#‚àFÅ…ˆ A7î]èN÷‚v!E ‚(A4.¤NǤMjAj,*¨ )Õ´¶Q“™ëB Étb(ô½)ø¾åysß™÷sg( …B¡P( …B¡øß Ñã¹ÉaÿkƧûãÑ!ÑžkAhÙìû`¨£\©×˜±/¥G²"}ׂ&ô×»–Úœž™æT·Pß5 ~ ä ì¦W+ÁŽLfÏ¢hÿVˆ]>ξ]µ Àßþs™…û·Bø Ã0:È,»%ã)54CÊH¥R ¬·1c¢ðBF ͶÓñø;"ÚïÔ‰1`æ'¯ËªÃ‰Ô=Ø ?pÞ©3袙ŸäWæÏ‘k“´ÞxÖ ¹â"¾é‹ÕJpPæ—"o^†ò…#î7ø3>YmH¥#‘Ï2k‘ÀË\1©g´×É‹ _&ï’]Ô3À4§º5âGhœ|m>êÅ䉌Mw²ÏÐÐû3ál*}"«'R(•J_ öÀîz×R±Èm54Cx̬͗Ü0Ø w“ñðeÑþ­pýf¿ž¼Ê/­ü7¸žwnÙ|FÖ]ÿ/„Þ¥R)0¿°ô@°ÎòÍo? öõIyßo…Ð-P.—mÕ:é«­Ù7ÊäÁèº^%à€93¤ñ½Ñ葞 …B¡P( …B¡P(­øÖªß?Ý(ãIEND®B`‚†‰PNG  IHDR szzô pHYsMMgŒà8IDATX…íα01.[PS³ÿxd(õ½}ŠX”Õ“Õ³ùx›ñE¨ŽEÜéIEND®B`‚†‰PNG  IHDR szzô pHYsMMgŒà8IDATX…íα @1ô“³b¥J_ŸÈ‹ª'«'7o3¾àî' j×IEND®B`‚‰‰PNG  IHDR szzô pHYsMMgŒà;IDATX…íÍ!Ñá²ìÊÞzßsYv ITóÓ´I’^v¤\P/}ÿ9ú/ %I’ŽÀIªv žIEND®B`‚¡‰PNG  IHDR szzô pHYsMMgŒàSIDATX…íÕ± DQ [GËh¶)‚+¹ƒ ä_áqMJqDíˆÚÏê)GH´€Ëó’¿X`ÏÑ嵜¾¸© ² KÛIEND®B`‚2‰PNG  IHDR@@ªiqÞ pHYsMMgŒàäIDATxœíб Â0Ño†ˆDKÍ4¬@ÅDlÁ4Ôi` SQAe;‡Ä½Ê…•|_"韕Ñ?Øîö‡”rN’Ôzºß®—ž÷[mF~Àë1ŸÎ½î7Y#ÀO3=€fzÍôšè4Ðh Ð @ €@3=€fzÍôšè4Ðh Ð @ €@3=€fzÍôšè4Ðh Ð @ €@3=€¶F€Ç—s¯ûM†¨5Ç$s’y9w½/I-žÎŸ"0íªIEND®B`‚q‰PNG  IHDR@@ªiqÞ pHYsMMgŒà#IDATxœíš»kQÆ¿³;&!SŒ…°°‰Éh'>@ »°3 *élBþc)$bg’B%˜YÝW` E‹±P4°Éî 5àÎìc'7qsóo¾ÌܹÜ=€F£‰2T`µ\è߬My €Þ|¡ pDK=Æü™DâG»É°ßÏÑ"ƒAtÄ`T˜yÒ:™~ÖjŠïìBi’Äüjìu®Yfê¡ÛE_عÒ"¬`÷ßü_êÜà‹nO‚çVËåþjí-€#¡XÛ)•>ãDóšàù?Ø¿Y›Â^»y þñþÞaæñP ©ÀÅ»w˜axQƒÓ»áC¥íw~ÌLÞ[!›/q›Ëï{eC Ú€jtª ¨F Ú€j"@ÇP.—;\‹7‰qàCægóÅwþí¡ÂÌ˼U½gYÖwÿZ]Vlwq­°~9ÆE€HqÀøÀDW,3i{ù³;AÇNµå+Í•fcÌO”Ü<†ü2[(Ý,ãÀë|i„iÉÂ]cÎί[R¬‹G¸/UÐqb~dÛö~ qGñÍ)æÂíë½.!퀗$ …ˆD|9×€]zÄÍ,ãËóHõã2FŒa¯´žÁ"-ž8mš6:­y•Ï#ޏ„t["¿Ö¨6 €jªÑ¨6 €jªÑ¨6 €jªñÓ :ÖéÄ· ªÍ{¸Ü<­ˆ–š‡¢£²ÑcÌ7G%€:3Oºõ G!€ß­²-ú…÷ÐWÀ]4Kÿl·Ë÷4—L¦~¶›ì8já[+¿3ÓÃa«º­_Â.ôYBÕós‰BAa ]g1¾`K¢˜ø«Ñ¨-H(;ËdÖ]Ѱ !vuddä“„¶ë>`4“¼ æÇ½Â„ÙQ3ùTJß5"jŒš© · îuøFÀ„•IÍHéø‹k¶PH¢AÓ :à ¤£B„åzŒoŸJ§?Š×Óh4‘æ›fÞn×îÔ‹IEND®B`‚ü‰PNG  IHDR szzô pHYsMMgŒà®IDATX…í“1kQ…¿ó‚² ÚXX° (Ù˜ÒNI©±ˆÆl´Ï/Œ¿@lÒ¨‚…(VZ()Sv‰$2…TŠ4 3'ÅÎl‚›lf5Í|0ðxïÎ9çÎ}%%%%ÇŒ²EÔôwàôòýQ¿¬3aßf8¤ø_:ñý˜E @Äã±HRM Ÿâú3ßöæV\·x‹¸æÖaÒŽÓ#i¯Ñ•>nŽ_Ôæ^¯ºoósÀít«÷B$„ÔåUõ$÷F´½¿¤cî£Ú®®3&3Ó®QgÐ\æn™ËÌT×¹ó§y«ìlk¶É4âaZ™àœ_"3,×k<’tà»Gv5<…x’å¢5’n„LWbjbHO»gÍAÔô] *]Cˆ€ÛLLëåQÚ¹gûbÉ#JxœØ!²Î;pk²¦÷yt ]®Ù†¯X¼N!ŒÓ{ÿ$0R¯i!¯fáÛ5| øˆ8›†È460×ëÃZ.¢Wü÷ž-ùBÅÌcΧ*_bqõAMkEµz ðü³Ï…˜¯ˆ•$pãþ ¾õªURRRr¬ìoÉ“TniFÃIEND®B`‚$‰PNG  IHDR@@ªiqÞ pHYsMMgŒàÖIDATxœíÛ± ADÑ9‹¸:ÁBlÁÈŠìÂBÁ.² Ï@Í4½'8/Úlg¾IUý³A]|¾\wsr|8l7ë“Ø±—&Éëñc’ñB`ò|ü§ó¢d€ŸÐz€Öz€Öz€Öz€Öz€Öz€Öz€Öz€Öz€Öz€Öz€Öz€Öz€Öz€Öz€Öz€Öz€Öz€Öz€Öz€Öz€Öz€Öz€Öz€Öz€&ܾœå~Ýç}2LÉ0=ÏUUË{›)HÜ¥f7IEND®B`‚‰PNG  IHDR@@ªiqÞ pHYsMMgŒà¸IDATxœíÙ± Ã`Åà‹É@Þ¿taðFΟá—8!^w3óºŸóºé°Éão Z@S- )€Ð@ h  4КhM´€¦Z@S- )€Ð@ h  4Кå|äqýŸióÕ33ÇþcK\~КhM´€¦Z@S- )€Ð@ h  4КhM´€¦Z@S- )€Ð,àfc J:ê¼IEND®B`‚†‰PNG  IHDR szzô pHYsMMgŒà8IDATX…íÎ10A&^0CTPîõÿ³‹²z²z6o3¾à9Æ“ÊÐÒIEND®B`‚ ‰PNG  IHDR@@ªiqÞ pHYsMMgŒàÒIDATxœíСÂPDÑ Ed‹¦Z@Q]P ]A6‡ö¨çþþ›TÕ?ÔÃÛÝþa8'Iæùt¿]/bÇF<š$ËçÇ$ã;à¼>ÿé^• ð@Ð@Ð@Ð@Ð@Ð@Ð@Ð@Ð@Ð@Ð@Ð@Ð@Ð@Ð@Ð@Ð@Ð@Ð@Ð@Ð@Ð@Ð@Ð@Ð@Ðd€Ç—{U,À<ç˜dJ2-wUÕêž ÞP{3‡ÖIEND®B`‚r‰PNG  IHDR@@ªiqÞ pHYsMMgŒà$IDATxœí›[h\U†¿µsÁjU¼t&5õÒ6“ˆoBñQªhµˆÅ è“H$·"Þ@Tˆ¡+ˆ>ˆ43FÅ‹úTZ ‚ ("8™ˆNs“*¢¡0½|0‘ì“)˜°Ï>[3ÿãúá¬u¾};ëÌhª©¦6³$ëþ­º_[FõEEÔ.n{ÖÇuÿvgo0b>e¹^[È·ø¸¶ñq‘4Õ3º¸ÇˆgÕ` ê­î¨t¹Ú{Øê:b}åˆÀ®bõ åÌpi[}å‰@׳ÛZh@äò´sE çõÅó©Ë=!òEàª7§Ï©×íqQÙ“°¼Mù¤¢°wX[Ûjç¸Ñ1¼mzIÅ@UªóEàN'.(šÞÍC$ ¥…@p‚‚¢þŽ»³)s]¥…'žXc(6Å¥ÿ2Ð]šëôH«ª†ÖP‰’*”æïVu£ `C¶(™Ì€Biî&àØšü*$ýy¿JÁtÍ^rhOXvùÈ ª ºFg¯QkNç%,Kˆ¯‚Ø9vòJ1f¸Ø14»›‡@›àî£s9cåc 3ae2íW+õ°«xêBcäC °” G~E©ØñÒO[ZLmáÚ„•êóýz”€½ÃÚºekû;(×'¬hnÒ0¬¦ºca 8àÄSîì6"ÿT¥»sáE”ûx€În#ò PšJá1'¨JŒ7žÁBi~8ìUA$Xs³^y›…ÒÜàh+Ê‘_‘Ç%x¡ j‘°ÍÍzå €H¸Þ§¼°*|²1±ÿúè @e ÷žˆ<´ÆPm‰‚×c°ÜŸ{UaØÊß"•÷ç€Jîè+nT@²ÛHþ‹ÑÉjþQàm'®HtÒ)hDìšë}ß5TR˹A¥VÌ׃RûCë÷_$,‰i9¤ZÈì`çR­­ývà[ÇP¢™ ©1Ó{ÑoµšÜ L%,Éü€ 2 3ææŒèÍÀI×QCÆj›†?ôwL[£·úkƒ2ƒtNõu|g­Ü,­©C³|#ª å¿T¸ ¨9†`–É Êd'® ä?ÑûXûZÜ a!dv•û;ÞE9èF×”éY<9˜/ŠèÓNPÂ6O™?Œ”ûòÏ ¼ìe¥yJ5d-Ws#¼åÄÁã7ÁgSöFÄvþœëÆNê}C€ÏFäL­mé^ÐÏ#å¾!3½;Oëé¶À7 +µÍ *•‡/ùÝÖu¿B9D¾èLìX¨›ú> šv®(L÷]ö£Z»8ÕÀö¶$¢PÚþ½»øÓu6É_fÊ}Û¿½Ãý–hüefµ&:>å u…:bŸËº¦¦šjêÿ¡¿êj3¬°ñ¶IEND®B`‚u‰PNG  IHDR@@ªiqÞ pHYsMMgŒà'IDATxœíÁ  ÷Om7 €w@@òÉQIEND®B`‚E‰PNG  IHDR@@ªiqÞ pHYsMMgŒà÷IDATxœíÖMˆwÇñï3IŠ èRHveÕº’ÝÅÕ‹žÔƒø† bK/ŠõbVÁT$ ZÁƒbvÄz¬b)míµÂ{Гˆn¶êÁu7‰]_W“8WH¦ZíÎ$}>Çÿ3ÿg~ÿg˜IÀcŒ1ÆcŒ1ÆcŒ1 Ú yb¨Kù^DZŽäR‰ÃˆháÞ¤£'ÿ¥Â!A¾éO'.éxínþw”ÅU OG›Š©ëëæ–‚ö®¡*I·¸[Ðo«VMÖDüJ·”ÇÛÖ ¬¶‡Âו‡‰ßfž|и÷˜%¶»…ã¾ÃC0ð^ œñ•&"ÞOÉl~Ûûöêt‡— N0ÃWúScÞ‚éxм¯…ò ÔȨ“lÍdï?jʡܽĈ÷¶ín~ƒ*½@ÌWºðD+뇺[Ÿ†7üŒiw‡Ó¨"µ7Ô³¥ØèÆ;›ÛžÕlP•¤[Ø/pà 1æšâ»X'/ÂÎY·tôV)ú0±z]¡/Z*qcû´¿ºÎ^û¤ü0ž6ùZ(èŽ\ºåh½2ÖuÉÞÂ<ñô`ª¯”Se•Gì~DÊ瀾ú3¯úÓÍ꙯œ‘è¯Àœ÷Ür_E×li¹\Ï\РÌÊŽL‰8åó(ËßqiÎsdõ_©Ä@#r…ö3ø.·º?}›R\pêm×ü!ZYÔ¨ÃݳÁT¥£§Q!㫜}RÚpwçôÑFÆiüÆ$Ý‚+ª›yõ¥ÿ.7ؼïßþ|–d4:óäí ÿwcŒ1ÆcŒ1ÆcŒ1æ#ñ¨³ônö©¬IEND®B`‚ljPNG  IHDR@@ªiqÞ pHYsMMgŒàyIDATxœíÐÁ Â@ÐIŠÛð X‡-xJEéÄ"Á2‚Eìz•Í9.˜÷ŽÃ?ÌŸ؃¡ Ï×µ&s’C‡>[Zj©·ËùtÿÇöêOŸO’ã0Žs®Ø›ÕC2%ywè²µ¥–2õ.üÞvšñƒfIEND®B`‚í‰PNG  IHDR@@ªiqÞ pHYsMMgŒàŸIDATxœí˜OhTWÆ¿sߤ›"Ò™üJ a¦íºÐUM±¢¥E\šøKAèÂþ R FŠkg’ˆÝtU¬v!¢vmuÛ:Z«Mt2í&ikм÷u¡sߙ̼7ñüàmî÷î¹ß9÷μ{/ (Š¢(Š¢(Š¢(Š¢<_ˆÝŸ®O†  è`|ʼn4„FÌ—%?}ÄW“+ÖVxm³×^‚òxOêÉÓ)'›…H„æ(ä™_öQš£Ñfƒ…;#FÌ9[ì ÂdƆA4¯%Jø^Åï»b¿ì, N×ß4 /xÙ’lÚ"¸’ÿ3³½ê§¯­ÕÁIÕO_3žyÛ’² ÿ;Ä™ü-㙡µ’‡£C„ÁSs¯˜ï" ¯[ÒfZ ŽäùK˜ ¶UG³öF:>•êhö¶0¦ð'K€Þú%Lzˆ$/W…ÁðzÉ#Úqm^›­o ‚ð¬ïXHÐhœØ ýíÜqÙóÁ±ôR#aþ-ßK/­tý»À÷öˆ<í[ ¤@$š»ð5Ÿö•¹Ì!‘¶\¶´÷:ƒ”üLísR¾r›‘/J~úX3‡šféÈ}N®P›€àäM„ð@Ùï-¶ÛKÇ.´rÅÚc Š|Tñ3ßuÊ‹¢(Š¢(Š¢(Š¢(Šò<ñ²#,;lŽgìIEND®B`‚Ò‰PNG  IHDR@@ªiqÞ pHYsMMgŒà„IDATxœí˜?lSWÆç¥UeªJ¤Ae¨ kq¢ „ª L„)Ñ©S[ÔY¨jËŒD!ñ‹«V T-‘Š‚Dl§kņÚní–%Â÷ë'Øï½8Žýž­Ðû›¬{ß=÷;ß»÷Ýs Çãñx<Çãñx<žÿ6¬‰Ë«šr7€ÀÄíé »2 C1 ¬jã&lµÉ˜Ÿ-ØÂ µ»?’.a]—1nEç6q{©¦Ï­g`+@’•×øFâÓ]½>]ಙiºb@±¢GóÜÌt9¤ôì.]´™ c„½Nž;À¹hŸÀ5Ķ¢ŒŸm‹3“öo–ú25àû'zs#Ç=ƒÉØÄFCÚþD.@T—,®è`0Â=Œö#Hl}ð“üËi øÅ=gªÛûCW|[×èkbYÐ^„l&¿§%—&FóÏ¢¶aœ½T°¿»‰Ñ‘RUcf<ÚÊP3¤þÎøÔ0#ÚsüÖhpfþû³ãØNa]ÇÙ¼ÔŒEõZÝe†‘x‰úÝÁ™¹q{ºÓ¸ë€ÅUL; #1 Z«Ÿx€óO£xúwnuØZœÇ?’-• qFà¥ØÜÔåJ­þΰõ mHâfи ð­ä'ùQq~ö I C×P ðq²TzæÁ õ Ô€r¹\àxþ €RG‡aW[ΠæêÖýü±#Gfþ”ÆÁ“[âÀ…Ž$Bã7š@œáÑùùùß¡s ø¾ÿtȱuÏvLFB”ôv#áH1]7æ^)Ÿ¿•µÖÌ ¸V«=ç(·⩎‰HIŠRÊruk»•“³ìº³×3’ ã: Z¯/:p¾‹%JBÊä[oŠv¦B†• pûWú/™P š/+âU€=” (žO‚€±Z€ãUjÍ—ú¼M&Tk’¾p «K@Úeo µeº;hmû.Ñ7}P š§|`O{;ÉèiÙ?Šh{¬vöø²4^ïwð¾ ¨ÔêoCZCF’2+e%É`‚Cᓲß|LiÌŽNI¬ÍÄ./"õ¶áSÃÖQÿÑ„‹ sïîäþгžçí;xè"ˆS†nӞ͂l•ÎÝZýåçŸÞ\YY a ãõ€çÝÌï-ÜùŒÀ ÃPݲO†„Á_MöŸœžžþ+õHiÜØ¸~p|ïƒ+âGµN®áA€2¿Ã¾ÝºŸ?žöþÊß÷'CޝŠ!D8ÜÔ;ænÝâlæôð¨ëº·Ó ‘HµúÃT” × Ìt…öwÆgAG±Ò™?j ËKss¿%Ç&àûßÏ„ŒÖLu½–¶…­/Ã%Š¿:–çn$Æš¨ 9k&»"„]”|;$)v‰º-8¯.-ÌÖL1ÆB¨ì7Jã¡;yìÞä@æÒy’ˆ¼k›õM1±PõgE¼gêÕ»n'–¶(œ+º‡ßooŒ­€ˆ:gŽo5þW>&ùÛ¹u3`8FSnqž'wï>ß)$":3MæWÃö—*[T~òÏÖ—2úL•A­O*_ òÂ3© ëŸøCV¿© ë ¤¬Êå•MTÆm“ŸL˜Sç²ÙûAίZß½V¯Cªõ¡0g=&™÷@tb®\V¡Þ--.žö;?)ë B P[¥yÎÕÉR¾×u½¡Ë’´¾@Æp±qçž{¾ëÃFµIŒúõD>ñdÕæN’¶¾ ¼Ì·|Z±|cO…4X_ZVø O}Z9R! Ö„ :§k~íD*¤Åú)cÀã­>ÍŽXÄßY O#ÖH P(TK¹ ¿T Èø\.ë ¤=††2¿I?â²¾@êc0`*ìA|ÖH p*x«õÒ'BûM…¸­/ˆd&Ø|*Äo}A$4™ ‰X_ÙZ h*$e}A¤‹!ÿTHÎú‚H( Ö»yl² u•÷’²¾ òåð¹lö>k0àIí7ƃ¾2‹’Xöä3™ oÅÑW³¤â­p’x °k?(‹jL±HÇ#öÝú6^<´œêí=)3¨8qÇÎõm¼(;Žª{o&&Ÿwë‘쀀›uES¥ÒJ·ÔÀb TZéÃ1ó¸7·v¶oô§­¨mÖôAAl–†sÇøjõÞœ4·]ž0•fR½]Þäóµ~ÛåóÆÒØÿრ`¦±|.sÝ«®á<àìà™ëÌ4]X±ÀÄ}5{æóF ~4E_è“ZôúhjŸÍq¶¶ë:…ŸÍñ@F3ŸÍµhÑâpó/uªÿ*CœIEND®B`‚ø‰PNG  IHDR@@ªiqÞ pHYsMMgŒàªIDATxœíС€0ÄPÊÕhöfŒbÙà#wêò26Ä<ÎõÝÏ} ᱋Ó?Q- )€Ð@ h  4КhM´€¦Z@S- )€Ð@ h  4КhM´€¦Z@S- )€Ð@ h  4КhM´€¦Z@S- )€Ð@ h  4КhM´€æL¡€€¢ÉIEND®B`‚¿‰PNG  IHDR@@ªiqÞ pHYsMMgŒàqIDATxœíš]s[W†Ÿµ•Д´N?œJ¶Ã…e¥™‰e©iCKÆIã„Ú ?€áGpU.à‚ÉEoú7úh:‰eÙ()P –äÌ0ÖDZl7'©ãíÅ…­ÔÖç>Ò9¶™ú½Ügµ×ój-££{ÚÓž¾É’F‹áDâ <Ô_L€kvŸ|°üÏLi{ËëLác‰AóDßÞ©êåÊûá­7ÔÆÖÐ{ìØóûŸ|ëSàDÍ¡{ÖÈØò|æÏÁ”í"ÑáphËavÕÝÍçW6/›ÚûŸìŸzx€CÆêdøhâ-? öSMá”ä3<ûËÚå:@.´Ø£g·šÐ¾*åbíR϶٫ÇXŒ &ßô\e@ê‹ÆO¶…PÔ.Õ èg{öˆ±©Ý`B_4~‘)ÚÁ¯«Ž­Î€î»ÜsH¶ã&x„_ úMíb7‹×çÕš³¸›°#ã°ß¾í×µbEÆn¯Ï×hxL¾)Ʀ€‡ î©Úw—ŠsuˆíZ}C#oSÀ á÷­ÈÙå|æóF›»Ó?á¡°»LðÞlÕRiö/ÖÈY`¥],pHĤú¢ñ“±žÔK¼¤p„7ÂX;xp耪ÂGo«“8vªg‹¹/\ó·R,ñºªN/:„ß7ÂØB>{Í%·³áXâ{Fõ*Ûh‚WxAÏ• ¹?¹æ÷d<5axÞ!ü?¨¾Û© á:æ:0:0Î,²ó²ÇvÀC‡@°&ôÇ’'Tmš€áÁá*ÐLËùÌçFî;„¿Lm\ÆZÊ;¼õ4óµê¸ªˆ¼m•«øÐ ÁϹüxkªŽ; ª…|öš çpï„ÔÆ5}‹úcÉŠužyÔœï|耪ú‡âßWä*ðœCø¿EäL9Ÿù;ÀÀÑã¯Y ¥Q^j{¦ò̹Åâì»,ðÑèÌ‘'v§àÁgÀ£ ¢+ª‚¸ÜXÐ?4üÅ\Á­ÚKyˆÊùÅRæ¾äÛ¤@ M|¸ 4S¹0÷jÎuF8KyHˆŸv@U}ÑäÁ^A8èéÄ*ü|ö÷•lƒ°n‚`'UÚ>rß>ÂÈXÐðàlVH++Vä±k¼Š<6T\Àt­À 82OTŒL êòôQz¬†ÒG¿dmðTá—;J Ü5R9½0㺿•}­À:`ž4Â(/Y ¥‡“þU¶Ut@8:2bD§Az}I(Ü5;ºPš›õ%ß–Ô>˼®m”ñŒCê;ÆÚÓ~›àëxüäWU8Çú#÷¯â_¶Æø>¾u@x(7Øgxì{K…¹€HläÇ¢||ÛáÜ;!«£7K¹LWoȺ¯j§Lèz¼Â[d¼`)ŸýTEßÃq*F¦ Æž ®QW¬Ã»Ï¼EÆ— ™éfKùÜï¼™@º[:6àkx;„¯•‰VðUm2aµ}Zé­ÒáèȈC 3trR$–µ3¸Á?2*ã ÅLÚÛñwDåÚ¿³èm«2º\Ìf½ì°ðO÷>%˜Ëh‚'6à§W»‚º§W0§– ™œk~çï¯ð¢Lt °T˜›±È8Žß ;JÄ]ó;u@$vü¸hhðåbvʵ…‡£ýŸ;¡mx†·zÁox€åBfÚ¨LàÜ :íÒ ­_’ê¾”K9Äv¬hâ´ýêßúl [mÕ M;`Þyæ½4<ÀB1“6*ãÀ#‡ðÃŽÄ’ÃÍvÀ&ø°Ã&kˆ^XÌç&b}“×NP1§–ò³sµê 8òjr R±_‡Ä;_UtäŒ ¿ÅÍ„/=Y.äþµy±n*û+þàÊÅì”(¸Ã+ ¿®]lôðŽC²5Ä\ÜIøªÊÅì”X½€ƒ ‚œª]kd@¥Mž øÙ«Ž5®r)—r1A°Õ ´úT×@~º›à«*—r)D/ÒÒ¹R»Rg€Ù÷ßK…g¯Ã2uIv‹ó¹É¦&(%•Ð¥ÚåPíÂýÛ·WŸ;üÄV"+¤ÈÏ ÁýKë—Üý²p¨·÷²ª‰/‚ÜÕÔìÿÙRþ·vº¾=íiO»KÿŠö)ve]6IEND®B`‚%‰PNG  IHDR szzô pHYsMMgŒà×IDATX…íÖ?‹AÆñﳉޡ v‚Šœ]¸ÄD ZÊ5g%b#hk!÷ DÄ7 Ö"¹.ˆb©¨9ÌŸ…”V‚r pÑd‹lôÊlÆBÍn13χ™ÙÙ½4M£V§÷UFÃAþD½^ø²H@4U+sÈp0·4jÆq¼?<àäÌÎЛý~ï_xüXû6>j4¹à¡D‚ËÇVNÞ·=ÕZlÀ¶ÁºÞêÆ·ÀÆŠ¢@ææ»÷ñFP@ª°`Œïnµ{Wƒ%%éûÃV»·“Í€ð´ÕŽÏ¤•¤ˆeps«Û-…ìF°õüM»½H2æpŽÜË×ΑÐl'éiy|£gÁòï×bp€¤(ýÁü©ähh@d[ˆÏ#FçÏ–ËŸB"Æ—›mHÖê•ʇ,¾ºP+—ã¬g.IJÃGÀ¥Z¥ø*ëùyÒmGŒת•Rs–afšIbnmT+¥Ç³Œ3@’ì$°¸S=]¼7k8d\‰tڅуکÕ[ó„CÖKiÂdÍŸÔÊ«7¤]ç^@zʽط”¿:¹ŒÌ[¿½]Îs±P(üXDxÀ¶±’A~½X)|_Tøÿø¢M‘õiIEND®B`‚k‰PNG  IHDR@@ªiqÞ pHYsMMgŒàIDATxœíÖ=luÇñïsç8U›„Æ¡õ9D­GªrÅ °A2/KZAÕ©AÕ¡X™‘ZUÊ‚Ä HP) õ!"ÁˆT«¢±‘ú;qk'®Q›ߟ…"û(%äÎF¨Ïg¼çîçß='Ý”RJ)¥”RJ)¥”RJ)õ  Î늱?1ÈÉbnæ`‚W»·Dêу‚ðàbî¹ yLðLC䇽kG³ÙìJÐléL¿e„wŽ-¶F·1=½ºÑP+x-ãË0¯U–#Ÿ'“m œ}W&I¤FÞõÝ<„ð/ÀÃ:,45Ï.Ûõ©Ä®áAó·¹nWÿÕ›Ÿ‚yÝ7Z‘Wƒ<}aƒñÝ#»¬5sØíEdÿÜìÌùä>¼s8щœžhe1ÞKs¹Ÿ§6Ö¸1*$n¬µ'šß€á7ƒ9TÌ_<óoòœ¡Ñ=b¼³Àß(okìzþ§_VÀ# Z-Ývúz>Z¥#¤ÿQy¹«×)Õ*ó?¬'+žyÎÂ|Ä‹á;cEž/ä~¼VïÐP.—ëµÊÂDw¯Exºa$"ìï‰mßr«²0É}>“Njä°ùØìMp;z xåüR˜C]ÀL­2?Ù‹€}4½heoO,>Ü÷ÐæÓ‹‹‹k¾ëÄL¿-Âø_{™ñBnøh­öeØŸÖðÞ÷â¤Òcbøa‹ïg§¬Ž•×/]º àºntqÙ~ÏÀ_„ÁÈ›…üÌx«:¶týC£c¼3Ís¹.Œ­z76ÉÊ)àߥwÀ¼RÈ]œhe¿–/ 1èî@ì³Àžõ]anxb½8?;ómK‹Æ?Áu(ä³Wî˜è^“ÿ|¶¹lã=ÙŽ›‡6- ’Ÿ^ŠuÖ÷ïßç´o°;Ÿº–Ëζ«W+¾«T*Õk•…Ïzú€LÓÐpÊ^é:8÷ë÷ÕvvjëîºUžÿª;¶}dðŒp¼˜»p¬Z½èýÿO&I&“›þëJ)¥”RJ)¥”RJ)¥Ôâwöômá…IEND®B`‚i‰PNG  IHDR@@ªiqÞ pHYsMMgŒàIDATxœí›OOA‡Ÿw†„‹Ò"^ôÐ&áâU¾…`$/B£‰è>žA#/b¢‰ð-ðê…bH¨WJ[ôBb€Höõ-Ëvù#ayÛ0Ïivf6ùÍ“Ù˼àp8®2r–Iýó¥Ží]oõ€Üz€–x£ý7{@tñmþ×å‘î?§½t²UÉÌnލèkàÆ½,6D˜X{šü„ˆ7éX÷rÚº%•i K¼Ë#שÉß³ò7j0z«ÊÖlå-0v¤ò"|S¥$¨ñYÏ"žݪÜè e·¤ªÏ¢vBäÈÌ•GTù˜õ_Ç c©/'m§†@UÒ³åGx2…r­Ú-“µÑÔ|xz€þùRÇöŽ8ü浨׿šíÚˆ/õÅs'·ycOüeàúAW1Ñ®™ðÑ ¿¸½ë ü᩾l¶Å¬f»6^ºz¶w¼Áð¼:ûGÝAò…±Ô—x"ÆÏÏÑäg…üaûç|up©á¿ù“Q–jHoxJ„zª Ê1E»4‚kP¸P;í¨;¡5ÔûQ®N€ukœëÖ8Ö¬q¬XãX°Æ °``À'À:€5N€ukœëÖ8Ö¬q¬XãX°Æ °``À'À:€5N€uk¢ìUŠ4½ ÐöÂãQ ,Sq„ºL‚kX£+Õ–¨ž©¦ !Qjèð”zâ-֚ЗùPŠ-`ÌdæÊÞ÷Âsê$Úü¯@ín°ªLßž+7ݧpë]¥[‘©@W1ÑîŸ.`y¤û®®%ŸÎ•†›âsP•t®4ÜÚªË@Wµ[„ɨšè©Jz®òžPµˆBÞƒ%Êv‹Tσ”¡‚ €™ÂhòyÔ½çèŠíÌé‹-©@@‚@ŸBŸTŸ”ÈT3š?îÒ÷éES*UyCóME˜˜Å߀Ǵµ>Í€=%{¾ûzðsIò1C9@í¯#øÕeç€ðÔ[†Î= ;Bñ')î GQ UøD|IEND®B`‚‰PNG  IHDR@@ªiqÞ pHYsMMgŒà²IDATxœí›ÍkAÆŸ7P„Öhí¦ñ£¤Z(jˆ…zñ"¿Jo¥[ŠtƇA¸ ÕWi¡DCM-æïù|îq¹¤²D:ãÃ`¾Pm„4µ˜…ù|n³×+hßö÷…ð΃t7;Ý5Ü*ú®UŠ~#Ü5Ýn,ðQÎØàì%‹{=nz{ƒ‘7Ÿ§PúÌ?,Z<˜K%²~Ë®fG,lè&€ƒŽp2³­e81Ä‹#sŸúài©…o¶†š‘2 žüÊ S25d`Щtzò«L éGàÃìÔ(ˆ€iËÖ[ ÂKù0;5*[Ì—÷{=–^“U 'AÕT£ P-@5ÚÕT£ P-@5ÚÕT£ P-@5ÚX¡´Ù_i?ñ2Ç¥½ fxðÎÙwM·û)©‘ˆÚ x+æ¸7JÏmcú/­1 Ö1!ôBÌqo”d+içÍŽXØgmuÇ숅tÎÇxiß4à•#´ÁÐÍ•d‚c³´sÇxÒ[ î .¶šCæk 9¢ÛD'›7š?×™m|ÞÿH6âû_ôá®`t}¨uˆˆnØíì%‹Ÿ|6#^UöWÖÿs`ñpf&qÑ««ì7ŸÏN4µ˜€ÔOYÝa0†3³‰Kå*®ñóùÜDsÈ|¢¬¼sCI²x0“Jܨ”Tõ±9=ö®ëewlÎ^ç_TslN£Ñ¬nþhwzFIEND®B`‚ʉPNG  IHDR@@ªiqÞ pHYsMMgŒà|IDATxœíÙQ €0ÁÿêDKÒ÷²é_gBëÝ{½{—îòøP¨ P¨ P¨ P¨ P¨ P¨ P¨ P¨ P¨ P¨ P¨ P¨ P¨ P¨à*×_ã3ý xâûp‚‘w BdâæsIEND®B`‚û‰PNG  IHDR szzô pHYsMMgŒà­IDATX…픿kA†Ÿw/ÁÂh‹4¦ŽÊ ¤bJ›  ’"…¨‚æO0Ør{!J@,P$éFäÜ+"ö±ì¬"—óZÜÞ¹z z›“ئٙùÞg¾Ù]((((8b”wãü{„2o0c¡ÄÈô%}ÍS'ʳ)þàÑPæf ¬?kø|žZ=w ®û°‚þcê[LL_ÖF/õzê@Üð8b 1,0°—g£ˆµ¸áñÿ"Pmx’À*pJ†™CVk‰¯÷U Z÷-–€AÀöoá-ZÏ Ú,×ê¾ÙZâû @) èÿE[¢dñÙoQ•*3‚ÓsæGß}{…OfÞYãøwáPp û€x“de¸Ôþ¿þ+ƒŽi‰ZP IEND®B`‚ À‰PNG  IHDR@@ªiqÞ pHYsMMgŒà rIDATxœí[{p\eÿ»4­mÊ£ C™ µ M6Ùl’m-E¥€0Èc€ ˆE… "h-Ò"3L--1Ì#ÎІ) ›ÝÍ>Ò‹jK µ¢)MóÞ½÷çIáÞ»wïÞÍn2Žö÷ß=ßï;çÜs¿ýç| ÅQÅÿ3d*Œ$“É9£ð´ õ:B p*€ÙÌáТO„ï“ò6ù<ØÖ¼7Ù¾MZ‰§å -á2@ê'¤„x"/+ŠþdsCCBDXa7+’OöœGðN‘Jê XãeîéP(”­”ÒŠ šHGDd5ÀP¥tÀ^+[š6TbD”€htljâÓpµM°‚ѹS‡²[5<Ý \;»³RíNýP kMB‘ëZƒ Åú @WW×qðVeœtDFAlèPÍÙ¥~ù7»»ÏPང`=s…˜ @¸¢ìIúä¹RÏc;ÐÌzß6ˆGuÊâÅ¡@Ò©oñt§×¸ÞÐáÀ¨èÍKƒÁnœSUÕß?¬­€ðfºé %à£{wïÚxùå—kn:ìÚµ«ª·o` €%_“}[#‘H®P?ÇDé¥"Øf‘%á¦@ÔS]‰ÔÅyÀénøy ÒùqkS`‹úø®Tpü™Pnk -Ô§à*@RDð+³?Øäæå‰„/Úy "Ïc¢/‚€¯uu§V’,ºb…Ãu|Ðä³ðžL&3£PŸ‚Jã©T€¥QVÑ”ûŠ9±­§çM¼/ x‹=c"s¦ÜëNÿ9‘HL/Æê›ý;@Þ7ˆŽÖøÝB|‡ Üaý¾¥¥~“qUUýþ¬ö€/fMp÷-r™.Þ ÅFB$R;,ÐÍЏ½ÐÞÀVY,¦~ÀùQ–YÏN†IÊÀhö1@Úœxå€À¥ñT¦è(Ü»{×F» ¢“æ¶à+v\ÛЛ» €1b[ÂẜŒÆ“™«A¹±˜så‚Ľ±dÒöeŽ`låà³–Ž×Ùqm À…¦gò/N‰Ät«lÜuê6qPYSl»«S1û,r]Ÿ¼d2™ägkéIû«£1Å{; %ƒU§nºy§q¹'ÜT‡À8jkjk4YyyÑà7XK‡B¡÷­¼#hoo÷øÇ+ !oslÑ¡ÃôátO~ž2/ºŽEÑ6+Lj“kçŸ`Žg2@ .šJÍ/Â2ûNœieä@„gX»¬#<Š\æìÄäAÑäR§vñX}—3¬»IðÓ“NǵŸÀdgl3ï7mBÖgñ]¯µRì0ÓD >)â…Íä7E™ëÔ<00Ãâ»Ì´rò–è̱‘T ¶ÙÚ©ÁØqº Î9ç”Æ“ _UU¿‘“ZÖ.ÒWp1ïèèPìtLøÊW‘9l|òè¹¼asãçlÇ⤂tÌ>wtì­à5ˆF-Z4j䨀¦Œª.˜]Ä‹8·O"¨8 ºº¿ÆÒá°•c7ì3 $oæ4·+ªcûdBàh[üÙS-¢½VNþF|ÛøLãfCžsjŸLè,fÛcñ]vZù?¦b£BSR$ÇfmPR5¦BØnªO9hñ]˜?bò0Tí{À§õwÁh&Sp¹Ï ?éÂá CwJÉ“Tü†Q¦‹tXyy«­ñM“)Í¬È Ÿè÷8TÌå âmd`­!žN7Á\QîêëMXy…ÖpÓ)Šä2'cÁ`ðcR3F•„w/Y²dȉ£kbõù»ô¸m²ÂM»Ô0nPÎM&“Ž'¾¡Ã½xÕ‰S!ll¬5AÆw¨¦Cu>eǵ ÀxÑc³AT•¥b-‡›‰Dr~…WxljW&¢ƒ}57+ÇÅ“é+-E˜‡ú?ÙlÇ-¸‘ßXDßÛžLÎs2ÜØØø‰hʘ” H‚YÏÅÅÊqªªúIù…Yʵ…ªCÐÜX¿ãdè÷PYYÌÍ––ú=~…a˜GPYàmd ­XbFr7AðÙæMЛª^Wˆï4¨ëúÝG®v÷8ŸÁ16û~‚ŸÈÛ~–€A°¢9¸ªØ¤‰Dâx÷e¢ã—K—.,胛âè×D0ëivó5`{&s‚WÓW‚r€¢•q¢`WÏ­ …B®–WUUý#Úf€g&e‡ZÈé^aÑŒ½{@œhw;kƹóçÏqã0–:×Å{>E.²Ž‚“@;Þü1€$E”g«ýÊkÖS[1t%Òë ¸Ù Ê*:–»¹â*oí΄ì€ùFØ-ÁÀå\èì쬞3gŽ^J íK¤n¡Èc&¡È­ÁÀúb}]'î£Éô5B˜n\ðøô*ï­¥~­J¤Ä»{n¥p- ó™@ii üÈ×Ùœp°a#€Õ&€›F´Íã“Ï”BUU¬;ó  ó{¼:Ð×k-ìDIé¬}»ß½Àóf)ÏÖÄïLôÔ•¢«lÏdNɽÁ fWð–_áN7B¬(¹vÕÞÞî™wÚ‚‡X‡X–‚ušGüR ðÏRõºAgggµ§ªúû€ÜàcÁ—½Ô®t»jÁįÊ&37€\ä%&û>ÌìÈšR.,:aëÖ­Þé3]áJØÕ ‰5ûö¼{§ÛûDF”U½Œ%{ÚHí9@޳i>òE*Ê‹Ú\j0TUõŽŽ¶éP.â"vÛð,DV¸™í ¡ìòm,¶ãdÝ£="À7hYB^`+ï´¿KÎ×;2ý¾ÁAMŸ6m¦¢ë5¢K-DX à<³tÆ eE¸©¾»ÿ+V¿Ž'{¾Fê¿&0©“¡Ü¿o÷»œÈ·ÑW9TbÉÌEîÐ\IÝö\3Ô7{}%¯æNÚŸ¦béô™Ð¸œ”e,˜ š¼$¢<ÕÜX÷ÆDþP Sò·¹XlÇÉôêg ¸ZBjÎÂXåæ }? `§¢ËN]Ñ^oml|û¿á.òQÅQüïâ?ToVRâgIEND®B`‚‰PNG  IHDR szzô pHYsMMgŒàAIDATX…íÒ1/CQÆñÿ{Õ D$Öú¤·‘°™t¦Ò†¤ÀÖIL&3½—t0˜$±HˆÐÞv˜$fÒA„Þ×b@R§½›8¿ñäÍsž“ó‚eY–õ߉iÀ tH6!»’’ÛvB½+M£ <ç\fED[Í:mä £]pYªë”iدk’×À$0hš7è}#+pôKȉèbËËk:£!çÀ°Â1Ò¿½¾­óòrï² ÂЭ°çWµ¨ªß¾Ï«éª*G@\ Ü×C:?&O¦|ã|度° 8Þ«PxLÒLl"¬ˆ²±”¢hzy¤^U3@å¡L ¼+ò))u’×q€ÝŠºŽpŒ0ôyÔa.çÊi§Y‘ ìWt¤ép<„!™åq¹‰š™ªÊÏe´,˲þœÿObÅXÓúÕIEND®B`‚y‰PNG  IHDR szzô pHYsMMgŒà+IDATX…íÎA ±ÿÆxtG ãx$v«€°Þsnr`%ãÀ5êiû)e³IEND®B`‚̉PNG  IHDR szzô pHYsMMgŒà~IDATX…å–IrÓ@†¿ggÀÊY+¸W` ƒ¤â$xà©â ,³ÃR2&UœC«`’Ø ¢Ç‘ñ É’ã Å[I­îÿû¤V«ÿ{ItpÓöXŽð²S7ÞMTvüy”W(¿ E^|ª Q‡º)p0QÚ–ãU¦·lo¥ ˜·ÂP7þr{%×úÆTåp–í-(r0ĺ#ÀNSÂr¼Ê(œPUÖF: ã ÑʼnƒI$,Ç«¨Êá\¨ºÍR{D SŸ=Š‘(數„Þ¹PuëÆ~_!¦Ê-ï)"{CŠFâbD Ô°‰òøÖÝR¯èî¢_/Ñæ´²ÿýúDf€r¬.IEND®B`‚ ‰PNG  IHDR szzô pHYsMMgŒà»IDATX…å—]l“UÇÏy» c¸@e€ (’²n¥-%ñkH0ùÐ(†h$!‰^˜  D.À1oLPjH W˜¸À”D‰¶ëÇ:¦A1ÃÁüHˆ| âڞNjw-íºnzÇÿ¦=ÏùŸçÿÏyÏyÎ ÷:d,äXì|­zrkDu©"µÀ P\FL/jϨ¡5ÜØØý¿hoïYC ÐTaÞvÙòû¾ûOâñøÔÎ~DÖ †úÚTõ„Šçbµdú2™Œ•ššéšÕÙjd™(«Ynv9®fC8¼à18›HÌqð}ÌÖý×ÿþ¢©©);šiU5±Tç˨îê@.!¹U‹üþtÅb±Î:ul˜†ÊÍÞ~-_Mx(º»»k®^ïßlúå‰`°!uW‘HdŠTM8ÌtOÐßð–ˆØ±ˆç¡ªMtnÑf”^‡l(ôs(¢Í£ˆLRÑ=±DzŸªÎëõxÀi)î+ˆÇãú ÂÕIÕžÏJf&‘Þ.ðªR „ÍÑDç›ÅÑ ßתpAaA,•ª/3g% (Ǽ^ï@>‹uÖ‰°Ý˜Jà²Dôƒï“É Q8êz”çË ˆò´Û©ß§³ûPU‘v)&U©³iˆ¹o—”@¤Öè¯%c”ãÏã¹â†5öâ`ÎåpƒV¤°TUêÆ¯¯³Š[Ó&Ovs å_D3̨bHœÑF;wÉ}'¹ —Ě醈í·Ë¥âæŸ7näs_ÉpFùÍåá’$*Ç/DKÆ:Öä—³· [ÐÓƒŸ-Ñ7öSÜãv¬Àc”D ËD8Uf #¶ÍUÔñx¼°íÂ~ÿ‚|\¹nþ¸’æE>_aG©ªˆ²ÊíÒÖ2ûýW€SÀ4KÕÆãšy©t)àË¿~gq4’ê\ ÌGù!ØÐP¸”¼ábt+€Ý‰D¦äã@ ãØÌA÷0úrdA›{~ùy]qíêêª6ª»DdÛˆÕ šè8¼‚Ê‘ÐÂú†–ãöö´WnVe9ÂL@A{¾köƒ¾ŸŠùª*±dz?𺧃¾%£ˆÇãS³â‰ÌCù(´Ð·e¤ ÉáÇ€µk׿†ëwÅ;¶€|(pe@lpp© ¶¾œM$æ8âDQîWøêvg}“×{s8îHpï¹½‚nn!ö©E~|(oÄíèx„GGˈì¸uíêÁJ.¥íÉô‹ »€¹(½Y½8àKǵ¦R©û¬|¼äf§£m¢æŽ^ÌÞòôMœ˜±¹œ3='v;õã¤ä<ëC!ïï#iTTâ£ÉäbTZ@ž¬„¤TÙ4œ¼qLŸfçR©ÙbY#ÈRÐZDf 8(½½ªrƃ´õÆ’÷ÞÆ¿Û ôÉN¿ƒèIEND®B`‚‰PNG  IHDR@@ªiqÞ pHYsMMgŒà¹IDATxœíÙ± ƒ`Åà#b jjö$ujÆ€1¾H¿½ÀYÖënrœ×sœ×#>òø?P- )€Ð@ h  4КhM´€¦Z@S- )€Ð@ h  4КhÍò6y\¿ÆgZÀìZ`fæþ}Ù—_@´€¦Z@S- )€Ð@ h  4КhM´€¦Z@S- )€Ð@ h  4ËxÇÉP+ igIEND®B`‚¸‰PNG  IHDR szzô pHYsMMgŒàjIDATX…íÒ1K[QÆñÿ{/ˆ´©¨Þ›‚bn„.ñÐM\2‰ÖA(ÝâÚN~ !_Bœ2thGEÚAH)tÕ¤“&ª½h ¡&‚CrßÍ B<‰.BÏo<<<xØ[þH\Þ»0¿jvf6yæ—™Û+æÇi†G™EQçv¬’· \ÔÀÏ9Û¿ܱ*éÃŸï …ÂÖa/(@>ùÀ…ÿõdØ95xíê[’ÚoÒ¾Â0l/Ó`<—x'Ç«6ÅAüÎÌÜÆ’¸¸=p¿¾Ÿ@IÔG%üæyHr{l’=uS¼) ·nh¼L’dí„ÌP\ŠÆ ½®õè'„ÉÁ¾Ùƒ¶S3ð÷ØÞ1ðlu€¼w£¿ð&=¿A@E¿b}EíÌÿhS|5‚ÍÜRç@†ÊôWœ>/¥½ô‹Û±FPÿƒ'=jµÅ@›„WžôìÄÉpz^ƒ’·ªm 48Ð7›_Ì|¹Þß÷Ž@©ò,z‡ ØýÎï’€óÍ~悤p¾Ú!]NÏi°û'§òÂ÷|¢ {×p1=œ! öil¶OÝqH­¡á³Ÿ%àTáX°Æ °``À'À:€5N€ukœëÖ8Ö¬q¬XãX°Æ °``À'À:€5N€ukœëÖ8Ö¬q¬XãX°&K@\i$Ië J­!Ng,µ&ÏçêDÙ»†õôp£bµ6¨!IGª)hF$у†ªäZzNƒIªm wq)É-aΗWn×ß§’¹ôœg|¾P½,àÙ§O¥–; «Ý”f*Ï»—¥½Ã …-Su]]l/—Šáòh+I,†Ë£j‹WtUûÉ鬚ÌIâÂRô©j¥œ‡ô½Ùn‘& <ç=h(U0/ ³î=gVŒT†eú@½„zA@Mv…š¡!–ð² ñä~—¾/šZ^¹ é)Z¬hŠÀ†ÈécMÕEQçNœ ‹Þðß+çÑ”esX¹F%s¾7w”²9‡ÃqºùÚ# ‡‰Åù(IEND®B`‚ Ö‰PNG  IHDR@@ªiqÞ pHYsMMgŒà ˆIDATxœå›[o[Ç€¿%EŠ”(‰’­»dÉNšÂAà&EÑ H ô©}é{ý£}*P´A6NR i’Æ–lÉ7]HI¤.äôaöîÙ3‡¤äôÉ$w÷ÌÎÌîÎmç²âxã@œòžþx“„ãùMB–W—íàðœ<Ün%`2‹/m¯ ¡fàbȘs g´w¾ÑþÜ©*Ï£5d¸í =^ø‰§€9ß.ÀYÐ>ëÛú¾£ 4Šï?÷4Tý÷%p—§P9òmS¨ ŸÿÄÿŸòmh§¾})è{îq1o…R’- @V1k Fûȼÿ=rä®ÿ̀ܲ#¤îÇ,¨àå®Ç1áûç@6óGUší ¿)_óˆËÈŸõÁ\ß6HÅhÛ?·â'[ŽðÝsr<ã%ÿÛyF¶ uKÛå-ƒÙ2ÈOŒö ?Ú¼ð‘0’š¬0Ƕí–íkLN`¢âöYN{IçËõ—³Ï¥»¹ò«q'ßlûPcÊÈmcü*Ⱥ}M|ó^y^dV·¼Ù7òdÑèkúg#Ë&›úÉ‚q&äà> þ'Ê£ì!£«py *_ád xZЗÚá< ðºO ö*hë(]”É)¼˜7˜0ÆÄ4Pæ Z½íÇl/T¡ñÄ,ùÿ‰éJ4xØFÐö %† ‚2¼çŸ]~|Ì "œÙŒ ÔÌ$ÿ7€6¸]?Q5]›À×à®ô|¹r ΛB}oG˜ŸøÙE`5©{ö³Ò†ú ÔŸX~€Ï€ç€·<ìøç{ªÛÜUÑÄ[ó&[YÍ)^|)FËL.¡¦rF?7YõÊx=kUÒþ™~‘ ßx+¶ŠQ@ɘµ¯² ®]þÜ „¬ÌaSçÜSß7檛àŸuO€Å}ØÌ÷‹Ö¿÷²sºžò"ëGÀÀp1zæ¿ó[é1Y! ¹ÙWÆÝåø<Ž îRq7ûÑn=¼ð=záê)NÇX-%8çW/ÙF•L)0wÀ}àÒ›«Wèî MžŸÐÜ%èúãV;Äö÷§.Hxfû:VîGèÊ­c ¨/’Ñgîä§ñcXy .*PFÓ)ƒ­½ üøKt桞?>N¨ùÎyÔRøÕ9‡úád]Ô<þø$ Ù+Ïz ¸ r-fn%bÄÑá]`&ªtûÙá²ü pŒ C€îm¨M@ý%¸NCGÙ¿Rà¢Õ“¼ÖN}“¿kàö‚Î6È ºcßöžkB[ŸèØÇ:  œû î¡«ò…M³,¡Û¾‡­¦U¨µU‰2o€ëè3Õ¶2«0ª£‡ú+ˆ¾ñcîùç:d¹"+Àð õŒ|غພàH1ÍC»†®ÂùX<Ûp<…ö$/`‡´]àLçŒÁ¡‹úƒç)Ç‹%€ê=-æ·|Í)p­l["„Ë÷a®³¯ÌGo³¯`öRq[G͵`® a5ß-ÔÂeÀÀ4ª= ”š,£nªÇM¨|Ì3~ì=ˆ òÎaÂsÛYr—@Ü1º¸° ¨iÙ) d¶ó2¥d"¬`ëÚnû”J^¿Äî”6±x:Ô#4Ú ”ƒL¿}Õ²á*Ì‚t˜ÙúZÀ»ÀÒŒN’Ô¨OaåçÐ@çý- ;sä?#Á¦}dѸcï'Y 1a5rIH™GƒeOd„œtf¡~ÄAÆ#OÐ×Áä+@:gPßtoA-É/>óßÎÏùš+ átf ž-œ·‰*¾¾î†TW%yËLÂÆZ™EàKª'à%¸C4Ós–ü¤õ2¸çyT‚g~8FWìcwµýø2ºâžŽÿh_.z;ñ¦ÏgCšd˜šlOà¤)²á±©J¨oƒÛ ì÷-lå7 Gih¥ÍônO‡‡Ä©/ g‡R'¾½_Ÿ¹r'¨·ºôHe H .åã( ¾óGF{e%´òœ³”›:QxŸaß?„´ 7N|j|Íêµ°Xöߺ„V$4 1œ“ó.ŒÔ{Š„iÑú C`GàZáì(Áô;§0”6<<×…ê¨|Û5r î Uæˆ0AqTw›^ƒndÞb! ½±¹©Qô\®=¶ËpeióÀ?È´—ìöšP{fÐ! Ñ$ À£ý2b…»}4sÐÃÎÖŽŠz˜¸ÎöOìuôGô¿&ÔÅGp¬y#”Ÿ¡ö7ÆÕ³ŽÌòš¤ÒmÿÈFX‡B(@Û³ÌÍë€.Q¼@³Ã¿ÊÏŸùQÖa([9ÆHrç9my"è“Þ')ƒóIbt|Û2zÑ Ž<½ìg·± ¹CYÏæÓs÷Opí`Ž‚<£š8Ùæ\Ó`-a< ‡Å¡»ú8Û ¶2Ùéø„W×ì¡ÁÏÈVàoe_ûn0¶+æ$“ÀË„ä:>£8‹”à…m ]_‰²êmÜÐòg0ÉÛ¿ó BhÝF$VN²Ð÷-£Öh,%ØFí¥?ûmnd_8ƒÆ2œœá%^œ(Ιew1,S|¼’…œYµp†*—rA†8î»÷O·ýWÐ8Ósúc¬*N¾Âô¥­®-t™ƒ³.ÊSNxEù€Äf¾mÔ«8’êOÓmžfa.NÖ&°SÓOˆpAš4pÞtɺ×í+rJØîí°õKU\²8ž¯/Î]I#<ó¡(«vúÔ4_TÀ}ëêDV 6ƒñ•Ó'h9À2Z'ÔÆ]¿E¯Ç— —¤ NÑøü=/„a‰!­5 ºÑVMÏ%ð{àS—£‰LÊk'õf;…ÃÝgÙB ¶†÷7¢ö¤oÍVCܨF0}~óREªh íÈ»¶N÷óÖJ>dŒ¡C #ëNफõ€)¢ðŠºîê%¾³ã‚Ì )ûïÑÌÏyŸ¦àà žän"–º@•LIìÌ ¨LzÉ…´ûЪªÎ¸V]î˜ eÅݪ¯úc{º2_*¨nËÕ(X±@ Üžnyy( §¨Fîÿ ˜$?wà”„¿¡ |H¼¿U”ѧ0'Q¿9ÆûÐh‘¹¾“ŠÒãv@îÌfà%i.Àí¢u6ÉÖTñTÈf"NT€ì£µ?÷Æ— hÝF›%à¹î°œŸ&]Õã&ºêeTyvÐÚæ%eÐËØŒ3íwš WÝZÿFfŸ£Bxç'P=R" Í[_çh$›I€ƒ ¿»ªÉ !{f¼©3ËeÅ/Ko#|<À½ Æ—ã{FëÄÌt=!Ñ‚EÐ+ïeß¾´ b÷‹ƒê¤g°SàÅ­À}LÁÑ÷Ùë9qž¶øÌ“B“;ç'S._ÏZðfè}ìDI2æÿP.ºòó¼ù•¦ZŠ\¹ü̲^ùÇØ/L¬’{aBú‚…ñC:f{̉CœψCßù¥ÑWÎú+©ªŒÿÂDŠèONÈF^òúÊJî¥^ë•™´­äŸ·@Þ1œŸ²¥å=óÈã+ž<‘ZY¥{€àw‚q}-~kÞÓO+tž*Œ~ijÊ /+ ^¾*z9j3f‘-ÏC)¿›bD†“#›¨¶õ5º´üïjæzrÁ൹@ 7 åÄ)JJd{Áÿäö&ym.(—Méëúq5a§þ÷$ªÄ}T¡ÎxöN“íÀ¸¢ŽA&0JÍ<ž:y-¿8YB¯Ê’#Teà_à™8$kó¬'…Aâ3£´6}¬ˆÇ7÷ÕY«á € Ïoó ¨þ´ ý~Ï"[ÂIEND®B`‚‰PNG  IHDR szzô pHYsMMgŒà3IDATX…í׿JÃPÇñï¹…t4.ñ2uòBû…‚¾AÕÁîí­ò "øè)ïÐÅ86`ŽCIÿd2ÁÁû~î]îÿ9,×:”‚[ Ü–z2 VÃüæR^ϱÎT¸¶@ |¶8| 2»äá°Šu„ð"ðfÆ“lZ* LÔ+r"…+5Œª›0?'„)°í¢`2q¹|1­æ¦v&Ò.Êë„t×upiïÍ›£dç§ ° ° ° °€: £ü½vÁEù8ˆ?LÔëª;LÔCñw]û5Ì~‘uxz׋"'í±¨æ{‹É*ÖG„{ GHQ²VÚËk÷§q1©²\ëp÷oê¿×_¥|óX{,W3›op_&šE]eIEND®B`‚­‰PNG  IHDR@@ªiqÞ pHYsMMgŒà_IDATxœí›Ës[E‡¿ÓÎP<&‰%¹˜Í”+ÅäA Ô !N Nò7¤"ÛUP,XÁTlXÎ`YžÍü¯JlKÞoŠç¶d¨Ô,,[¼bl« Y H²ÔWº×öÔø[ö=:Ý¿ÏÝÒÕðÁü?#µwÿ+wÃü¼<ŽH¯¢×ŠòŽ1úìD<òÍj/°bÉÜŸ­•'T8 È‚Š¾rÍyî?„.UÖV ˆý3“]²¯!ì­¸tÕžLä½ÀVîÝCÓw*fØ\qéS]Øô×ìc[çÊMeƒÂ’>Q#<ÀfDF¶'rû}\¯¯Ô °Ç\÷Ë“•ƒU=QgŽöõ*¡AxTÍÉʱ*×5˜«‘‘îáé»<¯2 bƒù}ÂÑk+Gª(¼å0g»Z3º$Äóû¬±c4 ¢ÕÙªX+g‹s¯¹/á¹%•g*«|=ʈ±Gp—°&Ça9¼Ã¶`NE{¾e*/Լ螾K­Ú&¸(Ø'û:?p¨m™®dþ£v ¸Ù¡ü'=’GÞ­uqE°>%ø€õ%ÁïðPëe°‚É3ï£z˜kT lVÌhl0¿Ï¡ÖÑDþö6-ŒâÞÒÓ(<8ì€Û¹ýˆŒà¸Œ5‡':>tí_h"»‘˜"t(/†¿ãÒÛY@4™»[TγŠ¼†·ptª/ü¶kOà7 #ÀMå?klV‚Çð—,ôx M﬘ÃSñŽ<ͱ á¡I¬„èðì^cm*èðàð*°Ùxä]µô?9”ßlÔŽu%ów4*ôÞog¾’¦w@‰èàÌ1œÇ‡ÐLø‰3a—7o+Ò²€®¡™{ œÃA‚ ?Xm;œíïø¸|<:<»W¬Ž[¦¼$*Ç&ûCo6¹ä²õøÄ²„óÀ'½ZÂö¡ÙÛ@S¸…¿,*Gý_\‹4#ADíZ…/®Ãg¼HæTAÄéÆÊ÷ð€€Øð̽Ör7 .\‘c“ñÐ>õû@€¯ -Ü4bâLø-Q9T}áËXy(¨ðà(ј½OEÏ7x|èe¬<”½ĺJ.–% #¹—XÀJOÐá!À#PŽÃÈç¨\Á8}Ó2 ˆ%gv[H£êòémÑvÐTñ)X=¿‡gk“-¾y ÓúÄ×…•؈%gv[%ÕBx€- ©îÁ™=¾-¬‚@ìHävY%ló¡Ý5¤ƒ’ເ‰Ü®‚H·ð‹ ‹u[´@v‚¯<†ŸW±G±æðsÃjekü{;<œÛi¬ŒãÞª}xª¿s :”ÿ›`_®oøHá;‡&âáÏZ[q©´¾ÄZIhùx oDW†Èöu¼¦bÆñ8XHÇ’3»½¯øjZ°ÞùÌÑãñHz¥‚l¼óßž$(©V%4- ,|‡Cù¼Xí­¾D™„y‡¾Û¬’Ú‘Èír¨­Is_Œ MÿE0㸅_«Ç'")Os$§ï5¯‚Ó¨oÛT}ÕùÜËÐÄXðPÜ VÝwBA$ÝÌNðöåh1|9”7¾œ®ÄôA#æw‚5zpêLä ×þμ†íÍôEÆ\û×#–̲*/€§#œ¾u­ÂLÄ#i#zÇã`¬Œw çvºôn( :8}«3ŽkxᄟáKLÄ#i±Ú‹»„´‹„ºG ©ðñð¨CmÓtæP#/U¿ú¬AÞ=Tï8¬(`9|;L´ ÊÉlxÄ¡¶e¼JPìÁl_ç—µ.Öà1ü¢*'V+| ¿$T è~þÛ[ØTøP•ˆCã5 _bûPî0ÈK¸I˜µ›–öM¾å¿åƒÕ?–n+<õ¿ ød«½À‚CyH xºr°úU@¹ß¡Ù¢²zg¾™¾È $ˆêÁʱZÿ/PhЧ¾/|Þy•“‰‡G]$(ÕÙjì©÷W]´VN­§ð%2ñð¨*'©#AŠ_Ñ]E•€¶_®œ¦j<~ÑZ955ªj²^Èö‡GêHøFÛôlå`•€¯ýÓw¢Kw«òw À,𢪹g=‡/Q|^’ /3þ¡›ìþìéÎüZ¯oƒ 6X_ü LF ëÓÝIEND®B`‚G‰PNG  IHDR@@ªiqÞ pHYsMMgŒàùIDATxœíÖ!NÄ@…ño¶]ƒ@ç$„ VâàHÁb8ìöX‚E’€C"0ÜEBÖ!È&Ûv°dpMè÷“ï_ñúL ’$I’$I’$IÒ „¿Ž1ÆpùHUªL/öé¦!t¿àê9nŒ#³GÀZoåÊXw]ÅÙÉnxKÙ7Oq}YóBd«D»‚æMËöé$¼GéSËŠé?|y€Íºf–†ÙÊôYÈae M6@€‡U)"pŸFÙã– ¯E •5oÎÓ0àx>F-{®Ï"Õúµn»Šô þI’$I’$I’$ICñ̓7¿ YQIEND®B`‚‘‰PNG  IHDR szzô pHYsMMgŒàCIDATX…í’±JAEÏ$T*$»ÚD7lü-,-¬´í,ü++¿@$…­Š¶‚~²MŒÕ&6+„Èî<«-w¶qO5fî;<rrrþ;*í}×ó5@9)Nw»÷¶…´£Ã°˜D°QÌZ@‹bs\ï)ÎZ€ÞKpr8ÊŽç?Ø­p½ßLVªËÀ¼‚êDÅqQÿÚ¤+í'üƒëùò+î†íà$S àz~2N¬„ÏÁ]ªK—“‰qâ6m­3ð tMßÛ ¨hXj5 Žk™ ÌÖ [À°¾>¾¥í0þ„Ž×ØVÈ) µÈz¿Óº2é1ÚÀÜBcU!MDöL‡ 8õ¥E­ä( …Ö±ép¥DŸS‚:뵃}›á¥´‚Rrü¹h[œœœœË×`ƵÄ|IEND®B`‚ì‰PNG  IHDR@@ªiqÞ pHYsMMgŒàžIDATxœí—?lSWÆ¿sŸƒ"íJCmül˜¡{ "aèÀF¢àNÚ2¡UB-lH•ì÷B˜ª­ S#E*©ìmþTT¥&vL—VªJãw¿6`?ߨ®ýüàþFŸwîýÎçóÞ=°X,‹Åb±X,‹Åb±¼^ÈvmœöJ“¾ ñW/ì»´:¶Å€´Wœ!$@=Bάdßš‹[‹jÿH´¤¼‹„xá½)â»ùÒ§q뉯Hqý«>ný®­^Øw"ŒCV,ŒÏ2Qx»äAä|gœ)ìÏþxY*ýUƒ‡o>LlîþZ€3MARWˆéU¼µ9ð÷¹_Ï¿óO?õõÕ€Ôõ?ÞPƒÁm‚'#D :‹†¶"ÀÖÅ¿Pe0‰‰•ìðÏIñöÖßœ»h*_}ZWsê¥ îeråã= ­#2Ò¹âû„ºb("ЦíÍhHÈbH+½”ñ‹'»Õ&\¿t–J¾°'ê¶øgÙÍìÑ”…´WšìzÝ:z6 å§A|`WcD4z)þ¦uvø6åmLõºxO¸~é¡ÌÖÑ£eM¤¼áz¥Ö£uº3€×+}âjsÌØ¶Q°ÕëtÍõŠŸƒìêDûßIã³L<)çN7iøpEÀðÇQdníÍ䇸À0dµY¬cß|88ðïî¯ 8Útõ¬ÖLÉ'¾ãÓ?Ï­}ä>ít¥Ž ÈÜ(ï }[€Æ#ˆµâcº¾Öm,ÆK”à¥Ôd§÷‡Ž Hç ÃZ9 BiBX»Ôl'¤ —!÷…›§W²#OÚ¥·5`l¾0ª*Î" GB™íçú¸0Ο(ÎÄÚÌÐï­S[Î?9BT!2 õ6àDŽBƒ øM9jby:¹¼Uæ–Ç`&W>N î6/;¬x zÕ0ž@‡t ïùåcÆ4laÀX~ý„Vz À°a§V|ædXQ/¥üõqSJÓ+àæË— ú²)ö’CPÍ®f“ŸÕÿhè€`¯^ñ µÚ0œ£¯bí5 µ ÐW°ã>r‘ kµY,‹Åb±X,‹Åb±X^kþ0+>%†óIEND®B`‚º‰PNG  IHDR szzô pHYsMMgŒàlIDATX…å—Kl”U†ŸïŸ¶€` TJi^HuÓR^@ T£5D¤7D&((±°Ö HÀš–2±«šØ@¥(‰VÐÎ!Aƒ!‚¥Õ˜Цíœ×Å\˜K/3ÅïæÏù®ï9çûÏwÜë°lŒö¨À<*$–³¸ôœR˜Žšr»ü¿U†Fƒ%Æ šØ^]f?ܦfL뢢A§Ç<ãJØð ãdÌÂc®s,7c-Pµï S»©Ü~Ëš@óYÍóG žõ½ƒ´î\b#ã‘®—¼Â¯ìŠëα¦n±]Ș@KŠ}F·`&p87̆õåö÷x‰S±÷²¦Üÿ'ûZ`ЃgªJíü„ÚÎhúp§ 0öô.âf.›ä1H²Ö ÛÌh0èóeoøm Ñ&'ÕiØÇ>Ä s´ä{T€ÇFO¬”˜ƒá€kGs 91™IRc H±Œ#Ž6IËÌL£®@K·üžGø}Ș¿ÙoÅtõ’Wä=3>¦Ž1éA3Þ¯ZÄç‰IÚ/*oð.«jJ­+¦ó½}ŸDiíJM^"`FÃ8Éò%ö ±_R|r•%6„ø :lLÔÅ |ÒC‚€?ò§Ò”µ(ÈÁzD¦Ør(ÈÛ‰‚êR:€KÀ“{x*@رšÈ–­,±¡˜¼¥GÅ2vÙ›ÆÇ-çôp|h&ÁÏã¥4ÏG¿ß%ÇáM 7‹Ô1ä{a6%I<¾@,M'^˜«IŒU“Hñ…“ÆC\‰*f§PTèñß(Z,Å“% ;G27gÆc?+D/Í+Ù6À7^ìdâyÌŠ‰vš9Áµ» p=qpßÍhl£?vÀ%Îî9ŠR‚t1y$ùæºÈvšè‹ÉîÔœŒ*W$:)ÌÀdzÁÏÑœ(pŽåÎ8‘NÀG'Dª¾)¤øoW[n?{c,³@ÆÅÿ£$kÌÑ‘F n¡õ#NfæÂƤ©ï]Dâë^?»E«£MîÇêRâwƒÔ ß`¢¾íŒ¦Ç„›ý6JëZʪ¥£ù¬æùÝÀƒÀ7·§QõV‰ÝÍv,´_TÞà¿ìCln;Çsu‹-”j7fYºõ¨ó8<†qÃDýÕ[2¹”yE»ó úœ±¶ÖoçF³·®[Ïë†ùãÕ¨q¢SpÌàÊȾi8ÂÌ’Qè9VÖr§GTÕ”Ù¯cåÈìaÒ­§G#ðl&ö‚óž±­ÚoÇ'²ÍêiÖzZsÉ¡"O3"O3‘•éœ2Ž¿]Ê&î½ÿ5…¯’Q*JqIEND®B`‚͉PNG  IHDR@@ªiqÞ pHYsMMgŒàIDATxœíÙÑ €0ÁÓþ‹ ØQ,cg¸Ç’¿Ì„Öýìu?»Üp–Ç¿@€z@M€z@M€z@M€z@M€z@M€z@M€z@M€z@M€z@M€z@M€z@M€z@M€z@M€z@í÷Žòxý5>¿€}ÌUÞ€ŸxVÚ 9®Ê™IEND®B`‚‰PNG  IHDR@@ªiqÞ pHYsMMgŒàOIDATxœí[pTÕ=ßÛ!ÒüÝŽ228„l²Ù$‹R$Nk§`µªvTF+¶Î´ŽÚÁ-m¥µ´‚øGi¥Ú"2í¤aJG-358@Øìn6»a-”ê0ê´QBB²Ù}÷ô„¾÷öíf7y»†6ç¿wî÷¾wîÙûîÞ½÷[` S˜Âÿ3¤‰Åb—µX¨2O€kÌ„`€"•H$¦çº'§‘žžÅ¨´¦kߟ¸ÌÒbx`æ/9a .ÑùÍ\ñyF€ö¸…úUKKýщ ,5ÚÚjFÊüAæZØ'? à‹*Í´ëÇT–ÇŽômÐg ®š=§ö v±¶й€Ñ±·‚Á8'±´X¾|¹ð&’¼Ï.ÖÖ–š®É?9¦®LPÔÌšE¾d÷dH$¦“¸Þ¤ÿÙq…%F°©>qÔV×ÔÔ6Yã² Héðž'ˆx 8a›ìÓ§\Ùû”Y(…: µÇameÍÚ‰ùÖˆ,Dx%¢Ïs¡@\Vír5Æn¼Æt¥8é¿ûs"í±hW5Ö;ªLÄÇŽŠ*#††¦[´K•5&û° \,jm2aÉ’kR2Ê›L&½Æ˜,hÙ$!=“â7©`ó Èiã•Ke²†Í…‚Ý»U0îŒÖÕÕcl  iGU f–B\9PY9XmfxÚc77’5s^(oúZ uÌ“½¯)˜ë¬¬rÂeÑ.‡¬Ù¯€‚é°Q£iSä‚-Ú…HZc² ®ôìpþü€?”H\]…%IMÀ/9%²Û—eÀÙ³5î7r¢›]ˆÄãM0Ÿ( ôG­q¹¶ÄL¿¢H.sP[Y t±j~³­­-c³5 -܆³E Ü‹Å.wVbé@Rƒàk&Nq‹]¬­Ÿìûï2PijÖãðI‹H,~—ó ÔûÃﲋ͹+,"?·PßÚ‹ÍvB`)‘L&½¤üÈÌrƒÝðòÐÜXÿ ÆÉÐë¢¶Æ ‘¥ÄP*ó ÿ]¼ úÓÕ›rÅçTJ=iâ€{CݽYûj“Ñhô2O9Qøñ¢Eó²–Àç÷lpasãn[ñµ=:påD„–ÉdÒ«‹ç0Õ±WCæ…|÷y:œqËc–ÝÕYâÑ_ëëë³G}ªÉlx£JkJVŽUT9¦7ø|’ò)}}ÿÀÐ/H–¥Öx,„£=Cð‰YUHÙNÁÅâ÷aª¸àÅ‹*ÜXc— $%ÒÝû…`ø0ò|K“ï»…ä(¸H*èoØ `IðàPJßõÉäSV$“Io¸;±™Â0÷ã/Cýփݜ(ªJìø‘ÃOØnfy£.îHg´wA1¹&‚}‰ÄC©Ì_!xÀ,ïx5Þ™ë;ßE¿Ãííí®ÙsjÀ:ÄÒlÒ]²öŸïÃbó‚ÎÎÎJWEå·Y àbcÁ7ÜÔï §ŠÉ9þRÙXâ›x,MƒŸc:µ¾˜‚Å|èèèp_TuÉ ×ÀP¯tÄúãG?qöT¸8LhÇz“úk€\jÓüÈÔ´ÞU¬ÉdÒ{ftt±‚¶TˆÛØ-ÃÓYÕê÷½4®ÀrùpøÀ,åÒŸàŽ¾Ê÷`ì­¦‡Š>N'wQõéÜÜiSoÕ +“Ü£”ûÃ!ÿ¯G!@”ÖtÞ²À”QÀöø”§O­›~~´ù(khôÄ/øB"ò$_B¹%Üæ}›©Ò;U¢ »ž"¸ÑR¢„¬\›#¡™miçMWïîÎÓ•z”‚Ç|ÎX&À~OŒw]ﻘNÎô ¨­´°{ ßµ”´SS«Z˜ý^ZùF‰@ÝÇE@ìõ„Wð7p/<2³7Õ\Z:7.-Šn²iü»nÑUã šÙÖß[`Ÿ©€ø"§êûÐ ®Ts¥Ü‚ußáo,µ÷ÄÜ—l½¯¸?Õ<ŽB„¥µÑÇA< s[¶†×ü0•)põÎîEJSGLýoEÁ+-k}÷dcv–.uß§p›‘#qoK¨`ïHuG|æÿ²m¦ÒÔ~à½AÏ¥Ðxh<D(x"/9ÔjºŽTwDt{+€BÕ®¹´UŸY··)2ÐWIà¯ÖCªº¥Uâ®ê°k¢‹Þo D(ßn.÷¶g¢7ˆ©„þ¸ñZ 5árÿ‰Ì%fCcMJ€G“Í l ˜÷«¨à­*¦“O9¨3«O÷¾ l  =ѯÙÅÚàÉ‘»{ûd¨ Ë9‰YÆjê"|ÕH¸×.ÔÖW˜¯ñ{çÔ eÕüM»× Á€¡Sn2rJâpX_Ön/x€±×æ{;o°Æ%Ð _à1P?QQxÊ7î±™Š éÁ)åJX§L0@4\kaŽ8­mÌ 0i× ¬!6c¯±„„c&tŠI»¢\c±ç™®„ãþÛŸ z̬`±5&Áy¦—êq^ÚáŠOÌÚÅÜ6ÀÆe1@âHkm<¡uͼqå)kh4ð6=@Ì‹$ta\üæÏÇ }¦k&v›‰‚y/µN`\<ºúÚAcL¢ÓŠªÒµéYQ7`üŠ|3ay¸°7à_æ:’0rN¸t\e"DZ­16!9f¾V¥N +h.X´³)!ÆJ4o6 [c& ”˜t ÂFkL‚9ƒ|€qÿ}áÐNÌC•h–›8M²†%pt½ï"ˆwŒ%¾Ü7Þ,ì¼æåÞ¢¶‚¬qÉ–ÄL¿¢dh{zBAà2i&ä­C›·ÆÙ iÚ> jøOeܨïð:®2[¨ Pw˜8j{ìBm h.÷¶“8h ¦hqͺ>n(ŒÞ pþåksÚ¼íb“ï èò ã¥ë‹wµÁ1•YBYC£‡ÀOŒœ(l³ëþÀ0´T¼m =nåÚäŒÌì!~Þ·€qòvNsk;’Å'ï¤Ô,ì=%uÝ ëjãÁš3³„ò„‘£à§ÍåÞ„)ðe »7 Í>Dàec¼¦ô×‹ëº 2Ôê8Ê=B×oa>Cô<ø¶WoäÝa]ƒqu•œ›#x-°=l=õ™"Öã}àW5éP刯ôG…r;€Ëœ7qjÞ “³Æ#!PÛµÀƒFN(•©ÛI¹Ú®» ˜O\jsft?lý=faim÷àlƒña Ÿ Wø~JŠ´ž`im×36XT¦èw´Tžµ­”%”54zâ=¾Rn)úcáßòdŸ=+Òë â*퉾 `•¥¤Õ%²²©ÂÿÏ´ò%;:}š‹¿ƒyû>Šåx·Þ7#å…Üôßáq{¢?ÀÚÅb ìPql9^é¦7UŸÎÍÍóëÛú¾£ 4Šï?÷4Tý÷%p—§P9òmS¨ ŸÿÄÿŸòmh§¾})è{îq1o…R’- @V1k Fûȼÿ=rä®ÿ̀ܲ#¤îÇ,¨àå®Ç1áûç@6óGUší ¿)_óˆËÈŸõÁ\ß6HÅhÛ?·â'[ŽðÝsr<ã%ÿÛyF¶ uKÛå-ƒÙ2ÈOŒö ?Ú¼ð‘0’š¬0Ƕí–íkLN`¢âöYN{IçËõ—³Ï¥»¹ò«q'ßlûPcÊÈmcü*Ⱥ}M|ó^y^dV·¼Ù7òdÑèkúg#Ë&›úÉ‚q&äà> þ'Ê£ì!£«py *_ád xZЗÚá< ðºO ö*hë(]”É)¼˜7˜0ÆÄ4Pæ Z½íÇl/T¡ñÄ,ùÿ‰éJ4xØFÐö %† ‚2¼çŸ]~|Ì "œÙŒ ÔÌ$ÿ7€6¸]?Q5]›À×à®ô|¹r ΛB}oG˜ŸøÙE`5©{ö³Ò†ú ÔŸX~€Ï€ç€·<ìøç{ªÛÜUÑÄ[ó&[YÍ)^|)FËL.¡¦rF?7YõÊx=kUÒþ™~‘ ßx+¶ŠQ@ɘµ¯² ®]þÜ „¬ÌaSçÜSß7檛àŸuO€Å}ØÌ÷‹Ö¿÷²sºžò"ëGÀÀp1zæ¿ó[é1Y! ¹ÙWÆÝåø<Ž îRq7ûÑn=¼ð=záê)NÇX-%8çW/ÙF•L)0wÀ}àÒ›«Wèî MžŸÐÜ%èúãV;Äö÷§.Hxfû:VîGèÊ­c ¨/’Ñgîä§ñcXy .*PFÓ)ƒ­½ üøKt桞?>N¨ùÎyÔRøÕ9‡úád]Ô<þø$ Ù+Ïz ¸ r-fn%bÄÑá]`&ªtûÙá²ü pŒ C€îm¨M@ý%¸NCGÙ¿Rà¢Õ“¼ÖN}“¿kàö‚Î6È ºcßöžkB[ŸèØÇ:  œû î¡«ò…M³,¡Û¾‡­¦U¨µU‰2o€ëè3Õ¶2«0ª£‡ú+ˆ¾ñcîùç:d¹"+Àð õŒ|غພàH1ÍC»†®ÂùX<Ûp<…ö$/`‡´]àLçŒÁ¡‹úƒç)Ç‹%€ê=-æ·|Í)p­l["„Ë÷a®³¯ÌGo³¯`öRq[G͵`® a5ß-ÔÂeÀÀ4ª= ”š,£nªÇM¨|Ì3~ì=ˆ òÎaÂsÛYr—@Ü1º¸° ¨iÙ) d¶ó2¥d"¬`ëÚnû”J^¿Äî”6±x:Ô#4Ú ”ƒL¿}Õ²á*Ì‚t˜ÙúZÀ»ÀÒŒN’Ô¨OaåçÐ@çý- ;sä?#Á¦}dѸcï'Y 1a5rIH™GƒeOd„œtf¡~ÄAÆ#OÐ×Áä+@:gPßtoA-É/>óßÎÏùš+ átf ž-œ·‰*¾¾î†TW%yËLÂÆZ™EàKª'à%¸C4Ós–ü¤õ2¸çyT‚g~8FWìcwµýø2ºâžŽÿh_.z;ñ¦ÏgCšd˜šlOà¤)²á±©J¨oƒÛ ì÷-lå7 Gih¥ÍônO‡‡Ä©/ g‡R'¾½_Ÿ¹r'¨·ºôHe H .åã( ¾óGF{e%´òœ³”›:QxŸaß?„´ 7N|j|Íêµ°Xöߺ„V$4 1œ“ó.ŒÔ{Š„iÑú C`GàZáì(Áô;§0”6<<×…ê¨|Û5r î Uæˆ0AqTw›^ƒndÞb! ½±¹©Qô\®=¶ËpeióÀ?È´—ìöšP{fÐ! Ñ$ À£ý2b…»}4sÐÃÎÖŽŠz˜¸ÎöOìuôGô¿&ÔÅGp¬y#”Ÿ¡ö7ÆÕ³ŽÌòš¤ÒmÿÈFX‡B(@Û³ÌÍë€.Q¼@³Ã¿ÊÏŸùQÖa([9ÆHrç9my"è“Þ')ƒóIbt|Û2zÑ Ž<½ìg·± ¹CYÏæÓs÷Opí`Ž‚<£š8Ùæ\Ó`-a< ‡Å¡»ú8Û ¶2Ùéø„W×ì¡ÁÏÈVàoe_ûn0¶+æ$“ÀË„ä:>£8‹”à…m ]_‰²êmÜÐòg0ÉÛ¿ó BhÝF$VN²Ð÷-£Öh,%ØFí¥?ûmnd_8ƒÆ2œœá%^œ(Ιew1,S|¼’…œYµp†*—rA†8î»÷O·ýWÐ8Ósúc¬*N¾Âô¥­®-t™ƒ³.ÊSNxEù€Äf¾mÔ«8’êOÓmžfa.NÖ&°SÓOˆpAš4pÞtɺ×í+rJØîí°õKU\²8ž¯/Î]I#<ó¡(«vúÔ4_TÀ}ëêDV 6ƒñ•Ó'h9À2Z'ÔÆ]¿E¯Ç— —¤ NÑøü=/„a‰!­5 ºÑVMÏ%ð{àS—£‰LÊk'õf;…ÃÝgÙB ¶†÷7¢ö¤oÍVCܨF0}~óREªh íÈ»¶N÷óÖJ>dŒ¡C #ëNफõ€)¢ðŠºîê%¾³ã‚Ì )ûïÑÌÏyŸ¦àà žän"–º@•LIìÌ ¨LzÉ…´ûЪªÎ¸V]î˜ eÅݪ¯úc{º2_*¨nËÕ(X±@ Üžnyy( §¨Fîÿ ˜$?wà”„¿¡ |H¼¿U”ѧ0'Q¿9ÆûÐh‘¹¾“ŠÒãv@îÌfà%i.Àí¢u6ÉÖTñTÈf"NT€ì£µ?÷Æ— hÝF›%à¹î°œŸ&]Õã&ºêeTyvÐÚæ%eÐËØŒ3íwš WÝZÿFfŸ£Bxç'P=R" Í[_çh$›I€ƒ ¿»ªÉ !{f¼©3ËeÅ/Ko#|<À½ Æ—ã{FëÄÌt=!Ñ‚EÐ+ïeß¾´ b÷‹ƒê¤g°SàÅ­À}LÁÑ÷Ùë9qž¶øÌ“B“;ç'S._ÏZðfè}ìDI2æÿP.ºòó¼ù•¦ZŠ\¹ü̲^ùÇØ/L¬’{aBú‚…ñC:f{̉CœψCßù¥ÑWÎú+©ªŒÿÂDŠèONÈF^òúÊJî¥^ë•™´­äŸ·@Þ1œŸ²¥å=óÈã+ž<‘ZY¥{€àw‚q}-~kÞÓO+tž*Œ~ijÊ /+ ^¾*z9j3f‘-ÏC)¿›bD†“#›¨¶õ5º´üïjæzrÁ൹@ 7 åÄ)JJd{Áÿäö&ym.(—Méëúq5a§þ÷$ªÄ}T¡ÎxöN“íÀ¸¢ŽA&0JÍ<ž:y-¿8YB¯Ê’#Teà_à™8$kó¬'…Aâ3£´6}¬ˆÇ7÷ÕY«á € Ïoó ¨þ´ ý~Ï"[ÂIEND®B`‚ΉPNG  IHDR@@ªiqÞ pHYsMMgŒà€IDATxœí×± ÂP Ðû X„EØQe¢”lÁLe‰Ð¢Èmʯ<¹8»s@?Ú:x¾—[’1-çúliJò¸_Ûë;<ƒG\>I.IÆuX +Õ†,™Þd{S’aïÀÿð ƒG\>ñ Ôütâ#¹6+ÐIEND®B`‚‡‰PNG  IHDR szzô pHYsMMgŒà9IDATX…íÍ¡Ñc']±‡Í,â=Š¤ŠŠûéÚ$I¿ 'Ú\Ȧ½¦Pâ‹¡$IÒÍ \˜ª¶QIEND®B`‚˜‰PNG  IHDR szzô pHYsMMgŒàJIDATX…í×± AƒŒÁ‘Ò§@l†0ÿ½£k#‘Qî¹O87†3®Òtö’ÔÝàæ}Fjëäß.­ ƒ™-(ìIEND®B`‚ð‰PNG  IHDR@@ªiqÞ pHYsMMgŒà¢IDATxœíÐ1 Ä@‚c $8ÇÐ3ðÍ­}îÚçʆ)ÇÐ:@ë u€Ð:@h ´ÐZè­t€Ö:@ë u€Ð:@h ´ÐZè­t€Ö:@ë u€Ð:@h ´ÐZè­t€Ö:@ë u€Ð:@h ´ÐZèíó¶Ið<îªIEND®B`‚…‰PNG  IHDR szzô pHYsMMgŒà7IDATX…퓽JQFÏl¶±±°µ ¨«¥¥…˜!…O šÂÆð‡ànkÄÎZ°MÒø’Tb¥M"HÜÏÆ„`·waïéæçpg.x<žÿŽeI–dI—€ùêŠ}ä²$ŸN xoµT*T a–bìŽãÁ"£Bª‘Ý ÎÆqÜÑs¡µu;F<þ†ËqG®™Žð/qGš4õʆ]gíáôcz“#”q•´µY¨@Ã,Î0;%ñT¨À|aô\ës H²Á'MDxû.Q.T érìC Ø>Xµ~Öοফ} *`§¶f.}B—¢¤­-DSÆ¡ëppXAÒÖ’Œ{A(8¯Fvé:<³€$S@ ˜nûGy†ƒË Ä«‰ ¨4ÌÒ¼Çó¦|`’R?IEND®B`‚I‰PNG  IHDR@@ªiqÞ pHYsMMgŒàûIDATxœí™QHSQÆ¿ÿÝšsÕ”j›Ë‚”Ý‚ × Ì—‚^*‚"z¨pº*ˆÞ‚|¨··m™>„DDQö=>Ams¥±;…DC³m÷ô’±Ý]‚ç\¡ÿïñûþï~÷ܳ;€a†a†a†a˜ÿ ’m 'Œa§þØËt‡d{®©ìüä]Wð-”k¦0dcáQ™¾+A“ùá¾ùõî*CÒ^è¹V™¾+AÅ# ìtÍ¥ùS9Ùþµº Ó¬Z`–ÌYô éþµà•ÄOwƒÝHoΗ¤û×@É¿¸yÖÔH·›EÆ+–CÙÌFƒãdŠƒV€v=aÜT•ÊÒg0}¾é9 º`3ºI§UfYBú.`‡ž4† p¦*Œf¶¥£á7*³8Rèq# BÕy@siÍ©ÎÀ7U9+XþŒ°ðã—oâòö»Ùjãè>ìA^¿Á3!”ÜG ‹QD1`7Ó“ùï*28~KÇš§MMì±5Dâùû²ý/²Ñ¦÷‚ÄQ«N$NÊö^€fºSŽø:aj¥%i„•F¬:¯e{;^@8>és <ÐR1 \Ow‡Úeû;[ÀáÚHîa{Ëe$3Ñ`ŸŠŽ ÏLÝÀ‘rO·M{Ad{HZmœ; 'r—ºm‘ßj.­Cå›"G Ø™0Ž à¡Å‚J®}éž-“*³(/@çÚ@4  ¾LžÓûS]¡wªó(ýÐr­ z‚Ê‹/‚p܉‹°ûÎ×M(Ò€Š³¿Å2]¡gªrXQR@¤?SW0=@ØU9¡ãÝÁ»*2,‡üú„F^ÿ €Žr™îeºפû×@zúÖüUüýop zi.ÎFUíõÿBê.éÏÔ‘×?À[fùa±èiÿÒÛ¨ä÷~-¤®€†ºˆ  P&å]¥Òáµrñ€äÆbTá,€i¤:ô±'üY¦'Ã0 Ã0 Ã0 Ã0L-~Çd¯bIEND®B`‚N‰PNG  IHDR@@ªiqÞ pHYsMMgŒàIDATxœíÖ1JQEáó2B,,D ;‹`'º€tº×aãÄdö)í²l„€‚ hÀÂBdòlåb•pÎWÞŠ;·™I’$I’$I’$©Ÿ÷~¿ÈÒdQF£90ÿíüã›Û{ëT³„#`eQÝ2™âEY´ﮟÒcm€µ^oµ»7ÀVŽvMªyÜy¹¿}þ¶Ò§:tÏø/°Qa†µˆd©³ ‘Ã4ªÐ0õWKè‘Gà2jÌø8rôÉlRUñ$ k¼ŽÇo­öç>Äsà=KµÅšâ°,ËÝô þI’$I’$I’$IMñ›§6ÝÌ®aIEND®B`‚ȉPNG  IHDR szzô pHYsMMgŒàzIDATX…íÒ!1„áÿuï@ÜCô&Ì æ È=ݤw Õš&¸¶f>7ÉKfÄ‘ɬÉ1.]ã x3`½l1¸=Î]@»¦Ïûê·û€r€ø£„ðïr„:ÀÍwƒ4 3cáVÂô'™î¬ š7^IEND®B`‚h‰PNG  IHDR szzô pHYsMMgŒàIDATX…íÁ‚ ÿ¯nH@ï G ˆIEND®B`‚þ‰PNG  IHDR@@ªiqÞ pHYsMMgŒà°IDATxœíš»ŽÓ@†¿“eµ4lʦŠ6.“4))yV{ÉB @Ë€%—½p‘¯²Þ2q™„"RDA`‡"Á±“Ø™Ïds¤¶ÇæûtlÙ#úֵ®ë\RôL×N­ñxŒ®O¿Ga8yüF1Ó²S•Zs_¡N€@Ix Ü›S*bb6*ÿ¯¶ããVR@ üžÇÇ®Ü%¯„ƒ~'ü¿R7ÁYðƒNø9霕VD@^xXËÀƒç–…è€Oè‚è„Ï膘€O˜‚˜„ǘ†‡Ø€GØ‚è†/‡¢x5ÞROúÝ˯“ÇZÒ _©5÷EqÜÿäM|Œ3LÀ'äM•,Á_)QÏâc _´âÑ {ù)>¾Ð› Mø~ž$S˜à¡ ®ÀC\‚Ë\ƒ‹\„ØÈúÇyʼ(ŽûQxš%ËxØ„ïe„Ã\‡ƒ|€C||‚Í|ƒ|„M|… ìÁK»]œeÍ+õ=Qòr¼¥yMÐuø Þ%g˜Xô%o™Ã˜kIÌyøÝÆŠ8¼ž5A/à…wñ<-k‚>ç½"/ü:ì:|e·qˆð~:oö+òBà¼JŸ÷Ü0·¬Á#G½èâ§÷»áGWòR+‚-Z­MWóªÕêMyëZ×5®¿pˆÊn2ÄIEND®B`‚¯‰PNG  IHDR@@ªiqÞ pHYsMMgŒàaIDATxœí›=LSQÇçµ~$¨Ñ‰¬:ˆÆjp4$Œ8hHX›Â`‰bœüI($.2@G‰Ôø1à FÔhH$Ò ¾¾¾ ô¾×KÃým÷¾ónÿ÷ßÞ¤ç€ÃáØÉÈf‚Ò/4Ù\G‡z\%4{â•¶eV€Ï*d%ÏÓ…e&ÎÊêF/mh@fNÛ ‰DfµæQntŸ’gÿ óÊ=PUÉÌi?0Ss“PŽ3#¯´OUË~Ñe ÍÒ܉C[5ánfŽ[eŸ‡ufæ´ ˜)êT¾©p;™gjï";;%­T3ÆÇ5ñ«‰ÆU‹¢ " „œ[%¤_h²iïý?{ç¿s\ém•¥´GÎð¬ÖïJðDáÜz§0¿ð“–àÆX²šëè¬ù¯Þn.×Êäz[eéwŽ+(ßÖ;•£ÍutcK PKEmH_m‘/±(‘ÞVYÂ#íï Î B (œóë$óLE/¯:$rLúÛ¢œ Æ„ þÆÞE>F¬«j„h?Œ 3 è†·Ývû­¢½äöZö°SpØ`g€m¶qØ`g€m¶qØ`g€m¶IF1ÈhVÏ+ÜG9Æ&ÿk0@w"ÜìJÉ̆Ñ`ü Éê5U¦QZˆò>ã¸*Ó#Y½f:˜‘™7zP”Ǧ"*E”G™7zÐd #¼UÎûMÆ0ä@ACÅìøMÐÈ€|’—Àˆ´T‚†Š12 û„|WáºÉ&¨p½û„|7Ãx ô¤dL„ À[þQq£ïD¸Ð“’1ÓÁ"¹Îcã3Ùn´-À6ÎÛlã °-À6ÎÛlã °-À6ÎÛlã é[ñ7ÆÇ5Q%-‘¢}%fÀgãWQŠª&!Ú?cJ%…¬¿½êq1b]U#— ÝßVáu0¦4Q2ÏÓ¢¶28<«õÑË‹—áY­'Ï€¿/871`a™ …ÿÞâЮOjÉ„µd颌qa~a™‰`ì–ÒåñH'rLnçtù\‚vò Tœ.¿F¡Z¤æ &Téï9-CaÏÊÞºR ©ÒŸ¬ª }ݧ¸W.`SES 58µÃ&‹¦*)›;É߬ëíX6÷I…×[)›s8;›?ÀW ÀT‰IEND®B`‚è‰PNG  IHDR szzô pHYsMMgŒàšIDATX…Å—ÙvâF†¿f`PœÄžñ$9YÞÿr1žIÏl#$¨\Ôߨ‘qr—Ô9‰V×ö×Ö X øÏÉX¤| ¬!X÷‘K –%ï“DJ\³dm›¬íô~zò—À:ôÀ0ªDYØ !ŒqÏ¥¬¬•Q¹ Kþ#0–òàÞ²ûDù‘q÷èÌLÊô˾¦€Ùx|”ŽAûNÿ+ص¾ý ÌÕ J\p¨Ï`s Åó¥Òó€'ôxÂãÿ °‚°%Üc)Ïåݸ’Å9”O‚qf$åÀ³”•B8îððdž;(ÜR›À˵ƒ \Hh¤9†¹øú×"mÄ»“¬Û÷JÌcîżsA0ýŒÇ²–õ#ÁVCu Óµ#uŽl!™50•SºÒIöÁ— ´/Imеá–]ÃtåH½I±Î¿ ‰X¶-<6xèú!hJXäxÒü%Á…úAîIg³î?ž7_as)Þ<‘ ,žIB ȼ‹‘ ÆR´b\á‰ö Hs=ƒ º ¦®œfÚ×â=åï1ChLûc @P× Þ¹‚2Û ‚ú»56î{½ýÊŽ|´`{†Ø3^†º$?©‚aRßI“©Ó=Úǽ{cé0»KŠ”NYÉ UçlÇ0‚ñ!ñ†ä½M„Œ€Æ=·5êLxF[ÒRJx5=És†¹O‡žÀ>kÏ–¼¿Ú…í QW Ömõ°gÀ0}¥ß2™'}ƒOFnߘ”¡U)õ¬-úußâ §ybÛ¾¦;D ¯ß­;î‹9`ÊpSÎ4XÀK³”âo¹j6–BÜßJ¸I¶!; 2vù¢;C•ÛÖ$4þßÀ3#!Ÿÿaî8žvÖ%,ï:yÀK8îÙÁIÆuâõ5 Q/?Éós™~´@~€åS·¼*¤7l¼‰_ ãê²c¢ö†ù->ÝúgÁ”"Ôàí6ø³L†W6“1'!Øûæâì†nàøIæw<ž{ t¤êA— õPÕ!{Æ»ŸŽw,¡¸•Ìmjn@xÐÅ|$o6`-ð áå<6w ƒëg|–l$såùÓE1 Aã“-te}r1ùíÌÅäØÛ÷‚m +3þo2y0„qz÷Ëu0n}ÔŽj¼gìÅÛÐ ¦5ÉÕìËiôâ˜pÃä}¢÷ l5;&•”ÝåT¹°óžÃkøŸ¯çˆ|Ó;í2OßIEND®B`‚‰PNG  IHDR szzô pHYsMMgŒà?IDATX…í—MOQ†Ÿ3Ö‰ &¸‘þ†ZKÔMÜAX±F£¬X). aU’HÄHÓ– ?À&îHH0Œ¿²…hâ‚ö[ÆÒv>:u£ÏnΜ¹ç÷Ìǹð¯#í²§÷^ãÀHJuN}âl^öº (}Ó—"¼é$,%ŒK³·åí%›zßQvŸÀbãœÚ³»ò#ªŸ¾êõ+W™Ö€k¬›ä(K¾ Å¹¼l¥QØâßÈVÙSQ¨ø-žpyãsji"Æ_[)ØXPÀH@í@xRïM%ܰ±L×ì„”uR ÔáùÓœ|ª€ª«Ù†að…ÜìuÓëd–U†CÉJjÆ<€‡Á˜ «E@ÕÕ,ÍØEqØ9α1p]¬?ËæWDLØõ¡aØS=æ±€\²þÕLAŽ¢Ü@¨~ñQ`T ÛUW³ö\?Ö[⾆C¿6—U4’[Y€ «¢¬ÛcG·y!éÇzKh Žsl(ìc¢¬#¼ÿ£xLë# X1Ãt“¶è:BÑ\Úö¸JˆZÙ. š™hþBú„.’‹Ò¦EK]$-[ز\9´)œ;` „0÷má½3ŠcwfÝÜ\8¼œó<Ï9ç=2222N%ƒBPû)< Sƒ_ÕòÐE·§ê]qü š^ÍÁ¹ÜÎUàcˆ€Æ±|Fk‚X½öX/oõ÷ÙM`e÷bÌa­+:Ân…Ì!x×ÞF׋Ã[¬Mä·ÏÖï˜é5„ƒ#…Ю†½:3P¿»6‘ßÞ?¡o,çÿç0Ç•š‡Ø¹y²Añ¼2à=áží&µÇLþB81ÕCˆ¦¹`f£ä=Ej»6õxý`sô,‰ùŸu«wL“•GÞlZÒT Aí¾àK ‘ôLÃăjièešvÇ æ›c ·@ß!!ó¿`ã•r~¹Ý®:ÜŸGp¶\@‡hûIc%ïC§š]?1?o€­’a–hÔ@·*eïS7z=½ñk aÁEö¸—¾EN£_‹^µ[­ž2×ë—£Fôøìrîö—‡—~ôª•‘‘‘qªü!‘Ê‹k*IEND®B`‚ ‰PNG  IHDR szzô pHYsMMgŒà¿IDATX…å–MlTUÇç½×)ˆòQ§¥µe:0N+¦JÂE%E>4ACb$AHŒ &(‘Šuc‚"~„Ä]AB# ’HÊJ ’ FÚ)m_¡†FS„¶3÷¸˜×ñ 3¥ê޳{÷ûÿÿνï¾sán)$¹¬:Z¬6ÈR  °ººNXj7v%~»ð¿ç>²P,m}jœº§,•-݉³?ý'€¡úi“dè `­—ì*4Gl£‰¤]äÚÎM“J1©*ëY`h¥'q8etýµÎØÕ‚Ê«#áöA`¾g¼Ý­˜ù ÍÍÉ1вÊBu¯¨è@¸l³²»óÜ™qÌ®© ÙFO‚Ì~”›¯]ooÿk 㬇ÃÅLÞ#Êz”bY‹{ÚÏ´Œ PO-ÖÉ?Ð]nGìmÀbî×/ G7£²èN&þqéwןàÜ>£XïÙ QÕ¦ÞD®yYu´‘ ª<<Q.áP2™Ü{›ºí±†ÒêhdCÀq¾–šwJCÑÇ9ôÝÒ@MâôŸ¾×Vi8ú.*ï“òÖ«ÜÑ÷z:bŸùM"‘H È>ƒ2O•彉ÖÃÑì“ÒTº#DŽѭi üï2³*ç—Ï \Ÿ^l¾òëCµÛ^ý·¦±Bß( Õ½åq±F„ó@휹×å9Ίtñr(ŒÏ®© YÂ6¯ÂB?¬xèÑ2ÿ€*Ô¶_Ì–ˆêQ¿ŒmäM…¢B¬=Ø)ÃÃó=’§óh€Xtf éò‚ÍG¦Š¼õœ²Iy)H%mÿ1”ÐD€Jÿý΀ë©ÎÁÛP+wNVÈ8rî4ÛC;kºÔ1A_’¸4a#—ý7RE#Ú=x?¸ €b®Ø†¹Yp˜‰†˜¬¹jYÞvj×ÈX@f]–-’ú’‰õ‚!ãX{³¤Òí9žà8N“G·œúú̱ëmÇ>-Ô]Uw^m;ë?Q‚²2 j̸ÒÖÒ#pdV°xƒ_Ìx‡B¶BÙß›ˆíð•…ëVà\÷…xænõ…5›Ó„º½$žšyqúô°;=°twÞŽ$èN71o­?/‰T5ÝgD¶àkTYÇd ¿¯ç¾™Ô‹Š"ý×öe’]× ô_;:¥$xÀR¸a*é“Ò úk½›hÝq×gjðsà9 ÙíhÝê÷Ìù»ÏÕO›d ý‚2å7Ѻiôª×xìO²"RV]»Iác…ž"ÇyüJ[KÏÀ»Š}¥ä@²8¹®/Å$oD"‘@ÿ ½Øüê“n"öëíyyÿr]ñvËX‹€6ЗœAû|Y8ú:K–äÜ ò„ G_î´bžy—ˆ,Îgc4تªÓ­Ô×kFØm¬#I8·†]Ç ˜ARA«c–!¬¯(Ç’&µ®ïb¼w4quø9áÚEÆÐ€ðÄxòD7»í±cc%tÅV-¨ˬÑ¥*D(G±.Ð.„$¥Ñ½Øz¾Ý»;þ_eÍCýcòIEND®B`‚•‰PNG  IHDR szzô pHYsMMgŒàGIDATX…íα AˆØfb‹Q6†0Rš¿ÞÖ·&ðÈå‘Kù0eü@” ý¢O¬ 8×¶õ£\€z|B †ŸàM¶IEND®B`‚š‰PNG  IHDR szzô pHYsMMgŒàLIDATX…í×Á Fa‹kS À¡lƒLéøÞñá»*BÉÔ|«ù~Ý£FÁ0“ûµ^9ú¤?£&²2;E¦Õ5ŽIEND®B`‚‰PNG  IHDR szzô pHYsMMgŒàºIDATX…å—Mh\U€¿sçÇàO[j&“š é$mCu£\øWK‹H› Z©…6q‚â"‹ ¸2Û¤Ñ¢è¦ ƒ‰Z-(éJJÅ"Ô˜&!É43´¶RƒfÞ»ÇÅ›’L’f˜‰›žÍ»ÜwÞý¾ûx÷¾sáN)&yÃG‰Zãö€nG©Eˆ ”h•s®JÿD[ÕXYû’[±¾.EŸ^æ°ÄÚC£mÕß•$Pßs}µO2Ç—³]I”A3b˜p}6éŸv­ôU‹úë0îQiVXï¥ë׎È+­á©¢6ô¥¢Æ2ºH Úù@"|jè°8KN©SMCdê%¬A¨~ËîѶðÏËhøðj=®œ*¾°ÿø_ýþ¿–Ï‹h÷Ø]Rqß €¿ÕÈãã-Uo+í¾¶J*œ€&÷Ç¡78,¶x>T¥ñdºCÑw¸_ÍÖ_c¡äì3ÿSášP,  ¢£­¡.é"±Ÿ¢:gÒs6žH?ª°H»ø÷•Ÿ%\jØÖØ—~vQkìÑìSïLÄÖÞ,žá½[fy ÀªvÍ~ yÍ=éuÀ3ÀõÀšTO¹à¹k©êϾ…‡{R8ÆÝˆÀWÃ{·Ì”[ðÚ<_ €ÊSªúMÙá9˜è·Šm…Pë È••G'²ÍÈB¿Ú9ë´œádn寮É}ˆûÀÿo3W@!àø¥z¥˜îÝ÷äÆžÌí1yAþð®<¸RÆ•úl3žïËßUʪì\)Ÿ‘ß¿ôDxî‘ ”®*ª²ÛCHÀ僡IõÌ*o’j)7?zrjÐü2ÖÊ×sVÁvˆÐí¾¶ª\ð¦Ó—‚‚P£‡¼]qÑ×Ö]>¦Âù„N-}™ªJæF¨¯¾?þrö퀫vÐ…æhMê½’$T¥¡7õ&&O^™={X¤$óêA=¬Eù<àÈþáöª[Ű›N_ ÎÜh¦5O^n ý8?oÑ¢tSoºÑU;€°H¨hg$þx9Ei´&ý‚ G€ ®jšÇc¡ŸJ_²,¯;õçš@æß^³]q„A=ƒÕ‰ q’Ó¾{mpÆ­¶â®7°S•æl5 p6“‘ý¿µW]]Œ±¬ƒIôÄÔc⣠å‰åä£\Tè…ÏÞ.µ¨£Ù¦ãÉ:Ç/{DÙR‹÷õám­qAΡ¦4V9R̸wvüw¦c?êz,3IEND®B`‚l‰PNG  IHDR@@ªiqÞ pHYsMMgŒàIDATxœíš=LAÇÿoáü ±!Ü‘BÂ.BbkïG¢…!±0p|„ĆPZ‰¥† ²g(m°1&`¢v¶¢„;¢Å)·G(4Çí³x»·Ç~ ñó+çÞ½÷ŸÿíÌ¾Ì  P(â …MП-·í$0I°o4àdºÂ° ð C[“z]/@ šY7ËÃ`¼ÂÑ<t±läÊCõ=ú¼uËaà!SáºÛ“àÛ€þl¹­’àUç"‘vxê«Ý|ÿ‚; L¢ù&]{Úÿ÷ÿvûæÄM{€5LQˆ‘ƒS{k€,žïùüh*toôØãc‡öfÙÅ…¡ -@6ÊÙd£ -@6±7 a#Ô7¿Ù¹«ÙĸÌ@ªQ¼aZ…z "ˆ—øOë|ánûϹ„gצ›¥›D´F»h!.|aâÛ…Lú½Ÿ/5èjÝ%`˜¥½4yè&¦w†i‹,âj@o®4ДȤÀ¬ž+]UÀaÀ…¹óÌdŠ*€bz¦?Þ:+"¹Ã€ª¦MR,ÝtzwLDbç ÜQ(4LBt¹íGô”—…èò} bkdˆ‚]èšÆËBZ|°>’ ÓèÔ¥çi°Edö&ö­°2@¶Ù(d 2@¶Ù(d 2@¶Ù(d Mû²èitâ{¶kböðJíH¬ `h‹µcq2 x¢‚lí`\ ¨‚0ìvo¸™6Á TA”Ég’®÷…»{—¥Ý'Oö¯ËŸ9eÏ~¼“þíì8jŽà]+ŠõühJ:©Û&¸u‘( À‘×Í€7" …… oEäu@6¨ˆ(ÂV«­Í‰Hí0`m<õ™ÂÝ€}˜xèóXdžˆÜ®Pá{ê€ç" ú…ˆg #é—¢ò»w‚÷ÉÎK2ó=È[?4¸–IO‹,Òð×Þ'ÖEÖ0à €‘bö(‚±ÔbÛV':¿B=…BcþŒÑÊÊË hIEND®B`‚Ò‰PNG  IHDR szzô pHYsMMgŒà„IDATX…å—Mh\UÇç½&%«HÀ’†úU²LãÂiiSA[Dc¾,]t').ÆbÑ€+KQ*©éhÜta0Q«K»jÓ™„*…BZt’Q\øÑ¦Ê|Ü¿‹y3™fš4ÃLÜôlÞ{÷ž{ÿ¿{ß=÷ž ·»Y9ΟS³yìxh6h<`HœV–±¾N»TU€hLá, lZf¿1ûzÃö]E‡ãºkµøPðRP”L˜qÜ3¦³FÒOãd4âÑê[ÌèZÿ¯3YúwwÚoe Ÿ×zÏ1nð0DD®Ì1²“e–‚ŽH^Kœ]€6àgçØ>°Ñ~X6À‘sjóIAðEM–W_é´¿—^h/iõòÐÌyðDO‡MÝ`ô¬Ö¤Wq±ãý+ñú~3WŽxÞ$ÙHŒA3Þ1HøF¸;dÉboa£´Ï!Ä`¢q3S_C&†MǨ¤}À‘I…€nà÷”Ñ]‰x1D}={‹›ÆyvQßãÝ\+ÞÞ²¿*ÏÛÎvK!Þ >‡Šg¡ðòi\k3bøãŽ:Öîl·Tµ ·ŽÆù xÈ9ÉGEa²ŽmÐWՇܯŒxÏçËçñtðü¦Úâóø±¹š¼,—WJßRLç^h*PPèÉ’–U²« …¾ïË/Ä’}àÿ²·‚˜ÈEÔÒ¸R¢õWƒ¾ÙüS<¿È±n¥jm&ù²ù5§‚Ê­+à[œq²ÀgÀŒçÇUSmqI†ÇvsŒ• ¯õ¾c¸øüz={ÛíZ9âÇ.¨vî_!v×ã©_è·hRÔÎcxcÆDäò5¢ËIJ×ÅxA÷$œÑÕ²ïoæ¿dZ>2¥»IóÆ‹s1!8n0Éôëpdi”Ñâ9¶ ºÈeÃ'=}aûu1å]L&õ¸óž\Ž¿`Ê3{CvâV¾e]ÍFΨ•Uì€ÜÕŒÜÕÌ'73 Áióë ÙÅrú½½í?!ÍJïõÔƒ¼IEND®B`‚ЉPNG  IHDR szzô pHYsMMgŒ଩•ªk€šZ©°â€7÷¾tO Ì‹Z9q9wÌõ¬DÙÓ_Ä:IEND®B`‚¶‰PNG  IHDR szzô pHYsMMgŒàhIDATX…åÖ1NÃ@…áÖ6Ü€œ A(A)‰*áHpRDƒ„”THn£@8Tô±3ÁH˜ ÞuÖ¦Àåzìïyw42ü÷KÚÄÒtÙ‘$¿Qxצ]|3FÇÅzÜ.®]g]›Ëâ^ãG`Á'ãñék+ªðF¸àpÅ àƒà‹ PÀX,ŽâLï€÷Ñp0…“Ð _ë 8Sá Xß+€7.œy$WŽÚGPÏb¹8ï÷ßö  ¯À³áæ@å)KdZƽ„ƽ4D!ñ4]v"Ù8ãà°>¸$ù̯ P ‡UËĆ{MÂFpË$´þ6ôås„¥Iø£ ýðφû·4æ·adÊÅ~¸v•®#kù4¦Ø_ꇻÔíÚ! ªÆ$ù}®ª"I~[…—ëvá_¶q(°¬ø“à`[gÇmu.óU5ªZ9˜TUBÖýùõÖêõ‹6åYïIEND®B`‚ ‰PNG  IHDR@@ªiqÞ pHYsMMgŒà¼IDATxœíÖKHTqÇñï¹ÙsZ¸ Š rT›jlr‚Àˆ ¢¢‹(GC²-"ˆ bZ=ÀEBzÇEôÚ„ꌶi•aÑ*„èeó:mn7³òÞ™ Ïgù?ÿ{îïî½Ì€1ÆcŒ1ÆcŒ1Æc¦ Ú ½WWˆC'ÂbàBC”s"¢!d›PªOwÎ8p8Q-÷ƒô <€Ž´>6z–®FæÒT·J²A{{©ª¸C9ãY~Ÿ4G%7Õ¾NÐ`òs}¿ð ã©Ví=.Ù­n†6ßá¿ß>˜ÀÈÃ!”·Þ5Z ô\Ô¥Aû_|¦ó—ÍçpÐWúв7ÈÓ‡&ÐÙ§U‡ûÀr_é Û÷Gep*}¯ftQ^¹¬ñ•Fv6TKÏTúz~êcò²#<ö•ŠòØÍèö¿íéöëʼÒËχvŠÔ„qxiMqýPÉk¾RD•Ûí=ô§½Út“ =À_©Og°>“çÊ'à•Tuª8­Ê‰ Êg_E9Þ"RüÕõnZë®3½ë*ÜÌÁÞæ¨|3oèçöëÚ€Þuºä‰ÆZûa]URiN©Ð2AÊÖÈ0Gëê¤vÎ’ •Ö­E¸D|¥§È®DLÞt=ÓYŸÆ¸„ÒàÛ§(G×Ik©2–t©´®.Â]`‘ïÎC[¿ÂÈlå†ÂfߥcN‘=‰˜Ü,e¾’ •Ñ%Eå°òOö Œ¨ÃŽÆµÒ[âháý L&•×Yaðð·›…¡¼CM9e@sTÞGæ² %5ɶ'Yˆ7­•åÊU–OÀKUÅÍ’¾ 7æå¨¯‹Ë—ræ)ûƹýzY…FUåü«jNNöÿà¿”ìÖŠŽnó¯scŒ1ÆcŒ1ÆcŒ1ÓÄ7‘ŸßYy·ØÇIEND®B`‚ ‰PNG  IHDR szzô pHYsMMgŒà¾IDATX…å–[l“eÇÏÛn.¢“àÚ ¶ŽvŒ½QI¼ð„ƒF1D# ln‘„ ”8 ˆóÆ„I;!áj&.0%ÑÀ•TB‚“­Ëlwh‚¢k¿ïñâ뺯t‡nxÇsÓ¾Ï÷ÿÿÿ{~à~™ xÞÁH©Iä¬]‚RŠP‚`P" aTÎX*í½µþÐÿj ¼µ¶§^ѳ¤='¶½­»¶èû{2PÖtýÄuÉT?J‡ÀI1ôZ»ß{Dz¹ž"Qï\ŒµTTV+Ìqàz"!²áruáठÌkŒÍ1Ð@¿ u³"…‡NïĸCªS,|[v!”WÅfUwmá…¬  ”aÉY @à¨ý÷ížÍþ=®ð]h= yïÙÜV#ÏöTùÏOh Ðp-_ò?• {Bß{ì{2â©P•ò–ØVEwa¯šE¿×øúÝïÝ}L^b¯B%*¡¾Lñy#¥ž„·Ê].*³E°U¹‚rÜ‹iMÑnÕú@k¬LT«âbAu ":ê Ìß{Ê6ö9 fiN°·fÆÍÔÇ:5åűýÈcÌ·Qù0ôŽïK·HeÛÅÜø‚ "²¢»Ú"5`woÛØŸ%}íÌŸ=œœÊ±Ä¦!º'Ø݇jjpk!ò€­Zïþ–2° )6x ¸ž3=Úäf G·«ðJ¶QlÜìN„ªüí —/oŠ>‘a a¬•€ï\»ph8<0P&°˜ä½)ŸÎ?›5Ò9–äy%Ã*/¨ê·nµå] g2ÒɘfÙvµ;aD¿s¤XœiJr9mª+¦ ž }9+¡½É¿%£(ðª=rŒT¤lªò¢2ÇÝNÄo soD“ÑËŸ bÆÏÜ#ä €„WŠR çºr®ºÖƒÓ†¹û†/¸”Aþt~y,CO0ÅP•´¾ÆJ-g8•s¡O'­,K#±u?0•·`ÈcìVwÂcd)€ÀŒ×t8Ьx²ISÇ®§vf'* I;Y««°»«ºhäD©Šª¬rX¤=Ã@×F_Ÿ:Î n­r“åã{ßYŠlo"ýª'ìßéÎZW•Ào¡j_ª6HÛá{+€u†kùÃù_j$ž¯…k@ö0þr$Tewhzá:÷+ZÙv1WÏÔè¶1_C€`óààM£Ýÿ«ÏqëÀB±¤V„åÀlœu¹òñHc×F_Wúd¨[¢@ ÈéPµoñ¸œ:pè' …/z"þ-c$mêœóµbú]U‚ÍÑ-Ÿ}ÆcžîÚèësCF]T§Ô³À ”¯s²¾s“ÿÖ¨"cDeÛÅÜ¡¾½ÕÀc›ç»j}?ßsWU4ÇÊ-µ!Ì"*ZW.<œMQ(޽&è. „UÍêž߯£ÁÇÝÖsý5='þo3ÈëÉT¡CEObko®Iôßñ#êQžËr^akOMᩉ “*1*ûç&¼²F”% ¥8/¨çj r5íÝ5—&Ã{ÇÐ0ã×µæIEND®B`‚¸‰PNG  IHDR szzô pHYsMMgŒàjIDATX…í—ÏJQ‡¿sÕ¸º¨­m5Ò°ÐRZè.Ò•o FÅUÕ¥ˆ«º¨X©‰Š î„BA_ 4mŠB­$‘ºpaÄ{º˜L:jþM2®ÚfÎ=wÎo~g˜¹þuäb ºž¿gÏdÑ»@O@uŽPÙ1mšÈLô~¬*`0™<¯$, ¬(s_¦z_\0´V¸¯¢€ß 3!SLš¸ý+ˆªwÖ¿_/ÚPL`¸f \'ÚÝ$…9@f²ñÞÍ »”ds(•UÞÚ3™ž˜r–ÓsB¦˜²¸srš.Õ)Ç<ã=µWÂç§·~–NÃn¬½JnÓ &s£ IçJã_ã}ïjå* šÊE¬ÊÐåD$ ôךcj úbAU³ñ·xcæÀÀÍÂ4ðÐSe©Þ¼@ˆ¦rq>`eÙΆ׮^@eëEì8‹bëM¯Û‚zouëg3ñ¾½úêr@’À ç­h*qGZ±ÞÅïKØ¥jÞ° ¬m€6e}ÃTYaµ|>è/L!-XïR·ÙÃðš ÛÞ˜«¯ÎÅ|Zß°ÅŠØqà¸F–oëd&ûöJ¿ëЍ2›™ôg½/Ùá×[Í[ï[@•V4m½8­°VÆ€}`ßZkÖzß?£oÓá÷@¤nbƒx8gÔÍ/2¼rÐ]:-\ ²P´¡ØU °±R­]7VniÓ„µŒ ,¥òbNNÓž5\K ¯tÛÎŽ˜*/kÚ4áŽUÚ˜$r¥tžêÏÖlÏêµE ¨ìVÚšýç²§ôût8IEND®B`‚W‰PNG  IHDR@@ªiqÞ pHYsMMgŒà IDATxœíš=hAÇo/ÁÄFDD ƒ±)òa¬ý-ìB@Tب¨¤³ )­Œ¥ ŠÞ›Øˆj'sI!¤°P¢p "Š Éí³HT¸Ý»ËÞîdrîþÊÙ¹÷þó¿™aöAJJJ’‘¨FçtGóxô"´ÛbÐ…e”9Æ›h“_•:G2 —×Y…ýQâ˜B  ÐïvÉ‹r}œZƒçòÚ<ߪƒX×6™V·\ŸšfÀØŒžTe’n2EàTÐLmÀúš²/i›„@aa;‡Kß ¡ÿÁæ%êmð°¶š—(m?…=zcQdƒíá XÛêê“í 5„©¸Ï»]ùl…\^µÂcŸözy‹#5À¶Û¤Ø`›ÔÛl“xª„ÎèÞUkÇ€=Õúçòú¡V1ªP&‹Ü=×#?k"gyÆòzF! ì2-$€yκ2æGUN‚¾“jÙ%›Öa…§Ø<@ ¯s3zÕd’@²ÓÚ‡0h2ñÉ ŒäfµÇTŸ¹7z@„{¦Ö@Ǧt§‰à>´!ÀH²´¬d¸b"°ÏN›HFtùgÀ½åCºB_ˆ™’)ÒŠ0Yî¹Æð'ˆÐ\êš:•¸?«d<‘+“ø£pj€m¶I °-À6©¶Ø&5À¶Û¤Ø`›ÔÛlSK}€-V»ñÝË¥ ÉšÊ\iS² p÷7%Âb£¥íI1 ¨ÐT7œŠª\.W/\O»@hþK÷w—/–þ ø[.ï42r¡]*uö]5ǰךâ£Û%­q z|‹;IL|5Ôg€ÀK‰"£¼2Ög@Ñã°b"Y¾78Ü1ØgÀÅ#òNÙµÿPÜóòÅDèÀsÀçNn <1‘04ʰÛ-ÏL…4à†ˆ7ßIŸÂuì-‡¢ô¹Ý2d2IÕ/®Þj{ÆaPá8°Û¤˜uA&d•›îQùd:_JJJ²ù N¶laXIEND®B`‚†‰PNG  IHDR szzô pHYsMMgŒà8IDATX…íα @1ôû/FÄFüPúúDŽX”Õ“Õ³ùx›ñEÍ ÞîûIEND®B`‚€‰PNG  IHDR szzô pHYsMMgŒà2IDATX…í—±NÂP†¿sI`´.”Õ C_ O`"ïÀ¬ #Îê ;Œ˜ycâô:(;íB!ã@k*Û8xÿñææ|_î]Îÿ=²p>‰®npJâ$@ 0|¿q_Ž \Lâè°BÐrørx@aðví>´ŸâŽ1ú ¼n7Ú÷Zq9ðtþxÑ45™— ì%LvÁí«*àó^+Þn´ ¬ú_ÜÜ«€ç%€PP¿HÀ)ïÏŠ&Šœ üI¬€°VÀ X+`òIº½Vq] €×/šU¡ÓÙž"Á€Âh˜šÌª8›Fnº–×FÙù÷b2Qî5»b’”ƒ‡]1©-&YÒjÖÔÏo¯¿Â£KE…Ñ~5³ùÀe9xê¥IEND®B`‚–‰PNG  IHDR szzô pHYsMMgŒàHIDATX…í“¡NÃP†¿SØ!ë&'è €A"p8$l< L`x¶2e‡&áÐ%0IJµ†[éÁl4Áõ6©¡Ÿ;ɽÿùr“óß‘D§UŹ"€Y¡X–&i¬D§ÏcáÂlúA_—²hK¤*»‹Ò™a¶ÀK˾CäâWÂõŸ3ðö©Âã¼Üp\ÿÊT ÙþÁq}“´å5ªnÒ £ ,ð†v¼„*]§lf*@[¢ÂLVâ4}ÊV LéõtªNÊ= ø¡õ]K±œ¦Ý ÎTؾ"±¶ßŽ*ïI3ŒA½ì«è îxÍêƒIŽÑÔ;ã-íÍËcÓæ`ðkîh]‘ûùÝK¯Yé˜6‡¤PÁꫪrë í“4ÍÁd …*¯añó€¶¥ÈÉÉÉùae¨ÚŠ|IEND®B`‚ö‰PNG  IHDR@@ªiqÞ pHYsMMgŒà¨IDATxœí˜Mh\UÇç%L°v"d ™I ’¤c“ÜØ ~´´ ~7®Ä…àÖE©*¥ÝTDÜ ŠàB°´]ˆ4 ꪶ‹Ìh33û1o Í$í MŒ™9.ÒÀäÍÍ8™yo2Òû[¾ÿ»çžÿ¹÷¾wï‹Åb±X,‹Åb±X,˃…xDG?ùPD;€2ˆnC^>¢8ªR=‘K'ŽWª†Œ­ŠÐѲüZˆ*¥\:ÞYùÌÙ®dÚCô$ð?ŸöFô¾· T-€èÐÞçç,öÄPr éù†: ^_}%—L\ô¾m,@lhìiÐAvy:hã"˜ÌëmÃn*~ÙÔbÓô ìVG'~oO@»Á¡ÚÏu)Ë¡ììÔt­F›’šôY„kIÖªÝ.hµyáš j™‡:þÙTâNhøu£"Ú¿Kí0¬ùK8¡ñl*qã¿Z×\•ì‡VCg^0È¥zãøˆ`@… «+¯ßžž.Ô¤î¼7?¿ydÇw%§k/0â‘[½ŒæAN‰›33÷ê ´¥)¼¸¸¸ZÜÿä÷á;Ë»žªNJÙ¤jjó*_ºéá·òùŸÿÙJ´F³•¾¡ÑSŠ5heÛH™Í+|’KÅßo¤ß†?b……¹ÉîžÞ%à G h ˜Í sSñã¦õÐÔW¼°0÷Kw$z x™Æ}^Fóe}'›J|ÑLd_2Œ î›@ô[ ä‘üØ0™68+¨¾é¦§›Œíßtí{Iá Âé‰"÷õEGåµ[é© Å܈¯ë5:°ÿqÊ?ÔHL#?¯Z>’Kÿv©Ñ½øþÁŠ=6:BY&Ýi+E0™¿‰£‡Ü™ÄÕfs¬$/vl`äQœÎóÀ㩞"˜Ìÿ–ºé߯û•ceg¾ãÎ^ý³ŒŒW<’PûÏÓAµù+*‚0¿žP`ôìÙÓÝ¥ž3ÈÞóƒ©0ÿ–¥WóÉä]ÿ³[#Ð=|>™¼ÛU*V8g×G{³Yq¶«T8¤yhÁ!&“É,çú#À×›ôoÚÝ}åöGÞÈd2ËAçôÉ¥'68ö)Â{5ßR>sÓñ£´èÆ©•Z\˜;ŽDW€oˆ~à¦ÑÂ[é–ßèóýîéͱv~XGwÝTâóVç³-WZÅ…¹Ë;{zw<Uäí\*þÍväb±X,‹Åb±X,‹ÅbyÐø‡h(×®²ƒÑIEND®B`‚Õ‰PNG  IHDR@@ªiqÞ pHYsMMgŒà‡IDATxœí×± ÂP Ð3CD,ÀB¬€(2Q ö`&,ñiQ"Ê”¼Wž\œÝ9`?jœnÏsZ†$Ý }S•1©ËãÚÝ?óÃlrƒË'Ik9¶Ö†i>?ÀÎÌP铼~_eYU«ª_»ð?ü³É .Ÿø¾ò °oŒ"œ›Ñ-IEND®B`‚—‰PNG  IHDR szzô pHYsMMgŒàIIDATX…í×A Á ÁJ} ³ZHê§&@Äñc÷ßË|+‘QdWd—³1=ÂYÞ½4Üp3?#í'Н»EcÅæIEND®B`‚؉PNG  IHDR@@ªiqÞ pHYsMMgŒàŠIDATxœíš¿nÓP‡¿“¶* ,ˆàhÝF¤‚©oÀJha…••7 Fþ”T0 ž k‚LBĘ'@BÊ‚ÒË’ÁÜÚMmß{}¯ã#y°}üÓý>%¶udhª©¦V¹¤êØ®a<¾ƒÈsà÷\µßjßø‘<¿^ѺœÔ`4é¢ÔÐX“ÓWÀídO«Š…¹¨ÁhÒ•ü¢®è}µ?W-õLï­€,x”êíEѽ¿V7Áóà;íèSÚ5µPj" (<Ô@@x\@YxX€ xT€)xP€IxL€ixH€ xD€-x@€Mxð\€mxðX€ xðT€+xðP€iøa<¾‹ÈËÅî£Îîöçäy¯"¦á£I‘càÚb{­÷x#À|JÞ™òB€#øS'zoåÜÁ«‡7w·?êý•ÞÝÂGGi×T&Àx¨H€/ðPŸàÁ±ßàÁ¡áÁÑcÐH73 ^ÞêyFf‚aל ú?ŒÇ÷yw&oÉ£óBê à ^‰ìílçÍ+KÔÎà=ühÒC©÷z^Þ¿Qª€U‡‹wç>†à3ò Ãgå½þ' Žã¹¬ÿ.'†ÏÈ+ o:´7ÁÙìê°™8Tê³””¼R‹5š€ýýëDÉSà/0S"Ý2ßäèy½2‹5—YÓét3Žã _óNN~^2™×TS+\ÿ-ûÌD$5‘úIEND®B`‚ð‰PNG  IHDR szzô pHYsMMgŒà¢IDATX…å—OheÀof’5"µA7Ɇ$›ñÐ1 ‘BÕX‹E¤MTDj¡*z„=(ð¤EÑ«ÁF­,éIJ•غSºÝT»ÉT#]”Ôv“ïyØIH² Éš]/}§a¾7ï÷{ÃÌ÷nõJ’[ººÛ€ýÙ-Ð$KaR 'pÚR{(—ýébUâ÷oKA^gݳ–Ê‘Éìùï6$°%Ù{çm2û>ðl”( 'l£Ù¢]ØÎ cqLØ!b= ôƒ¶G%¾úc"ý{ʼn.Ï ±[#ðÑ í®)®Ñ”Õ’ìyFEß’Ào–1û&'ÆÎ­[àî{SIÛè&à‹‚ÜxþZ&ó÷à%áºnl††÷D9„r],kçTæÜèš®»)¦ ßè;Á¥ô+€©¾¸~³Û=€Ê›@n®XÜþ篿‹¬åOÄôöc€§ªÃ„hI‚~$êçS–5½D 9Ùýè`º@ìÀá [bæ0¸®x2õتˆ¼ èëùìU€ïû³}­„`EoaA ©}k3ðµÍ1óAµàódÓCã@ªµó¾ž2:ÇÙ *_ù¾?[m@U9 ¶ýD™Ð ªßÔ^2@¾Lv­  mb1Q+ ­ld’XA@aѨQÜáÌ”j ­DbÙ<ð?Ær™PÇÄkE¼ÖÍמ"šcsÀ6tÖJ@-+]åæï-2 ¢{j%-׈ȩ2Çq†#»Çéí­«e_I *¸ratJàHS¤O,M²È½y۔ŀêO”,´p%ZŸšù^ë­oÌZô#¨ãûJÏv¯Ëm/ºð¨lðhlñ(\ðà\ñ |ð`lñìlš*ô#Èû¦,rЈñ9p ÍüQ<ð `YÁ­Yó>\®·e±;*@(Ü+€#¾Î— Éë¸s€Ð¸S€8X¾[<M2,vÀO”Ì]ðΞøªBf‡p§N otÂA¸¹¿:acqóÑVüÀÅlï„øXí+û^L©‡Æ¿ëÚvÈJ”û£L€ Ë&CSˆ}¿Um\éíñ0”8ókË^_ûTý[óƒÍp…5Uü€Ã™.Eýø1R¦*À.ÍAÜŽ–ãû~v À@Üš½E¿‰„Ì¥DkK—}Œ$&žEÍÞ‚_€DJWìÅÑ¥xÎ7$)˜”ÔŠšñiߣ{A¯,Ç5ß D23ÓÛcO¿ð @rKàÛv¯üÀèûzÊ?€¤_‚Jp0KÉ®í„.°³ëÓß²•ež½oŽùÏõ°›µ°ŒvÌ·Ìì\ûVø!oñ8²tmƒ.´Ỽ ÆIÀH ãZ·±¿U¤IцæÈ»™Õ‚} hÄÜxй>*jž@¦ ñæI§ÞÇòÎÀð¢±çí¯Ów0“_€Do&¦â«ÜݸæÿSú~÷¤Î¯_<1(ò'З°j0XøxeögYæÿð+0SÀã§IEND®B`‚3‰PNG  IHDR@@ªiqÞ pHYsMMgŒàåIDATxœí’± Â0Ÿ"LÀB¬Q0;°U˜ŠDT©°CâNŠäâ+¾r""ÿLé}Ááú<æ•ËrÛù~ÚßZÎײëùó$Y–’ ŸÅZÎWÒ?À¼ÌÚ¹Õ|[øi @ Ѐ 1-@cZ€Æ´hÐ4 h @ Ѐ 1-@cZ€Æ´hÐ4 h @ Ѐ 1-@cZ€Æ´hÐ4 h @ ÐtPJ¦µs«ùZ6xeLò˜¿2¶Ÿùž7ñ8>‡±IEND®B`‚u‰PNG  IHDR@@ªiqÞ pHYsMMgŒà'IDATxœíÁ  ÷Om7 €w@@òÉQIEND®B`‚ Ö‰PNG  IHDR@@ªiqÞ pHYsMMgŒà ˆIDATxœå›[o[Ç€¿%EŠ”(‰’­»dÉNšÂAà&EÑ H ô©}é{ý£}*P´A6NR i’Æ–lÉ7]HI¤.äôaöîÙ3‡¤äôÉ$w÷ÌÎÌîÎmç²âxã@œòžþx“„ãùMB–W—íàðœ<Ün%`2‹/m¯ ¡fàbȘs g´w¾ÑþÜ©*Ï£5d¸í =^ø‰§€9ß.ÀYÐ>ëÛú¾£ 4Šï?÷4Tý÷%p—§P9òmS¨ ŸÿÄÿŸòmh§¾})è{îq1o…R’- @V1k Fûȼÿ=rä®ÿ̀ܲ#¤îÇ,¨àå®Ç1áûç@6óGUší ¿)_óˆËÈŸõÁ\ß6HÅhÛ?·â'[ŽðÝsr<ã%ÿÛyF¶ uKÛå-ƒÙ2ÈOŒö ?Ú¼ð‘0’š¬0Ƕí–íkLN`¢âöYN{IçËõ—³Ï¥»¹ò«q'ßlûPcÊÈmcü*Ⱥ}M|ó^y^dV·¼Ù7òdÑèkúg#Ë&›úÉ‚q&äà> þ'Ê£ì!£«py *_ád xZЗÚá< ðºO ö*hë(]”É)¼˜7˜0ÆÄ4Pæ Z½íÇl/T¡ñÄ,ùÿ‰éJ4xØFÐö %† ‚2¼çŸ]~|Ì "œÙŒ ÔÌ$ÿ7€6¸]?Q5]›À×à®ô|¹r ΛB}oG˜ŸøÙE`5©{ö³Ò†ú ÔŸX~€Ï€ç€·<ìøç{ªÛÜUÑÄ[ó&[YÍ)^|)FËL.¡¦rF?7YõÊx=kUÒþ™~‘ ßx+¶ŠQ@ɘµ¯² ®]þÜ „¬ÌaSçÜSß7檛àŸuO€Å}ØÌ÷‹Ö¿÷²sºžò"ëGÀÀp1zæ¿ó[é1Y! ¹ÙWÆÝåø<Ž îRq7ûÑn=¼ð=záê)NÇX-%8çW/ÙF•L)0wÀ}àÒ›«Wèî MžŸÐÜ%èúãV;Äö÷§.Hxfû:VîGèÊ­c ¨/’Ñgîä§ñcXy .*PFÓ)ƒ­½ üøKt桞?>N¨ùÎyÔRøÕ9‡úád]Ô<þø$ Ù+Ïz ¸ r-fn%bÄÑá]`&ªtûÙá²ü pŒ C€îm¨M@ý%¸NCGÙ¿Rà¢Õ“¼ÖN}“¿kàö‚Î6È ºcßöžkB[ŸèØÇ:  œû î¡«ò…M³,¡Û¾‡­¦U¨µU‰2o€ëè3Õ¶2«0ª£‡ú+ˆ¾ñcîùç:d¹"+Àð õŒ|غພàH1ÍC»†®ÂùX<Ûp<…ö$/`‡´]àLçŒÁ¡‹úƒç)Ç‹%€ê=-æ·|Í)p­l["„Ë÷a®³¯ÌGo³¯`öRq[G͵`® a5ß-ÔÂeÀÀ4ª= ”š,£nªÇM¨|Ì3~ì=ˆ òÎaÂsÛYr—@Ü1º¸° ¨iÙ) d¶ó2¥d"¬`ëÚnû”J^¿Äî”6±x:Ô#4Ú ”ƒL¿}Õ²á*Ì‚t˜ÙúZÀ»ÀÒŒN’Ô¨OaåçÐ@çý- ;sä?#Á¦}dѸcï'Y 1a5rIH™GƒeOd„œtf¡~ÄAÆ#OÐ×Áä+@:gPßtoA-É/>óßÎÏùš+ átf ž-œ·‰*¾¾î†TW%yËLÂÆZ™EàKª'à%¸C4Ós–ü¤õ2¸çyT‚g~8FWìcwµýø2ºâžŽÿh_.z;ñ¦ÏgCšd˜šlOà¤)²á±©J¨oƒÛ ì÷-lå7 Gih¥ÍônO‡‡Ä©/ g‡R'¾½_Ÿ¹r'¨·ºôHe H .åã( ¾óGF{e%´òœ³”›:QxŸaß?„´ 7N|j|Íêµ°Xöߺ„V$4 1œ“ó.ŒÔ{Š„iÑú C`GàZáì(Áô;§0”6<<×…ê¨|Û5r î Uæˆ0AqTw›^ƒndÞb! ½±¹©Qô\®=¶ËpeióÀ?È´—ìöšP{fÐ! Ñ$ À£ý2b…»}4sÐÃÎÖŽŠz˜¸ÎöOìuôGô¿&ÔÅGp¬y#”Ÿ¡ö7ÆÕ³ŽÌòš¤ÒmÿÈFX‡B(@Û³ÌÍë€.Q¼@³Ã¿ÊÏŸùQÖa([9ÆHrç9my"è“Þ')ƒóIbt|Û2zÑ Ž<½ìg·± ¹CYÏæÓs÷Opí`Ž‚<£š8Ùæ\Ó`-a< ‡Å¡»ú8Û ¶2Ùéø„W×ì¡ÁÏÈVàoe_ûn0¶+æ$“ÀË„ä:>£8‹”à…m ]_‰²êmÜÐòg0ÉÛ¿ó BhÝF$VN²Ð÷-£Öh,%ØFí¥?ûmnd_8ƒÆ2œœá%^œ(Ιew1,S|¼’…œYµp†*—rA†8î»÷O·ýWÐ8Ósúc¬*N¾Âô¥­®-t™ƒ³.ÊSNxEù€Äf¾mÔ«8’êOÓmžfa.NÖ&°SÓOˆpAš4pÞtɺ×í+rJØîí°õKU\²8ž¯/Î]I#<ó¡(«vúÔ4_TÀ}ëêDV 6ƒñ•Ó'h9À2Z'ÔÆ]¿E¯Ç— —¤ NÑøü=/„a‰!­5 ºÑVMÏ%ð{àS—£‰LÊk'õf;…ÃÝgÙB ¶†÷7¢ö¤oÍVCܨF0}~óREªh íÈ»¶N÷óÖJ>dŒ¡C #ëNफõ€)¢ðŠºîê%¾³ã‚Ì )ûïÑÌÏyŸ¦àà žän"–º@•LIìÌ ¨LzÉ…´ûЪªÎ¸V]î˜ eÅݪ¯úc{º2_*¨nËÕ(X±@ Üžnyy( §¨Fîÿ ˜$?wà”„¿¡ |H¼¿U”ѧ0'Q¿9ÆûÐh‘¹¾“ŠÒãv@îÌfà%i.Àí¢u6ÉÖTñTÈf"NT€ì£µ?÷Æ— hÝF›%à¹î°œŸ&]Õã&ºêeTyvÐÚæ%eÐËØŒ3íwš WÝZÿFfŸ£Bxç'P=R" Í[_çh$›I€ƒ ¿»ªÉ !{f¼©3ËeÅ/Ko#|<À½ Æ—ã{FëÄÌt=!Ñ‚EÐ+ïeß¾´ b÷‹ƒê¤g°SàÅ­À}LÁÑ÷Ùë9qž¶øÌ“B“;ç'S._ÏZðfè}ìDI2æÿP.ºòó¼ù•¦ZŠ\¹ü̲^ùÇØ/L¬’{aBú‚…ñC:f{̉CœψCßù¥ÑWÎú+©ªŒÿÂDŠèONÈF^òúÊJî¥^ë•™´­äŸ·@Þ1œŸ²¥å=óÈã+ž<‘ZY¥{€àw‚q}-~kÞÓO+tž*Œ~ijÊ /+ ^¾*z9j3f‘-ÏC)¿›bD†“#›¨¶õ5º´üïjæzrÁ൹@ 7 åÄ)JJd{Áÿäö&ym.(—Méëúq5a§þ÷$ªÄ}T¡ÎxöN“íÀ¸¢ŽA&0JÍ<ž:y-¿8YB¯Ê’#Teà_à™8$kó¬'…Aâ3£´6}¬ˆÇ7÷ÕY«á € Ïoó ¨þ´ ý~Ï"[ÂIEND®B`‚¸‰PNG  IHDR szzô pHYsMMgŒàjIDATX…åÖ1NÃ0Æñÿ‹C¸8Aa@ ¬-@‚ $ÎQîÑ $¤víŠ å 01—V‰â4vj‡ö“Ÿlç)ð߇4‰ ÆÚJ 7ÀËyGN’&qcÀv1Ÿ6‰ ´ž Ž‹µèW`Á»—‡òÚH€*>ýë´ ˆN7IEND®B`‚è‰PNG  IHDR szzô pHYsMMgŒàšIDATX…Å—ÙvâF†¿f`PœÄžñ$9YÞÿr1žIÏl#$¨\Ôߨ‘qr—Ô9‰V×ö×Ö X øÏÉX¤| ¬!X÷‘K –%ï“DJ\³dm›¬íô~zò—À:ôÀ0ªDYØ !ŒqÏ¥¬¬•Q¹ Kþ#0–òàÞ²ûDù‘q÷èÌLÊô˾¦€Ùx|”ŽAûNÿ+ص¾ý ÌÕ J\p¨Ï`s Åó¥Òó€'ôxÂãÿ °‚°%Üc)Ïåݸ’Å9”O‚qf$åÀ³”•B8îððdž;(ÜR›À˵ƒ \Hh¤9†¹øú×"mÄ»“¬Û÷JÌcîżsA0ýŒÇ²–õ#ÁVCu Óµ#uŽl!™50•SºÒIöÁ— ´/Imеá–]ÃtåH½I±Î¿ ‰X¶-<6xèú!hJXäxÒü%Á…úAîIg³î?ž7_as)Þ<‘ ,žIB ȼ‹‘ ÆR´b\á‰ö Hs=ƒ º ¦®œfÚ×â=åï1ChLûc @P× Þ¹‚2Û ‚ú»56î{½ýÊŽ|´`{†Ø3^†º$?©‚aRßI“©Ó=Úǽ{cé0»KŠ”NYÉ UçlÇ0‚ñ!ñ†ä½M„Œ€Æ=·5êLxF[ÒRJx5=És†¹O‡žÀ>kÏ–¼¿Ú…í QW Ömõ°gÀ0}¥ß2™'}ƒOFnߘ”¡U)õ¬-úußâ §ybÛ¾¦;D ¯ß­;î‹9`ÊpSÎ4XÀK³”âo¹j6–BÜßJ¸I¶!; 2vù¢;C•ÛÖ$4þßÀ3#!Ÿÿaî8žvÖ%,ï:yÀK8îÙÁIÆuâõ5 Q/?Éós™~´@~€åS·¼*¤7l¼‰_ ãê²c¢ö†ù->ÝúgÁ”"Ôàí6ø³L†W6“1'!Øûæâì†nàøIæw<ž{ t¤êA— õPÕ!{Æ»ŸŽw,¡¸•Ìmjn@xÐÅ|$o6`-ð áå<6w ƒëg|–l$såùÓE1 Aã“-te}r1ùíÌÅäØÛ÷‚m +3þo2y0„qz÷Ëu0n}ÔŽj¼gìÅÛÐ ¦5ÉÕìËiôâ˜pÃä}¢÷ l5;&•”ÝåT¹°óžÃkøŸ¯çˆ|Ó;í2OßIEND®B`‚u‰PNG  IHDR@@ªiqÞ pHYsMMgŒà'IDATxœíÁ  ÷Om7 €w@@òÉQIEND®B`‚æ‰PNG  IHDR@@ªiqÞ pHYsMMgŒà˜IDATxœí™KhQ†ÿ“„ÖG0©ÍL´3Ѷ: ˆºR4*ЏP[pãÆ…¸—âJq%ˆ+7*ЏSW-ˆZPãÆ¨(•>ª±˜Eâ«´s]$ÕdæÖ¤“™´êù ɹÿüÜ{ç\0 Ã0 Ã0 Ã0 Ã0ÿ4S«šÞK ³ð8?2üüèLèðÎĤª–8Ð%~!å]áóÇ'ÍÖÒt"±ÄNúúv‚áo…|.ÝL=Í4€"Zò” ûC›­`ÇüB>w«Y¢šc@*åS|m¬Ý˜ÖùCáÎbrE?²YÃmi®‚ÑhtÎoÛU»­Qa”%x$]¯·Nöf³ÙïnêsÕ€`<hónb“$dž~rNÝo\1@­XòÞÐe µöµG¢kˆ ¶¾~ñÆ)•“9ŠºLïyÓ°—<Êm„é·.J«ÑD·+qÔ%¶r- º`±)Toò“€0›Ð /î«1}MC"M8fÀ¢Xr ‘ç€)4ÝäËÌ„ˆÃZr³=•V1@é}‰~”ÊÚJl&?‰Ô¿b@Õô^ûãþ¦áB(×÷t€¯:" €ÌâmðkŒÊƒÑ ОÀ‚Ž·…|îi#£7d€¢%8Ë©íTòUc ÊyD»üÁŽ/Å|îÝQí@Š–8IÀqkÈä’Ç@ mþPxnñsî¶Q§o@*åS=sD8d º•ü$dùR~XßìXTÌoì^NkþiB¥º>p}Ö¨ÛÉWÍEIpº6Ÿ¾îÍd2?ê©nÚ»ºÚZÆ[nÀô (Ö–âÅeä&àö¸o¬·ÞûC]¨ËW·cbl€¬™ª®o²müÞ–#¯Öê\Ó€ˆ¦w ¢›0—¡ ¾ãEvxå!Úö.óìm­ŽS¢FÝ”žåÉòÒ¹Ç"Yš4ßIª˜r¨1} µ›B³-ùJ$+AŒ´}døùã©:XP´•›@4ø—%Ho’Ô`P‰ë)YË¢h‰£º UÒžP2m6d«º•@ûüÁ°QÌçBՒ〘‘ÿ ܇&F†ŸUÝY¬ïQËYòï ËÍbN`vïs»å܆a†a†a†a˜ÿšŸæ6÷ì‡Ñq(IEND®B`‚ö‰PNG  IHDR@@ªiqÞ pHYsMMgŒà¨IDATxœíÐ1€@ÄPOB‚u×âàS$ÝT“—9Ï»Öwß׌ð8ÅéŸ(€Ð@ h  4КhM´€¦Z@S- )€Ð@ h  4КhM´€¦Z@S- )€Ð@ h  4КhM´€¦Z@S- )€Ð@ h  4КhM´€¦Z@³l¡€‘Þk:IEND®B`‚‹‰PNG  IHDR@@ªiqÞ pHYsMMgŒà=IDATxœíÐQ ÁÓþÅülä¥2“`Ù*þ7Ö>7‘4Ói¤à½þ‰m¯TyªIEND®B`‚L‰PNG  IHDR@@ªiqÞ pHYsMMgŒàþIDATxœíÖ!NAÅñ÷u³ T Às&¤AŒ¨Xà@8@/G’€[¹bMoPÕ¤©C@aé –Ìn§ ûÿÉ÷­xûÌ® ‚ýu !X]×E®2}¨ªÊ›™ÿíþãmÛïTÌev%iÜ[»<¶&=~˜¿žM§«ø˜ Ð4Í‘• I§9Úe´ ]1qn²þŽâ§FåáþßËKÒ‰•»y&é"OŸ}—q’ 04É&=ï£Hö'ɾ{»•´ÌQ'³M芛8Lpνèóý\!ÜKzÍR­_[“:ógñ@âGŠ/³<Àm\zXIEND®B`‚â‰PNG  IHDR szzô pHYsMMgŒà”IDATX…í—ÁJQ†ÿsÁ-ó%4Ô,z£ rÖ#dÐ"ƒöá3øA27E{ •‚p#(÷´hF¦GÒ šu9s8ÿwï¹ ÷ÿ]ô=Ðlvö• s€„O>C¦P\Éå2sÌV÷Œˆ¯ÜÀ|’"F9ŸM_ÿ0[ÏDxð΄’kõBa÷Í×Fãe[D§EbTl ŇöIh3B1¡dì¥k~Û²6R“í.ùÞjñ1Gžj¬Õý4ÿ¢ÉØ®­Û!'@ÂAˆt]µ–;nQl@[”ðÔêœ0Ñ%€Ø/kˆù"ŸÍÜy%yž€iö’Lt»„9ʨjš½äÒë'€a¤úÄ| `´Dí1— #Õ÷JZx¬zöqýí„!Àº†Àç2(3)eÜZÜLÑi1(D¢vmi‡f?"¡¸¢£*Û]Âd\w¼áV’”2ŽH´æJ(®ØßÜ“ ‚»ó[ŽÑL‡ãõº¢¤Ûhêš=ˆ |IEND®B`‚M‰PNG  IHDR@@ªiqÞ pHYsMMgŒàÿIDATxœíÖ±.Q…ñïÎlB6Z…è$  ã¼h·ñìl³V¯”Ðy¥DBC"Bc®Vî ›»‰ù~åùOqæ43 I’$I’$I’$uBøócØ>¦ÌÔ¥WP3 õo÷X?yX®Co`Xh«\&Sà¼(‹ÁÍ~ÿ1=6X?/†ùêXÍP.§Iظ;XyúéSaîsÈÿ{y€~/2JÃÆv²Ô™…Àn5è˜æ‘ËôÈ#r‘Fâ[yÜçè“Ù¤ ¦ac€ÛÁÒKùþ±áxÍR­]Sà¬(‹Íô þI’$I’$I’$I]ñ|ô4Cå꿤IEND®B`‚‹‰PNG  IHDR@@ªiqÞ pHYsMMgŒà=IDATxœíšÁOAÆ¿·[„h)"’ÒJ"Ðõ@-Ä£g=x3bbBbÔðþ¼Ðè£1zƒzÓ„˜˜Z‰‡V6@K‘jÀ²û¼»Û-ÝÝéXwÇÙé{ß|}™ ÀÏÛÝÉd¸eKcÂ%)­îe¹b@†3å6}jua¡d×Ù•ñþÔVh@¯›8É“Á£K‹™—Õ:¨N#ÇR£¬ÐNc4€ˆF"Ñ/›ëÅVÍ€˜–: ¦YŠ+yC'ƒÏ[Í„º èN&ámõ€#žHkùV}°rM¨ûlÙRÇÐ|ƒ€Þ]íQ·»«}Sb¥ÝÉ;œò@‹,LÚC‚Ø~ç—si×µ…b‰!¶ylÒÞ,«¸0d M`€l² -@6¾7 f!tøè`¬%ºà4Q»*≡¬S1Àäôâý|°–Í~wk¯ØVmqmø"3Oè-ÄcÑPh¤Ÿ«çg5*AS¥Zõèц&˜ùd ý ó›Ø@ê¦È4–Ä©Ëĸ-2ñQA4Õ†OŠJ`2 §ïDƒŠJèU1øÑ!M‹ˆn2€T}€dŽ!ôïãý7D„¶z.ˆHä ÑeeÀ?¹ÅÍ‚tÕ½!¢B?&BˆŽðlµçЇ8VÔm@>·à¸Ð±£7‘„îü˜Â1¾/…d M`€l² -@6²È&0@¶ÙÈ '÷¤À@¢ÖŽïØ®lðÛ ÈT6øÊbÌT¶ùÉ€|¹MŸªlô‹:Ï¿m@¾€€óxõØúá`©IEND®B`‚´‰PNG  IHDR@@ªiqÞ pHYsMMgŒàfIDATxœí›MK[A†ß“D •ŠéÆ…E—RAã­i½¥+¡ÕEÅ¥]TðHw­.*ZKWýø‚³]h Yö¦Mz‚%KEvÑHÀ‚h<]$ÂýJM27Ó̳›3'‡wÞÌ;$3€B¡hf¨œ¤x<jm 3ð@@'€k5UV9'$ X>ÎeW†‡‡Ï.ûÐ¥ßÍG|ø¶*ëÈŸó3ý®öù_IRÌL‰Tz–´Ñ€ƒ€^ І‘LÏ0sÉ/º¤ß~lÍüº6Úê¿I¤¶^”ì÷ ¦=m8ÂY/¢µÝLfobb"ï§PQb±X°»§§ güÀ<€°µŸÏyÄëqpÇC×ÛnnY§=ƒ¾à40©ë}‡µï7†±Ý–ó%?´„wþä²çÂèzZÛÂã¶gžð;ÂÓF<èzß!N“(ÌÚ z c³ã2 øª³æD"¿|WYct½ï„9kÌ56x/‚Q[+DkþJ«#AZµxЙâe@§µ±›Éìù*ªŽ¸µÓ-gŽ—¶ÞU[í+ÁC»k÷ZrÐ,(d 2@¶Ù(d 2@¶Ù(d 2@¶Ù„ü(b$7Gˆð@?Êü¯A#ÍÀs=:àüݲb„g@"iNa@µ<DXO$Í)ÑbB˜¦Ù¢O¢"ª†è£iší"%„ 8ãà}7DjÒVÔP5M¿ ¢üW9Ÿ´TC®¨¡j„ Ð4íÌÓ"5„`žÖ4íH¤„ð#0Õ™1 Æ&­W ͌ѡ¨¶(ZÌ—}@ñ},üN–Ze 2@¶Ù(d 2@¶Ù(d 2@¶Ù( À6e`z?ÔéïÖûÍ$˜4?œ iX§4$@^u™‰š¯mf`ÂQm™Öê}ŒcñµZÍh:§:íÚ{»âc€«Õ%ɘõïþdÄ©×^£÷I²kˆgü[ß,cT¯Ý®÷IôÔ8‘DFkl³‰`08@ýÉnóx<6µO¢¶@¦oGÍ& ``D]–‚ Ýf®P]]]õ*0á÷û'Ô>†X³¢*™–æG^þ¥¢biDï“h<«q 2Œœs… ÕêLA½1L§4EÐ5¦ªšAÖi'´ë}ŒKÝf#oÖûÌH¯]¢MïcH@l¡|‹õþûu+Ü UæËË;‚€ÛÔ)è°ÁIoèóû£ÌxKÓPÊÛô~³GíÚ ºåáHUÉ{z¿„ëLÚ¯(5›+/ÿ f^ÇáÆ}Œ„ (°ZZ Ýj¾Éá^¿Ü\‰yE€ð%µA2ö&vLÀÕuÿC*S!ɸ~;|Ö⬭ÿ*u*S8¼ªôP"ß«Âôkðg͵Ÿ0Gbþ¸:×ÿ¹ÚFÀou E.túÞ`hC ën3Dæ“¡qËwAªÏ^ÂÀ„uâédþ©v†Äh ø†ÓUoXW›-8Wo(càAµ™Ñú´a VÇ­O¸q²L@ RÜ5Ý¡ÊiéðõJ¢/W™¯báï0Cg§Ãán¸àïkŒÄ»Ò9¶“Ö ‘K‘óöÒò³m›Š¬·—T8W9Ëõõõ)«6ªtyàI¨ ¡@Û£I[©HûŒPt ×WTR±ˆ€Tæ cŠh´/_ùjôÃÐhº±ÌÀãñجvç3 <MO俇V–} Á`Zg3ìÂ;,NWû>ÛtAÉhŽt¶¶f/;*\ åòoêíû«œ,T,›ƒÁ÷Ó^ÈM»\ÁÏÑÁ_´/ë·ƒ°IU±T¾STâ([\RñßKƒ‘K™ÅMªªM -/ù‘ÿ 5ºêׯØvk¸ëøÅLbfTÖíý6˜žf @W%¢ÇÇ0º'“‹)ij²VžØÉÌ»¡:¯4cO¨³î¾«»Â‘Ó(¾/(MP=ð&q`£‡2M†Çã± Y™h+€Û6Là ˆxWO íÙ,o!÷×X¥Ë»’!žx{Š‹Ä8¢Bâ´`%À‚¬1ŠFm—•Eb±=.cÅ¢†­†Ä&n°$ť߰+ÔÑz,ý&þa¢þsü @½Y1Á@? uÔý1›.¯Ç쉌pº¼·t?€O𻓠{ÄGž5óŒbÞfr÷ºµ‚y'ƒ›¬Î2L˜ ‰°7t¦õ_ÈâÿÓ1#SÙJ—w¥„ØðZute—¶Wžq+€‹†Á8¢vf´CÄ„þSÈÃ?Åæ™gžy&ù—4ÚÏm¬ikIEND®B`‚õ‰PNG  IHDR szzô pHYsMMgŒà§IDATX…í—ÏJ#A‡¿êÄxÒƒâÎE‚ÅPPö²ìSødÍi1>‚ö`AãE|†¼€° /°¨ôÒ‘]Xr2b—‡Ì„ÁÄ‘M&*8¿SS]ôï›®f¨‚.y˜<°sîA ˆÎ#1ùÜ¢rbRZ<_ð~= ðyßþ6ÛÅ$'ÊÚŲ÷³ »WWÑcàŸB>cê•ß cãp>¸ª»LN  ×à&ÒA’ ùË%ï(ã@þ‡eËVT9tR¾˜fV£ædL½§yXæî¾â{Í4c¡ý‘mOtö}ô¿üÔàM”$o~)abß®lÿyvMaýrÉÛJмñ²õ¶;0(—­×1Àk(àjѳ «@­ƒ³k ù«EÏF%½øüFÖ±½ï$ ÀkÜB£ì•ÙÔÎͰ¿¬¶¨œÔ]&×+×ß—ó½NƒXóGdRZtޝ¥lÙŠ¹»¯„z¸®4µs3ìúûrªlΤ´ìµLŠôîmƒÝMòTáB¡®Ê¥xB»lfYÅø.àG,¡q„H¡îAs$, ›Y) –ÔÀºó@¶„==‡ä«­ŸªÒTv¶Ê’ßì_è>¼,”ûé e,ªª•›h}’^jýfñßmdü$Ö×(38à3ª‰%õžiÿÀ è Ë;…“>£3¦½·D"Lá»% |©»QÆ|F¦½/àVJ+KKxìÍ(W£ai3íh££ZR®ŒMya°;L¿ °PÏ àDüxzžó"â{Hú×wNèE„›y¢ð&û“›oŠ)àÎ+=•õ¸Ÿë/ðYÒ´DZå‹Í,Ö Ni³(ãÀ¶yÞóh4È[Ûy¬~ %´^”Gä\¼À p:ˆ‹‹ÜžÐ]ž0ùg…¾hXžØÊQˆ•¦4TRÊ`®.ʵhX†mdØãô«zÕ³Ä:ru»‘0WLûÃxu¯¹¼ößàžÏí¤×Ö^ÿ7ŒîSÚ1Ë,P±ª)¼…hë:(V~ïÃè ý ¤×囬p|«\<. ¯QÒ(Ý3À$p,zX¦Mz:‡Ãáp8‡Ãápã7/œ¾l1`IEND®B`‚%‰PNG  IHDR szzô pHYsMMgŒà×IDATX…å–±kA‡¿·{!¢˜Â°ÉUw9ÁÅœ)­,#Ò¢•‚?@,Ä^ÁRÐJ›`aì¬,ã&$w9,..Z<Éγ؅³sggˆ ¯{°ïý¾ù½™ÿ=ÄwÃz=ž:œû‚hº—œþÓ÷5ŸâÓíö¤êkS ¥j_âq ¾ë+…‹6užVï£ÚS„EÛJ5w z­ÈÍ‘DÍù{ ·òL e‡ï j´×€;…¦±wˆæÎ_GôAž‰õÊfæÚ˨<)ÒÊâ•¢Öü%U}„ ê"n 0ÛŠ0¬Çsa±ÚñNõÆB˘ð p2·;nNõ³f31o3…í^Ä¡ä]ef0ÎÜmÿ=J9 øµ¨…Áàcãí+ 0Øý°j¸|‘²uÞýÍnd—@|9aÕd¿ÛÙ$`øáË ëiwëˆ\2ò'Ó³®Ò >õ’uDoŽ{heˆÊ¦½íg¨¬¹B8Í0í'ûÅ$*A8o¢toë.¢óÌÂÇQÒ´wî¶"ÏÇG ¼Ì¦&o lØVzû£u:Ÿæ„¬¼ÿ+Ÿ“dŒ²%…E¾ùìýïÆ/s‚ùü SüIEND®B`‚À‰PNG  IHDR@@ªiqÞ pHYsMMgŒàrIDATxœíб ƒ@À}аÜÐ刊é‚"¨¹ HúÌL¸º`oà Ê1çµK2¤äuCŸ3-I¾Ÿ¶Lû°©þãóIòN2ÃÚR ÏšßåMη$éï.\o±õ D-ÚQIEND®B`‚‰PNG  IHDR@@ªiqÞ pHYsMMgŒà¹IDATxœíÙQ ƒPÑ-ÁC` QXC*¨ŒÓäÍØÉäþí 丞÷¸žW:lòø?P- )€Ð@ h  4КhM´€¦Z@S- )€Ð@ h  4КhÍò>ò¸~Ï´€ÙµÀÌÌ}~Ù—_@´€¦Z@S- )€Ð@ h  4КhM´€¦Z@S- )€Ð@ h  4Ëø:Ý ÎuñIEND®B`‚ЉPNG  IHDR@@ªiqÞ pHYsMMgŒà× #Ž;{é®lxbç?Xë¼÷¾±0 ¦·¿±Òé8Kp J¸öuŠG¶GÚCµd1XÜ^^g1)@`±¸¹Ûîy!)c4DéûDjx¸G3óúé;O¦ä ÌTˆ¼ÿ¹85@€ØŒÇ’$ùÿŧ¸8wFS‚€­™žoèè{ê'òï.Í5µQ¶¹z á®Ì6ô¦ìÕB8ï·iÝj¨[ešª¯`.±0ÛÔjÅy*­€†}æ~ë¬V / 0È¿{ÄãV“{» / ÀŇžm6¸#Éo¨ æÕÄAn.Jq·á¥†·Ù~._=©Ÿu„ȧÀbKFq‡™Mm×þ¿~ÁQQ=†ÈfIEND®B`‚ĉPNG  IHDR@@ªiqÞ pHYsMMgŒàvIDATxœí˜MˆE†Ÿ·Çx ‚ x= ™!ÍaT†€‘€–™]qAðÿ@B0‚ij »ÎæàI¢ñ ¢Dâ53#xŠäbb<ézÉŒ°»Øýå0+ÌT×8?Û=£¦žc}]UïûuWu}@ @ ; ¹ ·ì´Þ2(  ›…°Ìˆ±E¼]+êLءް¿ ÓS8=qµ¤»zÛ"÷¡ÿöëþg|ÞR ΢ÿaºžÎ¦›=l4­’{œIöê2%"í« <]+é²û°7õ–IøÚà~'ôïM‚ˆ°~O‚M"ªEµ|]|K€jQ­æ›î4Òà~3Ãcø%†ùAæ»Ý†°~Å’øxÔéÙýf½[tøÌÿdÆ‘¥²ÜØÇÐ7¹TÖÍqXÐì Â( OaÎùÆŽ8<Ì<Œ€•9mÆÛ<ñWÀlþóÊ¥d›'Wæ´9Ê #¯ååyµù“c&>ó„ ¦é} »sù´_hß˱åyµGk¬Í¬VÑÖ6'€57&#bI’yu¯íÿ™/?¢íñ†›3S½É*p*‰òªvÏõ©9á\mŽ×%=ïžÞX½a¯¬zB YŸª˜¼V-éÜäÃî‘õ¦=/ãCÜå”e%é7Ÿ^¨–”ZŽã õ†7ø¸Û eqjômw0ž«•uacg·i­]±§"ñ9°ß™Á°‰“2/è`‰ÙãÃó˜éQŠ4Ég²ý}oÌZ£iÆÐëÖÄeÙÞ»¢ Ö‘ñÃ×󢢚h[G ƒ¥²½}ߺ~<Ì9^¡ÍUºk[©.Ùþ>7àGxm‚pÙVꎊ¾6@7Æ8ï’ߨ‡÷ßà9$í4þÂíÉ›`âa…È0RäkÝ*ßùt기.ÿ<çÚÉüøÈ•Y”7 –HvjŽPW&/qGœÎçÒ£ªó(ý ˆ&ÌvͯQ¹øu戋~,PØ€––cMÜÁ €ŠÙŸ Ñ7K¿U•Ã’ÄãñP)Ä_8â*Ý›žH?V‘a3TL‚ÚŠ¨{ »BxjO¤n+ðßé; Ù0oá߃e¼ß£®CÑY¿ROx<Zu¿Ô–Éc5EtMN¦”Ü÷«!ud#@±Lš%Þ³[È~-nÛN}c4†³›Ò´R¯=‘ú.Õ“ ‚ ‚ ‚ ¢ ›'ËÐwœIEND®B`‚ΉPNG  IHDR@@ªiqÞ pHYsMMgŒà€IDATxœíÙÁ € A° Μí¿›Â2ÖÄ™þˆc„Ö¾ÏÚ÷)7\åñ/ P P P P P P P P P P P P P Pû}€Y¯¿ÆÇ¨_À#+2—ÀÛé7øòÌ&°¨èðA©C°–Yà|½þÙ‘×€/e &Ðn4vù"° (ÔKäJÒj4ÆR´Ìp‰Ü Zͳo„:À"•P©Î,tƒÖJ¼iq;R ‰ÂOØnÖŸÚÚH‰b¥jØ>?°¹`’ˆ¥›hu%¾ƒü(Rޤ|Óª´€$|·sËð Òrì«@§ÓYZª^^å×Á&Z­Vû±\Õ ÿWâ8þ¾Ø«^6ÞÅ| ™ûß=?ôà§ö=D'ÁIEND®B`‚h‰PNG  IHDR szzô pHYsMMgŒàIDATX…íÁ‚ ÿ¯nH@ï G ˆIEND®B`‚ ‰PNG  IHDR szzô pHYsMMgŒàRIDATX…íѱKaÆñïïE*ˆH¡`W݃Is‰ ts’j¶®éâ ÐµÐM‡–‚«ÐMì’Á¡k¡¸Š˜Ksw¹âìâ_ ‚ ¤y:˜B—ô’›„¾Ÿíއ‡çÇ žçyÞÿβa··NÖ¬V‹㔞'É‚ÉZ†]/¿XZ73ʺ̅ÆSŒÒÀé,ŒÒ•¬|;ú8YÛÄK¤gYùÌý»Û&ÒW`ô=ŒÓ7£²a7Y3'ˆçÀñ§Õ]c<€$÷#îí {÷ðÍN-(~ø»<Œ{[ˆÏ€´f§§6 …Â}V÷XþhGÉ[ÃögðÅ©¿Á¯N”~Âx?÷±·³.Ï5 ¥ ¡C`8ÅìéЗ±Y/—&é›x@Çä¾óÃ_7¯ë•ÒѤ]¹„iºH_—À6hÔÊå4oWn’LRî#<Ïó¼Gá7ÄEvz}LKIEND®B`‚ª‰PNG  IHDR szzô pHYsMMgŒà\IDATX…íÒ=(…QÇñïÿ¹ "¹’"÷ŨØl&yÙ”‰R¸·l›I&“÷)ƒÁ¤”,ДŠLî%åí¾,tÕ-/ço1 xÜ»ÉùŒ§_¿~§sÀ²,ËúïÄ/Id—m7ŽÖŸþ¦´uá®é%ଠúsûÑﲎ_™¢Õ@›cä0ꥻüò-^®ý5à t‚Ôúå}Ÿ†€M Ê¨ìD¼ÌðwÙðR®×Q³¸‚ì=—Wtÿtû_ ¸žl,$o‚ƒÀP®ÊZ(‘™FõÓó…™8b¶€JQÖ˪³Ý—#5÷~ý¾ࣈ—PÕyÀAuµ 7v|Ëk¸!3 2õ^9“«›ö»yIBËé~1²T yàE‘Xj<¸RL_Ñ¢‹¹ã˜m ø~”We sw‹í*i@ÔK7• T¯D¥ÿ,îž”ÚU:Uùú-˲¬?ç †þuhR÷ôIEND®B`‚ ‰PNG  IHDR szzô pHYsMMgŒà¾IDATX…í”=‹Q…Ÿ3Fv! ÚXØÙL²Ñ¤p±UdÑF+üû õˆÍ–6ŠXi¡µ‚Éâ&Ë…… (X)º:÷XÌd f?’ø±Í<0ðÎðÎ9gÞ{çBIIIÉ!£aÑYëŽÿ'߯­Fí@4|â‘ú_3êµS„ï•Ó@»h@€ìï\ Þ±×ÛÂ+¿M–¦iuë§Ÿ$Ûæí¨°z9_ÑÕ$I6Ç&$Éfu®rÙðÀ‘$1+’BÈÍ«s•+£æcâ8Þþ°±~y„íˆBHv$ ä•ÁÆú8Ž·Çúö°­N/½+s»P Lº…y^rïl=¹#i×wü²ö»tYòýa.ò͹9Îue-7Ï$öoŸ€v·Sæ!pD`ïBX@&¸ÕlÔ¤=ñÚvÖúKÀS`ž]&!ˆœëm×ZÚ‹It§Ú\«Ý÷çíð8ö[ˆ¨ÐúZj5’דjN½»W{½šƒ^'ó_ÜB€ø,ùb³^ïO£7õñÛ¬×ûÙ9`` sdΧ5‡&0äM·{ꨣ@úCáÒâ§YµJJJJ•_{ê«ÚÉàLIEND®B`‚‰PNG  IHDR@@ªiqÞ pHYsMMgŒàÑIDATxœí›lUåÇ?ÏéåÖ ,¢nQ"ÅH\@Íh •´f‹Û¢8ÝÒùcÐR:—Í8L´°‰ÛØá1Vé,é!c™Žd‚Áaí-l.u°°Ù S³¡–ÒRoïùî;çܽ·=·+ŸÿÎó>ïs¾ïsÞóžsÞ÷=p‘‹\äÿ‹“4ÒeN‚E‚ë뀙S“ =@ฉÃfüÙqÙ¿t¾uå[[ÞÐÜ¡kp©‘±¸a„aº0^Ä¥¹¦œ3S˜!äH²æ·ÉxÌ 2ÌØ®Xwx~E™ÅCŒ-ª”X'( +fJŒnÁêÚR¶…Ñ#F€†6]‰ð âþ n.â÷À>ŽþfƒœÄá”"$4HqĘâŠfIÜdðY (mD±ß`eÍ<{s4úG•€–˜æ¹° ødŠâSÀoLì6ñRõ|ûW.±[¨¨/Ê­w‹ÓœãŒ`Ù²rûyîê‡qš:t¢(L!jSa!?¸ÿ{¤ñ½´v*Ú×OÄ“—Ëk•ñä37ר9' ^rJò”Ī@‘k¢1®ÂÞÎ5n6l}UÅN!<*˜(ÞÕWÄ’‡æXo.1sJ@½äÌè`pO è„wU—[{.ñFJC›®šPÀÎà€+øC4Á-_­°žlc9¹œxFŒÕ/Ú"FÙX5 ®ÂÞþXœE;¼vƒO:ìhmUA¶±²N@c»¾‚ñ„Ï(Z8Må’2ûG¶q¢jõ×”²Äà;ÀùÇ¡ŒÛ{Kx:Û8YÝ[_W™ã°¸ÄSñùê2–æãí,Wšbú&°Ák3¨®)·mÃÕ¶´¼®Kvái<Ю^êÆCãjÊØhÐäµ ;4w¸ºÃ&ÀuX‡¸Òc:ápWm¥Ö¼`fê™ÊJà€Ç5ÑP¿W‘Lu3&`k‡Ë<&áð¥åsíÄÈ忇o\kƒ î6ø§Ç<·¤˜3ÕK›IV ¾ïµì¨-µ¶QjÍuö®`­Ï(žØö†&¦«“6M1 zLñ„¨½Ì<ÓËOãçÓqV¤sO›3¾í;†-ËçÙ[¡ˆÌ#µ•v†ä õHºwƒ” hj×'€ÏyLñx‚§BÒ˜w&v³ãèyƒ¸²÷>“Ê7up¸øOÆÄËuön¸2óGU•%¿ðÚ—êT¾©0ô êåWáH;d~Í‚/¤º ’pvÄ\àsrøuè ó̱Rb€·×Né/¡4è—”w€ ê1ý±ºÌŽýÆ;gç|εäyʤæLûÕ6vÈÚÅì Oò`|*p|4ÉçBÁõk7m#UÄŒ@¥qÿìOGP» $蓪ûŽÅa 3N'i/º¤z úœ.9ͱ'jnaÀ`ÐcжvÊ;À''À“$‘ãâ›?_$?‡æóÏ#7¹Û\(4ï£Pàø¨jŽ}äõIîÂ?£jLͼü3XÄ”€éTÐ'ù=À8æ=v•ƒèú¤*]²®1CN@»Ã‘ Oª§€o±ÑÌ7)rA¡díAŸ¤ôñà]ŸÛЦ«BÖ–wê%q»Ïhì ú%%àìÚÚk^[$t0#F)þåžî:‚~é¦Ä|_Q‹CÔ6&~Í2^ZSiƒA¿” p Øœ_j6¸µñ. ]ež¨—_öÚL´¤òM™€³óþ{<¦B'‘´>n)‰q/C»ÑÎñNw¯¯=çI?+,~ì=<ôÜ!]ŽÄüÑÚ©¨ŒïymRuÈ€êr^–0Œ¸¬GfþèíçüŸ½'lNçŸa]Àdð¸Ï(–6TÒ¼ÚxaK‡¦™üKøf<½|¡%½Ÿ#ãÚ`m¹í¶{ýå²³¡MWŒJihíT4*~ØCô§Ø”©Þ°«ÃQÅ?»:=RÀ ›Ž*¸9ê¿Êé~67{Lqê†ÛT9l–ÞhïáðE`Àc^Pü?‘4&{‡£©]_óÚ Vf³m'ë4Å´ðí¸ülR¿±Ç IÖr‡%6࿘ÏÖ–Û·²‰‘õ¡ÚrÛ.X絇ÇËvÜ‹\ä"ÿ›ü™•< ¶7IEND®B`‚”‰PNG  IHDR szzô pHYsMMgŒàFIDATX…íα A‚˜"uê¬3±`ÂHiþz[?†À#Gåc*ã €  =`©Uµ=²-àš™xš=ëšqqO4IEND®B`‚‰PNG  IHDR szzô pHYsMMgŒà3IDATX…í—1NÃ@E߯QRÆiÂ\9) ¥E"wÈ)L ¤ON‘3 $ÚHÆ"È…ï&¦ r4k#c'ÈûËÙÕþ·³[̇ÿ.U.<¯Ã+àvM> óó³áãQÿ%œ)Å °CˆQê½{‘ èŠbvq:¼«ëh"ȃ žöm¦c×ÝÔbžiE+e©K`’w¢õ‰xÀ® s€±ënöm¦ÀàåõVaÏ!n¼ÄèÿU°k{ó¥ àOd €0ÀPHé5o)6°= pVQ4hÊ:;ÛAg„ ÀèZ)Ë& |ÿíÄJY`‘׿“×ð^ ·èÑ9ÎÈ$6úæ£Á$WÍ<ôèÜ/¯ÿR[tÛåhfô „¬^.Cæ%XIEND®B`‚þ‰PNG  IHDR@@ªiqÞ pHYsMMgŒà°IDATxœí›OlEÆ¿76EꥪP½umUJ¸"ªT$rëÅàÒÒ ˆ‚h«âRn\¨H$â$Ên[‰ÔR.Uz ªöš^5ØŠÙQÖ‘Zõ„R%ó8¸Syÿe×Ý¿!þI9ìÎ[Ï›/ßÛµg€ìfÈKP©Âév¾sš˜Ï0Ó9/†œ[¿l€Ñ&âE&ºžkenÝ™¡M·‹\©ê¯ƒé€_&ÏȨƒðÑ?eí·í‚„c 3«k“`,ìÀÁÀ( Åï:`vüG; P¬éÌôY8¹Eƒ?/V×?qj·U¦k{,ùXL‘·ìÓVðm¬/npêècý0Kq$g´ßÐNxî,”*œnçÖÿ0Ùþ÷MÂÙ岦Ÿyð Uu-͘ðZÏéz®9n¾1ZJ ïœ6 þ¡Üâ÷wÊà`¹¬é›„³~Ôsz´;6#ˆùŒñ˜*ÍvBÈ3T–˚ΕÞsæ±60Ó˜á"!oŸ^4’ó½Ç zÕc¹ª;Éyƃ}ÚJà™E„MîysŒÝcÐ8ÃKÚݾ¬¹[f¯Î¡]B:î¼P¨u†S’§x ¸ Ælã­é÷³ï€#×:#Bò]ÎØ `/ç‰p¯Pë ûýüD Pªp:%äϲ6ÍY’ü©ß>-Àj^¿ ›G—‚€·ýö‘X µµcÌ4v?‰ Tá´øÀ Û~ñÛW"p³þSžð–¼ê·¯Ä àÕúšn\ÊÖýö—(¼ZŸ‰µ|DŸ‰À«õÁ|ÁËž^HŒ}Yÿbö¯ úM„qX_È»Àð·z„)51!à×-A³ÍñLÃËõ«yý28Zë+|; Pë áçñt®ÎÀ9!ùî‘k÷ë㱾·)ÉSp˜«‹”ü©TaG—Åi}…oÔ+ªÄ4¶š[ÿØ©=Ž»¾™Ðo‚ ž.ÔÖŽ™ÏÇm}E¸é²‡ß÷–B¬¯ðï)¿ðd»s)$Áú ß4.eëD<ã§J!)ÖWr8ÔÒ®|ß%lô£4‡X_ˆwfhS \€K)xÀq—˜H¬¯ì)Ð?ø§—Rp#*ë+} z,G¢´¾"Pú(;"µ¾"ð‰Ðó–BÔÖW„2ì·â°¾"ú,…X¬¯í]Àk)Äe}E¨/Cn¥§õ¡ Ð-ñ.»E+L⽸¬¯ýu¸9ži0¥O4àßîÍ3¥OxýÊ,L"YÐ(¿Ôðf}õK"¾Ž;6 G78M*!`Í}Ãb€Ñî=<úX?lVÑa“{Ëc](I¼Ø{ÌRœ 8¯È,Nõ“Í#ÙºP’èºáÉÙ¡ª®ž]È Uu 1ËØ`#@®•¹ÐßÏ.íO3æv’j±´iÅx½;6#}-—gˆŠ 9Ÿäåò’Å)‚œyîåòŠbumòÿ°a4¹t1sÅ®Íq°4®]!ÐdxiE3ÓÄRùÀN7Mák£¦>ž6Mõ¿m®ûƒFIÜ6´|¿Ÿms ØÝü×Òóy(IEND®B`‚‰PNG  IHDR szzô pHYsMMgŒàÃIDATX…å–¿kTA…¿³Ï5 h‘Æ*… Í.ÁÂ2‚’&ŦJâëlùÄBÒ‹ ØÏÄÊ-l‚…±³a·IBº€¤ÐÆÎÀ¢‰y{,6[ûæÍ  ·š¹÷|sïü¸ð¿›R|¹å«ÙŸçM]ùÝús)Å×w=Qó¸\Õ§‘J¼³ïñò„·ˆé¿$޳þwÖæ `[ý)žóƒ? °Öã ððl:€°D=¯X<@ (Pô¼<‰ãpñÚE׳@+^ àU×w # Pt}Ëb8ϰÞA'>  Øöuà=pIJ#^`mË×Èø€˜ØN#ÿ‚ÁŸGc'ÚùȪ–`/¥h0@ã'÷‡JøUXº­/œ2ƒùj”¢r |ZÀ]àÈF©2$oiGføáa7 àAK1m „âÚºZ;È[ÚrLýeíæM½Æ¬Àðf¸f&¢j˜·ôLf@®}ˆ–š<–yÔ*G4€$_ÀóEÞï{o´ÅðÓ @ €@3=€fzÍôšè4Ðh Ð @ €@3=€fzÍôšè4Ðh Ð @ €@3=€fzÍôšè4Ðh Ð @ màþá¾×ù&[˜“Üê58/I_{½8¬ÞÜIEND®B`‚q‰PNG  IHDR@@ªiqÞ pHYsMMgŒà#IDATxœí›_hUÆ¿3cÚ¢¤¶Ùݸ-ÙÝÔW_•b+üCñAµ³m )(b ¶ >D°n6QDѧZÄJŠðE¤²;5E’˜Ý¥ˆRm5;÷óaSmfFÍÚ{ïÞâþÞvXîwη÷Ï93³@Ÿ>}þψ®J•Ö‹õ¼@‘#µ`hBר&Ñf@aª à­~$Eí ƒá9]ã›Âû÷¯¬/K„Þ‰‘jë6]ã›B›€¨Ø…AêÓљ֨> ýh4L¹¶Uµ£S#o-nÓ¨£¤#²Íkû³¥ÊâVãZÿód‡òü“£3­A;zëǤk–„Pn"u¼0YßhP³kL  1€²qó{wæ5u»Âì`ÒÆ–ò­7Aj«A®ó{E!¶>Y¬6_6®½,l‚€x‡Š•Æ!óúÿŒ¥S%®^)U–÷ZŒ!MRÅë%ŠTŠÕƘÕ8.î"\ÝÖÆ@¼_œZ¾Ûj,ŠÛF@0±'läxiú‡;l‡cß«GcÜ„ëHï“bu雡ôƀə@lýSÛ§—n²D/ ø»å÷•?;rl9c#„Þ\Zñ’¹äùr²0yn³iùÞÐ!Q-¸U6­œ¸ùíùM&…]1 1äΕk?0Ù<¹d¦Í„ûolNã0Äê˜RÚhòÍWMt.Ði£ï¢1^šn<«[ÊM@$Š›@ÊÑb¥±O§Œ»€ ѤKÂm:§B¼P|]—‚ã¤CBÛ±x )1rF×èÎÜM H?þü–ÀSa9÷†.wg@Jò ^ËYmÉîà%’NÖË™#„Cà!–½ïÖ²OCâ½Â•ãÚà‰ŸþãA =‰Ä½D-¸c€¤%Ï/œÿíáú¸¬˜’uÅ€´ä¿Ž8pßÂxö‚Ia $ßU Û‚{æË×ÿdZ¼×› ¤Ä°äGj×|mØ —$’ðGBíþöÀðY[Aôj ¤ýò¿(Á½a0üÍ@ìϦ&¿Bb, r_ØÇò  t 5{ y4Ü—™µKˤtvÂýaýÐnas øñ y®ä¦,ÆÀ‚r©¾_×jÁPÏ_“±ð¢$Óª¼wÂ óŒ‰æ¦[ÌRß ðQ~1¸<`Ö€´Înî÷_™›¶AÝ®0i@|ÚÅ‹þgß~Ñ f×X9 ÔqÏ™ƒ[~¶¡× 6ŽÁÅö®3rM Z]£s$\ ÎÑîïÊùï5êhEã H<¾>/T{êÁ §õièÇÌ_fÅkåá/õo}ˆ:J ¢ â¥ÚþÜgÚÆîÓ§OCü›°8Ýê‚oVIEND®B`‚ g6ostinato.org®ÃÃthemes B”qds-lightƒrc âhgtransparent.pngy£Çline_horizontal_pressed.png NW§line_horizontal.png¶µ§arrow_left_disabled@2x.pngø§branch_more_focus.png5À.keep .©Garrow_down_disabled@2x.png!ñ7gtoolbar_move_vertical_pressed.png ôçwindow_close_pressed@2x.png x¶‡arrow_up_focus.png+ÒºÇtoolbar_separator_horizontal_pressed@2x.pngÏgwindow_close_pressed.png!§Çbranch_open_disabled@2x.png.Ógline_horizontal_focus@2x.pngu̇checkbox_unchecked.pngZ´§radio_checked_focus@2x.png÷checkbox_checked_pressed@2x.pngœ¤§base_icon_disabled.png¬9§toolbar_move_horizontal.png~oçwindow_close_disabled.png³L'branch_line@2x.pngМgtoolbar_move_vertical@2x.pngÇ%Garrow_right_pressed@2x.png `Ž'arrow_up_disabled.png ó«'checkbox_checked@2x.pngud‡toolbar_move_vertical_focus.pngàJradio_unchecked_disabled.png‡®gcheckbox_indeterminate.png¹gwindow_close_disabled@2x.png íGwindow_grip_pressed@2x.png) Ógtoolbar_separator_horizontal_focus@2x.png(§branch_closed_disabled.pngÖ%Çbranch_line.png"Š'checkbox_indeterminate_pressed.png“‡transparent_focus.png NZÇtoolbar_separator_horizontal.pngD Gbranch_end_focus.png 'xcheckbox_unchecked@2x.png  Gwindow_undock_disabled.pngµwgradio_checked_pressed@2x.png ë_gbranch_end_focus@2x.png!p0'toolbar_move_horizontal_focus.png -arrow_left_focus.pngüIçarrow_left@2x.png' ëågtoolbar_separator_vertical_focus@2x.pngH'window_grip_focus@2x.png /Ú‡window_undock_focus.pngUbwindow_close.png”9gline_horizontal@2x.png%~‡toolbar_move_vertical_disabled@2x.png ÕZgline_vertical_focus@2x.png¢³'arrow_right@2x.png¬ãÇbase_icon_pressed.png xçbranch_line_focus.png Jgwindow_grip_focus.png ý¢arrow_right_disabled@2x.pngˆ%branch_more_pressed@2x.png"Aî‡toolbar_move_vertical_disabled.png Ðzçarrow_up.png 3”'arrow_left_pressed@2x.pngìÑÇcheckbox_checked.png$ 'toolbar_move_horizontal_disabled.png6Gline_vertical_pressed.pngÒ£'base_icon_pressed@2x.png$” Gtoolbar_separator_vertical_focus.png5ûbranch_end_pressed.png#m‡checkbox_indeterminate_focus@2x.pngó&'window_minimize_disabled.pngO)Çwindow_undock_pressed.png. radio_checked@2x.png'=×Çtoolbar_move_horizontal_disabled@2x.png5ó§branch_closed_disabled@2x.png*pgline_vertical.png†N'branch_line_disabled.png–q‡toolbar_separator_vertical.pngƒþ'branch_more_disabled.pngu­'toolbar_move_horizontal@2x.pngº§window_undock_disabled@2x.png ÖGarrow_down_pressed.png, ¤ZGtoolbar_separator_horizontal_disabled@2x.png ¦Çbranch_more_disabled@2x.pngl£§line_vertical_focus.png#C¹gtoolbar_separator_horizontal@2x.pngþ2Garrow_left_focus@2x.png×4gtransparent_focus@2x.pngm›'branch_open_focus@2x.png*Ä÷toolbar_separator_vertical_disabled@2x.png‹Çarrow_down_focus@2x.pngø2Çarrow_up_pressed@2x.png Ú§arrow_up_disabled@2x.png checkbox_checked_disabled.png þvGcheckbox_checked_disabled@2x.pngéýçline_vertical@2x.pngÏ\Çwindow_close@2x.png¥±'arrow_right_disabled.pngµGbranch_open@2x.png HP‡checkbox_unchecked_focus@2x.png4ðÇline_vertical_disabled@2x.png%çbranch_more.png5¶gtransparent_pressed@2x.png&“çtoolbar_move_vertical.png $ìline_horizontal_focus.png8§window_minimize_focus.png fcheckbox_indeterminate@2x.png ^°branch_end_pressed@2x.png #!garrow_up_pressed.png,$Çarrow_right.png Ø'radio_checked_disabled@2x.png —Gbranch_open_pressed.png Ȥ§line_horizontal_disabled.png z‡gwindow_close_focus.png¼Ãgradio_checked_disabled.png±”'branch_more@2x.pngS%§branch_open.png"šgcheckbox_unchecked_disabled@2x.pngzÓgbase_icon_focus@2x.pngOËçcheckbox_unchecked_pressed.png˜'window_close_focus@2x.pngP+çwindow_minimize_pressed@2x.png Ú0§branch_closed.png †ùçarrow_up_focus@2x.pngå§radio_checked_pressed.pngd÷branch_end_disabled@2x.pngOj§radio_unchecked_disabled@2x.png=Çradio_unchecked_focus@2x.png>9gbase_icon@2x.png$˜ˆ‡toolbar_move_vertical_pressed@2x.png&5ð§toolbar_separator_vertical_pressed.png)g¤§toolbar_separator_horizontal_disabled.png ¦gbranch_line_disabled@2x.png·¡Gbranch_closed_pressed.pngÎúgbranch_closed_focus@2x.png ø'window_minimize@2x.pngvgwindow_minimize.png=§transparent_pressed.png§ZGwindow_grip@2x.png%lGçcheckbox_indeterminate_pressed@2x.png $9Gbase_icon_focus.pnguQ‡checkbox_checked_pressed.pngégarrow_down_focus.png ålradio_checked.png (§branch_end_disabled.png#¥‘Gtoolbar_move_horizontal_pressed.png Yn‡radio_unchecked_focus.png Fr‡window_undock_focus@2x.png û-'radio_unchecked_pressed.png' ’§toolbar_separator_vertical_disabled.png ËÌçwindow_undock.png®bçwindow_grip_disabled.pngšåbranch_open_pressed@2x.png›Gradio_checked_focus.pngàçcheckbox_checked_focus.pngs_Gwindow_undock_pressed@2x.png 3-§line_vertical_disabled.png?Úgcheckbox_unchecked_focus.png l"branch_closed_focus.png–§arrow_down@2x.png"§™Gtoolbar_move_vertical_focus@2x.png&$öçtoolbar_separator_horizontal_focus.pngÁ#gwindow_grip_disabled@2x.pngÈ–çradio_unchecked.png! Q_Gcheckbox_unchecked_pressed@2x.pngo‡window_grip.png!ö¢gtoolbar_separator_vertical@2x.png Çarrow_right_focus.png$íúçtoolbar_move_horizontal_focus@2x.pngÔ‡transparent_disabled@2x.png ¥Å'base_icon_disabled@2x.png ßϧwindow_grip_pressed.png r=base_icon.pnghÊçtransparent@2x.png@-§arrow_up@2x.png[°gline_vertical_pressed@2x.png ¤o§line_horizontal_disabled@2x.pngHÇwindow_minimize_disabled@2x.png#òGcheckbox_indeterminate_disabled.pngQçwindow_minimize_focus@2x.png nwindow_undock@2x.png&Kˆçcheckbox_indeterminate_disabled@2x.png8%branch_line_pressed@2x.png`9çbranch_line_focus@2x.png Mgbranch_line_pressed.png êçwindow_minimize_pressed.png€O§radio_unchecked@2x.png ×§checkbox_indeterminate_focus.png û Gbranch_closed_pressed@2x.pngúá'arrow_left.png) ä°toolbar_separator_vertical_pressed@2x.pnge‰çbranch_more_focus@2x.png Ò¹‡line_horizontal_pressed@2x.png gbranch_more_pressed.pngŸr‡arrow_right_pressed.png Šçarrow_down_pressed@2x.png(cSÇtoolbar_separator_horizontal_pressed.pngä-çbranch_closed@2x.pngöÿgbranch_end@2x.pngE‡arrow_left_disabled.png1C§transparent_disabled.png‹ìçbranch_open_disabled.pngb'branch_open_focus.pngÿüarrow_down_disabled.png< Çradio_unchecked_pressed@2x.png Nq‡branch_end.png ®'Gcheckbox_unchecked_disabled.pngáöÇcheckbox_checked_focus@2x.png­Çarrow_left_pressed.png æarrow_down.png&û"toolbar_move_horizontal_pressed@2x.pngCÆçarrow_right_focus@2x.png0HÑâH~•>jdI3~•>†ž¬§~•>`H›~•<Ã.ùh~•>ˆRX»~•>?fª°~•>“*å~•>V Àvu~•>~&PÉE~•>~„ÇC~•>`$ž¯ˆ~•>ˆ#R£õ~•>t#î¥i~•>ˆPær~•>~nU]~•>B(©Þ~•>ˆ,9ë~•>h'°Òg~•>ˆ&zɾ~•>- Ò~•>tLw~•>V*G#~•>L 6ý~•>-ìC!~•>j"åv~•>`$±+~•>|,û~•>7>_™~•>B(nÚÿ~•>B Vqr~•>ˆž?z~•>j$sŽ~•>LÞe~•>#%ˆµJ~•>~ÜW·~•>~¸ÇÍ~•>t æy§~•>V+*ñ^~•>-ì}Ø~•>r!bŒd~•>ˆ@Á~•>t¸'Ô~•>L  –î~•>j#œ¤Î~•>t ö:~•>`-Fõ~•>jd>©~•>?"@˜ø~•>“' Ï1~•>ˆ% ±ñ~•>-œ™~•>Vü,¾~•>5 ¢| ~•>+NÒ~•>B¢54~•>B²ï^~•>J²b~•>72Õå~•>-"–<~•>Lœç~•>†4Sº~•>7`¼ ~•>`„÷á~•>~ PÆ~•>5 di—~•>j6¾f~•>jfµ~•>ˆæ ~•>7\¤ð~•>~F] ~•>t+ì÷Õ~•>7%:´~•>j.`L~•>#..7~•>#`÷6~•>B.Ô­~•>-xÝ~•>J(¨Ûò~•>B,â7~•>L*~ï~•>B¨jï~•>VnÍà~•>L¨B~•>t~gí~•>ˆx¿œ~•>“bv~•>#'dÐ~•>Tï~•>BJg~•>~ìec~•>ˆÜ¦~•>V î—~•>`(×G~•>T X#~•>t’3~•>‘&žͨ~•>`½Á~•>Bˆ¼ª~•>B ˜Œ~•>J,¬þ“~•>L|í5~•>-.1ª~•>jz`~•>7 r”~•>L z6~•>tÒµ~•>Lé~•>^"¸œÝ~•>Væ^ý~•>t’Uç~•>V#*Ÿû~•>#ò^¯~•>h$*¨E~•>j&˜~•>7-î5~•>L*ì2~•># Úlj~•>#œÝÓ~•>LHÆ9~•>BX¡~•># Œt^~•>“¦þ<~•>BÜR”~•>t¤x~•>V!:ŠV~•>‘)€ç~•>V*&î[~•>~ ,…Ó~•>ˆ¢OÉ~•>ˆÚ F~•>ˆ¸Ã~•>#'îÓ¸~•>“€YW~•>V$V¬9~•>Vâõ~•>†¸\²~•>“-ª °~•>V0!~•>`x.~•>` ª…ò~•>jÞÿ ~•>- ¬j6~•># p‚‚~•>“ÜòÍ~•>V²è~•>V 8~v~•>j&0ÄY~•>- ü…H~•>B+^ós~•>#*öð¸~•>J(ÞÜå~•>B ¯~•>-Ô4f~•>7)Ýo~•>‘t=X~•>Lfú8~•>Tøn¢~•>7öøì~•>^ :Ä~•>+D;K~•>-êÅ~•>t&Ü΢~•>^%ĵÃ~•>5*´ð*~•>` h{~•>`R~•>~ 0hÇ~•>? p§~•>~)Æé ~•>7¸}?~•>7 æ‰È~•>|"~œS~•>`-ˆ ~•>7"öža~•>7¶~•>-ü5û~•>-p å~•>L%üÂ~•>ˆ \‡‚~•>-fÙ»~•>-þÑô~•>#¾øZ~•>r,vþ'~•>~ ¸Í~•>7Ì·“~•>jZ,j~•>†Å~•>`’ ·~•>tÈñ~•>#Üàr~•>L,Bûñ~•>#!Ò‘,~•>`  ~•>+”^r~•>~ª¥~•>?~ü~•>h+–÷;~•>tvl~•>`j2G~•>~)NÞ=~•>j¾½4~•>|!˜Ž~•>L Ì€\~•>7¤Ñ)~•>|Ì6(~•>r8Íg~•>~.‚x~•>r-å~•>#ostinato-1.3.0/client/variablefieldswidget.cpp000066400000000000000000000347761451413623100215360ustar00rootroot00000000000000/* Copyright (C) 2015 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #include "variablefieldswidget.h" #include "abstractprotocol.h" #include "protocollistiterator.h" #include "stream.h" #include #include #include Q_DECLARE_METATYPE(AbstractProtocol*); Q_DECLARE_METATYPE(OstProto::VariableField); QStringList typeNames = QStringList() << "Counter8" << "Counter16" << "Counter32"; QStringList modeNames = QStringList() << "Increment" << "Decrement" << "Random"; #define uintToHexStr(num, bytes) \ QString("%1").arg(num, bytes*2, BASE_HEX, QChar('0')).toUpper() #define hexStrToUInt(str) \ str.toUInt(NULL, BASE_HEX) /* * NOTES: * 1. We use a QSpinBox for all numeric values except for 'value' because * QSpinBox value is of type 'int' - we would like the ability to store * quint32 * 2. This widget will keep the stream always updated - every editing change * of a attribute is immediately updated in the stream; the consequence * of this design is that an explicit 'store' of widget contents to the * stream is no longer required - we still define a store() method in * case we need to change the design later */ VariableFieldsWidget::VariableFieldsWidget(QWidget *parent) : QWidget(parent) { stream_ = NULL; isProgLoad_ = false; lastSelectedProtocolIndex_ = 0; setupUi(this); attribGroup->setHidden(true); type->addItems(typeNames); mode->addItems(modeNames); valueRange_ = new QIntValidator(this); // FIXME: we can't use QIntValidator - since we want value to be able // to enter a quint32 //value->setValidator(valueRange_); connect(type, SIGNAL(currentIndexChanged(int)), SLOT(updateCurrentVariableField())); connect(offset, SIGNAL(valueChanged(int)), SLOT(updateCurrentVariableField())); connect(bitmask, SIGNAL(textChanged(QString)), SLOT(updateCurrentVariableField())); connect(mode, SIGNAL(currentIndexChanged(int)), SLOT(updateCurrentVariableField())); connect(value, SIGNAL(textChanged(QString)), SLOT(updateCurrentVariableField())); connect(count, SIGNAL(valueChanged(int)), SLOT(updateCurrentVariableField())); connect(step, SIGNAL(valueChanged(int)), SLOT(updateCurrentVariableField())); } void VariableFieldsWidget::setStream(Stream *stream) { stream_ = stream; } void VariableFieldsWidget::load() { Q_ASSERT(stream_); Q_ASSERT(protocolList->count() == 0); Q_ASSERT(variableFieldList->count() == 0); ProtocolListIterator *iter = stream_->createProtocolListIterator(); while (iter->hasNext()) { AbstractProtocol *proto = iter->next(); QListWidgetItem *protoItem = new QListWidgetItem; protoItem->setData(kProtocolPtrRole, QVariant::fromValue(proto)); protoItem->setData(kCurrentVarFieldRole, QVariant::fromValue(proto->variableFieldCount() ? 0 : -1)); protoItem->setText(proto->shortName()); decorateProtocolItem(protoItem); protocolList->addItem(protoItem); } delete iter; if (lastSelectedProtocolIndex_ < protocolList->count()) protocolList->setCurrentRow(lastSelectedProtocolIndex_); // XXX: protocolList->setCurrentRow() above will emit currentItemChanged // which will load variableFieldsList - no need to load it explicitly } void VariableFieldsWidget::store() { /* Do Nothing - see Notes at the top of the file */ } void VariableFieldsWidget::clear() { protocolList->clear(); variableFieldList->clear(); } void VariableFieldsWidget::on_protocolList_currentItemChanged( QListWidgetItem *current, QListWidgetItem *previous) { AbstractProtocol *proto; qDebug("%s: curr = %p, prev = %p", __FUNCTION__, current, previous); if (current == NULL) goto _exit; proto = current->data(kProtocolPtrRole).value(); loadProtocolFields(proto); variableFieldList->clear(); for (int i = 0; i < proto->variableFieldCount(); i++) { OstProto::VariableField vf = proto->variableField(i); QListWidgetItem *vfItem = new QListWidgetItem; setVariableFieldItem(vfItem, proto, vf); variableFieldList->addItem(vfItem); } // While switching protocols, we want to setup the attrib group // validation/ranges/masks for the current protocol, which is done // by the field/type signal handlers - so clear field/type index // now so that signals are emitted when we add/select a VF field->setCurrentIndex(-1); type->setCurrentIndex(-1); variableFieldList->setCurrentRow( current->data(kCurrentVarFieldRole).value()); lastSelectedProtocolIndex_ = protocolList->currentRow(); _exit: addButton->setEnabled(current != NULL); } void VariableFieldsWidget::on_variableFieldList_currentItemChanged( QListWidgetItem *current, QListWidgetItem *previous) { OstProto::VariableField vf; qDebug("%s: curr = %p, prev = %p", __FUNCTION__, current, previous); if (current == NULL) goto _exit; vf = current->data(kVarFieldRole).value(); isProgLoad_ = true; field->setCurrentIndex(fieldIndex(vf)); type->setCurrentIndex(vf.type()); offset->setValue(vf.offset()); bitmask->setText(uintToHexStr(vf.mask(), typeSize(vf.type()))); value->setText(QString().setNum(vf.value())); mode->setCurrentIndex(vf.mode()); count->setValue(vf.count()); step->setValue(vf.step()); isProgLoad_ = false; protocolList->currentItem()->setData( kCurrentVarFieldRole, QVariant::fromValue(variableFieldList->currentRow())); _exit: attribGroup->setHidden(current == NULL); deleteButton->setEnabled(current != NULL); } void VariableFieldsWidget::on_addButton_clicked() { QListWidgetItem *protoItem = protocolList->currentItem(); if (!protoItem) return; AbstractProtocol *proto = protoItem->data(kProtocolPtrRole) .value(); OstProto::VariableField vf; QListWidgetItem *vfItem = new QListWidgetItem; proto->appendVariableField(vf); setVariableFieldItem(vfItem, proto, vf); variableFieldList->addItem(vfItem); variableFieldList->setCurrentItem(vfItem); decorateProtocolItem(protoItem); } void VariableFieldsWidget::on_deleteButton_clicked() { QListWidgetItem *protoItem = protocolList->currentItem(); int vfIdx = variableFieldList->currentRow(); if (!protoItem || (vfIdx < 0)) return; AbstractProtocol *proto = protoItem->data(kProtocolPtrRole) .value(); proto->removeVariableField(vfIdx); delete variableFieldList->takeItem(vfIdx); // XXX: takeItem() above triggers a currentChanged, but the signal // is emitted after the "current" is changed to an item after // or before the item(s) to be deleted but before the item(s) // are actually deleted - so the current inside that slot is not // correct and we need to re-save it again protocolList->currentItem()->setData( kCurrentVarFieldRole, QVariant::fromValue(variableFieldList->currentRow())); decorateProtocolItem(protoItem); } void VariableFieldsWidget::on_field_currentIndexChanged(int index) { if (index < 0) return; QVariantMap vm = field->itemData(index).toMap(); if (index) { // standard frame fields offset->setValue(vm["offset"].toUInt()); offset->setDisabled(true); type->setCurrentIndex(vm["type"].toUInt()); type->setDisabled(true); bitmask->setText(uintToHexStr( vm["mask"].toUInt(), typeSize(OstProto::VariableField::Type( vm["type"].toUInt())))); bitmask->setDisabled(true); } else { // custom field offset->setEnabled(true); type->setEnabled(true); bitmask->setEnabled(true); } } void VariableFieldsWidget::on_type_currentIndexChanged(int index) { if ((index < 0) || !protocolList->currentItem()) return; AbstractProtocol *proto = protocolList->currentItem() ->data(kProtocolPtrRole) .value(); int protoSize = proto->protocolFrameSize(); switch (index) { case OstProto::VariableField::kCounter8: offset->setRange(0, protoSize - 1); bitmask->setInputMask("HH"); bitmask->setText("FF"); valueRange_->setRange(0, 0xFF); count->setRange(1, 0x100); step->setRange(0, 0xFF); break; case OstProto::VariableField::kCounter16: offset->setRange(0, protoSize - 2); bitmask->setInputMask("HHHH"); bitmask->setText("FFFF"); valueRange_->setRange(0, 0xFFFF); count->setRange(1, 0x10000); step->setRange(0, 0xFFFF); break; case OstProto::VariableField::kCounter32: offset->setRange(0, protoSize - 4); bitmask->setInputMask("HHHHHHHH"); bitmask->setText("FFFFFFFF"); valueRange_->setRange(0, 0xFFFFFFFF); count->setRange(1, 0x7FFFFFFF); // XXX: QSpinBox max limited to int32 step->setRange(0, 0x7FFFFFFF); break; default: Q_ASSERT(false); // unreachable break; } } void VariableFieldsWidget::updateCurrentVariableField() { // Prevent recursion if (isProgLoad_) return; if (!protocolList->currentItem()) return; if (!variableFieldList->currentItem()) return; OstProto::VariableField vf; vf.set_type(OstProto::VariableField::Type(type->currentIndex())); vf.set_offset(offset->value()); vf.set_mask(hexStrToUInt(bitmask->text())); vf.set_value(value->text().toUInt()); vf.set_mode(OstProto::VariableField::Mode(mode->currentIndex())); vf.set_count(count->value()); vf.set_step(step->value()); QListWidgetItem *protoItem = protocolList->currentItem(); AbstractProtocol *proto = protoItem->data(kProtocolPtrRole) .value(); proto->mutableVariableField(variableFieldList->currentRow())->CopyFrom(vf); setVariableFieldItem(variableFieldList->currentItem(), proto, vf); } void VariableFieldsWidget::decorateProtocolItem(QListWidgetItem *item) { AbstractProtocol *proto = item->data(kProtocolPtrRole) .value(); QFont font = item->font(); font.setBold(proto->variableFieldCount() > 0); item->setFont(font); } void VariableFieldsWidget::loadProtocolFields( const AbstractProtocol *protocol) { QVariantMap vm; field->clear(); field->addItem("Custom"); for (int i = 0; i < protocol->fieldCount(); i++) { if (!protocol->fieldFlags(i).testFlag(AbstractProtocol::FrameField)) continue; QString name = protocol->fieldData(i, AbstractProtocol::FieldName) .toString(); int bitOfs = protocol->fieldFrameBitOffset(i); int byteOfs = bitOfs >> 3; uint bitSize = protocol->fieldData(i, AbstractProtocol::FieldBitSize) .toInt(); if (bitSize == 0) continue; vm["offset"] = byteOfs; if (bitSize <= 8) { vm["type"] = int(OstProto::VariableField::kCounter8); vm["mask"] = ((0xFF << (8 - bitSize)) & 0xFF) >> (bitOfs & 0x7); } else if (bitSize <= 16) { vm["type"] = int(OstProto::VariableField::kCounter16); vm["mask"] = ((0xFFFF << (16 - bitSize)) & 0xFFFF) >> (bitOfs & 0x7); } else if (bitSize <= 32) { vm["type"] = int(OstProto::VariableField::kCounter32); vm["mask"] = ((0xFFFFFFFF << (32 - bitSize)) & 0xFFFFFFFF) >> (bitOfs & 0x7); } else { vm["type"] = int(OstProto::VariableField::kCounter32); vm["mask"] = 0xFFFFFFFF; } field->addItem(name, vm); } } /*! Given a VariableField::Type, return corresponding size in bytes */ int VariableFieldsWidget::typeSize(OstProto::VariableField::Type type) { switch(type) { case OstProto::VariableField::kCounter8 : return 1; case OstProto::VariableField::kCounter16: return 2; case OstProto::VariableField::kCounter32: return 4; default: break; } return 4; } /*! Given a variableField, return corresponding index in the field ComboBox */ int VariableFieldsWidget::fieldIndex(const OstProto::VariableField &vf) { QVariantMap vm; vm["type"] = int(vf.type()); vm["offset"] = vf.offset(); vm["mask"] = vf.mask(); int index = field->findData(vm); qDebug("vm %d %d 0x%x => index %d", vf.type(), vf.offset(), vf.mask(), index); // Not found? Use 'Custom' if (index < 0) index = 0; return index; } void VariableFieldsWidget::setVariableFieldItem( QListWidgetItem *item, const AbstractProtocol *protocol, const OstProto::VariableField &vf) { uint from = vf.value() & vf.mask(); uint to; QString fieldName = field->itemText(fieldIndex(vf)); QString itemText; if (vf.mode() == OstProto::VariableField::kDecrement) to = (vf.value() - (vf.count()-1)*vf.step()) & vf.mask(); else to = (vf.value() + (vf.count()-1)*vf.step()) & vf.mask(); item->setData(kVarFieldRole, QVariant::fromValue(vf)); itemText = QString("%1 %2 %3 from %4 to %5") .arg(protocol->shortName()) .arg(fieldName) .arg(modeNames.at(vf.mode())) .arg(from) .arg(to); if (vf.step() != 1) itemText.append(QString(" step %1").arg(vf.step())); item->setText(itemText); } ostinato-1.3.0/client/variablefieldswidget.h000066400000000000000000000046031451413623100211650ustar00rootroot00000000000000/* Copyright (C) 2015 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _VARIABLE_FIELDS_WIDGET_H #define _VARIABLE_FIELDS_WIDGET_H #include "protocol.pb.h" #include "ui_variablefieldswidget.h" #include class AbstractProtocol; class Stream; class QListWidgetItem; class VariableFieldsWidget : public QWidget, private Ui::VariableFieldsWidget { Q_OBJECT public: VariableFieldsWidget(QWidget *parent = 0); void setStream(Stream *stream); void load(); void store(); void clear(); private slots: void on_protocolList_currentItemChanged( QListWidgetItem *current, QListWidgetItem *previous); void on_variableFieldList_currentItemChanged( QListWidgetItem *current, QListWidgetItem *previous); void on_addButton_clicked(); void on_deleteButton_clicked(); void on_field_currentIndexChanged(int index); void on_type_currentIndexChanged(int index); void updateCurrentVariableField(); private: void decorateProtocolItem(QListWidgetItem *item); void loadProtocolFields(const AbstractProtocol *protocol); int typeSize(OstProto::VariableField::Type type); int fieldIndex(const OstProto::VariableField &vf); void setVariableFieldItem( QListWidgetItem *item, const AbstractProtocol *protocol, const OstProto::VariableField &vf); // Custom roles for protocol items enum { kProtocolPtrRole = Qt::UserRole, kCurrentVarFieldRole, }; // Custom roles for variable field items enum { kVarFieldRole = Qt::UserRole }; Stream *stream_; QIntValidator *valueRange_; bool isProgLoad_; // FIXME: make the lastXXX vars static? int lastSelectedProtocolIndex_; int lastSelectedVariableFieldIndex_; }; #endif ostinato-1.3.0/client/variablefieldswidget.ui000066400000000000000000000212011451413623100213440ustar00rootroot00000000000000 VariableFieldsWidget 0 0 579 384 Form true Qt::Horizontal 2 0 6 0 0 QFrame::Panel QFrame::Raised false Add variable field + :/icons/add.png:/icons/add.png false Remove variable field - :/icons/delete.png:/icons/delete.png Qt::Horizontal 40 20 A variable field is a protocol field which you want to vary across packets To create one, select the protocol on the left and click '+' above 0 0 Field 3 0 QComboBox::NoInsert Type 2 0 QComboBox::NoInsert Offset 2 0 true Mask bitmask 2 0 Mode mode QComboBox::NoInsert Value Count Step Qt::Vertical 561 41 XListWidget QListWidget
xlistwidget.h
protocolList variableFieldList deleteButton field type offset bitmask mode value count step
ostinato-1.3.0/client/xlistwidget.h000066400000000000000000000024661451413623100173610ustar00rootroot00000000000000/* Copyright (C) 2018 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _X_LIST_WIDGET_H #define _X_LIST_WIDGET_H #include #include class XListWidget : public QListWidget { public: XListWidget(QWidget *parent) : QListWidget(parent) {} virtual ~XListWidget() {} protected: virtual void paintEvent(QPaintEvent *event) { if (!model()->hasChildren()) { QPainter painter(viewport()); style()->drawItemText(&painter, viewport()->rect(), layoutDirection() | Qt::AlignCenter, palette(), true, whatsThis(), QPalette::WindowText); } else QListWidget::paintEvent(event); } }; #endif ostinato-1.3.0/client/xqlocale.h000066400000000000000000000075221451413623100166200ustar00rootroot00000000000000/* Copyright (C) 2018 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _X_LOCALE_H #define _X_LOCALE_H #include #include #include class XLocale: public QLocale { public: double toDouble(const QString &s, bool *ok = Q_NULLPTR) const { QString s2 = s; return QLocale::toDouble(s2.remove(groupSeparator()), ok); } double toPacketsPerSecond(const QString &s, bool *ok = Q_NULLPTR) const { QString text = s; double multiplier = 0; QRegularExpression regex("[a-zA-Z/]+$"); QRegularExpressionMatch match = regex.match(text); if (match.hasMatch()) { QString unit = match.captured(0).toCaseFolded(); if ((unit == "mpps") || (unit == "m")) multiplier = 1e6; else if ((unit == "kpps") || (unit == "k")) multiplier = 1e3; else if (unit == "pps") multiplier = 1; if (multiplier) text.remove(regex); } if (multiplier == 0) multiplier = 1; return toDouble(text, ok) * multiplier; } double toBitsPerSecond(const QString &s, bool *ok = Q_NULLPTR) const { QString text = s; double multiplier = 0; QRegularExpression regex("[a-zA-Z/]+$"); QRegularExpressionMatch match = regex.match(text); if (match.hasMatch()) { QString unit = match.captured(0).toCaseFolded(); if ((unit == "gbps") || (unit == "gb/s") || (unit == "g")) multiplier = 1e9; else if ((unit == "mbps") || (unit == "mb/s") || (unit == "m")) multiplier = 1e6; else if ((unit == "kbps") || (unit == "kb/s") || (unit == "k")) multiplier = 1e3; else if ((unit == "bps") || (unit == "b/s")) multiplier = 1; if (multiplier) text.remove(regex); } if (multiplier == 0) multiplier = 1; return toDouble(text, ok) * multiplier; } QString toPktRateString(double pps) const { QString text; if (pps >= 1e6) return QObject::tr("%L1 Mpps").arg(pps/1e6, 0, 'f', 3); if (pps >= 1e3) return QObject::tr("%L1 Kpps").arg(pps/1e3, 0, 'f', 3); return QObject::tr("%L1").arg(pps, 0, 'f', 3); } QString toBitRateString(double bps) const { QString text; if (bps >= 1e9) return QObject::tr("%L1 Gbps").arg(bps/1e9, 0, 'f', 3); if (bps >= 1e6) return QObject::tr("%L1 Mbps").arg(bps/1e6, 0, 'f', 3); if (bps >= 1e3) return QObject::tr("%L1 Kbps").arg(bps/1e3, 0, 'f', 3); return QObject::tr("%L1 bps").arg(bps, 0, 'f', 3); } QString toTimeIntervalString(qint64 nanosecs) const { QString text; if (nanosecs >= 1e9) return QObject::tr("%L1 s").arg(nanosecs/1e9, 0, 'f', 2); if (nanosecs >= 1e6) return QObject::tr("%L1 ms").arg(nanosecs/1e6, 0, 'f', 2); if (nanosecs >= 1e3) return QObject::tr("%L1 us").arg(nanosecs/1e3, 0, 'f', 2); return QObject::tr("%L1 ns").arg(nanosecs); } }; #endif ostinato-1.3.0/client/xtableview.h000066400000000000000000000142141451413623100171560ustar00rootroot00000000000000/* Copyright (C) 2017 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _X_TABLE_VIEW_H #define _X_TABLE_VIEW_H #include #include #include #include #include #include class XTableView : public QTableView { Q_OBJECT public: XTableView(QWidget *parent) : QTableView(parent) {} virtual ~XTableView() {} void setModel(QAbstractItemModel *model) { // This is only a heuristic, but works for us if (model && model->supportedDropActions() != Qt::IgnoreAction) _modelAllowsRemove = true; else _modelAllowsRemove = false; QTableView::setModel(model); } bool hasSelection() const { return !selectionModel()->selectedIndexes().isEmpty(); } bool canCut() const { return _modelAllowsRemove; } bool canPaste(const QMimeData *data) const { return model()->canDropMimeData(data, Qt::CopyAction, 0, 0, QModelIndex()); } public slots: void cut() { copy(); foreach(QItemSelectionRange range, selectionModel()->selection()) model()->removeRows(range.top(), range.height()); } void copy() { // Copy selection to clipboard (base class copies only current item) // Selection, by default, is in the order in which items were selected // - sort them before copying QModelIndexList selected = selectionModel()->selectedIndexes(); if (selected.isEmpty()) return; std::sort(selected.begin(), selected.end()); QMimeData *mimeData = model()->mimeData(selected); copyPlainText(selected, mimeData); qApp->clipboard()->setMimeData(mimeData); qDebug("Copied data in %d format(s) to clipboard", mimeData->formats().size()); for (int i = 0; i < mimeData->formats().size(); i++) { qDebug(" %d: %s, %d bytes", i+1, qPrintable(mimeData->formats().at(i)), mimeData->data(mimeData->formats().at(i)).size()); } } void paste() { const QMimeData *mimeData = qApp->clipboard()->mimeData(); if (!mimeData || mimeData->formats().isEmpty()) return; if (selectionModel()->hasSelection() && selectionModel()->selection().size() > 1) { qWarning("Cannot paste into multiple(%d) selections", selectionModel()->selection().size()); return; } // If no selection, insert at the end int row, column; if (selectionModel()->hasSelection() && selectionModel()->selection().size() == 1) { row = selectionModel()->selection().first().top(); column = selectionModel()->selection().first().left(); } else { row = model()->rowCount(); column = model()->columnCount(); } if (model()->canDropMimeData(mimeData, Qt::CopyAction, row, column, QModelIndex())) model()->dropMimeData(mimeData, Qt::CopyAction, row, column, QModelIndex()); } protected: virtual void paintEvent(QPaintEvent *event) { if (!model()->hasChildren()) { QPainter painter(viewport()); style()->drawItemText(&painter, viewport()->rect(), layoutDirection() | Qt::AlignCenter, palette(), true, whatsThis(), QPalette::WindowText); } else QTableView::paintEvent(event); } virtual void keyPressEvent(QKeyEvent *event) { if (event->matches(QKeySequence::Cut)) { cut(); } else if (event->matches(QKeySequence::Copy)) { copy(); } else if (event->matches(QKeySequence::Paste)) { paste(); } else QTableView::keyPressEvent(event); } private: void copyPlainText(const QModelIndexList &indexes, QMimeData *mimeData) { if (mimeData->hasText()) return; bool includeHeaders = (selectionBehavior() != QAbstractItemView::SelectItems); QString text; if (includeHeaders) { int start = 0, end = model()->columnCount(); // assume SelectRows if (selectionBehavior() == QAbstractItemView::SelectColumns) { start = indexes.first().column(); end = indexes.last().column()+1; } text.append("\t"); // column header for row number/title for (int i = start; i < end; i++) if (indexes.contains(model()->index(indexes.first().row(), i))) text.append(model()->headerData(i, Qt::Horizontal) .toString().replace('\n', ' ') +"\t");; text.append("\n"); } int lastRow = -1; foreach(QModelIndex index, indexes) { if (index.row() != lastRow) { // row changed if (lastRow >= 0) text.append("\n"); if (includeHeaders) text.append(model()->headerData(index.row(), Qt::Vertical) .toString()+"\t"); } else text.append("\t"); text.append(model()->data(index).toString()); lastRow = index.row(); } text.append("\n"); mimeData->setText(text); } bool _modelAllowsRemove{false}; }; #endif ostinato-1.3.0/client/xtreeview.h000066400000000000000000000021751451413623100170310ustar00rootroot00000000000000/* Copyright (C) 2017 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _X_TREE_VIEW_H #define _X_TREE_VIEW_H #include #include class XTreeView : public QTreeView { public: XTreeView(QWidget *parent) : QTreeView(parent) {} virtual ~XTreeView() {} private: virtual void mousePressEvent(QMouseEvent *event) { QModelIndex item = indexAt(event->pos()); if (!item.isValid()) setCurrentIndex(QModelIndex()); QTreeView::mousePressEvent(event); } }; #endif ostinato-1.3.0/common/000077500000000000000000000000001451413623100146435ustar00rootroot00000000000000ostinato-1.3.0/common/abstractprotocol.cpp000066400000000000000000001054541451413623100207450ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #include "abstractprotocol.h" #include "protocollistiterator.h" #include "streambase.h" #include "bswap.h" #include #if 0 #ifdef qDebug #undef qDebug #define qDebug(...) #endif #endif /*! \class AbstractProtocol AbstractProtocol is the base abstract class which provides the interface for all protocols. All protocols supported by Ostinato are derived from AbstractProtocol. Apart from defining the interface for a protocol, it also provides sensible default implementations for methods so that the subclasses need not re-implement. It also provides convenience functions for subclasses to use such as methods to retrieve payload size, checksum etc. A subclass typically needs to reimplement the following methods - - name() - shortName() - createInstance() - protocolNumber() - protoDataCopyInto() [pure virtual] - protoDataCopyFrom() [pure virtual] - fieldCount() - fieldFlags() - fieldData() - setFieldData() Depending on certain conditions, subclasses may need to reimplement the following additional methods - - protocolIdType() - protocolId() - protocolFrameSize() - isProtocolFrameSizeVariable() - protocolFrameVariableCount() See the description of the methods for more information. Most of the above methods just need some standard boilerplate code - the SampleProtocol implementation includes the boilerplate */ /*! Constructs an abstract protocol for the given stream and parent parent is typically NULL except for protocols which are part of a ComboProtocol */ AbstractProtocol::AbstractProtocol(StreamBase *stream, AbstractProtocol *parent) { //qDebug("%s: &prev = %p &next = %p", __FUNCTION__, &prev, &next); mpStream = stream; this->parent = parent; prev = next = NULL; _metaFieldCount = -1; _frameFieldCount = -1; _frameVariableCount = -1; protoSize = -1; _hasPayload = true; _cacheFlags |= FieldFrameBitOffsetCache; } /*! Destroys the abstract protocol */ AbstractProtocol::~AbstractProtocol() { } /*! Allocates and returns a new instance of the class. Caller is responsible for freeing up after use. Subclasses MUST implement this function */ AbstractProtocol* AbstractProtocol::createInstance(StreamBase* /* stream */, AbstractProtocol* /* parent */) { return NULL; } /*! Returns the protocol's field number as defined in message 'Protocol', enum 'k' (file: protocol.proto) Subclasses MUST implement this function \todo convert this to a protected data member instead of a virtual function */ quint32 AbstractProtocol::protocolNumber() const { qFatal("Something wrong!!!"); return 0xFFFFFFFF; } /*! Copies the common data (not specific to individual protocols) in the protocol member protobuf data into the passed in protocol parameter. The individual protocol specific protobuf data is copied using protoDataCopyInto() */ void AbstractProtocol::commonProtoDataCopyInto(OstProto::Protocol &protocol) const { protocol.clear_variable_field(); for (int i = 0; i < _data.variable_field_size(); i++) { OstProto::VariableField *vf; vf = protocol.add_variable_field(); vf->CopyFrom(_data.variable_field(i)); } } /*! Copies the common data (not specific to individual protocols) from the passed in param protocol protobuf into the member protobuf data. The individual protocol specific protobuf data is copied using protoDataCopyFrom() */ void AbstractProtocol::commonProtoDataCopyFrom(const OstProto::Protocol &protocol) { _data.clear_variable_field(); for (int i = 0; i < protocol.variable_field_size(); i++) { OstProto::VariableField *vf; vf = _data.add_variable_field(); vf->CopyFrom(protocol.variable_field(i)); } } /*! \fn virtual void AbstractProtocol::protoDataCopyInto(OstProto::Protocol &protocol) const = 0 Copy the protocol's protobuf as an extension into the passed in protocol In the base class this is a pure virtual function. Subclasses MUST implement this function. See the SampleProtocol for an example */ /*! \fn virtual void AbstractProtocol::protoDataCopyFrom(const OstProto::Protocol &protocol) = 0 Copy and update the protocol's protobuf member data variable from the passed in protocol In the base class this is a pure virtual function. Subclasses MUST implement this function. See the SampleProtocol for an example */ /*! Returns the full name of the protocol The default implementation returns a null string */ QString AbstractProtocol::name() const { return QString(); } /*! Returns the short name or abbreviation of the protocol The default implementation forms and returns an abbreviation composed of all the upper case chars in name() \n The default implementation caches the abbreviation on its first invocation and subsequently returns the cached abbreviation */ QString AbstractProtocol::shortName() const { if (protoAbbr.isNull()) { QString abbr; for (int i = 0; i < name().size(); i++) if (name().at(i).isUpper()) abbr.append(name().at(i)); if (abbr.size()) protoAbbr = abbr; else protoAbbr = QString(""); } return protoAbbr; } /*! Returns the number of fields in the protocol (both Frame fields and Meta fields) The default implementation returns zero. Subclasses MUST implement this function. */ int AbstractProtocol::fieldCount() const { return 0; } /*! Returns the number of meta fields The default implementation counts and returns the number of fields for which the MetaField flag is set\n The default implementation caches the count on its first invocation and subsequently returns the cached count */ int AbstractProtocol::metaFieldCount() const { if (_metaFieldCount < 0) { int c = 0; for (int i = 0; i < fieldCount() ; i++) if (fieldFlags(i).testFlag(MetaField)) c++; _metaFieldCount = c; } return _metaFieldCount; } /*! Returns the number of frame fields The default implementation counts and returns the number of fields for which the FrameField flag is set\n The default implementation caches the count on its first invocation and subsequently returns the cached count Subclasses which export different sets of fields based on a opcode/type (e.g. icmp) should re-implement this function */ int AbstractProtocol::frameFieldCount() const { if (_frameFieldCount < 0) { int c = 0; for (int i = 0; i < fieldCount() ; i++) if (fieldFlags(i).testFlag(FrameField)) c++; _frameFieldCount = c; } return _frameFieldCount; } /*! Returns the field flags for the passed in field index The default implementation assumes all fields to be frame fields and returns 'FrameField'. Subclasses must reimplement this method if they have any meta fields or checksum fields. See the SampleProtocol for an example. */ AbstractProtocol::FieldFlags AbstractProtocol::fieldFlags(int /*index*/) const { return FrameField; } /*! Returns the requested field attribute data Protocols which have meta fields that vary a frame field across streams may use the streamIndex to return the appropriate field value \n Some field attributes e.g. FieldName may be invariant across streams\n The FieldTextValue attribute may include additional information about the field's value e.g. a checksum field may include "(correct)" or "(incorrect)" alongwith the actual checksum value. \n The default implementation returns a empty string for FieldName and FieldTextValue; empty byte array of size 0 for FieldFrameValue; 0 for FieldValue; subclasses are expected to return meaning values for all these attributes. The only exception is the 'FieldBitSize' attribute - the default implementation takes the (byte) size of FieldFrameValue, multiplies it with 8 and returns the result - this can be used by subclasses for fields which are an integral multiple of bytes; for fields whose size are a non-integral multiple of bytes or smaller than a byte, subclasses should return the correct value. Also for fields which represent checksums, subclasses should return a value for FieldBitSize - even if it is an integral multiple of bytes. \note If a subclass uses any of the below functions to derive FieldFrameValue, the subclass should handle and return a value for FieldBitSize to prevent endless recursion - - protocolFrameCksum() - protocolFramePayloadSize() */ QVariant AbstractProtocol::fieldData(int index, FieldAttrib attrib, int streamIndex) const { switch (attrib) { case FieldName: return QString(); case FieldBitSize: Q_ASSERT_X(!fieldFlags(index).testFlag(CksumField), "AbstractProtocol::fieldData()", "FieldBitSize for checksum fields need to be handled by the subclass"); return fieldData(index, FieldFrameValue, streamIndex). toByteArray().size() * 8; case FieldValue: return 0; case FieldFrameValue: return QByteArray(); case FieldTextValue: return QString(); default: qFatal("%s:%d: unhandled case %d\n", __FUNCTION__, __LINE__, attrib); } return QVariant(); } /*! Sets the value of a field corresponding to index This method is called by the GUI code to store a user specified value into the protocol's protoBuf. Currently this method is called with FieldAttrib = FieldValue only. Returns true if field is successfully set, false otherwise. The default implementation always returns false. Subclasses should reimplement this method. See SampleProtocol for an example. */ bool AbstractProtocol::setFieldData(int /*index*/, const QVariant& /*value*/, FieldAttrib /*attrib*/) { return false; } /*! * Returns the bit offset where the specified field starts within the * protocolFrameValue() */ int AbstractProtocol::fieldFrameBitOffset(int index, int streamIndex) const { int ofs = 0; if ((index < 0) || (index >= fieldCount()) || !fieldFlags(index).testFlag(FrameField)) return -1; // Lookup Cache; if not available calculate and cache (if enabled) if (_fieldFrameBitOffset.contains(index)) { ofs = _fieldFrameBitOffset.value(index); goto _exit; } for (int i = 0; i < index; i++) { if ((_cacheFlags & FieldFrameBitOffsetCache) && !_fieldFrameBitOffset.contains(i)) _fieldFrameBitOffset.insert(i, ofs); ofs += fieldData(i, FieldBitSize, streamIndex).toInt(); } if ((_cacheFlags & FieldFrameBitOffsetCache)) _fieldFrameBitOffset.insert(index, ofs); qDebug("======> ffbo index: %d, ofs: %d", index, ofs); _exit: return ofs; } /*! * Returns the count of variableFields in the protocol */ int AbstractProtocol::variableFieldCount() const { return _data.variable_field_size(); } /*! * Appends a variableField to the protocol */ void AbstractProtocol::appendVariableField(const OstProto::VariableField &vf) { _data.add_variable_field()->CopyFrom(vf); // Update the cached value _frameVariableCount = lcm(_frameVariableCount, vf.count()); } /*! * Removes the variableField from the protocol at the specified index */ void AbstractProtocol::removeVariableField(int index) { OstProto::Protocol temp; if (index >= _data.variable_field_size()) { qWarning("%s: %s variableField[%d] out of range; count: %d)", __FUNCTION__, qPrintable(shortName()), index, _data.variable_field_size()); return; } // TODO: this is inefficient - evaluate using RepeatedPtrField? for (int i = 0; i < _data.variable_field_size(); i++) { if (i == index) continue; temp.add_variable_field()->CopyFrom(_data.variable_field(i)); } _data.clear_variable_field(); _frameVariableCount = 1; for (int i = 0; i < temp.variable_field_size(); i++) { _data.add_variable_field()->CopyFrom(temp.variable_field(i)); // Recalculate the cached value _frameVariableCount = lcm(_frameVariableCount, _data.variable_field(i).count()); } } /*! * Returns the variableField at the specified index as a constant Reference * i.e. read-only */ const OstProto::VariableField& AbstractProtocol::variableField(int index) const { Q_ASSERT(index < _data.variable_field_size()); return _data.variable_field(index); } /*! * Returns the variableField at the specified index as a mutable pointer. * Changes made via the pointer will be reflected in the protocol */ OstProto::VariableField* AbstractProtocol::mutableVariableField(int index) { if ((index < 0) || (index >= _data.variable_field_size())) return NULL; // Invalidate the cached value as the caller may potentially modify it _frameVariableCount = -1; return _data.mutable_variable_field(index); } /*! Returns the protocolIdType for the protocol The default implementation returns ProtocolIdNone. If a subclass has a protocolId field it should return the appropriate value e.g. IP protocol will return ProtocolIdIp, Ethernet will return ProtocolIdEth etc. */ AbstractProtocol::ProtocolIdType AbstractProtocol::protocolIdType() const { return ProtocolIdNone; } /*! Returns the protocol id of the protocol for the given type The default implementation returns 0. If a subclass represents a protocol which has a particular protocol id, it should return the appropriate value. If a protocol does not have an id for the given type, it should defer to the base class. e.g. IGMP will return 2 for ProtocolIdIp, and defer to the base class for the remaining ProtocolIdTypes; IP will return 0x800 for ProtocolIdEth type, 0x060603 for ProtocolIdLlc and 0x04 for ProtocolIdIp etc. */ quint32 AbstractProtocol::protocolId(ProtocolIdType /*type*/) const { return 0; } /*! Returns the protocol id of the payload protocol (the protocol that immediately follows the current one) A subclass which has a protocol id field, can use this to retrieve the appropriate value */ quint32 AbstractProtocol::payloadProtocolId(ProtocolIdType type) const { quint32 id; if (next) id = next->protocolId(type); else if (parent) id = parent->payloadProtocolId(type); else id = 0xFFFFFFFF; qDebug("%s: payloadProtocolId = 0x%x", __FUNCTION__, id); return id; } /*! Returns the protocol's size in bytes The default implementation sums up the individual field bit sizes and returns it. The default implementation calculates the caches the size on the first invocation and subsequently returns the cached size. If the subclass protocol has a varying protocol size, it MUST reimplement this method, otherwise the default implementation is sufficient. */ int AbstractProtocol::protocolFrameSize(int streamIndex) const { if (protoSize < 0) { int bitsize = 0; for (int i = 0; i < fieldCount(); i++) { if (fieldFlags(i).testFlag(FrameField)) bitsize += fieldData(i, FieldBitSize, streamIndex).toUInt(); } protoSize = (bitsize+7)/8; } qDebug("%s: protoSize = %d", __FUNCTION__, protoSize); return protoSize; } /*! Returns the byte offset in the packet where the protocol starts This method is useful only for "padding" protocols i.e. protocols which fill up the remaining space for the user defined packet size e.g. the PatternPayload protocol */ int AbstractProtocol::protocolFrameOffset(int streamIndex) const { int size = 0; AbstractProtocol *p = prev; while (p) { size += p->protocolFrameSize(streamIndex); p = p->prev; } if (parent) size += parent->protocolFrameOffset(streamIndex); qDebug("%s: ofs = %d", __FUNCTION__, size); return size; } /*! Returns the size of the payload in bytes. The payload includes all protocols subsequent to the current This method is useful for protocols which need to fill in a payload size field */ int AbstractProtocol::protocolFramePayloadSize(int streamIndex) const { int size = 0; AbstractProtocol *p = next; while (p) { size += p->protocolFrameSize(streamIndex); p = p->next; } if (parent) size += parent->protocolFramePayloadSize(streamIndex); qDebug("%s: payloadSize = %d", __FUNCTION__, size); return size; } /*! Returns a byte array encoding the protocol (and its fields) which can be inserted into the stream's frame The default implementation forms and returns an ordered concatenation of the FrameValue of all the 'frame' fields of the protocol also taking care of fields which are not an integral number of bytes\n */ QByteArray AbstractProtocol::protocolFrameValue(int streamIndex, bool forCksum, FrameValueAttrib */*attrib*/) const { QByteArray proto, field; uint bits, lastbitpos = 0; FieldFlags flags; for (int i=0; i < fieldCount() ; i++) { flags = fieldFlags(i); if (flags.testFlag(FrameField)) { bits = fieldData(i, FieldBitSize, streamIndex).toUInt(); if (bits == 0) continue; Q_ASSERT(bits > 0); if (forCksum && flags.testFlag(CksumField)) { field.resize((bits+7)/8); field.fill('\0'); } else field = fieldData(i, FieldFrameValue, streamIndex).toByteArray(); qDebug("<<< (%d, %db) %s >>>", proto.size(), lastbitpos, qPrintable(QString(proto.toHex()))); qDebug(" < %d: (%db/%dB) %s >", i, bits, field.size(), qPrintable(QString(field.toHex()))); if (bits == (uint) field.size() * 8) { if (lastbitpos == 0) proto.append(field); else { Q_ASSERT(field.size() > 0); char c = proto[proto.size() - 1]; proto[proto.size() - 1] = c | ((uchar)field.at(0) >> lastbitpos); for (int j = 0; j < field.size() - 1; j++) proto.append(field.at(j) << lastbitpos | (uchar)field.at(j+1) >> lastbitpos); proto.append(field.at(field.size() - 1) << lastbitpos); } } else if (bits < (uint) field.size() * 8) { uchar c; uint v; v = (field.size()*8) - bits; Q_ASSERT(v < 8); if (lastbitpos == 0) { for (int j = 0; j < field.size(); j++) { c = field.at(j) << v; if ((j+1) < field.size()) c |= ((uchar)field.at(j+1) >> (8-v)); proto.append(c); } lastbitpos = (lastbitpos + bits) % 8; } else { Q_ASSERT(proto.size() > 0); for (int j = 0; j < field.size(); j++) { uchar d; c = field.at(j) << v; if ((j+1) < field.size()) c |= ((uchar) field.at(j+1) >> (8-v)); d = proto[proto.size() - 1]; proto[proto.size() - 1] = d | ((uchar) c >> lastbitpos); if (bits > (8*j + (8 - v))) proto.append(c << (8-lastbitpos)); } lastbitpos = (lastbitpos + bits) % 8; } } else // if (bits > field.size() * 8) { qFatal("bitsize more than FrameValue size. skipping..."); continue; } } } // Overwrite proto with the variable fields, if any for (int i = 0; i < _data.variable_field_size(); i++) { OstProto::VariableField vf = _data.variable_field(i); varyProtocolFrameValue(proto, streamIndex, vf); } return proto; } /*! Returns true if the protocol varies one or more of its fields at run-time, false otherwise */ bool AbstractProtocol::isProtocolFrameValueVariable() const { return (protocolFrameVariableCount() > 1); } /*! Returns true if the protocol varies its size at run-time, false otherwise The default implmentation returns false. A subclass should reimplement if it varies its size at run-time e.g. a Payload protocol for a stream with incrementing/decrementing frame lengths */ bool AbstractProtocol::isProtocolFrameSizeVariable() const { return false; } /*! Returns the minimum number of frames required for the protocol to vary its fields This is the lowest common multiple (LCM) of the counts of all the varying fields in the protocol. Use the AbstractProtocol::lcm() static utility function to calculate the LCM. The default implementation returns the LCM of all variableFields A subclass should reimplement if it has varying fields e.g. an IP protocol that increments/decrements the IP address with every packet.\n Subclasses should call the base class method to retreive the count and do a LCM with the subclass' own varying fields */ int AbstractProtocol::protocolFrameVariableCount() const { if (_frameVariableCount > 0) return _frameVariableCount; _frameVariableCount = 1; for (int i = 0; i < _data.variable_field_size(); i++) _frameVariableCount = lcm(_frameVariableCount, _data.variable_field(i).count()); return _frameVariableCount; } /*! Returns true if the payload content for a protocol varies at run-time, false otherwise This is useful for subclasses which have fields dependent on payload content (e.g. UDP has a checksum field that varies if the payload varies) */ bool AbstractProtocol::isProtocolFramePayloadValueVariable() const { // TODO: it is simpler to do the following - // return (protocolFramePayloadVariableCount() > 1) // However, it may be inefficient till the time we cache the // variable count AbstractProtocol *p = next; while (p) { if (p->isProtocolFrameValueVariable()) return true; p = p->next; } if (parent && parent->isProtocolFramePayloadValueVariable()) return true; return false; } /*! Returns true if the payload size for a protocol varies at run-time, false otherwise This is useful for subclasses which have fields dependent on payload size (e.g. UDP has a checksum field that varies if the payload varies) */ bool AbstractProtocol::isProtocolFramePayloadSizeVariable() const { AbstractProtocol *p = next; while (p) { if (p->isProtocolFrameSizeVariable()) return true; p = p->next; } if (parent && parent->isProtocolFramePayloadSizeVariable()) return true; return false; } /*! Returns true if the payload size for a protocol varies at run-time, false otherwise This is useful for subclasses which have fields dependent on payload size (e.g. UDP has a checksum field that varies if the payload varies) */ int AbstractProtocol::protocolFramePayloadVariableCount() const { int count = 1; AbstractProtocol *p = next; while (p) { if (p->isProtocolFrameValueVariable() || p->isProtocolFrameSizeVariable()) count = lcm(count, p->protocolFrameVariableCount()); p = p->next; } if (parent && (parent->isProtocolFramePayloadValueVariable() || parent->isProtocolFramePayloadSizeVariable())) count = lcm(count, parent->protocolFramePayloadVariableCount()); return false; } /*! Returns true if the protocol typically contains a payload or other protocols following it e.g. TCP, UDP have payloads, while ARP, IGMP do not The default implementation returns true. If a subclass does not have a payload, it should set the _hasPayload data member to false */ bool AbstractProtocol::protocolHasPayload() const { return _hasPayload; } /*! Returns the checksum (of the requested type) of the protocol's contents Useful for protocols which have a checksum field \note If a subclass uses protocolFrameCksum() from within fieldData() to derive a cksum field, it MUST handle and return the 'FieldBitSize' attribute also for that particular field instead of using the default AbstractProtocol implementation for 'FieldBitSize' - this is required to prevent infinite recursion */ quint32 AbstractProtocol::protocolFrameCksum(int streamIndex, CksumType cksumType, CksumFlags cksumFlags) const { static int recursionCount = 0; quint32 cksum = 0xFFFFFFFF; recursionCount++; Q_ASSERT_X(recursionCount < 10, "protocolFrameCksum", "potential infinite recursion - does a protocol checksum field not implement FieldBitSize?"); switch(cksumType) { case CksumIp: { QByteArray fv; quint16 *ip; quint32 len, sum = 0; bool forCksum = cksumFlags.testFlag(IncludeCksumField) ? false : true; fv = protocolFrameValue(streamIndex, forCksum); ip = (quint16*) fv.constData(); len = fv.size(); while(len > 1) { sum += *ip; if(sum & 0x80000000) sum = (sum & 0xFFFF) + (sum >> 16); ip++; len -= 2; } if (len) sum += (unsigned short) *(unsigned char *)ip; while(sum>>16) sum = (sum & 0xFFFF) + (sum >> 16); cksum = qFromBigEndian((quint16) ~sum); break; } case CksumTcpUdp: { quint16 cks; quint32 sum = 0; cks = protocolFrameCksum(streamIndex, CksumIp); sum += (quint16) ~cks; cks = protocolFramePayloadCksum(streamIndex, CksumIp); sum += (quint16) ~cks; cks = protocolFrameHeaderCksum(streamIndex, CksumIpPseudo); sum += (quint16) ~cks; while(sum>>16) sum = (sum & 0xFFFF) + (sum >> 16); cksum = (~sum) & 0xFFFF; break; } case CksumIcmpIgmp: { quint16 cks; quint32 sum = 0; cks = protocolFrameCksum(streamIndex, CksumIp); sum += (quint16) ~cks; cks = protocolFramePayloadCksum(streamIndex, CksumIp); sum += (quint16) ~cks; while(sum>>16) sum = (sum & 0xFFFF) + (sum >> 16); cksum = (~sum) & 0xFFFF; break; } default: qDebug("Unknown cksumType %d", cksumType); break; } recursionCount--; return cksum; } /*! Returns the checksum of the requested type for the protocol's header This is useful for subclasses which needs the header's checksum e.g. TCP/UDP require a "Pseudo-IP" checksum. The checksum is limited to the specified scope. Currently the default implementation supports only type CksumIpPseudo \note The default value for cksumScope is different for protocolFrameHeaderCksum() and protocolFramePayloadCksum() */ quint32 AbstractProtocol::protocolFrameHeaderCksum(int streamIndex, CksumType cksumType, CksumScope cksumScope) const { quint32 sum = 0; quint32 cksum; AbstractProtocol *p = prev; Q_ASSERT(cksumType == CksumIpPseudo); // We may have extension headers between us and the IP header - skip 'em while (p) { cksum = p->protocolFrameCksum(streamIndex, cksumType); if (cksum <= 0xFFFF) // protocol has a valid pseudo cksum ie its IP { sum += (quint16) ~cksum; // Ip4/6Protocol::protocolFrameCksum(CksumIpPseudo) only // counts the src/dst IP (see Note in there) // Count the payload length and protocolId here sum += protocolFrameSize(streamIndex) + protocolFramePayloadSize(streamIndex); sum += protocolId(ProtocolIdIp); qDebug("%s: sum = %x, cksum = %x", __FUNCTION__, sum, cksum); if (cksumScope == CksumScopeAdjacentProtocol) goto out; } p = p->prev; } if (parent) { cksum = parent->protocolFrameHeaderCksum(streamIndex, cksumType, cksumScope); sum += (quint16) ~cksum; } out: while(sum>>16) sum = (sum & 0xFFFF) + (sum >> 16); return (quint16) ~sum; } /*! Returns the checksum of the requested type for the protocol's payload This is useful for subclasses which needs the payload's checksum e.g. TCP/UDP require a IP checksum of the payload (to be combined with other checksums to derive the final checksum). The checksum is limited to the specified scope. Currently the default implementation supports only type CksumIp \note The default value for cksumScope is different for protocolFrameHeaderCksum() and protocolFramePayloadCksum() */ quint32 AbstractProtocol::protocolFramePayloadCksum(int streamIndex, CksumType cksumType, CksumScope cksumScope) const { quint32 sum; quint16 cksum; AbstractProtocol *p = next; Q_ASSERT(cksumType == CksumIp); if (!p) return 0xFFFF; cksum = p->protocolFrameCksum(streamIndex, cksumType, IncludeCksumField); sum = (quint16) ~cksum; if (cksumScope == CksumScopeAdjacentProtocol) goto out; p = p->next; while (p) { cksum = p->protocolFrameCksum(streamIndex, cksumType, IncludeCksumField); // when combining cksums, a non-first protocol starting at odd offset // needs a byte swap (see RFC 1071 section(s) 2A, 2B) if (p->protocolFrameOffset(streamIndex) & 0x1) cksum = swap16(cksum); sum += (quint16) ~cksum; p = p->next; } if (parent) { cksum = parent->protocolFramePayloadCksum(streamIndex, cksumType, cksumScope); sum += (quint16) ~cksum; } out: while(sum>>16) sum = (sum & 0xFFFF) + (sum >> 16); cksum = (quint16) ~sum; qDebug("%s: cksum = %u", __FUNCTION__, cksum); return cksum; } /*! Returns true, if the protocol fields are incorrect or may cause overall packet to be invalid Error details are put in the optional INOUT param 'errors', if true. */ bool AbstractProtocol::hasErrors(QStringList* /*errors*/) const { return false; } // Stein's binary GCD algo - from wikipedia quint64 AbstractProtocol::gcd(quint64 u, quint64 v) { int shift; /* GCD(0,x) := x */ if (u == 0 || v == 0) return u | v; /* Let shift := lg K, where K is the greatest power of 2 dividing both u and v. */ for (shift = 0; ((u | v) & 1) == 0; ++shift) { u >>= 1; v >>= 1; } while ((u & 1) == 0) u >>= 1; /* From here on, u is always odd. */ do { while ((v & 1) == 0) /* Loop X */ v >>= 1; /* Now u and v are both odd, so diff(u, v) is even. Let u = min(u, v), v = diff(u, v)/2. */ if (u < v) { v -= u; } else { quint64 diff = u - v; u = v; v = diff; } v >>= 1; } while (v != 0); return u << shift; } quint64 AbstractProtocol::lcm(quint64 u, quint64 v) { #if 0 /* LCM(0,x) := x */ if (u == 0 || v == 0) return u | v; #else /* For our use case, neither u nor v can ever be 0, the minimum value is 1; we do this correction silently here */ if (u == 0) u = 1; if (v == 0) v = 1; if (u == 1 || v == 1) return (u * v); #endif return (u * v)/gcd(u, v); } /* * XXX: varyCounter() is not a member of AbstractProtocol to avoid * moving it into the header file and thereby keeping the header file * clean */ template bool varyCounter(QString protocolName, QByteArray &buf, int frameIndex, const OstProto::VariableField &varField) { int x = (frameIndex % varField.count()) * varField.step(); T oldfv, newfv; if ((varField.offset() + sizeof(T)) > uint(buf.size())) { qWarning("%s varField ofs %d beyond protocol frame %d - skipping", qPrintable(protocolName), varField.offset(), buf.size()); return false; } oldfv = *((T*)((uchar*)buf.constData() + varField.offset())); if (sizeof(T) > sizeof(quint8)) oldfv = qFromBigEndian(oldfv); switch(varField.mode()) { case OstProto::VariableField::kIncrement: newfv = (oldfv & ~varField.mask()) | ((varField.value() + x) & varField.mask()); break; case OstProto::VariableField::kDecrement: newfv = (oldfv & ~varField.mask()) | ((varField.value() - x) & varField.mask()); break; case OstProto::VariableField::kRandom: newfv = (oldfv & ~varField.mask()) | ((varField.value() + qrand()) & varField.mask()); break; default: qWarning("%s Unsupported varField mode %d", qPrintable(protocolName), varField.mode()); return false; } if (sizeof(T) == sizeof(quint8)) *((uchar*)buf.constData() + varField.offset()) = newfv; else qToBigEndian(newfv, (uchar*)buf.constData() + varField.offset()); qDebug("%s varField ofs %d oldfv %x newfv %x", qPrintable(protocolName), varField.offset(), oldfv, newfv); return true; } void AbstractProtocol::varyProtocolFrameValue(QByteArray &buf, int frameIndex, const OstProto::VariableField &varField) const { switch (varField.type()) { case OstProto::VariableField::kCounter8: varyCounter(shortName(), buf, frameIndex, varField); break; case OstProto::VariableField::kCounter16: varyCounter(shortName(), buf, frameIndex, varField); break; case OstProto::VariableField::kCounter32: varyCounter(shortName(), buf, frameIndex, varField); break; default: break; } return; } ostinato-1.3.0/common/abstractprotocol.h000066400000000000000000000153461451413623100204120ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _ABSTRACT_PROTOCOL_H #define _ABSTRACT_PROTOCOL_H #include #include #include #include #include #include #include //#include "../rpc/pbhelper.h" #include "protocol.pb.h" #define BASE_BIN (2) #define BASE_OCT (8) #define BASE_DEC (10) #define BASE_HEX (16) struct FrameValueAttrib; class StreamBase; class ProtocolListIterator; class AbstractProtocol { template friend class ComboProtocol; friend class ProtocolListIterator; private: mutable int _metaFieldCount; mutable int _frameFieldCount; mutable int _frameVariableCount; mutable int protoSize; mutable QString protoAbbr; mutable QHash _fieldFrameBitOffset; OstProto::Protocol _data; protected: StreamBase *mpStream; //!< Stream that this protocol belongs to AbstractProtocol *parent; //!< Parent protocol, if any AbstractProtocol *prev; //!< Protocol preceding this protocol AbstractProtocol *next; //!< Protocol succeeding this protocol //! Is protocol typically followed by payload or another protocol bool _hasPayload; //! Caching Control Flags enum CacheFlag { FieldFrameBitOffsetCache = 0x1 }; quint32 _cacheFlags; public: //! Properties of a field, can be OR'd enum FieldFlag { FrameField = 0x1, //!< field appears in frame content MetaField = 0x2, //!< field does not appear in frame, is meta data CksumField = 0x4 //!< field is a checksum and appears in frame content }; Q_DECLARE_FLAGS(FieldFlags, FieldFlag); //!< \private abcd //! Various attributes of a field enum FieldAttrib { FieldName, //!< name FieldValue, //!< value in host byte order (user editable) FieldTextValue, //!< value as text FieldFrameValue, //!< frame encoded value in network byte order FieldBitSize, //!< size in bits }; //! Supported Protocol Id types enum ProtocolIdType { ProtocolIdNone, //!< Marker representing non-existent protocol id ProtocolIdLlc, //!< LLC (802.2) ProtocolIdEth, //!< Ethernet II ProtocolIdIp, //!< IP ProtocolIdTcpUdp, //!< TCP/UDP Port Number }; //! Supported checksum types enum CksumType { CksumIp, //!< Standard IP Checksum CksumIpPseudo, //!< Standard checksum for Pseudo-IP header CksumTcpUdp, //!< Standard TCP/UDP checksum including pseudo-IP CksumIcmpIgmp, //!< Standard ICMP/IGMP checksum CksumMax //!< Marker for number of cksum types }; //! Flags affecting cksum calculation, can be OR'd enum CksumFlag { IncludeCksumField = 0x1, //!< Default: exclude cksum field(s) }; Q_DECLARE_FLAGS(CksumFlags, CksumFlag); //!< \private abcd //! Supported checksum scopes enum CksumScope { CksumScopeAdjacentProtocol, //!< Cksum only the adjacent protocol CksumScopeAllProtocols, //!< Cksum over all the protocols }; AbstractProtocol(StreamBase *stream, AbstractProtocol *parent = 0); virtual ~AbstractProtocol(); static AbstractProtocol* createInstance(StreamBase *stream, AbstractProtocol *parent = 0); virtual quint32 protocolNumber() const; void commonProtoDataCopyInto(OstProto::Protocol &protocol) const; void commonProtoDataCopyFrom(const OstProto::Protocol &protocol); virtual void protoDataCopyInto(OstProto::Protocol &protocol) const = 0; virtual void protoDataCopyFrom(const OstProto::Protocol &protocol) = 0; virtual QString name() const; virtual QString shortName() const; virtual ProtocolIdType protocolIdType() const; virtual quint32 protocolId(ProtocolIdType type) const; quint32 payloadProtocolId(ProtocolIdType type) const; virtual int fieldCount() const; int metaFieldCount() const; virtual int frameFieldCount() const; virtual FieldFlags fieldFlags(int index) const; virtual QVariant fieldData(int index, FieldAttrib attrib, int streamIndex = 0) const; virtual bool setFieldData(int index, const QVariant &value, FieldAttrib attrib = FieldValue); int fieldFrameBitOffset(int index, int streamIndex = 0) const; int variableFieldCount() const; void appendVariableField(const OstProto::VariableField &vf); void removeVariableField(int index); const OstProto::VariableField& variableField(int index) const; OstProto::VariableField* mutableVariableField(int index); virtual QByteArray protocolFrameValue(int streamIndex = 0, bool forCksum = false, FrameValueAttrib *attrib = nullptr) const; virtual int protocolFrameSize(int streamIndex = 0) const; int protocolFrameOffset(int streamIndex = 0) const; int protocolFramePayloadSize(int streamIndex = 0) const; virtual bool isProtocolFrameValueVariable() const; virtual bool isProtocolFrameSizeVariable() const; virtual int protocolFrameVariableCount() const; bool isProtocolFramePayloadValueVariable() const; bool isProtocolFramePayloadSizeVariable() const; int protocolFramePayloadVariableCount() const; bool protocolHasPayload() const; virtual quint32 protocolFrameCksum(int streamIndex = 0, CksumType cksumType = CksumIp, CksumFlags cksumFlags = 0) const; quint32 protocolFrameHeaderCksum(int streamIndex = 0, CksumType cksumType = CksumIp, CksumScope cksumScope = CksumScopeAdjacentProtocol) const; quint32 protocolFramePayloadCksum(int streamIndex = 0, CksumType cksumType = CksumIp, CksumScope cksumScope = CksumScopeAllProtocols) const; virtual bool hasErrors(QStringList *errors = nullptr) const; static quint64 lcm(quint64 u, quint64 v); static quint64 gcd(quint64 u, quint64 v); private: void varyProtocolFrameValue(QByteArray &buf, int frameIndex, const OstProto::VariableField &varField) const; }; Q_DECLARE_OPERATORS_FOR_FLAGS(AbstractProtocol::FieldFlags); #endif ostinato-1.3.0/common/abstractprotocolconfig.h000066400000000000000000000056231451413623100215750ustar00rootroot00000000000000/* Copyright (C) 2013-2014 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _ABSTRACT_PROTOCOL_CONFIG_H #define _ABSTRACT_PROTOCOL_CONFIG_H #include class AbstractProtocol; /*! Convenience Macro - can be used by loadWidget() methods */ #define uintToHexStr(num, bytes) \ QString("%1").arg(num, bytes*2, BASE_HEX, QChar('0')) class AbstractProtocolConfigForm : public QWidget { Q_OBJECT public: /*! Constructs the widget */ AbstractProtocolConfigForm(QWidget *parent = 0) : QWidget(parent) { // Do nothing! } /*! Destroys the widget */ virtual ~AbstractProtocolConfigForm() { // Do nothing! } /*! Allocates and returns a new instance of the widget. Caller is responsible for freeing up after use. Subclasses MUST implement this function */ static AbstractProtocolConfigForm* createInstance() { return NULL; } /*! Loads data from the protocol using it's fieldData() method into this widget. Any conversion to user friendly display/editing formats (e.g. hex format) SHOULD be done by this method. Subclasses MUST implement this function. See the SampleProtocol for an example */ virtual void loadWidget(AbstractProtocol* /*proto*/) { // Do nothing! } /*! Stores data from this widget into the protocol using the protocol's setFieldData() method. Field values MUST be converted from any user friendly display/editing formats (e.g. hex format) to simple Qt-style integers/strings before passing to setFieldData() Subclasses MUST implement this function. See the SampleProtocol for an example */ virtual void storeWidget(AbstractProtocol* /*proto*/) { // Do nothing! } /*! Convenience Method - can be used by storeWidget() implementations */ uint hexStrToUInt(QString text, bool *ok=NULL) { bool isOk; uint a_uint = text.remove(QChar(' ')).toUInt(&isOk, 16); if (ok) *ok = isOk; return a_uint; } /*! Convenience Method - can be used by storeWidget() implementations */ quint64 hexStrToUInt64(QString text, bool *ok=NULL) { bool isOk; quint64 a_uint = text.remove(QChar(' ')).toULongLong(&isOk, 16); if (ok) *ok = isOk; return a_uint; } }; #endif ostinato-1.3.0/common/arp.cpp000066400000000000000000000612121451413623100161330ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #include "arp.h" #include #include #define uintToMacStr(num) \ QString("%1").arg(num, 6*2, BASE_HEX, QChar('0')) \ .replace(QRegExp("([0-9a-fA-F]{2}\\B)"), "\\1:").toUpper() ArpProtocol::ArpProtocol(StreamBase *stream, AbstractProtocol *parent) : AbstractProtocol(stream, parent) { _hasPayload = false; } ArpProtocol::~ArpProtocol() { } AbstractProtocol* ArpProtocol::createInstance(StreamBase *stream, AbstractProtocol *parent) { return new ArpProtocol(stream, parent); } quint32 ArpProtocol::protocolNumber() const { return OstProto::Protocol::kArpFieldNumber; } void ArpProtocol::protoDataCopyInto(OstProto::Protocol &protocol) const { protocol.MutableExtension(OstProto::arp)->CopyFrom(data); protocol.mutable_protocol_id()->set_id(protocolNumber()); } void ArpProtocol::protoDataCopyFrom(const OstProto::Protocol &protocol) { if (protocol.protocol_id().id() == protocolNumber() && protocol.HasExtension(OstProto::arp)) data.MergeFrom(protocol.GetExtension(OstProto::arp)); } QString ArpProtocol::name() const { return QString("Address Resolution Protocol"); } QString ArpProtocol::shortName() const { return QString("ARP"); } /*! Return the ProtocolIdType for your protocol \n If your protocol doesn't have a protocolId field, you don't need to reimplement this method - the base class implementation will do the right thing */ #if 0 AbstractProtocol::ProtocolIdType ArpProtocol::protocolIdType() const { return ProtocolIdIp; } #endif /*! Return the protocolId for your protocol based on the 'type' requested \n If not all types are valid for your protocol, handle the valid type(s) and for the remaining fallback to the base class implementation; if your protocol doesn't have a protocolId at all, you don't need to reimplement this method - the base class will do the right thing */ quint32 ArpProtocol::protocolId(ProtocolIdType type) const { switch(type) { case ProtocolIdEth: return 0x0806; default:break; } return AbstractProtocol::protocolId(type); } int ArpProtocol::fieldCount() const { return arp_fieldCount; } AbstractProtocol::FieldFlags ArpProtocol::fieldFlags(int index) const { AbstractProtocol::FieldFlags flags; flags = AbstractProtocol::fieldFlags(index); switch (index) { case arp_hwType: case arp_protoType: case arp_hwAddrLen: case arp_protoAddrLen: case arp_opCode: case arp_senderHwAddr: case arp_senderProtoAddr: case arp_targetHwAddr: case arp_targetProtoAddr: break; case arp_senderHwAddrMode: case arp_senderHwAddrCount: case arp_senderProtoAddrMode: case arp_senderProtoAddrCount: case arp_senderProtoAddrMask: case arp_targetHwAddrMode: case arp_targetHwAddrCount: case arp_targetProtoAddrMode: case arp_targetProtoAddrCount: case arp_targetProtoAddrMask: flags &= ~FrameField; flags |= MetaField; break; default: qFatal("%s: unimplemented case %d in switch", __PRETTY_FUNCTION__, index); break; } return flags; } QVariant ArpProtocol::fieldData(int index, FieldAttrib attrib, int streamIndex) const { switch (index) { case arp_hwType: { switch(attrib) { case FieldName: return QString("Hardware Type"); case FieldValue: return data.hw_type(); case FieldTextValue: return QString("%1").arg(data.hw_type()); case FieldFrameValue: { QByteArray fv; fv.resize(2); qToBigEndian((quint16) data.hw_type(), (uchar*) fv.data()); return fv; } default: break; } break; } case arp_protoType: { switch(attrib) { case FieldName: return QString("Protocol Type"); case FieldValue: return data.proto_type(); case FieldTextValue: return QString("%1").arg(data.proto_type(), 4, BASE_HEX, QChar('0')); case FieldFrameValue: { QByteArray fv; fv.resize(2); qToBigEndian((quint16) data.proto_type(), (uchar*) fv.data()); return fv; } default: break; } break; } case arp_hwAddrLen: { switch(attrib) { case FieldName: return QString("Hardware Address Length"); case FieldValue: return data.hw_addr_len(); case FieldTextValue: return QString("%1").arg(data.hw_addr_len()); case FieldFrameValue: return QByteArray(1, (char) data.hw_addr_len()); default: break; } break; } case arp_protoAddrLen: { switch(attrib) { case FieldName: return QString("Protocol Address Length"); case FieldValue: return data.proto_addr_len(); case FieldTextValue: return QString("%1").arg(data.proto_addr_len()); case FieldFrameValue: return QByteArray(1, (char) data.proto_addr_len()); default: break; } break; } case arp_opCode: { switch(attrib) { case FieldName: return QString("Operation Code"); case FieldValue: return data.op_code(); case FieldTextValue: return QString("%1").arg(data.op_code()); case FieldFrameValue: { QByteArray fv; fv.resize(2); qToBigEndian((quint16) data.op_code(), (uchar*) fv.data()); return fv; } default: break; } break; } case arp_senderHwAddr: { int u; const int hwAddrStep = 1; quint64 hwAddr = 0; switch (data.sender_hw_addr_mode()) { case OstProto::Arp::kFixed: hwAddr = data.sender_hw_addr(); break; case OstProto::Arp::kIncrement: u = (streamIndex % data.sender_hw_addr_count()) * hwAddrStep; hwAddr = data.sender_hw_addr() + u; break; case OstProto::Arp::kDecrement: u = (streamIndex % data.sender_hw_addr_count()) * hwAddrStep; hwAddr = data.sender_hw_addr() - u; break; default: qWarning("Unhandled hw_addr_mode %d", data.sender_hw_addr_mode()); } switch(attrib) { case FieldName: return QString("Sender Hardware Address"); case FieldValue: return hwAddr; case FieldTextValue: return uintToMacStr(hwAddr); case FieldFrameValue: { QByteArray fv; fv.resize(8); qToBigEndian((quint64) hwAddr, (uchar*) fv.data()); fv.remove(0, 2); return fv; } default: break; } break; } case arp_senderProtoAddr: { int u; quint32 subnet, host, protoAddr = 0; switch(data.sender_proto_addr_mode()) { case OstProto::Arp::kFixedHost: protoAddr = data.sender_proto_addr(); break; case OstProto::Arp::kIncrementHost: u = streamIndex % data.sender_proto_addr_count(); subnet = data.sender_proto_addr() & data.sender_proto_addr_mask(); host = (((data.sender_proto_addr() & ~data.sender_proto_addr_mask()) + u) & ~data.sender_proto_addr_mask()); protoAddr = subnet | host; break; case OstProto::Arp::kDecrementHost: u = streamIndex % data.sender_proto_addr_count(); subnet = data.sender_proto_addr() & data.sender_proto_addr_mask(); host = (((data.sender_proto_addr() & ~data.sender_proto_addr_mask()) - u) & ~data.sender_proto_addr_mask()); protoAddr = subnet | host; break; case OstProto::Arp::kRandomHost: subnet = data.sender_proto_addr() & data.sender_proto_addr_mask(); host = (qrand() & ~data.sender_proto_addr_mask()); protoAddr = subnet | host; break; default: qWarning("Unhandled sender_proto_addr_mode = %d", data.sender_proto_addr_mode()); } switch(attrib) { case FieldName: return QString("Sender Protocol Address"); case FieldValue: return protoAddr; case FieldFrameValue: { QByteArray fv; fv.resize(4); qToBigEndian((quint32) protoAddr, (uchar*) fv.data()); return fv; } case FieldTextValue: return QHostAddress(protoAddr).toString(); default: break; } break; } case arp_targetHwAddr: { int u; const int hwAddrStep = 1; quint64 hwAddr = 0; switch (data.target_hw_addr_mode()) { case OstProto::Arp::kFixed: hwAddr = data.target_hw_addr(); break; case OstProto::Arp::kIncrement: u = (streamIndex % data.target_hw_addr_count()) * hwAddrStep; hwAddr = data.target_hw_addr() + u; break; case OstProto::Arp::kDecrement: u = (streamIndex % data.target_hw_addr_count()) * hwAddrStep; hwAddr = data.target_hw_addr() - u; break; default: qWarning("Unhandled hw_addr_mode %d", data.target_hw_addr_mode()); } switch(attrib) { case FieldName: return QString("Target Hardware Address"); case FieldValue: return hwAddr; case FieldTextValue: return uintToMacStr(hwAddr); case FieldFrameValue: { QByteArray fv; fv.resize(8); qToBigEndian((quint64) hwAddr, (uchar*) fv.data()); fv.remove(0, 2); return fv; } default: break; } break; } case arp_targetProtoAddr: { int u; quint32 subnet, host, protoAddr = 0; switch(data.target_proto_addr_mode()) { case OstProto::Arp::kFixedHost: protoAddr = data.target_proto_addr(); break; case OstProto::Arp::kIncrementHost: u = streamIndex % data.target_proto_addr_count(); subnet = data.target_proto_addr() & data.target_proto_addr_mask(); host = (((data.target_proto_addr() & ~data.target_proto_addr_mask()) + u) & ~data.target_proto_addr_mask()); protoAddr = subnet | host; break; case OstProto::Arp::kDecrementHost: u = streamIndex % data.target_proto_addr_count(); subnet = data.target_proto_addr() & data.target_proto_addr_mask(); host = (((data.target_proto_addr() & ~data.target_proto_addr_mask()) - u) & ~data.target_proto_addr_mask()); protoAddr = subnet | host; break; case OstProto::Arp::kRandomHost: subnet = data.target_proto_addr() & data.target_proto_addr_mask(); host = (qrand() & ~data.target_proto_addr_mask()); protoAddr = subnet | host; break; default: qWarning("Unhandled target_proto_addr_mode = %d", data.target_proto_addr_mode()); } switch(attrib) { case FieldName: return QString("Target Protocol Address"); case FieldValue: return protoAddr; case FieldFrameValue: { QByteArray fv; fv.resize(4); qToBigEndian((quint32) protoAddr, (uchar*) fv.data()); return fv; } case FieldTextValue: return QHostAddress(protoAddr).toString(); default: break; } break; } // Meta fields case arp_senderHwAddrMode: switch(attrib) { case FieldName: return QString("Sender Hardware Address Mode"); case FieldValue: return data.sender_hw_addr_mode(); default: break; } break; case arp_senderHwAddrCount: switch(attrib) { case FieldName: return QString("Sender Hardware Address Count"); case FieldValue: return data.sender_hw_addr_count(); default: break; } break; case arp_senderProtoAddrMode: switch(attrib) { case FieldName: return QString("Sender Protocol Address Mode"); case FieldValue: return data.sender_proto_addr_mode(); default: break; } break; case arp_senderProtoAddrCount: switch(attrib) { case FieldName: return QString("Sender Protocol Address Count"); case FieldValue: return data.sender_proto_addr_count(); default: break; } break; case arp_senderProtoAddrMask: switch(attrib) { case FieldName: return QString("Sender Protocol Address Mask"); case FieldValue: return data.sender_proto_addr_mask(); default: break; } break; case arp_targetHwAddrMode: switch(attrib) { case FieldName: return QString("Target Hardware Address Mode"); case FieldValue: return data.target_hw_addr_mode(); default: break; } break; case arp_targetHwAddrCount: switch(attrib) { case FieldName: return QString("Target Hardware Address Count"); case FieldValue: return data.target_hw_addr_count(); default: break; } break; case arp_targetProtoAddrMode: switch(attrib) { case FieldName: return QString("Target Protocol Address Mode"); case FieldValue: return data.target_proto_addr_mode(); default: break; } break; case arp_targetProtoAddrCount: switch(attrib) { case FieldName: return QString("Target Protocol Address Count"); case FieldValue: return data.target_proto_addr_count(); default: break; } break; case arp_targetProtoAddrMask: switch(attrib) { case FieldName: return QString("Target Protocol Address Mask"); case FieldValue: return data.target_proto_addr_mask(); default: break; } break; default: qFatal("%s: unimplemented case %d in switch", __PRETTY_FUNCTION__, index); break; } return AbstractProtocol::fieldData(index, attrib, streamIndex); } bool ArpProtocol::setFieldData(int index, const QVariant &value, FieldAttrib attrib) { bool isOk = false; if (attrib != FieldValue) goto _exit; switch (index) { case arp_hwType: { uint hwType = value.toUInt(&isOk); if (isOk) data.set_hw_type(hwType); break; } case arp_protoType: { uint protoType = value.toUInt(&isOk); if (isOk) data.set_proto_type(protoType); break; } case arp_hwAddrLen: { uint hwAddrLen = value.toUInt(&isOk); if (isOk) data.set_hw_addr_len(hwAddrLen); break; } case arp_protoAddrLen: { uint protoAddrLen = value.toUInt(&isOk); if (isOk) data.set_proto_addr_len(protoAddrLen); break; } case arp_opCode: { uint opCode = value.toUInt(&isOk); if (isOk) data.set_op_code(opCode); break; } case arp_senderHwAddr: { quint64 hwAddr = value.toULongLong(&isOk); if (isOk) data.set_sender_hw_addr(hwAddr); break; } case arp_senderHwAddrMode: { uint mode = value.toUInt(&isOk); if (isOk && data.HwAddrMode_IsValid(mode)) data.set_sender_hw_addr_mode((OstProto::Arp::HwAddrMode) mode); else isOk = false; break; } case arp_senderHwAddrCount: { uint count = value.toUInt(&isOk); if (isOk) data.set_sender_hw_addr_count(count); break; } case arp_senderProtoAddr: { uint protoAddr = value.toUInt(&isOk); if (isOk) data.set_sender_proto_addr(protoAddr); break; } case arp_senderProtoAddrMode: { uint mode = value.toUInt(&isOk); if (isOk && data.ProtoAddrMode_IsValid(mode)) data.set_sender_proto_addr_mode( (OstProto::Arp::ProtoAddrMode)mode); else isOk = false; break; } case arp_senderProtoAddrCount: { uint count = value.toUInt(&isOk); if (isOk) data.set_sender_proto_addr_count(count); break; } case arp_senderProtoAddrMask: { uint mask = value.toUInt(&isOk); if (isOk) data.set_sender_proto_addr_mask(mask); break; } case arp_targetHwAddr: { quint64 hwAddr = value.toULongLong(&isOk); if (isOk) data.set_target_hw_addr(hwAddr); break; } case arp_targetHwAddrMode: { uint mode = value.toUInt(&isOk); if (isOk && data.HwAddrMode_IsValid(mode)) data.set_target_hw_addr_mode((OstProto::Arp::HwAddrMode)mode); else isOk = false; break; } case arp_targetHwAddrCount: { uint count = value.toUInt(&isOk); if (isOk) data.set_target_hw_addr_count(count); break; } case arp_targetProtoAddr: { uint protoAddr = value.toUInt(&isOk); if (isOk) data.set_target_proto_addr(protoAddr); break; } case arp_targetProtoAddrMode: { uint mode = value.toUInt(&isOk); if (isOk && data.ProtoAddrMode_IsValid(mode)) data.set_target_proto_addr_mode( (OstProto::Arp::ProtoAddrMode)mode); else isOk = false; break; } case arp_targetProtoAddrCount: { uint count = value.toUInt(&isOk); if (isOk) data.set_target_proto_addr_count(count); break; } case arp_targetProtoAddrMask: { uint mask = value.toUInt(&isOk); if (isOk) data.set_target_proto_addr_mask(mask); break; } default: qFatal("%s: unimplemented case %d in switch", __PRETTY_FUNCTION__, index); break; } _exit: return isOk; } int ArpProtocol::protocolFrameVariableCount() const { int count = AbstractProtocol::protocolFrameVariableCount(); if (fieldData(arp_senderHwAddrMode, FieldValue).toUInt() != uint(OstProto::Arp::kFixed)) { count = AbstractProtocol::lcm(count, fieldData(arp_senderHwAddrCount, FieldValue).toUInt()); } if (fieldData(arp_senderProtoAddrMode, FieldValue).toUInt() != uint(OstProto::Arp::kFixedHost)) { count = AbstractProtocol::lcm(count, fieldData(arp_senderProtoAddrCount, FieldValue).toUInt()); } if (fieldData(arp_targetHwAddrMode, FieldValue).toUInt() != uint(OstProto::Arp::kFixed)) { count = AbstractProtocol::lcm(count, fieldData(arp_targetHwAddrCount, FieldValue).toUInt()); } if (fieldData(arp_targetProtoAddrMode, FieldValue).toUInt() != uint(OstProto::Arp::kFixedHost)) { count = AbstractProtocol::lcm(count, fieldData(arp_targetProtoAddrCount, FieldValue).toUInt()); } return count; } ostinato-1.3.0/common/arp.h000066400000000000000000000053671451413623100156110ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _ARP_H #define _ARP_H #include "abstractprotocol.h" #include "arp.pb.h" /* Arp Protocol Frame Format - +------+------+------+------+------+---------+-------+---------+-------+ | HTYP | PTYP | HLEN | PLEN | OPER | SHA | SPA | THA | TPA | | (2) | (2) | (1) | (1) | (2) | (6) | (4) | (6) | (4) | +------+------+------+------+------+---------+-------+---------+-------+ Figures in brackets represent field width in bytes */ class ArpProtocol : public AbstractProtocol { public: enum arpfield { // Frame Fields arp_hwType, arp_protoType, arp_hwAddrLen, arp_protoAddrLen, arp_opCode, arp_senderHwAddr, arp_senderProtoAddr, arp_targetHwAddr, arp_targetProtoAddr, // Meta Fields arp_senderHwAddrMode, arp_senderHwAddrCount, arp_senderProtoAddrMode, arp_senderProtoAddrCount, arp_senderProtoAddrMask, arp_targetHwAddrMode, arp_targetHwAddrCount, arp_targetProtoAddrMode, arp_targetProtoAddrCount, arp_targetProtoAddrMask, arp_fieldCount }; ArpProtocol(StreamBase *stream, AbstractProtocol *parent = 0); virtual ~ArpProtocol(); static AbstractProtocol* createInstance(StreamBase *stream, AbstractProtocol *parent = 0); virtual quint32 protocolNumber() const; virtual void protoDataCopyInto(OstProto::Protocol &protocol) const; virtual void protoDataCopyFrom(const OstProto::Protocol &protocol); virtual quint32 protocolId(ProtocolIdType type) const; virtual QString name() const; virtual QString shortName() const; virtual int fieldCount() const; virtual AbstractProtocol::FieldFlags fieldFlags(int index) const; virtual QVariant fieldData(int index, FieldAttrib attrib, int streamIndex = 0) const; virtual bool setFieldData(int index, const QVariant &value, FieldAttrib attrib = FieldValue); virtual int protocolFrameVariableCount() const; private: OstProto::Arp data; }; #endif ostinato-1.3.0/common/arp.proto000066400000000000000000000042161451413623100165150ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ import "protocol.proto"; package OstProto; // ARP Protocol message Arp { enum HwAddrMode { kFixed = 0; kIncrement = 1; kDecrement = 2; } enum ProtoAddrMode { kFixedHost = 0; kIncrementHost = 1; kDecrementHost = 2; kRandomHost = 3; } optional uint32 hw_type = 1 [default = 1]; optional uint32 proto_type = 2 [default = 0x800]; optional uint32 hw_addr_len = 3 [default = 6]; optional uint32 proto_addr_len = 4 [default = 4]; optional uint32 op_code = 5 [default = 1]; // 1 => ARP Request optional uint64 sender_hw_addr = 6; optional HwAddrMode sender_hw_addr_mode = 7 [default = kFixed]; optional uint32 sender_hw_addr_count = 8 [default = 16]; optional uint32 sender_proto_addr = 9; optional ProtoAddrMode sender_proto_addr_mode = 10 [default = kFixedHost]; optional uint32 sender_proto_addr_count = 11 [default = 16]; optional fixed32 sender_proto_addr_mask = 12 [default = 0xFFFFFF00]; optional uint64 target_hw_addr = 13; optional HwAddrMode target_hw_addr_mode = 14 [default = kFixed]; optional uint32 target_hw_addr_count = 15 [default = 16]; optional uint32 target_proto_addr = 16; optional ProtoAddrMode target_proto_addr_mode = 17 [default = kFixedHost]; optional uint32 target_proto_addr_count = 18 [default = 16]; optional fixed32 target_proto_addr_mask = 19 [default = 0xFFFFFF00]; } extend Protocol { optional Arp arp = 300; } ostinato-1.3.0/common/arp.ui000066400000000000000000000345271451413623100157770ustar00rootroot00000000000000 Arp 0 0 528 286 Form Hardware Type hwType false Hardware Address Length hwAddrLen false Protocol Type protoType false Protocol Address Length protoAddrLen false Operation Code 1 0 true QComboBox::NoInsert Qt::Horizontal 161 20 false Qt::Horizontal 101 20 Address Mode Count Mask Sender Hardware senderHwAddr >HH HH HH HH HH HH; Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter Fixed Increment Decrement false 255 0 Sender Protocol senderProtoAddr 009.009.009.009; ... Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter Fixed Increment Host Decrement Host Random Host false 255 0 false 009.009.009.009; ... Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter Target Hardware targetHwAddr 120 0 >HH HH HH HH HH HH; Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter Fixed Increment Decrement false 255 0 0 Target Protocol targetProtoAddr 000.000.000.000; ... Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter Fixed Increment Host Decrement Host Random Host false 255 0 false 009.009.009.009; ... Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter Qt::Vertical 20 61 IntComboBox QComboBox
intcombobox.h
hwType protoType hwAddrLen protoAddrLen senderHwAddr senderHwAddrMode senderHwAddrCount senderProtoAddr senderProtoAddrMode senderProtoAddrCount senderProtoAddrMask targetHwAddr targetHwAddrMode targetHwAddrCount targetProtoAddr targetProtoAddrMode targetProtoAddrCount targetProtoAddrMask
ostinato-1.3.0/common/arpconfig.cpp000066400000000000000000000212401451413623100173160ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #include "arpconfig.h" #include "arp.h" #include ArpConfigForm::ArpConfigForm(QWidget *parent) : AbstractProtocolConfigForm(parent) { setupUi(this); opCodeCombo->setValidator(new QIntValidator(0, 0xFFFF, this)); opCodeCombo->addItem(1, "ARP Request"); opCodeCombo->addItem(2, "ARP Reply"); connect(senderHwAddrMode, SIGNAL(currentIndexChanged(int)), SLOT(on_senderHwAddrMode_currentIndexChanged(int))); connect(senderProtoAddrMode, SIGNAL(currentIndexChanged(int)), SLOT(on_senderProtoAddrMode_currentIndexChanged(int))); connect(targetHwAddrMode, SIGNAL(currentIndexChanged(int)), SLOT(on_targetHwAddrMode_currentIndexChanged(int))); connect(targetProtoAddrMode, SIGNAL(currentIndexChanged(int)), SLOT(on_targetProtoAddrMode_currentIndexChanged(int))); } ArpConfigForm::~ArpConfigForm() { } ArpConfigForm* ArpConfigForm::createInstance() { return new ArpConfigForm; } void ArpConfigForm::loadWidget(AbstractProtocol *proto) { hwType->setText( proto->fieldData( ArpProtocol::arp_hwType, AbstractProtocol::FieldValue ).toString()); protoType->setText(uintToHexStr( proto->fieldData( ArpProtocol::arp_protoType, AbstractProtocol::FieldValue ).toUInt(), 2)); hwAddrLen->setText( proto->fieldData( ArpProtocol::arp_hwAddrLen, AbstractProtocol::FieldValue ).toString()); protoAddrLen->setText( proto->fieldData( ArpProtocol::arp_protoAddrLen, AbstractProtocol::FieldValue ).toString()); opCodeCombo->setValue( proto->fieldData( ArpProtocol::arp_opCode, AbstractProtocol::FieldValue ).toUInt()); senderHwAddr->setText(uintToHexStr( proto->fieldData( ArpProtocol::arp_senderHwAddr, AbstractProtocol::FieldValue ).toULongLong(), 6)); senderHwAddrMode->setCurrentIndex( proto->fieldData( ArpProtocol::arp_senderHwAddrMode, AbstractProtocol::FieldValue ).toUInt()); senderHwAddrCount->setText( proto->fieldData( ArpProtocol::arp_senderHwAddrCount, AbstractProtocol::FieldValue ).toString()); senderProtoAddr->setText(QHostAddress( proto->fieldData( ArpProtocol::arp_senderProtoAddr, AbstractProtocol::FieldValue ).toUInt()).toString()); senderProtoAddrMode->setCurrentIndex( proto->fieldData( ArpProtocol::arp_senderProtoAddrMode, AbstractProtocol::FieldValue ).toUInt()); senderProtoAddrCount->setText( proto->fieldData( ArpProtocol::arp_senderProtoAddrCount, AbstractProtocol::FieldValue ).toString()); senderProtoAddrMask->setText(QHostAddress( proto->fieldData( ArpProtocol::arp_senderProtoAddrMask, AbstractProtocol::FieldValue ).toUInt()).toString()); targetHwAddr->setText(uintToHexStr( proto->fieldData( ArpProtocol::arp_targetHwAddr, AbstractProtocol::FieldValue ).toULongLong(), 6)); targetHwAddrMode->setCurrentIndex( proto->fieldData( ArpProtocol::arp_targetHwAddrMode, AbstractProtocol::FieldValue ).toUInt()); targetHwAddrCount->setText( proto->fieldData( ArpProtocol::arp_targetHwAddrCount, AbstractProtocol::FieldValue ).toString()); targetProtoAddr->setText(QHostAddress( proto->fieldData( ArpProtocol::arp_targetProtoAddr, AbstractProtocol::FieldValue ).toUInt()).toString()); targetProtoAddrMode->setCurrentIndex( proto->fieldData( ArpProtocol::arp_targetProtoAddrMode, AbstractProtocol::FieldValue ).toUInt()); targetProtoAddrCount->setText( proto->fieldData( ArpProtocol::arp_targetProtoAddrCount, AbstractProtocol::FieldValue ).toString()); targetProtoAddrMask->setText(QHostAddress( proto->fieldData( ArpProtocol::arp_targetProtoAddrMask, AbstractProtocol::FieldValue ).toUInt()).toString()); } void ArpConfigForm::storeWidget(AbstractProtocol *proto) { proto->setFieldData( ArpProtocol::arp_hwType, hwType->text()); proto->setFieldData( ArpProtocol::arp_protoType, hexStrToUInt(protoType->text())); proto->setFieldData( ArpProtocol::arp_hwAddrLen, hwAddrLen->text()); proto->setFieldData( ArpProtocol::arp_protoAddrLen, protoAddrLen->text()); proto->setFieldData( ArpProtocol::arp_opCode, opCodeCombo->currentValue()); proto->setFieldData( ArpProtocol::arp_senderHwAddr, hexStrToUInt64(senderHwAddr->text())); proto->setFieldData( ArpProtocol::arp_senderHwAddrMode, senderHwAddrMode->currentIndex()); proto->setFieldData( ArpProtocol::arp_senderHwAddrCount, senderHwAddrCount->text()); proto->setFieldData( ArpProtocol::arp_senderProtoAddr, QHostAddress(senderProtoAddr->text()).toIPv4Address()); proto->setFieldData( ArpProtocol::arp_senderProtoAddrMode, senderProtoAddrMode->currentIndex()); proto->setFieldData( ArpProtocol::arp_senderProtoAddrCount, senderProtoAddrCount->text()); proto->setFieldData( ArpProtocol::arp_senderProtoAddrMask, QHostAddress(senderProtoAddrMask->text()).toIPv4Address()); proto->setFieldData( ArpProtocol::arp_targetHwAddr, hexStrToUInt64(targetHwAddr->text())); proto->setFieldData( ArpProtocol::arp_targetHwAddrMode, targetHwAddrMode->currentIndex()); proto->setFieldData( ArpProtocol::arp_targetHwAddrCount, targetHwAddrCount->text()); proto->setFieldData( ArpProtocol::arp_targetProtoAddr, QHostAddress(targetProtoAddr->text()).toIPv4Address()); proto->setFieldData( ArpProtocol::arp_targetProtoAddrMode, targetProtoAddrMode->currentIndex()); proto->setFieldData( ArpProtocol::arp_targetProtoAddrCount, targetProtoAddrCount->text()); proto->setFieldData( ArpProtocol::arp_targetProtoAddrMask, QHostAddress(targetProtoAddrMask->text()).toIPv4Address()); } /* * ------------ Private Slots -------------- */ void ArpConfigForm::on_senderHwAddrMode_currentIndexChanged(int index) { if (index == OstProto::Arp::kFixed) senderHwAddrCount->setDisabled(true); else senderHwAddrCount->setEnabled(true); } void ArpConfigForm::on_targetHwAddrMode_currentIndexChanged(int index) { if (index == OstProto::Arp::kFixed) targetHwAddrCount->setDisabled(true); else targetHwAddrCount->setEnabled(true); } void ArpConfigForm::on_senderProtoAddrMode_currentIndexChanged(int index) { if (index == OstProto::Arp::kFixedHost) { senderProtoAddrCount->setDisabled(true); senderProtoAddrMask->setDisabled(true); } else { senderProtoAddrCount->setEnabled(true); senderProtoAddrMask->setEnabled(true); } } void ArpConfigForm::on_targetProtoAddrMode_currentIndexChanged(int index) { if (index == OstProto::Arp::kFixedHost) { targetProtoAddrCount->setDisabled(true); targetProtoAddrMask->setDisabled(true); } else { targetProtoAddrCount->setEnabled(true); targetProtoAddrMask->setEnabled(true); } } ostinato-1.3.0/common/arpconfig.h000066400000000000000000000025461451413623100167730ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _ARP_CONFIG_H #define _ARP_CONFIG_H #include "abstractprotocolconfig.h" #include "ui_arp.h" class ArpConfigForm : public AbstractProtocolConfigForm, private Ui::Arp { Q_OBJECT public: ArpConfigForm(QWidget *parent = 0); virtual ~ArpConfigForm(); static ArpConfigForm* createInstance(); virtual void loadWidget(AbstractProtocol *proto); virtual void storeWidget(AbstractProtocol *proto); private slots: void on_senderHwAddrMode_currentIndexChanged(int index); void on_senderProtoAddrMode_currentIndexChanged(int index); void on_targetHwAddrMode_currentIndexChanged(int index); void on_targetProtoAddrMode_currentIndexChanged(int index); }; #endif ostinato-1.3.0/common/arppdml.cpp000066400000000000000000000024671451413623100170170ustar00rootroot00000000000000/* Copyright (C) 2011 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #include "arppdml.h" #include "arp.pb.h" PdmlArpProtocol::PdmlArpProtocol() { ostProtoId_ = OstProto::Protocol::kArpFieldNumber; fieldMap_.insert("arp.opcode", OstProto::Arp::kOpCodeFieldNumber); fieldMap_.insert("arp.src.hw_mac", OstProto::Arp::kSenderHwAddrFieldNumber); fieldMap_.insert("arp.src.proto_ipv4", OstProto::Arp::kSenderProtoAddrFieldNumber); fieldMap_.insert("arp.dst.hw_mac", OstProto::Arp::kTargetHwAddrFieldNumber); fieldMap_.insert("arp.dst.proto_ipv4", OstProto::Arp::kTargetProtoAddrFieldNumber); } PdmlProtocol* PdmlArpProtocol::createInstance() { return new PdmlArpProtocol(); } ostinato-1.3.0/common/arppdml.h000066400000000000000000000015651451413623100164620ustar00rootroot00000000000000/* Copyright (C) 2011 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _ARP_PDML_H #define _ARP_PDML_H #include "pdmlprotocol.h" class PdmlArpProtocol : public PdmlProtocol { public: static PdmlProtocol* createInstance(); protected: PdmlArpProtocol(); }; #endif ostinato-1.3.0/common/bswap.h000066400000000000000000000020621451413623100161300ustar00rootroot00000000000000/* Copyright (C) 2010-2016 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _B_SWAP_H #define _B_SWAP_H #include static inline quint32 swap32(quint32 val) { return (((val >> 24) & 0x000000FF) | ((val >> 16) & 0x0000FF00) | ((val << 16) & 0x00FF0000) | ((val << 24) & 0xFF000000)); } static inline quint16 swap16(quint16 val) { return (((val >> 8) & 0x00FF) | ((val << 8) & 0xFF00)); } #endif ostinato-1.3.0/common/comboprotocol.h000066400000000000000000000162611451413623100177030ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _COMBO_PROTOCOL_H #define _COMBO_PROTOCOL_H #include "abstractprotocol.h" template class ComboProtocol : public AbstractProtocol { public: ComboProtocol(StreamBase *stream, AbstractProtocol *parent = 0) : AbstractProtocol(stream, parent) { protoA = new ProtoA(stream, this); protoB = new ProtoB(stream, this); protoA->next = protoB; protoB->prev = protoA; qDebug("%s: protoNumber = %d, %p <--> %p", __FUNCTION__, protoNumber, protoA, protoB); if (protoA->protocolNumber() == protoB->protocolNumber()) fieldPrefix = OuterInnerPrefix; else if (similarProto()) fieldPrefix = ProtoNamePrefix; } virtual ~ComboProtocol() { delete protoA; delete protoB; } static ComboProtocol* createInstance(StreamBase *stream, AbstractProtocol *parent) { return new ComboProtocol(stream, parent); } virtual quint32 protocolNumber() const { return protoNumber; } virtual void protoDataCopyInto(OstProto::Protocol &protocol) const { protoA->protoDataCopyInto(protocol); protoB->protoDataCopyInto(protocol); protocol.mutable_protocol_id()->set_id(protocolNumber()); } virtual void protoDataCopyFrom(const OstProto::Protocol &protocol) { if (protocol.protocol_id().id() == protocolNumber()) { OstProto::Protocol proto; // NOTE: To use protoX->protoDataCopyFrom() we need to arrange // so that it sees its own protocolNumber() - but since the // input param 'protocol' is 'const', we work on a copy proto.CopyFrom(protocol); proto.mutable_protocol_id()->set_id(protoA->protocolNumber()); protoA->protoDataCopyFrom(proto); proto.mutable_protocol_id()->set_id(protoB->protocolNumber()); protoB->protoDataCopyFrom(proto); } } virtual QString name() const { return protoA->name() + "/" + protoB->name(); } virtual QString shortName() const { return protoA->shortName() + "/" + protoB->shortName(); } virtual ProtocolIdType protocolIdType() const { return protoB->protocolIdType(); } virtual quint32 protocolId(ProtocolIdType type) const { return protoA->protocolId(type); } //quint32 payloadProtocolId(ProtocolIdType type) const; virtual int fieldCount() const { return protoA->fieldCount() + protoB->fieldCount(); } //virtual int metaFieldCount() const; //int frameFieldCount() const; virtual FieldFlags fieldFlags(int index) const { int cnt = protoA->fieldCount(); if (index < cnt) return protoA->fieldFlags(index); else return protoB->fieldFlags(index - cnt); } virtual QVariant fieldData(int index, FieldAttrib attrib, int streamIndex = 0) const { int cnt = protoA->fieldCount(); QVariant value = index < cnt ? protoA->fieldData(index, attrib, streamIndex) : protoB->fieldData(index - cnt, attrib, streamIndex); if (attrib == FieldName) { switch (fieldPrefix) { case OuterInnerPrefix: value = QString("%1 %2") .arg(index < cnt ? QString("Outer") : QString("Inner")) .arg(value.toString()); break; case ProtoNamePrefix: value = QString("%1 %2") .arg(index < cnt ? protoA->shortName() : protoB->shortName()) .arg(value.toString()); break; case NoPrefix: // Fall-through break; } } return value; } virtual bool setFieldData(int index, const QVariant &value, FieldAttrib attrib = FieldValue) { int cnt = protoA->fieldCount(); if (index < cnt) return protoA->setFieldData(index, value, attrib); else return protoB->setFieldData(index - cnt, value, attrib); } #if 0 QByteArray protocolFrameValue(int streamIndex = 0, bool forCksum = false) const; virtual int protocolFrameSize() const; int protocolFrameOffset() const; int protocolFramePayloadSize() const; #endif virtual bool isProtocolFrameSizeVariable() const { return (protoA->isProtocolFrameSizeVariable() || protoB->isProtocolFrameSizeVariable()); } virtual int protocolFrameVariableCount() const { int count = AbstractProtocol::protocolFrameVariableCount(); count = AbstractProtocol::lcm( count, protoA->protocolFrameVariableCount()); count = AbstractProtocol::lcm( count, protoB->protocolFrameVariableCount()); return count; } virtual quint32 protocolFrameCksum(int streamIndex = 0, CksumType cksumType = CksumIp, CksumFlags cksumFlags = 0) const { // For a Pseudo IP cksum, we assume it is the succeeding protocol // that is requesting it and hence return protoB's cksum; if (cksumType == CksumIpPseudo) return protoB->protocolFrameCksum( streamIndex,cksumType, cksumFlags); return AbstractProtocol::protocolFrameCksum( streamIndex, cksumType, cksumFlags); } #if 0 quint32 protocolFrameHeaderCksum(int streamIndex = 0, CksumType cksumType = CksumIp) const; quint32 protocolFramePayloadCksum(int streamIndex = 0, CksumType cksumType = CksumIp) const; #endif template friend class ComboProtocolConfigForm; protected: ProtoA *protoA; ProtoB *protoB; private: bool similarProto() { // TODO: Use Levenshtein distance or something similar with > 70% match // For now we use an ugly hack! if (protoA->shortName().contains("IPv") && protoB->shortName().contains("IPv")) return true; if (protoA->shortName().contains("Vlan") && protoB->shortName().contains("Vlan")) return true; return false; } enum FieldNamePrefix { NoPrefix, ProtoNamePrefix, OuterInnerPrefix }; FieldNamePrefix fieldPrefix{NoPrefix}; }; #endif ostinato-1.3.0/common/comboprotocolconfig.h000066400000000000000000000060131451413623100210630ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _COMBO_PROTOCOL_CONFIG_H #define _COMBO_PROTOCOL_CONFIG_H #include "abstractprotocolconfig.h" #include "comboprotocol.h" template class ComboProtocolConfigForm : public AbstractProtocolConfigForm { public: ComboProtocolConfigForm(QWidget *parent = 0) : AbstractProtocolConfigForm(parent) { QVBoxLayout *layout = new QVBoxLayout; formA = new FormA(this); formB = new FormB(this); layout->addWidget(formA); layout->addWidget(formB); layout->setSpacing(0); layout->setContentsMargins(0, 0, 0, 0); setLayout(layout); qDebug("%s: protoNumber = %d, %p <--> %p", __FUNCTION__, protoNumber, formA, formB); } virtual ~ComboProtocolConfigForm() { formA->setParent(0); formB->setParent(0); delete formA; delete formB; } static ComboProtocolConfigForm* createInstance() { return new ComboProtocolConfigForm; } virtual void loadWidget(AbstractProtocol *proto) { class ComboProtocol *comboProto = dynamic_cast*>(proto); Q_ASSERT_X(comboProto != NULL, qPrintable(tr("ComboProtocolConfigForm{%1}::loadWidget()") .arg(protoNumber)), qPrintable(tr("Protocol{%1} is not a instance of ComboProtocol") .arg(proto->protocolNumber()))); formA->loadWidget(comboProto->protoA); formB->loadWidget(comboProto->protoB); } virtual void storeWidget(AbstractProtocol *proto) { class ComboProtocol *comboProto = dynamic_cast*>(proto); Q_ASSERT_X(comboProto != NULL, qPrintable(tr("ComboProtocolConfigForm{%1}::loadWidget()") .arg(protoNumber)), qPrintable(tr("Protocol{%1} is not a instance of ComboProtocol") .arg(proto->protocolNumber()))); formA->storeWidget(comboProto->protoA); formB->storeWidget(comboProto->protoB); } protected: FormA *formA; FormB *formB; }; #endif ostinato-1.3.0/common/crc32c.cpp000066400000000000000000000134231451413623100164310ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ /******************************************************************* ** IMPORTANT NOTE: ** This code is from RFC 4960 Stream Control Transmission Protocol ** It has been modified suitably while keeping the algorithm intact. ********************************************************************/ #include "crc32c.h" #define CRC32C(c,d) (c=(c>>8)^crc_c[(c^(d))&0xFF]) quint32 crc_c[256] = { 0x00000000L, 0xF26B8303L, 0xE13B70F7L, 0x1350F3F4L, 0xC79A971FL, 0x35F1141CL, 0x26A1E7E8L, 0xD4CA64EBL, 0x8AD958CFL, 0x78B2DBCCL, 0x6BE22838L, 0x9989AB3BL, 0x4D43CFD0L, 0xBF284CD3L, 0xAC78BF27L, 0x5E133C24L, 0x105EC76FL, 0xE235446CL, 0xF165B798L, 0x030E349BL, 0xD7C45070L, 0x25AFD373L, 0x36FF2087L, 0xC494A384L, 0x9A879FA0L, 0x68EC1CA3L, 0x7BBCEF57L, 0x89D76C54L, 0x5D1D08BFL, 0xAF768BBCL, 0xBC267848L, 0x4E4DFB4BL, 0x20BD8EDEL, 0xD2D60DDDL, 0xC186FE29L, 0x33ED7D2AL, 0xE72719C1L, 0x154C9AC2L, 0x061C6936L, 0xF477EA35L, 0xAA64D611L, 0x580F5512L, 0x4B5FA6E6L, 0xB93425E5L, 0x6DFE410EL, 0x9F95C20DL, 0x8CC531F9L, 0x7EAEB2FAL, 0x30E349B1L, 0xC288CAB2L, 0xD1D83946L, 0x23B3BA45L, 0xF779DEAEL, 0x05125DADL, 0x1642AE59L, 0xE4292D5AL, 0xBA3A117EL, 0x4851927DL, 0x5B016189L, 0xA96AE28AL, 0x7DA08661L, 0x8FCB0562L, 0x9C9BF696L, 0x6EF07595L, 0x417B1DBCL, 0xB3109EBFL, 0xA0406D4BL, 0x522BEE48L, 0x86E18AA3L, 0x748A09A0L, 0x67DAFA54L, 0x95B17957L, 0xCBA24573L, 0x39C9C670L, 0x2A993584L, 0xD8F2B687L, 0x0C38D26CL, 0xFE53516FL, 0xED03A29BL, 0x1F682198L, 0x5125DAD3L, 0xA34E59D0L, 0xB01EAA24L, 0x42752927L, 0x96BF4DCCL, 0x64D4CECFL, 0x77843D3BL, 0x85EFBE38L, 0xDBFC821CL, 0x2997011FL, 0x3AC7F2EBL, 0xC8AC71E8L, 0x1C661503L, 0xEE0D9600L, 0xFD5D65F4L, 0x0F36E6F7L, 0x61C69362L, 0x93AD1061L, 0x80FDE395L, 0x72966096L, 0xA65C047DL, 0x5437877EL, 0x4767748AL, 0xB50CF789L, 0xEB1FCBADL, 0x197448AEL, 0x0A24BB5AL, 0xF84F3859L, 0x2C855CB2L, 0xDEEEDFB1L, 0xCDBE2C45L, 0x3FD5AF46L, 0x7198540DL, 0x83F3D70EL, 0x90A324FAL, 0x62C8A7F9L, 0xB602C312L, 0x44694011L, 0x5739B3E5L, 0xA55230E6L, 0xFB410CC2L, 0x092A8FC1L, 0x1A7A7C35L, 0xE811FF36L, 0x3CDB9BDDL, 0xCEB018DEL, 0xDDE0EB2AL, 0x2F8B6829L, 0x82F63B78L, 0x709DB87BL, 0x63CD4B8FL, 0x91A6C88CL, 0x456CAC67L, 0xB7072F64L, 0xA457DC90L, 0x563C5F93L, 0x082F63B7L, 0xFA44E0B4L, 0xE9141340L, 0x1B7F9043L, 0xCFB5F4A8L, 0x3DDE77ABL, 0x2E8E845FL, 0xDCE5075CL, 0x92A8FC17L, 0x60C37F14L, 0x73938CE0L, 0x81F80FE3L, 0x55326B08L, 0xA759E80BL, 0xB4091BFFL, 0x466298FCL, 0x1871A4D8L, 0xEA1A27DBL, 0xF94AD42FL, 0x0B21572CL, 0xDFEB33C7L, 0x2D80B0C4L, 0x3ED04330L, 0xCCBBC033L, 0xA24BB5A6L, 0x502036A5L, 0x4370C551L, 0xB11B4652L, 0x65D122B9L, 0x97BAA1BAL, 0x84EA524EL, 0x7681D14DL, 0x2892ED69L, 0xDAF96E6AL, 0xC9A99D9EL, 0x3BC21E9DL, 0xEF087A76L, 0x1D63F975L, 0x0E330A81L, 0xFC588982L, 0xB21572C9L, 0x407EF1CAL, 0x532E023EL, 0xA145813DL, 0x758FE5D6L, 0x87E466D5L, 0x94B49521L, 0x66DF1622L, 0x38CC2A06L, 0xCAA7A905L, 0xD9F75AF1L, 0x2B9CD9F2L, 0xFF56BD19L, 0x0D3D3E1AL, 0x1E6DCDEEL, 0xEC064EEDL, 0xC38D26C4L, 0x31E6A5C7L, 0x22B65633L, 0xD0DDD530L, 0x0417B1DBL, 0xF67C32D8L, 0xE52CC12CL, 0x1747422FL, 0x49547E0BL, 0xBB3FFD08L, 0xA86F0EFCL, 0x5A048DFFL, 0x8ECEE914L, 0x7CA56A17L, 0x6FF599E3L, 0x9D9E1AE0L, 0xD3D3E1ABL, 0x21B862A8L, 0x32E8915CL, 0xC083125FL, 0x144976B4L, 0xE622F5B7L, 0xF5720643L, 0x07198540L, 0x590AB964L, 0xAB613A67L, 0xB831C993L, 0x4A5A4A90L, 0x9E902E7BL, 0x6CFBAD78L, 0x7FAB5E8CL, 0x8DC0DD8FL, 0xE330A81AL, 0x115B2B19L, 0x020BD8EDL, 0xF0605BEEL, 0x24AA3F05L, 0xD6C1BC06L, 0xC5914FF2L, 0x37FACCF1L, 0x69E9F0D5L, 0x9B8273D6L, 0x88D28022L, 0x7AB90321L, 0xAE7367CAL, 0x5C18E4C9L, 0x4F48173DL, 0xBD23943EL, 0xF36E6F75L, 0x0105EC76L, 0x12551F82L, 0xE03E9C81L, 0x34F4F86AL, 0xC69F7B69L, 0xD5CF889DL, 0x27A40B9EL, 0x79B737BAL, 0x8BDCB4B9L, 0x988C474DL, 0x6AE7C44EL, 0xBE2DA0A5L, 0x4C4623A6L, 0x5F16D052L, 0xAD7D5351L, }; quint32 checksumCrc32C(quint8 *buffer, uint length) { uint i; quint32 crc32 = ~0L; quint32 result; quint8 byte0,byte1,byte2,byte3; for (i = 0; i < length; i++) { CRC32C(crc32, buffer[i]); } result = ~crc32; /* result now holds the negated polynomial remainder; * since the table and algorithm is "reflected" [williams95]. * That is, result has the same value as if we mapped the message * to a polynomial, computed the host-bit-order polynomial * remainder, performed final negation, then did an end-for-end * bit-reversal. * Note that a 32-bit bit-reversal is identical to four inplace * 8-bit reversals followed by an end-for-end byteswap. * In other words, the bytes of each bit are in the right order, * but the bytes have been byteswapped. So we now do an explicit * byteswap. On a little-endian machine, this byteswap and * the final ntohl cancel out and could be elided. */ byte0 = result & 0xff; byte1 = (result>>8) & 0xff; byte2 = (result>>16) & 0xff; byte3 = (result>>24) & 0xff; crc32 = ((byte0 << 24) | (byte1 << 16) | (byte2 << 8) | byte3); return ( crc32 ); } ostinato-1.3.0/common/crc32c.h000066400000000000000000000013551451413623100160770ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #include quint32 checksumCrc32C(quint8 *buffer, uint length); ostinato-1.3.0/common/debugdefs.h000066400000000000000000000015041451413623100167440ustar00rootroot00000000000000/* Copyright (C) 2023 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _DEBUG_DEFS_H #define _DEBUG_DEFS_H #if 0 #define timingDebug(fmt, ...) qDebug("TIMING:" fmt, __VA_ARGS__) #else #define timingDebug(...) #endif #endif ostinato-1.3.0/common/dot2llc.h000066400000000000000000000016021451413623100163560ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _DOT2_LLC_H #define _DOT2_LLC_H #include "comboprotocol.h" #include "dot3.h" #include "llc.h" typedef ComboProtocol Dot2LlcProtocol; #endif ostinato-1.3.0/common/dot2llc.proto000066400000000000000000000015321451413623100172740ustar00rootroot00000000000000/// (802.2 LLC) /* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ import "protocol.proto"; package OstProto; message Dot2Llc { // Empty since this is a 'combo' protocol } extend Protocol { optional Dot2Llc dot2Llc = 206; } ostinato-1.3.0/common/dot2llcconfig.h000066400000000000000000000020141451413623100175420ustar00rootroot00000000000000/* Copyright (C) 2014 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _DOT2_LLC_CONFIG_H #define _DOT2_LLC_CONFIG_H #include "comboprotocolconfig.h" #include "dot3config.h" #include "llcconfig.h" #include "dot3.h" #include "llc.h" typedef ComboProtocolConfigForm < OstProto::Protocol::kDot2LlcFieldNumber, Dot3ConfigForm, LlcConfigForm, Dot3Protocol, LlcProtocol > Dot2LlcConfigForm; #endif ostinato-1.3.0/common/dot2snap.h000066400000000000000000000016161451413623100165520ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _DOT2_SNAP_H #define _DOT2_SNAP_H #include "comboprotocol.h" #include "dot2llc.h" #include "snap.h" typedef ComboProtocol Dot2SnapProtocol; #endif ostinato-1.3.0/common/dot2snap.proto000066400000000000000000000015541451413623100174670ustar00rootroot00000000000000/// (802.2 SNAP) /* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ import "protocol.proto"; package OstProto; // 802.2 SNAP message Dot2Snap { // Empty since this is a 'combo' protocol } extend Protocol { optional Dot2Snap dot2Snap = 207; } ostinato-1.3.0/common/dot2snapconfig.h000066400000000000000000000020321451413623100177310ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _DOT2_SNAP_CONFIG_H #define _DOT2_SNAP_CONFIG_H #include "comboprotocol.h" #include "dot2llcconfig.h" #include "snapconfig.h" #include "dot2llc.h" #include "snap.h" typedef ComboProtocolConfigForm < OstProto::Protocol::kDot2SnapFieldNumber, Dot2LlcConfigForm, SnapConfigForm, Dot2LlcProtocol, SnapProtocol > Dot2SnapConfigForm; #endif ostinato-1.3.0/common/dot3.cpp000066400000000000000000000115441451413623100162250ustar00rootroot00000000000000/* Copyright (C) 2010-2014 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #include "dot3.h" Dot3Protocol::Dot3Protocol(StreamBase *stream, AbstractProtocol *parent) : AbstractProtocol(stream, parent) { } Dot3Protocol::~Dot3Protocol() { } AbstractProtocol* Dot3Protocol::createInstance(StreamBase *stream, AbstractProtocol *parent) { return new Dot3Protocol(stream, parent); } quint32 Dot3Protocol::protocolNumber() const { return OstProto::Protocol::kDot3FieldNumber; } void Dot3Protocol::protoDataCopyInto(OstProto::Protocol &protocol) const { protocol.MutableExtension(OstProto::dot3)->CopyFrom(data); protocol.mutable_protocol_id()->set_id(protocolNumber()); } void Dot3Protocol::protoDataCopyFrom(const OstProto::Protocol &protocol) { if (protocol.protocol_id().id() == protocolNumber() && protocol.HasExtension(OstProto::dot3)) data.MergeFrom(protocol.GetExtension(OstProto::dot3)); } QString Dot3Protocol::name() const { return QString("802.3"); } QString Dot3Protocol::shortName() const { return QString("802.3"); } int Dot3Protocol::fieldCount() const { return dot3_fieldCount; } AbstractProtocol::FieldFlags Dot3Protocol::fieldFlags(int index) const { AbstractProtocol::FieldFlags flags; flags = AbstractProtocol::fieldFlags(index); switch (index) { case dot3_length: break; // Meta fields case dot3_is_override_length: flags &= ~FrameField; flags |= MetaField; break; default: break; } return flags; } QVariant Dot3Protocol::fieldData(int index, FieldAttrib attrib, int streamIndex) const { switch (index) { case dot3_length: switch(attrib) { case FieldName: return QString("Length"); case FieldValue: { quint16 len = data.is_override_length() ? data.length() : protocolFramePayloadSize(streamIndex); return len; } case FieldTextValue: { quint16 len = data.is_override_length() ? data.length() : protocolFramePayloadSize(streamIndex); return QString("%1").arg(len); } case FieldFrameValue: { quint16 len = data.is_override_length() ? data.length() : protocolFramePayloadSize(streamIndex); QByteArray fv; fv.resize(2); qToBigEndian(len, (uchar*) fv.data()); return fv; } case FieldBitSize: return 16; default: break; } break; // Meta fields case dot3_is_override_length: { switch(attrib) { case FieldValue: return data.is_override_length(); default: break; } break; } default: qFatal("%s: unimplemented case %d in switch", __PRETTY_FUNCTION__, index); break; } return AbstractProtocol::fieldData(index, attrib, streamIndex); } bool Dot3Protocol::setFieldData(int index, const QVariant &value, FieldAttrib attrib) { bool isOk = false; if (attrib != FieldValue) return false; switch (index) { case dot3_length: { uint len = value.toUInt(&isOk); if (isOk) data.set_length(len); break; } case dot3_is_override_length: { bool ovr = value.toBool(); data.set_is_override_length(ovr); isOk = true; break; } default: qFatal("%s: unimplemented case %d in switch", __PRETTY_FUNCTION__, index); break; } return isOk; } int Dot3Protocol::protocolFrameVariableCount() const { return AbstractProtocol::lcm( AbstractProtocol::protocolFrameVariableCount(), protocolFramePayloadVariableCount()); } ostinato-1.3.0/common/dot3.h000066400000000000000000000034701451413623100156710ustar00rootroot00000000000000/* Copyright (C) 2010-2014 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _DOT3_H #define _DOT3_H #include "abstractprotocol.h" #include "dot3.pb.h" class Dot3Protocol : public AbstractProtocol { public: enum Dot3field { dot3_length, // Meta-fields dot3_is_override_length, dot3_fieldCount }; Dot3Protocol(StreamBase *stream, AbstractProtocol *parent = 0); virtual ~Dot3Protocol(); static AbstractProtocol* createInstance(StreamBase *stream, AbstractProtocol *parent = 0); virtual quint32 protocolNumber() const; virtual void protoDataCopyInto(OstProto::Protocol &protocol) const; virtual void protoDataCopyFrom(const OstProto::Protocol &protocol); virtual QString name() const; virtual QString shortName() const; virtual int fieldCount() const; virtual AbstractProtocol::FieldFlags fieldFlags(int index) const; virtual QVariant fieldData(int index, FieldAttrib attrib, int streamIndex = 0) const; virtual bool setFieldData(int index, const QVariant &value, FieldAttrib attrib = FieldValue); virtual int protocolFrameVariableCount() const; private: OstProto::Dot3 data; }; #endif ostinato-1.3.0/common/dot3.proto000066400000000000000000000015621451413623100166050ustar00rootroot00000000000000/// (802.3) /* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ import "protocol.proto"; package OstProto; // 802.3 message Dot3 { optional bool is_override_length = 2; optional uint32 length = 1; } extend Protocol { optional Dot3 dot3 = 201; } ostinato-1.3.0/common/dot3.ui000066400000000000000000000035531451413623100160610ustar00rootroot00000000000000 dot3 0 0 181 98 Form 802.3 Length false Qt::Horizontal 16 54 Qt::Vertical 20 40 cbOverrideLength toggled(bool) leLength setEnabled(bool) 55 39 84 43 ostinato-1.3.0/common/dot3config.cpp000066400000000000000000000032411451413623100174060ustar00rootroot00000000000000/* Copyright (C) 2010-2014 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #include "dot3config.h" #include "dot3.h" #include Dot3ConfigForm::Dot3ConfigForm(QWidget *parent) : AbstractProtocolConfigForm(parent) { setupUi(this); leLength->setValidator(new QIntValidator(0, 16384, this)); } Dot3ConfigForm::~Dot3ConfigForm() { } Dot3ConfigForm* Dot3ConfigForm::createInstance() { return new Dot3ConfigForm; } void Dot3ConfigForm::loadWidget(AbstractProtocol *proto) { cbOverrideLength->setChecked( proto->fieldData( Dot3Protocol::dot3_is_override_length, AbstractProtocol::FieldValue ).toBool()); leLength->setText( proto->fieldData( Dot3Protocol::dot3_length, AbstractProtocol::FieldValue ).toString()); } void Dot3ConfigForm::storeWidget(AbstractProtocol *proto) { proto->setFieldData( Dot3Protocol::dot3_is_override_length, cbOverrideLength->isChecked()); proto->setFieldData( Dot3Protocol::dot3_length, leLength->text()); } ostinato-1.3.0/common/dot3config.h000066400000000000000000000021531451413623100170540ustar00rootroot00000000000000/* Copyright (C) 2010-2014 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _DOT3_CONFIG_H #define _DOT3_CONFIG_H #include "abstractprotocolconfig.h" #include "ui_dot3.h" class Dot3ConfigForm : public AbstractProtocolConfigForm, private Ui::dot3 { Q_OBJECT public: Dot3ConfigForm(QWidget *parent = 0); virtual ~Dot3ConfigForm(); static Dot3ConfigForm* createInstance(); virtual void loadWidget(AbstractProtocol *proto); virtual void storeWidget(AbstractProtocol *proto); }; #endif ostinato-1.3.0/common/emulation.h000066400000000000000000000034261451413623100170160ustar00rootroot00000000000000/* Copyright (C) 2016 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _EMULATION_H #define _EMULATION_H #include "emulproto.pb.h" #include static inline OstProto::DeviceGroup* newDeviceGroup(uint portId) { OstProto::DeviceGroup *devGrp = new OstProto::DeviceGroup; // To ensure that DeviceGroup have a unique key, we assign // a random mac address upon creation; ideally, it would // have been good to inherit OstProto::DeviceGroup and define // a new constructor, but ProtoBuf forbids against inheriting // generated classes, so we use this helper function instead // // Create a mac address as per RFC 4814 Sec 4.2 // (RR & 0xFC):PP:PP:RR:RR:RR // where RR is a random number, PP:PP is 1-indexed port index // NOTE: although qrand() return type is a int, the max value // is RAND_MAX (stdlib.h) which is often 16-bit only, so we // use two random numbers quint32 r1 = qrand(), r2 = qrand(); quint64 mac; mac = quint64(r1 & 0xfc00) << 32 | quint64(portId + 1) << 24 | quint64((r1 & 0xff) << 16 | (r2 & 0xffff)); devGrp->MutableExtension(OstEmul::mac)->set_address(mac); return devGrp; } #endif ostinato-1.3.0/common/emulproto.proto000066400000000000000000000057701451413623100177670ustar00rootroot00000000000000/* Copyright (C) 2015 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ import "protocol.proto"; package OstEmul; // ======= // Encap // ======= message VlanEmulation { message Vlan { optional uint32 tpid = 1 [default = 0x8100]; // includes prio, cfi and vlanid optional uint32 vlan_tag = 2 [default = 100]; optional uint32 count = 10 [default = 1]; optional uint32 step = 11 [default = 1]; } repeated Vlan stack = 1; // outer to inner } extend OstProto.EncapEmulation { optional VlanEmulation vlan = 1000; } // =========== // Protocols // =========== message MacEmulation { optional uint64 address = 1; // no default - need unique value optional uint64 step = 10 [default = 1]; } // No default values for IP addresses - user needs to explicitly set that // 'coz we derive if a device has a single/dual or no IP stack on the basis // of whether OstProto.DeviceGroup.ip[46] is set message Ip4Emulation { optional uint32 address = 1; optional uint32 prefix_length = 2 [default = 24]; optional uint32 default_gateway = 3; optional uint32 step = 10 [default = 1]; // FIXME: step for gateway? } message Ip6Address { optional uint64 hi = 1; optional uint64 lo = 2; } message Ip6Emulation { optional Ip6Address address = 1; optional uint32 prefix_length = 2 [default = 64]; optional Ip6Address default_gateway = 3; optional Ip6Address step = 10; // FIXME: step for gateway? } extend OstProto.DeviceGroup { optional MacEmulation mac = 2001; optional Ip4Emulation ip4 = 3000; optional Ip6Emulation ip6 = 3001; } message Device { optional uint64 mac = 1; repeated uint32 vlan = 2; // includes tpid 'n vlan tag optional uint32 ip4 = 10; optional uint32 ip4_prefix_length = 11; optional uint32 ip4_default_gateway = 12; optional Ip6Address ip6 = 20; optional uint32 ip6_prefix_length = 21; optional Ip6Address ip6_default_gateway = 22; } extend OstProto.PortDeviceList { repeated Device device = 100; } message ArpEntry { optional uint32 ip4 = 1; optional uint64 mac = 2; } message NdpEntry { optional Ip6Address ip6 = 1; optional uint64 mac = 2; } message DeviceNeighborList { optional uint32 device_index = 1; repeated ArpEntry arp = 2; repeated NdpEntry ndp = 3; } extend OstProto.PortNeighborList { repeated DeviceNeighborList device_neighbor = 100; } ostinato-1.3.0/common/eth2.cpp000066400000000000000000000112631451413623100162140ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #include "eth2.h" Eth2Protocol::Eth2Protocol(StreamBase *stream, AbstractProtocol *parent) : AbstractProtocol(stream, parent) { } Eth2Protocol::~Eth2Protocol() { } AbstractProtocol* Eth2Protocol::createInstance(StreamBase *stream, AbstractProtocol *parent) { return new Eth2Protocol(stream, parent); } quint32 Eth2Protocol::protocolNumber() const { return OstProto::Protocol::kEth2FieldNumber; } void Eth2Protocol::protoDataCopyInto(OstProto::Protocol &protocol) const { protocol.MutableExtension(OstProto::eth2)->CopyFrom(data); protocol.mutable_protocol_id()->set_id(protocolNumber()); } void Eth2Protocol::protoDataCopyFrom(const OstProto::Protocol &protocol) { if (protocol.protocol_id().id() == protocolNumber() && protocol.HasExtension(OstProto::eth2)) data.MergeFrom(protocol.GetExtension(OstProto::eth2)); } QString Eth2Protocol::name() const { return QString("Ethernet II"); } QString Eth2Protocol::shortName() const { return QString("Eth II"); } AbstractProtocol::ProtocolIdType Eth2Protocol::protocolIdType() const { return ProtocolIdEth; } int Eth2Protocol::fieldCount() const { return eth2_fieldCount; } AbstractProtocol::FieldFlags Eth2Protocol::fieldFlags(int index) const { AbstractProtocol::FieldFlags flags; flags = AbstractProtocol::fieldFlags(index); switch (index) { case eth2_type: break; case eth2_is_override_type: flags &= ~FrameField; flags |= MetaField; break; default: break; } return flags; } QVariant Eth2Protocol::fieldData(int index, FieldAttrib attrib, int streamIndex) const { switch (index) { case eth2_type: { switch(attrib) { case FieldName: return QString("Type"); case FieldValue: { quint16 type = data.is_override_type() ? data.type() : payloadProtocolId(ProtocolIdEth); return type; } case FieldTextValue: { quint16 type = data.is_override_type() ? data.type() : payloadProtocolId(ProtocolIdEth); return QString("0x%1").arg(type, 4, BASE_HEX, QChar('0')); } case FieldFrameValue: { QByteArray fv; quint16 type = data.is_override_type() ? data.type() : payloadProtocolId(ProtocolIdEth); fv.resize(2); qToBigEndian((quint16) type, (uchar*) fv.data()); return fv; } default: break; } break; } // Meta fields case eth2_is_override_type: { switch(attrib) { case FieldValue: return data.is_override_type(); default: break; } break; } default: qFatal("%s: unimplemented case %d in switch", __PRETTY_FUNCTION__, index); break; } return AbstractProtocol::fieldData(index, attrib, streamIndex); } bool Eth2Protocol::setFieldData(int index, const QVariant &value, FieldAttrib attrib) { bool isOk = false; if (attrib != FieldValue) return false; switch (index) { case eth2_type: { uint type = value.toUInt(&isOk); if (isOk) data.set_type(type); break; } case eth2_is_override_type: { bool ovr = value.toBool(); data.set_is_override_type(ovr); isOk = true; break; } default: qFatal("%s: unimplemented case %d in switch", __PRETTY_FUNCTION__, index); break; } return isOk; } ostinato-1.3.0/common/eth2.h000066400000000000000000000034311451413623100156570ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _ETH2_H #define _ETH2_H #include "abstractprotocol.h" #include "eth2.pb.h" class Eth2Protocol : public AbstractProtocol { public: enum eth2field { eth2_type = 0, eth2_is_override_type, eth2_fieldCount }; Eth2Protocol(StreamBase *stream, AbstractProtocol *parent = 0); virtual ~Eth2Protocol(); static AbstractProtocol* createInstance(StreamBase *stream, AbstractProtocol *parent = 0); virtual quint32 protocolNumber() const; virtual void protoDataCopyInto(OstProto::Protocol &protocol) const; virtual void protoDataCopyFrom(const OstProto::Protocol &protocol); virtual QString name() const; virtual QString shortName() const; virtual ProtocolIdType protocolIdType() const; virtual int fieldCount() const; virtual AbstractProtocol::FieldFlags fieldFlags(int index) const; virtual QVariant fieldData(int index, FieldAttrib attrib, int streamIndex = 0) const; virtual bool setFieldData(int index, const QVariant &value, FieldAttrib attrib = FieldValue); private: OstProto::Eth2 data; }; #endif ostinato-1.3.0/common/eth2.proto000066400000000000000000000015511451413623100165740ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ import "protocol.proto"; package OstProto; // Ethernet II message Eth2 { optional bool is_override_type = 2; optional uint32 type = 1; } extend Protocol { optional Eth2 eth2 = 200; } ostinato-1.3.0/common/eth2.ui000066400000000000000000000033431451413623100160470ustar00rootroot00000000000000 eth2 0 0 190 64 Form Ethernet Type false >HH HH; Qt::Horizontal 40 20 Qt::Vertical 20 40 cbOverrideType toggled(bool) leType setEnabled(bool) 98 27 118 27 ostinato-1.3.0/common/eth2config.cpp000066400000000000000000000032261451413623100174020ustar00rootroot00000000000000/* Copyright (C) 2010,2014 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #include "eth2config.h" #include "eth2.h" Eth2ConfigForm::Eth2ConfigForm(QWidget *parent) : AbstractProtocolConfigForm(parent) { setupUi(this); } Eth2ConfigForm::~Eth2ConfigForm() { } Eth2ConfigForm* Eth2ConfigForm::createInstance() { return new Eth2ConfigForm; } void Eth2ConfigForm::loadWidget(AbstractProtocol *proto) { cbOverrideType->setChecked( proto->fieldData( Eth2Protocol::eth2_is_override_type, AbstractProtocol::FieldValue ).toBool()); leType->setText(uintToHexStr( proto->fieldData( Eth2Protocol::eth2_type, AbstractProtocol::FieldValue ).toUInt(), 2)); } void Eth2ConfigForm::storeWidget(AbstractProtocol *proto) { bool isOk; proto->setFieldData( Eth2Protocol::eth2_is_override_type, cbOverrideType->isChecked()); proto->setFieldData( Eth2Protocol::eth2_type, leType->text().remove(QChar(' ')).toUInt(&isOk, BASE_HEX)); } ostinato-1.3.0/common/eth2config.h000066400000000000000000000021761451413623100170520ustar00rootroot00000000000000/* Copyright (C) 2010,2014 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _ETH2_CONFIG_H #define _ETH2_CONFIG_H #include "abstractprotocolconfig.h" #include "eth2.pb.h" #include "ui_eth2.h" class Eth2ConfigForm : public AbstractProtocolConfigForm, public Ui::eth2 { Q_OBJECT public: Eth2ConfigForm(QWidget *parent = 0); virtual ~Eth2ConfigForm(); static Eth2ConfigForm* createInstance(); virtual void loadWidget(AbstractProtocol *proto); virtual void storeWidget(AbstractProtocol *proto); }; #endif ostinato-1.3.0/common/eth2pdml.cpp000066400000000000000000000077541451413623100171030ustar00rootroot00000000000000/* Copyright (C) 2011 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #include "eth2pdml.h" #include "dot3.pb.h" #include "eth2.pb.h" #include "mac.pb.h" #include "vlan.pb.h" PdmlEthProtocol::PdmlEthProtocol() { ostProtoId_ = OstProto::Protocol::kMacFieldNumber; fieldMap_.insert("eth.dst", OstProto::Mac::kDstMacFieldNumber); fieldMap_.insert("eth.src", OstProto::Mac::kSrcMacFieldNumber); } PdmlProtocol* PdmlEthProtocol::createInstance() { return new PdmlEthProtocol(); } void PdmlEthProtocol::unknownFieldHandler(QString name, int /*pos*/, int /*size*/, const QXmlStreamAttributes &attributes, OstProto::Protocol* /*pbProto*/, OstProto::Stream *stream) { if (name == "eth.vlan.tpid") { bool isOk; uint tpid = attributes.value("value").toString() .toUInt(&isOk, kBaseHex); OstProto::Protocol *proto = stream->add_protocol(); proto->mutable_protocol_id()->set_id( OstProto::Protocol::kVlanFieldNumber); OstProto::Vlan *vlan = proto->MutableExtension(OstProto::vlan); vlan->set_tpid(tpid); vlan->set_is_override_tpid(true); } else if (name == "eth.vlan.id") { bool isOk; uint tag = attributes.value("unmaskedvalue").isEmpty() ? attributes.value("value").toString().toUInt(&isOk, kBaseHex) : attributes.value("unmaskedvalue").toString().toUInt(&isOk,kBaseHex); OstProto::Vlan *vlan = stream->mutable_protocol( stream->protocol_size()-1)->MutableExtension(OstProto::vlan); vlan->set_vlan_tag(tag); } else if (name == "eth.type") { bool isOk; uint type = attributes.value("value").toString() .toUInt(&isOk, kBaseHex); OstProto::Protocol *proto = stream->add_protocol(); proto->mutable_protocol_id()->set_id( OstProto::Protocol::kEth2FieldNumber); OstProto::Eth2 *eth2 = proto->MutableExtension(OstProto::eth2); eth2->set_type(type); eth2->set_is_override_type(true); } else if (name == "eth.len") { OstProto::Protocol *proto = stream->add_protocol(); proto->mutable_protocol_id()->set_id( OstProto::Protocol::kDot3FieldNumber); OstProto::Dot3 *dot3 = proto->MutableExtension(OstProto::dot3); bool isOk; dot3->set_length(attributes.value("value").toString().toUInt(&isOk, kBaseHex)); dot3->set_is_override_length(true); } #if 0 else if (name == "eth.trailer") { QByteArray trailer = QByteArray::fromHex( attributes.value("value").toString().toUtf8()); stream->mutable_core()->mutable_name()->append(trailer.constData(), trailer.size()); } else if ((name == "eth.fcs") || attributes.value("show").toString().startsWith("Frame check sequence")) { QByteArray trailer = QByteArray::fromHex( attributes.value("value").toString().toUtf8()); stream->mutable_core()->mutable_name()->append(trailer.constData(), trailer.size()); } #endif } void PdmlEthProtocol::postProtocolHandler(OstProto::Protocol *pbProto, OstProto::Stream* /*stream*/) { OstProto::Mac *mac = pbProto->MutableExtension(OstProto::mac); mac->set_dst_mac_mode(OstProto::Mac::e_mm_fixed); mac->set_src_mac_mode(OstProto::Mac::e_mm_fixed); } ostinato-1.3.0/common/eth2pdml.h000066400000000000000000000022411451413623100165320ustar00rootroot00000000000000/* Copyright (C) 2011 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _ETH2_PDML_H #define _ETH2_PDML_H #include "pdmlprotocol.h" class PdmlEthProtocol : public PdmlProtocol { public: static PdmlProtocol* createInstance(); virtual void unknownFieldHandler(QString name, int pos, int size, const QXmlStreamAttributes &attributes, OstProto::Protocol *pbProto, OstProto::Stream *stream); virtual void postProtocolHandler(OstProto::Protocol *pbProto, OstProto::Stream *stream); protected: PdmlEthProtocol(); }; #endif ostinato-1.3.0/common/fileformat.proto000066400000000000000000000071301451413623100200610ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ import "protocol.proto"; package OstProto; enum FileType { kReservedFileType = 0; kStreamsFileType = 1; kSessionFileType = 10; } message FileMetaData { required FileType file_type = 1; required uint32 format_version_major = 2; required uint32 format_version_minor = 3; required uint32 format_version_revision = 4; required string generator_name = 5; required string generator_version = 6; required string generator_revision = 7; } message PortContent { optional Port port_config = 1; repeated Stream streams = 2; repeated DeviceGroup device_groups = 3; } message PortGroupContent { optional string server_name = 1; optional uint32 server_port = 2; repeated PortContent ports = 15; } message SessionContent { repeated PortGroupContent port_groups = 1; } message FileContentMatter { optional StreamConfigList streams = 1; // TODO: optional DeviceGroupConfigList device_groups = 2; // TODO: optional PortContent port = 3; // FIXME: (single) portgroup? is there a usecase for this? optional SessionContent session = 10; } /* An Ostinato file is the binary encoding of the File message below STRICTLY in increasing order of field number for the top level fields We do not use field number '1' for magic value because its encoded key is '0a' (LF or \n) which occurs commonly in text files. Checksum should be the last field, so top level field numbers greater than 15 are not permitted. We use 15 as the checksum field number because it is the largest field number that can fit in a 1-byte tag The magic value is of a fixed length so that meta data has a fixed offset from the start in the encoded message. Checksum is fixed length so that it is at a fixed negative offset from the end in the encoded message. Because the protobuf serialization API does not _guarantee_ strict ordering, so we define wrapper messages for each top level field and serialize the individual wrapper messages. The field numbers MUST be the same in 'File' and the wrapper messages */ message File { // FieldNumber 1 is reserved and MUST not be used! required bytes magic_value = 2; required FileMetaData meta_data = 3; optional FileContentMatter content_matter = 9; required fixed32 checksum_value = 15; } /* The magic value is 10 bytes - "\xa7\xb7OSTINATO" The 1st-2nd byte has the MSB set to avoid mixup with text files The 3rd-10th byte spell OSTINATO (duh!) Encoded Size : Key(1) + Length(1) + Value(10) = 12 bytes Encoded Value: 120aa7b7 4f535449 4e41544f */ message FileMagic { required bytes value = 2; } message FileMeta { required FileMetaData data = 3; } message FileContent { optional FileContentMatter matter = 9; } /* Encoded Size : Key(1) + Value(4) = 5 bytes Encoded Value: 7d xxXXxxXX */ message FileChecksum { required fixed32 value = 15; // should always be a fixed 32-bit size } ostinato-1.3.0/common/framevalueattrib.h000066400000000000000000000025431451413623100203550ustar00rootroot00000000000000/* Copyright (C) 2019 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _FRAME_VALUE_ATTRIB_H #define _FRAME_VALUE_ATTRIB_H #include struct FrameValueAttrib { enum ErrorFlag { UnresolvedSrcMacError = 0x1, UnresolvedDstMacError = 0x2, OutOfMemoryError = 0x4, }; Q_DECLARE_FLAGS(ErrorFlags, ErrorFlag); ErrorFlags errorFlags{0}; // TODO?: int len; FrameValueAttrib& operator+=(const FrameValueAttrib& rhs) { errorFlags |= rhs.errorFlags; return *this; } FrameValueAttrib operator+(const FrameValueAttrib& rhs) { FrameValueAttrib result = *this; result += rhs; return result; } }; Q_DECLARE_OPERATORS_FOR_FLAGS(FrameValueAttrib::ErrorFlags) #endif ostinato-1.3.0/common/gmp.cpp000077500000000000000000000526041451413623100161440ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #include "gmp.h" #include QHash GmpProtocol::frameFieldCountMap; GmpProtocol::GmpProtocol(StreamBase *stream, AbstractProtocol *parent) : AbstractProtocol(stream, parent) { // field count may change based on msgType - so don't cache field offsets _cacheFlags &= ~FieldFrameBitOffsetCache; } GmpProtocol::~GmpProtocol() { } AbstractProtocol::ProtocolIdType GmpProtocol::protocolIdType() const { return ProtocolIdIp; } int GmpProtocol::fieldCount() const { return FIELD_COUNT; } int GmpProtocol::frameFieldCount() const { int type = msgType(); // frameFieldCountMap contains the frameFieldCounts for each // msgType - this is built on demand and cached for subsequent use // lookup if we have already cached ... if (frameFieldCountMap.contains(type)) return frameFieldCountMap.value(type); // ... otherwise calculate and cache int count = 0; for (int i = 0; i < FIELD_COUNT; i++) { if (fieldFlags(i).testFlag(AbstractProtocol::FrameField)) count++; } frameFieldCountMap.insert(type, count); return count; } AbstractProtocol::FieldFlags GmpProtocol::fieldFlags(int index) const { AbstractProtocol::FieldFlags flags; flags = AbstractProtocol::fieldFlags(index); flags &= ~FrameField; switch(index) { // Frame Fields - check against msgType() case kType: case kRsvdMrtCode: flags |= FrameField; break; case kChecksum: flags |= FrameField; flags |= CksumField; break; case kMldMrt: case kMldRsvd: // MLD subclass should handle suitably break; case kGroupAddress: if (!isSsmReport()) flags |= FrameField; break; case kRsvd1: case kSFlag: case kQrv: case kQqic: case kSourceCount: case kSources: if (isSsmQuery()) flags |= FrameField; break; case kRsvd2: case kGroupRecordCount: case kGroupRecords: if (isSsmReport()) flags |= FrameField; break; // Meta Fields case kIsOverrideChecksum: case kGroupMode: case kGroupCount: case kGroupPrefix: case kIsOverrideSourceCount: case kIsOverrideGroupRecordCount: flags |= MetaField; break; default: qFatal("%s: unimplemented case %d in switch", __PRETTY_FUNCTION__, index); break; } return flags; } QVariant GmpProtocol::fieldData(int index, FieldAttrib attrib, int streamIndex) const { switch (index) { case kType: { uint type = data.type(); switch(attrib) { case FieldName: return QString("Type"); case FieldValue: return type; case FieldTextValue: return QString("%1").arg(quint8(type)); case FieldFrameValue: return QByteArray(1, quint8(type)); default: break; } break; } case kRsvdMrtCode: { quint8 rsvd = 0; switch(attrib) { case FieldName: return QString("Reserved"); case FieldValue: return rsvd; case FieldTextValue: return QString("%1").arg(rsvd); case FieldFrameValue: return QByteArray(1, rsvd); default: break; } break; } case kChecksum: { switch(attrib) { case FieldName: return QString("Checksum"); case FieldBitSize: return 16; default: break; } quint16 cksum = data.is_override_checksum() ? data.checksum() : checksum(streamIndex); switch(attrib) { case FieldValue: return cksum; case FieldFrameValue: { QByteArray fv; fv.resize(2); qToBigEndian(cksum, (uchar*) fv.data()); return fv; } case FieldTextValue: return QString("0x%1").arg(cksum, 4, BASE_HEX, QChar('0')); default: break; } break; } case kMldMrt: case kMldRsvd: // XXX: Present only in MLD - hence handled by the mld subclass break; case kGroupAddress: // XXX: Handled by each subclass break; case kRsvd1: { quint8 rsvd = 0; switch(attrib) { case FieldName: return QString("Reserved"); case FieldValue: return rsvd; case FieldTextValue: return QString("%1").arg(rsvd); case FieldFrameValue: return QByteArray(1, char(rsvd)); case FieldBitSize: return 4; default: break; } break; } case kSFlag: { switch(attrib) { case FieldName: return QString("S Flag"); case FieldValue: return data.s_flag(); case FieldTextValue: return data.s_flag() ? QString("True") : QString("False"); case FieldFrameValue: return QByteArray(1, char(data.s_flag())); case FieldBitSize: return 1; default: break; } break; } case kQrv: { int qrv = data.qrv() & 0x7; switch(attrib) { case FieldName: return QString("QRV"); case FieldValue: return qrv; case FieldTextValue: return QString("%1").arg(qrv); case FieldFrameValue: return QByteArray(1, char(qrv)); case FieldBitSize: return 3; default: break; } break; } case kQqic: { int qqi = data.qqi(); switch(attrib) { case FieldName: return QString("QQIC"); case FieldValue: return qqi; case FieldTextValue: return QString("%1").arg(qqi); case FieldFrameValue: { char qqicode = char(qqic(qqi)); return QByteArray(1, qqicode); } default: break; } break; } case kSourceCount: { quint16 count = data.sources_size(); if (data.is_override_source_count()) count = data.source_count(); switch(attrib) { case FieldName: return QString("Number of Sources"); case FieldValue: return count; case FieldTextValue: return QString("%1").arg(count); case FieldFrameValue: { QByteArray fv; fv.resize(2); qToBigEndian(count, (uchar*) fv.data()); return fv; } default: break; } break; } case kSources: // XXX: Handled by each subclass break; case kRsvd2: { quint16 rsvd = 0; switch(attrib) { case FieldName: return QString("Reserved"); case FieldValue: return rsvd; case FieldTextValue: return QString("%1").arg(rsvd); case FieldFrameValue: { QByteArray fv; fv.resize(2); qToBigEndian(rsvd, (uchar*) fv.data()); return fv; } default: break; } break; } case kGroupRecordCount: { quint16 count = data.group_records_size(); if (data.is_override_group_record_count()) count = data.group_record_count(); switch(attrib) { case FieldName: return QString("Number of Group Records"); case FieldValue: return count; case FieldTextValue: return QString("%1").arg(count); case FieldFrameValue: { QByteArray fv; fv.resize(2); qToBigEndian(count, (uchar*) fv.data()); return fv; } default: break; } break; } case kGroupRecords: { switch(attrib) { case FieldName: return QString("Group List"); case FieldValue: { QVariantList grpRecords; for (int i = 0; i < data.group_records_size(); i++) { QVariantMap grpRec; OstProto::Gmp::GroupRecord rec = data.group_records(i); grpRec["groupRecordType"] = rec.type(); // grpRec["groupRecordAddress"] = subclass responsibility grpRec["overrideGroupRecordSourceCount"] = rec.is_override_source_count(); grpRec["groupRecordSourceCount"] = rec.source_count(); // grpRec["groupRecordSourceList"] = subclass responsibility grpRec["overrideAuxDataLength"] = rec.is_override_aux_data_length(); grpRec["auxDataLength"] = rec.aux_data_length(); grpRec["auxData"] = QByteArray(rec.aux_data().data(), rec.aux_data().size()); grpRecords.append(grpRec); } return grpRecords; } case FieldFrameValue: { QVariantList fv; for (int i = 0; i < data.group_records_size(); i++) { OstProto::Gmp::GroupRecord rec = data.group_records(i); QByteArray rv; quint16 srcCount; rv.resize(4); rv[0] = rec.type(); rv[1] = rec.is_override_aux_data_length() ? rec.aux_data_length() : rec.aux_data().size()/4; if (rec.is_override_source_count()) srcCount = rec.source_count(); else srcCount = rec.sources_size(); qToBigEndian(srcCount, (uchar*)(rv.data()+2)); // group_address => subclass responsibility // source list => subclass responsibility rv.append(QByteArray(rec.aux_data().data(), rv[1]*4)); fv.append(rv); } return fv; } case FieldTextValue: { QStringList list; for (int i = 0; i < data.group_records_size(); i++) { OstProto::Gmp::GroupRecord rec = data.group_records(i); QString str; str.append(" Type: "); switch(rec.type()) { case OstProto::Gmp::GroupRecord::kIsInclude: str.append("IS_INCLUDE"); break; case OstProto::Gmp::GroupRecord::kIsExclude: str.append("IS_EXCLUDE"); break; case OstProto::Gmp::GroupRecord::kToInclude: str.append("TO_INCLUDE"); break; case OstProto::Gmp::GroupRecord::kToExclude: str.append("TO_EXCLUDE"); break; case OstProto::Gmp::GroupRecord::kAllowNew: str.append("ALLOW_NEW"); break; case OstProto::Gmp::GroupRecord::kBlockOld: str.append("BLOCK_OLD"); break; default: str.append("UNKNOWN"); break; } int auxLen = rec.is_override_aux_data_length() ? rec.aux_data_length() : rec.aux_data().size()/4; str.append(QString("; AuxLen: %1").arg(auxLen)); str.append(QString("; Source Count: %1").arg( rec.is_override_source_count() ? rec.source_count(): rec.sources_size())); // NOTE: subclass should replace the XXX below with // group address and source list str.append(QString("; XXX")); str.append(QString("; AuxData: ").append( QByteArray(rec.aux_data().data(), auxLen*4) .toHex())); list.append(str); } return list; } default: break; } break; } // Meta Fields case kIsOverrideChecksum: { switch(attrib) { case FieldValue: return data.is_override_checksum(); default: break; } break; } case kGroupMode: { switch(attrib) { case FieldValue: return data.group_mode(); default: break; } break; } case kGroupCount: { switch(attrib) { case FieldValue: return data.group_count(); default: break; } break; } case kGroupPrefix: { switch(attrib) { case FieldValue: return data.group_prefix(); default: break; } break; } case kIsOverrideSourceCount: { switch(attrib) { case FieldValue: return data.is_override_source_count(); default: break; } break; } case kIsOverrideGroupRecordCount: { switch(attrib) { case FieldValue: return data.is_override_group_record_count(); default: break; } break; } default: qFatal("%s: unimplemented case %d in switch", __PRETTY_FUNCTION__, index); break; } return AbstractProtocol::fieldData(index, attrib, streamIndex); } bool GmpProtocol::setFieldData(int index, const QVariant &value, FieldAttrib attrib) { bool isOk = false; if (attrib != FieldValue) goto _exit; switch (index) { case kType: { uint type = value.toUInt(&isOk); if (isOk) data.set_type(type); break; } case kRsvdMrtCode: { uint val = value.toUInt(&isOk); if (isOk) data.set_rsvd_code(val); break; } case kChecksum: { uint csum = value.toUInt(&isOk); if (isOk) data.set_checksum(csum); break; } case kMldMrt: { uint mrt = value.toUInt(&isOk); if (isOk) data.set_max_response_time(mrt); break; } case kGroupAddress: // XXX: Handled by subclass isOk = false; break; case kRsvd1: isOk = false; break; case kSFlag: { bool flag = value.toBool(); data.set_s_flag(flag); isOk = true; break; } case kQrv: { uint qrv = value.toUInt(&isOk); if (isOk) data.set_qrv(qrv); break; } case kQqic: { uint qqi = value.toUInt(&isOk); if (isOk) data.set_qqi(qqi); break; } case kSourceCount: { uint count = value.toUInt(&isOk); if (isOk) data.set_source_count(count); break; } case kSources: // XXX: Handled by subclass isOk = false; break; case kRsvd2: isOk = false; break; case kGroupRecordCount: { uint count = value.toUInt(&isOk); if (isOk) data.set_group_record_count(count); break; } case kGroupRecords: { QVariantList list = value.toList(); data.clear_group_records(); for (int i = 0; i < list.count(); i++) { QVariantMap grpRec = list.at(i).toMap(); OstProto::Gmp::GroupRecord *rec = data.add_group_records(); rec->set_type(OstProto::Gmp::GroupRecord::RecordType( grpRec["groupRecordType"].toInt())); // NOTE: rec->group_address => subclass responsibility rec->set_is_override_source_count( grpRec["overrideGroupRecordSourceCount"].toBool()); rec->set_source_count(grpRec["groupRecordSourceCount"].toUInt()); // NOTE: rec->sources => subclass responsibility rec->set_is_override_aux_data_length( grpRec["overrideAuxDataLength"].toBool()); rec->set_aux_data_length(grpRec["auxDataLength"].toUInt()); QByteArray ba = grpRec["auxData"].toByteArray(); // pad to word boundary if (ba.size() % 4) ba.append(QByteArray(4 - (ba.size() % 4), char(0))); rec->set_aux_data(std::string(ba.constData(), ba.size())); } isOk = true; break; } // Meta Fields case kIsOverrideChecksum: { bool ovr = value.toBool(); data.set_is_override_checksum(ovr); isOk = true; break; } case kGroupMode: { uint mode = value.toUInt(&isOk); if (isOk && data.GroupMode_IsValid(mode)) data.set_group_mode((OstProto::Gmp::GroupMode)mode); break; } case kGroupCount: { uint count = value.toUInt(&isOk); if (isOk) data.set_group_count(count); break; } case kGroupPrefix: { uint prefix = value.toUInt(&isOk); if (isOk) data.set_group_prefix(prefix); break; } case kIsOverrideSourceCount: { bool ovr = value.toBool(); data.set_is_override_source_count(ovr); isOk = true; break; } case kIsOverrideGroupRecordCount: { bool ovr = value.toBool(); data.set_is_override_group_record_count(ovr); isOk = true; break; } default: qFatal("%s: unimplemented case %d in switch", __PRETTY_FUNCTION__, index); break; } _exit: return isOk; } int GmpProtocol::protocolFrameSize(int streamIndex) const { // TODO: Calculate to reduce processing cost return AbstractProtocol::protocolFrameValue(streamIndex, true).size(); } int GmpProtocol::protocolFrameVariableCount() const { int count = AbstractProtocol::protocolFrameVariableCount(); // No fields vary for Ssm Report if (isSsmReport()) return count; // For all other msg types, check the group mode if (fieldData(kGroupMode, FieldValue).toUInt() != uint(OstProto::Gmp::kFixed)) { count = AbstractProtocol::lcm(count, fieldData(kGroupCount, FieldValue).toUInt()); } return count; } ostinato-1.3.0/common/gmp.h000077500000000000000000000062211451413623100156030ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _GMP_H #define _GMP_H #include "abstractprotocol.h" #include "gmp.pb.h" #include /* Gmp Protocol Frame Format - TODO: for now see the respective RFCs */ class GmpProtocol : public AbstractProtocol { public: enum GmpField { // ------------ // Frame Fields // ------------ // Fields used in all ASM and SSM messages, unless otherwise specified kType = 0, kRsvdMrtCode, kChecksum, kMldMrt, // MLD Only (except MLDv2 Report) kMldRsvd, // MLD Only (except MLDv2 Report) // Field used in ASM messages kGroupAddress, FIELD_COUNT_ASM_ALL, // Fields used in SSM Query kRsvd1 = FIELD_COUNT_ASM_ALL, kSFlag, kQrv, kQqic, kSourceCount, kSources, FIELD_COUNT_SSM_QUERY, // Fields used in SSM Report kRsvd2 = FIELD_COUNT_SSM_QUERY, kGroupRecordCount, kGroupRecords, FIELD_COUNT_SSM_REPORT, FRAME_FIELD_COUNT = FIELD_COUNT_SSM_REPORT, // ----------- // Meta Fields // ----------- kIsOverrideChecksum = FRAME_FIELD_COUNT, kGroupMode, kGroupCount, kGroupPrefix, kIsOverrideSourceCount, kIsOverrideGroupRecordCount, FIELD_COUNT }; GmpProtocol(StreamBase *stream, AbstractProtocol *parent = 0); virtual ~GmpProtocol(); virtual ProtocolIdType protocolIdType() const; virtual int fieldCount() const; virtual int frameFieldCount() const; virtual AbstractProtocol::FieldFlags fieldFlags(int index) const; virtual QVariant fieldData(int index, FieldAttrib attrib, int streamIndex = 0) const; virtual bool setFieldData(int index, const QVariant &value, FieldAttrib attrib = FieldValue); virtual int protocolFrameSize(int streamIndex = 0) const; virtual int protocolFrameVariableCount() const; protected: OstProto::Gmp data; int msgType() const; virtual bool isSsmReport() const = 0; virtual bool isQuery() const = 0; virtual bool isSsmQuery() const = 0; int qqic(int value) const; virtual quint16 checksum(int streamIndex) const = 0; private: static QHash frameFieldCountMap; }; inline int GmpProtocol::msgType() const { return fieldData(kType, FieldValue).toInt(); } inline int GmpProtocol::qqic(int value) const { return quint8(value); // TODO: if value > 128 convert to mantissa/exp form } #endif ostinato-1.3.0/common/gmp.proto000077500000000000000000000054531451413623100165250ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ package OstProto; /// Group Management Protocol (i.e. IGMP and MLD) message Gmp { // // Common fields for both ASM and SSM messages // optional uint32 type = 1; optional bool is_override_rsvd_code = 2; optional uint32 rsvd_code = 3; // MaxRespTime is in milliseconds - MaxRespCode will be derived optional uint32 max_response_time = 4 [default = 100]; optional bool is_override_checksum = 5; optional uint32 checksum = 6; message IpAddress { optional fixed32 v4 = 1; optional fixed64 v6_hi = 2; optional fixed64 v6_lo = 3; } // // Fields used in ASM messages // enum GroupMode { kFixed = 0; kIncrementGroup = 1; kDecrementGroup = 2; kRandomGroup = 3; } optional IpAddress group_address = 10; optional GroupMode group_mode = 11 [default = kFixed]; optional uint32 group_count = 12 [default = 16]; optional uint32 group_prefix = 13 [default = 24]; // // Fields used in SSM Query // optional bool s_flag = 20; optional uint32 qrv = 21 [default = 2]; // QuerierQueryInterval is in seconds - QQIC will be derived optional uint32 qqi = 22 [default = 125]; repeated IpAddress sources = 23; optional bool is_override_source_count = 24; optional uint32 source_count = 25; // // Fields used in SSM Reports // message GroupRecord { enum RecordType { kReserved = 0; kIsInclude = 1; kIsExclude = 2; kToInclude = 3; kToExclude = 4; kAllowNew = 5; kBlockOld = 6; } optional RecordType type = 1 [default = kIsInclude]; optional IpAddress group_address = 2; repeated IpAddress sources = 3; optional bool is_override_source_count = 4; optional uint32 source_count = 5; optional bytes aux_data = 6; optional bool is_override_aux_data_length = 7; optional uint32 aux_data_length = 8; } repeated GroupRecord group_records = 30; optional bool is_override_group_record_count = 31; optional uint32 group_record_count = 32; } ostinato-1.3.0/common/gmp.ui000077500000000000000000000623551451413623100160030ustar00rootroot00000000000000 Gmp 0 0 509 355 Form Message Type msgTypeCombo Max Response Time (1/10s) maxResponseTime 0 0 Checksum false 0 0 >HHHH; Group Address groupAddress Mode msgTypeCombo Count msgTypeCombo Prefix msgTypeCombo 1 0 Fixed Increment Host Decrement Host Random Host false 0 0 false 0 0 /900; 1 0 0 0 0 S Flag (Suppress Router Processing) QRV qrv Qt::Horizontal 40 20 QQI qqi Qt::Vertical 61 41 Source List groupRecordAddress Qt::Horizontal 16 20 + – true QAbstractItemView::InternalMove Qt::Horizontal 40 20 Count false Qt::Vertical 20 40 0 0 0 0 Group Records groupRecordAddress Qt::Horizontal 16 20 + – true QAbstractItemView::InternalMove Number of Groups false false 0 0 0 0 Record Type groupRecordType Reserved Is Include Is Exclude To Include To Exclude Allow New Block Old Group Address groupRecordAddress Source List groupRecordAddress Qt::Horizontal 191 20 + – true QAbstractItemView::InternalMove Number of Sources false 0 0 Qt::Horizontal 81 20 Aux Data Qt::Horizontal 40 20 Length (x4) false 0 0 Qt::Vertical 101 21 IntComboBox QComboBox
intcombobox.h
msgTypeCombo maxResponseTime overrideChecksum checksum groupAddress groupMode groupCount groupPrefix overrideGroupRecordCount groupRecordCount groupRecordType groupRecordAddress overrideGroupRecordSourceCount groupRecordSourceCount overrideAuxDataLength auxDataLength auxData sFlag qrv qqi overrideSourceCount sourceCount overrideChecksum toggled(bool) checksum setEnabled(bool) 391 43 448 45 overrideGroupRecordSourceCount toggled(bool) groupRecordSourceCount setEnabled(bool) 402 202 436 204 overrideAuxDataLength toggled(bool) auxDataLength setEnabled(bool) 416 286 433 286 overrideGroupRecordCount toggled(bool) groupRecordCount setEnabled(bool) 112 309 138 312 overrideSourceCount toggled(bool) sourceCount setEnabled(bool) 413 154 434 151
ostinato-1.3.0/common/gmpconfig.cpp000066400000000000000000000303001451413623100173140ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #include "gmpconfig.h" #include "gmp.h" GmpConfigForm::GmpConfigForm(QWidget *parent) : AbstractProtocolConfigForm(parent) { setupUi(this); auxData->setValidator(new QRegExpValidator( QRegExp("[0-9A-Fa-f]*"), this)); } GmpConfigForm::~GmpConfigForm() { } void GmpConfigForm::loadWidget(AbstractProtocol *proto) { msgTypeCombo->setValue( proto->fieldData( GmpProtocol::kType, AbstractProtocol::FieldValue ).toUInt()); // XXX: maxResponseTime set by subclass overrideChecksum->setChecked( proto->fieldData( GmpProtocol::kIsOverrideChecksum, AbstractProtocol::FieldValue ).toBool()); checksum->setText(uintToHexStr( proto->fieldData( GmpProtocol::kChecksum, AbstractProtocol::FieldValue ).toUInt(), 2)); groupAddress->setText( proto->fieldData( GmpProtocol::kGroupAddress, AbstractProtocol::FieldTextValue ).toString()); groupMode->setCurrentIndex( proto->fieldData( GmpProtocol::kGroupMode, AbstractProtocol::FieldValue ).toUInt()); groupCount->setText( proto->fieldData( GmpProtocol::kGroupCount, AbstractProtocol::FieldValue ).toString()); groupPrefix->setText( proto->fieldData( GmpProtocol::kGroupPrefix, AbstractProtocol::FieldValue ).toString()); sFlag->setChecked( proto->fieldData( GmpProtocol::kSFlag, AbstractProtocol::FieldValue ).toBool()); qrv->setText( proto->fieldData( GmpProtocol::kQrv, AbstractProtocol::FieldValue ).toString()); qqi->setText( proto->fieldData( GmpProtocol::kQqic, AbstractProtocol::FieldValue ).toString()); QStringList sl = proto->fieldData( GmpProtocol::kSources, AbstractProtocol::FieldValue ).toStringList(); sourceList->clear(); foreach(QString src, sl) { QListWidgetItem *item = new QListWidgetItem(src); item->setFlags(item->flags() | Qt::ItemIsEditable); sourceList->addItem(item); } // NOTE: SourceCount should be loaded after sourceList overrideSourceCount->setChecked( proto->fieldData( GmpProtocol::kIsOverrideSourceCount, AbstractProtocol::FieldValue ).toBool()); sourceCount->setText( proto->fieldData( GmpProtocol::kSourceCount, AbstractProtocol::FieldValue ).toString()); QVariantList list = proto->fieldData( GmpProtocol::kGroupRecords, AbstractProtocol::FieldValue ).toList(); groupList->clear(); foreach (QVariant rec, list) { QVariantMap grpRec = rec.toMap(); QListWidgetItem *item = new QListWidgetItem; item->setData(Qt::UserRole, grpRec); item->setText(QString("%1: %2") .arg(groupRecordType->itemText( grpRec["groupRecordType"].toInt())) .arg(grpRec["groupRecordAddress"].toString())); groupList->addItem(item); } // NOTE: recordCount should be loaded after recordList overrideGroupRecordCount->setChecked( proto->fieldData( GmpProtocol::kIsOverrideGroupRecordCount, AbstractProtocol::FieldValue ).toBool()); groupRecordCount->setText( proto->fieldData( GmpProtocol::kGroupRecordCount, AbstractProtocol::FieldValue ).toString()); } void GmpConfigForm::storeWidget(AbstractProtocol *proto) { update(); proto->setFieldData( GmpProtocol::kType, msgTypeCombo->currentValue()); // XXX: maxResponseTime handled by subclass proto->setFieldData( GmpProtocol::kIsOverrideChecksum, overrideChecksum->isChecked()); proto->setFieldData( GmpProtocol::kChecksum, hexStrToUInt(checksum->text())); proto->setFieldData( GmpProtocol::kGroupAddress, groupAddress->text()); proto->setFieldData( GmpProtocol::kGroupMode, groupMode->currentIndex()); proto->setFieldData( GmpProtocol::kGroupCount, groupCount->text()); proto->setFieldData( GmpProtocol::kGroupPrefix, groupPrefix->text().remove('/')); proto->setFieldData( GmpProtocol::kSFlag, sFlag->isChecked()); proto->setFieldData( GmpProtocol::kQrv, qrv->text()); proto->setFieldData( GmpProtocol::kQqic, qqi->text()); QStringList list; for (int i = 0; i < sourceList->count(); i++) list.append(sourceList->item(i)->text()); proto->setFieldData( GmpProtocol::kSources, list); // sourceCount should be AFTER sources proto->setFieldData( GmpProtocol::kIsOverrideSourceCount, overrideSourceCount->isChecked()); proto->setFieldData( GmpProtocol::kSourceCount, sourceCount->text()); QVariantList grpList; for (int i = 0; i < groupList->count(); i++) { QVariant grp = groupList->item(i)->data(Qt::UserRole); grpList.append(grp.toMap()); } proto->setFieldData(GmpProtocol::kGroupRecords, grpList); // groupRecordCount should be AFTER groupRecords proto->setFieldData( GmpProtocol::kIsOverrideGroupRecordCount, overrideGroupRecordCount->isChecked()); proto->setFieldData( GmpProtocol::kGroupRecordCount, groupRecordCount->text()); } void GmpConfigForm::update() { // save the current group Record by simulating a currentItemChanged() on_groupList_currentItemChanged(groupList->currentItem(), groupList->currentItem()); } // // -- private slots // void GmpConfigForm::on_groupMode_currentIndexChanged(int index) { bool disabled = (index == 0); groupCount->setDisabled(disabled); groupPrefix->setDisabled(disabled); } void GmpConfigForm::on_addSource_clicked() { QListWidgetItem *item=new QListWidgetItem(_defaultSourceIp); item->setFlags(item->flags() | Qt::ItemIsEditable); sourceList->insertItem(sourceList->currentRow(), item); if (!overrideSourceCount->isChecked()) sourceCount->setText(QString().setNum(sourceList->count())); } void GmpConfigForm::on_deleteSource_clicked() { delete sourceList->takeItem(sourceList->currentRow()); if (!overrideSourceCount->isChecked()) sourceCount->setText(QString().setNum(sourceList->count())); } void GmpConfigForm::on_addGroupRecord_clicked() { OstProto::Gmp::GroupRecord defRec; QVariantMap grpRec; QListWidgetItem *item = new QListWidgetItem; grpRec["groupRecordType"] = defRec.type(); grpRec["groupRecordAddress"] = _defaultGroupIp; grpRec["overrideGroupRecordSourceCount"] =defRec.is_override_source_count(); grpRec["groupRecordSourceCount"] = defRec.source_count(); grpRec["groupRecordSourceList"] = QStringList(); grpRec["overrideAuxDataLength"] = defRec.is_override_aux_data_length(); grpRec["auxDataLength"] = defRec.aux_data_length(); grpRec["auxData"] = QByteArray().append( QString().fromStdString(defRec.aux_data())); item->setData(Qt::UserRole, grpRec); item->setText(QString("%1: %2") .arg(groupRecordType->itemText(grpRec["groupRecordType"].toInt())) .arg(grpRec["groupRecordAddress"].toString())); groupList->insertItem(groupList->currentRow(), item); if (!overrideGroupRecordCount->isChecked()) groupRecordCount->setText(QString().setNum(groupList->count())); } void GmpConfigForm::on_deleteGroupRecord_clicked() { delete groupList->takeItem(groupList->currentRow()); if (!overrideGroupRecordCount->isChecked()) groupRecordCount->setText(QString().setNum(groupList->count())); } void GmpConfigForm::on_groupList_currentItemChanged(QListWidgetItem *current, QListWidgetItem *previous) { QVariantMap rec; QStringList strList; qDebug("in %s", __FUNCTION__); // save previous record ... if (previous == NULL) goto _load_current_record; rec["groupRecordType"] = groupRecordType->currentIndex(); rec["groupRecordAddress"] = groupRecordAddress->text(); strList.clear(); while (groupRecordSourceList->count()) { QListWidgetItem *item = groupRecordSourceList->takeItem(0); strList.append(item->text()); delete item; } rec["groupRecordSourceList"] = strList; rec["overrideGroupRecordSourceCount"] = overrideGroupRecordSourceCount->isChecked(); rec["groupRecordSourceCount"] = groupRecordSourceCount->text().toUInt(); rec["overrideAuxDataLength"] = overrideAuxDataLength->isChecked(); rec["auxDataLength"] = auxDataLength->text().toUInt(); rec["auxData"] = QByteArray().fromHex(QByteArray().append(auxData->text())); previous->setData(Qt::UserRole, rec); previous->setText(QString("%1: %2") .arg(groupRecordType->itemText(rec["groupRecordType"].toInt())) .arg(rec["groupRecordAddress"].toString())); _load_current_record: // ... and load current record if (current == NULL) goto _exit; rec = current->data(Qt::UserRole).toMap(); groupRecordType->setCurrentIndex(rec["groupRecordType"].toInt()); groupRecordAddress->setText(rec["groupRecordAddress"].toString()); strList = rec["groupRecordSourceList"].toStringList(); groupRecordSourceList->clear(); foreach (QString str, strList) { QListWidgetItem *item = new QListWidgetItem(str, groupRecordSourceList); item->setFlags(item->flags() | Qt::ItemIsEditable); } overrideGroupRecordSourceCount->setChecked( rec["overrideGroupRecordSourceCount"].toBool()); groupRecordSourceCount->setText(QString().setNum( rec["groupRecordSourceCount"].toUInt())); overrideAuxDataLength->setChecked(rec["overrideAuxDataLength"].toBool()); auxDataLength->setText(QString().setNum(rec["auxDataLength"].toUInt())); auxData->setText(QString(rec["auxData"].toByteArray().toHex())); _exit: groupRecord->setEnabled(current != NULL); return; } void GmpConfigForm::on_addGroupRecordSource_clicked() { QListWidgetItem *item=new QListWidgetItem(_defaultSourceIp); item->setFlags(item->flags() | Qt::ItemIsEditable); groupRecordSourceList->insertItem(groupRecordSourceList->currentRow(),item); if (!overrideGroupRecordSourceCount->isChecked()) groupRecordSourceCount->setText(QString().setNum( groupRecordSourceList->count())); } void GmpConfigForm::on_deleteGroupRecordSource_clicked() { delete groupRecordSourceList->takeItem(groupRecordSourceList->currentRow()); if (!overrideGroupRecordSourceCount->isChecked()) groupRecordSourceCount->setText(QString().setNum( groupRecordSourceList->count())); } void GmpConfigForm::on_auxData_textChanged(const QString &text) { // auxDataLength is in units of words and each byte is 2 chars in text() if (!overrideAuxDataLength->isChecked()) auxDataLength->setText(QString().setNum((text.size()+7)/8)); } ostinato-1.3.0/common/gmpconfig.h000066400000000000000000000032551451413623100167720ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _GMP_CONFIG_H #define _GMP_CONFIG_H #include "abstractprotocolconfig.h" #include "ui_gmp.h" class GmpConfigForm : public AbstractProtocolConfigForm, protected Ui::Gmp { Q_OBJECT public: GmpConfigForm(QWidget *parent = 0); ~GmpConfigForm(); virtual void loadWidget(AbstractProtocol *proto); virtual void storeWidget(AbstractProtocol *proto); protected: QString _defaultGroupIp; QString _defaultSourceIp; enum { kSsmQueryPage = 0, kSsmReportPage = 1 }; private: void update(); private slots: void on_groupMode_currentIndexChanged(int index); void on_addSource_clicked(); void on_deleteSource_clicked(); void on_addGroupRecord_clicked(); void on_deleteGroupRecord_clicked(); void on_groupList_currentItemChanged(QListWidgetItem *current, QListWidgetItem *previous); void on_addGroupRecordSource_clicked(); void on_deleteGroupRecordSource_clicked(); void on_auxData_textChanged(const QString &text); }; #endif ostinato-1.3.0/common/gre.cpp000066400000000000000000000307101451413623100161250ustar00rootroot00000000000000/* Copyright (C) 2021 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #include "gre.h" GreProtocol::GreProtocol(StreamBase *stream, AbstractProtocol *parent) : AbstractProtocol(stream, parent) { } GreProtocol::~GreProtocol() { } AbstractProtocol* GreProtocol::createInstance(StreamBase *stream, AbstractProtocol *parent) { return new GreProtocol(stream, parent); } quint32 GreProtocol::protocolNumber() const { return OstProto::Protocol::kGreFieldNumber; } void GreProtocol::protoDataCopyInto(OstProto::Protocol &protocol) const { protocol.MutableExtension(OstProto::gre)->CopyFrom(data); protocol.mutable_protocol_id()->set_id(protocolNumber()); } void GreProtocol::protoDataCopyFrom(const OstProto::Protocol &protocol) { if (protocol.protocol_id().id() == protocolNumber() && protocol.HasExtension(OstProto::gre)) data.MergeFrom(protocol.GetExtension(OstProto::gre)); } QString GreProtocol::name() const { return QString("General Routing Encapsulation Protocol"); } QString GreProtocol::shortName() const { return QString("GRE"); } AbstractProtocol::ProtocolIdType GreProtocol::protocolIdType() const { return ProtocolIdEth; } quint32 GreProtocol::protocolId(ProtocolIdType type) const { switch(type) { case ProtocolIdIp: return 47; default:break; } return AbstractProtocol::protocolId(type); } int GreProtocol::fieldCount() const { return gre_fieldCount; } AbstractProtocol::FieldFlags GreProtocol::fieldFlags(int index) const { AbstractProtocol::FieldFlags flags; flags = AbstractProtocol::fieldFlags(index); switch (index) { case gre_checksum: flags |= CksumField; break; case gre_isOverrideChecksum: flags &= ~FrameField; flags |= MetaField; break; } return flags; } QVariant GreProtocol::fieldData(int index, FieldAttrib attrib, int streamIndex) const { switch (index) { case gre_flags: { switch(attrib) { case FieldName: return QString("Flags"); case FieldValue: return data.flags(); case FieldTextValue: { QString fstr; fstr.append("Cksum:"); fstr.append(data.flags() & GRE_FLAG_CKSUM ? "Y" : "N"); fstr.append(" Key:"); fstr.append(data.flags() & GRE_FLAG_KEY ? "Y" : "N"); fstr.append(" Seq:"); fstr.append(data.flags() & GRE_FLAG_SEQ ? "Y" : "N"); return fstr; } case FieldFrameValue: return QByteArray(1, char(data.flags())); case FieldBitSize: return 4; default: break; } break; } case gre_rsvd0: { switch(attrib) { case FieldName: return QString("Reserved0"); case FieldValue: return data.rsvd0(); case FieldTextValue: return QString("%1").arg(data.rsvd0()); case FieldFrameValue: { QByteArray fv; fv.resize(2); qToBigEndian(quint16(data.rsvd0()), (uchar*)fv.data()); return fv; } case FieldBitSize: return 9; default: break; } break; } case gre_version: { switch(attrib) { case FieldName: return QString("Version"); case FieldValue: return data.version(); case FieldFrameValue: return QByteArray(1, char(data.version())); case FieldTextValue: return QString("%1").arg(data.version()); case FieldBitSize: return 3; default: break; } break; } case gre_protocol: { quint16 protocol = payloadProtocolId(ProtocolIdEth); switch(attrib) { case FieldName: return QString("Protocol"); case FieldValue: return protocol; case FieldFrameValue: { QByteArray fv; fv.resize(2); qToBigEndian(protocol, (uchar*) fv.data()); return fv; } case FieldTextValue: return QString("0x%1").arg( protocol, 4, BASE_HEX, QChar('0'));; default: break; } break; } case gre_checksum: { if (attrib == FieldName) return QString("Checksum"); if ((data.flags() & GRE_FLAG_CKSUM) == 0) { if (attrib == FieldTextValue) return QObject::tr(""); else return QVariant(); } if (attrib == FieldBitSize) return 16; quint16 cksum; if (data.is_override_checksum()) { cksum = data.checksum(); } else { quint32 sum = 0; cksum = protocolFrameCksum(streamIndex, CksumIp); sum += (quint16) ~cksum; cksum = protocolFramePayloadCksum(streamIndex, CksumIp); sum += (quint16) ~cksum; while (sum >> 16) sum = (sum & 0xFFFF) + (sum >> 16); cksum = (~sum) & 0xFFFF; } switch(attrib) { case FieldValue: return cksum; case FieldFrameValue: { QByteArray fv; fv.resize(2); qToBigEndian(cksum, (uchar*) fv.data()); return fv; } case FieldTextValue: return QString("0x%1").arg( cksum, 4, BASE_HEX, QChar('0'));; default: break; } break; } case gre_rsvd1: { if (attrib == FieldName) return QString("Reserved1"); if ((data.flags() & GRE_FLAG_CKSUM) == 0) { if (attrib == FieldTextValue) return QObject::tr(""); else return QVariant(); } switch(attrib) { case FieldValue: return data.rsvd1(); case FieldTextValue: return QString("%1").arg(data.rsvd1()); case FieldFrameValue: { QByteArray fv; fv.resize(2); qToBigEndian((quint16) data.rsvd1(), (uchar*) fv.data()); return fv; } default: break; } break; } case gre_key: { if (attrib == FieldName) return QString("Key"); if ((data.flags() & GRE_FLAG_KEY) == 0) { if (attrib == FieldTextValue) return QObject::tr(""); else return QVariant(); } switch(attrib) { case FieldValue: return data.key(); case FieldTextValue: return QString("0x%1").arg(data.key(), 8, BASE_HEX, QChar('0')); case FieldFrameValue: { QByteArray fv; fv.resize(4); qToBigEndian((quint32) data.key(), (uchar*) fv.data()); return fv; } default: break; } break; } case gre_sequence: { if (attrib == FieldName) return QString("Sequence Number"); if ((data.flags() & GRE_FLAG_SEQ) == 0) { if (attrib == FieldTextValue) return QObject::tr(""); else return QVariant(); } switch(attrib) { case FieldValue: return data.sequence_num(); case FieldTextValue: return QString("%1").arg(data.sequence_num()); case FieldFrameValue: { QByteArray fv; fv.resize(4); qToBigEndian((quint32) data.sequence_num(), (uchar*) fv.data()); return fv; } default: break; } break; } // Meta fields case gre_isOverrideChecksum: { switch(attrib) { case FieldValue: return data.is_override_checksum(); default: break; } break; } default: qFatal("%s: unimplemented case %d in switch", __PRETTY_FUNCTION__, index); break; } return AbstractProtocol::fieldData(index, attrib, streamIndex); } bool GreProtocol::setFieldData(int index, const QVariant &value, FieldAttrib attrib) { bool isOk = false; if (attrib != FieldValue) goto _exit; switch (index) { case gre_flags: { uint flags = value.toUInt(&isOk); if (isOk) data.set_flags(flags); break; } case gre_rsvd0: { uint rsvd0 = value.toUInt(&isOk); if (isOk) data.set_rsvd0(rsvd0); break; } case gre_version: { uint ver = value.toUInt(&isOk); if (isOk) data.set_version(ver); break; } case gre_protocol: { uint proto = value.toUInt(&isOk); if (isOk) data.set_protocol_type(proto); break; } case gre_checksum: { uint csum = value.toUInt(&isOk); if (isOk) data.set_checksum(csum); break; } case gre_isOverrideChecksum: { data.set_is_override_checksum(value.toBool()); break; } case gre_rsvd1: { uint rsvd1 = value.toUInt(&isOk); if (isOk) data.set_rsvd1(rsvd1); break; } case gre_key: { uint key = value.toUInt(&isOk); if (isOk) data.set_key(key); break; } case gre_sequence: { uint seq = value.toUInt(&isOk); if (isOk) data.set_sequence_num(seq); break; } default: qFatal("%s: unimplemented case %d in switch", __PRETTY_FUNCTION__, index); break; } _exit: return isOk; } int GreProtocol::protocolFrameSize(int /*streamIndex*/) const { int size = 4; // mandatory fields - flags, rsvd0, version, protocol if (data.flags() & GRE_FLAG_CKSUM) size += 4; if (data.flags() & GRE_FLAG_KEY) size += 4; if (data.flags() & GRE_FLAG_SEQ) size += 4; return size; } ostinato-1.3.0/common/gre.h000066400000000000000000000057511451413623100156010ustar00rootroot00000000000000/* Copyright (C) 2021 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _GRE_H #define _GRE_H #include "abstractprotocol.h" #include "gre.pb.h" /* GRE Protocol Frame Format (RFC2890)- 0 1 2 3 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ |C| |K|S| Reserved0 | Ver | Protocol Type | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Checksum (optional) | Reserved1 (Optional) | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Key (optional) | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Sequence Number (Optional) | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ Figures in brackets represent field width in bits */ #define GRE_FLAG_CKSUM 0x8 #define GRE_FLAG_KEY 0x2 #define GRE_FLAG_SEQ 0x1 class GreProtocol : public AbstractProtocol { public: enum grefield { // Frame Fields gre_flags = 0, gre_rsvd0, gre_version, gre_protocol, gre_checksum, gre_rsvd1, gre_key, gre_sequence, // Meta Fields gre_isOverrideChecksum, gre_fieldCount }; GreProtocol(StreamBase *stream, AbstractProtocol *parent = 0); virtual ~GreProtocol(); static AbstractProtocol* createInstance(StreamBase *stream, AbstractProtocol *parent = 0); virtual quint32 protocolNumber() const; virtual void protoDataCopyInto(OstProto::Protocol &protocol) const; virtual void protoDataCopyFrom(const OstProto::Protocol &protocol); virtual ProtocolIdType protocolIdType() const; virtual quint32 protocolId(ProtocolIdType type) const; virtual QString name() const; virtual QString shortName() const; virtual int fieldCount() const; virtual AbstractProtocol::FieldFlags fieldFlags(int index) const; virtual QVariant fieldData(int index, FieldAttrib attrib, int streamIndex = 0) const; virtual bool setFieldData(int index, const QVariant &value, FieldAttrib attrib = FieldValue); virtual int protocolFrameSize(int streamIndex = 0) const; private: OstProto::Gre data; }; #endif ostinato-1.3.0/common/gre.proto000066400000000000000000000022321451413623100165040ustar00rootroot00000000000000/* Copyright (C) 2021 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ import "protocol.proto"; package OstProto; // GRE Protocol message Gre { optional uint32 flags = 1 [default = 0xa]; optional uint32 rsvd0 = 2; optional uint32 version = 3; optional uint32 protocol_type = 4; optional uint32 checksum = 5; optional bool is_override_checksum = 6; optional uint32 rsvd1 = 7; optional uint32 key = 8 [default = 0x2020bad7]; optional uint32 sequence_num = 9; } extend Protocol { optional Gre gre = 405; } ostinato-1.3.0/common/gre.ui000066400000000000000000000105661451413623100157670ustar00rootroot00000000000000 Gre 0 0 264 140 Gre Version 0 (RFC2784) 7 Qt::Horizontal 40 20 Checksum false <auto> 0x -1 65535 -1 16 Key false Sequence No false Qt::Vertical 20 40 IntEdit QSpinBox
intedit.h
UIntEdit QLineEdit
uintedit.h
hasChecksum checksum hasKey key hasSequence sequence hasKey toggled(bool) key setEnabled(bool) 32 69 107 71 hasSequence toggled(bool) sequence setEnabled(bool) 75 99 125 97 hasChecksum toggled(bool) checksum setEnabled(bool) 87 43 109 45
ostinato-1.3.0/common/greconfig.cpp000066400000000000000000000061021451413623100173110ustar00rootroot00000000000000/* Copyright (C) 2021 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #include "greconfig.h" #include "gre.h" GreConfigForm::GreConfigForm(QWidget *parent) : AbstractProtocolConfigForm(parent) { setupUi(this); connect(hasChecksum, SIGNAL(clicked(bool)), this, SLOT(setAutoChecksum(bool))); } GreConfigForm::~GreConfigForm() { } GreConfigForm* GreConfigForm::createInstance() { return new GreConfigForm; } // Load widget contents from proto void GreConfigForm::loadWidget(AbstractProtocol *proto) { uint flags = proto->fieldData(GreProtocol::gre_flags, AbstractProtocol::FieldValue) .toUInt(); version->setValue( proto->fieldData( GreProtocol::gre_version, AbstractProtocol::FieldValue ).toUInt()); hasChecksum->setChecked(flags & GRE_FLAG_CKSUM); checksum->setValue( proto->fieldData( GreProtocol::gre_isOverrideChecksum, AbstractProtocol::FieldValue).toBool() ? proto->fieldData( GreProtocol::gre_checksum, AbstractProtocol::FieldValue).toUInt() : -1); hasKey->setChecked(flags & GRE_FLAG_KEY); key->setValue( proto->fieldData( GreProtocol::gre_key, AbstractProtocol::FieldValue ).toUInt()); hasSequence->setChecked(flags & GRE_FLAG_SEQ); sequence->setValue( proto->fieldData( GreProtocol::gre_sequence, AbstractProtocol::FieldValue ).toUInt()); } // Store widget contents into proto void GreConfigForm::storeWidget(AbstractProtocol *proto) { uint flags = 0; if (hasChecksum->isChecked()) flags |= GRE_FLAG_CKSUM; if (hasKey->isChecked()) flags |= GRE_FLAG_KEY; if (hasSequence->isChecked()) flags |= GRE_FLAG_SEQ; proto->setFieldData( GreProtocol::gre_flags, flags); proto->setFieldData( GreProtocol::gre_version, version->value()); proto->setFieldData( GreProtocol::gre_checksum, checksum->value()); proto->setFieldData( GreProtocol::gre_isOverrideChecksum, checksum->value() > -1 ? true: false); proto->setFieldData( GreProtocol::gre_key, key->value()); proto->setFieldData( GreProtocol::gre_sequence, sequence->value()); } void GreConfigForm::setAutoChecksum(bool enabled) { if (enabled) checksum->setValue(-1); // auto } ostinato-1.3.0/common/greconfig.h000066400000000000000000000022321451413623100167560ustar00rootroot00000000000000/* Copyright (C) 2010, 2014 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _GRE_CONFIG_H #define _GRE_CONFIG_H #include "abstractprotocolconfig.h" #include "ui_gre.h" class GreConfigForm : public AbstractProtocolConfigForm, private Ui::Gre { Q_OBJECT public: GreConfigForm(QWidget *parent = 0); virtual ~GreConfigForm(); static GreConfigForm* createInstance(); virtual void loadWidget(AbstractProtocol *proto); virtual void storeWidget(AbstractProtocol *proto); private slots: void setAutoChecksum(bool enabled); }; #endif ostinato-1.3.0/common/grepdml.cpp000066400000000000000000000042331451413623100170030ustar00rootroot00000000000000/* Copyright (C) 2021 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #include "grepdml.h" #include "gre.pb.h" PdmlGreProtocol::PdmlGreProtocol() { ostProtoId_ = OstProto::Protocol::kGreFieldNumber; fieldMap_.insert("gre.proto", OstProto::Gre::kProtocolTypeFieldNumber); fieldMap_.insert("gre.checksum", OstProto::Gre::kChecksumFieldNumber); fieldMap_.insert("gre.offset", OstProto::Gre::kRsvd1FieldNumber); fieldMap_.insert("gre.key", OstProto::Gre::kKeyFieldNumber); fieldMap_.insert("gre.sequence_number", OstProto::Gre::kSequenceNumFieldNumber); } PdmlGreProtocol::~PdmlGreProtocol() { } PdmlProtocol* PdmlGreProtocol::createInstance() { return new PdmlGreProtocol(); } void PdmlGreProtocol::postProtocolHandler(OstProto::Protocol* pbProto, OstProto::Stream* /*stream*/) { OstProto::Gre *gre = pbProto->MutableExtension(OstProto::gre); qDebug("GRE: post"); gre->set_is_override_checksum(overrideCksum_); return; } void PdmlGreProtocol::unknownFieldHandler(QString name, int /*pos*/, int /*size*/, const QXmlStreamAttributes& attributes, OstProto::Protocol* proto, OstProto::Stream* /*stream*/) { if (name == "gre.flags_and_version") { bool isOk; OstProto::Gre *gre = proto->MutableExtension(OstProto::gre); quint16 flagsAndVersion = attributes.value("value") .toUInt(&isOk, kBaseHex); gre->set_flags(flagsAndVersion >> 12); gre->set_rsvd0((flagsAndVersion & 0x0FFF) >> 3); gre->set_version(flagsAndVersion & 0x0007); } } ostinato-1.3.0/common/grepdml.h000066400000000000000000000025171451413623100164530ustar00rootroot00000000000000/* Copyright (C) 2021 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _GRE_PDML_H #define _GRE_PDML_H #include "pdmlprotocol.h" class PdmlGreProtocol : public PdmlProtocol { public: virtual ~PdmlGreProtocol(); static PdmlProtocol* createInstance(); virtual void postProtocolHandler(OstProto::Protocol *pbProto, OstProto::Stream *stream); void fieldHandler(QString name, const QXmlStreamAttributes &attributes, OstProto::Protocol *pbProto, OstProto::Stream *stream); virtual void unknownFieldHandler(QString name, int pos, int size, const QXmlStreamAttributes &attributes, OstProto::Protocol *pbProto, OstProto::Stream *stream); protected: PdmlGreProtocol(); }; #endif ostinato-1.3.0/common/hexdump.cpp000066400000000000000000000125541451413623100170300ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #include "hexdump.h" #include "streambase.h" HexDumpProtocol::HexDumpProtocol(StreamBase *stream, AbstractProtocol *parent) : AbstractProtocol(stream, parent) { } HexDumpProtocol::~HexDumpProtocol() { } AbstractProtocol* HexDumpProtocol::createInstance(StreamBase *stream, AbstractProtocol *parent) { return new HexDumpProtocol(stream, parent); } quint32 HexDumpProtocol::protocolNumber() const { return OstProto::Protocol::kHexDumpFieldNumber; } void HexDumpProtocol::protoDataCopyInto(OstProto::Protocol &protocol) const { protocol.MutableExtension(OstProto::hexDump)->CopyFrom(data); protocol.mutable_protocol_id()->set_id(protocolNumber()); } void HexDumpProtocol::protoDataCopyFrom(const OstProto::Protocol &protocol) { if (protocol.protocol_id().id() == protocolNumber() && protocol.HasExtension(OstProto::hexDump)) data.MergeFrom(protocol.GetExtension(OstProto::hexDump)); } QString HexDumpProtocol::name() const { return QString("HexDump"); } QString HexDumpProtocol::shortName() const { return QString("HexDump"); } int HexDumpProtocol::fieldCount() const { return hexDump_fieldCount; } AbstractProtocol::FieldFlags HexDumpProtocol::fieldFlags(int index) const { AbstractProtocol::FieldFlags flags; flags = AbstractProtocol::fieldFlags(index); switch (index) { case hexDump_content: flags |= FrameField; break; case hexDump_pad_until_end: flags &= ~FrameField; flags |= MetaField; break; default: qFatal("%s: unimplemented case %d in switch", __PRETTY_FUNCTION__, index); break; } return flags; } QVariant HexDumpProtocol::fieldData(int index, FieldAttrib attrib, int streamIndex) const { switch (index) { case hexDump_content: { QByteArray ba; QByteArray pad; switch(attrib) { case FieldValue: case FieldTextValue: case FieldFrameValue: ba.append(data.content().c_str(), data.content().length()); if (padUntilEnd()) { pad = QByteArray( protocolFrameSize(streamIndex) - ba.size(), '\0'); } break; default: break; } switch(attrib) { case FieldName: return QString("Content"); case FieldValue: return ba; case FieldTextValue: return ba.append(pad).toHex(); case FieldFrameValue: return ba.append(pad); default: break; } break; } // Meta fields case hexDump_pad_until_end: { switch(attrib) { case FieldValue: return padUntilEnd(); default: break; } break; } default: qFatal("%s: unimplemented case %d in switch", __PRETTY_FUNCTION__, index); break; } return AbstractProtocol::fieldData(index, attrib, streamIndex); } bool HexDumpProtocol::setFieldData(int index, const QVariant &value, FieldAttrib attrib) { bool isOk = false; if (attrib != FieldValue) goto _exit; switch (index) { case hexDump_content: { QByteArray ba = value.toByteArray(); data.set_content(ba.constData(), ba.size()); isOk = true; break; } case hexDump_pad_until_end: { bool pad = value.toBool(); data.set_pad_until_end(pad); isOk = true; break; } default: qFatal("%s: unimplemented case %d in switch", __PRETTY_FUNCTION__, index); break; } _exit: return isOk; } int HexDumpProtocol::protocolFrameSize(int streamIndex) const { int len = data.content().size(); if (padUntilEnd()) { int pad = mpStream->frameLen(streamIndex) - (protocolFrameOffset(streamIndex) + len + protocolFramePayloadSize(streamIndex) + kFcsSize); if (pad < 0) pad = 0; len += pad; } return len; } bool HexDumpProtocol::padUntilEnd() const { if (next) return false; // No padding if we are not the last protocol return data.pad_until_end(); } ostinato-1.3.0/common/hexdump.h000066400000000000000000000040401451413623100164640ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _HEXDUMP_H #define _HEXDUMP_H #include "abstractprotocol.h" #include "hexdump.pb.h" /* HexDump Protocol Frame Format - +---------+---------+ | User | Zero | | HexDump | Padding | +---------+---------+ */ class HexDumpProtocol : public AbstractProtocol { public: enum hexDumpfield { // Frame Fields hexDump_content = 0, // Meta Fields hexDump_pad_until_end, hexDump_fieldCount }; HexDumpProtocol(StreamBase *stream, AbstractProtocol *parent = 0); virtual ~HexDumpProtocol(); static AbstractProtocol* createInstance(StreamBase *stream, AbstractProtocol *parent = 0); virtual quint32 protocolNumber() const; virtual void protoDataCopyInto(OstProto::Protocol &protocol) const; virtual void protoDataCopyFrom(const OstProto::Protocol &protocol); virtual QString name() const; virtual QString shortName() const; virtual int fieldCount() const; virtual AbstractProtocol::FieldFlags fieldFlags(int index) const; virtual QVariant fieldData(int index, FieldAttrib attrib, int streamIndex = 0) const; virtual bool setFieldData(int index, const QVariant &value, FieldAttrib attrib = FieldValue); virtual int protocolFrameSize(int streamIndex = 0) const; private: bool padUntilEnd() const; OstProto::HexDump data; }; #endif ostinato-1.3.0/common/hexdump.proto000066400000000000000000000016061451413623100174050ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ import "protocol.proto"; package OstProto; // HexDump Protocol message HexDump { optional bytes content = 1; optional bool pad_until_end = 2 [default = true]; } extend Protocol { optional HexDump hexDump = 104; } ostinato-1.3.0/common/hexdump.ui000066400000000000000000000033761451413623100166650ustar00rootroot00000000000000 HexDump 0 0 511 190 Form Pad until end of packet Qt::Horizontal 281 20 50 0 QFrame::Panel QFrame::Sunken 1 Qt::AlignCenter QHexEdit QWidget
qhexedit.h
1
ostinato-1.3.0/common/hexdumpconfig.cpp000066400000000000000000000037071451413623100202160ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #include "hexdumpconfig.h" #include "hexdump.h" HexDumpConfigForm::HexDumpConfigForm(QWidget *parent) : AbstractProtocolConfigForm(parent) { setupUi(this); hexEdit->setFont(QFont("Courier")); hexEdit->setOverwriteMode(false); } HexDumpConfigForm::~HexDumpConfigForm() { } HexDumpConfigForm* HexDumpConfigForm::createInstance() { return new HexDumpConfigForm; } void HexDumpConfigForm::loadWidget(AbstractProtocol *proto) { hexEdit->setData( proto->fieldData( HexDumpProtocol::hexDump_content, AbstractProtocol::FieldValue ).toByteArray()); padUntilEnd->setChecked( proto->fieldData( HexDumpProtocol::hexDump_pad_until_end, AbstractProtocol::FieldValue ).toBool()); } void HexDumpConfigForm::storeWidget(AbstractProtocol *proto) { proto->setFieldData( HexDumpProtocol::hexDump_content, hexEdit->data()); proto->setFieldData( HexDumpProtocol::hexDump_pad_until_end, padUntilEnd->isChecked()); } // // ------------ private slots // void HexDumpConfigForm::on_hexEdit_overwriteModeChanged(bool isOverwriteMode) { if (isOverwriteMode) mode->setText(tr("Ovr")); else mode->setText(tr("Ins")); } ostinato-1.3.0/common/hexdumpconfig.h000066400000000000000000000023171451413623100176570ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _HEX_DUMP_CONFIG_H #define _HEX_DUMP_CONFIG_H #include "abstractprotocolconfig.h" #include "ui_hexdump.h" class HexDumpConfigForm : public AbstractProtocolConfigForm, private Ui::HexDump { Q_OBJECT public: HexDumpConfigForm(QWidget *parent = 0); virtual ~HexDumpConfigForm(); static HexDumpConfigForm* createInstance(); virtual void loadWidget(AbstractProtocol *proto); virtual void storeWidget(AbstractProtocol *proto); private slots: void on_hexEdit_overwriteModeChanged(bool isOverwriteMode); }; #endif ostinato-1.3.0/common/icmp.cpp000066400000000000000000000237061451413623100163070ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #include "icmp.h" #include "icmphelper.h" IcmpProtocol::IcmpProtocol(StreamBase *stream, AbstractProtocol *parent) : AbstractProtocol(stream, parent) { } IcmpProtocol::~IcmpProtocol() { // field count may change based on msgType - so don't cache field offsets _cacheFlags &= ~FieldFrameBitOffsetCache; } AbstractProtocol* IcmpProtocol::createInstance(StreamBase *stream, AbstractProtocol *parent) { return new IcmpProtocol(stream, parent); } quint32 IcmpProtocol::protocolNumber() const { return OstProto::Protocol::kIcmpFieldNumber; } void IcmpProtocol::protoDataCopyInto(OstProto::Protocol &protocol) const { protocol.MutableExtension(OstProto::icmp)->CopyFrom(data); protocol.mutable_protocol_id()->set_id(protocolNumber()); } void IcmpProtocol::protoDataCopyFrom(const OstProto::Protocol &protocol) { if (protocol.protocol_id().id() == protocolNumber() && protocol.HasExtension(OstProto::icmp)) data.MergeFrom(protocol.GetExtension(OstProto::icmp)); } QString IcmpProtocol::name() const { return QString("Internet Control Message Protocol"); } QString IcmpProtocol::shortName() const { return QString("ICMP"); } quint32 IcmpProtocol::protocolId(ProtocolIdType type) const { switch(type) { case ProtocolIdIp: switch(icmpVersion()) { case OstProto::Icmp::kIcmp4: return 0x1; case OstProto::Icmp::kIcmp6: return 0x3A; default:break; } default:break; } return AbstractProtocol::protocolId(type); } int IcmpProtocol::fieldCount() const { return icmp_fieldCount; } int IcmpProtocol::frameFieldCount() const { int count; if (isIdSeqType(icmpVersion(), icmpType())) count = icmp_idSeqFrameFieldCount; else count = icmp_commonFrameFieldCount; return count; } AbstractProtocol::FieldFlags IcmpProtocol::fieldFlags(int index) const { AbstractProtocol::FieldFlags flags; flags = AbstractProtocol::fieldFlags(index); switch (index) { case icmp_type: case icmp_code: break; case icmp_checksum: flags |= CksumField; break; case icmp_identifier: case icmp_sequence: if (!isIdSeqType(icmpVersion(), icmpType())) flags &= ~FrameField; break; case icmp_version: case icmp_is_override_checksum: flags &= ~FrameField; flags |= MetaField; break; default: qFatal("%s: unimplemented case %d in switch", __PRETTY_FUNCTION__, index); break; } return flags; } QVariant IcmpProtocol::fieldData(int index, FieldAttrib attrib, int streamIndex) const { switch (index) { case icmp_type: { unsigned char type = data.type() & 0xFF; switch(attrib) { case FieldName: return QString("Type"); case FieldValue: return type; case FieldTextValue: return QString("%1").arg((uint) type); case FieldFrameValue: return QByteArray(1, type); default: break; } break; } case icmp_code: { unsigned char code = data.code() & 0xFF; switch(attrib) { case FieldName: return QString("Code"); case FieldValue: return code; case FieldTextValue: return QString("%1").arg((uint)code); case FieldFrameValue: return QByteArray(1, code); default: break; } break; } case icmp_checksum: { quint16 cksum; switch(attrib) { case FieldValue: case FieldFrameValue: case FieldTextValue: if (data.is_override_checksum()) { cksum = data.checksum(); } else { cksum = (icmpVersion() == OstProto::Icmp::kIcmp4) ? AbstractProtocol::protocolFrameCksum( streamIndex, CksumIcmpIgmp) : AbstractProtocol::protocolFrameCksum( streamIndex, CksumTcpUdp); } break; default: cksum = 0; // avoid the 'maybe used unitialized' warning break; } switch(attrib) { case FieldName: return QString("Checksum"); case FieldValue: return cksum; case FieldFrameValue: { QByteArray fv; fv.resize(2); qToBigEndian(cksum, (uchar*) fv.data()); return fv; } case FieldTextValue: return QString("0x%1").arg( cksum, 4, BASE_HEX, QChar('0'));; case FieldBitSize: return 16; default: break; } break; } case icmp_identifier: { switch(attrib) { case FieldName: return QString("Identifier"); case FieldValue: return data.identifier(); case FieldTextValue: return QString("%1").arg(data.identifier()); case FieldFrameValue: { QByteArray fv; fv.resize(2); qToBigEndian((quint16) data.identifier(), (uchar*) fv.data()); return fv; } default: break; } break; } case icmp_sequence: { switch(attrib) { case FieldName: return QString("Sequence"); case FieldValue: return data.sequence(); case FieldTextValue: return QString("%1").arg(data.sequence()); case FieldFrameValue: { QByteArray fv; fv.resize(2); qToBigEndian((quint16) data.sequence(), (uchar*) fv.data()); return fv; } default: break; } break; } // Meta fields case icmp_version: { switch(attrib) { case FieldValue: return data.icmp_version(); default: break; } break; } case icmp_is_override_checksum: { switch(attrib) { case FieldValue: return data.is_override_checksum(); default: break; } break; } default: qFatal("%s: unimplemented case %d in switch", __PRETTY_FUNCTION__, index); break; } return AbstractProtocol::fieldData(index, attrib, streamIndex); } bool IcmpProtocol::setFieldData(int index, const QVariant &value, FieldAttrib attrib) { bool isOk = false; if (attrib != FieldValue) goto _exit; switch (index) { case icmp_type: { uint type = value.toUInt(&isOk); if (isOk) data.set_type(type & 0xFF); break; } case icmp_code: { uint code = value.toUInt(&isOk); if (isOk) data.set_code(code & 0xFF); break; } case icmp_checksum: { uint csum = value.toUInt(&isOk); if (isOk) data.set_checksum(csum); break; } case icmp_identifier: { uint id = value.toUInt(&isOk); if (isOk) data.set_identifier(id); break; } case icmp_sequence: { uint seq = value.toUInt(&isOk); if (isOk) data.set_sequence(seq); break; } case icmp_version: { int ver = value.toUInt(&isOk); if (isOk) data.set_icmp_version(OstProto::Icmp::Version(ver)); break; } case icmp_is_override_checksum: { bool ovr = value.toBool(); data.set_is_override_checksum(ovr); isOk = true; break; } default: qFatal("%s: unimplemented case %d in switch", __PRETTY_FUNCTION__, index); break; } _exit: return isOk; } ostinato-1.3.0/common/icmp.h000066400000000000000000000052371451413623100157530ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _ICMP_H #define _ICMP_H #include "abstractprotocol.h" #include "icmp.pb.h" /* Icmp Protocol Frame Format - +-----+------+------+------+-------+ | TYP | CODE | CSUM | [ID] | [SEQ] | | (1) | (1) | (2) | (2) | (2) | +-----+------+------+------+-------+ Fields within [] are applicable only to certain TYPEs Figures in braces represent field width in bytes */ class IcmpProtocol : public AbstractProtocol { public: enum icmpfield { // Frame Fields icmp_type = 0, icmp_code, icmp_checksum, icmp_commonFrameFieldCount, icmp_identifier = icmp_commonFrameFieldCount, icmp_sequence, icmp_idSeqFrameFieldCount, // Meta Fields icmp_is_override_checksum = icmp_idSeqFrameFieldCount, icmp_version, icmp_fieldCount }; IcmpProtocol(StreamBase *stream, AbstractProtocol *parent = 0); virtual ~IcmpProtocol(); static AbstractProtocol* createInstance(StreamBase *stream, AbstractProtocol *parent = 0); virtual quint32 protocolNumber() const; virtual void protoDataCopyInto(OstProto::Protocol &protocol) const; virtual void protoDataCopyFrom(const OstProto::Protocol &protocol); virtual quint32 protocolId(ProtocolIdType type) const; virtual QString name() const; virtual QString shortName() const; virtual int fieldCount() const; virtual int frameFieldCount() const; virtual AbstractProtocol::FieldFlags fieldFlags(int index) const; virtual QVariant fieldData(int index, FieldAttrib attrib, int streamIndex = 0) const; virtual bool setFieldData(int index, const QVariant &value, FieldAttrib attrib = FieldValue); private: OstProto::Icmp data; OstProto::Icmp::Version icmpVersion() const { return OstProto::Icmp::Version( fieldData(icmp_version, FieldValue).toUInt()); } int icmpType() const { return fieldData(icmp_type, FieldValue).toInt(); } }; #endif ostinato-1.3.0/common/icmp.proto000066400000000000000000000022461451413623100166640ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ import "protocol.proto"; package OstProto; // Icmp Protocol message Icmp { enum Version { kIcmp4 = 4; kIcmp6 = 6; } optional Version icmp_version = 1 [default = kIcmp4]; optional bool is_override_checksum = 2; optional uint32 type = 6 [default = 0x8]; // echo request optional uint32 code = 7; optional uint32 checksum = 8; optional uint32 identifier = 9 [default = 1234]; optional uint32 sequence = 10; } extend Protocol { optional Icmp icmp = 402; } ostinato-1.3.0/common/icmp.ui000066400000000000000000000103741451413623100161370ustar00rootroot00000000000000 Icmp 0 0 373 166 Form Version ICMPv4 ICMPv6 Type typeCombo Code codeEdit Qt::Horizontal 31 20 Checksum false Identifier idEdit Sequence seqEdit Qt::Vertical 211 71 IntComboBox QComboBox
intcombobox.h
icmp4Button icmp6Button typeCombo codeEdit overrideCksum cksumEdit idEdit seqEdit overrideCksum toggled(bool) cksumEdit setEnabled(bool) 33 70 96 71
ostinato-1.3.0/common/icmp6pdml.cpp000066400000000000000000000054721451413623100172520ustar00rootroot00000000000000/* Copyright (C) 2011 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #include "icmp6pdml.h" #include "icmp.pb.h" #include "sample.pb.h" PdmlIcmp6Protocol::PdmlIcmp6Protocol() { ostProtoId_ = OstProto::Protocol::kSampleFieldNumber; proto_ = NULL; } PdmlProtocol* PdmlIcmp6Protocol::createInstance() { return new PdmlIcmp6Protocol(); } void PdmlIcmp6Protocol::preProtocolHandler(QString name, const QXmlStreamAttributes &attributes, int expectedPos, OstProto::Protocol *pbProto, OstProto::Stream *stream) { proto_ = NULL; ostProtoId_ = OstProto::Protocol::kSampleFieldNumber; icmp_.preProtocolHandler(name, attributes, expectedPos, pbProto, stream); mld_.preProtocolHandler(name, attributes, expectedPos, pbProto, stream); } void PdmlIcmp6Protocol::postProtocolHandler(OstProto::Protocol *pbProto, OstProto::Stream *stream) { if (proto_) proto_->postProtocolHandler(pbProto, stream); else stream->mutable_protocol()->RemoveLast(); proto_ = NULL; ostProtoId_ = OstProto::Protocol::kSampleFieldNumber; } void PdmlIcmp6Protocol::unknownFieldHandler(QString name, int pos, int size, const QXmlStreamAttributes &attributes, OstProto::Protocol *pbProto, OstProto::Stream *stream) { if (proto_) { proto_->unknownFieldHandler(name, pos, size, attributes, pbProto, stream); } else if (name == "icmpv6.type") { bool isOk; uint type = attributes.value("value").toString().toUInt( &isOk, kBaseHex); if (((type >= 130) && (type <= 132)) || (type == 143)) { // MLD proto_ = &mld_; fieldMap_ = mld_.fieldMap_; ostProtoId_ = OstProto::Protocol::kMldFieldNumber; } else { // ICMP proto_ = &icmp_; fieldMap_ = icmp_.fieldMap_; ostProtoId_ = OstProto::Protocol::kIcmpFieldNumber; } pbProto->mutable_protocol_id()->set_id(ostProtoId_); pbProto->MutableExtension(OstProto::sample)->Clear(); fieldHandler(name, attributes, pbProto, stream); } else { qDebug("unexpected field %s", qPrintable(name)); } } ostinato-1.3.0/common/icmp6pdml.h000066400000000000000000000027521451413623100167150ustar00rootroot00000000000000/* Copyright (C) 2011 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _ICMP6_PDML_H #define _ICMP6_PDML_H #include "pdmlprotocol.h" #include "icmppdml.h" #include "mldpdml.h" class PdmlIcmp6Protocol : public PdmlProtocol { public: static PdmlProtocol* createInstance(); virtual void preProtocolHandler(QString name, const QXmlStreamAttributes &attributes, int expectedPos, OstProto::Protocol *pbProto, OstProto::Stream *stream); virtual void postProtocolHandler(OstProto::Protocol *pbProto, OstProto::Stream *stream); virtual void unknownFieldHandler(QString name, int pos, int size, const QXmlStreamAttributes &attributes, OstProto::Protocol *pbProto, OstProto::Stream *stream); protected: PdmlIcmp6Protocol(); private: PdmlIcmpProtocol icmp_; PdmlMldProtocol mld_; PdmlProtocol *proto_; }; #endif ostinato-1.3.0/common/icmpconfig.cpp000066400000000000000000000142051451413623100174670ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #include "icmpconfig.h" #include "icmp.h" #include "icmphelper.h" #include IcmpConfigForm::IcmpConfigForm(QWidget *parent) : AbstractProtocolConfigForm(parent) { versionGroup = new QButtonGroup(this); setupUi(this); // auto-connect's not working, for some reason I can't figure out! // slot name changed to when_ instead of on_ so that connectSlotsByName() // doesn't complain connect(versionGroup, SIGNAL(buttonClicked(int)), SLOT(when_versionGroup_buttonClicked(int))); versionGroup->addButton(icmp4Button, OstProto::Icmp::kIcmp4); versionGroup->addButton(icmp6Button, OstProto::Icmp::kIcmp6); typeCombo->setValidator(new QIntValidator(0, 0xFF, this)); icmp4Button->click(); idEdit->setValidator(new QIntValidator(0, 0xFFFF, this)); seqEdit->setValidator(new QIntValidator(0, 0xFFFF, this)); } IcmpConfigForm::~IcmpConfigForm() { } IcmpConfigForm* IcmpConfigForm::createInstance() { return new IcmpConfigForm; } void IcmpConfigForm::loadWidget(AbstractProtocol *proto) { versionGroup->button( proto->fieldData( IcmpProtocol::icmp_version, AbstractProtocol::FieldValue ).toUInt())->click(); typeCombo->setValue( proto->fieldData( IcmpProtocol::icmp_type, AbstractProtocol::FieldValue ).toUInt()); codeEdit->setText( proto->fieldData( IcmpProtocol::icmp_code, AbstractProtocol::FieldValue ).toString()); overrideCksum->setChecked( proto->fieldData( IcmpProtocol::icmp_is_override_checksum, AbstractProtocol::FieldValue ).toBool()); cksumEdit->setText(uintToHexStr( proto->fieldData( IcmpProtocol::icmp_checksum, AbstractProtocol::FieldValue ).toUInt(), 2)); idEdit->setText( proto->fieldData( IcmpProtocol::icmp_identifier, AbstractProtocol::FieldValue ).toString()); seqEdit->setText( proto->fieldData( IcmpProtocol::icmp_sequence, AbstractProtocol::FieldValue ).toString()); } void IcmpConfigForm::storeWidget(AbstractProtocol *proto) { proto->setFieldData( IcmpProtocol::icmp_version, versionGroup->checkedId()); proto->setFieldData( IcmpProtocol::icmp_type, typeCombo->currentValue()); proto->setFieldData( IcmpProtocol::icmp_code, codeEdit->text()); proto->setFieldData( IcmpProtocol::icmp_is_override_checksum, overrideCksum->isChecked()); proto->setFieldData( IcmpProtocol::icmp_checksum, hexStrToUInt(cksumEdit->text())); proto->setFieldData( IcmpProtocol::icmp_identifier, idEdit->text()); proto->setFieldData( IcmpProtocol::icmp_sequence, seqEdit->text()); } // // -------- private slots // void IcmpConfigForm::on_typeCombo_currentIndexChanged(int /*index*/) { idSeqFrame->setVisible( isIdSeqType( OstProto::Icmp::Version(versionGroup->checkedId()), typeCombo->currentValue())); } void IcmpConfigForm::when_versionGroup_buttonClicked(int id) { int value = typeCombo->currentValue(); typeCombo->clear(); switch(id) { case OstProto::Icmp::kIcmp4: typeCombo->addItem(kIcmpEchoReply, "Echo Reply"); typeCombo->addItem(kIcmpDestinationUnreachable, "Destination Unreachable"); typeCombo->addItem(kIcmpSourceQuench, "Source Quench"); typeCombo->addItem(kIcmpRedirect, "Redirect"); typeCombo->addItem(kIcmpEchoRequest, "Echo Request"); typeCombo->addItem(kIcmpTimeExceeded, "Time Exceeded"); typeCombo->addItem(kIcmpParameterProblem, "Parameter Problem"); typeCombo->addItem(kIcmpTimestampRequest, "Timestamp Request"); typeCombo->addItem(kIcmpTimestampReply, "Timestamp Reply"); typeCombo->addItem(kIcmpInformationRequest, "Information Request"); typeCombo->addItem(kIcmpInformationReply, "Information Reply"); typeCombo->addItem(kIcmpAddressMaskRequest, "Address Mask Request"); typeCombo->addItem(kIcmpAddressMaskReply, "Address Mask Reply"); break; case OstProto::Icmp::kIcmp6: typeCombo->addItem(kIcmp6DestinationUnreachable, "Destination Unreachable"); typeCombo->addItem(kIcmp6PacketTooBig, "Packet Too Big"); typeCombo->addItem(kIcmp6TimeExceeded, "Time Exceeded"); typeCombo->addItem(kIcmp6ParameterProblem, "Parameter Problem"); typeCombo->addItem(kIcmp6EchoRequest, "Echo Request"); typeCombo->addItem(kIcmp6EchoReply, "Echo Reply"); typeCombo->addItem(kIcmp6RouterSolicitation, "Router Solicitation"); typeCombo->addItem(kIcmp6RouterAdvertisement, "Router Advertisement"); typeCombo->addItem(kIcmp6NeighbourSolicitation, "Neighbour Solicitation"); typeCombo->addItem(kIcmp6NeighbourAdvertisement, "Neighbour Advertisement"); typeCombo->addItem(kIcmp6Redirect, "Redirect"); typeCombo->addItem(kIcmp6InformationQuery, "Information Query"); typeCombo->addItem(kIcmp6InformationResponse, "Information Response"); break; default: Q_ASSERT(false); } typeCombo->setValue(value); } ostinato-1.3.0/common/icmpconfig.h000066400000000000000000000024341451413623100171350ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _ICMP_CONFIG_H #define _ICMP_CONFIG_H #include "abstractprotocolconfig.h" #include "ui_icmp.h" class QButtonGroup; class IcmpConfigForm : public AbstractProtocolConfigForm, private Ui::Icmp { Q_OBJECT public: IcmpConfigForm(QWidget *parent = 0); virtual ~IcmpConfigForm(); static IcmpConfigForm* createInstance(); virtual void loadWidget(AbstractProtocol *proto); virtual void storeWidget(AbstractProtocol *proto); private: QButtonGroup *versionGroup; private slots: void on_typeCombo_currentIndexChanged(int index); void when_versionGroup_buttonClicked(int id); }; #endif ostinato-1.3.0/common/icmphelper.h000066400000000000000000000047131451413623100171510ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _ICMP_HELPER_H #define _ICMP_HELPER_H #include "icmp.pb.h" #include enum IcmpType { kIcmpEchoReply = 0, kIcmpDestinationUnreachable = 3, kIcmpSourceQuench = 4, kIcmpRedirect = 5, kIcmpEchoRequest = 8, kIcmpTimeExceeded = 11, kIcmpParameterProblem = 12, kIcmpTimestampRequest = 13, kIcmpTimestampReply = 14, kIcmpInformationRequest = 15, kIcmpInformationReply = 16, kIcmpAddressMaskRequest = 17, kIcmpAddressMaskReply = 18 }; enum Icmp6Type { kIcmp6DestinationUnreachable = 1, kIcmp6PacketTooBig = 2, kIcmp6TimeExceeded = 3, kIcmp6ParameterProblem = 4, kIcmp6EchoRequest = 128, kIcmp6EchoReply = 129, kIcmp6RouterSolicitation = 133, kIcmp6RouterAdvertisement = 134, kIcmp6NeighbourSolicitation = 135, kIcmp6NeighbourAdvertisement = 136, kIcmp6Redirect = 137, kIcmp6InformationQuery = 139, kIcmp6InformationResponse = 140 }; static QSet icmpIdSeqSet = QSet() << kIcmpEchoRequest << kIcmpEchoReply << kIcmpInformationRequest << kIcmpInformationReply; static QSet icmp6IdSeqSet = QSet() << kIcmp6EchoRequest << kIcmp6EchoReply; bool inline isIdSeqType(OstProto::Icmp::Version ver, int type) { //qDebug("%s: ver = %d, type = %d", __FUNCTION__, ver, type); switch(ver) { case OstProto::Icmp::kIcmp4: return icmpIdSeqSet.contains(type); case OstProto::Icmp::kIcmp6: return icmp6IdSeqSet.contains(type); default: break; } Q_ASSERT(false); // unreachable return false; } #endif ostinato-1.3.0/common/icmppdml.cpp000066400000000000000000000065041451413623100171610ustar00rootroot00000000000000/* Copyright (C) 2011 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #include "icmppdml.h" #include "icmp.pb.h" PdmlIcmpProtocol::PdmlIcmpProtocol() { ostProtoId_ = OstProto::Protocol::kIcmpFieldNumber; fieldMap_.insert("icmp.type", OstProto::Icmp::kTypeFieldNumber); fieldMap_.insert("icmp.code", OstProto::Icmp::kCodeFieldNumber); fieldMap_.insert("icmp.checksum", OstProto::Icmp::kChecksumFieldNumber); fieldMap_.insert("icmp.ident", OstProto::Icmp::kIdentifierFieldNumber); fieldMap_.insert("icmp.seq", OstProto::Icmp::kSequenceFieldNumber); fieldMap_.insert("icmpv6.type", OstProto::Icmp::kTypeFieldNumber); fieldMap_.insert("icmpv6.code", OstProto::Icmp::kCodeFieldNumber); fieldMap_.insert("icmpv6.checksum", OstProto::Icmp::kChecksumFieldNumber); fieldMap_.insert("icmpv6.echo.identifier", OstProto::Icmp::kIdentifierFieldNumber); fieldMap_.insert("icmpv6.echo.sequence_number", OstProto::Icmp::kSequenceFieldNumber); } PdmlProtocol* PdmlIcmpProtocol::createInstance() { return new PdmlIcmpProtocol(); } void PdmlIcmpProtocol::preProtocolHandler(QString name, const QXmlStreamAttributes& /*attributes*/, int /*expectedPos*/, OstProto::Protocol *pbProto, OstProto::Stream* /*stream*/) { OstProto::Icmp *icmp = pbProto->MutableExtension(OstProto::icmp); if (name == "icmp") icmp->set_icmp_version(OstProto::Icmp::kIcmp4); else if (name == "icmpv6") icmp->set_icmp_version(OstProto::Icmp::kIcmp6); icmp->set_is_override_checksum(overrideCksum_); icmp->set_type(kIcmpInvalidType); } void PdmlIcmpProtocol::unknownFieldHandler(QString /*name*/, int /*pos*/, int /*size*/, const QXmlStreamAttributes &attributes, OstProto::Protocol *pbProto, OstProto::Stream* /*stream*/) { bool isOk; OstProto::Icmp *icmp = pbProto->MutableExtension(OstProto::icmp); if ((icmp->icmp_version() == OstProto::Icmp::kIcmp6) && (icmp->type() >= kIcmp6EchoRequest) && (icmp->type() <= kIcmp6EchoReply)) { QString addrHexStr = attributes.value("value").toString(); // Wireshark 1.4.x does not have these as filterable fields if (attributes.value("show").toString().startsWith("ID")) icmp->set_identifier(addrHexStr.toUInt(&isOk, kBaseHex)); else if (attributes.value("show").toString().startsWith("Sequence")) icmp->set_sequence(addrHexStr.toUInt(&isOk, kBaseHex)); } } void PdmlIcmpProtocol::postProtocolHandler(OstProto::Protocol *pbProto, OstProto::Stream *stream) { OstProto::Icmp *icmp = pbProto->MutableExtension(OstProto::icmp); if (icmp->type() == kIcmpInvalidType) stream->mutable_protocol()->RemoveLast(); } ostinato-1.3.0/common/icmppdml.h000066400000000000000000000030421451413623100166200ustar00rootroot00000000000000/* Copyright (C) 2011 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _ICMP_PDML_H #define _ICMP_PDML_H #include "pdmlprotocol.h" class PdmlIcmpProtocol : public PdmlProtocol { friend class PdmlIcmp6Protocol; public: static PdmlProtocol* createInstance(); virtual void preProtocolHandler(QString name, const QXmlStreamAttributes &attributes, int expectedPos, OstProto::Protocol *pbProto, OstProto::Stream *stream); virtual void unknownFieldHandler(QString name, int pos, int size, const QXmlStreamAttributes &attributes, OstProto::Protocol *pbProto, OstProto::Stream *stream); virtual void postProtocolHandler(OstProto::Protocol *pbProto, OstProto::Stream *stream); protected: PdmlIcmpProtocol(); private: static const uint kIcmpInvalidType = 0xFFFFFFFF; static const uint kIcmp6EchoRequest = 128; static const uint kIcmp6EchoReply = 129; }; #endif ostinato-1.3.0/common/igmp.cpp000066400000000000000000000246521451413623100163140ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #include "igmp.h" #include "iputils.h" #include #include IgmpProtocol::IgmpProtocol(StreamBase *stream, AbstractProtocol *parent) : GmpProtocol(stream, parent) { _hasPayload = false; data.set_type(kIgmpV2Query); } IgmpProtocol::~IgmpProtocol() { } AbstractProtocol* IgmpProtocol::createInstance(StreamBase *stream, AbstractProtocol *parent) { return new IgmpProtocol(stream, parent); } quint32 IgmpProtocol::protocolNumber() const { return OstProto::Protocol::kIgmpFieldNumber; } void IgmpProtocol::protoDataCopyInto(OstProto::Protocol &protocol) const { protocol.MutableExtension(OstProto::igmp)->CopyFrom(data); protocol.mutable_protocol_id()->set_id(protocolNumber()); } void IgmpProtocol::protoDataCopyFrom(const OstProto::Protocol &protocol) { if (protocol.protocol_id().id() == protocolNumber() && protocol.HasExtension(OstProto::igmp)) data.MergeFrom(protocol.GetExtension(OstProto::igmp)); } QString IgmpProtocol::name() const { return QString("Internet Group Management Protocol"); } QString IgmpProtocol::shortName() const { return QString("IGMP"); } quint32 IgmpProtocol::protocolId(ProtocolIdType type) const { switch(type) { case ProtocolIdIp: return 0x2; default:break; } return AbstractProtocol::protocolId(type); } QVariant IgmpProtocol::fieldData(int index, FieldAttrib attrib, int streamIndex) const { switch (index) { case kRsvdMrtCode: { uint mrt = 0; quint8 mrcode = 0; if (msgType() == kIgmpV3Query) { mrt = data.max_response_time(); mrcode = quint8(mrc(mrt)); } else if (msgType() == kIgmpV2Query) { mrt = data.max_response_time(); mrcode = mrt & 0xFF; } switch(attrib) { case FieldName: if (isQuery()) return QString("Max Response Time"); else return QString("Reserved"); case FieldValue: return mrt; case FieldTextValue: return QString("%1").arg(mrt); case FieldFrameValue: return QByteArray(1, mrcode); default: break; } break; } case kGroupAddress: { quint32 grpIp = ipUtils::ipAddress( data.group_address().v4(), data.group_prefix(), ipUtils::AddrMode(data.group_mode()), data.group_count(), streamIndex); switch(attrib) { case FieldName: return QString("Group Address"); case FieldValue: return grpIp; case FieldTextValue: return QHostAddress(grpIp).toString(); case FieldFrameValue: { QByteArray fv; fv.resize(4); qToBigEndian(grpIp, (uchar*) fv.data()); return fv; } default: break; } break; } case kSources: { switch(attrib) { case FieldName: return QString("Source List"); case FieldValue: { QStringList list; for (int i = 0; i < data.sources_size(); i++) list.append(QHostAddress(data.sources(i).v4()).toString()); return list; } case FieldFrameValue: { QByteArray fv; fv.resize(4 * data.sources_size()); for (int i = 0; i < data.sources_size(); i++) qToBigEndian(data.sources(i).v4(), (uchar*)(fv.data()+4*i)); return fv; } case FieldTextValue: { QStringList list; for (int i = 0; i < data.sources_size(); i++) list.append(QHostAddress(data.sources(i).v4()).toString()); return list.join(", "); } default: break; } break; } case kGroupRecords: { switch(attrib) { case FieldValue: { QVariantList grpRecords = GmpProtocol::fieldData( index, attrib, streamIndex).toList(); for (int i = 0; i < data.group_records_size(); i++) { QVariantMap grpRec = grpRecords.at(i).toMap(); OstProto::Gmp::GroupRecord rec = data.group_records(i); grpRec["groupRecordAddress"] = QHostAddress( rec.group_address().v4()).toString(); QStringList sl; for (int j = 0; j < rec.sources_size(); j++) sl.append(QHostAddress(rec.sources(j).v4()).toString()); grpRec["groupRecordSourceList"] = sl; grpRecords.replace(i, grpRec); } return grpRecords; } case FieldFrameValue: { QVariantList list = GmpProtocol::fieldData( index, attrib, streamIndex).toList(); QByteArray fv; for (int i = 0; i < data.group_records_size(); i++) { OstProto::Gmp::GroupRecord rec = data.group_records(i); QByteArray rv = list.at(i).toByteArray(); rv.insert(4, QByteArray(4+4*rec.sources_size(), char(0))); qToBigEndian(rec.group_address().v4(), (uchar*)(rv.data()+4)); for (int j = 0; j < rec.sources_size(); j++) { qToBigEndian(rec.sources(j).v4(), (uchar*)(rv.data()+8+4*j)); } fv.append(rv); } return fv; } case FieldTextValue: { QStringList list = GmpProtocol::fieldData( index, attrib, streamIndex).toStringList(); for (int i = 0; i < data.group_records_size(); i++) { OstProto::Gmp::GroupRecord rec = data.group_records(i); QString recStr = list.at(i); QString str; str.append(QString("Group: %1").arg( QHostAddress(rec.group_address().v4()).toString())); str.append("; Sources: "); QStringList sl; for (int j = 0; j < rec.sources_size(); j++) sl.append(QHostAddress(rec.sources(j).v4()).toString()); str.append(sl.join(", ")); recStr.replace("XXX", str); list.replace(i, recStr); } return list.join("\n").insert(0, "\n"); } default: break; } break; } default: break; } return GmpProtocol::fieldData(index, attrib, streamIndex); } bool IgmpProtocol::setFieldData(int index, const QVariant &value, FieldAttrib attrib) { bool isOk = false; if (attrib != FieldValue) goto _exit; switch (index) { case kRsvdMrtCode: { uint mrt = value.toUInt(&isOk); if (isOk) data.set_max_response_time(mrt); break; } case kGroupAddress: { quint32 ip = value.toUInt(&isOk); if (isOk) { data.mutable_group_address()->set_v4(ip); break; } QHostAddress addr(value.toString()); ip = addr.toIPv4Address(); isOk = (addr.protocol() == QAbstractSocket::IPv4Protocol); if (isOk) data.mutable_group_address()->set_v4(ip); break; } case kSources: { QStringList list = value.toStringList(); data.clear_sources(); foreach(QString str, list) { quint32 ip = QHostAddress(str).toIPv4Address(); data.add_sources()->set_v4(ip); } isOk = true; break; } case kGroupRecords: { GmpProtocol::setFieldData(index, value, attrib); QVariantList list = value.toList(); for (int i = 0; i < list.count(); i++) { QVariantMap grpRec = list.at(i).toMap(); OstProto::Gmp::GroupRecord *rec = data.mutable_group_records(i); rec->mutable_group_address()->set_v4(QHostAddress( grpRec["groupRecordAddress"].toString()) .toIPv4Address()); QStringList srcList = grpRec["groupRecordSourceList"] .toStringList(); rec->clear_sources(); foreach (QString src, srcList) { rec->add_sources()->set_v4( QHostAddress(src).toIPv4Address()); } } isOk = true; break; } default: isOk = GmpProtocol::setFieldData(index, value, attrib); break; } _exit: return isOk; } quint16 IgmpProtocol::checksum(int streamIndex) const { return AbstractProtocol::protocolFrameCksum(streamIndex, CksumIcmpIgmp); } ostinato-1.3.0/common/igmp.h000066400000000000000000000051431451413623100157530ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _IGMP_H #define _IGMP_H #include "gmp.h" #include "igmp.pb.h" // IGMP uses the same msg type value for 'Query' messages across // versions despite the fields being different. To distinguish // Query messages of different versions, we use an additional // upper byte enum IgmpMsgType { kIgmpV1Query = 0x11, kIgmpV1Report = 0x12, kIgmpV2Query = 0xFF11, kIgmpV2Report = 0x16, kIgmpV2Leave = 0x17, kIgmpV3Query = 0xFE11, kIgmpV3Report = 0x22, }; class IgmpProtocol : public GmpProtocol { public: IgmpProtocol(StreamBase *stream, AbstractProtocol *parent = 0); virtual ~IgmpProtocol(); static AbstractProtocol* createInstance(StreamBase *stream, AbstractProtocol *parent = 0); virtual quint32 protocolNumber() const; virtual void protoDataCopyInto(OstProto::Protocol &protocol) const; virtual void protoDataCopyFrom(const OstProto::Protocol &protocol); virtual quint32 protocolId(ProtocolIdType type) const; virtual QString name() const; virtual QString shortName() const; virtual QVariant fieldData(int index, FieldAttrib attrib, int streamIndex = 0) const; virtual bool setFieldData(int index, const QVariant &value, FieldAttrib attrib = FieldValue); protected: virtual bool isSsmReport() const; virtual bool isQuery() const; virtual bool isSsmQuery() const; virtual quint16 checksum(int streamIndex) const; private: int mrc(int value) const; }; inline bool IgmpProtocol::isSsmReport() const { return (msgType() == kIgmpV3Report); } inline bool IgmpProtocol::isQuery() const { return ((msgType() == kIgmpV1Query) || (msgType() == kIgmpV2Query) || (msgType() == kIgmpV3Query)); } inline bool IgmpProtocol::isSsmQuery() const { return (msgType() == kIgmpV3Query); } inline int IgmpProtocol::mrc(int value) const { return quint8(value); // TODO: if value > 128, convert to mantissa/exp form } #endif ostinato-1.3.0/common/igmp.proto000077500000000000000000000014241451413623100166700ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ import "protocol.proto"; import "gmp.proto"; package OstProto; extend Protocol { optional Gmp igmp = 403; } ostinato-1.3.0/common/igmpconfig.cpp000066400000000000000000000060141451413623100174720ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #include "igmpconfig.h" #include "igmp.h" #include "ipv4addressdelegate.h" IgmpConfigForm::IgmpConfigForm(QWidget *parent) : GmpConfigForm(parent) { connect(msgTypeCombo, SIGNAL(currentIndexChanged(int)), SLOT(on_msgTypeCombo_currentIndexChanged(int))); msgTypeCombo->setValueMask(0xFF); msgTypeCombo->addItem(kIgmpV1Query, "IGMPv1 Query"); msgTypeCombo->addItem(kIgmpV1Report, "IGMPv1 Report"); msgTypeCombo->addItem(kIgmpV2Query, "IGMPv2 Query"); msgTypeCombo->addItem(kIgmpV2Report, "IGMPv2 Report"); msgTypeCombo->addItem(kIgmpV2Leave, "IGMPv2 Leave"); msgTypeCombo->addItem(kIgmpV3Query, "IGMPv3 Query"); msgTypeCombo->addItem(kIgmpV3Report, "IGMPv3 Report"); _defaultGroupIp = "0.0.0.0"; _defaultSourceIp = "0.0.0.0"; groupAddress->setInputMask("009.009.009.009;"); // FIXME: use validator groupRecordAddress->setInputMask("009.009.009.009;"); // FIXME:use validator sourceList->setItemDelegate(new IPv4AddressDelegate(this)); groupRecordSourceList->setItemDelegate(new IPv4AddressDelegate(this)); } IgmpConfigForm::~IgmpConfigForm() { } IgmpConfigForm* IgmpConfigForm::createInstance() { return new IgmpConfigForm; } void IgmpConfigForm::loadWidget(AbstractProtocol *proto) { GmpConfigForm::loadWidget(proto); maxResponseTime->setText( proto->fieldData( IgmpProtocol::kRsvdMrtCode, AbstractProtocol::FieldValue ).toString()); } void IgmpConfigForm::storeWidget(AbstractProtocol *proto) { GmpConfigForm::storeWidget(proto); proto->setFieldData( IgmpProtocol::kRsvdMrtCode, maxResponseTime->text()); } // // -- private slots // void IgmpConfigForm::on_msgTypeCombo_currentIndexChanged(int /*index*/) { switch(msgTypeCombo->currentValue()) { case kIgmpV1Query: case kIgmpV1Report: case kIgmpV2Query: case kIgmpV2Report: case kIgmpV2Leave: asmGroup->show(); ssmWidget->hide(); break; case kIgmpV3Query: asmGroup->show(); ssmWidget->setCurrentIndex(kSsmQueryPage); ssmWidget->show(); break; case kIgmpV3Report: asmGroup->hide(); ssmWidget->setCurrentIndex(kSsmReportPage); ssmWidget->show(); break; default: asmGroup->hide(); ssmWidget->hide(); break; } } ostinato-1.3.0/common/igmpconfig.h000066400000000000000000000021441451413623100171370ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _IGMP_CONFIG_H #define _IGMP_CONFIG_H #include "gmpconfig.h" class IgmpConfigForm : public GmpConfigForm { Q_OBJECT public: IgmpConfigForm(QWidget *parent = 0); virtual ~IgmpConfigForm(); static IgmpConfigForm* createInstance(); virtual void loadWidget(AbstractProtocol *proto); virtual void storeWidget(AbstractProtocol *proto); private slots: void on_msgTypeCombo_currentIndexChanged(int index); }; #endif ostinato-1.3.0/common/igmppdml.cpp000066400000000000000000000114431451413623100171630ustar00rootroot00000000000000/* Copyright (C) 2011 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #include "igmppdml.h" #include "igmp.pb.h" PdmlIgmpProtocol::PdmlIgmpProtocol() { ostProtoId_ = OstProto::Protocol::kIgmpFieldNumber; fieldMap_.insert("igmp.max_resp", OstProto::Gmp::kMaxResponseTimeFieldNumber); // FIXME fieldMap_.insert("igmp.checksum", OstProto::Gmp::kChecksumFieldNumber); fieldMap_.insert("igmp.s", OstProto::Gmp::kSFlagFieldNumber); fieldMap_.insert("igmp.qrv", OstProto::Gmp::kQrvFieldNumber); fieldMap_.insert("igmp.qqic", OstProto::Gmp::kQqiFieldNumber); // FIXME fieldMap_.insert("igmp.num_grp_recs", OstProto::Gmp::kGroupRecordCountFieldNumber); } PdmlProtocol* PdmlIgmpProtocol::createInstance() { return new PdmlIgmpProtocol(); } void PdmlIgmpProtocol::preProtocolHandler(QString /*name*/, const QXmlStreamAttributes& /*attributes*/, int /*expectedPos*/, OstProto::Protocol *pbProto, OstProto::Stream* /*stream*/) { OstProto::Gmp *igmp = pbProto->MutableExtension(OstProto::igmp); igmp->set_is_override_rsvd_code(true); igmp->set_is_override_checksum(overrideCksum_); igmp->set_is_override_source_count(true); igmp->set_is_override_group_record_count(true); version_ = 0; } void PdmlIgmpProtocol::unknownFieldHandler(QString name, int /*pos*/, int /*size*/, const QXmlStreamAttributes &attributes, OstProto::Protocol *pbProto, OstProto::Stream* /*stream*/) { bool isOk; OstProto::Gmp *igmp = pbProto->MutableExtension(OstProto::igmp); QString valueHexStr = attributes.value("value").toString(); if (name == "igmp.version") { version_ = attributes.value("show").toString().toUInt(&isOk); } else if (name == "igmp.type") { uint type = valueHexStr.toUInt(&isOk, kBaseHex); if (type == kIgmpQuery) { switch(version_) { case 1: type = kIgmpV1Query; break; case 2: type = kIgmpV2Query; break; case 3: type = kIgmpV3Query; break; } } igmp->set_type(type); } else if (name == "igmp.record_type") { OstProto::Gmp::GroupRecord *rec = igmp->add_group_records(); rec->set_type(OstProto::Gmp::GroupRecord::RecordType( valueHexStr.toUInt(&isOk, kBaseHex))); rec->set_is_override_source_count(true); rec->set_is_override_aux_data_length(true); } else if (name == "igmp.aux_data_len") { igmp->mutable_group_records(igmp->group_records_size() - 1)-> set_aux_data_length(valueHexStr.toUInt(&isOk, kBaseHex)); } else if (name == "igmp.num_src") { if (igmp->group_record_count()) igmp->mutable_group_records(igmp->group_records_size() - 1)-> set_source_count(valueHexStr.toUInt(&isOk, kBaseHex)); else igmp->set_source_count(valueHexStr.toUInt(&isOk, kBaseHex)); } else if (name == "igmp.maddr") { if (igmp->group_record_count()) igmp->mutable_group_records(igmp->group_records_size() - 1)-> mutable_group_address()->set_v4( valueHexStr.toUInt(&isOk, kBaseHex)); else igmp->mutable_group_address()->set_v4( valueHexStr.toUInt(&isOk, kBaseHex)); } else if (name == "igmp.saddr") { if (igmp->group_record_count()) igmp->mutable_group_records(igmp->group_records_size() - 1)-> add_sources()->set_v4(valueHexStr.toUInt(&isOk, kBaseHex)); else igmp->add_sources()->set_v4(valueHexStr.toUInt(&isOk, kBaseHex)); } else if (name == "igmp.aux_data") { QByteArray ba = QByteArray::fromHex( attributes.value("value").toString().toUtf8()); igmp->mutable_group_records(igmp->group_records_size() - 1)-> set_aux_data(ba.constData(), ba.size()); } } void PdmlIgmpProtocol::postProtocolHandler(OstProto::Protocol* /*pbProto*/, OstProto::Stream *stream) { // version is 0 for IGMP like protocols such as RGMP which we don't // support currently if (version_ == 0) stream->mutable_protocol()->RemoveLast(); } ostinato-1.3.0/common/igmppdml.h000066400000000000000000000030521451413623100166250ustar00rootroot00000000000000/* Copyright (C) 2011 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _IGMP_PDML_H #define _IGMP_PDML_H #include "pdmlprotocol.h" class PdmlIgmpProtocol : public PdmlProtocol { public: static PdmlProtocol* createInstance(); virtual void preProtocolHandler(QString name, const QXmlStreamAttributes &attributes, int expectedPos, OstProto::Protocol *pbProto, OstProto::Stream *stream); virtual void unknownFieldHandler(QString name, int pos, int size, const QXmlStreamAttributes &attributes, OstProto::Protocol *pbProto, OstProto::Stream *stream); virtual void postProtocolHandler(OstProto::Protocol *pbProto, OstProto::Stream *stream); protected: PdmlIgmpProtocol(); private: static const uint kIgmpQuery = 0x11; static const uint kIgmpV1Query = 0x11; static const uint kIgmpV2Query = 0xFF11; static const uint kIgmpV3Query = 0xFE11; uint version_; }; #endif ostinato-1.3.0/common/intcombobox.h000066400000000000000000000032701451413623100173410ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef __INT_COMBO_BOX #define __INT_COMBO_BOX #include class IntComboBox : public QComboBox { public: IntComboBox(QWidget *parent = 0) : QComboBox(parent) { valueMask_ = 0xFFFFFFFF; setEditable(true); } void addItem(int value, const QString &text) { QComboBox::addItem( QString("%1 - %2").arg(value & valueMask_).arg(text), value); } int currentValue() { bool isOk; int index = findText(currentText()); if (index >= 0) return itemData(index).toInt(); else return currentText().toInt(&isOk, 0); } void setValue(int value) { int index = findData(value); if (index >= 0) setCurrentIndex(index); else setEditText(QString().setNum(value)); } uint valueMask() { return valueMask_; } void setValueMask(uint mask) { valueMask_ = mask; } private: uint valueMask_; }; #endif ostinato-1.3.0/common/intedit.h000066400000000000000000000017441451413623100164620ustar00rootroot00000000000000/* Copyright (C) 2016 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _INT_EDIT_H #define _INT_EDIT_H #include #include class IntEdit: public QSpinBox { public: IntEdit(QWidget *parent = 0); }; inline IntEdit::IntEdit(QWidget *parent) : QSpinBox(parent) { setRange(INT_MIN, INT_MAX); setButtonSymbols(QAbstractSpinBox::NoButtons); } #endif ostinato-1.3.0/common/ip4.cpp000066400000000000000000000647751451413623100160660ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #include "ip4.h" #include Ip4Protocol::Ip4Protocol(StreamBase *stream, AbstractProtocol *parent) : AbstractProtocol(stream, parent) { } Ip4Protocol::~Ip4Protocol() { } AbstractProtocol* Ip4Protocol::createInstance(StreamBase *stream, AbstractProtocol *parent) { return new Ip4Protocol(stream, parent); } quint32 Ip4Protocol::protocolNumber() const { return OstProto::Protocol::kIp4FieldNumber; } void Ip4Protocol::protoDataCopyInto(OstProto::Protocol &protocol) const { protocol.MutableExtension(OstProto::ip4)->CopyFrom(data); protocol.mutable_protocol_id()->set_id(protocolNumber()); } void Ip4Protocol::protoDataCopyFrom(const OstProto::Protocol &protocol) { if (protocol.protocol_id().id() == protocolNumber() && protocol.HasExtension(OstProto::ip4)) data.MergeFrom(protocol.GetExtension(OstProto::ip4)); } QString Ip4Protocol::name() const { return QString("Internet Protocol ver 4"); } QString Ip4Protocol::shortName() const { return QString("IPv4"); } AbstractProtocol::ProtocolIdType Ip4Protocol::protocolIdType() const { return ProtocolIdIp; } quint32 Ip4Protocol::protocolId(ProtocolIdType type) const { switch(type) { case ProtocolIdLlc: return 0x060603; case ProtocolIdEth: return 0x0800; case ProtocolIdIp: return 0x04; default:break; } return AbstractProtocol::protocolId(type); } int Ip4Protocol::fieldCount() const { return ip4_fieldCount; } AbstractProtocol::FieldFlags Ip4Protocol::fieldFlags(int index) const { AbstractProtocol::FieldFlags flags; flags = AbstractProtocol::fieldFlags(index); switch (index) { case ip4_ver: case ip4_hdrLen: case ip4_tos: case ip4_totLen: case ip4_id: case ip4_flags: case ip4_fragOfs: case ip4_ttl: case ip4_proto: break; case ip4_cksum: flags |= CksumField; break; case ip4_srcAddr: case ip4_dstAddr: case ip4_options: break; case ip4_isOverrideVer: case ip4_isOverrideHdrLen: case ip4_isOverrideTotLen: case ip4_isOverrideProto: case ip4_isOverrideCksum: case ip4_srcAddrMode: case ip4_srcAddrCount: case ip4_srcAddrMask: case ip4_dstAddrMode: case ip4_dstAddrCount: case ip4_dstAddrMask: flags &= ~FrameField; flags |= MetaField; break; default: break; } return flags; } QVariant Ip4Protocol::fieldData(int index, FieldAttrib attrib, int streamIndex) const { switch (index) { case ip4_ver: { int ver; ver = data.is_override_ver() ? (data.ver_hdrlen() >> 4) & 0x0F : 4; switch(attrib) { case FieldName: return QString("Version"); case FieldValue: return ver; case FieldTextValue: return QString("%1").arg(ver, 1, BASE_HEX, QChar('0')); case FieldFrameValue: return QByteArray(1, (char) ver); case FieldBitSize: return 4; default: break; } break; } case ip4_hdrLen: { int hdrlen; hdrlen = data.is_override_hdrlen() ? data.ver_hdrlen() : 5 + data.options().length()/4; hdrlen &= 0x0F; switch(attrib) { case FieldName: return QString("Header Length"); case FieldValue: return hdrlen; case FieldTextValue: return QString("%1").arg(hdrlen, 1, BASE_HEX, QChar('0')); case FieldFrameValue: return QByteArray(1, (char) hdrlen); case FieldBitSize: return 4; default: break; } break; } case ip4_tos: switch(attrib) { case FieldName: return QString("TOS/DSCP"); case FieldValue: return data.tos(); case FieldFrameValue: return QByteArray(1, (char) data.tos()); case FieldTextValue: return QString("0x%1"). arg(data.tos(), 2, BASE_HEX, QChar('0'));; default: break; } break; case ip4_totLen: { int ipLen = 20 + data.options().length(); switch(attrib) { case FieldName: return QString("Total Length"); case FieldValue: { int totlen; totlen = data.is_override_totlen() ? data.totlen() : (protocolFramePayloadSize(streamIndex) + ipLen); return totlen; } case FieldFrameValue: { QByteArray fv; int totlen; totlen = data.is_override_totlen() ? data.totlen() : (protocolFramePayloadSize(streamIndex) + ipLen); fv.resize(2); qToBigEndian((quint16) totlen, (uchar*) fv.data()); return fv; } case FieldTextValue: { int totlen; totlen = data.is_override_totlen() ? data.totlen() : (protocolFramePayloadSize(streamIndex) + ipLen); return QString("%1").arg(totlen); } case FieldBitSize: return 16; default: break; } break; } case ip4_id: switch(attrib) { case FieldName: return QString("Identification"); case FieldValue: return data.id(); case FieldFrameValue: { QByteArray fv; fv.resize(2); qToBigEndian((quint16) data.id(), (uchar*)fv.data()); return fv; } case FieldTextValue: return QString("0x%1"). arg(data.id(), 2, BASE_HEX, QChar('0'));; default: break; } break; case ip4_flags: switch(attrib) { case FieldName: return QString("Flags"); case FieldValue: return data.flags(); case FieldFrameValue: return QByteArray(1, (char) data.flags()); case FieldTextValue: { QString s; s.append("Unused:"); s.append(data.flags() & IP_FLAG_UNUSED ? "1" : "0"); s.append(" Don't Fragment:"); s.append(data.flags() & IP_FLAG_DF ? "1" : "0"); s.append(" More Fragments:"); s.append(data.flags() & IP_FLAG_MF ? "1" : "0"); return s; } case FieldBitSize: return 3; default: break; } break; case ip4_fragOfs: switch(attrib) { case FieldName: return QString("Fragment Offset"); case FieldValue: return data.frag_ofs(); case FieldFrameValue: { QByteArray fv; fv.resize(2); qToBigEndian((quint16) (data.frag_ofs()), (uchar*) fv.data()); return fv; } case FieldTextValue: return QString("%1").arg(data.frag_ofs()*8); case FieldBitSize: return 13; default: break; } break; case ip4_ttl: switch(attrib) { case FieldName: return QString("Time to Live"); case FieldValue: return data.ttl(); case FieldFrameValue: return QByteArray(1, (char)data.ttl()); case FieldTextValue: return QString("%1").arg(data.ttl()); default: break; } break; case ip4_proto: { switch(attrib) { case FieldName: return QString("Protocol"); case FieldValue: { unsigned char id = data.is_override_proto() ? data.proto() : payloadProtocolId(ProtocolIdIp); return id; } case FieldFrameValue: { unsigned char id = data.is_override_proto() ? data.proto() : payloadProtocolId(ProtocolIdIp); return QByteArray(1, (char) id); } case FieldTextValue: { unsigned char id = data.is_override_proto() ? data.proto() : payloadProtocolId(ProtocolIdIp); return QString("0x%1"). arg(id, 2, BASE_HEX, QChar('0')); } default: break; } break; } case ip4_cksum: { switch(attrib) { case FieldName: return QString("Header Checksum"); case FieldValue: { quint16 cksum; if (data.is_override_cksum()) cksum = data.cksum(); else cksum = protocolFrameCksum(streamIndex, CksumIp); return cksum; } case FieldFrameValue: { QByteArray fv; quint16 cksum; if (data.is_override_cksum()) cksum = data.cksum(); else cksum = protocolFrameCksum(streamIndex, CksumIp); fv.resize(2); qToBigEndian((quint16) cksum, (uchar*) fv.data()); return fv; } case FieldTextValue: { quint16 cksum; if (data.is_override_cksum()) cksum = data.cksum(); else cksum = protocolFrameCksum(streamIndex, CksumIp); return QString("0x%1"). arg(cksum, 4, BASE_HEX, QChar('0'));; } case FieldBitSize: return 16; default: break; } break; } case ip4_srcAddr: { int u; quint32 subnet, host, srcIp = 0; switch(data.src_ip_mode()) { case OstProto::Ip4::e_im_fixed: srcIp = data.src_ip(); break; case OstProto::Ip4::e_im_inc_host: u = streamIndex % data.src_ip_count(); subnet = data.src_ip() & data.src_ip_mask(); host = (((data.src_ip() & ~data.src_ip_mask()) + u) & ~data.src_ip_mask()); srcIp = subnet | host; break; case OstProto::Ip4::e_im_dec_host: u = streamIndex % data.src_ip_count(); subnet = data.src_ip() & data.src_ip_mask(); host = (((data.src_ip() & ~data.src_ip_mask()) - u) & ~data.src_ip_mask()); srcIp = subnet | host; break; case OstProto::Ip4::e_im_random_host: subnet = data.src_ip() & data.src_ip_mask(); host = (qrand() & ~data.src_ip_mask()); srcIp = subnet | host; break; default: qWarning("Unhandled src_ip_mode = %d", data.src_ip_mode()); } switch(attrib) { case FieldName: return QString("Source"); case FieldValue: return srcIp; case FieldFrameValue: { QByteArray fv; fv.resize(4); qToBigEndian(srcIp, (uchar*) fv.data()); return fv; } case FieldTextValue: return QHostAddress(srcIp).toString(); default: break; } break; } case ip4_dstAddr: { int u; quint32 subnet, host, dstIp = 0; switch(data.dst_ip_mode()) { case OstProto::Ip4::e_im_fixed: dstIp = data.dst_ip(); break; case OstProto::Ip4::e_im_inc_host: u = streamIndex % data.dst_ip_count(); subnet = data.dst_ip() & data.dst_ip_mask(); host = (((data.dst_ip() & ~data.dst_ip_mask()) + u) & ~data.dst_ip_mask()); dstIp = subnet | host; break; case OstProto::Ip4::e_im_dec_host: u = streamIndex % data.dst_ip_count(); subnet = data.dst_ip() & data.dst_ip_mask(); host = (((data.dst_ip() & ~data.dst_ip_mask()) - u) & ~data.dst_ip_mask()); dstIp = subnet | host; break; case OstProto::Ip4::e_im_random_host: subnet = data.dst_ip() & data.dst_ip_mask(); host = (qrand() & ~data.dst_ip_mask()); dstIp = subnet | host; break; default: qWarning("Unhandled dst_ip_mode = %d", data.dst_ip_mode()); } switch(attrib) { case FieldName: return QString("Destination"); case FieldValue: return dstIp; case FieldFrameValue: { QByteArray fv; fv.resize(4); qToBigEndian((quint32) dstIp, (uchar*) fv.data()); return fv; } case FieldTextValue: return QHostAddress(dstIp).toString(); default: break; } break; } case ip4_options: { QByteArray ba; switch(attrib) { case FieldValue: case FieldFrameValue: case FieldTextValue: ba.append(data.options().c_str(), data.options().length()); default: break; } switch(attrib) { case FieldName: return QString("Options"); case FieldValue: case FieldFrameValue: return ba; case FieldTextValue: return ba.toHex(); default: break; } break; } // Meta fields case ip4_isOverrideVer: switch(attrib) { case FieldValue: return data.is_override_ver(); default: break; } break; case ip4_isOverrideHdrLen: switch(attrib) { case FieldValue: return data.is_override_hdrlen(); default: break; } break; case ip4_isOverrideTotLen: switch(attrib) { case FieldValue: return data.is_override_totlen(); default: break; } break; case ip4_isOverrideProto: switch(attrib) { case FieldValue: return data.is_override_proto(); default: break; } break; case ip4_isOverrideCksum: switch(attrib) { case FieldValue: return data.is_override_cksum(); default: break; } break; case ip4_srcAddrMode: switch(attrib) { case FieldValue: return data.src_ip_mode(); default: break; } break; case ip4_srcAddrCount: switch(attrib) { case FieldValue: return data.src_ip_count(); default: break; } break; case ip4_srcAddrMask: switch(attrib) { case FieldValue: return data.src_ip_mask(); default: break; } break; case ip4_dstAddrMode: switch(attrib) { case FieldValue: return data.dst_ip_mode(); default: break; } break; case ip4_dstAddrCount: switch(attrib) { case FieldValue: return data.dst_ip_count(); default: break; } break; case ip4_dstAddrMask: switch(attrib) { case FieldValue: return data.dst_ip_mask(); default: break; } break; default: break; } return AbstractProtocol::fieldData(index, attrib, streamIndex); } bool Ip4Protocol::setFieldData(int index, const QVariant &value, FieldAttrib attrib) { bool isOk = false; if (attrib != FieldValue) goto _exit; switch (index) { case ip4_ver: { uint version = value.toUInt(&isOk); if (isOk) data.set_ver_hdrlen( ((version & 0xF) << 4) | (data.ver_hdrlen() & 0x0F)); break; } case ip4_hdrLen: { uint hdrLen = value.toUInt(&isOk); if (isOk) data.set_ver_hdrlen( (data.ver_hdrlen() & 0xF0) | (hdrLen & 0x0F)); break; } case ip4_tos: { uint tos = value.toUInt(&isOk); if (isOk) data.set_tos(tos); break; } case ip4_totLen: { uint totLen = value.toUInt(&isOk); if (isOk) data.set_totlen(totLen); break; } case ip4_id: { uint id = value.toUInt(&isOk); if (isOk) data.set_id(id); break; } case ip4_flags: { uint flags = value.toUInt(&isOk); if (isOk) data.set_flags(flags); break; } case ip4_fragOfs: { uint fragOfs = value.toUInt(&isOk); if (isOk) data.set_frag_ofs(fragOfs); break; } case ip4_ttl: { uint ttl = value.toUInt(&isOk); if (isOk) data.set_ttl(ttl); break; } case ip4_proto: { uint proto = value.toUInt(&isOk); if (isOk) data.set_proto(proto); break; } case ip4_cksum: { uint cksum = value.toUInt(&isOk); if (isOk) data.set_cksum(cksum); break; } case ip4_srcAddr: { quint32 srcIp = value.toUInt(&isOk); if (isOk) data.set_src_ip(srcIp); break; } case ip4_dstAddr: { quint32 dstIp = value.toUInt(&isOk); if (isOk) data.set_dst_ip(dstIp); break; } case ip4_options: { QByteArray ba = value.toByteArray(); int pad = (4 - (ba.size() % 4)) % 4; if (pad) ba.append(QByteArray(pad, 0)); data.set_options(ba.constData(), ba.size()); isOk = true; break; } // Meta-fields case ip4_isOverrideVer: { bool ovr = value.toBool(); data.set_is_override_ver(ovr); isOk = true; break; } case ip4_isOverrideHdrLen: { bool ovr = value.toBool(); data.set_is_override_hdrlen(ovr); isOk = true; break; } case ip4_isOverrideTotLen: { bool ovr = value.toBool(); data.set_is_override_totlen(ovr); isOk = true; break; } case ip4_isOverrideProto: { bool ovr = value.toBool(); data.set_is_override_proto(ovr); isOk = true; break; } case ip4_isOverrideCksum: { bool ovr = value.toBool(); data.set_is_override_cksum(ovr); isOk = true; break; } case ip4_srcAddrMode: { uint mode = value.toUInt(&isOk); if (isOk && data.IpAddrMode_IsValid(mode)) data.set_src_ip_mode(OstProto::Ip4::IpAddrMode(mode)); else isOk = false; break; } case ip4_srcAddrCount: { uint count = value.toUInt(&isOk); if (isOk) data.set_src_ip_count(count); break; } case ip4_srcAddrMask: { quint32 mask = value.toUInt(&isOk); if (isOk) data.set_src_ip_mask(mask); break; } case ip4_dstAddrMode: { uint mode = value.toUInt(&isOk); if (isOk && data.IpAddrMode_IsValid(mode)) data.set_dst_ip_mode(OstProto::Ip4::IpAddrMode(mode)); else isOk = false; break; } case ip4_dstAddrCount: { uint count = value.toUInt(&isOk); if (isOk) data.set_dst_ip_count(count); break; } case ip4_dstAddrMask: { quint32 mask = value.toUInt(&isOk); if (isOk) data.set_dst_ip_mask(mask); break; } default: qFatal("%s: unimplemented case %d in switch", __PRETTY_FUNCTION__, index); break; } _exit: return isOk; } int Ip4Protocol::protocolFrameVariableCount() const { int count = AbstractProtocol::protocolFrameVariableCount(); if (data.src_ip_mode() != OstProto::Ip4::e_im_fixed) count = AbstractProtocol::lcm(count, data.src_ip_count()); if (data.dst_ip_mode() != OstProto::Ip4::e_im_fixed) count = AbstractProtocol::lcm(count, data.dst_ip_count()); return count; } quint32 Ip4Protocol::protocolFrameCksum(int streamIndex, CksumType cksumType, CksumFlags cksumFlags) const { switch (cksumType) { case CksumIpPseudo: { quint32 sum = 0; QByteArray fv = protocolFrameValue(streamIndex); const quint8 *p = (quint8*) fv.constData(); sum += *((quint16*)(p + 12)); // src-ip hi sum += *((quint16*)(p + 14)); // src-ip lo sum += *((quint16*)(p + 16)); // dst-ip hi sum += *((quint16*)(p + 18)); // dst-ip lo // XXX: payload length and protocol are also part of the // pseudo cksum but for IPv6 we need to skip extension headers to // get to them, so these two fields are counted in the // pseudo cksum in AbstractProtocol::protocolFrameHeaderCksum() // Although not needed for IPv4 case, we do the same for IPv4 // also, so that code there is common for IPv4 and IPv6 while(sum>>16) sum = (sum & 0xFFFF) + (sum >> 16); return qFromBigEndian((quint16) ~sum); } default: break; } return AbstractProtocol::protocolFrameCksum( streamIndex, cksumType, cksumFlags); } bool Ip4Protocol::hasErrors(QStringList *errors) const { bool result = false; if ((data.dst_ip() == 0) && (data.dst_ip_mode() == OstProto::Ip4::e_im_fixed)) { if (errors) *errors << QObject::tr("Frames with Destination IP 0.0.0.0 " "are likely to be dropped"); result = true; } if ((data.src_ip() == 0) && (data.src_ip_mode() == OstProto::Ip4::e_im_fixed)) { if (errors) *errors << QObject::tr("Frames with Source IP 0.0.0.0 " "may be dropped except for special cases " "like BOOTP/DHCP"); result = true; } return result; } ostinato-1.3.0/common/ip4.h000066400000000000000000000052551451413623100155170ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _IPV4_H #define _IPV4_H #include "abstractprotocol.h" #include "ip4.pb.h" #define IP_FLAG_MF 0x1 #define IP_FLAG_DF 0x2 #define IP_FLAG_UNUSED 0x4 class Ip4Protocol : public AbstractProtocol { public: enum ip4field { ip4_ver = 0, ip4_hdrLen, ip4_tos, ip4_totLen, ip4_id, ip4_flags, ip4_fragOfs, ip4_ttl, ip4_proto, ip4_cksum, ip4_srcAddr, ip4_dstAddr, ip4_options, // Meta-fields ip4_isOverrideVer, ip4_isOverrideHdrLen, ip4_isOverrideTotLen, ip4_isOverrideProto, ip4_isOverrideCksum, ip4_srcAddrMode, ip4_srcAddrCount, ip4_srcAddrMask, ip4_dstAddrMode, ip4_dstAddrCount, ip4_dstAddrMask, ip4_fieldCount }; Ip4Protocol(StreamBase *stream, AbstractProtocol *parent = 0); virtual ~Ip4Protocol(); static AbstractProtocol* createInstance(StreamBase *stream, AbstractProtocol *parent = 0); virtual quint32 protocolNumber() const; virtual void protoDataCopyInto(OstProto::Protocol &protocol) const; virtual void protoDataCopyFrom(const OstProto::Protocol &protocol); virtual QString name() const; virtual QString shortName() const; virtual ProtocolIdType protocolIdType() const; virtual quint32 protocolId(ProtocolIdType type) const; virtual int fieldCount() const; virtual AbstractProtocol::FieldFlags fieldFlags(int index) const; virtual QVariant fieldData(int index, FieldAttrib attrib, int streamIndex = 0) const; virtual bool setFieldData(int index, const QVariant &value, FieldAttrib attrib = FieldValue); virtual int protocolFrameVariableCount() const; virtual quint32 protocolFrameCksum(int streamIndex = 0, CksumType cksumType = CksumIp, CksumFlags cksumFlags = 0) const; virtual bool hasErrors(QStringList *errors = nullptr) const; private: OstProto::Ip4 data; }; #endif ostinato-1.3.0/common/ip4.proto000066400000000000000000000040021451413623100164200ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ import "protocol.proto"; package OstProto; // IPv4 message Ip4 { enum IpAddrMode { e_im_fixed = 0; e_im_inc_host = 1; e_im_dec_host = 2; e_im_random_host = 3; } optional bool is_override_ver = 1; optional bool is_override_hdrlen = 2; optional bool is_override_totlen = 3; optional bool is_override_proto = 30; optional bool is_override_cksum = 4; optional uint32 ver_hdrlen = 5 [default = 0x45]; optional uint32 tos = 6; optional uint32 totlen = 7; optional uint32 id = 8 [default = 1234]; optional uint32 flags = 9; optional uint32 frag_ofs = 10; optional uint32 ttl = 11 [default = 127]; optional uint32 proto = 12; optional uint32 cksum = 13; // Source IP optional fixed32 src_ip = 14; optional IpAddrMode src_ip_mode = 15 [default = e_im_fixed]; optional uint32 src_ip_count = 16 [default = 16]; optional fixed32 src_ip_mask = 17 [default = 0xFFFFFF00]; // Destination IP optional fixed32 dst_ip = 18; optional IpAddrMode dst_ip_mode = 19 [default = e_im_fixed]; optional uint32 dst_ip_count = 20 [default = 16]; optional fixed32 dst_ip_mask = 21 [default = 0xFFFFFF00]; optional bytes options = 22; } extend Protocol { optional Ip4 ip4 = 301; } ostinato-1.3.0/common/ip4.ui000066400000000000000000000306131451413623100157010ustar00rootroot00000000000000 ip4 0 0 507 308 Form Override Version false Override Header Length (x4) false Override Length false Identification >HH HH Fragment Offset (x8) Don't Fragment More Fragments Time To Live (TTL) false >HH Override Checksum false >HH HH Override Protocol false Qt::Horizontal 101 20 Mode Count Mask Source Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter Fixed Increment Host Decrement Host Random Host false false Destination Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter Fixed Increment Host Decrement Host Random Host false false Options false ... Qt::Vertical 20 40 TosDscpWidget QWidget
tosdscp.h
1
cbIpVersionOverride leIpVersion cbIpHdrLenOverride leIpHdrLen tosDscp cbIpLengthOverride leIpLength leIpId leIpFragOfs cbIpFlagsDf cbIpFlagsMf leIpTtl cbIpProtocolOverride leIpProto cbIpCksumOverride leIpCksum leIpSrcAddr cmbIpSrcAddrMode leIpSrcAddrCount leIpSrcAddrMask leIpDstAddr cmbIpDstAddrMode leIpDstAddrCount leIpDstAddrMask leIpOptions tbIpOptionsEdit cbIpVersionOverride toggled(bool) leIpVersion setEnabled(bool) 108 11 195 11 cbIpHdrLenOverride toggled(bool) leIpHdrLen setEnabled(bool) 113 67 166 43 cbIpLengthOverride toggled(bool) leIpLength setEnabled(bool) 89 118 236 119 cbIpCksumOverride toggled(bool) leIpCksum setEnabled(bool) 387 140 406 122 cbIpProtocolOverride toggled(bool) leIpProto setEnabled(bool) 363 95 398 94
ostinato-1.3.0/common/ip4config.cpp000066400000000000000000000232551451413623100172400ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #include "ip4config.h" #include "ip4.h" #include "ipv4addressvalidator.h" #include Ip4ConfigForm::Ip4ConfigForm(QWidget *parent) : AbstractProtocolConfigForm(parent) { setupUi(this); leIpVersion->setValidator(new QIntValidator(0, 15, this)); leIpOptions->setValidator(new QRegExpValidator(QRegExp("[0-9a-fA-F]*"), this)); leIpSrcAddr->setValidator(new IPv4AddressValidator(this)); leIpSrcAddrMask->setValidator(new IPv4AddressValidator(this)); leIpDstAddr->setValidator(new IPv4AddressValidator(this)); leIpDstAddrMask->setValidator(new IPv4AddressValidator(this)); connect(cmbIpSrcAddrMode, SIGNAL(currentIndexChanged(int)), this, SLOT(on_cmbIpSrcAddrMode_currentIndexChanged(int))); connect(cmbIpDstAddrMode, SIGNAL(currentIndexChanged(int)), this, SLOT(on_cmbIpDstAddrMode_currentIndexChanged(int))); } Ip4ConfigForm::~Ip4ConfigForm() { } Ip4ConfigForm* Ip4ConfigForm::createInstance() { return new Ip4ConfigForm; } void Ip4ConfigForm::loadWidget(AbstractProtocol *proto) { cbIpVersionOverride->setChecked( proto->fieldData( Ip4Protocol::ip4_isOverrideVer, AbstractProtocol::FieldValue ).toBool()); leIpVersion->setText( proto->fieldData( Ip4Protocol::ip4_ver, AbstractProtocol::FieldValue ).toString()); cbIpHdrLenOverride->setChecked( proto->fieldData( Ip4Protocol::ip4_isOverrideHdrLen, AbstractProtocol::FieldValue ).toBool()); leIpHdrLen->setText( proto->fieldData( Ip4Protocol::ip4_hdrLen, AbstractProtocol::FieldValue ).toString()); tosDscp->setValue( proto->fieldData( Ip4Protocol::ip4_tos, AbstractProtocol::FieldValue ).toUInt()); cbIpLengthOverride->setChecked( proto->fieldData( Ip4Protocol::ip4_isOverrideTotLen, AbstractProtocol::FieldValue ).toBool()); leIpLength->setText( proto->fieldData( Ip4Protocol::ip4_totLen, AbstractProtocol::FieldValue ).toString()); leIpId->setText(uintToHexStr( proto->fieldData( Ip4Protocol::ip4_id, AbstractProtocol::FieldValue ).toUInt(), 2)); leIpFragOfs->setText( proto->fieldData( Ip4Protocol::ip4_fragOfs, AbstractProtocol::FieldValue ).toString()); cbIpFlagsDf->setChecked(( proto->fieldData( Ip4Protocol::ip4_flags, AbstractProtocol::FieldValue ).toUInt() & IP_FLAG_DF) > 0); cbIpFlagsMf->setChecked(( proto->fieldData( Ip4Protocol::ip4_flags, AbstractProtocol::FieldValue ).toUInt() & IP_FLAG_MF) > 0); leIpTtl->setText( proto->fieldData( Ip4Protocol::ip4_ttl, AbstractProtocol::FieldValue ).toString()); cbIpProtocolOverride->setChecked( proto->fieldData( Ip4Protocol::ip4_isOverrideProto, AbstractProtocol::FieldValue ).toBool()); leIpProto->setText(uintToHexStr( proto->fieldData( Ip4Protocol::ip4_proto, AbstractProtocol::FieldValue ).toUInt(), 1)); cbIpCksumOverride->setChecked( proto->fieldData( Ip4Protocol::ip4_isOverrideCksum, AbstractProtocol::FieldValue ).toBool()); leIpCksum->setText(uintToHexStr( proto->fieldData( Ip4Protocol::ip4_cksum, AbstractProtocol::FieldValue ).toUInt(), 2)); leIpSrcAddr->setText(QHostAddress( proto->fieldData( Ip4Protocol::ip4_srcAddr, AbstractProtocol::FieldValue ).toUInt()).toString()); cmbIpSrcAddrMode->setCurrentIndex( proto->fieldData( Ip4Protocol::ip4_srcAddrMode, AbstractProtocol::FieldValue ).toUInt()); leIpSrcAddrCount->setText( proto->fieldData( Ip4Protocol::ip4_srcAddrCount, AbstractProtocol::FieldValue ).toString()); leIpSrcAddrMask->setText(QHostAddress( proto->fieldData( Ip4Protocol::ip4_srcAddrMask, AbstractProtocol::FieldValue ).toUInt()).toString()); leIpDstAddr->setText(QHostAddress( proto->fieldData( Ip4Protocol::ip4_dstAddr, AbstractProtocol::FieldValue ).toUInt()).toString()); cmbIpDstAddrMode->setCurrentIndex( proto->fieldData( Ip4Protocol::ip4_dstAddrMode, AbstractProtocol::FieldValue ).toUInt()); leIpDstAddrCount->setText( proto->fieldData( Ip4Protocol::ip4_dstAddrCount, AbstractProtocol::FieldValue ).toString()); leIpDstAddrMask->setText(QHostAddress( proto->fieldData( Ip4Protocol::ip4_dstAddrMask, AbstractProtocol::FieldValue ).toUInt()).toString()); leIpOptions->setText( proto->fieldData( Ip4Protocol::ip4_options, AbstractProtocol::FieldValue ).toByteArray().toHex()); } void Ip4ConfigForm::storeWidget(AbstractProtocol *proto) { uint ff = 0; proto->setFieldData( Ip4Protocol::ip4_isOverrideVer, cbIpVersionOverride->isChecked()); proto->setFieldData( Ip4Protocol::ip4_ver, leIpVersion->text()); proto->setFieldData( Ip4Protocol::ip4_isOverrideHdrLen, cbIpHdrLenOverride->isChecked()); proto->setFieldData( Ip4Protocol::ip4_hdrLen, leIpHdrLen->text()); proto->setFieldData( Ip4Protocol::ip4_tos, tosDscp->value()); proto->setFieldData( Ip4Protocol::ip4_totLen, leIpLength->text()); proto->setFieldData( Ip4Protocol::ip4_isOverrideTotLen, cbIpLengthOverride->isChecked()); proto->setFieldData( Ip4Protocol::ip4_id, hexStrToUInt(leIpId->text())); proto->setFieldData( Ip4Protocol::ip4_fragOfs, leIpFragOfs->text()); if (cbIpFlagsDf->isChecked()) ff |= IP_FLAG_DF; if (cbIpFlagsMf->isChecked()) ff |= IP_FLAG_MF; proto->setFieldData( Ip4Protocol::ip4_flags, ff); proto->setFieldData( Ip4Protocol::ip4_ttl, leIpTtl->text()); proto->setFieldData( Ip4Protocol::ip4_isOverrideProto, cbIpProtocolOverride->isChecked()); proto->setFieldData( Ip4Protocol::ip4_proto, hexStrToUInt(leIpProto->text())); proto->setFieldData( Ip4Protocol::ip4_isOverrideCksum, cbIpCksumOverride->isChecked()); proto->setFieldData( Ip4Protocol::ip4_cksum, hexStrToUInt(leIpCksum->text())); proto->setFieldData( Ip4Protocol::ip4_srcAddr, QHostAddress(leIpSrcAddr->text()).toIPv4Address()); proto->setFieldData( Ip4Protocol::ip4_srcAddrMode, (OstProto::Ip4_IpAddrMode)cmbIpSrcAddrMode->currentIndex()); proto->setFieldData( Ip4Protocol::ip4_srcAddrCount, leIpSrcAddrCount->text()); proto->setFieldData( Ip4Protocol::ip4_srcAddrMask, QHostAddress(leIpSrcAddrMask->text()).toIPv4Address()); proto->setFieldData( Ip4Protocol::ip4_dstAddr, QHostAddress(leIpDstAddr->text()).toIPv4Address()); proto->setFieldData( Ip4Protocol::ip4_dstAddrMode, (OstProto::Ip4_IpAddrMode)cmbIpDstAddrMode->currentIndex()); proto->setFieldData( Ip4Protocol::ip4_dstAddrCount, leIpDstAddrCount->text()); proto->setFieldData( Ip4Protocol::ip4_dstAddrMask, QHostAddress(leIpDstAddrMask->text()).toIPv4Address()); proto->setFieldData( Ip4Protocol::ip4_options, QByteArray::fromHex(QByteArray().append(leIpOptions->text()))); } /* * Slots */ void Ip4ConfigForm::on_cmbIpSrcAddrMode_currentIndexChanged(int index) { if (index == OstProto::Ip4::e_im_fixed) { leIpSrcAddrCount->setDisabled(true); leIpSrcAddrMask->setDisabled(true); } else { leIpSrcAddrCount->setEnabled(true); leIpSrcAddrMask->setEnabled(true); } } void Ip4ConfigForm::on_cmbIpDstAddrMode_currentIndexChanged(int index) { if (index == OstProto::Ip4::e_im_fixed) { leIpDstAddrCount->setDisabled(true); leIpDstAddrMask->setDisabled(true); } else { leIpDstAddrCount->setEnabled(true); leIpDstAddrMask->setEnabled(true); } } ostinato-1.3.0/common/ip4config.h000066400000000000000000000023511451413623100166770ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _IPV4_CONFIG_H #define _IPV4_CONFIG_H #include "abstractprotocolconfig.h" #include "ui_ip4.h" class Ip4ConfigForm : public AbstractProtocolConfigForm, private Ui::ip4 { Q_OBJECT public: Ip4ConfigForm(QWidget *parent = 0); virtual ~Ip4ConfigForm(); static Ip4ConfigForm* createInstance(); virtual void loadWidget(AbstractProtocol *proto); virtual void storeWidget(AbstractProtocol *proto); private slots: void on_cmbIpSrcAddrMode_currentIndexChanged(int index); void on_cmbIpDstAddrMode_currentIndexChanged(int index); }; #endif ostinato-1.3.0/common/ip4edit.h000066400000000000000000000022561451413623100163630ustar00rootroot00000000000000/* Copyright (C) 2016 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _IP4_EDIT_H #define _IP4_EDIT_H #include #include class Ip4Edit: public QLineEdit { public: Ip4Edit(QWidget *parent = 0); quint32 value(); void setValue(quint32 val); }; inline Ip4Edit::Ip4Edit(QWidget *parent) : QLineEdit(parent) { setInputMask(QString("000.000.000.000; ")); } inline quint32 Ip4Edit::value() { return QHostAddress(text()).toIPv4Address(); } inline void Ip4Edit::setValue(quint32 val) { setText(QHostAddress(val).toString()); } #endif ostinato-1.3.0/common/ip4over4.h000066400000000000000000000057251451413623100165010ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _IP_4_OVER_4_H #define _IP_4_OVER_4_H #include "ip4over4.pb.h" #include "comboprotocol.h" #include "ip4.h" typedef ComboProtocol Ip4over4Combo; class Ip4over4Protocol : public Ip4over4Combo { public: Ip4over4Protocol(StreamBase *stream, AbstractProtocol *parent = 0) : Ip4over4Combo(stream, parent) { } static Ip4over4Protocol* createInstance(StreamBase *stream, AbstractProtocol *parent) { return new Ip4over4Protocol(stream, parent); } virtual void protoDataCopyInto(OstProto::Protocol &protocol) const { OstProto::Protocol tempProto; protoA->protoDataCopyInto(tempProto); protocol.MutableExtension(OstProto::ip4over4) ->MutableExtension(OstProto::ip4_outer) ->CopyFrom(tempProto.GetExtension(OstProto::ip4)); tempProto.Clear(); protoB->protoDataCopyInto(tempProto); protocol.MutableExtension(OstProto::ip4over4) ->MutableExtension(OstProto::ip4_inner) ->CopyFrom(tempProto.GetExtension(OstProto::ip4)); protocol.mutable_protocol_id()->set_id(protocolNumber()); } virtual void protoDataCopyFrom(const OstProto::Protocol &protocol) { if (protocol.protocol_id().id() == protocolNumber() && protocol.HasExtension(OstProto::ip4over4)) { OstProto::Protocol tempProto; // NOTE: To use protoX->protoDataCopyFrom() we need to arrange // so that it sees its own protocolNumber() and its own extension // in 'protocol' tempProto.mutable_protocol_id()->set_id(protoA->protocolNumber()); tempProto.MutableExtension(OstProto::ip4)->CopyFrom( protocol.GetExtension(OstProto::ip4over4).GetExtension( OstProto::ip4_outer)); protoA->protoDataCopyFrom(tempProto); tempProto.Clear(); tempProto.mutable_protocol_id()->set_id(protoB->protocolNumber()); tempProto.MutableExtension(OstProto::ip4)->CopyFrom( protocol.GetExtension(OstProto::ip4over4).GetExtension( OstProto::ip4_inner)); protoB->protoDataCopyFrom(tempProto); } } }; #endif ostinato-1.3.0/common/ip4over4.proto000066400000000000000000000016771451413623100174170ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ import "protocol.proto"; import "ip4.proto"; package OstProto; // IP 4over4 (also called IPIP) message Ip4over4 { extensions 1 to 2; } extend Ip4over4 { optional Ip4 ip4_outer = 1; optional Ip4 ip4_inner = 2; } extend Protocol { optional Ip4over4 ip4over4 = 305; } ostinato-1.3.0/common/ip4over4config.h000066400000000000000000000020011451413623100176470ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _IP_4_OVER_4_CONFIG_H #define _IP_4_OVER_4_CONFIG_H #include "comboprotocolconfig.h" #include "ip4config.h" #include "ip4.h" #include "protocol.pb.h" typedef ComboProtocolConfigForm < OstProto::Protocol::kIp4over4FieldNumber, Ip4ConfigForm, Ip4ConfigForm, Ip4Protocol, Ip4Protocol > Ip4over4ConfigForm; #endif ostinato-1.3.0/common/ip4over6.h000066400000000000000000000016101451413623100164700ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _IP_4_OVER_6_H #define _IP_4_OVER_6_H #include "comboprotocol.h" #include "ip4.h" #include "ip6.h" typedef ComboProtocol Ip4over6Protocol; #endif ostinato-1.3.0/common/ip4over6.proto000066400000000000000000000015521451413623100174110ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ import "protocol.proto"; package OstProto; // IP Tunelling - IP 4over6 message Ip4over6 { // Empty since this is a 'combo' protocol } extend Protocol { optional Ip4over6 ip4over6 = 304; } ostinato-1.3.0/common/ip4over6config.h000066400000000000000000000020171451413623100176600ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _IP_4_OVER_6_CONFIG_H #define _IP_4_OVER_6_CONFIG_H #include "comboprotocolconfig.h" #include "ip4config.h" #include "ip6config.h" #include "ip4.h" #include "ip6.h" typedef ComboProtocolConfigForm < OstProto::Protocol::kIp4over6FieldNumber, Ip6ConfigForm, Ip4ConfigForm, Ip6Protocol, Ip4Protocol > Ip4over6ConfigForm; #endif ostinato-1.3.0/common/ip4pdml.cpp000066400000000000000000000064021451413623100167220ustar00rootroot00000000000000/* Copyright (C) 2011 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #include "ip4pdml.h" #include "hexdump.pb.h" #include "ip4.pb.h" PdmlIp4Protocol::PdmlIp4Protocol() { ostProtoId_ = OstProto::Protocol::kIp4FieldNumber; fieldMap_.insert("ip.dsfield", OstProto::Ip4::kTosFieldNumber); fieldMap_.insert("ip.len", OstProto::Ip4::kTotlenFieldNumber); fieldMap_.insert("ip.id", OstProto::Ip4::kIdFieldNumber); //fieldMap_.insert("ip.flags", OstProto::Ip4::kFlagsFieldNumber); fieldMap_.insert("ip.frag_offset", OstProto::Ip4::kFragOfsFieldNumber); fieldMap_.insert("ip.ttl", OstProto::Ip4::kTtlFieldNumber); fieldMap_.insert("ip.proto", OstProto::Ip4::kProtoFieldNumber); fieldMap_.insert("ip.checksum", OstProto::Ip4::kCksumFieldNumber); fieldMap_.insert("ip.src", OstProto::Ip4::kSrcIpFieldNumber); fieldMap_.insert("ip.dst", OstProto::Ip4::kDstIpFieldNumber); } PdmlProtocol* PdmlIp4Protocol::createInstance() { return new PdmlIp4Protocol(); } void PdmlIp4Protocol::unknownFieldHandler(QString name, int /*pos*/, int /*size*/, const QXmlStreamAttributes &attributes, OstProto::Protocol *pbProto, OstProto::Stream* /*stream*/) { bool isOk; if (name == "ip.version") { OstProto::Ip4 *ip4 = pbProto->MutableExtension(OstProto::ip4); if (!attributes.value("unmaskedvalue").isEmpty()) ip4->set_ver_hdrlen(attributes.value("unmaskedvalue") .toString().toUInt(&isOk, kBaseHex)); else ip4->set_ver_hdrlen(attributes.value("value") .toString().toUInt(&isOk, kBaseHex)); } else if ((name == "ip.options") || attributes.value("show").toString().startsWith("Options")) { OstProto::Ip4 *ip4 = pbProto->MutableExtension(OstProto::ip4); QByteArray options = QByteArray::fromHex( attributes.value("value").toString().toUtf8()); ip4->set_options(options.constData(), options.size()); } else if (name == "ip.flags") { OstProto::Ip4 *ip4 = pbProto->MutableExtension(OstProto::ip4); int shift = attributes.value("size").toString().toUInt(&isOk) == 2 ? 13 : 5; ip4->set_flags(attributes.value("value").toString().toUInt(&isOk, kBaseHex) >> shift); } } void PdmlIp4Protocol::postProtocolHandler(OstProto::Protocol *pbProto, OstProto::Stream* /*stream*/) { OstProto::Ip4 *ip4 = pbProto->MutableExtension(OstProto::ip4); ip4->set_is_override_ver(true); ip4->set_is_override_hdrlen(true); ip4->set_is_override_totlen(true); ip4->set_is_override_proto(true); ip4->set_is_override_cksum(overrideCksum_); } ostinato-1.3.0/common/ip4pdml.h000066400000000000000000000022361451413623100163700ustar00rootroot00000000000000/* Copyright (C) 2011 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _IP4_PDML_H #define _IP4_PDML_H #include "pdmlprotocol.h" class PdmlIp4Protocol : public PdmlProtocol { public: static PdmlProtocol* createInstance(); virtual void unknownFieldHandler(QString name, int pos, int size, const QXmlStreamAttributes &attributes, OstProto::Protocol *pbProto, OstProto::Stream *stream); virtual void postProtocolHandler(OstProto::Protocol *pbProto, OstProto::Stream *stream); protected: PdmlIp4Protocol(); }; #endif ostinato-1.3.0/common/ip6.cpp000066400000000000000000000600131451413623100160450ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #include "ip6.h" #include "uint128.h" #include Ip6Protocol::Ip6Protocol(StreamBase *stream, AbstractProtocol *parent) : AbstractProtocol(stream, parent) { } Ip6Protocol::~Ip6Protocol() { } AbstractProtocol* Ip6Protocol::createInstance(StreamBase *stream, AbstractProtocol *parent) { return new Ip6Protocol(stream, parent); } quint32 Ip6Protocol::protocolNumber() const { return OstProto::Protocol::kIp6FieldNumber; } void Ip6Protocol::protoDataCopyInto(OstProto::Protocol &protocol) const { protocol.MutableExtension(OstProto::ip6)->CopyFrom(data); protocol.mutable_protocol_id()->set_id(protocolNumber()); } void Ip6Protocol::protoDataCopyFrom(const OstProto::Protocol &protocol) { if (protocol.protocol_id().id() == protocolNumber() && protocol.HasExtension(OstProto::ip6)) data.MergeFrom(protocol.GetExtension(OstProto::ip6)); } QString Ip6Protocol::name() const { return QString("Internet Protocol ver 6"); } QString Ip6Protocol::shortName() const { return QString("IPv6"); } AbstractProtocol::ProtocolIdType Ip6Protocol::protocolIdType() const { return ProtocolIdIp; } quint32 Ip6Protocol::protocolId(ProtocolIdType type) const { switch(type) { case ProtocolIdEth: return 0x86dd; case ProtocolIdIp: return 0x29; default:break; } return AbstractProtocol::protocolId(type); } int Ip6Protocol::fieldCount() const { return ip6_fieldCount; } AbstractProtocol::FieldFlags Ip6Protocol::fieldFlags(int index) const { AbstractProtocol::FieldFlags flags; flags = AbstractProtocol::fieldFlags(index); switch (index) { case ip6_version: case ip6_trafficClass: case ip6_flowLabel: case ip6_payloadLength: case ip6_nextHeader: case ip6_hopLimit: case ip6_srcAddress: case ip6_dstAddress: break; case ip6_isOverrideVersion: case ip6_isOverridePayloadLength: case ip6_isOverrideNextHeader: case ip6_srcAddrMode: case ip6_srcAddrCount: case ip6_srcAddrPrefix: case ip6_dstAddrMode: case ip6_dstAddrCount: case ip6_dstAddrPrefix: flags &= ~FrameField; flags |= MetaField; break; default: qFatal("%s: unimplemented case %d in switch", __PRETTY_FUNCTION__, index); break; } return flags; } QVariant Ip6Protocol::fieldData(int index, FieldAttrib attrib, int streamIndex) const { switch (index) { case ip6_version: { quint8 ver; switch(attrib) { case FieldValue: case FieldFrameValue: case FieldTextValue: if (data.is_override_version()) ver = data.version() & 0xF; else ver = 0x6; break; default: ver = 0; // avoid the 'maybe used unitialized' warning break; } switch(attrib) { case FieldName: return QString("Version"); case FieldValue: return ver; case FieldTextValue: return QString("%1").arg(ver); case FieldFrameValue: return QByteArray(1, char(ver)); case FieldBitSize: return 4; default: break; } break; } case ip6_trafficClass: { switch(attrib) { case FieldName: return QString("Traffic Class"); case FieldValue: return data.traffic_class() & 0xFF; case FieldTextValue: return QString("%1").arg(data.traffic_class() & 0xFF, 2, BASE_HEX, QChar('0')); case FieldFrameValue: return QByteArray(1, char(data.traffic_class() & 0xFF)); default: break; } break; } case ip6_flowLabel: { switch(attrib) { case FieldName: return QString("Flow Label"); case FieldValue: return data.flow_label() & 0xFFFFF; case FieldTextValue: return QString("%1").arg(data.flow_label() & 0xFFFFF, 5, BASE_HEX, QChar('0')); case FieldFrameValue: { QByteArray fv; fv.resize(4); qToBigEndian((quint32) data.flow_label() & 0xFFFFF, (uchar*) fv.data()); fv = fv.right(3); return fv; } case FieldBitSize: return 20; default: break; } break; } case ip6_payloadLength: { quint16 len; switch(attrib) { case FieldValue: case FieldFrameValue: case FieldTextValue: if (data.is_override_payload_length()) len = data.payload_length(); else len = protocolFramePayloadSize(streamIndex); break; default: len = 0; // avoid the 'maybe used unitialized' warning break; } switch(attrib) { case FieldName: return QString("Payload Length"); case FieldValue: return len; case FieldFrameValue: { QByteArray fv; fv.resize(2); qToBigEndian(len, (uchar*) fv.data()); return fv; } case FieldTextValue: return QString("%1").arg(len); case FieldBitSize: return 16; default: break; } break; } case ip6_nextHeader: { quint8 nextHdr; switch(attrib) { case FieldValue: case FieldFrameValue: case FieldTextValue: if (data.is_override_next_header()) { nextHdr = data.next_header(); } else { nextHdr = payloadProtocolId(ProtocolIdIp); if ((nextHdr == 0) && next && (next->protocolIdType() == ProtocolIdNone)) { nextHdr = 0x3b; // IPv6 No-Next-Header } } break; default: nextHdr = 0; // avoid the 'maybe used unitialized' warning break; } switch(attrib) { case FieldName: return QString("Next Header"); case FieldValue: return nextHdr; case FieldTextValue: return QString("%1").arg(nextHdr, 2, BASE_HEX, QChar('0')); case FieldFrameValue: return QByteArray(1, char(nextHdr)); default: break; } break; } case ip6_hopLimit: { switch(attrib) { case FieldName: return QString("Hop Limit"); case FieldValue: return data.hop_limit() & 0xFF; case FieldTextValue: return QString("%1").arg(data.hop_limit() & 0xFF); case FieldFrameValue: return QByteArray(1, char(data.hop_limit() & 0xFF)); default: break; } break; } case ip6_srcAddress: { int u; UInt128 mask = 0; UInt128 prefix = 0; UInt128 host = 0; UInt128 src(data.src_addr_hi(), data.src_addr_lo()); switch(data.src_addr_mode()) { case OstProto::Ip6::kFixed: break; case OstProto::Ip6::kIncHost: case OstProto::Ip6::kDecHost: case OstProto::Ip6::kRandomHost: u = streamIndex % data.src_addr_count(); mask = ~UInt128(0, 0) << (128 - data.src_addr_prefix()); prefix = src & mask; if (data.src_addr_mode() == OstProto::Ip6::kIncHost) { host = ((src & ~mask) + u) & ~mask; } else if (data.src_addr_mode() == OstProto::Ip6::kDecHost) { host = ((src & ~mask) - u) & ~mask; } else if (data.src_addr_mode()==OstProto::Ip6::kRandomHost) { // XXX: qrand is int (32bit) not 64bit, some stdlib // implementations have RAND_MAX as low as 0x7FFF host = UInt128(qrand(), qrand()) & ~mask; } src = prefix | host; break; default: qWarning("Unhandled src_addr_mode = %d", data.src_addr_mode()); } switch(attrib) { case FieldName: return QString("Source"); case FieldValue: { QVariant v; v.setValue(src); return v; } case FieldFrameValue: case FieldTextValue: { QByteArray fv; fv.resize(16); qToBigEndian(src, (uchar*) fv.data()); if (attrib == FieldTextValue) return QHostAddress(src.toArray()).toString(); else return fv; } default: break; } break; } case ip6_dstAddress: { int u; UInt128 mask = 0; UInt128 prefix = 0; UInt128 host = 0; UInt128 dst(data.dst_addr_hi(), data.dst_addr_lo()); switch(data.dst_addr_mode()) { case OstProto::Ip6::kFixed: break; case OstProto::Ip6::kIncHost: case OstProto::Ip6::kDecHost: case OstProto::Ip6::kRandomHost: u = streamIndex % data.dst_addr_count(); mask = ~UInt128(0, 0) << (128 - data.dst_addr_prefix()); prefix = dst & mask; if (data.dst_addr_mode() == OstProto::Ip6::kIncHost) { host = ((dst & ~mask) + u) & ~mask; } else if (data.dst_addr_mode() == OstProto::Ip6::kDecHost) { host = ((dst & ~mask) - u) & ~mask; } else if (data.dst_addr_mode()==OstProto::Ip6::kRandomHost) { // XXX: qrand is int (32bit) not 64bit, some stdlib // implementations have RAND_MAX as low as 0x7FFF host = UInt128(qrand(), qrand()) & ~mask; } dst = prefix | host; break; default: qWarning("Unhandled dst_addr_mode = %d", data.dst_addr_mode()); } switch(attrib) { case FieldName: return QString("Destination"); case FieldValue: { QVariant v; v.setValue(dst); return v; } case FieldFrameValue: case FieldTextValue: { QByteArray fv; fv.resize(16); qToBigEndian(dst, (uchar*) fv.data()); if (attrib == FieldTextValue) return QHostAddress(dst.toArray()).toString(); else return fv; } default: break; } break; } // Meta-Fields case ip6_isOverrideVersion: { switch(attrib) { case FieldValue: return data.is_override_version(); default: break; } break; } case ip6_isOverridePayloadLength: { switch(attrib) { case FieldValue: return data.is_override_payload_length(); default: break; } break; } case ip6_isOverrideNextHeader: { switch(attrib) { case FieldValue: return data.is_override_next_header(); default: break; } break; } case ip6_srcAddrMode: { switch(attrib) { case FieldValue: return data.src_addr_mode(); default: break; } break; } case ip6_srcAddrCount: { switch(attrib) { case FieldValue: return data.src_addr_count(); default: break; } break; } case ip6_srcAddrPrefix: { switch(attrib) { case FieldValue: return data.src_addr_prefix(); default: break; } break; } case ip6_dstAddrMode: { switch(attrib) { case FieldValue: return data.dst_addr_mode(); default: break; } break; } case ip6_dstAddrCount: { switch(attrib) { case FieldValue: return data.dst_addr_count(); default: break; } break; } case ip6_dstAddrPrefix: { switch(attrib) { case FieldValue: return data.dst_addr_prefix(); default: break; } break; } default: qFatal("%s: unimplemented case %d in switch", __PRETTY_FUNCTION__, index); break; } return AbstractProtocol::fieldData(index, attrib, streamIndex); } bool Ip6Protocol::setFieldData(int index, const QVariant &value, FieldAttrib attrib) { bool isOk = false; if (attrib != FieldValue) goto _exit; switch (index) { case ip6_version: { uint ver = value.toUInt(&isOk); if (isOk) data.set_version(ver & 0xF); break; } case ip6_trafficClass: { uint trfClass = value.toUInt(&isOk); if (isOk) data.set_traffic_class(trfClass & 0xFF); break; } case ip6_flowLabel: { uint fl = value.toUInt(&isOk); if (isOk) data.set_flow_label(fl & 0xFFFFF); break; } case ip6_payloadLength: { uint len = value.toUInt(&isOk); if (isOk) data.set_payload_length(len & 0xFFFF); break; } case ip6_nextHeader: { uint ver = value.toUInt(&isOk); if (isOk) data.set_next_header(ver & 0xFF); break; } case ip6_hopLimit: { uint hl = value.toUInt(&isOk); if (isOk) data.set_hop_limit(hl & 0xFF); break; } case ip6_srcAddress: { if (value.typeName() == QString("UInt128")) { UInt128 addr = value.value(); data.set_src_addr_hi(addr.hi64()); data.set_src_addr_lo(addr.lo64()); isOk = true; break; } Q_IPV6ADDR addr = QHostAddress(value.toString()).toIPv6Address(); quint64 x; x = (quint64(addr[0]) << 56) | (quint64(addr[1]) << 48) | (quint64(addr[2]) << 40) | (quint64(addr[3]) << 32) | (quint64(addr[4]) << 24) | (quint64(addr[5]) << 16) | (quint64(addr[6]) << 8) | (quint64(addr[7]) << 0); data.set_src_addr_hi(x); x = (quint64(addr[ 8]) << 56) | (quint64(addr[ 9]) << 48) | (quint64(addr[10]) << 40) | (quint64(addr[11]) << 32) | (quint64(addr[12]) << 24) | (quint64(addr[13]) << 16) | (quint64(addr[14]) << 8) | (quint64(addr[15]) << 0); data.set_src_addr_lo(x); isOk = true; break; } case ip6_dstAddress: { if (value.typeName() == QString("UInt128")) { UInt128 addr = value.value(); data.set_dst_addr_hi(addr.hi64()); data.set_dst_addr_lo(addr.lo64()); isOk = true; break; } Q_IPV6ADDR addr = QHostAddress(value.toString()).toIPv6Address(); quint64 x; x = (quint64(addr[0]) << 56) | (quint64(addr[1]) << 48) | (quint64(addr[2]) << 40) | (quint64(addr[3]) << 32) | (quint64(addr[4]) << 24) | (quint64(addr[5]) << 16) | (quint64(addr[6]) << 8) | (quint64(addr[7]) << 0); data.set_dst_addr_hi(x); x = (quint64(addr[ 8]) << 56) | (quint64(addr[ 9]) << 48) | (quint64(addr[10]) << 40) | (quint64(addr[11]) << 32) | (quint64(addr[12]) << 24) | (quint64(addr[13]) << 16) | (quint64(addr[14]) << 8) | (quint64(addr[15]) << 0); data.set_dst_addr_lo(x); isOk = true; break; } // Meta-Fields case ip6_isOverrideVersion: { bool ovr = value.toBool(); data.set_is_override_version(ovr); isOk = true; break; } case ip6_isOverridePayloadLength: { bool ovr = value.toBool(); data.set_is_override_payload_length(ovr); isOk = true; break; } case ip6_isOverrideNextHeader: { bool ovr = value.toBool(); data.set_is_override_next_header(ovr); isOk = true; break; } case ip6_srcAddrMode: { uint mode = value.toUInt(&isOk); if (isOk && data.AddrMode_IsValid(mode)) data.set_src_addr_mode((OstProto::Ip6::AddrMode) mode); else isOk = false; break; } case ip6_srcAddrCount: { uint count = value.toUInt(&isOk); if (isOk) data.set_src_addr_count(count); break; } case ip6_srcAddrPrefix: { uint prefix = value.toUInt(&isOk); if (isOk) data.set_src_addr_prefix(prefix); break; } case ip6_dstAddrMode: { uint mode = value.toUInt(&isOk); if (isOk && data.AddrMode_IsValid(mode)) data.set_dst_addr_mode((OstProto::Ip6::AddrMode) mode); else isOk = false; break; } case ip6_dstAddrCount: { uint count = value.toUInt(&isOk); if (isOk) data.set_dst_addr_count(count); break; } case ip6_dstAddrPrefix: { uint prefix = value.toUInt(&isOk); if (isOk) data.set_dst_addr_prefix(prefix); break; } default: qFatal("%s: unimplemented case %d in switch", __PRETTY_FUNCTION__, index); break; } _exit: return isOk; } int Ip6Protocol::protocolFrameVariableCount() const { int count = AbstractProtocol::protocolFrameVariableCount(); if (data.src_addr_mode() != OstProto::Ip6::kFixed) count = AbstractProtocol::lcm(count, data.src_addr_count()); if (data.dst_addr_mode() != OstProto::Ip6::kFixed) count = AbstractProtocol::lcm(count, data.dst_addr_count()); return count; } quint32 Ip6Protocol::protocolFrameCksum(int streamIndex, CksumType cksumType, CksumFlags cksumFlags) const { if (cksumType == CksumIpPseudo) { quint32 sum = 0; QByteArray fv = protocolFrameValue(streamIndex); const quint8 *p = (quint8*) fv.constData(); // src-ip, dst-ip for (int i = 8; i < fv.size(); i+=2) sum += *((quint16*)(p + i)); // XXX: payload length and protocol are also part of the // pseudo cksum but we need to skip extension headers to // get to them as per RFC 8200 Section 8.1 // Since we can't traverse beyond our immediate neighboring // protocol from here, these two fields are counted in the // pseudo cksum in AbstractProtocol::protocolFrameHeaderCksum() while(sum>>16) sum = (sum & 0xFFFF) + (sum >> 16); return qFromBigEndian((quint16) ~sum); } return AbstractProtocol::protocolFrameCksum( streamIndex, cksumType, cksumFlags); } bool Ip6Protocol::hasErrors(QStringList *errors) const { bool result = false; if ((data.dst_addr_hi() == 0ULL) && (data.dst_addr_lo() == 0ULL) && (data.dst_addr_mode() == OstProto::Ip6::kFixed)) { if (errors) *errors << QObject::tr("Frames with Destination IP :: (all zeroes) " "are likely to be dropped"); result = true; } if ((data.src_addr_hi() == 0ULL) && (data.src_addr_lo() == 0ULL) && (data.src_addr_mode() == OstProto::Ip6::kFixed)) { if (errors) *errors << QObject::tr("Frames with Source IP :: (all zeroes) " "are likely to be dropped"); result = true; } return result; } ostinato-1.3.0/common/ip6.h000066400000000000000000000065751451413623100155270ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _IP6_H #define _IP6_H #include "abstractprotocol.h" #include "ip6.pb.h" /* IPv6 Protocol Frame Format - +-----+----------+-----------------------+ | Ver | TrfClass | FlowLabel | | (4) | (8) | (20) | +-----+-------------+---------+----------+ | Payload Length | NextHdr | HopLimit | | (16) | (8) | (8) | +-------------------+---------+----------+ | | | Source Address | | (128) | | | +-----+------+------+------+------+------+ | | | Destination Address | | (128) | | | +-----+------+------+------+------+------+ Figures in brackets represent field width in bits */ class Ip6Protocol : public AbstractProtocol { public: enum ip6field { // Frame Fields ip6_version = 0, ip6_trafficClass, ip6_flowLabel, ip6_payloadLength, ip6_nextHeader, ip6_hopLimit, ip6_srcAddress, ip6_dstAddress, // Meta Fields ip6_isOverrideVersion, ip6_isOverridePayloadLength, ip6_isOverrideNextHeader, ip6_srcAddrMode, ip6_srcAddrCount, ip6_srcAddrPrefix, ip6_dstAddrMode, ip6_dstAddrCount, ip6_dstAddrPrefix, ip6_fieldCount }; Ip6Protocol(StreamBase *stream, AbstractProtocol *parent = 0); virtual ~Ip6Protocol(); static AbstractProtocol* createInstance(StreamBase *stream, AbstractProtocol *parent = 0); virtual quint32 protocolNumber() const; virtual void protoDataCopyInto(OstProto::Protocol &protocol) const; virtual void protoDataCopyFrom(const OstProto::Protocol &protocol); virtual ProtocolIdType protocolIdType() const; virtual quint32 protocolId(ProtocolIdType type) const; virtual QString name() const; virtual QString shortName() const; virtual int fieldCount() const; virtual AbstractProtocol::FieldFlags fieldFlags(int index) const; virtual QVariant fieldData(int index, FieldAttrib attrib, int streamIndex = 0) const; virtual bool setFieldData(int index, const QVariant &value, FieldAttrib attrib = FieldValue); virtual int protocolFrameVariableCount() const; virtual quint32 protocolFrameCksum(int streamIndex = 0, CksumType cksumType = CksumIp, CksumFlags cksumFlags = 0) const; virtual bool hasErrors(QStringList *errors = nullptr) const; private: OstProto::Ip6 data; }; #endif ostinato-1.3.0/common/ip6.proto000066400000000000000000000034301451413623100164260ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ import "protocol.proto"; package OstProto; // Ip6 Protocol message Ip6 { enum AddrMode { kFixed = 0; kIncHost = 1; kDecHost = 2; kRandomHost = 3; } optional bool is_override_version = 1; optional bool is_override_payload_length = 2; optional bool is_override_next_header = 3; optional uint32 version = 4 [default = 0x6]; optional uint32 traffic_class = 5; optional uint32 flow_label = 6; optional uint32 payload_length = 7; optional uint32 next_header = 8; optional uint32 hop_limit = 9 [default = 127]; optional uint64 src_addr_hi = 10; optional uint64 src_addr_lo = 11; optional AddrMode src_addr_mode = 12 [default = kFixed]; optional uint32 src_addr_count = 13 [default = 16]; optional uint32 src_addr_prefix = 14 [default = 64]; optional uint64 dst_addr_hi = 15; optional uint64 dst_addr_lo = 16; optional AddrMode dst_addr_mode = 17 [default = kFixed]; optional uint32 dst_addr_count = 18 [default = 16]; optional uint32 dst_addr_prefix = 19 [default = 64]; } extend Protocol { optional Ip6 ip6 = 302; } ostinato-1.3.0/common/ip6.ui000066400000000000000000000301411451413623100156770ustar00rootroot00000000000000 Ip6 0 0 506 233 Form Version false Qt::Vertical Payload Length false Next Header false HH; Flow Label flowLabel >H HH HH; Hop Limit hopLimit false Qt::Horizontal 51 20 Address Mode Count Prefix Source 1 0 Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter Fixed Increment Host Decrement Host Random Host false 0 0 50 16777215 10 false 0 0 50 16777215 /009; /64 Destination 1 0 Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter Fixed Increment Host Decrement Host Random Host false 0 0 50 16777215 10 false 0 0 50 16777215 /009; /64 Qt::Vertical 20 40 TosDscpWidget QWidget
tosdscp.h
1
isVersionOverride version tosDscp flowLabel isPayloadLengthOverride payloadLength isNextHeaderOverride nextHeader hopLimit srcAddr srcAddrModeCombo srcAddrCount srcAddrPrefix dstAddr dstAddrModeCombo dstAddrCount dstAddrPrefix isVersionOverride toggled(bool) version setEnabled(bool) 67 22 195 11 isPayloadLengthOverride toggled(bool) payloadLength setEnabled(bool) 319 28 493 29 isNextHeaderOverride toggled(bool) nextHeader setEnabled(bool) 316 41 348 46
ostinato-1.3.0/common/ip6config.cpp000066400000000000000000000157701451413623100172450ustar00rootroot00000000000000/* Copyright (C) 2010-2014 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #include "ip6config.h" #include "ip6.h" #include "ipv6addressvalidator.h" #include Ip6ConfigForm::Ip6ConfigForm(QWidget *parent) : AbstractProtocolConfigForm(parent) { setupUi(this); version->setValidator(new QIntValidator(0, 0xF, this)); payloadLength->setValidator(new QIntValidator(0, 0xFFFF, this)); hopLimit->setValidator(new QIntValidator(0, 0xFF, this)); srcAddr->setValidator(new IPv6AddressValidator(this)); srcAddrCount->setValidator(new QIntValidator(this)); //srcAddrPrefix->setValidator(new QIntValidator(0, 128, this)); dstAddr->setValidator(new IPv6AddressValidator(this)); dstAddrCount->setValidator(new QIntValidator(this)); //dstAddrPrefix->setValidator(new QIntValidator(0, 128, this)); } AbstractProtocolConfigForm* Ip6ConfigForm::createInstance() { return new Ip6ConfigForm; } void Ip6ConfigForm::on_srcAddr_editingFinished() { srcAddr->setText(QHostAddress(srcAddr->text()).toString()); } void Ip6ConfigForm::on_dstAddr_editingFinished() { dstAddr->setText(QHostAddress(dstAddr->text()).toString()); } void Ip6ConfigForm::on_srcAddrModeCombo_currentIndexChanged(int index) { bool enabled = (index > 0); srcAddrCount->setEnabled(enabled); srcAddrPrefix->setEnabled(enabled); } void Ip6ConfigForm::on_dstAddrModeCombo_currentIndexChanged(int index) { bool enabled = (index > 0); dstAddrCount->setEnabled(enabled); dstAddrPrefix->setEnabled(enabled); } void Ip6ConfigForm::loadWidget(AbstractProtocol *ip6Proto) { isVersionOverride->setChecked( ip6Proto->fieldData( Ip6Protocol::ip6_isOverrideVersion, AbstractProtocol::FieldValue ).toBool()); version->setText( ip6Proto->fieldData( Ip6Protocol::ip6_version, AbstractProtocol::FieldValue ).toString()); tosDscp->setValue( ip6Proto->fieldData( Ip6Protocol::ip6_trafficClass, AbstractProtocol::FieldValue ).toUInt()); flowLabel->setText(QString("%1").arg( ip6Proto->fieldData( Ip6Protocol::ip6_flowLabel, AbstractProtocol::FieldValue ).toUInt(), 5, BASE_HEX, QChar('0'))); isPayloadLengthOverride->setChecked( ip6Proto->fieldData( Ip6Protocol::ip6_isOverridePayloadLength, AbstractProtocol::FieldValue ).toBool()); payloadLength->setText( ip6Proto->fieldData( Ip6Protocol::ip6_payloadLength, AbstractProtocol::FieldValue ).toString()); isNextHeaderOverride->setChecked( ip6Proto->fieldData( Ip6Protocol::ip6_isOverrideNextHeader, AbstractProtocol::FieldValue ).toBool()); nextHeader->setText(uintToHexStr( ip6Proto->fieldData( Ip6Protocol::ip6_nextHeader, AbstractProtocol::FieldValue ).toUInt(), 1)); hopLimit->setText( ip6Proto->fieldData( Ip6Protocol::ip6_hopLimit, AbstractProtocol::FieldValue ).toString()); srcAddr->setText( ip6Proto->fieldData( Ip6Protocol::ip6_srcAddress, AbstractProtocol::FieldTextValue ).toString()); srcAddrModeCombo->setCurrentIndex( ip6Proto->fieldData( Ip6Protocol::ip6_srcAddrMode, AbstractProtocol::FieldValue ).toUInt()); srcAddrCount->setText( ip6Proto->fieldData( Ip6Protocol::ip6_srcAddrCount, AbstractProtocol::FieldValue ).toString()); srcAddrPrefix->setText( ip6Proto->fieldData( Ip6Protocol::ip6_srcAddrPrefix, AbstractProtocol::FieldValue ).toString()); dstAddr->setText( ip6Proto->fieldData( Ip6Protocol::ip6_dstAddress, AbstractProtocol::FieldTextValue ).toString()); dstAddrModeCombo->setCurrentIndex( ip6Proto->fieldData( Ip6Protocol::ip6_dstAddrMode, AbstractProtocol::FieldValue ).toUInt()); dstAddrCount->setText( ip6Proto->fieldData( Ip6Protocol::ip6_dstAddrCount, AbstractProtocol::FieldValue ).toString()); dstAddrPrefix->setText( ip6Proto->fieldData( Ip6Protocol::ip6_dstAddrPrefix, AbstractProtocol::FieldValue ).toString()); } void Ip6ConfigForm::storeWidget(AbstractProtocol *ip6Proto) { bool isOk; ip6Proto->setFieldData( Ip6Protocol::ip6_isOverrideVersion, isVersionOverride->isChecked()); ip6Proto->setFieldData( Ip6Protocol::ip6_version, version->text()); ip6Proto->setFieldData( Ip6Protocol::ip6_trafficClass, tosDscp->value()); ip6Proto->setFieldData( Ip6Protocol::ip6_flowLabel, flowLabel->text().remove(QChar(' ')).toUInt(&isOk, BASE_HEX)); ip6Proto->setFieldData( Ip6Protocol::ip6_isOverridePayloadLength, isPayloadLengthOverride->isChecked()); ip6Proto->setFieldData( Ip6Protocol::ip6_payloadLength, payloadLength->text()); ip6Proto->setFieldData( Ip6Protocol::ip6_isOverrideNextHeader, isNextHeaderOverride->isChecked()); ip6Proto->setFieldData( Ip6Protocol::ip6_nextHeader, nextHeader->text().remove(QChar(' ')).toUInt(&isOk, BASE_HEX)); ip6Proto->setFieldData( Ip6Protocol::ip6_hopLimit, hopLimit->text()); ip6Proto->setFieldData( Ip6Protocol::ip6_srcAddress, srcAddr->text()); ip6Proto->setFieldData( Ip6Protocol::ip6_srcAddrMode, srcAddrModeCombo->currentIndex()); ip6Proto->setFieldData( Ip6Protocol::ip6_srcAddrCount, srcAddrCount->text()); ip6Proto->setFieldData( Ip6Protocol::ip6_srcAddrPrefix, srcAddrPrefix->text().remove('/')); ip6Proto->setFieldData( Ip6Protocol::ip6_dstAddress, dstAddr->text()); ip6Proto->setFieldData( Ip6Protocol::ip6_dstAddrMode, dstAddrModeCombo->currentIndex()); ip6Proto->setFieldData( Ip6Protocol::ip6_dstAddrCount, dstAddrCount->text()); ip6Proto->setFieldData( Ip6Protocol::ip6_dstAddrPrefix, dstAddrPrefix->text().remove('/')); } ostinato-1.3.0/common/ip6config.h000066400000000000000000000024561451413623100167070ustar00rootroot00000000000000/* Copyright (C) 2010-2014 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _IP6_CONFIG_H #define _IP6_CONFIG_H #include "abstractprotocolconfig.h" #include "ui_ip6.h" class Ip6ConfigForm : public AbstractProtocolConfigForm, private Ui::Ip6 { Q_OBJECT public: Ip6ConfigForm(QWidget *parent = 0); static AbstractProtocolConfigForm* createInstance(); virtual void loadWidget(AbstractProtocol *ip6Proto); virtual void storeWidget(AbstractProtocol *ip6Proto); private slots: void on_srcAddr_editingFinished(); void on_dstAddr_editingFinished(); void on_srcAddrModeCombo_currentIndexChanged(int index); void on_dstAddrModeCombo_currentIndexChanged(int index); }; #endif ostinato-1.3.0/common/ip6edit.h000066400000000000000000000033621451413623100163640ustar00rootroot00000000000000/* Copyright (C) 2016 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _IP6_EDIT_H #define _IP6_EDIT_H #include "ipv6addressvalidator.h" #include "uint128.h" #include #include class Ip6Edit: public QLineEdit { public: Ip6Edit(QWidget *parent = 0); UInt128 value(); quint64 valueHi64(); quint64 valueLo64(); void setValue(UInt128 val); void setValue(quint64 hi, quint64 lo); void setValue(const QString &val); }; inline Ip6Edit::Ip6Edit(QWidget *parent) : QLineEdit(parent) { setValidator(new IPv6AddressValidator(this)); } inline UInt128 Ip6Edit::value() { Q_IPV6ADDR addr = QHostAddress(text()).toIPv6Address(); return UInt128((quint8*)&addr); } inline quint64 Ip6Edit::valueHi64() { return value().hi64(); } inline quint64 Ip6Edit::valueLo64() { return value().lo64(); } inline void Ip6Edit::setValue(UInt128 val) { setText(QHostAddress(val.toArray()).toString()); } inline void Ip6Edit::setValue(quint64 hi, quint64 lo) { UInt128 ip(hi, lo); setValue(ip); } inline void Ip6Edit::setValue(const QString &val) { setText(QHostAddress(val).toString()); } #endif ostinato-1.3.0/common/ip6over4.h000066400000000000000000000016101451413623100164700ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _IP_6_OVER_4_H #define _IP_6_OVER_4_H #include "comboprotocol.h" #include "ip4.h" #include "ip6.h" typedef ComboProtocol Ip6over4Protocol; #endif ostinato-1.3.0/common/ip6over4.proto000066400000000000000000000015521451413623100174110ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ import "protocol.proto"; package OstProto; // IP Tunelling - IP 6over4 message Ip6over4 { // Empty since this is a 'combo' protocol } extend Protocol { optional Ip6over4 ip6over4 = 303; } ostinato-1.3.0/common/ip6over4config.h000066400000000000000000000020171451413623100176600ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _IP_6_OVER_4_CONFIG_H #define _IP_6_OVER_4_CONFIG_H #include "comboprotocolconfig.h" #include "ip4config.h" #include "ip6config.h" #include "ip4.h" #include "ip6.h" typedef ComboProtocolConfigForm < OstProto::Protocol::kIp6over4FieldNumber, Ip4ConfigForm, Ip6ConfigForm, Ip4Protocol, Ip6Protocol > Ip6over4ConfigForm; #endif ostinato-1.3.0/common/ip6over6.h000066400000000000000000000057251451413623100165050ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _IP_6_OVER_6_H #define _IP_6_OVER_6_H #include "ip6over6.pb.h" #include "comboprotocol.h" #include "ip6.h" typedef ComboProtocol Ip6over6Combo; class Ip6over6Protocol : public Ip6over6Combo { public: Ip6over6Protocol(StreamBase *stream, AbstractProtocol *parent = 0) : Ip6over6Combo(stream, parent) { } static Ip6over6Protocol* createInstance(StreamBase *stream, AbstractProtocol *parent) { return new Ip6over6Protocol(stream, parent); } virtual void protoDataCopyInto(OstProto::Protocol &protocol) const { OstProto::Protocol tempProto; protoA->protoDataCopyInto(tempProto); protocol.MutableExtension(OstProto::ip6over6) ->MutableExtension(OstProto::ip6_outer) ->CopyFrom(tempProto.GetExtension(OstProto::ip6)); tempProto.Clear(); protoB->protoDataCopyInto(tempProto); protocol.MutableExtension(OstProto::ip6over6) ->MutableExtension(OstProto::ip6_inner) ->CopyFrom(tempProto.GetExtension(OstProto::ip6)); protocol.mutable_protocol_id()->set_id(protocolNumber()); } virtual void protoDataCopyFrom(const OstProto::Protocol &protocol) { if (protocol.protocol_id().id() == protocolNumber() && protocol.HasExtension(OstProto::ip6over6)) { OstProto::Protocol tempProto; // NOTE: To use protoX->protoDataCopyFrom() we need to arrange // so that it sees its own protocolNumber() and its own extension // in 'protocol' tempProto.mutable_protocol_id()->set_id(protoA->protocolNumber()); tempProto.MutableExtension(OstProto::ip6)->CopyFrom( protocol.GetExtension(OstProto::ip6over6).GetExtension( OstProto::ip6_outer)); protoA->protoDataCopyFrom(tempProto); tempProto.Clear(); tempProto.mutable_protocol_id()->set_id(protoB->protocolNumber()); tempProto.MutableExtension(OstProto::ip6)->CopyFrom( protocol.GetExtension(OstProto::ip6over6).GetExtension( OstProto::ip6_inner)); protoB->protoDataCopyFrom(tempProto); } } }; #endif ostinato-1.3.0/common/ip6over6.proto000066400000000000000000000016741451413623100174200ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ import "protocol.proto"; import "ip6.proto"; package OstProto; // IP Tunnelling - IP 6over6 message Ip6over6 { extensions 1 to 2; } extend Ip6over6 { optional Ip6 ip6_outer = 1; optional Ip6 ip6_inner = 2; } extend Protocol { optional Ip6over6 ip6over6 = 306; } ostinato-1.3.0/common/ip6over6config.h000066400000000000000000000017471451413623100176730ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _IP_6_OVER_6_CONFIG_H #define _IP_6_OVER_6_CONFIG_H #include "comboprotocolconfig.h" #include "ip6config.h" #include "ip6.h" typedef ComboProtocolConfigForm < OstProto::Protocol::kIp6over6FieldNumber, Ip6ConfigForm, Ip6ConfigForm, Ip6Protocol, Ip6Protocol > Ip6over6ConfigForm; #endif ostinato-1.3.0/common/ip6pdml.cpp000066400000000000000000000053341451413623100167270ustar00rootroot00000000000000/* Copyright (C) 2011 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #include "ip6pdml.h" #include "ip6.pb.h" PdmlIp6Protocol::PdmlIp6Protocol() { ostProtoId_ = OstProto::Protocol::kIp6FieldNumber; fieldMap_.insert("ipv6.version", OstProto::Ip6::kVersionFieldNumber); // Tshark 1.x uses .class while 2.x uses .tclass - we'll use either fieldMap_.insert("ipv6.class", OstProto::Ip6::kTrafficClassFieldNumber); fieldMap_.insert("ipv6.tclass", OstProto::Ip6::kTrafficClassFieldNumber); fieldMap_.insert("ipv6.flow", OstProto::Ip6::kFlowLabelFieldNumber); fieldMap_.insert("ipv6.plen", OstProto::Ip6::kPayloadLengthFieldNumber); fieldMap_.insert("ipv6.nxt", OstProto::Ip6::kNextHeaderFieldNumber); fieldMap_.insert("ipv6.hlim", OstProto::Ip6::kHopLimitFieldNumber); // ipv6.src and ipv6.dst handled as unknown fields } PdmlProtocol* PdmlIp6Protocol::createInstance() { return new PdmlIp6Protocol(); } void PdmlIp6Protocol::unknownFieldHandler(QString name, int /*pos*/, int /*size*/, const QXmlStreamAttributes &attributes, OstProto::Protocol *pbProto, OstProto::Stream* /*stream*/) { bool isOk; if (name == "ipv6.src") { OstProto::Ip6 *ip6 = pbProto->MutableExtension(OstProto::ip6); QString addrHexStr = attributes.value("value").toString(); ip6->set_src_addr_hi(addrHexStr.left(16).toULongLong(&isOk, kBaseHex)); ip6->set_src_addr_lo(addrHexStr.right(16).toULongLong(&isOk, kBaseHex)); } else if (name == "ipv6.dst") { OstProto::Ip6 *ip6 = pbProto->MutableExtension(OstProto::ip6); QString addrHexStr = attributes.value("value").toString(); ip6->set_dst_addr_hi(addrHexStr.left(16).toULongLong(&isOk, kBaseHex)); ip6->set_dst_addr_lo(addrHexStr.right(16).toULongLong(&isOk, kBaseHex)); } } void PdmlIp6Protocol::postProtocolHandler(OstProto::Protocol *pbProto, OstProto::Stream* /*stream*/) { OstProto::Ip6 *ip6 = pbProto->MutableExtension(OstProto::ip6); ip6->set_is_override_version(true); ip6->set_is_override_payload_length(true); ip6->set_is_override_next_header(true); } ostinato-1.3.0/common/ip6pdml.h000066400000000000000000000022361451413623100163720ustar00rootroot00000000000000/* Copyright (C) 2011 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _IP6_PDML_H #define _IP6_PDML_H #include "pdmlprotocol.h" class PdmlIp6Protocol : public PdmlProtocol { public: static PdmlProtocol* createInstance(); virtual void unknownFieldHandler(QString name, int pos, int size, const QXmlStreamAttributes &attributes, OstProto::Protocol *pbProto, OstProto::Stream *stream); virtual void postProtocolHandler(OstProto::Protocol *pbProto, OstProto::Stream *stream); protected: PdmlIp6Protocol(); }; #endif ostinato-1.3.0/common/iputils.h000066400000000000000000000100031451413623100164770ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _IP_UTILS_H #define _IP_UTILS_H #include "uint128.h" #include namespace ipUtils { enum AddrMode { kFixed = 0, kIncrement = 1, kDecrement = 2, kRandom = 3 }; quint32 inline ipAddress(quint32 baseIp, int prefix, AddrMode mode, int count, int index) { int u; quint32 mask = ((1< 64) { p = 64; q = prefix - 64; } else { p = prefix; q = 0; } if (p > 0) maskHi = ~((quint64(1) << p) - 1); if (q > 0) maskLo = ~((quint64(1) << q) - 1); prefixHi = baseIpHi & maskHi; prefixLo = baseIpLo & maskLo; if (mode == kIncrement) { hostHi = ((baseIpHi & ~maskHi) + 0) & ~maskHi; hostLo = ((baseIpLo & ~maskLo) + u) & ~maskLo; } else if (mode == kDecrement) { hostHi = ((baseIpHi & ~maskHi) - 0) & ~maskHi; hostLo = ((baseIpLo & ~maskLo) - u) & ~maskLo; } else if (mode==kRandom) { hostHi = qrand() & ~maskHi; hostLo = qrand() & ~maskLo; } ipHi = prefixHi | hostHi; ipLo = prefixLo | hostLo; break; default: qWarning("Unhandled mode = %d", mode); } } UInt128 inline ip6StringToUInt128(QString ip6Str) { Q_IPV6ADDR addr = QHostAddress(ip6Str).toIPv6Address(); quint64 hi, lo; hi = (quint64(addr[0]) << 56) | (quint64(addr[1]) << 48) | (quint64(addr[2]) << 40) | (quint64(addr[3]) << 32) | (quint64(addr[4]) << 24) | (quint64(addr[5]) << 16) | (quint64(addr[6]) << 8) | (quint64(addr[7]) << 0); lo = (quint64(addr[ 8]) << 56) | (quint64(addr[ 9]) << 48) | (quint64(addr[10]) << 40) | (quint64(addr[11]) << 32) | (quint64(addr[12]) << 24) | (quint64(addr[13]) << 16) | (quint64(addr[14]) << 8) | (quint64(addr[15]) << 0); return UInt128(hi, lo); } } // namespace ipUtils #endif ostinato-1.3.0/common/ipv4addressdelegate.h000066400000000000000000000030671451413623100207450ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _IPV4_ADDRESS_DELEGATE #define _IPV4_ADDRESS_DELEGATE #include #include class IPv4AddressDelegate : public QItemDelegate { Q_OBJECT public: IPv4AddressDelegate(QObject *parent = 0); ~IPv4AddressDelegate(); QWidget* createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const; }; inline IPv4AddressDelegate::IPv4AddressDelegate(QObject *parent) : QItemDelegate(parent) { } inline IPv4AddressDelegate::~IPv4AddressDelegate() { } inline QWidget* IPv4AddressDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const { QLineEdit *ipEdit; ipEdit = static_cast(QItemDelegate::createEditor( parent, option, index)); ipEdit->setInputMask("000.000.000.000;"); // FIXME: use validator return ipEdit; } #endif ostinato-1.3.0/common/ipv4addressvalidator.h000066400000000000000000000025111451413623100211510ustar00rootroot00000000000000/* Copyright (C) 2021 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _IPV4_ADDRESS_VALIDATOR_H #define _IPV4_ADDRESS_VALIDATOR_H #include #include class IPv4AddressValidator : public QRegularExpressionValidator { public: IPv4AddressValidator(QObject *parent = 0) : QRegularExpressionValidator( QRegularExpression( "((25[0-5]|(2[0-4]|1\\d|[1-9]|)\\d)(\\.(?!$)|$)){4}"), parent) { } virtual void fixup(QString &input) const { QStringList bytes = input.split('.', QString::SkipEmptyParts); while (bytes.count() < 4) bytes.append("0"); input = bytes.join('.'); } }; #endif ostinato-1.3.0/common/ipv6addressdelegate.h000066400000000000000000000031201451413623100207350ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _IPV4_ADDRESS_DELEGATE #define _IPV4_ADDRESS_DELEGATE #include "ipv6addressvalidator.h" #include #include class IPv6AddressDelegate : public QItemDelegate { Q_OBJECT public: IPv6AddressDelegate(QObject *parent = 0); ~IPv6AddressDelegate(); QWidget* createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const; }; inline IPv6AddressDelegate::IPv6AddressDelegate(QObject *parent) : QItemDelegate(parent) { } inline IPv6AddressDelegate::~IPv6AddressDelegate() { } inline QWidget* IPv6AddressDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const { QLineEdit *ipEdit; ipEdit = static_cast(QItemDelegate::createEditor( parent, option, index)); ipEdit->setValidator(new IPv6AddressValidator(ipEdit)); return ipEdit; } #endif ostinato-1.3.0/common/ipv6addressvalidator.h000066400000000000000000000041061451413623100211550ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _IPV6_ADDRESS_VALIDATOR_H #define _IPV6_ADDRESS_VALIDATOR_H #include #include class IPv6AddressValidator : public QValidator { public: IPv6AddressValidator(QObject *parent = 0) : QValidator(parent) { _ip6ValidChars.setPattern("[0-9a-fA-F]{0,4}(:[0-9a-fA-F]{0,4}){0,7}"); } ~IPv6AddressValidator() {} virtual QValidator::State validate(QString &input, int& /*pos*/) const { QValidator::State state; QHostAddress addr(input); //qDebug("%s: %s (%d)", __FUNCTION__, qPrintable(input), pos); if (addr.protocol() == QAbstractSocket::IPv6Protocol) state = Acceptable; else if (_ip6ValidChars.exactMatch(input)) state = Intermediate; else state = Invalid; //qDebug("%s(%d): %s (%d), ", __FUNCTION__, state, //qPrintable(input), pos); return state; } virtual void fixup(QString &input) const { input.append("::"); QHostAddress addr(input); int len = input.size(); //qDebug("%s: %s", __FUNCTION__, qPrintable(input)); while (addr.protocol() != QAbstractSocket::IPv6Protocol) { len--; Q_ASSERT(len >= 0); addr.setAddress(input.left(len)); } input = addr.toString(); } private: QRegExp _ip6ValidChars; }; #endif ostinato-1.3.0/common/llc.cpp000066400000000000000000000152201451413623100161210ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #include "llc.h" LlcProtocol::LlcProtocol(StreamBase *stream, AbstractProtocol *parent) : AbstractProtocol(stream, parent) { } LlcProtocol::~LlcProtocol() { } AbstractProtocol* LlcProtocol::createInstance(StreamBase *stream, AbstractProtocol *parent) { return new LlcProtocol(stream, parent); } quint32 LlcProtocol::protocolNumber() const { return OstProto::Protocol::kLlcFieldNumber; } void LlcProtocol::protoDataCopyInto(OstProto::Protocol &protocol) const { protocol.MutableExtension(OstProto::llc)->CopyFrom(data); protocol.mutable_protocol_id()->set_id(protocolNumber()); } void LlcProtocol::protoDataCopyFrom(const OstProto::Protocol &protocol) { if (protocol.protocol_id().id() == protocolNumber() && protocol.HasExtension(OstProto::llc)) data.MergeFrom(protocol.GetExtension(OstProto::llc)); } QString LlcProtocol::name() const { return QString("802.3 Logical Link Control"); } QString LlcProtocol::shortName() const { return QString("LLC"); } AbstractProtocol::ProtocolIdType LlcProtocol::protocolIdType() const { return ProtocolIdLlc; } int LlcProtocol::fieldCount() const { return llc_fieldCount; } AbstractProtocol::FieldFlags LlcProtocol::fieldFlags(int index) const { AbstractProtocol::FieldFlags flags; flags = AbstractProtocol::fieldFlags(index); switch (index) { case llc_dsap: case llc_ssap: case llc_ctl: break; case llc_is_override_dsap: case llc_is_override_ssap: case llc_is_override_ctl: flags &= ~FrameField; flags |= MetaField; break; default: break; } return flags; } QVariant LlcProtocol::fieldData(int index, FieldAttrib attrib, int streamIndex) const { quint32 id; quint8 dsap, ssap, ctl; id = payloadProtocolId(ProtocolIdLlc); dsap = data.is_override_dsap() ? data.dsap() : (id >> 16) & 0xFF; ssap = data.is_override_ssap() ? data.ssap() : (id >> 8) & 0xFF; ctl = data.is_override_ctl() ? data.ctl() : (id >> 0) & 0xFF; switch (index) { case llc_dsap: switch(attrib) { case FieldName: return QString("DSAP"); case FieldValue: return dsap; case FieldTextValue: return QString("%1").arg(dsap, 2, BASE_HEX, QChar('0')); case FieldFrameValue: return QByteArray(1, (char)(dsap)); default: break; } break; case llc_ssap: switch(attrib) { case FieldName: return QString("SSAP"); case FieldValue: return ssap; case FieldTextValue: return QString("%1").arg(ssap, 2, BASE_HEX, QChar('0')); case FieldFrameValue: return QByteArray(1, (char)(ssap)); default: break; } break; case llc_ctl: switch(attrib) { case FieldName: return QString("Control"); case FieldValue: return ctl; case FieldTextValue: return QString("%1").arg(ctl, 2, BASE_HEX, QChar('0')); case FieldFrameValue: return QByteArray(1, (char)(ctl)); default: break; } break; // Meta fields case llc_is_override_dsap: { switch(attrib) { case FieldValue: return data.is_override_dsap(); default: break; } break; } case llc_is_override_ssap: { switch(attrib) { case FieldValue: return data.is_override_ssap(); default: break; } break; } case llc_is_override_ctl: { switch(attrib) { case FieldValue: return data.is_override_ctl(); default: break; } break; } default: qFatal("%s: unimplemented case %d in switch", __PRETTY_FUNCTION__, index); break; } return AbstractProtocol::fieldData(index, attrib, streamIndex); } bool LlcProtocol::setFieldData(int index, const QVariant &value, FieldAttrib attrib) { bool isOk = false; if (attrib != FieldValue) return false; switch (index) { case llc_dsap: { uint dsap = value.toUInt(&isOk) & 0xFF; if (isOk) data.set_dsap(dsap); break; } case llc_ssap: { uint ssap = value.toUInt(&isOk) & 0xFF; if (isOk) data.set_ssap(ssap); break; } case llc_ctl: { uint ctl = value.toUInt(&isOk) & 0xFF; if (isOk) data.set_ctl(ctl); break; } case llc_is_override_dsap: { bool ovr = value.toBool(); data.set_is_override_dsap(ovr); isOk = true; break; } case llc_is_override_ssap: { bool ovr = value.toBool(); data.set_is_override_ssap(ovr); isOk = true; break; } case llc_is_override_ctl: { bool ovr = value.toBool(); data.set_is_override_ctl(ovr); isOk = true; break; } default: qFatal("%s: unimplemented case %d in switch", __PRETTY_FUNCTION__, index); break; } return isOk; } ostinato-1.3.0/common/llc.h000066400000000000000000000036001451413623100155650ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _LLC_H #define _LLC_H #include "abstractprotocol.h" #include "llc.pb.h" class LlcProtocol : public AbstractProtocol { public: enum llcfield { llc_dsap = 0, llc_ssap, llc_ctl, // Meta fields llc_is_override_dsap, llc_is_override_ssap, llc_is_override_ctl, llc_fieldCount }; LlcProtocol(StreamBase *stream, AbstractProtocol *parent = 0); virtual ~LlcProtocol(); static AbstractProtocol* createInstance(StreamBase *stream, AbstractProtocol *parent = 0); virtual quint32 protocolNumber() const; virtual void protoDataCopyInto(OstProto::Protocol &protocol) const; virtual void protoDataCopyFrom(const OstProto::Protocol &protocol); virtual QString name() const; virtual QString shortName() const; virtual ProtocolIdType protocolIdType() const; virtual int fieldCount() const; virtual AbstractProtocol::FieldFlags fieldFlags(int index) const; virtual QVariant fieldData(int index, FieldAttrib attrib, int streamIndex = 0) const; virtual bool setFieldData(int index, const QVariant &value, FieldAttrib attrib = FieldValue); private: OstProto::Llc data; }; #endif ostinato-1.3.0/common/llc.proto000066400000000000000000000017411451413623100165050ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ import "protocol.proto"; package OstProto; message Llc { optional bool is_override_dsap = 4; optional bool is_override_ssap = 5; optional bool is_override_ctl = 6; optional uint32 dsap = 1; optional uint32 ssap = 2; optional uint32 ctl = 3; } extend Protocol { optional Llc llc = 202; } ostinato-1.3.0/common/llc.ui000066400000000000000000000072511451413623100157610ustar00rootroot00000000000000 llc 0 0 396 98 0 0 Form LLC DSAP false >HH; SSAP false >HH; Control false >HH; Qt::Horizontal 20 20 Qt::Vertical 20 40 cbOverrideDsap toggled(bool) leDsap setEnabled(bool) 54 34 92 33 cbOverrideSsap toggled(bool) leSsap setEnabled(bool) 167 34 192 33 cbOverrideControl toggled(bool) leControl setEnabled(bool) 285 34 310 33 ostinato-1.3.0/common/llcconfig.cpp000066400000000000000000000054561451413623100173210ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #include "llcconfig.h" #include "llc.h" LlcConfigForm::LlcConfigForm(QWidget *parent) : AbstractProtocolConfigForm(parent) { setupUi(this); } LlcConfigForm::~LlcConfigForm() { } LlcConfigForm* LlcConfigForm::createInstance() { return new LlcConfigForm; } void LlcConfigForm::loadWidget(AbstractProtocol *proto) { cbOverrideDsap->setChecked( proto->fieldData( LlcProtocol::llc_is_override_dsap, AbstractProtocol::FieldValue ).toBool()); leDsap->setText(uintToHexStr( proto->fieldData( LlcProtocol::llc_dsap, AbstractProtocol::FieldValue ).toUInt(), 1)); cbOverrideSsap->setChecked( proto->fieldData( LlcProtocol::llc_is_override_ssap, AbstractProtocol::FieldValue ).toBool()); leSsap->setText(uintToHexStr( proto->fieldData( LlcProtocol::llc_ssap, AbstractProtocol::FieldValue ).toUInt(), 1)); cbOverrideControl->setChecked( proto->fieldData( LlcProtocol::llc_is_override_ctl, AbstractProtocol::FieldValue ).toBool()); leControl->setText(uintToHexStr( proto->fieldData( LlcProtocol::llc_ctl, AbstractProtocol::FieldValue ).toUInt(), 1)); } void LlcConfigForm::storeWidget(AbstractProtocol *proto) { bool isOk; proto->setFieldData( LlcProtocol::llc_is_override_dsap, cbOverrideDsap->isChecked()); proto->setFieldData( LlcProtocol::llc_dsap, leDsap->text().toUInt(&isOk, BASE_HEX)); proto->setFieldData( LlcProtocol::llc_is_override_ssap, cbOverrideSsap->isChecked()); proto->setFieldData( LlcProtocol::llc_ssap, leSsap->text().toUInt(&isOk, BASE_HEX)); proto->setFieldData( LlcProtocol::llc_is_override_ctl, cbOverrideControl->isChecked()); proto->setFieldData( LlcProtocol::llc_ctl, leControl->text().toUInt(&isOk, BASE_HEX)); } ostinato-1.3.0/common/llcconfig.h000066400000000000000000000021361451413623100167560ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _LLC_CONFIG_H #define _LLC_CONFIG_H #include "abstractprotocolconfig.h" #include "ui_llc.h" class LlcConfigForm : public AbstractProtocolConfigForm, private Ui::llc { Q_OBJECT public: LlcConfigForm(QWidget *parent = 0); virtual ~LlcConfigForm(); static LlcConfigForm* createInstance(); virtual void loadWidget(AbstractProtocol *proto); virtual void storeWidget(AbstractProtocol *proto); }; #endif ostinato-1.3.0/common/llcpdml.cpp000066400000000000000000000046551451413623100170100ustar00rootroot00000000000000/* Copyright (C) 2011 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #include "llcpdml.h" #include "llc.pb.h" #include "snap.pb.h" #include PdmlLlcProtocol::PdmlLlcProtocol() { ostProtoId_ = OstProto::Protocol::kLlcFieldNumber; fieldMap_.insert("llc.dsap", OstProto::Llc::kDsapFieldNumber); fieldMap_.insert("llc.ssap", OstProto::Llc::kSsapFieldNumber); fieldMap_.insert("llc.control", OstProto::Llc::kCtlFieldNumber); } PdmlProtocol* PdmlLlcProtocol::createInstance() { return new PdmlLlcProtocol(); } void PdmlLlcProtocol::unknownFieldHandler(QString name, int /*pos*/, int /*size*/, const QXmlStreamAttributes &attributes, OstProto::Protocol* /*pbProto*/, OstProto::Stream *stream) { if (name == "llc.oui") { OstProto::Protocol *proto = stream->add_protocol(); proto->mutable_protocol_id()->set_id( OstProto::Protocol::kSnapFieldNumber); OstProto::Snap *snap = proto->MutableExtension(OstProto::snap); bool isOk; snap->set_oui(attributes.value("value").toString() .toUInt(&isOk, kBaseHex)); snap->set_is_override_oui(true); } else if ((name == "llc.type") || (name.contains(QRegExp("llc\\..*pid")))) { OstProto::Snap *snap = stream->mutable_protocol( stream->protocol_size()-1)->MutableExtension(OstProto::snap); bool isOk; snap->set_type(attributes.value("value").toString() .toUInt(&isOk, kBaseHex)); snap->set_is_override_type(true); } } void PdmlLlcProtocol::postProtocolHandler(OstProto::Protocol *pbProto, OstProto::Stream* /*stream*/) { OstProto::Llc *llc = pbProto->MutableExtension(OstProto::llc); llc->set_is_override_dsap(true); llc->set_is_override_ssap(true); llc->set_is_override_ctl(true); } ostinato-1.3.0/common/llcpdml.h000066400000000000000000000022361451413623100164460ustar00rootroot00000000000000/* Copyright (C) 2011 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _LLC_PDML_H #define _LLC_PDML_H #include "pdmlprotocol.h" class PdmlLlcProtocol : public PdmlProtocol { public: static PdmlProtocol* createInstance(); virtual void unknownFieldHandler(QString name, int pos, int size, const QXmlStreamAttributes &attributes, OstProto::Protocol *pbProto, OstProto::Stream *stream); virtual void postProtocolHandler(OstProto::Protocol *pbProto, OstProto::Stream *stream); protected: PdmlLlcProtocol(); }; #endif ostinato-1.3.0/common/mac.cpp000066400000000000000000000276671451413623100161310ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #include "mac.h" #include "framevalueattrib.h" #include "../common/streambase.h" #include #define uintToMacStr(num) \ QString("%1").arg(num, 6*2, BASE_HEX, QChar('0')) \ .replace(QRegExp("([0-9a-fA-F]{2}\\B)"), "\\1:").toUpper() MacProtocol::MacProtocol(StreamBase *stream, AbstractProtocol *parent) : AbstractProtocol(stream, parent) { forResolve_ = false; } MacProtocol::~MacProtocol() { } AbstractProtocol* MacProtocol::createInstance(StreamBase *stream , AbstractProtocol *parent) { return new MacProtocol(stream, parent); } quint32 MacProtocol::protocolNumber() const { return OstProto::Protocol::kMacFieldNumber; } void MacProtocol::protoDataCopyInto(OstProto::Protocol &protocol) const { protocol.MutableExtension(OstProto::mac)->CopyFrom(data); protocol.mutable_protocol_id()->set_id(protocolNumber()); } void MacProtocol::protoDataCopyFrom(const OstProto::Protocol &protocol) { if (protocol.protocol_id().id() == protocolNumber() && protocol.HasExtension(OstProto::mac)) data.MergeFrom(protocol.GetExtension(OstProto::mac)); } QString MacProtocol::name() const { return QString("Media Access Protocol"); } QString MacProtocol::shortName() const { return QString("MAC"); } int MacProtocol::fieldCount() const { return mac_fieldCount; } AbstractProtocol::FieldFlags MacProtocol::fieldFlags(int index) const { AbstractProtocol::FieldFlags flags; flags = AbstractProtocol::fieldFlags(index); switch (index) { case mac_dstAddr: case mac_srcAddr: break; case mac_dstMacMode: case mac_dstMacCount: case mac_dstMacStep: case mac_srcMacMode: case mac_srcMacCount: case mac_srcMacStep: flags &= ~FrameField; flags |= MetaField; break; } return flags; } QVariant MacProtocol::fieldData(int index, FieldAttrib attrib, int streamIndex) const { switch (index) { case mac_dstAddr: { int u; quint64 dstMac = 0; switch (data.dst_mac_mode()) { case OstProto::Mac::e_mm_fixed: dstMac = data.dst_mac(); break; case OstProto::Mac::e_mm_inc: u = (streamIndex % data.dst_mac_count()) * data.dst_mac_step(); dstMac = data.dst_mac() + u; break; case OstProto::Mac::e_mm_dec: u = (streamIndex % data.dst_mac_count()) * data.dst_mac_step(); dstMac = data.dst_mac() - u; break; case OstProto::Mac::e_mm_resolve: if (forResolve_) dstMac = 0; else { forResolve_ = true; dstMac = mpStream->neighborMacAddress(streamIndex); forResolve_ = false; } break; default: qWarning("Unhandled dstMac_mode %d", data.dst_mac_mode()); } switch(attrib) { case FieldName: return QString("Destination"); case FieldValue: return dstMac; case FieldTextValue: return uintToMacStr(dstMac); case FieldFrameValue: { QByteArray fv; fv.resize(8); qToBigEndian(dstMac, (uchar*) fv.data()); fv.remove(0, 2); return fv; } default: break; } break; } case mac_srcAddr: { int u; quint64 srcMac = 0; switch (data.src_mac_mode()) { case OstProto::Mac::e_mm_fixed: srcMac = data.src_mac(); break; case OstProto::Mac::e_mm_inc: u = (streamIndex % data.src_mac_count()) * data.src_mac_step(); srcMac = data.src_mac() + u; break; case OstProto::Mac::e_mm_dec: u = (streamIndex % data.src_mac_count()) * data.src_mac_step(); srcMac = data.src_mac() - u; break; case OstProto::Mac::e_mm_resolve: if (forResolve_) srcMac = 0; else { forResolve_ = true; srcMac = mpStream->deviceMacAddress(streamIndex); forResolve_ = false; } break; default: qWarning("Unhandled srcMac_mode %d", data.src_mac_mode()); } switch(attrib) { case FieldName: return QString("Source"); case FieldValue: return srcMac; case FieldTextValue: return uintToMacStr(srcMac); case FieldFrameValue: { QByteArray fv; fv.resize(8); qToBigEndian(srcMac, (uchar*) fv.data()); fv.remove(0, 2); return fv; } default: break; } break; } // Meta fields case mac_dstMacMode: switch(attrib) { case FieldValue: return data.dst_mac_mode(); default: break; } break; case mac_dstMacCount: switch(attrib) { case FieldValue: return data.dst_mac_count(); default: break; } break; case mac_dstMacStep: switch(attrib) { case FieldValue: return data.dst_mac_step(); default: break; } break; case mac_srcMacMode: switch(attrib) { case FieldValue: return data.src_mac_mode(); default: break; } break; case mac_srcMacCount: switch(attrib) { case FieldValue: return data.src_mac_count(); default: break; } break; case mac_srcMacStep: switch(attrib) { case FieldValue: return data.src_mac_step(); default: break; } break; default: break; } return AbstractProtocol::fieldData(index, attrib, streamIndex); } bool MacProtocol::setFieldData(int index, const QVariant &value, FieldAttrib attrib) { bool isOk = false; if (attrib != FieldValue) goto _exit; switch (index) { case mac_dstAddr: { quint64 mac = value.toULongLong(&isOk); if (isOk) data.set_dst_mac(mac); break; } case mac_srcAddr: { quint64 mac = value.toULongLong(&isOk); if (isOk) data.set_src_mac(mac); break; } // Meta-Fields case mac_dstMacMode: { uint mode = value.toUInt(&isOk); if (isOk && data.MacAddrMode_IsValid(mode)) data.set_dst_mac_mode((OstProto::Mac::MacAddrMode) mode); else isOk = false; break; } case mac_dstMacCount: { uint count = value.toUInt(&isOk); if (isOk) data.set_dst_mac_count(count); break; } case mac_dstMacStep: { uint step = value.toUInt(&isOk); if (isOk) data.set_dst_mac_step(step); break; } case mac_srcMacMode: { uint mode = value.toUInt(&isOk); if (isOk && data.MacAddrMode_IsValid(mode)) data.set_src_mac_mode((OstProto::Mac::MacAddrMode) mode); else isOk = false; break; } case mac_srcMacCount: { uint count = value.toUInt(&isOk); if (isOk) data.set_src_mac_count(count); break; } case mac_srcMacStep: { uint step = value.toUInt(&isOk); if (isOk) data.set_src_mac_step(step); break; } default: qFatal("%s: unimplemented case %d in switch", __PRETTY_FUNCTION__, index); break; } _exit: return isOk; } int MacProtocol::protocolFrameVariableCount() const { int count = AbstractProtocol::protocolFrameVariableCount(); switch (data.dst_mac_mode()) { case OstProto::Mac::e_mm_inc: case OstProto::Mac::e_mm_dec: count = AbstractProtocol::lcm(count, data.dst_mac_count()); break; default: break; } switch (data.src_mac_mode()) { case OstProto::Mac::e_mm_inc: case OstProto::Mac::e_mm_dec: count = AbstractProtocol::lcm(count, data.src_mac_count()); break; default: break; } return count; } QByteArray MacProtocol::protocolFrameValue(int streamIndex, bool /*forCksum*/, FrameValueAttrib *attrib) const { QByteArray ba; ba.resize(12); quint64 dstMac = fieldData(mac_dstAddr, FieldValue, streamIndex) .toULongLong(); quint64 srcMac = fieldData(mac_srcAddr, FieldValue, streamIndex) .toULongLong(); char *p = ba.data(); *(quint32*)(p ) = qToBigEndian(quint32(dstMac >> 16)); *(quint16*)(p + 4) = qToBigEndian(quint16(dstMac & 0xffff)); *(quint32*)(p + 6) = qToBigEndian(quint32(srcMac >> 16)); *(quint16*)(p + 10) = qToBigEndian(quint16(srcMac & 0xffff)); if (attrib) { if (!dstMac && data.dst_mac_mode() == OstProto::Mac::e_mm_resolve) attrib->errorFlags |= FrameValueAttrib::UnresolvedDstMacError; if (!srcMac && data.src_mac_mode() == OstProto::Mac::e_mm_resolve) attrib->errorFlags |= FrameValueAttrib::UnresolvedSrcMacError; } return ba; } bool MacProtocol::hasErrors(QStringList *errors) const { bool result = false; if ((data.dst_mac() == 0ULL) && (data.dst_mac_mode() != OstProto::Mac::e_mm_resolve)) { if (errors) *errors << QObject::tr("Frames with Destination Mac " "00:00:00:00:00:00 are likely " "to be dropped"); result = true; } if ((data.src_mac() == 0ULL) && (data.src_mac_mode() != OstProto::Mac::e_mm_resolve)) { if (errors) *errors << QObject::tr("Frames with Source Mac " "00:00:00:00:00:00 are likely " "to be dropped"); result = true; } return result; } ostinato-1.3.0/common/mac.h000066400000000000000000000042101451413623100155510ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _MAC_H #define _MAC_H #include "abstractprotocol.h" #include "mac.pb.h" class MacProtocol : public AbstractProtocol { public: enum macfield { mac_dstAddr = 0, mac_srcAddr, mac_dstMacMode, mac_dstMacCount, mac_dstMacStep, mac_srcMacMode, mac_srcMacCount, mac_srcMacStep, mac_fieldCount }; MacProtocol(StreamBase *stream, AbstractProtocol *parent = 0); virtual ~MacProtocol(); static AbstractProtocol* createInstance(StreamBase *stream, AbstractProtocol *parent = 0); virtual quint32 protocolNumber() const; virtual void protoDataCopyInto(OstProto::Protocol &protocol) const; virtual void protoDataCopyFrom(const OstProto::Protocol &protocol); virtual QString name() const; virtual QString shortName() const; virtual int fieldCount() const; virtual AbstractProtocol::FieldFlags fieldFlags(int index) const; virtual QVariant fieldData(int index, FieldAttrib attrib, int streamIndex = 0) const; virtual bool setFieldData(int index, const QVariant &value, FieldAttrib attrib = FieldValue); virtual int protocolFrameVariableCount() const; virtual QByteArray protocolFrameValue(int streamIndex = 0, bool forCksum = false, FrameValueAttrib *attrib = nullptr) const; virtual bool hasErrors(QStringList *errors = nullptr) const; private: OstProto::Mac data; mutable bool forResolve_; }; #endif ostinato-1.3.0/common/mac.proto000066400000000000000000000027261451413623100164770ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ import "protocol.proto"; package OstProto; // Ethernet message Mac { enum MacAddrMode { e_mm_fixed = 0; e_mm_inc = 1; e_mm_dec = 2; e_mm_resolve = 3; // dst: resolve neighbor; src: from device config } // Dst Mac optional uint64 dst_mac = 1; optional MacAddrMode dst_mac_mode = 2 [default = e_mm_resolve]; optional uint32 dst_mac_count = 3 [default = 16]; optional uint32 dst_mac_step = 4 [default = 1]; // Src Mac optional uint64 src_mac = 5; optional MacAddrMode src_mac_mode = 6 [default = e_mm_resolve]; optional uint32 src_mac_count = 7 [default = 16]; optional uint32 src_mac_step = 8 [default = 1]; } extend Protocol { optional Mac mac = 100; } ostinato-1.3.0/common/mac.ui000066400000000000000000000107161451413623100157470ustar00rootroot00000000000000 mac 0 0 400 200 Form Mode Address Count Step Destination Fixed Increment Decrement Resolve 120 0 false false Source Fixed Increment Decrement Resolve false false Please ensure that a corresponding device is configured on the port to enable source/destination mac address resolution. A corresponding device is one which has VLANs and source/gateway IP corresponding to this stream. true Qt::Vertical 20 40 IntEdit QSpinBox
intedit.h
MacEdit QLineEdit
macedit.h
ostinato-1.3.0/common/macaddressvalidator.h000066400000000000000000000027561451413623100210420ustar00rootroot00000000000000/* Copyright (C) 2021 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _MAC_ADDRESS_VALIDATOR_H #define _MAC_ADDRESS_VALIDATOR_H #include // Allow : or - as separator class MacAddressValidator : public QRegularExpressionValidator { public: MacAddressValidator(QObject *parent = 0) : QRegularExpressionValidator( QRegularExpression( "([0-9,a-f,A-F]{2,2}[:-]){5,5}[0-9,a-f,A-F]{2,2}"), parent) { } virtual void fixup(QString &input) const { QStringList bytes = input.split(QRegularExpression("[:-]"), QString::SkipEmptyParts); if (!bytes.isEmpty() && bytes.last().size() == 1) bytes.last().prepend("0"); while (bytes.count() < 6) bytes.append("00"); input = bytes.join(":"); } }; #endif ostinato-1.3.0/common/macconfig.cpp000066400000000000000000000123301451413623100172740ustar00rootroot00000000000000/* Copyright (C) 2010,2013-2014 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #include "macconfig.h" #include "mac.h" MacConfigForm::MacConfigForm(QWidget *parent) : AbstractProtocolConfigForm(parent) { setupUi(this); resolveInfo->hide(); #if 0 // not working for some reason resolveInfo->setPixmap(resolveInfo->style()->standardIcon( QStyle::SP_MessageBoxInformation).pixmap(128)); #endif leDstMacCount->setMinimum(1); leSrcMacCount->setMinimum(1); leDstMacStep->setMinimum(0); leSrcMacStep->setMinimum(0); } MacConfigForm::~MacConfigForm() { } MacConfigForm* MacConfigForm::createInstance() { MacConfigForm *f = new MacConfigForm; return f; } void MacConfigForm::on_cmbDstMacMode_currentIndexChanged(int index) { switch (index) { case OstProto::Mac::e_mm_resolve: leDstMac->setEnabled(false); leDstMacCount->setEnabled(false); leDstMacStep->setEnabled(false); break; case OstProto::Mac::e_mm_fixed: leDstMac->setEnabled(true); leDstMacCount->setEnabled(false); leDstMacStep->setEnabled(false); break; default: leDstMac->setEnabled(true); leDstMacCount->setEnabled(true); leDstMacStep->setEnabled(true); break; } resolveInfo->setVisible( cmbDstMacMode->currentIndex() == OstProto::Mac::e_mm_resolve || cmbSrcMacMode->currentIndex() == OstProto::Mac::e_mm_resolve); } void MacConfigForm::on_cmbSrcMacMode_currentIndexChanged(int index) { switch (index) { case OstProto::Mac::e_mm_resolve: leSrcMac->setEnabled(false); leSrcMacCount->setEnabled(false); leSrcMacStep->setEnabled(false); break; case OstProto::Mac::e_mm_fixed: leSrcMac->setEnabled(true); leSrcMacCount->setEnabled(false); leSrcMacStep->setEnabled(false); break; default: leSrcMac->setEnabled(true); leSrcMacCount->setEnabled(true); leSrcMacStep->setEnabled(true); break; } resolveInfo->setVisible( cmbDstMacMode->currentIndex() == OstProto::Mac::e_mm_resolve || cmbSrcMacMode->currentIndex() == OstProto::Mac::e_mm_resolve); } void MacConfigForm::loadWidget(AbstractProtocol *proto) { leDstMac->setValue( proto->fieldData( MacProtocol::mac_dstAddr, AbstractProtocol::FieldValue ).toULongLong()); cmbDstMacMode->setCurrentIndex( proto->fieldData( MacProtocol::mac_dstMacMode, AbstractProtocol::FieldValue ).toUInt()); leDstMacCount->setValue( proto->fieldData( MacProtocol::mac_dstMacCount, AbstractProtocol::FieldValue ).toUInt()); leDstMacStep->setValue( proto->fieldData( MacProtocol::mac_dstMacStep, AbstractProtocol::FieldValue ).toUInt()); leSrcMac->setValue( proto->fieldData( MacProtocol::mac_srcAddr, AbstractProtocol::FieldValue ).toULongLong()); cmbSrcMacMode->setCurrentIndex( proto->fieldData( MacProtocol::mac_srcMacMode, AbstractProtocol::FieldValue ).toUInt()); leSrcMacCount->setValue( proto->fieldData( MacProtocol::mac_srcMacCount, AbstractProtocol::FieldValue ).toUInt()); leSrcMacStep->setValue( proto->fieldData( MacProtocol::mac_srcMacStep, AbstractProtocol::FieldValue ).toUInt()); } void MacConfigForm::storeWidget(AbstractProtocol *proto) { proto->setFieldData( MacProtocol::mac_dstAddr, leDstMac->value()); proto->setFieldData( MacProtocol::mac_dstMacMode, cmbDstMacMode->currentIndex()); proto->setFieldData( MacProtocol::mac_dstMacCount, leDstMacCount->value()); proto->setFieldData( MacProtocol::mac_dstMacStep, leDstMacStep->value()); proto->setFieldData( MacProtocol::mac_srcAddr, leSrcMac->value()); proto->setFieldData( MacProtocol::mac_srcMacMode, cmbSrcMacMode->currentIndex()); proto->setFieldData( MacProtocol::mac_srcMacCount, leSrcMacCount->value()); proto->setFieldData( MacProtocol::mac_srcMacStep, leSrcMacStep->value()); } ostinato-1.3.0/common/macconfig.h000066400000000000000000000023461451413623100167470ustar00rootroot00000000000000/* Copyright (C) 2010-2012 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _MAC_CONFIG_H #define _MAC_CONFIG_H #include "abstractprotocolconfig.h" #include "ui_mac.h" class MacConfigForm : public AbstractProtocolConfigForm, private Ui::mac { Q_OBJECT public: MacConfigForm(QWidget *parent = 0); virtual ~MacConfigForm(); static MacConfigForm* createInstance(); virtual void loadWidget(AbstractProtocol *proto); virtual void storeWidget(AbstractProtocol *proto); private slots: void on_cmbDstMacMode_currentIndexChanged(int index); void on_cmbSrcMacMode_currentIndexChanged(int index); }; #endif ostinato-1.3.0/common/macedit.h000066400000000000000000000034661451413623100164330ustar00rootroot00000000000000/* Copyright (C) 2016 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _MAC_EDIT_H #define _MAC_EDIT_H #include #include class MacEdit: public QLineEdit { public: MacEdit(QWidget *parent = 0); quint64 value(); void setValue(quint64 val); protected: virtual void focusOutEvent(QFocusEvent *e); }; inline MacEdit::MacEdit(QWidget *parent) : QLineEdit(parent) { // Allow : or - as separator QRegExp reMac("([0-9,a-f,A-F]{0,2}[:-]){5,5}[0-9,a-f,A-F]{0,2}"); setValidator(new QRegExpValidator(reMac, this)); } inline quint64 MacEdit::value() { QStringList bytes = text().split(QRegExp("[:-]")); quint64 mac = 0; while (bytes.count() > 6) bytes.removeLast(); for (int i = 0; i < bytes.count(); i++) mac |= (bytes.at(i).toULongLong(NULL, 16) & 0xff) << (5-i)*8; return mac; } inline void MacEdit::setValue(quint64 val) { setText(QString("%1").arg(val, 6*2, 16, QChar('0')) .replace(QRegExp("([0-9a-fA-F]{2}\\B)"), "\\1:").toUpper()); } inline void MacEdit::focusOutEvent(QFocusEvent *e) { // be helpful and show a well-formatted value on focus out setValue(value()); QLineEdit::focusOutEvent(e); } #endif ostinato-1.3.0/common/mld.cpp000066400000000000000000000433101451413623100161240ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #include "mld.h" #include "iputils.h" #include #include MldProtocol::MldProtocol(StreamBase *stream, AbstractProtocol *parent) : GmpProtocol(stream, parent) { _hasPayload = false; data.set_type(kMldV1Query); } MldProtocol::~MldProtocol() { } AbstractProtocol* MldProtocol::createInstance(StreamBase *stream, AbstractProtocol *parent) { return new MldProtocol(stream, parent); } quint32 MldProtocol::protocolNumber() const { return OstProto::Protocol::kMldFieldNumber; } void MldProtocol::protoDataCopyInto(OstProto::Protocol &protocol) const { protocol.MutableExtension(OstProto::mld)->CopyFrom(data); protocol.mutable_protocol_id()->set_id(protocolNumber()); } void MldProtocol::protoDataCopyFrom(const OstProto::Protocol &protocol) { if (protocol.protocol_id().id() == protocolNumber() && protocol.HasExtension(OstProto::mld)) data.MergeFrom(protocol.GetExtension(OstProto::mld)); } QString MldProtocol::name() const { return QString("Multicast Listener Discovery"); } QString MldProtocol::shortName() const { return QString("MLD"); } quint32 MldProtocol::protocolId(ProtocolIdType type) const { switch(type) { case ProtocolIdIp: return 0x3a; default:break; } return AbstractProtocol::protocolId(type); } AbstractProtocol::FieldFlags MldProtocol::fieldFlags(int index) const { AbstractProtocol::FieldFlags flags; flags = GmpProtocol::fieldFlags(index); switch(index) { case kMldMrt: case kMldRsvd: if (msgType() != kMldV2Report) flags |= FrameField; break; default: break; } return flags; } QVariant MldProtocol::fieldData(int index, FieldAttrib attrib, int streamIndex) const { switch (index) { case kRsvdMrtCode: { switch(attrib) { case FieldName: return QString("Code"); default: break; } break; } case kMldMrt: { quint16 mrt = 0, mrcode = 0; if (msgType() == kMldV2Query) { mrt = data.max_response_time(); mrcode = mrc(mrt); } else if (msgType() == kMldV1Query) mrcode = mrt = data.max_response_time() & 0xFFFF; switch(attrib) { case FieldName: if (isQuery()) return QString("Max Response Time"); return QString("Reserved"); case FieldValue: return mrt; case FieldTextValue: return QString("%1 ms").arg(mrt); case FieldFrameValue: { QByteArray fv; fv.resize(2); qToBigEndian(mrcode, (uchar*) fv.data()); return fv; } default: break; } break; } case kMldRsvd: { quint16 rsvd = 0; switch(attrib) { case FieldName: return QString("Reserved"); case FieldValue: return rsvd; case FieldTextValue: return QString("%1").arg(rsvd); case FieldFrameValue: { QByteArray fv; fv.resize(2); qToBigEndian(rsvd, (uchar*) fv.data()); return fv; } default: break; } break; } case kGroupAddress: { quint64 grpHi = 0, grpLo = 0; ipUtils::ipAddress( data.group_address().v6_hi(), data.group_address().v6_lo(), data.group_prefix(), ipUtils::AddrMode(data.group_mode()), data.group_count(), streamIndex, grpHi, grpLo); switch(attrib) { case FieldName: return QString("Group Address"); case FieldValue: return QVariant::fromValue(UInt128(grpHi, grpLo)); case FieldTextValue: case FieldFrameValue: { QByteArray fv; fv.resize(16); qToBigEndian(grpHi, (uchar*) fv.data()); qToBigEndian(grpLo, (uchar*) (fv.data() + 8)); if (attrib == FieldFrameValue) return fv; else return QHostAddress((quint8*)fv.constData()).toString(); } default: break; } break; } case kSources: { switch(attrib) { case FieldName: return QString("Source List"); case FieldValue: { QStringList list; QByteArray fv; fv.resize(16); for (int i = 0; i < data.sources_size(); i++) { qToBigEndian(quint64(data.sources(i).v6_hi()), (uchar*)fv.data()); qToBigEndian(quint64(data.sources(i).v6_lo()), (uchar*)fv.data()+8); list << QHostAddress((quint8*)fv.constData()).toString(); } return list; } case FieldFrameValue: { QByteArray fv; fv.resize(16 * data.sources_size()); for (int i = 0; i < data.sources_size(); i++) { qToBigEndian(quint64(data.sources(i).v6_hi()), (uchar*)(fv.data() + i*16)); qToBigEndian(quint64(data.sources(i).v6_lo()), (uchar*)(fv.data() + i*16 + 8)); } return fv; } case FieldTextValue: { QStringList list; QByteArray fv; fv.resize(16); for (int i = 0; i < data.sources_size(); i++) { qToBigEndian(quint64(data.sources(i).v6_hi()), (uchar*)fv.data()); qToBigEndian(quint64(data.sources(i).v6_lo()), (uchar*)fv.data()+8); list << QHostAddress((quint8*)fv.constData()).toString(); } return list.join(", "); } default: break; } break; } case kGroupRecords: { switch(attrib) { case FieldValue: { QVariantList grpRecords = GmpProtocol::fieldData( index, attrib, streamIndex).toList(); QByteArray ip; ip.resize(16); for (int i = 0; i < data.group_records_size(); i++) { QVariantMap grpRec = grpRecords.at(i).toMap(); OstProto::Gmp::GroupRecord rec = data.group_records(i); qToBigEndian(quint64(rec.group_address().v6_hi()), (uchar*)(ip.data())); qToBigEndian(quint64(rec.group_address().v6_lo()), (uchar*)(ip.data() + 8)); grpRec["groupRecordAddress"] = QHostAddress( (quint8*)ip.constData()).toString(); QStringList sl; for (int j = 0; j < rec.sources_size(); j++) { qToBigEndian(quint64(rec.sources(j).v6_hi()), (uchar*)(ip.data())); qToBigEndian(quint64(rec.sources(j).v6_lo()), (uchar*)(ip.data() + 8)); sl.append(QHostAddress( (quint8*)ip.constData()).toString()); } grpRec["groupRecordSourceList"] = sl; grpRecords.replace(i, grpRec); } return grpRecords; } case FieldFrameValue: { QVariantList list = GmpProtocol::fieldData( index, attrib, streamIndex).toList(); QByteArray fv; QByteArray ip; ip.resize(16); for (int i = 0; i < data.group_records_size(); i++) { OstProto::Gmp::GroupRecord rec = data.group_records(i); QByteArray rv = list.at(i).toByteArray(); rv.insert(4, QByteArray(16+16*rec.sources_size(), char(0))); qToBigEndian(quint64(rec.group_address().v6_hi()), (uchar*)(rv.data()+4)); qToBigEndian(quint64(rec.group_address().v6_lo()), (uchar*)(rv.data()+4+8)); for (int j = 0; j < rec.sources_size(); j++) { qToBigEndian(quint64(rec.sources(j).v6_hi()), (uchar*)(rv.data()+20+16*j)); qToBigEndian(quint64(rec.sources(j).v6_lo()), (uchar*)(rv.data()+20+16*j+8)); } fv.append(rv); } return fv; } case FieldTextValue: { QStringList list = GmpProtocol::fieldData( index, attrib, streamIndex).toStringList(); QByteArray ip; ip.resize(16); for (int i = 0; i < data.group_records_size(); i++) { OstProto::Gmp::GroupRecord rec = data.group_records(i); QString recStr = list.at(i); QString str; qToBigEndian(quint64(rec.group_address().v6_hi()), (uchar*)(ip.data())); qToBigEndian(quint64(rec.group_address().v6_lo()), (uchar*)(ip.data() + 8)); str.append(QString("Group: %1").arg( QHostAddress((quint8*)ip.constData()).toString())); str.append("; Sources: "); QStringList sl; for (int j = 0; j < rec.sources_size(); j++) { qToBigEndian(quint64(rec.sources(j).v6_hi()), (uchar*)(ip.data())); qToBigEndian(quint64(rec.sources(j).v6_lo()), (uchar*)(ip.data() + 8)); sl.append(QHostAddress( (quint8*)ip.constData()).toString()); } str.append(sl.join(", ")); recStr.replace("XXX", str); list.replace(i, recStr); } return list.join("\n").insert(0, "\n"); } default: break; } break; } default: break; } return GmpProtocol::fieldData(index, attrib, streamIndex); } bool MldProtocol::setFieldData(int index, const QVariant &value, FieldAttrib attrib) { bool isOk = false; if (attrib != FieldValue) goto _exit; switch (index) { case kGroupAddress: { if (value.typeName() == QString("UInt128")) { UInt128 addr = value.value(); data.mutable_group_address()->set_v6_hi(addr.hi64()); data.mutable_group_address()->set_v6_lo(addr.lo64()); isOk = true; break; } Q_IPV6ADDR addr = QHostAddress(value.toString()).toIPv6Address(); quint64 x; x = (quint64(addr[0]) << 56) | (quint64(addr[1]) << 48) | (quint64(addr[2]) << 40) | (quint64(addr[3]) << 32) | (quint64(addr[4]) << 24) | (quint64(addr[5]) << 16) | (quint64(addr[6]) << 8) | (quint64(addr[7]) << 0); data.mutable_group_address()->set_v6_hi(x); x = (quint64(addr[ 8]) << 56) | (quint64(addr[ 9]) << 48) | (quint64(addr[10]) << 40) | (quint64(addr[11]) << 32) | (quint64(addr[12]) << 24) | (quint64(addr[13]) << 16) | (quint64(addr[14]) << 8) | (quint64(addr[15]) << 0); data.mutable_group_address()->set_v6_lo(x); isOk = true; break; } case kSources: { QStringList list = value.toStringList(); data.clear_sources(); foreach(QString str, list) { OstProto::Gmp::IpAddress *src = data.add_sources(); Q_IPV6ADDR addr = QHostAddress(str).toIPv6Address(); quint64 x; x = (quint64(addr[0]) << 56) | (quint64(addr[1]) << 48) | (quint64(addr[2]) << 40) | (quint64(addr[3]) << 32) | (quint64(addr[4]) << 24) | (quint64(addr[5]) << 16) | (quint64(addr[6]) << 8) | (quint64(addr[7]) << 0); src->set_v6_hi(x); x = (quint64(addr[ 8]) << 56) | (quint64(addr[ 9]) << 48) | (quint64(addr[10]) << 40) | (quint64(addr[11]) << 32) | (quint64(addr[12]) << 24) | (quint64(addr[13]) << 16) | (quint64(addr[14]) << 8) | (quint64(addr[15]) << 0); src->set_v6_lo(x); } isOk = true; break; } case kGroupRecords: { GmpProtocol::setFieldData(index, value, attrib); QVariantList list = value.toList(); for (int i = 0; i < list.count(); i++) { QVariantMap grpRec = list.at(i).toMap(); OstProto::Gmp::GroupRecord *rec = data.mutable_group_records(i); Q_IPV6ADDR addr = QHostAddress( grpRec["groupRecordAddress"].toString()) .toIPv6Address(); quint64 x; x = (quint64(addr[0]) << 56) | (quint64(addr[1]) << 48) | (quint64(addr[2]) << 40) | (quint64(addr[3]) << 32) | (quint64(addr[4]) << 24) | (quint64(addr[5]) << 16) | (quint64(addr[6]) << 8) | (quint64(addr[7]) << 0); rec->mutable_group_address()->set_v6_hi(x); x = (quint64(addr[ 8]) << 56) | (quint64(addr[ 9]) << 48) | (quint64(addr[10]) << 40) | (quint64(addr[11]) << 32) | (quint64(addr[12]) << 24) | (quint64(addr[13]) << 16) | (quint64(addr[14]) << 8) | (quint64(addr[15]) << 0); rec->mutable_group_address()->set_v6_lo(x); QStringList srcList = grpRec["groupRecordSourceList"] .toStringList(); rec->clear_sources(); foreach (QString str, srcList) { OstProto::Gmp::IpAddress *src = rec->add_sources(); Q_IPV6ADDR addr = QHostAddress(str).toIPv6Address(); quint64 x; x = (quint64(addr[0]) << 56) | (quint64(addr[1]) << 48) | (quint64(addr[2]) << 40) | (quint64(addr[3]) << 32) | (quint64(addr[4]) << 24) | (quint64(addr[5]) << 16) | (quint64(addr[6]) << 8) | (quint64(addr[7]) << 0); src->set_v6_hi(x); x = (quint64(addr[ 8]) << 56) | (quint64(addr[ 9]) << 48) | (quint64(addr[10]) << 40) | (quint64(addr[11]) << 32) | (quint64(addr[12]) << 24) | (quint64(addr[13]) << 16) | (quint64(addr[14]) << 8) | (quint64(addr[15]) << 0); src->set_v6_lo(x); } } isOk = true; break; } default: isOk = GmpProtocol::setFieldData(index, value, attrib); break; } _exit: return isOk; } quint16 MldProtocol::checksum(int streamIndex) const { return AbstractProtocol::protocolFrameCksum(streamIndex, CksumTcpUdp); } ostinato-1.3.0/common/mld.h000066400000000000000000000050631451413623100155740ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _MLD_H #define _MLD_H #include "gmp.h" #include "mld.pb.h" // MLD uses the same msg type value for 'Query' messages across // versions despite the fields being different. To distinguish // Query messages of different versions, we use an additional // upper byte enum MldMsgType { kMldV1Query = 0x82, kMldV1Report = 0x83, kMldV1Done = 0x84, kMldV2Query = 0xFF82, kMldV2Report = 0x8F }; class MldProtocol : public GmpProtocol { public: MldProtocol(StreamBase *stream, AbstractProtocol *parent = 0); virtual ~MldProtocol(); static AbstractProtocol* createInstance(StreamBase *stream, AbstractProtocol *parent = 0); virtual quint32 protocolNumber() const; virtual void protoDataCopyInto(OstProto::Protocol &protocol) const; virtual void protoDataCopyFrom(const OstProto::Protocol &protocol); virtual quint32 protocolId(ProtocolIdType type) const; virtual QString name() const; virtual QString shortName() const; virtual AbstractProtocol::FieldFlags fieldFlags(int index) const; virtual QVariant fieldData(int index, FieldAttrib attrib, int streamIndex = 0) const; virtual bool setFieldData(int index, const QVariant &value, FieldAttrib attrib = FieldValue); protected: virtual bool isSsmReport() const; virtual bool isQuery() const; virtual bool isSsmQuery() const; virtual quint16 checksum(int streamIndex) const; private: int mrc(int value) const; }; inline bool MldProtocol::isSsmReport() const { return (msgType() == kMldV2Report); } inline bool MldProtocol::isQuery() const { return ((msgType() == kMldV1Query) || (msgType() == kMldV2Query)); } inline bool MldProtocol::isSsmQuery() const { return (msgType() == kMldV2Query); } inline int MldProtocol::mrc(int value) const { return quint16(value); // TODO: if value > 128, convert to mantissa/exp form } #endif ostinato-1.3.0/common/mld.proto000077500000000000000000000014231451413623100165070ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ import "protocol.proto"; import "gmp.proto"; package OstProto; extend Protocol { optional Gmp mld = 404; } ostinato-1.3.0/common/mldconfig.cpp000066400000000000000000000055011451413623100173120ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #include "mldconfig.h" #include "mld.h" #include "ipv6addressdelegate.h" #include "ipv6addressvalidator.h" MldConfigForm::MldConfigForm(QWidget *parent) : GmpConfigForm(parent) { connect(msgTypeCombo, SIGNAL(currentIndexChanged(int)), SLOT(on_msgTypeCombo_currentIndexChanged(int))); msgTypeCombo->setValueMask(0xFF); msgTypeCombo->addItem(kMldV1Query, "MLDv1 Query"); msgTypeCombo->addItem(kMldV1Report, "MLDv1 Report"); msgTypeCombo->addItem(kMldV1Done, "MLDv1 Done"); msgTypeCombo->addItem(kMldV2Query, "MLDv2 Query"); msgTypeCombo->addItem(kMldV2Report, "MLDv2 Report"); _defaultGroupIp = "::"; _defaultSourceIp = "::"; groupAddress->setValidator(new IPv6AddressValidator(this)); groupRecordAddress->setValidator(new IPv6AddressValidator(this)); sourceList->setItemDelegate(new IPv6AddressDelegate(this)); groupRecordSourceList->setItemDelegate(new IPv6AddressDelegate(this)); } MldConfigForm::~MldConfigForm() { } MldConfigForm* MldConfigForm::createInstance() { return new MldConfigForm; } void MldConfigForm::loadWidget(AbstractProtocol *proto) { GmpConfigForm::loadWidget(proto); maxResponseTime->setText( proto->fieldData( MldProtocol::kMldMrt, AbstractProtocol::FieldValue ).toString()); } void MldConfigForm::storeWidget(AbstractProtocol *proto) { GmpConfigForm::storeWidget(proto); proto->setFieldData( MldProtocol::kMldMrt, maxResponseTime->text()); } // // -- private slots // void MldConfigForm::on_msgTypeCombo_currentIndexChanged(int /*index*/) { switch(msgTypeCombo->currentValue()) { case kMldV1Query: case kMldV1Report: case kMldV1Done: asmGroup->show(); ssmWidget->hide(); break; case kMldV2Query: asmGroup->show(); ssmWidget->setCurrentIndex(kSsmQueryPage); ssmWidget->show(); break; case kMldV2Report: asmGroup->hide(); ssmWidget->setCurrentIndex(kSsmReportPage); ssmWidget->show(); break; default: asmGroup->hide(); ssmWidget->hide(); break; } } ostinato-1.3.0/common/mldconfig.h000066400000000000000000000021371451413623100167610ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _MLD_CONFIG_H #define _MLD_CONFIG_H #include "gmpconfig.h" class MldConfigForm : public GmpConfigForm { Q_OBJECT public: MldConfigForm(QWidget *parent = 0); virtual ~MldConfigForm(); static MldConfigForm* createInstance(); virtual void loadWidget(AbstractProtocol *proto); virtual void storeWidget(AbstractProtocol *proto); private slots: void on_msgTypeCombo_currentIndexChanged(int index); }; #endif ostinato-1.3.0/common/mldpdml.cpp000066400000000000000000000117231451413623100170040ustar00rootroot00000000000000/* Copyright (C) 2011 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #include "mldpdml.h" #include "mld.pb.h" PdmlMldProtocol::PdmlMldProtocol() { ostProtoId_ = OstProto::Protocol::kMldFieldNumber; fieldMap_.insert("icmpv6.code", OstProto::Gmp::kRsvdCodeFieldNumber); fieldMap_.insert("icmpv6.checksum", OstProto::Gmp::kChecksumFieldNumber); fieldMap_.insert("icmpv6.mld.maximum_response_delay", OstProto::Gmp::kMaxResponseTimeFieldNumber); // FIXME fieldMap_.insert("icmpv6.mld.flag.s", OstProto::Gmp::kSFlagFieldNumber); fieldMap_.insert("icmpv6.mld.flag.qrv", OstProto::Gmp::kQrvFieldNumber); fieldMap_.insert("icmpv6.mld.qqi", OstProto::Gmp::kQqiFieldNumber); // FIXME fieldMap_.insert("icmpv6.mld.nb_sources", OstProto::Gmp::kSourceCountFieldNumber); fieldMap_.insert("icmpv6.mldr.nb_mcast_records", OstProto::Gmp::kGroupRecordCountFieldNumber); } PdmlProtocol* PdmlMldProtocol::createInstance() { return new PdmlMldProtocol(); } void PdmlMldProtocol::preProtocolHandler(QString /*name*/, const QXmlStreamAttributes &attributes, int /*expectedPos*/, OstProto::Protocol *pbProto, OstProto::Stream* /*stream*/) { bool isOk; OstProto::Gmp *mld = pbProto->MutableExtension(OstProto::mld); mld->set_is_override_rsvd_code(true); mld->set_is_override_checksum(overrideCksum_); mld->set_is_override_source_count(true); mld->set_is_override_group_record_count(true); protoSize_ = attributes.value("size").toString().toUInt(&isOk); } void PdmlMldProtocol::unknownFieldHandler(QString name, int /*pos*/, int /*size*/, const QXmlStreamAttributes &attributes, OstProto::Protocol *pbProto, OstProto::Stream* /*stream*/) { bool isOk; OstProto::Gmp *mld = pbProto->MutableExtension(OstProto::mld); QString valueHexStr = attributes.value("value").toString(); if (name == "icmpv6.type") { uint type = valueHexStr.toUInt(&isOk, kBaseHex); if ((type == kMldQuery) && (protoSize_ >= 28)) type = kMldV2Query; mld->set_type(type); } else if (name == "icmpv6.mld.multicast_address") { mld->mutable_group_address()->set_v6_hi( valueHexStr.left(16).toULongLong(&isOk, kBaseHex)); mld->mutable_group_address()->set_v6_lo( valueHexStr.right(16).toULongLong(&isOk, kBaseHex)); } else if (name == "icmpv6.mld.source_address") { OstProto::Gmp::IpAddress *ip = mld->add_sources(); ip->set_v6_hi(valueHexStr.left(16).toULongLong(&isOk, kBaseHex)); ip->set_v6_lo(valueHexStr.right(16).toULongLong(&isOk, kBaseHex)); } else if (name == "icmpv6.mldr.mar.record_type") { OstProto::Gmp::GroupRecord *rec = mld->add_group_records(); rec->set_type(OstProto::Gmp::GroupRecord::RecordType( valueHexStr.toUInt(&isOk, kBaseHex))); rec->set_is_override_source_count(true); rec->set_is_override_aux_data_length(true); } else if (name == "icmpv6.mldr.mar.aux_data_len") { mld->mutable_group_records(mld->group_records_size() - 1)-> set_aux_data_length(valueHexStr.toUInt(&isOk, kBaseHex)); } else if (name == "icmpv6.mldr.mar.nb_sources") { mld->mutable_group_records(mld->group_records_size() - 1)-> set_source_count(valueHexStr.toUInt(&isOk, kBaseHex)); } else if (name == "icmpv6.mldr.mar.multicast_address") { OstProto::Gmp::IpAddress *ip = mld->mutable_group_records( mld->group_records_size() - 1)->mutable_group_address(); ip->set_v6_hi(valueHexStr.left(16).toULongLong(&isOk, kBaseHex)); ip->set_v6_lo(valueHexStr.right(16).toULongLong(&isOk, kBaseHex)); } else if (name == "icmpv6.mldr.mar.source_address") { OstProto::Gmp::IpAddress *ip = mld->mutable_group_records( mld->group_records_size() - 1)->add_sources(); ip->set_v6_hi(valueHexStr.left(16).toULongLong(&isOk, kBaseHex)); ip->set_v6_lo(valueHexStr.right(16).toULongLong(&isOk, kBaseHex)); } else if (name == "icmpv6.mldr.mar.auxiliary_data") { QByteArray ba = QByteArray::fromHex( attributes.value("value").toString().toUtf8()); mld->mutable_group_records(mld->group_records_size() - 1)-> set_aux_data(ba.constData(), ba.size()); } } ostinato-1.3.0/common/mldpdml.h000066400000000000000000000026631451413623100164540ustar00rootroot00000000000000/* Copyright (C) 2011 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _MLD_PDML_H #define _MLD_PDML_H #include "pdmlprotocol.h" class PdmlMldProtocol : public PdmlProtocol { friend class PdmlIcmp6Protocol; public: static PdmlProtocol* createInstance(); virtual void preProtocolHandler(QString name, const QXmlStreamAttributes &attributes, int expectedPos, OstProto::Protocol *pbProto, OstProto::Stream *stream); virtual void unknownFieldHandler(QString name, int pos, int size, const QXmlStreamAttributes &attributes, OstProto::Protocol *pbProto, OstProto::Stream *stream); protected: PdmlMldProtocol(); private: static const uint kMldQuery = 0x82; static const uint kMldV1Query = 0x82; static const uint kMldV2Query = 0xFF82; uint protoSize_; }; #endif ostinato-1.3.0/common/nativefileformat.cpp000066400000000000000000000407411451413623100207140ustar00rootroot00000000000000/* Copyright (C) 2010, 2016 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #include "nativefileformat.h" #include "crc32c.h" #include #include #include #define tr(str) QObject::tr(str) const char* NativeFileFormat::kFileMagicValue = "\xa7\xb7OSTINATO"; static const int kBaseHex = 16; static QString fileTypeStr(OstProto::FileType fileType) { switch (fileType) { case OstProto::kReservedFileType: return QString("Reserved"); case OstProto::kStreamsFileType: return QString("Streams"); case OstProto::kSessionFileType: return QString("Streams"); default: Q_ASSERT(false); } return QString("Unknown"); } NativeFileFormat::NativeFileFormat() { /* * We don't have any "real" work to do here in the constructor. * What we do is run some "assert" tests so that these get caught * at init itself instead of while saving/restoring when a user * might lose some data! */ OstProto::FileMagic magic; OstProto::FileChecksum cksum; magic.set_value(kFileMagicValue); cksum.set_value(quint32(0)); // TODO: convert Q_ASSERT to something that will run in RELEASE mode also Q_ASSERT(magic.IsInitialized()); Q_ASSERT(cksum.IsInitialized()); Q_ASSERT(magic.ByteSize() == kFileMagicSize); Q_ASSERT(cksum.ByteSize() == kFileChecksumSize); } bool NativeFileFormat::open( const QString fileName, OstProto::FileType fileType, OstProto::FileMeta &meta, OstProto::FileContent &content, QString &error) { QFile file(fileName); QByteArray buf; int size, contentOffset, contentSize; quint32 calcCksum; OstProto::FileMagic magic; OstProto::FileChecksum cksum, zeroCksum; if (!file.open(QIODevice::ReadOnly)) goto _open_fail; if (file.size() < kFileMagicSize) goto _magic_missing; if (file.size() < kFileMinSize) goto _checksum_missing; buf.resize(file.size()); size = file.read(buf.data(), buf.size()); if (size < 0) goto _read_fail; Q_ASSERT(file.atEnd()); file.close(); qDebug("%s: file.size() = %lld", __FUNCTION__, file.size()); qDebug("%s: size = %d", __FUNCTION__, size); //qDebug("Read %d bytes", buf.size()); //qDebug("%s", qPrintable(QString(buf.toHex()))); // Parse and verify magic if (!magic.ParseFromArray( (void*)(buf.constData() + kFileMagicOffset), kFileMagicSize)) { goto _magic_parse_fail; } if (magic.value() != kFileMagicValue) goto _magic_match_fail; // Parse and verify checksum if (!cksum.ParseFromArray( (void*)(buf.constData() + size - kFileChecksumSize), kFileChecksumSize)) { goto _cksum_parse_fail; } zeroCksum.set_value(0); if (!zeroCksum.SerializeToArray( (void*) (buf.data() + size - kFileChecksumSize), kFileChecksumSize)) { goto _zero_cksum_serialize_fail; } calcCksum = checksumCrc32C((quint8*) buf.constData(), size); qDebug("checksum \nExpected:%x Actual:%x", calcCksum, cksum.value()); if (cksum.value() != calcCksum) goto _cksum_verify_fail; // Parse the metadata first before we parse the full contents if (!meta.ParseFromArray( (void*)(buf.constData() + kFileMetaDataOffset), fileMetaSize((quint8*)buf.constData(), size))) { goto _metadata_parse_fail; } qDebug("%s: File MetaData (INFORMATION) - \n%s", __FUNCTION__, meta.DebugString().c_str()); qDebug("%s: END MetaData", __FUNCTION__); // MetaData Validation(s) if (meta.data().file_type() != fileType) goto _unexpected_file_type; if (meta.data().format_version_major() != kFileFormatVersionMajor) goto _incompatible_file_version; if (meta.data().format_version_minor() > kFileFormatVersionMinor) goto _incompatible_file_version; if (meta.data().format_version_minor() < kFileFormatVersionMinor) { // TODO: need to modify 'buf' such that we can parse successfully // assuming the native minor version } if (meta.data().format_version_revision() > kFileFormatVersionRevision) { error = QString(tr("%1 was created using a newer version of Ostinato." " New features/protocols will not be available.")).arg(fileName); } Q_ASSERT(meta.data().format_version_major() == kFileFormatVersionMajor); // ByteSize() does not include the Tag/Key, so we add 2 for that contentOffset = kFileMetaDataOffset + meta.data().ByteSize() + 2; contentSize = size - contentOffset - kFileChecksumSize; qDebug("%s: content offset/size = %d/%d", __FUNCTION__, contentOffset, contentSize); // Parse full contents if (!content.ParseFromArray( (void*)(buf.constData() + contentOffset), contentSize)) { goto _content_parse_fail; } return true; _content_parse_fail: error = QString(tr("Failed parsing %1 contents")).arg(fileName); qDebug("Error: %s", content.InitializationErrorString().c_str()); qDebug("Debug: %s", content.DebugString().c_str()); goto _fail; _incompatible_file_version: error = QString(tr("%1 is in an incompatible format version - %2.%3.%4" " (Native version is %5.%6.%7)")) .arg(fileName) .arg(meta.data().format_version_major()) .arg(meta.data().format_version_minor()) .arg(meta.data().format_version_revision()) .arg(kFileFormatVersionMajor) .arg(kFileFormatVersionMinor) .arg(kFileFormatVersionRevision); goto _fail; _unexpected_file_type: error = QString(tr("%1 is not a %2 file")) .arg(fileName) .arg(fileTypeStr(fileType)); goto _fail; _metadata_parse_fail: error = QString(tr("Failed parsing %1 meta data")).arg(fileName); qDebug("Error: %s", meta.data().InitializationErrorString().c_str()); goto _fail; _cksum_verify_fail: error = QString(tr("%1 checksum validation failed!\nExpected:%2 Actual:%3")) .arg(fileName) .arg(calcCksum, 0, kBaseHex) .arg(cksum.value(), 0, kBaseHex); goto _fail; _zero_cksum_serialize_fail: error = QString(tr("Internal Error: Zero Checksum Serialize failed!\n" "Error: %1\nDebug: %2")) .arg(QString().fromStdString( cksum.InitializationErrorString())) .arg(QString().fromStdString(cksum.DebugString())); goto _fail; _cksum_parse_fail: error = QString(tr("Failed parsing %1 checksum")).arg(fileName); qDebug("Error: %s", cksum.InitializationErrorString().c_str()); goto _fail; _magic_match_fail: error = QString(tr("%1 is not an Ostinato file")).arg(fileName); goto _fail; _magic_parse_fail: error = QString(tr("%1 does not look like an Ostinato file")).arg(fileName); qDebug("Error: %s", magic.InitializationErrorString().c_str()); goto _fail; _read_fail: error = QString(tr("Error reading from %1")).arg(fileName); goto _fail; _checksum_missing: error = QString(tr("%1 is too small (missing checksum)")).arg(fileName); goto _fail; _magic_missing: error = QString(tr("%1 is too small (missing magic value)")) .arg(fileName); goto _fail; _open_fail: error = QString(tr("Error opening %1")).arg(fileName); goto _fail; _fail: qDebug("%s", qPrintable(error)); return false; } bool NativeFileFormat::save( OstProto::FileType fileType, const OstProto::FileContent &content, const QString fileName, QString &error) { OstProto::FileMagic magic; OstProto::FileMeta meta; OstProto::FileChecksum cksum; QFile file(fileName); int metaSize, contentSize; int contentOffset, cksumOffset; QByteArray buf; quint32 calcCksum; magic.set_value(kFileMagicValue); Q_ASSERT(magic.IsInitialized()); cksum.set_value(0); Q_ASSERT(cksum.IsInitialized()); initFileMetaData(*(meta.mutable_data())); meta.mutable_data()->set_file_type(fileType); Q_ASSERT(meta.IsInitialized()); if (!content.IsInitialized()) goto _content_not_init; Q_ASSERT(content.IsInitialized()); metaSize = meta.ByteSize(); contentSize = content.ByteSize(); contentOffset = kFileMetaDataOffset + metaSize; cksumOffset = contentOffset + contentSize; Q_ASSERT(magic.ByteSize() == kFileMagicSize); Q_ASSERT(cksum.ByteSize() == kFileChecksumSize); buf.resize(kFileMagicSize + metaSize + contentSize + kFileChecksumSize); // Serialize everything if (!magic.SerializeToArray((void*) (buf.data() + kFileMagicOffset), kFileMagicSize)) { goto _magic_serialize_fail; } if (!meta.SerializeToArray((void*) (buf.data() + kFileMetaDataOffset), metaSize)) { goto _meta_serialize_fail; } if (!content.SerializeToArray((void*) (buf.data() + contentOffset), contentSize)) { goto _content_serialize_fail; } if (!cksum.SerializeToArray((void*) (buf.data() + cksumOffset), kFileChecksumSize)) { goto _zero_cksum_serialize_fail; } // TODO: emit status("Calculating checksum..."); // Calculate and write checksum calcCksum = checksumCrc32C((quint8*)buf.constData(), buf.size()); cksum.set_value(calcCksum); if (!cksum.SerializeToArray( (void*) (buf.data() + cksumOffset), kFileChecksumSize)) { goto _cksum_serialize_fail; } qDebug("Writing %d bytes", buf.size()); //qDebug("%s", qPrintable(QString(buf.toHex()))); // TODO: emit status("Writing to disk..."); if (!file.open(QIODevice::WriteOnly | QIODevice::Truncate)) goto _open_fail; if (file.write(buf) < 0) goto _write_fail; file.close(); return true; _write_fail: error = QString(tr("Error writing to %1")).arg(fileName); goto _fail; _open_fail: error = QString(tr("Error opening %1 (Error Code = %2)")) .arg(fileName) .arg(file.error()); goto _fail; _cksum_serialize_fail: error = QString(tr("Internal Error: Checksum Serialize failed\n%1\n%2")) .arg(QString().fromStdString( cksum.InitializationErrorString())) .arg(QString().fromStdString(cksum.DebugString())); goto _fail; _zero_cksum_serialize_fail: error = QString(tr("Internal Eror: Zero Checksum Serialize failed\n%1\n%2")) .arg(QString().fromStdString( cksum.InitializationErrorString())) .arg(QString().fromStdString(cksum.DebugString())); goto _fail; _content_serialize_fail: error = QString(tr("Internal Error: Content Serialize failed\n%1\n%2")) .arg(QString().fromStdString( content.InitializationErrorString())) .arg(QString().fromStdString(content.DebugString())); goto _fail; _meta_serialize_fail: error = QString(tr("Internal Error: Meta Data Serialize failed\n%1\n%2")) .arg(QString().fromStdString( meta.InitializationErrorString())) .arg(QString().fromStdString(meta.DebugString())); goto _fail; _magic_serialize_fail: error = QString(tr("Internal Error: Magic Serialize failed\n%1\n%2")) .arg(QString().fromStdString( magic.InitializationErrorString())) .arg(QString().fromStdString(magic.DebugString())); goto _fail; _content_not_init: error = QString(tr("Internal Error: Content not initialized\n%1\n%2")) .arg(QString().fromStdString( content.InitializationErrorString())) .arg(QString().fromStdString(content.DebugString())); goto _fail; _fail: qDebug("%s", qPrintable(error)); return false; } bool NativeFileFormat::isNativeFileFormat( const QString fileName, OstProto::FileType fileType) { bool ret = false; QFile file(fileName); QByteArray buf; OstProto::FileMagic magic; if (!file.open(QIODevice::ReadOnly)) goto _exit; // Assume tag/length for MetaData will fit in 8 bytes buf = file.peek(kFileMagicOffset + kFileMagicSize + 8); if (!magic.ParseFromArray((void*)(buf.constData() + kFileMagicOffset), kFileMagicSize)) goto _close_exit; if (magic.value() == kFileMagicValue) { OstProto::FileMeta meta; int metaSize = fileMetaSize((quint8*)buf.constData(), buf.size()); buf = file.peek(kFileMagicOffset + kFileMagicSize + metaSize); if (!meta.ParseFromArray( (void*)(buf.constData() + kFileMetaDataOffset), metaSize)) { qDebug("%s: File MetaData\n%s", __FUNCTION__, meta.DebugString().c_str()); goto _close_exit; } if (meta.data().file_type() == fileType) ret = true; } _close_exit: file.close(); _exit: return ret; } void NativeFileFormat::initFileMetaData(OstProto::FileMetaData &metaData) { // Fill in the "native" file format version metaData.set_format_version_major(kFileFormatVersionMajor); metaData.set_format_version_minor(kFileFormatVersionMinor); metaData.set_format_version_revision(kFileFormatVersionRevision); metaData.set_generator_name( qApp->applicationName().toUtf8().constData()); metaData.set_generator_version( qApp->property("version").toString().toUtf8().constData()); metaData.set_generator_revision( qApp->property("revision").toString().toUtf8().constData()); } int NativeFileFormat::fileMetaSize(const quint8* file, int size) { int i = kFileMetaDataOffset; uint result, shift; const int kWireTypeLengthDelimited = 2; // An embedded Message field is encoded as // // See Protobuf Encoding for more details // Decode 'Key' varint result = 0; shift = 0; while (i < size) { quint8 byte = file[i++]; result |= (byte & 0x7f) << shift; if (!(byte & 0x80)) // MSB == 0? break; shift += 7; } if (i >= size) return 0; Q_ASSERT(result == ((OstProto::File::kMetaDataFieldNumber << 3) | kWireTypeLengthDelimited)); // Decode 'Length' varint result = 0; shift = 0; while (i < size) { quint8 byte = file[i++]; result |= (byte & 0x7f) << shift; if (!(byte & 0x80)) // MSB == 0? break; shift += 7; } if (i >= size) return 0; return int(result+(i-kFileMetaDataOffset)); } #pragma GCC diagnostic ignored "-Wdeprecated-declarations" /*! Fixup content to what is expected in the native version */ void NativeFileFormat::postParseFixup(OstProto::FileMetaData metaData, OstProto::FileContent &content) { Q_ASSERT(metaData.format_version_major() == kFileFormatVersionMajor); // Do fixups from oldest to newest versions switch (metaData.format_version_minor()) { case 1: { int n = content.matter().streams().stream_size(); for (int i = 0; i < n; i++) { OstProto::StreamControl *sctl = content.mutable_matter()->mutable_streams()->mutable_stream(i)->mutable_control(); sctl->set_packets_per_sec(sctl->obsolete_packets_per_sec()); sctl->set_bursts_per_sec(sctl->obsolete_bursts_per_sec()); } // fall-through to next higher version until native version } case kFileFormatVersionMinor: // native version break; case 0: default: qWarning("%s: minor version %u unhandled", __FUNCTION__, metaData.format_version_minor()); Q_ASSERT_X(false, "postParseFixup", "unhandled minor version"); } } #pragma GCC diagnostic warning "-Wdeprecated-declarations" ostinato-1.3.0/common/nativefileformat.h000066400000000000000000000046221451413623100203570ustar00rootroot00000000000000/* Copyright (C) 2010, 2016 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _NATIVE_FILE_FORMAT_H #define _NATIVE_FILE_FORMAT_H /* * This file contains helper functions for the native file format * defined in fileformat.proto * * The actual file format classes - (Ostm)FileFormat and OssnFileFormat * use multiple inheritance from the abstract interface class and this * helper class * * The primary reason for the existence of this class is to have a common * code for dealing with native file formats */ #include "fileformat.pb.h" #include class NativeFileFormat { public: NativeFileFormat(); bool open(const QString fileName, OstProto::FileType fileType, OstProto::FileMeta &meta, OstProto::FileContent &content, QString &error); bool save(OstProto::FileType fileType, const OstProto::FileContent &content, const QString fileName, QString &error); bool isNativeFileFormat(const QString fileName, OstProto::FileType fileType); void postParseFixup(OstProto::FileMetaData metaData, OstProto::FileContent &content); private: void initFileMetaData(OstProto::FileMetaData &metaData); int fileMetaSize(const quint8* file, int size); static const int kFileMagicSize = 12; static const int kFileChecksumSize = 5; static const int kFileMinSize = kFileMagicSize + kFileChecksumSize; static const int kFileMagicOffset = 0; static const int kFileMetaDataOffset = kFileMagicSize; static const char* kFileMagicValue; // Native file format version static const uint kFileFormatVersionMajor = 0; static const uint kFileFormatVersionMinor = 2; static const uint kFileFormatVersionRevision = 4; }; #endif ostinato-1.3.0/common/netdefs.h000066400000000000000000000016761451413623100164560ustar00rootroot00000000000000/* Copyright (C) 2018 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _NET_DEFS_H static const quint64 kBcastMac = 0xffffffffffffULL; static const quint16 kEthTypeIp4 = 0x0800; static const quint16 kEthTypeArp = 0x0806; static const quint16 kEthTypeIp6 = 0x86dd; static const int kIp6HdrLen = 40; static const quint8 kIpProtoIcmp6 = 58; #endif ostinato-1.3.0/common/ossnfileformat.cpp000066400000000000000000000050611451413623100204040ustar00rootroot00000000000000/* Copyright (C) 2016 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #include "ossnfileformat.h" OssnFileFormat ossnFileFormat; OssnFileFormat::OssnFileFormat() : SessionFileFormat(), NativeFileFormat() { // Do Nothing } bool OssnFileFormat::open(const QString fileName, OstProto::SessionContent &session, QString &error) { OstProto::FileMeta meta; OstProto::FileContent content; bool ret = NativeFileFormat::open(fileName, OstProto::kSessionFileType, meta, content, error); if (!ret) goto _exit; if (!content.matter().has_session()) goto _missing_session; postParseFixup(meta.data(), content); session.CopyFrom(content.matter().session()); return true; _missing_session: error = QString(tr("%1 does not contain a session")).arg(fileName); goto _fail; _fail: qDebug("%s", qPrintable(error)); _exit: return false; } bool OssnFileFormat::save(const OstProto::SessionContent &session, const QString fileName, QString &error) { OstProto::FileContent content; if (!session.IsInitialized()) goto _session_not_init; content.mutable_matter()->mutable_session()->CopyFrom(session); Q_ASSERT(content.IsInitialized()); return NativeFileFormat::save(OstProto::kSessionFileType, content, fileName, error); _session_not_init: error = QString(tr("Internal Error: Session not initialized\n%1\n%2")) .arg(QString().fromStdString( session.InitializationErrorString())) .arg(QString().fromStdString(session.DebugString())); goto _fail; _fail: qDebug("%s", qPrintable(error)); return false; } bool OssnFileFormat::isMyFileFormat(const QString fileName) { return isNativeFileFormat(fileName, OstProto::kSessionFileType); } bool OssnFileFormat::isMyFileType(const QString fileType) { return fileType.contains("(*.ossn)") ? true : false; } ostinato-1.3.0/common/ossnfileformat.h000066400000000000000000000024111451413623100200450ustar00rootroot00000000000000/* Copyright (C) 2016 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _OSSN_FILE_FORMAT_H #define _OSSN_FILE_FORMAT_H #include "nativefileformat.h" #include "sessionfileformat.h" class OssnFileFormat : public SessionFileFormat, public NativeFileFormat { public: OssnFileFormat(); virtual bool open(const QString fileName, OstProto::SessionContent &session, QString &error); virtual bool save(const OstProto::SessionContent &session, const QString fileName, QString &error); virtual bool isMyFileFormat(const QString fileName); virtual bool isMyFileType(const QString fileType); }; extern OssnFileFormat ossnFileFormat; #endif ostinato-1.3.0/common/ostmfileformat.cpp000066400000000000000000000051221451413623100204020ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #include "ostmfileformat.h" OstmFileFormat fileFormat; OstmFileFormat::OstmFileFormat() : StreamFileFormat(), NativeFileFormat() { // Do Nothing! } bool OstmFileFormat::open(const QString fileName, OstProto::StreamConfigList &streams, QString &error) { OstProto::FileMeta meta; OstProto::FileContent content; bool ret = NativeFileFormat::open(fileName, OstProto::kStreamsFileType, meta, content, error); if (!ret) goto _fail; if (!content.matter().has_streams()) goto _missing_streams; postParseFixup(meta.data(), content); streams.CopyFrom(content.matter().streams()); return true; _missing_streams: error = QString(tr("%1 does not contain any streams")).arg(fileName); goto _fail; _fail: qDebug("%s", qPrintable(error)); return false; } bool OstmFileFormat::save(const OstProto::StreamConfigList streams, const QString fileName, QString &error) { OstProto::FileContent content; if (!streams.IsInitialized()) goto _stream_not_init; content.mutable_matter()->mutable_streams()->CopyFrom(streams); Q_ASSERT(content.IsInitialized()); return NativeFileFormat::save(OstProto::kStreamsFileType, content, fileName, error); _stream_not_init: error = QString(tr("Internal Error: Streams not initialized\n%1\n%2")) .arg(QString().fromStdString( streams.InitializationErrorString())) .arg(QString().fromStdString(streams.DebugString())); goto _fail; _fail: qDebug("%s", qPrintable(error)); return false; } bool OstmFileFormat::isMyFileFormat(const QString fileName) { return isNativeFileFormat(fileName, OstProto::kStreamsFileType); } bool OstmFileFormat::isMyFileType(const QString fileType) { if (fileType.startsWith("Ostinato")) return true; else return false; } ostinato-1.3.0/common/ostmfileformat.h000066400000000000000000000024201451413623100200450ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _OSTM_FILE_FORMAT_H #define _OSTM_FILE_FORMAT_H #include "nativefileformat.h" #include "streamfileformat.h" #include "fileformat.pb.h" class OstmFileFormat : public StreamFileFormat, public NativeFileFormat { public: OstmFileFormat(); virtual bool open(const QString fileName, OstProto::StreamConfigList &streams, QString &error); virtual bool save(const OstProto::StreamConfigList streams, const QString fileName, QString &error); bool isMyFileFormat(const QString fileName); bool isMyFileType(const QString fileType); }; extern OstmFileFormat fileFormat; #endif ostinato-1.3.0/common/ostproto.pro000066400000000000000000000040451451413623100172610ustar00rootroot00000000000000TEMPLATE = lib CONFIG += qt staticlib QT -= gui QT += network script xml LIBS += \ -lprotobuf PROTOS = \ protocol.proto \ emulproto.proto PROTOS += \ mac.proto \ payload.proto \ eth2.proto \ dot3.proto \ llc.proto \ snap.proto \ dot2llc.proto \ dot2snap.proto \ vlan.proto \ svlan.proto \ vlanstack.proto \ stp.proto \ arp.proto \ ip4.proto \ ip6.proto \ ip6over4.proto \ ip4over6.proto \ ip4over4.proto \ ip6over6.proto \ gre.proto \ icmp.proto \ gmp.proto \ igmp.proto \ mld.proto \ tcp.proto \ udp.proto \ textproto.proto \ userscript.proto \ hexdump.proto \ sample.proto \ sign.proto HEADERS = \ abstractprotocol.h \ comboprotocol.h \ protocolmanager.h \ protocollist.h \ protocollistiterator.h \ streambase.h \ updater.h \ HEADERS += \ mac.h \ vlan.h \ svlan.h \ vlanstack.h \ eth2.h \ dot3.h \ llc.h \ dot2llc.h \ snap.h \ dot2snap.h \ arp.h \ ip4.h \ ip6.h \ ip4over4.h \ ip4over6.h \ ip6over4.h \ ip6over6.h \ gmp.h \ gre.h \ icmp.h \ igmp.h \ mld.h \ tcp.h \ udp.h \ textproto.h \ hexdump.h \ payload.h \ sample.h \ sign.h \ userscript.h SOURCES = \ abstractprotocol.cpp \ crc32c.cpp \ protocolmanager.cpp \ protocollist.cpp \ protocollistiterator.cpp \ streambase.cpp \ updater.cpp \ SOURCES += \ mac.cpp \ vlan.cpp \ svlan.cpp \ eth2.cpp \ dot3.cpp \ llc.cpp \ snap.cpp \ stp.cpp \ arp.cpp \ ip4.cpp \ ip6.cpp \ gmp.cpp \ gre.cpp \ icmp.cpp \ igmp.cpp \ mld.cpp \ tcp.cpp \ udp.cpp \ textproto.cpp \ hexdump.cpp \ packet.cpp \ payload.cpp \ sample.cpp \ sign.cpp \ userscript.cpp QMAKE_DISTCLEAN += object_script.* #binding.depends = compiler_protobuf_py_make_all #QMAKE_EXTRA_TARGETS += binding include(../protobuf.pri) include(../options.pri) ostinato-1.3.0/common/ostprotogui.pro000066400000000000000000000056451451413623100177750ustar00rootroot00000000000000TEMPLATE = lib CONFIG += qt staticlib QT += widgets network xml script INCLUDEPATH += "../extra/qhexedit2/src" LIBS += \ -lprotobuf FORMS = \ pcapfileimport.ui \ FORMS += \ mac.ui \ vlan.ui \ eth2.ui \ dot3.ui \ llc.ui \ snap.ui \ stp.ui \ arp.ui \ ip4.ui \ ip6.ui \ gmp.ui \ gre.ui \ icmp.ui \ tcp.ui \ udp.ui \ textproto.ui \ tosdscp.ui \ hexdump.ui \ payload.ui \ sample.ui \ sign.ui \ userscript.ui PROTOS = \ fileformat.proto # TODO: Move fileformat related stuff into a different library - why? HEADERS = \ ostprotolib.h \ ipv4addressdelegate.h \ ipv6addressdelegate.h \ nativefileformat.h \ ossnfileformat.h \ ostmfileformat.h \ pcapfileformat.h \ pdmlfileformat.h \ pythonfileformat.h \ pdmlprotocol.h \ pdmlprotocols.h \ pdmlreader.h \ sessionfileformat.h \ streamfileformat.h \ spinboxdelegate.h HEADERS += \ tosdscp.h HEADERS += \ abstractprotocolconfig.h \ comboprotocolconfig.h \ protocolwidgetfactory.h \ macconfig.h \ vlanconfig.h \ svlanconfig.h \ vlanstackconfig.h \ eth2config.h \ dot3config.h \ llcconfig.h \ dot2llcconfig.h \ snapconfig.h \ dot2snapconfig.h \ stpconfig.h \ arpconfig.h \ ip4config.h \ ip6config.h \ ip4over4config.h \ gmpconfig.h \ greconfig.h \ icmpconfig.h \ igmpconfig.h \ mldconfig.h \ tcpconfig.h \ udpconfig.h \ textprotoconfig.h \ hexdumpconfig.h \ payloadconfig.h \ sampleconfig.h \ signconfig.h \ userscriptconfig.h SOURCES += \ ostprotolib.cpp \ nativefileformat.cpp \ ossnfileformat.cpp \ ostmfileformat.cpp \ pcapfileformat.cpp \ pdmlfileformat.cpp \ pythonfileformat.cpp \ pdmlprotocol.cpp \ pdmlprotocols.cpp \ pdmlreader.cpp \ sessionfileformat.cpp \ streamfileformat.cpp \ spinboxdelegate.cpp SOURCES += \ tosdscp.cpp SOURCES += \ protocolwidgetfactory.cpp \ macconfig.cpp \ vlanconfig.cpp \ eth2config.cpp \ dot3config.cpp \ llcconfig.cpp \ snapconfig.cpp \ stpconfig.cpp \ arpconfig.cpp \ ip4config.cpp \ ip6config.cpp \ gmpconfig.cpp \ greconfig.cpp \ icmpconfig.cpp \ igmpconfig.cpp \ mldconfig.cpp \ tcpconfig.cpp \ udpconfig.cpp \ textprotoconfig.cpp \ hexdumpconfig.cpp \ payloadconfig.cpp \ sampleconfig.cpp \ signconfig.cpp \ userscriptconfig.cpp SOURCES += \ vlanpdml.cpp \ svlanpdml.cpp \ stppdml.cpp \ eth2pdml.cpp \ llcpdml.cpp \ arppdml.cpp \ ip4pdml.cpp \ ip6pdml.cpp \ grepdml.cpp \ icmppdml.cpp \ icmp6pdml.cpp \ igmppdml.cpp \ mldpdml.cpp \ tcppdml.cpp \ udppdml.cpp \ textprotopdml.cpp \ samplepdml.cpp QMAKE_DISTCLEAN += object_script.* include(../protobuf.pri) include(../options.pri) ostinato-1.3.0/common/ostprotolib.cpp000066400000000000000000000025101451413623100177250ustar00rootroot00000000000000/* Copyright (C) 2011 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #include "ostprotolib.h" QString OstProtoLib::tsharkPath_; QString OstProtoLib::gzipPath_; QString OstProtoLib::diffPath_; QString OstProtoLib::awkPath_; // TODO: one set method for each external app void OstProtoLib::setExternalApplicationPaths(QString tsharkPath, QString gzipPath, QString diffPath, QString awkPath) { tsharkPath_ = tsharkPath; gzipPath_ = gzipPath; diffPath_ = diffPath; awkPath_ = awkPath; } QString OstProtoLib::tsharkPath() { return tsharkPath_; } QString OstProtoLib::gzipPath() { return gzipPath_; } QString OstProtoLib::diffPath() { return diffPath_; } QString OstProtoLib::awkPath() { return awkPath_; } ostinato-1.3.0/common/ostprotolib.h000066400000000000000000000022221451413623100173720ustar00rootroot00000000000000/* Copyright (C) 2011 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _OST_PROTO_LIB_H #define _OST_PROTO_LIB_H #include class OstProtoLib { public: static void setExternalApplicationPaths(QString tsharkPath, QString gzipPath, QString diffPath, QString awkPath); static QString tsharkPath(); static QString gzipPath(); static QString diffPath(); static QString awkPath(); private: static QString tsharkPath_; static QString gzipPath_; static QString diffPath_; static QString awkPath_; }; #endif ostinato-1.3.0/common/packet.cpp000066400000000000000000000067441451413623100166310ustar00rootroot00000000000000/* Copyright (C) 2023 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #include "packet.h" using namespace Packet; quint16 Packet::l4ChecksumOffset(const uchar *pktData, int pktLen) { Parser parser(pktData, pktLen); quint16 offset = kEthTypeOffset; // Skip VLANs, if any quint16 ethType = parser.field16(offset); if (!parser.ok()) return 0; // TODO: support 802.3 frames if (ethType <= 1500) return 0; while (kVlanEthTypes.contains(ethType)) { offset += kVlanTagSize; ethType = parser.field16(offset); if (!parser.ok()) return 0; } offset += kEthTypeSize; // XXX: offset now points to Eth payload // Skip MPLS tags, if any if (ethType == kMplsEthType) { while (1) { quint32 mplsTag = parser.field32(offset); if (!parser.ok()) return 0; offset += kMplsTagSize; if (mplsTag & 0x100) { // BOS bit quint32 nextWord = parser.field32(offset); if (!parser.ok()) return 0; if (nextWord == 0) { // PW Control Word offset += kMplsTagSize; ethType = 0; break; } quint8 firstPayloadNibble = nextWord >> 28; if (firstPayloadNibble == 0x4) ethType = kIp4EthType; else if (firstPayloadNibble == 0x6) ethType = kIp6EthType; else ethType = 0; break; } } } quint8 ipProto = 0; if (ethType == kIp4EthType) { ipProto = parser.field8(offset + kIp4ProtocolOffset); if (!parser.ok()) return 0; quint8 ipHdrLen = parser.field8(offset) & 0x0F; if (!parser.ok()) return 0; offset += 4*ipHdrLen; } else if (ethType == kIp6EthType) { ipProto = parser.field8(offset + kIp6NextHeaderOffset); if (!parser.ok()) return 0; offset += kIp6HeaderSize; // XXX: offset now points to IPv6 payload // Skip IPv6 extension headers, if any while (kIp6ExtensionHeaders.contains(ipProto)) { ipProto = parser.field8(offset + kIp6ExtNextHeaderOffset); if (!parser.ok()) return 0; quint16 extHdrLen = parser.field8(offset + kIp6ExtLengthOffset); offset += 8 + 8*extHdrLen; } } else { // Non-IP // TODO: support MPLS PW with Eth payload return 0; } // XXX: offset now points to IP payload if (ipProto == kIpProtoTcp) { parser.field16(offset + kTcpChecksumOffset); if (!parser.ok()) return 0; return offset + kTcpChecksumOffset; } else if (ipProto == kIpProtoUdp) { parser.field16(offset + kUdpChecksumOffset); if (!parser.ok()) return 0; return offset + kUdpChecksumOffset; } // No L4 return 0; } ostinato-1.3.0/common/packet.h000066400000000000000000000052211451413623100162630ustar00rootroot00000000000000/* Copyright (C) 2023 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _PACKET_H #define _PACKET_H #include #include namespace Packet { class Parser { public: Parser(const uchar *data, int length) : pktData_(data), pktLen_(length) {} quint8 field8(int offset) { if (offset >= pktLen_) { ok_ = false; return 0; } ok_ = true; return pktData_[offset]; } quint16 field16(int offset) { if (offset + 1 >= pktLen_) { ok_ = false; return 0; } ok_ = true; return pktData_[offset] << 8 | pktData_[offset+1]; } quint32 field32(int offset) { if (offset + 3 >= pktLen_) { ok_ = false; return 0; } ok_ = true; return pktData_[offset] << 24 | pktData_[offset+1] << 16 | pktData_[offset+2] << 8 | pktData_[offset+3]; } bool ok() { return ok_; } private: const uchar *pktData_; int pktLen_; bool ok_{false}; }; quint16 l4ChecksumOffset(const uchar *pktData, int pktLen); // // Constants // // Ethernet const quint16 kEthTypeOffset = 12; const quint16 kEthTypeSize = 2; const quint16 kIp4EthType = 0x0800; const quint16 kIp6EthType = 0x86dd; const quint16 kMplsEthType = 0x8847; const QSet kVlanEthTypes = {0x8100, 0x9100, 0x88a8}; const uint kEthOverhead = 20; // VLAN const quint16 kVlanTagSize = 4; // MPLS const quint16 kMplsTagSize = 4; // IPv4 const quint16 kIp4ProtocolOffset = 9; // IPv6 const quint16 kIp6HeaderSize = 40; const quint16 kIp6NextHeaderOffset = 6; // IPv6 Extension Header const quint16 kIp6ExtNextHeaderOffset = 0; const quint16 kIp6ExtLengthOffset = 1; // IPv4/IPv6 Proto/NextHeader values const quint8 kIpProtoTcp = 6; const quint8 kIpProtoUdp = 17; const QSet kIp6ExtensionHeaders = {0, 60, 43, 44, 51, 50, 60, 135}; // FIXME: use names // TCP const quint16 kTcpChecksumOffset = 16; // UDP const quint16 kUdpChecksumOffset = 6; }; #endif ostinato-1.3.0/common/payload.cpp000066400000000000000000000153531451413623100170070ustar00rootroot00000000000000/* Copyright (C) 2010, 2013-2014 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #include "payload.h" #include "streambase.h" PayloadProtocol::PayloadProtocol(StreamBase *stream, AbstractProtocol *parent) : AbstractProtocol(stream, parent) { } PayloadProtocol::~PayloadProtocol() { } AbstractProtocol* PayloadProtocol::createInstance(StreamBase *stream, AbstractProtocol *parent) { return new PayloadProtocol(stream, parent); } quint32 PayloadProtocol::protocolNumber() const { return OstProto::Protocol::kPayloadFieldNumber; } void PayloadProtocol::protoDataCopyInto(OstProto::Protocol &protocol) const { protocol.MutableExtension(OstProto::payload)->CopyFrom(data); protocol.mutable_protocol_id()->set_id(protocolNumber()); } void PayloadProtocol::protoDataCopyFrom(const OstProto::Protocol &protocol) { if (protocol.protocol_id().id() == protocolNumber() && protocol.HasExtension(OstProto::payload)) data.MergeFrom(protocol.GetExtension(OstProto::payload)); } QString PayloadProtocol::name() const { return QString("Payload Data"); } QString PayloadProtocol::shortName() const { return QString("DATA"); } int PayloadProtocol::protocolFrameSize(int streamIndex) const { int len; len = mpStream->frameLen(streamIndex) - protocolFrameOffset(streamIndex) - protocolFramePayloadSize(streamIndex) - kFcsSize; if (len < 0) len = 0; qDebug("%s: this = %p, streamIndex = %d, len = %d", __FUNCTION__, this, streamIndex, len); return len; } int PayloadProtocol::fieldCount() const { return payload_fieldCount; } AbstractProtocol::FieldFlags PayloadProtocol::fieldFlags(int index) const { AbstractProtocol::FieldFlags flags; flags = AbstractProtocol::fieldFlags(index); switch (index) { case payload_dataPattern: break; // Meta fields case payload_dataPatternMode: flags &= ~FrameField; flags |= MetaField; break; } return flags; } QVariant PayloadProtocol::fieldData(int index, FieldAttrib attrib, int streamIndex) const { switch (index) { case payload_dataPattern: switch(attrib) { case FieldName: return QString("Data"); case FieldValue: return data.pattern(); case FieldTextValue: return QString(fieldData(index, FieldFrameValue, streamIndex).toByteArray().toHex()); case FieldFrameValue: { QByteArray fv; int dataLen; dataLen = protocolFrameSize(streamIndex); // FIXME: Hack! Bad! Bad! Very Bad!!! if (dataLen <= 0) dataLen = 1; fv.resize(dataLen+4); switch(data.pattern_mode()) { case OstProto::Payload::e_dp_fixed_word: for (int i = 0; i < (dataLen/4)+1; i++) qToBigEndian((quint32) data.pattern(), (uchar*)(fv.data()+(i*4)) ); break; case OstProto::Payload::e_dp_inc_byte: for (int i = 0; i < dataLen; i++) fv[i] = i % (0xFF + 1); break; case OstProto::Payload::e_dp_dec_byte: for (int i = 0; i < dataLen; i++) fv[i] = 0xFF - (i % (0xFF + 1)); break; case OstProto::Payload::e_dp_random: //! \todo (HIGH) cksum is incorrect for random pattern for (int i = 0; i < dataLen; i++) fv[i] = qrand() % (0xFF + 1); break; default: qWarning("Unhandled data pattern %d", data.pattern_mode()); } fv.resize(dataLen); return fv; } default: break; } break; // Meta fields case payload_dataPatternMode: switch(attrib) { case FieldValue: return data.pattern_mode(); default: break; } break; default: break; } return AbstractProtocol::fieldData(index, attrib, streamIndex); } bool PayloadProtocol::setFieldData(int index, const QVariant &value, FieldAttrib attrib) { bool isOk = false; if (attrib != FieldValue) return false; switch (index) { case payload_dataPattern: { uint pattern = value.toUInt(&isOk); if (isOk) data.set_pattern(pattern); break; } case payload_dataPatternMode: { uint mode = value.toUInt(&isOk); if (isOk && data.DataPatternMode_IsValid(mode)) data.set_pattern_mode(OstProto::Payload::DataPatternMode(mode)); else isOk = false; break; } default: qFatal("%s: unimplemented case %d in switch", __PRETTY_FUNCTION__, index); break; } return isOk; } bool PayloadProtocol::isProtocolFrameValueVariable() const { return (AbstractProtocol::isProtocolFrameValueVariable() || isProtocolFrameSizeVariable()); } bool PayloadProtocol::isProtocolFrameSizeVariable() const { if (mpStream->lenMode() == StreamBase::e_fl_fixed) return false; else return true; } int PayloadProtocol::protocolFrameVariableCount() const { int count = AbstractProtocol::protocolFrameVariableCount(); if (data.pattern_mode() == OstProto::Payload::e_dp_random) return mpStream->frameCount(); count = AbstractProtocol::lcm(count, mpStream->frameSizeVariableCount()); return count; } ostinato-1.3.0/common/payload.h000066400000000000000000000040201451413623100164410ustar00rootroot00000000000000/* Copyright (C) 2010-2014 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _PAYLOAD_H #define _PAYLOAD_H #include "abstractprotocol.h" #include "payload.pb.h" class PayloadProtocol : public AbstractProtocol { public: enum payloadfield { payload_dataPattern, // Meta fields payload_dataPatternMode, payload_fieldCount }; PayloadProtocol(StreamBase *stream, AbstractProtocol *parent = 0); virtual ~PayloadProtocol(); static AbstractProtocol* createInstance(StreamBase *stream, AbstractProtocol *parent = 0); virtual quint32 protocolNumber() const; virtual void protoDataCopyInto(OstProto::Protocol &protocol) const; virtual void protoDataCopyFrom(const OstProto::Protocol &protocol); virtual QString name() const; virtual QString shortName() const; virtual int protocolFrameSize(int streamIndex = 0) const; virtual int fieldCount() const; virtual AbstractProtocol::FieldFlags fieldFlags(int index) const; virtual QVariant fieldData(int index, FieldAttrib attrib, int streamIndex = 0) const; virtual bool setFieldData(int index, const QVariant &value, FieldAttrib attrib = FieldValue); virtual bool isProtocolFrameValueVariable() const; virtual bool isProtocolFrameSizeVariable() const; virtual int protocolFrameVariableCount() const; private: OstProto::Payload data; }; #endif ostinato-1.3.0/common/payload.proto000066400000000000000000000021331451413623100173600ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ import "protocol.proto"; package OstProto; message Payload { enum DataPatternMode { e_dp_fixed_word = 0; e_dp_inc_byte = 1; e_dp_dec_byte = 2; e_dp_random = 3; } // Data Pattern optional DataPatternMode pattern_mode = 1; optional uint32 pattern = 2; //optional uint32 data_start_ofs = 13; } extend Protocol { optional Payload payload = 101; } ostinato-1.3.0/common/payload.ui000066400000000000000000000046541451413623100166440ustar00rootroot00000000000000 payload 0 0 299 114 Form Type cmbPatternMode Fixed Word Increment Byte Decrement Byte Random Qt::Horizontal 40 20 Pattern lePattern >HH HH HH HH; 11 Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter Qt::Vertical 20 40 ostinato-1.3.0/common/payloadconfig.cpp000066400000000000000000000044161451413623100201730ustar00rootroot00000000000000/* Copyright (C) 2010-2014 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #include "payloadconfig.h" #include "payload.h" PayloadConfigForm::PayloadConfigForm(QWidget *parent) : AbstractProtocolConfigForm(parent) { setupUi(this); } PayloadConfigForm::~PayloadConfigForm() { } AbstractProtocolConfigForm* PayloadConfigForm::createInstance() { return new PayloadConfigForm; } void PayloadConfigForm::loadWidget(AbstractProtocol *proto) { cmbPatternMode->setCurrentIndex( proto->fieldData( PayloadProtocol::payload_dataPatternMode, AbstractProtocol::FieldValue ).toUInt()); lePattern->setText(uintToHexStr( proto->fieldData( PayloadProtocol::payload_dataPattern, AbstractProtocol::FieldValue ).toUInt(), 4)); } void PayloadConfigForm::storeWidget(AbstractProtocol *proto) { bool isOk; proto->setFieldData( PayloadProtocol::payload_dataPatternMode, cmbPatternMode->currentIndex()); proto->setFieldData( PayloadProtocol::payload_dataPattern, lePattern->text().remove(QChar(' ')).toUInt(&isOk, BASE_HEX)); } void PayloadConfigForm::on_cmbPatternMode_currentIndexChanged(int index) { switch(index) { case OstProto::Payload::e_dp_fixed_word: lePattern->setEnabled(true); break; case OstProto::Payload::e_dp_inc_byte: case OstProto::Payload::e_dp_dec_byte: case OstProto::Payload::e_dp_random: lePattern->setDisabled(true); break; default: qWarning("Unhandled/Unknown PatternMode = %d",index); } } ostinato-1.3.0/common/payloadconfig.h000066400000000000000000000023361451413623100176370ustar00rootroot00000000000000/* Copyright (C) 2010-2014 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _PAYLOAD_CONFIG_H #define _PAYLOAD_CONFIG_H #include "abstractprotocolconfig.h" #include "ui_payload.h" class PayloadConfigForm : public AbstractProtocolConfigForm, private Ui::payload { Q_OBJECT public: PayloadConfigForm(QWidget *parent = 0); virtual ~PayloadConfigForm(); static AbstractProtocolConfigForm* createInstance(); virtual void loadWidget(AbstractProtocol *proto); virtual void storeWidget(AbstractProtocol *proto); private slots: void on_cmbPatternMode_currentIndexChanged(int index); }; #endif ostinato-1.3.0/common/pcapfileformat.cpp000066400000000000000000000530351451413623100203510ustar00rootroot00000000000000/* Copyright (C) 2011 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #include "pcapfileformat.h" #include "pdmlreader.h" #include "ostprotolib.h" #include "streambase.h" #include "hexdump.pb.h" #include #include #include #include #include #include const quint32 kPcapFileMagic = 0xa1b2c3d4; const quint32 kPcapFileMagicSwapped = 0xd4c3b2a1; const quint32 kNanoSecondPcapFileMagic = 0xa1b23c4d; const quint32 kNanoSecondPcapFileMagicSwapped = 0x4d3cb2a1; const quint16 kPcapFileVersionMajor = 2; const quint16 kPcapFileVersionMinor = 4; const quint32 kMaxSnapLen = 65535; const quint32 kDltEthernet = 1; PcapFileFormat pcapFileFormat; PcapImportOptionsDialog::PcapImportOptionsDialog(QVariantMap *options) : QDialog(NULL) { setupUi(this); setAttribute(Qt::WA_DeleteOnClose); options_ = options; viaPdml->setChecked(options_->value("ViaPdml").toBool()); // XXX: By default this key is absent - so that pcap import tests // evaluate to false and hence show minimal diffs. // However, for the GUI user, this should be enabled by default. recalculateCksums->setChecked( options_->value("RecalculateCksums", QVariant(true)) .toBool()); doDiff->setChecked(options_->value("DoDiff").toBool()); connect(buttonBox, SIGNAL(accepted()), this, SLOT(accept())); } PcapImportOptionsDialog::~PcapImportOptionsDialog() { } void PcapImportOptionsDialog::accept() { options_->insert("ViaPdml", viaPdml->isChecked()); options_->insert("RecalculateCksums", recalculateCksums->isChecked()); options_->insert("DoDiff", doDiff->isChecked()); QDialog::accept(); } PcapFileFormat::PcapFileFormat() { importOptions_.insert("ViaPdml", true); importOptions_.insert("DoDiff", true); } PcapFileFormat::~PcapFileFormat() { } bool PcapFileFormat::open(const QString fileName, OstProto::StreamConfigList &streams, QString &error) { bool isOk = false; QFile file(fileName); QTemporaryFile file2; quint32 magic; uchar gzipMagic[2]; bool nsecResolution = false; int len; PcapFileHeader fileHdr; PcapPacketHeader pktHdr; OstProto::Stream *prevStream = NULL; quint64 lastXsec = 0; int pktCount; qint64 byteCount = 0; qint64 byteTotal; QByteArray pktBuf; bool tryConvert = true; if (!file.open(QIODevice::ReadOnly)) goto _err_open; len = file.peek((char*)gzipMagic, sizeof(gzipMagic)); if (len < int(sizeof(gzipMagic))) goto _err_reading_magic; if ((gzipMagic[0] == 0x1f) && (gzipMagic[1] == 0x8b)) { QProcess gzip; emit status("Decompressing..."); emit target(0); if (!file2.open()) { error.append("Unable to open temporary file to uncompress .gz\n"); goto _err_unzip_fail; } qDebug("decompressing to %s", qPrintable(file2.fileName())); gzip.setStandardOutputFile(file2.fileName()); gzip.start(OstProtoLib::gzipPath(), QStringList() << "-d" << "-c" << fileName); if (!gzip.waitForStarted(-1)) { error.append(QString("Unable to start gzip. Check path in Preferences.\n")); goto _err_unzip_fail; } if (!gzip.waitForFinished(-1)) { error.append(QString("Error running gzip\n")); goto _err_unzip_fail; } file2.seek(0); fd_.setDevice(&file2); } else { fd_.setDevice(&file); } _retry: byteTotal = fd_.device()->size() - sizeof(fileHdr); emit status("Reading File Header..."); emit target(0); fd_ >> magic; qDebug("magic = %08x", magic); if (magic == kPcapFileMagic) { // Do nothing } else if (magic == kNanoSecondPcapFileMagic) { nsecResolution = true; } else if ((magic == kPcapFileMagicSwapped) || (magic == kNanoSecondPcapFileMagicSwapped)) { // Toggle Byte order if (fd_.byteOrder() == QDataStream::BigEndian) fd_.setByteOrder(QDataStream::LittleEndian); else fd_.setByteOrder(QDataStream::BigEndian); nsecResolution = (magic == kNanoSecondPcapFileMagicSwapped); } else // Not a pcap file (could be pcapng or something else) { if (tryConvert) { // Close and reopen the temp file to be safe file2.close(); if (!file2.open()) { error.append("Unable to open temporary file to convert to PCAP\n"); goto _err_convert2pcap; } fd_.setDevice(0); // disconnect data stream from file if (convertToStandardPcap(fileName, file2.fileName(), error)) { fd_.setDevice(&file2); tryConvert = false; goto _retry; } else { error = QString(tr("Unable to convert %1 to standard PCAP format")) .arg(fileName); goto _err_convert2pcap; } } else goto _err_bad_magic; } qDebug("reading filehdr"); fd_ >> fileHdr.versionMajor; fd_ >> fileHdr.versionMinor; fd_ >> fileHdr.thisZone; fd_ >> fileHdr.sigfigs; fd_ >> fileHdr.snapLen; fd_ >> fileHdr.network; qDebug("version check"); if ((fileHdr.versionMajor != kPcapFileVersionMajor) || (fileHdr.versionMinor != kPcapFileVersionMinor)) goto _err_unsupported_version; #if 1 // XXX: we support only Ethernet, for now if (fileHdr.network != kDltEthernet) goto _err_unsupported_encap; #endif pktBuf.resize(fileHdr.snapLen); // XXX: PDML also needs the PCAP file to cross check packet bytes // with the PDML data, so we can't do PDML conversion any earlier // than this qDebug("pdml check"); if (importOptions_.value("ViaPdml").toBool()) { QProcess tshark; QTemporaryFile pdmlFile; PdmlReader reader(&streams, importOptions_); if (!pdmlFile.open()) { error.append("Unable to open temporary file to create PDML\n"); goto _non_pdml; } qDebug("generating PDML %s", qPrintable(pdmlFile.fileName())); emit status("Generating PDML..."); emit target(0); tshark.setStandardOutputFile(pdmlFile.fileName()); tshark.start(OstProtoLib::tsharkPath(), QStringList() << QString("-r%1").arg(fileName) << "-otcp.desegment_tcp_streams:FALSE" << "-Tpdml"); if (!tshark.waitForStarted(-1)) { error.append(QString("Unable to start tshark. Check path in preferences.\n")); goto _non_pdml; } if (!tshark.waitForFinished(-1)) { error.append(QString("Error running tshark\n")); goto _non_pdml; } connect(&reader, SIGNAL(progress(int)), this, SIGNAL(progress(int))); emit status("Reading PDML packets..."); emit target(100); // in percentage // pdml reader needs pcap, so pass self isOk = reader.read(&pdmlFile, this, &stop_); if (stop_) goto _user_cancel; if (!isOk) { error.append(QString("Error processing PDML (%1, %2): %3\n") .arg(reader.lineNumber()) .arg(reader.columnNumber()) .arg(reader.errorString())); goto _exit; } if (!importOptions_.value("DoDiff").toBool()) goto _exit; // !-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-! // Let's do the diff ... // !-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-! QProcess awk; QProcess diff; QTemporaryFile originalTextFile; QTemporaryFile importedPcapFile; QTemporaryFile importedTextFile; QTemporaryFile diffFile; const QString kAwkFilter = "/^[^0]/ { " "printf \" %s \", $1;" "for (i=4; i %s", qPrintable(originalTextFile.fileName()), qPrintable(importedTextFile.fileName()), qPrintable(diffFile.fileName())); emit status("Taking diff..."); emit target(0); diff.setStandardOutputFile(diffFile.fileName()); diff.start(OstProtoLib::diffPath(), QStringList() << "-u" << "-F^ [1-9]" << QString("--label=%1 (actual)") .arg(QFileInfo(fileName).fileName()) << QString("--label=%1 (imported)") .arg(QFileInfo(fileName).fileName()) << originalTextFile.fileName() << importedTextFile.fileName()); if (!diff.waitForStarted(-1)) { error.append(QString("Unable to start diff. Check path in Preferences.\n") .arg(diff.exitCode())); goto _diff_fail; } if (!diff.waitForFinished(-1)) { error.append(QString("Error running diff\n")); goto _diff_fail; } diffFile.close(); if (diffFile.size()) { error.append(tr("

There is a diff between the original and imported streams. See details to review the diff.

Why a diff? See possible reasons.

\n\n\n\n").arg("https://jump.ostinato.org/pcapdiff")); diffFile.open(); diffFile.seek(0); error.append(QString(diffFile.readAll())); } goto _exit; } _non_pdml: qDebug("pcap resolution: %s", nsecResolution ? "nsec" : "usec"); emit status("Reading Packets..."); emit target(100); // in percentage pktCount = 1; while (!fd_.atEnd()) { OstProto::Stream *stream = streams.add_stream(); OstProto::Protocol *proto = stream->add_protocol(); OstProto::HexDump *hexDump = proto->MutableExtension(OstProto::hexDump); proto->mutable_protocol_id()->set_id( OstProto::Protocol::kHexDumpFieldNumber); readPacket(pktHdr, pktBuf); // validations on inclLen <= origLen && inclLen <= snapLen Q_ASSERT(pktHdr.inclLen <= fileHdr.snapLen); // TODO: convert to if hexDump->set_content(pktBuf.data(), pktHdr.inclLen); hexDump->set_pad_until_end(false); stream->mutable_stream_id()->set_id(pktCount); stream->mutable_core()->set_is_enabled(true); stream->mutable_core()->set_frame_len(pktHdr.inclLen+4); // FCS stream->mutable_control()->set_num_packets(1); // setup packet rate to the timing in pcap (as close as possible) // use quint64 rather than double to store micro/nano second as // it has a larger range (~580 years) and therefore better accuracy const quint64 kXsecsInSec = nsecResolution ? 1e9 : 1e6; quint64 xsec = (pktHdr.tsSec*kXsecsInSec + pktHdr.tsUsec); quint64 delta = xsec - lastXsec; qDebug("pktCount = %d, delta = %llu", pktCount, delta); if ((pktCount != 1) && delta) stream->mutable_control()->set_packets_per_sec(double(kXsecsInSec)/delta); if (prevStream) prevStream->mutable_control()->CopyFrom(stream->control()); lastXsec = xsec; prevStream = stream; pktCount++; byteCount += pktHdr.inclLen + sizeof(pktHdr); emit progress(int(byteCount*100/byteTotal)); // in percentage if (stop_) goto _user_cancel; } isOk = true; goto _exit; _user_cancel: isOk = true; goto _exit; _diff_fail: goto _exit; _err_unsupported_encap: error = QString(tr("%1 has non-ethernet encapsulation (%2) which is " "not supported - Sorry!")) .arg(QFileInfo(fileName).fileName()).arg(fileHdr.network); goto _exit; _err_unsupported_version: error = QString(tr("%1 is in PCAP version %2.%3 format which is " "not supported - Sorry!")) .arg(fileName).arg(fileHdr.versionMajor).arg(fileHdr.versionMinor); goto _exit; _err_bad_magic: error = QString(tr("%1 is not a valid PCAP file")).arg(fileName); goto _exit; #if 0 _err_truncated: error = QString(tr("%1 is too short")).arg(fileName); goto _exit; #endif _err_unzip_fail: goto _exit; _err_reading_magic: error = QString(tr("Unable to read magic from %1")).arg(fileName); goto _exit; _err_convert2pcap: goto _exit; _err_open: error = QString(tr("Unable to open file: %1")).arg(fileName); goto _exit; _exit: if (!error.isEmpty()) qDebug("%s", qPrintable(error)); file.close(); return isOk; } /*! Converts a non-PCAP capture file to standard PCAP file format using tshark Returns true if conversion was successful, false otherwise. */ bool PcapFileFormat::convertToStandardPcap( QString fileName, QString outputFileName, QString &error) { qDebug("converting to PCAP %s", qPrintable(outputFileName)); emit status("Unsupported format. Converting to standard PCAP format..."); emit target(0); QProcess tshark; tshark.start(OstProtoLib::tsharkPath(), QStringList() << QString("-r%1").arg(fileName) << "-Fnsecpcap" << QString("-w%1").arg(outputFileName)); if (!tshark.waitForStarted(-1)) { error.append(QString("Unable to start tshark. Check path in preferences.\n")); return false; } if (!tshark.waitForFinished(-1)) { error.append(QString("Error running tshark\n")); return false; } return true; } /*! Reads packet meta data into pktHdr and packet content into buf. Returns true if packet is read successfully, false otherwise. */ bool PcapFileFormat::readPacket(PcapPacketHeader &pktHdr, QByteArray &pktBuf) { quint32 len; // TODO: chk fd_.status() // read PcapPacketHeader fd_ >> pktHdr.tsSec; fd_ >> pktHdr.tsUsec; fd_ >> pktHdr.inclLen; fd_ >> pktHdr.origLen; // TODO: chk fd_.status() // XXX: should never be required, but we play safe if (quint32(pktBuf.size()) < pktHdr.inclLen) pktBuf.resize(pktHdr.inclLen); // read Pkt contents len = fd_.readRawData(pktBuf.data(), pktHdr.inclLen); // TODO: use while? Q_ASSERT(len == pktHdr.inclLen); // TODO: remove assert pktBuf.resize(len); return true; } bool PcapFileFormat::save(const OstProto::StreamConfigList streams, const QString fileName, QString &error) { bool isOk = false; QFile file(fileName); PcapFileHeader fileHdr; PcapPacketHeader pktHdr; QByteArray pktBuf; if (!file.open(QIODevice::WriteOnly)) goto _err_open; fd_.setDevice(&file); fileHdr.magicNumber = kNanoSecondPcapFileMagic; fileHdr.versionMajor = kPcapFileVersionMajor; fileHdr.versionMinor = kPcapFileVersionMinor; fileHdr.thisZone = 0; fileHdr.sigfigs = 0; fileHdr.snapLen = kMaxSnapLen; fileHdr.network = kDltEthernet; fd_ << fileHdr.magicNumber; fd_ << fileHdr.versionMajor; fd_ << fileHdr.versionMinor; fd_ << fileHdr.thisZone; fd_ << fileHdr.sigfigs; fd_ << fileHdr.snapLen; fd_ << fileHdr.network; pktBuf.resize(kMaxSnapLen); emit status("Writing Packets..."); emit target(streams.stream_size()); pktHdr.tsSec = 0; pktHdr.tsUsec = 0; for (int i = 0; i < streams.stream_size(); i++) { StreamBase s; s.setId(i); s.protoDataCopyFrom(streams.stream(i)); // TODO: expand frameIndex for each stream s.frameValue((uchar*)pktBuf.data(), pktBuf.size(), 0); pktHdr.inclLen = s.frameProtocolLength(0); // FIXME: stream index = 0 pktHdr.origLen = s.frameLen() - 4; // FCS; FIXME: Hardcoding qDebug("savepcap i=%d, incl/orig len = %d/%d", i, pktHdr.inclLen, pktHdr.origLen); if (pktHdr.inclLen > fileHdr.snapLen) pktHdr.inclLen = fileHdr.snapLen; fd_ << pktHdr.tsSec; fd_ << pktHdr.tsUsec; fd_ << pktHdr.inclLen; fd_ << pktHdr.origLen; fd_.writeRawData(pktBuf.data(), pktHdr.inclLen); if (s.packetRate()) { quint64 delta = quint64(1e9/s.packetRate()); pktHdr.tsSec += delta/quint32(1e9); pktHdr.tsUsec += delta % quint32(1e9); } if (pktHdr.tsUsec >= quint32(1e9)) { pktHdr.tsSec++; pktHdr.tsUsec -= quint32(1e9); } emit progress(i); } file.close(); isOk = true; goto _exit; _err_open: error = QString(tr("Unable to open file: %1")).arg(fileName); goto _exit; _exit: return isOk; } QDialog* PcapFileFormat::openOptionsDialog() { return new PcapImportOptionsDialog(&importOptions_); } bool PcapFileFormat::isMyFileFormat(const QString /*fileName*/) { // TODO return true; } bool PcapFileFormat::isMyFileType(const QString fileType) { if (fileType.startsWith("PCAP")) return true; else return false; } ostinato-1.3.0/common/pcapfileformat.h000066400000000000000000000051321451413623100200110ustar00rootroot00000000000000/* Copyright (C) 2011 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _PCAP_FILE_FORMAT_H #define _PCAP_FILE_FORMAT_H #include "streamfileformat.h" #include "ui_pcapfileimport.h" #include #include class PcapImportOptionsDialog: public QDialog, public Ui::PcapFileImport { public: PcapImportOptionsDialog(QVariantMap *options); ~PcapImportOptionsDialog(); private slots: void accept(); private: QVariantMap *options_; }; class PdmlReader; class PcapFileFormat : public StreamFileFormat { friend class PdmlReader; public: PcapFileFormat(); ~PcapFileFormat(); bool open(const QString fileName, OstProto::StreamConfigList &streams, QString &error); bool save(const OstProto::StreamConfigList streams, const QString fileName, QString &error); virtual QDialog* openOptionsDialog(); bool isMyFileFormat(const QString fileName); bool isMyFileType(const QString fileType); private: typedef struct { quint32 magicNumber; /* magic number */ quint16 versionMajor; /* major version number */ quint16 versionMinor; /* minor version number */ qint32 thisZone; /* GMT to local correction */ quint32 sigfigs; /* accuracy of timestamps */ quint32 snapLen; /* max length of captured packets, in octets */ quint32 network; /* data link type */ } PcapFileHeader; typedef struct { quint32 tsSec; /* timestamp seconds */ quint32 tsUsec; /* timestamp microseconds */ quint32 inclLen; /* number of octets of packet saved in file */ quint32 origLen; /* actual length of packet */ } PcapPacketHeader; bool convertToStandardPcap(QString fileName, QString outputFileName, QString &error); bool readPacket(PcapPacketHeader &pktHdr, QByteArray &pktBuf); QDataStream fd_; QVariantMap importOptions_; }; extern PcapFileFormat pcapFileFormat; #endif ostinato-1.3.0/common/pcapfileimport.ui000066400000000000000000000076531451413623100202330ustar00rootroot00000000000000 PcapFileImport 0 0 326 132 PCAP import options Intelligent Import (via PDML) 0 0 16 16 false Recalculate Checksums false Do a diff after import Qt::Horizontal QDialogButtonBox::Cancel|QDialogButtonBox::Ok buttonBox accepted() PcapFileImport accept() 249 81 157 90 buttonBox rejected() PcapFileImport reject() 249 81 258 90 viaPdml toggled(bool) doDiff setEnabled(bool) 15 16 68 71 viaPdml toggled(bool) doDiff setChecked(bool) 151 14 181 71 viaPdml toggled(bool) recalculateCksums setEnabled(bool) 29 17 38 39 viaPdml toggled(bool) recalculateCksums setChecked(bool) 67 18 66 33 ostinato-1.3.0/common/pdmlfileformat.cpp000066400000000000000000000077551451413623100203720ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #include "pdmlfileformat.h" #include "ostprotolib.h" #include "pdmlreader.h" #include #include PdmlFileFormat pdmlFileFormat; PdmlFileFormat::PdmlFileFormat() { } PdmlFileFormat::~PdmlFileFormat() { } bool PdmlFileFormat::open(const QString fileName, OstProto::StreamConfigList &streams, QString &error) { bool isOk = false; QFile file(fileName); PdmlReader *reader = new PdmlReader(&streams); if (!file.open(QIODevice::ReadOnly)) goto _open_fail; connect(reader, SIGNAL(progress(int)), this, SIGNAL(progress(int))); emit status("Reading PDML packets..."); emit target(100); // in percentage isOk = reader->read(&file, NULL, &stop_); if (stop_) goto _user_cancel; if (!isOk) { error.append(QString("Error processing PDML (%1, %2): %3\n") .arg(reader->lineNumber()) .arg(reader->columnNumber()) .arg(reader->errorString())); goto _exit; } goto _exit; _open_fail: isOk = false; _user_cancel: _exit: delete reader; return isOk; } bool PdmlFileFormat::save(const OstProto::StreamConfigList streams, const QString fileName, QString &error) { bool isOk = false; QTemporaryFile pcapFile; StreamFileFormat *fmt = StreamFileFormat::fileFormatFromType("PCAP"); QProcess tshark; Q_ASSERT(fmt); if (!pcapFile.open()) { error.append("Unable to open temporary file to create PCAP\n"); goto _fail; } qDebug("intermediate PCAP %s", qPrintable(pcapFile.fileName())); connect(fmt, SIGNAL(target(int)), this, SIGNAL(target(int))); connect(fmt, SIGNAL(progress(int)), this, SIGNAL(progress(int))); emit status("Writing intermediate PCAP file..."); isOk = fmt->save(streams, pcapFile.fileName(), error); qDebug("generating PDML %s", qPrintable(fileName)); emit status("Converting PCAP to PDML..."); emit target(0); tshark.setStandardOutputFile(fileName); tshark.start(OstProtoLib::tsharkPath(), QStringList() << QString("-r%1").arg(pcapFile.fileName()) << "-Tpdml"); if (!tshark.waitForStarted(-1)) { error.append(QString("Unable to start tshark. Check path in preferences.\n")); goto _fail; } if (!tshark.waitForFinished(-1)) { error.append(QString("Error running tshark\n")); goto _fail; } isOk = true; _fail: return isOk; } bool PdmlFileFormat::isMyFileFormat(const QString fileName) { bool ret = false; QFile file(fileName); QByteArray buf; QXmlStreamReader xml; if (!file.open(QIODevice::ReadOnly)) goto _exit; xml.setDevice(&file); xml.readNext(); if (xml.hasError() || !xml.isStartDocument()) goto _close_exit; // skip everything until the start of the first element while (!xml.isStartElement()) { xml.readNext(); if (xml.hasError()) goto _close_exit; } if (!xml.hasError() && xml.isStartElement() && (xml.name() == "pdml")) ret = true; else ret = false; _close_exit: xml.clear(); file.close(); _exit: return ret; } bool PdmlFileFormat::isMyFileType(const QString fileType) { if (fileType.startsWith("PDML")) return true; else return false; } ostinato-1.3.0/common/pdmlfileformat.h000066400000000000000000000023311451413623100200200ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _PDML_FILE_FORMAT_H #define _PDML_FILE_FORMAT_H #include "streamfileformat.h" class PdmlFileFormat : public StreamFileFormat { public: PdmlFileFormat(); ~PdmlFileFormat(); virtual bool open(const QString fileName, OstProto::StreamConfigList &streams, QString &error); virtual bool save(const OstProto::StreamConfigList streams, const QString fileName, QString &error); bool isMyFileFormat(const QString fileName); bool isMyFileType(const QString fileType); }; extern PdmlFileFormat pdmlFileFormat; #endif ostinato-1.3.0/common/pdmlprotocol.cpp000066400000000000000000000163521451413623100200740ustar00rootroot00000000000000/* Copyright (C) 2011 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #include "pdmlprotocol.h" /*! \class PdmlProtocol PdmlProtocol is the base class which provides the interface for all PDML decode helper protocols All Pdml helper classes derived from PdmlProtocol MUST register themselves with PdmlReader. When PdmlReader encounters a 'proto' tag in the PDML during parsing, it instantiates the corresponding helper PdmlProtocol class and calls its methods to decode the protocol. A subclass MUST initialize the following inherited protected variables in its constructor - - ostProtoId_ - fieldMap_ A subclass typically needs to reimplement the following methods - - createInstance() Depending on certain conditions, subclasses may need to reimplement the following additional methods - - unknownFieldHandler() - preProtocolHandler() - postProtocolHandler() See the description of the methods for more information. Use the SamplePdmlProtocol implementation as boilerplate code and for guidelines and tips */ /*! Constructs the PdmlProtocol */ PdmlProtocol::PdmlProtocol() { ostProtoId_ = -1; } /*! Destroys the PdmlProtocol */ PdmlProtocol::~PdmlProtocol() { } /*! Allocates and returns a new instance of the class Caller is responsible for freeing up after use. Subclasses MUST implement this function and register it with PdmlReader */ PdmlProtocol* PdmlProtocol::createInstance() { return new PdmlProtocol(); } /*! Returns the protocol's field number as defined in message 'Protocol', enum 'k' (file: protocol.proto) */ int PdmlProtocol::ostProtoId() const { return ostProtoId_; } /*! Returns true if name is a 'known' field that can be directly mapped to the protobuf field */ bool PdmlProtocol::hasField(QString name) const { return fieldMap_.contains(name); } /*! Returns the protocol's protobuf field number corresponding to name */ int PdmlProtocol::fieldId(QString name) const { return fieldMap_.value(name); } void PdmlProtocol::setRecalculateCksum(bool recalculate) { overrideCksum_ = !recalculate; } /*! This method is called by PdmlReader before any fields within the protocol are processed. All attributes associated with the 'proto' tag in the PDML are passed to this method Use this method to do any special handling that may be required for preprocessing */ void PdmlProtocol::preProtocolHandler(QString /*name*/, const QXmlStreamAttributes& /*attributes*/, int /*expectedPos*/, OstProto::Protocol* /*pbProto*/, OstProto::Stream* /*stream*/) { return; // do nothing! } /*! This method is called by PdmlReader when it encounters a nested protocol in the PDML i.e. a protocol within a protocol or a protocol within a field This is a notification to the protocol that protocol processing will be ending prematurely. postProtocolHandler() will still be called in such cases. */ void PdmlProtocol::prematureEndHandler(int /*pos*/, OstProto::Protocol* /*pbProto*/, OstProto::Stream* /*stream*/) { return; // do nothing! } /*! This method is called by PdmlReader after all fields within the protocol are processed. Use this method to do any special handling that may be required for postprocessing */ void PdmlProtocol::postProtocolHandler(OstProto::Protocol* /*pbProto*/, OstProto::Stream* /*stream*/) { return; // do nothing! } /*! This method is called by PdmlReader for each field in the protocol Depending on whether it is a known or unknown field, the virtual methods knownFieldHandler() and unknownFieldHandler() are invoked */ void PdmlProtocol::fieldHandler(QString name, const QXmlStreamAttributes &attributes, OstProto::Protocol *pbProto, OstProto::Stream *stream) { if (hasField(name)) { QString valueHexStr = attributes.value("value").toString(); qDebug("\t(KNOWN) fieldName:%s, value:%s", qPrintable(name), qPrintable(valueHexStr)); if (!valueHexStr.isEmpty()) knownFieldHandler(name, valueHexStr, pbProto); } else { int pos = -1; int size = -1; if (!attributes.value("pos").isEmpty()) pos = attributes.value("pos").toString().toInt(); if (!attributes.value("size").isEmpty()) size = attributes.value("size").toString().toInt(); qDebug("\t(UNKNOWN) fieldName:%s, pos:%d, size:%d", qPrintable(name), pos, size); unknownFieldHandler(name, pos, size, attributes, pbProto, stream); } } /*! Handles a 'known' field Uses protobuf reflection interface to set the protobuf field name to valueHexStr as per the field's datatype */ void PdmlProtocol::knownFieldHandler(QString name, QString valueHexStr, OstProto::Protocol *pbProto) { const google::protobuf::Reflection *protoRefl = pbProto->GetReflection(); const google::protobuf::FieldDescriptor *extDesc = protoRefl->FindKnownExtensionByNumber(ostProtoId()); google::protobuf::Message *msg = protoRefl->MutableMessage(pbProto,extDesc); const google::protobuf::Reflection *msgRefl = msg->GetReflection(); const google::protobuf::FieldDescriptor *fieldDesc = msg->GetDescriptor()->FindFieldByNumber(fieldId(name)); bool isOk; Q_ASSERT(fieldDesc != NULL); switch(fieldDesc->cpp_type()) { case google::protobuf::FieldDescriptor::CPPTYPE_BOOL: msgRefl->SetBool(msg, fieldDesc, bool(valueHexStr.toUInt(&isOk))); break; case google::protobuf::FieldDescriptor::CPPTYPE_ENUM: // TODO case google::protobuf::FieldDescriptor::CPPTYPE_UINT32: msgRefl->SetUInt32(msg, fieldDesc, valueHexStr.toUInt(&isOk, kBaseHex)); break; case google::protobuf::FieldDescriptor::CPPTYPE_UINT64: msgRefl->SetUInt64(msg, fieldDesc, valueHexStr.toULongLong(&isOk, kBaseHex)); break; case google::protobuf::FieldDescriptor::CPPTYPE_STRING: { QByteArray hexVal = QByteArray::fromHex(valueHexStr.toUtf8()); std::string str(hexVal.constData(), hexVal.size()); msgRefl->SetString(msg, fieldDesc, str); break; } default: qDebug("%s: unhandled cpptype = %d", __FUNCTION__, fieldDesc->cpp_type()); } } /*! Handles a 'unknown' field The default implementation does nothing. Subclasses may need to implement this if the protocol contains 'unknown' fields. */ void PdmlProtocol::unknownFieldHandler(QString /*name*/, int /*pos*/, int /*size*/, const QXmlStreamAttributes& /*attributes*/, OstProto::Protocol* /*pbProto*/, OstProto::Stream* /*stream*/) { return; // do nothing! } ostinato-1.3.0/common/pdmlprotocol.h000066400000000000000000000043461451413623100175410ustar00rootroot00000000000000/* Copyright (C) 2011 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _PDML_PROTOCOL_H #define _PDML_PROTOCOL_H #include "protocol.pb.h" #include #include #include #include const int kBaseHex = 16; class PdmlProtocol { public: virtual ~PdmlProtocol(); static PdmlProtocol* createInstance(); int ostProtoId() const; bool hasField(QString name) const; int fieldId(QString name) const; void setRecalculateCksum(bool recalculate); virtual void preProtocolHandler(QString name, const QXmlStreamAttributes &attributes, int expectedPos, OstProto::Protocol *pbProto, OstProto::Stream *stream); virtual void prematureEndHandler(int pos, OstProto::Protocol *pbProto, OstProto::Stream *stream); virtual void postProtocolHandler(OstProto::Protocol *pbProto, OstProto::Stream *stream); void fieldHandler(QString name, const QXmlStreamAttributes &attributes, OstProto::Protocol *pbProto, OstProto::Stream *stream); void knownFieldHandler(QString name, QString valueHexStr, OstProto::Protocol *pbProto); virtual void unknownFieldHandler(QString name, int pos, int size, const QXmlStreamAttributes &attributes, OstProto::Protocol *pbProto, OstProto::Stream *stream); protected: PdmlProtocol(); //!< Protocol's field number as defined in message 'Protocol', enum 'k' int ostProtoId_; //!< Map of PDML field names to protobuf field numbers for 'known' fields QMap fieldMap_; bool overrideCksum_{true}; }; #endif ostinato-1.3.0/common/pdmlprotocols.cpp000066400000000000000000000137241451413623100202570ustar00rootroot00000000000000/* Copyright (C) 2011 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #include "pdmlprotocols.h" #include "hexdump.pb.h" // ---------------------------------------------------------- // // PdmlUnknownProtocol // // ---------------------------------------------------------- // PdmlUnknownProtocol::PdmlUnknownProtocol() { ostProtoId_ = OstProto::Protocol::kHexDumpFieldNumber; endPos_ = expPos_ = -1; } PdmlProtocol* PdmlUnknownProtocol::createInstance() { return new PdmlUnknownProtocol(); } void PdmlUnknownProtocol::preProtocolHandler(QString /*name*/, const QXmlStreamAttributes &attributes, int expectedPos, OstProto::Protocol* /*pbProto*/, OstProto::Stream *stream) { bool isOk; int size; int pos = attributes.value("pos").toString().toUInt(&isOk); if (!isOk) { if (expectedPos >= 0) expPos_ = pos = expectedPos; else goto _skip_pos_size_proc; } size = attributes.value("size").toString().toUInt(&isOk); if (!isOk) goto _skip_pos_size_proc; // If pos+size goes beyond the frame length, this is a "reassembled" // protocol and should be skipped if ((pos + size) > int(stream->core().frame_len())) goto _skip_pos_size_proc; expPos_ = pos; endPos_ = expPos_ + size; _skip_pos_size_proc: OstProto::HexDump *hexDump = stream->mutable_protocol( stream->protocol_size()-1)->MutableExtension(OstProto::hexDump); hexDump->set_pad_until_end(false); } void PdmlUnknownProtocol::prematureEndHandler(int pos, OstProto::Protocol* /*pbProto*/, OstProto::Stream* /*stream*/) { endPos_ = pos; } void PdmlUnknownProtocol::postProtocolHandler(OstProto::Protocol *pbProto, OstProto::Stream *stream) { OstProto::HexDump *hexDump = pbProto->MutableExtension(OstProto::hexDump); // Skipped field(s) at end? Pad with zero! if (endPos_ > expPos_) { QByteArray hexVal(endPos_ - expPos_, char(0)); hexDump->mutable_content()->append(hexVal.constData(), hexVal.size()); expPos_ += hexVal.size(); } qDebug(" hexdump: expPos_ = %d, endPos_ = %d\n", expPos_, endPos_); // If empty for some reason, remove the protocol if (hexDump->content().size() == 0) stream->mutable_protocol()->RemoveLast(); endPos_ = expPos_ = -1; } void PdmlUnknownProtocol::unknownFieldHandler(QString name, int pos, int /*size*/, const QXmlStreamAttributes &attributes, OstProto::Protocol *pbProto, OstProto::Stream* /*stream*/) { OstProto::HexDump *hexDump = pbProto->MutableExtension(OstProto::hexDump); qDebug(" hexdump: %s, pos = %d, expPos_ = %d, endPos_ = %d\n", qPrintable(name), pos, expPos_, endPos_); // Skipped field? Pad with zero! if ((pos > expPos_) && (expPos_ < endPos_)) { QByteArray hexVal(pos - expPos_, char(0)); hexDump->mutable_content()->append(hexVal.constData(), hexVal.size()); expPos_ += hexVal.size(); } if (pos == expPos_) { QByteArray hexVal = attributes.value("unmaskedvalue").isEmpty() ? QByteArray::fromHex(attributes.value("value").toString().toUtf8()) : QByteArray::fromHex(attributes.value("unmaskedvalue").toString().toUtf8()); hexDump->mutable_content()->append(hexVal.constData(), hexVal.size()); expPos_ += hexVal.size(); } } // ---------------------------------------------------------- // // PdmlGenInfoProtocol // // ---------------------------------------------------------- // PdmlGenInfoProtocol::PdmlGenInfoProtocol() { } PdmlProtocol* PdmlGenInfoProtocol::createInstance() { return new PdmlGenInfoProtocol(); } // ---------------------------------------------------------- // // PdmlFrameProtocol // // ---------------------------------------------------------- // PdmlFrameProtocol::PdmlFrameProtocol() { } PdmlProtocol* PdmlFrameProtocol::createInstance() { return new PdmlFrameProtocol(); } void PdmlFrameProtocol::unknownFieldHandler(QString name, int /*pos*/, int /*size*/, const QXmlStreamAttributes &attributes, OstProto::Protocol* /*pbProto*/, OstProto::Stream *stream) { if (name == "frame.len") { int len = -1; if (!attributes.value("show").isEmpty()) len = attributes.value("show").toString().toInt(); stream->mutable_core()->set_frame_len(len+4); // TODO:check FCS } else if (name == "frame.time_delta") { if (!attributes.value("show").isEmpty()) { QString delta = attributes.value("show").toString(); int decimal = delta.indexOf('.'); if (decimal >= 0) { const double kNsecsInSec = 1e9; uint sec = delta.left(decimal).toUInt(); uint nsec = delta.mid(decimal+1).toUInt(); uint ipg = sec*kNsecsInSec + nsec; if (ipg) { stream->mutable_control()->set_packets_per_sec( kNsecsInSec/ipg); } qDebug("sec.nsec = %u.%u, ipg = %u", sec, nsec, ipg); } } } else if (name == "frame.number") stream->mutable_control()->set_num_packets(1); } ostinato-1.3.0/common/pdmlprotocols.h000066400000000000000000000037531451413623100177250ustar00rootroot00000000000000/* Copyright (C) 2011 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _PDML_PROTOCOLS_H #define _PDML_PROTOCOLS_H #include "pdmlprotocol.h" class PdmlUnknownProtocol : public PdmlProtocol { public: static PdmlProtocol* createInstance(); virtual void preProtocolHandler(QString name, const QXmlStreamAttributes &attributes, int expectedPos, OstProto::Protocol *pbProto, OstProto::Stream *stream); virtual void prematureEndHandler(int pos, OstProto::Protocol *pbProto, OstProto::Stream *stream); virtual void postProtocolHandler(OstProto::Protocol *pbProto, OstProto::Stream *stream); virtual void unknownFieldHandler(QString name, int pos, int size, const QXmlStreamAttributes &attributes, OstProto::Protocol *pbProto, OstProto::Stream *stream); protected: PdmlUnknownProtocol(); private: int endPos_; int expPos_; }; class PdmlGenInfoProtocol : public PdmlProtocol { public: static PdmlProtocol* createInstance(); protected: PdmlGenInfoProtocol(); }; class PdmlFrameProtocol : public PdmlProtocol { public: static PdmlProtocol* createInstance(); virtual void unknownFieldHandler(QString name, int pos, int size, const QXmlStreamAttributes &attributes, OstProto::Protocol *pbProto, OstProto::Stream *stream); protected: PdmlFrameProtocol(); }; #endif ostinato-1.3.0/common/pdmlreader.cpp000066400000000000000000000375651451413623100175060ustar00rootroot00000000000000/* Copyright (C) 2011 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #include "pdmlreader.h" #include "abstractprotocol.h" #include "hexdump.pb.h" #include "pcapfileformat.h" #include "streambase.h" #include "pdmlprotocols.h" #include "arppdml.h" #include "eth2pdml.h" #include "grepdml.h" #include "llcpdml.h" #include "icmppdml.h" #include "icmp6pdml.h" #include "igmppdml.h" #include "ip4pdml.h" #include "ip6pdml.h" #include "mldpdml.h" #include "stppdml.h" #include "svlanpdml.h" #include "tcppdml.h" #include "textprotopdml.h" #include "udppdml.h" #include "vlanpdml.h" PdmlReader::PdmlReader(OstProto::StreamConfigList *streams, const QVariantMap &options) { //gPdmlReader = this; pcap_ = NULL; streams_ = streams; recalculateCksums_ = options.value("RecalculateCksums").toBool(); currentStream_ = NULL; prevStream_ = NULL; stop_ = NULL; factory_.insert("hexdump", PdmlUnknownProtocol::createInstance); factory_.insert("geninfo", PdmlGenInfoProtocol::createInstance); factory_.insert("frame", PdmlFrameProtocol::createInstance); factory_.insert("arp", PdmlArpProtocol::createInstance); factory_.insert("eth", PdmlEthProtocol::createInstance); factory_.insert("gre", PdmlGreProtocol::createInstance); factory_.insert("http", PdmlTextProtocol::createInstance); factory_.insert("icmp", PdmlIcmpProtocol::createInstance); factory_.insert("icmpv6", PdmlIcmp6Protocol::createInstance); factory_.insert("igmp", PdmlIgmpProtocol::createInstance); factory_.insert("ieee8021ad", PdmlSvlanProtocol::createInstance); factory_.insert("imap", PdmlTextProtocol::createInstance); factory_.insert("ip", PdmlIp4Protocol::createInstance); factory_.insert("ipv6", PdmlIp6Protocol::createInstance); factory_.insert("llc", PdmlLlcProtocol::createInstance); factory_.insert("nntp", PdmlTextProtocol::createInstance); factory_.insert("pop", PdmlTextProtocol::createInstance); factory_.insert("rtsp", PdmlTextProtocol::createInstance); factory_.insert("sdp", PdmlTextProtocol::createInstance); factory_.insert("sip", PdmlTextProtocol::createInstance); factory_.insert("smtp", PdmlTextProtocol::createInstance); factory_.insert("stp", PdmlStpProtocol::createInstance); factory_.insert("tcp", PdmlTcpProtocol::createInstance); factory_.insert("udp", PdmlUdpProtocol::createInstance); factory_.insert("udplite", PdmlUdpProtocol::createInstance); factory_.insert("vlan", PdmlVlanProtocol::createInstance); } PdmlReader::~PdmlReader() { } bool PdmlReader::read(QIODevice *device, PcapFileFormat *pcap, bool *stop) { setDevice(device); pcap_ = pcap; stop_ = stop; while (!atEnd()) { readNext(); if (isStartElement()) { if (name() == "pdml") readPdml(); else raiseError("Not a pdml file!"); } } if (error() && (errorString() != "USER-CANCEL")) { qDebug("Line %lld", lineNumber()); qDebug("Col %lld", columnNumber()); qDebug("%s", qPrintable(errorString())); return false; } return true; } // TODO: use a temp pool to avoid a lot of new/delete PdmlProtocol* PdmlReader::allocPdmlProtocol(QString protoName) { // If protoName is not known, we use a hexdump if (!factory_.contains(protoName)) protoName = "hexdump"; // If MLD is not supported by the creator of the PDML, we interpret // ICMPv6 as ICMP since our implementation of the ICMPv6 PDML protocol // exists just to distinguish between MLD and ICMP. Non MLD ICMPv6 is // also handled by ICMP only if (!isMldSupport_ && (protoName == "icmpv6")) protoName = "icmp"; return (*(factory_.value(protoName)))(); } void PdmlReader::freePdmlProtocol(PdmlProtocol *proto) { delete proto; } bool PdmlReader::isDontCareProto() { Q_ASSERT(isStartElement() && name() == "proto"); QStringRef protoName = attributes().value("name"); if (protoName.isEmpty() || (protoName == "expert")) return true; return false; } void PdmlReader::skipElement() { Q_ASSERT(isStartElement()); qDebug("skipping element - <%s>", qPrintable(name().toString())); while (!atEnd()) { readNext(); if (isEndElement()) break; if (isStartElement()) skipElement(); } } void PdmlReader::readPdml() { QStringList creator; Q_ASSERT(isStartElement() && name() == "pdml"); isMldSupport_ = true; creator = attributes().value("creator").toString().split('/'); if ((creator.size() >= 2) && (creator.at(0) == "wireshark")) { QList minMldVer; minMldVer << 1 << 5 << 0; QStringList version = creator.at(1).split('.'); for (int i = 0; i < qMin(version.size(), minMldVer.size()); i++) { if (version.at(i).toUInt() < minMldVer.at(i)) { isMldSupport_ = false; break; } } } packetCount_ = 1; while (!atEnd()) { readNext(); if (isEndElement()) break; if (isStartElement()) { if (name() == "packet") readPacket(); else skipElement(); } } } void PdmlReader::readPacket() { PcapFileFormat::PcapPacketHeader pktHdr; Q_ASSERT(isStartElement() && name() == "packet"); qDebug("%s: packetNum = %d", __FUNCTION__, packetCount_); skipUntilEnd_ = false; // XXX: we play dumb and convert each packet to a stream, for now prevStream_ = currentStream_; currentStream_ = streams_->add_stream(); currentStream_->mutable_stream_id()->set_id(packetCount_); currentStream_->mutable_core()->set_is_enabled(true); // Set to a high number; will get reset to correct value during parse currentStream_->mutable_core()->set_frame_len(16384); // FIXME: Hard coding! expPos_ = 0; if (pcap_) pcap_->readPacket(pktHdr, pktBuf_); while (!atEnd()) { readNext(); if (isEndElement()) break; if (isStartElement()) { if (skipUntilEnd_) skipElement(); else if (name() == "proto") readProto(); else if (name() == "field") readField(NULL, NULL); // TODO: top level field!!!! else skipElement(); } } currentStream_->mutable_core()->set_name(""); // FIXME // If trailing bytes are missing, add those from the pcap if ((expPos_ < pktBuf_.size()) && pcap_) { OstProto::Protocol *proto = currentStream_->add_protocol(); OstProto::HexDump *hexDump = proto->MutableExtension( OstProto::hexDump); proto->mutable_protocol_id()->set_id( OstProto::Protocol::kHexDumpFieldNumber); qDebug("adding trailing %d bytes starting from %d", pktBuf_.size() - expPos_, expPos_); hexDump->set_content(pktBuf_.constData() + expPos_, pktBuf_.size() - expPos_); hexDump->set_pad_until_end(false); } packetCount_++; emit progress(int(characterOffset()*100/device()->size())); // in % if (prevStream_) prevStream_->mutable_control()->CopyFrom(currentStream_->control()); if (stop_ && *stop_) raiseError("USER-CANCEL"); } void PdmlReader::readProto() { PdmlProtocol *pdmlProto = NULL; OstProto::Protocol *pbProto = NULL; Q_ASSERT(isStartElement() && name() == "proto"); QString protoName; int pos = -1; int size = -1; if (!attributes().value("name").isEmpty()) protoName = attributes().value("name").toString(); if (!attributes().value("pos").isEmpty()) pos = attributes().value("pos").toString().toInt(); if (!attributes().value("size").isEmpty()) size = attributes().value("size").toString().toInt(); qDebug("proto: %s, pos = %d, expPos_ = %d, size = %d", qPrintable(protoName), pos, expPos_, size); // This is a heuristic to skip protocols which are not part of // this frame, but of a reassembled segment spanning several frames // 1. Proto starting pos is 0, but we've already seen some protocols // 2. Protocol Size exceeds frame length if (((pos == 0) && (currentStream_->protocol_size() > 0)) || ((pos + size) > int(currentStream_->core().frame_len()))) { skipElement(); return; } if (isDontCareProto()) { skipElement(); return; } // if we detect a gap between subsequent protocols, we "fill-in" // with a "hexdump" from the pcap if (pos > expPos_ && pcap_) { appendHexDumpProto(expPos_, pos - expPos_); expPos_ = pos; } // for unknown protocol, read a hexdump from the pcap if (!factory_.contains(protoName) && pcap_) { int size = -1; if (!attributes().value("size").isEmpty()) size = attributes().value("size").toString().toInt(); // Check if this proto is a subset of previous proto - if so, do nothing if ((pos >= 0) && (size > 0) && ((pos + size) <= expPos_)) { qDebug("subset proto"); skipElement(); return; } if (pos >= 0 && size > 0 && ((pos + size) <= pktBuf_.size())) { appendHexDumpProto(pos, size); expPos_ += size; skipElement(); return; } } pdmlProto = appendPdmlProto(protoName, &pbProto); pdmlProto->setRecalculateCksum(recalculateCksums_); qDebug("%s: preProtocolHandler(expPos = %d)", qPrintable(protoName), expPos_); pdmlProto->preProtocolHandler(protoName, attributes(), expPos_, pbProto, currentStream_); while (!atEnd()) { readNext(); if (isEndElement()) break; if (isStartElement()) { if (name() == "proto") { // an embedded proto qDebug("embedded proto: %s\n", qPrintable(attributes().value("name").toString())); if (isDontCareProto()) { skipElement(); continue; } // if we are in the midst of processing a protocol, we // end it prematurely before we start processing the // embedded protocol // // XXX: pdmlProto may be NULL for a sequence of embedded protos if (pdmlProto) { int endPos = -1; if (!attributes().value("pos").isEmpty()) endPos = attributes().value("pos").toString().toInt(); pdmlProto->prematureEndHandler(endPos, pbProto, currentStream_); pdmlProto->postProtocolHandler(pbProto, currentStream_); StreamBase s; s.protoDataCopyFrom(*currentStream_); expPos_ = s.frameProtocolLength(0); } readProto(); pdmlProto = NULL; pbProto = NULL; } else if (name() == "field") { if ((protoName == "fake-field-wrapper") && (attributes().value("name") == "tcp.segments")) { skipElement(); qDebug("[skipping reassembled tcp segments]"); skipUntilEnd_ = true; continue; } if (pdmlProto == NULL) { pdmlProto = appendPdmlProto(protoName, &pbProto); qDebug("%s: preProtocolHandler(expPos = %d)", qPrintable(protoName), expPos_); pdmlProto->preProtocolHandler(protoName, attributes(), expPos_, pbProto, currentStream_); } readField(pdmlProto, pbProto); } else skipElement(); } } // Close-off current protocol if (pdmlProto) { pdmlProto->postProtocolHandler(pbProto, currentStream_); freePdmlProtocol(pdmlProto); StreamBase s; s.protoDataCopyFrom(*currentStream_); expPos_ = s.frameProtocolLength(0); } } void PdmlReader::readField(PdmlProtocol *pdmlProto, OstProto::Protocol *pbProto) { Q_ASSERT(isStartElement() && name() == "field"); // fields with "hide='yes'" are informational and should be skipped if (attributes().value("hide") == "yes") { skipElement(); return; } QString fieldName = attributes().value("name").toString(); qDebug(" fieldName:%s", qPrintable(fieldName)); pdmlProto->fieldHandler(fieldName, attributes(), pbProto, currentStream_); while (!atEnd()) { readNext(); if (isEndElement()) break; if (isStartElement()) { if (name() == "proto") { // Since we are in the midst of processing a protocol, we // end it prematurely before we start processing the // embedded protocol // int endPos = -1; if (!attributes().value("pos").isEmpty()) endPos = attributes().value("pos").toString().toInt(); pdmlProto->prematureEndHandler(endPos, pbProto, currentStream_); pdmlProto->postProtocolHandler(pbProto, currentStream_); StreamBase s; s.protoDataCopyFrom(*currentStream_); expPos_ = s.frameProtocolLength(0); readProto(); } else if (name() == "field") readField(pdmlProto, pbProto); else skipElement(); } } } void PdmlReader::appendHexDumpProto(int offset, int size) { OstProto::Protocol *proto = currentStream_->add_protocol(); OstProto::HexDump *hexDump = proto->MutableExtension(OstProto::hexDump); proto->mutable_protocol_id()->set_id( OstProto::Protocol::kHexDumpFieldNumber); qDebug("filling in gap of %d bytes starting from %d", size, offset); hexDump->set_content(pktBuf_.constData() + offset, size); hexDump->set_pad_until_end(false); } PdmlProtocol* PdmlReader::appendPdmlProto(const QString &protoName, OstProto::Protocol **pbProto) { PdmlProtocol* pdmlProto = allocPdmlProtocol(protoName); Q_ASSERT(pdmlProto != NULL); int protoId = pdmlProto->ostProtoId(); if (protoId > 0) // Non-Base Class { OstProto::Protocol *proto = currentStream_->add_protocol(); proto->mutable_protocol_id()->set_id(protoId); const google::protobuf::Reflection *msgRefl = proto->GetReflection(); const google::protobuf::FieldDescriptor *fieldDesc = msgRefl->FindKnownExtensionByNumber(protoId); // TODO: if !fDesc // init default values of all fields in protocol msgRefl->MutableMessage(proto, fieldDesc); *pbProto = proto; qDebug("%s: name = %s", __FUNCTION__, qPrintable(protoName)); } else *pbProto = NULL; return pdmlProto; } ostinato-1.3.0/common/pdmlreader.h000066400000000000000000000040371451413623100171370ustar00rootroot00000000000000/* Copyright (C) 2011 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _PDML_READER_H #define _PDML_READER_H #include "pdmlprotocol.h" #include #include #include class PcapFileFormat; class PdmlReader : public QObject, public QXmlStreamReader { Q_OBJECT public: PdmlReader(OstProto::StreamConfigList *streams, const QVariantMap &options = QVariantMap()); ~PdmlReader(); bool read(QIODevice *device, PcapFileFormat *pcap = NULL, bool *stop = NULL); signals: void progress(int value); private: PdmlProtocol* allocPdmlProtocol(QString protoName); void freePdmlProtocol(PdmlProtocol *proto); bool isDontCareProto(); void skipElement(); void readPdml(); void readPacket(); void readProto(); void readField(PdmlProtocol *pdmlProto, OstProto::Protocol *pbProto); void appendHexDumpProto(int offset, int size); PdmlProtocol* appendPdmlProto(const QString &protoName, OstProto::Protocol **pbProto); typedef PdmlProtocol* (*FactoryMethod)(); QMap factory_; bool *stop_; OstProto::StreamConfigList *streams_; PcapFileFormat *pcap_; QByteArray pktBuf_; bool recalculateCksums_{false}; bool isMldSupport_; int packetCount_; int expPos_; bool skipUntilEnd_; OstProto::Stream *prevStream_; OstProto::Stream *currentStream_; }; #endif ostinato-1.3.0/common/protocol.proto000066400000000000000000000242151451413623100175750ustar00rootroot00000000000000/* Copyright (C) 2010-2015 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ package OstProto; option cc_generic_services = true; option py_generic_services = true; message VersionInfo { required string version = 1; optional string client_name = 2; } message VersionCompatibility { enum Compatibility { kIncompatible = 0; kCompatible = 1; } required Compatibility result = 1; optional string notes = 2; } message StreamId { required uint32 id = 1; } message StreamCore { enum FrameLengthMode { e_fl_fixed = 0; e_fl_inc = 1; e_fl_dec = 2; e_fl_random = 3; e_fl_imix = 4; } // Basics optional string name = 1; optional bool is_enabled = 2; optional uint32 ordinal = 3; optional FrameLengthMode len_mode = 14 [default = e_fl_fixed]; /// Frame Length (includes CRC) optional uint32 frame_len = 15 [default = 64]; optional uint32 frame_len_min = 16 [default = 64]; optional uint32 frame_len_max = 17 [default = 1518]; } message StreamControl { enum SendUnit { e_su_packets = 0; e_su_bursts = 1; } enum SendMode { e_sm_fixed = 0; e_sm_continuous = 1; } enum NextWhat { e_nw_stop = 0; e_nw_goto_next = 1; e_nw_goto_id = 2; } optional SendUnit unit = 1 [default = e_su_packets]; optional SendMode mode = 2 [default = e_sm_fixed]; optional uint32 num_packets = 3 [default = 10]; optional uint32 num_bursts = 4 [default = 1]; optional uint32 packets_per_burst = 5 [default = 10]; optional NextWhat next = 6 [default = e_nw_goto_next]; optional uint32 OBSOLETE_packets_per_sec = 7 [default = 1, deprecated=true]; optional uint32 OBSOLETE_bursts_per_sec = 8 [default = 1, deprecated=true]; optional double packets_per_sec = 9 [default = 1]; optional double bursts_per_sec = 10 [default = 1]; } message ProtocolId { required uint32 id = 1; } message VariableField { enum Type { kCounter8 = 0; kCounter16 = 1; kCounter32 = 2; } enum Mode { kIncrement = 0; kDecrement = 1; kRandom = 2; } optional Type type = 1 [default = kCounter8]; optional uint32 offset = 2; optional fixed32 mask = 3 [default = 0xffffffff]; optional uint32 value = 4; optional Mode mode = 5 [default = kIncrement]; optional uint32 count = 6 [default = 16]; optional uint32 step = 7 [default = 1]; } message Protocol { required ProtocolId protocol_id = 1; repeated VariableField variable_field = 2; extensions 100 to 199; // Reserved for Ostinato Use extensions 200 to 500; // Available for use by protocols enum k { kMacFieldNumber = 100; kPayloadFieldNumber = 101; kSampleFieldNumber = 102; kUserScriptFieldNumber = 103; kHexDumpFieldNumber = 104; kSignFieldNumber = 105; kEth2FieldNumber = 200; kDot3FieldNumber = 201; kLlcFieldNumber = 202; kSnapFieldNumber = 203; kSvlanFieldNumber = 204; kVlanFieldNumber = 205; kDot2LlcFieldNumber = 206; kDot2SnapFieldNumber = 207; kVlanStackFieldNumber = 208; kStpFieldNumber = 209; kArpFieldNumber = 300; kIp4FieldNumber = 301; kIp6FieldNumber = 302; kIp6over4FieldNumber = 303; kIp4over6FieldNumber = 304; kIp4over4FieldNumber = 305; kIp6over6FieldNumber = 306; kTcpFieldNumber = 400; kUdpFieldNumber = 401; kIcmpFieldNumber = 402; kIgmpFieldNumber = 403; kMldFieldNumber = 404; kGreFieldNumber = 405; kTextProtocolFieldNumber = 500; } } message Stream { required StreamId stream_id = 1; optional StreamCore core = 2; optional StreamControl control = 3; repeated Protocol protocol = 4; } message Void { // nothing! } message Ack { enum RpcStatus { kRpcSuccess = 0; kRpcError = 1; } required RpcStatus status = 1; optional string notes = 2; } message PortId { required uint32 id = 1; } message PortIdList { repeated PortId port_id = 1; } message StreamIdList { required PortId port_id = 1; repeated StreamId stream_id = 2; } enum TransmitMode { kSequentialTransmit = 0; kInterleavedTransmit = 1; } message Port { required PortId port_id = 1; optional string name = 2; optional string description = 3; optional string notes = 4; optional bool is_enabled = 5; optional bool is_exclusive_control = 6; optional TransmitMode transmit_mode = 7 [default = kSequentialTransmit]; optional string user_name = 8; optional bool is_tracking_stream_stats = 9; optional double speed = 10; // in Mbps optional uint32 mtu = 11; optional string user_description = 12; } message PortConfigList { repeated Port port = 1; } message StreamConfigList { required PortId port_id = 1; repeated Stream stream = 2; } message CaptureBuffer { //! \todo (HIGH) define CaptureBuffer } message CaptureBufferList { repeated CaptureBuffer list = 1; } enum LinkState { LinkStateUnknown = 0; LinkStateDown = 1; LinkStateUp = 2; } message PortState { optional LinkState link_state = 1 [default = LinkStateUnknown]; optional bool is_transmit_on = 2 [default = false]; optional bool is_capture_on = 3 [default = false]; } message PortStats { required PortId port_id = 1; optional PortState state = 2; optional uint64 rx_pkts = 11; optional uint64 rx_bytes = 12; optional uint64 rx_pkts_nic = 13; optional uint64 rx_bytes_nic = 14; optional uint64 rx_pps = 15; optional uint64 rx_bps = 16; optional uint64 tx_pkts = 21; optional uint64 tx_bytes = 22; optional uint64 tx_pkts_nic = 23; optional uint64 tx_bytes_nic = 24; optional uint64 tx_pps = 25; optional uint64 tx_bps = 26; optional uint64 rx_drops = 100; optional uint64 rx_errors = 101; optional uint64 rx_fifo_errors = 102; optional uint64 rx_frame_errors = 103; } message PortStatsList { repeated PortStats port_stats = 1; } message StreamGuid { required uint32 id = 1; } message StreamGuidList { required PortIdList port_id_list = 1; repeated StreamGuid stream_guid = 2; } message StreamStats { required PortId port_id = 1; required StreamGuid stream_guid = 2; optional double tx_duration = 3; // in seconds optional uint64 latency = 4; // in nanoseconds optional uint64 jitter = 5; // in nanoseconds optional uint64 rx_pkts = 11; optional uint64 rx_bytes = 12; optional uint64 tx_pkts = 13; optional uint64 tx_bytes = 14; } message StreamStatsList { repeated StreamStats stream_stats = 1; } enum NotifType { portConfigChanged = 1; } message Notification { required NotifType notif_type = 1; optional PortIdList port_id_list = 6; } message BuildConfig { required PortId port_id = 1; } /* * Protocol Emulation */ message DeviceGroupId { required uint32 id = 1; } message DeviceGroupCore { optional string name = 1; } message DeviceGroupIdList { required PortId port_id = 1; repeated DeviceGroupId device_group_id = 2; } message EncapEmulation { // Encap Protocols implemented as extensions extensions 1000 to 1999; } message DeviceGroup { required DeviceGroupId device_group_id = 1; optional DeviceGroupCore core = 2; optional EncapEmulation encap = 3; optional uint32 device_count = 4 [default = 1]; // per-encap // Device Protocols implemented as extensions extensions 2000 to 5999; } message DeviceGroupConfigList { required PortId port_id = 1; repeated DeviceGroup device_group = 2; } message PortDeviceList { required PortId port_id = 1; extensions 100 to 199; } message PortNeighborList { required PortId port_id = 1; extensions 100 to 199; } service OstService { rpc getPortIdList(Void) returns (PortIdList); rpc getPortConfig(PortIdList) returns (PortConfigList); rpc modifyPort(PortConfigList) returns (Ack); rpc getStreamIdList(PortId) returns (StreamIdList); rpc getStreamConfig(StreamIdList) returns (StreamConfigList); rpc addStream(StreamIdList) returns (Ack); rpc deleteStream(StreamIdList) returns (Ack); rpc modifyStream(StreamConfigList) returns (Ack); rpc startTransmit(PortIdList) returns (Ack); rpc stopTransmit(PortIdList) returns (Ack); rpc startCapture(PortIdList) returns (Ack); rpc stopCapture(PortIdList) returns (Ack); rpc getCaptureBuffer(PortId) returns (CaptureBuffer); rpc getStats(PortIdList) returns (PortStatsList); rpc clearStats(PortIdList) returns (Ack); rpc checkVersion(VersionInfo) returns (VersionCompatibility); // Device Emulation rpc getDeviceGroupIdList(PortId) returns (DeviceGroupIdList); rpc getDeviceGroupConfig(DeviceGroupIdList) returns (DeviceGroupConfigList); rpc addDeviceGroup(DeviceGroupIdList) returns (Ack); rpc deleteDeviceGroup(DeviceGroupIdList) returns (Ack); rpc modifyDeviceGroup(DeviceGroupConfigList) returns (Ack); rpc getDeviceList(PortId) returns (PortDeviceList); rpc resolveDeviceNeighbors(PortIdList) returns (Ack); rpc clearDeviceNeighbors(PortIdList) returns (Ack); rpc getDeviceNeighbors(PortId) returns (PortNeighborList); // Stream Stats rpc getStreamStats(StreamGuidList) returns (StreamStatsList); rpc clearStreamStats(StreamGuidList) returns (Ack); rpc build(BuildConfig) returns (Ack); // XXX: Add new RPCs at the end only to preserve backward compatibility } ostinato-1.3.0/common/protocollist.cpp000066400000000000000000000014611451413623100201060ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #include "protocollist.h" #include "abstractprotocol.h" void ProtocolList::destroy() { while (!isEmpty()) delete takeFirst(); } ostinato-1.3.0/common/protocollist.h000066400000000000000000000014571451413623100175600ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #include class AbstractProtocol; class ProtocolList : public QLinkedList { public: void destroy(); }; ostinato-1.3.0/common/protocollistiterator.cpp000066400000000000000000000060441451413623100216620ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #include "protocollistiterator.h" #include "protocollist.h" #include "abstractprotocol.h" ProtocolListIterator::ProtocolListIterator(ProtocolList &list) { _iter = new QMutableLinkedListIterator(list); } ProtocolListIterator::~ProtocolListIterator() { delete _iter; } bool ProtocolListIterator::findNext(const AbstractProtocol* value) const { return _iter->findNext(const_cast(value)); } bool ProtocolListIterator::findPrevious(const AbstractProtocol* value) { return _iter->findPrevious(const_cast(value)); } bool ProtocolListIterator::hasNext() const { return _iter->hasNext(); } bool ProtocolListIterator::hasPrevious() const { return _iter->hasPrevious(); } void ProtocolListIterator::insert(AbstractProtocol* value) { if (_iter->hasPrevious()) { value->prev = _iter->peekPrevious(); value->prev->next = value; } else value->prev = NULL; if (_iter->hasNext()) { value->next = _iter->peekNext(); value->next->prev = value; } else value->next = NULL; _iter->insert(const_cast(value)); } AbstractProtocol* ProtocolListIterator::next() { return _iter->next(); } AbstractProtocol* ProtocolListIterator::peekNext() const { return _iter->peekNext(); } AbstractProtocol* ProtocolListIterator::peekPrevious() const { return _iter->peekPrevious(); } AbstractProtocol* ProtocolListIterator::previous() { return _iter->previous(); } void ProtocolListIterator::remove() { if (_iter->value()->prev) _iter->value()->prev->next = _iter->value()->next; if (_iter->value()->next) _iter->value()->next->prev = _iter->value()->prev; _iter->remove(); } void ProtocolListIterator::setValue(AbstractProtocol* value) const { if (_iter->value()->prev) _iter->value()->prev->next = value; if (_iter->value()->next) _iter->value()->next->prev = value; value->prev = _iter->value()->prev; value->next = _iter->value()->next; _iter->setValue(const_cast(value)); } void ProtocolListIterator::toBack() { _iter->toBack(); } void ProtocolListIterator::toFront() { _iter->toFront(); } const AbstractProtocol* ProtocolListIterator::value() const { return _iter->value(); } AbstractProtocol* ProtocolListIterator::value() { return _iter->value(); } ostinato-1.3.0/common/protocollistiterator.h000066400000000000000000000027101451413623100213230ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #include class AbstractProtocol; class ProtocolList; class ProtocolListIterator { private: QMutableLinkedListIterator *_iter; public: ProtocolListIterator(ProtocolList &list); ~ProtocolListIterator(); bool findNext(const AbstractProtocol* value) const; bool findPrevious(const AbstractProtocol* value); bool hasNext() const; bool hasPrevious() const; void insert(AbstractProtocol* value); AbstractProtocol* next(); AbstractProtocol* peekNext() const; AbstractProtocol* peekPrevious() const; AbstractProtocol* previous(); void remove(); void setValue(AbstractProtocol* value) const; void toBack(); void toFront(); const AbstractProtocol* value() const; AbstractProtocol* value(); }; ostinato-1.3.0/common/protocolmanager.cpp000066400000000000000000000210501451413623100205410ustar00rootroot00000000000000/* Copyright (C) 2010, 2014 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #include "protocolmanager.h" #include "abstractprotocol.h" #include "protocol.pb.h" #include "mac.h" #include "vlan.h" #include "svlan.h" #include "vlanstack.h" // L2 Protos #include "dot3.h" #include "llc.h" #include "dot2llc.h" #include "snap.h" #include "dot2snap.h" #include "eth2.h" #include "stp.h" // L3 Protos #include "arp.h" #include "ip4.h" #include "ip6.h" #include "ip4over4.h" #include "ip4over6.h" #include "ip6over4.h" #include "ip6over6.h" // L4 Protos #include "gre.h" #include "icmp.h" #include "igmp.h" #include "mld.h" #include "tcp.h" #include "udp.h" // L5 Protos #include "textproto.h" // Special Protos #include "hexdump.h" #include "payload.h" #include "sample.h" #include "sign.h" #include "userscript.h" ProtocolManager *OstProtocolManager; ProtocolManager::ProtocolManager() { /*! \todo (LOW) calls to registerProtocol() should be done by the protocols themselves (once this is done remove the #includes for all the protocols) */ registerProtocol(OstProto::Protocol::kMacFieldNumber, (void*) MacProtocol::createInstance); registerProtocol(OstProto::Protocol::kVlanFieldNumber, (void*) VlanProtocol::createInstance); registerProtocol(OstProto::Protocol::kSvlanFieldNumber, (void*) SVlanProtocol::createInstance); registerProtocol(OstProto::Protocol::kVlanStackFieldNumber, (void*) VlanStackProtocol::createInstance); registerProtocol(OstProto::Protocol::kEth2FieldNumber, (void*) Eth2Protocol::createInstance); registerProtocol(OstProto::Protocol::kDot3FieldNumber, (void*) Dot3Protocol::createInstance); registerProtocol(OstProto::Protocol::kLlcFieldNumber, (void*) LlcProtocol::createInstance); registerProtocol(OstProto::Protocol::kDot2LlcFieldNumber, (void*) Dot2LlcProtocol::createInstance); registerProtocol(OstProto::Protocol::kSnapFieldNumber, (void*) SnapProtocol::createInstance); registerProtocol(OstProto::Protocol::kDot2SnapFieldNumber, (void*) Dot2SnapProtocol::createInstance); registerProtocol(OstProto::Protocol::kStpFieldNumber, (void*) StpProtocol::createInstance); // Layer 3 Protocols registerProtocol(OstProto::Protocol::kArpFieldNumber, (void*) ArpProtocol::createInstance); registerProtocol(OstProto::Protocol::kIp4FieldNumber, (void*) Ip4Protocol::createInstance); registerProtocol(OstProto::Protocol::kIp6FieldNumber, (void*) Ip6Protocol::createInstance); registerProtocol(OstProto::Protocol::kIp4over4FieldNumber, (void*) Ip4over4Protocol::createInstance); registerProtocol(OstProto::Protocol::kIp4over6FieldNumber, (void*) Ip4over6Protocol::createInstance); registerProtocol(OstProto::Protocol::kIp6over4FieldNumber, (void*) Ip6over4Protocol::createInstance); registerProtocol(OstProto::Protocol::kIp6over6FieldNumber, (void*) Ip6over6Protocol::createInstance); // Layer 4 Protocols registerProtocol(OstProto::Protocol::kGreFieldNumber, (void*) GreProtocol::createInstance); registerProtocol(OstProto::Protocol::kIcmpFieldNumber, (void*) IcmpProtocol::createInstance); registerProtocol(OstProto::Protocol::kIgmpFieldNumber, (void*) IgmpProtocol::createInstance); registerProtocol(OstProto::Protocol::kMldFieldNumber, (void*) MldProtocol::createInstance); registerProtocol(OstProto::Protocol::kTcpFieldNumber, (void*) TcpProtocol::createInstance); registerProtocol(OstProto::Protocol::kUdpFieldNumber, (void*) UdpProtocol::createInstance); // Layer 5 Protocols registerProtocol(OstProto::Protocol::kTextProtocolFieldNumber, (void*) TextProtocol::createInstance); // Special Protocols registerProtocol(OstProto::Protocol::kHexDumpFieldNumber, (void*) HexDumpProtocol::createInstance); registerProtocol(OstProto::Protocol::kPayloadFieldNumber, (void*) PayloadProtocol::createInstance); registerProtocol(OstProto::Protocol::kSampleFieldNumber, (void*) SampleProtocol::createInstance); registerProtocol(OstProto::Protocol::kSignFieldNumber, (void*) SignProtocol::createInstance); registerProtocol(OstProto::Protocol::kUserScriptFieldNumber, (void*) UserScriptProtocol::createInstance); populateNeighbourProtocols(); } ProtocolManager::~ProtocolManager() { numberToNameMap.clear(); nameToNumberMap.clear(); neighbourProtocols.clear(); factory.clear(); QList pl = protocolList.values(); while (!pl.isEmpty()) delete pl.takeFirst(); } void ProtocolManager::registerProtocol(int protoNumber, void *protoInstanceCreator) { AbstractProtocol *p; Q_ASSERT(!factory.contains(protoNumber)); factory.insert(protoNumber, protoInstanceCreator); p = createProtocol(protoNumber, NULL); protocolList.insert(protoNumber, p); numberToNameMap.insert(protoNumber, p->shortName()); nameToNumberMap.insert(p->shortName(), protoNumber); } void ProtocolManager::populateNeighbourProtocols() { neighbourProtocols.clear(); foreach(AbstractProtocol *p, protocolList) { if (p->protocolIdType() != AbstractProtocol::ProtocolIdNone) { foreach(AbstractProtocol *q, protocolList) { if (q->protocolId(p->protocolIdType())) neighbourProtocols.insert( p->protocolNumber(), q->protocolNumber()); } } } } bool ProtocolManager::isRegisteredProtocol(int protoNumber) { return factory.contains(protoNumber); } AbstractProtocol* ProtocolManager::createProtocol(int protoNumber, StreamBase *stream, AbstractProtocol *parent) { AbstractProtocol* (*pc)(StreamBase*, AbstractProtocol*); AbstractProtocol* p; pc = (AbstractProtocol* (*)(StreamBase*, AbstractProtocol*)) factory.value(protoNumber); Q_ASSERT_X(pc != NULL, __FUNCTION__, qPrintable(QString("No Protocol Creator registered for protocol %1") .arg(protoNumber))); p = (*pc)(stream, parent); return p; } AbstractProtocol* ProtocolManager::createProtocol(QString protoName, StreamBase *stream, AbstractProtocol *parent) { return createProtocol(nameToNumberMap.value(protoName), stream, parent); } bool ProtocolManager::isValidNeighbour(int protoPrefix, int protoSuffix) { if (neighbourProtocols.contains(protoPrefix, protoSuffix)) return true; else return false; } bool ProtocolManager::protocolHasPayload(int protoNumber) { Q_ASSERT(protocolList.contains(protoNumber)); return protocolList.value(protoNumber)->protocolHasPayload(); } QStringList ProtocolManager::protocolDatabase() { return numberToNameMap.values(); } #if 0 void ProtocolManager::showFieldAttribs() { QStringList protocolList = protocolDatabase(); Stream stream; foreach(QString name, protocolList) { if (name.contains("/")) // assume combo continue; AbstractProtocol *protocol = OstProtocolManager->createProtocol(name, &stream); int count = protocol->fieldCount(); for (int i = 0; i < count; i++) { if (!protocol->fieldFlags(i).testFlag(AbstractProtocol::FrameField)) continue; uint bitSize = protocol->fieldData(i, AbstractProtocol::FieldBitSize) .toInt(); qDebug("$$$$, %s, %d, %s, %u, %x, %llu", qPrintable(name), i, qPrintable(protocol->fieldData(i, AbstractProtocol::FieldName).toString()), bitSize, 0, // min (1 << bitSize) - 1); } delete protocol; } } #endif ostinato-1.3.0/common/protocolmanager.h000066400000000000000000000032371451413623100202150ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _PROTOCOL_MANAGER_H #define _PROTOCOL_MANAGER_H #include #include class AbstractProtocol; class StreamBase; class ProtocolManager { QMap numberToNameMap; QMap nameToNumberMap; QMultiMap neighbourProtocols; QMap factory; QMap protocolList; void populateNeighbourProtocols(); public: ProtocolManager(); ~ProtocolManager(); // TODO: make registerProtocol static void registerProtocol(int protoNumber, void *protoInstanceCreator); bool isRegisteredProtocol(int protoNumber); AbstractProtocol* createProtocol(int protoNumber, StreamBase *stream, AbstractProtocol *parent = 0); AbstractProtocol* createProtocol(QString protoName, StreamBase *stream, AbstractProtocol *parent = 0); bool isValidNeighbour(int protoPrefix, int protoSuffix); bool protocolHasPayload(int protoNumber); QStringList protocolDatabase(); }; #endif ostinato-1.3.0/common/protocolwidgetfactory.cpp000066400000000000000000000172631451413623100220150ustar00rootroot00000000000000/* Copyright (C) 2014 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #include "protocolwidgetfactory.h" #include "macconfig.h" #include "vlanconfig.h" #include "svlanconfig.h" #include "vlanstackconfig.h" // L2 Protocol Widgets #include "eth2config.h" #include "dot3config.h" #include "llcconfig.h" #include "dot2llcconfig.h" #include "snapconfig.h" #include "dot2snapconfig.h" #include "stpconfig.h" // L3 Protocol Widgets #include "arpconfig.h" #include "ip4config.h" #include "ip6config.h" #include "ip4over4config.h" #include "ip4over6config.h" #include "ip6over4config.h" #include "ip6over6config.h" // L4 Protocol Widgets #include "greconfig.h" #include "icmpconfig.h" #include "igmpconfig.h" #include "mldconfig.h" #include "tcpconfig.h" #include "udpconfig.h" // L5 Protocol Widgets #include "textprotoconfig.h" // Special Protocol Widgets #include "hexdumpconfig.h" #include "payloadconfig.h" #include "sampleconfig.h" #include "signconfig.h" #include "userscriptconfig.h" ProtocolWidgetFactory *OstProtocolWidgetFactory; QMap ProtocolWidgetFactory::configWidgetFactory; ProtocolWidgetFactory::ProtocolWidgetFactory() { /*! * Ideally Protocol Widgets should register themselves * with the Factory */ OstProtocolWidgetFactory->registerProtocolConfigWidget( OstProto::Protocol::kMacFieldNumber, (void*) MacConfigForm::createInstance); OstProtocolWidgetFactory->registerProtocolConfigWidget( OstProto::Protocol::kVlanFieldNumber, (void*) VlanConfigForm::createInstance); OstProtocolWidgetFactory->registerProtocolConfigWidget( OstProto::Protocol::kSvlanFieldNumber, (void*) SVlanConfigForm::createInstance); OstProtocolWidgetFactory->registerProtocolConfigWidget( OstProto::Protocol::kVlanStackFieldNumber, (void*) VlanStackConfigForm::createInstance); OstProtocolWidgetFactory->registerProtocolConfigWidget( OstProto::Protocol::kEth2FieldNumber, (void*) Eth2ConfigForm::createInstance); OstProtocolWidgetFactory->registerProtocolConfigWidget( OstProto::Protocol::kDot3FieldNumber, (void*) Dot3ConfigForm::createInstance); OstProtocolWidgetFactory->registerProtocolConfigWidget( OstProto::Protocol::kLlcFieldNumber, (void*) LlcConfigForm::createInstance); OstProtocolWidgetFactory->registerProtocolConfigWidget( OstProto::Protocol::kDot2LlcFieldNumber, (void*) Dot2LlcConfigForm::createInstance); OstProtocolWidgetFactory->registerProtocolConfigWidget( OstProto::Protocol::kSnapFieldNumber, (void*) SnapConfigForm::createInstance); OstProtocolWidgetFactory->registerProtocolConfigWidget( OstProto::Protocol::kDot2SnapFieldNumber, (void*) Dot2SnapConfigForm::createInstance); OstProtocolWidgetFactory->registerProtocolConfigWidget( OstProto::Protocol::kStpFieldNumber, (void*) StpConfigForm::createInstance); // Layer 3 Protocols OstProtocolWidgetFactory->registerProtocolConfigWidget( OstProto::Protocol::kArpFieldNumber, (void*) ArpConfigForm::createInstance); OstProtocolWidgetFactory->registerProtocolConfigWidget( OstProto::Protocol::kIp4FieldNumber, (void*) Ip4ConfigForm::createInstance); OstProtocolWidgetFactory->registerProtocolConfigWidget( OstProto::Protocol::kIp6FieldNumber, (void*) Ip6ConfigForm::createInstance); OstProtocolWidgetFactory->registerProtocolConfigWidget( OstProto::Protocol::kIp4over4FieldNumber, (void*) Ip4over4ConfigForm::createInstance); OstProtocolWidgetFactory->registerProtocolConfigWidget( OstProto::Protocol::kIp4over6FieldNumber, (void*) Ip4over6ConfigForm::createInstance); OstProtocolWidgetFactory->registerProtocolConfigWidget( OstProto::Protocol::kIp6over4FieldNumber, (void*) Ip6over4ConfigForm::createInstance); OstProtocolWidgetFactory->registerProtocolConfigWidget( OstProto::Protocol::kIp6over6FieldNumber, (void*) Ip6over6ConfigForm::createInstance); // Layer 4 Protocols OstProtocolWidgetFactory->registerProtocolConfigWidget( OstProto::Protocol::kGreFieldNumber, (void*) GreConfigForm::createInstance); OstProtocolWidgetFactory->registerProtocolConfigWidget( OstProto::Protocol::kIcmpFieldNumber, (void*) IcmpConfigForm::createInstance); OstProtocolWidgetFactory->registerProtocolConfigWidget( OstProto::Protocol::kIgmpFieldNumber, (void*) IgmpConfigForm::createInstance); OstProtocolWidgetFactory->registerProtocolConfigWidget( OstProto::Protocol::kMldFieldNumber, (void*) MldConfigForm::createInstance); OstProtocolWidgetFactory->registerProtocolConfigWidget( OstProto::Protocol::kTcpFieldNumber, (void*) TcpConfigForm::createInstance); OstProtocolWidgetFactory->registerProtocolConfigWidget( OstProto::Protocol::kUdpFieldNumber, (void*) UdpConfigForm::createInstance); // Layer 5 Protocols OstProtocolWidgetFactory->registerProtocolConfigWidget( OstProto::Protocol::kTextProtocolFieldNumber, (void*) TextProtocolConfigForm::createInstance); // Special Protocols OstProtocolWidgetFactory->registerProtocolConfigWidget( OstProto::Protocol::kHexDumpFieldNumber, (void*) HexDumpConfigForm::createInstance); OstProtocolWidgetFactory->registerProtocolConfigWidget( OstProto::Protocol::kPayloadFieldNumber, (void*) PayloadConfigForm::createInstance); OstProtocolWidgetFactory->registerProtocolConfigWidget( OstProto::Protocol::kSampleFieldNumber, (void*) SampleConfigForm::createInstance); OstProtocolWidgetFactory->registerProtocolConfigWidget( OstProto::Protocol::kSignFieldNumber, (void*) SignConfigForm::createInstance); OstProtocolWidgetFactory->registerProtocolConfigWidget( OstProto::Protocol::kUserScriptFieldNumber, (void*) UserScriptConfigForm::createInstance); } ProtocolWidgetFactory::~ProtocolWidgetFactory() { configWidgetFactory.clear(); } void ProtocolWidgetFactory::registerProtocolConfigWidget(int protoNumber, void *protoConfigWidgetInstanceCreator) { Q_ASSERT(!configWidgetFactory.contains(protoNumber)); configWidgetFactory.insert(protoNumber, protoConfigWidgetInstanceCreator); } AbstractProtocolConfigForm* ProtocolWidgetFactory::createConfigWidget( int protoNumber) { AbstractProtocolConfigForm* (*pc)(); AbstractProtocolConfigForm* p; pc = (AbstractProtocolConfigForm* (*)()) configWidgetFactory.value(protoNumber); Q_ASSERT_X(pc != NULL, __FUNCTION__, qPrintable(QString(protoNumber))); p = (*pc)(); return p; } void ProtocolWidgetFactory::deleteConfigWidget( AbstractProtocolConfigForm *configWidget) { delete configWidget; } ostinato-1.3.0/common/protocolwidgetfactory.h000066400000000000000000000025771451413623100214640ustar00rootroot00000000000000/* Copyright (C) 2014 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _PROTOCOL_WIDGET_FACTORY_H #define _PROTOCOL_WIDGET_FACTORY_H #include class AbstractProtocolConfigForm; // Singleton class class ProtocolWidgetFactory { static QMap configWidgetFactory; public: ProtocolWidgetFactory(); ~ProtocolWidgetFactory(); // TODO: make registerProtocolConfigWidget static?? // TODO: define a function pointer prototype instead of void* for // protoConfigWidgetInstanceCreator static void registerProtocolConfigWidget(int protoNumber, void *protoConfigWidgetInstanceCreator); AbstractProtocolConfigForm* createConfigWidget(int protoNumber); void deleteConfigWidget(AbstractProtocolConfigForm *configWidget); }; #endif ostinato-1.3.0/common/pythonfileformat.cpp000066400000000000000000000513331451413623100207460ustar00rootroot00000000000000/* Copyright (C) 2015 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #include "pythonfileformat.h" #include #include #include #include #include using google::protobuf::Message; using google::protobuf::Reflection; using google::protobuf::FieldDescriptor; PythonFileFormat pythonFileFormat; extern char *version; extern char *revision; PythonFileFormat::PythonFileFormat() { // Nothing to do } PythonFileFormat::~PythonFileFormat() { // Nothing to do } bool PythonFileFormat::open(const QString /*fileName*/, OstProto::StreamConfigList &/*streams*/, QString &/*error*/) { // NOT SUPPORTED! return false; } bool PythonFileFormat::save(const OstProto::StreamConfigList streams, const QString fileName, QString &error) { QFile file(fileName); QTextStream out(&file); QSet imports; if (!file.open(QIODevice::WriteOnly | QIODevice::Truncate)) goto _open_fail; out.setCodec("UTF-8"); // import standard modules emit status("Writing imports ..."); emit target(0); writeStandardImports(out); emit target(streams.stream_size()); // import protocols from respective modules // build the import list using a QSet to eliminate duplicates for (int i = 0; i < streams.stream_size(); i++) { const OstProto::Stream &stream = streams.stream(i); for (int j = 0 ; j < stream.protocol_size(); j++) { const OstProto::Protocol &protocol = stream.protocol(j); const Reflection *refl = protocol.GetReflection(); std::vector fields; refl->ListFields(protocol, &fields); for (uint k = 0; k < fields.size(); k++) { // skip non extension fields if (!fields.at(k)->is_extension()) continue; if (fields.at(k)->file()->name() != fields.at(k)->message_type()->file()->name()) { imports.insert( QString("%1 import %2").arg( QString(fields.at(k)->message_type() ->file()->name().c_str()) .replace(".proto", "_pb2"), fields.at(k)->message_type()->name().c_str())); imports.insert( QString("%1 import %2").arg( QString(fields.at(k) ->file()->name().c_str()) .replace(".proto", "_pb2"), fields.at(k)->name().c_str())); } else { imports.insert( QString("%1 import %2, %3").arg( QString(fields.at(k)->file()->name().c_str()) .replace(".proto", "_pb2"), fields.at(k)->message_type()->name().c_str(), fields.at(k)->name().c_str())); } } } emit progress(i); } // write the import statements out << "# import ostinato modules\n"; out << "from ostinato.core import DroneProxy, ost_pb\n"; foreach (QString str, imports) out << "from ostinato.protocols." << str << "\n"; out << "\n"; // start of script - init, connect to drone etc. emit status("Writing prologue ..."); emit target(0); writePrologue(out); // Add streams emit status("Writing stream adds ..."); emit target(streams.stream_size()); out << " # ------------#\n"; out << " # add streams #\n"; out << " # ------------#\n"; out << " stream_id = ost_pb.StreamIdList()\n"; out << " stream_id.port_id.id = tx_port_number\n"; for (int i = 0; i < streams.stream_size(); i++) { out << " stream_id.stream_id.add().id = " << streams.stream(i).stream_id().id() << "\n"; emit progress(i); } out << " drone.addStream(stream_id)\n"; out << "\n"; // Configure streams with actual values emit status("Writing stream configuration ..."); emit target(streams.stream_size()); out << " # ------------------#\n"; out << " # configure streams #\n"; out << " # ------------------#\n"; out << " stream_cfg = ost_pb.StreamConfigList()\n"; out << " stream_cfg.port_id.id = tx_port_number\n"; for (int i = 0; i < streams.stream_size(); i++) { const OstProto::Stream &stream = streams.stream(i); const Reflection *refl; std::vector fields; out << "\n"; out << " # stream " << stream.stream_id().id() << " " << stream.core().name().c_str() << "\n"; out << " s = stream_cfg.stream.add()\n"; out << " s.stream_id.id = " << stream.stream_id().id() << "\n"; // Stream Core values refl = stream.core().GetReflection(); refl->ListFields(stream.core(), &fields); for (uint j = 0; j < fields.size(); j++) { writeFieldAssignment(out, QString(" s.core.") .append(fields.at(j)->name().c_str()), stream.core(), refl, fields.at(j)); } // Stream Control values refl = stream.control().GetReflection(); refl->ListFields(stream.control(), &fields); for (uint j = 0; j < fields.size(); j++) { writeFieldAssignment(out, QString(" s.control.") .append(fields.at(j)->name().c_str()), stream.control(), refl, fields.at(j)); } // Protocols for (int j = 0 ; j < stream.protocol_size(); j++) { const OstProto::Protocol &protocol = stream.protocol(j); out << "\n" << " p = s.protocol.add()\n" << " p.protocol_id.id = " << QString(OstProto::Protocol_k_descriptor() ->FindValueByNumber(protocol.protocol_id().id()) ->full_name().c_str()) .replace("OstProto", "ost_pb"); out << "\n"; refl = protocol.GetReflection(); refl->ListFields(protocol, &fields); for (uint k = 0; k < fields.size(); k++) { // skip protocol_id field if (fields.at(k)->number() == OstProto::Protocol::kProtocolIdFieldNumber) continue; QString pfx(" p.Extensions[X]"); pfx.replace(fields.at(k)->is_extension()? "X": "Extensions[X]", fields.at(k)->name().c_str()); writeFieldAssignment(out, pfx, protocol, refl, fields.at(k)); } } emit progress(i); } out << "\n"; out << " drone.modifyStream(stream_cfg)\n"; // end of script - transmit streams, disconnect from drone etc. emit status("Writing epilogue ..."); emit target(0); writeEpilogue(out); out.flush(); file.close(); return true; _open_fail: error = QString(tr("Error opening %1 (Error Code = %2)")) .arg(fileName) .arg(file.error()); return false; } bool PythonFileFormat::isMyFileFormat(const QString /*fileName*/) { // isMyFileFormat() is used for file open case to detect // file format - Open not supported for Python Scripts return false; } bool PythonFileFormat::isMyFileType(const QString fileType) { if (fileType.startsWith("PythonScript")) return true; else return false; } // // Private Member Functions // void PythonFileFormat::writeStandardImports(QTextStream &out) { out << "#! /usr/bin/env python\n"; out << "\n"; out << "# This script was programmatically generated\n" << "# by Ostinato version " << version << " revision " << revision << "\n" << "# The script should work out of the box mostly,\n" << "# but occassionally might need minor tweaking\n" << "# Please report any bugs at https://ostinato.org\n"; out << "\n"; out << "# standard modules\n"; out << "import logging\n"; out << "import os\n"; out << "import sys\n"; out << "import time\n"; out << "\n"; } void PythonFileFormat::writePrologue(QTextStream &out) { out << "# initialize the below variables appropriately " << "to avoid manual input\n"; out << "host_name = ''\n"; out << "tx_port_number = -1\n"; out << "\n"; out << "# setup logging\n"; out << "log = logging.getLogger(__name__)\n"; out << "logging.basicConfig(level=logging.INFO)\n"; out << "\n"; out << "# get inputs, if required\n"; out << "while len(host_name) == 0:\n"; out << " host_name = input('Drone\\'s Hostname/IP: ')\n"; out << "while tx_port_number < 0:\n"; out << " tx_port_number = int(input('Tx Port Number: '))\n"; out << "\n"; out << "drone = DroneProxy(host_name)\n"; out << "\n"; out << "try:\n"; out << " # connect to drone\n"; out << " log.info('connecting to drone(%s:%d)' \n"; out << " % (drone.hostName(), drone.portNumber()))\n"; out << " drone.connect()\n"; out << "\n"; out << " # setup tx port list\n"; out << " tx_port = ost_pb.PortIdList()\n"; out << " tx_port.port_id.add().id = tx_port_number;\n"; out << "\n"; } void PythonFileFormat::writeEpilogue(QTextStream &out) { out << " # clear tx/rx stats\n"; out << " log.info('clearing tx stats')\n"; out << " drone.clearStats(tx_port)\n"; out << "\n"; out << " log.info('starting transmit')\n"; out << " drone.startTransmit(tx_port)\n"; out << "\n"; out << " # wait for transmit to finish\n"; out << " log.info('waiting for transmit to finish ...')\n"; out << " while True:\n"; out << " try:\n"; out << " time.sleep(5)\n"; out << " tx_stats = drone.getStats(tx_port)\n"; out << " if tx_stats.port_stats[0].state.is_transmit_on" " == False:\n"; out << " break\n"; out << " except KeyboardInterrupt:\n"; out << " log.info('transmit interrupted by user')\n"; out << " break\n"; out << "\n"; out << " # stop transmit and capture\n"; out << " log.info('stopping transmit')\n"; out << " drone.stopTransmit(tx_port)\n"; out << "\n"; out << " # get tx stats\n"; out << " log.info('retreiving stats')\n"; out << " tx_stats = drone.getStats(tx_port)\n"; out << "\n"; out << " log.info('tx pkts = %d' % (tx_stats.port_stats[0].tx_pkts))\n"; out << "\n"; out << " # delete streams\n"; out << " log.info('deleting tx_streams')\n"; out << " drone.deleteStream(stream_id)\n"; out << "\n"; out << " # bye for now\n"; out << " drone.disconnect()\n"; out << "\n"; out << "except Exception as ex:\n"; out << " log.exception(ex)\n"; out << " sys.exit(1)\n"; } void PythonFileFormat::writeFieldAssignment( QTextStream &out, QString fieldName, const Message &msg, const Reflection *refl, const FieldDescriptor *fieldDesc, int index) { // for a repeated field, // if index < 0 => we are writing a repeated aggregate // if index >= 0 => we are writing a repeated element if (fieldDesc->is_repeated() && (index < 0)) { int n = refl->FieldSize(msg, fieldDesc); QString var = singularize(fieldDesc->name().c_str()); for (int i = 0; i < n; i++) { out << " " << var << " = " << fieldName.trimmed() << ".add()\n"; writeFieldAssignment(out, QString(" ").append(var), msg, refl, fieldDesc, i); } return; } // Ideally fields should not be set if they have the same // value as the default value - but currently protocols don't // check this when setting values in the protobuf data object // so here we check that explicitly for each field and if true // we don't output anything switch(fieldDesc->cpp_type()) { case FieldDescriptor::CPPTYPE_INT32: { qint32 val = fieldDesc->is_repeated() ? refl->GetRepeatedInt32(msg, fieldDesc, index) : refl->GetInt32(msg, fieldDesc); if (val != fieldDesc->default_value_int32()) out << fieldName << " = " << val << "\n"; break; } case FieldDescriptor::CPPTYPE_INT64: { qint64 val = fieldDesc->is_repeated() ? refl->GetRepeatedInt64(msg, fieldDesc, index) : refl->GetInt64(msg, fieldDesc); if (val != fieldDesc->default_value_int64()) out << fieldName << " = " << val << "\n"; break; } case FieldDescriptor::CPPTYPE_UINT32: { quint32 val = fieldDesc->is_repeated() ? refl->GetRepeatedUInt32(msg, fieldDesc, index) : refl->GetUInt32(msg, fieldDesc); QString valStr; if (useDecimalBase(fieldName)) valStr.setNum(val); else valStr.setNum(val, 16).prepend("0x"); if (val != fieldDesc->default_value_uint32()) out << fieldName << " = " << valStr << "\n"; break; } case FieldDescriptor::CPPTYPE_UINT64: { quint64 val = fieldDesc->is_repeated() ? refl->GetRepeatedUInt64(msg, fieldDesc, index) : refl->GetUInt64(msg, fieldDesc); QString valStr; if (useDecimalBase(fieldName)) valStr.setNum(val); else valStr.setNum(val, 16).prepend("0x"); if (val != fieldDesc->default_value_uint64()) out << fieldName << " = " << valStr << "\n"; break; } case FieldDescriptor::CPPTYPE_DOUBLE: { double val = fieldDesc->is_repeated() ? refl->GetRepeatedDouble(msg, fieldDesc, index) : refl->GetDouble(msg, fieldDesc); if (val != fieldDesc->default_value_double()) out << fieldName << " = " << val << "\n"; break; } case FieldDescriptor::CPPTYPE_FLOAT: { float val = fieldDesc->is_repeated() ? refl->GetRepeatedFloat(msg, fieldDesc, index) : refl->GetFloat(msg, fieldDesc); if (val != fieldDesc->default_value_float()) out << fieldName << " = " << val << "\n"; break; } case FieldDescriptor::CPPTYPE_BOOL: { bool val = fieldDesc->is_repeated() ? refl->GetRepeatedBool(msg, fieldDesc, index) : refl->GetBool(msg, fieldDesc); if (val != fieldDesc->default_value_bool()) out << fieldName << " = " << (refl->GetBool(msg, fieldDesc) ? "True" : "False") << "\n"; break; } case FieldDescriptor::CPPTYPE_STRING: { std::string val = fieldDesc->is_repeated() ? refl->GetRepeatedStringReference(msg, fieldDesc, index, &val) : refl->GetStringReference(msg, fieldDesc, &val); if (val == fieldDesc->default_value_string()) break; if (fieldDesc->type() == FieldDescriptor::TYPE_BYTES) { QString strVal = byteString(QByteArray(val.c_str(), val.size())); out << fieldName << " = b'" << strVal << "'\n"; } else { QString strVal = QString::fromStdString(val); out << fieldName << " = u'" << strVal << "'\n"; } break; } case FieldDescriptor::CPPTYPE_ENUM: { // Fields defined in protocol.proto are within ost_pb scope QString module = fieldDesc->file()->name() == "protocol.proto" ? "ost_pb." : ""; std::string val = fieldDesc->is_repeated() ? refl->GetRepeatedEnum(msg, fieldDesc, index)->full_name() : refl->GetEnum(msg, fieldDesc)->full_name(); if (val != fieldDesc->default_value_enum()->full_name()) out << fieldName << " = " << QString::fromStdString(val) .replace("OstProto.", module) << "\n"; break; } case FieldDescriptor::CPPTYPE_MESSAGE: { QString pfxStr(fieldName); const Message &msg2 = fieldDesc->is_repeated() ? refl->GetRepeatedMessage(msg, fieldDesc, index) : refl->GetMessage(msg, fieldDesc); const Reflection *refl2 = msg2.GetReflection(); std::vector fields2; QList autoFields; refl2->ListFields(msg2, &fields2); // Unfortunately, auto-calculated fields such as cksum, length // and protocol-type etc. may be set in the protobuf even if // they are not being overridden; // Intelligence regarding them is inside the respective protocol // implementation, not inside the protobuf objects - the latter // is all we have available here to work with; // We attempt a crude hack here to detect such fields and avoid // writing assignment statements for them for (uint i = 0; i < fields2.size(); i++) { std::string name = fields2.at(i)->name(); if ((fields2.at(i)->cpp_type() == FieldDescriptor::CPPTYPE_BOOL) && (name.find("is_override_") == 0) && (refl2->GetBool(msg2, fields2.at(i)) == false)) { name.erase(0, sizeof("is_override_") - 1); autoFields.append(name); } } for (uint i = 0 ; i < fields2.size(); i++) { // skip auto fields that are not overridden if (autoFields.contains(fields2.at(i)->name())) continue; writeFieldAssignment(out, QString("%1.%2").arg(pfxStr, fields2.at(i)->name().c_str()), msg2, refl2, fields2.at(i)); } break; } default: qWarning("unable to write field of unsupported type %d", fieldDesc->cpp_type()); } } QString PythonFileFormat::singularize(QString plural) { QString singular = plural; // Apply some heuristics if (plural.endsWith("ies")) singular.replace(singular.length()-3, 3, "y"); else if (plural.endsWith("ses")) singular.chop(2); else if (plural.endsWith("s")) singular.chop(1); return singular; } QString PythonFileFormat::escapeString(QByteArray str) { QString escStr = ""; for (int i=0; i < str.length(); i++) { uchar c = uchar(str.at(i)); if ((c < 128) && isprint(c)) { if (c == '\'') escStr.append("\\'"); else escStr.append(QChar(c)); } else escStr.append(QString("\\x%1").arg(int(c), 2, 16, QChar('0'))); } return escStr; } QString PythonFileFormat::byteString(QByteArray str) { QString byteStr = ""; for (int i=0; i < str.length(); i++) { uchar c = uchar(str.at(i)); byteStr.append(QString("\\x%1").arg(int(c), 2, 16, QChar('0'))); } return byteStr; } bool PythonFileFormat::useDecimalBase(QString fieldName) { // Heuristic - use Hex base for all except for the following return fieldName.endsWith("count") || fieldName.endsWith("length") || fieldName.endsWith("len") || fieldName.endsWith("time"); } ostinato-1.3.0/common/pythonfileformat.h000066400000000000000000000035131451413623100204100ustar00rootroot00000000000000/* Copyright (C) 2015 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _PYTHON_FILE_FORMAT_H #define _PYTHON_FILE_FORMAT_H #include "streamfileformat.h" #include class PythonFileFormat : public StreamFileFormat { public: PythonFileFormat(); ~PythonFileFormat(); virtual bool open(const QString fileName, OstProto::StreamConfigList &streams, QString &error); virtual bool save(const OstProto::StreamConfigList streams, const QString fileName, QString &error); bool isMyFileFormat(const QString fileName); bool isMyFileType(const QString fileType); private: void writeStandardImports(QTextStream &out); void writePrologue(QTextStream &out); void writeEpilogue(QTextStream &out); void writeFieldAssignment(QTextStream &out, QString fieldName, const google::protobuf::Message &msg, const google::protobuf::Reflection *refl, const google::protobuf::FieldDescriptor *fieldDesc, int index = -1); QString singularize(QString plural); QString escapeString(QByteArray str); QString byteString(QByteArray str); bool useDecimalBase(QString fieldName); }; extern PythonFileFormat pythonFileFormat; #endif ostinato-1.3.0/common/qtport.h000066400000000000000000000016231451413623100163470ustar00rootroot00000000000000/* Copyright (C) 2018 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _QT_PORT_H #define _QT_PORT_H // // Make Qt stuff portable across Qt versions // #if QT_VERSION < 0x050700 template T qFromBigEndian(const void *src) { return qFromBigEndian((const uchar*)src); } #endif #endif ostinato-1.3.0/common/sample.cpp000066400000000000000000000313631451413623100166360ustar00rootroot00000000000000/* Copyright (C) 2010, 2014 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #include "sample.h" SampleProtocol::SampleProtocol(StreamBase *stream, AbstractProtocol *parent) : AbstractProtocol(stream, parent) { } SampleProtocol::~SampleProtocol() { } AbstractProtocol* SampleProtocol::createInstance(StreamBase *stream, AbstractProtocol *parent) { return new SampleProtocol(stream, parent); } quint32 SampleProtocol::protocolNumber() const { return OstProto::Protocol::kSampleFieldNumber; } void SampleProtocol::protoDataCopyInto(OstProto::Protocol &protocol) const { protocol.MutableExtension(OstProto::sample)->CopyFrom(data); protocol.mutable_protocol_id()->set_id(protocolNumber()); } void SampleProtocol::protoDataCopyFrom(const OstProto::Protocol &protocol) { if (protocol.protocol_id().id() == protocolNumber() && protocol.HasExtension(OstProto::sample)) data.MergeFrom(protocol.GetExtension(OstProto::sample)); } QString SampleProtocol::name() const { return QString("Sample Protocol"); } QString SampleProtocol::shortName() const { return QString("SAMPLE"); } /*! TODO Return the ProtocolIdType for your protocol \n If your protocol doesn't have a protocolId field, you don't need to reimplement this method - the base class implementation will do the right thing */ AbstractProtocol::ProtocolIdType SampleProtocol::protocolIdType() const { return ProtocolIdIp; } /*! TODO Return the protocolId for your protoocol based on the 'type' requested \n If not all types are valid for your protocol, handle the valid type(s) and for the remaining fallback to the base class implementation; if your protocol doesn't have a protocolId at all, you don't need to reimplement this method - the base class will do the right thing */ quint32 SampleProtocol::protocolId(ProtocolIdType type) const { switch(type) { case ProtocolIdLlc: return 0xFFFFFF; case ProtocolIdEth: return 0xFFFF; case ProtocolIdIp: return 0xFF; default:break; } return AbstractProtocol::protocolId(type); } int SampleProtocol::fieldCount() const { return sample_fieldCount; } /*! TODO Return the number of frame fields for your protocol. A frame field is a field which has the FrameField flag set \n If your protocol has different sets of fields based on a OpCode/Type field (e.g. icmp), you MUST re-implement this function; however, if your protocol has a fixed set of frame fields always, you don't need to reimplement this method - the base class implementation will do the right thing */ int SampleProtocol::frameFieldCount() const { return AbstractProtocol::frameFieldCount(); } /*! TODO Edit this function to return the appropriate flags for each field \n See AbstractProtocol::FieldFlags for more info */ AbstractProtocol::FieldFlags SampleProtocol::fieldFlags(int index) const { AbstractProtocol::FieldFlags flags; flags = AbstractProtocol::fieldFlags(index); switch (index) { case sample_a: case sample_b: case sample_payloadLength: break; case sample_checksum: flags |= CksumField; break; case sample_x: case sample_y: break; case sample_is_override_checksum: flags &= ~FrameField; flags |= MetaField; break; default: qFatal("%s: unimplemented case %d in switch", __PRETTY_FUNCTION__, index); break; } return flags; } /*! TODO: Edit this function to return the data for each field See AbstractProtocol::fieldData() for more info */ QVariant SampleProtocol::fieldData(int index, FieldAttrib attrib, int streamIndex) const { switch (index) { case sample_a: { int a = data.ab() >> 13; switch(attrib) { case FieldName: return QString("A"); case FieldValue: return a; case FieldTextValue: return QString("%1").arg(a); case FieldFrameValue: return QByteArray(1, (char) a); case FieldBitSize: return 3; default: break; } break; } case sample_b: { int b = data.ab() & 0x1FFF; switch(attrib) { case FieldName: return QString("B"); case FieldValue: return b; case FieldTextValue: return QString("%1").arg(b, 4, BASE_HEX, QChar('0')); case FieldFrameValue: { QByteArray fv; fv.resize(2); qToBigEndian((quint16) b, (uchar*) fv.data()); return fv; } case FieldBitSize: return 13; default: break; } break; } case sample_payloadLength: { switch(attrib) { case FieldName: return QString("Payload Length"); case FieldValue: return protocolFramePayloadSize(streamIndex); case FieldFrameValue: { QByteArray fv; int totlen; totlen = protocolFramePayloadSize(streamIndex); fv.resize(2); qToBigEndian((quint16) totlen, (uchar*) fv.data()); return fv; } case FieldTextValue: return QString("%1").arg( protocolFramePayloadSize(streamIndex)); case FieldBitSize: return 16; default: break; } break; } case sample_checksum: { quint16 cksum; switch(attrib) { case FieldValue: case FieldFrameValue: case FieldTextValue: if (data.is_override_checksum()) cksum = data.checksum(); else cksum = protocolFrameCksum(streamIndex, CksumIp); break; default: cksum = 0; // avoid the 'maybe used unitialized' warning break; } switch(attrib) { case FieldName: return QString("Checksum"); case FieldValue: return cksum; case FieldFrameValue: { QByteArray fv; fv.resize(2); qToBigEndian(cksum, (uchar*) fv.data()); return fv; } case FieldTextValue: return QString("0x%1").arg( cksum, 4, BASE_HEX, QChar('0'));; case FieldBitSize: return 16; default: break; } break; } case sample_x: { switch(attrib) { case FieldName: return QString("X"); case FieldValue: return data.x(); case FieldTextValue: // Use the following line for display in decimal return QString("%1").arg(data.x()); // Use the following line for display in hexa-decimal //return QString("%1").arg(data.x(), 8, BASE_HEX, QChar('0')); case FieldFrameValue: { QByteArray fv; fv.resize(4); qToBigEndian((quint32) data.x(), (uchar*) fv.data()); return fv; } default: break; } break; } case sample_y: { switch(attrib) { case FieldName: return QString("Y"); case FieldValue: return data.y(); case FieldTextValue: // Use the following line for display in decimal //return QString("%1").arg(data.y()); // Use the following line for display in hexa-decimal return QString("%1").arg(data.y(), 4, BASE_HEX, QChar('0')); case FieldFrameValue: { QByteArray fv; fv.resize(2); qToBigEndian((quint16) data.y(), (uchar*) fv.data()); return fv; } default: break; } break; } // Meta fields case sample_is_override_checksum: { switch(attrib) { case FieldValue: return data.is_override_checksum(); default: break; } break; } default: qFatal("%s: unimplemented case %d in switch", __PRETTY_FUNCTION__, index); break; } return AbstractProtocol::fieldData(index, attrib, streamIndex); } /*! TODO: Edit this function to set the data for each field See AbstractProtocol::setFieldData() for more info */ bool SampleProtocol::setFieldData(int index, const QVariant &value, FieldAttrib attrib) { bool isOk = false; if (attrib != FieldValue) goto _exit; switch (index) { case sample_a: { uint a = value.toUInt(&isOk); if (isOk) data.set_ab((data.ab() & 0x1FFF) | ((a & 0x07) << 13)); break; } case sample_b: { uint b = value.toUInt(&isOk); if (isOk) data.set_ab((data.ab() & 0xe000) | (b & 0x1FFF)); break; } case sample_payloadLength: { uint len = value.toUInt(&isOk); if (isOk) data.set_payload_length(len); break; } case sample_checksum: { uint csum = value.toUInt(&isOk); if (isOk) data.set_checksum(csum); break; } case sample_x: { uint x = value.toUInt(&isOk); if (isOk) data.set_x(x); break; } case sample_y: { uint y = value.toUInt(&isOk); if (isOk) data.set_y(y); break; } case sample_is_override_checksum: { bool ovr = value.toBool(); data.set_is_override_checksum(ovr); isOk = true; break; } default: qFatal("%s: unimplemented case %d in switch", __PRETTY_FUNCTION__, index); break; } _exit: return isOk; } /*! TODO: Return the protocol frame size in bytes\n If your protocol has a fixed size - you don't need to reimplement this; the base class implementation is good enough */ int SampleProtocol::protocolFrameSize(int streamIndex) const { return AbstractProtocol::protocolFrameSize(streamIndex); } /*! TODO: If your protocol frame size can vary across pkts of the same stream, return true \n Otherwise you don't need to reimplement this method - the base class always returns false */ bool SampleProtocol::isProtocolFrameSizeVariable() const { return false; } /*! TODO: If your protocol frame has any variable fields or has a variable size, return the minimum number of frames required to vary the fields \n See AbstractProtocol::protocolFrameVariableCount() for more info */ int SampleProtocol::protocolFrameVariableCount() const { return AbstractProtocol::protocolFrameVariableCount(); } ostinato-1.3.0/common/sample.h000066400000000000000000000047641451413623100163100ustar00rootroot00000000000000/* Copyright (C) 2010, 2014 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _SAMPLE_H #define _SAMPLE_H #include "abstractprotocol.h" #include "sample.pb.h" /* Sample Protocol Frame Format - +-----+------+------+------+------+------+ | A | B | LEN | CSUM | X | Y | | (3) | (13) | (16) | (16) | (32) | (32) | +-----+------+------+------+------+------+ Figures in brackets represent field width in bits */ class SampleProtocol : public AbstractProtocol { public: enum samplefield { // Frame Fields sample_a = 0, sample_b, sample_payloadLength, sample_checksum, sample_x, sample_y, // Meta Fields sample_is_override_checksum, sample_fieldCount }; SampleProtocol(StreamBase *stream, AbstractProtocol *parent = 0); virtual ~SampleProtocol(); static AbstractProtocol* createInstance(StreamBase *stream, AbstractProtocol *parent = 0); virtual quint32 protocolNumber() const; virtual void protoDataCopyInto(OstProto::Protocol &protocol) const; virtual void protoDataCopyFrom(const OstProto::Protocol &protocol); virtual ProtocolIdType protocolIdType() const; virtual quint32 protocolId(ProtocolIdType type) const; virtual QString name() const; virtual QString shortName() const; virtual int fieldCount() const; virtual int frameFieldCount() const; virtual AbstractProtocol::FieldFlags fieldFlags(int index) const; virtual QVariant fieldData(int index, FieldAttrib attrib, int streamIndex = 0) const; virtual bool setFieldData(int index, const QVariant &value, FieldAttrib attrib = FieldValue); virtual int protocolFrameSize(int streamIndex = 0) const; virtual bool isProtocolFrameSizeVariable() const; virtual int protocolFrameVariableCount() const; private: OstProto::Sample data; }; #endif ostinato-1.3.0/common/sample.proto000066400000000000000000000020321451413623100172060ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ import "protocol.proto"; package OstProto; // Sample Protocol message Sample { optional bool is_override_checksum = 1; optional uint32 ab = 2; optional uint32 payload_length = 3; optional uint32 checksum = 4; optional uint32 x = 5 [default = 1234]; optional uint32 y = 6; } extend Protocol { optional Sample sample = 102; } ostinato-1.3.0/common/sample.ui000066400000000000000000000113661451413623100164720ustar00rootroot00000000000000 Sample 0 0 263 116 Form Field A Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter sampleA >HH; Checksum false >HH HH; Field B Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter sampleB >HH HH; Field X Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter sampleX Qt::Horizontal 40 20 Length Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter samplePayloadLength false Field Y Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter sampleY Qt::Vertical 20 40 sampleA sampleB samplePayloadLength isChecksumOverride sampleChecksum sampleX sampleY isChecksumOverride toggled(bool) sampleChecksum setEnabled(bool) 345 122 406 122 ostinato-1.3.0/common/sampleconfig.cpp000066400000000000000000000062251451413623100200230ustar00rootroot00000000000000/* Copyright (C) 2010, 2014 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #include "sampleconfig.h" #include "sample.h" SampleConfigForm::SampleConfigForm(QWidget *parent) : AbstractProtocolConfigForm(parent) { setupUi(this); } SampleConfigForm::~SampleConfigForm() { } SampleConfigForm* SampleConfigForm::createInstance() { return new SampleConfigForm; } /*! TODO: Edit this function to load each field's data into the config Widget See AbstractProtocolConfigForm::loadWidget() for more info */ void SampleConfigForm::loadWidget(AbstractProtocol *proto) { sampleA->setText( proto->fieldData( SampleProtocol::sample_a, AbstractProtocol::FieldValue ).toString()); sampleB->setText( proto->fieldData( SampleProtocol::sample_b, AbstractProtocol::FieldValue ).toString()); samplePayloadLength->setText( proto->fieldData( SampleProtocol::sample_payloadLength, AbstractProtocol::FieldValue ).toString()); isChecksumOverride->setChecked( proto->fieldData( SampleProtocol::sample_is_override_checksum, AbstractProtocol::FieldValue ).toBool()); sampleChecksum->setText(uintToHexStr( proto->fieldData( SampleProtocol::sample_checksum, AbstractProtocol::FieldValue ).toUInt(), 2)); sampleX->setText( proto->fieldData( SampleProtocol::sample_x, AbstractProtocol::FieldValue ).toString()); sampleY->setText( proto->fieldData( SampleProtocol::sample_y, AbstractProtocol::FieldValue ).toString()); } /*! TODO: Edit this function to store each field's data from the config Widget See AbstractProtocolConfigForm::storeWidget() for more info */ void SampleConfigForm::storeWidget(AbstractProtocol *proto) { proto->setFieldData( SampleProtocol::sample_a, sampleA->text()); proto->setFieldData( SampleProtocol::sample_b, sampleB->text()); proto->setFieldData( SampleProtocol::sample_payloadLength, samplePayloadLength->text()); proto->setFieldData( SampleProtocol::sample_is_override_checksum, isChecksumOverride->isChecked()); proto->setFieldData( SampleProtocol::sample_checksum, hexStrToUInt(sampleChecksum->text())); proto->setFieldData( SampleProtocol::sample_x, sampleX->text()); proto->setFieldData( SampleProtocol::sample_y, sampleY->text()); } ostinato-1.3.0/common/sampleconfig.h000066400000000000000000000022141451413623100174620ustar00rootroot00000000000000/* Copyright (C) 2010, 2014 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _SAMPLE_CONFIG_H #define _SAMPLE_CONFIG_H #include "abstractprotocolconfig.h" #include "ui_sample.h" class SampleConfigForm : public AbstractProtocolConfigForm, private Ui::Sample { Q_OBJECT public: SampleConfigForm(QWidget *parent = 0); virtual ~SampleConfigForm(); static SampleConfigForm* createInstance(); virtual void loadWidget(AbstractProtocol *proto); virtual void storeWidget(AbstractProtocol *proto); private slots: }; #endif ostinato-1.3.0/common/samplepdml.cpp000066400000000000000000000101111451413623100174770ustar00rootroot00000000000000/* Copyright (C) 2014 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #include "samplepdml.h" #include "sample.pb.h" /*! TODO : Initialize the following inherited protected members - - ostProtoId_ - fieldMap_ ostProtoId_ is the protocol's protobuf field number as defined in message 'Protocol' enum 'k' in file protocol.proto fieldMap_ is a mapping of the protocol's field names as they appear in the PDML to the protobuf field numbers for the protocol. All such fields are classified as 'known' fields and the base class will take care of decoding these without any help from the subclass. Note that the PDML field names are same as the field names used in Wireshark display filters. The full reference for these is available at - http://www.wireshark.org/docs/dfref/ */ PdmlSampleProtocol::PdmlSampleProtocol() { ostProtoId_ = OstProto::Protocol::kSampleFieldNumber; fieldMap_.insert("sample.checksum", OstProto::Sample::kChecksumFieldNumber); fieldMap_.insert("sample.x", OstProto::Sample::kXFieldNumber); fieldMap_.insert("sample.y", OstProto::Sample::kYFieldNumber); } PdmlSampleProtocol::~PdmlSampleProtocol() { } PdmlSampleProtocol* PdmlSampleProtocol::createInstance() { return new PdmlSampleProtocol(); } /*! TODO: Use this method to do any special handling that may be required for preprocessing a protocol before parsing/decoding the protocol's fields */ void PdmlSampleProtocol::preProtocolHandler(QString /*name*/, const QXmlStreamAttributes& /*attributes*/, int /*expectedPos*/, OstProto::Protocol* /*pbProto*/, OstProto::Stream* /*stream*/) { return; } /*! TODO: Use this method to do any special handling or cleanup that may be required when a protocol decode is ending prematurely */ void PdmlSampleProtocol::prematureEndHandler(int /*pos*/, OstProto::Protocol* /*pbProto*/, OstProto::Stream* /*stream*/) { return; } /*! TODO: Use this method to do any special handling that may be required for postprocessing a protocol after parsing/decoding all the protocol fields If your protocol's protobuf has some meta-fields that should be set to their non default values, this is a good place to do that. e.g. derived fields such as length, checksum etc. may be correct or incorrect in the PCAP/PDML - to retain the same value as in the PCAP/PDML and not let Ostinato recalculate these, you can set the is_override_length, is_override_cksum meta-fields to true here; for cksum, use the base class attribute overrideCksum_ to decide - this is set based on user input */ void PdmlSampleProtocol::postProtocolHandler(OstProto::Protocol* /*pbProto*/, OstProto::Stream* /*stream*/) { return; } /*! TODO: Handle all 'unknown' fields using this method You need to typically only handle frame fields or fields actually present in the protocol on the wire. So you can safely ignore meta-fields such as Good/Bad Checksum. Some fields may not have a 'name' attribute, so cannot be classified as a 'known' field. Use this method to identify such fields using other attributes such as 'show' or 'showname' and populate the corresponding protobuf field. If the PDML protocol contains some fields that are not supported by Ostinato, use a HexDump protocol as a replacement to store these bytes */ void PdmlSampleProtocol::unknownFieldHandler(QString /*name*/, int /*pos*/, int /*size*/, const QXmlStreamAttributes& /*attributes*/, OstProto::Protocol* /*pbProto*/, OstProto::Stream* /*stream*/) { return; } ostinato-1.3.0/common/samplepdml.h000066400000000000000000000032271451413623100171560ustar00rootroot00000000000000/* Copyright (C) 2014 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _SAMPLE_PDML_H #define _SAMPLE_PDML_H #include "pdmlprotocol.h" class PdmlSampleProtocol : public PdmlProtocol { public: virtual ~PdmlSampleProtocol(); static PdmlSampleProtocol* createInstance(); virtual void preProtocolHandler(QString name, const QXmlStreamAttributes &attributes, int expectedPos, OstProto::Protocol *pbProto, OstProto::Stream *stream); virtual void prematureEndHandler(int pos, OstProto::Protocol *pbProto, OstProto::Stream *stream); virtual void postProtocolHandler(OstProto::Protocol *pbProto, OstProto::Stream *stream); void fieldHandler(QString name, const QXmlStreamAttributes &attributes, OstProto::Protocol *pbProto, OstProto::Stream *stream); virtual void unknownFieldHandler(QString name, int pos, int size, const QXmlStreamAttributes &attributes, OstProto::Protocol *pbProto, OstProto::Stream *stream); protected: PdmlSampleProtocol(); }; #endif ostinato-1.3.0/common/sessionfileformat.cpp000066400000000000000000000046421451413623100211110ustar00rootroot00000000000000/* Copyright (C) 2016 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #include "sessionfileformat.h" #include "ossnfileformat.h" #include SessionFileFormat::SessionFileFormat() { stop_ = false; } SessionFileFormat::~SessionFileFormat() { } QDialog* SessionFileFormat::openOptionsDialog() { return NULL; } QDialog* SessionFileFormat::saveOptionsDialog() { return NULL; } QStringList SessionFileFormat::supportedFileTypes(Operation op) { QStringList fileTypes; fileTypes << "Ostinato Session (*.ossn)"; if (op == kOpenFile) fileTypes << "All files (*)"; return fileTypes; } void SessionFileFormat::openAsync(const QString fileName, OstProto::SessionContent &session, QString &error) { fileName_ = fileName; openSession_ = &session; error_ = &error; op_ = kOpenFile; stop_ = false; start(); } void SessionFileFormat::saveAsync( const OstProto::SessionContent &session, const QString fileName, QString &error) { saveSession_ = &session; fileName_ = fileName; error_ = &error; op_ = kSaveFile; stop_ = false; start(); } bool SessionFileFormat::result() { return result_; } SessionFileFormat* SessionFileFormat::fileFormatFromFile( const QString fileName) { if (ossnFileFormat.isMyFileFormat(fileName)) return &ossnFileFormat; return NULL; } SessionFileFormat* SessionFileFormat::fileFormatFromType( const QString fileType) { if (ossnFileFormat.isMyFileType(fileType)) return &ossnFileFormat; return NULL; } void SessionFileFormat::cancel() { stop_ = true; } void SessionFileFormat::run() { if (op_ == kOpenFile) result_ = open(fileName_, *openSession_, *error_); else if (op_ == kSaveFile) result_ = save(*saveSession_, fileName_, *error_); } ostinato-1.3.0/common/sessionfileformat.h000066400000000000000000000043471451413623100205600ustar00rootroot00000000000000/* Copyright (C) 2016 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _SESSION_FILE_FORMAT_H #define _SESSION_FILE_FORMAT_H #include "fileformat.pb.h" #include "protocol.pb.h" #include #include class QDialog; class SessionFileFormat : public QThread { Q_OBJECT public: enum Operation { kOpenFile, kSaveFile }; SessionFileFormat(); virtual ~SessionFileFormat(); virtual bool open(const QString fileName, OstProto::SessionContent &session, QString &error) = 0; virtual bool save(const OstProto::SessionContent &session, const QString fileName, QString &error) = 0; virtual QDialog* openOptionsDialog(); virtual QDialog* saveOptionsDialog(); void openAsync(const QString fileName, OstProto::SessionContent &session, QString &error); void saveAsync(const OstProto::SessionContent &session, const QString fileName, QString &error); bool result(); static QStringList supportedFileTypes(Operation op); static SessionFileFormat* fileFormatFromFile(const QString fileName); static SessionFileFormat* fileFormatFromType(const QString fileType); virtual bool isMyFileFormat(const QString fileName) = 0; virtual bool isMyFileType(const QString fileType) = 0; signals: void status(QString text); void target(int value); void progress(int value); public slots: void cancel(); protected: void run(); bool stop_; private: QString fileName_; OstProto::SessionContent *openSession_; const OstProto::SessionContent *saveSession_; QString *error_; Operation op_; bool result_; }; #endif ostinato-1.3.0/common/sign.cpp000066400000000000000000000171651451413623100163210ustar00rootroot00000000000000/* Copyright (C) 2016 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #include "sign.h" #include "../common/streambase.h" SignProtocol::SignProtocol(StreamBase *stream, AbstractProtocol *parent) : AbstractProtocol(stream, parent) { } SignProtocol::~SignProtocol() { } AbstractProtocol* SignProtocol::createInstance(StreamBase *stream, AbstractProtocol *parent) { return new SignProtocol(stream, parent); } quint32 SignProtocol::protocolNumber() const { return OstProto::Protocol::kSignFieldNumber; } void SignProtocol::protoDataCopyInto(OstProto::Protocol &protocol) const { protocol.MutableExtension(OstProto::sign)->CopyFrom(data); protocol.mutable_protocol_id()->set_id(protocolNumber()); } void SignProtocol::protoDataCopyFrom(const OstProto::Protocol &protocol) { if (protocol.protocol_id().id() == protocolNumber() && protocol.HasExtension(OstProto::sign)) data.MergeFrom(protocol.GetExtension(OstProto::sign)); } QString SignProtocol::name() const { return QString("Signature"); } QString SignProtocol::shortName() const { return QString("SIGN"); } int SignProtocol::fieldCount() const { return sign_fieldCount; } AbstractProtocol::FieldFlags SignProtocol::fieldFlags(int index) const { AbstractProtocol::FieldFlags flags; flags = AbstractProtocol::fieldFlags(index); switch (index) { case sign_magic: case sign_tlv_tx_port: case sign_tlv_guid: case sign_tlv_ttag: case sign_tlv_end: break; default: qFatal("%s: unimplemented case %d in switch", __PRETTY_FUNCTION__, index); break; } return flags; } QVariant SignProtocol::fieldData(int index, FieldAttrib attrib, int streamIndex) const { switch (index) { case sign_magic: { switch(attrib) { case FieldName: return QString("Magic"); case FieldValue: return kSignMagic; case FieldTextValue: return QString("%1").arg(kSignMagic); case FieldFrameValue: { QByteArray fv; fv.resize(4); qToBigEndian(kSignMagic, (uchar*) fv.data()); return fv; } default: break; } break; } case sign_tlv_ttag: { switch(attrib) { case FieldName: return QString("T-Tag"); case FieldValue: return 0; case FieldTextValue: return QString("%1").arg(0); case FieldFrameValue: { QByteArray fv; fv.resize(2); fv[0] = 0; fv[1] = kTypeLenTtagPlaceholder; return fv; } default: break; } break; } case sign_tlv_tx_port: { switch(attrib) { case FieldName: return QString("TxPort"); case FieldValue: return mpStream->portId(); case FieldTextValue: return QString("%1").arg(mpStream->portId()); case FieldFrameValue: { QByteArray fv; fv.resize(2); fv[0] = mpStream->portId() & 0xFF; fv[1] = kTypeLenTxPort; return fv; } default: break; } break; } case sign_tlv_guid: { quint32 guid = data.stream_guid() & 0xFFFFFF; switch(attrib) { case FieldName: return QString("Stream GUID"); case FieldValue: return guid; case FieldTextValue: return QString("%1").arg(guid); case FieldFrameValue: { QByteArray fv; fv.resize(4); fv[0] = (guid >> 16) & 0xff; fv[1] = (guid >> 8) & 0xff; fv[2] = (guid >> 0) & 0xff; fv[3] = kTypeLenGuid; return fv; } default: break; } break; } case sign_tlv_end: { switch(attrib) { case FieldName: return QString("End TLV"); case FieldValue: return 0; case FieldTextValue: return QString("NA"); case FieldFrameValue: return QByteArray(1, kTypeLenEnd); default: break; } break; } default: qFatal("%s: unimplemented case %d in switch", __PRETTY_FUNCTION__, index); break; } return AbstractProtocol::fieldData(index, attrib, streamIndex); } bool SignProtocol::setFieldData(int index, const QVariant &value, FieldAttrib attrib) { bool isOk = false; if (attrib != FieldValue) goto _exit; switch (index) { case sign_tlv_guid: { uint guid = value.toUInt(&isOk); if (isOk) data.set_stream_guid(guid & 0xFFFFFF); break; } default: qFatal("%s: unimplemented case %d in switch", __PRETTY_FUNCTION__, index); break; } _exit: return isOk; } quint32 SignProtocol::magic() { return kSignMagic; } bool SignProtocol::packetGuid(const uchar *pkt, int pktLen, uint *guid) { const uchar *p = pkt + pktLen - sizeof(kSignMagic); quint32 magic = qFromBigEndian(p); if (magic != kSignMagic) return false; p--; while (*p != kTypeLenEnd) { if (*p == kTypeLenGuid) { *guid = qFromBigEndian(p - 3) >> 8; return true; } p -= 1 + (*p >> 5); // move to next TLV } return false; } bool SignProtocol::packetTtagId(const uchar *pkt, int pktLen, uint *ttagId, uint *guid) { bool ret = false; const uchar *p = pkt + pktLen - sizeof(kSignMagic); quint32 magic = qFromBigEndian(p); if (magic != kSignMagic) return ret; *guid = kInvalidGuid; p--; while (*p != kTypeLenEnd) { if (*p == kTypeLenTtag) { *ttagId = *(p - 1); ret = true; } else if (*p == kTypeLenGuid) { *guid = qFromBigEndian(p - 3) >> 8; } else if (*p == kTypeLenTxPort) { #ifdef Q_OS_WIN32 *ttagId |= uint(*(p - 1)) << 8; #endif } p -= 1 + (*p >> 5); // move to next TLV } return ret; } ostinato-1.3.0/common/sign.h000066400000000000000000000067331451413623100157650ustar00rootroot00000000000000/* Copyright (C) 2016 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _SIGN_H #define _SIGN_H #include "abstractprotocol.h" #include "sign.pb.h" #include /* Sign Protocol is expected at the end of the frame (just before the Eth FCS) ---+--------+-------+ . . .| TLV(s) | Magic | | (8+) | (32) | ---+--------+-------+ Figures in brackets represent field width in bits TLVs are encoded as +-------+-----+------+ | Value | Len | Type | | (...) | (3) | (5) | +-------+-----+------+ Len does NOT include the one byte of TypeLen Size of the value field varies between 0 to 7 bytes Defined TLVs Type = 0, Len = 0 (0x00): End of TLVs Type = 1, Len = 3 (0x61): Stream GUID Type = 2, Len = 1 (0x22): T-Tag Placeholder (0 value) Type = 3, Len = 1 (0x23): T-Tag with actual value Type = 4, Len = 1 (0x24): Tx Port Id Order of TLVs from end of packet towards beginning [Offset, Size] [ -4, 4 bytes] Magic [ -6, 2 bytes] TTag (Placeholder or actual) [-10, 4 bytes] Stream Guid [-12, 2 bytes] Tx Port Id [-13, 1 byte ] End */ class SignProtocol : public AbstractProtocol { public: enum samplefield { // Frame Fields sign_tlv_end = 0, sign_tlv_tx_port, sign_tlv_guid, sign_tlv_ttag, sign_magic, // Meta Fields // - None sign_fieldCount }; SignProtocol(StreamBase *stream, AbstractProtocol *parent = 0); virtual ~SignProtocol(); static AbstractProtocol* createInstance(StreamBase *stream, AbstractProtocol *parent = 0); virtual quint32 protocolNumber() const; virtual void protoDataCopyInto(OstProto::Protocol &protocol) const; virtual void protoDataCopyFrom(const OstProto::Protocol &protocol); virtual QString name() const; virtual QString shortName() const; virtual int fieldCount() const; virtual AbstractProtocol::FieldFlags fieldFlags(int index) const; virtual QVariant fieldData(int index, FieldAttrib attrib, int streamIndex = 0) const; virtual bool setFieldData(int index, const QVariant &value, FieldAttrib attrib = FieldValue); static quint32 magic(); static bool packetGuid(const uchar *pkt, int pktLen, uint *guid); static bool packetTtagId(const uchar *pkt, int pktLen, uint *ttagId, uint *guid); // XXX: Any change in kTypeLenXXX or magic value should also be done in // TxThread/Ttag code as well where hardcoded values are used static const quint32 kMaxGuid = 0x00ffffff; static const quint32 kInvalidGuid = UINT_MAX; static const quint8 kTypeLenTtagPlaceholder = 0x22; static const quint8 kTypeLenTtag = 0x23; private: static const quint32 kSignMagic = 0x1d10c0da; // coda! (unicode - 0x1d10c) static const quint8 kTypeLenEnd = 0x00; static const quint8 kTypeLenGuid = 0x61; static const quint8 kTypeLenTxPort = 0x24; OstProto::Sign data; }; #endif ostinato-1.3.0/common/sign.proto000066400000000000000000000015111451413623100166660ustar00rootroot00000000000000/* Copyright (C) 2016 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ import "protocol.proto"; package OstProto; // Sign Protocol message Sign { optional uint32 stream_guid = 1; } extend Protocol { optional Sign sign = 105; } ostinato-1.3.0/common/sign.ui000066400000000000000000000024101451413623100161370ustar00rootroot00000000000000 Sign 0 0 400 64 Form Stream GUID guid Qt::Horizontal 40 20 Qt::Vertical 20 40 ostinato-1.3.0/common/signconfig.cpp000066400000000000000000000024721451413623100175020ustar00rootroot00000000000000/* Copyright (C) 2016 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #include "signconfig.h" #include "sign.h" SignConfigForm::SignConfigForm(QWidget *parent) : AbstractProtocolConfigForm(parent) { setupUi(this); } SignConfigForm::~SignConfigForm() { } SignConfigForm* SignConfigForm::createInstance() { return new SignConfigForm; } void SignConfigForm::loadWidget(AbstractProtocol *proto) { guid->setText( proto->fieldData( SignProtocol::sign_tlv_guid, AbstractProtocol::FieldValue ).toString()); } void SignConfigForm::storeWidget(AbstractProtocol *proto) { proto->setFieldData( SignProtocol::sign_tlv_guid, guid->text()); } ostinato-1.3.0/common/signconfig.h000066400000000000000000000021641451413623100171450ustar00rootroot00000000000000/* Copyright (C) 2016 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _SIGN_CONFIG_H #define _SIGN_CONFIG_H #include "abstractprotocolconfig.h" #include "ui_sign.h" class SignConfigForm : public AbstractProtocolConfigForm, private Ui::Sign { Q_OBJECT public: SignConfigForm(QWidget *parent = 0); virtual ~SignConfigForm(); static SignConfigForm* createInstance(); virtual void loadWidget(AbstractProtocol *proto); virtual void storeWidget(AbstractProtocol *proto); private slots: }; #endif ostinato-1.3.0/common/snap.cpp000066400000000000000000000147371451413623100163240ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #include "snap.h" quint32 kStdOui = 0x000000; SnapProtocol::SnapProtocol(StreamBase *stream, AbstractProtocol *parent) : AbstractProtocol(stream, parent) { } SnapProtocol::~SnapProtocol() { } AbstractProtocol* SnapProtocol::createInstance(StreamBase *stream, AbstractProtocol *parent) { return new SnapProtocol(stream, parent); } quint32 SnapProtocol::protocolNumber() const { return OstProto::Protocol::kSnapFieldNumber; } void SnapProtocol::protoDataCopyInto(OstProto::Protocol &protocol) const { protocol.MutableExtension(OstProto::snap)->CopyFrom(data); protocol.mutable_protocol_id()->set_id(protocolNumber()); } void SnapProtocol::protoDataCopyFrom(const OstProto::Protocol &protocol) { if (protocol.protocol_id().id() == protocolNumber() && protocol.HasExtension(OstProto::snap)) data.MergeFrom(protocol.GetExtension(OstProto::snap)); } QString SnapProtocol::name() const { return QString("SubNetwork Access Protocol"); } QString SnapProtocol::shortName() const { return QString("SNAP"); } AbstractProtocol::ProtocolIdType SnapProtocol::protocolIdType() const { return ProtocolIdEth; } quint32 SnapProtocol::protocolId(ProtocolIdType type) const { switch(type) { case ProtocolIdLlc: return 0xAAAA03; default: break; } return AbstractProtocol::protocolId(type); } int SnapProtocol::fieldCount() const { return snap_fieldCount; } AbstractProtocol::FieldFlags SnapProtocol::fieldFlags(int index) const { AbstractProtocol::FieldFlags flags; flags = AbstractProtocol::fieldFlags(index); switch (index) { case snap_oui: case snap_type: break; case snap_is_override_oui: case snap_is_override_type: flags &= ~FrameField; flags |= MetaField; break; default: break; } return flags; } QVariant SnapProtocol::fieldData(int index, FieldAttrib attrib, int streamIndex) const { switch (index) { case snap_oui: switch(attrib) { case FieldName: return QString("OUI"); case FieldValue: { quint32 oui = data.is_override_oui() ? data.oui() : kStdOui; return oui; } case FieldTextValue: { quint32 oui = data.is_override_oui() ? data.oui() : kStdOui; return QString("%1").arg(oui, 6, BASE_HEX, QChar('0')); } case FieldFrameValue: { quint32 oui = data.is_override_oui() ? data.oui() : kStdOui; QByteArray fv; fv.resize(4); qToBigEndian(oui, (uchar*) fv.data()); fv.remove(0, 1); return fv; } default: break; } break; case snap_type: { quint16 type; switch(attrib) { case FieldName: return QString("Type"); case FieldValue: type = data.is_override_type() ? data.type() : payloadProtocolId(ProtocolIdEth); return type; case FieldTextValue: type = data.is_override_type() ? data.type() : payloadProtocolId(ProtocolIdEth); return QString("%1").arg(type, 4, BASE_HEX, QChar('0')); case FieldFrameValue: { QByteArray fv; fv.resize(2); type = data.is_override_type() ? data.type() : payloadProtocolId(ProtocolIdEth); qToBigEndian(type, (uchar*) fv.data()); return fv; } default: break; } break; } // Meta fields case snap_is_override_oui: { switch(attrib) { case FieldValue: return data.is_override_oui(); default: break; } break; } case snap_is_override_type: { switch(attrib) { case FieldValue: return data.is_override_type(); default: break; } break; } default: qFatal("%s: unimplemented case %d in switch", __PRETTY_FUNCTION__, index); break; } return AbstractProtocol::fieldData(index, attrib, streamIndex); } bool SnapProtocol::setFieldData(int index, const QVariant &value, FieldAttrib attrib) { bool isOk = false; if (attrib != FieldValue) return false; switch (index) { case snap_oui: { uint oui = value.toUInt(&isOk); if (isOk) data.set_oui(oui); break; } case snap_type: { uint type = value.toUInt(&isOk); if (isOk) data.set_type(type); break; } case snap_is_override_oui: { bool ovr = value.toBool(); data.set_is_override_oui(ovr); isOk = true; break; } case snap_is_override_type: { bool ovr = value.toBool(); data.set_is_override_type(ovr); isOk = true; break; } default: qFatal("%s: unimplemented case %d in switch", __PRETTY_FUNCTION__, index); break; } return isOk; } ostinato-1.3.0/common/snap.h000066400000000000000000000036331451413623100157620ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _SNAP_H #define _SNAP_H #include "abstractprotocol.h" #include "snap.pb.h" class SnapProtocol : public AbstractProtocol { public: enum snapfield { snap_oui = 0, snap_type, // Meta fields snap_is_override_oui, snap_is_override_type, snap_fieldCount }; SnapProtocol(StreamBase *stream, AbstractProtocol *parent = 0); virtual ~SnapProtocol(); static AbstractProtocol* createInstance(StreamBase *stream, AbstractProtocol *parent = 0); virtual quint32 protocolNumber() const; virtual void protoDataCopyInto(OstProto::Protocol &protocol) const; virtual void protoDataCopyFrom(const OstProto::Protocol &protocol); virtual QString name() const; virtual QString shortName() const; virtual ProtocolIdType protocolIdType() const; virtual quint32 protocolId(ProtocolIdType type) const; virtual int fieldCount() const; virtual AbstractProtocol::FieldFlags fieldFlags(int index) const; virtual QVariant fieldData(int index, FieldAttrib attrib, int streamIndex = 0) const; virtual bool setFieldData(int index, const QVariant &value, FieldAttrib attrib = FieldValue); private: OstProto::Snap data; }; #endif ostinato-1.3.0/common/snap.proto000066400000000000000000000016361451413623100166770ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ import "protocol.proto"; package OstProto; message Snap { optional bool is_override_oui = 3; optional bool is_override_type = 4; optional uint32 oui = 1; optional uint32 type = 2; } extend Protocol { optional Snap snap = 203; } ostinato-1.3.0/common/snap.ui000066400000000000000000000053221451413623100161450ustar00rootroot00000000000000 snap 0 0 268 98 Form SNAP OUI false >HH HH HH; Type false >HH HH; Qt::Horizontal 40 20 Qt::Vertical 20 40 cbOverrideOui toggled(bool) leOui setEnabled(bool) 49 42 68 43 cbOverrideType toggled(bool) leType setEnabled(bool) 161 34 183 33 ostinato-1.3.0/common/snapconfig.cpp000066400000000000000000000043121451413623100174760ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #include "snapconfig.h" #include "snap.h" SnapConfigForm::SnapConfigForm(QWidget *parent) : AbstractProtocolConfigForm(parent) { setupUi(this); } SnapConfigForm::~SnapConfigForm() { } SnapConfigForm* SnapConfigForm::createInstance() { return new SnapConfigForm; } void SnapConfigForm::loadWidget(AbstractProtocol *proto) { cbOverrideOui->setChecked( proto->fieldData( SnapProtocol::snap_is_override_oui, AbstractProtocol::FieldValue ).toBool()); leOui->setText(uintToHexStr( proto->fieldData( SnapProtocol::snap_oui, AbstractProtocol::FieldValue ).toUInt(), 3)); cbOverrideType->setChecked( proto->fieldData( SnapProtocol::snap_is_override_type, AbstractProtocol::FieldValue ).toBool()); leType->setText(uintToHexStr( proto->fieldData( SnapProtocol::snap_type, AbstractProtocol::FieldValue ).toUInt(), 2)); } void SnapConfigForm::storeWidget(AbstractProtocol *proto) { proto->setFieldData( SnapProtocol::snap_is_override_oui, cbOverrideOui->isChecked()); proto->setFieldData( SnapProtocol::snap_oui, hexStrToUInt(leOui->text())); proto->setFieldData( SnapProtocol::snap_is_override_type, cbOverrideType->isChecked()); proto->setFieldData( SnapProtocol::snap_type, hexStrToUInt(leType->text())); } ostinato-1.3.0/common/snapconfig.h000066400000000000000000000021451451413623100171450ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _SNAP_CONFIG_H #define _SNAP_CONFIG_H #include "abstractprotocolconfig.h" #include "ui_snap.h" class SnapConfigForm : public AbstractProtocolConfigForm, private Ui::snap { Q_OBJECT public: SnapConfigForm(QWidget *parent = 0); virtual ~SnapConfigForm(); static SnapConfigForm* createInstance(); virtual void loadWidget(AbstractProtocol *proto); virtual void storeWidget(AbstractProtocol *proto); }; #endif ostinato-1.3.0/common/spinboxdelegate.cpp000066400000000000000000000077251451413623100205370ustar00rootroot00000000000000/* Copyright (C) 2016 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see This file incorporates work covered by the following copyright and permission notice: Copyright (C) 2015 The Qt Company Ltd. Contact: http://www.qt.io/licensing/ This file is part of the examples of the Qt Toolkit. $QT_BEGIN_LICENSE:BSD$ You may use this file under the terms of the BSD license as follows: "Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. Neither the name of The Qt Company Ltd nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." $QT_END_LICENSE$ */ //#include #include "spinboxdelegate.h" #include SpinBoxDelegate::SpinBoxDelegate(QObject *parent) : QItemDelegate(parent) { } QWidget *SpinBoxDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem &/* option */, const QModelIndex &index) const { QSpinBox *editor = new QSpinBox(parent); editor->setMinimum(colMin_.contains(index.column()) ? colMin_.value(index.column()) : 0); editor->setMaximum(colMax_.contains(index.column()) ? colMax_.value(index.column()) : INT_MAX); return editor; } void SpinBoxDelegate::setColumnRange(int col, int min, int max) { colMin_.insert(col, min); colMax_.insert(col, max); } void SpinBoxDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const { int value = index.model()->data(index, Qt::EditRole).toInt(); QSpinBox *spinBox = static_cast(editor); spinBox->setValue(value); } void SpinBoxDelegate::setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const { QSpinBox *spinBox = static_cast(editor); spinBox->interpretText(); int value = spinBox->value(); model->setData(index, value, Qt::EditRole); } void SpinBoxDelegate::updateEditorGeometry(QWidget *editor, const QStyleOptionViewItem &option, const QModelIndex &/* index */) const { editor->setGeometry(option.rect); } ostinato-1.3.0/common/spinboxdelegate.h000066400000000000000000000065421451413623100202000ustar00rootroot00000000000000/* Copyright (C) 2016 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see This file incorporates work covered by the following copyright and permission notice: Copyright (C) 2015 The Qt Company Ltd. Contact: http://www.qt.io/licensing/ This file is part of the examples of the Qt Toolkit. $QT_BEGIN_LICENSE:BSD$ You may use this file under the terms of the BSD license as follows: "Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. Neither the name of The Qt Company Ltd nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." $QT_END_LICENSE$ */ #ifndef _SPIN_BOX_DELEGATE_H #define _SPIN_BOX_DELEGATE_H #include #include #include #include #include #include class SpinBoxDelegate : public QItemDelegate { Q_OBJECT public: SpinBoxDelegate(QObject *parent = 0); void setColumnRange(int col, int min, int max); QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const; void setEditorData(QWidget *editor, const QModelIndex &index) const; void setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const; void updateEditorGeometry(QWidget *editor, const QStyleOptionViewItem &option, const QModelIndex &index) const; private: QHash colMin_; QHash colMax_; }; #endif ostinato-1.3.0/common/stp.cpp000066400000000000000000000405521451413623100161630ustar00rootroot00000000000000/* Copyright (C) 2014 PLVision. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see This module is developed by PLVision */ #include "stp.h" #include #define uintToMacStr(num) \ QString("%1").arg(num, 6 * 2, BASE_HEX, QChar('0')) \ .replace(QRegExp("([0-9a-fA-F]{2}\\B)"), "\\1:").toUpper() #define ONE_BIT(pos) ((unsigned int)(1 << (pos))) #define BITS(bit) (bit) #define BYTES(byte) (byte) #define BYTES_TO_BITS(byte) (byte * 8) #define STP_LLC 0x424203 StpProtocol::StpProtocol(StreamBase *stream, AbstractProtocol *parent) : AbstractProtocol(stream, parent) { } StpProtocol::~StpProtocol() { } AbstractProtocol* StpProtocol::createInstance(StreamBase *stream, AbstractProtocol *parent) { return new StpProtocol(stream, parent); } quint32 StpProtocol::protocolNumber() const { return OstProto::Protocol::kStpFieldNumber; } void StpProtocol::protoDataCopyInto(OstProto::Protocol &protocol) const { protocol.MutableExtension(OstProto::stp)->CopyFrom(data_); protocol.mutable_protocol_id()->set_id(protocolNumber()); } void StpProtocol::protoDataCopyFrom(const OstProto::Protocol &protocol) { if (protocol.protocol_id().id() == protocolNumber() && protocol.HasExtension(OstProto::stp)) data_.MergeFrom(protocol.GetExtension(OstProto::stp)); } QString StpProtocol::name() const { return QString("Spanning Tree Protocol"); } QString StpProtocol::shortName() const { return QString("STP"); } quint32 StpProtocol::protocolId(ProtocolIdType type) const { switch(type) { case ProtocolIdLlc: return STP_LLC; default: break; } return AbstractProtocol::protocolId(type); } int StpProtocol::fieldCount() const { return stp_fieldCount; } AbstractProtocol::FieldFlags StpProtocol::fieldFlags(int index) const { AbstractProtocol::FieldFlags flags; flags = AbstractProtocol::fieldFlags(index); switch (index) { case stp_protocol_id: case stp_version_id: case stp_bpdu_type: case stp_flags: case stp_root_id: case stp_root_path_cost: case stp_bridge_id: case stp_port_id: case stp_message_age: case stp_max_age: case stp_hello_time: case stp_forward_delay: break; default: qFatal("%s: unimplemented case %d in switch", __PRETTY_FUNCTION__, index); break; } return flags; } QVariant StpProtocol::fieldData(int index, FieldAttrib attrib, int streamIndex) const { QString str[] = {"Topology Change", "Topology Change Acknowledgment"}; switch (index) { case stp_protocol_id: { switch (attrib) { case FieldName: return QString("Protocol Identifier"); case FieldValue: return data_.protocol_id(); case FieldTextValue: return QString("0x%1").arg(data_.protocol_id(), 4, BASE_HEX, QChar('0')); case FieldFrameValue: { QByteArray fv; fv.resize(BYTES(2)); qToBigEndian((quint16)data_.protocol_id(), (uchar*)fv.data()); return fv; } case FieldBitSize: return BYTES_TO_BITS(2); default: break; } break; } case stp_version_id: { switch (attrib) { case FieldName: return QString("Version Identifier"); case FieldValue: return data_.protocol_version_id(); case FieldTextValue: return QString("%1").arg( data_.protocol_version_id()); case FieldFrameValue: return QByteArray(1, (char)data_.protocol_version_id()); case FieldBitSize: return BYTES_TO_BITS(1); default: break; } break; } case stp_bpdu_type: { switch (attrib) { case FieldName: return QString("BPDU Type"); case FieldValue: return data_.bpdu_type(); case FieldTextValue: return QString("0x%1").arg(data_.bpdu_type(), 2, BASE_HEX, QChar('0')); case FieldFrameValue: return QByteArray(1, (char)data_.bpdu_type()); case FieldBitSize: return BYTES_TO_BITS(1); default: break; } break; } case stp_flags: { switch (attrib) { case FieldName: return QString("BPDU Flags"); case FieldValue: return data_.flags(); case FieldTextValue: { QString strTemp = "("; if((data_.flags() & ONE_BIT(0))) strTemp += str[0] + ", "; if((data_.flags() & ONE_BIT(7))) strTemp += str[1] + ", "; strTemp += ")"; strTemp.replace(", )", ")"); return strTemp; } case FieldFrameValue: return QByteArray(1, (char)data_.flags()); case FieldBitSize: return BYTES_TO_BITS(1); default: break; } break; } case stp_root_id: { switch (attrib) { case FieldName: return QString("Root Identifier"); case FieldValue: return (quint64) data_.root_id(); case FieldTextValue: { // Root ID contain two value: // Root ID Priority(first 2 bytes) // and Root ID MAC (last 6 bytes). (IEEE802.1D-2008) quint16 priority = ( data_.root_id() & 0xFFFF000000000000ULL) >> (BYTES_TO_BITS(6)); quint64 mac = data_.root_id() & 0x0000FFFFFFFFFFFFULL; return QString("Priority: %1 / MAC: %2") .arg(QString::number(priority), uintToMacStr(mac)); } case FieldFrameValue: { QByteArray fv; fv.resize(BYTES(8)); qToBigEndian((quint64)data_.root_id(), (uchar*)fv.data()); return fv; } case FieldBitSize: return BYTES_TO_BITS(8); default: break; } break; } case stp_root_path_cost: { switch (attrib) { case FieldName: return QString("Root Path Cost"); case FieldValue: return data_.root_path_cost(); case FieldTextValue: return QString("%1").arg(data_.root_path_cost()); case FieldFrameValue: { QByteArray fv; fv.resize(BYTES(4)); qToBigEndian(data_.root_path_cost(), (uchar*)fv.data()); return fv; } case FieldBitSize: return BYTES_TO_BITS(4); default: break; } break; } case stp_bridge_id: { switch (attrib) { case FieldName: return QString("Bridge Identifier"); case FieldValue: return (quint64) data_.bridge_id(); case FieldTextValue: { // Bridge ID contain two value: // Bridge ID Priority(first 2 bytes) // and Bridge ID MAC (last 6 bytes). (IEEE802.1D-2008) quint16 priority = (data_.bridge_id() & 0xFFFF000000000000ULL ) >> (BYTES_TO_BITS(6)); quint64 mac = data_.bridge_id() & 0x0000FFFFFFFFFFFFULL; return QString("Priority: %1 / MAC: %2").arg(QString::number(priority), uintToMacStr(mac)); } case FieldFrameValue: { QByteArray fv; fv.resize(BYTES(8)); qToBigEndian((quint64)data_.bridge_id(), (uchar*)fv.data()); return fv; } case FieldBitSize: return BYTES_TO_BITS(8); default: break; } break; } case stp_port_id: { switch (attrib) { case FieldName: return QString("Port Identifier"); case FieldValue: return data_.port_id(); case FieldTextValue: return QString("0x%1").arg(data_.port_id(), 4, BASE_HEX, QChar('0')); case FieldFrameValue: { QByteArray fv; fv.resize(BYTES(2)); qToBigEndian((quint16)data_.port_id(), (uchar*)fv.data()); return fv; } case FieldBitSize: return BYTES_TO_BITS(2); default: break; } break; } case stp_message_age: { switch (attrib) { case FieldName: return QString("Message Age"); case FieldValue: return data_.message_age(); case FieldTextValue: return QString("%1").arg(data_.message_age()); case FieldFrameValue: { QByteArray fv; fv.resize(BYTES(2)); qToBigEndian((quint16)(data_.message_age()), (uchar*)fv.data()); return fv; } case FieldBitSize: return BYTES_TO_BITS(2); default: break; } break; } case stp_max_age: { switch (attrib) { case FieldName: return QString("Max Age"); case FieldValue: return data_.max_age(); case FieldTextValue: return QString("%1").arg(data_.max_age()); case FieldFrameValue: { QByteArray fv; fv.resize(BYTES(2)); qToBigEndian((quint16)data_.max_age(), (uchar*)fv.data()); return fv; } case FieldBitSize: return BYTES_TO_BITS(2); default: break; } break; } case stp_hello_time: { switch (attrib) { case FieldName: return QString("Hello Time"); case FieldValue: return data_.hello_time(); case FieldTextValue: return QString("%1").arg(data_.hello_time()); case FieldFrameValue: { QByteArray fv; fv.resize(BYTES(2)); qToBigEndian((quint16)data_.hello_time(), (uchar*)fv.data()); return fv; } case FieldBitSize: return BYTES_TO_BITS(2); default: break; } break; } case stp_forward_delay: { switch (attrib) { case FieldName: return QString("Forward Delay"); case FieldValue: return data_.forward_delay(); case FieldTextValue: return QString("%1").arg(data_.forward_delay()); case FieldFrameValue: { QByteArray fv; fv.resize(BYTES(2)); qToBigEndian((quint16)data_.forward_delay(), (uchar*)fv.data()); return fv; } case FieldBitSize: return BYTES_TO_BITS(2); default: break; } break; } default: break; } return AbstractProtocol::fieldData(index, attrib, streamIndex); } bool StpProtocol::setFieldData(int index, const QVariant &value, FieldAttrib attrib) { bool isOk = false; if (attrib != FieldValue) return isOk; switch (index) { case stp_protocol_id: { quint16 protoId = value.toUInt(&isOk); if (isOk) data_.set_protocol_id(protoId); break; } case stp_version_id: { quint8 versionId = value.toUInt(&isOk); if (isOk) data_.set_protocol_version_id(versionId); break; } case stp_bpdu_type: { quint8 bpdu = value.toUInt(&isOk); if (isOk) data_.set_bpdu_type(bpdu); break; } case stp_flags: { quint8 flags = value.toUInt(&isOk); if (isOk) data_.set_flags(flags); break; } case stp_root_id: { quint64 rootId = value.toULongLong(&isOk); if (isOk) data_.set_root_id(rootId); break; } case stp_root_path_cost: { quint32 pathCost = value.toUInt(&isOk); if (isOk) data_.set_root_path_cost(pathCost); break; } case stp_bridge_id: { quint64 bridgeId = value.toULongLong(&isOk); if (isOk) data_.set_bridge_id(bridgeId); break; } case stp_port_id: { quint32 port_id = value.toUInt(&isOk); if (isOk) data_.set_port_id(port_id); break; } case stp_message_age: { quint32 messageAge = value.toUInt(&isOk); if (isOk) data_.set_message_age(messageAge); break; } case stp_max_age: { quint32 maxAge = value.toUInt(&isOk); if (isOk) data_.set_max_age(maxAge); break; } case stp_hello_time: { quint32 helloTime = value.toUInt(&isOk); if (isOk) data_.set_hello_time(helloTime); break; } case stp_forward_delay: { quint32 forwardDelay = value.toUInt(&isOk); if (isOk) data_.set_forward_delay(forwardDelay); break; } default: break; } return isOk; } ostinato-1.3.0/common/stp.h000066400000000000000000000105031451413623100156210ustar00rootroot00000000000000/* Copyright (C) 2014 PLVision. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see This module is developed by PLVision */ #ifndef _STP_H #define _STP_H #include "abstractprotocol.h" #include "stp.pb.h" /* Stp Protocol Frame Format - +------------------------------------+------------------+------------------+ | Protocol ID | Protocol VID | BPDU type | | (16) | (8) | (8) | +------------------+-----------------+------------------+------------------+ | Flags | Root Identifier ->| | (8) | (64) ->| +------------------+-------------------------------------------------------+ |-> Root Identifier ->| |-> (64) ->| +------------------+-------------------------------------------------------+ |-> Root Identifier| Root Path Cost ->| |-> (8) | (32) ->| +------------------+-------------------------------------------------------+ |-> Root Path Cost | Bridge Identifier ->| |-> (32) | (64) ->| +------------------+-------------------------------------------------------+ |-> Bridge Identifier ->| |-> (64) ->| +------------------+--------------------------------- --+------------------+ |-> Bridge Identif.| Port Identifier | Message Age ->| |-> (64) | (16) | (16) ->| +------------------+------------------------------------+------------------+ |-> Message Age | Max Age | Hello Time ->| |-> (16) | (16) | (16) ->| +------------------+------------------------------------+------------------+ |-> Hello Time | Forward delay | |-> (16) | (16) | +------------------+------------------------------------+ Figures in brackets represent field width in bits */ class StpProtocol : public AbstractProtocol { public: enum stpfield { stp_protocol_id = 0, stp_version_id, stp_bpdu_type, stp_flags, stp_root_id, stp_root_path_cost, stp_bridge_id, stp_port_id, stp_message_age, stp_max_age, stp_hello_time, stp_forward_delay, stp_fieldCount }; StpProtocol(StreamBase *stream, AbstractProtocol *parent = 0); virtual ~StpProtocol(); static AbstractProtocol* createInstance(StreamBase *stream, AbstractProtocol *parent = 0); virtual quint32 protocolNumber() const; virtual void protoDataCopyInto(OstProto::Protocol &protocol) const; virtual void protoDataCopyFrom(const OstProto::Protocol &protocol); virtual quint32 protocolId(ProtocolIdType type) const; virtual QString name() const; virtual QString shortName() const; virtual int fieldCount() const; virtual AbstractProtocol::FieldFlags fieldFlags(int index) const; virtual QVariant fieldData(int index, FieldAttrib attrib, int streamIndex = 0) const; virtual bool setFieldData(int index, const QVariant &value, FieldAttrib attrib = FieldValue); private: OstProto::Stp data_; }; #endif ostinato-1.3.0/common/stp.proto000066400000000000000000000025671451413623100165500ustar00rootroot00000000000000/* Copyright (C) 2014 PLVision. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see This module is developed by PLVision */ import "protocol.proto"; package OstProto; // Spanning Tree Protocol message Stp { optional uint32 protocol_id = 1 [default = 0x0000]; optional uint32 protocol_version_id = 2 [default = 0x00]; optional uint32 bpdu_type = 3 [default = 0x00]; optional uint32 flags = 4; optional uint64 root_id = 5; optional uint32 root_path_cost = 6; optional uint64 bridge_id = 7; optional uint32 port_id = 8; optional uint32 message_age = 9; optional uint32 max_age = 10; optional uint32 hello_time = 11; optional uint32 forward_delay = 12; } extend Protocol { optional Stp stp = 209; } ostinato-1.3.0/common/stp.ui000066400000000000000000000343401451413623100160140ustar00rootroot00000000000000 Stp 0 0 615 378 Form 0 Protocol Identifier Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter true 5 0 Version Identifier Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter ui_version_id true 3 0 3 BPDU Type Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter 0 0 TC TCA Qt::LeftToRight BPDU Flags Root Identifier 9 9 Priority 5 MAC Address Path Cost 0 0 10 0 0 Bridge Identifier 0 0 Priority MAC Address 5 Port Identifier 9 9 3 Number Priority 3 Timers Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter false 0 9 0 Message Age (1/256s) 5 Max Age (1/256s) 5 0 0 Hello Time (1/256s) 5 Forward Delay (1/256s) 5 Qt::Vertical 20 40 groupBox_4 ui_protocol_id ui_version_id ui_bpdu_type ui_flags_tc_check ui_flags_tca_check ui_root_id_priority ui_root_id ui_root_path_cost ui_bridge_id_priority ui_bridge_id ui_port_id_priority ui_port_id_number ui_message_age ui_max_age ui_hello_time ui_forward_delay MacEdit QLineEdit
macedit.h
ostinato-1.3.0/common/stpconfig.cpp000066400000000000000000000202361451413623100173460ustar00rootroot00000000000000/* Copyright (C) 2014 PLVision. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see This module is developed by PLVision */ #include "stpconfig.h" #include "stp.h" #include #include #define ONE_BYTE_MAX 255 #define TWO_BYTE_MAX 65535 #define FOUR_BYTE_MAX 4294967295U #define BIT_0 0 #define BIT_7 7 #define ONE_BIT(pos) ((unsigned int)(1 << (pos))) #define BYTES(byte) (byte * sizeof(unsigned char)) #define STR_BYTES_LEN(len) (BYTES(len) * 2) class UNumberValidator : public QValidator { private: quint64 min_; quint64 max_; public: UNumberValidator(quint64 min, quint64 max, QObject * parent = 0) : QValidator(parent), min_(min), max_(max){} virtual ~UNumberValidator(){} virtual QValidator::State validate(QString& input, int& /*pos*/) const { QValidator::State state = QValidator::Acceptable; quint64 val = input.toULongLong(); if(val < min_ || val > max_) state = QValidator::Invalid; return state; } }; StpConfigForm::StpConfigForm(QWidget *parent) : AbstractProtocolConfigForm(parent) { QRegExp reMac("([0-9,a-f,A-F]{2,2}[:-]){5,5}[0-9,a-f,A-F]{2,2}"); setupUi(this); QRegExpValidator *validateMACAddress = new QRegExpValidator(reMac, this); UNumberValidator *validateByte = new UNumberValidator(0, ONE_BYTE_MAX, this); UNumberValidator *validate2Byte = new UNumberValidator(0, TWO_BYTE_MAX, this); UNumberValidator *validate4Byte = new UNumberValidator(0, FOUR_BYTE_MAX, this); ui_protocol_id->setValidator(validate2Byte); ui_version_id->setValidator(validateByte); ui_bpdu_type->setValidator(validateByte); ui_root_id_priority->setValidator(validate2Byte); ui_root_id->setValidator(validateMACAddress); ui_root_path_cost->setValidator(validate4Byte); ui_bridge_id_priority->setValidator(validate2Byte); ui_bridge_id->setValidator(validateMACAddress); ui_port_id_priority->setValidator(validateByte); ui_port_id_number->setValidator(validateByte); ui_message_age->setValidator(validate2Byte); ui_max_age->setValidator(validate2Byte); ui_hello_time->setValidator(validate2Byte); ui_forward_delay->setValidator(validate2Byte); } StpConfigForm::~StpConfigForm() { } StpConfigForm* StpConfigForm::createInstance() { return new StpConfigForm; } void StpConfigForm::loadWidget(AbstractProtocol *proto) { bool isOk; ui_protocol_id->setText( proto->fieldData( StpProtocol::stp_protocol_id, AbstractProtocol::FieldValue ).toString()); ui_version_id->setText( proto->fieldData( StpProtocol::stp_version_id, AbstractProtocol::FieldValue ).toString()); ui_bpdu_type->setText( proto->fieldData( StpProtocol::stp_bpdu_type, AbstractProtocol::FieldValue ).toString()); quint8 flags = proto->fieldData( StpProtocol::stp_flags, AbstractProtocol::FieldValue ).toUInt(); ui_flags_tc_check->setChecked(flags & ONE_BIT(BIT_0)); ui_flags_tca_check->setChecked(flags & ONE_BIT(BIT_7)); // root priority value stored as the first two bytes of stp_root_id // and the last 6 bytes are root MAC address (IEEE802.1D-2008) quint64 rootId = proto->fieldData( StpProtocol::stp_root_id, AbstractProtocol::FieldValue ).toULongLong(&isOk); ui_root_id->setValue(rootId & 0x0000FFFFFFFFFFFFULL); ui_root_id_priority->setText(QString::number(rootId >> 48)); ui_root_path_cost->setText( proto->fieldData( StpProtocol::stp_root_path_cost, AbstractProtocol::FieldValue ).toString()); // bridge priority value stored as the first two bytes of stp_bridge_id // and the last 6 bytes are bridge MAC address (IEEE802.1D-2008) quint64 bridgeId = proto->fieldData( StpProtocol::stp_bridge_id, AbstractProtocol::FieldValue ).toULongLong(&isOk); ui_bridge_id->setValue(bridgeId & 0x0000FFFFFFFFFFFFULL); ui_bridge_id_priority->setText(QString::number(bridgeId >> 48)); // port priority is a first byte of stp_port_id field // and port ID is a second byte (IEEE802.1D-2008) uint portId = proto->fieldData( StpProtocol::stp_port_id, AbstractProtocol::FieldValue ).toUInt(&isOk); ui_port_id_priority->setText(QString::number(portId >> 8)); ui_port_id_number->setText(QString::number(portId & ONE_BYTE_MAX)); ui_message_age->setText( proto->fieldData( StpProtocol::stp_message_age, AbstractProtocol::FieldValue ).toString()); ui_max_age->setText( proto->fieldData( StpProtocol::stp_max_age, AbstractProtocol::FieldValue ).toString()); ui_hello_time->setText( proto->fieldData( StpProtocol::stp_hello_time, AbstractProtocol::FieldValue ).toString()); ui_forward_delay->setText( proto->fieldData( StpProtocol::stp_forward_delay, AbstractProtocol::FieldValue ).toString()); } void StpConfigForm::storeWidget(AbstractProtocol *proto) { bool isOk; proto->setFieldData( StpProtocol::stp_protocol_id, QString("%1").arg( ui_protocol_id->text().toUInt(&isOk) & TWO_BYTE_MAX)); proto->setFieldData( StpProtocol::stp_version_id, ui_version_id->text()); proto->setFieldData( StpProtocol::stp_bpdu_type, ui_bpdu_type->text()); char flags = 0; if (ui_flags_tc_check->isChecked()) flags = flags | ONE_BIT(BIT_0); if (ui_flags_tca_check->isChecked()) flags = flags | ONE_BIT(BIT_7); proto->setFieldData(StpProtocol::stp_flags, flags); // root priority value stored as the first two bytes of stp_root_id // and the last 6 bytes are root MAC address (IEEE802.1D-2008) quint64 rootIdPrio = ui_root_id_priority->text() .toULongLong(&isOk) & TWO_BYTE_MAX; quint64 rootId = ui_root_id->value() | (rootIdPrio << 48); proto->setFieldData(StpProtocol::stp_root_id, rootId); proto->setFieldData( StpProtocol::stp_root_path_cost, ui_root_path_cost->text()); // bridge priority value stored as the first two bytes of stp_bridge_id // and the last 6 bytes are bridge MAC address (IEEE802.1D-2008) quint64 bridgeIdPrio = ui_bridge_id_priority->text().toULongLong(&isOk) & TWO_BYTE_MAX; quint64 bridgeId = ui_bridge_id->value() | (bridgeIdPrio << 48); proto->setFieldData(StpProtocol::stp_bridge_id, bridgeId); // port priority is a first byte of stp_port_id field // and port ID is a second byte (IEEE802.1D-2008) ushort portIdPrio = ui_port_id_priority->text().toUInt(&isOk, BASE_DEC) & ONE_BYTE_MAX; ushort portId = ui_port_id_number->text().toUInt(&isOk, BASE_DEC) & ONE_BYTE_MAX; proto->setFieldData(StpProtocol::stp_port_id, portIdPrio << 8 | portId); // timers proto->setFieldData( StpProtocol::stp_message_age, ui_message_age->text().toUInt(&isOk, BASE_DEC) & TWO_BYTE_MAX); proto->setFieldData( StpProtocol::stp_max_age, QString("%1").arg( ui_max_age->text().toUInt(&isOk, BASE_DEC) & TWO_BYTE_MAX)); proto->setFieldData( StpProtocol::stp_hello_time, QString("%1").arg( ui_hello_time->text().toUInt(&isOk, BASE_DEC) & TWO_BYTE_MAX)); proto->setFieldData( StpProtocol::stp_forward_delay, QString("%1").arg( ui_forward_delay->text().toUInt(&isOk, BASE_DEC) & TWO_BYTE_MAX)); } ostinato-1.3.0/common/stpconfig.h000066400000000000000000000022331451413623100170100ustar00rootroot00000000000000/* Copyright (C) 2014 PLVision. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see This module is developed by PLVision */ #ifndef _STP_CONFIG_H #define _STP_CONFIG_H #include "abstractprotocolconfig.h" #include "ui_stp.h" class StpConfigForm : public AbstractProtocolConfigForm, private Ui::Stp { Q_OBJECT public: StpConfigForm(QWidget *parent = 0); virtual ~StpConfigForm(); static StpConfigForm* createInstance(); virtual void loadWidget(AbstractProtocol *proto); virtual void storeWidget(AbstractProtocol *proto); }; #endif ostinato-1.3.0/common/stppdml.cpp000066400000000000000000000054561451413623100170440ustar00rootroot00000000000000/* Copyright (C) 2014 PLVision. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see This module is developed by PLVision */ #include "stppdml.h" #include "stp.pb.h" #define ROOT_IDENTIFIER_POS 5 #define BRIDGE_IDENTIFIER_POS 17 #define BASE_DEC 10 #define BASE_HEX 16 PdmlStpProtocol::PdmlStpProtocol() { ostProtoId_ = OstProto::Protocol::kStpFieldNumber; fieldMap_.insert("stp.protocol", OstProto::Stp::kProtocolIdFieldNumber); fieldMap_.insert("stp.version", OstProto::Stp::kProtocolVersionIdFieldNumber); fieldMap_.insert("stp.type", OstProto::Stp::kBpduTypeFieldNumber); fieldMap_.insert("stp.flags", OstProto::Stp::kFlagsFieldNumber); fieldMap_.insert("stp.root.cost", OstProto::Stp::kRootPathCostFieldNumber); fieldMap_.insert("stp.port", OstProto::Stp::kPortIdFieldNumber); fieldMap_.insert("stp.msg_age", OstProto::Stp::kMessageAgeFieldNumber); fieldMap_.insert("stp.max_age", OstProto::Stp::kMaxAgeFieldNumber); fieldMap_.insert("stp.hello", OstProto::Stp::kHelloTimeFieldNumber); fieldMap_.insert("stp.forward", OstProto::Stp::kForwardDelayFieldNumber); } PdmlStpProtocol::~PdmlStpProtocol() { } PdmlProtocol* PdmlStpProtocol::createInstance() { return new PdmlStpProtocol(); } void PdmlStpProtocol::preProtocolHandler(QString /*name*/, const QXmlStreamAttributes& attributes, int /*expectedPos*/, OstProto::Protocol* /*pbProto*/, OstProto::Stream* /*stream*/) { _protoStartPos = attributes.value("pos").toUInt(); } void PdmlStpProtocol::unknownFieldHandler( QString name, int pos, int /*size*/, const QXmlStreamAttributes &attributes, OstProto::Protocol *pbProto, OstProto::Stream* /*stream*/) { bool isOk; OstProto::Stp *stp = pbProto->MutableExtension(OstProto::stp); if ((name == "") && (relativePos(pos) == ROOT_IDENTIFIER_POS)) { stp->set_root_id(attributes.value("value").toString(). toULongLong(&isOk, BASE_HEX)); } if ((name == "") && (relativePos(pos) == BRIDGE_IDENTIFIER_POS)) { stp->set_bridge_id(attributes.value("value").toString(). toULongLong(&isOk, BASE_HEX)); } } ostinato-1.3.0/common/stppdml.h000066400000000000000000000030001451413623100164700ustar00rootroot00000000000000/* Copyright (C) 2014 PLVision. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see This module is developed by PLVision */ #ifndef _STP_PDML_H #define _STP_PDML_H #include "pdmlprotocol.h" class PdmlStpProtocol : public PdmlProtocol { public: virtual ~PdmlStpProtocol(); static PdmlProtocol *createInstance(); void preProtocolHandler(QString name, const QXmlStreamAttributes& attributes, int expectedPos, OstProto::Protocol* pbProto, OstProto::Stream* stream); void unknownFieldHandler(QString name, int pos, int size, const QXmlStreamAttributes &attributes, OstProto::Protocol *pbProto, OstProto::Stream* stream); protected: PdmlStpProtocol(); private: int relativePos(int pos) { return pos - _protoStartPos; } int _protoStartPos{0}; }; #endif ostinato-1.3.0/common/streambase.cpp000066400000000000000000000526761451413623100175150ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #include "streambase.h" #include "abstractprotocol.h" #include "framevalueattrib.h" #include "protocollist.h" #include "protocollistiterator.h" #include "protocolmanager.h" #include "uint128.h" #include extern ProtocolManager *OstProtocolManager; extern quint64 getDeviceMacAddress(int portId, int streamId, int frameIndex); extern quint64 getNeighborMacAddress(int portId, int streamId, int frameIndex); StreamBase::StreamBase(int portId) : portId_(portId), mStreamId(new OstProto::StreamId), mCore(new OstProto::StreamCore), mControl(new OstProto::StreamControl) { AbstractProtocol *proto; ProtocolListIterator *iter; mStreamId->set_id(0xFFFFFFFF); currentFrameProtocols = new ProtocolList; iter = createProtocolListIterator(); // By default newly created streams have the mac and payload protocols proto = OstProtocolManager->createProtocol( OstProto::Protocol::kMacFieldNumber, this); iter->insert(proto); qDebug("stream: mac = %p", proto); proto = OstProtocolManager->createProtocol( OstProto::Protocol::kPayloadFieldNumber, this); iter->insert(proto); qDebug("stream: payload = %p", proto); #ifndef QT_NO_DEBUG_OUTPUT { iter->toFront(); while (iter->hasNext()) { qDebug("{{%p}}", iter->next()); // qDebug("{{%p}: %d}", iter->peekNext(), iter->next()->protocolNumber()); } iter->toFront(); while (iter->hasNext()) { qDebug("{[%d]}", iter->next()->protocolNumber()); // qDebug("{{%p}: %d}", iter->peekNext(), iter->next()->protocolNumber()); } } #endif delete iter; } StreamBase::~StreamBase() { currentFrameProtocols->destroy(); delete currentFrameProtocols; delete mControl; delete mCore; delete mStreamId; } void StreamBase::protoDataCopyFrom(const OstProto::Stream &stream) { AbstractProtocol *proto; ProtocolListIterator *iter; mStreamId->CopyFrom(stream.stream_id()); mCore->CopyFrom(stream.core()); mControl->CopyFrom(stream.control()); currentFrameProtocols->destroy(); iter = createProtocolListIterator(); for (int i=0; i < stream.protocol_size(); i++) { int protoId = stream.protocol(i).protocol_id().id(); if (!OstProtocolManager->isRegisteredProtocol(protoId)) { qWarning("Skipping unregistered protocol %d", protoId); continue; } proto = OstProtocolManager->createProtocol(protoId, this); proto->commonProtoDataCopyFrom(stream.protocol(i)); proto->protoDataCopyFrom(stream.protocol(i)); iter->insert(proto); } delete iter; } void StreamBase::protoDataCopyInto(OstProto::Stream &stream) const { stream.mutable_stream_id()->CopyFrom(*mStreamId); stream.mutable_core()->CopyFrom(*mCore); stream.mutable_control()->CopyFrom(*mControl); stream.clear_protocol(); foreach (const AbstractProtocol* proto, *currentFrameProtocols) { OstProto::Protocol *p; p = stream.add_protocol(); proto->commonProtoDataCopyInto(*p); proto->protoDataCopyInto(*p); } } #if 0 ProtocolList StreamBase::frameProtocol() { return currentFrameProtocols; } void StreamBase::setFrameProtocol(ProtocolList protocolList) { //currentFrameProtocols.destroy(); currentFrameProtocols = protocolList; } #endif bool StreamBase::hasProtocol(quint32 protocolNumber) const { foreach(const AbstractProtocol *proto, *currentFrameProtocols) if (proto->protocolNumber() == protocolNumber) return true; return false; } ProtocolListIterator* StreamBase::createProtocolListIterator() const { return new ProtocolListIterator(*currentFrameProtocols); } quint32 StreamBase::id() const { return mStreamId->id(); } bool StreamBase::setId(quint32 id) { mStreamId->set_id(id); return true; } quint32 StreamBase::ordinal() { return mCore->ordinal(); } bool StreamBase::setOrdinal(quint32 ordinal) { mCore->set_ordinal(ordinal); return true; } bool StreamBase::isEnabled() const { return mCore->is_enabled(); } bool StreamBase::setEnabled(bool flag) { mCore->set_is_enabled(flag); return true; } const QString StreamBase::name() const { return QString().fromStdString(mCore->name()); } bool StreamBase::setName(QString name) { mCore->set_name(name.toStdString()); return true; } StreamBase::FrameLengthMode StreamBase::lenMode() const { return (StreamBase::FrameLengthMode) mCore->len_mode(); } bool StreamBase::setLenMode(FrameLengthMode lenMode) { mCore->set_len_mode((OstProto::StreamCore::FrameLengthMode) lenMode); return true; } quint16 StreamBase::frameLen(int streamIndex) const { int pktLen; // Decide a frame length based on length mode switch(lenMode()) { case e_fl_fixed: pktLen = mCore->frame_len(); break; case e_fl_inc: pktLen = frameLenMin() + (streamIndex % (frameLenMax() - frameLenMin() + 1)); break; case e_fl_dec: pktLen = frameLenMax() - (streamIndex % (frameLenMax() - frameLenMin() + 1)); break; case e_fl_random: //! \todo (MED) This 'random' sequence is same across iterations pktLen = 64; // to avoid the 'maybe used uninitialized' warning qsrand(reinterpret_cast(this)); for (int i = 0; i <= streamIndex; i++) pktLen = qrand(); pktLen = frameLenMin() + (pktLen % (frameLenMax() - frameLenMin() + 1)); break; case e_fl_imix: { // 64, 594, 1518 in 7:4:1 ratio // sizes mixed up intentionally below static int imixPattern[12] = {64, 594, 64, 594, 64, 1518, 64, 64, 594, 64, 594, 64}; pktLen = imixPattern[streamIndex % 12]; break; } default: qWarning("Unhandled len mode %d. Using default 64", lenMode()); pktLen = 64; break; } return pktLen; } bool StreamBase::setFrameLen(quint16 frameLen) { mCore->set_frame_len(frameLen); return true; } quint16 StreamBase::frameLenMin() const { return mCore->frame_len_min(); } bool StreamBase::setFrameLenMin(quint16 frameLenMin) { mCore->set_frame_len_min(frameLenMin); return true; } quint16 StreamBase::frameLenMax() const { return mCore->frame_len_max(); } bool StreamBase::setFrameLenMax(quint16 frameLenMax) { mCore->set_frame_len_max(frameLenMax); return true; } /*! Convenience Function */ quint16 StreamBase::frameLenAvg() const { quint16 avgFrameLen; if (lenMode() == e_fl_fixed) avgFrameLen = frameLen(); else if (lenMode() == e_fl_imix) avgFrameLen = (7*64 + 4*594 + 1*1518)/12; // 64,594,1518 in 7:4:1 ratio else avgFrameLen = (frameLenMin() + frameLenMax())/2; return avgFrameLen; } StreamBase::SendUnit StreamBase::sendUnit() const { return (StreamBase::SendUnit) mControl->unit(); } bool StreamBase::setSendUnit(SendUnit sendUnit) { mControl->set_unit((OstProto::StreamControl::SendUnit) sendUnit); return true; } StreamBase::SendMode StreamBase::sendMode() const { return (StreamBase::SendMode) mControl->mode(); } bool StreamBase::setSendMode(SendMode sendMode) { mControl->set_mode( (OstProto::StreamControl::SendMode) sendMode); return true; } StreamBase::NextWhat StreamBase::nextWhat() const { return (StreamBase::NextWhat) mControl->next(); } bool StreamBase::setNextWhat(NextWhat nextWhat) { mControl->set_next((OstProto::StreamControl::NextWhat) nextWhat); return true; } quint32 StreamBase::numPackets() const { return (quint32) mControl->num_packets(); } bool StreamBase::setNumPackets(quint32 numPackets) { mControl->set_num_packets(numPackets); return true; } quint32 StreamBase::numBursts() const { return (quint32) mControl->num_bursts(); } bool StreamBase::setNumBursts(quint32 numBursts) { mControl->set_num_bursts(numBursts); return true; } quint32 StreamBase::burstSize() const { return (quint32) mControl->packets_per_burst(); } bool StreamBase::setBurstSize(quint32 packetsPerBurst) { mControl->set_packets_per_burst(packetsPerBurst); return true; } double StreamBase::packetRate() const { return (double) mControl->packets_per_sec(); } bool StreamBase::setPacketRate(double packetsPerSec) { mControl->set_packets_per_sec(packetsPerSec); return true; } double StreamBase::burstRate() const { return (double) mControl->bursts_per_sec(); } bool StreamBase::setBurstRate(double burstsPerSec) { mControl->set_bursts_per_sec(burstsPerSec); return true; } /*! Convenience Function */ double StreamBase::averagePacketRate() const { double avgPacketRate = 0; switch (sendUnit()) { case e_su_bursts: avgPacketRate = burstRate() * burstSize(); break; case e_su_packets: avgPacketRate = packetRate(); break; default: Q_ASSERT(false); // Unreachable!! } return avgPacketRate; } /*! Convenience Function */ bool StreamBase::setAveragePacketRate(double packetsPerSec) { switch (sendUnit()) { case e_su_bursts: setBurstRate(packetsPerSec/double(burstSize())); break; case e_su_packets: setPacketRate(packetsPerSec); break; default: Q_ASSERT(false); // Unreachable!! } return true; } bool StreamBase::isFrameVariable() const { ProtocolListIterator *iter; iter = createProtocolListIterator(); while (iter->hasNext()) { AbstractProtocol *proto; proto = iter->next(); if (proto->isProtocolFrameValueVariable()) goto _exit; } delete iter; return false; _exit: delete iter; return true; } bool StreamBase::isFrameSizeVariable() const { ProtocolListIterator *iter; iter = createProtocolListIterator(); while (iter->hasNext()) { AbstractProtocol *proto; proto = iter->next(); if (proto->isProtocolFrameSizeVariable()) goto _exit; } delete iter; return false; _exit: delete iter; return true; } int StreamBase::frameSizeVariableCount() const { int count = 1; switch(lenMode()) { case e_fl_fixed: break; case e_fl_inc: case e_fl_dec: case e_fl_random: count = qMin(frameLenMax() - frameLenMin() + 1, frameCount()); break; case e_fl_imix: count = 12; // 7:4:1 ratio, so 7+4+1 break; default: qWarning("%s: Unhandled len mode %d", __FUNCTION__, lenMode()); break; } return count; } int StreamBase::frameVariableCount() const { ProtocolListIterator *iter; quint64 frameCount = 1; iter = createProtocolListIterator(); while (iter->hasNext()) { AbstractProtocol *proto; int count; proto = iter->next(); count = proto->protocolFrameVariableCount(); // correct count for mis-behaving protocols if (count <= 0) count = 1; frameCount = AbstractProtocol::lcm(frameCount, count); } delete iter; return AbstractProtocol::lcm(frameCount, frameSizeVariableCount()); } // frameProtocolLength() returns the sum of all the individual protocol sizes // which may be different from frameLen() int StreamBase::frameProtocolLength(int frameIndex) const { int len = 0; ProtocolListIterator *iter = createProtocolListIterator(); while (iter->hasNext()) { AbstractProtocol *proto = iter->next(); len += proto->protocolFrameSize(frameIndex); } delete iter; return len; } int StreamBase::frameCount() const { int count = 0; switch (sendUnit()) { case e_su_packets: count = numPackets(); break; case e_su_bursts: count = numBursts() * burstSize(); break; default: Q_ASSERT(false); // unreachable } return count; } // Returns packet length - if bufMaxSize < frameLen(), returns truncated // length i.e. bufMaxSize int StreamBase::frameValue(uchar *buf, int bufMaxSize, int frameIndex, FrameValueAttrib *attrib) const { int maxSize, size, pktLen, len = 0; pktLen = frameLen(frameIndex); // pktLen is adjusted for CRC/FCS which will be added by the NIC pktLen -= kFcsSize; if (pktLen <= 0) return 0; maxSize = qMin(pktLen, bufMaxSize); ProtocolListIterator *iter; iter = createProtocolListIterator(); while (iter->hasNext()) { AbstractProtocol *proto; QByteArray ba; FrameValueAttrib protoAttrib; proto = iter->next(); ba = proto->protocolFrameValue(frameIndex, false, &protoAttrib); if (attrib) *attrib += protoAttrib; size = qMin(ba.size(), maxSize-len); memcpy(buf+len, ba.constData(), size); len += size; if (len == maxSize) break; } delete iter; // Pad with zero, if required and if we have space if (len < maxSize) { size = maxSize-len; memset(buf+len, 0, size); len += size; } return len; } template int StreamBase::findReplace(quint32 protocolNumber, int fieldIndex, QVariant findValue, QVariant findMask, QVariant replaceValue, QVariant replaceMask) { int replaceCount = 0; ProtocolListIterator *iter = createProtocolListIterator(); // FIXME: Because protocol list iterator is unaware of combo protocols // search for ip4.src will NOT succeed in a combo protocol containing ip4 while (iter->hasNext()) { AbstractProtocol *proto = iter->next(); if (proto->protocolNumber() != protocolNumber) continue; T fieldValue = proto->fieldData(fieldIndex, AbstractProtocol::FieldValue).value(); qDebug() << "findReplace:" << "stream" << mStreamId->id() << "field" << fieldValue << "findMask" << hex << findMask.value() << dec << "findValue" << findValue.value(); if ((fieldValue & findMask.value()) == findValue.value()) { T newValue = (fieldValue & ~replaceMask.value()) | (replaceValue.value() & replaceMask.value()); qDebug() << "findReplace:" << "replaceMask" << hex << replaceMask.value() << dec << "replaceValue" << replaceValue.value() << "newValue" << newValue; QVariant nv; nv.setValue(newValue); if (proto->setFieldData(fieldIndex, nv)) replaceCount++; } } delete iter; return replaceCount; } int StreamBase::protocolFieldReplace(quint32 protocolNumber, int fieldIndex, int fieldBitSize, QVariant findValue, QVariant findMask, QVariant replaceValue, QVariant replaceMask) { if (fieldBitSize <= 64) return findReplace(protocolNumber, fieldIndex, findValue, findMask, replaceValue, replaceMask); if (fieldBitSize == 128) return findReplace(protocolNumber, fieldIndex, findValue, findMask, replaceValue, replaceMask); qWarning("Unknown find/replace type %d", findValue.type()); return 0; } quint64 StreamBase::deviceMacAddress(int frameIndex) const { return getDeviceMacAddress(portId_, int(mStreamId->id()), frameIndex); } quint64 StreamBase::neighborMacAddress(int frameIndex) const { return getNeighborMacAddress(portId_, int(mStreamId->id()), frameIndex); } /*! Checks for any potential errors with the packets generated by this stream. Returns true if no problems are found, false otherwise. Details of the error(s) are available in the INOUT param result All errors found are returned. However, each type of error is reported only once, even if multiple packets may have that error. */ bool StreamBase::preflightCheck(QStringList &result) const { bool pass = true; bool chkShort = true; bool chkTrunc = true; bool chkJumbo = true; bool chkSignIcmp = true; int count = isFrameSizeVariable() ? frameSizeVariableCount() : 1; for (int i = 0; i < count; i++) { int pktLen = frameLen(i); if (chkShort && hasProtocol(OstProto::Protocol::kSignFieldNumber) && (pktLen > (frameProtocolLength(i) + kFcsSize))) { result << QObject::tr("Stream statistics may not work since " "frame content < 64 bytes and hence will get padded - " "make sure special signature is at the end of the " "frame and frame content ≥ 64 bytes"); chkShort = false; pass = false; } if (chkSignIcmp && hasProtocol(OstProto::Protocol::kSignFieldNumber) && hasProtocol(OstProto::Protocol::kIcmpFieldNumber)) { result << QObject::tr("Stream statistics are not supported " "for ICMP packets - please use a non-ICMP protocol or " "remove special signature from ICMP streams"); chkSignIcmp = false; pass = false; } if (chkTrunc && (pktLen < (frameProtocolLength(i) + kFcsSize))) { result << QObject::tr("One or more frames may be truncated - " "frame length should be at least %1") .arg(frameProtocolLength(i) + kFcsSize); chkTrunc = false; pass = false; } if (chkJumbo && (pktLen > 1522)) { result << QObject::tr("Jumbo frames may be truncated or dropped " "if not supported by the hardware"); chkJumbo = false; pass = false; } // Break out of loop if we've seen at least one instance of all // the above errors if (!chkTrunc && !chkJumbo) break; } ProtocolListIterator *iter = createProtocolListIterator(); while (iter->hasNext()) { QStringList errors; AbstractProtocol *proto = iter->next(); if (proto->hasErrors(&errors)) { result += errors; pass = false; } } delete iter; if (isFrameVariable()) { if (frameVariableCount() > frameCount()) { if (frameCount() == 1) { result << QObject::tr("Variable fields won't change since " "only 1 frame%1 is configured to be transmitted - " "increase number of packets to %L2 to have variable " "fields change across the configured range") .arg(sendUnit() == e_su_bursts ? " (number of bursts * packets per burst)" : "") .arg(frameVariableCount()); pass = false; } else if (frameCount() > 1) { result << QObject::tr("Variable fields will change for " "%L1 counts since only %L1 frames%2 are configured " "to be transmitted - increase number of packets " "from %L1 to %L3 to have variable fields change " "across the configured range") .arg(frameCount()) .arg(sendUnit() == e_su_bursts ? " (number of bursts * packets per burst)" : "") .arg(frameVariableCount()); pass = false; } } } #if 0 // see XXX note below // XXX: This causes false positives for - // * interleaved streams (a port property that we don't have access to) // * pcap imported streams where each stream has only one packet // Ideally we need to get the transmit duration for all the streams // to perform this check if (frameCount() < averagePacketRate() && nextWhat() != e_nw_goto_id) { result << QObject::tr("Only %L1 frames at the rate of " "%L2 frames/sec are configured to be transmitted. " "Transmission will last for only %L3 second - " "to transmit for a longer duration, " "increase the number of packets (bursts) and/or " "set the 'After this stream' action as 'Goto First'") .arg(frameCount()) .arg(averagePacketRate(), 0, 'f', 2) .arg(frameCount()/averagePacketRate(), 0, 'f'); pass = false; } #endif return pass; } bool StreamBase::StreamLessThan(StreamBase* stream1, StreamBase* stream2) { return stream1->ordinal() < stream2->ordinal() ? true : false; } ostinato-1.3.0/common/streambase.h000066400000000000000000000105471451413623100171510ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _STREAM_BASE_H #define _STREAM_BASE_H #include #include #include #include "protocol.pb.h" const int kFcsSize = 4; class AbstractProtocol; struct FrameValueAttrib; class ProtocolList; class ProtocolListIterator; class StreamBase { public: StreamBase(int portId = -1); ~StreamBase(); void protoDataCopyFrom(const OstProto::Stream &stream); void protoDataCopyInto(OstProto::Stream &stream) const; bool hasProtocol(quint32 protocolNumber) const; ProtocolListIterator* createProtocolListIterator() const; //! \todo (LOW) should we have a copy constructor?? public: enum FrameLengthMode { e_fl_fixed, e_fl_inc, e_fl_dec, e_fl_random, e_fl_imix }; enum SendUnit { e_su_packets, e_su_bursts }; enum SendMode { e_sm_fixed, e_sm_continuous }; enum NextWhat { e_nw_stop, e_nw_goto_next, e_nw_goto_id }; quint32 id() const; bool setId(quint32 id); quint32 portId() { return portId_;} #if 0 // FIXME(HI): needed? bool setPortId(quint32 id) { mCore->set_port_id(id); return true;} #endif quint32 ordinal(); bool setOrdinal(quint32 ordinal); bool isEnabled() const; bool setEnabled(bool flag); const QString name() const ; bool setName(QString name) ; // Frame Length (includes FCS); FrameLengthMode lenMode() const; bool setLenMode(FrameLengthMode lenMode); quint16 frameLen(int streamIndex = 0) const; bool setFrameLen(quint16 frameLen); quint16 frameLenMin() const; bool setFrameLenMin(quint16 frameLenMin); quint16 frameLenMax() const; bool setFrameLenMax(quint16 frameLenMax); quint16 frameLenAvg() const; SendUnit sendUnit() const; bool setSendUnit(SendUnit sendUnit); SendMode sendMode() const; bool setSendMode(SendMode sendMode); NextWhat nextWhat() const; bool setNextWhat(NextWhat nextWhat); quint32 numPackets() const; bool setNumPackets(quint32 numPackets); quint32 numBursts() const; bool setNumBursts(quint32 numBursts); quint32 burstSize() const; bool setBurstSize(quint32 packetsPerBurst); double packetRate() const; bool setPacketRate(double packetsPerSec); double burstRate() const; bool setBurstRate(double burstsPerSec); double averagePacketRate() const; bool setAveragePacketRate(double packetsPerSec); bool isFrameVariable() const; bool isFrameSizeVariable() const; int frameSizeVariableCount() const; int frameVariableCount() const; int frameProtocolLength(int frameIndex) const; int frameCount() const; int frameValue(uchar *buf, int bufMaxSize, int frameIndex, FrameValueAttrib *attrib = nullptr) const; int protocolFieldReplace(quint32 protocolNumber, int fieldIndex, int fieldBitSize, QVariant findValue, QVariant findMask, QVariant replaceValue, QVariant replaceMask); quint64 deviceMacAddress(int frameIndex) const; quint64 neighborMacAddress(int frameIndex) const; bool preflightCheck(QStringList &result) const; static bool StreamLessThan(StreamBase* stream1, StreamBase* stream2); private: template int findReplace(quint32 protocolNumber, int fieldIndex, QVariant findValue, QVariant findMask, QVariant replaceValue, QVariant replaceMask); int portId_; OstProto::StreamId *mStreamId; OstProto::StreamCore *mCore; OstProto::StreamControl *mControl; ProtocolList *currentFrameProtocols; }; #endif ostinato-1.3.0/common/streamfileformat.cpp000066400000000000000000000057611451413623100207240ustar00rootroot00000000000000/* Copyright (C) 2011 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #include "streamfileformat.h" #include "ostmfileformat.h" #include "pcapfileformat.h" #include "pdmlfileformat.h" #include "pythonfileformat.h" #include StreamFileFormat::StreamFileFormat() { stop_ = false; } StreamFileFormat::~StreamFileFormat() { } QDialog* StreamFileFormat::openOptionsDialog() { return NULL; } QDialog* StreamFileFormat::saveOptionsDialog() { return NULL; } QStringList StreamFileFormat::supportedFileTypes(Operation op) { QStringList fileTypes; fileTypes << "Ostinato (*.ostm)" << "PCAP (*.pcap)" << "PDML (*.pdml)"; if (op == kSaveFile) fileTypes << "PythonScript (*.py)"; else if (op == kOpenFile) fileTypes << "All files (*)"; return fileTypes; } void StreamFileFormat::openAsync(const QString fileName, OstProto::StreamConfigList &streams, QString &error) { fileName_ = fileName; openStreams_ = &streams; error_ = &error; op_ = kOpenFile; stop_ = false; start(); } void StreamFileFormat::saveAsync( const OstProto::StreamConfigList streams, const QString fileName, QString &error) { saveStreams_ = streams; fileName_ = fileName; error_ = &error; op_ = kSaveFile; stop_ = false; start(); } bool StreamFileFormat::result() { return result_; } StreamFileFormat* StreamFileFormat::fileFormatFromFile( const QString fileName) { if (fileFormat.isMyFileFormat(fileName)) return &fileFormat; if (pdmlFileFormat.isMyFileFormat(fileName)) return &pdmlFileFormat; if (pcapFileFormat.isMyFileFormat(fileName)) return &pcapFileFormat; return NULL; } StreamFileFormat* StreamFileFormat::fileFormatFromType( const QString fileType) { if (fileFormat.isMyFileType(fileType)) return &fileFormat; if (pdmlFileFormat.isMyFileType(fileType)) return &pdmlFileFormat; if (pcapFileFormat.isMyFileType(fileType)) return &pcapFileFormat; if (pythonFileFormat.isMyFileType(fileType)) return &pythonFileFormat; return NULL; } void StreamFileFormat::cancel() { stop_ = true; } void StreamFileFormat::run() { if (op_ == kOpenFile) result_ = open(fileName_, *openStreams_, *error_); else if (op_ == kSaveFile) result_ = save(saveStreams_, fileName_, *error_); } ostinato-1.3.0/common/streamfileformat.h000066400000000000000000000043051451413623100203620ustar00rootroot00000000000000/* Copyright (C) 2011 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _STREAM_FILE_FORMAT_H #define _STREAM_FILE_FORMAT_H #include "protocol.pb.h" #include #include class QDialog; class StreamFileFormat : public QThread { Q_OBJECT public: enum Operation { kOpenFile, kSaveFile }; StreamFileFormat(); virtual ~StreamFileFormat(); virtual bool open(const QString fileName, OstProto::StreamConfigList &streams, QString &error) = 0; virtual bool save(const OstProto::StreamConfigList streams, const QString fileName, QString &error) = 0; virtual QDialog* openOptionsDialog(); virtual QDialog* saveOptionsDialog(); void openAsync(const QString fileName, OstProto::StreamConfigList &streams, QString &error); void saveAsync(const OstProto::StreamConfigList streams, const QString fileName, QString &error); bool result(); static QStringList supportedFileTypes(Operation op); static StreamFileFormat* fileFormatFromFile(const QString fileName); static StreamFileFormat* fileFormatFromType(const QString fileType); #if 0 bool isMyFileFormat(const QString fileName) = 0; bool isMyFileType(const QString fileType) = 0; #endif signals: void status(QString text); void target(int value); void progress(int value); public slots: void cancel(); protected: void run(); bool stop_; private: QString fileName_; OstProto::StreamConfigList *openStreams_; OstProto::StreamConfigList saveStreams_; QString *error_; Operation op_; bool result_; }; #endif ostinato-1.3.0/common/svlan.cpp000066400000000000000000000033711451413623100164760ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #include "svlan.h" #include "svlan.pb.h" SVlanProtocol::SVlanProtocol(StreamBase *stream, AbstractProtocol *parent) : VlanProtocol(stream, parent) { data.set_tpid(0x88a8); data.set_is_override_tpid(true); } SVlanProtocol::~SVlanProtocol() { } AbstractProtocol* SVlanProtocol::createInstance(StreamBase *stream, AbstractProtocol *parent) { return new SVlanProtocol(stream, parent); } quint32 SVlanProtocol::protocolNumber() const { return OstProto::Protocol::kSvlanFieldNumber; } void SVlanProtocol::protoDataCopyInto(OstProto::Protocol &protocol) const { protocol.MutableExtension(OstProto::svlan)->CopyFrom(data); protocol.mutable_protocol_id()->set_id(protocolNumber()); } void SVlanProtocol::protoDataCopyFrom(const OstProto::Protocol &protocol) { if (protocol.protocol_id().id() == protocolNumber() && protocol.HasExtension(OstProto::svlan)) data.MergeFrom(protocol.GetExtension(OstProto::svlan)); } QString SVlanProtocol::name() const { return QString("SVlan"); } QString SVlanProtocol::shortName() const { return QString("SVlan"); } ostinato-1.3.0/common/svlan.h000066400000000000000000000023511451413623100161400ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _SVLAN_H #define _SVLAN_H #include "vlan.h" class SVlanProtocol : public VlanProtocol { public: SVlanProtocol(StreamBase *stream, AbstractProtocol *parent = 0); virtual ~SVlanProtocol(); static AbstractProtocol* createInstance(StreamBase *stream, AbstractProtocol *parent = 0); virtual quint32 protocolNumber() const; virtual void protoDataCopyInto(OstProto::Protocol &protocol) const; virtual void protoDataCopyFrom(const OstProto::Protocol &protocol); virtual QString name() const; virtual QString shortName() const; }; #endif ostinato-1.3.0/common/svlan.proto000066400000000000000000000014271451413623100170570ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ import "protocol.proto"; import "vlan.proto"; package OstProto; extend Protocol { optional Vlan svlan = 204; } ostinato-1.3.0/common/svlanconfig.h000066400000000000000000000014351451413623100173300ustar00rootroot00000000000000/* Copyright (C) 2014 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _SVLAN_CONFIG_H #define _SVLAN_CONFIG_H #include "vlanconfig.h" typedef VlanConfigForm SVlanConfigForm; #endif ostinato-1.3.0/common/svlanpdml.cpp000066400000000000000000000074211451413623100173530ustar00rootroot00000000000000/* Copyright (C) 2011 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #include "svlanpdml.h" #include "eth2.pb.h" #include "svlan.pb.h" PdmlSvlanProtocol::PdmlSvlanProtocol() { ostProtoId_ = OstProto::Protocol::kSvlanFieldNumber; } PdmlProtocol* PdmlSvlanProtocol::createInstance() { return new PdmlSvlanProtocol(); } void PdmlSvlanProtocol::preProtocolHandler(QString /*name*/, const QXmlStreamAttributes& /*attributes*/, int /*expectedPos*/, OstProto::Protocol *pbProto, OstProto::Stream *stream) { OstProto::Vlan *svlan = pbProto->MutableExtension(OstProto::svlan); svlan->set_tpid(0x88a8); svlan->set_is_override_tpid(true); // If a eth2 protocol precedes svlan, we remove the eth2 protocol // 'coz the eth2.etherType is actually the svlan.tpid // // We assume that the current protocol is the last in the stream int index = stream->protocol_size() - 1; if ((index > 1) && (stream->protocol(index).protocol_id().id() == OstProto::Protocol::kSvlanFieldNumber) && (stream->protocol(index - 1).protocol_id().id() == OstProto::Protocol::kEth2FieldNumber)) { stream->mutable_protocol()->SwapElements(index, index - 1); Q_ASSERT(stream->protocol(index).protocol_id().id() == OstProto::Protocol::kEth2FieldNumber); stream->mutable_protocol()->RemoveLast(); } } void PdmlSvlanProtocol::unknownFieldHandler(QString name, int /*pos*/, int /*size*/, const QXmlStreamAttributes &attributes, OstProto::Protocol *pbProto, OstProto::Stream *stream) { if ((name == "ieee8021ad.id") || (name == "ieee8021ad.svid")) { bool isOk; OstProto::Vlan *svlan = pbProto->MutableExtension(OstProto::svlan); uint tag = attributes.value("unmaskedvalue").isEmpty() ? attributes.value("value").toString().toUInt(&isOk, kBaseHex) : attributes.value("unmaskedvalue").toString().toUInt(&isOk,kBaseHex); svlan->set_vlan_tag(tag); } else if (name == "ieee8021ad.cvid") { OstProto::Protocol *proto = stream->add_protocol(); proto->mutable_protocol_id()->set_id( OstProto::Protocol::kSvlanFieldNumber); OstProto::Vlan *svlan = proto->MutableExtension(OstProto::svlan); svlan->set_tpid(0x88a8); svlan->set_is_override_tpid(true); bool isOk; uint tag = attributes.value("unmaskedvalue").isEmpty() ? attributes.value("value").toString().toUInt(&isOk, kBaseHex) : attributes.value("unmaskedvalue").toString().toUInt(&isOk,kBaseHex); svlan->set_vlan_tag(tag); } else if (name == "ieee8021ah.etype") // yes 'ah' not 'ad' - not a typo! { OstProto::Protocol *proto = stream->add_protocol(); proto->mutable_protocol_id()->set_id( OstProto::Protocol::kEth2FieldNumber); bool isOk; OstProto::Eth2 *eth2 = proto->MutableExtension(OstProto::eth2); eth2->set_type(attributes.value("value") .toString().toUInt(&isOk, kBaseHex)); eth2->set_is_override_type(true); } } ostinato-1.3.0/common/svlanpdml.h000066400000000000000000000023721451413623100170200ustar00rootroot00000000000000/* Copyright (C) 2011 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _SVLAN_PDML_H #define _SVLAN_PDML_H #include "pdmlprotocol.h" class PdmlSvlanProtocol : public PdmlProtocol { public: static PdmlProtocol* createInstance(); virtual void preProtocolHandler(QString name, const QXmlStreamAttributes &attributes, int expectedPos, OstProto::Protocol *pbProto, OstProto::Stream *stream); virtual void unknownFieldHandler(QString name, int pos, int size, const QXmlStreamAttributes &attributes, OstProto::Protocol *pbProto, OstProto::Stream *stream); protected: PdmlSvlanProtocol(); }; #endif ostinato-1.3.0/common/tcp.cpp000066400000000000000000000415611451413623100161440ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #include "tcp.h" TcpProtocol::TcpProtocol(StreamBase *stream, AbstractProtocol *parent) : AbstractProtocol(stream, parent) { } TcpProtocol::~TcpProtocol() { } AbstractProtocol* TcpProtocol::createInstance(StreamBase *stream, AbstractProtocol *parent) { return new TcpProtocol(stream, parent); } quint32 TcpProtocol::protocolNumber() const { return OstProto::Protocol::kTcpFieldNumber; } void TcpProtocol::protoDataCopyInto(OstProto::Protocol &protocol) const { protocol.MutableExtension(OstProto::tcp)->CopyFrom(data); protocol.mutable_protocol_id()->set_id(protocolNumber()); } void TcpProtocol::protoDataCopyFrom(const OstProto::Protocol &protocol) { if (protocol.protocol_id().id() == protocolNumber() && protocol.HasExtension(OstProto::tcp)) data.MergeFrom(protocol.GetExtension(OstProto::tcp)); } QString TcpProtocol::name() const { return QString("Transmission Control Protocol (state less)"); } QString TcpProtocol::shortName() const { return QString("TCP"); } AbstractProtocol::ProtocolIdType TcpProtocol::protocolIdType() const { return ProtocolIdTcpUdp; } quint32 TcpProtocol::protocolId(ProtocolIdType type) const { switch(type) { case ProtocolIdIp: return 0x06; default: break; } return AbstractProtocol::protocolId(type); } int TcpProtocol::fieldCount() const { return tcp_fieldCount; } AbstractProtocol::FieldFlags TcpProtocol::fieldFlags(int index) const { AbstractProtocol::FieldFlags flags; flags = AbstractProtocol::fieldFlags(index); switch (index) { case tcp_src_port: case tcp_dst_port: case tcp_seq_num: case tcp_ack_num: case tcp_hdrlen: case tcp_rsvd: case tcp_flags: case tcp_window: break; case tcp_cksum: flags |= CksumField; break; case tcp_urg_ptr: break; case tcp_is_override_src_port: case tcp_is_override_dst_port: case tcp_is_override_hdrlen: case tcp_is_override_cksum: flags &= ~FrameField; flags |= MetaField; break; default: qFatal("%s: unimplemented case %d in switch", __PRETTY_FUNCTION__, index); break; } return flags; } QVariant TcpProtocol::fieldData(int index, FieldAttrib attrib, int streamIndex) const { switch (index) { case tcp_src_port: { quint16 srcPort; switch(attrib) { case FieldValue: case FieldFrameValue: case FieldTextValue: if (data.is_override_src_port()) srcPort = data.src_port(); else srcPort = payloadProtocolId(ProtocolIdTcpUdp); break; default: srcPort = 0; // avoid the 'maybe used unitialized' warning break; } switch(attrib) { case FieldName: return QString("Source Port"); case FieldValue: return srcPort; case FieldTextValue: return QString("%1").arg(srcPort); case FieldFrameValue: { QByteArray fv; fv.resize(2); qToBigEndian(srcPort, (uchar*) fv.data()); return fv; } default: break; } break; } case tcp_dst_port: { quint16 dstPort; switch(attrib) { case FieldValue: case FieldFrameValue: case FieldTextValue: if (data.is_override_dst_port()) dstPort = data.dst_port(); else dstPort = payloadProtocolId(ProtocolIdTcpUdp); break; default: dstPort = 0; // avoid the 'maybe used unitialized' warning break; } switch(attrib) { case FieldName: return QString("Destination Port"); case FieldValue: return dstPort; case FieldTextValue: return QString("%1").arg(dstPort); case FieldFrameValue: { QByteArray fv; fv.resize(2); qToBigEndian(dstPort, (uchar*) fv.data()); return fv; } default: break; } break; } case tcp_seq_num: switch(attrib) { case FieldName: return QString("Sequence Number"); case FieldValue: return data.seq_num(); case FieldTextValue: return QString("%1").arg(data.seq_num()); case FieldFrameValue: { QByteArray fv; fv.resize(4); qToBigEndian((quint32) data.seq_num(), (uchar*) fv.data()); return fv; } default: break; } break; case tcp_ack_num: switch(attrib) { case FieldName: return QString("Acknowledgement Number"); case FieldValue: return data.ack_num(); case FieldTextValue: return QString("%1").arg(data.ack_num()); case FieldFrameValue: { QByteArray fv; fv.resize(4); qToBigEndian((quint32) data.ack_num(), (uchar*) fv.data()); return fv; } default: break; } break; case tcp_hdrlen: switch(attrib) { case FieldName: return QString("Header Length"); case FieldValue: if (data.is_override_hdrlen()) return ((data.hdrlen_rsvd() >> 4) & 0x0F); else return 5; case FieldTextValue: if (data.is_override_hdrlen()) return QString("%1 bytes").arg( 4 * ((data.hdrlen_rsvd() >> 4) & 0x0F)); else return QString("20 bytes"); case FieldFrameValue: if (data.is_override_hdrlen()) return QByteArray(1, (char)((data.hdrlen_rsvd() >> 4) & 0x0F)); else return QByteArray(1, (char) 0x05); case FieldBitSize: return 4; default: break; } break; case tcp_rsvd: switch(attrib) { case FieldName: return QString("Reserved"); case FieldValue: return (data.hdrlen_rsvd() & 0x0F); case FieldTextValue: return QString("%1").arg(data.hdrlen_rsvd() & 0x0F); case FieldFrameValue: return QByteArray(1, (char)(data.hdrlen_rsvd() & 0x0F)); case FieldBitSize: return 4; default: break; } break; case tcp_flags: switch(attrib) { case FieldName: return QString("Flags"); case FieldValue: return (data.flags()); case FieldTextValue: { QString s; s.append("URG: "); s.append(data.flags() & TCP_FLAG_URG ? "1" : "0"); s.append(" ACK: "); s.append(data.flags() & TCP_FLAG_ACK ? "1" : "0"); s.append(" PSH: "); s.append(data.flags() & TCP_FLAG_PSH ? "1" : "0"); s.append(" RST: "); s.append(data.flags() & TCP_FLAG_RST ? "1" : "0"); s.append(" SYN: "); s.append(data.flags() & TCP_FLAG_SYN ? "1" : "0"); s.append(" FIN: "); s.append(data.flags() & TCP_FLAG_FIN ? "1" : "0"); return s; } case FieldFrameValue: return QByteArray(1, (char)(data.flags() & 0x3F)); default: break; } break; case tcp_window: switch(attrib) { case FieldName: return QString("Window Size"); case FieldValue: return data.window(); case FieldTextValue: return QString("%1").arg(data.window()); case FieldFrameValue: { QByteArray fv; fv.resize(2); qToBigEndian((quint16) data.window(), (uchar*) fv.data()); return fv; } default: break; } break; case tcp_cksum: switch(attrib) { case FieldName: return QString("Checksum"); case FieldValue: { quint16 cksum; if (data.is_override_cksum()) cksum = data.cksum(); else cksum = protocolFrameCksum(streamIndex, CksumTcpUdp); return cksum; } case FieldTextValue: { quint16 cksum; if (data.is_override_cksum()) cksum = data.cksum(); else cksum = protocolFrameCksum(streamIndex, CksumTcpUdp); return QString("0x%1").arg(cksum, 4, BASE_HEX, QChar('0')); } case FieldFrameValue: { quint16 cksum; if (data.is_override_cksum()) cksum = data.cksum(); else cksum = protocolFrameCksum(streamIndex, CksumTcpUdp); QByteArray fv; fv.resize(2); qToBigEndian(cksum, (uchar*) fv.data()); return fv; } case FieldBitSize: return 16; default: break; } break; case tcp_urg_ptr: switch(attrib) { case FieldName: return QString("Urgent Pointer"); case FieldValue: return data.urg_ptr(); case FieldTextValue: return QString("%1").arg(data.urg_ptr()); case FieldFrameValue: { QByteArray fv; fv.resize(2); qToBigEndian((quint16) data.urg_ptr(), (uchar*) fv.data()); return fv; } default: break; } break; // Meta fields case tcp_is_override_src_port: { switch(attrib) { case FieldValue: return data.is_override_src_port(); default: break; } break; } case tcp_is_override_dst_port: { switch(attrib) { case FieldValue: return data.is_override_dst_port(); default: break; } break; } case tcp_is_override_hdrlen: { switch(attrib) { case FieldValue: return data.is_override_hdrlen(); default: break; } break; } case tcp_is_override_cksum: { switch(attrib) { case FieldValue: return data.is_override_cksum(); default: break; } break; } default: qFatal("%s: unimplemented case %d in switch", __PRETTY_FUNCTION__, index); break; } return AbstractProtocol::fieldData(index, attrib, streamIndex); } bool TcpProtocol::setFieldData(int index, const QVariant &value, FieldAttrib attrib) { bool isOk = false; if (attrib != FieldValue) goto _exit; switch (index) { case tcp_src_port: { uint srcPort = value.toUInt(&isOk); if (isOk) data.set_src_port(srcPort); break; } case tcp_dst_port: { uint dstPort = value.toUInt(&isOk); if (isOk) data.set_dst_port(dstPort); break; } case tcp_seq_num: { uint seqNum = value.toUInt(&isOk); if (isOk) data.set_seq_num(seqNum); break; } case tcp_ack_num: { uint ackNum = value.toUInt(&isOk); if (isOk) data.set_ack_num(ackNum); break; } case tcp_hdrlen: { uint hdrLen = value.toUInt(&isOk); if (isOk) data.set_hdrlen_rsvd( (data.hdrlen_rsvd() & 0x0F) | (hdrLen << 4)); break; } case tcp_rsvd: { uint rsvd = value.toUInt(&isOk); if (isOk) data.set_hdrlen_rsvd( (data.hdrlen_rsvd() & 0xF0) | (rsvd & 0x0F)); break; } case tcp_flags: { uint flags = value.toUInt(&isOk); if (isOk) data.set_flags(flags); break; } case tcp_window: { uint window = value.toUInt(&isOk); if (isOk) data.set_window(window); break; } case tcp_cksum: { uint cksum = value.toUInt(&isOk); if (isOk) data.set_cksum(cksum); break; } case tcp_urg_ptr: { uint urgPtr = value.toUInt(&isOk); if (isOk) data.set_urg_ptr(urgPtr); break; } case tcp_is_override_src_port: { data.set_is_override_src_port(value.toBool()); isOk = true; break; } case tcp_is_override_dst_port: { data.set_is_override_dst_port(value.toBool()); isOk = true; break; } case tcp_is_override_hdrlen: { data.set_is_override_hdrlen(value.toBool()); isOk = true; break; } case tcp_is_override_cksum: { data.set_is_override_cksum(value.toBool()); isOk = true; break; } default: qFatal("%s: unimplemented case %d in switch", __PRETTY_FUNCTION__, index); break; } _exit: return isOk; } int TcpProtocol::protocolFrameVariableCount() const { int count = AbstractProtocol::protocolFrameVariableCount(); if (!data.is_override_cksum()) count = AbstractProtocol::lcm(count, protocolFramePayloadVariableCount()); return count; } ostinato-1.3.0/common/tcp.h000066400000000000000000000045131451413623100156050ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _TCP_H #define _TCP_H #include "abstractprotocol.h" #include "tcp.pb.h" #define TCP_FLAG_URG 0x20 #define TCP_FLAG_ACK 0x10 #define TCP_FLAG_PSH 0x08 #define TCP_FLAG_RST 0x04 #define TCP_FLAG_SYN 0x02 #define TCP_FLAG_FIN 0x01 class TcpProtocol : public AbstractProtocol { public: enum tcpfield { tcp_src_port = 0, tcp_dst_port, tcp_seq_num, tcp_ack_num, tcp_hdrlen, tcp_rsvd, tcp_flags, tcp_window, tcp_cksum, tcp_urg_ptr, tcp_is_override_src_port, tcp_is_override_dst_port, tcp_is_override_hdrlen, tcp_is_override_cksum, tcp_fieldCount }; TcpProtocol(StreamBase *stream, AbstractProtocol *parent = 0); virtual ~TcpProtocol(); static AbstractProtocol* createInstance(StreamBase *stream, AbstractProtocol *parent = 0); virtual quint32 protocolNumber() const; virtual void protoDataCopyInto(OstProto::Protocol &protocol) const; virtual void protoDataCopyFrom(const OstProto::Protocol &protocol); virtual QString name() const; virtual QString shortName() const; virtual ProtocolIdType protocolIdType() const; virtual quint32 protocolId(ProtocolIdType type) const; virtual int fieldCount() const; virtual AbstractProtocol::FieldFlags fieldFlags(int index) const; virtual QVariant fieldData(int index, FieldAttrib attrib, int streamIndex = 0) const; virtual bool setFieldData(int index, const QVariant &value, FieldAttrib attrib = FieldValue); virtual int protocolFrameVariableCount() const; private: OstProto::Tcp data; }; #endif ostinato-1.3.0/common/tcp.proto000066400000000000000000000025251451413623100165220ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ import "protocol.proto"; package OstProto; // Tcp message Tcp { optional bool is_override_src_port = 1; optional bool is_override_dst_port = 2; optional bool is_override_hdrlen = 3; optional bool is_override_cksum = 4; optional uint32 src_port = 5 [default = 49152]; optional uint32 dst_port = 6 [default = 49153]; optional uint32 seq_num = 7 [default = 129018]; optional uint32 ack_num = 8; optional uint32 hdrlen_rsvd = 9 [default = 0x50]; optional uint32 flags = 10; optional uint32 window = 11 [default = 1024]; optional uint32 cksum = 12; optional uint32 urg_ptr = 13; } extend Protocol { optional Tcp tcp = 400; } ostinato-1.3.0/common/tcp.ui000066400000000000000000000164501451413623100157760ustar00rootroot00000000000000 tcp 0 0 447 194 Form Override Source Port false Qt::Vertical Override Checksum false >HH HH; Override Destination Port false Urgent Pointer Sequence Number Flags URG ACK PSH RST SYN FIN Qt::Horizontal 21 20 Acknowledgement Number Override Header Length (x4) false Window Qt::Vertical 20 40 <i>Note: Ostinato is stateless- it cannot establish a TCP connection and generate seq/ack numbers accordingly</i> true Qt::Vertical 20 40 cbTcpHdrLenOverride toggled(bool) leTcpHdrLen setEnabled(bool) 141 123 187 123 cbTcpCksumOverride toggled(bool) leTcpCksum setEnabled(bool) 316 14 384 17 cbTcpSrcPortOverride toggled(bool) leTcpSrcPort setEnabled(bool) 159 16 178 18 cbTcpDstPortOverride toggled(bool) leTcpDstPort setEnabled(bool) 147 45 180 44 ostinato-1.3.0/common/tcpconfig.cpp000066400000000000000000000125611451413623100173300ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #include "tcpconfig.h" #include "tcp.h" TcpConfigForm::TcpConfigForm(QWidget *parent) : AbstractProtocolConfigForm(parent) { setupUi(this); } TcpConfigForm::~TcpConfigForm() { } TcpConfigForm* TcpConfigForm::createInstance() { return new TcpConfigForm; } void TcpConfigForm::loadWidget(AbstractProtocol *proto) { leTcpSrcPort->setText( proto->fieldData( TcpProtocol::tcp_src_port, AbstractProtocol::FieldValue ).toString()); cbTcpSrcPortOverride->setChecked( proto->fieldData( TcpProtocol::tcp_is_override_src_port, AbstractProtocol::FieldValue ).toBool()); leTcpDstPort->setText( proto->fieldData( TcpProtocol::tcp_dst_port, AbstractProtocol::FieldValue ).toString()); cbTcpDstPortOverride->setChecked( proto->fieldData( TcpProtocol::tcp_is_override_dst_port, AbstractProtocol::FieldValue ).toBool()); leTcpSeqNum->setText( proto->fieldData( TcpProtocol::tcp_seq_num, AbstractProtocol::FieldValue ).toString()); leTcpAckNum->setText( proto->fieldData( TcpProtocol::tcp_ack_num, AbstractProtocol::FieldValue ).toString()); leTcpHdrLen->setText( proto->fieldData( TcpProtocol::tcp_hdrlen, AbstractProtocol::FieldValue ).toString()); cbTcpHdrLenOverride->setChecked( proto->fieldData( TcpProtocol::tcp_is_override_hdrlen, AbstractProtocol::FieldValue ).toBool()); leTcpWindow->setText( proto->fieldData( TcpProtocol::tcp_window, AbstractProtocol::FieldValue ).toString()); leTcpCksum->setText(uintToHexStr( proto->fieldData( TcpProtocol::tcp_cksum, AbstractProtocol::FieldValue ).toUInt(), 2)); cbTcpCksumOverride->setChecked( proto->fieldData( TcpProtocol::tcp_is_override_cksum, AbstractProtocol::FieldValue ).toBool()); leTcpUrgentPointer->setText( proto->fieldData( TcpProtocol::tcp_urg_ptr, AbstractProtocol::FieldValue ).toString()); uint flags = proto->fieldData( TcpProtocol::tcp_flags, AbstractProtocol::FieldValue ).toUInt(); cbTcpFlagsUrg->setChecked((flags & TCP_FLAG_URG) > 0); cbTcpFlagsAck->setChecked((flags & TCP_FLAG_ACK) > 0); cbTcpFlagsPsh->setChecked((flags & TCP_FLAG_PSH) > 0); cbTcpFlagsRst->setChecked((flags & TCP_FLAG_RST) > 0); cbTcpFlagsSyn->setChecked((flags & TCP_FLAG_SYN) > 0); cbTcpFlagsFin->setChecked((flags & TCP_FLAG_FIN) > 0); } void TcpConfigForm::storeWidget(AbstractProtocol *proto) { int ff = 0; proto->setFieldData( TcpProtocol::tcp_src_port, leTcpSrcPort->text()); proto->setFieldData( TcpProtocol::tcp_is_override_src_port, cbTcpSrcPortOverride->isChecked()); proto->setFieldData( TcpProtocol::tcp_dst_port, leTcpDstPort->text()); proto->setFieldData( TcpProtocol::tcp_is_override_dst_port, cbTcpDstPortOverride->isChecked()); proto->setFieldData( TcpProtocol::tcp_seq_num, leTcpSeqNum->text()); proto->setFieldData( TcpProtocol::tcp_ack_num, leTcpAckNum->text()); proto->setFieldData( TcpProtocol::tcp_hdrlen, leTcpHdrLen->text()); proto->setFieldData( TcpProtocol::tcp_is_override_hdrlen, cbTcpHdrLenOverride->isChecked()); proto->setFieldData( TcpProtocol::tcp_window, leTcpWindow->text()); proto->setFieldData( TcpProtocol::tcp_cksum, hexStrToUInt(leTcpCksum->text())); proto->setFieldData( TcpProtocol::tcp_is_override_cksum, cbTcpCksumOverride->isChecked()); proto->setFieldData( TcpProtocol::tcp_urg_ptr, leTcpUrgentPointer->text()); if (cbTcpFlagsUrg->isChecked()) ff |= TCP_FLAG_URG; if (cbTcpFlagsAck->isChecked()) ff |= TCP_FLAG_ACK; if (cbTcpFlagsPsh->isChecked()) ff |= TCP_FLAG_PSH; if (cbTcpFlagsRst->isChecked()) ff |= TCP_FLAG_RST; if (cbTcpFlagsSyn->isChecked()) ff |= TCP_FLAG_SYN; if (cbTcpFlagsFin->isChecked()) ff |= TCP_FLAG_FIN; proto->setFieldData(TcpProtocol::tcp_flags, ff); } ostinato-1.3.0/common/tcpconfig.h000066400000000000000000000021351451413623100167710ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _TCP_CONFIG_H #define _TCP_CONFIG_H #include "abstractprotocolconfig.h" #include "ui_tcp.h" class TcpConfigForm : public AbstractProtocolConfigForm, private Ui::tcp { Q_OBJECT public: TcpConfigForm(QWidget *parent = 0); virtual ~TcpConfigForm(); static TcpConfigForm* createInstance(); virtual void loadWidget(AbstractProtocol *proto); virtual void storeWidget(AbstractProtocol *proto); }; #endif ostinato-1.3.0/common/tcppdml.cpp000066400000000000000000000065341451413623100170220ustar00rootroot00000000000000/* Copyright (C) 2011 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #include "tcppdml.h" #include "hexdump.pb.h" #include "tcp.pb.h" PdmlTcpProtocol::PdmlTcpProtocol() { ostProtoId_ = OstProto::Protocol::kTcpFieldNumber; fieldMap_.insert("tcp.srcport", OstProto::Tcp::kSrcPortFieldNumber); fieldMap_.insert("tcp.dstport", OstProto::Tcp::kDstPortFieldNumber); fieldMap_.insert("tcp.seq", OstProto::Tcp::kSeqNumFieldNumber); fieldMap_.insert("tcp.ack", OstProto::Tcp::kAckNumFieldNumber); fieldMap_.insert("tcp.hdr_len", OstProto::Tcp::kHdrlenRsvdFieldNumber); fieldMap_.insert("tcp.flags", OstProto::Tcp::kFlagsFieldNumber); fieldMap_.insert("tcp.window_size", OstProto::Tcp::kWindowFieldNumber); fieldMap_.insert("tcp.checksum", OstProto::Tcp::kCksumFieldNumber); fieldMap_.insert("tcp.urgent_pointer", OstProto::Tcp::kUrgPtrFieldNumber); } PdmlProtocol* PdmlTcpProtocol::createInstance() { return new PdmlTcpProtocol(); } void PdmlTcpProtocol::unknownFieldHandler(QString name, int /*pos*/, int /*size*/, const QXmlStreamAttributes &attributes, OstProto::Protocol *pbProto, OstProto::Stream* /*stream*/) { if (name == "tcp.options") options_ = QByteArray::fromHex(attributes.value("value").toString().toUtf8()); else if (name == "") { if (attributes.value("show").toString().startsWith("Acknowledgement number")) { bool isOk; OstProto::Tcp *tcp = pbProto->MutableExtension(OstProto::tcp); tcp->set_ack_num(attributes.value("value").toString().toUInt(&isOk, kBaseHex)); } #if 0 else if (attributes.value("show").toString().startsWith("TCP segment data")) { segmentData_ = QByteArray::fromHex(attributes.value("value").toString().toUtf8()); stream->mutable_core()->mutable_name()->insert(0, segmentData_.constData(), segmentData_.size()); } #endif } } void PdmlTcpProtocol::postProtocolHandler(OstProto::Protocol *pbProto, OstProto::Stream *stream) { OstProto::Tcp *tcp = pbProto->MutableExtension(OstProto::tcp); qDebug("Tcp: post\n"); tcp->set_is_override_src_port(true); tcp->set_is_override_dst_port(true); tcp->set_is_override_hdrlen(true); tcp->set_is_override_cksum(overrideCksum_); if (options_.size()) { OstProto::Protocol *proto = stream->add_protocol(); proto->mutable_protocol_id()->set_id( OstProto::Protocol::kHexDumpFieldNumber); OstProto::HexDump *hexDump = proto->MutableExtension(OstProto::hexDump); hexDump->mutable_content()->append(options_.constData(), options_.size()); hexDump->set_pad_until_end(false); options_.resize(0); } } ostinato-1.3.0/common/tcppdml.h000066400000000000000000000023351451413623100164620ustar00rootroot00000000000000/* Copyright (C) 2011 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _TCP_PDML_H #define _TCP_PDML_H #include "pdmlprotocol.h" class PdmlTcpProtocol : public PdmlProtocol { public: static PdmlProtocol* createInstance(); virtual void unknownFieldHandler(QString name, int pos, int size, const QXmlStreamAttributes &attributes, OstProto::Protocol *pbProto, OstProto::Stream *stream); virtual void postProtocolHandler(OstProto::Protocol *pbProto, OstProto::Stream *stream); protected: PdmlTcpProtocol(); private: QByteArray options_; QByteArray segmentData_; }; #endif ostinato-1.3.0/common/textproto.cpp000066400000000000000000000137461451413623100174320ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #include "textproto.h" TextProtocol::TextProtocol(StreamBase *stream, AbstractProtocol *parent) : AbstractProtocol(stream, parent) { } TextProtocol::~TextProtocol() { } AbstractProtocol* TextProtocol::createInstance(StreamBase *stream, AbstractProtocol *parent) { return new TextProtocol(stream, parent); } quint32 TextProtocol::protocolNumber() const { return OstProto::Protocol::kTextProtocolFieldNumber; } void TextProtocol::protoDataCopyInto(OstProto::Protocol &protocol) const { protocol.MutableExtension(OstProto::textProtocol)->CopyFrom(data); protocol.mutable_protocol_id()->set_id(protocolNumber()); } void TextProtocol::protoDataCopyFrom(const OstProto::Protocol &protocol) { if (protocol.protocol_id().id() == protocolNumber() && protocol.HasExtension(OstProto::textProtocol)) data.MergeFrom(protocol.GetExtension(OstProto::textProtocol)); } QString TextProtocol::name() const { return QString("Text Protocol"); } QString TextProtocol::shortName() const { return QString("TEXT"); } quint32 TextProtocol::protocolId(ProtocolIdType type) const { switch(type) { case ProtocolIdTcpUdp: return data.port_num(); default:break; } return AbstractProtocol::protocolId(type); } int TextProtocol::fieldCount() const { return textProto_fieldCount; } AbstractProtocol::FieldFlags TextProtocol::fieldFlags(int index) const { AbstractProtocol::FieldFlags flags; flags = AbstractProtocol::fieldFlags(index); switch (index) { case textProto_text: break; case textProto_portNum: case textProto_eol: case textProto_encoding: flags &= ~FrameField; flags |= MetaField; break; default: qFatal("%s: unimplemented case %d in switch", __PRETTY_FUNCTION__, index); break; } return flags; } QVariant TextProtocol::fieldData(int index, FieldAttrib attrib, int streamIndex) const { switch (index) { case textProto_text: { switch(attrib) { case FieldName: return QString("Text"); case FieldValue: case FieldTextValue: return QString().fromStdString(data.text()); case FieldFrameValue: { QString text; Q_ASSERT(data.encoding() == OstProto::TextProtocol::kUtf8); text = QString().fromStdString(data.text()); if (data.eol() == OstProto::TextProtocol::kCrLf) text.replace('\n', "\r\n"); else if (data.eol() == OstProto::TextProtocol::kCr) text.replace('\n', '\r'); return text.toUtf8(); } default: break; } break; } // Meta fields case textProto_portNum: { switch(attrib) { case FieldValue: return data.port_num(); default: break; } break; } case textProto_eol: { switch(attrib) { case FieldValue: return data.eol(); default: break; } break; } case textProto_encoding: { switch(attrib) { case FieldValue: return data.encoding(); default: break; } break; } default: qFatal("%s: unimplemented case %d in switch", __PRETTY_FUNCTION__, index); break; } return AbstractProtocol::fieldData(index, attrib, streamIndex); } bool TextProtocol::setFieldData(int index, const QVariant &value, FieldAttrib attrib) { bool isOk = false; if (attrib != FieldValue) goto _exit; switch (index) { case textProto_text: { data.set_text(value.toString().toUtf8()); isOk = true; break; } case textProto_portNum: { uint portNum = value.toUInt(&isOk); if (isOk) data.set_port_num(portNum); break; } case textProto_eol: { uint eol = value.toUInt(&isOk); if (isOk && data.EndOfLine_IsValid(eol)) data.set_eol((OstProto::TextProtocol::EndOfLine) eol); else isOk = false; break; } case textProto_encoding: { uint enc = value.toUInt(&isOk); if (isOk && data.TextEncoding_IsValid(enc)) data.set_encoding((OstProto::TextProtocol::TextEncoding) enc); else isOk = false; break; } default: qFatal("%s: unimplemented case %d in switch", __PRETTY_FUNCTION__, index); break; } _exit: return isOk; } int TextProtocol::protocolFrameSize(int streamIndex) const { return fieldData(textProto_text, FieldFrameValue, streamIndex) .toByteArray().size() ; } ostinato-1.3.0/common/textproto.h000066400000000000000000000041701451413623100170660ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _TEXT_PROTOCOL_H #define _TEXT_PROTOCOL_H #include "abstractprotocol.h" #include "textproto.pb.h" /* TextProtocol Protocol Frame Format - specified text with the specified line ending and encoded with the specified encoding */ class TextProtocol : public AbstractProtocol { public: enum textProtocolField { // Frame Fields textProto_text = 0, // Meta Fields textProto_portNum, textProto_eol, textProto_encoding, textProto_fieldCount }; TextProtocol(StreamBase *stream, AbstractProtocol *parent = 0); virtual ~TextProtocol(); static AbstractProtocol* createInstance(StreamBase *stream, AbstractProtocol *parent = 0); virtual quint32 protocolNumber() const; virtual void protoDataCopyInto(OstProto::Protocol &protocol) const; virtual void protoDataCopyFrom(const OstProto::Protocol &protocol); virtual quint32 protocolId(ProtocolIdType type) const; virtual QString name() const; virtual QString shortName() const; virtual int fieldCount() const; virtual AbstractProtocol::FieldFlags fieldFlags(int index) const; virtual QVariant fieldData(int index, FieldAttrib attrib, int streamIndex = 0) const; virtual bool setFieldData(int index, const QVariant &value, FieldAttrib attrib = FieldValue); virtual int protocolFrameSize(int streamIndex = 0) const; private: OstProto::TextProtocol data; }; #endif ostinato-1.3.0/common/textproto.proto000066400000000000000000000022071451413623100200010ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ import "protocol.proto"; package OstProto; // Any Text based protocol message TextProtocol { enum TextEncoding { kUtf8 = 0; } enum EndOfLine { kCr = 0; kLf = 1; kCrLf = 2; } optional uint32 port_num = 1 [default = 80]; optional TextEncoding encoding = 2 [default = kUtf8]; optional string text = 3; optional EndOfLine eol = 4 [default = kLf]; } extend Protocol { optional TextProtocol textProtocol = 500; } ostinato-1.3.0/common/textproto.ui000066400000000000000000000051751451413623100172620ustar00rootroot00000000000000 TextProto 0 0 535 300 Form TCP/UDP Port Number (Protocol) portNumCombo 2 0 Line Ending 2 CR LF CRLF Encode as encodingCombo 1 0 UTF-8 false IntComboBox QComboBox
intcombobox.h
ostinato-1.3.0/common/textprotoconfig.cpp000066400000000000000000000047561451413623100206210ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #include "textprotoconfig.h" #include "textproto.h" TextProtocolConfigForm::TextProtocolConfigForm(QWidget *parent) : AbstractProtocolConfigForm(parent) { setupUi(this); portNumCombo->setValidator(new QIntValidator(0, 0xFFFF, this)); portNumCombo->addItem(0, "Reserved"); portNumCombo->addItem(80, "HTTP"); portNumCombo->addItem(554, "RTSP"); portNumCombo->addItem(5060, "SIP"); } TextProtocolConfigForm::~TextProtocolConfigForm() { } TextProtocolConfigForm* TextProtocolConfigForm::createInstance() { return new TextProtocolConfigForm; } void TextProtocolConfigForm::loadWidget(AbstractProtocol *proto) { portNumCombo->setValue( proto->fieldData( TextProtocol::textProto_portNum, AbstractProtocol::FieldValue ).toUInt()); eolCombo->setCurrentIndex( proto->fieldData( TextProtocol::textProto_eol, AbstractProtocol::FieldValue ).toUInt()); encodingCombo->setCurrentIndex( proto->fieldData( TextProtocol::textProto_encoding, AbstractProtocol::FieldValue ).toUInt()); protoText->setText( proto->fieldData( TextProtocol::textProto_text, AbstractProtocol::FieldValue ).toString()); } void TextProtocolConfigForm::storeWidget(AbstractProtocol *proto) { proto->setFieldData( TextProtocol::textProto_portNum, portNumCombo->currentValue()); proto->setFieldData( TextProtocol::textProto_eol, eolCombo->currentIndex()); proto->setFieldData( TextProtocol::textProto_encoding, encodingCombo->currentIndex()); proto->setFieldData( TextProtocol::textProto_text, protoText->toPlainText()); } ostinato-1.3.0/common/textprotoconfig.h000066400000000000000000000022411451413623100202510ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _TEXT_PROTOCOL_CONFIG_H #define _TEXT_PROTOCOL_CONFIG_H #include "abstractprotocolconfig.h" #include "ui_textproto.h" class TextProtocolConfigForm : public AbstractProtocolConfigForm, private Ui::TextProto { Q_OBJECT public: TextProtocolConfigForm(QWidget *parent = 0); virtual ~TextProtocolConfigForm(); static TextProtocolConfigForm* createInstance(); virtual void loadWidget(AbstractProtocol *proto); virtual void storeWidget(AbstractProtocol *proto); }; #endif ostinato-1.3.0/common/textprotopdml.cpp000066400000000000000000000117001451413623100202730ustar00rootroot00000000000000/* Copyright (C) 2011 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #include "textprotopdml.h" #include "textproto.pb.h" PdmlTextProtocol::PdmlTextProtocol() { ostProtoId_ = OstProto::Protocol::kTextProtocolFieldNumber; } PdmlProtocol* PdmlTextProtocol::createInstance() { return new PdmlTextProtocol(); } void PdmlTextProtocol::preProtocolHandler(QString /*name*/, const QXmlStreamAttributes &attributes, int expectedPos, OstProto::Protocol *pbProto, OstProto::Stream *stream) { bool isOk; int size; int pos = attributes.value("pos").toString().toUInt(&isOk); if (!isOk) { if (expectedPos >= 0) expPos_ = pos = expectedPos; else goto _skip_pos_size_proc; } size = attributes.value("size").toString().toUInt(&isOk); if (!isOk) goto _skip_pos_size_proc; // If pos+size goes beyond the frame length, this is a "reassembled" // protocol and should be skipped if ((pos + size) > int(stream->core().frame_len())) goto _skip_pos_size_proc; expPos_ = pos; endPos_ = expPos_ + size; _skip_pos_size_proc: qDebug("expPos_ = %d, endPos_ = %d", expPos_, endPos_); OstProto::TextProtocol *text = pbProto->MutableExtension( OstProto::textProtocol); text->set_port_num(0); text->set_eol(OstProto::TextProtocol::kCrLf); // by default we assume CRLF detectEol_ = true; contentType_ = kUnknownContent; } void PdmlTextProtocol::unknownFieldHandler(QString name, int pos, int size, const QXmlStreamAttributes &attributes, OstProto::Protocol *pbProto, OstProto::Stream* /*stream*/) { if (name == "http.file_data") // ignore as it's dup of other fields return; _retry: switch(contentType_) { case kUnknownContent: if (name == "data") contentType_ = kOtherContent; else contentType_ = kTextContent; goto _retry; break; case kTextContent: { OstProto::TextProtocol *text = pbProto->MutableExtension( OstProto::textProtocol); if ((name == "data") || (attributes.value("show") == "HTTP chunked response")) { contentType_ = kOtherContent; goto _retry; } QByteArray line = QByteArray::fromHex( attributes.value("value").toString().toUtf8()); foreach(char c, line) { if (!isprint(c) && !isspace(c)) { contentType_ = kOtherContent; goto _retry; } } if (pos < expPos_) break; if ((pos + size) > endPos_) break; if (pos > expPos_) { int gap = pos - expPos_; QByteArray filler(gap, '\n'); if (text->eol() == OstProto::TextProtocol::kCrLf) { if (gap & 0x01) // Odd { filler.resize(gap/2 + 1); filler[0]=int(' '); } else // Even filler.resize(gap/2); } text->mutable_text()->append(filler.constData(), filler.size()); expPos_ += gap; } if (detectEol_) { if (line.right(2) == "\r\n") text->set_eol(OstProto::TextProtocol::kCrLf); else if (line.right(1) == "\r") text->set_eol(OstProto::TextProtocol::kCr); else if (line.right(1) == "\n") text->set_eol(OstProto::TextProtocol::kLf); detectEol_ = false; } // Convert line endings to LF only - Qt reqmt that TextProto honours line.replace("\r\n", "\n"); line.replace('\r', '\n'); text->mutable_text()->append(line.constData(), line.size()); expPos_ += size; break; } case kOtherContent: // Do nothing! break; default: Q_ASSERT(false); } } void PdmlTextProtocol::postProtocolHandler(OstProto::Protocol *pbProto, OstProto::Stream *stream) { OstProto::TextProtocol *text = pbProto->MutableExtension( OstProto::textProtocol); // Empty Text Content - remove ourselves if (text->text().length() == 0) stream->mutable_protocol()->RemoveLast(); expPos_ = endPos_ = -1; detectEol_ = true; contentType_ = kUnknownContent; } ostinato-1.3.0/common/textprotopdml.h000066400000000000000000000030551451413623100177440ustar00rootroot00000000000000/* Copyright (C) 2011 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _TEXT_PROTO_PDML_H #define _TEXT_PROTO_PDML_H #include "pdmlprotocol.h" class PdmlTextProtocol : public PdmlProtocol { public: static PdmlProtocol* createInstance(); virtual void preProtocolHandler(QString name, const QXmlStreamAttributes &attributes, int expectedPos, OstProto::Protocol *pbProto, OstProto::Stream *stream); virtual void unknownFieldHandler(QString name, int pos, int size, const QXmlStreamAttributes &attributes, OstProto::Protocol *pbProto, OstProto::Stream *stream); virtual void postProtocolHandler(OstProto::Protocol *pbProto, OstProto::Stream *stream); protected: PdmlTextProtocol(); private: enum ContentType { kUnknownContent, kTextContent, kOtherContent }; bool detectEol_; ContentType contentType_; int expPos_; int endPos_; }; #endif ostinato-1.3.0/common/tosdscp.cpp000066400000000000000000000063251451413623100170340ustar00rootroot00000000000000/* Copyright (C) 2018 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software FoundatiTosDscpWidget::on, either versiTosDscpWidget::on 3 of the License, or (at your optiTosDscpWidget::on) any later versiTosDscpWidget::on. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License alTosDscpWidget::ong with this program. If not, see */ #include "tosdscp.h" TosDscpWidget::TosDscpWidget(QWidget *parent) : QWidget(parent) { codePoints_.insert("cs0", 0); codePoints_.insert("cs1", 8); codePoints_.insert("cs2", 16); codePoints_.insert("cs3", 24); codePoints_.insert("cs4", 32); codePoints_.insert("cs5", 40); codePoints_.insert("cs6", 48); codePoints_.insert("cs7", 56); codePoints_.insert("af11", 10); codePoints_.insert("af12", 12); codePoints_.insert("af13", 14); codePoints_.insert("af21", 18); codePoints_.insert("af22", 20); codePoints_.insert("af23", 22); codePoints_.insert("af31", 26); codePoints_.insert("af32", 28); codePoints_.insert("af33", 30); codePoints_.insert("af41", 34); codePoints_.insert("af42", 36); codePoints_.insert("af43", 38); codePoints_.insert("ef", 46); codePoints_.insert("voice-admit", 44); setupUi(this); dscp->addItems(codePoints_.keys()); connect(precedence, SIGNAL(activated(int)), SLOT(setTosValue())); connect(lowDelay, SIGNAL(clicked(bool)), SLOT(setTosValue())); connect(highThroughput, SIGNAL(clicked(bool)), SLOT(setTosValue())); connect(highReliability, SIGNAL(clicked(bool)), SLOT(setTosValue())); connect(dscp, SIGNAL(activated(int)), SLOT(setDscpValue())); connect(ecn, SIGNAL(activated(int)), SLOT(setDscpValue())); connect(customValue, SIGNAL(valueChanged(int)), SLOT(setValue(int))); } int TosDscpWidget::value() { return customValue->value() & 0xff; } void TosDscpWidget::setValue(int value) { value &= 0xff; qDebug("value %02x", value); customValue->blockSignals(true); customValue->setValue(value); // avoid signal-slot loop customValue->blockSignals(false); dscp->setCurrentIndex(codePoints_.values().indexOf(value >> 2)); ecn->setCurrentIndex(value & 0x03); precedence->setCurrentIndex(value >> 5); lowDelay->setChecked(value & 0x10); highThroughput->setChecked(value & 0x08); highReliability->setChecked(value & 0x04); } void TosDscpWidget::setTosValue() { qDebug("prec %d", precedence->currentIndex()); qDebug("delay %d", lowDelay->isChecked() ? 1 : 0); setValue((precedence->currentIndex() << 5) | (lowDelay->isChecked() ? 0x10 : 0x00) | (highThroughput->isChecked() ? 0x08 : 0x00) | (highReliability->isChecked() ? 0x04 : 0x00) | 0x00); } void TosDscpWidget::setDscpValue() { setValue((codePoints_.value(dscp->currentText()) << 2) | ecn->currentIndex()); } ostinato-1.3.0/common/tosdscp.h000066400000000000000000000020511451413623100164710ustar00rootroot00000000000000/* Copyright (C) 2018 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _TOS_DSCP_H #define _TOS_DSCP_H #include "ui_tosdscp.h" #include class TosDscpWidget: public QWidget, private Ui::TosDscp { Q_OBJECT public: TosDscpWidget(QWidget *parent); int value(); public slots: void setValue(int value); private slots: void setTosValue(); void setDscpValue(); private: QMap codePoints_; }; #endif ostinato-1.3.0/common/tosdscp.ui000066400000000000000000000147271451413623100166740ustar00rootroot00000000000000 TosDscp 0 0 232 38 0 0 0 0 1 TOS DSCP Custom 1 0 0 0 0 Precedence 2 Routine Priority Immediate Flash Flash Override Critical Internetwork Control Network Control Low Delay D High Throughput T High Reliability R 0 0 0 0 DSCP ECN Not-ECT ECT (1) ECT (0) CE 0 0 0 0 Enter value in hexadecimal 255 16 IntEdit QSpinBox
intedit.h
cosType currentIndexChanged(int) stackedWidget setCurrentIndex(int) 58 67 110 36
ostinato-1.3.0/common/udp.cpp000066400000000000000000000266671451413623100161600ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #include "udp.h" UdpProtocol::UdpProtocol(StreamBase *stream, AbstractProtocol *parent) : AbstractProtocol(stream, parent) { } UdpProtocol::~UdpProtocol() { } AbstractProtocol* UdpProtocol::createInstance(StreamBase *stream, AbstractProtocol *parent) { return new UdpProtocol(stream, parent); } quint32 UdpProtocol::protocolNumber() const { return OstProto::Protocol::kUdpFieldNumber; } void UdpProtocol::protoDataCopyInto(OstProto::Protocol &protocol) const { protocol.MutableExtension(OstProto::udp)->CopyFrom(data); protocol.mutable_protocol_id()->set_id(protocolNumber()); } void UdpProtocol::protoDataCopyFrom(const OstProto::Protocol &protocol) { if (protocol.protocol_id().id() == protocolNumber() && protocol.HasExtension(OstProto::udp)) data.MergeFrom(protocol.GetExtension(OstProto::udp)); } QString UdpProtocol::name() const { return QString("User Datagram Protocol"); } QString UdpProtocol::shortName() const { return QString("UDP"); } AbstractProtocol::ProtocolIdType UdpProtocol::protocolIdType() const { return ProtocolIdTcpUdp; } quint32 UdpProtocol::protocolId(ProtocolIdType type) const { switch(type) { case ProtocolIdIp: return 0x11; default: break; } return AbstractProtocol::protocolId(type); } int UdpProtocol::fieldCount() const { return udp_fieldCount; } AbstractProtocol::FieldFlags UdpProtocol::fieldFlags(int index) const { AbstractProtocol::FieldFlags flags; flags = AbstractProtocol::fieldFlags(index); switch (index) { case udp_srcPort: case udp_dstPort: case udp_totLen: break; case udp_cksum: flags |= CksumField; break; case udp_isOverrideSrcPort: case udp_isOverrideDstPort: case udp_isOverrideTotLen: case udp_isOverrideCksum: flags &= ~FrameField; flags |= MetaField; break; default: qFatal("%s: unimplemented case %d in switch", __PRETTY_FUNCTION__, index); break; } return flags; } QVariant UdpProtocol::fieldData(int index, FieldAttrib attrib, int streamIndex) const { switch (index) { case udp_srcPort: { quint16 srcPort; switch(attrib) { case FieldValue: case FieldFrameValue: case FieldTextValue: if (data.is_override_src_port()) srcPort = data.src_port(); else srcPort = payloadProtocolId(ProtocolIdTcpUdp); break; default: srcPort = 0; // avoid the 'maybe used unitialized' warning break; } switch(attrib) { case FieldName: return QString("Source Port"); case FieldValue: return srcPort; case FieldTextValue: return QString("%1").arg(srcPort); case FieldFrameValue: { QByteArray fv; fv.resize(2); qToBigEndian(srcPort, (uchar*) fv.data()); return fv; } default: break; } break; } case udp_dstPort: { quint16 dstPort; switch(attrib) { case FieldValue: case FieldFrameValue: case FieldTextValue: if (data.is_override_dst_port()) dstPort = data.dst_port(); else dstPort = payloadProtocolId(ProtocolIdTcpUdp); break; default: dstPort = 0; // avoid the 'maybe used unitialized' warning break; } switch(attrib) { case FieldName: return QString("Destination Port"); case FieldValue: return dstPort; case FieldTextValue: return QString("%1").arg(dstPort); case FieldFrameValue: { QByteArray fv; fv.resize(2); qToBigEndian(dstPort, (uchar*) fv.data()); return fv; } default: break; } break; } case udp_totLen: { switch(attrib) { case FieldName: return QString("Datagram Length"); case FieldValue: { int totlen; totlen = data.is_override_totlen() ? data.totlen() : (protocolFramePayloadSize(streamIndex) + 8); return totlen; } case FieldFrameValue: { QByteArray fv; int totlen; totlen = data.is_override_totlen() ? data.totlen() : (protocolFramePayloadSize(streamIndex) + 8); fv.resize(2); qToBigEndian((quint16) totlen, (uchar*) fv.data()); return fv; } case FieldTextValue: { int totlen; totlen = data.is_override_totlen() ? data.totlen() : (protocolFramePayloadSize(streamIndex) + 8); return QString("%1").arg(totlen); } case FieldBitSize: return 16; default: break; } break; } case udp_cksum: { quint16 cksum; switch(attrib) { case FieldValue: case FieldFrameValue: case FieldTextValue: { if (data.is_override_cksum()) cksum = data.cksum(); else { cksum = protocolFrameCksum(streamIndex, CksumTcpUdp); if (cksum == 0) cksum = 0xFFFF; } qDebug("UDP cksum = %hu", cksum); break; } default: cksum = 0; break; } switch(attrib) { case FieldName: return QString("Checksum"); case FieldValue: return cksum; case FieldFrameValue: { QByteArray fv; fv.resize(2); qToBigEndian(cksum, (uchar*) fv.data()); return fv; } case FieldTextValue: return QString("0x%1"). arg(cksum, 4, BASE_HEX, QChar('0'));; case FieldBitSize: return 16; default: break; } break; } // Meta fields case udp_isOverrideSrcPort: { switch(attrib) { case FieldValue: return data.is_override_src_port(); default: break; } break; } case udp_isOverrideDstPort: { switch(attrib) { case FieldValue: return data.is_override_dst_port(); default: break; } break; } case udp_isOverrideTotLen: { switch(attrib) { case FieldValue: return data.is_override_totlen(); default: break; } break; } case udp_isOverrideCksum: { switch(attrib) { case FieldValue: return data.is_override_cksum(); default: break; } break; } default: qFatal("%s: unimplemented case %d in switch", __PRETTY_FUNCTION__, index); break; } return AbstractProtocol::fieldData(index, attrib, streamIndex); } bool UdpProtocol::setFieldData(int index, const QVariant& value, FieldAttrib attrib) { bool isOk = false; if (attrib != FieldValue) goto _exit; switch (index) { case udp_isOverrideSrcPort: { data.set_is_override_src_port(value.toBool()); isOk = true; break; } case udp_isOverrideDstPort: { data.set_is_override_dst_port(value.toBool()); isOk = true; break; } case udp_isOverrideTotLen: { data.set_is_override_totlen(value.toBool()); isOk = true; break; } case udp_isOverrideCksum: { data.set_is_override_cksum(value.toBool()); isOk = true; break; } case udp_srcPort: { uint srcPort = value.toUInt(&isOk); if (isOk) data.set_src_port(srcPort); break; } case udp_dstPort: { uint dstPort = value.toUInt(&isOk); if (isOk) data.set_dst_port(dstPort); break; } case udp_totLen: { uint totLen = value.toUInt(&isOk); if (isOk) data.set_totlen(totLen); break; } case udp_cksum: { uint cksum = value.toUInt(&isOk); if (isOk) data.set_cksum(cksum); break; } default: qFatal("%s: unimplemented case %d in switch", __PRETTY_FUNCTION__, index); break; } _exit: return isOk; } int UdpProtocol::protocolFrameVariableCount() const { int count; if (data.is_override_totlen() && data.is_override_cksum()) count = AbstractProtocol::protocolFrameVariableCount(); else count = AbstractProtocol::lcm( AbstractProtocol::protocolFrameVariableCount(), protocolFramePayloadVariableCount()); return count; } ostinato-1.3.0/common/udp.h000066400000000000000000000040271451413623100156070ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _UDP_H #define _UDP_H #include "abstractprotocol.h" #include "udp.pb.h" class UdpProtocol : public AbstractProtocol { public: enum udpfield { udp_srcPort = 0, udp_dstPort, udp_totLen, udp_cksum, udp_isOverrideSrcPort, udp_isOverrideDstPort, udp_isOverrideTotLen, udp_isOverrideCksum, udp_fieldCount }; UdpProtocol(StreamBase *stream, AbstractProtocol *parent = 0); virtual ~UdpProtocol(); static AbstractProtocol* createInstance(StreamBase *stream, AbstractProtocol *parent = 0); virtual quint32 protocolNumber() const; virtual void protoDataCopyInto(OstProto::Protocol &protocol) const; virtual void protoDataCopyFrom(const OstProto::Protocol &protocol); virtual QString name() const; virtual QString shortName() const; virtual ProtocolIdType protocolIdType() const; virtual quint32 protocolId(ProtocolIdType type) const; virtual int fieldCount() const; virtual AbstractProtocol::FieldFlags fieldFlags(int index) const; virtual QVariant fieldData(int index, FieldAttrib attrib, int streamIndex = 0) const; virtual bool setFieldData(int index, const QVariant &value, FieldAttrib attrib = FieldValue); virtual int protocolFrameVariableCount() const; private: OstProto::Udp data; }; #endif ostinato-1.3.0/common/udp.proto000066400000000000000000000021521451413623100165200ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ import "protocol.proto"; package OstProto; // UDP message Udp { optional bool is_override_src_port = 1; optional bool is_override_dst_port = 2; optional bool is_override_totlen = 3; optional bool is_override_cksum = 4; optional uint32 src_port = 5 [default = 49152]; optional uint32 dst_port = 6 [default = 49153]; optional uint32 totlen = 7; optional uint32 cksum = 8; } extend Protocol { optional Udp udp = 401; } ostinato-1.3.0/common/udp.ui000066400000000000000000000101341451413623100157710ustar00rootroot00000000000000 udp 0 0 246 144 Form Override Source Port false Override Destination Port false Override Length false Override Checksum false >HH HH; Qt::Horizontal 40 20 Qt::Vertical 20 40 cbUdpLengthOverride toggled(bool) leUdpLength setEnabled(bool) 59 63 209 81 cbUdpCksumOverride toggled(bool) leUdpCksum setEnabled(bool) 55 106 209 107 cbUdpDstPortOverride toggled(bool) leUdpDstPort setEnabled(bool) 131 43 166 46 cbUdpSrcPortOverride toggled(bool) leUdpSrcPort setEnabled(bool) 125 21 167 20 ostinato-1.3.0/common/udpconfig.cpp000066400000000000000000000062221451413623100173270ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #include "udpconfig.h" #include "udp.h" UdpConfigForm::UdpConfigForm(QWidget *parent) : AbstractProtocolConfigForm(parent) { setupUi(this); } UdpConfigForm::~UdpConfigForm() { } UdpConfigForm* UdpConfigForm::createInstance() { return new UdpConfigForm; } void UdpConfigForm::loadWidget(AbstractProtocol *proto) { leUdpSrcPort->setText( proto->fieldData( UdpProtocol::udp_srcPort, AbstractProtocol::FieldValue ).toString()); cbUdpSrcPortOverride->setChecked( proto->fieldData( UdpProtocol::udp_isOverrideSrcPort, AbstractProtocol::FieldValue ).toBool()); leUdpDstPort->setText( proto->fieldData( UdpProtocol::udp_dstPort, AbstractProtocol::FieldValue ).toString()); cbUdpDstPortOverride->setChecked( proto->fieldData( UdpProtocol::udp_isOverrideDstPort, AbstractProtocol::FieldValue ).toBool()); leUdpLength->setText( proto->fieldData( UdpProtocol::udp_totLen, AbstractProtocol::FieldValue ).toString()); cbUdpLengthOverride->setChecked( proto->fieldData( UdpProtocol::udp_isOverrideTotLen, AbstractProtocol::FieldValue ).toBool()); leUdpCksum->setText(uintToHexStr( proto->fieldData( UdpProtocol::udp_cksum, AbstractProtocol::FieldValue ).toUInt(), 2)); cbUdpCksumOverride->setChecked( proto->fieldData( UdpProtocol::udp_isOverrideCksum, AbstractProtocol::FieldValue ).toBool()); } void UdpConfigForm::storeWidget(AbstractProtocol *proto) { proto->setFieldData( UdpProtocol::udp_srcPort, leUdpSrcPort->text()); proto->setFieldData( UdpProtocol::udp_isOverrideSrcPort, cbUdpSrcPortOverride->isChecked()); proto->setFieldData( UdpProtocol::udp_dstPort, leUdpDstPort->text()); proto->setFieldData( UdpProtocol::udp_isOverrideDstPort, cbUdpDstPortOverride->isChecked()); proto->setFieldData( UdpProtocol::udp_totLen, leUdpLength->text()); proto->setFieldData( UdpProtocol::udp_isOverrideTotLen, cbUdpLengthOverride->isChecked()); proto->setFieldData( UdpProtocol::udp_cksum, hexStrToUInt(leUdpCksum->text())); proto->setFieldData( UdpProtocol::udp_isOverrideCksum, cbUdpCksumOverride->isChecked()); } ostinato-1.3.0/common/udpconfig.h000066400000000000000000000021361451413623100167740ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _UDP_CONFIG_H #define _UDP_CONFIG_H #include "abstractprotocolconfig.h" #include "ui_udp.h" class UdpConfigForm : public AbstractProtocolConfigForm, private Ui::udp { Q_OBJECT public: UdpConfigForm(QWidget *parent = 0); virtual ~UdpConfigForm(); static UdpConfigForm* createInstance(); virtual void loadWidget(AbstractProtocol *proto); virtual void storeWidget(AbstractProtocol *proto); }; #endif ostinato-1.3.0/common/udppdml.cpp000066400000000000000000000032101451413623100170100ustar00rootroot00000000000000/* Copyright (C) 2011 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #include "udppdml.h" #include "udp.pb.h" PdmlUdpProtocol::PdmlUdpProtocol() { ostProtoId_ = OstProto::Protocol::kUdpFieldNumber; fieldMap_.insert("udp.srcport", OstProto::Udp::kSrcPortFieldNumber); fieldMap_.insert("udp.dstport", OstProto::Udp::kDstPortFieldNumber); fieldMap_.insert("udp.length", OstProto::Udp::kTotlenFieldNumber); fieldMap_.insert("udp.checksum_coverage", OstProto::Udp::kTotlenFieldNumber); fieldMap_.insert("udp.checksum", OstProto::Udp::kCksumFieldNumber); } PdmlProtocol* PdmlUdpProtocol::createInstance() { return new PdmlUdpProtocol(); } void PdmlUdpProtocol::postProtocolHandler(OstProto::Protocol *pbProto, OstProto::Stream* /*stream*/) { OstProto::Udp *udp = pbProto->MutableExtension(OstProto::udp); qDebug("Udp: post\n"); udp->set_is_override_src_port(true); udp->set_is_override_dst_port(true); udp->set_is_override_totlen(true); udp->set_is_override_cksum(overrideCksum_); } ostinato-1.3.0/common/udppdml.h000066400000000000000000000017351451413623100164670ustar00rootroot00000000000000/* Copyright (C) 2011 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _UDP_PDML_H #define _UDP_PDML_H #include "pdmlprotocol.h" class PdmlUdpProtocol : public PdmlProtocol { public: static PdmlProtocol* createInstance(); virtual void postProtocolHandler(OstProto::Protocol *pbProto, OstProto::Stream *stream); protected: PdmlUdpProtocol(); }; #endif ostinato-1.3.0/common/uint128.h000066400000000000000000000120711451413623100162270ustar00rootroot00000000000000/* Copyright (C) 2015 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _UINT128_H #define _UINT128_H #include #include #include #include class UInt128 { public: UInt128(); UInt128(int lo); UInt128(quint64 hi, quint64 lo); UInt128(quint8 *value); quint64 hi64() const; quint64 lo64() const; quint8* toArray() const; bool operator!() const; bool operator==(const UInt128 &other) const; bool operator!=(const UInt128 &other) const; UInt128 operator+(const UInt128 &other) const; UInt128 operator-(const UInt128 &other) const; UInt128 operator*(const uint &other) const; UInt128 operator<<(const int &shift) const; UInt128 operator~() const; UInt128 operator&(const UInt128 &other) const; UInt128 operator|(const UInt128 &other) const; private: quint64 hi_; quint64 lo_; quint8 array_[16]; }; Q_DECLARE_METATYPE(UInt128); inline UInt128::UInt128() { // Do nothing - value will be garbage like any other uint } inline UInt128::UInt128(int lo) { hi_ = 0; lo_ = lo; } inline UInt128::UInt128(quint64 hi, quint64 lo) { hi_ = hi; lo_ = lo; } inline UInt128::UInt128(quint8 *value) { hi_ = (quint64(value[0]) << 56) | (quint64(value[1]) << 48) | (quint64(value[2]) << 40) | (quint64(value[3]) << 32) | (quint64(value[4]) << 24) | (quint64(value[5]) << 16) | (quint64(value[6]) << 8) | (quint64(value[7]) << 0); lo_ = (quint64(value[ 8]) << 56) | (quint64(value[ 9]) << 48) | (quint64(value[10]) << 40) | (quint64(value[11]) << 32) | (quint64(value[12]) << 24) | (quint64(value[13]) << 16) | (quint64(value[14]) << 8) | (quint64(value[15]) << 0); } inline quint64 UInt128::hi64() const { return hi_; } inline quint64 UInt128::lo64() const { return lo_; } inline quint8* UInt128::toArray() const { qToBigEndian(hi_, const_cast(array_ + 0)); qToBigEndian(lo_, const_cast(array_ + 8)); return (quint8*)array_; } inline bool UInt128::operator!() const { return (hi_ == 0) && (lo_ == 0); } inline bool UInt128::operator==(const UInt128 &other) const { return ((hi_ == other.hi_) && (lo_ == other.lo_)); } inline bool UInt128::operator!=(const UInt128 &other) const { return ((hi_ != other.hi_) || (lo_ != other.lo_)); } inline UInt128 UInt128::operator+(const UInt128 &other) const { UInt128 sum; sum.lo_ = lo_ + other.lo_; sum.hi_ = hi_ + other.hi_ + (sum.lo_ < lo_); return sum; } inline UInt128 UInt128::operator-(const UInt128 &other) const { UInt128 diff; diff.lo_ = lo_ - other.lo_; diff.hi_ = hi_ - other.hi_ - (diff.lo_ > lo_); return diff; } inline UInt128 UInt128::operator*(const uint &other) const { UInt128 product; // FIXME product.hi_ = 0; product.lo_ = lo_ * other; return product; } inline UInt128 UInt128::operator<<(const int &shift) const { UInt128 shifted; if (shift < 64) return UInt128((hi_<>(64-shift)), lo_ << shift); return UInt128(hi_<<(shift-64), 0); } inline UInt128 UInt128::operator~() const { return UInt128(~hi_, ~lo_); } inline UInt128 UInt128::operator&(const UInt128 &other) const { return UInt128(hi_ & other.hi_, lo_ & other.lo_); } inline UInt128 UInt128::operator|(const UInt128 &other) const { return UInt128(hi_ | other.hi_, lo_ | other.lo_); } #if QT_VERSION >= 0x050700 template <> inline UInt128 qFromBigEndian(const void *src) #else template <> inline UInt128 qFromBigEndian(const uchar *src) #endif { quint64 hi, lo; hi = qFromBigEndian(src); lo = qFromBigEndian((uchar*)src+8); return UInt128(hi, lo); } #if QT_VERSION >= 0x050700 template <> inline void qToBigEndian(UInt128 src, void *dest) #else template <> inline void qToBigEndian(UInt128 src, uchar *dest) #endif { memcpy(dest, src.toArray(), 16); } template <> inline UInt128 qToBigEndian(const UInt128 src) { quint64 hi, lo; hi = qToBigEndian(src.hi64()); lo = qToBigEndian(src.lo64()); return UInt128(hi, lo); } inline QDebug operator<<(QDebug debug, const UInt128 &value) { QDebugStateSaver saver(debug); debug.maybeSpace() << hex << value.hi64() << " " << value.lo64(); return debug; } inline uint qHash(const UInt128 &key) { return qHash(key.hi64()) ^ qHash(key.lo64()); } #endif ostinato-1.3.0/common/uintedit.h000066400000000000000000000023461451413623100166460ustar00rootroot00000000000000/* Copyright (C) 2022 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _UINT_EDIT_H #define _UINT_EDIT_H #include "ulonglongvalidator.h" #include #include class UIntEdit: public QLineEdit { public: UIntEdit(QWidget *parent = 0); quint32 value(); void setValue(quint32 val); }; // -------------------- // inline UIntEdit::UIntEdit(QWidget *parent) : QLineEdit(parent) { setValidator(new ULongLongValidator(0, UINT_MAX)); } inline quint32 UIntEdit::value() { return text().toUInt(Q_NULLPTR, 0); } inline void UIntEdit::setValue(quint32 val) { setText(QString::number(val)); } #endif ostinato-1.3.0/common/ulonglongvalidator.h000066400000000000000000000037651451413623100207410ustar00rootroot00000000000000/* Copyright (C) 2021 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _ULONGLONG_VALIDATOR_H #define _ULONGLONG_VALIDATOR_H #include class ULongLongValidator : public QValidator { public: ULongLongValidator(QObject *parent = 0) : QValidator(parent) { } ULongLongValidator(qulonglong min, qulonglong max, QObject *parent = 0) : QValidator(parent) { setRange(min, max); } ~ULongLongValidator() {} void setRange(qulonglong min, qulonglong max) { min_ = min; max_ = max; } virtual QValidator::State validate(QString &input, int& /*pos*/) const { if (input.isEmpty()) return Intermediate; if (input.compare("0x", Qt::CaseInsensitive) == 0) return Intermediate; bool isOk; qulonglong v = input.toULongLong(&isOk, 0); //qDebug("input: %s, ok: %d, %llu", qPrintable(input), isOk, v); if (!isOk) return Invalid; if (v > max_) return Invalid; if (v < min_) return Intermediate; return Acceptable; } virtual void fixup(QString &input) const { int dummyPos = 0; State state = validate(input, dummyPos); if (state == Acceptable) return; input.setNum(min_); } private: qulonglong min_{0}; qulonglong max_{~0ULL}; }; #endif ostinato-1.3.0/common/updater.cpp000066400000000000000000000100031451413623100170050ustar00rootroot00000000000000/* Copyright (C) 2015 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #include "updater.h" #include #include #include #include extern const char* version; Updater::Updater() { http_ = new QNetworkAccessManager(this); #if 1 // Tests! Q_ASSERT(isVersionNewer("1.3.0", "1.2.0") == true); Q_ASSERT(isVersionNewer("1.3.0", "1.1") == true); Q_ASSERT(isVersionNewer("1.2.0", "1.1") == true); Q_ASSERT(isVersionNewer("1.1", "1") == true); Q_ASSERT(isVersionNewer("10.1", "2") == true); Q_ASSERT(isVersionNewer("0.10", "0.2") == true); Q_ASSERT(isVersionNewer("1.10.1", "1.2.3") == true); Q_ASSERT(isVersionNewer("0.7.1", "0.8") == false); #endif } Updater::~Updater() { delete http_; } void Updater::checkForNewVersion() { QNetworkRequest request(QUrl("http://update.ostinato.org/update/pad.xml")); //reqHdr.setHeader("Host", host); request.setHeader(QNetworkRequest::UserAgentHeader, userAgent()); connect(http_, SIGNAL(finished(QNetworkReply*)), this, SLOT(parseXml(QNetworkReply*))); http_->get(request); QList headers = request.rawHeaderList(); foreach(QByteArray hdr, headers ) { QByteArray val = request.rawHeader(hdr); qDebug("Updater: %s: %s", qPrintable(QString(hdr)), qPrintable(QString(val))); } } void Updater::parseXml(QNetworkReply *reply) { QXmlStreamReader xml; QString newVersion; if (reply->error()) { qDebug("Updater: %s", qPrintable(reply->errorString())); goto _exit; } xml.setDevice(reply); while (!xml.atEnd()) { xml.readNext(); if (xml.isStartElement() && (xml.name() == "Program_Version")) newVersion = xml.readElementText(); } qDebug("Updater: latest version = %s", qPrintable(newVersion)); if (!newVersion.isEmpty() && isVersionNewer(newVersion, QString(version))) emit newVersionAvailable(newVersion); emit latestVersion(newVersion); _exit: // Job done, time to self-destruct deleteLater(); } bool Updater::isVersionNewer(QString newVersion, QString curVersion) { QStringList curVer = QString(curVersion).split('.'); QStringList newVer = QString(newVersion).split('.'); for (int i = 0; i < qMin(curVer.size(), newVer.size()); i++) { bool isOk; uint n = newVer.at(i).toUInt(&isOk); uint c = curVer.at(i).toUInt(&isOk); if (n > c) return true; else if (n < c) return false; } if (newVer.size() > curVer.size()) return true; return false; } QString Updater::userAgent() { QString ua = QString("Mozilla/5.0 (%1) %2/%3 (Qt/%6)") .arg(sysInfo()) .arg(QCoreApplication::instance()->applicationName()) .arg(version) .arg(qVersion()); return ua; } QString Updater::sysInfo() { #if QT_VERSION >= 0x050400 return QSysInfo::prettyProductName(); #else #if defined(Q_OS_WIN32) return QString("Windows/0x%1").arg(QSysInfo::WindowsVersion, 0, 16); #elif defined(Q_OS_LINUX) return QString("Linux"); #elif defined(Q_OS_MAC) return QString("MacOSX/0x%1").arg(QSysInfo::MacintoshVersion, 0, 16); #elif defined(Q_OS_BSD4) return QString("BSD"); #elif defined(Q_OS_UNIX) return QString("Unix"); #else return QString("Unknown"); #endif #endif } ostinato-1.3.0/common/updater.h000066400000000000000000000023331451413623100164610ustar00rootroot00000000000000/* Copyright (C) 2015 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _UPDATER_H #define _UPDATER_H #include #include class QNetworkAccessManager; class QNetworkReply; class Updater : public QObject { Q_OBJECT public: Updater(); virtual ~Updater(); void checkForNewVersion(); static bool isVersionNewer(QString newVersion, QString curVersion); signals: void newVersionAvailable(QString); void latestVersion(QString); private slots: void parseXml(QNetworkReply *reply); private: QString userAgent(); QString sysInfo(); QNetworkAccessManager *http_; }; #endif ostinato-1.3.0/common/userscript.cpp000066400000000000000000000342511451413623100175570ustar00rootroot00000000000000/* Copyright (C) 2010, 2014 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #include "userscript.h" // // -------------------- UserScriptProtocol -------------------- // UserScriptProtocol::UserScriptProtocol(StreamBase *stream, AbstractProtocol *parent) : AbstractProtocol(stream, parent), userProtocol_(this) { isScriptValid_ = false; errorLineNumber_ = 0; userProtocolScriptValue_ = engine_.newQObject(&userProtocol_); engine_.globalObject().setProperty("protocol", userProtocolScriptValue_); QScriptValue meta = engine_.newQMetaObject(userProtocol_.metaObject()); engine_.globalObject().setProperty("Protocol", meta); } UserScriptProtocol::~UserScriptProtocol() { } AbstractProtocol* UserScriptProtocol::createInstance(StreamBase *stream, AbstractProtocol *parent) { return new UserScriptProtocol(stream, parent); } quint32 UserScriptProtocol::protocolNumber() const { return OstProto::Protocol::kUserScriptFieldNumber; } void UserScriptProtocol::protoDataCopyInto(OstProto::Protocol &protocol) const { protocol.MutableExtension(OstProto::userScript)->CopyFrom(data); protocol.mutable_protocol_id()->set_id(protocolNumber()); } void UserScriptProtocol::protoDataCopyFrom(const OstProto::Protocol &protocol) { if (protocol.protocol_id().id() == protocolNumber() && protocol.HasExtension(OstProto::userScript)) data.MergeFrom(protocol.GetExtension(OstProto::userScript)); evaluateUserScript(); } QString UserScriptProtocol::name() const { return QString("%1:{UserScript} [EXPERIMENTAL]").arg(userProtocol_.name()); } QString UserScriptProtocol::shortName() const { return QString("%1:{Script} [EXPERIMENTAL]").arg(userProtocol_.name()); } quint32 UserScriptProtocol::protocolId(ProtocolIdType type) const { QScriptValue userFunction; QScriptValue userValue; if (!isScriptValid_) goto _do_default; userFunction = userProtocolScriptValue_.property("protocolId"); if (!userFunction.isValid()) goto _do_default; Q_ASSERT(userFunction.isFunction()); userValue = userFunction.call(QScriptValue(), QScriptValueList() << QScriptValue(&engine_, type)); Q_ASSERT(userValue.isValid()); Q_ASSERT(userValue.isNumber()); return userValue.toUInt32(); _do_default: return AbstractProtocol::protocolId(type); } int UserScriptProtocol::fieldCount() const { return userScript_fieldCount; } QVariant UserScriptProtocol::fieldData(int index, FieldAttrib attrib, int streamIndex) const { switch (index) { case userScript_program: switch(attrib) { case FieldName: return QString("UserProtocol"); case FieldValue: case FieldTextValue: return QString().fromStdString(data.program()); case FieldFrameValue: { if (!isScriptValid_) return QByteArray(); QScriptValue userFunction = userProtocolScriptValue_.property( "protocolFrameValue"); Q_ASSERT(userFunction.isValid()); Q_ASSERT(userFunction.isFunction()); QScriptValue userValue = userFunction.call(QScriptValue(), QScriptValueList() << QScriptValue(&engine_, streamIndex)); Q_ASSERT(userValue.isValid()); Q_ASSERT(userValue.isArray()); QByteArray fv; QList pktBuf; qScriptValueToSequence(userValue, pktBuf); fv.resize(pktBuf.size()); for (int i = 0; i < pktBuf.size(); i++) fv[i] = pktBuf.at(i) & 0xFF; return fv; } default: break; } break; default: qFatal("%s: unimplemented case %d in switch", __PRETTY_FUNCTION__, index); break; } return AbstractProtocol::fieldData(index, attrib, streamIndex); } bool UserScriptProtocol::setFieldData(int index, const QVariant &value, FieldAttrib attrib) { bool isOk = false; if (attrib != FieldValue) goto _exit; switch (index) { case userScript_program: { data.set_program(value.toString().toStdString()); evaluateUserScript(); break; } default: qFatal("%s: unimplemented case %d in switch", __PRETTY_FUNCTION__, index); break; } _exit: return isOk; } int UserScriptProtocol::protocolFrameSize(int streamIndex) const { if (!isScriptValid_) return 0; QScriptValue userFunction = userProtocolScriptValue_.property( "protocolFrameSize"); Q_ASSERT(userFunction.isValid()); Q_ASSERT(userFunction.isFunction()); QScriptValue userValue = userFunction.call(QScriptValue(), QScriptValueList() << QScriptValue(&engine_, streamIndex)); Q_ASSERT(userValue.isNumber()); return userValue.toInt32(); } bool UserScriptProtocol::isProtocolFrameSizeVariable() const { return userProtocol_.isProtocolFrameSizeVariable(); } int UserScriptProtocol::protocolFrameVariableCount() const { return AbstractProtocol::lcm( AbstractProtocol::protocolFrameVariableCount(), userProtocol_.protocolFrameVariableCount()); } quint32 UserScriptProtocol::protocolFrameCksum(int streamIndex, CksumType cksumType, CksumFlags cksumFlags) const { QScriptValue userFunction; QScriptValue userValue; if (!isScriptValid_) goto _do_default; userFunction = userProtocolScriptValue_.property("protocolFrameCksum"); qDebug("userscript protoFrameCksum(): isValid:%d/isFunc:%d", userFunction.isValid(), userFunction.isFunction()); if (!userFunction.isValid()) goto _do_default; Q_ASSERT(userFunction.isFunction()); userValue = userFunction.call( QScriptValue(), QScriptValueList() << QScriptValue(&engine_, streamIndex) << QScriptValue(&engine_, cksumType) << QScriptValue(&engine_, cksumFlags)); Q_ASSERT(userValue.isValid()); Q_ASSERT(userValue.isNumber()); return userValue.toUInt32(); _do_default: return AbstractProtocol::protocolFrameCksum( streamIndex, cksumType, cksumFlags); } void UserScriptProtocol::evaluateUserScript() const { QScriptValue userFunction; QScriptValue userValue; QString property; isScriptValid_ = false; errorLineNumber_ = userScriptLineCount(); // Reset all properties including the dynamic ones userProtocol_.reset(); userProtocolScriptValue_.setProperty("protocolFrameValue", QScriptValue()); userProtocolScriptValue_.setProperty("protocolFrameSize", QScriptValue()); userProtocolScriptValue_.setProperty("protocolFrameCksum", QScriptValue()); userProtocolScriptValue_.setProperty("protocolId", QScriptValue()); engine_.evaluate(fieldData(userScript_program, FieldValue).toString()); if (engine_.hasUncaughtException()) goto _error_exception; // Validate protocolFrameValue() property = QString("protocolFrameValue"); userFunction = userProtocolScriptValue_.property(property); qDebug("userscript property %s: isValid:%d/isFunc:%d", qPrintable(property), userFunction.isValid(), userFunction.isFunction()); if (!userFunction.isValid()) { errorText_ = property + QString(" not set"); goto _error_exit; } if (!userFunction.isFunction()) { errorText_ = property + QString(" is not a function"); goto _error_exit; } userValue = userFunction.call(); if (engine_.hasUncaughtException()) goto _error_exception; qDebug("userscript property %s return value: isValid:%d/isArray:%d", qPrintable(property), userValue.isValid(), userValue.isArray()); if (!userValue.isArray()) { errorText_ = property + QString(" does not return an array"); goto _error_exit; } // Validate protocolFrameSize() property = QString("protocolFrameSize"); userFunction = userProtocolScriptValue_.property(property); qDebug("userscript property %s: isValid:%d/isFunc:%d", qPrintable(property), userFunction.isValid(), userFunction.isFunction()); if (!userFunction.isValid()) { errorText_ = property + QString(" not set"); goto _error_exit; } if (!userFunction.isFunction()) { errorText_ = property + QString(" is not a function"); goto _error_exit; } userValue = userFunction.call(); if (engine_.hasUncaughtException()) goto _error_exception; qDebug("userscript property %s return value: isValid:%d/isNumber:%d", qPrintable(property), userValue.isValid(), userValue.isNumber()); if (!userValue.isNumber()) { errorText_ = property + QString(" does not return a number"); goto _error_exit; } // Validate protocolFrameCksum() [optional] property = QString("protocolFrameCksum"); userFunction = userProtocolScriptValue_.property(property); qDebug("userscript property %s: isValid:%d/isFunc:%d", qPrintable(property), userFunction.isValid(), userFunction.isFunction()); if (!userFunction.isValid()) goto _skip_cksum; if (!userFunction.isFunction()) { errorText_ = property + QString(" is not a function"); goto _error_exit; } userValue = userFunction.call(); if (engine_.hasUncaughtException()) goto _error_exception; qDebug("userscript property %s return value: isValid:%d/isNumber:%d", qPrintable(property), userValue.isValid(), userValue.isNumber()); if (!userValue.isNumber()) { errorText_ = property + QString(" does not return a number"); goto _error_exit; } _skip_cksum: // Validate protocolId() [optional] property = QString("protocolId"); userFunction = userProtocolScriptValue_.property(property); qDebug("userscript property %s: isValid:%d/isFunc:%d", qPrintable(property), userFunction.isValid(), userFunction.isFunction()); if (!userFunction.isValid()) goto _skip_protocol_id; if (!userFunction.isFunction()) { errorText_ = property + QString(" is not a function"); goto _error_exit; } userValue = userFunction.call(); if (engine_.hasUncaughtException()) goto _error_exception; qDebug("userscript property %s return value: isValid:%d/isNumber:%d", qPrintable(property), userValue.isValid(), userValue.isNumber()); if (!userValue.isNumber()) { errorText_ = property + QString(" does not return a number"); goto _error_exit; } _skip_protocol_id: errorText_ = QString(""); isScriptValid_ = true; return; _error_exception: errorLineNumber_ = engine_.uncaughtExceptionLineNumber(); errorText_ = engine_.uncaughtException().toString(); _error_exit: userProtocol_.reset(); return; } bool UserScriptProtocol::isScriptValid() const { return isScriptValid_; } int UserScriptProtocol::userScriptErrorLineNumber() const { return errorLineNumber_; } QString UserScriptProtocol::userScriptErrorText() const { return errorText_; } int UserScriptProtocol::userScriptLineCount() const { return fieldData(userScript_program, FieldValue).toString().count( QChar('\n')) + 1; } // // -------------------- UserProtocol -------------------- // UserProtocol::UserProtocol(AbstractProtocol *parent) : parent_ (parent) { reset(); } void UserProtocol::reset() { name_ = QString(); protocolFrameSizeVariable_ = false; protocolFrameVariableCount_ = 1; } QString UserProtocol::name() const { return name_; } void UserProtocol::setName(QString &name) { name_ = name; } bool UserProtocol::isProtocolFrameSizeVariable() const { return protocolFrameSizeVariable_; } void UserProtocol::setProtocolFrameSizeVariable(bool variable) { protocolFrameSizeVariable_ = variable; } int UserProtocol::protocolFrameVariableCount() const { return protocolFrameVariableCount_; } void UserProtocol::setProtocolFrameVariableCount(int count) { protocolFrameVariableCount_ = count; } quint32 UserProtocol::payloadProtocolId(UserProtocol::ProtocolIdType type) const { return parent_->payloadProtocolId( static_cast(type)); } int UserProtocol::protocolFrameOffset(int streamIndex) const { return parent_->protocolFrameOffset(streamIndex); } int UserProtocol::protocolFramePayloadSize(int streamIndex) const { return parent_->protocolFramePayloadSize(streamIndex); } bool UserProtocol::isProtocolFramePayloadValueVariable() const { return parent_->isProtocolFramePayloadValueVariable(); } bool UserProtocol::isProtocolFramePayloadSizeVariable() const { return parent_->isProtocolFramePayloadSizeVariable(); } int UserProtocol::protocolFramePayloadVariableCount() const { return parent_->protocolFramePayloadVariableCount(); } quint32 UserProtocol::protocolFrameHeaderCksum(int streamIndex, AbstractProtocol::CksumType cksumType) const { return parent_->protocolFrameHeaderCksum(streamIndex, cksumType); } quint32 UserProtocol::protocolFramePayloadCksum(int streamIndex, AbstractProtocol::CksumType cksumType) const { quint32 cksum; cksum = parent_->protocolFramePayloadCksum(streamIndex, cksumType); qDebug("UserProto:%s = %d", __FUNCTION__, cksum); return cksum; } /* vim: set shiftwidth=4 tabstop=8 softtabstop=4 expandtab: */ ostinato-1.3.0/common/userscript.h000066400000000000000000000113421451413623100172200ustar00rootroot00000000000000/* Copyright (C) 2010, 2014 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _USER_SCRIPT_H #define _USER_SCRIPT_H #include "abstractprotocol.h" #include "userscript.pb.h" #include #include class UserScriptProtocol; class UserProtocol : public QObject { Q_OBJECT; Q_ENUMS(ProtocolIdType); Q_ENUMS(CksumType); Q_PROPERTY(QString name READ name WRITE setName); Q_PROPERTY(bool protocolFrameSizeVariable READ isProtocolFrameSizeVariable WRITE setProtocolFrameSizeVariable); Q_PROPERTY(int protocolFrameVariableCount READ protocolFrameVariableCount WRITE setProtocolFrameVariableCount); public: enum ProtocolIdType { ProtocolIdLlc = AbstractProtocol::ProtocolIdLlc, ProtocolIdEth = AbstractProtocol::ProtocolIdEth, ProtocolIdIp = AbstractProtocol::ProtocolIdIp, ProtocolIdTcpUdp = AbstractProtocol::ProtocolIdTcpUdp }; enum CksumType { CksumIp = AbstractProtocol::CksumIp, CksumIpPseudo = AbstractProtocol::CksumIpPseudo, CksumTcpUdp = AbstractProtocol::CksumTcpUdp }; enum CksumFlag { IncludeCksumField = AbstractProtocol::IncludeCksumField }; Q_DECLARE_FLAGS(CksumFlags, CksumFlag); Q_FLAG(CksumFlags); UserProtocol(AbstractProtocol *parent); public slots: void reset(); QString name() const; void setName(QString &name); bool isProtocolFrameSizeVariable() const; void setProtocolFrameSizeVariable(bool variable); int protocolFrameVariableCount() const; void setProtocolFrameVariableCount(int count); quint32 payloadProtocolId(UserProtocol::ProtocolIdType type) const; int protocolFrameOffset(int streamIndex = 0) const; int protocolFramePayloadSize(int streamIndex = 0) const; bool isProtocolFramePayloadValueVariable() const; bool isProtocolFramePayloadSizeVariable() const; int protocolFramePayloadVariableCount() const; quint32 protocolFrameHeaderCksum(int streamIndex = 0, AbstractProtocol::CksumType cksumType = AbstractProtocol::CksumIp) const; quint32 protocolFramePayloadCksum(int streamIndex = 0, AbstractProtocol::CksumType cksumType = AbstractProtocol::CksumIp) const; private: AbstractProtocol *parent_; QString name_; bool protocolFrameSizeVariable_; int protocolFrameVariableCount_; }; class UserScriptProtocol : public AbstractProtocol { public: enum userScriptfield { // Frame Fields userScript_program = 0, userScript_fieldCount }; UserScriptProtocol(StreamBase *stream, AbstractProtocol *parent = 0); virtual ~UserScriptProtocol(); static AbstractProtocol* createInstance(StreamBase *stream, AbstractProtocol *parent = 0); virtual quint32 protocolNumber() const; virtual void protoDataCopyInto(OstProto::Protocol &protocol) const; virtual void protoDataCopyFrom(const OstProto::Protocol &protocol); virtual quint32 protocolId(ProtocolIdType type) const; virtual QString name() const; virtual QString shortName() const; virtual int fieldCount() const; virtual QVariant fieldData(int index, FieldAttrib attrib, int streamIndex = 0) const; virtual bool setFieldData(int index, const QVariant &value, FieldAttrib attrib = FieldValue); virtual int protocolFrameSize(int streamIndex = 0) const; virtual bool isProtocolFrameSizeVariable() const; virtual int protocolFrameVariableCount() const; virtual quint32 protocolFrameCksum(int streamIndex = 0, CksumType cksumType = CksumIp, CksumFlags cksumFlags = 0) const; void evaluateUserScript() const; bool isScriptValid() const; int userScriptErrorLineNumber() const; QString userScriptErrorText() const; private: int userScriptLineCount() const; OstProto::UserScript data; mutable QScriptEngine engine_; mutable UserProtocol userProtocol_; mutable QScriptValue userProtocolScriptValue_; mutable bool isScriptValid_; mutable int errorLineNumber_; mutable QString errorText_; }; #endif ostinato-1.3.0/common/userscript.proto000066400000000000000000000015361451413623100201400ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ import "protocol.proto"; package OstProto; // Sample Protocol message UserScript { optional string program = 1; } extend Protocol { optional UserScript userScript = 103; } ostinato-1.3.0/common/userscript.ui000066400000000000000000000033761451413623100174160ustar00rootroot00000000000000 UserScript 0 0 517 335 Form 10 0 QFrame::Panel QFrame::Sunken 4 4 4 4 4 Unknown Compile ostinato-1.3.0/common/userscriptconfig.cpp000066400000000000000000000057171451413623100207520ustar00rootroot00000000000000/* Copyright (C) 2010, 2014 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #include "userscriptconfig.h" #include "userscript.h" #include UserScriptConfigForm::UserScriptConfigForm(QWidget *parent) : AbstractProtocolConfigForm(parent) { setupUi(this); // The protocol_ (UserScriptProtocol) is a dummy protocol internal // to UserScriptConfigForm whose sole purpose is to "compile" the script // so that the configForm widget can display the compilation result. // It is *not* used for actual packet contents at any time protocol_ = new UserScriptProtocol(NULL); compileScript(); } UserScriptConfigForm::~UserScriptConfigForm() { delete protocol_; } UserScriptConfigForm* UserScriptConfigForm::createInstance() { return new UserScriptConfigForm; } void UserScriptConfigForm::loadWidget(AbstractProtocol *proto) { programEdit->setPlainText( proto->fieldData( UserScriptProtocol::userScript_program, AbstractProtocol::FieldValue ).toString()); compileScript(); } void UserScriptConfigForm::storeWidget(AbstractProtocol *proto) { proto->setFieldData( UserScriptProtocol::userScript_program, programEdit->toPlainText()); } // // ----- private methods // void UserScriptConfigForm::compileScript() { // storeWidget() will save the updated userscript into // the protocol_ which in turn triggers the protocol_ to // compile it storeWidget(protocol_); if (protocol_->isScriptValid()) { statusLabel->setText(QString("Success")); compileButton->setDisabled(true); } else { statusLabel->setText( QString("Error: %1: %2").arg( protocol_->userScriptErrorLineNumber()).arg( protocol_->userScriptErrorText())); compileButton->setEnabled(true); } } // // ----- private slots // void UserScriptConfigForm::on_programEdit_textChanged() { compileButton->setEnabled(true); } void UserScriptConfigForm::on_compileButton_clicked(bool /*checked*/) { compileScript(); if (!protocol_->isScriptValid()) { QMessageBox::critical(this, "Error", QString("%1: %2").arg( protocol_->userScriptErrorLineNumber()).arg( protocol_->userScriptErrorText())); } } ostinato-1.3.0/common/userscriptconfig.h000066400000000000000000000025661451413623100204160ustar00rootroot00000000000000/* Copyright (C) 2010, 2014 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _USER_SCRIPT_CONFIG_H #define _USER_SCRIPT_CONFIG_H #include "abstractprotocolconfig.h" #include "ui_userscript.h" class UserScriptProtocol; class UserScriptConfigForm : public AbstractProtocolConfigForm, private Ui::UserScript { Q_OBJECT public: UserScriptConfigForm(QWidget *parent = 0); virtual ~UserScriptConfigForm(); static UserScriptConfigForm* createInstance(); virtual void loadWidget(AbstractProtocol *proto); virtual void storeWidget(AbstractProtocol *proto); private: void compileScript(); UserScriptProtocol *protocol_; private slots: void on_programEdit_textChanged(); void on_compileButton_clicked(bool checked = false); }; #endif ostinato-1.3.0/common/vlan.cpp000066400000000000000000000155721451413623100163210ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #include "vlan.h" VlanProtocol::VlanProtocol(StreamBase *stream, AbstractProtocol *parent) : AbstractProtocol(stream, parent) { } VlanProtocol::~VlanProtocol() { } AbstractProtocol* VlanProtocol::createInstance(StreamBase *stream, AbstractProtocol *parent) { return new VlanProtocol(stream, parent); } quint32 VlanProtocol::protocolNumber() const { return OstProto::Protocol::kVlanFieldNumber; } void VlanProtocol::protoDataCopyInto(OstProto::Protocol &protocol) const { protocol.MutableExtension(OstProto::vlan)->CopyFrom(data); protocol.mutable_protocol_id()->set_id(protocolNumber()); } void VlanProtocol::protoDataCopyFrom(const OstProto::Protocol &protocol) { if (protocol.protocol_id().id() == protocolNumber() && protocol.HasExtension(OstProto::vlan)) data.MergeFrom(protocol.GetExtension(OstProto::vlan)); } QString VlanProtocol::name() const { return QString("Vlan"); } QString VlanProtocol::shortName() const { return QString("Vlan"); } int VlanProtocol::fieldCount() const { return vlan_fieldCount; } AbstractProtocol::FieldFlags VlanProtocol::fieldFlags(int index) const { AbstractProtocol::FieldFlags flags; flags = AbstractProtocol::fieldFlags(index); switch (index) { case vlan_tpid: case vlan_prio: case vlan_cfiDei: case vlan_vlanId: break; // meta-fields case vlan_isOverrideTpid: flags &= ~FrameField; flags |= MetaField; break; } return flags; } QVariant VlanProtocol::fieldData(int index, FieldAttrib attrib, int streamIndex) const { switch (index) { case vlan_tpid: { quint16 tpid; tpid = data.is_override_tpid() ? data.tpid() : 0x8100; switch(attrib) { case FieldName: return QString("Tag Protocol Id"); case FieldValue: return tpid; case FieldTextValue: return QString("0x%1").arg(tpid, 2, BASE_HEX, QChar('0')); case FieldFrameValue: { QByteArray fv; fv.resize(2); qToBigEndian(tpid, (uchar*) fv.data()); return fv; } default: break; } break; } case vlan_prio: { uint prio = ((data.vlan_tag() >> 13) & 0x07); switch(attrib) { case FieldName: return QString("Priority"); case FieldValue: return prio; case FieldTextValue: return QString("%1").arg(prio); case FieldFrameValue: return QByteArray(1, (char) prio); case FieldBitSize: return 3; default: break; } break; } case vlan_cfiDei: { uint cfiDei = ((data.vlan_tag() >> 12) & 0x01); switch(attrib) { case FieldName: return QString("CFI/DEI"); case FieldValue: return cfiDei; case FieldTextValue: return QString("%1").arg(cfiDei); case FieldFrameValue: return QByteArray(1, (char) cfiDei); case FieldBitSize: return 1; default: break; } break; } case vlan_vlanId: { quint16 vlanId = (data.vlan_tag() & 0x0FFF); switch(attrib) { case FieldName: return QString("VLAN Id"); case FieldValue: return vlanId; case FieldTextValue: return QString("%1").arg(vlanId); case FieldFrameValue: { QByteArray fv; fv.resize(2); qToBigEndian((quint16) vlanId, (uchar*) fv.data()); return fv; } case FieldBitSize: return 12; default: break; } break; } // Meta fields case vlan_isOverrideTpid: switch(attrib) { case FieldValue: return data.is_override_tpid(); default: break; } break; default: break; } return AbstractProtocol::fieldData(index, attrib, streamIndex); } bool VlanProtocol::setFieldData(int index, const QVariant &value, FieldAttrib attrib) { bool isOk = false; if (attrib != FieldValue) goto _exit; switch (index) { case vlan_tpid: { uint tpid = value.toUInt(&isOk); if (isOk) data.set_tpid(tpid); break; } case vlan_prio: { uint prio = value.toUInt(&isOk); if (isOk) data.set_vlan_tag( ((prio & 0x07) << 13) | (data.vlan_tag() & 0x1FFF)); break; } case vlan_cfiDei: { uint cfiDei = value.toUInt(&isOk); if (isOk) data.set_vlan_tag( ((cfiDei & 0x01) << 12) | (data.vlan_tag() & 0xEFFF)); break; } case vlan_vlanId: { uint vlanId = value.toUInt(&isOk); if (isOk) data.set_vlan_tag( (vlanId & 0x0FFF) | (data.vlan_tag() & 0xF000)); break; } // Meta-Fields case vlan_isOverrideTpid: { bool override = value.toUInt(&isOk); if (isOk) data.set_is_override_tpid(override); break; } default: qFatal("%s: unimplemented case %d in switch", __PRETTY_FUNCTION__, index); break; } _exit: return isOk; } ostinato-1.3.0/common/vlan.h000066400000000000000000000034671451413623100157660ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _VLAN_H #define _VLAN_H #include "abstractprotocol.h" #include "vlan.pb.h" class VlanProtocol : public AbstractProtocol { public: enum Vlanfield { vlan_tpid, vlan_prio, vlan_cfiDei, vlan_vlanId, // meta-fields vlan_isOverrideTpid, vlan_fieldCount }; VlanProtocol(StreamBase *stream, AbstractProtocol *parent = 0); virtual ~VlanProtocol(); static AbstractProtocol* createInstance(StreamBase *stream, AbstractProtocol *parent = 0); virtual quint32 protocolNumber() const; virtual void protoDataCopyInto(OstProto::Protocol &protocol) const; virtual void protoDataCopyFrom(const OstProto::Protocol &protocol); virtual QString name() const; virtual QString shortName() const; virtual int fieldCount() const; virtual AbstractProtocol::FieldFlags fieldFlags(int index) const; virtual QVariant fieldData(int index, FieldAttrib attrib, int streamIndex = 0) const; virtual bool setFieldData(int index, const QVariant &value, FieldAttrib attrib = FieldValue); protected: OstProto::Vlan data; }; #endif ostinato-1.3.0/common/vlan.proto000066400000000000000000000017221451413623100166720ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ import "protocol.proto"; package OstProto; message Vlan { // VLAN presence/absence optional bool is_override_tpid = 1; // VLAN values optional uint32 tpid = 2; optional uint32 vlan_tag = 3; // includes prio, cfi and vlanid } extend Protocol { optional Vlan vlan = 205; } ostinato-1.3.0/common/vlan.ui000066400000000000000000000076541451413623100161560ustar00rootroot00000000000000 Vlan 0 0 274 106 Form true Override TPID Priority CFI/DEI VLAN false >HH HH; true 0 1 2 3 4 5 6 7 true 0 1 true 0 Qt::Horizontal 111 20 Qt::Vertical 20 51 cbTpidOverride toggled(bool) leTpid setEnabled(bool) 59 41 59 57 ostinato-1.3.0/common/vlanconfig.cpp000066400000000000000000000047331451413623100175040ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #include "vlanconfig.h" #include "vlan.h" VlanConfigForm::VlanConfigForm(QWidget *parent) : AbstractProtocolConfigForm(parent) { setupUi(this); } VlanConfigForm::~VlanConfigForm() { } VlanConfigForm* VlanConfigForm::createInstance() { return new VlanConfigForm; } void VlanConfigForm::loadWidget(AbstractProtocol *proto) { cbTpidOverride->setChecked( proto->fieldData( VlanProtocol::vlan_isOverrideTpid, AbstractProtocol::FieldValue ).toBool()); leTpid->setText(uintToHexStr( proto->fieldData( VlanProtocol::vlan_tpid, AbstractProtocol::FieldValue) .toUInt(), 2)); cmbPrio->setCurrentIndex( proto->fieldData( VlanProtocol::vlan_prio, AbstractProtocol::FieldValue) .toUInt()); cmbCfiDei->setCurrentIndex( proto->fieldData( VlanProtocol::vlan_cfiDei, AbstractProtocol::FieldValue) .toUInt()); leVlanId->setText( proto->fieldData( VlanProtocol::vlan_vlanId, AbstractProtocol::FieldValue) .toString()); } void VlanConfigForm::storeWidget(AbstractProtocol *proto) { bool isOk; proto->setFieldData( VlanProtocol::vlan_isOverrideTpid, cbTpidOverride->isChecked()); proto->setFieldData( VlanProtocol::vlan_tpid, leTpid->text().remove(QChar(' ')).toUInt(&isOk, BASE_HEX)); proto->setFieldData( VlanProtocol::vlan_prio, cmbPrio->currentIndex()); proto->setFieldData( VlanProtocol::vlan_cfiDei, cmbCfiDei->currentIndex()); proto->setFieldData( VlanProtocol::vlan_vlanId, leVlanId->text()); } ostinato-1.3.0/common/vlanconfig.h000066400000000000000000000021461451413623100171450ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _VLAN_CONFIG_H #define _VLAN_CONFIG_H #include "abstractprotocolconfig.h" #include "ui_vlan.h" class VlanConfigForm : public AbstractProtocolConfigForm, private Ui::Vlan { Q_OBJECT public: VlanConfigForm(QWidget *parent = 0); virtual ~VlanConfigForm(); static VlanConfigForm* createInstance(); virtual void loadWidget(AbstractProtocol *proto); virtual void storeWidget(AbstractProtocol *proto); }; #endif ostinato-1.3.0/common/vlanpdml.cpp000066400000000000000000000060531451413623100171700ustar00rootroot00000000000000/* Copyright (C) 2011 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #include "vlanpdml.h" #include "eth2.pb.h" #include "vlan.pb.h" PdmlVlanProtocol::PdmlVlanProtocol() { ostProtoId_ = OstProto::Protocol::kVlanFieldNumber; } PdmlProtocol* PdmlVlanProtocol::createInstance() { return new PdmlVlanProtocol(); } void PdmlVlanProtocol::preProtocolHandler(QString /*name*/, const QXmlStreamAttributes& /*attributes*/, int /*expectedPos*/, OstProto::Protocol *pbProto, OstProto::Stream *stream) { OstProto::Vlan *vlan = pbProto->MutableExtension(OstProto::vlan); vlan->set_tpid(0x8100); vlan->set_is_override_tpid(true); // If a eth2 protocol precedes vlan, we remove the eth2 protocol // 'coz the eth2.etherType is actually the vlan.tpid // // We assume that the current protocol is the last in the stream int index = stream->protocol_size() - 1; if ((index > 1) && (stream->protocol(index).protocol_id().id() == OstProto::Protocol::kVlanFieldNumber) && (stream->protocol(index - 1).protocol_id().id() == OstProto::Protocol::kEth2FieldNumber)) { stream->mutable_protocol()->SwapElements(index, index - 1); Q_ASSERT(stream->protocol(index).protocol_id().id() == OstProto::Protocol::kEth2FieldNumber); stream->mutable_protocol()->RemoveLast(); } } void PdmlVlanProtocol::unknownFieldHandler(QString name, int /*pos*/, int /*size*/, const QXmlStreamAttributes &attributes, OstProto::Protocol *pbProto, OstProto::Stream *stream) { if (name == "vlan.id") { bool isOk; OstProto::Vlan *vlan = pbProto->MutableExtension(OstProto::vlan); uint tag = attributes.value("unmaskedvalue").isEmpty() ? attributes.value("value").toString().toUInt(&isOk, kBaseHex) : attributes.value("unmaskedvalue").toString().toUInt(&isOk,kBaseHex); vlan->set_vlan_tag(tag); } else if (name == "vlan.etype") { OstProto::Protocol *proto = stream->add_protocol(); proto->mutable_protocol_id()->set_id( OstProto::Protocol::kEth2FieldNumber); bool isOk; OstProto::Eth2 *eth2 = proto->MutableExtension(OstProto::eth2); eth2->set_type(attributes.value("value") .toString().toUInt(&isOk, kBaseHex)); eth2->set_is_override_type(true); } } ostinato-1.3.0/common/vlanpdml.h000066400000000000000000000023661451413623100166400ustar00rootroot00000000000000/* Copyright (C) 2011 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _VLAN_PDML_H #define _VLAN_PDML_H #include "pdmlprotocol.h" class PdmlVlanProtocol : public PdmlProtocol { public: static PdmlProtocol* createInstance(); virtual void preProtocolHandler(QString name, const QXmlStreamAttributes &attributes, int expectedPos, OstProto::Protocol *pbProto, OstProto::Stream *stream); virtual void unknownFieldHandler(QString name, int pos, int size, const QXmlStreamAttributes &attributes, OstProto::Protocol *pbProto, OstProto::Stream *stream); protected: PdmlVlanProtocol(); }; #endif ostinato-1.3.0/common/vlanstack.h000066400000000000000000000016161451413623100170060ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _VLAN_STACK_H #define _VLAN_STACK_H #include "comboprotocol.h" #include "svlan.h" #include "vlan.h" typedef ComboProtocol VlanStackProtocol; #endif ostinato-1.3.0/common/vlanstack.proto000066400000000000000000000015511451413623100177200ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ import "protocol.proto"; package OstProto; // Stacked VLAN (2 tags) message VlanStack { // Empty since this is a 'combo' protocol } extend Protocol { optional VlanStack vlanStack = 208; } ostinato-1.3.0/common/vlanstackconfig.h000066400000000000000000000020701451413623100201670ustar00rootroot00000000000000/* Copyright (C) 2014 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _VLAN_STACK_CONFIG_H #define _VLAN_STACK_CONFIG_H #include "comboprotocolconfig.h" #include "svlanconfig.h" #include "vlanconfig.h" #include "svlan.h" #include "vlan.h" #include "protocol.pb.h" typedef ComboProtocolConfigForm < OstProto::Protocol::kVlanStackFieldNumber, SVlanConfigForm, VlanConfigForm, SVlanProtocol, VlanProtocol > VlanStackConfigForm; #endif ostinato-1.3.0/extra/000077500000000000000000000000001451413623100144765ustar00rootroot00000000000000ostinato-1.3.0/extra/extra.pro000066400000000000000000000001501451413623100163370ustar00rootroot00000000000000TEMPLATE = subdirs SUBDIRS = \ qhexedit2 greaterThan(QT_MINOR_VERSION, 6) { SUBDIRS += modeltest } ostinato-1.3.0/extra/modeltest/000077500000000000000000000000001451413623100164765ustar00rootroot00000000000000ostinato-1.3.0/extra/modeltest/dynamictreemodel.cpp000066400000000000000000000247551451413623100225440ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (C) 2009 Stephen Kelly ** Contact: https://www.qt.io/licensing/ ** ** This file is part of the test suite of the Qt Toolkit. ** ** $QT_BEGIN_LICENSE:GPL-EXCEPT$ ** Commercial License Usage ** Licensees holding valid commercial Qt licenses may use this file in ** accordance with the commercial license agreement provided with the ** Software or, alternatively, in accordance with the terms contained in ** a written agreement between you and The Qt Company. For licensing terms ** and conditions see https://www.qt.io/terms-conditions. For further ** information use the contact form at https://www.qt.io/contact-us. ** ** GNU General Public License Usage ** Alternatively, this file may be used under the terms of the GNU ** General Public License version 3 as published by the Free Software ** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT ** included in the packaging of this file. Please review the following ** information to ensure the GNU General Public License requirements will ** be met: https://www.gnu.org/licenses/gpl-3.0.html. ** ** $QT_END_LICENSE$ ** ****************************************************************************/ #include "dynamictreemodel.h" #include #include #include #include DynamicTreeModel::DynamicTreeModel(QObject *parent) : QAbstractItemModel(parent), nextId(1) { } QModelIndex DynamicTreeModel::index(int row, int column, const QModelIndex &parent) const { // if (column != 0) // return QModelIndex(); if ( column < 0 || row < 0 ) return QModelIndex(); QList > childIdColumns = m_childItems.value(parent.internalId()); const qint64 grandParent = findParentId(parent.internalId()); if (grandParent >= 0) { QList > parentTable = m_childItems.value(grandParent); if (parent.column() >= parentTable.size()) qFatal("%s: parent.column() must be less than parentTable.size()", Q_FUNC_INFO); QList parentSiblings = parentTable.at(parent.column()); if (parent.row() >= parentSiblings.size()) qFatal("%s: parent.row() must be less than parentSiblings.size()", Q_FUNC_INFO); } if (childIdColumns.size() == 0) return QModelIndex(); if (column >= childIdColumns.size()) return QModelIndex(); QList rowIds = childIdColumns.at(column); if ( row >= rowIds.size()) return QModelIndex(); qint64 id = rowIds.at(row); return createIndex(row, column, reinterpret_cast(id)); } qint64 DynamicTreeModel::findParentId(qint64 searchId) const { if (searchId <= 0) return -1; QHashIterator > > i(m_childItems); while (i.hasNext()) { i.next(); QListIterator > j(i.value()); while (j.hasNext()) { QList l = j.next(); if (l.contains(searchId)) { return i.key(); } } } return -1; } QModelIndex DynamicTreeModel::parent(const QModelIndex &index) const { if (!index.isValid()) return QModelIndex(); qint64 searchId = index.internalId(); qint64 parentId = findParentId(searchId); // Will never happen for valid index, but what the hey... if (parentId <= 0) return QModelIndex(); qint64 grandParentId = findParentId(parentId); if (grandParentId < 0) grandParentId = 0; int column = 0; QList childList = m_childItems.value(grandParentId).at(column); int row = childList.indexOf(parentId); return createIndex(row, column, reinterpret_cast(parentId)); } int DynamicTreeModel::rowCount(const QModelIndex &index ) const { QList > cols = m_childItems.value(index.internalId()); if (cols.size() == 0 ) return 0; if (index.column() > 0) return 0; return cols.at(0).size(); } int DynamicTreeModel::columnCount(const QModelIndex &index ) const { // Q_UNUSED(index); return m_childItems.value(index.internalId()).size(); } QVariant DynamicTreeModel::data(const QModelIndex &index, int role) const { if (!index.isValid()) return QVariant(); if (Qt::DisplayRole == role) { return m_items.value(index.internalId()); } return QVariant(); } void DynamicTreeModel::clear() { beginResetModel(); m_items.clear(); m_childItems.clear(); nextId = 1; endResetModel(); } ModelChangeCommand::ModelChangeCommand( DynamicTreeModel *model, QObject *parent ) : QObject(parent), m_model(model), m_numCols(1), m_startRow(-1), m_endRow(-1) { } QModelIndex ModelChangeCommand::findIndex(QList rows) { const int col = 0; QModelIndex parent = QModelIndex(); QListIterator i(rows); while (i.hasNext()) { parent = m_model->index(i.next(), col, parent); if (!parent.isValid()) qFatal("%s: parent must be valid", Q_FUNC_INFO); } return parent; } ModelInsertCommand::ModelInsertCommand(DynamicTreeModel *model, QObject *parent ) : ModelChangeCommand(model, parent) { } void ModelInsertCommand::doCommand() { QModelIndex parent = findIndex(m_rowNumbers); m_model->beginInsertRows(parent, m_startRow, m_endRow); qint64 parentId = parent.internalId(); for (int row = m_startRow; row <= m_endRow; row++) { for(int col = 0; col < m_numCols; col++ ) { if (m_model->m_childItems[parentId].size() <= col) { m_model->m_childItems[parentId].append(QList()); } // QString name = QUuid::createUuid().toString(); qint64 id = m_model->newId(); QString name = QString::number(id); m_model->m_items.insert(id, name); m_model->m_childItems[parentId][col].insert(row, id); } } m_model->endInsertRows(); } ModelMoveCommand::ModelMoveCommand(DynamicTreeModel *model, QObject *parent) : ModelChangeCommand(model, parent) { } bool ModelMoveCommand::emitPreSignal(const QModelIndex &srcParent, int srcStart, int srcEnd, const QModelIndex &destParent, int destRow) { return m_model->beginMoveRows(srcParent, srcStart, srcEnd, destParent, destRow); } void ModelMoveCommand::doCommand() { QModelIndex srcParent = findIndex(m_rowNumbers); QModelIndex destParent = findIndex(m_destRowNumbers); if (!emitPreSignal(srcParent, m_startRow, m_endRow, destParent, m_destRow)) { return; } for (int column = 0; column < m_numCols; ++column) { QList l = m_model->m_childItems.value(srcParent.internalId())[column].mid(m_startRow, m_endRow - m_startRow + 1 ); for (int i = m_startRow; i <= m_endRow ; i++) { m_model->m_childItems[srcParent.internalId()][column].removeAt(m_startRow); } int d; if (m_destRow < m_startRow) d = m_destRow; else { if (srcParent == destParent) d = m_destRow - (m_endRow - m_startRow + 1); else d = m_destRow - (m_endRow - m_startRow) + 1; } foreach(const qint64 id, l) { m_model->m_childItems[destParent.internalId()][column].insert(d++, id); } } emitPostSignal(); } void ModelMoveCommand::emitPostSignal() { m_model->endMoveRows(); } ModelResetCommand::ModelResetCommand(DynamicTreeModel* model, QObject* parent) : ModelMoveCommand(model, parent) { } ModelResetCommand::~ModelResetCommand() { } bool ModelResetCommand::emitPreSignal(const QModelIndex &srcParent, int srcStart, int srcEnd, const QModelIndex &destParent, int destRow) { Q_UNUSED(srcParent); Q_UNUSED(srcStart); Q_UNUSED(srcEnd); Q_UNUSED(destParent); Q_UNUSED(destRow); return true; } void ModelResetCommand::emitPostSignal() { m_model->beginResetModel(); m_model->endResetModel(); } ModelResetCommandFixed::ModelResetCommandFixed(DynamicTreeModel* model, QObject* parent) : ModelMoveCommand(model, parent) { } ModelResetCommandFixed::~ModelResetCommandFixed() { } bool ModelResetCommandFixed::emitPreSignal(const QModelIndex &srcParent, int srcStart, int srcEnd, const QModelIndex &destParent, int destRow) { Q_UNUSED(srcParent); Q_UNUSED(srcStart); Q_UNUSED(srcEnd); Q_UNUSED(destParent); Q_UNUSED(destRow); m_model->beginResetModel(); return true; } void ModelResetCommandFixed::emitPostSignal() { m_model->endResetModel(); } ModelChangeChildrenLayoutsCommand::ModelChangeChildrenLayoutsCommand(DynamicTreeModel* model, QObject* parent) : ModelChangeCommand(model, parent) { } void ModelChangeChildrenLayoutsCommand::doCommand() { const QPersistentModelIndex parent1 = findIndex(m_rowNumbers); const QPersistentModelIndex parent2 = findIndex(m_secondRowNumbers); QList parents; parents << parent1; parents << parent2; emit m_model->layoutAboutToBeChanged(parents); int rowSize1 = -1; int rowSize2 = -1; for (int column = 0; column < m_numCols; ++column) { { QList &l = m_model->m_childItems[parent1.internalId()][column]; rowSize1 = l.size(); l.prepend(l.takeLast()); } { QList &l = m_model->m_childItems[parent2.internalId()][column]; rowSize2 = l.size(); l.append(l.takeFirst()); } } // If we're changing one of the parent indexes, we need to ensure that we do that before // changing any children of that parent. The reason is that we're keeping parent1 and parent2 // around as QPersistentModelIndex instances, and we query idx.parent() in the loop. QModelIndexList persistent = m_model->persistentIndexList(); foreach (const QModelIndex &parent, parents) { int idx = persistent.indexOf(parent); if (idx != -1) persistent.move(idx, 0); } foreach (const QModelIndex &idx, persistent) { if (idx.parent() == parent1) { if (idx.row() == rowSize1 - 1) { m_model->changePersistentIndex(idx, m_model->createIndex(0, idx.column(), idx.internalPointer())); } else { m_model->changePersistentIndex(idx, m_model->createIndex(idx.row() + 1, idx.column(), idx.internalPointer())); } } else if (idx.parent() == parent2) { if (idx.row() == 0) { m_model->changePersistentIndex(idx, m_model->createIndex(rowSize2 - 1, idx.column(), idx.internalPointer())); } else { m_model->changePersistentIndex(idx, m_model->createIndex(idx.row() - 1, idx.column(), idx.internalPointer())); } } } emit m_model->layoutChanged(parents); } ostinato-1.3.0/extra/modeltest/dynamictreemodel.h000066400000000000000000000121121451413623100221710ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (C) 2009 Stephen Kelly ** Contact: https://www.qt.io/licensing/ ** ** This file is part of the test suite of the Qt Toolkit. ** ** $QT_BEGIN_LICENSE:GPL-EXCEPT$ ** Commercial License Usage ** Licensees holding valid commercial Qt licenses may use this file in ** accordance with the commercial license agreement provided with the ** Software or, alternatively, in accordance with the terms contained in ** a written agreement between you and The Qt Company. For licensing terms ** and conditions see https://www.qt.io/terms-conditions. For further ** information use the contact form at https://www.qt.io/contact-us. ** ** GNU General Public License Usage ** Alternatively, this file may be used under the terms of the GNU ** General Public License version 3 as published by the Free Software ** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT ** included in the packaging of this file. Please review the following ** information to ensure the GNU General Public License requirements will ** be met: https://www.gnu.org/licenses/gpl-3.0.html. ** ** $QT_END_LICENSE$ ** ****************************************************************************/ #ifndef DYNAMICTREEMODEL_H #define DYNAMICTREEMODEL_H #include #include #include class DynamicTreeModel : public QAbstractItemModel { Q_OBJECT public: DynamicTreeModel(QObject *parent = 0); QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const; QModelIndex parent(const QModelIndex &index) const; int rowCount(const QModelIndex &index = QModelIndex()) const; int columnCount(const QModelIndex &index = QModelIndex()) const; QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const; void clear(); protected slots: /** Finds the parent id of the string with id @p searchId. Returns -1 if not found. */ qint64 findParentId(qint64 searchId) const; private: QHash m_items; QHash > > m_childItems; qint64 nextId; qint64 newId() { return nextId++; }; QModelIndex m_nextParentIndex; int m_nextRow; int m_depth; int maxDepth; friend class ModelInsertCommand; friend class ModelMoveCommand; friend class ModelResetCommand; friend class ModelResetCommandFixed; friend class ModelChangeChildrenLayoutsCommand; }; class ModelChangeCommand : public QObject { Q_OBJECT public: ModelChangeCommand( DynamicTreeModel *model, QObject *parent = 0 ); virtual ~ModelChangeCommand() {} void setAncestorRowNumbers(QList rowNumbers) { m_rowNumbers = rowNumbers; } QModelIndex findIndex(QList rows); void setStartRow(int row) { m_startRow = row; } void setEndRow(int row) { m_endRow = row; } void setNumCols(int cols) { m_numCols = cols; } virtual void doCommand() = 0; protected: DynamicTreeModel* m_model; QList m_rowNumbers; int m_numCols; int m_startRow; int m_endRow; }; typedef QList ModelChangeCommandList; class ModelInsertCommand : public ModelChangeCommand { Q_OBJECT public: ModelInsertCommand(DynamicTreeModel *model, QObject *parent = 0 ); virtual ~ModelInsertCommand() {} virtual void doCommand(); }; class ModelMoveCommand : public ModelChangeCommand { Q_OBJECT public: ModelMoveCommand(DynamicTreeModel *model, QObject *parent); virtual ~ModelMoveCommand() {} virtual bool emitPreSignal(const QModelIndex &srcParent, int srcStart, int srcEnd, const QModelIndex &destParent, int destRow); virtual void doCommand(); virtual void emitPostSignal(); void setDestAncestors( QList rows ) { m_destRowNumbers = rows; } void setDestRow(int row) { m_destRow = row; } protected: QList m_destRowNumbers; int m_destRow; }; /** A command which does a move and emits a reset signal. */ class ModelResetCommand : public ModelMoveCommand { Q_OBJECT public: ModelResetCommand(DynamicTreeModel* model, QObject* parent = 0); virtual ~ModelResetCommand(); virtual bool emitPreSignal(const QModelIndex &srcParent, int srcStart, int srcEnd, const QModelIndex &destParent, int destRow); virtual void emitPostSignal(); }; /** A command which does a move and emits a beginResetModel and endResetModel signals. */ class ModelResetCommandFixed : public ModelMoveCommand { Q_OBJECT public: ModelResetCommandFixed(DynamicTreeModel* model, QObject* parent = 0); virtual ~ModelResetCommandFixed(); virtual bool emitPreSignal(const QModelIndex &srcParent, int srcStart, int srcEnd, const QModelIndex &destParent, int destRow); virtual void emitPostSignal(); }; class ModelChangeChildrenLayoutsCommand : public ModelChangeCommand { Q_OBJECT public: ModelChangeChildrenLayoutsCommand(DynamicTreeModel *model, QObject *parent); virtual ~ModelChangeChildrenLayoutsCommand() {} virtual void doCommand(); void setSecondAncestorRowNumbers( QList rows ) { m_secondRowNumbers = rows; } protected: QList m_secondRowNumbers; int m_destRow; }; #endif ostinato-1.3.0/extra/modeltest/modeltest.cpp000066400000000000000000000525261451413623100212140ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (C) 2016 The Qt Company Ltd. ** Contact: https://www.qt.io/licensing/ ** ** This file is part of the test suite of the Qt Toolkit. ** ** $QT_BEGIN_LICENSE:GPL-EXCEPT$ ** Commercial License Usage ** Licensees holding valid commercial Qt licenses may use this file in ** accordance with the commercial license agreement provided with the ** Software or, alternatively, in accordance with the terms contained in ** a written agreement between you and The Qt Company. For licensing terms ** and conditions see https://www.qt.io/terms-conditions. For further ** information use the contact form at https://www.qt.io/contact-us. ** ** GNU General Public License Usage ** Alternatively, this file may be used under the terms of the GNU ** General Public License version 3 as published by the Free Software ** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT ** included in the packaging of this file. Please review the following ** information to ensure the GNU General Public License requirements will ** be met: https://www.gnu.org/licenses/gpl-3.0.html. ** ** $QT_END_LICENSE$ ** ****************************************************************************/ #include "modeltest.h" #include #include /*! Connect to all of the models signals. Whenever anything happens recheck everything. */ ModelTest::ModelTest ( QAbstractItemModel *_model, QObject *parent ) : QObject ( parent ), model ( _model ), fetchingMore ( false ) { if (!model) qFatal("%s: model must not be null", Q_FUNC_INFO); connect(model, SIGNAL(columnsAboutToBeInserted(QModelIndex,int,int)), this, SLOT(runAllTests()) ); connect(model, SIGNAL(columnsAboutToBeRemoved(QModelIndex,int,int)), this, SLOT(runAllTests()) ); connect(model, SIGNAL(columnsInserted(QModelIndex,int,int)), this, SLOT(runAllTests()) ); connect(model, SIGNAL(columnsRemoved(QModelIndex,int,int)), this, SLOT(runAllTests()) ); connect(model, SIGNAL(dataChanged(QModelIndex,QModelIndex)), this, SLOT(runAllTests()) ); connect(model, SIGNAL(headerDataChanged(Qt::Orientation,int,int)), this, SLOT(runAllTests()) ); connect(model, SIGNAL(layoutAboutToBeChanged()), this, SLOT(runAllTests()) ); connect(model, SIGNAL(layoutChanged()), this, SLOT(runAllTests()) ); connect(model, SIGNAL(modelReset()), this, SLOT(runAllTests()) ); connect(model, SIGNAL(rowsAboutToBeInserted(QModelIndex,int,int)), this, SLOT(runAllTests()) ); connect(model, SIGNAL(rowsAboutToBeRemoved(QModelIndex,int,int)), this, SLOT(runAllTests()) ); connect(model, SIGNAL(rowsInserted(QModelIndex,int,int)), this, SLOT(runAllTests()) ); connect(model, SIGNAL(rowsRemoved(QModelIndex,int,int)), this, SLOT(runAllTests()) ); // Special checks for changes connect(model, SIGNAL(layoutAboutToBeChanged()), this, SLOT(layoutAboutToBeChanged()) ); connect(model, SIGNAL(layoutChanged()), this, SLOT(layoutChanged()) ); connect(model, SIGNAL(rowsAboutToBeInserted(QModelIndex,int,int)), this, SLOT(rowsAboutToBeInserted(QModelIndex,int,int)) ); connect(model, SIGNAL(rowsAboutToBeRemoved(QModelIndex,int,int)), this, SLOT(rowsAboutToBeRemoved(QModelIndex,int,int)) ); connect(model, SIGNAL(rowsInserted(QModelIndex,int,int)), this, SLOT(rowsInserted(QModelIndex,int,int)) ); connect(model, SIGNAL(rowsRemoved(QModelIndex,int,int)), this, SLOT(rowsRemoved(QModelIndex,int,int)) ); connect(model, SIGNAL(dataChanged(QModelIndex,QModelIndex)), this, SLOT(dataChanged(QModelIndex,QModelIndex)) ); connect(model, SIGNAL(headerDataChanged(Qt::Orientation,int,int)), this, SLOT(headerDataChanged(Qt::Orientation,int,int)) ); runAllTests(); } void ModelTest::runAllTests() { if ( fetchingMore ) return; nonDestructiveBasicTest(); rowCount(); columnCount(); hasIndex(); index(); parent(); data(); } /*! nonDestructiveBasicTest tries to call a number of the basic functions (not all) to make sure the model doesn't outright segfault, testing the functions that makes sense. */ void ModelTest::nonDestructiveBasicTest() { QVERIFY(!model->buddy(QModelIndex()).isValid()); model->canFetchMore ( QModelIndex() ); QVERIFY( model->columnCount ( QModelIndex() ) >= 0 ); QCOMPARE(model->data(QModelIndex()), QVariant()); fetchingMore = true; model->fetchMore ( QModelIndex() ); fetchingMore = false; Qt::ItemFlags flags = model->flags ( QModelIndex() ); QVERIFY( flags == Qt::ItemIsDropEnabled || flags == 0 ); model->hasChildren ( QModelIndex() ); model->hasIndex ( 0, 0 ); model->headerData ( 0, Qt::Horizontal ); model->index ( 0, 0 ); model->itemData ( QModelIndex() ); QVariant cache; model->match ( QModelIndex(), -1, cache ); model->mimeTypes(); QVERIFY(!model->parent(QModelIndex()).isValid()); QVERIFY( model->rowCount() >= 0 ); QVariant variant; model->setData ( QModelIndex(), variant, -1 ); model->setHeaderData ( -1, Qt::Horizontal, QVariant() ); model->setHeaderData ( 999999, Qt::Horizontal, QVariant() ); QMap roles; model->sibling ( 0, 0, QModelIndex() ); model->span ( QModelIndex() ); model->supportedDropActions(); } /*! Tests model's implementation of QAbstractItemModel::rowCount() and hasChildren() Models that are dynamically populated are not as fully tested here. */ void ModelTest::rowCount() { // qDebug() << "rc"; // check top row QModelIndex topIndex = model->index ( 0, 0, QModelIndex() ); int rows = model->rowCount ( topIndex ); QVERIFY( rows >= 0 ); if ( rows > 0 ) QVERIFY( model->hasChildren ( topIndex ) ); QModelIndex secondLevelIndex = model->index ( 0, 0, topIndex ); if ( secondLevelIndex.isValid() ) { // not the top level // check a row count where parent is valid rows = model->rowCount ( secondLevelIndex ); QVERIFY( rows >= 0 ); if ( rows > 0 ) QVERIFY( model->hasChildren ( secondLevelIndex ) ); } // The models rowCount() is tested more extensively in checkChildren(), // but this catches the big mistakes } /*! Tests model's implementation of QAbstractItemModel::columnCount() and hasChildren() */ void ModelTest::columnCount() { // check top row QModelIndex topIndex = model->index ( 0, 0, QModelIndex() ); QVERIFY( model->columnCount ( topIndex ) >= 0 ); // check a column count where parent is valid QModelIndex childIndex = model->index ( 0, 0, topIndex ); if ( childIndex.isValid() ) QVERIFY( model->columnCount ( childIndex ) >= 0 ); // columnCount() is tested more extensively in checkChildren(), // but this catches the big mistakes } /*! Tests model's implementation of QAbstractItemModel::hasIndex() */ void ModelTest::hasIndex() { // qDebug() << "hi"; // Make sure that invalid values returns an invalid index QVERIFY( !model->hasIndex ( -2, -2 ) ); QVERIFY( !model->hasIndex ( -2, 0 ) ); QVERIFY( !model->hasIndex ( 0, -2 ) ); int rows = model->rowCount(); int columns = model->columnCount(); // check out of bounds QVERIFY( !model->hasIndex ( rows, columns ) ); QVERIFY( !model->hasIndex ( rows + 1, columns + 1 ) ); if ( rows > 0 ) QVERIFY( model->hasIndex ( 0, 0 ) ); // hasIndex() is tested more extensively in checkChildren(), // but this catches the big mistakes } /*! Tests model's implementation of QAbstractItemModel::index() */ void ModelTest::index() { // qDebug() << "i"; // Make sure that invalid values returns an invalid index QVERIFY(!model->index(-2, -2).isValid()); QVERIFY(!model->index(-2, 0).isValid()); QVERIFY(!model->index(0, -2).isValid()); int rows = model->rowCount(); int columns = model->columnCount(); if ( rows == 0 ) return; // Catch off by one errors QVERIFY(!model->index(rows, columns).isValid()); QVERIFY(model->index(0, 0).isValid()); // Make sure that the same index is *always* returned QModelIndex a = model->index ( 0, 0 ); QModelIndex b = model->index ( 0, 0 ); QCOMPARE(a, b); // index() is tested more extensively in checkChildren(), // but this catches the big mistakes } /*! Tests model's implementation of QAbstractItemModel::parent() */ void ModelTest::parent() { // qDebug() << "p"; // Make sure the model won't crash and will return an invalid QModelIndex // when asked for the parent of an invalid index. QVERIFY(!model->parent(QModelIndex()).isValid()); if ( model->rowCount() == 0 ) return; // Column 0 | Column 1 | // QModelIndex() | | // \- topIndex | topIndex1 | // \- childIndex | childIndex1 | // Common error test #1, make sure that a top level index has a parent // that is a invalid QModelIndex. QModelIndex topIndex = model->index ( 0, 0, QModelIndex() ); QVERIFY(!model->parent(topIndex).isValid()); // Common error test #2, make sure that a second level index has a parent // that is the first level index. if ( model->rowCount ( topIndex ) > 0 ) { QModelIndex childIndex = model->index ( 0, 0, topIndex ); QCOMPARE(model->parent(childIndex), topIndex); } // Common error test #3, the second column should NOT have the same children // as the first column in a row. // Usually the second column shouldn't have children. QModelIndex topIndex1 = model->index ( 0, 1, QModelIndex() ); if ( model->rowCount ( topIndex1 ) > 0 ) { QModelIndex childIndex = model->index ( 0, 0, topIndex ); QModelIndex childIndex1 = model->index ( 0, 0, topIndex1 ); QVERIFY( childIndex != childIndex1 ); } // Full test, walk n levels deep through the model making sure that all // parent's children correctly specify their parent. checkChildren ( QModelIndex() ); } /*! Called from the parent() test. A model that returns an index of parent X should also return X when asking for the parent of the index. This recursive function does pretty extensive testing on the whole model in an effort to catch edge cases. This function assumes that rowCount(), columnCount() and index() already work. If they have a bug it will point it out, but the above tests should have already found the basic bugs because it is easier to figure out the problem in those tests then this one. */ void ModelTest::checkChildren ( const QModelIndex &parent, int currentDepth ) { // First just try walking back up the tree. QModelIndex p = parent; while ( p.isValid() ) p = p.parent(); // For models that are dynamically populated if ( model->canFetchMore ( parent ) ) { fetchingMore = true; model->fetchMore ( parent ); fetchingMore = false; } int rows = model->rowCount ( parent ); int columns = model->columnCount ( parent ); if ( rows > 0 ) QVERIFY( model->hasChildren ( parent ) ); // Some further testing against rows(), columns(), and hasChildren() QVERIFY( rows >= 0 ); QVERIFY( columns >= 0 ); if ( rows > 0 ) QVERIFY( model->hasChildren ( parent ) ); //qDebug() << "parent:" << model->data(parent).toString() << "rows:" << rows // << "columns:" << columns << "parent column:" << parent.column(); const QModelIndex topLeftChild = model->index( 0, 0, parent ); QVERIFY( !model->hasIndex ( rows + 1, 0, parent ) ); for ( int r = 0; r < rows; ++r ) { if ( model->canFetchMore ( parent ) ) { fetchingMore = true; model->fetchMore ( parent ); fetchingMore = false; } QVERIFY( !model->hasIndex ( r, columns + 1, parent ) ); for ( int c = 0; c < columns; ++c ) { QVERIFY( model->hasIndex ( r, c, parent ) ); QModelIndex index = model->index ( r, c, parent ); // rowCount() and columnCount() said that it existed... QVERIFY(index.isValid()); // index() should always return the same index when called twice in a row QModelIndex modifiedIndex = model->index ( r, c, parent ); QCOMPARE(index, modifiedIndex); // Make sure we get the same index if we request it twice in a row QModelIndex a = model->index ( r, c, parent ); QModelIndex b = model->index ( r, c, parent ); QCOMPARE(a, b); { const QModelIndex sibling = model->sibling( r, c, topLeftChild ); QCOMPARE(index, sibling); } { const QModelIndex sibling = topLeftChild.sibling( r, c ); QCOMPARE(index, sibling); } // Some basic checking on the index that is returned QCOMPARE(index.model(), model); QCOMPARE( index.row(), r ); QCOMPARE( index.column(), c ); // While you can technically return a QVariant usually this is a sign // of a bug in data(). Disable if this really is ok in your model. // QVERIFY( model->data ( index, Qt::DisplayRole ).isValid() ); // If the next test fails here is some somewhat useful debug you play with. if (model->parent(index) != parent) { qDebug() << r << c << currentDepth << model->data(index).toString() << model->data(parent).toString(); qDebug() << index << parent << model->parent(index); // And a view that you can even use to show the model. // QTreeView view; // view.setModel(model); // view.show(); } // Check that we can get back our real parent. QCOMPARE( model->parent ( index ), parent ); // recursively go down the children if ( model->hasChildren ( index ) && currentDepth < 10 ) { //qDebug() << r << c << "has children" << model->rowCount(index); checkChildren ( index, ++currentDepth ); }/* else { if (currentDepth >= 10) qDebug() << "checked 10 deep"; };*/ // make sure that after testing the children that the index doesn't change. QModelIndex newerIndex = model->index ( r, c, parent ); QCOMPARE(index, newerIndex); } } } /*! Tests model's implementation of QAbstractItemModel::data() */ void ModelTest::data() { // Invalid index should return an invalid qvariant QVERIFY( !model->data ( QModelIndex() ).isValid() ); if ( model->rowCount() == 0 ) return; // A valid index should have a valid QVariant data QVERIFY( model->index ( 0, 0 ).isValid() ); // shouldn't be able to set data on an invalid index QVERIFY( !model->setData ( QModelIndex(), QLatin1String ( "foo" ), Qt::DisplayRole ) ); // General Purpose roles that should return a QString QVariant variant = model->data ( model->index ( 0, 0 ), Qt::ToolTipRole ); if ( variant.isValid() ) { QVERIFY( variant.canConvert() ); } variant = model->data ( model->index ( 0, 0 ), Qt::StatusTipRole ); if ( variant.isValid() ) { QVERIFY( variant.canConvert() ); } variant = model->data ( model->index ( 0, 0 ), Qt::WhatsThisRole ); if ( variant.isValid() ) { QVERIFY( variant.canConvert() ); } // General Purpose roles that should return a QSize variant = model->data ( model->index ( 0, 0 ), Qt::SizeHintRole ); if ( variant.isValid() ) { QVERIFY( variant.canConvert() ); } // General Purpose roles that should return a QFont QVariant fontVariant = model->data ( model->index ( 0, 0 ), Qt::FontRole ); if ( fontVariant.isValid() ) { QVERIFY( fontVariant.canConvert() ); } // Check that the alignment is one we know about QVariant textAlignmentVariant = model->data ( model->index ( 0, 0 ), Qt::TextAlignmentRole ); if ( textAlignmentVariant.isValid() ) { Qt::Alignment alignment = textAlignmentVariant.value(); QCOMPARE( alignment, ( alignment & ( Qt::AlignHorizontal_Mask | Qt::AlignVertical_Mask ) ) ); } // General Purpose roles that should return a QColor QVariant colorVariant = model->data ( model->index ( 0, 0 ), Qt::BackgroundColorRole ); if ( colorVariant.isValid() ) { QVERIFY( colorVariant.canConvert() ); } colorVariant = model->data ( model->index ( 0, 0 ), Qt::TextColorRole ); if ( colorVariant.isValid() ) { QVERIFY( colorVariant.canConvert() ); } // Check that the "check state" is one we know about. QVariant checkStateVariant = model->data ( model->index ( 0, 0 ), Qt::CheckStateRole ); if ( checkStateVariant.isValid() ) { int state = checkStateVariant.toInt(); QVERIFY( state == Qt::Unchecked || state == Qt::PartiallyChecked || state == Qt::Checked ); } } /*! Store what is about to be inserted to make sure it actually happens \sa rowsInserted() */ void ModelTest::rowsAboutToBeInserted ( const QModelIndex &parent, int start, int /* end */) { // Q_UNUSED(end); // qDebug() << "rowsAboutToBeInserted" << "start=" << start << "end=" << end << "parent=" << model->data ( parent ).toString() // << "current count of parent=" << model->rowCount ( parent ); // << "display of last=" << model->data( model->index(start-1, 0, parent) ); // qDebug() << model->index(start-1, 0, parent) << model->data( model->index(start-1, 0, parent) ); Changing c; c.parent = parent; c.oldSize = model->rowCount ( parent ); c.last = model->data ( model->index ( start - 1, 0, parent ) ); c.next = model->data ( model->index ( start, 0, parent ) ); insert.push ( c ); } /*! Confirm that what was said was going to happen actually did \sa rowsAboutToBeInserted() */ void ModelTest::rowsInserted ( const QModelIndex & parent, int start, int end ) { Changing c = insert.pop(); QCOMPARE(c.parent, parent); // qDebug() << "rowsInserted" << "start=" << start << "end=" << end << "oldsize=" << c.oldSize // << "parent=" << model->data ( parent ).toString() << "current rowcount of parent=" << model->rowCount ( parent ); // for (int ii=start; ii <= end; ii++) // { // qDebug() << "itemWasInserted:" << ii << model->data ( model->index ( ii, 0, parent )); // } // qDebug(); QCOMPARE(c.oldSize + (end - start + 1), model->rowCount(parent)); QCOMPARE(c.last, model->data(model->index(start - 1, 0, c.parent))); if (c.next != model->data(model->index(end + 1, 0, c.parent))) { qDebug() << start << end; for (int i=0; i < model->rowCount(); ++i) qDebug() << model->index(i, 0).data().toString(); qDebug() << c.next << model->data(model->index(end + 1, 0, c.parent)); } QCOMPARE(c.next, model->data(model->index(end + 1, 0, c.parent))); } void ModelTest::layoutAboutToBeChanged() { for ( int i = 0; i < qBound ( 0, model->rowCount(), 100 ); ++i ) changing.append ( QPersistentModelIndex ( model->index ( i, 0 ) ) ); } void ModelTest::layoutChanged() { for ( int i = 0; i < changing.count(); ++i ) { QPersistentModelIndex p = changing[i]; QCOMPARE(QModelIndex(p), model->index(p.row(), p.column(), p.parent())); } changing.clear(); } /*! Store what is about to be inserted to make sure it actually happens \sa rowsRemoved() */ void ModelTest::rowsAboutToBeRemoved ( const QModelIndex &parent, int start, int end ) { qDebug() << "ratbr" << parent << start << end; Changing c; c.parent = parent; c.oldSize = model->rowCount ( parent ); c.last = model->data ( model->index ( start - 1, 0, parent ) ); c.next = model->data ( model->index ( end + 1, 0, parent ) ); remove.push ( c ); } /*! Confirm that what was said was going to happen actually did \sa rowsAboutToBeRemoved() */ void ModelTest::rowsRemoved ( const QModelIndex & parent, int start, int end ) { qDebug() << "rr" << parent << start << end; Changing c = remove.pop(); QCOMPARE(c.parent, parent); QCOMPARE(c.oldSize - (end - start + 1), model->rowCount(parent)); QCOMPARE(c.last, model->data(model->index(start - 1, 0, c.parent))); QCOMPARE(c.next, model->data(model->index(start, 0, c.parent))); } void ModelTest::dataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight) { QVERIFY(topLeft.isValid()); QVERIFY(bottomRight.isValid()); QModelIndex commonParent = bottomRight.parent(); QCOMPARE(topLeft.parent(), commonParent); QVERIFY(topLeft.row() <= bottomRight.row()); QVERIFY(topLeft.column() <= bottomRight.column()); int rowCount = model->rowCount(commonParent); int columnCount = model->columnCount(commonParent); QVERIFY(bottomRight.row() < rowCount); QVERIFY(bottomRight.column() < columnCount); } void ModelTest::headerDataChanged(Qt::Orientation orientation, int start, int end) { QVERIFY(start >= 0); QVERIFY(end >= 0); QVERIFY(start <= end); int itemCount = orientation == Qt::Vertical ? model->rowCount() : model->columnCount(); QVERIFY(start < itemCount); QVERIFY(end < itemCount); } ostinato-1.3.0/extra/modeltest/modeltest.h000066400000000000000000000050241451413623100206500ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (C) 2016 The Qt Company Ltd. ** Contact: https://www.qt.io/licensing/ ** ** This file is part of the test suite of the Qt Toolkit. ** ** $QT_BEGIN_LICENSE:GPL-EXCEPT$ ** Commercial License Usage ** Licensees holding valid commercial Qt licenses may use this file in ** accordance with the commercial license agreement provided with the ** Software or, alternatively, in accordance with the terms contained in ** a written agreement between you and The Qt Company. For licensing terms ** and conditions see https://www.qt.io/terms-conditions. For further ** information use the contact form at https://www.qt.io/contact-us. ** ** GNU General Public License Usage ** Alternatively, this file may be used under the terms of the GNU ** General Public License version 3 as published by the Free Software ** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT ** included in the packaging of this file. Please review the following ** information to ensure the GNU General Public License requirements will ** be met: https://www.gnu.org/licenses/gpl-3.0.html. ** ** $QT_END_LICENSE$ ** ****************************************************************************/ #ifndef MODELTEST_H #define MODELTEST_H #include #include #include class ModelTest : public QObject { Q_OBJECT public: ModelTest( QAbstractItemModel *model, QObject *parent = 0 ); private Q_SLOTS: void nonDestructiveBasicTest(); void rowCount(); void columnCount(); void hasIndex(); void index(); void parent(); void data(); protected Q_SLOTS: void runAllTests(); void layoutAboutToBeChanged(); void layoutChanged(); void rowsAboutToBeInserted( const QModelIndex &parent, int start, int end ); void rowsInserted( const QModelIndex & parent, int start, int end ); void rowsAboutToBeRemoved( const QModelIndex &parent, int start, int end ); void rowsRemoved( const QModelIndex & parent, int start, int end ); void dataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight); void headerDataChanged(Qt::Orientation orientation, int start, int end); private: void checkChildren( const QModelIndex &parent, int currentDepth = 0 ); QAbstractItemModel *model; struct Changing { QModelIndex parent; int oldSize; QVariant last; QVariant next; }; QStack insert; QStack remove; bool fetchingMore; QList changing; }; #endif ostinato-1.3.0/extra/modeltest/modeltest.pro000066400000000000000000000002741451413623100212230ustar00rootroot00000000000000TEMPLATE = lib CONFIG += testcase static no_testcase_installs QT += widgets testlib SOURCES += modeltest.cpp dynamictreemodel.cpp HEADERS += modeltest.h dynamictreemodel.h ostinato-1.3.0/extra/modeltest/tst_modeltest.cpp000066400000000000000000000223551451413623100221030ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (C) 2016 The Qt Company Ltd. ** Contact: https://www.qt.io/licensing/ ** ** This file is part of the test suite of the Qt Toolkit. ** ** $QT_BEGIN_LICENSE:GPL-EXCEPT$ ** Commercial License Usage ** Licensees holding valid commercial Qt licenses may use this file in ** accordance with the commercial license agreement provided with the ** Software or, alternatively, in accordance with the terms contained in ** a written agreement between you and The Qt Company. For licensing terms ** and conditions see https://www.qt.io/terms-conditions. For further ** information use the contact form at https://www.qt.io/contact-us. ** ** GNU General Public License Usage ** Alternatively, this file may be used under the terms of the GNU ** General Public License version 3 as published by the Free Software ** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT ** included in the packaging of this file. Please review the following ** information to ensure the GNU General Public License requirements will ** be met: https://www.gnu.org/licenses/gpl-3.0.html. ** ** $QT_END_LICENSE$ ** ****************************************************************************/ #include #include #include #include "modeltest.h" #include "dynamictreemodel.h" class tst_ModelTest : public QObject { Q_OBJECT private slots: void stringListModel(); void treeWidgetModel(); void standardItemModel(); void testInsertThroughProxy(); void moveSourceItems(); void testResetThroughProxy(); }; /* tests */ void tst_ModelTest::stringListModel() { QStringListModel model; QSortFilterProxyModel proxy; ModelTest t1(&model); ModelTest t2(&proxy); proxy.setSourceModel(&model); model.setStringList(QStringList() << "2" << "3" << "1"); model.setStringList(QStringList() << "a" << "e" << "plop" << "b" << "c" ); proxy.setDynamicSortFilter(true); proxy.setFilterRegExp(QRegExp("[^b]")); } void tst_ModelTest::treeWidgetModel() { QTreeWidget widget; ModelTest t1(widget.model()); QTreeWidgetItem *root = new QTreeWidgetItem(&widget, QStringList("root")); for (int i = 0; i < 20; ++i) { new QTreeWidgetItem(root, QStringList(QString::number(i))); } QTreeWidgetItem *remove = root->child(2); root->removeChild(remove); QTreeWidgetItem *parent = new QTreeWidgetItem(&widget, QStringList("parent")); new QTreeWidgetItem(parent, QStringList("child")); widget.setItemHidden(parent, true); widget.sortByColumn(0); } void tst_ModelTest::standardItemModel() { QStandardItemModel model(10,10); QSortFilterProxyModel proxy; ModelTest t1(&model); ModelTest t2(&proxy); proxy.setSourceModel(&model); model.insertRows(2, 5); model.removeRows(4, 5); model.insertColumns(2, 5); model.removeColumns(4, 5); model.insertRows(0,5, model.index(1,1)); model.insertColumns(0,5, model.index(1,3)); } void tst_ModelTest::testInsertThroughProxy() { DynamicTreeModel *model = new DynamicTreeModel(this); QSortFilterProxyModel *proxy = new QSortFilterProxyModel(this); proxy->setSourceModel(model); new ModelTest(proxy, this); ModelInsertCommand *insertCommand = new ModelInsertCommand(model, this); insertCommand->setNumCols(4); insertCommand->setStartRow(0); insertCommand->setEndRow(9); // Parent is QModelIndex() insertCommand->doCommand(); insertCommand = new ModelInsertCommand(model, this); insertCommand->setNumCols(4); insertCommand->setAncestorRowNumbers(QList() << 5); insertCommand->setStartRow(0); insertCommand->setEndRow(9); insertCommand->doCommand(); ModelMoveCommand *moveCommand = new ModelMoveCommand(model, this); moveCommand->setNumCols(4); moveCommand->setStartRow(0); moveCommand->setEndRow(0); moveCommand->setDestRow(9); moveCommand->setDestAncestors(QList() << 5); moveCommand->doCommand(); } /** Makes the persistent index list publicly accessible */ class AccessibleProxyModel : public QSortFilterProxyModel { Q_OBJECT public: AccessibleProxyModel(QObject *parent = 0) : QSortFilterProxyModel(parent) {} QModelIndexList persistent() { return persistentIndexList(); } }; class ObservingObject : public QObject { Q_OBJECT public: ObservingObject(AccessibleProxyModel *proxy, QObject *parent = 0) : QObject(parent) , m_proxy(proxy) , storePersistentFailureCount(0) , checkPersistentFailureCount(0) { connect(m_proxy, SIGNAL(rowsAboutToBeMoved(QModelIndex,int,int,QModelIndex,int)), SLOT(storePersistent())); connect(m_proxy, SIGNAL(rowsMoved(QModelIndex,int,int,QModelIndex,int)), SLOT(checkPersistent())); } public slots: void storePersistent(const QModelIndex &parent) { for (int row = 0; row < m_proxy->rowCount(parent); ++row) { QModelIndex proxyIndex = m_proxy->index(row, 0, parent); QModelIndex sourceIndex = m_proxy->mapToSource(proxyIndex); if (!proxyIndex.isValid()) { qWarning("%s: Invalid proxy index", Q_FUNC_INFO); ++storePersistentFailureCount; } if (!sourceIndex.isValid()) { qWarning("%s: invalid source index", Q_FUNC_INFO); ++storePersistentFailureCount; } m_persistentSourceIndexes.append(sourceIndex); m_persistentProxyIndexes.append(proxyIndex); if (m_proxy->hasChildren(proxyIndex)) storePersistent(proxyIndex); } } void storePersistent() { // This method is called from rowsAboutToBeMoved. Persistent indexes should be valid foreach(const QModelIndex &idx, m_persistentProxyIndexes) if (!idx.isValid()) { qWarning("%s: persistentProxyIndexes contains invalid index", Q_FUNC_INFO); ++storePersistentFailureCount; } if (!m_proxy->persistent().isEmpty()) { qWarning("%s: proxy should have no persistent indexes when storePersistent called", Q_FUNC_INFO); ++storePersistentFailureCount; } storePersistent(QModelIndex()); if (m_proxy->persistent().isEmpty()) { qWarning("%s: proxy should have persistent index after storePersistent called", Q_FUNC_INFO); ++storePersistentFailureCount; } } void checkPersistent() { for (int row = 0; row < m_persistentProxyIndexes.size(); ++row) { m_persistentProxyIndexes.at(row); m_persistentSourceIndexes.at(row); } for (int row = 0; row < m_persistentProxyIndexes.size(); ++row) { QModelIndex updatedProxy = m_persistentProxyIndexes.at(row); QModelIndex updatedSource = m_persistentSourceIndexes.at(row); if (m_proxy->mapToSource(updatedProxy) != updatedSource) { qWarning("%s: check failed at row %d", Q_FUNC_INFO, row); ++checkPersistentFailureCount; } } m_persistentSourceIndexes.clear(); m_persistentProxyIndexes.clear(); } private: AccessibleProxyModel *m_proxy; QList m_persistentSourceIndexes; QList m_persistentProxyIndexes; public: int storePersistentFailureCount; int checkPersistentFailureCount; }; void tst_ModelTest::moveSourceItems() { DynamicTreeModel *model = new DynamicTreeModel(this); AccessibleProxyModel *proxy = new AccessibleProxyModel(this); proxy->setSourceModel(model); ModelInsertCommand *insertCommand = new ModelInsertCommand(model, this); insertCommand->setStartRow(0); insertCommand->setEndRow(2); insertCommand->doCommand(); insertCommand = new ModelInsertCommand(model, this); insertCommand->setAncestorRowNumbers(QList() << 1); insertCommand->setStartRow(0); insertCommand->setEndRow(2); insertCommand->doCommand(); ObservingObject observer(proxy); ModelMoveCommand *moveCommand = new ModelMoveCommand(model, this); moveCommand->setStartRow(0); moveCommand->setEndRow(0); moveCommand->setDestAncestors(QList() << 1); moveCommand->setDestRow(0); moveCommand->doCommand(); QCOMPARE(observer.storePersistentFailureCount, 0); QCOMPARE(observer.checkPersistentFailureCount, 0); } void tst_ModelTest::testResetThroughProxy() { DynamicTreeModel *model = new DynamicTreeModel(this); ModelInsertCommand *insertCommand = new ModelInsertCommand(model, this); insertCommand->setStartRow(0); insertCommand->setEndRow(2); insertCommand->doCommand(); QPersistentModelIndex persistent = model->index(0, 0); AccessibleProxyModel *proxy = new AccessibleProxyModel(this); proxy->setSourceModel(model); ObservingObject observer(proxy); observer.storePersistent(); ModelResetCommand *resetCommand = new ModelResetCommand(model, this); resetCommand->setNumCols(0); resetCommand->doCommand(); QCOMPARE(observer.storePersistentFailureCount, 0); QCOMPARE(observer.checkPersistentFailureCount, 0); } QTEST_MAIN(tst_ModelTest) #include "tst_modeltest.moc" ostinato-1.3.0/extra/qhexedit2/000077500000000000000000000000001451413623100163735ustar00rootroot00000000000000ostinato-1.3.0/extra/qhexedit2/qhexedit2.pro000066400000000000000000000004771451413623100210220ustar00rootroot00000000000000TEMPLATE = lib CONFIG += qt staticlib warn_on QT += core gui greaterThan(QT_MAJOR_VERSION, 4): QT += widgets VERSION = 4.0.0 DEFINES += QHEXEDIT_EXPORTS HEADERS = src/chunks.h\ src/commands.h \ src/qhexedit.h \ SOURCES = src/chunks.cpp \ src/commands.cpp \ src/qhexedit.cpp ostinato-1.3.0/extra/qhexedit2/src/000077500000000000000000000000001451413623100171625ustar00rootroot00000000000000ostinato-1.3.0/extra/qhexedit2/src/VERSION.txt000066400000000000000000000000321451413623100210430ustar00rootroot00000000000000Release 0.8.4, 2017-01-16 ostinato-1.3.0/extra/qhexedit2/src/chunks.cpp000066400000000000000000000204441451413623100211650ustar00rootroot00000000000000#include "chunks.h" #include #define NORMAL 0 #define HIGHLIGHTED 1 #define BUFFER_SIZE 0x10000 #define CHUNK_SIZE 0x1000 #define READ_CHUNK_MASK Q_INT64_C(0xfffffffffffff000) // ***************************************** Constructors and file settings Chunks::Chunks(QObject *parent): QObject(parent) { QBuffer *buf = new QBuffer(this); setIODevice(*buf); } Chunks::Chunks(QIODevice &ioDevice, QObject *parent): QObject(parent) { setIODevice(ioDevice); } bool Chunks::setIODevice(QIODevice &ioDevice) { _ioDevice = &ioDevice; bool ok = _ioDevice->open(QIODevice::ReadOnly); if (ok) // Try to open IODevice { _size = _ioDevice->size(); _ioDevice->close(); } else // Fallback is an empty buffer { QBuffer *buf = new QBuffer(this); _ioDevice = buf; _size = 0; } _chunks.clear(); _pos = 0; return ok; } // ***************************************** Getting data out of Chunks QByteArray Chunks::data(qint64 pos, qint64 maxSize, QByteArray *highlighted) { qint64 ioDelta = 0; int chunkIdx = 0; Chunk chunk; QByteArray buffer; // Do some checks and some arrangements if (highlighted) highlighted->clear(); if (pos >= _size) return buffer; if (maxSize < 0) maxSize = _size; else if ((pos + maxSize) > _size) maxSize = _size - pos; _ioDevice->open(QIODevice::ReadOnly); while (maxSize > 0) { chunk.absPos = LLONG_MAX; bool chunksLoopOngoing = true; while ((chunkIdx < _chunks.count()) && chunksLoopOngoing) { // In this section, we track changes before our required data and // we take the editdet data, if availible. ioDelta is a difference // counter to justify the read pointer to the original data, if // data in between was deleted or inserted. chunk = _chunks[chunkIdx]; if (chunk.absPos > pos) chunksLoopOngoing = false; else { chunkIdx += 1; qint64 count; qint64 chunkOfs = pos - chunk.absPos; if (maxSize > ((qint64)chunk.data.size() - chunkOfs)) { count = (qint64)chunk.data.size() - chunkOfs; ioDelta += CHUNK_SIZE - chunk.data.size(); } else count = maxSize; if (count > 0) { buffer += chunk.data.mid(chunkOfs, (int)count); maxSize -= count; pos += count; if (highlighted) *highlighted += chunk.dataChanged.mid(chunkOfs, (int)count); } } } if ((maxSize > 0) && (pos < chunk.absPos)) { // In this section, we read data from the original source. This only will // happen, whe no copied data is available qint64 byteCount; QByteArray readBuffer; if ((chunk.absPos - pos) > maxSize) byteCount = maxSize; else byteCount = chunk.absPos - pos; maxSize -= byteCount; _ioDevice->seek(pos + ioDelta); readBuffer = _ioDevice->read(byteCount); buffer += readBuffer; if (highlighted) *highlighted += QByteArray(readBuffer.size(), NORMAL); pos += readBuffer.size(); } } _ioDevice->close(); return buffer; } bool Chunks::write(QIODevice &iODevice, qint64 pos, qint64 count) { if (count == -1) count = _size; bool ok = iODevice.open(QIODevice::WriteOnly); if (ok) { for (qint64 idx=pos; idx < count; idx += BUFFER_SIZE) { QByteArray ba = data(idx, BUFFER_SIZE); iODevice.write(ba); } iODevice.close(); } return ok; } // ***************************************** Set and get highlighting infos void Chunks::setDataChanged(qint64 pos, bool dataChanged) { if ((pos < 0) || (pos >= _size)) return; int chunkIdx = getChunkIndex(pos); qint64 posInBa = pos - _chunks[chunkIdx].absPos; _chunks[chunkIdx].dataChanged[(int)posInBa] = char(dataChanged); } bool Chunks::dataChanged(qint64 pos) { QByteArray highlighted; data(pos, 1, &highlighted); return bool(highlighted.at(0)); } // ***************************************** Search API qint64 Chunks::indexOf(const QByteArray &ba, qint64 from) { qint64 result = -1; QByteArray buffer; for (qint64 pos=from; (pos < _size) && (result < 0); pos += BUFFER_SIZE) { buffer = data(pos, BUFFER_SIZE + ba.size() - 1); int findPos = buffer.indexOf(ba); if (findPos >= 0) result = pos + (qint64)findPos; } return result; } qint64 Chunks::lastIndexOf(const QByteArray &ba, qint64 from) { qint64 result = -1; QByteArray buffer; for (qint64 pos=from; (pos > 0) && (result < 0); pos -= BUFFER_SIZE) { qint64 sPos = pos - BUFFER_SIZE - (qint64)ba.size() + 1; if (sPos < 0) sPos = 0; buffer = data(sPos, pos - sPos); int findPos = buffer.lastIndexOf(ba); if (findPos >= 0) result = sPos + (qint64)findPos; } return result; } // ***************************************** Char manipulations bool Chunks::insert(qint64 pos, char b) { if ((pos < 0) || (pos > _size)) return false; int chunkIdx; if (pos == _size) chunkIdx = getChunkIndex(pos-1); else chunkIdx = getChunkIndex(pos); qint64 posInBa = pos - _chunks[chunkIdx].absPos; _chunks[chunkIdx].data.insert(posInBa, b); _chunks[chunkIdx].dataChanged.insert(posInBa, char(1)); for (int idx=chunkIdx+1; idx < _chunks.size(); idx++) _chunks[idx].absPos += 1; _size += 1; _pos = pos; return true; } bool Chunks::overwrite(qint64 pos, char b) { if ((pos < 0) || (pos >= _size)) return false; int chunkIdx = getChunkIndex(pos); qint64 posInBa = pos - _chunks[chunkIdx].absPos; _chunks[chunkIdx].data[(int)posInBa] = b; _chunks[chunkIdx].dataChanged[(int)posInBa] = char(1); _pos = pos; return true; } bool Chunks::removeAt(qint64 pos) { if ((pos < 0) || (pos >= _size)) return false; int chunkIdx = getChunkIndex(pos); qint64 posInBa = pos - _chunks[chunkIdx].absPos; _chunks[chunkIdx].data.remove(posInBa, 1); _chunks[chunkIdx].dataChanged.remove(posInBa, 1); for (int idx=chunkIdx+1; idx < _chunks.size(); idx++) _chunks[idx].absPos -= 1; _size -= 1; _pos = pos; return true; } // ***************************************** Utility functions char Chunks::operator[](qint64 pos) { return data(pos, 1)[0]; } qint64 Chunks::pos() { return _pos; } qint64 Chunks::size() { return _size; } int Chunks::getChunkIndex(qint64 absPos) { // This routine checks, if there is already a copied chunk available. If os, it // returns a reference to it. If there is no copied chunk available, original // data will be copied into a new chunk. int foundIdx = -1; int insertIdx = 0; qint64 ioDelta = 0; for (int idx=0; idx < _chunks.size(); idx++) { Chunk chunk = _chunks[idx]; if ((absPos >= chunk.absPos) && (absPos < (chunk.absPos + chunk.data.size()))) { foundIdx = idx; break; } if (absPos < chunk.absPos) { insertIdx = idx; break; } ioDelta += chunk.data.size() - CHUNK_SIZE; insertIdx = idx + 1; } if (foundIdx == -1) { Chunk newChunk; qint64 readAbsPos = absPos - ioDelta; qint64 readPos = (readAbsPos & READ_CHUNK_MASK); _ioDevice->open(QIODevice::ReadOnly); _ioDevice->seek(readPos); newChunk.data = _ioDevice->read(CHUNK_SIZE); _ioDevice->close(); newChunk.absPos = absPos - (readAbsPos - readPos); newChunk.dataChanged = QByteArray(newChunk.data.size(), char(0)); _chunks.insert(insertIdx, newChunk); foundIdx = insertIdx; } return foundIdx; } #ifdef MODUL_TEST int Chunks::chunkSize() { return _chunks.size(); } #endif ostinato-1.3.0/extra/qhexedit2/src/chunks.h000066400000000000000000000037451451413623100206370ustar00rootroot00000000000000#ifndef CHUNKS_H #define CHUNKS_H /** \cond docNever */ /*! The Chunks class is the storage backend for QHexEdit. * * When QHexEdit loads data, Chunks access them using a QIODevice interface. When the app uses * a QByteArray interface, QBuffer is used to provide again a QIODevice like interface. No data * will be changed, therefore Chunks opens the QIODevice in QIODevice::ReadOnly mode. After every * access Chunks closes the QIODevice, that's why external applications can overwrite files while * QHexEdit shows them. * * When the the user starts to edit the data, Chunks creates a local copy of a chunk of data (4 * kilobytes) and notes all changes there. Parallel to that chunk, there is a second chunk, * which keep track of which bytes are changed and which not. * */ #include struct Chunk { QByteArray data; QByteArray dataChanged; qint64 absPos; }; class Chunks: public QObject { Q_OBJECT public: // Constructors and file settings Chunks(QObject *parent); Chunks(QIODevice &ioDevice, QObject *parent); bool setIODevice(QIODevice &ioDevice); // Getting data out of Chunks QByteArray data(qint64 pos=0, qint64 count=-1, QByteArray *highlighted=0); bool write(QIODevice &iODevice, qint64 pos=0, qint64 count=-1); // Set and get highlighting infos void setDataChanged(qint64 pos, bool dataChanged); bool dataChanged(qint64 pos); // Search API qint64 indexOf(const QByteArray &ba, qint64 from); qint64 lastIndexOf(const QByteArray &ba, qint64 from); // Char manipulations bool insert(qint64 pos, char b); bool overwrite(qint64 pos, char b); bool removeAt(qint64 pos); // Utility functions char operator[](qint64 pos); qint64 pos(); qint64 size(); private: int getChunkIndex(qint64 absPos); QIODevice * _ioDevice; qint64 _pos; qint64 _size; QList _chunks; #ifdef MODUL_TEST public: int chunkSize(); #endif }; /** \endcond docNever */ #endif // CHUNKS_H ostinato-1.3.0/extra/qhexedit2/src/commands.cpp000066400000000000000000000100411451413623100214630ustar00rootroot00000000000000#include "commands.h" #include // Helper class to store single byte commands class CharCommand : public QUndoCommand { public: enum CCmd {insert, removeAt, overwrite}; CharCommand(Chunks * chunks, CCmd cmd, qint64 charPos, char newChar, QUndoCommand *parent=0); void undo(); void redo(); bool mergeWith(const QUndoCommand *command); int id() const { return 1234; } private: Chunks * _chunks; qint64 _charPos; bool _wasChanged; char _newChar; char _oldChar; CCmd _cmd; }; CharCommand::CharCommand(Chunks * chunks, CCmd cmd, qint64 charPos, char newChar, QUndoCommand *parent) : QUndoCommand(parent) { _chunks = chunks; _charPos = charPos; _newChar = newChar; _cmd = cmd; } bool CharCommand::mergeWith(const QUndoCommand *command) { const CharCommand *nextCommand = static_cast(command); bool result = false; if (_cmd != CharCommand::removeAt) { if (nextCommand->_cmd == overwrite) if (nextCommand->_charPos == _charPos) { _newChar = nextCommand->_newChar; result = true; } } return result; } void CharCommand::undo() { switch (_cmd) { case insert: _chunks->removeAt(_charPos); break; case overwrite: _chunks->overwrite(_charPos, _oldChar); _chunks->setDataChanged(_charPos, _wasChanged); break; case removeAt: _chunks->insert(_charPos, _oldChar); _chunks->setDataChanged(_charPos, _wasChanged); break; } } void CharCommand::redo() { switch (_cmd) { case insert: _chunks->insert(_charPos, _newChar); break; case overwrite: _oldChar = (*_chunks)[_charPos]; _wasChanged = _chunks->dataChanged(_charPos); _chunks->overwrite(_charPos, _newChar); break; case removeAt: _oldChar = (*_chunks)[_charPos]; _wasChanged = _chunks->dataChanged(_charPos); _chunks->removeAt(_charPos); break; } } UndoStack::UndoStack(Chunks * chunks, QObject * parent) : QUndoStack(parent) { _chunks = chunks; _parent = parent; } void UndoStack::insert(qint64 pos, char c) { if ((pos >= 0) && (pos <= _chunks->size())) { QUndoCommand *cc = new CharCommand(_chunks, CharCommand::insert, pos, c); this->push(cc); } } void UndoStack::insert(qint64 pos, const QByteArray &ba) { if ((pos >= 0) && (pos <= _chunks->size())) { QString txt = QString(tr("Inserting %1 bytes")).arg(ba.size()); beginMacro(txt); for (int idx=0; idx < ba.size(); idx++) { QUndoCommand *cc = new CharCommand(_chunks, CharCommand::insert, pos + idx, ba.at(idx)); this->push(cc); } endMacro(); } } void UndoStack::removeAt(qint64 pos, qint64 len) { if ((pos >= 0) && (pos < _chunks->size())) { if (len==1) { QUndoCommand *cc = new CharCommand(_chunks, CharCommand::removeAt, pos, char(0)); this->push(cc); } else { QString txt = QString(tr("Delete %1 chars")).arg(len); beginMacro(txt); for (qint64 cnt=0; cnt= 0) && (pos < _chunks->size())) { QUndoCommand *cc = new CharCommand(_chunks, CharCommand::overwrite, pos, c); this->push(cc); } } void UndoStack::overwrite(qint64 pos, int len, const QByteArray &ba) { if ((pos >= 0) && (pos < _chunks->size())) { QString txt = QString(tr("Overwrite %1 chars")).arg(len); beginMacro(txt); removeAt(pos, len); insert(pos, ba); endMacro(); } } ostinato-1.3.0/extra/qhexedit2/src/commands.h000066400000000000000000000026431451413623100211410ustar00rootroot00000000000000#ifndef COMMANDS_H #define COMMANDS_H /** \cond docNever */ #include #include "chunks.h" /*! CharCommand is a class to provid undo/redo functionality in QHexEdit. A QUndoCommand represents a single editing action on a document. CharCommand is responsable for manipulations on single chars. It can insert. overwrite and remove characters. A manipulation stores allways two actions 1. redo (or do) action 2. undo action. CharCommand also supports command compression via mergeWidht(). This allows the user to execute a undo command contation e.g. 3 steps in a single command. If you for example insert a new byt "34" this means for the editor doing 3 steps: insert a "00", overwrite it with "03" and the overwrite it with "34". These 3 steps are combined into a single step, insert a "34". The byte array oriented commands are just put into a set of single byte commands, which are pooled together with the macroBegin() and macroEnd() functionality of Qt's QUndoStack. */ class UndoStack : public QUndoStack { Q_OBJECT public: UndoStack(Chunks *chunks, QObject * parent=0); void insert(qint64 pos, char c); void insert(qint64 pos, const QByteArray &ba); void removeAt(qint64 pos, qint64 len=1); void overwrite(qint64 pos, char c); void overwrite(qint64 pos, int len, const QByteArray &ba); private: Chunks * _chunks; QObject * _parent; }; /** \endcond docNever */ #endif // COMMANDS_H ostinato-1.3.0/extra/qhexedit2/src/license.txt000066400000000000000000000636411451413623100213570ustar00rootroot00000000000000 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!ostinato-1.3.0/extra/qhexedit2/src/qhexedit.cpp000066400000000000000000001000701451413623100214770ustar00rootroot00000000000000#include #include #include #include #include #include "qhexedit.h" #include // ********************************************************************** Constructor, destructor QHexEdit::QHexEdit(QWidget *parent) : QAbstractScrollArea(parent) { _addressArea = true; _addressWidth = 4; _asciiArea = true; _overwriteMode = true; _highlighting = true; _readOnly = false; _cursorPosition = 0; _lastEventSize = 0; _hexCharsInLine = 47; _bytesPerLine = 16; _editAreaIsAscii = false; _hexCaps = false; _dynamicBytesPerLine = false; _chunks = new Chunks(this); _undoStack = new UndoStack(_chunks, this); #ifdef Q_OS_WIN32 setFont(QFont("Courier", 10)); #else setFont(QFont("Monospace", 10)); #endif setAddressAreaColor(this->palette().alternateBase().color()); setHighlightingColor(QColor(0xff, 0xff, 0x99, 0xff)); setSelectionColor(this->palette().highlight().color()); connect(&_cursorTimer, SIGNAL(timeout()), this, SLOT(updateCursor())); connect(verticalScrollBar(), SIGNAL(valueChanged(int)), this, SLOT(adjust())); connect(horizontalScrollBar(), SIGNAL(valueChanged(int)), this, SLOT(adjust())); connect(_undoStack, SIGNAL(indexChanged(int)), this, SLOT(dataChangedPrivate(int))); _cursorTimer.setInterval(500); _cursorTimer.start(); setAddressWidth(4); setAddressArea(true); setAsciiArea(true); setOverwriteMode(true); setHighlighting(true); setReadOnly(false); init(); } QHexEdit::~QHexEdit() { } // ********************************************************************** Properties void QHexEdit::setAddressArea(bool addressArea) { _addressArea = addressArea; adjust(); setCursorPosition(_cursorPosition); viewport()->update(); } bool QHexEdit::addressArea() { return _addressArea; } void QHexEdit::setAddressAreaColor(const QColor &color) { _addressAreaColor = color; viewport()->update(); } QColor QHexEdit::addressAreaColor() { return _addressAreaColor; } void QHexEdit::setAddressOffset(qint64 addressOffset) { _addressOffset = addressOffset; adjust(); setCursorPosition(_cursorPosition); viewport()->update(); } qint64 QHexEdit::addressOffset() { return _addressOffset; } void QHexEdit::setAddressWidth(int addressWidth) { _addressWidth = addressWidth; adjust(); setCursorPosition(_cursorPosition); viewport()->update(); } int QHexEdit::addressWidth() { qint64 size = _chunks->size(); int n = 1; if (size > Q_INT64_C(0x100000000)){ n += 8; size /= Q_INT64_C(0x100000000);} if (size > 0x10000){ n += 4; size /= 0x10000;} if (size > 0x100){ n += 2; size /= 0x100;} if (size > 0x10){ n += 1; size /= 0x10;} if (n > _addressWidth) return n; else return _addressWidth; } void QHexEdit::setAsciiArea(bool asciiArea) { if (!asciiArea) _editAreaIsAscii = false; _asciiArea = asciiArea; adjust(); setCursorPosition(_cursorPosition); viewport()->update(); } bool QHexEdit::asciiArea() { return _asciiArea; } void QHexEdit::setBytesPerLine(int count) { _bytesPerLine = count; _hexCharsInLine = count * 3 - 1; adjust(); setCursorPosition(_cursorPosition); viewport()->update(); } int QHexEdit::bytesPerLine() { return _bytesPerLine; } void QHexEdit::setCursorPosition(qint64 position) { // 1. delete old cursor _blink = false; viewport()->update(_cursorRect); // 2. Check, if cursor in range? if (position > (_chunks->size() * 2 - 1)) position = _chunks->size() * 2 - (_overwriteMode ? 1 : 0); if (position < 0) position = 0; // 3. Calc new position of cursor _bPosCurrent = position / 2; _pxCursorY = ((position / 2 - _bPosFirst) / _bytesPerLine + 1) * _pxCharHeight; int x = (position % (2 * _bytesPerLine)); if (_editAreaIsAscii) { _pxCursorX = x / 2 * _pxCharWidth + _pxPosAsciiX; _cursorPosition = position & 0xFFFFFFFFFFFFFFFELL; } else { _pxCursorX = (((x / 2) * 3) + (x % 2)) * _pxCharWidth + _pxPosHexX; _cursorPosition = position; } if (_overwriteMode) _cursorRect = QRect(_pxCursorX - horizontalScrollBar()->value(), _pxCursorY + _pxCursorWidth, _pxCharWidth, _pxCursorWidth); else _cursorRect = QRect(_pxCursorX - horizontalScrollBar()->value(), _pxCursorY - _pxCharHeight + 4, _pxCursorWidth, _pxCharHeight); // 4. Immediately draw new cursor _blink = true; viewport()->update(_cursorRect); emit currentAddressChanged(_bPosCurrent); } qint64 QHexEdit::cursorPosition(QPoint pos) { // Calc cursor position depending on a graphical position qint64 result = -1; int posX = pos.x() + horizontalScrollBar()->value(); int posY = pos.y() - 3; if ((posX >= _pxPosHexX) && (posX < (_pxPosHexX + (1 + _hexCharsInLine) * _pxCharWidth))) { _editAreaIsAscii = false; int x = (posX - _pxPosHexX) / _pxCharWidth; x = (x / 3) * 2 + x % 3; int y = (posY / _pxCharHeight) * 2 * _bytesPerLine; result = _bPosFirst * 2 + x + y; } else if (_asciiArea && (posX >= _pxPosAsciiX) && (posX < (_pxPosAsciiX + (1 + _bytesPerLine) * _pxCharWidth))) { _editAreaIsAscii = true; int x = 2 * (posX - _pxPosAsciiX) / _pxCharWidth; int y = (posY / _pxCharHeight) * 2 * _bytesPerLine; result = _bPosFirst * 2 + x + y; } return result; } qint64 QHexEdit::cursorPosition() { return _cursorPosition; } void QHexEdit::setData(const QByteArray &ba) { _data = ba; _bData.setData(_data); setData(_bData); } QByteArray QHexEdit::data() { return _chunks->data(0, -1); } void QHexEdit::setHighlighting(bool highlighting) { _highlighting = highlighting; viewport()->update(); } bool QHexEdit::highlighting() { return _highlighting; } void QHexEdit::setHighlightingColor(const QColor &color) { _brushHighlighted = QBrush(color); _penHighlighted = QPen(viewport()->palette().color(QPalette::WindowText)); viewport()->update(); } QColor QHexEdit::highlightingColor() { return _brushHighlighted.color(); } void QHexEdit::setOverwriteMode(bool overwriteMode) { _overwriteMode = overwriteMode; emit overwriteModeChanged(overwriteMode); } bool QHexEdit::overwriteMode() { return _overwriteMode; } void QHexEdit::setSelectionColor(const QColor &color) { _brushSelection = QBrush(color); _penSelection = QPen(Qt::white); viewport()->update(); } QColor QHexEdit::selectionColor() { return _brushSelection.color(); } bool QHexEdit::isReadOnly() { return _readOnly; } void QHexEdit::setReadOnly(bool readOnly) { _readOnly = readOnly; } void QHexEdit::setHexCaps(const bool isCaps) { if (_hexCaps != isCaps) { _hexCaps = isCaps; viewport()->update(); } } bool QHexEdit::hexCaps() { return _hexCaps; } void QHexEdit::setDynamicBytesPerLine(const bool isDynamic) { _dynamicBytesPerLine = isDynamic; resizeEvent(NULL); } bool QHexEdit::dynamicBytesPerLine() { return _dynamicBytesPerLine; } // ********************************************************************** Access to data of qhexedit bool QHexEdit::setData(QIODevice &iODevice) { bool ok = _chunks->setIODevice(iODevice); init(); dataChangedPrivate(); return ok; } QByteArray QHexEdit::dataAt(qint64 pos, qint64 count) { return _chunks->data(pos, count); } bool QHexEdit::write(QIODevice &iODevice, qint64 pos, qint64 count) { return _chunks->write(iODevice, pos, count); } // ********************************************************************** Char handling void QHexEdit::insert(qint64 index, char ch) { _undoStack->insert(index, ch); refresh(); } void QHexEdit::remove(qint64 index, qint64 len) { _undoStack->removeAt(index, len); refresh(); } void QHexEdit::replace(qint64 index, char ch) { _undoStack->overwrite(index, ch); refresh(); } // ********************************************************************** ByteArray handling void QHexEdit::insert(qint64 pos, const QByteArray &ba) { _undoStack->insert(pos, ba); refresh(); } void QHexEdit::replace(qint64 pos, qint64 len, const QByteArray &ba) { _undoStack->overwrite(pos, len, ba); refresh(); } // ********************************************************************** Utility functions void QHexEdit::ensureVisible() { if (_cursorPosition < (_bPosFirst * 2)) verticalScrollBar()->setValue((int)(_cursorPosition / 2 / _bytesPerLine)); if (_cursorPosition > ((_bPosFirst + (_rowsShown - 1)*_bytesPerLine) * 2)) verticalScrollBar()->setValue((int)(_cursorPosition / 2 / _bytesPerLine) - _rowsShown + 1); if (_pxCursorX < horizontalScrollBar()->value()) horizontalScrollBar()->setValue(_pxCursorX); if ((_pxCursorX + _pxCharWidth) > (horizontalScrollBar()->value() + viewport()->width())) horizontalScrollBar()->setValue(_pxCursorX + _pxCharWidth - viewport()->width()); viewport()->update(); } qint64 QHexEdit::indexOf(const QByteArray &ba, qint64 from) { qint64 pos = _chunks->indexOf(ba, from); if (pos > -1) { qint64 curPos = pos*2; setCursorPosition(curPos + ba.length()*2); resetSelection(curPos); setSelection(curPos + ba.length()*2); ensureVisible(); } return pos; } bool QHexEdit::isModified() { return _modified; } qint64 QHexEdit::lastIndexOf(const QByteArray &ba, qint64 from) { qint64 pos = _chunks->lastIndexOf(ba, from); if (pos > -1) { qint64 curPos = pos*2; setCursorPosition(curPos - 1); resetSelection(curPos); setSelection(curPos + ba.length()*2); ensureVisible(); } return pos; } void QHexEdit::redo() { _undoStack->redo(); setCursorPosition(_chunks->pos()*(_editAreaIsAscii ? 1 : 2)); refresh(); } QString QHexEdit::selectionToReadableString() { QByteArray ba = _chunks->data(getSelectionBegin(), getSelectionEnd() - getSelectionBegin()); return toReadable(ba); } void QHexEdit::setFont(const QFont &font) { QWidget::setFont(font); _pxCharWidth = fontMetrics().width(QLatin1Char('2')); _pxCharHeight = fontMetrics().height(); _pxGapAdr = _pxCharWidth / 2; _pxGapAdrHex = _pxCharWidth; _pxGapHexAscii = 2 * _pxCharWidth; _pxCursorWidth = _pxCharHeight / 7; _pxSelectionSub = _pxCharHeight / 5; viewport()->update(); } QString QHexEdit::toReadableString() { QByteArray ba = _chunks->data(); return toReadable(ba); } void QHexEdit::undo() { _undoStack->undo(); setCursorPosition(_chunks->pos()*(_editAreaIsAscii ? 1 : 2)); refresh(); } // ********************************************************************** Handle events void QHexEdit::keyPressEvent(QKeyEvent *event) { // Cursor movements if (event->matches(QKeySequence::MoveToNextChar)) { qint64 pos = _cursorPosition + 1; if (_editAreaIsAscii) pos += 1; setCursorPosition(pos); resetSelection(pos); } if (event->matches(QKeySequence::MoveToPreviousChar)) { qint64 pos = _cursorPosition - 1; if (_editAreaIsAscii) pos -= 1; setCursorPosition(pos); resetSelection(pos); } if (event->matches(QKeySequence::MoveToEndOfLine)) { qint64 pos = _cursorPosition - (_cursorPosition % (2 * _bytesPerLine)) + (2 * _bytesPerLine) - 1; setCursorPosition(pos); resetSelection(_cursorPosition); } if (event->matches(QKeySequence::MoveToStartOfLine)) { qint64 pos = _cursorPosition - (_cursorPosition % (2 * _bytesPerLine)); setCursorPosition(pos); resetSelection(_cursorPosition); } if (event->matches(QKeySequence::MoveToPreviousLine)) { setCursorPosition(_cursorPosition - (2 * _bytesPerLine)); resetSelection(_cursorPosition); } if (event->matches(QKeySequence::MoveToNextLine)) { setCursorPosition(_cursorPosition + (2 * _bytesPerLine)); resetSelection(_cursorPosition); } if (event->matches(QKeySequence::MoveToNextPage)) { setCursorPosition(_cursorPosition + (((_rowsShown - 1) * 2 * _bytesPerLine))); resetSelection(_cursorPosition); } if (event->matches(QKeySequence::MoveToPreviousPage)) { setCursorPosition(_cursorPosition - (((_rowsShown - 1) * 2 * _bytesPerLine))); resetSelection(_cursorPosition); } if (event->matches(QKeySequence::MoveToEndOfDocument)) { setCursorPosition(_chunks->size() * 2 ); resetSelection(_cursorPosition); } if (event->matches(QKeySequence::MoveToStartOfDocument)) { setCursorPosition(0); resetSelection(_cursorPosition); } // Select commands if (event->matches(QKeySequence::SelectAll)) { resetSelection(0); setSelection(2 * _chunks->size() + 1); } if (event->matches(QKeySequence::SelectNextChar)) { qint64 pos = _cursorPosition + 1; if (_editAreaIsAscii) pos += 1; setCursorPosition(pos); setSelection(pos); } if (event->matches(QKeySequence::SelectPreviousChar)) { qint64 pos = _cursorPosition - 1; if (_editAreaIsAscii) pos -= 1; setSelection(pos); setCursorPosition(pos); } if (event->matches(QKeySequence::SelectEndOfLine)) { qint64 pos = _cursorPosition - (_cursorPosition % (2 * _bytesPerLine)) + (2 * _bytesPerLine) - 1; setCursorPosition(pos); setSelection(pos); } if (event->matches(QKeySequence::SelectStartOfLine)) { qint64 pos = _cursorPosition - (_cursorPosition % (2 * _bytesPerLine)); setCursorPosition(pos); setSelection(pos); } if (event->matches(QKeySequence::SelectPreviousLine)) { qint64 pos = _cursorPosition - (2 * _bytesPerLine); setCursorPosition(pos); setSelection(pos); } if (event->matches(QKeySequence::SelectNextLine)) { qint64 pos = _cursorPosition + (2 * _bytesPerLine); setCursorPosition(pos); setSelection(pos); } if (event->matches(QKeySequence::SelectNextPage)) { qint64 pos = _cursorPosition + (((viewport()->height() / _pxCharHeight) - 1) * 2 * _bytesPerLine); setCursorPosition(pos); setSelection(pos); } if (event->matches(QKeySequence::SelectPreviousPage)) { qint64 pos = _cursorPosition - (((viewport()->height() / _pxCharHeight) - 1) * 2 * _bytesPerLine); setCursorPosition(pos); setSelection(pos); } if (event->matches(QKeySequence::SelectEndOfDocument)) { qint64 pos = _chunks->size() * 2; setCursorPosition(pos); setSelection(pos); } if (event->matches(QKeySequence::SelectStartOfDocument)) { qint64 pos = 0; setCursorPosition(pos); setSelection(pos); } // Edit Commands if (!_readOnly) { /* Cut */ if (event->matches(QKeySequence::Cut)) { QByteArray ba = _chunks->data(getSelectionBegin(), getSelectionEnd() - getSelectionBegin()).toHex(); for (qint64 idx = 32; idx < ba.size(); idx +=33) ba.insert(idx, "\n"); QClipboard *clipboard = QApplication::clipboard(); clipboard->setText(ba); if (_overwriteMode) { qint64 len = getSelectionEnd() - getSelectionBegin(); replace(getSelectionBegin(), (int)len, QByteArray((int)len, char(0))); } else { remove(getSelectionBegin(), getSelectionEnd() - getSelectionBegin()); } setCursorPosition(2 * getSelectionBegin()); resetSelection(2 * getSelectionBegin()); } else /* Paste */ if (event->matches(QKeySequence::Paste)) { QClipboard *clipboard = QApplication::clipboard(); QByteArray ba = QByteArray().fromHex(clipboard->text().toLatin1()); if (_overwriteMode) { ba = ba.left(std::min(ba.size(), (_chunks->size() - _bPosCurrent))); replace(_bPosCurrent, ba.size(), ba); } else insert(_bPosCurrent, ba); setCursorPosition(_cursorPosition + 2 * ba.size()); resetSelection(getSelectionBegin()); } else /* Delete char */ if (event->matches(QKeySequence::Delete)) { if (getSelectionBegin() != getSelectionEnd()) { _bPosCurrent = getSelectionBegin(); if (_overwriteMode) { QByteArray ba = QByteArray(getSelectionEnd() - getSelectionBegin(), char(0)); replace(_bPosCurrent, ba.size(), ba); } else { remove(_bPosCurrent, getSelectionEnd() - getSelectionBegin()); } } else { if (_overwriteMode) replace(_bPosCurrent, char(0)); else remove(_bPosCurrent, 1); } setCursorPosition(2 * _bPosCurrent); resetSelection(2 * _bPosCurrent); } else /* Backspace */ if ((event->key() == Qt::Key_Backspace) && (event->modifiers() == Qt::NoModifier)) { if (getSelectionBegin() != getSelectionEnd()) { _bPosCurrent = getSelectionBegin(); setCursorPosition(2 * _bPosCurrent); if (_overwriteMode) { QByteArray ba = QByteArray(getSelectionEnd() - getSelectionBegin(), char(0)); replace(_bPosCurrent, ba.size(), ba); } else { remove(_bPosCurrent, getSelectionEnd() - getSelectionBegin()); } resetSelection(2 * _bPosCurrent); } else { bool behindLastByte = false; if ((_cursorPosition / 2) == _chunks->size()) behindLastByte = true; _bPosCurrent -= 1; if (_overwriteMode) replace(_bPosCurrent, char(0)); else remove(_bPosCurrent, 1); if (!behindLastByte) _bPosCurrent -= 1; setCursorPosition(2 * _bPosCurrent); resetSelection(2 * _bPosCurrent); } } else /* undo */ if (event->matches(QKeySequence::Undo)) { undo(); } else /* redo */ if (event->matches(QKeySequence::Redo)) { redo(); } else if ((QApplication::keyboardModifiers() == Qt::NoModifier) || (QApplication::keyboardModifiers() == Qt::KeypadModifier) || (QApplication::keyboardModifiers() == Qt::ShiftModifier) || (QApplication::keyboardModifiers() == (Qt::AltModifier | Qt::ControlModifier)) || (QApplication::keyboardModifiers() == Qt::GroupSwitchModifier)) { /* Hex and ascii input */ int key; if (_editAreaIsAscii) key = (uchar)event->text()[0].toLatin1(); else key = int(event->text()[0].toLower().toLatin1()); if ((((key >= '0' && key <= '9') || (key >= 'a' && key <= 'f')) && _editAreaIsAscii == false) || (key >= ' ' && _editAreaIsAscii)) { if (getSelectionBegin() != getSelectionEnd()) { if (_overwriteMode) { qint64 len = getSelectionEnd() - getSelectionBegin(); replace(getSelectionBegin(), (int)len, QByteArray((int)len, char(0))); } else { remove(getSelectionBegin(), getSelectionEnd() - getSelectionBegin()); _bPosCurrent = getSelectionBegin(); } setCursorPosition(2 * _bPosCurrent); resetSelection(2 * _bPosCurrent); } // If insert mode, then insert a byte if (_overwriteMode == false) if ((_cursorPosition % 2) == 0) insert(_bPosCurrent, char(0)); // Change content if (_chunks->size() > 0) { char ch = key; if (!_editAreaIsAscii){ QByteArray hexValue = _chunks->data(_bPosCurrent, 1).toHex(); if ((_cursorPosition % 2) == 0) hexValue[0] = key; else hexValue[1] = key; ch = QByteArray().fromHex(hexValue)[0]; } replace(_bPosCurrent, ch); if (_editAreaIsAscii) setCursorPosition(_cursorPosition + 2); else setCursorPosition(_cursorPosition + 1); resetSelection(_cursorPosition); } } } } /* Copy */ if (event->matches(QKeySequence::Copy)) { QByteArray ba = _chunks->data(getSelectionBegin(), getSelectionEnd() - getSelectionBegin()).toHex(); for (qint64 idx = 32; idx < ba.size(); idx +=33) ba.insert(idx, "\n"); QClipboard *clipboard = QApplication::clipboard(); clipboard->setText(ba); } // Switch between insert/overwrite mode if ((event->key() == Qt::Key_Insert) && (event->modifiers() == Qt::NoModifier)) { setOverwriteMode(!overwriteMode()); setCursorPosition(_cursorPosition); } // switch from hex to ascii edit if (event->key() == Qt::Key_Tab && !_editAreaIsAscii){ _editAreaIsAscii = true; setCursorPosition(_cursorPosition); } // switch from ascii to hex edit if (event->key() == Qt::Key_Backtab && _editAreaIsAscii){ _editAreaIsAscii = false; setCursorPosition(_cursorPosition); } refresh(); } void QHexEdit::mouseMoveEvent(QMouseEvent * event) { _blink = false; viewport()->update(); qint64 actPos = cursorPosition(event->pos()); if (actPos >= 0) { setCursorPosition(actPos); setSelection(actPos); } } void QHexEdit::mousePressEvent(QMouseEvent * event) { _blink = false; viewport()->update(); qint64 cPos = cursorPosition(event->pos()); if (cPos >= 0) { resetSelection(cPos); setCursorPosition(cPos); } } void QHexEdit::paintEvent(QPaintEvent *event) { QPainter painter(viewport()); int pxOfsX = horizontalScrollBar()->value(); if (event->rect() != _cursorRect) { int pxPosStartY = _pxCharHeight; // draw some patterns if needed painter.fillRect(event->rect(), viewport()->palette().color(QPalette::Base)); if (_addressArea) painter.fillRect(QRect(-pxOfsX, event->rect().top(), _pxPosHexX - _pxGapAdrHex/2, height()), _addressAreaColor); if (_asciiArea) { int linePos = _pxPosAsciiX - (_pxGapHexAscii / 2); painter.setPen(Qt::gray); painter.drawLine(linePos - pxOfsX, event->rect().top(), linePos - pxOfsX, height()); } painter.setPen(viewport()->palette().color(QPalette::WindowText)); // paint address area if (_addressArea) { QString address; for (int row=0, pxPosY = _pxCharHeight; row <= (_dataShown.size()/_bytesPerLine); row++, pxPosY +=_pxCharHeight) { address = QString("%1").arg(_bPosFirst + row*_bytesPerLine + _addressOffset, _addrDigits, 16, QChar('0')); painter.drawText(_pxPosAdrX - pxOfsX, pxPosY, address); } } // paint hex and ascii area QPen colStandard = QPen(viewport()->palette().color(QPalette::WindowText)); painter.setBackgroundMode(Qt::TransparentMode); for (int row = 0, pxPosY = pxPosStartY; row <= _rowsShown; row++, pxPosY +=_pxCharHeight) { QByteArray hex; int pxPosX = _pxPosHexX - pxOfsX; int pxPosAsciiX2 = _pxPosAsciiX - pxOfsX; qint64 bPosLine = row * _bytesPerLine; for (int colIdx = 0; ((bPosLine + colIdx) < _dataShown.size() && (colIdx < _bytesPerLine)); colIdx++) { QColor c = viewport()->palette().color(QPalette::Base); painter.setPen(colStandard); qint64 posBa = _bPosFirst + bPosLine + colIdx; if ((getSelectionBegin() <= posBa) && (getSelectionEnd() > posBa)) { c = _brushSelection.color(); painter.setPen(_penSelection); } else { if (_highlighting) if (_markedShown.at((int)(posBa - _bPosFirst))) { c = _brushHighlighted.color(); painter.setPen(_penHighlighted); } } // render hex value QRect r; if (colIdx == 0) r.setRect(pxPosX, pxPosY - _pxCharHeight + _pxSelectionSub, 2*_pxCharWidth, _pxCharHeight); else r.setRect(pxPosX - _pxCharWidth, pxPosY - _pxCharHeight + _pxSelectionSub, 3*_pxCharWidth, _pxCharHeight); painter.fillRect(r, c); hex = _hexDataShown.mid((bPosLine + colIdx) * 2, 2); painter.drawText(pxPosX, pxPosY, hexCaps()?hex.toUpper():hex); pxPosX += 3*_pxCharWidth; // render ascii value if (_asciiArea) { int ch = (uchar)_dataShown.at(bPosLine + colIdx); if ( ch < 0x20 ) ch = '.'; r.setRect(pxPosAsciiX2, pxPosY - _pxCharHeight + _pxSelectionSub, _pxCharWidth, _pxCharHeight); painter.fillRect(r, c); painter.drawText(pxPosAsciiX2, pxPosY, QChar(ch)); pxPosAsciiX2 += _pxCharWidth; } } } painter.setBackgroundMode(Qt::TransparentMode); painter.setPen(viewport()->palette().color(QPalette::WindowText)); } // paint cursor if (_blink && !_readOnly && hasFocus()) painter.fillRect(_cursorRect, this->palette().color(QPalette::WindowText)); else { painter.fillRect(QRect(_pxCursorX - pxOfsX, _pxCursorY - _pxCharHeight, _pxCharWidth, _pxCharHeight), viewport()->palette().color(QPalette::Base)); if (_editAreaIsAscii) { QByteArray ba = _dataShown.mid((_cursorPosition - _bPosFirst) / 2, 1); if (ba != "") { if (ba.at(0) <= ' ') ba[0] = '.'; painter.drawText(_pxCursorX - pxOfsX, _pxCursorY, ba); } } else { painter.drawText(_pxCursorX - pxOfsX, _pxCursorY, _hexDataShown.mid(_cursorPosition - _bPosFirst, 1)); } } // emit event, if size has changed if (_lastEventSize != _chunks->size()) { _lastEventSize = _chunks->size(); emit currentSizeChanged(_lastEventSize); } } void QHexEdit::resizeEvent(QResizeEvent *) { if (_dynamicBytesPerLine) { int pxFixGaps = 0; if (_addressArea) pxFixGaps = addressWidth() * _pxCharWidth + _pxGapAdr; pxFixGaps += _pxGapAdrHex; if (_asciiArea) pxFixGaps += _pxGapHexAscii; // +1 because the last hex value do not have space. so it is effective one char more int charWidth = (viewport()->width() - pxFixGaps ) / _pxCharWidth + 1; // 2 hex alfa-digits 1 space 1 ascii per byte = 4; if ascii is disabled then 3 // to prevent devision by zero use the min value 1 setBytesPerLine(std::max(charWidth / (_asciiArea ? 4 : 3),1)); } adjust(); } bool QHexEdit::focusNextPrevChild(bool next) { if (_addressArea) { if ( (next && _editAreaIsAscii) || (!next && !_editAreaIsAscii )) return QWidget::focusNextPrevChild(next); else return false; } else { return QWidget::focusNextPrevChild(next); } } // ********************************************************************** Handle selections void QHexEdit::resetSelection() { _bSelectionBegin = _bSelectionInit; _bSelectionEnd = _bSelectionInit; } void QHexEdit::resetSelection(qint64 pos) { pos = pos / 2 ; if (pos < 0) pos = 0; if (pos > _chunks->size()) pos = _chunks->size(); _bSelectionInit = pos; _bSelectionBegin = pos; _bSelectionEnd = pos; } void QHexEdit::setSelection(qint64 pos) { pos = pos / 2; if (pos < 0) pos = 0; if (pos > _chunks->size()) pos = _chunks->size(); if (pos >= _bSelectionInit) { _bSelectionEnd = pos; _bSelectionBegin = _bSelectionInit; } else { _bSelectionBegin = pos; _bSelectionEnd = _bSelectionInit; } } int QHexEdit::getSelectionBegin() { return _bSelectionBegin; } int QHexEdit::getSelectionEnd() { return _bSelectionEnd; } // ********************************************************************** Private utility functions void QHexEdit::init() { _undoStack->clear(); setAddressOffset(0); resetSelection(0); setCursorPosition(0); verticalScrollBar()->setValue(0); _modified = false; } void QHexEdit::adjust() { // recalc Graphics if (_addressArea) { _addrDigits = addressWidth(); _pxPosHexX = _pxGapAdr + _addrDigits*_pxCharWidth + _pxGapAdrHex; } else _pxPosHexX = _pxGapAdrHex; _pxPosAdrX = _pxGapAdr; _pxPosAsciiX = _pxPosHexX + _hexCharsInLine * _pxCharWidth + _pxGapHexAscii; // set horizontalScrollBar() int pxWidth = _pxPosAsciiX; if (_asciiArea) pxWidth += _bytesPerLine*_pxCharWidth; horizontalScrollBar()->setRange(0, pxWidth - viewport()->width()); horizontalScrollBar()->setPageStep(viewport()->width()); // set verticalScrollbar() _rowsShown = ((viewport()->height()-4)/_pxCharHeight); int lineCount = (int)(_chunks->size() / (qint64)_bytesPerLine) + 1; verticalScrollBar()->setRange(0, lineCount - _rowsShown); verticalScrollBar()->setPageStep(_rowsShown); int value = verticalScrollBar()->value(); _bPosFirst = (qint64)value * _bytesPerLine; _bPosLast = _bPosFirst + (qint64)(_rowsShown * _bytesPerLine) - 1; if (_bPosLast >= _chunks->size()) _bPosLast = _chunks->size() - 1; readBuffers(); setCursorPosition(_cursorPosition); } void QHexEdit::dataChangedPrivate(int) { _modified = _undoStack->index() != 0; adjust(); emit dataChanged(); } void QHexEdit::refresh() { ensureVisible(); readBuffers(); } void QHexEdit::readBuffers() { _dataShown = _chunks->data(_bPosFirst, _bPosLast - _bPosFirst + _bytesPerLine + 1, &_markedShown); _hexDataShown = QByteArray(_dataShown.toHex()); } QString QHexEdit::toReadable(const QByteArray &ba) { QString result; for (int i=0; i < ba.size(); i += 16) { QString addrStr = QString("%1").arg(_addressOffset + i, addressWidth(), 16, QChar('0')); QString hexStr; QString ascStr; for (int j=0; j<16; j++) { if ((i + j) < ba.size()) { hexStr.append(" ").append(ba.mid(i+j, 1).toHex()); char ch = ba[i + j]; if ((ch < 0x20) || (ch > 0x7e)) ch = '.'; ascStr.append(QChar(ch)); } } result += addrStr + " " + QString("%1").arg(hexStr, -48) + " " + QString("%1").arg(ascStr, -17) + "\n"; } return result; } void QHexEdit::updateCursor() { if (_blink) _blink = false; else _blink = true; viewport()->update(_cursorRect); } ostinato-1.3.0/extra/qhexedit2/src/qhexedit.h000066400000000000000000000370641451413623100211600ustar00rootroot00000000000000#ifndef QHEXEDIT_H #define QHEXEDIT_H #include #include #include #include "chunks.h" #include "commands.h" #ifdef QHEXEDIT_EXPORTS #define QHEXEDIT_API Q_DECL_EXPORT #elif QHEXEDIT_IMPORTS #define QHEXEDIT_API Q_DECL_IMPORT #else #define QHEXEDIT_API #endif /** \mainpage QHexEdit is a binary editor widget for Qt. \version Version 0.8.3 \image html qhexedit.png */ /** QHexEdit is a hex editor widget written in C++ for the Qt (Qt4, Qt5) framework. It is a simple editor for binary data, just like QPlainTextEdit is for text data. There are sip configuration files included, so it is easy to create bindings for PyQt and you can use this widget also in python 2 and 3. QHexEdit takes the data of a QByteArray (setData()) and shows it. You can use the mouse or the keyboard to navigate inside the widget. If you hit the keys (0..9, a..f) you will change the data. Changed data is highlighted and can be accessed via data(). Normaly QHexEdit works in the overwrite Mode. You can set overwriteMode(false) and insert data. In this case the size of data() increases. It is also possible to delete bytes (del or backspace), here the size of data decreases. You can select data with keyboard hits or mouse movements. The copy-key will copy the selected data into the clipboard. The cut-key copies also but delets it afterwards. In overwrite mode, the paste function overwrites the content of the (does not change the length) data. In insert mode, clipboard data will be inserted. The clipboard content is expected in ASCII Hex notation. Unknown characters will be ignored. QHexEdit comes with undo/redo functionality. All changes can be undone, by pressing the undo-key (usually ctr-z). They can also be redone afterwards. The undo/redo framework is cleared, when setData() sets up a new content for the editor. You can search data inside the content with indexOf() and lastIndexOf(). The replace() function is to change located subdata. This 'replaced' data can also be undone by the undo/redo framework. QHexEdit is based on QIODevice, that's why QHexEdit can handle big amounts of data. The size of edited data can be more then two gigabytes without any restrictions. */ class QHEXEDIT_API QHexEdit : public QAbstractScrollArea { Q_OBJECT /*! Property address area switch the address area on or off. Set addressArea true (show it), false (hide it). */ Q_PROPERTY(bool addressArea READ addressArea WRITE setAddressArea) /*! Property address area color sets (setAddressAreaColor()) the backgorund color of address areas. You can also read the color (addressaAreaColor()). */ Q_PROPERTY(QColor addressAreaColor READ addressAreaColor WRITE setAddressAreaColor) /*! Property addressOffset is added to the Numbers of the Address Area. A offset in the address area (left side) is sometimes usefull, whe you show only a segment of a complete memory picture. With setAddressOffset() you set this property - with addressOffset() you get the current value. */ Q_PROPERTY(qint64 addressOffset READ addressOffset WRITE setAddressOffset) /*! Set and get the minimum width of the address area, width in characters. */ Q_PROPERTY(int addressWidth READ addressWidth WRITE setAddressWidth) /*! Switch the ascii area on (true, show it) or off (false, hide it). */ Q_PROPERTY(bool asciiArea READ asciiArea WRITE setAsciiArea) /*! Set and get bytes number per line.*/ Q_PROPERTY(int bytesPerLine READ bytesPerLine WRITE setBytesPerLine) /*! Porperty cursorPosition sets or gets the position of the editor cursor in QHexEdit. Every byte in data has to cursor positions: the lower and upper Nibble. Maximum cursor position is factor two of data.size(). */ Q_PROPERTY(qint64 cursorPosition READ cursorPosition WRITE setCursorPosition) /*! Property data holds the content of QHexEdit. Call setData() to set the content of QHexEdit, data() returns the actual content. When calling setData() with a QByteArray as argument, QHexEdit creates a internal copy of the data If you want to edit big files please use setData(), based on QIODevice. */ Q_PROPERTY(QByteArray data READ data WRITE setData NOTIFY dataChanged) /*! That property defines if the hex values looks as a-f if the value is false(default) or A-F if value is true. */ Q_PROPERTY(bool hexCaps READ hexCaps WRITE setHexCaps) /*! Property defines the dynamic calculation of bytesPerLine parameter depends of width of widget. set this property true to avoid horizontal scrollbars and show the maximal possible data. defalut value is false*/ Q_PROPERTY(bool dynamicBytesPerLine READ dynamicBytesPerLine WRITE setDynamicBytesPerLine) /*! Switch the highlighting feature on or of: true (show it), false (hide it). */ Q_PROPERTY(bool highlighting READ highlighting WRITE setHighlighting) /*! Property highlighting color sets (setHighlightingColor()) the backgorund color of highlighted text areas. You can also read the color (highlightingColor()). */ Q_PROPERTY(QColor highlightingColor READ highlightingColor WRITE setHighlightingColor) /*! Porperty overwrite mode sets (setOverwriteMode()) or gets (overwriteMode()) the mode in which the editor works. In overwrite mode the user will overwrite existing data. The size of data will be constant. In insert mode the size will grow, when inserting new data. */ Q_PROPERTY(bool overwriteMode READ overwriteMode WRITE setOverwriteMode) /*! Property selection color sets (setSelectionColor()) the backgorund color of selected text areas. You can also read the color (selectionColor()). */ Q_PROPERTY(QColor selectionColor READ selectionColor WRITE setSelectionColor) /*! Porperty readOnly sets (setReadOnly()) or gets (isReadOnly) the mode in which the editor works. In readonly mode the the user can only navigate through the data and select data; modifying is not possible. This property's default is false. */ Q_PROPERTY(bool readOnly READ isReadOnly WRITE setReadOnly) /*! Set the font of the widget. Please use fixed width fonts like Mono or Courier.*/ Q_PROPERTY(QFont font READ font WRITE setFont) public: /*! Creates an instance of QHexEdit. \param parent Parent widget of QHexEdit. */ QHexEdit(QWidget *parent=0); // Access to data of qhexedit /*! Sets the data of QHexEdit. The QIODevice will be opend just before reading and closed immediately afterwards. This is to allow other programs to rewrite the file while editing it. */ bool setData(QIODevice &iODevice); /*! Givs back the data as a QByteArray starting at position \param pos and delivering \param count bytes. */ QByteArray dataAt(qint64 pos, qint64 count=-1); /*! Givs back the data into a \param iODevice starting at position \param pos and delivering \param count bytes. */ bool write(QIODevice &iODevice, qint64 pos=0, qint64 count=-1); // Char handling /*! Inserts a char. \param pos Index position, where to insert \param ch Char, which is to insert The char will be inserted and size of data grows. */ void insert(qint64 pos, char ch); /*! Removes len bytes from the content. \param pos Index position, where to remove \param len Amount of bytes to remove */ void remove(qint64 pos, qint64 len=1); /*! Replaces a char. \param pos Index position, where to overwrite \param ch Char, which is to insert The char will be overwritten and size remains constant. */ void replace(qint64 pos, char ch); // ByteArray handling /*! Inserts a byte array. \param pos Index position, where to insert \param ba QByteArray, which is to insert The QByteArray will be inserted and size of data grows. */ void insert(qint64 pos, const QByteArray &ba); /*! Replaces \param len bytes with a byte array \param ba \param pos Index position, where to overwrite \param ba QByteArray, which is inserted \param len count of bytes to overwrite The data is overwritten and size of data may change. */ void replace(qint64 pos, qint64 len, const QByteArray &ba); // Utility functioins /*! Calc cursor position from graphics position * \param point from where the cursor position should be calculated * \return Cursor postioin */ qint64 cursorPosition(QPoint point); /*! Ensure the cursor to be visble */ void ensureVisible(); /*! Find first occurence of ba in QHexEdit data * \param ba Data to find * \param from Point where the search starts * \return pos if fond, else -1 */ qint64 indexOf(const QByteArray &ba, qint64 from); /*! Returns if any changes where done on document * \return true when document is modified else false */ bool isModified(); /*! Find last occurence of ba in QHexEdit data * \param ba Data to find * \param from Point where the search starts * \return pos if fond, else -1 */ qint64 lastIndexOf(const QByteArray &ba, qint64 from); /*! Gives back a formatted image of the selected content of QHexEdit */ QString selectionToReadableString(); /*! Set Font of QHexEdit * \param font */ void setFont(const QFont &font); /*! Gives back a formatted image of the content of QHexEdit */ QString toReadableString(); public slots: /*! Redoes the last operation. If there is no operation to redo, i.e. there is no redo step in the undo/redo history, nothing happens. */ void redo(); /*! Undoes the last operation. If there is no operation to undo, i.e. there is no undo step in the undo/redo history, nothing happens. */ void undo(); signals: /*! Contains the address, where the cursor is located. */ void currentAddressChanged(qint64 address); /*! Contains the size of the data to edit. */ void currentSizeChanged(qint64 size); /*! The signal is emitted every time, the data is changed. */ void dataChanged(); /*! The signal is emitted every time, the overwrite mode is changed. */ void overwriteModeChanged(bool state); /*! \cond docNever */ public: ~QHexEdit(); // Properties bool addressArea(); void setAddressArea(bool addressArea); QColor addressAreaColor(); void setAddressAreaColor(const QColor &color); qint64 addressOffset(); void setAddressOffset(qint64 addressArea); int addressWidth(); void setAddressWidth(int addressWidth); bool asciiArea(); void setAsciiArea(bool asciiArea); int bytesPerLine(); void setBytesPerLine(int count); qint64 cursorPosition(); void setCursorPosition(qint64 position); QByteArray data(); void setData(const QByteArray &ba); void setHexCaps(const bool isCaps); bool hexCaps(); void setDynamicBytesPerLine(const bool isDynamic); bool dynamicBytesPerLine(); bool highlighting(); void setHighlighting(bool mode); QColor highlightingColor(); void setHighlightingColor(const QColor &color); bool overwriteMode(); void setOverwriteMode(bool overwriteMode); bool isReadOnly(); void setReadOnly(bool readOnly); QColor selectionColor(); void setSelectionColor(const QColor &color); protected: // Handle events void keyPressEvent(QKeyEvent *event); void mouseMoveEvent(QMouseEvent * event); void mousePressEvent(QMouseEvent * event); void paintEvent(QPaintEvent *event); void resizeEvent(QResizeEvent *); virtual bool focusNextPrevChild(bool next); private: // Handle selections void resetSelection(qint64 pos); // set selectionStart and selectionEnd to pos void resetSelection(); // set selectionEnd to selectionStart void setSelection(qint64 pos); // set min (if below init) or max (if greater init) int getSelectionBegin(); int getSelectionEnd(); // Private utility functions void init(); void readBuffers(); QString toReadable(const QByteArray &ba); private slots: void adjust(); // recalc pixel positions void dataChangedPrivate(int idx=0); // emit dataChanged() signal void refresh(); // ensureVisible() and readBuffers() void updateCursor(); // update blinking cursor private: // Name convention: pixel positions start with _px int _pxCharWidth, _pxCharHeight; // char dimensions (dpendend on font) int _pxPosHexX; // X-Pos of HeaxArea int _pxPosAdrX; // X-Pos of Address Area int _pxPosAsciiX; // X-Pos of Ascii Area int _pxGapAdr; // gap left from AddressArea int _pxGapAdrHex; // gap between AddressArea and HexAerea int _pxGapHexAscii; // gap between HexArea and AsciiArea int _pxCursorWidth; // cursor width int _pxSelectionSub; // offset selection rect int _pxCursorX; // current cursor pos int _pxCursorY; // current cursor pos // Name convention: absolute byte positions in chunks start with _b qint64 _bSelectionBegin; // first position of Selection qint64 _bSelectionEnd; // end of Selection qint64 _bSelectionInit; // memory position of Selection qint64 _bPosFirst; // position of first byte shown qint64 _bPosLast; // position of last byte shown qint64 _bPosCurrent; // current position // variables to store the property values bool _addressArea; // left area of QHexEdit QColor _addressAreaColor; int _addressWidth; bool _asciiArea; qint64 _addressOffset; int _bytesPerLine; int _hexCharsInLine; bool _highlighting; bool _overwriteMode; QBrush _brushSelection; QPen _penSelection; QBrush _brushHighlighted; QPen _penHighlighted; bool _readOnly; bool _hexCaps; bool _dynamicBytesPerLine; // other variables bool _editAreaIsAscii; // flag about the ascii mode edited int _addrDigits; // real no of addressdigits, may be > addressWidth bool _blink; // help get cursor blinking QBuffer _bData; // buffer, when setup with QByteArray Chunks *_chunks; // IODevice based access to data QTimer _cursorTimer; // for blinking cursor qint64 _cursorPosition; // absolute positioin of cursor, 1 Byte == 2 tics QRect _cursorRect; // physical dimensions of cursor QByteArray _data; // QHexEdit's data, when setup with QByteArray QByteArray _dataShown; // data in the current View QByteArray _hexDataShown; // data in view, transformed to hex qint64 _lastEventSize; // size, which was emitted last time QByteArray _markedShown; // marked data in view bool _modified; // Is any data in editor modified? int _rowsShown; // lines of text shown UndoStack * _undoStack; // Stack to store edit actions for undo/redo /*! \endcond docNever */ }; #endif // QHEXEDIT_H ostinato-1.3.0/install.pri000066400000000000000000000005511451413623100155360ustar00rootroot00000000000000# A custom install path prefix can be provided by passing PREFIX=/absolute/path # to qmake; if one is not provided, we use the below defaults - isEmpty(PREFIX) { unix:PREFIX = "/usr/local/" macx:PREFIX = "/Applications/" win32:PREFIX = "../" } macx { target.path = $$PREFIX/Ostinato } else { target.path = $$PREFIX/bin } INSTALLS += target ostinato-1.3.0/options.pri000066400000000000000000000003431451413623100155620ustar00rootroot00000000000000QMAKE_CXXFLAGS += -isystem $$[QT_INSTALL_HEADERS] -std=c++11 CONFIG(debug, debug|release): QMAKE_CXXFLAGS_WARN_ON += -Wall -W -Wextra -Werror CONFIG(debug, debug|release): QMAKE_CXXFLAGS_WARN_ON += -Wno-deprecated-declarations ostinato-1.3.0/ost.pro000066400000000000000000000006171451413623100147060ustar00rootroot00000000000000TEMPLATE = subdirs SUBDIRS = client server ostproto ostprotogui rpc extra client.target = client client.file = client/ostinato.pro client.depends = ostproto ostprotogui rpc extra server.target = server server.file = server/drone.pro server.depends = ostproto rpc ostproto.file = common/ostproto.pro ostprotogui.file = common/ostprotogui.pro ostprotogui.depends = extra rpc.file = rpc/pbrpc.pro ostinato-1.3.0/protobuf.pri000066400000000000000000000034561451413623100157370ustar00rootroot00000000000000# # Qt qmake integration with Google Protocol Buffers compiler protoc # Author: Srivats P. # # To compile protocol buffers with qt qmake, specify PROTOS variable and # include this file # # Example: # PROTOS = a.proto b.proto # include(protobuf.pri) # # By default protoc looks for .proto files (including the imported ones) in # the current directory where protoc is run. If you need to include additional # paths specify the PROTOPATH variable # PROTOPATH += . PROTOPATHS = for(p, PROTOPATH):PROTOPATHS += --proto_path=$${p} PROTO_CC += $$replace(PROTOS, \.proto, .pb.cc) protobuf_decl.name = protobuf header protobuf_decl.input = PROTOS protobuf_decl.output = ${QMAKE_FILE_BASE}.pb.h protobuf_decl.commands = protoc --cpp_out="." $${PROTOPATHS} ${QMAKE_FILE_NAME} protobuf_decl.variable_out = GENERATED_FILES QMAKE_EXTRA_COMPILERS += protobuf_decl protobuf_impl.name = protobuf implementation protobuf_impl.input = PROTOS protobuf_impl.output = ${QMAKE_FILE_BASE}.pb.cc protobuf_impl.depends = ${QMAKE_FILE_BASE}.pb.h protobuf_impl.commands = $$escape_expand(\\n) protobuf_impl.variable_out = GENERATED_FILES QMAKE_EXTRA_COMPILERS += protobuf_impl # protobuf generated code emits compiler warnings, so use -Wno-error to # compile 'em; if and when protobuf generates clean code, make the following # changes - # - protobuf_impl.variable_out = GENERATED_FILES # + protobuf_impl.variable_out = GENERATED_SOURCES # - QMAKE_EXTRA_COMPILERS += protobuf_cc # - #QMAKE_EXTRA_COMPILERS += protobuf_cc protobuf_cc.name = protobuf cc compilation protobuf_cc.input = PROTO_CC protobuf_cc.output = ${QMAKE_FILE_BASE}.o protobuf_cc.dependency_type = TYPE_C protobuf_cc.commands = $(CXX) -c $(CXXFLAGS) -Wno-error $(INCPATH) -o ${QMAKE_FILE_OUT} ${QMAKE_FILE_IN} protobuf_cc.variable_out = OBJECTS QMAKE_EXTRA_COMPILERS += protobuf_cc ostinato-1.3.0/rpc/000077500000000000000000000000001451413623100141375ustar00rootroot00000000000000ostinato-1.3.0/rpc/pbhelper.h000066400000000000000000000131171451413623100161140ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _PB_HELPER_H #define _PB_HELPER_H #include #include #include #if 0 // not reqd. any longer? class PbHelper { public: // FIXME: Change msg from * to & void ForceSetSingularDefault(::google::protobuf::Message *msg) { const ::google::protobuf::Descriptor *desc; ::google::protobuf::Message::Reflection *refl; qDebug("In %s", __FUNCTION__); desc = msg->GetDescriptor(); refl = msg->GetReflection(); for (int i=0; i < desc->field_count(); i++) { const ::google::protobuf::FieldDescriptor *f; f = desc->field(i); // Ensure field is singular and not already set if (f->label() == ::google::protobuf::FieldDescriptor::LABEL_REPEATED) continue; if (refl->HasField(f)) continue; switch(f->type()) { case ::google::protobuf::FieldDescriptor::TYPE_DOUBLE: refl->SetDouble(f, refl->GetDouble(f)); break; case ::google::protobuf::FieldDescriptor::TYPE_FLOAT: refl->SetFloat(f, refl->GetFloat(f)); break; case ::google::protobuf::FieldDescriptor::TYPE_INT32: case ::google::protobuf::FieldDescriptor::TYPE_SINT32: case ::google::protobuf::FieldDescriptor::TYPE_SFIXED32: refl->SetInt32(f, refl->GetInt32(f)); break; case ::google::protobuf::FieldDescriptor::TYPE_INT64: case ::google::protobuf::FieldDescriptor::TYPE_SINT64: case ::google::protobuf::FieldDescriptor::TYPE_SFIXED64: refl->SetInt64(f, refl->GetInt64(f)); break; case ::google::protobuf::FieldDescriptor::TYPE_UINT32: case ::google::protobuf::FieldDescriptor::TYPE_FIXED32: refl->SetUInt32(f, refl->GetUInt32(f)); break; case ::google::protobuf::FieldDescriptor::TYPE_UINT64: case ::google::protobuf::FieldDescriptor::TYPE_FIXED64: refl->SetUInt64(f, refl->GetUInt64(f)); break; case ::google::protobuf::FieldDescriptor::TYPE_BOOL: refl->SetBool(f, refl->GetBool(f)); break; case ::google::protobuf::FieldDescriptor::TYPE_ENUM: refl->SetEnum(f, refl->GetEnum(f)); break; case ::google::protobuf::FieldDescriptor::TYPE_STRING: case ::google::protobuf::FieldDescriptor::TYPE_BYTES: refl->SetString(f, refl->GetString(f)); break; case ::google::protobuf::FieldDescriptor::TYPE_MESSAGE: case ::google::protobuf::FieldDescriptor::TYPE_GROUP: ForceSetSingularDefault(refl->MutableMessage(f)); // recursion! break; default: qDebug("unhandled Field Type"); break; } } } bool update( ::google::protobuf::Message *target, ::google::protobuf::Message *source) { // FIXME(HI): Depracate: use MergeFrom() directly qDebug("In %s", __FUNCTION__); target->MergeFrom(*source); return true; #if 0 ::google::protobuf::Message::Reflection *sourceRef; ::google::protobuf::Message::Reflection *targetRef; std::vector srcFieldList; if (source->GetDescriptor()->full_name() != target->GetDescriptor()->full_name()) goto _error_exit; sourceRef = source->GetReflection(); targetRef = target->GetReflection(); sourceRef->ListFields(&srcFieldList); for (uint i=0; i < srcFieldList.size(); i++) { const ::google::protobuf::FieldDescriptor *srcField, *targetField; srcField = srcFieldList[i]; targetField = target->GetDescriptor()->FindFieldByName( srcField->name()); switch(targetField->type()) { case ::google::protobuf::FieldDescriptor::TYPE_UINT32: targetRef->SetUInt32(targetField, sourceRef->GetUInt32(srcField)); break; case ::google::protobuf::FieldDescriptor::TYPE_BOOL: targetRef->SetBool(targetField, sourceRef->GetBool(srcField)); break; case ::google::protobuf::FieldDescriptor::TYPE_STRING: targetRef->SetString(targetField, sourceRef->GetString(srcField)); break; default: qDebug("unhandled Field Type"); break; } } _error_exit: qDebug("%s: error!", __FUNCTION__); return false; #endif } }; #endif #endif ostinato-1.3.0/rpc/pbqtio.h000066400000000000000000000014751451413623100156150ustar00rootroot00000000000000#ifndef _PBQTIO_H #define _PBQTIO_H #include class PbQtInputStream : public google::protobuf::io::CopyingInputStream { public: PbQtInputStream(QIODevice *dev) : dev_(dev) {}; int Read(void *buffer, int size) { if (dev_->bytesAvailable()) return dev_->read(static_cast(buffer), size); else return 0; } private: QIODevice *dev_; }; class PbQtOutputStream : public google::protobuf::io::CopyingOutputStream { public: PbQtOutputStream(QIODevice *dev) : dev_(dev) {}; bool Write(const void *buffer, int size) { if (dev_->write(static_cast(buffer), size) == size) return true; else return false; } private: QIODevice *dev_; }; #endif ostinato-1.3.0/rpc/pbrpc.pro000066400000000000000000000003711451413623100157700ustar00rootroot00000000000000TEMPLATE = lib CONFIG += qt staticlib QT += network DEFINES += HAVE_REMOTE LIBS += -lprotobuf HEADERS += rpcserver.h rpcconn.h pbrpccontroller.h pbrpcchannel.h pbqtio.h SOURCES += rpcserver.cpp rpcconn.cpp pbrpcchannel.cpp include (../options.pri) ostinato-1.3.0/rpc/pbrpcchannel.cpp000066400000000000000000000355071451413623100173140ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #include "pbrpcchannel.h" #include "pbqtio.h" #include #include PbRpcChannel::PbRpcChannel(QString serverName, quint16 port, const ::google::protobuf::Message ¬ifProto) : notifPrototype(notifProto) { isPending = false; pendingMethodId = -1; // don't care as long as isPending is false method = NULL; controller = NULL; done = NULL; response = NULL; mServerHost = serverName; mServerPort = port; mpSocket = new QTcpSocket(this); inStream = new google::protobuf::io::CopyingInputStreamAdaptor( new PbQtInputStream(mpSocket)); inStream->SetOwnsCopyingStream(true); outStream = new google::protobuf::io::CopyingOutputStreamAdaptor( new PbQtOutputStream(mpSocket)); outStream->SetOwnsCopyingStream(true); // FIXME: Not quite sure why this ain't working! // QMetaObject::connectSlotsByName(this); connect(mpSocket, SIGNAL(connected()), this, SLOT(on_mpSocket_connected())); connect(mpSocket, SIGNAL(disconnected()), this, SLOT(on_mpSocket_disconnected())); connect(mpSocket, SIGNAL(stateChanged(QAbstractSocket::SocketState)), this, SLOT(on_mpSocket_stateChanged(QAbstractSocket::SocketState))); connect(mpSocket, SIGNAL(error(QAbstractSocket::SocketError)), this, SLOT(on_mpSocket_error(QAbstractSocket::SocketError))); connect(mpSocket, SIGNAL(readyRead()), this, SLOT(on_mpSocket_readyRead())); } PbRpcChannel::~PbRpcChannel() { delete inStream; delete outStream; delete mpSocket; } void PbRpcChannel::establish() { qDebug("In %s", __FUNCTION__); mpSocket->connectToHost(mServerHost, mServerPort); } void PbRpcChannel::establish(QString serverName, quint16 port) { mServerHost = serverName; mServerPort = port; establish(); } void PbRpcChannel::tearDown() { qDebug("In %s", __FUNCTION__); mpSocket->disconnectFromHost(); } void PbRpcChannel::CallMethod( const ::google::protobuf::MethodDescriptor *method, ::google::protobuf::RpcController *controller, const ::google::protobuf::Message *req, ::google::protobuf::Message *response, ::google::protobuf::Closure* done) { char* msg = (char*) &sendBuffer_[0]; int len; bool ret; if (isPending) { RpcCall call; qDebug("RpcChannel: queueing rpc since method %d is pending;<----\n " "queued method = %d:%s\n" "queued message = \n%s\n---->", pendingMethodId, method->index(), method->name().c_str(), req->DebugString().c_str()); call.method = method; call.controller = controller; call.request = req; call.response = response; call.done = done; pendingCallList.append(call); qDebug("pendingCallList size = %d", pendingCallList.size()); Q_ASSERT(pendingCallList.size() < 100); return; } if (!req->IsInitialized()) { qWarning("RpcChannel: missing required fields in request <----"); qDebug("req = %s\n%s", method->input_type()->name().c_str(), req->DebugString().c_str()); qDebug("error = \n%s\n--->", req->InitializationErrorString().c_str()); controller->SetFailed("Required fields missing"); done->Run(); return; } pendingMethodId = method->index(); this->method=method; this->controller=controller; this->done=done; this->response=response; isPending = true; len = req->ByteSize(); *((quint16*)(msg+0)) = qToBigEndian(quint16(PB_MSG_TYPE_REQUEST)); // type *((quint16*)(msg+2)) = qToBigEndian(quint16(method->index())); // method id *((quint32*)(msg+4)) = qToBigEndian(quint32(len)); // len // Avoid printing stats since it happens every couple of seconds if (pendingMethodId != 13) { qDebug("client(%s) sending %d bytes <----", __FUNCTION__, PB_HDR_SIZE + len); BUFDUMP(msg, PB_HDR_SIZE); qDebug("method = %d:%s\n req = %s\n%s\n---->", method->index(), method->name().c_str(), method->input_type()->name().c_str(), req->DebugString().c_str()); } mpSocket->write(msg, PB_HDR_SIZE); ret = req->SerializeToZeroCopyStream(outStream); Q_ASSERT(ret == true); Q_UNUSED(ret); outStream->Flush(); } void PbRpcChannel::on_mpSocket_readyRead() { const uchar *msg; int msgLen; _top: //qDebug("%s(entry): bytesAvail = %d", __FUNCTION__, mpSocket->bytesAvailable()); if (!parsing) { // Do we have an entire header? If not, we'll wait ... if (inStream->Next((const void**)&msg, &msgLen) == false) { qDebug("No more data or stream error"); goto _exit; } if (msgLen < PB_HDR_SIZE) { qDebug("read less than PB_HDR_SIZE bytes; putting back"); inStream->BackUp(msgLen); goto _exit; } type = qFromBigEndian(msg+0); methodId = qFromBigEndian(msg+2); len = qFromBigEndian(msg+4); if (msgLen > PB_HDR_SIZE) inStream->BackUp(msgLen - PB_HDR_SIZE); //BUFDUMP(msg, PB_HDR_SIZE); //qDebug("type = %hu, method = %hu, len = %u", type, method, len); parsing = true; } switch (type) { case PB_MSG_TYPE_BINBLOB: { QIODevice *blob; int l = 0; blob = static_cast(controller)->binaryBlob(); Q_ASSERT(blob != NULL); msgLen = 0; while (cumLen < len) { if (inStream->Next((const void**)&msg, &msgLen) == false) { //qDebug("No more data or stream error"); goto _exit; } l = qMin(msgLen, int(len - cumLen)); blob->write((char*)msg, l); cumLen += l; //qDebug("%s: bin blob rcvd %d/%d/%d", __PRETTY_FUNCTION__, l, cumLen, len); } if (l < msgLen) { qDebug("read extra bytes after blob; putting back"); inStream->BackUp(msgLen - l); } qDebug("%s: bin blob rcvd %d/%d", __PRETTY_FUNCTION__, cumLen, len); if (cumLen < len) goto _exit; cumLen = 0; if (!isPending) { qWarning("not waiting for response"); goto _error_exit2; } if (pendingMethodId != methodId) { qWarning("invalid method id %d (expected = %d)", methodId, pendingMethodId); goto _error_exit2; } break; } case PB_MSG_TYPE_RESPONSE: { int l = 0; if (!isPending) { qWarning("not waiting for response"); goto _error_exit; } if (pendingMethodId != methodId) { qWarning("invalid method id %d (expected = %d)", methodId, pendingMethodId); goto _error_exit; } msgLen = 0; while (cumLen < len) { if (inStream->Next((const void**)&msg, &msgLen) == false) { //qDebug("No more data or stream error"); goto _exit; } l = qMin(msgLen, int(len - cumLen)); buffer.append(QByteArray((char*)msg, l)); cumLen += l; //qDebug("%s: buffer rcvd %d/%d/%d", __PRETTY_FUNCTION__, l, cumLen, len); } if (l < msgLen) { qDebug("read extra bytes after response; putting back"); inStream->BackUp(msgLen - l); } #if 0 // not needed? if (cumLen < len) goto _exit; #endif if (len) response->ParseFromArray((const void*)buffer, len); cumLen = 0; buffer.resize(0); // Avoid printing stats if (methodId != 13) { qDebug("client(%s): Received Msg <---- ", __FUNCTION__); qDebug("method = %d:%s\nresp = %s\n%s\n---->", methodId, this->method->name().c_str(), this->method->output_type()->name().c_str(), response->DebugString().c_str()); } if (!response->IsInitialized()) { qWarning("RpcChannel: missing required fields in response <----"); qDebug("resp = %s\n%s", this->method->output_type()->name().c_str(), response->DebugString().c_str()); qDebug("error = \n%s\n--->", response->InitializationErrorString().c_str()); controller->SetFailed("Required fields missing"); } break; } case PB_MSG_TYPE_ERROR: { int l = 0; msgLen = 0; while (cumLen < len) { if (inStream->Next((const void**)&msg, &msgLen) == false) { //qDebug("No more data or stream error"); goto _exit; } l = qMin(msgLen, int(len - cumLen)); errorBuf.append(QByteArray((char*)msg, l)); cumLen += l; //qDebug("%s: error rcvd %d/%d/%d", __PRETTY_FUNCTION__, l, cumLen, len); } if (l < msgLen) { qDebug("read extra bytes after error; putting back"); inStream->BackUp(msgLen - l); } qDebug("%s: error rcvd %d/%d", __PRETTY_FUNCTION__, cumLen, len); if (cumLen < len) goto _exit; static_cast(controller)->SetFailed( QString::fromUtf8(errorBuf, len)); cumLen = 0; errorBuf.resize(0); if (!isPending) { qWarning("not waiting for response"); goto _error_exit2; } if (pendingMethodId != methodId) { qWarning("invalid method id %d (expected = %d)", methodId, pendingMethodId); goto _error_exit2; } break; } case PB_MSG_TYPE_NOTIFY: { notif = notifPrototype.New(); if (!notif) { qWarning("failed to alloc notify"); goto _error_exit; } if (len) notif->ParseFromBoundedZeroCopyStream(inStream, len); qDebug("client(%s): Received Notif Msg <---- ", __FUNCTION__); qDebug("type = %d\nnotif = \n%s\n---->", methodId, notif->DebugString().c_str()); if (!notif->IsInitialized()) { qWarning("RpcChannel: missing required fields in notify <----"); qDebug("notify = \n%s", notif->DebugString().c_str()); qDebug("error = \n%s\n--->", notif->InitializationErrorString().c_str()); } else emit notification(methodId, notif); delete notif; notif = NULL; parsing = false; goto _exit; break; } default: qWarning("%s: unexpected type %d", __PRETTY_FUNCTION__, type); qWarning("aborting %s:%u", qPrintable(mServerHost), mServerPort); // emit a error for user reporting; we are not a SSL socket, // so we overload a SSL error to indicate abort emit error(QAbstractSocket::SslInvalidUserDataError); mpSocket->abort(); // reset inStream - there's no way to do that currently, so // we delete-create delete inStream; inStream = new google::protobuf::io::CopyingInputStreamAdaptor( new PbQtInputStream(mpSocket)); inStream->SetOwnsCopyingStream(true); goto _exit2; } done->Run(); pendingMethodId = -1; this->method = NULL; controller = NULL; response = NULL; isPending = false; parsing = false; if (pendingCallList.size()) { RpcCall call = pendingCallList.takeFirst(); qDebug("RpcChannel: executing queued method <----\n" "method = %d:%s\n" "req = %s\n%s\n---->", call.method->index(), call.method->name().c_str(), call.method->input_type()->name().c_str(), call.request->DebugString().c_str()); CallMethod(call.method, call.controller, call.request, call.response, call.done); } goto _exit; _error_exit: inStream->Skip(len); _error_exit2: parsing = false; qDebug("client(%s) discarding received msg <----", __FUNCTION__); qDebug("method = %d\n---->", methodId); _exit: // If we have some data still available continue reading/parsing if (inStream->Next((const void**)&msg, &msgLen)) { if (msgLen >= PB_HDR_SIZE) { inStream->BackUp(msgLen); qDebug("===>> MORE DATA PENDING (%d bytes)... CONTINUE", msgLen); goto _top; } } if (mpSocket->bytesAvailable()) qDebug("%s (exit): bytesAvail = %lld", __FUNCTION__, mpSocket->bytesAvailable()); _exit2: return; } void PbRpcChannel::on_mpSocket_stateChanged( QAbstractSocket::SocketState socketState) { qDebug("In %s", __FUNCTION__); emit stateChanged(socketState); } void PbRpcChannel::on_mpSocket_connected() { qDebug("In %s", __FUNCTION__); emit connected(); } void PbRpcChannel::on_mpSocket_disconnected() { qDebug("In %s", __FUNCTION__); pendingMethodId = -1; method = NULL; controller = NULL; response = NULL; isPending = false; parsing = false; pendingCallList.clear(); emit disconnected(); } void PbRpcChannel::on_mpSocket_error(QAbstractSocket::SocketError socketError) { qDebug("In %s", __FUNCTION__); emit error(socketError); } ostinato-1.3.0/rpc/pbrpcchannel.h000066400000000000000000000101631451413623100167500ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _PB_RPC_CHANNEL_H #define _PB_RPC_CHANNEL_H #include #include #include #include #include #include #include #include "pbrpccommon.h" #include "pbrpccontroller.h" class PbRpcChannel : public QObject, public ::google::protobuf::RpcChannel { Q_OBJECT // If isPending is TRUE, then controller, done, response // and pendingMethodId correspond to the last method called by // the service stub bool isPending; int pendingMethodId; // controller, done, response are set to the corresponding values // passed by the stub to CallMethod(). They are reset to NULL when // we get a response back from the server in on_mpSocket_readyRead() // after calling done->Run(). /*! \todo (MED) : change controller, done and response to references instead of pointers? */ const ::google::protobuf::MethodDescriptor *method; ::google::protobuf::RpcController *controller; ::google::protobuf::Closure *done; ::google::protobuf::Message *response; typedef struct _RpcCall { const ::google::protobuf::MethodDescriptor *method; ::google::protobuf::RpcController *controller; const ::google::protobuf::Message *request; ::google::protobuf::Message *response; ::google::protobuf::Closure *done; } RpcCall; QList pendingCallList; const ::google::protobuf::Message ¬ifPrototype; ::google::protobuf::Message *notif; QString mServerHost; quint16 mServerPort; QTcpSocket *mpSocket; ::google::protobuf::io::CopyingInputStreamAdaptor *inStream; ::google::protobuf::io::CopyingOutputStreamAdaptor *outStream; uchar sendBuffer_[4096]; // receive RPC related vars bool parsing{false}; QByteArray buffer; // used for response type messages QByteArray errorBuf; // used for error type messages quint32 cumLen{0}; quint16 type; quint16 methodId; quint32 len; public: PbRpcChannel(QString serverName, quint16 port, const ::google::protobuf::Message ¬ifProto); ~PbRpcChannel(); void establish(); void establish(QString serverName, quint16 port); void tearDown(); const QString serverName() const { return mpSocket->peerName(); } quint16 serverPort() const { return mServerPort; } QAbstractSocket::SocketState state() const { return mpSocket->state(); } void CallMethod(const ::google::protobuf::MethodDescriptor *method, ::google::protobuf::RpcController *controller, const ::google::protobuf::Message *req, ::google::protobuf::Message *response, ::google::protobuf::Closure* done); signals: void connected(); void disconnected(); void error(QAbstractSocket::SocketError socketError); void stateChanged(QAbstractSocket::SocketState socketState); void notification(int notifType, ::google::protobuf::Message *notifData); private slots: void on_mpSocket_connected(); void on_mpSocket_disconnected(); void on_mpSocket_stateChanged(QAbstractSocket::SocketState socketState); void on_mpSocket_error(QAbstractSocket::SocketError socketError); void on_mpSocket_readyRead(); }; #endif ostinato-1.3.0/rpc/pbrpccommon.h000066400000000000000000000022731451413623100166330ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _PB_RPC_COMMON_H #define _PB_RPC_COMMON_H // Print a HexDump #define BUFDUMP(ptr, len) qDebug("%s", \ qPrintable(QString(QByteArray((char*)(ptr), (len)).toHex()))); /* ** RPC Header (8) ** - MSG_TYPE (2) ** - METHOD_ID/NOTIF_TYPE (2) ** - LEN (4) [not including this header] */ #define PB_HDR_SIZE 8 #define PB_MSG_TYPE_REQUEST 1 #define PB_MSG_TYPE_RESPONSE 2 #define PB_MSG_TYPE_BINBLOB 3 #define PB_MSG_TYPE_ERROR 4 #define PB_MSG_TYPE_NOTIFY 5 #endif ostinato-1.3.0/rpc/pbrpccontroller.h000066400000000000000000000053611451413623100175270ustar00rootroot00000000000000/* Copyright (C) 2010, 2014 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _PB_RPC_CONTROLLER_H #define _PB_RPC_CONTROLLER_H #include #include class QIODevice; /*! PbRpcController takes ownership of the 'request' and 'response' messages and will delete them when it itself is destroyed */ class PbRpcController : public ::google::protobuf::RpcController { public: PbRpcController(::google::protobuf::Message *request, ::google::protobuf::Message *response) { request_ = request; response_ = response; Reset(); } ~PbRpcController() { delete request_; delete response_; } ::google::protobuf::Message* request() { return request_; } ::google::protobuf::Message* response() { return response_; } // Client Side Methods void Reset() { failed = false; disconnect = false; notif = true; blob = NULL; errStr = ""; } bool Failed() const { return failed; } void StartCancel() { /*! \todo (MED) */} std::string ErrorText() const { return errStr.toStdString(); } // Server Side Methods void SetFailed(const QString &reason) { failed = true; errStr = reason; qWarning("%s", qPrintable(errStr)); } void SetFailed(const std::string &reason) { SetFailed(QString::fromStdString(reason)); } QString ErrorString() const { return errStr; } bool IsCanceled() const { return false; }; void NotifyOnCancel(::google::protobuf::Closure* /* callback */) { /*! \todo (MED) */ } void TriggerDisconnect() { disconnect = true; } bool Disconnect() const { return disconnect; } void EnableNotif(bool enabled) { notif = enabled; } bool NotifEnabled() { return notif; } // srivatsp added QIODevice* binaryBlob() { return blob; }; void setBinaryBlob(QIODevice *binaryBlob) { blob = binaryBlob; }; private: bool failed; bool disconnect; bool notif; QIODevice *blob; QString errStr; ::google::protobuf::Message *request_; ::google::protobuf::Message *response_; }; #endif ostinato-1.3.0/rpc/rpcconn.cpp000066400000000000000000000316101451413623100163060ustar00rootroot00000000000000/* Copyright (C) 2010, 2014 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #include "rpcconn.h" #include "pbqtio.h" #include "pbrpccommon.h" #include "pbrpccontroller.h" #include #include #include #include #include #include #include #include #include #include #include #include #include static QThreadStorage connId; RpcConnection::RpcConnection(qintptr socketDescriptor, ::google::protobuf::Service *service) : socketDescriptor(socketDescriptor), service(service) { inStream = NULL; outStream = NULL; isPending = false; pendingMethodId = -1; // don't care as long as isPending is false isCompatCheckDone = false; isNotifEnabled = true; } RpcConnection::~RpcConnection() { qDebug("destroying connection to %s: %d", qPrintable(clientSock->peerAddress().toString()), clientSock->peerPort()); // If still connected, disconnect if (clientSock->state() != QAbstractSocket::UnconnectedState) { clientSock->disconnectFromHost(); clientSock->waitForDisconnected(); } delete inStream; delete outStream; delete clientSock; } void RpcConnection::start() { QString id = QString("[%1:%2] "); clientSock = new QTcpSocket; if (!clientSock->setSocketDescriptor(socketDescriptor)) { qWarning("Unable to initialize TCP socket for incoming connection"); return; } qDebug("clientSock Thread = %p", clientSock->thread()); qsrand(QDateTime::currentDateTime().toTime_t()); connId.setLocalData(new QString(id.arg(clientSock->peerAddress().toString()) .arg(clientSock->peerPort()))); qDebug("accepting new connection from %s: %d", qPrintable(clientSock->peerAddress().toString()), clientSock->peerPort()); inStream = new google::protobuf::io::CopyingInputStreamAdaptor( new PbQtInputStream(clientSock)); inStream->SetOwnsCopyingStream(true); outStream = new google::protobuf::io::CopyingOutputStreamAdaptor( new PbQtOutputStream(clientSock)); outStream->SetOwnsCopyingStream(true); connect(clientSock, SIGNAL(readyRead()), this, SLOT(on_clientSock_dataAvail())); connect(clientSock, SIGNAL(disconnected()), this, SLOT(on_clientSock_disconnected())); connect(clientSock, SIGNAL(error(QAbstractSocket::SocketError)), this, SLOT(on_clientSock_error(QAbstractSocket::SocketError))); } void RpcConnection::writeHeader(char* header, quint16 type, quint16 method, quint32 length) { *((quint16*)(header+0)) = qToBigEndian(type); *((quint16*)(header+2)) = qToBigEndian(method); *((quint32*)(header+4)) = qToBigEndian(length); } void RpcConnection::sendRpcReply( const ::google::protobuf::MethodDescriptor *method, PbRpcController *controller) { google::protobuf::Message *response = controller->response(); QIODevice *blob; char msgBuf[PB_HDR_SIZE]; char* const msg = &msgBuf[0]; int len; if (controller->Failed()) { QByteArray err = controller->ErrorString().toUtf8(); qWarning("rpc failed (%s)", qPrintable(controller->ErrorString())); len = err.size(); writeHeader(msg, PB_MSG_TYPE_ERROR, pendingMethodId, len); clientSock->write(msg, PB_HDR_SIZE); clientSock->write(err.constData(), len); goto _exit; } blob = controller->binaryBlob(); if (blob) { len = blob->size(); qDebug("is binary blob of len %d", len); writeHeader(msg, PB_MSG_TYPE_BINBLOB, pendingMethodId, len); clientSock->write(msg, PB_HDR_SIZE); blob->seek(0); while (!blob->atEnd()) { int l; len = blob->read(msg, sizeof(msgBuf)); l = clientSock->write(msg, len); Q_ASSERT(l == len); Q_UNUSED(l); } goto _exit; } if (!response->IsInitialized()) { qWarning("response missing required fields!! <----"); qDebug("response = \n%s" "missing = \n%s---->", response->DebugString().c_str(), response->InitializationErrorString().c_str()); qFatal("exiting"); goto _exit; } len = response->ByteSize(); writeHeader(msg, PB_MSG_TYPE_RESPONSE, pendingMethodId, len); // Avoid printing stats since it happens once every couple of seconds if (pendingMethodId != 13) { qDebug("Server(%s): sending %d bytes to client <----", __FUNCTION__, len + PB_HDR_SIZE); BUFDUMP(msg, 8); qDebug("method = %d:%s\nresp = %s\n%s---->", pendingMethodId, method ? method->name().c_str() : "", method ? method->output_type()->name().c_str() : "", response->DebugString().c_str()); } clientSock->write(msg, PB_HDR_SIZE); response->SerializeToZeroCopyStream(outStream); outStream->Flush(); if (pendingMethodId == 15) { isCompatCheckDone = true; isNotifEnabled = controller->NotifEnabled(); } _exit: if (controller->Disconnect()) clientSock->disconnectFromHost(); delete controller; isPending = false; } void RpcConnection::sendNotification(int notifType, SharedProtobufMessage notifData) { char msgBuf[PB_HDR_SIZE]; char* const msg = &msgBuf[0]; int len; if (!isCompatCheckDone) return; if (!isNotifEnabled) return; if (!notifData->IsInitialized()) { qWarning("notification missing required fields!! <----"); qDebug("notif = \n%s" "missing = \n%s---->", notifData->DebugString().c_str(), notifData->InitializationErrorString().c_str()); qFatal("exiting"); return; } len = notifData->ByteSize(); writeHeader(msg, PB_MSG_TYPE_NOTIFY, notifType, len); qDebug("Server(%s): sending %d bytes to client <----", __FUNCTION__, len + PB_HDR_SIZE); BUFDUMP(msg, 8); qDebug("notif = %d\ndata = \n%s---->", notifType, notifData->DebugString().c_str()); clientSock->write(msg, PB_HDR_SIZE); notifData->SerializeToZeroCopyStream(outStream); outStream->Flush(); } void RpcConnection::on_clientSock_disconnected() { qDebug("connection closed from %s: %d", qPrintable(clientSock->peerAddress().toString()), clientSock->peerPort()); deleteLater(); emit closed(); } void RpcConnection::on_clientSock_error(QAbstractSocket::SocketError socketError) { qDebug("%s (%d)", qPrintable(clientSock->errorString()), socketError); } void RpcConnection::on_clientSock_dataAvail() { uchar msg[PB_HDR_SIZE]; int msgLen; quint16 type, method; quint32 len; const ::google::protobuf::MethodDescriptor *methodDesc; ::google::protobuf::Message *req, *resp; PbRpcController *controller; QString error; bool disconnect = false; // Do we have enough bytes for a msg header? // If yes, peek into the header and get msg length if (clientSock->bytesAvailable() < PB_HDR_SIZE) return; msgLen = clientSock->peek((char*)msg, PB_HDR_SIZE); if (msgLen != PB_HDR_SIZE) { qWarning("asked to peek %d bytes, was given only %d bytes", PB_HDR_SIZE, msgLen); return; } len = qFromBigEndian(&msg[4]); // Is the full msg available to read? If not, wait till such time if (clientSock->bytesAvailable() < (PB_HDR_SIZE+len)) return; msgLen = clientSock->read((char*)msg, PB_HDR_SIZE); Q_ASSERT(msgLen == PB_HDR_SIZE); type = qFromBigEndian(&msg[0]); method = qFromBigEndian(&msg[2]); len = qFromBigEndian(&msg[4]); //qDebug("type = %d, method = %d, len = %d", type, method, len); if (type != PB_MSG_TYPE_REQUEST) { qDebug("server(%s): unexpected msg type %d (expected %d)", __FUNCTION__, type, PB_MSG_TYPE_REQUEST); error = QString("unexpected msg type %1; expected %2") .arg(type).arg(PB_MSG_TYPE_REQUEST); goto _error_exit; } // If RPC is not checkVersion, ensure compat check is already done if (!isCompatCheckDone && method != 15) { qDebug("server(%s): version compatibility check pending", __FUNCTION__); error = "version compatibility check pending"; disconnect = true; goto _error_exit; } if (method >= service->GetDescriptor()->method_count()) { qDebug("server(%s): invalid method id %d", __FUNCTION__, method); error = QString("invalid RPC method %1").arg(method); goto _error_exit; } methodDesc = service->GetDescriptor()->method(method); if (!methodDesc) { qDebug("server(%s): invalid method id %d", __FUNCTION__, method); error = QString("invalid RPC method %1").arg(method); goto _error_exit; } if (isPending) { qDebug("server(%s): rpc pending, try again", __FUNCTION__); error = QString("RPC %1() is pending; only one RPC allowed at a time; " "try again!").arg(QString::fromStdString( service->GetDescriptor()->method( pendingMethodId)->name())); goto _error_exit; } pendingMethodId = method; isPending = true; req = service->GetRequestPrototype(methodDesc).New(); resp = service->GetResponsePrototype(methodDesc).New(); if (len) { bool ok = req->ParseFromBoundedZeroCopyStream(inStream, len); if (!ok) qWarning("ParseFromBoundedZeroCopyStream fail " "for method %d:%s and len %d", method, methodDesc->name().c_str(),len); } if (!req->IsInitialized()) { qWarning("Missing required fields in request <----"); qDebug("method = %d:%s\n" "req = %s\n%s" "missing = \n%s----->", method, methodDesc->name().c_str(), methodDesc->input_type()->name().c_str(), req->DebugString().c_str(), req->InitializationErrorString().c_str()); error = QString("RPC %1() missing required fields in request - %2") .arg(QString::fromStdString( service->GetDescriptor()->method( pendingMethodId)->name()), QString(req->InitializationErrorString().c_str())); delete req; delete resp; goto _error_exit2; } if (method != 13) { qDebug("Server(%s): successfully received/parsed msg <----", __FUNCTION__); qDebug("method = %d:%s\n" "req = %s\n%s---->", method, methodDesc->name().c_str(), methodDesc->input_type()->name().c_str(), req->DebugString().c_str()); } controller = new PbRpcController(req, resp); //qDebug("before service->callmethod()"); service->CallMethod(methodDesc, controller, req, resp, google::protobuf::NewCallback(this, &RpcConnection::sendRpcReply, methodDesc, controller)); return; _error_exit: inStream->Skip(len); _error_exit2: qDebug("server(%s): return error %s for msg from client", __FUNCTION__, qPrintable(error)); pendingMethodId = method; isPending = true; controller = new PbRpcController(NULL, NULL); controller->SetFailed(error); if (disconnect) controller->TriggerDisconnect(); sendRpcReply(methodDesc, controller); return; } void RpcConnection::connIdMsgHandler(QtMsgType /*type*/, const QMessageLogContext &/*context*/, const QString &msg) { if (connId.hasLocalData()) { QString newMsg(*connId.localData()); newMsg.append(msg); newMsg.replace(QChar('\n'), QString("\n").append(*connId.localData())); fprintf(stderr, "%s\n", qPrintable(newMsg)); fflush(stderr); return; } fprintf(stderr, "%s\n", qPrintable(msg)); fflush(stderr); } ostinato-1.3.0/rpc/rpcconn.h000066400000000000000000000044651451413623100157630ustar00rootroot00000000000000/* Copyright (C) 2010, 2014 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _RPC_CONNECTION_H #define _RPC_CONNECTION_H #include "sharedprotobufmessage.h" #include // forward declarations class PbRpcController; class QTcpSocket; namespace google { namespace protobuf { class Service; namespace io { class CopyingInputStreamAdaptor; class CopyingOutputStreamAdaptor; } class Message; class MethodDescriptor; } } class RpcConnection : public QObject { Q_OBJECT public: RpcConnection(qintptr socketDescriptor, ::google::protobuf::Service *service); virtual ~RpcConnection(); static void connIdMsgHandler(QtMsgType type, const QMessageLogContext &context, const QString &msg); private: void writeHeader(char* header, quint16 type, quint16 method, quint32 length); void sendRpcReply(const ::google::protobuf::MethodDescriptor *method, PbRpcController *controller); signals: void closed(); public slots: void sendNotification(int notifType, SharedProtobufMessage notifData); private slots: void start(); void on_clientSock_dataAvail(); void on_clientSock_error(QAbstractSocket::SocketError socketError); void on_clientSock_disconnected(); private: qintptr socketDescriptor; QTcpSocket *clientSock; ::google::protobuf::Service *service; ::google::protobuf::io::CopyingInputStreamAdaptor *inStream; ::google::protobuf::io::CopyingOutputStreamAdaptor *outStream; bool isPending; int pendingMethodId; bool isCompatCheckDone; bool isNotifEnabled; }; #endif ostinato-1.3.0/rpc/rpcserver.cpp000066400000000000000000000046401451413623100166620ustar00rootroot00000000000000/* Copyright (C) 2010, 2014 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #include "rpcserver.h" #include "rpcconn.h" #include RpcServer::RpcServer(bool perConnLogs) { service = NULL; if (perConnLogs) qInstallMessageHandler(RpcConnection::connIdMsgHandler); } RpcServer::~RpcServer() { close(); emit closed(); } bool RpcServer::registerService(::google::protobuf::Service *service, QHostAddress address, quint16 tcpPortNum) { this->service = service; if (!listen(address, tcpPortNum)) { qDebug("Unable to start the server on <%s>: %s", qPrintable(address.toString()), qPrintable(errorString())); return false; } qDebug("The server is running on %s: %d", qPrintable(serverAddress().toString()), serverPort()); fprintf(stderr, "Ready to send traffic! Listening on %s:%d ...\n", qPrintable(serverAddress().toString()), serverPort()); fflush(stderr); return true; } void RpcServer::incomingConnection(qintptr socketDescriptor) { QThread *thread = new QThread; RpcConnection *conn = new RpcConnection(socketDescriptor, service); thread->setObjectName("RPC"); conn->moveToThread(thread); connect(thread, SIGNAL(started()), conn, SLOT(start())); // NOTE: conn "self-destructs" after emitting closed // use 'closed' to stop execution of the thread connect(conn, SIGNAL(closed()), thread, SLOT(quit())); // setup thread to "self-destruct" when it is done connect(thread, SIGNAL(finished()), thread, SLOT(deleteLater())); connect(this, SIGNAL(notifyClients(int, SharedProtobufMessage)), conn, SLOT(sendNotification(int, SharedProtobufMessage))); connect(this, SIGNAL(closed()), thread, SLOT(quit())); thread->start(); } ostinato-1.3.0/rpc/rpcserver.h000066400000000000000000000026011451413623100163220ustar00rootroot00000000000000/* Copyright (C) 2010, 2014 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _RPC_SERVER_H #define _RPC_SERVER_H #include "sharedprotobufmessage.h" #include // forward declaration namespace google { namespace protobuf { class Service; class Message; } } class RpcServer : public QTcpServer { Q_OBJECT public: RpcServer(bool perConnLogs); //! \todo (LOW) use 'parent' param virtual ~RpcServer(); bool registerService(::google::protobuf::Service *service, QHostAddress address, quint16 tcpPortNum); signals: void closed(); void notifyClients(int notifType, SharedProtobufMessage notifData); protected: void incomingConnection(qintptr socketDescriptor); private: ::google::protobuf::Service *service; }; #endif ostinato-1.3.0/rpc/sharedprotobufmessage.h000066400000000000000000000041401451413623100207030ustar00rootroot00000000000000/* Copyright (C) 2015 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _SHARED_PROTOBUF_MESSAGE_H #define _SHARED_PROTOBUF_MESSAGE_H #include #include // TODO: Use QSharedPointer instead once the minimum Qt version becomes >= 4.5 template class SharedPointer { public: SharedPointer(T *ptr = 0) : ptr_(ptr) { mutex_ = new QMutex(); refCnt_ = new unsigned int; *refCnt_ = 1; qDebug("sharedptr %p(constr) refcnt %p(%u)", this, refCnt_, *refCnt_); } ~SharedPointer() { mutex_->lock(); (*refCnt_)--; if (*refCnt_ == 0) { delete ptr_; delete refCnt_; mutex_->unlock(); delete mutex_; qDebug("sharedptr %p destroyed", this); return; } qDebug("sharedptr %p(destr) refcnt %p(%u)", this, refCnt_, *refCnt_); mutex_->unlock(); } SharedPointer(const SharedPointer &other) { ptr_ = other.ptr_; refCnt_ = other.refCnt_; mutex_ = other.mutex_; mutex_->lock(); (*refCnt_)++; qDebug("sharedptr %p(copy) refcnt %p(%u)", this, refCnt_,*refCnt_); mutex_->unlock(); } T* operator->() const { return ptr_; } protected: T *ptr_; // use uint+mutex to simulate a QAtomicInt unsigned int *refCnt_; QMutex *mutex_; }; typedef class SharedPointer< ::google::protobuf::Message> SharedProtobufMessage; #endif ostinato-1.3.0/server/000077500000000000000000000000001451413623100146615ustar00rootroot00000000000000ostinato-1.3.0/server/abstractport.cpp000066400000000000000000001022751451413623100201040ustar00rootroot00000000000000/* Copyright (C) 2010-2012 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #include "abstractport.h" #include "../common/abstractprotocol.h" #include "../common/framevalueattrib.h" #include "../common/packet.h" #include "../common/streambase.h" #include "devicemanager.h" #include "interfaceinfo.h" #include "packetbuffer.h" #include #include #include #include AbstractPort::AbstractPort(int id, const char *device) { isUsable_ = true; data_.mutable_port_id()->set_id(id); data_.set_name(device); //! \todo (LOW) admin enable/disable of port data_.set_is_enabled(true); data_.set_is_exclusive_control(false); isSendQueueDirty_ = false; rateAccuracy_ = kHighAccuracy; linkState_ = OstProto::LinkStateUnknown; minPacketSetSize_ = 1; deviceManager_ = new DeviceManager(this); interfaceInfo_ = NULL; maxStatsValue_ = ULLONG_MAX; // assume 64-bit stats memset((void*) &stats_, 0, sizeof(stats_)); resetStats(); streamTiming_ = StreamTiming::instance(); } AbstractPort::~AbstractPort() { delete deviceManager_; delete interfaceInfo_; } void AbstractPort::init() { if (interfaceInfo_) { data_.set_speed(interfaceInfo_->speed); data_.set_mtu(interfaceInfo_->mtu); } if (deviceManager_) deviceManager_->createHostDevices(); } /*! Can we modify Port with these params? Should modify cause port dirty? */ bool AbstractPort::canModify(const OstProto::Port &port, bool *dirty) { bool allow = true; *dirty = false; if (port.has_transmit_mode() && (port.transmit_mode() != data_.transmit_mode())) { *dirty = true; allow = !isTransmitOn(); } if (port.has_is_tracking_stream_stats() && (port.is_tracking_stream_stats() != data_.is_tracking_stream_stats())) { *dirty = true; allow = !isTransmitOn(); } if (*dirty) isSendQueueDirty_ = true; return allow; } bool AbstractPort::modify(const OstProto::Port &port) { bool ret = true; //! \todo Use reflection to find out which fields are set if (port.has_user_description()) { data_.set_user_description(port.user_description()); } if (port.has_is_exclusive_control()) { bool val = port.is_exclusive_control(); ret = setExclusiveControl(val); if (ret) data_.set_is_exclusive_control(val); } if (port.has_transmit_mode()) data_.set_transmit_mode(port.transmit_mode()); if (port.has_is_tracking_stream_stats()) ret |= setTrackStreamStats(port.is_tracking_stream_stats()); if (port.has_user_name()) { data_.set_user_name(port.user_name()); } return ret; } DeviceManager* AbstractPort::deviceManager() { return deviceManager_; } StreamBase* AbstractPort::streamAtIndex(int index) { Q_ASSERT(index < streamList_.size()); return streamList_.at(index); } StreamBase* AbstractPort::stream(int streamId) { for (int i = 0; i < streamList_.size(); i++) { if ((uint)streamId == streamList_.at(i)->id()) return streamList_.at(i); } return NULL; } bool AbstractPort::addStream(StreamBase *stream) { streamList_.append(stream); isSendQueueDirty_ = true; return true; } bool AbstractPort::deleteStream(int streamId) { for (int i = 0; i < streamList_.size(); i++) { StreamBase *stream; if ((uint)streamId == streamList_.at(i)->id()) { stream = streamList_.takeAt(i); delete stream; isSendQueueDirty_ = true; return true; } } return false; } void AbstractPort::addNote(QString note) { QString notes = QString::fromStdString(data_.notes()); note.prepend("
  • "); note.append("
  • "); if (notes.isEmpty()) notes="Limitation(s)
      "; else notes.remove("
    "); notes.append(note); notes.append(""); data_.set_notes(notes.toStdString()); } bool AbstractPort::setTrackStreamStats(bool enable) { // XXX: This function is called by modify() in context of the RPC // thread (1 thread per connected client), but the StreamTiming // singleton resides in the main thread and its' start/stop methods // start/stop timers which cannot be done across Qt Threads. Hence // this slightly hacky way of invoking those methods QMetaObject::invokeMethod(streamTiming_, enable ? "start" : "stop", Qt::QueuedConnection, Q_ARG(uint, id())); data_.set_is_tracking_stream_stats(enable); return true; } AbstractPort::Accuracy AbstractPort::rateAccuracy() { return rateAccuracy_; } bool AbstractPort::setRateAccuracy(Accuracy accuracy) { rateAccuracy_ = accuracy; return true; } int AbstractPort::updatePacketList() { switch(data_.transmit_mode()) { case OstProto::kSequentialTransmit: return updatePacketListSequential(); break; case OstProto::kInterleavedTransmit: return updatePacketListInterleaved(); break; default: Q_ASSERT(false); // Unreachable!!! break; } return 0; } int AbstractPort::updatePacketListSequential() { quint64 duration = 0; // in nanosec quint64 totalPkts = 0; QList ttagMarkers; uint ttagRepeatInterval; FrameValueAttrib packetListAttrib; long sec = 0; long nsec = 0; qDebug("In %s", __FUNCTION__); // First sort the streams by ordinalValue std::sort(streamList_.begin(), streamList_.end(), StreamBase::StreamLessThan); clearPacketList(); for (int i = 0; i < streamList_.size(); i++) { if (streamList_[i]->isEnabled()) { int len = 0; ulong n, x, y; ulong burstSize; double ibg = 0; quint64 ibg1 = 0, ibg2 = 0; quint64 nb1 = 0, nb2 = 0; double ipg = 0; quint64 ipg1 = 0, ipg2 = 0; quint64 npx1 = 0, npx2 = 0; quint64 npy1 = 0, npy2 = 0; quint64 loopDelay; ulong frameVariableCount = streamList_[i]->frameVariableCount(); bool hasTtag = streamList_[i]->hasProtocol( OstProto::Protocol::kSignFieldNumber); // We derive n, x, y such that // n * x + y = total number of packets to be sent switch (streamList_[i]->sendUnit()) { case StreamBase::e_su_bursts: burstSize = streamList_[i]->burstSize(); x = AbstractProtocol::lcm(frameVariableCount, burstSize); n = ulong(burstSize * streamList_[i]->numBursts()) / x; y = ulong(burstSize * streamList_[i]->numBursts()) % x; if (streamList_[i]->burstRate() > 0) { ibg = 1e9/double(streamList_[i]->burstRate()); ibg1 = quint64(ceil(ibg)); ibg2 = quint64(floor(ibg)); nb1 = quint64((ibg - double(ibg2)) * double(x)); nb2 = x - nb1; } loopDelay = ibg2; break; case StreamBase::e_su_packets: x = frameVariableCount; n = 2; while (x < minPacketSetSize_) x = frameVariableCount*n++; n = streamList_[i]->numPackets() / x; y = streamList_[i]->numPackets() % x; burstSize = x + y; if (streamList_[i]->packetRate() > 0) { ipg = 1e9/double(streamList_[i]->packetRate()); ipg1 = quint64(ceil(ipg)); ipg2 = quint64(floor(ipg)); npx1 = quint64((ipg - double(ipg2)) * double(x)); npx2 = x - npx1; npy1 = quint64((ipg - double(ipg2)) * double(y)); npy2 = y - npy1; } loopDelay = ipg2; break; default: qWarning("Unhandled stream control unit %d", streamList_[i]->sendUnit()); continue; } qDebug("\nframeVariableCount = %lu", frameVariableCount); qDebug("n = %lu, x = %lu, y = %lu, burstSize = %lu", n, x, y, burstSize); qDebug("ibg = %g", ibg); qDebug("ibg1 = %llu", ibg1); qDebug("nb1 = %llu", nb1); qDebug("ibg2 = %llu", ibg2); qDebug("nb2 = %llu\n", nb2); qDebug("ipg = %g", ipg); qDebug("ipg1 = %llu", ipg1); qDebug("npx1 = %llu", npx1); qDebug("npy1 = %llu", npy1); qDebug("ipg2 = %llu", ipg2); qDebug("npx2 = %llu", npx2); qDebug("npy2 = %llu\n", npy2); if (n >= 1) { loopNextPacketSet(x, n, 0, loopDelay); qDebug("PacketSet: n = %lu, x = %lu, delay = %llu ns", n, x, loopDelay); } else if (n == 0) x = 0; quint64 pktCount = n*x + y; for (uint j = 0; j < (x+y); j++) { if (j == 0 || frameVariableCount > 1) { FrameValueAttrib attrib; len = streamList_[i]->frameValue( pktBuf_, sizeof(pktBuf_), j, &attrib); packetListAttrib += attrib; } if (len <= 0) continue; // Create a packet set for 'y' with repeat = 1 if (j == x) { loopNextPacketSet(y, 1, 0, loopDelay); qDebug("PacketSet: n = 1, y = %lu, delay = %llu", y, loopDelay); } qDebug("q(%d, %d) sec = %lu nsec = %lu", i, j, sec, nsec); if (!appendToPacketList(sec, nsec, pktBuf_, len)) { clearPacketList(); // don't leave it half baked/inconsitent packetListAttrib.errorFlags |= FrameValueAttrib::OutOfMemoryError; goto _out_of_memory; } if ((j > 0) && (((j+1) % burstSize) == 0)) { nsec += (j < nb1) ? ibg1 : ibg2; while (nsec >= long(1e9)) { sec++; nsec -= long(1e9); } } else { if (j < x) nsec += (j < npx1) ? ipg1 : ipg2; else nsec += ((j-x) < npy1) ? ipg1 : ipg2; while (nsec >= long(1e9)) { sec++; nsec -= long(1e9); } } } // loopDelay == 0 implies 0 pps i.e. top speed // For ttag calc/config below we need loopDelay to be non-zero, // so we re-calc based on max line rate (speed). If we don't // have the actual port speed, we assume 1000 Mbps if (loopDelay == 0) { double maxSpeed = data_.speed() ? data_.speed(): 1000; double maxPktRate = (maxSpeed*1e6) /(8*(streamList_[i]->frameLenAvg() + Packet::kEthOverhead)); loopDelay = 1e9/maxPktRate; // in nanosec } // Add a Ttag marker after every kTtagTimeInterval_ worth of pkts if (hasTtag) { uint ttagPktInterval = kTtagTimeInterval_*1e9/loopDelay; for (uint k = 0; k < pktCount; k += ttagPktInterval) ttagMarkers.append(totalPkts + k); } totalPkts += pktCount; duration += pktCount*loopDelay; // in nanosecs switch(streamList_[i]->nextWhat()) { case StreamBase::e_nw_stop: goto _stop_no_more_pkts; case StreamBase::e_nw_goto_id: /*! \todo (MED): define and use streamList_[i].d.control().goto_stream_id(); */ /*! \todo (MED): assumes goto Id is less than current!!!! To support goto to any id, do if goto_id > curr_id then i = goto_id; goto restart; else returnToQIdx = 0; */ // XXX: no list loop delay required since we don't create // any implicit packet sets now setPacketListLoopMode(true, 0, 0); qDebug("Seq mode list loop true with 0 delay"); goto _stop_no_more_pkts; case StreamBase::e_nw_goto_next: break; default: qFatal("---------- %s: Unhandled case (%d) -----------", __FUNCTION__, streamList_[i]->nextWhat() ); break; } } // if (stream is enabled) } // for (numStreams) _stop_no_more_pkts: // See comments in updatePacketListInterleaved() for calc explanation ttagRepeatInterval = ttagMarkers.isEmpty() ? 0 : qMax(uint(kTtagTimeInterval_*1e9/(duration)), 1U) * totalPkts; if (!setPacketListTtagMarkers(ttagMarkers, ttagRepeatInterval)) { clearPacketList(); // don't leave it half baked/inconsitent packetListAttrib.errorFlags |= FrameValueAttrib::OutOfMemoryError; } _out_of_memory: isSendQueueDirty_ = false; qDebug("PacketListAttrib = %x", static_cast(packetListAttrib.errorFlags)); return static_cast(packetListAttrib.errorFlags); } int AbstractPort::updatePacketListInterleaved() { FrameValueAttrib packetListAttrib; int numStreams = 0; quint64 minGap = ULLONG_MAX; quint64 duration = quint64(1e3); // 1000ns (1us) // TODO: convert the below to a QVector of struct aggregating all list vars QList streamId; QList ibg1, ibg2; QList nb1, nb2; QList ipg1, ipg2; QList np1, np2; QList schedSec, schedNsec; QList pktCount, burstCount; QList burstSize; QList isVariable; QList hasTtag; QList pktBuf; QList pktLen; int activeStreamCount = 0; qDebug("In %s", __FUNCTION__); clearPacketList(); for (int i = 0; i < streamList_.size(); i++) { if (streamList_[i]->isEnabled()) activeStreamCount++; } if (activeStreamCount == 0) { isSendQueueDirty_ = false; return 0; } // First sort the streams by ordinalValue std::sort(streamList_.begin(), streamList_.end(), StreamBase::StreamLessThan); // FIXME: we are calculating n[bp][12], i[bp]g[12] for a duration of 1sec; // this was fine when the actual packet list duration was also 1sec. But // in the current code (post Turbo changes), the latter can be different! for (int i = 0; i < streamList_.size(); i++) { if (!streamList_[i]->isEnabled()) continue; streamId.append(i); double numBursts = 0; double numPackets = 0; quint64 _burstSize = 0; double ibg = 0; quint64 _ibg1 = 0, _ibg2 = 0; quint64 _nb1 = 0, _nb2 = 0; double ipg = 0; quint64 _ipg1 = 0, _ipg2 = 0; quint64 _np1 = 0, _np2 = 0; switch (streamList_[i]->sendUnit()) { case StreamBase::e_su_bursts: numBursts = streamList_[i]->burstRate(); _burstSize = streamList_[i]->burstSize(); if (streamList_[i]->burstRate() > 0) { ibg = 1e9/double(streamList_[i]->burstRate()); _ibg1 = quint64(ceil(ibg)); _ibg2 = quint64(floor(ibg)); _nb1 = quint64((ibg - double(_ibg2)) * double(numBursts)); _nb2 = quint64(numBursts) - _nb1; } break; case StreamBase::e_su_packets: numPackets = streamList_[i]->packetRate(); _burstSize = 1; if (streamList_[i]->packetRate() > 0) { ipg = 1e9/double(streamList_[i]->packetRate()); _ipg1 = llrint(ceil(ipg)); _ipg2 = quint64(floor(ipg)); _np1 = quint64((ipg - double(_ipg2)) * double(numPackets)); _np2 = quint64(numPackets) - _np1; } break; default: qWarning("Unhandled stream control unit %d", streamList_[i]->sendUnit()); continue; } qDebug("numBursts = %g, numPackets = %g\n", numBursts, numPackets); qDebug("ibg = %g", ibg); qDebug("ibg1 = %llu", _ibg1); qDebug("nb1 = %llu", _nb1); qDebug("ibg2 = %llu", _ibg2); qDebug("nb2 = %llu\n", _nb2); qDebug("ipg = %g", ipg); qDebug("ipg1 = %llu", _ipg1); qDebug("np1 = %llu", _np1); qDebug("ipg2 = %llu", _ipg2); qDebug("np2 = %llu\n", _np2); if (_ibg2 && (_ibg2 < minGap)) minGap = _ibg2; if (_ibg1 && (_ibg1 > duration)) duration = _ibg1; ibg1.append(_ibg1); ibg2.append(_ibg2); nb1.append(_nb1); nb2.append(_nb1); burstSize.append(_burstSize); if (_ipg2 && (_ipg2 < minGap)) minGap = _ipg2; if (_np1) { if (_ipg1 && (_ipg1 > duration)) duration = _ipg1; } else { if (_ipg2 && (_ipg2 > duration)) duration = _ipg2; } ipg1.append(_ipg1); ipg2.append(_ipg2); np1.append(_np1); np2.append(_np1); schedSec.append(0); schedNsec.append(0); pktCount.append(0); burstCount.append(0); if (streamList_[i]->isFrameVariable()) { isVariable.append(true); pktBuf.append(QByteArray()); pktLen.append(0); } else { FrameValueAttrib attrib; isVariable.append(false); pktBuf.append(QByteArray()); pktBuf.last().resize(kMaxPktSize); pktLen.append(streamList_[i]->frameValue( (uchar*)pktBuf.last().data(), pktBuf.last().size(), 0, &attrib)); packetListAttrib += attrib; } hasTtag.append(streamList_[i]->hasProtocol( OstProto::Protocol::kSignFieldNumber)); numStreams++; } // for i // handle burst/packet rate = 0 // i.e. send all streams "simultaneously" as fast as possible // as a result all streams will be at the same rate e.g. for 2 streams, // it would 50% each; for 3 streams - all at 33.3% and so on // FIXME: Should we calc minGap based on max line rate and avg pkt size? if (minGap == ULLONG_MAX) { minGap = 1; duration = 1; } qDebug("minGap = %llu", minGap); qDebug("duration = %llu", duration); if (duration < minGap*100) { duration = minGap*100; qDebug("increase duration to %llu for better accuracy", duration); } uchar* buf; int len; const quint64 durSec = duration/ulong(1e9); const quint64 durNsec = duration % ulong(1e9); quint64 sec = 0; quint64 nsec = 0; quint64 lastPktTxSec = 0; quint64 lastPktTxNsec = 0; // Count total packets we are going to add, so that we can create // an explicit packet set first // TODO: Find less expensive way to do this counting quint64 totalPkts = 0; QVector ttagSchedSec(numStreams, 0); QVector ttagSchedNsec(numStreams, 0); QList ttagMarkers; uint ttagRepeatInterval; do { for (int i = 0; i < numStreams; i++) { // If a packet is not scheduled yet, look at the next stream if ((schedSec.at(i) > sec) || (schedNsec.at(i) > nsec)) continue; // Ttag marker every TtagTimeInterval for each stream if (hasTtag.at(i) && ((schedSec.at(i) > ttagSchedSec.at(i)) || ((schedSec.at(i) == ttagSchedSec.at(i)) && (schedNsec.at(i) >= ttagSchedNsec.at(i))))) { ttagMarkers.append(totalPkts); ttagSchedSec[i] = schedSec.at(i) + kTtagTimeInterval_; ttagSchedNsec[i] = schedNsec.at(i); } for (uint j = 0; j < burstSize[i]; j++) { pktCount[i]++; schedNsec[i] += (pktCount.at(i) < np1.at(i)) ? ipg1.at(i) : ipg2.at(i); while (schedNsec.at(i) >= 1e9) { schedSec[i]++; schedNsec[i] -= long(1e9); } lastPktTxSec = sec; lastPktTxNsec = nsec; totalPkts++; } burstCount[i]++; schedNsec[i] += (burstCount.at(i) < nb1.at(i)) ? ibg1.at(i) : ibg2.at(i); while (schedNsec.at(i) >= 1e9) { schedSec[i]++; schedNsec[i] -= long(1e9); } } nsec += minGap; while (nsec >= 1e9) { sec++; nsec -= long(1e9); } } while ((sec < durSec) || ((sec == durSec) && (nsec < durNsec))); qint64 delaySec = durSec - lastPktTxSec; qint64 delayNsec = durNsec - lastPktTxNsec; while (delayNsec < 0) { delayNsec += long(1e9); delaySec--; } // XXX: For interleaved mode, we ALWAYS have a single packet set with // one repeat loopNextPacketSet(totalPkts, 1, delaySec, delayNsec); qDebug("Interleaved single PacketSet of size %lld, duration %llu.%09llu " "repeat 1 and delay %lld.%09lld", totalPkts, durSec, durNsec, delaySec, delayNsec); // Reset working sched/counts before building the packet list sec = nsec = 0; for (int i = 0; i < numStreams; i++) { schedSec[i] = 0; schedNsec[i] = 0; pktCount[i] = 0; burstCount[i] = 0; } // Now build the packet list do { for (int i = 0; i < numStreams; i++) { // If a packet is not scheduled yet, look at the next stream if ((schedSec.at(i) > sec) || (schedNsec.at(i) > nsec)) continue; for (uint j = 0; j < burstSize[i]; j++) { if (isVariable.at(i)) { FrameValueAttrib attrib; buf = pktBuf_; len = streamList_[streamId.at(i)]->frameValue(pktBuf_, sizeof(pktBuf_), pktCount[i], &attrib); packetListAttrib += attrib; } else { buf = (uchar*) pktBuf.at(i).data(); len = pktLen.at(i); } if (len <= 0) continue; qDebug("q(%d) TS = %llu.%09llu", i, sec, nsec); if (!appendToPacketList(sec, nsec, buf, len)) { clearPacketList(); // don't leave it half baked/inconsitent packetListAttrib.errorFlags |= FrameValueAttrib::OutOfMemoryError; goto _out_of_memory; } pktCount[i]++; schedNsec[i] += (pktCount.at(i) < np1.at(i)) ? ipg1.at(i) : ipg2.at(i); while (schedNsec.at(i) >= 1e9) { schedSec[i]++; schedNsec[i] -= long(1e9); } } burstCount[i]++; schedNsec[i] += (burstCount.at(i) < nb1.at(i)) ? ibg1.at(i) : ibg2.at(i); while (schedNsec.at(i) >= 1e9) { schedSec[i]++; schedNsec[i] -= long(1e9); } } nsec += minGap; while (nsec >= 1e9) { sec++; nsec -= long(1e9); } } while ((sec < durSec) || ((sec == durSec) && (nsec < durNsec))); // XXX: The single packet has the delay, so no list loop delay required // XXX: Both seq/interleaved mode no longer use list loop delay! setPacketListLoopMode(true, 0, 0); // XXX: TTag repeat interval calculation: // CASE 1. pktListDuration < kTtagTimeInterval: // e.g. if pktListDuration is 1sec and TtagTimerInterval is 5s, we // skip 5 times total packets before we repeat the markers // CASE 2. pktListDuration > kTtagTimeInterval: // e.g. if pktListDuration is 7sec and TtagTimerInterval is 5s, we // skip repeat markers every pktList iteration ttagRepeatInterval = ttagMarkers.isEmpty() ? 0 : qMax(uint(kTtagTimeInterval_*1e9/(durSec*1e9+durNsec)), 1U) * totalPkts; if (!setPacketListTtagMarkers(ttagMarkers, ttagRepeatInterval)) { clearPacketList(); // don't leave it half baked/inconsitent packetListAttrib.errorFlags |= FrameValueAttrib::OutOfMemoryError; } _out_of_memory: isSendQueueDirty_ = false; qDebug("PacketListAttrib = %x", static_cast(packetListAttrib.errorFlags)); return static_cast(packetListAttrib.errorFlags); } void AbstractPort::stats(PortStats *stats) { stats->rxPkts = (stats_.rxPkts >= epochStats_.rxPkts) ? stats_.rxPkts - epochStats_.rxPkts : stats_.rxPkts + (maxStatsValue_ - epochStats_.rxPkts); stats->rxBytes = (stats_.rxBytes >= epochStats_.rxBytes) ? stats_.rxBytes - epochStats_.rxBytes : stats_.rxBytes + (maxStatsValue_ - epochStats_.rxBytes); stats->rxPps = stats_.rxPps; stats->rxBps = stats_.rxBps; stats->txPkts = (stats_.txPkts >= epochStats_.txPkts) ? stats_.txPkts - epochStats_.txPkts : stats_.txPkts + (maxStatsValue_ - epochStats_.txPkts); stats->txBytes = (stats_.txBytes >= epochStats_.txBytes) ? stats_.txBytes - epochStats_.txBytes : stats_.txBytes + (maxStatsValue_ - epochStats_.txBytes); stats->txPps = stats_.txPps; stats->txBps = stats_.txBps; stats->rxDrops = (stats_.rxDrops >= epochStats_.rxDrops) ? stats_.rxDrops - epochStats_.rxDrops : stats_.rxDrops + (maxStatsValue_ - epochStats_.rxDrops); stats->rxErrors = (stats_.rxErrors >= epochStats_.rxErrors) ? stats_.rxErrors - epochStats_.rxErrors : stats_.rxErrors + (maxStatsValue_ - epochStats_.rxErrors); stats->rxFifoErrors = (stats_.rxFifoErrors >= epochStats_.rxFifoErrors) ? stats_.rxFifoErrors - epochStats_.rxFifoErrors : stats_.rxFifoErrors + (maxStatsValue_ - epochStats_.rxFifoErrors); stats->rxFrameErrors = (stats_.rxFrameErrors >= epochStats_.rxFrameErrors) ? stats_.rxFrameErrors - epochStats_.rxFrameErrors : stats_.rxFrameErrors + (maxStatsValue_ - epochStats_.rxFrameErrors); } StreamTiming::Stats AbstractPort::streamTimingStats(uint guid) { return streamTiming_->stats(id(), guid); } void AbstractPort::clearStreamTiming(uint guid) { streamTiming_->clear(id(), guid); } void AbstractPort::streamStats(uint guid, OstProto::StreamStatsList *stats) { // In case stats are being maintained elsewhere updateStreamStats(); // Lock for read here as updateStreamStats() above will take write lock // and the lock is NOT recursive QReadLocker lock(&streamStatsLock_); if (streamStats_.contains(guid)) { StreamStatsTuple sst = streamStats_.value(guid); OstProto::StreamStats *s = stats->add_stream_stats(); StreamTiming::Stats t = streamTimingStats(guid); s->mutable_stream_guid()->set_id(guid); s->mutable_port_id()->set_id(id()); s->set_tx_duration(lastTransmitDuration()); s->set_latency(t.latency); s->set_jitter(t.jitter); s->set_tx_pkts(sst.tx_pkts); s->set_tx_bytes(sst.tx_bytes); s->set_rx_pkts(sst.rx_pkts); s->set_rx_bytes(sst.rx_bytes); } } void AbstractPort::streamStatsAll(OstProto::StreamStatsList *stats) { // In case stats are being maintained elsewhere updateStreamStats(); // Lock for read here as updateStreamStats() above will take write lock // and the lock is NOT recursive QReadLocker lock(&streamStatsLock_); // FIXME: change input param to a non-OstProto type and/or have // a getFirst/Next like API? double txDur = lastTransmitDuration(); StreamStatsIterator i(streamStats_); while (i.hasNext()) { i.next(); StreamStatsTuple sst = i.value(); OstProto::StreamStats *s = stats->add_stream_stats(); StreamTiming::Stats t = streamTimingStats(i.key()); s->mutable_stream_guid()->set_id(i.key()); s->mutable_port_id()->set_id(id()); s->set_tx_duration(txDur); s->set_latency(t.latency); s->set_jitter(t.jitter); s->set_tx_pkts(sst.tx_pkts); s->set_tx_bytes(sst.tx_bytes); s->set_rx_pkts(sst.rx_pkts); s->set_rx_bytes(sst.rx_bytes); } } void AbstractPort::resetStreamStats(uint guid) { QWriteLocker lock(&streamStatsLock_); streamStats_.remove(guid); clearStreamTiming(guid); } void AbstractPort::resetStreamStatsAll() { QWriteLocker lock(&streamStatsLock_); streamStats_.clear(); clearStreamTiming(); } void AbstractPort::clearDeviceNeighbors() { deviceManager_->clearDeviceNeighbors(); isSendQueueDirty_ = true; } void AbstractPort::resolveDeviceNeighbors() { // For a user triggered 'Resolve Neighbors', the behaviour we want is // IP not in cache - send ARP/NDP request // IP present in cache, but unresolved - re-send ARP/NDP request // IP present in cache and resolved - don't sent ARP/NDP // // Device does not resend ARP/NDP requests if the IP address is // already present in the cache, irrespective of whether it is // resolved or not (this is done to avoid sending duplicate requests). // // So, to get the behaviour we want, let's clear all unresolved neighbors // before calling resolve deviceManager_->clearDeviceNeighbors(Device::kUnresolvedNeighbors); // Resolve gateway for each device first ... deviceManager_->resolveDeviceGateways(); // ... then resolve neighbor for each unique frame of each stream // NOTE: // 1. All the frames may have the same destination ip,but may have // different source ip so may belong to a different emulated device; // so we cannot optimize and send only one ARP // 2. For a unidirectional stream, at egress, this will create ARP // entries on the DUT for each of the source addresses // // TODO(optimization): Identify if stream does not vary in srcIp or dstIp // - in which case resolve for only one frame of the stream for (int i = 0; i < streamList_.size(); i++) { const StreamBase *stream = streamList_.at(i); int frameCount = stream->frameVariableCount(); for (int j = 0; j < frameCount; j++) { // we need the packet contents only uptil the L3 header int pktLen = stream->frameValue(pktBuf_, kMaxL3PktSize, j); if (pktLen) { PacketBuffer pktBuf(pktBuf_, pktLen); deviceManager_->resolveDeviceNeighbor(&pktBuf); } } } isSendQueueDirty_ = true; } quint64 AbstractPort::deviceMacAddress(int streamId, int frameIndex) { // we need the packet contents only uptil the L3 header StreamBase *s = stream(streamId); int pktLen = s->frameValue(pktBuf_, kMaxL3PktSize, frameIndex); if (pktLen) { PacketBuffer pktBuf(pktBuf_, pktLen); return deviceManager_->deviceMacAddress(&pktBuf); } return 0; } quint64 AbstractPort::neighborMacAddress(int streamId, int frameIndex) { // we need the packet contents only uptil the L3 header StreamBase *s = stream(streamId); int pktLen = s->frameValue(pktBuf_, kMaxL3PktSize, frameIndex); if (pktLen) { PacketBuffer pktBuf(pktBuf_, pktLen); return deviceManager_->neighborMacAddress(&pktBuf); } return 0; } const InterfaceInfo* AbstractPort::interfaceInfo() const { return interfaceInfo_; } ostinato-1.3.0/server/abstractport.h000066400000000000000000000127511451413623100175500ustar00rootroot00000000000000/* Copyright (C) 2010-2012 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _SERVER_ABSTRACT_PORT_H #define _SERVER_ABSTRACT_PORT_H #include "../common/protocol.pb.h" #include "streamstats.h" #include "streamtiming.h" #include #include #include #include class DeviceManager; struct InterfaceInfo; class PacketBuffer; class QIODevice; class StreamBase; // TODO: send notification back to client(s) #define Xnotify qWarning class AbstractPort { public: struct PortStats { quint64 rxPkts; quint64 rxBytes; quint64 rxPps; quint64 rxBps; quint64 rxDrops; quint64 rxErrors; quint64 rxFifoErrors; quint64 rxFrameErrors; quint64 txPkts; quint64 txBytes; quint64 txPps; quint64 txBps; }; enum Accuracy { kHighAccuracy, kMediumAccuracy, kLowAccuracy, }; AbstractPort(int id, const char *device); virtual ~AbstractPort(); bool isUsable() { return isUsable_; } virtual void init(); int id() { return data_.port_id().id(); } const char* name() { return data_.name().c_str(); } void protoDataCopyInto(OstProto::Port *port) { port->CopyFrom(data_); } bool canModify(const OstProto::Port &port, bool *dirty); bool modify(const OstProto::Port &port); const InterfaceInfo* interfaceInfo() const; virtual OstProto::LinkState linkState() { return linkState_; } virtual bool hasExclusiveControl() = 0; virtual bool setExclusiveControl(bool exclusive) = 0; int streamCount() { return streamList_.size(); } StreamBase* streamAtIndex(int index); StreamBase* stream(int streamId); bool addStream(StreamBase *stream); bool deleteStream(int streamId); bool isDirty() { return isSendQueueDirty_; } void setDirty() { isSendQueueDirty_ = true; } virtual bool setTrackStreamStats(bool enable); Accuracy rateAccuracy(); virtual bool setRateAccuracy(Accuracy accuracy); virtual void clearPacketList() = 0; virtual void loopNextPacketSet(qint64 size, qint64 repeats, long repeatDelaySec, long repeatDelayNsec) = 0; virtual bool appendToPacketList(long sec, long nsec, const uchar *packet, int length) = 0; virtual void setPacketListLoopMode(bool loop, quint64 secDelay, quint64 nsecDelay) = 0; virtual bool setPacketListTtagMarkers(QList markers, uint repeatInterval) = 0; int updatePacketList(); virtual void startTransmit() = 0; virtual void stopTransmit() = 0; virtual bool isTransmitOn() = 0; virtual double lastTransmitDuration() = 0; virtual void startCapture() = 0; virtual void stopCapture() = 0; virtual bool isCaptureOn() = 0; virtual QIODevice* captureData() = 0; void stats(PortStats *stats); void resetStats() { epochStats_ = stats_; } StreamTiming::Stats streamTimingStats(uint guid); void clearStreamTiming(uint guid = UINT_MAX); // FIXME: combine single and All calls? void streamStats(uint guid, OstProto::StreamStatsList *stats); void streamStatsAll(OstProto::StreamStatsList *stats); void resetStreamStats(uint guid); void resetStreamStatsAll(); virtual void updateStreamStats() { // subclasses may implement - if required } DeviceManager* deviceManager(); virtual void startDeviceEmulation() = 0; virtual void stopDeviceEmulation() = 0; virtual int sendEmulationPacket(PacketBuffer *pktBuf) = 0; void clearDeviceNeighbors(); void resolveDeviceNeighbors(); quint64 deviceMacAddress(int streamId, int frameIndex); quint64 neighborMacAddress(int streamId, int frameIndex); protected: void addNote(QString note); int updatePacketListSequential(); int updatePacketListInterleaved(); bool isUsable_; OstProto::Port data_; OstProto::LinkState linkState_; ulong minPacketSetSize_; Accuracy rateAccuracy_; quint64 maxStatsValue_; struct PortStats stats_; StreamStats streamStats_; QReadWriteLock streamStatsLock_; //! \todo Need lock for stats access/update const uint kTtagTimeInterval_{5}; // in seconds struct InterfaceInfo *interfaceInfo_; DeviceManager *deviceManager_; private: bool isSendQueueDirty_; static const int kMaxPktSize = 16384; uchar pktBuf_[kMaxPktSize]; // When finding a corresponding device for a packet, we need to inspect // only uptil the L3 header; in the worst case this would be - // mac (12) + 4 x vlan (16) + ethType (2) + ipv6 (40) = 74 bytes // let's round it up to 80 bytes static const int kMaxL3PktSize = 80; /*! \note StreamBase::id() and index into streamList[] are NOT same! */ QList streamList_; struct PortStats epochStats_; StreamTiming *streamTiming_{nullptr}; }; #endif ostinato-1.3.0/server/bsdhostdevice.cpp000066400000000000000000000373601451413623100202240ustar00rootroot00000000000000/* Copyright (C) 2018 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #include "bsdhostdevice.h" #include "netdefs.h" #include "packetbuffer.h" #include #ifdef Q_OS_BSD4 #include "../common/qtport.h" #include #include #include #include #include #include #include #include #include #ifndef SA_SIZE // For some reason MacOS doesn't define this while BSD does // And the story of how to roundup is ugly - see // https://github.com/FRRouting/frr/blob/master/zebra/kernel_socket.c #ifdef __APPLE__ #define ROUNDUP_TYPE int #else #define ROUNDUP_TYPE long #endif #define SA_SIZE(sa) \ ( (!(sa) || ((struct sockaddr *)(sa))->sa_len == 0) ? \ sizeof(ROUNDUP_TYPE) : \ 1 + ( (((struct sockaddr *)(sa))->sa_len - 1) | (sizeof(ROUNDUP_TYPE) - 1) ) ) #endif quint32 sumUInt128(UInt128 value); BsdHostDevice::BsdHostDevice(QString portName, DeviceManager *deviceManager) : Device(deviceManager) { ifName_ = portName; ifIndex_ = if_nametoindex(qPrintable(ifName_)); qDebug("Port %s: ifIndex %d", qPrintable(ifName_), ifIndex_); rtSock_ = socket(PF_ROUTE, SOCK_RAW, AF_UNSPEC); shutdown(rtSock_, SHUT_RD); // we don't read from rtSock char errbuf[PCAP_ERRBUF_SIZE] = ""; txHandle_ = pcap_open_live(qPrintable(ifName_), 64, 0, 0, errbuf); if (txHandle_ == NULL) { qWarning("pcap open %s failed (%s)", qPrintable(ifName_), errbuf); } } void BsdHostDevice::receivePacket(PacketBuffer* /*pktBuf*/) { // Do Nothing } void BsdHostDevice::clearNeighbors(Device::NeighborSet set) { // No need to do anything - see AbstractPort::resolveDeviceNeighbors() // on when this is used if (set == kUnresolvedNeighbors) return; size_t len; int mib[] = {CTL_NET, PF_ROUTE, 0, AF_INET, NET_RT_FLAGS, 0}; const int mibLen = sizeof(mib)/sizeof(mib[0]); QByteArray buf; #if defined(RTF_LLDATA) mib[5] = RTF_LLDATA; #else mib[5] = RTF_LLINFO; #endif if (sysctl(mib, mibLen, NULL, &len, NULL, 0) < 0) { // find buffer len qWarning("sysctl NET_RT_FLAGS(1) failed (%s)\n", strerror(errno)); return; } buf.resize(len); if (sysctl(mib, mibLen, buf.data(), &len, NULL, 0) < 0) { // now retreive ARP/NDP qWarning("sysctl NET_RT_FLAGS(2) failed(%s)\n", strerror(errno)); return; } int count=0, fail=0; char *p = buf.data(); const char *end = p + len; while (p < end) { struct rt_msghdr *rtm = (struct rt_msghdr*) p; if ((rtm->rtm_index == ifIndex_) && !(rtm->rtm_flags & RTF_PINNED)) { const struct sockaddr *sa = (const struct sockaddr*)(rtm + 1); rtm->rtm_type = RTM_DELETE; if (write(rtSock_, p, rtm->rtm_msglen) < 0) { qWarning("RTM_DELETE failed for ip %s (%s)", qPrintable(QHostAddress(sa).toString()) , strerror(errno)); fail++; } count++; } p += rtm->rtm_msglen; } qDebug("Flush ARP table for ifIndex %u: %d/%d deleted", ifIndex_, count - fail, count); // We need to query AF_INET and AF_INET6 separately as sysctl with AF_UNSPEC // doesn't work mib[3] = AF_INET6; if (sysctl(mib, mibLen, NULL, &len, NULL, 0) < 0) { // find buffer len qWarning("sysctl NET_RT_FLAGS(3) failed (%s)\n", strerror(errno)); return; } buf.resize(len); if (sysctl(mib, mibLen, buf.data(), &len, NULL, 0) < 0) { // now retreive ARP/NDP qWarning("sysctl NET_RT_FLAGS(4) failed(%s)\n", strerror(errno)); return; } count = fail = 0; p = buf.data(); end = p + len; while (p < end) { struct rt_msghdr *rtm = (struct rt_msghdr*) p; if ((rtm->rtm_index == ifIndex_) && !(rtm->rtm_flags & RTF_PINNED)) { const struct sockaddr *sa = (const struct sockaddr*)(rtm + 1); rtm->rtm_type = RTM_DELETE; if (write(rtSock_, p, rtm->rtm_msglen) < 0) { qWarning("RTM_DELETE failed for ip %s (%s)", qPrintable(QHostAddress(sa).toString()) , strerror(errno)); fail++; } count++; } p += rtm->rtm_msglen; } qDebug("Flush ND table for ifIndex %u: %d/%d deleted", ifIndex_, count - fail, count); } void BsdHostDevice::getNeighbors(OstEmul::DeviceNeighborList *neighbors) { size_t len; int mib[] = {CTL_NET, PF_ROUTE, 0, AF_INET, NET_RT_FLAGS, 0}; const int mibLen = sizeof(mib)/sizeof(mib[0]); QByteArray buf; #if defined(RTF_LLDATA) mib[5] = RTF_LLDATA; #else mib[5] = RTF_LLINFO; #endif if (sysctl(mib, mibLen, NULL, &len, NULL, 0) < 0) { // find buffer len qWarning("sysctl NET_RT_FLAGS(1) failed (%s)\n", strerror(errno)); return; } buf.resize(len); if (sysctl(mib, mibLen, buf.data(), &len, NULL, 0) < 0) { // now retreive ARP/NDP qWarning("sysctl NET_RT_FLAGS(2) failed(%s)\n", strerror(errno)); return; } const char *p = buf.constData(); const char *end = p + len; while (p < end) { const struct rt_msghdr *rtm = (const struct rt_msghdr*) p; const struct sockaddr_in *sin = (const struct sockaddr_in*)(rtm + 1); const struct sockaddr_dl *sdl = (const struct sockaddr_dl*) ((char*)sin + SA_SIZE(sin)); if (sdl->sdl_index == ifIndex_) { OstEmul::ArpEntry *arp = neighbors->add_arp(); arp->set_ip4(qFromBigEndian(sin->sin_addr.s_addr)); arp->set_mac(qFromBigEndian(LLADDR(sdl)) >> 16); } p += rtm->rtm_msglen; } // We need to query AF_INET and AF_INET6 separately as sysctl with AF_UNSPEC // doesn't work mib[3] = AF_INET6; if (sysctl(mib, mibLen, NULL, &len, NULL, 0) < 0) { // find buffer len qWarning("sysctl NET_RT_FLAGS(1) failed (%s)\n", strerror(errno)); return; } buf.resize(len); if (sysctl(mib, mibLen, buf.data(), &len, NULL, 0) < 0) { // now retreive ARP/NDP qWarning("sysctl NET_RT_FLAGS(2) failed(%s)\n", strerror(errno)); return; } p = buf.constData(); end = p + len; while (p < end) { const struct rt_msghdr *rtm = (const struct rt_msghdr*) p; const struct sockaddr_in6 *sin = (const struct sockaddr_in6*)(rtm + 1); const struct sockaddr_dl *sdl = (const struct sockaddr_dl*) ((char*)sin + SA_SIZE(sin)); if (sdl->sdl_index == ifIndex_) { #ifdef __KAME__ if (IN6_IS_ADDR_LINKLOCAL(&sin->sin6_addr) || IN6_IS_ADDR_MC_LINKLOCAL(&sin->sin6_addr)) { // remove the embedded ifIndex in the 2nd hextet (u16) const_cast(sin)->sin6_addr.s6_addr[2] = 0; const_cast(sin)->sin6_addr.s6_addr[3] = 0; #endif } OstEmul::NdpEntry *ndp = neighbors->add_ndp(); ndp->mutable_ip6()->set_hi(qFromBigEndian(sin->sin6_addr.s6_addr)); ndp->mutable_ip6()->set_lo(qFromBigEndian(sin->sin6_addr.s6_addr+8)); ndp->set_mac(qFromBigEndian(LLADDR(sdl)) >> 16); } p += rtm->rtm_msglen; } } quint64 BsdHostDevice::arpLookup(quint32 ip) { quint64 mac = 0; size_t len; int mib[] = {CTL_NET, PF_ROUTE, 0, AF_INET, NET_RT_FLAGS, 0}; const int mibLen = sizeof(mib)/sizeof(mib[0]); QByteArray buf; #if defined(RTF_LLDATA) mib[5] = RTF_LLDATA; #else mib[5] = RTF_LLINFO; #endif if (sysctl(mib, mibLen, NULL, &len, NULL, 0) < 0) { // find buffer len qWarning("sysctl NET_RT_FLAGS(1) failed (%s)\n", strerror(errno)); return mac; } buf.resize(len); if (sysctl(mib, mibLen, buf.data(), &len, NULL, 0) < 0) { // now retreive ARP/NDP qWarning("sysctl NET_RT_FLAGS(2) failed(%s)\n", strerror(errno)); return mac; } const char *p = buf.constData(); const char *end = p + len; while (p < end) { const struct rt_msghdr *rtm = (const struct rt_msghdr*) p; if (rtm->rtm_index == ifIndex_) { const struct sockaddr_in *sin = (const struct sockaddr_in*)(rtm + 1); if (qFromBigEndian(sin->sin_addr.s_addr) == ip) { const struct sockaddr_dl *sdl = (const struct sockaddr_dl*) ((char*)sin + SA_SIZE(sin)); mac = qFromBigEndian(LLADDR(sdl)) >> 16; break; } } p += rtm->rtm_msglen; } return mac; } quint64 BsdHostDevice::ndpLookup(UInt128 ip) { quint64 mac = 0; size_t len; int mib[] = {CTL_NET, PF_ROUTE, 0, AF_INET6, NET_RT_FLAGS, 0}; const int mibLen = sizeof(mib)/sizeof(mib[0]); QByteArray buf; #if defined(RTF_LLDATA) mib[5] = RTF_LLDATA; #else mib[5] = RTF_LLINFO; #endif if (sysctl(mib, mibLen, NULL, &len, NULL, 0) < 0) { // find buffer len qWarning("sysctl NET_RT_FLAGS(1) failed (%s)\n", strerror(errno)); return mac; } buf.resize(len); if (sysctl(mib, mibLen, buf.data(), &len, NULL, 0) < 0) { // now retreive ARP/NDP qWarning("sysctl NET_RT_FLAGS(2) failed(%s)\n", strerror(errno)); return mac; } const char *p = buf.constData(); const char *end = p + len; while (p < end) { const struct rt_msghdr *rtm = (const struct rt_msghdr*) p; if (rtm->rtm_index == ifIndex_) { const struct sockaddr_in6 *sin = (const struct sockaddr_in6*)(rtm + 1); if ((qFromBigEndian(sin->sin6_addr.s6_addr) == ip.hi64()) && (qFromBigEndian(sin->sin6_addr.s6_addr+8) == ip.lo64())) { const struct sockaddr_dl *sdl = (const struct sockaddr_dl*) ((char*)sin + SA_SIZE(sin)); mac = qFromBigEndian(LLADDR(sdl)) >> 16; break; } } p += rtm->rtm_msglen; } return mac; } void BsdHostDevice::sendArpRequest(quint32 tgtIp) { // // XXX: I can't seem to find a BSD syscall to trigger the kernel to send an ARP; // so for now craft one from scratch and send // NOTE: RTM_RESOLVE has been removed - see // http://conferences.sigcomm.org/sigcomm/2009/workshops/presto/papers/p37.pdf // quint32 srcIp = ip4_; PacketBuffer *reqPkt; uchar *pktData; // Validate target IP if (!tgtIp) return; reqPkt = new PacketBuffer; reqPkt->reserve(encapSize()); pktData = reqPkt->put(28); if (pktData) { // HTYP, PTYP *(quint32*)(pktData ) = qToBigEndian(quint32(0x00010800)); // HLEN, PLEN, OPER *(quint32*)(pktData+ 4) = qToBigEndian(quint32(0x06040001)); // Source H/W Addr, Proto Addr *(quint32*)(pktData+ 8) = qToBigEndian(quint32(mac_ >> 16)); *(quint16*)(pktData+12) = qToBigEndian(quint16(mac_ & 0xffff)); *(quint32*)(pktData+14) = qToBigEndian(srcIp); // Target H/W Addr, Proto Addr *(quint32*)(pktData+18) = qToBigEndian(quint32(0)); *(quint16*)(pktData+22) = qToBigEndian(quint16(0)); *(quint32*)(pktData+24) = qToBigEndian(tgtIp); } ethEncap(reqPkt, kBcastMac, kEthTypeArp); pcap_sendpacket((pcap_t*)txHandle_, reqPkt->data(), reqPkt->length()); qDebug("Sent ARP Request for srcIp/tgtIp=%s/%s", qPrintable(QHostAddress(srcIp).toString()), qPrintable(QHostAddress(tgtIp).toString())); } void BsdHostDevice::sendNeighborSolicit(UInt128 tgtIp) { // XXX: See note in sendArpRequest() - applies here too UInt128 dstIp, srcIp = ip6_; PacketBuffer *reqPkt; uchar *pktData; // Validate target IP if (tgtIp == UInt128(0, 0)) return; // Form the solicited node address to be used as dstIp // ff02::1:ffXX:XXXX/104 dstIp = UInt128((quint64(0xff02) << 48), (quint64(0x01ff) << 24) | (tgtIp.lo64() & 0xFFFFFF)); reqPkt = new PacketBuffer; reqPkt->reserve(encapSize() + kIp6HdrLen); pktData = reqPkt->put(32); if (pktData) { // Calculate checksum first - // start with fixed fields in ICMP Header and IPv6 Pseudo Header ... quint32 sum = 0x8700 + 0x0101 + 32 + kIpProtoIcmp6; // then variable fields from ICMP header ... sum += sumUInt128(tgtIp); sum += (mac_ >> 32) + ((mac_ >> 16) & 0xffff) + (mac_ & 0xffff); // and variable fields from IPv6 pseudo header sum += sumUInt128(ip6_); sum += sumUInt128(dstIp); while(sum >> 16) sum = (sum & 0xFFFF) + (sum >> 16); // Type, Code *(quint16*)(pktData ) = qToBigEndian(quint16(0x8700)); // Checksum *(quint16*)(pktData+ 2) = qToBigEndian(quint16(~sum)); // Reserved *(quint32*)(pktData+ 4) = qToBigEndian(quint32(0)); // Target IP memcpy(pktData+ 8, tgtIp.toArray(), 16); // Source Addr TLV + MacAddr *(quint16*)(pktData+24) = qToBigEndian(quint16(0x0101)); *(quint32*)(pktData+26) = qToBigEndian(quint32(mac_ >> 16)); *(quint16*)(pktData+30) = qToBigEndian(quint16(mac_ & 0xffff)); } int payloadLen = reqPkt->length(); uchar *p = reqPkt->push(kIp6HdrLen); quint64 dstMac = (quint64(0x3333) << 32) | (dstIp.lo64() & 0xffffffff); // Ver(4), TrfClass(8), FlowLabel(8) *(quint32*)(p ) = qToBigEndian(quint32(0x60000000)); *(quint16*)(p+ 4) = qToBigEndian(quint16(payloadLen)); p[6] = kIpProtoIcmp6; // protocol p[7] = 255; // HopLimit memcpy(p+ 8, ip6_.toArray(), 16); // Source IP memcpy(p+24, dstIp.toArray(), 16); // Destination IP ethEncap(reqPkt, dstMac, kEthTypeIp6); pcap_sendpacket((pcap_t*)txHandle_, reqPkt->data(), reqPkt->length()); qDebug("Sent NDP Request for srcIp/tgtIp=%s/%s", qPrintable(QHostAddress(srcIp.toArray()).toString()), qPrintable(QHostAddress(tgtIp.toArray()).toString())); } int BsdHostDevice::encapSize() { Q_ASSERT(numVlanTags_ >= 0); // ethernet header + vlans int size = 14 + kMaxVlan*numVlanTags_; return size; } void BsdHostDevice::ethEncap(PacketBuffer *pktBuf, quint64 dstMac, quint16 type) { int ofs; quint64 srcMac = mac_; uchar *p = pktBuf->push(encapSize()); if (!p) { qWarning("%s: failed to push %d bytes [0x%p, 0x%p]", __FUNCTION__, encapSize(), pktBuf->head(), pktBuf->data()); goto _exit; } *(quint32*)(p ) = qToBigEndian(quint32(dstMac >> 16)); *(quint16*)(p + 4) = qToBigEndian(quint16(dstMac & 0xffff)); *(quint32*)(p + 6) = qToBigEndian(quint32(srcMac >> 16)); *(quint16*)(p + 10) = qToBigEndian(quint16(srcMac & 0xffff)); ofs = 12; for (int i = 0; i < numVlanTags_; i++) { *(quint32*)(p + ofs) = qToBigEndian(vlan_[i]); ofs += 4; } *(quint16*)(p + ofs) = qToBigEndian(type); ofs += 2; Q_ASSERT(ofs == encapSize()); _exit: return; } #endif ostinato-1.3.0/server/bsdhostdevice.h000066400000000000000000000027611451413623100176660ustar00rootroot00000000000000/* Copyright (C) 2018 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _BSD_HOST_DEVICE_H #define _BSD_HOST_DEVICE_H #include "device.h" #ifdef Q_OS_BSD4 class BsdHostDevice: public Device { public: BsdHostDevice(QString portName, DeviceManager *deviceManager); virtual ~BsdHostDevice() {}; virtual void receivePacket(PacketBuffer *pktBuf); virtual void clearNeighbors(Device::NeighborSet set); virtual void getNeighbors(OstEmul::DeviceNeighborList *neighbors); protected: virtual quint64 arpLookup(quint32 ip); virtual quint64 ndpLookup(UInt128 ip); virtual void sendArpRequest(quint32 tgtIp); virtual void sendNeighborSolicit(UInt128 tgtIp); private: int encapSize(); void ethEncap(PacketBuffer *pktBuf, quint64 dstMac, quint16 type); void *txHandle_{nullptr}; QString ifName_; unsigned int ifIndex_{0}; int rtSock_{-1}; }; #endif #endif ostinato-1.3.0/server/bsdport.cpp000066400000000000000000000365361451413623100170570ustar00rootroot00000000000000/* Copyright (C) 2012 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #include "bsdport.h" #include "interfaceinfo.h" #ifdef Q_OS_BSD4 #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef Q_OS_MAC #define ifr_flagshigh ifr_flags #define IFF_PPROMISC (IFF_PROMISC << 16) #endif #ifndef SA_SIZE // For some reason MacOS doesn't define this while BSD does // And the story of how to roundup is ugly - see // https://github.com/FRRouting/frr/blob/master/zebra/kernel_socket.c #ifdef __APPLE__ #define ROUNDUP_TYPE int #else #define ROUNDUP_TYPE long #endif #define SA_SIZE(sa) \ ( (!(sa) || ((struct sockaddr *)(sa))->sa_len == 0) ? \ sizeof(ROUNDUP_TYPE) : \ 1 + ( (((struct sockaddr *)(sa))->sa_len - 1) | (sizeof(ROUNDUP_TYPE) - 1) ) ) #endif struct ifaddrs *BsdPort::addressList_{nullptr}; QByteArray BsdPort::routeListBuffer_; QList BsdPort::allPorts_; BsdPort::StatsMonitor *BsdPort::monitor_; const quint32 kMaxValue32 = 0xffffffff; BsdPort::BsdPort(int id, const char *device) : PcapPort(id, device) { isPromisc_ = true; clearPromisc_ = false; ifIndex_ = if_nametoindex(device); populateInterfaceInfo(); // We don't need per port Rx/Tx monitors for Bsd delete monitorRx_; delete monitorTx_; monitorRx_ = monitorTx_ = NULL; // We have one monitor for both Rx/Tx of all ports if (!monitor_) monitor_ = new StatsMonitor(); data_.set_is_exclusive_control(hasExclusiveControl()); minPacketSetSize_ = 16; qDebug("adding dev to all ports list <%s>", device); allPorts_.append(this); maxStatsValue_ = ULONG_MAX; } BsdPort::~BsdPort() { qDebug("In %s", __FUNCTION__); if (monitor_->isRunning()) { monitor_->stop(); monitor_->wait(); } if (clearPromisc_) { int sd = socket(AF_INET, SOCK_DGRAM, 0); struct ifreq ifr; memset(&ifr, 0, sizeof(ifr)); strncpy(ifr.ifr_name, name(), sizeof(ifr.ifr_name)); if (ioctl(sd, SIOCGIFFLAGS, &ifr) != -1) { short promisc = IFF_PPROMISC >> 16; if (ifr.ifr_flagshigh & promisc) { ifr.ifr_flagshigh &= ~promisc; if (ioctl(sd, SIOCSIFFLAGS, &ifr) == -1) qDebug("Failed clearing promisc flag. SIOCSIFFLAGS failed: %s", strerror(errno)); else qDebug("Cleared promisc successfully"); } else qDebug("clear_promisc is set but IFF_PPROMISC is not?"); } else qDebug("Failed clearing promisc flag. SIOCGIFFLAGS failed: %s", strerror(errno)); close(sd); } } void BsdPort::fetchHostNetworkInfo() { if (getifaddrs(&addressList_) < 0) { qWarning("getifaddrs() failed: %s", strerror(errno)); return; } size_t len; int mib[] = {CTL_NET, PF_ROUTE, 0, AF_UNSPEC, NET_RT_FLAGS, RTF_GATEWAY}; if (sysctl(mib, sizeof(mib)/sizeof(int), 0, &len, 0, 0) < 0) { qWarning("sysctl CTL_NET|PF_ROUTE failed fetching buflen: %s", strerror(errno)); return; } routeListBuffer_.resize(len); if (sysctl(mib, sizeof(mib)/sizeof(int), routeListBuffer_.data(), &len, 0, 0) < 0) { qWarning("sysctl CTL_NET|PF_ROUTE failed: %s", strerror(errno)); return; } } void BsdPort::freeHostNetworkInfo() { freeifaddrs(addressList_); addressList_ = nullptr; routeListBuffer_.resize(0); // release allocated memory } void BsdPort::init() { if (!monitor_->isRunning()) monitor_->start(); monitor_->waitForSetupFinished(); if (!isPromisc_) addNote("Non Promiscuous Mode"); AbstractPort::init(); } bool BsdPort::hasExclusiveControl() { // TODO return false; } bool BsdPort::setExclusiveControl(bool /*exclusive*/) { // TODO return false; } void BsdPort::populateInterfaceInfo() { // // Find Mac // quint64 mac = 0; struct ifaddrs *addr; for (addr = addressList_; addr != NULL; addr = addr->ifa_next) { if (strcmp(addr->ifa_name, name()) == 0) { if (addr->ifa_addr->sa_family == AF_LINK) { mac = qFromBigEndian( LLADDR((struct sockaddr_dl *)(addr->ifa_addr))) >> 16; break; } } } interfaceInfo_ = new InterfaceInfo; interfaceInfo_->mac = mac; if (mac) { interfaceInfo_->speed = ((struct if_data*)addr->ifa_data)->ifi_baudrate/1e6; interfaceInfo_->mtu = ((struct if_data*)addr->ifa_data)->ifi_mtu; } // // Find gateways // static_assert(RTA_DST == 0x1, "RTA_DST is not 0x1"); // Validate assumption static_assert(RTA_GATEWAY == 0x2, "RTA_GATEWAY is not 0x2"); // Validate assumption quint32 gw4 = 0; UInt128 gw6 = 0; const char *p = routeListBuffer_.constData(); const char *end = p + routeListBuffer_.size(); while (!gw4 || !gw6) { const struct rt_msghdr *rt = (const struct rt_msghdr*) p; const struct sockaddr *sa = (const struct sockaddr*)(rt + 1); // RTA_DST = 0x1 if ((rt->rtm_index == ifIndex_) && ((rt->rtm_addrs & (RTA_DST|RTA_GATEWAY)) == (RTA_DST|RTA_GATEWAY))) { if (!gw4 && sa->sa_family == AF_INET) { if (((sockaddr_in*)sa)->sin_addr.s_addr == 0) // default route 0.0.0.0 { sa = (struct sockaddr *)((char *)sa + SA_SIZE(sa)); gw4 = qFromBigEndian( ((sockaddr_in*)sa)->sin_addr.s_addr); // RTA_GW = 0x2 } } if (!gw6 && sa->sa_family == AF_INET6) { if (UInt128((quint8*)(((sockaddr_in6*)sa)->sin6_addr.s6_addr)) == UInt128(0,0)) // default route :: { sa = (struct sockaddr *)((char *)sa + SA_SIZE(sa)); gw6 = UInt128((quint8*)( ((sockaddr_in6*)sa)->sin6_addr.s6_addr)); // RTA_GW = 0x2 } } } p += rt->rtm_msglen; if (p >= end) break; } // // Find self IP // addr = addressList_; while (addr) { if (strcmp(addr->ifa_name, name()) == 0) { if (addr->ifa_addr && addr->ifa_addr->sa_family == AF_INET) { Ip4Config ip; ip.address = qFromBigEndian( ((struct sockaddr_in *)(addr->ifa_addr))->sin_addr.s_addr); ip.prefixLength = std::bitset<32>( ((struct sockaddr_in *)(addr->ifa_netmask))->sin_addr.s_addr) .count(); ip.gateway = gw4; interfaceInfo_->ip4.append(ip); } else if (addr->ifa_addr && addr->ifa_addr->sa_family == AF_INET6) { Ip6Config ip; ip.address = UInt128((quint8*) ((struct sockaddr_in6 *)(addr->ifa_addr))->sin6_addr.s6_addr); Q_ASSERT(addr->ifa_netmask); ip.prefixLength = std::bitset<64>(qFromBigEndian( ((struct sockaddr_in6 *)(addr->ifa_netmask)) ->sin6_addr.s6_addr)) .count(); ip.prefixLength += std::bitset<64>(qFromBigEndian( ((struct sockaddr_in6 *)(addr->ifa_netmask)) ->sin6_addr.s6_addr+8)) .count(); ip.gateway = gw6; interfaceInfo_->ip6.append(ip); } } addr = addr->ifa_next; } } BsdPort::StatsMonitor::StatsMonitor() : QThread() { setObjectName("StatsMon"); stop_ = false; setupDone_ = false; } void BsdPort::StatsMonitor::run() { int mib[] = {CTL_NET, PF_ROUTE, 0, 0, NET_RT_IFLIST, 0}; const int mibLen = sizeof(mib)/sizeof(mib[0]); QHash portStats; QHash linkState; int sd; QByteArray buf; size_t len; char *p, *end; int count; struct ifreq ifr; // // We first setup stuff before we start polling for stats // if (sysctl(mib, mibLen, NULL, &len, NULL, 0) < 0) { qWarning("sysctl NET_RT_IFLIST(1) failed (%s)\n", strerror(errno)); return; } qDebug("sysctl mib returns reqd len = %d\n", (int) len); len *= 2; // for extra room, just in case! buf.fill('\0', len); if (sysctl(mib, mibLen, buf.data(), &len, NULL, 0) < 0) { qWarning("sysctl NET_RT_IFLIST(2) failed(%s)\n", strerror(errno)); return; } sd = socket(AF_INET, SOCK_DGRAM, 0); Q_ASSERT(sd >= 0); memset(&ifr, 0, sizeof(ifr)); // // Populate the port stats hash table // p = buf.data(); end = p + len; count = 0; while (p < end) { struct if_msghdr *ifm = (struct if_msghdr*) p; struct sockaddr_dl *sdl = (struct sockaddr_dl*) (ifm + 1); if (ifm->ifm_type == RTM_IFINFO) { char ifname[1024]; strncpy(ifname, sdl->sdl_data, sdl->sdl_nlen); ifname[sdl->sdl_nlen] = 0; qDebug("if: %s(%d, %d)", ifname, ifm->ifm_index, sdl->sdl_index); foreach(BsdPort* port, allPorts_) { if (strncmp(port->name(), sdl->sdl_data, sdl->sdl_nlen) == 0) { Q_ASSERT(ifm->ifm_index == sdl->sdl_index); portStats[uint(ifm->ifm_index)] = &(port->stats_); linkState[uint(ifm->ifm_index)] = &(port->linkState_); // Set promisc mode, if not already set strncpy(ifr.ifr_name, port->name(), sizeof(ifr.ifr_name)); if (ioctl(sd, SIOCGIFFLAGS, &ifr) != -1) { short promisc = IFF_PPROMISC >> 16; if ((ifr.ifr_flagshigh & promisc) == 0) { ifr.ifr_flagshigh |= promisc; if (ioctl(sd, SIOCSIFFLAGS, &ifr) != -1) { qDebug("%s: set promisc successful", port->name()); port->clearPromisc_ = true; } else { port->isPromisc_ = false; qDebug("%s: failed to set promisc; " "SIOCSIFFLAGS failed (%s)", port->name(), strerror(errno)); } } else qDebug("%s: promisc already set", port->name()); } else { port->isPromisc_ = false; qDebug("%s: failed to set promisc; SIOCGIFFLAGS failed (%s)", port->name(), strerror(errno)); } break; } } count++; } p += ifm->ifm_msglen; } qDebug("port count = %d\n", count); if (count <= 0) { qWarning("no ports in NET_RT_IFLIST - no stats will be available"); return; } close(sd); qDebug("stats for %d ports setup", count); setupDone_ = true; // // We are all set - Let's start polling for stats! // while (!stop_) { if (sysctl(mib, mibLen, buf.data(), &len, NULL, 0) < 0) { qWarning("sysctl NET_RT_IFLIST(3) failed(%s)\n", strerror(errno)); goto _try_later; } p = buf.data(); end = p + len; while (p < end) { struct if_msghdr *ifm = (struct if_msghdr*) p; AbstractPort::PortStats *stats; if (ifm->ifm_type != RTM_IFINFO) goto _next; stats = portStats[ifm->ifm_index]; if (stats) { struct if_data *ifd = &(ifm->ifm_data); OstProto::LinkState *state = linkState[ifm->ifm_index]; u_long in_packets; Q_ASSERT(state); #ifdef Q_OS_MAC *state = ifm->ifm_flags & IFF_RUNNING ? OstProto::LinkStateUp : OstProto::LinkStateDown; #else *state = (OstProto::LinkState) ifd->ifi_link_state; #endif in_packets = ifd->ifi_ipackets + ifd->ifi_noproto; stats->rxPps = ((in_packets >= stats->rxPkts) ? in_packets - stats->rxPkts : in_packets + (kMaxValue32 - stats->rxPkts)) / kRefreshFreq_; stats->rxBps = ((ifd->ifi_ibytes >= stats->rxBytes) ? ifd->ifi_ibytes - stats->rxBytes : ifd->ifi_ibytes + (kMaxValue32 - stats->rxBytes)) / kRefreshFreq_; stats->rxPkts = in_packets; stats->rxBytes = ifd->ifi_ibytes; stats->txPps = ((ifd->ifi_opackets >= stats->txPkts) ? ifd->ifi_opackets - stats->txPkts : ifd->ifi_opackets + (kMaxValue32 - stats->txPkts)) / kRefreshFreq_; stats->txBps = ((ifd->ifi_obytes >= stats->txBytes) ? ifd->ifi_obytes - stats->txBytes : ifd->ifi_obytes + (kMaxValue32 - stats->txBytes)) / kRefreshFreq_; stats->txPkts = ifd->ifi_opackets; stats->txBytes = ifd->ifi_obytes; stats->rxDrops = ifd->ifi_iqdrops; stats->rxErrors = ifd->ifi_ierrors; } _next: p += ifm->ifm_msglen; } _try_later: QThread::sleep(kRefreshFreq_); } portStats.clear(); linkState.clear(); } void BsdPort::StatsMonitor::stop() { stop_ = true; } bool BsdPort::StatsMonitor::waitForSetupFinished(int msecs) { QTime t; t.start(); while (!setupDone_) { if (t.elapsed() > msecs) return false; QThread::msleep(10); } return true; } #endif ostinato-1.3.0/server/bsdport.h000066400000000000000000000032651451413623100165150ustar00rootroot00000000000000/* Copyright (C) 2012 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _SERVER_BSD_PORT_H #define _SERVER_BSD_PORT_H #include #ifdef Q_OS_BSD4 #include "pcapport.h" class BsdPort : public PcapPort { public: BsdPort(int id, const char *device); ~BsdPort(); void init(); virtual bool hasExclusiveControl(); virtual bool setExclusiveControl(bool exclusive); static void fetchHostNetworkInfo(); static void freeHostNetworkInfo(); protected: class StatsMonitor: public QThread { public: StatsMonitor(); void run(); void stop(); bool waitForSetupFinished(int msecs = 10000); private: static const int kRefreshFreq_ = 1; // in seconds bool stop_; bool setupDone_; }; bool isPromisc_; bool clearPromisc_; static QList allPorts_; static StatsMonitor *monitor_; // rx/tx stats for ALL ports private: void populateInterfaceInfo(); unsigned int ifIndex_{0}; static struct ifaddrs *addressList_; static QByteArray routeListBuffer_; }; #endif #endif ostinato-1.3.0/server/device.cpp000066400000000000000000000271771451413623100166420ustar00rootroot00000000000000/* Copyright (C) 2015 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #include "device.h" #include "../common/emulproto.pb.h" #include "devicemanager.h" #include "packetbuffer.h" #include #include const int kBaseHex = 16; const quint16 kEthTypeIp4 = 0x0800; const quint16 kEthTypeIp6 = 0x86dd; const int kIp6HdrLen = 40; /* * NOTE: * 1. Device Key is (VLANS [without prio/cfi] + MAC) * - is assumed to be unique for a device * 2. Device clients/users (viz. DeviceManager) should take care when * setting params that change the key, if the key is used elsewhere * (e.g. in a hash) */ Device::Device(DeviceManager *deviceManager) { deviceManager_ = deviceManager; for (int i = 0; i < kMaxVlan; i++) vlan_[i] = 0; numVlanTags_ = 0; mac_ = 0; hasIp4_ = false; hasIp6_ = false; clearKey(); } void Device::setVlan(int index, quint16 vlan, quint16 tpid) { int ofs; if ((index < 0) || (index >= kMaxVlan)) { qWarning("%s: vlan index %d out of range (0 - %d)", __FUNCTION__, index, kMaxVlan - 1); return; } vlan_[index] = (tpid << 16) | vlan; ofs = index * sizeof(quint16); key_[ofs] = (vlan >> 8) & 0x0f; // Vlan prio/cfi should not be part of key key_[ofs+1] = vlan & 0xff; if (index >= numVlanTags_) numVlanTags_ = index + 1; } quint64 Device::mac() { return mac_; } void Device::setMac(quint64 mac) { int ofs = kMaxVlan * sizeof(quint16); mac_ = mac & ~(0xffffULL << 48); qToBigEndian(mac_, (uchar*)key_.data()+ofs); } void Device::setIp4(quint32 address, int prefixLength, quint32 gateway) { ip4_ = address; ip4PrefixLength_ = prefixLength; ip4Gateway_ = gateway; hasIp4_ = true; // Precalculate our mask 'n subnet to avoid doing so at pkt rx/tx time ip4Mask_ = ~0U << (32 - ip4PrefixLength_); ip4Subnet_ = ip4_ & ip4Mask_; } void Device::setIp6(UInt128 address, int prefixLength, UInt128 gateway) { ip6_ = address; ip6PrefixLength_ = prefixLength; ip6Gateway_ = gateway; hasIp6_ = true; // Precalculate our mask 'n subnet to avoid doing so at pkt rx/tx time ip6Mask_ = ~UInt128(0, 0) << (128 - ip6PrefixLength_); ip6Subnet_ = ip6_ & ip6Mask_; } void Device::getConfig(OstEmul::Device *deviceConfig) { for (int i = 0; i < numVlanTags_; i++) deviceConfig->add_vlan(vlan_[i]); deviceConfig->set_mac(mac_); if (hasIp4_) { deviceConfig->set_ip4(ip4_); deviceConfig->set_ip4_prefix_length(ip4PrefixLength_); deviceConfig->set_ip4_default_gateway(ip4Gateway_); } if (hasIp6_) { deviceConfig->mutable_ip6()->set_hi(ip6_.hi64()); deviceConfig->mutable_ip6()->set_lo(ip6_.lo64()); deviceConfig->set_ip6_prefix_length(ip6PrefixLength_); deviceConfig->mutable_ip6_default_gateway()->set_hi(ip6Gateway_.hi64()); deviceConfig->mutable_ip6_default_gateway()->set_lo(ip6Gateway_.lo64()); } } QString Device::config() { QString config; for (int i = 0; i < numVlanTags_; i++) { config.append(i == 0 ? "vlans=" : "|"); config.append( (vlan_[i] >> 16) != kVlanTpid ? QString("0x%1-%2") .arg(vlan_[i] >> 16, 4, kBaseHex, QChar('0')) .arg(vlan_[i] & 0xFFFF) : QString("%1") .arg(vlan_[i] & 0xFFFF)); } config.append(QString(" mac=%1") .arg(mac_, 12, kBaseHex, QChar('0'))); if (hasIp4_) config.append(QString(" ip4=%1/%2") .arg(QHostAddress(ip4_).toString()) .arg(ip4PrefixLength_)); if (hasIp6_) config.append(QString(" ip6=%1/%2") .arg(QHostAddress(ip6_.toArray()).toString()) .arg(ip6PrefixLength_)); return config; } DeviceKey Device::key() { return key_; } void Device::clearKey() { key_.fill(0, kMaxVlan * sizeof(quint16) + sizeof(quint64)); } /* void Device::receivePacket(PacketBuffer *pktBuf) { // XXX: Pure Virtual: Subclass should implement // We expect pktBuf to point to EthType on entry } */ void Device::transmitPacket(PacketBuffer *pktBuf) { deviceManager_->transmitPacket(pktBuf); } void Device::resolveGateway() { if (hasIp4_ && ip4Gateway_ && !isResolved(ip4Gateway_)) sendArpRequest(ip4Gateway_); if (hasIp6_ && (ip6Gateway_ != 0) && !isResolved(ip6Gateway_)) sendNeighborSolicit(ip6Gateway_); } // Resolve the Neighbor IP address for this to-be-transmitted pktBuf // We expect pktBuf to point to EthType on entry void Device::resolveNeighbor(PacketBuffer *pktBuf) { quint16 ethType = qFromBigEndian(pktBuf->data()); pktBuf->pull(2); qDebug("%s: ethType 0x%x", __PRETTY_FUNCTION__, ethType); switch(ethType) { case kEthTypeIp4: // IPv4 if (hasIp4_) sendArpRequest(pktBuf); break; case kEthTypeIp6: // IPv6 if (hasIp6_) sendNeighborSolicit(pktBuf); break; default: break; } // FIXME: temporary hack till DeviceManager clones pbufs pktBuf->push(2); } /* void Device::clearNeighbors(Device::NeighborSet set) { // XXX: Pure virtual: Subclass should implement } // Append this device's neighbors to the list void Device::getNeighbors(OstEmul::DeviceNeighborList* neighbors) { // XXX: Subclass should implement } */ // Are we the source of the given packet? // We expect pktBuf to point to EthType on entry bool Device::isOrigin(const PacketBuffer *pktBuf) { const uchar *pktData = pktBuf->data(); quint16 ethType = qFromBigEndian(pktData); qDebug("%s: ethType 0x%x", __PRETTY_FUNCTION__, ethType); pktData += 2; // We know only about IP packets - adjust for ethType length (2 bytes) // when checking that we have a complete IP header if ((ethType == kEthTypeIp4) && hasIp4_) { // IPv4 int ipHdrLen = (pktData[0] & 0x0F) << 2; quint32 srcIp; if (pktBuf->length() < (ipHdrLen+2)) { qDebug("incomplete IPv4 header: expected %d, actual %d", ipHdrLen, pktBuf->length()); return false; } srcIp = qFromBigEndian(pktData + ipHdrLen - 8); qDebug("%s: pktSrcIp/selfIp = 0x%x/0x%x", __FUNCTION__, srcIp, ip4_); return (srcIp == ip4_); } else if ((ethType == kEthTypeIp6) && hasIp6_) { // IPv6 UInt128 srcIp; if (pktBuf->length() < (kIp6HdrLen+2)) { qDebug("incomplete IPv6 header: expected %d, actual %d", kIp6HdrLen, pktBuf->length()-2); return false; } srcIp = qFromBigEndian(pktData + 8); qDebug("%s: pktSrcIp6/selfIp6 = %llx-%llx/%llx-%llx", __FUNCTION__, srcIp.hi64(), srcIp.lo64(), ip6_.hi64(), ip6_.lo64()); return (srcIp == ip6_); } return false; } // Return the mac address corresponding to the dstIp of the given packet // We expect pktBuf to point to EthType on entry quint64 Device::neighborMac(const PacketBuffer *pktBuf) { quint64 mac = 0; const uchar *pktData = pktBuf->data(); quint16 ethType = qFromBigEndian(pktData); qDebug("%s: ethType 0x%x", __PRETTY_FUNCTION__, ethType); pktData += 2; // We know only about IP packets if ((ethType == kEthTypeIp4) && hasIp4_) { // IPv4 int ipHdrLen = (pktData[0] & 0x0F) << 2; quint32 dstIp, tgtIp; if (pktBuf->length() < ipHdrLen) { qDebug("incomplete IPv4 header: expected %d, actual %d", ipHdrLen, pktBuf->length()); return mac; } dstIp = qFromBigEndian(pktData + ipHdrLen - 4); if ((dstIp & 0xF0000000) == 0xE0000000) { // Mcast IP? mac = (quint64(0x01005e) << 24) | (dstIp & 0x7FFFFF); qDebug("mcast dst %08x, mac: %012llx", dstIp, mac); } else { tgtIp = ((dstIp & ip4Mask_) == ip4Subnet_) ? dstIp : ip4Gateway_; mac = arpLookup(tgtIp); qDebug("tgtIp: %08x, mac: %012llx", tgtIp, mac); } } else if ((ethType == kEthTypeIp6) && hasIp6_) { // IPv6 UInt128 dstIp, tgtIp; if (pktBuf->length() < (kIp6HdrLen+2)) { qDebug("incomplete IPv6 header: expected %d, actual %d", kIp6HdrLen, pktBuf->length()-2); return mac; } dstIp = qFromBigEndian(pktData + 24); if (dstIp.toArray()[0] == 0xFF) { // Mcast IP? mac = (quint64(0x3333) << 32) | (dstIp.lo64() & 0xFFFFFFFF); qDebug("mcast dst %s, mac: %012llx", qPrintable(QHostAddress(dstIp.toArray()).toString()), mac); } else { tgtIp = ((dstIp & ip6Mask_) == ip6Subnet_) ? dstIp : ip6Gateway_; mac = ndpLookup(tgtIp); qDebug("tgtIp %s, mac: %012llx", qPrintable(QHostAddress(dstIp.toArray()).toString()), mac); } } return mac; } /* quint64 Device::arpLookup(quint32 ip) { // XXX: Pure virtual: Subclass should implement } quint64 Device::ndpLookup(UInt128 ip) { // XXX: Pure virtual: Subclass should implement } */ // Send ARP request for the IPv4 packet in pktBuf // pktBuf points to start of IP header void Device::sendArpRequest(PacketBuffer *pktBuf) { uchar *pktData = pktBuf->data(); int ipHdrLen = (pktData[0] & 0x0F) << 2; quint32 dstIp, tgtIp; if (pktBuf->length() < ipHdrLen) { qDebug("incomplete IPv4 header: expected %d, actual %d", ipHdrLen, pktBuf->length()); return; } dstIp = qFromBigEndian(pktData + ipHdrLen - 4); tgtIp = ((dstIp & ip4Mask_) == ip4Subnet_) ? dstIp : ip4Gateway_; if (!isResolved(tgtIp)) sendArpRequest(tgtIp); } // Send NS for the IPv6 packet in pktBuf // caller is responsible to check that pktBuf originates from this device // pktBuf should point to start of IP header void Device::sendNeighborSolicit(PacketBuffer *pktBuf) { uchar *pktData = pktBuf->data(); UInt128 dstIp, tgtIp; if (pktBuf->length() < kIp6HdrLen) { qDebug("incomplete IPv6 header: expected %d, actual %d", kIp6HdrLen, pktBuf->length()); return; } dstIp = qFromBigEndian(pktData + 24); tgtIp = ((dstIp & ip6Mask_) == ip6Subnet_) ? dstIp : ip6Gateway_; if (!isResolved(tgtIp)) sendNeighborSolicit(tgtIp); } bool Device::isResolved(quint32 ip) { return arpLookup(ip) > 0; } bool Device::isResolved(UInt128 ip) { return ndpLookup(ip) > 0; } // // Protected Methods // /* void Device::sendArpRequest(quint32 tgtIp) { // XXX: Pure virtual: Subclass should implement } void Device::sendNeighborSolicit(UInt128 tgtIp) { // XXX: Pure virtual: Subclass should implement } */ bool operator<(const DeviceKey &a1, const DeviceKey &a2) { int i = 0; while (i < a1.size()) { if (uchar(a1.at(i)) < uchar(a2.at(i))) return true; if (uchar(a1.at(i)) > uchar(a2.at(i))) return false; i++; } return false; } ostinato-1.3.0/server/device.h000066400000000000000000000055461451413623100163030ustar00rootroot00000000000000/* Copyright (C) 2015 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _DEVICE_H #define _DEVICE_H #include "../common/emulproto.pb.h" #include "../common/protocol.pb.h" #include "../common/uint128.h" #include #include #include class DeviceManager; class PacketBuffer; class DeviceKey: public QByteArray { }; class Device { public: static const quint16 kVlanTpid = 0x8100; enum NeighborSet { kAllNeighbors, kUnresolvedNeighbors }; public: Device(DeviceManager *deviceManager); virtual ~Device() = default; void setVlan(int index, quint16 vlan, quint16 tpid = kVlanTpid); quint64 mac(); void setMac(quint64 mac); void setIp4(quint32 address, int prefixLength, quint32 gateway); void setIp6(UInt128 address, int prefixLength, UInt128 gateway); void getConfig(OstEmul::Device *deviceConfig); QString config(); DeviceKey key(); void clearKey(); virtual void receivePacket(PacketBuffer *pktBuf) = 0; void transmitPacket(PacketBuffer *pktBuf); void resolveGateway(); void resolveNeighbor(PacketBuffer *pktBuf); virtual void clearNeighbors(Device::NeighborSet set) = 0; virtual void getNeighbors(OstEmul::DeviceNeighborList *neighbors) = 0; bool isOrigin(const PacketBuffer *pktBuf); quint64 neighborMac(const PacketBuffer *pktBuf); protected: // methods virtual quint64 arpLookup(quint32 ip) = 0; virtual quint64 ndpLookup(UInt128 ip) = 0; virtual void sendArpRequest(quint32 tgtIp) = 0; virtual void sendNeighborSolicit(UInt128 tgtIp) = 0; protected: // data static const int kMaxVlan = 4; DeviceManager *deviceManager_; int numVlanTags_; quint32 vlan_[kMaxVlan]; quint64 mac_; bool hasIp4_; quint32 ip4_; int ip4PrefixLength_; quint32 ip4Gateway_; quint32 ip4Mask_; quint32 ip4Subnet_; bool hasIp6_; UInt128 ip6_; int ip6PrefixLength_; UInt128 ip6Gateway_; UInt128 ip6Mask_; UInt128 ip6Subnet_; DeviceKey key_; private: // methods void sendArpRequest(PacketBuffer *pktBuf); void sendNeighborSolicit(PacketBuffer *pktBuf); bool isResolved(quint32 ip); bool isResolved(UInt128 ip); }; bool operator<(const DeviceKey &a1, const DeviceKey &a2); #endif ostinato-1.3.0/server/devicemanager.cpp000066400000000000000000000410551451413623100201640ustar00rootroot00000000000000/* Copyright (C) 2015 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #include "devicemanager.h" #include "abstractport.h" #include "emuldevice.h" #include "../common/emulation.h" #include "hostdevice.h" #include "interfaceinfo.h" #include "nulldevice.h" #include "packetbuffer.h" #include "../common/emulproto.pb.h" #include const quint64 kBcastMac = 0xffffffffffffULL; inline UInt128 UINT128(OstEmul::Ip6Address x) { return UInt128(x.hi(), x.lo()); } inline bool isMacMcast(quint64 mac) { return ((mac >> 40) & 0x01) == 0x01; } // XXX: Port owning DeviceManager already uses locks, so we don't use any // locks within DeviceManager to protect deviceGroupList_ et.al. DeviceManager::DeviceManager(AbstractPort *parent) { port_ = parent; } void DeviceManager::createHostDevices(void) { const InterfaceInfo *ifInfo = port_->interfaceInfo(); if (!ifInfo) return; Device *device = HostDevice::create(port_->name(), this); device->setMac(ifInfo->mac); int count = ifInfo->ip4.size(); for (int i = 0; i < count; i++) { // XXX: Since we can't support multiple IPs with same mac, // skip link-local IP - unless it is the only one if (((ifInfo->ip4.at(i).address & 0xffff0000) == 0xa9fe0000) && (count > 1)) continue; device->setIp4(ifInfo->ip4.at(i).address, ifInfo->ip4.at(i).prefixLength, ifInfo->ip4.at(i).gateway); break; // TODO: support multiple IPs with same mac } count = ifInfo->ip6.size(); for (int i = 0; i < count; i++) { // XXX: Since we can't support multiple IPs with same mac, // skip link-local IP - unless it is the only one if (((ifInfo->ip6.at(i).address.hi64() >> 48) == 0xfe80) && (count > 1)) continue; device->setIp6(ifInfo->ip6.at(i).address, ifInfo->ip6.at(i).prefixLength, ifInfo->ip6.at(i).gateway); break; // TODO: support multiple IPs with same mac } if (deviceList_.contains(device->key())) { qWarning("%s: error adding host device %s (EEXIST)", __FUNCTION__, qPrintable(device->config())); delete device; return; } hostDeviceList_.append(device); insertDevice(device->key(), device); qDebug("host(add): %s", qPrintable(device->config())); } DeviceManager::~DeviceManager() { // Delete *all* devices - host and enumerated foreach(Device *dev, deviceList_) delete dev; foreach(OstProto::DeviceGroup *devGrp, deviceGroupList_) delete devGrp; } int DeviceManager::deviceGroupCount() { return deviceGroupList_.size(); } const OstProto::DeviceGroup* DeviceManager::deviceGroupAtIndex(int index) { if ((index < 0) || (index >= deviceGroupCount())) { qWarning("%s: index %d out of range (0 - %d)", __FUNCTION__, index, deviceGroupCount() - 1); return NULL; } // Sort List by 'id', get the id at 'index' and then corresponding devGrp return deviceGroupList_.value(deviceGroupList_.uniqueKeys().value(index)); } const OstProto::DeviceGroup* DeviceManager::deviceGroup(uint deviceGroupId) { return deviceGroupList_.value(deviceGroupId); } bool DeviceManager::addDeviceGroup(uint deviceGroupId) { OstProto::DeviceGroup *deviceGroup; if (deviceGroupList_.contains(deviceGroupId)) { qWarning("%s: deviceGroup id %u already exists", __FUNCTION__, deviceGroupId); return false; } deviceGroup = newDeviceGroup(port_->id()); deviceGroup->mutable_device_group_id()->set_id(deviceGroupId); deviceGroupList_.insert(deviceGroupId, deviceGroup); enumerateDevices(deviceGroup, kAdd); // Start emulation when first device group is added // NOTE: Host devices don't have a deviceGroup and don't need emulation if ((deviceGroupCount() == 1) && port_) port_->startDeviceEmulation(); return true; } bool DeviceManager::deleteDeviceGroup(uint deviceGroupId) { OstProto::DeviceGroup *deviceGroup; if (!deviceGroupList_.contains(deviceGroupId)) { qWarning("%s: deviceGroup id %u does not exist", __FUNCTION__, deviceGroupId); return false; } deviceGroup = deviceGroupList_.take(deviceGroupId); enumerateDevices(deviceGroup, kDelete); delete deviceGroup; // Stop emulation if no device groups remain // NOTE: Host devices don't have a deviceGroup and don't need emulation if ((deviceGroupCount() == 0) && port_) port_->stopDeviceEmulation(); return true; } bool DeviceManager::modifyDeviceGroup(const OstProto::DeviceGroup *deviceGroup) { quint32 id = deviceGroup->device_group_id().id(); OstProto::DeviceGroup *myDeviceGroup = deviceGroupList_.value(id); if (!myDeviceGroup) { qWarning("%s: deviceGroup id %u does not exist", __FUNCTION__, id); return false; } enumerateDevices(myDeviceGroup, kDelete); myDeviceGroup->CopyFrom(*deviceGroup); // If mac step is 0, silently override to 1 - otherwise we won't have // unique DeviceKeys if (myDeviceGroup->GetExtension(OstEmul::mac).step() == 0) myDeviceGroup->MutableExtension(OstEmul::mac)->set_step(1); // Default value for ip6 step should be 1 (not 0) if (myDeviceGroup->HasExtension(OstEmul::ip6) && !myDeviceGroup->GetExtension(OstEmul::ip6).has_step()) myDeviceGroup->MutableExtension(OstEmul::ip6) ->mutable_step()->set_lo(1); enumerateDevices(myDeviceGroup, kAdd); return true; } int DeviceManager::deviceCount() { return deviceList_.size(); } void DeviceManager::getDeviceList( OstProto::PortDeviceList *deviceList) { foreach(Device *device, sortedDeviceList_) { OstEmul::Device *dev = deviceList->AddExtension(OstEmul::device); device->getConfig(dev); } } void DeviceManager::receivePacket(PacketBuffer *pktBuf) { QMutexLocker locker(&listLock_); uchar *pktData = pktBuf->data(); int offset = 0; EmulDevice dk(this); Device *device; quint64 dstMac; quint16 ethType; quint16 vlan; int idx = 0; // We assume pkt is ethernet // TODO: extend for other link layer types // All frames we are interested in should be at least 32 bytes if (pktBuf->length() < 32) { qWarning("short frame of %d bytes, skipping ...", pktBuf->length()); goto _exit; } // Extract dstMac dstMac = qFromBigEndian(pktData + offset); offset += 4; dstMac = (dstMac << 16) | qFromBigEndian(pktData + offset); qDebug("dstMac %012llx", dstMac); // XXX: Treat multicast as bcast if (isMacMcast(dstMac)) dstMac = kBcastMac; dk.setMac(dstMac); offset += 2; // Skip srcMac - don't care offset += 6; _eth_type: // Extract EthType ethType = qFromBigEndian(pktData + offset); qDebug("%s: ethType 0x%x", __PRETTY_FUNCTION__, ethType); if (tpidList_.contains(ethType)) { offset += 2; vlan = qFromBigEndian(pktData + offset); dk.setVlan(idx++, vlan); offset += 2; qDebug("%s: idx: %d vlan: 0x%04x/%d", __FUNCTION__, idx, vlan, vlan & 0x0fff); goto _eth_type; } pktBuf->pull(offset); if (dstMac == kBcastMac) { QList list = bcastList_.values(dk.key()); // FIXME: We need to clone the pktBuf before passing to each // device, otherwise only the first device gets the original // packet - all subsequent ones get the modified packet! // NOTE: modification may not be in the pkt data buffer but // in the HDTE pointers - which is bad as well! foreach(Device *device, list) device->receivePacket(pktBuf); goto _exit; } // Is it destined for us? device = deviceList_.value(dk.key()); if (!device) { qDebug("%s: dstMac %012llx is not us", __FUNCTION__, dstMac); goto _exit; } device->receivePacket(pktBuf); _exit: delete pktBuf; } void DeviceManager::transmitPacket(PacketBuffer *pktBuf) { port_->sendEmulationPacket(pktBuf); } void DeviceManager::resolveDeviceGateways() { foreach(Device *device, deviceList_) { device->resolveGateway(); } } void DeviceManager::clearDeviceNeighbors(Device::NeighborSet set) { foreach(Device *device, deviceList_) device->clearNeighbors(set); } void DeviceManager::getDeviceNeighbors( OstProto::PortNeighborList *neighborList) { int count = 0; foreach(Device *device, sortedDeviceList_) { OstEmul::DeviceNeighborList *neighList = neighborList->AddExtension(OstEmul::device_neighbor); neighList->set_device_index(count++); device->getNeighbors(neighList); } } void DeviceManager::resolveDeviceNeighbor(PacketBuffer *pktBuf) { Device *device = originDevice(pktBuf); if (device) device->resolveNeighbor(pktBuf); } quint64 DeviceManager::deviceMacAddress(PacketBuffer *pktBuf) { Device *device = originDevice(pktBuf); return device ? device->mac() : 0; } quint64 DeviceManager::neighborMacAddress(PacketBuffer *pktBuf) { Device *device = originDevice(pktBuf); return device ? device->neighborMac(pktBuf) : 0; } // ------------------------------------ // // Private Methods // ------------------------------------ // Device* DeviceManager::originDevice(PacketBuffer *pktBuf) { uchar *pktData = pktBuf->data(); int offset = 12; // start parsing after mac addresses EmulDevice dk(this); quint16 ethType; quint16 vlan; int idx = 0; // Do we have any devices at all? if (!deviceCount()) return NULL; // pktBuf will not have the correct dstMac populated, so use bcastMac // and search for device by IP dk.setMac(kBcastMac); _eth_type: ethType = qFromBigEndian(pktData + offset); qDebug("%s: ethType 0x%x", __PRETTY_FUNCTION__, ethType); if (tpidList_.contains(ethType)) { offset += 2; vlan = qFromBigEndian(pktData + offset); dk.setVlan(idx++, vlan); offset += 2; qDebug("%s: idx: %d vlan: 0x%04x/%d", __FUNCTION__, idx, vlan, vlan & 0x0fff); goto _eth_type; } pktBuf->pull(offset); foreach(Device *device, bcastList_.values(dk.key())) { if (device->isOrigin(pktBuf)) return device; } qDebug("couldn't find origin device for packet"); return NULL; } void DeviceManager::enumerateDevices( const OstProto::DeviceGroup *deviceGroup, Operation oper) { QMutexLocker locker(&listLock_); EmulDevice dk(this); OstEmul::VlanEmulation pbVlan = deviceGroup->encap() .GetExtension(OstEmul::vlan); int numTags = pbVlan.stack_size(); int n = 1; QList vlanCount; bool hasIp4 = deviceGroup->HasExtension(OstEmul::ip4); bool hasIp6 = deviceGroup->HasExtension(OstEmul::ip6); OstEmul::MacEmulation mac = deviceGroup->GetExtension(OstEmul::mac); OstEmul::Ip4Emulation ip4 = deviceGroup->GetExtension(OstEmul::ip4); OstEmul::Ip6Emulation ip6 = deviceGroup->GetExtension(OstEmul::ip6); /* * vlanCount[] stores the number of unique vlans at each tag level * e.g. for a 3-tag config with 2, 3, 4 vlans at each level respectively * vlanCount = [24, 12, 4] * 0 - 0, 0, 0 * 1 - 0, 0, 1 * 2 - 0, 0, 2 * 3 - 0, 0, 3 * 4 - 0, 1, 0 * 5 - 0, 1, 1 * 6 - 0, 1, 2 * 7 - 0, 1, 3 * 8 - 0, 2, 0 * 9 - 0, 2, 1 * 10 - 0, 2, 2 * 11 - 0, 2, 3 * 12 - 1, 0, 0 * 13 - 1, 0, 1 * 14 - 1, 0, 2 * 15 - 1, 0, 3 * 16 - 1, 1, 0 * 17 - 1, 1, 1 * 18 - 1, 1, 2 * 19 - 1, 1, 3 * 21 - 1, 2, 0 * 21 - 1, 2, 1 * 22 - 1, 2, 2 * 23 - 1, 2, 3 * * Note that vlanCount[0] repesents total-number-of-vlans * * Another way to think about this is that at a particular vlan tag * level, we need to repeat a particular vlan-id as many times as the * next level's count before we can increment the vlan-id at that level * * We use this list to calculate the vlan ids for each tag level for * all the vlans. * * For implementation convenience we append a '1' as the last element */ vlanCount.append(n); for (int i = numTags - 1; i >= 0 ; i--) { OstEmul::VlanEmulation::Vlan vlan = pbVlan.stack(i); n *= vlan.count(); vlanCount.prepend(n); // Update TPID list switch (oper) { case kAdd: tpidList_[vlan.tpid()]++; break; case kDelete: tpidList_[vlan.tpid()]--; if (tpidList_[vlan.tpid()] == 0) tpidList_.remove(vlan.tpid()); break; default: Q_ASSERT(0); // Unreachable } } QHash::const_iterator iter = tpidList_.constBegin(); qDebug("Port %s TPID List:", port_->name()); while (iter != tpidList_.constEnd()) { qDebug("tpid: %x (%d)", iter.key(), iter.value()); iter++; } for (int i = 0; i < vlanCount.at(0); i++) { for (int j = 0; j < numTags; j++) { OstEmul::VlanEmulation::Vlan vlan = pbVlan.stack(j); quint16 vlanAdd = (i/vlanCount.at(j+1) % vlan.count())*vlan.step(); dk.setVlan(j, vlan.vlan_tag() + vlanAdd, vlan.tpid()); } for (uint k = 0; k < deviceGroup->device_count(); k++) { Device *device; quint64 macAdd = k * mac.step(); quint32 ip4Add = k * ip4.step(); UInt128 ip6Add = UINT128(ip6.step()) * k; dk.setMac(mac.address() + macAdd); if (hasIp4) dk.setIp4(ip4.address() + ip4Add, ip4.prefix_length(), ip4.default_gateway()); if (hasIp6) dk.setIp6(UINT128(ip6.address()) + ip6Add, ip6.prefix_length(), UINT128(ip6.default_gateway())); switch (oper) { case kAdd: if (deviceList_.contains(dk.key())) { qWarning("%s: error adding device %s (EEXIST)", __FUNCTION__, qPrintable(dk.config())); break; } device = new EmulDevice(this); *device = dk; insertDevice(dk.key(), device); qDebug("enumerate(add): %p %s", device, qPrintable(device->config())); break; case kDelete: device = deviceList_.value(dk.key()); if (!device) { qWarning("%s: error deleting device %s (NOTFOUND)", __FUNCTION__, qPrintable(dk.config())); break; } qDebug("enumerate(del): %p %s", device, qPrintable(device->config())); deleteDevice(dk.key()); break; default: Q_ASSERT(0); // Unreachable } } // foreach device } // foreach vlan } bool DeviceManager::insertDevice(DeviceKey key, Device *device) { deviceList_.insert(key, device); sortedDeviceList_.insert(key, device); NullDevice bcastDevice = *(static_cast(device)); bcastDevice.setMac(kBcastMac); bcastList_.insert(bcastDevice.key(), device); return true; } bool DeviceManager::deleteDevice(DeviceKey key) { Device *device = deviceList_.take(key); if (!device) return false; sortedDeviceList_.take(key); NullDevice bcastDevice = *(static_cast(device)); bcastDevice.setMac(kBcastMac); bcastList_.take(bcastDevice.key()); delete device; return true; } ostinato-1.3.0/server/devicemanager.h000066400000000000000000000051301451413623100176230ustar00rootroot00000000000000/* Copyright (C) 2015 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _DEVICE_MANAGER_H #define _DEVICE_MANAGER_H #include "device.h" #include #include #include #include #include class AbstractPort; class PacketBuffer; namespace OstProto { class DeviceGroup; }; class DeviceManager { public: DeviceManager(AbstractPort *parent = 0); ~DeviceManager(); void createHostDevices(); int deviceGroupCount(); const OstProto::DeviceGroup* deviceGroupAtIndex(int index); const OstProto::DeviceGroup* deviceGroup(uint deviceGroupId); bool addDeviceGroup(uint deviceGroupId); bool deleteDeviceGroup(uint deviceGroupId); bool modifyDeviceGroup(const OstProto::DeviceGroup *deviceGroup); int deviceCount(); void getDeviceList(OstProto::PortDeviceList *deviceList); void receivePacket(PacketBuffer *pktBuf); void transmitPacket(PacketBuffer *pktBuf); void resolveDeviceGateways(); void clearDeviceNeighbors(Device::NeighborSet set = Device::kAllNeighbors); void resolveDeviceNeighbor(PacketBuffer *pktBuf); void getDeviceNeighbors(OstProto::PortNeighborList *neighborList); quint64 deviceMacAddress(PacketBuffer *pktBuf); quint64 neighborMacAddress(PacketBuffer *pktBuf); private: enum Operation { kAdd, kDelete }; Device* originDevice(PacketBuffer *pktBuf); void enumerateDevices( const OstProto::DeviceGroup *deviceGroup, Operation oper); bool insertDevice(DeviceKey key, Device *device); bool deleteDevice(DeviceKey key); AbstractPort *port_; QMutex listLock_; // protects all the lists QHash deviceGroupList_; QHash deviceList_; // fast access to devices QMap sortedDeviceList_; // sorted access to devices QMultiHash bcastList_; QHash tpidList_; // Key: TPID, Value: RefCount QList hostDeviceList_; }; #endif ostinato-1.3.0/server/drone.cpp000066400000000000000000000046541451413623100165050ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #include "drone.h" #include "myservice.h" #include "params.h" #include "rpcserver.h" #include "settings.h" #include "../common/updater.h" #include extern Params appParams; extern const char* version; extern const char* revision; Drone::Drone(QObject *parent) : QObject(parent) { Updater *updater = new Updater(); #ifdef QT_DEBUG bool enableLogs = true; #else bool enableLogs = !appParams.optLogsDisabled(); #endif rpcServer = new RpcServer(enableLogs); service = new MyService(); connect(updater, SIGNAL(newVersionAvailable(QString)), this, SLOT(onNewVersion(QString))); updater->checkForNewVersion(); } Drone::~Drone() { delete rpcServer; delete service; } bool Drone::init() { QString addr = appSettings->value(kRpcServerAddress).toString(); QHostAddress address = addr.isEmpty() ? QHostAddress::Any : QHostAddress(addr); Q_ASSERT(rpcServer); qRegisterMetaType("SharedProtobufMessage"); if (address.isNull()) { qWarning("Invalid RpcServer Address <%s> specified. Using 'Any'", qPrintable(addr)); address = QHostAddress::Any; } if (!rpcServer->registerService(service, address, appParams.servicePortNumber())) { //qCritical(qPrintable(rpcServer->errorString())); return false; } connect(service, SIGNAL(notification(int, SharedProtobufMessage)), rpcServer, SIGNAL(notifyClients(int, SharedProtobufMessage))); return true; } MyService* Drone::rpcService() { return service; } void Drone::onNewVersion(QString newVersion) { qWarning("%s", qPrintable(QString("New Ostinato version %1 available. " "Visit http://ostinato.org to download").arg(newVersion))); } ostinato-1.3.0/server/drone.h000066400000000000000000000020501451413623100161360ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _DRONE_H #define _DRONE_H #include class RpcServer; class MyService; class Drone : public QObject { Q_OBJECT public: Drone(QObject *parent = 0); ~Drone(); bool init(); MyService* rpcService(); private slots: void onNewVersion(QString version); private: RpcServer *rpcServer; MyService *service; }; #endif ostinato-1.3.0/server/drone.pro000066400000000000000000000040101451413623100165050ustar00rootroot00000000000000TEMPLATE = app CONFIG += qt ver_info c++11 addon: CONFIG -= ver_info QT += network script xml QT -= gui linux*:system(grep -q IFLA_STATS64 /usr/include/linux/if_link.h): \ DEFINES += HAVE_IFLA_STATS64 INCLUDEPATH += "../common" INCLUDEPATH += "../rpc" win32 { # Support Windows Vista and above only DEFINES += WIN32_LEAN_AND_MEAN NTDDI_VERSION=0x06000000 _WIN32_WINNT=0x0600 DEFINES += HAVE_REMOTE WPCAP CONFIG += console QMAKE_LFLAGS += -static LIBS += -lwpcap -lpacket -liphlpapi CONFIG(debug, debug|release) { LIBS += -L"../common/debug" -lostproto LIBS += -L"../rpc/debug" -lpbrpc POST_TARGETDEPS += \ "../common/debug/libostproto.a" \ "../rpc/debug/libpbrpc.a" } else { LIBS += -L"../common/release" -lostproto LIBS += -L"../rpc/release" -lpbrpc POST_TARGETDEPS += \ "../common/release/libostproto.a" \ "../rpc/release/libpbrpc.a" } } else { LIBS += -lpcap LIBS += -L"../common" -lostproto LIBS += -L"../rpc" -lpbrpc POST_TARGETDEPS += "../common/libostproto.a" "../rpc/libpbrpc.a" } linux { INCLUDEPATH += "/usr/include/libnl3" LIBS += -lnl-3 -lnl-route-3 } LIBS += -lm LIBS += -lprotobuf HEADERS += drone.h \ pcaptransmitter.h \ myservice.h \ streamtiming.h SOURCES += \ devicemanager.cpp \ device.cpp \ emuldevice.cpp \ drone_main.cpp \ drone.cpp \ portmanager.cpp \ abstractport.cpp \ pcapport.cpp \ pcapsession.cpp \ pcaptransmitter.cpp \ pcaprxstats.cpp \ pcaptxstats.cpp \ pcaptxthread.cpp \ pcaptxttagstats.cpp \ bsdhostdevice.cpp \ bsdport.cpp \ linuxhostdevice.cpp \ linuxport.cpp \ linuxutils.cpp \ params.cpp \ streamtiming.cpp \ turbo.cpp \ winhostdevice.cpp \ winpcapport.cpp SOURCES += myservice.cpp SOURCES += pcapextra.cpp SOURCES += packetbuffer.cpp QMAKE_DISTCLEAN += object_script.* include (../install.pri) include (../version.pri) include (../options.pri) ostinato-1.3.0/server/drone_main.cpp000066400000000000000000000075161451413623100175110ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #include "drone.h" #include "../common/protocolmanager.h" #include "params.h" #include "settings.h" #include "turbo.h" #include #include #include #include extern ProtocolManager *OstProtocolManager; extern char *version; extern char *revision; Drone *drone; QSettings *appSettings; Params appParams; void NoMsgHandler(QtMsgType type, const QMessageLogContext &context, const QString &msg); void cleanup(int /*signum*/) { fprintf(stderr, "\nCleaning up (may take a few seconds) ... "); fflush(stderr); QCoreApplication::instance()->exit(-1); } int main(int argc, char *argv[]) { int exitCode = 0; QCoreApplication app(argc, argv); app.setApplicationName("Drone"); app.setOrganizationName("Ostinato"); appParams.parseCommandLine(argc, argv); fprintf(stderr, "Starting (will take a few seconds) ...\n"); fflush(stderr); #ifdef QT_NO_DEBUG if (appParams.optLogsDisabled()) qInstallMessageHandler(NoMsgHandler); #endif qDebug("Version: %s", version); qDebug("Revision: %s", revision); /* (Portable Mode) If we have a .ini file in the same directory as the executable, we use that instead of the platform specific location and format for the settings */ QString portableIni = QCoreApplication::applicationDirPath() + "/drone.ini"; if (QFile::exists(portableIni)) appSettings = new QSettings(portableIni, QSettings::IniFormat); else appSettings = new QSettings(QSettings::IniFormat, QSettings::UserScope, app.organizationName(), app.applicationName().toLower()); qDebug("Settings: %s", qPrintable(appSettings->fileName())); if (!initTurbo()) { exitCode = 1; goto _exit2; } drone = new Drone(); OstProtocolManager = new ProtocolManager(); if (!drone->init()) { exitCode = 1; goto _exit; } qDebug("Version: %s", version); qDebug("Revision: %s", revision); #ifdef Q_OS_UNIX struct sigaction sa; memset(&sa, 0, sizeof(sa)); sa.sa_handler = cleanup; if (sigaction(SIGTERM, &sa, NULL)) qDebug("Failed to install SIGTERM handler. Cleanup may not happen!!!"); if (sigaction(SIGINT, &sa, NULL)) qDebug("Failed to install SIGINT handler. Cleanup may not happen!!!"); #elif defined(Q_OS_WIN32) if (signal(SIGTERM, cleanup) == SIG_ERR) qDebug("Failed to install SIGTERM handler. Cleanup may not happen!!!"); if (signal(SIGINT, cleanup) == SIG_ERR) qDebug("Failed to install SIGINT handler. Cleanup may not happen!!!"); #endif exitCode = app.exec(); _exit: delete drone; delete OstProtocolManager; _exit2: google::protobuf::ShutdownProtobufLibrary(); fprintf(stderr, "done.\n"); fflush(stderr); return exitCode; } void NoMsgHandler(QtMsgType type, const QMessageLogContext &/*context*/, const QString &msg) { if (type == QtFatalMsg) { fprintf(stderr, "%s\n", qPrintable(msg)); fflush(stderr); abort(); } } ostinato-1.3.0/server/emuldevice.cpp000066400000000000000000000560161451413623100175170ustar00rootroot00000000000000/* Copyright (C) 2015 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #include "emuldevice.h" #include "devicemanager.h" #include "netdefs.h" #include "packetbuffer.h" #include quint32 sumUInt128(UInt128 value) { quint8 *arr = value.toArray(); quint32 sum = 0; for (int i = 0; i < 16; i += 2) sum += qToBigEndian(*((quint16*)(arr + i))); return sum; } inline bool isIp6Mcast(UInt128 ip) { return (ip.hi64() >> 56) == 0xff; } EmulDevice::EmulDevice(DeviceManager *deviceManager) : Device(deviceManager) { } int EmulDevice::encapSize() { Q_ASSERT(numVlanTags_ >= 0); // ethernet header + vlans int size = 14 + kMaxVlan*numVlanTags_; return size; } void EmulDevice::encap(PacketBuffer *pktBuf, quint64 dstMac, quint16 type) { int ofs; quint64 srcMac = mac_; uchar *p = pktBuf->push(encapSize()); if (!p) { qWarning("%s: failed to push %d bytes [0x%p, 0x%p]", __FUNCTION__, encapSize(), pktBuf->head(), pktBuf->data()); goto _exit; } *(quint32*)(p ) = qToBigEndian(quint32(dstMac >> 16)); *(quint16*)(p + 4) = qToBigEndian(quint16(dstMac & 0xffff)); *(quint32*)(p + 6) = qToBigEndian(quint32(srcMac >> 16)); *(quint16*)(p + 10) = qToBigEndian(quint16(srcMac & 0xffff)); ofs = 12; for (int i = 0; i < numVlanTags_; i++) { *(quint32*)(p + ofs) = qToBigEndian(vlan_[i]); ofs += 4; } *(quint16*)(p + ofs) = qToBigEndian(type); ofs += 2; Q_ASSERT(ofs == encapSize()); _exit: return; } // We expect pktBuf to point to EthType on entry void EmulDevice::receivePacket(PacketBuffer *pktBuf) { quint16 ethType = qFromBigEndian(pktBuf->data()); pktBuf->pull(2); qDebug("%s: ethType 0x%x", __PRETTY_FUNCTION__, ethType); switch(ethType) { case kEthTypeArp: if (hasIp4_) receiveArp(pktBuf); break; case kEthTypeIp4: if (hasIp4_) receiveIp4(pktBuf); break; case kEthTypeIp6: if (hasIp6_) receiveIp6(pktBuf); break; default: break; } // FIXME: temporary hack till DeviceManager clones pbufs pktBuf->push(2); } void EmulDevice::clearNeighbors(EmulDevice::NeighborSet set) { QMutableHashIterator arpIter(arpTable_); QMutableHashIterator ndpIter(ndpTable_); switch (set) { case kAllNeighbors: arpTable_.clear(); ndpTable_.clear(); break; case kUnresolvedNeighbors: while (arpIter.hasNext()) { arpIter.next(); if (arpIter.value() == 0) arpIter.remove(); } while (ndpIter.hasNext()) { ndpIter.next(); if (ndpIter.value() == 0) ndpIter.remove(); } break; default: Q_ASSERT(false); // Unreachable! } } // Append this device's neighbors to the list void EmulDevice::getNeighbors(OstEmul::DeviceNeighborList *neighbors) { QList ip4List = arpTable_.keys(); QList ip6List = ndpTable_.keys(); QList macList; macList = arpTable_.values(); Q_ASSERT(ip4List.size() == macList.size()); for (int i = 0; i < ip4List.size(); i++) { OstEmul::ArpEntry *arp = neighbors->add_arp(); arp->set_ip4(ip4List.at(i)); arp->set_mac(macList.at(i)); } macList = ndpTable_.values(); Q_ASSERT(ip6List.size() == macList.size()); for (int i = 0; i < ip6List.size(); i++) { OstEmul::NdpEntry *ndp = neighbors->add_ndp(); ndp->mutable_ip6()->set_hi(ip6List.at(i).hi64()); ndp->mutable_ip6()->set_lo(ip6List.at(i).lo64()); ndp->set_mac(macList.at(i)); } } // // Protected/Private Methods // /* * --------------------------------------------------------- * IPv4 related protected/private methods * --------------------------------------------------------- */ void EmulDevice::receiveArp(PacketBuffer *pktBuf) { PacketBuffer *rspPkt; uchar *pktData = pktBuf->data(); int offset = 0; quint16 hwType, protoType; quint8 hwAddrLen, protoAddrLen; quint16 opCode; quint64 srcMac, tgtMac; quint32 srcIp, tgtIp; // Extract tgtIp first to check quickly if this packet is for us or not tgtIp = qFromBigEndian(pktData + 24); if (tgtIp != ip4_) { qDebug("tgtIp %s is not me %s", qPrintable(QHostAddress(tgtIp).toString()), qPrintable(QHostAddress(ip4_).toString())); return; } // Extract annd verify ARP packet contents hwType = qFromBigEndian(pktData + offset); offset += 2; if (hwType != 1) // Mac goto _invalid_exit; protoType = qFromBigEndian(pktData + offset); offset += 2; if (protoType != kEthTypeIp4) goto _invalid_exit; hwAddrLen = pktData[offset]; offset += 1; if (hwAddrLen != 6) goto _invalid_exit; protoAddrLen = pktData[offset]; offset += 1; if (protoAddrLen != 4) goto _invalid_exit; opCode = qFromBigEndian(pktData + offset); offset += 2; srcMac = qFromBigEndian(pktData + offset); offset += 4; srcMac = (srcMac << 16) | qFromBigEndian(pktData + offset); offset += 2; srcIp = qFromBigEndian(pktData + offset); offset += 4; tgtMac = qFromBigEndian(pktData + offset); offset += 4; tgtMac = (tgtMac << 16) | qFromBigEndian(pktData + offset); offset += 2; switch (opCode) { case 1: // ARP Request arpTable_.insert(srcIp, srcMac); rspPkt = new PacketBuffer; rspPkt->reserve(encapSize()); pktData = rspPkt->put(28); if (pktData) { // HTYP, PTYP *(quint32*)(pktData ) = qToBigEndian(quint32(0x00010800)); // HLEN, PLEN, OPER *(quint32*)(pktData+ 4) = qToBigEndian(quint32(0x06040002)); // Source H/W Addr, Proto Addr *(quint32*)(pktData+ 8) = qToBigEndian(quint32(mac_ >> 16)); *(quint16*)(pktData+12) = qToBigEndian(quint16(mac_ & 0xffff)); *(quint32*)(pktData+14) = qToBigEndian(ip4_); // Target H/W Addr, Proto Addr *(quint32*)(pktData+18) = qToBigEndian(quint32(srcMac >> 16)); *(quint16*)(pktData+22) = qToBigEndian(quint16(srcMac & 0xffff)); *(quint32*)(pktData+24) = qToBigEndian(srcIp); } encap(rspPkt, srcMac, kEthTypeArp); transmitPacket(rspPkt); qDebug("Sent ARP Reply for srcIp/tgtIp=%s/%s", qPrintable(QHostAddress(srcIp).toString()), qPrintable(QHostAddress(tgtIp).toString())); break; case 2: // ARP Response arpTable_.insert(srcIp, srcMac); break; default: break; } return; _invalid_exit: qWarning("Invalid ARP content"); return; } quint64 EmulDevice::arpLookup(quint32 ip) { return arpTable_.value(ip); } quint64 EmulDevice::ndpLookup(UInt128 ip) { return ndpTable_.value(ip); } void EmulDevice::sendArpRequest(quint32 tgtIp) { quint32 srcIp = ip4_; PacketBuffer *reqPkt; uchar *pktData; // Validate target IP if (!tgtIp) return; reqPkt = new PacketBuffer; reqPkt->reserve(encapSize()); pktData = reqPkt->put(28); if (pktData) { // HTYP, PTYP *(quint32*)(pktData ) = qToBigEndian(quint32(0x00010800)); // HLEN, PLEN, OPER *(quint32*)(pktData+ 4) = qToBigEndian(quint32(0x06040001)); // Source H/W Addr, Proto Addr *(quint32*)(pktData+ 8) = qToBigEndian(quint32(mac_ >> 16)); *(quint16*)(pktData+12) = qToBigEndian(quint16(mac_ & 0xffff)); *(quint32*)(pktData+14) = qToBigEndian(srcIp); // Target H/W Addr, Proto Addr *(quint32*)(pktData+18) = qToBigEndian(quint32(0)); *(quint16*)(pktData+22) = qToBigEndian(quint16(0)); *(quint32*)(pktData+24) = qToBigEndian(tgtIp); } encap(reqPkt, kBcastMac, kEthTypeArp); transmitPacket(reqPkt); arpTable_.insert(tgtIp, 0); qDebug("Sent ARP Request for srcIp/tgtIp=%s/%s", qPrintable(QHostAddress(srcIp).toString()), qPrintable(QHostAddress(tgtIp).toString())); } void EmulDevice::receiveIp4(PacketBuffer *pktBuf) { uchar *pktData = pktBuf->data(); uchar ipProto; quint32 dstIp; if (pktData[0] != 0x45) { qDebug("%s: Unsupported IP version or options (%02x) ", __FUNCTION__, pktData[0]); goto _invalid_exit; } if (pktBuf->length() < 20) { qDebug("incomplete IPv4 header: expected 20, actual %d", pktBuf->length()); goto _invalid_exit; } // XXX: We don't verify IP Header checksum dstIp = qFromBigEndian(pktData + 16); if (dstIp != ip4_) { qDebug("%s: dstIp %x is not me (%x)", __FUNCTION__, dstIp, ip4_); goto _invalid_exit; } ipProto = pktData[9]; qDebug("%s: ipProto = %d", __FUNCTION__, ipProto); switch (ipProto) { case 1: // ICMP pktBuf->pull(20); receiveIcmp4(pktBuf); break; default: qWarning("%s: Unsupported ipProto %d", __FUNCTION__, ipProto); break; } _invalid_exit: return; } // This function assumes we are replying back to the same IP // that originally sent us the packet and therefore we can reuse the // ingress packet for egress; in other words, it assumes the // original IP header is intact and will just reuse it after // minimal modifications void EmulDevice::sendIp4Reply(PacketBuffer *pktBuf) { uchar *pktData = pktBuf->push(20); uchar origTtl = pktData[8]; uchar ipProto = pktData[9]; quint32 srcIp, dstIp, tgtIp; quint32 sum; // Swap src/dst IP addresses dstIp = qFromBigEndian(pktData + 12); // srcIp in original pkt srcIp = qFromBigEndian(pktData + 16); // dstIp in original pkt tgtIp = ((dstIp & ip4Mask_) == ip4Subnet_) ? dstIp : ip4Gateway_; if (!arpTable_.contains(tgtIp)) { qWarning("%s: mac not found for %s; unable to send IPv4 packet", __FUNCTION__, qPrintable(QHostAddress(tgtIp).toString())); return; } *(quint32*)(pktData + 12) = qToBigEndian(srcIp); *(quint32*)(pktData + 16) = qToBigEndian(dstIp); // Reset TTL pktData[8] = 64; // Incremental checksum update (RFC 1624 [Eqn.3]) // HC' = ~(~HC + ~m + m') sum = quint16(~qFromBigEndian(pktData + 10)); // old cksum sum += quint16(~quint16(origTtl << 8 | ipProto)); // old value sum += quint16(pktData[8] << 8 | ipProto); // new value while(sum >> 16) sum = (sum & 0xFFFF) + (sum >> 16); *(quint16*)(pktData + 10) = qToBigEndian(quint16(~sum)); encap(pktBuf, arpTable_.value(tgtIp), kEthTypeIp4); transmitPacket(pktBuf); } void EmulDevice::receiveIcmp4(PacketBuffer *pktBuf) { uchar *pktData = pktBuf->data(); quint32 sum; // XXX: We don't verify icmp checksum // We handle only ping request if (pktData[0] != 8) { // Echo Request qDebug("%s: Ignoring non echo request (%d)", __FUNCTION__, pktData[0]); return; } pktData[0] = 0; // Echo Reply // Incremental checksum update (RFC 1624 [Eqn.3]) // HC' = ~(~HC + ~m + m') sum = quint16(~qFromBigEndian(pktData + 2)); // old cksum sum += quint16(~quint16(8 << 8 | pktData[1])); // old value sum += quint16(0 << 8 | pktData[1]); // new value while(sum >> 16) sum = (sum & 0xFFFF) + (sum >> 16); *(quint16*)(pktData + 2) = qToBigEndian(quint16(~sum)); sendIp4Reply(pktBuf); qDebug("Sent ICMP Echo Reply"); } /* * --------------------------------------------------------- * IPV6 related private methods * --------------------------------------------------------- */ void EmulDevice::receiveIp6(PacketBuffer *pktBuf) { uchar *pktData = pktBuf->data(); uchar ipProto; UInt128 dstIp; if ((pktData[0] & 0xF0) != 0x60) { qDebug("%s: Unsupported IP version (%02x) ", __FUNCTION__, pktData[0]); goto _invalid_exit; } if (pktBuf->length() < kIp6HdrLen) { qDebug("incomplete IPv6 header: expected %d, actual %d", kIp6HdrLen, pktBuf->length()); goto _invalid_exit; } // FIXME: check for specific mcast address(es) instead of any mcast? dstIp = qFromBigEndian(pktData + 24); if (!isIp6Mcast(dstIp) && (dstIp != ip6_)) { qDebug("%s: dstIp %s is not me (%s)", __FUNCTION__, qPrintable(QHostAddress(dstIp.toArray()).toString()), qPrintable(QHostAddress(ip6_.toArray()).toString())); goto _invalid_exit; } ipProto = pktData[6]; switch (ipProto) { case kIpProtoIcmp6: pktBuf->pull(kIp6HdrLen); receiveIcmp6(pktBuf); break; default: break; } _invalid_exit: return; } // pktBuf should point to start of IP payload bool EmulDevice::sendIp6(PacketBuffer *pktBuf, UInt128 dstIp, quint8 protocol) { int payloadLen = pktBuf->length(); uchar *p = pktBuf->push(kIp6HdrLen); quint64 dstMac; if (!p) { qWarning("%s: failed to push %d bytes [0x%p, 0x%p]", __FUNCTION__, kIp6HdrLen, pktBuf->head(), pktBuf->data()); goto _error_exit; } // In case of mcast, derive dstMac if ((dstIp.hi64() >> 56) == 0xff) dstMac = (quint64(0x3333) << 32) | (dstIp.lo64() & 0xffffffff); else { UInt128 tgtIp = ((dstIp & ip6Mask_) == ip6Subnet_)? dstIp : ip6Gateway_; dstMac = ndpTable_.value(tgtIp); } if (!dstMac) { qWarning("%s: mac not found for %s; unable to send IPv6 packet", __FUNCTION__, qPrintable(QHostAddress(dstIp.toArray()).toString())); goto _error_exit; } // Ver(4), TrfClass(8), FlowLabel(8) *(quint32*)(p ) = qToBigEndian(quint32(0x60000000)); *(quint16*)(p+ 4) = qToBigEndian(quint16(payloadLen)); p[6] = protocol; p[7] = 255; // HopLimit memcpy(p+ 8, ip6_.toArray(), 16); // Source IP memcpy(p+24, dstIp.toArray(), 16); // Destination IP // FIXME: both these functions should return success/failure encap(pktBuf, dstMac, kEthTypeIp6); transmitPacket(pktBuf); return true; _error_exit: return false; } // This function assumes we are replying back to the same IP // that originally sent us the packet and therefore we can reuse the // ingress packet for egress; in other words, it assumes the // original IP header is intact and will just reuse it after // minimal modifications void EmulDevice::sendIp6Reply(PacketBuffer *pktBuf) { uchar *pktData = pktBuf->push(kIp6HdrLen); UInt128 srcIp, dstIp, tgtIp; // Swap src/dst IP addresses dstIp = qFromBigEndian(pktData + 8); // srcIp in original pkt srcIp = qFromBigEndian(pktData + 24); // dstIp in original pkt tgtIp = ((dstIp & ip6Mask_) == ip6Subnet_) ? dstIp : ip6Gateway_; if (!ndpTable_.contains(tgtIp)) { qWarning("%s: mac not found for %s; unable to send IPv6 packet", __FUNCTION__, qPrintable(QHostAddress(tgtIp.toArray()).toString())); return; } memcpy(pktData + 8, srcIp.toArray(), 16); // Source IP memcpy(pktData + 24, dstIp.toArray(), 16); // Destination IP // Reset TTL pktData[7] = 64; encap(pktBuf, ndpTable_.value(tgtIp), kEthTypeIp6); transmitPacket(pktBuf); } void EmulDevice::receiveIcmp6(PacketBuffer *pktBuf) { uchar *pktData = pktBuf->data(); quint8 type = pktData[0]; quint32 sum; // XXX: We don't verify icmp checksum switch (type) { case 128: // ICMPv6 Echo Request pktData[0] = 129; // Echo Reply // Incremental checksum update (RFC 1624 [Eqn.3]) // HC' = ~(~HC + ~m + m') sum = quint16(~qFromBigEndian(pktData + 2)); // old cksum sum += quint16(~quint16(128 << 8 | pktData[1])); // old value sum += quint16(129 << 8 | pktData[1]); // new value while(sum >> 16) sum = (sum & 0xFFFF) + (sum >> 16); *(quint16*)(pktData + 2) = qToBigEndian(quint16(~sum)); sendIp6Reply(pktBuf); qDebug("Sent ICMPv6 Echo Reply"); break; case 135: // Neigh Solicit case 136: // Neigh Advt receiveNdp(pktBuf); break; default: break; } } void EmulDevice::receiveNdp(PacketBuffer *pktBuf) { uchar *pktData = pktBuf->data(); quint8 type = pktData[0]; int len = pktBuf->length(); int minLen = 24 + (type == 136 ? 8 : 0); // NA should have the Target TLV if (len < minLen) { qDebug("%s: incomplete NS/NA header: expected %d, actual %d", __FUNCTION__, minLen, pktBuf->length()); goto _invalid_exit; } switch (type) { case 135: { // Neigh Solicit // TODO: Validation as per RFC 4861 sendNeighborAdvertisement(pktBuf); break; } case 136: { // Neigh Advt quint8 flags = pktData[4]; const quint8 kSFlag = 0x40; const quint8 kOFlag = 0x20; UInt128 tgtIp = qFromBigEndian(pktData + 8); quint64 mac = ndpTable_.value(tgtIp); // Update NDP table only for solicited responses if (!(flags & kSFlag)) break; if ((flags & kOFlag) || (mac == 0)) { // Check if we have a Target Link-Layer TLV if ((pktData[24] != 2) || (pktData[25] != 1)) goto _invalid_exit; mac = qFromBigEndian(pktData + 26); mac = (mac << 16) | qFromBigEndian(pktData + 30); ndpTable_.insert(tgtIp, mac); } break; } } _invalid_exit: return; } void EmulDevice::sendNeighborSolicit(UInt128 tgtIp) { UInt128 dstIp, srcIp = ip6_; PacketBuffer *reqPkt; uchar *pktData; // Validate target IP if (tgtIp == UInt128(0, 0)) return; // Form the solicited node address to be used as dstIp // ff02::1:ffXX:XXXX/104 dstIp = UInt128((quint64(0xff02) << 48), (quint64(0x01ff) << 24) | (tgtIp.lo64() & 0xFFFFFF)); reqPkt = new PacketBuffer; reqPkt->reserve(encapSize() + kIp6HdrLen); pktData = reqPkt->put(32); if (pktData) { // Calculate checksum first - // start with fixed fields in ICMP Header and IPv6 Pseudo Header ... quint32 sum = 0x8700 + 0x0101 + 32 + kIpProtoIcmp6; // then variable fields from ICMP header ... sum += sumUInt128(tgtIp); sum += (mac_ >> 32) + ((mac_ >> 16) & 0xffff) + (mac_ & 0xffff); // and variable fields from IPv6 pseudo header sum += sumUInt128(ip6_); sum += sumUInt128(dstIp); while(sum >> 16) sum = (sum & 0xFFFF) + (sum >> 16); // Type, Code *(quint16*)(pktData ) = qToBigEndian(quint16(0x8700)); // Checksum *(quint16*)(pktData+ 2) = qToBigEndian(quint16(~sum)); // Reserved *(quint32*)(pktData+ 4) = qToBigEndian(quint32(0)); // Target IP memcpy(pktData+ 8, tgtIp.toArray(), 16); // Source Addr TLV + MacAddr *(quint16*)(pktData+24) = qToBigEndian(quint16(0x0101)); *(quint32*)(pktData+26) = qToBigEndian(quint32(mac_ >> 16)); *(quint16*)(pktData+30) = qToBigEndian(quint16(mac_ & 0xffff)); } if (!sendIp6(reqPkt, dstIp , kIpProtoIcmp6)) return; ndpTable_.insert(tgtIp, 0); qDebug("Sent NDP Request for srcIp/tgtIp=%s/%s", qPrintable(QHostAddress(srcIp.toArray()).toString()), qPrintable(QHostAddress(tgtIp.toArray()).toString())); } // Send NA for the NS packet in pktBuf // pktBuf should point to start of ICMPv6 header void EmulDevice::sendNeighborAdvertisement(PacketBuffer *pktBuf) { PacketBuffer *naPkt; uchar *pktData = pktBuf->data(); quint16 flags = 0x6000; // solicit = 1; overide = 1 uchar *ip6Hdr; UInt128 tgtIp, srcIp; tgtIp = qFromBigEndian(pktData + 8); if (tgtIp != ip6_) { qDebug("%s: NS tgtIp %s is not us %s", __FUNCTION__, qPrintable(QHostAddress(tgtIp.toArray()).toString()), qPrintable(QHostAddress(ip6_.toArray()).toString())); ip6Hdr = pktBuf->push(kIp6HdrLen); return; } ip6Hdr = pktBuf->push(kIp6HdrLen); srcIp = qFromBigEndian(ip6Hdr + 8); if (srcIp == UInt128(0, 0)) { // reset solicit flag flags &= ~0x4000; // NA should be sent to All nodes address srcIp = UInt128(quint64(0xff02) << 48, quint64(1)); } else if (pktBuf->length() >= 32) { // have TLVs? if ((pktData[24] == 0x01) && (pktData[25] == 0x01)) { // Source TLV quint64 mac; mac = qFromBigEndian(pktData + 26); mac = (mac << 16) | qFromBigEndian(pktData + 30); ndpTable_.insert(srcIp, mac); } } naPkt = new PacketBuffer; naPkt->reserve(encapSize() + kIp6HdrLen); pktData = naPkt->put(32); if (pktData) { // Calculate checksum first - // start with fixed fields in ICMP Header and IPv6 Pseudo Header ... quint32 sum = (0x8800 + flags + 0x0201) + (32 + kIpProtoIcmp6); // then variable fields from ICMP header ... sum += sumUInt128(tgtIp); sum += (mac_ >> 32) + ((mac_ >> 16) & 0xffff) + (mac_ & 0xffff); // and variable fields from IPv6 pseudo header sum += sumUInt128(ip6_); sum += sumUInt128(srcIp); while(sum >> 16) sum = (sum & 0xFFFF) + (sum >> 16); // Type, Code *(quint16*)(pktData ) = qToBigEndian(quint16(0x8800)); // Checksum *(quint16*)(pktData+ 2) = qToBigEndian(quint16(~sum)); // Flags-Reserved *(quint32*)(pktData+ 4) = qToBigEndian(quint32(flags << 16)); // Target IP memcpy(pktData+ 8, tgtIp.toArray(), 16); // Target Addr TLV + MacAddr *(quint16*)(pktData+24) = qToBigEndian(quint16(0x0201)); *(quint32*)(pktData+26) = qToBigEndian(quint32(mac_ >> 16)); *(quint16*)(pktData+30) = qToBigEndian(quint16(mac_ & 0xffff)); } if (!sendIp6(naPkt, srcIp , kIpProtoIcmp6)) return; qDebug("Sent Neigh Advt to dstIp for tgtIp=%s/%s", qPrintable(QHostAddress(srcIp.toArray()).toString()), qPrintable(QHostAddress(tgtIp.toArray()).toString())); } ostinato-1.3.0/server/emuldevice.h000066400000000000000000000040741451413623100171610ustar00rootroot00000000000000/* Copyright (C) 2015 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _EMUL_DEVICE_H #define _EMUL_DEVICE_H #include "device.h" class EmulDevice: public Device { public: EmulDevice(DeviceManager *deviceManager); virtual ~EmulDevice() = default; void receivePacket(PacketBuffer *pktBuf); virtual void clearNeighbors(Device::NeighborSet set); virtual void getNeighbors(OstEmul::DeviceNeighborList *neighbors); bool isOrigin(const PacketBuffer *pktBuf); quint64 neighborMac(const PacketBuffer *pktBuf); protected: virtual quint64 arpLookup(quint32 ip); virtual quint64 ndpLookup(UInt128 ip); virtual void sendArpRequest(quint32 tgtIp); virtual void sendNeighborSolicit(UInt128 tgtIp); private: // methods int encapSize(); void encap(PacketBuffer *pktBuf, quint64 dstMac, quint16 type); void receiveArp(PacketBuffer *pktBuf); void receiveIp4(PacketBuffer *pktBuf); void sendIp4Reply(PacketBuffer *pktBuf); void receiveIcmp4(PacketBuffer *pktBuf); void receiveIp6(PacketBuffer *pktBuf); void sendIp6Reply(PacketBuffer *pktBuf); bool sendIp6(PacketBuffer *pktBuf, UInt128 dstIp, quint8 protocol); void receiveIcmp6(PacketBuffer *pktBuf); void receiveNdp(PacketBuffer *pktBuf); void sendNeighborAdvertisement(PacketBuffer *pktBuf); private: // data QHash arpTable_; QHash ndpTable_; }; bool operator<(const DeviceKey &a1, const DeviceKey &a2); #endif ostinato-1.3.0/server/hostdevice.h000066400000000000000000000026331451413623100171730ustar00rootroot00000000000000/* Copyright (C) 2018 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _HOST_DEVICE_H #define _HOST_DEVICE_H #include "bsdhostdevice.h" #include "linuxhostdevice.h" #include "winhostdevice.h" class DeviceManager; /*! * HostDevice abstracts the various OS-specific host device classes */ class HostDevice { public: static Device* create(QString portName, DeviceManager *deviceManager) { #if defined(Q_OS_WIN32) return new WindowsHostDevice(portName, deviceManager); #elif defined(Q_OS_LINUX) return new LinuxHostDevice(portName, deviceManager); #elif defined(Q_OS_BSD4) return new BsdHostDevice(portName, deviceManager); #else (void)portName; // squelch unused warning (void)deviceManager; // squelch unused warning return nullptr; #endif } }; #endif ostinato-1.3.0/server/icons/000077500000000000000000000000001451413623100157745ustar00rootroot00000000000000ostinato-1.3.0/server/icons/portgroup.png000066400000000000000000000012331451413623100205420ustar00rootroot00000000000000‰PNG  IHDRóÿagAMA¯È7ŠétEXtSoftwareAdobe ImageReadyqÉe<-IDAT8Ë}S;lA}»w¶¥8§ØŽÃ7€RãD ˆ !ACI‡D‹¨h)¶k¢ÐñQ  ”¥¤á° H(4 $ÛI” ó}–™Ù³EP’=ÍÎÝíÎ{ovf•1» ß÷GQt# C—<È‹õû}öÏÔNÍfsŒæ …Âõ\n!I »)¦X,`ié-Üí‚Æ%.—Ëóù */ #ifndef _INTERFACE_INFO_H #define _INTERFACE_INFO_H #include "../common/uint128.h" #include template struct IpConfig { IpType address; int prefixLength; IpType gateway; }; using Ip4Config = IpConfig; using Ip6Config = IpConfig; struct InterfaceInfo { quint64 mac; QList ip4; QList ip6; double speed{0}; // in Mbps quint32 mtu{0}; }; #endif ostinato-1.3.0/server/linuxhostdevice.cpp000066400000000000000000000220411451413623100206010ustar00rootroot00000000000000/* Copyright (C) 2018 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #include "linuxhostdevice.h" #ifdef Q_OS_LINUX #include "../common/qtport.h" #include #include #include LinuxHostDevice::LinuxHostDevice(QString portName, DeviceManager *deviceManager) : Device(deviceManager) { ifName_ = portName; netSock_ = nl_socket_alloc(); if (!netSock_) { qWarning("Failed to open netlink socket for %s", qPrintable(ifName_)); return; } if (nl_connect(netSock_, NETLINK_ROUTE) < 0) { qWarning("Failed to connect netlink socket for %s", qPrintable(ifName_)); return; } rtnl_link *link; if (rtnl_link_get_kernel(netSock_, 0, qPrintable(ifName_), &link) < 0) { qWarning("Failed to get rtnet link from kernel for %s", qPrintable(ifName_)); return; } ifIndex_ = rtnl_link_get_ifindex(link); qDebug("Port %s: ifIndex %d", qPrintable(ifName_), ifIndex_); rtnl_link_put(link); } void LinuxHostDevice::receivePacket(PacketBuffer* /*pktBuf*/) { // Do Nothing } void LinuxHostDevice::clearNeighbors(Device::NeighborSet set) { // No need to do anything - see AbstractPort::resolveDeviceNeighbors() // on when this is used if (set == kUnresolvedNeighbors) return; nl_cache *neighCache; if (rtnl_neigh_alloc_cache(netSock_, &neighCache) < 0) { qWarning("Failed to get neigh cache from kernel"); return; } if (!neighCache) { qWarning("Neigh cache empty"); return; } int count=0, fail=0; rtnl_neigh *neigh = (rtnl_neigh*) nl_cache_get_first(neighCache); while (neigh) { if ((rtnl_neigh_get_ifindex(neigh) == ifIndex_) && (rtnl_neigh_get_family(neigh) == AF_INET || rtnl_neigh_get_family(neigh) == AF_INET6) && !(rtnl_neigh_get_state(neigh) & (NUD_PERMANENT|NUD_NOARP))) { count++; if (rtnl_neigh_delete(netSock_, neigh, 0) < 0) fail++; } neigh = (rtnl_neigh*) nl_cache_get_next(OBJ_CAST(neigh)); } nl_cache_put(neighCache); qDebug("Flush ARP/ND table for ifIndex %u: %d/%d deleted", ifIndex_, count - fail, count); } void LinuxHostDevice::getNeighbors(OstEmul::DeviceNeighborList *neighbors) { nl_cache *neighCache; if (rtnl_neigh_alloc_cache(netSock_, &neighCache) < 0) { qWarning("Failed to get neigh cache from kernel"); return; } if (!neighCache) { qWarning("Neigh cache empty"); return; } rtnl_neigh *neigh = (rtnl_neigh*) nl_cache_get_first(neighCache); while (neigh) { if ((rtnl_neigh_get_ifindex(neigh) == ifIndex_) && !(rtnl_neigh_get_state(neigh) & NUD_NOARP)) { if (rtnl_neigh_get_family(neigh) == AF_INET) { OstEmul::ArpEntry *arp = neighbors->add_arp(); arp->set_ip4(qFromBigEndian( nl_addr_get_binary_addr(rtnl_neigh_get_dst(neigh)))); nl_addr *lladdr = rtnl_neigh_get_lladdr(neigh); arp->set_mac(lladdr ? qFromBigEndian( nl_addr_get_binary_addr(lladdr)) >> 16 : 0); } else if (rtnl_neigh_get_family(neigh) == AF_INET6) { OstEmul::NdpEntry *ndp = neighbors->add_ndp(); ndp->mutable_ip6()->set_hi(qFromBigEndian( nl_addr_get_binary_addr( rtnl_neigh_get_dst(neigh)))); ndp->mutable_ip6()->set_lo(qFromBigEndian((const uchar*) nl_addr_get_binary_addr( rtnl_neigh_get_dst(neigh))+8)); nl_addr *lladdr = rtnl_neigh_get_lladdr(neigh); ndp->set_mac(lladdr ? qFromBigEndian( nl_addr_get_binary_addr(lladdr)) >> 16 : 0); } } neigh = (rtnl_neigh*) nl_cache_get_next(OBJ_CAST(neigh)); } nl_cache_put(neighCache); } quint64 LinuxHostDevice::arpLookup(quint32 ip) { quint64 mac = 0; nl_cache *neighCache; if (rtnl_neigh_alloc_cache(netSock_, &neighCache) < 0) { qWarning("Failed to get neigh cache from kernel"); return mac; } if (!neighCache) { qWarning("Neigh cache empty"); return mac; } quint32 ipBig = qToBigEndian(ip); nl_addr *dst = nl_addr_build(AF_INET, &ipBig, sizeof(ipBig)); #if 0 // // libnl 3.2.[15..21] have a bug in rtnl_neigh_get and fail to find entry // https://github.com/tgraf/libnl/commit/8571f58f23763d8db7365d02c9b27832ad3d7005 // rtnl_neigh *neigh = rtnl_neigh_get(neighCache, ifIndex_, dst); if (neigh) { mac = qFromBigEndian( nl_addr_get_binary_addr(rtnl_neigh_get_lladdr(neigh))) >> 16; rtnl_neigh_put(neigh); } #else rtnl_neigh *neigh = (rtnl_neigh*) nl_cache_get_first(neighCache); while (neigh) { if ((rtnl_neigh_get_ifindex(neigh) == ifIndex_) && (rtnl_neigh_get_family(neigh) == AF_INET) && !nl_addr_cmp(rtnl_neigh_get_dst(neigh), dst)) { nl_addr *lladdr = rtnl_neigh_get_lladdr(neigh); if (lladdr) mac = qFromBigEndian( nl_addr_get_binary_addr(lladdr)) >> 16; break; } neigh = (rtnl_neigh*) nl_cache_get_next(OBJ_CAST(neigh)); } #endif nl_addr_put(dst); nl_cache_put(neighCache); return mac; } quint64 LinuxHostDevice::ndpLookup(UInt128 ip) { quint64 mac = 0; nl_cache *neighCache; if (rtnl_neigh_alloc_cache(netSock_, &neighCache) < 0) { qWarning("Failed to get neigh cache from kernel"); return mac; } if (!neighCache) { qWarning("Neigh cache empty"); return mac; } nl_addr *dst = nl_addr_build(AF_INET6, ip.toArray(), 16); #if 0 // // libnl 3.2.[15..21] have a bug in rtnl_neigh_get and fail to find entry // https://github.com/tgraf/libnl/commit/8571f58f23763d8db7365d02c9b27832ad3d7005 // rtnl_neigh *neigh = rtnl_neigh_get(neighCache, ifIndex_, dst); if (neigh) { mac = qFromBigEndian( nl_addr_get_binary_addr(rtnl_neigh_get_lladdr(neigh))) >> 16; rtnl_neigh_put(neigh); } #else rtnl_neigh *neigh = (rtnl_neigh*) nl_cache_get_first(neighCache); while (neigh) { if ((rtnl_neigh_get_ifindex(neigh) == ifIndex_) && (rtnl_neigh_get_family(neigh) == AF_INET6) && !nl_addr_cmp(rtnl_neigh_get_dst(neigh), dst)) { nl_addr *lladdr = rtnl_neigh_get_lladdr(neigh); if (lladdr) mac = qFromBigEndian( nl_addr_get_binary_addr(lladdr)) >> 16; break; } neigh = (rtnl_neigh*) nl_cache_get_next(OBJ_CAST(neigh)); } #endif nl_addr_put(dst); nl_cache_put(neighCache); return mac; } void LinuxHostDevice::sendArpRequest(quint32 tgtIp) { quint32 ipBig = qToBigEndian(tgtIp); nl_addr *dst = nl_addr_build(AF_INET, &ipBig, sizeof(ipBig)); rtnl_neigh *neigh = rtnl_neigh_alloc(); rtnl_neigh_set_ifindex(neigh, ifIndex_); rtnl_neigh_set_state(neigh, NUD_NONE); rtnl_neigh_set_dst(neigh, dst); rtnl_neigh_set_flags(neigh, NTF_USE); // force kernel to send ARP request if (int err = rtnl_neigh_add(netSock_, neigh, NLM_F_CREATE) < 0) qWarning("Resolve arp failed for port %s ip %08x: %s", qPrintable(ifName_), tgtIp, strerror(err)); rtnl_neigh_put(neigh); nl_addr_put(dst); } void LinuxHostDevice::sendNeighborSolicit(UInt128 tgtIp) { nl_addr *dst = nl_addr_build(AF_INET6, tgtIp.toArray(), 16); rtnl_neigh *neigh = rtnl_neigh_alloc(); rtnl_neigh_set_ifindex(neigh, ifIndex_); rtnl_neigh_set_state(neigh, NUD_NONE); rtnl_neigh_set_dst(neigh, dst); rtnl_neigh_set_flags(neigh, NTF_USE); // force kernel to send ARP request if (int err = rtnl_neigh_add(netSock_, neigh, NLM_F_CREATE) < 0) qWarning("Resolve ndp failed for port %s ip %016llx-%016llx: %s", qPrintable(ifName_), tgtIp.hi64(), tgtIp.lo64(), strerror(err)); rtnl_neigh_put(neigh); nl_addr_put(dst); } #endif ostinato-1.3.0/server/linuxhostdevice.h000066400000000000000000000026121451413623100202500ustar00rootroot00000000000000/* Copyright (C) 2018 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _LINUX_HOST_DEVICE_H #define _LINUX_HOST_DEVICE_H #include "device.h" #ifdef Q_OS_LINUX class LinuxHostDevice: public Device { public: LinuxHostDevice(QString portName, DeviceManager *deviceManager); virtual ~LinuxHostDevice() {}; virtual void receivePacket(PacketBuffer *pktBuf); virtual void clearNeighbors(Device::NeighborSet set); virtual void getNeighbors(OstEmul::DeviceNeighborList *neighbors); protected: virtual quint64 arpLookup(quint32 ip); virtual quint64 ndpLookup(UInt128 ip); virtual void sendArpRequest(quint32 tgtIp); virtual void sendNeighborSolicit(UInt128 tgtIp); private: QString ifName_; int ifIndex_{-1}; struct nl_sock *netSock_{nullptr}; }; #endif #endif ostinato-1.3.0/server/linuxport.cpp000066400000000000000000000667731451413623100174540ustar00rootroot00000000000000/* Copyright (C) 2011 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #include "linuxport.h" #include "interfaceinfo.h" #include "linuxutils.h" #ifdef Q_OS_LINUX #include "../common/qtport.h" #include #include #include #include #include #include #include #include #if (LIBNL_VER_NUM > 0x0302) || ((LIBNL_VER_NUM == 0x0302) && (LIBNL_VER_MIC >= 26)) #include #endif #include #include #include #include #include #include QList LinuxPort::allPorts_; LinuxPort::StatsMonitor *LinuxPort::monitor_; const quint32 kMaxValue32 = 0xffffffff; const quint64 kMaxValue64 = 0xffffffffffffffffULL; #ifdef HAVE_IFLA_STATS64 #define X_IFLA_STATS IFLA_STATS64 typedef struct rtnl_link_stats64 x_rtnl_link_stats; #else #define X_IFLA_STATS IFLA_STATS typedef struct rtnl_link_stats x_rtnl_link_stats; #endif nl_sock *LinuxPort::netSock_{nullptr}; nl_cache *LinuxPort::linkCache_{nullptr}; nl_cache *LinuxPort::addressCache_{nullptr}; nl_cache *LinuxPort::routeCache_{nullptr}; LinuxPort::LinuxPort(int id, const char *device) : PcapPort(id, device) { isPromisc_ = true; clearPromisc_ = false; populateInterfaceInfo(); // We don't need per port Rx/Tx monitors for Linux // No need to stop them because we start them only in // PcapPort::init which has not yet been called delete monitorRx_; delete monitorTx_; monitorRx_ = monitorTx_ = NULL; // We have one monitor for both Rx/Tx of all ports if (!monitor_) monitor_ = new StatsMonitor(); data_.set_is_exclusive_control(hasExclusiveControl()); minPacketSetSize_ = 16; qDebug("adding dev to all ports list <%s>", device); allPorts_.append(this); // A port can support either 32 or 64 bit stats - we will attempt // to guess this for each port and initialize this variable at // run time when the counter wraps around maxStatsValue_ = 0; } LinuxPort::~LinuxPort() { qDebug("In %s", __FUNCTION__); allPorts_.removeAll(this); if (monitor_->isRunning()) { monitor_->stop(); monitor_->wait(); } if (clearPromisc_) { int sd = socket(AF_INET, SOCK_DGRAM, 0); struct ifreq ifr; memset(&ifr, 0, sizeof(ifr)); strncpy(ifr.ifr_name, name(), sizeof(ifr.ifr_name)); if (ioctl(sd, SIOCGIFFLAGS, &ifr) != -1) { if (ifr.ifr_flags & IFF_PROMISC) { ifr.ifr_flags &= ~IFF_PROMISC; if (ioctl(sd, SIOCSIFFLAGS, &ifr) == -1) qDebug("Failed clearing promisc flag. SIOCSIFFLAGS failed: %s", strerror(errno)); } } else qDebug("Failed clearing promisc flag. SIOCGIFFLAGS failed: %s", strerror(errno)); close(sd); } } void LinuxPort::fetchHostNetworkInfo() { netSock_ = nl_socket_alloc(); if (!netSock_) { qWarning("Failed to open netlink socket"); return; } if (nl_connect(netSock_, NETLINK_ROUTE) < 0) { qWarning("Failed to connect netlink socket"); return; } if (rtnl_link_alloc_cache(netSock_, AF_UNSPEC, &linkCache_) < 0) { qWarning("Failed to populate link cache"); return; } if (rtnl_addr_alloc_cache(netSock_, &addressCache_) < 0) { qWarning("Failed to populate addr cache"); return; } if (rtnl_route_alloc_cache(netSock_, AF_UNSPEC, 0, &routeCache_) < 0) { qWarning("Failed to populate addr cache"); return; } } void LinuxPort::freeHostNetworkInfo() { nl_cache_put(routeCache_); nl_cache_put(addressCache_); nl_cache_put(linkCache_); nl_socket_free(netSock_); } void LinuxPort::init() { if (!monitor_->isRunning()) monitor_->start(); monitor_->waitForSetupFinished(); if (!isPromisc_) addNote("Non Promiscuous Mode"); AbstractPort::init(); } OstProto::LinkState LinuxPort::linkState() { return linkState_; } bool LinuxPort::hasExclusiveControl() { // TODO return false; } bool LinuxPort::setExclusiveControl(bool /*exclusive*/) { // TODO return false; } void LinuxPort::populateInterfaceInfo() { // // Find Mac // if (!linkCache_) { qWarning("rtnetlink link cache empty for %s", name()); return; } rtnl_link *link = rtnl_link_get_by_name(linkCache_, name()); if (!link) { qWarning("rtnetlink link not found for %s", name()); return; } nl_addr *addr = rtnl_link_get_addr(link); if (!addr) { qWarning("rtnetlink mac addr not found for %s", name()); return; } if (nl_addr_get_family(addr) != AF_LLC) { qWarning("unexpected mac family found for %s:%d", name(), nl_addr_get_family(addr)); rtnl_link_put(link); return; } if (nl_addr_get_prefixlen(addr) != 48) { qWarning("unexpected mac length for %s:%d", name(), nl_addr_get_prefixlen(addr)); rtnl_link_put(link); return; } quint64 mac = qFromBigEndian(nl_addr_get_binary_addr(addr)) >> 16; if (!mac) { qWarning("zero mac for %s - skipping", name()); rtnl_link_put(link); return; } interfaceInfo_ = new InterfaceInfo; interfaceInfo_->speed = sysfsAttrib(name(), "speed").toDouble(); interfaceInfo_->mtu = rtnl_link_get_mtu(link); int ifIndex = rtnl_link_get_ifindex(link); rtnl_link_put(link); interfaceInfo_->mac = mac; // // Find gateways // quint32 gw4 = 0; UInt128 gw6 = 0; for (rtnl_route *rt = routeCache_ ? (rtnl_route*) nl_cache_get_first(routeCache_) : 0; rt && (!gw4 || !gw6); rt = (rtnl_route*) nl_cache_get_next(OBJ_CAST(rt))) { if (rtnl_route_get_table(rt) != RT_TABLE_MAIN) // we want only main RTT continue; nl_addr *pfx = rtnl_route_get_dst(rt); if (nl_addr_get_len(pfx)) // default route has len = 0 continue; if (!rtnl_route_get_nnexthops(rt)) // at least one nh is required continue; rtnl_nexthop *nh = rtnl_route_nexthop_n(rt, 0); if (rtnl_route_nh_get_ifindex(nh) != ifIndex) // ignore gw on other links continue; if (!gw4 && rtnl_route_get_family(rt) == AF_INET) { nl_addr *gwa = rtnl_route_nh_get_gateway(nh); if (gwa) gw4 = qFromBigEndian(nl_addr_get_binary_addr(gwa)); } else if (!gw6 && rtnl_route_get_family(rt) == AF_INET6) { nl_addr *gwa = rtnl_route_nh_get_gateway(nh); if (gwa) gw6 = UInt128((quint8*) nl_addr_get_binary_addr(gwa)); } } // // Find self IP // if (!addressCache_) { qWarning("rtnetlink address cache empty for %s", name()); return; } rtnl_addr *l3addr = (rtnl_addr*) nl_cache_get_first(addressCache_); while (l3addr) { if (rtnl_addr_get_ifindex(l3addr) == ifIndex) { if (rtnl_addr_get_family(l3addr) == AF_INET) { Ip4Config ip; ip.address = qFromBigEndian( nl_addr_get_binary_addr( rtnl_addr_get_local(l3addr))); ip.prefixLength = rtnl_addr_get_prefixlen(l3addr); ip.gateway = gw4; interfaceInfo_->ip4.append(ip); } else if (rtnl_addr_get_family(l3addr) == AF_INET6) { Ip6Config ip; ip.address = UInt128((quint8*)nl_addr_get_binary_addr( rtnl_addr_get_local(l3addr))); ip.prefixLength = rtnl_addr_get_prefixlen(l3addr); ip.gateway = gw6; interfaceInfo_->ip6.append(ip); } } l3addr = (rtnl_addr*) nl_cache_get_next((nl_object*)l3addr); } } LinuxPort::StatsMonitor::StatsMonitor() : QThread() { setObjectName("StatsMon"); stop_ = false; setupDone_ = false; ioctlSocket_ = socket(AF_INET, SOCK_DGRAM, 0); Q_ASSERT(ioctlSocket_ >= 0); } LinuxPort::StatsMonitor::~StatsMonitor() { close(ioctlSocket_); } void LinuxPort::StatsMonitor::run() { if (netlinkStats() < 0) { qDebug("netlink stats not available - using /proc stats"); procStats(); } } void LinuxPort::StatsMonitor::procStats() { PortStats **portStats; int fd; QByteArray buf; int len; char *p, *end; int count, index; const char* fmtopt[] = { "%llu%llu%llu%llu%llu%llu%u%u%llu%llu%u%u%u%u%u%u\n", "%llu%llu%llu%llu%llu%llu%n%n%llu%llu%u%u%u%u%u%n\n", }; const char *fmt; // // We first setup stuff before we start polling for stats // fd = open("/proc/net/dev", O_RDONLY); if (fd < 0) { qWarning("Unable to open /proc/net/dev - no stats will be available"); return; } buf.fill('\0', 8192); len = read(fd, (void*) buf.data(), buf.size()); if (len < 0) { qWarning("initial buffer size is too small. no stats will be available"); return; } p = buf.data(); end = p + len; // Select scanf format if (strstr(buf, "compressed")) fmt = fmtopt[0]; else fmt = fmtopt[1]; // Count number of lines - number of ports is 2 less than number of lines count = 0; while (p < end) { if (*p == '\n') count++; p++; } count -= 2; if (count <= 0) { qWarning("no ports in /proc/dev/net - no stats will be available"); return; } portStats = (PortStats**) calloc(count, sizeof(PortStats)); Q_ASSERT(portStats != NULL); // // Populate the port stats array // p = buf.data(); // Skip first two lines while (*p != '\n') p++; p++; while (*p != '\n') p++; p++; index = 0; while (p < end) { char* q; // Skip whitespace while ((p < end) && (*p == ' ')) p++; q = p; // Get interface name while ((q < end) && (*q != ':') && (*q != '\n')) q++; if ((q < end) && (*q == ':')) { foreach(LinuxPort* port, allPorts_) { if (strncmp(port->name(), p, int(q-p)) == 0) { portStats[index] = &(port->stats_); if (setPromisc(port->name())) port->clearPromisc_ = true; else port->isPromisc_ = false; break; } } } index++; // Skip till newline p = q; while (*p != '\n') p++; p++; } Q_ASSERT(index == count); qDebug("stats for %d ports setup", count); setupDone_ = true; // // We are all set - Let's start polling for stats! // while (!stop_) { lseek(fd, 0, SEEK_SET); len = read(fd, (void*) buf.data(), buf.size()); if (len < 0) { if (buf.size() > 1*1024*1024) { qWarning("buffer size hit limit. no more stats"); return; } qDebug("doubling buffer size. curr = %d", buf.size()); buf.resize(buf.size() * 2); continue; } p = buf.data(); end = p + len; // Skip first two lines while (*p != '\n') p++; p++; while (*p != '\n') p++; p++; index = 0; while (p < end) { uint dummy; quint64 rxBytes, rxPkts; quint64 rxErrors, rxDrops, rxFifo, rxFrame; quint64 txBytes, txPkts; // Skip interface name - we assume the number and order of ports // won't change since we parsed the output before we started polling while ((p < end) && (*p != ':') && (*p != '\n')) p++; if (p >= end) break; if (*p == '\n') { index++; continue; } p++; sscanf(p, fmt, &rxBytes, &rxPkts, &rxErrors, &rxDrops, &rxFifo, &rxFrame, &dummy, &dummy, &txBytes, &txPkts, &dummy, &dummy, &dummy, &dummy, &dummy, &dummy); if (index < count) { AbstractPort::PortStats *stats = portStats[index]; if (stats) { // TODO: fix the pps/Bps calc similar to netlink stats stats->rxPps = ((rxPkts >= stats->rxPkts) ? rxPkts - stats->rxPkts : rxPkts + (kMaxValue32 - stats->rxPkts)) / kRefreshFreq_; stats->rxBps = ((rxBytes >= stats->rxBytes) ? rxBytes - stats->rxBytes : rxBytes + (kMaxValue32 - stats->rxBytes)) / kRefreshFreq_; stats->rxPkts = rxPkts; stats->rxBytes = rxBytes; stats->txPps = ((txPkts >= stats->txPkts) ? txPkts - stats->txPkts : txPkts + (kMaxValue32 - stats->txPkts)) / kRefreshFreq_; stats->txBps = ((txBytes >= stats->txBytes) ? txBytes - stats->txBytes : txBytes + (kMaxValue32 - stats->txBytes)) / kRefreshFreq_; stats->txPkts = txPkts; stats->txBytes = txBytes; stats->rxDrops = rxDrops; stats->rxErrors = rxErrors; stats->rxFifoErrors = rxFifo; stats->rxFrameErrors = rxFrame; } } while (*p != '\n') p++; p++; index++; } QThread::sleep(kRefreshFreq_); } free(portStats); } int LinuxPort::StatsMonitor::netlinkStats() { QHash portStats; QHash portMaxStatsValue; QHash linkState; int fd; struct sockaddr_nl local; struct sockaddr_nl kernel; QByteArray buf; int len, count; struct { struct nlmsghdr nlh; struct rtgenmsg rtg; } ifListReq; struct iovec iov; struct msghdr msg; struct nlmsghdr *nlm; bool done = false; // // We first setup stuff before we start polling for stats // fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE); if (fd < 0) { qWarning("Unable to open netlink socket (errno %d)", errno); return -1; } memset(&local, 0, sizeof(local)); local.nl_family = AF_NETLINK; if (bind(fd, (struct sockaddr*) &local, sizeof(local)) < 0) { qWarning("Unable to bind netlink socket (errno %d)", errno); return -1; } memset(&ifListReq, 0, sizeof(ifListReq)); ifListReq.nlh.nlmsg_len = sizeof(ifListReq); ifListReq.nlh.nlmsg_type = RTM_GETLINK; ifListReq.nlh.nlmsg_flags = NLM_F_REQUEST | NLM_F_DUMP; ifListReq.nlh.nlmsg_pid = 0; ifListReq.rtg.rtgen_family = AF_PACKET; buf.fill('\0', 1024); msg.msg_name = &kernel; msg.msg_namelen = sizeof(kernel); msg.msg_iov = &iov; msg.msg_iovlen = 1; msg.msg_control = NULL; msg.msg_controllen = 0; msg.msg_flags = 0; qDebug("nlmsg_flags = %x", ifListReq.nlh.nlmsg_flags); if (send(fd, (void*)&ifListReq, sizeof(ifListReq), 0) < 0) { qWarning("Unable to send GETLINK request (errno %d)", errno); return -1; } count = 0; _retry: // Find required size of buffer and resize accordingly while (1) { iov.iov_base = buf.data(); iov.iov_len = buf.size(); msg.msg_flags = 0; // Peek at reply to check buffer size required len = recvmsg(fd, &msg, MSG_PEEK|MSG_TRUNC); if (len < 0) { if (errno == EINTR || errno == EAGAIN) continue; qWarning("netlink recv error %d", errno); return -1; } else if (len == 0) { qWarning("netlink closed the socket on my face!"); return -1; } else { if (msg.msg_flags & MSG_TRUNC) { if (len == buf.size()) // Older Kernel returns truncated size { qDebug("netlink buffer size %d not enough", buf.size()); qDebug("retrying with double the size"); // Double the size and retry buf.resize(buf.size()*2); continue; } else // Newer Kernel returns actual size required { qDebug("netlink required buffer size = %d", len); buf.resize(len); continue; } } else qDebug("buffer size %d enough for netlink", buf.size()); break; } } msg.msg_flags = 0; // Actually receive the reply now len = recvmsg(fd, &msg, 0); if (len < 0) { if (errno == EINTR || errno == EAGAIN) goto _retry; qWarning("netlink recv error %d", errno); return -1; } else if (len == 0) { qWarning("netlink socket closed unexpectedly"); return -1; } // // Populate the port stats hash table // nlm = (struct nlmsghdr*) buf.data(); while (NLMSG_OK(nlm, (uint)len)) { struct ifinfomsg *ifi; struct rtattr *rta; int rtaLen; char ifname[64] = ""; if (nlm->nlmsg_type == NLMSG_DONE) { done = true; break; } if (nlm->nlmsg_type == NLMSG_ERROR) { struct nlmsgerr *err = (struct nlmsgerr*) NLMSG_DATA(nlm); qDebug("RTNETLINK error %d", err->error); done = true; break; } Q_ASSERT(nlm->nlmsg_type == RTM_NEWLINK); ifi = (struct ifinfomsg*) NLMSG_DATA(nlm); rta = IFLA_RTA(ifi); rtaLen = len - NLMSG_LENGTH(sizeof(*ifi)); while (RTA_OK(rta, rtaLen)) { if (rta->rta_type == IFLA_IFNAME) { strncpy(ifname, (char*)RTA_DATA(rta), RTA_PAYLOAD(rta)); ifname[RTA_PAYLOAD(rta)] = 0; break; } rta = RTA_NEXT(rta, rtaLen); } qDebug("if: %s(%d)", ifname, ifi->ifi_index); foreach(LinuxPort* port, allPorts_) { if (strcmp(port->name(), ifname) == 0) { portStats[uint(ifi->ifi_index)] = &(port->stats_); portMaxStatsValue[uint(ifi->ifi_index)] = &(port->maxStatsValue_); linkState[uint(ifi->ifi_index)] = &(port->linkState_); if (setPromisc(port->name())) port->clearPromisc_ = true; else port->isPromisc_ = false; count++; break; } } nlm = NLMSG_NEXT(nlm, len); } if (!done) goto _retry; qDebug("port count = %d\n", count); if (count <= 0) { qWarning("no ports in RTNETLINK GET_LINK - no stats will be available"); return - 1; } qDebug("stats for %d ports setup", count); setupDone_ = true; // // We are all set - Let's start polling for stats! // while (!stop_) { if (send(fd, (void*)&ifListReq, sizeof(ifListReq), 0) < 0) { qWarning("Unable to send GETLINK request (errno %d)", errno); goto _try_later; } done = false; _retry_recv: msg.msg_flags = 0; len = recvmsg(fd, &msg, 0); if (len < 0) { if (errno == EINTR || errno == EAGAIN) goto _retry_recv; qWarning("netlink recv error %d", errno); break; } else if (len == 0) { qWarning("netlink socket closed unexpectedly"); break; } nlm = (struct nlmsghdr*) buf.data(); while (NLMSG_OK(nlm, (uint)len)) { struct ifinfomsg *ifi; struct rtattr *rta; int rtaLen; if (nlm->nlmsg_type == NLMSG_DONE) { done = true; break; } if (nlm->nlmsg_type == NLMSG_ERROR) { struct nlmsgerr *err = (struct nlmsgerr*) NLMSG_DATA(nlm); qDebug("RTNETLINK error: %s", strerror(-err->error)); done = true; break; } Q_ASSERT(nlm->nlmsg_type == RTM_NEWLINK); ifi = (struct ifinfomsg*) NLMSG_DATA(nlm); rta = IFLA_RTA(ifi); rtaLen = len - NLMSG_LENGTH(sizeof(*ifi)); while (RTA_OK(rta, rtaLen)) { if (rta->rta_type == X_IFLA_STATS) { x_rtnl_link_stats *rtnlStats = (x_rtnl_link_stats*) RTA_DATA(rta); AbstractPort::PortStats *stats = portStats[ifi->ifi_index]; quint64 *maxStatsValue = portMaxStatsValue[ifi->ifi_index]; OstProto::LinkState *state = linkState[ifi->ifi_index]; if (!stats) break; if (rtnlStats->rx_packets >= stats->rxPkts) { stats->rxPps = (rtnlStats->rx_packets - stats->rxPkts) / kRefreshFreq_; } else { if (*maxStatsValue == 0) { *maxStatsValue = stats->rxPkts > kMaxValue32 ? kMaxValue64 : kMaxValue32; } stats->rxPps = ((*maxStatsValue - stats->rxPkts) + rtnlStats->rx_packets) / kRefreshFreq_; } if (rtnlStats->rx_bytes >= stats->rxBytes) { stats->rxBps = (rtnlStats->rx_bytes - stats->rxBytes) / kRefreshFreq_; } else { if (*maxStatsValue == 0) { *maxStatsValue = stats->rxBytes > kMaxValue32 ? kMaxValue64 : kMaxValue32; } stats->rxBps = ((*maxStatsValue - stats->rxBytes) + rtnlStats->rx_bytes) / kRefreshFreq_; } stats->rxPkts = rtnlStats->rx_packets; stats->rxBytes = rtnlStats->rx_bytes; if (rtnlStats->tx_packets >= stats->txPkts) { stats->txPps = (rtnlStats->tx_packets - stats->txPkts) / kRefreshFreq_; } else { if (*maxStatsValue == 0) { *maxStatsValue = stats->txPkts > kMaxValue32 ? kMaxValue64 : kMaxValue32; } stats->txPps = ((*maxStatsValue - stats->txPkts) + rtnlStats->tx_packets) / kRefreshFreq_; } if (rtnlStats->tx_bytes >= stats->txBytes) { stats->txBps = (rtnlStats->tx_bytes - stats->txBytes) / kRefreshFreq_; } else { if (*maxStatsValue == 0) { *maxStatsValue = stats->txBytes > kMaxValue32 ? kMaxValue64 : kMaxValue32; } stats->txBps = ((*maxStatsValue - stats->txBytes) + rtnlStats->tx_bytes) / kRefreshFreq_; } stats->txPkts = rtnlStats->tx_packets; stats->txBytes = rtnlStats->tx_bytes; // TODO: export detailed error stats stats->rxDrops = rtnlStats->rx_dropped + rtnlStats->rx_missed_errors; stats->rxErrors = rtnlStats->rx_errors; stats->rxFifoErrors = rtnlStats->rx_fifo_errors; stats->rxFrameErrors = rtnlStats->rx_crc_errors + rtnlStats->rx_length_errors + rtnlStats->rx_over_errors + rtnlStats->rx_frame_errors; Q_ASSERT(state); *state = ifi->ifi_flags & IFF_RUNNING ? OstProto::LinkStateUp : OstProto::LinkStateDown; break; } rta = RTA_NEXT(rta, rtaLen); } nlm = NLMSG_NEXT(nlm, len); } if (!done) goto _retry_recv; _try_later: QThread::sleep(kRefreshFreq_); } portStats.clear(); linkState.clear(); return 0; } int LinuxPort::StatsMonitor::setPromisc(const char * portName) { struct ifreq ifr; memset(&ifr, 0, sizeof(ifr)); strncpy(ifr.ifr_name, portName, sizeof(ifr.ifr_name)); if (ioctl(ioctlSocket_, SIOCGIFFLAGS, &ifr) != -1) { if ((ifr.ifr_flags & IFF_PROMISC) == 0) { ifr.ifr_flags |= IFF_PROMISC; if (ioctl(ioctlSocket_, SIOCSIFFLAGS, &ifr) != -1) { return 1; } else { qDebug("%s: failed to set promisc; " "SIOCSIFFLAGS failed (%s)", portName, strerror(errno)); } } } else { qDebug("%s: failed to set promisc; SIOCGIFFLAGS failed (%s)", portName, strerror(errno)); } return 0; } void LinuxPort::StatsMonitor::stop() { stop_ = true; } bool LinuxPort::StatsMonitor::waitForSetupFinished(int msecs) { QTime t; t.start(); while (!setupDone_) { if (t.elapsed() > msecs) return false; QThread::msleep(10); } return true; } #endif ostinato-1.3.0/server/linuxport.h000066400000000000000000000037001451413623100170760ustar00rootroot00000000000000/* Copyright (C) 2011 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _SERVER_LINUX_PORT_H #define _SERVER_LINUX_PORT_H #include #ifdef Q_OS_LINUX #include "pcapport.h" class LinuxPort : public PcapPort { public: LinuxPort(int id, const char *device); virtual ~LinuxPort(); void init(); virtual OstProto::LinkState linkState(); virtual bool hasExclusiveControl(); virtual bool setExclusiveControl(bool exclusive); static void fetchHostNetworkInfo(); static void freeHostNetworkInfo(); protected: class StatsMonitor: public QThread { public: StatsMonitor(); ~StatsMonitor(); void run(); void stop(); bool waitForSetupFinished(int msecs = 10000); private: int netlinkStats(); void procStats(); int setPromisc(const char* portName); static const int kRefreshFreq_ = 1; // in seconds bool stop_; bool setupDone_; int ioctlSocket_; }; bool isPromisc_; bool clearPromisc_; static QList allPorts_; static StatsMonitor *monitor_; // rx/tx stats for ALL ports private: void populateInterfaceInfo(); static struct nl_sock *netSock_; static struct nl_cache *linkCache_; static struct nl_cache *addressCache_; static struct nl_cache *routeCache_; }; #endif #endif ostinato-1.3.0/server/linuxutils.cpp000066400000000000000000000035671451413623100176200ustar00rootroot00000000000000/* Copyright (C) 2021 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #include "linuxutils.h" #include #include // Reads a text file (size < 4K) and returns content as a string // A terminating \n will be removed // There's no way to distinguish an empty file and error while reading QString readTextFile(QString fileName) { QFile file(fileName); if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { qWarning("Can't read %s", qUtf8Printable(fileName)); return QString(); } if (file.size() > 4096) { qWarning("Can't read %s - too large (%lld)", qUtf8Printable(fileName), file.size()); return QString(); } QTextStream in(&file); QString text = in.readAll(); file.close(); if (text.endsWith('\n')) text.chop(1); return text; } // Reads value from /sys/class/net// // and returns as string // XXX: reading from sysfs is discouraged QString sysfsAttrib(const char *device, const char *attribPath) { return readTextFile(QString("/sys/class/net/%1/%2") .arg(device).arg(attribPath)); } // Convenience overload QString sysfsAttrib(QString device, const char *attribPath) { return sysfsAttrib(qPrintable(device), attribPath); } ostinato-1.3.0/server/linuxutils.h000066400000000000000000000016231451413623100172540ustar00rootroot00000000000000/* Copyright (C) 2021 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _LINUX_UTILS_H #define _LINUX_UTILS_H #include QString readTextFile(QString fileName); QString sysfsAttrib(const char *device, const char *attribPath); QString sysfsAttrib(QString device, const char *attribPath); #endif ostinato-1.3.0/server/myservice.cpp000066400000000000000000001161371451413623100174040ustar00rootroot00000000000000/* Copyright (C) 2010-2016 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #include "myservice.h" #include "drone.h" #if 0 #include #include #include "qdebug.h" #include "../common/protocollistiterator.h" #include "../common/abstractprotocol.h" #endif #include "../common/framevalueattrib.h" #include "../common/streambase.h" #include "../rpc/pbrpccontroller.h" #include "device.h" #include "devicemanager.h" #include "portmanager.h" #include #include extern Drone *drone; extern char *version; MyService::MyService() { PortManager *portManager = PortManager::instance(); int n = portManager->portCount(); for (int i = 0; i < n; i++) { portInfo.append(portManager->port(i)); #if QT_VERSION >= 0x040400 portLock.append(new QReadWriteLock(QReadWriteLock::Recursive)); #else portLock.append(new QReadWriteLock()); #endif } } MyService::~MyService() { while (!portLock.isEmpty()) delete portLock.takeFirst(); //! \todo Use a singleton destroyer instead // http://www.research.ibm.com/designpatterns/pubs/ph-jun96.txt delete PortManager::instance(); } void MyService::getPortIdList(::google::protobuf::RpcController* /*controller*/, const ::OstProto::Void* /*request*/, ::OstProto::PortIdList* response, ::google::protobuf::Closure* done) { qDebug("In %s", __PRETTY_FUNCTION__); // No locks are needed here because the list does not change // and neither does the port_id for (int i = 0; i < portInfo.size(); i++) { ::OstProto::PortId *p; p = response->add_port_id(); p->set_id(portInfo[i]->id()); } done->Run(); } void MyService::getPortConfig(::google::protobuf::RpcController* /*controller*/, const ::OstProto::PortIdList* request, ::OstProto::PortConfigList* response, ::google::protobuf::Closure* done) { qDebug("In %s", __PRETTY_FUNCTION__); for (int i = 0; i < request->port_id_size(); i++) { int id; id = request->port_id(i).id(); if (id < portInfo.size()) { OstProto::Port *p; p = response->add_port(); portLock[id]->lockForRead(); portInfo[id]->protoDataCopyInto(p); portLock[id]->unlock(); } // XXX: no way to inform RPC caller of an invalid port! } done->Run(); } void MyService::modifyPort(::google::protobuf::RpcController* /*controller*/, const ::OstProto::PortConfigList* request, ::OstProto::Ack *response, ::google::protobuf::Closure* done) { bool error = false; QString notes; // notification needs to be on heap because signal/slot is across threads! OstProto::Notification *notif = new OstProto::Notification; qDebug("In %s", __PRETTY_FUNCTION__); for (int i = 0; i < request->port_size(); i++) { OstProto::Port port; int id; port = request->port(i); id = port.port_id().id(); if (id < portInfo.size()) { bool dirty; if (!portInfo[id]->canModify(port, &dirty)) { qDebug("Port %d cannot be modified - stop tx and retry", id); error = true; notes += QString("Port %1 modify: operation disallowed on " "transmitting port\n").arg(id); continue; } portLock[id]->lockForWrite(); portInfo[id]->modify(port); portLock[id]->unlock(); notif->mutable_port_id_list()->add_port_id()->set_id(id); } else { error = true; notes += QString("Port %1 modify: invalid port\n").arg(id); } } if (error) { response->set_status(OstProto::Ack::kRpcError); response->set_notes(notes.toStdString()); } else response->set_status(OstProto::Ack::kRpcSuccess); done->Run(); if (notif->port_id_list().port_id_size()) { notif->set_notif_type(OstProto::portConfigChanged); emit notification(notif->notif_type(), SharedProtobufMessage(notif)); } else delete notif; } void MyService::getStreamIdList(::google::protobuf::RpcController* controller, const ::OstProto::PortId* request, ::OstProto::StreamIdList* response, ::google::protobuf::Closure* done) { int portId; qDebug("In %s", __PRETTY_FUNCTION__); portId = request->id(); if ((portId < 0) || (portId >= portInfo.size())) goto _invalid_port; response->mutable_port_id()->set_id(portId); portLock[portId]->lockForRead(); for (int i = 0; i < portInfo[portId]->streamCount(); i++) { OstProto::StreamId *s; s = response->add_stream_id(); s->set_id(portInfo[portId]->streamAtIndex(i)->id()); } portLock[portId]->unlock(); done->Run(); return; _invalid_port: controller->SetFailed(QString("Port %1 get stream id list: invalid port") .arg(portId).toStdString()); done->Run(); } void MyService::getStreamConfig(::google::protobuf::RpcController* controller, const ::OstProto::StreamIdList* request, ::OstProto::StreamConfigList* response, ::google::protobuf::Closure* done) { int portId; qDebug("In %s", __PRETTY_FUNCTION__); portId = request->port_id().id(); if ((portId < 0) || (portId >= portInfo.size())) goto _invalid_port; response->mutable_port_id()->set_id(portId); portLock[portId]->lockForRead(); for (int i = 0; i < request->stream_id_size(); i++) { StreamBase *stream; OstProto::Stream *s; stream = portInfo[portId]->stream(request->stream_id(i).id()); if (!stream) continue; //! XXX: no way to inform RPC caller of invalid stream id s = response->add_stream(); stream->protoDataCopyInto(*s); } portLock[portId]->unlock(); done->Run(); return; _invalid_port: controller->SetFailed(QString("Port %1 get stream config: invalid port") .arg(portId).toStdString()); done->Run(); } void MyService::addStream(::google::protobuf::RpcController* controller, const ::OstProto::StreamIdList* request, ::OstProto::Ack* response, ::google::protobuf::Closure* done) { bool error = false; QString notes; int portId; qDebug("In %s", __PRETTY_FUNCTION__); portId = request->port_id().id(); if ((portId < 0) || (portId >= portInfo.size())) goto _invalid_port; if (portInfo[portId]->isTransmitOn()) goto _port_busy; portLock[portId]->lockForWrite(); for (int i = 0; i < request->stream_id_size(); i++) { StreamBase *stream; // If stream with same id as in request exists already ==> error!! stream = portInfo[portId]->stream(request->stream_id(i).id()); if (stream) { error = true; notes += QString("Port %1 Stream %2 add stream: " "stream already exists\n") .arg(portId).arg(request->stream_id(i).id()); continue; } // Append a new "default" stream - actual contents of the new stream is // expected in a subsequent "modifyStream" request - set the stream id // now itself however!!! stream = new StreamBase(portId); stream->setId(request->stream_id(i).id()); portInfo[portId]->addStream(stream); } portLock[portId]->unlock(); if (error) { response->set_status(OstProto::Ack::kRpcError); response->set_notes(notes.toStdString()); } else response->set_status(OstProto::Ack::kRpcSuccess); done->Run(); return; _port_busy: controller->SetFailed(QString("Port %1 add stream: operation disallowed on " "transmitting port") .arg(portId).toStdString()); goto _exit; _invalid_port: controller->SetFailed(QString("Port %1 add stream: invalid port") .arg(portId).toStdString()); _exit: done->Run(); } void MyService::deleteStream(::google::protobuf::RpcController* controller, const ::OstProto::StreamIdList* request, ::OstProto::Ack* response, ::google::protobuf::Closure* done) { bool error = false; QString notes; int portId; qDebug("In %s", __PRETTY_FUNCTION__); portId = request->port_id().id(); if ((portId < 0) || (portId >= portInfo.size())) goto _invalid_port; if (portInfo[portId]->isTransmitOn()) goto _port_busy; portLock[portId]->lockForWrite(); for (int i = 0; i < request->stream_id_size(); i++) { if (!portInfo[portId]->deleteStream(request->stream_id(i).id())) { error = true; notes += QString("Port %1 Stream %2 stream delete: " "stream not found\n") .arg(portId).arg(request->stream_id(i).id()); } } portLock[portId]->unlock(); if (error) { response->set_status(OstProto::Ack::kRpcError); response->set_notes(notes.toStdString()); } else response->set_status(OstProto::Ack::kRpcSuccess); done->Run(); return; _port_busy: controller->SetFailed(QString("Port %1 delete stream: operation disallowed " "on transmitting port") .arg(portId).toStdString()); goto _exit; _invalid_port: controller->SetFailed(QString("Port %1 delete stream: invalid port") .arg(portId).toStdString()); _exit: done->Run(); } void MyService::modifyStream(::google::protobuf::RpcController* controller, const ::OstProto::StreamConfigList* request, ::OstProto::Ack* response, ::google::protobuf::Closure* done) { bool error = false; QString notes; int portId; qDebug("In %s", __PRETTY_FUNCTION__); portId = request->port_id().id(); if ((portId < 0) || (portId >= portInfo.size())) goto _invalid_port; if (portInfo[portId]->isTransmitOn()) goto _port_busy; portLock[portId]->lockForWrite(); for (int i = 0; i < request->stream_size(); i++) { quint32 sid = request->stream(i).stream_id().id(); StreamBase *stream = portInfo[portId]->stream(sid); if (stream) { stream->protoDataCopyFrom(request->stream(i)); portInfo[portId]->setDirty(); } else { error = true; notes += QString("Port %1 Stream %2 modify stream: " "stream not found\n").arg(portId).arg(sid); } } portLock[portId]->unlock(); if (error) { response->set_status(OstProto::Ack::kRpcError); response->set_notes(notes.toStdString()); } else response->set_status(OstProto::Ack::kRpcSuccess); done->Run(); return; _port_busy: controller->SetFailed(QString("Port %1 modify stream: operation disallowed " "on transmitting port") .arg(portId).toStdString()); goto _exit; _invalid_port: controller->SetFailed(QString("Port %1 modify stream: invalid port") .arg(portId).toStdString()); _exit: done->Run(); } void MyService::startTransmit(::google::protobuf::RpcController* /*controller*/, const ::OstProto::PortIdList* request, ::OstProto::Ack* response, ::google::protobuf::Closure* done) { bool error = false; QString notes; qDebug("In %s", __PRETTY_FUNCTION__); // XXX: stream stats uses port tx duration to calculate per stream // rates; tx duration is for the last tx run only - so stream stats // should also correspond to the last run only. // Hence clear stream stats before Tx. // XXX: clear stream stats on ALL ports in user provided list before // starting Tx on any of them for (int i = 0; i < request->port_id_size(); i++) { int portId = request->port_id(i).id(); if ((portId < 0) || (portId >= portInfo.size())) { error = true; notes += QString("Port %1 start transmit: invalid port\n") .arg(portId); continue; } portLock[portId]->lockForWrite(); portInfo[portId]->resetStreamStatsAll(); portLock[portId]->unlock(); } for (int i = 0; i < request->port_id_size(); i++) { int frameError = 0; int portId = request->port_id(i).id(); if ((portId < 0) || (portId >= portInfo.size())) { continue; } portLock[portId]->lockForWrite(); if (portInfo[portId]->isDirty()) frameError = portInfo[portId]->updatePacketList(); portInfo[portId]->startTransmit(); portLock[portId]->unlock(); if (frameError) { error = true; notes += frameValueErrorNotes(portId, frameError); } } if (error) { response->set_status(OstProto::Ack::kRpcError); response->set_notes(notes.toStdString()); } else response->set_status(OstProto::Ack::kRpcSuccess); done->Run(); } void MyService::stopTransmit(::google::protobuf::RpcController* /*controller*/, const ::OstProto::PortIdList* request, ::OstProto::Ack* response, ::google::protobuf::Closure* done) { bool error = false; QString notes; qDebug("In %s", __PRETTY_FUNCTION__); for (int i = 0; i < request->port_id_size(); i++) { int portId; portId = request->port_id(i).id(); if ((portId < 0) || (portId >= portInfo.size())) { error = true; notes += QString("Port %1 stop transmit: invalid port\n") .arg(portId); continue; } portLock[portId]->lockForWrite(); portInfo[portId]->stopTransmit(); portLock[portId]->unlock(); } if (error) { response->set_status(OstProto::Ack::kRpcError); response->set_notes(notes.toStdString()); } else response->set_status(OstProto::Ack::kRpcSuccess); done->Run(); } void MyService::startCapture(::google::protobuf::RpcController* /*controller*/, const ::OstProto::PortIdList* request, ::OstProto::Ack* response, ::google::protobuf::Closure* done) { bool error = false; QString notes; qDebug("In %s", __PRETTY_FUNCTION__); for (int i = 0; i < request->port_id_size(); i++) { int portId; portId = request->port_id(i).id(); if ((portId < 0) || (portId >= portInfo.size())) { error = true; notes += QString("Port %1 start capture: invalid port\n") .arg(portId); continue; } portLock[portId]->lockForWrite(); portInfo[portId]->startCapture(); portLock[portId]->unlock(); } if (error) { response->set_status(OstProto::Ack::kRpcError); response->set_notes(notes.toStdString()); } else response->set_status(OstProto::Ack::kRpcSuccess); done->Run(); } void MyService::stopCapture(::google::protobuf::RpcController* /*controller*/, const ::OstProto::PortIdList* request, ::OstProto::Ack* response, ::google::protobuf::Closure* done) { bool error = false; QString notes; qDebug("In %s", __PRETTY_FUNCTION__); for (int i=0; i < request->port_id_size(); i++) { int portId; portId = request->port_id(i).id(); if ((portId < 0) || (portId >= portInfo.size())) { error = true; notes += QString("Port %1 stop capture: invalid port\n") .arg(portId); continue; } portLock[portId]->lockForWrite(); portInfo[portId]->stopCapture(); portLock[portId]->unlock(); } if (error) { response->set_status(OstProto::Ack::kRpcError); response->set_notes(notes.toStdString()); } else response->set_status(OstProto::Ack::kRpcSuccess); done->Run(); } void MyService::getCaptureBuffer(::google::protobuf::RpcController* controller, const ::OstProto::PortId* request, ::OstProto::CaptureBuffer* /*response*/, ::google::protobuf::Closure* done) { int portId; qDebug("In %s", __PRETTY_FUNCTION__); portId = request->id(); if ((portId < 0) || (portId >= portInfo.size())) goto _invalid_port; portLock[portId]->lockForWrite(); portInfo[portId]->stopCapture(); static_cast(controller)->setBinaryBlob( portInfo[portId]->captureData()); portLock[portId]->unlock(); done->Run(); return; _invalid_port: controller->SetFailed("invalid portid"); controller->SetFailed(QString("Port %1 get capture buffer: invalid port") .arg(portId).toStdString()); done->Run(); } void MyService::getStats(::google::protobuf::RpcController* /*controller*/, const ::OstProto::PortIdList* request, ::OstProto::PortStatsList* response, ::google::protobuf::Closure* done) { //qDebug("In %s", __PRETTY_FUNCTION__); for (int i = 0; i < request->port_id_size(); i++) { int portId; AbstractPort::PortStats stats; OstProto::PortStats *s; OstProto::PortState *st; portId = request->port_id(i).id(); if ((portId < 0) || (portId >= portInfo.size())) continue; // XXX: no way to inform RPC caller of invalid port s = response->add_port_stats(); s->mutable_port_id()->set_id(request->port_id(i).id()); st = s->mutable_state(); portLock[portId]->lockForRead(); st->set_link_state(portInfo[portId]->linkState()); st->set_is_transmit_on(portInfo[portId]->isTransmitOn()); st->set_is_capture_on(portInfo[portId]->isCaptureOn()); portInfo[portId]->stats(&stats); portLock[portId]->unlock(); #if 0 if (portId == 2) qDebug(">%llu", stats.rxPkts); #endif s->set_rx_pkts(stats.rxPkts); s->set_rx_bytes(stats.rxBytes); s->set_rx_pps(stats.rxPps); s->set_rx_bps(stats.rxBps); s->set_tx_pkts(stats.txPkts); s->set_tx_bytes(stats.txBytes); s->set_tx_pps(stats.txPps); s->set_tx_bps(stats.txBps); s->set_rx_drops(stats.rxDrops); s->set_rx_errors(stats.rxErrors); s->set_rx_fifo_errors(stats.rxFifoErrors); s->set_rx_frame_errors(stats.rxFrameErrors); } done->Run(); } void MyService::clearStats(::google::protobuf::RpcController* /*controller*/, const ::OstProto::PortIdList* request, ::OstProto::Ack* response, ::google::protobuf::Closure* done) { bool error = false; QString notes; qDebug("In %s", __PRETTY_FUNCTION__); for (int i = 0; i < request->port_id_size(); i++) { int portId; portId = request->port_id(i).id(); if ((portId < 0) || (portId >= portInfo.size())) { error = true; notes += QString("Port %1 clear statistics: invalid port\n") .arg(portId); continue; } portLock[portId]->lockForWrite(); portInfo[portId]->resetStats(); portLock[portId]->unlock(); } if (error) { response->set_status(OstProto::Ack::kRpcError); response->set_notes(notes.toStdString()); } else response->set_status(OstProto::Ack::kRpcSuccess); done->Run(); } void MyService::getStreamStats( ::google::protobuf::RpcController* /*controller*/, const ::OstProto::StreamGuidList* request, ::OstProto::StreamStatsList* response, ::google::protobuf::Closure* done) { qDebug("In %s", __PRETTY_FUNCTION__); for (int i = 0; i < request->port_id_list().port_id_size(); i++) { int portId; portId = request->port_id_list().port_id(i).id(); if ((portId < 0) || (portId >= portInfo.size())) continue; // XXX: no way to inform RPC caller of invalid port portLock[portId]->lockForRead(); if (request->stream_guid_size()) for (int j = 0; j < request->stream_guid_size(); j++) portInfo[portId]->streamStats(request->stream_guid(j).id(), response); else portInfo[portId]->streamStatsAll(response); portLock[portId]->unlock(); } done->Run(); } void MyService::clearStreamStats( ::google::protobuf::RpcController* /*controller*/, const ::OstProto::StreamGuidList* request, ::OstProto::Ack* response, ::google::protobuf::Closure* done) { bool error = false; QString notes; qDebug("In %s", __PRETTY_FUNCTION__); for (int i = 0; i < request->port_id_list().port_id_size(); i++) { int portId; portId = request->port_id_list().port_id(i).id(); if ((portId < 0) || (portId >= portInfo.size())) { error = true; notes += QString("Port %1 clear stream statistics: invalid port\n") .arg(portId); continue; } portLock[portId]->lockForWrite(); if (request->stream_guid_size()) for (int j = 0; j < request->stream_guid_size(); j++) portInfo[portId]->resetStreamStats( request->stream_guid(j).id()); else portInfo[portId]->resetStreamStatsAll(); portLock[portId]->unlock(); } if (error) { response->set_status(OstProto::Ack::kRpcError); response->set_notes(notes.toStdString()); } else response->set_status(OstProto::Ack::kRpcSuccess); done->Run(); } void MyService::checkVersion(::google::protobuf::RpcController* controller, const ::OstProto::VersionInfo* request, ::OstProto::VersionCompatibility* response, ::google::protobuf::Closure* done) { QString myVersion(version); QString clientVersion; QStringList my, client; qDebug("In %s", __PRETTY_FUNCTION__); my = myVersion.split('.'); Q_ASSERT(my.size() >= 2); clientVersion = QString::fromStdString(request->version()); client = clientVersion.split('.'); qDebug("client = %s, my = %s", qPrintable(clientVersion), qPrintable(myVersion)); if (client.size() < 2) goto _invalid_version; // Compare only major and minor numbers if (client[0] == my[0] && client[1] == my[1]) { response->set_result(OstProto::VersionCompatibility::kCompatible); static_cast(controller)->EnableNotif( request->client_name() == "python-ostinato" ? false : true); } else { response->set_result(OstProto::VersionCompatibility::kIncompatible); response->set_notes(QString("Drone needs controller version %1.%2.x") .arg(my[0], my[1]).toStdString()); static_cast(controller)->TriggerDisconnect(); } done->Run(); return; _invalid_version: controller->SetFailed("invalid version information"); done->Run(); } void MyService::build(::google::protobuf::RpcController* controller, const ::OstProto::BuildConfig* request, ::OstProto::Ack* response, ::google::protobuf::Closure* done) { QString notes; int portId; int frameError = 0; qDebug("In %s", __PRETTY_FUNCTION__); portId = request->port_id().id(); if ((portId < 0) || (portId >= portInfo.size())) goto _invalid_port; if (portInfo[portId]->isTransmitOn()) goto _port_busy; portLock[portId]->lockForWrite(); if (portInfo[portId]->isDirty()) frameError = portInfo[portId]->updatePacketList(); portLock[portId]->unlock(); if (frameError) { notes += frameValueErrorNotes(portId, frameError); response->set_status(OstProto::Ack::kRpcError); response->set_notes(notes.toStdString()); } else response->set_status(OstProto::Ack::kRpcSuccess); done->Run(); return; _port_busy: controller->SetFailed(QString("Port %1 build: operation disallowed " "on transmitting port") .arg(portId).toStdString()); goto _exit; _invalid_port: controller->SetFailed(QString("Port %1 build: invalid port") .arg(portId).toStdString()); _exit: done->Run(); } /* * =================================================================== * Device Emulation * =================================================================== * XXX: Streams and Devices are largely non-overlapping from a RPC * point of view but they *do* intersect e.g. when a stream is trying to * find its associated device and info from that device such as src/dst * mac addresses. For this reason, both set of RPCs share the common * port level locking * =================================================================== */ void MyService::getDeviceGroupIdList( ::google::protobuf::RpcController* controller, const ::OstProto::PortId* request, ::OstProto::DeviceGroupIdList* response, ::google::protobuf::Closure* done) { DeviceManager *devMgr; int portId; qDebug("In %s", __PRETTY_FUNCTION__); portId = request->id(); if ((portId < 0) || (portId >= portInfo.size())) goto _invalid_port; devMgr = portInfo[portId]->deviceManager(); response->mutable_port_id()->set_id(portId); portLock[portId]->lockForRead(); for (int i = 0; i < devMgr->deviceGroupCount(); i++) { OstProto::DeviceGroupId *dgid; dgid = response->add_device_group_id(); dgid->CopyFrom(devMgr->deviceGroupAtIndex(i)->device_group_id()); } portLock[portId]->unlock(); done->Run(); return; _invalid_port: controller->SetFailed(QString("Port %1 get device group id list: " "invalid port").arg(portId).toStdString()); done->Run(); } void MyService::getDeviceGroupConfig( ::google::protobuf::RpcController* controller, const ::OstProto::DeviceGroupIdList* request, ::OstProto::DeviceGroupConfigList* response, ::google::protobuf::Closure* done) { DeviceManager *devMgr; int portId; qDebug("In %s", __PRETTY_FUNCTION__); portId = request->port_id().id(); if ((portId < 0) || (portId >= portInfo.size())) goto _invalid_port; devMgr = portInfo[portId]->deviceManager(); response->mutable_port_id()->set_id(portId); portLock[portId]->lockForRead(); for (int i = 0; i < request->device_group_id_size(); i++) { const OstProto::DeviceGroup *dg; dg = devMgr->deviceGroup(request->device_group_id(i).id()); if (!dg) continue; // XXX: no way to inform RPC caller of invalid dgid response->add_device_group()->CopyFrom(*dg); } portLock[portId]->unlock(); done->Run(); return; _invalid_port: controller->SetFailed(QString("Port %1 get device group config: " "invalid port").arg(portId).toStdString()); done->Run(); } void MyService::addDeviceGroup( ::google::protobuf::RpcController* controller, const ::OstProto::DeviceGroupIdList* request, ::OstProto::Ack* response, ::google::protobuf::Closure* done) { bool error = false; QString notes; DeviceManager *devMgr; int portId; qDebug("In %s", __PRETTY_FUNCTION__); portId = request->port_id().id(); if ((portId < 0) || (portId >= portInfo.size())) goto _invalid_port; devMgr = portInfo[portId]->deviceManager(); if (portInfo[portId]->isTransmitOn()) goto _port_busy; portLock[portId]->lockForWrite(); for (int i = 0; i < request->device_group_id_size(); i++) { quint32 id = request->device_group_id(i).id(); const OstProto::DeviceGroup *dg = devMgr->deviceGroup(id); // If device group with same id as in request exists already ==> error! if (dg) { error = true; notes += QString("Port %1 DeviceGroup %2 add device group: " " device group already exists\n") .arg(portId).arg(id); continue; } devMgr->addDeviceGroup(id); } portLock[portId]->unlock(); if (error) { response->set_status(OstProto::Ack::kRpcError); response->set_notes(notes.toStdString()); } else response->set_status(OstProto::Ack::kRpcSuccess); done->Run(); return; _port_busy: controller->SetFailed(QString("Port %1 add device group: " "operation disallowed on transmitting port") .arg(portId).toStdString()); goto _exit; _invalid_port: controller->SetFailed(QString("Port %1 add device group: " "invalid port") .arg(portId).toStdString()); _exit: done->Run(); } void MyService::deleteDeviceGroup( ::google::protobuf::RpcController* controller, const ::OstProto::DeviceGroupIdList* request, ::OstProto::Ack* response, ::google::protobuf::Closure* done) { bool error = false; QString notes; DeviceManager *devMgr; int portId; qDebug("In %s", __PRETTY_FUNCTION__); portId = request->port_id().id(); if ((portId < 0) || (portId >= portInfo.size())) goto _invalid_port; devMgr = portInfo[portId]->deviceManager(); if (portInfo[portId]->isTransmitOn()) goto _port_busy; portLock[portId]->lockForWrite(); for (int i = 0; i < request->device_group_id_size(); i++) { quint32 id = request->device_group_id(i).id(); if (!devMgr->deleteDeviceGroup(id)) { error = true; notes += QString("Port %1 DeviceGroup %2 delete device group: " "device group not found\n").arg(portId).arg(id); } } portLock[portId]->unlock(); if (error) { response->set_status(OstProto::Ack::kRpcError); response->set_notes(notes.toStdString()); } else response->set_status(OstProto::Ack::kRpcSuccess); done->Run(); return; _port_busy: controller->SetFailed(QString("Port %1 delete device group: " "operation disallowed on transmitting port") .arg(portId).toStdString()); goto _exit; _invalid_port: controller->SetFailed(QString("Port %1 delete device group: " "invalid port").arg(portId).toStdString()); _exit: done->Run(); } void MyService::modifyDeviceGroup( ::google::protobuf::RpcController* controller, const ::OstProto::DeviceGroupConfigList* request, ::OstProto::Ack* response, ::google::protobuf::Closure* done) { bool error = false; QString notes; DeviceManager *devMgr; int portId; qDebug("In %s", __PRETTY_FUNCTION__); portId = request->port_id().id(); if ((portId < 0) || (portId >= portInfo.size())) goto _invalid_port; devMgr = portInfo[portId]->deviceManager(); if (portInfo[portId]->isTransmitOn()) goto _port_busy; portLock[portId]->lockForWrite(); for (int i = 0; i < request->device_group_size(); i++) { quint32 id = request->device_group(i).device_group_id().id(); if (!devMgr->modifyDeviceGroup(&request->device_group(i))) { error = true; notes += QString("Port %1 DeviceGroup %2 modify device group: " "device group not found\n").arg(portId).arg(id); } } portLock[portId]->unlock(); if (error) { response->set_status(OstProto::Ack::kRpcError); response->set_notes(notes.toStdString()); } else response->set_status(OstProto::Ack::kRpcSuccess); done->Run(); return; _port_busy: controller->SetFailed(QString("Port %1 modify device group: " "operation disallowed on transmitting port") .arg(portId).toStdString()); goto _exit; _invalid_port: controller->SetFailed(QString("Port %1 modify device group: " "invalid port").arg(portId).toStdString()); _exit: done->Run(); } void MyService::getDeviceList( ::google::protobuf::RpcController* controller, const ::OstProto::PortId* request, ::OstProto::PortDeviceList* response, ::google::protobuf::Closure* done) { DeviceManager *devMgr; int portId; qDebug("In %s", __PRETTY_FUNCTION__); portId = request->id(); if ((portId < 0) || (portId >= portInfo.size())) goto _invalid_port; devMgr = portInfo[portId]->deviceManager(); response->mutable_port_id()->set_id(portId); portLock[portId]->lockForRead(); devMgr->getDeviceList(response); portLock[portId]->unlock(); done->Run(); return; _invalid_port: controller->SetFailed(QString("Port %1 get device list: " "invalid port").arg(portId).toStdString()); done->Run(); } void MyService::resolveDeviceNeighbors( ::google::protobuf::RpcController* /*controller*/, const ::OstProto::PortIdList* request, ::OstProto::Ack* response, ::google::protobuf::Closure* done) { bool error = false; QString notes; qDebug("In %s", __PRETTY_FUNCTION__); for (int i = 0; i < request->port_id_size(); i++) { int portId; portId = request->port_id(i).id(); if ((portId < 0) || (portId >= portInfo.size())) { error = true; notes += QString("Port %1 resolve device neighbors: " "invalid port\n").arg(portId); continue; } portLock[portId]->lockForWrite(); portInfo[portId]->resolveDeviceNeighbors(); portLock[portId]->unlock(); } if (error) { response->set_status(OstProto::Ack::kRpcError); response->set_notes(notes.toStdString()); } else { // XXX: allow time for ARP/ND to finish; if more time is required, // the client should wait/check before invoking build() QThread::msleep(500); response->set_status(OstProto::Ack::kRpcSuccess); } done->Run(); } void MyService::clearDeviceNeighbors( ::google::protobuf::RpcController* /*controller*/, const ::OstProto::PortIdList* request, ::OstProto::Ack* response, ::google::protobuf::Closure* done) { bool error = false; QString notes; qDebug("In %s", __PRETTY_FUNCTION__); for (int i = 0; i < request->port_id_size(); i++) { int portId; portId = request->port_id(i).id(); if ((portId < 0) || (portId >= portInfo.size())) { error = true; notes += QString("Port %1 clear device neighbors: " "invalid port\n").arg(portId); continue; } portLock[portId]->lockForWrite(); portInfo[portId]->clearDeviceNeighbors(); portLock[portId]->unlock(); } if (error) { response->set_status(OstProto::Ack::kRpcError); response->set_notes(notes.toStdString()); } else response->set_status(OstProto::Ack::kRpcSuccess); done->Run(); } void MyService::getDeviceNeighbors( ::google::protobuf::RpcController* controller, const ::OstProto::PortId* request, ::OstProto::PortNeighborList* response, ::google::protobuf::Closure* done) { DeviceManager *devMgr; int portId; qDebug("In %s", __PRETTY_FUNCTION__); portId = request->id(); if ((portId < 0) || (portId >= portInfo.size())) goto _invalid_port; devMgr = portInfo[portId]->deviceManager(); response->mutable_port_id()->set_id(portId); portLock[portId]->lockForRead(); devMgr->getDeviceNeighbors(response); portLock[portId]->unlock(); done->Run(); return; _invalid_port: controller->SetFailed(QString("Port %1 get device neighbors: " "invalid port").arg(portId).toStdString()); done->Run(); } QString MyService::frameValueErrorNotes(int portId, int error) { if (!error) return QString(); QString pfx = QString("Port %1: ").arg(portId); auto errorFlags = static_cast(error); if (errorFlags & FrameValueAttrib::OutOfMemoryError) return pfx + "Error building packet buffers - out of buffer memory\n"; // If smac resolve fails, dmac will always fail - so check that first // and report only that so as not to confuse users (they may not realize // that without a source device, we have no ARP table to lookup for dmac) if (errorFlags & FrameValueAttrib::UnresolvedSrcMacError) return pfx + "Source mac resolve failed for one or more " "streams - Device matching stream's source IP not found\n"; if (errorFlags & FrameValueAttrib::UnresolvedDstMacError) return pfx + "Destination mac resolve failed for one or more " "streams - possible ARP/ND failure\n"; return QString(); } /* * =================================================================== * Friends * TODO: Encap these global functions into a DeviceBroker singleton? * =================================================================== */ quint64 getDeviceMacAddress(int portId, int streamId, int frameIndex) { MyService *service = drone->rpcService(); DeviceManager *devMgr = NULL; quint64 mac; if (!service) return 0; if ((portId >= 0) && (portId < service->portInfo.size())) devMgr = service->portInfo[portId]->deviceManager(); if (!devMgr || !devMgr->deviceCount()) return 0; /* * FIXME: We don't need lockForWrite, only lockForRead here. * However, this function is called in the following sequence * modifyPort() --> updatePacketList() --> frameValue() * where modifyPort has already taken a write lock. Qt allows * recursive locks, but not of a different type, so we are * forced to use lockForWrite here - till we find a different * solution. */ service->portLock[portId]->lockForWrite(); mac = service->portInfo[portId]->deviceMacAddress(streamId, frameIndex); service->portLock[portId]->unlock(); return mac; } quint64 getNeighborMacAddress(int portId, int streamId, int frameIndex) { MyService *service = drone->rpcService(); DeviceManager *devMgr = NULL; quint64 mac; if (!service) return 0; if ((portId >= 0) && (portId < service->portInfo.size())) devMgr = service->portInfo[portId]->deviceManager(); if (!devMgr || !devMgr->deviceCount()) return 0; /* * FIXME: We don't need lockForWrite, only lockForRead here. * See comment in getDeviceMacAddress() for more */ service->portLock[portId]->lockForWrite(); mac = service->portInfo[portId]->neighborMacAddress(streamId, frameIndex); service->portLock[portId]->unlock(); return mac; } ostinato-1.3.0/server/myservice.h000066400000000000000000000176431451413623100170530ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _MY_SERVICE_H #define _MY_SERVICE_H #include "../common/protocol.pb.h" #include "../rpc/sharedprotobufmessage.h" #include #include #include #define MAX_PKT_HDR_SIZE 1536 #define MAX_STREAM_NAME_SIZE 64 class AbstractPort; class MyService: public QObject, public OstProto::OstService { Q_OBJECT public: MyService(); virtual ~MyService(); /* Methods provided by the service */ virtual void getPortIdList(::google::protobuf::RpcController* controller, const ::OstProto::Void* request, ::OstProto::PortIdList* response, ::google::protobuf::Closure* done); virtual void getPortConfig(::google::protobuf::RpcController* controller, const ::OstProto::PortIdList* request, ::OstProto::PortConfigList* response, ::google::protobuf::Closure* done); virtual void modifyPort(::google::protobuf::RpcController* /*controller*/, const ::OstProto::PortConfigList* request, ::OstProto::Ack* response, ::google::protobuf::Closure* done); virtual void getStreamIdList(::google::protobuf::RpcController* controller, const ::OstProto::PortId* request, ::OstProto::StreamIdList* response, ::google::protobuf::Closure* done); virtual void getStreamConfig(::google::protobuf::RpcController* controller, const ::OstProto::StreamIdList* request, ::OstProto::StreamConfigList* response, ::google::protobuf::Closure* done); virtual void addStream(::google::protobuf::RpcController* controller, const ::OstProto::StreamIdList* request, ::OstProto::Ack* response, ::google::protobuf::Closure* done); virtual void deleteStream(::google::protobuf::RpcController* controller, const ::OstProto::StreamIdList* request, ::OstProto::Ack* response, ::google::protobuf::Closure* done); virtual void modifyStream(::google::protobuf::RpcController* controller, const ::OstProto::StreamConfigList* request, ::OstProto::Ack* response, ::google::protobuf::Closure* done); virtual void startTransmit(::google::protobuf::RpcController* controller, const ::OstProto::PortIdList* request, ::OstProto::Ack* response, ::google::protobuf::Closure* done); virtual void stopTransmit(::google::protobuf::RpcController* controller, const ::OstProto::PortIdList* request, ::OstProto::Ack* response, ::google::protobuf::Closure* done); virtual void startCapture(::google::protobuf::RpcController* controller, const ::OstProto::PortIdList* request, ::OstProto::Ack* response, ::google::protobuf::Closure* done); virtual void stopCapture(::google::protobuf::RpcController* controller, const ::OstProto::PortIdList* request, ::OstProto::Ack* response, ::google::protobuf::Closure* done); virtual void getCaptureBuffer(::google::protobuf::RpcController* controller, const ::OstProto::PortId* request, ::OstProto::CaptureBuffer* response, ::google::protobuf::Closure* done); virtual void getStats(::google::protobuf::RpcController* controller, const ::OstProto::PortIdList* request, ::OstProto::PortStatsList* response, ::google::protobuf::Closure* done); virtual void clearStats(::google::protobuf::RpcController* controller, const ::OstProto::PortIdList* request, ::OstProto::Ack* response, ::google::protobuf::Closure* done); virtual void getStreamStats(::google::protobuf::RpcController* controller, const ::OstProto::StreamGuidList* request, ::OstProto::StreamStatsList* response, ::google::protobuf::Closure* done); virtual void clearStreamStats(::google::protobuf::RpcController* controller, const ::OstProto::StreamGuidList* request, ::OstProto::Ack* response, ::google::protobuf::Closure* done); virtual void checkVersion(::google::protobuf::RpcController* controller, const ::OstProto::VersionInfo* request, ::OstProto::VersionCompatibility* response, ::google::protobuf::Closure* done); virtual void build(::google::protobuf::RpcController* controller, const ::OstProto::BuildConfig* request, ::OstProto::Ack* response, ::google::protobuf::Closure* done); // DeviceGroup and Protocol Emulation virtual void getDeviceGroupIdList( ::google::protobuf::RpcController* controller, const ::OstProto::PortId* request, ::OstProto::DeviceGroupIdList* response, ::google::protobuf::Closure* done); virtual void getDeviceGroupConfig( ::google::protobuf::RpcController* controller, const ::OstProto::DeviceGroupIdList* request, ::OstProto::DeviceGroupConfigList* response, ::google::protobuf::Closure* done); virtual void addDeviceGroup( ::google::protobuf::RpcController* controller, const ::OstProto::DeviceGroupIdList* request, ::OstProto::Ack* response, ::google::protobuf::Closure* done); virtual void deleteDeviceGroup( ::google::protobuf::RpcController* controller, const ::OstProto::DeviceGroupIdList* request, ::OstProto::Ack* response, ::google::protobuf::Closure* done); virtual void modifyDeviceGroup( ::google::protobuf::RpcController* controller, const ::OstProto::DeviceGroupConfigList* request, ::OstProto::Ack* response, ::google::protobuf::Closure* done); virtual void getDeviceList( ::google::protobuf::RpcController* controller, const ::OstProto::PortId* request, ::OstProto::PortDeviceList* response, ::google::protobuf::Closure* done); virtual void resolveDeviceNeighbors( ::google::protobuf::RpcController* controller, const ::OstProto::PortIdList* request, ::OstProto::Ack* response, ::google::protobuf::Closure* done); virtual void clearDeviceNeighbors( ::google::protobuf::RpcController* controller, const ::OstProto::PortIdList* request, ::OstProto::Ack* response, ::google::protobuf::Closure* done); virtual void getDeviceNeighbors( ::google::protobuf::RpcController* controller, const ::OstProto::PortId* request, ::OstProto::PortNeighborList* response, ::google::protobuf::Closure* done); friend quint64 getDeviceMacAddress( int portId, int streamId, int frameIndex); friend quint64 getNeighborMacAddress( int portId, int streamId, int frameIndex); signals: void notification(int notifType, SharedProtobufMessage notifData); private: QString frameValueErrorNotes(int portId, int error); /* * NOTES: * - AbstractPort::id() and index into portInfo[] are same! * - portLock[] size and order should be same as portInfo[] as the * same index is used for both. * - we assume that once populated by the constructor, the list(s) * never change (objects in the list can change, but not the list itself) * - locking is at port granularity, not at stream granularity - for now * this seems sufficient. Revisit later, if required */ QList portInfo; QList portLock; }; #endif ostinato-1.3.0/server/nulldevice.h000066400000000000000000000026471451413623100171750ustar00rootroot00000000000000/* Copyright (C) 2018 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _NULL_DEVICE_H #define _NULL_DEVICE_H #include "device.h" /*! * NullDevice does nothing. * Since the base Device class is abstract, NullDevice is a convenience * subclass that can be instantiated for DeviceKey related operations */ class NullDevice: public Device { public: using Device::Device; virtual void receivePacket(PacketBuffer* /*pktBuf*/) {} virtual void clearNeighbors(Device::NeighborSet /*set*/) {} virtual void getNeighbors(OstEmul::DeviceNeighborList* /*neighbors*/) {} protected: virtual quint64 arpLookup(quint32 /*ip*/) { return 0; } virtual quint64 ndpLookup(UInt128 /*ip*/) { return 0; } virtual void sendArpRequest(quint32 /*tgtIp*/) {} virtual void sendNeighborSolicit(UInt128 /*tgtIp*/) {} }; #endif ostinato-1.3.0/server/packetbuffer.cpp000066400000000000000000000040601451413623100200260ustar00rootroot00000000000000/* Copyright (C) 2015 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #include "packetbuffer.h" // PacketBuffer with full control PacketBuffer::PacketBuffer(int size) { if (size == 0) size = 1600; buffer_ = new uchar[size]; is_own_buffer_ = true; head_ = data_ = tail_ = buffer_; end_ = head_ + size; } // PacketBuffer wrapping already existing const buffer PacketBuffer::PacketBuffer(const uchar *buffer, int size) { // FIXME: ugly const_cast hack!! buffer_ = const_cast(buffer); is_own_buffer_ = false; head_ = data_ = buffer_; tail_ = end_ = buffer_ + size; } PacketBuffer::~PacketBuffer() { if (is_own_buffer_) delete[] buffer_; } int PacketBuffer::length() const { return tail_ - data_; } uchar* PacketBuffer::head() const { return head_; } uchar* PacketBuffer::data() const { return data_; } uchar* PacketBuffer::tail() const { return tail_; } uchar* PacketBuffer::end() const { return end_; } void PacketBuffer::reserve(int len) { data_ += len; tail_ += len; } uchar* PacketBuffer::pull(int len) { if ((tail_ - data_) < len) return NULL; data_ += len; return data_; } uchar* PacketBuffer::push(int len) { if ((data_ - head_) < len) return NULL; data_ -= len; return data_; } uchar* PacketBuffer::put(int len) { uchar *oldTail = tail_; if ((end_ - tail_) < len) return NULL; tail_ += len; return oldTail; } ostinato-1.3.0/server/packetbuffer.h000066400000000000000000000022771451413623100175030ustar00rootroot00000000000000/* Copyright (C) 2015 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _PACKET_BUFFER_H #define _PACKET_BUFFER_H #include class PacketBuffer { public: PacketBuffer(int size = 0); PacketBuffer(const uchar *buffer, int size); ~PacketBuffer(); int length() const; uchar* head() const; uchar* data() const; uchar* tail() const; uchar* end() const; void reserve(int len); uchar* pull(int len); uchar* push(int len); uchar* put(int len); private: uchar *buffer_; bool is_own_buffer_; uchar *head_, *data_, *tail_, *end_; }; #endif ostinato-1.3.0/server/packetsequence.h000066400000000000000000000064041451413623100200360ustar00rootroot00000000000000/* Copyright (C) 2010-2016 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _PACKET_SEQUENCE_H #define _PACKET_SEQUENCE_H #include "../common/packet.h" #include "../common/sign.h" #include "pcapextra.h" #include "streamstats.h" class PacketSequence { public: PacketSequence(bool trackGuidStats) { trackGuidStats_ = trackGuidStats; sendQueue_ = pcap_sendqueue_alloc(1*1024*1024); lastPacket_ = NULL; packets_ = 0; bytes_ = 0; usecDuration_ = 0; repeatCount_ = 1; repeatSize_ = 1; usecDelay_ = 0; ttagL4CksumOffset_ = 0; } ~PacketSequence() { pcap_sendqueue_destroy(sendQueue_); } bool hasFreeSpace(int size) { if ((sendQueue_->len + size) <= sendQueue_->maxlen) return true; else return false; } int appendPacket(const struct pcap_pkthdr *pktHeader, const uchar *pktData) { int ret; if (lastPacket_) { usecDuration_ += (pktHeader->ts.tv_sec - lastPacket_->ts.tv_sec) * long(1e6); usecDuration_ += (pktHeader->ts.tv_usec - lastPacket_->ts.tv_usec); } packets_++; bytes_ += pktHeader->caplen; lastPacket_ = (struct pcap_pkthdr *) (sendQueue_->buffer + sendQueue_->len); ret = pcap_sendqueue_queue(sendQueue_, pktHeader, pktData); if (trackGuidStats_ && (ret >= 0)) { uint guid; if (SignProtocol::packetGuid(pktData, pktHeader->caplen, &guid)) { streamStatsMeta_[guid].tx_pkts++; streamStatsMeta_[guid].tx_bytes += pktHeader->caplen; } } // TODO: A PacketSequence belongs to a unique stream only in case of // sequential streams; for interleaved streams, we have only a single // packet set (with one or more sequences) containing packets from // multiple streams. To support this, we need to make l4cksum a packet // property not a sequence property // Till the above is fixed, Ttag packets will have wrong checksum #if 0 if (trackGuidStats_ && (packets_ == 1)) // first packet of seq ttagL4CksumOffset_ = Packet::l4ChecksumOffset(pktData, pktHeader->caplen); #endif return ret; } pcap_send_queue *sendQueue_; struct pcap_pkthdr *lastPacket_; long packets_; long bytes_; ulong usecDuration_; int repeatCount_; int repeatSize_; long usecDelay_; quint16 ttagL4CksumOffset_; // For ttag packets StreamStats streamStatsMeta_; private: bool trackGuidStats_; }; #endif ostinato-1.3.0/server/params.cpp000066400000000000000000000042031451413623100166470ustar00rootroot00000000000000/* Copyright (C) 2019 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #include "params.h" #include "pcapextra.h" #include "turbo.h" #include extern char *version; extern char *revision; Params::Params() { logsDisabled_ = true; myPort_ = 7878; } int Params::parseCommandLine(int argc, char* argv[]) { int c, n = 0; opterr = 0; while ((c = getopt (argc, argv, "dhp:v")) != -1) { switch (c) { case 'd': logsDisabled_ = false; break; case 'p': myPort_ = atoi(optarg); break; case 'v': printf("Ostinato Drone %s rev %s\n", version, revision); printf("PCAP Lib: %s\n", pcap_lib_version()); #ifdef Q_OS_WIN32 printf("Service npf status %s\n", pcapServiceStatus(L"npf")); printf("Service npcap status %s\n", pcapServiceStatus(L"npcap")); #endif exit(0); case 'h': default: if (c != 'h') { if (processTurboOption(optopt)) continue; } printf("usage: %s [-dhv] [-p ]\n", argv[0]); exit(1); } n++; } for (int i = optind; i < argc; i++, n++) args_ << argv[i]; return n; } bool Params::optLogsDisabled() { return logsDisabled_; } int Params::servicePortNumber() { return myPort_; } int Params::argumentCount() { return args_.size(); } QString Params::argument(int index) { return index < args_.size() ? args_.at(index) : QString(); } ostinato-1.3.0/server/params.h000066400000000000000000000020331451413623100163130ustar00rootroot00000000000000/* Copyright (C) 2019 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _PARAMS_H #define _PARAMS_H #include class Params { public: Params(); int parseCommandLine(int argc, char* argv[]); bool optLogsDisabled(); int servicePortNumber(); int argumentCount(); QString argument(int index); private: bool logsDisabled_; int myPort_; QStringList args_; }; extern Params appParams; #endif ostinato-1.3.0/server/pcapextra.cpp000066400000000000000000000065061451413623100173630ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #include "pcapextra.h" #include // memcpy() #include // malloc(), free() /* NOTE: All code borrowed from WinPcap */ #ifdef Q_OS_WIN32 const char* pcapServiceStatus(const wchar_t* name) { SC_HANDLE scm, svc; SERVICE_STATUS_PROCESS svcStatus; DWORD size; BOOL result; const char *status = "unknown"; scm = OpenSCManager(nullptr, nullptr, SC_MANAGER_ENUMERATE_SERVICE); if(!scm) goto _exit; svc = OpenService(scm, name, SERVICE_QUERY_STATUS); if(!svc) goto _close_exit; result = QueryServiceStatusEx(svc, SC_STATUS_PROCESS_INFO, reinterpret_cast(&svcStatus), sizeof(svcStatus), &size); if(result == 0) goto _close_exit; switch(svcStatus.dwCurrentState) { case SERVICE_CONTINUE_PENDING: status = "continue pending"; break; case SERVICE_PAUSE_PENDING: status = "pause pending"; break; case SERVICE_PAUSED: status = "paused"; break; case SERVICE_RUNNING: status = "running"; break; case SERVICE_START_PENDING: status = "start pending"; break; case SERVICE_STOP_PENDING: status = "stop pending"; break; case SERVICE_STOPPED: status = "stopped"; break; } _close_exit: if (svc) CloseServiceHandle(svc); if (scm) CloseServiceHandle(scm); _exit: return status; } #else // non-Windows pcap_send_queue* pcap_sendqueue_alloc (u_int memsize) { pcap_send_queue *tqueue; /* Allocate the queue */ tqueue = (pcap_send_queue*)malloc(sizeof(pcap_send_queue)); if(tqueue == NULL){ return NULL; } /* Allocate the buffer */ tqueue->buffer = (char*)malloc(memsize); if(tqueue->buffer == NULL){ free(tqueue); return NULL; } tqueue->maxlen = memsize; tqueue->len = 0; return tqueue; } void pcap_sendqueue_destroy (pcap_send_queue *queue) { free(queue->buffer); free(queue); } int pcap_sendqueue_queue (pcap_send_queue *queue, const struct pcap_pkthdr *pkt_header, const u_char *pkt_data) { if(queue->len + sizeof(struct pcap_pkthdr) + pkt_header->caplen > queue->maxlen) { return -1; } /* Copy the pcap_pkthdr header*/ memcpy(queue->buffer + queue->len, pkt_header, sizeof(struct pcap_pkthdr)); queue->len += sizeof(struct pcap_pkthdr); /* copy the packet */ memcpy(queue->buffer + queue->len, pkt_data, pkt_header->caplen); queue->len += pkt_header->caplen; return 0; } #endif ostinato-1.3.0/server/pcapextra.h000066400000000000000000000024001451413623100170150ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _PCAP_EXTRA_H #define _PCAP_EXTRA_H #include #include #ifdef Q_OS_WIN32 const char* pcapServiceStatus(const wchar_t *name); #else // non-windows #ifndef PCAP_OPENFLAG_PROMISCUOUS #define PCAP_OPENFLAG_PROMISCUOUS 1 #endif struct pcap_send_queue { u_int maxlen; u_int len; char *buffer; }; pcap_send_queue* pcap_sendqueue_alloc (u_int memsize); void pcap_sendqueue_destroy (pcap_send_queue *queue); int pcap_sendqueue_queue (pcap_send_queue *queue, const struct pcap_pkthdr *pkt_header, const u_char *pkt_data); #endif #endif ostinato-1.3.0/server/pcapport.cpp000066400000000000000000000500731451413623100172220ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #include "pcapport.h" #include "devicemanager.h" #include "packetbuffer.h" #include pcap_if_t *PcapPort::deviceList_ = NULL; PcapPort::PcapPort(int id, const char *device) : AbstractPort(id, device) { monitorRx_ = new PortMonitor(device, kDirectionRx, &stats_); monitorTx_ = new PortMonitor(device, kDirectionTx, &stats_); transmitter_ = new PcapTransmitter(device); capturer_ = new PortCapturer(device); emulXcvr_ = new EmulationTransceiver(device, deviceManager_); txTtagStatsPoller_ = new PcapTxTtagStats(device, id); rxStatsPoller_ = new PcapRxStats(device, id); if (!monitorRx_->handle() || !monitorTx_->handle()) isUsable_ = false; if (!deviceList_) { char errbuf[PCAP_ERRBUF_SIZE]; if (pcap_findalldevs(&deviceList_, errbuf) == -1) qDebug("Error in pcap_findalldevs_ex: %s\n", errbuf); } for (pcap_if_t *dev = deviceList_; dev != NULL; dev = dev->next) { if (strcmp(device, dev->name) == 0) { if (dev->name) data_.set_name(dev->name); if (dev->description) data_.set_description(dev->description); //! \todo set port IP addr also } } } void PcapPort::init() { AbstractPort::init(); if (!monitorTx_->isDirectional()) transmitter_->useExternalStats(&stats_); transmitter_->setHandle(monitorRx_->handle()); updateNotes(); monitorRx_->start(); monitorTx_->start(); } PcapPort::~PcapPort() { qDebug("In %s", __FUNCTION__); if (monitorRx_) monitorRx_->stop(); if (monitorTx_) monitorTx_->stop(); txTtagStatsPoller_->stop(); delete txTtagStatsPoller_; rxStatsPoller_->stop(); delete rxStatsPoller_; delete emulXcvr_; delete capturer_; delete transmitter_; if (monitorRx_) monitorRx_->wait(); delete monitorRx_; if (monitorTx_) monitorTx_->wait(); delete monitorTx_; } void PcapPort::updateNotes() { QString notes; if ((!monitorRx_->isPromiscuous()) || (!monitorTx_->isPromiscuous())) notes.append("
  • Non Promiscuous Mode
  • "); if (!monitorRx_->isDirectional() && !hasExclusiveControl()) notes.append("
  • Rx Frames/Bytes: Includes non Ostinato Tx pkts also (Tx by Ostinato are not included)
  • "); if (!monitorTx_->isDirectional() && !hasExclusiveControl()) notes.append("
  • Tx Frames/Bytes: Only Ostinato Tx pkts (Tx by others NOT included)
  • "); if (notes.isEmpty()) data_.set_notes(""); else data_.set_notes(QString("Limitation(s)" "
      %1
    " "Rx/Tx Rates are also subject to above limitation(s)"). arg(notes).toStdString()); } bool PcapPort::setTrackStreamStats(bool enable) { bool val = enable ? startStreamStatsTracking() : stopStreamStatsTracking(); if (val) AbstractPort::setTrackStreamStats(enable); return val; } bool PcapPort::setRateAccuracy(AbstractPort::Accuracy accuracy) { if (transmitter_->setRateAccuracy(accuracy)) { AbstractPort::setRateAccuracy(accuracy); return true; } return false; } void PcapPort::updateStreamStats() { QWriteLocker lock(&streamStatsLock_); // XXX: Transmitter may also 'adjust' rx stats in some cases (pcap // direction not supported platforms) transmitter_->updateTxRxStreamStats(streamStats_); rxStatsPoller_->updateRxStreamStats(streamStats_); // Dump tx/rx stats poller debug stats qDebug("port %d txTtagStatsPoller: %s", id(), qUtf8Printable(txTtagStatsPoller_->debugStats())); qDebug("port %d rxStatsPoller: %s", id(), qUtf8Printable(rxStatsPoller_->debugStats())); } void PcapPort::startDeviceEmulation() { emulXcvr_->start(); } void PcapPort::stopDeviceEmulation() { emulXcvr_->stop(); } int PcapPort::sendEmulationPacket(PacketBuffer *pktBuf) { return emulXcvr_->transmitPacket(pktBuf); } bool PcapPort::startStreamStatsTracking() { if (!transmitter_->setStreamStatsTracking(true)) goto _tx_fail; if (!txTtagStatsPoller_->start()) goto _tx_ttag_fail; if (!rxStatsPoller_->start()) goto _rx_fail; /* * If RxPoller receives both IN and OUT packets, packets Tx on this * port will also be received by it and we consider it to be a Rx (IN) * packet incorrectly - so adjust Rx stats for this case * XXX - ideally, RxPoller should do this adjustment, but given our * design, it is easier to implement in transmitter */ transmitter_->adjustRxStreamStats(!rxStatsPoller_->isDirectional()); return true; _rx_fail: txTtagStatsPoller_->stop(); _tx_ttag_fail: transmitter_->setStreamStatsTracking(false); _tx_fail: qWarning("failed to start stream stats tracking"); return false; } bool PcapPort::stopStreamStatsTracking() { bool ret = true; if (!transmitter_->setStreamStatsTracking(false)) { qWarning("failed to stop Transmitter stream stats tracking"); ret = false; } if (!txTtagStatsPoller_->stop()) { qWarning("failed to stop TxTtag stream stats thread"); ret = false; } if (!rxStatsPoller_->stop()) { qWarning("failed to stop Rx stream stats thread"); ret = false; } return ret; } /* * ------------------------------------------------------------------- * * Port Monitor * ------------------------------------------------------------------- * */ PcapPort::PortMonitor::PortMonitor(const char *device, Direction direction, AbstractPort::PortStats *stats) { int ret; char errbuf[PCAP_ERRBUF_SIZE] = ""; bool noLocalCapture; setObjectName(QString("Mon%1:%2") .arg(direction == kDirectionRx ? "Rx" : "Tx") .arg(device)); direction_ = direction; isDirectional_ = true; isPromisc_ = true; noLocalCapture = true; stats_ = stats; stop_ = false; _retry: #ifdef Q_OS_WIN32 int flags = 0; if (isPromisc_) flags |= PCAP_OPENFLAG_PROMISCUOUS; if (noLocalCapture) flags |= PCAP_OPENFLAG_NOCAPTURE_LOCAL; handle_ = pcap_open(device, 64 /* FIXME */, flags, 1000 /* ms */, NULL, errbuf); #else handle_ = pcap_open_live(device, 64 /* FIXME */, int(isPromisc_), 1000 /* ms */, errbuf); #endif if (handle_ == NULL) { if (isPromisc_ && QString(errbuf).contains("promiscuous")) { qDebug("Can't set promiscuous mode, trying non-promisc %s", device); isPromisc_ = false; goto _retry; } else if (noLocalCapture && QString(errbuf).contains("loopback")) { qDebug("Can't set no local capture mode %s", device); noLocalCapture = false; goto _retry; } else goto _open_error; } #ifdef Q_OS_WIN32 // pcap_setdirection() API is not supported in Windows. // NOTE: WinPcap 4.1.1 and above exports a dummy API that returns -1 // but since we would like to work with previous versions of WinPcap // also, we assume the API does not exist ret = -1; #else switch (direction_) { case kDirectionRx: ret = pcap_setdirection(handle_, PCAP_D_IN); break; case kDirectionTx: ret = pcap_setdirection(handle_, PCAP_D_OUT); break; default: ret = -1; // avoid 'may be used uninitialized' warning Q_ASSERT(false); } #endif if (ret < 0) goto _set_direction_error; return; _set_direction_error: qDebug("Error setting direction(%d) %s: %s\n", direction, device, pcap_geterr(handle_)); isDirectional_ = false; return; _open_error: qDebug("%s: Error opening port %s: %s\n", __FUNCTION__, device, errbuf); } PcapPort::PortMonitor::~PortMonitor() { if (handle_) pcap_close(handle_); } void PcapPort::PortMonitor::run() { while (!stop_) { int ret; struct pcap_pkthdr *hdr; const uchar *data; ret = pcap_next_ex(handle_, &hdr, &data); switch (ret) { case 1: switch (direction_) { case kDirectionRx: stats_->rxPkts++; stats_->rxBytes += hdr->len; break; case kDirectionTx: if (isDirectional_) { stats_->txPkts++; stats_->txBytes += hdr->len; } break; default: Q_ASSERT(false); } //! \todo TODO pkt/bit rates break; case 0: //qDebug("%s: timeout. continuing ...", __PRETTY_FUNCTION__); continue; case -1: qWarning("%s: error reading packet (%d): %s", __PRETTY_FUNCTION__, ret, pcap_geterr(handle_)); break; case -2: qWarning("%s: error reading packet (%d): %s", __PRETTY_FUNCTION__, ret, pcap_geterr(handle_)); break; default: qFatal("%s: Unexpected return value %d", __PRETTY_FUNCTION__, ret); } } } void PcapPort::PortMonitor::stop() { stop_ = true; if (handle()) pcap_breakloop(handle()); } /* * ------------------------------------------------------------------- * * Port Capturer * ------------------------------------------------------------------- * */ PcapPort::PortCapturer::PortCapturer(const char *device) { device_ = QString::fromLatin1(device); setObjectName(QString("Capture:%1").arg(device_)); stop_ = false; state_ = kNotStarted; if (!capFile_.open()) qWarning("Unable to open temp cap file"); qDebug("cap file = %s", qPrintable(capFile_.fileName())); dumpHandle_ = NULL; handle_ = NULL; } PcapPort::PortCapturer::~PortCapturer() { capFile_.close(); } void PcapPort::PortCapturer::run() { int flag = PCAP_OPENFLAG_PROMISCUOUS; char errbuf[PCAP_ERRBUF_SIZE] = ""; qDebug("In %s", __PRETTY_FUNCTION__); if (!capFile_.isOpen()) { qWarning("temp cap file is not open"); goto _exit; } _retry: handle_ = pcap_open_live(qPrintable(device_), 65535, flag, 1000 /* ms */, errbuf); if (handle_ == NULL) { if (flag && QString(errbuf).contains("promiscuous")) { qDebug("%s:can't set promiscuous mode, trying non-promisc", qPrintable(device_)); flag = 0; goto _retry; } else { qDebug("%s: Error opening port %s: %s\n", __FUNCTION__, qPrintable(device_), errbuf); goto _exit; } } dumpHandle_ = pcap_dump_open(handle_, qPrintable(capFile_.fileName())); if (!dumpHandle_) { qDebug("failed to start capture: %s", pcap_geterr(handle_)); goto _exit; } PcapSession::preRun(); state_ = kRunning; while (1) { int ret; struct pcap_pkthdr *hdr; const uchar *data; ret = pcap_next_ex(handle_, &hdr, &data); switch (ret) { case 1: pcap_dump((uchar*) dumpHandle_, hdr, data); break; case 0: // timeout: just go back to the loop break; case -1: qWarning("%s: error reading packet (%d): %s", __PRETTY_FUNCTION__, ret, pcap_geterr(handle_)); break; case -2: qDebug("Loop/signal break or some other error"); break; default: qWarning("%s: Unexpected return value %d", __PRETTY_FUNCTION__, ret); stop_ = true; } if (stop_) { qDebug("user requested capture stop"); break; } } PcapSession::postRun(); pcap_dump_close(dumpHandle_); pcap_close(handle_); dumpHandle_ = NULL; handle_ = NULL; stop_ = false; _exit: state_ = kFinished; } void PcapPort::PortCapturer::start() { // FIXME: return error if (state_ == kRunning) { qWarning("Capture start requested but is already running!"); return; } state_ = kNotStarted; QThread::start(); while (state_ == kNotStarted) QThread::msleep(10); } void PcapPort::PortCapturer::stop() { if (state_ == kRunning) { stop_ = true; PcapSession::stop(); while (state_ == kRunning) QThread::msleep(10); } else { // FIXME: return error qWarning("Capture stop requested but is not running!"); return; } } bool PcapPort::PortCapturer::isRunning() { return (state_ == kRunning); } QFile* PcapPort::PortCapturer::captureFile() { return &capFile_; } /* * ------------------------------------------------------------------- * * Transmit+Receiver for Device/ProtocolEmulation * ------------------------------------------------------------------- * */ PcapPort::EmulationTransceiver::EmulationTransceiver(const char *device, DeviceManager *deviceManager) { device_ = QString::fromLatin1(device); setObjectName(QString("EmulXcvr:%1").arg(device_)); deviceManager_ = deviceManager; stop_ = false; state_ = kNotStarted; handle_ = NULL; } PcapPort::EmulationTransceiver::~EmulationTransceiver() { stop(); } void PcapPort::EmulationTransceiver::run() { int flags = PCAP_OPENFLAG_PROMISCUOUS; char errbuf[PCAP_ERRBUF_SIZE] = ""; struct bpf_program bpf; #if 0 const char *capture_filter = "arp or icmp or icmp6 or " "(vlan and (arp or icmp or icmp6)) or " "(vlan and vlan and (arp or icmp or icmp6)) or " "(vlan and vlan and vlan and (arp or icmp or icmp6)) or " "(vlan and vlan and vlan and vlan and (arp or icmp or icmp6))"; /* Ideally we should use the above filter, but the 'vlan' capture filter in libpcap is implemented as a kludge. From the pcap-filter man page - vlan [vlan_id] Note that the first vlan keyword encountered in expression changes the decoding offsets for the remainder of expression on the assumption that the packet is a VLAN packet. The vlan [vlan_id] expression may be used more than once, to filter on VLAN hierarchies. Each use of that expression increments the filter offsets by 4. See https://ask.wireshark.org/questions/31953/unusual-behavior-with-stacked-vlan-tags-and-capture-filter So we use the modified filter expression that works as we intend. If ever libpcap changes their implementation, this will need to change as well. */ #else const char *capture_filter = "arp or icmp or icmp6 or " "(vlan and (arp or icmp or icmp6)) or " "(vlan and (arp or icmp or icmp6)) or " "(vlan and (arp or icmp or icmp6)) or " "(vlan and (arp or icmp or icmp6))"; #endif const int optimize = 1; qDebug("In %s", __PRETTY_FUNCTION__); #ifdef Q_OS_WIN32 flags |= PCAP_OPENFLAG_NOCAPTURE_LOCAL; #endif #ifdef Q_OS_WIN32 _retry: // NOCAPTURE_LOCAL needs windows only pcap_open() handle_ = pcap_open(qPrintable(device_), 65535, flags, 100 /* ms */, NULL, errbuf); #else handle_ = pcap_open_live(qPrintable(device_), 65535, flags, 100 /* ms */, errbuf); #endif if (handle_ == NULL) { if (flags && QString(errbuf).contains("promiscuous")) { Xnotify("Unable to set promiscuous mode on <%s> - " "device emulation will not work", qPrintable(device_)); goto _exit; } #ifdef Q_OS_WIN32 else if ((flags & PCAP_OPENFLAG_NOCAPTURE_LOCAL) && QString(errbuf).contains("loopback")) { qDebug("Can't set no local capture mode %s", qPrintable(device_)); flags &= ~PCAP_OPENFLAG_NOCAPTURE_LOCAL; goto _retry; } #endif else { Xnotify("Unable to open <%s> [%s] - device emulation will not work", qPrintable(device_), errbuf); goto _exit; } } // TODO: for now the filter is hardcoded to accept tagged/untagged // ARP/NDP or ICMPv4/v6; when more protocols are added, we may need // to derive this filter based on which protocols are configured // on the devices if (pcap_compile(handle_, &bpf, capture_filter, optimize, 0) < 0) { qWarning("%s: error compiling filter: %s", qPrintable(device_), pcap_geterr(handle_)); goto _skip_filter; } if (pcap_setfilter(handle_, &bpf) < 0) { qWarning("%s: error setting filter: %s", qPrintable(device_), pcap_geterr(handle_)); goto _skip_filter; } _skip_filter: PcapSession::preRun(); state_ = kRunning; while (1) { int ret; struct pcap_pkthdr *hdr; const uchar *data; ret = pcap_next_ex(handle_, &hdr, &data); switch (ret) { case 1: { PacketBuffer *pktBuf = new PacketBuffer(data, hdr->caplen); #if 0 for (int i = 0; i < 64; i++) { printf("%02x ", data[i]); if (i % 16 == 0) printf("\n"); } printf("\n"); #endif // XXX: deviceManager should free pktBuf before returning // from this call; if it needs to process the pkt async // it should make a copy as the pktBuf's data buffer is // owned by libpcap which does not guarantee data will // persist across calls to pcap_next_ex() deviceManager_->receivePacket(pktBuf); break; } case 0: // timeout: just go back to the loop break; case -1: qWarning("%s: error reading packet (%d): %s", __PRETTY_FUNCTION__, ret, pcap_geterr(handle_)); break; case -2: qDebug("Loop/signal break or some other error"); break; default: qWarning("%s: Unexpected return value %d", __PRETTY_FUNCTION__, ret); stop_ = true; } if (stop_) { qDebug("user requested receiver stop"); break; } } PcapSession::postRun(); pcap_close(handle_); handle_ = NULL; stop_ = false; _exit: state_ = kFinished; } void PcapPort::EmulationTransceiver::start() { if (state_ == kRunning) { qWarning("Receive start requested but is already running!"); return; } state_ = kNotStarted; QThread::start(); while (state_ == kNotStarted) QThread::msleep(10); } void PcapPort::EmulationTransceiver::stop() { if (state_ == kRunning) { stop_ = true; PcapSession::stop(); while (state_ == kRunning) QThread::msleep(10); } else { qWarning("Receive stop requested but is not running!"); return; } } bool PcapPort::EmulationTransceiver::isRunning() { return (state_ == kRunning); } int PcapPort::EmulationTransceiver::transmitPacket(PacketBuffer *pktBuf) { return pcap_sendpacket(handle_, pktBuf->data(), pktBuf->length()); } ostinato-1.3.0/server/pcapport.h000066400000000000000000000124001451413623100166570ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _SERVER_PCAP_PORT_H #define _SERVER_PCAP_PORT_H #include #include #include #include "abstractport.h" #include "pcapextra.h" #include "pcaprxstats.h" #include "pcaptxttagstats.h" #include "pcapsession.h" #include "pcaptransmitter.h" class PcapPort : public AbstractPort { public: PcapPort(int id, const char *device); ~PcapPort(); void init(); virtual bool hasExclusiveControl() { return false; } virtual bool setExclusiveControl(bool /*exclusive*/) { return false; } virtual bool setTrackStreamStats(bool enable); virtual bool setRateAccuracy(AbstractPort::Accuracy accuracy); virtual void clearPacketList() { transmitter_->clearPacketList(); setPacketListLoopMode(false, 0, 0); setPacketListTtagMarkers(QList(), 0); } virtual void loopNextPacketSet(qint64 size, qint64 repeats, long repeatDelaySec, long repeatDelayNsec) { transmitter_->loopNextPacketSet(size, repeats, repeatDelaySec, repeatDelayNsec); } virtual bool appendToPacketList(long sec, long nsec, const uchar *packet, int length) { return transmitter_->appendToPacketList(sec, nsec, packet, length); } virtual void setPacketListLoopMode(bool loop, quint64 secDelay, quint64 nsecDelay) { transmitter_->setPacketListLoopMode(loop, secDelay, nsecDelay); } virtual bool setPacketListTtagMarkers(QList markers, uint repeatInterval) { return transmitter_->setPacketListTtagMarkers(markers, repeatInterval); } virtual void startTransmit() { Q_ASSERT(!isDirty()); transmitter_->start(); } virtual void stopTransmit() { transmitter_->stop(); } virtual bool isTransmitOn() { return transmitter_->isRunning(); } virtual double lastTransmitDuration() { return transmitter_->lastTxDuration(); } virtual void startCapture() { capturer_->start(); } virtual void stopCapture() { capturer_->stop(); } virtual bool isCaptureOn() { return capturer_->isRunning(); } virtual QIODevice* captureData() { return capturer_->captureFile(); } virtual void updateStreamStats(); virtual void startDeviceEmulation(); virtual void stopDeviceEmulation(); virtual int sendEmulationPacket(PacketBuffer *pktBuf); protected: enum Direction { kDirectionRx, kDirectionTx }; class PortMonitor: public QThread // TODO: inherit from PcapSession (only if required) { public: PortMonitor(const char *device, Direction direction, AbstractPort::PortStats *stats); ~PortMonitor(); void run(); void stop(); pcap_t* handle() { return handle_; } Direction direction() { return direction_; } bool isDirectional() { return isDirectional_; } bool isPromiscuous() { return isPromisc_; } protected: AbstractPort::PortStats *stats_; bool stop_; private: pcap_t *handle_; Direction direction_; bool isDirectional_; bool isPromisc_; }; class PortCapturer: public PcapSession { public: PortCapturer(const char *device); ~PortCapturer(); void run(); void start(); void stop(); bool isRunning(); QFile* captureFile(); private: enum State { kNotStarted, kRunning, kFinished }; QString device_; volatile bool stop_; QTemporaryFile capFile_; pcap_dumper_t *dumpHandle_; volatile State state_; }; class EmulationTransceiver: public PcapSession { public: EmulationTransceiver(const char *device, DeviceManager *deviceManager); ~EmulationTransceiver(); void run(); void start(); void stop(); bool isRunning(); int transmitPacket(PacketBuffer *pktBuf); private: enum State { kNotStarted, kRunning, kFinished }; QString device_; DeviceManager *deviceManager_; volatile bool stop_; volatile State state_; }; PortMonitor *monitorRx_; PortMonitor *monitorTx_; PcapRxStats *rxStatsPoller_; void updateNotes(); private: bool startStreamStatsTracking(); bool stopStreamStatsTracking(); PcapTransmitter *transmitter_; PortCapturer *capturer_; EmulationTransceiver *emulXcvr_; PcapTxTtagStats *txTtagStatsPoller_; static pcap_if_t *deviceList_; }; #endif ostinato-1.3.0/server/pcaprxstats.cpp000066400000000000000000000166301451413623100177470ustar00rootroot00000000000000/* Copyright (C) 2016 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #include "pcaprxstats.h" #include "pcapextra.h" #include "../common/debugdefs.h" #include "../common/sign.h" #include "settings.h" #include "streamtiming.h" #define Xnotify qWarning // FIXME PcapRxStats::PcapRxStats(const char *device, int id) { setObjectName(QString("Rx$:%1").arg(device)); device_ = QString::fromLatin1(device); stop_ = false; state_ = kNotStarted; isDirectional_ = true; handle_ = NULL; portId_ = id; timing_ = StreamTiming::instance(); } pcap_t* PcapRxStats::handle() { return handle_; } void PcapRxStats::run() { int flags = PCAP_OPENFLAG_PROMISCUOUS; char errbuf[PCAP_ERRBUF_SIZE] = ""; struct bpf_program bpf; const int optimize = 1; QString capture_filter = QString("(ether[len - 4:4] == 0x%1)").arg( SignProtocol::magic(), 0, BASE_HEX); // XXX: Exclude ICMP packets which contain an embedded signed packet // For now we check upto 4 vlan tags // XXX: libpcap for Linux has a special bpf vlan check which generates // incorrect BPF instructions for our capture filter expression, // so we modify it to work correctly // See https://srivatsp.com/ostinato/ostinato-rx-stream-stats-zero/ #ifdef Q_OS_LINUX capture_filter.prepend( "not (" "icmp or " "(vlan and icmp) or " "(vlan and icmp) or " "(vlan and icmp) or " "(vlan and icmp) " ") and "); #else capture_filter.append( "and not (" "icmp or " "(vlan and icmp) or " "(vlan and icmp) or " "(vlan and icmp) or " "(vlan and icmp) " ")"); #endif // Override filter expression if one is specified in .ini if (appSettings->contains(kInternalRxStatsFilterKey)) capture_filter = appSettings->value(kInternalRxStatsFilterKey) .toString(); qDebug("In %s", __PRETTY_FUNCTION__); qDebug("RxStats Filter: %s", qPrintable(capture_filter)); handle_ = pcap_open_live(qPrintable(device_), 65535, flags, 100 /* ms */, errbuf); if (handle_ == NULL) { if (flags && QString(errbuf).contains("promiscuous")) { Xnotify("Unable to set promiscuous mode on <%s> - " "stream stats rx will not work", qPrintable(device_)); goto _exit; } else { Xnotify("Unable to open <%s> [%s] - stream stats rx will not work", qPrintable(device_), errbuf); goto _exit; } } #ifdef Q_OS_WIN32 // pcap_setdirection() API is not supported in Windows. // NOTE: WinPcap 4.1.1 and above exports a dummy API that returns -1 // but since we would like to work with previous versions of WinPcap // also, we assume the API does not exist isDirectional_ = false; #else if (pcap_setdirection(handle_, PCAP_D_IN) < 0) { qDebug("RxStats: Error setting IN direction %s: %s\n", qPrintable(device_), pcap_geterr(handle_)); isDirectional_ = false; } #endif if (pcap_compile(handle_, &bpf, qPrintable(capture_filter), optimize, 0) < 0) { qWarning("%s: error compiling filter: %s", qPrintable(device_), pcap_geterr(handle_)); goto _skip_filter; } if (pcap_setfilter(handle_, &bpf) < 0) { qWarning("%s: error setting filter: %s", qPrintable(device_), pcap_geterr(handle_)); goto _skip_filter; } _skip_filter: clearDebugStats(); PcapSession::preRun(); state_ = kRunning; while (1) { int ret; struct pcap_pkthdr *hdr; const uchar *data; ret = pcap_next_ex(handle_, &hdr, &data); switch (ret) { case 1: { uint ttagId, guid; #ifdef Q_OS_WIN32 // Npcap (Windows) doesn't support direction, so packets // Tx by PcapTxThread are received back by us here - use // TxPort to filter out. TxPort is returned as byte 1 of // ttagId (byte 0 is ttagId). // If TxPort is us ==> Tx Packet, so skip // FIXME: remove once npcap supports pcap direction if (SignProtocol::packetTtagId(data, hdr->caplen, &ttagId, &guid) && (ttagId >> 8 != uint(portId_))) { ttagId &= 0xFF; timing_->recordRxTime(portId_, guid, ttagId, hdr->ts); } #else if (SignProtocol::packetTtagId(data, hdr->caplen, &ttagId, &guid)) { timing_->recordRxTime(portId_, guid, ttagId, hdr->ts); } #endif if (guid != SignProtocol::kInvalidGuid) { streamStats_[guid].rx_pkts++; streamStats_[guid].rx_bytes += hdr->caplen; } break; } case 0: // timeout: just go back to the loop break; case -1: qWarning("%s: error reading packet (%d): %s", __PRETTY_FUNCTION__, ret, pcap_geterr(handle_)); break; case -2: qDebug("Loop/signal break or some other error"); break; default: qWarning("%s: Unexpected return value %d", __PRETTY_FUNCTION__, ret); stop_ = true; } if (stop_) { qDebug("user requested rxstats stop"); break; } } PcapSession::postRun(); pcap_close(handle_); handle_ = NULL; stop_ = false; _exit: state_ = kFinished; } bool PcapRxStats::start() { if (state_ == kRunning) { qWarning("RxStats start requested but is already running!"); goto _exit; } state_ = kNotStarted; PcapSession::start(); while (state_ == kNotStarted) QThread::msleep(10); _exit: return true; } bool PcapRxStats::stop() { if (state_ == kRunning) { stop_ = true; PcapSession::stop(); while (state_ == kRunning) QThread::msleep(10); } else qWarning("RxStats stop requested but is not running!"); return true; } bool PcapRxStats::isRunning() { return (state_ == kRunning); } bool PcapRxStats::isDirectional() { return isDirectional_; } // XXX: Stats are reset on read void PcapRxStats::updateRxStreamStats(StreamStats &streamStats) { QMutexLocker lock(&streamStatsLock_); StreamStatsIterator i(streamStats_); while (i.hasNext()) { i.next(); uint guid = i.key(); StreamStatsTuple sst = i.value(); streamStats[guid].rx_pkts += sst.rx_pkts; streamStats[guid].rx_bytes += sst.rx_bytes; } streamStats_.clear(); } ostinato-1.3.0/server/pcaprxstats.h000066400000000000000000000026131451413623100174100ustar00rootroot00000000000000/* Copyright (C) 2016 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _PCAP_RX_STATS_H #define _PCAP_RX_STATS_H #include "streamstats.h" #include "pcapsession.h" #include class StreamTiming; class PcapRxStats: public PcapSession { public: PcapRxStats(const char *device, int id); pcap_t* handle(); void run(); bool start(); bool stop(); bool isRunning(); bool isDirectional(); void updateRxStreamStats(StreamStats &streamStats); // Reset on read private: enum State { kNotStarted, kRunning, kFinished }; QString device_; StreamStats streamStats_; QMutex streamStatsLock_; volatile bool stop_; volatile State state_; bool isDirectional_; int portId_; StreamTiming *timing_{nullptr}; }; #endif ostinato-1.3.0/server/pcapsession.cpp000066400000000000000000000114461451413623100177220ustar00rootroot00000000000000/* Copyright (C) 2019 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #include "pcapsession.h" // XXX: Implemented as reset on read QString PcapSession::debugStats() { QString dbgStats; if (!handle_) return QString(); #ifdef Q_OS_WIN32 static_assert(sizeof(struct pcap_stat) == 6*sizeof(uint), "pcap_stat has less or more than 6 values"); int size; struct pcap_stat incPcapStats; struct pcap_stat *pcapStats = pcap_stats_ex(handle_, &size); if (pcapStats && (uint(size) >= 6*sizeof(uint))) { incPcapStats.ps_recv = pcapStats->ps_recv - lastPcapStats_.ps_recv; incPcapStats.ps_drop = pcapStats->ps_drop - lastPcapStats_.ps_drop; incPcapStats.ps_ifdrop = pcapStats->ps_ifdrop - lastPcapStats_.ps_ifdrop; incPcapStats.ps_capt = pcapStats->ps_capt - lastPcapStats_.ps_capt; incPcapStats.ps_sent = pcapStats->ps_sent - lastPcapStats_.ps_sent; incPcapStats.ps_netdrop = pcapStats->ps_netdrop - lastPcapStats_.ps_netdrop; dbgStats = QString("recv: %1 drop: %2 ifdrop: %3 " "capt: %4 sent: %5 netdrop: %6") .arg(incPcapStats.ps_recv) .arg(incPcapStats.ps_drop) .arg(incPcapStats.ps_ifdrop) .arg(incPcapStats.ps_capt) .arg(incPcapStats.ps_sent) .arg(incPcapStats.ps_netdrop); lastPcapStats_ = *pcapStats; } else { dbgStats = QString("error reading pcap stats: %1") .arg(pcap_geterr(handle_)); } #else struct pcap_stat pcapStats; struct pcap_stat incPcapStats; int ret = pcap_stats(handle_, &pcapStats); if (ret == 0) { incPcapStats.ps_recv = pcapStats.ps_recv - lastPcapStats_.ps_recv; incPcapStats.ps_drop = pcapStats.ps_drop - lastPcapStats_.ps_drop; incPcapStats.ps_ifdrop = pcapStats.ps_ifdrop - lastPcapStats_.ps_ifdrop; dbgStats = QString("recv: %1 drop: %2 ifdrop: %3") .arg(incPcapStats.ps_recv) .arg(incPcapStats.ps_drop) .arg(incPcapStats.ps_ifdrop); lastPcapStats_ = pcapStats; } else { dbgStats = QString("error reading pcap stats: %1") .arg(pcap_geterr(handle_)); } #endif return dbgStats; } bool PcapSession::clearDebugStats() { memset(&lastPcapStats_, 0, sizeof(lastPcapStats_)); return true; } #ifdef Q_OS_UNIX #include #include #define MY_BREAK_SIGNAL SIGUSR1 QHash PcapSession::signalSeen_; void PcapSession::preRun() { // Should be called in the thread's context thread_ = pthread_self(); struct sigaction sa; memset(&sa, 0, sizeof(sa)); sigemptyset(&sa.sa_mask); sa.sa_handler = PcapSession::signalBreakHandler; if (!sigaction(MY_BREAK_SIGNAL, &sa, NULL)) { signalSeen_[thread_] = false; qDebug("Break signal handler installed"); } else qWarning("Failed to install MY_BREAK_SIGNAL handler"); } void PcapSession::postRun() { // Should be called in the thread's context ThreadId id = pthread_self(); qDebug("In %s::%s", typeid(*this).name(), __FUNCTION__); if (!signalSeen_.contains(id)) { qWarning("Thread not found in signalSeen"); return; } bool &seen = signalSeen_[id]; // XXX: don't exit the thread until we see the signal; if we don't // some platforms will crash if (!seen) { qDebug("Wait for signal"); while (!seen) QThread::msleep(10); } signalSeen_.remove(id); qDebug("Signal seen and handled"); } void PcapSession::stop() { // Should be called OUTSIDE the thread's context // XXX: As per the man page for pcap_breakloop, we need both // pcap_breakloop and a mechanism to interrupt system calls; // we use a signal for the latter // TODO: If the signal mechanism doesn't work, we could try // pthread_cancel(thread_); pcap_breakloop(handle_); pthread_kill(thread_.nativeId(), MY_BREAK_SIGNAL); } void PcapSession::signalBreakHandler(int /*signum*/) { qDebug("In %s", __FUNCTION__); signalSeen_[pthread_self()] = true; } #endif ostinato-1.3.0/server/pcapsession.h000066400000000000000000000040221451413623100173570ustar00rootroot00000000000000/* Copyright (C) 2019 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _PCAP_SESSION_H #define _PCAP_SESSION_H #include #include #ifdef Q_OS_UNIX #include #include class ThreadId { public: ThreadId() { id_ = pthread_self(); } ThreadId(pthread_t id) { id_ = id; } pthread_t nativeId() { return id_; } uint hash() const { QByteArray t((const char*)(&id_), sizeof(id_)); return qHash(t); } bool operator==(const ThreadId &other) const { return (pthread_equal(id_, other.id_) != 0); } private: pthread_t id_; }; inline uint qHash(const ThreadId &key) { return key.hash(); } class PcapSession: public QThread { public: QString debugStats(); protected: bool clearDebugStats(); void preRun(); void postRun(); void stop(); pcap_t *handle_{nullptr}; private: static void signalBreakHandler(int /*signum*/); ThreadId thread_; static QHash signalSeen_; struct pcap_stat lastPcapStats_; }; #else class PcapSession: public QThread { public: QString debugStats(); protected: bool clearDebugStats(); void preRun() {}; void postRun() {}; void stop() { qDebug("calling breakloop with handle %p", handle_); pcap_breakloop(handle_); } pcap_t *handle_{nullptr}; private: struct pcap_stat lastPcapStats_; }; #endif #endif ostinato-1.3.0/server/pcaptransmitter.cpp000066400000000000000000000102161451413623100206050ustar00rootroot00000000000000/* Copyright (C) 2010-2016 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #include "pcaptransmitter.h" PcapTransmitter::PcapTransmitter( const char *device) : txThread_(device) { adjustRxStreamStats_ = false; txStats_.setObjectName(QString("TxStats:%1").arg(device)); memset(&stats_, 0, sizeof(stats_)); txStats_.setTxThreadStats(&stats_); txStats_.start(); // TODO: alongwith user transmit start txThread_.setStats(&stats_); connect(&txThread_, SIGNAL(finished()), SLOT(updateTxThreadStreamStats())); } PcapTransmitter::~PcapTransmitter() { txStats_.stop(); // TODO: alongwith user transmit stop } bool PcapTransmitter::setRateAccuracy( AbstractPort::Accuracy accuracy) { return txThread_.setRateAccuracy(accuracy); } void PcapTransmitter::adjustRxStreamStats(bool enable) { adjustRxStreamStats_ = enable; } bool PcapTransmitter::setStreamStatsTracking(bool enable) { return txThread_.setStreamStatsTracking(enable); } // XXX: Stats are reset on read void PcapTransmitter::updateTxRxStreamStats(StreamStats &streamStats) { QMutexLocker lock(&streamStatsLock_); StreamStatsIterator i(streamStats_); while (i.hasNext()) { i.next(); uint guid = i.key(); StreamStatsTuple sst = i.value(); streamStats[guid].tx_pkts += sst.tx_pkts; streamStats[guid].tx_bytes += sst.tx_bytes; if (adjustRxStreamStats_) { // XXX: rx_pkts counting may lag behind tx_pkts, so stream stats // may become negative after adjustment transiently. But this // should fix itself once all the rx pkts come in streamStats[guid].rx_pkts -= sst.tx_pkts; streamStats[guid].rx_bytes -= sst.tx_bytes; } } streamStats_.clear(); } void PcapTransmitter::clearPacketList() { txThread_.clearPacketList(); } void PcapTransmitter::loopNextPacketSet( qint64 size, qint64 repeats, long repeatDelaySec, long repeatDelayNsec) { txThread_.loopNextPacketSet(size, repeats, repeatDelaySec, repeatDelayNsec); } bool PcapTransmitter::appendToPacketList(long sec, long nsec, const uchar *packet, int length) { return txThread_.appendToPacketList(sec, nsec, packet, length); } void PcapTransmitter::setHandle(pcap_t *handle) { txThread_.setHandle(handle); } void PcapTransmitter::setPacketListLoopMode( bool loop, quint64 secDelay, quint64 nsecDelay) { txThread_.setPacketListLoopMode(loop, secDelay, nsecDelay); } bool PcapTransmitter::setPacketListTtagMarkers( QList markers, uint repeatInterval) { return txThread_.setPacketListTtagMarkers(markers, repeatInterval); } void PcapTransmitter::useExternalStats(AbstractPort::PortStats *stats) { txStats_.useExternalStats(stats); } void PcapTransmitter::start() { txThread_.start(); } void PcapTransmitter::stop() { txThread_.stop(); } bool PcapTransmitter::isRunning() { return txThread_.isRunning(); } double PcapTransmitter::lastTxDuration() { return txThread_.lastTxDuration(); } void PcapTransmitter::updateTxThreadStreamStats() { QMutexLocker lock(&streamStatsLock_); PcapTxThread *txThread = dynamic_cast(sender()); StreamStats threadStreamStats = txThread->streamStats(); StreamStatsIterator i(threadStreamStats); while (i.hasNext()) { i.next(); uint guid = i.key(); StreamStatsTuple sst = i.value(); streamStats_[guid].tx_pkts += sst.tx_pkts; streamStats_[guid].tx_bytes += sst.tx_bytes; } } ostinato-1.3.0/server/pcaptransmitter.h000066400000000000000000000037561451413623100202650ustar00rootroot00000000000000/* Copyright (C) 2010-2016 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _PCAP_TRANSMITTER_H #define _PCAP_TRANSMITTER_H #include "abstractport.h" #include "pcaptxstats.h" #include "pcaptxthread.h" #include "statstuple.h" class PcapTransmitter : QObject { Q_OBJECT public: PcapTransmitter(const char *device); ~PcapTransmitter(); bool setRateAccuracy(AbstractPort::Accuracy accuracy); bool setStreamStatsTracking(bool enable); void adjustRxStreamStats(bool enable); void updateTxRxStreamStats(StreamStats &streamStats); // Reset on read void clearPacketList(); void loopNextPacketSet(qint64 size, qint64 repeats, long repeatDelaySec, long repeatDelayNsec); bool appendToPacketList(long sec, long usec, const uchar *packet, int length); void setPacketListLoopMode(bool loop, quint64 secDelay, quint64 nsecDelay); bool setPacketListTtagMarkers(QList markers, uint repeatInterval); void setHandle(pcap_t *handle); void useExternalStats(AbstractPort::PortStats *stats); void start(); void stop(); bool isRunning(); double lastTxDuration(); private slots: void updateTxThreadStreamStats(); private: StreamStats streamStats_; QMutex streamStatsLock_; PcapTxThread txThread_; PcapTxStats txStats_; StatsTuple stats_; bool adjustRxStreamStats_; }; #endif ostinato-1.3.0/server/pcaptxstats.cpp000066400000000000000000000034221451413623100177440ustar00rootroot00000000000000/* Copyright (C) 2016 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #include "pcaptxstats.h" #include "pcaptxstats.h" #include "statstuple.h" PcapTxStats::PcapTxStats() { txThreadStats_ = NULL; stats_ = new AbstractPort::PortStats; usingInternalStats_ = true; stop_ = false; } PcapTxStats::~PcapTxStats() { if (usingInternalStats_) delete stats_; } void PcapTxStats::setTxThreadStats(StatsTuple *stats) { txThreadStats_ = stats; } void PcapTxStats::useExternalStats(AbstractPort::PortStats *stats) { if (usingInternalStats_) delete stats_; stats_ = stats; usingInternalStats_ = false; } void PcapTxStats::start() { QThread::start(); while (!isRunning()) QThread::msleep(10); } void PcapTxStats::stop() { stop_ = true; while (isRunning()) QThread::msleep(10); } void PcapTxStats::run() { Q_ASSERT(txThreadStats_); qDebug("txStats: collection start"); while (1) { stats_->txPkts = txThreadStats_->pkts; stats_->txBytes = txThreadStats_->bytes; if (stop_) break; QThread::msleep(1000); } stop_ = false; qDebug("txStats: collection end"); } ostinato-1.3.0/server/pcaptxstats.h000066400000000000000000000022371451413623100174140ustar00rootroot00000000000000/* Copyright (C) 2016 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _PCAP_TX_STATS_H #define _PCAP_TX_STATS_H #include "abstractport.h" #include struct StatsTuple; class PcapTxStats : public QThread { public: PcapTxStats(); ~PcapTxStats(); void setTxThreadStats(StatsTuple *stats); void useExternalStats(AbstractPort::PortStats *stats); void start(); void stop(); private: void run(); StatsTuple *txThreadStats_; bool usingInternalStats_; AbstractPort::PortStats *stats_; volatile bool stop_; }; #endif ostinato-1.3.0/server/pcaptxthread.cpp000066400000000000000000000522261451413623100200630ustar00rootroot00000000000000/* Copyright (C) 2010-2016 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #include "pcaptxthread.h" #include "sign.h" #include "statstuple.h" #include "timestamp.h" #include PcapTxThread::PcapTxThread(const char *device) { char errbuf[PCAP_ERRBUF_SIZE] = ""; setObjectName(QString("Tx:%1").arg(device)); #ifdef Q_OS_WIN32 LARGE_INTEGER freq; if (QueryPerformanceFrequency(&freq)) gTicksFreq = freq.QuadPart; else Q_ASSERT_X(false, "PcapTxThread::PcapTxThread", "This Win32 platform does not support performance counter"); #endif state_ = kNotStarted; stop_ = false; trackStreamStats_ = false; clearPacketList(); handle_ = pcap_open_live(device, 64 /* FIXME */, 0, 1000 /* ms */, errbuf); if (handle_ == NULL) goto _open_error; usingInternalHandle_ = true; stats_ = NULL; return; _open_error: qDebug("%s: Error opening port %s: %s\n", __FUNCTION__, device, errbuf); usingInternalHandle_ = false; } PcapTxThread::~PcapTxThread() { if (usingInternalHandle_) pcap_close(handle_); } bool PcapTxThread::setRateAccuracy( AbstractPort::Accuracy accuracy) { switch (accuracy) { case AbstractPort::kHighAccuracy: udelayFn_ = udelay; qWarning("%s: rate accuracy set to High - busy wait", __FUNCTION__); break; case AbstractPort::kLowAccuracy: udelayFn_ = QThread::usleep; qWarning("%s: rate accuracy set to Low - usleep", __FUNCTION__); break; default: qWarning("%s: unsupported rate accuracy value %d", __FUNCTION__, accuracy); return false; } return true; } bool PcapTxThread::setStreamStatsTracking(bool enable) { trackStreamStats_ = enable; return true; } void PcapTxThread::clearPacketList() { Q_ASSERT(!isRunning()); // \todo lock for packetSequenceList while(packetSequenceList_.size()) delete packetSequenceList_.takeFirst(); currentPacketSequence_ = NULL; repeatSequenceStart_ = -1; repeatSize_ = 0; packetCount_ = 0; packetListSize_ = 0; returnToQIdx_ = -1; setPacketListLoopMode(false, 0, 0); } void PcapTxThread::loopNextPacketSet(qint64 size, qint64 repeats, long repeatDelaySec, long repeatDelayNsec) { currentPacketSequence_ = new PacketSequence(trackStreamStats_); currentPacketSequence_->repeatCount_ = repeats; currentPacketSequence_->usecDelay_ = repeatDelaySec * long(1e6) + repeatDelayNsec/1000; repeatSequenceStart_ = packetSequenceList_.size(); repeatSize_ = size; packetCount_ = 0; packetSequenceList_.append(currentPacketSequence_); } bool PcapTxThread::appendToPacketList(long sec, long nsec, const uchar *packet, int length) { bool op = true; pcap_pkthdr pktHdr; pktHdr.caplen = pktHdr.len = length; pktHdr.ts.tv_sec = sec; pktHdr.ts.tv_usec = nsec/1000; // loopNextPacketSet should have created a seq Q_ASSERT(currentPacketSequence_ != NULL); // If not enough space, update usecDelay and alloc a new seq if (!currentPacketSequence_->hasFreeSpace(2*sizeof(pcap_pkthdr)+length)) { struct timeval diff; timersub(&pktHdr.ts, ¤tPacketSequence_->lastPacket_->ts, &diff); currentPacketSequence_->usecDelay_ = diff.tv_usec; if (diff.tv_sec) currentPacketSequence_->usecDelay_ += diff.tv_sec*1e6; //! \todo (LOW): calculate sendqueue size currentPacketSequence_ = new PacketSequence(trackStreamStats_); packetSequenceList_.append(currentPacketSequence_); // Validate that the pkt will fit inside the new currentSendQueue_ Q_ASSERT(currentPacketSequence_->hasFreeSpace( sizeof(pcap_pkthdr) + length)); } if (currentPacketSequence_->appendPacket(&pktHdr, (u_char*) packet) < 0) { op = false; } packetCount_++; packetListSize_ += repeatSize_ ? currentPacketSequence_->repeatCount_ : 1; // Last packet of packet-set? if (repeatSize_ > 0 && packetCount_ == repeatSize_) { qDebug("repeatSequenceStart_=%d, repeatSize_ = %llu", repeatSequenceStart_, repeatSize_); // Set the packetSequence repeatSize Q_ASSERT(repeatSequenceStart_ >= 0); Q_ASSERT(repeatSequenceStart_ < packetSequenceList_.size()); if (currentPacketSequence_ != packetSequenceList_[repeatSequenceStart_]) { PacketSequence *start = packetSequenceList_[repeatSequenceStart_]; currentPacketSequence_->usecDelay_ = start->usecDelay_; start->usecDelay_ = 0; start->repeatSize_ = packetSequenceList_.size() - repeatSequenceStart_; } repeatSize_ = 0; // End current pktSeq currentPacketSequence_ = NULL; } return op; } void PcapTxThread::setPacketListLoopMode( bool loop, quint64 secDelay, quint64 nsecDelay) { returnToQIdx_ = loop ? 0 : -1; loopDelay_ = secDelay*long(1e6) + nsecDelay/1000; } bool PcapTxThread::setPacketListTtagMarkers( QList markers, uint repeatInterval) { // XXX: Empty markers => no streams have Ttag firstTtagPkt_ = markers.isEmpty() ? -1 : int(markers.first()); // Calculate delta markers ttagDeltaMarkers_.clear(); for (int i = 1; i < markers.size(); i++) ttagDeltaMarkers_.append(markers.at(i) - markers.at(i-1)); if (!markers.isEmpty()) { ttagDeltaMarkers_.append(repeatInterval - markers.last() + markers.first()); qDebug() << "TtagRepeatInterval:" << repeatInterval; qDebug() << "FirstTtagPkt:" << firstTtagPkt_; qDebug() << "TtagMarkers:" << ttagDeltaMarkers_; } return true; } void PcapTxThread::setHandle(pcap_t *handle) { if (usingInternalHandle_) pcap_close(handle_); handle_ = handle; usingInternalHandle_ = false; } void PcapTxThread::setStats(StatsTuple *stats) { stats_ = stats; } StreamStats PcapTxThread::streamStats() { // This function is typically called in client-specific-RPC-thread // context; hence different client RPC threads may call this function, // so use a lock. Although RPCs are protected by the portLock just // for this purpose, the streamStats RPC takes a Read lock, so it can // still happen that multiple RPC threads land up here - that's why // this lock is required QMutexLocker lock(&streamStatsLock_); StreamStats ss(streamStats_); // Make a copy streamStats_.clear(); // Reset on read semantics return ss; // Return copy } void PcapTxThread::run() { //! \todo (MED) Stream Mode - continuous: define before implement // NOTE1: We can't use pcap_sendqueue_transmit() directly even on Win32 // 'coz of 2 reasons - there's no way of stopping it before all packets // in the sendQueue are sent out and secondly, stats are available only // when all packets have been sent - no periodic updates // // NOTE2: Transmit on the Rx Handle so that we can receive it back // on the Tx Handle to do stats // // NOTE3: Update pcapExtra counters - port TxStats will be updated in the // 'stats callback' function so that both Rx and Tx stats are updated // together const int kSyncTransmit = 1; int i; long overHead = 0; // overHead should be negative or zero TimeStamp startTime, endTime; qDebug("packetSequenceList_.size = %d", packetSequenceList_.size()); if (packetSequenceList_.size() <= 0) { lastTxDuration_ = 0.0; goto _exit2; } for(i = 0; i < packetSequenceList_.size(); i++) { qDebug("sendQ[%d]: rptCnt = %d, rptSz = %d, usecDelay = %ld", i, packetSequenceList_.at(i)->repeatCount_, packetSequenceList_.at(i)->repeatSize_, packetSequenceList_.at(i)->usecDelay_); qDebug("sendQ[%d]: pkts = %ld, usecDuration = %ld, ttagL4CksumOfs = %hu", i, packetSequenceList_.at(i)->packets_, packetSequenceList_.at(i)->usecDuration_, packetSequenceList_.at(i)->ttagL4CksumOffset_); } qDebug() << "Loop:" << (returnToQIdx_ >= 0) << "LoopDelay:" << loopDelay_; qDebug() << "First Ttag: " << firstTtagPkt_ << "Ttag Markers:" << ttagDeltaMarkers_; lastStats_ = *stats_; // used for stream stats // Init Ttag related vars. If no packets need ttag, firstTtagPkt_ is -1, // so nextTagPkt_ is set to practically unreachable value (due to // 64 bit counter wraparound time!) ttagMarkerIndex_ = 0; nextTtagPkt_ = stats_->pkts + firstTtagPkt_; getTimeStamp(&startTime); state_ = kRunning; i = 0; while (i < packetSequenceList_.size()) { _restart: int rptSz = packetSequenceList_.at(i)->repeatSize_; int rptCnt = packetSequenceList_.at(i)->repeatCount_; for (int j = 0; j < rptCnt; j++) { for (int k = 0; k < rptSz; k++) { int ret; PacketSequence *seq = packetSequenceList_.at(i+k); #ifdef Q_OS_WIN32 TimeStamp ovrStart, ovrEnd; // Use Windows-only pcap_sendqueue_transmit() if duration < 1s // and no stream timing is configured if (seq->usecDuration_ <= long(1e6) && firstTtagPkt_ < 0) { getTimeStamp(&ovrStart); ret = pcap_sendqueue_transmit(handle_, seq->sendQueue_, kSyncTransmit); if (ret >= 0) { stats_->pkts += seq->packets_; stats_->bytes += seq->bytes_; getTimeStamp(&ovrEnd); overHead += seq->usecDuration_ - udiffTimeStamp(&ovrStart, &ovrEnd); Q_ASSERT(overHead <= 0); } if (stop_) ret = -2; } else { ret = sendQueueTransmit(handle_, seq, overHead, kSyncTransmit); } #else ret = sendQueueTransmit(handle_, seq, overHead, kSyncTransmit); #endif if (ret >= 0) { long usecs = seq->usecDelay_ + overHead; if (usecs > 0) { (*udelayFn_)(usecs); overHead = 0; } else overHead = usecs; } else { qDebug("error %d in sendQueueTransmit()", ret); qDebug("overHead = %ld", overHead); stop_ = false; goto _exit; } } // rptSz } // rptCnt // Move to the next Packet Set i += rptSz; } if (returnToQIdx_ >= 0) { long usecs = loopDelay_ + overHead; if (usecs > 0) { (*udelayFn_)(usecs); overHead = 0; } else overHead = usecs; i = returnToQIdx_; goto _restart; } _exit: getTimeStamp(&endTime); lastTxDuration_ = udiffTimeStamp(&startTime, &endTime)/1e6; _exit2: qDebug("Tx duration = %fs", lastTxDuration_); //Q_ASSERT(lastTxDuration_ >= 0); if (trackStreamStats_) updateTxStreamStats(); state_ = kFinished; } void PcapTxThread::start() { // FIXME: return error if (state_ == kRunning) { qWarning("Transmit start requested but is already running!"); return; } state_ = kNotStarted; QThread::start(); while (state_ == kNotStarted) QThread::msleep(10); } void PcapTxThread::stop() { if (state_ == kRunning) { stop_ = true; while (state_ == kRunning) QThread::msleep(10); } else { // FIXME: return error qWarning("Transmit stop requested but is not running!"); return; } } bool PcapTxThread::isRunning() { return (state_ == kRunning); } double PcapTxThread::lastTxDuration() { return lastTxDuration_; } int PcapTxThread::sendQueueTransmit(pcap_t *p, PacketSequence *seq, long &overHead, int sync) { TimeStamp ovrStart, ovrEnd; struct timeval ts; pcap_send_queue *queue = seq->sendQueue_; struct pcap_pkthdr *hdr = (struct pcap_pkthdr*) queue->buffer; char *end = queue->buffer + queue->len; ts = hdr->ts; getTimeStamp(&ovrStart); while((char*) hdr < end) { uchar *pkt = (uchar*)hdr + sizeof(*hdr); int pktLen = hdr->caplen; bool ttagPkt = false; #if 0 quint16 origCksum = 0; #endif // Time for a T-Tag packet? if (stats_->pkts == nextTtagPkt_) { ttagPkt = true; // XXX: write 2xBytes instead of 1xHalf-word to avoid // potential alignment problem *(pkt+pktLen-5) = SignProtocol::kTypeLenTtag; *(pkt+pktLen-6) = ttagId_; #if 0 // Recalc L4 checksum; use incremental checksum as per RFC 1624 // HC' = ~(~HC + ~m + m') if (seq->ttagL4CksumOffset_) { quint16 *cksum = reinterpret_cast( pkt + seq->ttagL4CksumOffset_); origCksum = qFromBigEndian(*cksum); // XXX: SignProtocol trailer // ... | | 0x61 | 0x00 | 0x22 | 0x1d10c0da // ... | | 0x61 | | 0x23 | 0x1d10c0da // For odd pkt Length, Ttag spans across 2 half-words // XXX: Hardcoded values instead of sign protocol constants // used below for readability quint32 newCksum = pktLen & 1 ? quint16(~origCksum) + quint16(~0x221d) + 0x231d + quint16(~0x6100) + (0x6100 | ttagId_) : quint16(~origCksum) + quint16(~0x0022) + (ttagId_ << 8 | 0x23); while (newCksum > 0xffff) newCksum = (newCksum & 0xffff) + (newCksum >> 16); // XXX: For IPv4/UDP, if ~newcksum is 0x0000 we are supposed to // set the checksum as 0xffff since 0x0000 indicates no cksum // is present - we choose not to do this to avoid extra cost *cksum = qToBigEndian(quint16(~newCksum)); } #endif ttagId_++; nextTtagPkt_ += ttagDeltaMarkers_.at(ttagMarkerIndex_); ttagMarkerIndex_++; if (ttagMarkerIndex_ >= ttagDeltaMarkers_.size()) ttagMarkerIndex_ = 0; } if (sync) { long usec = (hdr->ts.tv_sec - ts.tv_sec) * 1000000 + (hdr->ts.tv_usec - ts.tv_usec); getTimeStamp(&ovrEnd); overHead -= udiffTimeStamp(&ovrStart, &ovrEnd); Q_ASSERT(overHead <= 0); usec += overHead; if (usec > 0) { (*udelayFn_)(usec); overHead = 0; } else overHead = usec; ts = hdr->ts; getTimeStamp(&ovrStart); } Q_ASSERT(pktLen > 0); pcap_sendpacket(p, pkt, pktLen); stats_->pkts++; stats_->bytes += pktLen; // Revert T-Tag packet changes if (ttagPkt) { *(pkt+pktLen-5) = SignProtocol::kTypeLenTtagPlaceholder; *(pkt+pktLen-6) = 0; #if 0 if (seq->ttagL4CksumOffset_) { quint16 *cksum = reinterpret_cast( pkt + seq->ttagL4CksumOffset_); *cksum = qToBigEndian(origCksum); } #endif } // Step to the next packet in the buffer hdr = (struct pcap_pkthdr*) (pkt + pktLen); pkt = (uchar*) ((uchar*)hdr + sizeof(*hdr)); // FIXME: superfluous? if (stop_) { return -2; } } return 0; } void PcapTxThread::updateTxStreamStats() { QMutexLocker lock(&streamStatsLock_); // If no packets in list, nothing to be done if (!packetListSize_) return; // Get number of tx packets sent during last transmit quint64 pkts = stats_->pkts > lastStats_.pkts ? stats_->pkts - lastStats_.pkts : stats_->pkts + (ULLONG_MAX - lastStats_.pkts); // Calculate - // number of complete repeats of packetList_ // => each PacketSet in the packetList is repeated these many times // number of pkts sent in last partial repeat of packetList_ // - This encompasses 0 or more potentially partial PacketSets // XXX: Note for the above, we consider a PacketSet to include its // own repeats within itself int c = pkts/packetListSize_; int d = pkts%packetListSize_; qDebug("%s:", __FUNCTION__); qDebug("txPkts = %llu", pkts); qDebug("packetListSize_ = %llu", packetListSize_); qDebug("c = %d, d = %d\n", c, d); int i; if (!c) goto _last_repeat; i = 0; while (i < packetSequenceList_.size()) { PacketSequence *seq = packetSequenceList_.at(i); int rptSz = seq->repeatSize_; int rptCnt = seq->repeatCount_; for (int k = 0; k < rptSz; k++) { seq = packetSequenceList_.at(i+k); StreamStatsIterator iter(seq->streamStatsMeta_); while (iter.hasNext()) { iter.next(); uint guid = iter.key(); StreamStatsTuple ssm = iter.value(); streamStats_[guid].tx_pkts += c * rptCnt * ssm.tx_pkts; streamStats_[guid].tx_bytes += c * rptCnt * ssm.tx_bytes; } } // Move to the next Packet Set i += rptSz; } _last_repeat: if (!d) goto _done; i = 0; while (i < packetSequenceList_.size()) { PacketSequence *seq = packetSequenceList_.at(i); int rptSz = seq->repeatSize_; int rptCnt = seq->repeatCount_; for (int j = 0; j < rptCnt; j++) { for (int k = 0; k < rptSz; k++) { seq = packetSequenceList_.at(i+k); Q_ASSERT(seq->packets_); if (d >= seq->packets_) { // All packets of this seq were sent StreamStatsIterator iter(seq->streamStatsMeta_); while (iter.hasNext()) { iter.next(); uint guid = iter.key(); StreamStatsTuple ssm = iter.value(); streamStats_[guid].tx_pkts += ssm.tx_pkts; streamStats_[guid].tx_bytes += ssm.tx_bytes; } d -= seq->packets_; } else { // (d < seq->packets_) // not all packets of this seq were sent, so we need to // traverse this seq upto 'd' pkts, parse guid from the // packet and update streamStats struct pcap_pkthdr *hdr = (struct pcap_pkthdr*) seq->sendQueue_->buffer; char *end = seq->sendQueue_->buffer + seq->sendQueue_->len; while(d && ((char*) hdr < end)) { uchar *pkt = (uchar*)hdr + sizeof(*hdr); uint guid; if (SignProtocol::packetGuid(pkt, hdr->caplen, &guid)) { streamStats_[guid].tx_pkts++; streamStats_[guid].tx_bytes += hdr->caplen; } // Step to the next packet in the buffer hdr = (struct pcap_pkthdr*) (pkt + hdr->caplen); d--; } Q_ASSERT(d == 0); goto _done; } } } // Move to the next Packet Set i += rptSz; } _done: return; } void PcapTxThread::udelay(unsigned long usec) { #if defined(Q_OS_WIN32) LARGE_INTEGER tgtTicks; LARGE_INTEGER curTicks; QueryPerformanceCounter(&curTicks); tgtTicks.QuadPart = curTicks.QuadPart + (usec*gTicksFreq)/1000000; while (curTicks.QuadPart < tgtTicks.QuadPart) QueryPerformanceCounter(&curTicks); #elif defined(Q_OS_LINUX) struct timeval delay, target, now; //qDebug("usec delay = %ld", usec); delay.tv_sec = 0; delay.tv_usec = usec; while (delay.tv_usec >= 1000000) { delay.tv_sec++; delay.tv_usec -= 1000000; } gettimeofday(&now, NULL); timeradd(&now, &delay, &target); do { gettimeofday(&now, NULL); } while (timercmp(&now, &target, <)); #else QThread::usleep(usec); #endif } ostinato-1.3.0/server/pcaptxthread.h000066400000000000000000000056341451413623100175310ustar00rootroot00000000000000/* Copyright (C) 2010-2016 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _PCAP_TX_THREAD_H #define _PCAP_TX_THREAD_H #include "abstractport.h" #include "packetsequence.h" #include "statstuple.h" #include #include #include class PcapTxThread: public QThread { public: PcapTxThread(const char *device); ~PcapTxThread(); bool setRateAccuracy(AbstractPort::Accuracy accuracy); bool setStreamStatsTracking(bool enable); void clearPacketList(); void loopNextPacketSet(qint64 size, qint64 repeats, long repeatDelaySec, long repeatDelayNsec); bool appendToPacketList(long sec, long usec, const uchar *packet, int length); void setPacketListLoopMode(bool loop, quint64 secDelay, quint64 nsecDelay); bool setPacketListTtagMarkers(QList markers, uint repeatInterval); void setHandle(pcap_t *handle); void setStats(StatsTuple *stats); StreamStats streamStats(); // reset on read void run(); void start(); void stop(); bool isRunning(); double lastTxDuration(); private: enum State { kNotStarted, kRunning, kFinished }; static void udelay(unsigned long usec); int sendQueueTransmit(pcap_t *p, PacketSequence *seq, long &overHead, int sync); void updateTxStreamStats(); // Intermediate state variables used while building the packet list PacketSequence *currentPacketSequence_; int repeatSequenceStart_; quint64 repeatSize_; quint64 packetCount_; QList packetSequenceList_; quint64 packetListSize_; // count of pkts in packet List including repeats int returnToQIdx_; quint64 loopDelay_; // in nanosecs void (*udelayFn_)(unsigned long); bool usingInternalHandle_; pcap_t *handle_; volatile bool stop_; volatile State state_; bool trackStreamStats_; StatsTuple *stats_; StatsTuple lastStats_; StreamStats streamStats_; QMutex streamStatsLock_; quint8 ttagId_{0}; double lastTxDuration_{0.0}; // in secs // XXX: Ttag Marker config derived; not updated during Tx int firstTtagPkt_; QList ttagDeltaMarkers_; // XXX: Ttag related; updated during Tx int ttagMarkerIndex_; quint64 nextTtagPkt_{0}; }; #endif ostinato-1.3.0/server/pcaptxttagstats.cpp000066400000000000000000000126141451413623100206270ustar00rootroot00000000000000/* Copyright (C) 2023 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #include "pcaptxttagstats.h" #include "pcapextra.h" #include "../common/debugdefs.h" #include "../common/sign.h" #include "streamtiming.h" #define Xnotify qWarning // FIXME PcapTxTtagStats::PcapTxTtagStats(const char *device, int id) : portId_(id) { setObjectName(QString("TxT$:%1").arg(device)); device_ = QString::fromLatin1(device); timing_ = StreamTiming::instance(); } void PcapTxTtagStats::run() { int flags = PCAP_OPENFLAG_PROMISCUOUS; char errbuf[PCAP_ERRBUF_SIZE] = ""; struct bpf_program bpf; const int optimize = 1; QString capture_filter = QString( "(ether[len - 4:4] == 0x%1) and (ether[len - 5:1] == 0x%2)") .arg(SignProtocol::magic(), 0, BASE_HEX) .arg(SignProtocol::kTypeLenTtag, 0, BASE_HEX); qDebug("In %s", __PRETTY_FUNCTION__); qDebug("pcap-filter: %s", qPrintable(capture_filter)); handle_ = pcap_open_live(qPrintable(device_), 65535, flags, 100 /* ms */, errbuf); if (!handle_) { if (flags && QString(errbuf).contains("promiscuous")) { Xnotify("Unable to set promiscuous mode on <%s> - " "stream stats time tracking will not work", qPrintable(device_)); goto _exit; } else { Xnotify("Unable to open <%s> [%s] - stream stats rx will not work", qPrintable(device_), errbuf); goto _exit; } } #ifdef Q_OS_WIN32 // pcap_setdirection() API is not supported in Windows. // NOTE: WinPcap 4.1.1 and above exports a dummy API that returns -1 // but since we would like to work with previous versions of WinPcap // also, we assume the API does not exist isDirectional_ = false; #else if (pcap_setdirection(handle_, PCAP_D_OUT) < 0) { qDebug("TxTtagStats: Error setting OUT direction %s: %s\n", qPrintable(device_), pcap_geterr(handle_)); isDirectional_ = false; } #endif if (pcap_compile(handle_, &bpf, qPrintable(capture_filter), optimize, 0) < 0) { qWarning("%s: error compiling filter: %s", qPrintable(device_), pcap_geterr(handle_)); goto _skip_filter; } if (pcap_setfilter(handle_, &bpf) < 0) { qWarning("%s: error setting filter: %s", qPrintable(device_), pcap_geterr(handle_)); goto _skip_filter; } _skip_filter: clearDebugStats(); PcapSession::preRun(); state_ = kRunning; while (1) { int ret; struct pcap_pkthdr *hdr; const uchar *data; ret = pcap_next_ex(handle_, &hdr, &data); switch (ret) { case 1: { uint ttagId; uint guid; if (SignProtocol::packetTtagId(data, hdr->caplen, &ttagId, &guid)) { #ifdef Q_OS_WIN32 // TxPort is NOT us ==> Rx Packet, so skip // See similar check in PcapRxStats for details if (ttagId >> 8 != uint(portId_)) break; ttagId &= 0xFF; #endif timing_->recordTxTime(portId_, guid, ttagId, hdr->ts); } break; } case 0: // timeout: just go back to the loop break; case -1: qWarning("%s: error reading packet (%d): %s", __PRETTY_FUNCTION__, ret, pcap_geterr(handle_)); break; case -2: qDebug("%s: Loop/signal break or some other error", __PRETTY_FUNCTION__); break; default: qWarning("%s: Unexpected return value %d", __PRETTY_FUNCTION__, ret); stop_ = true; } if (stop_) { qDebug("user requested txTtagStats stop"); break; } } PcapSession::postRun(); pcap_close(handle_); handle_ = NULL; stop_ = false; _exit: state_ = kFinished; } bool PcapTxTtagStats::start() { if (state_ == kRunning) { qWarning("TxTtagStats start requested but is already running!"); goto _exit; } state_ = kNotStarted; PcapSession::start(); while (state_ == kNotStarted) QThread::msleep(10); _exit: return true; } bool PcapTxTtagStats::stop() { if (state_ == kRunning) { stop_ = true; PcapSession::stop(); while (state_ == kRunning) QThread::msleep(10); } else qWarning("TxTtagStats stop requested but is not running!"); return true; } bool PcapTxTtagStats::isRunning() { return (state_ == kRunning); } bool PcapTxTtagStats::isDirectional() { return isDirectional_; } ostinato-1.3.0/server/pcaptxttagstats.h000066400000000000000000000023451451413623100202740ustar00rootroot00000000000000/* Copyright (C) 2023 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _PCAP_TX_TTAG_H #define _PCAP_TX_TTAG_H #include "pcapsession.h" class StreamTiming; class PcapTxTtagStats: public PcapSession { public: PcapTxTtagStats(const char *device, int id); void run(); bool start(); bool stop(); bool isRunning(); bool isDirectional(); private: enum State { kNotStarted, kRunning, kFinished }; QString device_; bool isDirectional_{true}; volatile State state_{kNotStarted}; volatile bool stop_{false}; int portId_; StreamTiming *timing_{nullptr}; }; #endif ostinato-1.3.0/server/portmanager.cpp000066400000000000000000000204371451413623100177120ustar00rootroot00000000000000/* Copyright (C) 2010-2012 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #include "portmanager.h" #include "bsdport.h" #include "interfaceinfo.h" #include "linuxport.h" #include "pcapport.h" #include "settings.h" #include "turbo.h" #include "winpcapport.h" #include #include PortManager *PortManager::instance_ = NULL; #if defined(Q_OS_WIN32) #include #include // Define the function prototypes since they are not defined in ipHlpApi.h NETIO_STATUS WINAPI ConvertInterfaceGuidToLuid( const GUID *InterfaceGuid, PNET_LUID InterfaceLuid); NETIO_STATUS WINAPI ConvertInterfaceLuidToAlias( const NET_LUID *InterfaceLuid, PWSTR InterfaceAlias, SIZE_T Length); #define MyGetProcAddress(hDll, function) \ hDll ? reinterpret_cast (GetProcAddress(hDll, #function)) : NULL; #endif PortManager::PortManager() { int i; pcap_if_t *device; AbstractPort::Accuracy txRateAccuracy; qDebug("PCAP Lib: %s", pcap_lib_version()); #ifdef Q_OS_WIN32 qDebug("Service npf status %s\n", pcapServiceStatus(L"npf")); qDebug("Service npcap status %s\n", pcapServiceStatus(L"npcap")); #endif qDebug("Retrieving the device list from the local machine\n"); #if defined(Q_OS_WIN32) WinPcapPort::fetchHostNetworkInfo(); #elif defined(Q_OS_LINUX) LinuxPort::fetchHostNetworkInfo(); #elif defined(Q_OS_BSD4) BsdPort::fetchHostNetworkInfo(); #endif txRateAccuracy = rateAccuracy(); pcap_if_t *deviceList = GetPortList(); for(device = deviceList, i = 0; device != NULL; device = device->next, i++) { AbstractPort *port = nullptr; qDebug("==========\n%d. %s", i, device->name); if (device->description) qDebug(" (%s)\n", device->description); #if defined(Q_OS_WIN32) if (!filterAcceptsPort(device->description)) #else if (!filterAcceptsPort(device->name)) #endif { qDebug("%s (%s) rejected by filter. Skipping!", device->name, device->description); i--; continue; } #if defined(Q_OS_WIN32) port = new WinPcapPort(i, device->name, device->description); #elif defined(Q_OS_LINUX) if (isTurboPort(device->name)) port = createTurboPort(i, device->name); if (!port) port = new LinuxPort(i, device->name); #elif defined(Q_OS_BSD4) port = new BsdPort(i, device->name); #else port = new PcapPort(i, device->name); #endif if (port && !port->isUsable()) { qDebug("%s: unable to open %s. Skipping!", __FUNCTION__, device->name); delete port; i--; continue; } const InterfaceInfo *intfInfo = port->interfaceInfo(); if (intfInfo) { qDebug("Mac: %012llx", intfInfo->mac); foreach(Ip4Config ip, intfInfo->ip4) qDebug("Ip4: %s/%d gw: %s", qPrintable(QHostAddress(ip.address).toString()), ip.prefixLength, qPrintable(QHostAddress(ip.gateway).toString())); foreach(Ip6Config ip, intfInfo->ip6) qDebug("Ip6: %s/%d gw: %s", qPrintable(QHostAddress(ip.address.toArray()).toString()), ip.prefixLength, qPrintable(QHostAddress(ip.gateway.toArray()).toString())); qDebug("Speed: %g Mbps", intfInfo->speed); qDebug("Mtu: %u", intfInfo->mtu); } if (!port->setRateAccuracy(txRateAccuracy)) qWarning("failed to set rateAccuracy (%d)", txRateAccuracy); portList_.append(port); } FreePortList(deviceList); foreach(AbstractPort *port, portList_) port->init(); #if defined(Q_OS_WIN32) WinPcapPort::freeHostNetworkInfo(); #elif defined(Q_OS_LINUX) LinuxPort::freeHostNetworkInfo(); #elif defined(Q_OS_BSD4) BsdPort::freeHostNetworkInfo(); #endif return; } PortManager::~PortManager() { while (!portList_.isEmpty()) delete portList_.takeFirst(); } PortManager* PortManager::instance() { if (!instance_) instance_ = new PortManager; return instance_; } pcap_if_t* PortManager::GetPortList() { pcap_if_t *deviceList = NULL; char errbuf[PCAP_ERRBUF_SIZE]; if (pcap_findalldevs(&deviceList, errbuf) == -1) qDebug("Error in pcap_findalldevs_ex: %s\n", errbuf); #if defined(Q_OS_WIN32) // Use windows' connection name as the description for a better UX ipHlpApi_ = LoadLibrary(TEXT("ipHlpApi.dll")); auto guid2Luid = MyGetProcAddress(ipHlpApi_, ConvertInterfaceGuidToLuid); auto luid2Alias = MyGetProcAddress(ipHlpApi_, ConvertInterfaceLuidToAlias); if (guid2Luid && luid2Alias) { pcap_if_t *device; for(device = deviceList; device != NULL; device = device->next) { GUID guid = static_cast(QUuid( QString(device->name).remove("\\Device\\NPF_"))); NET_LUID luid; oldDescriptions_.append(device->description); newDescriptions_.append(new QByteArray()); if (guid2Luid(&guid, &luid) == NO_ERROR) { WCHAR conn[256]; if (luid2Alias(&luid, conn, 256) == NO_ERROR) { *(newDescriptions_.last()) = QString().fromWCharArray(conn) .toLocal8Bit(); device->description = newDescriptions_.last()->data(); } } } } #endif return deviceList; } void PortManager::FreePortList(pcap_if_t *deviceList) { #if defined(Q_OS_WIN32) int i = 0; pcap_if_t *device; if (oldDescriptions_.size()) { for(device = deviceList; device != NULL; device = device->next) device->description = oldDescriptions_.at(i++); } oldDescriptions_.clear(); while (newDescriptions_.size()) delete newDescriptions_.takeFirst(); if (ipHlpApi_) FreeLibrary(ipHlpApi_); #endif pcap_freealldevs(deviceList); } AbstractPort::Accuracy PortManager::rateAccuracy() { QString rateAccuracy = appSettings->value(kRateAccuracyKey, kRateAccuracyDefaultValue).toString(); if (rateAccuracy == "High") return AbstractPort::kHighAccuracy; else if (rateAccuracy == "Low") return AbstractPort::kLowAccuracy; else qWarning("Unsupported RateAccuracy setting - %s", qPrintable(rateAccuracy)); return AbstractPort::kHighAccuracy; } bool PortManager::filterAcceptsPort(const char *name) { QRegExp pattern; QStringList includeList = appSettings->value(kPortListIncludeKey) .toStringList(); QStringList excludeList = appSettings->value(kPortListExcludeKey) .toStringList(); pattern.setPatternSyntax(QRegExp::Wildcard); // An empty (or missing) includeList accepts all ports // NOTE: A blank "IncludeList=" is read as a stringlist with one // string which is empty => treat it same as an empty stringlist if (includeList.isEmpty() || (includeList.size() == 1 && includeList.at(0).isEmpty())) goto _include_pass; foreach (QString str, includeList) { pattern.setPattern(str); if (pattern.exactMatch(name)) goto _include_pass; } // IncludeList is not empty and port did not match a pattern return false; _include_pass: foreach (QString str, excludeList) { pattern.setPattern(str); if (pattern.exactMatch(name)) return false; } // Port did not match a pattern in ExcludeList return true; } ostinato-1.3.0/server/portmanager.h000066400000000000000000000026721451413623100173600ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _SERVER_PORT_MANAGER_H #define _SERVER_PORT_MANAGER_H #include "abstractport.h" #include #include #include class PortManager { public: PortManager(); ~PortManager(); int portCount() { return portList_.size(); } AbstractPort* port(int id) { return portList_[id]; } static PortManager* instance(); private: AbstractPort::Accuracy rateAccuracy(); bool filterAcceptsPort(const char *name); private: pcap_if_t* GetPortList(); void FreePortList(pcap_if_t *deviceList); QList portList_; static PortManager *instance_; #ifdef Q_OS_WIN32 HMODULE ipHlpApi_; QList oldDescriptions_; QList newDescriptions_; #endif }; #endif ostinato-1.3.0/server/settings.h000066400000000000000000000023421451413623100166730ustar00rootroot00000000000000/* Copyright (C) 2014 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _SETTINGS_H #define _SETTINGS_H #include #include extern QSettings *appSettings; // // General Section Keys // const QString kRateAccuracyKey("RateAccuracy"); const QString kRateAccuracyDefaultValue("High"); // // RpcServer Section Keys // const QString kRpcServerAddress("RpcServer/Address"); // // PortList Section Keys // const QString kPortListIncludeKey("PortList/Include"); const QString kPortListExcludeKey("PortList/Exclude"); // // Internal Section Keys // const QString kInternalRxStatsFilterKey("Internal/RxStatsFilter"); #endif ostinato-1.3.0/server/statstuple.h000066400000000000000000000014521451413623100172440ustar00rootroot00000000000000/* Copyright (C) 2016 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _STATS_TUPLE_H #define _STATS_TUPLE_H #include struct StatsTuple { quint64 pkts; quint64 bytes; }; #endif ostinato-1.3.0/server/streamstats.h000066400000000000000000000017541451413623100174130ustar00rootroot00000000000000/* Copyright (C) 2016 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _STREAM_STATS_H #define _STREAM_STATS_H #include struct StreamStatsTuple { quint64 rx_pkts; quint64 rx_bytes; quint64 tx_pkts; quint64 tx_bytes; }; // Key(uint) is GUID typedef QHash StreamStats; typedef QHashIterator StreamStatsIterator; #endif ostinato-1.3.0/server/streamtiming.cpp000066400000000000000000000151001451413623100200650ustar00rootroot00000000000000/* Copyright (C) 2023 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #include "streamtiming.h" #include "timestamp.h" #include StreamTiming::StreamTiming(QObject *parent) : QObject(parent) { // This class MUST be part of the main thread so that timers can work Q_ASSERT(this->thread() == QCoreApplication::instance()->thread()); timer_ = new QTimer(this); connect(timer_, &QTimer::timeout, this, &StreamTiming::processRecords); timer_->setInterval(3000); gcTimer_ = new QTimer(this); connect(gcTimer_, &QTimer::timeout, this, &StreamTiming::deleteStaleRecords); gcTimer_->setInterval(30000); } void StreamTiming::start(uint portId) { if (activePortSet_.isEmpty()) { // First port? timer_->start(); gcTimer_->start(); qDebug("Stream Latency tracking started"); } activePortSet_.insert(portId); qDebug("Stream Latency tracking started for port %u", portId); } void StreamTiming::stop(uint portId) { activePortSet_.remove(portId); qDebug("Stream Latency tracking stopped for port %u", portId); if (activePortSet_.isEmpty()) { // Last port? processRecords(); deleteStaleRecords(); timer_->stop(); gcTimer_->stop(); qDebug("Stream Latency tracking stopped"); } } StreamTiming::Stats StreamTiming::stats(uint portId, uint guid) { Stats stats = {0, 0}; Q_ASSERT(guid <= SignProtocol::kMaxGuid); // Process anything pending first processRecords(); QMutexLocker locker(&timingLock_); if (!timing_.contains(portId)) return stats; Timing t = timing_.value(portId)->value(guid); if (t.countDelays == 0) return stats; stats.latency = timespecToNsecs(t.sumDelays)/t.countDelays; if (t.countDelays > 1) stats.jitter = t.sumJitter/(t.countDelays-1); return stats; } void StreamTiming::clear(uint portId, uint guid) { // XXX: We need to clear only the final timing hash; rx/tx hashes // are cleared by StreamTiming itself as part of processRecords and // deleteStaleRecords respectively QMutexLocker locker(&timingLock_); if (!timing_.contains(portId)) return; PortTiming *portTiming = timing_.value(portId); if (!portTiming) return; if (guid >= SignProtocol::kInvalidGuid) portTiming->clear(); // remove ALL guids else portTiming->remove(guid); } int StreamTiming::processRecords() { // TODO: yield after a certain count of records or time when called in // timer context; when called from delay(), process ALL int count = 0; QMutexLocker txLocker(&txHashLock_); QMutexLocker rxLocker(&rxHashLock_); QMutexLocker timingLocker(&timingLock_); auto i = rxHash_.begin(); while (i != rxHash_.end()) { if (txHash_.contains(i.key())) { struct timespec txTime = txHash_.take(i.key()).timeStamp; struct timespec rxTime = i.value().timeStamp; struct timespec diff; timespecsub(&rxTime, &txTime, &diff); uint guid = guidFromKey(i.key()); uint portId = i.value().portId; if (!timing_.contains(portId)) timing_.insert(portId, new PortTiming); PortTiming *portTiming = timing_.value(portId); Timing &guidTiming = (*portTiming)[guid]; timespecadd(&guidTiming.sumDelays, &diff, &guidTiming.sumDelays); if (guidTiming.countDelays) guidTiming.sumJitter += abs( diff.tv_sec*long(1e9) + diff.tv_nsec - guidTiming.lastDelay.tv_sec*long(1e9) - guidTiming.lastDelay.tv_nsec); guidTiming.lastDelay = diff; guidTiming.countDelays++; count++; timingDebug("[%u/%u/%u] diff %ld.%09ld (%ld.%09ld - %ld.%09ld)", i.value().portId, guid, ttagIdFromKey(i.key()), diff.tv_sec, diff.tv_nsec, rxTime.tv_sec, rxTime.tv_nsec, txTime.tv_sec, txTime.tv_nsec); timingDebug("[%u/%u](%d) total %ld.%09ld count %u jittersum %09llu", i.value().portId, guid, count, guidTiming.sumDelays.tv_sec, guidTiming.sumDelays.tv_nsec, guidTiming.countDelays, guidTiming.sumJitter); } i = rxHash_.erase(i); } Q_ASSERT(rxHash_.isEmpty()); return count; } int StreamTiming::deleteStaleRecords() { // TODO: yield after a certain count of records or time unless we are // idle when we process all; how do we determine we are "idle"? // XXX: We assume the Tx packet timestamps are based on CLOCK_REALTIME // (or a similar and comparable source). Since garbage collection timer // is not a short interval, it need not be the exact same source as long // as the values are comparable int count = 0; struct timespec now; clock_gettime(CLOCK_REALTIME, &now); // XXX: processRecords() iterates and deletes all rx records irrespective // of whether it found a matching tx record. So for garbage collection we // only need to look at (and delete) tx records QMutexLocker locker(&txHashLock_); auto i = txHash_.begin(); while (i != txHash_.end()) { struct timespec txTime = i.value().timeStamp; struct timespec diff; timespecsub(&now, &txTime, &diff); timingDebug("gc diff %ld", diff.tv_sec); if (diff.tv_sec > 30) { i = txHash_.erase(i); count++; } else { i++; } } if (count) qDebug("Latency garbage collected %d stale tx timing records", count); return count; } StreamTiming* StreamTiming::instance() { static StreamTiming *instance{nullptr}; // XXX: As of this writing, AbstractPort constructor is the first one // to call this - hence this singleton is created when the first port // is created if (!instance) instance = new StreamTiming(QCoreApplication::instance()); return instance; } ostinato-1.3.0/server/streamtiming.h000066400000000000000000000127171451413623100175450ustar00rootroot00000000000000/* Copyright (C) 2023 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _STREAM_TIMING #define _STREAM_TIMING #include "../common/debugdefs.h" #include "../common/sign.h" #include #include #include #include #include class StreamTiming : public QObject { Q_OBJECT public: struct Stats { quint64 latency; quint64 jitter; }; bool recordTxTime(uint portId, uint guid, uint ttagId, const struct timespec ×tamp); bool recordRxTime(uint portId, uint guid, uint ttagId, const struct timespec ×tamp); bool recordTxTime(uint portId, uint guid, uint ttagId, const struct timeval ×tamp); bool recordRxTime(uint portId, uint guid, uint ttagId, const struct timeval ×tamp); bool recordTxTime(uint portId, uint *ttagList, int count, const struct timespec ×tamp); Stats stats(uint portId, uint guid); void clear(uint portId, uint guid = SignProtocol::kInvalidGuid); static StreamTiming* instance(); public slots: void start(uint portId); void stop(uint portId); private: StreamTiming(QObject *parent=nullptr); int processRecords(); int deleteStaleRecords(); // XXX: use only time intervals, not absolute time quint64 timespecToNsecs(const struct timespec &interval) { return interval.tv_nsec + interval.tv_sec*1e9; } struct TtagData { struct timespec timeStamp; // nanosec resolution uint portId; }; // XXX: used only as a Qt Container value, so members will get init to 0 // when this struct is retrieved from the container due to Qt's default- // cosntructed value semantics struct Timing { struct timespec sumDelays; // nanosec resolution struct timespec lastDelay; quint64 sumJitter; // nanosec resolution uint countDelays; }; QSet activePortSet_; // XXX: TxRxKey = ttagid (8 bit MSB) + guid (24 bit LSB) // TODO: encode tx port in packet and use as part of key typedef quint32 TxRxKey; TxRxKey makeKey(uint guid, uint ttagId) { return (ttagId << 24 ) | (guid & 0x00FFFFFF); } uint guidFromKey(TxRxKey key) { return uint(key) & 0x00FFFFFF; } uint ttagIdFromKey(TxRxKey key) { return uint(key) >> 24; } QHash txHash_; QHash rxHash_; QMutex txHashLock_; QMutex rxHashLock_; typedef uint PortIdKey; typedef uint GuidKey; // guid only, no ttagid typedef QHash PortTiming; QHash timing_; QMutex timingLock_; QTimer *timer_; // Periodic timer to process tx/rx records QTimer *gcTimer_; // Garbage collection for stale tx records }; inline bool StreamTiming::recordTxTime(uint portId, uint guid, uint ttagId, const struct timespec ×tamp) { TxRxKey key = makeKey(guid, ttagId); TtagData value = { .timeStamp = timestamp, .portId = portId}; timingDebug("[%d TX] %ld:%ld ttag %u guid %u", portId, timestamp.tv_sec, long(timestamp.tv_nsec), ttagId, guid); QMutexLocker locker(&txHashLock_); txHash_.insert(key, value); return true; } inline bool StreamTiming::recordRxTime(uint portId, uint guid, uint ttagId, const struct timespec ×tamp) { TxRxKey key = makeKey(guid, ttagId); TtagData value = { .timeStamp = timestamp, .portId = portId}; timingDebug("[%d RX] %ld:%ld ttag %u guid %u", portId, timestamp.tv_sec, long(timestamp.tv_nsec), ttagId, guid); QMutexLocker locker(&rxHashLock_); rxHash_.insert(key, value); return true; } inline bool StreamTiming::recordTxTime(uint portId, uint guid, uint ttagId, const struct timeval ×tamp) { struct timespec ts; ts.tv_sec = timestamp.tv_sec; ts.tv_nsec = timestamp.tv_usec*1000; return recordTxTime(portId, guid, ttagId, ts); } inline bool StreamTiming::recordRxTime(uint portId, uint guid, uint ttagId, const struct timeval ×tamp) { struct timespec ts; ts.tv_sec = timestamp.tv_sec; ts.tv_nsec = timestamp.tv_usec*1000; return recordRxTime(portId, guid, ttagId, ts); } // TTagList contains 32-bit ttags formatted as ttagId (8msb) + guid (24lsb) inline bool StreamTiming::recordTxTime(uint portId, uint *ttagList, int count, const struct timespec ×tamp) { TtagData value = { .timeStamp = timestamp, .portId = portId}; QMutexLocker locker(&txHashLock_); for (int i = 0; i < count; i++) { TxRxKey key = TxRxKey(ttagList[i]); timingDebug("[%d TX] %ld:%ld ttag %u guid %u", portId, timestamp.tv_sec, long(timestamp.tv_nsec), ttagIdFromKey(key), guidFromKey(key)); txHash_.insert(key, value); } return true; } #endif ostinato-1.3.0/server/timespecops.h000066400000000000000000000021511451413623100173640ustar00rootroot00000000000000/* This file is part of "Ostinato" These macros are copied from BSD sys/time.h */ /* Operations on timespecs. */ #ifndef timespecclear #define timespecclear(tsp) (tsp)->tv_sec = (tsp)->tv_nsec = 0 #endif #ifndef timespecisset #define timespecisset(tsp) ((tsp)->tv_sec || (tsp)->tv_nsec) #endif #ifndef timespeccmp #define timespeccmp(tsp, usp, cmp) \ (((tsp)->tv_sec == (usp)->tv_sec) ? \ ((tsp)->tv_nsec cmp (usp)->tv_nsec) : \ ((tsp)->tv_sec cmp (usp)->tv_sec)) #endif #ifndef timespecadd #define timespecadd(tsp, usp, vsp) \ do { \ (vsp)->tv_sec = (tsp)->tv_sec + (usp)->tv_sec; \ (vsp)->tv_nsec = (tsp)->tv_nsec + (usp)->tv_nsec; \ if ((vsp)->tv_nsec >= 1000000000L) { \ (vsp)->tv_sec++; \ (vsp)->tv_nsec -= 1000000000L; \ } \ } while (0) #endif #ifndef timespecsub #define timespecsub(tsp, usp, vsp) \ do { \ (vsp)->tv_sec = (tsp)->tv_sec - (usp)->tv_sec; \ (vsp)->tv_nsec = (tsp)->tv_nsec - (usp)->tv_nsec; \ if ((vsp)->tv_nsec < 0) { \ (vsp)->tv_sec--; \ (vsp)->tv_nsec += 1000000000L; \ } \ } while (0) #endif ostinato-1.3.0/server/timestamp.h000066400000000000000000000046771451413623100170530ustar00rootroot00000000000000/* Copyright (C) 2010-2016 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _TIMESTAMP_H #define _TIMESTAMP_H #include "timespecops.h" #include "timevalops.h" #include #if defined(Q_OS_LINUX) #include #ifdef USE_NSEC_TIMESTAMP typedef struct timespec TimeStamp; static void inline getTimeStamp(TimeStamp *stamp) { clock_gettime(CLOCK_MONOTONIC, stamp); } // Returns time diff in nsecs between end and start static long inline ndiffTimeStamp(const TimeStamp *start, const TimeStamp *end) { struct timespec diff; timespecsub(end, start, &diff); long nsecs = diff.tv_nsec; if (diff.tv_sec) nsecs += diff.tv_sec*1e9; return nsecs; } #else typedef struct timeval TimeStamp; static void inline getTimeStamp(TimeStamp *stamp) { gettimeofday(stamp, NULL); } // Returns time diff in usecs between end and start static long inline udiffTimeStamp(const TimeStamp *start, const TimeStamp *end) { struct timeval diff; long usecs; timersub(end, start, &diff); usecs = diff.tv_usec; if (diff.tv_sec) usecs += diff.tv_sec*1e6; return usecs; } #endif #elif defined(Q_OS_WIN32) #include static quint64 gTicksFreq; typedef LARGE_INTEGER TimeStamp; static void inline getTimeStamp(TimeStamp* stamp) { QueryPerformanceCounter(stamp); } static long inline udiffTimeStamp(const TimeStamp *start, const TimeStamp *end) { if (end->QuadPart >= start->QuadPart) return (end->QuadPart - start->QuadPart)*long(1e6)/gTicksFreq; else { // FIXME: incorrect! what's the max value for this counter before // it rolls over? return (start->QuadPart)*long(1e6)/gTicksFreq; } } #else typedef int TimeStamp; static void inline getTimeStamp(TimeStamp*) {} static long inline udiffTimeStamp(const TimeStamp*, const TimeStamp*) { return 0; } #endif #endif ostinato-1.3.0/server/timevalops.h000066400000000000000000000032421451413623100172160ustar00rootroot00000000000000/* This file is part of "Ostinato" These macros are copied from BSD sys/time.h */ #ifndef _TIME_VAL_OPS #define _TIME_VAL_OPS /* Operations on timeval - for platforms where some are not already defined*/ #if defined(Q_OS_WIN32) #ifndef timerclear #define timerclear(tvp) ((tvp)->tv_sec = (tvp)->tv_usec = 0) #endif #ifndef timerisset #define timerisset(tvp) ((tvp)->tv_sec || (tvp)->tv_usec) #endif #ifndef timercmp #define timercmp(tvp, uvp, cmp) \ (((tvp)->tv_sec == (uvp)->tv_sec) ? \ g((tvp)->tv_usec cmp (uvp)->tv_usec) : \ g((tvp)->tv_sec cmp (uvp)->tv_sec)) #endif #ifndef timeradd #define timeradd(tvp, uvp, vvp) \ do { \ (vvp)->tv_sec = (tvp)->tv_sec + (uvp)->tv_sec; \ (vvp)->tv_usec = (tvp)->tv_usec + (uvp)->tv_usec; \ if ((vvp)->tv_usec >= 1000000) { \ (vvp)->tv_sec++; \ (vvp)->tv_usec -= 1000000; \ } \ } while (0) #endif #ifndef timersub #define timersub(tvp, uvp, vvp) \ do { \ (vvp)->tv_sec = (tvp)->tv_sec - (uvp)->tv_sec; \ (vvp)->tv_usec = (tvp)->tv_usec - (uvp)->tv_usec; \ if ((vvp)->tv_usec < 0) { \ (vvp)->tv_sec--; \ (vvp)->tv_usec += 1000000; \ } \ } while (0) #endif #endif /* Q_OS_WIN32 */ #endif ostinato-1.3.0/server/turbo.cpp000066400000000000000000000016631451413623100165260ustar00rootroot00000000000000/* Copyright (C) 2021 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #include "turbo.h" bool initTurbo() { return true; } bool isTurboPort(const char* /*device*/) { return false; } AbstractPort* createTurboPort(int /*id*/, const char* /*device*/) { return nullptr; } bool processTurboOption(int /*opt*/) { return false; } ostinato-1.3.0/server/turbo.h000066400000000000000000000015701451413623100161700ustar00rootroot00000000000000/* Copyright (C) 2021 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _TURBO_H #define _TURBO_H class AbstractPort; bool initTurbo(); bool isTurboPort(const char* device); AbstractPort* createTurboPort(int id, const char* device); bool processTurboOption(int opt); #endif ostinato-1.3.0/server/winhostdevice.cpp000066400000000000000000000141341451413623100202430ustar00rootroot00000000000000/* Copyright (C) 2018 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #include "winhostdevice.h" #include #ifdef Q_OS_WIN32 static WCHAR errBuf[256]; static inline QString errStr(ulong err) { return FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM, NULL, err, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), errBuf, sizeof(errBuf)-1, NULL) > 0 ? QString("error 0x%1 %2").arg(err, 0, 16) .arg(QString().fromWCharArray(errBuf)) : QString("error 0x%1").arg(err, 0, 16); } WindowsHostDevice::WindowsHostDevice(QString portName, DeviceManager *deviceManager) : Device(deviceManager) { GUID guid = static_cast(QUuid(portName.right(38))); ulong status = ConvertInterfaceGuidToLuid(&guid, &luid_); if (status != NO_ERROR) { qWarning("ConvertInterfaceGuidToLuid failed for %s: %s", qPrintable(portName), qPrintable(errStr(status))); luid_.Value = 0; } qInfo("Port %s: Luid %llx", qPrintable(portName), luid_.Value); } void WindowsHostDevice::receivePacket(PacketBuffer* /*pktBuf*/) { // Do Nothing } void WindowsHostDevice::clearNeighbors(Device::NeighborSet set) { // No need to do anything - see AbstractPort::resolveDeviceNeighbors() // on when this is used if (set == kUnresolvedNeighbors) return; NET_IFINDEX ifIndex; ulong status = ConvertInterfaceLuidToIndex(&luid_, &ifIndex); if (status != NO_ERROR) { qWarning("luid2ifIdx convert failed for LUID %llx: %s", luid_.Value, qPrintable(errStr(status))); return; } status = FlushIpNetTable2(AF_UNSPEC, ifIndex); if(status != NO_ERROR) qWarning("Flush ARP/ND table failed for LUID %llx: %s", luid_.Value, qPrintable(errStr(status))); } void WindowsHostDevice::getNeighbors(OstEmul::DeviceNeighborList *neighbors) { PMIB_IPNET_TABLE2 nbrs = NULL; ADDRESS_FAMILY af = AF_UNSPEC; // TODO: the following can potentially be used elsewhere // but definition of AF_XXX may be different in different // platforms if (!hasIp4_) if (!hasIp6_) return; else af = AF_INET6; else if (!hasIp6_) af = AF_INET; ulong status = GetIpNetTable2(af, &nbrs) != NO_ERROR; if (status != NO_ERROR) { qWarning("Get ARP/ND table failed for LUID %llx: %s", luid_.Value, qPrintable(errStr(status))); return; } for (uint i = 0; i < nbrs->NumEntries; i++) { if (nbrs->Table[i].InterfaceLuid.Value != luid_.Value) continue; if (nbrs->Table[i].Address.si_family == AF_INET) { OstEmul::ArpEntry *arp = neighbors->add_arp(); arp->set_ip4(qFromBigEndian( nbrs->Table[i].Address.Ipv4.sin_addr.s_addr)); arp->set_mac(qFromBigEndian( nbrs->Table[i].PhysicalAddress) >> 16); } else if (nbrs->Table[i].Address.si_family == AF_INET6) { OstEmul::NdpEntry *ndp = neighbors->add_ndp(); ndp->mutable_ip6()->set_hi(qFromBigEndian( nbrs->Table[i].Address.Ipv6.sin6_addr.u.Byte)); ndp->mutable_ip6()->set_lo(qFromBigEndian( nbrs->Table[i].Address.Ipv6.sin6_addr.u.Byte+8)); ndp->set_mac(qFromBigEndian( nbrs->Table[i].PhysicalAddress) >> 16); } } FreeMibTable(nbrs); } quint64 WindowsHostDevice::arpLookup(quint32 ip) { if (!luid_.Value) return 0; MIB_IPNET_ROW2 arpEntry; arpEntry.InterfaceLuid = luid_; arpEntry.Address.si_family = AF_INET; arpEntry.Address.Ipv4.sin_addr.s_addr = qToBigEndian(ip); if ((GetIpNetEntry2(&arpEntry) == NO_ERROR) && (arpEntry.PhysicalAddressLength == 6)) { return qFromBigEndian(arpEntry.PhysicalAddress) >> 16; } else return 0; } quint64 WindowsHostDevice::ndpLookup(UInt128 ip) { if (!luid_.Value) return 0; MIB_IPNET_ROW2 ndpEntry; ndpEntry.InterfaceLuid = luid_; ndpEntry.Address.si_family = AF_INET6; memcpy(&ndpEntry.Address.Ipv6.sin6_addr.u, ip.toArray(), 16); if ((GetIpNetEntry2(&ndpEntry) == NO_ERROR) && (ndpEntry.PhysicalAddressLength == 6)) { return qFromBigEndian(ndpEntry.PhysicalAddress) >> 16; } else return 0; } void WindowsHostDevice::sendArpRequest(quint32 tgtIp) { SOCKADDR_INET src; src.Ipv4.sin_addr.s_addr = qToBigEndian(ip4_); MIB_IPNET_ROW2 arpEntry; arpEntry.InterfaceLuid = luid_; arpEntry.Address.si_family = AF_INET; arpEntry.Address.Ipv4.sin_addr.s_addr = qToBigEndian(tgtIp); ulong status = ResolveIpNetEntry2(&arpEntry, &src); if (ResolveIpNetEntry2(&arpEntry, &src) != NO_ERROR) qWarning("Resolve arp failed for LUID %llx: %s", luid_.Value, qPrintable(errStr(status))); } void WindowsHostDevice::sendNeighborSolicit(UInt128 tgtIp) { SOCKADDR_INET src; memcpy(&src.Ipv6.sin6_addr.u, ip6_.toArray(), 16); MIB_IPNET_ROW2 ndpEntry; ndpEntry.InterfaceLuid = luid_; ndpEntry.Address.si_family = AF_INET6; memcpy(&ndpEntry.Address.Ipv6.sin6_addr.u, tgtIp.toArray(), 16); ulong status = ResolveIpNetEntry2(&ndpEntry, &src); if (ResolveIpNetEntry2(&ndpEntry, &src) != NO_ERROR) qWarning("Resolve ndp failed for LUID %llx: %s", luid_.Value, qPrintable(errStr(status))); } #endif ostinato-1.3.0/server/winhostdevice.h000066400000000000000000000026031451413623100177060ustar00rootroot00000000000000/* Copyright (C) 2018 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _WINDOWS_HOST_DEVICE_H #define _WINDOWS_HOST_DEVICE_H #include "device.h" #ifdef Q_OS_WIN32 #include #include class WindowsHostDevice: public Device { public: WindowsHostDevice(QString portName, DeviceManager *deviceManager); virtual ~WindowsHostDevice() {}; virtual void receivePacket(PacketBuffer *pktBuf); virtual void clearNeighbors(Device::NeighborSet set); virtual void getNeighbors(OstEmul::DeviceNeighborList *neighbors); protected: virtual quint64 arpLookup(quint32 ip); virtual quint64 ndpLookup(UInt128 ip); virtual void sendArpRequest(quint32 tgtIp); virtual void sendNeighborSolicit(UInt128 tgtIp); private: NET_LUID luid_; }; #endif #endif ostinato-1.3.0/server/winpcapport.cpp000066400000000000000000000245461451413623100177460ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #include "winpcapport.h" #include "interfaceinfo.h" #include #include #ifdef Q_OS_WIN32 #include #include PIP_ADAPTER_ADDRESSES WinPcapPort::adapterList_ = NULL; WinPcapPort::WinPcapPort(int id, const char *device, const char *description) : PcapPort(id, device) { populateInterfaceInfo(); monitorRx_->stop(); monitorTx_->stop(); monitorRx_->wait(); monitorTx_->wait(); delete monitorRx_; delete monitorTx_; monitorRx_ = new PortMonitor(device, kDirectionRx, &stats_); monitorTx_ = new PortMonitor(device, kDirectionTx, &stats_); data_.set_description(description); adapter_ = PacketOpenAdapter((CHAR*)device); if (!adapter_) qFatal("Unable to open adapter %s", device); linkStateOid_ = (PPACKET_OID_DATA) malloc(sizeof(PACKET_OID_DATA) + sizeof(NDIS_LINK_STATE)); if (!linkStateOid_) qFatal("failed to alloc oidData"); data_.set_is_exclusive_control(hasExclusiveControl()); minPacketSetSize_ = 256; } WinPcapPort::~WinPcapPort() { } OstProto::LinkState WinPcapPort::linkState() { memset(linkStateOid_, 0, sizeof(PACKET_OID_DATA) + sizeof(NDIS_LINK_STATE)); linkStateOid_->Oid = OID_GEN_LINK_STATE; linkStateOid_->Length = sizeof(NDIS_LINK_STATE); // TODO: migrate to the npcap-only pcap_oid_get_request() when Ostinato // stops supporting WinPcap if (PacketRequest(adapter_, 0, linkStateOid_)) { uint state; if (linkStateOid_->Length == sizeof(NDIS_LINK_STATE)) { memcpy((void*)&state, (void*)(linkStateOid_->Data+sizeof(NDIS_OBJECT_HEADER)), sizeof(state)); //qDebug("%s: state = %d", data_.description().c_str(), state); if (state == 0) linkState_ = OstProto::LinkStateUnknown; else if (state == 1) linkState_ = OstProto::LinkStateUp; else if (state == 2) linkState_ = OstProto::LinkStateDown; } else { //qDebug("%s: link state fail", data_.description().c_str()); } } else { //qDebug("%s: link state request fail", data_.description().c_str()); } return linkState_; } bool WinPcapPort::hasExclusiveControl() { QString portName(adapter_->Name + strlen("\\Device\\NPF_")); QString bindConfigFilePath(QCoreApplication::applicationDirPath() + "/bindconfig.exe"); int exitCode; qDebug("%s: %s", __FUNCTION__, qPrintable(portName)); if (!QFile::exists(bindConfigFilePath)) return false; exitCode = QProcess::execute(bindConfigFilePath, QStringList() << "comp" << portName); qDebug("%s: exit code %d", __FUNCTION__, exitCode); if (exitCode == 0) return true; else return false; } bool WinPcapPort::setExclusiveControl(bool exclusive) { QString portName(adapter_->Name + strlen("\\Device\\NPF_")); QString bindConfigFilePath(QCoreApplication::applicationDirPath() + "/bindconfig.exe"); QString status; qDebug("%s: %s", __FUNCTION__, qPrintable(portName)); if (!QFile::exists(bindConfigFilePath)) return false; status = exclusive ? "disable" : "enable"; QProcess::execute(bindConfigFilePath, QStringList() << "comp" << portName << status); updateNotes(); return (exclusive == hasExclusiveControl()); } WinPcapPort::PortMonitor::PortMonitor(const char *device, Direction direction, AbstractPort::PortStats *stats) : PcapPort::PortMonitor(device, direction, stats) { if (handle()) pcap_setmode(handle(), MODE_STAT); } void WinPcapPort::PortMonitor::run() { struct timeval lastTs; quint64 lastTxPkts = 0; quint64 lastTxBytes = 0; qDebug("in %s", __PRETTY_FUNCTION__); lastTs.tv_sec = 0; lastTs.tv_usec = 0; while (!stop_) { int ret; struct pcap_pkthdr *hdr; const uchar *data; ret = pcap_next_ex(handle(), &hdr, &data); switch (ret) { case 1: { quint64 pkts = *((quint64*)(data + 0)); quint64 bytes = *((quint64*)(data + 8)); // TODO: is it 12 or 16? bytes -= pkts * 12; uint usec = (hdr->ts.tv_sec - lastTs.tv_sec) * 1000000 + (hdr->ts.tv_usec - lastTs.tv_usec); switch (direction()) { case kDirectionRx: stats_->rxPkts += pkts; stats_->rxBytes += bytes; stats_->rxPps = qRound64(pkts * 1e6 / usec); stats_->rxBps = qRound64(bytes * 1e6 / usec); break; case kDirectionTx: if (isDirectional()) { stats_->txPkts += pkts; stats_->txBytes += bytes; } else { // Assuming stats_->txXXX are updated externally quint64 txPkts = stats_->txPkts; quint64 txBytes = stats_->txBytes; pkts = txPkts - lastTxPkts; bytes = txBytes - lastTxBytes; lastTxPkts = txPkts; lastTxBytes = txBytes; } stats_->txPps = qRound64(pkts * 1e6 / usec); stats_->txBps = qRound64(bytes * 1e6 / usec); break; default: Q_ASSERT(false); } break; } case 0: //qDebug("%s: timeout. continuing ...", __PRETTY_FUNCTION__); continue; case -1: qWarning("%s: error reading packet (%d): %s", __PRETTY_FUNCTION__, ret, pcap_geterr(handle())); break; case -2: qWarning("%s: error reading packet (%d): %s", __PRETTY_FUNCTION__, ret, pcap_geterr(handle())); break; default: qFatal("%s: Unexpected return value %d", __PRETTY_FUNCTION__, ret); } lastTs.tv_sec = hdr->ts.tv_sec; lastTs.tv_usec = hdr->ts.tv_usec; if (!stop_) QThread::msleep(1000); } } void WinPcapPort::populateInterfaceInfo() { if (!adapterList_) { qWarning("Adapter List not available"); return; } PIP_ADAPTER_ADDRESSES adapter = adapterList_; while (adapter && !QString(name()).endsWith(QString(adapter->AdapterName))) adapter = adapter->Next; if (!adapter) { qWarning("Adapter info not found for %s", name()); return; } interfaceInfo_ = new InterfaceInfo; interfaceInfo_->speed = adapter->TransmitLinkSpeed != quint64(-1) ? adapter->TransmitLinkSpeed/1e6 : 0; interfaceInfo_->mtu = adapter->Mtu; if (adapter->PhysicalAddressLength == 6) { interfaceInfo_->mac = qFromBigEndian( adapter->PhysicalAddress) >> 16; } else interfaceInfo_->mac = 0; #define SOCKET_ADDRESS_FAMILY(x) \ (x.lpSockaddr->sa_family) #define SOCKET_ADDRESS_IP4(x) \ (qFromBigEndian(((sockaddr_in*)(x.lpSockaddr))->sin_addr.S_un.S_addr)); #define SOCKET_ADDRESS_IP6(x) \ (UInt128(((PSOCKADDR_IN6)(x.lpSockaddr))->sin6_addr.u.Byte)); // We may have multiple gateways - use the first for each family quint32 ip4Gateway = 0; PIP_ADAPTER_GATEWAY_ADDRESS gateway = adapter->FirstGatewayAddress; while (gateway) { if (SOCKET_ADDRESS_FAMILY(gateway->Address) == AF_INET) { ip4Gateway = SOCKET_ADDRESS_IP4(gateway->Address); break; } gateway = gateway->Next; } UInt128 ip6Gateway(0, 0); gateway = adapter->FirstGatewayAddress; while (gateway) { if (SOCKET_ADDRESS_FAMILY(gateway->Address) == AF_INET6) { ip6Gateway = SOCKET_ADDRESS_IP6(gateway->Address); break; } gateway = gateway->Next; } PIP_ADAPTER_UNICAST_ADDRESS ucast = adapter->FirstUnicastAddress; while (ucast) { if (SOCKET_ADDRESS_FAMILY(ucast->Address) == AF_INET) { Ip4Config ip; ip.address = SOCKET_ADDRESS_IP4(ucast->Address); ip.prefixLength = ucast->OnLinkPrefixLength; ip.gateway = ip4Gateway; interfaceInfo_->ip4.append(ip); } else if (SOCKET_ADDRESS_FAMILY(ucast->Address) == AF_INET6) { Ip6Config ip; ip.address = SOCKET_ADDRESS_IP6(ucast->Address); ip.prefixLength = ucast->OnLinkPrefixLength; ip.gateway = ip6Gateway; interfaceInfo_->ip6.append(ip); } ucast = ucast->Next; } #undef SOCKET_ADDRESS_FAMILY #undef SOCKET_ADDRESS_IP4 #undef SOCKET_ADDRESS_IP6 } void WinPcapPort::fetchHostNetworkInfo() { DWORD ret; ULONG bufLen = 15*1024; // MS recommended starting size while (1) { adapterList_ = (IP_ADAPTER_ADDRESSES *) malloc(bufLen); ret = GetAdaptersAddresses(AF_UNSPEC, GAA_FLAG_INCLUDE_ALL_INTERFACES | GAA_FLAG_INCLUDE_GATEWAYS, 0, adapterList_, &bufLen); if (ret == ERROR_BUFFER_OVERFLOW) { free(adapterList_); continue; } break; } if (ret != NO_ERROR) { free(adapterList_); adapterList_ = NULL; return; } } void WinPcapPort::freeHostNetworkInfo() { free(adapterList_); adapterList_ = NULL; } #endif ostinato-1.3.0/server/winpcapport.h000066400000000000000000000030741451413623100174040ustar00rootroot00000000000000/* Copyright (C) 2010 Srivats P. This file is part of "Ostinato" This is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #ifndef _SERVER_WIN_PCAP_PORT_H #define _SERVER_WIN_PCAP_PORT_H #include #ifdef Q_OS_WIN32 #include "pcapport.h" #include #include class WinPcapPort : public PcapPort { public: WinPcapPort(int id, const char *device, const char *description); ~WinPcapPort(); virtual OstProto::LinkState linkState(); virtual bool hasExclusiveControl(); virtual bool setExclusiveControl(bool exclusive); static void fetchHostNetworkInfo(); static void freeHostNetworkInfo(); protected: class PortMonitor: public PcapPort::PortMonitor { public: PortMonitor(const char *device, Direction direction, AbstractPort::PortStats *stats); void run(); }; private: void populateInterfaceInfo(); LPADAPTER adapter_; PPACKET_OID_DATA linkStateOid_ ; static PIP_ADAPTER_ADDRESSES adapterList_; }; #endif #endif ostinato-1.3.0/shared.pri000066400000000000000000000012221451413623100153320ustar00rootroot00000000000000# # Qt qmake integration for installing files other than executables # Author: Srivats P # # To install files other than executables, specify them in SHARED variable # and include this file AFTER install.pri # # Example: # SHARED = file1 file2 # include(install.pri) # include(shared.pri) # macx { shared.path = $${PREFIX}/Ostinato/Ostinato.app/Contents/SharedSupport/ } else: unix { shared.path = $${PREFIX}/share/ostinato-controller/ } else { shared.path = $${PREFIX}/bin } shared.files = $${SHARED} INSTALLS += shared # Themes - need a subdir inside shared themes.files = $${THEMES} themes.path = $${shared.path}/themes INSTALLS += themes ostinato-1.3.0/test/000077500000000000000000000000001451413623100143325ustar00rootroot00000000000000ostinato-1.3.0/test/main.cpp000066400000000000000000000060311451413623100157620ustar00rootroot00000000000000 #include "ostprotolib.h" #include "pcapfileformat.h" #include "protocol.pb.h" #include "protocolmanager.h" #include "settings.h" #include #include #include #include extern ProtocolManager *OstProtocolManager; QSettings *appSettings; #if defined(Q_OS_WIN32) QString kGzipPathDefaultValue; QString kDiffPathDefaultValue; QString kAwkPathDefaultValue; #endif /* * Dummy Stuff for successful linking */ const char *version = ""; const char *revision = ""; quint64 getDeviceMacAddress( int /*portId*/, int /*streamId*/, int /*frameIndex*/) { return 0; } quint64 getNeighborMacAddress( int /*portId*/, int /*streamId*/, int /*frameIndex*/) { return 0; } int usage(int /*argc*/, char* argv[]) { printf("usage:\n"); printf("%s \n", argv[0]); printf("command -\n"); printf(" importpcap\n"); return 255; } /* End of dummy stuff */ int testImportPcap(int argc, char* argv[]) { bool isOk; QString error; if (argc != 3) { printf("usage:\n"); printf("%s importpcap \n", argv[0]); return 255; } OstProto::StreamConfigList streams; QString inFile(argv[2]); isOk = pcapFileFormat.open(inFile, streams, error); if (!error.isEmpty()) { printf("%s: %s\n", qPrintable(inFile), qPrintable(error)); } if (!isOk) return 1; return 0; } int main(int argc, char* argv[]) { QCoreApplication app(argc, argv); int exitCode = 0; // app init starts ... #if defined(Q_OS_WIN32) kGzipPathDefaultValue = app.applicationDirPath() + "/gzip.exe"; kDiffPathDefaultValue = app.applicationDirPath() + "/diff.exe"; kAwkPathDefaultValue = app.applicationDirPath() + "/gawk.exe"; #endif app.setApplicationName("Ostinato"); app.setOrganizationName("Ostinato"); OstProtocolManager = new ProtocolManager(); /* (Portable Mode) If we have a .ini file in the same directory as the executable, we use that instead of the platform specific location and format for the settings */ QString portableIni = QCoreApplication::applicationDirPath() + "/ostinato.ini"; if (QFile::exists(portableIni)) appSettings = new QSettings(portableIni, QSettings::IniFormat); else appSettings = new QSettings(); OstProtoLib::setExternalApplicationPaths( appSettings->value(kTsharkPathKey, kTsharkPathDefaultValue).toString(), appSettings->value(kGzipPathKey, kGzipPathDefaultValue).toString(), appSettings->value(kDiffPathKey, kDiffPathDefaultValue).toString(), appSettings->value(kAwkPathKey, kAwkPathDefaultValue).toString()); // ... app init finished // // identify and run specified test // if (argc < 2) exitCode = usage(argc, argv); else if (strcmp(argv[1],"importpcap") == 0) exitCode = testImportPcap(argc, argv); else exitCode = usage(argc, argv); delete appSettings; return exitCode; } ostinato-1.3.0/test/test.pro000066400000000000000000000022111451413623100160270ustar00rootroot00000000000000TEMPLATE = app CONFIG += qt console QT += xml network script widgets INCLUDEPATH += "../rpc/" "../common/" "../client" win32 { LIBS += -lwpcap -lpacket CONFIG(debug, debug|release) { LIBS += -L"../common/debug" -lostprotogui -lostproto LIBS += -L"../rpc/debug" -lpbrpc POST_TARGETDEPS += \ "../common/debug/libostprotogui.a" \ "../common/debug/libostproto.a" \ "../rpc/debug/libpbrpc.a" } else { LIBS += -L"../common/release" -lostprotogui -lostproto LIBS += -L"../rpc/release" -lpbrpc POST_TARGETDEPS += \ "../common/release/libostprotogui.a" \ "../common/release/libostproto.a" \ "../rpc/release/libpbrpc.a" } } else { LIBS += -lpcap LIBS += -L"../common" -lostprotogui -lostproto LIBS += -L"../rpc" -lpbrpc POST_TARGETDEPS += \ "../common/libostprotogui.a" \ "../common/libostproto.a" \ "../rpc/libpbrpc.a" } LIBS += -lprotobuf LIBS += -L"../extra/qhexedit2/$(OBJECTS_DIR)/" -lqhexedit2 HEADERS += SOURCES += main.cpp QMAKE_DISTCLEAN += object_script.* include(../install.pri) ostinato-1.3.0/version.pri000066400000000000000000000015571451413623100155640ustar00rootroot00000000000000APP_VERSION = 1.3.0 APP_REVISION = $(shell git rev-parse --short=12 --verify HEAD) #uncomment the below line in a source package and fill-in the correct revision #APP_REVISION = @ ver_info { APP_VERSION_FILE = version.cpp revtarget.target = $$APP_VERSION_FILE win32:revtarget.commands = echo "const char *version = \"$$APP_VERSION\";" \ "const char *revision = \"$$APP_REVISION\";" \ > $$APP_VERSION_FILE unix:revtarget.commands = echo \ "\"const char *version = \\\"$$APP_VERSION\\\";" \ "const char *revision = \\\"$$APP_REVISION\\\";\"" \ > $$APP_VERSION_FILE revtarget.depends = $$SOURCES $$HEADERS $$FORMS $$POST_TARGETDEPS SOURCES += $$APP_VERSION_FILE QMAKE_EXTRA_TARGETS += revtarget POST_TARGETDEPS += $$APP_VERSION_FILE QMAKE_DISTCLEAN += $$APP_VERSION_FILE }