diff --git a/acceptance/tests/resource/scheduled_task/should_destroy.rb b/acceptance/tests/resource/scheduled_task/should_destroy.rb index e61caa45b..8fdbb959c 100644 --- a/acceptance/tests/resource/scheduled_task/should_destroy.rb +++ b/acceptance/tests/resource/scheduled_task/should_destroy.rb @@ -1,27 +1,35 @@ test_name "should delete a scheduled task" name = "pl#{rand(999999).to_i}" confine :to, :platform => 'windows' agents.each do |agent| # Have to use /v1 parameter for Vista and later, older versions # don't accept the parameter version = '/v1' # query only supports /tn parameter on Vista and later query_cmd = "schtasks.exe /query /v /fo list /tn #{name}" - on agents, facter('kernelmajversion') do + on agent, facter('kernelmajversion') do if stdout.chomp.to_f < 6.0 version = '' - query_cmd = "schtasks.exe /query /v /fo list | grep -vq #{name}" + query_cmd = "schtasks.exe /query /v /fo list | grep #{name}" end end step "create the task" on agent, "schtasks.exe /create #{version} /tn #{name} /tr c:\\\\windows\\\\system32\\\\notepad.exe /sc daily /ru system" step "delete the task" on agent, puppet_resource('scheduled_task', name, 'ensure=absent') step "verify the task was deleted" - on agent, query_cmd + Timeout.timeout(5) do + loop do + step "Win32::TaskScheduler#delete call seems to be asynchronous, so we my need to test multiple times" + on agent, query_cmd, :acceptable_exit_codes => [0,1] + break if exit_code == 1 + sleep 1 + end + end + fail_test "Unable to verify that scheduled task was removed" unless exit_code == 1 end diff --git a/lib/puppet/util/windows.rb b/lib/puppet/util/windows.rb index af440552f..af8a36995 100644 --- a/lib/puppet/util/windows.rb +++ b/lib/puppet/util/windows.rb @@ -1,16 +1,17 @@ module Puppet::Util::Windows if Puppet::Util::Platform.windows? # these reference platform specific gems require 'puppet/util/windows/error' require 'puppet/util/windows/sid' require 'puppet/util/windows/security' require 'puppet/util/windows/user' require 'puppet/util/windows/process' + require 'puppet/util/windows/string' require 'puppet/util/windows/file' require 'puppet/util/windows/root_certs' require 'puppet/util/windows/access_control_entry' require 'puppet/util/windows/access_control_list' require 'puppet/util/windows/security_descriptor' end require 'puppet/util/windows/registry' end diff --git a/lib/puppet/util/windows/file.rb b/lib/puppet/util/windows/file.rb index d43edd4d3..6e124b812 100644 --- a/lib/puppet/util/windows/file.rb +++ b/lib/puppet/util/windows/file.rb @@ -1,241 +1,264 @@ require 'puppet/util/windows' module Puppet::Util::Windows::File require 'ffi' require 'windows/api' - require 'windows/wide_string' - ReplaceFileWithoutBackupW = Windows::API.new('ReplaceFileW', 'PPVLVV', 'B') def replace_file(target, source) - result = ReplaceFileWithoutBackupW.call(WideString.new(target.to_s), - WideString.new(source.to_s), - 0, 0x1, 0, 0) - return true unless result == 0 + target_encoded = Puppet::Util::Windows::String.wide_string(target.to_s) + source_encoded = Puppet::Util::Windows::String.wide_string(source.to_s) + + flags = 0x1 + backup_file = nil + result = API.replace_file( + target_encoded, + source_encoded, + backup_file, + flags, + 0, + 0 + ) + + return true if result raise Puppet::Util::Windows::Error.new("ReplaceFile(#{target}, #{source})") end module_function :replace_file MoveFileEx = Windows::API.new('MoveFileExW', 'PPL', 'B') def move_file_ex(source, target, flags = 0) - result = MoveFileEx.call(WideString.new(source.to_s), - WideString.new(target.to_s), + result = MoveFileEx.call(Puppet::Util::Windows::String.wide_string(source.to_s), + Puppet::Util::Windows::String.wide_string(target.to_s), flags) return true unless result == 0 raise Puppet::Util::Windows::Error. new("MoveFileEx(#{source}, #{target}, #{flags.to_s(8)})") end module_function :move_file_ex module API extend FFI::Library ffi_lib 'kernel32' ffi_convention :stdcall + # http://msdn.microsoft.com/en-us/library/windows/desktop/aa365512(v=vs.85).aspx + # BOOL WINAPI ReplaceFile( + # _In_ LPCTSTR lpReplacedFileName, + # _In_ LPCTSTR lpReplacementFileName, + # _In_opt_ LPCTSTR lpBackupFileName, + # _In_ DWORD dwReplaceFlags - 0x1 REPLACEFILE_WRITE_THROUGH, + # 0x2 REPLACEFILE_IGNORE_MERGE_ERRORS, + # 0x4 REPLACEFILE_IGNORE_ACL_ERRORS + # _Reserved_ LPVOID lpExclude, + # _Reserved_ LPVOID lpReserved + # ); + attach_function :replace_file, :ReplaceFileW, + [:buffer_in, :buffer_in, :buffer_in, :uint, :uint, :uint], :bool + # BOOLEAN WINAPI CreateSymbolicLink( # _In_ LPTSTR lpSymlinkFileName, - symbolic link to be created # _In_ LPTSTR lpTargetFileName, - name of target for symbolic link # _In_ DWORD dwFlags - 0x0 target is a file, 0x1 target is a directory # ); # rescue on Windows < 6.0 so that code doesn't explode begin attach_function :create_symbolic_link, :CreateSymbolicLinkW, [:buffer_in, :buffer_in, :uint], :bool rescue LoadError end # DWORD WINAPI GetFileAttributes( # _In_ LPCTSTR lpFileName # ); attach_function :get_file_attributes, :GetFileAttributesW, [:buffer_in], :uint # HANDLE WINAPI CreateFile( # _In_ LPCTSTR lpFileName, # _In_ DWORD dwDesiredAccess, # _In_ DWORD dwShareMode, # _In_opt_ LPSECURITY_ATTRIBUTES lpSecurityAttributes, # _In_ DWORD dwCreationDisposition, # _In_ DWORD dwFlagsAndAttributes, # _In_opt_ HANDLE hTemplateFile # ); attach_function :create_file, :CreateFileW, [:buffer_in, :uint, :uint, :pointer, :uint, :uint, :uint], :uint # http://msdn.microsoft.com/en-us/library/windows/desktop/aa363216(v=vs.85).aspx # BOOL WINAPI DeviceIoControl( # _In_ HANDLE hDevice, # _In_ DWORD dwIoControlCode, # _In_opt_ LPVOID lpInBuffer, # _In_ DWORD nInBufferSize, # _Out_opt_ LPVOID lpOutBuffer, # _In_ DWORD nOutBufferSize, # _Out_opt_ LPDWORD lpBytesReturned, # _Inout_opt_ LPOVERLAPPED lpOverlapped # ); attach_function :device_io_control, :DeviceIoControl, [:uint, :uint, :pointer, :uint, :pointer, :uint, :pointer, :pointer], :bool MAXIMUM_REPARSE_DATA_BUFFER_SIZE = 16384 # REPARSE_DATA_BUFFER # http://msdn.microsoft.com/en-us/library/cc232006.aspx # http://msdn.microsoft.com/en-us/library/windows/hardware/ff552012(v=vs.85).aspx # struct is always MAXIMUM_REPARSE_DATA_BUFFER_SIZE bytes class ReparseDataBuffer < FFI::Struct layout :reparse_tag, :uint, :reparse_data_length, :ushort, :reserved, :ushort, :substitute_name_offset, :ushort, :substitute_name_length, :ushort, :print_name_offset, :ushort, :print_name_length, :ushort, :flags, :uint, # max less above fields dword / uint 4 bytes, ushort 2 bytes :path_buffer, [:uchar, MAXIMUM_REPARSE_DATA_BUFFER_SIZE - 20] end # BOOL WINAPI CloseHandle( # _In_ HANDLE hObject # ); attach_function :close_handle, :CloseHandle, [:uint], :bool end def symlink(target, symlink) flags = File.directory?(target) ? 0x1 : 0x0 - result = API.create_symbolic_link(WideString.new(symlink.to_s), - WideString.new(target.to_s), flags) + result = API.create_symbolic_link(Puppet::Util::Windows::String.wide_string(symlink.to_s), + Puppet::Util::Windows::String.wide_string(target.to_s), flags) return true if result raise Puppet::Util::Windows::Error.new( "CreateSymbolicLink(#{symlink}, #{target}, #{flags.to_s(8)})") end module_function :symlink INVALID_FILE_ATTRIBUTES = 0xFFFFFFFF #define INVALID_FILE_ATTRIBUTES (DWORD (-1)) def self.get_file_attributes(file_name) - result = API.get_file_attributes(WideString.new(file_name.to_s)) + result = API.get_file_attributes(Puppet::Util::Windows::String.wide_string(file_name.to_s)) return result unless result == INVALID_FILE_ATTRIBUTES raise Puppet::Util::Windows::Error.new("GetFileAttributes(#{file_name})") end INVALID_HANDLE_VALUE = -1 #define INVALID_HANDLE_VALUE ((HANDLE)(LONG_PTR)-1) def self.create_file(file_name, desired_access, share_mode, security_attributes, creation_disposition, flags_and_attributes, template_file_handle) - result = API.create_file(WideString.new(file_name.to_s), + result = API.create_file(Puppet::Util::Windows::String.wide_string(file_name.to_s), desired_access, share_mode, security_attributes, creation_disposition, flags_and_attributes, template_file_handle) return result unless result == INVALID_HANDLE_VALUE raise Puppet::Util::Windows::Error.new( "CreateFile(#{file_name}, #{desired_access.to_s(8)}, #{share_mode.to_s(8)}, " + "#{security_attributes}, #{creation_disposition.to_s(8)}, " + "#{flags_and_attributes.to_s(8)}, #{template_file_handle})") end def self.device_io_control(handle, io_control_code, in_buffer = nil, out_buffer = nil) if out_buffer.nil? raise Puppet::Util::Windows::Error.new("out_buffer is required") end result = API.device_io_control( handle, io_control_code, in_buffer, in_buffer.nil? ? 0 : in_buffer.size, out_buffer, out_buffer.size, FFI::MemoryPointer.new(:uint, 1), nil ) return out_buffer if result raise Puppet::Util::Windows::Error.new( "DeviceIoControl(#{handle}, #{io_control_code}, " + "#{in_buffer}, #{in_buffer ? in_buffer.size : ''}, " + "#{out_buffer}, #{out_buffer ? out_buffer.size : ''}") end FILE_ATTRIBUTE_REPARSE_POINT = 0x400 def symlink?(file_name) begin attributes = get_file_attributes(file_name) (attributes & FILE_ATTRIBUTE_REPARSE_POINT) == FILE_ATTRIBUTE_REPARSE_POINT rescue # raised INVALID_FILE_ATTRIBUTES is equivalent to file not found false end end module_function :symlink? GENERIC_READ = 0x80000000 FILE_SHARE_READ = 1 OPEN_EXISTING = 3 FILE_FLAG_OPEN_REPARSE_POINT = 0x00200000 FILE_FLAG_BACKUP_SEMANTICS = 0x02000000 def self.open_symlink(link_name) begin yield handle = create_file( - WideString.new(link_name.to_s), + Puppet::Util::Windows::String.wide_string(link_name.to_s), GENERIC_READ, FILE_SHARE_READ, nil, # security_attributes OPEN_EXISTING, FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS, 0) # template_file ensure API.close_handle(handle) if handle end end def readlink(link_name) open_symlink(link_name) do |handle| resolve_symlink(handle) end end module_function :readlink def stat(file_name) file_name = file_name.to_s # accomodate PathName or String stat = File.stat(file_name) if symlink?(file_name) link_ftype = File.stat(readlink(file_name)).ftype # sigh, monkey patch instance method for instance, and close over link_ftype singleton_class = class << stat; self; end singleton_class.send(:define_method, :ftype) do link_ftype end end stat end module_function :stat def lstat(file_name) file_name = file_name.to_s # accomodate PathName or String # monkey'ing around! stat = File.lstat(file_name) if symlink?(file_name) def stat.ftype "link" end end stat end module_function :lstat private # http://msdn.microsoft.com/en-us/library/windows/desktop/aa364571(v=vs.85).aspx FSCTL_GET_REPARSE_POINT = 0x900a8 def self.resolve_symlink(handle) # must be multiple of 1024, min 10240 out_buffer = FFI::MemoryPointer.new(API::ReparseDataBuffer.size) device_io_control(handle, FSCTL_GET_REPARSE_POINT, nil, out_buffer) reparse_data = API::ReparseDataBuffer.new(out_buffer) offset = reparse_data[:print_name_offset] length = reparse_data[:print_name_length] result = reparse_data[:path_buffer].to_a[offset, length].pack('C*') result.force_encoding('UTF-16LE').encode(Encoding.default_external) end end diff --git a/lib/puppet/util/windows/process.rb b/lib/puppet/util/windows/process.rb index 165e90af0..c1f0bedd4 100644 --- a/lib/puppet/util/windows/process.rb +++ b/lib/puppet/util/windows/process.rb @@ -1,235 +1,238 @@ require 'puppet/util/windows' require 'windows/process' require 'windows/handle' require 'windows/synchronize' module Puppet::Util::Windows::Process extend ::Windows::Process extend ::Windows::Handle extend ::Windows::Synchronize module API require 'ffi' extend FFI::Library ffi_convention :stdcall ffi_lib 'kernel32' # http://msdn.microsoft.com/en-us/library/windows/desktop/ms683179(v=vs.85).aspx # HANDLE WINAPI GetCurrentProcess(void); attach_function :get_current_process, :GetCurrentProcess, [], :uint # BOOL WINAPI CloseHandle( # _In_ HANDLE hObject # ); attach_function :close_handle, :CloseHandle, [:uint], :bool ffi_lib 'advapi32' # http://msdn.microsoft.com/en-us/library/windows/desktop/aa379295(v=vs.85).aspx # BOOL WINAPI OpenProcessToken( # _In_ HANDLE ProcessHandle, # _In_ DWORD DesiredAccess, # _Out_ PHANDLE TokenHandle # ); attach_function :open_process_token, :OpenProcessToken, [:uint, :uint, :pointer], :bool # http://msdn.microsoft.com/en-us/library/windows/desktop/aa379261(v=vs.85).aspx # typedef struct _LUID { # DWORD LowPart; # LONG HighPart; # } LUID, *PLUID; class LUID < FFI::Struct layout :low_part, :uint, :high_part, :int end # http://msdn.microsoft.com/en-us/library/Windows/desktop/aa379180(v=vs.85).aspx # BOOL WINAPI LookupPrivilegeValue( # _In_opt_ LPCTSTR lpSystemName, # _In_ LPCTSTR lpName, # _Out_ PLUID lpLuid # ); - attach_function :lookup_privilege_value, :LookupPrivilegeValueW, - [:buffer_in, :buffer_in, :pointer], :bool + attach_function :lookup_privilege_value, :LookupPrivilegeValueA, + [:string, :string, :pointer], :bool Token_Information = enum( :token_user, 1, :token_groups, :token_privileges, :token_owner, :token_primary_group, :token_default_dacl, :token_source, :token_type, :token_impersonation_level, :token_statistics, :token_restricted_sids, :token_session_id, :token_groups_and_privileges, :token_session_reference, :token_sandbox_inert, :token_audit_policy, :token_origin, :token_elevation_type, :token_linked_token, :token_elevation, :token_has_restrictions, :token_access_information, :token_virtualization_allowed, :token_virtualization_enabled, :token_integrity_level, :token_ui_access, :token_mandatory_policy, :token_logon_sid, :max_token_info_class ) # http://msdn.microsoft.com/en-us/library/windows/desktop/aa379263(v=vs.85).aspx # typedef struct _LUID_AND_ATTRIBUTES { # LUID Luid; # DWORD Attributes; # } LUID_AND_ATTRIBUTES, *PLUID_AND_ATTRIBUTES; class LUID_And_Attributes < FFI::Struct layout :luid, LUID, :attributes, :uint end # http://msdn.microsoft.com/en-us/library/windows/desktop/aa379630(v=vs.85).aspx # typedef struct _TOKEN_PRIVILEGES { # DWORD PrivilegeCount; # LUID_AND_ATTRIBUTES Privileges[ANYSIZE_ARRAY]; # } TOKEN_PRIVILEGES, *PTOKEN_PRIVILEGES; class Token_Privileges < FFI::Struct layout :privilege_count, :uint, :privileges, [LUID_And_Attributes, 1] # placeholder for offset end # http://msdn.microsoft.com/en-us/library/windows/desktop/aa446671(v=vs.85).aspx # BOOL WINAPI GetTokenInformation( # _In_ HANDLE TokenHandle, # _In_ TOKEN_INFORMATION_CLASS TokenInformationClass, # _Out_opt_ LPVOID TokenInformation, # _In_ DWORD TokenInformationLength, # _Out_ PDWORD ReturnLength # ); attach_function :get_token_information, :GetTokenInformation, [:uint, Token_Information, :pointer, :uint, :pointer ], :bool end def execute(command, arguments, stdin, stdout, stderr) Process.create( :command_line => command, :startup_info => {:stdin => stdin, :stdout => stdout, :stderr => stderr}, :close_handles => false ) end module_function :execute def wait_process(handle) while WaitForSingleObject(handle, 0) == Windows::Synchronize::WAIT_TIMEOUT sleep(1) end exit_status = [0].pack('L') unless GetExitCodeProcess(handle, exit_status) raise Puppet::Util::Windows::Error.new("Failed to get child process exit code") end exit_status = exit_status.unpack('L').first # $CHILD_STATUS is not set when calling win32/process Process.create # and since it's read-only, we can't set it. But we can execute a # a shell that simply returns the desired exit status, which has the # desired effect. %x{#{ENV['COMSPEC']} /c exit #{exit_status}} exit_status end module_function :wait_process def get_current_process # this pseudo-handle does not require closing per MSDN docs API.get_current_process end module_function :get_current_process def open_process_token(handle, desired_access) token_handle_ptr = FFI::MemoryPointer.new(:uint, 1) result = API.open_process_token(handle, desired_access, token_handle_ptr) if !result raise Puppet::Util::Windows::Error.new( "OpenProcessToken(#{handle}, #{desired_access.to_s(8)}, #{token_handle_ptr})") end begin yield token_handle = token_handle_ptr.read_uint ensure API.close_handle(token_handle) end end module_function :open_process_token def lookup_privilege_value(name, system_name = '') luid = FFI::MemoryPointer.new(API::LUID.size) - result = API.lookup_privilege_value(WideString.new(system_name), - WideString.new(name.to_s), luid) + result = API.lookup_privilege_value( + system_name, + name.to_s, + luid + ) return API::LUID.new(luid) if result raise Puppet::Util::Windows::Error.new( "LookupPrivilegeValue(#{system_name}, #{name}, #{luid})") end module_function :lookup_privilege_value def get_token_information(token_handle, token_information) # to determine buffer size return_length_ptr = FFI::MemoryPointer.new(:uint, 1) result = API.get_token_information(token_handle, token_information, nil, 0, return_length_ptr) return_length = return_length_ptr.read_uint if return_length <= 0 raise Puppet::Util::Windows::Error.new( "GetTokenInformation(#{token_handle}, #{token_information}, nil, 0, #{return_length_ptr})") end # re-call API with properly sized buffer for all results token_information_buf = FFI::MemoryPointer.new(return_length) result = API.get_token_information(token_handle, token_information, token_information_buf, return_length, return_length_ptr) if !result raise Puppet::Util::Windows::Error.new( "GetTokenInformation(#{token_handle}, #{token_information}, #{token_information_buf}, " + "#{return_length}, #{return_length_ptr})") end raw_privileges = API::Token_Privileges.new(token_information_buf) privileges = { :count => raw_privileges[:privilege_count], :privileges => [] } offset = token_information_buf + API::Token_Privileges.offset_of(:privileges) privilege_ptr = FFI::Pointer.new(API::LUID_And_Attributes, offset) # extract each instance of LUID_And_Attributes 0.upto(privileges[:count] - 1) do |i| privileges[:privileges] << API::LUID_And_Attributes.new(privilege_ptr[i]) end privileges end module_function :get_token_information TOKEN_ALL_ACCESS = 0xF01FF ERROR_NO_SUCH_PRIVILEGE = 1313 def process_privilege_symlink? handle = get_current_process open_process_token(handle, TOKEN_ALL_ACCESS) do |token_handle| luid = lookup_privilege_value('SeCreateSymbolicLinkPrivilege') token_info = get_token_information(token_handle, :token_privileges) token_info[:privileges].any? { |p| p[:luid].values == luid.values } end rescue Puppet::Util::Windows::Error => e if e.code == ERROR_NO_SUCH_PRIVILEGE false # pre-Vista else raise e end end module_function :process_privilege_symlink? end diff --git a/lib/puppet/util/windows/string.rb b/lib/puppet/util/windows/string.rb new file mode 100644 index 000000000..13d9839d1 --- /dev/null +++ b/lib/puppet/util/windows/string.rb @@ -0,0 +1,14 @@ +require 'puppet/util/windows' + +module Puppet::Util::Windows::String + def wide_string(str) + # ruby (< 2.1) does not respect multibyte terminators, so it is possible + # for a string to contain a single trailing null byte, followed by garbage + # causing buffer overruns. + # + # See http://svn.ruby-lang.org/cgi-bin/viewvc.cgi?revision=41920&view=revision + newstr = str + "\0".encode(str.encoding) + newstr.encode!('UTF-16LE') + end + module_function :wide_string +end diff --git a/spec/integration/util_spec.rb b/spec/integration/util_spec.rb index 8de64cc6c..d8d96aad8 100755 --- a/spec/integration/util_spec.rb +++ b/spec/integration/util_spec.rb @@ -1,80 +1,111 @@ #!/usr/bin/env ruby require 'spec_helper' describe Puppet::Util do include PuppetSpec::Files describe "#execute" do it "should properly allow stdout and stderr to share a file" do command = "ruby -e '(1..10).each {|i| (i%2==0) ? $stdout.puts(i) : $stderr.puts(i)}'" Puppet::Util::Execution.execute(command, :combine => true).split.should =~ [*'1'..'10'] end it "should return output and set $CHILD_STATUS" do command = "ruby -e 'puts \"foo\"; exit 42'" output = Puppet::Util::Execution.execute(command, {:failonfail => false}) output.should == "foo\n" $CHILD_STATUS.exitstatus.should == 42 end it "should raise an error if non-zero exit status is returned" do command = "ruby -e 'exit 43'" expect { Puppet::Util::Execution.execute(command) }.to raise_error(Puppet::ExecutionFailure, /Execution of '#{command}' returned 43: /) $CHILD_STATUS.exitstatus.should == 43 end it "replace_file should preserve original ACEs from existing replaced file on Windows", :if => Puppet.features.microsoft_windows? do file = tmpfile("somefile") FileUtils.touch(file) admins = 'S-1-5-32-544' dacl = Puppet::Util::Windows::AccessControlList.new dacl.allow(admins, Windows::File::FILE_ALL_ACCESS) protect = true expected_sd = Puppet::Util::Windows::SecurityDescriptor.new(admins, admins, dacl, protect) Puppet::Util::Windows::Security.set_security_descriptor(file, expected_sd) ignored_mode = 0644 Puppet::Util.replace_file(file, ignored_mode) do |temp_file| ignored_sd = Puppet::Util::Windows::Security.get_security_descriptor(temp_file.path) users = 'S-1-5-11' ignored_sd.dacl.allow(users, Windows::File::FILE_GENERIC_READ) Puppet::Util::Windows::Security.set_security_descriptor(temp_file.path, ignored_sd) end replaced_sd = Puppet::Util::Windows::Security.get_security_descriptor(file) replaced_sd.dacl.should == expected_sd.dacl end it "replace_file should use reasonable default ACEs on a new file on Windows", :if => Puppet.features.microsoft_windows? do dir = tmpdir('DACL_playground') protected_sd = Puppet::Util::Windows::Security.get_security_descriptor(dir) protected_sd.protect = true Puppet::Util::Windows::Security.set_security_descriptor(dir, protected_sd) sibling_path = File.join(dir, 'sibling_file') FileUtils.touch(sibling_path) expected_sd = Puppet::Util::Windows::Security.get_security_descriptor(sibling_path) new_file_path = File.join(dir, 'new_file') ignored_mode = nil Puppet::Util.replace_file(new_file_path, ignored_mode) { |tmp_file| } new_sd = Puppet::Util::Windows::Security.get_security_descriptor(new_file_path) new_sd.dacl.should == expected_sd.dacl end end + + it "replace_file should work with filenames that include - and . (PUP-1389)", :if => Puppet.features.microsoft_windows? do + expected_content = 'some content' + dir = tmpdir('ReplaceFile_playground') + destination_file = File.join(dir, 'some-file.xml') + + Puppet::Util.replace_file(destination_file, nil) do |temp_file| + temp_file.open + temp_file.write(expected_content) + end + + actual_content = File.read(destination_file) + actual_content.should == expected_content + end + + it "replace_file should work with filenames that include special characters (PUP-1389)", :if => Puppet.features.microsoft_windows? do + expected_content = 'some content' + dir = tmpdir('ReplaceFile_playground') + # http://www.fileformat.info/info/unicode/char/00e8/index.htm + # dest_name = "somèfile.xml" + dest_name = "som\u00E8file.xml" + destination_file = File.join(dir, dest_name) + + Puppet::Util.replace_file(destination_file, nil) do |temp_file| + temp_file.open + temp_file.write(expected_content) + end + + actual_content = File.read(destination_file) + actual_content.should == expected_content + end end diff --git a/spec/unit/util/windows/string_spec.rb b/spec/unit/util/windows/string_spec.rb new file mode 100644 index 000000000..60f7e6449 --- /dev/null +++ b/spec/unit/util/windows/string_spec.rb @@ -0,0 +1,54 @@ +# encoding: UTF-8 +#!/usr/bin/env ruby + +require 'spec_helper' +require 'puppet/util/windows' + +describe "Puppet::Util::Windows::String", :if => Puppet.features.microsoft_windows? do + UTF16_NULL = [0, 0] + + def wide_string(str) + Puppet::Util::Windows::String.wide_string(str) + end + + def converts_to_wide_string(string_value) + expected = string_value.encode(Encoding::UTF_16LE) + expected_bytes = expected.bytes.to_a + UTF16_NULL + + wide_string(string_value).bytes.to_a.should == expected_bytes + end + + context "wide_string" do + it "should return encoding of UTF-16LE" do + wide_string("bob").encoding.should == Encoding::UTF_16LE + end + + it "should return valid encoding" do + wide_string("bob").valid_encoding?.should be_true + end + + it "should convert an ASCII string" do + converts_to_wide_string("bob".encode(Encoding::US_ASCII)) + end + + it "should convert a UTF-8 string" do + converts_to_wide_string("bob".encode(Encoding::UTF_8)) + end + + it "should convert a UTF-16LE string" do + converts_to_wide_string("bob\u00E8".encode(Encoding::UTF_16LE)) + end + + it "should convert a UTF-16BE string" do + converts_to_wide_string("bob\u00E8".encode(Encoding::UTF_16BE)) + end + + it "should convert an UTF-32LE string" do + converts_to_wide_string("bob\u00E8".encode(Encoding::UTF_32LE)) + end + + it "should convert an UTF-32BE string" do + converts_to_wide_string("bob\u00E8".encode(Encoding::UTF_32BE)) + end + end +end