diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..df80420 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +vmdk +vmdk.exe \ No newline at end of file diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..269653d --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2018 velocidex + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..ce5036a --- /dev/null +++ b/Makefile @@ -0,0 +1,5 @@ +all: + go build -o ./vmdk ./bin + +generate: + cd parser/ && binparsegen conversion.spec.yaml > vmdk_gen.go diff --git a/README.md b/README.md new file mode 100644 index 0000000..ddac727 --- /dev/null +++ b/README.md @@ -0,0 +1,5 @@ +# Go reader for vmdk files. + +Currently supported: + +* Multi-Extent SPARSE files (as used by vmplayer) diff --git a/bin/dump.go b/bin/dump.go new file mode 100644 index 0000000..4961d0f --- /dev/null +++ b/bin/dump.go @@ -0,0 +1,11 @@ +package main + +import ( + "encoding/json" + "fmt" +) + +func Dump(v interface{}) { + serialized, _ := json.MarshalIndent(v, " ", " ") + fmt.Printf(string(serialized)) +} diff --git a/bin/main.go b/bin/main.go new file mode 100644 index 0000000..1ec68cd --- /dev/null +++ b/bin/main.go @@ -0,0 +1,36 @@ +package main + +import ( + "io" + "os" + + kingpin "github.com/alecthomas/kingpin/v2" +) + +type CommandHandler func(command string) bool + +var ( + app = kingpin.New("govmdk", + "A tool for inspecting vmdk volumes.") + + verbose_flag = app.Flag( + "verbose", "Show verbose information").Bool() + + command_handlers []CommandHandler +) + +func main() { + app.HelpFlag.Short('h') + app.UsageTemplate(kingpin.CompactUsageTemplate) + command := kingpin.MustParse(app.Parse(os.Args[1:])) + + for _, command_handler := range command_handlers { + if command_handler(command) { + break + } + } +} + +func getReader(reader io.ReaderAt) io.ReaderAt { + return reader +} diff --git a/bin/stat.go b/bin/stat.go new file mode 100644 index 0000000..5bb1c10 --- /dev/null +++ b/bin/stat.go @@ -0,0 +1,60 @@ +package main + +import ( + "io" + "os" + "path/filepath" + + "github.com/Velocidex/go-vmdk/parser" + kingpin "github.com/alecthomas/kingpin/v2" + ntfs_parser "www.velocidex.com/golang/go-ntfs/parser" +) + +var ( + info_command = app.Command( + "info", "Stat a vmdk file.") + + info_command_file_arg = info_command.Arg( + "file", "The image file to inspect", + ).Required().String() +) + +func doInfo() { + fd, err := os.Open(*info_command_file_arg) + kingpin.FatalIfError(err, "Can not open filesystem") + + reader, _ := ntfs_parser.NewPagedReader( + getReader(fd), 1024, 10000) + + st, err := fd.Stat() + kingpin.FatalIfError(err, "Can not open filesystem") + + vmdk, err := parser.GetVMDKContext(reader, int(st.Size()), + func(filename string) (reader io.ReaderAt, err error) { + full_path := filepath.Join( + filepath.Dir(*info_command_file_arg), filename) + fd, err := os.Open(full_path) + if err != nil { + return nil, err + } + + reader, err := ntfs_parser.NewPagedReader( + getReader(fd), 1024, 10000) + return reader, func() { fd.Close() }, nil + }) + kingpin.FatalIfError(err, "Can not open filesystem") + + vmdk.Debug() +} + +func init() { + command_handlers = append(command_handlers, func(command string) bool { + switch command { + case info_command.FullCommand(): + doInfo() + default: + return false + } + return true + }) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..4f079f3 --- /dev/null +++ b/go.mod @@ -0,0 +1,15 @@ +module github.com/Velocidex/go-vmdk + +go 1.22.2 + +require ( + github.com/Velocidex/go-fat v0.0.0-20230923165230-3e6c4265297a + github.com/alecthomas/kingpin/v2 v2.4.0 + www.velocidex.com/golang/go-ntfs v0.2.0 +) + +require ( + github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/xhit/go-str2duration/v2 v2.1.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..80ff18c --- /dev/null +++ b/go.sum @@ -0,0 +1,37 @@ +github.com/Velocidex/go-fat v0.0.0-20230923165230-3e6c4265297a h1:dWHPlB3C86vh+M5P14dZxF6Hh8o2/u8FTRF/bs2EM+Q= +github.com/Velocidex/go-fat v0.0.0-20230923165230-3e6c4265297a/go.mod h1:g74FCv59tsVP48V2o1eyIK8aKbNKPLJIJ+HuiUPVc6E= +github.com/alecthomas/assert v1.0.0 h1:3XmGh/PSuLzDbK3W2gUbRXwgW5lqPkuqvRgeQ30FI5o= +github.com/alecthomas/assert v1.0.0/go.mod h1:va/d2JC+M7F6s+80kl/R3G7FUiW6JzUO+hPhLyJ36ZY= +github.com/alecthomas/colour v0.1.0 h1:nOE9rJm6dsZ66RGWYSFrXw461ZIt9A6+nHgL7FRrDUk= +github.com/alecthomas/colour v0.1.0/go.mod h1:QO9JBoKquHd+jz9nshCh40fOfO+JzsoXy8qTHF68zU0= +github.com/alecthomas/kingpin/v2 v2.4.0 h1:f48lwail6p8zpO1bC4TxtqACaGqHYA22qkHjHpqDjYY= +github.com/alecthomas/kingpin/v2 v2.4.0/go.mod h1:0gyi0zQnjuFk8xrkNKamJoyUo382HRL7ATRpFZCw6tE= +github.com/alecthomas/repr v0.1.1 h1:87P60cSmareLAxMc4Hro0r2RBY4ROm0dYwkJNpS4pPs= +github.com/alecthomas/repr v0.1.1/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= +github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 h1:s6gZFSlWYmbqAuRjVTiNNhvNRfY2Wxp9nhfyel4rklc= +github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/sebdah/goldie v1.0.0 h1:9GNhIat69MSlz/ndaBg48vl9dF5fI+NBB6kfOxgfkMc= +github.com/sebdah/goldie v1.0.0/go.mod h1:jXP4hmWywNEwZzhMuv2ccnqTSFpuq8iyQhtQdkkZBH4= +github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ= +github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/xhit/go-str2duration/v2 v2.1.0 h1:lxklc02Drh6ynqX+DdPyp5pCKLUQpRT8bp8Ydu2Bstc= +github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU= +golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +www.velocidex.com/golang/go-ntfs v0.2.0 h1:JLS4hOQLupiVzo+1z4Xb8AZyIaXHDmiGnKyoM/bRYq0= +www.velocidex.com/golang/go-ntfs v0.2.0/go.mod h1:itvbHQcnLdTVIDY6fI3lR0zeBwXwBYBdUFtswE0x1vc= diff --git a/parser/context.go b/parser/context.go new file mode 100644 index 0000000..3ca49e8 --- /dev/null +++ b/parser/context.go @@ -0,0 +1,165 @@ +package parser + +import ( + "errors" + "fmt" + "io" + "regexp" + "slices" + "strings" +) + +const ( + SPARSE_MAGICNUMBER = 0x564d444b + SECTOR_SIZE = 512 +) + +var ( + StartExtentRegex = regexp.MustCompile("^# Extent description") + ExtentRegex = regexp.MustCompile(`(RW|R) (\d+) ([A-Z]+) "([^"]+)"`) +) + +type VMDKContext struct { + profile *VMDKProfile + reader io.ReaderAt + + extents []*SparseExtent + + total_size int64 +} + +func (self *VMDKContext) Size() int64 { + return self.total_size +} + +func (self *VMDKContext) Debug() { + for _, i := range self.extents { + i.Debug() + } +} + +func (self *VMDKContext) Close() { + for _, i := range self.extents { + i.Close() + } +} + +func (self *VMDKContext) getGrainForOffset(offset int64) ( + reader io.ReaderAt, start, length int64, err error) { + + n, _ := slices.BinarySearchFunc(self.extents, + offset, func(item *SparseExtent, offset int64) int { + if offset < item.offset { + return 1 + } else if offset == item.offset { + return 0 + } + return -1 + }) + + if n < 1 || n > len(self.extents) { + return nil, 0, 0, io.EOF + } + + extent := self.extents[n-1] + if extent.offset > offset || extent.offset+extent.total_size < offset { + return nil, 0, 0, io.EOF + } + + start, length, err = extent.getGrainForOffset(offset - extent.offset) + return extent.reader, start, length, err +} + +func (self *VMDKContext) ReadAt(buf []byte, offset int64) (int, error) { + i := int64(0) + buf_len := int64(len(buf)) + + for i < buf_len { + reader, start, available_length, err := self.getGrainForOffset(offset) + if err != nil { + return 0, err + } + + to_read := buf_len - i + if to_read > available_length { + to_read = available_length + } + n, err := reader.ReadAt(buf[i:i+to_read], start) + if err != nil && err != io.EOF { + return int(i), err + } + + i += int64(n) + } + + return int(i), nil +} + +func GetVMDKContext( + reader io.ReaderAt, size int, + opener func(filename string) ( + reader io.ReaderAt, closer func(), err error), +) (*VMDKContext, error) { + profile := NewVMDKProfile() + res := &VMDKContext{ + profile: profile, + reader: reader, + } + + if size > 64*1024 { + size = 64 * 1024 + } + + buf := make([]byte, size) + n, err := reader.ReadAt(buf, 0) + if err != nil && err != io.EOF { + return nil, err + } + + state := "" + for _, line := range strings.Split(string(buf[:n]), "\n") { + if StartExtentRegex.MatchString(line) { + state = "Extents" + continue + } + + if state == "Extents" { + match := ExtentRegex.FindStringSubmatch(line) + if len(match) > 0 { + extent_type := match[3] + extent_filename := match[4] + + // Try to open the extent file. + reader, closer, err := opener(extent_filename) + if err != nil { + return nil, err + } + + switch extent_type { + case "SPARSE": + extent, err := GetSpaseExtent(reader) + if err != nil { + return nil, fmt.Errorf("While opening %v: %w", + extent_filename, err) + } + + extent.offset = res.total_size + extent.closer = closer + extent.filename = extent_filename + + res.total_size += extent.total_size + + res.extents = append(res.extents, extent) + + default: + return nil, errors.New("Unsupported extent type " + extent_type) + } + + } else { + state = "" + } + } + } + + return res, nil +} diff --git a/parser/conversion.spec.yaml b/parser/conversion.spec.yaml new file mode 100644 index 0000000..ccab217 --- /dev/null +++ b/parser/conversion.spec.yaml @@ -0,0 +1,7 @@ +Module: parser +Profile: VMDKProfile +Filename: vmdk_profile.json +GenerateDebugString: true +Structs: + - SparseExtentHeader + - Misc diff --git a/parser/sparse.go b/parser/sparse.go new file mode 100644 index 0000000..ce6d0d0 --- /dev/null +++ b/parser/sparse.go @@ -0,0 +1,90 @@ +package parser + +import ( + "errors" + "fmt" + "io" +) + +type SparseExtent struct { + profile *VMDKProfile + reader io.ReaderAt + + header *SparseExtentHeader + + // Size of grains in bytes + grain_size int64 + + // Coverage of each grain table in bytes + grain_table_coverage int64 + + gde_offset int64 + + total_size int64 + + // The offset in the logical image where this extent sits. + offset int64 + filename string + + closer func() +} + +func (self *SparseExtent) Close() { + self.closer() +} + +func (self *SparseExtent) Debug() { + fmt.Println(self.header.DebugString()) +} + +func (self *SparseExtent) getGrainForOffset(offset int64) ( + start, length int64, err error) { + + grain_table_number := offset / self.grain_table_coverage + grain_directory_entry := ParseUint32( + self.reader, self.gde_offset+4*grain_table_number) + if grain_directory_entry == 0 { + return 0, 0, io.EOF + } + + grain_entry_number := (offset % self.grain_table_coverage) / self.grain_size + grain_table_entry := ParseUint32(self.reader, + int64(grain_directory_entry*SECTOR_SIZE)+4*grain_entry_number) + + grain_start := int64(grain_table_entry) * SECTOR_SIZE + offset_within_grain := offset % self.grain_size + + return grain_start + offset_within_grain, self.grain_size - offset_within_grain, nil +} + +func GetSpaseExtent(reader io.ReaderAt) (*SparseExtent, error) { + profile := NewVMDKProfile() + res := &SparseExtent{ + profile: profile, + reader: reader, + header: profile.SparseExtentHeader(reader, 0), + } + + if res.header.magicNumber() != SPARSE_MAGICNUMBER { + return nil, errors.New("Invalid magic") + } + + if res.header.version() != 1 { + return nil, errors.New("Unsupported version") + } + + if res.header.grainSize() < 8 { + return nil, errors.New("Grain size invalid") + } + + if res.header.numGTEsPerGT() != 512 { + return nil, errors.New("numGTEsPerGT must be 512") + } + + res.grain_size = int64(res.header.grainSize() * SECTOR_SIZE) + res.grain_table_coverage = int64(res.header.numGTEsPerGT()) * res.grain_size + res.gde_offset = int64(res.header.gdOffset() * SECTOR_SIZE) + res.total_size = int64(res.header.capacity() * SECTOR_SIZE) + + return res, nil +} diff --git a/parser/stats.go b/parser/stats.go new file mode 100644 index 0000000..2d188c4 --- /dev/null +++ b/parser/stats.go @@ -0,0 +1,30 @@ +package parser + +type ExtentStat struct { + Type string `json:"type"` + VirtualOffset int64 `json:"VirtualOffset"` + Size int64 `json:"Size"` + Filename string `json:"Filename"` +} + +type VMDKStats struct { + TotalSize int64 `json:"TotalSize"` + Extents []ExtentStat `json:"Extents"` +} + +func (self *VMDKContext) Stats() VMDKStats { + res := VMDKStats{ + TotalSize: self.total_size, + } + + for _, e := range self.extents { + res.Extents = append(res.Extents, ExtentStat{ + Type: "SPARSE", + VirtualOffset: e.offset, + Size: e.total_size, + Filename: e.filename, + }) + } + + return res +} diff --git a/parser/vmdk_gen.go b/parser/vmdk_gen.go new file mode 100644 index 0000000..85596ca --- /dev/null +++ b/parser/vmdk_gen.go @@ -0,0 +1,202 @@ + +package parser + +// Autogenerated code from vmdk_profile.json. Do not edit. + +import ( + "encoding/binary" + "fmt" + "bytes" + "io" + "sort" + "strings" + "unicode/utf16" + "unicode/utf8" +) + +var ( + // Depending on autogenerated code we may use this. Add a reference + // to shut the compiler up. + _ = bytes.MinRead + _ = fmt.Sprintf + _ = utf16.Decode + _ = binary.LittleEndian + _ = utf8.RuneError + _ = sort.Strings + _ = strings.Join + _ = io.Copy +) + +func indent(text string) string { + result := []string{} + lines := strings.Split(text,"\n") + for _, line := range lines { + result = append(result, " " + line) + } + return strings.Join(result, "\n") +} + + +type VMDKProfile struct { + Off_Misc_a int64 + Off_SparseExtentHeader_magicNumber int64 + Off_SparseExtentHeader_version int64 + Off_SparseExtentHeader_flags int64 + Off_SparseExtentHeader_capacity int64 + Off_SparseExtentHeader_grainSize int64 + Off_SparseExtentHeader_descriptorOffset int64 + Off_SparseExtentHeader_descriptorSize int64 + Off_SparseExtentHeader_numGTEsPerGT int64 + Off_SparseExtentHeader_rgdOffset int64 + Off_SparseExtentHeader_gdOffset int64 + Off_SparseExtentHeader_overHead int64 + Off_SparseExtentHeader_compressAlgorithm int64 +} + +func NewVMDKProfile() *VMDKProfile { + // Specific offsets can be tweaked to cater for slight version mismatches. + self := &VMDKProfile{0,0,4,8,12,20,28,36,44,48,56,64,77} + return self +} + +func (self *VMDKProfile) Misc(reader io.ReaderAt, offset int64) *Misc { + return &Misc{Reader: reader, Offset: offset, Profile: self} +} + +func (self *VMDKProfile) SparseExtentHeader(reader io.ReaderAt, offset int64) *SparseExtentHeader { + return &SparseExtentHeader{Reader: reader, Offset: offset, Profile: self} +} + + +type Misc struct { + Reader io.ReaderAt + Offset int64 + Profile *VMDKProfile +} + +func (self *Misc) Size() int { + return 0 +} + +func (self *Misc) a() []uint32 { + return ParseArray_uint32(self.Profile, self.Reader, self.Profile.Off_Misc_a + self.Offset, 0) +} +func (self *Misc) DebugString() string { + result := fmt.Sprintf("struct Misc @ %#x:\n", self.Offset) + return result +} + +type SparseExtentHeader struct { + Reader io.ReaderAt + Offset int64 + Profile *VMDKProfile +} + +func (self *SparseExtentHeader) Size() int { + return 0 +} + +func (self *SparseExtentHeader) magicNumber() uint32 { + return ParseUint32(self.Reader, self.Profile.Off_SparseExtentHeader_magicNumber + self.Offset) +} + +func (self *SparseExtentHeader) version() uint32 { + return ParseUint32(self.Reader, self.Profile.Off_SparseExtentHeader_version + self.Offset) +} + +func (self *SparseExtentHeader) flags() uint32 { + return ParseUint32(self.Reader, self.Profile.Off_SparseExtentHeader_flags + self.Offset) +} + +func (self *SparseExtentHeader) capacity() uint64 { + return ParseUint64(self.Reader, self.Profile.Off_SparseExtentHeader_capacity + self.Offset) +} + +func (self *SparseExtentHeader) grainSize() uint64 { + return ParseUint64(self.Reader, self.Profile.Off_SparseExtentHeader_grainSize + self.Offset) +} + +func (self *SparseExtentHeader) descriptorOffset() uint64 { + return ParseUint64(self.Reader, self.Profile.Off_SparseExtentHeader_descriptorOffset + self.Offset) +} + +func (self *SparseExtentHeader) descriptorSize() uint64 { + return ParseUint64(self.Reader, self.Profile.Off_SparseExtentHeader_descriptorSize + self.Offset) +} + +func (self *SparseExtentHeader) numGTEsPerGT() uint32 { + return ParseUint32(self.Reader, self.Profile.Off_SparseExtentHeader_numGTEsPerGT + self.Offset) +} + +func (self *SparseExtentHeader) rgdOffset() uint64 { + return ParseUint64(self.Reader, self.Profile.Off_SparseExtentHeader_rgdOffset + self.Offset) +} + +func (self *SparseExtentHeader) gdOffset() uint64 { + return ParseUint64(self.Reader, self.Profile.Off_SparseExtentHeader_gdOffset + self.Offset) +} + +func (self *SparseExtentHeader) overHead() uint64 { + return ParseUint64(self.Reader, self.Profile.Off_SparseExtentHeader_overHead + self.Offset) +} + +func (self *SparseExtentHeader) compressAlgorithm() uint16 { + return ParseUint16(self.Reader, self.Profile.Off_SparseExtentHeader_compressAlgorithm + self.Offset) +} +func (self *SparseExtentHeader) DebugString() string { + result := fmt.Sprintf("struct SparseExtentHeader @ %#x:\n", self.Offset) + result += fmt.Sprintf(" magicNumber: %#0x\n", self.magicNumber()) + result += fmt.Sprintf(" version: %#0x\n", self.version()) + result += fmt.Sprintf(" flags: %#0x\n", self.flags()) + result += fmt.Sprintf(" capacity: %#0x\n", self.capacity()) + result += fmt.Sprintf(" grainSize: %#0x\n", self.grainSize()) + result += fmt.Sprintf(" descriptorOffset: %#0x\n", self.descriptorOffset()) + result += fmt.Sprintf(" descriptorSize: %#0x\n", self.descriptorSize()) + result += fmt.Sprintf(" numGTEsPerGT: %#0x\n", self.numGTEsPerGT()) + result += fmt.Sprintf(" rgdOffset: %#0x\n", self.rgdOffset()) + result += fmt.Sprintf(" gdOffset: %#0x\n", self.gdOffset()) + result += fmt.Sprintf(" overHead: %#0x\n", self.overHead()) + result += fmt.Sprintf(" compressAlgorithm: %#0x\n", self.compressAlgorithm()) + return result +} + +func ParseArray_uint32(profile *VMDKProfile, reader io.ReaderAt, offset int64, count int) []uint32 { + result := make([]uint32, 0, count) + for i:=0; i