Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions doc/ref/string.xml
Original file line number Diff line number Diff line change
Expand Up @@ -692,6 +692,10 @@ gap> HexStringInt(last);
<#Include Label="CrcString">
<#Include Label="HexSHA256">
<#Include Label="HexSHA256File">
<#Include Label="IsSHA256State">
<#Include Label="SHA256State">
<#Include Label="UpdateSHA256">
<#Include Label="UpdateSHA256File">
<#Include Label="Pluralize">

</Section>
Expand Down
93 changes: 92 additions & 1 deletion lib/files.gd
Original file line number Diff line number Diff line change
Expand Up @@ -828,6 +828,7 @@ DeclareGlobalFunction( "Edit" );
## <ManSection>
## <Func Name="HexSHA256" Arg='string'/>
## <Func Name="HexSHA256" Arg='stream' Label="for a stream"/>
## <Func Name="HexSHA256" Arg='state' Label="for a SHA256 state"/>
##
## <Description>
## <Index>hash function</Index>
Expand All @@ -837,6 +838,10 @@ DeclareGlobalFunction( "Edit" );
## (see Chapter&nbsp;<Ref Chap="Streams"/> to learn about streams)
## when read from the current position until EOF (end-of-file).
## <P/>
## Given a SHA-256 state (see <Ref Func="SHA256State"/>), return the checksum
## of everything accumulated in it so far. Reading it does not consume the
## state: it may be read again, and fed more data afterwards.
## <P/>
## The checksum is returned as string with 64 lowercase hexadecimal digits.
## <Example><![CDATA[
## gap> HexSHA256("abcd");
Expand Down Expand Up @@ -889,5 +894,91 @@ DeclareGlobalFunction("HexSHA256");
##
DeclareGlobalFunction("HexSHA256File");

## <#GAPDoc Label="IsSHA256State">
## <ManSection>
## <Filt Name="IsSHA256State" Arg='obj' Type='Category'/>
##
## <Description>
## The category of SHA-256 states, as returned by
## <Ref Func="SHA256State"/>.
## </Description>
## </ManSection>
## <#/GAPDoc>
##
DeclareCategory("IsSHA256State", IsObject);

BIND_GLOBAL("GAP_SHA256_State_Family", NewFamily("GAP_SHA256_State_Family"));

BIND_GLOBAL("GAP_SHA256_State_Type",
NewType(NewFamily("GAP_SHA256_State_Family"), IsObject) );
NewType(GAP_SHA256_State_Family, IsSHA256State) );


## <#GAPDoc Label="SHA256State">
## <ManSection>
## <Func Name="SHA256State" Arg=''/>
##
## <Description>
## <Index>hash function</Index>
## <Index>checksum</Index>
## Return a new SHA-256 state, which accumulates data fed to it with
## <Ref Func="UpdateSHA256"/> and <Ref Func="UpdateSHA256File"/>. Its digest
## is read with <Ref Func="HexSHA256"/>.
## <Log><![CDATA[
## gap> s := SHA256State();;
## gap> UpdateSHA256(s, "ab");;
## gap> UpdateSHA256(s, "cd");;
## gap> HexSHA256(s) = HexSHA256("abcd");
## true
## ]]></Log>
## </Description>
## </ManSection>
## <#/GAPDoc>
##
DeclareGlobalFunction("SHA256State");


## <#GAPDoc Label="UpdateSHA256">
## <ManSection>
## <Func Name="UpdateSHA256" Arg='state, string'/>
##
## <Description>
## Append <A>string</A> to the data accumulated in <A>state</A>.
## <A>string</A> is left untouched.
## </Description>
## </ManSection>
## <#/GAPDoc>
##
DeclareGlobalFunction("UpdateSHA256");


## <#GAPDoc Label="UpdateSHA256File">
## <ManSection>
## <Func Name="UpdateSHA256File" Arg='state, filename, decompress'/>
##
## <Description>
## Append the contents of the file <A>filename</A> to the data accumulated in
## <A>state</A>. Return <K>true</K> on success, or <K>fail</K> if the file
## cannot be read, in which case <A>state</A> is left as it was.
## <P/>
## The file is read as binary data, so the result always describes the bytes
## on disk, also on systems which distinguish text and binary mode and would
## otherwise translate line endings. It is read in chunks, so the size of
## the file is not limited by the available memory.
## <P/>
## <A>decompress</A> means what it does for
## <Ref Func="HexSHA256File"/>, and is likewise required.
## <Log><![CDATA[
## gap> name := Filename(DirectoryTemporary(), "test.txt");;
## gap> FileString(name, "cd");;
## gap> s := SHA256State();;
## gap> UpdateSHA256(s, "ab");
## gap> UpdateSHA256File(s, name, false);
## true
## gap> HexSHA256(s) = HexSHA256("abcd");
## true
## ]]></Log>
## </Description>
## </ManSection>
## <#/GAPDoc>
##
DeclareGlobalFunction("UpdateSHA256File");
56 changes: 44 additions & 12 deletions lib/files.gi
Original file line number Diff line number Diff line change
Expand Up @@ -407,10 +407,47 @@ function(words)
return res;
end);

InstallGlobalFunction( SHA256State, GAP_SHA256_INIT );

InstallMethod( PrintObj, "for a SHA256 state", [ IsSHA256State ],
function(state)
Print("<SHA256 state>");
end);

InstallGlobalFunction( UpdateSHA256,
function(state, string)
if not IsSHA256State(state) then
ErrorNoReturn("<state> must be a SHA256 state");
elif not IsString(string) then
ErrorNoReturn("<string> must be a string");
fi;

# CopyToStringRep: the kernel converts its argument to a string in place,
# which would retype a list of characters belonging to the caller.
GAP_SHA256_UPDATE(state, CopyToStringRep(string));
end);

InstallGlobalFunction( UpdateSHA256File,
function(state, filename, decompress)
if not IsSHA256State(state) then
ErrorNoReturn("<state> must be a SHA256 state");
elif not IsString(filename) then
ErrorNoReturn("<filename> must be a string");
elif not decompress in [ true, false ] then
ErrorNoReturn("<decompress> must be 'true' or 'false'");
fi;

return GAP_SHA256_UPDATE_FILE(state, UserHomeExpand(filename), decompress);
end);

InstallGlobalFunction( HexSHA256,
function(str)
local s, chunk;

if IsSHA256State(str) then
return GAP_SHA256_HexOfWords(GAP_SHA256_DIGEST(str));
fi;

s := GAP_SHA256_INIT();
if IsString(str) then
GAP_SHA256_UPDATE(s, CopyToStringRep(str));
Expand All @@ -423,25 +460,20 @@ function(str)
fi;
until not IsString(chunk) or Length(chunk) = 0;
else
ErrorNoReturn("<str> has to be a string or an input stream");
ErrorNoReturn("<str> has to be a string, an input stream, or a ",
"SHA256 state");
fi;

return GAP_SHA256_HexOfWords(GAP_SHA256_FINAL(s));
return GAP_SHA256_HexOfWords(GAP_SHA256_DIGEST(s));
end);

InstallGlobalFunction( HexSHA256File,
function(filename, decompress)
local res;

if not IsString(filename) then
ErrorNoReturn("<filename> must be a string");
elif not decompress in [ true, false ] then
ErrorNoReturn("<decompress> must be 'true' or 'false'");
fi;
local s;

res := GAP_SHA256_FILE(UserHomeExpand(filename), decompress);
if res = fail then
s := SHA256State();
if UpdateSHA256File(s, filename, decompress) = fail then
return fail;
fi;
return GAP_SHA256_HexOfWords(res);
return HexSHA256(s);
end);
139 changes: 79 additions & 60 deletions src/sha256.c
Original file line number Diff line number Diff line change
Expand Up @@ -240,12 +240,63 @@ static int sha256_final(sha256_state_t * state)
return 0;
}

static sha256_state_t * SHA256_STATE(Obj state)
{
return (sha256_state_t *)(&ADDR_OBJ(state)[1]);
}

#define RequireSHA256State(funcname, op) \
RequireArgumentCondition(funcname, op, \
IS_DATOBJ(op) && \
TYPE_OBJ(op) == GAP_SHA256_State_Type, \
"must be a SHA256 state")

// Feed the contents of <filename> into <st>. Mode "rb" rather than "r" so
// that no line ending translation happens; the digest has to describe the
// bytes on disk. With <decompress> set, a file whose name ends in '.gz' is
// read as its decompressed content instead, matching 'InputTextFile'.
// Returns 0 on success, -1 if the file could not be read.
static int sha256_update_file(sha256_state_t * st,
Obj filename,
Obj decompress)
{
Int fid, len;
UChar buf[16384];

fid = SyFopen(CONST_CSTR_STRING(filename), "rb", decompress == True);
if (fid == -1)
return -1;

while ((len = SyRead(fid, buf, sizeof(buf))) > 0) {
sha256_update(st, buf, len);
}
SyFclose(fid);
return len < 0 ? -1 : 0;
}

// The eight words of <st> as a plain list, most significant first. <st> is
// taken by value on purpose: NEW_PLIST below may trigger a garbage collection,
// which can move bags, so this must not be handed a pointer into one.
static Obj sha256_words(sha256_state_t st)
{
Obj result;
int i;

result = NEW_PLIST(T_PLIST, 8);
SET_LEN_PLIST(result, 8);
for (i = 0; i < 8; i++) {
SET_ELM_PLIST(result, i + 1, ObjInt_UInt(st.r[i]));
CHANGED_BAG(result);
}
return result;
}

Obj FuncGAP_SHA256_INIT(Obj self)
{
Obj result;
sha256_state_t * sptr;

result = NewBag(T_DATOBJ, sizeof(UInt4) + sizeof(sha256_state_t));
result = NewBag(T_DATOBJ, sizeof(Obj) + sizeof(sha256_state_t));
SET_TYPE_OBJ(result, GAP_SHA256_State_Type);

sptr = (sha256_state_t *)(&ADDR_OBJ(result)[1]);
Expand All @@ -256,82 +307,50 @@ Obj FuncGAP_SHA256_INIT(Obj self)

Obj FuncGAP_SHA256_UPDATE(Obj self, Obj state, Obj bytes)
{
sha256_state_t * sptr;

RequireArgumentCondition(SELF_NAME, state,
IS_DATOBJ(state) &&
TYPE_OBJ(state) == GAP_SHA256_State_Type,
"must be a SHA256 state");
RequireSHA256State(SELF_NAME, state);
RequireStringRep(SELF_NAME, bytes);

sptr = (sha256_state_t *)(&ADDR_OBJ(state)[1]);
sha256_update(sptr, CHARS_STRING(bytes), GET_LEN_STRING(bytes));
sha256_update(SHA256_STATE(state), CHARS_STRING(bytes),
GET_LEN_STRING(bytes));
CHANGED_BAG(state);

return 0;
}

Obj FuncGAP_SHA256_FINAL(Obj self, Obj state)
// Feed a whole file into <state>. Returns 'true' on success, or 'fail' if
// the file could not be read, in which case <state> is left as it was.
Obj FuncGAP_SHA256_UPDATE_FILE(Obj self, Obj state, Obj filename,
Obj decompress)
{
Obj result;
sha256_state_t * sptr;
int i;
sha256_state_t st;

RequireArgumentCondition(SELF_NAME, state,
IS_DATOBJ(state) &&
TYPE_OBJ(state) == GAP_SHA256_State_Type,
"must be a SHA256 state");
RequireSHA256State(SELF_NAME, state);
RequireStringRep(SELF_NAME, filename);
RequireTrueOrFalse(SELF_NAME, decompress);

result = NEW_PLIST(T_PLIST, 8);
SET_LEN_PLIST(result, 8);
// Work on a copy, so that a file which turns out to be unreadable part
// way through does not leave half of itself in the caller's state.
st = *SHA256_STATE(state);
if (sha256_update_file(&st, filename, decompress) < 0)
return Fail;

sptr = (sha256_state_t *)(&ADDR_OBJ(state)[1]);
sha256_final(sptr);
*SHA256_STATE(state) = st;
CHANGED_BAG(state);

for (i = 0; i < 8; i++) {
SET_ELM_PLIST(result, i + 1, ObjInt_UInt(sptr->r[i]));
CHANGED_BAG(result);
}
return result;
return True;
}

Obj FuncGAP_SHA256_FILE(Obj self, Obj filename, Obj decompress)
// The digest of <state> as it stands, leaving <state> usable. Padding a
// SHA256 state is destructive, so this finalizes a copy: reading the digest
// must not be a one-shot operation the caller has to know about.
Obj FuncGAP_SHA256_DIGEST(Obj self, Obj state)
{
Obj result;
sha256_state_t st;
Int fid, len;
int i;
UChar buf[16384];

RequireStringRep(SELF_NAME, filename);
RequireTrueOrFalse(SELF_NAME, decompress);

// Mode "rb" rather than "r" so that no line ending translation happens;
// the digest has to describe the bytes on disk. With <decompress> set,
// a file whose name ends in '.gz' is hashed as its decompressed content
// instead, matching what 'InputTextFile' would read.
fid = SyFopen(CONST_CSTR_STRING(filename), "rb", decompress == True);
if (fid == -1)
return Fail;

sha256_init(&st);
while ((len = SyRead(fid, buf, sizeof(buf))) > 0) {
sha256_update(&st, buf, len);
}
SyFclose(fid);
if (len < 0)
return Fail;
RequireSHA256State(SELF_NAME, state);

st = *SHA256_STATE(state);
sha256_final(&st);

result = NEW_PLIST(T_PLIST, 8);
SET_LEN_PLIST(result, 8);
for (i = 0; i < 8; i++) {
SET_ELM_PLIST(result, i + 1, ObjInt_UInt(st.r[i]));
CHANGED_BAG(result);
}
return result;
return sha256_words(st);
}

Obj FuncGAP_SHA256_HMAC(Obj self, Obj key, Obj text)
Expand Down Expand Up @@ -393,8 +412,8 @@ Obj FuncGAP_SHA256_HMAC(Obj self, Obj key, Obj text)
static StructGVarFunc GVarFuncs[] = {
GVAR_FUNC_0ARGS(GAP_SHA256_INIT),
GVAR_FUNC_2ARGS(GAP_SHA256_UPDATE, state, bytes),
GVAR_FUNC_1ARGS(GAP_SHA256_FINAL, state),
GVAR_FUNC_2ARGS(GAP_SHA256_FILE, filename, decompress),
GVAR_FUNC_3ARGS(GAP_SHA256_UPDATE_FILE, state, filename, decompress),
GVAR_FUNC_1ARGS(GAP_SHA256_DIGEST, state),
GVAR_FUNC_2ARGS(GAP_SHA256_HMAC, key, text),

{ 0 } // Finish with an empty entry
Expand Down
Loading
Loading