├── .gitignore ├── .travis.yml ├── COPYING ├── Makefile ├── README.md ├── configure ├── install.sh ├── main.cpp ├── test-dummyeditor.pl ├── test1.sh ├── test2.sh ├── test3.sh ├── test4.sh ├── test5.sh ├── test6.sh ├── test7.sh └── testlib.sh /.gitignore: -------------------------------------------------------------------------------- 1 | gitbslr.so 2 | test/ 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: cpp 2 | os: 3 | - linux 4 | # - osx 5 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-2.0-only 2 | # GitBSLR is available under the same license as Git itself. 3 | 4 | ifeq ($(shell uname -s),Darwin) 5 | $(error This program does not function on macOS. I couldn't find a functional LD_PRELOAD equivalent.) 6 | endif 7 | 8 | OPT ?= 0 9 | ifeq ($(OPT),0) 10 | DEBUG ?= 1 11 | else 12 | DEBUG ?= 0 13 | endif 14 | 15 | CFLAGS = -g 16 | CXXFLAGS = $(CFLAGS) 17 | 18 | TRUE_FLAGS := -std=c++98 -fno-rtti -fvisibility=hidden 19 | TRUE_FLAGS += -fvisibility=hidden -Wall -Wmissing-declarations -pipe -fno-exceptions 20 | TRUE_FLAGS += -fPIC -ldl -Wl,-z,relro,-z,now,--no-undefined -shared 21 | 22 | ifneq ($(OPT),0) 23 | TRUE_FLAGS += -Os -fomit-frame-pointer -fmerge-all-constants -fvisibility=hidden 24 | TRUE_FLAGS += -fno-unwind-tables -fno-asynchronous-unwind-tables 25 | TRUE_FLAGS += -ffunction-sections -fdata-sections 26 | TRUE_FLAGS += -fno-ident -DNDEBUG 27 | TRUE_FLAGS += -Wl,--gc-sections,--build-id=none,--hash-style=gnu,--relax 28 | ifneq ($(DEBUG),1) 29 | TRUE_FLAGS += -s 30 | CFLAGS = 31 | endif 32 | endif 33 | 34 | TRUE_FLAGS += $(CXXFLAGS) $(LFLAGS) 35 | 36 | gitbslr.so: main.cpp 37 | $(CXX) $+ $(TRUE_FLAGS) -o $@ -lm 38 | 39 | clean: 40 | rm gitbslr.so 41 | 42 | install: 43 | ./install.sh 44 | uninstall: 45 | ./install.sh uninstall 46 | 47 | test: gitbslr.so 48 | sh test1.sh | tee /dev/stderr | grep -q 'Test passed' 49 | sh test2.sh | tee /dev/stderr | grep -q 'Test passed' 50 | sh test3.sh | tee /dev/stderr | grep -q 'Test passed' 51 | sh test4.sh | tee /dev/stderr | grep -q 'Test passed' 52 | sh test5.sh | tee /dev/stderr | grep -q 'Test passed' 53 | sh test6.sh | tee /dev/stderr | grep -q 'Test passed' 54 | sh test7.sh | tee /dev/stderr | grep -q 'Test passed' 55 | rm -rf test/ 56 | echo All tests passed 57 | check: test 58 | 59 | .PHONY: clean install uninstall test check 60 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

Moved to https://git.disroot.org/Sir_Walrus/GitBSLR

2 | 3 | GitBSLR - make Git follow symlinks 4 | ======== 5 | 6 | Making Git follow symlinks is a fairly common request . 7 | 8 | But there's no real answer, only things that haven't worked since Git 1.6.1 (September 2010), hardlinks (requires root, and are increasingly rarely supported by filesystems), and other silly workarounds. 9 | 10 | So I made a LD_PRELOAD-based tool to fix that. With this tool installed, symlinks to outside the repo are treated as their contents. (To avoid duplicate files, symlinks to anywhere inside the repo are still symlinks.) 11 | 12 | Things that don't work, or aren't tested: 13 | - If someone checked in a symlink to outside the repo, GitBSLR will refuse to clone it. This is for security reasons; if vanilla Git creates a symlink to /home/username/, and GitBSLR follows it and creates a .bashrc, you would be quite disappointed. This also applies to repositories cloned prior to installing GitBSLR; if you think they may contain inappropriate links, check them before using GitBSLR, or delete and reclone. 14 | - Interaction with rarer Git features, like rebase or the cross-filesystem detector, is untested. If you think it should work, submit a PR or issue. Please include complete steps to reproduce, I don't know much about Git. 15 | - Anything complex (links to links, links within links, links to nonexistent files, etc) may yield unexpected results. (If sufficiently complex, it's not even clear what behavior would be expected.) 16 | - GitBSLR is only tested on Linux. Other Unix-likes may work, but are untested; feel free to try. For Windows, WSL or Cygwin will probably work (though symlinks are rare on Windows). 17 | - GitBSLR is only tested with glibc. Other libcs may work, but I've had a few bugs around glibc upgrades, so no promises. 18 | - --work-tree, --git-dir and similar don't work; GitBSLR can't see command line arguments, and will be confused. Use the GITBSLR_GIT_DIR and GITBSLR_WORK_TREE environment variables instead. 19 | - Performance is not a goal of GitBSLR; I haven't noticed any slowdown, but I also haven't used GitBSLR on any large repos where performance is relevant. If it's too slow for you, the best solution is to petition upstream Git to add this functionality. 20 | 21 | To enable GitBSLR on your machine: 22 | 1. Install your favorite Linux distro (or other Unix-like environment, if you're feeling lucky) 23 | 2. Install make and a C++ compiler; only tested with GNU make and g++, but others will probably work (if not, report the bug) 24 | 3. Compile GitBSLR with 'make', or 'make OPT=1' to enable my recommended optimizations, or 'make CFLAGS=-O3 LFLAGS=-s' if you want your own flags 25 | 4. Run GitBSLR's test suite, with 'make test'; GitBSLR makes many guesses about implementation details of Git and libc, and may yield subtle breakage or security holes if it guesses wrong 26 | 5. Add a wrapper script in your PATH that sets LD_PRELOAD=/path/to/gitbslr.so, then execs the real Git 27 | 28 | install.sh will do steps 3 to 5 for you, but not 1 or 2. 29 | 30 | Configuration: GitBSLR obeys a few environment variables, which can be set per-invocation, or permanently in the wrapper script: 31 | - GITBSLR_DEBUG 32 | If set, GitBSLR prints everything it does. If not, GitBSLR emits output only if it's unable to continue (for example Git trying to create symlinks to outside the repo, bad GitBSLR configuration, or a GitBSLR bug). 33 | - GITBSLR_FOLLOW 34 | A colon-separated list of paths, as seen by Git, optionally prefixed with the absolute path to the repo. 35 | 'path/link' or 'path/link/' will cause 'path/link' to be inlined. If path/link is nonexistent, not a symlink, or is outside the repo, the entry will be silently ignored. 36 | 'path/link/*' will cause path/link/, and every symlink accessible under that path, to be inlined. 37 | '*' alone is a valid value and will cause exactly everything to be inlined. This applies only to paths as seen by Git; . and .. components are invalid, and symlinks inside paths are not followed. 38 | If the path is prefixed with !, it causes the non-inlining of an otherwise listed symlink. 39 | The last match applies, so paths should be in order from least to most specific. 40 | If using this, you most likely want to .gitignore the symlink target, to avoid duplicate files. 41 | To avoid infinite loops, symlinks that (after inlining) point to one of their in-repo parent directories will remain as symlinks. Additionally, if there are symlinks to one of the repo's parent directories, the repo root will be treated as a symlink. 42 | - GITBSLR_GIT_DIR 43 | By default, GitBSLR assumes the Git directory is the first existing accessed path containing a .git component. If yours is elsewhere, you can override this default. 44 | Note that GitBSLR does not use the GIT_DIR variable. This is since there are three ways to set this path: GIT_DIR=, --git-dir=, and defaulting to the closest .git in the working directory. 45 | For architectural reasons, GitBSLR cannot access the command line arguments, and having two of three ways functional would be misleading. Better obviously dumb than giving people bad expectations. 46 | If this is set, GitBSLR will set GIT_DIR for you. However, --git-dir overrides GIT_DIR, so don't use that. 47 | WARNING: Setting this variable incorrectly, or not setting it if it should be set, is very likely to yield security holes or other trouble. 48 | - GITBSLR_WORK_TREE 49 | By default, GitBSLR assumes the work tree is the parent of the Git directory. If yours is elsewhere, you can override this default. 50 | Note that GitBSLR does not use the GIT_WORK_TREE variable. This is since there are four ways to set this path: GIT_DIR=, --git-dir=, .git/config, and defaulting to GIT_DIR's parent. Like GIT_DIR, some of those are unavailable to GitBSLR; better obviously dumb than almost smart enough. 51 | If this is set, GitBSLR will set GIT_WORK_TREE for you. However, --work-tree overrides GIT_WORK_TREE, so don't use that. 52 | WARNING: Setting this variable incorrectly, or not setting it if it should be set, is very likely to yield security holes or other trouble. 53 | 54 | GitBSLR will not automatically deduplicate anything, or otherwise create any symlinks for Git to follow. You have to create the symlinks yourself. 55 | -------------------------------------------------------------------------------- /configure: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | echo "Not needed, just use 'make'." 3 | -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # SPDX-License-Identifier: GPL-2.0-only 3 | # GitBSLR is available under the same license as Git itself. 4 | 5 | #dash doesn't support pipefail 6 | set -eu 7 | 8 | [ -e gitbslr.so ] || make OPT=1 || exit $? 9 | make test || $? 10 | 11 | TARGET="$HOME/bin" 12 | [ $(id -u) -eq 0 ] && TARGET="/usr/local/bin" 13 | 14 | CREATED_TARGET=0 15 | 16 | if [ ! -d $TARGET ]; then 17 | mkdir TARGET 18 | CREATED_TARGET=1 19 | fi 20 | if [ -e $TARGET/git ]; then 21 | if grep -q gitbslr.so $TARGET/git; then 22 | rm $TARGET/git 23 | rm $TARGET/gitbslr.so 24 | else 25 | echo "error: $TARGET/git exists and isn't GitBSLR, not going to overwrite that" 26 | exit 1 27 | fi 28 | fi 29 | 30 | if [ "x${1:-x}" = "xuninstall" ]; then 31 | echo "Uninstalled GitBSLR from $TARGET/git" 32 | exit 0 33 | fi 34 | 35 | GITORIG=$(which git) 36 | if [ x"$GITORIG" = x ]; then 37 | echo "error: you need to install Git before you can install GitBSLR" 38 | exit 1 39 | fi 40 | cp $(readlink -f $(dirname $0))/gitbslr.so $TARGET/gitbslr.so 41 | 42 | #TODO: make this append to LD_PRELOAD if one is already set 43 | #(also requires making the initialization unsetenv remove GitBSLR only) 44 | cat > $TARGET/git << EOF 45 | #!/bin/sh 46 | export LD_PRELOAD=$TARGET/gitbslr.so 47 | exec $GITORIG "\$@" 48 | EOF 49 | 50 | chmod +x $TARGET/git 51 | chmod -x $TARGET/gitbslr.so 52 | 53 | if [ "$(which git)" != $TARGET/git ]; then 54 | case ":$PATH:" in 55 | *:$TARGET:*) 56 | echo "warning: installed to $TARGET/git, but another Git is already in your \$PATH, in front of $TARGET/; fix that to complete the installation" 57 | ;; 58 | *:$TARGET/:*) 59 | # can't find a way to deduplicate these branches 60 | echo "warning: installed to $TARGET/git, but another Git is already in your \$PATH, in front of $TARGET/; fix that to complete the installation" 61 | ;; 62 | *) 63 | # don't source .profile directly, it's incompatible with set -u 64 | if [ $CREATED_TARGET = 1 ] && [ $(sh -c ". ~/.profile; which git") = $TARGET/git ]; then 65 | echo "Installed GitBSLR to $TARGET/git" 66 | echo "To complete installation, run" 67 | echo " export PATH=\"\$HOME/bin:\$PATH\"" 68 | echo "in all terminals, or restart your login session or computer" 69 | elif [ TARGET = "$HOME/bin" ]; then 70 | echo "Installed GitBSLR to $TARGET/git; run" 71 | echo " echo 'export PATH=\"\$HOME/bin:\$PATH\"' >> ~/.profile" 72 | echo "and restart your computer to complete the installation" 73 | else 74 | echo "warning: installed GitBSLR to $TARGET/git, but $TARGET/ is not in your PATH; fix that to complete the installation" 75 | fi 76 | ;; 77 | esac 78 | else 79 | echo "Installed GitBSLR to $TARGET/git" 80 | fi 81 | echo "To verify whether GitBSLR is correctly installed, run" 82 | echo " GITBSLR_DEBUG=1 git version" 83 | echo "and check if it says \"GitBSLR: Loaded\"." 84 | 85 | echo "You should also run GitBSLR's test suite, with" 86 | echo " make test" 87 | echo "to ensure GitBSLR works on your platform." 88 | -------------------------------------------------------------------------------- /main.cpp: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-only 2 | // GitBSLR is available under the same license as Git itself. If Git relicenses, you may choose 3 | // whether to use GitBSLR under GPLv2 or Git's new license. 4 | 5 | // Terminology: 6 | // Git - obvious 7 | // GitBSLR - this tool 8 | // Work tree - where your repo is checked out 9 | // Git directory - .git, usually in work tree 10 | // Real path - a path as seen by the kernel 11 | // Virtual path - a path as seen by Git (always relative to work tree) 12 | 13 | #include 14 | #include 15 | #include 16 | #include 17 | 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | 25 | #ifndef BUG_URL 26 | #define BUG_URL "https://github.com/Alcaro/GitBSLR/issues" 27 | #endif 28 | 29 | #if defined(__linux__) 30 | # define HAVE_STAT64 1 31 | #else 32 | # define HAVE_STAT64 0 33 | # warning "Untested platform, please report whether it works: https://github.com/Alcaro/GitBSLR/issues" 34 | #endif 35 | 36 | #ifdef _STAT_VER 37 | # define HAVE_STAT_VER 1 38 | #else 39 | # define HAVE_STAT_VER 0 40 | #endif 41 | 42 | // TODO: add a test for git clone 43 | // I don't want tests to touch the network, but clones from local directories fail because unexpected access to 44 | // not sure if that's fixable without creating a GITBSLR_THIRD_DIR env, and I don't know if I want to do that (needs a better name first) 45 | 46 | #undef DEBUG 47 | #define DEBUG(...) do { if (debug_level >= 1) fprintf(stderr, __VA_ARGS__); } while(0) 48 | #define DEBUG_VERBOSE(...) do { if (debug_level >= 2) fprintf(stderr, __VA_ARGS__); } while(0) 49 | #define FATAL(...) do { fprintf(stderr, __VA_ARGS__); exit(1); } while(0) 50 | static int debug_level = 0; 51 | 52 | 53 | class anyptr { 54 | void* data; 55 | public: 56 | template anyptr(T* data_) { data = (void*)data_; } 57 | template operator T*() { return (T*)data; } 58 | template operator const T*() const { return (const T*)data; } 59 | }; 60 | 61 | template static T min(const T& a, const T& b) { return a < b ? a : b; } 62 | 63 | #define DLLEXPORT extern "C" __attribute__((__visibility__("default"))) 64 | 65 | static void malloc_fail() 66 | { 67 | FATAL("GitBSLR: out of memory\n"); 68 | } 69 | 70 | static anyptr malloc_check(size_t size) 71 | { 72 | void* ret = malloc(size); 73 | if (size && !ret) malloc_fail(); 74 | return ret; 75 | } 76 | 77 | static anyptr realloc_check(anyptr ptr, size_t size) 78 | { 79 | void* ret = realloc(ptr, size); 80 | if (size && !ret) malloc_fail(); 81 | return ret; 82 | } 83 | 84 | #define malloc malloc_check 85 | #define realloc realloc_check 86 | 87 | #ifndef __GLIBC__ 88 | static const char * strchrnul(const char * s, int c) 89 | { 90 | const char * ret = strchr(s, c); 91 | return ret ? ret : s+strlen(s); 92 | } 93 | 94 | static void * memrchr(const void * s, int c, size_t n) 95 | { 96 | const uint8_t * s8 = (uint8_t*)s; 97 | while (--n) 98 | { 99 | if (s8[n] == c) 100 | return (void*)(s8+n); 101 | } 102 | return NULL; 103 | } 104 | 105 | static char* my_getcwd(char* buf, size_t size) 106 | { 107 | if (buf) return getcwd(buf, size); 108 | 109 | size_t buflen = 64; 110 | char* buf = malloc(buflen); 111 | 112 | while (!getcwd(buf, buflen)) 113 | { 114 | buflen *= 2; 115 | buf = realloc(buf, buflen); 116 | } 117 | 118 | buf[r] = '\0'; 119 | return buf; 120 | } 121 | #define getcwd my_getcwd 122 | #endif 123 | 124 | class string { 125 | char* ptr; 126 | size_t len; 127 | 128 | void set(const char * other, size_t len) 129 | { 130 | if (!len) 131 | { 132 | this->ptr = NULL; 133 | this->len = 0; 134 | return; 135 | } 136 | 137 | this->ptr = malloc(len+1); 138 | this->len = len; 139 | memcpy(ptr, other, len); 140 | ptr[len] = '\0'; 141 | } 142 | void set(const char * other) { set(other, strlen(other)); } 143 | 144 | public: 145 | string() { ptr = NULL; len = 0; } 146 | string(const string& other) { set(other.ptr, other.len); } 147 | string(const char * other) { set(other); } 148 | string(const char * other, size_t len) { set(other, len); } 149 | ~string() { free(ptr); } 150 | 151 | operator const char *() const { return ptr ? ptr : ""; } 152 | const char * c_str() const { return ptr ? ptr : ""; } 153 | size_t length() const { return len; } 154 | operator bool() const { return len; } 155 | bool operator!() const { return len==0; } 156 | 157 | string& operator=(const char * other) 158 | { 159 | free(ptr); 160 | set(other); 161 | return *this; 162 | } 163 | string& operator=(const string& other) 164 | { 165 | free(ptr); 166 | set(other.ptr, other.len); 167 | return *this; 168 | } 169 | 170 | string& operator+=(const string& other) 171 | { 172 | if (!other.len) return *this; 173 | ptr = realloc(ptr, len+other.len+1); 174 | strcpy(ptr+len, other.ptr); 175 | len += other.len; 176 | return *this; 177 | } 178 | 179 | string operator+(const string& other) const 180 | { 181 | string ret = *this; 182 | ret += other; 183 | return ret; 184 | } 185 | 186 | string operator+(const char * other) const 187 | { 188 | string ret = *this; 189 | ret += other; 190 | return ret; 191 | } 192 | 193 | bool operator==(const char * other) const 194 | { 195 | if (ptr) return !strcmp(ptr, other); 196 | else return (!other || !*other); 197 | } 198 | 199 | bool operator!=(const char * other) const 200 | { 201 | return !operator==(other); 202 | } 203 | 204 | static string create_usurp(char * str) 205 | { 206 | string ret; 207 | ret.ptr = str; 208 | ret.len = str ? strlen(str) : 0; 209 | return ret; 210 | } 211 | 212 | bool contains(const char * other) const 213 | { 214 | if (ptr) return strstr(ptr, other); 215 | else return (!other || !*other); 216 | } 217 | bool startswith(const char * other) const 218 | { 219 | if (ptr) return !memcmp(ptr, other, strlen(other)); 220 | else return (!other || !*other); 221 | } 222 | bool endswith(const char * other) const 223 | { 224 | if (ptr) return !memcmp(ptr+len-strlen(other), other, strlen(other)); 225 | else return (!other || !*other); 226 | } 227 | }; 228 | 229 | 230 | 231 | typedef int (*lstat_t)(const char * path, struct stat* buf); 232 | typedef ssize_t (*readlink_t)(const char * path, char * buf, size_t bufsiz); 233 | typedef struct dirent* (*readdir_t)(DIR* dirp); 234 | typedef int (*symlink_t)(const char * target, const char * linkpath); 235 | 236 | static lstat_t lstat_o; 237 | static readlink_t readlink_o; 238 | static readdir_t readdir_o; 239 | static symlink_t symlink_o; 240 | 241 | #if HAVE_STAT_VER 242 | typedef int (*__lxstat_t)(int ver, const char * path, struct stat* buf); 243 | static __lxstat_t __lxstat_o; 244 | #endif 245 | 246 | #if HAVE_STAT64 247 | typedef struct dirent64* (*readdir64_t)(DIR* dirp); 248 | static readdir64_t readdir64_o; 249 | typedef int (*lstat64_t)(const char * path, struct stat64* buf); 250 | static lstat64_t lstat64_o; 251 | #endif 252 | 253 | #if HAVE_STAT64 && HAVE_STAT_VER 254 | typedef int (*__lxstat64_t)(int ver, const char * path, struct stat64* buf); 255 | static __lxstat64_t __lxstat64_o; 256 | #endif 257 | 258 | static inline void ensure_type_correctness() 259 | { 260 | // If any of the above typedefs are incorrect, these will throw various compile errors. 261 | // If the functions don't exist at all, it will also throw errors, signaling that some of the HAVE_ checks are wrong. 262 | (void)(lstat_o == lstat); 263 | (void)(readlink_o == readlink); 264 | (void)(readdir_o == readdir); 265 | (void)(symlink_o == symlink); 266 | #if HAVE_STAT_VER 267 | (void)(__lxstat == __lxstat_o); 268 | #endif 269 | #if HAVE_STAT64 270 | (void)(readdir64 == readdir64_o); 271 | (void)(lstat64_o == lstat64); 272 | #endif 273 | #if HAVE_STAT64 && HAVE_STAT_VER 274 | (void)(__lxstat64 == __lxstat64_o); 275 | #endif 276 | } 277 | 278 | 279 | static string readlink_d(const string& path) 280 | { 281 | size_t buflen = 64; 282 | char* buf = malloc(buflen); 283 | 284 | again: ; 285 | ssize_t r = readlink_o(path.c_str(), buf, buflen); 286 | if (r <= 0) return ""; 287 | if ((size_t)r >= buflen-1) 288 | { 289 | buflen *= 2; 290 | buf = realloc(buf, buflen); 291 | goto again; 292 | } 293 | 294 | buf[r] = '\0'; 295 | return string::create_usurp(buf); 296 | } 297 | 298 | static string realpath_d(const string& path) 299 | { 300 | return string::create_usurp(realpath(path.c_str(), NULL)); 301 | } 302 | 303 | static string dirname_d(const string& path) 304 | { 305 | if (path.endswith("/")) 306 | { 307 | return dirname_d(string(path, path.length()-1)); 308 | } 309 | const char * start = path; 310 | const char * last = strrchr(start, '/'); 311 | return string(start, last-start+1); 312 | } 313 | 314 | 315 | 316 | enum path_class_t { 317 | cls_git_dir, // or in /usr/share/git-core/ 318 | cls_work_tree, // not necessarily actually in the work tree, could be hopping through a symlink to outside 319 | cls_unknown, // if fatal_unknown is true, this can't be returned; if it would be this, the program terminates instead 320 | }; 321 | class path_handler { 322 | public: 323 | // These two always end with slash, if configured. 324 | string work_tree; 325 | string git_dir; 326 | 327 | // These are filenames, not paths. 328 | string git_config_path_1; // ~/.gitconfig 329 | string git_config_path_2; // $XDG_CONFIG_HOME/git/config 330 | 331 | path_handler() 332 | { 333 | const char * HOME = getenv("HOME"); 334 | if (HOME) 335 | git_config_path_1 = normalize_path((string)HOME + "/.gitconfig"); 336 | const char * XDG_CONFIG_HOME = getenv("XDG_CONFIG_HOME"); 337 | if (XDG_CONFIG_HOME) 338 | git_config_path_2 = normalize_path((string)XDG_CONFIG_HOME + "/git/config"); 339 | } 340 | 341 | static string append_slash(string path) 342 | { 343 | if (path.endswith("/") || path == "") return path; 344 | else return path+"/"; 345 | } 346 | 347 | // Removes ./ and ../ components, and double slashes, from the path. Does not follow symlinks. 348 | static string normalize_path(const string& path) 349 | { 350 | // fast path for easy cases (can't just look for "/.", that'd hit the slow path for every /.git) 351 | if (!path.contains("/..") && !path.contains("/./") && !path.contains("//")) 352 | { 353 | if (path == "/.") // I don't think this is a possible input, but better handle it anyways, for completeness 354 | return "/"; 355 | return path; 356 | } 357 | 358 | char * ret = strdup(path); 359 | 360 | size_t off_in = 0; 361 | size_t off_out = 0; 362 | while (ret[off_in]) 363 | { 364 | if (ret[off_in] == '/' && ret[off_in+1] == '/') 365 | { 366 | off_in += 1; 367 | continue; 368 | } 369 | if (ret[off_in] == '/' && ret[off_in+1] == '.') 370 | { 371 | if (ret[off_in+2] == '/' || ret[off_in+2] == '\0') 372 | { 373 | off_in += 2; 374 | continue; 375 | } 376 | if (ret[off_in+2] == '.' && (ret[off_in+3] == '/' || ret[off_in+3] == '\0')) 377 | { 378 | off_in += 3; 379 | if (off_out) off_out--; 380 | while (off_out && ret[off_out] != '/') off_out--; 381 | continue; 382 | } 383 | } 384 | ret[off_out] = ret[off_in]; 385 | off_in++; 386 | off_out++; 387 | } 388 | if (off_out == 0) // this throws it out of bounds if input was an empty string, 389 | ret[off_out++] = '/'; // but empty string does not contain /.. so that can't happen 390 | ret[off_out] = '\0'; 391 | return string::create_usurp(ret); 392 | } 393 | 394 | // Input may or may not have slash. Output will not have a slash. 395 | static string parent_dir(const string& path) 396 | { 397 | const char * start = path.c_str(); 398 | const char * end = (char*)memrchr((void*)path.c_str(), '/', path.length()-1); 399 | return string(start, end-start); 400 | } 401 | 402 | // A directory is considered to be inside itself. Don't use . or .. components or double slashes. 403 | // Returns false if one path is relative and the other is absolute; this is probably not the desired answer. 404 | // A trailing slash will be ignored, on both sides. 405 | static bool is_inside(const string& parent, const string& child) 406 | { 407 | return append_slash(child).startswith(append_slash(parent)); 408 | } 409 | static bool is_same(const string& parent, const string& child) 410 | { 411 | return append_slash(child) == append_slash(parent); 412 | } 413 | 414 | bool initialized() const { return git_dir && work_tree; } 415 | 416 | // Paths may, but are not required to, end with a slash. However, they must be absolute. 417 | // Configuring the Git directory will also configure the work tree, if it's not set already. 418 | void set_git_dir(const string& dir) 419 | { 420 | git_dir = normalize_path(append_slash(dir)); 421 | 422 | if (!git_dir.endswith("/.git/")) 423 | FATAL("GitBSLR: The git directory path must end with .git, it can't be %s\n", git_dir.c_str()); 424 | 425 | if (!work_tree) 426 | { 427 | // if this repo is a submodule, .git will be accessed via a few extra ../ components 428 | // the work tree will be whatever is before the ..s 429 | // https://github.com/Alcaro/GitBSLR/issues/16 430 | string tmp = parent_dir(dir); 431 | while (tmp.endswith("/..")) 432 | tmp = string(tmp, tmp.length()-3); 433 | set_work_tree(normalize_path(tmp)); 434 | DEBUG("GitBSLR: Using work tree %s (autodetected)\n", work_tree.c_str()); 435 | } 436 | } 437 | 438 | void set_work_tree(const string& dir) 439 | { 440 | work_tree = normalize_path(append_slash(dir)); 441 | } 442 | 443 | // Call only on paths known to exist. If it contains a /.git/, the Git directory is configured. This may set the work tree. 444 | void try_init(const string& path) 445 | { 446 | if (git_dir) 447 | return; 448 | if (path.endswith("/.git")) 449 | { 450 | DEBUG("GitBSLR: Using git dir %s (autodetected)\n", path.c_str()); 451 | set_git_dir(path); 452 | return; 453 | } 454 | if (path.contains("/.git/")) 455 | { 456 | const char * gitdir_start = path.c_str(); 457 | const char * gitdir_end = strstr(gitdir_start, "/.git/") + strlen("/.git/"); 458 | DEBUG("GitBSLR: Using git dir %.*s (autodetected)\n", (int)(gitdir_end-gitdir_start), gitdir_start); 459 | set_git_dir(string(gitdir_start, gitdir_end-gitdir_start)); 460 | return; 461 | } 462 | } 463 | 464 | // This one does not consider GITBSLR_FOLLOW. 465 | // If the Git directory or work tree are not yet known, this function won't return that. 466 | path_class_t classify(const string& path, bool fatal_unknown) const 467 | { 468 | if (path[0] != '/') 469 | return classify(normalize_path(string::create_usurp(getcwd(NULL, 0)) + "/" + path), fatal_unknown); 470 | 471 | if (is_inside("/usr/share/git-core/", path)) 472 | return cls_git_dir; 473 | if (git_dir && is_inside(git_dir, path)) 474 | return cls_git_dir; 475 | if (work_tree && is_inside(work_tree, path)) 476 | return cls_work_tree; 477 | // git status in a submodule will lstat the work tree and git dir, and all parents 478 | // https://github.com/Alcaro/GitBSLR/issues/16 479 | if (git_dir && is_inside(path, git_dir)) 480 | return cls_git_dir; 481 | if (work_tree && is_inside(path, work_tree)) 482 | return cls_work_tree; 483 | if (git_config_path_1 && is_same(path, git_config_path_1)) 484 | return cls_git_dir; 485 | if (git_config_path_2 && is_same(path, git_config_path_2)) 486 | return cls_git_dir; 487 | if (fatal_unknown) 488 | { 489 | if (!git_dir || !work_tree) 490 | FATAL("GitBSLR: unexpected access to %s before locating Git directory and/or work tree. " 491 | "Either you're missing GITBSLR_GIT_DIR and/or GITBSLR_WORK_TREE, or you found a GitBSLR bug. " 492 | "If latter, please report it: " BUG_URL "\n", 493 | path.c_str()); 494 | else 495 | FATAL("GitBSLR: unexpected access to %s; should only be in %s or %s. " 496 | "Either you're missing GITBSLR_GIT_DIR and/or GITBSLR_WORK_TREE, or you found a GitBSLR bug. " 497 | "If latter, please report it: " BUG_URL "\n", 498 | path.c_str(), work_tree.c_str(), git_dir.c_str()); 499 | } 500 | return cls_unknown; 501 | } 502 | 503 | bool is_in_git_dir(const string& path) const { return classify(path, true) == cls_git_dir; } 504 | 505 | //Input: A path to a symlink, relative to the current directory, no trailing slash. 506 | //Output: Whether GITBSLR_FOLLOW says that path should be inlined. False = it's a link. 507 | bool link_force_inline(const string& path) const 508 | { 509 | const char * rules = getenv("GITBSLR_FOLLOW"); 510 | if (!rules || !*rules) return false; 511 | 512 | string cwd = string::create_usurp(getcwd(NULL, 0))+"/"; 513 | if (!is_inside(work_tree, cwd)) 514 | FATAL("GitBSLR: current directory %s should be in work tree %s\n", cwd.c_str(), work_tree.c_str()); 515 | 516 | //path is relative to cwd 517 | //path_rel is relative to work tree 518 | //path_abs is work tree plus path_rel 519 | 520 | string path_rel = append_slash(string(cwd.c_str()+work_tree.length(), cwd.length()-work_tree.length())) + path; 521 | string path_abs = work_tree + path_rel; 522 | 523 | bool ret = false; // no matching rule -> default to keeping it as a link 524 | 525 | while (true) 526 | { 527 | const char * next = strchrnul(rules, ':'); 528 | const char * end = next; 529 | 530 | bool ret_this = true; 531 | bool wildcard = false; 532 | if (*rules == '!') { rules++; ret_this = false; } 533 | 534 | if (*rules == ':') 535 | FATAL("GitBSLR: empty GITBSLR_FOLLOW entries are not allowed"); 536 | 537 | // this intentionally accepts * as an entry 538 | if (end > rules && end[-1] == '*') 539 | { 540 | end--; 541 | wildcard = true; 542 | 543 | if (end > rules && end[-1] != '/') 544 | FATAL("GitBSLR: GITBSLR_FOLLOW entries can't end with * unless they end with /*"); 545 | } 546 | 547 | if (end > rules && end[-1] == '/') end--; // ignore trailing slashes 548 | 549 | // keep running if the rule matches, so the last one wins 550 | if (memcmp(path_rel.c_str(), rules, end-rules)==0 && (wildcard || (size_t)(end-rules) == path_rel.length())) ret = ret_this; 551 | if (memcmp(path_abs.c_str(), rules, end-rules)==0 && (wildcard || (size_t)(end-rules) == path_abs.length())) ret = ret_this; 552 | 553 | if (!*next) return ret; 554 | rules = next+1; 555 | } 556 | } 557 | 558 | //Input: 559 | // Any virtual path. 560 | //Output: 561 | // If that path should refer to a symlink, return what it points to, relative to the presumed link's parent directory. 562 | // If it doesn't exist, or should be a normal file or directory (not a link), return a blank string. 563 | //The function may not call lstat or readlink, that'd yield infinite recursion. It may call readlink_o, which is the real readlink. 564 | string resolve_symlink(string path) const 565 | { 566 | //algorithm: 567 | //if the path is inside git directory: 568 | // tell the truth 569 | //for each prefix of the path: 570 | // if path is the same thing as prefix (realpath identical): 571 | // it's a link 572 | // if path is a link, points to inside prefix, and GITBSLR_FOLLOW doesn't say to inline it: 573 | // it's a link (but check realpath of all prefixes to determine where it leads) 574 | //otherwise, it's not a link 575 | 576 | string path_linktarget = readlink_d(path); 577 | 578 | string root_abs = realpath_d("."); 579 | if (!is_inside(work_tree, root_abs)) 580 | FATAL("GitBSLR: internal error, attempted symlink check with cwd (%s) outside worktree (%s). " 581 | "Please report this bug: " BUG_URL "\n", 582 | root_abs.c_str(), work_tree.c_str()); 583 | 584 | string path_abs = realpath_d(path); // if 'path' is a link, this refers to the link target 585 | if (!path_abs) return ""; // nonexistent -> not a symlink 586 | if (is_inside(git_dir, path_abs)) return path_linktarget; // git dir -> return truth 587 | if (is_inside("/usr/share/git-core/", path_abs)) return path_linktarget; // git likes reading some random stuff here, let it 588 | if (is_same(work_tree, path)) return ""; // work tree isn't a link 589 | if (is_inside(work_tree, path)) 590 | path = string(path.c_str() + strlen(work_tree)); // unreachable on ubuntu 21.10 and 22.04, but can show up on 16.04 591 | 592 | if (path[0] == '/') 593 | FATAL("GitBSLR: internal error, unexpected absolute path %s. Please report this bug: " BUG_URL "\n", path.c_str()); 594 | 595 | const char * start = path; 596 | const char * iter = start; 597 | 598 | bool target_is_in_repo = false; 599 | 600 | while (true) 601 | { 602 | const char * next = strchrnul(iter+1, '/'); 603 | 604 | string newpath = string(start, iter-start); 605 | if (newpath == "") newpath = "."; 606 | string newpath_abs = realpath_d(newpath); 607 | 608 | // if this path is the same as the link target, 609 | if (newpath_abs == path_abs) 610 | { 611 | // it's a link 612 | if (!*next) return "."; 613 | string ret; 614 | while (*next) 615 | { 616 | ret += "../"; 617 | next = strchrnul(next+1, '/'); 618 | } 619 | return string(ret, ret.length()-1); 620 | } 621 | 622 | //if it's originally a symlink, and points to inside the repo, 623 | //it's a candidate for inlining - but the above check overrides it, if necessary 624 | if (path_linktarget && path_abs.startswith(newpath_abs+"/")) 625 | target_is_in_repo = true; 626 | 627 | iter = next; 628 | 629 | if (iter[0] == '\0' || (iter[0] == '/' && iter[1] == '\0')) 630 | { 631 | if (!target_is_in_repo) return ""; // if it'd point outside the repo, it's not a link 632 | if (link_force_inline(path)) return ""; // if GITBSLR_FOLLOW says inline, it's not a link 633 | 634 | // if the link's target is absolute, or the realpath is not in the work dir but the target is, 635 | // ignore readlink and create a new path 636 | if (path_linktarget[0]=='/' || !newpath_abs.startswith(work_tree)) 637 | { 638 | // path is virtual path to link 639 | // path_abs is real path to link, including work tree 640 | string& source_virt = path; // rename this variable 641 | string target_virt = string(path_abs.c_str() + strlen(root_abs)+1); 642 | 643 | // if /a/b/c points to /a/d, emit ../d, not ../../a/d 644 | size_t start = 0; 645 | for (size_t i=0;source_virt[i] == target_virt[i];i++) 646 | { 647 | if (source_virt[i] == '/') start = i+1; 648 | } 649 | 650 | string up; 651 | const char * next = strchr(source_virt.c_str()+start, '/'); 652 | while (next) 653 | { 654 | up += "../"; 655 | next = strchr(next+1, '/'); 656 | } 657 | return up + (target_virt.c_str()+start); 658 | } 659 | else 660 | { 661 | return path_linktarget; 662 | } 663 | } 664 | } 665 | } 666 | }; 667 | 668 | 669 | #if HAVE_STAT_VER 670 | static int lstat_lxstat_wrap(const char * path, struct stat * buf) 671 | { 672 | return __lxstat_o(_STAT_VER, path, buf); 673 | } 674 | #endif 675 | #if HAVE_STAT64 && HAVE_STAT_VER 676 | static int lstat64_lxstat_wrap(const char * path, struct stat64 * buf) 677 | { 678 | return __lxstat64_o(_STAT_VER, path, buf); 679 | } 680 | #endif 681 | 682 | class gitbslr { 683 | public: 684 | path_handler gitpath; 685 | 686 | gitbslr() 687 | { 688 | // I'd prefer a function with __attribute__((constructor)), but that'd risk it running before path_handler's ctor, 689 | // which will screw up everything related to GITBSLR_WORK_TREE and GITBSLR_GIT_DIR 690 | if (getenv("GITBSLR_DEBUG")) 691 | { 692 | char * end; 693 | debug_level = strtol(getenv("GITBSLR_DEBUG"), &end, 0); 694 | if (*end) debug_level = 1; 695 | DEBUG("GitBSLR: Loaded\n"); 696 | } 697 | 698 | lstat_o = (lstat_t)dlsym(RTLD_NEXT, "lstat"); 699 | #if HAVE_STAT_VER 700 | if (!lstat_o) 701 | { 702 | __lxstat_o = (__lxstat_t)dlsym(RTLD_NEXT, "__lxstat"); 703 | if (__lxstat_o) lstat_o = lstat_lxstat_wrap; 704 | } 705 | #endif 706 | readlink_o = (readlink_t)dlsym(RTLD_NEXT, "readlink"); 707 | readdir_o = (readdir_t)dlsym(RTLD_NEXT, "readdir"); 708 | symlink_o = (symlink_t)dlsym(RTLD_NEXT, "symlink"); 709 | 710 | #if HAVE_STAT64 711 | readdir64_o = (readdir64_t)dlsym(RTLD_NEXT, "readdir64"); 712 | lstat64_o = (lstat64_t)dlsym(RTLD_NEXT, "lstat64"); 713 | #if HAVE_STAT_VER 714 | if (!lstat64_o) 715 | { 716 | __lxstat64_o = (__lxstat64_t)dlsym(RTLD_NEXT, "__lxstat64"); 717 | if (__lxstat64_o) lstat64_o = lstat64_lxstat_wrap; 718 | } 719 | #endif 720 | #endif 721 | 722 | if (!lstat_o || !readlink_o || !readdir_o || !symlink_o 723 | #if HAVE_STAT64 724 | || !readdir64_o || !lstat64_o 725 | #endif 726 | ) 727 | FATAL("GitBSLR: couldn't dlsym required symbols (this is a GitBSLR bug, please report it: " BUG_URL ")\n"); 728 | 729 | // GitBSLR shouldn't be loaded into the EDITOR 730 | unsetenv("LD_PRELOAD"); 731 | 732 | // if this env is set and the entire repo is behind a symlink, Git occasionally accesses it via the link instead 733 | // GitBSLR will see this as access to an unrelated path and ask for a bug report 734 | unsetenv("PWD"); 735 | 736 | const char * gitbslr_work_tree = getenv("GITBSLR_WORK_TREE"); 737 | if (gitbslr_work_tree) 738 | { 739 | gitpath.set_work_tree(gitbslr_work_tree); 740 | DEBUG("GitBSLR: Using work tree %s (from env)\n", gitbslr_work_tree); 741 | setenv("GIT_WORK_TREE", gitbslr_work_tree, true); 742 | } 743 | else if (getenv("GIT_WORK_TREE")) 744 | FATAL("GitBSLR: use GITBSLR_WORK_TREE, not GIT_WORK_TREE\n"); 745 | 746 | const char * gitbslr_git_dir = getenv("GITBSLR_GIT_DIR"); 747 | if (gitbslr_git_dir) 748 | { 749 | gitpath.set_git_dir(gitbslr_git_dir); 750 | DEBUG("GitBSLR: Using git dir %s (from env)\n", gitbslr_git_dir); 751 | setenv("GIT_DIR", gitbslr_git_dir, true); 752 | } 753 | else if (getenv("GIT_DIR")) 754 | FATAL("GitBSLR: use GITBSLR_GIT_DIR, not GIT_DIR\n"); 755 | 756 | } 757 | }; 758 | static gitbslr g_gitbslr; 759 | 760 | static path_handler& gitpath = g_gitbslr.gitpath; 761 | 762 | 763 | static int stat_3264(const char * path, struct stat* buf) 764 | { 765 | return stat(path, buf); 766 | } 767 | static int lstat_o_3264(const char * path, struct stat* buf) 768 | { 769 | return lstat_o(path, buf); 770 | } 771 | #if HAVE_STAT64 772 | static int stat_3264(const char * path, struct stat64* buf) 773 | { 774 | return stat64(path, buf); 775 | } 776 | static int lstat_o_3264(const char * path, struct stat64* buf) 777 | { 778 | return lstat64_o(path, buf); 779 | } 780 | #endif 781 | 782 | template 783 | int inner_lstat(const char * fn_name, const char * path, stat_t* buf) 784 | { 785 | DEBUG_VERBOSE("GitBSLR: %s(%s)\n", fn_name, path); 786 | if (!gitpath.initialized() || gitpath.is_in_git_dir(path)) 787 | { 788 | DEBUG("GitBSLR: %s(%s) - untouched because %s\n", fn_name, path, gitpath.initialized() ? "in .git" : ".git not yet located"); 789 | int ret = lstat_o_3264(path, buf); 790 | int errno_tmp = errno; 791 | if (ret >= 0) gitpath.try_init(path); 792 | errno = errno_tmp; 793 | return ret; 794 | } 795 | 796 | int ret = stat_3264(path, buf); 797 | if (ret < 0) 798 | { 799 | DEBUG("GitBSLR: %s(%s) - untouched because can't stat (%s)\n", fn_name, path, strerror(errno)); 800 | return lstat_o_3264(path, buf); 801 | } 802 | 803 | string newpath = gitpath.resolve_symlink(path); 804 | if (newpath) DEBUG("GitBSLR: %s(%s) -> %s\n", fn_name, path, newpath.c_str()); 805 | else DEBUG("GitBSLR: %s(%s) - not a link\n", fn_name, path); 806 | if (newpath) 807 | { 808 | buf->st_mode &= ~S_IFMT; 809 | buf->st_mode |= S_IFLNK; 810 | buf->st_size = newpath.length(); 811 | } 812 | // looking for the else clause to make it say 'no, it's not a link'? that's done by calling stat rather than lstat 813 | return ret; 814 | } 815 | 816 | DLLEXPORT int lstat(const char * path, struct stat* buf) 817 | { 818 | return inner_lstat("lstat", path, buf); 819 | } 820 | 821 | DLLEXPORT int __lxstat(int ver, const char * path, struct stat* buf); // -Wmissing-declarations - we want to override it even on libc mismatch 822 | DLLEXPORT int __lxstat(int ver, const char * path, struct stat* buf) 823 | { 824 | // according to , 825 | // ver should be 3, but _STAT_VER is 1 826 | // no clue what it's doing 827 | // and glibc 2.33 deletes _STAT_VER from the headers 828 | #if HAVE_STAT_VER 829 | if (ver != _STAT_VER) 830 | FATAL("GitBSLR: git called __lxstat(%s) with wrong version (got %d, expected %d)\n", path, ver, _STAT_VER); 831 | 832 | return inner_lstat("__lxstat", path, buf); 833 | #else 834 | FATAL("GitBSLR: git unexpectedly called __lxstat; are Git and GitBSLR compiled against different libc?\n"); 835 | #endif 836 | } 837 | 838 | #if HAVE_STAT64 839 | DLLEXPORT int lstat64(const char * path, struct stat64* buf) 840 | { 841 | return inner_lstat("lstat64", path, buf); 842 | } 843 | 844 | DLLEXPORT int __lxstat64(int ver, const char * path, struct stat64* buf); 845 | DLLEXPORT int __lxstat64(int ver, const char * path, struct stat64* buf) 846 | { 847 | #if HAVE_STAT_VER 848 | if (ver != _STAT_VER) 849 | FATAL("GitBSLR: git called __lxstat64(%s) with wrong version (got %d, expected %d)\n", path, ver, _STAT_VER); 850 | return inner_lstat("__lxstat64", path, buf); 851 | 852 | #else 853 | FATAL("GitBSLR: git unexpectedly called __lxstat64; are Git and GitBSLR compiled against different libc?\n"); 854 | #endif 855 | } 856 | #else 857 | DLLEXPORT int lstat64(const char * path, void* buf) 858 | { 859 | FATAL("GitBSLR: git unexpectedly called lstat64; are Git and GitBSLR compiled against different libc?\n"); 860 | } 861 | 862 | DLLEXPORT int __lxstat64(int ver, const char * path, void* buf) 863 | { 864 | FATAL("GitBSLR: git unexpectedly called __lxstat64; are Git and GitBSLR compiled against different libc?\n"); 865 | } 866 | #endif 867 | 868 | DLLEXPORT ssize_t readlink(const char * path, char * buf, size_t bufsiz) 869 | { 870 | DEBUG_VERBOSE("GitBSLR: readlink(%s)\n", path); 871 | if (!gitpath.initialized() || gitpath.is_in_git_dir(path)) 872 | { 873 | DEBUG("GitBSLR: readlink(%s) - untouched because %s\n", path, gitpath.initialized() ? "in .git" : ".git not yet located"); 874 | return readlink_o(path, buf, bufsiz); 875 | } 876 | 877 | string newpath = gitpath.resolve_symlink(path); 878 | DEBUG("GitBSLR: readlink(%s) -> %s\n", path, newpath ? newpath.c_str() : "(not link)"); 879 | if (!newpath) 880 | { 881 | errno = EINVAL; 882 | return -1; 883 | } 884 | 885 | ssize_t nbytes = min(bufsiz, newpath.length()); 886 | memcpy(buf, (const char*)newpath, nbytes); 887 | return nbytes; 888 | } 889 | 890 | DLLEXPORT int symlink(const char * target, const char * linkpath) 891 | { 892 | DEBUG_VERBOSE("GitBSLR: symlink(%s <- %s)\n", target, linkpath); 893 | 894 | if (strstr(linkpath, "/.git/")) 895 | { 896 | // git init (and clone) create a symlink at some random filename in .git to 'testing', to check if that works. let it 897 | return symlink_o(target, linkpath); 898 | } 899 | if ((string("/")+target+"/").contains("/.git/")) // make sure to reject all .git, not just current gitdir 900 | { 901 | fprintf(stderr, "GitBSLR: link at %s is not allowed to point to %s, since that's under .git/\n", linkpath, target); 902 | errno = EPERM; 903 | return -1; 904 | } 905 | if (!gitpath.work_tree) 906 | { 907 | fprintf(stderr, "GitBSLR: cannot create symlinks before finding the work tree (this is a GitBSLR bug, please report it: " 908 | BUG_URL ")"); 909 | errno = EPERM; 910 | return -1; 911 | } 912 | 913 | if (linkpath[0] == '/') 914 | { 915 | fprintf(stderr, "GitBSLR: link at %s is not allowed to point to absolute path %s\n", linkpath, target); 916 | errno = EPERM; 917 | return -1; 918 | } 919 | 920 | int n_leading_up = 0; 921 | while (memcmp(target + n_leading_up*3, "../", 3) == 0) 922 | n_leading_up++; 923 | if ((string(target + n_leading_up*3)+"/").contains("/../")) 924 | { 925 | fprintf(stderr, "GitBSLR: link at %s is not allowed to point to %s; ../ components must be at the start\n", linkpath, target); 926 | errno = EPERM; 927 | return -1; 928 | } 929 | 930 | // the work tree, and every symlink, is one-way; links may not point up past them 931 | string linkpath_abs = realpath_d(".")+"/"+linkpath; 932 | for (int i=0;i<=n_leading_up;i++) 933 | { 934 | linkpath_abs = dirname_d(linkpath_abs); 935 | 936 | struct stat buf; 937 | if (lstat_o(linkpath_abs, &buf) < 0) 938 | { 939 | int errno_tmp = errno; 940 | fprintf(stderr, "GitBSLR: link at %s is not allowed to point to %s, since %s is inaccessible (%s)\n", 941 | linkpath, target, linkpath_abs.c_str(), strerror(errno_tmp)); 942 | errno = errno_tmp; 943 | return -1; 944 | } 945 | if (S_ISLNK(buf.st_mode)) 946 | { 947 | fprintf(stderr, "GitBSLR: link at %s is not allowed to point to %s, since %s is a symlink\n", 948 | linkpath, target, linkpath_abs.c_str()); 949 | errno = EPERM; 950 | return -1; 951 | } 952 | } 953 | 954 | if (!(linkpath_abs+"/").startswith(gitpath.work_tree)) 955 | { 956 | fprintf(stderr, "GitBSLR: link at %s is not allowed to point to %s, since %s is not under %s\n", 957 | linkpath, target, linkpath_abs.c_str(), gitpath.work_tree.c_str()); 958 | errno = EPERM; 959 | return -1; 960 | } 961 | 962 | DEBUG("GitBSLR: symlink(%s <- %s) - creating\n", target, linkpath); 963 | return symlink_o(target, linkpath); 964 | } 965 | 966 | // I could hijack opendir and keep track of what path this DIR* is for, or I could tell Git that we don't know the filetype. 967 | // The latter causes Git to fall back to some appropriate stat() variant, where I have the path easily available. 968 | DLLEXPORT struct dirent* readdir(DIR* dirp) 969 | { 970 | dirent* r = readdir_o(dirp); 971 | if (r) r->d_type = DT_UNKNOWN; 972 | return r; 973 | } 974 | #if HAVE_STAT64 975 | DLLEXPORT struct dirent64* readdir64(DIR* dirp) 976 | { 977 | dirent64* r = readdir64_o(dirp); 978 | if (r) r->d_type = DT_UNKNOWN; 979 | return r; 980 | } 981 | #else 982 | DLLEXPORT void* readdir64(DIR* dirp) 983 | { 984 | FATAL("GitBSLR: git unexpectedly called readdir64; are Git and GitBSLR compiled against different libc?\n"); 985 | } 986 | #endif 987 | -------------------------------------------------------------------------------- /test-dummyeditor.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env perl 2 | # SPDX-License-Identifier: GPL-2.0-only 3 | # GitBSLR is available under the same license as Git itself. 4 | 5 | #this thing's sole purpose is to fail if GitBSLR is loaded into the process 6 | 7 | chdir "."; 8 | readlink("/dev/null"); 9 | open(my $fh, '>', $ARGV[0]); 10 | print $fh "GitBSLR test\n"; 11 | -------------------------------------------------------------------------------- /test1.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # SPDX-License-Identifier: GPL-2.0-only 3 | # GitBSLR is available under the same license as Git itself. 4 | 5 | cd $(dirname $0) 6 | . ./testlib.sh 7 | 8 | #This script tests Git's basics: init, add, commit, checkout (latter via reset). 9 | #To run the tests, `make test` 10 | 11 | #input: 12 | mkdir test/input/ 13 | mkdir test/input/wrap/ 14 | mkdir test/input/wrap/the_repo/ 15 | mkdir test/input/wrap/the_repo/subdir1/ 16 | echo file1 > test/input/wrap/the_repo/subdir1/file1 17 | mkdir test/input/wrap/the_repo/subdir2/ 18 | echo file2 > test/input/wrap/the_repo/subdir2/file2 19 | echo file3 > test/input/wrap/the_repo/file3 20 | ln_sr test/input/wrap/ test/input/wrap/the_repo/to_outside 21 | ln_sr test/input/wrap/ test/input/wrap/the_repo/to_outside2 22 | ln -s /bin/sh test/input/wrap/the_repo/to_bin_sh 23 | ln_sr test/input/wrap/the_repo/ test/input/wrap/the_repo/to_root 24 | ln_sr test/input/wrap/the_repo/file3 test/input/wrap/the_repo/to_file3 25 | ln_sr test/input/wrap/the_repo/subdir1/ test/input/wrap/the_repo/to_subdir1 26 | ln_sr test/input/wrap/ test/input/wrap/to_outside_again 27 | ln_sr test/input/ test/input/wrap/further_outside 28 | mkdir test/input/wrap/subdir3/ 29 | echo file5 > test/input/wrap/subdir3/file5 30 | echo file4 > test/input/wrap/file4 31 | 32 | 33 | #under the careful delusions of GitBSLR, Git will see this as: 34 | mkdir test/expected/ 35 | mkdir test/expected/subdir1/ 36 | echo file1 > test/expected/subdir1/file1 37 | mkdir test/expected/subdir2/ 38 | echo file2 > test/expected/subdir2/file2 39 | echo file3 > test/expected/file3 40 | ln_sr test/expected/ test/expected/to_root 41 | mkdir test/expected/to_outside/ 42 | ln_sr test/expected/ test/expected/to_outside/the_repo 43 | ln_sr test/expected/to_outside test/expected/to_outside/to_outside_again 44 | mkdir test/expected/to_outside/further_outside 45 | ln_sr test/expected/to_outside test/expected/to_outside/further_outside/wrap 46 | echo file4 > test/expected/to_outside/file4 47 | mkdir test/expected/to_outside/subdir3/ 48 | echo file5 > test/expected/to_outside/subdir3/file5 49 | mkdir test/expected/to_outside2/ 50 | #yes, duplicate; neither to_outside nor to_outside2 is subordinate to the other, 51 | # so GitBSLR doesn't know which to make a link, and instead inlines both 52 | ln_sr test/expected/ test/expected/to_outside2/the_repo 53 | ln_sr test/expected/to_outside2 test/expected/to_outside2/to_outside_again 54 | mkdir test/expected/to_outside2/further_outside 55 | ln_sr test/expected/to_outside2 test/expected/to_outside2/further_outside/wrap 56 | echo file4 > test/expected/to_outside2/file4 57 | mkdir test/expected/to_outside2/subdir3/ 58 | echo file5 > test/expected/to_outside2/subdir3/file5 59 | cp /bin/sh test/expected/to_bin_sh 60 | ln_sr test/expected/file3 test/expected/to_file3 61 | ln_sr test/expected/subdir1 test/expected/to_subdir1 62 | 63 | cd test/input/wrap/the_repo/ 64 | gitbslr init 65 | grep -q 'symlinks = false' .git/config && echo Error: No symlink support 66 | grep -q 'symlinks = false' .git/config && exit 1 67 | gitbslr add . 68 | #this could simply be 69 | #gitbslr commit -m "GitBSLR test" 70 | #but I want this to ensure https://github.com/Alcaro/GitBSLR/issues/1 doesn't regress 71 | export EDITOR=../../../../test-dummyeditor.pl 72 | gitbslr commit 73 | cd ../../../../ 74 | 75 | mkdir test/output/ 76 | mv test/input/wrap/the_repo/.git test/output/.git 77 | cd test/output/ 78 | #not gitbslr here, we want to extract what Git actually saw 79 | git reset --hard HEAD 80 | cd ../../ 81 | 82 | tree test/output/ > test/output.log 83 | tree test/expected/ > test/expected.log 84 | diff -U999 test/output.log test/expected.log 85 | 86 | echo Test passed 87 | -------------------------------------------------------------------------------- /test2.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # SPDX-License-Identifier: GPL-2.0-only 3 | # GitBSLR is available under the same license as Git itself. 4 | 5 | cd $(dirname $0) 6 | . ./testlib.sh 7 | 8 | #This script ensures GitBSLR can't create symlinks to outside the repository. 9 | 10 | #With GitBSLR installed, Git can end up writing to outside the repository directory. If a pulled 11 | # repository is malicious, this can cause remote code execution, for example by scribbling across your .bashrc. 12 | #GitBSLR must prevent that. Since the entire point of GitBSLR is writing outside the repo, something 13 | # else must be changed. The only available option is preventing Git from creating symlinks to outside the repo root. 14 | #The simplest and most effective way would be returning an error. The most appropriate one would be EPERM, 15 | # "The filesystem containing linkpath does not support the creation of symbolic links." 16 | #(Git claims to support filesystems not supporting symlinks, but that just replaces the links with 17 | # plaintext files containing their target, aka silently corrupting the tree. Better tell Git we 18 | # support links, causing it to show the unexpected error to the user.) 19 | 20 | 21 | mkdir test/victim/ 22 | echo echo Test passed > test/victim/script.sh 23 | mkdir test/evilrepo_v1/ 24 | 25 | cd test/evilrepo_v1/ 26 | git init 27 | ln -s ../victim/ evil_symlink 28 | git add . 29 | git commit -m "GitBSLR test part 1" 30 | cd ../.. 31 | 32 | mkdir test/evilrepo_v2/ 33 | cd test/evilrepo_v2/ 34 | git init 35 | mkdir evil_symlink/ 36 | echo echo Installing Bitcoin miner... > evil_symlink/script.sh 37 | git add . 38 | git commit -m "GitBSLR test part 2" 39 | cd ../.. 40 | 41 | mkdir test/clone/ 42 | cd test/clone/ 43 | mv ../evilrepo_v1/.git ./.git 44 | gitbslr reset --hard || true # supposed to fail, shouldn't hit -e 45 | mv .git ../evilrepo_v1/.git 46 | mv ../evilrepo_v2/.git ./.git 47 | gitbslr reset --hard 48 | mv .git ../evilrepo_v2/.git 49 | 50 | cd ../../ 51 | sh test/victim/script.sh 52 | -------------------------------------------------------------------------------- /test3.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # SPDX-License-Identifier: GPL-2.0-only 3 | # GitBSLR is available under the same license as Git itself. 4 | 5 | cd $(dirname $0) 6 | . ./testlib.sh 7 | 8 | #This script tests inlining of symlinks inside the repo, via GITBSLR_FOLLOW. 9 | 10 | 11 | #input: 12 | mkdir test/input/ 13 | mkdir test/input/sub1/ 14 | ln_sr test/input/sub1/ test/input/sub1/to_sub1 15 | ln_sr test/input/ test/input/sub1/to_root 16 | ln_sr test/input/sub2/ test/input/sub1/to_sub2 17 | ln_sr test/input/sub2/ test/input/sub1/to_sub2_again 18 | echo file1 > test/input/sub1/file1 19 | 20 | mkdir test/input/sub2/ 21 | ln_sr test/input/sub1/ test/input/sub2/to_sub1 22 | ln_sr test/input/sub1/ test/input/sub2/to_sub1_again 23 | ln_sr test/input/sub2/ test/input/sub2/to_sub2 24 | echo file2 > test/input/sub2/file2 25 | 26 | mkdir test/input/sub3/ 27 | ln_sr test/input/sub1/ test/input/sub3/to_sub1 28 | echo file3 > test/input/sub3/file3 29 | 30 | #ensure a /* inlines the indicated link too 31 | mkdir test/input/sub4/ 32 | mkdir test/input/sub5/ 33 | echo file4 > test/input/sub5/file4 34 | ln_sr test/input/sub5/ test/input/sub4/to_sub5 35 | ln_sr test/input/sub4/ test/input/to_sub4 36 | 37 | export GITBSLR_FOLLOW="sub1/*:!sub1/to_sub2:$(pwd)/test/input/sub2/to_sub1/:to_sub4/*" 38 | 39 | 40 | #expected output: 41 | mkdir test/expected/ 42 | mkdir test/expected/sub1/ 43 | echo file1 > test/expected/sub1/file1 44 | ln_sr test/expected/sub1/ test/expected/sub1/to_sub1 45 | ln_sr test/expected/ test/expected/sub1/to_root 46 | ln_sr test/expected/sub2/ test/expected/sub1/to_sub2 47 | mkdir test/expected/sub1/to_sub2_again/ 48 | echo file2 > test/expected/sub1/to_sub2_again/file2 49 | ln_sr test/expected/sub1/ test/expected/sub1/to_sub2_again/to_sub1 50 | ln_sr test/expected/sub1/ test/expected/sub1/to_sub2_again/to_sub1_again 51 | ln_sr test/expected/sub1/to_sub2_again/ test/expected/sub1/to_sub2_again/to_sub2 52 | 53 | mkdir test/expected/sub2/ 54 | echo file2 > test/expected/sub2/file2 55 | mkdir test/expected/sub2/to_sub1/ 56 | echo file1 > test/expected/sub2/to_sub1/file1 57 | ln_sr test/expected/sub2/to_sub1/ test/expected/sub2/to_sub1/to_sub1 58 | ln_sr test/expected/sub2/ test/expected/sub2/to_sub1/to_sub2 59 | ln_sr test/expected/sub2/ test/expected/sub2/to_sub1/to_sub2_again 60 | ln_sr test/expected/ test/expected/sub2/to_sub1/to_root 61 | ln_sr test/expected/sub1/ test/expected/sub2/to_sub1_again 62 | ln_sr test/expected/sub2/ test/expected/sub2/to_sub2 63 | 64 | mkdir test/expected/sub3/ 65 | ln_sr test/expected/sub1/ test/expected/sub3/to_sub1 66 | echo file3 > test/expected/sub3/file3 67 | 68 | mkdir test/expected/sub4/ 69 | mkdir test/expected/sub5/ 70 | echo file4 > test/expected/sub5/file4 71 | ln_sr test/expected/sub5/ test/expected/sub4/to_sub5 72 | mkdir test/expected/to_sub4/ 73 | mkdir test/expected/to_sub4/to_sub5 74 | echo file4 > test/expected/to_sub4/to_sub5/file4 75 | 76 | 77 | cd test/input/ 78 | git init 79 | gitbslr add . 80 | git commit -m 'GitBSLR test' 81 | cd ../../ 82 | 83 | mkdir test/output/ 84 | mv test/input/.git test/output/.git 85 | cd test/output/ 86 | git reset --hard HEAD 87 | cd ../../ 88 | 89 | tree test/output/ > test/output.log 90 | tree test/expected/ > test/expected.log 91 | diff -U999 test/output.log test/expected.log 92 | 93 | echo Test passed 94 | -------------------------------------------------------------------------------- /test4.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # SPDX-License-Identifier: GPL-2.0-only 3 | # GitBSLR is available under the same license as Git itself. 4 | 5 | cd $(dirname $0) 6 | . ./testlib.sh 7 | 8 | #This script tests some rarer stuff, like ls-files, symlinks to symlinks, and symlinks to absolute paths in the repo. 9 | 10 | # ls-files 11 | mkdir test/repo/ 12 | mkdir test/repo/dir/ 13 | echo test > test/repo/dir/file 14 | ln_sr test/repo/dir/ test/repo/ln_dir 15 | 16 | echo dir/file >> test/expected.log 17 | echo ln_dir >> test/expected.log 18 | 19 | cd test/repo/ 20 | gitbslr init 21 | gitbslr ls-files --others --exclude-standard > ../output.log 22 | cd ../../ 23 | 24 | diff -U999 test/output.log test/expected.log || exit $? 25 | rm -rf test/* 26 | 27 | 28 | # Symlinks to symlinks 29 | mkdir test/repo/ 30 | mkdir test/expected/ 31 | 32 | # repo/a -> repo/b -> repo/c - should let a point to b 33 | echo test > test/repo/file 34 | echo test > test/expected/file 35 | ln_sr test/repo/file test/repo/to_file 36 | ln_sr test/expected/file test/expected/to_file 37 | ln_sr test/repo/to_file test/repo/to_to_file 38 | ln_sr test/expected/to_file test/expected/to_to_file 39 | 40 | # repo/a -> not_repo/b -> repo/c - should inline a once, leaving it as link to c 41 | ln_sr test/repo/file test/file_detour 42 | ln_sr test/file_detour test/repo/to_to_file_detour 43 | ln_sr test/expected/file test/expected/to_to_file_detour 44 | 45 | # repo/a -> not_repo/b; not_repo/b/c -> repo/d - should inline a, and leave c as link, while ensuring it still points where it should 46 | mkdir test/dir 47 | ln_sr test/dir test/repo/to_dir 48 | mkdir test/expected/to_dir 49 | ln_sr test/repo/file test/dir/to_file 50 | ln_sr test/expected/file test/expected/to_dir/to_file 51 | 52 | # the above, but with an extra subdirectory 53 | # repo/a -> not_repo/b; not_repo/b/c -> repo/d - should inline a, and leave c as link, while ensuring it still points where it should 54 | mkdir test/repo/sub 55 | mkdir test/expected/sub 56 | echo test > test/repo/sub/file 57 | echo test > test/expected/sub/file 58 | mkdir test/subdir 59 | ln_sr test/subdir test/repo/sub/to_subdir 60 | mkdir test/expected/sub/to_subdir 61 | ln_sr test/repo/sub/file test/subdir/to_file 62 | ln_sr test/expected/sub/file test/expected/sub/to_subdir/to_file 63 | 64 | # absolute symlinks into repo 65 | mkdir test/repo/sub2/ 66 | mkdir test/expected/sub2/ 67 | echo test > test/repo/sub2/file2 68 | echo test > test/expected/sub2/file2 69 | ln -s $(realpath test/repo/sub2/) test/repo/to_sub2 70 | ln_sr test/expected/sub2/ test/expected/to_sub2 71 | ln -s $(realpath test/repo/sub2/) test/repo/sub/to_sub2 72 | ln_sr test/expected/sub2/ test/expected/sub/to_sub2 73 | ln -s $(realpath test/repo/sub/file) test/repo/sub/to_subfile 74 | ln_sr test/expected/sub/file test/expected/sub/to_subfile 75 | 76 | #TODO: figure out what to do with links to nonexistent in-repo, or out-of-repo, targets 77 | 78 | 79 | 80 | 81 | cd test/repo/ 82 | gitbslr init 83 | gitbslr add . 84 | gitbslr commit -m "GitBSLR test" 85 | cd ../../ 86 | 87 | mkdir test/output/ 88 | mv test/repo/.git test/output/.git 89 | cd test/output/ 90 | #not gitbslr here, we want to extract what Git actually saw 91 | git reset --hard HEAD 92 | cd ../../ 93 | 94 | 95 | tree test/output/ > test/output.log 96 | tree test/expected/ > test/expected.log 97 | diff -U999 test/output.log test/expected.log 98 | 99 | echo Test passed 100 | -------------------------------------------------------------------------------- /test5.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # SPDX-License-Identifier: GPL-2.0-only 3 | # GitBSLR is available under the same license as Git itself. 4 | 5 | cd $(dirname $0) 6 | . ./testlib.sh 7 | 8 | #This script tests symlinks to not-yet-existing targets. 9 | 10 | mkdir test/repo/ 11 | mkdir test/repo/b/ 12 | mkdir test/repo/c/ 13 | mkdir test/repo/d/ 14 | echo test > test/inaccessible 15 | ln_sr test/repo/c/b test/repo/a 16 | ln_sr test/repo/c/b test/repo/b/a 17 | ln_sr test/repo/c/b test/repo/b/b 18 | ln_sr test/repo/c/b test/repo/b/c 19 | ln_sr test/repo/c/b test/repo/c/a 20 | echo test > test/repo/c/b 21 | ln_sr test/repo/c/b test/repo/c/c 22 | ln_sr test/repo/c/b test/repo/d/a 23 | ln_sr test/repo/c/b test/repo/d/b 24 | ln_sr test/repo/c/b test/repo/d/c 25 | ln_sr test/repo/c/b test/repo/e 26 | ln -s ../output/c/b test/repo/fFAIL 27 | 28 | ln_sr test/inaccessible test/repo/gFAIL 29 | ln -s ../output/c test/repo/hFAIL 30 | ln -s ../c/../c/b test/repo/c/dFAIL 31 | 32 | cd test/repo/ 33 | # gitbslr would rewrite the symlinks above, so use original git 34 | git init 35 | git add . 36 | git commit -m test 37 | cd ../../ 38 | 39 | mkdir test/output/ 40 | mv test/repo/.git/ test/output/.git/ 41 | cd test/output/ 42 | gitbslr checkout -f HEAD || true # parts of this will fail 43 | cd ../../ 44 | mv test/output/.git/ test/.git/ 45 | 46 | tree test/output/ > test/output.log 47 | tree test/repo/ | grep -v FAIL > test/expected.log 48 | diff -U999 test/output.log test/expected.log 49 | 50 | echo Test passed 51 | -------------------------------------------------------------------------------- /test6.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # SPDX-License-Identifier: GPL-2.0-only 3 | # GitBSLR is available under the same license as Git itself. 4 | 5 | cd $(dirname $0) 6 | . ./testlib.sh 7 | 8 | #This script tests what happens if cwd is a symlink to the repo. 9 | 10 | mkdir test/repo 11 | echo test > test/repo/file 12 | ln_sr test/repo test/repolink 13 | 14 | cd test/repolink 15 | gitbslr init 16 | gitbslr add file 17 | gitbslr commit -m test 18 | rm file 19 | gitbslr checkout -f HEAD 20 | rm file 21 | gitbslr reset --hard 22 | rm file 23 | 24 | echo Test passed 25 | -------------------------------------------------------------------------------- /test7.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # SPDX-License-Identifier: GPL-2.0-only 3 | # GitBSLR is available under the same license as Git itself. 4 | 5 | cd $(dirname $0) 6 | . ./testlib.sh 7 | 8 | #This script tests what happens if cwd is a subdirectory of the repo. 9 | 10 | mkdir test/repo 11 | mkdir test/repo/dir 12 | echo test > test/repo/dir/file 13 | mkdir test/repo/dir2 14 | echo test2 > test/repo/dir2/file2 15 | mkdir test/not_repo 16 | echo test3 > test/not_repo/file3 17 | ln_sr test/not_repo test/repo/dir2/another_dir 18 | ln_sr test/repo/dir2/file2 test/not_repo/another_link 19 | 20 | mkdir test/expected 21 | cd test/repo 22 | gitbslr init 23 | gitbslr add . 24 | gitbslr commit -m test 25 | cd ../.. 26 | mv test/repo/.git test/expected/.git 27 | cd test/expected 28 | gitbslr reset --hard 29 | cd ../.. 30 | 31 | cd test/repo 32 | gitbslr init 33 | cd dir 34 | gitbslr add .. 35 | gitbslr commit -m test 36 | cd ../../.. 37 | mkdir test/actual1 38 | mv test/repo/.git test/actual1/.git 39 | cd test/actual1 40 | gitbslr reset --hard 41 | cd ../.. 42 | 43 | mkdir test/actual2 44 | mkdir test/actual2/dir2 45 | mv test/actual1/.git test/actual2/.git 46 | cd test/actual2/dir2 47 | gitbslr reset --hard 48 | cd ../../.. 49 | 50 | tree test/expected/ > test/expected.log 51 | tree test/actual1/ > test/actual1.log 52 | tree test/actual2/ > test/actual2.log 53 | diff -U999 test/actual1.log test/expected.log 54 | diff -U999 test/actual2.log test/expected.log 55 | 56 | echo Test passed 57 | -------------------------------------------------------------------------------- /testlib.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # SPDX-License-Identifier: GPL-2.0-only 3 | # GitBSLR is available under the same license as Git itself. 4 | 5 | #This script contains various helper functions needed by all of GitBSLR's tests. 6 | 7 | #dash doesn't support pipefail 8 | set -eu 9 | 10 | make 11 | rm -rf test/ 12 | [ -e test/ ] && exit 1 13 | mkdir test/ 14 | echo "Signature: 8a477f597d28d172789f06886806bc55" > test/CACHEDIR.TAG 15 | 16 | GIT=/usr/bin/git 17 | git() 18 | { 19 | >&2 echo git "$@" 20 | $GIT "$@" 21 | } 22 | GITBSLR=$(pwd)/gitbslr.so 23 | gitbslr() 24 | { 25 | >&2 echo gitbslr "$@" 26 | LD_PRELOAD=$GITBSLR $GIT "$@" 27 | } 28 | export GITBSLR_DEBUG=1 29 | 30 | ln_sr() 31 | { 32 | #Perl is no beauty, but anything else I could find requires bash, or other programs not guaranteed to exist 33 | ln -sr $1 $2 || perl -e'use File::Spec; use File::Basename; 34 | symlink File::Spec->abs2rel($ARGV[0], dirname($ARGV[1])), $ARGV[1] or 35 | die qq{cannot create symlink: $!$/}' $1 $2 36 | } 37 | 38 | tree() 39 | { 40 | find $1 -printf '%P -> %l\n' | grep -v .git | LC_ALL=C sort || 41 | perl -e ' 42 | chdir $ARGV[0]; 43 | use File::Find qw(finddepth); 44 | finddepth(sub { 45 | print $File::Find::name, " -> ", (readlink $_ or ""), "\n"; 46 | }, "."); 47 | ' $1 | grep -v .git | LC_ALL=C sort 48 | } 49 | --------------------------------------------------------------------------------