2025-02-06 21:23:07 +01:00
// Copyright (C) 2025, 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
2024-05-21 15:48:29 +02:00
// DEALINGS IN THE SOFTWARE.
2024-02-28 15:45:30 +01:00
using Duplicati.Library.Interface ;
2024-09-09 15:27:37 +02:00
using Duplicati.Library.Utility ;
2013-04-04 20:34:26 +02:00
using Newtonsoft.Json ;
2017-12-25 04:12:19 +07:00
using System ;
using System.Collections.Generic ;
2013-04-04 20:34:26 +02:00
using System.IO ;
2013-05-08 20:17:07 +02:00
namespace Duplicati.Library.Main.Volumes
2013-04-04 20:34:26 +02:00
{
public abstract class VolumeReaderBase : VolumeBase , IDisposable
{
2019-09-10 13:07:57 -04:00
public bool IsFullBackup { get ; set ; }
2019-10-19 10:56:21 -07:00
protected readonly bool m_disposeCompression = false ;
2013-04-04 20:34:26 +02:00
protected ICompression m_compression ;
2017-12-25 04:12:19 +07:00
protected Stream m_stream ;
2024-05-21 15:48:29 +02:00
2017-12-25 04:12:19 +07:00
private static ICompression LoadCompressor ( string compressor , Stream stream , Options options )
2013-04-04 20:34:26 +02:00
{
2017-12-25 04:12:19 +07:00
var tmp = DynamicLoader . CompressionLoader . GetModule ( compressor , stream , Interface . ArchiveMode . Read , options . RawOptions );
2013-04-04 20:34:26 +02:00
if ( tmp == null )
2017-12-25 04:12:19 +07:00
{
var name = "[stream]" ;
2019-09-29 20:16:28 -07:00
if ( stream is FileStream fileStream )
name = fileStream . Name ;
2017-12-25 04:12:19 +07:00
throw new Exception ( string . Format ( "Unable to create {0} decompressor on file {1}" , compressor , name ));
}
2013-04-04 20:34:26 +02:00
return tmp ;
}
2017-12-25 04:12:19 +07:00
private static ICompression LoadCompressor ( string compressor , string file , Options options , out Stream stream )
{
stream = new System . IO . FileStream ( file , FileMode . Open , FileAccess . Read , FileShare . Read );
return LoadCompressor ( compressor , stream , options );
}
2018-04-07 17:58:46 -07:00
protected VolumeReaderBase ( string compressor , string file , Options options )
2017-12-25 04:12:19 +07:00
: base ( options )
2013-04-04 20:34:26 +02:00
{
2017-12-25 04:12:19 +07:00
m_compression = LoadCompressor ( compressor , file , options , out m_stream );
2019-09-10 13:07:57 -04:00
ReadFileset ();
ReadManifests ( options );
2024-05-21 15:48:29 +02:00
2013-04-04 20:34:26 +02:00
m_disposeCompression = true ;
}
2018-04-07 17:58:46 -07:00
protected VolumeReaderBase ( ICompression compression , Options options )
2013-04-04 20:34:26 +02:00
: base ( options )
{
m_compression = compression ;
2017-12-25 04:12:19 +07:00
ReadManifests ( options );
}
2019-09-10 13:07:57 -04:00
private void ReadFileset ()
{
using ( var s = m_compression . OpenRead ( FILESET_FILENAME ))
{
if ( s == null )
{
IsFullBackup = new FilesetData (). IsFullBackup ; // use default value
}
else
{
using ( var fs = new StreamReader ( s , ENCODING ))
{
FilesetData fileset = JsonConvert . DeserializeObject < FilesetData >( fs . ReadToEnd ());
IsFullBackup = fileset . IsFullBackup ;
}
}
}
}
2017-12-25 04:12:19 +07:00
private void ReadManifests ( Options options )
{
2019-09-10 13:07:57 -04:00
if ( options . DontReadManifests ) return ;
using ( var s = m_compression . OpenRead ( MANIFEST_FILENAME ))
2013-04-04 20:34:26 +02:00
{
2019-09-10 13:07:57 -04:00
if ( s == null )
2013-05-08 19:57:13 +02:00
{
2019-09-10 13:07:57 -04:00
throw new InvalidManifestException ( "No manifest file found in volume" );
}
2017-12-25 04:12:19 +07:00
2019-09-10 13:07:57 -04:00
using ( var fs = new StreamReader ( s , ENCODING ))
{
ManifestData . VerifyManifest ( fs . ReadToEnd (), m_blocksize , options . BlockHashAlgorithm , options . FileHashAlgorithm );
}
}
}
public static FilesetData GetFilesetData ( string compressor , string file , Options options )
{
using ( var stream = new FileStream ( file , FileMode . Open , FileAccess . Read , FileShare . Read ))
using ( var c = LoadCompressor ( compressor , stream , options ))
2025-02-06 21:23:07 +01:00
return GetFilesetData ( c , options );
}
public static FilesetData GetFilesetData ( ICompression compressor , Options options )
{
using ( var s = compressor . OpenRead ( FILESET_FILENAME ))
2019-09-10 13:07:57 -04:00
{
if ( s == null )
{
return new FilesetData (); // return default
}
using ( var fs = new StreamReader ( s , ENCODING ))
{
return JsonConvert . DeserializeObject < FilesetData >( fs . ReadToEnd ());
2013-05-08 19:57:13 +02:00
}
2013-04-04 20:34:26 +02:00
}
}
2024-05-21 15:48:29 +02:00
/// <summary>
/// Updates the options with data from the manifest file, but does not overwrite existing values
/// </summary>
/// <param name="compressor">The compressor to use</param>
/// <param name="file">The file to read the manifest from</param>
/// <param name="options">The options to update</param>
2015-04-08 21:01:36 +02:00
public static void UpdateOptionsFromManifest ( string compressor , string file , Options options )
{
2017-12-25 04:12:19 +07:00
using ( var stream = new System . IO . FileStream ( file , FileMode . Open , FileAccess . Read , FileShare . Read ))
using ( var c = LoadCompressor ( compressor , stream , options ))
2025-02-06 21:23:07 +01:00
UpdateOptionsFromManifest ( c , options );
}
/// <summary>
/// Updates the options with data from the manifest file, but does not overwrite existing values
/// </summary>
/// <param name="compressor">The compressor to use</param>
/// <param name="file">The file to read the manifest from</param>
/// <param name="options">The options to update</param>
public static void UpdateOptionsFromManifest ( ICompression compressor , Options options )
{
using ( var s = compressor . OpenRead ( MANIFEST_FILENAME ))
2015-04-08 21:01:36 +02:00
using ( var fs = new StreamReader ( s , ENCODING ))
{
var d = JsonConvert . DeserializeObject < ManifestData >( fs . ReadToEnd ());
if ( d . Version > ManifestData . VERSION )
throw new InvalidManifestException ( "Version" , d . Version . ToString (), ManifestData . VERSION . ToString ());
string n ;
if (! options . RawOptions . TryGetValue ( "blocksize" , out n ) || string . IsNullOrEmpty ( n ))
2019-09-10 13:07:57 -04:00
options . RawOptions [ "blocksize" ] = d . Blocksize + "b" ;
2015-04-08 21:01:36 +02:00
if (! options . RawOptions . TryGetValue ( "block-hash-algorithm" , out n ) || string . IsNullOrEmpty ( n ))
options . RawOptions [ "block-hash-algorithm" ] = d . BlockHash ;
if (! options . RawOptions . TryGetValue ( "file-hash-algorithm" , out n ) || string . IsNullOrEmpty ( n ))
options . RawOptions [ "file-hash-algorithm" ] = d . FileHash ;
}
}
2013-04-04 20:34:26 +02:00
public virtual void Dispose ()
{
if ( m_disposeCompression && m_compression != null )
{
m_compression . Dispose ();
m_compression = null ;
2017-12-25 04:12:19 +07:00
m_stream ?. Dispose ();
m_stream = null ;
2013-04-04 20:34:26 +02:00
}
}
2024-09-09 15:27:37 +02:00
/// <summary>
/// Reads the blocklist from the file, not checking if the hash is correct
/// </summary>
/// <param name="compression">The compression to use</param>
/// <param name="filename">The file to read the blocklist from</param>
/// <param name="hashsize">The size of the hash</param>
/// <returns>The blocklist</returns>
public static IEnumerable < string > ReadBlocklistUnverified ( ICompression compression , string filename , long hashsize )
2016-03-17 21:41:42 +01:00
{
var buffer = new byte [ hashsize ];
2017-12-25 04:12:19 +07:00
using ( var fs = compression . OpenRead ( filename ))
2016-03-17 21:41:42 +01:00
{
int s ;
2024-05-21 15:48:29 +02:00
var read = 0L ;
2016-03-17 21:41:42 +01:00
while (( s = Library . Utility . Utility . ForceStreamRead ( fs , buffer , buffer . Length )) != 0 )
2017-12-25 04:12:19 +07:00
{
2016-03-17 21:41:42 +01:00
if ( s != buffer . Length )
2024-05-21 15:48:29 +02:00
throw new InvalidDataException ( $"Premature End-of-stream encountered while reading blocklist hashes for {filename}. Got {s} bytes of {buffer.Length} at offset {read * buffer.Length}" );
2016-03-17 21:41:42 +01:00
2024-05-21 15:48:29 +02:00
read ++;
2016-03-17 21:41:42 +01:00
yield return Convert . ToBase64String ( buffer );
}
}
}
2023-11-30 22:37:07 +01:00
/// <summary>
/// Read blocklist and check the hash. Throws InvalidDataException if not matching
/// </summary>
2024-09-09 15:27:37 +02:00
/// <param name="compression">The compression to use</param>
/// <param name="filename">The file to read the blocklist from</param>
/// <param name="hashsize">The size of the hash</param>
/// <param name="hash">The hash to check against</param>
/// <param name="blockHashAlgorithm">The block hash algorithm to use</param>
public static IEnumerable < string > ReadBlocklistVerified ( ICompression compression , string filename , long hashsize , string hash , string blockHashAlgorithm )
2023-11-30 22:37:07 +01:00
{
var buffer = new byte [ hashsize ];
2024-09-09 15:27:37 +02:00
using var hashalg = HashFactory . CreateHasher ( blockHashAlgorithm );
using var fs = compression . OpenRead ( filename );
int s ;
var read = 0L ;
while (( s = Library . Utility . Utility . ForceStreamRead ( fs , buffer , buffer . Length )) != 0 )
2023-11-30 22:37:07 +01:00
{
2024-09-09 15:27:37 +02:00
if ( s != buffer . Length )
throw new InvalidDataException ( $"Premature End-of-stream encountered while reading blocklist hashes for {filename}. Got {s} bytes of {buffer.Length} at offset {read * buffer.Length}" );
2023-11-30 22:37:07 +01:00
2024-09-09 15:27:37 +02:00
read ++;
hashalg . TransformBlock ( buffer , 0 , s , buffer , 0 );
yield return Convert . ToBase64String ( buffer );
2023-11-30 22:37:07 +01:00
}
2024-09-09 15:27:37 +02:00
2023-11-30 22:37:07 +01:00
hashalg . TransformFinalBlock ( buffer , 0 , 0 );
2024-09-09 15:27:37 +02:00
var calculatedHash = Convert . ToBase64String ( hashalg . Hash );
2023-11-30 22:37:07 +01:00
if ( hash != calculatedHash )
throw new InvalidDataException ( $"Blocklist hash does not match: expected {hash}, got {calculatedHash}" );
}
2013-04-04 20:34:26 +02:00
protected static object SkipJsonToken ( JsonReader reader , JsonToken type )
{
if (! reader . Read () || reader . TokenType != type )
throw new InvalidDataException ( string . Format ( "Invalid JSON, expected \"{0}\", but got {1}, {2}" , type , reader . TokenType , reader . Value ));
return reader . Value ;
}
protected static object ReadJsonProperty ( JsonReader reader , string propertyname , JsonToken type )
{
var p = SkipJsonToken ( reader , JsonToken . PropertyName );
if ( p == null || p . ToString () != propertyname )
throw new InvalidDataException ( string . Format ( "Invalid JSON, expected property \"{0}\", but got {1}, {2}" , propertyname , reader . TokenType , reader . Value ));
return SkipJsonToken ( reader , type );
}
protected static string ReadJsonStringProperty ( JsonReader reader , string propertyname )
{
return ReadJsonProperty ( reader , propertyname , JsonToken . String ). ToString ();
}
protected static long ReadJsonInt64Property ( JsonReader reader , string propertyname )
{
return Convert . ToInt64 ( ReadJsonProperty ( reader , propertyname , JsonToken . Integer ));
}
protected static DateTime ReadJsonDateTimeProperty ( JsonReader reader , string propertyname )
{
2013-05-08 21:29:59 +02:00
return Library . Utility . Utility . DeserializeDateTime ( ReadJsonStringProperty ( reader , propertyname ));
2013-04-04 20:34:26 +02:00
}
}
}