├── .github └── workflows │ ├── block-autosquash-commits.yml │ └── check.yml ├── .gitignore ├── COPYING ├── NEWS ├── README.md ├── meson.build ├── meson_options.txt ├── po ├── LINGUAS ├── Makevars └── POTFILES.in ├── src ├── backport-autoptr.h ├── flatpak-portal.h ├── flatpak-session-helper.h ├── flatpak-spawn.c ├── meson.build ├── xdg-email.c └── xdg-open.c └── tests ├── common.c ├── common.h ├── meson.build ├── template-tap.test.in ├── test-email.c ├── test-open.c └── test-spawn.c /.github/workflows/block-autosquash-commits.yml: -------------------------------------------------------------------------------- 1 | on: pull_request 2 | 3 | name: Pull Requests 4 | 5 | jobs: 6 | message-check: 7 | name: Block Autosquash Commits 8 | 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - name: Block Autosquash Commits 13 | uses: xt0rted/block-autosquash-commits-action@v2.2.0 14 | with: 15 | repo-token: ${{ secrets.GITHUB_TOKEN }} 16 | -------------------------------------------------------------------------------- /.github/workflows/check.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | - push 5 | - pull_request 6 | 7 | jobs: 8 | check: 9 | name: Build with gcc and test 10 | runs-on: ubuntu-20.04 11 | steps: 12 | - name: Install Dependencies 13 | run: | 14 | sudo apt-get update 15 | sudo apt-get install -y dbus libglib2.0-dev meson 16 | - name: Check out 17 | uses: actions/checkout@v3 18 | - name: Configure 19 | run: meson -Db_sanitize=address,undefined _build 20 | - name: Build 21 | run: ninja -C _build 22 | - name: Test 23 | run: meson test -C _build -v 24 | env: 25 | ASAN_OPTIONS: detect_leaks=0 # Right now we're not fully clean, but this gets us use-after-free etc 26 | - name: Upload test logs 27 | uses: actions/upload-artifact@v4 28 | if: failure() || cancelled() 29 | with: 30 | name: logs 31 | path: _build/meson-logs 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ABOUT-NLS 2 | Makefile 3 | Makefile.in 4 | aclocal.m4 5 | autom4te.cache/ 6 | compile 7 | config.* 8 | configure 9 | depcomp 10 | install-sh 11 | ltmain.sh 12 | m4/ 13 | missing 14 | po/*.header 15 | po/*.sed 16 | po/*.sin 17 | po/Makefile.in.in 18 | po/Makevars.template 19 | po/Rules-quot 20 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 2.1, February 1999 3 | 4 | Copyright (C) 1991, 1999 Free Software Foundation, Inc. 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | [This is the first released version of the Lesser GPL. It also counts 10 | as the successor of the GNU Library Public License, version 2, hence 11 | the version number 2.1.] 12 | 13 | Preamble 14 | 15 | The licenses for most software are designed to take away your 16 | freedom to share and change it. By contrast, the GNU General Public 17 | Licenses are intended to guarantee your freedom to share and change 18 | free software--to make sure the software is free for all its users. 19 | 20 | This license, the Lesser General Public License, applies to some 21 | specially designated software packages--typically libraries--of the 22 | Free Software Foundation and other authors who decide to use it. You 23 | can use it too, but we suggest you first think carefully about whether 24 | this license or the ordinary General Public License is the better 25 | strategy to use in any particular case, based on the explanations below. 26 | 27 | When we speak of free software, we are referring to freedom of use, 28 | not price. Our General Public Licenses are designed to make sure that 29 | you have the freedom to distribute copies of free software (and charge 30 | for this service if you wish); that you receive source code or can get 31 | it if you want it; that you can change the software and use pieces of 32 | it in new free programs; and that you are informed that you can do 33 | these things. 34 | 35 | To protect your rights, we need to make restrictions that forbid 36 | distributors to deny you these rights or to ask you to surrender these 37 | rights. These restrictions translate to certain responsibilities for 38 | you if you distribute copies of the library or if you modify it. 39 | 40 | For example, if you distribute copies of the library, whether gratis 41 | or for a fee, you must give the recipients all the rights that we gave 42 | you. You must make sure that they, too, receive or can get the source 43 | code. If you link other code with the library, you must provide 44 | complete object files to the recipients, so that they can relink them 45 | with the library after making changes to the library and recompiling 46 | it. And you must show them these terms so they know their rights. 47 | 48 | We protect your rights with a two-step method: (1) we copyright the 49 | library, and (2) we offer you this license, which gives you legal 50 | permission to copy, distribute and/or modify the library. 51 | 52 | To protect each distributor, we want to make it very clear that 53 | there is no warranty for the free library. Also, if the library is 54 | modified by someone else and passed on, the recipients should know 55 | that what they have is not the original version, so that the original 56 | author's reputation will not be affected by problems that might be 57 | introduced by others. 58 | 59 | Finally, software patents pose a constant threat to the existence of 60 | any free program. We wish to make sure that a company cannot 61 | effectively restrict the users of a free program by obtaining a 62 | restrictive license from a patent holder. Therefore, we insist that 63 | any patent license obtained for a version of the library must be 64 | consistent with the full freedom of use specified in this license. 65 | 66 | Most GNU software, including some libraries, is covered by the 67 | ordinary GNU General Public License. This license, the GNU Lesser 68 | General Public License, applies to certain designated libraries, and 69 | is quite different from the ordinary General Public License. We use 70 | this license for certain libraries in order to permit linking those 71 | libraries into non-free programs. 72 | 73 | When a program is linked with a library, whether statically or using 74 | a shared library, the combination of the two is legally speaking a 75 | combined work, a derivative of the original library. The ordinary 76 | General Public License therefore permits such linking only if the 77 | entire combination fits its criteria of freedom. The Lesser General 78 | Public License permits more lax criteria for linking other code with 79 | the library. 80 | 81 | We call this license the "Lesser" General Public License because it 82 | does Less to protect the user's freedom than the ordinary General 83 | Public License. It also provides other free software developers Less 84 | of an advantage over competing non-free programs. These disadvantages 85 | are the reason we use the ordinary General Public License for many 86 | libraries. However, the Lesser license provides advantages in certain 87 | special circumstances. 88 | 89 | For example, on rare occasions, there may be a special need to 90 | encourage the widest possible use of a certain library, so that it becomes 91 | a de-facto standard. To achieve this, non-free programs must be 92 | allowed to use the library. A more frequent case is that a free 93 | library does the same job as widely used non-free libraries. In this 94 | case, there is little to gain by limiting the free library to free 95 | software only, so we use the Lesser General Public License. 96 | 97 | In other cases, permission to use a particular library in non-free 98 | programs enables a greater number of people to use a large body of 99 | free software. For example, permission to use the GNU C Library in 100 | non-free programs enables many more people to use the whole GNU 101 | operating system, as well as its variant, the GNU/Linux operating 102 | system. 103 | 104 | Although the Lesser General Public License is Less protective of the 105 | users' freedom, it does ensure that the user of a program that is 106 | linked with the Library has the freedom and the wherewithal to run 107 | that program using a modified version of the Library. 108 | 109 | The precise terms and conditions for copying, distribution and 110 | modification follow. Pay close attention to the difference between a 111 | "work based on the library" and a "work that uses the library". The 112 | former contains code derived from the library, whereas the latter must 113 | be combined with the library in order to run. 114 | 115 | GNU LESSER GENERAL PUBLIC LICENSE 116 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 117 | 118 | 0. This License Agreement applies to any software library or other 119 | program which contains a notice placed by the copyright holder or 120 | other authorized party saying it may be distributed under the terms of 121 | this Lesser General Public License (also called "this License"). 122 | Each licensee is addressed as "you". 123 | 124 | A "library" means a collection of software functions and/or data 125 | prepared so as to be conveniently linked with application programs 126 | (which use some of those functions and data) to form executables. 127 | 128 | The "Library", below, refers to any such software library or work 129 | which has been distributed under these terms. A "work based on the 130 | Library" means either the Library or any derivative work under 131 | copyright law: that is to say, a work containing the Library or a 132 | portion of it, either verbatim or with modifications and/or translated 133 | straightforwardly into another language. (Hereinafter, translation is 134 | included without limitation in the term "modification".) 135 | 136 | "Source code" for a work means the preferred form of the work for 137 | making modifications to it. For a library, complete source code means 138 | all the source code for all modules it contains, plus any associated 139 | interface definition files, plus the scripts used to control compilation 140 | and installation of the library. 141 | 142 | Activities other than copying, distribution and modification are not 143 | covered by this License; they are outside its scope. The act of 144 | running a program using the Library is not restricted, and output from 145 | such a program is covered only if its contents constitute a work based 146 | on the Library (independent of the use of the Library in a tool for 147 | writing it). Whether that is true depends on what the Library does 148 | and what the program that uses the Library does. 149 | 150 | 1. You may copy and distribute verbatim copies of the Library's 151 | complete source code as you receive it, in any medium, provided that 152 | you conspicuously and appropriately publish on each copy an 153 | appropriate copyright notice and disclaimer of warranty; keep intact 154 | all the notices that refer to this License and to the absence of any 155 | warranty; and distribute a copy of this License along with the 156 | Library. 157 | 158 | You may charge a fee for the physical act of transferring a copy, 159 | and you may at your option offer warranty protection in exchange for a 160 | fee. 161 | 162 | 2. You may modify your copy or copies of the Library or any portion 163 | of it, thus forming a work based on the Library, and copy and 164 | distribute such modifications or work under the terms of Section 1 165 | above, provided that you also meet all of these conditions: 166 | 167 | a) The modified work must itself be a software library. 168 | 169 | b) You must cause the files modified to carry prominent notices 170 | stating that you changed the files and the date of any change. 171 | 172 | c) You must cause the whole of the work to be licensed at no 173 | charge to all third parties under the terms of this License. 174 | 175 | d) If a facility in the modified Library refers to a function or a 176 | table of data to be supplied by an application program that uses 177 | the facility, other than as an argument passed when the facility 178 | is invoked, then you must make a good faith effort to ensure that, 179 | in the event an application does not supply such function or 180 | table, the facility still operates, and performs whatever part of 181 | its purpose remains meaningful. 182 | 183 | (For example, a function in a library to compute square roots has 184 | a purpose that is entirely well-defined independent of the 185 | application. Therefore, Subsection 2d requires that any 186 | application-supplied function or table used by this function must 187 | be optional: if the application does not supply it, the square 188 | root function must still compute square roots.) 189 | 190 | These requirements apply to the modified work as a whole. If 191 | identifiable sections of that work are not derived from the Library, 192 | and can be reasonably considered independent and separate works in 193 | themselves, then this License, and its terms, do not apply to those 194 | sections when you distribute them as separate works. But when you 195 | distribute the same sections as part of a whole which is a work based 196 | on the Library, the distribution of the whole must be on the terms of 197 | this License, whose permissions for other licensees extend to the 198 | entire whole, and thus to each and every part regardless of who wrote 199 | it. 200 | 201 | Thus, it is not the intent of this section to claim rights or contest 202 | your rights to work written entirely by you; rather, the intent is to 203 | exercise the right to control the distribution of derivative or 204 | collective works based on the Library. 205 | 206 | In addition, mere aggregation of another work not based on the Library 207 | with the Library (or with a work based on the Library) on a volume of 208 | a storage or distribution medium does not bring the other work under 209 | the scope of this License. 210 | 211 | 3. You may opt to apply the terms of the ordinary GNU General Public 212 | License instead of this License to a given copy of the Library. To do 213 | this, you must alter all the notices that refer to this License, so 214 | that they refer to the ordinary GNU General Public License, version 2, 215 | instead of to this License. (If a newer version than version 2 of the 216 | ordinary GNU General Public License has appeared, then you can specify 217 | that version instead if you wish.) Do not make any other change in 218 | these notices. 219 | 220 | Once this change is made in a given copy, it is irreversible for 221 | that copy, so the ordinary GNU General Public License applies to all 222 | subsequent copies and derivative works made from that copy. 223 | 224 | This option is useful when you wish to copy part of the code of 225 | the Library into a program that is not a library. 226 | 227 | 4. You may copy and distribute the Library (or a portion or 228 | derivative of it, under Section 2) in object code or executable form 229 | under the terms of Sections 1 and 2 above provided that you accompany 230 | it with the complete corresponding machine-readable source code, which 231 | must be distributed under the terms of Sections 1 and 2 above on a 232 | medium customarily used for software interchange. 233 | 234 | If distribution of object code is made by offering access to copy 235 | from a designated place, then offering equivalent access to copy the 236 | source code from the same place satisfies the requirement to 237 | distribute the source code, even though third parties are not 238 | compelled to copy the source along with the object code. 239 | 240 | 5. A program that contains no derivative of any portion of the 241 | Library, but is designed to work with the Library by being compiled or 242 | linked with it, is called a "work that uses the Library". Such a 243 | work, in isolation, is not a derivative work of the Library, and 244 | therefore falls outside the scope of this License. 245 | 246 | However, linking a "work that uses the Library" with the Library 247 | creates an executable that is a derivative of the Library (because it 248 | contains portions of the Library), rather than a "work that uses the 249 | library". The executable is therefore covered by this License. 250 | Section 6 states terms for distribution of such executables. 251 | 252 | When a "work that uses the Library" uses material from a header file 253 | that is part of the Library, the object code for the work may be a 254 | derivative work of the Library even though the source code is not. 255 | Whether this is true is especially significant if the work can be 256 | linked without the Library, or if the work is itself a library. The 257 | threshold for this to be true is not precisely defined by law. 258 | 259 | If such an object file uses only numerical parameters, data 260 | structure layouts and accessors, and small macros and small inline 261 | functions (ten lines or less in length), then the use of the object 262 | file is unrestricted, regardless of whether it is legally a derivative 263 | work. (Executables containing this object code plus portions of the 264 | Library will still fall under Section 6.) 265 | 266 | Otherwise, if the work is a derivative of the Library, you may 267 | distribute the object code for the work under the terms of Section 6. 268 | Any executables containing that work also fall under Section 6, 269 | whether or not they are linked directly with the Library itself. 270 | 271 | 6. As an exception to the Sections above, you may also combine or 272 | link a "work that uses the Library" with the Library to produce a 273 | work containing portions of the Library, and distribute that work 274 | under terms of your choice, provided that the terms permit 275 | modification of the work for the customer's own use and reverse 276 | engineering for debugging such modifications. 277 | 278 | You must give prominent notice with each copy of the work that the 279 | Library is used in it and that the Library and its use are covered by 280 | this License. You must supply a copy of this License. If the work 281 | during execution displays copyright notices, you must include the 282 | copyright notice for the Library among them, as well as a reference 283 | directing the user to the copy of this License. Also, you must do one 284 | of these things: 285 | 286 | a) Accompany the work with the complete corresponding 287 | machine-readable source code for the Library including whatever 288 | changes were used in the work (which must be distributed under 289 | Sections 1 and 2 above); and, if the work is an executable linked 290 | with the Library, with the complete machine-readable "work that 291 | uses the Library", as object code and/or source code, so that the 292 | user can modify the Library and then relink to produce a modified 293 | executable containing the modified Library. (It is understood 294 | that the user who changes the contents of definitions files in the 295 | Library will not necessarily be able to recompile the application 296 | to use the modified definitions.) 297 | 298 | b) Use a suitable shared library mechanism for linking with the 299 | Library. A suitable mechanism is one that (1) uses at run time a 300 | copy of the library already present on the user's computer system, 301 | rather than copying library functions into the executable, and (2) 302 | will operate properly with a modified version of the library, if 303 | the user installs one, as long as the modified version is 304 | interface-compatible with the version that the work was made with. 305 | 306 | c) Accompany the work with a written offer, valid for at 307 | least three years, to give the same user the materials 308 | specified in Subsection 6a, above, for a charge no more 309 | than the cost of performing this distribution. 310 | 311 | d) If distribution of the work is made by offering access to copy 312 | from a designated place, offer equivalent access to copy the above 313 | specified materials from the same place. 314 | 315 | e) Verify that the user has already received a copy of these 316 | materials or that you have already sent this user a copy. 317 | 318 | For an executable, the required form of the "work that uses the 319 | Library" must include any data and utility programs needed for 320 | reproducing the executable from it. However, as a special exception, 321 | the materials to be distributed need not include anything that is 322 | normally distributed (in either source or binary form) with the major 323 | components (compiler, kernel, and so on) of the operating system on 324 | which the executable runs, unless that component itself accompanies 325 | the executable. 326 | 327 | It may happen that this requirement contradicts the license 328 | restrictions of other proprietary libraries that do not normally 329 | accompany the operating system. Such a contradiction means you cannot 330 | use both them and the Library together in an executable that you 331 | distribute. 332 | 333 | 7. You may place library facilities that are a work based on the 334 | Library side-by-side in a single library together with other library 335 | facilities not covered by this License, and distribute such a combined 336 | library, provided that the separate distribution of the work based on 337 | the Library and of the other library facilities is otherwise 338 | permitted, and provided that you do these two things: 339 | 340 | a) Accompany the combined library with a copy of the same work 341 | based on the Library, uncombined with any other library 342 | facilities. This must be distributed under the terms of the 343 | Sections above. 344 | 345 | b) Give prominent notice with the combined library of the fact 346 | that part of it is a work based on the Library, and explaining 347 | where to find the accompanying uncombined form of the same work. 348 | 349 | 8. You may not copy, modify, sublicense, link with, or distribute 350 | the Library except as expressly provided under this License. Any 351 | attempt otherwise to copy, modify, sublicense, link with, or 352 | distribute the Library is void, and will automatically terminate your 353 | rights under this License. However, parties who have received copies, 354 | or rights, from you under this License will not have their licenses 355 | terminated so long as such parties remain in full compliance. 356 | 357 | 9. You are not required to accept this License, since you have not 358 | signed it. However, nothing else grants you permission to modify or 359 | distribute the Library or its derivative works. These actions are 360 | prohibited by law if you do not accept this License. Therefore, by 361 | modifying or distributing the Library (or any work based on the 362 | Library), you indicate your acceptance of this License to do so, and 363 | all its terms and conditions for copying, distributing or modifying 364 | the Library or works based on it. 365 | 366 | 10. Each time you redistribute the Library (or any work based on the 367 | Library), the recipient automatically receives a license from the 368 | original licensor to copy, distribute, link with or modify the Library 369 | subject to these terms and conditions. You may not impose any further 370 | restrictions on the recipients' exercise of the rights granted herein. 371 | You are not responsible for enforcing compliance by third parties with 372 | this License. 373 | 374 | 11. If, as a consequence of a court judgment or allegation of patent 375 | infringement or for any other reason (not limited to patent issues), 376 | conditions are imposed on you (whether by court order, agreement or 377 | otherwise) that contradict the conditions of this License, they do not 378 | excuse you from the conditions of this License. If you cannot 379 | distribute so as to satisfy simultaneously your obligations under this 380 | License and any other pertinent obligations, then as a consequence you 381 | may not distribute the Library at all. For example, if a patent 382 | license would not permit royalty-free redistribution of the Library by 383 | all those who receive copies directly or indirectly through you, then 384 | the only way you could satisfy both it and this License would be to 385 | refrain entirely from distribution of the Library. 386 | 387 | If any portion of this section is held invalid or unenforceable under any 388 | particular circumstance, the balance of the section is intended to apply, 389 | and the section as a whole is intended to apply in other circumstances. 390 | 391 | It is not the purpose of this section to induce you to infringe any 392 | patents or other property right claims or to contest validity of any 393 | such claims; this section has the sole purpose of protecting the 394 | integrity of the free software distribution system which is 395 | implemented by public license practices. Many people have made 396 | generous contributions to the wide range of software distributed 397 | through that system in reliance on consistent application of that 398 | system; it is up to the author/donor to decide if he or she is willing 399 | to distribute software through any other system and a licensee cannot 400 | impose that choice. 401 | 402 | This section is intended to make thoroughly clear what is believed to 403 | be a consequence of the rest of this License. 404 | 405 | 12. If the distribution and/or use of the Library is restricted in 406 | certain countries either by patents or by copyrighted interfaces, the 407 | original copyright holder who places the Library under this License may add 408 | an explicit geographical distribution limitation excluding those countries, 409 | so that distribution is permitted only in or among countries not thus 410 | excluded. In such case, this License incorporates the limitation as if 411 | written in the body of this License. 412 | 413 | 13. The Free Software Foundation may publish revised and/or new 414 | versions of the Lesser General Public License from time to time. 415 | Such new versions will be similar in spirit to the present version, 416 | but may differ in detail to address new problems or concerns. 417 | 418 | Each version is given a distinguishing version number. If the Library 419 | specifies a version number of this License which applies to it and 420 | "any later version", you have the option of following the terms and 421 | conditions either of that version or of any later version published by 422 | the Free Software Foundation. If the Library does not specify a 423 | license version number, you may choose any version ever published by 424 | the Free Software Foundation. 425 | 426 | 14. If you wish to incorporate parts of the Library into other free 427 | programs whose distribution conditions are incompatible with these, 428 | write to the author to ask for permission. For software which is 429 | copyrighted by the Free Software Foundation, write to the Free 430 | Software Foundation; we sometimes make exceptions for this. Our 431 | decision will be guided by the two goals of preserving the free status 432 | of all derivatives of our free software and of promoting the sharing 433 | and reuse of software generally. 434 | 435 | NO WARRANTY 436 | 437 | 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO 438 | WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. 439 | EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR 440 | OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY 441 | KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE 442 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 443 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE 444 | LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME 445 | THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 446 | 447 | 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN 448 | WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY 449 | AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU 450 | FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR 451 | CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE 452 | LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING 453 | RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A 454 | FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF 455 | SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH 456 | DAMAGES. 457 | 458 | END OF TERMS AND CONDITIONS 459 | 460 | How to Apply These Terms to Your New Libraries 461 | 462 | If you develop a new library, and you want it to be of the greatest 463 | possible use to the public, we recommend making it free software that 464 | everyone can redistribute and change. You can do so by permitting 465 | redistribution under these terms (or, alternatively, under the terms of the 466 | ordinary General Public License). 467 | 468 | To apply these terms, attach the following notices to the library. It is 469 | safest to attach them to the start of each source file to most effectively 470 | convey the exclusion of warranty; and each file should have at least the 471 | "copyright" line and a pointer to where the full notice is found. 472 | 473 | 474 | Copyright (C) 475 | 476 | This library is free software; you can redistribute it and/or 477 | modify it under the terms of the GNU Lesser General Public 478 | License as published by the Free Software Foundation; either 479 | version 2.1 of the License, or (at your option) any later version. 480 | 481 | This library is distributed in the hope that it will be useful, 482 | but WITHOUT ANY WARRANTY; without even the implied warranty of 483 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 484 | Lesser General Public License for more details. 485 | 486 | You should have received a copy of the GNU Lesser General Public 487 | License along with this library; if not, write to the Free Software 488 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 489 | 490 | Also add information on how to contact you by electronic and paper mail. 491 | 492 | You should also get your employer (if you work as a programmer) or your 493 | school, if any, to sign a "copyright disclaimer" for the library, if 494 | necessary. Here is a sample; alter the names: 495 | 496 | Yoyodyne, Inc., hereby disclaims all copyright interest in the 497 | library `Frob' (a library for tweaking knobs) written by James Random Hacker. 498 | 499 | , 1 April 1990 500 | Ty Coon, President of Vice 501 | 502 | That's all there is to it! 503 | -------------------------------------------------------------------------------- /NEWS: -------------------------------------------------------------------------------- 1 | Changes in 1.0.6 2 | ================ 3 | 4 | flatpak-spawn now supports --sandbox-a11y-own-name (if supported by portal) 5 | flatpak-spawn prints a useful error when --host isn't permitted 6 | fixed minor leak in flatpak-spawn 7 | 8 | Changes in 1.0.5 9 | ================ 10 | 11 | flatpak-spawn supports --app-path and --usr-path 12 | flatpak-spawn --sandbox-expose-path now tries to normalize paths to work better with portals. 13 | flatpak-spawn now supports --unset-env and --env-fd 14 | flatpak-spwan now supports --share-pid (if supported by portal) 15 | xdg-email now passes attachments correctly 16 | Added tests 17 | Fixed leaks 18 | 19 | Changes in 1.0.4 20 | ================ 21 | 22 | Close intermediate copies of forwarded fds in flatpak-spawn. 23 | 24 | Changes in 1.0.3 25 | ================ 26 | 27 | Better compatibility with xdg-desktop-portals 1.8.0. 28 | 29 | Changes in 1.0.2 30 | ================ 31 | 32 | Better implementation of xdg-email. 33 | flatpak-spawn supports new features of the sandbox portal. 34 | Fixes to flatpak-spawn signal handling. 35 | 36 | 37 | Changes in 1.0.1 38 | ================ 39 | 40 | New --directory option to specify working directory. 41 | 42 | Changes in 1.0.0 43 | ================ 44 | 45 | This is the first official release of flatpak-xdg-utils. 46 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This repository contains a number of commandline utilities for use 2 | inside Flatpak sandboxes. They work by talking to portals. 3 | 4 | Currently, there is flatpak-spawn for running commands in sandboxes 5 | as well as `xdg-open` and `xdg-email`, which are compatible with the 6 | well-known scripts of the same name. 7 | 8 | Everything else in `xdg-utils` is not provided. That includes 9 | `xdg-settings` and `xdg-mime` as they deal with settings that Flatpaks 10 | do not have access or control to. 11 | 12 | See https://flatpak.org/ for more information. 13 | 14 | # Installation 15 | 16 | This repository uses meson to build. Just do 17 | ``` 18 | meson [args] build 19 | ninja -Cbuild 20 | ninja -Cbuild install 21 | ``` 22 | 23 | The tools in flatpak-xdg-utils are only useful inside a Flatpak sandbox. 24 | -------------------------------------------------------------------------------- /meson.build: -------------------------------------------------------------------------------- 1 | project( 2 | 'flatpak-xdg-utils', 'c', 3 | version: '1.0.6', 4 | license: 'LGPL-2.1+', 5 | default_options: [ 6 | 'buildtype=debugoptimized', 7 | 'c_std=gnu99', 8 | 'warning_level=2', 9 | ], 10 | meson_version: '>= 0.46.0', 11 | ) 12 | 13 | bindir = join_paths(get_option('prefix'), get_option('bindir')) 14 | installed_tests_metadir = join_paths(get_option('prefix'), 15 | get_option('datadir'), 16 | 'installed-tests', 17 | meson.project_name()) 18 | installed_tests_execdir = join_paths(get_option('prefix'), 19 | get_option('libexecdir'), 20 | 'installed-tests', 21 | meson.project_name()) 22 | installed_tests_enabled = get_option('installed_tests') 23 | 24 | conf = configuration_data() 25 | conf.set_quoted('GETTEXT_PACKAGE', meson.project_name()) 26 | conf.set_quoted('PACKAGE_VERSION', meson.project_version()) 27 | conf.set_quoted('BINDIR', bindir) 28 | conf.set('_GNU_SOURCE', 1) 29 | config_h = configure_file(output: 'config.h', configuration: conf) 30 | 31 | gio_unix = dependency('gio-unix-2.0') 32 | threads = dependency('threads') 33 | 34 | srcinc = include_directories('src') 35 | 36 | subdir('src') 37 | subdir('tests') 38 | -------------------------------------------------------------------------------- /meson_options.txt: -------------------------------------------------------------------------------- 1 | option('installed_tests', 2 | type : 'boolean', 3 | value : false, 4 | description : 'enable installed tests') 5 | -------------------------------------------------------------------------------- /po/LINGUAS: -------------------------------------------------------------------------------- 1 | # Please keep this list sorted alphabetically. 2 | -------------------------------------------------------------------------------- /po/Makevars: -------------------------------------------------------------------------------- 1 | # Makefile variables for PO directory in any package using GNU gettext. 2 | 3 | # Usually the message domain is the same as the package name. 4 | DOMAIN = $(GETTEXT_DOMAIN) 5 | 6 | # These two variables depend on the location of this directory. 7 | subdir = po 8 | top_builddir = .. 9 | 10 | # These options get passed to xgettext. 11 | XGETTEXT_OPTIONS = --keyword=_ --keyword=N_ \ 12 | --keyword=C_:1c,2 --keyword=NC_:1c,2 \ 13 | --keyword=g_dngettext:2,3 \ 14 | --from-code=UTF-8 15 | 16 | 17 | # This is the copyright holder that gets inserted into the header of the 18 | # $(DOMAIN).pot file. Set this to the copyright holder of the surrounding 19 | # package. (Note that the msgstr strings, extracted from the package's 20 | # sources, belong to the copyright holder of the package.) Translators are 21 | # expected to transfer the copyright for their translations to this person 22 | # or entity, or to disclaim their copyright. The empty string stands for 23 | # the public domain; in this case the translators are expected to disclaim 24 | # their copyright. 25 | COPYRIGHT_HOLDER = Copyright © 2017 Red Hat 26 | 27 | # This is the email address or URL to which the translators shall report 28 | # bugs in the untranslated strings: 29 | # - Strings which are not entire sentences, see the maintainer guidelines 30 | # in the GNU gettext documentation, section 'Preparing Strings'. 31 | # - Strings which use unclear terms or require additional context to be 32 | # understood. 33 | # - Strings which make invalid assumptions about notation of date, time or 34 | # money. 35 | # - Pluralisation problems. 36 | # - Incorrect English spelling. 37 | # - Incorrect formatting. 38 | # It can be your email address, or a mailing list address where translators 39 | # can write to without being subscribed, or the URL of a web page through 40 | # which the translators can contact you. 41 | MSGID_BUGS_ADDRESS = https://github.com/flatpak/flatpak-xdg-utils/issues 42 | 43 | # This is the list of locale categories, beyond LC_MESSAGES, for which the 44 | # message catalogs shall be used. It is usually empty. 45 | EXTRA_LOCALE_CATEGORIES = 46 | 47 | # Ignore the timestamp of the .pot file, as git clones do not have 48 | # deterministic timestamps, and .po files are updated by translators 49 | # (only) in GNOME projects. 50 | PO_DEPENDS_ON_POT = no 51 | 52 | # This tells whether or not to forcibly update $(DOMAIN).pot and 53 | # regenerate PO files on "make dist". Possible values are "yes" and 54 | # "no". Set this to no if the POT file and PO files are maintained 55 | # externally. 56 | DIST_DEPENDS_ON_UPDATE_PO = no 57 | -------------------------------------------------------------------------------- /po/POTFILES.in: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flatpak/flatpak-xdg-utils/7c77bb30415aece282a54106fdb8ce3367ddd0b5/po/POTFILES.in -------------------------------------------------------------------------------- /src/backport-autoptr.h: -------------------------------------------------------------------------------- 1 | /* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- 2 | * 3 | * Copyright (C) 2015 Colin Walters 4 | * 5 | * GLIB - Library of useful routines for C programming 6 | * Copyright (C) 1995-1997 Peter Mattis, Spencer Kimball and Josh MacDonald 7 | * 8 | * This library is free software; you can redistribute it and/or 9 | * modify it under the terms of the GNU Lesser General Public 10 | * License as published by the Free Software Foundation; either 11 | * version 2 of the License, or (at your option) any later version. 12 | * 13 | * This library is distributed in the hope that it will be useful, 14 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 16 | * Lesser General Public License for more details. 17 | * 18 | * You should have received a copy of the GNU Lesser General Public 19 | * License along with this library; if not, write to the 20 | * Free Software Foundation, Inc., 59 Temple Place - Suite 330, 21 | * Boston, MA 02111-1307, USA. 22 | */ 23 | 24 | #pragma once 25 | 26 | #include 27 | 28 | G_BEGIN_DECLS 29 | 30 | #ifdef G_OS_UNIX 31 | #include 32 | #endif 33 | 34 | #if !GLIB_CHECK_VERSION(2, 43, 4) 35 | 36 | #define _GLIB_AUTOPTR_FUNC_NAME(TypeName) glib_autoptr_cleanup_##TypeName 37 | #define _GLIB_AUTOPTR_TYPENAME(TypeName) TypeName##_autoptr 38 | #define _GLIB_AUTO_FUNC_NAME(TypeName) glib_auto_cleanup_##TypeName 39 | #define _GLIB_CLEANUP(func) __attribute__((cleanup(func))) 40 | #define _GLIB_DEFINE_AUTOPTR_CHAINUP(ModuleObjName, ParentName) \ 41 | typedef ModuleObjName *_GLIB_AUTOPTR_TYPENAME(ModuleObjName); \ 42 | static inline void _GLIB_AUTOPTR_FUNC_NAME(ModuleObjName) (ModuleObjName **_ptr) { \ 43 | _GLIB_AUTOPTR_FUNC_NAME(ParentName) ((ParentName **) _ptr); } \ 44 | 45 | 46 | /* these macros are API */ 47 | #define G_DEFINE_AUTOPTR_CLEANUP_FUNC(TypeName, func) \ 48 | typedef TypeName *_GLIB_AUTOPTR_TYPENAME(TypeName); \ 49 | G_GNUC_BEGIN_IGNORE_DEPRECATIONS \ 50 | static inline void _GLIB_AUTOPTR_FUNC_NAME(TypeName) (TypeName **_ptr) { if (*_ptr) (func) (*_ptr); } \ 51 | G_GNUC_END_IGNORE_DEPRECATIONS 52 | #define G_DEFINE_AUTO_CLEANUP_CLEAR_FUNC(TypeName, func) \ 53 | G_GNUC_BEGIN_IGNORE_DEPRECATIONS \ 54 | static inline void _GLIB_AUTO_FUNC_NAME(TypeName) (TypeName *_ptr) { (func) (_ptr); } \ 55 | G_GNUC_END_IGNORE_DEPRECATIONS 56 | #define G_DEFINE_AUTO_CLEANUP_FREE_FUNC(TypeName, func, none) \ 57 | G_GNUC_BEGIN_IGNORE_DEPRECATIONS \ 58 | static inline void _GLIB_AUTO_FUNC_NAME(TypeName) (TypeName *_ptr) { if (*_ptr != none) (func) (*_ptr); } \ 59 | G_GNUC_END_IGNORE_DEPRECATIONS 60 | #define g_autoptr(TypeName) _GLIB_CLEANUP(_GLIB_AUTOPTR_FUNC_NAME(TypeName)) _GLIB_AUTOPTR_TYPENAME(TypeName) 61 | #define g_auto(TypeName) _GLIB_CLEANUP(_GLIB_AUTO_FUNC_NAME(TypeName)) TypeName 62 | #define g_autofree _GLIB_CLEANUP(g_autoptr_cleanup_generic_gfree) 63 | 64 | /** 65 | * g_steal_pointer: 66 | * @pp: a pointer to a pointer 67 | * 68 | * Sets @pp to %NULL, returning the value that was there before. 69 | * 70 | * Conceptually, this transfers the ownership of the pointer from the 71 | * referenced variable to the "caller" of the macro (ie: "steals" the 72 | * reference). 73 | * 74 | * The return value will be properly typed, according to the type of 75 | * @pp. 76 | * 77 | * This can be very useful when combined with g_autoptr() to prevent the 78 | * return value of a function from being automatically freed. Consider 79 | * the following example (which only works on GCC and clang): 80 | * 81 | * |[ 82 | * GObject * 83 | * create_object (void) 84 | * { 85 | * g_autoptr(GObject) obj = g_object_new (G_TYPE_OBJECT, NULL); 86 | * 87 | * if (early_error_case) 88 | * return NULL; 89 | * 90 | * return g_steal_pointer (&obj); 91 | * } 92 | * ]| 93 | * 94 | * It can also be used in similar ways for 'out' parameters and is 95 | * particularly useful for dealing with optional out parameters: 96 | * 97 | * |[ 98 | * gboolean 99 | * get_object (GObject **obj_out) 100 | * { 101 | * g_autoptr(GObject) obj = g_object_new (G_TYPE_OBJECT, NULL); 102 | * 103 | * if (early_error_case) 104 | * return FALSE; 105 | * 106 | * if (obj_out) 107 | * *obj_out = g_steal_pointer (&obj); 108 | * 109 | * return TRUE; 110 | * } 111 | * ]| 112 | * 113 | * In the above example, the object will be automatically freed in the 114 | * early error case and also in the case that %NULL was given for 115 | * @obj_out. 116 | * 117 | * Since: 2.44 118 | */ 119 | static inline gpointer 120 | (g_steal_pointer) (gpointer pp) 121 | { 122 | gpointer *ptr = (gpointer *) pp; 123 | gpointer ref; 124 | 125 | ref = *ptr; 126 | *ptr = NULL; 127 | 128 | return ref; 129 | } 130 | 131 | /* type safety */ 132 | #define g_steal_pointer(pp) \ 133 | (0 ? (*(pp)) : (g_steal_pointer) (pp)) 134 | 135 | static inline void 136 | g_autoptr_cleanup_generic_gfree (void *p) 137 | { 138 | void **pp = (void**)p; 139 | if (*pp) 140 | g_free (*pp); 141 | } 142 | 143 | G_DEFINE_AUTOPTR_CLEANUP_FUNC(GAsyncQueue, g_async_queue_unref) 144 | G_DEFINE_AUTOPTR_CLEANUP_FUNC(GBookmarkFile, g_bookmark_file_free) 145 | G_DEFINE_AUTOPTR_CLEANUP_FUNC(GBytes, g_bytes_unref) 146 | G_DEFINE_AUTOPTR_CLEANUP_FUNC(GChecksum, g_checksum_free) 147 | G_DEFINE_AUTOPTR_CLEANUP_FUNC(GDateTime, g_date_time_unref) 148 | G_DEFINE_AUTOPTR_CLEANUP_FUNC(GDir, g_dir_close) 149 | G_DEFINE_AUTOPTR_CLEANUP_FUNC(GError, g_error_free) 150 | G_DEFINE_AUTOPTR_CLEANUP_FUNC(GHashTable, g_hash_table_unref) 151 | G_DEFINE_AUTOPTR_CLEANUP_FUNC(GHmac, g_hmac_unref) 152 | G_DEFINE_AUTOPTR_CLEANUP_FUNC(GIOChannel, g_io_channel_unref) 153 | G_DEFINE_AUTOPTR_CLEANUP_FUNC(GKeyFile, g_key_file_unref) 154 | G_DEFINE_AUTOPTR_CLEANUP_FUNC(GList, g_list_free) 155 | G_DEFINE_AUTOPTR_CLEANUP_FUNC(GArray, g_array_unref) 156 | G_DEFINE_AUTOPTR_CLEANUP_FUNC(GPtrArray, g_ptr_array_unref) 157 | G_DEFINE_AUTOPTR_CLEANUP_FUNC(GMainContext, g_main_context_unref) 158 | G_DEFINE_AUTOPTR_CLEANUP_FUNC(GMainLoop, g_main_loop_unref) 159 | G_DEFINE_AUTOPTR_CLEANUP_FUNC(GSource, g_source_unref) 160 | G_DEFINE_AUTOPTR_CLEANUP_FUNC(GMappedFile, g_mapped_file_unref) 161 | G_DEFINE_AUTOPTR_CLEANUP_FUNC(GMarkupParseContext, g_markup_parse_context_unref) 162 | G_DEFINE_AUTOPTR_CLEANUP_FUNC(gchar, g_free) 163 | G_DEFINE_AUTOPTR_CLEANUP_FUNC(GNode, g_node_destroy) 164 | G_DEFINE_AUTOPTR_CLEANUP_FUNC(GOptionContext, g_option_context_free) 165 | G_DEFINE_AUTOPTR_CLEANUP_FUNC(GOptionGroup, g_option_group_free) 166 | G_DEFINE_AUTOPTR_CLEANUP_FUNC(GPatternSpec, g_pattern_spec_free) 167 | G_DEFINE_AUTOPTR_CLEANUP_FUNC(GQueue, g_queue_free) 168 | G_DEFINE_AUTO_CLEANUP_CLEAR_FUNC(GQueue, g_queue_clear) 169 | G_DEFINE_AUTOPTR_CLEANUP_FUNC(GRand, g_rand_free) 170 | G_DEFINE_AUTOPTR_CLEANUP_FUNC(GRegex, g_regex_unref) 171 | G_DEFINE_AUTOPTR_CLEANUP_FUNC(GMatchInfo, g_match_info_unref) 172 | G_DEFINE_AUTOPTR_CLEANUP_FUNC(GScanner, g_scanner_destroy) 173 | G_DEFINE_AUTOPTR_CLEANUP_FUNC(GSequence, g_sequence_free) 174 | G_DEFINE_AUTOPTR_CLEANUP_FUNC(GSList, g_slist_free) 175 | G_DEFINE_AUTOPTR_CLEANUP_FUNC(GStringChunk, g_string_chunk_free) 176 | G_DEFINE_AUTO_CLEANUP_FREE_FUNC(GStrv, g_strfreev, NULL) 177 | G_DEFINE_AUTOPTR_CLEANUP_FUNC(GThread, g_thread_unref) 178 | G_DEFINE_AUTO_CLEANUP_CLEAR_FUNC(GMutex, g_mutex_clear) 179 | G_DEFINE_AUTO_CLEANUP_CLEAR_FUNC(GCond, g_cond_clear) 180 | G_DEFINE_AUTOPTR_CLEANUP_FUNC(GTimer, g_timer_destroy) 181 | G_DEFINE_AUTOPTR_CLEANUP_FUNC(GTimeZone, g_time_zone_unref) 182 | G_DEFINE_AUTOPTR_CLEANUP_FUNC(GTree, g_tree_unref) 183 | G_DEFINE_AUTOPTR_CLEANUP_FUNC(GVariant, g_variant_unref) 184 | G_DEFINE_AUTOPTR_CLEANUP_FUNC(GVariantBuilder, g_variant_builder_unref) 185 | G_DEFINE_AUTO_CLEANUP_CLEAR_FUNC(GVariantBuilder, g_variant_builder_clear) 186 | G_DEFINE_AUTOPTR_CLEANUP_FUNC(GVariantIter, g_variant_iter_free) 187 | G_DEFINE_AUTOPTR_CLEANUP_FUNC(GVariantDict, g_variant_dict_unref) 188 | G_DEFINE_AUTO_CLEANUP_CLEAR_FUNC(GVariantDict, g_variant_dict_clear) 189 | G_DEFINE_AUTOPTR_CLEANUP_FUNC(GVariantType, g_variant_type_free) 190 | 191 | /* Add GObject-based types as needed. */ 192 | G_DEFINE_AUTOPTR_CLEANUP_FUNC(GAsyncResult, g_object_unref) 193 | G_DEFINE_AUTOPTR_CLEANUP_FUNC(GCancellable, g_object_unref) 194 | G_DEFINE_AUTOPTR_CLEANUP_FUNC(GConverter, g_object_unref) 195 | G_DEFINE_AUTOPTR_CLEANUP_FUNC(GConverterOutputStream, g_object_unref) 196 | G_DEFINE_AUTOPTR_CLEANUP_FUNC(GDataInputStream, g_object_unref) 197 | G_DEFINE_AUTOPTR_CLEANUP_FUNC(GFile, g_object_unref) 198 | G_DEFINE_AUTOPTR_CLEANUP_FUNC(GFileEnumerator, g_object_unref) 199 | G_DEFINE_AUTOPTR_CLEANUP_FUNC(GFileIOStream, g_object_unref) 200 | G_DEFINE_AUTOPTR_CLEANUP_FUNC(GFileInfo, g_object_unref) 201 | G_DEFINE_AUTOPTR_CLEANUP_FUNC(GFileInputStream, g_object_unref) 202 | G_DEFINE_AUTOPTR_CLEANUP_FUNC(GFileMonitor, g_object_unref) 203 | G_DEFINE_AUTOPTR_CLEANUP_FUNC(GFileOutputStream, g_object_unref) 204 | G_DEFINE_AUTOPTR_CLEANUP_FUNC(GInputStream, g_object_unref) 205 | G_DEFINE_AUTOPTR_CLEANUP_FUNC(GMemoryInputStream, g_object_unref) 206 | G_DEFINE_AUTOPTR_CLEANUP_FUNC(GMemoryOutputStream, g_object_unref) 207 | G_DEFINE_AUTOPTR_CLEANUP_FUNC(GMount, g_object_unref) 208 | G_DEFINE_AUTOPTR_CLEANUP_FUNC(GOutputStream, g_object_unref) 209 | G_DEFINE_AUTOPTR_CLEANUP_FUNC(GSocket, g_object_unref) 210 | G_DEFINE_AUTOPTR_CLEANUP_FUNC(GSocketAddress, g_object_unref) 211 | G_DEFINE_AUTOPTR_CLEANUP_FUNC(GSubprocess, g_object_unref) 212 | G_DEFINE_AUTOPTR_CLEANUP_FUNC(GSubprocessLauncher, g_object_unref) 213 | G_DEFINE_AUTOPTR_CLEANUP_FUNC(GTask, g_object_unref) 214 | G_DEFINE_AUTOPTR_CLEANUP_FUNC(GTlsCertificate, g_object_unref) 215 | G_DEFINE_AUTOPTR_CLEANUP_FUNC(GTlsDatabase, g_object_unref) 216 | G_DEFINE_AUTOPTR_CLEANUP_FUNC(GTlsInteraction, g_object_unref) 217 | G_DEFINE_AUTOPTR_CLEANUP_FUNC(GDBusConnection, g_object_unref) 218 | G_DEFINE_AUTOPTR_CLEANUP_FUNC(GDBusMessage, g_object_unref) 219 | G_DEFINE_AUTOPTR_CLEANUP_FUNC(GDBusMethodInvocation, g_object_unref) 220 | G_DEFINE_AUTOPTR_CLEANUP_FUNC(GVolumeMonitor, g_object_unref) 221 | G_DEFINE_AUTOPTR_CLEANUP_FUNC(GZlibCompressor, g_object_unref) 222 | G_DEFINE_AUTOPTR_CLEANUP_FUNC(GZlibDecompressor, g_object_unref) 223 | 224 | #ifdef G_OS_UNIX 225 | G_DEFINE_AUTOPTR_CLEANUP_FUNC(GUnixFDList, g_object_unref) 226 | #endif 227 | 228 | #endif /* !GLIB_CHECK_VERSION(2, 43, 3) */ 229 | 230 | #if !GLIB_CHECK_VERSION(2, 45, 8) 231 | 232 | static inline void 233 | g_autoptr_cleanup_gstring_free (GString *string) 234 | { 235 | if (string) 236 | g_string_free (string, TRUE); 237 | } 238 | 239 | G_DEFINE_AUTOPTR_CLEANUP_FUNC(GString, g_autoptr_cleanup_gstring_free) 240 | 241 | #endif 242 | 243 | G_END_DECLS 244 | -------------------------------------------------------------------------------- /src/flatpak-portal.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2018 Red Hat, Inc 3 | * 4 | * This program is free software; you can redistribute it and/or 5 | * modify it under the terms of the GNU Lesser General Public 6 | * License as published by the Free Software Foundation; either 7 | * version 2.1 of the License, or (at your option) any later version. 8 | * 9 | * This library is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | * Lesser General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Lesser General Public 15 | * License along with this library. If not, see . 16 | * 17 | * Authors: 18 | * Alexander Larsson 19 | */ 20 | 21 | #ifndef __FLATPAK_PORTAL_H__ 22 | #define __FLATPAK_PORTAL_H__ 23 | 24 | #define FLATPAK_PORTAL_BUS_NAME "org.freedesktop.portal.Flatpak" 25 | #define FLATPAK_PORTAL_PATH "/org/freedesktop/portal/Flatpak" 26 | #define FLATPAK_PORTAL_INTERFACE FLATPAK_PORTAL_BUS_NAME 27 | #define FLATPAK_PORTAL_INTERFACE_UPDATE_MONITOR FLATPAK_PORTAL_BUS_NAME ".UpdateMonitor" 28 | 29 | typedef enum { 30 | FLATPAK_SPAWN_FLAGS_CLEAR_ENV = 1 << 0, 31 | FLATPAK_SPAWN_FLAGS_LATEST_VERSION = 1 << 1, 32 | FLATPAK_SPAWN_FLAGS_SANDBOX = 1 << 2, 33 | FLATPAK_SPAWN_FLAGS_NO_NETWORK = 1 << 3, 34 | FLATPAK_SPAWN_FLAGS_WATCH_BUS = 1 << 4, 35 | FLATPAK_SPAWN_FLAGS_EXPOSE_PIDS = 1 << 5, 36 | FLATPAK_SPAWN_FLAGS_NOTIFY_START = 1 << 6, 37 | FLATPAK_SPAWN_FLAGS_SHARE_PIDS = 1 << 7, 38 | FLATPAK_SPAWN_FLAGS_EMPTY_APP = 1 << 8, 39 | FLATPAK_SPAWN_FLAGS_NONE = 0 40 | } FlatpakSpawnFlags; 41 | 42 | typedef enum { 43 | FLATPAK_SPAWN_SANDBOX_FLAGS_SHARE_DISPLAY = 1 << 0, 44 | FLATPAK_SPAWN_SANDBOX_FLAGS_SHARE_SOUND = 1 << 1, 45 | FLATPAK_SPAWN_SANDBOX_FLAGS_SHARE_GPU = 1 << 2, 46 | FLATPAK_SPAWN_SANDBOX_FLAGS_ALLOW_DBUS = 1 << 3, 47 | FLATPAK_SPAWN_SANDBOX_FLAGS_ALLOW_A11Y = 1 << 4, 48 | FLATPAK_SPAWN_SANDBOX_FLAGS_NONE = 0 49 | } FlatpakSpawnSandboxFlags; 50 | 51 | 52 | typedef enum { 53 | FLATPAK_SPAWN_SUPPORT_FLAGS_EXPOSE_PIDS = 1 << 0, 54 | FLATPAK_SPAWN_SUPPORT_FLAGS_NONE = 0 55 | } FlatpakSpawnSupportFlags; 56 | 57 | /* The same flag is reused: this feature is available under the same 58 | * circumstances */ 59 | #define FLATPAK_SPAWN_SUPPORT_FLAGS_SHARE_PIDS FLATPAK_SPAWN_SUPPORT_FLAGS_EXPOSE_PIDS 60 | 61 | #define FLATPAK_SPAWN_FLAGS_ALL (FLATPAK_SPAWN_FLAGS_CLEAR_ENV | \ 62 | FLATPAK_SPAWN_FLAGS_LATEST_VERSION | \ 63 | FLATPAK_SPAWN_FLAGS_SANDBOX | \ 64 | FLATPAK_SPAWN_FLAGS_NO_NETWORK | \ 65 | FLATPAK_SPAWN_FLAGS_WATCH_BUS | \ 66 | FLATPAK_SPAWN_FLAGS_EXPOSE_PIDS | \ 67 | FLATPAK_SPAWN_FLAGS_NOTIFY_START | \ 68 | FLATPAK_SPAWN_FLAGS_SHARE_PIDS | \ 69 | FLATPAK_SPAWN_FLAGS_EMPTY_APP) 70 | 71 | #define FLATPAK_SPAWN_SANDBOX_FLAGS_ALL (FLATPAK_SPAWN_SANDBOX_FLAGS_SHARE_DISPLAY | \ 72 | FLATPAK_SPAWN_SANDBOX_FLAGS_SHARE_SOUND | \ 73 | FLATPAK_SPAWN_SANDBOX_FLAGS_SHARE_GPU | \ 74 | FLATPAK_SPAWN_SANDBOX_FLAGS_ALLOW_DBUS | \ 75 | FLATPAK_SPAWN_SANDBOX_FLAGS_ALLOW_A11Y) 76 | 77 | #endif /* __FLATPAK_PORTAL_H__ */ 78 | -------------------------------------------------------------------------------- /src/flatpak-session-helper.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2021 Collabora Ltd. 3 | * 4 | * This program is free software; you can redistribute it and/or 5 | * modify it under the terms of the GNU Lesser General Public 6 | * License as published by the Free Software Foundation; either 7 | * version 2.1 of the License, or (at your option) any later version. 8 | * 9 | * This library is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | * Lesser General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Lesser General Public 15 | * License along with this library. If not, see . 16 | */ 17 | 18 | #ifndef __FLATPAK_SESSION_HELPER_H__ 19 | #define __FLATPAK_SESSION_HELPER_H__ 20 | 21 | #define FLATPAK_SESSION_HELPER_BUS_NAME "org.freedesktop.Flatpak" 22 | 23 | #define FLATPAK_SESSION_HELPER_PATH "/org/freedesktop/Flatpak/SessionHelper" 24 | #define FLATPAK_SESSION_HELPER_INTERFACE "org.freedesktop.Flatpak.SessionHelper" 25 | 26 | #define FLATPAK_SESSION_HELPER_PATH_DEVELOPMENT "/org/freedesktop/Flatpak/Development" 27 | #define FLATPAK_SESSION_HELPER_INTERFACE_DEVELOPMENT "org.freedesktop.Flatpak.Development" 28 | 29 | typedef enum { 30 | FLATPAK_HOST_COMMAND_FLAGS_CLEAR_ENV = 1 << 0, 31 | FLATPAK_HOST_COMMAND_FLAGS_WATCH_BUS = 1 << 1, 32 | FLATPAK_HOST_COMMAND_FLAGS_NONE = 0 33 | } FlatpakHostCommandFlags; 34 | 35 | #endif 36 | -------------------------------------------------------------------------------- /src/flatpak-spawn.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2018 Red Hat, Inc 3 | * 4 | * This program is free software; you can redistribute it and/or 5 | * modify it under the terms of the GNU Lesser General Public 6 | * License as published by the Free Software Foundation; either 7 | * version 2.1 of the License, or (at your option) any later version. 8 | * 9 | * This library is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | * Lesser General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Lesser General Public 15 | * License along with this library. If not, see . 16 | * 17 | * Authors: 18 | * Alexander Larsson 19 | */ 20 | 21 | #include 22 | #include 23 | #include 24 | #include 25 | #include 26 | #include 27 | #include 28 | #include 29 | #include 30 | #include 31 | 32 | #include 33 | #include 34 | #include 35 | 36 | #include "backport-autoptr.h" 37 | #include "flatpak-portal.h" 38 | #include "flatpak-session-helper.h" 39 | 40 | /* Change to #if 1 to check backwards-compatibility code paths */ 41 | #if 0 42 | #undef GLIB_CHECK_VERSION 43 | #define GLIB_CHECK_VERSION(x, y, z) (0) 44 | #endif 45 | 46 | static GDBusConnection *session_bus = NULL; 47 | 48 | guint child_pid = 0; 49 | gboolean opt_host; 50 | 51 | const char *service_iface; 52 | const char *service_obj_path; 53 | const char *service_bus_name; 54 | 55 | static void 56 | spawn_exited_cb (G_GNUC_UNUSED GDBusConnection *connection, 57 | G_GNUC_UNUSED const gchar *sender_name, 58 | G_GNUC_UNUSED const gchar *object_path, 59 | G_GNUC_UNUSED const gchar *interface_name, 60 | G_GNUC_UNUSED const gchar *signal_name, 61 | GVariant *parameters, 62 | G_GNUC_UNUSED gpointer user_data) 63 | { 64 | guint32 client_pid = 0; 65 | guint32 wait_status = 0; 66 | 67 | if (!g_variant_is_of_type (parameters, G_VARIANT_TYPE ("(uu)"))) 68 | return; 69 | 70 | g_variant_get (parameters, "(uu)", &client_pid, &wait_status); 71 | g_debug ("child exited %d: %d", client_pid, wait_status); 72 | 73 | if (child_pid == client_pid) 74 | { 75 | int exit_code; 76 | 77 | if (WIFEXITED (wait_status)) 78 | { 79 | exit_code = WEXITSTATUS (wait_status); 80 | } 81 | else if (WIFSIGNALED (wait_status)) 82 | { 83 | /* Smush the signal into an unsigned byte, as the shell does. This is 84 | * not quite right from the perspective of whatever ran flatpak-spawn 85 | * — it will get WIFEXITED() not WIFSIGNALED() — but the 86 | * alternative is to disconnect all signal() handlers then send this 87 | * signal to ourselves and hope it kills us. 88 | */ 89 | exit_code = 128 + WTERMSIG (wait_status); 90 | } 91 | else 92 | { 93 | /* wait(3p) claims that if the waitpid() call that returned the exit 94 | * code specified neither WUNTRACED nor WIFSIGNALED, then exactly one 95 | * of WIFEXITED() or WIFSIGNALED() will be true. 96 | */ 97 | g_warning ("wait status %d is neither WIFEXITED() nor WIFSIGNALED()", 98 | wait_status); 99 | /* EX_SOFTWARE "internal software error" from sysexits.h, for want of 100 | * a better code. 101 | */ 102 | exit_code = 70; 103 | } 104 | 105 | g_debug ("child exit code %d: %d", client_pid, exit_code); 106 | exit (exit_code); 107 | } 108 | } 109 | 110 | static void 111 | message_handler (G_GNUC_UNUSED const gchar *log_domain, 112 | GLogLevelFlags log_level, 113 | const gchar *message, 114 | G_GNUC_UNUSED gpointer user_data) 115 | { 116 | /* Make this look like normal console output */ 117 | if (log_level & G_LOG_LEVEL_DEBUG) 118 | g_printerr ("F: %s\n", message); 119 | else 120 | g_printerr ("%s: %s\n", g_get_prgname (), message); 121 | } 122 | 123 | static void 124 | forward_signal (int sig) 125 | { 126 | g_autoptr(GVariant) reply = NULL; 127 | gboolean to_process_group = FALSE; 128 | g_autoptr(GError) error = NULL; 129 | 130 | if (child_pid == 0) 131 | { 132 | /* We are not monitoring a child yet, so let the signal act on 133 | * this main process instead */ 134 | if (sig == SIGTSTP || sig == SIGSTOP || sig == SIGTTIN || sig == SIGTTOU) 135 | { 136 | raise (SIGSTOP); 137 | } 138 | else if (sig != SIGCONT) 139 | { 140 | sigset_t mask; 141 | 142 | sigemptyset (&mask); 143 | sigaddset (&mask, sig); 144 | /* Unblock it, so that it will be delivered properly this time. 145 | * Use pthread_sigmask instead of sigprocmask because the latter 146 | * has unspecified behaviour in a multi-threaded process. */ 147 | pthread_sigmask (SIG_UNBLOCK, &mask, NULL); 148 | raise (sig); 149 | } 150 | 151 | return; 152 | } 153 | 154 | g_debug ("Forwarding signal: %d", sig); 155 | 156 | /* We forward stop requests as real stop, because the default doesn't 157 | seem to be to stop for non-kernel sent TSTP??? */ 158 | if (sig == SIGTSTP) 159 | sig = SIGSTOP; 160 | 161 | /* ctrl-c/z is typically for the entire process group */ 162 | if (sig == SIGINT || sig == SIGSTOP || sig == SIGCONT) 163 | to_process_group = TRUE; 164 | 165 | reply = g_dbus_connection_call_sync (session_bus, 166 | service_bus_name, 167 | service_obj_path, 168 | service_iface, 169 | opt_host ? "HostCommandSignal" : "SpawnSignal", 170 | g_variant_new ("(uub)", 171 | child_pid, sig, to_process_group), 172 | G_VARIANT_TYPE ("()"), 173 | G_DBUS_CALL_FLAGS_NONE, 174 | -1, NULL, &error); 175 | 176 | if (error) 177 | g_debug ("Failed to forward signal: %s", error->message); 178 | 179 | if (sig == SIGSTOP) 180 | { 181 | g_debug ("SIGSTOP:ing flatpak-spawn"); 182 | raise (SIGSTOP); 183 | } 184 | } 185 | 186 | static gboolean 187 | forward_signal_handler ( 188 | #if GLIB_CHECK_VERSION (2, 36, 0) 189 | int sfd, 190 | #else 191 | GIOChannel *source, 192 | #endif 193 | G_GNUC_UNUSED GIOCondition condition, 194 | G_GNUC_UNUSED gpointer data) 195 | { 196 | struct signalfd_siginfo info; 197 | ssize_t size; 198 | 199 | #if !GLIB_CHECK_VERSION (2, 36, 0) 200 | int sfd; 201 | 202 | sfd = g_io_channel_unix_get_fd (source); 203 | g_return_val_if_fail (sfd >= 0, G_SOURCE_CONTINUE); 204 | #endif 205 | 206 | size = read (sfd, &info, sizeof (info)); 207 | 208 | if (size < 0) 209 | { 210 | if (errno != EINTR && errno != EAGAIN) 211 | g_warning ("Unable to read struct signalfd_siginfo: %s", 212 | g_strerror (errno)); 213 | } 214 | else if (size != sizeof (info)) 215 | { 216 | g_warning ("Expected struct signalfd_siginfo of size %" 217 | G_GSIZE_FORMAT ", got %" G_GSSIZE_FORMAT, 218 | sizeof (info), size); 219 | } 220 | else 221 | { 222 | forward_signal (info.ssi_signo); 223 | } 224 | 225 | return G_SOURCE_CONTINUE; 226 | } 227 | 228 | static guint 229 | forward_signals (void) 230 | { 231 | static int forward[] = { 232 | SIGHUP, SIGINT, SIGQUIT, SIGTERM, SIGCONT, SIGTSTP, SIGUSR1, SIGUSR2 233 | }; 234 | sigset_t mask; 235 | guint i; 236 | int sfd; 237 | 238 | sigemptyset (&mask); 239 | 240 | for (i = 0; i < G_N_ELEMENTS (forward); i++) 241 | sigaddset (&mask, forward[i]); 242 | 243 | sfd = signalfd (-1, &mask, SFD_NONBLOCK | SFD_CLOEXEC); 244 | 245 | if (sfd < 0) 246 | { 247 | g_warning ("Unable to watch signals: %s", g_strerror (errno)); 248 | return 0; 249 | } 250 | 251 | /* 252 | * We have to block the signals, for two reasons: 253 | * - If we didn't, most of them would kill our process. 254 | * Listening for a signal with a signalfd does not prevent the signal's 255 | * default disposition from being acted on. 256 | * - Reading from a signalfd only returns information about the signals 257 | * that are still pending for the process. If we ignored them instead 258 | * of blocking them, they would no longer be pending by the time the 259 | * main loop wakes up and reads from the signalfd. 260 | */ 261 | pthread_sigmask (SIG_BLOCK, &mask, NULL); 262 | 263 | #if GLIB_CHECK_VERSION (2, 36, 0) 264 | return g_unix_fd_add (sfd, G_IO_IN, forward_signal_handler, NULL); 265 | #else 266 | GIOChannel *channel = g_io_channel_unix_new (sfd); 267 | guint ret; 268 | 269 | /* Disable text recoding, treat it as a bytestream */ 270 | g_io_channel_set_encoding (channel, NULL, NULL); 271 | ret = g_io_add_watch (channel, G_IO_IN, forward_signal_handler, NULL); 272 | g_io_channel_unref (channel); 273 | return ret; 274 | #endif 275 | } 276 | 277 | static void 278 | name_owner_changed (G_GNUC_UNUSED GDBusConnection *connection, 279 | G_GNUC_UNUSED const gchar *sender_name, 280 | G_GNUC_UNUSED const gchar *object_path, 281 | G_GNUC_UNUSED const gchar *interface_name, 282 | G_GNUC_UNUSED const gchar *signal_name, 283 | GVariant *parameters, 284 | G_GNUC_UNUSED gpointer user_data) 285 | { 286 | const char *name, *from, *to; 287 | g_variant_get (parameters, "(&s&s&s)", &name, &from, &to); 288 | 289 | /* Check if the service dies, then we exit, because we can't track it anymore */ 290 | if (strcmp (name, service_bus_name) == 0 && 291 | strcmp (to, "") == 0) 292 | { 293 | g_debug ("portal exited"); 294 | exit (1); 295 | } 296 | } 297 | 298 | static gboolean 299 | command_specified (GPtrArray *child_argv, 300 | GError **error) 301 | { 302 | if (child_argv->len > 1) 303 | return TRUE; 304 | 305 | g_set_error (error, G_OPTION_ERROR, G_OPTION_ERROR_FAILED, 306 | "No command specified"); 307 | return FALSE; 308 | } 309 | 310 | static void 311 | session_bus_closed_cb (G_GNUC_UNUSED GDBusConnection *bus, 312 | G_GNUC_UNUSED gboolean remote_peer_vanished, 313 | G_GNUC_UNUSED GError *error, 314 | GMainLoop *loop) 315 | { 316 | g_debug ("Session bus connection closed, quitting"); 317 | g_main_loop_quit (loop); 318 | } 319 | 320 | static gboolean opt_sandbox_flags = 0; 321 | 322 | static gboolean 323 | sandbox_flag_callback (G_GNUC_UNUSED const gchar *option_name, 324 | const gchar *value, 325 | G_GNUC_UNUSED gpointer data, 326 | GError **error) 327 | { 328 | long val; 329 | char *end; 330 | 331 | if (strcmp (value, "share-display") == 0) 332 | { 333 | opt_sandbox_flags |= FLATPAK_SPAWN_SANDBOX_FLAGS_SHARE_DISPLAY; 334 | return TRUE; 335 | } 336 | 337 | if (strcmp (value, "share-sound") == 0) 338 | { 339 | opt_sandbox_flags |= FLATPAK_SPAWN_SANDBOX_FLAGS_SHARE_SOUND; 340 | return TRUE; 341 | } 342 | 343 | if (strcmp (value, "share-gpu") == 0) 344 | { 345 | opt_sandbox_flags |= FLATPAK_SPAWN_SANDBOX_FLAGS_SHARE_GPU; 346 | return TRUE; 347 | } 348 | 349 | if (strcmp (value, "allow-dbus") == 0) 350 | { 351 | opt_sandbox_flags |= FLATPAK_SPAWN_SANDBOX_FLAGS_ALLOW_DBUS; 352 | return TRUE; 353 | } 354 | 355 | if (strcmp (value, "allow-a11y") == 0) 356 | { 357 | opt_sandbox_flags |= FLATPAK_SPAWN_SANDBOX_FLAGS_ALLOW_A11Y; 358 | return TRUE; 359 | } 360 | 361 | val = strtol (value, &end, 10); 362 | if (val > 0 && *end == 0) 363 | { 364 | opt_sandbox_flags |= val; 365 | return TRUE; 366 | } 367 | 368 | g_set_error (error, G_OPTION_ERROR, G_OPTION_ERROR_FAILED, 369 | "Unknown sandbox flag %s", value); 370 | return FALSE; 371 | } 372 | 373 | static GPtrArray *sandbox_a11y_own_names = NULL; 374 | 375 | static gboolean 376 | sandbox_a11y_own_name_callback (G_GNUC_UNUSED const gchar *option_name, 377 | const gchar *value, 378 | G_GNUC_UNUSED gpointer data, 379 | GError **error) 380 | { 381 | if (sandbox_a11y_own_names == NULL) 382 | sandbox_a11y_own_names = g_ptr_array_new (); 383 | 384 | if (!g_dbus_is_name (value) || g_dbus_is_unique_name (value)) 385 | { 386 | g_set_error (error, G_OPTION_ERROR, G_OPTION_ERROR_FAILED, 387 | "Invalid bus name"); 388 | return FALSE; 389 | } 390 | 391 | g_ptr_array_add (sandbox_a11y_own_names, g_strdup (value)); 392 | return TRUE; 393 | } 394 | 395 | static guint32 396 | get_portal_version (void) 397 | { 398 | static guint32 version = 0; 399 | 400 | if (version == 0) 401 | { 402 | g_autoptr(GError) error = NULL; 403 | g_autoptr(GVariant) reply = 404 | g_dbus_connection_call_sync (session_bus, 405 | service_bus_name, 406 | service_obj_path, 407 | "org.freedesktop.DBus.Properties", 408 | "Get", 409 | g_variant_new ("(ss)", service_iface, "version"), 410 | G_VARIANT_TYPE ("(v)"), 411 | G_DBUS_CALL_FLAGS_NONE, 412 | -1, 413 | NULL, &error); 414 | 415 | if (reply == NULL) 416 | g_debug ("Failed to get version: %s", error->message); 417 | else 418 | { 419 | g_autoptr(GVariant) v = g_variant_get_child_value (reply, 0); 420 | g_autoptr(GVariant) v2 = g_variant_get_variant (v); 421 | version = g_variant_get_uint32 (v2); 422 | } 423 | } 424 | 425 | return version; 426 | } 427 | 428 | static void 429 | check_portal_version (const char *option, guint32 version_needed) 430 | { 431 | guint32 portal_version = get_portal_version (); 432 | if (portal_version < version_needed) 433 | { 434 | g_printerr ("--%s not supported by host portal version (need version %d, has %d)\n", option, version_needed, portal_version); 435 | exit (1); 436 | } 437 | } 438 | 439 | static guint32 440 | get_portal_supports (void) 441 | { 442 | static guint32 supports = 0; 443 | static gboolean ran = FALSE; 444 | 445 | if (!ran) 446 | { 447 | g_autoptr(GError) error = NULL; 448 | g_autoptr(GVariant) reply = NULL; 449 | 450 | ran = TRUE; 451 | 452 | /* Support flags were added in version 3 */ 453 | if (get_portal_version () >= 3) 454 | { 455 | reply = g_dbus_connection_call_sync (session_bus, 456 | service_bus_name, 457 | service_obj_path, 458 | "org.freedesktop.DBus.Properties", 459 | "Get", 460 | g_variant_new ("(ss)", service_iface, "supports"), 461 | G_VARIANT_TYPE ("(v)"), 462 | G_DBUS_CALL_FLAGS_NONE, 463 | -1, 464 | NULL, &error); 465 | if (reply == NULL) 466 | g_debug ("Failed to get supports: %s", error->message); 467 | else 468 | { 469 | g_autoptr(GVariant) v = g_variant_get_child_value (reply, 0); 470 | g_autoptr(GVariant) v2 = g_variant_get_variant (v); 471 | supports = g_variant_get_uint32 (v2); 472 | } 473 | } 474 | } 475 | 476 | return supports; 477 | } 478 | 479 | #define NOT_SETUID_ROOT_MESSAGE \ 480 | "This feature requires Flatpak to be using a bubblewrap (bwrap) executable\n" \ 481 | "that is not setuid root.\n" \ 482 | "\n" \ 483 | "The non-setuid version of bubblewrap requires a kernel that allows\n" \ 484 | "unprivileged users to create new user namespaces.\n" \ 485 | "\n" \ 486 | "For more details please see:\n" \ 487 | "https://github.com/flatpak/flatpak/wiki/User-namespace-requirements\n" \ 488 | "\n" 489 | 490 | static void 491 | check_portal_supports (const char *option, guint32 supports_needed) 492 | { 493 | guint32 supports = get_portal_supports (); 494 | 495 | if ((supports & supports_needed) != supports_needed) 496 | { 497 | g_printerr ("--%s not supported by host portal\n", option); 498 | 499 | if (supports_needed == FLATPAK_SPAWN_SUPPORT_FLAGS_EXPOSE_PIDS) 500 | g_printerr ("\n%s", NOT_SETUID_ROOT_MESSAGE); 501 | 502 | exit (1); 503 | } 504 | } 505 | 506 | /* 507 | * @str: A path 508 | * @prefix: A possible prefix 509 | * 510 | * The same as flatpak_has_path_prefix(), but instead of a boolean, 511 | * return the part of @str after @prefix (non-%NULL but possibly empty) 512 | * if @str has prefix @prefix, or %NULL if it does not. 513 | * 514 | * Returns: (nullable) (transfer none): the part of @str after @prefix, 515 | * or %NULL if @str is not below @prefix 516 | */ 517 | static const char * 518 | get_path_after (const char *str, 519 | const char *prefix) 520 | { 521 | while (TRUE) 522 | { 523 | /* Skip consecutive slashes to reach next path 524 | element */ 525 | while (*str == '/') 526 | str++; 527 | while (*prefix == '/') 528 | prefix++; 529 | 530 | /* No more prefix path elements? Done! */ 531 | if (*prefix == 0) 532 | return str; 533 | 534 | /* Compare path element */ 535 | while (*prefix != 0 && *prefix != '/') 536 | { 537 | if (*str != *prefix) 538 | return NULL; 539 | str++; 540 | prefix++; 541 | } 542 | 543 | /* Matched prefix path element, 544 | must be entire str path element */ 545 | if (*str != '/' && *str != 0) 546 | return NULL; 547 | } 548 | } 549 | 550 | static gint32 551 | path_to_handle (GUnixFDList *fd_list, 552 | const char *path, 553 | const char *home_realpath, 554 | const char *flatpak_id, 555 | GError **error) 556 | { 557 | int path_fd = open (path, O_PATH|O_CLOEXEC|O_NOFOLLOW|O_RDONLY); 558 | int saved_errno; 559 | gint32 handle; 560 | 561 | if (path_fd < 0) 562 | { 563 | saved_errno = errno; 564 | g_set_error (error, G_IO_ERROR, g_io_error_from_errno (saved_errno), 565 | "Failed to open %s to expose in sandbox: %s", 566 | path, g_strerror (saved_errno)); 567 | return -1; 568 | } 569 | 570 | if (home_realpath != NULL && flatpak_id != NULL) 571 | { 572 | g_autofree char *real = NULL; 573 | const char *after = NULL; 574 | 575 | real = realpath (path, NULL); 576 | 577 | if (real != NULL) 578 | after = get_path_after (real, home_realpath); 579 | 580 | if (after != NULL) 581 | { 582 | g_autofree char *var_path = NULL; 583 | int var_fd = -1; 584 | struct stat path_buf; 585 | struct stat var_buf; 586 | 587 | /* @after is possibly "", but that's OK: if @path is exactly $HOME, 588 | * we want to check whether it's the same file as 589 | * ~/.var/app/$FLATPAK_ID, with no suffix 590 | */ 591 | var_path = g_build_filename (home_realpath, ".var", "app", flatpak_id, 592 | after, NULL); 593 | 594 | var_fd = open (var_path, O_PATH|O_CLOEXEC|O_NOFOLLOW|O_RDONLY); 595 | 596 | if (var_fd >= 0 && 597 | fstat (path_fd, &path_buf) == 0 && 598 | fstat (var_fd, &var_buf) == 0 && 599 | path_buf.st_dev == var_buf.st_dev && 600 | path_buf.st_ino == var_buf.st_ino) 601 | { 602 | close (path_fd); 603 | path_fd = var_fd; 604 | var_fd = -1; 605 | } 606 | else 607 | { 608 | close (var_fd); 609 | } 610 | } 611 | } 612 | 613 | 614 | handle = g_unix_fd_list_append (fd_list, path_fd, error); 615 | 616 | if (handle < 0) 617 | { 618 | g_prefix_error (error, "Failed to add fd to list for %s: ", path); 619 | close (path_fd); 620 | return -1; 621 | } 622 | 623 | /* The GUnixFdList keeps a duplicate, so we should release the original */ 624 | close (path_fd); 625 | return handle; 626 | } 627 | 628 | static gboolean 629 | add_paths_to_variant (GVariantBuilder *builder, 630 | GUnixFDList *fd_list, 631 | const GStrv paths, 632 | const char *home_realpath, 633 | const char *flatpak_id, 634 | gboolean ignore_errors) 635 | { 636 | g_autoptr(GError) error = NULL; 637 | 638 | if (!paths) 639 | return TRUE; 640 | 641 | for (gsize i = 0; paths[i] != NULL; i++) 642 | { 643 | gint32 handle = path_to_handle (fd_list, paths[i], home_realpath, 644 | flatpak_id, 645 | ignore_errors ? NULL : &error); 646 | 647 | if (handle < 0) 648 | { 649 | if (ignore_errors) 650 | continue; 651 | 652 | g_printerr ("%s\n", error->message); 653 | return FALSE; 654 | } 655 | 656 | g_variant_builder_add (builder, "h", handle); 657 | } 658 | 659 | return TRUE; 660 | } 661 | 662 | static GHashTable *opt_env = NULL; 663 | static GHashTable *opt_unsetenv = NULL; 664 | 665 | static gboolean 666 | opt_env_cb (G_GNUC_UNUSED const char *option_name, 667 | const gchar *value, 668 | G_GNUC_UNUSED gpointer data, 669 | GError **error) 670 | { 671 | g_auto(GStrv) split = g_strsplit (value, "=", 2); 672 | 673 | g_assert (opt_env != NULL); 674 | g_assert (opt_unsetenv != NULL); 675 | 676 | if (split == NULL || 677 | split[0] == NULL || 678 | split[0][0] == 0 || 679 | split[1] == NULL) 680 | { 681 | g_set_error (error, G_OPTION_ERROR, G_OPTION_ERROR_BAD_VALUE, 682 | "Invalid env format %s", value); 683 | return FALSE; 684 | } 685 | 686 | g_hash_table_remove (opt_unsetenv, split[0]); 687 | g_hash_table_replace (opt_env, 688 | g_steal_pointer (&split[0]), 689 | g_steal_pointer (&split[1])); 690 | return TRUE; 691 | } 692 | 693 | static gboolean 694 | opt_unset_env_cb (G_GNUC_UNUSED const char *option_name, 695 | const gchar *value, 696 | G_GNUC_UNUSED gpointer data, 697 | G_GNUC_UNUSED GError **error) 698 | { 699 | g_assert (opt_env != NULL); 700 | g_assert (opt_unsetenv != NULL); 701 | 702 | g_hash_table_remove (opt_env, value); 703 | g_hash_table_add (opt_unsetenv, g_strdup (value)); 704 | return TRUE; 705 | } 706 | 707 | static gboolean 708 | option_env_fd_cb (G_GNUC_UNUSED const gchar *option_name, 709 | const gchar *value, 710 | G_GNUC_UNUSED gpointer data, 711 | GError **error) 712 | { 713 | g_autofree gchar *proc_filename = NULL; 714 | g_autofree gchar *env_block = NULL; 715 | gsize remaining; 716 | const char *p; 717 | guint64 fd; 718 | gchar *endptr; 719 | 720 | g_assert (opt_env != NULL); 721 | g_assert (opt_unsetenv != NULL); 722 | 723 | fd = g_ascii_strtoull (value, &endptr, 10); 724 | 725 | if (endptr == NULL || *endptr != '\0' || fd > G_MAXINT) 726 | { 727 | g_set_error (error, G_OPTION_ERROR, G_OPTION_ERROR_BAD_VALUE, 728 | "Not a valid file descriptor: %s", value); 729 | return FALSE; 730 | } 731 | 732 | proc_filename = g_strdup_printf ("/proc/self/fd/%d", (int) fd); 733 | 734 | if (!g_file_get_contents (proc_filename, &env_block, &remaining, error)) 735 | return FALSE; 736 | 737 | p = env_block; 738 | 739 | while (remaining > 0) 740 | { 741 | g_autofree gchar *var = NULL; 742 | g_autofree gchar *val = NULL; 743 | size_t len = strnlen (p, remaining); 744 | const char *equals; 745 | 746 | g_assert (len <= remaining); 747 | 748 | equals = memchr (p, '=', len); 749 | 750 | if (equals == NULL || equals == p) 751 | { 752 | g_set_error (error, G_OPTION_ERROR, G_OPTION_ERROR_BAD_VALUE, 753 | "Environment variable must be given in the form VARIABLE=VALUE, not %.*s", 754 | (int) len, p); 755 | return FALSE; 756 | } 757 | 758 | var = g_strndup (p, equals - p); 759 | val = g_strndup (equals + 1, len - (equals - p) - 1); 760 | g_hash_table_remove (opt_unsetenv, var); 761 | g_hash_table_replace (opt_env, 762 | g_steal_pointer (&var), 763 | g_steal_pointer (&val)); 764 | 765 | p += len; 766 | remaining -= len; 767 | 768 | if (remaining > 0) 769 | { 770 | g_assert (*p == '\0'); 771 | p += 1; 772 | remaining -= 1; 773 | } 774 | } 775 | 776 | if (fd >= 3) 777 | close (fd); 778 | 779 | return TRUE; 780 | } 781 | 782 | int 783 | main (int argc, 784 | char **argv) 785 | { 786 | GMainLoop *loop; 787 | g_autoptr(GError) error = NULL; 788 | GOptionContext *context; 789 | g_autoptr(GPtrArray) child_argv = NULL; 790 | int i, opt_argc; 791 | gboolean verbose = FALSE; 792 | char **forward_fds = NULL; 793 | guint spawn_flags; 794 | gboolean opt_clear_env = FALSE; 795 | gboolean opt_watch_bus = FALSE; 796 | gboolean opt_expose_pids = FALSE; 797 | gboolean opt_share_pids = FALSE; 798 | gboolean opt_latest_version = FALSE; 799 | gboolean opt_sandbox = FALSE; 800 | gboolean opt_no_network = FALSE; 801 | char **opt_sandbox_expose = NULL; 802 | char **opt_sandbox_expose_ro = NULL; 803 | char **opt_sandbox_expose_path = NULL; 804 | char **opt_sandbox_expose_path_ro = NULL; 805 | char **opt_sandbox_expose_path_try = NULL; 806 | char **opt_sandbox_expose_path_ro_try = NULL; 807 | char *opt_directory = NULL; 808 | char *opt_app_path = NULL; 809 | char *opt_usr_path = NULL; 810 | g_autofree char *cwd = NULL; 811 | g_autofree char *home_realpath = NULL; 812 | const char *flatpak_id = NULL; 813 | GVariantBuilder options_builder; 814 | const GOptionEntry options[] = { 815 | { "verbose", 'v', 0, G_OPTION_ARG_NONE, &verbose, "Enable debug output", NULL }, 816 | { "forward-fd", 0, 0, G_OPTION_ARG_STRING_ARRAY, &forward_fds, "Forward file descriptor", "FD" }, 817 | { "clear-env", 0, 0, G_OPTION_ARG_NONE, &opt_clear_env, "Run with clean environment", NULL }, 818 | { "watch-bus", 0, 0, G_OPTION_ARG_NONE, &opt_watch_bus, "Make the spawned command exit if we do", NULL }, 819 | { "expose-pids", 0, 0, G_OPTION_ARG_NONE, &opt_expose_pids, "Expose sandbox pid in calling sandbox", NULL }, 820 | { "share-pids", 0, 0, G_OPTION_ARG_NONE, &opt_share_pids, "Use same pid namespace as calling sandbox", NULL }, 821 | { "env", 0, 0, G_OPTION_ARG_CALLBACK, &opt_env_cb, "Set environment variable", "VAR=VALUE" }, 822 | { "unset-env", 0, 0, G_OPTION_ARG_CALLBACK, &opt_unset_env_cb, "Unset environment variable", "VAR=VALUE" }, 823 | { "env-fd", 0, 0, G_OPTION_ARG_CALLBACK, &option_env_fd_cb, "Read environment variables in env -0 format from FD", "FD" }, 824 | { "latest-version", 0, 0, G_OPTION_ARG_NONE, &opt_latest_version, "Run latest version", NULL }, 825 | { "sandbox", 0, 0, G_OPTION_ARG_NONE, &opt_sandbox, "Run sandboxed", NULL }, 826 | { "no-network", 0, 0, G_OPTION_ARG_NONE, &opt_no_network, "Run without network access", NULL }, 827 | { "sandbox-expose", 0, 0, G_OPTION_ARG_STRING_ARRAY, &opt_sandbox_expose, "Expose access to named file", "NAME" }, 828 | { "sandbox-expose-ro", 0, 0, G_OPTION_ARG_STRING_ARRAY, &opt_sandbox_expose_ro, "Expose readonly access to named file", "NAME" }, 829 | { "sandbox-expose-path", 0, 0, G_OPTION_ARG_FILENAME_ARRAY, &opt_sandbox_expose_path, "Expose access to path", "PATH" }, 830 | { "sandbox-expose-path-ro", 0, 0, G_OPTION_ARG_FILENAME_ARRAY, &opt_sandbox_expose_path_ro, "Expose readonly access to path", "PATH" }, 831 | { "sandbox-expose-path-try", 0, 0, G_OPTION_ARG_FILENAME_ARRAY, &opt_sandbox_expose_path_try, "Expose access to path if it exists", "PATH" }, 832 | { "sandbox-expose-path-ro-try", 0, 0, G_OPTION_ARG_FILENAME_ARRAY, &opt_sandbox_expose_path_ro_try, "Expose readonly access to path if it exists", "PATH" }, 833 | { "sandbox-flag", 0, 0, G_OPTION_ARG_CALLBACK, sandbox_flag_callback, "Enable sandbox flag", "FLAG" }, 834 | { "sandbox-a11y-own-name", 0, 0, G_OPTION_ARG_CALLBACK, sandbox_a11y_own_name_callback, "Allow owning the name on the a11y bus", "DBUS_NAME" }, 835 | { "host", 0, 0, G_OPTION_ARG_NONE, &opt_host, "Start the command on the host", NULL }, 836 | { "directory", 0, 0, G_OPTION_ARG_FILENAME, &opt_directory, "Working directory in which to run the command", "DIR" }, 837 | { "app-path", 0, 0, G_OPTION_ARG_FILENAME, &opt_app_path, "Replace runtime's /app with DIR or empty", "DIR|\"\"" }, 838 | { "usr-path", 0, 0, G_OPTION_ARG_FILENAME, &opt_usr_path, "Replace runtime's /usr with DIR", "DIR" }, 839 | { NULL } 840 | }; 841 | guint signal_source = 0; 842 | GHashTableIter iter; 843 | gpointer key, value; 844 | 845 | setlocale (LC_ALL, ""); 846 | 847 | g_setenv ("GIO_USE_VFS", "local", TRUE); 848 | 849 | g_set_prgname (argv[0]); 850 | 851 | child_argv = g_ptr_array_new (); 852 | 853 | cwd = g_get_current_dir (); 854 | 855 | i = 1; 856 | while (i < argc && argv[i][0] == '-') 857 | i++; 858 | 859 | opt_argc = i; 860 | opt_env = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free); 861 | opt_unsetenv = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL); 862 | 863 | while (i < argc) 864 | { 865 | g_ptr_array_add (child_argv, argv[i]); 866 | i++; 867 | } 868 | g_ptr_array_add (child_argv, NULL); 869 | 870 | context = g_option_context_new ("COMMAND [ARGUMENT…]"); 871 | 872 | g_option_context_set_summary (context, "Run a command in a sandbox"); 873 | g_option_context_add_main_entries (context, options, GETTEXT_PACKAGE); 874 | 875 | if (!g_option_context_parse (context, &opt_argc, &argv, &error) || 876 | !command_specified (child_argv, &error)) 877 | { 878 | g_printerr ("%s: %s", g_get_application_name(), error->message); 879 | g_printerr ("\n"); 880 | g_printerr ("Try \"%s --help\" for more information.", 881 | g_get_prgname ()); 882 | g_printerr ("\n"); 883 | g_option_context_free (context); 884 | return 1; 885 | } 886 | 887 | if (verbose) 888 | g_log_set_handler (G_LOG_DOMAIN, G_LOG_LEVEL_DEBUG, message_handler, NULL); 889 | 890 | /* We have to block the signals we want to forward before we start any 891 | * other thread, and in particular the GDBus worker thread, because 892 | * the signal mask is per-thread. We need all threads to have the same 893 | * mask, otherwise a thread that doesn't have the mask will receive 894 | * process-directed signals, causing the whole process to exit. */ 895 | signal_source = forward_signals (); 896 | 897 | if (signal_source == 0) 898 | return 1; 899 | 900 | flatpak_id = g_getenv ("FLATPAK_ID"); 901 | 902 | if (flatpak_id != NULL) 903 | home_realpath = realpath (g_get_home_dir (), NULL); 904 | 905 | session_bus = g_bus_get_sync (G_BUS_TYPE_SESSION, NULL, &error); 906 | if (session_bus == NULL) 907 | { 908 | g_printerr ("Can't find bus: %s\n", error->message); 909 | return 1; 910 | } 911 | 912 | if (opt_host) 913 | { 914 | service_iface = FLATPAK_SESSION_HELPER_INTERFACE_DEVELOPMENT; 915 | service_obj_path = FLATPAK_SESSION_HELPER_PATH_DEVELOPMENT; 916 | service_bus_name = FLATPAK_SESSION_HELPER_BUS_NAME; 917 | } 918 | else 919 | { 920 | service_iface = FLATPAK_PORTAL_INTERFACE; 921 | service_obj_path = FLATPAK_PORTAL_PATH; 922 | service_bus_name = FLATPAK_PORTAL_BUS_NAME; 923 | } 924 | 925 | g_dbus_connection_signal_subscribe (session_bus, 926 | NULL, 927 | service_iface, 928 | opt_host ? "HostCommandExited" : "SpawnExited", 929 | service_obj_path, 930 | NULL, 931 | G_DBUS_SIGNAL_FLAGS_NONE, 932 | spawn_exited_cb, 933 | NULL, NULL); 934 | 935 | g_autoptr(GVariantBuilder) fd_builder = g_variant_builder_new (G_VARIANT_TYPE ("a{uh}")); 936 | g_autoptr(GVariantBuilder) env_builder = g_variant_builder_new (G_VARIANT_TYPE ("a{ss}")); 937 | g_autoptr(GUnixFDList) fd_list = g_unix_fd_list_new (); 938 | gint stdin_handle = -1; 939 | gint stdout_handle = -1; 940 | gint stderr_handle = -1; 941 | 942 | stdin_handle = g_unix_fd_list_append (fd_list, 0, &error); 943 | if (stdin_handle == -1) 944 | { 945 | g_printerr ("Can't append fd: %s\n", error->message); 946 | return 1; 947 | } 948 | stdout_handle = g_unix_fd_list_append (fd_list, 1, &error); 949 | if (stdout_handle == -1) 950 | { 951 | g_printerr ("Can't append fd: %s\n", error->message); 952 | return 1; 953 | } 954 | stderr_handle = g_unix_fd_list_append (fd_list, 2, &error); 955 | if (stderr_handle == -1) 956 | { 957 | g_printerr ("Can't append fd: %s\n", error->message); 958 | return 1; 959 | } 960 | 961 | g_variant_builder_add (fd_builder, "{uh}", 0, stdin_handle); 962 | g_variant_builder_add (fd_builder, "{uh}", 1, stdout_handle); 963 | g_variant_builder_add (fd_builder, "{uh}", 2, stderr_handle); 964 | g_autoptr(GVariant) reply = NULL; 965 | 966 | for (i = 0; forward_fds != NULL && forward_fds[i] != NULL; i++) 967 | { 968 | int fd = strtol (forward_fds[i], NULL, 10); 969 | gint handle = -1; 970 | 971 | if (fd == 0) 972 | { 973 | g_printerr ("Invalid fd '%s'\n", forward_fds[i]); 974 | return 1; 975 | } 976 | 977 | if (fd >= 0 && fd <= 2) 978 | continue; // We always forward these 979 | 980 | handle = g_unix_fd_list_append (fd_list, fd, &error); 981 | if (handle == -1) 982 | { 983 | g_printerr ("Can't append fd: %s\n", error->message); 984 | return 1; 985 | } 986 | /* The GUnixFdList keeps a duplicate, so we should release the original */ 987 | close (fd); 988 | g_variant_builder_add (fd_builder, "{uh}", fd, handle); 989 | } 990 | 991 | g_hash_table_iter_init (&iter, opt_env); 992 | 993 | while (g_hash_table_iter_next (&iter, &key, &value)) 994 | g_variant_builder_add (env_builder, "{ss}", key, value); 995 | 996 | g_clear_pointer (&opt_env, g_hash_table_unref); 997 | 998 | spawn_flags = 0; 999 | 1000 | if (opt_clear_env) 1001 | spawn_flags |= opt_host ? FLATPAK_HOST_COMMAND_FLAGS_CLEAR_ENV : FLATPAK_SPAWN_FLAGS_CLEAR_ENV; 1002 | 1003 | if (opt_watch_bus) 1004 | spawn_flags |= opt_host ? FLATPAK_HOST_COMMAND_FLAGS_WATCH_BUS : FLATPAK_SPAWN_FLAGS_WATCH_BUS; 1005 | 1006 | if (opt_share_pids) 1007 | { 1008 | if (opt_host) 1009 | { 1010 | g_printerr ("--host not compatible with --share-pids\n"); 1011 | return 1; 1012 | } 1013 | 1014 | check_portal_version ("share-pids", 5); 1015 | check_portal_supports ("share-pids", FLATPAK_SPAWN_SUPPORT_FLAGS_SHARE_PIDS); 1016 | 1017 | spawn_flags |= FLATPAK_SPAWN_FLAGS_SHARE_PIDS; 1018 | } 1019 | else if (opt_expose_pids) 1020 | { 1021 | if (opt_host) 1022 | { 1023 | g_printerr ("--host not compatible with --expose-pids\n"); 1024 | return 1; 1025 | } 1026 | 1027 | check_portal_version ("expose-pids", 3); 1028 | check_portal_supports ("expose-pids", FLATPAK_SPAWN_SUPPORT_FLAGS_EXPOSE_PIDS); 1029 | 1030 | spawn_flags |= FLATPAK_SPAWN_FLAGS_EXPOSE_PIDS; 1031 | } 1032 | 1033 | if (opt_latest_version) 1034 | { 1035 | if (opt_host) 1036 | { 1037 | g_printerr ("--host not compatible with --latest-version\n"); 1038 | return 1; 1039 | } 1040 | spawn_flags |= FLATPAK_SPAWN_FLAGS_LATEST_VERSION; 1041 | } 1042 | 1043 | if (opt_sandbox) 1044 | { 1045 | if (opt_host) 1046 | { 1047 | g_printerr ("--host not compatible with --sandbox\n"); 1048 | return 1; 1049 | } 1050 | spawn_flags |= FLATPAK_SPAWN_FLAGS_SANDBOX; 1051 | } 1052 | 1053 | if (opt_no_network) 1054 | { 1055 | if (opt_host) 1056 | { 1057 | g_printerr ("--host not compatible with --no-network\n"); 1058 | return 1; 1059 | } 1060 | spawn_flags |= FLATPAK_SPAWN_FLAGS_NO_NETWORK; 1061 | } 1062 | 1063 | g_variant_builder_init (&options_builder, G_VARIANT_TYPE ("a{sv}")); 1064 | 1065 | if (g_hash_table_size (opt_unsetenv) > 0) 1066 | { 1067 | g_hash_table_iter_init (&iter, opt_unsetenv); 1068 | 1069 | /* The host portal doesn't support options, so we always have to do 1070 | * this the hard way. The subsandbox portal supports unset-env in 1071 | * versions >= 5. */ 1072 | if (opt_host ? FALSE : (get_portal_version () >= 5)) 1073 | { 1074 | GVariantBuilder strv_builder; 1075 | 1076 | g_variant_builder_init (&strv_builder, G_VARIANT_TYPE_STRING_ARRAY); 1077 | 1078 | while (g_hash_table_iter_next (&iter, &key, NULL)) 1079 | g_variant_builder_add (&strv_builder, "s", key); 1080 | 1081 | g_variant_builder_add (&options_builder, "{s@v}", "unset-env", 1082 | g_variant_new_variant (g_variant_builder_end (&strv_builder))); 1083 | } 1084 | else 1085 | { 1086 | /* env(1) will do the wrong thing if argv[0] contains an equals 1087 | * sign, so we might need to prepend this incantation - and 1088 | * because we're prepending, we need to do it backwards. 1089 | * More legibly, we're replacing MY=COMMAND ARGS with: 1090 | * 1091 | * /usr/bin/env -u VAR -u VAR2 /bin/sh -euc 'exec "$@"' sh MY=COMMAND ARGS 1092 | * 1093 | * This is a standard trick for dealing with env(1). */ 1094 | g_assert (child_argv->len >= 1); 1095 | 1096 | if (strchr (g_ptr_array_index (child_argv, 0), '=') != NULL) 1097 | { 1098 | g_ptr_array_insert (child_argv, 0, g_strdup ("sh")); /* argv[0] */ 1099 | g_ptr_array_insert (child_argv, 0, g_strdup ("exec \"$@\"")); 1100 | g_ptr_array_insert (child_argv, 0, g_strdup ("-euc")); 1101 | g_ptr_array_insert (child_argv, 0, g_strdup ("/bin/sh")); 1102 | } 1103 | 1104 | while (g_hash_table_iter_next (&iter, &key, NULL)) 1105 | { 1106 | /* Again, yes, this is backwards: we're prepending. */ 1107 | g_ptr_array_insert (child_argv, 0, g_strdup (key)); 1108 | g_ptr_array_insert (child_argv, 0, g_strdup ("-u")); 1109 | } 1110 | 1111 | g_ptr_array_insert (child_argv, 0, g_strdup ("/usr/bin/env")); 1112 | } 1113 | } 1114 | 1115 | g_clear_pointer (&opt_unsetenv, g_hash_table_unref); 1116 | 1117 | if (opt_sandbox_expose) 1118 | { 1119 | if (opt_host) 1120 | { 1121 | g_printerr ("--host not compatible with --sandbox-expose\n"); 1122 | return 1; 1123 | } 1124 | g_variant_builder_add (&options_builder, "{s@v}", "sandbox-expose", 1125 | g_variant_new_variant (g_variant_new_strv ((const char * const *)opt_sandbox_expose, -1))); 1126 | } 1127 | 1128 | if (opt_sandbox_expose_ro) 1129 | { 1130 | if (opt_host) 1131 | { 1132 | g_printerr ("--host not compatible with --sandbox-expose-ro\n"); 1133 | return 1; 1134 | } 1135 | g_variant_builder_add (&options_builder, "{s@v}", "sandbox-expose-ro", 1136 | g_variant_new_variant (g_variant_new_strv ((const char * const *)opt_sandbox_expose_ro, -1))); 1137 | } 1138 | 1139 | if (opt_sandbox_flags) 1140 | { 1141 | if (opt_host) 1142 | { 1143 | g_printerr ("--host not compatible with --sandbox-flag\n"); 1144 | return 1; 1145 | } 1146 | 1147 | check_portal_version ("sandbox-flags", 3); 1148 | 1149 | g_variant_builder_add (&options_builder, "{s@v}", "sandbox-flags", 1150 | g_variant_new_variant (g_variant_new_uint32 (opt_sandbox_flags))); 1151 | } 1152 | 1153 | if (opt_sandbox_expose_path || opt_sandbox_expose_path_try) 1154 | { 1155 | g_autoptr(GVariantBuilder) expose_fd_builder = g_variant_builder_new (G_VARIANT_TYPE ("ah")); 1156 | 1157 | if (opt_host) 1158 | { 1159 | g_printerr ("--host not compatible with --sandbox-expose-path\n"); 1160 | return 1; 1161 | } 1162 | 1163 | check_portal_version ("sandbox-expose-path", 3); 1164 | 1165 | if (!add_paths_to_variant (expose_fd_builder, fd_list, opt_sandbox_expose_path, home_realpath, flatpak_id, FALSE) 1166 | || !add_paths_to_variant (expose_fd_builder, fd_list, opt_sandbox_expose_path_try, home_realpath, flatpak_id, TRUE)) 1167 | return 1; 1168 | 1169 | g_variant_builder_add (&options_builder, "{s@v}", "sandbox-expose-fd", 1170 | g_variant_new_variant (g_variant_builder_end (g_steal_pointer (&expose_fd_builder)))); 1171 | } 1172 | 1173 | if (opt_sandbox_expose_path_ro || opt_sandbox_expose_path_ro_try) 1174 | { 1175 | g_autoptr(GVariantBuilder) expose_fd_builder = g_variant_builder_new (G_VARIANT_TYPE ("ah")); 1176 | 1177 | if (opt_host) 1178 | { 1179 | g_printerr ("--host not compatible with --sandbox-expose-path-ro\n"); 1180 | return 1; 1181 | } 1182 | 1183 | check_portal_version ("sandbox-expose-path-ro", 3); 1184 | 1185 | if (!add_paths_to_variant (expose_fd_builder, fd_list, opt_sandbox_expose_path_ro, home_realpath, flatpak_id, FALSE) 1186 | || !add_paths_to_variant (expose_fd_builder, fd_list, opt_sandbox_expose_path_ro_try, home_realpath, flatpak_id, TRUE)) 1187 | return 1; 1188 | 1189 | g_variant_builder_add (&options_builder, "{s@v}", "sandbox-expose-fd-ro", 1190 | g_variant_new_variant (g_variant_builder_end (g_steal_pointer (&expose_fd_builder)))); 1191 | } 1192 | 1193 | if (sandbox_a11y_own_names != NULL) 1194 | { 1195 | g_autoptr(GVariantBuilder) sandbox_a11y_own_names_builder = g_variant_builder_new (G_VARIANT_TYPE ("as")); 1196 | 1197 | check_portal_version ("sandbox-a11y-own-names", 7); 1198 | 1199 | for (size_t i = 0; i < sandbox_a11y_own_names->len; i++) 1200 | g_variant_builder_add (sandbox_a11y_own_names_builder, "s", g_ptr_array_index (sandbox_a11y_own_names, i)); 1201 | 1202 | g_variant_builder_add (&options_builder, "{s@v}", "sandbox-a11y-own-names", 1203 | g_variant_new_variant (g_variant_builder_end (g_steal_pointer (&sandbox_a11y_own_names_builder)))); 1204 | } 1205 | 1206 | if (opt_app_path != NULL) 1207 | { 1208 | gint32 handle; 1209 | 1210 | g_debug ("Using \"%s\" as /app instead of runtime", opt_app_path); 1211 | 1212 | if (opt_host) 1213 | { 1214 | g_printerr ("--host not compatible with --app-path\n"); 1215 | return 1; 1216 | } 1217 | 1218 | check_portal_version ("app-path", 6); 1219 | 1220 | if (opt_app_path[0] == '\0') 1221 | { 1222 | /* Empty path is special-cased to mean an empty directory */ 1223 | spawn_flags |= FLATPAK_SPAWN_FLAGS_EMPTY_APP; 1224 | } 1225 | else 1226 | { 1227 | handle = path_to_handle (fd_list, opt_app_path, home_realpath, 1228 | flatpak_id, &error); 1229 | 1230 | if (handle < 0) 1231 | { 1232 | g_printerr ("%s\n", error->message); 1233 | return 1; 1234 | } 1235 | 1236 | g_variant_builder_add (&options_builder, "{s@v}", "app-fd", 1237 | g_variant_new_variant (g_variant_new_handle (handle))); 1238 | } 1239 | } 1240 | 1241 | if (opt_usr_path != NULL) 1242 | { 1243 | gint32 handle; 1244 | 1245 | g_debug ("Using %s as /usr instead of runtime", opt_usr_path); 1246 | 1247 | if (opt_host) 1248 | { 1249 | g_printerr ("--host not compatible with --usr-path\n"); 1250 | return 1; 1251 | } 1252 | 1253 | check_portal_version ("usr-path", 6); 1254 | 1255 | handle = path_to_handle (fd_list, opt_usr_path, home_realpath, 1256 | flatpak_id, &error); 1257 | 1258 | if (handle < 0) 1259 | { 1260 | g_printerr ("%s\n", error->message); 1261 | return 1; 1262 | } 1263 | 1264 | g_variant_builder_add (&options_builder, "{s@v}", "usr-fd", 1265 | g_variant_new_variant (g_variant_new_handle (handle))); 1266 | } 1267 | 1268 | if (!opt_directory) 1269 | { 1270 | opt_directory = cwd; 1271 | } 1272 | 1273 | g_dbus_connection_signal_subscribe (session_bus, 1274 | "org.freedesktop.DBus", 1275 | "org.freedesktop.DBus", 1276 | "NameOwnerChanged", 1277 | "/org/freedesktop/DBus", 1278 | NULL, 1279 | G_DBUS_SIGNAL_FLAGS_NONE, 1280 | name_owner_changed, 1281 | NULL, NULL); 1282 | 1283 | { 1284 | g_autoptr(GVariant) fds = NULL; 1285 | g_autoptr(GVariant) env = NULL; 1286 | g_autoptr(GVariant) opts = NULL; 1287 | 1288 | fds = g_variant_ref_sink (g_variant_builder_end (g_steal_pointer (&fd_builder))); 1289 | env = g_variant_ref_sink (g_variant_builder_end (g_steal_pointer (&env_builder))); 1290 | opts = g_variant_ref_sink (g_variant_builder_end (&options_builder)); 1291 | 1292 | retry: 1293 | reply = g_dbus_connection_call_with_unix_fd_list_sync (session_bus, 1294 | service_bus_name, 1295 | service_obj_path, 1296 | service_iface, 1297 | opt_host ? "HostCommand" : "Spawn", 1298 | opt_host ? 1299 | g_variant_new ("(^ay^aay@a{uh}@a{ss}u)", 1300 | opt_directory, 1301 | (const char * const *) child_argv->pdata, 1302 | fds, 1303 | env, 1304 | spawn_flags) 1305 | : 1306 | g_variant_new ("(^ay^aay@a{uh}@a{ss}u@a{sv})", 1307 | opt_directory, 1308 | (const char * const *) child_argv->pdata, 1309 | fds, 1310 | env, 1311 | spawn_flags, 1312 | opts), 1313 | G_VARIANT_TYPE ("(u)"), 1314 | G_DBUS_CALL_FLAGS_NONE, 1315 | -1, 1316 | fd_list, 1317 | NULL, 1318 | NULL, &error); 1319 | 1320 | if (reply == NULL) 1321 | { 1322 | if (g_error_matches (error, G_DBUS_ERROR, G_DBUS_ERROR_INVALID_ARGS) && 1323 | opt_watch_bus) 1324 | { 1325 | g_debug ("Got an invalid argument error; trying again without --watch-bus"); 1326 | 1327 | opt_watch_bus = FALSE; 1328 | spawn_flags &= opt_host ? ~FLATPAK_HOST_COMMAND_FLAGS_WATCH_BUS : ~FLATPAK_SPAWN_FLAGS_WATCH_BUS; 1329 | g_clear_error (&error); 1330 | 1331 | goto retry; 1332 | } 1333 | 1334 | g_dbus_error_strip_remote_error (error); 1335 | g_printerr ("Portal call failed: %s\n", error->message); 1336 | if (opt_host && g_error_matches (error, G_DBUS_ERROR, G_DBUS_ERROR_SERVICE_UNKNOWN)) { 1337 | g_printerr ("Hint: --host only works when the Flatpak is allowed to talk to org.freedesktop.Flatpak\n"); 1338 | } 1339 | return 1; 1340 | } 1341 | 1342 | g_variant_get (reply, "(u)", &child_pid); 1343 | } 1344 | 1345 | g_debug ("child_pid: %d", child_pid); 1346 | 1347 | /* Release our reference to the fds, so that only the copy we sent over 1348 | * D-Bus remains open */ 1349 | g_clear_object (&fd_list); 1350 | 1351 | loop = g_main_loop_new (NULL, FALSE); 1352 | 1353 | g_signal_connect (session_bus, "closed", G_CALLBACK (session_bus_closed_cb), loop); 1354 | 1355 | g_main_loop_run (loop); 1356 | 1357 | if (signal_source != 0) 1358 | g_source_remove (signal_source); 1359 | 1360 | return 0; 1361 | } 1362 | -------------------------------------------------------------------------------- /src/meson.build: -------------------------------------------------------------------------------- 1 | flatpak_spawn = executable( 2 | 'flatpak-spawn', 3 | sources: 'flatpak-spawn.c', 4 | dependencies: [gio_unix, threads], 5 | c_args: ['-include', '@0@'.format(config_h)], 6 | install: true, 7 | ) 8 | 9 | xdg_email = executable( 10 | 'xdg-email', 11 | sources: 'xdg-email.c', 12 | dependencies: [gio_unix], 13 | c_args: ['-include', '@0@'.format(config_h)], 14 | install: true, 15 | ) 16 | 17 | xdg_open = executable( 18 | 'xdg-open', 19 | sources: 'xdg-open.c', 20 | dependencies: [gio_unix], 21 | c_args: ['-include', '@0@'.format(config_h)], 22 | install: true, 23 | ) 24 | -------------------------------------------------------------------------------- /src/xdg-email.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2017 Red Hat, Inc 3 | * 4 | * This program is free software; you can redistribute it and/or 5 | * modify it under the terms of the GNU Lesser General Public 6 | * License as published by the Free Software Foundation; either 7 | * version 2 of the License, or (at your option) any later version. 8 | * 9 | * This library is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | * Lesser General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Lesser General Public 15 | * License along with this library. If not, see . 16 | * 17 | * Authors: 18 | * Florian Müllner 19 | */ 20 | 21 | #include 22 | 23 | #include 24 | #include 25 | 26 | #include 27 | #include 28 | #include 29 | 30 | #include "backport-autoptr.h" 31 | 32 | #define PORTAL_BUS_NAME "org.freedesktop.portal.Desktop" 33 | #define PORTAL_OBJECT_PATH "/org/freedesktop/portal/desktop" 34 | #define PORTAL_IFACE_NAME "org.freedesktop.portal.Email" 35 | #define PORTAL_IFACE_NAME_OPENURI "org.freedesktop.portal.OpenURI" 36 | 37 | static char **addresses = NULL; 38 | static char **opt_cc = NULL; 39 | static char **opt_bcc = NULL; 40 | static gboolean show_help = FALSE; 41 | static gboolean show_version = FALSE; 42 | 43 | static gboolean use_utf8 = FALSE; 44 | static char *subject = NULL; 45 | static char *body = NULL; 46 | static char *attach = NULL; 47 | 48 | static GOptionEntry entries[] = { 49 | {"utf8", 0, 0, G_OPTION_ARG_NONE, &use_utf8, 50 | N_("Indicates that all command line options are in utf8"), NULL }, 51 | { "cc", 0, 0, G_OPTION_ARG_STRING_ARRAY, &opt_cc, 52 | N_("Specify a recipient to be copied on the e-mail"), N_("address")}, 53 | { "bcc", 0, 0, G_OPTION_ARG_STRING_ARRAY, &opt_bcc, 54 | N_("Specify a recipient to be blindly copied on the e-mail"), N_("address")}, 55 | { "subject", 0, 0, G_OPTION_ARG_STRING, &subject, 56 | N_("Specify a subject for the e-mail"), N_("text")}, 57 | { "body", 0, 0, G_OPTION_ARG_STRING, &body, 58 | N_("Specify a body for the e-mail"), N_("text")}, 59 | { "attach", 0, 0, G_OPTION_ARG_FILENAME, &attach, 60 | N_("Specify an attachment for the e-mail"), N_("file")}, 61 | 62 | /* Compat options with "real" xdg-open */ 63 | { "manual", 0, G_OPTION_FLAG_HIDDEN, G_OPTION_ARG_NONE, &show_help, NULL, NULL }, 64 | { "version", 0, 0, G_OPTION_ARG_NONE, &show_version, N_("Show program version"), NULL }, 65 | 66 | { G_OPTION_REMAINING, 0, 0, G_OPTION_ARG_FILENAME_ARRAY, &addresses, NULL, NULL }, 67 | { NULL, 0, 0, 0, NULL, NULL, NULL } 68 | }; 69 | 70 | int 71 | main (int argc, char *argv[]) 72 | { 73 | GOptionContext *context; 74 | GError *error = NULL; 75 | GDBusConnection *connection; 76 | GVariantBuilder opt_builder; 77 | GVariant *parameters; 78 | GUnixFDList *fd_list = NULL; 79 | g_autoptr(GVariant) ret = NULL; 80 | g_autoptr(GVariant) v = NULL; 81 | guint32 version = 0; 82 | g_autoptr(GPtrArray) to = NULL; 83 | g_autoptr(GPtrArray) cc = NULL; 84 | g_autoptr(GPtrArray) bcc = NULL; 85 | gsize i; 86 | const char *single_uri = NULL; 87 | 88 | context = g_option_context_new ("[ mailto-uri | address(es) ]"); 89 | 90 | g_option_context_add_main_entries (context, entries, NULL); 91 | g_option_context_parse (context, &argc, &argv, &error); 92 | 93 | if (error != NULL) 94 | { 95 | g_printerr ("Error parsing commandline options: %s\n", error->message); 96 | g_printerr ("\n"); 97 | g_printerr ("Try \"%s --help\" for more information.\n", g_get_prgname ()); 98 | 99 | g_error_free (error); 100 | return 1; 101 | } 102 | 103 | if (show_version) 104 | { 105 | g_print ("%s\n", PACKAGE_VERSION); 106 | 107 | return 0; 108 | } 109 | 110 | if (show_help || addresses == NULL || addresses[0] == NULL) 111 | { 112 | char *help = g_option_context_get_help (context, TRUE, NULL); 113 | g_print ("%s\n", help); 114 | 115 | g_free (help); 116 | return 0; 117 | } 118 | 119 | /* If there is only one argument and it is a mailto: URI, behave like 120 | * xdg-open instead, allowing the full generality of RFC 6068 mailto: 121 | * URLs */ 122 | if ((opt_cc == NULL || *opt_cc == NULL) && 123 | (opt_bcc == NULL || *opt_bcc == NULL) && 124 | subject == NULL && body == NULL && attach == NULL && 125 | addresses[1] == NULL && 126 | g_ascii_strncasecmp (addresses[0], "mailto:", strlen ("mailto:")) == 0) 127 | { 128 | single_uri = addresses[0]; 129 | } 130 | else 131 | { 132 | to = g_ptr_array_new_full (g_strv_length (addresses), g_free); 133 | cc = g_ptr_array_new_full (opt_cc == NULL ? 0 : g_strv_length (opt_cc), g_free); 134 | bcc = g_ptr_array_new_full (opt_bcc == NULL ? 0 : g_strv_length (opt_bcc), g_free); 135 | 136 | for (i = 0; opt_cc != NULL && opt_cc[i] != NULL; i++) 137 | g_ptr_array_add (cc, g_strdup (opt_cc[i])); 138 | 139 | for (i = 0; opt_bcc != NULL && opt_bcc[i] != NULL; i++) 140 | g_ptr_array_add (bcc, g_strdup (opt_bcc[i])); 141 | 142 | for (i = 0; addresses[i] != NULL; i++) 143 | { 144 | if (g_ascii_strncasecmp (addresses[i], "mailto:", 145 | strlen ("mailto:")) == 0) 146 | { 147 | g_autofree gchar *rest = g_strdup (addresses[i] + strlen ("mailto:")); 148 | char *token; 149 | char *question_mark = strchr (rest, '?'); 150 | char *saveptr = NULL; 151 | 152 | if (question_mark != NULL) 153 | *question_mark = '\0'; 154 | 155 | /* The part before any '?' is a comma-separated list of URI-escaped 156 | * email addresses, but may be empty */ 157 | if (rest[0] != '\0') 158 | { 159 | for (token = strtok_r (rest, ",", &saveptr); 160 | token != NULL; 161 | token = strtok_r (NULL, ",", &saveptr)) 162 | { 163 | g_autofree gchar *addr = g_uri_unescape_string (token, NULL); 164 | 165 | if (addr != NULL) 166 | g_ptr_array_add (to, g_steal_pointer (&addr)); 167 | else 168 | g_warning ("Invalid URI-escaped email address: %s", token); 169 | } 170 | } 171 | 172 | if (question_mark == NULL) 173 | continue; 174 | 175 | /* The part after '?' (if any) is an &-separated list of header 176 | * field/value pairs */ 177 | for (token = strtok_r (question_mark + 1, "&", &saveptr); 178 | token != NULL; 179 | token = strtok_r (NULL, "&", &saveptr)) 180 | { 181 | g_autofree gchar *value = NULL; 182 | char *equals = strchr (token, '='); 183 | const char *header; 184 | 185 | if (equals == NULL) 186 | { 187 | g_warning ("No '=' found in %s", token); 188 | continue; 189 | } 190 | 191 | *equals = '\0'; 192 | header = token; 193 | value = g_uri_unescape_string (equals + 1, NULL); 194 | 195 | if (value == NULL) 196 | { 197 | g_warning ("Invalid URI-escaped value for '%s': %s", 198 | header, value); 199 | continue; 200 | } 201 | 202 | if (g_ascii_strcasecmp (header, "to") == 0) 203 | { 204 | char *a, *saveptr2 = NULL; 205 | 206 | for (a = strtok_r (value, ",", &saveptr2); 207 | a != NULL; 208 | a = strtok_r (NULL, ",", &saveptr2)) 209 | g_ptr_array_add (to, g_strdup (a)); 210 | } 211 | else if (g_ascii_strcasecmp (header, "cc") == 0) 212 | { 213 | char *a, *saveptr2 = NULL; 214 | 215 | for (a = strtok_r (value, ",", &saveptr2); 216 | a != NULL; 217 | a = strtok_r (NULL, ",", &saveptr2)) 218 | g_ptr_array_add (cc, g_strdup (a)); 219 | } 220 | else if (g_ascii_strcasecmp (header, "bcc") == 0) 221 | { 222 | char *a, *saveptr2 = NULL; 223 | 224 | for (a = strtok_r (value, ",", &saveptr2); 225 | a != NULL; 226 | a = strtok_r (NULL, ",", &saveptr2)) 227 | g_ptr_array_add (bcc, g_strdup (a)); 228 | } 229 | else if (g_ascii_strcasecmp (header, "subject") == 0) 230 | { 231 | g_clear_pointer (&subject, g_free); 232 | subject = g_steal_pointer (&value); 233 | } 234 | else if (g_ascii_strcasecmp (header, "body") == 0) 235 | { 236 | g_clear_pointer (&body, g_free); 237 | body = g_steal_pointer (&value); 238 | } 239 | else 240 | { 241 | g_debug ("Ignoring unknown header field in mailto: URI: %s", 242 | header); 243 | } 244 | } 245 | } 246 | else 247 | { 248 | g_ptr_array_add (to, g_strdup (addresses[i])); 249 | } 250 | } 251 | } 252 | 253 | connection = g_bus_get_sync (G_BUS_TYPE_SESSION, NULL, &error); 254 | 255 | if (connection == NULL) 256 | { 257 | if (error) 258 | g_printerr ("Failed to connect to session bus: %s", error->message); 259 | else 260 | g_printerr ("Failed to connect to session bus"); 261 | 262 | g_clear_pointer (&error, g_error_free); 263 | return 3; 264 | } 265 | 266 | g_variant_builder_init (&opt_builder, G_VARIANT_TYPE_VARDICT); 267 | 268 | if (single_uri != NULL) 269 | { 270 | ret = g_dbus_connection_call_sync (connection, 271 | PORTAL_BUS_NAME, 272 | PORTAL_OBJECT_PATH, 273 | PORTAL_IFACE_NAME_OPENURI, 274 | "OpenURI", 275 | g_variant_new ("(ss@a{sv})", 276 | "", single_uri, 277 | g_variant_builder_end (&opt_builder)), 278 | NULL, 279 | G_DBUS_CALL_FLAGS_NONE, 280 | -1, 281 | NULL, 282 | &error); 283 | 284 | if (ret == NULL) 285 | { 286 | g_printerr ("Failed to call portal: %s\n", error->message); 287 | 288 | g_object_unref (connection); 289 | g_error_free (error); 290 | return 4; 291 | } 292 | 293 | g_object_unref (connection); 294 | return 0; 295 | } 296 | 297 | ret = g_dbus_connection_call_sync (connection, 298 | PORTAL_BUS_NAME, 299 | PORTAL_OBJECT_PATH, 300 | "org.freedesktop.DBus.Properties", 301 | "Get", 302 | g_variant_new ("(ss)", "org.freedesktop.portal.Email", "version"), 303 | G_VARIANT_TYPE ("(v)"), 304 | 0, 305 | G_MAXINT, 306 | NULL, 307 | NULL); 308 | if (ret != NULL) 309 | { 310 | g_variant_get (ret, "(v)", &v); 311 | 312 | if (g_variant_is_of_type (v, G_VARIANT_TYPE ("u"))) 313 | g_variant_get (v, "u", &version); 314 | else 315 | g_warning ("o.fd.portal.Email.version had unexpected type %s", 316 | g_variant_get_type_string (v)); 317 | } 318 | 319 | if (version >= 3) 320 | { 321 | g_variant_builder_add (&opt_builder, 322 | "{sv}", 323 | "addresses", 324 | g_variant_new_strv ((const char * const *) to->pdata, 325 | to->len)); 326 | 327 | if (cc->len > 0) 328 | g_variant_builder_add (&opt_builder, 329 | "{sv}", 330 | "cc", g_variant_new_strv ((const char * const *) cc->pdata, 331 | cc->len)); 332 | 333 | if (bcc->len > 0) 334 | g_variant_builder_add (&opt_builder, 335 | "{sv}", 336 | "bcc", g_variant_new_strv ((const char * const *) bcc->pdata, 337 | bcc->len)); 338 | } 339 | else 340 | { 341 | if (to->len == 0) 342 | { 343 | g_printerr ("xdg-email: No valid addresses found\n"); 344 | return 1; 345 | } 346 | 347 | g_variant_builder_add (&opt_builder, 348 | "{sv}", 349 | "address", g_variant_new_string (g_ptr_array_index (to, 0))); 350 | } 351 | 352 | if (subject != NULL) 353 | g_variant_builder_add (&opt_builder, 354 | "{sv}", 355 | "subject", g_variant_new_string (subject)); 356 | 357 | if (body != NULL) 358 | g_variant_builder_add (&opt_builder, 359 | "{sv}", 360 | "body", g_variant_new_string (body)); 361 | 362 | if (attach != NULL) 363 | { 364 | GFile *file = g_file_new_for_commandline_arg (attach); 365 | char *path; 366 | int fd; 367 | 368 | if (!g_file_is_native (file)) 369 | { 370 | g_printerr ("Only native files can be used as attachments"); 371 | g_object_unref (file); 372 | return 2; 373 | } 374 | 375 | path = g_file_get_path (file); 376 | fd = open (path, O_PATH | O_CLOEXEC); 377 | if (fd == -1) 378 | { 379 | g_printerr ("Failed to open '%s': %s", path, g_strerror (errno)); 380 | return 2; 381 | } 382 | 383 | fd_list = g_unix_fd_list_new_from_array (&fd, 1); 384 | fd = -1; 385 | 386 | g_variant_builder_add (&opt_builder, 387 | "{sv}", 388 | "attachment_fds", g_variant_new_parsed ("@ah [0]")); 389 | } 390 | 391 | parameters = g_variant_new ("(s@a{sv})", 392 | "", 393 | g_variant_builder_end (&opt_builder)); 394 | 395 | g_dbus_connection_call_with_unix_fd_list_sync (connection, 396 | PORTAL_BUS_NAME, 397 | PORTAL_OBJECT_PATH, 398 | PORTAL_IFACE_NAME, 399 | "ComposeEmail", 400 | parameters, 401 | NULL, 402 | G_DBUS_CALL_FLAGS_NONE, 403 | -1, 404 | fd_list, 405 | NULL, 406 | NULL, 407 | &error); 408 | 409 | if (error) 410 | { 411 | g_printerr ("Failed to call portal: %s\n", error->message); 412 | 413 | g_object_unref (connection); 414 | g_error_free (error); 415 | 416 | return 4; 417 | } 418 | 419 | g_object_unref (connection); 420 | 421 | return 0; 422 | } 423 | -------------------------------------------------------------------------------- /src/xdg-open.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2017 Red Hat, Inc 3 | * 4 | * This program is free software; you can redistribute it and/or 5 | * modify it under the terms of the GNU Lesser General Public 6 | * License as published by the Free Software Foundation; either 7 | * version 2 of the License, or (at your option) any later version. 8 | * 9 | * This library is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | * Lesser General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Lesser General Public 15 | * License along with this library. If not, see . 16 | * 17 | * Authors: 18 | * Florian Müllner 19 | */ 20 | 21 | #include 22 | 23 | #include 24 | #include 25 | #include 26 | #include 27 | #include 28 | 29 | #include "backport-autoptr.h" 30 | 31 | #define PORTAL_BUS_NAME "org.freedesktop.portal.Desktop" 32 | #define PORTAL_OBJECT_PATH "/org/freedesktop/portal/desktop" 33 | #define PORTAL_IFACE_NAME "org.freedesktop.portal.OpenURI" 34 | 35 | static char **uris = NULL; 36 | static gboolean show_help = FALSE; 37 | static gboolean show_version = FALSE; 38 | static gboolean ask = FALSE; 39 | 40 | static GOptionEntry entries[] = { 41 | /* Compat options with "real" xdg-open */ 42 | { "manual", 0, G_OPTION_FLAG_HIDDEN, G_OPTION_ARG_NONE, &show_help, NULL, NULL }, 43 | { "version", 0, 0, G_OPTION_ARG_NONE, &show_version, N_("Show program version"), NULL }, 44 | 45 | /* Portal-specific options */ 46 | { "ask", 0, 0, G_OPTION_ARG_NONE, &ask, N_("Ask the user to choose an app"), NULL }, 47 | 48 | { G_OPTION_REMAINING, 0, 0, G_OPTION_ARG_FILENAME_ARRAY, &uris, NULL, NULL }, 49 | { NULL, 0, 0, 0, NULL, NULL, NULL } 50 | }; 51 | 52 | int 53 | main (int argc, char *argv[]) 54 | { 55 | g_autoptr(GOptionContext) context = NULL; 56 | g_autoptr(GError) error = NULL; 57 | g_autoptr(GDBusConnection) connection = NULL; 58 | g_autoptr(GFile) file = NULL; 59 | g_autoptr(GVariant) reply = NULL; 60 | GVariantBuilder opt_builder; 61 | 62 | context = g_option_context_new ("{ file | URL }"); 63 | 64 | g_option_context_add_main_entries (context, entries, NULL); 65 | g_option_context_parse (context, &argc, &argv, &error); 66 | 67 | if (error != NULL) 68 | { 69 | g_printerr ("Error parsing commandline options: %s\n", error->message); 70 | g_printerr ("\n"); 71 | g_printerr ("Try \"%s --help\" for more information.\n", g_get_prgname ()); 72 | 73 | return 1; 74 | } 75 | 76 | if (show_version) 77 | { 78 | g_print ("%s\n", PACKAGE_VERSION); 79 | 80 | return 0; 81 | } 82 | 83 | if (show_help || uris == NULL || g_strv_length (uris) > 1) 84 | { 85 | g_autofree gchar *help = g_option_context_get_help (context, TRUE, NULL); 86 | g_print ("%s\n", help); 87 | 88 | return 0; 89 | } 90 | 91 | connection = g_bus_get_sync (G_BUS_TYPE_SESSION, NULL, &error); 92 | 93 | if (connection == NULL) 94 | { 95 | if (error) 96 | g_printerr ("Failed to connect to session bus: %s", error->message); 97 | else 98 | g_printerr ("Failed to connect to session bus"); 99 | 100 | return 3; 101 | } 102 | 103 | g_variant_builder_init (&opt_builder, G_VARIANT_TYPE_VARDICT); 104 | g_variant_builder_add (&opt_builder, "{sv}", "ask", g_variant_new_boolean (ask)); 105 | 106 | file = g_file_new_for_commandline_arg (uris[0]); 107 | if (g_file_is_native (file)) 108 | { 109 | g_autofree gchar *path = NULL; 110 | int fd; 111 | g_autoptr(GUnixFDList) fd_list = NULL; 112 | 113 | path = g_file_get_path (file); 114 | fd = open (path, O_RDONLY | O_CLOEXEC); 115 | if (fd == -1) 116 | { 117 | g_printerr ("Failed to open '%s': %s", path, g_strerror (errno)); 118 | g_variant_unref (g_variant_builder_end (&opt_builder)); 119 | return 5; 120 | } 121 | 122 | fd_list = g_unix_fd_list_new_from_array (&fd, 1); 123 | fd = -1; 124 | 125 | reply = g_dbus_connection_call_with_unix_fd_list_sync (connection, 126 | PORTAL_BUS_NAME, 127 | PORTAL_OBJECT_PATH, 128 | PORTAL_IFACE_NAME, 129 | "OpenFile", 130 | g_variant_new ("(sh@a{sv})", 131 | "", 0, 132 | g_variant_builder_end (&opt_builder)), 133 | NULL, 134 | G_DBUS_CALL_FLAGS_NONE, 135 | -1, 136 | fd_list, 137 | NULL, 138 | NULL, 139 | &error); 140 | } 141 | else 142 | { 143 | reply = g_dbus_connection_call_sync (connection, 144 | PORTAL_BUS_NAME, 145 | PORTAL_OBJECT_PATH, 146 | PORTAL_IFACE_NAME, 147 | "OpenURI", 148 | g_variant_new ("(ss@a{sv})", 149 | "", uris[0], 150 | g_variant_builder_end (&opt_builder)), 151 | NULL, 152 | G_DBUS_CALL_FLAGS_NONE, 153 | -1, 154 | NULL, 155 | &error); 156 | } 157 | 158 | if (error) 159 | { 160 | g_printerr ("Failed to call portal: %s\n", error->message); 161 | 162 | return 4; 163 | } 164 | 165 | return 0; 166 | } 167 | -------------------------------------------------------------------------------- /tests/common.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2018-2019 Collabora Ltd. 3 | * 4 | * This program is free software; you can redistribute it and/or 5 | * modify it under the terms of the GNU Lesser General Public 6 | * License as published by the Free Software Foundation; either 7 | * version 2.1 of the License, or (at your option) any later version. 8 | * 9 | * This library is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | * Lesser General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Lesser General Public 15 | * License along with this library. If not, see . 16 | */ 17 | 18 | #include "common.h" 19 | 20 | #include 21 | 22 | #include 23 | 24 | #include "backport-autoptr.h" 25 | 26 | void 27 | setup_dbus_daemon (GSubprocess **dbus_daemon, 28 | gchar **dbus_address) 29 | { 30 | g_autoptr(GSubprocessLauncher) launcher = NULL; 31 | g_autoptr(GError) error = NULL; 32 | GInputStream *address_pipe; 33 | gchar address_buffer[4096] = { 0 }; 34 | g_autofree gchar *escaped = NULL; 35 | char *newline; 36 | 37 | launcher = g_subprocess_launcher_new (G_SUBPROCESS_FLAGS_STDOUT_PIPE); 38 | *dbus_daemon = g_subprocess_launcher_spawn (launcher, &error, 39 | "dbus-daemon", 40 | "--session", 41 | "--print-address=1", 42 | "--nofork", 43 | NULL); 44 | g_assert_no_error (error); 45 | g_assert_nonnull (*dbus_daemon); 46 | 47 | address_pipe = g_subprocess_get_stdout_pipe (*dbus_daemon); 48 | 49 | /* Crash if it takes too long to get the address */ 50 | alarm (30); 51 | 52 | while (strchr (address_buffer, '\n') == NULL) 53 | { 54 | if (strlen (address_buffer) >= sizeof (address_buffer) - 1) 55 | g_error ("Read %" G_GSIZE_FORMAT " bytes from dbus-daemon with " 56 | "no newline", 57 | sizeof (address_buffer) - 1); 58 | 59 | g_input_stream_read (address_pipe, 60 | address_buffer + strlen (address_buffer), 61 | sizeof (address_buffer) - strlen (address_buffer), 62 | NULL, &error); 63 | g_assert_no_error (error); 64 | } 65 | 66 | /* Disable alarm */ 67 | alarm (0); 68 | 69 | newline = strchr (address_buffer, '\n'); 70 | g_assert_nonnull (newline); 71 | *newline = '\0'; 72 | *dbus_address = g_strdup (address_buffer); 73 | } 74 | 75 | void 76 | own_name_sync (GDBusConnection *conn, 77 | const char *name) 78 | { 79 | g_autoptr(GVariant) variant = NULL; 80 | g_autoptr(GError) error = NULL; 81 | guint32 result; 82 | 83 | /* We don't use g_bus_own_name() here because it's easier to be 84 | * synchronous */ 85 | variant = g_dbus_connection_call_sync (conn, 86 | DBUS_SERVICE_DBUS, 87 | DBUS_PATH_DBUS, 88 | DBUS_IFACE_DBUS, 89 | "RequestName", 90 | g_variant_new ("(su)", 91 | name, 92 | DBUS_NAME_FLAG_DO_NOT_QUEUE), 93 | G_VARIANT_TYPE ("(u)"), 94 | G_DBUS_CALL_FLAGS_NONE, -1, 95 | NULL, &error); 96 | g_assert_no_error (error); 97 | g_variant_get (variant, "(u)", &result); 98 | g_assert_cmpuint (result, ==, DBUS_REQUEST_NAME_REPLY_PRIMARY_OWNER); 99 | } 100 | 101 | /* A GAsyncReadyCallback that stores @res via a `GAsyncResult **`. */ 102 | void 103 | store_result_cb (G_GNUC_UNUSED GObject *source, 104 | GAsyncResult *res, 105 | gpointer data) 106 | { 107 | GAsyncResult **res_p = data; 108 | 109 | g_assert_nonnull (res_p); 110 | g_assert_null (*res_p); 111 | g_assert_true (G_IS_ASYNC_RESULT (res)); 112 | *res_p = g_object_ref (res); 113 | } 114 | -------------------------------------------------------------------------------- /tests/common.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2018-2019 Collabora Ltd. 3 | * 4 | * This program is free software; you can redistribute it and/or 5 | * modify it under the terms of the GNU Lesser General Public 6 | * License as published by the Free Software Foundation; either 7 | * version 2.1 of the License, or (at your option) any later version. 8 | * 9 | * This library is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | * Lesser General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Lesser General Public 15 | * License along with this library. If not, see . 16 | */ 17 | 18 | #pragma once 19 | 20 | #include 21 | 22 | /* GDBus interface info contains padding for future expansion, silence 23 | * warnings about this */ 24 | #ifdef __GNUC__ 25 | #pragma GCC diagnostic ignored "-Wmissing-field-initializers" 26 | #endif 27 | 28 | #define DBUS_SERVICE_DBUS "org.freedesktop.DBus" 29 | #define DBUS_PATH_DBUS "/org/freedesktop/DBus" 30 | #define DBUS_IFACE_DBUS DBUS_SERVICE_DBUS 31 | #define DBUS_NAME_FLAG_DO_NOT_QUEUE 4 32 | #define DBUS_REQUEST_NAME_REPLY_PRIMARY_OWNER 1 33 | 34 | void setup_dbus_daemon (GSubprocess **dbus_daemon, 35 | gchar **dbus_address); 36 | void own_name_sync (GDBusConnection *conn, 37 | const char *name); 38 | void store_result_cb (GObject *source, GAsyncResult *res, gpointer data); 39 | 40 | #ifndef g_assert_no_errno 41 | #define g_assert_no_errno(expr) \ 42 | g_assert_cmpstr ((expr) >= 0 ? NULL : g_strerror (errno), ==, NULL) 43 | #endif 44 | -------------------------------------------------------------------------------- /tests/meson.build: -------------------------------------------------------------------------------- 1 | test_env = environment() 2 | test_env.set('G_DEBUG', 'gc-friendly,fatal-criticals') 3 | test_env.set('G_TEST_SRCDIR', meson.current_source_dir()) 4 | test_env.set('G_TEST_BUILDDIR', meson.current_build_dir()) 5 | test_env.set('GIO_USE_VFS', 'local') 6 | test_env.set('FLATPAK_SPAWN', flatpak_spawn.full_path()) 7 | test_env.set('XDG_EMAIL', xdg_email.full_path()) 8 | test_env.set('XDG_OPEN', xdg_open.full_path()) 9 | test_env.set('MALLOC_CHECK_', '2') 10 | 11 | tests = [ 12 | 'test-email', 13 | 'test-open', 14 | 'test-spawn', 15 | ] 16 | 17 | test_timeout = 60 18 | 19 | installed_tests_template_tap = files('template-tap.test.in') 20 | 21 | foreach test_name : tests 22 | template = installed_tests_template_tap 23 | 24 | if installed_tests_enabled 25 | test_conf = configuration_data() 26 | test_conf.set('installed_tests_dir', installed_tests_execdir) 27 | test_conf.set('program', test_name) 28 | configure_file( 29 | input: template, 30 | output: test_name + '.test', 31 | install_dir: installed_tests_metadir, 32 | configuration: test_conf 33 | ) 34 | endif 35 | 36 | exe = executable(test_name, [test_name + '.c', 'common.c', 'common.h'], 37 | c_args: ['-include', '@0@'.format(config_h)], 38 | dependencies: [gio_unix], 39 | include_directories : [srcinc], 40 | install_dir: installed_tests_execdir, 41 | install: installed_tests_enabled, 42 | ) 43 | 44 | test(test_name, exe, env : test_env, timeout : test_timeout, 45 | suite : ['flatpak-xdg-utils'], args : ['--tap']) 46 | endforeach 47 | -------------------------------------------------------------------------------- /tests/template-tap.test.in: -------------------------------------------------------------------------------- 1 | [Test] 2 | Type=session 3 | Exec=@installed_tests_dir@/@program@ --tap 4 | Output=TAP 5 | -------------------------------------------------------------------------------- /tests/test-email.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2018-2019 Collabora Ltd. 3 | * 4 | * This program is free software; you can redistribute it and/or 5 | * modify it under the terms of the GNU Lesser General Public 6 | * License as published by the Free Software Foundation; either 7 | * version 2.1 of the License, or (at your option) any later version. 8 | * 9 | * This library is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | * Lesser General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Lesser General Public 15 | * License along with this library. If not, see . 16 | */ 17 | 18 | #include 19 | #include 20 | #include 21 | #include 22 | 23 | #include 24 | #include 25 | #include 26 | 27 | #include "backport-autoptr.h" 28 | #include "common.h" 29 | 30 | #define PORTAL_BUS_NAME "org.freedesktop.portal.Desktop" 31 | #define PORTAL_OBJECT_PATH "/org/freedesktop/portal/desktop" 32 | #define PORTAL_IFACE_NAME "org.freedesktop.portal.Email" 33 | #define PORTAL_IFACE_NAME_OPENURI "org.freedesktop.portal.OpenURI" 34 | 35 | typedef struct 36 | { 37 | guint32 iface_version; 38 | } Config; 39 | 40 | typedef struct 41 | { 42 | const Config *config; 43 | GSubprocess *dbus_daemon; 44 | gchar *dbus_address; 45 | GSubprocess *xdg_email; 46 | gchar *xdg_email_path; 47 | GDBusConnection *mock_conn; 48 | guint mock_object; 49 | guint mock_openuri_object; 50 | GQueue invocations; 51 | } Fixture; 52 | 53 | static void 54 | mock_method_call (GDBusConnection *conn G_GNUC_UNUSED, 55 | const gchar *sender G_GNUC_UNUSED, 56 | const gchar *object_path G_GNUC_UNUSED, 57 | const gchar *interface_name, 58 | const gchar *method_name, 59 | GVariant *parameters G_GNUC_UNUSED, 60 | GDBusMethodInvocation *invocation, 61 | gpointer user_data) 62 | { 63 | Fixture *f = user_data; 64 | g_autofree gchar *params = NULL; 65 | 66 | params = g_variant_print (parameters, TRUE); 67 | 68 | g_test_message ("Method called: %s.%s%s", interface_name, method_name, 69 | params); 70 | 71 | if (g_strcmp0 (interface_name, "org.freedesktop.portal.Email") == 0 && 72 | g_strcmp0 (method_name, "ComposeEmail") == 0) 73 | { 74 | /* OK */ 75 | } 76 | else if (g_strcmp0 (interface_name, "org.freedesktop.portal.OpenURI") == 0 && 77 | g_strcmp0 (method_name, "OpenURI") == 0) 78 | { 79 | /* OK */ 80 | } 81 | else 82 | { 83 | g_dbus_method_invocation_return_error (invocation, 84 | G_DBUS_ERROR, 85 | G_DBUS_ERROR_UNKNOWN_METHOD, 86 | "Not ComposeEmail or OpenURI"); 87 | return; 88 | } 89 | 90 | g_queue_push_tail (&f->invocations, g_object_ref (invocation)); 91 | g_dbus_method_invocation_return_value (invocation, 92 | g_variant_new ("(o)", "/foo")); 93 | } 94 | 95 | GVariant * 96 | mock_getter (GDBusConnection *conn G_GNUC_UNUSED, 97 | const gchar *sender G_GNUC_UNUSED, 98 | const gchar *object_path G_GNUC_UNUSED, 99 | const gchar *interface_name, 100 | const gchar *property_name, 101 | GError **error, 102 | gpointer user_data) 103 | { 104 | Fixture *f = user_data; 105 | 106 | g_test_message ("Get property: %s.%s", interface_name, property_name); 107 | 108 | if (g_strcmp0 (interface_name, "org.freedesktop.portal.Email") == 0 && 109 | g_strcmp0 (property_name, "version") == 0) 110 | { 111 | return g_variant_new_uint32 (f->config->iface_version); 112 | } 113 | else 114 | { 115 | #if GLIB_CHECK_VERSION(2, 42, 0) 116 | g_set_error (error, G_DBUS_ERROR, G_DBUS_ERROR_UNKNOWN_PROPERTY, 117 | "Unknown property"); 118 | #else 119 | /* This is the closest we can do in older versions */ 120 | g_set_error (error, G_DBUS_ERROR, G_DBUS_ERROR_UNKNOWN_METHOD, 121 | "Unknown property"); 122 | #endif 123 | return NULL; 124 | } 125 | } 126 | 127 | static GDBusArgInfo arg_parent_window = 128 | { 129 | -1, 130 | "parent_window", 131 | "s", 132 | NULL /* annotations */ 133 | }; 134 | 135 | static GDBusArgInfo arg_options = 136 | { 137 | -1, 138 | "options", 139 | "a{sv}", 140 | NULL /* annotations */ 141 | }; 142 | 143 | static GDBusArgInfo arg_out_handle = 144 | { 145 | -1, 146 | "handle", 147 | "o", 148 | NULL /* annotations */ 149 | }; 150 | 151 | static GDBusArgInfo *in_args[] = 152 | { 153 | &arg_parent_window, 154 | &arg_options, 155 | NULL 156 | }; 157 | 158 | static GDBusArgInfo *out_args[] = 159 | { 160 | &arg_out_handle, 161 | NULL 162 | }; 163 | 164 | static GDBusMethodInfo compose_email_info = 165 | { 166 | -1, 167 | "ComposeEmail", 168 | in_args, 169 | out_args, 170 | NULL /* annotations */ 171 | }; 172 | 173 | static GDBusMethodInfo *method_info[] = 174 | { 175 | &compose_email_info, 176 | NULL 177 | }; 178 | 179 | static GDBusArgInfo arg_uri = 180 | { 181 | -1, 182 | "uri", 183 | "s", 184 | NULL /* annotations */ 185 | }; 186 | 187 | static GDBusArgInfo *openuri_in_args[] = 188 | { 189 | &arg_parent_window, 190 | &arg_uri, 191 | &arg_options, 192 | NULL 193 | }; 194 | 195 | static GDBusMethodInfo openuri_info = 196 | { 197 | -1, 198 | "OpenURI", 199 | openuri_in_args, 200 | out_args, 201 | NULL /* annotations */ 202 | }; 203 | 204 | static GDBusMethodInfo *openuri_method_info[] = 205 | { 206 | &openuri_info, 207 | NULL 208 | }; 209 | 210 | static GDBusPropertyInfo prop_version_info = 211 | { 212 | .ref_count = -1, 213 | .name = "version", 214 | .signature = "u", 215 | .flags = G_DBUS_PROPERTY_INFO_FLAGS_READABLE, 216 | .annotations = NULL 217 | }; 218 | 219 | static GDBusPropertyInfo *props_info[] = 220 | { 221 | &prop_version_info, 222 | NULL 223 | }; 224 | 225 | static GDBusInterfaceInfo iface_info = 226 | { 227 | -1, 228 | PORTAL_IFACE_NAME, 229 | method_info, 230 | NULL, /* signals */ 231 | props_info, 232 | NULL /* annotations */ 233 | }; 234 | 235 | static GDBusInterfaceInfo v0_iface_info = 236 | { 237 | .ref_count = -1, 238 | .name = PORTAL_IFACE_NAME, 239 | .methods = method_info, 240 | .signals = NULL, 241 | .properties = NULL, 242 | .annotations = NULL 243 | }; 244 | 245 | static GDBusInterfaceInfo openuri_iface_info = 246 | { 247 | .ref_count = -1, 248 | .name = PORTAL_IFACE_NAME_OPENURI, 249 | .methods = openuri_method_info, 250 | .signals = NULL, 251 | .properties = NULL, 252 | .annotations = NULL 253 | }; 254 | 255 | static const GDBusInterfaceVTable vtable = 256 | { 257 | mock_method_call, 258 | mock_getter, 259 | NULL /* set */ 260 | }; 261 | 262 | static void 263 | setup (Fixture *f, 264 | gconstpointer context) 265 | { 266 | g_autoptr(GError) error = NULL; 267 | GDBusInterfaceInfo *chosen_iface_info; 268 | 269 | f->config = context; 270 | 271 | g_queue_init (&f->invocations); 272 | 273 | setup_dbus_daemon (&f->dbus_daemon, &f->dbus_address); 274 | 275 | f->xdg_email_path = g_strdup (g_getenv ("XDG_EMAIL")); 276 | 277 | if (f->xdg_email_path == NULL) 278 | f->xdg_email_path = g_strdup (BINDIR "/xdg-email"); 279 | 280 | f->mock_conn = g_dbus_connection_new_for_address_sync (f->dbus_address, 281 | (G_DBUS_CONNECTION_FLAGS_AUTHENTICATION_CLIENT | 282 | G_DBUS_CONNECTION_FLAGS_MESSAGE_BUS_CONNECTION), 283 | NULL, NULL, &error); 284 | g_assert_no_error (error); 285 | g_assert_nonnull (f->mock_conn); 286 | 287 | if (f->config->iface_version == 0) 288 | chosen_iface_info = &v0_iface_info; 289 | else 290 | chosen_iface_info = &iface_info; 291 | 292 | f->mock_object = g_dbus_connection_register_object (f->mock_conn, 293 | PORTAL_OBJECT_PATH, 294 | chosen_iface_info, 295 | &vtable, 296 | f, 297 | NULL, 298 | &error); 299 | g_assert_no_error (error); 300 | g_assert_cmpuint (f->mock_object, !=, 0); 301 | 302 | f->mock_openuri_object = g_dbus_connection_register_object (f->mock_conn, 303 | PORTAL_OBJECT_PATH, 304 | &openuri_iface_info, 305 | &vtable, 306 | f, 307 | NULL, 308 | &error); 309 | g_assert_no_error (error); 310 | g_assert_cmpuint (f->mock_openuri_object, !=, 0); 311 | 312 | own_name_sync (f->mock_conn, PORTAL_BUS_NAME); 313 | } 314 | 315 | static void 316 | test_help (Fixture *f, 317 | gconstpointer context G_GNUC_UNUSED) 318 | { 319 | g_autoptr(GSubprocessLauncher) launcher = NULL; 320 | g_autofree gchar *stdout_buf; 321 | g_autofree gchar *stderr_buf; 322 | g_autoptr(GError) error = NULL; 323 | 324 | launcher = g_subprocess_launcher_new (G_SUBPROCESS_FLAGS_STDOUT_PIPE | 325 | G_SUBPROCESS_FLAGS_STDERR_PIPE); 326 | g_subprocess_launcher_setenv (launcher, 327 | "DBUS_SESSION_BUS_ADDRESS", 328 | f->dbus_address, 329 | TRUE); 330 | 331 | f->xdg_email = g_subprocess_launcher_spawn (launcher, &error, 332 | f->xdg_email_path, 333 | "--help", 334 | NULL); 335 | g_assert_no_error (error); 336 | g_assert_nonnull (f->xdg_email); 337 | 338 | g_subprocess_communicate_utf8 (f->xdg_email, NULL, NULL, &stdout_buf, 339 | &stderr_buf, &error); 340 | g_assert_cmpstr (stderr_buf, ==, ""); 341 | g_assert_nonnull (stdout_buf); 342 | g_test_message ("xdg-open --help: %s", stdout_buf); 343 | g_assert_true (strstr (stdout_buf, "--version") != NULL); 344 | 345 | g_subprocess_wait_check (f->xdg_email, NULL, &error); 346 | g_assert_no_error (error); 347 | } 348 | 349 | static void 350 | test_minimal (Fixture *f, 351 | gconstpointer context G_GNUC_UNUSED) 352 | { 353 | g_autoptr(GSubprocessLauncher) launcher = NULL; 354 | g_autoptr(GError) error = NULL; 355 | g_autoptr(GDBusMethodInvocation) invocation = NULL; 356 | g_autoptr(GVariant) asv = NULL; 357 | g_autoptr(GVariantDict) dict = NULL; 358 | GVariant *parameters; 359 | const gchar *window; 360 | const gchar **addresses; 361 | const gchar *address; 362 | 363 | launcher = g_subprocess_launcher_new (G_SUBPROCESS_FLAGS_STDOUT_PIPE); 364 | g_subprocess_launcher_setenv (launcher, 365 | "DBUS_SESSION_BUS_ADDRESS", 366 | f->dbus_address, 367 | TRUE); 368 | 369 | f->xdg_email = g_subprocess_launcher_spawn (launcher, &error, 370 | f->xdg_email_path, 371 | "me@example.com", 372 | NULL); 373 | g_assert_no_error (error); 374 | g_assert_nonnull (f->xdg_email); 375 | 376 | while (g_queue_get_length (&f->invocations) < 1) 377 | g_main_context_iteration (NULL, TRUE); 378 | 379 | g_subprocess_wait_check (f->xdg_email, NULL, &error); 380 | g_assert_no_error (error); 381 | 382 | g_assert_cmpuint (g_queue_get_length (&f->invocations), ==, 1); 383 | invocation = g_queue_pop_head (&f->invocations); 384 | 385 | g_assert_cmpstr (g_dbus_method_invocation_get_interface_name (invocation), 386 | ==, PORTAL_IFACE_NAME); 387 | g_assert_cmpstr (g_dbus_method_invocation_get_method_name (invocation), 388 | ==, "ComposeEmail"); 389 | 390 | parameters = g_dbus_method_invocation_get_parameters (invocation); 391 | g_assert_cmpstr (g_variant_get_type_string (parameters), ==, "(sa{sv})"); 392 | g_variant_get (parameters, "(&s@a{sv})", 393 | &window, &asv); 394 | g_assert_cmpstr (window, ==, ""); 395 | 396 | dict = g_variant_dict_new (asv); 397 | 398 | if (f->config->iface_version >= 3) 399 | { 400 | g_assert_true (g_variant_dict_lookup (dict, "addresses", "^a&s", &addresses)); 401 | g_assert_cmpstr (addresses[0], ==, "me@example.com"); 402 | g_assert_cmpstr (addresses[1], ==, NULL); 403 | g_free (addresses); 404 | } 405 | else 406 | { 407 | g_assert_true (g_variant_dict_lookup (dict, "address", "&s", &address)); 408 | g_assert_cmpstr (address, ==, "me@example.com"); 409 | } 410 | 411 | g_assert_false (g_variant_dict_contains (dict, "subject")); 412 | g_assert_false (g_variant_dict_contains (dict, "body")); 413 | g_assert_false (g_variant_dict_contains (dict, "attachments")); 414 | } 415 | 416 | static void 417 | test_maximal (Fixture *f, 418 | gconstpointer context G_GNUC_UNUSED) 419 | { 420 | g_autoptr(GSubprocessLauncher) launcher = NULL; 421 | g_autoptr(GError) error = NULL; 422 | g_autoptr(GDBusMethodInvocation) invocation = NULL; 423 | g_autoptr(GVariant) asv = NULL; 424 | g_autoptr(GVariantDict) dict = NULL; 425 | g_autoptr(GVariant) attachments = NULL; 426 | GVariant *parameters; 427 | const gchar *window; 428 | const gchar **addresses; 429 | const gchar *address; 430 | const gchar *subject; 431 | const gchar *body; 432 | 433 | launcher = g_subprocess_launcher_new (G_SUBPROCESS_FLAGS_STDOUT_PIPE); 434 | g_subprocess_launcher_setenv (launcher, 435 | "DBUS_SESSION_BUS_ADDRESS", 436 | f->dbus_address, 437 | TRUE); 438 | 439 | f->xdg_email = g_subprocess_launcher_spawn (launcher, &error, 440 | f->xdg_email_path, 441 | "--subject", "Make Money Fast", 442 | "--body", "Your spam here", 443 | "--attach", "/dev/null", 444 | "--cc", "us@example.com", 445 | "--cc", "them@example.com", 446 | "--bcc", "hidden@example.com", 447 | "--bcc", "secret@example.com", 448 | "me@example.com", 449 | "you@example.com", 450 | NULL); 451 | g_assert_no_error (error); 452 | g_assert_nonnull (f->xdg_email); 453 | 454 | while (g_queue_get_length (&f->invocations) < 1) 455 | g_main_context_iteration (NULL, TRUE); 456 | 457 | g_subprocess_wait_check (f->xdg_email, NULL, &error); 458 | g_assert_no_error (error); 459 | 460 | g_assert_cmpuint (g_queue_get_length (&f->invocations), ==, 1); 461 | invocation = g_queue_pop_head (&f->invocations); 462 | 463 | g_assert_cmpstr (g_dbus_method_invocation_get_interface_name (invocation), 464 | ==, PORTAL_IFACE_NAME); 465 | g_assert_cmpstr (g_dbus_method_invocation_get_method_name (invocation), 466 | ==, "ComposeEmail"); 467 | 468 | parameters = g_dbus_method_invocation_get_parameters (invocation); 469 | g_assert_cmpstr (g_variant_get_type_string (parameters), ==, "(sa{sv})"); 470 | g_variant_get (parameters, "(&s@a{sv})", 471 | &window, &asv); 472 | g_assert_cmpstr (window, ==, ""); 473 | 474 | dict = g_variant_dict_new (asv); 475 | 476 | if (f->config->iface_version >= 3) 477 | { 478 | g_assert_true (g_variant_dict_lookup (dict, "addresses", "^a&s", &addresses)); 479 | g_assert_cmpstr (addresses[0], ==, "me@example.com"); 480 | g_assert_cmpstr (addresses[1], ==, "you@example.com"); 481 | g_assert_cmpstr (addresses[2], ==, NULL); 482 | g_free (addresses); 483 | 484 | g_assert_true (g_variant_dict_lookup (dict, "cc", "^a&s", &addresses)); 485 | g_assert_cmpstr (addresses[0], ==, "us@example.com"); 486 | g_assert_cmpstr (addresses[1], ==, "them@example.com"); 487 | g_assert_cmpstr (addresses[2], ==, NULL); 488 | g_free (addresses); 489 | 490 | g_assert_true (g_variant_dict_lookup (dict, "bcc", "^a&s", &addresses)); 491 | g_assert_cmpstr (addresses[0], ==, "hidden@example.com"); 492 | g_assert_cmpstr (addresses[1], ==, "secret@example.com"); 493 | g_assert_cmpstr (addresses[2], ==, NULL); 494 | g_free (addresses); 495 | } 496 | else 497 | { 498 | /* all addresses except the first are ignored */ 499 | g_assert_true (g_variant_dict_lookup (dict, "address", "&s", &address)); 500 | g_assert_cmpstr (address, ==, "me@example.com"); 501 | } 502 | 503 | g_assert_true (g_variant_dict_lookup (dict, "subject", "&s", &subject)); 504 | g_assert_cmpstr (subject, ==, "Make Money Fast"); 505 | g_assert_true (g_variant_dict_lookup (dict, "body", "&s", &body)); 506 | g_assert_cmpstr (body, ==, "Your spam here"); 507 | /* TODO: Also test that the attachment went through correctly (this 508 | * doesn't seem to work at the moment) */ 509 | } 510 | 511 | static void 512 | test_mailto_none (Fixture *f, 513 | gconstpointer context G_GNUC_UNUSED) 514 | { 515 | g_autoptr(GSubprocessLauncher) launcher = NULL; 516 | g_autofree gchar *stdout_buf; 517 | g_autofree gchar *stderr_buf; 518 | g_autoptr(GError) error = NULL; 519 | 520 | launcher = g_subprocess_launcher_new (G_SUBPROCESS_FLAGS_STDOUT_PIPE | 521 | G_SUBPROCESS_FLAGS_STDERR_PIPE); 522 | g_subprocess_launcher_setenv (launcher, 523 | "DBUS_SESSION_BUS_ADDRESS", 524 | f->dbus_address, 525 | TRUE); 526 | 527 | f->xdg_email = g_subprocess_launcher_spawn (launcher, &error, 528 | f->xdg_email_path, 529 | "mailto:?cc=one@example.com&bcc=two@example.com", 530 | "mailto:?none-here-either=true", 531 | NULL); 532 | g_assert_no_error (error); 533 | g_assert_nonnull (f->xdg_email); 534 | 535 | g_subprocess_communicate_utf8 (f->xdg_email, NULL, NULL, &stdout_buf, 536 | &stderr_buf, &error); 537 | g_test_message ("%s", stderr_buf); 538 | g_assert_nonnull (strstr (stderr_buf, "No valid addresses found")); 539 | 540 | g_subprocess_wait_check (f->xdg_email, NULL, &error); 541 | g_assert_error (error, G_SPAWN_EXIT_ERROR, 1); 542 | } 543 | 544 | static void 545 | test_mailto_single (Fixture *f, 546 | gconstpointer context G_GNUC_UNUSED) 547 | { 548 | g_autoptr(GSubprocessLauncher) launcher = NULL; 549 | g_autoptr(GError) error = NULL; 550 | g_autoptr(GDBusMethodInvocation) invocation = NULL; 551 | g_autoptr(GVariant) asv = NULL; 552 | GVariant *parameters; 553 | const gchar *window; 554 | const gchar *uri; 555 | 556 | launcher = g_subprocess_launcher_new (G_SUBPROCESS_FLAGS_STDOUT_PIPE); 557 | g_subprocess_launcher_setenv (launcher, 558 | "DBUS_SESSION_BUS_ADDRESS", 559 | f->dbus_address, 560 | TRUE); 561 | 562 | f->xdg_email = g_subprocess_launcher_spawn (launcher, &error, 563 | f->xdg_email_path, 564 | /* Deliberarely not RFC 6068 565 | * compliant, to check that 566 | * we pass it through without 567 | * parsing or understanding it */ 568 | "MailTo:?you-are-not-expected-to-understand-this", 569 | NULL); 570 | g_assert_no_error (error); 571 | g_assert_nonnull (f->xdg_email); 572 | 573 | while (g_queue_get_length (&f->invocations) < 1) 574 | g_main_context_iteration (NULL, TRUE); 575 | 576 | g_subprocess_wait_check (f->xdg_email, NULL, &error); 577 | g_assert_no_error (error); 578 | 579 | g_assert_cmpuint (g_queue_get_length (&f->invocations), ==, 1); 580 | invocation = g_queue_pop_head (&f->invocations); 581 | 582 | g_assert_cmpstr (g_dbus_method_invocation_get_interface_name (invocation), 583 | ==, PORTAL_IFACE_NAME_OPENURI); 584 | g_assert_cmpstr (g_dbus_method_invocation_get_method_name (invocation), 585 | ==, "OpenURI"); 586 | 587 | parameters = g_dbus_method_invocation_get_parameters (invocation); 588 | g_assert_cmpstr (g_variant_get_type_string (parameters), ==, "(ssa{sv})"); 589 | g_variant_get (parameters, "(&s&s@a{sv})", 590 | &window, &uri, &asv); 591 | g_assert_cmpstr (window, ==, ""); 592 | g_assert_cmpstr (uri, ==, "MailTo:?you-are-not-expected-to-understand-this"); 593 | } 594 | 595 | static void 596 | test_mailto_multiple (Fixture *f, 597 | gconstpointer context G_GNUC_UNUSED) 598 | { 599 | g_autoptr(GSubprocessLauncher) launcher = NULL; 600 | g_autoptr(GError) error = NULL; 601 | g_autoptr(GDBusMethodInvocation) invocation = NULL; 602 | g_autoptr(GVariant) asv = NULL; 603 | g_autoptr(GVariantDict) dict = NULL; 604 | GVariant *parameters; 605 | const gchar *window; 606 | const gchar **addresses; 607 | const gchar *address; 608 | 609 | launcher = g_subprocess_launcher_new (G_SUBPROCESS_FLAGS_STDOUT_PIPE); 610 | g_subprocess_launcher_setenv (launcher, 611 | "DBUS_SESSION_BUS_ADDRESS", 612 | f->dbus_address, 613 | TRUE); 614 | 615 | f->xdg_email = g_subprocess_launcher_spawn (launcher, &error, 616 | f->xdg_email_path, 617 | "mailto:me@example.com", 618 | "mailto:you@example.com", 619 | NULL); 620 | g_assert_no_error (error); 621 | g_assert_nonnull (f->xdg_email); 622 | 623 | while (g_queue_get_length (&f->invocations) < 1) 624 | g_main_context_iteration (NULL, TRUE); 625 | 626 | g_subprocess_wait_check (f->xdg_email, NULL, &error); 627 | g_assert_no_error (error); 628 | 629 | g_assert_cmpuint (g_queue_get_length (&f->invocations), ==, 1); 630 | invocation = g_queue_pop_head (&f->invocations); 631 | 632 | g_assert_cmpstr (g_dbus_method_invocation_get_interface_name (invocation), 633 | ==, PORTAL_IFACE_NAME); 634 | g_assert_cmpstr (g_dbus_method_invocation_get_method_name (invocation), 635 | ==, "ComposeEmail"); 636 | 637 | parameters = g_dbus_method_invocation_get_parameters (invocation); 638 | g_assert_cmpstr (g_variant_get_type_string (parameters), ==, "(sa{sv})"); 639 | g_variant_get (parameters, "(&s@a{sv})", 640 | &window, &asv); 641 | g_assert_cmpstr (window, ==, ""); 642 | 643 | dict = g_variant_dict_new (asv); 644 | 645 | if (f->config->iface_version >= 3) 646 | { 647 | g_assert_true (g_variant_dict_lookup (dict, "addresses", "^a&s", &addresses)); 648 | g_assert_cmpstr (addresses[0], ==, "me@example.com"); 649 | g_assert_cmpstr (addresses[1], ==, "you@example.com"); 650 | g_assert_cmpstr (addresses[2], ==, NULL); 651 | g_free (addresses); 652 | } 653 | else 654 | { 655 | g_assert_true (g_variant_dict_lookup (dict, "address", "&s", &address)); 656 | g_assert_cmpstr (address, ==, "me@example.com"); 657 | } 658 | 659 | g_assert_false (g_variant_dict_contains (dict, "subject")); 660 | g_assert_false (g_variant_dict_contains (dict, "body")); 661 | g_assert_false (g_variant_dict_contains (dict, "attachments")); 662 | } 663 | 664 | static void 665 | test_mailto_complex (Fixture *f, 666 | gconstpointer context G_GNUC_UNUSED) 667 | { 668 | g_autoptr(GSubprocessLauncher) launcher = NULL; 669 | g_autoptr(GError) error = NULL; 670 | g_autoptr(GDBusMethodInvocation) invocation = NULL; 671 | g_autoptr(GVariant) asv = NULL; 672 | g_autoptr(GVariantDict) dict = NULL; 673 | g_autoptr(GVariant) attachments = NULL; 674 | GVariant *parameters; 675 | const gchar *window; 676 | const gchar **addresses; 677 | const gchar *address; 678 | const gchar *subject; 679 | const gchar *body; 680 | 681 | launcher = g_subprocess_launcher_new (G_SUBPROCESS_FLAGS_STDOUT_PIPE); 682 | g_subprocess_launcher_setenv (launcher, 683 | "DBUS_SESSION_BUS_ADDRESS", 684 | f->dbus_address, 685 | TRUE); 686 | 687 | f->xdg_email = g_subprocess_launcher_spawn (launcher, &error, 688 | f->xdg_email_path, 689 | "mailto:nobody@example.com", 690 | ( 691 | "mailto:" 692 | "me@example.com" 693 | "," 694 | "you@example.com" 695 | "?" 696 | "subject=Make%20Money%20Fast" 697 | "&" 698 | "body=Your%20spam%20here" 699 | "&" 700 | "cc=" 701 | "us@example.com" 702 | "," 703 | "them@example.com" 704 | "&" 705 | "Bcc=" 706 | "hidden@example.com" 707 | "," 708 | "secret@example.com" 709 | "&" 710 | "Precedence=bulk" 711 | "&" 712 | "X-Mailer=xdg-email" 713 | ), 714 | NULL); 715 | g_assert_no_error (error); 716 | g_assert_nonnull (f->xdg_email); 717 | 718 | while (g_queue_get_length (&f->invocations) < 1) 719 | g_main_context_iteration (NULL, TRUE); 720 | 721 | g_subprocess_wait_check (f->xdg_email, NULL, &error); 722 | g_assert_no_error (error); 723 | 724 | g_assert_cmpuint (g_queue_get_length (&f->invocations), ==, 1); 725 | invocation = g_queue_pop_head (&f->invocations); 726 | 727 | g_assert_cmpstr (g_dbus_method_invocation_get_interface_name (invocation), 728 | ==, PORTAL_IFACE_NAME); 729 | g_assert_cmpstr (g_dbus_method_invocation_get_method_name (invocation), 730 | ==, "ComposeEmail"); 731 | 732 | parameters = g_dbus_method_invocation_get_parameters (invocation); 733 | g_assert_cmpstr (g_variant_get_type_string (parameters), ==, "(sa{sv})"); 734 | g_variant_get (parameters, "(&s@a{sv})", 735 | &window, &asv); 736 | g_assert_cmpstr (window, ==, ""); 737 | 738 | dict = g_variant_dict_new (asv); 739 | 740 | if (f->config->iface_version >= 3) 741 | { 742 | g_assert_true (g_variant_dict_lookup (dict, "addresses", "^a&s", &addresses)); 743 | g_assert_cmpstr (addresses[0], ==, "nobody@example.com"); 744 | g_assert_cmpstr (addresses[1], ==, "me@example.com"); 745 | g_assert_cmpstr (addresses[2], ==, "you@example.com"); 746 | g_assert_cmpstr (addresses[3], ==, NULL); 747 | g_free (addresses); 748 | 749 | g_assert_true (g_variant_dict_lookup (dict, "cc", "^a&s", &addresses)); 750 | g_assert_cmpstr (addresses[0], ==, "us@example.com"); 751 | g_assert_cmpstr (addresses[1], ==, "them@example.com"); 752 | g_assert_cmpstr (addresses[2], ==, NULL); 753 | g_free (addresses); 754 | 755 | g_assert_true (g_variant_dict_lookup (dict, "bcc", "^a&s", &addresses)); 756 | g_assert_cmpstr (addresses[0], ==, "hidden@example.com"); 757 | g_assert_cmpstr (addresses[1], ==, "secret@example.com"); 758 | g_assert_cmpstr (addresses[2], ==, NULL); 759 | g_free (addresses); 760 | } 761 | else 762 | { 763 | /* all addresses except the first are ignored */ 764 | g_assert_true (g_variant_dict_lookup (dict, "address", "&s", &address)); 765 | g_assert_cmpstr (address, ==, "nobody@example.com"); 766 | } 767 | 768 | g_assert_true (g_variant_dict_lookup (dict, "subject", "&s", &subject)); 769 | g_assert_cmpstr (subject, ==, "Make Money Fast"); 770 | g_assert_true (g_variant_dict_lookup (dict, "body", "&s", &body)); 771 | g_assert_cmpstr (body, ==, "Your spam here"); 772 | } 773 | 774 | static void 775 | test_mailto_combined (Fixture *f, 776 | gconstpointer context G_GNUC_UNUSED) 777 | { 778 | g_autoptr(GSubprocessLauncher) launcher = NULL; 779 | g_autoptr(GError) error = NULL; 780 | g_autoptr(GDBusMethodInvocation) invocation = NULL; 781 | g_autoptr(GVariant) asv = NULL; 782 | g_autoptr(GVariantDict) dict = NULL; 783 | g_autoptr(GVariant) attachments = NULL; 784 | GVariant *parameters; 785 | const gchar *window; 786 | const gchar **addresses; 787 | const gchar *address; 788 | const gchar *subject; 789 | const gchar *body; 790 | 791 | launcher = g_subprocess_launcher_new (G_SUBPROCESS_FLAGS_STDOUT_PIPE); 792 | g_subprocess_launcher_setenv (launcher, 793 | "DBUS_SESSION_BUS_ADDRESS", 794 | f->dbus_address, 795 | TRUE); 796 | 797 | f->xdg_email = g_subprocess_launcher_spawn (launcher, &error, 798 | f->xdg_email_path, 799 | "--cc", "us@example.com", 800 | "--bcc", "hidden@example.com", 801 | "--subject", "ignored", 802 | "--body", "ignored", 803 | "me@example.com", 804 | ( 805 | "mailto:" 806 | "you@example.com" 807 | "?" 808 | "Precedence=bulk" 809 | "&" 810 | "X-Mailer=xdg-email" 811 | "&" 812 | "subject=Make%20Money%20Fast" 813 | "&" 814 | "body=Your%20spam%20here" 815 | "&" 816 | "cc=" 817 | "them@example.com" 818 | "&" 819 | "Bcc=" 820 | "secret@example.com" 821 | ), 822 | NULL); 823 | g_assert_no_error (error); 824 | g_assert_nonnull (f->xdg_email); 825 | 826 | while (g_queue_get_length (&f->invocations) < 1) 827 | g_main_context_iteration (NULL, TRUE); 828 | 829 | g_subprocess_wait_check (f->xdg_email, NULL, &error); 830 | g_assert_no_error (error); 831 | 832 | g_assert_cmpuint (g_queue_get_length (&f->invocations), ==, 1); 833 | invocation = g_queue_pop_head (&f->invocations); 834 | 835 | g_assert_cmpstr (g_dbus_method_invocation_get_interface_name (invocation), 836 | ==, PORTAL_IFACE_NAME); 837 | g_assert_cmpstr (g_dbus_method_invocation_get_method_name (invocation), 838 | ==, "ComposeEmail"); 839 | 840 | parameters = g_dbus_method_invocation_get_parameters (invocation); 841 | g_assert_cmpstr (g_variant_get_type_string (parameters), ==, "(sa{sv})"); 842 | g_variant_get (parameters, "(&s@a{sv})", 843 | &window, &asv); 844 | g_assert_cmpstr (window, ==, ""); 845 | 846 | dict = g_variant_dict_new (asv); 847 | 848 | if (f->config->iface_version >= 3) 849 | { 850 | g_assert_true (g_variant_dict_lookup (dict, "addresses", "^a&s", &addresses)); 851 | g_assert_cmpstr (addresses[0], ==, "me@example.com"); 852 | g_assert_cmpstr (addresses[1], ==, "you@example.com"); 853 | g_assert_cmpstr (addresses[2], ==, NULL); 854 | g_free (addresses); 855 | 856 | g_assert_true (g_variant_dict_lookup (dict, "cc", "^a&s", &addresses)); 857 | g_assert_cmpstr (addresses[0], ==, "us@example.com"); 858 | g_assert_cmpstr (addresses[1], ==, "them@example.com"); 859 | g_assert_cmpstr (addresses[2], ==, NULL); 860 | g_free (addresses); 861 | 862 | g_assert_true (g_variant_dict_lookup (dict, "bcc", "^a&s", &addresses)); 863 | g_assert_cmpstr (addresses[0], ==, "hidden@example.com"); 864 | g_assert_cmpstr (addresses[1], ==, "secret@example.com"); 865 | g_assert_cmpstr (addresses[2], ==, NULL); 866 | g_free (addresses); 867 | } 868 | else 869 | { 870 | /* all addresses except the first are ignored */ 871 | g_assert_true (g_variant_dict_lookup (dict, "address", "&s", &address)); 872 | g_assert_cmpstr (address, ==, "me@example.com"); 873 | } 874 | 875 | g_assert_true (g_variant_dict_lookup (dict, "subject", "&s", &subject)); 876 | g_assert_cmpstr (subject, ==, "Make Money Fast"); 877 | g_assert_true (g_variant_dict_lookup (dict, "body", "&s", &body)); 878 | g_assert_cmpstr (body, ==, "Your spam here"); 879 | } 880 | 881 | static void 882 | teardown (Fixture *f, 883 | gconstpointer context G_GNUC_UNUSED) 884 | { 885 | g_autoptr(GError) error = NULL; 886 | gpointer free_me; 887 | 888 | for (free_me = g_queue_pop_head (&f->invocations); 889 | free_me != NULL; 890 | free_me = g_queue_pop_head (&f->invocations)) 891 | g_object_unref (free_me); 892 | 893 | if (f->mock_object != 0) 894 | g_dbus_connection_unregister_object (f->mock_conn, f->mock_object); 895 | 896 | if (f->dbus_daemon != NULL) 897 | { 898 | g_subprocess_send_signal (f->dbus_daemon, SIGTERM); 899 | g_subprocess_wait (f->dbus_daemon, NULL, &error); 900 | g_assert_no_error (error); 901 | } 902 | 903 | g_clear_object (&f->dbus_daemon); 904 | g_clear_object (&f->xdg_email); 905 | g_clear_object (&f->mock_conn); 906 | g_free (f->dbus_address); 907 | g_free (f->xdg_email_path); 908 | alarm (0); 909 | } 910 | 911 | static const Config v0 = 912 | { 913 | .iface_version = 0, 914 | }; 915 | 916 | static const Config v1 = 917 | { 918 | .iface_version = 1, 919 | }; 920 | 921 | static const Config v3 = 922 | { 923 | .iface_version = 3, 924 | }; 925 | 926 | int 927 | main (int argc, 928 | char **argv) 929 | { 930 | g_test_init (&argc, &argv, NULL); 931 | 932 | g_test_add ("/help", Fixture, &v0, setup, test_help, teardown); 933 | g_test_add ("/minimal/v0", Fixture, &v0, setup, test_minimal, teardown); 934 | g_test_add ("/minimal/v1", Fixture, &v1, setup, test_minimal, teardown); 935 | g_test_add ("/minimal/v3", Fixture, &v3, setup, test_minimal, teardown); 936 | g_test_add ("/maximal/v0", Fixture, &v0, setup, test_maximal, teardown); 937 | g_test_add ("/maximal/v1", Fixture, &v1, setup, test_maximal, teardown); 938 | g_test_add ("/maximal/v3", Fixture, &v3, setup, test_maximal, teardown); 939 | g_test_add ("/mailto/none", Fixture, &v0, setup, test_mailto_none, teardown); 940 | g_test_add ("/mailto/single", Fixture, &v3, setup, test_mailto_single, teardown); 941 | g_test_add ("/mailto/multiple", Fixture, &v3, setup, test_mailto_multiple, teardown); 942 | g_test_add ("/mailto/complex", Fixture, &v3, setup, test_mailto_complex, teardown); 943 | g_test_add ("/mailto/combined", Fixture, &v3, setup, test_mailto_combined, teardown); 944 | 945 | return g_test_run (); 946 | } 947 | -------------------------------------------------------------------------------- /tests/test-open.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2018-2019 Collabora Ltd. 3 | * 4 | * This program is free software; you can redistribute it and/or 5 | * modify it under the terms of the GNU Lesser General Public 6 | * License as published by the Free Software Foundation; either 7 | * version 2.1 of the License, or (at your option) any later version. 8 | * 9 | * This library is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | * Lesser General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Lesser General Public 15 | * License along with this library. If not, see . 16 | */ 17 | 18 | #include 19 | #include 20 | #include 21 | #include 22 | 23 | #include 24 | #include 25 | #include 26 | 27 | #include "backport-autoptr.h" 28 | #include "common.h" 29 | 30 | #define PORTAL_BUS_NAME "org.freedesktop.portal.Desktop" 31 | #define PORTAL_OBJECT_PATH "/org/freedesktop/portal/desktop" 32 | #define PORTAL_IFACE_NAME "org.freedesktop.portal.OpenURI" 33 | 34 | typedef struct 35 | { 36 | GSubprocess *dbus_daemon; 37 | gchar *dbus_address; 38 | GSubprocess *xdg_open; 39 | gchar *xdg_open_path; 40 | GDBusConnection *mock_conn; 41 | guint mock_object; 42 | GQueue invocations; 43 | } Fixture; 44 | 45 | typedef struct 46 | { 47 | int dummy; 48 | } Config; 49 | 50 | static void 51 | mock_method_call (GDBusConnection *conn G_GNUC_UNUSED, 52 | const gchar *sender G_GNUC_UNUSED, 53 | const gchar *object_path G_GNUC_UNUSED, 54 | const gchar *interface_name, 55 | const gchar *method_name, 56 | GVariant *parameters G_GNUC_UNUSED, 57 | GDBusMethodInvocation *invocation, 58 | gpointer user_data) 59 | { 60 | Fixture *f = user_data; 61 | g_autofree gchar *params = NULL; 62 | 63 | params = g_variant_print (parameters, TRUE); 64 | 65 | g_test_message ("Method called: %s.%s%s", interface_name, method_name, 66 | params); 67 | 68 | g_queue_push_tail (&f->invocations, g_object_ref (invocation)); 69 | g_dbus_method_invocation_return_value (invocation, 70 | g_variant_new ("(o)", "/foo")); 71 | } 72 | 73 | static GDBusArgInfo arg_parent_window = 74 | { 75 | -1, 76 | "parent_window", 77 | "s", 78 | NULL /* annotations */ 79 | }; 80 | 81 | static GDBusArgInfo arg_uri = 82 | { 83 | -1, 84 | "uri", 85 | "s", 86 | NULL /* annotations */ 87 | }; 88 | 89 | static GDBusArgInfo arg_options = 90 | { 91 | -1, 92 | "options", 93 | "a{sv}", 94 | NULL /* annotations */ 95 | }; 96 | 97 | static GDBusArgInfo arg_fd = 98 | { 99 | -1, 100 | "fd", 101 | "h", 102 | NULL /* annotations */ 103 | }; 104 | 105 | static GDBusArgInfo arg_out_handle = 106 | { 107 | -1, 108 | "handle", 109 | "o", 110 | NULL /* annotations */ 111 | }; 112 | 113 | static GDBusArgInfo *open_uri_in_args[] = 114 | { 115 | &arg_parent_window, 116 | &arg_uri, 117 | &arg_options, 118 | NULL 119 | }; 120 | 121 | static GDBusArgInfo *open_uri_out_args[] = 122 | { 123 | &arg_out_handle, 124 | NULL 125 | }; 126 | 127 | static GDBusMethodInfo open_uri_info = 128 | { 129 | -1, 130 | "OpenURI", 131 | open_uri_in_args, 132 | open_uri_out_args, 133 | NULL /* annotations */ 134 | }; 135 | 136 | static GDBusArgInfo *open_file_in_args[] = 137 | { 138 | &arg_parent_window, 139 | &arg_fd, 140 | &arg_options, 141 | NULL 142 | }; 143 | 144 | static GDBusMethodInfo open_file_info = 145 | { 146 | -1, 147 | "OpenFile", 148 | open_file_in_args, 149 | open_uri_out_args, /* the same */ 150 | NULL /* annotations */ 151 | }; 152 | 153 | static GDBusMethodInfo *method_info[] = 154 | { 155 | &open_uri_info, 156 | &open_file_info, 157 | NULL 158 | }; 159 | 160 | static GDBusInterfaceInfo iface_info = 161 | { 162 | -1, 163 | PORTAL_IFACE_NAME, 164 | method_info, 165 | NULL, /* signals */ 166 | NULL, /* properties */ 167 | NULL /* annotations */ 168 | }; 169 | 170 | static const GDBusInterfaceVTable vtable = 171 | { 172 | mock_method_call, 173 | NULL, /* get */ 174 | NULL /* set */ 175 | }; 176 | 177 | static void 178 | setup (Fixture *f, 179 | gconstpointer context G_GNUC_UNUSED) 180 | { 181 | g_autoptr(GError) error = NULL; 182 | 183 | g_queue_init (&f->invocations); 184 | 185 | setup_dbus_daemon (&f->dbus_daemon, &f->dbus_address); 186 | 187 | f->xdg_open_path = g_strdup (g_getenv ("XDG_OPEN")); 188 | 189 | if (f->xdg_open_path == NULL) 190 | f->xdg_open_path = g_strdup (BINDIR "/xdg-open"); 191 | 192 | f->mock_conn = g_dbus_connection_new_for_address_sync (f->dbus_address, 193 | (G_DBUS_CONNECTION_FLAGS_AUTHENTICATION_CLIENT | 194 | G_DBUS_CONNECTION_FLAGS_MESSAGE_BUS_CONNECTION), 195 | NULL, NULL, &error); 196 | g_assert_no_error (error); 197 | g_assert_nonnull (f->mock_conn); 198 | 199 | f->mock_object = g_dbus_connection_register_object (f->mock_conn, 200 | PORTAL_OBJECT_PATH, 201 | &iface_info, 202 | &vtable, 203 | f, 204 | NULL, 205 | &error); 206 | g_assert_no_error (error); 207 | g_assert_cmpuint (f->mock_object, !=, 0); 208 | 209 | own_name_sync (f->mock_conn, PORTAL_BUS_NAME); 210 | } 211 | 212 | static void 213 | test_help (Fixture *f, 214 | gconstpointer context G_GNUC_UNUSED) 215 | { 216 | g_autoptr(GSubprocessLauncher) launcher = NULL; 217 | g_autofree gchar *stdout_buf; 218 | g_autofree gchar *stderr_buf; 219 | g_autoptr(GError) error = NULL; 220 | 221 | launcher = g_subprocess_launcher_new (G_SUBPROCESS_FLAGS_STDOUT_PIPE | 222 | G_SUBPROCESS_FLAGS_STDERR_PIPE); 223 | g_subprocess_launcher_setenv (launcher, 224 | "DBUS_SESSION_BUS_ADDRESS", 225 | f->dbus_address, 226 | TRUE); 227 | 228 | f->xdg_open = g_subprocess_launcher_spawn (launcher, &error, 229 | f->xdg_open_path, 230 | "--help", 231 | NULL); 232 | g_assert_no_error (error); 233 | g_assert_nonnull (f->xdg_open); 234 | 235 | g_subprocess_communicate_utf8 (f->xdg_open, NULL, NULL, &stdout_buf, 236 | &stderr_buf, &error); 237 | g_assert_cmpstr (stderr_buf, ==, ""); 238 | g_assert_nonnull (stdout_buf); 239 | g_test_message ("xdg-open --help: %s", stdout_buf); 240 | g_assert_true (strstr (stdout_buf, "--version") != NULL); 241 | 242 | g_subprocess_wait_check (f->xdg_open, NULL, &error); 243 | g_assert_no_error (error); 244 | } 245 | 246 | static void 247 | test_uri (Fixture *f, 248 | gconstpointer context G_GNUC_UNUSED) 249 | { 250 | g_autoptr(GSubprocessLauncher) launcher = NULL; 251 | g_autoptr(GError) error = NULL; 252 | g_autoptr(GDBusMethodInvocation) invocation = NULL; 253 | GVariant *parameters; 254 | const gchar *window; 255 | const gchar *uri; 256 | 257 | launcher = g_subprocess_launcher_new (G_SUBPROCESS_FLAGS_STDOUT_PIPE); 258 | g_subprocess_launcher_setenv (launcher, 259 | "DBUS_SESSION_BUS_ADDRESS", 260 | f->dbus_address, 261 | TRUE); 262 | 263 | f->xdg_open = g_subprocess_launcher_spawn (launcher, &error, 264 | f->xdg_open_path, 265 | "http://example.com/", 266 | NULL); 267 | g_assert_no_error (error); 268 | g_assert_nonnull (f->xdg_open); 269 | 270 | while (g_queue_get_length (&f->invocations) < 1) 271 | g_main_context_iteration (NULL, TRUE); 272 | 273 | g_subprocess_wait_check (f->xdg_open, NULL, &error); 274 | g_assert_no_error (error); 275 | 276 | g_assert_cmpuint (g_queue_get_length (&f->invocations), ==, 1); 277 | invocation = g_queue_pop_head (&f->invocations); 278 | 279 | g_assert_cmpstr (g_dbus_method_invocation_get_interface_name (invocation), 280 | ==, PORTAL_IFACE_NAME); 281 | g_assert_cmpstr (g_dbus_method_invocation_get_method_name (invocation), 282 | ==, "OpenURI"); 283 | 284 | parameters = g_dbus_method_invocation_get_parameters (invocation); 285 | g_assert_cmpstr (g_variant_get_type_string (parameters), ==, "(ssa{sv})"); 286 | g_variant_get (parameters, "(&s&sa{sv})", 287 | &window, &uri, NULL); 288 | g_assert_cmpstr (window, ==, ""); 289 | g_assert_cmpstr (uri, ==, "http://example.com/"); 290 | } 291 | 292 | static void 293 | test_file (Fixture *f, 294 | gconstpointer context G_GNUC_UNUSED) 295 | { 296 | g_autoptr(GSubprocessLauncher) launcher = NULL; 297 | g_autoptr(GError) error = NULL; 298 | g_autoptr(GDBusMethodInvocation) invocation = NULL; 299 | GVariant *parameters; 300 | const gchar *window; 301 | gint32 handle; 302 | GDBusMessage *message; 303 | GUnixFDList *fd_list; 304 | const int *fds; 305 | struct stat ours, theirs; 306 | 307 | if (stat ("/dev/null", &ours) < 0) 308 | g_error ("stat(/dev/null): %s", g_strerror (errno)); 309 | 310 | launcher = g_subprocess_launcher_new (G_SUBPROCESS_FLAGS_STDOUT_PIPE); 311 | g_subprocess_launcher_setenv (launcher, 312 | "DBUS_SESSION_BUS_ADDRESS", 313 | f->dbus_address, 314 | TRUE); 315 | 316 | f->xdg_open = g_subprocess_launcher_spawn (launcher, &error, 317 | f->xdg_open_path, 318 | "/dev/null", 319 | NULL); 320 | g_assert_no_error (error); 321 | g_assert_nonnull (f->xdg_open); 322 | 323 | while (g_queue_get_length (&f->invocations) < 1) 324 | g_main_context_iteration (NULL, TRUE); 325 | 326 | g_subprocess_wait_check (f->xdg_open, NULL, &error); 327 | g_assert_no_error (error); 328 | 329 | g_assert_cmpuint (g_queue_get_length (&f->invocations), ==, 1); 330 | invocation = g_queue_pop_head (&f->invocations); 331 | 332 | g_assert_cmpstr (g_dbus_method_invocation_get_interface_name (invocation), 333 | ==, PORTAL_IFACE_NAME); 334 | g_assert_cmpstr (g_dbus_method_invocation_get_method_name (invocation), 335 | ==, "OpenFile"); 336 | 337 | parameters = g_dbus_method_invocation_get_parameters (invocation); 338 | g_assert_cmpstr (g_variant_get_type_string (parameters), ==, "(sha{sv})"); 339 | g_variant_get (parameters, "(&sha{sv})", 340 | &window, &handle, NULL); 341 | g_assert_cmpstr (window, ==, ""); 342 | g_assert_cmpint (handle, ==, 0); 343 | 344 | message = g_dbus_method_invocation_get_message (invocation); 345 | g_assert_cmpuint (g_dbus_message_get_num_unix_fds (message), ==, 1); 346 | fd_list = g_dbus_message_get_unix_fd_list (message); 347 | fds = g_unix_fd_list_peek_fds (fd_list, NULL); 348 | g_assert_cmpint (fds[0], >=, 0); 349 | g_assert_cmpint (fds[1], ==, -1); 350 | 351 | if (fstat (fds[0], &theirs) < 0) 352 | g_error ("stat(their fd): %s", g_strerror (errno)); 353 | 354 | /* It's really /dev/null */ 355 | g_assert_cmpuint (ours.st_dev, ==, theirs.st_dev); 356 | g_assert_cmpuint (ours.st_ino, ==, theirs.st_ino); 357 | } 358 | 359 | static void 360 | teardown (Fixture *f, 361 | gconstpointer context G_GNUC_UNUSED) 362 | { 363 | g_autoptr(GError) error = NULL; 364 | gpointer free_me; 365 | 366 | for (free_me = g_queue_pop_head (&f->invocations); 367 | free_me != NULL; 368 | free_me = g_queue_pop_head (&f->invocations)) 369 | g_object_unref (free_me); 370 | 371 | if (f->mock_object != 0) 372 | g_dbus_connection_unregister_object (f->mock_conn, f->mock_object); 373 | 374 | if (f->dbus_daemon != NULL) 375 | { 376 | g_subprocess_send_signal (f->dbus_daemon, SIGTERM); 377 | g_subprocess_wait (f->dbus_daemon, NULL, &error); 378 | g_assert_no_error (error); 379 | } 380 | 381 | g_clear_object (&f->dbus_daemon); 382 | g_clear_object (&f->xdg_open); 383 | g_clear_object (&f->mock_conn); 384 | g_free (f->dbus_address); 385 | g_free (f->xdg_open_path); 386 | alarm (0); 387 | } 388 | 389 | int 390 | main (int argc, 391 | char **argv) 392 | { 393 | g_test_init (&argc, &argv, NULL); 394 | 395 | g_test_add ("/help", Fixture, NULL, setup, test_help, teardown); 396 | g_test_add ("/uri", Fixture, NULL, setup, test_uri, teardown); 397 | g_test_add ("/file", Fixture, NULL, setup, test_file, teardown); 398 | 399 | return g_test_run (); 400 | } 401 | -------------------------------------------------------------------------------- /tests/test-spawn.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2018-2019 Collabora Ltd. 3 | * Copyright © 2021 Simon McVittie 4 | * 5 | * This program is free software; you can redistribute it and/or 6 | * modify it under the terms of the GNU Lesser General Public 7 | * License as published by the Free Software Foundation; either 8 | * version 2.1 of the License, or (at your option) any later version. 9 | * 10 | * This library is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 13 | * Lesser General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU Lesser General Public 16 | * License along with this library. If not, see . 17 | */ 18 | 19 | #include 20 | #include 21 | #include 22 | #include 23 | 24 | #include 25 | #include 26 | #include 27 | 28 | #include "backport-autoptr.h" 29 | #include "common.h" 30 | 31 | typedef enum 32 | { 33 | FLATPAK_HOST_COMMAND_FLAGS_CLEAR_ENV = 1 << 0, 34 | FLATPAK_HOST_COMMAND_FLAGS_WATCH_BUS = 1 << 1, 35 | } FlatpakHostCommandFlags; 36 | 37 | #define FLATPAK_SESSION_HELPER_BUS_NAME "org.freedesktop.Flatpak" 38 | #define FLATPAK_SESSION_HELPER_PATH_DEVELOPMENT "/org/freedesktop/Flatpak/Development" 39 | #define FLATPAK_SESSION_HELPER_INTERFACE_DEVELOPMENT "org.freedesktop.Flatpak.Development" 40 | 41 | typedef enum { 42 | FLATPAK_SPAWN_FLAGS_CLEAR_ENV = 1 << 0, 43 | FLATPAK_SPAWN_FLAGS_LATEST_VERSION = 1 << 1, 44 | FLATPAK_SPAWN_FLAGS_SANDBOX = 1 << 2, 45 | FLATPAK_SPAWN_FLAGS_NO_NETWORK = 1 << 3, 46 | FLATPAK_SPAWN_FLAGS_WATCH_BUS = 1 << 4, 47 | FLATPAK_SPAWN_FLAGS_EXPOSE_PIDS = 1 << 5, 48 | FLATPAK_SPAWN_FLAGS_NOTIFY_START = 1 << 6, 49 | FLATPAK_SPAWN_FLAGS_SHARE_PIDS = 1 << 7, 50 | FLATPAK_SPAWN_FLAGS_EMPTY_APP = 1 << 8, 51 | } FlatpakSpawnFlags; 52 | 53 | typedef enum { 54 | FLATPAK_SPAWN_SANDBOX_FLAGS_SHARE_DISPLAY = 1 << 0, 55 | FLATPAK_SPAWN_SANDBOX_FLAGS_SHARE_SOUND = 1 << 1, 56 | FLATPAK_SPAWN_SANDBOX_FLAGS_SHARE_GPU = 1 << 2, 57 | FLATPAK_SPAWN_SANDBOX_FLAGS_ALLOW_DBUS = 1 << 3, 58 | FLATPAK_SPAWN_SANDBOX_FLAGS_ALLOW_A11Y = 1 << 4, 59 | } FlatpakSpawnSandboxFlags; 60 | #define FLATPAK_SPAWN_SANDBOX_FLAGS_FUTURE (1 << 23) 61 | 62 | typedef enum { 63 | FLATPAK_SPAWN_SUPPORT_FLAGS_EXPOSE_PIDS = 1 << 0, 64 | } FlatpakSpawnSupportFlags; 65 | 66 | #define FLATPAK_PORTAL_BUS_NAME "org.freedesktop.portal.Flatpak" 67 | #define FLATPAK_PORTAL_PATH "/org/freedesktop/portal/Flatpak" 68 | #define FLATPAK_PORTAL_INTERFACE FLATPAK_PORTAL_BUS_NAME 69 | 70 | typedef struct 71 | { 72 | const char *extra_arg; 73 | FlatpakHostCommandFlags host_flags; 74 | FlatpakSpawnFlags subsandbox_flags; 75 | FlatpakSpawnSandboxFlags subsandbox_sandbox_flags; 76 | FlatpakSpawnSupportFlags portal_supports; 77 | const char *app_path; 78 | const char *usr_path; 79 | int fails_immediately; 80 | int fails_after_version_check; 81 | gboolean awkward_command_name; 82 | gboolean dbus_call_fails; 83 | gboolean extra; 84 | gboolean host; 85 | gboolean no_command; 86 | gboolean no_session_bus; 87 | gboolean sandbox_complex; 88 | } Config; 89 | 90 | typedef struct 91 | { 92 | const Config *config; 93 | GSubprocess *dbus_daemon; 94 | gchar *dbus_address; 95 | GSubprocess *flatpak_spawn; 96 | gchar *flatpak_spawn_path; 97 | GDBusConnection *mock_development_conn; 98 | GDBusConnection *mock_portal_conn; 99 | guint mock_development_object; 100 | guint mock_portal_object; 101 | GQueue invocations; 102 | guint32 mock_development_version; 103 | guint32 mock_portal_version; 104 | guint32 mock_portal_supports; 105 | } Fixture; 106 | 107 | static const Config default_config = {}; 108 | 109 | static void 110 | mock_method_call (GDBusConnection *conn G_GNUC_UNUSED, 111 | const gchar *sender G_GNUC_UNUSED, 112 | const gchar *object_path G_GNUC_UNUSED, 113 | const gchar *interface_name, 114 | const gchar *method_name, 115 | GVariant *parameters G_GNUC_UNUSED, 116 | GDBusMethodInvocation *invocation, 117 | gpointer user_data) 118 | { 119 | Fixture *f = user_data; 120 | g_autofree gchar *params = NULL; 121 | 122 | params = g_variant_print (parameters, TRUE); 123 | 124 | g_test_message ("Method called: %s.%s%s", interface_name, method_name, 125 | params); 126 | 127 | g_queue_push_tail (&f->invocations, g_object_ref (invocation)); 128 | 129 | if (f->config->dbus_call_fails) 130 | { 131 | g_dbus_method_invocation_return_dbus_error (invocation, 132 | "com.example.No", 133 | "Mock portal failed"); 134 | return; 135 | } 136 | 137 | if (strcmp (method_name, "HostCommand") == 0 || 138 | strcmp (method_name, "Spawn") == 0) 139 | g_dbus_method_invocation_return_value (invocation, 140 | g_variant_new ("(u)", 12345)); 141 | else /* HostCommandSignal or SpawnSignal */ 142 | g_dbus_method_invocation_return_value (invocation, NULL); 143 | } 144 | 145 | static GVariant * 146 | mock_get_property (GDBusConnection *conn G_GNUC_UNUSED, 147 | const gchar *sender G_GNUC_UNUSED, 148 | const gchar *object_path G_GNUC_UNUSED, 149 | const gchar *interface_name, 150 | const gchar *property_name, 151 | GError **error, 152 | gpointer user_data) 153 | { 154 | Fixture *f = user_data; 155 | 156 | g_test_message ("Property retrieved: %s.%s", interface_name, property_name); 157 | 158 | if (strcmp (interface_name, FLATPAK_SESSION_HELPER_INTERFACE_DEVELOPMENT) == 0) 159 | { 160 | if (strcmp (property_name, "version") == 0) 161 | return g_variant_new_uint32 (f->mock_development_version); 162 | } 163 | 164 | if (strcmp (interface_name, FLATPAK_PORTAL_INTERFACE) == 0) 165 | { 166 | if (strcmp (property_name, "supports") == 0) 167 | return g_variant_new_uint32 (f->mock_portal_supports); 168 | 169 | if (strcmp (property_name, "version") == 0) 170 | return g_variant_new_uint32 (f->mock_portal_version); 171 | } 172 | 173 | g_set_error (error, G_DBUS_ERROR, G_DBUS_ERROR_UNKNOWN_PROPERTY, 174 | "Unknown interface or property %s.%s", 175 | interface_name, property_name); 176 | return NULL; 177 | } 178 | 179 | static GDBusArgInfo arg_cwd_path = 180 | { 181 | -1, 182 | "cwd_path", 183 | "ay", 184 | NULL /* annotations */ 185 | }; 186 | 187 | static GDBusArgInfo arg_argv = 188 | { 189 | -1, 190 | "argv", 191 | "aay", 192 | NULL /* annotations */ 193 | }; 194 | 195 | static GDBusArgInfo arg_fds = 196 | { 197 | -1, 198 | "fds", 199 | "a{uh}", 200 | NULL /* annotations */ 201 | }; 202 | 203 | static GDBusArgInfo arg_envs = 204 | { 205 | -1, 206 | "envs", 207 | "a{ss}", 208 | NULL /* annotations */ 209 | }; 210 | 211 | static GDBusArgInfo arg_flags = 212 | { 213 | -1, 214 | "flags", 215 | "u", 216 | NULL /* annotations */ 217 | }; 218 | 219 | static GDBusArgInfo arg_out_pid = 220 | { 221 | -1, 222 | "pid", 223 | "u", 224 | NULL /* annotations */ 225 | }; 226 | 227 | static GDBusArgInfo *host_command_in_args[] = 228 | { 229 | &arg_cwd_path, 230 | &arg_argv, 231 | &arg_fds, 232 | &arg_envs, 233 | &arg_flags, 234 | NULL 235 | }; 236 | 237 | static GDBusArgInfo *host_command_out_args[] = 238 | { 239 | &arg_out_pid, 240 | NULL 241 | }; 242 | 243 | static GDBusMethodInfo host_command_info = 244 | { 245 | -1, 246 | "HostCommand", 247 | host_command_in_args, 248 | host_command_out_args, 249 | NULL /* annotations */ 250 | }; 251 | 252 | static GDBusArgInfo arg_pid = 253 | { 254 | -1, 255 | "pid", 256 | "u", 257 | NULL /* annotations */ 258 | }; 259 | 260 | static GDBusArgInfo arg_signal = 261 | { 262 | -1, 263 | "signal", 264 | "u", 265 | NULL /* annotations */ 266 | }; 267 | 268 | static GDBusArgInfo arg_to_process_group = 269 | { 270 | -1, 271 | "to_process_group", 272 | "b", 273 | NULL /* annotations */ 274 | }; 275 | 276 | static GDBusArgInfo *host_command_signal_in_args[] = 277 | { 278 | &arg_pid, 279 | &arg_signal, 280 | &arg_to_process_group, 281 | NULL 282 | }; 283 | 284 | static GDBusMethodInfo host_command_signal_info = 285 | { 286 | -1, 287 | "HostCommandSignal", 288 | host_command_signal_in_args, 289 | NULL, /* out args */ 290 | NULL /* annotations */ 291 | }; 292 | 293 | static GDBusMethodInfo *development_method_info[] = 294 | { 295 | &host_command_info, 296 | &host_command_signal_info, 297 | NULL 298 | }; 299 | 300 | static GDBusPropertyInfo version_property_info = 301 | { 302 | -1, 303 | "version", 304 | "u", 305 | G_DBUS_PROPERTY_INFO_FLAGS_READABLE, 306 | NULL /* annotations */ 307 | }; 308 | 309 | static GDBusPropertyInfo *version_properties_info[] = 310 | { 311 | &version_property_info, 312 | NULL 313 | }; 314 | 315 | static GDBusInterfaceInfo development_iface_info = 316 | { 317 | -1, 318 | FLATPAK_SESSION_HELPER_INTERFACE_DEVELOPMENT, 319 | development_method_info, 320 | NULL, /* signals */ 321 | version_properties_info, 322 | NULL /* annotations */ 323 | }; 324 | 325 | static GDBusArgInfo arg_options = 326 | { 327 | -1, 328 | "options", 329 | "a{sv}", 330 | NULL /* annotations */ 331 | }; 332 | 333 | static GDBusArgInfo *spawn_in_args[] = 334 | { 335 | &arg_cwd_path, 336 | &arg_argv, 337 | &arg_fds, 338 | &arg_envs, 339 | &arg_flags, 340 | &arg_options, 341 | NULL 342 | }; 343 | 344 | static GDBusArgInfo *spawn_out_args[] = 345 | { 346 | &arg_out_pid, 347 | NULL 348 | }; 349 | 350 | static GDBusMethodInfo spawn_info = 351 | { 352 | -1, 353 | "Spawn", 354 | spawn_in_args, 355 | spawn_out_args, 356 | NULL /* annotations */ 357 | }; 358 | 359 | static GDBusMethodInfo spawn_signal_info = 360 | { 361 | -1, 362 | "SpawnSignal", 363 | host_command_signal_in_args, /* they're the same */ 364 | NULL, /* out args */ 365 | NULL /* annotations */ 366 | }; 367 | 368 | static GDBusMethodInfo *portal_method_info[] = 369 | { 370 | &spawn_info, 371 | &spawn_signal_info, 372 | NULL 373 | }; 374 | 375 | static GDBusPropertyInfo supports_property_info = 376 | { 377 | -1, 378 | "supports", 379 | "u", 380 | G_DBUS_PROPERTY_INFO_FLAGS_READABLE, 381 | NULL /* annotations */ 382 | }; 383 | 384 | static GDBusPropertyInfo *portal_properties_info[] = 385 | { 386 | &version_property_info, 387 | &supports_property_info, 388 | NULL 389 | }; 390 | 391 | static GDBusInterfaceInfo portal_iface_info = 392 | { 393 | -1, 394 | FLATPAK_PORTAL_INTERFACE, 395 | portal_method_info, 396 | NULL, /* signals */ 397 | portal_properties_info, 398 | NULL /* annotations */ 399 | }; 400 | 401 | static const GDBusInterfaceVTable vtable = 402 | { 403 | mock_method_call, 404 | mock_get_property, 405 | NULL /* set */ 406 | }; 407 | 408 | static void 409 | setup (Fixture *f, 410 | gconstpointer context) 411 | { 412 | g_autoptr(GError) error = NULL; 413 | 414 | if (context == NULL) 415 | f->config = &default_config; 416 | else 417 | f->config = context; 418 | 419 | f->mock_development_version = 1; 420 | f->mock_portal_version = 6; 421 | f->mock_portal_supports = f->config->portal_supports; 422 | 423 | g_queue_init (&f->invocations); 424 | 425 | setup_dbus_daemon (&f->dbus_daemon, &f->dbus_address); 426 | 427 | f->flatpak_spawn_path = g_strdup (g_getenv ("FLATPAK_SPAWN")); 428 | 429 | if (f->flatpak_spawn_path == NULL) 430 | f->flatpak_spawn_path = g_strdup (BINDIR "/flatpak-spawn"); 431 | 432 | f->mock_development_conn = g_dbus_connection_new_for_address_sync (f->dbus_address, 433 | (G_DBUS_CONNECTION_FLAGS_AUTHENTICATION_CLIENT | 434 | G_DBUS_CONNECTION_FLAGS_MESSAGE_BUS_CONNECTION), 435 | NULL, NULL, &error); 436 | g_assert_no_error (error); 437 | g_assert_nonnull (f->mock_development_conn); 438 | 439 | f->mock_portal_conn = g_dbus_connection_new_for_address_sync (f->dbus_address, 440 | (G_DBUS_CONNECTION_FLAGS_AUTHENTICATION_CLIENT | 441 | G_DBUS_CONNECTION_FLAGS_MESSAGE_BUS_CONNECTION), 442 | NULL, NULL, &error); 443 | g_assert_no_error (error); 444 | g_assert_nonnull (f->mock_portal_conn); 445 | 446 | f->mock_development_object = g_dbus_connection_register_object (f->mock_development_conn, 447 | FLATPAK_SESSION_HELPER_PATH_DEVELOPMENT, 448 | &development_iface_info, 449 | &vtable, 450 | f, 451 | NULL, 452 | &error); 453 | g_assert_no_error (error); 454 | g_assert_cmpuint (f->mock_development_object, !=, 0); 455 | 456 | f->mock_portal_object = g_dbus_connection_register_object (f->mock_portal_conn, 457 | FLATPAK_PORTAL_PATH, 458 | &portal_iface_info, 459 | &vtable, 460 | f, 461 | NULL, 462 | &error); 463 | g_assert_no_error (error); 464 | g_assert_cmpuint (f->mock_portal_object, !=, 0); 465 | 466 | own_name_sync (f->mock_development_conn, FLATPAK_SESSION_HELPER_BUS_NAME); 467 | own_name_sync (f->mock_portal_conn, FLATPAK_PORTAL_BUS_NAME); 468 | } 469 | 470 | static void 471 | test_help (Fixture *f, 472 | gconstpointer context G_GNUC_UNUSED) 473 | { 474 | g_autoptr(GSubprocessLauncher) launcher = NULL; 475 | g_autofree gchar *stdout_buf; 476 | g_autofree gchar *stderr_buf; 477 | g_autoptr(GError) error = NULL; 478 | 479 | launcher = g_subprocess_launcher_new (G_SUBPROCESS_FLAGS_STDOUT_PIPE | 480 | G_SUBPROCESS_FLAGS_STDERR_PIPE); 481 | g_subprocess_launcher_setenv (launcher, 482 | "DBUS_SESSION_BUS_ADDRESS", 483 | f->dbus_address, 484 | TRUE); 485 | 486 | f->flatpak_spawn = g_subprocess_launcher_spawn (launcher, &error, 487 | f->flatpak_spawn_path, 488 | "--help", 489 | NULL); 490 | g_assert_no_error (error); 491 | g_assert_nonnull (f->flatpak_spawn); 492 | 493 | g_subprocess_communicate_utf8 (f->flatpak_spawn, NULL, NULL, &stdout_buf, 494 | &stderr_buf, &error); 495 | g_assert_cmpstr (stderr_buf, ==, ""); 496 | g_assert_nonnull (stdout_buf); 497 | g_test_message ("flatpak-spawn --help: %s", stdout_buf); 498 | g_assert_true (strstr (stdout_buf, "--latest-version") != NULL); 499 | 500 | g_subprocess_wait_check (f->flatpak_spawn, NULL, &error); 501 | g_assert_no_error (error); 502 | } 503 | 504 | static void 505 | test_command (Fixture *f, 506 | gconstpointer context) 507 | { 508 | const Config *config = context; 509 | g_autoptr(GSubprocessLauncher) launcher = NULL; 510 | g_autoptr(GError) error = NULL; 511 | g_autoptr(GDBusMethodInvocation) invocation = NULL; 512 | g_autoptr(GPtrArray) command = NULL; 513 | GVariant *parameters; 514 | const char *cwd; 515 | g_autofree const char **argv = NULL; 516 | g_autoptr(GVariant) fds_variant = NULL; 517 | g_autoptr(GVariant) envs_variant = NULL; 518 | guint32 flags; 519 | g_autoptr(GVariant) options_variant = NULL; 520 | GDBusMessage *message; 521 | GUnixFDList *fd_list; 522 | const int *fds; 523 | const char *s; 524 | gsize i; 525 | guint n_fds_for_options = 0; 526 | 527 | g_test_timer_start (); 528 | 529 | launcher = g_subprocess_launcher_new (G_SUBPROCESS_FLAGS_STDOUT_PIPE); 530 | g_subprocess_launcher_set_cwd (launcher, "/"); 531 | 532 | if (config->no_session_bus) 533 | g_subprocess_launcher_setenv (launcher, 534 | "DBUS_SESSION_BUS_ADDRESS", 535 | "nope:", 536 | TRUE); 537 | else 538 | g_subprocess_launcher_setenv (launcher, 539 | "DBUS_SESSION_BUS_ADDRESS", 540 | f->dbus_address, 541 | TRUE); 542 | 543 | command = g_ptr_array_new_with_free_func (g_free); 544 | g_ptr_array_add (command, g_strdup (f->flatpak_spawn_path)); 545 | 546 | if (config->host) 547 | { 548 | g_assert_cmpint (config->subsandbox_flags, ==, 0); 549 | 550 | g_ptr_array_add (command, g_strdup ("--host")); 551 | 552 | if (config->host_flags & FLATPAK_HOST_COMMAND_FLAGS_CLEAR_ENV) 553 | g_ptr_array_add (command, g_strdup ("--clear-env")); 554 | 555 | if (config->host_flags & FLATPAK_HOST_COMMAND_FLAGS_WATCH_BUS) 556 | g_ptr_array_add (command, g_strdup ("--watch-bus")); 557 | } 558 | else 559 | { 560 | g_assert_cmpint (config->host_flags, ==, 0); 561 | 562 | if (config->subsandbox_flags & FLATPAK_SPAWN_FLAGS_CLEAR_ENV) 563 | g_ptr_array_add (command, g_strdup ("--clear-env")); 564 | 565 | if (config->subsandbox_flags & FLATPAK_SPAWN_FLAGS_LATEST_VERSION) 566 | g_ptr_array_add (command, g_strdup ("--latest-version")); 567 | 568 | if (config->subsandbox_flags & FLATPAK_SPAWN_FLAGS_SANDBOX) 569 | { 570 | g_ptr_array_add (command, g_strdup ("--sandbox")); 571 | 572 | if (config->subsandbox_sandbox_flags & FLATPAK_SPAWN_SANDBOX_FLAGS_SHARE_DISPLAY) 573 | g_ptr_array_add (command, g_strdup ("--sandbox-flag=share-display")); 574 | 575 | if (config->subsandbox_sandbox_flags & FLATPAK_SPAWN_SANDBOX_FLAGS_SHARE_SOUND) 576 | g_ptr_array_add (command, g_strdup ("--sandbox-flag=share-sound")); 577 | 578 | if (config->subsandbox_sandbox_flags & FLATPAK_SPAWN_SANDBOX_FLAGS_SHARE_GPU) 579 | g_ptr_array_add (command, g_strdup ("--sandbox-flag=share-gpu")); 580 | 581 | if (config->subsandbox_sandbox_flags & FLATPAK_SPAWN_SANDBOX_FLAGS_ALLOW_DBUS) 582 | g_ptr_array_add (command, g_strdup ("--sandbox-flag=allow-dbus")); 583 | 584 | if (config->subsandbox_sandbox_flags & FLATPAK_SPAWN_SANDBOX_FLAGS_ALLOW_A11Y) 585 | g_ptr_array_add (command, g_strdup ("--sandbox-flag=allow-a11y")); 586 | 587 | if (config->subsandbox_sandbox_flags & FLATPAK_SPAWN_SANDBOX_FLAGS_FUTURE) 588 | g_ptr_array_add (command, g_strdup ("--sandbox-flag=8388608")); 589 | 590 | if (config->sandbox_complex) 591 | { 592 | g_ptr_array_add (command, g_strdup ("--sandbox-expose=/foo")); 593 | g_ptr_array_add (command, g_strdup ("--sandbox-expose=/bar")); 594 | g_ptr_array_add (command, g_strdup ("--sandbox-expose-ro=/proc")); 595 | g_ptr_array_add (command, g_strdup ("--sandbox-expose-ro=/sys")); 596 | g_ptr_array_add (command, g_strdup ("--sandbox-expose-path=/")); 597 | g_ptr_array_add (command, g_strdup ("--sandbox-expose-path-ro=/dev")); 598 | n_fds_for_options += 2; 599 | } 600 | } 601 | 602 | if (config->subsandbox_flags & FLATPAK_SPAWN_FLAGS_NO_NETWORK) 603 | g_ptr_array_add (command, g_strdup ("--no-network")); 604 | 605 | if (config->subsandbox_flags & FLATPAK_SPAWN_FLAGS_WATCH_BUS) 606 | g_ptr_array_add (command, g_strdup ("--watch-bus")); 607 | 608 | if (config->subsandbox_flags & FLATPAK_SPAWN_FLAGS_EXPOSE_PIDS) 609 | g_ptr_array_add (command, g_strdup ("--expose-pids")); 610 | 611 | if (config->subsandbox_flags & FLATPAK_SPAWN_FLAGS_NOTIFY_START) 612 | g_assert_not_reached (); /* TODO: unimplemented */ 613 | 614 | if (config->subsandbox_flags & FLATPAK_SPAWN_FLAGS_SHARE_PIDS) 615 | g_ptr_array_add (command, g_strdup ("--share-pids")); 616 | } 617 | 618 | if (config->app_path != NULL) 619 | { 620 | g_ptr_array_add (command, g_strdup_printf ("--app-path=%s", config->app_path)); 621 | 622 | if (config->app_path[0] != '\0') 623 | n_fds_for_options++; 624 | } 625 | 626 | if (config->usr_path != NULL) 627 | { 628 | g_ptr_array_add (command, g_strdup_printf ("--usr-path=%s", config->usr_path)); 629 | n_fds_for_options++; 630 | } 631 | 632 | /* Generic "extra complexity" options */ 633 | if (config->extra) 634 | { 635 | g_ptr_array_add (command, g_strdup ("--directory=/dev")); 636 | g_ptr_array_add (command, g_strdup ("--env=FOO=bar")); 637 | g_ptr_array_add (command, g_strdup ("--forward-fd=2")); 638 | g_subprocess_launcher_take_fd (launcher, open ("/dev/null", O_RDWR|O_CLOEXEC), 4); 639 | g_ptr_array_add (command, g_strdup ("--forward-fd=4")); 640 | g_ptr_array_add (command, g_strdup ("--unset-env=NOPE")); 641 | g_ptr_array_add (command, g_strdup ("--verbose")); 642 | } 643 | 644 | if (config->extra_arg != NULL) 645 | g_ptr_array_add (command, g_strdup (config->extra_arg)); 646 | 647 | if (config->awkward_command_name) 648 | g_ptr_array_add (command, g_strdup ("some=command")); 649 | else if (!config->no_command) 650 | g_ptr_array_add (command, g_strdup ("some-command")); 651 | 652 | if (config->extra) 653 | { 654 | g_ptr_array_add (command, g_strdup ("--arg1")); 655 | g_ptr_array_add (command, g_strdup ("arg2")); 656 | } 657 | 658 | g_ptr_array_add (command, NULL); 659 | 660 | f->flatpak_spawn = g_subprocess_launcher_spawnv (launcher, 661 | (const char * const *) command->pdata, 662 | &error); 663 | 664 | g_assert_no_error (error); 665 | g_assert_nonnull (f->flatpak_spawn); 666 | 667 | if (config->fails_immediately) 668 | { 669 | g_subprocess_wait_check (f->flatpak_spawn, NULL, &error); 670 | g_assert_error (error, G_SPAWN_EXIT_ERROR, config->fails_immediately); 671 | 672 | /* Make sure we didn't wait for the entire 25 second D-Bus timeout */ 673 | g_assert_cmpfloat (g_test_timer_elapsed (), <=, 20); 674 | 675 | g_test_minimized_result (g_test_timer_elapsed (), 676 | "time to fail immediately: %.1f", 677 | g_test_timer_elapsed ()); 678 | return; 679 | } 680 | 681 | if (config->fails_after_version_check) 682 | { 683 | g_autoptr(GAsyncResult) result = NULL; 684 | 685 | g_subprocess_wait_check_async (f->flatpak_spawn, NULL, 686 | store_result_cb, &result); 687 | 688 | while (result == NULL) 689 | g_main_context_iteration (NULL, TRUE); 690 | 691 | g_subprocess_wait_check_finish (f->flatpak_spawn, result, &error); 692 | g_assert_error (error, G_SPAWN_EXIT_ERROR, config->fails_after_version_check); 693 | 694 | /* Make sure we didn't wait for the entire 25 second D-Bus timeout */ 695 | g_assert_cmpfloat (g_test_timer_elapsed (), <=, 20); 696 | 697 | g_test_minimized_result (g_test_timer_elapsed (), 698 | "time to fail after version check: %.1f", 699 | g_test_timer_elapsed ()); 700 | 701 | g_assert_cmpuint (g_queue_get_length (&f->invocations), ==, 0); 702 | return; 703 | } 704 | 705 | while (g_queue_get_length (&f->invocations) < 1) 706 | g_main_context_iteration (NULL, TRUE); 707 | 708 | g_assert_cmpuint (g_queue_get_length (&f->invocations), ==, 1); 709 | invocation = g_queue_pop_head (&f->invocations); 710 | message = g_dbus_method_invocation_get_message (invocation); 711 | fd_list = g_dbus_message_get_unix_fd_list (message); 712 | fds = g_unix_fd_list_peek_fds (fd_list, NULL); 713 | 714 | if (config->host) 715 | { 716 | g_assert_cmpstr (g_dbus_method_invocation_get_interface_name (invocation), 717 | ==, FLATPAK_SESSION_HELPER_INTERFACE_DEVELOPMENT); 718 | g_assert_cmpstr (g_dbus_method_invocation_get_method_name (invocation), 719 | ==, "HostCommand"); 720 | } 721 | else 722 | { 723 | g_assert_cmpstr (g_dbus_method_invocation_get_interface_name (invocation), 724 | ==, FLATPAK_PORTAL_INTERFACE); 725 | g_assert_cmpstr (g_dbus_method_invocation_get_method_name (invocation), 726 | ==, "Spawn"); 727 | } 728 | 729 | parameters = g_dbus_method_invocation_get_parameters (invocation); 730 | 731 | if (config->host) 732 | { 733 | g_assert_cmpstr (g_variant_get_type_string (parameters), ==, 734 | "(ayaaya{uh}a{ss}u)"); 735 | g_variant_get (parameters, "(^&ay^a&ay@a{uh}@a{ss}u)", 736 | &cwd, &argv, &fds_variant, &envs_variant, &flags); 737 | } 738 | else 739 | { 740 | g_assert_cmpstr (g_variant_get_type_string (parameters), ==, 741 | "(ayaaya{uh}a{ss}ua{sv})"); 742 | g_variant_get (parameters, "(^&ay^a&ay@a{uh}@a{ss}u@a{sv})", 743 | &cwd, &argv, &fds_variant, &envs_variant, &flags, 744 | &options_variant); 745 | } 746 | 747 | if (config->extra) 748 | g_assert_cmpstr (cwd, ==, "/dev"); 749 | else 750 | g_assert_cmpstr (cwd, ==, "/"); 751 | 752 | g_assert_nonnull (argv); 753 | i = 0; 754 | 755 | if (config->extra && config->host) 756 | { 757 | g_assert_cmpstr (argv[i++], ==, "/usr/bin/env"); 758 | g_assert_cmpstr (argv[i++], ==, "-u"); 759 | g_assert_cmpstr (argv[i++], ==, "NOPE"); 760 | 761 | if (config->awkward_command_name) 762 | { 763 | g_assert_cmpstr (argv[i++], ==, "/bin/sh"); 764 | g_assert_cmpstr (argv[i++], ==, "-euc"); 765 | g_assert_cmpstr (argv[i++], ==, "exec \"$@\""); 766 | g_assert_cmpstr (argv[i++], ==, "sh"); /* sh's argv[0] */ 767 | } 768 | } 769 | 770 | if (config->awkward_command_name) 771 | { 772 | g_assert_cmpstr (argv[i++], ==, "some=command"); 773 | } 774 | else 775 | { 776 | g_assert_cmpstr (argv[i++], ==, "some-command"); 777 | } 778 | 779 | if (config->extra) 780 | { 781 | g_assert_cmpstr (argv[i++], ==, "--arg1"); 782 | g_assert_cmpstr (argv[i++], ==, "arg2"); 783 | } 784 | 785 | g_assert_cmpstr (argv[i++], ==, NULL); 786 | 787 | g_assert_cmpstr (g_variant_get_type_string (fds_variant), ==, "a{uh}"); 788 | 789 | /* it carries stdin, stdout, stderr, and maybe fd 4 */ 790 | if (config->extra) 791 | g_assert_cmpuint (g_variant_n_children (fds_variant), ==, 4); 792 | else 793 | g_assert_cmpuint (g_variant_n_children (fds_variant), ==, 3); 794 | 795 | g_assert_cmpstr (g_variant_get_type_string (envs_variant), ==, "a{ss}"); 796 | 797 | if (config->extra) 798 | { 799 | g_assert_cmpuint (g_variant_n_children (envs_variant), ==, 1); 800 | g_assert_true (g_variant_lookup (envs_variant, "FOO", "&s", &s)); 801 | g_assert_cmpstr (s, ==, "bar"); 802 | } 803 | else 804 | { 805 | g_assert_cmpuint (g_variant_n_children (envs_variant), ==, 0); 806 | } 807 | 808 | if (config->host) 809 | { 810 | g_assert_cmpuint (flags, ==, config->host_flags); 811 | } 812 | else 813 | { 814 | guint options_handled = 0; 815 | 816 | g_assert_cmpuint (flags, ==, config->subsandbox_flags); 817 | g_assert_cmpstr (g_variant_get_type_string (options_variant), ==, "a{sv}"); 818 | 819 | if (config->sandbox_complex) 820 | { 821 | g_autofree const char **expose = NULL; 822 | g_autofree const char **ro = NULL; 823 | GVariantIter *handles_iter; 824 | 825 | g_assert_true (g_variant_lookup (options_variant, "sandbox-expose", "^a&s", &expose)); 826 | g_assert_nonnull (expose); 827 | i = 0; 828 | g_assert_cmpstr (expose[i++], ==, "/foo"); 829 | g_assert_cmpstr (expose[i++], ==, "/bar"); 830 | g_assert_cmpstr (expose[i++], ==, NULL); 831 | options_handled++; 832 | 833 | g_assert_true (g_variant_lookup (options_variant, "sandbox-expose-ro", "^a&s", &ro)); 834 | g_assert_nonnull (ro); 835 | i = 0; 836 | g_assert_cmpstr (ro[i++], ==, "/proc"); 837 | g_assert_cmpstr (ro[i++], ==, "/sys"); 838 | g_assert_cmpstr (ro[i++], ==, NULL); 839 | options_handled++; 840 | 841 | g_assert_true (g_variant_lookup (options_variant, "sandbox-flags", "u", &flags)); 842 | g_assert_cmpuint (flags, ==, config->subsandbox_sandbox_flags); 843 | options_handled++; 844 | 845 | g_assert_true (g_variant_lookup (options_variant, "sandbox-expose-fd", "ah", &handles_iter)); 846 | g_assert_nonnull (handles_iter); 847 | g_variant_iter_free (handles_iter); 848 | options_handled++; 849 | 850 | g_assert_true (g_variant_lookup (options_variant, "sandbox-expose-fd-ro", "ah", &handles_iter)); 851 | g_assert_nonnull (handles_iter); 852 | g_variant_iter_free (handles_iter); 853 | options_handled++; 854 | } 855 | 856 | if (config->extra) 857 | { 858 | g_autofree const char **unset = NULL; 859 | 860 | g_assert_true (g_variant_lookup (options_variant, "unset-env", "^a&s", &unset)); 861 | g_assert_nonnull (unset); 862 | i = 0; 863 | g_assert_cmpstr (unset[i++], ==, "NOPE"); 864 | g_assert_cmpstr (unset[i++], ==, NULL); 865 | options_handled++; 866 | } 867 | 868 | if (config->app_path != NULL && config->app_path[0] != '\0') 869 | { 870 | struct stat expected, got; 871 | gint32 handle; 872 | 873 | g_assert_no_errno (stat (config->app_path, &expected)); 874 | g_assert_true (g_variant_lookup (options_variant, "app-fd", "h", &handle)); 875 | g_assert_cmpint (handle, >=, 0); 876 | g_assert_cmpint (handle, <, g_unix_fd_list_get_length (fd_list)); 877 | g_assert_no_errno (fstat (fds[handle], &got)); 878 | g_assert_cmpint (expected.st_dev, ==, got.st_dev); 879 | g_assert_cmpint (expected.st_ino, ==, got.st_ino); 880 | options_handled++; 881 | } 882 | 883 | if (config->usr_path != NULL) 884 | { 885 | struct stat expected, got; 886 | gint32 handle; 887 | 888 | g_assert_no_errno (stat (config->usr_path, &expected)); 889 | g_assert_true (g_variant_lookup (options_variant, "usr-fd", "h", &handle)); 890 | g_assert_cmpint (handle, >=, 0); 891 | g_assert_cmpint (handle, <, g_unix_fd_list_get_length (fd_list)); 892 | g_assert_no_errno (fstat (fds[handle], &got)); 893 | g_assert_cmpint (expected.st_dev, ==, got.st_dev); 894 | g_assert_cmpint (expected.st_ino, ==, got.st_ino); 895 | options_handled++; 896 | } 897 | 898 | g_assert_cmpuint (g_variant_n_children (options_variant), ==, options_handled); 899 | } 900 | 901 | /* it carries stdin, stdout, stderr, and maybe fd 4 */ 902 | g_assert_cmpuint (g_dbus_message_get_num_unix_fds (message), ==, 903 | g_variant_n_children (fds_variant) + 904 | n_fds_for_options); 905 | for (i = 0; i < g_variant_n_children (fds_variant) + n_fds_for_options; i++) 906 | g_assert_cmpint (fds[i], >=, 0); 907 | g_assert_cmpint (fds[i], ==, -1); 908 | 909 | if (f->config->dbus_call_fails) 910 | { 911 | g_subprocess_wait_check (f->flatpak_spawn, NULL, &error); 912 | g_assert_error (error, G_SPAWN_EXIT_ERROR, 1); 913 | g_clear_error (&error); 914 | g_test_minimized_result (g_test_timer_elapsed (), 915 | "time to fail: %.1f", 916 | g_test_timer_elapsed ()); 917 | return; 918 | } 919 | 920 | if (config->host) 921 | { 922 | if (config->extra) 923 | { 924 | /* Pretend the command was killed by SIGSEGV and dumped core */ 925 | g_dbus_connection_emit_signal (f->mock_development_conn, 926 | NULL, 927 | FLATPAK_SESSION_HELPER_PATH_DEVELOPMENT, 928 | FLATPAK_SESSION_HELPER_INTERFACE_DEVELOPMENT, 929 | "HostCommandExited", 930 | g_variant_new ("(uu)", 931 | 12345, 932 | SIGSEGV | 0x80), 933 | &error); 934 | g_assert_no_error (error); 935 | 936 | g_subprocess_wait_check (f->flatpak_spawn, NULL, &error); 937 | g_assert_error (error, G_SPAWN_EXIT_ERROR, 128 + SIGSEGV); 938 | g_clear_error (&error); 939 | } 940 | else 941 | { 942 | /* Pretend the command exited with status 0 */ 943 | g_dbus_connection_emit_signal (f->mock_development_conn, 944 | NULL, 945 | FLATPAK_SESSION_HELPER_PATH_DEVELOPMENT, 946 | FLATPAK_SESSION_HELPER_INTERFACE_DEVELOPMENT, 947 | "HostCommandExited", 948 | g_variant_new ("(uu)", 12345, 0), 949 | &error); 950 | g_assert_no_error (error); 951 | 952 | g_subprocess_wait_check (f->flatpak_spawn, NULL, &error); 953 | g_assert_no_error (error); 954 | } 955 | } 956 | else 957 | { 958 | if (config->extra) 959 | { 960 | /* Pretend the command exited with status 23 */ 961 | g_dbus_connection_emit_signal (f->mock_portal_conn, 962 | NULL, 963 | FLATPAK_PORTAL_PATH, 964 | FLATPAK_PORTAL_INTERFACE, 965 | "SpawnExited", 966 | g_variant_new ("(uu)", 12345, (23 << 8)), 967 | &error); 968 | g_assert_no_error (error); 969 | 970 | g_subprocess_wait_check (f->flatpak_spawn, NULL, &error); 971 | g_assert_error (error, G_SPAWN_EXIT_ERROR, 23); 972 | g_clear_error (&error); 973 | } 974 | else 975 | { 976 | /* Pretend the command exited with status 0 */ 977 | g_dbus_connection_emit_signal (f->mock_portal_conn, 978 | NULL, 979 | FLATPAK_PORTAL_PATH, 980 | FLATPAK_PORTAL_INTERFACE, 981 | "SpawnExited", 982 | g_variant_new ("(uu)", 12345, 0), 983 | &error); 984 | g_assert_no_error (error); 985 | 986 | g_subprocess_wait_check (f->flatpak_spawn, NULL, &error); 987 | g_assert_no_error (error); 988 | } 989 | } 990 | 991 | g_test_minimized_result (g_test_timer_elapsed (), 992 | "time to succeed: %.1f", 993 | g_test_timer_elapsed ()); 994 | } 995 | 996 | static void 997 | teardown (Fixture *f, 998 | gconstpointer context G_GNUC_UNUSED) 999 | { 1000 | g_autoptr(GError) error = NULL; 1001 | gpointer free_me; 1002 | 1003 | for (free_me = g_queue_pop_head (&f->invocations); 1004 | free_me != NULL; 1005 | free_me = g_queue_pop_head (&f->invocations)) 1006 | g_object_unref (free_me); 1007 | 1008 | if (f->mock_development_object != 0) 1009 | g_dbus_connection_unregister_object (f->mock_development_conn, 1010 | f->mock_development_object); 1011 | 1012 | if (f->mock_portal_object != 0) 1013 | g_dbus_connection_unregister_object (f->mock_portal_conn, 1014 | f->mock_portal_object); 1015 | 1016 | if (f->dbus_daemon != NULL) 1017 | { 1018 | g_subprocess_send_signal (f->dbus_daemon, SIGTERM); 1019 | g_subprocess_wait (f->dbus_daemon, NULL, &error); 1020 | g_assert_no_error (error); 1021 | } 1022 | 1023 | g_clear_object (&f->dbus_daemon); 1024 | g_clear_object (&f->flatpak_spawn); 1025 | g_clear_object (&f->mock_development_conn); 1026 | g_clear_object (&f->mock_portal_conn); 1027 | g_free (f->dbus_address); 1028 | g_free (f->flatpak_spawn_path); 1029 | alarm (0); 1030 | } 1031 | 1032 | static const Config subsandbox_fails = 1033 | { 1034 | .dbus_call_fails = TRUE, 1035 | }; 1036 | 1037 | static const Config subsandbox_complex = 1038 | { 1039 | .awkward_command_name = TRUE, 1040 | .extra = TRUE, 1041 | /* This is obviously not a realistic thing to put at /app, but it needs 1042 | * to be something that will certainly exist on the host system! */ 1043 | .app_path = "/dev", 1044 | /* Similar */ 1045 | .usr_path = "/", 1046 | }; 1047 | 1048 | static const Config subsandbox_empty_app = 1049 | { 1050 | .subsandbox_flags = FLATPAK_SPAWN_FLAGS_EMPTY_APP, 1051 | .app_path = "", 1052 | }; 1053 | 1054 | static const Config subsandbox_clear_env = 1055 | { 1056 | .subsandbox_flags = FLATPAK_SPAWN_FLAGS_CLEAR_ENV, 1057 | }; 1058 | 1059 | static const Config subsandbox_latest = 1060 | { 1061 | .subsandbox_flags = FLATPAK_SPAWN_FLAGS_LATEST_VERSION, 1062 | }; 1063 | 1064 | static const Config subsandbox_no_net = 1065 | { 1066 | .subsandbox_flags = FLATPAK_SPAWN_FLAGS_NO_NETWORK, 1067 | }; 1068 | 1069 | static const Config subsandbox_watch_bus = 1070 | { 1071 | .subsandbox_flags = FLATPAK_SPAWN_FLAGS_WATCH_BUS, 1072 | }; 1073 | 1074 | static const Config subsandbox_expose_pids = 1075 | { 1076 | .subsandbox_flags = FLATPAK_SPAWN_FLAGS_EXPOSE_PIDS, 1077 | .portal_supports = FLATPAK_SPAWN_SUPPORT_FLAGS_EXPOSE_PIDS, 1078 | }; 1079 | 1080 | static const Config subsandbox_share_pids = 1081 | { 1082 | .subsandbox_flags = FLATPAK_SPAWN_FLAGS_SHARE_PIDS, 1083 | .portal_supports = FLATPAK_SPAWN_SUPPORT_FLAGS_EXPOSE_PIDS, 1084 | }; 1085 | 1086 | static const Config subsandbox_sandbox_simple = 1087 | { 1088 | .subsandbox_flags = FLATPAK_SPAWN_FLAGS_SANDBOX, 1089 | }; 1090 | 1091 | static const Config subsandbox_sandbox_complex = 1092 | { 1093 | .sandbox_complex = TRUE, 1094 | .subsandbox_flags = FLATPAK_SPAWN_FLAGS_SANDBOX, 1095 | /* TODO: Exercise these separately */ 1096 | .subsandbox_sandbox_flags = (FLATPAK_SPAWN_SANDBOX_FLAGS_SHARE_DISPLAY | 1097 | FLATPAK_SPAWN_SANDBOX_FLAGS_SHARE_SOUND | 1098 | FLATPAK_SPAWN_SANDBOX_FLAGS_SHARE_GPU | 1099 | FLATPAK_SPAWN_SANDBOX_FLAGS_ALLOW_DBUS | 1100 | FLATPAK_SPAWN_SANDBOX_FLAGS_ALLOW_A11Y | 1101 | FLATPAK_SPAWN_SANDBOX_FLAGS_FUTURE), 1102 | }; 1103 | 1104 | static const Config host_simple = 1105 | { 1106 | .host = TRUE, 1107 | }; 1108 | 1109 | static const Config host_fails = 1110 | { 1111 | .dbus_call_fails = TRUE, 1112 | .host = TRUE, 1113 | }; 1114 | 1115 | static const Config host_complex1 = 1116 | { 1117 | .extra = TRUE, 1118 | .host = TRUE, 1119 | .host_flags = FLATPAK_HOST_COMMAND_FLAGS_CLEAR_ENV, 1120 | }; 1121 | 1122 | static const Config host_complex2 = 1123 | { 1124 | .awkward_command_name = TRUE, 1125 | .extra = TRUE, 1126 | .host = TRUE, 1127 | .host_flags = FLATPAK_HOST_COMMAND_FLAGS_WATCH_BUS, 1128 | }; 1129 | 1130 | static const Config fail_invalid_env = 1131 | { 1132 | .fails_immediately = 1, 1133 | .extra_arg = "--env=", 1134 | }; 1135 | 1136 | static const Config fail_invalid_env2 = 1137 | { 1138 | .fails_immediately = 1, 1139 | .extra_arg = "--env=NOPE", 1140 | }; 1141 | 1142 | static const Config fail_invalid_fd = 1143 | { 1144 | .fails_immediately = 1, 1145 | .extra_arg = "--forward-fd=", 1146 | }; 1147 | 1148 | static const Config fail_invalid_fd2 = 1149 | { 1150 | .fails_immediately = 1, 1151 | .extra_arg = "--forward-fd=yesplease", 1152 | }; 1153 | 1154 | /* TODO: These should fail, but succeed */ 1155 | #if 0 1156 | static const Config fail_invalid_fd3 = 1157 | { 1158 | .fails_immediately = 1, 1159 | .extra_arg = "--forward-fd=1a", 1160 | }; 1161 | 1162 | static const Config fail_invalid_fd4 = 1163 | { 1164 | .fails_immediately = 1, 1165 | .extra_arg = "--forward-fd=-1", 1166 | }; 1167 | #endif 1168 | 1169 | static const Config fail_invalid_sandbox_flag = 1170 | { 1171 | .fails_immediately = 1, 1172 | .extra_arg = "--sandbox-flag=tricolore", 1173 | }; 1174 | 1175 | static const Config fail_invalid_sandbox_flag2 = 1176 | { 1177 | .fails_immediately = 1, 1178 | .extra_arg = "--sandbox-flag=1e6", 1179 | }; 1180 | 1181 | static const Config fail_no_command = 1182 | { 1183 | .fails_immediately = 1, 1184 | .no_command = TRUE, 1185 | }; 1186 | 1187 | static const Config fail_no_session_bus = 1188 | { 1189 | .fails_immediately = 1, 1190 | .no_session_bus = TRUE, 1191 | }; 1192 | 1193 | static const Config fail_no_usr_path = 1194 | { 1195 | .fails_after_version_check = 1, 1196 | .usr_path = "", 1197 | }; 1198 | 1199 | static const Config fail_nonexistent_app_path = 1200 | { 1201 | .fails_after_version_check = 1, 1202 | .app_path = "/nonexistent", 1203 | }; 1204 | 1205 | static const Config fail_nonexistent_usr_path = 1206 | { 1207 | .fails_after_version_check = 1, 1208 | .usr_path = "/nonexistent", 1209 | }; 1210 | 1211 | static const Config host_cannot[] = 1212 | { 1213 | { .fails_immediately = 1, .host = TRUE, .extra_arg = "--expose-pids" }, 1214 | { .fails_immediately = 1, .host = TRUE, .extra_arg = "--latest-version" }, 1215 | { .fails_immediately = 1, .host = TRUE, .extra_arg = "--no-network" }, 1216 | { .fails_immediately = 1, .host = TRUE, .extra_arg = "--sandbox" }, 1217 | { .fails_immediately = 1, .host = TRUE, .extra_arg = "--sandbox-expose=/" }, 1218 | { .fails_immediately = 1, .host = TRUE, .extra_arg = "--sandbox-expose-path=/" }, 1219 | { .fails_immediately = 1, .host = TRUE, .extra_arg = "--sandbox-expose-path-ro=/" }, 1220 | { .fails_immediately = 1, .host = TRUE, .extra_arg = "--sandbox-expose-ro=/" }, 1221 | { .fails_immediately = 1, .host = TRUE, .extra_arg = "--sandbox-flag=1" }, 1222 | { .fails_immediately = 1, .host = TRUE, .extra_arg = "--share-pids" }, 1223 | { .fails_immediately = 1, .host = TRUE, .extra_arg = "--app-path=" }, 1224 | { .fails_immediately = 1, .host = TRUE, .extra_arg = "--app-path=/" }, 1225 | { .fails_immediately = 1, .host = TRUE, .extra_arg = "--usr-path=/" }, 1226 | }; 1227 | 1228 | int 1229 | main (int argc, 1230 | char **argv) 1231 | { 1232 | gsize i; 1233 | 1234 | g_test_init (&argc, &argv, NULL); 1235 | 1236 | g_test_add ("/help", Fixture, NULL, setup, test_help, teardown); 1237 | 1238 | g_test_add ("/host/simple", Fixture, &host_simple, setup, test_command, teardown); 1239 | g_test_add ("/host/complex1", Fixture, &host_complex1, setup, test_command, teardown); 1240 | g_test_add ("/host/complex2", Fixture, &host_complex2, setup, test_command, teardown); 1241 | g_test_add ("/host/fails", Fixture, &host_fails, setup, test_command, teardown); 1242 | 1243 | g_test_add ("/subsandbox/simple", Fixture, &default_config, setup, test_command, teardown); 1244 | g_test_add ("/subsandbox/clear-env", Fixture, &subsandbox_clear_env, setup, test_command, teardown); 1245 | g_test_add ("/subsandbox/complex", Fixture, &subsandbox_complex, setup, test_command, teardown); 1246 | g_test_add ("/subsandbox/empty_app", Fixture, &subsandbox_empty_app, setup, test_command, teardown); 1247 | g_test_add ("/subsandbox/expose-pids", Fixture, &subsandbox_expose_pids, setup, test_command, teardown); 1248 | g_test_add ("/subsandbox/fails", Fixture, &subsandbox_fails, setup, test_command, teardown); 1249 | g_test_add ("/subsandbox/latest", Fixture, &subsandbox_latest, setup, test_command, teardown); 1250 | g_test_add ("/subsandbox/no-net", Fixture, &subsandbox_no_net, setup, test_command, teardown); 1251 | g_test_add ("/subsandbox/sandbox/simple", Fixture, &subsandbox_sandbox_simple, setup, test_command, teardown); 1252 | g_test_add ("/subsandbox/sandbox/complex", Fixture, &subsandbox_sandbox_complex, setup, test_command, teardown); 1253 | g_test_add ("/subsandbox/share-pids", Fixture, &subsandbox_share_pids, setup, test_command, teardown); 1254 | g_test_add ("/subsandbox/watch-bus", Fixture, &subsandbox_watch_bus, setup, test_command, teardown); 1255 | 1256 | g_test_add ("/fail/invalid-env", Fixture, &fail_invalid_env, setup, test_command, teardown); 1257 | g_test_add ("/fail/invalid-env2", Fixture, &fail_invalid_env2, setup, test_command, teardown); 1258 | g_test_add ("/fail/invalid-fd", Fixture, &fail_invalid_fd, setup, test_command, teardown); 1259 | g_test_add ("/fail/invalid-fd2", Fixture, &fail_invalid_fd2, setup, test_command, teardown); 1260 | g_test_add ("/fail/invalid-sandbox-flag", Fixture, &fail_invalid_sandbox_flag, setup, test_command, teardown); 1261 | g_test_add ("/fail/invalid-sandbox-flag2", Fixture, &fail_invalid_sandbox_flag2, setup, test_command, teardown); 1262 | g_test_add ("/fail/no-command", Fixture, &fail_no_command, setup, test_command, teardown); 1263 | g_test_add ("/fail/no-session-bus", Fixture, &fail_no_session_bus, setup, test_command, teardown); 1264 | g_test_add ("/fail/no-usr-path", Fixture, &fail_no_usr_path, setup, test_command, teardown); 1265 | g_test_add ("/fail/nonexistent-app-path", Fixture, &fail_nonexistent_app_path, setup, test_command, teardown); 1266 | g_test_add ("/fail/nonexistent-usr-path", Fixture, &fail_nonexistent_usr_path, setup, test_command, teardown); 1267 | 1268 | for (i = 0; i < G_N_ELEMENTS (host_cannot); i++) 1269 | { 1270 | g_autofree gchar *name = g_strdup_printf ("/fail/host-cannot/%s", 1271 | host_cannot[i].extra_arg); 1272 | 1273 | g_strdelimit (name + strlen ("/fail/host-cannot/"), "/", 'x'); 1274 | 1275 | g_test_add (name, Fixture, &host_cannot[i], setup, test_command, teardown); 1276 | } 1277 | 1278 | return g_test_run (); 1279 | } 1280 | --------------------------------------------------------------------------------