-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathOdfCodePointMappingTableTests.cs
More file actions
169 lines (147 loc) · 6.84 KB
/
Copy pathOdfCodePointMappingTableTests.cs
File metadata and controls
169 lines (147 loc) · 6.84 KB
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
using System;
using System.Collections.Generic;
using System.IO;
using OdfKit.Core;
using Xunit;
namespace OdfKit.Tests;
/// <summary>
/// 鎖定 OdfCodePointMappingTable 通用碼位對照表解析與聯結行為之單元測試。
/// </summary>
public class OdfCodePointMappingTableTests
{
/// <summary>
/// 驗證可直接解析 Unicode.org 官方對照檔格式(TAB 分隔、0x 前綴、# 註解)。
/// </summary>
[Fact]
public void ParseDelimitedHex_ParsesUnicodeOrgVendorMappingFormat()
{
// 取自 unicode.org Public/MAPPINGS 之 BIG5.TXT 實際格式樣本
const string sample =
"# BIG5.TXT vendor mapping sample\n" +
"#\n" +
"0xA140\t0x3000\t# IDEOGRAPHIC SPACE\n" +
"0xA141\t0xFF0C\t# FULLWIDTH COMMA\n" +
"\n" +
"0xA440\t0x4E00\t# CJK UNIFIED IDEOGRAPH\n";
IReadOnlyDictionary<int, int> mapping =
OdfCodePointMappingTable.ParseDelimitedHex(new StringReader(sample), '\t');
Assert.Equal(3, mapping.Count);
Assert.Equal(0x3000, mapping[0xA140]);
Assert.Equal(0xFF0C, mapping[0xA141]);
Assert.Equal(0x4E00, mapping[0xA440]);
}
/// <summary>
/// 驗證分號分隔的 UCD 式清單(欄位周圍空白、U+ 前綴、行尾註解)與重複鍵後者覆蓋。
/// </summary>
[Fact]
public void ParseDelimitedHex_ParsesSemicolonListWithPrefixesAndDuplicates()
{
const string sample =
"U+F0000 ; 20BB7 # PUA to Ext-B\n" +
"E000;4E00\n" +
"E000 ; 4E8C # duplicate key, last wins\n";
IReadOnlyDictionary<int, int> mapping =
OdfCodePointMappingTable.ParseDelimitedHex(new StringReader(sample), ';');
Assert.Equal(2, mapping.Count);
Assert.Equal(0x20BB7, mapping[0xF0000]);
Assert.Equal(0x4E8C, mapping[0xE000]);
}
/// <summary>
/// 驗證格式不符的資料行擲出 FormatException,null 讀取器擲出 ArgumentNullException。
/// </summary>
[Fact]
public void ParseDelimitedHex_ValidatesArguments()
{
Assert.Throws<ArgumentNullException>(
() => OdfCodePointMappingTable.ParseDelimitedHex(null!, '\t'));
Assert.Throws<FormatException>(
() => OdfCodePointMappingTable.ParseDelimitedHex(new StringReader("only-one-field"), '\t'));
Assert.Throws<FormatException>(
() => OdfCodePointMappingTable.ParseDelimitedHex(new StringReader("XYZ\t4E00"), '\t'));
Assert.Throws<FormatException>(
() => OdfCodePointMappingTable.ParseDelimitedHex(new StringReader("E000;;4E00"), ';'));
}
/// <summary>
/// 驗證委派式解析:空行略過、回傳 null 略過、自訂格式可解析。
/// </summary>
[Fact]
public void Parse_WithLineParserDelegate_SupportsCustomFormats()
{
const string sample =
"MJ000001,F0001,20BB7\n" +
"\n" +
"skip-this-line\n" +
"MJ000002,F0002,4E00\n";
IReadOnlyDictionary<int, int> mapping = OdfCodePointMappingTable.Parse(
new StringReader(sample),
line =>
{
string[] fields = line.Split(',');
if (fields.Length != 3)
{
return null;
}
return new KeyValuePair<int, int>(
Convert.ToInt32(fields[1], 16),
Convert.ToInt32(fields[2], 16));
});
Assert.Equal(2, mapping.Count);
Assert.Equal(0x20BB7, mapping[0xF0001]);
Assert.Equal(0x4E00, mapping[0xF0002]);
Assert.Throws<ArgumentNullException>(
() => OdfCodePointMappingTable.Parse(null!, static _ => null));
Assert.Throws<ArgumentNullException>(
() => OdfCodePointMappingTable.Parse(new StringReader(""), null!));
}
/// <summary>
/// 驗證資源預算防線:超長資料行、8 位十六進位溢為負值一律拒絕,例外訊息截斷原始行。
/// </summary>
[Fact]
public void ParseDelimitedHex_EnforcesResourceBudgetAndRejectsNegativeHex()
{
// 超過 4,096 字元的資料行:資源預算拒絕
string longLine = new string('A', 5_000) + "\t4E00";
Assert.Throws<FormatException>(
() => OdfCodePointMappingTable.ParseDelimitedHex(new StringReader(longLine), '\t'));
string boundaryCrLf = new string(' ', 1_023) + "\r\n4E00\t4E01";
IReadOnlyDictionary<int, int> boundaryResult = OdfCodePointMappingTable.ParseDelimitedHex(
new StringReader(boundaryCrLf),
'\t');
Assert.Equal(0x4E01, boundaryResult[0x4E00]);
// 8 位十六進位(FFFFFFFF)溢位為負值:視為無效輸入
Assert.Throws<FormatException>(
() => OdfCodePointMappingTable.ParseDelimitedHex(new StringReader("FFFFFFFF\t4E00"), '\t'));
Assert.Throws<FormatException>(
() => OdfCodePointMappingTable.ParseDelimitedHex(new StringReader("4E00\tFFFFFFFF"), '\t'));
// 例外訊息中的原始行須截斷並清洗控制字元;以 ANSI ESC 為例,
// 用 (char)0x1B 建構以避免原始碼內出現裸控制字元或跨層逸出問題。
char escapeChar = (char)0x1B;
string sanitized = OdfCodePointMappingTable.FormatLineForMessage(
"bad" + escapeChar + "[31mline" + new string('x', 200));
Assert.DoesNotContain(escapeChar, sanitized);
Assert.True(sanitized.Length <= 65, $"訊息行未截斷:{sanitized.Length}");
Assert.EndsWith("…", sanitized);
// 項目數預算檢查本體(直接驗證 internal 檢查點,避免建構兩百萬筆的慢測試)
OdfCodePointMappingTable.EnsureEntryBudget(OdfCodePointMappingTable.MaxEntryCount);
Assert.Throws<FormatException>(
() => OdfCodePointMappingTable.EnsureEntryBudget(OdfCodePointMappingTable.MaxEntryCount + 1));
}
/// <summary>
/// 驗證通用 Join 與 CNS 特化 JoinOnCns 的聯結結果一致。
/// </summary>
[Fact]
public void Join_MatchesJoinOnCnsSemantics()
{
var keyToSource = new Dictionary<string, int> { ["1-2121"] = 0x4E00, ["1-2122"] = 0x4E8C };
var keyToTarget = new Dictionary<string, int> { ["1-2121"] = 0x8E40, ["9-9999"] = 0x8E41 };
IReadOnlyDictionary<int, int> joined = OdfCodePointMappingTable.Join(keyToSource, keyToTarget);
IReadOnlyDictionary<int, int> cnsJoined = OdfCns11643MappingTable.JoinOnCns(keyToSource, keyToTarget);
Assert.Single(joined);
Assert.Equal(0x8E40, joined[0x4E00]);
Assert.Equal(joined, cnsJoined);
Assert.Throws<ArgumentNullException>(
() => OdfCodePointMappingTable.Join(null!, keyToTarget));
Assert.Throws<ArgumentNullException>(
() => OdfCodePointMappingTable.Join(keyToSource, null!));
}
}