輸入法詞庫解析(七)微軟使用者自定義短語.dat

2022-09-17 18:00:58

詳細程式碼:https://github.com/cxcn/dtool

前言

微軟拼音和微軟五筆通用的使用者自定義短語 dat 格式。

解析

前 8 個位元組標識檔案格式 machxudp,微軟五筆的 lex 格式是 imscwubi

下面 8 個位元組應該是版本號。

接下來每 4 位元組一組,分別表示偏移表開始詞條開始檔案總長詞條數匯出的時間戳

然後補 0 一直到偏移表開始

偏移表記錄了每個詞條從詞條開始的偏移量,每 4 個位元組一組。

接下來就是詞條本體部分:

# 佔用位元組數 描述
4 10 00 10 00 標記
a 2 該詞條總位元組長 - 詞佔用的位元組長
1 在候選中的位置
1 0x060x13,未知
4 0
4 2010-01-01開始的時間戳
a - 16 編碼(utf-16le),00 標識結束
詞條總位元組長 - a 詞(utf-16le),00 標識結束

程式碼實現:

func (MsUDP) Parse(filename string) Table {
    data, _ := os.ReadFile(filename)
    r := bytes.NewReader(data)
    ret := make(Table, 0, r.Len()>>8)

    // 詞庫偏移量
    r.Seek(0x10, 0)
    offset_start := ReadUint32(r) // 偏移表開始
    entry_start := ReadUint32(r)  // 詞條開始
    entry_end := ReadUint32(r)    // 詞條結束
    entry_count := ReadUint32(r)  // 詞條數
    export_time := ReadUint32(r)  // 匯出的時間
    t := time.Unix(int64(export_time), 0)
    fmt.Println(t, entry_end)

    // 第一個偏移量
    offset := 0
    for i := 0; i < entry_count; i++ {
        var next, length int
        if i == entry_count-1 {
            length = entry_end - entry_start - offset
        } else {
            r.Seek(int64(offset_start+4*(i+1)), 0)
            next = ReadUint32(r)
            length = next - offset
        }
        // fmt.Println(offset, next, length)

        r.Seek(int64(offset+entry_start), 0)
        offset = next
        ReadUint32(r)            // 0x10001000
        codeLen := ReadUint16(r) // 編碼位元組長+0x12
        order, _ := r.ReadByte() // 順序
        _, _ = r.ReadByte()      // 0x06 不明
        ReadUint32(r)            // 4 個空位元組
        ReadUint32(r)            // 時間戳
        tmp := make([]byte, codeLen-0x12)
        r.Read(tmp)
        code, _ := util.Decode(tmp, "UTF-16LE")
        ReadUint16(r) // 兩個空位元組
        tmp = make([]byte, length-codeLen-2)
        r.Read(tmp)
        word, _ := util.Decode(tmp, "UTF-16LE")
        fmt.Println(code, word)
        ret = append(ret, Entry{word, code, order})
    }
    return ret
}

生成

只需注意檔案總長先用空位元組代替,最後才寫入。

程式碼實現:

func (MsUDP) Gen(table Table) []byte {
    var buf bytes.Buffer
    stamp := util.GetUint32(int(time.Now().Unix()))
    buf.Write([]byte{0x6D, 0x73, 0x63, 0x68, 0x78, 0x75, 0x64, 0x70,
        0x02, 0x00, 0x60, 0x00, 0x01, 0x00, 0x00, 0x00})
    buf.Write(util.GetUint32(0x40))
    buf.Write(util.GetUint32(0x40 + 4*len(table)))
    buf.Write(make([]byte, 4)) // 待定 檔案總長
    buf.Write(util.GetUint32(len(table)))
    buf.Write(stamp)
    buf.Write(make([]byte, 28))
    buf.Write(make([]byte, 4))

    words := make([][]byte, 0, len(table))
    codes := make([][]byte, 0, len(table))
    sum := 0
    for i := range table {
        word, _ := util.Encode([]byte(table[i].Word), "UTF-16LE")
        code, _ := util.Encode([]byte(table[i].Code), "UTF-16LE")
        words = append(words, word)
        codes = append(codes, code)
        if i != len(table)-1 {
            sum += len(word) + len(code) + 20
            buf.Write(util.GetUint32(sum))
        }
    }
    for i := range table {
        buf.Write([]byte{0x10, 0x00, 0x10, 0x00})
        // fmt.Println(words[i], len(words[i]), codes[i], len(codes[i]))
        buf.Write(util.GetUint16(len(codes[i]) + 18))
        buf.WriteByte(table[i].Order)
        buf.WriteByte(0x06)
        buf.Write(make([]byte, 4))
        buf.Write(stamp)
        buf.Write(codes[i])
        buf.Write([]byte{0, 0})
        buf.Write(words[i])
        buf.Write([]byte{0, 0})
    }
    b := buf.Bytes()
    copy(b[0x18:0x1c], util.GetUint32(len(b)))
    return b
}