Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
37 changes: 37 additions & 0 deletions lib/files.gd
Original file line number Diff line number Diff line change
Expand Up @@ -850,5 +850,42 @@ 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/>
## If the optional argument <A>decompress</A> is <K>true</K>, then a file
## whose name ends in <C>.gz</C> is decompressed while reading, so that the
## checksum describes the decompressed data, as read by
## <Ref Func="StringFile" BookName="gapdoc"/>.
## The default is <K>false</K>, which is what one wants in order to compare
## against a checksum published alongside a file.
## <Log><![CDATA[
## gap> name := Filename(DirectoryTemporary(), "test.txt");;
## gap> FileString(name, "abcd");;
## gap> HexSHA256File(name);
## "88d4266fd4e6338d13b845fcf289579d209c897823b9217da3e161936f031589"
## gap> HexSHA256File("/no/such/file");
## fail
## ]]></Log>
## </Description>
## </ManSection>
## <#/GAPDoc>
##
DeclareGlobalFunction("HexSHA256File");

BIND_GLOBAL("GAP_SHA256_State_Type",
NewType(NewFamily("GAP_SHA256_State_Family"), IsObject) );
66 changes: 50 additions & 16 deletions lib/files.gi
Original file line number Diff line number Diff line change
Expand Up @@ -393,30 +393,64 @@ 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(args...)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
function(args...)
function(filename, decompress...)

and then simplify the code accordingly

local filename, decompress, res;

if Length(args) = 0 or 2 < Length(args) then
ErrorNoReturn("usage: HexSHA256File( <filename>[, <decompress>] )");
fi;
return res;
filename := args[1];
if not IsString(filename) then
ErrorNoReturn("<filename> must be a string");
fi;
decompress := false;
if Length(args) = 2 then
if not args[2] in [ true, false ] then
ErrorNoReturn("<decompress> must be 'true' or 'false'");
fi;
decompress := args[2];

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps assign decompress before the check for true/false, then the error message fits better.

fi;

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
43 changes: 43 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,47 @@ 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);
"88d4266fd4e6338d13b845fcf289579d209c897823b9217da3e161936f031589"
gap> FileString(Filename(dir, "empty.txt"), "");;
gap> HexSHA256File(Filename(dir, "empty.txt"));
"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")) = HexSHA256(str);
true

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

# '.gz' files are hashed as they are on disk unless asked otherwise
gap> gzname := Filename(dir, "compressed.txt.gz");;
gap> out := OutputGzipFile(gzname, false);;
gap> WriteAll(out, "abcd");;
gap> CloseStream(out);
gap> HexSHA256File(gzname) = HexSHA256("abcd");
false
gap> HexSHA256File(gzname, true) = HexSHA256("abcd");
true
gap> HexSHA256File(gzname, false) = HexSHA256File(gzname);
true

# argument checking
gap> HexSHA256File();
Error, usage: HexSHA256File( <filename>[, <decompress>] )
gap> HexSHA256File(name, true, true);
Error, usage: HexSHA256File( <filename>[, <decompress>] )
gap> HexSHA256File(42);
Error, <filename> must be a string
gap> HexSHA256File(name, "yes");
Error, <decompress> must be 'true' or 'false'

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