Skip to content

Commit

Permalink
added crunched package and implemented a simple RLE crunching interface
Browse files Browse the repository at this point in the history
elf SRAM is crunched with this new package on Snapshot()

.gitigore updated with */testdata

added fuzz target to Makefile
  • Loading branch information
JetSetIlly committed Jul 6, 2024
1 parent 61baee5 commit dc084be
Show file tree
Hide file tree
Showing 8 changed files with 396 additions and 18 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
*.swo
.DS_Store
Session.vim
*/testdata
vendor/*
tags
test_roms/*
Expand Down
6 changes: 5 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ vet: check_awk
'

### testing targets
.PHONY: test race race_debug
.PHONY: test race race_debug fuzz

test:
# testing with shuffle preferred but it's only available in go 1.17 onwards
Expand All @@ -100,6 +100,10 @@ race: generate test
race_debug: generate test
$(goBinary) run -race gopher2600.go debug $(profilingRom)

fuzz: generate test
# fuzz testing cannot work with multiple packages so we must list the ones
# we want to include
$(goBinary) test -fuzztime=30s -fuzz . ./crunched/

### profiling targets
.PHONY: profile profile_cpu profile_cpu_play profile_cpu_debug profile_mem_play profile_mem_debug profile_trace
Expand Down
43 changes: 43 additions & 0 deletions crunched/crunched.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// This file is part of Gopher2600.
//
// Gopher2600 is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Gopher2600 is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Gopher2600. If not, see <https://www.gnu.org/licenses/>.

package crunched

// Data provides the interface to a crunched data type
type Data interface {
// IsCrunched returns true if data is currently crunched
IsCrunched() bool

// Size returns the uncrunched size and the current size of the data. If the
// data is currently crunched then the two values will be the same
Size() (int, int)

// Data returns a pointer to the uncrunched data
Data() *[]byte

// Snapshot makes a copy of the data and crunching it if required. The data will
// be uncrunched automatically when Data() function is called
Snapshot() Data
}

// Inspection provides the interface to the crunched data type and provides the
// ability to inspect the data in its current form
type Inspection interface {
Data

// Inspect returns data in the current state. In other words, the data will
// not be decrunched as it would be with the Data() function
Inspect() *[]byte
}
21 changes: 21 additions & 0 deletions crunched/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// This file is part of Gopher2600.
//
// Gopher2600 is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Gopher2600 is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Gopher2600. If not, see <https://www.gnu.org/licenses/>.

// Package crunched provides the Data interface. Implementations of this
// interface can store data in a crunched and uncrunched state.
//
// The intention is that implementations crunch data after a call to Snapshot()
// and transparently uncrunch it on a call to Data().
package crunched
156 changes: 156 additions & 0 deletions crunched/quick.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
// This file is part of Gopher2600.
//
// Gopher2600 is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Gopher2600 is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Gopher2600. If not, see <https://www.gnu.org/licenses/>.

package crunched

type quick struct {
crunched bool
data []byte
uncrunchedSize int
}

// NewQuick returns an implementation of the Data interface that is intended to
// perform quickly on both crunching and decrunching.
//
// For simplicity, the minimum amount of data allocated will be 4 bytes.
func NewQuick(size int) Data {
size = max(size, 4)
return &quick{
data: make([]byte, size),
uncrunchedSize: size,
}
}

// IsCrunched returns true if data is currently crunched
//
// This function implements the Data interface
func (c *quick) IsCrunched() bool {
return c.crunched
}

// Size returns the uncrunched size and the current size of the data. If the
// data is currently crunched then the two values will be the same
//
// This function implements the Data interface
func (c *quick) Size() (int, int) {
return c.uncrunchedSize, len(c.data)
}

// Data returns a pointer to the uncrunched data
//
// This function implements the Data interface
func (c *quick) Data() *[]byte {
if c.crunched {
// sanity check. with the current RLE method the number of bytes in the
// crunched data should be a multiple of two
if len(c.data)&0x01 == 0x01 {
panic("crunched data should have an even number of bytes")
}

// make a reference to the crunched data before creating space for the
// uncrunched data
working := c.data
c.data = make([]byte, c.uncrunchedSize)

// undo the RLE process
var idx int
for i := 0; i < len(working); i += 2 {
for r := 0; r <= int(working[i+1]); r++ {
c.data[idx] = working[i]
idx++
}
}

// data is now uncrunched
c.crunched = false
}

return &c.data
}

// Snapshot makes a copy of the data and crunching it if required. The data will
// be uncrunched automatically when Data() function is called
//
// This function implements the Data interface
func (c *quick) Snapshot() Data {
d := *c

if !d.crunched {
working := make([]byte, d.uncrunchedSize)

var ct int
var idx int
working[idx] = c.data[0]

// assume crunching has succeeded unless explicitely told otherwise
d.crunched = true

// very basic RLE algorithm:
// 1) each byte is followed by a count value
// 2) maximum count value is 255
for _, v := range c.data[1:] {
if v == working[idx] && ct < 255 {
ct++
} else {
// check that the crunched data isn't getting too large. we'll
// be adding two bytes to the crunch stream so the check here is
// to make sure that won't overflow the size of the array
if idx >= len(working)-2 {
d.crunched = false
break // for loop
}

// output count to the crunch stream
idx++
working[idx] = byte(ct)

// output new byte to crunch stream
idx++
working[idx] = v

// count will begin again with the new byte
ct = 0
}
}

// if the data has been crunched then allocate just enough memory to
// store the crunched data before returning
if d.crunched {
idx++
working[idx] = byte(ct)
d.data = make([]byte, idx+1)
copy(d.data, working[:idx+1])
return &d
}

// if data is not crunched then we intentionally fall through to the
// plain data copy below
}

// copy data as it exists now. this may be crunched data or uncrunched data.
// it doesn't matter either way
d.data = make([]byte, len(c.data))
copy(d.data, c.data)

return &d
}

// Inspect returns data in the current state. In other words, the data will
// not be decrunched as it would be with the Data() function
//
// This function implements the Peep interface
func (c *quick) Inspect() *[]byte {
return &c.data
}
137 changes: 137 additions & 0 deletions crunched/quick_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
// This file is part of Gopher2600.
//
// Gopher2600 is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Gopher2600 is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Gopher2600. If not, see <https://www.gnu.org/licenses/>.

package crunched_test

import (
"crypto/md5"
"math/rand"
"testing"

"github.com/jetsetilly/gopher2600/crunched"
"github.com/jetsetilly/gopher2600/test"
)

func TestEmptyData_Quick(t *testing.T) {
// create 100 bytes of empty data
qa := crunched.NewQuick(100)

// take hash of data before crunching
preCrunchHash := md5.Sum(*qa.Data())

// data should not be crunched
test.ExpectFailure(t, qa.IsCrunched())

// take a snapshot of the data
qb := qa.Snapshot()

// the snapshotted data should be crunched
test.ExpectSuccess(t, qb.IsCrunched())

// the original data should be left uncrunched
test.ExpectSuccess(t, !qa.IsCrunched())

// inspect the crunched data
inspection := qb.(crunched.Inspection).Inspect()
expectedData := []byte{0, 99}
test.DemandEquality(t, len(*inspection), len(expectedData))
for i, v := range *inspection {
test.ExpectEquality(t, v, expectedData[i])
}

// check that hash of uncrunched data is the same as it was before
postCrunchedHash := md5.Sum(*qb.Data())
test.ExpectEquality(t, preCrunchHash, postCrunchedHash)

// obtaining the data from the snapshot should leave the data in the
// snapshot in an uncrunched state
test.ExpectSuccess(t, !qb.IsCrunched())
}

func TestUncompressableData_quick(t *testing.T) {
// create 256 bytes of empty data
qa := crunched.NewQuick(256)

// insert data that can't be compressed by the quick method
data := qa.Data()
for i := 0; i < len(*data); i++ {
(*data)[i] = byte(i)
}

// take hash of data before crunching
preCrunchHash := md5.Sum(*data)

// take a snapshot of the data
qb := qa.Snapshot()

// the snapshotted data should not be crunched
test.ExpectSuccess(t, !qb.IsCrunched())

// check that hash of uncrunched data is the same as it was before
postCrunchedHash := md5.Sum(*qb.Data())
test.ExpectEquality(t, preCrunchHash, postCrunchedHash)
}

func TestEmptyData_ExampleData(t *testing.T) {
// create 100 bytes of empty data
qa := crunched.NewQuick(20)

// insert data that can't be compressed by the quick method
data := qa.Data()
copy(*data, []byte{1, 2, 3, 3, 3, 3, 4, 4, 5, 6})

// snapshot should successfully crunch the data
qb := qa.Snapshot()
test.ExpectSuccess(t, qb.IsCrunched())

inspection := qb.(crunched.Inspection).Inspect()

expectedData := []byte{1, 0, 2, 0, 3, 3, 4, 1, 5, 0, 6, 0, 0, 9}
test.DemandEquality(t, len(*inspection), len(expectedData))
for i, v := range *inspection {
test.ExpectEquality(t, v, expectedData[i])
}
}

func FuzzQuick(f *testing.F) {
f.Fuzz(func(t *testing.T, size uint) {
qa := crunched.NewQuick(int(size))

// insert data that can't be compressed by the quick method
data := qa.Data()
(*data)[0] = byte(rand.Intn(255))
for i := 1; i < len(*data); i++ {
b := byte(rand.Intn(255))
for b == (*data)[i-1] {
b = byte(rand.Intn(255))
}
(*data)[i] = b
}

// take hash of data before crunching
preCrunchHash := md5.Sum(*data)

// take a snapshot of the data
qb := qa.Snapshot()

// the snapshotted data should not be crunched because it should be
// impossible with the data we've given it
test.ExpectSuccess(t, !qb.IsCrunched())

// check that hash of uncrunched data is the same as it was before
postCrunchedHash := md5.Sum(*qb.Data())
test.ExpectEquality(t, preCrunchHash, postCrunchedHash)
})
}
Loading

0 comments on commit dc084be

Please sign in to comment.