2014年8月31日 星期日

ph: python HTML generator without using any template syntax

偶而需要用 python 產生 HTML 報表, 自己手刻 HTML 頗麻煩的。寫個小 script 不會像寫網頁那頁使用 MVC 將資料和顯示分離, 不方便套 template (況且學 template 的語法也有點麻煩)。

找了一下發現 pyh, 語法滿直覺的, 大致符合我的需求。於是自己重做了一個 ph, 改成更合自己習慣的語法, 順便練習自己設計一套簡單的 DSL, 寫起來像這樣:

from ph import *
doc = HTML()
doc.body() << p('Hello, world!') << (p('What a ') << strong('wonderful') << ' world!')
print unicode(doc)
輸出像這樣 (有手動排版過):
<!DOCTYPE html>
<html>
<head><title>No Title</title></head>
<body>
<p>Hello, world!</p>
<p>What a <strong>wonderful</strong> world!</p>
</body>
</html>

以前沒想過覆寫 operator, 覺得這種作法容易讓人困惑。但是以 HTML generator 的例子來說, 用 << 組合 HTML 元素挺方便的。

2014年8月5日 星期二

在 vim 內 indent JSON

太容易忘了,乾脆寫篇文章加強記憶:

:%!python -m json.tool

是的,其實就是呼叫 python module 來處理

2014年7月23日 星期三

wmic: Windows Management Instrumentation Command-line

Windows 方面的技能毫無長進, 這就是典型的同樣的事做個十年, 沒有學習的話也不會有所長進...。最近重灌電腦才發覺 wmic 這個好東西, 可以取得硬體資訊 (如主機版型號), 管理 process 等。搜尋 "wmic tips" 會看到很多介紹它的好處和用法。寫這篇廢文只是想降低我忘掉 wmic 的機率...。

Wikipedia 有相關介紹, 其中有幾個關鍵字等要用的時候再深入研究一下, 像是 CIM, DMI, SNMP, 概念是對硬體裝置定好統一的 API, 方便本機或網路查詢、更新設定。

找出 dangle pointer 的方法

有時候 C++ 程式會莫明地 crash 在呼叫 method 的時候, 像是 obj->method()。檢查 obj 的值有幾種情況:

  • NULL: 一看就知道有問題
  • 某種規律的數字, 像是 0x23232323, 0xCDCDCDCD: 99% 有問題, 還沒猜錯過
  • 特定的位置, 如 0xDEADBEEF: 看了也知道有問題, 這是 debugger 特意覆寫的值
  • 看起來像個正常的指標, 可能是 dangle pointer

需要注意的是, 透過 dangle pointer 呼叫 method, 不一定會 crash。不過呼叫 virtual method 時一定會 crash, 因為 destructor 會重置 virtual method table 內的值 (相關文章見《C++ 執行後掛在 __cxa_pure_virtual》)。

以下是一個測試例子:

$ cat a.cpp
#include 

class Rect {
public:
    Rect(int x, int y, int w, int h)
        : x(x), y(y), w(w), h(h) {}

    int area() const { return w * h; };
    virtual int area2() const { return w * h; };

private:
    int x, y, w, h;
};

int main(void) {
    int w, h;
    scanf("%d%d", &w, &h);
    Rect *r = new Rect(10, 20, w, h);
    printf("area: %d\n", r->area());
    printf("area2: %d\n", r->area2());
    delete r;
    printf("area: %d\n", r->area());
    printf("area2: %d\n", r->area2());
    return 0;
}
$ ./a
30 40
area: 1200
area2: 1200
area: 1200
Segmentation fault (core dumped)

以前我判斷 dangle pointer 的小撇步是印出 pointer 指向的 object 的某些 member field, 若出現很怪的數字, 十之八九是 dangle pointer。不過有時還是會出包, 像上面的例子就是反例。看來用 virtual method 一定會 crash 這點做判斷, 會更準確。

在不改 code 的前提下, 可以設中斷點在懷疑是 dangle pointer 的 object 的 destructor, 再重執行程式。但若有大量同 class 的 object, 在 destructor log object 的記憶體位置會比較方便。此外, Scott 提到可以用 glibc MALLOC_PERTURB_, 看起來滿不錯的。不過我試了一下覺得不太管用, 沒有深入研究。

若使用的 gcc 比較新 (>=4.8, Ubuntu 14.04 有支援) 或用 llvm 的話, 可以考慮用 AddressSanitizer, 重點是執行效率比以往的替代工具快上不少。

2014-07-23 更新

Thinker 提到對於有 virtual method 的 object, 多數平台可以藉由印出 vptr 的值來確認是否已變成 dangle pointer。下面是用這個概念檢查 dangle pointer 的例子:

$ gdb a
...
(gdb) b 22  # break on "delete r"
Breakpoint 1 at 0x400746: file a.cpp, line 22.
(gdb) r
Starting program: /home/fcamel/dev/tmp/a
30 40
area: 1200
area2: 1200

Breakpoint 1, main () at a.cpp:22
22          delete r;
(gdb) p (void**)r[0]
$1 = (void **) 0x400950
(gdb) n
23          printf("area: %d\n", r->area());
(gdb) p (void**)r[0]
$2 = (void **) 0x0
(gdb) p *r
$3 = {_vptr.Rect = 0x0, x = 10, y = 20, w = 30, h = 40}

雖然 vptr (pointer to Virtual Method Table) 在 object 內的位置依實作而定, 在自己的平台實驗一下, 就可以用這招配合 debugger 除錯了。

2014年7月6日 星期日

用 regexp 的 lookahead 尋找符合 pattern A 但不符合 pattern B 的字串

偶而會需要找內含字串 A 但不含字串 B 的字串, 若剛好得用 regexp 表示的話, 會有點麻煩 (像是用 Android logcat filter 的時候)。查了一下, 發現 regular expression 有個強大的語法叫作 lookaround, 用它可相對容易地達到此需求, 還可以應付各種情況。詳見 Regex Lookarounds: Lookahead and Lookbehind 的介紹。關鍵在於 lookaround 的語法只是從「目前的位置」往前或往後「看看是否符合目標 pattern」,不會實際占去符合的字串。看文件的例子會比較好理解。

以下是用 Python RE 比對「內含 abc 但 abc 之後不含 def 的字串」:

In [70]: re.search('(?!.*def)abc', 'abcdef')

(?!.*def)abc 的意思是每一個位置都做以下的事:

  1. 先往後找看看有沒有符合 .*def, 找不到才算成立。(?!...) 的意思是 negative lookahead
  2. negative lookahead 成立後, 看看目前位置是否能找到 abc

反之, 下面這個寫法是錯的, 比對 abcdef 仍會有結果:

In [69]: re.search('(?!def)abc', 'abcdef')
Out[69]: <_sre.SRE_Match at 0x2f2fd98>

因為它的意思是在目前位置看看是否不符合 def, 於是一開始比對 abc 時就成立了, 然後再比對成功 abc, 於是回傳比對成功的結果。

(?!.*def)abc 看起來很美好, 但它無法避開 defabc, 也就是 abc 之前有 def 的情況。雖然有 lookbehind 的語法, 但它只能比對固定長度的 pattern, 無法應付 xxxdefxxxabc, 所以得換個方式表示。

regex - Regular expression to match string not containing a word? 說明如何用 regexp 表示不含目標字串的字串, 並有圖解說明運作的過程。了解之後, 可運用同樣的技巧表示「內含 abc 但 abc 之前不含 def 的字串」:

In [145]: re.search('^((?!def).)*abc', 'defabc')
  1. (?!def) 檢查目前位置是否不含 def
  2. (?!def). 注意多加了一個 '.', 表示檢查完後占去一個字元
  3. ((?!def).)* 比對 0 到多個符合 (?!def). 的字元

利用 regexp greedy 的特性, pattern 3 會盡可能占去符合這個的字元, 於是 ^((?!def.)*abc 就會比對出「內含 abc 但 abc 之前不含 def 的字串」。

再和一開始用的 lookahead 合在一起, 就能表示「內含 abc 但不含 def 的字串了」:

In [147]: re.search('^((?!def).)*(?!.*def)abc', 'abcdef')

In [148]: re.search('^((?!def).)*(?!.*def)abc', 'defabc')

In [149]: re.search('^((?!def).)*(?!.*def)abc', 'abc')
Out[149]: <_sre.SRE_Match at 0x2f2de40>

In [150]: re.search('^((?!def).)*(?!.*def)abc', 'xxxabcxdefxx')

In [151]: re.search('^((?!def).)*(?!.*def)abc', 'xdefxxabcxxx')

In [152]: 
就算一時之間無法消化也無所謂, 記得關鍵字 lookaround, lookahead, lookbehind, 之後比較方便找 regexp 進階用法。每次找 regexp 的說明, 都會學到新東西, 真是博大精深的表示法。

2014/07/07 更新

經 weiyu 提醒, 移動 abc 到中間效率會比較好, 以下是程式和測試結果:

$ cat a.py
import re
import time

begin = time.time()
pattern = re.compile('^((?!def).)*(?!.*def)abc')
for i in xrange(1000000):
    pattern.search('xxxxxxabcxxxxxx')
    pattern.search('xxxdefxxxabcxxxxxx')
    pattern.search('xxxxxxabcxxxdefxxx')
print time.time() - begin

begin = time.time()
pattern = re.compile('^((?!def).)*abc(?!.*def)')
for i in xrange(1000000):
    pattern.search('xxxxxxabcxxxxxx')
    pattern.search('xxxdefxxxabcxxxxxx')
    pattern.search('xxxxxxabcxxxdefxxx')
print time.time() - begin
$ python a.py
4.64261507988
3.08146381378

2014年5月23日 星期五

讓 git diff 顯示 utf-16 (或其它binary) 檔案的差異

兩種作法:

透過 difftool

unicode - Can I make git recognize a UTF-16 file as text? - Stack Overflow

$ git difftool commit1 commit2

我直接跑 difftool, 沒設定的情況會問我要不要用 vimdiff。

用 textconv

Textconv - Git SCM Wiki

這個作法比較好, 適用 git diff, show, blame。

比方說對於 iOS L10N 的檔案, 可以這麼設:

  1. 編輯 PROJECT/.git/info/attributes: 加入 *.strings diff=localizablestrings
  2. 編譯 /.gitconfig: 加入
 [diff "localizablestrings"]
 textconv = "iconv -f utf-16 -t utf-8"

2014年5月14日 星期三

使用 OAuth2 refresh token 的好處

初次看到 refresh token, 想說這東西真礙事, 原本 OAuth2 基本的流程已將使用者的密碼和授權分離, 看起來很完美了, 為什麼授權的 access token 之外, 還要多一個 refresh token? 看了 security - Why Does OAuth v2 Have Both Access and Refresh Tokens? 才明白 refresh token 更進一步提升安全性。

access token 解決了:

  • 使用者不需告訴應用程式密碼
  • 使用者可以細分授權項目
  • 使用者可隨時撤消授權

當 access token 外洩的時候, 只要使用者立即撤消授權, 就可以中止傷害, 相較於修改密碼方便許多 (一組密碼可能用在一到多個帳號上, 甚至可能忘了那些帳號使用同一密碼)。

但是為了減少使用者的認證的麻煩, 一個 access token 通常會授權使用很長一段時間 (比方說一個月)。使用者不容易知道什麼時候發出去的 access token 外洩了, 傷害期可能會太長。外洩的原因可能是使用 access token 取存服務時被竊聽破解, 或是在裝置端的應用程式 (PC、smartphone) 存放的 access token 直接外洩。

有沒有辦法可以兼顧減少使用者認證, 又降低 access token 外洩的傷害期呢? 答案是加上 refresh token。設定授權的 access token 在很短的時間內會過期 (如一小時), 然後 client(應用程式) 要用 refresh token + client id + client secret 取得新的 access token。

client 不需要透過使用者就可以取得新的 access token, 不會打擾到使用者。沒有 client secret 的情況下, 單是外洩 access token, 別人用一小段時間後, token 就過期了, 傷害時間很短。

不過要達到上面的好處, client 必須在不同地方存下 refresh token 和 client secret, 偷懶存在同一地方, 一但破台也是一樣。

security - Why Does OAuth v2 Have Both Access and Refresh Tokens? 還有人提供其它回答, 提到 refresh token 有機會減輕認證伺服器的負擔, 不知是意外的副作用, 或是設計之初就有考慮到。愈了解 OAuth2, 愈覺得制定 OAuth2 的人們考慮的真詳細, 提供很彈性的機制讓實作者可自行調整安全的程度、使用者操作的便利性以及伺服器負擔。

Btw, Google 提供 refresh token 的方式還有一點小撇步。使用預設的方式認證時, Google 只有第一次認證時才會傳回 refresh token, 之後重新認證不會再傳回 refresh token。除非使用者到 Google Account 的 Security 分頁取消對 app 的授權, 接著再認證才會再收到 refresh token。或是在認證 URL 的參數加上 approval_prompt=force。詳細的內容寫在 Using OAuth 2.0 for Web Server Applications - Google Accounts Authentication and Authorization — Google Developers:

Important: When your application receives a refresh token, it is important to store that refresh token for future use. If your application loses the refresh token, it will have to re-prompt the user for consent before obtaining another refresh token. If you need to re-prompt the user for consent, include the approval_prompt parameter in the authorization code request, and set the value to force.

在 Fedora 下裝 id-utils

Fedora 似乎因為執行檔撞名,而沒有提供 id-utils 的套件 ,但這是使用 gj 的必要套件,只好自己編。從官網抓好 tarball ,解開來編譯 (./configure && make)就是了。 但編譯後會遇到錯誤: ./stdio.h:10...