using System.Linq;
namespace Semmle.Extraction
{
///
/// A tuple represents a string of the form "a(b,c,d)"
/// Its purpose is mainly to make various method calls typesafe.
///
public struct Tuple : ITrapEmitter
{
readonly string Name;
readonly object[] Args;
public Tuple(string name, params object[] args)
{
Name = name;
Args = args;
}
const int maxStringBytes = 1<<20; // 1MB
static readonly System.Text.Encoding encoding = System.Text.Encoding.UTF8;
private static bool NeedsTruncation(string s)
{
// Optimization: only count the actual number of bytes if there is the possibility
// of the string exceeding maxStringBytes
return encoding.GetMaxByteCount(s.Length) > maxStringBytes &&
encoding.GetByteCount(s) > maxStringBytes;
}
private static bool NeedsTruncation(string[] array)
{
// Optimization: only count the actual number of bytes if there is the possibility
// of the strings exceeding maxStringBytes
return encoding.GetMaxByteCount(array.Sum(s => s.Length)) > maxStringBytes &&
array.Sum(encoding.GetByteCount) > maxStringBytes;
}
private static void WriteString(ITrapBuilder tb, string s) => tb.Append(EncodeString(s));
///
/// Truncates a string such that the output UTF8 does not exceed bytes.
///
/// The input string to truncate.
/// The number of bytes available.
/// The truncated string.
private static string TruncateString(string s, ref int bytesRemaining)
{
int outputLen = encoding.GetByteCount(s);
if (outputLen > bytesRemaining)
{
outputLen = 0;
int chars;
for (chars = 0; chars < s.Length; ++chars)
{
var bytes = encoding.GetByteCount(s, chars, 1);
if (outputLen + bytes <= bytesRemaining)
outputLen += bytes;
else
break;
}
s = s.Substring(0, chars);
}
bytesRemaining -= outputLen;
return s;
}
private static string EncodeString(string s) => s.Replace("\"", "\"\"");
///
/// Output a string to the trap file, such that the encoded output does not exceed
/// bytes.
///
/// The trapbuilder
/// The string to output.
/// The remaining bytes available to output.
private static void WriteTruncatedString(ITrapBuilder tb, string s, ref int bytesRemaining)
{
WriteString(tb, TruncateString(s, ref bytesRemaining));
}
///
/// Constructs a unique string for this tuple.
///
/// The trap builder used to store the result.
public void EmitToTrapBuilder(ITrapBuilder tb)
{
tb.Append(Name).Append("(");
int column = 0;
foreach (var a in Args)
{
if (column > 0) tb.Append(", ");
switch(a)
{
case Label l:
l.AppendTo(tb);
break;
case IEntity e:
e.Label.AppendTo(tb);
break;
case string s:
tb.Append("\"");
if (NeedsTruncation(s))
{
// Slow path
int remaining = maxStringBytes;
WriteTruncatedString(tb, s, ref remaining);
}
else
{
// Fast path
WriteString(tb, s);
}
tb.Append("\"");
break;
case System.Enum _:
tb.Append((int)a);
break;
case int i:
tb.Append(i);
break;
case float f:
tb.Append(f.ToString("0.#####e0")); // Trap importer won't accept ints
break;
case string[] array:
tb.Append("\"");
if (NeedsTruncation(array))
{
// Slow path
int remaining = maxStringBytes;
foreach (var element in array)
WriteTruncatedString(tb, element, ref remaining);
}
else
{
// Fast path
foreach (var element in array)
WriteString(tb, element);
}
tb.Append("\"");
break;
case null:
throw new InternalError($"Attempt to write a null argument tuple {Name} at column {column}");
default:
throw new InternalError($"Attempt to write an invalid argument type {a.GetType()} in tuple {Name} at column {column}");
}
++column;
}
tb.Append(")");
tb.AppendLine();
}
public override string ToString()
{
// Only implemented for debugging purposes
var tsb = new TrapStringBuilder();
EmitToTrapBuilder(tsb);
return tsb.ToString();
}
}
}