package dict
import (
"bytes"
"cmp"
"encoding/binary"
"errors"
"fmt"
"os"
"slices"
"unicode/utf8"
"chirami/internal/jp"
"golang.org/x/sys/unix"
)
// Ruby is one segmented ruby element: Text is the surface text (kanji or kana),
// and Ruby is the reading annotation (empty string for kana parts).
type Ruby struct {
Text string
Ruby string
}
// Dict is an open, memory-mapped Japanese dictionary.
type Dict struct {
file *os.File
mmap []byte
header *Header
interned []string
tags map[string]string
keyIndexBytes []byte
keyDataBytes []byte
postingBytes []byte
entryIndexBytes []byte
entryDataBytes []byte
kanjiIndexBytes []byte
kanjiDataBytes []byte
furiganaIndexBytes []byte
furiganaKeyBytes []byte
furiganaDataBytes []byte
kradIndexBytes []byte
kradDataBytes []byte
radkIndexBytes []byte
radkDataBytes []byte
kanjiWordsIndexBytes []byte
kanjiWordsDataBytes []byte
}
// Open memory-maps a dictionary file produced by build.Build.
func Open(path string) (*Dict, error) {
f, err := os.Open(path)
if err != nil {
return nil, err
}
st, err := f.Stat()
if err != nil {
f.Close()
return nil, err
}
if st.Size() < 16 {
f.Close()
return nil, fmt.Errorf("%w: file size %d smaller than header", ErrBufferTooSmall, st.Size())
}
data, err := unix.Mmap(int(f.Fd()), 0, int(st.Size()), unix.PROT_READ, unix.MAP_SHARED)
if err != nil {
f.Close()
return nil, fmt.Errorf("mmap %s: %w", path, err)
}
hdr, err := DecodeHeader(data)
if err != nil {
unix.Munmap(data)
f.Close()
return nil, err
}
// Validate section bounds
checkSection := func(name string, s *SectionHeader) error {
if uint64(s.Offset)+uint64(s.Size) > uint64(len(data)) {
return fmt.Errorf("section %s out of bounds: offset %d + size %d > file %d", name, s.Offset, s.Size, len(data))
}
return nil
}
sections := []struct {
name string
s *SectionHeader
}{
{"InternedStrings", &hdr.InternedStrings},
{"TagMap", &hdr.TagMap},
{"KeyIndex", &hdr.KeyIndex},
{"KeyData", &hdr.KeyData},
{"PostingData", &hdr.PostingData},
{"EntryIndex", &hdr.EntryIndex},
{"EntryData", &hdr.EntryData},
{"KanjiIndex", &hdr.KanjiIndex},
{"KanjiData", &hdr.KanjiData},
{"FuriganaIndex", &hdr.FuriganaIndex},
{"FuriganaKeyData", &hdr.FuriganaKeyData},
{"FuriganaData", &hdr.FuriganaData},
{"KradIndex", &hdr.KradIndex},
{"KradData", &hdr.KradData},
{"RadkIndex", &hdr.RadkIndex},
{"RadkData", &hdr.RadkData},
{"KanjiWordsIndex", &hdr.KanjiWordsIndex},
{"KanjiWordsData", &hdr.KanjiWordsData},
}
for _, sec := range sections {
if err := checkSection(sec.name, sec.s); err != nil {
unix.Munmap(data)
f.Close()
return nil, err
}
}
// Read interned strings
internedBytes := data[hdr.InternedStrings.Offset : hdr.InternedStrings.Offset+hdr.InternedStrings.Size]
interned := make([]string, hdr.InternedStrings.Count)
ir := &ByteReader{B: internedBytes}
for i := uint32(0); i < hdr.InternedStrings.Count; i++ {
s, err := ir.Str()
if err != nil {
unix.Munmap(data)
f.Close()
return nil, fmt.Errorf("read interned string %d: %w", i, err)
}
interned[i] = s
}
// Read tags map
tagBytes := data[hdr.TagMap.Offset : hdr.TagMap.Offset+hdr.TagMap.Size]
tags := make(map[string]string, hdr.TagMap.Count)
tr := &ByteReader{B: tagBytes}
for i := uint32(0); i < hdr.TagMap.Count; i++ {
code, err := tr.Str()
if err != nil {
unix.Munmap(data)
f.Close()
return nil, fmt.Errorf("read tag code %d: %w", i, err)
}
label, err := tr.Str()
if err != nil {
unix.Munmap(data)
f.Close()
return nil, fmt.Errorf("read tag label %d: %w", i, err)
}
tags[code] = label
}
d := &Dict{
file: f,
mmap: data,
header: hdr,
interned: interned,
tags: tags,
keyIndexBytes: data[hdr.KeyIndex.Offset : hdr.KeyIndex.Offset+hdr.KeyIndex.Size],
keyDataBytes: data[hdr.KeyData.Offset : hdr.KeyData.Offset+hdr.KeyData.Size],
postingBytes: data[hdr.PostingData.Offset : hdr.PostingData.Offset+hdr.PostingData.Size],
entryIndexBytes: data[hdr.EntryIndex.Offset : hdr.EntryIndex.Offset+hdr.EntryIndex.Size],
entryDataBytes: data[hdr.EntryData.Offset : hdr.EntryData.Offset+hdr.EntryData.Size],
kanjiIndexBytes: data[hdr.KanjiIndex.Offset : hdr.KanjiIndex.Offset+hdr.KanjiIndex.Size],
kanjiDataBytes: data[hdr.KanjiData.Offset : hdr.KanjiData.Offset+hdr.KanjiData.Size],
furiganaIndexBytes: data[hdr.FuriganaIndex.Offset : hdr.FuriganaIndex.Offset+hdr.FuriganaIndex.Size],
furiganaKeyBytes: data[hdr.FuriganaKeyData.Offset : hdr.FuriganaKeyData.Offset+hdr.FuriganaKeyData.Size],
furiganaDataBytes: data[hdr.FuriganaData.Offset : hdr.FuriganaData.Offset+hdr.FuriganaData.Size],
kradIndexBytes: data[hdr.KradIndex.Offset : hdr.KradIndex.Offset+hdr.KradIndex.Size],
kradDataBytes: data[hdr.KradData.Offset : hdr.KradData.Offset+hdr.KradData.Size],
radkIndexBytes: data[hdr.RadkIndex.Offset : hdr.RadkIndex.Offset+hdr.RadkIndex.Size],
radkDataBytes: data[hdr.RadkData.Offset : hdr.RadkData.Offset+hdr.RadkData.Size],
kanjiWordsIndexBytes: data[hdr.KanjiWordsIndex.Offset : hdr.KanjiWordsIndex.Offset+hdr.KanjiWordsIndex.Size],
kanjiWordsDataBytes: data[hdr.KanjiWordsData.Offset : hdr.KanjiWordsData.Offset+hdr.KanjiWordsData.Size],
}
return d, nil
}
// Close unmaps memory and closes the dictionary file.
func (d *Dict) Close() error {
var errs []error
if d.mmap != nil {
if err := unix.Munmap(d.mmap); err != nil {
errs = append(errs, err)
}
d.mmap = nil
}
if d.file != nil {
if err := d.file.Close(); err != nil {
errs = append(errs, err)
}
d.file = nil
}
return errors.Join(errs...)
}
// TagLabel returns the English description of a JMdict tag code (e.g. "v1" -> "Ichidan verb")
// using this dictionary's tags map, falling back to built-in common tags, or returning code unchanged.
func (d *Dict) TagLabel(code string) string {
if d != nil && d.tags != nil {
if label, ok := d.tags[code]; ok {
return label
}
}
if label, ok := fallbackTags[code]; ok {
return label
}
return code
}
// searchKey performs a binary search in the sorted key index for exact match of key.
func (d *Dict) searchKey(target []byte) (postingOffset uint32, postingCount uint16, found bool) {
numKeys := len(d.keyIndexBytes) / KeyIndexRecordSize
low := 0
high := numKeys - 1
for low <= high {
mid := int(uint(low+high) >> 1)
recOff := mid * KeyIndexRecordSize
keyOff := binary.LittleEndian.Uint32(d.keyIndexBytes[recOff : recOff+4])
keyLen := binary.LittleEndian.Uint16(d.keyIndexBytes[recOff+4 : recOff+6])
keyBytes := d.keyDataBytes[keyOff : keyOff+uint32(keyLen)]
cmp := bytes.Compare(keyBytes, target)
if cmp < 0 {
low = mid + 1
} else if cmp > 0 {
high = mid - 1
} else {
pCount := binary.LittleEndian.Uint16(d.keyIndexBytes[recOff+6 : recOff+8])
pOff := binary.LittleEndian.Uint32(d.keyIndexBytes[recOff+8 : recOff+12])
return pOff, pCount, true
}
}
return 0, 0, false
}
// getPostings reads the list of entry indices for a given posting offset and count.
func (d *Dict) getPostings(postOff uint32, postCount uint16) []uint32 {
if postCount == 0 {
return nil
}
out := make([]uint32, postCount)
byteStart := postOff * 4
for i := uint16(0); i < postCount; i++ {
out[i] = binary.LittleEndian.Uint32(d.postingBytes[byteStart+uint32(i)*4 : byteStart+uint32(i)*4+4])
}
return out
}
// getEntry decodes entry record by index.
func (d *Dict) getEntry(entryIdx uint32) (Entry, error) {
numEntries := uint32(len(d.entryIndexBytes) / 4)
if entryIdx >= numEntries {
return Entry{}, fmt.Errorf("entry index %d out of bounds (%d)", entryIdx, numEntries)
}
off := binary.LittleEndian.Uint32(d.entryIndexBytes[entryIdx*4 : entryIdx*4+4])
if off >= uint32(len(d.entryDataBytes)) {
return Entry{}, fmt.Errorf("entry data offset %d out of bounds", off)
}
e, _, err := DecodeEntry(d.entryDataBytes[off:], d.interned)
return e, err
}
// Lookup performs an exact match on any kanji or kana form.
// Kana keys match across hiragana and katakana.
func (d *Dict) Lookup(word string) []Entry {
if len(word) == 0 {
return nil
}
norm := jp.ToHiragana(word)
// Collect candidate posting lists.
var candidateIndices []uint32
seen := make(map[uint32]bool)
if pOff, pCount, ok := d.searchKey([]byte(word)); ok {
posts := d.getPostings(pOff, pCount)
for _, idx := range posts {
if !seen[idx] {
seen[idx] = true
candidateIndices = append(candidateIndices, idx)
}
}
}
// If word has kana and norm != word, also look up normalized form
if norm != word {
if pOff, pCount, ok := d.searchKey([]byte(norm)); ok {
posts := d.getPostings(pOff, pCount)
for _, idx := range posts {
if !seen[idx] {
seen[idx] = true
candidateIndices = append(candidateIndices, idx)
}
}
}
}
if len(candidateIndices) == 0 {
return nil
}
entries := make([]Entry, 0, len(candidateIndices))
for _, idx := range candidateIndices {
if e, err := d.getEntry(idx); err == nil {
entries = append(entries, e)
}
}
return entries
}
// LookupReading returns entries matching word, with entries where kana==reading
// placed first, followed by the rest of Lookup(word). When word is kana-only,
// entries ordinarily written in kana (particles, kana words, and "uk" entries)
// come before kanji words that merely share the reading.
func (d *Dict) LookupReading(word, reading string) []Entry {
entries := d.Lookup(word)
if len(entries) <= 1 {
return entries
}
wordNorm := jp.ToHiragana(word)
if jp.IsKanaOnly(word) {
slices.SortStableFunc(entries, func(a, b Entry) int {
return cmp.Compare(kanaRank(b, word, wordNorm), kanaRank(a, word, wordNorm))
})
return entries
}
readingNorm := jp.ToHiragana(reading)
var matched []Entry
var rest []Entry
for _, e := range entries {
hasWord := false
for _, k := range e.Kanji {
if k.Text == word {
hasWord = true
break
}
}
if !hasWord {
for _, r := range e.Kana {
if r.Text == word || jp.ToHiragana(r.Text) == wordNorm {
hasWord = true
break
}
}
}
hasReading := false
for _, r := range e.Kana {
if r.Text == reading || jp.ToHiragana(r.Text) == readingNorm {
hasReading = true
break
}
}
if hasWord && hasReading {
matched = append(matched, e)
} else {
rest = append(rest, e)
}
}
return append(matched, rest...)
}
var (
nonDisplayKanji = []string{"sK", "iK", "rK", "oK"}
nonDisplayKana = []string{"sk", "ik", "rk", "ok"}
)
func hasAnyTag(tags, set []string) bool {
for _, t := range tags {
if slices.Contains(set, t) {
return true
}
}
return false
}
// WrittenInKana reports whether e is ordinarily written in kana: it has no
// displayable kanji form, or its primary sense is marked "uk" (usually kana).
func WrittenInKana(e Entry) bool {
if len(e.Senses) > 0 && slices.Contains(e.Senses[0].Misc, "uk") {
return true
}
for _, k := range e.Kanji {
if !hasAnyTag(k.Tags, nonDisplayKanji) {
return false
}
}
return true
}
func kanaRank(e Entry, word, wordNorm string) int {
best := 0
inKana := WrittenInKana(e)
for _, r := range e.Kana {
if r.Text != word && jp.ToHiragana(r.Text) != wordNorm {
continue
}
s := 1
if !hasAnyTag(r.Tags, nonDisplayKana) {
s += 16
}
if r.Common || (word != wordNorm && r.Text == word && inKana) {
s += 8
if inKana {
s += 4
}
}
if r.Text == word {
s += 2
}
if s > best {
best = s
}
}
return best
}
// LongestPrefix finds the longest prefix of s (up to maxRunes) that has
// dictionary entries. It returns the byte length n of the prefix and the entries.
func (d *Dict) LongestPrefix(s string, maxRunes int) (int, []Entry) {
if len(s) == 0 || maxRunes <= 0 {
return 0, nil
}
boundaries := make([]int, 0, maxRunes+1)
boundaries = append(boundaries, 0)
cur := 0
for cur < len(s) && len(boundaries) <= maxRunes {
_, size := utf8.DecodeRuneInString(s[cur:])
cur += size
boundaries = append(boundaries, cur)
}
for i := len(boundaries) - 1; i >= 1; i-- {
prefix := s[:boundaries[i]]
entries := d.Lookup(prefix)
if len(entries) > 0 {
return len(prefix), entries
}
}
return 0, nil
}
// Kanji looks up a single kanji character by rune.
func (d *Dict) Kanji(r rune) (Kanji, bool) {
target := uint32(r)
numKanji := len(d.kanjiIndexBytes) / KanjiIndexRecordSize
low := 0
high := numKanji - 1
for low <= high {
mid := int(uint(low+high) >> 1)
recOff := mid * KanjiIndexRecordSize
kRune := binary.LittleEndian.Uint32(d.kanjiIndexBytes[recOff : recOff+4])
if kRune < target {
low = mid + 1
} else if kRune > target {
high = mid - 1
} else {
dataOff := binary.LittleEndian.Uint32(d.kanjiIndexBytes[recOff+4 : recOff+8])
if dataOff >= uint32(len(d.kanjiDataBytes)) {
return Kanji{}, false
}
k, _, err := DecodeKanji(d.kanjiDataBytes[dataOff:])
if err != nil {
return Kanji{}, false
}
return k, true
}
}
return Kanji{}, false
}
// Furigana looks up per-kanji furigana segmentation for word with reading.
// word may be any form (typically kanji); reading is matched normalized to hiragana.
func (d *Dict) Furigana(word, reading string) ([]Ruby, bool) {
if len(d.furiganaIndexBytes) == 0 {
return nil, false
}
readingNorm := jp.ToHiragana(reading)
targetKey := []byte(word + "\x00" + readingNorm)
numRecords := len(d.furiganaIndexBytes) / FuriganaIndexRecordSize
low := 0
high := numRecords - 1
for low <= high {
mid := int(uint(low+high) >> 1)
recOff := mid * FuriganaIndexRecordSize
kOff := binary.LittleEndian.Uint32(d.furiganaIndexBytes[recOff : recOff+4])
kLen := binary.LittleEndian.Uint16(d.furiganaIndexBytes[recOff+4 : recOff+6])
kBytes := d.furiganaKeyBytes[kOff : kOff+uint32(kLen)]
cmp := bytes.Compare(kBytes, targetKey)
if cmp < 0 {
low = mid + 1
} else if cmp > 0 {
high = mid - 1
} else {
dataOff := binary.LittleEndian.Uint32(d.furiganaIndexBytes[recOff+8 : recOff+12])
if dataOff >= uint32(len(d.furiganaDataBytes)) {
return nil, false
}
rubies, _, err := DecodeRubies(d.furiganaDataBytes[dataOff:])
if err != nil {
return nil, false
}
return rubies, true
}
}
return nil, false
}
var fallbackTags = map[string]string{
"v1": "Ichidan verb",
"v5u": "Godan verb with 'u' ending",
"v5k": "Godan verb with 'ku' ending",
"v5s": "Godan verb with 'su' ending",
"v5t": "Godan verb with 'tsu' ending",
"v5n": "Godan verb with 'nu' ending",
"v5m": "Godan verb with 'mu' ending",
"v5r": "Godan verb with 'ru' ending",
"v5g": "Godan verb with 'gu' ending",
"v5b": "Godan verb with 'bu' ending",
"vz": "Ichidan verb - zuru verb",
"vk": "Kuru verb - special class",
"vn": "irregular nu verb",
"vr": "irregular ru verb",
"vs": "noun or participle which takes the aux. verb suru",
"adj-i": "adjective (keiyoushi)",
"adj-na": "adjectival nouns or quasi-adjectives (keiyodoushi)",
"adj-no": "nouns which may take the genitive case particle 'no'",
"adj-pn": "pre-noun adjectival (rentaishi)",
"adj-t": "'taru' adjective",
"adj-f": "noun or verb acting prenominally",
"n": "noun (common) (futsuumeishi)",
"n-adv": "adverbial noun (fukushitekimeishi)",
"n-pref": "noun, used as a prefix",
"n-suf": "noun, used as a suffix",
"n-t": "noun (temporal) (jisoumeishi)",
"adv": "adverb (fukushi)",
"adv-to": "adverb taking the 'to' particle",
"prt": "particle",
"conj": "conjunction",
"exp": "expressions (phrases, clauses, etc.)",
"int": "interjection (kandoushi)",
"pref": "prefix",
"suf": "suffix",
"ctr": "counter",
"pn": "pronoun",
"uk": "word usually written using kana alone",
"sK": "search-only kanji form",
"sk": "search-only kana form",
"iK": "word containing irregular kanji usage",
"ik": "word containing irregular kana usage",
"rK": "rarely used kanji form",
"rk": "rarely used kana form",
"io": "irregular okurigana usage",
"pol": "polite (teineigo) language",
"hon": "honorific or respectful (sonkeigo) language",
"hum": "humble (kenjougo) language",
"col": "colloquialism",
"sl": "slang",
"id": "idiomatic expression",
"arch": "archaic",
"obs": "obsolete term",
"rare": "rare term",
"ateji": "ateji (phonetic) reading",
"abbr": "abbreviation",
"form": "formal or literary term",
"vt": "transitive verb",
"vi": "intransitive verb",
}
// Components returns the KRADFILE components of k in KRADFILE order,
// mapped to their real display component forms (e.g. 化->⺅, 艾->艹, 汁->氵, 込->辶, |->丨, ノ->丿).
// Returns nil if k is not found in the index.
func (d *Dict) Components(k rune) []rune {
if len(d.kradIndexBytes) == 0 {
return nil
}
target := uint32(k)
numRecords := len(d.kradIndexBytes) / KradIndexRecordSize
low := 0
high := numRecords - 1
for low <= high {
mid := int(uint(low+high) >> 1)
recOff := mid * KradIndexRecordSize
r := binary.LittleEndian.Uint32(d.kradIndexBytes[recOff : recOff+4])
if r < target {
low = mid + 1
} else if r > target {
high = mid - 1
} else {
dataOff := binary.LittleEndian.Uint32(d.kradIndexBytes[recOff+4 : recOff+8])
count := binary.LittleEndian.Uint16(d.kradIndexBytes[recOff+8 : recOff+10])
if count == 0 {
return nil
}
byteLen := uint32(count) * 4
if dataOff+byteLen > uint32(len(d.kradDataBytes)) {
return nil
}
out := make([]rune, count)
for i := uint16(0); i < count; i++ {
rVal := rune(binary.LittleEndian.Uint32(d.kradDataBytes[dataOff+uint32(i)*4 : dataOff+uint32(i)*4+4]))
out[i] = DisplayComponent(rVal)
}
return out
}
}
return nil
}
// KanjiWith returns kanji containing the given component from RADKFILE,
// ordered most frequent first (by KANJIDIC2 freq rank; unranked last by stroke count).
// Accepts both real component forms (e.g. ⺅, 艹, 氵, 辶, 丨, 丿) and legacy RADKFILE
// placeholder glyphs (e.g. 化, 艾, 汁, 込, |, ノ).
// If max <= 0, it returns nil.
func (d *Dict) KanjiWith(component rune, max int) []rune {
if max <= 0 || len(d.radkIndexBytes) == 0 {
return nil
}
searchRad := func(rad rune) []rune {
target := uint32(rad)
numRecords := len(d.radkIndexBytes) / RadkIndexRecordSize
low := 0
high := numRecords - 1
for low <= high {
mid := int(uint(low+high) >> 1)
recOff := mid * RadkIndexRecordSize
r := binary.LittleEndian.Uint32(d.radkIndexBytes[recOff : recOff+4])
if r < target {
low = mid + 1
} else if r > target {
high = mid - 1
} else {
dataOff := binary.LittleEndian.Uint32(d.radkIndexBytes[recOff+4 : recOff+8])
count := binary.LittleEndian.Uint32(d.radkIndexBytes[recOff+8 : recOff+12])
if count == 0 {
return nil
}
n := int(count)
if max > 0 && max < n {
n = max
}
byteLen := uint32(n) * 4
if dataOff+byteLen > uint32(len(d.radkDataBytes)) {
return nil
}
out := make([]rune, n)
for i := 0; i < n; i++ {
out[i] = rune(binary.LittleEndian.Uint32(d.radkDataBytes[dataOff+uint32(i)*4 : dataOff+uint32(i)*4+4]))
}
return out
}
}
return nil
}
// 1. Direct search
if res := searchRad(component); len(res) > 0 {
return res
}
// 2. Try display form
if disp, ok := ToDisplayForm[component]; ok && disp != component {
if res := searchRad(disp); len(res) > 0 {
return res
}
}
// 3. Try placeholder form
if orig, ok := ToPlaceholderForm[component]; ok && orig != component {
if res := searchRad(orig); len(res) > 0 {
return res
}
}
// 4. Single-component kanji (e.g. 良 -> 艮, 八 -> ハ)
if comps := d.Components(component); len(comps) == 1 && comps[0] != component {
if res := searchRad(comps[0]); len(res) > 0 {
return res
}
if orig, ok := ToPlaceholderForm[comps[0]]; ok && orig != comps[0] {
if res := searchRad(orig); len(res) > 0 {
return res
}
}
if disp, ok := ToDisplayForm[comps[0]]; ok && disp != comps[0] {
if res := searchRad(disp); len(res) > 0 {
return res
}
}
}
return nil
}
// ComponentName returns the English name for display of component r:
// for Kangxi radicals (or variants per CJKRadicals.txt), it returns the lowercased
// Unicode radical name. For non-radical components, it uses KANJIDIC2's first meaning
// with any "... radical (no. N)" suffixes stripped. Names are never empty.
func (d *Dict) ComponentName(r rune) string {
// 1. Check Kangxi radical names table
if name, ok := kangxiRadicalNames[r]; ok {
return name
}
if disp, ok := ToDisplayForm[r]; ok {
if name, ok := kangxiRadicalNames[disp]; ok {
return name
}
}
if orig, ok := ToPlaceholderForm[r]; ok {
if name, ok := kangxiRadicalNames[orig]; ok {
return name
}
}
// 2. Non-radical components: KANJIDIC2 first meaning, stripping "... radical (no. N)"
if k, ok := d.Kanji(r); ok && len(k.Meanings) > 0 {
for _, m := range k.Meanings {
cleaned := CleanKanjidicMeaning(m)
if cleaned != "" {
return cleaned
}
}
}
// 3. Fallback to general radical names table
if name, ok := radicalNames[r]; ok {
return name
}
// 4. Fallback must never be empty
return "component"
}
// WordsWith returns JMdict entries with a displayable kanji form containing k,
// in the same priority order Lookup uses (EDRDG pri, common first), excluding
// entries whose only k-forms are search-only.
// If max <= 0, it returns nil.
func (d *Dict) WordsWith(k rune, max int) []Entry {
if max <= 0 || len(d.kanjiWordsIndexBytes) == 0 {
return nil
}
target := uint32(k)
numRecords := len(d.kanjiWordsIndexBytes) / KanjiWordsIndexRecordSize
low := 0
high := numRecords - 1
for low <= high {
mid := int(uint(low+high) >> 1)
recOff := mid * KanjiWordsIndexRecordSize
r := binary.LittleEndian.Uint32(d.kanjiWordsIndexBytes[recOff : recOff+4])
if r < target {
low = mid + 1
} else if r > target {
high = mid - 1
} else {
dataOff := binary.LittleEndian.Uint32(d.kanjiWordsIndexBytes[recOff+4 : recOff+8])
count := binary.LittleEndian.Uint16(d.kanjiWordsIndexBytes[recOff+8 : recOff+10])
if count == 0 {
return nil
}
n := int(count)
if max > 0 && max < n {
n = max
}
byteLen := uint32(n) * 4
if dataOff+byteLen > uint32(len(d.kanjiWordsDataBytes)) {
return nil
}
entries := make([]Entry, 0, n)
for i := 0; i < n; i++ {
entryIdx := binary.LittleEndian.Uint32(d.kanjiWordsDataBytes[dataOff+uint32(i)*4 : dataOff+uint32(i)*4+4])
if e, err := d.getEntry(entryIdx); err == nil {
entries = append(entries, e)
}
}
return entries
}
}
return nil
}