src/JsonUtil.cs
Rama main · réplica de lectura del repositorio original.
using System;
using System.IO;
using System.Text;
using System.Threading;
using System.Web.Script.Serialization;
namespace Clipman
{
internal static class JsonUtil
{
private static readonly JavaScriptSerializer Serializer = new JavaScriptSerializer
{
MaxJsonLength = int.MaxValue,
RecursionLimit = 100
};
public static T Load<T>(string path) where T : new()
{
if (!File.Exists(path))
{
return new T();
}
var text = File.ReadAllText(path, Encoding.UTF8);
if (string.IsNullOrWhiteSpace(text))
{
return new T();
}
var value = Serializer.Deserialize<T>(text);
if (value == null)
{
return new T();
}
return value;
}
internal static T LoadRequired<T>(string path) where T : class
{
var value = Serializer.Deserialize<T>(File.ReadAllText(path, Encoding.UTF8));
if (value == null) throw new InvalidDataException("The settings file is empty or invalid.");
return value;
}
public static void SaveAtomic<T>(string path, T value)
{
var dir = Path.GetDirectoryName(path);
if (!string.IsNullOrEmpty(dir))
{
Directory.CreateDirectory(dir);
}
var temp = path + "." + Guid.NewGuid().ToString("N") + ".tmp";
var json = Serializer.Serialize(value);
try
{
File.WriteAllText(temp, PrettyPrint(json), Encoding.UTF8);
ReplaceTempWithRetry(temp, path);
}
finally
{
try
{
if (File.Exists(temp)) File.Delete(temp);
}
catch
{
}
}
}
private static void ReplaceTempWithRetry(string temp, string path)
{
Exception last = null;
var accessDenied = false;
for (var attempt = 0; attempt < 8; attempt++)
{
try
{
if (File.Exists(path))
{
File.Replace(temp, path, null);
}
else
{
File.Move(temp, path);
}
return;
}
catch (IOException ex)
{
last = ex;
}
catch (UnauthorizedAccessException ex)
{
last = ex;
accessDenied = true;
}
if (attempt < 7)
{
Thread.Sleep(50 * (attempt + 1));
}
}
if (!accessDenied && File.Exists(path))
{
try
{
ReplaceWithVerifiedCopyFallback(temp, path);
return;
}
catch (Exception ex)
{
last = new IOException("Clipman could not use the verified remote-filesystem fallback for " + path + ".", ex);
}
}
throw new IOException("Clipman could not replace " + path + " after waiting for the file to become available.", last);
}
internal static void ReplaceWithVerifiedCopyFallback(string temp, string path)
{
if (string.IsNullOrWhiteSpace(temp)) throw new ArgumentException("A temporary file path is required.", "temp");
if (string.IsNullOrWhiteSpace(path)) throw new ArgumentException("A destination file path is required.", "path");
if (!File.Exists(temp)) throw new FileNotFoundException("The completed temporary settings file was not found.", temp);
if (!File.Exists(path)) throw new FileNotFoundException("The existing settings file was not found.", path);
var expected = File.ReadAllBytes(temp);
var backup = path + "." + Guid.NewGuid().ToString("N") + ".fallback-backup";
var mayDeleteBackup = false;
try
{
File.Copy(path, backup, true);
File.Copy(temp, path, true);
if (!BytesEqual(expected, File.ReadAllBytes(path)))
{
throw new IOException("The replacement settings file did not match the completed temporary file.");
}
mayDeleteBackup = true;
}
catch (Exception writeException)
{
try
{
if (File.Exists(backup))
{
var original = File.ReadAllBytes(backup);
File.Copy(backup, path, true);
if (!BytesEqual(original, File.ReadAllBytes(path)))
{
throw new IOException("The restored settings file did not match its backup.");
}
mayDeleteBackup = true;
}
}
catch (Exception restoreException)
{
throw new IOException(
"Clipman could not replace the settings file and could not restore its backup. The backup remains at " + backup + ".",
new AggregateException(writeException, restoreException));
}
throw new IOException("Clipman could not replace the settings file. The previous file was restored.", writeException);
}
finally
{
if (mayDeleteBackup)
{
try { if (File.Exists(backup)) File.Delete(backup); }
catch { }
}
}
}
private static bool BytesEqual(byte[] left, byte[] right)
{
if (ReferenceEquals(left, right)) return true;
if (left == null || right == null || left.Length != right.Length) return false;
for (var index = 0; index < left.Length; index++)
{
if (left[index] != right[index]) return false;
}
return true;
}
public static string SerializePretty<T>(T value)
{
return PrettyPrint(Serializer.Serialize(value));
}
public static T Deserialize<T>(string text) where T : new()
{
if (string.IsNullOrWhiteSpace(text))
{
return new T();
}
var value = Serializer.Deserialize<T>(text);
return value == null ? new T() : value;
}
private static string PrettyPrint(string json)
{
var output = new StringBuilder();
var indent = 0;
var quoted = false;
for (var i = 0; i < json.Length; i++)
{
var ch = json[i];
if (ch == '"' && !IsEscaped(json, i))
{
quoted = !quoted;
}
if (quoted)
{
output.Append(ch);
continue;
}
switch (ch)
{
case '{':
case '[':
output.Append(ch).AppendLine();
indent++;
output.Append(new string(' ', indent * 2));
break;
case '}':
case ']':
output.AppendLine();
indent--;
output.Append(new string(' ', indent * 2)).Append(ch);
break;
case ',':
output.Append(ch).AppendLine();
output.Append(new string(' ', indent * 2));
break;
case ':':
output.Append(": ");
break;
default:
output.Append(ch);
break;
}
}
return output.ToString();
}
private static bool IsEscaped(string text, int index)
{
var slashCount = 0;
for (var i = index - 1; i >= 0 && text[i] == '\\'; i--)
{
slashCount++;
}
return slashCount % 2 == 1;
}
}
}