比尔盖子 博客

wget 滚动显示文件名的 Bug —— 低级错误永不可避

更新 2:wget 1.16.1 版本已包括补丁,问题彻底解决。
更新 1:我的补丁已经被接受了,Bug 已修复。

wget 是 GNU 开发的实用下载工具,最近它刚刚发布了 v1.16。

进度条样式

先介绍一下背景,wget 向来就有两种进度条,“点形”和“条形”,其中,“条形”还有可选的跑马灯效果。

刷屏点形”是指这样的效果

  0K .......... .......... .......... .......... ..........  0%  107K 8m30s
 50K .......... .......... .......... .......... ..........  0% 52.0K 13m0s
100K .......... .......... .......... .......... ..........  0% 31.3K 18m21s
150K .......... .......... .......... .......... ..........  0% 22.0K 24m4s

如果你的终端设备功能有限,不能做到即时更新屏幕内容,或者是重定向到了文件,“点形”进度条就很实用。

“条形”就是指这样的效果

filename   0%[                     ] 134.05K  59.7KB/s

很适合可以即时更新的终端。

这两种类型可以使用

wget --progress=dot
wget --progress=bar

切换。

跑马灯

现在问题来了。如果文件名称很长,条形进度条左侧那一点点空间显示不下该怎么办呢?wget 的开发者想出了一个跑马灯效果,很像大街上的 LED 横幅,不停的让文字滚动,这样用户就可以“管窥”整个文件名了。

然而,盖子发现一个特别郁闷,而且能逼死强迫症患者的问题。wget 的滚动有 Bug,文件名的最后一个字符始终不显示!就象这样:

this_is_a_
his_is_a_f
is_is_a_fi
s_is_a_fil
_is_a_file
is_a_file_
s_a_file_n
_a_file_na
a_file_nam
            <-- WTF!
this_is_a_

实现

progress.c 中,不难发现这段代码

  if (((orig_filename_cols > MAX_FILENAME_COLS) && !opt.noscroll) && !done)
    offset_cols = ((int) bp->tick) % (orig_filename_cols - MAX_FILENAME_COLS);      
  else
    offset_cols = 0;
  offset_bytes = cols_to_bytes (bp->f_download, offset_cols, cols_ret);
  bytes_in_filename = cols_to_bytes (bp->f_download + offset_bytes, MAX_FILENAME_COLS, cols_ret);
  memcpy (p, bp->f_download + offset_bytes, bytes_in_filename);
  p += bytes_in_filename;

由于有一些字符占用 1 字节,有些占用 2 字节,因此下面部分的代码全都在处理把字符转换成字节数的问题,其实这段代码做的事情很简单

 if (orig_filename_cols > MAX_FILENAME_COLS
     && !opt.noscroll  // 没有禁用跑马灯滚动效果
     && !done)  // 下载仍未结束

     offset_cols = bp->tick % (orig_filename_cols - MAX_FILENAME_COLS);

bp->tickint,每刷新一次进度条,它会就自增 1,可以把它理解成进度条刷新的次数,(orig_filename_cols - MAX_FILENAME_COLS) 就不用多说了,显然是计算文件名超出最大允许长度的字符数。

最后,从 offset_cols 开始截取 orig_filename_cols,一直截取 MAX_FILENAME_COLS 个字符。

用取余数运算来实现不断截取字符串的特性,看起来还是挺巧妙的。不过,正是这里的代码存在着问题。

范围差 1

写程序的时候,经常因为该 + 1/- 1 而忘记了(len() vs. 下标),不该加减 1 的时候乱加减,导致典型的越界往往和正确范围只差 1 位。

再比如这些令人困惑的表述

def range(begin, end)
/* 11 日到 21 日间断电 */
/* 变量取值范围 0 ~ 256 */

到底包不包括这个 end,或者包不包括 21 日,或者 256?P.S:幸好数学家们早就意识到了这个问题,发明了区间表示法,圆括号表示“排除”,方括号表示“包括”。

wget 的“最后一个字符始终不显示”的 Bug,具有典型的“范围差 1”的特征,那真正的问题到底出不出在这里呢?

实验

为了看看 wget 的这个算法到底有没有 Bug,写个程序检验一下。

FILENAME = "this_is_a_file_name"
MAX = 10

def cut(string, _min, _max):
    assert _max <= len(string) - 1 
    return string[_min:_max]

for i in range(0, 20):
    offset = i % (len(FILENAME) - MAX)
    print(offset, offset + MAX, cut(FILENAME, offset, offset + MAX))

这就是 Python 版本的简单算法实现了,运行之后:

0 10 this_is_a_
1 11 his_is_a_f
2 12 is_is_a_fi
3 13 s_is_a_fil
4 14 _is_a_file
5 15 is_a_file_
6 16 s_a_file_n
7 17 _a_file_na
8 18 a_file_nam
                 <-- WTF!
0 10 this_is_a_

果然出错?那么问题究竟出在哪里呢?拿这个例子分析一下,”this_is_a_file_name” 有 19 个字符,而最大的允许字符是 10 个。那么,那么,相差 9,看来编写者认为 9 就是需要的滚动次数。然而,滚动 9 次,就是有 10 种组合啊!

果然忘记 + 1,而接下来就不用多说了。就等开发者接受补丁了。真是低级错误不可避啊……

Categories: IT生活, Linux

想加密数据?不要购买 ACOS5-64 智能卡! » « OpenRC 无法正确配置 IPv6

3 Comments

  1. It’s the funniest bug I’ve ever seen…

    BTW will you take part in Fedora 21 Release Party this winter vacation?

  2. 話說控制台動畫可以用 https://asciinema.org/docs/embedding 嵌入, 更形象

发表评论

Your email address will not be published.

To create code blocks or other preformatted text, indent by four spaces:

    This will be displayed in a monospaced font. The first four 
    spaces will be stripped off, but all other whitespace
    will be preserved.
    
    Markdown is turned off in code blocks:
     [This is not a link](http://example.com)

To create not a block, but an inline code span, use backticks:

Here is some inline `code`.

For more help see http://daringfireball.net/projects/markdown/syntax

Copyright © 2018 比尔盖子 博客

Theme by Anders NorenUp ↑