From efbfb630f60f6663deba83ec12c1fbf2f9bab183 Mon Sep 17 00:00:00 2001
From: wanshenmean <cathay_xy@163.com>
Date: 星期五, 13 三月 2026 13:44:08 +0800
Subject: [PATCH] fix: improve FilePersistenceService code quality and security
---
Code/WCS/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Core/Persistence/FilePersistenceService.cs | 344 ++++++++++++++++++++++++++++++++++++++++----------------
1 files changed, 245 insertions(+), 99 deletions(-)
diff --git a/Code/WCS/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Core/Persistence/FilePersistenceService.cs b/Code/WCS/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Core/Persistence/FilePersistenceService.cs
index a188c0a..27002dd 100644
--- a/Code/WCS/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Core/Persistence/FilePersistenceService.cs
+++ b/Code/WCS/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Core/Persistence/FilePersistenceService.cs
@@ -2,7 +2,9 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
+using System.Text;
using System.Text.Json;
+using System.Threading;
using System.Threading.Tasks;
using WIDESEAWCS_S7Simulator.Core.Entities;
using WIDESEAWCS_S7Simulator.Core.Enums;
@@ -23,9 +25,18 @@
private readonly string _dataPath;
/// <summary>
- /// JSON搴忓垪鍖栭�夐」
+ /// JSON搴忓垪鍖栭�夐」锛堢嚎绋嬪畨鍏級
/// </summary>
- private readonly JsonSerializerOptions _jsonOptions;
+ private static readonly JsonSerializerOptions _jsonOptions = new JsonSerializerOptions
+ {
+ WriteIndented = true,
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase
+ };
+
+ /// <summary>
+ /// 鏂囦欢鎿嶄綔閿侊紙绾跨▼瀹夊叏锛�
+ /// </summary>
+ private readonly SemaphoreSlim _fileLock = new SemaphoreSlim(1, 1);
/// <summary>
/// 鏋勯�犲嚱鏁�
@@ -34,16 +45,19 @@
public FilePersistenceService(string dataPath = "Data")
{
_dataPath = dataPath;
- _jsonOptions = new JsonSerializerOptions
- {
- WriteIndented = true,
- PropertyNamingPolicy = JsonNamingPolicy.CamelCase
- };
- // 纭繚鏁版嵁鐩綍瀛樺湪
- if (!Directory.Exists(_dataPath))
+ try
{
- Directory.CreateDirectory(_dataPath);
+ // 纭繚鏁版嵁鐩綍瀛樺湪
+ if (!Directory.Exists(_dataPath))
+ {
+ Directory.CreateDirectory(_dataPath);
+ }
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"Error creating data directory '{_dataPath}': {ex.Message}");
+ throw;
}
}
@@ -55,17 +69,30 @@
if (config == null)
throw new ArgumentNullException(nameof(config));
- var instanceDir = GetInstanceDirectory(config.Id);
- if (!Directory.Exists(instanceDir))
+ await _fileLock.WaitAsync();
+ try
{
- Directory.CreateDirectory(instanceDir);
+ var instanceDir = GetInstanceDirectory(config.Id);
+ if (!Directory.Exists(instanceDir))
+ {
+ Directory.CreateDirectory(instanceDir);
+ }
+
+ var configPath = Path.Combine(instanceDir, "config.json");
+ var model = ToDataModel(config);
+
+ var json = JsonSerializer.Serialize(model, _jsonOptions);
+ await WriteFileAtomicAsync(configPath, json);
}
-
- var configPath = Path.Combine(instanceDir, "config.json");
- var model = ToDataModel(config);
-
- var json = JsonSerializer.Serialize(model, _jsonOptions);
- await File.WriteAllTextAsync(configPath, json);
+ catch (Exception ex)
+ {
+ Console.WriteLine($"Error saving instance config for '{config.Id}': {ex.Message}");
+ throw;
+ }
+ finally
+ {
+ _fileLock.Release();
+ }
}
/// <summary>
@@ -76,17 +103,32 @@
if (string.IsNullOrWhiteSpace(instanceId))
throw new ArgumentException("瀹炰緥ID涓嶈兘涓虹┖", nameof(instanceId));
- var configPath = Path.Combine(GetInstanceDirectory(instanceId), "config.json");
- if (!File.Exists(configPath))
- throw new FileNotFoundException($"瀹炰緥閰嶇疆鏂囦欢涓嶅瓨鍦�: {configPath}");
+ ValidateInstanceId(instanceId);
- var json = await File.ReadAllTextAsync(configPath);
- var model = JsonSerializer.Deserialize<InstanceDataModel>(json, _jsonOptions);
+ await _fileLock.WaitAsync();
+ try
+ {
+ var configPath = Path.Combine(GetInstanceDirectory(instanceId), "config.json");
+ if (!File.Exists(configPath))
+ throw new FileNotFoundException($"瀹炰緥閰嶇疆鏂囦欢涓嶅瓨鍦�: {configPath}");
- if (model == null)
- throw new InvalidOperationException("鏃犳硶鍙嶅簭鍒楀寲瀹炰緥閰嶇疆");
+ var json = await File.ReadAllTextAsync(configPath);
+ var model = JsonSerializer.Deserialize<InstanceDataModel>(json, _jsonOptions);
- return ToEntity(model);
+ if (model == null)
+ throw new InvalidOperationException("鏃犳硶鍙嶅簭鍒楀寲瀹炰緥閰嶇疆");
+
+ return ToEntity(model);
+ }
+ catch (Exception ex) when (!(ex is FileNotFoundException || ex is InvalidOperationException || ex is ArgumentException))
+ {
+ Console.WriteLine($"Error loading instance config for '{instanceId}': {ex.Message}");
+ throw;
+ }
+ finally
+ {
+ _fileLock.Release();
+ }
}
/// <summary>
@@ -94,55 +136,77 @@
/// </summary>
public async Task<List<InstanceConfig>> LoadAllInstanceConfigsAsync()
{
- var configs = new List<InstanceConfig>();
-
- if (!Directory.Exists(_dataPath))
- return configs;
-
- var instanceDirs = Directory.GetDirectories(_dataPath)
- .Where(d => Path.GetFileName(d).StartsWith("instance-"))
- .ToList();
-
- foreach (var dir in instanceDirs)
+ await _fileLock.WaitAsync();
+ try
{
- var configPath = Path.Combine(dir, "config.json");
- if (File.Exists(configPath))
+ var configs = new List<InstanceConfig>();
+
+ if (!Directory.Exists(_dataPath))
+ return configs;
+
+ var instanceDirs = Directory.GetDirectories(_dataPath)
+ .Where(d => Path.GetFileName(d).StartsWith("instance-"))
+ .ToList();
+
+ foreach (var dir in instanceDirs)
{
- try
+ var configPath = Path.Combine(dir, "config.json");
+ if (File.Exists(configPath))
{
- var json = await File.ReadAllTextAsync(configPath);
- var model = JsonSerializer.Deserialize<InstanceDataModel>(json, _jsonOptions);
- if (model != null)
+ try
{
- configs.Add(ToEntity(model));
+ var json = await File.ReadAllTextAsync(configPath);
+ var model = JsonSerializer.Deserialize<InstanceDataModel>(json, _jsonOptions);
+ if (model != null)
+ {
+ configs.Add(ToEntity(model));
+ }
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"Error loading instance config from '{configPath}': {ex.Message}");
+ // 璺宠繃鏃犳硶鍔犺浇鐨勯厤缃枃浠�
+ continue;
}
}
- catch (Exception)
- {
- // 璺宠繃鏃犳硶鍔犺浇鐨勯厤缃枃浠�
- continue;
- }
}
- }
- return configs;
+ return configs;
+ }
+ finally
+ {
+ _fileLock.Release();
+ }
}
/// <summary>
/// 鍒犻櫎瀹炰緥閰嶇疆
/// </summary>
- public Task DeleteInstanceConfigAsync(string instanceId)
+ public async Task DeleteInstanceConfigAsync(string instanceId)
{
if (string.IsNullOrWhiteSpace(instanceId))
throw new ArgumentException("瀹炰緥ID涓嶈兘涓虹┖", nameof(instanceId));
- var instanceDir = GetInstanceDirectory(instanceId);
- if (Directory.Exists(instanceDir))
- {
- Directory.Delete(instanceDir, recursive: true);
- }
+ ValidateInstanceId(instanceId);
- return Task.CompletedTask;
+ await _fileLock.WaitAsync();
+ try
+ {
+ var instanceDir = GetInstanceDirectory(instanceId);
+ if (Directory.Exists(instanceDir))
+ {
+ Directory.Delete(instanceDir, recursive: true);
+ }
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"Error deleting instance '{instanceId}': {ex.Message}");
+ throw;
+ }
+ finally
+ {
+ _fileLock.Release();
+ }
}
/// <summary>
@@ -153,29 +217,44 @@
if (string.IsNullOrWhiteSpace(instanceId))
throw new ArgumentException("瀹炰緥ID涓嶈兘涓虹┖", nameof(instanceId));
+ ValidateInstanceId(instanceId);
+
if (memoryStore == null)
throw new ArgumentNullException(nameof(memoryStore));
- var instanceDir = GetInstanceDirectory(instanceId);
- if (!Directory.Exists(instanceDir))
+ await _fileLock.WaitAsync();
+ try
{
- Directory.CreateDirectory(instanceDir);
+ var instanceDir = GetInstanceDirectory(instanceId);
+ if (!Directory.Exists(instanceDir))
+ {
+ Directory.CreateDirectory(instanceDir);
+ }
+
+ var memoryPath = Path.Combine(instanceDir, "memory.json");
+ var exportedData = memoryStore.Export();
+
+ // 灏嗗瓧鑺傛暟缁勮浆鎹负Base64瀛楃涓蹭互渚縅SON搴忓垪鍖�
+ var memoryDataModel = new MemoryDataModel
+ {
+ MemoryData = exportedData.ToDictionary(
+ kvp => kvp.Key,
+ kvp => Convert.ToBase64String(kvp.Value)
+ )
+ };
+
+ var json = JsonSerializer.Serialize(memoryDataModel, _jsonOptions);
+ await WriteFileAtomicAsync(memoryPath, json);
}
-
- var memoryPath = Path.Combine(instanceDir, "memory.json");
- var exportedData = memoryStore.Export();
-
- // 灏嗗瓧鑺傛暟缁勮浆鎹负Base64瀛楃涓蹭互渚縅SON搴忓垪鍖�
- var memoryDataModel = new MemoryDataModel
+ catch (Exception ex)
{
- MemoryData = exportedData.ToDictionary(
- kvp => kvp.Key,
- kvp => Convert.ToBase64String(kvp.Value)
- )
- };
-
- var json = JsonSerializer.Serialize(memoryDataModel, _jsonOptions);
- await File.WriteAllTextAsync(memoryPath, json);
+ Console.WriteLine($"Error saving memory data for instance '{instanceId}': {ex.Message}");
+ throw;
+ }
+ finally
+ {
+ _fileLock.Release();
+ }
}
/// <summary>
@@ -186,35 +265,51 @@
if (string.IsNullOrWhiteSpace(instanceId))
throw new ArgumentException("瀹炰緥ID涓嶈兘涓虹┖", nameof(instanceId));
+ ValidateInstanceId(instanceId);
+
if (memoryStore == null)
throw new ArgumentNullException(nameof(memoryStore));
- var memoryPath = Path.Combine(GetInstanceDirectory(instanceId), "memory.json");
- if (!File.Exists(memoryPath))
- return; // 鍐呭瓨鏂囦欢涓嶅瓨鍦紝璺宠繃鍔犺浇
-
- var json = await File.ReadAllTextAsync(memoryPath);
- var memoryDataModel = JsonSerializer.Deserialize<MemoryDataModel>(json, _jsonOptions);
-
- if (memoryDataModel?.MemoryData == null)
- return;
-
- // 灏咮ase64瀛楃涓茶浆鎹㈠洖瀛楄妭鏁扮粍
- var importedData = new Dictionary<string, byte[]>();
- foreach (var kvp in memoryDataModel.MemoryData)
+ await _fileLock.WaitAsync();
+ try
{
- try
- {
- importedData[kvp.Key] = Convert.FromBase64String(kvp.Value);
- }
- catch (FormatException)
- {
- // 璺宠繃鏃犳晥鐨凚ase64鏁版嵁
- continue;
- }
- }
+ var memoryPath = Path.Combine(GetInstanceDirectory(instanceId), "memory.json");
+ if (!File.Exists(memoryPath))
+ return; // 鍐呭瓨鏂囦欢涓嶅瓨鍦紝璺宠繃鍔犺浇
- memoryStore.Import(importedData);
+ var json = await File.ReadAllTextAsync(memoryPath);
+ var memoryDataModel = JsonSerializer.Deserialize<MemoryDataModel>(json, _jsonOptions);
+
+ if (memoryDataModel?.MemoryData == null)
+ return;
+
+ // 灏咮ase64瀛楃涓茶浆鎹㈠洖瀛楄妭鏁扮粍
+ var importedData = new Dictionary<string, byte[]>();
+ foreach (var kvp in memoryDataModel.MemoryData)
+ {
+ try
+ {
+ importedData[kvp.Key] = Convert.FromBase64String(kvp.Value);
+ }
+ catch (FormatException ex)
+ {
+ Console.WriteLine($"Warning: Invalid Base64 data for memory region '{kvp.Key}' in instance '{instanceId}': {ex.Message}");
+ // 璺宠繃鏃犳晥鐨凚ase64鏁版嵁
+ continue;
+ }
+ }
+
+ memoryStore.Import(importedData);
+ }
+ catch (Exception ex) when (!(ex is FileNotFoundException))
+ {
+ Console.WriteLine($"Error loading memory data for instance '{instanceId}': {ex.Message}");
+ throw;
+ }
+ finally
+ {
+ _fileLock.Release();
+ }
}
/// <summary>
@@ -226,6 +321,57 @@
}
/// <summary>
+ /// 楠岃瘉瀹炰緥ID锛堥槻姝㈣矾寰勯亶鍘嗘敾鍑伙級
+ /// </summary>
+ private void ValidateInstanceId(string instanceId)
+ {
+ if (string.IsNullOrWhiteSpace(instanceId))
+ throw new ArgumentException("瀹炰緥ID涓嶈兘涓虹┖", nameof(instanceId));
+
+ // 妫�鏌ヨ矾寰勯亶鍘嗗瓧绗�
+ if (instanceId.Contains("..") || instanceId.Contains("/") || instanceId.Contains("\\"))
+ throw new ArgumentException("瀹炰緥ID鍖呭惈闈炴硶瀛楃", nameof(instanceId));
+
+ // 妫�鏌ユ棤鏁堣矾寰勫瓧绗�
+ var invalidChars = Path.GetInvalidFileNameChars();
+ if (instanceId.IndexOfAny(invalidChars) >= 0)
+ throw new ArgumentException("瀹炰緥ID鍖呭惈闈炴硶瀛楃", nameof(instanceId));
+ }
+
+ /// <summary>
+ /// 鍘熷瓙鎬у啓鍏ユ枃浠讹紙浣跨敤涓存椂鏂囦欢+鏇挎崲妯″紡锛�
+ /// </summary>
+ private async Task WriteFileAtomicAsync(string filePath, string content)
+ {
+ var tempPath = filePath + ".tmp";
+
+ try
+ {
+ // 鍐欏叆涓存椂鏂囦欢
+ await File.WriteAllTextAsync(tempPath, content, Encoding.UTF8);
+
+ // 鍘熷瓙鎬ф浛鎹㈢洰鏍囨枃浠�
+ File.Replace(tempPath, filePath, null);
+ }
+ catch
+ {
+ // 娓呯悊涓存椂鏂囦欢
+ if (File.Exists(tempPath))
+ {
+ try
+ {
+ File.Delete(tempPath);
+ }
+ catch
+ {
+ // 蹇界暐娓呯悊澶辫触
+ }
+ }
+ throw;
+ }
+ }
+
+ /// <summary>
/// 灏嗗疄浣撹浆鎹负鏁版嵁妯″瀷
/// </summary>
private InstanceDataModel ToDataModel(InstanceConfig config)
--
Gitblit v1.9.3