tool_xattr: add support for Windows alternate data stream

Requires a supported filesystem, i.e. NTFS.

Example:
```console
> curl.exe https://curl.se/index.html --output test.txt --xattr --referer https://curl.se/
> cat < test.txt:Zone.Identifier

[ZoneTransfer]
HostUrl=https://curl.se/index.html
ReferrerUrl=https://curl.se/
```

Where newlines are CLRF, `ReferrerUrl` is set when using `--referer`
option.

Also:
- test688: make it test `--referer`.
- document `user.xdg.referrer.url` in `--xattr` man page.

Refs:
https://en.wikipedia.org/wiki/Mark_of_the_Web
https://en.wikipedia.org/wiki/NTFS#Alternate_data_stream_(ADS)
https://learn.microsoft.com/en-us/windows/win32/fileio/file-streams
https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-fscc/c54dec26-1551-4d3a-a0ea-4fa40f848eb3

Ref: #22345

Closes #22354
This commit is contained in:
Viktor Szakats 2026-07-20 10:29:41 +02:00
parent 057b251358
commit 6bff85be68
No known key found for this signature in database
8 changed files with 107 additions and 22 deletions

View file

@ -392,6 +392,7 @@ IMAPS
imaps
impacket
implementers
INI
init
initializer
inlined

View file

@ -20,6 +20,11 @@ Store metadata in the extended file attributes.
When saving output to a file, tell curl to store file metadata in extended
file attributes. Currently, `curl` is stored in the `creator` attribute,
the URL is stored in the `xdg.origin.url` attribute and, for HTTP, the content
type is stored in the `mime_type` attribute. If the file system does not
support extended attributes, a warning is issued.
the URL is stored in the `xdg.origin.url` attribute, for HTTP, the content
type is stored in the `mime_type` attribute, and if set, the referrer URL in
`user.xdg.referrer.url`. If the file system does not support extended
attributes, a warning is issued.
Since curl 8.22.0 this option is also supported on Windows, where it creates
an Alternate Data Stream named `Zone.Identifier`. It contains an INI formatted
`ZoneTransfer` section, with values: `HostUrl`, `ReferrerUrl` (if set).

View file

@ -650,7 +650,7 @@ static CURLcode post_output_handling(struct per_transfer *per,
/* Set file extended attributes */
if(!result && config->xattr && outs->fopened && outs->stream) {
rc = fwrite_xattr(curl, per->url, fileno(outs->stream));
rc = fwrite_xattr(curl, per->url, fileno(outs->stream), outs->filename);
if(rc) {
char errbuf[STRERROR_LEN];
warnf("Error setting extended attributes on '%s': %s", outs->filename,

View file

@ -27,19 +27,6 @@
#ifdef USE_XATTR
/* mapping table of curl metadata to extended attribute names */
static const struct xattr_mapping {
const char *attr; /* name of the xattr */
CURLINFO info;
} mappings[] = {
/* mappings proposed by
* https://freedesktop.org/wiki/CommonExtendedAttributes/
*/
{ "user.xdg.referrer.url", CURLINFO_REFERER },
{ "user.mime_type", CURLINFO_CONTENT_TYPE },
{ NULL, CURLINFO_NONE } /* last element, abort here */
};
/* returns a new URL that needs to be freed */
/* @unittest: 1621 */
UNITTEST char *stripcredentials(const char *url)
@ -74,6 +61,20 @@ error:
return NULL;
}
#ifndef _WIN32
/* mapping table of curl metadata to extended attribute names */
static const struct xattr_mapping {
const char *attr; /* name of the xattr */
CURLINFO info;
} mappings[] = {
/* mappings proposed by
* https://freedesktop.org/wiki/CommonExtendedAttributes/
*/
{ "user.xdg.referrer.url", CURLINFO_REFERER },
{ "user.mime_type", CURLINFO_CONTENT_TYPE },
{ NULL, CURLINFO_NONE } /* last element, abort here */
};
static int xattr(int fd,
const char *attr, /* name of the xattr */
const char *value)
@ -102,13 +103,70 @@ static int xattr(int fd,
}
return err;
}
#else
static int win32_file_stream(CURL *curl, FILE *fs, const char *url)
{
int err = 1;
char *value = NULL;
char *nurl = stripcredentials(url);
CURLcode result = curl_easy_getinfo(curl, CURLINFO_REFERER, &value);
if(nurl && !result) {
err = 0;
err |= (fputs("[ZoneTransfer]\n", fs) == EOF);
if(value) {
err |= (fputs("ReferrerUrl=", fs) == EOF);
err |= (fputs(value, fs) == EOF);
err |= (fputs("\n", fs) == EOF);
}
err |= (fputs("HostUrl=", fs) == EOF);
err |= (fputs(nurl, fs) == EOF);
err |= (fputs("\n", fs) == EOF);
}
curl_free(nurl);
return err;
}
#endif /* !_WIN32 */
/* store metadata from the curl request alongside the downloaded
* file using extended attributes
*/
int fwrite_xattr(CURL *curl, const char *url, int fd)
int fwrite_xattr(CURL *curl, const char *url, int fd, const char *filename)
{
int err;
#ifdef _WIN32
char *fn_abs, *fn_stream;
FILE *fs;
(void)fd;
/* convert to absolute path to prevent Windows interpreting a 'X:<stream>'
filename as 'drive-letter:<filename>'. */
fn_abs = _fullpath(NULL, filename, 0);
if(!fn_abs)
return 1;
fn_stream = curl_maprintf("%s:%s", fn_abs, "Zone.Identifier");
/* !checksrc! disable BANNEDFUNC 1 */
free(fn_abs); /* allocated by CRT, use system free() */
if(!fn_stream)
return 1;
fs = curlx_fopen(fn_stream, FOPEN_WRITETEXT);
curl_free(fn_stream);
if(!fs)
return 1;
#ifdef DEBUGBUILD
if(getenv("CURL_FAKE_XATTR"))
win32_file_stream(curl, stdout, url);
#endif
err = win32_file_stream(curl, fs, url);
curlx_fclose(fs);
#else
int i = 0;
int err = xattr(fd, "user.creator", "curl");
(void)filename;
err = xattr(fd, "user.creator", "curl");
/* loop through all xattr-curlinfo pairs and abort on a set error */
while(!err && mappings[i].attr) {
@ -125,6 +183,7 @@ int fwrite_xattr(CURL *curl, const char *url, int fd)
err = xattr(fd, "user.xdg.origin.url", nurl);
curl_free(nurl);
}
#endif
return err;
}
#endif

View file

@ -33,17 +33,19 @@
# include <sys/types.h>
# include <sys/extattr.h>
# define USE_XATTR
#elif defined(_WIN32)
# define USE_XATTR
#endif
#ifdef USE_XATTR
int fwrite_xattr(CURL *curl, const char *url, int fd);
int fwrite_xattr(CURL *curl, const char *url, int fd, const char *filename);
#ifdef UNITTESTS
UNITTEST char *stripcredentials(const char *url);
#endif
#else
#define fwrite_xattr(a, b, c) 0
#define fwrite_xattr(a, b, c, d) 0
#endif
#endif /* HEADER_CURL_TOOL_XATTR_H */

View file

@ -76,9 +76,14 @@ Accept: */*
</protocol>
<stdout mode="text">
%if win32
[ZoneTransfer]
HostUrl=http://%HOSTIP:%HTTPPORT/%TESTNUMBER
%else
user.creator => curl
user.mime_type => text/html
user.xdg.origin.url => http://%HOSTIP:%HTTPPORT/%TESTNUMBER
%endif
</stdout>
</verify>
</testcase>

View file

@ -53,9 +53,14 @@ Accept: */*
</protocol>
<stdout mode="text">
%if win32
[ZoneTransfer]
HostUrl=http://%HOSTIP:%HTTPPORT/%TESTNUMBER
%else
user.creator => curl
user.mime_type => fake/data
user.xdg.origin.url => http://%HOSTIP:%HTTPPORT/%TESTNUMBER
%endif
</stdout>
</verify>
</testcase>

View file

@ -39,7 +39,7 @@ CURL_FAKE_XATTR=1
basic --xattr with (uppercase) -O
</name>
<command>
--xattr -O --output-dir %LOGDIR http://%HOSTIP:%HTTPPORT/%TESTNUMBER
--xattr -O --output-dir %LOGDIR http://%HOSTIP:%HTTPPORT/%TESTNUMBER --referer https://referrer.invalid/
</command>
</client>
@ -50,12 +50,20 @@ GET /%TESTNUMBER HTTP/1.1
Host: %HOSTIP:%HTTPPORT
User-Agent: curl/%VERSION
Accept: */*
Referer: https://referrer.invalid/
</protocol>
<stdout mode="text">
%if win32
[ZoneTransfer]
ReferrerUrl=https://referrer.invalid/
HostUrl=http://%HOSTIP:%HTTPPORT/%TESTNUMBER
%else
user.creator => curl
user.xdg.referrer.url => https://referrer.invalid/
user.mime_type => fake/data
user.xdg.origin.url => http://%HOSTIP:%HTTPPORT/%TESTNUMBER
%endif
</stdout>
</verify>
</testcase>