教程:模糊測試入門

本教程介紹了 Go 語言中模糊測試的基礎知識。透過模糊測試,隨機資料被用於您的測試,以試圖發現漏洞或導致程式崩潰的輸入。模糊測試可以發現的一些漏洞示例包括 SQL 注入、緩衝區溢位、拒絕服務和跨站指令碼攻擊。

在本教程中,您將為一個簡單函式編寫模糊測試,執行 go 命令,並除錯和修復程式碼中的問題。

如需瞭解本教程中的術語,請參閱 Go 模糊測試術語表

您將按以下章節進行

  1. 為您的程式碼建立一個資料夾。
  2. 新增要測試的程式碼。
  3. 新增單元測試。
  4. 新增模糊測試。
  5. 修復兩個 bug。
  6. 探索其他資源。

注意:有關其他教程,請參閱教程

注意:Go 模糊測試目前支援一部分內建型別,具體列表請參見Go 模糊測試文件,未來將增加更多內建型別的支援。

先決條件

為您的程式碼建立一個資料夾

首先,為您將編寫的程式碼建立一個資料夾。

  1. 開啟命令列提示符並切換到您的主目錄。

    在 Linux 或 Mac 上

    $ cd
    

    在 Windows 上

    C:\> cd %HOMEPATH%
    

    本教程的其餘部分將使用 $ 作為提示符。您使用的命令在 Windows 上也同樣有效。

  2. 在命令列提示符下,為您的程式碼建立一個名為 fuzz 的目錄。

    $ mkdir fuzz
    $ cd fuzz
    
  3. 建立一個模組來存放您的程式碼。

    執行 go mod init 命令,並指定新程式碼的模組路徑。

    $ go mod init example/fuzz
    go: creating new go.mod: module example/fuzz
    

    注意:對於生產程式碼,您應該指定一個更符合您自身需求的模組路徑。更多資訊請參閱管理依賴項

接下來,您將新增一些用於反轉字串的簡單程式碼,我們稍後將對其進行模糊測試。

新增要測試的程式碼

在本步驟中,您將新增一個反轉字串的函式。

編寫程式碼

  1. 使用您的文字編輯器,在 fuzz 目錄中建立一個名為 main.go 的檔案。

  2. 在 main.go 檔案頂部,貼上以下包宣告。

    package main
    

    一個獨立的程式(與庫相對)總是屬於 main 包。

  3. 在包宣告下方,貼上以下函式宣告。

    func Reverse(s string) string {
        b := []byte(s)
        for i, j := 0, len(b)-1; i < len(b)/2; i, j = i+1, j-1 {
            b[i], b[j] = b[j], b[i]
        }
        return string(b)
    }
    

    此函式將接受一個 string,每次迴圈處理一個 byte,並在最後返回反轉後的字串。

    注意:此程式碼基於 golang.org/x/example 中的 stringutil.Reverse 函式。

  4. 在 main.go 檔案頂部,包宣告下方,貼上以下 main 函式,用於初始化一個字串,反轉它,列印輸出,並重復此過程。

    func main() {
        input := "The quick brown fox jumped over the lazy dog"
        rev := Reverse(input)
        doubleRev := Reverse(rev)
        fmt.Printf("original: %q\n", input)
        fmt.Printf("reversed: %q\n", rev)
        fmt.Printf("reversed again: %q\n", doubleRev)
    }
    

    此函式將執行幾次 Reverse 操作,然後將輸出列印到命令列。這有助於檢視程式碼的實際執行情況,並可能有助於除錯。

  5. main 函式使用了 fmt 包,因此您需要匯入它。

    程式碼的開頭部分應如下所示

    package main
    
    import "fmt"
    

執行程式碼

在包含 main.go 的目錄的命令列中,執行程式碼。

$ go run .
original: "The quick brown fox jumped over the lazy dog"
reversed: "god yzal eht revo depmuj xof nworb kciuq ehT"
reversed again: "The quick brown fox jumped over the lazy dog"

您可以看到原始字串、第一次反轉的結果,然後是再次反轉的結果,它與原始字串相同。

程式碼執行後,現在該進行測試了。

新增單元測試

在本步驟中,您將為 Reverse 函式編寫一個基本單元測試。

編寫程式碼

  1. 使用您的文字編輯器,在 fuzz 目錄中建立一個名為 reverse_test.go 的檔案。

  2. 將以下程式碼貼上到 reverse_test.go 中。

    package main
    
    import (
        "testing"
    )
    
    func TestReverse(t *testing.T) {
        testcases := []struct {
            in, want string
        }{
            {"Hello, world", "dlrow ,olleH"},
            {" ", " "},
            {"!12345", "54321!"},
        }
        for _, tc := range testcases {
            rev := Reverse(tc.in)
            if rev != tc.want {
                    t.Errorf("Reverse: %q, want %q", rev, tc.want)
            }
        }
    }
    

    這個簡單的測試將斷言列出的輸入字串將被正確反轉。

執行程式碼

使用 go test 執行單元測試

$ go test
PASS
ok      example/fuzz  0.013s

接下來,您將把單元測試轉換為模糊測試。

新增模糊測試

單元測試存在侷限性,即每個輸入都必須由開發者新增到測試中。模糊測試的一個好處是它可以為您的程式碼生成輸入,並可能發現您手動設計的測試用例未能觸及的邊緣情況。

在本節中,您將把單元測試轉換為模糊測試,以便用更少的工作生成更多的輸入!

請注意,您可以將單元測試、基準測試和模糊測試放在同一個 *_test.go 檔案中,但在此示例中,您將把單元測試轉換為模糊測試。

編寫程式碼

在您的文字編輯器中,將 reverse_test.go 中的單元測試替換為以下模糊測試。

func FuzzReverse(f *testing.F) {
    testcases := []string{"Hello, world", " ", "!12345"}
    for _, tc := range testcases {
        f.Add(tc)  // Use f.Add to provide a seed corpus
    }
    f.Fuzz(func(t *testing.T, orig string) {
        rev := Reverse(orig)
        doubleRev := Reverse(rev)
        if orig != doubleRev {
            t.Errorf("Before: %q, after: %q", orig, doubleRev)
        }
        if utf8.ValidString(orig) && !utf8.ValidString(rev) {
            t.Errorf("Reverse produced invalid UTF-8 string %q", rev)
        }
    })
}

模糊測試也有一些侷限性。在您的單元測試中,您可以預測 Reverse 函式的預期輸出,並驗證實際輸出是否符合這些預期。

例如,在測試用例 Reverse("Hello, world") 中,單元測試指定返回值為 "dlrow ,olleH"

進行模糊測試時,您無法預測預期輸出,因為您無法控制輸入。

但是,在模糊測試中可以驗證 Reverse 函式的幾個屬性。此模糊測試中檢查的兩個屬性是

  1. 將字串反轉兩次會保留原始值
  2. 反轉後的字串保持其有效的 UTF-8 狀態。

請注意單元測試和模糊測試之間的語法差異

確保新的包 unicode/utf8 已被匯入。

package main

import (
    "testing"
    "unicode/utf8"
)

將單元測試轉換為模糊測試後,是時候再次執行測試了。

執行程式碼

  1. 在不進行模糊測試的情況下執行模糊測試,以確保種子輸入透過。

    $ go test
    PASS
    ok      example/fuzz  0.013s
    

    如果您在該檔案中有其他測試,並且只希望執行模糊測試,也可以執行 go test -run=FuzzReverse

  2. 執行帶模糊測試的 FuzzReverse,以檢視是否有任何隨機生成的字串輸入會導致失敗。這透過使用帶有新標誌 -fuzz(設定為引數 Fuzz)的 go test 執行。複製下面的命令。

    $ go test -fuzz=Fuzz
    

    另一個有用的標誌是 -fuzztime,它限制了模糊測試的執行時間。例如,在下面的測試中指定 -fuzztime 10s 意味著,只要之前沒有發生失敗,測試將在經過 10 秒後預設退出。請參閱 cmd/go 文件的此部分,以檢視其他測試標誌。

    現在,執行您剛剛複製的命令。

    $ go test -fuzz=Fuzz
    fuzz: elapsed: 0s, gathering baseline coverage: 0/3 completed
    fuzz: elapsed: 0s, gathering baseline coverage: 3/3 completed, now fuzzing with 8 workers
    fuzz: minimizing 38-byte failing input file...
    --- FAIL: FuzzReverse (0.01s)
        --- FAIL: FuzzReverse (0.00s)
            reverse_test.go:20: Reverse produced invalid UTF-8 string "\x9c\xdd"
    
        Failing input written to testdata/fuzz/FuzzReverse/af69258a12129d6cbba438df5d5f25ba0ec050461c116f777e77ea7c9a0d217a
        To re-run:
        go test -run=FuzzReverse/af69258a12129d6cbba438df5d5f25ba0ec050461c116f777e77ea7c9a0d217a
    FAIL
    exit status 1
    FAIL    example/fuzz  0.030s
    

    模糊測試時發生了失敗,導致問題的輸入被寫入一個種子語料庫檔案,該檔案將在下次呼叫 go test 時執行,即使沒有 -fuzz 標誌。要檢視導致失敗的輸入,請在文字編輯器中開啟寫入 testdata/fuzz/FuzzReverse 目錄的語料庫檔案。您的種子語料庫檔案可能包含不同的字串,但格式是相同的。

    go test fuzz v1
    string("泃")
    

    語料庫檔案的第一行指示編碼版本。後面的每一行表示構成語料庫條目的每種型別的值。由於模糊測試目標只接受 1 個輸入,因此版本之後只有 1 個值。

  3. 再次執行 go test,不帶 -fuzz 標誌;新的失敗種子語料庫條目將被使用

    $ go test
    --- FAIL: FuzzReverse (0.00s)
        --- FAIL: FuzzReverse/af69258a12129d6cbba438df5d5f25ba0ec050461c116f777e77ea7c9a0d217a (0.00s)
            reverse_test.go:20: Reverse produced invalid string
    FAIL
    exit status 1
    FAIL    example/fuzz  0.016s
    

    由於我們的測試失敗了,是時候進行除錯了。

修復無效字串錯誤

在本節中,您將除錯失敗並修復 bug。

在繼續之前,您可以花一些時間思考並嘗試自己解決問題。

診斷錯誤

有幾種不同的方法可以除錯此錯誤。如果您使用 VS Code 作為文字編輯器,可以設定偵錯程式進行調查。

在本教程中,我們將有用的除錯資訊記錄到您的終端。

首先,考慮 utf8.ValidString 的文件。

ValidString reports whether s consists entirely of valid UTF-8-encoded runes.

當前的 Reverse 函式是按位元組反轉字串的,問題就出在這裡。為了保留原始字串的 UTF-8 編碼的 rune,我們必須改為按 rune 反轉字串。

要檢查為什麼輸入(在本例中為漢字 )導致 Reverse 在反轉時產生無效字串,您可以檢查反轉字串中的 rune 數量。

編寫程式碼

在您的文字編輯器中,將 FuzzReverse 中的模糊測試目標替換為以下程式碼。

f.Fuzz(func(t *testing.T, orig string) {
    rev := Reverse(orig)
    doubleRev := Reverse(rev)
    t.Logf("Number of runes: orig=%d, rev=%d, doubleRev=%d", utf8.RuneCountInString(orig), utf8.RuneCountInString(rev), utf8.RuneCountInString(doubleRev))
    if orig != doubleRev {
        t.Errorf("Before: %q, after: %q", orig, doubleRev)
    }
    if utf8.ValidString(orig) && !utf8.ValidString(rev) {
        t.Errorf("Reverse produced invalid UTF-8 string %q", rev)
    }
})

如果發生錯誤,或者使用 -v 執行測試時,此 t.Logf 行將列印到命令列,這有助於您除錯此特定問題。

執行程式碼

使用 go test 執行測試

$ go test
--- FAIL: FuzzReverse (0.00s)
    --- FAIL: FuzzReverse/28f36ef487f23e6c7a81ebdaa9feffe2f2b02b4cddaa6252e87f69863046a5e0 (0.00s)
        reverse_test.go:16: Number of runes: orig=1, rev=3, doubleRev=1
        reverse_test.go:21: Reverse produced invalid UTF-8 string "\x83\xb3\xe6"
FAIL
exit status 1
FAIL    example/fuzz    0.598s

整個種子語料庫使用的字串中的每個字元都是單位元組的。然而,像“泃”這樣的字元可能需要多個位元組。因此,按位元組反轉字串會使多位元組字元失效。

注意:如果您對 Go 如何處理字串感到好奇,請閱讀部落格文章Go 中的字串、位元組、rune 和字元以獲得更深入的瞭解。

更好地理解這個 bug 後,修正 Reverse 函式中的錯誤。

修復錯誤

為了修正 Reverse 函式,讓我們按 rune(而不是按位元組)遍歷字串。

編寫程式碼

在您的文字編輯器中,將現有的 Reverse() 函式替換為以下程式碼。

func Reverse(s string) string {
    r := []rune(s)
    for i, j := 0, len(r)-1; i < len(r)/2; i, j = i+1, j-1 {
        r[i], r[j] = r[j], r[i]
    }
    return string(r)
}

關鍵區別在於 Reverse 現在是遍歷字串中的每個 rune,而不是每個 byte。請注意,這只是一個示例,並且沒有正確處理組合字元

執行程式碼

  1. 使用 go test 執行測試

    $ go test
    PASS
    ok      example/fuzz  0.016s
    

    測試現在通過了!

  2. 再次使用 go test -fuzz 進行模糊測試,看看是否有新的 bug。

    $ go test -fuzz=Fuzz
    fuzz: elapsed: 0s, gathering baseline coverage: 0/37 completed
    fuzz: minimizing 506-byte failing input file...
    fuzz: elapsed: 0s, gathering baseline coverage: 5/37 completed
    --- FAIL: FuzzReverse (0.02s)
        --- FAIL: FuzzReverse (0.00s)
            reverse_test.go:33: Before: "\x91", after: "�"
    
        Failing input written to testdata/fuzz/FuzzReverse/1ffc28f7538e29d79fce69fef20ce5ea72648529a9ca10bea392bcff28cd015c
        To re-run:
        go test -run=FuzzReverse/1ffc28f7538e29d79fce69fef20ce5ea72648529a9ca10bea392bcff28cd015c
    FAIL
    exit status 1
    FAIL    example/fuzz  0.032s
    

    我們可以看到,字串在反轉兩次後與原始字串不同。這次的輸入本身是無效的 unicode。如果我們在用字串進行模糊測試,這怎麼可能呢?

    讓我們再次進行除錯。

修復兩次反轉錯誤

在本節中,您將除錯兩次反轉失敗並修復 bug。

在繼續之前,您可以花一些時間思考並嘗試自己解決問題。

診斷錯誤

和之前一樣,有幾種方法可以除錯此失敗。在這種情況下,使用偵錯程式將是一個很好的方法。

在本教程中,我們將在 Reverse 函式中記錄有用的除錯資訊。

仔細檢視反轉後的字串以發現錯誤。在 Go 中,字串是一個只讀的位元組切片,可以包含不是有效 UTF-8 的位元組。原始字串是一個包含一個位元組 '\x91' 的位元組切片。當輸入字串被設定為 []rune 時,Go 會將位元組切片編碼為 UTF-8,並將該位元組替換為 UTF-8 字元 �。當我們比較替換後的 UTF-8 字元與輸入位元組切片時,它們顯然不相等。

編寫程式碼

  1. 在您的文字編輯器中,將 Reverse 函式替換為以下程式碼。

    func Reverse(s string) string {
        fmt.Printf("input: %q\n", s)
        r := []rune(s)
        fmt.Printf("runes: %q\n", r)
        for i, j := 0, len(r)-1; i < len(r)/2; i, j = i+1, j-1 {
            r[i], r[j] = r[j], r[i]
        }
        return string(r)
    }
    

    這將幫助我們理解將字串轉換為 rune 切片時出了什麼問題。

執行程式碼

這次,我們只希望執行失敗的測試以檢查日誌。為此,我們將使用 go test -run

要在 FuzzXxx/testdata 中執行特定的語料庫條目,可以將 {FuzzTestName}/{filename} 提供給 -run。這在除錯時會很有幫助。在此示例中,將 -run 標誌設定為失敗測試的精確雜湊值。從您的終端複製並貼上唯一的雜湊值;它將與下方顯示的雜湊值不同。

$ go test -run=FuzzReverse/28f36ef487f23e6c7a81ebdaa9feffe2f2b02b4cddaa6252e87f69863046a5e0
input: "\x91"
runes: ['�']
input: "�"
runes: ['�']
--- FAIL: FuzzReverse (0.00s)
    --- FAIL: FuzzReverse/28f36ef487f23e6c7a81ebdaa9feffe2f2b02b4cddaa6252e87f69863046a5e0 (0.00s)
        reverse_test.go:16: Number of runes: orig=1, rev=1, doubleRev=1
        reverse_test.go:18: Before: "\x91", after: "�"
FAIL
exit status 1
FAIL    example/fuzz    0.145s

知道了輸入是無效的 unicode,讓我們修復 Reverse 函式中的錯誤。

修復錯誤

要解決此問題,如果 Reverse 的輸入不是有效的 UTF-8,我們應該返回一個錯誤。

編寫程式碼

  1. 在您的文字編輯器中,將現有的 Reverse 函式替換為以下程式碼。

    func Reverse(s string) (string, error) {
        if !utf8.ValidString(s) {
            return s, errors.New("input is not valid UTF-8")
        }
        r := []rune(s)
        for i, j := 0, len(r)-1; i < len(r)/2; i, j = i+1, j-1 {
            r[i], r[j] = r[j], r[i]
        }
        return string(r), nil
    }
    

    此更改將在輸入字串包含無效 UTF-8 字元時返回錯誤。

  2. 由於 Reverse 函式現在返回一個錯誤,修改 main 函式以丟棄額外的錯誤值。將現有的 main 函式替換為以下程式碼。

    func main() {
        input := "The quick brown fox jumped over the lazy dog"
        rev, revErr := Reverse(input)
        doubleRev, doubleRevErr := Reverse(rev)
        fmt.Printf("original: %q\n", input)
        fmt.Printf("reversed: %q, err: %v\n", rev, revErr)
        fmt.Printf("reversed again: %q, err: %v\n", doubleRev, doubleRevErr)
    }
    

    這些對 Reverse 的呼叫應該返回 nil 錯誤,因為輸入字串是有效的 UTF-8。

  3. 您需要匯入 errors 和 unicode/utf8 包。main.go 中的 import 語句應如下所示。

    import (
        "errors"
        "fmt"
        "unicode/utf8"
    )
    
  4. 修改 reverse_test.go 檔案以檢查錯誤,並在生成錯誤時透過返回來跳過測試。

    func FuzzReverse(f *testing.F) {
        testcases := []string {"Hello, world", " ", "!12345"}
        for _, tc := range testcases {
            f.Add(tc)  // Use f.Add to provide a seed corpus
        }
        f.Fuzz(func(t *testing.T, orig string) {
            rev, err1 := Reverse(orig)
            if err1 != nil {
                return
            }
            doubleRev, err2 := Reverse(rev)
            if err2 != nil {
                 return
            }
            if orig != doubleRev {
                t.Errorf("Before: %q, after: %q", orig, doubleRev)
            }
            if utf8.ValidString(orig) && !utf8.ValidString(rev) {
                t.Errorf("Reverse produced invalid UTF-8 string %q", rev)
            }
        })
    }
    

    除了返回,您還可以呼叫 t.Skip() 來停止該模糊輸入的執行。

執行程式碼

  1. 使用 go test 執行測試

    $ go test
    PASS
    ok      example/fuzz  0.019s
    
  2. 使用 go test -fuzz=Fuzz 進行模糊測試,然後等待幾秒鐘後,按 ctrl-C 停止模糊測試。除非您傳遞 -fuzztime 標誌,否則模糊測試將一直執行直到遇到失敗輸入。預設情況下,如果未發生失敗,它將永遠執行,可以透過 ctrl-C 中斷程序。

$ go test -fuzz=Fuzz
fuzz: elapsed: 0s, gathering baseline coverage: 0/38 completed
fuzz: elapsed: 0s, gathering baseline coverage: 38/38 completed, now fuzzing with 4 workers
fuzz: elapsed: 3s, execs: 86342 (28778/sec), new interesting: 2 (total: 35)
fuzz: elapsed: 6s, execs: 193490 (35714/sec), new interesting: 4 (total: 37)
fuzz: elapsed: 9s, execs: 304390 (36961/sec), new interesting: 4 (total: 37)
...
fuzz: elapsed: 3m45s, execs: 7246222 (32357/sec), new interesting: 8 (total: 41)
^Cfuzz: elapsed: 3m48s, execs: 7335316 (31648/sec), new interesting: 8 (total: 41)
PASS
ok      example/fuzz  228.000s
  1. 使用 go test -fuzz=Fuzz -fuzztime 30s 進行模糊測試,如果在 30 秒內沒有找到失敗,則退出。

    $ go test -fuzz=Fuzz -fuzztime 30s
    fuzz: elapsed: 0s, gathering baseline coverage: 0/5 completed
    fuzz: elapsed: 0s, gathering baseline coverage: 5/5 completed, now fuzzing with 4 workers
    fuzz: elapsed: 3s, execs: 80290 (26763/sec), new interesting: 12 (total: 12)
    fuzz: elapsed: 6s, execs: 210803 (43501/sec), new interesting: 14 (total: 14)
    fuzz: elapsed: 9s, execs: 292882 (27360/sec), new interesting: 14 (total: 14)
    fuzz: elapsed: 12s, execs: 371872 (26329/sec), new interesting: 14 (total: 14)
    fuzz: elapsed: 15s, execs: 517169 (48433/sec), new interesting: 15 (total: 15)
    fuzz: elapsed: 18s, execs: 663276 (48699/sec), new interesting: 15 (total: 15)
    fuzz: elapsed: 21s, execs: 771698 (36143/sec), new interesting: 15 (total: 15)
    fuzz: elapsed: 24s, execs: 924768 (50990/sec), new interesting: 16 (total: 16)
    fuzz: elapsed: 27s, execs: 1082025 (52427/sec), new interesting: 17 (total: 17)
    fuzz: elapsed: 30s, execs: 1172817 (30281/sec), new interesting: 17 (total: 17)
    fuzz: elapsed: 31s, execs: 1172817 (0/sec), new interesting: 17 (total: 17)
    PASS
    ok      example/fuzz  31.025s
    

    模糊測試透過!

    除了 -fuzz 標誌之外,go test 還添加了幾個新標誌,可以在文件中檢視。

    有關模糊測試輸出中使用的術語的更多資訊,請參閱Go 模糊測試。例如,“new interesting” 指的是能夠擴充套件現有模糊測試語料庫程式碼覆蓋率的輸入。“new interesting” 輸入的數量預計會在模糊測試開始時急劇增加,在發現新程式碼路徑時出現幾次高峰,然後隨著時間的推移而逐漸減少。

結論

幹得好!您剛剛入門了 Go 語言的模糊測試。

下一步是選擇程式碼中您想進行模糊測試的函式,然後試試看!如果模糊測試在您的程式碼中發現了 bug,考慮將其新增到榮譽榜

如果您遇到任何問題或有功能建議,請提交問題

對於關於此功能的討論和一般反饋,您也可以在 Gophers Slack 的#fuzzing 頻道中參與。

請查閱go.dev/security/fuzz 上的文件以供進一步閱讀。

完整程式碼

— main.go —

package main

import (
    "errors"
    "fmt"
    "unicode/utf8"
)

func main() {
    input := "The quick brown fox jumped over the lazy dog"
    rev, revErr := Reverse(input)
    doubleRev, doubleRevErr := Reverse(rev)
    fmt.Printf("original: %q\n", input)
    fmt.Printf("reversed: %q, err: %v\n", rev, revErr)
    fmt.Printf("reversed again: %q, err: %v\n", doubleRev, doubleRevErr)
}

func Reverse(s string) (string, error) {
    if !utf8.ValidString(s) {
        return s, errors.New("input is not valid UTF-8")
    }
    r := []rune(s)
    for i, j := 0, len(r)-1; i < len(r)/2; i, j = i+1, j-1 {
        r[i], r[j] = r[j], r[i]
    }
    return string(r), nil
}

— reverse_test.go —

package main

import (
    "testing"
    "unicode/utf8"
)

func FuzzReverse(f *testing.F) {
    testcases := []string{"Hello, world", " ", "!12345"}
    for _, tc := range testcases {
        f.Add(tc) // Use f.Add to provide a seed corpus
    }
    f.Fuzz(func(t *testing.T, orig string) {
        rev, err1 := Reverse(orig)
        if err1 != nil {
            return
        }
        doubleRev, err2 := Reverse(rev)
        if err2 != nil {
            return
        }
        if orig != doubleRev {
            t.Errorf("Before: %q, after: %q", orig, doubleRev)
        }
        if utf8.ValidString(orig) && !utf8.ValidString(rev) {
            t.Errorf("Reverse produced invalid UTF-8 string %q", rev)
        }
    })
}

返回頂部