require 'puppet' require 'spec_helper' require 'puppet_spec/character_encoding' # The Ruby::Etc module is largely non-functional on Windows - many methods # simply return nil regardless of input, the Etc::Group struct is not defined, # and Etc::Passwd is missing fields # We want to test that: # - We correctly set external encoding values IF they're valid UTF-8 bytes # - We do not modify non-UTF-8 values if they're NOT valid UTF-8 bytes describe Puppet::Etc, :if => !Puppet::Util::Platform.windows? do # http://www.fileformat.info/info/unicode/char/5e0c/index.htm # 希 Han Character 'rare; hope, expect, strive for' # In EUC_KR: \xfd \xf1 - 253 241 # In UTF-8: \u5e0c - \xe5 \xb8 \x8c - 229 184 140 let(:euc_kr) { [253, 241].pack('C*').force_encoding(Encoding::EUC_KR) } # valid_encoding? == true let(:euc_kr_as_binary) { [253, 241].pack('C*') } # valid_encoding? == true let(:euc_kr_as_utf_8) { [253, 241].pack('C*').force_encoding(Encoding::UTF_8) } # valid_encoding? == false # characters representing different UTF-8 widths # 1-byte A # 2-byte ۿ - http://www.fileformat.info/info/unicode/char/06ff/index.htm - 0xDB 0xBF / 219 191 # 3-byte ᚠ - http://www.fileformat.info/info/unicode/char/16A0/index.htm - 0xE1 0x9A 0xA0 / 225 154 160 # 4-byte 𠜎 - http://www.fileformat.info/info/unicode/char/2070E/index.htm - 0xF0 0xA0 0x9C 0x8E / 240 160 156 142 let(:mixed_utf_8) { "A\u06FF\u16A0\u{2070E}".force_encoding(Encoding::UTF_8) } # Aۿᚠ𠜎 let(:mixed_utf_8_as_binary) { "A\u06FF\u16A0\u{2070E}".force_encoding(Encoding::BINARY) } let(:mixed_utf_8_as_euc_kr) { "A\u06FF\u16A0\u{2070E}".force_encoding(Encoding::EUC_KR) } # An uninteresting value that ruby might return in an Etc struct. let(:root) { 'root' } # Set up example Etc Group structs with values representative of what we would # get back in these encodings let(:utf_8_group_struct) do group = Etc::Group.new # In a UTF-8 environment, these values will come back as UTF-8, even if # they're not valid UTF-8. We do not modify anything about either the # valid or invalid UTF-8 strings. # Group member contains a mix of valid and invalid UTF-8-labeled strings group.mem = [mixed_utf_8, root.dup.force_encoding(Encoding::UTF_8), euc_kr_as_utf_8] # group name contains same EUC_KR bytes labeled as UTF-8 group.name = euc_kr_as_utf_8 # group passwd field is valid UTF-8 group.passwd = mixed_utf_8 group.gid = 12345 group end let(:euc_kr_group_struct) do # In an EUC_KR environment, values will come back as EUC_KR, even if they're # not valid in that encoding. For values that are valid in UTF-8 we expect # their external encoding to be set to UTF-8 by Puppet::Etc. For values that # are invalid in UTF-8, we expect the string to be kept intact, unmodified, # as we can't transcode it. group = Etc::Group.new group.mem = [euc_kr, root.dup.force_encoding(Encoding::EUC_KR), mixed_utf_8_as_euc_kr] group.name = euc_kr group.passwd = mixed_utf_8_as_euc_kr group.gid = 12345 group end let(:ascii_group_struct) do # In a POSIX environment, any strings containing only values under # code-point 128 will be returned as ASCII, whereas anything above that # point will be returned as BINARY. In either case we override the encoding # to UTF-8 if that would be valid. group = Etc::Group.new group.mem = [euc_kr_as_binary, root.dup.force_encoding(Encoding::ASCII), mixed_utf_8_as_binary] group.name = euc_kr_as_binary group.passwd = mixed_utf_8_as_binary group.gid = 12345 group end let(:utf_8_user_struct) do user = Etc::Passwd.new # user name contains same EUC_KR bytes labeled as UTF-8 user.name = euc_kr_as_utf_8 # group passwd field is valid UTF-8 user.passwd = mixed_utf_8 user.uid = 12345 user end let(:euc_kr_user_struct) do user = Etc::Passwd.new user.name = euc_kr user.passwd = mixed_utf_8_as_euc_kr user.uid = 12345 user end let(:ascii_user_struct) do user = Etc::Passwd.new user.name = euc_kr_as_binary user.passwd = mixed_utf_8_as_binary user.uid = 12345 user end shared_examples "methods that return an overridden group struct from Etc" do |param| it "should return a new Struct object with corresponding canonical_ members" do group = Etc::Group.new expect(Etc).to receive(subject).with(param.nil? ? no_args : param).and_return(group) puppet_group = Puppet::Etc.send(subject, *param) expect(puppet_group.members).to include(*group.members) expect(puppet_group.members).to include(*group.members.map { |mem| "canonical_#{mem}".to_sym }) # Confirm we haven't just added the new members to the original struct object, ie this is really a new struct expect(group.members.any? { |elem| elem.match(/^canonical_/) }).to be_falsey end context "when Encoding.default_external is UTF-8" do before do expect(Etc).to receive(subject).with(param.nil? ? no_args : param).and_return(utf_8_group_struct) end let(:overridden) { PuppetSpec::CharacterEncoding.with_external_encoding(Encoding::UTF_8) do Puppet::Etc.send(subject, *param) end } it "should leave the valid UTF-8 values in arrays unmodified" do expect(overridden.mem[0]).to eq(mixed_utf_8) expect(overridden.mem[1]).to eq(root) end it "should replace invalid characters with replacement characters in invalid UTF-8 values in arrays" do expect(overridden.mem[2]).to eq("\uFFFD\uFFFD") end it "should keep an unmodified version of the invalid UTF-8 values in arrays in the corresponding canonical_ member" do expect(overridden.canonical_mem[2]).to eq(euc_kr_as_utf_8) end it "should leave the valid UTF-8 values unmodified" do expect(overridden.passwd).to eq(mixed_utf_8) end it "should replace invalid characters with '?' characters in invalid UTF-8 values" do expect(overridden.name).to eq("\uFFFD\uFFFD") end it "should keep an unmodified version of the invalid UTF-8 values in the corresponding canonical_ member" do expect(overridden.canonical_name).to eq(euc_kr_as_utf_8) end it "should copy all values to the new struct object" do # Confirm we've actually copied all the values to the canonical_members utf_8_group_struct.each_pair do |member, value| expect(overridden["canonical_#{member}"]).to eq(value) # Confirm we've reassigned all non-string and array values if !value.is_a?(String) && !value.is_a?(Array) expect(overridden[member]).to eq(value) expect(overridden[member].object_id).to eq(value.object_id) end end end end context "when Encoding.default_external is EUC_KR (i.e., neither UTF-8 nor POSIX)" do before do expect(Etc).to receive(subject).with(param.nil? ? no_args : param).and_return(euc_kr_group_struct) end let(:overridden) { PuppetSpec::CharacterEncoding.with_external_encoding(Encoding::EUC_KR) do Puppet::Etc.send(subject, *param) end } it "should override EUC_KR-labeled values in arrays to UTF-8 if that would result in valid UTF-8" do expect(overridden.mem[2]).to eq(mixed_utf_8) expect(overridden.mem[1]).to eq(root) end it "should leave valid EUC_KR-labeled values that would not be valid UTF-8 in arrays unmodified" do expect(overridden.mem[0]).to eq(euc_kr) end it "should override EUC_KR-labeled values to UTF-8 if that would result in valid UTF-8" do expect(overridden.passwd).to eq(mixed_utf_8) end it "should leave valid EUC_KR-labeled values that would not be valid UTF-8 unmodified" do expect(overridden.name).to eq(euc_kr) end it "should copy all values to the new struct object" do # Confirm we've actually copied all the values to the canonical_members euc_kr_group_struct.each_pair do |member, value| expect(overridden["canonical_#{member}"]).to eq(value) # Confirm we've reassigned all non-string and array values if !value.is_a?(String) && !value.is_a?(Array) expect(overridden[member]).to eq(value) expect(overridden[member].object_id).to eq(value.object_id) end end end end context "when Encoding.default_external is POSIX (ASCII-7bit)" do before do expect(Etc).to receive(subject).with(param.nil? ? no_args : param).and_return(ascii_group_struct) end let(:overridden) { PuppetSpec::CharacterEncoding.with_external_encoding(Encoding::ASCII) do Puppet::Etc.send(subject, *param) end } it "should not modify binary values in arrays that would be invalid UTF-8" do expect(overridden.mem[0]).to eq(euc_kr_as_binary) end it "should set the encoding to UTF-8 on binary values in arrays that would be valid UTF-8" do expect(overridden.mem[1]).to eq(root.dup.force_encoding(Encoding::UTF_8)) expect(overridden.mem[2]).to eq(mixed_utf_8) end it "should not modify binary values that would be invalid UTF-8" do expect(overridden.name).to eq(euc_kr_as_binary) end it "should set the encoding to UTF-8 on binary values that would be valid UTF-8" do expect(overridden.passwd).to eq(mixed_utf_8) end it "should copy all values to the new struct object" do # Confirm we've actually copied all the values to the canonical_members ascii_group_struct.each_pair do |member, value| expect(overridden["canonical_#{member}"]).to eq(value) # Confirm we've reassigned all non-string and array values if !value.is_a?(String) && !value.is_a?(Array) expect(overridden[member]).to eq(value) expect(overridden[member].object_id).to eq(value.object_id) end end end end end shared_examples "methods that return an overridden user struct from Etc" do |param| it "should return a new Struct object with corresponding canonical_ members" do user = Etc::Passwd.new expect(Etc).to receive(subject).with(param.nil? ? no_args : param).and_return(user) puppet_user = Puppet::Etc.send(subject, *param) expect(puppet_user.members).to include(*user.members) expect(puppet_user.members).to include(*user.members.map { |mem| "canonical_#{mem}".to_sym }) # Confirm we haven't just added the new members to the original struct object, ie this is really a new struct expect(user.members.any? { |elem| elem.match(/^canonical_/)}).to be_falsey end context "when Encoding.default_external is UTF-8" do before do expect(Etc).to receive(subject).with(param.nil? ? no_args : param).and_return(utf_8_user_struct) end let(:overridden) { PuppetSpec::CharacterEncoding.with_external_encoding(Encoding::UTF_8) do Puppet::Etc.send(subject, *param) end } it "should leave the valid UTF-8 values unmodified" do expect(overridden.passwd).to eq(mixed_utf_8) end it "should replace invalid characters with unicode replacement characters in invalid UTF-8 values" do expect(overridden.name).to eq("\uFFFD\uFFFD") end it "should keep an unmodified version of the invalid UTF-8 values in the corresponding canonical_ member" do expect(overridden.canonical_name).to eq(euc_kr_as_utf_8) end it "should copy all values to the new struct object" do # Confirm we've actually copied all the values to the canonical_members utf_8_user_struct.each_pair do |member, value| expect(overridden["canonical_#{member}"]).to eq(value) # Confirm we've reassigned all non-string and array values if !value.is_a?(String) && !value.is_a?(Array) expect(overridden[member]).to eq(value) expect(overridden[member].object_id).to eq(value.object_id) end end end end context "when Encoding.default_external is EUC_KR (i.e., neither UTF-8 nor POSIX)" do before do expect(Etc).to receive(subject).with(param.nil? ? no_args : param).and_return(euc_kr_user_struct) end let(:overridden) { PuppetSpec::CharacterEncoding.with_external_encoding(Encoding::EUC_KR) do Puppet::Etc.send(subject, *param) end } it "should override valid UTF-8 EUC_KR-labeled values to UTF-8" do expect(overridden.passwd).to eq(mixed_utf_8) end it "should leave invalid EUC_KR-labeled values unmodified" do expect(overridden.name).to eq(euc_kr) end it "should copy all values to the new struct object" do # Confirm we've actually copied all the values to the canonical_members euc_kr_user_struct.each_pair do |member, value| expect(overridden["canonical_#{member}"]).to eq(value) # Confirm we've reassigned all non-string and array values if !value.is_a?(String) && !value.is_a?(Array) expect(overridden[member]).to eq(value) expect(overridden[member].object_id).to eq(value.object_id) end end end end context "when Encoding.default_external is POSIX (ASCII-7bit)" do before do expect(Etc).to receive(subject).with(param.nil? ? no_args : param).and_return(ascii_user_struct) end let(:overridden) { PuppetSpec::CharacterEncoding.with_external_encoding(Encoding::ASCII) do Puppet::Etc.send(subject, *param) end } it "should not modify binary values that would be invalid UTF-8" do expect(overridden.name).to eq(euc_kr_as_binary) end it "should set the encoding to UTF-8 on binary values that would be valid UTF-8" do expect(overridden.passwd).to eq(mixed_utf_8) end it "should copy all values to the new struct object" do # Confirm we've actually copied all the values to the canonical_members ascii_user_struct.each_pair do |member, value| expect(overridden["canonical_#{member}"]).to eq(value) # Confirm we've reassigned all non-string and array values if !value.is_a?(String) && !value.is_a?(Array) expect(overridden[member]).to eq(value) expect(overridden[member].object_id).to eq(value.object_id) end end end end end describe :getgrent do it_should_behave_like "methods that return an overridden group struct from Etc" end describe :getgrnam do it_should_behave_like "methods that return an overridden group struct from Etc", 'foo' it "should call Etc.getgrnam with the supplied group name" do expect(Etc).to receive(:getgrnam).with('foo') Puppet::Etc.getgrnam('foo') end end describe :getgrgid do it_should_behave_like "methods that return an overridden group struct from Etc", 0 it "should call Etc.getgrgid with supplied group id" do expect(Etc).to receive(:getgrgid).with(0) Puppet::Etc.getgrgid(0) end end describe :getpwent do it_should_behave_like "methods that return an overridden user struct from Etc" end describe :getpwnam do it_should_behave_like "methods that return an overridden user struct from Etc", 'foo' it "should call Etc.getpwnam with that username" do expect(Etc).to receive(:getpwnam).with('foo') Puppet::Etc.getpwnam('foo') end end describe :getpwuid do it_should_behave_like "methods that return an overridden user struct from Etc", 2 it "should call Etc.getpwuid with the id" do expect(Etc).to receive(:getpwuid).with(2) Puppet::Etc.getpwuid(2) end end describe :group do it 'should return the next group struct if a block is not provided' do expect(Puppet::Etc).to receive(:getgrent).and_return(ascii_group_struct) expect(Puppet::Etc.group).to eql(ascii_group_struct) end it 'should iterate over the available groups if a block is provided' do expected_groups = [ utf_8_group_struct, euc_kr_group_struct, ascii_group_struct ] allow(Puppet::Etc).to receive(:getgrent).and_return(*(expected_groups + [nil])) expect(Puppet::Etc).to receive(:setgrent) expect(Puppet::Etc).to receive(:endgrent) actual_groups = [] Puppet::Etc.group { |group| actual_groups << group } expect(actual_groups).to eql(expected_groups) end end describe "endgrent" do it "should call Etc.getgrent" do expect(Etc).to receive(:getgrent) Puppet::Etc.getgrent end end describe "setgrent" do it "should call Etc.setgrent" do expect(Etc).to receive(:setgrent) Puppet::Etc.setgrent end end describe "endpwent" do it "should call Etc.endpwent" do expect(Etc).to receive(:endpwent) Puppet::Etc.endpwent end end describe "setpwent" do it "should call Etc.setpwent" do expect(Etc).to receive(:setpwent) Puppet::Etc.setpwent end end end