// 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 System.IO; using Newtonsoft.Json; using Duplicati.Library.Interface; namespace Duplicati.Library.Main.Volumes { public class IndexVolumeReader : VolumeReaderBase { public interface IIndexBlocklist { string Hash { get; } long Length { get; } Stream Data { get; } IEnumerable Blocklist { get; } } public interface IIndexBlockVolume { string Filename { get; } long Length { get; } string Hash { get; } IEnumerable> Blocks { get; } } private class IndexBlockVolumeEnumerable : IEnumerable { private readonly ICompression m_compression; public IndexBlockVolumeEnumerable(ICompression compression) { m_compression = compression; } private class IndexBlockVolumeEnumerator : IEnumerator { private class BlockEnumerable : IEnumerable> { private class BlockEnumerator : IEnumerator> { private readonly ICompression m_compression; private readonly string m_filename; private KeyValuePair? m_current; private System.IO.StreamReader m_stream; private JsonReader m_reader; private bool m_done; private KeyValuePair? m_volumeProps = null; public BlockEnumerator(ICompression compression, string filename) { m_compression = compression; m_filename = filename; this.Reset(); } public KeyValuePair Current { get { return m_current.Value; } } public void Dispose() { if (m_reader != null) this.ReadVolumeProps(); 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_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)); var hash = ReadJsonStringProperty(m_reader, "hash"); var size = ReadJsonInt64Property(m_reader, "size"); m_current = new KeyValuePair(hash, size); while (m_reader.Read() && m_reader.TokenType != JsonToken.EndObject) { /* skip */ } return true; } public KeyValuePair ReadVolumeProps() { if (m_volumeProps != null) return m_volumeProps.Value; while (this.MoveNext()) { /*skip*/ } var hash = ReadJsonStringProperty(m_reader, "volumehash"); var size = ReadJsonInt64Property(m_reader, "volumesize"); return (m_volumeProps = new KeyValuePair(hash, size)).Value; } public void Reset() { this.Dispose(); m_stream = new StreamReader(m_compression.OpenRead(m_filename)); m_reader = new JsonTextReader(m_stream); SkipJsonToken(m_reader, JsonToken.StartObject); var p = SkipJsonToken(m_reader, JsonToken.PropertyName); if (p == null || p.ToString() != "blocks") throw new InvalidDataException(string.Format("Invalid JSON, expected property \"blocks\", but got {0}, {1}", m_reader.TokenType, m_reader.Value)); SkipJsonToken(m_reader, JsonToken.StartArray); m_current = null; m_done = false; } } private readonly ICompression m_compression; private readonly string m_filename; private BlockEnumerator m_enumerator; public BlockEnumerable(ICompression compression, string filename) { m_compression = compression; m_filename = filename; } public KeyValuePair ReadVolumeProps() { if (m_enumerator == null) m_enumerator = (BlockEnumerator)this.GetEnumerator(); return m_enumerator.ReadVolumeProps(); } public IEnumerator> GetEnumerator() { if (m_enumerator != null) throw new NotSupportedException("Cannot read block stream twice"); return m_enumerator = new BlockEnumerator(m_compression, m_filename); } System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { return this.GetEnumerator(); } } private class IndexBlockVolume : IIndexBlockVolume { private readonly ICompression m_compression; private readonly string m_filename; private long? m_length; private string m_hash; private BlockEnumerable m_blocks; public IndexBlockVolume(ICompression compression, string filename) { m_compression = compression; m_filename = filename; } public string Filename { get { return m_filename.Substring(INDEX_VOLUME_FOLDER.Length); } } private void ReadVolumeProps() { if (m_length == null || m_hash == null) { if (m_blocks == null) m_blocks = (BlockEnumerable)this.Blocks; var kp = m_blocks.ReadVolumeProps(); m_length = kp.Value; m_hash = kp.Key; } } public long Length { get { this.ReadVolumeProps(); return m_length.Value; } } public string Hash { get { this.ReadVolumeProps(); return m_hash; } } public IEnumerable> Blocks { get { var n = new BlockEnumerable(m_compression, m_filename); if (m_blocks == null) m_blocks = n; return n; } } } private readonly ICompression m_compression; private IndexBlockVolume m_current; private string[] m_files; private long m_index; public IndexBlockVolumeEnumerator(ICompression compression) { m_compression = compression; this.Reset(); } public IIndexBlockVolume Current { get { return m_current; } } public void Dispose() { } object System.Collections.IEnumerator.Current { get { return this.Current; } } public bool MoveNext() { if (m_index + 1 >= m_files.Length) return false; m_index++; while (m_index < m_files.Length && ParseFilename(m_files[m_index]) == null) m_index++; m_current = new IndexBlockVolume(m_compression, m_files[m_index]); return true; } public void Reset() { m_files = m_compression.ListFiles(INDEX_VOLUME_FOLDER); m_index = -1; m_current = null; } } public IEnumerator GetEnumerator() { return new IndexBlockVolumeEnumerator(m_compression); } System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { return this.GetEnumerator(); } } private class IndexBlocklistEnumerable : IEnumerable { private readonly ICompression m_compression; private readonly long m_hashsize; public IndexBlocklistEnumerable(ICompression compression, long hashsize) { m_compression = compression; m_hashsize = hashsize; } private class IndexBlocklistEnumerator : IEnumerator { private class IndexBlocklist : IIndexBlocklist { private readonly ICompression m_compression; private readonly string m_filename; private readonly long m_size; private readonly long m_hashsize; public IndexBlocklist(ICompression compression, string filename, long size, long hashsize) { m_compression = compression; m_filename = filename; m_size = size; m_hashsize = hashsize; } public string Hash { //Filenames are encoded with "modified Base64 for URL" https://en.wikipedia.org/wiki/Base64#URL_applications, // to prevent clashes with filename paths where forward slash has special meaning get { return Library.Utility.Utility.Base64UrlToBase64Plain(m_filename.Substring(INDEX_BLOCKLIST_FOLDER.Length)); } } public long Length { get { return m_size; } } public Stream Data { get { return m_compression.OpenRead(m_filename); } } public IEnumerable Blocklist { get { return VolumeReaderBase.ReadBlocklist(m_compression, m_filename, m_hashsize); } } } private readonly ICompression m_compression; private long m_index; private KeyValuePair[] m_files; private IndexBlocklist m_current; private readonly long m_hashsize; public IndexBlocklistEnumerator(ICompression compression, long hashsize) { m_compression = compression; m_hashsize = hashsize; this.Reset(); } public IIndexBlocklist Current { get { return m_current; } } public void Dispose() { } object System.Collections.IEnumerator.Current { get { return this.Current; } } private readonly System.Text.RegularExpressions.Regex m_base64_urlsafe_detector = new System.Text.RegularExpressions.Regex("[a-zA-Z0-9-_]+={0,2}"); private bool IsValidBase64Hash(string value, long hashsize) { if (value.Length != ((hashsize + 2) / 3) * 4) return false; var m = m_base64_urlsafe_detector.Match(value); return m.Success && m.Length == value.Length; } public bool MoveNext() { if (m_index + 1 >= m_files.Length) return false; m_index++; while (m_index < m_files.Length && IsValidBase64Hash(m_files[m_index].Key, m_hashsize)) m_index++; m_current = new IndexBlocklist(m_compression, m_files[m_index].Key, m_files[m_index].Value, m_hashsize); return true; } public void Reset() { m_files = m_compression.ListFilesWithSize(INDEX_BLOCKLIST_FOLDER).ToArray(); m_index = -1; m_current = null; } } public IEnumerator GetEnumerator() { return new IndexBlocklistEnumerator(m_compression, m_hashsize); } System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { return this.GetEnumerator(); } } private readonly long m_hashsize; public IndexVolumeReader(ICompression compression, Options options, long hashsize) : base(compression, options) { m_hashsize = hashsize; } public IndexVolumeReader(string compressor, string file, Options options, long hashsize) : base(compressor, file, options) { m_hashsize = hashsize; } public IEnumerable Volumes { get { return new IndexBlockVolumeEnumerable(m_compression); } } public IEnumerable BlockLists { get { return new IndexBlocklistEnumerable(m_compression, m_hashsize); } } } }