fix(glance): support prob iso image (#24136)

This commit is contained in:
屈轩
2026-01-28 15:31:34 +08:00
committed by GitHub
parent 60ae50c608
commit 246721c529
37 changed files with 5790 additions and 40 deletions
+87
View File
@@ -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)
}
}
+82
View File
@@ -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
})
}
+5 -2
View File
@@ -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
+8 -2
View File
@@ -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=
+2 -1
View File
@@ -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 (
+1
View File
@@ -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"
+54 -5
View File
@@ -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 {
+1
View File
@@ -0,0 +1 @@
package isoutils // import "yunion.io/x/onecloud/pkg/util/isoutils"
+148
View File
@@ -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
}
+428
View File
@@ -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)
}
+187
View File
@@ -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 := &currentDirEntry[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 := &currentDirEntry[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
}
+26
View File
@@ -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")
}
+98
View File
@@ -0,0 +1,98 @@
// Copyright 2019 Yunion
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//go:build linux
// +build linux
package isoutils
import (
"fmt"
"github.com/Microsoft/go-winio/wim"
"yunion.io/x/log"
"yunion.io/x/pkg/util/imagetools"
)
// ========== 7. 保留Windows版本识别函数(适配新结构) ==========
func DetectWindowsEdition(r *ISOFileReader) (*ISOInfo, error) {
wimFile, err := r.GetFile("sources/install.wim")
if err != nil {
return nil, err
}
wim, err := wim.NewReader(wimFile.NewReader())
if err != nil {
return nil, err
}
result := &ISOInfo{}
for _, image := range wim.Image {
version := fmt.Sprintf("%d.%d.%d", image.Windows.Version.Major, image.Windows.Version.Minor, image.Windows.Version.Build)
if image.Windows != nil {
if image.Windows.Arch == 9 {
result.Arch = "x86_64"
} else if image.Windows.Arch == 12 {
result.Arch = "arm64"
} else if image.Windows.Arch == 0 {
result.Arch = "x86"
}
result.Distro = imagetools.OS_DIST_WINDOWS
result.Language = image.Windows.DefaultLanguage
switch fmt.Sprintf("%d.%d", image.Windows.Version.Major, image.Windows.Version.Minor) {
case "6.0":
result.Version = "Windows Vista"
case "6.1":
result.Version = "Windows 7"
case "6.2":
result.Version = "Windows 8"
case "6.3":
result.Version = "Windows 8.1"
case "10.0":
if image.Windows.Version.Build >= 27500 {
result.Version = "Windows 12"
} else if image.Windows.Version.Build >= 22000 {
result.Version = "Windows 11"
} else {
result.Version = "Windows 10"
}
}
if image.Windows.ProductType == "ServerNT" {
result.Distro = imagetools.OS_DIST_WINDOWS_SERVER
switch fmt.Sprintf("%d.%d", image.Windows.Version.Major, image.Windows.Version.Minor) {
case "6.0":
result.Version = "Windows Server 2008"
case "6.1":
result.Version = "Windows Server 2008 R2"
case "6.2":
result.Version = "Windows Server 2012"
case "6.3":
result.Version = "Windows Server 2012 R2"
case "10.0":
if image.Windows.Version.Build >= 26040 {
result.Version = "Windows Server 2025"
} else if image.Windows.Version.Build >= 20348 {
result.Version = "Windows Server 2022"
} else if image.Windows.Version.Build >= 17763 {
result.Version = "Windows Server 2019"
} else if image.Windows.Version.Build >= 14393 {
result.Version = "Windows Server 2016"
}
}
}
log.Debugf("识别到 %s 版本: %s -> %s", result.Distro, version, result.Version)
break
}
}
return result, nil
}
+22
View File
@@ -0,0 +1,22 @@
The MIT License (MIT)
Copyright (c) 2015 Microsoft
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+139
View File
@@ -0,0 +1,139 @@
//go:build windows || linux
// +build windows linux
package wim
import (
"encoding/binary"
"io"
"github.com/Microsoft/go-winio/wim/lzx"
)
const chunkSize = 32768 // Compressed resource chunk size
type compressedReader struct {
r *io.SectionReader
d io.ReadCloser
chunks []int64
curChunk int
originalSize int64
}
func newCompressedReader(r *io.SectionReader, originalSize int64, offset int64) (*compressedReader, error) {
nchunks := (originalSize + chunkSize - 1) / chunkSize
var base int64
chunks := make([]int64, nchunks)
if originalSize <= 0xffffffff {
// 32-bit chunk offsets
base = (nchunks - 1) * 4
chunks32 := make([]uint32, nchunks-1)
err := binary.Read(r, binary.LittleEndian, chunks32)
if err != nil {
return nil, err
}
for i, n := range chunks32 {
chunks[i+1] = int64(n)
}
} else {
// 64-bit chunk offsets
base = (nchunks - 1) * 8
err := binary.Read(r, binary.LittleEndian, chunks[1:])
if err != nil {
return nil, err
}
}
for i, c := range chunks {
chunks[i] = c + base
}
cr := &compressedReader{
r: r,
chunks: chunks,
originalSize: originalSize,
}
err := cr.reset(int(offset / chunkSize))
if err != nil {
return nil, err
}
suboff := offset % chunkSize
if suboff != 0 {
_, err := io.CopyN(io.Discard, cr.d, suboff)
if err != nil {
return nil, err
}
}
return cr, nil
}
func (r *compressedReader) chunkOffset(n int) int64 {
if n == len(r.chunks) {
return r.r.Size()
}
return r.chunks[n]
}
func (r *compressedReader) chunkSize(n int) int {
return int(r.chunkOffset(n+1) - r.chunkOffset(n))
}
func (r *compressedReader) uncompressedSize(n int) int {
if n < len(r.chunks)-1 {
return chunkSize
}
size := int(r.originalSize % chunkSize)
if size == 0 {
size = chunkSize
}
return size
}
func (r *compressedReader) reset(n int) error {
if n >= len(r.chunks) {
return io.EOF
}
if r.d != nil {
r.d.Close()
}
r.curChunk = n
size := r.chunkSize(n)
uncompressedSize := r.uncompressedSize(n)
section := io.NewSectionReader(r.r, r.chunkOffset(n), int64(size))
if size != uncompressedSize {
d, err := lzx.NewReader(section, uncompressedSize)
if err != nil {
return err
}
r.d = d
} else {
r.d = io.NopCloser(section)
}
return nil
}
func (r *compressedReader) Read(b []byte) (int, error) {
for {
n, err := r.d.Read(b)
if err != io.EOF { //nolint:errorlint
return n, err
}
err = r.reset(r.curChunk + 1)
if err != nil {
return n, err
}
}
}
func (r *compressedReader) Close() error {
var err error
if r.d != nil {
err = r.d.Close()
r.d = nil
}
return err
}
+598
View File
@@ -0,0 +1,598 @@
// Package lzx implements a decompressor for the the WIM variant of the
// LZX compression algorithm.
//
// The LZX algorithm is an earlier variant of LZX DELTA, which is documented
// at https://msdn.microsoft.com/en-us/library/cc483133(v=exchg.80).aspx.
package lzx
import (
"bytes"
"encoding/binary"
"errors"
"io"
)
const (
maincodecount = 496
maincodesplit = 256
lencodecount = 249
lenshift = 9
codemask = 0x1ff
tablebits = 9
tablesize = 1 << tablebits
maxBlockSize = 32768
windowSize = 32768
maxTreePathLen = 16
e8filesize = 12000000
maxe8offset = 0x3fffffff
verbatimBlock = 1
alignedOffsetBlock = 2
uncompressedBlock = 3
)
var footerBits = [...]byte{
0, 0, 0, 0, 1, 1, 2, 2,
3, 3, 4, 4, 5, 5, 6, 6,
7, 7, 8, 8, 9, 9, 10, 10,
11, 11, 12, 12, 13, 13, 14,
}
var basePosition = [...]uint16{
0, 1, 2, 3, 4, 6, 8, 12,
16, 24, 32, 48, 64, 96, 128, 192,
256, 384, 512, 768, 1024, 1536, 2048, 3072,
4096, 6144, 8192, 12288, 16384, 24576, 32768,
}
var (
errCorrupt = errors.New("LZX data corrupt")
)
// Reader is an interface used by the decompressor to access
// the input stream. If the provided io.Reader does not implement
// Reader, then a bufio.Reader is used.
type Reader interface {
io.Reader
io.ByteReader
}
type decompressor struct {
r io.Reader
err error
unaligned bool
nbits byte
c uint32
lru [3]uint16
uncompressed int
windowReader *bytes.Reader
mainlens [maincodecount]byte
lenlens [lencodecount]byte
window [windowSize]byte
b []byte
bv int
bo int
}
//go:noinline
func (f *decompressor) fail(err error) {
if f.err == nil {
f.err = err
}
f.bo = 0
f.bv = 0
}
func (f *decompressor) ensureAtLeast(n int) error {
if f.bv-f.bo >= n {
return nil
}
if f.err != nil {
return f.err
}
if f.bv != f.bo {
copy(f.b[:f.bv-f.bo], f.b[f.bo:f.bv])
}
n, err := io.ReadAtLeast(f.r, f.b[f.bv-f.bo:], n)
if err != nil {
if err == io.EOF { //nolint:errorlint
err = io.ErrUnexpectedEOF
} else {
f.fail(err)
}
return err
}
f.bv = f.bv - f.bo + n
f.bo = 0
return nil
}
// feed retrieves another 16-bit word from the stream and consumes
// it into f.c. It returns false if there are no more bytes available.
// Otherwise, on error, it sets f.err.
func (f *decompressor) feed() bool {
err := f.ensureAtLeast(2)
if err == io.ErrUnexpectedEOF { //nolint:errorlint // returns io.ErrUnexpectedEOF by contract
return false
}
f.c |= (uint32(f.b[f.bo+1])<<8 | uint32(f.b[f.bo])) << (16 - f.nbits)
f.nbits += 16
f.bo += 2
return true
}
// getBits retrieves the next n bits from the byte stream. n
// must be <= 16. It sets f.err on error.
func (f *decompressor) getBits(n byte) uint16 {
if f.nbits < n {
if !f.feed() {
f.fail(io.ErrUnexpectedEOF)
}
}
c := uint16(f.c >> (32 - n))
f.c <<= n
f.nbits -= n
return c
}
type huffman struct {
extra [][]uint16
maxbits byte
table [tablesize]uint16
}
// buildTable builds a huffman decoding table from a slice of code lengths,
// one per code, in order. Each code length must be <= maxTreePathLen.
// See https://en.wikipedia.org/wiki/Canonical_Huffman_code.
func buildTable(codelens []byte) *huffman {
// Determine the number of codes of each length, and the
// maximum length.
var count [maxTreePathLen + 1]uint
var max byte
for _, cl := range codelens {
count[cl]++
if max < cl {
max = cl
}
}
if max == 0 {
return &huffman{}
}
// Determine the first code of each length.
var first [maxTreePathLen + 1]uint
code := uint(0)
for i := byte(1); i <= max; i++ {
code <<= 1
first[i] = code
code += count[i]
}
if code != 1<<max {
return nil
}
// Build a table for code lookup. For code sizes < max,
// put all possible suffixes for the code into the table, too.
// For max > tablebits, split long codes into additional tables
// of suffixes of max-tablebits length.
h := &huffman{maxbits: max}
if max > tablebits {
core := first[tablebits+1] / 2 // Number of codes that fit without extra tables
nextra := 1<<tablebits - core // Number of extra entries
h.extra = make([][]uint16, nextra)
for code := core; code < 1<<tablebits; code++ {
h.table[code] = uint16(code - core)
h.extra[code-core] = make([]uint16, 1<<(max-tablebits))
}
}
for i, cl := range codelens {
if cl != 0 {
code := first[cl]
first[cl]++
v := uint16(cl)<<lenshift | uint16(i)
if cl <= tablebits {
extendedCode := code << (tablebits - cl)
for j := uint(0); j < 1<<(tablebits-cl); j++ {
h.table[extendedCode+j] = v
}
} else {
prefix := code >> (cl - tablebits)
suffix := code & (1<<(cl-tablebits) - 1)
extendedCode := suffix << (max - cl)
for j := uint(0); j < 1<<(max-cl); j++ {
h.extra[h.table[prefix]][extendedCode+j] = v
}
}
}
}
return h
}
// getCode retrieves the next code using the provided
// huffman tree. It sets f.err on error.
func (f *decompressor) getCode(h *huffman) uint16 {
if h.maxbits > 0 {
if f.nbits < maxTreePathLen {
f.feed()
}
// For codes with length < tablebits, it doesn't matter
// what the remainder of the bits used for table lookup
// are, since entries with all possible suffixes were
// added to the table.
c := h.table[f.c>>(32-tablebits)]
if !(c >= 1<<lenshift) {
// The code is not in c.
c = h.extra[c][f.c<<tablebits>>(32-(h.maxbits-tablebits))]
}
n := byte(c >> lenshift)
if f.nbits >= n {
// Only consume the length of the code, not the maximum
// code length.
f.c <<= n
f.nbits -= n
return c & codemask
}
f.fail(io.ErrUnexpectedEOF)
return 0
}
// This is an empty tree. It should not be used.
f.fail(errCorrupt)
return 0
}
// readTree updates the huffman tree path lengths in lens by
// reading and decoding lengths from the byte stream. lens
// should be prepopulated with the previous block's tree's path
// lengths. For the first block, lens should be zero.
func (f *decompressor) readTree(lens []byte) error {
// Get the pre-tree for the main tree.
var pretreeLen [20]byte
for i := range pretreeLen {
pretreeLen[i] = byte(f.getBits(4))
}
if f.err != nil {
return f.err
}
h := buildTable(pretreeLen[:])
// The lengths are encoded as a series of huffman codes
// encoded by the pre-tree.
for i := 0; i < len(lens); {
c := byte(f.getCode(h))
if f.err != nil {
return f.err
}
switch {
case c <= 16: // length is delta from previous length
lens[i] = (lens[i] + 17 - c) % 17
i++
case c == 17: // next n + 4 lengths are zero
zeroes := int(f.getBits(4)) + 4
if i+zeroes > len(lens) {
return errCorrupt
}
for j := 0; j < zeroes; j++ {
lens[i+j] = 0
}
i += zeroes
case c == 18: // next n + 20 lengths are zero
zeroes := int(f.getBits(5)) + 20
if i+zeroes > len(lens) {
return errCorrupt
}
for j := 0; j < zeroes; j++ {
lens[i+j] = 0
}
i += zeroes
case c == 19: // next n + 4 lengths all have the same value
same := int(f.getBits(1)) + 4
if i+same > len(lens) {
return errCorrupt
}
c = byte(f.getCode(h))
if c > 16 {
return errCorrupt
}
l := (lens[i] + 17 - c) % 17
for j := 0; j < same; j++ {
lens[i+j] = l
}
i += same
default:
return errCorrupt
}
}
if f.err != nil {
return f.err
}
return nil
}
func (f *decompressor) readBlockHeader() (byte, uint16, error) {
// If the previous block was an unaligned uncompressed block, restore
// 2-byte alignment.
if f.unaligned {
err := f.ensureAtLeast(1)
if err != nil {
return 0, 0, err
}
f.bo++
f.unaligned = false
}
blockType := f.getBits(3)
full := f.getBits(1)
var blockSize uint16
if full != 0 {
blockSize = maxBlockSize
} else {
blockSize = f.getBits(16)
if blockSize > maxBlockSize {
return 0, 0, errCorrupt
}
}
if f.err != nil {
return 0, 0, f.err
}
switch blockType {
case verbatimBlock, alignedOffsetBlock:
// The caller will read the huffman trees.
case uncompressedBlock:
if f.nbits > 16 {
panic("impossible: more than one 16-bit word remains")
}
// Drop the remaining bits in the current 16-bit word
// If there are no bits left, discard a full 16-bit word.
n := f.nbits
if n == 0 {
n = 16
}
f.getBits(n)
// Read the LRU values for the next block.
err := f.ensureAtLeast(12)
if err != nil {
return 0, 0, err
}
f.lru[0] = uint16(binary.LittleEndian.Uint32(f.b[f.bo : f.bo+4]))
f.lru[1] = uint16(binary.LittleEndian.Uint32(f.b[f.bo+4 : f.bo+8]))
f.lru[2] = uint16(binary.LittleEndian.Uint32(f.b[f.bo+8 : f.bo+12]))
f.bo += 12
default:
return 0, 0, errCorrupt
}
return byte(blockType), blockSize, nil
}
// readTrees reads the two or three huffman trees for the current block.
// readAligned specifies whether to read the aligned offset tree.
func (f *decompressor) readTrees(readAligned bool) (main *huffman, length *huffman, aligned *huffman, err error) {
// Aligned offset blocks start with a small aligned offset tree.
if readAligned {
var alignedLen [8]byte
for i := range alignedLen {
alignedLen[i] = byte(f.getBits(3))
}
aligned = buildTable(alignedLen[:])
if aligned == nil {
return main, length, aligned, errors.New("corrupt")
}
}
// The main tree is encoded in two parts.
err = f.readTree(f.mainlens[:maincodesplit])
if err != nil {
return main, length, aligned, err
}
err = f.readTree(f.mainlens[maincodesplit:])
if err != nil {
return main, length, aligned, err
}
main = buildTable(f.mainlens[:])
if main == nil {
return main, length, aligned, errors.New("corrupt")
}
// The length tree is encoding in a single part.
err = f.readTree(f.lenlens[:])
if err != nil {
return main, length, aligned, err
}
length = buildTable(f.lenlens[:])
if length == nil {
return main, length, aligned, errors.New("corrupt")
}
return main, length, aligned, f.err
}
// readCompressedBlock decodes a compressed block, writing into the window
// starting at start and ending at end, and using the provided huffman trees.
func (f *decompressor) readCompressedBlock(start, end uint16, hmain, hlength, haligned *huffman) (int, error) {
i := start
for i < end {
main := f.getCode(hmain)
if f.err != nil {
break
}
if main < 256 {
// Literal byte.
f.window[i] = byte(main)
i++
continue
}
// This is a match backward in the window. Determine
// the offset and dlength.
matchlen := (main - 256) % 8
slot := (main - 256) / 8
// The length is either the low bits of the code,
// or if this is 7, is encoded with the length tree.
if matchlen == 7 {
matchlen += f.getCode(hlength)
}
matchlen += 2
var matchoffset uint16
if slot < 3 { //nolint:nestif // todo: simplify nested complexity
// The offset is one of the LRU values.
matchoffset = f.lru[slot]
f.lru[slot] = f.lru[0]
f.lru[0] = matchoffset
} else {
// The offset is encoded as a combination of the
// slot and more bits from the bit stream.
offsetbits := footerBits[slot]
var verbatimbits, alignedbits uint16
if offsetbits > 0 {
if haligned != nil && offsetbits >= 3 {
// This is an aligned offset block. Combine
// the bits written verbatim with the aligned
// offset tree code.
verbatimbits = f.getBits(offsetbits-3) * 8
alignedbits = f.getCode(haligned)
} else {
// There are no aligned offset bits to read,
// only verbatim bits.
verbatimbits = f.getBits(offsetbits)
alignedbits = 0
}
}
matchoffset = basePosition[slot] + verbatimbits + alignedbits - 2
// Update the LRU cache.
f.lru[2] = f.lru[1]
f.lru[1] = f.lru[0]
f.lru[0] = matchoffset
}
if !(matchoffset <= i && matchlen <= end-i) {
f.fail(errCorrupt)
break
}
copyend := i + matchlen
for ; i < copyend; i++ {
f.window[i] = f.window[i-matchoffset]
}
}
return int(i - start), f.err
}
// readBlock decodes the current block and returns the number of uncompressed bytes.
func (f *decompressor) readBlock(start uint16) (int, error) {
blockType, size, err := f.readBlockHeader()
if err != nil {
return 0, err
}
if blockType == uncompressedBlock {
if size%2 == 1 {
// Remember to realign the byte stream at the next block.
f.unaligned = true
}
copied := 0
if f.bo < f.bv {
copied = int(size)
s := int(start)
if copied > f.bv-f.bo {
copied = f.bv - f.bo
}
copy(f.window[s:s+copied], f.b[f.bo:f.bo+copied])
f.bo += copied
}
n, err := io.ReadFull(f.r, f.window[start+uint16(copied):start+size])
return copied + n, err
}
hmain, hlength, haligned, err := f.readTrees(blockType == alignedOffsetBlock)
if err != nil {
return 0, err
}
return f.readCompressedBlock(start, start+size, hmain, hlength, haligned)
}
// decodeE8 reverses the 0xe8 x86 instruction encoding that was performed
// to the uncompressed data before it was compressed.
func decodeE8(b []byte, off int64) {
if off > maxe8offset || len(b) < 10 {
return
}
for i := 0; i < len(b)-10; i++ {
if b[i] == 0xe8 {
currentPtr := int32(off) + int32(i)
abs := int32(binary.LittleEndian.Uint32(b[i+1 : i+5]))
if abs >= -currentPtr && abs < e8filesize {
var rel int32
if abs >= 0 {
rel = abs - currentPtr
} else {
rel = abs + e8filesize
}
binary.LittleEndian.PutUint32(b[i+1:i+5], uint32(rel))
}
i += 4
}
}
}
func (f *decompressor) Read(b []byte) (int, error) {
// Read and uncompress everything.
if f.windowReader == nil {
n := 0
for n < f.uncompressed {
k, err := f.readBlock(uint16(n))
if err != nil {
return 0, err
}
n += k
}
decodeE8(f.window[:f.uncompressed], 0)
f.windowReader = bytes.NewReader(f.window[:f.uncompressed])
}
// Just read directly from the window.
return f.windowReader.Read(b)
}
func (*decompressor) Close() error {
return nil
}
// NewReader returns a new io.ReadCloser that decompresses a
// WIM LZX stream until uncompressedSize bytes have been returned.
func NewReader(r io.Reader, uncompressedSize int) (io.ReadCloser, error) {
if uncompressedSize > windowSize {
return nil, errors.New("uncompressed size is limited to 32KB")
}
f := &decompressor{
lru: [3]uint16{1, 1, 1},
uncompressed: uncompressedSize,
b: make([]byte, 4096),
r: r,
}
return f, nil
}
+898
View File
@@ -0,0 +1,898 @@
//go:build windows || linux
// +build windows linux
// Package wim implements a WIM file parser.
//
// WIM files are used to distribute Windows file system and container images.
// They are documented at https://msdn.microsoft.com/en-us/library/windows/desktop/dd861280.aspx.
package wim
import (
"bytes"
"crypto/sha1" //nolint:gosec // not used for secure application
"encoding/binary"
"encoding/xml"
"errors"
"fmt"
"io"
"strconv"
"sync"
"time"
"unicode/utf16"
)
// File attribute constants from Windows.
//
//nolint:revive // var-naming: ALL_CAPS
const (
FILE_ATTRIBUTE_READONLY = 0x00000001
FILE_ATTRIBUTE_HIDDEN = 0x00000002
FILE_ATTRIBUTE_SYSTEM = 0x00000004
FILE_ATTRIBUTE_DIRECTORY = 0x00000010
FILE_ATTRIBUTE_ARCHIVE = 0x00000020
FILE_ATTRIBUTE_DEVICE = 0x00000040
FILE_ATTRIBUTE_NORMAL = 0x00000080
FILE_ATTRIBUTE_TEMPORARY = 0x00000100
FILE_ATTRIBUTE_SPARSE_FILE = 0x00000200
FILE_ATTRIBUTE_REPARSE_POINT = 0x00000400
FILE_ATTRIBUTE_COMPRESSED = 0x00000800
FILE_ATTRIBUTE_OFFLINE = 0x00001000
FILE_ATTRIBUTE_NOT_CONTENT_INDEXED = 0x00002000
FILE_ATTRIBUTE_ENCRYPTED = 0x00004000
FILE_ATTRIBUTE_INTEGRITY_STREAM = 0x00008000
FILE_ATTRIBUTE_VIRTUAL = 0x00010000
FILE_ATTRIBUTE_NO_SCRUB_DATA = 0x00020000
FILE_ATTRIBUTE_EA = 0x00040000
)
// Windows processor architectures.
//
//nolint:revive // var-naming: ALL_CAPS
const (
PROCESSOR_ARCHITECTURE_INTEL = 0
PROCESSOR_ARCHITECTURE_MIPS = 1
PROCESSOR_ARCHITECTURE_ALPHA = 2
PROCESSOR_ARCHITECTURE_PPC = 3
PROCESSOR_ARCHITECTURE_SHX = 4
PROCESSOR_ARCHITECTURE_ARM = 5
PROCESSOR_ARCHITECTURE_IA64 = 6
PROCESSOR_ARCHITECTURE_ALPHA64 = 7
PROCESSOR_ARCHITECTURE_MSIL = 8
PROCESSOR_ARCHITECTURE_AMD64 = 9
PROCESSOR_ARCHITECTURE_IA32_ON_WIN64 = 10
PROCESSOR_ARCHITECTURE_NEUTRAL = 11
PROCESSOR_ARCHITECTURE_ARM64 = 12
)
var wimImageTag = [...]byte{'M', 'S', 'W', 'I', 'M', 0, 0, 0}
// todo: replace this with pkg/guid.GUID (and add tests to make sure nothing breaks)
type guid struct {
Data1 uint32
Data2 uint16
Data3 uint16
Data4 [8]byte
}
func (g guid) String() string {
return fmt.Sprintf("%08x-%04x-%04x-%02x%02x-%02x%02x%02x%02x%02x%02x",
g.Data1,
g.Data2,
g.Data3,
g.Data4[0],
g.Data4[1],
g.Data4[2],
g.Data4[3],
g.Data4[4],
g.Data4[5],
g.Data4[6],
g.Data4[7])
}
type resourceDescriptor struct {
FlagsAndCompressedSize uint64
Offset int64
OriginalSize int64
}
type resFlag byte
//nolint:deadcode,varcheck // need unused variables for iota to work
const (
resFlagFree resFlag = 1 << iota
resFlagMetadata
resFlagCompressed
resFlagSpanned
)
const validate = false
const supportedResFlags = resFlagMetadata | resFlagCompressed
func (r *resourceDescriptor) Flags() resFlag {
return resFlag(r.FlagsAndCompressedSize >> 56)
}
func (r *resourceDescriptor) CompressedSize() int64 {
return int64(r.FlagsAndCompressedSize & 0xffffffffffffff)
}
func (r *resourceDescriptor) String() string {
s := fmt.Sprintf("%d bytes at %d", r.CompressedSize(), r.Offset)
if r.Flags()&4 != 0 {
s += fmt.Sprintf(" (uncompresses to %d)", r.OriginalSize)
}
return s
}
// SHA1Hash contains the SHA1 hash of a file or stream.
type SHA1Hash [20]byte
type streamDescriptor struct {
resourceDescriptor
PartNumber uint16
RefCount uint32
Hash SHA1Hash
}
type hdrFlag uint32
//nolint:deadcode,varcheck // need unused variables for iota to work
const (
hdrFlagReserved hdrFlag = 1 << iota
hdrFlagCompressed
hdrFlagReadOnly
hdrFlagSpanned
hdrFlagResourceOnly
hdrFlagMetadataOnly
hdrFlagWriteInProgress
hdrFlagRpFix
)
//nolint:deadcode,varcheck // need unused variables for iota to work
const (
hdrFlagCompressReserved hdrFlag = 1 << (iota + 16)
hdrFlagCompressXpress
hdrFlagCompressLzx
)
const supportedHdrFlags = hdrFlagRpFix | hdrFlagReadOnly | hdrFlagCompressed | hdrFlagCompressLzx
type wimHeader struct {
ImageTag [8]byte
Size uint32
Version uint32
Flags hdrFlag
CompressionSize uint32
WIMGuid guid
PartNumber uint16
TotalParts uint16
ImageCount uint32
OffsetTable resourceDescriptor
XMLData resourceDescriptor
BootMetadata resourceDescriptor
BootIndex uint32
Padding uint32
Integrity resourceDescriptor
Unused [60]byte
}
type securityblockDisk struct {
TotalLength uint32
NumEntries uint32
}
const securityblockDiskSize = 8
type direntry struct {
Attributes uint32
SecurityID uint32
SubdirOffset int64
Unused1, Unused2 int64
CreationTime Filetime
LastAccessTime Filetime
LastWriteTime Filetime
Hash SHA1Hash
Padding uint32
ReparseHardLink int64
StreamCount uint16
ShortNameLength uint16
FileNameLength uint16
}
var direntrySize = int64(binary.Size(direntry{}) + 8) // includes an 8-byte length prefix
type streamentry struct {
Unused int64
Hash SHA1Hash
NameLength int16
}
var streamentrySize = int64(binary.Size(streamentry{}) + 8) // includes an 8-byte length prefix
// Filetime represents a Windows time.
type Filetime struct {
LowDateTime uint32
HighDateTime uint32
}
// Time returns the time as time.Time.
func (ft *Filetime) Time() time.Time {
// 100-nanosecond intervals since January 1, 1601
nsec := int64(ft.HighDateTime)<<32 + int64(ft.LowDateTime)
// change starting time to the Epoch (00:00:00 UTC, January 1, 1970)
nsec -= 116444736000000000
// convert into nanoseconds
nsec *= 100
return time.Unix(0, nsec)
}
// UnmarshalXML unmarshalls the time from a WIM XML blob.
func (ft *Filetime) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
type Time struct {
Low string `xml:"LOWPART"`
High string `xml:"HIGHPART"`
}
var t Time
err := d.DecodeElement(&t, &start)
if err != nil {
return err
}
low, err := strconv.ParseUint(t.Low, 0, 32)
if err != nil {
return err
}
high, err := strconv.ParseUint(t.High, 0, 32)
if err != nil {
return err
}
ft.LowDateTime = uint32(low)
ft.HighDateTime = uint32(high)
return nil
}
type info struct {
Image []ImageInfo `xml:"IMAGE"`
}
// ImageInfo contains information about the image.
type ImageInfo struct {
Name string `xml:"NAME"`
Index int `xml:"INDEX,attr"`
CreationTime Filetime `xml:"CREATIONTIME"`
ModTime Filetime `xml:"LASTMODIFICATIONTIME"`
Windows *WindowsInfo `xml:"WINDOWS"`
}
// WindowsInfo contains information about the Windows installation in the image.
type WindowsInfo struct {
Arch byte `xml:"ARCH"`
ProductName string `xml:"PRODUCTNAME"`
EditionID string `xml:"EDITIONID"`
InstallationType string `xml:"INSTALLATIONTYPE"`
ProductType string `xml:"PRODUCTTYPE"`
Languages []string `xml:"LANGUAGES>LANGUAGE"`
DefaultLanguage string `xml:"LANGUAGES>DEFAULT"`
Version Version `xml:"VERSION"`
SystemRoot string `xml:"SYSTEMROOT"`
}
// Version represents a Windows build version.
type Version struct {
Major int `xml:"MAJOR"`
Minor int `xml:"MINOR"`
Build int `xml:"BUILD"`
SPBuild int `xml:"SPBUILD"`
SPLevel int `xml:"SPLEVEL"`
}
// ParseError is returned when the WIM cannot be parsed.
type ParseError struct {
Oper string
Path string
Err error
}
func (e *ParseError) Error() string {
if e.Path == "" {
return "WIM parse error at " + e.Oper + ": " + e.Err.Error()
}
return fmt.Sprintf("WIM parse error: %s %s: %s", e.Oper, e.Path, e.Err.Error())
}
func (e *ParseError) Unwrap() error { return e.Err }
// Reader provides functions to read a WIM file.
type Reader struct {
hdr wimHeader
r io.ReaderAt
fileData map[SHA1Hash]resourceDescriptor
XMLInfo string // The XML information about the WIM.
Image []*Image // The WIM's images.
}
// Image represents an image within a WIM file.
type Image struct {
wim *Reader
offset resourceDescriptor
sds [][]byte
rootOffset int64
r io.ReadCloser
curOffset int64
m sync.Mutex
ImageInfo
}
// StreamHeader contains alternate data stream metadata.
type StreamHeader struct {
Name string
Hash SHA1Hash
Size int64
}
// Stream represents an alternate data stream or reparse point data stream.
type Stream struct {
StreamHeader
wim *Reader
offset resourceDescriptor
}
// FileHeader contains file metadata.
type FileHeader struct {
Name string
ShortName string
Attributes uint32
SecurityDescriptor []byte
CreationTime Filetime
LastAccessTime Filetime
LastWriteTime Filetime
Hash SHA1Hash
Size int64
LinkID int64
ReparseTag uint32
ReparseReserved uint32
}
// File represents a file or directory in a WIM image.
type File struct {
FileHeader
Streams []*Stream
offset resourceDescriptor
img *Image
subdirOffset int64
}
// NewReader returns a Reader that can be used to read WIM file data.
func NewReader(f io.ReaderAt) (*Reader, error) {
r := &Reader{r: f}
section := io.NewSectionReader(f, 0, 0xffff)
err := binary.Read(section, binary.LittleEndian, &r.hdr)
if err != nil {
return nil, err
}
if r.hdr.ImageTag != wimImageTag {
return nil, &ParseError{Oper: "image tag", Err: errors.New("not a WIM file")}
}
if r.hdr.Flags&^supportedHdrFlags != 0 {
return nil, fmt.Errorf("unsupported WIM flags %x", r.hdr.Flags&^supportedHdrFlags)
}
if r.hdr.CompressionSize != 0x8000 {
return nil, fmt.Errorf("unsupported compression size %d", r.hdr.CompressionSize)
}
if r.hdr.TotalParts != 1 {
return nil, errors.New("multi-part WIM not supported")
}
fileData, images, err := r.readOffsetTable(&r.hdr.OffsetTable)
if err != nil {
return nil, err
}
xmlinfo, err := r.readXML()
if err != nil {
return nil, err
}
var inf info
err = xml.Unmarshal([]byte(xmlinfo), &inf)
if err != nil {
return nil, &ParseError{Oper: "XML info", Err: err}
}
for i, img := range images {
for _, imgInfo := range inf.Image {
if imgInfo.Index == i+1 {
img.ImageInfo = imgInfo
break
}
}
}
r.fileData = fileData
r.Image = images
r.XMLInfo = xmlinfo
return r, nil
}
// Close releases resources associated with the Reader.
func (r *Reader) Close() error {
for _, img := range r.Image {
img.reset()
}
return nil
}
func (r *Reader) resourceReader(hdr *resourceDescriptor) (io.ReadCloser, error) {
return r.resourceReaderWithOffset(hdr, 0)
}
func (r *Reader) resourceReaderWithOffset(hdr *resourceDescriptor, offset int64) (io.ReadCloser, error) {
var sr io.ReadCloser
section := io.NewSectionReader(r.r, hdr.Offset, hdr.CompressedSize())
if hdr.Flags()&resFlagCompressed == 0 {
_, _ = section.Seek(offset, 0)
sr = io.NopCloser(section)
} else {
cr, err := newCompressedReader(section, hdr.OriginalSize, offset)
if err != nil {
return nil, err
}
sr = cr
}
return sr, nil
}
func (r *Reader) readResource(hdr *resourceDescriptor) ([]byte, error) {
rsrc, err := r.resourceReader(hdr)
if err != nil {
return nil, err
}
defer rsrc.Close()
return io.ReadAll(rsrc)
}
func (r *Reader) readXML() (string, error) {
if r.hdr.XMLData.CompressedSize() == 0 {
return "", nil
}
rsrc, err := r.resourceReader(&r.hdr.XMLData)
if err != nil {
return "", err
}
defer rsrc.Close()
xmlData := make([]uint16, r.hdr.XMLData.OriginalSize/2)
err = binary.Read(rsrc, binary.LittleEndian, xmlData)
if err != nil {
return "", &ParseError{Oper: "XML data", Err: err}
}
// The BOM will always indicate little-endian UTF-16.
if xmlData[0] != 0xfeff {
return "", &ParseError{Oper: "XML data", Err: errors.New("invalid BOM")}
}
return string(utf16.Decode(xmlData[1:])), nil
}
func (r *Reader) readOffsetTable(res *resourceDescriptor) (map[SHA1Hash]resourceDescriptor, []*Image, error) {
fileData := make(map[SHA1Hash]resourceDescriptor)
var images []*Image
offsetTable, err := r.readResource(res)
if err != nil {
return nil, nil, &ParseError{Oper: "offset table", Err: err}
}
br := bytes.NewReader(offsetTable)
for i := 0; ; i++ {
var res streamDescriptor
err := binary.Read(br, binary.LittleEndian, &res)
if err == io.EOF { //nolint:errorlint
break
}
if err != nil {
return nil, nil, &ParseError{Oper: "offset table", Err: err}
}
if res.Flags()&^supportedResFlags != 0 {
return nil, nil, &ParseError{Oper: "offset table", Err: errors.New("unsupported resource flag")}
}
// Validation for ad-hoc testing
if validate {
sec, err := r.resourceReader(&res.resourceDescriptor)
if err != nil {
panic(fmt.Sprint(i, err))
}
hash := sha1.New() //nolint:gosec // not used for secure application
_, err = io.Copy(hash, sec)
sec.Close()
if err != nil {
panic(fmt.Sprint(i, err))
}
var cmphash SHA1Hash
copy(cmphash[:], hash.Sum(nil))
if cmphash != res.Hash {
panic(fmt.Sprint(i, "hash mismatch"))
}
}
if res.Flags()&resFlagMetadata != 0 {
image := &Image{
wim: r,
offset: res.resourceDescriptor,
}
images = append(images, image)
} else {
fileData[res.Hash] = res.resourceDescriptor
}
}
if len(images) != int(r.hdr.ImageCount) {
return nil, nil, &ParseError{Oper: "offset table", Err: errors.New("mismatched image count")}
}
return fileData, images, nil
}
func (*Reader) readSecurityDescriptors(rsrc io.Reader) (sds [][]byte, n int64, err error) {
var secBlock securityblockDisk
err = binary.Read(rsrc, binary.LittleEndian, &secBlock)
if err != nil {
return sds, 0, &ParseError{Oper: "security table", Err: err}
}
n += securityblockDiskSize
secSizes := make([]int64, secBlock.NumEntries)
err = binary.Read(rsrc, binary.LittleEndian, &secSizes)
if err != nil {
return sds, n, &ParseError{Oper: "security table sizes", Err: err}
}
n += int64(secBlock.NumEntries * 8)
sds = make([][]byte, secBlock.NumEntries)
for i, size := range secSizes {
sd := make([]byte, size&0xffffffff)
_, err = io.ReadFull(rsrc, sd)
if err != nil {
return sds, n, &ParseError{Oper: "security descriptor", Err: err}
}
n += int64(len(sd))
sds[i] = sd
}
secsize := int64((secBlock.TotalLength + 7) &^ 7)
if n > secsize {
return sds, n, &ParseError{Oper: "security descriptor", Err: errors.New("security descriptor table too small")}
}
_, err = io.CopyN(io.Discard, rsrc, secsize-n)
if err != nil {
return sds, n, err
}
n = secsize
return sds, n, nil
}
// Open parses the image and returns the root directory.
func (img *Image) Open() (*File, error) {
if img.sds == nil {
rsrc, err := img.wim.resourceReaderWithOffset(&img.offset, img.rootOffset)
if err != nil {
return nil, err
}
sds, n, err := img.wim.readSecurityDescriptors(rsrc)
if err != nil {
rsrc.Close()
return nil, err
}
img.sds = sds
img.r = rsrc
img.rootOffset = n
img.curOffset = n
}
f, err := img.readdir(img.rootOffset)
if err != nil {
return nil, err
}
if len(f) != 1 {
return nil, &ParseError{Oper: "root directory", Err: errors.New("expected exactly 1 root directory entry")}
}
return f[0], err
}
func (img *Image) reset() {
if img.r != nil {
img.r.Close()
img.r = nil
}
img.curOffset = -1
}
func (img *Image) readdir(offset int64) ([]*File, error) {
img.m.Lock()
defer img.m.Unlock()
if offset < img.curOffset || offset > img.curOffset+chunkSize {
// Reset to seek backward or to seek forward very far.
img.reset()
}
if img.r == nil {
rsrc, err := img.wim.resourceReaderWithOffset(&img.offset, offset)
if err != nil {
return nil, err
}
img.r = rsrc
img.curOffset = offset
}
if offset > img.curOffset {
_, err := io.CopyN(io.Discard, img.r, offset-img.curOffset)
if err != nil {
img.reset()
if err == io.EOF { //nolint:errorlint
err = io.ErrUnexpectedEOF
}
return nil, err
}
}
var entries []*File
for {
e, n, err := img.readNextEntry(img.r)
img.curOffset += n
if err == io.EOF { //nolint:errorlint
break
}
if err != nil {
img.reset()
return nil, err
}
entries = append(entries, e)
}
return entries, nil
}
func (img *Image) readNextEntry(r io.Reader) (*File, int64, error) {
var length int64
err := binary.Read(r, binary.LittleEndian, &length)
if err != nil {
return nil, 0, &ParseError{Oper: "directory length check", Err: err}
}
if length == 0 {
return nil, 8, io.EOF
}
left := length
if left < direntrySize {
return nil, 0, &ParseError{Oper: "directory entry", Err: errors.New("size too short")}
}
var dentry direntry
err = binary.Read(r, binary.LittleEndian, &dentry)
if err != nil {
return nil, 0, &ParseError{Oper: "directory entry", Err: err}
}
left -= direntrySize
namesLen := int64(dentry.FileNameLength + 2 + dentry.ShortNameLength)
if left < namesLen {
return nil, 0, &ParseError{Oper: "directory entry", Err: errors.New("size too short for names")}
}
names := make([]uint16, namesLen/2)
err = binary.Read(r, binary.LittleEndian, names)
if err != nil {
return nil, 0, &ParseError{Oper: "file name", Err: err}
}
left -= namesLen
var name, shortName string
if dentry.FileNameLength > 0 {
name = string(utf16.Decode(names[:dentry.FileNameLength/2]))
}
if dentry.ShortNameLength > 0 {
shortName = string(utf16.Decode(names[dentry.FileNameLength/2+1:]))
}
var offset resourceDescriptor
zerohash := SHA1Hash{}
if dentry.Hash != zerohash {
var ok bool
offset, ok = img.wim.fileData[dentry.Hash]
if !ok {
return nil, 0, &ParseError{
Oper: "directory entry",
Path: name,
Err: fmt.Errorf("could not find file data matching hash %#v", dentry),
}
}
}
f := &File{
FileHeader: FileHeader{
Attributes: dentry.Attributes,
CreationTime: dentry.CreationTime,
LastAccessTime: dentry.LastAccessTime,
LastWriteTime: dentry.LastWriteTime,
Hash: dentry.Hash,
Size: offset.OriginalSize,
Name: name,
ShortName: shortName,
},
offset: offset,
img: img,
subdirOffset: dentry.SubdirOffset,
}
isDir := false
if dentry.Attributes&FILE_ATTRIBUTE_REPARSE_POINT == 0 {
f.LinkID = dentry.ReparseHardLink
if dentry.Attributes&FILE_ATTRIBUTE_DIRECTORY != 0 {
isDir = true
}
} else {
f.ReparseTag = uint32(dentry.ReparseHardLink)
f.ReparseReserved = uint32(dentry.ReparseHardLink >> 32)
}
if isDir && f.subdirOffset == 0 {
return nil, 0, &ParseError{Oper: "directory entry", Path: name, Err: errors.New("no subdirectory data for directory")}
} else if !isDir && f.subdirOffset != 0 {
return nil, 0, &ParseError{Oper: "directory entry", Path: name, Err: errors.New("unexpected subdirectory data for non-directory")}
}
if dentry.SecurityID != 0xffffffff {
f.SecurityDescriptor = img.sds[dentry.SecurityID]
}
_, err = io.CopyN(io.Discard, r, left)
if err != nil {
if err == io.EOF { //nolint:errorlint
err = io.ErrUnexpectedEOF
}
return nil, 0, err
}
if dentry.StreamCount > 0 {
var streams []*Stream
for i := uint16(0); i < dentry.StreamCount; i++ {
s, n, err := img.readNextStream(r)
length += n
if err != nil {
return nil, 0, err
}
// The first unnamed stream should be treated as the file stream.
if i == 0 && s.Name == "" {
f.Hash = s.Hash
f.Size = s.Size
f.offset = s.offset
} else if s.Name != "" {
streams = append(streams, s)
}
}
f.Streams = streams
}
if dentry.Attributes&FILE_ATTRIBUTE_REPARSE_POINT != 0 && f.Size == 0 {
return nil, 0, &ParseError{
Oper: "directory entry",
Path: name,
Err: errors.New("reparse point is missing reparse stream"),
}
}
return f, length, nil
}
func (img *Image) readNextStream(r io.Reader) (*Stream, int64, error) {
var length int64
err := binary.Read(r, binary.LittleEndian, &length)
if err != nil {
if err == io.EOF { //nolint:errorlint
err = io.ErrUnexpectedEOF
}
return nil, 0, &ParseError{Oper: "stream length check", Err: err}
}
left := length
if left < streamentrySize {
return nil, 0, &ParseError{Oper: "stream entry", Err: errors.New("size too short")}
}
var sentry streamentry
err = binary.Read(r, binary.LittleEndian, &sentry)
if err != nil {
return nil, 0, &ParseError{Oper: "stream entry", Err: err}
}
left -= streamentrySize
if left < int64(sentry.NameLength) {
return nil, 0, &ParseError{Oper: "stream entry", Err: errors.New("size too short for name")}
}
names := make([]uint16, sentry.NameLength/2)
err = binary.Read(r, binary.LittleEndian, names)
if err != nil {
return nil, 0, &ParseError{Oper: "file name", Err: err}
}
left -= int64(sentry.NameLength)
name := string(utf16.Decode(names))
var offset resourceDescriptor
if sentry.Hash != (SHA1Hash{}) {
var ok bool
offset, ok = img.wim.fileData[sentry.Hash]
if !ok {
return nil, 0, &ParseError{
Oper: "stream entry",
Path: name,
Err: fmt.Errorf("could not find file data matching hash %v", sentry.Hash),
}
}
}
s := &Stream{
StreamHeader: StreamHeader{
Hash: sentry.Hash,
Size: offset.OriginalSize,
Name: name,
},
wim: img.wim,
offset: offset,
}
_, err = io.CopyN(io.Discard, r, left)
if err != nil {
if err == io.EOF { //nolint:errorlint
err = io.ErrUnexpectedEOF
}
return nil, 0, err
}
return s, length, nil
}
// Open returns an io.ReadCloser that can be used to read the stream's contents.
func (s *Stream) Open() (io.ReadCloser, error) {
return s.wim.resourceReader(&s.offset)
}
// Open returns an io.ReadCloser that can be used to read the file's contents.
func (f *File) Open() (io.ReadCloser, error) {
return f.img.wim.resourceReader(&f.offset)
}
// Readdir reads the directory entries.
func (f *File) Readdir() ([]*File, error) {
if !f.IsDir() {
return nil, errors.New("not a directory")
}
return f.img.readdir(f.subdirOffset)
}
// IsDir returns whether the given file is a directory. It returns false when it
// is a directory reparse point.
func (f *FileHeader) IsDir() bool {
return f.Attributes&(FILE_ATTRIBUTE_DIRECTORY|FILE_ATTRIBUTE_REPARSE_POINT) == FILE_ATTRIBUTE_DIRECTORY
}
+21
View File
@@ -0,0 +1,21 @@
Copyright (c) 2019-2020, Kamil Domański and contributors
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+161
View File
@@ -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)
}
}
```
+292
View File
@@ -0,0 +1,292 @@
package iso9660
import (
"fmt"
"io"
"os"
"strings"
"time"
)
// Image is a wrapper around an image file that allows reading its ISO9660 data
type Image struct {
ra io.ReaderAt
volumeDescriptors []volumeDescriptor
}
// OpenImage returns an Image reader reating from a given file
func OpenImage(ra io.ReaderAt) (*Image, error) {
i := &Image{ra: ra}
if err := i.readVolumes(); err != nil {
return nil, err
}
return i, nil
}
func (i *Image) readVolumes() error {
buffer := make([]byte, sectorSize)
// skip the 16 sectors of system area
for sector := 16; ; sector++ {
if _, err := i.ra.ReadAt(buffer, int64(sector)*int64(sectorSize)); err != nil {
return err
}
var vd volumeDescriptor
if err := vd.UnmarshalBinary(buffer); err != nil {
return err
}
// NOTE: the instance of the root Directory Record that appears
// in the Primary Volume Descriptor cannot contain a System Use
// field. See the SUSP standard.
i.volumeDescriptors = append(i.volumeDescriptors, vd)
if vd.Header.Type == volumeTypeTerminator {
break
}
}
return nil
}
// RootDir returns the File structure corresponding to the root directory
// of the first primary volume
func (i *Image) RootDir() (*File, error) {
for _, vd := range i.volumeDescriptors {
if vd.Type() == volumeTypePrimary {
return &File{de: vd.Primary.RootDirectoryEntry, ra: i.ra, children: nil, isRootDir: true}, nil
}
}
return nil, os.ErrNotExist
}
// RootDir returns the label of the first Primary Volume
func (i *Image) Label() (string, error) {
for _, vd := range i.volumeDescriptors {
if vd.Type() == volumeTypePrimary {
return string(vd.Primary.VolumeIdentifier), nil
}
}
return "", os.ErrNotExist
}
// File is a os.FileInfo-compatible wrapper around an ISO9660 directory entry
type File struct {
ra io.ReaderAt
de *DirectoryEntry
children []*File
isRootDir bool
susp *SUSPMetadata
}
var _ os.FileInfo = &File{}
func (f *File) hasRockRidge() bool {
return f.susp != nil && f.susp.HasRockRidge
}
// IsDir returns true if the entry is a directory or false otherwise
func (f *File) IsDir() bool {
if f.hasRockRidge() {
if mode, err := f.de.SystemUseEntries.GetPosixAttr(); err == nil {
return mode&os.ModeDir != 0
}
}
return f.de.FileFlags&dirFlagDir != 0
}
// ModTime returns the entry's recording time
func (f *File) ModTime() time.Time {
return time.Time(f.de.RecordingDateTime)
}
// Mode returns file mode when available.
// Otherwise it returns os.FileMode flag set with the os.ModeDir flag enabled in case of directories.
func (f *File) Mode() os.FileMode {
if f.hasRockRidge() {
if mode, err := f.de.SystemUseEntries.GetPosixAttr(); err == nil {
return mode
}
}
var mode os.FileMode
if f.IsDir() {
mode |= os.ModeDir
}
return mode
}
// Name returns the base name of the given entry
func (f *File) Name() string {
if f.hasRockRidge() {
if name := f.de.SystemUseEntries.GetRockRidgeName(); name != "" {
return name
}
}
if f.IsDir() {
return f.de.Identifier
}
// drop the version part
// assume only one ';'
fileIdentifier := strings.Split(f.de.Identifier, ";")[0]
// split into filename and extension
// assume only only one '.'
splitFileIdentifier := strings.Split(fileIdentifier, ".")
// there's no dot in the name, thus no extension
if len(splitFileIdentifier) == 1 {
return splitFileIdentifier[0]
}
// extension is empty, return just the name without a dot
if len(splitFileIdentifier[1]) == 0 {
return splitFileIdentifier[0]
}
// return file with extension
return fileIdentifier
}
// Size returns the size in bytes of the extent occupied by the file or directory
func (f *File) Size() int64 {
return int64(f.de.ExtentLength)
}
// Sys returns nil
func (f *File) Sys() interface{} {
return nil
}
// GetAllChildren returns the children entries in case of a directory
// or an error in case of a file. It includes the "." and ".." entries.
func (f *File) GetAllChildren() ([]*File, error) {
if !f.IsDir() {
return nil, fmt.Errorf("%s is not a directory", f.Name())
}
if f.children != nil {
return f.children, nil
}
baseOffset := uint32(f.de.ExtentLocation) * sectorSize
buffer := make([]byte, sectorSize)
for bytesProcessed := uint32(0); bytesProcessed < uint32(f.de.ExtentLength); bytesProcessed += sectorSize {
if _, err := f.ra.ReadAt(buffer, int64(baseOffset+bytesProcessed)); err != nil {
return nil, nil
}
for i := uint32(0); i < sectorSize; {
entryLength := uint32(buffer[i])
if entryLength == 0 {
break
}
if i+entryLength > sectorSize {
return nil, fmt.Errorf("reading directory entries: DE outside of sector boundries")
}
newDE := &DirectoryEntry{}
if err := newDE.UnmarshalBinary(buffer[i : i+entryLength]); err != nil {
return nil, err
}
// Is this a root directory '.' record?
if f.isRootDir && newDE.Identifier == string([]byte{0}) {
newDE.SystemUseEntries, _ = splitSystemUseEntries(newDE.SystemUse, f.ra)
// get the SP record
if len(newDE.SystemUseEntries) > 0 && newDE.SystemUseEntries[0].Type() == "SP" {
sprecord, err := SPRecordDecode(newDE.SystemUseEntries[0])
if err != nil {
return nil, fmt.Errorf("invalid SP record: %w", err)
}
hasRockRidge, err := suspHasRockRidge(newDE.SystemUseEntries)
if err != nil {
return nil, fmt.Errorf("failed to check for Rock Ridge extension: %w", err)
}
// save SUSP offset from the SP record
f.susp = &SUSPMetadata{
Offset: sprecord.BytesSkipped,
HasRockRidge: hasRockRidge,
}
}
} else {
// are we on a volume with SUSP?
if f.susp != nil {
// Ignore error if some of the SUSP data is malformed. Just take the valid part.
offsetSystemUse := newDE.SystemUse[f.susp.Offset:]
newDE.SystemUseEntries, _ = splitSystemUseEntries(offsetSystemUse, f.ra)
}
}
i += entryLength
newFile := &File{ra: f.ra,
de: newDE,
children: nil,
susp: f.susp.Clone(),
}
f.children = append(f.children, newFile)
}
}
return f.children, nil
}
// GetChildren returns the children entries in case of a directory
// or an error in case of a file. It does NOT include the "." and ".." entries.
func (f *File) GetChildren() ([]*File, error) {
children, err := f.GetAllChildren()
if err != nil {
return nil, err
}
filteredChildren := make([]*File, 0, len(children)-2)
for _, child := range children {
if child.de.Identifier == string([]byte{0}) || child.de.Identifier == string([]byte{1}) {
continue
}
filteredChildren = append(filteredChildren, child)
}
return filteredChildren, nil
}
// GetDotEntry returns the "." entry of a directory
// or an error in case of a file.
func (f *File) GetDotEntry() (*File, error) {
children, err := f.GetAllChildren()
if err != nil {
return nil, err
}
for _, child := range children {
if child.de.Identifier == string([]byte{0}) {
return child, nil
}
}
return nil, nil
}
// Reader returns a reader that allows to read the file's data.
// If File is a directory, it returns nil.
func (f *File) Reader() io.Reader {
if f.IsDir() {
return nil
}
baseOffset := int64(f.de.ExtentLocation) * int64(sectorSize)
return io.NewSectionReader(f.ra, baseOffset, int64(f.de.ExtentLength))
}
+658
View File
@@ -0,0 +1,658 @@
package iso9660
import (
"bytes"
"container/list"
"errors"
"fmt"
"io"
"math"
"os"
"path"
"path/filepath"
"runtime"
"strings"
"sync/atomic"
"time"
)
const (
primaryVolumeDirectoryIdentifierMaxLength = 31 // ECMA-119 7.6.3
primaryVolumeFileIdentifierMaxLength = 30 // ECMA-119 7.5
)
var (
// ErrFileTooLarge is returned when trying to process a file of size greater
// than 4GB, which due to the 32-bit address limitation is not possible
// except with ISO 9660-Level 3
ErrFileTooLarge = errors.New("file is exceeding the maximum file size of 4GB")
)
// ImageWriter is responsible for staging an image's contents
// and writing them to an image.
type ImageWriter struct {
stagingDir string
}
// NewWriter creates a new ImageWrite and initializes its temporary staging dir.
// Cleanup should be called after the ImageWriter is no longer needed.
func NewWriter() (*ImageWriter, error) {
tmp, err := os.MkdirTemp("", "")
if err != nil {
return nil, err
}
return &ImageWriter{stagingDir: tmp}, nil
}
// Cleanup deletes the underlying temporary staging directory of an ImageWriter.
// It can be called multiple times without issues.
func (iw *ImageWriter) Cleanup() error {
if iw.stagingDir == "" {
return nil
}
if err := os.RemoveAll(iw.stagingDir); err != nil {
return err
}
iw.stagingDir = ""
return nil
}
// AddFile adds a file to the ImageWriter's staging area.
// All path components are mangled to match basic ISO9660 filename requirements.
func (iw *ImageWriter) AddFile(data io.Reader, filePath string) error {
directoryPath, fileName := manglePath(filePath)
if err := os.MkdirAll(path.Join(iw.stagingDir, directoryPath), 0755); err != nil {
return err
}
f, err := os.OpenFile(path.Join(iw.stagingDir, directoryPath, fileName), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
if err != nil {
return err
}
defer f.Close()
_, err = io.Copy(f, data)
return err
}
func failIfSymlink(path string) error {
info, err := os.Lstat(path)
if err != nil {
return err
}
if info.Mode()&os.ModeSymlink != 0 {
return fmt.Errorf("%q is a symlink - these are not yet supported", path)
}
return nil
}
// AddLocalFile adds a file identified by its path to the ImageWriter's staging area.
func (iw *ImageWriter) AddLocalFile(origin, target string) error {
if err := failIfSymlink(origin); err != nil {
return err
}
directoryPath, fileName := manglePath(target)
if err := os.MkdirAll(path.Join(iw.stagingDir, directoryPath), 0755); err != nil {
return err
}
// try to hardlink file to staging area before copying.
stagedFile := path.Join(iw.stagingDir, directoryPath, fileName)
if err := os.Remove(stagedFile); err != nil && !os.IsNotExist(err) {
return err
}
if err := os.Link(origin, stagedFile); err == nil {
return nil
}
f, err := os.Open(origin)
if err != nil {
return err
}
defer f.Close()
return iw.AddFile(f, target)
}
func ensureIsDirectory(path string) error {
f, err := os.Open(path)
if err != nil {
return err
}
defer f.Close()
fileinfo, err := f.Stat()
if err != nil {
return err
}
if !fileinfo.IsDir() {
return fmt.Errorf("%q is not a directory", path)
}
return nil
}
// AddLocalDirectory adds a directory recursively to the ImageWriter's staging area.
func (iw *ImageWriter) AddLocalDirectory(origin, target string) error {
if err := ensureIsDirectory(origin); err != nil {
return err
}
walkfn := func(path string, info os.FileInfo, err error) error {
if info.IsDir() {
return nil
}
relPath := path[len(origin):] // We need the path to be relative to the origin.
return iw.AddLocalFile(path, filepath.Join(target, relPath))
}
return filepath.Walk(origin, walkfn)
}
func manglePath(input string) (string, string) {
input = posixifyPath(input)
nonEmptySegments := splitPath(input)
dirSegments := nonEmptySegments[:len(nonEmptySegments)-1]
name := nonEmptySegments[len(nonEmptySegments)-1]
for i := 0; i < len(dirSegments); i++ {
dirSegments[i] = mangleDirectoryName(dirSegments[i])
}
name = mangleFileName(name)
return path.Join(dirSegments...), name
}
// Converts given path to Posix (replacing \ with /)
//
// @param {string} givenPath Path to convert
//
// @returns {string} Converted filepath
func posixifyPath(path string) string {
if runtime.GOOS == "windows" {
return strings.ReplaceAll(path, "\\", "/")
}
return path
}
func splitPath(input string) []string {
rawSegments := strings.Split(input, "/")
var nonEmptySegments []string
for _, s := range rawSegments {
if len(s) > 0 {
nonEmptySegments = append(nonEmptySegments, s)
}
}
return nonEmptySegments
}
// See ECMA-119 7.5
func mangleFileName(input string) string {
// https://github.com/torvalds/linux/blob/v5.6/fs/isofs/dir.c#L29
input = strings.ToLower(input)
split := strings.Split(input, ".")
version := "1"
var filename, extension string
if len(split) == 1 {
filename = split[0]
} else {
filename = strings.Join(split[:len(split)-1], "_")
extension = split[len(split)-1]
}
// enough characters for the `.ignition` extension
extension = mangleD1String(extension, 8)
maxRemainingFilenameLength := primaryVolumeFileIdentifierMaxLength - (1 + len(version))
if len(extension) > 0 {
maxRemainingFilenameLength -= (1 + len(extension))
}
filename = mangleD1String(filename, maxRemainingFilenameLength)
if len(extension) > 0 {
return filename + "." + extension + ";" + version
}
return filename + ";" + version
}
// See ECMA-119 7.6
func mangleDirectoryName(input string) string {
return mangleD1String(input, primaryVolumeDirectoryIdentifierMaxLength)
}
func mangleD1String(input string, maxCharacters int) string {
// https://github.com/torvalds/linux/blob/v5.6/fs/isofs/dir.c#L29
input = strings.ToLower(input)
var mangledString string
for i := 0; i < len(input) && i < maxCharacters; i++ {
r := rune(input[i])
if strings.ContainsRune(d1Characters, r) {
mangledString += string(r)
} else {
mangledString += "_"
}
}
return mangledString
}
// calculateDirChildrenSectors calculates the total mashalled size of all DirectoryEntries
// within a directory. The size of each entry depends of the length of the filename.
func calculateDirChildrenSectors(path string) (uint32, error) {
contents, err := os.ReadDir(path)
if err != nil {
return 0, err
}
var sectors uint32
var currentSectorOccupied uint32 = 68 // the 0x00 and 0x01 entries
for _, c := range contents {
identifierLen := len(c.Name())
idPaddingLen := (identifierLen + 1) % 2
entryLength := uint32(33 + identifierLen + idPaddingLen)
if currentSectorOccupied+entryLength > sectorSize {
sectors++
currentSectorOccupied = entryLength
} else {
currentSectorOccupied += entryLength
}
}
if currentSectorOccupied > 0 {
sectors++
}
return sectors, nil
}
func fileLengthToSectors(l uint32) uint32 {
if (l % sectorSize) == 0 {
return l / sectorSize
}
return (l / sectorSize) + 1
}
type writeContext struct {
stagingDir string
timestamp RecordingTimestamp
freeSectorPointer uint32
}
func (wc *writeContext) allocateSectors(n uint32) uint32 {
return atomic.AddUint32(&wc.freeSectorPointer, n) - n
}
func (wc *writeContext) createDEForRoot() (*DirectoryEntry, error) {
extentLengthInSectors, err := calculateDirChildrenSectors(wc.stagingDir)
if err != nil {
return nil, err
}
extentLocation := wc.allocateSectors(extentLengthInSectors)
de := &DirectoryEntry{
ExtendedAtributeRecordLength: 0,
ExtentLocation: int32(extentLocation),
ExtentLength: uint32(extentLengthInSectors * sectorSize),
RecordingDateTime: wc.timestamp,
FileFlags: dirFlagDir,
FileUnitSize: 0, // 0 for non-interleaved write
InterleaveGap: 0, // not interleaved
VolumeSequenceNumber: 1, // we only have one volume
Identifier: string([]byte{0}),
SystemUse: []byte{},
}
return de, nil
}
type itemToWrite struct {
isDirectory bool
dirPath string
ownEntry *DirectoryEntry
parentEntery *DirectoryEntry
childrenEntries []*DirectoryEntry
targetSector uint32
}
// scanDirectory reads the directory's contents and adds them to the queue, as well as stores all their DirectoryEntries in the item,
// because we'll need them to write this item's descriptor.
func (wc *writeContext) scanDirectory(item *itemToWrite, dirPath string, ownEntry *DirectoryEntry, parentEntery *DirectoryEntry, targetSector uint32) (*list.List, error) {
contents, err := os.ReadDir(dirPath)
if err != nil {
return nil, err
}
itemsToWrite := list.New()
for _, c := range contents {
var (
fileFlags byte
extentLengthInSectors uint32
extentLength uint32
)
if c.IsDir() {
extentLengthInSectors, err = calculateDirChildrenSectors(path.Join(dirPath, c.Name()))
if err != nil {
return nil, err
}
fileFlags = dirFlagDir
extentLength = extentLengthInSectors * sectorSize
} else {
fileinfo, err := c.Info()
if err != nil {
return nil, err
}
if fileinfo.Size() > int64(math.MaxUint32) {
return nil, ErrFileTooLarge
}
extentLength = uint32(fileinfo.Size())
extentLengthInSectors = fileLengthToSectors(extentLength)
fileFlags = 0
}
extentLocation := wc.allocateSectors(extentLengthInSectors)
de := &DirectoryEntry{
ExtendedAtributeRecordLength: 0,
ExtentLocation: int32(extentLocation),
ExtentLength: uint32(extentLength),
RecordingDateTime: wc.timestamp,
FileFlags: fileFlags,
FileUnitSize: 0, // 0 for non-interleaved write
InterleaveGap: 0, // not interleaved
VolumeSequenceNumber: 1, // we only have one volume
Identifier: c.Name(),
SystemUse: []byte{},
}
// Add this child's descriptor to the currently scanned directory's list of children,
// so that later we can use it for writing the current item.
if item.childrenEntries == nil {
item.childrenEntries = []*DirectoryEntry{de}
} else {
item.childrenEntries = append(item.childrenEntries, de)
}
// queue this child for processing
itemsToWrite.PushBack(itemToWrite{
isDirectory: c.IsDir(),
dirPath: path.Join(dirPath, c.Name()),
ownEntry: de,
parentEntery: ownEntry,
targetSector: uint32(de.ExtentLocation),
})
}
return itemsToWrite, nil
}
// processDirectory writes a given directory item to the destination sectors
func processDirectory(w io.Writer, children []*DirectoryEntry, ownEntry *DirectoryEntry, parentEntry *DirectoryEntry) error {
var currentOffset uint32
currentDE := ownEntry.Clone()
currentDE.Identifier = string([]byte{0})
parentDE := parentEntry.Clone()
parentDE.Identifier = string([]byte{1})
currentDEData, err := currentDE.MarshalBinary()
if err != nil {
return err
}
parentDEData, err := parentDE.MarshalBinary()
if err != nil {
return err
}
n, err := w.Write(currentDEData)
if err != nil {
return err
}
currentOffset += uint32(n)
n, err = w.Write(parentDEData)
if err != nil {
return err
}
currentOffset += uint32(n)
for _, childDescriptor := range children {
data, err := childDescriptor.MarshalBinary()
if err != nil {
return err
}
remainingSectorSpace := sectorSize - (currentOffset % sectorSize)
if remainingSectorSpace < uint32(len(data)) {
// ECMA-119 6.8.1.1 If the body of the next descriptor won't fit into the sector,
// we fill the rest of space with zeros and skip to the next sector.
zeros := bytes.Repeat([]byte{0}, int(remainingSectorSpace))
_, err = w.Write(zeros)
if err != nil {
return err
}
// skip to the next sector
currentOffset = 0
}
n, err = w.Write(data)
if err != nil {
return err
}
currentOffset += uint32(n)
}
// fill with zeros to the end of the sector
remainingSectorSpace := sectorSize - (currentOffset % sectorSize)
if remainingSectorSpace != 0 {
zeros := bytes.Repeat([]byte{0}, int(remainingSectorSpace))
_, err = w.Write(zeros)
if err != nil {
return err
}
}
return nil
}
func processFile(w io.Writer, dirPath string) error {
f, err := os.Open(dirPath)
if err != nil {
return err
}
defer f.Close()
fileinfo, err := f.Stat()
if err != nil {
return err
}
if fileinfo.Size() > int64(math.MaxUint32) {
return ErrFileTooLarge
}
buffer := make([]byte, sectorSize)
for bytesLeft := uint32(fileinfo.Size()); bytesLeft > 0; {
var toRead uint32
if bytesLeft < sectorSize {
toRead = bytesLeft
} else {
toRead = sectorSize
}
if _, err = io.ReadAtLeast(f, buffer, int(toRead)); err != nil {
return err
}
if _, err = w.Write(buffer); err != nil {
return err
}
bytesLeft -= toRead
}
// We already write a whole sector-sized buffer, so there's need to fill with zeroes.
return nil
}
// traverseStagingDir creates a new queue of items to write by traversing the staging directory
func (wc *writeContext) traverseStagingDir(rootItem itemToWrite) (*list.List, error) {
itemsToWrite := list.New()
itemsToWrite.PushBack(rootItem)
for item := itemsToWrite.Front(); item != nil; item = item.Next() {
it := item.Value.(itemToWrite)
if it.isDirectory {
newItems, err := wc.scanDirectory(&it, it.dirPath, it.ownEntry, it.parentEntery, it.targetSector)
if err != nil {
relativePath := it.dirPath[len(wc.stagingDir):]
return nil, fmt.Errorf("processing %s: %s", relativePath, err)
}
itemsToWrite.PushBackList(newItems)
}
item.Value = it
}
return itemsToWrite, nil
}
func writeAll(w io.Writer, itemsToWrite *list.List) error {
for item := itemsToWrite.Front(); item != nil; item = item.Next() {
it := item.Value.(itemToWrite)
var err error
if it.isDirectory {
err = processDirectory(w, it.childrenEntries, it.ownEntry, it.parentEntery)
} else {
err = processFile(w, it.dirPath)
}
if err != nil {
return err
}
}
return nil
}
// WriteTo writes the image to the given WriterAt
func (iw *ImageWriter) WriteTo(w io.Writer, volumeIdentifier string) error {
now := time.Now()
wc := writeContext{
stagingDir: iw.stagingDir,
timestamp: RecordingTimestamp{},
freeSectorPointer: 18, // system area (16) + 2 volume descriptors
}
rootDE, err := wc.createDEForRoot()
if err != nil {
return fmt.Errorf("creating root directory descriptor: %s", err)
}
rootItem := itemToWrite{
isDirectory: true,
dirPath: wc.stagingDir,
ownEntry: rootDE,
parentEntery: rootDE,
targetSector: uint32(rootDE.ExtentLocation),
}
itemsToWrite, err := wc.traverseStagingDir(rootItem)
if err != nil {
return fmt.Errorf("tranversing staging directory: %s", err)
}
pvd := volumeDescriptor{
Header: volumeDescriptorHeader{
Type: volumeTypePrimary,
Identifier: standardIdentifierBytes,
Version: 1,
},
Primary: &PrimaryVolumeDescriptorBody{
SystemIdentifier: runtime.GOOS,
VolumeIdentifier: volumeIdentifier,
VolumeSpaceSize: int32(wc.freeSectorPointer),
VolumeSetSize: 1,
VolumeSequenceNumber: 1,
LogicalBlockSize: int16(sectorSize),
PathTableSize: 0,
TypeLPathTableLoc: 0,
OptTypeLPathTableLoc: 0,
TypeMPathTableLoc: 0,
OptTypeMPathTableLoc: 0,
RootDirectoryEntry: rootDE,
VolumeSetIdentifier: "",
PublisherIdentifier: "",
DataPreparerIdentifier: "",
ApplicationIdentifier: "github.com/kdomanski/iso9660",
CopyrightFileIdentifier: "",
AbstractFileIdentifier: "",
BibliographicFileIdentifier: "",
VolumeCreationDateAndTime: VolumeDescriptorTimestampFromTime(now),
VolumeModificationDateAndTime: VolumeDescriptorTimestampFromTime(now),
VolumeExpirationDateAndTime: VolumeDescriptorTimestamp{},
VolumeEffectiveDateAndTime: VolumeDescriptorTimestampFromTime(now),
FileStructureVersion: 1,
ApplicationUsed: [512]byte{},
},
}
terminator := volumeDescriptor{
Header: volumeDescriptorHeader{
Type: volumeTypeTerminator,
Identifier: standardIdentifierBytes,
Version: 1,
},
}
// write 16 sectors of zeroes
zeroSector := bytes.Repeat([]byte{0}, int(sectorSize))
for i := uint32(0); i < 16; i++ {
if _, err = w.Write(zeroSector); err != nil {
return err
}
}
buffer, err := pvd.MarshalBinary()
if err != nil {
return err
}
if _, err = w.Write(buffer); err != nil {
return err
}
if buffer, err = terminator.MarshalBinary(); err != nil {
return err
}
if _, err = w.Write(buffer); err != nil {
return err
}
if err = writeAll(w, itemsToWrite); err != nil {
return fmt.Errorf("writing files: %s", err)
}
return nil
}
+605
View File
@@ -0,0 +1,605 @@
// Package iso9660 implements reading and creating basic ISO9660 images.
package iso9660
import (
"encoding"
"encoding/binary"
"errors"
"fmt"
"io"
"strconv"
"strings"
"time"
)
// ISO 9660 Overview
// https://archive.fo/xs9ac
const (
sectorSize uint32 = 2048
systemAreaSize = sectorSize * 16
standardIdentifier = "CD001"
udfIdentifier = "BEA01"
volumeTypeBoot byte = 0
volumeTypePrimary byte = 1
volumeTypeSupplementary byte = 2
volumeTypePartition byte = 3
volumeTypeTerminator byte = 255
volumeDescriptorBodySize = sectorSize - 7
aCharacters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_!\"%&'()*+,-./:;<=>?"
dCharacters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_"
// ECMA-119 7.4.2.2 defines d1-characters as
// "subject to agreement between the originator and the recipient of the volume".
d1Characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_!\"%&'()*+,-./:;<=>?"
)
const (
dirFlagHidden = 1 << iota
dirFlagDir
dirFlagAssociated
dirFlagRecord
dirFlagProtection
_
_
dirFlagMultiExtent
)
var standardIdentifierBytes = [5]byte{'C', 'D', '0', '0', '1'}
var ErrUDFNotSupported = errors.New("UDF volumes are not supported")
// volumeDescriptorHeader represents the data in bytes 0-6
// of a Volume Descriptor as defined in ECMA-119 8.1
type volumeDescriptorHeader struct {
Type byte
Identifier [5]byte
Version byte
}
var _ encoding.BinaryUnmarshaler = &volumeDescriptorHeader{}
var _ encoding.BinaryMarshaler = &volumeDescriptorHeader{}
// UnmarshalBinary decodes a volumeDescriptorHeader from binary form
func (vdh *volumeDescriptorHeader) UnmarshalBinary(data []byte) error {
if len(data) < 7 {
return io.ErrUnexpectedEOF
}
vdh.Type = data[0]
copy(vdh.Identifier[:], data[1:6])
vdh.Version = data[6]
return nil
}
func (vdh volumeDescriptorHeader) MarshalBinary() ([]byte, error) {
data := make([]byte, 7)
data[0] = vdh.Type
data[6] = vdh.Version
copy(data[1:6], vdh.Identifier[:])
return data, nil
}
// BootVolumeDescriptorBody represents the data in bytes 7-2047
// of a Boot Record as defined in ECMA-119 8.2
type BootVolumeDescriptorBody struct {
BootSystemIdentifier string
BootIdentifier string
BootSystemUse [1977]byte
}
var _ encoding.BinaryUnmarshaler = &BootVolumeDescriptorBody{}
// PrimaryVolumeDescriptorBody represents the data in bytes 7-2047
// of a Primary Volume Descriptor as defined in ECMA-119 8.4
type PrimaryVolumeDescriptorBody struct {
SystemIdentifier string
VolumeIdentifier string
VolumeSpaceSize int32
VolumeSetSize int16
VolumeSequenceNumber int16
LogicalBlockSize int16
PathTableSize int32
TypeLPathTableLoc int32
OptTypeLPathTableLoc int32
TypeMPathTableLoc int32
OptTypeMPathTableLoc int32
RootDirectoryEntry *DirectoryEntry
VolumeSetIdentifier string
PublisherIdentifier string
DataPreparerIdentifier string
ApplicationIdentifier string
CopyrightFileIdentifier string
AbstractFileIdentifier string
BibliographicFileIdentifier string
VolumeCreationDateAndTime VolumeDescriptorTimestamp
VolumeModificationDateAndTime VolumeDescriptorTimestamp
VolumeExpirationDateAndTime VolumeDescriptorTimestamp
VolumeEffectiveDateAndTime VolumeDescriptorTimestamp
FileStructureVersion byte
ApplicationUsed [512]byte
}
var _ encoding.BinaryUnmarshaler = &PrimaryVolumeDescriptorBody{}
var _ encoding.BinaryMarshaler = PrimaryVolumeDescriptorBody{}
// DirectoryEntry contains data from a Directory Descriptor
// as described by ECMA-119 9.1
type DirectoryEntry struct {
ExtendedAtributeRecordLength byte
ExtentLocation int32
ExtentLength uint32
RecordingDateTime RecordingTimestamp
FileFlags byte
FileUnitSize byte
InterleaveGap byte
VolumeSequenceNumber int16
Identifier string
SystemUse []byte
SystemUseEntries SystemUseEntrySlice
}
var _ encoding.BinaryUnmarshaler = &DirectoryEntry{}
var _ encoding.BinaryMarshaler = &DirectoryEntry{}
// UnmarshalBinary decodes a DirectoryEntry from binary form
func (de *DirectoryEntry) UnmarshalBinary(data []byte) error {
length := data[0]
if length == 0 {
return io.EOF
}
var err error
de.ExtendedAtributeRecordLength = data[1]
if de.ExtentLocation, err = UnmarshalInt32LSBMSB(data[2:10]); err != nil {
return err
}
if de.ExtentLength, err = UnmarshalUint32LSBMSB(data[10:18]); err != nil {
return err
}
if err = de.RecordingDateTime.UnmarshalBinary(data[18:25]); err != nil {
return err
}
de.FileFlags = data[25]
de.FileUnitSize = data[26]
de.InterleaveGap = data[27]
if de.VolumeSequenceNumber, err = UnmarshalInt16LSBMSB(data[28:32]); err != nil {
return err
}
identifierLen := data[32]
de.Identifier = string(data[33 : 33+identifierLen])
// add padding if identifier length was even]
idPaddingLen := (identifierLen + 1) % 2
de.SystemUse = data[33+identifierLen+idPaddingLen : length]
return nil
}
// MarshalBinary encodes a DirectoryEntry to binary form
func (de *DirectoryEntry) MarshalBinary() ([]byte, error) {
identifierLen := len(de.Identifier)
idPaddingLen := (identifierLen + 1) % 2
totalLen := 33 + identifierLen + idPaddingLen + len(de.SystemUse)
if totalLen > 255 {
return nil, fmt.Errorf("identifier %q is too long", de.Identifier)
}
data := make([]byte, totalLen)
data[0] = byte(totalLen)
data[1] = de.ExtendedAtributeRecordLength
WriteInt32LSBMSB(data[2:10], de.ExtentLocation)
WriteInt32LSBMSB(data[10:18], int32(de.ExtentLength))
de.RecordingDateTime.MarshalBinary(data[18:25])
data[25] = de.FileFlags
data[26] = de.FileUnitSize
data[27] = de.InterleaveGap
WriteInt16LSBMSB(data[28:32], de.VolumeSequenceNumber)
data[32] = byte(identifierLen)
copy(data[33:33+identifierLen], []byte(de.Identifier))
copy(data[33+identifierLen+idPaddingLen:totalLen], de.SystemUse)
return data, nil
}
// Clone creates a copy of the DirectoryEntry
func (de *DirectoryEntry) Clone() DirectoryEntry {
newDE := DirectoryEntry{
ExtendedAtributeRecordLength: de.ExtendedAtributeRecordLength,
ExtentLocation: de.ExtentLocation,
ExtentLength: de.ExtentLength,
RecordingDateTime: de.RecordingDateTime,
FileFlags: de.FileFlags,
FileUnitSize: de.FileUnitSize,
InterleaveGap: de.InterleaveGap,
VolumeSequenceNumber: de.VolumeSequenceNumber,
Identifier: de.Identifier,
SystemUse: make([]byte, len(de.SystemUse)),
}
copy(newDE.SystemUse, de.SystemUse)
return newDE
}
// UnmarshalBinary decodes a PrimaryVolumeDescriptorBody from binary form as defined in ECMA-119 8.4
func (pvd *PrimaryVolumeDescriptorBody) UnmarshalBinary(data []byte) error {
if len(data) < 2048 {
return io.ErrUnexpectedEOF
}
var err error
pvd.SystemIdentifier = strings.TrimRight(string(data[8:40]), " ")
pvd.VolumeIdentifier = strings.TrimRight(string(data[40:72]), " ")
if pvd.VolumeSpaceSize, err = UnmarshalInt32LSBMSB(data[80:88]); err != nil {
return err
}
if pvd.VolumeSetSize, err = UnmarshalInt16LSBMSB(data[120:124]); err != nil {
return err
}
if pvd.VolumeSequenceNumber, err = UnmarshalInt16LSBMSB(data[124:128]); err != nil {
return err
}
if pvd.LogicalBlockSize, err = UnmarshalInt16LSBMSB(data[128:132]); err != nil {
return err
}
if pvd.PathTableSize, err = UnmarshalInt32LSBMSB(data[132:140]); err != nil {
return err
}
pvd.TypeLPathTableLoc = int32(binary.LittleEndian.Uint32(data[140:144]))
pvd.OptTypeLPathTableLoc = int32(binary.LittleEndian.Uint32(data[144:148]))
pvd.TypeMPathTableLoc = int32(binary.BigEndian.Uint32(data[148:152]))
pvd.OptTypeMPathTableLoc = int32(binary.BigEndian.Uint32(data[152:156]))
if pvd.RootDirectoryEntry == nil {
pvd.RootDirectoryEntry = &DirectoryEntry{}
}
if err = pvd.RootDirectoryEntry.UnmarshalBinary(data[156:190]); err != nil {
return err
}
pvd.VolumeSetIdentifier = strings.TrimRight(string(data[190:318]), " ")
pvd.PublisherIdentifier = strings.TrimRight(string(data[318:446]), " ")
pvd.DataPreparerIdentifier = strings.TrimRight(string(data[446:574]), " ")
pvd.ApplicationIdentifier = strings.TrimRight(string(data[574:702]), " ")
pvd.CopyrightFileIdentifier = strings.TrimRight(string(data[702:740]), " ")
pvd.AbstractFileIdentifier = strings.TrimRight(string(data[740:776]), " ")
pvd.BibliographicFileIdentifier = strings.TrimRight(string(data[776:813]), " ")
if pvd.VolumeCreationDateAndTime.UnmarshalBinary(data[813:830]) != nil {
return err
}
if pvd.VolumeModificationDateAndTime.UnmarshalBinary(data[830:847]) != nil {
return err
}
if pvd.VolumeExpirationDateAndTime.UnmarshalBinary(data[847:864]) != nil {
return err
}
if pvd.VolumeEffectiveDateAndTime.UnmarshalBinary(data[864:881]) != nil {
return err
}
pvd.FileStructureVersion = data[881]
copy(pvd.ApplicationUsed[:], data[883:1395])
return nil
}
// MarshalBinary encodes the PrimaryVolumeDescriptorBody to its binary form
func (pvd PrimaryVolumeDescriptorBody) MarshalBinary() ([]byte, error) {
output := make([]byte, sectorSize)
d := MarshalString(pvd.SystemIdentifier, 32)
copy(output[8:40], d)
d = MarshalString(pvd.VolumeIdentifier, 32)
copy(output[40:72], d)
WriteInt32LSBMSB(output[80:88], pvd.VolumeSpaceSize)
WriteInt16LSBMSB(output[120:124], pvd.VolumeSetSize)
WriteInt16LSBMSB(output[124:128], pvd.VolumeSequenceNumber)
WriteInt16LSBMSB(output[128:132], pvd.LogicalBlockSize)
WriteInt32LSBMSB(output[132:140], pvd.PathTableSize)
binary.LittleEndian.PutUint32(output[140:144], uint32(pvd.TypeLPathTableLoc))
binary.LittleEndian.PutUint32(output[144:148], uint32(pvd.OptTypeLPathTableLoc))
binary.BigEndian.PutUint32(output[148:152], uint32(pvd.TypeMPathTableLoc))
binary.BigEndian.PutUint32(output[152:156], uint32(pvd.OptTypeMPathTableLoc))
binaryRDE, err := pvd.RootDirectoryEntry.MarshalBinary()
if err != nil {
return nil, err
}
copy(output[156:190], binaryRDE)
copy(output[190:318], MarshalString(pvd.VolumeSetIdentifier, 128))
copy(output[318:446], MarshalString(pvd.PublisherIdentifier, 128))
copy(output[446:574], MarshalString(pvd.DataPreparerIdentifier, 128))
copy(output[574:702], MarshalString(pvd.ApplicationIdentifier, 128))
copy(output[702:740], MarshalString(pvd.CopyrightFileIdentifier, 38))
copy(output[740:776], MarshalString(pvd.AbstractFileIdentifier, 36))
copy(output[776:813], MarshalString(pvd.BibliographicFileIdentifier, 37))
d, err = pvd.VolumeCreationDateAndTime.MarshalBinary()
if err != nil {
return nil, err
}
copy(output[813:830], d)
d, err = pvd.VolumeModificationDateAndTime.MarshalBinary()
if err != nil {
return nil, err
}
copy(output[830:847], d)
d, err = pvd.VolumeExpirationDateAndTime.MarshalBinary()
if err != nil {
return nil, err
}
copy(output[847:864], d)
d, err = pvd.VolumeEffectiveDateAndTime.MarshalBinary()
if err != nil {
return nil, err
}
copy(output[864:881], d)
output[881] = pvd.FileStructureVersion
output[882] = 0
copy(output[883:1395], pvd.ApplicationUsed[:])
for i := 1395; i < 2048; i++ {
output[i] = 0
}
return output, nil
}
// UnmarshalBinary decodes a BootVolumeDescriptorBody from binary form
func (bvd *BootVolumeDescriptorBody) UnmarshalBinary(data []byte) error {
bvd.BootSystemIdentifier = strings.TrimRight(string(data[7:39]), " ")
bvd.BootIdentifier = strings.TrimRight(string(data[39:71]), " ")
if n := copy(bvd.BootSystemUse[:], data[71:2048]); n != 1977 {
return fmt.Errorf("BootVolumeDescriptorBody.UnmarshalBinary: copied %d bytes", n)
}
return nil
}
type volumeDescriptor struct {
Header volumeDescriptorHeader
Boot *BootVolumeDescriptorBody
Primary *PrimaryVolumeDescriptorBody
}
var _ encoding.BinaryUnmarshaler = &volumeDescriptor{}
var _ encoding.BinaryMarshaler = &volumeDescriptor{}
func (vd volumeDescriptor) Type() byte {
return vd.Header.Type
}
// UnmarshalBinary decodes a volumeDescriptor from binary form
func (vd *volumeDescriptor) UnmarshalBinary(data []byte) error {
if uint32(len(data)) < sectorSize {
return io.ErrUnexpectedEOF
}
if err := vd.Header.UnmarshalBinary(data); err != nil {
// this should never fail, since volumeDescriptorHeader.UnmarshalBinary( ) only checks data size too
return err
}
id := string(vd.Header.Identifier[:])
if id != standardIdentifier {
if id == udfIdentifier {
return ErrUDFNotSupported
}
return fmt.Errorf("volume descriptor %q != %q", id, standardIdentifier)
}
switch vd.Header.Type {
case volumeTypeBoot:
vd.Boot = &BootVolumeDescriptorBody{}
return vd.Boot.UnmarshalBinary(data)
case volumeTypePartition:
return errors.New("partition volumes are not yet supported")
case volumeTypePrimary, volumeTypeSupplementary:
vd.Primary = &PrimaryVolumeDescriptorBody{}
return vd.Primary.UnmarshalBinary(data)
case volumeTypeTerminator:
return nil
}
return fmt.Errorf("unknown volume type 0x%X", vd.Header.Type)
}
// UnmarshalBinary decodes a volumeDescriptor from binary form
func (vd volumeDescriptor) MarshalBinary() ([]byte, error) {
var output []byte
var err error
switch vd.Header.Type {
case volumeTypeBoot:
return nil, errors.New("boot volumes are not yet supported")
case volumeTypePartition:
return nil, errors.New("partition volumes are not yet supported")
case volumeTypePrimary, volumeTypeSupplementary:
if output, err = vd.Primary.MarshalBinary(); err != nil {
return nil, err
}
case volumeTypeTerminator:
output = make([]byte, sectorSize)
}
data, err := vd.Header.MarshalBinary()
if err != nil {
return nil, err
}
copy(output[0:7], data)
return output, nil
}
// VolumeDescriptorTimestamp represents a time and date format
// that can be encoded according to ECMA-119 8.4.26.1
type VolumeDescriptorTimestamp struct {
Year int
Month int
Day int
Hour int
Minute int
Second int
Hundredth int
Offset int
}
var _ encoding.BinaryMarshaler = &VolumeDescriptorTimestamp{}
var _ encoding.BinaryUnmarshaler = &VolumeDescriptorTimestamp{}
// MarshalBinary encodes the timestamp into a binary form
func (ts *VolumeDescriptorTimestamp) MarshalBinary() ([]byte, error) {
formatted := fmt.Sprintf("%04d%02d%02d%02d%02d%02d%02d", ts.Year, ts.Month, ts.Day, ts.Hour, ts.Minute, ts.Second, ts.Hundredth)
formattedBytes := append([]byte(formatted), byte(ts.Offset))
if len(formattedBytes) != 17 {
return nil, fmt.Errorf("VolumeDescriptorTimestamp.MarshalBinary: the formatted timestamp is %d bytes long", len(formatted))
}
return formattedBytes, nil
}
// UnmarshalBinary decodes a VolumeDescriptorTimestamp from binary form
func (ts *VolumeDescriptorTimestamp) UnmarshalBinary(data []byte) error {
if len(data) < 17 {
return io.ErrUnexpectedEOF
}
year, err := strconv.Atoi(strings.TrimSpace(string(data[0:4])))
if err != nil {
return err
}
month, err := strconv.Atoi(strings.TrimSpace(string(data[4:6])))
if err != nil {
return err
}
day, err := strconv.Atoi(strings.TrimSpace(string(data[6:8])))
if err != nil {
return err
}
hour, err := strconv.Atoi(strings.TrimSpace(string(data[8:10])))
if err != nil {
return err
}
min, err := strconv.Atoi(strings.TrimSpace(string(data[10:12])))
if err != nil {
return err
}
sec, err := strconv.Atoi(strings.TrimSpace(string(data[12:14])))
if err != nil {
return err
}
hundredth, err := strconv.Atoi(strings.TrimSpace(string(data[14:16])))
if err != nil {
return err
}
*ts = VolumeDescriptorTimestamp{
Year: year,
Month: month,
Day: day,
Hour: hour,
Minute: min,
Second: sec,
Hundredth: hundredth,
Offset: int(data[16]),
}
return nil
}
// RecordingTimestamp represents a time and date format
// that can be encoded according to ECMA-119 9.1.5
type RecordingTimestamp time.Time
var _ encoding.BinaryUnmarshaler = &RecordingTimestamp{}
// UnmarshalBinary decodes a RecordingTimestamp from binary form
func (ts *RecordingTimestamp) UnmarshalBinary(data []byte) error {
if len(data) < 7 {
return io.ErrUnexpectedEOF
}
year := 1900 + int(data[0])
month := int(data[1])
day := int(data[2])
hour := int(data[3])
min := int(data[4])
sec := int(data[5])
tzOffset := int(data[6])
secondsInAQuarter := 60 * 15
tz := time.FixedZone("", tzOffset*secondsInAQuarter)
*ts = RecordingTimestamp(time.Date(year, time.Month(month), day, hour, min, sec, 0, tz))
return nil
}
// MarshalBinary encodes the RecordingTimestamp in its binary form to a buffer
// of the length of 7 or more bytes
func (ts RecordingTimestamp) MarshalBinary(dst []byte) {
_ = dst[6] // early bounds check to guarantee safety of writes below
t := time.Time(ts)
year, month, day := t.Date()
hour, min, sec := t.Clock()
_, secOffset := t.Zone()
secondsInAQuarter := 60 * 15
offsetInQuarters := secOffset / secondsInAQuarter
dst[0] = byte(year - 1900)
dst[1] = byte(month)
dst[2] = byte(day)
dst[3] = byte(hour)
dst[4] = byte(min)
dst[5] = byte(sec)
dst[6] = byte(offsetInQuarters)
}
// VolumeDescriptorTimestampFromTime converts time.Time to VolumeDescriptorTimestamp
func VolumeDescriptorTimestampFromTime(t time.Time) VolumeDescriptorTimestamp {
t = t.UTC()
year, month, day := t.Date()
hour, minute, second := t.Clock()
hundredth := t.Nanosecond() / 10000000
return VolumeDescriptorTimestamp{
Year: year,
Month: int(month),
Day: day,
Hour: hour,
Minute: minute,
Second: second,
Hundredth: hundredth,
Offset: 0, // we converted to UTC
}
}
+70
View File
@@ -0,0 +1,70 @@
package iso9660
import (
"encoding/binary"
"fmt"
"io"
"strings"
)
// MarshalString encodes the given string as a byte array padded to the given length
func MarshalString(s string, padToLength int) []byte {
if len(s) > padToLength {
s = s[:padToLength]
}
missingPadding := padToLength - len(s)
s = s + strings.Repeat(" ", missingPadding)
return []byte(s)
}
// UnmarshalInt32LSBMSB decodes a 32-bit integer in both byte orders, as defined in ECMA-119 7.3.3
func UnmarshalInt32LSBMSB(data []byte) (int32, error) {
if len(data) < 8 {
return 0, io.ErrUnexpectedEOF
}
lsb := int32(binary.LittleEndian.Uint32(data[0:4]))
msb := int32(binary.BigEndian.Uint32(data[4:8]))
if lsb != msb {
return 0, fmt.Errorf("little-endian and big-endian value mismatch: %d != %d", lsb, msb)
}
return lsb, nil
}
// UnmarshalUint32LSBMSB is the same as UnmarshalInt32LSBMSB but returns an unsigned integer
func UnmarshalUint32LSBMSB(data []byte) (uint32, error) {
n, err := UnmarshalInt32LSBMSB(data)
return uint32(n), err
}
// UnmarshalInt16LSBMSB decodes a 16-bit integer in both byte orders, as defined in ECMA-119 7.3.3
func UnmarshalInt16LSBMSB(data []byte) (int16, error) {
if len(data) < 4 {
return 0, io.ErrUnexpectedEOF
}
lsb := int16(binary.LittleEndian.Uint16(data[0:2]))
msb := int16(binary.BigEndian.Uint16(data[2:4]))
if lsb != msb {
return 0, fmt.Errorf("little-endian and big-endian value mismatch: %d != %d", lsb, msb)
}
return lsb, nil
}
// WriteInt32LSBMSB writes a 32-bit integer in both byte orders, as defined in ECMA-119 7.3.3
func WriteInt32LSBMSB(dst []byte, value int32) {
_ = dst[7] // early bounds check to guarantee safety of writes below
binary.LittleEndian.PutUint32(dst[0:4], uint32(value))
binary.BigEndian.PutUint32(dst[4:8], uint32(value))
}
// WriteInt16LSBMSB writes a 16-bit integer in both byte orders, as defined in ECMA-119 7.2.3
func WriteInt16LSBMSB(dst []byte, value int16) {
_ = dst[3] // early bounds check to guarantee safety of writes below
binary.LittleEndian.PutUint16(dst[0:2], uint16(value))
binary.BigEndian.PutUint16(dst[2:4], uint16(value))
}
+99
View File
@@ -0,0 +1,99 @@
package iso9660
import (
"fmt"
"io/fs"
"os"
)
/* The following types of Rock Ridge records are being handled in some way:
* - [X] PX (RR 4.1.1: POSIX file attributes)
* - [ ] PN (RR 4.1.2: POSIX device number)
* - [ ] SL (RR 4.1.3: symbolic link)
* - [x] NM (RR 4.1.4: alternate name)
* - [ ] CL (RR 4.1.5.1: child link)
* - [ ] PL (RR 4.1.5.2: parent link)
* - [ ] RE (RR 4.1.5.3: relocated directory)
* - [ ] TF (RR 4.1.6: time stamp(s) for a file)
* - [ ] SF (RR 4.1.7: file data in sparse file format)
*/
const (
RockRidgeIdentifier = "RRIP_1991A"
RockRidgeVersion = 1
)
type RockRidgeNameEntry struct {
Flags byte
Name string
}
func suspHasRockRidge(se SystemUseEntrySlice) (bool, error) {
extensions, err := se.GetExtensionRecords()
if err != nil {
return false, err
}
for _, entry := range extensions {
if entry.Identifier == RockRidgeIdentifier && entry.Version == RockRidgeVersion {
return true, nil
}
}
return false, nil
}
func (s SystemUseEntrySlice) GetRockRidgeName() string {
var name string
for _, entry := range s {
// There is a continuation flag in the record, but we determine continuation
// by simply reading all NM entries.
if entry.Type() == "NM" {
nm := umarshalRockRidgeNameEntry(entry)
name += nm.Name
}
}
return name
}
func (s SystemUseEntrySlice) GetPosixAttr() (fs.FileMode, error) {
for _, entry := range s {
if entry.Type() == "PX" {
// BUG(kdomanski): If there are multiple RR PX entries (which is forbidden by the spec), the reader will use the first one.
return umarshalRockRidgeAttrEntry(entry)
}
}
return 0, fmt.Errorf("mandatory entry PX not found")
}
func umarshalRockRidgeAttrEntry(e SystemUseEntry) (fs.FileMode, error) {
rrMode, err := UnmarshalUint32LSBMSB(e.Data()[0:8])
if err != nil {
return 0, fmt.Errorf("unmarshall RR PX entry: %w", err)
}
S_IFLNK := (rrMode & 0170000) == 0120000
S_IFDIR := (rrMode & 0170000) == 0040000
mode := rrMode & uint32(fs.ModePerm) // UNIX permissions
if S_IFLNK {
mode |= uint32(os.ModeSymlink)
}
if S_IFDIR {
mode |= uint32(os.ModeDir)
}
return fs.FileMode(mode), nil
}
func umarshalRockRidgeNameEntry(e SystemUseEntry) *RockRidgeNameEntry {
return &RockRidgeNameEntry{
Flags: e.Data()[0],
Name: string(e.Data()[1:]),
}
}
+211
View File
@@ -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,
}
}
+14
View File
@@ -0,0 +1,14 @@
# Binaries for programs and plugins
*.exe
*.dll
*.so
*.dylib
# Test binary, build with `go test -c`
*.test
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
# Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736
.glide/
+29
View File
@@ -0,0 +1,29 @@
BSD 3-Clause License
Copyright (c) 2017, Vladimir Jigulin
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+37
View File
@@ -0,0 +1,37 @@
## Udf filesystem golang library
- Non-optimized
- Some functioal is broken
- `recovery()` style error handling interface
- Work only with certain iso's
It's all because I has reached requried functional for me.
## Example
```go
package main
import (
"fmt"
"os"
"github.com/mogaika/udf"
)
func main() {
r, _ := os.Open("example.iso")
u := udf.NewUdfFromReader(r)
for _, f := range u.ReadDir(nil) {
fmt.Printf("%s %-10d %-20s %v\n", f.Mode().String(), f.Size(), f.Name(), f.ModTime())
}
}
```
Output:
```
-r-xr-xr-x 57 system.cnf 2006-02-11 00:00:00 +0000 UTC
-r-xr-xr-x 1911580 SCUS_973.99 2006-03-15 00:00:00 +0000 UTC
-r-xr-xr-x 278305 ioprp300.img 2005-11-14 00:00:00 +0000 UTC
-r-xr-xr-x 6641 sio2man.irx 2005-10-18 00:00:00 +0000 UTC
-r-xr-xr-x 15653 dbcman.irx 2005-10-18 00:00:00 +0000 UTC
```
+99
View File
@@ -0,0 +1,99 @@
package udf
import (
"encoding/binary"
"time"
"golang.org/x/text/encoding/charmap"
"golang.org/x/text/encoding/unicode"
"golang.org/x/text/transform"
)
func r_u8(b []byte) uint8 {
return b[0]
}
func r_i8(b []byte) int8 {
return int8(r_u8(b))
}
var rl_u64 = binary.LittleEndian.Uint64
func rl_u48(b []byte) uint64 {
var buf [8]byte
copy(buf[:], b[:6])
return rl_u64(buf[:])
}
var rl_u32 = binary.LittleEndian.Uint32
var rl_u16 = binary.LittleEndian.Uint16
func rl_i64(b []byte) int64 {
return int64(rl_u64(b))
}
func rl_i32(b []byte) int32 {
return int32(rl_u32(b))
}
func rl_i16(b []byte) int16 {
return int16(rl_u16(b))
}
var rb_u64 = binary.BigEndian.Uint64
var rb_u32 = binary.BigEndian.Uint32
var rb_u16 = binary.BigEndian.Uint16
func rb_u8(b []byte) uint8 {
return b[0]
}
func rb_i64(b []byte) int64 {
return int64(rb_u64(b))
}
func rb_i32(b []byte) int32 {
return int32(rb_u32(b))
}
func rb_i16(b []byte) int16 {
return int16(rb_u16(b))
}
func r_dstring(b []byte, fieldlen int) string {
if fieldlen == 0 {
return ""
}
return string(b[:b[fieldlen-1]])
}
func r_dcharacters(b []byte) string {
if len(b) == 0 {
return ""
}
switch b[0] {
case 8:
s, _, err := transform.Bytes(charmap.Windows1252.NewDecoder(), b[1:])
if err != nil {
panic(err)
}
return string(s)
case 16:
s, _, err := transform.Bytes(unicode.UTF16(unicode.BigEndian, unicode.IgnoreBOM).NewDecoder(), b[1:])
if err != nil {
panic(err)
}
return string(s)
default:
return ""
}
}
func r_timestamp(b []byte) time.Time {
var t time.Time
t = t.AddDate(int(rl_u16(b[2:])), int(b[4]), int(b[5]))
t.Add(time.Duration(b[6])*time.Hour +
time.Duration(b[7])*time.Minute +
time.Duration(b[8])*time.Second)
return t
}
+368
View File
@@ -0,0 +1,368 @@
package udf
import (
"time"
)
const (
DESCRIPTOR_PRIMARY_VOLUME = 0x1
DESCRIPTOR_ANCHOR_VOLUME_POINTER = 0x2
DESCRIPTOR_VOLUME_POINTER = 0x3
DESCRIPTOR_IMPLEMENTATION_USE_VOLUME = 0x4
DESCRIPTOR_PARTITION = 0x5
DESCRIPTOR_LOGICAL_VOLUME = 0x6
DESCRIPTOR_UNALLOCATED = 0x7
DESCRIPTOR_TERMINATING = 0x8
DESCRIPTOR_FILE_SET = 0x100
DESCRIPTOR_IDENTIFIER = 0x101
DESCRIPTOR_ALLOCATION_EXTENT = 0x102
DESCRIPTOR_INDIRECT_ENTRY = 0x103
DESCRIPTOR_TERMINAL_ENTRY = 0x104
DESCRIPTOR_FILE_ENTRY = 0x105
)
type Descriptor struct {
TagIdentifier uint16
DescriptorVersion uint16
TagChecksum uint8
TagSerialNumber uint16
DescriptorCRC uint16
DescriptorCRCLength uint16
TagLocation uint32
data []byte
}
func (d *Descriptor) Data() []byte {
buf := make([]byte, len(d.data))
copy(buf, d.data[16:])
return buf
}
func (d *Descriptor) FromBytes(b []byte) *Descriptor {
d.TagIdentifier = rl_u16(b[0:])
d.DescriptorVersion = rl_u16(b[2:])
d.TagChecksum = r_u8(b[3:])
d.TagSerialNumber = rl_u16(b[6:])
d.DescriptorCRC = rl_u16(b[8:])
d.DescriptorCRCLength = rl_u16(b[10:])
d.TagLocation = rl_u32(b[12:])
d.data = b[:]
return d
}
func NewDescriptor(b []byte) *Descriptor {
return new(Descriptor).FromBytes(b)
}
type AnchorVolumeDescriptorPointer struct {
Descriptor Descriptor
MainVolumeDescriptorSeq Extent
ReserveVolumeDescriptorSeq Extent
}
func (ad *AnchorVolumeDescriptorPointer) FromBytes(b []byte) *AnchorVolumeDescriptorPointer {
ad.Descriptor.FromBytes(b)
ad.MainVolumeDescriptorSeq = NewExtent(b[16:])
ad.ReserveVolumeDescriptorSeq = NewExtent(b[24:])
return ad
}
func NewAnchorVolumeDescriptorPointer(b []byte) *AnchorVolumeDescriptorPointer {
return new(AnchorVolumeDescriptorPointer).FromBytes(b)
}
func (d *Descriptor) AnchorVolumeDescriptorPointer() *AnchorVolumeDescriptorPointer {
return NewAnchorVolumeDescriptorPointer(d.data)
}
type PrimaryVolumeDescriptor struct {
Descriptor Descriptor
VolumeDescriptorSequenceNumber uint32
PrimaryVolumeDescriptorNumber uint32
VolumeIdentifier string
VolumeSequenceNumber uint16
MaximumVolumeSequenceNumber uint16
InterchangeLevel uint16
MaximumInterchangeLevel uint16
CharacterSetList uint32
MaximumCharacterSetList uint32
VolumeSetIdentifier string
VolumeAbstract Extent
VolumeCopyrightNoticeExtent Extent
ApplicationIdentifier EntityID
RecordingDateTime time.Time
ImplementationIdentifier EntityID
ImplementationUse []byte
PredecessorVolumeDescriptorSequenceLocation uint32
Flags uint16
}
func (pvd *PrimaryVolumeDescriptor) FromBytes(b []byte) *PrimaryVolumeDescriptor {
pvd.Descriptor.FromBytes(b)
pvd.VolumeDescriptorSequenceNumber = rl_u32(b[16:])
pvd.PrimaryVolumeDescriptorNumber = rl_u32(b[20:])
pvd.VolumeIdentifier = r_dstring(b[24:], 32)
pvd.VolumeSequenceNumber = rl_u16(b[56:])
pvd.MaximumVolumeSequenceNumber = rl_u16(b[58:])
pvd.InterchangeLevel = rl_u16(b[60:])
pvd.MaximumInterchangeLevel = rl_u16(b[62:])
pvd.CharacterSetList = rl_u32(b[64:])
pvd.MaximumCharacterSetList = rl_u32(b[68:])
pvd.VolumeSetIdentifier = r_dstring(b[72:], 128)
pvd.VolumeAbstract = NewExtent(b[328:])
pvd.VolumeCopyrightNoticeExtent = NewExtent(b[336:])
pvd.ApplicationIdentifier = NewEntityID(b[344:])
pvd.RecordingDateTime = r_timestamp(b[376:])
pvd.ImplementationIdentifier = NewEntityID(b[388:])
pvd.ImplementationUse = b[420:484]
pvd.PredecessorVolumeDescriptorSequenceLocation = rl_u32(b[484:])
pvd.Flags = rl_u16(b[488:])
return pvd
}
func NewPrimaryVolumeDescriptor(b []byte) *PrimaryVolumeDescriptor {
return new(PrimaryVolumeDescriptor).FromBytes(b)
}
func (d *Descriptor) PrimaryVolumeDescriptor() *PrimaryVolumeDescriptor {
return NewPrimaryVolumeDescriptor(d.data)
}
type PartitionDescriptor struct {
Descriptor Descriptor
VolumeDescriptorSequenceNumber uint32
PartitionFlags uint16
PartitionNumber uint16
PartitionContents EntityID
PartitionContentsUse []byte
AccessType uint32
PartitionStartingLocation uint32
PartitionLength uint32
ImplementationIdentifier EntityID
ImplementationUse []byte
}
func (pd *PartitionDescriptor) FromBytes(b []byte) *PartitionDescriptor {
pd.Descriptor.FromBytes(b)
pd.VolumeDescriptorSequenceNumber = rl_u32(b[16:])
pd.PartitionFlags = rl_u16(b[20:])
pd.PartitionNumber = rl_u16(b[22:])
pd.PartitionContents = NewEntityID(b[24:])
pd.PartitionContentsUse = b[56:184]
pd.AccessType = rl_u32(b[184:])
pd.PartitionStartingLocation = rl_u32(b[188:])
pd.PartitionLength = rl_u32(b[192:])
pd.ImplementationIdentifier = NewEntityID(b[196:])
pd.ImplementationUse = b[228:356]
return pd
}
func NewPartitionDescriptor(b []byte) *PartitionDescriptor {
return new(PartitionDescriptor).FromBytes(b)
}
func (d *Descriptor) PartitionDescriptor() *PartitionDescriptor {
return NewPartitionDescriptor(d.data)
}
type PartitionMap struct {
PartitionMapType uint8
PartitionMapLength uint8
VolumeSequenceNumber uint16
PartitionNumber uint16
}
func (pm *PartitionMap) FromBytes(b []byte) *PartitionMap {
pm.PartitionMapType = rb_u8(b[0:])
pm.PartitionMapLength = rb_u8(b[1:])
pm.VolumeSequenceNumber = rb_u16(b[2:])
pm.PartitionNumber = rb_u16(b[4:])
return pm
}
type LogicalVolumeDescriptor struct {
Descriptor Descriptor
VolumeDescriptorSequenceNumber uint32
LogicalVolumeIdentifier string
LogicalBlockSize uint32
DomainIdentifier EntityID
LogicalVolumeContentsUse ExtentLong
MapTableLength uint32
NumberOfPartitionMaps uint32
ImplementationIdentifier EntityID
ImplementationUse []byte
IntegritySequenceExtent Extent
PartitionMaps []PartitionMap
}
func (lvd *LogicalVolumeDescriptor) FromBytes(b []byte) *LogicalVolumeDescriptor {
lvd.Descriptor.FromBytes(b)
lvd.VolumeDescriptorSequenceNumber = rl_u32(b[16:])
lvd.LogicalVolumeIdentifier = r_dstring(b[84:], 128)
lvd.LogicalBlockSize = rl_u32(b[212:])
lvd.DomainIdentifier = NewEntityID(b[216:])
lvd.LogicalVolumeContentsUse = NewExtentLong(b[248:])
lvd.MapTableLength = rl_u32(b[264:])
lvd.NumberOfPartitionMaps = rl_u32(b[268:])
lvd.ImplementationIdentifier = NewEntityID(b[272:])
lvd.ImplementationUse = b[304:432]
lvd.IntegritySequenceExtent = NewExtent(b[432:])
lvd.PartitionMaps = make([]PartitionMap, lvd.NumberOfPartitionMaps)
for i := range lvd.PartitionMaps {
lvd.PartitionMaps[i].FromBytes(b[440+i*6:])
}
return lvd
}
func NewLogicalVolumeDescriptor(b []byte) *LogicalVolumeDescriptor {
return new(LogicalVolumeDescriptor).FromBytes(b)
}
func (d *Descriptor) LogicalVolumeDescriptor() *LogicalVolumeDescriptor {
return NewLogicalVolumeDescriptor(d.data)
}
type FileSetDescriptor struct {
Descriptor Descriptor
RecordingDateTime time.Time
InterchangeLevel uint16
MaximumInterchangeLevel uint16
CharacterSetList uint32
MaximumCharacterSetList uint32
FileSetNumber uint32
FileSetDescriptorNumber uint32
LogicalVolumeIdentifier string
FileSetIdentifier string
CopyrightFileIdentifier string
AbstractFileIdentifier string
RootDirectoryICB ExtentLong
DomainIdentifier EntityID
NexExtent ExtentLong
}
func (fsd *FileSetDescriptor) FromBytes(b []byte) *FileSetDescriptor {
fsd.Descriptor.FromBytes(b)
fsd.RecordingDateTime = r_timestamp(b[16:])
fsd.InterchangeLevel = rl_u16(b[28:])
fsd.MaximumInterchangeLevel = rl_u16(b[30:])
fsd.CharacterSetList = rl_u32(b[32:])
fsd.MaximumCharacterSetList = rl_u32(b[36:])
fsd.FileSetNumber = rl_u32(b[40:])
fsd.FileSetDescriptorNumber = rl_u32(b[44:])
fsd.LogicalVolumeIdentifier = r_dstring(b[112:], 128)
fsd.FileSetIdentifier = r_dstring(b[304:], 32)
fsd.CopyrightFileIdentifier = r_dstring(b[336:], 32)
fsd.AbstractFileIdentifier = r_dstring(b[368:], 32)
fsd.RootDirectoryICB = NewExtentLong(b[400:])
fsd.DomainIdentifier = NewEntityID(b[416:])
fsd.NexExtent = NewExtentLong(b[448:])
return fsd
}
func NewFileSetDescriptor(b []byte) *FileSetDescriptor {
return new(FileSetDescriptor).FromBytes(b)
}
func (d *Descriptor) FileSetDescriptor() *FileSetDescriptor {
return NewFileSetDescriptor(d.data)
}
type FileIdentifierDescriptor struct {
Descriptor Descriptor
FileVersionNumber uint16
FileCharacteristics uint8
LengthOfFileIdentifier uint8
ICB ExtentLong
LengthOfImplementationUse uint16
ImplementationUse EntityID
FileIdentifier string
}
func (fid *FileIdentifierDescriptor) Len() uint64 {
l := 38 + uint64(fid.LengthOfImplementationUse) + uint64(fid.LengthOfFileIdentifier)
return 4 * ((l + 3) / 4) // padding = 4
}
func (fid *FileIdentifierDescriptor) FromBytes(b []byte) *FileIdentifierDescriptor {
fid.Descriptor.FromBytes(b)
fid.FileVersionNumber = rl_u16(b[16:])
fid.FileCharacteristics = r_u8(b[18:])
fid.LengthOfFileIdentifier = r_u8(b[19:])
fid.ICB = NewExtentLong(b[20:])
fid.LengthOfImplementationUse = rl_u16(b[36:])
fid.ImplementationUse = NewEntityID(b[38:])
identStart := 38 + fid.LengthOfImplementationUse
fid.FileIdentifier = r_dcharacters(b[identStart : fid.LengthOfFileIdentifier+uint8(identStart)])
return fid
}
func NewFileIdentifierDescriptor(b []byte) *FileIdentifierDescriptor {
return new(FileIdentifierDescriptor).FromBytes(b)
}
func (d *Descriptor) FileIdentifierDescriptor() *FileIdentifierDescriptor {
return NewFileIdentifierDescriptor(d.data)
}
type FileEntry struct {
Descriptor Descriptor
ICBTag *ICBTag
Uid uint32
Gid uint32
Permissions uint32
FileLinkCount uint16
RecordFormat uint8
RecordDisplayAttributes uint8
RecordLength uint32
InformationLength uint64
LogicalBlocksRecorded uint64
AccessTime time.Time
ModificationTime time.Time
AttributeTime time.Time
Checkpoint uint32
ExtendedAttributeICB ExtentLong
ImplementationIdentifier EntityID
UniqueId uint64
LengthOfExtendedAttributes uint32
LengthOfAllocationDescriptors uint32
ExtendedAttributes []byte
AllocationDescriptors []Extent
}
func (fe *FileEntry) FromBytes(b []byte) *FileEntry {
fe.Descriptor.FromBytes(b)
fe.ICBTag = NewICBTag(b[16:])
fe.Uid = rl_u32(b[36:])
fe.Gid = rl_u32(b[40:])
fe.Permissions = rl_u32(b[44:])
fe.FileLinkCount = rl_u16(b[48:])
fe.RecordFormat = r_u8(b[50:])
fe.RecordDisplayAttributes = r_u8(b[51:])
fe.RecordLength = rl_u32(b[52:])
fe.InformationLength = rl_u64(b[56:])
fe.LogicalBlocksRecorded = rl_u64(b[64:])
fe.AccessTime = r_timestamp(b[72:])
fe.ModificationTime = r_timestamp(b[84:])
fe.AttributeTime = r_timestamp(b[96:])
fe.Checkpoint = rl_u32(b[108:])
fe.ExtendedAttributeICB = NewExtentLong(b[112:])
fe.ImplementationIdentifier = NewEntityID(b[128:])
fe.UniqueId = rl_u64(b[160:])
fe.LengthOfExtendedAttributes = rl_u32(b[168:])
fe.LengthOfAllocationDescriptors = rl_u32(b[172:])
allocDescStart := 176 + fe.LengthOfExtendedAttributes
fe.ExtendedAttributes = b[176:allocDescStart]
fe.AllocationDescriptors = make([]Extent, fe.LengthOfAllocationDescriptors/8)
for i := range fe.AllocationDescriptors {
fe.AllocationDescriptors[i] = NewExtent(b[allocDescStart+uint32(i)*8:])
}
return fe
}
func NewFileEntry(b []byte) *FileEntry {
return new(FileEntry).FromBytes(b)
}
func (d *Descriptor) FileEntry() *FileEntry {
return NewFileEntry(d.data)
}
+14
View File
@@ -0,0 +1,14 @@
package udf
type EntityID struct {
Flags uint8
Identifier [23]byte
IdentifierSuffix [8]byte
}
func NewEntityID(b []byte) EntityID {
e := EntityID{Flags: b[0]}
copy(e.Identifier[:], b[1:24])
copy(e.IdentifierSuffix[:], b[24:32])
return e
}
+37
View File
@@ -0,0 +1,37 @@
package udf
type Extent struct {
Length uint32
Location uint32
}
func NewExtent(b []byte) Extent {
return Extent{
Length: rl_u32(b[0:]),
Location: rl_u32(b[4:]),
}
}
type ExtentSmall struct {
Length uint16
Location uint64
}
func NewExtentSmall(b []byte) ExtentSmall {
return ExtentSmall{
Length: rl_u16(b[0:]),
Location: rl_u48(b[2:]),
}
}
type ExtentLong struct {
Length uint32
Location uint64
}
func NewExtentLong(b []byte) ExtentLong {
return ExtentLong{
Length: rl_u32(b[0:]),
Location: rl_u48(b[4:]),
}
}
+74
View File
@@ -0,0 +1,74 @@
package udf
import (
"io"
"os"
"time"
)
type File struct {
Udf *Udf
Fid *FileIdentifierDescriptor
fe *FileEntry
fileEntryPosition uint64
}
func (f *File) GetFileEntryPosition() int64 {
return int64(f.fileEntryPosition)
}
func (f *File) GetFileOffset() int64 {
return SECTOR_SIZE * (int64(f.FileEntry().AllocationDescriptors[0].Location) + int64(f.Udf.PartitionStart()))
}
func (f *File) FileEntry() *FileEntry {
if f.fe == nil {
f.fileEntryPosition = f.Fid.ICB.Location
f.fe = NewFileEntry(f.Udf.ReadSector(f.Udf.PartitionStart() + f.fileEntryPosition))
}
return f.fe
}
func (f *File) NewReader() *io.SectionReader {
return io.NewSectionReader(f.Udf.r, f.GetFileOffset(), f.Size())
}
func (f *File) Name() string {
return f.Fid.FileIdentifier
}
func (f *File) Mode() os.FileMode {
var mode os.FileMode
perms := os.FileMode(f.FileEntry().Permissions)
mode |= ((perms >> 0) & 7) << 0
mode |= ((perms >> 5) & 7) << 3
mode |= ((perms >> 10) & 7) << 6
if f.IsDir() {
mode |= os.ModeDir
}
return mode
}
func (f *File) Size() int64 {
return int64(f.FileEntry().InformationLength)
}
func (f *File) ModTime() time.Time {
return f.FileEntry().ModificationTime
}
func (f *File) IsDir() bool {
// TODO :Fix! This field always 0 :(
return f.FileEntry().ICBTag.FileType == 4
}
func (f *File) Sys() interface{} {
return f.Fid
}
func (f *File) ReadDir() []File {
return f.Udf.ReadDir(f.FileEntry())
}
+26
View File
@@ -0,0 +1,26 @@
package udf
type ICBTag struct {
PriorRecordedNumberOfDirectEntries uint32
StrategyType uint16
StrategyParameter uint16
MaximumNumberOfEntries uint16
FileType uint8
ParentICBLocation uint64
Flags uint16
}
func (itag *ICBTag) FromBytes(b []byte) *ICBTag {
itag.PriorRecordedNumberOfDirectEntries = rl_u32(b[0:])
itag.StrategyType = rl_u16(b[4:])
itag.StrategyParameter = rl_u16(b[4:])
itag.MaximumNumberOfEntries = rl_u16(b[8:])
itag.FileType = r_u8(b[1:])
itag.ParentICBLocation = rl_u48(b[12:])
itag.Flags = rl_u16(b[18:])
return itag
}
func NewICBTag(b []byte) *ICBTag {
return new(ICBTag).FromBytes(b)
}
+118
View File
@@ -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
}
+11 -1
View File
@@ -78,6 +78,10 @@ github.com/DataDog/zstd
# github.com/LeeEirc/terminalparser v0.0.0-20240205084113-fbf78c8480f2
## explicit; go 1.15
github.com/LeeEirc/terminalparser
# github.com/Microsoft/go-winio v0.6.2
## explicit; go 1.21
github.com/Microsoft/go-winio/wim
github.com/Microsoft/go-winio/wim/lzx
# github.com/RoaringBitmap/roaring v1.2.3
## explicit; go 1.14
github.com/RoaringBitmap/roaring
@@ -628,6 +632,9 @@ github.com/jtolds/gls
# github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0
## explicit
github.com/kardianos/osext
# github.com/kdomanski/iso9660 v0.4.0
## explicit; go 1.19
github.com/kdomanski/iso9660
# github.com/klauspost/cpuid/v2 v2.0.9
## explicit; go 1.13
github.com/klauspost/cpuid/v2
@@ -774,6 +781,9 @@ github.com/modern-go/concurrent
# github.com/modern-go/reflect2 v1.0.2
## explicit; go 1.12
github.com/modern-go/reflect2
# github.com/mogaika/udf v0.0.0-20171019171931-167f0ab01c73
## explicit
github.com/mogaika/udf
# github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826
## explicit
github.com/mohae/deepcopy
@@ -1687,7 +1697,7 @@ yunion.io/x/log/hooks
yunion.io/x/ovsdb/cli_util
yunion.io/x/ovsdb/schema/ovn_nb
yunion.io/x/ovsdb/types
# yunion.io/x/pkg v1.10.4-0.20251114095758-2a2f105d9712
# yunion.io/x/pkg v1.10.4-0.20260127060125-8939521ef75e
## explicit; go 1.18
yunion.io/x/pkg/appctx
yunion.io/x/pkg/errors
+62 -29
View File
@@ -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"},
// UbuntuServer版补充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 {