|
2 | 2 |
|
3 | 3 | #include "../../git-compat-util.h"
|
4 | 4 | #include "../../environment.h"
|
| 5 | +#include "../../wrapper.h" |
| 6 | +#include "../../strbuf.h" |
| 7 | +#include "../../versioncmp.h" |
5 | 8 |
|
6 | 9 | int win32_has_dos_drive_prefix(const char *path)
|
7 | 10 | {
|
@@ -89,3 +92,199 @@ int win32_fspathcmp(const char *a, const char *b)
|
89 | 92 | {
|
90 | 93 | return win32_fspathncmp(a, b, (size_t)-1);
|
91 | 94 | }
|
| 95 | + |
| 96 | +static int read_at(int fd, char *buffer, size_t offset, size_t size) |
| 97 | +{ |
| 98 | + if (lseek(fd, offset, SEEK_SET) < 0) { |
| 99 | + fprintf(stderr, "could not seek to 0x%x\n", (unsigned int)offset); |
| 100 | + return -1; |
| 101 | + } |
| 102 | + |
| 103 | + return read_in_full(fd, buffer, size); |
| 104 | +} |
| 105 | + |
| 106 | +static size_t le16(const char *buffer) |
| 107 | +{ |
| 108 | + unsigned char *u = (unsigned char *)buffer; |
| 109 | + return u[0] | (u[1] << 8); |
| 110 | +} |
| 111 | + |
| 112 | +static size_t le32(const char *buffer) |
| 113 | +{ |
| 114 | + return le16(buffer) | (le16(buffer + 2) << 16); |
| 115 | +} |
| 116 | + |
| 117 | +/* |
| 118 | + * Determine the Go version of a given executable, if it was built with Go. |
| 119 | + * |
| 120 | + * This recapitulates the logic from |
| 121 | + * https://github.com/golang/go/blob/master/src/cmd/go/internal/version/version.go |
| 122 | + * (without requiring the user to install `go.exe` to find out). |
| 123 | + */ |
| 124 | +static ssize_t get_go_version(const char *path, char *go_version, size_t go_version_size) |
| 125 | +{ |
| 126 | + int fd = open(path, O_RDONLY); |
| 127 | + char buffer[1024]; |
| 128 | + off_t offset; |
| 129 | + size_t num_sections, opt_header_size, i; |
| 130 | + char *p = NULL, *q; |
| 131 | + ssize_t res = -1; |
| 132 | + |
| 133 | + if (fd < 0) |
| 134 | + return -1; |
| 135 | + |
| 136 | + if (read_in_full(fd, buffer, 2) < 0) |
| 137 | + goto fail; |
| 138 | + |
| 139 | + /* |
| 140 | + * Parse the PE file format, for more details, see |
| 141 | + * https://en.wikipedia.org/wiki/Portable_Executable#Layout and |
| 142 | + * https://learn.microsoft.com/en-us/windows/win32/debug/pe-format |
| 143 | + */ |
| 144 | + if (buffer[0] != 'M' || buffer[1] != 'Z') |
| 145 | + goto fail; |
| 146 | + |
| 147 | + if (read_at(fd, buffer, 0x3c, 4) < 0) |
| 148 | + goto fail; |
| 149 | + |
| 150 | + /* Read the `PE\0\0` signature and the COFF file header */ |
| 151 | + offset = le32(buffer); |
| 152 | + if (read_at(fd, buffer, offset, 24) < 0) |
| 153 | + goto fail; |
| 154 | + |
| 155 | + if (buffer[0] != 'P' || buffer[1] != 'E' || buffer[2] != '\0' || buffer[3] != '\0') |
| 156 | + goto fail; |
| 157 | + |
| 158 | + num_sections = le16(buffer + 6); |
| 159 | + opt_header_size = le16(buffer + 20); |
| 160 | + offset += 24; /* skip file header */ |
| 161 | + |
| 162 | + /* |
| 163 | + * Validate magic number 0x10b or 0x20b, for full details see |
| 164 | + * https://learn.microsoft.com/en-us/windows/win32/debug/pe-format#optional-header-standard-fields-image-only |
| 165 | + */ |
| 166 | + if (read_at(fd, buffer, offset, 2) < 0 || |
| 167 | + ((i = le16(buffer)) != 0x10b && i != 0x20b)) |
| 168 | + goto fail; |
| 169 | + |
| 170 | + offset += opt_header_size; |
| 171 | + |
| 172 | + for (i = 0; i < num_sections; i++) { |
| 173 | + if (read_at(fd, buffer, offset + i * 40, 40) < 0) |
| 174 | + goto fail; |
| 175 | + |
| 176 | + /* |
| 177 | + * For full details about the section headers, see |
| 178 | + * https://learn.microsoft.com/en-us/windows/win32/debug/pe-format#section-table-section-headers |
| 179 | + */ |
| 180 | + if ((le32(buffer + 36) /* characteristics */ & ~0x600000) /* IMAGE_SCN_ALIGN_32BYTES */ == |
| 181 | + (/* IMAGE_SCN_CNT_INITIALIZED_DATA */ 0x00000040 | |
| 182 | + /* IMAGE_SCN_MEM_READ */ 0x40000000 | |
| 183 | + /* IMAGE_SCN_MEM_WRITE */ 0x80000000)) { |
| 184 | + size_t size = le32(buffer + 16); /* "SizeOfRawData " */ |
| 185 | + size_t pointer = le32(buffer + 20); /* "PointerToRawData " */ |
| 186 | + |
| 187 | + /* |
| 188 | + * Skip the section if either size or pointer is 0, see |
| 189 | + * https://github.com/golang/go/blob/go1.21.0/src/debug/buildinfo/buildinfo.go#L333 |
| 190 | + * for full details. |
| 191 | + * |
| 192 | + * Merely seeing a non-zero size will not actually do, |
| 193 | + * though: he size must be at least `buildInfoSize`, |
| 194 | + * i.e. 32, and we expect a UVarint (at least another |
| 195 | + * byte) _and_ the bytes representing the string, |
| 196 | + * which we expect to start with the letters "go" and |
| 197 | + * continue with the Go version number. |
| 198 | + */ |
| 199 | + if (size < 32 + 1 + 2 + 1 || !pointer) |
| 200 | + continue; |
| 201 | + |
| 202 | + p = malloc(size); |
| 203 | + |
| 204 | + if (!p || read_at(fd, p, pointer, size) < 0) |
| 205 | + goto fail; |
| 206 | + |
| 207 | + /* |
| 208 | + * Look for the build information embedded by Go, see |
| 209 | + * https://github.com/golang/go/blob/go1.21.0/src/debug/buildinfo/buildinfo.go#L165-L175 |
| 210 | + * for full details. |
| 211 | + * |
| 212 | + * Note: Go contains code to enforce alignment along a |
| 213 | + * 16-byte boundary. In practice, no `.exe` has been |
| 214 | + * observed that required any adjustment, therefore |
| 215 | + * this here code skips that logic for simplicity. |
| 216 | + */ |
| 217 | + q = memmem(p, size - 18, "\xff Go buildinf:", 14); |
| 218 | + if (!q) |
| 219 | + goto fail; |
| 220 | + /* |
| 221 | + * Decode the build blob. For full details, see |
| 222 | + * https://github.com/golang/go/blob/go1.21.0/src/debug/buildinfo/buildinfo.go#L177-L191 |
| 223 | + * |
| 224 | + * Note: The `endianness` values observed in practice |
| 225 | + * were always 2, therefore the complex logic to handle |
| 226 | + * any other value is skipped for simplicty. |
| 227 | + */ |
| 228 | + if ((q[14] == 8 || q[14] == 4) && q[15] == 2) { |
| 229 | + /* |
| 230 | + * Only handle a Go version string with fewer |
| 231 | + * than 128 characters, so the Go UVarint at |
| 232 | + * q[32] that indicates the string's length must |
| 233 | + * be only one byte (without the high bit set). |
| 234 | + */ |
| 235 | + if ((q[32] & 0x80) || |
| 236 | + !q[32] || |
| 237 | + (q + 33 + q[32] - p) > (ssize_t)size || |
| 238 | + q[32] + 1 > (ssize_t)go_version_size) |
| 239 | + goto fail; |
| 240 | + res = q[32]; |
| 241 | + memcpy(go_version, q + 33, res); |
| 242 | + go_version[res] = '\0'; |
| 243 | + break; |
| 244 | + } |
| 245 | + } |
| 246 | + } |
| 247 | + |
| 248 | +fail: |
| 249 | + free(p); |
| 250 | + close(fd); |
| 251 | + return res; |
| 252 | +} |
| 253 | + |
| 254 | +void win32_warn_about_git_lfs_on_windows7(int exit_code, const char *argv0) |
| 255 | +{ |
| 256 | + char buffer[128], *git_lfs = NULL; |
| 257 | + const char *p; |
| 258 | + |
| 259 | + /* |
| 260 | + * Git LFS v3.5.1 fails with an Access Violation on Windows 7; That |
| 261 | + * would usually show up as an exit code 0xc0000005. For some reason |
| 262 | + * (probably because at this point, we no longer have the _original_ |
| 263 | + * HANDLE that was returned by `CreateProcess()`) we observe other |
| 264 | + * values like 0xb00 and 0x2 instead. Since the exact exit code |
| 265 | + * seems to be inconsistent, we check for a non-zero exit status. |
| 266 | + */ |
| 267 | + if (exit_code == 0) |
| 268 | + return; |
| 269 | + if (GetVersion() >> 16 > 7601) |
| 270 | + return; /* Warn only on Windows 7 or older */ |
| 271 | + if (!istarts_with(argv0, "git-lfs ") && |
| 272 | + strcasecmp(argv0, "git-lfs")) |
| 273 | + return; |
| 274 | + if (!(git_lfs = locate_in_PATH("git-lfs"))) |
| 275 | + return; |
| 276 | + if (get_go_version(git_lfs, buffer, sizeof(buffer)) > 0 && |
| 277 | + skip_prefix(buffer, "go", &p) && |
| 278 | + versioncmp("1.21.0", p) <= 0) |
| 279 | + warning("This program was built with Go v%s\n" |
| 280 | + "i.e. without support for this Windows version:\n" |
| 281 | + "\n\t%s\n" |
| 282 | + "\n" |
| 283 | + "To work around this, you can download and install a " |
| 284 | + "working version from\n" |
| 285 | + "\n" |
| 286 | + "\thttps://github.com/git-lfs/git-lfs/releases/tag/" |
| 287 | + "v3.4.1\n", |
| 288 | + p, git_lfs); |
| 289 | + free(git_lfs); |
| 290 | +} |
0 commit comments