diff --git a/src/chocolatey/infrastructure.app/domain/RegistryValueExtensions.cs b/src/chocolatey/infrastructure.app/domain/RegistryValueExtensions.cs index b754b7f3a9..244aafce3c 100644 --- a/src/chocolatey/infrastructure.app/domain/RegistryValueExtensions.cs +++ b/src/chocolatey/infrastructure.app/domain/RegistryValueExtensions.cs @@ -24,7 +24,11 @@ public static string get_value_as_string(this RegistryKey key, string name) { if (key == null) return string.Empty; - return key.GetValue(name).to_string().Replace("\0", string.Empty); + // Since it is possible that registry keys contain characters that are not valid + // in XML files, ensure that all content is escaped, prior to serialization + var escapedXml = System.Security.SecurityElement.Escape(key.GetValue(name).to_string()); + + return escapedXml == null ? string.Empty : escapedXml.Replace("\0", string.Empty); } } } diff --git a/src/chocolatey/infrastructure.app/services/ChocolateyPackageInformationService.cs b/src/chocolatey/infrastructure.app/services/ChocolateyPackageInformationService.cs index ce94753d58..c342d4e4b6 100644 --- a/src/chocolatey/infrastructure.app/services/ChocolateyPackageInformationService.cs +++ b/src/chocolatey/infrastructure.app/services/ChocolateyPackageInformationService.cs @@ -16,6 +16,7 @@ namespace chocolatey.infrastructure.app.services { + using System; using System.IO; using System.Text; using NuGet; @@ -29,6 +30,7 @@ public class ChocolateyPackageInformationService : IChocolateyPackageInformation private readonly IRegistryService _registryService; private readonly IFilesService _filesService; private const string REGISTRY_SNAPSHOT_FILE = ".registry"; + private const string REGISTRY_SNAPSHOT_BAD_FILE = ".registry.bad"; private const string FILES_SNAPSHOT_FILE = ".files"; private const string SILENT_UNINSTALLER_FILE = ".silentUninstaller"; private const string SIDE_BY_SIDE_FILE = ".sxs"; @@ -59,16 +61,44 @@ public ChocolateyPackageInformation get_package_information(IPackage package) return packageInformation; } - FaultTolerance.try_catch_with_logging_exception( - () => + var deserializationErrorMessage = @" +A corrupt .registry file exists at {0}. + Open this file in a text editor, and remove/escape any characters that + are regarded as illegal within XML contents. These are typically the + characters <, >, "", ', and &. Once these have been corrected, rename + the .registry.bad file to .registry. Once saved, try running the same + Chocolatey command that was just executed, so verify problem is fixed. + NOTE: It will not be possible to rename the file in Windows Explorer. + Instead, you can use the following PowerShell command: + Move-Item .\.registry.bad .\.registry +".format_with(_fileSystem.combine_paths(pkgStorePath, REGISTRY_SNAPSHOT_BAD_FILE)); + try + { + if (_fileSystem.file_exists(_fileSystem.combine_paths(pkgStorePath, REGISTRY_SNAPSHOT_BAD_FILE))) + { + this.Log().Warn(deserializationErrorMessage); + } + else + { + packageInformation.RegistrySnapshot = _registryService.read_from_file(_fileSystem.combine_paths(pkgStorePath, REGISTRY_SNAPSHOT_FILE)); + } + } + catch (Exception) + { + FaultTolerance.try_catch_with_logging_exception( + () => { - packageInformation.RegistrySnapshot = _registryService.read_from_file(_fileSystem.combine_paths(pkgStorePath, REGISTRY_SNAPSHOT_FILE)); + this.Log().Warn(deserializationErrorMessage); + + // rename the bad registry file so that it isn't processed again + _fileSystem.move_file(_fileSystem.combine_paths(pkgStorePath, REGISTRY_SNAPSHOT_FILE), _fileSystem.combine_paths(pkgStorePath, REGISTRY_SNAPSHOT_BAD_FILE)); }, "Unable to read registry snapshot file for {0} (located at {1})".format_with(package.Id, _fileSystem.combine_paths(pkgStorePath, REGISTRY_SNAPSHOT_FILE)), throwError: false, logWarningInsteadOfError: true, isSilent: true - ); + ); + } FaultTolerance.try_catch_with_logging_exception( () => diff --git a/src/chocolatey/infrastructure.app/services/RegistryService.cs b/src/chocolatey/infrastructure.app/services/RegistryService.cs index fd4798e9d8..3cfb805ec7 100644 --- a/src/chocolatey/infrastructure.app/services/RegistryService.cs +++ b/src/chocolatey/infrastructure.app/services/RegistryService.cs @@ -398,7 +398,7 @@ public Registry read_from_file(string filePath) return null; } - return _xmlService.deserialize(filePath); + return _xmlService.deserialize(filePath, 1); } private void get_values(RegistryKey key, string subKeyName, IList values, bool expandValues) diff --git a/src/chocolatey/infrastructure/services/IXmlService.cs b/src/chocolatey/infrastructure/services/IXmlService.cs index d9a8ddaee2..b9445384f4 100644 --- a/src/chocolatey/infrastructure/services/IXmlService.cs +++ b/src/chocolatey/infrastructure/services/IXmlService.cs @@ -26,6 +26,15 @@ public interface IXmlService /// XmlType deserialize(string xmlFilePath); + /// + /// Deserializes the specified XML file path. + /// + /// The type of the ml type. + /// The XML file path. + /// The number of times to attempt deserialization on event of a failure. + /// + XmlType deserialize(string xmlFilePath, int retryCount); + /// /// Serializes the specified XML type. /// diff --git a/src/chocolatey/infrastructure/services/XmlService.cs b/src/chocolatey/infrastructure/services/XmlService.cs index 3a7236b5f7..a8f34923f8 100644 --- a/src/chocolatey/infrastructure/services/XmlService.cs +++ b/src/chocolatey/infrastructure/services/XmlService.cs @@ -45,75 +45,81 @@ public XmlService(IFileSystem fileSystem, IHashProvider hashProvider) public XmlType deserialize(string xmlFilePath) { - return FaultTolerance.retry(3, () => GlobalMutex.enter( - () => - { - this.Log().Trace("Entered mutex to deserialize '{0}'".format_with(xmlFilePath)); - - return FaultTolerance.try_catch_with_logging_exception( - () => - { - var xmlSerializer = new XmlSerializer(typeof(XmlType)); - using (var fileStream = _fileSystem.open_file_readonly(xmlFilePath)) - using (var fileReader = new StreamReader(fileStream)) - using (var xmlReader = XmlReader.Create(fileReader)) - { - if (!xmlSerializer.CanDeserialize(xmlReader)) - { - this.Log().Warn("Cannot deserialize response of type {0}", typeof(XmlType)); - return default(XmlType); - } - - try - { - return (XmlType)xmlSerializer.Deserialize(xmlReader); - } - catch (InvalidOperationException ex) - { - // Check if its just a malformed document. - if (ex.Message.Contains("There is an error in XML document")) - { - // If so, check for a backup file and try an parse that. - if (_fileSystem.file_exists(xmlFilePath + ".backup")) - { - using (var backupStream = _fileSystem.open_file_readonly(xmlFilePath + ".backup")) - using (var backupReader = new StreamReader(backupStream)) - using (var backupXmlReader = XmlReader.Create(backupReader)) - { - var validConfig = (XmlType)xmlSerializer.Deserialize(backupXmlReader); - - // If there's no errors and it's valid, go ahead and replace the bad file with the backup. - if (validConfig != null) - { - _fileSystem.copy_file(xmlFilePath + ".backup", xmlFilePath, overwriteExisting: true); - } - - return validConfig; - } - } - } - - throw; - } - finally - { - foreach (var updateFile in _fileSystem.get_files(_fileSystem.get_directory_name(xmlFilePath), "*.update").or_empty_list_if_null()) - { - this.Log().Debug("Removing '{0}'".format_with(updateFile)); - FaultTolerance.try_catch_with_logging_exception( - () => _fileSystem.delete_file(updateFile), - errorMessage: "Unable to remove update file", - logDebugInsteadOfError: true, - isSilent: true - ); - } - } - } - }, - "Error deserializing response of type {0}".format_with(typeof(XmlType)), - throwError: true); + return deserialize(xmlFilePath, 3); + } - }, MUTEX_TIMEOUT), + public XmlType deserialize(string xmlFilePath, int retryCount) + { + return FaultTolerance.retry(retryCount, () => GlobalMutex.enter( + () => + { + this.Log().Trace("Entered mutex to deserialize '{0}'".format_with(xmlFilePath)); + + return FaultTolerance.try_catch_with_logging_exception( + () => + { + var xmlSerializer = new XmlSerializer(typeof(XmlType)); + using (var fileStream = _fileSystem.open_file_readonly(xmlFilePath)) + using (var fileReader = new StreamReader(fileStream)) + using (var xmlReader = XmlReader.Create(fileReader)) + { + if (!xmlSerializer.CanDeserialize(xmlReader)) + { + this.Log().Warn("Cannot deserialize response of type {0}", typeof(XmlType)); + return default(XmlType); + } + + try + { + return (XmlType)xmlSerializer.Deserialize(xmlReader); + } + catch (InvalidOperationException ex) + { + // Check if its just a malformed document. + if (ex.Message.Contains("There is an error in XML document")) + { + // If so, check for a backup file and try an parse that. + if (_fileSystem.file_exists(xmlFilePath + ".backup")) + { + using (var backupStream = _fileSystem.open_file_readonly(xmlFilePath + ".backup")) + using (var backupReader = new StreamReader(backupStream)) + using (var backupXmlReader = XmlReader.Create(backupReader)) + { + var validConfig = (XmlType)xmlSerializer.Deserialize(backupXmlReader); + + // If there's no errors and it's valid, go ahead and replace the bad file with the backup. + if (validConfig != null) + { + _fileSystem.copy_file(xmlFilePath + ".backup", xmlFilePath, overwriteExisting: true); + } + + return validConfig; + } + } + } + + throw; + + } + finally + { + foreach (var updateFile in _fileSystem.get_files(_fileSystem.get_directory_name(xmlFilePath), "*.update").or_empty_list_if_null()) + { + this.Log().Debug("Removing '{0}'".format_with(updateFile)); + FaultTolerance.try_catch_with_logging_exception( + () => _fileSystem.delete_file(updateFile), + errorMessage: "Unable to remove update file", + logDebugInsteadOfError: true, + isSilent: true + ); + } + } + } + }, + "Error deserializing response of type {0}".format_with(typeof(XmlType)), + throwError: true); + + }, MUTEX_TIMEOUT), waitDurationMilliseconds: 200, increaseRetryByMilliseconds: 200); }