// Copyright (C) 2024, The Duplicati Team // https://duplicati.com, hello@duplicati.com // // Permission is hereby granted, free of charge, to any person obtaining a // copy of this software and associated documentation files (the "Software"), // to deal in the Software without restriction, including without limitation // the rights to use, copy, modify, merge, publish, distribute, sublicense, // and/or sell copies of the Software, and to permit persons to whom the // Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. using System; using System.Collections.Generic; using System.Linq; using System.Text; using Newtonsoft.Json; using System.IO; using Duplicati.Library.Interface; namespace Duplicati.Library.Main.Volumes { public class FilesetVolumeReader : VolumeReaderBase { private class FileEntryEnumerable : IEnumerable { private class FileEntryEnumerator : IEnumerator { private class FileEntry : IFileEntry { private class BlocklistHashEnumerable : IEnumerable { private class BlocklistHashEnumerator : IEnumerator { private readonly JsonReader m_reader; private string m_current; private bool m_firstReset = true; private bool m_done = false; public BlocklistHashEnumerator(JsonReader reader) { m_reader = reader; } public string Current { get { return m_current; } } public void Dispose() { } object System.Collections.IEnumerator.Current { get { return this.Current; } } public bool MoveNext() { m_firstReset = false; if (m_reader == null || m_done) return false; if (!m_reader.Read()) throw new InvalidDataException("Got EOF while reading blocklist hashes"); if (m_reader.TokenType == JsonToken.EndArray) { m_done = true; return false; } if (m_reader.TokenType != JsonToken.String) throw new InvalidDataException(string.Format("Invalid JSON, expected String, but found {0}, {1}", m_reader.TokenType, m_reader.Value)); m_current = m_reader.Value == null ? null : m_reader.Value.ToString(); return true; } public void Reset() { if (!m_firstReset) throw new NotImplementedException(); } } private readonly JsonReader m_reader; public BlocklistHashEnumerable(JsonReader reader) { m_reader = reader; } public IEnumerator GetEnumerator() { return new BlocklistHashEnumerator(m_reader); } System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { return this.GetEnumerator(); } } public FilelistEntryType Type { get; private set; } public string TypeString { get; private set; } public string Path { get; private set; } public string Hash { get; private set; } public long Size { get; private set; } public DateTime Time { get; private set; } public string Metahash { get; private set; } public string Metablockhash { get; private set; } public long Metasize { get; private set; } public string Blockhash { get; private set; } public long Blocksize { get; private set; } public IEnumerable BlocklistHashes { get; private set; } public IEnumerable MetaBlocklistHashes { get; private set; } private readonly JsonReader m_reader; public FileEntry(JsonReader reader) { m_reader = reader; this.TypeString = ReadJsonStringProperty(m_reader, "type"); FilelistEntryType et; if (!Enum.TryParse(this.TypeString, true, out et)) et = FilelistEntryType.Unknown; this.Type = et; if (this.Type == FilelistEntryType.Unknown) { //Skip this entry by finding the EndObject that matches the StartObject long balance = 1; while (balance > 0 && m_reader.Read()) { if (m_reader.TokenType == JsonToken.StartObject) balance++; else if (m_reader.TokenType == JsonToken.EndObject) balance--; } if (balance != 0) throw new InvalidDataException(string.Format("Invalid JSON, EOF found while reading entry of type {0}", this.TypeString)); } else { this.Path = ReadJsonStringProperty(m_reader, "path"); if (this.Type == FilelistEntryType.File || this.Type == FilelistEntryType.AlternateStream) { this.Hash = ReadJsonStringProperty(m_reader, "hash"); this.Size = ReadJsonInt64Property(m_reader, "size"); this.Time = ReadJsonDateTimeProperty(m_reader, "time"); } if (!m_reader.Read()) throw new InvalidDataException(string.Format("Invalid JSON, EOF found while reading entry {0}", this.Path)); if (m_reader.TokenType == JsonToken.PropertyName && m_reader.Value != null && m_reader.Value.ToString() == "metahash") { if (!m_reader.Read()) throw new InvalidDataException(string.Format("Invalid JSON, EOF found while reading entry {0}", this.Path)); this.Metahash = m_reader.Value.ToString(); this.Metasize = ReadJsonInt64Property(m_reader, "metasize"); if (!m_reader.Read()) throw new InvalidDataException(string.Format("Invalid JSON, EOF found while reading entry {0}", this.Path)); if (m_reader.TokenType == JsonToken.PropertyName && m_reader.Value != null && m_reader.Value.ToString() == "metablocklists") { var metadatablocklisthashes = new List(); SkipJsonToken(m_reader, JsonToken.StartArray); if (!m_reader.Read()) throw new InvalidDataException(string.Format("Invalid JSON, EOF found while reading entry {0}", this.Path)); while(m_reader.TokenType == JsonToken.String) { metadatablocklisthashes.Add(m_reader.Value.ToString()); if (!m_reader.Read()) throw new InvalidDataException(string.Format("Invalid JSON, EOF found while reading entry {0}", this.Path)); } if (m_reader.TokenType != JsonToken.EndArray) throw new InvalidDataException(string.Format("Invalid JSON, unexpected token {1} found while reading entry {0}", this.Path, m_reader.TokenType)); if (!m_reader.Read()) throw new InvalidDataException(string.Format("Invalid JSON, EOF found while reading entry {0}", this.Path)); this.MetaBlocklistHashes = metadatablocklisthashes; this.Metablockhash = null; } else if (m_reader.TokenType == JsonToken.PropertyName && m_reader.Value != null && m_reader.Value.ToString() == "metablockhash") { if (!m_reader.Read()) throw new InvalidDataException(string.Format("Invalid JSON, EOF found while reading entry {0}", this.Path)); this.Metablockhash = m_reader.Value.ToString(); if (!m_reader.Read()) throw new InvalidDataException(string.Format("Invalid JSON, EOF found while reading entry {0}", this.Path)); this.MetaBlocklistHashes = null; } } if ((this.Type == FilelistEntryType.File || this.Type == FilelistEntryType.AlternateStream) && m_reader.TokenType == JsonToken.PropertyName && m_reader.Value != null && m_reader.Value.ToString() == "blocklists") { SkipJsonToken(m_reader, JsonToken.StartArray); this.BlocklistHashes = new BlocklistHashEnumerable(m_reader); } else if ((this.Type == FilelistEntryType.File || this.Type == FilelistEntryType.AlternateStream) && m_reader.TokenType == JsonToken.PropertyName && m_reader.Value != null && m_reader.Value.ToString() == "blockhash") { if (!m_reader.Read()) throw new InvalidDataException(string.Format("Invalid JSON, EOF found while reading entry {0}", this.Path)); this.Blockhash = m_reader.Value.ToString(); this.Blocksize = ReadJsonInt64Property(m_reader, "blocksize"); this.BlocklistHashes = null; } else { this.BlocklistHashes = null; if (m_reader.TokenType != JsonToken.EndObject) throw new InvalidDataException(string.Format("Invalid JSON, expected EndObject, but found {0}, \"{1}\" while reading entry {2}", m_reader.TokenType, m_reader.Value, this.Path)); } } } } private readonly ICompression m_compression; private FileEntry m_current; private StreamReader m_stream; private JsonReader m_reader; private bool m_done; private bool m_first; public FileEntryEnumerator(ICompression compression) { m_compression = compression; this.Reset(); } public IFileEntry Current { get { return m_current; } } public void Dispose() { if (m_reader != null) try { m_reader.Close(); } finally { m_reader = null; } if (m_stream != null) try { m_stream.Dispose(); } finally { m_stream = null; } } object System.Collections.IEnumerator.Current { get { return this.Current; } } public bool MoveNext() { if (m_done) return false; if (m_first) { m_first = false; } else { while (m_reader.TokenType != JsonToken.EndObject && m_reader.Read()) { /*skip */ } } if (!m_reader.Read()) throw new InvalidDataException("Invalid JSON, EOF found while reading hashes"); if (m_reader.TokenType == JsonToken.EndArray) { m_done = true; m_current = null; return false; } if (m_reader.TokenType != JsonToken.StartObject) throw new InvalidDataException(string.Format("Invalid JSON, expected StartObject, but got {0}, {1}", m_reader.TokenType, m_reader.Value)); m_current = new FileEntry(m_reader); return true; } public void Reset() { this.Dispose(); m_stream = new StreamReader(m_compression.OpenRead(FILELIST)); m_reader = new JsonTextReader(m_stream); SkipJsonToken(m_reader, JsonToken.StartArray); m_current = null; m_done = false; m_first = true; } } private readonly ICompression m_compression; public FileEntryEnumerable(ICompression compression) { m_compression = compression; } public IEnumerator GetEnumerator() { return new FileEntryEnumerator(m_compression); } System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { return this.GetEnumerator(); } } private class ControlFileEnumerable : IEnumerable> { private class ControlFileEnumerator : IEnumerator> { private readonly ICompression m_compression; private string[] m_files; private long m_index; private KeyValuePair? m_current; public ControlFileEnumerator(ICompression compression) { m_compression = compression; this.Reset(); } public KeyValuePair Current { get { return m_current.Value; } } object System.Collections.IEnumerator.Current { get { return this.Current; } } public void Dispose() { if (m_current != null) try { m_current.Value.Value.Dispose(); } finally { m_current = null; } } public bool MoveNext() { if (m_index + 1 >= m_files.Length) return false; m_index++; if (m_current != null) try { m_current.Value.Value.Dispose(); } finally { m_current = null; } m_current = new KeyValuePair(m_files[m_index].Substring(CONTROL_FILES_FOLDER.Length), m_compression.OpenRead(m_files[m_index])); return true; } public void Reset() { m_files = m_compression.ListFiles(CONTROL_FILES_FOLDER); m_index = -1; } } private readonly ICompression m_compression; public ControlFileEnumerable(ICompression compression) { m_compression = compression; } public IEnumerator> GetEnumerator() { return new ControlFileEnumerator(m_compression); } System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { return this.GetEnumerator(); } } public FilesetVolumeReader(ICompression compression, Options options) : base(compression, options) { } public FilesetVolumeReader(string compressor, string file, Options options) : base(compressor, file, options) { } public IEnumerable Files { get { return new FileEntryEnumerable(m_compression); } } public IEnumerable> ControlFiles { get { return new ControlFileEnumerable(m_compression); } } } }