Initial BinaryConverter implementation.
This commit is contained in:
@@ -0,0 +1,11 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = crlf
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.cs]
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
@@ -0,0 +1,5 @@
|
||||
* text=auto
|
||||
*.cs text eol=crlf
|
||||
*.csproj text eol=crlf
|
||||
*.sln text eol=crlf
|
||||
*.md text eol=lf
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
bin/
|
||||
obj/
|
||||
.vs/
|
||||
.vscode/
|
||||
*.user
|
||||
*.suo
|
||||
*.userosscache
|
||||
*.sln.docstates
|
||||
TestResults/
|
||||
*.nupkg
|
||||
*.snupkg
|
||||
*.log
|
||||
*.tmp
|
||||
*.bak
|
||||
*.b64.txt
|
||||
@@ -0,0 +1,21 @@
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.0.31903.59
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BinaryConverter", "BinaryConverter\BinaryConverter.csproj", "{B2D8CB84-1E44-4F67-B6D0-6F9FB3480D5C}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{B2D8CB84-1E44-4F67-B6D0-6F9FB3480D5C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{B2D8CB84-1E44-4F67-B6D0-6F9FB3480D5C}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{B2D8CB84-1E44-4F67-B6D0-6F9FB3480D5C}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{B2D8CB84-1E44-4F67-B6D0-6F9FB3480D5C}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<configuration>
|
||||
<startup>
|
||||
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.8.1" />
|
||||
</startup>
|
||||
</configuration>
|
||||
@@ -0,0 +1,47 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<Import Project="$(MSBuildToolsPath)\Microsoft.Common.props" Condition="Exists('$(MSBuildToolsPath)\Microsoft.Common.props')" />
|
||||
<PropertyGroup>
|
||||
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
|
||||
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
|
||||
<ProjectGuid>{B2D8CB84-1E44-4F67-B6D0-6F9FB3480D5C}</ProjectGuid>
|
||||
<OutputType>Exe</OutputType>
|
||||
<RootNamespace>BinaryConverter</RootNamespace>
|
||||
<AssemblyName>BinaryConverter</AssemblyName>
|
||||
<TargetFrameworkVersion>v4.8.1</TargetFrameworkVersion>
|
||||
<FileAlignment>512</FileAlignment>
|
||||
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
|
||||
<Deterministic>true</Deterministic>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
||||
<PlatformTarget>AnyCPU</PlatformTarget>
|
||||
<DebugSymbols>true</DebugSymbols>
|
||||
<DebugType>full</DebugType>
|
||||
<Optimize>false</Optimize>
|
||||
<OutputPath>bin\Debug\</OutputPath>
|
||||
<DefineConstants>DEBUG;TRACE</DefineConstants>
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
<WarningLevel>4</WarningLevel>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
|
||||
<PlatformTarget>AnyCPU</PlatformTarget>
|
||||
<DebugType>pdbonly</DebugType>
|
||||
<Optimize>true</Optimize>
|
||||
<OutputPath>bin\Release\</OutputPath>
|
||||
<DefineConstants>TRACE</DefineConstants>
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
<WarningLevel>4</WarningLevel>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Reference Include="System" />
|
||||
<Reference Include="System.Core" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="Program.cs" />
|
||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="App.config" />
|
||||
</ItemGroup>
|
||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||
</Project>
|
||||
@@ -0,0 +1,864 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
|
||||
namespace BinaryConverter
|
||||
{
|
||||
internal static class Program
|
||||
{
|
||||
private const int RawReadBufferSize = 1024 * 1024;
|
||||
private const int EncodeBlockSize = 57 * 4096;
|
||||
private const int Base64LineLength = 76;
|
||||
private const string BeginMarker = "-----BEGIN BINARYCONVERTER BASE64-----";
|
||||
private const string EndMarker = "-----END BINARYCONVERTER BASE64-----";
|
||||
|
||||
private static int Main(string[] args)
|
||||
{
|
||||
Options options;
|
||||
string parseError;
|
||||
|
||||
if (!Options.TryParse(args, out options, out parseError))
|
||||
{
|
||||
Console.Error.WriteLine(parseError);
|
||||
Console.Error.WriteLine();
|
||||
PrintUsage();
|
||||
return 2;
|
||||
}
|
||||
|
||||
if (options.ShowHelp)
|
||||
{
|
||||
PrintUsage();
|
||||
return 0;
|
||||
}
|
||||
|
||||
Logger logger = null;
|
||||
|
||||
try
|
||||
{
|
||||
logger = new Logger(options.Quiet, options.LogPath);
|
||||
logger.Info("BinaryConverter " + Assembly.GetExecutingAssembly().GetName().Version);
|
||||
logger.Info("Operation: " + options.Mode);
|
||||
logger.Info("Input: " + Path.GetFullPath(options.InputPath));
|
||||
logger.Info("Output: " + Path.GetFullPath(options.OutputPath));
|
||||
|
||||
if (options.Mode == OperationMode.Encode)
|
||||
{
|
||||
Encode(options, logger);
|
||||
}
|
||||
else
|
||||
{
|
||||
Decode(options, logger);
|
||||
}
|
||||
|
||||
logger.Success("Finished successfully.");
|
||||
return 0;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (logger != null)
|
||||
{
|
||||
logger.Error(ex.Message);
|
||||
logger.Debug(ex.ToString());
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.Error.WriteLine(ex);
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (logger != null)
|
||||
{
|
||||
logger.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void Encode(Options options, Logger logger)
|
||||
{
|
||||
FileInfo inputInfo = new FileInfo(options.InputPath);
|
||||
if (!inputInfo.Exists)
|
||||
{
|
||||
throw new FileNotFoundException("Input file was not found.", options.InputPath);
|
||||
}
|
||||
|
||||
EnsureOutputCanBeWritten(options.OutputPath, options.Force);
|
||||
|
||||
string outputDirectory = Path.GetDirectoryName(Path.GetFullPath(options.OutputPath));
|
||||
if (!string.IsNullOrEmpty(outputDirectory))
|
||||
{
|
||||
Directory.CreateDirectory(outputDirectory);
|
||||
}
|
||||
|
||||
logger.Info("Reading input to calculate SHA-256.");
|
||||
string sha256 = ComputeSha256(options.InputPath, inputInfo.Length, options, logger);
|
||||
logger.Info("SHA-256: " + sha256);
|
||||
|
||||
logger.Info("Writing Base64 transfer file.");
|
||||
string tempOutputPath = options.OutputPath + ".tmp." + Guid.NewGuid().ToString("N");
|
||||
|
||||
try
|
||||
{
|
||||
using (FileStream input = new FileStream(options.InputPath, FileMode.Open, FileAccess.Read, FileShare.Read, RawReadBufferSize))
|
||||
using (StreamWriter writer = new StreamWriter(tempOutputPath, false, new UTF8Encoding(false), 64 * 1024))
|
||||
{
|
||||
writer.WriteLine("# BinaryConverter-Format: 1");
|
||||
writer.WriteLine("# Encoding: Base64");
|
||||
writer.WriteLine("# SourceFileNameBase64: " + Convert.ToBase64String(Encoding.UTF8.GetBytes(inputInfo.Name)));
|
||||
writer.WriteLine("# SourceLength: " + inputInfo.Length.ToString(CultureInfo.InvariantCulture));
|
||||
writer.WriteLine("# SHA256: " + sha256);
|
||||
writer.WriteLine("# CreatedUtc: " + DateTime.UtcNow.ToString("o", CultureInfo.InvariantCulture));
|
||||
writer.WriteLine("# LineLength: " + Base64LineLength.ToString(CultureInfo.InvariantCulture));
|
||||
writer.WriteLine(BeginMarker);
|
||||
|
||||
byte[] buffer = new byte[EncodeBlockSize];
|
||||
long processed = 0;
|
||||
int read;
|
||||
ProgressReporter progress = new ProgressReporter("Encode", inputInfo.Length, options.NoProgress, logger);
|
||||
|
||||
while ((read = input.Read(buffer, 0, buffer.Length)) > 0)
|
||||
{
|
||||
string encoded = Convert.ToBase64String(buffer, 0, read);
|
||||
for (int offset = 0; offset < encoded.Length; offset += Base64LineLength)
|
||||
{
|
||||
int length = Math.Min(Base64LineLength, encoded.Length - offset);
|
||||
writer.WriteLine(encoded.Substring(offset, length));
|
||||
}
|
||||
|
||||
processed += read;
|
||||
progress.Report(processed);
|
||||
}
|
||||
|
||||
writer.WriteLine(EndMarker);
|
||||
progress.Done(processed);
|
||||
}
|
||||
|
||||
MoveOrReplace(tempOutputPath, options.OutputPath);
|
||||
}
|
||||
catch
|
||||
{
|
||||
TryDelete(tempOutputPath);
|
||||
throw;
|
||||
}
|
||||
|
||||
logger.Info("Transfer file size: " + FormatBytes(new FileInfo(options.OutputPath).Length));
|
||||
}
|
||||
|
||||
private static void Decode(Options options, Logger logger)
|
||||
{
|
||||
FileInfo inputInfo = new FileInfo(options.InputPath);
|
||||
if (!inputInfo.Exists)
|
||||
{
|
||||
throw new FileNotFoundException("Input file was not found.", options.InputPath);
|
||||
}
|
||||
|
||||
EnsureOutputCanBeWritten(options.OutputPath, options.Force);
|
||||
|
||||
string outputDirectory = Path.GetDirectoryName(Path.GetFullPath(options.OutputPath));
|
||||
if (!string.IsNullOrEmpty(outputDirectory))
|
||||
{
|
||||
Directory.CreateDirectory(outputDirectory);
|
||||
}
|
||||
|
||||
string tempOutputPath = options.OutputPath + ".tmp." + Guid.NewGuid().ToString("N");
|
||||
DecodeMetadata metadata = new DecodeMetadata();
|
||||
long decodedBytes = 0;
|
||||
|
||||
try
|
||||
{
|
||||
using (FileStream output = new FileStream(tempOutputPath, FileMode.CreateNew, FileAccess.Write, FileShare.None, RawReadBufferSize))
|
||||
using (SHA256 sha256 = SHA256.Create())
|
||||
using (StreamReader reader = new StreamReader(options.InputPath, Encoding.UTF8, true, 64 * 1024))
|
||||
{
|
||||
logger.Info("Reading Base64 transfer file.");
|
||||
|
||||
bool sawBeginMarker = false;
|
||||
bool sawEndMarker = false;
|
||||
bool insidePayload = false;
|
||||
string line;
|
||||
int lineNumber = 0;
|
||||
StringBuilder carry = new StringBuilder(64 * 1024);
|
||||
ProgressReporter progress = new ProgressReporter("Decode input", inputInfo.Length, options.NoProgress, logger);
|
||||
|
||||
while ((line = reader.ReadLine()) != null)
|
||||
{
|
||||
lineNumber++;
|
||||
progress.Report(reader.BaseStream.Position);
|
||||
|
||||
string trimmed = line.Trim();
|
||||
if (trimmed.Length == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (trimmed.StartsWith("#", StringComparison.Ordinal))
|
||||
{
|
||||
metadata.TryParseComment(trimmed);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (string.Equals(trimmed, BeginMarker, StringComparison.Ordinal))
|
||||
{
|
||||
sawBeginMarker = true;
|
||||
insidePayload = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (string.Equals(trimmed, EndMarker, StringComparison.Ordinal))
|
||||
{
|
||||
sawEndMarker = true;
|
||||
DecodeBufferedBase64(carry, output, sha256, ref decodedBytes, true, lineNumber);
|
||||
insidePayload = false;
|
||||
break;
|
||||
}
|
||||
|
||||
if (sawBeginMarker && !insidePayload)
|
||||
{
|
||||
throw new InvalidDataException("Unexpected data after end marker at line " + lineNumber + ".");
|
||||
}
|
||||
|
||||
if (sawBeginMarker || !LooksLikeBinaryConverterEnvelopeLine(trimmed))
|
||||
{
|
||||
AppendBase64(trimmed, carry, lineNumber);
|
||||
DecodeBufferedBase64(carry, output, sha256, ref decodedBytes, false, lineNumber);
|
||||
}
|
||||
}
|
||||
|
||||
if (sawBeginMarker && !sawEndMarker)
|
||||
{
|
||||
throw new InvalidDataException("Transfer file is incomplete: missing end marker.");
|
||||
}
|
||||
|
||||
if (!sawEndMarker)
|
||||
{
|
||||
DecodeBufferedBase64(carry, output, sha256, ref decodedBytes, true, lineNumber);
|
||||
}
|
||||
|
||||
sha256.TransformFinalBlock(new byte[0], 0, 0);
|
||||
progress.Done(inputInfo.Length);
|
||||
|
||||
string actualHash = ToHex(sha256.Hash);
|
||||
ValidateDecodedOutput(metadata, decodedBytes, actualHash, logger);
|
||||
}
|
||||
|
||||
MoveOrReplace(tempOutputPath, options.OutputPath);
|
||||
logger.Info("Decoded file size: " + FormatBytes(decodedBytes));
|
||||
}
|
||||
catch
|
||||
{
|
||||
TryDelete(tempOutputPath);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private static string ComputeSha256(string path, long totalBytes, Options options, Logger logger)
|
||||
{
|
||||
using (FileStream input = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read, RawReadBufferSize))
|
||||
using (SHA256 sha256 = SHA256.Create())
|
||||
{
|
||||
byte[] buffer = new byte[RawReadBufferSize];
|
||||
long processed = 0;
|
||||
int read;
|
||||
ProgressReporter progress = new ProgressReporter("Hash", totalBytes, options.NoProgress, logger);
|
||||
|
||||
while ((read = input.Read(buffer, 0, buffer.Length)) > 0)
|
||||
{
|
||||
sha256.TransformBlock(buffer, 0, read, buffer, 0);
|
||||
processed += read;
|
||||
progress.Report(processed);
|
||||
}
|
||||
|
||||
sha256.TransformFinalBlock(new byte[0], 0, 0);
|
||||
progress.Done(processed);
|
||||
return ToHex(sha256.Hash);
|
||||
}
|
||||
}
|
||||
|
||||
private static void AppendBase64(string text, StringBuilder carry, int lineNumber)
|
||||
{
|
||||
for (int i = 0; i < text.Length; i++)
|
||||
{
|
||||
char c = text[i];
|
||||
if (char.IsWhiteSpace(c))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (IsBase64Char(c))
|
||||
{
|
||||
carry.Append(c);
|
||||
continue;
|
||||
}
|
||||
|
||||
throw new InvalidDataException("Invalid Base64 character at line " + lineNumber + ": '" + c + "'.");
|
||||
}
|
||||
}
|
||||
|
||||
private static void DecodeBufferedBase64(StringBuilder carry, Stream output, HashAlgorithm hash, ref long decodedBytes, bool final, int lineNumber)
|
||||
{
|
||||
if (carry.Length == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
int processLength;
|
||||
if (final)
|
||||
{
|
||||
if (carry.Length % 4 != 0)
|
||||
{
|
||||
throw new InvalidDataException("Base64 payload length is invalid near line " + lineNumber + ".");
|
||||
}
|
||||
|
||||
processLength = carry.Length;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (carry.Length <= 64 * 1024)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
processLength = ((carry.Length - 4) / 4) * 4;
|
||||
if (processLength <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < processLength; i++)
|
||||
{
|
||||
if (carry[i] == '=')
|
||||
{
|
||||
throw new InvalidDataException("Base64 padding was found before the end of the payload near line " + lineNumber + ".");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
string chunk = carry.ToString(0, processLength);
|
||||
byte[] decoded;
|
||||
try
|
||||
{
|
||||
decoded = Convert.FromBase64String(chunk);
|
||||
}
|
||||
catch (FormatException ex)
|
||||
{
|
||||
throw new InvalidDataException("Base64 payload is invalid near line " + lineNumber + ": " + ex.Message, ex);
|
||||
}
|
||||
|
||||
if (decoded.Length > 0)
|
||||
{
|
||||
output.Write(decoded, 0, decoded.Length);
|
||||
hash.TransformBlock(decoded, 0, decoded.Length, decoded, 0);
|
||||
decodedBytes += decoded.Length;
|
||||
}
|
||||
|
||||
carry.Remove(0, processLength);
|
||||
}
|
||||
|
||||
private static void ValidateDecodedOutput(DecodeMetadata metadata, long decodedBytes, string actualHash, Logger logger)
|
||||
{
|
||||
if (metadata.SourceLength.HasValue)
|
||||
{
|
||||
if (metadata.SourceLength.Value != decodedBytes)
|
||||
{
|
||||
throw new InvalidDataException(
|
||||
"Decoded file length does not match metadata. Expected " +
|
||||
metadata.SourceLength.Value.ToString(CultureInfo.InvariantCulture) +
|
||||
" bytes, got " + decodedBytes.ToString(CultureInfo.InvariantCulture) + " bytes.");
|
||||
}
|
||||
|
||||
logger.Info("Length check: OK (" + decodedBytes.ToString(CultureInfo.InvariantCulture) + " bytes)");
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.Warn("No source length metadata found; length validation skipped.");
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(metadata.Sha256))
|
||||
{
|
||||
if (!string.Equals(metadata.Sha256, actualHash, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
throw new InvalidDataException("SHA-256 check failed. Expected " + metadata.Sha256 + ", got " + actualHash + ".");
|
||||
}
|
||||
|
||||
logger.Info("SHA-256 check: OK (" + actualHash + ")");
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.Warn("No SHA-256 metadata found; hash validation skipped. Actual SHA-256: " + actualHash);
|
||||
}
|
||||
}
|
||||
|
||||
private static void EnsureOutputCanBeWritten(string outputPath, bool force)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(outputPath))
|
||||
{
|
||||
throw new ArgumentException("Output path is missing.");
|
||||
}
|
||||
|
||||
if (File.Exists(outputPath) && !force)
|
||||
{
|
||||
throw new IOException("Output file already exists. Use -Force to overwrite: " + outputPath);
|
||||
}
|
||||
}
|
||||
|
||||
private static void MoveOrReplace(string sourcePath, string destinationPath)
|
||||
{
|
||||
if (File.Exists(destinationPath))
|
||||
{
|
||||
File.Replace(sourcePath, destinationPath, null);
|
||||
return;
|
||||
}
|
||||
|
||||
File.Move(sourcePath, destinationPath);
|
||||
}
|
||||
|
||||
private static bool LooksLikeBinaryConverterEnvelopeLine(string trimmed)
|
||||
{
|
||||
return string.Equals(trimmed, BeginMarker, StringComparison.Ordinal) ||
|
||||
string.Equals(trimmed, EndMarker, StringComparison.Ordinal) ||
|
||||
trimmed.StartsWith("# BinaryConverter-", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private static bool IsBase64Char(char c)
|
||||
{
|
||||
return (c >= 'A' && c <= 'Z') ||
|
||||
(c >= 'a' && c <= 'z') ||
|
||||
(c >= '0' && c <= '9') ||
|
||||
c == '+' ||
|
||||
c == '/' ||
|
||||
c == '=';
|
||||
}
|
||||
|
||||
private static string ToHex(byte[] bytes)
|
||||
{
|
||||
StringBuilder builder = new StringBuilder(bytes.Length * 2);
|
||||
for (int i = 0; i < bytes.Length; i++)
|
||||
{
|
||||
builder.Append(bytes[i].ToString("x2", CultureInfo.InvariantCulture));
|
||||
}
|
||||
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
private static string FormatBytes(long value)
|
||||
{
|
||||
string[] units = { "B", "KB", "MB", "GB", "TB" };
|
||||
double size = value;
|
||||
int unit = 0;
|
||||
while (size >= 1024 && unit < units.Length - 1)
|
||||
{
|
||||
size /= 1024;
|
||||
unit++;
|
||||
}
|
||||
|
||||
return size.ToString(unit == 0 ? "0" : "0.00", CultureInfo.InvariantCulture) + " " + units[unit];
|
||||
}
|
||||
|
||||
private static void TryDelete(string path)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!string.IsNullOrEmpty(path) && File.Exists(path))
|
||||
{
|
||||
File.Delete(path);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
private static void PrintUsage()
|
||||
{
|
||||
Console.WriteLine("BinaryConverter - streaming binary transfer helper");
|
||||
Console.WriteLine();
|
||||
Console.WriteLine("Usage:");
|
||||
Console.WriteLine(" BinaryConverter -Encode <input-file> -Output <transfer-file> [-Force] [-Log <log-file>]");
|
||||
Console.WriteLine(" BinaryConverter -Decode <transfer-file> <output-file> [-Force] [-Log <log-file>]");
|
||||
Console.WriteLine(" BinaryConverter -Decode <transfer-file> -Output <output-file> [-Force] [-Log <log-file>]");
|
||||
Console.WriteLine();
|
||||
Console.WriteLine("Examples:");
|
||||
Console.WriteLine(@" BinaryConverter -Encode C:\temp\PowerShell.exe -Output Out.txt");
|
||||
Console.WriteLine(@" BinaryConverter -Decode Out.txt PowerShell.exe");
|
||||
Console.WriteLine();
|
||||
Console.WriteLine("Options:");
|
||||
Console.WriteLine(" -Encode Encode a binary file to a copy/paste friendly Base64 text file.");
|
||||
Console.WriteLine(" -Decode Decode a transfer text file back to binary.");
|
||||
Console.WriteLine(" -Output Output path.");
|
||||
Console.WriteLine(" -Force Overwrite an existing output file.");
|
||||
Console.WriteLine(" -Log Optional log file path.");
|
||||
Console.WriteLine(" -NoProgress Disable periodic progress lines.");
|
||||
Console.WriteLine(" -Quiet Only write errors to the console.");
|
||||
Console.WriteLine(" -Help Show this help.");
|
||||
}
|
||||
|
||||
private enum OperationMode
|
||||
{
|
||||
None,
|
||||
Encode,
|
||||
Decode
|
||||
}
|
||||
|
||||
private sealed class Options
|
||||
{
|
||||
public OperationMode Mode;
|
||||
public string InputPath;
|
||||
public string OutputPath;
|
||||
public string LogPath;
|
||||
public bool Force;
|
||||
public bool Quiet;
|
||||
public bool NoProgress;
|
||||
public bool ShowHelp;
|
||||
|
||||
public static bool TryParse(string[] args, out Options options, out string error)
|
||||
{
|
||||
options = new Options();
|
||||
error = null;
|
||||
List<string> positionals = new List<string>();
|
||||
|
||||
if (args == null || args.Length == 0)
|
||||
{
|
||||
options.ShowHelp = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
for (int i = 0; i < args.Length; i++)
|
||||
{
|
||||
string arg = args[i];
|
||||
if (IsOption(arg, "Help") || arg == "/?" || arg == "-?")
|
||||
{
|
||||
options.ShowHelp = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (IsOption(arg, "Encode"))
|
||||
{
|
||||
if (options.Mode != OperationMode.None)
|
||||
{
|
||||
error = "Specify only one operation: -Encode or -Decode.";
|
||||
return false;
|
||||
}
|
||||
|
||||
options.Mode = OperationMode.Encode;
|
||||
if (i + 1 < args.Length && !IsKnownOption(args[i + 1]))
|
||||
{
|
||||
options.InputPath = args[++i];
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (IsOption(arg, "Decode"))
|
||||
{
|
||||
if (options.Mode != OperationMode.None)
|
||||
{
|
||||
error = "Specify only one operation: -Encode or -Decode.";
|
||||
return false;
|
||||
}
|
||||
|
||||
options.Mode = OperationMode.Decode;
|
||||
if (i + 1 < args.Length && !IsKnownOption(args[i + 1]))
|
||||
{
|
||||
options.InputPath = args[++i];
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (IsOption(arg, "Output") || IsOption(arg, "Out") || IsOption(arg, "O"))
|
||||
{
|
||||
if (i + 1 >= args.Length)
|
||||
{
|
||||
error = "-Output requires a path.";
|
||||
return false;
|
||||
}
|
||||
|
||||
options.OutputPath = args[++i];
|
||||
continue;
|
||||
}
|
||||
|
||||
if (IsOption(arg, "Log"))
|
||||
{
|
||||
if (i + 1 >= args.Length)
|
||||
{
|
||||
error = "-Log requires a path.";
|
||||
return false;
|
||||
}
|
||||
|
||||
options.LogPath = args[++i];
|
||||
continue;
|
||||
}
|
||||
|
||||
if (IsOption(arg, "Force") || IsOption(arg, "Overwrite"))
|
||||
{
|
||||
options.Force = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (IsOption(arg, "Quiet"))
|
||||
{
|
||||
options.Quiet = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (IsOption(arg, "NoProgress"))
|
||||
{
|
||||
options.NoProgress = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg.StartsWith("-", StringComparison.Ordinal))
|
||||
{
|
||||
error = "Unknown option: " + arg;
|
||||
return false;
|
||||
}
|
||||
|
||||
positionals.Add(arg);
|
||||
}
|
||||
|
||||
if (options.Mode == OperationMode.None)
|
||||
{
|
||||
error = "Missing operation. Use -Encode or -Decode.";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(options.InputPath) && positionals.Count > 0)
|
||||
{
|
||||
options.InputPath = positionals[0];
|
||||
positionals.RemoveAt(0);
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(options.OutputPath) && positionals.Count > 0)
|
||||
{
|
||||
options.OutputPath = positionals[0];
|
||||
positionals.RemoveAt(0);
|
||||
}
|
||||
|
||||
if (positionals.Count > 0)
|
||||
{
|
||||
error = "Too many positional arguments.";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(options.InputPath))
|
||||
{
|
||||
error = "Missing input path.";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(options.OutputPath))
|
||||
{
|
||||
if (options.Mode == OperationMode.Encode)
|
||||
{
|
||||
options.OutputPath = Path.GetFileName(options.InputPath) + ".b64.txt";
|
||||
}
|
||||
else
|
||||
{
|
||||
error = "Missing output path for decode.";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool IsOption(string value, string name)
|
||||
{
|
||||
return string.Equals(value, "-" + name, StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(value, "/" + name, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private static bool IsKnownOption(string value)
|
||||
{
|
||||
return IsOption(value, "Encode") ||
|
||||
IsOption(value, "Decode") ||
|
||||
IsOption(value, "Output") ||
|
||||
IsOption(value, "Out") ||
|
||||
IsOption(value, "O") ||
|
||||
IsOption(value, "Log") ||
|
||||
IsOption(value, "Force") ||
|
||||
IsOption(value, "Overwrite") ||
|
||||
IsOption(value, "Quiet") ||
|
||||
IsOption(value, "NoProgress") ||
|
||||
IsOption(value, "Help") ||
|
||||
string.Equals(value, "/?", StringComparison.Ordinal) ||
|
||||
string.Equals(value, "-?", StringComparison.Ordinal);
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class DecodeMetadata
|
||||
{
|
||||
public long? SourceLength;
|
||||
public string Sha256;
|
||||
|
||||
public void TryParseComment(string line)
|
||||
{
|
||||
string text = line.Substring(1).Trim();
|
||||
int separator = text.IndexOf(':');
|
||||
if (separator < 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
string key = text.Substring(0, separator).Trim();
|
||||
string value = text.Substring(separator + 1).Trim();
|
||||
|
||||
if (string.Equals(key, "SourceLength", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
long parsed;
|
||||
if (long.TryParse(value, NumberStyles.None, CultureInfo.InvariantCulture, out parsed))
|
||||
{
|
||||
SourceLength = parsed;
|
||||
}
|
||||
}
|
||||
else if (string.Equals(key, "SHA256", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
Sha256 = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class ProgressReporter
|
||||
{
|
||||
private readonly string _operation;
|
||||
private readonly long _total;
|
||||
private readonly bool _disabled;
|
||||
private readonly Logger _logger;
|
||||
private readonly Stopwatch _stopwatch;
|
||||
private long _lastReportMilliseconds;
|
||||
|
||||
public ProgressReporter(string operation, long total, bool disabled, Logger logger)
|
||||
{
|
||||
_operation = operation;
|
||||
_total = total;
|
||||
_disabled = disabled;
|
||||
_logger = logger;
|
||||
_stopwatch = Stopwatch.StartNew();
|
||||
_lastReportMilliseconds = -1000;
|
||||
}
|
||||
|
||||
public void Report(long current)
|
||||
{
|
||||
if (_disabled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
long elapsed = _stopwatch.ElapsedMilliseconds;
|
||||
if (elapsed - _lastReportMilliseconds < 1000)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_lastReportMilliseconds = elapsed;
|
||||
_logger.Info(BuildMessage(current));
|
||||
}
|
||||
|
||||
public void Done(long current)
|
||||
{
|
||||
if (!_disabled)
|
||||
{
|
||||
_logger.Info(BuildMessage(current));
|
||||
}
|
||||
}
|
||||
|
||||
private string BuildMessage(long current)
|
||||
{
|
||||
string percent = _total > 0
|
||||
? ((current * 100.0) / _total).ToString("0.0", CultureInfo.InvariantCulture) + "%"
|
||||
: "100.0%";
|
||||
double seconds = Math.Max(0.001, _stopwatch.Elapsed.TotalSeconds);
|
||||
long bytesPerSecond = (long)(current / seconds);
|
||||
|
||||
return _operation + ": " + FormatBytes(current) + " / " + FormatBytes(_total) +
|
||||
" (" + percent + ", " + FormatBytes(bytesPerSecond) + "/s)";
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class Logger : IDisposable
|
||||
{
|
||||
private readonly bool _quiet;
|
||||
private readonly StreamWriter _fileWriter;
|
||||
|
||||
public Logger(bool quiet, string logPath)
|
||||
{
|
||||
_quiet = quiet;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(logPath))
|
||||
{
|
||||
string directory = Path.GetDirectoryName(Path.GetFullPath(logPath));
|
||||
if (!string.IsNullOrEmpty(directory))
|
||||
{
|
||||
Directory.CreateDirectory(directory);
|
||||
}
|
||||
|
||||
_fileWriter = new StreamWriter(logPath, true, new UTF8Encoding(false));
|
||||
}
|
||||
}
|
||||
|
||||
public void Info(string message)
|
||||
{
|
||||
Write("INFO", message, false);
|
||||
}
|
||||
|
||||
public void Success(string message)
|
||||
{
|
||||
Write("OK", message, false);
|
||||
}
|
||||
|
||||
public void Warn(string message)
|
||||
{
|
||||
Write("WARN", message, false);
|
||||
}
|
||||
|
||||
public void Error(string message)
|
||||
{
|
||||
Write("ERROR", message, true);
|
||||
}
|
||||
|
||||
public void Debug(string message)
|
||||
{
|
||||
Write("DEBUG", message, false);
|
||||
}
|
||||
|
||||
private void Write(string level, string message, bool error)
|
||||
{
|
||||
string line = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff", CultureInfo.InvariantCulture) +
|
||||
" [" + level + "] " + message;
|
||||
|
||||
if (!_quiet || error)
|
||||
{
|
||||
if (error)
|
||||
{
|
||||
Console.Error.WriteLine(line);
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine(line);
|
||||
}
|
||||
}
|
||||
|
||||
if (_fileWriter != null)
|
||||
{
|
||||
_fileWriter.WriteLine(line);
|
||||
_fileWriter.Flush();
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_fileWriter != null)
|
||||
{
|
||||
_fileWriter.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
using System.Reflection;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
[assembly: AssemblyTitle("BinaryConverter")]
|
||||
[assembly: AssemblyDescription("Streaming Base64 encoder and decoder for binary file transfer via copy and paste.")]
|
||||
[assembly: AssemblyConfiguration("")]
|
||||
[assembly: AssemblyCompany("")]
|
||||
[assembly: AssemblyProduct("BinaryConverter")]
|
||||
[assembly: AssemblyCopyright("Copyright (c) 2026")]
|
||||
[assembly: AssemblyTrademark("")]
|
||||
[assembly: AssemblyCulture("")]
|
||||
[assembly: ComVisible(false)]
|
||||
[assembly: Guid("36f68715-813b-49a0-b2d0-0442d353fd6d")]
|
||||
[assembly: AssemblyVersion("1.0.0.0")]
|
||||
[assembly: AssemblyFileVersion("1.0.0.0")]
|
||||
@@ -0,0 +1,107 @@
|
||||
# Dokumentation
|
||||
|
||||
## Ziel
|
||||
|
||||
BinaryConverter ist fuer Umgebungen gedacht, in denen eine binaere Datei nicht direkt uebertragen werden kann, Text per Copy & Paste aber moeglich ist. Die Anwendung erzeugt deshalb eine robuste Textdarstellung und prueft beim Rueckweg, ob die rekonstruierte Datei bytegenau passt.
|
||||
|
||||
## Dateiformat
|
||||
|
||||
Das Transferfile besteht aus Kommentar-Metadaten, einem Beginn-Marker, Base64-Nutzdaten und einem Ende-Marker.
|
||||
|
||||
```text
|
||||
# BinaryConverter-Format: 1
|
||||
# Encoding: Base64
|
||||
# SourceFileNameBase64: <UTF-8-Dateiname als Base64>
|
||||
# SourceLength: <Bytes>
|
||||
# SHA256: <Hex-Hash>
|
||||
# CreatedUtc: <ISO-8601>
|
||||
# LineLength: 76
|
||||
-----BEGIN BINARYCONVERTER BASE64-----
|
||||
<Base64-Daten>
|
||||
-----END BINARYCONVERTER BASE64-----
|
||||
```
|
||||
|
||||
Die Base64-Nutzdaten werden mit 76 Zeichen pro Zeile geschrieben. Das ist bewusst konservativ, damit Zeilenumbrueche in vielen Editoren, Konsolen und Remote-Sessions gut handhabbar bleiben.
|
||||
|
||||
## Integritaet
|
||||
|
||||
Beim Encode wird die Eingabedatei zuerst einmal gelesen, um SHA-256 zu berechnen. Danach wird sie in einem zweiten Durchlauf als Base64 geschrieben. Der zweite Durchlauf vermeidet, dass grosse Dateien komplett in den Speicher geladen werden.
|
||||
|
||||
Bei Encode und Decode wird zuerst in eine temporaere Datei geschrieben. Erst nach erfolgreicher Verarbeitung wird diese temporaere Datei auf den finalen Ausgabepfad verschoben beziehungsweise ersetzt. Beim Decode passiert das erst nach erfolgreicher Laengen- und SHA-256-Pruefung. Bei Fehlern wird die temporaere Datei geloescht.
|
||||
|
||||
## Speicherverhalten
|
||||
|
||||
Die Anwendung arbeitet streaming-basiert:
|
||||
|
||||
- Eingabe wird blockweise gelesen.
|
||||
- Base64 wird blockweise geschrieben beziehungsweise dekodiert.
|
||||
- Es wird nicht die komplette Eingabe- oder Ausgabedatei in den Arbeitsspeicher geladen.
|
||||
|
||||
Beim Decode wird ein kleiner Base64-Puffer verwendet, um auch unguenstig umgebrochene Base64-Zeilen verarbeiten zu koennen.
|
||||
|
||||
## Fehlerverhalten
|
||||
|
||||
Typische Fehler werden mit Exitcode `1` beendet:
|
||||
|
||||
- Eingabedatei fehlt.
|
||||
- Ausgabedatei existiert bereits und `-Force` wurde nicht gesetzt.
|
||||
- Transferfile ist unvollstaendig, zum Beispiel fehlender Ende-Marker.
|
||||
- Base64-Inhalt ist ungueltig.
|
||||
- Dekodierte Laenge passt nicht zur Metadatenlaenge.
|
||||
- SHA-256 passt nicht zum Metadatenhash.
|
||||
|
||||
Argumentfehler werden mit Exitcode `2` beendet. Erfolgreiche Ausfuehrung endet mit Exitcode `0`.
|
||||
|
||||
## Logging
|
||||
|
||||
Standardmaessig schreibt BinaryConverter auf die Konsole:
|
||||
|
||||
- Version und Operation
|
||||
- Eingabe- und Ausgabepfad
|
||||
- Fortschritt und Durchsatz
|
||||
- berechnete beziehungsweise gepruefte SHA-256-Summe
|
||||
- finale Dateigroessen
|
||||
|
||||
Mit `-Log <Datei>` werden dieselben Informationen zusaetzlich in eine Logdatei geschrieben.
|
||||
|
||||
## Beispiele
|
||||
|
||||
Datei fuer Transfer vorbereiten:
|
||||
|
||||
```powershell
|
||||
.\BinaryConverter.exe -Encode C:\temp\PowerShell.exe -Output C:\temp\Out.txt -Log C:\temp\encode.log
|
||||
```
|
||||
|
||||
Auf dem Zielhost zurueckwandeln:
|
||||
|
||||
```powershell
|
||||
.\BinaryConverter.exe -Decode C:\temp\Out.txt C:\temp\PowerShell.exe -Log C:\temp\decode.log
|
||||
```
|
||||
|
||||
Bereits vorhandene Zieldatei ersetzen:
|
||||
|
||||
```powershell
|
||||
.\BinaryConverter.exe -Decode C:\temp\Out.txt C:\temp\PowerShell.exe -Force
|
||||
```
|
||||
|
||||
## Repository-Struktur
|
||||
|
||||
```text
|
||||
BinaryConverter/
|
||||
BinaryConverter.sln
|
||||
BinaryConverter/
|
||||
BinaryConverter.csproj
|
||||
App.config
|
||||
Program.cs
|
||||
Properties/
|
||||
AssemblyInfo.cs
|
||||
.editorconfig
|
||||
.gitattributes
|
||||
.gitignore
|
||||
Dokumentation.md
|
||||
Readme.md
|
||||
```
|
||||
|
||||
## Gitea-Hinweise
|
||||
|
||||
Das Repository enthaelt keine Build-Artefakte. `bin/`, `obj/`, Visual-Studio-Userdateien, Logs und temporaere Dateien sind ueber `.gitignore` ausgeschlossen.
|
||||
@@ -0,0 +1,90 @@
|
||||
# BinaryConverter
|
||||
|
||||
BinaryConverter ist eine C# Konsolenanwendung fuer Windows 11 und .NET Framework 4.8.1. Sie kodiert binaere Dateien in ein Base64-Textformat, das sich per Copy & Paste auf einen anderen Host uebertragen laesst, und dekodiert die Textdatei dort wieder in die binaere Originalform.
|
||||
|
||||
## Funktionen
|
||||
|
||||
- Streaming-Verarbeitung fuer grosse Dateien, auch mehrere hundert MB
|
||||
- Base64-Ausgabe mit stabilen Zeilenlaengen fuer Copy & Paste
|
||||
- Metadaten im Transferfile: Formatversion, Originaldateiname, Laenge, SHA-256
|
||||
- Integritaetspruefung beim Decode ueber Dateigroesse und SHA-256
|
||||
- Fortschrittsausgabe auf der Konsole
|
||||
- Optionales Logfile
|
||||
- Temporaere Ausgabedatei bei Encode und Decode, damit bei Fehlern keine kaputte Zieldatei stehen bleibt
|
||||
|
||||
## Build
|
||||
|
||||
Voraussetzungen auf Windows:
|
||||
|
||||
- Windows 11
|
||||
- Visual Studio 2022 oder Build Tools for Visual Studio
|
||||
- .NET Framework 4.8.1 Developer Pack / Targeting Pack
|
||||
|
||||
Build in einer Developer PowerShell:
|
||||
|
||||
```powershell
|
||||
msbuild .\BinaryConverter.sln /p:Configuration=Release
|
||||
```
|
||||
|
||||
Die EXE liegt danach hier:
|
||||
|
||||
```text
|
||||
BinaryConverter\bin\Release\BinaryConverter.exe
|
||||
```
|
||||
|
||||
## Nutzung
|
||||
|
||||
Encode:
|
||||
|
||||
```powershell
|
||||
BinaryConverter.exe -Encode C:\temp\PowerShell.exe -Output Out.txt
|
||||
```
|
||||
|
||||
Decode:
|
||||
|
||||
```powershell
|
||||
BinaryConverter.exe -Decode Out.txt PowerShell.exe
|
||||
```
|
||||
|
||||
Alternative Decode-Syntax:
|
||||
|
||||
```powershell
|
||||
BinaryConverter.exe -Decode Out.txt -Output PowerShell.exe
|
||||
```
|
||||
|
||||
Mit Logfile:
|
||||
|
||||
```powershell
|
||||
BinaryConverter.exe -Encode C:\temp\PowerShell.exe -Output Out.txt -Log BinaryConverter.log
|
||||
```
|
||||
|
||||
Existierende Ausgabedatei ueberschreiben:
|
||||
|
||||
```powershell
|
||||
BinaryConverter.exe -Decode Out.txt PowerShell.exe -Force
|
||||
```
|
||||
|
||||
## Optionen
|
||||
|
||||
| Option | Bedeutung |
|
||||
| --- | --- |
|
||||
| `-Encode <Datei>` | Binaerdatei in Transfer-Textdatei kodieren |
|
||||
| `-Decode <Datei>` | Transfer-Textdatei zurueck in Binaerdatei dekodieren |
|
||||
| `-Output <Datei>` | Ausgabepfad |
|
||||
| `-Force` | Existierende Ausgabedatei ueberschreiben |
|
||||
| `-Log <Datei>` | Zusaetzlich in eine Logdatei schreiben |
|
||||
| `-NoProgress` | Periodische Fortschrittszeilen abschalten |
|
||||
| `-Quiet` | Nur Fehler auf der Konsole ausgeben |
|
||||
| `-Help` | Hilfe anzeigen |
|
||||
|
||||
## Transferablauf
|
||||
|
||||
1. Auf dem Quellhost `-Encode` ausfuehren.
|
||||
2. Inhalt der erzeugten Textdatei auf den Zielhost kopieren.
|
||||
3. Auf dem Zielhost den Text als Datei speichern, zum Beispiel `Out.txt`.
|
||||
4. Auf dem Zielhost `-Decode Out.txt <Zieldatei>` ausfuehren.
|
||||
5. Die Anwendung prueft Laenge und SHA-256 automatisch, sofern das Transferfile von BinaryConverter erzeugt wurde.
|
||||
|
||||
## Hinweise
|
||||
|
||||
Base64 vergroessert die Datenmenge um ungefaehr ein Drittel. Eine 300-MB-Datei erzeugt also grob eine 400-MB-Textdatei plus wenige Metadatenzeilen. Fuer sehr grosse Dateien ist es sinnvoll, das Copy & Paste ueber einen Editor oder ein Terminal zu machen, das grosse Textmengen stabil verarbeiten kann.
|
||||
Reference in New Issue
Block a user