mirror of
https://github.com/yunionio/cloudpods.git
synced 2026-09-21 06:09:39 +08:00
fix(glance): support prob iso image (#24136)
This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
// Copyright 2019 Yunion
|
||||
//
|
||||
// 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.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"yunion.io/x/pkg/util/shellutils"
|
||||
"yunion.io/x/structarg"
|
||||
|
||||
_ "yunion.io/x/onecloud/cmd/isocli/shell"
|
||||
)
|
||||
|
||||
type BaseOptions struct {
|
||||
Debug bool `help:"debug mode"`
|
||||
SUBCOMMAND string `help:"aliyuncli subcommand" subcommand:"true"`
|
||||
}
|
||||
|
||||
func getSubcommandParser() (*structarg.ArgumentParser, error) {
|
||||
parse, e := structarg.NewArgumentParserWithHelp(&BaseOptions{},
|
||||
"isocli",
|
||||
"Command-line ISO tools.",
|
||||
`See "isocli COMMAND --help" for help on a specific command.`)
|
||||
|
||||
if e != nil {
|
||||
return nil, e
|
||||
}
|
||||
|
||||
subcmd := parse.GetSubcommand()
|
||||
if subcmd == nil {
|
||||
return nil, fmt.Errorf("No subcommand argument.")
|
||||
}
|
||||
for _, v := range shellutils.CommandTable {
|
||||
_, e := subcmd.AddSubParserWithHelp(v.Options, v.Command, v.Desc, v.Callback)
|
||||
if e != nil {
|
||||
return nil, e
|
||||
}
|
||||
}
|
||||
return parse, nil
|
||||
}
|
||||
|
||||
func showErrorAndExit(e error) {
|
||||
fmt.Fprintf(os.Stderr, "%s", e)
|
||||
fmt.Fprintln(os.Stderr)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
func main() {
|
||||
parser, e := getSubcommandParser()
|
||||
if e != nil {
|
||||
showErrorAndExit(e)
|
||||
}
|
||||
e = parser.ParseArgs(os.Args[1:], false)
|
||||
|
||||
if parser.IsHelpSet() {
|
||||
fmt.Print(parser.HelpString())
|
||||
return
|
||||
}
|
||||
subcmd := parser.GetSubcommand()
|
||||
subparser := subcmd.GetSubParser()
|
||||
if e != nil || subparser == nil {
|
||||
if subparser != nil {
|
||||
fmt.Print(subparser.Usage())
|
||||
} else {
|
||||
fmt.Print(parser.Usage())
|
||||
}
|
||||
showErrorAndExit(e)
|
||||
}
|
||||
suboptions := subparser.Options()
|
||||
e = subcmd.Invoke(suboptions)
|
||||
if e != nil {
|
||||
showErrorAndExit(e)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
// Copyright 2019 Yunion
|
||||
//
|
||||
// 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.
|
||||
|
||||
package shell
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"yunion.io/x/jsonutils"
|
||||
"yunion.io/x/pkg/util/shellutils"
|
||||
|
||||
"yunion.io/x/onecloud/pkg/util/isoutils"
|
||||
)
|
||||
|
||||
func init() {
|
||||
type DetectOSOptions struct {
|
||||
ISO string `help:"ISO file"`
|
||||
}
|
||||
shellutils.R(&DetectOSOptions{}, "detect", "Detect", func(args *DetectOSOptions) error {
|
||||
stat, err := os.Stat(args.ISO)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if stat.IsDir() {
|
||||
// 如果是目录,仅遍历一层目录下的 .iso 文件
|
||||
entries, err := os.ReadDir(args.ISO)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() {
|
||||
continue // 跳过子目录
|
||||
}
|
||||
if strings.HasSuffix(strings.ToLower(entry.Name()), ".iso") {
|
||||
path := filepath.Join(args.ISO, entry.Name())
|
||||
fmt.Printf("Processing: %s\n", path)
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
fmt.Printf("Error opening %s: %v\n", path, err)
|
||||
continue // 继续处理其他文件
|
||||
}
|
||||
isoInfo, err := isoutils.DetectOSFromISO(f)
|
||||
f.Close()
|
||||
if err != nil {
|
||||
fmt.Printf("Error detecting OS from %s: %v\n", path, err)
|
||||
continue
|
||||
}
|
||||
fmt.Printf("%s: %s\n", path, jsonutils.Marshal(isoInfo))
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// 如果是文件,按原逻辑处理
|
||||
f, err := os.Open(args.ISO)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
isoInfo, err := isoutils.DetectOSFromISO(f)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Printf("%s: %s\n", args.ISO, jsonutils.Marshal(isoInfo))
|
||||
return nil
|
||||
})
|
||||
}
|
||||
@@ -6,6 +6,7 @@ require (
|
||||
bazil.org/fuse v0.0.0-20180421153158-65cc252bf669
|
||||
github.com/360EntSecGroup-Skylar/excelize v1.4.0
|
||||
github.com/LeeEirc/terminalparser v0.0.0-20240205084113-fbf78c8480f2
|
||||
github.com/Microsoft/go-winio v0.6.2
|
||||
github.com/aliyun/alibaba-cloud-sdk-go v1.61.684
|
||||
github.com/anacrolix/torrent v0.0.0-20181129073333-cc531b8c4a80
|
||||
github.com/benbjohnson/clock v1.0.0
|
||||
@@ -34,6 +35,7 @@ require (
|
||||
github.com/influxdata/influxql v1.1.0
|
||||
github.com/influxdata/promql/v2 v2.12.0
|
||||
github.com/jaypipes/ghw v0.11.0
|
||||
github.com/kdomanski/iso9660 v0.4.0
|
||||
github.com/koding/websocketproxy v0.0.0-20181220232114-7ed82d81a28c
|
||||
github.com/lestrrat-go/jwx v1.0.2
|
||||
github.com/lestrrat/go-jwx v0.0.0-20180221005942-b7d4802280ae
|
||||
@@ -45,6 +47,7 @@ require (
|
||||
github.com/mholt/caddy v0.10.11
|
||||
github.com/miekg/dns v1.1.25
|
||||
github.com/minio/minio-go v6.0.14+incompatible
|
||||
github.com/mogaika/udf v0.0.0-20171019171931-167f0ab01c73
|
||||
github.com/petermattis/goid v0.0.0-20250508124226-395b08cebbdb
|
||||
github.com/pierrec/lz4/v4 v4.1.15
|
||||
github.com/pkg/errors v0.9.1
|
||||
@@ -82,6 +85,7 @@ require (
|
||||
google.golang.org/grpc v1.62.0
|
||||
google.golang.org/protobuf v1.32.0
|
||||
gopkg.in/fatih/set.v0 v0.2.1
|
||||
gopkg.in/ini.v1 v1.62.0
|
||||
gopkg.in/mail.v2 v2.3.1
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
k8s.io/api v0.19.3
|
||||
@@ -94,7 +98,7 @@ require (
|
||||
yunion.io/x/jsonutils v1.0.1-0.20250507052344-1abcf4f443b1
|
||||
yunion.io/x/log v1.0.1-0.20240305175729-7cf2d6cd5a91
|
||||
yunion.io/x/ovsdb v0.0.0-20230306173834-f164f413a900
|
||||
yunion.io/x/pkg v1.10.4-0.20251114095758-2a2f105d9712
|
||||
yunion.io/x/pkg v1.10.4-0.20260127060125-8939521ef75e
|
||||
yunion.io/x/s3cli v0.0.0-20241221171442-1c11599d28e1
|
||||
yunion.io/x/sqlchemy v1.1.3-0.20251231025938-b0a38f6e9fab
|
||||
yunion.io/x/structarg v0.0.0-20231017124457-df4d5009457c
|
||||
@@ -276,7 +280,6 @@ require (
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240228224816-df926f6c8641 // indirect
|
||||
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
|
||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||
gopkg.in/ini.v1 v1.62.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
k8s.io/klog/v2 v2.20.0 // indirect
|
||||
k8s.io/utils v0.0.0-20200729134348-d5654de09c73 // indirect
|
||||
|
||||
@@ -82,6 +82,8 @@ github.com/DataDog/zstd v1.3.4 h1:LAGHkXuvC6yky+C2CUG2tD7w8QlrUwpue8XwIh0X4AY=
|
||||
github.com/DataDog/zstd v1.3.4/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo=
|
||||
github.com/LeeEirc/terminalparser v0.0.0-20240205084113-fbf78c8480f2 h1:XGB3B0651J1uKOE1KJa1gsrV/DO1kthhk2NTDUHATgs=
|
||||
github.com/LeeEirc/terminalparser v0.0.0-20240205084113-fbf78c8480f2/go.mod h1:tiLv6VBLH4Z3KdBSe2qIKRwQDGCVQ9/F5fOKpQGvyoA=
|
||||
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||
github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ=
|
||||
github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE=
|
||||
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||
@@ -485,6 +487,8 @@ github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7V
|
||||
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
|
||||
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 h1:iQTw/8FWTuc7uiaSepXwyf3o52HaUYcV+Tu66S3F5GA=
|
||||
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8=
|
||||
github.com/kdomanski/iso9660 v0.4.0 h1:BPKKdcINz3m0MdjIMwS0wx1nofsOjxOq8TOr45WGHFg=
|
||||
github.com/kdomanski/iso9660 v0.4.0/go.mod h1:OxUSupHsO9ceI8lBLPJKWBTphLemjrCQY8LPXM7qSzU=
|
||||
github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
|
||||
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
@@ -584,6 +588,8 @@ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lN
|
||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/mogaika/udf v0.0.0-20171019171931-167f0ab01c73 h1:HpHNB68mF30LkUorpr7B22Xy0XwParP/n+AL0z4VlFc=
|
||||
github.com/mogaika/udf v0.0.0-20171019171931-167f0ab01c73/go.mod h1:OVErfG87tRGCHZpQGO8pL4vDZuY0qr4blPOTR0go6e8=
|
||||
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw=
|
||||
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8=
|
||||
github.com/mozillazg/go-httpheader v0.2.1 h1:geV7TrjbL8KXSyvghnFm+NyTux/hxwueTSrwhe88TQQ=
|
||||
@@ -1281,8 +1287,8 @@ yunion.io/x/ovsdb v0.0.0-20230306173834-f164f413a900 h1:Hu/4ERvoWaN6aiFs4h4/yvVB
|
||||
yunion.io/x/ovsdb v0.0.0-20230306173834-f164f413a900/go.mod h1:0vLkNEhlmA64HViPBAnSTUMrx5QP1CLsxXmxDKQ80tc=
|
||||
yunion.io/x/pkg v0.0.0-20190620104149-945c25821dbf/go.mod h1:t6rEGG2sQ4J7DhFxSZVOTjNd0YO/KlfWQyK1W4tog+E=
|
||||
yunion.io/x/pkg v0.0.0-20190628082551-f4033ba2ea30/go.mod h1:t6rEGG2sQ4J7DhFxSZVOTjNd0YO/KlfWQyK1W4tog+E=
|
||||
yunion.io/x/pkg v1.10.4-0.20251114095758-2a2f105d9712 h1:LUYhaE2PbRrlO2dJTII3K5L62wYlwDet2drziGv6yLY=
|
||||
yunion.io/x/pkg v1.10.4-0.20251114095758-2a2f105d9712/go.mod h1:0Bwxqd9MA3ACi119/l02FprY/o9gHahmYC2bsSbnVpM=
|
||||
yunion.io/x/pkg v1.10.4-0.20260127060125-8939521ef75e h1:py5Kd6cgP6pxB9pSc4ry/gnmA6rJZwMVOnAjra4sb2g=
|
||||
yunion.io/x/pkg v1.10.4-0.20260127060125-8939521ef75e/go.mod h1:0Bwxqd9MA3ACi119/l02FprY/o9gHahmYC2bsSbnVpM=
|
||||
yunion.io/x/s3cli v0.0.0-20241221171442-1c11599d28e1 h1:1KJ3YYinydPHpDEQRXdr/T8SYcKZ5Er+m489H+PnaQ4=
|
||||
yunion.io/x/s3cli v0.0.0-20241221171442-1c11599d28e1/go.mod h1:0iFKpOs1y4lbCxeOmq3Xx/0AcQoewVPwj62eRluioEo=
|
||||
yunion.io/x/sqlchemy v1.1.3-0.20251231025938-b0a38f6e9fab h1:5m/bSzW3uTAk83rp9eethbYsxJFYInFVeU1RDkteW4E=
|
||||
|
||||
@@ -19,13 +19,14 @@ import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"yunion.io/x/pkg/errors"
|
||||
|
||||
"yunion.io/x/onecloud/pkg/apigateway/clientman"
|
||||
"yunion.io/x/onecloud/pkg/appsrv"
|
||||
"yunion.io/x/onecloud/pkg/httperrors"
|
||||
"yunion.io/x/onecloud/pkg/mcclient"
|
||||
"yunion.io/x/onecloud/pkg/mcclient/auth"
|
||||
"yunion.io/x/onecloud/pkg/util/netutils2"
|
||||
"yunion.io/x/pkg/errors"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@@ -59,6 +59,7 @@ const (
|
||||
IMAGE_OS_DISTRO = "os_distribution"
|
||||
IMAGE_OS_TYPE = "os_type"
|
||||
IMAGE_OS_VERSION = "os_version"
|
||||
IMAGE_OS_LANGUAGE = "os_language"
|
||||
IMAGE_DISK_FORMAT = "disk_format"
|
||||
IMAGE_UEFI_SUPPORT = "uefi_support"
|
||||
IMAGE_BIOS_SUPPORT = "bios_support"
|
||||
|
||||
@@ -62,6 +62,7 @@ import (
|
||||
"yunion.io/x/onecloud/pkg/mcclient/modules/notify"
|
||||
"yunion.io/x/onecloud/pkg/util/cephutils"
|
||||
"yunion.io/x/onecloud/pkg/util/fileutils2"
|
||||
"yunion.io/x/onecloud/pkg/util/isoutils"
|
||||
"yunion.io/x/onecloud/pkg/util/logclient"
|
||||
"yunion.io/x/onecloud/pkg/util/procutils"
|
||||
"yunion.io/x/onecloud/pkg/util/qemuimg"
|
||||
@@ -1717,8 +1718,24 @@ func (m *SImageManager) PerformVmwareAccountAdded(ctx context.Context, userCred
|
||||
|
||||
func (image *SImage) doProbeImageInfo(ctx context.Context, userCred mcclient.TokenCredential) (bool, error) {
|
||||
if image.IsIso() {
|
||||
// no need to probe
|
||||
return false, nil
|
||||
imagePath := image.GetLocalLocation()
|
||||
if len(imagePath) == 0 {
|
||||
return false, errors.Wrapf(httperrors.ErrNotFound, "image file %s not found", image.Location)
|
||||
}
|
||||
fp, err := os.Open(imagePath)
|
||||
if err != nil {
|
||||
return false, errors.Wrap(err, "Open image file")
|
||||
}
|
||||
defer fp.Close()
|
||||
isoInfo, err := isoutils.DetectOSFromISO(fp)
|
||||
if err != nil {
|
||||
return false, errors.Wrap(err, "DetectOSFromISO")
|
||||
}
|
||||
err = image.updateIsoInfo(ctx, userCred, isoInfo)
|
||||
if err != nil {
|
||||
return false, errors.Wrap(err, "updateIsoInfo")
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
if image.IsData.IsTrue() {
|
||||
// no need to probe
|
||||
@@ -1772,9 +1789,9 @@ func (image *SImage) updateImageInfo(
|
||||
imageProperties := jsonutils.Marshal(imageInfo.OsInfo).(*jsonutils.JSONDict)
|
||||
|
||||
imageProperties.Set(api.IMAGE_OS_ARCH, jsonutils.NewString(imageInfo.OsInfo.Arch))
|
||||
imageProperties.Set("os_version", jsonutils.NewString(imageInfo.OsInfo.Version))
|
||||
imageProperties.Set("os_distribution", jsonutils.NewString(imageInfo.OsInfo.Distro))
|
||||
imageProperties.Set("os_language", jsonutils.NewString(imageInfo.OsInfo.Language))
|
||||
imageProperties.Set(api.IMAGE_OS_VERSION, jsonutils.NewString(imageInfo.OsInfo.Version))
|
||||
imageProperties.Set(api.IMAGE_OS_DISTRO, jsonutils.NewString(imageInfo.OsInfo.Distro))
|
||||
imageProperties.Set(api.IMAGE_OS_LANGUAGE, jsonutils.NewString(imageInfo.OsInfo.Language))
|
||||
|
||||
imageProperties.Set(api.IMAGE_OS_TYPE, jsonutils.NewString(imageInfo.OsType))
|
||||
imageProperties.Set(api.IMAGE_PARTITION_TYPE, jsonutils.NewString(imageInfo.PhysicalPartitionType))
|
||||
@@ -1787,6 +1804,38 @@ func (image *SImage) updateImageInfo(
|
||||
return ImagePropertyManager.SaveProperties(ctx, userCred, image.Id, imageProperties)
|
||||
}
|
||||
|
||||
func (image *SImage) updateIsoInfo(ctx context.Context, userCred mcclient.TokenCredential, imageInfo *isoutils.ISOInfo) error {
|
||||
if gotypes.IsNil(imageInfo) || len(imageInfo.Distro) == 0 {
|
||||
return nil
|
||||
}
|
||||
change := false
|
||||
imageProperties := jsonutils.Marshal(imageInfo).(*jsonutils.JSONDict)
|
||||
if len(imageInfo.Arch) > 0 {
|
||||
imageProperties.Set(api.IMAGE_OS_ARCH, jsonutils.NewString(imageInfo.Arch))
|
||||
change = true
|
||||
db.Update(image, func() error {
|
||||
image.OsArch = imageInfo.Arch
|
||||
return nil
|
||||
})
|
||||
}
|
||||
if len(imageInfo.Version) > 0 {
|
||||
imageProperties.Set(api.IMAGE_OS_VERSION, jsonutils.NewString(imageInfo.Version))
|
||||
change = true
|
||||
}
|
||||
if len(imageInfo.Distro) > 0 {
|
||||
imageProperties.Set(api.IMAGE_OS_DISTRO, jsonutils.NewString(imageInfo.Distro))
|
||||
change = true
|
||||
}
|
||||
if len(imageInfo.Language) > 0 {
|
||||
imageProperties.Set(api.IMAGE_OS_LANGUAGE, jsonutils.NewString(imageInfo.Language))
|
||||
change = true
|
||||
}
|
||||
if change {
|
||||
return ImagePropertyManager.SaveProperties(ctx, userCred, image.Id, imageProperties)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (image *SImage) updateChecksum() error {
|
||||
imagePath := image.GetLocalLocation()
|
||||
if len(imagePath) == 0 {
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
package isoutils // import "yunion.io/x/onecloud/pkg/util/isoutils"
|
||||
@@ -0,0 +1,148 @@
|
||||
// Copyright 2019 Yunion
|
||||
//
|
||||
// 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.
|
||||
|
||||
package isoutils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"github.com/kdomanski/iso9660"
|
||||
|
||||
"yunion.io/x/log"
|
||||
)
|
||||
|
||||
// findISO9660File 在ISO9660中查找文件,返回 *iso9660.File
|
||||
func (r *ISOFileReader) findISO9660File(path string) (*iso9660.File, error) {
|
||||
if r.format != ISOFormatISO9660 || r.iso9660Img == nil {
|
||||
return nil, fmt.Errorf("ISO9660格式未初始化")
|
||||
}
|
||||
|
||||
rootDir, err := r.iso9660Img.RootDir()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("获取根目录失败: %v", err)
|
||||
}
|
||||
|
||||
// 规范化路径
|
||||
path = strings.Trim(path, "/")
|
||||
if path == "" {
|
||||
return rootDir, nil
|
||||
}
|
||||
|
||||
parts := strings.Split(path, "/")
|
||||
currentDir := rootDir
|
||||
|
||||
for i, part := range parts {
|
||||
if part == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// 获取当前目录的子项
|
||||
children, err := currentDir.GetChildren()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("读取目录失败: %v", err)
|
||||
}
|
||||
|
||||
// 查找匹配的文件或目录(不区分大小写)
|
||||
var found *iso9660.File
|
||||
for _, child := range children {
|
||||
if strings.EqualFold(child.Name(), part) {
|
||||
found = child
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if found == nil {
|
||||
return nil, fmt.Errorf("文件或目录不存在: %s", part)
|
||||
}
|
||||
|
||||
// 如果是最后一个部分,返回找到的文件
|
||||
if i == len(parts)-1 {
|
||||
return found, nil
|
||||
}
|
||||
|
||||
// 检查是否为目录
|
||||
if !found.IsDir() {
|
||||
return nil, fmt.Errorf("路径中的%s不是目录", part)
|
||||
}
|
||||
|
||||
currentDir = found
|
||||
}
|
||||
|
||||
return currentDir, nil
|
||||
}
|
||||
|
||||
// listISO9660Dir 列出ISO9660格式指定目录下的所有文件和子目录
|
||||
func (r *ISOFileReader) listISO9660Dir(path string) ([]ISO9660FileInfo, error) {
|
||||
if r.format != ISOFormatISO9660 {
|
||||
return nil, fmt.Errorf("此方法仅支持ISO9660格式")
|
||||
}
|
||||
|
||||
// 获取目录
|
||||
dir, err := r.findISO9660File(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !dir.IsDir() {
|
||||
return nil, fmt.Errorf("路径%s不是目录", path)
|
||||
}
|
||||
|
||||
// 获取子项
|
||||
children, err := dir.GetChildren()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("读取目录失败: %v", err)
|
||||
}
|
||||
|
||||
var files []ISO9660FileInfo
|
||||
for _, child := range children {
|
||||
fileInfo := ISO9660FileInfo{
|
||||
Name: child.Name(),
|
||||
IsDir: child.IsDir(),
|
||||
Size: child.Size(),
|
||||
Location: 0, // 使用库时不需要直接访问位置
|
||||
}
|
||||
files = append(files, fileInfo)
|
||||
}
|
||||
|
||||
return files, nil
|
||||
}
|
||||
|
||||
// readISO9660FileContent 读取ISO9660格式文件内容
|
||||
func (r *ISOFileReader) readISO9660FileContent(path string) (string, error) {
|
||||
file, err := r.findISO9660File(path)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("文件%s不存在: %v", path, err)
|
||||
}
|
||||
|
||||
if file.IsDir() {
|
||||
return "", fmt.Errorf("路径%s是目录,不是文件", path)
|
||||
}
|
||||
|
||||
reader := file.Reader()
|
||||
if reader == nil {
|
||||
return "", fmt.Errorf("无法读取文件%s", path)
|
||||
}
|
||||
|
||||
// 读取前10KB内容(足够识别特征)
|
||||
buf := make([]byte, 10*1024)
|
||||
n, err := reader.Read(buf)
|
||||
if err != nil && err != io.EOF {
|
||||
log.Errorf("读取ISO9660文件%s失败: %v", path, err)
|
||||
return "", fmt.Errorf("读取文件%s失败: %v", path, err)
|
||||
}
|
||||
|
||||
return strings.TrimSpace(string(buf[:n])), nil
|
||||
}
|
||||
@@ -0,0 +1,428 @@
|
||||
// Copyright 2019 Yunion
|
||||
//
|
||||
// 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.
|
||||
|
||||
package isoutils
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/kdomanski/iso9660"
|
||||
"github.com/mogaika/udf"
|
||||
"gopkg.in/ini.v1"
|
||||
|
||||
"yunion.io/x/log"
|
||||
"yunion.io/x/pkg/util/imagetools"
|
||||
)
|
||||
|
||||
// ========== 2. 新增结构化返回结果(包含发行版、版本号、架构) ==========
|
||||
type ISOInfo struct {
|
||||
Distro string // 发行版(如 CentOS、Ubuntu Server)
|
||||
Version string // 版本号(如 7.9、22.04 LTS、2022)
|
||||
Arch string // 架构(如 x86_64、riscv64、arm64)
|
||||
Language string // 语言(如 en-US、zh-CN)
|
||||
}
|
||||
|
||||
// ISO格式类型
|
||||
type ISOFormat string
|
||||
|
||||
const (
|
||||
ISOFormatUnknown ISOFormat = "unknown"
|
||||
ISOFormatUDF ISOFormat = "udf"
|
||||
ISOFormatISO9660 ISOFormat = "iso9660"
|
||||
)
|
||||
|
||||
// ========== 3. 优化ISOFileReader:增加缓存、日志、架构识别 ==========
|
||||
type ISOFileReader struct {
|
||||
format ISOFormat
|
||||
img *udf.Udf
|
||||
iso9660Img *iso9660.Image // ISO9660格式的读取器
|
||||
reader io.Reader
|
||||
cache sync.Map // 缓存已读取的文件内容:key=文件路径,value=文件内容
|
||||
}
|
||||
|
||||
// isIsoFile 检测ISO格式(UDF或ISO9660)
|
||||
func isIsoFile(readerAt io.ReaderAt) (bool, error) {
|
||||
// 读取0x8000地址的内容(ISO9660的Primary Volume Descriptor位置)
|
||||
buf := make([]byte, 6)
|
||||
n, err := readerAt.ReadAt(buf, 0x8000)
|
||||
if err != nil && err != io.EOF {
|
||||
return false, fmt.Errorf("读取ISO格式标识失败: %v", err)
|
||||
}
|
||||
|
||||
if n < 6 {
|
||||
return false, fmt.Errorf("读取数据不足")
|
||||
}
|
||||
|
||||
// ISO9660格式:偏移0x8001-0x8005应该是"CD001"
|
||||
if bytes.Equal(buf[1:6], []byte("CD001")) {
|
||||
return true, nil
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// NewISOFileReader 初始化ISO读取器(新增格式检测和日志配置)
|
||||
func NewISOFileReader(reader io.Reader) (*ISOFileReader, error) {
|
||||
readerAt, ok := reader.(io.ReaderAt)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("reader is not io.ReaderAt")
|
||||
}
|
||||
|
||||
// 检测ISO格式
|
||||
isIso, err := isIsoFile(readerAt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !isIso {
|
||||
return nil, fmt.Errorf("ISO镜像格式不正确")
|
||||
}
|
||||
|
||||
ret := &ISOFileReader{
|
||||
format: ISOFormatISO9660,
|
||||
reader: reader,
|
||||
cache: sync.Map{},
|
||||
}
|
||||
|
||||
if isUdfFile(readerAt) {
|
||||
ret.format = ISOFormatUDF
|
||||
ret.img = udf.NewUdfFromReader(readerAt)
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
isoImg, err := iso9660.OpenImage(readerAt)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("打开ISO9660镜像失败: %v", err)
|
||||
}
|
||||
ret.iso9660Img = isoImg
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
// ISO9660FileInfo ISO9660文件信息
|
||||
type ISO9660FileInfo struct {
|
||||
Name string // 文件名
|
||||
IsDir bool // 是否为目录
|
||||
Size int64 // 文件大小(字节)
|
||||
Location int64 // 文件在ISO中的位置(字节偏移,使用库时可能为0)
|
||||
}
|
||||
|
||||
func (r *ISOFileReader) list(path string) ([]ISO9660FileInfo, error) {
|
||||
if r.format == ISOFormatISO9660 {
|
||||
return r.listISO9660Dir(path)
|
||||
}
|
||||
return r.listUdfDir(path)
|
||||
}
|
||||
|
||||
// FileExists 检查ISO内指定路径的文件是否存在(支持UDF和ISO9660)
|
||||
func (r *ISOFileReader) FileExists(path string) bool {
|
||||
if r.format == ISOFormatISO9660 {
|
||||
_, err := r.findISO9660File(path)
|
||||
return err == nil
|
||||
}
|
||||
_, err := r.GetFile(path)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// ReadFileContent 读取ISO内指定文件的内容(新增缓存、日志,支持UDF和ISO9660)
|
||||
func (r *ISOFileReader) ReadFileContent(path string) (string, error) {
|
||||
// 优先从缓存读取
|
||||
if cacheVal, ok := r.cache.Load(path); ok {
|
||||
log.Debugf("从缓存读取文件内容: %s", path)
|
||||
return cacheVal.(string), nil
|
||||
}
|
||||
|
||||
var content string
|
||||
var err error
|
||||
|
||||
// 根据格式选择相应的读取方法
|
||||
if r.format == ISOFormatISO9660 {
|
||||
content, err = r.readISO9660FileContent(path)
|
||||
} else if r.format == ISOFormatUDF {
|
||||
content, err = r.readUdfFileContent(path)
|
||||
} else {
|
||||
return "", fmt.Errorf("未知的ISO格式: %s", r.format)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// 写入缓存
|
||||
r.cache.Store(path, content)
|
||||
log.Debugf("读取文件%s内容(长度: %d)并缓存", path, len(content))
|
||||
|
||||
return content, nil
|
||||
}
|
||||
|
||||
// ========== 6. 核心识别函数:整合版本号、架构、日志、缓存 ==========
|
||||
func DetectOSFromISO(r io.Reader) (*ISOInfo, error) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
log.Errorf("DetectOSFromISO panic error: %v", r)
|
||||
}
|
||||
}()
|
||||
|
||||
result := &ISOInfo{}
|
||||
|
||||
// 初始化读取器
|
||||
reader, err := NewISOFileReader(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// ========== 识别Windows系列 ==========
|
||||
if reader.FileExists("sources/install.wim") {
|
||||
return DetectWindowsEdition(reader)
|
||||
}
|
||||
|
||||
files, err := reader.list("/")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, file := range files {
|
||||
fileName := file.Name
|
||||
|
||||
if fileName == ".treeinfo" {
|
||||
content, _ := reader.ReadFileContent(fileName)
|
||||
result = getOsInfoByIniFile(content)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if reader.FileExists(".disk/info") {
|
||||
content, _ := reader.ReadFileContent(".disk/info")
|
||||
info := imagetools.NormalizeImageInfo(content, "", "", "", "")
|
||||
result = &ISOInfo{
|
||||
Distro: info.OsDistro,
|
||||
Version: info.OsVersion,
|
||||
Arch: info.OsArch,
|
||||
Language: info.OsLang,
|
||||
}
|
||||
}
|
||||
|
||||
if len(result.Distro) == 0 || result.Distro == imagetools.OS_DIST_OTHER_LINUX {
|
||||
realeaseFile := ""
|
||||
if reader.FileExists("dists") {
|
||||
files, err := reader.list("dists")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, file := range files {
|
||||
log.Debugf("file: %s", file.Name)
|
||||
if !file.IsDir {
|
||||
continue
|
||||
}
|
||||
subFiles, err := reader.list(fmt.Sprintf("dists/%s", file.Name))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, subFile := range subFiles {
|
||||
if subFile.Name == "Release" {
|
||||
realeaseFile = fmt.Sprintf("dists/%s/%s", file.Name, subFile.Name)
|
||||
break
|
||||
}
|
||||
}
|
||||
if realeaseFile != "" {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if realeaseFile != "" {
|
||||
content, _ := reader.ReadFileContent(realeaseFile)
|
||||
result = getOsInfoByReleaseFile(content)
|
||||
} else if reader.FileExists("boot/grub2/grub.cfg") {
|
||||
content, _ := reader.ReadFileContent("boot/grub2/grub.cfg")
|
||||
result = getOsInfoByGrub(content)
|
||||
} else if reader.FileExists("EFI/BOOT/grub.cfg") {
|
||||
content, _ := reader.ReadFileContent("EFI/BOOT/grub.cfg")
|
||||
result = getOsInfoByGrub(content)
|
||||
} else if reader.FileExists("isolinux/isolinux.cfg") {
|
||||
content, _ := reader.ReadFileContent("isolinux/isolinux.cfg")
|
||||
result = getOsInfoByIsoLinux(content)
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func getOsInfoByReleaseFile(content string) *ISOInfo {
|
||||
result := &ISOInfo{}
|
||||
for _, line := range strings.Split(content, "\n") {
|
||||
if strings.HasPrefix(line, "Origin:") {
|
||||
result.Distro = strings.TrimSpace(strings.TrimPrefix(line, "Origin:"))
|
||||
}
|
||||
if strings.HasPrefix(line, "Label:") && len(result.Distro) == 0 {
|
||||
result.Distro = strings.TrimSpace(strings.TrimPrefix(line, "Label:"))
|
||||
}
|
||||
if strings.HasPrefix(line, "Version:") {
|
||||
result.Version = strings.TrimSpace(strings.TrimPrefix(line, "Version:"))
|
||||
}
|
||||
if strings.HasPrefix(line, "Architectures:") {
|
||||
result.Arch = strings.TrimSpace(strings.TrimPrefix(line, "Architectures:"))
|
||||
result.Arch = detectArchitecture(strings.ToLower(result.Arch), result.Arch)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func getOsInfoByIniFile(content string) *ISOInfo {
|
||||
cfg, err := ini.Load([]byte(content))
|
||||
if err != nil {
|
||||
// 兼容手动解析(应对部分非标准 INI 格式的 .treeinfo)
|
||||
return parseTreeInfoFallback(content)
|
||||
}
|
||||
release := cfg.Section("release")
|
||||
ret := &ISOInfo{}
|
||||
ret.Distro = release.Key("name").String()
|
||||
ret.Version = release.Key("version").String()
|
||||
general := cfg.Section("general")
|
||||
ret.Arch = general.Key("arch").String()
|
||||
if len(ret.Version) == 0 {
|
||||
ret.Version = general.Key("version").String()
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func parseTreeInfoFallback(content string) *ISOInfo {
|
||||
result := &ISOInfo{}
|
||||
info := strings.Split(content, "\n")
|
||||
for _, line := range info {
|
||||
if strings.HasPrefix(line, "arch =") {
|
||||
result.Arch = strings.TrimPrefix(line, "arch = ")
|
||||
}
|
||||
if strings.HasPrefix(line, "version =") {
|
||||
result.Version = strings.TrimPrefix(line, "version = ")
|
||||
}
|
||||
if strings.HasPrefix(line, "name =") {
|
||||
result.Distro = strings.TrimPrefix(line, "name = ")
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func getOsInfoByIsoLinux(content string) *ISOInfo {
|
||||
result := &ISOInfo{}
|
||||
lowerContent := strings.ToLower(content)
|
||||
// 5.1 识别发行版(关键词匹配)
|
||||
result.Distro = detectDistro(lowerContent)
|
||||
|
||||
// 5.2 识别版本号(正则提取)
|
||||
result.Version = detectVersion(content)
|
||||
|
||||
// 5.3 识别 CPU 架构(关键词+正则)
|
||||
result.Arch = detectArchitecture(lowerContent, content)
|
||||
return result
|
||||
}
|
||||
|
||||
func getOsInfoByGrub(content string) *ISOInfo {
|
||||
result := &ISOInfo{}
|
||||
lowerContent := strings.ToLower(content)
|
||||
result.Distro = detectDistro(lowerContent)
|
||||
result.Version = detectGrubVersion(content)
|
||||
result.Arch = detectArchitecture(lowerContent, content)
|
||||
return result
|
||||
}
|
||||
|
||||
func detectDistro(lowerContent string) string {
|
||||
info := imagetools.NormalizeImageInfo(lowerContent, "", "", "", "")
|
||||
return info.OsDistro
|
||||
}
|
||||
|
||||
// detectVersion 从配置内容中提取版本号
|
||||
func detectVersion(content string) string {
|
||||
// 匹配版本号的正则(支持 x x.y、x.y.z、x.y-LTS 等格式)
|
||||
versionRegex := regexp.MustCompile(`(\d+(\.\d+(\.\d+)?)?(-[A-Za-z0-9]+)?)`)
|
||||
|
||||
// 优先从启动标题(label/menu label)中提取
|
||||
labelLines := regexp.MustCompile(`(?i)menu label .+Install.+`).FindAllStringSubmatch(content, -1)
|
||||
for _, line := range labelLines {
|
||||
log.Debugf("line: %s", line)
|
||||
if len(line) >= 1 {
|
||||
version := versionRegex.FindString(line[0])
|
||||
if version != "" {
|
||||
return version
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 从整个内容中提取第一个匹配的版本号
|
||||
return versionRegex.FindString(content)
|
||||
}
|
||||
|
||||
// detectArchitecture 识别 CPU 架构
|
||||
func detectArchitecture(lowerContent, rawContent string) string {
|
||||
// 架构关键词映射
|
||||
archKeywords := map[string][]string{
|
||||
"x86_64": {"x86_64", "amd64"},
|
||||
"aarch64": {"aarch64", "arm64"},
|
||||
"i386": {"i386", "i686"},
|
||||
"armhfp": {"armhfp", "armv7"},
|
||||
"ppc64le": {"ppc64le"},
|
||||
"s390x": {"s390x"},
|
||||
}
|
||||
|
||||
for arch, keywords := range archKeywords {
|
||||
for _, kw := range keywords {
|
||||
if strings.Contains(lowerContent, kw) {
|
||||
return arch
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 从内核文件名(vmlinuz/initrd)中提取
|
||||
kernelRegex := regexp.MustCompile(`vmlinuz-([a-zA-Z0-9_]+)`)
|
||||
match := kernelRegex.FindStringSubmatch(rawContent)
|
||||
if len(match) >= 2 {
|
||||
return match[1]
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// detectGrubVersion 从 grub.cfg 提取版本号
|
||||
func detectGrubVersion(content string) string {
|
||||
// 匹配版本号的正则(支持 x x.y、x.y.z、x.y-LTS、x.y.z-xxx 等格式)
|
||||
versionRegex := regexp.MustCompile(`(\d+(\.\d+(\.\d+)?)?(-[A-Za-z0-9]+)?)`)
|
||||
|
||||
// 优先从 GRUB 菜单标题(menuentry)中提取(准确性更高)
|
||||
menuEntryRegex := regexp.MustCompile(`(?i)menuentry\s+["'](.+?)["']`)
|
||||
menuEntries := menuEntryRegex.FindAllStringSubmatch(content, -1)
|
||||
for _, entry := range menuEntries {
|
||||
log.Debugf("entry: %s", entry)
|
||||
if len(entry) >= 1 {
|
||||
version := versionRegex.FindString(entry[0])
|
||||
if version != "" {
|
||||
return version
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 从内核文件名/参数中提取
|
||||
kernelLines := regexp.MustCompile(`linux\s+.+`).FindAllString(content, -1)
|
||||
for _, line := range kernelLines {
|
||||
version := versionRegex.FindString(line)
|
||||
if version != "" {
|
||||
return version
|
||||
}
|
||||
}
|
||||
|
||||
// 最后从整个内容中提取第一个匹配的版本号
|
||||
return versionRegex.FindString(content)
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
// Copyright 2019 Yunion
|
||||
//
|
||||
// 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.
|
||||
|
||||
package isoutils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"github.com/mogaika/udf"
|
||||
|
||||
"yunion.io/x/log"
|
||||
)
|
||||
|
||||
// isUdfFile 检测是否为UDF格式
|
||||
func isUdfFile(readerAt io.ReaderAt) bool {
|
||||
defer func() {
|
||||
recover()
|
||||
}()
|
||||
|
||||
img := udf.NewUdfFromReader(readerAt)
|
||||
files := img.ReadDir(nil)
|
||||
if len(files) == 0 {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// findUdfDir 在UDF中查找目录,返回目录的 FileEntry
|
||||
func (r *ISOFileReader) findUdfDir(path string) ([]udf.File, error) {
|
||||
if r.format != ISOFormatUDF || r.img == nil {
|
||||
return nil, fmt.Errorf("UDF格式未初始化")
|
||||
}
|
||||
|
||||
// 规范化路径
|
||||
path = strings.Trim(path, "/")
|
||||
if path == "" {
|
||||
// 根目录
|
||||
return r.img.ReadDir(nil), nil
|
||||
}
|
||||
|
||||
// 查找目录路径
|
||||
parts := strings.Split(path, "/")
|
||||
var entry *udf.FileEntry = nil
|
||||
currentDirEntry := r.img.ReadDir(entry) // 从根目录开始
|
||||
|
||||
for i, part := range parts {
|
||||
if part == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
var found *udf.File
|
||||
// 在当前目录中查找
|
||||
for idx := range currentDirEntry {
|
||||
child := ¤tDirEntry[idx]
|
||||
childName := child.Name()
|
||||
// 匹配文件名(不区分大小写)
|
||||
if strings.EqualFold(childName, part) {
|
||||
found = child
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if found == nil {
|
||||
return nil, fmt.Errorf("目录不存在: %s", part)
|
||||
}
|
||||
|
||||
// 如果是最后一个部分,返回该目录的内容
|
||||
if i == len(parts)-1 {
|
||||
return found.ReadDir(), nil
|
||||
}
|
||||
|
||||
// 继续查找下一级目录
|
||||
currentDirEntry = found.ReadDir()
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("未找到目录: %s", path)
|
||||
}
|
||||
|
||||
// listUdfDir 列出UDF格式指定目录下的所有文件和子目录
|
||||
func (r *ISOFileReader) listUdfDir(path string) ([]ISO9660FileInfo, error) {
|
||||
if r.format != ISOFormatUDF {
|
||||
return nil, fmt.Errorf("此方法仅支持UDF格式")
|
||||
}
|
||||
|
||||
// 获取目录内容
|
||||
children, err := r.findUdfDir(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var files []ISO9660FileInfo
|
||||
for _, child := range children {
|
||||
fileInfo := ISO9660FileInfo{
|
||||
Name: child.Name(),
|
||||
IsDir: child.IsDir(),
|
||||
Size: child.Size(),
|
||||
Location: 0, // 使用库时不需要直接访问位置
|
||||
}
|
||||
files = append(files, fileInfo)
|
||||
}
|
||||
|
||||
return files, nil
|
||||
}
|
||||
|
||||
// GetFile 在UDF中查找指定路径的文件
|
||||
func (r *ISOFileReader) GetFile(path string) (*udf.File, error) {
|
||||
if r.format != ISOFormatUDF {
|
||||
return nil, fmt.Errorf("此方法仅支持UDF格式")
|
||||
}
|
||||
|
||||
// UDF格式的原有逻辑
|
||||
parts := strings.Split(strings.Trim(path, "/"), "/")
|
||||
|
||||
var entry *udf.FileEntry = nil
|
||||
currentDirEntry := r.img.ReadDir(entry) // 从根目录开始
|
||||
|
||||
for i, part := range parts {
|
||||
if part == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
var found *udf.File
|
||||
// 在当前目录中查找
|
||||
for idx := range currentDirEntry {
|
||||
child := ¤tDirEntry[idx]
|
||||
childName := child.Name()
|
||||
// 匹配文件名(UDF 文件名通常不包含版本号后缀)
|
||||
if childName == part {
|
||||
found = child
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if found == nil {
|
||||
return nil, fmt.Errorf("文件或目录不存在: %s", part)
|
||||
}
|
||||
|
||||
// 如果是最后一个部分,返回文件
|
||||
if i == len(parts)-1 {
|
||||
return found, nil
|
||||
}
|
||||
|
||||
currentDirEntry = found.ReadDir()
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("未找到文件: %s", path)
|
||||
}
|
||||
|
||||
// readUdfFileContent 读取UDF格式文件内容
|
||||
func (r *ISOFileReader) readUdfFileContent(path string) (string, error) {
|
||||
file, err := r.GetFile(path)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("文件%s不存在: %v", path, err)
|
||||
}
|
||||
|
||||
if file.IsDir() {
|
||||
return "", fmt.Errorf("路径%s是目录,不是文件", path)
|
||||
}
|
||||
|
||||
reader := file.NewReader()
|
||||
if reader == nil {
|
||||
return "", fmt.Errorf("无法读取文件%s", path)
|
||||
}
|
||||
|
||||
// 读取前10KB内容(足够识别特征)
|
||||
buf := make([]byte, 10*1024)
|
||||
n, err := reader.Read(buf)
|
||||
if err != nil && err != io.EOF {
|
||||
log.Errorf("读取UDF文件%s失败: %v", path, err)
|
||||
return "", fmt.Errorf("读取文件%s失败: %v", path, err)
|
||||
}
|
||||
|
||||
return strings.TrimSpace(string(buf[:n])), nil
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
// Copyright 2019 Yunion
|
||||
//
|
||||
// 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.
|
||||
|
||||
//go:build !linux
|
||||
// +build !linux
|
||||
|
||||
package isoutils
|
||||
|
||||
import (
|
||||
"yunion.io/x/pkg/errors"
|
||||
)
|
||||
|
||||
func DetectWindowsEdition(r *ISOFileReader) (*ISOInfo, error) {
|
||||
return nil, errors.Wrap(errors.ErrNotSupported, "not supported")
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
// Copyright 2019 Yunion
|
||||
//
|
||||
// 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.
|
||||
|
||||
//go:build linux
|
||||
// +build linux
|
||||
|
||||
package isoutils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/Microsoft/go-winio/wim"
|
||||
|
||||
"yunion.io/x/log"
|
||||
"yunion.io/x/pkg/util/imagetools"
|
||||
)
|
||||
|
||||
// ========== 7. 保留Windows版本识别函数(适配新结构) ==========
|
||||
func DetectWindowsEdition(r *ISOFileReader) (*ISOInfo, error) {
|
||||
wimFile, err := r.GetFile("sources/install.wim")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
wim, err := wim.NewReader(wimFile.NewReader())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result := &ISOInfo{}
|
||||
for _, image := range wim.Image {
|
||||
version := fmt.Sprintf("%d.%d.%d", image.Windows.Version.Major, image.Windows.Version.Minor, image.Windows.Version.Build)
|
||||
if image.Windows != nil {
|
||||
if image.Windows.Arch == 9 {
|
||||
result.Arch = "x86_64"
|
||||
} else if image.Windows.Arch == 12 {
|
||||
result.Arch = "arm64"
|
||||
} else if image.Windows.Arch == 0 {
|
||||
result.Arch = "x86"
|
||||
}
|
||||
result.Distro = imagetools.OS_DIST_WINDOWS
|
||||
result.Language = image.Windows.DefaultLanguage
|
||||
switch fmt.Sprintf("%d.%d", image.Windows.Version.Major, image.Windows.Version.Minor) {
|
||||
case "6.0":
|
||||
result.Version = "Windows Vista"
|
||||
case "6.1":
|
||||
result.Version = "Windows 7"
|
||||
case "6.2":
|
||||
result.Version = "Windows 8"
|
||||
case "6.3":
|
||||
result.Version = "Windows 8.1"
|
||||
case "10.0":
|
||||
if image.Windows.Version.Build >= 27500 {
|
||||
result.Version = "Windows 12"
|
||||
} else if image.Windows.Version.Build >= 22000 {
|
||||
result.Version = "Windows 11"
|
||||
} else {
|
||||
result.Version = "Windows 10"
|
||||
}
|
||||
}
|
||||
if image.Windows.ProductType == "ServerNT" {
|
||||
result.Distro = imagetools.OS_DIST_WINDOWS_SERVER
|
||||
switch fmt.Sprintf("%d.%d", image.Windows.Version.Major, image.Windows.Version.Minor) {
|
||||
case "6.0":
|
||||
result.Version = "Windows Server 2008"
|
||||
case "6.1":
|
||||
result.Version = "Windows Server 2008 R2"
|
||||
case "6.2":
|
||||
result.Version = "Windows Server 2012"
|
||||
case "6.3":
|
||||
result.Version = "Windows Server 2012 R2"
|
||||
case "10.0":
|
||||
if image.Windows.Version.Build >= 26040 {
|
||||
result.Version = "Windows Server 2025"
|
||||
} else if image.Windows.Version.Build >= 20348 {
|
||||
result.Version = "Windows Server 2022"
|
||||
} else if image.Windows.Version.Build >= 17763 {
|
||||
result.Version = "Windows Server 2019"
|
||||
} else if image.Windows.Version.Build >= 14393 {
|
||||
result.Version = "Windows Server 2016"
|
||||
}
|
||||
}
|
||||
}
|
||||
log.Debugf("识别到 %s 版本: %s -> %s", result.Distro, version, result.Version)
|
||||
break
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015 Microsoft
|
||||
|
||||
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.
|
||||
|
||||
+139
@@ -0,0 +1,139 @@
|
||||
//go:build windows || linux
|
||||
// +build windows linux
|
||||
|
||||
package wim
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"io"
|
||||
|
||||
"github.com/Microsoft/go-winio/wim/lzx"
|
||||
)
|
||||
|
||||
const chunkSize = 32768 // Compressed resource chunk size
|
||||
|
||||
type compressedReader struct {
|
||||
r *io.SectionReader
|
||||
d io.ReadCloser
|
||||
chunks []int64
|
||||
curChunk int
|
||||
originalSize int64
|
||||
}
|
||||
|
||||
func newCompressedReader(r *io.SectionReader, originalSize int64, offset int64) (*compressedReader, error) {
|
||||
nchunks := (originalSize + chunkSize - 1) / chunkSize
|
||||
var base int64
|
||||
chunks := make([]int64, nchunks)
|
||||
if originalSize <= 0xffffffff {
|
||||
// 32-bit chunk offsets
|
||||
base = (nchunks - 1) * 4
|
||||
chunks32 := make([]uint32, nchunks-1)
|
||||
err := binary.Read(r, binary.LittleEndian, chunks32)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for i, n := range chunks32 {
|
||||
chunks[i+1] = int64(n)
|
||||
}
|
||||
} else {
|
||||
// 64-bit chunk offsets
|
||||
base = (nchunks - 1) * 8
|
||||
err := binary.Read(r, binary.LittleEndian, chunks[1:])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
for i, c := range chunks {
|
||||
chunks[i] = c + base
|
||||
}
|
||||
|
||||
cr := &compressedReader{
|
||||
r: r,
|
||||
chunks: chunks,
|
||||
originalSize: originalSize,
|
||||
}
|
||||
|
||||
err := cr.reset(int(offset / chunkSize))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
suboff := offset % chunkSize
|
||||
if suboff != 0 {
|
||||
_, err := io.CopyN(io.Discard, cr.d, suboff)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return cr, nil
|
||||
}
|
||||
|
||||
func (r *compressedReader) chunkOffset(n int) int64 {
|
||||
if n == len(r.chunks) {
|
||||
return r.r.Size()
|
||||
}
|
||||
return r.chunks[n]
|
||||
}
|
||||
|
||||
func (r *compressedReader) chunkSize(n int) int {
|
||||
return int(r.chunkOffset(n+1) - r.chunkOffset(n))
|
||||
}
|
||||
|
||||
func (r *compressedReader) uncompressedSize(n int) int {
|
||||
if n < len(r.chunks)-1 {
|
||||
return chunkSize
|
||||
}
|
||||
size := int(r.originalSize % chunkSize)
|
||||
if size == 0 {
|
||||
size = chunkSize
|
||||
}
|
||||
return size
|
||||
}
|
||||
|
||||
func (r *compressedReader) reset(n int) error {
|
||||
if n >= len(r.chunks) {
|
||||
return io.EOF
|
||||
}
|
||||
if r.d != nil {
|
||||
r.d.Close()
|
||||
}
|
||||
r.curChunk = n
|
||||
size := r.chunkSize(n)
|
||||
uncompressedSize := r.uncompressedSize(n)
|
||||
section := io.NewSectionReader(r.r, r.chunkOffset(n), int64(size))
|
||||
if size != uncompressedSize {
|
||||
d, err := lzx.NewReader(section, uncompressedSize)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
r.d = d
|
||||
} else {
|
||||
r.d = io.NopCloser(section)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *compressedReader) Read(b []byte) (int, error) {
|
||||
for {
|
||||
n, err := r.d.Read(b)
|
||||
if err != io.EOF { //nolint:errorlint
|
||||
return n, err
|
||||
}
|
||||
|
||||
err = r.reset(r.curChunk + 1)
|
||||
if err != nil {
|
||||
return n, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (r *compressedReader) Close() error {
|
||||
var err error
|
||||
if r.d != nil {
|
||||
err = r.d.Close()
|
||||
r.d = nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
+598
@@ -0,0 +1,598 @@
|
||||
// Package lzx implements a decompressor for the the WIM variant of the
|
||||
// LZX compression algorithm.
|
||||
//
|
||||
// The LZX algorithm is an earlier variant of LZX DELTA, which is documented
|
||||
// at https://msdn.microsoft.com/en-us/library/cc483133(v=exchg.80).aspx.
|
||||
package lzx
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"io"
|
||||
)
|
||||
|
||||
const (
|
||||
maincodecount = 496
|
||||
maincodesplit = 256
|
||||
lencodecount = 249
|
||||
lenshift = 9
|
||||
codemask = 0x1ff
|
||||
tablebits = 9
|
||||
tablesize = 1 << tablebits
|
||||
|
||||
maxBlockSize = 32768
|
||||
windowSize = 32768
|
||||
|
||||
maxTreePathLen = 16
|
||||
|
||||
e8filesize = 12000000
|
||||
maxe8offset = 0x3fffffff
|
||||
|
||||
verbatimBlock = 1
|
||||
alignedOffsetBlock = 2
|
||||
uncompressedBlock = 3
|
||||
)
|
||||
|
||||
var footerBits = [...]byte{
|
||||
0, 0, 0, 0, 1, 1, 2, 2,
|
||||
3, 3, 4, 4, 5, 5, 6, 6,
|
||||
7, 7, 8, 8, 9, 9, 10, 10,
|
||||
11, 11, 12, 12, 13, 13, 14,
|
||||
}
|
||||
|
||||
var basePosition = [...]uint16{
|
||||
0, 1, 2, 3, 4, 6, 8, 12,
|
||||
16, 24, 32, 48, 64, 96, 128, 192,
|
||||
256, 384, 512, 768, 1024, 1536, 2048, 3072,
|
||||
4096, 6144, 8192, 12288, 16384, 24576, 32768,
|
||||
}
|
||||
|
||||
var (
|
||||
errCorrupt = errors.New("LZX data corrupt")
|
||||
)
|
||||
|
||||
// Reader is an interface used by the decompressor to access
|
||||
// the input stream. If the provided io.Reader does not implement
|
||||
// Reader, then a bufio.Reader is used.
|
||||
type Reader interface {
|
||||
io.Reader
|
||||
io.ByteReader
|
||||
}
|
||||
|
||||
type decompressor struct {
|
||||
r io.Reader
|
||||
err error
|
||||
unaligned bool
|
||||
nbits byte
|
||||
c uint32
|
||||
lru [3]uint16
|
||||
uncompressed int
|
||||
windowReader *bytes.Reader
|
||||
mainlens [maincodecount]byte
|
||||
lenlens [lencodecount]byte
|
||||
window [windowSize]byte
|
||||
b []byte
|
||||
bv int
|
||||
bo int
|
||||
}
|
||||
|
||||
//go:noinline
|
||||
func (f *decompressor) fail(err error) {
|
||||
if f.err == nil {
|
||||
f.err = err
|
||||
}
|
||||
f.bo = 0
|
||||
f.bv = 0
|
||||
}
|
||||
|
||||
func (f *decompressor) ensureAtLeast(n int) error {
|
||||
if f.bv-f.bo >= n {
|
||||
return nil
|
||||
}
|
||||
|
||||
if f.err != nil {
|
||||
return f.err
|
||||
}
|
||||
|
||||
if f.bv != f.bo {
|
||||
copy(f.b[:f.bv-f.bo], f.b[f.bo:f.bv])
|
||||
}
|
||||
n, err := io.ReadAtLeast(f.r, f.b[f.bv-f.bo:], n)
|
||||
if err != nil {
|
||||
if err == io.EOF { //nolint:errorlint
|
||||
err = io.ErrUnexpectedEOF
|
||||
} else {
|
||||
f.fail(err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
f.bv = f.bv - f.bo + n
|
||||
f.bo = 0
|
||||
return nil
|
||||
}
|
||||
|
||||
// feed retrieves another 16-bit word from the stream and consumes
|
||||
// it into f.c. It returns false if there are no more bytes available.
|
||||
// Otherwise, on error, it sets f.err.
|
||||
func (f *decompressor) feed() bool {
|
||||
err := f.ensureAtLeast(2)
|
||||
if err == io.ErrUnexpectedEOF { //nolint:errorlint // returns io.ErrUnexpectedEOF by contract
|
||||
return false
|
||||
}
|
||||
f.c |= (uint32(f.b[f.bo+1])<<8 | uint32(f.b[f.bo])) << (16 - f.nbits)
|
||||
f.nbits += 16
|
||||
f.bo += 2
|
||||
return true
|
||||
}
|
||||
|
||||
// getBits retrieves the next n bits from the byte stream. n
|
||||
// must be <= 16. It sets f.err on error.
|
||||
func (f *decompressor) getBits(n byte) uint16 {
|
||||
if f.nbits < n {
|
||||
if !f.feed() {
|
||||
f.fail(io.ErrUnexpectedEOF)
|
||||
}
|
||||
}
|
||||
c := uint16(f.c >> (32 - n))
|
||||
f.c <<= n
|
||||
f.nbits -= n
|
||||
return c
|
||||
}
|
||||
|
||||
type huffman struct {
|
||||
extra [][]uint16
|
||||
maxbits byte
|
||||
table [tablesize]uint16
|
||||
}
|
||||
|
||||
// buildTable builds a huffman decoding table from a slice of code lengths,
|
||||
// one per code, in order. Each code length must be <= maxTreePathLen.
|
||||
// See https://en.wikipedia.org/wiki/Canonical_Huffman_code.
|
||||
func buildTable(codelens []byte) *huffman {
|
||||
// Determine the number of codes of each length, and the
|
||||
// maximum length.
|
||||
var count [maxTreePathLen + 1]uint
|
||||
var max byte
|
||||
for _, cl := range codelens {
|
||||
count[cl]++
|
||||
if max < cl {
|
||||
max = cl
|
||||
}
|
||||
}
|
||||
|
||||
if max == 0 {
|
||||
return &huffman{}
|
||||
}
|
||||
|
||||
// Determine the first code of each length.
|
||||
var first [maxTreePathLen + 1]uint
|
||||
code := uint(0)
|
||||
for i := byte(1); i <= max; i++ {
|
||||
code <<= 1
|
||||
first[i] = code
|
||||
code += count[i]
|
||||
}
|
||||
|
||||
if code != 1<<max {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Build a table for code lookup. For code sizes < max,
|
||||
// put all possible suffixes for the code into the table, too.
|
||||
// For max > tablebits, split long codes into additional tables
|
||||
// of suffixes of max-tablebits length.
|
||||
h := &huffman{maxbits: max}
|
||||
if max > tablebits {
|
||||
core := first[tablebits+1] / 2 // Number of codes that fit without extra tables
|
||||
nextra := 1<<tablebits - core // Number of extra entries
|
||||
h.extra = make([][]uint16, nextra)
|
||||
for code := core; code < 1<<tablebits; code++ {
|
||||
h.table[code] = uint16(code - core)
|
||||
h.extra[code-core] = make([]uint16, 1<<(max-tablebits))
|
||||
}
|
||||
}
|
||||
|
||||
for i, cl := range codelens {
|
||||
if cl != 0 {
|
||||
code := first[cl]
|
||||
first[cl]++
|
||||
v := uint16(cl)<<lenshift | uint16(i)
|
||||
if cl <= tablebits {
|
||||
extendedCode := code << (tablebits - cl)
|
||||
for j := uint(0); j < 1<<(tablebits-cl); j++ {
|
||||
h.table[extendedCode+j] = v
|
||||
}
|
||||
} else {
|
||||
prefix := code >> (cl - tablebits)
|
||||
suffix := code & (1<<(cl-tablebits) - 1)
|
||||
extendedCode := suffix << (max - cl)
|
||||
for j := uint(0); j < 1<<(max-cl); j++ {
|
||||
h.extra[h.table[prefix]][extendedCode+j] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return h
|
||||
}
|
||||
|
||||
// getCode retrieves the next code using the provided
|
||||
// huffman tree. It sets f.err on error.
|
||||
func (f *decompressor) getCode(h *huffman) uint16 {
|
||||
if h.maxbits > 0 {
|
||||
if f.nbits < maxTreePathLen {
|
||||
f.feed()
|
||||
}
|
||||
|
||||
// For codes with length < tablebits, it doesn't matter
|
||||
// what the remainder of the bits used for table lookup
|
||||
// are, since entries with all possible suffixes were
|
||||
// added to the table.
|
||||
c := h.table[f.c>>(32-tablebits)]
|
||||
if !(c >= 1<<lenshift) {
|
||||
// The code is not in c.
|
||||
c = h.extra[c][f.c<<tablebits>>(32-(h.maxbits-tablebits))]
|
||||
}
|
||||
|
||||
n := byte(c >> lenshift)
|
||||
if f.nbits >= n {
|
||||
// Only consume the length of the code, not the maximum
|
||||
// code length.
|
||||
f.c <<= n
|
||||
f.nbits -= n
|
||||
return c & codemask
|
||||
}
|
||||
|
||||
f.fail(io.ErrUnexpectedEOF)
|
||||
return 0
|
||||
}
|
||||
|
||||
// This is an empty tree. It should not be used.
|
||||
f.fail(errCorrupt)
|
||||
return 0
|
||||
}
|
||||
|
||||
// readTree updates the huffman tree path lengths in lens by
|
||||
// reading and decoding lengths from the byte stream. lens
|
||||
// should be prepopulated with the previous block's tree's path
|
||||
// lengths. For the first block, lens should be zero.
|
||||
func (f *decompressor) readTree(lens []byte) error {
|
||||
// Get the pre-tree for the main tree.
|
||||
var pretreeLen [20]byte
|
||||
for i := range pretreeLen {
|
||||
pretreeLen[i] = byte(f.getBits(4))
|
||||
}
|
||||
if f.err != nil {
|
||||
return f.err
|
||||
}
|
||||
h := buildTable(pretreeLen[:])
|
||||
|
||||
// The lengths are encoded as a series of huffman codes
|
||||
// encoded by the pre-tree.
|
||||
for i := 0; i < len(lens); {
|
||||
c := byte(f.getCode(h))
|
||||
if f.err != nil {
|
||||
return f.err
|
||||
}
|
||||
switch {
|
||||
case c <= 16: // length is delta from previous length
|
||||
lens[i] = (lens[i] + 17 - c) % 17
|
||||
i++
|
||||
case c == 17: // next n + 4 lengths are zero
|
||||
zeroes := int(f.getBits(4)) + 4
|
||||
if i+zeroes > len(lens) {
|
||||
return errCorrupt
|
||||
}
|
||||
for j := 0; j < zeroes; j++ {
|
||||
lens[i+j] = 0
|
||||
}
|
||||
i += zeroes
|
||||
case c == 18: // next n + 20 lengths are zero
|
||||
zeroes := int(f.getBits(5)) + 20
|
||||
if i+zeroes > len(lens) {
|
||||
return errCorrupt
|
||||
}
|
||||
for j := 0; j < zeroes; j++ {
|
||||
lens[i+j] = 0
|
||||
}
|
||||
i += zeroes
|
||||
case c == 19: // next n + 4 lengths all have the same value
|
||||
same := int(f.getBits(1)) + 4
|
||||
if i+same > len(lens) {
|
||||
return errCorrupt
|
||||
}
|
||||
c = byte(f.getCode(h))
|
||||
if c > 16 {
|
||||
return errCorrupt
|
||||
}
|
||||
l := (lens[i] + 17 - c) % 17
|
||||
for j := 0; j < same; j++ {
|
||||
lens[i+j] = l
|
||||
}
|
||||
i += same
|
||||
default:
|
||||
return errCorrupt
|
||||
}
|
||||
}
|
||||
|
||||
if f.err != nil {
|
||||
return f.err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *decompressor) readBlockHeader() (byte, uint16, error) {
|
||||
// If the previous block was an unaligned uncompressed block, restore
|
||||
// 2-byte alignment.
|
||||
if f.unaligned {
|
||||
err := f.ensureAtLeast(1)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
f.bo++
|
||||
f.unaligned = false
|
||||
}
|
||||
|
||||
blockType := f.getBits(3)
|
||||
full := f.getBits(1)
|
||||
var blockSize uint16
|
||||
if full != 0 {
|
||||
blockSize = maxBlockSize
|
||||
} else {
|
||||
blockSize = f.getBits(16)
|
||||
if blockSize > maxBlockSize {
|
||||
return 0, 0, errCorrupt
|
||||
}
|
||||
}
|
||||
|
||||
if f.err != nil {
|
||||
return 0, 0, f.err
|
||||
}
|
||||
|
||||
switch blockType {
|
||||
case verbatimBlock, alignedOffsetBlock:
|
||||
// The caller will read the huffman trees.
|
||||
case uncompressedBlock:
|
||||
if f.nbits > 16 {
|
||||
panic("impossible: more than one 16-bit word remains")
|
||||
}
|
||||
|
||||
// Drop the remaining bits in the current 16-bit word
|
||||
// If there are no bits left, discard a full 16-bit word.
|
||||
n := f.nbits
|
||||
if n == 0 {
|
||||
n = 16
|
||||
}
|
||||
|
||||
f.getBits(n)
|
||||
|
||||
// Read the LRU values for the next block.
|
||||
err := f.ensureAtLeast(12)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
|
||||
f.lru[0] = uint16(binary.LittleEndian.Uint32(f.b[f.bo : f.bo+4]))
|
||||
f.lru[1] = uint16(binary.LittleEndian.Uint32(f.b[f.bo+4 : f.bo+8]))
|
||||
f.lru[2] = uint16(binary.LittleEndian.Uint32(f.b[f.bo+8 : f.bo+12]))
|
||||
f.bo += 12
|
||||
|
||||
default:
|
||||
return 0, 0, errCorrupt
|
||||
}
|
||||
|
||||
return byte(blockType), blockSize, nil
|
||||
}
|
||||
|
||||
// readTrees reads the two or three huffman trees for the current block.
|
||||
// readAligned specifies whether to read the aligned offset tree.
|
||||
func (f *decompressor) readTrees(readAligned bool) (main *huffman, length *huffman, aligned *huffman, err error) {
|
||||
// Aligned offset blocks start with a small aligned offset tree.
|
||||
if readAligned {
|
||||
var alignedLen [8]byte
|
||||
for i := range alignedLen {
|
||||
alignedLen[i] = byte(f.getBits(3))
|
||||
}
|
||||
aligned = buildTable(alignedLen[:])
|
||||
if aligned == nil {
|
||||
return main, length, aligned, errors.New("corrupt")
|
||||
}
|
||||
}
|
||||
|
||||
// The main tree is encoded in two parts.
|
||||
err = f.readTree(f.mainlens[:maincodesplit])
|
||||
if err != nil {
|
||||
return main, length, aligned, err
|
||||
}
|
||||
err = f.readTree(f.mainlens[maincodesplit:])
|
||||
if err != nil {
|
||||
return main, length, aligned, err
|
||||
}
|
||||
|
||||
main = buildTable(f.mainlens[:])
|
||||
if main == nil {
|
||||
return main, length, aligned, errors.New("corrupt")
|
||||
}
|
||||
|
||||
// The length tree is encoding in a single part.
|
||||
err = f.readTree(f.lenlens[:])
|
||||
if err != nil {
|
||||
return main, length, aligned, err
|
||||
}
|
||||
|
||||
length = buildTable(f.lenlens[:])
|
||||
if length == nil {
|
||||
return main, length, aligned, errors.New("corrupt")
|
||||
}
|
||||
|
||||
return main, length, aligned, f.err
|
||||
}
|
||||
|
||||
// readCompressedBlock decodes a compressed block, writing into the window
|
||||
// starting at start and ending at end, and using the provided huffman trees.
|
||||
func (f *decompressor) readCompressedBlock(start, end uint16, hmain, hlength, haligned *huffman) (int, error) {
|
||||
i := start
|
||||
for i < end {
|
||||
main := f.getCode(hmain)
|
||||
if f.err != nil {
|
||||
break
|
||||
}
|
||||
if main < 256 {
|
||||
// Literal byte.
|
||||
f.window[i] = byte(main)
|
||||
i++
|
||||
continue
|
||||
}
|
||||
|
||||
// This is a match backward in the window. Determine
|
||||
// the offset and dlength.
|
||||
matchlen := (main - 256) % 8
|
||||
slot := (main - 256) / 8
|
||||
|
||||
// The length is either the low bits of the code,
|
||||
// or if this is 7, is encoded with the length tree.
|
||||
if matchlen == 7 {
|
||||
matchlen += f.getCode(hlength)
|
||||
}
|
||||
matchlen += 2
|
||||
|
||||
var matchoffset uint16
|
||||
if slot < 3 { //nolint:nestif // todo: simplify nested complexity
|
||||
// The offset is one of the LRU values.
|
||||
matchoffset = f.lru[slot]
|
||||
f.lru[slot] = f.lru[0]
|
||||
f.lru[0] = matchoffset
|
||||
} else {
|
||||
// The offset is encoded as a combination of the
|
||||
// slot and more bits from the bit stream.
|
||||
offsetbits := footerBits[slot]
|
||||
var verbatimbits, alignedbits uint16
|
||||
if offsetbits > 0 {
|
||||
if haligned != nil && offsetbits >= 3 {
|
||||
// This is an aligned offset block. Combine
|
||||
// the bits written verbatim with the aligned
|
||||
// offset tree code.
|
||||
verbatimbits = f.getBits(offsetbits-3) * 8
|
||||
alignedbits = f.getCode(haligned)
|
||||
} else {
|
||||
// There are no aligned offset bits to read,
|
||||
// only verbatim bits.
|
||||
verbatimbits = f.getBits(offsetbits)
|
||||
alignedbits = 0
|
||||
}
|
||||
}
|
||||
matchoffset = basePosition[slot] + verbatimbits + alignedbits - 2
|
||||
// Update the LRU cache.
|
||||
f.lru[2] = f.lru[1]
|
||||
f.lru[1] = f.lru[0]
|
||||
f.lru[0] = matchoffset
|
||||
}
|
||||
|
||||
if !(matchoffset <= i && matchlen <= end-i) {
|
||||
f.fail(errCorrupt)
|
||||
break
|
||||
}
|
||||
copyend := i + matchlen
|
||||
for ; i < copyend; i++ {
|
||||
f.window[i] = f.window[i-matchoffset]
|
||||
}
|
||||
}
|
||||
return int(i - start), f.err
|
||||
}
|
||||
|
||||
// readBlock decodes the current block and returns the number of uncompressed bytes.
|
||||
func (f *decompressor) readBlock(start uint16) (int, error) {
|
||||
blockType, size, err := f.readBlockHeader()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
if blockType == uncompressedBlock {
|
||||
if size%2 == 1 {
|
||||
// Remember to realign the byte stream at the next block.
|
||||
f.unaligned = true
|
||||
}
|
||||
copied := 0
|
||||
if f.bo < f.bv {
|
||||
copied = int(size)
|
||||
s := int(start)
|
||||
if copied > f.bv-f.bo {
|
||||
copied = f.bv - f.bo
|
||||
}
|
||||
copy(f.window[s:s+copied], f.b[f.bo:f.bo+copied])
|
||||
f.bo += copied
|
||||
}
|
||||
n, err := io.ReadFull(f.r, f.window[start+uint16(copied):start+size])
|
||||
return copied + n, err
|
||||
}
|
||||
|
||||
hmain, hlength, haligned, err := f.readTrees(blockType == alignedOffsetBlock)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return f.readCompressedBlock(start, start+size, hmain, hlength, haligned)
|
||||
}
|
||||
|
||||
// decodeE8 reverses the 0xe8 x86 instruction encoding that was performed
|
||||
// to the uncompressed data before it was compressed.
|
||||
func decodeE8(b []byte, off int64) {
|
||||
if off > maxe8offset || len(b) < 10 {
|
||||
return
|
||||
}
|
||||
for i := 0; i < len(b)-10; i++ {
|
||||
if b[i] == 0xe8 {
|
||||
currentPtr := int32(off) + int32(i)
|
||||
abs := int32(binary.LittleEndian.Uint32(b[i+1 : i+5]))
|
||||
if abs >= -currentPtr && abs < e8filesize {
|
||||
var rel int32
|
||||
if abs >= 0 {
|
||||
rel = abs - currentPtr
|
||||
} else {
|
||||
rel = abs + e8filesize
|
||||
}
|
||||
binary.LittleEndian.PutUint32(b[i+1:i+5], uint32(rel))
|
||||
}
|
||||
i += 4
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (f *decompressor) Read(b []byte) (int, error) {
|
||||
// Read and uncompress everything.
|
||||
if f.windowReader == nil {
|
||||
n := 0
|
||||
for n < f.uncompressed {
|
||||
k, err := f.readBlock(uint16(n))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
n += k
|
||||
}
|
||||
decodeE8(f.window[:f.uncompressed], 0)
|
||||
f.windowReader = bytes.NewReader(f.window[:f.uncompressed])
|
||||
}
|
||||
|
||||
// Just read directly from the window.
|
||||
return f.windowReader.Read(b)
|
||||
}
|
||||
|
||||
func (*decompressor) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewReader returns a new io.ReadCloser that decompresses a
|
||||
// WIM LZX stream until uncompressedSize bytes have been returned.
|
||||
func NewReader(r io.Reader, uncompressedSize int) (io.ReadCloser, error) {
|
||||
if uncompressedSize > windowSize {
|
||||
return nil, errors.New("uncompressed size is limited to 32KB")
|
||||
}
|
||||
f := &decompressor{
|
||||
lru: [3]uint16{1, 1, 1},
|
||||
uncompressed: uncompressedSize,
|
||||
b: make([]byte, 4096),
|
||||
r: r,
|
||||
}
|
||||
return f, nil
|
||||
}
|
||||
+898
@@ -0,0 +1,898 @@
|
||||
//go:build windows || linux
|
||||
// +build windows linux
|
||||
|
||||
// Package wim implements a WIM file parser.
|
||||
//
|
||||
// WIM files are used to distribute Windows file system and container images.
|
||||
// They are documented at https://msdn.microsoft.com/en-us/library/windows/desktop/dd861280.aspx.
|
||||
package wim
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha1" //nolint:gosec // not used for secure application
|
||||
"encoding/binary"
|
||||
"encoding/xml"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
"unicode/utf16"
|
||||
)
|
||||
|
||||
// File attribute constants from Windows.
|
||||
//
|
||||
//nolint:revive // var-naming: ALL_CAPS
|
||||
const (
|
||||
FILE_ATTRIBUTE_READONLY = 0x00000001
|
||||
FILE_ATTRIBUTE_HIDDEN = 0x00000002
|
||||
FILE_ATTRIBUTE_SYSTEM = 0x00000004
|
||||
FILE_ATTRIBUTE_DIRECTORY = 0x00000010
|
||||
FILE_ATTRIBUTE_ARCHIVE = 0x00000020
|
||||
FILE_ATTRIBUTE_DEVICE = 0x00000040
|
||||
FILE_ATTRIBUTE_NORMAL = 0x00000080
|
||||
FILE_ATTRIBUTE_TEMPORARY = 0x00000100
|
||||
FILE_ATTRIBUTE_SPARSE_FILE = 0x00000200
|
||||
FILE_ATTRIBUTE_REPARSE_POINT = 0x00000400
|
||||
FILE_ATTRIBUTE_COMPRESSED = 0x00000800
|
||||
FILE_ATTRIBUTE_OFFLINE = 0x00001000
|
||||
FILE_ATTRIBUTE_NOT_CONTENT_INDEXED = 0x00002000
|
||||
FILE_ATTRIBUTE_ENCRYPTED = 0x00004000
|
||||
FILE_ATTRIBUTE_INTEGRITY_STREAM = 0x00008000
|
||||
FILE_ATTRIBUTE_VIRTUAL = 0x00010000
|
||||
FILE_ATTRIBUTE_NO_SCRUB_DATA = 0x00020000
|
||||
FILE_ATTRIBUTE_EA = 0x00040000
|
||||
)
|
||||
|
||||
// Windows processor architectures.
|
||||
//
|
||||
//nolint:revive // var-naming: ALL_CAPS
|
||||
const (
|
||||
PROCESSOR_ARCHITECTURE_INTEL = 0
|
||||
PROCESSOR_ARCHITECTURE_MIPS = 1
|
||||
PROCESSOR_ARCHITECTURE_ALPHA = 2
|
||||
PROCESSOR_ARCHITECTURE_PPC = 3
|
||||
PROCESSOR_ARCHITECTURE_SHX = 4
|
||||
PROCESSOR_ARCHITECTURE_ARM = 5
|
||||
PROCESSOR_ARCHITECTURE_IA64 = 6
|
||||
PROCESSOR_ARCHITECTURE_ALPHA64 = 7
|
||||
PROCESSOR_ARCHITECTURE_MSIL = 8
|
||||
PROCESSOR_ARCHITECTURE_AMD64 = 9
|
||||
PROCESSOR_ARCHITECTURE_IA32_ON_WIN64 = 10
|
||||
PROCESSOR_ARCHITECTURE_NEUTRAL = 11
|
||||
PROCESSOR_ARCHITECTURE_ARM64 = 12
|
||||
)
|
||||
|
||||
var wimImageTag = [...]byte{'M', 'S', 'W', 'I', 'M', 0, 0, 0}
|
||||
|
||||
// todo: replace this with pkg/guid.GUID (and add tests to make sure nothing breaks)
|
||||
|
||||
type guid struct {
|
||||
Data1 uint32
|
||||
Data2 uint16
|
||||
Data3 uint16
|
||||
Data4 [8]byte
|
||||
}
|
||||
|
||||
func (g guid) String() string {
|
||||
return fmt.Sprintf("%08x-%04x-%04x-%02x%02x-%02x%02x%02x%02x%02x%02x",
|
||||
g.Data1,
|
||||
g.Data2,
|
||||
g.Data3,
|
||||
g.Data4[0],
|
||||
g.Data4[1],
|
||||
g.Data4[2],
|
||||
g.Data4[3],
|
||||
g.Data4[4],
|
||||
g.Data4[5],
|
||||
g.Data4[6],
|
||||
g.Data4[7])
|
||||
}
|
||||
|
||||
type resourceDescriptor struct {
|
||||
FlagsAndCompressedSize uint64
|
||||
Offset int64
|
||||
OriginalSize int64
|
||||
}
|
||||
|
||||
type resFlag byte
|
||||
|
||||
//nolint:deadcode,varcheck // need unused variables for iota to work
|
||||
const (
|
||||
resFlagFree resFlag = 1 << iota
|
||||
resFlagMetadata
|
||||
resFlagCompressed
|
||||
resFlagSpanned
|
||||
)
|
||||
|
||||
const validate = false
|
||||
|
||||
const supportedResFlags = resFlagMetadata | resFlagCompressed
|
||||
|
||||
func (r *resourceDescriptor) Flags() resFlag {
|
||||
return resFlag(r.FlagsAndCompressedSize >> 56)
|
||||
}
|
||||
|
||||
func (r *resourceDescriptor) CompressedSize() int64 {
|
||||
return int64(r.FlagsAndCompressedSize & 0xffffffffffffff)
|
||||
}
|
||||
|
||||
func (r *resourceDescriptor) String() string {
|
||||
s := fmt.Sprintf("%d bytes at %d", r.CompressedSize(), r.Offset)
|
||||
if r.Flags()&4 != 0 {
|
||||
s += fmt.Sprintf(" (uncompresses to %d)", r.OriginalSize)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// SHA1Hash contains the SHA1 hash of a file or stream.
|
||||
type SHA1Hash [20]byte
|
||||
|
||||
type streamDescriptor struct {
|
||||
resourceDescriptor
|
||||
PartNumber uint16
|
||||
RefCount uint32
|
||||
Hash SHA1Hash
|
||||
}
|
||||
|
||||
type hdrFlag uint32
|
||||
|
||||
//nolint:deadcode,varcheck // need unused variables for iota to work
|
||||
const (
|
||||
hdrFlagReserved hdrFlag = 1 << iota
|
||||
hdrFlagCompressed
|
||||
hdrFlagReadOnly
|
||||
hdrFlagSpanned
|
||||
hdrFlagResourceOnly
|
||||
hdrFlagMetadataOnly
|
||||
hdrFlagWriteInProgress
|
||||
hdrFlagRpFix
|
||||
)
|
||||
|
||||
//nolint:deadcode,varcheck // need unused variables for iota to work
|
||||
const (
|
||||
hdrFlagCompressReserved hdrFlag = 1 << (iota + 16)
|
||||
hdrFlagCompressXpress
|
||||
hdrFlagCompressLzx
|
||||
)
|
||||
|
||||
const supportedHdrFlags = hdrFlagRpFix | hdrFlagReadOnly | hdrFlagCompressed | hdrFlagCompressLzx
|
||||
|
||||
type wimHeader struct {
|
||||
ImageTag [8]byte
|
||||
Size uint32
|
||||
Version uint32
|
||||
Flags hdrFlag
|
||||
CompressionSize uint32
|
||||
WIMGuid guid
|
||||
PartNumber uint16
|
||||
TotalParts uint16
|
||||
ImageCount uint32
|
||||
OffsetTable resourceDescriptor
|
||||
XMLData resourceDescriptor
|
||||
BootMetadata resourceDescriptor
|
||||
BootIndex uint32
|
||||
Padding uint32
|
||||
Integrity resourceDescriptor
|
||||
Unused [60]byte
|
||||
}
|
||||
|
||||
type securityblockDisk struct {
|
||||
TotalLength uint32
|
||||
NumEntries uint32
|
||||
}
|
||||
|
||||
const securityblockDiskSize = 8
|
||||
|
||||
type direntry struct {
|
||||
Attributes uint32
|
||||
SecurityID uint32
|
||||
SubdirOffset int64
|
||||
Unused1, Unused2 int64
|
||||
CreationTime Filetime
|
||||
LastAccessTime Filetime
|
||||
LastWriteTime Filetime
|
||||
Hash SHA1Hash
|
||||
Padding uint32
|
||||
ReparseHardLink int64
|
||||
StreamCount uint16
|
||||
ShortNameLength uint16
|
||||
FileNameLength uint16
|
||||
}
|
||||
|
||||
var direntrySize = int64(binary.Size(direntry{}) + 8) // includes an 8-byte length prefix
|
||||
|
||||
type streamentry struct {
|
||||
Unused int64
|
||||
Hash SHA1Hash
|
||||
NameLength int16
|
||||
}
|
||||
|
||||
var streamentrySize = int64(binary.Size(streamentry{}) + 8) // includes an 8-byte length prefix
|
||||
|
||||
// Filetime represents a Windows time.
|
||||
type Filetime struct {
|
||||
LowDateTime uint32
|
||||
HighDateTime uint32
|
||||
}
|
||||
|
||||
// Time returns the time as time.Time.
|
||||
func (ft *Filetime) Time() time.Time {
|
||||
// 100-nanosecond intervals since January 1, 1601
|
||||
nsec := int64(ft.HighDateTime)<<32 + int64(ft.LowDateTime)
|
||||
// change starting time to the Epoch (00:00:00 UTC, January 1, 1970)
|
||||
nsec -= 116444736000000000
|
||||
// convert into nanoseconds
|
||||
nsec *= 100
|
||||
return time.Unix(0, nsec)
|
||||
}
|
||||
|
||||
// UnmarshalXML unmarshalls the time from a WIM XML blob.
|
||||
func (ft *Filetime) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
||||
type Time struct {
|
||||
Low string `xml:"LOWPART"`
|
||||
High string `xml:"HIGHPART"`
|
||||
}
|
||||
var t Time
|
||||
err := d.DecodeElement(&t, &start)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
low, err := strconv.ParseUint(t.Low, 0, 32)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
high, err := strconv.ParseUint(t.High, 0, 32)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ft.LowDateTime = uint32(low)
|
||||
ft.HighDateTime = uint32(high)
|
||||
return nil
|
||||
}
|
||||
|
||||
type info struct {
|
||||
Image []ImageInfo `xml:"IMAGE"`
|
||||
}
|
||||
|
||||
// ImageInfo contains information about the image.
|
||||
type ImageInfo struct {
|
||||
Name string `xml:"NAME"`
|
||||
Index int `xml:"INDEX,attr"`
|
||||
CreationTime Filetime `xml:"CREATIONTIME"`
|
||||
ModTime Filetime `xml:"LASTMODIFICATIONTIME"`
|
||||
Windows *WindowsInfo `xml:"WINDOWS"`
|
||||
}
|
||||
|
||||
// WindowsInfo contains information about the Windows installation in the image.
|
||||
type WindowsInfo struct {
|
||||
Arch byte `xml:"ARCH"`
|
||||
ProductName string `xml:"PRODUCTNAME"`
|
||||
EditionID string `xml:"EDITIONID"`
|
||||
InstallationType string `xml:"INSTALLATIONTYPE"`
|
||||
ProductType string `xml:"PRODUCTTYPE"`
|
||||
Languages []string `xml:"LANGUAGES>LANGUAGE"`
|
||||
DefaultLanguage string `xml:"LANGUAGES>DEFAULT"`
|
||||
Version Version `xml:"VERSION"`
|
||||
SystemRoot string `xml:"SYSTEMROOT"`
|
||||
}
|
||||
|
||||
// Version represents a Windows build version.
|
||||
type Version struct {
|
||||
Major int `xml:"MAJOR"`
|
||||
Minor int `xml:"MINOR"`
|
||||
Build int `xml:"BUILD"`
|
||||
SPBuild int `xml:"SPBUILD"`
|
||||
SPLevel int `xml:"SPLEVEL"`
|
||||
}
|
||||
|
||||
// ParseError is returned when the WIM cannot be parsed.
|
||||
type ParseError struct {
|
||||
Oper string
|
||||
Path string
|
||||
Err error
|
||||
}
|
||||
|
||||
func (e *ParseError) Error() string {
|
||||
if e.Path == "" {
|
||||
return "WIM parse error at " + e.Oper + ": " + e.Err.Error()
|
||||
}
|
||||
return fmt.Sprintf("WIM parse error: %s %s: %s", e.Oper, e.Path, e.Err.Error())
|
||||
}
|
||||
|
||||
func (e *ParseError) Unwrap() error { return e.Err }
|
||||
|
||||
// Reader provides functions to read a WIM file.
|
||||
type Reader struct {
|
||||
hdr wimHeader
|
||||
r io.ReaderAt
|
||||
fileData map[SHA1Hash]resourceDescriptor
|
||||
|
||||
XMLInfo string // The XML information about the WIM.
|
||||
Image []*Image // The WIM's images.
|
||||
}
|
||||
|
||||
// Image represents an image within a WIM file.
|
||||
type Image struct {
|
||||
wim *Reader
|
||||
offset resourceDescriptor
|
||||
sds [][]byte
|
||||
rootOffset int64
|
||||
r io.ReadCloser
|
||||
curOffset int64
|
||||
m sync.Mutex
|
||||
|
||||
ImageInfo
|
||||
}
|
||||
|
||||
// StreamHeader contains alternate data stream metadata.
|
||||
type StreamHeader struct {
|
||||
Name string
|
||||
Hash SHA1Hash
|
||||
Size int64
|
||||
}
|
||||
|
||||
// Stream represents an alternate data stream or reparse point data stream.
|
||||
type Stream struct {
|
||||
StreamHeader
|
||||
wim *Reader
|
||||
offset resourceDescriptor
|
||||
}
|
||||
|
||||
// FileHeader contains file metadata.
|
||||
type FileHeader struct {
|
||||
Name string
|
||||
ShortName string
|
||||
Attributes uint32
|
||||
SecurityDescriptor []byte
|
||||
CreationTime Filetime
|
||||
LastAccessTime Filetime
|
||||
LastWriteTime Filetime
|
||||
Hash SHA1Hash
|
||||
Size int64
|
||||
LinkID int64
|
||||
ReparseTag uint32
|
||||
ReparseReserved uint32
|
||||
}
|
||||
|
||||
// File represents a file or directory in a WIM image.
|
||||
type File struct {
|
||||
FileHeader
|
||||
Streams []*Stream
|
||||
offset resourceDescriptor
|
||||
img *Image
|
||||
subdirOffset int64
|
||||
}
|
||||
|
||||
// NewReader returns a Reader that can be used to read WIM file data.
|
||||
func NewReader(f io.ReaderAt) (*Reader, error) {
|
||||
r := &Reader{r: f}
|
||||
section := io.NewSectionReader(f, 0, 0xffff)
|
||||
err := binary.Read(section, binary.LittleEndian, &r.hdr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if r.hdr.ImageTag != wimImageTag {
|
||||
return nil, &ParseError{Oper: "image tag", Err: errors.New("not a WIM file")}
|
||||
}
|
||||
|
||||
if r.hdr.Flags&^supportedHdrFlags != 0 {
|
||||
return nil, fmt.Errorf("unsupported WIM flags %x", r.hdr.Flags&^supportedHdrFlags)
|
||||
}
|
||||
|
||||
if r.hdr.CompressionSize != 0x8000 {
|
||||
return nil, fmt.Errorf("unsupported compression size %d", r.hdr.CompressionSize)
|
||||
}
|
||||
|
||||
if r.hdr.TotalParts != 1 {
|
||||
return nil, errors.New("multi-part WIM not supported")
|
||||
}
|
||||
|
||||
fileData, images, err := r.readOffsetTable(&r.hdr.OffsetTable)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
xmlinfo, err := r.readXML()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var inf info
|
||||
err = xml.Unmarshal([]byte(xmlinfo), &inf)
|
||||
if err != nil {
|
||||
return nil, &ParseError{Oper: "XML info", Err: err}
|
||||
}
|
||||
|
||||
for i, img := range images {
|
||||
for _, imgInfo := range inf.Image {
|
||||
if imgInfo.Index == i+1 {
|
||||
img.ImageInfo = imgInfo
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
r.fileData = fileData
|
||||
r.Image = images
|
||||
r.XMLInfo = xmlinfo
|
||||
return r, nil
|
||||
}
|
||||
|
||||
// Close releases resources associated with the Reader.
|
||||
func (r *Reader) Close() error {
|
||||
for _, img := range r.Image {
|
||||
img.reset()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Reader) resourceReader(hdr *resourceDescriptor) (io.ReadCloser, error) {
|
||||
return r.resourceReaderWithOffset(hdr, 0)
|
||||
}
|
||||
|
||||
func (r *Reader) resourceReaderWithOffset(hdr *resourceDescriptor, offset int64) (io.ReadCloser, error) {
|
||||
var sr io.ReadCloser
|
||||
section := io.NewSectionReader(r.r, hdr.Offset, hdr.CompressedSize())
|
||||
if hdr.Flags()&resFlagCompressed == 0 {
|
||||
_, _ = section.Seek(offset, 0)
|
||||
sr = io.NopCloser(section)
|
||||
} else {
|
||||
cr, err := newCompressedReader(section, hdr.OriginalSize, offset)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sr = cr
|
||||
}
|
||||
|
||||
return sr, nil
|
||||
}
|
||||
|
||||
func (r *Reader) readResource(hdr *resourceDescriptor) ([]byte, error) {
|
||||
rsrc, err := r.resourceReader(hdr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rsrc.Close()
|
||||
return io.ReadAll(rsrc)
|
||||
}
|
||||
|
||||
func (r *Reader) readXML() (string, error) {
|
||||
if r.hdr.XMLData.CompressedSize() == 0 {
|
||||
return "", nil
|
||||
}
|
||||
rsrc, err := r.resourceReader(&r.hdr.XMLData)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer rsrc.Close()
|
||||
|
||||
xmlData := make([]uint16, r.hdr.XMLData.OriginalSize/2)
|
||||
err = binary.Read(rsrc, binary.LittleEndian, xmlData)
|
||||
if err != nil {
|
||||
return "", &ParseError{Oper: "XML data", Err: err}
|
||||
}
|
||||
|
||||
// The BOM will always indicate little-endian UTF-16.
|
||||
if xmlData[0] != 0xfeff {
|
||||
return "", &ParseError{Oper: "XML data", Err: errors.New("invalid BOM")}
|
||||
}
|
||||
return string(utf16.Decode(xmlData[1:])), nil
|
||||
}
|
||||
|
||||
func (r *Reader) readOffsetTable(res *resourceDescriptor) (map[SHA1Hash]resourceDescriptor, []*Image, error) {
|
||||
fileData := make(map[SHA1Hash]resourceDescriptor)
|
||||
var images []*Image
|
||||
|
||||
offsetTable, err := r.readResource(res)
|
||||
if err != nil {
|
||||
return nil, nil, &ParseError{Oper: "offset table", Err: err}
|
||||
}
|
||||
|
||||
br := bytes.NewReader(offsetTable)
|
||||
for i := 0; ; i++ {
|
||||
var res streamDescriptor
|
||||
err := binary.Read(br, binary.LittleEndian, &res)
|
||||
if err == io.EOF { //nolint:errorlint
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return nil, nil, &ParseError{Oper: "offset table", Err: err}
|
||||
}
|
||||
if res.Flags()&^supportedResFlags != 0 {
|
||||
return nil, nil, &ParseError{Oper: "offset table", Err: errors.New("unsupported resource flag")}
|
||||
}
|
||||
|
||||
// Validation for ad-hoc testing
|
||||
if validate {
|
||||
sec, err := r.resourceReader(&res.resourceDescriptor)
|
||||
if err != nil {
|
||||
panic(fmt.Sprint(i, err))
|
||||
}
|
||||
hash := sha1.New() //nolint:gosec // not used for secure application
|
||||
_, err = io.Copy(hash, sec)
|
||||
sec.Close()
|
||||
if err != nil {
|
||||
panic(fmt.Sprint(i, err))
|
||||
}
|
||||
var cmphash SHA1Hash
|
||||
copy(cmphash[:], hash.Sum(nil))
|
||||
if cmphash != res.Hash {
|
||||
panic(fmt.Sprint(i, "hash mismatch"))
|
||||
}
|
||||
}
|
||||
|
||||
if res.Flags()&resFlagMetadata != 0 {
|
||||
image := &Image{
|
||||
wim: r,
|
||||
offset: res.resourceDescriptor,
|
||||
}
|
||||
images = append(images, image)
|
||||
} else {
|
||||
fileData[res.Hash] = res.resourceDescriptor
|
||||
}
|
||||
}
|
||||
|
||||
if len(images) != int(r.hdr.ImageCount) {
|
||||
return nil, nil, &ParseError{Oper: "offset table", Err: errors.New("mismatched image count")}
|
||||
}
|
||||
|
||||
return fileData, images, nil
|
||||
}
|
||||
|
||||
func (*Reader) readSecurityDescriptors(rsrc io.Reader) (sds [][]byte, n int64, err error) {
|
||||
var secBlock securityblockDisk
|
||||
err = binary.Read(rsrc, binary.LittleEndian, &secBlock)
|
||||
if err != nil {
|
||||
return sds, 0, &ParseError{Oper: "security table", Err: err}
|
||||
}
|
||||
|
||||
n += securityblockDiskSize
|
||||
|
||||
secSizes := make([]int64, secBlock.NumEntries)
|
||||
err = binary.Read(rsrc, binary.LittleEndian, &secSizes)
|
||||
if err != nil {
|
||||
return sds, n, &ParseError{Oper: "security table sizes", Err: err}
|
||||
}
|
||||
|
||||
n += int64(secBlock.NumEntries * 8)
|
||||
|
||||
sds = make([][]byte, secBlock.NumEntries)
|
||||
for i, size := range secSizes {
|
||||
sd := make([]byte, size&0xffffffff)
|
||||
_, err = io.ReadFull(rsrc, sd)
|
||||
if err != nil {
|
||||
return sds, n, &ParseError{Oper: "security descriptor", Err: err}
|
||||
}
|
||||
n += int64(len(sd))
|
||||
sds[i] = sd
|
||||
}
|
||||
|
||||
secsize := int64((secBlock.TotalLength + 7) &^ 7)
|
||||
if n > secsize {
|
||||
return sds, n, &ParseError{Oper: "security descriptor", Err: errors.New("security descriptor table too small")}
|
||||
}
|
||||
|
||||
_, err = io.CopyN(io.Discard, rsrc, secsize-n)
|
||||
if err != nil {
|
||||
return sds, n, err
|
||||
}
|
||||
|
||||
n = secsize
|
||||
return sds, n, nil
|
||||
}
|
||||
|
||||
// Open parses the image and returns the root directory.
|
||||
func (img *Image) Open() (*File, error) {
|
||||
if img.sds == nil {
|
||||
rsrc, err := img.wim.resourceReaderWithOffset(&img.offset, img.rootOffset)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sds, n, err := img.wim.readSecurityDescriptors(rsrc)
|
||||
if err != nil {
|
||||
rsrc.Close()
|
||||
return nil, err
|
||||
}
|
||||
img.sds = sds
|
||||
img.r = rsrc
|
||||
img.rootOffset = n
|
||||
img.curOffset = n
|
||||
}
|
||||
|
||||
f, err := img.readdir(img.rootOffset)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(f) != 1 {
|
||||
return nil, &ParseError{Oper: "root directory", Err: errors.New("expected exactly 1 root directory entry")}
|
||||
}
|
||||
return f[0], err
|
||||
}
|
||||
|
||||
func (img *Image) reset() {
|
||||
if img.r != nil {
|
||||
img.r.Close()
|
||||
img.r = nil
|
||||
}
|
||||
img.curOffset = -1
|
||||
}
|
||||
|
||||
func (img *Image) readdir(offset int64) ([]*File, error) {
|
||||
img.m.Lock()
|
||||
defer img.m.Unlock()
|
||||
|
||||
if offset < img.curOffset || offset > img.curOffset+chunkSize {
|
||||
// Reset to seek backward or to seek forward very far.
|
||||
img.reset()
|
||||
}
|
||||
if img.r == nil {
|
||||
rsrc, err := img.wim.resourceReaderWithOffset(&img.offset, offset)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
img.r = rsrc
|
||||
img.curOffset = offset
|
||||
}
|
||||
if offset > img.curOffset {
|
||||
_, err := io.CopyN(io.Discard, img.r, offset-img.curOffset)
|
||||
if err != nil {
|
||||
img.reset()
|
||||
if err == io.EOF { //nolint:errorlint
|
||||
err = io.ErrUnexpectedEOF
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
var entries []*File
|
||||
for {
|
||||
e, n, err := img.readNextEntry(img.r)
|
||||
img.curOffset += n
|
||||
if err == io.EOF { //nolint:errorlint
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
img.reset()
|
||||
return nil, err
|
||||
}
|
||||
entries = append(entries, e)
|
||||
}
|
||||
return entries, nil
|
||||
}
|
||||
|
||||
func (img *Image) readNextEntry(r io.Reader) (*File, int64, error) {
|
||||
var length int64
|
||||
err := binary.Read(r, binary.LittleEndian, &length)
|
||||
if err != nil {
|
||||
return nil, 0, &ParseError{Oper: "directory length check", Err: err}
|
||||
}
|
||||
|
||||
if length == 0 {
|
||||
return nil, 8, io.EOF
|
||||
}
|
||||
|
||||
left := length
|
||||
if left < direntrySize {
|
||||
return nil, 0, &ParseError{Oper: "directory entry", Err: errors.New("size too short")}
|
||||
}
|
||||
|
||||
var dentry direntry
|
||||
err = binary.Read(r, binary.LittleEndian, &dentry)
|
||||
if err != nil {
|
||||
return nil, 0, &ParseError{Oper: "directory entry", Err: err}
|
||||
}
|
||||
|
||||
left -= direntrySize
|
||||
|
||||
namesLen := int64(dentry.FileNameLength + 2 + dentry.ShortNameLength)
|
||||
if left < namesLen {
|
||||
return nil, 0, &ParseError{Oper: "directory entry", Err: errors.New("size too short for names")}
|
||||
}
|
||||
|
||||
names := make([]uint16, namesLen/2)
|
||||
err = binary.Read(r, binary.LittleEndian, names)
|
||||
if err != nil {
|
||||
return nil, 0, &ParseError{Oper: "file name", Err: err}
|
||||
}
|
||||
|
||||
left -= namesLen
|
||||
|
||||
var name, shortName string
|
||||
if dentry.FileNameLength > 0 {
|
||||
name = string(utf16.Decode(names[:dentry.FileNameLength/2]))
|
||||
}
|
||||
|
||||
if dentry.ShortNameLength > 0 {
|
||||
shortName = string(utf16.Decode(names[dentry.FileNameLength/2+1:]))
|
||||
}
|
||||
|
||||
var offset resourceDescriptor
|
||||
zerohash := SHA1Hash{}
|
||||
if dentry.Hash != zerohash {
|
||||
var ok bool
|
||||
offset, ok = img.wim.fileData[dentry.Hash]
|
||||
if !ok {
|
||||
return nil, 0, &ParseError{
|
||||
Oper: "directory entry",
|
||||
Path: name,
|
||||
Err: fmt.Errorf("could not find file data matching hash %#v", dentry),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
f := &File{
|
||||
FileHeader: FileHeader{
|
||||
Attributes: dentry.Attributes,
|
||||
CreationTime: dentry.CreationTime,
|
||||
LastAccessTime: dentry.LastAccessTime,
|
||||
LastWriteTime: dentry.LastWriteTime,
|
||||
Hash: dentry.Hash,
|
||||
Size: offset.OriginalSize,
|
||||
Name: name,
|
||||
ShortName: shortName,
|
||||
},
|
||||
|
||||
offset: offset,
|
||||
img: img,
|
||||
subdirOffset: dentry.SubdirOffset,
|
||||
}
|
||||
|
||||
isDir := false
|
||||
|
||||
if dentry.Attributes&FILE_ATTRIBUTE_REPARSE_POINT == 0 {
|
||||
f.LinkID = dentry.ReparseHardLink
|
||||
if dentry.Attributes&FILE_ATTRIBUTE_DIRECTORY != 0 {
|
||||
isDir = true
|
||||
}
|
||||
} else {
|
||||
f.ReparseTag = uint32(dentry.ReparseHardLink)
|
||||
f.ReparseReserved = uint32(dentry.ReparseHardLink >> 32)
|
||||
}
|
||||
|
||||
if isDir && f.subdirOffset == 0 {
|
||||
return nil, 0, &ParseError{Oper: "directory entry", Path: name, Err: errors.New("no subdirectory data for directory")}
|
||||
} else if !isDir && f.subdirOffset != 0 {
|
||||
return nil, 0, &ParseError{Oper: "directory entry", Path: name, Err: errors.New("unexpected subdirectory data for non-directory")}
|
||||
}
|
||||
|
||||
if dentry.SecurityID != 0xffffffff {
|
||||
f.SecurityDescriptor = img.sds[dentry.SecurityID]
|
||||
}
|
||||
|
||||
_, err = io.CopyN(io.Discard, r, left)
|
||||
if err != nil {
|
||||
if err == io.EOF { //nolint:errorlint
|
||||
err = io.ErrUnexpectedEOF
|
||||
}
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
if dentry.StreamCount > 0 {
|
||||
var streams []*Stream
|
||||
for i := uint16(0); i < dentry.StreamCount; i++ {
|
||||
s, n, err := img.readNextStream(r)
|
||||
length += n
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
// The first unnamed stream should be treated as the file stream.
|
||||
if i == 0 && s.Name == "" {
|
||||
f.Hash = s.Hash
|
||||
f.Size = s.Size
|
||||
f.offset = s.offset
|
||||
} else if s.Name != "" {
|
||||
streams = append(streams, s)
|
||||
}
|
||||
}
|
||||
f.Streams = streams
|
||||
}
|
||||
|
||||
if dentry.Attributes&FILE_ATTRIBUTE_REPARSE_POINT != 0 && f.Size == 0 {
|
||||
return nil, 0, &ParseError{
|
||||
Oper: "directory entry",
|
||||
Path: name,
|
||||
Err: errors.New("reparse point is missing reparse stream"),
|
||||
}
|
||||
}
|
||||
|
||||
return f, length, nil
|
||||
}
|
||||
|
||||
func (img *Image) readNextStream(r io.Reader) (*Stream, int64, error) {
|
||||
var length int64
|
||||
err := binary.Read(r, binary.LittleEndian, &length)
|
||||
if err != nil {
|
||||
if err == io.EOF { //nolint:errorlint
|
||||
err = io.ErrUnexpectedEOF
|
||||
}
|
||||
return nil, 0, &ParseError{Oper: "stream length check", Err: err}
|
||||
}
|
||||
|
||||
left := length
|
||||
if left < streamentrySize {
|
||||
return nil, 0, &ParseError{Oper: "stream entry", Err: errors.New("size too short")}
|
||||
}
|
||||
|
||||
var sentry streamentry
|
||||
err = binary.Read(r, binary.LittleEndian, &sentry)
|
||||
if err != nil {
|
||||
return nil, 0, &ParseError{Oper: "stream entry", Err: err}
|
||||
}
|
||||
|
||||
left -= streamentrySize
|
||||
|
||||
if left < int64(sentry.NameLength) {
|
||||
return nil, 0, &ParseError{Oper: "stream entry", Err: errors.New("size too short for name")}
|
||||
}
|
||||
|
||||
names := make([]uint16, sentry.NameLength/2)
|
||||
err = binary.Read(r, binary.LittleEndian, names)
|
||||
if err != nil {
|
||||
return nil, 0, &ParseError{Oper: "file name", Err: err}
|
||||
}
|
||||
|
||||
left -= int64(sentry.NameLength)
|
||||
name := string(utf16.Decode(names))
|
||||
|
||||
var offset resourceDescriptor
|
||||
if sentry.Hash != (SHA1Hash{}) {
|
||||
var ok bool
|
||||
offset, ok = img.wim.fileData[sentry.Hash]
|
||||
if !ok {
|
||||
return nil, 0, &ParseError{
|
||||
Oper: "stream entry",
|
||||
Path: name,
|
||||
Err: fmt.Errorf("could not find file data matching hash %v", sentry.Hash),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
s := &Stream{
|
||||
StreamHeader: StreamHeader{
|
||||
Hash: sentry.Hash,
|
||||
Size: offset.OriginalSize,
|
||||
Name: name,
|
||||
},
|
||||
wim: img.wim,
|
||||
offset: offset,
|
||||
}
|
||||
|
||||
_, err = io.CopyN(io.Discard, r, left)
|
||||
if err != nil {
|
||||
if err == io.EOF { //nolint:errorlint
|
||||
err = io.ErrUnexpectedEOF
|
||||
}
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
return s, length, nil
|
||||
}
|
||||
|
||||
// Open returns an io.ReadCloser that can be used to read the stream's contents.
|
||||
func (s *Stream) Open() (io.ReadCloser, error) {
|
||||
return s.wim.resourceReader(&s.offset)
|
||||
}
|
||||
|
||||
// Open returns an io.ReadCloser that can be used to read the file's contents.
|
||||
func (f *File) Open() (io.ReadCloser, error) {
|
||||
return f.img.wim.resourceReader(&f.offset)
|
||||
}
|
||||
|
||||
// Readdir reads the directory entries.
|
||||
func (f *File) Readdir() ([]*File, error) {
|
||||
if !f.IsDir() {
|
||||
return nil, errors.New("not a directory")
|
||||
}
|
||||
return f.img.readdir(f.subdirOffset)
|
||||
}
|
||||
|
||||
// IsDir returns whether the given file is a directory. It returns false when it
|
||||
// is a directory reparse point.
|
||||
func (f *FileHeader) IsDir() bool {
|
||||
return f.Attributes&(FILE_ATTRIBUTE_DIRECTORY|FILE_ATTRIBUTE_REPARSE_POINT) == FILE_ATTRIBUTE_DIRECTORY
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
Copyright (c) 2019-2020, Kamil Domański and contributors
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
|
||||
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
+161
@@ -0,0 +1,161 @@
|
||||
## iso9660
|
||||
[](https://pkg.go.dev/github.com/kdomanski/iso9660)
|
||||
[](https://codecov.io/gh/kdomanski/iso9660)
|
||||
[](https://goreportcard.com/report/github.com/kdomanski/iso9660)
|
||||
|
||||
A package for reading and creating ISO9660
|
||||
|
||||
Joliet extension is **NOT** supported.
|
||||
|
||||
Experimental support for reading Rock Ridge extension is currently in the works.
|
||||
If you are experiencing issues, please use the v0.3 release, which ignores Rock Ridge.
|
||||
|
||||
## References for the format:
|
||||
- [ECMA-119 1st edition (December 1986)](https://www.ecma-international.org/wp-content/uploads/ECMA-119_1st_edition_december_1986.pdf) ([Web Archive link](http://web.archive.org/web/20210122025258/https://www.ecma-international.org/wp-content/uploads/ECMA-119_1st_edition_december_1986.pdf))
|
||||
- [ECMA-119 2nd edition (December 1987)](https://www.ecma-international.org/wp-content/uploads/ECMA-119_2nd_edition_december_1987.pdf) ([Web Archive link](http://web.archive.org/web/20210418211711/https://www.ecma-international.org/wp-content/uploads/ECMA-119_2nd_edition_december_1987.pdf))
|
||||
- [ECMA-119 3rd edition (December 2017)](https://www.ecma-international.org/wp-content/uploads/ECMA-119_3rd_edition_december_2017.pdf) ([Web Archive link](http://web.archive.org/web/20210527165925/https://www.ecma-international.org/wp-content/uploads/ECMA-119_3rd_edition_december_2017.pdf))
|
||||
- [ECMA-119 4th edition (June 2019)](https://www.ecma-international.org/wp-content/uploads/ECMA-119_4th_edition_june_2019.pdf) ([Web Archive link](https://www.ecma-international.org/wp-content/uploads/ECMA-119_4th_edition_june_2019.pdf))
|
||||
- [Rock Ridge Interchange Protocol](http://www.nextcomputers.org/NeXTfiles/Projects/CD-ROM/Rock_Ridge_Interchange_Protocol.pdf) ([Web Archive link](http://web.archive.org/web/20071017082049/http://www.nextcomputers.org/NeXTfiles/Projects/CD-ROM/Rock_Ridge_Interchange_Protocol.pdf))
|
||||
- [System Use Sharing Protocol v1.12](http://aminet.net/package/docs/misc/RRIP)
|
||||
|
||||
## Examples
|
||||
|
||||
### Extracting an ISO
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"github.com/kdomanski/iso9660/util"
|
||||
)
|
||||
|
||||
func main() {
|
||||
f, err := os.Open("/home/user/myImage.iso")
|
||||
if err != nil {
|
||||
log.Fatalf("failed to open file: %s", err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
if err = util.ExtractImageToDirectory(f, "/home/user/target_dir"); err != nil {
|
||||
log.Fatalf("failed to extract image: %s", err)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Creating an ISO
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/kdomanski/iso9660"
|
||||
)
|
||||
|
||||
func main() {
|
||||
writer, err := iso9660.NewWriter()
|
||||
if err != nil {
|
||||
log.Fatalf("failed to create writer: %s", err)
|
||||
}
|
||||
defer writer.Cleanup()
|
||||
|
||||
f, err := os.Open("/home/user/myFile.txt")
|
||||
if err != nil {
|
||||
log.Fatalf("failed to open file: %s", err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
err = writer.AddFile(f, "folder/MYFILE.TXT")
|
||||
if err != nil {
|
||||
log.Fatalf("failed to add file: %s", err)
|
||||
}
|
||||
|
||||
outputFile, err := os.OpenFile("/home/user/output.iso", os.O_WRONLY | os.O_TRUNC | os.O_CREATE, 0644)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to create file: %s", err)
|
||||
}
|
||||
|
||||
err = writer.WriteTo(outputFile, "testvol")
|
||||
if err != nil {
|
||||
log.Fatalf("failed to write ISO image: %s", err)
|
||||
}
|
||||
|
||||
err = outputFile.Close()
|
||||
if err != nil {
|
||||
log.Fatalf("failed to close output file: %s", err)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Recursively create an ISO image from the given directories
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/kdomanski/iso9660"
|
||||
)
|
||||
|
||||
func main() {
|
||||
writer, err := iso9660.NewWriter()
|
||||
if err != nil {
|
||||
log.Fatalf("failed to create writer: %s", err)
|
||||
}
|
||||
defer writer.Cleanup()
|
||||
|
||||
isoFile, err := os.OpenFile("C:/output.iso", os.O_WRONLY|os.O_TRUNC|os.O_CREATE, 0644)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to create file: %s", err)
|
||||
}
|
||||
defer isoFile.Close()
|
||||
|
||||
prefix := "F:\\" // the prefix to remove in the output iso file
|
||||
sourceFolders := []string{"F:\\test1", "F:\\test2"} // the given directories to create an ISO file from
|
||||
|
||||
for _, folderName := range sourceFolders {
|
||||
folderPath := strings.Join([]string{prefix, folderName}, "/")
|
||||
|
||||
walk_err := filepath.Walk(folderPath, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
log.Fatalf("walk: %s", err)
|
||||
return err
|
||||
}
|
||||
if info.IsDir() {
|
||||
return nil
|
||||
}
|
||||
outputPath := strings.TrimPrefix(path, prefix) // remove the source drive name
|
||||
fmt.Printf("Adding file: %s\n", outputPath)
|
||||
|
||||
fileToAdd, err := os.Open(path)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to open file: %s", err)
|
||||
}
|
||||
defer fileToAdd.Close()
|
||||
|
||||
err = writer.AddFile(fileToAdd, outputPath)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to add file: %s", err)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if walk_err != nil {
|
||||
log.Fatalf("%s", walk_err)
|
||||
}
|
||||
}
|
||||
|
||||
err = writer.WriteTo(isoFile, "Test")
|
||||
if err != nil {
|
||||
log.Fatalf("failed to write ISO image: %s", err)
|
||||
}
|
||||
}
|
||||
```
|
||||
+292
@@ -0,0 +1,292 @@
|
||||
package iso9660
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Image is a wrapper around an image file that allows reading its ISO9660 data
|
||||
type Image struct {
|
||||
ra io.ReaderAt
|
||||
volumeDescriptors []volumeDescriptor
|
||||
}
|
||||
|
||||
// OpenImage returns an Image reader reating from a given file
|
||||
func OpenImage(ra io.ReaderAt) (*Image, error) {
|
||||
i := &Image{ra: ra}
|
||||
|
||||
if err := i.readVolumes(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return i, nil
|
||||
}
|
||||
|
||||
func (i *Image) readVolumes() error {
|
||||
buffer := make([]byte, sectorSize)
|
||||
// skip the 16 sectors of system area
|
||||
for sector := 16; ; sector++ {
|
||||
if _, err := i.ra.ReadAt(buffer, int64(sector)*int64(sectorSize)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var vd volumeDescriptor
|
||||
if err := vd.UnmarshalBinary(buffer); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// NOTE: the instance of the root Directory Record that appears
|
||||
// in the Primary Volume Descriptor cannot contain a System Use
|
||||
// field. See the SUSP standard.
|
||||
|
||||
i.volumeDescriptors = append(i.volumeDescriptors, vd)
|
||||
if vd.Header.Type == volumeTypeTerminator {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RootDir returns the File structure corresponding to the root directory
|
||||
// of the first primary volume
|
||||
func (i *Image) RootDir() (*File, error) {
|
||||
for _, vd := range i.volumeDescriptors {
|
||||
if vd.Type() == volumeTypePrimary {
|
||||
return &File{de: vd.Primary.RootDirectoryEntry, ra: i.ra, children: nil, isRootDir: true}, nil
|
||||
}
|
||||
}
|
||||
return nil, os.ErrNotExist
|
||||
}
|
||||
|
||||
// RootDir returns the label of the first Primary Volume
|
||||
func (i *Image) Label() (string, error) {
|
||||
for _, vd := range i.volumeDescriptors {
|
||||
if vd.Type() == volumeTypePrimary {
|
||||
return string(vd.Primary.VolumeIdentifier), nil
|
||||
}
|
||||
}
|
||||
return "", os.ErrNotExist
|
||||
}
|
||||
|
||||
// File is a os.FileInfo-compatible wrapper around an ISO9660 directory entry
|
||||
type File struct {
|
||||
ra io.ReaderAt
|
||||
de *DirectoryEntry
|
||||
children []*File
|
||||
isRootDir bool
|
||||
susp *SUSPMetadata
|
||||
}
|
||||
|
||||
var _ os.FileInfo = &File{}
|
||||
|
||||
func (f *File) hasRockRidge() bool {
|
||||
return f.susp != nil && f.susp.HasRockRidge
|
||||
}
|
||||
|
||||
// IsDir returns true if the entry is a directory or false otherwise
|
||||
func (f *File) IsDir() bool {
|
||||
if f.hasRockRidge() {
|
||||
if mode, err := f.de.SystemUseEntries.GetPosixAttr(); err == nil {
|
||||
return mode&os.ModeDir != 0
|
||||
}
|
||||
}
|
||||
|
||||
return f.de.FileFlags&dirFlagDir != 0
|
||||
}
|
||||
|
||||
// ModTime returns the entry's recording time
|
||||
func (f *File) ModTime() time.Time {
|
||||
return time.Time(f.de.RecordingDateTime)
|
||||
}
|
||||
|
||||
// Mode returns file mode when available.
|
||||
// Otherwise it returns os.FileMode flag set with the os.ModeDir flag enabled in case of directories.
|
||||
func (f *File) Mode() os.FileMode {
|
||||
if f.hasRockRidge() {
|
||||
if mode, err := f.de.SystemUseEntries.GetPosixAttr(); err == nil {
|
||||
return mode
|
||||
}
|
||||
}
|
||||
|
||||
var mode os.FileMode
|
||||
if f.IsDir() {
|
||||
mode |= os.ModeDir
|
||||
}
|
||||
return mode
|
||||
}
|
||||
|
||||
// Name returns the base name of the given entry
|
||||
func (f *File) Name() string {
|
||||
if f.hasRockRidge() {
|
||||
if name := f.de.SystemUseEntries.GetRockRidgeName(); name != "" {
|
||||
return name
|
||||
}
|
||||
}
|
||||
|
||||
if f.IsDir() {
|
||||
return f.de.Identifier
|
||||
}
|
||||
|
||||
// drop the version part
|
||||
// assume only one ';'
|
||||
fileIdentifier := strings.Split(f.de.Identifier, ";")[0]
|
||||
|
||||
// split into filename and extension
|
||||
// assume only only one '.'
|
||||
splitFileIdentifier := strings.Split(fileIdentifier, ".")
|
||||
|
||||
// there's no dot in the name, thus no extension
|
||||
if len(splitFileIdentifier) == 1 {
|
||||
return splitFileIdentifier[0]
|
||||
}
|
||||
|
||||
// extension is empty, return just the name without a dot
|
||||
if len(splitFileIdentifier[1]) == 0 {
|
||||
return splitFileIdentifier[0]
|
||||
}
|
||||
|
||||
// return file with extension
|
||||
return fileIdentifier
|
||||
}
|
||||
|
||||
// Size returns the size in bytes of the extent occupied by the file or directory
|
||||
func (f *File) Size() int64 {
|
||||
return int64(f.de.ExtentLength)
|
||||
}
|
||||
|
||||
// Sys returns nil
|
||||
func (f *File) Sys() interface{} {
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetAllChildren returns the children entries in case of a directory
|
||||
// or an error in case of a file. It includes the "." and ".." entries.
|
||||
func (f *File) GetAllChildren() ([]*File, error) {
|
||||
if !f.IsDir() {
|
||||
return nil, fmt.Errorf("%s is not a directory", f.Name())
|
||||
}
|
||||
|
||||
if f.children != nil {
|
||||
return f.children, nil
|
||||
}
|
||||
|
||||
baseOffset := uint32(f.de.ExtentLocation) * sectorSize
|
||||
|
||||
buffer := make([]byte, sectorSize)
|
||||
for bytesProcessed := uint32(0); bytesProcessed < uint32(f.de.ExtentLength); bytesProcessed += sectorSize {
|
||||
if _, err := f.ra.ReadAt(buffer, int64(baseOffset+bytesProcessed)); err != nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
for i := uint32(0); i < sectorSize; {
|
||||
entryLength := uint32(buffer[i])
|
||||
if entryLength == 0 {
|
||||
break
|
||||
}
|
||||
|
||||
if i+entryLength > sectorSize {
|
||||
return nil, fmt.Errorf("reading directory entries: DE outside of sector boundries")
|
||||
}
|
||||
|
||||
newDE := &DirectoryEntry{}
|
||||
if err := newDE.UnmarshalBinary(buffer[i : i+entryLength]); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Is this a root directory '.' record?
|
||||
if f.isRootDir && newDE.Identifier == string([]byte{0}) {
|
||||
newDE.SystemUseEntries, _ = splitSystemUseEntries(newDE.SystemUse, f.ra)
|
||||
|
||||
// get the SP record
|
||||
if len(newDE.SystemUseEntries) > 0 && newDE.SystemUseEntries[0].Type() == "SP" {
|
||||
sprecord, err := SPRecordDecode(newDE.SystemUseEntries[0])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid SP record: %w", err)
|
||||
}
|
||||
|
||||
hasRockRidge, err := suspHasRockRidge(newDE.SystemUseEntries)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to check for Rock Ridge extension: %w", err)
|
||||
}
|
||||
|
||||
// save SUSP offset from the SP record
|
||||
f.susp = &SUSPMetadata{
|
||||
Offset: sprecord.BytesSkipped,
|
||||
HasRockRidge: hasRockRidge,
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// are we on a volume with SUSP?
|
||||
if f.susp != nil {
|
||||
// Ignore error if some of the SUSP data is malformed. Just take the valid part.
|
||||
offsetSystemUse := newDE.SystemUse[f.susp.Offset:]
|
||||
newDE.SystemUseEntries, _ = splitSystemUseEntries(offsetSystemUse, f.ra)
|
||||
}
|
||||
}
|
||||
|
||||
i += entryLength
|
||||
|
||||
newFile := &File{ra: f.ra,
|
||||
de: newDE,
|
||||
children: nil,
|
||||
susp: f.susp.Clone(),
|
||||
}
|
||||
|
||||
f.children = append(f.children, newFile)
|
||||
}
|
||||
}
|
||||
|
||||
return f.children, nil
|
||||
}
|
||||
|
||||
// GetChildren returns the children entries in case of a directory
|
||||
// or an error in case of a file. It does NOT include the "." and ".." entries.
|
||||
func (f *File) GetChildren() ([]*File, error) {
|
||||
children, err := f.GetAllChildren()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
filteredChildren := make([]*File, 0, len(children)-2)
|
||||
for _, child := range children {
|
||||
if child.de.Identifier == string([]byte{0}) || child.de.Identifier == string([]byte{1}) {
|
||||
continue
|
||||
}
|
||||
|
||||
filteredChildren = append(filteredChildren, child)
|
||||
}
|
||||
|
||||
return filteredChildren, nil
|
||||
}
|
||||
|
||||
// GetDotEntry returns the "." entry of a directory
|
||||
// or an error in case of a file.
|
||||
func (f *File) GetDotEntry() (*File, error) {
|
||||
children, err := f.GetAllChildren()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, child := range children {
|
||||
if child.de.Identifier == string([]byte{0}) {
|
||||
return child, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Reader returns a reader that allows to read the file's data.
|
||||
// If File is a directory, it returns nil.
|
||||
func (f *File) Reader() io.Reader {
|
||||
if f.IsDir() {
|
||||
return nil
|
||||
}
|
||||
|
||||
baseOffset := int64(f.de.ExtentLocation) * int64(sectorSize)
|
||||
return io.NewSectionReader(f.ra, baseOffset, int64(f.de.ExtentLength))
|
||||
}
|
||||
+658
@@ -0,0 +1,658 @@
|
||||
package iso9660
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"container/list"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
primaryVolumeDirectoryIdentifierMaxLength = 31 // ECMA-119 7.6.3
|
||||
primaryVolumeFileIdentifierMaxLength = 30 // ECMA-119 7.5
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrFileTooLarge is returned when trying to process a file of size greater
|
||||
// than 4GB, which due to the 32-bit address limitation is not possible
|
||||
// except with ISO 9660-Level 3
|
||||
ErrFileTooLarge = errors.New("file is exceeding the maximum file size of 4GB")
|
||||
)
|
||||
|
||||
// ImageWriter is responsible for staging an image's contents
|
||||
// and writing them to an image.
|
||||
type ImageWriter struct {
|
||||
stagingDir string
|
||||
}
|
||||
|
||||
// NewWriter creates a new ImageWrite and initializes its temporary staging dir.
|
||||
// Cleanup should be called after the ImageWriter is no longer needed.
|
||||
func NewWriter() (*ImageWriter, error) {
|
||||
tmp, err := os.MkdirTemp("", "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &ImageWriter{stagingDir: tmp}, nil
|
||||
}
|
||||
|
||||
// Cleanup deletes the underlying temporary staging directory of an ImageWriter.
|
||||
// It can be called multiple times without issues.
|
||||
func (iw *ImageWriter) Cleanup() error {
|
||||
if iw.stagingDir == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := os.RemoveAll(iw.stagingDir); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
iw.stagingDir = ""
|
||||
return nil
|
||||
}
|
||||
|
||||
// AddFile adds a file to the ImageWriter's staging area.
|
||||
// All path components are mangled to match basic ISO9660 filename requirements.
|
||||
func (iw *ImageWriter) AddFile(data io.Reader, filePath string) error {
|
||||
directoryPath, fileName := manglePath(filePath)
|
||||
|
||||
if err := os.MkdirAll(path.Join(iw.stagingDir, directoryPath), 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
f, err := os.OpenFile(path.Join(iw.stagingDir, directoryPath, fileName), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
_, err = io.Copy(f, data)
|
||||
return err
|
||||
}
|
||||
|
||||
func failIfSymlink(path string) error {
|
||||
info, err := os.Lstat(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if info.Mode()&os.ModeSymlink != 0 {
|
||||
return fmt.Errorf("%q is a symlink - these are not yet supported", path)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// AddLocalFile adds a file identified by its path to the ImageWriter's staging area.
|
||||
func (iw *ImageWriter) AddLocalFile(origin, target string) error {
|
||||
if err := failIfSymlink(origin); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
directoryPath, fileName := manglePath(target)
|
||||
|
||||
if err := os.MkdirAll(path.Join(iw.stagingDir, directoryPath), 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// try to hardlink file to staging area before copying.
|
||||
stagedFile := path.Join(iw.stagingDir, directoryPath, fileName)
|
||||
if err := os.Remove(stagedFile); err != nil && !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := os.Link(origin, stagedFile); err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
f, err := os.Open(origin)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer f.Close()
|
||||
|
||||
return iw.AddFile(f, target)
|
||||
}
|
||||
|
||||
func ensureIsDirectory(path string) error {
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
fileinfo, err := f.Stat()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !fileinfo.IsDir() {
|
||||
return fmt.Errorf("%q is not a directory", path)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// AddLocalDirectory adds a directory recursively to the ImageWriter's staging area.
|
||||
func (iw *ImageWriter) AddLocalDirectory(origin, target string) error {
|
||||
if err := ensureIsDirectory(origin); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
walkfn := func(path string, info os.FileInfo, err error) error {
|
||||
if info.IsDir() {
|
||||
return nil
|
||||
}
|
||||
relPath := path[len(origin):] // We need the path to be relative to the origin.
|
||||
return iw.AddLocalFile(path, filepath.Join(target, relPath))
|
||||
}
|
||||
|
||||
return filepath.Walk(origin, walkfn)
|
||||
}
|
||||
|
||||
func manglePath(input string) (string, string) {
|
||||
input = posixifyPath(input)
|
||||
|
||||
nonEmptySegments := splitPath(input)
|
||||
|
||||
dirSegments := nonEmptySegments[:len(nonEmptySegments)-1]
|
||||
name := nonEmptySegments[len(nonEmptySegments)-1]
|
||||
|
||||
for i := 0; i < len(dirSegments); i++ {
|
||||
dirSegments[i] = mangleDirectoryName(dirSegments[i])
|
||||
}
|
||||
name = mangleFileName(name)
|
||||
|
||||
return path.Join(dirSegments...), name
|
||||
}
|
||||
|
||||
// Converts given path to Posix (replacing \ with /)
|
||||
//
|
||||
// @param {string} givenPath Path to convert
|
||||
//
|
||||
// @returns {string} Converted filepath
|
||||
func posixifyPath(path string) string {
|
||||
if runtime.GOOS == "windows" {
|
||||
return strings.ReplaceAll(path, "\\", "/")
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
func splitPath(input string) []string {
|
||||
rawSegments := strings.Split(input, "/")
|
||||
var nonEmptySegments []string
|
||||
for _, s := range rawSegments {
|
||||
if len(s) > 0 {
|
||||
nonEmptySegments = append(nonEmptySegments, s)
|
||||
}
|
||||
}
|
||||
return nonEmptySegments
|
||||
}
|
||||
|
||||
// See ECMA-119 7.5
|
||||
func mangleFileName(input string) string {
|
||||
// https://github.com/torvalds/linux/blob/v5.6/fs/isofs/dir.c#L29
|
||||
input = strings.ToLower(input)
|
||||
split := strings.Split(input, ".")
|
||||
|
||||
version := "1"
|
||||
var filename, extension string
|
||||
if len(split) == 1 {
|
||||
filename = split[0]
|
||||
} else {
|
||||
filename = strings.Join(split[:len(split)-1], "_")
|
||||
extension = split[len(split)-1]
|
||||
}
|
||||
|
||||
// enough characters for the `.ignition` extension
|
||||
extension = mangleD1String(extension, 8)
|
||||
|
||||
maxRemainingFilenameLength := primaryVolumeFileIdentifierMaxLength - (1 + len(version))
|
||||
if len(extension) > 0 {
|
||||
maxRemainingFilenameLength -= (1 + len(extension))
|
||||
}
|
||||
|
||||
filename = mangleD1String(filename, maxRemainingFilenameLength)
|
||||
|
||||
if len(extension) > 0 {
|
||||
return filename + "." + extension + ";" + version
|
||||
}
|
||||
|
||||
return filename + ";" + version
|
||||
}
|
||||
|
||||
// See ECMA-119 7.6
|
||||
func mangleDirectoryName(input string) string {
|
||||
return mangleD1String(input, primaryVolumeDirectoryIdentifierMaxLength)
|
||||
}
|
||||
|
||||
func mangleD1String(input string, maxCharacters int) string {
|
||||
// https://github.com/torvalds/linux/blob/v5.6/fs/isofs/dir.c#L29
|
||||
input = strings.ToLower(input)
|
||||
|
||||
var mangledString string
|
||||
for i := 0; i < len(input) && i < maxCharacters; i++ {
|
||||
r := rune(input[i])
|
||||
if strings.ContainsRune(d1Characters, r) {
|
||||
mangledString += string(r)
|
||||
} else {
|
||||
mangledString += "_"
|
||||
}
|
||||
}
|
||||
|
||||
return mangledString
|
||||
}
|
||||
|
||||
// calculateDirChildrenSectors calculates the total mashalled size of all DirectoryEntries
|
||||
// within a directory. The size of each entry depends of the length of the filename.
|
||||
func calculateDirChildrenSectors(path string) (uint32, error) {
|
||||
contents, err := os.ReadDir(path)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
var sectors uint32
|
||||
var currentSectorOccupied uint32 = 68 // the 0x00 and 0x01 entries
|
||||
|
||||
for _, c := range contents {
|
||||
identifierLen := len(c.Name())
|
||||
idPaddingLen := (identifierLen + 1) % 2
|
||||
entryLength := uint32(33 + identifierLen + idPaddingLen)
|
||||
|
||||
if currentSectorOccupied+entryLength > sectorSize {
|
||||
sectors++
|
||||
currentSectorOccupied = entryLength
|
||||
} else {
|
||||
currentSectorOccupied += entryLength
|
||||
}
|
||||
}
|
||||
|
||||
if currentSectorOccupied > 0 {
|
||||
sectors++
|
||||
}
|
||||
|
||||
return sectors, nil
|
||||
}
|
||||
|
||||
func fileLengthToSectors(l uint32) uint32 {
|
||||
if (l % sectorSize) == 0 {
|
||||
return l / sectorSize
|
||||
}
|
||||
|
||||
return (l / sectorSize) + 1
|
||||
}
|
||||
|
||||
type writeContext struct {
|
||||
stagingDir string
|
||||
timestamp RecordingTimestamp
|
||||
freeSectorPointer uint32
|
||||
}
|
||||
|
||||
func (wc *writeContext) allocateSectors(n uint32) uint32 {
|
||||
return atomic.AddUint32(&wc.freeSectorPointer, n) - n
|
||||
}
|
||||
|
||||
func (wc *writeContext) createDEForRoot() (*DirectoryEntry, error) {
|
||||
extentLengthInSectors, err := calculateDirChildrenSectors(wc.stagingDir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
extentLocation := wc.allocateSectors(extentLengthInSectors)
|
||||
de := &DirectoryEntry{
|
||||
ExtendedAtributeRecordLength: 0,
|
||||
ExtentLocation: int32(extentLocation),
|
||||
ExtentLength: uint32(extentLengthInSectors * sectorSize),
|
||||
RecordingDateTime: wc.timestamp,
|
||||
FileFlags: dirFlagDir,
|
||||
FileUnitSize: 0, // 0 for non-interleaved write
|
||||
InterleaveGap: 0, // not interleaved
|
||||
VolumeSequenceNumber: 1, // we only have one volume
|
||||
Identifier: string([]byte{0}),
|
||||
SystemUse: []byte{},
|
||||
}
|
||||
return de, nil
|
||||
}
|
||||
|
||||
type itemToWrite struct {
|
||||
isDirectory bool
|
||||
dirPath string
|
||||
ownEntry *DirectoryEntry
|
||||
parentEntery *DirectoryEntry
|
||||
childrenEntries []*DirectoryEntry
|
||||
targetSector uint32
|
||||
}
|
||||
|
||||
// scanDirectory reads the directory's contents and adds them to the queue, as well as stores all their DirectoryEntries in the item,
|
||||
// because we'll need them to write this item's descriptor.
|
||||
func (wc *writeContext) scanDirectory(item *itemToWrite, dirPath string, ownEntry *DirectoryEntry, parentEntery *DirectoryEntry, targetSector uint32) (*list.List, error) {
|
||||
contents, err := os.ReadDir(dirPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
itemsToWrite := list.New()
|
||||
|
||||
for _, c := range contents {
|
||||
var (
|
||||
fileFlags byte
|
||||
extentLengthInSectors uint32
|
||||
extentLength uint32
|
||||
)
|
||||
if c.IsDir() {
|
||||
extentLengthInSectors, err = calculateDirChildrenSectors(path.Join(dirPath, c.Name()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fileFlags = dirFlagDir
|
||||
extentLength = extentLengthInSectors * sectorSize
|
||||
} else {
|
||||
fileinfo, err := c.Info()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if fileinfo.Size() > int64(math.MaxUint32) {
|
||||
return nil, ErrFileTooLarge
|
||||
}
|
||||
extentLength = uint32(fileinfo.Size())
|
||||
extentLengthInSectors = fileLengthToSectors(extentLength)
|
||||
|
||||
fileFlags = 0
|
||||
}
|
||||
|
||||
extentLocation := wc.allocateSectors(extentLengthInSectors)
|
||||
de := &DirectoryEntry{
|
||||
ExtendedAtributeRecordLength: 0,
|
||||
ExtentLocation: int32(extentLocation),
|
||||
ExtentLength: uint32(extentLength),
|
||||
RecordingDateTime: wc.timestamp,
|
||||
FileFlags: fileFlags,
|
||||
FileUnitSize: 0, // 0 for non-interleaved write
|
||||
InterleaveGap: 0, // not interleaved
|
||||
VolumeSequenceNumber: 1, // we only have one volume
|
||||
Identifier: c.Name(),
|
||||
SystemUse: []byte{},
|
||||
}
|
||||
|
||||
// Add this child's descriptor to the currently scanned directory's list of children,
|
||||
// so that later we can use it for writing the current item.
|
||||
if item.childrenEntries == nil {
|
||||
item.childrenEntries = []*DirectoryEntry{de}
|
||||
} else {
|
||||
item.childrenEntries = append(item.childrenEntries, de)
|
||||
}
|
||||
|
||||
// queue this child for processing
|
||||
itemsToWrite.PushBack(itemToWrite{
|
||||
isDirectory: c.IsDir(),
|
||||
dirPath: path.Join(dirPath, c.Name()),
|
||||
ownEntry: de,
|
||||
parentEntery: ownEntry,
|
||||
targetSector: uint32(de.ExtentLocation),
|
||||
})
|
||||
}
|
||||
|
||||
return itemsToWrite, nil
|
||||
}
|
||||
|
||||
// processDirectory writes a given directory item to the destination sectors
|
||||
func processDirectory(w io.Writer, children []*DirectoryEntry, ownEntry *DirectoryEntry, parentEntry *DirectoryEntry) error {
|
||||
var currentOffset uint32
|
||||
|
||||
currentDE := ownEntry.Clone()
|
||||
currentDE.Identifier = string([]byte{0})
|
||||
parentDE := parentEntry.Clone()
|
||||
parentDE.Identifier = string([]byte{1})
|
||||
|
||||
currentDEData, err := currentDE.MarshalBinary()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
parentDEData, err := parentDE.MarshalBinary()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
n, err := w.Write(currentDEData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
currentOffset += uint32(n)
|
||||
n, err = w.Write(parentDEData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
currentOffset += uint32(n)
|
||||
|
||||
for _, childDescriptor := range children {
|
||||
data, err := childDescriptor.MarshalBinary()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
remainingSectorSpace := sectorSize - (currentOffset % sectorSize)
|
||||
if remainingSectorSpace < uint32(len(data)) {
|
||||
// ECMA-119 6.8.1.1 If the body of the next descriptor won't fit into the sector,
|
||||
// we fill the rest of space with zeros and skip to the next sector.
|
||||
zeros := bytes.Repeat([]byte{0}, int(remainingSectorSpace))
|
||||
_, err = w.Write(zeros)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// skip to the next sector
|
||||
currentOffset = 0
|
||||
}
|
||||
|
||||
n, err = w.Write(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
currentOffset += uint32(n)
|
||||
}
|
||||
|
||||
// fill with zeros to the end of the sector
|
||||
remainingSectorSpace := sectorSize - (currentOffset % sectorSize)
|
||||
if remainingSectorSpace != 0 {
|
||||
zeros := bytes.Repeat([]byte{0}, int(remainingSectorSpace))
|
||||
_, err = w.Write(zeros)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func processFile(w io.Writer, dirPath string) error {
|
||||
f, err := os.Open(dirPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
fileinfo, err := f.Stat()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if fileinfo.Size() > int64(math.MaxUint32) {
|
||||
return ErrFileTooLarge
|
||||
}
|
||||
|
||||
buffer := make([]byte, sectorSize)
|
||||
|
||||
for bytesLeft := uint32(fileinfo.Size()); bytesLeft > 0; {
|
||||
var toRead uint32
|
||||
if bytesLeft < sectorSize {
|
||||
toRead = bytesLeft
|
||||
} else {
|
||||
toRead = sectorSize
|
||||
}
|
||||
|
||||
if _, err = io.ReadAtLeast(f, buffer, int(toRead)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err = w.Write(buffer); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
bytesLeft -= toRead
|
||||
}
|
||||
// We already write a whole sector-sized buffer, so there's need to fill with zeroes.
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// traverseStagingDir creates a new queue of items to write by traversing the staging directory
|
||||
func (wc *writeContext) traverseStagingDir(rootItem itemToWrite) (*list.List, error) {
|
||||
itemsToWrite := list.New()
|
||||
itemsToWrite.PushBack(rootItem)
|
||||
|
||||
for item := itemsToWrite.Front(); item != nil; item = item.Next() {
|
||||
it := item.Value.(itemToWrite)
|
||||
|
||||
if it.isDirectory {
|
||||
newItems, err := wc.scanDirectory(&it, it.dirPath, it.ownEntry, it.parentEntery, it.targetSector)
|
||||
if err != nil {
|
||||
relativePath := it.dirPath[len(wc.stagingDir):]
|
||||
return nil, fmt.Errorf("processing %s: %s", relativePath, err)
|
||||
}
|
||||
itemsToWrite.PushBackList(newItems)
|
||||
}
|
||||
|
||||
item.Value = it
|
||||
}
|
||||
|
||||
return itemsToWrite, nil
|
||||
}
|
||||
|
||||
func writeAll(w io.Writer, itemsToWrite *list.List) error {
|
||||
for item := itemsToWrite.Front(); item != nil; item = item.Next() {
|
||||
it := item.Value.(itemToWrite)
|
||||
var err error
|
||||
if it.isDirectory {
|
||||
err = processDirectory(w, it.childrenEntries, it.ownEntry, it.parentEntery)
|
||||
} else {
|
||||
err = processFile(w, it.dirPath)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// WriteTo writes the image to the given WriterAt
|
||||
func (iw *ImageWriter) WriteTo(w io.Writer, volumeIdentifier string) error {
|
||||
now := time.Now()
|
||||
|
||||
wc := writeContext{
|
||||
stagingDir: iw.stagingDir,
|
||||
timestamp: RecordingTimestamp{},
|
||||
freeSectorPointer: 18, // system area (16) + 2 volume descriptors
|
||||
}
|
||||
|
||||
rootDE, err := wc.createDEForRoot()
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating root directory descriptor: %s", err)
|
||||
}
|
||||
|
||||
rootItem := itemToWrite{
|
||||
isDirectory: true,
|
||||
dirPath: wc.stagingDir,
|
||||
ownEntry: rootDE,
|
||||
parentEntery: rootDE,
|
||||
targetSector: uint32(rootDE.ExtentLocation),
|
||||
}
|
||||
|
||||
itemsToWrite, err := wc.traverseStagingDir(rootItem)
|
||||
if err != nil {
|
||||
return fmt.Errorf("tranversing staging directory: %s", err)
|
||||
}
|
||||
|
||||
pvd := volumeDescriptor{
|
||||
Header: volumeDescriptorHeader{
|
||||
Type: volumeTypePrimary,
|
||||
Identifier: standardIdentifierBytes,
|
||||
Version: 1,
|
||||
},
|
||||
Primary: &PrimaryVolumeDescriptorBody{
|
||||
SystemIdentifier: runtime.GOOS,
|
||||
VolumeIdentifier: volumeIdentifier,
|
||||
VolumeSpaceSize: int32(wc.freeSectorPointer),
|
||||
VolumeSetSize: 1,
|
||||
VolumeSequenceNumber: 1,
|
||||
LogicalBlockSize: int16(sectorSize),
|
||||
PathTableSize: 0,
|
||||
TypeLPathTableLoc: 0,
|
||||
OptTypeLPathTableLoc: 0,
|
||||
TypeMPathTableLoc: 0,
|
||||
OptTypeMPathTableLoc: 0,
|
||||
RootDirectoryEntry: rootDE,
|
||||
VolumeSetIdentifier: "",
|
||||
PublisherIdentifier: "",
|
||||
DataPreparerIdentifier: "",
|
||||
ApplicationIdentifier: "github.com/kdomanski/iso9660",
|
||||
CopyrightFileIdentifier: "",
|
||||
AbstractFileIdentifier: "",
|
||||
BibliographicFileIdentifier: "",
|
||||
VolumeCreationDateAndTime: VolumeDescriptorTimestampFromTime(now),
|
||||
VolumeModificationDateAndTime: VolumeDescriptorTimestampFromTime(now),
|
||||
VolumeExpirationDateAndTime: VolumeDescriptorTimestamp{},
|
||||
VolumeEffectiveDateAndTime: VolumeDescriptorTimestampFromTime(now),
|
||||
FileStructureVersion: 1,
|
||||
ApplicationUsed: [512]byte{},
|
||||
},
|
||||
}
|
||||
|
||||
terminator := volumeDescriptor{
|
||||
Header: volumeDescriptorHeader{
|
||||
Type: volumeTypeTerminator,
|
||||
Identifier: standardIdentifierBytes,
|
||||
Version: 1,
|
||||
},
|
||||
}
|
||||
|
||||
// write 16 sectors of zeroes
|
||||
zeroSector := bytes.Repeat([]byte{0}, int(sectorSize))
|
||||
for i := uint32(0); i < 16; i++ {
|
||||
if _, err = w.Write(zeroSector); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
buffer, err := pvd.MarshalBinary()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err = w.Write(buffer); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if buffer, err = terminator.MarshalBinary(); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err = w.Write(buffer); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = writeAll(w, itemsToWrite); err != nil {
|
||||
return fmt.Errorf("writing files: %s", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
+605
@@ -0,0 +1,605 @@
|
||||
// Package iso9660 implements reading and creating basic ISO9660 images.
|
||||
package iso9660
|
||||
|
||||
import (
|
||||
"encoding"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ISO 9660 Overview
|
||||
// https://archive.fo/xs9ac
|
||||
|
||||
const (
|
||||
sectorSize uint32 = 2048
|
||||
systemAreaSize = sectorSize * 16
|
||||
standardIdentifier = "CD001"
|
||||
udfIdentifier = "BEA01"
|
||||
|
||||
volumeTypeBoot byte = 0
|
||||
volumeTypePrimary byte = 1
|
||||
volumeTypeSupplementary byte = 2
|
||||
volumeTypePartition byte = 3
|
||||
volumeTypeTerminator byte = 255
|
||||
|
||||
volumeDescriptorBodySize = sectorSize - 7
|
||||
|
||||
aCharacters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_!\"%&'()*+,-./:;<=>?"
|
||||
dCharacters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_"
|
||||
// ECMA-119 7.4.2.2 defines d1-characters as
|
||||
// "subject to agreement between the originator and the recipient of the volume".
|
||||
d1Characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_!\"%&'()*+,-./:;<=>?"
|
||||
)
|
||||
|
||||
const (
|
||||
dirFlagHidden = 1 << iota
|
||||
dirFlagDir
|
||||
dirFlagAssociated
|
||||
dirFlagRecord
|
||||
dirFlagProtection
|
||||
_
|
||||
_
|
||||
dirFlagMultiExtent
|
||||
)
|
||||
|
||||
var standardIdentifierBytes = [5]byte{'C', 'D', '0', '0', '1'}
|
||||
|
||||
var ErrUDFNotSupported = errors.New("UDF volumes are not supported")
|
||||
|
||||
// volumeDescriptorHeader represents the data in bytes 0-6
|
||||
// of a Volume Descriptor as defined in ECMA-119 8.1
|
||||
type volumeDescriptorHeader struct {
|
||||
Type byte
|
||||
Identifier [5]byte
|
||||
Version byte
|
||||
}
|
||||
|
||||
var _ encoding.BinaryUnmarshaler = &volumeDescriptorHeader{}
|
||||
var _ encoding.BinaryMarshaler = &volumeDescriptorHeader{}
|
||||
|
||||
// UnmarshalBinary decodes a volumeDescriptorHeader from binary form
|
||||
func (vdh *volumeDescriptorHeader) UnmarshalBinary(data []byte) error {
|
||||
if len(data) < 7 {
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
|
||||
vdh.Type = data[0]
|
||||
copy(vdh.Identifier[:], data[1:6])
|
||||
vdh.Version = data[6]
|
||||
return nil
|
||||
}
|
||||
|
||||
func (vdh volumeDescriptorHeader) MarshalBinary() ([]byte, error) {
|
||||
data := make([]byte, 7)
|
||||
data[0] = vdh.Type
|
||||
data[6] = vdh.Version
|
||||
copy(data[1:6], vdh.Identifier[:])
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// BootVolumeDescriptorBody represents the data in bytes 7-2047
|
||||
// of a Boot Record as defined in ECMA-119 8.2
|
||||
type BootVolumeDescriptorBody struct {
|
||||
BootSystemIdentifier string
|
||||
BootIdentifier string
|
||||
BootSystemUse [1977]byte
|
||||
}
|
||||
|
||||
var _ encoding.BinaryUnmarshaler = &BootVolumeDescriptorBody{}
|
||||
|
||||
// PrimaryVolumeDescriptorBody represents the data in bytes 7-2047
|
||||
// of a Primary Volume Descriptor as defined in ECMA-119 8.4
|
||||
type PrimaryVolumeDescriptorBody struct {
|
||||
SystemIdentifier string
|
||||
VolumeIdentifier string
|
||||
VolumeSpaceSize int32
|
||||
VolumeSetSize int16
|
||||
VolumeSequenceNumber int16
|
||||
LogicalBlockSize int16
|
||||
PathTableSize int32
|
||||
TypeLPathTableLoc int32
|
||||
OptTypeLPathTableLoc int32
|
||||
TypeMPathTableLoc int32
|
||||
OptTypeMPathTableLoc int32
|
||||
RootDirectoryEntry *DirectoryEntry
|
||||
VolumeSetIdentifier string
|
||||
PublisherIdentifier string
|
||||
DataPreparerIdentifier string
|
||||
ApplicationIdentifier string
|
||||
CopyrightFileIdentifier string
|
||||
AbstractFileIdentifier string
|
||||
BibliographicFileIdentifier string
|
||||
VolumeCreationDateAndTime VolumeDescriptorTimestamp
|
||||
VolumeModificationDateAndTime VolumeDescriptorTimestamp
|
||||
VolumeExpirationDateAndTime VolumeDescriptorTimestamp
|
||||
VolumeEffectiveDateAndTime VolumeDescriptorTimestamp
|
||||
FileStructureVersion byte
|
||||
ApplicationUsed [512]byte
|
||||
}
|
||||
|
||||
var _ encoding.BinaryUnmarshaler = &PrimaryVolumeDescriptorBody{}
|
||||
var _ encoding.BinaryMarshaler = PrimaryVolumeDescriptorBody{}
|
||||
|
||||
// DirectoryEntry contains data from a Directory Descriptor
|
||||
// as described by ECMA-119 9.1
|
||||
type DirectoryEntry struct {
|
||||
ExtendedAtributeRecordLength byte
|
||||
ExtentLocation int32
|
||||
ExtentLength uint32
|
||||
RecordingDateTime RecordingTimestamp
|
||||
FileFlags byte
|
||||
FileUnitSize byte
|
||||
InterleaveGap byte
|
||||
VolumeSequenceNumber int16
|
||||
Identifier string
|
||||
SystemUse []byte
|
||||
SystemUseEntries SystemUseEntrySlice
|
||||
}
|
||||
|
||||
var _ encoding.BinaryUnmarshaler = &DirectoryEntry{}
|
||||
var _ encoding.BinaryMarshaler = &DirectoryEntry{}
|
||||
|
||||
// UnmarshalBinary decodes a DirectoryEntry from binary form
|
||||
func (de *DirectoryEntry) UnmarshalBinary(data []byte) error {
|
||||
length := data[0]
|
||||
if length == 0 {
|
||||
return io.EOF
|
||||
}
|
||||
|
||||
var err error
|
||||
|
||||
de.ExtendedAtributeRecordLength = data[1]
|
||||
|
||||
if de.ExtentLocation, err = UnmarshalInt32LSBMSB(data[2:10]); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if de.ExtentLength, err = UnmarshalUint32LSBMSB(data[10:18]); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = de.RecordingDateTime.UnmarshalBinary(data[18:25]); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
de.FileFlags = data[25]
|
||||
de.FileUnitSize = data[26]
|
||||
de.InterleaveGap = data[27]
|
||||
|
||||
if de.VolumeSequenceNumber, err = UnmarshalInt16LSBMSB(data[28:32]); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
identifierLen := data[32]
|
||||
de.Identifier = string(data[33 : 33+identifierLen])
|
||||
|
||||
// add padding if identifier length was even]
|
||||
idPaddingLen := (identifierLen + 1) % 2
|
||||
de.SystemUse = data[33+identifierLen+idPaddingLen : length]
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// MarshalBinary encodes a DirectoryEntry to binary form
|
||||
func (de *DirectoryEntry) MarshalBinary() ([]byte, error) {
|
||||
identifierLen := len(de.Identifier)
|
||||
idPaddingLen := (identifierLen + 1) % 2
|
||||
totalLen := 33 + identifierLen + idPaddingLen + len(de.SystemUse)
|
||||
if totalLen > 255 {
|
||||
return nil, fmt.Errorf("identifier %q is too long", de.Identifier)
|
||||
}
|
||||
|
||||
data := make([]byte, totalLen)
|
||||
|
||||
data[0] = byte(totalLen)
|
||||
data[1] = de.ExtendedAtributeRecordLength
|
||||
|
||||
WriteInt32LSBMSB(data[2:10], de.ExtentLocation)
|
||||
WriteInt32LSBMSB(data[10:18], int32(de.ExtentLength))
|
||||
de.RecordingDateTime.MarshalBinary(data[18:25])
|
||||
data[25] = de.FileFlags
|
||||
data[26] = de.FileUnitSize
|
||||
data[27] = de.InterleaveGap
|
||||
WriteInt16LSBMSB(data[28:32], de.VolumeSequenceNumber)
|
||||
data[32] = byte(identifierLen)
|
||||
copy(data[33:33+identifierLen], []byte(de.Identifier))
|
||||
|
||||
copy(data[33+identifierLen+idPaddingLen:totalLen], de.SystemUse)
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// Clone creates a copy of the DirectoryEntry
|
||||
func (de *DirectoryEntry) Clone() DirectoryEntry {
|
||||
newDE := DirectoryEntry{
|
||||
ExtendedAtributeRecordLength: de.ExtendedAtributeRecordLength,
|
||||
ExtentLocation: de.ExtentLocation,
|
||||
ExtentLength: de.ExtentLength,
|
||||
RecordingDateTime: de.RecordingDateTime,
|
||||
FileFlags: de.FileFlags,
|
||||
FileUnitSize: de.FileUnitSize,
|
||||
InterleaveGap: de.InterleaveGap,
|
||||
VolumeSequenceNumber: de.VolumeSequenceNumber,
|
||||
Identifier: de.Identifier,
|
||||
SystemUse: make([]byte, len(de.SystemUse)),
|
||||
}
|
||||
copy(newDE.SystemUse, de.SystemUse)
|
||||
return newDE
|
||||
}
|
||||
|
||||
// UnmarshalBinary decodes a PrimaryVolumeDescriptorBody from binary form as defined in ECMA-119 8.4
|
||||
func (pvd *PrimaryVolumeDescriptorBody) UnmarshalBinary(data []byte) error {
|
||||
if len(data) < 2048 {
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
|
||||
var err error
|
||||
|
||||
pvd.SystemIdentifier = strings.TrimRight(string(data[8:40]), " ")
|
||||
pvd.VolumeIdentifier = strings.TrimRight(string(data[40:72]), " ")
|
||||
|
||||
if pvd.VolumeSpaceSize, err = UnmarshalInt32LSBMSB(data[80:88]); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if pvd.VolumeSetSize, err = UnmarshalInt16LSBMSB(data[120:124]); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if pvd.VolumeSequenceNumber, err = UnmarshalInt16LSBMSB(data[124:128]); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if pvd.LogicalBlockSize, err = UnmarshalInt16LSBMSB(data[128:132]); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if pvd.PathTableSize, err = UnmarshalInt32LSBMSB(data[132:140]); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
pvd.TypeLPathTableLoc = int32(binary.LittleEndian.Uint32(data[140:144]))
|
||||
pvd.OptTypeLPathTableLoc = int32(binary.LittleEndian.Uint32(data[144:148]))
|
||||
pvd.TypeMPathTableLoc = int32(binary.BigEndian.Uint32(data[148:152]))
|
||||
pvd.OptTypeMPathTableLoc = int32(binary.BigEndian.Uint32(data[152:156]))
|
||||
|
||||
if pvd.RootDirectoryEntry == nil {
|
||||
pvd.RootDirectoryEntry = &DirectoryEntry{}
|
||||
}
|
||||
if err = pvd.RootDirectoryEntry.UnmarshalBinary(data[156:190]); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
pvd.VolumeSetIdentifier = strings.TrimRight(string(data[190:318]), " ")
|
||||
pvd.PublisherIdentifier = strings.TrimRight(string(data[318:446]), " ")
|
||||
pvd.DataPreparerIdentifier = strings.TrimRight(string(data[446:574]), " ")
|
||||
pvd.ApplicationIdentifier = strings.TrimRight(string(data[574:702]), " ")
|
||||
pvd.CopyrightFileIdentifier = strings.TrimRight(string(data[702:740]), " ")
|
||||
pvd.AbstractFileIdentifier = strings.TrimRight(string(data[740:776]), " ")
|
||||
pvd.BibliographicFileIdentifier = strings.TrimRight(string(data[776:813]), " ")
|
||||
|
||||
if pvd.VolumeCreationDateAndTime.UnmarshalBinary(data[813:830]) != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if pvd.VolumeModificationDateAndTime.UnmarshalBinary(data[830:847]) != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if pvd.VolumeExpirationDateAndTime.UnmarshalBinary(data[847:864]) != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if pvd.VolumeEffectiveDateAndTime.UnmarshalBinary(data[864:881]) != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
pvd.FileStructureVersion = data[881]
|
||||
copy(pvd.ApplicationUsed[:], data[883:1395])
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// MarshalBinary encodes the PrimaryVolumeDescriptorBody to its binary form
|
||||
func (pvd PrimaryVolumeDescriptorBody) MarshalBinary() ([]byte, error) {
|
||||
output := make([]byte, sectorSize)
|
||||
|
||||
d := MarshalString(pvd.SystemIdentifier, 32)
|
||||
copy(output[8:40], d)
|
||||
|
||||
d = MarshalString(pvd.VolumeIdentifier, 32)
|
||||
copy(output[40:72], d)
|
||||
|
||||
WriteInt32LSBMSB(output[80:88], pvd.VolumeSpaceSize)
|
||||
WriteInt16LSBMSB(output[120:124], pvd.VolumeSetSize)
|
||||
WriteInt16LSBMSB(output[124:128], pvd.VolumeSequenceNumber)
|
||||
WriteInt16LSBMSB(output[128:132], pvd.LogicalBlockSize)
|
||||
WriteInt32LSBMSB(output[132:140], pvd.PathTableSize)
|
||||
|
||||
binary.LittleEndian.PutUint32(output[140:144], uint32(pvd.TypeLPathTableLoc))
|
||||
binary.LittleEndian.PutUint32(output[144:148], uint32(pvd.OptTypeLPathTableLoc))
|
||||
binary.BigEndian.PutUint32(output[148:152], uint32(pvd.TypeMPathTableLoc))
|
||||
binary.BigEndian.PutUint32(output[152:156], uint32(pvd.OptTypeMPathTableLoc))
|
||||
|
||||
binaryRDE, err := pvd.RootDirectoryEntry.MarshalBinary()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
copy(output[156:190], binaryRDE)
|
||||
|
||||
copy(output[190:318], MarshalString(pvd.VolumeSetIdentifier, 128))
|
||||
copy(output[318:446], MarshalString(pvd.PublisherIdentifier, 128))
|
||||
copy(output[446:574], MarshalString(pvd.DataPreparerIdentifier, 128))
|
||||
copy(output[574:702], MarshalString(pvd.ApplicationIdentifier, 128))
|
||||
copy(output[702:740], MarshalString(pvd.CopyrightFileIdentifier, 38))
|
||||
copy(output[740:776], MarshalString(pvd.AbstractFileIdentifier, 36))
|
||||
copy(output[776:813], MarshalString(pvd.BibliographicFileIdentifier, 37))
|
||||
|
||||
d, err = pvd.VolumeCreationDateAndTime.MarshalBinary()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
copy(output[813:830], d)
|
||||
|
||||
d, err = pvd.VolumeModificationDateAndTime.MarshalBinary()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
copy(output[830:847], d)
|
||||
|
||||
d, err = pvd.VolumeExpirationDateAndTime.MarshalBinary()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
copy(output[847:864], d)
|
||||
|
||||
d, err = pvd.VolumeEffectiveDateAndTime.MarshalBinary()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
copy(output[864:881], d)
|
||||
|
||||
output[881] = pvd.FileStructureVersion
|
||||
output[882] = 0
|
||||
copy(output[883:1395], pvd.ApplicationUsed[:])
|
||||
for i := 1395; i < 2048; i++ {
|
||||
output[i] = 0
|
||||
}
|
||||
|
||||
return output, nil
|
||||
}
|
||||
|
||||
// UnmarshalBinary decodes a BootVolumeDescriptorBody from binary form
|
||||
func (bvd *BootVolumeDescriptorBody) UnmarshalBinary(data []byte) error {
|
||||
bvd.BootSystemIdentifier = strings.TrimRight(string(data[7:39]), " ")
|
||||
bvd.BootIdentifier = strings.TrimRight(string(data[39:71]), " ")
|
||||
if n := copy(bvd.BootSystemUse[:], data[71:2048]); n != 1977 {
|
||||
return fmt.Errorf("BootVolumeDescriptorBody.UnmarshalBinary: copied %d bytes", n)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type volumeDescriptor struct {
|
||||
Header volumeDescriptorHeader
|
||||
Boot *BootVolumeDescriptorBody
|
||||
Primary *PrimaryVolumeDescriptorBody
|
||||
}
|
||||
|
||||
var _ encoding.BinaryUnmarshaler = &volumeDescriptor{}
|
||||
var _ encoding.BinaryMarshaler = &volumeDescriptor{}
|
||||
|
||||
func (vd volumeDescriptor) Type() byte {
|
||||
return vd.Header.Type
|
||||
}
|
||||
|
||||
// UnmarshalBinary decodes a volumeDescriptor from binary form
|
||||
func (vd *volumeDescriptor) UnmarshalBinary(data []byte) error {
|
||||
if uint32(len(data)) < sectorSize {
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
|
||||
if err := vd.Header.UnmarshalBinary(data); err != nil {
|
||||
// this should never fail, since volumeDescriptorHeader.UnmarshalBinary( ) only checks data size too
|
||||
return err
|
||||
}
|
||||
|
||||
id := string(vd.Header.Identifier[:])
|
||||
if id != standardIdentifier {
|
||||
if id == udfIdentifier {
|
||||
return ErrUDFNotSupported
|
||||
}
|
||||
return fmt.Errorf("volume descriptor %q != %q", id, standardIdentifier)
|
||||
}
|
||||
|
||||
switch vd.Header.Type {
|
||||
case volumeTypeBoot:
|
||||
vd.Boot = &BootVolumeDescriptorBody{}
|
||||
return vd.Boot.UnmarshalBinary(data)
|
||||
case volumeTypePartition:
|
||||
return errors.New("partition volumes are not yet supported")
|
||||
case volumeTypePrimary, volumeTypeSupplementary:
|
||||
vd.Primary = &PrimaryVolumeDescriptorBody{}
|
||||
return vd.Primary.UnmarshalBinary(data)
|
||||
case volumeTypeTerminator:
|
||||
return nil
|
||||
}
|
||||
|
||||
return fmt.Errorf("unknown volume type 0x%X", vd.Header.Type)
|
||||
}
|
||||
|
||||
// UnmarshalBinary decodes a volumeDescriptor from binary form
|
||||
func (vd volumeDescriptor) MarshalBinary() ([]byte, error) {
|
||||
var output []byte
|
||||
var err error
|
||||
|
||||
switch vd.Header.Type {
|
||||
case volumeTypeBoot:
|
||||
return nil, errors.New("boot volumes are not yet supported")
|
||||
case volumeTypePartition:
|
||||
return nil, errors.New("partition volumes are not yet supported")
|
||||
case volumeTypePrimary, volumeTypeSupplementary:
|
||||
if output, err = vd.Primary.MarshalBinary(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
case volumeTypeTerminator:
|
||||
output = make([]byte, sectorSize)
|
||||
}
|
||||
|
||||
data, err := vd.Header.MarshalBinary()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
copy(output[0:7], data)
|
||||
|
||||
return output, nil
|
||||
}
|
||||
|
||||
// VolumeDescriptorTimestamp represents a time and date format
|
||||
// that can be encoded according to ECMA-119 8.4.26.1
|
||||
type VolumeDescriptorTimestamp struct {
|
||||
Year int
|
||||
Month int
|
||||
Day int
|
||||
Hour int
|
||||
Minute int
|
||||
Second int
|
||||
Hundredth int
|
||||
Offset int
|
||||
}
|
||||
|
||||
var _ encoding.BinaryMarshaler = &VolumeDescriptorTimestamp{}
|
||||
var _ encoding.BinaryUnmarshaler = &VolumeDescriptorTimestamp{}
|
||||
|
||||
// MarshalBinary encodes the timestamp into a binary form
|
||||
func (ts *VolumeDescriptorTimestamp) MarshalBinary() ([]byte, error) {
|
||||
formatted := fmt.Sprintf("%04d%02d%02d%02d%02d%02d%02d", ts.Year, ts.Month, ts.Day, ts.Hour, ts.Minute, ts.Second, ts.Hundredth)
|
||||
formattedBytes := append([]byte(formatted), byte(ts.Offset))
|
||||
if len(formattedBytes) != 17 {
|
||||
return nil, fmt.Errorf("VolumeDescriptorTimestamp.MarshalBinary: the formatted timestamp is %d bytes long", len(formatted))
|
||||
}
|
||||
return formattedBytes, nil
|
||||
}
|
||||
|
||||
// UnmarshalBinary decodes a VolumeDescriptorTimestamp from binary form
|
||||
func (ts *VolumeDescriptorTimestamp) UnmarshalBinary(data []byte) error {
|
||||
if len(data) < 17 {
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
|
||||
year, err := strconv.Atoi(strings.TrimSpace(string(data[0:4])))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
month, err := strconv.Atoi(strings.TrimSpace(string(data[4:6])))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
day, err := strconv.Atoi(strings.TrimSpace(string(data[6:8])))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
hour, err := strconv.Atoi(strings.TrimSpace(string(data[8:10])))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
min, err := strconv.Atoi(strings.TrimSpace(string(data[10:12])))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sec, err := strconv.Atoi(strings.TrimSpace(string(data[12:14])))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
hundredth, err := strconv.Atoi(strings.TrimSpace(string(data[14:16])))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*ts = VolumeDescriptorTimestamp{
|
||||
Year: year,
|
||||
Month: month,
|
||||
Day: day,
|
||||
Hour: hour,
|
||||
Minute: min,
|
||||
Second: sec,
|
||||
Hundredth: hundredth,
|
||||
Offset: int(data[16]),
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RecordingTimestamp represents a time and date format
|
||||
// that can be encoded according to ECMA-119 9.1.5
|
||||
type RecordingTimestamp time.Time
|
||||
|
||||
var _ encoding.BinaryUnmarshaler = &RecordingTimestamp{}
|
||||
|
||||
// UnmarshalBinary decodes a RecordingTimestamp from binary form
|
||||
func (ts *RecordingTimestamp) UnmarshalBinary(data []byte) error {
|
||||
if len(data) < 7 {
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
|
||||
year := 1900 + int(data[0])
|
||||
month := int(data[1])
|
||||
day := int(data[2])
|
||||
hour := int(data[3])
|
||||
min := int(data[4])
|
||||
sec := int(data[5])
|
||||
tzOffset := int(data[6])
|
||||
secondsInAQuarter := 60 * 15
|
||||
|
||||
tz := time.FixedZone("", tzOffset*secondsInAQuarter)
|
||||
*ts = RecordingTimestamp(time.Date(year, time.Month(month), day, hour, min, sec, 0, tz))
|
||||
return nil
|
||||
}
|
||||
|
||||
// MarshalBinary encodes the RecordingTimestamp in its binary form to a buffer
|
||||
// of the length of 7 or more bytes
|
||||
func (ts RecordingTimestamp) MarshalBinary(dst []byte) {
|
||||
_ = dst[6] // early bounds check to guarantee safety of writes below
|
||||
t := time.Time(ts)
|
||||
year, month, day := t.Date()
|
||||
hour, min, sec := t.Clock()
|
||||
_, secOffset := t.Zone()
|
||||
secondsInAQuarter := 60 * 15
|
||||
offsetInQuarters := secOffset / secondsInAQuarter
|
||||
dst[0] = byte(year - 1900)
|
||||
dst[1] = byte(month)
|
||||
dst[2] = byte(day)
|
||||
dst[3] = byte(hour)
|
||||
dst[4] = byte(min)
|
||||
dst[5] = byte(sec)
|
||||
dst[6] = byte(offsetInQuarters)
|
||||
}
|
||||
|
||||
// VolumeDescriptorTimestampFromTime converts time.Time to VolumeDescriptorTimestamp
|
||||
func VolumeDescriptorTimestampFromTime(t time.Time) VolumeDescriptorTimestamp {
|
||||
t = t.UTC()
|
||||
year, month, day := t.Date()
|
||||
hour, minute, second := t.Clock()
|
||||
hundredth := t.Nanosecond() / 10000000
|
||||
return VolumeDescriptorTimestamp{
|
||||
Year: year,
|
||||
Month: int(month),
|
||||
Day: day,
|
||||
Hour: hour,
|
||||
Minute: minute,
|
||||
Second: second,
|
||||
Hundredth: hundredth,
|
||||
Offset: 0, // we converted to UTC
|
||||
}
|
||||
}
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
package iso9660
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// MarshalString encodes the given string as a byte array padded to the given length
|
||||
func MarshalString(s string, padToLength int) []byte {
|
||||
if len(s) > padToLength {
|
||||
s = s[:padToLength]
|
||||
}
|
||||
missingPadding := padToLength - len(s)
|
||||
s = s + strings.Repeat(" ", missingPadding)
|
||||
return []byte(s)
|
||||
}
|
||||
|
||||
// UnmarshalInt32LSBMSB decodes a 32-bit integer in both byte orders, as defined in ECMA-119 7.3.3
|
||||
func UnmarshalInt32LSBMSB(data []byte) (int32, error) {
|
||||
if len(data) < 8 {
|
||||
return 0, io.ErrUnexpectedEOF
|
||||
}
|
||||
|
||||
lsb := int32(binary.LittleEndian.Uint32(data[0:4]))
|
||||
msb := int32(binary.BigEndian.Uint32(data[4:8]))
|
||||
|
||||
if lsb != msb {
|
||||
return 0, fmt.Errorf("little-endian and big-endian value mismatch: %d != %d", lsb, msb)
|
||||
}
|
||||
|
||||
return lsb, nil
|
||||
}
|
||||
|
||||
// UnmarshalUint32LSBMSB is the same as UnmarshalInt32LSBMSB but returns an unsigned integer
|
||||
func UnmarshalUint32LSBMSB(data []byte) (uint32, error) {
|
||||
n, err := UnmarshalInt32LSBMSB(data)
|
||||
return uint32(n), err
|
||||
}
|
||||
|
||||
// UnmarshalInt16LSBMSB decodes a 16-bit integer in both byte orders, as defined in ECMA-119 7.3.3
|
||||
func UnmarshalInt16LSBMSB(data []byte) (int16, error) {
|
||||
if len(data) < 4 {
|
||||
return 0, io.ErrUnexpectedEOF
|
||||
}
|
||||
|
||||
lsb := int16(binary.LittleEndian.Uint16(data[0:2]))
|
||||
msb := int16(binary.BigEndian.Uint16(data[2:4]))
|
||||
|
||||
if lsb != msb {
|
||||
return 0, fmt.Errorf("little-endian and big-endian value mismatch: %d != %d", lsb, msb)
|
||||
}
|
||||
|
||||
return lsb, nil
|
||||
}
|
||||
|
||||
// WriteInt32LSBMSB writes a 32-bit integer in both byte orders, as defined in ECMA-119 7.3.3
|
||||
func WriteInt32LSBMSB(dst []byte, value int32) {
|
||||
_ = dst[7] // early bounds check to guarantee safety of writes below
|
||||
binary.LittleEndian.PutUint32(dst[0:4], uint32(value))
|
||||
binary.BigEndian.PutUint32(dst[4:8], uint32(value))
|
||||
}
|
||||
|
||||
// WriteInt16LSBMSB writes a 16-bit integer in both byte orders, as defined in ECMA-119 7.2.3
|
||||
func WriteInt16LSBMSB(dst []byte, value int16) {
|
||||
_ = dst[3] // early bounds check to guarantee safety of writes below
|
||||
binary.LittleEndian.PutUint16(dst[0:2], uint16(value))
|
||||
binary.BigEndian.PutUint16(dst[2:4], uint16(value))
|
||||
}
|
||||
+99
@@ -0,0 +1,99 @@
|
||||
package iso9660
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
)
|
||||
|
||||
/* The following types of Rock Ridge records are being handled in some way:
|
||||
* - [X] PX (RR 4.1.1: POSIX file attributes)
|
||||
* - [ ] PN (RR 4.1.2: POSIX device number)
|
||||
* - [ ] SL (RR 4.1.3: symbolic link)
|
||||
* - [x] NM (RR 4.1.4: alternate name)
|
||||
* - [ ] CL (RR 4.1.5.1: child link)
|
||||
* - [ ] PL (RR 4.1.5.2: parent link)
|
||||
* - [ ] RE (RR 4.1.5.3: relocated directory)
|
||||
* - [ ] TF (RR 4.1.6: time stamp(s) for a file)
|
||||
* - [ ] SF (RR 4.1.7: file data in sparse file format)
|
||||
*/
|
||||
|
||||
const (
|
||||
RockRidgeIdentifier = "RRIP_1991A"
|
||||
RockRidgeVersion = 1
|
||||
)
|
||||
|
||||
type RockRidgeNameEntry struct {
|
||||
Flags byte
|
||||
Name string
|
||||
}
|
||||
|
||||
func suspHasRockRidge(se SystemUseEntrySlice) (bool, error) {
|
||||
extensions, err := se.GetExtensionRecords()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
for _, entry := range extensions {
|
||||
if entry.Identifier == RockRidgeIdentifier && entry.Version == RockRidgeVersion {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (s SystemUseEntrySlice) GetRockRidgeName() string {
|
||||
var name string
|
||||
|
||||
for _, entry := range s {
|
||||
// There is a continuation flag in the record, but we determine continuation
|
||||
// by simply reading all NM entries.
|
||||
if entry.Type() == "NM" {
|
||||
nm := umarshalRockRidgeNameEntry(entry)
|
||||
name += nm.Name
|
||||
}
|
||||
}
|
||||
|
||||
return name
|
||||
}
|
||||
|
||||
func (s SystemUseEntrySlice) GetPosixAttr() (fs.FileMode, error) {
|
||||
for _, entry := range s {
|
||||
if entry.Type() == "PX" {
|
||||
// BUG(kdomanski): If there are multiple RR PX entries (which is forbidden by the spec), the reader will use the first one.
|
||||
return umarshalRockRidgeAttrEntry(entry)
|
||||
}
|
||||
}
|
||||
|
||||
return 0, fmt.Errorf("mandatory entry PX not found")
|
||||
}
|
||||
|
||||
func umarshalRockRidgeAttrEntry(e SystemUseEntry) (fs.FileMode, error) {
|
||||
rrMode, err := UnmarshalUint32LSBMSB(e.Data()[0:8])
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("unmarshall RR PX entry: %w", err)
|
||||
}
|
||||
|
||||
S_IFLNK := (rrMode & 0170000) == 0120000
|
||||
S_IFDIR := (rrMode & 0170000) == 0040000
|
||||
|
||||
mode := rrMode & uint32(fs.ModePerm) // UNIX permissions
|
||||
|
||||
if S_IFLNK {
|
||||
mode |= uint32(os.ModeSymlink)
|
||||
}
|
||||
|
||||
if S_IFDIR {
|
||||
mode |= uint32(os.ModeDir)
|
||||
}
|
||||
|
||||
return fs.FileMode(mode), nil
|
||||
}
|
||||
|
||||
func umarshalRockRidgeNameEntry(e SystemUseEntry) *RockRidgeNameEntry {
|
||||
return &RockRidgeNameEntry{
|
||||
Flags: e.Data()[0],
|
||||
Name: string(e.Data()[1:]),
|
||||
}
|
||||
}
|
||||
+211
@@ -0,0 +1,211 @@
|
||||
package iso9660
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
)
|
||||
|
||||
/* The following types of core SUSP records are being handled in some way:
|
||||
* - [x] CE (SUSP 5.1: continuation)
|
||||
* - [ ] PD (SUSP 5.2: padding)
|
||||
* - [x] SP (SUSP 5.3: offset)
|
||||
* - [ ] ST (SUSP 5.4)
|
||||
* - [x] ER (SUSP 5.5: extension record)
|
||||
* - [ ] ES (SUSP 5.6)
|
||||
*/
|
||||
|
||||
// SUSP-112 4.1
|
||||
type SystemUseEntry []byte
|
||||
|
||||
func (e SystemUseEntry) Length() int {
|
||||
return int(e[2])
|
||||
}
|
||||
|
||||
func (e SystemUseEntry) Data() []byte {
|
||||
return e[4:]
|
||||
}
|
||||
|
||||
func (e SystemUseEntry) Type() string {
|
||||
return string(e[:2])
|
||||
}
|
||||
|
||||
type ExtensionRecord struct {
|
||||
Version int
|
||||
Identifier string
|
||||
Descriptor string
|
||||
Source string
|
||||
}
|
||||
|
||||
// See SUSP-112 5.5
|
||||
func ExtensionRecordDecode(e SystemUseEntry) (*ExtensionRecord, error) {
|
||||
if e.Type() != "ER" {
|
||||
return nil, fmt.Errorf("wrong type of record, expected ER")
|
||||
}
|
||||
if e.Length() < 8 {
|
||||
return nil, io.ErrUnexpectedEOF
|
||||
}
|
||||
|
||||
identifierLen := int(e[4])
|
||||
if e.Length() < 8+identifierLen {
|
||||
return nil, io.ErrUnexpectedEOF
|
||||
}
|
||||
|
||||
descriptorLen := int(e[5])
|
||||
if e.Length() < 8+identifierLen+descriptorLen {
|
||||
return nil, io.ErrUnexpectedEOF
|
||||
}
|
||||
|
||||
sourceLen := int(e[6])
|
||||
if e.Length() < 8+identifierLen+descriptorLen+sourceLen {
|
||||
return nil, io.ErrUnexpectedEOF
|
||||
}
|
||||
|
||||
return &ExtensionRecord{
|
||||
Version: int(e[7]),
|
||||
Identifier: string(e[8 : 8+identifierLen]),
|
||||
Descriptor: string(e[8+identifierLen : 8+identifierLen+descriptorLen]),
|
||||
Source: string(e[8+identifierLen+descriptorLen : 8+identifierLen+descriptorLen+sourceLen]),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// See SUSP-112 5.3
|
||||
func SPRecordDecode(e SystemUseEntry) (*SPRecord, error) {
|
||||
if e.Type() != "SP" {
|
||||
return nil, fmt.Errorf("wrong type of record, expected SP")
|
||||
}
|
||||
if e.Length() < 7 {
|
||||
return nil, io.ErrUnexpectedEOF
|
||||
}
|
||||
|
||||
if beByte := e[4]; beByte != 0xBE {
|
||||
return nil, fmt.Errorf("invalid control byte, %x != 0xBE", beByte)
|
||||
}
|
||||
if efByte := e[5]; efByte != 0xEF {
|
||||
return nil, fmt.Errorf("invalid control byte, %x != 0xEF", efByte)
|
||||
}
|
||||
|
||||
return &SPRecord{
|
||||
BytesSkipped: e[6],
|
||||
}, nil
|
||||
}
|
||||
|
||||
type SPRecord struct {
|
||||
BytesSkipped uint8
|
||||
}
|
||||
|
||||
type SystemUseEntrySlice []SystemUseEntry
|
||||
|
||||
func (s SystemUseEntrySlice) GetExtensionRecords() ([]*ExtensionRecord, error) {
|
||||
results := make([]*ExtensionRecord, 0)
|
||||
for _, entry := range s {
|
||||
if entry.Type() == "ER" {
|
||||
er, err := ExtensionRecordDecode(entry)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
results = append(results, er)
|
||||
}
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// SUSP-112 5.1
|
||||
type ContinuationEntry struct {
|
||||
blockLocation uint32
|
||||
offset uint32
|
||||
lengthOfArea uint32
|
||||
}
|
||||
|
||||
func umarshalContinuationEntry(e SystemUseEntry) (*ContinuationEntry, error) {
|
||||
if e.Length() != 28 {
|
||||
return nil, fmt.Errorf("invalid ContinuationArea record with length %d instead of 28", e.Length())
|
||||
}
|
||||
|
||||
location, err := UnmarshalUint32LSBMSB(e.Data()[0:8])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("block location: %w", err)
|
||||
}
|
||||
offset, err := UnmarshalUint32LSBMSB(e.Data()[8:16])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("offset: %w", err)
|
||||
}
|
||||
length, err := UnmarshalUint32LSBMSB(e.Data()[16:24])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("length: %w", err)
|
||||
}
|
||||
|
||||
return &ContinuationEntry{
|
||||
blockLocation: location,
|
||||
offset: offset,
|
||||
lengthOfArea: length,
|
||||
}, nil
|
||||
}
|
||||
|
||||
const (
|
||||
SUEType_ContinuationArea = "CE"
|
||||
SUEType_PaddingField = "PD"
|
||||
SUEType_SharingProtocolIndicator = "SP"
|
||||
SUEType_SharingProtocolTerminator = "ST"
|
||||
SUEType_ExtensionsReference = "ER"
|
||||
SUEType_ExtensionSelector = "ES"
|
||||
)
|
||||
|
||||
func splitSystemUseEntries(data []byte, ra io.ReaderAt) ([]SystemUseEntry, error) {
|
||||
output := make([]SystemUseEntry, 0)
|
||||
|
||||
for len(data) > 0 {
|
||||
if len(data) < 4 {
|
||||
// SUSP-112 4
|
||||
// If the remaining allocated space /.../ is less than four bytes long /.../ shall be ignored.
|
||||
break
|
||||
}
|
||||
|
||||
entryLen := int(data[2])
|
||||
if len(data) < entryLen {
|
||||
return nil, fmt.Errorf("splitting System Use entries: %w, expected %d bytes but have only %d", io.ErrUnexpectedEOF, entryLen, len(data))
|
||||
}
|
||||
|
||||
entry := SystemUseEntry(data[:entryLen])
|
||||
|
||||
if entry.Type() == SUEType_ContinuationArea {
|
||||
ce, err := umarshalContinuationEntry(entry)
|
||||
if err != nil {
|
||||
return output, fmt.Errorf("unmarshaling ContinuationEntry: %w", err)
|
||||
}
|
||||
continuation := make([]byte, ce.lengthOfArea)
|
||||
finalOffset := (ce.blockLocation * sectorSize) + ce.offset
|
||||
if _, err := ra.ReadAt(continuation, int64(finalOffset)); err != nil {
|
||||
return output, fmt.Errorf("reading Continuation Area: %w", err)
|
||||
}
|
||||
|
||||
continuedEntries, err := splitSystemUseEntries(continuation, ra)
|
||||
if err != nil {
|
||||
return output, fmt.Errorf("splitting Continuation Area: %w", err)
|
||||
}
|
||||
output = append(output, continuedEntries...)
|
||||
} else {
|
||||
output = append(output, entry)
|
||||
}
|
||||
|
||||
data = data[entryLen:]
|
||||
}
|
||||
|
||||
return output, nil
|
||||
}
|
||||
|
||||
type SUSPMetadata struct {
|
||||
Offset uint8
|
||||
HasRockRidge bool
|
||||
}
|
||||
|
||||
func (sm *SUSPMetadata) Clone() *SUSPMetadata {
|
||||
if sm == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &SUSPMetadata{
|
||||
Offset: sm.Offset,
|
||||
HasRockRidge: sm.HasRockRidge,
|
||||
}
|
||||
}
|
||||
Generated
+14
@@ -0,0 +1,14 @@
|
||||
# Binaries for programs and plugins
|
||||
*.exe
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
|
||||
# Test binary, build with `go test -c`
|
||||
*.test
|
||||
|
||||
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||
*.out
|
||||
|
||||
# Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736
|
||||
.glide/
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
BSD 3-Clause License
|
||||
|
||||
Copyright (c) 2017, Vladimir Jigulin
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
* Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
* Neither the name of the copyright holder nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
## Udf filesystem golang library
|
||||
- Non-optimized
|
||||
- Some functioal is broken
|
||||
- `recovery()` style error handling interface
|
||||
- Work only with certain iso's
|
||||
|
||||
It's all because I has reached requried functional for me.
|
||||
|
||||
## Example
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"github.com/mogaika/udf"
|
||||
)
|
||||
|
||||
func main() {
|
||||
r, _ := os.Open("example.iso")
|
||||
u := udf.NewUdfFromReader(r)
|
||||
for _, f := range u.ReadDir(nil) {
|
||||
fmt.Printf("%s %-10d %-20s %v\n", f.Mode().String(), f.Size(), f.Name(), f.ModTime())
|
||||
}
|
||||
}
|
||||
```
|
||||
Output:
|
||||
```
|
||||
-r-xr-xr-x 57 system.cnf 2006-02-11 00:00:00 +0000 UTC
|
||||
-r-xr-xr-x 1911580 SCUS_973.99 2006-03-15 00:00:00 +0000 UTC
|
||||
-r-xr-xr-x 278305 ioprp300.img 2005-11-14 00:00:00 +0000 UTC
|
||||
-r-xr-xr-x 6641 sio2man.irx 2005-10-18 00:00:00 +0000 UTC
|
||||
-r-xr-xr-x 15653 dbcman.irx 2005-10-18 00:00:00 +0000 UTC
|
||||
```
|
||||
|
||||
|
||||
|
||||
+99
@@ -0,0 +1,99 @@
|
||||
package udf
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"time"
|
||||
|
||||
"golang.org/x/text/encoding/charmap"
|
||||
"golang.org/x/text/encoding/unicode"
|
||||
"golang.org/x/text/transform"
|
||||
)
|
||||
|
||||
func r_u8(b []byte) uint8 {
|
||||
return b[0]
|
||||
}
|
||||
|
||||
func r_i8(b []byte) int8 {
|
||||
return int8(r_u8(b))
|
||||
}
|
||||
|
||||
var rl_u64 = binary.LittleEndian.Uint64
|
||||
|
||||
func rl_u48(b []byte) uint64 {
|
||||
var buf [8]byte
|
||||
copy(buf[:], b[:6])
|
||||
return rl_u64(buf[:])
|
||||
}
|
||||
|
||||
var rl_u32 = binary.LittleEndian.Uint32
|
||||
var rl_u16 = binary.LittleEndian.Uint16
|
||||
|
||||
func rl_i64(b []byte) int64 {
|
||||
return int64(rl_u64(b))
|
||||
}
|
||||
|
||||
func rl_i32(b []byte) int32 {
|
||||
return int32(rl_u32(b))
|
||||
}
|
||||
|
||||
func rl_i16(b []byte) int16 {
|
||||
return int16(rl_u16(b))
|
||||
}
|
||||
|
||||
var rb_u64 = binary.BigEndian.Uint64
|
||||
var rb_u32 = binary.BigEndian.Uint32
|
||||
var rb_u16 = binary.BigEndian.Uint16
|
||||
|
||||
func rb_u8(b []byte) uint8 {
|
||||
return b[0]
|
||||
}
|
||||
|
||||
func rb_i64(b []byte) int64 {
|
||||
return int64(rb_u64(b))
|
||||
}
|
||||
|
||||
func rb_i32(b []byte) int32 {
|
||||
return int32(rb_u32(b))
|
||||
}
|
||||
|
||||
func rb_i16(b []byte) int16 {
|
||||
return int16(rb_u16(b))
|
||||
}
|
||||
|
||||
func r_dstring(b []byte, fieldlen int) string {
|
||||
if fieldlen == 0 {
|
||||
return ""
|
||||
}
|
||||
return string(b[:b[fieldlen-1]])
|
||||
}
|
||||
|
||||
func r_dcharacters(b []byte) string {
|
||||
if len(b) == 0 {
|
||||
return ""
|
||||
}
|
||||
switch b[0] {
|
||||
case 8:
|
||||
s, _, err := transform.Bytes(charmap.Windows1252.NewDecoder(), b[1:])
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return string(s)
|
||||
case 16:
|
||||
s, _, err := transform.Bytes(unicode.UTF16(unicode.BigEndian, unicode.IgnoreBOM).NewDecoder(), b[1:])
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return string(s)
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func r_timestamp(b []byte) time.Time {
|
||||
var t time.Time
|
||||
t = t.AddDate(int(rl_u16(b[2:])), int(b[4]), int(b[5]))
|
||||
t.Add(time.Duration(b[6])*time.Hour +
|
||||
time.Duration(b[7])*time.Minute +
|
||||
time.Duration(b[8])*time.Second)
|
||||
return t
|
||||
}
|
||||
+368
@@ -0,0 +1,368 @@
|
||||
package udf
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
DESCRIPTOR_PRIMARY_VOLUME = 0x1
|
||||
DESCRIPTOR_ANCHOR_VOLUME_POINTER = 0x2
|
||||
DESCRIPTOR_VOLUME_POINTER = 0x3
|
||||
DESCRIPTOR_IMPLEMENTATION_USE_VOLUME = 0x4
|
||||
DESCRIPTOR_PARTITION = 0x5
|
||||
DESCRIPTOR_LOGICAL_VOLUME = 0x6
|
||||
DESCRIPTOR_UNALLOCATED = 0x7
|
||||
DESCRIPTOR_TERMINATING = 0x8
|
||||
DESCRIPTOR_FILE_SET = 0x100
|
||||
DESCRIPTOR_IDENTIFIER = 0x101
|
||||
DESCRIPTOR_ALLOCATION_EXTENT = 0x102
|
||||
DESCRIPTOR_INDIRECT_ENTRY = 0x103
|
||||
DESCRIPTOR_TERMINAL_ENTRY = 0x104
|
||||
DESCRIPTOR_FILE_ENTRY = 0x105
|
||||
)
|
||||
|
||||
type Descriptor struct {
|
||||
TagIdentifier uint16
|
||||
DescriptorVersion uint16
|
||||
TagChecksum uint8
|
||||
TagSerialNumber uint16
|
||||
DescriptorCRC uint16
|
||||
DescriptorCRCLength uint16
|
||||
TagLocation uint32
|
||||
data []byte
|
||||
}
|
||||
|
||||
func (d *Descriptor) Data() []byte {
|
||||
buf := make([]byte, len(d.data))
|
||||
copy(buf, d.data[16:])
|
||||
return buf
|
||||
}
|
||||
|
||||
func (d *Descriptor) FromBytes(b []byte) *Descriptor {
|
||||
d.TagIdentifier = rl_u16(b[0:])
|
||||
d.DescriptorVersion = rl_u16(b[2:])
|
||||
d.TagChecksum = r_u8(b[3:])
|
||||
d.TagSerialNumber = rl_u16(b[6:])
|
||||
d.DescriptorCRC = rl_u16(b[8:])
|
||||
d.DescriptorCRCLength = rl_u16(b[10:])
|
||||
d.TagLocation = rl_u32(b[12:])
|
||||
d.data = b[:]
|
||||
return d
|
||||
}
|
||||
|
||||
func NewDescriptor(b []byte) *Descriptor {
|
||||
return new(Descriptor).FromBytes(b)
|
||||
}
|
||||
|
||||
type AnchorVolumeDescriptorPointer struct {
|
||||
Descriptor Descriptor
|
||||
MainVolumeDescriptorSeq Extent
|
||||
ReserveVolumeDescriptorSeq Extent
|
||||
}
|
||||
|
||||
func (ad *AnchorVolumeDescriptorPointer) FromBytes(b []byte) *AnchorVolumeDescriptorPointer {
|
||||
ad.Descriptor.FromBytes(b)
|
||||
ad.MainVolumeDescriptorSeq = NewExtent(b[16:])
|
||||
ad.ReserveVolumeDescriptorSeq = NewExtent(b[24:])
|
||||
return ad
|
||||
}
|
||||
|
||||
func NewAnchorVolumeDescriptorPointer(b []byte) *AnchorVolumeDescriptorPointer {
|
||||
return new(AnchorVolumeDescriptorPointer).FromBytes(b)
|
||||
}
|
||||
|
||||
func (d *Descriptor) AnchorVolumeDescriptorPointer() *AnchorVolumeDescriptorPointer {
|
||||
return NewAnchorVolumeDescriptorPointer(d.data)
|
||||
}
|
||||
|
||||
type PrimaryVolumeDescriptor struct {
|
||||
Descriptor Descriptor
|
||||
VolumeDescriptorSequenceNumber uint32
|
||||
PrimaryVolumeDescriptorNumber uint32
|
||||
VolumeIdentifier string
|
||||
VolumeSequenceNumber uint16
|
||||
MaximumVolumeSequenceNumber uint16
|
||||
InterchangeLevel uint16
|
||||
MaximumInterchangeLevel uint16
|
||||
CharacterSetList uint32
|
||||
MaximumCharacterSetList uint32
|
||||
VolumeSetIdentifier string
|
||||
VolumeAbstract Extent
|
||||
VolumeCopyrightNoticeExtent Extent
|
||||
ApplicationIdentifier EntityID
|
||||
RecordingDateTime time.Time
|
||||
ImplementationIdentifier EntityID
|
||||
ImplementationUse []byte
|
||||
PredecessorVolumeDescriptorSequenceLocation uint32
|
||||
Flags uint16
|
||||
}
|
||||
|
||||
func (pvd *PrimaryVolumeDescriptor) FromBytes(b []byte) *PrimaryVolumeDescriptor {
|
||||
pvd.Descriptor.FromBytes(b)
|
||||
pvd.VolumeDescriptorSequenceNumber = rl_u32(b[16:])
|
||||
pvd.PrimaryVolumeDescriptorNumber = rl_u32(b[20:])
|
||||
pvd.VolumeIdentifier = r_dstring(b[24:], 32)
|
||||
pvd.VolumeSequenceNumber = rl_u16(b[56:])
|
||||
pvd.MaximumVolumeSequenceNumber = rl_u16(b[58:])
|
||||
pvd.InterchangeLevel = rl_u16(b[60:])
|
||||
pvd.MaximumInterchangeLevel = rl_u16(b[62:])
|
||||
pvd.CharacterSetList = rl_u32(b[64:])
|
||||
pvd.MaximumCharacterSetList = rl_u32(b[68:])
|
||||
pvd.VolumeSetIdentifier = r_dstring(b[72:], 128)
|
||||
pvd.VolumeAbstract = NewExtent(b[328:])
|
||||
pvd.VolumeCopyrightNoticeExtent = NewExtent(b[336:])
|
||||
pvd.ApplicationIdentifier = NewEntityID(b[344:])
|
||||
pvd.RecordingDateTime = r_timestamp(b[376:])
|
||||
pvd.ImplementationIdentifier = NewEntityID(b[388:])
|
||||
pvd.ImplementationUse = b[420:484]
|
||||
pvd.PredecessorVolumeDescriptorSequenceLocation = rl_u32(b[484:])
|
||||
pvd.Flags = rl_u16(b[488:])
|
||||
return pvd
|
||||
}
|
||||
|
||||
func NewPrimaryVolumeDescriptor(b []byte) *PrimaryVolumeDescriptor {
|
||||
return new(PrimaryVolumeDescriptor).FromBytes(b)
|
||||
}
|
||||
|
||||
func (d *Descriptor) PrimaryVolumeDescriptor() *PrimaryVolumeDescriptor {
|
||||
return NewPrimaryVolumeDescriptor(d.data)
|
||||
}
|
||||
|
||||
type PartitionDescriptor struct {
|
||||
Descriptor Descriptor
|
||||
VolumeDescriptorSequenceNumber uint32
|
||||
PartitionFlags uint16
|
||||
PartitionNumber uint16
|
||||
PartitionContents EntityID
|
||||
PartitionContentsUse []byte
|
||||
AccessType uint32
|
||||
PartitionStartingLocation uint32
|
||||
PartitionLength uint32
|
||||
ImplementationIdentifier EntityID
|
||||
ImplementationUse []byte
|
||||
}
|
||||
|
||||
func (pd *PartitionDescriptor) FromBytes(b []byte) *PartitionDescriptor {
|
||||
pd.Descriptor.FromBytes(b)
|
||||
pd.VolumeDescriptorSequenceNumber = rl_u32(b[16:])
|
||||
pd.PartitionFlags = rl_u16(b[20:])
|
||||
pd.PartitionNumber = rl_u16(b[22:])
|
||||
pd.PartitionContents = NewEntityID(b[24:])
|
||||
pd.PartitionContentsUse = b[56:184]
|
||||
pd.AccessType = rl_u32(b[184:])
|
||||
pd.PartitionStartingLocation = rl_u32(b[188:])
|
||||
pd.PartitionLength = rl_u32(b[192:])
|
||||
pd.ImplementationIdentifier = NewEntityID(b[196:])
|
||||
pd.ImplementationUse = b[228:356]
|
||||
return pd
|
||||
}
|
||||
|
||||
func NewPartitionDescriptor(b []byte) *PartitionDescriptor {
|
||||
return new(PartitionDescriptor).FromBytes(b)
|
||||
}
|
||||
|
||||
func (d *Descriptor) PartitionDescriptor() *PartitionDescriptor {
|
||||
return NewPartitionDescriptor(d.data)
|
||||
}
|
||||
|
||||
type PartitionMap struct {
|
||||
PartitionMapType uint8
|
||||
PartitionMapLength uint8
|
||||
VolumeSequenceNumber uint16
|
||||
PartitionNumber uint16
|
||||
}
|
||||
|
||||
func (pm *PartitionMap) FromBytes(b []byte) *PartitionMap {
|
||||
pm.PartitionMapType = rb_u8(b[0:])
|
||||
pm.PartitionMapLength = rb_u8(b[1:])
|
||||
pm.VolumeSequenceNumber = rb_u16(b[2:])
|
||||
pm.PartitionNumber = rb_u16(b[4:])
|
||||
return pm
|
||||
}
|
||||
|
||||
type LogicalVolumeDescriptor struct {
|
||||
Descriptor Descriptor
|
||||
VolumeDescriptorSequenceNumber uint32
|
||||
LogicalVolumeIdentifier string
|
||||
LogicalBlockSize uint32
|
||||
DomainIdentifier EntityID
|
||||
LogicalVolumeContentsUse ExtentLong
|
||||
MapTableLength uint32
|
||||
NumberOfPartitionMaps uint32
|
||||
ImplementationIdentifier EntityID
|
||||
ImplementationUse []byte
|
||||
IntegritySequenceExtent Extent
|
||||
PartitionMaps []PartitionMap
|
||||
}
|
||||
|
||||
func (lvd *LogicalVolumeDescriptor) FromBytes(b []byte) *LogicalVolumeDescriptor {
|
||||
lvd.Descriptor.FromBytes(b)
|
||||
lvd.VolumeDescriptorSequenceNumber = rl_u32(b[16:])
|
||||
lvd.LogicalVolumeIdentifier = r_dstring(b[84:], 128)
|
||||
lvd.LogicalBlockSize = rl_u32(b[212:])
|
||||
lvd.DomainIdentifier = NewEntityID(b[216:])
|
||||
lvd.LogicalVolumeContentsUse = NewExtentLong(b[248:])
|
||||
lvd.MapTableLength = rl_u32(b[264:])
|
||||
lvd.NumberOfPartitionMaps = rl_u32(b[268:])
|
||||
lvd.ImplementationIdentifier = NewEntityID(b[272:])
|
||||
lvd.ImplementationUse = b[304:432]
|
||||
lvd.IntegritySequenceExtent = NewExtent(b[432:])
|
||||
lvd.PartitionMaps = make([]PartitionMap, lvd.NumberOfPartitionMaps)
|
||||
for i := range lvd.PartitionMaps {
|
||||
lvd.PartitionMaps[i].FromBytes(b[440+i*6:])
|
||||
}
|
||||
return lvd
|
||||
}
|
||||
|
||||
func NewLogicalVolumeDescriptor(b []byte) *LogicalVolumeDescriptor {
|
||||
return new(LogicalVolumeDescriptor).FromBytes(b)
|
||||
}
|
||||
|
||||
func (d *Descriptor) LogicalVolumeDescriptor() *LogicalVolumeDescriptor {
|
||||
return NewLogicalVolumeDescriptor(d.data)
|
||||
}
|
||||
|
||||
type FileSetDescriptor struct {
|
||||
Descriptor Descriptor
|
||||
RecordingDateTime time.Time
|
||||
InterchangeLevel uint16
|
||||
MaximumInterchangeLevel uint16
|
||||
CharacterSetList uint32
|
||||
MaximumCharacterSetList uint32
|
||||
FileSetNumber uint32
|
||||
FileSetDescriptorNumber uint32
|
||||
LogicalVolumeIdentifier string
|
||||
FileSetIdentifier string
|
||||
CopyrightFileIdentifier string
|
||||
AbstractFileIdentifier string
|
||||
RootDirectoryICB ExtentLong
|
||||
DomainIdentifier EntityID
|
||||
NexExtent ExtentLong
|
||||
}
|
||||
|
||||
func (fsd *FileSetDescriptor) FromBytes(b []byte) *FileSetDescriptor {
|
||||
fsd.Descriptor.FromBytes(b)
|
||||
fsd.RecordingDateTime = r_timestamp(b[16:])
|
||||
fsd.InterchangeLevel = rl_u16(b[28:])
|
||||
fsd.MaximumInterchangeLevel = rl_u16(b[30:])
|
||||
fsd.CharacterSetList = rl_u32(b[32:])
|
||||
fsd.MaximumCharacterSetList = rl_u32(b[36:])
|
||||
fsd.FileSetNumber = rl_u32(b[40:])
|
||||
fsd.FileSetDescriptorNumber = rl_u32(b[44:])
|
||||
fsd.LogicalVolumeIdentifier = r_dstring(b[112:], 128)
|
||||
fsd.FileSetIdentifier = r_dstring(b[304:], 32)
|
||||
fsd.CopyrightFileIdentifier = r_dstring(b[336:], 32)
|
||||
fsd.AbstractFileIdentifier = r_dstring(b[368:], 32)
|
||||
fsd.RootDirectoryICB = NewExtentLong(b[400:])
|
||||
fsd.DomainIdentifier = NewEntityID(b[416:])
|
||||
fsd.NexExtent = NewExtentLong(b[448:])
|
||||
return fsd
|
||||
}
|
||||
|
||||
func NewFileSetDescriptor(b []byte) *FileSetDescriptor {
|
||||
return new(FileSetDescriptor).FromBytes(b)
|
||||
}
|
||||
|
||||
func (d *Descriptor) FileSetDescriptor() *FileSetDescriptor {
|
||||
return NewFileSetDescriptor(d.data)
|
||||
}
|
||||
|
||||
type FileIdentifierDescriptor struct {
|
||||
Descriptor Descriptor
|
||||
FileVersionNumber uint16
|
||||
FileCharacteristics uint8
|
||||
LengthOfFileIdentifier uint8
|
||||
ICB ExtentLong
|
||||
LengthOfImplementationUse uint16
|
||||
ImplementationUse EntityID
|
||||
FileIdentifier string
|
||||
}
|
||||
|
||||
func (fid *FileIdentifierDescriptor) Len() uint64 {
|
||||
l := 38 + uint64(fid.LengthOfImplementationUse) + uint64(fid.LengthOfFileIdentifier)
|
||||
return 4 * ((l + 3) / 4) // padding = 4
|
||||
}
|
||||
|
||||
func (fid *FileIdentifierDescriptor) FromBytes(b []byte) *FileIdentifierDescriptor {
|
||||
fid.Descriptor.FromBytes(b)
|
||||
fid.FileVersionNumber = rl_u16(b[16:])
|
||||
fid.FileCharacteristics = r_u8(b[18:])
|
||||
fid.LengthOfFileIdentifier = r_u8(b[19:])
|
||||
fid.ICB = NewExtentLong(b[20:])
|
||||
fid.LengthOfImplementationUse = rl_u16(b[36:])
|
||||
fid.ImplementationUse = NewEntityID(b[38:])
|
||||
identStart := 38 + fid.LengthOfImplementationUse
|
||||
fid.FileIdentifier = r_dcharacters(b[identStart : fid.LengthOfFileIdentifier+uint8(identStart)])
|
||||
return fid
|
||||
}
|
||||
|
||||
func NewFileIdentifierDescriptor(b []byte) *FileIdentifierDescriptor {
|
||||
return new(FileIdentifierDescriptor).FromBytes(b)
|
||||
}
|
||||
|
||||
func (d *Descriptor) FileIdentifierDescriptor() *FileIdentifierDescriptor {
|
||||
return NewFileIdentifierDescriptor(d.data)
|
||||
}
|
||||
|
||||
type FileEntry struct {
|
||||
Descriptor Descriptor
|
||||
ICBTag *ICBTag
|
||||
Uid uint32
|
||||
Gid uint32
|
||||
Permissions uint32
|
||||
FileLinkCount uint16
|
||||
RecordFormat uint8
|
||||
RecordDisplayAttributes uint8
|
||||
RecordLength uint32
|
||||
InformationLength uint64
|
||||
LogicalBlocksRecorded uint64
|
||||
AccessTime time.Time
|
||||
ModificationTime time.Time
|
||||
AttributeTime time.Time
|
||||
Checkpoint uint32
|
||||
ExtendedAttributeICB ExtentLong
|
||||
ImplementationIdentifier EntityID
|
||||
UniqueId uint64
|
||||
LengthOfExtendedAttributes uint32
|
||||
LengthOfAllocationDescriptors uint32
|
||||
ExtendedAttributes []byte
|
||||
AllocationDescriptors []Extent
|
||||
}
|
||||
|
||||
func (fe *FileEntry) FromBytes(b []byte) *FileEntry {
|
||||
fe.Descriptor.FromBytes(b)
|
||||
fe.ICBTag = NewICBTag(b[16:])
|
||||
fe.Uid = rl_u32(b[36:])
|
||||
fe.Gid = rl_u32(b[40:])
|
||||
fe.Permissions = rl_u32(b[44:])
|
||||
fe.FileLinkCount = rl_u16(b[48:])
|
||||
fe.RecordFormat = r_u8(b[50:])
|
||||
fe.RecordDisplayAttributes = r_u8(b[51:])
|
||||
fe.RecordLength = rl_u32(b[52:])
|
||||
fe.InformationLength = rl_u64(b[56:])
|
||||
fe.LogicalBlocksRecorded = rl_u64(b[64:])
|
||||
fe.AccessTime = r_timestamp(b[72:])
|
||||
fe.ModificationTime = r_timestamp(b[84:])
|
||||
fe.AttributeTime = r_timestamp(b[96:])
|
||||
fe.Checkpoint = rl_u32(b[108:])
|
||||
fe.ExtendedAttributeICB = NewExtentLong(b[112:])
|
||||
fe.ImplementationIdentifier = NewEntityID(b[128:])
|
||||
fe.UniqueId = rl_u64(b[160:])
|
||||
fe.LengthOfExtendedAttributes = rl_u32(b[168:])
|
||||
fe.LengthOfAllocationDescriptors = rl_u32(b[172:])
|
||||
allocDescStart := 176 + fe.LengthOfExtendedAttributes
|
||||
fe.ExtendedAttributes = b[176:allocDescStart]
|
||||
fe.AllocationDescriptors = make([]Extent, fe.LengthOfAllocationDescriptors/8)
|
||||
for i := range fe.AllocationDescriptors {
|
||||
fe.AllocationDescriptors[i] = NewExtent(b[allocDescStart+uint32(i)*8:])
|
||||
}
|
||||
return fe
|
||||
}
|
||||
|
||||
func NewFileEntry(b []byte) *FileEntry {
|
||||
return new(FileEntry).FromBytes(b)
|
||||
}
|
||||
|
||||
func (d *Descriptor) FileEntry() *FileEntry {
|
||||
return NewFileEntry(d.data)
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
package udf
|
||||
|
||||
type EntityID struct {
|
||||
Flags uint8
|
||||
Identifier [23]byte
|
||||
IdentifierSuffix [8]byte
|
||||
}
|
||||
|
||||
func NewEntityID(b []byte) EntityID {
|
||||
e := EntityID{Flags: b[0]}
|
||||
copy(e.Identifier[:], b[1:24])
|
||||
copy(e.IdentifierSuffix[:], b[24:32])
|
||||
return e
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
package udf
|
||||
|
||||
type Extent struct {
|
||||
Length uint32
|
||||
Location uint32
|
||||
}
|
||||
|
||||
func NewExtent(b []byte) Extent {
|
||||
return Extent{
|
||||
Length: rl_u32(b[0:]),
|
||||
Location: rl_u32(b[4:]),
|
||||
}
|
||||
}
|
||||
|
||||
type ExtentSmall struct {
|
||||
Length uint16
|
||||
Location uint64
|
||||
}
|
||||
|
||||
func NewExtentSmall(b []byte) ExtentSmall {
|
||||
return ExtentSmall{
|
||||
Length: rl_u16(b[0:]),
|
||||
Location: rl_u48(b[2:]),
|
||||
}
|
||||
}
|
||||
|
||||
type ExtentLong struct {
|
||||
Length uint32
|
||||
Location uint64
|
||||
}
|
||||
|
||||
func NewExtentLong(b []byte) ExtentLong {
|
||||
return ExtentLong{
|
||||
Length: rl_u32(b[0:]),
|
||||
Location: rl_u48(b[4:]),
|
||||
}
|
||||
}
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
package udf
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
type File struct {
|
||||
Udf *Udf
|
||||
Fid *FileIdentifierDescriptor
|
||||
fe *FileEntry
|
||||
fileEntryPosition uint64
|
||||
}
|
||||
|
||||
func (f *File) GetFileEntryPosition() int64 {
|
||||
return int64(f.fileEntryPosition)
|
||||
}
|
||||
|
||||
func (f *File) GetFileOffset() int64 {
|
||||
return SECTOR_SIZE * (int64(f.FileEntry().AllocationDescriptors[0].Location) + int64(f.Udf.PartitionStart()))
|
||||
}
|
||||
|
||||
func (f *File) FileEntry() *FileEntry {
|
||||
if f.fe == nil {
|
||||
f.fileEntryPosition = f.Fid.ICB.Location
|
||||
f.fe = NewFileEntry(f.Udf.ReadSector(f.Udf.PartitionStart() + f.fileEntryPosition))
|
||||
}
|
||||
return f.fe
|
||||
}
|
||||
|
||||
func (f *File) NewReader() *io.SectionReader {
|
||||
return io.NewSectionReader(f.Udf.r, f.GetFileOffset(), f.Size())
|
||||
}
|
||||
|
||||
func (f *File) Name() string {
|
||||
return f.Fid.FileIdentifier
|
||||
}
|
||||
|
||||
func (f *File) Mode() os.FileMode {
|
||||
var mode os.FileMode
|
||||
|
||||
perms := os.FileMode(f.FileEntry().Permissions)
|
||||
mode |= ((perms >> 0) & 7) << 0
|
||||
mode |= ((perms >> 5) & 7) << 3
|
||||
mode |= ((perms >> 10) & 7) << 6
|
||||
|
||||
if f.IsDir() {
|
||||
mode |= os.ModeDir
|
||||
}
|
||||
|
||||
return mode
|
||||
}
|
||||
|
||||
func (f *File) Size() int64 {
|
||||
return int64(f.FileEntry().InformationLength)
|
||||
}
|
||||
|
||||
func (f *File) ModTime() time.Time {
|
||||
return f.FileEntry().ModificationTime
|
||||
}
|
||||
|
||||
func (f *File) IsDir() bool {
|
||||
// TODO :Fix! This field always 0 :(
|
||||
return f.FileEntry().ICBTag.FileType == 4
|
||||
}
|
||||
|
||||
func (f *File) Sys() interface{} {
|
||||
return f.Fid
|
||||
}
|
||||
|
||||
func (f *File) ReadDir() []File {
|
||||
return f.Udf.ReadDir(f.FileEntry())
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
package udf
|
||||
|
||||
type ICBTag struct {
|
||||
PriorRecordedNumberOfDirectEntries uint32
|
||||
StrategyType uint16
|
||||
StrategyParameter uint16
|
||||
MaximumNumberOfEntries uint16
|
||||
FileType uint8
|
||||
ParentICBLocation uint64
|
||||
Flags uint16
|
||||
}
|
||||
|
||||
func (itag *ICBTag) FromBytes(b []byte) *ICBTag {
|
||||
itag.PriorRecordedNumberOfDirectEntries = rl_u32(b[0:])
|
||||
itag.StrategyType = rl_u16(b[4:])
|
||||
itag.StrategyParameter = rl_u16(b[4:])
|
||||
itag.MaximumNumberOfEntries = rl_u16(b[8:])
|
||||
itag.FileType = r_u8(b[1:])
|
||||
itag.ParentICBLocation = rl_u48(b[12:])
|
||||
itag.Flags = rl_u16(b[18:])
|
||||
return itag
|
||||
}
|
||||
|
||||
func NewICBTag(b []byte) *ICBTag {
|
||||
return new(ICBTag).FromBytes(b)
|
||||
}
|
||||
+118
@@ -0,0 +1,118 @@
|
||||
package udf
|
||||
|
||||
import (
|
||||
"io"
|
||||
)
|
||||
|
||||
const SECTOR_SIZE = 2048
|
||||
|
||||
type Udf struct {
|
||||
r io.ReaderAt
|
||||
isInited bool
|
||||
pvd *PrimaryVolumeDescriptor
|
||||
pd *PartitionDescriptor
|
||||
lvd *LogicalVolumeDescriptor
|
||||
fsd *FileSetDescriptor
|
||||
root_fe *FileEntry
|
||||
}
|
||||
|
||||
func (udf *Udf) PartitionStart() uint64 {
|
||||
if udf.pd == nil {
|
||||
panic(udf)
|
||||
} else {
|
||||
return uint64(udf.pd.PartitionStartingLocation)
|
||||
}
|
||||
}
|
||||
|
||||
func (udf *Udf) GetReader() io.ReaderAt {
|
||||
return udf.r
|
||||
}
|
||||
|
||||
func (udf *Udf) ReadSectors(sectorNumber uint64, sectorsCount uint64) []byte {
|
||||
buf := make([]byte, SECTOR_SIZE*sectorsCount)
|
||||
readed, err := udf.r.ReadAt(buf[:], int64(SECTOR_SIZE*sectorNumber))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if readed != int(SECTOR_SIZE*sectorsCount) {
|
||||
panic(readed)
|
||||
}
|
||||
return buf[:]
|
||||
}
|
||||
|
||||
func (udf *Udf) ReadSector(sectorNumber uint64) []byte {
|
||||
return udf.ReadSectors(sectorNumber, 1)
|
||||
}
|
||||
|
||||
func (udf *Udf) init() {
|
||||
if udf.isInited {
|
||||
return
|
||||
}
|
||||
|
||||
anchorDesc := NewAnchorVolumeDescriptorPointer(udf.ReadSector(256))
|
||||
if anchorDesc.Descriptor.TagIdentifier != DESCRIPTOR_ANCHOR_VOLUME_POINTER {
|
||||
panic(anchorDesc.Descriptor.TagIdentifier)
|
||||
}
|
||||
|
||||
for sector := uint64(anchorDesc.MainVolumeDescriptorSeq.Location); ; sector++ {
|
||||
desc := NewDescriptor(udf.ReadSector(sector))
|
||||
if desc.TagIdentifier == DESCRIPTOR_TERMINATING {
|
||||
break
|
||||
}
|
||||
switch desc.TagIdentifier {
|
||||
case DESCRIPTOR_PRIMARY_VOLUME:
|
||||
udf.pvd = desc.PrimaryVolumeDescriptor()
|
||||
case DESCRIPTOR_PARTITION:
|
||||
udf.pd = desc.PartitionDescriptor()
|
||||
case DESCRIPTOR_LOGICAL_VOLUME:
|
||||
udf.lvd = desc.LogicalVolumeDescriptor()
|
||||
}
|
||||
}
|
||||
|
||||
partitionStart := udf.PartitionStart()
|
||||
|
||||
udf.fsd = NewFileSetDescriptor(udf.ReadSector(partitionStart + udf.lvd.LogicalVolumeContentsUse.Location))
|
||||
udf.root_fe = NewFileEntry(udf.ReadSector(partitionStart + udf.fsd.RootDirectoryICB.Location))
|
||||
|
||||
udf.isInited = true
|
||||
}
|
||||
|
||||
func (udf *Udf) ReadDir(fe *FileEntry) []File {
|
||||
udf.init()
|
||||
|
||||
if fe == nil {
|
||||
fe = udf.root_fe
|
||||
}
|
||||
|
||||
ps := udf.PartitionStart()
|
||||
|
||||
adPos := fe.AllocationDescriptors[0]
|
||||
fdLen := uint64(adPos.Length)
|
||||
|
||||
fdBuf := udf.ReadSectors(ps+uint64(adPos.Location), (fdLen+SECTOR_SIZE-1)/SECTOR_SIZE)
|
||||
fdOff := uint64(0)
|
||||
|
||||
result := make([]File, 0)
|
||||
|
||||
for uint32(fdOff) < adPos.Length {
|
||||
fid := NewFileIdentifierDescriptor(fdBuf[fdOff:])
|
||||
if fid.FileIdentifier != "" {
|
||||
result = append(result, File{
|
||||
Udf: udf,
|
||||
Fid: fid,
|
||||
})
|
||||
}
|
||||
fdOff += fid.Len()
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func NewUdfFromReader(r io.ReaderAt) *Udf {
|
||||
udf := &Udf{
|
||||
r: r,
|
||||
isInited: false,
|
||||
}
|
||||
|
||||
return udf
|
||||
}
|
||||
Vendored
+11
-1
@@ -78,6 +78,10 @@ github.com/DataDog/zstd
|
||||
# github.com/LeeEirc/terminalparser v0.0.0-20240205084113-fbf78c8480f2
|
||||
## explicit; go 1.15
|
||||
github.com/LeeEirc/terminalparser
|
||||
# github.com/Microsoft/go-winio v0.6.2
|
||||
## explicit; go 1.21
|
||||
github.com/Microsoft/go-winio/wim
|
||||
github.com/Microsoft/go-winio/wim/lzx
|
||||
# github.com/RoaringBitmap/roaring v1.2.3
|
||||
## explicit; go 1.14
|
||||
github.com/RoaringBitmap/roaring
|
||||
@@ -628,6 +632,9 @@ github.com/jtolds/gls
|
||||
# github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0
|
||||
## explicit
|
||||
github.com/kardianos/osext
|
||||
# github.com/kdomanski/iso9660 v0.4.0
|
||||
## explicit; go 1.19
|
||||
github.com/kdomanski/iso9660
|
||||
# github.com/klauspost/cpuid/v2 v2.0.9
|
||||
## explicit; go 1.13
|
||||
github.com/klauspost/cpuid/v2
|
||||
@@ -774,6 +781,9 @@ github.com/modern-go/concurrent
|
||||
# github.com/modern-go/reflect2 v1.0.2
|
||||
## explicit; go 1.12
|
||||
github.com/modern-go/reflect2
|
||||
# github.com/mogaika/udf v0.0.0-20171019171931-167f0ab01c73
|
||||
## explicit
|
||||
github.com/mogaika/udf
|
||||
# github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826
|
||||
## explicit
|
||||
github.com/mohae/deepcopy
|
||||
@@ -1687,7 +1697,7 @@ yunion.io/x/log/hooks
|
||||
yunion.io/x/ovsdb/cli_util
|
||||
yunion.io/x/ovsdb/schema/ovn_nb
|
||||
yunion.io/x/ovsdb/types
|
||||
# yunion.io/x/pkg v1.10.4-0.20251114095758-2a2f105d9712
|
||||
# yunion.io/x/pkg v1.10.4-0.20260127060125-8939521ef75e
|
||||
## explicit; go 1.18
|
||||
yunion.io/x/pkg/appctx
|
||||
yunion.io/x/pkg/errors
|
||||
|
||||
+62
-29
@@ -32,12 +32,14 @@ const (
|
||||
OS_DIST_UBUNTU_SERVER = "Ubuntu Server"
|
||||
OS_DIST_UBUNTU = "Ubuntu"
|
||||
|
||||
OS_DIST_OPEN_SUSE = "OpenSUSE"
|
||||
OS_DIST_SUSE = "SUSE"
|
||||
OS_DIST_DEBIAN = "Debian"
|
||||
OS_DIST_CORE_OS = "CoreOS"
|
||||
OS_DIST_EULER_OS = "EulerOS"
|
||||
OS_DIST_ALIYUN = "Aliyun"
|
||||
OS_DIST_OPEN_SUSE = "OpenSUSE"
|
||||
OS_DIST_SUSE = "SUSE"
|
||||
OS_DIST_DEBIAN = "Debian"
|
||||
OS_DIST_CORE_OS = "CoreOS"
|
||||
OS_DIST_EULER_OS = "EulerOS"
|
||||
OS_DIST_OPEN_EULER = "OpenEuler"
|
||||
OS_DIST_ALIYUN = "Aliyun"
|
||||
OS_DIST_DEEPIN = "Deepin"
|
||||
|
||||
OS_DIST_ALIBABA_CLOUD_LINUX = "Alibaba Cloud Linux"
|
||||
OS_DIST_ANOLIS = "Anolis OS"
|
||||
@@ -136,6 +138,8 @@ func normalizeOsDistribution(osDist string, imageName string) string {
|
||||
return OS_DIST_FREE_BSD
|
||||
} else if strings.Contains(osDist, "euleros") {
|
||||
return OS_DIST_EULER_OS
|
||||
} else if strings.Contains(osDist, "openeuler") {
|
||||
return OS_DIST_OPEN_EULER
|
||||
} else if strings.Contains(osDist, "alibaba cloud linux") {
|
||||
return OS_DIST_ALIBABA_CLOUD_LINUX
|
||||
} else if strings.Contains(osDist, "anolis") {
|
||||
@@ -159,42 +163,71 @@ func normalizeOsDistribution(osDist string, imageName string) string {
|
||||
}
|
||||
}
|
||||
return OS_DIST_WINDOWS
|
||||
} else if strings.Contains(osDist, "deepin") {
|
||||
return OS_DIST_DEEPIN
|
||||
} else {
|
||||
return OS_DIST_OTHER_LINUX
|
||||
}
|
||||
}
|
||||
|
||||
var imageVersions = map[string][]string{
|
||||
OS_DIST_CENTOS: {"5", "6", "7", "8"},
|
||||
OS_DIST_CENTOS_STREAM: {"8", "9"},
|
||||
// CentOS:补充停更版本和完整迭代,CentOS 8已EOL,Stream补充最新版本
|
||||
OS_DIST_CENTOS: {"4", "5", "6", "7", "8", "9"},
|
||||
OS_DIST_CENTOS_STREAM: {"8", "9", "10"},
|
||||
|
||||
OS_DIST_RHEL: {"5", "6", "7", "8", "9"},
|
||||
OS_DIST_FREE_BSD: {"10", "11", "12"},
|
||||
// RHEL:补充完整主版本,覆盖从5到最新的10
|
||||
OS_DIST_RHEL: {"5", "6", "7", "8", "9", "10"},
|
||||
// FreeBSD:补充最新稳定版,覆盖10到15
|
||||
OS_DIST_FREE_BSD: {"10", "11", "12", "13", "14", "15"},
|
||||
|
||||
OS_DIST_UBUNTU_SERVER: {"10", "12", "14", "16", "18", "20", "22"},
|
||||
OS_DIST_UBUNTU: {"10", "12", "14", "16", "17", "18", "19", "20", "21", "22"},
|
||||
// Ubuntu:Server版补充LTS版本(每2年一个),Desktop版补充所有主要版本
|
||||
OS_DIST_UBUNTU_SERVER: {"10.04", "12.04", "14.04", "16.04", "18.04", "20.04", "22.04", "24.04",
|
||||
"10", "12", "14", "16", "18", "20", "22", "24"},
|
||||
OS_DIST_UBUNTU: {"10.04", "12.04", "14.04", "15.04", "16.04", "17.04", "18.04", "19.04", "20.04", "21.04", "22.04", "23.04", "24.04",
|
||||
"10", "12", "14", "16", "17", "18", "19", "20", "21", "22", "23", "24"},
|
||||
|
||||
OS_DIST_OPEN_SUSE: {"11", "12"},
|
||||
OS_DIST_SUSE: {"10", "11", "12", "13"},
|
||||
OS_DIST_DEBIAN: {"6", "7", "8", "9", "10", "11"},
|
||||
OS_DIST_CORE_OS: {"7"},
|
||||
OS_DIST_EULER_OS: {"2"},
|
||||
OS_DIST_ALIYUN: {},
|
||||
// OpenSUSE:补充Leap版本,SUSE补充SLES主版本
|
||||
OS_DIST_OPEN_SUSE: {"11", "12", "13", "42", "15.0", "15.1", "15.2", "15.3", "15.4", "15.5", "15.6"},
|
||||
OS_DIST_SUSE: {"10", "11", "12", "15", "15 SP1", "15 SP2", "15 SP3", "15 SP4", "15 SP5"},
|
||||
// Debian:补充从6到最新的12,覆盖所有稳定版
|
||||
OS_DIST_DEBIAN: {"6", "7", "8", "9", "10", "11", "12"},
|
||||
// CoreOS:补充Container Linux和Fedora CoreOS的主要版本
|
||||
OS_DIST_CORE_OS: {"7", "200", "213", "224", "234", "246", "251", "3033"},
|
||||
// 欧拉OS:补充openEuler和EulerOS完整版本
|
||||
OS_DIST_OPEN_EULER: {"2.0 SP1", "2.0 SP2", "2.0 SP3", "2.0 SP8", "3.0", "22.03", "23.09"},
|
||||
OS_DIST_EULER_OS: {"2"},
|
||||
// 阿里云Linux:补充1代和2/3代版本
|
||||
OS_DIST_ALIYUN: {"1", "2.1903", "3.2104", "3.2304"},
|
||||
|
||||
OS_DIST_ALIBABA_CLOUD_LINUX: {"2.1903", "3.2104"},
|
||||
OS_DIST_ANOLIS: {"7.9", "8.2", "8.4"},
|
||||
OS_DIST_ROCKY_LINUX: {"8.5", "8.6", "8.7", "8.8", "8.9", "8.10", "9.0", "9.1", "9.2", "9.3", "9.4", "9.5"},
|
||||
OS_DIST_FEDORA: {"33", "34", "35"},
|
||||
OS_DIST_ALMA_LINUX: {"8.5"},
|
||||
OS_DIST_AMAZON_LINUX: {"2023", "2"},
|
||||
// 阿里云轻量版:补充完整版本
|
||||
OS_DIST_ALIBABA_CLOUD_LINUX: {"2.1903", "3.2104", "3.2304", "3.2404"},
|
||||
// 龙蜥OS:补充7/8系列完整小版本
|
||||
OS_DIST_ANOLIS: {"7.6", "7.9", "8.2", "8.4", "8.6", "8.8", "9.0", "9.2"},
|
||||
// Rocky Linux:补充8/9全系列小版本
|
||||
OS_DIST_ROCKY_LINUX: {"8.5", "8.6", "8.7", "8.8", "8.9", "8.10", "9.0", "9.1", "9.2", "9.3", "9.4", "9.5"},
|
||||
// Fedora:补充近年主流版本(33到40)
|
||||
OS_DIST_FEDORA: {"33", "34", "35", "36", "37", "38", "39", "40"},
|
||||
// AlmaLinux:补充8/9全系列
|
||||
OS_DIST_ALMA_LINUX: {"8.5", "8.6", "8.7", "8.8", "8.9", "8.10", "9.0", "9.1", "9.2", "9.3", "9.4", "9.5"},
|
||||
// Amazon Linux:补充1/2/2023版本
|
||||
OS_DIST_AMAZON_LINUX: {"2022", "2023", "1", "2"},
|
||||
|
||||
OS_DIST_WINDOWS_SERVER: {"2003", "2008", "2012", "2016", "2019", "2022"},
|
||||
OS_DIST_WINDOWS: {"XP", "7", "8", "Vista", "10", "11"},
|
||||
// Windows Server:补充完整服务器版本
|
||||
OS_DIST_WINDOWS_SERVER: {"2003", "2008", "2008 R2", "2012", "2012 R2", "2016", "2019", "2022"},
|
||||
// Windows 桌面版:补充完整版本
|
||||
OS_DIST_WINDOWS: {"XP", "Vista", "7", "8", "8.1", "10", "11"},
|
||||
|
||||
OS_DIST_KYLIN: {"V10"},
|
||||
OS_DIST_UOS: {"V20", "20 1050", "1050", "1060", "1070"},
|
||||
// 麒麟OS:补充V10各版本和V11
|
||||
OS_DIST_KYLIN: {"V10", "V10 SP1", "V10 SP2", "V10 SP3", "V11", "Nile"},
|
||||
// UOS:补充统信UOS完整版本
|
||||
OS_DIST_UOS: {"V20 1050", "20 1050", "1050", "V20 1060", "1060", "V20 1070", "1070", "V20 1080", "V20 1090", "V23", "Eagle", "V20"},
|
||||
|
||||
OS_DIST_TENCENTOS_SERVER: {"2.4", "3.1"},
|
||||
// 腾讯云OS:补充完整版本
|
||||
OS_DIST_TENCENTOS_SERVER: {"2.4", "3.1", "3.2", "3.3", "4.0", "4"},
|
||||
// 其他Linux:预留空列表,可根据实际场景补充
|
||||
|
||||
OS_DIST_DEEPIN: {"20", "20.9", "21", "21.9", "22", "22.9", "23", "23.9", "Crimson"},
|
||||
OS_DIST_OTHER_LINUX: {},
|
||||
}
|
||||
|
||||
func normalizeOsVersion(imageName string, osDist string, osVersion string) string {
|
||||
|
||||
Reference in New Issue
Block a user