From 0c124cba5435c59591da7c91e16bbd901a518bd4 Mon Sep 17 00:00:00 2001 From: Usman Akinyemi Date: Sat, 15 Feb 2025 21:20:47 +0530 Subject: [PATCH 1/6] version: replace manual ASCII checks with isprint() for clarity Since the isprint() function checks for printable characters, let's replace the existing hardcoded ASCII checks with it. However, since the original checks also handled spaces, we need to account for spaces explicitly in the new check. Mentored-by: Christian Couder Signed-off-by: Usman Akinyemi Signed-off-by: Junio C Hamano --- version.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/version.c b/version.c index 4d763ab48d..6cfbb8ca56 100644 --- a/version.c +++ b/version.c @@ -2,6 +2,7 @@ #include "version.h" #include "version-def.h" #include "strbuf.h" +#include "sane-ctype.h" const char git_version_string[] = GIT_VERSION; const char git_built_from_commit_string[] = GIT_BUILT_FROM_COMMIT; @@ -29,7 +30,7 @@ const char *git_user_agent_sanitized(void) strbuf_addstr(&buf, git_user_agent()); strbuf_trim(&buf); for (size_t i = 0; i < buf.len; i++) { - if (buf.buf[i] <= 32 || buf.buf[i] >= 127) + if (!isprint(buf.buf[i]) || buf.buf[i] == ' ') buf.buf[i] = '.'; } agent = buf.buf; From cdfd081df6fa42e6cd0da1d978d41b836c1f292b Mon Sep 17 00:00:00 2001 From: Usman Akinyemi Date: Sat, 15 Feb 2025 21:20:48 +0530 Subject: [PATCH 2/6] version: refactor redact_non_printables() The git_user_agent_sanitized() function performs some sanitizing to avoid special characters being sent over the line and possibly messing up with the protocol or with the parsing on the other side. Let's extract this sanitizing into a new redact_non_printables() function, as we will want to reuse it in a following patch. For now the new redact_non_printables() function is still static as it's only needed locally. While at it, let's use strbuf_detach() to explicitly detach the string contained by the 'buf' strbuf. Mentored-by: Christian Couder Signed-off-by: Usman Akinyemi Signed-off-by: Junio C Hamano --- version.c | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/version.c b/version.c index 6cfbb8ca56..60df71fd0e 100644 --- a/version.c +++ b/version.c @@ -7,6 +7,19 @@ const char git_version_string[] = GIT_VERSION; const char git_built_from_commit_string[] = GIT_BUILT_FROM_COMMIT; +/* + * Trim and replace each character with ascii code below 32 or above + * 127 (included) using a dot '.' character. + */ +static void redact_non_printables(struct strbuf *buf) +{ + strbuf_trim(buf); + for (size_t i = 0; i < buf->len; i++) { + if (!isprint(buf->buf[i]) || buf->buf[i] == ' ') + buf->buf[i] = '.'; + } +} + const char *git_user_agent(void) { static const char *agent = NULL; @@ -28,12 +41,8 @@ const char *git_user_agent_sanitized(void) struct strbuf buf = STRBUF_INIT; strbuf_addstr(&buf, git_user_agent()); - strbuf_trim(&buf); - for (size_t i = 0; i < buf.len; i++) { - if (!isprint(buf.buf[i]) || buf.buf[i] == ' ') - buf.buf[i] = '.'; - } - agent = buf.buf; + redact_non_printables(&buf); + agent = strbuf_detach(&buf, NULL); } return agent; From 0a78d61247922f30ebf2ce09025dcaa7bd7e3583 Mon Sep 17 00:00:00 2001 From: Usman Akinyemi Date: Sat, 15 Feb 2025 21:20:49 +0530 Subject: [PATCH 3/6] version: refactor get_uname_info() Some code from "builtin/bugreport.c" uses uname(2) to get system information. Let's refactor this code into a new get_uname_info() function, so that we can reuse it in a following commit. Mentored-by: Christian Couder Signed-off-by: Usman Akinyemi Signed-off-by: Junio C Hamano --- builtin/bugreport.c | 13 ++----------- version.c | 20 ++++++++++++++++++++ version.h | 7 +++++++ 3 files changed, 29 insertions(+), 11 deletions(-) diff --git a/builtin/bugreport.c b/builtin/bugreport.c index 7c2df035c9..5e13d532a8 100644 --- a/builtin/bugreport.c +++ b/builtin/bugreport.c @@ -12,10 +12,10 @@ #include "diagnose.h" #include "object-file.h" #include "setup.h" +#include "version.h" static void get_system_info(struct strbuf *sys_info) { - struct utsname uname_info; char *shell = NULL; /* get git version from native cmd */ @@ -24,16 +24,7 @@ static void get_system_info(struct strbuf *sys_info) /* system call for other version info */ strbuf_addstr(sys_info, "uname: "); - if (uname(&uname_info)) - strbuf_addf(sys_info, _("uname() failed with error '%s' (%d)\n"), - strerror(errno), - errno); - else - strbuf_addf(sys_info, "%s %s %s %s\n", - uname_info.sysname, - uname_info.release, - uname_info.version, - uname_info.machine); + get_uname_info(sys_info); strbuf_addstr(sys_info, _("compiler info: ")); get_compiler_info(sys_info); diff --git a/version.c b/version.c index 60df71fd0e..3ec8b8243d 100644 --- a/version.c +++ b/version.c @@ -3,6 +3,7 @@ #include "version-def.h" #include "strbuf.h" #include "sane-ctype.h" +#include "gettext.h" const char git_version_string[] = GIT_VERSION; const char git_built_from_commit_string[] = GIT_BUILT_FROM_COMMIT; @@ -47,3 +48,22 @@ const char *git_user_agent_sanitized(void) return agent; } + +int get_uname_info(struct strbuf *buf) +{ + struct utsname uname_info; + + if (uname(&uname_info)) { + strbuf_addf(buf, _("uname() failed with error '%s' (%d)\n"), + strerror(errno), + errno); + return -1; + } + + strbuf_addf(buf, "%s %s %s %s\n", + uname_info.sysname, + uname_info.release, + uname_info.version, + uname_info.machine); + return 0; +} diff --git a/version.h b/version.h index 7c62e80577..afe3dbbab7 100644 --- a/version.h +++ b/version.h @@ -7,4 +7,11 @@ extern const char git_built_from_commit_string[]; const char *git_user_agent(void); const char *git_user_agent_sanitized(void); +/* + Try to get information about the system using uname(2). + Return -1 and put an error message into 'buf' in case of uname() + error. Return 0 and put uname info into 'buf' otherwise. +*/ +int get_uname_info(struct strbuf *buf); + #endif /* VERSION_H */ From 6aa09fd8726b7e8de37c0187a83c2c0fca280358 Mon Sep 17 00:00:00 2001 From: Usman Akinyemi Date: Sat, 15 Feb 2025 21:20:50 +0530 Subject: [PATCH 4/6] version: extend get_uname_info() to hide system details Currently, get_uname_info() function provides the full OS information. In a following commit, we will need it to provide only the OS name. Let's extend it to accept a "full" flag that makes it switch between providing full OS information and providing only the OS name. We may need to refactor this function in the future if an `osVersion.format` is added. Mentored-by: Christian Couder Signed-off-by: Usman Akinyemi Signed-off-by: Junio C Hamano --- builtin/bugreport.c | 2 +- version.c | 16 +++++++++------- version.h | 2 +- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/builtin/bugreport.c b/builtin/bugreport.c index 5e13d532a8..e3288a86c8 100644 --- a/builtin/bugreport.c +++ b/builtin/bugreport.c @@ -24,7 +24,7 @@ static void get_system_info(struct strbuf *sys_info) /* system call for other version info */ strbuf_addstr(sys_info, "uname: "); - get_uname_info(sys_info); + get_uname_info(sys_info, 1); strbuf_addstr(sys_info, _("compiler info: ")); get_compiler_info(sys_info); diff --git a/version.c b/version.c index 3ec8b8243d..d95221a72a 100644 --- a/version.c +++ b/version.c @@ -49,7 +49,7 @@ const char *git_user_agent_sanitized(void) return agent; } -int get_uname_info(struct strbuf *buf) +int get_uname_info(struct strbuf *buf, unsigned int full) { struct utsname uname_info; @@ -59,11 +59,13 @@ int get_uname_info(struct strbuf *buf) errno); return -1; } - - strbuf_addf(buf, "%s %s %s %s\n", - uname_info.sysname, - uname_info.release, - uname_info.version, - uname_info.machine); + if (full) + strbuf_addf(buf, "%s %s %s %s\n", + uname_info.sysname, + uname_info.release, + uname_info.version, + uname_info.machine); + else + strbuf_addf(buf, "%s\n", uname_info.sysname); return 0; } diff --git a/version.h b/version.h index afe3dbbab7..5eb586c0bd 100644 --- a/version.h +++ b/version.h @@ -12,6 +12,6 @@ const char *git_user_agent_sanitized(void); Return -1 and put an error message into 'buf' in case of uname() error. Return 0 and put uname info into 'buf' otherwise. */ -int get_uname_info(struct strbuf *buf); +int get_uname_info(struct strbuf *buf, unsigned int full); #endif /* VERSION_H */ From 15ff206863a77e3396f5a1e1ed4910b7b70c9f8d Mon Sep 17 00:00:00 2001 From: Usman Akinyemi Date: Sat, 15 Feb 2025 21:20:51 +0530 Subject: [PATCH 5/6] t5701: add setup test to remove side-effect dependency Currently, the "test capability advertisement" test creates some files with expected content which are used by other tests below it. To remove that side-effect from this test, let's split up part of it into a "setup"-type test which creates the files with expected content which gets reused by multiple tests. This will be useful in a following commit. Mentored-by: Christian Couder Signed-off-by: Usman Akinyemi Signed-off-by: Junio C Hamano --- t/t5701-git-serve.sh | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/t/t5701-git-serve.sh b/t/t5701-git-serve.sh index de904c1655..4c24a188b9 100755 --- a/t/t5701-git-serve.sh +++ b/t/t5701-git-serve.sh @@ -7,22 +7,28 @@ export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME . ./test-lib.sh -test_expect_success 'test capability advertisement' ' +test_expect_success 'setup to generate files with expected content' ' + printf "agent=git/%s\n" "$(git version | cut -d" " -f3)" >agent_capability && + test_oid_cache <<-EOF && wrong_algo sha1:sha256 wrong_algo sha256:sha1 EOF + cat >expect.base <<-EOF && version 2 - agent=git/$(git version | cut -d" " -f3) + $(cat agent_capability) ls-refs=unborn fetch=shallow wait-for-done server-option object-format=$(test_oid algo) EOF - cat >expect.trailer <<-EOF && + cat >expect.trailer <<-EOF 0000 EOF +' + +test_expect_success 'test capability advertisement' ' cat expect.base expect.trailer >expect && GIT_TEST_SIDEBAND_ALL=0 test-tool serve-v2 \ From cf7ee481902df64b26ac8b1741eca861a8d2f7cc Mon Sep 17 00:00:00 2001 From: Usman Akinyemi Date: Sat, 15 Feb 2025 21:20:52 +0530 Subject: [PATCH 6/6] agent: advertise OS name via agent capability As some issues that can happen with a Git client can be operating system specific, it can be useful for a server to know which OS a client is using. In the same way it can be useful for a client to know which OS a server is using. Our current agent capability is in the form of "package/version" (e.g., "git/1.8.3.1"). Let's extend it to include the operating system name (os) i.e in the form "package/version-os" (e.g., "git/1.8.3.1-Linux"). Including OS details in the agent capability simplifies implementation, maintains backward compatibility, avoids introducing a new capability, encourages adoption across Git-compatible software, and enhances debugging by providing complete environment information without affecting functionality. The operating system name is retrieved using the 'sysname' field of the `uname(2)` system call or its equivalent. However, there are differences between `uname(1)` (command-line utility) and `uname(2)` (system call) outputs on Windows. These discrepancies complicate testing on Windows platforms. For example: - `uname(1)` output: MINGW64_NT-10.0-20348.3.4.10-87d57229.x86_64\ .2024-02-14.20:17.UTC.x86_64 - `uname(2)` output: Windows.10.0.20348 On Windows, uname(2) is not actually system-supplied but is instead already faked up by Git itself. We could have overcome the test issue on Windows by implementing a new `uname` subcommand in `test-tool` using uname(2), but except uname(2), which would be tested against itself, there would be nothing platform specific, so it's just simpler to disable the tests on Windows. Mentored-by: Christian Couder Signed-off-by: Usman Akinyemi Signed-off-by: Junio C Hamano --- Documentation/gitprotocol-v2.txt | 10 +++++++--- connect.c | 2 +- t/t5701-git-serve.sh | 16 +++++++++++++++- t/test-lib-functions.sh | 8 ++++++++ version.c | 29 ++++++++++++++++++++++++++++- version.h | 3 +++ 6 files changed, 62 insertions(+), 6 deletions(-) diff --git a/Documentation/gitprotocol-v2.txt b/Documentation/gitprotocol-v2.txt index 1652fef3ae..9f6350bbf2 100644 --- a/Documentation/gitprotocol-v2.txt +++ b/Documentation/gitprotocol-v2.txt @@ -184,9 +184,13 @@ form `agent=X`) to notify the client that the server is running version the `agent` capability with a value `Y` (in the form `agent=Y`) in its request to the server (but it MUST NOT do so if the server did not advertise the agent capability). The `X` and `Y` strings may contain any -printable ASCII characters except space (i.e., the byte range 32 < x < -127), and are typically of the form "package/version" (e.g., -"git/1.8.3.1"). The agent strings are purely informative for statistics +printable ASCII characters except space (i.e., the byte range 33 <= x <= +126), and are typically of the form "package/version-os" (e.g., +"git/1.8.3.1-Linux") where `os` is the operating system name (e.g., +"Linux"). `X` and `Y` can be configured using the GIT_USER_AGENT +environment variable and it takes priority. The `os` is +retrieved using the 'sysname' field of the `uname(2)` system call +or its equivalent. The agent strings are purely informative for statistics and debugging purposes, and MUST NOT be used to programmatically assume the presence or absence of particular features. diff --git a/connect.c b/connect.c index 10fad43e98..4d85479075 100644 --- a/connect.c +++ b/connect.c @@ -625,7 +625,7 @@ const char *parse_feature_value(const char *feature_list, const char *feature, s *offset = found + len - orig_start; return value; } - /* feature with a value (e.g., "agent=git/1.2.3") */ + /* feature with a value (e.g., "agent=git/1.2.3-Linux") */ else if (*value == '=') { size_t end; diff --git a/t/t5701-git-serve.sh b/t/t5701-git-serve.sh index 4c24a188b9..678a346ed0 100755 --- a/t/t5701-git-serve.sh +++ b/t/t5701-git-serve.sh @@ -8,13 +8,19 @@ export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME . ./test-lib.sh test_expect_success 'setup to generate files with expected content' ' - printf "agent=git/%s\n" "$(git version | cut -d" " -f3)" >agent_capability && + printf "agent=git/%s" "$(git version | cut -d" " -f3)" >agent_capability && test_oid_cache <<-EOF && wrong_algo sha1:sha256 wrong_algo sha256:sha1 EOF + if test_have_prereq WINDOWS + then + printf "agent=FAKE\n" >agent_capability + else + printf -- "-%s\n" $(uname -s | test_redact_non_printables) >>agent_capability + fi && cat >expect.base <<-EOF && version 2 $(cat agent_capability) @@ -31,6 +37,10 @@ test_expect_success 'setup to generate files with expected content' ' test_expect_success 'test capability advertisement' ' cat expect.base expect.trailer >expect && + if test_have_prereq WINDOWS + then + GIT_USER_AGENT=FAKE && export GIT_USER_AGENT + fi && GIT_TEST_SIDEBAND_ALL=0 test-tool serve-v2 \ --advertise-capabilities >out && test-tool pkt-line unpack actual && @@ -361,6 +371,10 @@ test_expect_success 'test capability advertisement with uploadpack.advertiseBund expect.extra \ expect.trailer >expect && + if test_have_prereq WINDOWS + then + GIT_USER_AGENT=FAKE && export GIT_USER_AGENT + fi && GIT_TEST_SIDEBAND_ALL=0 test-tool serve-v2 \ --advertise-capabilities >out && test-tool pkt-line unpack actual && diff --git a/t/test-lib-functions.sh b/t/test-lib-functions.sh index 78e054ab50..3465904323 100644 --- a/t/test-lib-functions.sh +++ b/t/test-lib-functions.sh @@ -2007,3 +2007,11 @@ test_trailing_hash () { test-tool hexdump | sed "s/ //g" } + +# Trim and replace each character with ascii code below 32 or above +# 127 (included) using a dot '.' character. +# Octal intervals \001-\040 and \177-\377 +# correspond to decimal intervals 1-32 and 127-255 +test_redact_non_printables () { + tr -d "\n\r" | tr "[\001-\040][\177-\377]" "." +} diff --git a/version.c b/version.c index d95221a72a..8e927cf1eb 100644 --- a/version.c +++ b/version.c @@ -1,8 +1,9 @@ +#define USE_THE_REPOSITORY_VARIABLE + #include "git-compat-util.h" #include "version.h" #include "version-def.h" #include "strbuf.h" -#include "sane-ctype.h" #include "gettext.h" const char git_version_string[] = GIT_VERSION; @@ -34,6 +35,27 @@ const char *git_user_agent(void) return agent; } +/* + Retrieve, sanitize and cache operating system info for subsequent + calls. Return a pointer to the sanitized operating system info + string. +*/ +static const char *os_info(void) +{ + static const char *os = NULL; + + if (!os) { + struct strbuf buf = STRBUF_INIT; + + get_uname_info(&buf, 0); + /* Sanitize the os information immediately */ + redact_non_printables(&buf); + os = strbuf_detach(&buf, NULL); + } + + return os; +} + const char *git_user_agent_sanitized(void) { static const char *agent = NULL; @@ -42,6 +64,11 @@ const char *git_user_agent_sanitized(void) struct strbuf buf = STRBUF_INIT; strbuf_addstr(&buf, git_user_agent()); + + if (!getenv("GIT_USER_AGENT")) { + strbuf_addch(&buf, '-'); + strbuf_addstr(&buf, os_info()); + } redact_non_printables(&buf); agent = strbuf_detach(&buf, NULL); } diff --git a/version.h b/version.h index 5eb586c0bd..bbde6d371a 100644 --- a/version.h +++ b/version.h @@ -1,6 +1,8 @@ #ifndef VERSION_H #define VERSION_H +struct repository; + extern const char git_version_string[]; extern const char git_built_from_commit_string[]; @@ -14,4 +16,5 @@ const char *git_user_agent_sanitized(void); */ int get_uname_info(struct strbuf *buf, unsigned int full); + #endif /* VERSION_H */