├── .appveyor.yml ├── .git-blame-ignore-revs ├── .gitignore ├── .pre-commit-config.yaml ├── .prettierignore ├── .prettierrc.json ├── .prospector.yaml ├── AUTHORS ├── COPYING ├── ChangeLog.rst ├── cache.c ├── cache.h ├── commitlint.config.js ├── compat ├── darwin_compat.c ├── darwin_compat.h ├── fuse_opt.c └── fuse_opt.h ├── make_release_tarball.sh ├── meson.build ├── pdm.lock ├── pyproject.toml ├── readme.md ├── sshfs.c ├── sshfs.rst ├── test ├── .gitignore ├── Dockerfile ├── conftest.py ├── docker_test.sh ├── gesftpserver-git │ └── PKGBUILD ├── lint.sh ├── lsan_suppress.txt ├── meson.build ├── pytest.ini ├── test_sshfs.py ├── util.py └── wrong_command.c └── utils └── install_helper.sh /.appveyor.yml: -------------------------------------------------------------------------------- 1 | version: '{build}' 2 | 3 | install: 4 | # install WinFsp 5 | - appveyor DownloadFile https://github.com/billziss-gh/winfsp/releases/download/v1.4B2/winfsp-1.4.18211.msi 6 | - for %%f in ("winfsp-*.msi") do start /wait msiexec /i %%f /qn INSTALLLEVEL=1000 7 | 8 | # install FUSE for Cygwin (64-bit and 32-bit) 9 | - C:\cygwin64\bin\env.exe -i PATH=/bin bash "%ProgramFiles(x86)%\WinFsp\opt\cygfuse\install.sh" 10 | - C:\cygwin\bin\env.exe -i PATH=/bin bash "%ProgramFiles(x86)%\WinFsp\opt\cygfuse\install.sh" 11 | 12 | # install additional Cygwin packages (64-bit and 32-bit) 13 | - C:\cygwin64\setup-x86_64.exe -qnNdO -R C:\cygwin64 -s http://cygwin.mirror.constant.com -l C:\cygwin64\var\cache\setup -P libglib2.0-devel -P meson 14 | - C:\cygwin\setup-x86.exe -qnNdO -R C:\cygwin -s http://cygwin.mirror.constant.com -l C:\cygwin\var\cache\setup -P libglib2.0-devel -P meson 15 | 16 | build_script: 17 | - C:\cygwin64\bin\env.exe -i PATH=/bin bash test\appveyor-build.sh 18 | - C:\cygwin\bin\env.exe -i PATH=/bin bash test\appveyor-build.sh 19 | -------------------------------------------------------------------------------- /.git-blame-ignore-revs: -------------------------------------------------------------------------------- 1 | d54c7ecbd618afb4df524e0d96dec7fe7cc2935d 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # 2 | # NOTE! Don't add files that are generated in specific 3 | # subdirectories here. Add them in the ".gitignore" file 4 | # in that subdirectory instead. 5 | # 6 | # NOTE! Please use 'git ls-files -i --exclude-standard' 7 | # command after changing this file, to see if there are 8 | # any tracked files which get ignored after the change. 9 | *.o 10 | *.lo 11 | *.la 12 | *.gz 13 | \#*# 14 | *.orig 15 | *~ 16 | Makefile.in 17 | Makefile 18 | *.m4 19 | stamp-h* 20 | config.* 21 | sshfs.1 22 | /sshfs 23 | /ltmain.sh 24 | /configure 25 | /install-sh 26 | /mkinstalldirs 27 | /missing 28 | /*.cache 29 | /depcomp 30 | /compile 31 | /libtool 32 | /INSTALL 33 | /.pc 34 | /patches 35 | /m4 36 | .deps/ 37 | /build 38 | 39 | # PDM 40 | /.pdm-python 41 | 42 | # Python Library 43 | /__pypackages__/ 44 | 45 | # Test directories 46 | /scratch/ 47 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | fail_fast: true 2 | repos: 3 | - repo: https://github.com/pre-commit/pre-commit-hooks 4 | rev: v4.4.0 5 | hooks: 6 | - id: check-added-large-files 7 | args: ['--maxkb=500'] 8 | - id: check-case-conflict 9 | - id: check-merge-conflict 10 | - id: check-symlinks 11 | - id: check-yaml 12 | - id: debug-statements 13 | - id: detect-private-key 14 | - id: end-of-file-fixer 15 | - id: fix-byte-order-marker 16 | - id: trailing-whitespace 17 | exclude: '\.patch$' 18 | - repo: https://github.com/PyCQA/isort 19 | rev: 5.12.0 20 | hooks: 21 | - id: isort 22 | - repo: https://github.com/PyCQA/prospector 23 | rev: v1.9.0 24 | hooks: 25 | - id: prospector 26 | - repo: https://github.com/psf/black 27 | rev: 23.1.0 28 | hooks: 29 | - id: black 30 | - repo: https://github.com/pre-commit/mirrors-prettier 31 | rev: v2.7.1 32 | hooks: 33 | - id: prettier 34 | stages: [commit] 35 | - repo: https://github.com/alessandrojcm/commitlint-pre-commit-hook 36 | rev: v9.4.0 37 | hooks: 38 | - id: commitlint 39 | stages: [commit-msg] 40 | additional_dependencies: 41 | - '@commitlint/config-conventional' 42 | - 'conventional-changelog-conventionalcommits' 43 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | *.md 2 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "tabWidth": 2, 4 | "semi": false, 5 | "singleQuote": true, 6 | "printWidth": 120 7 | } 8 | -------------------------------------------------------------------------------- /.prospector.yaml: -------------------------------------------------------------------------------- 1 | output-format: text 2 | 3 | strictness: veryhigh 4 | test-warnings: true 5 | doc-warnings: false 6 | member-warnings: true 7 | 8 | uses: 9 | 10 | pycodestyle: 11 | full: true 12 | disable: 13 | - D100 14 | - D101 15 | - D102 16 | - D103 17 | - D105 18 | - D205 19 | - D400 20 | - N802 # function name should be lowercase, breaks on tests 21 | options: 22 | max-line-length: 120 23 | 24 | pyflakes: 25 | disable: 26 | - F999 27 | 28 | pylint: 29 | disable: 30 | - invalid-name 31 | - no-member 32 | - no-self-use 33 | - too-few-public-methods 34 | - too-many-ancestors 35 | - too-many-arguments 36 | options: 37 | max-line-length: 120 38 | 39 | mccabe: 40 | disable: 41 | - MC0001 42 | 43 | dodgy: 44 | run: true 45 | 46 | ignore-paths: 47 | 48 | ignore-patterns: 49 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Current Maintainer 2 | ------------------ 3 | 4 | None. 5 | 6 | 7 | Past Maintainers 8 | ---------------- 9 | 10 | Nikolaus Rath (until 05/2022) 11 | Miklos Szeredi (until 12/2015) 12 | 13 | 14 | Contributors (autogenerated list) 15 | --------------------------------- 16 | 17 | a1346054 <36859588+a1346054@users.noreply.github.com> 18 | Alan Jenkins 19 | Alexander Neumann 20 | Anatol Pomozov 21 | Andrew Stone 22 | Antonio Rojas 23 | Benjamin Fleischer 24 | Berserker 25 | Bill Zissimopoulos 26 | bjoe2k4 27 | Brandon Carter 28 | Cam Cope 29 | Chris Wolfe 30 | Clayton G. Hobbs 31 | Daniel Lublin 32 | Dominique Martinet 33 | DrDaveD <2129743+DrDaveD@users.noreply.github.com> 34 | Fabrice Fontaine 35 | gala 36 | Galen Getsov <4815620+ggetsov@users.noreply.github.com> 37 | George Vlahavas 38 | G.raud Meyer 39 | harrim4n 40 | Jakub Jelen 41 | jeg139 <54814784+jeg139@users.noreply.github.com> 42 | Josh Triplett 43 | Julio Merino 44 | Julio Merino 45 | Junichi Uekawa 46 | Junichi Uekawa 47 | kalvdans 48 | Kim Brose 49 | Matthew Berginski 50 | Michael Forney 51 | Mike Kelly 52 | Mike Salvatore 53 | Miklos Szeredi 54 | Miklos Szeredi 55 | mssalvatore 56 | Nikolaus Rath 57 | Percy Jahn 58 | Peter Belm 59 | Peter Wienemann 60 | Qais Patankar 61 | Quentin Rameau 62 | Reid Wagner 63 | Rian Hunter 64 | Rian Hunter 65 | Samuel Murray 66 | S. D. Cloudt 67 | Simon Arlott <70171+nomis@users.noreply.github.com> 68 | smheidrich 69 | sunwire <50745572+sunwire@users.noreply.github.com> 70 | Tim Harder 71 | Timo Savola 72 | tpoindessous 73 | Viktor Szakats 74 | Zoltan Kuscsik 75 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 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 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | 294 | Copyright (C) 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | , 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. 340 | -------------------------------------------------------------------------------- /ChangeLog.rst: -------------------------------------------------------------------------------- 1 | Release 3.7.3 (2022-05-26) 2 | -------------------------- 3 | 4 | * Minor bugfixes. 5 | 6 | * This is the last release from the current maintainer. SSHFS is now no longer maintained 7 | or developed. Github issue tracking and pull requests have therefore been disabled. The 8 | mailing list (see below) is still available for use. 9 | 10 | If you would like to take over this project, you are welcome to do so. Please fork it 11 | and develop the fork for a while. Once there has been 6 months of reasonable activity, 12 | please contact Nikolaus@rath.org and I'll be happy to give you ownership of this 13 | repository or replace with a pointer to the fork. 14 | 15 | 16 | Release 3.7.2 (2021-06-08) 17 | -------------------------- 18 | 19 | * Added a secondary check so if a mkdir request fails with EPERM an access request will be 20 | tried - returning EEXIST if the access was successful. 21 | Fixes: https://github.com/libfuse/sshfs/issues/243 22 | 23 | 24 | Release 3.7.1 (2020-11-09) 25 | -------------------------- 26 | 27 | * Minor bugfixes. 28 | 29 | 30 | Release 3.7.0 (2020-01-03) 31 | -------------------------- 32 | 33 | * New max_conns option enables the use of multiple connections to improve responsiveness 34 | during large file transfers. Thanks to Timo Savola for doing most of the implementation 35 | work, and thanks to CEA.fr for sponsoring remaining bugfixes and cleanups! 36 | 37 | * The `buflimit` workaround is now disabled by default. The corresponding bug in OpenSSH 38 | has been fixed in 2007 39 | (cf. https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=365541#37), so this shouldn't be 40 | needed anymore. If you depend on this workaround, please let the SSHFS maintainers know, 41 | otherwise support for the workaround will be removed completely in a future version. 42 | 43 | 44 | Release 3.6.0 (2019-11-03) 45 | -------------------------- 46 | 47 | * Added "-o direct_io" option. 48 | This option disables the use of page cache in kernel. 49 | This is useful for example if the file size is not known before reading it. 50 | For example if you mount /proc dir from a remote host without the direct_io 51 | option, the read always will return zero bytes instead of actual data. 52 | * Added --verbose option. 53 | * Fixed a number of compiler warnings. 54 | * Improved performance under OS X. 55 | 56 | 57 | Release 3.5.2 (2019-04-13) 58 | -------------------------- 59 | 60 | * Fixed "-o idmap=user" to map both UID and GID on all OSs. 61 | * Fixed improper handling of sequential spaces spaces in "ssh_command" option 62 | 63 | Release 3.5.1 (2018-12-22) 64 | -------------------------- 65 | 66 | * Documentation updates 67 | * Build system updates 68 | * Added "BindInterface" as valid "-o" option. 69 | 70 | Release 3.5.0 (2018-08-28) 71 | -------------------------- 72 | 73 | * Fixed error code returned by rename(), allowing proper fallback. 74 | * Port to Cygwin. 75 | 76 | Release 3.4.0 (2018-06-29) 77 | -------------------------- 78 | 79 | * Make utimens(NULL) result in timestamp "now" -- no more touched files 80 | dated 1970-01-01 81 | * New `createmode` workaround. 82 | * Fix `fstat` workaround regression. 83 | 84 | Release 3.3.2 (2018-04-29) 85 | -------------------------- 86 | 87 | * New `renamexdev` workaround. 88 | 89 | Release 3.3.1 (2017-10-25) 90 | -------------------------- 91 | 92 | * Manpage is now installed in correct directory. 93 | * SSHFS now supports (or rather: ignores) some options that it may 94 | receive as result of being mounted from ``/etc/mtab``. This includes 95 | things like ``user``, ``netdev``, or ``auto``. 96 | 97 | SSHFS 3.3.0 (2017-09-20) 98 | ------------------------ 99 | 100 | * Dropped support for writeback caching (and, as a consequence, 101 | "unreliable append" operation). As of kernel 4.14, the FUSE module's 102 | writeback implementation is not compatible with network filesystems 103 | and there are no imminent plans to change that. 104 | * Add support for mounting from /etc/fstab 105 | * Dropped support for building with autotools. 106 | * Added missing options to man page. 107 | 108 | Release 3.2.0 (2017-08-06) 109 | -------------------------- 110 | 111 | * Re-enabled writeback cache. 112 | * SSHFS now supports O_APPEND. 113 | 114 | Release 3.1.0 (2017-08-04) 115 | -------------------------- 116 | 117 | * Temporarily disabled the writeback cache feature, since there 118 | have been reports of dataloss when appending to files when 119 | writeback caching is enabled. 120 | 121 | * Fixed a crash due to a race condition when listing 122 | directory contents. 123 | 124 | * For improved backwards compatibility, SSHFS now also silently 125 | accepts the old ``-o cache_*`` options. 126 | 127 | Release 3.0.0 (2017-07-08) 128 | -------------------------- 129 | 130 | * sshfs now requires libfuse 3.1.0 or newer. 131 | * When supported by the kernel, sshfs now uses writeback caching. 132 | * The `cache` option has been renamed to `dir_cache` for clarity. 133 | * Added unit tests 134 | * --debug now behaves like -o debug_sshfs, i.e. it enables sshfs 135 | debugging messages rather than libfuse debugging messages. 136 | * Documented limited hardlink support. 137 | * Added support for building with Meson. 138 | * Added support for more SSH options. 139 | * Dropped support for the *nodelay* workaround - the last OpenSSH 140 | version for which this was useful was released in 2006. 141 | * Dropped support for the *nodelaysrv* workaround. The same effect 142 | (enabling NODELAY on the server side *and* enabling X11 forwarding) 143 | can be achieved by explicitly passing `-o ForwardX11` 144 | * Removed support for `-o workaround=all`. Workarounds should always 145 | enabled explicitly and only when needed. There is no point in always 146 | enabling a potentially changing set of workarounds. 147 | 148 | Release 2.9 (2017-04-17) 149 | ------------------------ 150 | 151 | * Improved support for Cygwin. 152 | * Various small bugfixes. 153 | 154 | Release 2.8 (2016-06-22) 155 | ------------------------ 156 | 157 | * Added support for the "fsync" extension. 158 | * Fixed a build problem with bitbake 159 | 160 | Release 2.7 (2016-03-01) 161 | ------------------------ 162 | 163 | * Integrated osxfuse's copy of sshfs, which means that sshfs now works 164 | on OS X out of the box. 165 | * Added -o cache_max_size=N option to let users tune the maximum size of 166 | the cache in number of entries. 167 | * Added -o cache_clean_interval=N and -o cache_min_clean_interval=N 168 | options to let users tune the cleaning behavior of the cache. 169 | 170 | Release 2.6 (2015-01-28) 171 | ------------------------ 172 | 173 | * New maintainer (Nikolaus Rath ) 174 | 175 | Release 2.5 (2014-01-14) 176 | ------------------------ 177 | 178 | * Some performance improvements for large directories. 179 | * New `disable_hardlink` option. 180 | * Various small bugfixes. 181 | 182 | Release 2.4 (2012-03-08) 183 | ------------------------ 184 | 185 | * New `slave` option. 186 | * New `idmap`, `uidmap` and `gidmap` options. 187 | * Various small bugfixes. 188 | 189 | Release 2.3 (2011-07-01) 190 | ------------------------ 191 | 192 | * Support hard link creation if server is OpenSSH 5.7 or later 193 | * Small improvements and bug fixes 194 | * Check mount point and options before connecting to ssh server 195 | * New 'delay_connect' option 196 | 197 | Release 2.2 (2008-10-20) 198 | ------------------------ 199 | 200 | * Handle numerical IPv6 addresses enclosed in square brackets 201 | * Handle commas in usernames 202 | 203 | Release 2.1 (2008-07-11) 204 | ------------------------ 205 | 206 | * Small improvements and bug fixes 207 | 208 | Release 2.0 (2008-04-23) 209 | ------------------------ 210 | 211 | * Support password authentication with pam_mount 212 | 213 | * Support atomic renames if server is OpenSSH 4.9 or later 214 | 215 | * Support getting disk usage if server is OpenSSH 5.1 or later 216 | 217 | * Small enhancements and bug fixes 218 | 219 | What is new in 1.9 220 | ------------------ 221 | 222 | * Fix a serious bug, that could result in sshfs hanging, crashing, or 223 | reporting out-of-memory 224 | 225 | What is new in 1.8 226 | ------------------ 227 | 228 | * Bug fixes 229 | 230 | What is new in 1.7 231 | ------------------ 232 | 233 | * Tolerate servers which print a banner on login 234 | 235 | * Small improvements 236 | 237 | What is new in 1.6 238 | ------------------ 239 | 240 | * Workaround for missing truncate operation on old sftp servers 241 | 242 | * Bug fixes 243 | 244 | What is new in 1.5 245 | ------------------ 246 | 247 | * Improvements to read performance. Now both read and write 248 | throughput should be very close to 'scp' 249 | 250 | * If used with FUSE 2.6.0 or later, then perform better data caching. 251 | This should show dramatic speed improvements when a file is opened 252 | more than once 253 | 254 | * Bug fixes 255 | 256 | What is new in 1.4 257 | ------------------ 258 | 259 | * Updated to version 25 of libfuse API 260 | 261 | * This means that the 'cp' of readonly file to sshfs bug is finally 262 | solved (as long as using libfuse 2.5.0 or later *and* Linux 2.6.15 263 | or later) 264 | 265 | * Sshfs now works on FreeBSD 266 | 267 | * Added option to "transform" absolute symbolic links 268 | 269 | What is new in 1.3 270 | ------------------ 271 | 272 | * Add workaround for failure to rename to an existing file 273 | 274 | * Simple user ID mapping 275 | 276 | * Estimate disk usage of files based on size 277 | 278 | * Report "infinite" disk space 279 | 280 | * Bug fixes 281 | 282 | What is new in 1.2 283 | ------------------ 284 | 285 | * Better compatibility with different sftp servers 286 | 287 | * Automatic reconnect (optional) 288 | 289 | What is new in 1.1 290 | ------------------ 291 | 292 | * Performance improvements: 293 | 294 | - directory content caching 295 | 296 | - symlink caching 297 | 298 | - asynchronous writeback 299 | 300 | - readahead 301 | 302 | * Fixed '-p' option 303 | 304 | What is new in 1.0 305 | ------------------ 306 | 307 | * Initial release 308 | -------------------------------------------------------------------------------- /cache.c: -------------------------------------------------------------------------------- 1 | /* 2 | Caching file system proxy 3 | Copyright (C) 2004 Miklos Szeredi 4 | 5 | This program can be distributed under the terms of the GNU GPL. 6 | See the file COPYING. 7 | */ 8 | 9 | #include "cache.h" 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | 18 | #define DEFAULT_CACHE_TIMEOUT_SECS 20 19 | #define DEFAULT_MAX_CACHE_SIZE 10000 20 | #define DEFAULT_CACHE_CLEAN_INTERVAL_SECS 60 21 | #define DEFAULT_MIN_CACHE_CLEAN_INTERVAL_SECS 5 22 | 23 | struct cache { 24 | int on; 25 | unsigned int stat_timeout_secs; 26 | unsigned int dir_timeout_secs; 27 | unsigned int link_timeout_secs; 28 | unsigned int max_size; 29 | unsigned int clean_interval_secs; 30 | unsigned int min_clean_interval_secs; 31 | struct fuse_operations *next_oper; 32 | GHashTable *table; 33 | pthread_mutex_t lock; 34 | time_t last_cleaned; 35 | uint64_t write_ctr; 36 | }; 37 | 38 | static struct cache cache; 39 | 40 | struct node { 41 | struct stat stat; 42 | time_t stat_valid; 43 | char **dir; 44 | time_t dir_valid; 45 | char *link; 46 | time_t link_valid; 47 | time_t valid; 48 | }; 49 | 50 | struct readdir_handle { 51 | const char *path; 52 | void *buf; 53 | fuse_fill_dir_t filler; 54 | GPtrArray *dir; 55 | uint64_t wrctr; 56 | }; 57 | 58 | struct file_handle { 59 | /* Did we send an open request to the underlying fs? */ 60 | int is_open; 61 | 62 | /* If so, this will hold its handle */ 63 | unsigned long fs_fh; 64 | }; 65 | 66 | static void free_node(gpointer node_) 67 | { 68 | struct node *node = (struct node *) node_; 69 | g_strfreev(node->dir); 70 | g_free(node); 71 | } 72 | 73 | static int cache_clean_entry(void *key_, struct node *node, time_t *now) 74 | { 75 | (void) key_; 76 | if (*now > node->valid) 77 | return TRUE; 78 | else 79 | return FALSE; 80 | } 81 | 82 | static void cache_clean(void) 83 | { 84 | time_t now = time(NULL); 85 | if (now > cache.last_cleaned + cache.min_clean_interval_secs && 86 | (g_hash_table_size(cache.table) > cache.max_size || 87 | now > cache.last_cleaned + cache.clean_interval_secs)) { 88 | g_hash_table_foreach_remove(cache.table, 89 | (GHRFunc) cache_clean_entry, &now); 90 | cache.last_cleaned = now; 91 | } 92 | } 93 | 94 | static struct node *cache_lookup(const char *path) 95 | { 96 | return (struct node *) g_hash_table_lookup(cache.table, path); 97 | } 98 | 99 | static void cache_purge(const char *path) 100 | { 101 | g_hash_table_remove(cache.table, path); 102 | } 103 | 104 | static void cache_purge_parent(const char *path) 105 | { 106 | const char *s = strrchr(path, '/'); 107 | if (s) { 108 | if (s == path) 109 | g_hash_table_remove(cache.table, "/"); 110 | else { 111 | char *parent = g_strndup(path, s - path); 112 | cache_purge(parent); 113 | g_free(parent); 114 | } 115 | } 116 | } 117 | 118 | void cache_invalidate(const char *path) 119 | { 120 | pthread_mutex_lock(&cache.lock); 121 | cache_purge(path); 122 | pthread_mutex_unlock(&cache.lock); 123 | } 124 | 125 | static void cache_invalidate_write(const char *path) 126 | { 127 | pthread_mutex_lock(&cache.lock); 128 | cache_purge(path); 129 | cache.write_ctr++; 130 | pthread_mutex_unlock(&cache.lock); 131 | } 132 | 133 | static void cache_invalidate_dir(const char *path) 134 | { 135 | pthread_mutex_lock(&cache.lock); 136 | cache_purge(path); 137 | cache_purge_parent(path); 138 | pthread_mutex_unlock(&cache.lock); 139 | } 140 | 141 | static int cache_del_children(const char *key, void *val_, const char *path) 142 | { 143 | (void) val_; 144 | if (strncmp(key, path, strlen(path)) == 0) 145 | return TRUE; 146 | else 147 | return FALSE; 148 | } 149 | 150 | static void cache_do_rename(const char *from, const char *to) 151 | { 152 | pthread_mutex_lock(&cache.lock); 153 | g_hash_table_foreach_remove(cache.table, (GHRFunc) cache_del_children, 154 | (char *) from); 155 | cache_purge(from); 156 | cache_purge(to); 157 | cache_purge_parent(from); 158 | cache_purge_parent(to); 159 | pthread_mutex_unlock(&cache.lock); 160 | } 161 | 162 | static struct node *cache_get(const char *path) 163 | { 164 | struct node *node = cache_lookup(path); 165 | if (node == NULL) { 166 | char *pathcopy = g_strdup(path); 167 | node = g_new0(struct node, 1); 168 | g_hash_table_insert(cache.table, pathcopy, node); 169 | } 170 | return node; 171 | } 172 | 173 | void cache_add_attr(const char *path, const struct stat *stbuf, uint64_t wrctr) 174 | { 175 | struct node *node; 176 | 177 | pthread_mutex_lock(&cache.lock); 178 | if (wrctr == cache.write_ctr) { 179 | node = cache_get(path); 180 | node->stat = *stbuf; 181 | node->stat_valid = time(NULL) + cache.stat_timeout_secs; 182 | if (node->stat_valid > node->valid) 183 | node->valid = node->stat_valid; 184 | cache_clean(); 185 | } 186 | pthread_mutex_unlock(&cache.lock); 187 | } 188 | 189 | static void cache_add_dir(const char *path, char **dir) 190 | { 191 | struct node *node; 192 | 193 | pthread_mutex_lock(&cache.lock); 194 | node = cache_get(path); 195 | g_strfreev(node->dir); 196 | node->dir = dir; 197 | node->dir_valid = time(NULL) + cache.dir_timeout_secs; 198 | if (node->dir_valid > node->valid) 199 | node->valid = node->dir_valid; 200 | cache_clean(); 201 | pthread_mutex_unlock(&cache.lock); 202 | } 203 | 204 | static size_t my_strnlen(const char *s, size_t maxsize) 205 | { 206 | const char *p; 207 | for (p = s; maxsize && *p; maxsize--, p++); 208 | return p - s; 209 | } 210 | 211 | static void cache_add_link(const char *path, const char *link, size_t size) 212 | { 213 | struct node *node; 214 | 215 | pthread_mutex_lock(&cache.lock); 216 | node = cache_get(path); 217 | g_free(node->link); 218 | node->link = g_strndup(link, my_strnlen(link, size-1)); 219 | node->link_valid = time(NULL) + cache.link_timeout_secs; 220 | if (node->link_valid > node->valid) 221 | node->valid = node->link_valid; 222 | cache_clean(); 223 | pthread_mutex_unlock(&cache.lock); 224 | } 225 | 226 | static int cache_get_attr(const char *path, struct stat *stbuf) 227 | { 228 | struct node *node; 229 | int err = -EAGAIN; 230 | pthread_mutex_lock(&cache.lock); 231 | node = cache_lookup(path); 232 | if (node != NULL) { 233 | time_t now = time(NULL); 234 | if (node->stat_valid - now >= 0) { 235 | *stbuf = node->stat; 236 | err = 0; 237 | } 238 | } 239 | pthread_mutex_unlock(&cache.lock); 240 | return err; 241 | } 242 | 243 | uint64_t cache_get_write_ctr(void) 244 | { 245 | uint64_t res; 246 | 247 | pthread_mutex_lock(&cache.lock); 248 | res = cache.write_ctr; 249 | pthread_mutex_unlock(&cache.lock); 250 | 251 | return res; 252 | } 253 | 254 | static void *cache_init(struct fuse_conn_info *conn, 255 | struct fuse_config *cfg) 256 | { 257 | void *res; 258 | res = cache.next_oper->init(conn, cfg); 259 | 260 | // Cache requires a path for each request 261 | cfg->nullpath_ok = 0; 262 | 263 | return res; 264 | } 265 | 266 | static int cache_getattr(const char *path, struct stat *stbuf, 267 | struct fuse_file_info *fi) 268 | { 269 | int err = cache_get_attr(path, stbuf); 270 | if (err) { 271 | uint64_t wrctr = cache_get_write_ctr(); 272 | err = cache.next_oper->getattr(path, stbuf, fi); 273 | if (!err) 274 | cache_add_attr(path, stbuf, wrctr); 275 | } 276 | return err; 277 | } 278 | 279 | static int cache_readlink(const char *path, char *buf, size_t size) 280 | { 281 | struct node *node; 282 | int err; 283 | 284 | pthread_mutex_lock(&cache.lock); 285 | node = cache_lookup(path); 286 | if (node != NULL) { 287 | time_t now = time(NULL); 288 | if (node->link_valid - now >= 0) { 289 | strncpy(buf, node->link, size-1); 290 | buf[size-1] = '\0'; 291 | pthread_mutex_unlock(&cache.lock); 292 | return 0; 293 | } 294 | } 295 | pthread_mutex_unlock(&cache.lock); 296 | err = cache.next_oper->readlink(path, buf, size); 297 | if (!err) 298 | cache_add_link(path, buf, size); 299 | 300 | return err; 301 | } 302 | 303 | 304 | static int cache_opendir(const char *path, struct fuse_file_info *fi) 305 | { 306 | (void) path; 307 | struct file_handle *cfi; 308 | 309 | cfi = malloc(sizeof(struct file_handle)); 310 | if(cfi == NULL) 311 | return -ENOMEM; 312 | cfi->is_open = 0; 313 | fi->fh = (unsigned long) cfi; 314 | return 0; 315 | } 316 | 317 | static int cache_releasedir(const char *path, struct fuse_file_info *fi) 318 | { 319 | int err; 320 | struct file_handle *cfi; 321 | 322 | cfi = (struct file_handle*) fi->fh; 323 | 324 | if(cfi->is_open) { 325 | fi->fh = cfi->fs_fh; 326 | err = cache.next_oper->releasedir(path, fi); 327 | } else 328 | err = 0; 329 | 330 | free(cfi); 331 | return err; 332 | } 333 | 334 | static int cache_dirfill (void *buf, const char *name, 335 | const struct stat *stbuf, off_t off, 336 | enum fuse_fill_dir_flags flags) 337 | { 338 | int err; 339 | struct readdir_handle *ch; 340 | 341 | ch = (struct readdir_handle*) buf; 342 | err = ch->filler(ch->buf, name, stbuf, off, flags); 343 | if (!err) { 344 | g_ptr_array_add(ch->dir, g_strdup(name)); 345 | if (stbuf->st_mode & S_IFMT) { 346 | char *fullpath; 347 | const char *basepath = !ch->path[1] ? "" : ch->path; 348 | 349 | fullpath = g_strdup_printf("%s/%s", basepath, name); 350 | cache_add_attr(fullpath, stbuf, ch->wrctr); 351 | g_free(fullpath); 352 | } 353 | } 354 | return err; 355 | } 356 | 357 | static int cache_readdir(const char *path, void *buf, fuse_fill_dir_t filler, 358 | off_t offset, struct fuse_file_info *fi, 359 | enum fuse_readdir_flags flags) 360 | { 361 | struct readdir_handle ch; 362 | struct file_handle *cfi; 363 | int err; 364 | char **dir; 365 | struct node *node; 366 | 367 | assert(offset == 0); 368 | 369 | pthread_mutex_lock(&cache.lock); 370 | node = cache_lookup(path); 371 | if (node != NULL && node->dir != NULL) { 372 | time_t now = time(NULL); 373 | if (node->dir_valid - now >= 0) { 374 | for(dir = node->dir; *dir != NULL; dir++) 375 | // FIXME: What about st_mode? 376 | filler(buf, *dir, NULL, 0, 0); 377 | pthread_mutex_unlock(&cache.lock); 378 | return 0; 379 | } 380 | } 381 | pthread_mutex_unlock(&cache.lock); 382 | 383 | cfi = (struct file_handle*) fi->fh; 384 | if(cfi->is_open) 385 | fi->fh = cfi->fs_fh; 386 | else { 387 | if(cache.next_oper->opendir) { 388 | err = cache.next_oper->opendir(path, fi); 389 | if(err) 390 | return err; 391 | } 392 | cfi->is_open = 1; 393 | cfi->fs_fh = fi->fh; 394 | } 395 | 396 | ch.path = path; 397 | ch.buf = buf; 398 | ch.filler = filler; 399 | ch.dir = g_ptr_array_new(); 400 | ch.wrctr = cache_get_write_ctr(); 401 | err = cache.next_oper->readdir(path, &ch, cache_dirfill, offset, fi, flags); 402 | g_ptr_array_add(ch.dir, NULL); 403 | dir = (char **) ch.dir->pdata; 404 | if (!err) { 405 | cache_add_dir(path, dir); 406 | } else { 407 | g_strfreev(dir); 408 | } 409 | g_ptr_array_free(ch.dir, FALSE); 410 | 411 | return err; 412 | } 413 | 414 | static int cache_mknod(const char *path, mode_t mode, dev_t rdev) 415 | { 416 | int err = cache.next_oper->mknod(path, mode, rdev); 417 | if (!err) 418 | cache_invalidate_dir(path); 419 | return err; 420 | } 421 | 422 | static int cache_mkdir(const char *path, mode_t mode) 423 | { 424 | int err = cache.next_oper->mkdir(path, mode); 425 | if (!err) 426 | cache_invalidate_dir(path); 427 | return err; 428 | } 429 | 430 | static int cache_unlink(const char *path) 431 | { 432 | int err = cache.next_oper->unlink(path); 433 | if (!err) 434 | cache_invalidate_dir(path); 435 | return err; 436 | } 437 | 438 | static int cache_rmdir(const char *path) 439 | { 440 | int err = cache.next_oper->rmdir(path); 441 | if (!err) 442 | cache_invalidate_dir(path); 443 | return err; 444 | } 445 | 446 | static int cache_symlink(const char *from, const char *to) 447 | { 448 | int err = cache.next_oper->symlink(from, to); 449 | if (!err) 450 | cache_invalidate_dir(to); 451 | return err; 452 | } 453 | 454 | static int cache_rename(const char *from, const char *to, unsigned int flags) 455 | { 456 | int err = cache.next_oper->rename(from, to, flags); 457 | if (!err) 458 | cache_do_rename(from, to); 459 | return err; 460 | } 461 | 462 | static int cache_link(const char *from, const char *to) 463 | { 464 | int err = cache.next_oper->link(from, to); 465 | if (!err) { 466 | cache_invalidate(from); 467 | cache_invalidate_dir(to); 468 | } 469 | return err; 470 | } 471 | 472 | static int cache_chmod(const char *path, mode_t mode, 473 | struct fuse_file_info *fi) 474 | { 475 | int err = cache.next_oper->chmod(path, mode, fi); 476 | if (!err) 477 | cache_invalidate(path); 478 | return err; 479 | } 480 | 481 | static int cache_chown(const char *path, uid_t uid, gid_t gid, 482 | struct fuse_file_info *fi) 483 | { 484 | int err = cache.next_oper->chown(path, uid, gid, fi); 485 | if (!err) 486 | cache_invalidate(path); 487 | return err; 488 | } 489 | 490 | static int cache_utimens(const char *path, const struct timespec tv[2], 491 | struct fuse_file_info *fi) 492 | { 493 | int err = cache.next_oper->utimens(path, tv, fi); 494 | if (!err) 495 | cache_invalidate(path); 496 | return err; 497 | } 498 | 499 | static int cache_write(const char *path, const char *buf, size_t size, 500 | off_t offset, struct fuse_file_info *fi) 501 | { 502 | int res = cache.next_oper->write(path, buf, size, offset, fi); 503 | if (res >= 0) 504 | cache_invalidate_write(path); 505 | return res; 506 | } 507 | 508 | static int cache_create(const char *path, mode_t mode, 509 | struct fuse_file_info *fi) 510 | { 511 | int err = cache.next_oper->create(path, mode, fi); 512 | if (!err) 513 | cache_invalidate_dir(path); 514 | return err; 515 | } 516 | 517 | static int cache_truncate(const char *path, off_t size, 518 | struct fuse_file_info *fi) 519 | { 520 | int err = cache.next_oper->truncate(path, size, fi); 521 | if (!err) 522 | cache_invalidate(path); 523 | return err; 524 | } 525 | 526 | static void cache_fill(struct fuse_operations *oper, 527 | struct fuse_operations *cache_oper) 528 | { 529 | cache_oper->access = oper->access; 530 | cache_oper->chmod = oper->chmod ? cache_chmod : NULL; 531 | cache_oper->chown = oper->chown ? cache_chown : NULL; 532 | cache_oper->create = oper->create ? cache_create : NULL; 533 | cache_oper->flush = oper->flush; 534 | cache_oper->fsync = oper->fsync; 535 | cache_oper->getattr = oper->getattr ? cache_getattr : NULL; 536 | cache_oper->getxattr = oper->getxattr; 537 | cache_oper->init = cache_init; 538 | cache_oper->link = oper->link ? cache_link : NULL; 539 | cache_oper->listxattr = oper->listxattr; 540 | cache_oper->mkdir = oper->mkdir ? cache_mkdir : NULL; 541 | cache_oper->mknod = oper->mknod ? cache_mknod : NULL; 542 | cache_oper->open = oper->open; 543 | cache_oper->opendir = cache_opendir; 544 | cache_oper->read = oper->read; 545 | cache_oper->readdir = oper->readdir ? cache_readdir : NULL; 546 | cache_oper->readlink = oper->readlink ? cache_readlink : NULL; 547 | cache_oper->release = oper->release; 548 | cache_oper->releasedir = cache_releasedir; 549 | cache_oper->removexattr = oper->removexattr; 550 | cache_oper->rename = oper->rename ? cache_rename : NULL; 551 | cache_oper->rmdir = oper->rmdir ? cache_rmdir : NULL; 552 | cache_oper->setxattr = oper->setxattr; 553 | cache_oper->statfs = oper->statfs; 554 | cache_oper->symlink = oper->symlink ? cache_symlink : NULL; 555 | cache_oper->truncate = oper->truncate ? cache_truncate : NULL; 556 | cache_oper->unlink = oper->unlink ? cache_unlink : NULL; 557 | cache_oper->utimens = oper->utimens ? cache_utimens : NULL; 558 | cache_oper->write = oper->write ? cache_write : NULL; 559 | } 560 | 561 | struct fuse_operations *cache_wrap(struct fuse_operations *oper) 562 | { 563 | static struct fuse_operations cache_oper; 564 | cache.next_oper = oper; 565 | 566 | cache_fill(oper, &cache_oper); 567 | pthread_mutex_init(&cache.lock, NULL); 568 | cache.table = g_hash_table_new_full(g_str_hash, g_str_equal, 569 | g_free, free_node); 570 | if (cache.table == NULL) { 571 | fprintf(stderr, "failed to create cache\n"); 572 | return NULL; 573 | } 574 | return &cache_oper; 575 | } 576 | 577 | static const struct fuse_opt cache_opts[] = { 578 | { "dcache_timeout=%u", offsetof(struct cache, stat_timeout_secs), 0 }, 579 | { "dcache_timeout=%u", offsetof(struct cache, dir_timeout_secs), 0 }, 580 | { "dcache_timeout=%u", offsetof(struct cache, link_timeout_secs), 0 }, 581 | { "dcache_stat_timeout=%u", offsetof(struct cache, stat_timeout_secs), 0 }, 582 | { "dcache_dir_timeout=%u", offsetof(struct cache, dir_timeout_secs), 0 }, 583 | { "dcache_link_timeout=%u", offsetof(struct cache, link_timeout_secs), 0 }, 584 | { "dcache_max_size=%u", offsetof(struct cache, max_size), 0 }, 585 | { "dcache_clean_interval=%u", offsetof(struct cache, 586 | clean_interval_secs), 0 }, 587 | { "dcache_min_clean_interval=%u", offsetof(struct cache, 588 | min_clean_interval_secs), 0 }, 589 | 590 | /* For backwards compatibility */ 591 | { "cache_timeout=%u", offsetof(struct cache, stat_timeout_secs), 0 }, 592 | { "cache_timeout=%u", offsetof(struct cache, dir_timeout_secs), 0 }, 593 | { "cache_timeout=%u", offsetof(struct cache, link_timeout_secs), 0 }, 594 | { "cache_stat_timeout=%u", offsetof(struct cache, stat_timeout_secs), 0 }, 595 | { "cache_dir_timeout=%u", offsetof(struct cache, dir_timeout_secs), 0 }, 596 | { "cache_link_timeout=%u", offsetof(struct cache, link_timeout_secs), 0 }, 597 | { "cache_max_size=%u", offsetof(struct cache, max_size), 0 }, 598 | { "cache_clean_interval=%u", offsetof(struct cache, 599 | clean_interval_secs), 0 }, 600 | { "cache_min_clean_interval=%u", offsetof(struct cache, 601 | min_clean_interval_secs), 0 }, 602 | FUSE_OPT_END 603 | }; 604 | 605 | int cache_parse_options(struct fuse_args *args) 606 | { 607 | cache.stat_timeout_secs = DEFAULT_CACHE_TIMEOUT_SECS; 608 | cache.dir_timeout_secs = DEFAULT_CACHE_TIMEOUT_SECS; 609 | cache.link_timeout_secs = DEFAULT_CACHE_TIMEOUT_SECS; 610 | cache.max_size = DEFAULT_MAX_CACHE_SIZE; 611 | cache.clean_interval_secs = DEFAULT_CACHE_CLEAN_INTERVAL_SECS; 612 | cache.min_clean_interval_secs = DEFAULT_MIN_CACHE_CLEAN_INTERVAL_SECS; 613 | 614 | return fuse_opt_parse(args, &cache, cache_opts, NULL); 615 | } 616 | -------------------------------------------------------------------------------- /cache.h: -------------------------------------------------------------------------------- 1 | /* 2 | Caching file system proxy 3 | Copyright (C) 2004 Miklos Szeredi 4 | 5 | This program can be distributed under the terms of the GNU GPL. 6 | See the file COPYING. 7 | */ 8 | 9 | #include 10 | #include 11 | 12 | struct fuse_operations *cache_wrap(struct fuse_operations *oper); 13 | int cache_parse_options(struct fuse_args *args); 14 | void cache_add_attr(const char *path, const struct stat *stbuf, uint64_t wrctr); 15 | void cache_invalidate(const char *path); 16 | uint64_t cache_get_write_ctr(void); 17 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parserPreset: 'conventional-changelog-conventionalcommits', 3 | rules: { 4 | 'body-leading-blank': [1, 'always'], 5 | 'footer-leading-blank': [1, 'always'], 6 | 'scope-case': [2, 'always', 'lower-case'], 7 | 'subject-case': [2, 'never', ['sentence-case', 'start-case', 'pascal-case', 'upper-case']], 8 | 'subject-empty': [2, 'never'], 9 | 'subject-full-stop': [2, 'never', '.'], 10 | 'type-case': [2, 'always', 'lower-case'], 11 | 'type-empty': [2, 'never'], 12 | 'type-enum': [ 13 | 2, 14 | 'always', 15 | ['build', 'chore', 'ci', 'docs', 'feat', 'fix', 'perf', 'refactor', 'revert', 'style', 'test'], 16 | ], 17 | }, 18 | } 19 | -------------------------------------------------------------------------------- /compat/darwin_compat.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2006-2008 Amit Singh/Google Inc. 3 | * Copyright (c) 2012 Anatol Pomozov 4 | * Copyright (c) 2011-2013 Benjamin Fleischer 5 | */ 6 | 7 | #include "darwin_compat.h" 8 | 9 | #include 10 | #include 11 | #include 12 | 13 | /* 14 | * Semaphore implementation based on: 15 | * 16 | * Copyright (C) 2000,02 Free Software Foundation, Inc. 17 | * This file is part of the GNU C Library. 18 | * Written by Gal Le Mignot 19 | * 20 | * The GNU C Library is free software; you can redistribute it and/or 21 | * modify it under the terms of the GNU Library General Public License as 22 | * published by the Free Software Foundation; either version 2 of the 23 | * License, or (at your option) any later version. 24 | * 25 | * The GNU C Library is distributed in the hope that it will be useful, 26 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 27 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 28 | * Library General Public License for more details. 29 | * 30 | * You should have received a copy of the GNU Library General Public 31 | * License along with the GNU C Library; see the file COPYING.LIB. If not, 32 | * write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, 33 | * Boston, MA 02111-1307, USA. 34 | */ 35 | 36 | /* Semaphores */ 37 | 38 | #define __SEM_ID_NONE ((int)0x0) 39 | #define __SEM_ID_LOCAL ((int)0xcafef00d) 40 | 41 | /* http://www.opengroup.org/onlinepubs/007908799/xsh/sem_init.html */ 42 | int 43 | darwin_sem_init(darwin_sem_t *sem, int pshared, unsigned int value) 44 | { 45 | if (pshared) { 46 | errno = ENOSYS; 47 | return -1; 48 | } 49 | 50 | sem->id = __SEM_ID_NONE; 51 | 52 | if (pthread_cond_init(&sem->__data.local.count_cond, NULL)) { 53 | goto cond_init_fail; 54 | } 55 | 56 | if (pthread_mutex_init(&sem->__data.local.count_lock, NULL)) { 57 | goto mutex_init_fail; 58 | } 59 | 60 | sem->__data.local.count = value; 61 | sem->id = __SEM_ID_LOCAL; 62 | 63 | return 0; 64 | 65 | mutex_init_fail: 66 | 67 | pthread_cond_destroy(&sem->__data.local.count_cond); 68 | 69 | cond_init_fail: 70 | 71 | return -1; 72 | } 73 | 74 | /* http://www.opengroup.org/onlinepubs/007908799/xsh/sem_destroy.html */ 75 | int 76 | darwin_sem_destroy(darwin_sem_t *sem) 77 | { 78 | int res = 0; 79 | 80 | pthread_mutex_lock(&sem->__data.local.count_lock); 81 | 82 | sem->id = __SEM_ID_NONE; 83 | pthread_cond_broadcast(&sem->__data.local.count_cond); 84 | 85 | if (pthread_cond_destroy(&sem->__data.local.count_cond)) { 86 | res = -1; 87 | } 88 | 89 | pthread_mutex_unlock(&sem->__data.local.count_lock); 90 | 91 | if (pthread_mutex_destroy(&sem->__data.local.count_lock)) { 92 | res = -1; 93 | } 94 | 95 | return res; 96 | } 97 | 98 | int 99 | darwin_sem_getvalue(darwin_sem_t *sem, unsigned int *sval) 100 | { 101 | int res = 0; 102 | 103 | pthread_mutex_lock(&sem->__data.local.count_lock); 104 | 105 | if (sem->id != __SEM_ID_LOCAL) { 106 | res = -1; 107 | errno = EINVAL; 108 | } else { 109 | *sval = sem->__data.local.count; 110 | } 111 | 112 | pthread_mutex_unlock(&sem->__data.local.count_lock); 113 | 114 | return res; 115 | } 116 | 117 | /* http://www.opengroup.org/onlinepubs/007908799/xsh/sem_post.html */ 118 | int 119 | darwin_sem_post(darwin_sem_t *sem) 120 | { 121 | int res = 0; 122 | 123 | pthread_mutex_lock(&sem->__data.local.count_lock); 124 | 125 | if (sem->id != __SEM_ID_LOCAL) { 126 | res = -1; 127 | errno = EINVAL; 128 | } else if (sem->__data.local.count < DARWIN_SEM_VALUE_MAX) { 129 | sem->__data.local.count++; 130 | if (sem->__data.local.count == 1) { 131 | pthread_cond_signal(&sem->__data.local.count_cond); 132 | } 133 | } else { 134 | errno = ERANGE; 135 | res = -1; 136 | } 137 | 138 | pthread_mutex_unlock(&sem->__data.local.count_lock); 139 | 140 | return res; 141 | } 142 | 143 | /* http://www.opengroup.org/onlinepubs/009695399/functions/sem_timedwait.html */ 144 | int 145 | darwin_sem_timedwait(darwin_sem_t *sem, const struct timespec *abs_timeout) 146 | { 147 | int res = 0; 148 | 149 | if (abs_timeout && 150 | (abs_timeout->tv_nsec < 0 || abs_timeout->tv_nsec >= 1000000000)) { 151 | errno = EINVAL; 152 | return -1; 153 | } 154 | 155 | pthread_cleanup_push((void(*)(void*))&pthread_mutex_unlock, 156 | &sem->__data.local.count_lock); 157 | 158 | pthread_mutex_lock(&sem->__data.local.count_lock); 159 | 160 | if (sem->id != __SEM_ID_LOCAL) { 161 | errno = EINVAL; 162 | res = -1; 163 | } else { 164 | if (!sem->__data.local.count) { 165 | res = pthread_cond_timedwait(&sem->__data.local.count_cond, 166 | &sem->__data.local.count_lock, 167 | abs_timeout); 168 | } 169 | if (res) { 170 | assert(res == ETIMEDOUT); 171 | res = -1; 172 | errno = ETIMEDOUT; 173 | } else if (sem->id != __SEM_ID_LOCAL) { 174 | res = -1; 175 | errno = EINVAL; 176 | } else { 177 | sem->__data.local.count--; 178 | } 179 | } 180 | 181 | pthread_cleanup_pop(1); 182 | 183 | return res; 184 | } 185 | 186 | /* http://www.opengroup.org/onlinepubs/007908799/xsh/sem_trywait.html */ 187 | int 188 | darwin_sem_trywait(darwin_sem_t *sem) 189 | { 190 | int res = 0; 191 | 192 | pthread_mutex_lock(&sem->__data.local.count_lock); 193 | 194 | if (sem->id != __SEM_ID_LOCAL) { 195 | res = -1; 196 | errno = EINVAL; 197 | } else if (sem->__data.local.count) { 198 | sem->__data.local.count--; 199 | } else { 200 | res = -1; 201 | errno = EAGAIN; 202 | } 203 | 204 | pthread_mutex_unlock (&sem->__data.local.count_lock); 205 | 206 | return res; 207 | } 208 | 209 | /* http://www.opengroup.org/onlinepubs/007908799/xsh/sem_wait.html */ 210 | int 211 | darwin_sem_wait(darwin_sem_t *sem) 212 | { 213 | int res = 0; 214 | 215 | pthread_cleanup_push((void(*)(void*))&pthread_mutex_unlock, 216 | &sem->__data.local.count_lock); 217 | 218 | pthread_mutex_lock(&sem->__data.local.count_lock); 219 | 220 | if (sem->id != __SEM_ID_LOCAL) { 221 | errno = EINVAL; 222 | res = -1; 223 | } else { 224 | if (!sem->__data.local.count) { 225 | pthread_cond_wait(&sem->__data.local.count_cond, 226 | &sem->__data.local.count_lock); 227 | if (!sem->__data.local.count) { 228 | /* spurious wakeup, assume it is an interruption */ 229 | res = -1; 230 | errno = EINTR; 231 | goto out; 232 | } 233 | } 234 | if (sem->id != __SEM_ID_LOCAL) { 235 | res = -1; 236 | errno = EINVAL; 237 | } else { 238 | sem->__data.local.count--; 239 | } 240 | } 241 | 242 | out: 243 | pthread_cleanup_pop(1); 244 | 245 | return res; 246 | } 247 | -------------------------------------------------------------------------------- /compat/darwin_compat.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2006-2008 Amit Singh/Google Inc. 3 | * Copyright (c) 2011-2013 Benjamin Fleischer 4 | */ 5 | 6 | #ifndef _DARWIN_COMPAT_ 7 | #define _DARWIN_COMPAT_ 8 | 9 | #include 10 | 11 | /* Semaphores */ 12 | 13 | typedef struct darwin_sem { 14 | int id; 15 | union { 16 | struct 17 | { 18 | unsigned int count; 19 | pthread_mutex_t count_lock; 20 | pthread_cond_t count_cond; 21 | } local; 22 | } __data; 23 | } darwin_sem_t; 24 | 25 | #define DARWIN_SEM_VALUE_MAX ((int32_t)32767) 26 | 27 | int darwin_sem_init(darwin_sem_t *sem, int pshared, unsigned int value); 28 | int darwin_sem_destroy(darwin_sem_t *sem); 29 | int darwin_sem_getvalue(darwin_sem_t *sem, unsigned int *value); 30 | int darwin_sem_post(darwin_sem_t *sem); 31 | int darwin_sem_timedwait(darwin_sem_t *sem, const struct timespec *abs_timeout); 32 | int darwin_sem_trywait(darwin_sem_t *sem); 33 | int darwin_sem_wait(darwin_sem_t *sem); 34 | 35 | /* Caller must not include */ 36 | 37 | typedef darwin_sem_t sem_t; 38 | 39 | #define sem_init(s, p, v) darwin_sem_init(s, p, v) 40 | #define sem_destroy(s) darwin_sem_destroy(s) 41 | #define sem_getvalue(s, v) darwin_sem_getvalue(s, v) 42 | #define sem_post(s) darwin_sem_post(s) 43 | #define sem_timedwait(s, t) darwin_sem_timedwait(s, t) 44 | #define sem_trywait(s) darwin_sem_trywait(s) 45 | #define sem_wait(s) darwin_sem_wait(s) 46 | 47 | #define SEM_VALUE_MAX DARWIN_SEM_VALUE_MAX 48 | 49 | #endif /* _DARWIN_COMPAT_ */ 50 | -------------------------------------------------------------------------------- /compat/fuse_opt.c: -------------------------------------------------------------------------------- 1 | /* 2 | FUSE: Filesystem in Userspace 3 | Copyright (C) 2001-2006 Miklos Szeredi 4 | 5 | This program can be distributed under the terms of the GNU LGPL. 6 | See the file COPYING.LIB 7 | */ 8 | 9 | #include "fuse_opt.h" 10 | 11 | #include 12 | #include 13 | #include 14 | #include 15 | 16 | struct fuse_opt_context { 17 | void *data; 18 | const struct fuse_opt *opt; 19 | fuse_opt_proc_t proc; 20 | int argctr; 21 | int argc; 22 | char **argv; 23 | struct fuse_args outargs; 24 | char *opts; 25 | int nonopt; 26 | }; 27 | 28 | void fuse_opt_free_args(struct fuse_args *args) 29 | { 30 | if (args && args->argv && args->allocated) { 31 | int i; 32 | for (i = 0; i < args->argc; i++) 33 | free(args->argv[i]); 34 | free(args->argv); 35 | args->argv = NULL; 36 | args->allocated = 0; 37 | } 38 | } 39 | 40 | static int alloc_failed(void) 41 | { 42 | fprintf(stderr, "fuse: memory allocation failed\n"); 43 | return -1; 44 | } 45 | 46 | int fuse_opt_add_arg(struct fuse_args *args, const char *arg) 47 | { 48 | char **newargv; 49 | char *newarg; 50 | 51 | assert(!args->argv || args->allocated); 52 | 53 | newargv = realloc(args->argv, (args->argc + 2) * sizeof(char *)); 54 | newarg = newargv ? strdup(arg) : NULL; 55 | if (!newargv || !newarg) 56 | return alloc_failed(); 57 | 58 | args->argv = newargv; 59 | args->allocated = 1; 60 | args->argv[args->argc++] = newarg; 61 | args->argv[args->argc] = NULL; 62 | return 0; 63 | } 64 | 65 | int fuse_opt_insert_arg(struct fuse_args *args, int pos, const char *arg) 66 | { 67 | assert(pos <= args->argc); 68 | if (fuse_opt_add_arg(args, arg) == -1) 69 | return -1; 70 | 71 | if (pos != args->argc - 1) { 72 | char *newarg = args->argv[args->argc - 1]; 73 | memmove(&args->argv[pos + 1], &args->argv[pos], 74 | sizeof(char *) * (args->argc - pos - 1)); 75 | args->argv[pos] = newarg; 76 | } 77 | return 0; 78 | } 79 | 80 | static int next_arg(struct fuse_opt_context *ctx, const char *opt) 81 | { 82 | if (ctx->argctr + 1 >= ctx->argc) { 83 | fprintf(stderr, "fuse: missing argument after `%s'\n", opt); 84 | return -1; 85 | } 86 | ctx->argctr++; 87 | return 0; 88 | } 89 | 90 | static int add_arg(struct fuse_opt_context *ctx, const char *arg) 91 | { 92 | return fuse_opt_add_arg(&ctx->outargs, arg); 93 | } 94 | 95 | int fuse_opt_add_opt(char **opts, const char *opt) 96 | { 97 | char *newopts; 98 | if (!*opts) 99 | newopts = strdup(opt); 100 | else { 101 | unsigned oldlen = strlen(*opts); 102 | newopts = realloc(*opts, oldlen + 1 + strlen(opt) + 1); 103 | if (newopts) { 104 | newopts[oldlen] = ','; 105 | strcpy(newopts + oldlen + 1, opt); 106 | } 107 | } 108 | if (!newopts) 109 | return alloc_failed(); 110 | 111 | *opts = newopts; 112 | return 0; 113 | } 114 | 115 | static int add_opt(struct fuse_opt_context *ctx, const char *opt) 116 | { 117 | return fuse_opt_add_opt(&ctx->opts, opt); 118 | } 119 | 120 | static int call_proc(struct fuse_opt_context *ctx, const char *arg, int key, 121 | int iso) 122 | { 123 | if (key == FUSE_OPT_KEY_DISCARD) 124 | return 0; 125 | 126 | if (key != FUSE_OPT_KEY_KEEP && ctx->proc) { 127 | int res = ctx->proc(ctx->data, arg, key, &ctx->outargs); 128 | if (res == -1 || !res) 129 | return res; 130 | } 131 | if (iso) 132 | return add_opt(ctx, arg); 133 | else 134 | return add_arg(ctx, arg); 135 | } 136 | 137 | static int match_template(const char *t, const char *arg, unsigned *sepp) 138 | { 139 | int arglen = strlen(arg); 140 | const char *sep = strchr(t, '='); 141 | sep = sep ? sep : strchr(t, ' '); 142 | if (sep && (!sep[1] || sep[1] == '%')) { 143 | int tlen = sep - t; 144 | if (sep[0] == '=') 145 | tlen ++; 146 | if (arglen >= tlen && strncmp(arg, t, tlen) == 0) { 147 | *sepp = sep - t; 148 | return 1; 149 | } 150 | } 151 | if (strcmp(t, arg) == 0) { 152 | *sepp = 0; 153 | return 1; 154 | } 155 | return 0; 156 | } 157 | 158 | static const struct fuse_opt *find_opt(const struct fuse_opt *opt, 159 | const char *arg, unsigned *sepp) 160 | { 161 | for (; opt && opt->templ; opt++) 162 | if (match_template(opt->templ, arg, sepp)) 163 | return opt; 164 | return NULL; 165 | } 166 | 167 | int fuse_opt_match(const struct fuse_opt *opts, const char *opt) 168 | { 169 | unsigned dummy; 170 | return find_opt(opts, opt, &dummy) ? 1 : 0; 171 | } 172 | 173 | static int process_opt_param(void *var, const char *format, const char *param, 174 | const char *arg) 175 | { 176 | assert(format[0] == '%'); 177 | if (format[1] == 's') { 178 | char *copy = strdup(param); 179 | if (!copy) 180 | return alloc_failed(); 181 | 182 | *(char **) var = copy; 183 | } else { 184 | if (sscanf(param, format, var) != 1) { 185 | fprintf(stderr, "fuse: invalid parameter in option `%s'\n", arg); 186 | return -1; 187 | } 188 | } 189 | return 0; 190 | } 191 | 192 | static int process_opt(struct fuse_opt_context *ctx, 193 | const struct fuse_opt *opt, unsigned sep, 194 | const char *arg, int iso) 195 | { 196 | if (opt->offset == -1U) { 197 | if (call_proc(ctx, arg, opt->value, iso) == -1) 198 | return -1; 199 | } else { 200 | void *var = ctx->data + opt->offset; 201 | if (sep && opt->templ[sep + 1]) { 202 | const char *param = arg + sep; 203 | if (opt->templ[sep] == '=') 204 | param ++; 205 | if (process_opt_param(var, opt->templ + sep + 1, 206 | param, arg) == -1) 207 | return -1; 208 | } else 209 | *(int *)var = opt->value; 210 | } 211 | return 0; 212 | } 213 | 214 | static int process_opt_sep_arg(struct fuse_opt_context *ctx, 215 | const struct fuse_opt *opt, unsigned sep, 216 | const char *arg, int iso) 217 | { 218 | int res; 219 | char *newarg; 220 | char *param; 221 | 222 | if (next_arg(ctx, arg) == -1) 223 | return -1; 224 | 225 | param = ctx->argv[ctx->argctr]; 226 | newarg = malloc(sep + strlen(param) + 1); 227 | if (!newarg) 228 | return alloc_failed(); 229 | 230 | memcpy(newarg, arg, sep); 231 | strcpy(newarg + sep, param); 232 | res = process_opt(ctx, opt, sep, newarg, iso); 233 | free(newarg); 234 | 235 | return res; 236 | } 237 | 238 | static int process_gopt(struct fuse_opt_context *ctx, const char *arg, int iso) 239 | { 240 | unsigned sep; 241 | const struct fuse_opt *opt = find_opt(ctx->opt, arg, &sep); 242 | if (opt) { 243 | for (; opt; opt = find_opt(opt + 1, arg, &sep)) { 244 | int res; 245 | if (sep && opt->templ[sep] == ' ' && !arg[sep]) 246 | res = process_opt_sep_arg(ctx, opt, sep, arg, iso); 247 | else 248 | res = process_opt(ctx, opt, sep, arg, iso); 249 | if (res == -1) 250 | return -1; 251 | } 252 | return 0; 253 | } else 254 | return call_proc(ctx, arg, FUSE_OPT_KEY_OPT, iso); 255 | } 256 | 257 | static int process_real_option_group(struct fuse_opt_context *ctx, char *opts) 258 | { 259 | char *sep; 260 | 261 | do { 262 | int res; 263 | sep = strchr(opts, ','); 264 | if (sep) 265 | *sep = '\0'; 266 | res = process_gopt(ctx, opts, 1); 267 | if (res == -1) 268 | return -1; 269 | opts = sep + 1; 270 | } while (sep); 271 | 272 | return 0; 273 | } 274 | 275 | static int process_option_group(struct fuse_opt_context *ctx, const char *opts) 276 | { 277 | int res; 278 | char *copy; 279 | const char *sep = strchr(opts, ','); 280 | if (!sep) 281 | return process_gopt(ctx, opts, 1); 282 | 283 | copy = strdup(opts); 284 | if (!copy) { 285 | fprintf(stderr, "fuse: memory allocation failed\n"); 286 | return -1; 287 | } 288 | res = process_real_option_group(ctx, copy); 289 | free(copy); 290 | return res; 291 | } 292 | 293 | static int process_one(struct fuse_opt_context *ctx, const char *arg) 294 | { 295 | if (ctx->nonopt || arg[0] != '-') 296 | return call_proc(ctx, arg, FUSE_OPT_KEY_NONOPT, 0); 297 | else if (arg[1] == 'o') { 298 | if (arg[2]) 299 | return process_option_group(ctx, arg + 2); 300 | else { 301 | if (next_arg(ctx, arg) == -1) 302 | return -1; 303 | 304 | return process_option_group(ctx, ctx->argv[ctx->argctr]); 305 | } 306 | } else if (arg[1] == '-' && !arg[2]) { 307 | if (add_arg(ctx, arg) == -1) 308 | return -1; 309 | ctx->nonopt = ctx->outargs.argc; 310 | return 0; 311 | } else 312 | return process_gopt(ctx, arg, 0); 313 | } 314 | 315 | static int opt_parse(struct fuse_opt_context *ctx) 316 | { 317 | if (ctx->argc) { 318 | if (add_arg(ctx, ctx->argv[0]) == -1) 319 | return -1; 320 | } 321 | 322 | for (ctx->argctr = 1; ctx->argctr < ctx->argc; ctx->argctr++) 323 | if (process_one(ctx, ctx->argv[ctx->argctr]) == -1) 324 | return -1; 325 | 326 | if (ctx->opts) { 327 | if (fuse_opt_insert_arg(&ctx->outargs, 1, "-o") == -1 || 328 | fuse_opt_insert_arg(&ctx->outargs, 2, ctx->opts) == -1) 329 | return -1; 330 | } 331 | if (ctx->nonopt && ctx->nonopt == ctx->outargs.argc) { 332 | free(ctx->outargs.argv[ctx->outargs.argc - 1]); 333 | ctx->outargs.argv[--ctx->outargs.argc] = NULL; 334 | } 335 | 336 | return 0; 337 | } 338 | 339 | int fuse_opt_parse(struct fuse_args *args, void *data, 340 | const struct fuse_opt opts[], fuse_opt_proc_t proc) 341 | { 342 | int res; 343 | struct fuse_opt_context ctx = { 344 | .data = data, 345 | .opt = opts, 346 | .proc = proc, 347 | }; 348 | 349 | if (!args || !args->argv || !args->argc) 350 | return 0; 351 | 352 | ctx.argc = args->argc; 353 | ctx.argv = args->argv; 354 | 355 | res = opt_parse(&ctx); 356 | if (res != -1) { 357 | struct fuse_args tmp = *args; 358 | *args = ctx.outargs; 359 | ctx.outargs = tmp; 360 | } 361 | free(ctx.opts); 362 | fuse_opt_free_args(&ctx.outargs); 363 | return res; 364 | } 365 | -------------------------------------------------------------------------------- /compat/fuse_opt.h: -------------------------------------------------------------------------------- 1 | /* 2 | FUSE: Filesystem in Userspace 3 | Copyright (C) 2001-2006 Miklos Szeredi 4 | 5 | This program can be distributed under the terms of the GNU GPL. 6 | See the file COPYING. 7 | */ 8 | 9 | #ifndef _FUSE_OPT_H_ 10 | #define _FUSE_OPT_H_ 11 | 12 | /* This file defines the option parsing interface of FUSE */ 13 | 14 | #ifdef __cplusplus 15 | extern "C" { 16 | #endif 17 | 18 | /** 19 | * Option description 20 | * 21 | * This structure describes a single option, and an action associated 22 | * with it, in case it matches. 23 | * 24 | * More than one such match may occur, in which case the action for 25 | * each match is executed. 26 | * 27 | * There are three possible actions in case of a match: 28 | * 29 | * i) An integer (int or unsigned) variable determined by 'offset' is 30 | * set to 'value' 31 | * 32 | * ii) The processing function is called, with 'value' as the key 33 | * 34 | * iii) An integer (any) or string (char *) variable determined by 35 | * 'offset' is set to the value of an option parameter 36 | * 37 | * 'offset' should normally be either set to 38 | * 39 | * - 'offsetof(struct foo, member)' actions i) and iii) 40 | * 41 | * - -1 action ii) 42 | * 43 | * The 'offsetof()' macro is defined in the header. 44 | * 45 | * The template determines which options match, and also have an 46 | * effect on the action. Normally the action is either i) or ii), but 47 | * if a format is present in the template, then action iii) is 48 | * performed. 49 | * 50 | * The types of templates are: 51 | * 52 | * 1) "-x", "-foo", "--foo", "--foo-bar", etc. These match only 53 | * themselves. Invalid values are "--" and anything beginning 54 | * with "-o" 55 | * 56 | * 2) "foo", "foo-bar", etc. These match "-ofoo", "-ofoo-bar" or 57 | * the relevant option in a comma separated option list 58 | * 59 | * 3) "bar=", "--foo=", etc. These are variations of 1) and 2) 60 | * which have a parameter 61 | * 62 | * 4) "bar=%s", "--foo=%lu", etc. Same matching as above but perform 63 | * action iii). 64 | * 65 | * 5) "-x ", etc. Matches either "-xparam" or "-x param" as 66 | * two separate arguments 67 | * 68 | * 6) "-x %s", etc. Combination of 4) and 5) 69 | * 70 | * If the format is "%s", memory is allocated for the string unlike 71 | * with scanf(). 72 | */ 73 | struct fuse_opt { 74 | /** Matching template and optional parameter formatting */ 75 | const char *templ; 76 | 77 | /** 78 | * Offset of variable within 'data' parameter of fuse_opt_parse() 79 | * or -1 80 | */ 81 | unsigned long offset; 82 | 83 | /** 84 | * Value to set the variable to, or to be passed as 'key' to the 85 | * processing function. Ignored if template has a format 86 | */ 87 | int value; 88 | }; 89 | 90 | /** 91 | * Key option. In case of a match, the processing function will be 92 | * called with the specified key. 93 | */ 94 | #define FUSE_OPT_KEY(templ, key) { templ, -1U, key } 95 | 96 | /** 97 | * Last option. An array of 'struct fuse_opt' must end with a NULL 98 | * template value 99 | */ 100 | #define FUSE_OPT_END { .templ = NULL } 101 | 102 | /** 103 | * Argument list 104 | */ 105 | struct fuse_args { 106 | /** Argument count */ 107 | int argc; 108 | 109 | /** Argument vector. NULL terminated */ 110 | char **argv; 111 | 112 | /** Is 'argv' allocated? */ 113 | int allocated; 114 | }; 115 | 116 | /** 117 | * Initializer for 'struct fuse_args' 118 | */ 119 | #define FUSE_ARGS_INIT(argc, argv) { argc, argv, 0 } 120 | 121 | /** 122 | * Key value passed to the processing function if an option did not 123 | * match any template 124 | */ 125 | #define FUSE_OPT_KEY_OPT -1 126 | 127 | /** 128 | * Key value passed to the processing function for all non-options 129 | * 130 | * Non-options are the arguments beginning with a charater other than 131 | * '-' or all arguments after the special '--' option 132 | */ 133 | #define FUSE_OPT_KEY_NONOPT -2 134 | 135 | /** 136 | * Special key value for options to keep 137 | * 138 | * Argument is not passed to processing function, but behave as if the 139 | * processing function returned 1 140 | */ 141 | #define FUSE_OPT_KEY_KEEP -3 142 | 143 | /** 144 | * Special key value for options to discard 145 | * 146 | * Argument is not passed to processing function, but behave as if the 147 | * processing function returned zero 148 | */ 149 | #define FUSE_OPT_KEY_DISCARD -4 150 | 151 | /** 152 | * Processing function 153 | * 154 | * This function is called if 155 | * - option did not match any 'struct fuse_opt' 156 | * - argument is a non-option 157 | * - option did match and offset was set to -1 158 | * 159 | * The 'arg' parameter will always contain the whole argument or 160 | * option including the parameter if exists. A two-argument option 161 | * ("-x foo") is always converted to single arguemnt option of the 162 | * form "-xfoo" before this function is called. 163 | * 164 | * Options of the form '-ofoo' are passed to this function without the 165 | * '-o' prefix. 166 | * 167 | * The return value of this function determines whether this argument 168 | * is to be inserted into the output argument vector, or discarded. 169 | * 170 | * @param data is the user data passed to the fuse_opt_parse() function 171 | * @param arg is the whole argument or option 172 | * @param key determines why the processing function was called 173 | * @param outargs the current output argument list 174 | * @return -1 on error, 0 if arg is to be discarded, 1 if arg should be kept 175 | */ 176 | typedef int (*fuse_opt_proc_t)(void *data, const char *arg, int key, 177 | struct fuse_args *outargs); 178 | 179 | /** 180 | * Option parsing function 181 | * 182 | * If 'args' was returned from a previous call to fuse_opt_parse() or 183 | * it was constructed from 184 | * 185 | * A NULL 'args' is equivalent to an empty argument vector 186 | * 187 | * A NULL 'opts' is equivalent to an 'opts' array containing a single 188 | * end marker 189 | * 190 | * A NULL 'proc' is equivalent to a processing function always 191 | * returning '1' 192 | * 193 | * @param args is the input and output argument list 194 | * @param data is the user data 195 | * @param opts is the option description array 196 | * @param proc is the processing function 197 | * @return -1 on error, 0 on success 198 | */ 199 | int fuse_opt_parse(struct fuse_args *args, void *data, 200 | const struct fuse_opt opts[], fuse_opt_proc_t proc); 201 | 202 | /** 203 | * Add an option to a comma separated option list 204 | * 205 | * @param opts is a pointer to an option list, may point to a NULL value 206 | * @param opt is the option to add 207 | * @return -1 on allocation error, 0 on success 208 | */ 209 | int fuse_opt_add_opt(char **opts, const char *opt); 210 | 211 | /** 212 | * Add an argument to a NULL terminated argument vector 213 | * 214 | * @param args is the structure containing the current argument list 215 | * @param arg is the new argument to add 216 | * @return -1 on allocation error, 0 on success 217 | */ 218 | int fuse_opt_add_arg(struct fuse_args *args, const char *arg); 219 | 220 | /** 221 | * Add an argument at the specified position in a NULL terminated 222 | * argument vector 223 | * 224 | * Adds the argument to the N-th position. This is useful for adding 225 | * options at the beggining of the array which must not come after the 226 | * special '--' option. 227 | * 228 | * @param args is the structure containing the current argument list 229 | * @param pos is the position at which to add the argument 230 | * @param arg is the new argument to add 231 | * @return -1 on allocation error, 0 on success 232 | */ 233 | int fuse_opt_insert_arg(struct fuse_args *args, int pos, const char *arg); 234 | 235 | /** 236 | * Free the contents of argument list 237 | * 238 | * The structure itself is not freed 239 | * 240 | * @param args is the structure containing the argument list 241 | */ 242 | void fuse_opt_free_args(struct fuse_args *args); 243 | 244 | 245 | /** 246 | * Check if an option matches 247 | * 248 | * @param opts is the option description array 249 | * @param opt is the option to match 250 | * @return 1 if a match is found, 0 if not 251 | */ 252 | int fuse_opt_match(const struct fuse_opt opts[], const char *opt); 253 | 254 | #ifdef __cplusplus 255 | } 256 | #endif 257 | 258 | #endif /* _FUSE_OPT_H_ */ 259 | -------------------------------------------------------------------------------- /make_release_tarball.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # Create tarball from Git tag, removing and adding 4 | # some files. 5 | # 6 | 7 | set -e 8 | 9 | if [ -z "$1" ]; then 10 | TAG="$(git tag --list 'sshfs-3*' --sort=-taggerdate | head -1)" 11 | else 12 | TAG="$1" 13 | fi 14 | 15 | echo "Creating release tarball for ${TAG}..." 16 | 17 | mkdir "${TAG}" 18 | git archive --format=tar "${TAG}" | tar -x "--directory=${TAG}" 19 | find "${TAG}" -name .gitignore -delete 20 | rm "${TAG}/make_release_tarball.sh" 21 | tar -cJf "${TAG}.tar.xz" "${TAG}/" 22 | gpg --armor --detach-sign "${TAG}.tar.xz" 23 | 24 | PREV_TAG="$(git tag --list 'sshfs-3*' --sort=-taggerdate --merged "${TAG}^"| head -1)" 25 | echo "Contributors from ${PREV_TAG} to ${TAG}:" 26 | git log --pretty="format:%an <%aE>" "${PREV_TAG}..${TAG}" | sort -u 27 | -------------------------------------------------------------------------------- /meson.build: -------------------------------------------------------------------------------- 1 | project('sshfs', 'c', version: '3.7.3', 2 | meson_version: '>= 0.40', 3 | default_options: [ 'buildtype=debugoptimized' ]) 4 | 5 | add_global_arguments('-D_REENTRANT', '-DHAVE_CONFIG_H', 6 | '-Wall', '-Wextra', '-Wno-sign-compare', 7 | '-Wmissing-declarations', '-Wwrite-strings', 8 | language: 'c') 9 | 10 | # Some (stupid) GCC versions warn about unused return values even when they are 11 | # casted to void. This makes -Wunused-result pretty useless, since there is no 12 | # way to suppress the warning when we really *want* to ignore the value. 13 | cc = meson.get_compiler('c') 14 | code = ''' 15 | __attribute__((warn_unused_result)) int get_4() { 16 | return 4; 17 | } 18 | int main(void) { 19 | (void) get_4(); 20 | return 0; 21 | }''' 22 | if not cc.compiles(code, args: [ '-O0', '-Werror=unused-result' ]) 23 | message('Compiler warns about unused result even when casting to void') 24 | add_global_arguments('-Wno-unused-result', language: 'c') 25 | endif 26 | 27 | 28 | rst2man = find_program('rst2man', 'rst2man.py', required: false) 29 | 30 | cfg = configuration_data() 31 | 32 | cfg.set_quoted('PACKAGE_VERSION', meson.project_version()) 33 | 34 | include_dirs = [ include_directories('.') ] 35 | sshfs_sources = ['sshfs.c', 'cache.c'] 36 | cfg.set_quoted('NAMEMAP_DEFAULT', 'user') 37 | if target_machine.system() == 'darwin' 38 | sshfs_sources += [ 'compat/fuse_opt.c', 'compat/darwin_compat.c' ] 39 | include_dirs += [ include_directories('compat') ] 40 | endif 41 | 42 | configure_file(output: 'config.h', 43 | configuration : cfg) 44 | 45 | sshfs_deps = [ dependency('fuse3', version: '>= 3.1.0'), 46 | dependency('glib-2.0'), 47 | dependency('gthread-2.0') ] 48 | 49 | executable('sshfs', sshfs_sources, 50 | include_directories: include_dirs, 51 | dependencies: sshfs_deps, 52 | c_args: ['-DFUSE_USE_VERSION=31'], 53 | install: true, 54 | install_dir: get_option('bindir')) 55 | 56 | if rst2man.found() 57 | custom_target('manpages', input: [ 'sshfs.rst' ], output: [ 'sshfs.1' ], 58 | command: [rst2man, '@INPUT@', '@OUTPUT@'], install: true, 59 | install_dir: join_paths(get_option('mandir'), 'man1')) 60 | else 61 | message('rst2man not found, not building manual page.') 62 | endif 63 | 64 | meson.add_install_script('utils/install_helper.sh', 65 | get_option('sbindir'), 66 | get_option('bindir')) 67 | 68 | 69 | subdir('test') 70 | -------------------------------------------------------------------------------- /pdm.lock: -------------------------------------------------------------------------------- 1 | # This file is @generated by PDM. 2 | # It is not intended for manual editing. 3 | 4 | [[package]] 5 | name = "colorama" 6 | version = "0.3.9" 7 | summary = "Cross-platform colored terminal text." 8 | 9 | [[package]] 10 | name = "execnet" 11 | version = "1.9.0" 12 | requires_python = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 13 | summary = "execnet: rapid multi-Python deployment" 14 | 15 | [[package]] 16 | name = "iniconfig" 17 | version = "2.0.0" 18 | requires_python = ">=3.7" 19 | summary = "brain-dead simple config-ini parsing" 20 | 21 | [[package]] 22 | name = "packaging" 23 | version = "23.0" 24 | requires_python = ">=3.7" 25 | summary = "Core utilities for Python packages" 26 | 27 | [[package]] 28 | name = "pluggy" 29 | version = "1.0.0" 30 | requires_python = ">=3.6" 31 | summary = "plugin and hook calling mechanisms for python" 32 | 33 | [[package]] 34 | name = "pytest" 35 | version = "7.3.0" 36 | requires_python = ">=3.7" 37 | summary = "pytest: simple powerful testing with Python" 38 | dependencies = [ 39 | "colorama; sys_platform == \"win32\"", 40 | "iniconfig", 41 | "packaging", 42 | "pluggy<2.0,>=0.12", 43 | ] 44 | 45 | [[package]] 46 | name = "pytest-xdist" 47 | version = "3.2.1" 48 | requires_python = ">=3.7" 49 | summary = "pytest xdist plugin for distributed testing, most importantly across multiple CPUs" 50 | dependencies = [ 51 | "execnet>=1.1", 52 | "pytest>=6.2.0", 53 | ] 54 | 55 | [[package]] 56 | name = "setuptools" 57 | version = "67.6.1" 58 | requires_python = ">=3.7" 59 | summary = "Easily download, build, install, upgrade, and uninstall Python packages" 60 | 61 | [metadata] 62 | lock_version = "4.2" 63 | groups = ["default"] 64 | content_hash = "sha256:88d18479cd7581d6e6a61e5e52f13eaa9242dca43ee6fd5d8d11ff1ec13919db" 65 | 66 | [metadata.files] 67 | "colorama 0.3.9" = [ 68 | {url = "https://files.pythonhosted.org/packages/db/c8/7dcf9dbcb22429512708fe3a547f8b6101c0d02137acbd892505aee57adf/colorama-0.3.9-py2.py3-none-any.whl", hash = "sha256:463f8483208e921368c9f306094eb6f725c6ca42b0f97e313cb5d5512459feda"}, 69 | {url = "https://files.pythonhosted.org/packages/e6/76/257b53926889e2835355d74fec73d82662100135293e17d382e2b74d1669/colorama-0.3.9.tar.gz", hash = "sha256:48eb22f4f8461b1df5734a074b57042430fb06e1d61bd1e11b078c0fe6d7a1f1"}, 70 | ] 71 | "execnet 1.9.0" = [ 72 | {url = "https://files.pythonhosted.org/packages/7a/3c/b5ac9fc61e1e559ced3e40bf5b518a4142536b34eb274aa50dff29cb89f5/execnet-1.9.0.tar.gz", hash = "sha256:8f694f3ba9cc92cab508b152dcfe322153975c29bda272e2fd7f3f00f36e47c5"}, 73 | {url = "https://files.pythonhosted.org/packages/81/c0/3072ecc23f4c5e0a1af35e3a222855cfd9c80a1a105ca67be3b6172637dd/execnet-1.9.0-py2.py3-none-any.whl", hash = "sha256:a295f7cc774947aac58dde7fdc85f4aa00c42adf5d8f5468fc630c1acf30a142"}, 74 | ] 75 | "iniconfig 2.0.0" = [ 76 | {url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, 77 | {url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, 78 | ] 79 | "packaging 23.0" = [ 80 | {url = "https://files.pythonhosted.org/packages/47/d5/aca8ff6f49aa5565df1c826e7bf5e85a6df852ee063600c1efa5b932968c/packaging-23.0.tar.gz", hash = "sha256:b6ad297f8907de0fa2fe1ccbd26fdaf387f5f47c7275fedf8cce89f99446cf97"}, 81 | {url = "https://files.pythonhosted.org/packages/ed/35/a31aed2993e398f6b09a790a181a7927eb14610ee8bbf02dc14d31677f1c/packaging-23.0-py3-none-any.whl", hash = "sha256:714ac14496c3e68c99c29b00845f7a2b85f3bb6f1078fd9f72fd20f0570002b2"}, 82 | ] 83 | "pluggy 1.0.0" = [ 84 | {url = "https://files.pythonhosted.org/packages/9e/01/f38e2ff29715251cf25532b9082a1589ab7e4f571ced434f98d0139336dc/pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, 85 | {url = "https://files.pythonhosted.org/packages/a1/16/db2d7de3474b6e37cbb9c008965ee63835bba517e22cdb8c35b5116b5ce1/pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, 86 | ] 87 | "pytest 7.3.0" = [ 88 | {url = "https://files.pythonhosted.org/packages/2b/a6/22c1138c2a7b60c9eb9ddeac017d82a58dfd9661651c721e7466af648764/pytest-7.3.0.tar.gz", hash = "sha256:58ecc27ebf0ea643ebfdf7fb1249335da761a00c9f955bcd922349bcb68ee57d"}, 89 | {url = "https://files.pythonhosted.org/packages/83/b8/345f25e35406da60b5cb0cb9de9f92a96f44eae618dc2cc4a00a2bf416f9/pytest-7.3.0-py3-none-any.whl", hash = "sha256:933051fa1bfbd38a21e73c3960cebdad4cf59483ddba7696c48509727e17f201"}, 90 | ] 91 | "pytest-xdist 3.2.1" = [ 92 | {url = "https://files.pythonhosted.org/packages/41/69/319ff6c2bda31b0ab0710d2bb406d53f7d9c13a1c572696479a16322d9dc/pytest_xdist-3.2.1-py3-none-any.whl", hash = "sha256:37290d161638a20b672401deef1cba812d110ac27e35d213f091d15b8beb40c9"}, 93 | {url = "https://files.pythonhosted.org/packages/e3/f8/de2dcd2938c05270c9881cb1463dea388acd0b239ee76809160420157784/pytest-xdist-3.2.1.tar.gz", hash = "sha256:1849bd98d8b242b948e472db7478e090bf3361912a8fed87992ed94085f54727"}, 94 | ] 95 | "setuptools 67.6.1" = [ 96 | {url = "https://files.pythonhosted.org/packages/0b/fc/8781442def77b0aa22f63f266d4dadd486ebc0c5371d6290caf4320da4b7/setuptools-67.6.1-py3-none-any.whl", hash = "sha256:e728ca814a823bf7bf60162daf9db95b93d532948c4c0bea762ce62f60189078"}, 97 | {url = "https://files.pythonhosted.org/packages/cb/46/22ec35f286a77e6b94adf81b4f0d59f402ed981d4251df0ba7b992299146/setuptools-67.6.1.tar.gz", hash = "sha256:257de92a9d50a60b8e22abfcbb771571fde0dbf3ec234463212027a4eeecbe9a"}, 98 | ] 99 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = '' 3 | version = '' 4 | description = '' 5 | authors = [] 6 | dependencies = [ 7 | 'pytest >= 7, < 8', 8 | 'setuptools >= 67', 9 | 'pytest-xdist >= 3.2.1', 10 | ] 11 | requires-python = '>=3.11' 12 | 13 | [tool.black] 14 | target-version = ['py311'] 15 | line-length = 120 16 | skip-string-normalization = true 17 | quiet = true 18 | exclude = ''' 19 | /( 20 | \.git 21 | )/ 22 | ''' 23 | 24 | [tool.isort] 25 | profile = 'black' 26 | line_length = 120 27 | sections = ['FUTURE', 'STDLIB', 'THIRDPARTY', 'FIRSTPARTY', 'LOCALFOLDER'] 28 | multi_line_output = 3 29 | use_parentheses = true 30 | atomic = true 31 | lines_after_imports = 2 32 | combine_star = true 33 | include_trailing_comma = true 34 | force_grid_wrap = 0 35 | 36 | [tool.pdm] 37 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # SSHFS 2 | ## About 3 | This is an updated version of *SSHFS*, that supports the latest version of *SFTP*, v6. For ease of maintenance, only the latest version will be supported. 4 | 5 | It supports [Green End SFTP Server](https://www.greenend.org.uk/rjk/sftpserver/). 6 | 7 | Compared to the *SFTP* spec, it does not support: 8 | 9 | - Custom line endings. 10 | 11 | *SSHFS* allows you to mount a remote filesystem using *SFTP*. Most *SSH* servers support and enable this *SFTP* access by default, so *SSHFS* is very simple to use - there's nothing to do on the server-side. 12 | 13 | ## How to use 14 | Once *SSHFS* is installed (see next section) running it is very simple: 15 | 16 | sshfs [user@]hostname:[directory] mountpoint 17 | 18 | It is recommended to run *SSHFS* as regular user (not as root). For this to work the mountpoint must be owned by the user. If username is omitted *SSHFS* will use the local username. If the directory is omitted, *SSHFS* will mount the (remote) home directory. If you need to enter a password *SSHFS* will ask for it (actually it just runs *SSH* which asks for the password if needed). 19 | 20 | Also many *SSH* options can be specified (see the manual pages for *sftp(1)* and *ssh_config(5)*), including the remote port number (`-oport=PORT`) 21 | 22 | To unmount the filesystem: 23 | 24 | fusermount3 -u mountpoint 25 | 26 | On *BSD* and *macOS*, to unmount the filesystem: 27 | 28 | umount mountpoint 29 | 30 | ## Installation 31 | First, download the latest *SSHFS* release. You also need [libfuse](http://github.com/libfuse/libfuse) 3.1.0 or newer (or a similar library that provides a libfuse3 compatible interface for your operating system). Finally, you need the [Glib](https://developer.gnome.org/glib/stable/) library with development headers (which should be available from your operating system's package manager). 32 | 33 | To build and install, we recommend to use [Meson](http://mesonbuild.com/) (version 0.38 or newer) and [Ninja](https://ninja-build.org/). After extracting the sshfs tarball, create a (temporary) build directory and run Meson: 34 | 35 | $ mkdir build; cd build 36 | $ meson .. 37 | 38 | Normally, the default build options will work fine. If you nevertheless want to adjust them, you can do so with the *mesonconf* command: 39 | 40 | $ mesonconf # list options 41 | $ mesonconf -D strip=true # set an option 42 | 43 | To build, test and install *SSHFS*, you then use *Ninja* (running the tests requires the [py.test](http://www.pytest.org/) *Python* module): 44 | 45 | $ ninja 46 | $ python -m pytest --numprocesses 10 test/ 47 | $ sudo ninja install 48 | -------------------------------------------------------------------------------- /sshfs.rst: -------------------------------------------------------------------------------- 1 | ======= 2 | SSHFS 3 | ======= 4 | 5 | --------------------------------------------- 6 | filesystem client based on SSH 7 | --------------------------------------------- 8 | 9 | :Manual section: 1 10 | :Manual group: User Commands 11 | 12 | Synopsis 13 | ======== 14 | 15 | To mount a filesystem:: 16 | 17 | sshfs [user@]host:[dir] mountpoint [options] 18 | 19 | If *host* is a numeric IPv6 address, it needs to be enclosed in square 20 | brackets. 21 | 22 | To unmount it:: 23 | 24 | fusermount3 -u mountpoint # Linux 25 | umount mountpoint # OS X, FreeBSD 26 | 27 | Description 28 | =========== 29 | 30 | SSHFS allows you to mount a remote filesystem using SSH (more precisely, the SFTP 31 | subsystem). Most SSH servers support and enable this SFTP access by default, so SSHFS is 32 | very simple to use - there's nothing to do on the server-side. 33 | 34 | By default, file permissions are ignored by SSHFS. Any user that can access the filesystem 35 | will be able to perform any operation that the remote server permits - based on the 36 | credentials that were used to connect to the server. If this is undesired, local 37 | permission checking can be enabled with ``-o default_permissions``. 38 | 39 | By default, only the mounting user will be able to access the filesystem. Access for other 40 | users can be enabled by passing ``-o allow_other``. In this case you most likely also 41 | want to use ``-o default_permissions``. 42 | 43 | It is recommended to run SSHFS as regular user (not as root). For this to work the 44 | mountpoint must be owned by the user. If username is omitted SSHFS will use the local 45 | username. If the directory is omitted, SSHFS will mount the (remote) home directory. If 46 | you need to enter a password sshfs will ask for it (actually it just runs ssh which ask 47 | for the password if needed). 48 | 49 | 50 | Options 51 | ======= 52 | 53 | 54 | -o opt,[opt...] 55 | mount options, see below for details. A a variety of SSH options can 56 | be given here as well, see the manual pages for *sftp(1)* and 57 | *ssh_config(5)*. 58 | 59 | -h, --help 60 | print help and exit. 61 | 62 | -V, --version 63 | print version information and exit. 64 | 65 | -d, --debug 66 | print debugging information. 67 | 68 | -p PORT 69 | equivalent to '-o port=PORT' 70 | 71 | -f 72 | do not daemonize, stay in foreground. 73 | 74 | -s 75 | Single threaded operation. 76 | 77 | -C 78 | equivalent to '-o compression=yes' 79 | 80 | -F ssh_configfile 81 | specifies alternative ssh configuration file 82 | 83 | -o reconnect 84 | automatically reconnect to server if connection is 85 | interrupted. Attempts to access files that were opened before the 86 | reconnection will give errors and need to be re-opened. 87 | 88 | -o delay_connect 89 | Don't immediately connect to server, wait until mountpoint is first 90 | accessed. 91 | 92 | -o sshfs_sync 93 | synchronous writes. This will slow things down, but may be useful 94 | in some situations. 95 | 96 | -o no_readahead 97 | Only read exactly the data that was requested, instead of 98 | speculatively reading more to anticipate the next read request. 99 | 100 | -o sync_readdir 101 | synchronous readdir. This will slow things down, but may be useful 102 | in some situations. 103 | 104 | -o namemap=TYPE 105 | How to map remote username/groupnames to local values. Possible values are: 106 | 107 | :none: no translation of the name space (default). 108 | 109 | :user: map the username/groupname of the remote user to username/groupname of the 110 | mounting user. 111 | 112 | :file: translate username/groupname based upon the contents of `--unamefile` 113 | and `--gnamefile`. 114 | 115 | -o unamefile=FILE 116 | file containing ``username:remote_username`` mappings for `-o namemap=file` 117 | 118 | -o gnamefile=FILE 119 | file containing ``groupname:remote_groupname`` mappings for `-o namemap=file` 120 | 121 | -o nomap=TYPE 122 | with namemap=file, how to handle missing mappings: 123 | 124 | :ignore: don't do any re-mapping 125 | :error: return an error (default) 126 | 127 | -o ssh_command=CMD 128 | execute CMD instead of 'ssh' 129 | 130 | -o sftp_server=SERV 131 | path to sftp server or subsystem (default: /usr/lib/gesftpserver) 132 | 133 | -o directport=PORT 134 | directly connect to PORT bypassing ssh 135 | 136 | -o passive 137 | communicate over stdin and stdout bypassing network. Useful for 138 | mounting local filesystem on the remote side. An example using 139 | dpipe command would be ``dpipe /usr/lib/openssh/gesftpserver = ssh 140 | RemoteHostname sshfs :/directory/to/be/shared ~/mnt/src -o passive`` 141 | 142 | -o disable_hardlink 143 | With this option set, attempts to call `link(2)` will fail with 144 | error code ENOSYS. 145 | 146 | -o transform_symlinks 147 | transform absolute symlinks on remote side to relative 148 | symlinks. This means that if e.g. on the server side 149 | ``/foo/bar/com`` is a symlink to ``/foo/blub``, SSHFS will 150 | transform the link target to ``../blub`` on the client side. 151 | 152 | -o follow_symlinks 153 | follow symlinks on the server, i.e. present them as regular 154 | files on the client. If a symlink is dangling (i.e, the target does 155 | not exist) the behavior depends on the remote server - the entry 156 | may appear as a symlink on the client, or it may appear as a 157 | regular file that cannot be accessed. 158 | 159 | -o no_check_root 160 | don't check for existence of 'dir' on server 161 | 162 | -o password_stdin 163 | read password from stdin (only for pam_mount!) 164 | 165 | -o dir_cache=BOOL 166 | Enables (*yes*) or disables (*no*) the SSHFS directory cache. The 167 | directory cache holds the names of directory entries. Enabling it 168 | allows `readdir(3)` system calls to be processed without network 169 | access. 170 | 171 | -o dcache_max_size=N 172 | sets the maximum size of the directory cache. 173 | 174 | -o dcache_timeout=N 175 | sets timeout for directory cache in seconds. 176 | 177 | -o dcache_{stat,link,dir}_timeout=N 178 | sets separate timeout for {attributes, symlinks, names} in the 179 | directory cache. 180 | 181 | -o dcache_clean_interval=N 182 | sets the interval for automatic cleaning of the directory cache. 183 | 184 | -o dcache_min_clean_interval=N 185 | sets the interval for forced cleaning of the directory cache 186 | when full. 187 | 188 | -o direct_io 189 | This option disables the use of page cache (file content cache) in 190 | the kernel for this filesystem. 191 | This has several affects: 192 | 193 | 1. Each read() or write() system call will initiate one or more read or 194 | write operations, data will not be cached in the kernel. 195 | 196 | 2. The return value of the read() and write() system calls will correspond 197 | to the return values of the read and write operations. This is useful 198 | for example if the file size is not known in advance (before reading it). 199 | e.g. /proc filesystem 200 | 201 | -o max_conns=N 202 | sets the maximum number of simultaneous SSH connections 203 | to use. Each connection is established with a separate SSH process. 204 | The primary purpose of this feature is to improve the responsiveness of the 205 | file system during large file transfers. When using more than once 206 | connection, the *password_stdin* and *passive* options can not be 207 | used, and the *buflimit* workaround is not supported. 208 | 209 | In addition, SSHFS accepts several options common to all FUSE file 210 | systems. These are described in the `mount.fuse` manpage (look 211 | for "general", "libfuse specific", and "high-level API" options). 212 | 213 | Caveats / Workarounds 214 | ===================== 215 | 216 | Hardlinks 217 | ~~~~~~~~~ 218 | 219 | If the SSH server supports the *hardlinks* extension, SSHFS will allow 220 | you to create hardlinks. However, hardlinks will always appear as 221 | individual files when seen through an SSHFS mount, i.e. they will 222 | appear to have different inodes and an *st_nlink* value of 1. 223 | 224 | 225 | SSHFS hangs for no apparent reason 226 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 227 | 228 | In some cases, attempts to access the SSHFS mountpoint may freeze if 229 | no filesystem activity has occurred for some time. This is typically 230 | caused by the SSH connection being dropped because of inactivity 231 | without SSHFS being informed about that. As a workaround, you can try 232 | to mount with ``-o ServerAliveInterval=15``. This will force the SSH 233 | connection to stay alive even if you have no activity. 234 | 235 | 236 | SSHFS hangs after the connection was interrupted 237 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 238 | 239 | By default, network operations in SSHFS run without timeouts, mirroring the 240 | default behavior of SSH itself. As a consequence, if the connection to the 241 | remote host is interrupted (e.g. because a network cable was removed), 242 | operations on files or directories under the mountpoint will block until the 243 | connection is either restored or closed altogether (e.g. manually). 244 | Applications that try to access such files or directories will generally appear 245 | to "freeze" when this happens. 246 | 247 | If it is acceptable to discard data being read or written, a quick workaround 248 | is to kill the responsible ``sshfs`` process, which will make any blocking 249 | operations on the mounted filesystem error out and thereby "unfreeze" the 250 | relevant applications. Note that force unmounting with ``fusermount3 -zu``, on 251 | the other hand, does not help in this case and will leave read/write operations 252 | in the blocking state. 253 | 254 | For a more automatic solution, one can use the ``-o ServerAliveInterval=15`` 255 | option mentioned above, which will drop the connection after not receiving a 256 | response for 3 * 15 = 45 seconds from the remote host. By also supplying ``-o 257 | reconnect``, one can ensure that the connection is re-established as soon as 258 | possible afterwards. As before, this will naturally lead to loss of data that 259 | was in the process of being read or written at the time when the connection was 260 | interrupted. 261 | 262 | 263 | Mounting from /etc/fstab 264 | ======================== 265 | 266 | To mount an SSHFS filesystem from ``/etc/fstab``, simply use ``sshfs`` 267 | as the file system type. (For backwards compatibility, you may also 268 | use ``fuse.sshfs``). 269 | 270 | 271 | See also 272 | ======== 273 | 274 | The `mount.fuse(8)` manpage. 275 | 276 | Getting Help 277 | ============ 278 | 279 | If you need help, please ask on the 280 | mailing list (subscribe at 281 | https://lists.sourceforge.net/lists/listinfo/fuse-sshfs). 282 | 283 | Please report any bugs on the GitHub issue tracker at 284 | https://github.com/libfuse/libfuse/issues. 285 | 286 | 287 | Authors 288 | ======= 289 | 290 | SSHFS is currently maintained by Nikolaus Rath , 291 | and was created by Miklos Szeredi . 292 | 293 | This man page was originally written by Bartosz Fenski 294 | for the Debian GNU/Linux distribution (but it may 295 | be used by others). 296 | -------------------------------------------------------------------------------- /test/.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | -------------------------------------------------------------------------------- /test/Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax = docker/dockerfile:1.2 2 | FROM archlinux 3 | 4 | RUN echo 'Server = https://sydney.mirror.pkgbuild.com/$repo/os/$arch' > /etc/pacman.d/mirrorlist 5 | 6 | # `makepkg` cannot (and should not) be run as root: 7 | RUN useradd --create-home build && \ 8 | mkdir /etc/sudoers.d/ && \ 9 | echo "build ALL=(ALL) NOPASSWD: ALL" > /etc/sudoers.d/build 10 | 11 | # Install packages 12 | RUN --mount=type=cache,sharing=locked,target=/var/cache/pacman \ 13 | pacman --sync --refresh --sysupgrade --noconfirm --needed \ 14 | base-devel \ 15 | fakeroot \ 16 | fuse3 \ 17 | git \ 18 | openssh \ 19 | python-exceptiongroup \ 20 | python-pytest \ 21 | python-pytest-xdist \ 22 | which 23 | 24 | # Continue execution (and `CMD`) as build: 25 | USER build 26 | WORKDIR /home/build 27 | 28 | # Install *Green End SFTP Server* 29 | COPY --chown=build:wheel gesftpserver-git gesftpserver-git 30 | RUN \ 31 | cd gesftpserver-git/ && \ 32 | makepkg --noconfirm --syncdeps --rmdeps --install --clean 33 | 34 | # Setup *SSH* 35 | USER root 36 | RUN ssh-keygen -A 37 | RUN echo -n "localhost " > ~/.ssh/known_hosts 38 | RUN cat /etc/ssh/ssh_host_rsa_key.pub >> ~/.ssh/known_hosts 39 | RUN ssh-keygen -b 1024 -t rsa -f ~/.ssh/id_rsa -P '' 40 | RUN cat ~/.ssh/id_rsa.pub >> ~/.ssh/authorized_keys 41 | RUN chmod 600 ~/.ssh/authorized_keys 42 | -------------------------------------------------------------------------------- /test/conftest.py: -------------------------------------------------------------------------------- 1 | import itertools 2 | import os 3 | import random 4 | import string 5 | import subprocess 6 | import time 7 | from contextlib import contextmanager 8 | from enum import Enum, auto 9 | from pathlib import Path 10 | from typing import Callable, Generator, NamedTuple 11 | 12 | import pytest 13 | from util import base_cmdline, basename, cleanup, umount, wait_for_mount 14 | 15 | 16 | __all__ = ['DataFile', 'SshfsDirs'] 17 | 18 | 19 | # If a test fails, wait a moment before retrieving the captured 20 | # stdout/stderr. When using a server process, this makes sure that we capture 21 | # any potential output of the server that comes *after* a test has failed. For 22 | # example, if a request handler raises an exception, the server first signals an 23 | # error to FUSE (causing the test to fail), and then logs the exception. Without 24 | # the extra delay, the exception will go into nowhere. 25 | @pytest.hookimpl(hookwrapper=True) 26 | def pytest_pyfunc_call(pyfuncitem: pytest.Function) -> Generator[None, None, None]: # noqa: unused-argument 27 | outcome = yield 28 | failed = outcome.excinfo is not None 29 | if failed: 30 | time.sleep(1) 31 | 32 | 33 | class DataFile(NamedTuple): 34 | path: Path 35 | data: bytes 36 | 37 | 38 | @pytest.fixture(scope='session', autouse=True) 39 | def data_file(tmp_path_factory: pytest.TempPathFactory) -> DataFile: 40 | data_dir = tmp_path_factory.mktemp('data') 41 | test_data_file = data_dir / 'data.txt' 42 | random.seed(12345) 43 | test_data = ''.join(random.choices(string.ascii_letters + string.digits, k=2048)).encode() 44 | test_data_file.write_bytes(test_data) 45 | return DataFile(test_data_file, test_data) 46 | 47 | 48 | def product_dict_values(options: dict) -> list: 49 | return [tuple(zip(options.keys(), value_combo)) for value_combo in itertools.product(*options.values())] 50 | 51 | 52 | class SshfsDirs(NamedTuple): 53 | src_dir: Path 54 | mnt_dir: Path 55 | cache_timeout: int 56 | 57 | 58 | class TestNamemapType(Enum): 59 | NONE = auto() 60 | USER = auto() 61 | FILE = auto() 62 | FILE_EMPTY = auto() 63 | 64 | 65 | @contextmanager 66 | def mount_sshfs( # noqa: too-many-locals 67 | tmp_path_factory: pytest.TempPathFactory, 68 | debug: bool, 69 | cache_timeout: int, 70 | sync_rd: bool, 71 | multiconn: bool, 72 | namemap: TestNamemapType, 73 | ) -> Generator[SshfsDirs, None, None]: 74 | # Test if we can ssh into localhost without password 75 | try: 76 | res = subprocess.call( 77 | [ 78 | 'ssh', 79 | '-o', 80 | 'KbdInteractiveAuthentication=no', 81 | '-o', 82 | 'ChallengeResponseAuthentication=no', 83 | '-o', 84 | 'PasswordAuthentication=no', 85 | 'localhost', 86 | '--', 87 | 'true', 88 | ], 89 | stdin=subprocess.DEVNULL, 90 | timeout=10, 91 | ) 92 | except subprocess.TimeoutExpired: 93 | res = 1 94 | if res != 0: 95 | pytest.fail('Unable to ssh into localhost without password prompt.') 96 | 97 | conf_dir = tmp_path_factory.mktemp('conf') 98 | mnt_dir = tmp_path_factory.mktemp('mnt') 99 | src_dir = tmp_path_factory.mktemp('src') 100 | 101 | cmdline = [*base_cmdline, str(basename / 'build/sshfs'), '-f', f'localhost:{src_dir}', str(mnt_dir)] 102 | if debug: 103 | cmdline += ['-o', 'sshfs_debug'] 104 | 105 | if sync_rd: 106 | cmdline += ['-o', 'sync_readdir'] 107 | 108 | # SSHFS Cache 109 | if cache_timeout == 0: 110 | cmdline += ['-o', 'dir_cache=no'] 111 | else: 112 | cmdline += [ 113 | '-o', 114 | f'dcache_timeout={cache_timeout}', 115 | '-o', 116 | 'dir_cache=yes', 117 | ] 118 | 119 | # FUSE Cache 120 | cmdline += [ 121 | '-o', 122 | 'entry_timeout=0', 123 | '-o', 124 | 'attr_timeout=0', 125 | ] 126 | 127 | if multiconn: 128 | cmdline += ['-o', 'max_conns=3'] 129 | 130 | match namemap: 131 | case TestNamemapType.USER: 132 | cmdline += ['-o', 'namemap=user'] 133 | case TestNamemapType.FILE: 134 | cmdline += ['-o', 'namemap=file'] 135 | unamemap_path = conf_dir / 'unamefile.txt' 136 | with unamemap_path.open('w') as sr: 137 | sr.write('foo_user:root\n') 138 | gnamemap_path = conf_dir / 'gnamefile.txt' 139 | gnamemap_path.write_text('bar_group:root\n') 140 | cmdline += ['-o', f'unamefile={unamemap_path}', '-o', f'gnamefile={gnamemap_path}'] 141 | case TestNamemapType.FILE_EMPTY: 142 | cmdline += ['-o', 'namemap=file'] 143 | unamemap_path = conf_dir / 'unamefile.txt' 144 | unamemap_path.touch() 145 | gnamemap_path = conf_dir / 'gnamefile.txt' 146 | gnamemap_path.touch() 147 | cmdline += ['-o', f'unamefile={unamemap_path}', '-o', f'gnamefile={gnamemap_path}'] 148 | 149 | new_env = dict(os.environ) # copy, don't modify 150 | 151 | # Abort on warnings from glib 152 | new_env['G_DEBUG'] = 'fatal-warnings' 153 | 154 | with subprocess.Popen(cmdline, env=new_env) as mount_process: 155 | try: # noqa: no-else-return 156 | wait_for_mount(mount_process, mnt_dir) 157 | yield SshfsDirs(src_dir, mnt_dir, cache_timeout) 158 | except: # noqa: E722 159 | cleanup(mount_process, mnt_dir) 160 | raise 161 | else: 162 | umount(mount_process, mnt_dir) 163 | 164 | 165 | def create_sshfs_dirs_fixture(name_map: TestNamemapType) -> Callable: 166 | @pytest.fixture( 167 | scope='session', 168 | params=product_dict_values( 169 | { 170 | 'debug': [False, True], 171 | 'cache_timeout': [0, 1], 172 | 'sync_rd': [True, False], 173 | 'multiconn': [True, False], 174 | } 175 | ), 176 | ) 177 | def fixture( # noqa: too-many-statements 178 | tmp_path_factory: pytest.TempPathFactory, request: pytest.FixtureRequest 179 | ) -> Generator[SshfsDirs, None, None]: 180 | param_dict = dict(request.param) 181 | with mount_sshfs( 182 | tmp_path_factory, 183 | param_dict['debug'], 184 | param_dict['cache_timeout'], 185 | param_dict['sync_rd'], 186 | param_dict['multiconn'], 187 | name_map, 188 | ) as sshfs_dirs_: 189 | yield sshfs_dirs_ 190 | 191 | return fixture 192 | 193 | 194 | sshfs_dirs = create_sshfs_dirs_fixture(TestNamemapType.NONE) 195 | sshfs_dirs_namemap_user = create_sshfs_dirs_fixture(TestNamemapType.USER) 196 | sshfs_dirs_namemap_file = create_sshfs_dirs_fixture(TestNamemapType.FILE) 197 | sshfs_dirs_namemap_file_not_found = create_sshfs_dirs_fixture(TestNamemapType.FILE_EMPTY) 198 | -------------------------------------------------------------------------------- /test/docker_test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -o xtrace 4 | set -e 5 | 6 | export DOCKER_HOST="unix://${XDG_RUNTIME_DIR}/docker.sock" 7 | 8 | container_name=sshfs_container 9 | image_name=sshfs_image 10 | dir=$(dirname "$0") 11 | 12 | docker container stop $container_name || true 13 | docker container rm $container_name || true 14 | 15 | docker container run \ 16 | --detach \ 17 | --rm \ 18 | --interactive \ 19 | --name $container_name \ 20 | --mount type=bind,source="${dir}",target=/tmp/sshfs/test/ \ 21 | --cap-add SYS_ADMIN \ 22 | --device /dev/fuse \ 23 | $image_name 24 | 25 | docker exec $container_name groupadd bar_group 26 | docker exec $container_name useradd foo_user --no-user-group --gid bar_group 27 | 28 | docker exec $container_name mkdir /tmp/sshfs/build/ 29 | docker cp $dir/../build/sshfs $container_name:/tmp/sshfs/build/ 30 | 31 | docker exec $container_name sh -c '/usr/bin/sshd -Dp 22 &' 32 | docker exec --workdir /tmp/sshfs/ $container_name python -m pytest --numprocesses 10 test/ 33 | 34 | docker container stop $container_name 35 | -------------------------------------------------------------------------------- /test/gesftpserver-git/PKGBUILD: -------------------------------------------------------------------------------- 1 | pkgname=gesftpserver-git 2 | _pkgname=sftpserver 3 | pkgver=2.r11.g11eb2e3 4 | pkgrel=1 5 | pkgdesc="Green End SFTP Server - experimental free SFTP server" 6 | arch=('any') 7 | url="http://www.greenend.org.uk/rjk/sftpserver/" 8 | license=('GPL2') 9 | depends=() 10 | source=('git+https://github.com/ewxrjk/sftpserver.git') 11 | sha512sums=('SKIP') 12 | 13 | if [ -z "$PREFIX" ] || [ "$PREFIX" != /data/data/com.termux/files/usr ]; then 14 | PREFIX='/usr' 15 | fi 16 | 17 | pkgver() { 18 | cd "$_pkgname" 19 | git describe --long --abbrev=7 | sed 's/\([^-]*-g\)/r\1/;s/-/./g' 20 | } 21 | 22 | build() { 23 | cd "$_pkgname" 24 | ./autogen.sh 25 | ./configure --prefix=$PREFIX 26 | make 27 | } 28 | 29 | check() { 30 | cd "$_pkgname" 31 | make check || warning "Tests failed" 32 | } 33 | 34 | package() { 35 | cd "$_pkgname" 36 | make DESTDIR=${pkgdir} install 37 | mv "${pkgdir}${PREFIX}/libexec/" "${pkgdir}${PREFIX}/lib/" 38 | } 39 | -------------------------------------------------------------------------------- /test/lint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | pip install --user pre-commit 4 | pre-commit run --all-files --show-diff-on-failure 5 | -------------------------------------------------------------------------------- /test/lsan_suppress.txt: -------------------------------------------------------------------------------- 1 | # Suppression file for address sanitizer. 2 | 3 | # There are some leaks in command line option parsing. They should be 4 | # fixed at some point, but are harmless since the consume just a small, 5 | # constant amount of memory and do not grow. 6 | leak:fuse_opt_parse 7 | 8 | 9 | # Leaks in fusermount3 are harmless as well (it's a short-lived 10 | # process) - but patches are welcome! 11 | leak:fusermount3.c 12 | -------------------------------------------------------------------------------- /test/meson.build: -------------------------------------------------------------------------------- 1 | test_scripts = [ 'conftest.py', 'pytest.ini', 'test_sshfs.py', 2 | 'util.py' ] 3 | custom_target('test_scripts', input: test_scripts, 4 | output: test_scripts, build_by_default: true, 5 | command: ['cp', '-fPp', 6 | '@INPUT@', meson.current_build_dir() ]) 7 | 8 | # Provide something helpful when running 'ninja test' 9 | wrong_cmd = executable('wrong_command', 'wrong_command.c', 10 | install: false) 11 | test('wrong_cmd', wrong_cmd) 12 | -------------------------------------------------------------------------------- /test/pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | addopts = --verbose --assert=rewrite --tb=native -x -r a 3 | markers = uses_fuse 4 | -------------------------------------------------------------------------------- /test/test_sshfs.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import errno 3 | import filecmp 4 | import grp 5 | import os 6 | import pwd 7 | import shutil 8 | import stat 9 | import sys 10 | from pathlib import Path 11 | from tempfile import NamedTemporaryFile 12 | 13 | import pytest 14 | from conftest import DataFile, SshfsDirs 15 | from util import fuse_test_marker, safe_sleep 16 | 17 | 18 | pytestmark = fuse_test_marker() 19 | 20 | 21 | class NameGenerator: 22 | counter = 0 23 | 24 | def __call__(self) -> str: 25 | self.counter += 1 26 | return f'testfile_{self.counter}' 27 | 28 | 29 | name_generator = NameGenerator() 30 | 31 | 32 | def is_docker() -> bool: 33 | if Path('/.dockerenv').is_file(): 34 | return True 35 | cgroup_path = Path('/proc/self/cgroup') 36 | return cgroup_path.is_file() and 'docker' in cgroup_path.read_text(encoding='utf-8') 37 | 38 | 39 | def name_in_dir(name: str, path: Path) -> bool: 40 | return any(name == cur_path.name for cur_path in path.iterdir()) 41 | 42 | 43 | def os_create(path: Path) -> None: 44 | with os.fdopen(os.open(path, os.O_CREAT | os.O_RDWR)) as _: 45 | pass 46 | 47 | 48 | def test_utimens(sshfs_dirs: SshfsDirs) -> None: 49 | path = sshfs_dirs.mnt_dir / name_generator() 50 | path.mkdir() 51 | fstat = path.lstat() 52 | 53 | atime_ns = fstat.st_atime_ns + 42 54 | mtime_ns = fstat.st_mtime_ns - 42 55 | os.utime(path, None, ns=(atime_ns, mtime_ns)) 56 | 57 | fstat = path.lstat() 58 | 59 | assert fstat.st_atime_ns == atime_ns 60 | assert fstat.st_mtime_ns == mtime_ns 61 | 62 | 63 | def test_utimens_now(sshfs_dirs: SshfsDirs) -> None: 64 | path = sshfs_dirs.mnt_dir / name_generator() 65 | os_create(path) 66 | os.utime(path, None) 67 | 68 | fstat = path.lstat() 69 | # We should get now-timestamps 70 | assert fstat.st_atime_ns != 0 71 | assert fstat.st_mtime_ns != 0 72 | 73 | 74 | def test_statvfs(sshfs_dirs: SshfsDirs) -> None: 75 | os.statvfs(sshfs_dirs.mnt_dir) 76 | 77 | 78 | def test_chmod(sshfs_dirs: SshfsDirs) -> None: 79 | mode = 0o600 80 | path = sshfs_dirs.mnt_dir / name_generator() 81 | os_create(path) 82 | path.chmod(mode) 83 | 84 | assert path.lstat().st_mode & 0o777 == mode 85 | 86 | 87 | def test_create(sshfs_dirs: SshfsDirs) -> None: 88 | name = name_generator() 89 | path = sshfs_dirs.mnt_dir / name 90 | with pytest.raises(OSError) as exc_info: 91 | path.lstat() 92 | assert exc_info.value.errno == errno.ENOENT 93 | assert not name_in_dir(name, sshfs_dirs.mnt_dir) 94 | 95 | os_create(path) 96 | 97 | assert name_in_dir(name, sshfs_dirs.mnt_dir) 98 | fstat = path.lstat() 99 | assert stat.S_ISREG(fstat.st_mode) 100 | assert fstat.st_nlink == 1 101 | assert fstat.st_size == 0 102 | assert fstat.st_uid == os.getuid() 103 | assert fstat.st_gid == os.getgid() 104 | 105 | 106 | def test_open_read(sshfs_dirs: SshfsDirs, data_file: DataFile) -> None: 107 | name = name_generator() 108 | with (sshfs_dirs.src_dir / name).open('wb') as fh_out, data_file.path.open('rb') as fh_in: 109 | shutil.copyfileobj(fh_in, fh_out) 110 | 111 | assert filecmp.cmp(sshfs_dirs.mnt_dir / name, data_file.path, False) 112 | 113 | 114 | def test_open_write(sshfs_dirs: SshfsDirs, data_file: DataFile) -> None: 115 | name = name_generator() 116 | fd = os.open(sshfs_dirs.src_dir / name, os.O_CREAT | os.O_RDWR) 117 | os.close(fd) 118 | path = sshfs_dirs.mnt_dir / name 119 | with path.open('wb') as fh_out, data_file.path.open('rb') as fh_in: 120 | shutil.copyfileobj(fh_in, fh_out) 121 | 122 | assert filecmp.cmp(path, data_file.path, False) 123 | 124 | 125 | def test_append(sshfs_dirs: SshfsDirs) -> None: 126 | name = name_generator() 127 | os_create(sshfs_dirs.src_dir / name) 128 | path = sshfs_dirs.mnt_dir / name 129 | with os.fdopen(os.open(path, os.O_WRONLY), 'wb') as fd: 130 | fd.write(b'foo\n') 131 | with os.fdopen(os.open(path, os.O_WRONLY | os.O_APPEND), 'ab') as fd: 132 | fd.write(b'bar\n') 133 | 134 | assert path.read_bytes() == b'foo\nbar\n' 135 | 136 | 137 | def test_seek(sshfs_dirs: SshfsDirs) -> None: 138 | name = name_generator() 139 | os_create(sshfs_dirs.src_dir / name) 140 | path = sshfs_dirs.mnt_dir / name 141 | with os.fdopen(os.open(path, os.O_WRONLY), 'wb') as fd: 142 | fd.seek(1, os.SEEK_SET) 143 | fd.write(b'foobar\n') 144 | with os.fdopen(os.open(path, os.O_WRONLY), 'wb') as fd: 145 | fd.seek(4, os.SEEK_SET) 146 | fd.write(b'com') 147 | 148 | assert path.read_bytes() == b'\0foocom\n' 149 | 150 | 151 | def test_truncate_path(sshfs_dirs: SshfsDirs, data_file: DataFile) -> None: 152 | assert len(data_file.data) > 1024 153 | 154 | path = sshfs_dirs.mnt_dir / name_generator() 155 | with path.open('wb') as fh_: 156 | fh_.write(data_file.data) 157 | 158 | fstat = path.lstat() 159 | size = fstat.st_size 160 | assert size == len(data_file.data) 161 | 162 | # Add zeros at the end 163 | os.truncate(path, size + 1024) 164 | assert path.lstat().st_size == size + 1024 165 | with path.open('rb') as fh_: 166 | assert fh_.read(size) == data_file.data 167 | assert fh_.read(1025) == b'\0' * 1024 168 | 169 | # Truncate data 170 | os.truncate(path, size - 1024) 171 | assert path.lstat().st_size == size - 1024 172 | with path.open('rb') as fh_: 173 | assert fh_.read(size) == data_file.data[: size - 1024] 174 | 175 | path.unlink() 176 | 177 | 178 | def test_truncate_fd(sshfs_dirs: SshfsDirs, data_file: DataFile) -> None: 179 | assert len(data_file.data) > 1024 180 | with NamedTemporaryFile('w+b', 0, dir=sshfs_dirs.mnt_dir) as fh_: 181 | fd = fh_.fileno() 182 | fh_.write(data_file.data) 183 | fstat = os.fstat(fd) 184 | size = fstat.st_size 185 | assert size == len(data_file.data) 186 | 187 | # Add zeros at the end 188 | os.ftruncate(fd, size + 1024) 189 | assert os.fstat(fd).st_size == size + 1024 190 | fh_.seek(0) 191 | assert fh_.read(size) == data_file.data 192 | assert fh_.read(1025) == b'\0' * 1024 193 | 194 | # Truncate data 195 | os.ftruncate(fd, size - 1024) 196 | assert os.fstat(fd).st_size == size - 1024 197 | fh_.seek(0) 198 | assert fh_.read(size) == data_file.data[: size - 1024] 199 | 200 | 201 | def test_passthrough(sshfs_dirs: SshfsDirs) -> None: 202 | name = name_generator() 203 | src_path = sshfs_dirs.src_dir / name 204 | mnt_path = sshfs_dirs.src_dir / name 205 | assert not name_in_dir(name, sshfs_dirs.src_dir) 206 | assert not name_in_dir(name, sshfs_dirs.mnt_dir) 207 | src_path.write_text('Hello, world') 208 | assert name_in_dir(name, sshfs_dirs.src_dir) 209 | if sshfs_dirs.cache_timeout: 210 | safe_sleep(sshfs_dirs.cache_timeout + 1) 211 | assert name_in_dir(name, sshfs_dirs.mnt_dir) 212 | assert src_path.lstat() == mnt_path.lstat() 213 | 214 | name = name_generator() 215 | src_path = sshfs_dirs.src_dir / name 216 | mnt_path = sshfs_dirs.src_dir / name 217 | assert not name_in_dir(name, sshfs_dirs.src_dir) 218 | assert not name_in_dir(name, sshfs_dirs.mnt_dir) 219 | with mnt_path.open('w', encoding='utf-8') as fh_: 220 | fh_.write('Hello, world') 221 | assert name_in_dir(name, sshfs_dirs.src_dir) 222 | if sshfs_dirs.cache_timeout: 223 | safe_sleep(sshfs_dirs.cache_timeout + 1) 224 | assert name_in_dir(name, sshfs_dirs.mnt_dir) 225 | assert src_path.lstat() == mnt_path.lstat() 226 | 227 | 228 | def test_mkdir(sshfs_dirs: SshfsDirs) -> None: 229 | dirname = name_generator() 230 | path = sshfs_dirs.mnt_dir / dirname 231 | path.mkdir() 232 | fstat = path.lstat() 233 | assert stat.S_ISDIR(fstat.st_mode) 234 | assert not list(path.iterdir()) 235 | assert fstat.st_nlink in (1, 2) 236 | assert name_in_dir(dirname, sshfs_dirs.mnt_dir) 237 | 238 | 239 | def test_readdir(sshfs_dirs: SshfsDirs, data_file: DataFile) -> None: 240 | newdir = name_generator() 241 | src_newdir = sshfs_dirs.src_dir / newdir 242 | mnt_newdir = sshfs_dirs.mnt_dir / newdir 243 | file_ = src_newdir / name_generator() 244 | subdir = src_newdir / name_generator() 245 | subfile = subdir / name_generator() 246 | 247 | src_newdir.mkdir() 248 | shutil.copyfile(data_file.path, file_) 249 | subdir.mkdir() 250 | shutil.copyfile(data_file.path, subfile) 251 | 252 | listdir_is = sorted(path.name for path in mnt_newdir.iterdir()) 253 | listdir_should = [file_.name, subdir.name] 254 | listdir_should.sort() 255 | assert listdir_is == listdir_should 256 | 257 | file_.unlink() 258 | subfile.unlink() 259 | subdir.rmdir() 260 | src_newdir.rmdir() 261 | 262 | 263 | def test_rmdir(sshfs_dirs: SshfsDirs) -> None: 264 | dirname = name_generator() 265 | filename = name_generator() 266 | (sshfs_dirs.src_dir / dirname).mkdir() 267 | (sshfs_dirs.src_dir / dirname / filename).touch() 268 | dir_path = sshfs_dirs.mnt_dir / dirname 269 | file_path = dir_path / filename 270 | 271 | if sshfs_dirs.cache_timeout: 272 | safe_sleep(sshfs_dirs.cache_timeout + 1) 273 | 274 | assert name_in_dir(dirname, sshfs_dirs.mnt_dir) 275 | 276 | with pytest.raises(OSError) as exc_info: 277 | dir_path.rmdir() 278 | assert exc_info.value.errno == errno.ENOTEMPTY 279 | assert exc_info.value.filename == str(dir_path) 280 | 281 | file_path.unlink() 282 | dir_path.rmdir() 283 | with pytest.raises(OSError) as exc_info: 284 | dir_path.lstat() 285 | assert exc_info.value.errno == errno.ENOENT 286 | 287 | assert not name_in_dir(dirname, sshfs_dirs.mnt_dir) 288 | assert not name_in_dir(dirname, sshfs_dirs.src_dir) 289 | 290 | 291 | def test_rename(sshfs_dirs: SshfsDirs) -> None: 292 | name1 = name_generator() 293 | name2 = name_generator() 294 | path1 = sshfs_dirs.mnt_dir / name1 295 | path2 = sshfs_dirs.mnt_dir / name2 296 | 297 | data1 = b'foo' 298 | with path1.open('wb', buffering=0) as fh_: 299 | fh_.write(data1) 300 | 301 | fstat1 = path1.lstat() 302 | path1.rename(path2) 303 | if sshfs_dirs.cache_timeout: 304 | safe_sleep(sshfs_dirs.cache_timeout) 305 | 306 | fstat2 = path2.lstat() 307 | 308 | with path2.open('rb', buffering=0) as fh_: 309 | data2 = fh_.read() 310 | 311 | for attr in ('st_mode', 'st_dev', 'st_uid', 'st_gid', 'st_size', 'st_atime_ns', 'st_mtime_ns', 'st_ino'): 312 | assert getattr(fstat1, attr) == getattr(fstat2, attr) 313 | assert getattr(fstat2, 'st_ctime_ns') >= getattr(fstat1, 'st_ctime_ns') 314 | 315 | assert name_in_dir(path2.name, sshfs_dirs.mnt_dir) 316 | assert data1 == data2 317 | 318 | 319 | def test_link(sshfs_dirs: SshfsDirs, data_file: DataFile) -> None: 320 | path1 = sshfs_dirs.mnt_dir / name_generator() 321 | path2 = sshfs_dirs.mnt_dir / name_generator() 322 | shutil.copyfile(data_file.path, path1) 323 | assert filecmp.cmp(path1, data_file.path, False) 324 | 325 | fstat1 = path1.lstat() 326 | assert fstat1.st_nlink == 1 327 | 328 | path2.hardlink_to(path1) 329 | 330 | # The link operation changes st_ctime, and if we're unlucky 331 | # the kernel will keep the old value cached for path1, and 332 | # retrieve the new value for path2 (at least, this is the only 333 | # way I can explain the test failure). To avoid this problem, 334 | # we need to wait until the cached value has expired. 335 | if sshfs_dirs.cache_timeout: 336 | safe_sleep(sshfs_dirs.cache_timeout) 337 | 338 | fstat1 = path1.lstat() 339 | assert fstat1.st_nlink == 2 340 | 341 | fstat2 = path2.lstat() 342 | for attr in ('st_mode', 'st_dev', 'st_uid', 'st_gid', 'st_size', 'st_atime_ns', 'st_mtime_ns', 'st_ctime_ns'): 343 | assert getattr(fstat1, attr) == getattr(fstat2, attr) 344 | 345 | assert name_in_dir(path2.name, sshfs_dirs.mnt_dir) 346 | assert filecmp.cmp(path1, path2, False) 347 | 348 | path2.unlink() 349 | 350 | assert not name_in_dir(path2.name, sshfs_dirs.mnt_dir) 351 | with pytest.raises(FileNotFoundError): 352 | path2.lstat() 353 | 354 | path1.unlink() 355 | 356 | 357 | def test_symlink(sshfs_dirs: SshfsDirs) -> None: 358 | linkname = name_generator() 359 | path = sshfs_dirs.mnt_dir / linkname 360 | path.symlink_to('/imaginary/dest') 361 | fstat = path.lstat() 362 | assert stat.S_ISLNK(fstat.st_mode) 363 | assert str(path.readlink()) == '/imaginary/dest' 364 | assert fstat.st_nlink == 1 365 | assert name_in_dir(linkname, sshfs_dirs.mnt_dir) 366 | 367 | 368 | def test_unlink(sshfs_dirs: SshfsDirs) -> None: 369 | name = name_generator() 370 | path = sshfs_dirs.mnt_dir / name 371 | (sshfs_dirs.src_dir / name).write_bytes(b'hello') 372 | if sshfs_dirs.cache_timeout: 373 | safe_sleep(sshfs_dirs.cache_timeout + 1) 374 | assert name_in_dir(name, sshfs_dirs.mnt_dir) 375 | path.unlink() 376 | with pytest.raises(OSError) as exc_info: 377 | path.lstat() 378 | assert exc_info.value.errno == errno.ENOENT 379 | assert not name_in_dir(name, sshfs_dirs.mnt_dir) 380 | assert not name_in_dir(name, sshfs_dirs.src_dir) 381 | 382 | 383 | def test_open_unlink(sshfs_dirs: SshfsDirs) -> None: 384 | name = name_generator() 385 | data1 = b'foo' 386 | data2 = b'bar' 387 | path = sshfs_dirs.mnt_dir / name 388 | with path.open('wb+', buffering=0) as fh_: 389 | fh_.write(data1) 390 | path.unlink() 391 | with pytest.raises(OSError) as exc_info: 392 | path.lstat() 393 | assert exc_info.value.errno == errno.ENOENT 394 | assert not name_in_dir(name, sshfs_dirs.mnt_dir) 395 | fh_.write(data2) 396 | fh_.seek(0) 397 | assert fh_.read() == data1 + data2 398 | 399 | 400 | def test_namemap_user(sshfs_dirs_namemap_user: SshfsDirs) -> None: 401 | name = name_generator() 402 | src_path = sshfs_dirs_namemap_user.src_dir / name 403 | src_path.mkdir() 404 | 405 | cur_pw = pwd.getpwuid(os.geteuid()) 406 | cur_grp = grp.getgrgid(cur_pw.pw_gid) 407 | 408 | mnt_path = sshfs_dirs_namemap_user.mnt_dir / name 409 | assert mnt_path.owner() == cur_pw.pw_name 410 | assert mnt_path.group() == cur_grp.gr_name 411 | 412 | 413 | def test_namemap_file(sshfs_dirs_namemap_file: SshfsDirs) -> None: 414 | if not is_docker(): 415 | pytest.skip('Docker required') 416 | 417 | name = name_generator() 418 | src_path = sshfs_dirs_namemap_file.src_dir / name 419 | src_path.mkdir() 420 | 421 | mnt_path = sshfs_dirs_namemap_file.mnt_dir / name 422 | assert mnt_path.owner() == 'foo_user' 423 | assert mnt_path.group() == 'bar_group' 424 | 425 | 426 | def test_namemap_file_empty(sshfs_dirs_namemap_file_not_found: SshfsDirs) -> None: 427 | name = name_generator() 428 | src_path = sshfs_dirs_namemap_file_not_found.src_dir / name 429 | src_path.mkdir() 430 | 431 | mnt_path = sshfs_dirs_namemap_file_not_found.mnt_dir / name 432 | 433 | pw_nobody = pwd.getpwnam('nobody') 434 | grp_nobody = grp.getgrgid(pw_nobody.pw_gid) 435 | assert mnt_path.owner() == pw_nobody.pw_name 436 | assert mnt_path.group() == grp_nobody.gr_name 437 | 438 | 439 | def test_chown(sshfs_dirs_namemap_file: SshfsDirs) -> None: 440 | if os.getuid() != 0 or not is_docker(): 441 | pytest.skip('Root and docker required') 442 | 443 | name = name_generator() 444 | src_path = sshfs_dirs_namemap_file.src_dir / name 445 | mnt_path = sshfs_dirs_namemap_file.mnt_dir / name 446 | 447 | mnt_path.mkdir() 448 | fstat = mnt_path.lstat() 449 | gid = fstat.st_gid 450 | 451 | pw_new = pwd.getpwnam('foo_user') 452 | 453 | uid_new = pw_new.pw_uid 454 | os.chown(mnt_path, uid_new, -1) 455 | fstat = mnt_path.lstat() 456 | assert fstat.st_uid == uid_new 457 | assert fstat.st_gid == gid 458 | 459 | gid_new = pw_new.pw_gid 460 | os.chown(mnt_path, -1, gid_new) 461 | fstat = mnt_path.lstat() 462 | assert fstat.st_uid == uid_new 463 | assert fstat.st_gid == gid_new 464 | 465 | pw_root = pwd.getpwnam('root') 466 | 467 | fstat = src_path.lstat() 468 | assert fstat.st_uid == pw_root.pw_uid 469 | assert fstat.st_gid == pw_root.pw_gid 470 | 471 | 472 | if __name__ == '__main__': 473 | sys.exit(pytest.main([__file__] + sys.argv[1:])) 474 | -------------------------------------------------------------------------------- /test/util.py: -------------------------------------------------------------------------------- 1 | import os 2 | import stat 3 | import subprocess 4 | import time 5 | from pathlib import Path 6 | 7 | import pytest 8 | 9 | 10 | __all__ = ['base_cmdline', 'basename', 'cleanup', 'fuse_test_marker', 'safe_sleep', 'umount', 'wait_for_mount'] 11 | 12 | basename = Path(__file__).parent.parent 13 | 14 | 15 | def wait_for_mount(mount_process: subprocess.Popen, mnt_dir: Path, test_fn=os.path.ismount) -> None: 16 | elapsed = 0 17 | while elapsed < 30: 18 | if test_fn(mnt_dir): 19 | return 20 | if mount_process.poll() is not None: 21 | pytest.fail('file system process terminated prematurely') 22 | time.sleep(0.1) 23 | elapsed += 0.1 24 | pytest.fail('mountpoint failed to come up') 25 | 26 | 27 | def cleanup(mount_process: subprocess.Popen, mnt_dir: Path) -> None: 28 | subprocess.call(['fusermount3', '-z', '-u', str(mnt_dir)], stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT) 29 | mount_process.terminate() 30 | try: 31 | mount_process.wait(1) 32 | except subprocess.TimeoutExpired: 33 | mount_process.kill() 34 | 35 | 36 | def umount(mount_process: subprocess.Popen, mnt_dir: Path) -> None: 37 | subprocess.check_call(['fusermount3', '-z', '-u', str(mnt_dir)]) 38 | assert not os.path.ismount(mnt_dir) 39 | try: 40 | code = mount_process.wait(30) 41 | if code != 0: 42 | pytest.fail(f'file system process terminated with code {code}') 43 | except subprocess.TimeoutExpired: 44 | pytest.fail('mount process did not terminate') 45 | 46 | 47 | def safe_sleep(secs: int) -> None: 48 | '''Like time.sleep(), but sleep for at least *secs* 49 | 50 | `time.sleep` may sleep less than the given period if a signal is 51 | received. This function ensures that we sleep for at least the 52 | desired time. 53 | ''' 54 | 55 | now = time.time() 56 | end = now + secs 57 | while now < end: 58 | time.sleep(end - now) 59 | now = time.time() 60 | 61 | 62 | def fuse_test_marker() -> pytest.MarkDecorator: 63 | '''Return a pytest.marker that indicates FUSE availability 64 | 65 | If system/user/environment does not support FUSE, return 66 | a `pytest.mark.skip` object with more details. If FUSE is 67 | supported, return `pytest.mark.uses_fuse()`. 68 | ''' 69 | 70 | def skip(x): 71 | return pytest.mark.skip(reason=x) 72 | 73 | with subprocess.Popen(['which', 'fusermount3'], stdout=subprocess.PIPE, universal_newlines=True) as which: 74 | fusermount_path = which.communicate()[0].strip() 75 | 76 | if not fusermount_path or which.returncode != 0: 77 | return skip('Can\'t find fusermount3 executable') 78 | 79 | if not Path('/dev/fuse').exists(): 80 | return skip('FUSE kernel module does not seem to be loaded') 81 | 82 | if os.getuid() == 0: 83 | return pytest.mark.uses_fuse() 84 | 85 | mode = Path(fusermount_path).stat().st_mode 86 | if mode & stat.S_ISUID == 0: 87 | return skip('fusermount3 executable not setuid, and we are not root.') 88 | 89 | try: 90 | with os.fdopen(os.open('/dev/fuse', os.O_RDWR)) as _: 91 | pass 92 | except OSError as exc: 93 | return skip(f'Unable to open /dev/fuse: {exc.strerror}') 94 | 95 | return pytest.mark.uses_fuse() 96 | 97 | 98 | # Use valgrind if requested 99 | if os.environ.get('TEST_WITH_VALGRIND', 'no').lower().strip() not in ('no', 'false', '0'): 100 | base_cmdline = ['valgrind', '-q', '--'] 101 | else: 102 | base_cmdline = [] 103 | -------------------------------------------------------------------------------- /test/wrong_command.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | int main(void) { 4 | fprintf(stderr, "\x1B[31m\e[1m" 5 | "This is not the command you are looking for.\n" 6 | "You probably want to run 'python -m pytest test/' instead" 7 | "\e[0m\n"); 8 | return 1; 9 | } 10 | -------------------------------------------------------------------------------- /utils/install_helper.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # Don't call this script. It is used internally by the Meson 4 | # build system. Thank you for your cooperation. 5 | # 6 | 7 | set -e 8 | 9 | bindir="$2" 10 | sbindir="$1" 11 | prefix="${MESON_INSTALL_DESTDIR_PREFIX}" 12 | 13 | mkdir -p "${prefix}/${sbindir}" 14 | 15 | ln -svf --relative "${prefix}/${bindir}/sshfs" \ 16 | "${prefix}/${sbindir}/mount.sshfs" 17 | 18 | ln -svf --relative "${prefix}/${bindir}/sshfs" \ 19 | "${prefix}/${sbindir}/mount.fuse.sshfs" 20 | --------------------------------------------------------------------------------