Skip to content

Commit 283fdb4

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 6019ac9 + 46d14a6 commit 283fdb4

File tree

4 files changed

+205
-0
lines changed

4 files changed

+205
-0
lines changed

compat/win32/path-utils.c

Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
#include "../../git-compat-util.h"
2+
#include "../../wrapper.h"
3+
#include "../../strbuf.h"
4+
#include "../../versioncmp.h"
25

36
int win32_has_dos_drive_prefix(const char *path)
47
{
@@ -50,3 +53,195 @@ int win32_offset_1st_component(const char *path)
5053

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

compat/win32/path-utils.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,4 +30,7 @@ static inline int win32_has_dir_sep(const char *path)
3030
int win32_offset_1st_component(const char *path);
3131
#define offset_1st_component win32_offset_1st_component
3232

33+
void win32_warn_about_git_lfs_on_windows7(int exit_code, const char *argv0);
34+
#define warn_about_git_lfs_on_windows7 win32_warn_about_git_lfs_on_windows7
35+
3336
#endif

git-compat-util.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -518,6 +518,12 @@ static inline int git_offset_1st_component(const char *path)
518518
#define offset_1st_component git_offset_1st_component
519519
#endif
520520

521+
#ifndef warn_about_git_lfs_on_windows7
522+
static inline void warn_about_git_lfs_on_windows7(int exit_code, const char *argv0)
523+
{
524+
}
525+
#endif
526+
521527
#ifndef is_valid_path
522528
#define is_valid_path(path) 1
523529
#endif

run-command.c

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -568,6 +568,7 @@ static int wait_or_whine(pid_t pid, const char *argv0, int in_signal)
568568
*/
569569
code += 128;
570570
} else if (WIFEXITED(status)) {
571+
warn_about_git_lfs_on_windows7(status, argv0);
571572
code = WEXITSTATUS(status);
572573
} else {
573574
if (!in_signal)

0 commit comments

Comments
 (0)