Skip to content

Commit 3c6f5d1

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 efeb89d + 2d4c3f4 commit 3c6f5d1

File tree

4 files changed

+209
-0
lines changed

4 files changed

+209
-0
lines changed

compat/win32/path-utils.c

Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
#include "../../git-compat-util.h"
22
#include "../../environment.h"
3+
#include "../../wrapper.h"
4+
#include "../../strbuf.h"
5+
#include "../../versioncmp.h"
36

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

compat/win32/path-utils.h

Lines changed: 3 additions & 0 deletions
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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -526,6 +526,12 @@ static inline int git_offset_1st_component(const char *path)
526526
#define fspathncmp git_fspathncmp
527527
#endif
528528

529+
#ifndef warn_about_git_lfs_on_windows7
530+
static inline void warn_about_git_lfs_on_windows7(int exit_code, const char *argv0)
531+
{
532+
}
533+
#endif
534+
529535
#ifndef is_valid_path
530536
#define is_valid_path(path) 1
531537
#endif

run-command.c

Lines changed: 1 addition & 0 deletions
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)