Files
Disco/Disco.Services/Documents/DocumentUniqueIdentifier.cs
T
Gary Sharp acfa0e5094 Document Template Binary QR Codes
The QR Code now stores the data using a custom binary encoding format.
This reduces the amount of data stored by ~30%. This increases the
chance of a lower-version QR Code being used and the likelyhood of even
small QR Codes being detected. Compatibility with previously generated
documents is maintained.
2016-09-15 19:27:52 +10:00

428 lines
17 KiB
C#

using Disco.Data.Repository;
using Disco.Models.Repository;
using Disco.Services.Interop.ActiveDirectory;
using Newtonsoft.Json;
using System;
using System.IO;
using System.Text;
namespace Disco.Services.Documents
{
public class DocumentUniqueIdentifier
{
private const int CurrentVersion = 2;
private const byte MagicNumber = 0xC4;
private DiscoDataContext database;
public int Version { get; private set; }
public short DeploymentChecksum { get; private set; }
public string DocumentTemplateId { get; private set; }
public string TargetId { get; private set; }
public string CreatorId { get; private set; }
public DateTime TimeStamp { get; private set; }
public int PageIndex { get; private set; }
private DocumentTemplate documentTemplate;
private AttachmentTypes? attachmentType;
private IAttachmentTarget target;
private User creator;
public DocumentTemplate DocumentTemplate
{
get
{
if (documentTemplate == null && !string.IsNullOrWhiteSpace(DocumentTemplateId) && !DocumentTemplateId.StartsWith("--", StringComparison.Ordinal))
{
documentTemplate = database.DocumentTemplates.Find(DocumentTemplateId);
if (documentTemplate != null)
{
attachmentType = documentTemplate.AttachmentType;
}
}
return documentTemplate;
}
}
public string DocumentGroupingId
{
get
{
// Unique identifier to distinguish this document from others (but keep pages together)
return $"{TimeStamp.Ticks}|{TargetId}|{DocumentTemplateId}|{CreatorId}|{Version}|{DeploymentChecksum}";
}
}
public AttachmentTypes? AttachmentType
{
get
{
if (!attachmentType.HasValue)
{
if (DocumentTemplateId.StartsWith("--", StringComparison.Ordinal))
{
if (DocumentTemplateId.Equals("--DEVICE", StringComparison.Ordinal))
{
attachmentType = AttachmentTypes.Device;
}
else if (DocumentTemplateId.Equals("--JOB", StringComparison.Ordinal))
{
attachmentType = AttachmentTypes.Job;
}
else if (DocumentTemplateId.Equals("--USER", StringComparison.Ordinal))
{
attachmentType = AttachmentTypes.User;
}
}
else
{
var dt = DocumentTemplate;
if (dt != null)
{
attachmentType = dt.AttachmentType;
}
}
}
return attachmentType;
}
}
public IAttachmentTarget Target
{
get
{
if (target == null)
{
switch (AttachmentType)
{
case AttachmentTypes.Device:
target = database.Devices.Find(TargetId);
break;
case AttachmentTypes.Job:
target = database.Jobs.Find(int.Parse(TargetId));
break;
case AttachmentTypes.User:
target = database.Users.Find(ActiveDirectory.ParseDomainAccountId(TargetId));
break;
default:
throw new ArgumentException("Unexpected Attachment Type", nameof(AttachmentType));
}
}
return target;
}
}
public User Creator
{
get
{
if (creator == null)
{
creator = database.Users.Find(ActiveDirectory.ParseDomainAccountId(CreatorId));
}
return creator;
}
}
private DocumentUniqueIdentifier(DiscoDataContext Database, int Version, short DeploymentChecksum, string DocumentTemplateId, string TargetId, string CreatorId, DateTime TimeStamp, int PageIndex, AttachmentTypes? AttachmentType)
{
this.database = Database;
this.Version = Version;
this.DeploymentChecksum = DeploymentChecksum;
this.DocumentTemplateId = DocumentTemplateId;
this.attachmentType = AttachmentType;
this.TargetId = TargetId;
this.CreatorId = ActiveDirectory.ParseDomainAccountId(CreatorId);
this.TimeStamp = TimeStamp;
this.PageIndex = PageIndex;
}
public string ToQRCodeString()
{
return $"Disco|1|{DocumentTemplate.Id}|{TargetId}|{CreatorId}|{TimeStamp:s}|{PageIndex}";
}
public string ToJson()
{
var builder = new StringBuilder();
using (var stringWriter = new StringWriter(builder))
{
using (var writer = new JsonTextWriter(stringWriter))
{
writer.Formatting = Formatting.Indented;
writer.WriteStartObject();
writer.WritePropertyName("version");
writer.WriteValue(CurrentVersion);
writer.WritePropertyName("attachmentType");
writer.WriteValue(AttachmentType.Value.ToString());
writer.WritePropertyName("documentTemplateId");
writer.WriteValue(DocumentTemplateId);
writer.WritePropertyName("targetId");
writer.WriteValue(TargetId);
writer.WritePropertyName("creatorId");
writer.WriteValue(CreatorId);
writer.WritePropertyName("timestamp");
writer.WriteValue(TimeStamp);
writer.WritePropertyName("pageIndex");
writer.WriteValue(PageIndex);
writer.WriteEndObject();
}
}
return builder.ToString();
}
public byte[] ToQRCodeBytes()
{
// Byte | Meaning
// 0 | magic number = 0x0D
// 1 | bits 0-3 = version; 4-7 = flags (1 = has document template id, 2 = device attachment,
// | 4 = job attachment, 8 = user attachment)
// 2-3 | deployment checksum (int16)
// 4-7 | timestamp (uint32 unix epoch)
// 8-9 | page index (uint16)
// 10 | creator id length
// 11-? | creator id (UTF8)
// ? | target id length
// ?-? | target id
// ? | document template id length (optional based on flag)
// ?-? | document template id (UTF8, optional based on flag)
var encoding = Encoding.UTF8;
byte flags = 0;
switch (AttachmentType)
{
case AttachmentTypes.Device:
flags = 2;
break;
case AttachmentTypes.Job:
flags = 4;
break;
case AttachmentTypes.User:
flags = 8;
break;
}
var deploymentChecksumBytes = BitConverter.GetBytes(DeploymentChecksum);
var timeStampEpochBytes = BitConverter.GetBytes((uint)(TimeStamp.ToUniversalTime().Subtract(DateTime.FromFileTimeUtc(116444736000000000L)).Ticks / TimeSpan.TicksPerSecond));
var pageIndexBytes = BitConverter.GetBytes((ushort)PageIndex);
// magic number (1) + version/flags (1) + deployment checksum (2) + timestamp (4) +
// page index (2) + creator id length (1) + target id length (1)
var creatorIdLength = encoding.GetByteCount(CreatorId);
var targetIdLength = encoding.GetByteCount(TargetId);
var requiredBytes = 12 + creatorIdLength + targetIdLength;
var documentTemplateIdLength = 0;
if (DocumentTemplateId != null)
{
flags |= 1;
requiredBytes++;
documentTemplateIdLength = encoding.GetByteCount(DocumentTemplateId);
requiredBytes += documentTemplateIdLength;
}
int position = 0;
var result = new byte[requiredBytes];
// magic number
result[position++] = MagicNumber;
// version & flags
result[position++] = (byte)(flags | (2 << 4));
// deployment checksum
deploymentChecksumBytes.CopyTo(result, position);
position += 2;
// timestamp
timeStampEpochBytes.CopyTo(result, position);
position += 4;
// page index
pageIndexBytes.CopyTo(result, position);
position += 2;
// creator id length
result[position++] = (byte)creatorIdLength;
// creator id
position += encoding.GetBytes(CreatorId, 0, CreatorId.Length, result, position);
// target id length
result[position++] = (byte)targetIdLength;
// target id
position += encoding.GetBytes(TargetId, 0, TargetId.Length, result, position);
if (documentTemplateIdLength > 0)
{
// document template id length
result[position++] = (byte)documentTemplateIdLength;
// document template id
position += encoding.GetBytes(DocumentTemplateId, 0, DocumentTemplateId.Length, result, position);
}
return result;
}
public static DocumentUniqueIdentifier Create(DiscoDataContext Database, DocumentTemplate DocumentTemplate, IAttachmentTarget Target, User Creator, DateTime TimeStamp, int PageIndex)
{
var deploymentChecksum = Database.DiscoConfiguration.DeploymentChecksum;
var documentTemplateId = DocumentTemplate.Id;
var targetId = Target.AttachmentReferenceId;
var creatorId = Creator.UserId;
var attachmentType = DocumentTemplate.AttachmentType;
var identifier = new DocumentUniqueIdentifier(Database, CurrentVersion, deploymentChecksum, documentTemplateId, targetId, creatorId, TimeStamp, PageIndex, attachmentType);
identifier.documentTemplate = DocumentTemplate;
identifier.target = Target;
identifier.creator = Creator;
return identifier;
}
public static DocumentUniqueIdentifier Create(DiscoDataContext Database, string DocumentTemplateId, string TargetId, string CreatorId, DateTime TimeStamp, int PageIndex)
{
var deploymentChecksum = Database.DiscoConfiguration.DeploymentChecksum;
return new DocumentUniqueIdentifier(Database, CurrentVersion, deploymentChecksum, DocumentTemplateId, TargetId, CreatorId, TimeStamp, PageIndex, null);
}
public static DocumentUniqueIdentifier Parse(DiscoDataContext Database, byte[] UniqueIdentifier)
{
DocumentUniqueIdentifier identifier;
if (TryParse(Database, UniqueIdentifier, out identifier))
{
return identifier;
}
else
{
throw new FormatException("Invalid Document Unique Identifier");
}
}
public static DocumentUniqueIdentifier Parse(DiscoDataContext Database, string UniqueIdentifier)
{
DocumentUniqueIdentifier identifier;
if (TryParse(Database, UniqueIdentifier, out identifier))
{
return identifier;
}
else
{
throw new FormatException("Invalid Document Unique Identifier");
}
}
public static bool TryParse(DiscoDataContext Database, string UniqueIdentifier, out DocumentUniqueIdentifier Identifier)
{
if (IsDocumentUniqueIdentifier(UniqueIdentifier))
{
var components = UniqueIdentifier.Split('|');
// Version 0, Version 1 Handling
if ((components.Length == 7 || components.Length == 6) &&
(components[1].Equals("1", StringComparison.Ordinal) || components[1].Equals("AT", StringComparison.OrdinalIgnoreCase)))
{
var documentTemplateId = components[2];
var targetId = components[3];
var creatorId = components[4];
var timeStamp = DateTime.Parse(components[5]);
var page = 0;
if (components.Length == 7)
{
page = int.Parse(components[6]);
}
Identifier = new DocumentUniqueIdentifier(Database, 1, -1, documentTemplateId, targetId, creatorId, timeStamp, page, null);
return true;
}
}
Identifier = null;
return false;
}
public static bool TryParse(DiscoDataContext Database, byte[] UniqueIdentifier, out DocumentUniqueIdentifier Identifier)
{
if (IsDocumentUniqueIdentifier(UniqueIdentifier))
{
// first 4 bit indicate version
var version = UniqueIdentifier[1] >> 4;
// Version 2
if (version == 2)
{
// Byte | Meaning
// 0 | magic number = 0x0D
// 1 | bits 0-3 = version; 4-7 = flags (1 = has document template id, 2 = device attachment,
// | 4 = job attachment, 8 = user attachment)
// 2-3 | deployment checksum (int16)
// 4-7 | timestamp (uint32 unix epoch)
// 8-9 | page index (uint16)
// 10 | creator id length
// 11-? | creator id (UTF8)
// ? | target id length
// ?-? | target id
// ? | document template id length (optional based on flag)
// ?-? | document template id (UTF8, optional based on flag)
var encoding = Encoding.UTF8;
var position = 1;
// next 4 bits are flags
var flags = UniqueIdentifier[position++] & 0x0F;
var deploymentChecksum = BitConverter.ToInt16(UniqueIdentifier, position);
position += 2;
var timeStampEpoch = BitConverter.ToUInt32(UniqueIdentifier, position);
position += 4;
var pageIndex = BitConverter.ToUInt16(UniqueIdentifier, position);
position += 2;
var creatorIdLength = UniqueIdentifier[position++];
var creatorId = encoding.GetString(UniqueIdentifier, position, creatorIdLength);
position += creatorIdLength;
var targetIdLength = UniqueIdentifier[position++];
var targetId = encoding.GetString(UniqueIdentifier, position, targetIdLength);
position += targetIdLength;
string documentTemplateId = null;
// Has document template id flag
if ((flags & 1) == 1)
{
var documentTemplateIdLength = UniqueIdentifier[position++];
documentTemplateId = encoding.GetString(UniqueIdentifier, position, documentTemplateIdLength);
}
AttachmentTypes? attachmentType = null;
if ((flags & 2) == 2)
attachmentType = AttachmentTypes.Device;
else if ((flags & 4) == 4)
attachmentType = AttachmentTypes.Job;
else if ((flags & 8) == 8)
attachmentType = AttachmentTypes.User;
var timeStamp = DateTime.FromFileTimeUtc(116444736000000000L).AddTicks(TimeSpan.TicksPerSecond * timeStampEpoch).ToLocalTime();
Identifier = new DocumentUniqueIdentifier(Database, version, deploymentChecksum, documentTemplateId, targetId, creatorId, timeStamp, pageIndex, attachmentType);
return true;
}
}
Identifier = null;
return false;
}
public static bool IsDocumentUniqueIdentifier(string Identifier)
{
return Identifier != null && Identifier.StartsWith("Disco|", System.StringComparison.OrdinalIgnoreCase);
}
public static bool IsDocumentUniqueIdentifier(byte[] Identifier)
{
// Identifier[0] = 0xC4; Magic number to identify Disco ICT QR Codes
return Identifier != null && Identifier.Length > 2 && Identifier[0] == MagicNumber;
}
}
}