Ruby: Caution with sub/gsub

February 1, 2008 – 3:08 pm

如果你不喜欢听我讲故事,那么请直接跳到末尾。其实故事很简单,最近几天的故事都是和 RMMSeg 有关。这次我是在做 RMMSeg 的主页,昨天晚上(或者说今天凌晨)我做完了和 Ferret 的集成,并发布了 0.0.1 版。可以看到,主页我也做好了。

其实主页早就做得差不多了,只是还缺一个和 Ferret 配合使用的例子。现在那里已经有一个例子了,用 Ferret 的 Highlight 输出为 HTML 格式:

highlights = $index.highlight("content:#{key}", id,
                              :field => :content,
                              :pre_tag => "<font color=red>",
                              :post_tag => "</font>")

其实原来是一个用终端的 Escape Sequence 进行高亮的例子:

highlights = $index.highlight("content:#{key}", id,
                              :field => :content,
                              :pre_tag => "\033[36m",
                              :post_tag => "\033[m")

修改是迫不得已,我那个主页是用 Gerbil 生成的,不知道怎么回事, \033 被它搞成了一堆乱七八糟的东西,类似于这样:

5d2dedb7d78d6d1f0629ea781cb92b6822c8648e33

当时很郁闷,想大概是处理 \ 的时候的 BUG 吧,因为已经很晚了,不想再去追究,就改成了 HTML 格式发布了。今天早上起来,便想探个究竟。先做了一些实验,发现诸如 \t 之类的都是正常的,而数字就不正常了。关键是,那一长串东西是什么?

还好 Gerbil 的代码不多,看了看大概 lib/gerbil/html.rb 是我要找的文件,文件很短,所以很快就看到了可疑的东西: Digest::SHA1.hexdigest 。在 IRB 里做了一下实验,用如下的 erb 文档:

<% chapter "Foo" do %>\0<% end %>

生成出来的结果里面那串东西正是 \0 经过 Digest::SHA1.hexdigest 的结果!我看到它在那里把一些需要保护的内容先替换为 digest ,最后再替换回来。于是就进行跟踪。也就是插入一些 puts 语句了,像这种 yml 和 erb 中都夹杂着代码又有 eval 的程序,恐怕也只有用 puts 进行调试方便些了。

奇怪的是一切正常,被 digest 的东西后来都 restore 回来了,而且 \0 根本没有在这里被处理。但是问题肯定是出在 digest 上,我在 lib 里面 grep 了一下,没有发现 digest 相关的额外的东西。真是奇怪!

又仔细检查了下它的目录结构,发现 fmt 目录下有不少 yml 文件,打开一看,里面有不少地方是 erb 数据,内嵌了一些 Ruby 代码。可是并没有 digest 相关的东西,更奇怪的是:我跟踪了 chapter 这个 node 的内容,当它被产生出来的时候还是好好的 \0 ,可是到了 content 那里就变成奇怪的东西了。

百思不得其解,最后想恐怕只有弄明白整个执行流程才能知道到底在哪个地方被修改了。这才突然发现 /usr/bin/gerbil 和 Gerbil 安装目录下的那个 bin/gerbil 不是同一个文件! /usr/bin/ 下那个文件很简单,目标定位到安装目录下那个 gerbil ,里面确实也有 digest ,大致跟踪了一下,发现 node 先辈编码为它的 digest ,再在最后被替换回原来的内容,问题最后定位到这段代码:

252
253
254
255
256
257
258
# replace node with output
if nodeDefs[n.type]['silent']
  buf.sub! n.digest, ''
  buf = n.output
else
  buf.sub! n.digest, n.output
end

主要就是第 257 行,通过跟踪,在 257 行之前:

variable value
buf 5d2dedb7d78d6d1f0629ea781cb92b6822c8648e
n.digest 5d2dedb7d78d6d1f0629ea781cb92b6822c8648e
n.output <% chapter "Foo" do %>\0<% end %>

在那之后, n.digestn.output 都没有变,而 buf 却变成了:

<% chapter "Foo" do %>5d2dedb7d78d6d1f0629ea781cb92b6822c8648e<% end %>

我感到非常惊奇,难道哪个地方内存块重叠了? 8-O 想来想去,才突然恍然大悟!赶紧查了一下文档,这不正是 sub 应该有的行为吗?


Ruby 中 subgsub (当然还包括 sub!gsub! )都会处理要替换的字符串,例如:

"foobar".sub(/f(o+)b/, '<\0>[\1]') # => "<foob>[oo]ar"

这本来是很方便的,但是有时候没有注意就会造成诡异的 Bug ,例如我前面提到的这个。我没有找到是否有其他不处理替换字符串的替换方法,不过用一个 block 可以解决这个问题:

"foobar".sub(/f(o+)b/) { |match| '<\0>[\1]' } # => "<\\0>[\\1]ar"

或者做一个 Monkey Patching :

class String
  [:sub, :gsub, :sub!, :gsub!].each { |name|
    define_method "plain_#{name}" do |p, r|
      self.send(name, p) { |m| r }
    end
  }
end

所谓的各种注入,也就是字符串被误处理造成的吧,处理外来的文本的时候一定要小心再小心啊!

  1. 2 Responses to “Ruby: Caution with sub/gsub”

  2. Ruby太囧了。。。
    这个。。。好复杂。。。

    By Mike on Feb 17, 2008

  3. @Mike:
    其实是一个方便之举啊,只是在有些情况下没有注意到反而导致 Bug 了。

    By pluskid on Feb 17, 2008

Post a Comment