@@ -40,18 +40,23 @@ def self.for_command(target, stdout, stderr, exit_code, action, command)
4040 end
4141
4242 def self . for_task ( target , stdout , stderr , exit_code , task )
43- begin
44- value = JSON . parse ( stdout )
45- unless value . is_a? Hash
46- value = nil
47- end
48- rescue JSON ::ParserError
49- value = nil
50- end
51- value ||= { '_output' => stdout }
43+ stdout . force_encoding ( 'utf-8' ) unless stdout . encoding == Encoding ::UTF_8
44+ value = if stdout . valid_encoding?
45+ parse_hash ( stdout ) || { '_output' => stdout }
46+ else
47+ { '_error' => { 'kind' => 'puppetlabs.tasks/task-error' ,
48+ 'issue_code' => 'TASK_ERROR' ,
49+ 'msg' => 'The task result contained invalid UTF-8 on stdout' ,
50+ 'details' => { } } }
51+ end
52+
5253 if exit_code != 0 && value [ '_error' ] . nil?
5354 msg = if stdout . empty?
54- "The task failed with exit code #{ exit_code } :\n #{ stderr } "
55+ if stderr . empty?
56+ "The task failed with exit code #{ exit_code } and no output"
57+ else
58+ "The task failed with exit code #{ exit_code } and no stdout, but stderr contained:\n #{ stderr } "
59+ end
5560 else
5661 "The task failed with exit code #{ exit_code } "
5762 end
@@ -63,6 +68,13 @@ def self.for_task(target, stdout, stderr, exit_code, task)
6368 new ( target , value : value , action : 'task' , object : task )
6469 end
6570
71+ def self . parse_hash ( string )
72+ value = JSON . parse ( string )
73+ value if value . is_a? Hash
74+ rescue JSON ::ParserError
75+ nil
76+ end
77+
6678 def self . for_upload ( target , source , destination )
6779 new ( target , message : "Uploaded '#{ source } ' to '#{ target . host } :#{ destination } '" , action : 'upload' , object : source )
6880 end
@@ -110,18 +122,8 @@ def message?
110122 message && !message . strip . empty?
111123 end
112124
113- def status_hash
114- {
115- target : @target . name ,
116- action : action ,
117- object : object ,
118- status : status ,
119- value : @value
120- }
121- end
122-
123125 def generic_value
124- value . reject { |k , _ | %w[ _error _output ] . include? k }
126+ safe_value . reject { |k , _ | %w[ _error _output ] . include? k }
125127 end
126128
127129 def eql? ( other )
@@ -139,15 +141,36 @@ def ==(other)
139141 end
140142
141143 def to_json ( opts = nil )
142- status_hash . to_json ( opts )
144+ to_data . to_json ( opts )
143145 end
144146
145147 def to_s
146148 to_json
147149 end
148150
151+ # This is the value with all non-UTF-8 characters removed, suitable for
152+ # printing or converting to JSON. It *should* only be possible to have
153+ # non-UTF-8 characters in stdout/stderr keys as they are not allowed from
154+ # tasks but we scrub the whole thing just in case.
155+ def safe_value
156+ Bolt ::Util . walk_vals ( value ) do |val |
157+ if val . is_a? ( String )
158+ # Replace invalid bytes with hex codes, ie. \xDE\xAD\xBE\xEF
159+ val . scrub { |c | c . bytes . map { |b | "\\ x" + b . to_s ( 16 ) . upcase } . join }
160+ else
161+ val
162+ end
163+ end
164+ end
165+
149166 def to_data
150- Bolt ::Util . walk_keys ( status_hash , &:to_s )
167+ {
168+ "target" => @target . name ,
169+ "action" => action ,
170+ "object" => object ,
171+ "status" => status ,
172+ "value" => safe_value
173+ }
151174 end
152175
153176 def status
0 commit comments