Skip to content
Merged
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
1 change: 1 addition & 0 deletions doc/ref/string.xml
Original file line number Diff line number Diff line change
Expand Up @@ -691,6 +691,7 @@ gap> HexStringInt(last);
<#Include Label="EvalString">
<#Include Label="CrcString">
<#Include Label="HexSHA256">
<#Include Label="HexSHA256File">
<#Include Label="Pluralize">

</Section>
Expand Down
39 changes: 39 additions & 0 deletions lib/files.gd
Original file line number Diff line number Diff line change
Expand Up @@ -850,5 +850,44 @@ DeclareGlobalFunction( "Edit" );
##
DeclareGlobalFunction("HexSHA256");


## <#GAPDoc Label="HexSHA256File">
## <ManSection>
## <Func Name="HexSHA256File" Arg='filename, decompress'/>
##
## <Description>
## <Index>hash function</Index>
## <Index>checksum</Index>
## Return the SHA-256 cryptographic checksum of the contents of the file
## <A>filename</A>, as a string with 64 lowercase hexadecimal digits,
## or <K>fail</K> if the file cannot be read.
## <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> must be <K>true</K> or <K>false</K>, and says which of
## two different checksums is wanted: with <K>true</K>, a file whose name
## ends in <C>.gz</C> is decompressed while reading, so that the checksum
## describes the data as <Ref Func="StringFile" BookName="gapdoc"/> would
## read it; with <K>false</K>, it describes the bytes on disk, which is what
## one wants in order to compare against a checksum published alongside a
## file. There is no default, because everything else in &GAP; decompresses
## and silently doing the opposite would be a trap.
## <Log><![CDATA[
## gap> name := Filename(DirectoryTemporary(), "test.txt");;
## gap> FileString(name, "abcd");;
## gap> HexSHA256File(name, false);
## "88d4266fd4e6338d13b845fcf289579d209c897823b9217da3e161936f031589"
## gap> HexSHA256File("/no/such/file", false);
## fail
## ]]></Log>
## </Description>
## </ManSection>
## <#/GAPDoc>
##
DeclareGlobalFunction("HexSHA256File");

BIND_GLOBAL("GAP_SHA256_State_Type",
NewType(NewFamily("GAP_SHA256_State_Family"), IsObject) );
57 changes: 41 additions & 16 deletions lib/files.gi
Original file line number Diff line number Diff line change
Expand Up @@ -393,30 +393,55 @@ InstallGlobalFunction(RemoveDirectoryRecursively,
return Dowork(dirname);
end );

BindGlobal( "GAP_SHA256_HexOfWords",
function(words)
local res;

res := Sum([0..7], i -> words[8-i]*2^(32*i));
res := LowercaseString(HexStringInt(res));
# HexStringInt drops leading zero digits, but a SHA256 digest is always
# 256 bits = 64 hex digits, so left-pad with '0' if the top byte(s) were 0.
if Length(res) < 64 then
res := Concatenation(ListWithIdenticalEntries(64 - Length(res), '0'), res);
fi;
return res;
end);

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

s := GAP_SHA256_INIT();
if IsString(str) then
str := CopyToStringRep(str);
GAP_SHA256_UPDATE(s, CopyToStringRep(str));
elif IsInputStream(str) then
str := ReadAll(str);
# TODO: instead o reading the complete stream at once (which might be
# huge), it would be better to read it in chunks, say 16kb at a time.
# Alas, our streams API currently offers no way to do that.
# read in chunks: the stream may deliver more than fits in memory
repeat
chunk := ReadAll(str, 65536);
if IsString(chunk) and Length(chunk) > 0 then
GAP_SHA256_UPDATE(s, CopyToStringRep(chunk));
fi;
until not IsString(chunk) or Length(chunk) = 0;
else
ErrorNoReturn("<str> has to be a string or an input stream");
fi;

s := GAP_SHA256_INIT();
GAP_SHA256_UPDATE(s, str);
res := GAP_SHA256_FINAL(s);
res := Sum([0..7], i -> res[8-i]*2^(32*i));;
res := LowercaseString(HexStringInt(res));
# HexStringInt drops leading zero digits, but a SHA256 digest is always
# 256 bits = 64 hex digits, so left-pad with '0' if the top byte(s) were 0.
if Length(res) < 64 then
res := Concatenation(ListWithIdenticalEntries(64 - Length(res), '0'), res);
return GAP_SHA256_HexOfWords(GAP_SHA256_FINAL(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;
return res;

res := GAP_SHA256_FILE(UserHomeExpand(filename), decompress);
if res = fail then
return fail;
fi;
return GAP_SHA256_HexOfWords(res);
end);
40 changes: 40 additions & 0 deletions src/sha256.c
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
#include "objects.h"
#include "plist.h"
#include "stringobj.h"
#include "sysfiles.h"

#include "config.h"

Expand Down Expand Up @@ -295,6 +296,44 @@ Obj FuncGAP_SHA256_FINAL(Obj self, Obj state)
return result;
}

Obj FuncGAP_SHA256_FILE(Obj self, Obj filename, Obj decompress)
{
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;

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;
}

Obj FuncGAP_SHA256_HMAC(Obj self, Obj key, Obj text)
{
UInt i, klen;
Expand Down Expand Up @@ -355,6 +394,7 @@ 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_2ARGS(GAP_SHA256_HMAC, key, text),

{ 0 } // Finish with an empty entry
Expand Down
39 changes: 39 additions & 0 deletions tst/testinstall/sha256.tst
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#
#@local dir, gzname, name, out, state, str
gap> START_TEST("sha256.tst");

#
Expand Down Expand Up @@ -57,5 +58,43 @@ gap> HexSHA256(InputTextString("abcd\r\n"));
gap> HexSHA256(InputTextString(""));
"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"

# HexSHA256File
gap> dir := DirectoryTemporary();;
gap> name := Filename(dir, "test.txt");;
gap> FileString(name, "abcd");;
gap> HexSHA256File(name, false);
"88d4266fd4e6338d13b845fcf289579d209c897823b9217da3e161936f031589"
gap> FileString(Filename(dir, "empty.txt"), "");;
gap> HexSHA256File(Filename(dir, "empty.txt"), false);
"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"

# larger than the read buffer, so that more than one chunk gets hashed
gap> str := Concatenation(List([1 .. 5000], i -> "0123456789"));;
gap> FileString(Filename(dir, "big.txt"), str);;
gap> HexSHA256File(Filename(dir, "big.txt"), false) = HexSHA256(str);
true

# a file that is not there
gap> HexSHA256File(Filename(dir, "no-such-file"), false);
fail

# the two answers a '.gz' file has
gap> gzname := Filename(dir, "compressed.txt.gz");;
gap> out := OutputGzipFile(gzname, false);;
gap> WriteAll(out, "abcd");;
gap> CloseStream(out);
gap> HexSHA256File(gzname, false) = HexSHA256("abcd");
false
gap> HexSHA256File(gzname, true) = HexSHA256("abcd");
true

# argument checking
gap> HexSHA256File(name);
Error, Function: number of arguments must be 2 (not 1)
gap> HexSHA256File(42, false);
Error, <filename> must be a string
gap> HexSHA256File(name, "yes");
Error, <decompress> must be 'true' or 'false'

#
gap> STOP_TEST("sha256.tst");
Loading