Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changes/next-release/config-utf8-bom.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[
{
"type": "bugfix",
"category": "config",
"description": "Fix config file parsing failure when file contains a UTF-8 BOM (Byte Order Mark)"
}
]
2 changes: 1 addition & 1 deletion awscli/botocore/configloader.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ def raw_config_parse(config_filename, parse_subsections=True):
raise botocore.exceptions.ConfigNotFound(path=_unicode_path(path))
cp = configparser.RawConfigParser()
try:
cp.read([path])
cp.read([path], encoding='utf-8-sig')
except (configparser.Error, UnicodeDecodeError):
raise botocore.exceptions.ConfigParseError(
path=_unicode_path(path)
Expand Down
76 changes: 76 additions & 0 deletions tests/unit/botocore/test_configloader.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,26 @@ def create_config_file(self, filename):
f.write(contents)
return full_path

def create_config_file_with_bom(self, filename, contents=None):
if contents is None:
contents = (
'[default]\n'
'aws_access_key_id = foo\n'
'aws_secret_access_key = bar\n\n'
'[profile "personal"]\n'
'aws_access_key_id = fie\n'
'aws_secret_access_key = baz\n'
'aws_security_token = fiebaz\n'
)
directory = self.tempdir
if isinstance(filename, bytes):
directory = directory.encode('latin-1')
full_path = os.path.join(directory, filename)

with open(full_path, 'wb') as f:
f.write(b'\xef\xbb\xbf' + contents.encode('utf-8'))
return full_path

def test_config_not_found(self):
with self.assertRaises(botocore.exceptions.ConfigNotFound):
raw_config_parse(path('aws_config_notfound'))
Expand Down Expand Up @@ -207,6 +227,62 @@ def test_services_config(self):
'https://localhost:8888/',
)

def test_raw_config_parse_with_utf8_bom(self):
filename = self.create_config_file_with_bom('aws_config_bom')
loaded_config = raw_config_parse(filename)
self.assertIn('default', loaded_config)
self.assertIn('profile "personal"', loaded_config)
self.assertEqual(
loaded_config['default']['aws_access_key_id'], 'foo'
)
self.assertEqual(
loaded_config['default']['aws_secret_access_key'], 'bar'
)

def test_load_config_with_utf8_bom(self):
filename = self.create_config_file_with_bom('aws_config_bom')
loaded_config = load_config(filename)
self.assertIn('profiles', loaded_config)
self.assertEqual(
sorted(loaded_config['profiles'].keys()), ['default', 'personal']
)
self.assertEqual(
loaded_config['profiles']['default']['aws_access_key_id'], 'foo'
)
self.assertEqual(
loaded_config['profiles']['personal']['aws_security_token'],
'fiebaz',
)

def test_nested_hierarchy_parsing_with_utf8_bom(self):
contents = (
'[default]\n'
'aws_access_key_id = foo\n'
'region = us-west-2\n'
's3 =\n'
' signature_version = s3v4\n'
)
filename = self.create_config_file_with_bom(
'aws_config_nested_bom', contents
)
loaded_config = load_config(filename)
config = loaded_config['profiles']['default']
self.assertEqual(config['aws_access_key_id'], 'foo')
self.assertEqual(config['region'], 'us-west-2')
self.assertEqual(config['s3']['signature_version'], 's3v4')

def test_multi_file_load_with_utf8_bom(self):
bom_file = self.create_config_file_with_bom('aws_config_bom')
non_bom_file = path('aws_third_config')
loaded_config = multi_file_load_config(bom_file, non_bom_file)
self.assertEqual(
loaded_config['profiles']['default']['aws_access_key_id'], 'foo'
)
self.assertEqual(
loaded_config['profiles']['third']['aws_access_key_id'],
'third_fie',
)


if __name__ == "__main__":
unittest.main()
18 changes: 18 additions & 0 deletions tests/unit/botocore/test_credentials.py
Original file line number Diff line number Diff line change
Expand Up @@ -1654,6 +1654,24 @@ def test_credentials_file_does_not_exist_returns_none(self):
creds = provider.load()
self.assertIsNone(creds)

def test_credentials_file_with_utf8_bom(self):
with tempfile.NamedTemporaryFile('wb', delete=False) as f:
f.write(
b'\xef\xbb\xbf[default]\n'
b'aws_access_key_id = bom_key\n'
b'aws_secret_access_key = bom_secret\n'
)
creds_file = f.name
self.addCleanup(os.unlink, creds_file)
provider = credentials.SharedCredentialProvider(
creds_filename=creds_file,
profile_name='default',
)
creds = provider.load()
self.assertIsNotNone(creds)
self.assertEqual(creds.access_key, 'bom_key')
self.assertEqual(creds.secret_key, 'bom_secret')

def test_credentials_file_exists_with_account_id(self):
self.ini_parser.return_value = {
'default': {
Expand Down