puppet 4.10.4 → 4.10.5

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of puppet might be problematic. Click here for more details.

Files changed (70) hide show
  1. checksums.yaml +7 -0
  2. data/lib/puppet/application/lookup.rb +2 -2
  3. data/lib/puppet/configurer/fact_handler.rb +1 -1
  4. data/lib/puppet/face/epp.rb +26 -24
  5. data/lib/puppet/file_serving/metadata.rb +2 -2
  6. data/lib/puppet/forge.rb +5 -3
  7. data/lib/puppet/forge/cache.rb +1 -0
  8. data/lib/puppet/forge/repository.rb +2 -1
  9. data/lib/puppet/indirector/catalog/compiler.rb +4 -3
  10. data/lib/puppet/indirector/request.rb +9 -8
  11. data/lib/puppet/module.rb +30 -0
  12. data/lib/puppet/network/http/rack/rest.rb +2 -1
  13. data/lib/puppet/parser/compiler.rb +4 -0
  14. data/lib/puppet/parser/functions/assert_type.rb +1 -1
  15. data/lib/puppet/parser/functions/binary_file.rb +1 -1
  16. data/lib/puppet/parser/functions/break.rb +1 -1
  17. data/lib/puppet/parser/functions/defined.rb +1 -1
  18. data/lib/puppet/parser/functions/dig.rb +1 -1
  19. data/lib/puppet/parser/functions/each.rb +1 -1
  20. data/lib/puppet/parser/functions/epp.rb +1 -1
  21. data/lib/puppet/parser/functions/filter.rb +1 -1
  22. data/lib/puppet/parser/functions/find_file.rb +1 -1
  23. data/lib/puppet/parser/functions/inline_epp.rb +1 -1
  24. data/lib/puppet/parser/functions/lest.rb +1 -1
  25. data/lib/puppet/parser/functions/map.rb +1 -1
  26. data/lib/puppet/parser/functions/match.rb +1 -1
  27. data/lib/puppet/parser/functions/new.rb +1 -1
  28. data/lib/puppet/parser/functions/next.rb +1 -1
  29. data/lib/puppet/parser/functions/reduce.rb +1 -1
  30. data/lib/puppet/parser/functions/return.rb +1 -1
  31. data/lib/puppet/parser/functions/reverse_each.rb +1 -1
  32. data/lib/puppet/parser/functions/slice.rb +1 -1
  33. data/lib/puppet/parser/functions/step.rb +1 -1
  34. data/lib/puppet/parser/functions/strftime.rb +1 -1
  35. data/lib/puppet/parser/functions/then.rb +1 -1
  36. data/lib/puppet/parser/functions/type.rb +1 -1
  37. data/lib/puppet/parser/functions/with.rb +1 -1
  38. data/lib/puppet/pops/evaluator/collector_transformer.rb +2 -2
  39. data/lib/puppet/pops/evaluator/collectors/catalog_collector.rb +2 -2
  40. data/lib/puppet/pops/evaluator/collectors/exported_collector.rb +2 -2
  41. data/lib/puppet/pops/merge_strategy.rb +1 -1
  42. data/lib/puppet/provider/nameservice.rb +4 -2
  43. data/lib/puppet/reports/http.rb +4 -2
  44. data/lib/puppet/resource/capability_finder.rb +1 -1
  45. data/lib/puppet/type/file/source.rb +9 -3
  46. data/lib/puppet/util.rb +122 -2
  47. data/lib/puppet/util/execution.rb +1 -1
  48. data/lib/puppet/util/rdoc/generators/puppet_generator.rb +1 -1
  49. data/lib/puppet/version.rb +1 -1
  50. data/locales/puppet.pot +12 -4
  51. data/spec/integration/application/apply_spec.rb +10 -0
  52. data/spec/integration/type/file_spec.rb +29 -0
  53. data/spec/integration/util/execution_spec.rb +8 -0
  54. data/spec/unit/application/lookup_spec.rb +1 -1
  55. data/spec/unit/configurer/fact_handler_spec.rb +30 -8
  56. data/spec/unit/face/epp_face_spec.rb +9 -0
  57. data/spec/unit/file_serving/metadata_spec.rb +21 -0
  58. data/spec/unit/forge/forge_spec.rb +112 -0
  59. data/spec/unit/forge/repository_spec.rb +4 -4
  60. data/spec/unit/functions/lookup_spec.rb +26 -0
  61. data/spec/unit/indirector/catalog/compiler_spec.rb +1 -1
  62. data/spec/unit/indirector/file_bucket_file/file_spec.rb +3 -3
  63. data/spec/unit/indirector/request_spec.rb +16 -1
  64. data/spec/unit/module_spec.rb +29 -0
  65. data/spec/unit/network/http/api/indirected_routes_spec.rb +3 -3
  66. data/spec/unit/network/http/rack/rest_spec.rb +3 -3
  67. data/spec/unit/type/file_spec.rb +46 -0
  68. data/spec/unit/util_spec.rb +230 -1
  69. data/tasks/parallel.rake +12 -7
  70. metadata +184 -194
@@ -79,5 +79,5 @@ $transformed_data = map(reverse_each($data)) |$item| { $item * 10 }
79
79
 
80
80
  DOC
81
81
  ) do |args|
82
- Error.is4x('reverse_each')
82
+ Puppet::Parser::Functions::Error.is4x('reverse_each')
83
83
  end
@@ -35,5 +35,5 @@ to empty arrays for a hash.
35
35
  - Since 4.0.0
36
36
  DOC
37
37
  ) do |args|
38
- Error.is4x('slice')
38
+ Puppet::Parser::Functions::Error.is4x('slice')
39
39
  end
@@ -80,5 +80,5 @@ $transformed_data contains [0,50,100,150,200]
80
80
 
81
81
  DOC
82
82
  ) do |args|
83
- Error.is4x('step')
83
+ Puppet::Parser::Functions::Error.is4x('step')
84
84
  end
@@ -181,5 +181,5 @@ notice($duration.strftime('%M:%S')) # outputs '200:30'
181
181
  - Since 4.8.0
182
182
  DOC
183
183
  ) do |args|
184
- Error.is4x('strftime')
184
+ Puppet::Parser::Functions::Error.is4x('strftime')
185
185
  end
@@ -69,5 +69,5 @@ was not a String.
69
69
 
70
70
  DOC
71
71
  ) do |args|
72
- Error.is4x('then')
72
+ Puppet::Parser::Functions::Error.is4x('then')
73
73
  end
@@ -49,5 +49,5 @@ function type(Any $value, InferenceFidelity $fidelity = 'detailed') # returns Ty
49
49
 
50
50
  DOC
51
51
  ) do |args|
52
- Error.is4x('type')
52
+ Puppet::Parser::Functions::Error.is4x('type')
53
53
  end
@@ -24,5 +24,5 @@ $check_var = $x
24
24
  - Since 4.0.0
25
25
  DOC
26
26
  ) do |args|
27
- Error.is4x('with')
27
+ Puppet::Parser::Functions::Error.is4x('with')
28
28
  end
@@ -42,10 +42,10 @@ class CollectorTransformer
42
42
 
43
43
  case o.query
44
44
  when Model::VirtualQuery
45
- newcoll = Collectors::CatalogCollector.new(scope, resource_type.name, code, overrides)
45
+ newcoll = Collectors::CatalogCollector.new(scope, resource_type, code, overrides)
46
46
  when Model::ExportedQuery
47
47
  match = match_unless_nop(o.query, scope)
48
- newcoll = Collectors::ExportedCollector.new(scope, resource_type.name, match, code, overrides)
48
+ newcoll = Collectors::ExportedCollector.new(scope, resource_type, match, code, overrides)
49
49
  end
50
50
 
51
51
  scope.compiler.add_collection(newcoll)
@@ -3,14 +3,14 @@ class Puppet::Pops::Evaluator::Collectors::CatalogCollector < Puppet::Pops::Eval
3
3
  # Creates a CatalogCollector using the AbstractCollector's
4
4
  # constructor to set the scope and overrides
5
5
  #
6
- # param [Symbol] type the resource type to be collected
6
+ # param [Puppet::CompilableResourceType] type the resource type to be collected
7
7
  # param [Proc] query the query which defines which resources to match
8
8
  def initialize(scope, type, query, overrides = nil)
9
9
  super(scope, overrides)
10
10
 
11
11
  @query = query
12
12
 
13
- @type = Puppet::Resource.new(type, "whatever").type
13
+ @type = Puppet::Resource.new(type, 'whatever').type
14
14
  end
15
15
 
16
16
  # Collects virtual resources based off a collection in a manifest
@@ -3,7 +3,7 @@ class Puppet::Pops::Evaluator::Collectors::ExportedCollector < Puppet::Pops::Eva
3
3
  # Creates an ExportedCollector using the AbstractCollector's
4
4
  # constructor to set the scope and overrides
5
5
  #
6
- # param [Symbol] type the resource type to be collected
6
+ # param [Puppet::CompilableResourceType] type the resource type to be collected
7
7
  # param [Array] equery an array representation of the query (exported query)
8
8
  # param [Proc] cquery a proc representation of the query (catalog query)
9
9
  def initialize(scope, type, equery, cquery, overrides = nil)
@@ -12,7 +12,7 @@ class Puppet::Pops::Evaluator::Collectors::ExportedCollector < Puppet::Pops::Eva
12
12
  @equery = equery
13
13
  @cquery = cquery
14
14
 
15
- @type = Puppet::Resource.new(type, "whatever").type
15
+ @type = Puppet::Resource.new(type, 'whatever').type
16
16
  end
17
17
 
18
18
  # Ensures that storeconfigs is present before calling AbstractCollector's
@@ -375,7 +375,7 @@ module Puppet::Pops
375
375
  'knockout_prefix=>Optional[String],'\
376
376
  'merge_debug=>Optional[Boolean],'\
377
377
  'merge_hash_arrays=>Optional[Boolean],'\
378
- 'sort_merge_arrays=>Optional[Boolean],'\
378
+ 'sort_merged_arrays=>Optional[Boolean],'\
379
379
  '}]')
380
380
  end
381
381
 
@@ -24,11 +24,13 @@ class Puppet::Provider::NameService < Puppet::Provider
24
24
 
25
25
  def instances
26
26
  objects = []
27
- Puppet::Etc.send("set#{section}ent")
28
27
  begin
29
- while ent = Puppet::Etc.send("get#{section}ent")
28
+ method = Puppet::Etc.method(:"get#{section}ent")
29
+ while ent = method.call
30
30
  objects << new(:name => ent.name, :canonical_name => ent.canonical_name, :ensure => :present)
31
31
  end
32
+ ensure
33
+ Puppet::Etc.send("end#{section}ent")
32
34
  end
33
35
  objects
34
36
  end
@@ -6,8 +6,10 @@ Puppet::Reports.register_report(:http) do
6
6
 
7
7
  desc <<-DESC
8
8
  Send reports via HTTP or HTTPS. This report processor submits reports as
9
- POST requests to the address in the `reporturl` setting. The body of each POST
10
- request is the YAML dump of a Puppet::Transaction::Report object, and the
9
+ POST requests to the address in the `reporturl` setting. When a HTTPS URL
10
+ is used, the remote server must present a certificate issued by the Puppet
11
+ CA or the connection will fail validation. The body of each POST request
12
+ is the YAML dump of a Puppet::Transaction::Report object, and the
11
13
  Content-Type is set as `application/x-yaml`.
12
14
  DESC
13
15
 
@@ -87,7 +87,7 @@ module Puppet::Resource::CapabilityFinder
87
87
  Puppet::Util::Puppetdb.query_puppetdb(["from", "resources", query])
88
88
  # For PuppetDB < 4, use the old internal method action()
89
89
  else
90
- url = "/pdb/query/v4/resource?query=#{CGI.escape(query.to_json)}"
90
+ url = "/pdb/query/v4/resource?query=#{Puppet::Util.uri_query_encode(query.to_json)}"
91
91
  response = Puppet::Util::Puppetdb::Http.action(url) do |conn, uri|
92
92
  conn.get(uri, { 'Accept' => 'application/json'})
93
93
  end
@@ -70,7 +70,7 @@ module Puppet
70
70
  next if Puppet::Util.absolute_path?(source)
71
71
 
72
72
  begin
73
- uri = URI.parse(URI.escape(source))
73
+ uri = URI.parse(Puppet::Util.uri_encode(source))
74
74
  rescue => detail
75
75
  self.fail Puppet::Error, "Could not understand source #{source}: #{detail}", detail
76
76
  end
@@ -91,7 +91,13 @@ module Puppet
91
91
  source = self.class.normalize(source)
92
92
 
93
93
  if Puppet::Util.absolute_path?(source)
94
- URI.unescape(Puppet::Util.path_to_uri(source).to_s)
94
+ # CGI.unescape will butcher properly escaped URIs
95
+ uri_string = Puppet::Util.path_to_uri(source).to_s
96
+ # Ruby 1.9.3 and earlier have a URI bug in URI
97
+ # to_s returns an ASCII string despite UTF-8 fragments
98
+ # since its escaped its safe to universally call encode
99
+ # URI.unescape always returns strings in the original encoding
100
+ URI.unescape(uri_string.encode(Encoding::UTF_8))
95
101
  else
96
102
  source
97
103
  end
@@ -219,7 +225,7 @@ module Puppet
219
225
  end
220
226
 
221
227
  def uri
222
- @uri ||= URI.parse(URI.escape(metadata.source))
228
+ @uri ||= URI.parse(Puppet::Util.uri_encode(metadata.source))
223
229
  end
224
230
 
225
231
  def write(file)
@@ -321,7 +321,12 @@ module Util
321
321
  end
322
322
  end
323
323
 
324
- params[:path] = URI.escape(path)
324
+ # have to split *after* any relevant escaping
325
+ params[:path], params[:query] = uri_encode(path).split('?')
326
+ search_for_fragment = params[:query] ? :query : :path
327
+ if params[search_for_fragment].include?('#')
328
+ params[search_for_fragment], _, params[:fragment] = params[search_for_fragment].rpartition('#')
329
+ end
325
330
 
326
331
  begin
327
332
  URI::Generic.build(params)
@@ -335,7 +340,9 @@ module Util
335
340
  def uri_to_path(uri)
336
341
  return unless uri.is_a?(URI)
337
342
 
338
- path = URI.unescape(uri.path)
343
+ # CGI.unescape doesn't handle space rules properly in uri paths
344
+ # URI.unescape does, but returns strings in their original encoding
345
+ path = URI.unescape(uri.path.encode(Encoding::UTF_8))
339
346
 
340
347
  if Puppet.features.microsoft_windows? and uri.scheme == 'file'
341
348
  if uri.host
@@ -349,6 +356,119 @@ module Util
349
356
  end
350
357
  module_function :uri_to_path
351
358
 
359
+ private
360
+
361
+ RFC_3986_URI_REGEX = /^(?<scheme>([^:\/?#]+):)?(?<authority>\/\/([^\/?#]*))?(?<path>[^?#]*)(\?(?<query>[^#]*))?(#(?<fragment>.*))?$/
362
+
363
+ public
364
+
365
+ # Percent-encodes a URI query parameter per RFC3986 - https://tools.ietf.org/html/rfc3986
366
+ #
367
+ # The output will correctly round-trip through URI.unescape
368
+ #
369
+ # @param [String query_string] A URI query parameter that may contain reserved
370
+ # characters that must be percent encoded for the key or value to be
371
+ # properly decoded as part of a larger query string:
372
+ #
373
+ # query
374
+ # encodes as : query
375
+ #
376
+ # query_with_special=chars like&and * and# plus+this
377
+ # encodes as:
378
+ # query_with_special%3Dchars%20like%26and%20%2A%20and%23%20plus%2Bthis
379
+ #
380
+ # Note: Also usable by fragments, but not suitable for paths
381
+ #
382
+ # @return [String] a new string containing an encoded query string per the
383
+ # rules of RFC3986.
384
+ #
385
+ # In particular,
386
+ # query will encode + as %2B and space as %20
387
+ def uri_query_encode(query_string)
388
+ return nil if query_string.nil?
389
+
390
+ # query can encode space to %20 OR +
391
+ # + MUST be encoded as %2B
392
+ # in RFC3968 both query and fragment are defined as:
393
+ # = *( pchar / "/" / "?" )
394
+ # CGI.escape turns space into + which is the most backward compatible
395
+ # however it doesn't roundtrip through URI.unescape which prefers %20
396
+ CGI.escape(query_string).gsub('+', '%20')
397
+ end
398
+ module_function :uri_query_encode
399
+
400
+ # Percent-encodes a URI string per RFC3986 - https://tools.ietf.org/html/rfc3986
401
+ #
402
+ # Properly handles escaping rules for paths, query strings and fragments
403
+ # independently
404
+ #
405
+ # The output is safe to pass to URI.parse or URI::Generic.build and will
406
+ # correctly round-trip through URI.unescape
407
+ #
408
+ # @param [String path] A URI string that may be in the form of:
409
+ #
410
+ # http://foo.com/bar?query
411
+ # file://tmp/foo bar
412
+ # //foo.com/bar?query
413
+ # /bar?query
414
+ # bar?query
415
+ # bar
416
+ # .
417
+ # C:\Windows\Temp
418
+ #
419
+ # Note that with no specified scheme, authority or query parameter delimiter
420
+ # ? that a naked string will be treated as a path.
421
+ #
422
+ # Note that if query parameters need to contain data such as & or =
423
+ # that this method should not be used, as there is no way to differentiate
424
+ # query parameter data from query delimiters when multiple parameters
425
+ # are specified
426
+ #
427
+ # @param [Hash{Symbol=>String} opts] Options to alter encoding
428
+ # @option opts [Array<Symbol>] :allow_fragment defaults to false. When false
429
+ # will treat # as part of a path or query and not a fragment delimiter
430
+ #
431
+ # @return [String] a new string containing appropriate portions of the URI
432
+ # encoded per the rules of RFC3986.
433
+ # In particular,
434
+ # path will not encode +, but will encode space as %20
435
+ # query will encode + as %2B and space as %20
436
+ # fragment behaves like query
437
+ def uri_encode(path, opts = { :allow_fragment => false })
438
+ raise ArgumentError.new('path may not be nil') if path.nil?
439
+
440
+ # ensure string starts as UTF-8 for the sake of Ruby 1.9.3
441
+ encoded = ''.encode!(Encoding::UTF_8)
442
+
443
+ # parse uri into named matches, then reassemble properly encoded
444
+ parts = path.match(RFC_3986_URI_REGEX)
445
+
446
+ encoded += parts[:scheme] unless parts[:scheme].nil?
447
+ encoded += parts[:authority] unless parts[:authority].nil?
448
+
449
+ # path requires space to be encoded as %20 (NEVER +)
450
+ # + should be left unencoded
451
+ # URI::parse and URI::Generic.build don't like paths encoded with CGI.escape
452
+ # URI.escape does not change / to %2F and : to %3A like CGI.escape
453
+ encoded += URI.escape(parts[:path]) unless parts[:path].nil?
454
+
455
+ # each query parameter
456
+ if !parts[:query].nil?
457
+ query_string = parts[:query].split('&').map do |pair|
458
+ # can optionally be separated by an =
459
+ pair.split('=').map do |v|
460
+ uri_query_encode(v)
461
+ end.join('=')
462
+ end.join('&')
463
+ encoded += '?' + query_string
464
+ end
465
+
466
+ encoded += ((opts[:allow_fragment] ? '#' : '%23') + uri_query_encode(parts[:fragment])) unless parts[:fragment].nil?
467
+
468
+ encoded
469
+ end
470
+ module_function :uri_encode
471
+
352
472
  def safe_posix_fork(stdin=$stdin, stdout=$stdout, stderr=$stderr, &block)
353
473
  child_pid = Kernel.fork do
354
474
  $stdin.reopen(stdin)
@@ -83,7 +83,7 @@ module Puppet::Util::Execution
83
83
  end
84
84
 
85
85
  if failonfail && exitstatus != 0
86
- raise Puppet::ExecutionFailure, output
86
+ raise Puppet::ExecutionFailure, output.to_s
87
87
  end
88
88
 
89
89
  output
@@ -383,7 +383,7 @@ module Generators
383
383
  resources.each do |r|
384
384
  res << {
385
385
  "name" => CGI.escapeHTML(r.name),
386
- "aref" => CGI.escape(path_prefix)+"\#"+CGI.escape(r.aref)
386
+ "aref" => Puppet::Util.uri_encode(path_prefix)+"\#"+Puppet::Util.uri_query_encode(r.aref)
387
387
  }
388
388
  end
389
389
  res
@@ -6,7 +6,7 @@
6
6
  # Raketasks and such to set the version based on the output of `git describe`
7
7
 
8
8
  module Puppet
9
- PUPPETVERSION = '4.10.4'
9
+ PUPPETVERSION = '4.10.5'
10
10
 
11
11
  ##
12
12
  # version is a public API method intended to always provide a fast and
@@ -6,11 +6,11 @@
6
6
  #, fuzzy
7
7
  msgid ""
8
8
  msgstr ""
9
- "Project-Id-Version: Puppet automation framework 4.10.1-82-gd7074ee\n"
9
+ "Project-Id-Version: Puppet automation framework 5.0.0-48-gd9d2edc\n"
10
10
  "\n"
11
11
  "Report-Msgid-Bugs-To: https://tickets.puppetlabs.com\n"
12
- "POT-Creation-Date: 2017-05-25 21:24+0000\n"
13
- "PO-Revision-Date: 2017-05-25 21:24+0000\n"
12
+ "POT-Creation-Date: 2017-07-14 21:19+0000\n"
13
+ "PO-Revision-Date: 2017-07-14 21:19+0000\n"
14
14
  "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
15
15
  "Language-Team: LANGUAGE <LL@li.org>\n"
16
16
  "Language: \n"
@@ -122,6 +122,14 @@ msgstr ""
122
122
  msgid "Existing backup does not match its expected sum, %{sum}. Overwriting corrupted backup."
123
123
  msgstr ""
124
124
 
125
+ #: ../lib/puppet/module.rb:77
126
+ msgid "GettextSetup initialization for %{module_name} failed with: %{error_message}"
127
+ msgstr ""
128
+
129
+ #: ../lib/puppet/module.rb:80
130
+ msgid "GettextSetup is not available, skipping GettextSetup initialization for %{module_name}."
131
+ msgstr ""
132
+
125
133
  #: ../lib/puppet/pops/lookup/environment_data_provider.rb:20
126
134
  msgid "hiera.yaml version 3 found at the environment root was ignored"
127
135
  msgstr ""
@@ -130,7 +138,7 @@ msgstr ""
130
138
  msgid "hiera.yaml version 3 found at module root was ignored"
131
139
  msgstr ""
132
140
 
133
- #: ../lib/puppet/provider/nameservice.rb:56
141
+ #: ../lib/puppet/provider/nameservice.rb:58
134
142
  msgid "listbyname is deprecated and will be removed in a future release of Puppet. Please use `self.instances` to obtain a list of users."
135
143
  msgstr ""
136
144
 
@@ -131,6 +131,16 @@ end
131
131
  expect(notices).to include('false')
132
132
  expect(notices).not_to include('the Puppet::Type says hello')
133
133
  end
134
+
135
+ it 'does not load the ruby type when when referenced from collector during compile' do
136
+ notices = eval_and_collect_notices("@applytest { 'applytest was here': }\nApplytest<| title == 'applytest was here' |>", node)
137
+ expect(notices).not_to include('the Puppet::Type says hello')
138
+ end
139
+
140
+ it 'does not load the ruby type when when referenced from exported collector during compile' do
141
+ notices = eval_and_collect_notices("@@applytest { 'applytest was here': }\nApplytest<<| |>>", node)
142
+ expect(notices).not_to include('the Puppet::Type says hello')
143
+ end
134
144
  end
135
145
  end
136
146
 
@@ -1074,6 +1074,35 @@ describe Puppet::Type.type(:file), :uses_checksums => true do
1074
1074
  expect(File.read(dest)).to eq("foo")
1075
1075
  end
1076
1076
 
1077
+ it "should maintain source URIs as UTF-8 with Unicode characters in their names and be able to copy such files" do
1078
+ # different UTF-8 widths
1079
+ # 1-byte A
1080
+ # 2-byte ۿ - http://www.fileformat.info/info/unicode/char/06ff/index.htm - 0xDB 0xBF / 219 191
1081
+ # 3-byte ᚠ - http://www.fileformat.info/info/unicode/char/16A0/index.htm - 0xE1 0x9A 0xA0 / 225 154 160
1082
+ # 4-byte <U+070E> - http://www.fileformat.info/info/unicode/char/2070E/index.htm - 0xF0 0xA0 0x9C 0x8E / 240 160 156 142
1083
+ mixed_utf8 = "A\u06FF\u16A0\u{2070E}" # Aۿᚠ<U+070E>
1084
+
1085
+ dest = tmpfile("destwith #{mixed_utf8}")
1086
+ source = tmpfile_with_contents("filewith #{mixed_utf8}", "foo")
1087
+ catalog.add_resource described_class.new(:path => dest, :source => source)
1088
+
1089
+ catalog.apply
1090
+
1091
+ # find the resource and verify
1092
+ resource = catalog.resources.first { |r| r.title == "File[#{dest}]" }
1093
+ uri_path = resource.parameters[:source].uri.path
1094
+
1095
+ # note that Windows file:// style URIs get an extra / in front of c:/ like /c:/
1096
+ source_prefix = Puppet.features.microsoft_windows? ? '/' : ''
1097
+
1098
+ # the URI can be round-tripped through unescape
1099
+ expect(URI.unescape(uri_path)).to eq(source_prefix + source)
1100
+ # and is properly UTF-8
1101
+ expect(uri_path.encoding).to eq (Encoding::UTF_8)
1102
+
1103
+ expect(File.read(dest)).to eq('foo')
1104
+ end
1105
+
1077
1106
  it "should be able to copy individual files even if recurse has been specified" do
1078
1107
  source = tmpfile_with_contents("source", "foo")
1079
1108
  dest = tmpfile("dest")