tool_cb_hdr: with -J, use the redirect name as a backup

The -J / --remote-header-name logic now records the file name part used
in the redirects so that it can use the last one as a name if no
Content-Disposition header arrives.

Add tests to verify:

1641: -J with a redirect and extract the CD contents in the second
response

1642: -J with a redirect but no Content-Disposition, use the name from
the Location: header

1643: -J with two redirects, using the last file name and also use
queries and fragments to verify them stripped off

Closes #20430
This commit is contained in:
Daniel Stenberg 2026-01-25 16:35:53 +01:00
parent 146de2460a
commit fa6a46473e
No known key found for this signature in database
GPG key ID: 5CC908FDB71E12C2
8 changed files with 267 additions and 43 deletions

View file

@ -148,30 +148,40 @@ locdone:
/*
* Copies a filename part and returns an ALLOCATED data buffer.
*/
static char *parse_filename(const char *ptr, size_t len)
static char *parse_filename(const char *ptr, size_t len, char stop)
{
char *copy;
char *p;
char *q;
char stop = '\0';
copy = memdup0(ptr, len);
if(!copy)
return NULL;
p = copy;
if(*p == '\'' || *p == '"') {
/* store the starting quote */
stop = *p;
p++;
}
else
stop = ';';
if(stop) {
/* a Content-Disposition: header */
if(*p == '\'' || *p == '"') {
/* store the starting quote */
stop = *p;
p++;
}
/* scan for the end letter and stop there */
q = strchr(p, stop);
if(q)
*q = '\0';
/* scan for the end letter and stop there */
q = strchr(p, stop);
if(q)
*q = '\0';
}
else {
/* this is a Location: header, so we need to trim off any queries and
fragments present */
q = strchr(p, '?'); /* trim off query, if present */
if(q)
*q = '\0';
q = strchr(p, '#'); /* trim off fragment, if present */
if(q)
*q = '\0';
}
/* if the filename contains a path, only use filename portion */
q = strrchr(p, '/');
@ -283,12 +293,44 @@ static size_t save_etag(const char *etag_h, const char *endp,
* Content-Disposition header specifying a filename property.
*/
static size_t content_disposition(const char *str, const char *end,
size_t cb, struct per_transfer *per)
size_t cb, struct per_transfer *per,
long response) /* response code */
{
struct HdrCbData *hdrcbdata = &per->hdrcbdata;
struct OutStruct *outs = &per->outs;
if((cb > 20) && checkprefix("Content-disposition:", str)) {
if((cb > 9) && checkprefix("Location:", str) && (response/100 == 3)) {
/* Get the name off the location header as a temporary measure in case
there is no Content-Disposition */
const char *p = &str[9];
curlx_str_passblanks(&p);
if(p < end) { /* as a precaution */
char *filename = parse_filename(p, cb - (p - str), 0);
if(filename) {
if(outs->stream) {
/* indication of problem, get out! */
curlx_free(filename);
return CURL_WRITEFUNC_ERROR;
}
if(outs->alloc_filename)
curlx_free(outs->filename);
if(per->config->output_dir) {
outs->filename = curl_maprintf("%s/%s", per->config->output_dir,
filename);
curlx_free(filename);
if(!outs->filename)
return CURL_WRITEFUNC_ERROR;
}
else
outs->filename = filename;
outs->alloc_filename = TRUE;
outs->is_cd_filename = TRUE; /* set to avoid clobbering existing files
by default */
}
}
}
else if((cb > 20) && checkprefix("Content-disposition:", str)) {
const char *p = str + 20;
/* look for the 'filename=' parameter (encoded filenames (*=) are not
supported) */
@ -312,14 +354,17 @@ static size_t content_disposition(const char *str, const char *end,
}
p += 9;
curlx_str_passblanks(&p);
len = cb - (size_t)(p - str);
filename = parse_filename(p, len);
filename = parse_filename(p, len, ';');
if(filename) {
if(outs->stream) {
/* indication of problem, get out! */
curlx_free(filename);
return CURL_WRITEFUNC_ERROR;
}
if(outs->alloc_filename)
curlx_free(outs->filename);
if(per->config->output_dir) {
outs->filename = curl_maprintf("%s/%s", per->config->output_dir,
@ -440,13 +485,13 @@ size_t tool_header_cb(char *ptr, size_t size, size_t nmemb, void *userdata)
return rc;
}
/* Parse the content-disposition header. When honor_cd_filename is true
other headers may be stored until the content-disposition header is
reached, at which point the saved headers can be written. That means
the content_disposition() may return an rc when it has saved a
different header for writing later. */
/* Parse the content-disposition and location headers. When
honor_cd_filename is true, other headers may be stored until the
content-disposition header is reached, at which point the saved headers
can be written. That means the content_disposition() may return an rc
when it has saved a different header for writing later. */
else if(hdrcbdata->honor_cd_filename) {
size_t rc = content_disposition(str, end, cb, per);
size_t rc = content_disposition(str, end, cb, per, response);
if(rc)
return rc;
}

View file

@ -36,7 +36,8 @@
* dynamically allocated and 'belongs' to this OutStruct, otherwise FALSE.
*
* 'is_cd_filename' member is TRUE when string pointed by 'filename' has been
* set using a server-specified Content-Disposition filename, otherwise FALSE.
* set using a server-specified Content-Disposition or Location filename,
* otherwise FALSE.
*
* 'regular_file' member is TRUE when output goes to a regular file, this also
* implies that output is 'seekable' and 'appendable' and also that member