Skip to content

Commit da8a441

Browse files
authored
Improved terminfo parser (#51198)
This bundles up the following changes: - Rejiged TermInfo struct - Read the extended terminfo table using the same method as the non-extended table - Use signed integer types for most numeric values, as per term(5) - More robust get(::TermInfo, ...) methods - Better match the terminfo(5) "Fetching Compiled Descriptions" behaviour
1 parent 94fd312 commit da8a441

File tree

2 files changed

+116
-85
lines changed

2 files changed

+116
-85
lines changed

base/terminfo.jl

Lines changed: 108 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
# This file is a part of Julia. License is MIT: https://julialang.org/license
22

3+
# Since this code is in the startup-path, we go to some effort to
4+
# be easier on the compiler, such as using `map` over broadcasting.
5+
36
include("terminfo_data.jl")
47

58
"""
@@ -15,7 +18,7 @@ particular capabilities, solely based on `term(5)`.
1518
1619
- `names::Vector{String}`: The names this terminal is known by.
1720
- `flags::BitVector`: A list of 0–$(length(TERM_FLAGS)) flag values.
18-
- `numbers::Union{Vector{UInt16}, Vector{UInt32}}`: A list of 0–$(length(TERM_NUMBERS))
21+
- `numbers::Union{Vector{Int16}, Vector{Int32}}`: A list of 0–$(length(TERM_NUMBERS))
1922
number values. A value of `typemax(eltype(numbers))` is used to skip over
2023
unspecified capabilities while ensuring value indices are correct.
2124
- `strings::Vector{Union{String, Nothing}}`: A list of 0–$(length(TERM_STRINGS))
@@ -30,9 +33,9 @@ See also: `TermInfo` and `TermCapability`.
3033
struct TermInfoRaw
3134
names::Vector{String}
3235
flags::BitVector
33-
numbers::Union{Vector{UInt16}, Vector{UInt32}}
36+
numbers::Vector{Int}
3437
strings::Vector{Union{String, Nothing}}
35-
extended::Union{Nothing, Dict{Symbol, Union{Bool, Int, String}}}
38+
extended::Union{Nothing, Dict{Symbol, Union{Bool, Int, String, Nothing}}}
3639
end
3740

3841
"""
@@ -59,94 +62,105 @@ See also: `TermInfoRaw` and `TermCapability`.
5962
"""
6063
struct TermInfo
6164
names::Vector{String}
62-
flags::Int
63-
numbers::BitVector
64-
strings::BitVector
65-
extensions::Vector{Symbol}
66-
capabilities::Dict{Symbol, Union{Bool, Int, String}}
65+
flags::Dict{Symbol, Bool}
66+
numbers::Dict{Symbol, Int}
67+
strings::Dict{Symbol, String}
68+
extensions::Union{Nothing, Set{Symbol}}
6769
end
6870

69-
TermInfo() = TermInfo([], 0, [], [], [], Dict())
71+
TermInfo() = TermInfo([], Dict(), Dict(), Dict(), nothing)
7072

7173
function read(data::IO, ::Type{TermInfoRaw})
7274
# Parse according to `term(5)`
7375
# Header
7476
magic = read(data, UInt16) |> ltoh
7577
NumInt = if magic == 0o0432
76-
UInt16
78+
Int16
7779
elseif magic == 0o01036
78-
UInt32
80+
Int32
7981
else
8082
throw(ArgumentError("Terminfo data did not start with the magic number 0o0432 or 0o01036"))
8183
end
82-
name_bytes = read(data, UInt16) |> ltoh
83-
flag_bytes = read(data, UInt16) |> ltoh
84-
numbers_count = read(data, UInt16) |> ltoh
85-
string_count = read(data, UInt16) |> ltoh
86-
table_bytes = read(data, UInt16) |> ltoh
84+
name_bytes, flag_bytes, numbers_count, string_count, table_bytes =
85+
@ntuple 5 _->read(data, Int16) |> ltoh
8786
# Terminal Names
88-
term_names = split(String(read(data, name_bytes - 1)), '|') .|> String
87+
term_names = map(String, split(String(read(data, name_bytes - 1)), '|'))
8988
0x00 == read(data, UInt8) ||
9089
throw(ArgumentError("Terminfo data did not contain a null byte after the terminal names section"))
9190
# Boolean Flags
92-
flags = read(data, flag_bytes) .== 0x01
91+
flags = map(==(0x01), read(data, flag_bytes))
9392
if position(data) % 2 != 0
9493
0x00 == read(data, UInt8) ||
9594
throw(ArgumentError("Terminfo did not contain a null byte after the flag section, expected to position the start of the numbers section on an even byte"))
9695
end
9796
# Numbers, Strings, Table
98-
numbers = map(ltoh, reinterpret(NumInt, read(data, numbers_count * sizeof(NumInt))))
99-
string_indices = map(ltoh, reinterpret(UInt16, read(data, string_count * sizeof(UInt16))))
97+
numbers = map(Int ltoh, reinterpret(NumInt, read(data, numbers_count * sizeof(NumInt))))
98+
string_indices = map(ltoh, reinterpret(Int16, read(data, string_count * sizeof(Int16))))
10099
strings_table = read(data, table_bytes)
101-
strings = map(string_indices) do idx
102-
if idx (0xffff, 0xfffe)
103-
len = findfirst(==(0x00), view(strings_table, 1+idx:length(strings_table)))
104-
!isnothing(len) ||
105-
throw(ArgumentError("Terminfo string table entry does not terminate with a null byte"))
106-
String(strings_table[1+idx:idx+len-1])
107-
end
108-
end
100+
strings = _terminfo_read_strings(strings_table, string_indices)
109101
TermInfoRaw(term_names, flags, numbers, strings,
110102
if !eof(data) extendedterminfo(data, NumInt) end)
111103
end
112104

113105
"""
114-
extendedterminfo(data::IO; NumInt::Union{Type{UInt16}, Type{UInt32}})
106+
extendedterminfo(data::IO; NumInt::Union{Type{Int16}, Type{Int32}})
115107
116108
Read an extended terminfo section from `data`, with `NumInt` as the numbers type.
117109
118110
This will accept any terminfo content that conforms with `term(5)`.
119111
120112
See also: `read(::IO, ::Type{TermInfoRaw})`
121113
"""
122-
function extendedterminfo(data::IO, NumInt::Union{Type{UInt16}, Type{UInt32}})
114+
function extendedterminfo(data::IO, NumInt::Union{Type{Int16}, Type{Int32}})
123115
# Extended info
124116
if position(data) % 2 != 0
125117
0x00 == read(data, UInt8) ||
126-
throw(ArgumentError("Terminfo did not contain a null byte before the extended section, expected to position the start on an even byte"))
118+
throw(ArgumentError("Terminfo did not contain a null byte before the extended section; expected to position the start on an even byte"))
127119
end
128120
# Extended header
129-
flag_bytes = read(data, UInt16) |> ltoh
130-
numbers_count = read(data, UInt16) |> ltoh
131-
string_count = read(data, UInt16) |> ltoh
132-
table_count = read(data, UInt16) |> ltoh
133-
table_bytes = read(data, UInt16) |> ltoh
121+
flag_bytes, numbers_count, string_count, table_count, table_bytes =
122+
@ntuple 5 _->read(data, Int16) |> ltoh
134123
# Extended flags/numbers/strings
135-
flags = read(data, flag_bytes) .== 0x01
124+
flags = map(==(0x01), read(data, flag_bytes))
136125
if flag_bytes % 2 != 0
137126
0x00 == read(data, UInt8) ||
138-
throw(ArgumentError("Terminfo did not contain a null byte after the extended flag section, expected to position the start of the numbers section on an even byte"))
127+
throw(ArgumentError("Terminfo did not contain a null byte after the extended flag section; expected to position the start of the numbers section on an even byte"))
128+
end
129+
numbers = map(Int ltoh, reinterpret(NumInt, read(data, numbers_count * sizeof(NumInt))))
130+
table_indices = map(ltoh, reinterpret(Int16, read(data, table_count * sizeof(Int16))))
131+
table_data = read(data, table_bytes)
132+
strings = _terminfo_read_strings(table_data, table_indices[1:string_count])
133+
table_halfoffset = Int16(get(table_indices, string_count, 0) +
134+
ncodeunits(something(get(strings, length(strings), ""), "")) + 1)
135+
for index in string_count+1:lastindex(table_indices)
136+
table_indices[index] += table_halfoffset
139137
end
140-
numbers = map(n -> Int(ltoh(n)), reinterpret(NumInt, read(data, numbers_count * sizeof(NumInt))))
141-
table_indices = map(ltoh, reinterpret(UInt16, read(data, table_count * sizeof(UInt16))))
142-
table_strings = [String(readuntil(data, 0x00)) for _ in 1:length(table_indices)]
143-
info = Dict{Symbol, Union{Bool, Int, String}}()
144-
strings = table_strings[1:string_count]
145-
labels = table_strings[string_count+1:end]
146-
for (label, val) in zip(labels, vcat(flags, numbers, strings))
147-
info[Symbol(label)] = val
138+
labels = map(Symbol, _terminfo_read_strings(table_data, table_indices[string_count+1:end]))
139+
Dict{Symbol, Union{Bool, Int, String, Nothing}}(
140+
zip(labels, Iterators.flatten((flags, numbers, strings))))
141+
end
142+
143+
"""
144+
_terminfo_read_strings(table::Vector{UInt8}, indices::Vector{Int16})
145+
146+
From `table`, read a string starting at each position in `indices`. Each string
147+
must be null-terminated. Should an index be -1 or -2, `nothing` is given instead
148+
of a string.
149+
"""
150+
function _terminfo_read_strings(table::Vector{UInt8}, indices::Vector{Int16})
151+
strings = Vector{Union{Nothing, String}}(undef, length(indices))
152+
map!(strings, indices) do idx
153+
if idx >= 0
154+
len = findfirst(==(0x00), view(table, 1+idx:length(table)))
155+
!isnothing(len) ||
156+
throw(ArgumentError("Terminfo table entry @$idx does not terminate with a null byte"))
157+
String(table[1+idx:idx+len-1])
158+
elseif idx (-1, -2)
159+
else
160+
throw(ArgumentError("Terminfo table index is invalid: -2 ≰ $idx"))
161+
end
148162
end
149-
return info
163+
strings
150164
end
151165

152166
"""
@@ -158,45 +172,60 @@ NCurses 6.3, see `TERM_FLAGS`, `TERM_NUMBERS`, and `TERM_STRINGS`).
158172
function TermInfo(raw::TermInfoRaw)
159173
capabilities = Dict{Symbol, Union{Bool, Int, String}}()
160174
sizehint!(capabilities, 2 * (length(raw.flags) + length(raw.numbers) + length(raw.strings)))
175+
flags = Dict{Symbol, Bool}()
176+
numbers = Dict{Symbol, Int}()
177+
strings = Dict{Symbol, String}()
178+
extensions = nothing
161179
for (flag, value) in zip(TERM_FLAGS, raw.flags)
162-
capabilities[flag.short] = value
163-
capabilities[flag.long] = value
180+
flags[flag.short] = value
181+
flags[flag.long] = value
164182
end
165183
for (num, value) in zip(TERM_NUMBERS, raw.numbers)
166-
if value != typemax(eltype(raw.numbers))
167-
capabilities[num.short] = Int(value)
168-
capabilities[num.long] = Int(value)
169-
end
184+
numbers[num.short] = Int(value)
185+
numbers[num.long] = Int(value)
170186
end
171187
for (str, value) in zip(TERM_STRINGS, raw.strings)
172188
if !isnothing(value)
173-
capabilities[str.short] = value
174-
capabilities[str.long] = value
189+
strings[str.short] = value
190+
strings[str.long] = value
175191
end
176192
end
177-
extensions = if !isnothing(raw.extended)
178-
capabilities = merge(capabilities, raw.extended)
179-
keys(raw.extended) |> collect
180-
else
181-
Symbol[]
193+
if !isnothing(raw.extended)
194+
extensions = Set{Symbol}()
195+
for (key, value) in raw.extended
196+
push!(extensions, key)
197+
if value isa Bool
198+
flags[key] = value
199+
elseif value isa Int
200+
numbers[key] = value
201+
elseif value isa String
202+
strings[key] = value
203+
end
204+
end
182205
end
183-
TermInfo(raw.names, length(raw.flags),
184-
map(n-> n != typemax(typeof(n)), raw.numbers),
185-
map(!isnothing, raw.strings),
186-
extensions, capabilities)
206+
TermInfo(raw.names, flags, numbers, strings, extensions)
207+
end
208+
209+
get(ti::TermInfo, key::Symbol, default::Bool) = get(ti.flags, key, default)
210+
get(ti::TermInfo, key::Symbol, default::Int) = get(ti.numbers, key, default)
211+
get(ti::TermInfo, key::Symbol, default::String) = get(ti.strings, key, default)
212+
213+
haskey(ti::TermInfo, key::Symbol) =
214+
haskey(ti.flags, key) || haskey(ti.numbers, key) || haskey(ti.strings, key)
215+
216+
function getindex(ti::TermInfo, key::Symbol)
217+
haskey(ti.flags, key) && return ti.flags[key]
218+
haskey(ti.numbers, key) && return ti.numbers[key]
219+
haskey(ti.strings, key) && return ti.strings[key]
220+
throw(KeyError(key))
187221
end
188222

189-
getindex(ti::TermInfo, key::Symbol) = ti.capabilities[key]
190-
get(ti::TermInfo, key::Symbol, default::D) where D<:Union{Bool, Int, String} =
191-
get(ti.capabilities, key, default)::D
192-
get(ti::TermInfo, key::Symbol, default) = get(ti.capabilities, key, default)
193-
keys(ti::TermInfo) = keys(ti.capabilities)
194-
haskey(ti::TermInfo, key::Symbol) = haskey(ti.capabilities, key)
223+
keys(ti::TermInfo) = keys(ti.flags) keys(ti.numbers) keys(ti.strings)
195224

196225
function show(io::IO, ::MIME"text/plain", ti::TermInfo)
197-
print(io, "TermInfo(", ti.names, "; ", ti.flags, " flags, ",
198-
sum(ti.numbers), " numbers, ", sum(ti.strings), " strings")
199-
!isempty(ti.extensions) > 0 &&
226+
print(io, "TermInfo(", ti.names, "; ", length(ti.flags), " flags, ",
227+
length(ti.numbers), " numbers, ", length(ti.strings), " strings")
228+
!isnothing(ti.extensions) &&
200229
print(io, ", ", length(ti.extensions), " extended capabilities")
201230
print(io, ')')
202231
end
@@ -216,13 +245,15 @@ function find_terminfo_file(term::String)
216245
[ENV["TERMINFO"]]
217246
elseif isdir(joinpath(homedir(), ".terminfo"))
218247
[joinpath(homedir(), ".terminfo")]
219-
elseif haskey(ENV, "TERMINFO_DIRS")
220-
split(ENV["TERMINFO_DIRS"], ':')
221-
elseif Sys.isunix()
222-
["/usr/share/terminfo"]
223248
else
224249
String[]
225250
end
251+
haskey(ENV, "TERMINFO_DIRS") &&
252+
append!(terminfo_dirs,
253+
replace(split(ENV["TERMINFO_DIRS"], ':'),
254+
"" => "/usr/share/terminfo"))
255+
Sys.isunix() &&
256+
push!(terminfo_dirs, "/etc/terminfo", "/usr/share/terminfo")
226257
for dir in terminfo_dirs
227258
if isfile(joinpath(dir, chr, term))
228259
return joinpath(dir, chr, term)

test/terminfo.jl

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -889,20 +889,20 @@ let
889889
@testset "terminfo" begin
890890
dumb = Base.TermInfo(read(IOBuffer(dumb_terminfo), Base.TermInfoRaw))
891891
@test dumb.names == ["dumb", "80-column dumb tty"]
892-
@test dumb.flags == 2
893-
@test dumb.numbers == [true]
894-
@test dumb.extensions == Symbol[]
895-
@test length(dumb.capabilities) == 14
892+
@test length(dumb.flags) == 4
893+
@test length(dumb.numbers) == 2
894+
@test length(dumb.strings) == 8
895+
@test isnothing(dumb.extensions)
896896
for (key, value) in dumb_capabilities
897897
@test dumb[key] == value
898898
end
899899

900900
xterm = Base.TermInfo(read(IOBuffer(xterm_terminfo), Base.TermInfoRaw))
901901
@test xterm.names == ["xterm", "xterm terminal emulator (X Window System)"]
902-
@test xterm.flags == 38
903-
@test xterm.numbers == Bool[1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1]
904-
@test sort(xterm.extensions) == sort(xterm_extensions)
905-
@test length(xterm.capabilities) == 519
902+
@test length(xterm.flags) == 78
903+
@test length(xterm.numbers) == 29
904+
@test length(xterm.strings) == 432
905+
@test sort(xterm.extensions |> collect) == sort(xterm_extensions)
906906
for (key, value) in xterm_capabilities
907907
@test xterm[key] == value
908908
end

0 commit comments

Comments
 (0)