diff --git a/.gitignore b/.gitignore index 057fdab6e..543c518cb 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,5 @@ Lantern.app lantern.dmg src/github.com/getlantern/flashlight/ui/resources.go src/github.com/getlantern/flashlight/lantern.syso +dmgbackground.png +dmgbackground_versioned.svg diff --git a/README.md b/README.md index a267b1bec..03c50bde1 100644 --- a/README.md +++ b/README.md @@ -7,9 +7,7 @@ lantern). Flashlight requires [Go 1.4.x](http://golang.org/dl/). -You will also need [npm](https://www.npmjs.com/) and gulp. - -`npm install -g gulp` +You will also need [npm](https://www.npmjs.com/). It is convenient to build flashlight for multiple platforms using [gox](https://github.com/mitchellh/gox). @@ -87,12 +85,13 @@ Lantern on OS X is packaged as the `Lantern.app` app bundle, distributed inside of a drag-and-drop dmg installer. The app bundle and dmg can be created using `./package_osx.bash`. -This script requires the node module `appdmg`. Assuming you have homebrew -installed, you can get it with ... +This script requires that you have [nodejs](http://nodejs.org/) installed. + +The script takes a single parameter, which is the version string to display in +the installer background, for example: ```bash -brew install node -npm install -g appdmg +./package_osx.bash 2.0.0_beta1 ``` `./package_osx.bash` signs the Lantern.app using the BNS code signing diff --git a/dmgbackground.png b/dmgbackground.png deleted file mode 100644 index eaa580169..000000000 Binary files a/dmgbackground.png and /dev/null differ diff --git a/dmgbackground.svg b/dmgbackground.svg new file mode 100644 index 000000000..f9006c942 --- /dev/null +++ b/dmgbackground.svg @@ -0,0 +1,3539 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +__VERSION__ + diff --git a/genassets.bash b/genassets.bash index 4d268f91f..302addff3 100755 --- a/genassets.bash +++ b/genassets.bash @@ -7,6 +7,10 @@ APP="$LANTERN_UI/app" DIST="$LANTERN_UI/dist" if [ ! -d $DIST ] || [ $APP -nt $DIST ]; then + # Install gulp (requires nodejs) + echo "Installing gulp tool if necessary (requires nodejs)" + which gulp || npm install -g gulp + echo "Updating dist folder" cd $LANTERN_UI npm install @@ -25,5 +29,5 @@ echo " " >> $dest tarfs -pkg ui src/github.com/getlantern/lantern-ui/dist >> $dest echo "Now embedding lantern.ico to windows executable" -go get github.com/akavel/rsrc +go install github.com/akavel/rsrc rsrc -ico lantern.ico -o src/github.com/getlantern/flashlight/lantern.syso diff --git a/lantern.dmg.json b/lantern.dmg.json index 9bebc427a..ea810fa1d 100644 --- a/lantern.dmg.json +++ b/lantern.dmg.json @@ -4,7 +4,7 @@ "background": "dmgbackground.png", "icon-size": 100, "contents": [ - { "x": 100, "y": 140, "type": "file", "path": "Lantern.app" }, - { "x": 380, "y": 140, "type": "link", "path": "/Applications" } + { "x": 115, "y": 265, "type": "file", "path": "Lantern.app" }, + { "x": 440, "y": 265, "type": "link", "path": "/Applications" } ] } \ No newline at end of file diff --git a/package_osx.bash b/package_osx.bash index 44ce5f89b..b2cccd9e7 100755 --- a/package_osx.bash +++ b/package_osx.bash @@ -5,6 +5,17 @@ function die() { exit 1 } +if [ $# -lt "1" ] +then + die "$0: Version required" +fi + +echo "Installing svgexport tool if necessary (requires nodejs)" +which svgexport || npm install -g svgexport + +echo "Installing appdmg tool if necessary (requires nodejs)" +which appdmg || npm install -g appdmg + binary="lantern_darwin_amd64" dmg="Lantern.dmg" @@ -27,4 +38,9 @@ if [ -e $dmg ] then rm -Rf lantern.dmg || die "Could not remove existing lantern.dmg" fi + +echo "Generating background image" +sed "s/__VERSION__/$1/g" dmgbackground.svg > dmgbackground_versioned.svg +svgexport dmgbackground_versioned.svg dmgbackground.png 600:400 + appdmg lantern.dmg.json $dmg || "Could not package Lantern.app into dmg" \ No newline at end of file diff --git a/src/github.com/akavel/rsrc/.hgignore b/src/github.com/akavel/rsrc/.hgignore new file mode 100644 index 000000000..cc0aa48d4 --- /dev/null +++ b/src/github.com/akavel/rsrc/.hgignore @@ -0,0 +1,4 @@ +glob:*.res +glob:*.exe + +glob:tmp diff --git a/src/github.com/akavel/rsrc/AUTHORS b/src/github.com/akavel/rsrc/AUTHORS new file mode 100644 index 000000000..4bc50dc31 --- /dev/null +++ b/src/github.com/akavel/rsrc/AUTHORS @@ -0,0 +1,2 @@ +Mateusz Czapliński +shnmng diff --git a/src/github.com/akavel/rsrc/LICENSE.txt b/src/github.com/akavel/rsrc/LICENSE.txt new file mode 100644 index 000000000..d67e00ef3 --- /dev/null +++ b/src/github.com/akavel/rsrc/LICENSE.txt @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2013-2014 The rsrc Authors. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/src/github.com/akavel/rsrc/README.txt b/src/github.com/akavel/rsrc/README.txt new file mode 100644 index 000000000..2ee5af25a --- /dev/null +++ b/src/github.com/akavel/rsrc/README.txt @@ -0,0 +1,46 @@ +rsrc - Tool for embedding binary resources in Go programs. + +INSTALL: go get github.com/akavel/rsrc + +PREBUILT BINARIES for Windows/Linux/MacOSX available via 3rd party site: + http://gobuild.io/download/github.com/akavel/rsrc + +USAGE: + +rsrc [-manifest FILE.exe.manifest] [-ico FILE.ico[,FILE2.ico...]] -o FILE.syso + Generates a .syso file with specified resources embedded in .rsrc section. + The .syso file can be linked by Go linker when building Win32 executables. + Icon embedded this way will show up on application's .exe instead of empty icon. + Manifest file embedded this way will be recognized and detected by Windows. + +rsrc -data FILE.dat -o FILE.syso > FILE.c + Generates a .syso file with specified opaque binary blob embedded, + together with related .c file making it possible to access from Go code. + Theoretically cross-platform, but reportedly cannot compile together with cgo. + +The generated *.syso and *.c files should get automatically recognized +by 'go build' command and linked into an executable/library, as long as +there are any *.go files in the same directory. + +NOTE: starting with Go 1.4+, *.c files reportedly won't be linkable any more, + see: https://codereview.appspot.com/149720043 + +OPTIONS: + -data="": path to raw data file to embed + -ico="": comma-separated list of paths to .ico files to embed + -manifest="": path to a Windows manifest file to embed + -o="rsrc.syso": name of output COFF (.res or .syso) file + +Based on ideas presented by Minux. + +In case anything does not work, it'd be nice if you could report (either via Github +issues, or via email to czapkofan@gmail.com), and please attach the input file(s) +which resulted in a problem, plus error message & symptoms, and/or any other details. + +TODO MAYBE/LATER: +- fix or remove FIXMEs + +LICENSE: MIT + Copyright 2013-2014 The rsrc Authors. + +http://github.com/akavel/rsrc diff --git a/src/github.com/akavel/rsrc/binutil/plain.go b/src/github.com/akavel/rsrc/binutil/plain.go new file mode 100644 index 000000000..ddf5238b9 --- /dev/null +++ b/src/github.com/akavel/rsrc/binutil/plain.go @@ -0,0 +1,13 @@ +package binutil + +import ( + "reflect" +) + +func Plain(kind reflect.Kind) bool { + switch kind { + case reflect.Bool, reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr, reflect.Float32, reflect.Float64, reflect.Complex64, reflect.Complex128: + return true + } + return false +} diff --git a/src/github.com/akavel/rsrc/binutil/sizedfile.go b/src/github.com/akavel/rsrc/binutil/sizedfile.go new file mode 100644 index 000000000..3a6606fd8 --- /dev/null +++ b/src/github.com/akavel/rsrc/binutil/sizedfile.go @@ -0,0 +1,35 @@ +package binutil + +import ( + "io" + "os" +) + +type SizedReader interface { + io.Reader + Size() int64 +} + +type SizedFile struct { + f *os.File + s *io.SectionReader // helper, for Size() +} + +func (r *SizedFile) Read(p []byte) (n int, err error) { return r.s.Read(p) } +func (r *SizedFile) Size() int64 { return r.s.Size() } +func (r *SizedFile) Close() error { return r.f.Close() } + +func SizedOpen(filename string) (*SizedFile, error) { + f, err := os.Open(filename) + if err != nil { + return nil, err + } + info, err := f.Stat() + if err != nil { + return nil, err + } + return &SizedFile{ + f: f, + s: io.NewSectionReader(f, 0, info.Size()), + }, nil +} diff --git a/src/github.com/akavel/rsrc/binutil/walk.go b/src/github.com/akavel/rsrc/binutil/walk.go new file mode 100644 index 000000000..4aa4ad30e --- /dev/null +++ b/src/github.com/akavel/rsrc/binutil/walk.go @@ -0,0 +1,63 @@ +package binutil + +import ( + "errors" + "fmt" + "path" + "reflect" +) + +var ( + WALK_SKIP = errors.New("") +) + +type Walker func(v reflect.Value, path string) error + +func Walk(value interface{}, walker Walker) error { + err := walk(reflect.ValueOf(value), "/", walker) + if err == WALK_SKIP { + err = nil + } + return err +} + +func stopping(err error) bool { + return err != nil && err != WALK_SKIP +} + +func walk(v reflect.Value, spath string, walker Walker) error { + err := walker(v, spath) + if err != nil { + return err + } + v = reflect.Indirect(v) + switch v.Kind() { + case reflect.Slice, reflect.Array: + for i := 0; i < v.Len(); i++ { + err = walk(v.Index(i), spath+fmt.Sprintf("[%d]", i), walker) + if stopping(err) { + return err + } + } + case reflect.Interface: + err = walk(v.Elem(), spath, walker) + if stopping(err) { + return err + } + case reflect.Struct: + //t := v.Type() + for i := 0; i < v.NumField(); i++ { + //f := t.Field(i) //TODO: handle unexported fields + vv := v.Field(i) + err = walk(vv, path.Join(spath, v.Type().Field(i).Name), walker) + if stopping(err) { + return err + } + } + default: + // FIXME: handle other special cases too + // String + return nil + } + return nil +} diff --git a/src/github.com/akavel/rsrc/binutil/writer.go b/src/github.com/akavel/rsrc/binutil/writer.go new file mode 100644 index 000000000..cd2ef9781 --- /dev/null +++ b/src/github.com/akavel/rsrc/binutil/writer.go @@ -0,0 +1,33 @@ +package binutil + +import ( + "encoding/binary" + "io" + "reflect" +) + +type Writer struct { + W io.Writer + Offset uint32 //FIXME: int64? + Err error +} + +func (w *Writer) WriteLE(v interface{}) { + if w.Err != nil { + return + } + w.Err = binary.Write(w.W, binary.LittleEndian, v) + if w.Err != nil { + return + } + w.Offset += uint32(reflect.TypeOf(v).Size()) +} + +func (w *Writer) WriteFromSized(r SizedReader) { + if w.Err != nil { + return + } + var n int64 + n, w.Err = io.CopyN(w.W, r, r.Size()) + w.Offset += uint32(n) +} diff --git a/src/github.com/akavel/rsrc/coff/coff.go b/src/github.com/akavel/rsrc/coff/coff.go new file mode 100644 index 000000000..0678afc78 --- /dev/null +++ b/src/github.com/akavel/rsrc/coff/coff.go @@ -0,0 +1,380 @@ +package coff + +import ( + "debug/pe" + "encoding/binary" + "io" + "reflect" + "regexp" + "sort" + "strconv" + "strings" + + "github.com/akavel/rsrc/binutil" +) + +type Dir struct { // struct IMAGE_RESOURCE_DIRECTORY + Characteristics uint32 + TimeDateStamp uint32 + MajorVersion uint16 + MinorVersion uint16 + NumberOfNamedEntries uint16 + NumberOfIdEntries uint16 + DirEntries + Dirs +} + +type DirEntries []DirEntry +type Dirs []Dir + +type DirEntry struct { // struct IMAGE_RESOURCE_DIRECTORY_ENTRY + NameOrId uint32 + OffsetToData uint32 +} + +type DataEntry struct { // struct IMAGE_RESOURCE_DATA_ENTRY + OffsetToData uint32 + Size1 uint32 + CodePage uint32 //FIXME: what value here? for now just using 0 + Reserved uint32 +} + +type RelocationEntry struct { + RVA uint32 // "offset within the Section's raw data where the address starts." + SymbolIndex uint32 // "(zero based) index in the Symbol table to which the reference refers." + Type uint16 +} + +type Auxiliary [18]byte + +type Symbol struct { + Name [8]byte + Value uint32 + SectionNumber uint16 + Type uint16 + StorageClass uint8 + AuxiliaryCount uint8 + Auxiliaries []Auxiliary +} + +type StringsHeader struct { + Length uint32 +} + +const ( + MASK_SUBDIRECTORY = 1 << 31 + + RT_ICON = 3 + RT_GROUP_ICON = 3 + 11 + RT_MANIFEST = 24 +) + +// http://www.delorie.com/djgpp/doc/coff/symtab.html +const ( + DT_PTR = 1 + T_UCHAR = 12 +) + +var ( + STRING_RSRC = [8]byte{'.', 'r', 's', 'r', 'c', 0, 0, 0} + STRING_RDATA = [8]byte{'.', 'r', 'd', 'a', 't', 'a', 0, 0} + + LANG_ENTRY = DirEntry{NameOrId: 0x0409} //FIXME: language; what value should be here? + RELOC_ENTRY = RelocationEntry{ + SymbolIndex: 0, // "(zero based) index in the Symbol table to which the reference refers. Once you have loaded the COFF file into memory and know where each symbol is, you find the new updated address for the given symbol and update the reference accordingly." + Type: 7, // according to ldpe.c, this decodes to: IMAGE_REL_I386_DIR32NB + } +) + +type Sizer interface { + Size() int64 //NOTE: must not exceed limits of uint32, or behavior is undefined +} + +type Coff struct { + pe.FileHeader + pe.SectionHeader32 + + *Dir + DataEntries []DataEntry + Data []Sizer + + Relocations []RelocationEntry + Symbols []Symbol + StringsHeader + Strings []Sizer +} + +func NewRDATA() *Coff { + return &Coff{ + pe.FileHeader{ + Machine: 0x014c, //FIXME: find out how to differentiate this value, or maybe not necessary for Go + NumberOfSections: 1, // .data + TimeDateStamp: 0, + NumberOfSymbols: 2, // starting only with '.rdata', will increase; must include auxiliaries, apparently + SizeOfOptionalHeader: 0, + Characteristics: 0x0105, //http://www.delorie.com/djgpp/doc/coff/filhdr.html + }, + pe.SectionHeader32{ + Name: STRING_RDATA, + Characteristics: 0x40000040, // "INITIALIZED_DATA MEM_READ" ? + }, + + // "directory hierarchy" of .rsrc section; empty for .data function + nil, + []DataEntry{}, + + []Sizer{}, + + []RelocationEntry{}, + + []Symbol{Symbol{ + Name: STRING_RDATA, + Value: 0, + SectionNumber: 1, + Type: 0, // FIXME: wtf? + StorageClass: 3, // FIXME: is it ok? and uint8? and what does the value mean? + AuxiliaryCount: 1, + Auxiliaries: []Auxiliary{{}}, //http://www6.cptec.inpe.br/sx4/sx4man2/g1af01e/chap5.html + }}, + + StringsHeader{ + Length: uint32(binary.Size(StringsHeader{})), // empty strings table for now -- but we must still show size of the table's header... + }, + []Sizer{}, + } +} + +//NOTE: only usable for Coff created using NewRDATA +//NOTE: symbol names must be probably >8 characters long +//NOTE: symbol names should not contain embedded zeroes +func (coff *Coff) AddData(symbol string, data Sizer) { + coff.addSymbol(symbol) + coff.Data = append(coff.Data, data) + coff.SectionHeader32.SizeOfRawData += uint32(data.Size()) +} + +// addSymbol appends a symbol to Coff.Symbols and to Coff.Strings. +//NOTE: symbol s must be probably >8 characters long +//NOTE: symbol s should not contain embedded zeroes +func (coff *Coff) addSymbol(s string) { + coff.FileHeader.NumberOfSymbols++ + + buf := strings.NewReader(s + "\000") // ASCIIZ + r := io.NewSectionReader(buf, 0, int64(len(s)+1)) + coff.Strings = append(coff.Strings, r) + + coff.StringsHeader.Length += uint32(r.Size()) + + coff.Symbols = append(coff.Symbols, Symbol{ + //Name: // will be filled in Freeze + //Value: // as above + SectionNumber: 1, + Type: 0, // why 0??? // DT_PTR<<4 | T_UCHAR, // unsigned char* // (?) or use void* ? T_VOID=1 + StorageClass: 2, // 2=C_EXT, or 5=C_EXTDEF ? + AuxiliaryCount: 0, + }) +} + +func NewRSRC() *Coff { + return &Coff{ + pe.FileHeader{ + Machine: 0x014c, //FIXME: find out how to differentiate this value, or maybe not necessary for Go + NumberOfSections: 1, // .rsrc + TimeDateStamp: 0, // was also 0 in sample data from MinGW's windres.exe + NumberOfSymbols: 1, + SizeOfOptionalHeader: 0, + Characteristics: 0x0104, //FIXME: copied from windres.exe output, find out what should be here and why + }, + pe.SectionHeader32{ + Name: STRING_RSRC, + Characteristics: 0x40000040, // "INITIALIZED_DATA MEM_READ" ? + }, + + // "directory hierarchy" of .rsrc section: top level goes resource type, then id/name, then language + &Dir{}, + + []DataEntry{}, + []Sizer{}, + + []RelocationEntry{}, + + []Symbol{Symbol{ + Name: STRING_RSRC, + Value: 0, + SectionNumber: 1, + Type: 0, // FIXME: wtf? + StorageClass: 3, // FIXME: is it ok? and uint8? and what does the value mean? + AuxiliaryCount: 0, // FIXME: wtf? + }}, + + StringsHeader{ + Length: uint32(binary.Size(StringsHeader{})), // empty strings table -- but we must still show size of the table's header... + }, + []Sizer{}, + } +} + +//NOTE: function assumes that 'id' is increasing on each entry +//NOTE: only usable for Coff created using NewRSRC +func (coff *Coff) AddResource(kind uint32, id uint16, data Sizer) { + coff.Relocations = append(coff.Relocations, RELOC_ENTRY) + coff.SectionHeader32.NumberOfRelocations++ + + // find top level entry, inserting new if necessary at correct sorted position + entries0 := coff.Dir.DirEntries + dirs0 := coff.Dir.Dirs + i0 := sort.Search(len(entries0), func(i int) bool { + return entries0[i].NameOrId >= kind + }) + if i0 >= len(entries0) || entries0[i0].NameOrId != kind { + // inserting new entry & dir + entries0 = append(entries0[:i0], append([]DirEntry{{NameOrId: kind}}, entries0[i0:]...)...) + dirs0 = append(dirs0[:i0], append([]Dir{{}}, dirs0[i0:]...)...) + coff.Dir.NumberOfIdEntries++ + } + coff.Dir.DirEntries = entries0 + coff.Dir.Dirs = dirs0 + + // for second level, assume ID is always increasing, so we don't have to sort + dirs0[i0].DirEntries = append(dirs0[i0].DirEntries, DirEntry{NameOrId: uint32(id)}) + dirs0[i0].Dirs = append(dirs0[i0].Dirs, Dir{ + NumberOfIdEntries: 1, + DirEntries: DirEntries{LANG_ENTRY}, + }) + dirs0[i0].NumberOfIdEntries++ + + // calculate preceding DirEntry leaves, to find new index in Data & DataEntries + n := 0 + for _, dir0 := range dirs0[:i0+1] { + n += len(dir0.DirEntries) //NOTE: assuming 1 language here; TODO: dwell deeper if more langs added + } + n-- + + // insert new data in correct place + coff.DataEntries = append(coff.DataEntries[:n], append([]DataEntry{{Size1: uint32(data.Size())}}, coff.DataEntries[n:]...)...) + coff.Data = append(coff.Data[:n], append([]Sizer{data}, coff.Data[n:]...)...) +} + +// Freeze fills in some important offsets in resulting file. +func (coff *Coff) Freeze() { + switch coff.SectionHeader32.Name { + case STRING_RSRC: + coff.freezeRSRC() + case STRING_RDATA: + coff.freezeRDATA() + } +} + +func (coff *Coff) freezeCommon1(path string, offset, diroff uint32) (newdiroff uint32) { + switch path { + case "/Dir": + coff.SectionHeader32.PointerToRawData = offset + diroff = offset + case "/Relocations": + coff.SectionHeader32.PointerToRelocations = offset + coff.SectionHeader32.SizeOfRawData = offset - diroff + case "/Symbols": + coff.FileHeader.PointerToSymbolTable = offset + } + return diroff +} + +func freezeCommon2(v reflect.Value, offset *uint32) error { + if binutil.Plain(v.Kind()) { + *offset += uint32(binary.Size(v.Interface())) // TODO: change to v.Type().Size() ? + return nil + } + vv, ok := v.Interface().(Sizer) + if ok { + *offset += uint32(vv.Size()) + return binutil.WALK_SKIP + } + return nil +} + +func (coff *Coff) freezeRDATA() { + var offset, diroff, stringsoff uint32 + binutil.Walk(coff, func(v reflect.Value, path string) error { + diroff = coff.freezeCommon1(path, offset, diroff) + + RE := regexp.MustCompile + const N = `\[(\d+)\]` + m := matcher{} + //TODO: adjust symbol pointers + //TODO: fill Symbols.Name, .Value + switch { + case m.Find(path, RE("^/Data"+N+"$")): + n := m[0] + coff.Symbols[1+n].Value = offset - diroff // FIXME: is it ok? + sz := uint64(coff.Data[n].Size()) + binary.LittleEndian.PutUint64(coff.Symbols[0].Auxiliaries[0][0:8], binary.LittleEndian.Uint64(coff.Symbols[0].Auxiliaries[0][0:8])+sz) + case path == "/StringsHeader": + stringsoff = offset + case m.Find(path, RE("^/Strings"+N+"$")): + binary.LittleEndian.PutUint32(coff.Symbols[m[0]+1].Name[4:8], offset-stringsoff) + } + + return freezeCommon2(v, &offset) + }) + coff.SectionHeader32.PointerToRelocations = 0 +} + +func (coff *Coff) freezeRSRC() { + leafwalker := make(chan *DirEntry) + go func() { + for _, dir1 := range coff.Dir.Dirs { // resource type + for _, dir2 := range dir1.Dirs { // resource ID + for i := range dir2.DirEntries { // resource lang + leafwalker <- &dir2.DirEntries[i] + } + } + } + }() + + var offset, diroff uint32 + binutil.Walk(coff, func(v reflect.Value, path string) error { + diroff = coff.freezeCommon1(path, offset, diroff) + + RE := regexp.MustCompile + const N = `\[(\d+)\]` + m := matcher{} + switch { + case m.Find(path, RE("^/Dir/Dirs"+N+"$")): + coff.Dir.DirEntries[m[0]].OffsetToData = MASK_SUBDIRECTORY | (offset - diroff) + case m.Find(path, RE("^/Dir/Dirs"+N+"/Dirs"+N+"$")): + coff.Dir.Dirs[m[0]].DirEntries[m[1]].OffsetToData = MASK_SUBDIRECTORY | (offset - diroff) + case m.Find(path, RE("^/DataEntries"+N+"$")): + direntry := <-leafwalker + direntry.OffsetToData = offset - diroff + case m.Find(path, RE("^/DataEntries"+N+"/OffsetToData$")): + coff.Relocations[m[0]].RVA = offset - diroff + case m.Find(path, RE("^/Data"+N+"$")): + coff.DataEntries[m[0]].OffsetToData = offset - diroff + } + + return freezeCommon2(v, &offset) + }) +} + +func mustAtoi(s string) int { + i, err := strconv.Atoi(s) + if err != nil { + panic(err) + } + return i +} + +type matcher []int + +func (m *matcher) Find(s string, re *regexp.Regexp) bool { + subs := re.FindStringSubmatch(s) + if subs == nil { + return false + } + + *m = (*m)[:0] + for i := 1; i < len(subs); i++ { + *m = append(*m, mustAtoi(subs[i])) + } + return true +} diff --git a/src/github.com/akavel/rsrc/ico/ico.go b/src/github.com/akavel/rsrc/ico/ico.go new file mode 100644 index 000000000..0d419b127 --- /dev/null +++ b/src/github.com/akavel/rsrc/ico/ico.go @@ -0,0 +1,214 @@ +// Package ico describes Windows ICO file format. +package ico + +// ICO: http://msdn.microsoft.com/en-us/library/ms997538.aspx +// BMP/DIB: http://msdn.microsoft.com/en-us/library/windows/desktop/dd183562%28v=vs.85%29.aspx + +import ( + "bytes" + "encoding/binary" + "fmt" + "image" + "image/color" + "io" + "io/ioutil" + "sort" +) + +const ( + BI_RGB = 0 +) + +type ICONDIR struct { + Reserved uint16 // must be 0 + Type uint16 // Resource Type (1 for icons) + Count uint16 // How many images? +} + +type IconDirEntryCommon struct { + Width byte // Width, in pixels, of the image + Height byte // Height, in pixels, of the image + ColorCount byte // Number of colors in image (0 if >=8bpp) + Reserved byte // Reserved (must be 0) + Planes uint16 // Color Planes + BitCount uint16 // Bits per pixel + BytesInRes uint32 // How many bytes in this resource? +} + +type ICONDIRENTRY struct { + IconDirEntryCommon + ImageOffset uint32 // Where in the file is this image? [from beginning of file] +} + +type BITMAPINFOHEADER struct { + Size uint32 + Width int32 + Height int32 // NOTE: "represents the combined height of the XOR and AND masks. Remember to divide this number by two before using it to perform calculations for either of the XOR or AND masks." + Planes uint16 // [BMP/DIB]: "is always 1" + BitCount uint16 + Compression uint32 // for ico = 0 + SizeImage uint32 + XPelsPerMeter int32 // for ico = 0 + YPelsPerMeter int32 // for ico = 0 + ClrUsed uint32 // for ico = 0 + ClrImportant uint32 // for ico = 0 +} + +type RGBQUAD struct { + Blue byte + Green byte + Red byte + Reserved byte // must be 0 +} + +func skip(r io.Reader, n int64) error { + _, err := io.CopyN(ioutil.Discard, r, n) + return err +} + +type icoOffset struct { + n int + offset uint32 +} + +type rawico struct { + icoinfo ICONDIRENTRY + bmpinfo *BITMAPINFOHEADER + idx int + data []byte +} + +type byOffsets []rawico + +func (o byOffsets) Len() int { return len(o) } +func (o byOffsets) Less(i, j int) bool { return o[i].icoinfo.ImageOffset < o[j].icoinfo.ImageOffset } +func (o byOffsets) Swap(i, j int) { + tmp := o[i] + o[i] = o[j] + o[j] = tmp +} + +type ICO struct { + image.Image +} + +func DecodeHeaders(r io.Reader) ([]ICONDIRENTRY, error) { + var hdr ICONDIR + err := binary.Read(r, binary.LittleEndian, &hdr) + if err != nil { + return nil, err + } + if hdr.Reserved != 0 || hdr.Type != 1 { + return nil, fmt.Errorf("bad magic number") + } + + entries := make([]ICONDIRENTRY, hdr.Count) + for i := 0; i < len(entries); i++ { + err = binary.Read(r, binary.LittleEndian, &entries[i]) + if err != nil { + return nil, err + } + } + return entries, nil +} + +// NOTE: won't succeed on files with overlapping offsets +func unused_decodeAll(r io.Reader) ([]*ICO, error) { + var hdr ICONDIR + err := binary.Read(r, binary.LittleEndian, &hdr) + if err != nil { + return nil, err + } + if hdr.Reserved != 0 || hdr.Type != 1 { + return nil, fmt.Errorf("bad magic number") + } + + raws := make([]rawico, hdr.Count) + for i := 0; i < len(raws); i++ { + err = binary.Read(r, binary.LittleEndian, &raws[i].icoinfo) + if err != nil { + return nil, err + } + raws[i].idx = i + } + + sort.Sort(byOffsets(raws)) + + offset := uint32(binary.Size(&hdr) + len(raws)*binary.Size(ICONDIRENTRY{})) + for i := 0; i < len(raws); i++ { + err = skip(r, int64(raws[i].icoinfo.ImageOffset-offset)) + if err != nil { + return nil, err + } + offset = raws[i].icoinfo.ImageOffset + + raws[i].bmpinfo = &BITMAPINFOHEADER{} + err = binary.Read(r, binary.LittleEndian, raws[i].bmpinfo) + if err != nil { + return nil, err + } + + err = skip(r, int64(raws[i].bmpinfo.Size-uint32(binary.Size(BITMAPINFOHEADER{})))) + if err != nil { + return nil, err + } + raws[i].data = make([]byte, raws[i].icoinfo.BytesInRes-raws[i].bmpinfo.Size) + _, err = io.ReadFull(r, raws[i].data) + if err != nil { + return nil, err + } + } + + icos := make([]*ICO, len(raws)) + for i := 0; i < len(raws); i++ { + fmt.Println(i) + icos[raws[i].idx], err = decode(raws[i].bmpinfo, &raws[i].icoinfo, raws[i].data) + if err != nil { + return nil, err + } + } + return icos, nil +} + +func decode(info *BITMAPINFOHEADER, icoinfo *ICONDIRENTRY, data []byte) (*ICO, error) { + if info.Compression != BI_RGB { + return nil, fmt.Errorf("ICO compression not supported (got %d)", info.Compression) + } + + //if info.ClrUsed!=0 { + // panic(info.ClrUsed) + //} + + r := bytes.NewBuffer(data) + + bottomup := info.Height > 0 + if !bottomup { + info.Height = -info.Height + } + + switch info.BitCount { + case 8: + ncol := int(icoinfo.ColorCount) + if ncol == 0 { + ncol = 256 + } + + pal := make(color.Palette, ncol) + for i := 0; i < ncol; i++ { + var rgb RGBQUAD + err := binary.Read(r, binary.LittleEndian, &rgb) + if err != nil { + return nil, err + } + pal[i] = color.NRGBA{R: rgb.Red, G: rgb.Green, B: rgb.Blue, A: 0xff} //FIXME: is Alpha ok 0xff? + } + fmt.Println(pal) + + fmt.Println(info.SizeImage, len(data)-binary.Size(RGBQUAD{})*len(pal), info.Width, info.Height) + + default: + return nil, fmt.Errorf("unsupported ICO bit depth (BitCount) %d", info.BitCount) + } + + return nil, nil +} diff --git a/src/github.com/akavel/rsrc/rsrc.go b/src/github.com/akavel/rsrc/rsrc.go new file mode 100644 index 000000000..7c2eb0ad8 --- /dev/null +++ b/src/github.com/akavel/rsrc/rsrc.go @@ -0,0 +1,223 @@ +package main + +import ( + "encoding/binary" + "flag" + "fmt" + "io" + "os" + "reflect" + "regexp" + "strings" + + "github.com/akavel/rsrc/binutil" + "github.com/akavel/rsrc/coff" + "github.com/akavel/rsrc/ico" +) + +const ( + RT_ICON = coff.RT_ICON + RT_GROUP_ICON = coff.RT_GROUP_ICON + RT_MANIFEST = coff.RT_MANIFEST +) + +// on storing icons, see: http://blogs.msdn.com/b/oldnewthing/archive/2012/07/20/10331787.aspx +type GRPICONDIR struct { + ico.ICONDIR + Entries []GRPICONDIRENTRY +} + +func (group GRPICONDIR) Size() int64 { + return int64(binary.Size(group.ICONDIR) + len(group.Entries)*binary.Size(group.Entries[0])) +} + +type GRPICONDIRENTRY struct { + ico.IconDirEntryCommon + Id uint16 +} + +var usage = `USAGE: + +%s [-manifest FILE.exe.manifest] [-ico FILE.ico[,FILE2.ico...]] -o FILE.syso + Generates a .syso file with specified resources embedded in .rsrc section, + aimed for consumption by Go linker when building Win32 excecutables. + +%s -data FILE.dat -o FILE.syso > FILE.c + Generates a .syso file with specified opaque binary blob embedded, + together with related .c file making it possible to access from Go code. + Theoretically cross-platform, but reportedly cannot compile together with cgo. + +The generated *.syso and *.c files should get automatically recognized +by 'go build' command and linked into an executable/library, as long as +there are any *.go files in the same directory. + +OPTIONS: +` + +func main() { + //TODO: allow in options advanced specification of multiple resources, as a tree (json?) + //FIXME: verify that data file size doesn't exceed uint32 max value + var fnamein, fnameico, fnamedata, fnameout string + flags := flag.NewFlagSet("", flag.ContinueOnError) + flags.StringVar(&fnamein, "manifest", "", "path to a Windows manifest file to embed") + flags.StringVar(&fnameico, "ico", "", "comma-separated list of paths to .ico files to embed") + flags.StringVar(&fnamedata, "data", "", "path to raw data file to embed") + flags.StringVar(&fnameout, "o", "rsrc.syso", "name of output COFF (.res or .syso) file") + _ = flags.Parse(os.Args[1:]) + if fnameout == "" || (fnamein == "" && fnamedata == "" && fnameico == "") { + fmt.Fprintf(os.Stderr, usage, os.Args[0], os.Args[0]) + flags.PrintDefaults() + os.Exit(1) + } + + var err error + switch { + case fnamein != "" || fnameico != "": + err = run(fnamein, fnameico, fnameout) + case fnamedata != "": + err = rundata(fnamedata, fnameout) + } + if err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } +} + +func rundata(fnamedata, fnameout string) error { + if !strings.HasSuffix(fnameout, ".syso") { + return fmt.Errorf("Output file name '%s' must end with '.syso'", fnameout) + } + symname := strings.TrimSuffix(fnameout, ".syso") + ok, err := regexp.MatchString(`^[a-z0-9_]+$`, symname) + if err != nil { + return fmt.Errorf("Internal error: %s", err) + } + if !ok { + return fmt.Errorf("Output file name '%s' must be composed of only lowercase letters (a-z), digits (0-9) and underscore (_)", fnameout) + } + + dat, err := binutil.SizedOpen(fnamedata) + if err != nil { + return fmt.Errorf("Error opening data file '%s': %s", fnamedata, err) + } + defer dat.Close() + + coff := coff.NewRDATA() + coff.AddData("_brsrc_"+symname, dat) + coff.AddData("_ersrc_"+symname, io.NewSectionReader(strings.NewReader("\000\000"), 0, 2)) // TODO: why? copied from as-generated + coff.Freeze() + err = write(coff, fnameout) + if err != nil { + return err + } + + //FIXME: output a .c file + fmt.Println(strings.Replace(`#include "runtime.h" +extern byte _brsrc_NAME[], _ersrc_NAME; + +/* func get_NAME() []byte */ +void ·get_NAME(Slice a) { + a.array = _brsrc_NAME; + a.len = a.cap = &_ersrc_NAME - _brsrc_NAME; + FLUSH(&a); +}`, "NAME", symname, -1)) + + return nil +} + +func run(fnamein, fnameico, fnameout string) error { + newid := make(chan uint16) + go func() { + for i := uint16(1); ; i++ { + newid <- i + } + }() + + coff := coff.NewRSRC() + + if fnamein != "" { + manifest, err := binutil.SizedOpen(fnamein) + if err != nil { + return fmt.Errorf("Error opening manifest file '%s': %s", fnamein, err) + } + defer manifest.Close() + + id := <-newid + coff.AddResource(RT_MANIFEST, id, manifest) + fmt.Println("Manifest ID: ", id) + } + if fnameico != "" { + for _, fnameicosingle := range strings.Split(fnameico, ",") { + err := addicon(coff, fnameicosingle, newid) + if err != nil { + return err + } + } + } + + coff.Freeze() + + return write(coff, fnameout) +} + +func addicon(coff *coff.Coff, fname string, newid <-chan uint16) error { + f, err := os.Open(fname) + if err != nil { + return err + } + //defer f.Close() don't defer, files will be closed by OS when app closes + + icons, err := ico.DecodeHeaders(f) + if err != nil { + return err + } + + if len(icons) > 0 { + // RT_ICONs + group := GRPICONDIR{ICONDIR: ico.ICONDIR{ + Reserved: 0, // magic num. + Type: 1, // magic num. + Count: uint16(len(icons)), + }} + for _, icon := range icons { + id := <-newid + r := io.NewSectionReader(f, int64(icon.ImageOffset), int64(icon.BytesInRes)) + coff.AddResource(RT_ICON, id, r) + group.Entries = append(group.Entries, GRPICONDIRENTRY{icon.IconDirEntryCommon, id}) + } + id := <-newid + coff.AddResource(RT_GROUP_ICON, id, group) + fmt.Println("Icon ", fname, " ID: ", id) + } + + return nil +} + +func write(coff *coff.Coff, fnameout string) error { + out, err := os.Create(fnameout) + if err != nil { + return err + } + defer out.Close() + w := binutil.Writer{W: out} + + // write the resulting file to disk + binutil.Walk(coff, func(v reflect.Value, path string) error { + if binutil.Plain(v.Kind()) { + w.WriteLE(v.Interface()) + return nil + } + vv, ok := v.Interface().(binutil.SizedReader) + if ok { + w.WriteFromSized(vv) + return binutil.WALK_SKIP + } + return nil + }) + + if w.Err != nil { + return fmt.Errorf("Error writing output file: %s", w.Err) + } + + return nil +}