]> git.ipfire.org Git - thirdparty/curl.git/commitdiff
tool: support fractions for --limit-rate and --max-filesize
authorDaniel Stenberg <daniel@haxx.se>
Sat, 17 Jan 2026 22:11:07 +0000 (23:11 +0100)
committerDaniel Stenberg <daniel@haxx.se>
Sat, 17 Jan 2026 22:25:26 +0000 (23:25 +0100)
Allow 2.5k or 3.7M etc. Add mention in documentation.

Verify in test case 1623.

Closes #20266

docs/cmdline-opts/limit-rate.md
docs/cmdline-opts/max-filesize.md
src/tool_getparam.c
src/tool_getparam.h
tests/data/Makefile.am
tests/data/test1623 [new file with mode: 0644]
tests/tunit/Makefile.inc
tests/tunit/tool1623.c [new file with mode: 0644]

index f58fd1fcd865cf9c76e68397900eaedc5a85386e..88f62709b0ab8d09d3e866197201f21ddfbd8cd9 100644 (file)
@@ -12,7 +12,7 @@ See-also:
   - speed-limit
   - speed-time
 Example:
-  - --limit-rate 100K $URL
+  - --limit-rate 123.45K $URL
   - --limit-rate 1000 $URL
   - --limit-rate 10M $URL
   - --limit-rate 200K --max-time 60 $URL
@@ -27,8 +27,8 @@ otherwise would be.
 
 The given speed is measured in bytes/second, unless a suffix is appended.
 Appending 'k' or 'K' counts the number as kilobytes, 'm' or 'M' makes it
-megabytes, while 'g' or 'G' makes it gigabytes. The suffixes (k, M, G, T, P)
-are 1024 based. For example 1k is 1024. Examples: 200K, 3m and 1G.
+megabytes etc. The supported suffixes (k, M, G, T, P) are 1024-based. For
+example 1k is 1024. Examples: 200K, 3m and 1G.
 
 The rate limiting logic works on averaging the transfer speed to no more than
 the set threshold over a period of multiple seconds.
@@ -36,3 +36,7 @@ the set threshold over a period of multiple seconds.
 If you also use the --speed-limit option, that option takes precedence and
 might cripple the rate-limiting slightly, to help keep the speed-limit
 logic working.
+
+Starting in curl 8.19.0, the rate can be specified using a fraction as in
+`2.5M` for two and a half megabytes per second. It only works with a period
+(`.`) delimiter, independent of what your locale might prefer.
index cf2ac6537655a4eb5bbac3fdeb76d79fe88284b0..e3fd900fe696ef1760e918afaa5a5e7ee74a9194 100644 (file)
@@ -12,6 +12,7 @@ See-also:
   - limit-rate
 Example:
   - --max-filesize 100K $URL
+  - --max-filesize 2.6M $URL
 ---
 
 # `--max-filesize`
@@ -22,9 +23,9 @@ transfer does not start and curl returns with exit code 63.
 
 Setting the maximum value to zero disables the limit.
 
-A size modifier may be used. For example, Appending 'k' or 'K' counts the
-number as kilobytes, 'm' or 'M' makes it megabytes, while 'g' or 'G' makes it
-gigabytes. Examples: 200K, 3m and 1G. (Added in 7.58.0)
+A unit suffix letter can be used. Appending 'k' or 'K' counts the number as
+kilobytes, 'm' or 'M' makes it megabytes etc. The supported suffixes (k, M, G,
+T, P) are 1024-based. Examples: 200K, 3m and 1G. (Added in 7.58.0)
 
 **NOTE**: before curl 8.4.0, when the file size is not known prior to
 download, for such files this option has no effect even if the file transfer
@@ -32,3 +33,7 @@ ends up being larger than this given limit.
 
 Starting with curl 8.4.0, this option aborts the transfer if it reaches the
 threshold during transfer.
+
+Starting in curl 8.19.0, the maximum size can be specified using a fraction as
+in `2.5M` for two and a half megabytes. It only works with a period (`.`)
+delimiter, independent of what your locale might prefer.
index e51b6f440dc6565f215f4bcda2430b5a04fbb808..56f4d58f7338a686a926cd7e96410815110530cd 100644 (file)
@@ -534,54 +534,93 @@ static ParameterError GetFileAndPassword(const char *nextarg, char **file,
   return err;
 }
 
+struct sizeunit {
+  char unit; /* single lowercase ASCII letter */
+  curl_off_t mul;
+  size_t mlen; /* number of digits in 'mul', when written in decimal */
+};
+
+static const struct sizeunit *getunit(char unit)
+{
+  static const struct sizeunit list[] = {
+    {'p', (curl_off_t)1125899906842624, 16 }, /* Peta */
+    {'t', (curl_off_t)1099511627776,    13 }, /* Tera */
+    {'g', 1073741824,                   10 }, /* Giga */
+    {'m', 1048576,                       7 }, /* Mega */
+    {'k', 1024,                          4 }, /* Kilo */
+  };
+
+  size_t i;
+  for(i = 0; i < CURL_ARRAYSIZE(list); i++)
+    if((unit | 0x20) == list[i].unit)
+      return &list[i];
+  return NULL;
+}
+
 /* Get a size parameter for '--limit-rate' or '--max-filesize'.
- * We support a 'G', 'M' or 'K' suffix too.
+   We support P, T, G, M and K (case insensitive) suffixes.
+
+   Unit test 1623
   */
-static ParameterError GetSizeParameter(const char *arg,
-                                       const char *which,
-                                       curl_off_t *value_out)
+UNITTEST ParameterError GetSizeParameter(const char *arg, curl_off_t *out)
 {
   const char *unit = arg;
   curl_off_t value;
+  curl_off_t prec = 0;
+  size_t plen = 0;
+  curl_off_t add = 0;
+  curl_off_t mul = 1;
+  int rc;
+
+  rc = curlx_str_number(&unit, &value, CURL_OFF_T_MAX);
+  if(rc == STRE_OVERFLOW)
+    return PARAM_NUMBER_TOO_LARGE;
+  else if(rc)
+    return PARAM_BAD_NUMERIC;
 
-  if(curlx_str_number(&unit, &value, CURL_OFF_T_MAX)) {
-    warnf("invalid number specified for %s", which);
+  if(!curlx_str_single(&unit, '.')) {
+    const char *s = unit;
+    if(curlx_str_number(&unit, &prec, CURL_OFF_T_MAX))
+      return PARAM_BAD_NUMERIC;
+    plen = unit - s;
+  }
+
+  if(strlen(unit) > 1)
     return PARAM_BAD_USE;
+  else if(!*unit || ((*unit | 0x20) == 'b')) {
+    if(plen)
+      /* cannot handle partial bytes */
+      return PARAM_BAD_USE;
   }
+  else {
+    const struct sizeunit *su = getunit(*unit);
+    if(!su)
+      return PARAM_BAD_USE;
+    mul = su->mul;
 
-  if(!*unit)
-    unit = "b";
-  else if(strlen(unit) > 1)
-    unit = "w"; /* unsupported */
+    if(prec) {
+      /* precision was provided */
+      curl_off_t frac = 1;
 
-  switch(*unit) {
-  case 'G':
-  case 'g':
-    if(value > (CURL_OFF_T_MAX / (1024 * 1024 * 1024)))
-      return PARAM_NUMBER_TOO_LARGE;
-    value *= 1024 * 1024 * 1024;
-    break;
-  case 'M':
-  case 'm':
-    if(value > (CURL_OFF_T_MAX / (1024 * 1024)))
-      return PARAM_NUMBER_TOO_LARGE;
-    value *= 1024 * 1024;
-    break;
-  case 'K':
-  case 'k':
-    if(value > (CURL_OFF_T_MAX / 1024))
-      return PARAM_NUMBER_TOO_LARGE;
-    value *= 1024;
-    break;
-  case 'b':
-  case 'B':
-    /* for plain bytes, leave as-is */
-    break;
-  default:
-    warnf("unsupported %s unit. Use G, M, K or B", which);
-    return PARAM_BAD_USE;
+      /* too many precision digits, trim them */
+      while(su->mlen <= plen) {
+        prec /= 10;
+        plen--;
+      }
+
+      while(plen--)
+        frac *= 10;
+
+      if((CURL_OFF_T_MAX / mul) > prec)
+        add = mul * prec / frac;
+      else
+        add = (mul / frac) * prec;
+    }
   }
-  *value_out = value;
+  if(value > ((CURL_OFF_T_MAX - add) / mul))
+    return PARAM_NUMBER_TOO_LARGE;
+
+  *out = value * mul + add;
   return PARAM_OK;
 }
 
@@ -2365,7 +2404,7 @@ static ParameterError opt_string(struct OperationConfig *config,
       err = getstr(&config->dns_servers, nextarg, DENY_BLANK);
     break;
   case C_LIMIT_RATE: /* --limit-rate */
-    err = GetSizeParameter(nextarg, "rate", &value);
+    err = GetSizeParameter(nextarg, &value);
     if(!err) {
       config->recvpersecond = value;
       config->sendpersecond = value;
@@ -2401,7 +2440,7 @@ static ParameterError opt_string(struct OperationConfig *config,
     err = getstr(&config->haproxy_clientip, nextarg, DENY_BLANK);
     break;
   case C_MAX_FILESIZE: /* --max-filesize */
-    err = GetSizeParameter(nextarg, "max-filesize", &value);
+    err = GetSizeParameter(nextarg, &value);
     if(!err)
       config->max_filesize = value;
     break;
index 44eb361edca5752a43f32bc4293be4ab11019caf..0ff0a78182d871d1ab110f9c438a582e7c22f452 100644 (file)
@@ -375,6 +375,7 @@ ParameterError getparameter(const char *flag, const char *nextarg,
 ParameterError parse_cert_parameter(const char *cert_parameter,
                                     char **certname,
                                     char **passphrase);
+UNITTEST ParameterError GetSizeParameter(const char *arg, curl_off_t *out);
 #endif
 
 ParameterError parse_args(int argc, argv_item_t argv[]);
index 99184130afcbd3f6d4e0da976f47bf40cf224b1b..7de9f5f5958ca7bbc0e3ef63ed3f90c941c0eccf 100644 (file)
@@ -214,7 +214,7 @@ test1590 test1591 test1592 test1593 test1594 test1595 test1596 test1597 \
 test1598 test1599 test1600 test1601 test1602 test1603 test1604 test1605 \
 test1606 test1607 test1608 test1609 test1610 test1611 test1612 test1613 \
 test1614 test1615 test1616 test1617 \
-test1620 test1621 test1622 \
+test1620 test1621 test1622 test1623 \
 \
 test1630 test1631 test1632 test1633 test1634 test1635 test1636 \
 \
diff --git a/tests/data/test1623 b/tests/data/test1623
new file mode 100644 (file)
index 0000000..000c08a
--- /dev/null
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="US-ASCII"?>
+<testcase>
+<info>
+<keywords>
+unittest
+tool-sizeparser
+</keywords>
+</info>
+
+# Client-side
+<client>
+<features>
+unittest
+</features>
+<name>
+the size parser for --limit-rate
+</name>
+<tool>
+tool%TESTNUMBER
+</tool>
+</client>
+
+<verify>
+</verify>
+</testcase>
index 419606568688471be7ceccc74422901a5e8ba7d5..1f3c8cb9b7e4016a7e38f332f897fbedcad4a077 100644 (file)
@@ -33,4 +33,5 @@ TESTS_C = \
   tool1394.c \
   tool1604.c \
   tool1621.c \
-  tool1622.c
+  tool1622.c \
+  tool1623.c
diff --git a/tests/tunit/tool1623.c b/tests/tunit/tool1623.c
new file mode 100644 (file)
index 0000000..9fcc0b4
--- /dev/null
@@ -0,0 +1,127 @@
+/***************************************************************************
+ *                                  _   _ ____  _
+ *  Project                     ___| | | |  _ \| |
+ *                             / __| | | | |_) | |
+ *                            | (__| |_| |  _ <| |___
+ *                             \___|\___/|_| \_\_____|
+ *
+ * Copyright (C) Daniel Stenberg, <daniel@haxx.se>, et al.
+ *
+ * This software is licensed as described in the file COPYING, which
+ * you should have received as part of this distribution. The terms
+ * are also available at https://curl.se/docs/copyright.html.
+ *
+ * You may opt to use, copy, modify, merge, publish, distribute and/or sell
+ * copies of the Software, and permit persons to whom the Software is
+ * furnished to do so, under the terms of the COPYING file.
+ *
+ * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
+ * KIND, either express or implied.
+ *
+ * SPDX-License-Identifier: curl
+ *
+ ***************************************************************************/
+#include "unitcheck.h"
+
+#include "tool_getparam.h"
+
+struct check1623 {
+  const char *input;
+  curl_off_t amount;
+  ParameterError err;
+};
+
+static CURLcode test_tool1623(const char *arg)
+{
+  UNITTEST_BEGIN_SIMPLE
+  {
+    int i;
+    static const struct check1623 check[] = {
+      { "0", 0, PARAM_OK},
+      { "00", 0, PARAM_OK},
+      { "000", 0, PARAM_OK},
+      { "1", 1, PARAM_OK},
+      { "1b", 1, PARAM_OK},
+      { "99B", 99, PARAM_OK},
+      { "2", 2, PARAM_OK},
+      { "3", 3, PARAM_OK},
+      { "4", 4, PARAM_OK},
+      { "5", 5, PARAM_OK},
+      { "6", 6, PARAM_OK},
+      { "7", 7, PARAM_OK},
+      { "77", 77, PARAM_OK},
+      { "8", 8, PARAM_OK},
+      { "9", 9, PARAM_OK},
+      { "10", 10, PARAM_OK},
+      { "010", 10, PARAM_OK},
+      { "000000000000000000000000000000000010", 10, PARAM_OK},
+      { "1k", 1024, PARAM_OK},
+      { "2K", 2048, PARAM_OK},
+      { "3k", 3072, PARAM_OK},
+      { "4K", 4096, PARAM_OK},
+      { "5k", 5120, PARAM_OK},
+      { "6K", 6144, PARAM_OK},
+      { "7k", 7168, PARAM_OK},
+      { "8K", 8192, PARAM_OK},
+      { "9k", 9216, PARAM_OK},
+      { "10K", 10240, PARAM_OK},
+      { "20M", 20971520, PARAM_OK},
+      { "30G", 32212254720, PARAM_OK},
+      { "40T", 43980465111040, PARAM_OK},
+      { "50P", 56294995342131200, PARAM_OK},
+      { "1.1k", 1126, PARAM_OK},
+      { "1.01k", 1034, PARAM_OK},
+      { "1.001k", 1025, PARAM_OK},
+      { "1.0001k", 1024, PARAM_OK},
+      { "22.1m", 23173529, PARAM_OK},
+      { "22.01m", 23079157, PARAM_OK},
+      { "22.001m", 23069720, PARAM_OK},
+      { "22.0001m", 23068776, PARAM_OK},
+      { "22.00001m", 23068682, PARAM_OK},
+      { "22.000001m", 23068673, PARAM_OK},
+      { "22.0000001m", 23068672, PARAM_OK},
+      { "22.000000001m", 23068672, PARAM_OK},
+      { "3.4", 0, PARAM_BAD_USE},
+      { "3.14b", 0, PARAM_BAD_USE},
+      { "5000.9P", 5630512844129278361, PARAM_OK},
+      { "5000.99P", 5630614175120894197, PARAM_OK},
+      { "5000.999P", 5630624308220055781, PARAM_OK},
+      { "5000.9999P", 5630625321529969316, PARAM_OK},
+      { "8191P", 9222246136947933184, PARAM_OK},
+      { "8191.9999999P", 9223372036735343194, PARAM_OK},
+      { "8192P", 0, PARAM_NUMBER_TOO_LARGE},
+      { "9223372036854775807", 9223372036854775807, PARAM_OK},
+      { "9223372036854775808", 0, PARAM_NUMBER_TOO_LARGE},
+      { "a", 0, PARAM_BAD_NUMERIC},
+      { "-2", 0, PARAM_BAD_NUMERIC},
+      { "+2", 0, PARAM_BAD_NUMERIC},
+      { "2,2k", 0, PARAM_BAD_USE},
+      { NULL, 0, PARAM_OK } /* end of list */
+    };
+
+    for(i = 0; check[i].input; i++) {
+      bool ok = FALSE;
+      curl_off_t output = 0;
+      ParameterError err =
+        GetSizeParameter(check[i].input, &output);
+      if(err != check[i].err)
+        curl_mprintf("'%s' unexpectedly returned %d \n",
+                     check[i].input, err);
+      else if(check[i].amount != output)
+        curl_mprintf("'%s' unexpectedly gave %" FMT_OFF_T "\n",
+                     check[i].input, output);
+      else {
+#if 0 /* enable for debugging */
+        if(err)
+          curl_mprintf("'%s' returned %d\n", check[i].input, err);
+        else
+          curl_mprintf("'%s' == %" FMT_OFF_T "\n", check[i].input, output);
+#endif
+        ok = TRUE;
+      }
+      if(!ok)
+        unitfail++;
+    }
+  }
+  UNITTEST_END_SIMPLE
+}