fix(glance): detect windows iso (#25340)

This commit is contained in:
屈轩
2026-08-14 19:02:12 +08:00
committed by GitHub
parent ea65b5d1a5
commit d84952bf77
12 changed files with 484 additions and 1724 deletions

4
go.mod
View File

@@ -6,7 +6,6 @@ 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 v1.57.0
github.com/aws/aws-sdk-go-v2 v1.41.5
@@ -113,7 +112,7 @@ require (
k8s.io/cri-api v0.28.15
k8s.io/klog/v2 v2.90.1
moul.io/http2curl/v2 v2.3.0
yunion.io/x/cloudmux v0.3.10-0-alpha.1.0.20260722023537-f41e01f2eee3
yunion.io/x/cloudmux v0.3.10-0-alpha.1.0.20260812062623-2b79dfea19bd
yunion.io/x/executor v0.0.0-20260312022053-f538abd2b005
yunion.io/x/jsonutils v1.0.1-0.20260715075349-615cfb44ff7c
yunion.io/x/log v1.0.1-0.20240305175729-7cf2d6cd5a91
@@ -141,6 +140,7 @@ require (
github.com/DataDog/datadog-go/v5 v5.0.2 // indirect
github.com/DataDog/go-tuf v0.3.0--fix-localmeta-fork // indirect
github.com/DataDog/sketches-go v1.2.1 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/Microsoft/hcsshim v0.11.4 // indirect
github.com/RoaringBitmap/roaring v1.2.3 // indirect
github.com/StackExchange/wmi v1.2.1 // indirect

4
go.sum
View File

@@ -1754,8 +1754,8 @@ sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o=
sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc=
sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo=
sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8=
yunion.io/x/cloudmux v0.3.10-0-alpha.1.0.20260722023537-f41e01f2eee3 h1:QBX44ByHjCYbstpxDejNPI+GqJmcsJIE8nTQCGfxUsw=
yunion.io/x/cloudmux v0.3.10-0-alpha.1.0.20260722023537-f41e01f2eee3/go.mod h1:QgrcekfkD3fELNYipZnlUfhR8dPI8CCUsuzC2SkP3YE=
yunion.io/x/cloudmux v0.3.10-0-alpha.1.0.20260812062623-2b79dfea19bd h1:rtRzbOZP24DqQ0u0SYfrSdYWLX6Taov0AwkVQyv95p0=
yunion.io/x/cloudmux v0.3.10-0-alpha.1.0.20260812062623-2b79dfea19bd/go.mod h1:QgrcekfkD3fELNYipZnlUfhR8dPI8CCUsuzC2SkP3YE=
yunion.io/x/executor v0.0.0-20260312022053-f538abd2b005 h1:3sWwcjGXGjG9mLBWa7AyLq+QSi0udTAx21pfVQRFMBE=
yunion.io/x/executor v0.0.0-20260312022053-f538abd2b005/go.mod h1:Uxuou9WQIeJXNpy7t2fPLL0BYLvLiMvGQwY7Qc6aSws=
yunion.io/x/jsonutils v0.0.0-20190625054549-a964e1e8a051/go.mod h1:4N0/RVzsYL3kH3WE/H1BjUQdFiWu50JGCFQuuy+Z634=

View File

@@ -186,8 +186,8 @@ func DetectOSFromISO(r io.Reader) (*ISOInfo, error) {
return nil, err
}
// ========== 识别Windows系列 ==========
if reader.FileExists("sources/install.wim") {
// ========== 识别Windows系列install.wim 或 install.esd ==========
if reader.FileExists("sources/install.wim") || reader.FileExists("sources/install.esd") {
return DetectWindowsEdition(reader)
}

View File

@@ -0,0 +1,211 @@
// 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 (
"encoding/binary"
"encoding/xml"
"fmt"
"io"
"unicode/utf16"
"yunion.io/x/log"
"yunion.io/x/pkg/util/imagetools"
)
var wimImageTag = [8]byte{'M', 'S', 'W', 'I', 'M', 0, 0, 0}
const (
wimResFlagCompressed = 0x04
)
// wimResourceDesc matches the on-disk WIM resource descriptor (24 bytes).
type wimResourceDesc struct {
FlagsAndCompressedSize uint64
Offset int64
OriginalSize int64
}
func (r wimResourceDesc) flags() byte {
return byte(r.FlagsAndCompressedSize >> 56)
}
func (r wimResourceDesc) compressedSize() int64 {
return int64(r.FlagsAndCompressedSize & 0xffffffffffffff)
}
// wimHeaderDisk is the on-disk WIM header (208 bytes / 0xd0).
type wimHeaderDisk struct {
ImageTag [8]byte
Size uint32
Version uint32
Flags uint32
CompressionSize uint32
WIMGuid [16]byte
PartNumber uint16
TotalParts uint16
ImageCount uint32
OffsetTable wimResourceDesc
XMLData wimResourceDesc
BootMetadata wimResourceDesc
BootIndex uint32
Padding uint32
Integrity wimResourceDesc
Unused [60]byte
}
type wimXMLInfo struct {
Image []wimXMLImage `xml:"IMAGE"`
}
type wimXMLImage struct {
Name string `xml:"NAME"`
Index int `xml:"INDEX,attr"`
Windows *wimWindowsInfo `xml:"WINDOWS"`
}
type wimWindowsInfo struct {
Arch byte `xml:"ARCH"`
ProductName string `xml:"PRODUCTNAME"`
EditionID string `xml:"EDITIONID"`
ProductType string `xml:"PRODUCTTYPE"`
DefaultLanguage string `xml:"LANGUAGES>DEFAULT"`
Version wimXMLVersion `xml:"VERSION"`
}
type wimXMLVersion struct {
Major int `xml:"MAJOR"`
Minor int `xml:"MINOR"`
Build int `xml:"BUILD"`
}
// parseWimXmlMetadata reads WIM/ESD header + uncompressed XML metadata and maps Windows version.
// Content compression (LZMS/XPRESS) is ignored; only the XML blob is required for edition detection.
func parseWimXmlMetadata(r io.ReaderAt) (*ISOInfo, error) {
var hdr wimHeaderDisk
if err := binary.Read(io.NewSectionReader(r, 0, int64(binary.Size(hdr))), binary.LittleEndian, &hdr); err != nil {
return nil, fmt.Errorf("read WIM header: %w", err)
}
if hdr.ImageTag != wimImageTag {
return nil, fmt.Errorf("not a WIM/ESD file")
}
if hdr.XMLData.compressedSize() == 0 || hdr.XMLData.OriginalSize == 0 {
return nil, fmt.Errorf("WIM/ESD has no XML metadata")
}
if hdr.XMLData.flags()&wimResFlagCompressed != 0 {
return nil, fmt.Errorf("compressed WIM XML metadata is not supported")
}
xmlBytes := make([]byte, hdr.XMLData.OriginalSize)
if _, err := r.ReadAt(xmlBytes, hdr.XMLData.Offset); err != nil {
return nil, fmt.Errorf("read WIM XML metadata: %w", err)
}
xmlStr, err := decodeWimUTF16XML(xmlBytes)
if err != nil {
return nil, err
}
var info wimXMLInfo
if err := xml.Unmarshal([]byte(xmlStr), &info); err != nil {
return nil, fmt.Errorf("parse WIM XML: %w", err)
}
for _, image := range info.Image {
if image.Windows == nil {
continue
}
result := mapWindowsVersion(image.Windows)
ver := fmt.Sprintf("%d.%d.%d", image.Windows.Version.Major, image.Windows.Version.Minor, image.Windows.Version.Build)
log.Debugf("识别到 %s 版本: %s -> %s", result.Distro, ver, result.Version)
return result, nil
}
return nil, fmt.Errorf("no WINDOWS metadata found in WIM/ESD XML")
}
func decodeWimUTF16XML(data []byte) (string, error) {
if len(data) < 2 || len(data)%2 != 0 {
return "", fmt.Errorf("invalid WIM XML encoding")
}
u16 := make([]uint16, len(data)/2)
for i := 0; i < len(u16); i++ {
u16[i] = binary.LittleEndian.Uint16(data[i*2:])
}
// BOM is little-endian UTF-16 (0xFEFF)
if u16[0] != 0xfeff {
return "", fmt.Errorf("invalid WIM XML BOM")
}
return string(utf16.Decode(u16[1:])), nil
}
func mapWindowsVersion(win *wimWindowsInfo) *ISOInfo {
result := &ISOInfo{
Distro: imagetools.OS_DIST_WINDOWS,
Language: win.DefaultLanguage,
}
switch win.Arch {
case 9:
result.Arch = "x86_64"
case 12:
result.Arch = "arm64"
case 0:
result.Arch = "x86"
}
majMin := fmt.Sprintf("%d.%d", win.Version.Major, win.Version.Minor)
switch majMin {
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 win.Version.Build >= 27500 {
result.Version = "Windows 12"
} else if win.Version.Build >= 22000 {
result.Version = "Windows 11"
} else {
result.Version = "Windows 10"
}
}
if win.ProductType == "ServerNT" {
result.Distro = imagetools.OS_DIST_WINDOWS_SERVER
switch majMin {
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 win.Version.Build >= 26040 {
result.Version = "Windows Server 2025"
} else if win.Version.Build >= 20348 {
result.Version = "Windows Server 2022"
} else if win.Version.Build >= 17763 {
result.Version = "Windows Server 2019"
} else if win.Version.Build >= 14393 {
result.Version = "Windows Server 2016"
}
}
}
return result
}

View File

@@ -0,0 +1,197 @@
// 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"
"encoding/binary"
"testing"
"unicode/utf16"
"yunion.io/x/pkg/util/imagetools"
)
func encodeWimUTF16XML(s string) []byte {
u16 := utf16.Encode([]rune(s))
out := make([]byte, 2+len(u16)*2)
binary.LittleEndian.PutUint16(out[0:], 0xfeff) // BOM
for i, v := range u16 {
binary.LittleEndian.PutUint16(out[2+i*2:], v)
}
return out
}
func buildMinimalWimWithXML(xmlASCII string) []byte {
xmlData := encodeWimUTF16XML(xmlASCII)
hdrSize := binary.Size(wimHeaderDisk{})
offset := int64(hdrSize)
var hdr wimHeaderDisk
hdr.ImageTag = wimImageTag
hdr.Size = uint32(hdrSize)
hdr.Version = 0x10d00
hdr.PartNumber = 1
hdr.TotalParts = 1
hdr.ImageCount = 1
// uncompressed XML resource: flags=0, compressed size = original size
hdr.XMLData = wimResourceDesc{
FlagsAndCompressedSize: uint64(len(xmlData)),
Offset: offset,
OriginalSize: int64(len(xmlData)),
}
buf := &bytes.Buffer{}
_ = binary.Write(buf, binary.LittleEndian, &hdr)
buf.Write(xmlData)
return buf.Bytes()
}
func TestParseWimXmlMetadataWindows11(t *testing.T) {
xml := `<?xml version="1.0"?>
<WIM>
<IMAGE INDEX="1">
<NAME>Windows 11 Pro</NAME>
<WINDOWS>
<ARCH>9</ARCH>
<PRODUCTNAME>Microsoft® Windows® Operating System</PRODUCTNAME>
<EDITIONID>Professional</EDITIONID>
<PRODUCTTYPE>WinNT</PRODUCTTYPE>
<LANGUAGES>
<LANGUAGE>zh-CN</LANGUAGE>
<DEFAULT>zh-CN</DEFAULT>
</LANGUAGES>
<VERSION>
<MAJOR>10</MAJOR>
<MINOR>0</MINOR>
<BUILD>22631</BUILD>
</VERSION>
</WINDOWS>
</IMAGE>
</WIM>`
data := buildMinimalWimWithXML(xml)
info, err := parseWimXmlMetadata(bytes.NewReader(data))
if err != nil {
t.Fatalf("parseWimXmlMetadata: %v", err)
}
if info.Distro != imagetools.OS_DIST_WINDOWS {
t.Fatalf("distro: got %s want %s", info.Distro, imagetools.OS_DIST_WINDOWS)
}
if info.Version != "Windows 11" {
t.Fatalf("version: got %s want Windows 11", info.Version)
}
if info.Arch != "x86_64" {
t.Fatalf("arch: got %s want x86_64", info.Arch)
}
if info.Language != "zh-CN" {
t.Fatalf("language: got %s want zh-CN", info.Language)
}
}
func TestParseWimXmlMetadataWindowsServer2022(t *testing.T) {
xml := `<?xml version="1.0"?>
<WIM>
<IMAGE INDEX="1">
<NAME>Windows Server 2022 SERVERSTANDARD</NAME>
<WINDOWS>
<ARCH>9</ARCH>
<PRODUCTTYPE>ServerNT</PRODUCTTYPE>
<LANGUAGES>
<DEFAULT>en-US</DEFAULT>
</LANGUAGES>
<VERSION>
<MAJOR>10</MAJOR>
<MINOR>0</MINOR>
<BUILD>20348</BUILD>
</VERSION>
</WINDOWS>
</IMAGE>
</WIM>`
data := buildMinimalWimWithXML(xml)
info, err := parseWimXmlMetadata(bytes.NewReader(data))
if err != nil {
t.Fatalf("parseWimXmlMetadata: %v", err)
}
if info.Distro != imagetools.OS_DIST_WINDOWS_SERVER {
t.Fatalf("distro: got %s want %s", info.Distro, imagetools.OS_DIST_WINDOWS_SERVER)
}
if info.Version != "Windows Server 2022" {
t.Fatalf("version: got %s want Windows Server 2022", info.Version)
}
}
func TestParseWimXmlMetadataRejectsCompressedXML(t *testing.T) {
xmlData := encodeWimUTF16XML(`<?xml version="1.0"?><WIM></WIM>`)
hdrSize := binary.Size(wimHeaderDisk{})
var hdr wimHeaderDisk
hdr.ImageTag = wimImageTag
hdr.Size = uint32(hdrSize)
hdr.PartNumber = 1
hdr.TotalParts = 1
hdr.XMLData = wimResourceDesc{
FlagsAndCompressedSize: uint64(wimResFlagCompressed)<<56 | uint64(len(xmlData)),
Offset: int64(hdrSize),
OriginalSize: int64(len(xmlData)),
}
buf := &bytes.Buffer{}
_ = binary.Write(buf, binary.LittleEndian, &hdr)
buf.Write(xmlData)
_, err := parseWimXmlMetadata(bytes.NewReader(buf.Bytes()))
if err == nil {
t.Fatal("expected error for compressed XML")
}
}
func TestMapWindowsVersion(t *testing.T) {
cases := []struct {
name string
win wimWindowsInfo
distro string
version string
}{
{
name: "win10",
win: wimWindowsInfo{
Arch: 9, ProductType: "WinNT",
Version: wimXMLVersion{Major: 10, Minor: 0, Build: 19045},
},
distro: imagetools.OS_DIST_WINDOWS, version: "Windows 10",
},
{
name: "win7",
win: wimWindowsInfo{
Arch: 9, ProductType: "WinNT",
Version: wimXMLVersion{Major: 6, Minor: 1, Build: 7601},
},
distro: imagetools.OS_DIST_WINDOWS, version: "Windows 7",
},
{
name: "server2019",
win: wimWindowsInfo{
Arch: 9, ProductType: "ServerNT",
Version: wimXMLVersion{Major: 10, Minor: 0, Build: 17763},
},
distro: imagetools.OS_DIST_WINDOWS_SERVER, version: "Windows Server 2019",
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
info := mapWindowsVersion(&c.win)
if info.Distro != c.distro || info.Version != c.version {
t.Fatalf("got %s/%s want %s/%s", info.Distro, info.Version, c.distro, c.version)
}
})
}
}

View File

@@ -19,80 +19,30 @@ package isoutils
import (
"fmt"
"github.com/Microsoft/go-winio/wim"
"yunion.io/x/log"
"yunion.io/x/pkg/util/imagetools"
)
// ========== 7. 保留Windows版本识别函数适配新结构 ==========
// DetectWindowsEdition reads sources/install.wim or sources/install.esd XML metadata
// to determine Windows edition/version. Prefer .wim, fall back to .esd.
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
var lastErr error
for _, path := range []string{"sources/install.wim", "sources/install.esd"} {
if !r.FileExists(path) {
continue
}
f, err := r.GetFile(path)
if err != nil {
lastErr = fmt.Errorf("open %s: %w", path, err)
continue
}
info, err := parseWimXmlMetadata(f.NewReader())
if err != nil {
lastErr = fmt.Errorf("parse %s: %w", path, err)
continue
}
return info, nil
}
return result, nil
if lastErr != nil {
return nil, lastErr
}
return nil, fmt.Errorf("sources/install.wim or sources/install.esd not found")
}

View File

@@ -1,139 +0,0 @@
//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
}

View File

@@ -1,598 +0,0 @@
// 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
}

View File

@@ -1,898 +0,0 @@
//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
}

4
vendor/modules.txt vendored
View File

@@ -98,8 +98,6 @@ github.com/Microsoft/go-winio/internal/stringbuffer
github.com/Microsoft/go-winio/pkg/bindfilter
github.com/Microsoft/go-winio/pkg/guid
github.com/Microsoft/go-winio/vhd
github.com/Microsoft/go-winio/wim
github.com/Microsoft/go-winio/wim/lzx
# github.com/Microsoft/hcsshim v0.11.4
## explicit; go 1.18
github.com/Microsoft/hcsshim
@@ -2547,7 +2545,7 @@ sigs.k8s.io/structured-merge-diff/v4/value
# sigs.k8s.io/yaml v1.3.0
## explicit; go 1.12
sigs.k8s.io/yaml
# yunion.io/x/cloudmux v0.3.10-0-alpha.1.0.20260722023537-f41e01f2eee3
# yunion.io/x/cloudmux v0.3.10-0-alpha.1.0.20260812062623-2b79dfea19bd
## explicit; go 1.24
yunion.io/x/cloudmux/pkg/apis
yunion.io/x/cloudmux/pkg/apis/billing

View File

@@ -376,12 +376,12 @@ func (self *SAliyunClient) GetEcsMetrics(opts *cloudprovider.MetricListOptions)
}
case cloudprovider.VM_METRIC_TYPE_PROCESS_NUMBER:
metricTags = map[string]string{
"process.count_processname": "",
"process.number": "",
}
tagKey = cloudprovider.METRIC_TAG_PROCESS_NAME
case cloudprovider.VM_METRIC_TYPE_NET_TCP_CONNECTION:
metricTags = map[string]string{
"network.tcp.connection_state": "",
"net_tcpconnection": "",
}
tagKey = cloudprovider.METRIC_TAG_STATE
default:

View File

@@ -34,7 +34,17 @@ import (
const aliyunNoticeRssURL = "https://www.aliyun.com/rss/notice/zh.xml"
var htmlTagRe = regexp.MustCompile(`<[^>]*>`)
var (
// 仅匹配真实 HTML 标签,避免把版本比较里的 <= / >= 当成标签删掉
htmlTagRe = regexp.MustCompile(`(?i)</?[a-z][a-z0-9]*\b[^>]*>`)
htmlBreakTagRe = regexp.MustCompile(`(?i)<br\s*/?>`)
htmlLinkRe = regexp.MustCompile(`(?i)<a\s[^>]*href\s*=\s*["']([^"']+)["'][^>]*>([\s\S]*?)</a>`)
htmlLiTagRe = regexp.MustCompile(`(?i)<li\b[^>]*>`)
htmlTdThTagRe = regexp.MustCompile(`(?i)</t[dh]\s*>`)
htmlBlockTagRe = regexp.MustCompile(`(?i)</?(p|div|tr|ul|ol|table|thead|tbody|tfoot|h[1-6]|section|article|blockquote|hr)\b[^>]*>`)
htmlMultiLineRe = regexp.MustCompile(`\n{3,}`)
htmlSpaceRe = regexp.MustCompile(`[^\S\n]{2,}`)
)
type SNotice struct {
title string
@@ -137,16 +147,45 @@ func stripHTML(s string) string {
if len(s) == 0 {
return ""
}
// 阿里云 RSS 常把 &nbsp; 误用作标签内属性分隔符,需先还原成空格再解析标签
s = strings.ReplaceAll(s, "&nbsp;", " ")
s = strings.ReplaceAll(s, "&#160;", " ")
s = html.UnescapeString(s)
s = htmlTagRe.ReplaceAllString(s, "")
s = strings.ReplaceAll(s, "\u00a0", " ")
s = strings.ReplaceAll(s, "\r\n", "\n")
s = strings.ReplaceAll(s, "\r", "\n")
// 保留超链接:文案 (url);文案与 url 相同时只保留 url
s = htmlLinkRe.ReplaceAllStringFunc(s, func(m string) string {
parts := htmlLinkRe.FindStringSubmatch(m)
if len(parts) < 3 {
return m
}
url := strings.TrimSpace(parts[1])
text := strings.TrimSpace(htmlTagRe.ReplaceAllString(parts[2], ""))
text = strings.Join(strings.Fields(text), " ")
if len(text) == 0 || text == url {
return url
}
return fmt.Sprintf("%s (%s)", text, url)
})
s = htmlBreakTagRe.ReplaceAllString(s, "\n")
s = htmlLiTagRe.ReplaceAllString(s, "\n- ")
s = htmlTdThTagRe.ReplaceAllString(s, " | ")
s = htmlBlockTagRe.ReplaceAllString(s, "\n")
s = htmlTagRe.ReplaceAllString(s, "")
lines := strings.Split(s, "\n")
parts := []string{}
parts := make([]string, 0, len(lines))
for _, line := range lines {
line = strings.TrimSpace(line)
if len(line) > 0 {
parts = append(parts, line)
}
line = strings.Trim(line, "|")
line = strings.TrimSpace(line)
line = htmlSpaceRe.ReplaceAllString(line, " ")
parts = append(parts, line)
}
return strings.Join(parts, "\n")
s = strings.Join(parts, "\n")
s = htmlMultiLineRe.ReplaceAllString(s, "\n\n")
return strings.TrimSpace(s)
}