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
|
# frozen_string_literal: true
module Gitlab
module Ci
module Ansi2json
class Converter
def convert(stream, new_state)
@lines = []
@state = State.new(new_state, stream.size)
append = false
truncated = false
cur_offset = stream.tell
if cur_offset > @state.offset
@state.offset = cur_offset
truncated = true
else
stream.seek(@state.offset)
append = @state.offset > 0
end
start_offset = @state.offset
@state.set_current_line!(style: Style.new(@state.inherited_style))
stream.each_line do |line|
s = StringScanner.new(line)
convert_line(s)
end
# This must be assigned before flushing the current line
# or the @current_line.offset will advance to the very end
# of the trace. Instead we want @last_line_offset to always
# point to the beginning of last line.
@state.set_last_line_offset
flush_current_line
# TODO: replace OpenStruct with a better type
# https://gitlab.com/gitlab-org/gitlab/issues/34305
OpenStruct.new(
lines: @lines,
state: @state.encode,
append: append,
truncated: truncated,
offset: start_offset,
size: stream.tell - start_offset,
total: stream.size
)
end
private
def convert_line(scanner)
until scanner.eos?
if scanner.scan(Gitlab::Regex.build_trace_section_regex)
handle_section(scanner)
elsif scanner.scan(/\e([@-_])(.*?)([@-~])/)
handle_sequence(scanner)
elsif scanner.scan(/\e(([@-_])(.*?)?)?$/)
break
elsif scanner.scan(/</)
@state.current_line << '<'
elsif scanner.scan(/\r?\n/)
# we advance the offset of the next current line
# so it does not start from \n
flush_current_line(advance_offset: scanner.matched_size)
else
@state.current_line << scanner.scan(/./m)
end
@state.offset += scanner.matched_size
end
end
def handle_sequence(scanner)
indicator = scanner[1]
commands = scanner[2].split ';'
terminator = scanner[3]
# We are only interested in color and text style changes - triggered by
# sequences starting with '\e[' and ending with 'm'. Any other control
# sequence gets stripped (including stuff like "delete last line")
return unless indicator == '[' && terminator == 'm'
@state.update_style(commands)
end
def handle_section(scanner)
action = scanner[1]
timestamp = scanner[2]
section = scanner[3]
section_name = sanitize_section_name(section)
if action == "start"
handle_section_start(section_name, timestamp)
elsif action == "end"
handle_section_end(section_name, timestamp)
end
end
def handle_section_start(section, timestamp)
flush_current_line unless @state.current_line.empty?
@state.open_section(section, timestamp)
end
def handle_section_end(section, timestamp)
return unless @state.section_open?(section)
flush_current_line unless @state.current_line.empty?
@state.close_section(section, timestamp)
# ensure that section end is detached from the last
# line in the section
flush_current_line
end
def flush_current_line(advance_offset: 0)
@lines << @state.current_line.to_h
@state.set_current_line!(advance_offset: advance_offset)
end
def sanitize_section_name(section)
section.to_s.downcase.gsub(/[^a-z0-9]/, '-')
end
end
end
end
end
|