Skip to content

Commit 341c690

Browse files
committed
Merge branch 'run-command-be-helpful-when-Git-LFS-fails-on-Windows-7'
Since Git LFS v3.5.x implicitly dropped Windows 7 support, we now want users to be advised _what_ is going wrong on that Windows version. This topic branch goes out of its way to provide users with such guidance. Signed-off-by: Johannes Schindelin <[email protected]>
2 parents f971d21 + df03e8a commit 341c690

File tree

4 files changed

+210
-0
lines changed

4 files changed

+210
-0
lines changed

compat/win32/path-utils.c

+199
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22

33
#include "../../git-compat-util.h"
44
#include "../../environment.h"
5+
#include "../../wrapper.h"
6+
#include "../../strbuf.h"
7+
#include "../../versioncmp.h"
58

69
int win32_has_dos_drive_prefix(const char *path)
710
{
@@ -89,3 +92,199 @@ int win32_fspathcmp(const char *a, const char *b)
8992
{
9093
return win32_fspathncmp(a, b, (size_t)-1);
9194
}
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) > size ||
238+
q[32] + 1 > 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+
}

compat/win32/path-utils.h

+3
Original file line numberDiff line numberDiff line change
@@ -34,4 +34,7 @@ int win32_fspathcmp(const char *a, const char *b);
3434
int win32_fspathncmp(const char *a, const char *b, size_t count);
3535
#define fspathncmp win32_fspathncmp
3636

37+
void win32_warn_about_git_lfs_on_windows7(int exit_code, const char *argv0);
38+
#define warn_about_git_lfs_on_windows7 win32_warn_about_git_lfs_on_windows7
39+
3740
#endif

git-compat-util.h

+7
Original file line numberDiff line numberDiff line change
@@ -539,6 +539,13 @@ static inline int git_offset_1st_component(const char *path)
539539
#define fspathncmp git_fspathncmp
540540
#endif
541541

542+
#ifndef warn_about_git_lfs_on_windows7
543+
static inline void warn_about_git_lfs_on_windows7(int exit_code UNUSED,
544+
const char *argv0 UNUSED)
545+
{
546+
}
547+
#endif
548+
542549
#ifndef is_valid_path
543550
#define is_valid_path(path) 1
544551
#endif

run-command.c

+1
Original file line numberDiff line numberDiff line change
@@ -575,6 +575,7 @@ static int wait_or_whine(pid_t pid, const char *argv0, int in_signal)
575575
*/
576576
code += 128;
577577
} else if (WIFEXITED(status)) {
578+
warn_about_git_lfs_on_windows7(status, argv0);
578579
code = WEXITSTATUS(status);
579580
} else {
580581
if (!in_signal)

0 commit comments

Comments
 (0)