EasySign BETA
Digital Signing Tool
Loading...
Searching...
No Matches
Bundle.cs
Go to the documentation of this file.
1using System.Collections.Concurrent;
2using System.Diagnostics.CodeAnalysis;
3using System.IO.Compression;
4using System.Security.Cryptography;
5using System.Security.Cryptography.X509Certificates;
6using System.Text;
7using System.Text.Json;
8using System.Text.Json.Serialization;
9using System.Text.RegularExpressions;
10
11using EnsureThat;
12
13using Microsoft.Extensions.Logging;
14using Microsoft.Extensions.Logging.Abstractions;
15
16namespace SAPTeam.EasySign
17{
21 public class Bundle
22 {
23 private readonly string _bundleName;
24 private byte[] _rawZipContents = [];
25
26 private readonly ConcurrentDictionary<string, byte[]> _cache = new();
27 private byte[] _zipCache = [];
28 private readonly int _maxCacheSize;
29 private int _currentCacheSize;
30
31 private readonly ConcurrentDictionary<string, byte[]> _pendingForAdd = new();
32 private readonly List<string> _pendingForRemove = [];
33
37 protected readonly JsonSerializerOptions SerializerOptions = new JsonSerializerOptions()
38 {
39 WriteIndented = false,
40 DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault,
41 };
42
46 protected ILogger Logger { get; }
47
56 protected HashSet<string> ProtectedEntryNames { get; private set; } =
57 [
58 ".manifest.ec",
59 ".signatures.ec",
60 ];
61
68 protected virtual string DefaultBundleName => ".eSign";
69
73 public string RootPath { get; }
74
78 public string BundleName => LoadedFromMemory ? throw new InvalidOperationException("Bundle is loaded from memory") : _bundleName;
79
83 public string BundlePath => Path.Combine(RootPath, BundleName);
84
88 public Manifest Manifest { get; private set; } = new();
89
93 public Signatures Signatures { get; private set; } = new();
94
98 public bool ReadOnly { get; private set; }
99
103 public bool LoadedFromMemory => _rawZipContents != null && _rawZipContents.Length > 0;
104
108 public bool Loaded { get; private set; }
109
113 public event Action<ZipArchive>? Updating;
114
121 public Bundle(string bundlePath, ILogger? logger = null, int maxCacheSize = 0x8000000)
122 {
123 Ensure.String.IsNotNullOrEmpty(bundlePath.Trim(), nameof(bundlePath));
124
125 string fullPath = Path.GetFullPath(bundlePath);
126
127 if (Directory.Exists(fullPath))
128 {
129 _bundleName = DefaultBundleName;
130 RootPath = fullPath;
131 }
132 else
133 {
134 _bundleName = Path.GetFileName(fullPath);
135 RootPath = Path.GetDirectoryName(fullPath) ?? throw new ArgumentException("Cannot resolve root path of the bundle: " + fullPath);
136 }
137
138 Logger = logger ?? NullLogger.Instance;
139
140 _maxCacheSize = maxCacheSize;
141 }
142
146 protected void EnsureWritable()
147 {
148 if (ReadOnly)
149 {
150 throw new InvalidOperationException("Bundle is read-only"); ;
151 }
152 }
153
161 protected bool CheckEntryNameSecurity(string entryName, bool throwException = true)
162 {
163 foreach (string pattern in ProtectedEntryNames)
164 {
165 if (Regex.IsMatch(entryName, pattern))
166 {
167 return throwException ? throw new UnauthorizedAccessException("Entry name is protected: " + entryName) : false;
168 }
169 }
170
171 return true;
172 }
173
174 private void EvictIfNecessary(long incomingFileSize)
175 {
176 while (_currentCacheSize + incomingFileSize > _maxCacheSize && !_cache.IsEmpty)
177 {
178 string leastUsedKey = _cache.Keys.First();
179 if (_cache.TryRemove(leastUsedKey, out byte[]? removed))
180 {
181 _currentCacheSize -= removed.Length;
182 }
183
184 Logger.LogDebug("Evicted entry: {name} from the cache", leastUsedKey);
185 }
186 }
187
194 protected bool CacheEntry(string entryName, byte[] data)
195 {
196 Ensure.String.IsNotNullOrEmpty(entryName.Trim(), nameof(entryName));
197 Ensure.Collection.HasItems(data, nameof(data));
198
199 if (!ReadOnly || _maxCacheSize < data.Length)
200 {
201 return false;
202 }
203
204 if (_cache.TryGetValue(entryName, out byte[]? existing))
205 {
206 if (existing.SequenceEqual(data))
207 {
208 return false;
209 }
210 }
211
212 EvictIfNecessary(data.Length);
213
214 _cache[entryName] = data;
215 _currentCacheSize += data.Length;
216
217 Logger.LogDebug("Cached entry: {name} with {size} bytes", entryName, data.Length);
218
219 return true;
220 }
221
227 public ZipArchive GetZipArchive(ZipArchiveMode mode = ZipArchiveMode.Read)
228 {
229 if (mode != ZipArchiveMode.Read) EnsureWritable();
230
231 Logger.LogDebug("Opening bundle archive in {Mode} mode", mode);
232
234 {
235 Logger.LogDebug("Loading bundle from memory with {Size} bytes", _rawZipContents.Length);
236
237 MemoryStream ms = new MemoryStream(_rawZipContents, writable: false);
238 return new ZipArchive(ms, mode);
239 }
240 else
241 {
242 if (mode == ZipArchiveMode.Read)
243 {
244 Stream stream;
245 if (_zipCache.Length == 0)
246 {
247 Logger.LogDebug("Loading bundle from file: {file}", BundlePath);
248 stream = File.OpenRead(BundlePath);
249
250 if (ReadOnly && stream.Length < _maxCacheSize)
251 {
252 Logger.LogDebug("Caching bundle with {Size} bytes", stream.Length);
253 _zipCache = ReadStream(stream);
254 }
255 }
256 else
257 {
258 Logger.LogDebug("Loading bundle from cache with {Size} bytes", _zipCache.Length);
259 stream = new MemoryStream(_zipCache, writable: false);
260 }
261
262 return new ZipArchive(stream, ZipArchiveMode.Read);
263 }
264
265 _zipCache = Array.Empty<byte>();
266
267 Logger.LogDebug("Loading bundle from file: {file}", BundlePath);
268 return ZipFile.Open(BundlePath, mode);
269 }
270 }
271
276 public void LoadFromFile(bool readOnly = true)
277 {
278 if (Loaded)
279 {
280 throw new InvalidOperationException("The bundle is already loaded");
281 }
282
283 ReadOnly = readOnly;
284 using ZipArchive zip = GetZipArchive();
285 Parse(zip);
286
287 Loaded = true;
288
289 Logger.LogInformation("Bundle loaded from file: {file}", BundlePath);
290 }
291
299 public void LoadFromBytes(byte[] bundleContent)
300 {
301 if (Loaded)
302 {
303 throw new InvalidOperationException("The bundle is already loaded");
304 }
305
306 Ensure.Collection.HasItems(bundleContent, nameof(bundleContent));
307
308 ReadOnly = true;
309 _rawZipContents = bundleContent;
310
311 using ZipArchive zip = GetZipArchive();
312 Parse(zip);
313
314 Loaded = true;
315
316 Logger.LogInformation("Bundle loaded from memory with {Size} bytes", bundleContent.Length);
317 }
318
323 protected virtual void Parse(ZipArchive zip)
324 {
325 Logger.LogInformation("Parsing bundle contents");
326
327 ZipArchiveEntry? entry;
328 if ((entry = zip.GetEntry(".manifest.ec")) != null)
329 {
330 Logger.LogDebug("Parsing manifest");
331 Manifest = JsonSerializer.Deserialize(entry.Open(), typeof(Manifest), SourceGenerationManifestContext.Default) as Manifest ?? new Manifest();
332
333 HashSet<string> protectedEntries = ProtectedEntryNames.Union(Manifest.ProtectedEntryNames).ToHashSet();
334 ProtectedEntryNames = protectedEntries;
335 Manifest.ProtectedEntryNames = protectedEntries;
336 }
337 else
338 {
339 Logger.LogWarning("Manifest not found in the bundle");
340 }
341
342 if ((entry = zip.GetEntry(".signatures.ec")) != null)
343 {
344 Logger.LogDebug("Parsing signatures");
345 Signatures = JsonSerializer.Deserialize(entry.Open(), typeof(Signatures), SourceGenerationSignaturesContext.Default) as Signatures ?? new Signatures();
346 }
347 else
348 {
349 Logger.LogWarning("Signatures not found in the bundle");
350 }
351 }
352
361 public void AddEntry(string path, string destinationPath = "./", string? rootPath = null)
362 {
364
365 Ensure.String.IsNotNullOrEmpty(path.Trim(), nameof(path));
366
367 if (!File.Exists(path))
368 {
369 throw new FileNotFoundException("File not found", path);
370 }
371
372 Logger.LogInformation("Adding file: {path}", path);
373
374 if (!destinationPath.EndsWith('/'))
375 destinationPath += "/";
376
377 if (string.IsNullOrEmpty(rootPath))
378 rootPath = RootPath;
379
380 using FileStream file = File.OpenRead(path);
381 string name = Manifest.GetNormalizedEntryName(Path.GetRelativePath(rootPath, path));
382
384
385 byte[] hash = ComputeSHA512Hash(file);
386
387 if (Manifest.StoreOriginalFiles && !string.IsNullOrEmpty(destinationPath) && destinationPath != "./")
388 {
389 name = destinationPath + name;
390 }
391
392 Logger.LogDebug("Adding entry: {name} with hash {hash} to manifest", name, BitConverter.ToString(hash).Replace("-", string.Empty));
393 Manifest.AddEntry(name, hash);
394
396 {
397 Logger.LogDebug("Pending file: {name} for adding tp the bundle", name);
398 _pendingForAdd[name] = File.ReadAllBytes(path);
399 }
400 }
401
406 public void DeleteEntry(string entryName)
407 {
409
410 Ensure.String.IsNotNullOrEmpty(entryName.Trim(), nameof(entryName));
411
412 Logger.LogInformation("Deleting entry: {name}", entryName);
413
414 CheckEntryNameSecurity(entryName);
415
416 Logger.LogDebug("Deleting entry: {name} from manifest", entryName);
417 Manifest.DeleteEntry(entryName);
418
420 {
421 Logger.LogDebug("Pending entry: {name} for deletion from the bundle", entryName);
422 _pendingForRemove.Add(entryName);
423
424 if (_pendingForAdd.ContainsKey(entryName))
425 {
426 Logger.LogDebug("Removing pending entry: {name} from the bundle", entryName);
427 _pendingForAdd.Remove(entryName, out _);
428 }
429 }
430 }
431
437 public void Sign(X509Certificate2 certificate, RSA privateKey)
438 {
440
441 Ensure.Any.IsNotNull(certificate, nameof(certificate));
442 Ensure.Any.IsNotNull(privateKey, nameof(privateKey));
443
444 Logger.LogInformation("Signing bundle with certificate: {name}", certificate.Subject);
445
446 Logger.LogDebug("Exporting certificate");
447 byte[] certData = certificate.Export(X509ContentType.Cert);
448 string name = certificate.GetCertHashString();
449
450 Logger.LogDebug("Signing manifest");
451 byte[] manifestData = GetManifestData();
452 byte[] signature = privateKey.SignData(manifestData, HashAlgorithmName.SHA512, RSASignaturePadding.Pkcs1);
453
454 Logger.LogDebug("Adding signature for certificate: {name} to signatures", name);
455 Signatures.Entries[name] = signature;
456 Signatures.Certificates[name] = certData;
457
458 Logger.LogInformation("Bundle signed with certificate: {name}", certificate.Subject);
459 }
460
466 public bool VerifyFile(string entryName)
467 {
468 Ensure.String.IsNotNullOrEmpty(entryName.Trim(), nameof(entryName));
469
470 Logger.LogInformation("Verifying file integrity: {name}", entryName);
471
472 using Stream stream = GetStream(entryName);
473 byte[] hash = ComputeSHA512Hash(stream);
474 bool result = Manifest.GetEntries()[entryName].SequenceEqual(hash);
475
476 Logger.LogInformation("File integrity verification result for {name}: {result}", entryName, result);
477
478 return result;
479 }
480
486 public bool VerifySignature(string certificateHash)
487 {
488 Ensure.String.IsNotNullOrEmpty(certificateHash.Trim(), nameof(certificateHash));
489 byte[] hash = Signatures.Entries[certificateHash];
490
491 X509Certificate2 certificate = GetCertificate(certificateHash);
492 RSA pubKey = certificate.GetRSAPublicKey() ?? throw new CryptographicException("Public key not found");
493
494 Logger.LogInformation("Verifying signature with certificate: {name}", certificate.Subject);
495
496 byte[] manifestHash = ComputeSHA512Hash(GetBytes(".manifest.ec", ReadSource.Bundle));
497 bool result = pubKey.VerifyHash(manifestHash, hash, HashAlgorithmName.SHA512, RSASignaturePadding.Pkcs1);
498
499 Logger.LogInformation("Signature verification result for certificate {name}: {result}", certificate.Subject, result);
500
501 return result;
502 }
503
511 public bool VerifyCertificate(string certificateHash, out X509ChainStatus[] statuses, X509ChainPolicy? policy = null)
512 {
513 Ensure.String.IsNotNullOrEmpty(certificateHash.Trim(), nameof(certificateHash));
514
515 X509Certificate2 certificate = GetCertificate(certificateHash);
516 return VerifyCertificate(certificate, out statuses, policy);
517 }
518
525 public bool VerifyCertificate(string certificateHash, X509ChainPolicy? policy = null) => VerifyCertificate(certificateHash, out _, policy);
526
534 public bool VerifyCertificate(X509Certificate2 certificate, out X509ChainStatus[] statuses, X509ChainPolicy? policy = null)
535 {
536 Ensure.Any.IsNotNull(certificate, nameof(certificate));
537
538 X509Chain chain = new X509Chain
539 {
540 ChainPolicy = policy ?? new X509ChainPolicy()
541 };
542
543 Logger.LogInformation("Verifying certificate: {name}", certificate.Subject);
544
545 if (policy != null)
546 {
547 Logger.LogDebug("Using custom chain policy for verification");
548 }
549
550 bool isValid = chain.Build(certificate);
551 statuses = chain.ChainStatus;
552
553 Logger.LogInformation("Certificate verification result for {name}: {result}", certificate.Subject, isValid);
554
555 return isValid;
556 }
557
564 public bool VerifyCertificate(X509Certificate2 certificate, X509ChainPolicy? policy = null) => VerifyCertificate(certificate, out _, policy);
565
571 public X509Certificate2 GetCertificate(string certificateHash)
572 {
573 Ensure.String.IsNotNullOrEmpty(certificateHash.Trim(), nameof(certificateHash));
574
575 Logger.LogInformation("Getting certificate with hash: {hash}", certificateHash);
576
577 byte[] certData = Signatures.Certificates[certificateHash];
578
579#if NET9_0_OR_GREATER
580 X509Certificate2 certificate = X509CertificateLoader.LoadCertificate(certData);
581#else
582 X509Certificate2 certificate = new X509Certificate2(certData);
583#endif
584
585 return certificate;
586 }
587
597 public byte[] GetBytes(string entryName, ReadSource readSource)
598 {
599 Ensure.String.IsNotNullOrEmpty(entryName.Trim(), nameof(entryName));
600
601 Logger.LogInformation("Getting file data for entry: {name}", entryName);
602
603 if (!_cache.TryGetValue(entryName, out byte[]? data))
604 {
605 using Stream stream = GetStream(entryName, readSource);
606 data = ReadStream(stream);
607
608 _ = CacheEntry(entryName, data);
609 }
610
611 return data;
612 }
613
623 public Stream GetStream(string entryName, ReadSource readSource = ReadSource.Automatic)
624 {
625 Ensure.String.IsNotNullOrEmpty(entryName.Trim(), nameof(entryName));
626
627 Logger.LogInformation("Getting file stream for entry: {name}", entryName);
628
629 if (_cache.TryGetValue(entryName, out byte[]? data))
630 {
631 Logger.LogDebug("Reading entry {name} from cache", entryName);
632 return new MemoryStream(data, writable: false);
633 }
634 else
635 {
636 Logger.LogDebug("Entry {name} not found in cache", entryName);
637 }
638
639 readSource = GetReadSource(entryName, readSource);
640
641 Stream stream;
642
643 if (readSource == ReadSource.Bundle)
644 {
645 Logger.LogDebug("Reading file: {name} from the bundle", entryName);
646
647 ZipArchive zip = GetZipArchive();
648
649 ZipArchiveEntry entry = zip.GetEntry(entryName) ?? throw new FileNotFoundException("Entry not found", entryName);
650 stream = entry.Open();
651 }
652 else
653 {
654 Logger.LogDebug("Reading file: {name} from the file system", entryName);
655
656 string path = Path.GetFullPath(entryName, RootPath);
657 stream = File.OpenRead(path);
658 }
659
660 return stream;
661 }
662
675 public bool Exists(string entryName, ReadSource readSource = ReadSource.Automatic)
676 {
677 Ensure.String.IsNotNullOrEmpty(entryName, nameof(entryName));
678
679 Logger.LogInformation("Checking if entry {entryName} exists", entryName);
680
681 bool result;
682 readSource = GetReadSource(entryName, readSource);
683
684 if (readSource == ReadSource.Bundle)
685 {
686 using ZipArchive zip = GetZipArchive();
687 result = zip.GetEntry(entryName) != null;
688 }
689 else
690 {
691 string path = Path.GetFullPath(entryName, RootPath);
692 result = File.Exists(path);
693 }
694
695 Logger.LogInformation("Entry {entryName} exists: {result}", entryName, result);
696 return result;
697 }
698
711 protected ReadSource GetReadSource(string entryName, ReadSource readSource = ReadSource.Automatic)
712 {
713 Ensure.String.IsNotNullOrEmpty(entryName, nameof(entryName));
714
715 if (!CheckEntryNameSecurity(entryName, false))
716 {
717 readSource = ReadSource.Bundle;
718 }
719
720 if (readSource == ReadSource.Automatic)
721 {
722 readSource = Manifest.StoreOriginalFiles ? ReadSource.Bundle : ReadSource.Disk;
723 }
724
725 return readSource;
726 }
727
731 public void Update()
732 {
734
735 Logger.LogInformation("Updating bundle file: {file}", BundlePath);
736
737 using (ZipArchive zip = GetZipArchive(ZipArchiveMode.Update))
738 {
739 Logger.LogDebug("Invoking Updating event");
740 Updating?.Invoke(zip);
741
742 Logger.LogDebug("Processing pending files");
743 ProcessPendingFiles(zip);
744
745 Logger.LogDebug("Writing manifest to the bundle");
746 byte[] manifestData = GetManifestData();
747 WriteEntry(zip, ".manifest.ec", manifestData);
748
749 Logger.LogDebug("Writing signatures to the bundle");
750 byte[] signatureData = Export(Signatures, SourceGenerationSignaturesContext.Default);
751 WriteEntry(zip, ".signatures.ec", signatureData);
752 }
753 }
754
755 private void ProcessPendingFiles(ZipArchive zip)
756 {
757 ZipArchiveEntry? tempEntry;
758 foreach (string entryName in _pendingForRemove)
759 {
760 if ((tempEntry = zip.GetEntry(entryName)) != null)
761 {
762 Logger.LogDebug("Deleting entry: {name}", entryName);
763 tempEntry.Delete();
764 }
765 else
766 {
767 Logger.LogWarning("Entry {name} not found in the bundle", entryName);
768 }
769 }
770
771 foreach (KeyValuePair<string, byte[]> newFile in _pendingForAdd)
772 {
773 WriteEntry(zip, newFile.Key, newFile.Value);
774 }
775 }
776
783 protected virtual byte[] GetManifestData()
784 {
785 Manifest.UpdatedBy = GetType().FullName;
786 Manifest.ProtectedEntryNames = ProtectedEntryNames.Union(Manifest.ProtectedEntryNames).ToHashSet();
787
788 return Export(Manifest, SourceGenerationManifestContext.Default);
789 }
790
797 protected byte[] Export(object structuredData, JsonSerializerContext jsonSerializerContext)
798 {
799 Ensure.Any.IsNotNull(structuredData, nameof(structuredData));
800 Ensure.Any.IsNotNull(jsonSerializerContext, nameof(jsonSerializerContext));
801
802 Logger.LogInformation("Exporting data from a {type} object as byte array", structuredData.GetType().Name);
803
804 string data = JsonSerializer.Serialize(structuredData, structuredData.GetType(), jsonSerializerContext);
805 return Encoding.UTF8.GetBytes(data);
806 }
807
813#if NET6_0_OR_GREATER
814 [RequiresUnreferencedCode("This method is not compatible with AOT.")]
815#endif
816#if NET8_0_OR_GREATER
817 [RequiresDynamicCode("This method is not compatible with AOT.")]
818#endif
819 protected byte[] Export(object structuredData)
820 {
821 Ensure.Any.IsNotNull(structuredData, nameof(structuredData));
822
823 Logger.LogInformation("Exporting data from a {type} object as byte array", structuredData.GetType().Name);
824
825 string data = JsonSerializer.Serialize(structuredData, SerializerOptions);
826 return Encoding.UTF8.GetBytes(data);
827 }
828
836 protected void WriteEntry(ZipArchive zip, string entryName, byte[] data)
837 {
838 Ensure.Any.IsNotNull(zip, nameof(zip));
839 Ensure.String.IsNotNullOrEmpty(entryName.Trim(), nameof(entryName));
840 Ensure.Collection.HasItems(data, nameof(data));
841
842 Logger.LogDebug("Writing entry: {name} to the bundle", entryName);
843
844 ZipArchiveEntry? tempEntry;
845 if ((tempEntry = zip.GetEntry(entryName)) != null)
846 {
847 Logger.LogDebug("Deleting existing entry: {name}", entryName);
848 tempEntry.Delete();
849 }
850
851#if NET6_0_OR_GREATER
852 CompressionLevel compressionLevel = CompressionLevel.SmallestSize;
853#else
854 CompressionLevel compressionLevel = CompressionLevel.Optimal;
855#endif
856
857 Logger.LogDebug("Creating new entry: {name} in the bundle with compression level {level}", entryName, compressionLevel);
858 ZipArchiveEntry entry = zip.CreateEntry(entryName, compressionLevel);
859
860 using Stream stream = entry.Open();
861 stream.Write(data, 0, data.Length);
862 stream.Flush();
863
864 Logger.LogInformation("Wrote entry: {name} with {size} bytes to the bundle", entryName, data.Length);
865 }
866
872 protected static byte[] ReadStream(Stream stream)
873 {
874 Ensure.Any.IsNotNull(stream, nameof(stream));
875
876
877 byte[] result;
878 if (stream is MemoryStream memoryStream)
879 {
880 result = memoryStream.ToArray();
881 }
882 else
883 {
884 MemoryStream ms = new();
885 stream.CopyTo(ms);
886 result = ms.ToArray();
887 }
888
889 return result;
890 }
891
897 protected static byte[] ComputeSHA512Hash(Stream stream)
898 {
899 Ensure.Any.IsNotNull(stream, nameof(stream));
900
901 using SHA512 sha512 = SHA512.Create();
902 return sha512.ComputeHash(stream);
903 }
904
910 protected static byte[] ComputeSHA512Hash(byte[] data)
911 {
912 Ensure.Collection.HasItems(data, nameof(data));
913
914 using SHA512 sha512 = SHA512.Create();
915 return sha512.ComputeHash(data);
916 }
917 }
918}
Represents a bundle that holds file hashes and signatures.
Definition Bundle.cs:22
virtual string DefaultBundleName
Gets the default name of the bundle.
Definition Bundle.cs:68
Stream GetStream(string entryName, ReadSource readSource=ReadSource.Automatic)
Gets a read-only stream for an entry in the bundle and caches the entry data if the bundle is Read-on...
Definition Bundle.cs:623
readonly JsonSerializerOptions SerializerOptions
Gets the JSON serializer options.
Definition Bundle.cs:37
string BundleName
Gets the name of the bundle file.
Definition Bundle.cs:78
void WriteEntry(ZipArchive zip, string entryName, byte[] data)
Writes an entry to a ZipArchive.
Definition Bundle.cs:836
virtual void Parse(ZipArchive zip)
Parses the bundle contents from a ZipArchive.
Definition Bundle.cs:323
bool CheckEntryNameSecurity(string entryName, bool throwException=true)
Checks whether the entry name is protected and throws an exception if it is.
Definition Bundle.cs:161
byte[] GetBytes(string entryName, ReadSource readSource)
Gets the data of an entry in the bundle as bytes array and caches the entry data if the bundle is Rea...
Definition Bundle.cs:597
byte[] Export(object structuredData)
Exports the specified structured data to a byte array.
Definition Bundle.cs:819
void AddEntry(string path, string destinationPath="./", string? rootPath=null)
Adds a file entry to the bundle.
Definition Bundle.cs:361
static byte[] ReadStream(Stream stream)
Reads a stream into a byte array.
Definition Bundle.cs:872
string BundlePath
Gets the full path of the bundle file.
Definition Bundle.cs:83
void EnsureWritable()
Throws an exception if the bundle is read-only.
Definition Bundle.cs:146
string RootPath
Gets the root path of the bundle.
Definition Bundle.cs:73
ILogger Logger
Gets the logger to use for logging.
Definition Bundle.cs:46
bool VerifyFile(string entryName)
Verifies the integrity of a file in the bundle.
Definition Bundle.cs:466
bool VerifyCertificate(string certificateHash, X509ChainPolicy? policy=null)
Verifies the validity of a certificate using the specified certificate hash.
bool VerifyCertificate(X509Certificate2 certificate, X509ChainPolicy? policy=null)
Verifies the validity of a certificate.
void DeleteEntry(string entryName)
Deletes an entry from the bundle.
Definition Bundle.cs:406
void Sign(X509Certificate2 certificate, RSA privateKey)
Signs the bundle with the specified certificate and private key.
Definition Bundle.cs:437
void LoadFromBytes(byte[] bundleContent)
Loads the bundle from a byte array.
Definition Bundle.cs:299
static byte[] ComputeSHA512Hash(Stream stream)
Computes the SHA-512 hash of a stream.
Definition Bundle.cs:897
void Update()
Writes changes to the bundle file.
Definition Bundle.cs:731
void LoadFromFile(bool readOnly=true)
Loads the bundle from the file system.
Definition Bundle.cs:276
bool CacheEntry(string entryName, byte[] data)
Caches an entry in memory.
Definition Bundle.cs:194
static byte[] ComputeSHA512Hash(byte[] data)
Computes the SHA-512 hash of a byte array.
Definition Bundle.cs:910
bool Exists(string entryName, ReadSource readSource=ReadSource.Automatic)
Checks whether an entry exists in the bundle or on the disk.
Definition Bundle.cs:675
byte[] Export(object structuredData, JsonSerializerContext jsonSerializerContext)
Exports the specified structured data to a byte array.
Definition Bundle.cs:797
virtual byte[] GetManifestData()
Gets the manifest data as a byte array.
Definition Bundle.cs:783
bool LoadedFromMemory
Gets a value indicating whether the bundle is loaded from memory.
Definition Bundle.cs:103
bool VerifyCertificate(string certificateHash, out X509ChainStatus[] statuses, X509ChainPolicy? policy=null)
Verifies the validity of a certificate using the specified certificate hash.
Definition Bundle.cs:511
Bundle(string bundlePath, ILogger? logger=null, int maxCacheSize=0x8000000)
Initializes a new instance of the Bundle class.
Definition Bundle.cs:121
X509Certificate2 GetCertificate(string certificateHash)
Gets a certificate from the bundle using the specified certificate hash.
Definition Bundle.cs:571
ZipArchive GetZipArchive(ZipArchiveMode mode=ZipArchiveMode.Read)
Gets a ZipArchive for the bundle.
Definition Bundle.cs:227
bool VerifyCertificate(X509Certificate2 certificate, out X509ChainStatus[] statuses, X509ChainPolicy? policy=null)
Verifies the validity of a certificate.
Definition Bundle.cs:534
bool Loaded
Gets a value indicating whether the bundle is loaded.
Definition Bundle.cs:108
Action< ZipArchive >? Updating
Occurs when the bundle file is being updated.
Definition Bundle.cs:113
bool ReadOnly
Gets a value indicating whether the bundle is read-only.
Definition Bundle.cs:98
Manifest Manifest
Gets the manifest of the bundle.
Definition Bundle.cs:88
HashSet< string > ProtectedEntryNames
Gets the list of sensitive names.
Definition Bundle.cs:56
bool VerifySignature(string certificateHash)
Verifies the signature of the bundle using the specified certificate hash.
Definition Bundle.cs:486
Signatures Signatures
Gets the signatures of the bundle.
Definition Bundle.cs:93
ReadSource GetReadSource(string entryName, ReadSource readSource=ReadSource.Automatic)
Gets the read source for an entry name.
Definition Bundle.cs:711
Represents a manifest that holds entries of file names and their corresponding hashes.
Definition Manifest.cs:11
static string GetNormalizedEntryName(string path)
Converts the path to an standard zip entry name.
void DeleteEntry(string entryName)
Deletes an entry from the manifest.
Definition Manifest.cs:72
bool StoreOriginalFiles
Gets or sets a value indicating whether the files should be stored in the bundle.
Definition Manifest.cs:34
void AddEntry(string entryName, byte[] hash)
Adds an entry to the manifest.
Definition Manifest.cs:58
ConcurrentDictionary< string, byte[]> GetEntries()
Gets the entries as a thread-safe concurrent dictionary.
HashSet< string > ProtectedEntryNames
Gets or sets the list of entry names that should be protected by the bundle from accidental modificat...
Definition Manifest.cs:39
Represents a collection of manifest signatures.
Definition Signatures.cs:9
Dictionary< string, byte[]> Certificates
Gets or sets the signature certificates.
Definition Signatures.cs:18
Dictionary< string, byte[]> Entries
Gets or sets the signature entries.
Definition Signatures.cs:13
ReadSource
Specifies the source from which to read the data.
Definition ReadSource.cs:7