Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

run-command: be helpful when Git LFS fails on Windows 7 #5042

Merged
merged 1 commit into from
Jul 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
195 changes: 195 additions & 0 deletions compat/win32/path-utils.c
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
#include "../../git-compat-util.h"
#include "../../wrapper.h"
#include "../../strbuf.h"
#include "../../versioncmp.h"

int win32_has_dos_drive_prefix(const char *path)
{
Expand Down Expand Up @@ -50,3 +53,195 @@ int win32_offset_1st_component(const char *path)

return pos + is_dir_sep(*pos) - path;
}

static int read_at(int fd, char *buffer, size_t offset, size_t size)
{
if (lseek(fd, offset, SEEK_SET) < 0) {
fprintf(stderr, "could not seek to 0x%x\n", (unsigned int)offset);
return -1;
}

return read_in_full(fd, buffer, size);
}

static size_t le16(const char *buffer)
{
unsigned char *u = (unsigned char *)buffer;
return u[0] | (u[1] << 8);
}

static size_t le32(const char *buffer)
{
return le16(buffer) | (le16(buffer + 2) << 16);
}

/*
* Determine the Go version of a given executable, if it was built with Go.
*
* This recapitulates the logic from
* https://github.com/golang/go/blob/master/src/cmd/go/internal/version/version.go
* (without requiring the user to install `go.exe` to find out).
*/
static ssize_t get_go_version(const char *path, char *go_version, size_t go_version_size)
{
int fd = open(path, O_RDONLY);
char buffer[1024];
off_t offset;
size_t num_sections, opt_header_size, i;
char *p = NULL, *q;
ssize_t res = -1;

if (fd < 0)
return -1;

if (read_in_full(fd, buffer, 2) < 0)
goto fail;

/*
* Parse the PE file format, for more details, see
* https://en.wikipedia.org/wiki/Portable_Executable#Layout and
* https://learn.microsoft.com/en-us/windows/win32/debug/pe-format
*/
if (buffer[0] != 'M' || buffer[1] != 'Z')
goto fail;

if (read_at(fd, buffer, 0x3c, 4) < 0)
goto fail;

/* Read the `PE\0\0` signature and the COFF file header */
offset = le32(buffer);
if (read_at(fd, buffer, offset, 24) < 0)
goto fail;

if (buffer[0] != 'P' || buffer[1] != 'E' || buffer[2] != '\0' || buffer[3] != '\0')
goto fail;

num_sections = le16(buffer + 6);
opt_header_size = le16(buffer + 20);
offset += 24; /* skip file header */

/*
* Validate magic number 0x10b or 0x20b, for full details see
* https://learn.microsoft.com/en-us/windows/win32/debug/pe-format#optional-header-standard-fields-image-only
*/
if (read_at(fd, buffer, offset, 2) < 0 ||
((i = le16(buffer)) != 0x10b && i != 0x20b))
goto fail;

offset += opt_header_size;

for (i = 0; i < num_sections; i++) {
if (read_at(fd, buffer, offset + i * 40, 40) < 0)
goto fail;

/*
* For full details about the section headers, see
* https://learn.microsoft.com/en-us/windows/win32/debug/pe-format#section-table-section-headers
*/
if ((le32(buffer + 36) /* characteristics */ & ~0x600000) /* IMAGE_SCN_ALIGN_32BYTES */ ==
(/* IMAGE_SCN_CNT_INITIALIZED_DATA */ 0x00000040 |
/* IMAGE_SCN_MEM_READ */ 0x40000000 |
/* IMAGE_SCN_MEM_WRITE */ 0x80000000)) {
size_t size = le32(buffer + 16); /* "SizeOfRawData " */
size_t pointer = le32(buffer + 20); /* "PointerToRawData " */

/*
* Skip the section if either size or pointer is 0, see
* https://github.com/golang/go/blob/go1.21.0/src/debug/buildinfo/buildinfo.go#L333
* for full details.
*
* Merely seeing a non-zero size will not actually do,
* though: he size must be at least `buildInfoSize`,
* i.e. 32, and we expect a UVarint (at least another
* byte) _and_ the bytes representing the string,
* which we expect to start with the letters "go" and
* continue with the Go version number.
*/
if (size < 32 + 1 + 2 + 1 || !pointer)
continue;

p = malloc(size);

if (!p || read_at(fd, p, pointer, size) < 0)
goto fail;

/*
* Look for the build information embedded by Go, see
* https://github.com/golang/go/blob/go1.21.0/src/debug/buildinfo/buildinfo.go#L165-L175
* for full details.
*
* Note: Go contains code to enforce alignment along a
* 16-byte boundary. In practice, no `.exe` has been
* observed that required any adjustment, therefore
* this here code skips that logic for simplicity.
*/
q = memmem(p, size - 18, "\xff Go buildinf:", 14);
if (!q)
goto fail;
/*
* Decode the build blob. For full details, see
* https://github.com/golang/go/blob/go1.21.0/src/debug/buildinfo/buildinfo.go#L177-L191
*
* Note: The `endianness` values observed in practice
* were always 2, therefore the complex logic to handle
* any other value is skipped for simplicty.
*/
if ((q[14] == 8 || q[14] == 4) && q[15] == 2) {
/*
* Only handle a Go version string with fewer
* than 128 characters, so the Go UVarint at
* q[32] that indicates the string's length must
* be only one byte (without the high bit set).
*/
if ((q[32] & 0x80) ||
!q[32] ||
(q + 33 + q[32] - p) > size ||
q[32] + 1 > go_version_size)
goto fail;
res = q[32];
memcpy(go_version, q + 33, res);
go_version[res] = '\0';
break;
}
}
}

fail:
free(p);
close(fd);
return res;
}

void win32_warn_about_git_lfs_on_windows7(int exit_code, const char *argv0)
{
char buffer[128], *git_lfs = NULL;
const char *p;

/*
* Git LFS v3.5.1 fails with an Access Violation on Windows 7; That
* would usually show up as an exit code 0xc0000005. For some reason
* (probably because at this point, we no longer have the _original_
* HANDLE that was returned by `CreateProcess()`) we get 0xb00 instead.
*/
if (exit_code != 0x0b00)
return;
if (GetVersion() >> 16 > 7601)
return; /* Warn only on Windows 7 or older */
if (!starts_with(argv0, "git-lfs ") ||
!(git_lfs = locate_in_PATH("git-lfs")))
return;
if (get_go_version(git_lfs, buffer, sizeof(buffer)) > 0 &&
skip_prefix(buffer, "go", &p) &&
versioncmp("1.21.0", p) <= 0)
warning("This program was built with Go v%s\n"
"i.e. without support for this Windows version:\n"
"\n\t%s\n"
"\n"
"To work around this, you can download and install a "
"working version from\n"
"\n"
"\thttps://github.com/git-lfs/git-lfs/releases/tag/"
"v3.4.1\n",
p, git_lfs);
free(git_lfs);
}
3 changes: 3 additions & 0 deletions compat/win32/path-utils.h
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,7 @@ static inline int win32_has_dir_sep(const char *path)
int win32_offset_1st_component(const char *path);
#define offset_1st_component win32_offset_1st_component

void win32_warn_about_git_lfs_on_windows7(int exit_code, const char *argv0);
#define warn_about_git_lfs_on_windows7 win32_warn_about_git_lfs_on_windows7

#endif
6 changes: 6 additions & 0 deletions git-compat-util.h
Original file line number Diff line number Diff line change
Expand Up @@ -520,6 +520,12 @@ static inline int git_offset_1st_component(const char *path)
#define offset_1st_component git_offset_1st_component
#endif

#ifndef warn_about_git_lfs_on_windows7
static inline void warn_about_git_lfs_on_windows7(int exit_code, const char *argv0)
{
}
#endif

#ifndef is_valid_path
#define is_valid_path(path) 1
#endif
Expand Down
1 change: 1 addition & 0 deletions run-command.c
Original file line number Diff line number Diff line change
Expand Up @@ -566,6 +566,7 @@ static int wait_or_whine(pid_t pid, const char *argv0, int in_signal)
*/
code += 128;
} else if (WIFEXITED(status)) {
warn_about_git_lfs_on_windows7(status, argv0);
code = WEXITSTATUS(status);
} else {
if (!in_signal)
Expand Down
Loading