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

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

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

进度条样式

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

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

  0K .......... .......... .......... .......... ..........  0%  107K 8m30s 50K .......... .......... .......... .......... ..........  0% 52.0K 13m0s100K .......... .......... .......... .......... ..........  0% 31.3K 18m21s150K .......... .......... .......... .......... ..........  0% 22.0K 24m4s

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

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

filename   0%[                     ] 134.05K  59.7KB/s

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

这两种类型可以使用

wget --progress=dotwget --progress=bar

切换。

跑马灯

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

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

this_is_a_his_is_a_fis_is_a_fis_is_a_fil_is_a_fileis_a_file_s_a_file_n_a_file_naa_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 = 10def 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_f2 12 is_is_a_fi3 13 s_is_a_fil4 14 _is_a_file5 15 is_a_file_6 16 s_a_file_n7 17 _a_file_na8 18 a_file_nam                 <-- WTF!0 10 this_is_a_

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

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

随机文章原创文章采用知识共享 Attribution-ShareAlike 3.0 China Mainland 许可协议进行许可.

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

相关文章:

你感兴趣的文章:

标签云: