summaryrefslogtreecommitdiff
path: root/lib/gitlab/ci/ansi2json/converter.rb
blob: 53adaf38b8783fa076850cd3897d55d9e417f252 (plain)
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
# 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

          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 << '&lt;'
            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