Registry / RepositoryTypeScriptRustPython
Rollpie
ReadmeFiles
Versions
Info
Download
0.1.098.9 KB2026-09-14
Version
0.1.0
Copyright
Rollpie, Irohabook
Publisher
math
Published
2026-09-14
Size
98.9 KB
Downloads
1
Checksum
bdac84a1e4fe4dd3505b93c2781b1f15833b5189c2e4ee976225fa3c512885cc
Dependencies
None

doc.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
//! 旧形式の文書 (MS-DOC)
//!
//! .doc は複合ファイルの中の WordDocument ストリームである。
//! 先頭に FIB という目録があり、そこから「表ストリーム」(0Table か 1Table) の在り処が分かる。
//!
//! # 本文はひと続きに置かれていない
//!
//! Word は編集のたびに本文を末尾へ書き足し、どこからどこまでが何文字目にあたるかを
//! ピーステーブルという表に持つ。だから本文を読むには、この表の順にたどって継ぎ合わせる。
//! 断片ごとに、1 バイト文字で入っているか 2 バイト文字で入っているかも変わる。
//!
//! # 何を返すか
//!
//! 段落ごとの文字を返す。太字や大きさは、別の表に符号として入っていて、ここでは読まない。
//! 表のセルは 1 つずつ段落として出てくる。行や列のまとまりは戻せない。

use crate::error::Error;


/// FIB の印
const IDENT: u16 = 0xa5ec;

/// Word 97 の FIB の版。これより前は作りが違う
const OLDEST: u16 = 193;

/// パスワードが掛かっている印
const ENCRYPTED: u16 = 0x0100;

/// 表ストリームがどちらかを表す印
const SECOND_TABLE: u16 = 0x0200;

/// 本文の文字数がある位置
const TEXT_COUNT: usize = 76;

/// ピーステーブルの在り処がある位置
const CLX: usize = 418;


/// 表ストリームの名前を、FIB から決める
pub fn table_name(main: &[u8]) -> Result<&'static str, Error> {
    check(main)?;

    if short(main, 10)? & SECOND_TABLE != 0 {
        Ok("1Table")
    } else {
        Ok("0Table")
    }
}


/// 本文を段落ごとに取り出す
pub fn read(main: &[u8], table: &[u8]) -> Result<Vec<String>, Error> {
    check(main)?;
    let count = long(main, TEXT_COUNT)? as usize;
    let start = long(main, CLX)? as usize;
    let length = long(main, CLX + 4)? as usize;

    if length == 0 || start + length > table.len() {
        return Err(Error::new("Word のピーステーブルが見つかりません"));
    }

    let piece = find_piece(&table[start..start + length])?;
    let text = gather(main, &piece, count)?;
    Ok(split(&text))
}


/// ピーステーブル 1 つ分
struct Piece {
    /// 何文字目から
    from: usize,
    /// 何文字目まで
    to: usize,
    /// WordDocument ストリームでの位置
    offset: usize,
    /// 1 バイト文字で入っているか
    narrow: bool,
}


/// Clx をたどってピーステーブルを取り出す
fn find_piece(clx: &[u8]) -> Result<Vec<Piece>, Error> {
    let mut cursor = 0;

    while cursor < clx.len() {
        match clx[cursor] {
            // 書式のかたまり。飛ばす
            0x01 => {
                let count = short(clx, cursor + 1)? as usize;
                cursor += 3 + count;
            }

            0x02 => {
                let length = long(clx, cursor + 1)? as usize;
                let from = cursor + 5;

                if from + length > clx.len() || length < 16 {
                    return Err(Error::new("Word のピーステーブルが途中で終わっています"));
                }

                return read_piece(&clx[from..from + length]);
            }

            _ => return Err(Error::new("Word のピーステーブルに知らない印があります")),
        }
    }

    Err(Error::new("Word のピーステーブルが見つかりません"))
}


/// ピーステーブルの中身を読む
///
/// 先頭に文字位置が 1 つ多く並び、そのあとに断片の記述が同じ数だけ続く。
fn read_piece(plc: &[u8]) -> Result<Vec<Piece>, Error> {
    let count = (plc.len() - 4) / 12;
    let mut result = Vec::with_capacity(count);

    for index in 0..count {
        let from = long(plc, index * 4)? as usize;
        let to = long(plc, index * 4 + 4)? as usize;
        let at = 4 * (count + 1) + index * 8;
        let value = long(plc, at + 2)?;

        // 30 ビット目が立っていれば 1 バイト文字で、位置は半分になっている
        let narrow = value & 0x40000000 != 0;
        let raw = (value & 0x3fffffff) as usize;
        let offset = if narrow {raw / 2} else {raw};

        result.push(Piece {from, to, offset, narrow});
    }

    Ok(result)
}


/// 断片をたどって本文をつなぐ
fn gather(main: &[u8], piece: &[Piece], count: usize) -> Result<Vec<char>, Error> {
    let mut result = Vec::with_capacity(count);

    for one in piece {
        if result.len() >= count {
            break;
        }

        let length = one.to.saturating_sub(one.from);

        if one.narrow {
            let stop = (one.offset + length).min(main.len());

            for index in one.offset..stop {
                result.push(narrow_char(main[index]));
            }

            continue;
        }

        // 2 バイト文字。BMP の外は 2 つで 1 文字になるので、まとめて直す
        let stop = (one.offset + length * 2).min(main.len());
        let mut unit = Vec::with_capacity(length);

        for index in (one.offset..stop).step_by(2) {
            if index + 2 > main.len() {
                break;
            }

            unit.push(u16::from_le_bytes([main[index], main[index + 1]]));
        }

        for value in char::decode_utf16(unit) {
            result.push(value.unwrap_or('\u{fffd}'));
        }
    }

    result.truncate(count);
    Ok(result)
}


/// 本文を段落に切る
///
/// 0x0D が段落の終わり、0x07 がセルの終わりである。
/// 0x13 から 0x14 までは差し込みの指示なので落とし、0x14 から 0x15 までの結果は残す。
fn split(text: &[char]) -> Vec<String> {
    let mut result = Vec::new();
    let mut line = String::new();
    let mut skipping = false;

    for value in text {
        let code = *value as u32;

        if skipping {
            if code == 0x14 || code == 0x15 {
                skipping = false;
            }

            continue;
        }

        match code {
            0x0d | 0x07 | 0x0c => {
                result.push(std::mem::take(&mut line));
            }

            0x13 => skipping = true,
            0x14 | 0x15 => {}
            0x0b => line.push('\n'),
            0x09 => line.push('\t'),
            0x1e => line.push('-'),
            0x1f => {}
            _ if code < 0x20 => {}
            _ => line.push(*value),
        }
    }

    if !line.is_empty() {
        result.push(line);
    }

    result
}


/// FIB を見て、読める形かどうか確かめる
fn check(main: &[u8]) -> Result<(), Error> {
    if short(main, 0)? != IDENT {
        return Err(Error::new("Word の FIB の印が見つかりません"));
    }

    let version = short(main, 2)?;

    if version < OLDEST {
        return Err(Error::new(format!(
            "Word 97 より前の形式です (FIB の版 {version})。文字の符号が戻せないので読めません"
        )));
    }

    if short(main, 10)? & ENCRYPTED != 0 {
        return Err(Error::new("パスワードが掛かっているので読めません"));
    }

    Ok(())
}


/// 1 バイト文字を Unicode に直す
///
/// 0x80 から 0x9F までは Windows の符号だけにある字なので、対応を持つ。
fn narrow_char(value: u8) -> char {
    const UPPER: [u16; 32] = [
        0x20ac, 0x0081, 0x201a, 0x0192, 0x201e, 0x2026, 0x2020, 0x2021, 0x02c6, 0x2030, 0x0160,
        0x2039, 0x0152, 0x008d, 0x017d, 0x008f, 0x0090, 0x2018, 0x2019, 0x201c, 0x201d, 0x2022,
        0x2013, 0x2014, 0x02dc, 0x2122, 0x0161, 0x203a, 0x0153, 0x009d, 0x017e, 0x0178,
    ];

    if (0x80..0xa0).contains(&value) {
        return char::from_u32(UPPER[value as usize - 0x80] as u32).unwrap_or('\u{fffd}');
    }

    value as char
}


/// 2 バイトを読む
fn short(data: &[u8], offset: usize) -> Result<u16, Error> {
    if offset + 2 > data.len() {
        return Err(Error::new("Word の記録が途中で終わっています"));
    }

    Ok(u16::from_le_bytes([data[offset], data[offset + 1]]))
}


/// 4 バイトを読む
fn long(data: &[u8], offset: usize) -> Result<u32, Error> {
    if offset + 4 > data.len() {
        return Err(Error::new("Word の記録が途中で終わっています"));
    }

    Ok(u32::from_le_bytes([
        data[offset],
        data[offset + 1],
        data[offset + 2],
        data[offset + 3],
    ]))
}