diff --git a/cmd/isocli/main.go b/cmd/isocli/main.go new file mode 100644 index 0000000000..4bb85f84c3 --- /dev/null +++ b/cmd/isocli/main.go @@ -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) + } +} diff --git a/cmd/isocli/shell/iso.go b/cmd/isocli/shell/iso.go new file mode 100644 index 0000000000..033a7329de --- /dev/null +++ b/cmd/isocli/shell/iso.go @@ -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 + }) +} diff --git a/go.mod b/go.mod index 97866fa74a..f31b8107c2 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index 107c7516a1..c6ab493fc7 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/pkg/apigateway/handler/assume.go b/pkg/apigateway/handler/assume.go index 7ea95c82e9..0344426f16 100644 --- a/pkg/apigateway/handler/assume.go +++ b/pkg/apigateway/handler/assume.go @@ -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 ( diff --git a/pkg/apis/image/consts.go b/pkg/apis/image/consts.go index b07a35f825..2af06340c7 100644 --- a/pkg/apis/image/consts.go +++ b/pkg/apis/image/consts.go @@ -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" diff --git a/pkg/image/models/images.go b/pkg/image/models/images.go index 27fb1e5f9a..2849ea1302 100644 --- a/pkg/image/models/images.go +++ b/pkg/image/models/images.go @@ -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 { diff --git a/pkg/util/isoutils/doc.go b/pkg/util/isoutils/doc.go new file mode 100644 index 0000000000..7d48e0733f --- /dev/null +++ b/pkg/util/isoutils/doc.go @@ -0,0 +1 @@ +package isoutils // import "yunion.io/x/onecloud/pkg/util/isoutils" diff --git a/pkg/util/isoutils/iso9660_reader.go b/pkg/util/isoutils/iso9660_reader.go new file mode 100644 index 0000000000..253b10afa2 --- /dev/null +++ b/pkg/util/isoutils/iso9660_reader.go @@ -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 +} diff --git a/pkg/util/isoutils/isoutils.go b/pkg/util/isoutils/isoutils.go new file mode 100644 index 0000000000..de55ca70fd --- /dev/null +++ b/pkg/util/isoutils/isoutils.go @@ -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) +} diff --git a/pkg/util/isoutils/udf_reader.go b/pkg/util/isoutils/udf_reader.go new file mode 100644 index 0000000000..9d4ca117db --- /dev/null +++ b/pkg/util/isoutils/udf_reader.go @@ -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 +} diff --git a/pkg/util/isoutils/winiso_other.go b/pkg/util/isoutils/winiso_other.go new file mode 100644 index 0000000000..0ba52432cd --- /dev/null +++ b/pkg/util/isoutils/winiso_other.go @@ -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") +} diff --git a/pkg/util/isoutils/winiso_unix.go b/pkg/util/isoutils/winiso_unix.go new file mode 100644 index 0000000000..ce10f23316 --- /dev/null +++ b/pkg/util/isoutils/winiso_unix.go @@ -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 +} diff --git a/vendor/github.com/Microsoft/go-winio/LICENSE b/vendor/github.com/Microsoft/go-winio/LICENSE new file mode 100644 index 0000000000..b8b569d774 --- /dev/null +++ b/vendor/github.com/Microsoft/go-winio/LICENSE @@ -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. + diff --git a/vendor/github.com/Microsoft/go-winio/wim/decompress.go b/vendor/github.com/Microsoft/go-winio/wim/decompress.go new file mode 100644 index 0000000000..993a413187 --- /dev/null +++ b/vendor/github.com/Microsoft/go-winio/wim/decompress.go @@ -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 +} diff --git a/vendor/github.com/Microsoft/go-winio/wim/lzx/lzx.go b/vendor/github.com/Microsoft/go-winio/wim/lzx/lzx.go new file mode 100644 index 0000000000..e5db82602c --- /dev/null +++ b/vendor/github.com/Microsoft/go-winio/wim/lzx/lzx.go @@ -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< 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<> (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<>(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 +} diff --git a/vendor/github.com/Microsoft/go-winio/wim/wim.go b/vendor/github.com/Microsoft/go-winio/wim/wim.go new file mode 100644 index 0000000000..8f272f3da8 --- /dev/null +++ b/vendor/github.com/Microsoft/go-winio/wim/wim.go @@ -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 +} diff --git a/vendor/github.com/kdomanski/iso9660/LICENSE b/vendor/github.com/kdomanski/iso9660/LICENSE new file mode 100644 index 0000000000..221d965e58 --- /dev/null +++ b/vendor/github.com/kdomanski/iso9660/LICENSE @@ -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. diff --git a/vendor/github.com/kdomanski/iso9660/README.md b/vendor/github.com/kdomanski/iso9660/README.md new file mode 100644 index 0000000000..9df867ab0b --- /dev/null +++ b/vendor/github.com/kdomanski/iso9660/README.md @@ -0,0 +1,161 @@ +## iso9660 +[![Go Reference](https://pkg.go.dev/badge/github.com/kdomanski/iso9660.svg)](https://pkg.go.dev/github.com/kdomanski/iso9660) +[![codecov](https://codecov.io/gh/kdomanski/iso9660/branch/master/graph/badge.svg?token=14MJSZYZ24)](https://codecov.io/gh/kdomanski/iso9660) +[![Go Report Card](https://goreportcard.com/badge/github.com/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) + } +} +``` diff --git a/vendor/github.com/kdomanski/iso9660/image_reader.go b/vendor/github.com/kdomanski/iso9660/image_reader.go new file mode 100644 index 0000000000..32227a1957 --- /dev/null +++ b/vendor/github.com/kdomanski/iso9660/image_reader.go @@ -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)) +} diff --git a/vendor/github.com/kdomanski/iso9660/image_writer.go b/vendor/github.com/kdomanski/iso9660/image_writer.go new file mode 100644 index 0000000000..db16318c61 --- /dev/null +++ b/vendor/github.com/kdomanski/iso9660/image_writer.go @@ -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 +} diff --git a/vendor/github.com/kdomanski/iso9660/iso9660.go b/vendor/github.com/kdomanski/iso9660/iso9660.go new file mode 100644 index 0000000000..a810911b8f --- /dev/null +++ b/vendor/github.com/kdomanski/iso9660/iso9660.go @@ -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 + } +} diff --git a/vendor/github.com/kdomanski/iso9660/iso9660_datatypes.go b/vendor/github.com/kdomanski/iso9660/iso9660_datatypes.go new file mode 100644 index 0000000000..703111ff57 --- /dev/null +++ b/vendor/github.com/kdomanski/iso9660/iso9660_datatypes.go @@ -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)) +} diff --git a/vendor/github.com/kdomanski/iso9660/rockridge.go b/vendor/github.com/kdomanski/iso9660/rockridge.go new file mode 100644 index 0000000000..79c7443cee --- /dev/null +++ b/vendor/github.com/kdomanski/iso9660/rockridge.go @@ -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:]), + } +} diff --git a/vendor/github.com/kdomanski/iso9660/susp.go b/vendor/github.com/kdomanski/iso9660/susp.go new file mode 100644 index 0000000000..394a943687 --- /dev/null +++ b/vendor/github.com/kdomanski/iso9660/susp.go @@ -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, + } +} diff --git a/vendor/github.com/mogaika/udf/.gitignore b/vendor/github.com/mogaika/udf/.gitignore new file mode 100644 index 0000000000..a1338d6851 --- /dev/null +++ b/vendor/github.com/mogaika/udf/.gitignore @@ -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/ diff --git a/vendor/github.com/mogaika/udf/LICENSE b/vendor/github.com/mogaika/udf/LICENSE new file mode 100644 index 0000000000..7fb8c33684 --- /dev/null +++ b/vendor/github.com/mogaika/udf/LICENSE @@ -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. diff --git a/vendor/github.com/mogaika/udf/README.md b/vendor/github.com/mogaika/udf/README.md new file mode 100644 index 0000000000..5389cb096c --- /dev/null +++ b/vendor/github.com/mogaika/udf/README.md @@ -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 +``` + + + diff --git a/vendor/github.com/mogaika/udf/binary.go b/vendor/github.com/mogaika/udf/binary.go new file mode 100644 index 0000000000..9767fcd618 --- /dev/null +++ b/vendor/github.com/mogaika/udf/binary.go @@ -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 +} diff --git a/vendor/github.com/mogaika/udf/descr.go b/vendor/github.com/mogaika/udf/descr.go new file mode 100644 index 0000000000..daf829b5f2 --- /dev/null +++ b/vendor/github.com/mogaika/udf/descr.go @@ -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) +} diff --git a/vendor/github.com/mogaika/udf/entity.go b/vendor/github.com/mogaika/udf/entity.go new file mode 100644 index 0000000000..3f9ef0fcc5 --- /dev/null +++ b/vendor/github.com/mogaika/udf/entity.go @@ -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 +} diff --git a/vendor/github.com/mogaika/udf/extent.go b/vendor/github.com/mogaika/udf/extent.go new file mode 100644 index 0000000000..2551bbbbcb --- /dev/null +++ b/vendor/github.com/mogaika/udf/extent.go @@ -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:]), + } +} diff --git a/vendor/github.com/mogaika/udf/file.go b/vendor/github.com/mogaika/udf/file.go new file mode 100644 index 0000000000..90367dba92 --- /dev/null +++ b/vendor/github.com/mogaika/udf/file.go @@ -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()) +} diff --git a/vendor/github.com/mogaika/udf/icb.go b/vendor/github.com/mogaika/udf/icb.go new file mode 100644 index 0000000000..0a21d81191 --- /dev/null +++ b/vendor/github.com/mogaika/udf/icb.go @@ -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) +} diff --git a/vendor/github.com/mogaika/udf/udf.go b/vendor/github.com/mogaika/udf/udf.go new file mode 100644 index 0000000000..058862a320 --- /dev/null +++ b/vendor/github.com/mogaika/udf/udf.go @@ -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 +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 808643910f..23d30147fc 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -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 diff --git a/vendor/yunion.io/x/pkg/util/imagetools/imagetools.go b/vendor/yunion.io/x/pkg/util/imagetools/imagetools.go index 317c318ec9..3ef3e3cb2e 100644 --- a/vendor/yunion.io/x/pkg/util/imagetools/imagetools.go +++ b/vendor/yunion.io/x/pkg/util/imagetools/imagetools.go @@ -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 {