Update to go1.25.4
This commit is contained in:
4
VERSION
4
VERSION
@@ -1,2 +1,2 @@
|
||||
go1.25.3
|
||||
time 2025-10-13T16:08:43Z
|
||||
go1.25.4
|
||||
time 2025-10-31T13:24:27Z
|
||||
|
||||
@@ -49,9 +49,6 @@ type pkgReader struct {
|
||||
// but bitwise inverted so we can detect if we're missing the entry
|
||||
// or not.
|
||||
newindex []index
|
||||
|
||||
// indicates whether the data is reading during reshaping.
|
||||
reshaping bool
|
||||
}
|
||||
|
||||
func newPkgReader(pr pkgbits.PkgDecoder) *pkgReader {
|
||||
@@ -119,10 +116,6 @@ type reader struct {
|
||||
// find parameters/results.
|
||||
funarghack bool
|
||||
|
||||
// reshaping is used during reading exprReshape code, preventing
|
||||
// the reader from shapifying the re-shaped type.
|
||||
reshaping bool
|
||||
|
||||
// methodSym is the name of method's name, if reading a method.
|
||||
// It's nil if reading a normal function or closure body.
|
||||
methodSym *types.Sym
|
||||
@@ -937,8 +930,19 @@ func shapify(targ *types.Type, basic bool) *types.Type {
|
||||
// types, and discarding struct field names and tags. However, we'll
|
||||
// need to start tracking how type parameters are actually used to
|
||||
// implement some of these optimizations.
|
||||
pointerShaping := basic && targ.IsPtr() && !targ.Elem().NotInHeap()
|
||||
// The exception is when the type parameter is a pointer to a type
|
||||
// which `Type.HasShape()` returns true, but `Type.IsShape()` returns
|
||||
// false, like `*[]go.shape.T`. This is because the type parameter is
|
||||
// used to instantiate a generic function inside another generic function.
|
||||
// In this case, we want to keep the targ as-is, otherwise, we may lose the
|
||||
// original type after `*[]go.shape.T` is shapified to `*go.shape.uint8`.
|
||||
// See issue #54535, #71184.
|
||||
if pointerShaping && !targ.Elem().IsShape() && targ.Elem().HasShape() {
|
||||
return targ
|
||||
}
|
||||
under := targ.Underlying()
|
||||
if basic && targ.IsPtr() && !targ.Elem().NotInHeap() {
|
||||
if pointerShaping {
|
||||
under = types.NewPtr(types.Types[types.TUINT8])
|
||||
}
|
||||
|
||||
@@ -1014,25 +1018,7 @@ func (pr *pkgReader) objDictIdx(sym *types.Sym, idx index, implicits, explicits
|
||||
// arguments.
|
||||
for i, targ := range dict.targs {
|
||||
basic := r.Bool()
|
||||
isPointerShape := basic && targ.IsPtr() && !targ.Elem().NotInHeap()
|
||||
// We should not do shapify during the reshaping process, see #71184.
|
||||
// However, this only matters for shapify a pointer type, which will
|
||||
// lose the original underlying type.
|
||||
//
|
||||
// Example with a pointer type:
|
||||
//
|
||||
// - First, shapifying *[]T -> *uint8
|
||||
// - During the reshaping process, *uint8 is shapified to *go.shape.uint8
|
||||
// - This ends up with a different type with the original *[]T
|
||||
//
|
||||
// For a non-pointer type:
|
||||
//
|
||||
// - int -> go.shape.int
|
||||
// - go.shape.int -> go.shape.int
|
||||
//
|
||||
// We always end up with the identical type.
|
||||
canShapify := !pr.reshaping || !isPointerShape
|
||||
if dict.shaped && canShapify {
|
||||
if dict.shaped {
|
||||
dict.targs[i] = shapify(targ, basic)
|
||||
}
|
||||
}
|
||||
@@ -2470,10 +2456,7 @@ func (r *reader) expr() (res ir.Node) {
|
||||
|
||||
case exprReshape:
|
||||
typ := r.typ()
|
||||
old := r.reshaping
|
||||
r.reshaping = true
|
||||
x := r.expr()
|
||||
r.reshaping = old
|
||||
|
||||
if types.IdenticalStrict(x.Type(), typ) {
|
||||
return x
|
||||
@@ -2596,10 +2579,7 @@ func (r *reader) funcInst(pos src.XPos) (wrapperFn, baseFn, dictPtr ir.Node) {
|
||||
info := r.dict.subdicts[idx]
|
||||
explicits := r.p.typListIdx(info.explicits, r.dict)
|
||||
|
||||
old := r.p.reshaping
|
||||
r.p.reshaping = r.reshaping
|
||||
baseFn = r.p.objIdx(info.idx, implicits, explicits, true).(*ir.Name)
|
||||
r.p.reshaping = old
|
||||
|
||||
// TODO(mdempsky): Is there a more robust way to get the
|
||||
// dictionary pointer type here?
|
||||
|
||||
@@ -2555,7 +2555,7 @@ func rewriteStructStore(v *Value) *Value {
|
||||
|
||||
// isDirectType reports whether v represents a type
|
||||
// (a *runtime._type) whose value is stored directly in an
|
||||
// interface (i.e., is pointer or pointer-like).
|
||||
// interface (i.e., is pointer or pointer-like) and is comparable.
|
||||
func isDirectType(v *Value) bool {
|
||||
return isDirectType1(v)
|
||||
}
|
||||
@@ -2571,7 +2571,8 @@ func isDirectType1(v *Value) bool {
|
||||
return false
|
||||
}
|
||||
if ti, ok := (*lsym.Extra).(*obj.TypeInfo); ok {
|
||||
return types.IsDirectIface(ti.Type.(*types.Type))
|
||||
t := ti.Type.(*types.Type)
|
||||
return types.IsDirectIface(t) && types.IsComparable(t)
|
||||
}
|
||||
}
|
||||
return false
|
||||
@@ -2588,7 +2589,7 @@ func isDirectType2(v *Value) bool {
|
||||
|
||||
// isDirectIface reports whether v represents an itab
|
||||
// (a *runtime._itab) for a type whose value is stored directly
|
||||
// in an interface (i.e., is pointer or pointer-like).
|
||||
// in an interface (i.e., is pointer or pointer-like) and is comparable.
|
||||
func isDirectIface(v *Value) bool {
|
||||
return isDirectIface1(v, 9)
|
||||
}
|
||||
@@ -2607,7 +2608,8 @@ func isDirectIface1(v *Value, depth int) bool {
|
||||
return false
|
||||
}
|
||||
if ii, ok := (*lsym.Extra).(*obj.ItabInfo); ok {
|
||||
return types.IsDirectIface(ii.Type.(*types.Type))
|
||||
t := ii.Type.(*types.Type)
|
||||
return types.IsDirectIface(t) && types.IsComparable(t)
|
||||
}
|
||||
case OpConstNil:
|
||||
// We can treat this as direct, because if the itab is
|
||||
|
||||
78
src/cmd/compile/testdata/script/issue75461.txt
vendored
Normal file
78
src/cmd/compile/testdata/script/issue75461.txt
vendored
Normal file
@@ -0,0 +1,78 @@
|
||||
go build main.go
|
||||
! stdout .
|
||||
! stderr .
|
||||
|
||||
-- main.go --
|
||||
package main
|
||||
|
||||
import (
|
||||
"demo/registry"
|
||||
)
|
||||
|
||||
func main() {
|
||||
_ = registry.NewUserRegistry()
|
||||
}
|
||||
|
||||
-- go.mod --
|
||||
module demo
|
||||
|
||||
go 1.24
|
||||
|
||||
-- model/user.go --
|
||||
package model
|
||||
|
||||
type User struct {
|
||||
ID int
|
||||
}
|
||||
|
||||
func (c *User) String() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
-- ordered/map.go --
|
||||
package ordered
|
||||
|
||||
type OrderedMap[K comparable, V any] struct {
|
||||
m map[K]V
|
||||
}
|
||||
|
||||
func New[K comparable, V any](options ...any) *OrderedMap[K, V] {
|
||||
orderedMap := &OrderedMap[K, V]{}
|
||||
return orderedMap
|
||||
}
|
||||
|
||||
-- registry/user.go --
|
||||
package registry
|
||||
|
||||
import (
|
||||
"demo/model"
|
||||
"demo/ordered"
|
||||
)
|
||||
|
||||
type baseRegistry = Registry[model.User, *model.User]
|
||||
|
||||
type UserRegistry struct {
|
||||
*baseRegistry
|
||||
}
|
||||
|
||||
type Registry[T any, P PStringer[T]] struct {
|
||||
m *ordered.OrderedMap[string, P]
|
||||
}
|
||||
|
||||
type PStringer[T any] interface {
|
||||
*T
|
||||
String() string
|
||||
}
|
||||
|
||||
func NewRegistry[T any, P PStringer[T]]() *Registry[T, P] {
|
||||
r := &Registry[T, P]{
|
||||
m: ordered.New[string, P](),
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
func NewUserRegistry() *UserRegistry {
|
||||
return &UserRegistry{
|
||||
baseRegistry: NewRegistry[model.User](),
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build (amd64 || arm64 || mips || mipsle || mips64 || mips64le || ppc64 || ppc64le || riscv64) && !purego
|
||||
//go:build (amd64 || arm64 || ppc64 || ppc64le || riscv64) && !purego
|
||||
|
||||
package subtle
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build (!amd64 && !arm64 && !loong64 && !mips && !mipsle && !mips64 && !mips64le && !ppc64 && !ppc64le && !riscv64) || purego
|
||||
//go:build (!amd64 && !arm64 && !loong64 && !ppc64 && !ppc64le && !riscv64) || purego
|
||||
|
||||
package subtle
|
||||
|
||||
|
||||
@@ -1,153 +0,0 @@
|
||||
// Copyright 2025 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build (mips64 || mips64le) && !purego
|
||||
|
||||
#include "textflag.h"
|
||||
|
||||
// func xorBytes(dst, a, b *byte, n int)
|
||||
TEXT ·xorBytes(SB), NOSPLIT|NOFRAME, $0
|
||||
MOVV dst+0(FP), R1
|
||||
MOVV a+8(FP), R2
|
||||
MOVV b+16(FP), R3
|
||||
MOVV n+24(FP), R4
|
||||
|
||||
xor_64_check:
|
||||
SGTU $64, R4, R5 // R5 = 1 if (64 > R4)
|
||||
BNE R5, xor_32_check
|
||||
xor_64:
|
||||
MOVV (R2), R6
|
||||
MOVV 8(R2), R7
|
||||
MOVV 16(R2), R8
|
||||
MOVV 24(R2), R9
|
||||
MOVV (R3), R10
|
||||
MOVV 8(R3), R11
|
||||
MOVV 16(R3), R12
|
||||
MOVV 24(R3), R13
|
||||
XOR R6, R10
|
||||
XOR R7, R11
|
||||
XOR R8, R12
|
||||
XOR R9, R13
|
||||
MOVV R10, (R1)
|
||||
MOVV R11, 8(R1)
|
||||
MOVV R12, 16(R1)
|
||||
MOVV R13, 24(R1)
|
||||
MOVV 32(R2), R6
|
||||
MOVV 40(R2), R7
|
||||
MOVV 48(R2), R8
|
||||
MOVV 56(R2), R9
|
||||
MOVV 32(R3), R10
|
||||
MOVV 40(R3), R11
|
||||
MOVV 48(R3), R12
|
||||
MOVV 56(R3), R13
|
||||
XOR R6, R10
|
||||
XOR R7, R11
|
||||
XOR R8, R12
|
||||
XOR R9, R13
|
||||
MOVV R10, 32(R1)
|
||||
MOVV R11, 40(R1)
|
||||
MOVV R12, 48(R1)
|
||||
MOVV R13, 56(R1)
|
||||
ADDV $64, R2
|
||||
ADDV $64, R3
|
||||
ADDV $64, R1
|
||||
SUBV $64, R4
|
||||
SGTU $64, R4, R5
|
||||
BEQ R0, R5, xor_64
|
||||
BEQ R0, R4, end
|
||||
|
||||
xor_32_check:
|
||||
SGTU $32, R4, R5
|
||||
BNE R5, xor_16_check
|
||||
xor_32:
|
||||
MOVV (R2), R6
|
||||
MOVV 8(R2), R7
|
||||
MOVV 16(R2), R8
|
||||
MOVV 24(R2), R9
|
||||
MOVV (R3), R10
|
||||
MOVV 8(R3), R11
|
||||
MOVV 16(R3), R12
|
||||
MOVV 24(R3), R13
|
||||
XOR R6, R10
|
||||
XOR R7, R11
|
||||
XOR R8, R12
|
||||
XOR R9, R13
|
||||
MOVV R10, (R1)
|
||||
MOVV R11, 8(R1)
|
||||
MOVV R12, 16(R1)
|
||||
MOVV R13, 24(R1)
|
||||
ADDV $32, R2
|
||||
ADDV $32, R3
|
||||
ADDV $32, R1
|
||||
SUBV $32, R4
|
||||
BEQ R0, R4, end
|
||||
|
||||
xor_16_check:
|
||||
SGTU $16, R4, R5
|
||||
BNE R5, xor_8_check
|
||||
xor_16:
|
||||
MOVV (R2), R6
|
||||
MOVV 8(R2), R7
|
||||
MOVV (R3), R8
|
||||
MOVV 8(R3), R9
|
||||
XOR R6, R8
|
||||
XOR R7, R9
|
||||
MOVV R8, (R1)
|
||||
MOVV R9, 8(R1)
|
||||
ADDV $16, R2
|
||||
ADDV $16, R3
|
||||
ADDV $16, R1
|
||||
SUBV $16, R4
|
||||
BEQ R0, R4, end
|
||||
|
||||
xor_8_check:
|
||||
SGTU $8, R4, R5
|
||||
BNE R5, xor_4_check
|
||||
xor_8:
|
||||
MOVV (R2), R6
|
||||
MOVV (R3), R7
|
||||
XOR R6, R7
|
||||
MOVV R7, (R1)
|
||||
ADDV $8, R1
|
||||
ADDV $8, R2
|
||||
ADDV $8, R3
|
||||
SUBV $8, R4
|
||||
BEQ R0, R4, end
|
||||
|
||||
xor_4_check:
|
||||
SGTU $4, R4, R5
|
||||
BNE R5, xor_2_check
|
||||
xor_4:
|
||||
MOVW (R2), R6
|
||||
MOVW (R3), R7
|
||||
XOR R6, R7
|
||||
MOVW R7, (R1)
|
||||
ADDV $4, R2
|
||||
ADDV $4, R3
|
||||
ADDV $4, R1
|
||||
SUBV $4, R4
|
||||
BEQ R0, R4, end
|
||||
|
||||
xor_2_check:
|
||||
SGTU $2, R4, R5
|
||||
BNE R5, xor_1
|
||||
xor_2:
|
||||
MOVH (R2), R6
|
||||
MOVH (R3), R7
|
||||
XOR R6, R7
|
||||
MOVH R7, (R1)
|
||||
ADDV $2, R2
|
||||
ADDV $2, R3
|
||||
ADDV $2, R1
|
||||
SUBV $2, R4
|
||||
BEQ R0, R4, end
|
||||
|
||||
xor_1:
|
||||
MOVB (R2), R6
|
||||
MOVB (R3), R7
|
||||
XOR R6, R7
|
||||
MOVB R7, (R1)
|
||||
|
||||
end:
|
||||
RET
|
||||
@@ -1,212 +0,0 @@
|
||||
// Copyright 2025 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build (mips || mipsle) && !purego
|
||||
|
||||
#include "textflag.h"
|
||||
|
||||
// func xorBytes(dst, a, b *byte, n int)
|
||||
TEXT ·xorBytes(SB), NOSPLIT|NOFRAME, $0
|
||||
MOVW dst+0(FP), R1
|
||||
MOVW a+4(FP), R2
|
||||
MOVW b+8(FP), R3
|
||||
MOVW n+12(FP), R4
|
||||
|
||||
SGTU $64, R4, R5 // R5 = 1 if (64 > R4)
|
||||
BNE R5, xor_32_check
|
||||
xor_64:
|
||||
MOVW (R2), R6
|
||||
MOVW 4(R2), R7
|
||||
MOVW 8(R2), R8
|
||||
MOVW 12(R2), R9
|
||||
MOVW (R3), R10
|
||||
MOVW 4(R3), R11
|
||||
MOVW 8(R3), R12
|
||||
MOVW 12(R3), R13
|
||||
XOR R6, R10
|
||||
XOR R7, R11
|
||||
XOR R8, R12
|
||||
XOR R9, R13
|
||||
MOVW R10, (R1)
|
||||
MOVW R11, 4(R1)
|
||||
MOVW R12, 8(R1)
|
||||
MOVW R13, 12(R1)
|
||||
MOVW 16(R2), R6
|
||||
MOVW 20(R2), R7
|
||||
MOVW 24(R2), R8
|
||||
MOVW 28(R2), R9
|
||||
MOVW 16(R3), R10
|
||||
MOVW 20(R3), R11
|
||||
MOVW 24(R3), R12
|
||||
MOVW 28(R3), R13
|
||||
XOR R6, R10
|
||||
XOR R7, R11
|
||||
XOR R8, R12
|
||||
XOR R9, R13
|
||||
MOVW R10, 16(R1)
|
||||
MOVW R11, 20(R1)
|
||||
MOVW R12, 24(R1)
|
||||
MOVW R13, 28(R1)
|
||||
MOVW 32(R2), R6
|
||||
MOVW 36(R2), R7
|
||||
MOVW 40(R2), R8
|
||||
MOVW 44(R2), R9
|
||||
MOVW 32(R3), R10
|
||||
MOVW 36(R3), R11
|
||||
MOVW 40(R3), R12
|
||||
MOVW 44(R3), R13
|
||||
XOR R6, R10
|
||||
XOR R7, R11
|
||||
XOR R8, R12
|
||||
XOR R9, R13
|
||||
MOVW R10, 32(R1)
|
||||
MOVW R11, 36(R1)
|
||||
MOVW R12, 40(R1)
|
||||
MOVW R13, 44(R1)
|
||||
MOVW 48(R2), R6
|
||||
MOVW 52(R2), R7
|
||||
MOVW 56(R2), R8
|
||||
MOVW 60(R2), R9
|
||||
MOVW 48(R3), R10
|
||||
MOVW 52(R3), R11
|
||||
MOVW 56(R3), R12
|
||||
MOVW 60(R3), R13
|
||||
XOR R6, R10
|
||||
XOR R7, R11
|
||||
XOR R8, R12
|
||||
XOR R9, R13
|
||||
MOVW R10, 48(R1)
|
||||
MOVW R11, 52(R1)
|
||||
MOVW R12, 56(R1)
|
||||
MOVW R13, 60(R1)
|
||||
ADD $64, R2
|
||||
ADD $64, R3
|
||||
ADD $64, R1
|
||||
SUB $64, R4
|
||||
SGTU $64, R4, R5
|
||||
BEQ R0, R5, xor_64
|
||||
BEQ R0, R4, end
|
||||
|
||||
xor_32_check:
|
||||
SGTU $32, R4, R5
|
||||
BNE R5, xor_16_check
|
||||
xor_32:
|
||||
MOVW (R2), R6
|
||||
MOVW 4(R2), R7
|
||||
MOVW 8(R2), R8
|
||||
MOVW 12(R2), R9
|
||||
MOVW (R3), R10
|
||||
MOVW 4(R3), R11
|
||||
MOVW 8(R3), R12
|
||||
MOVW 12(R3), R13
|
||||
XOR R6, R10
|
||||
XOR R7, R11
|
||||
XOR R8, R12
|
||||
XOR R9, R13
|
||||
MOVW R10, (R1)
|
||||
MOVW R11, 4(R1)
|
||||
MOVW R12, 8(R1)
|
||||
MOVW R13, 12(R1)
|
||||
MOVW 16(R2), R6
|
||||
MOVW 20(R2), R7
|
||||
MOVW 24(R2), R8
|
||||
MOVW 28(R2), R9
|
||||
MOVW 16(R3), R10
|
||||
MOVW 20(R3), R11
|
||||
MOVW 24(R3), R12
|
||||
MOVW 28(R3), R13
|
||||
XOR R6, R10
|
||||
XOR R7, R11
|
||||
XOR R8, R12
|
||||
XOR R9, R13
|
||||
MOVW R10, 16(R1)
|
||||
MOVW R11, 20(R1)
|
||||
MOVW R12, 24(R1)
|
||||
MOVW R13, 28(R1)
|
||||
ADD $32, R2
|
||||
ADD $32, R3
|
||||
ADD $32, R1
|
||||
SUB $32, R4
|
||||
BEQ R0, R4, end
|
||||
|
||||
xor_16_check:
|
||||
SGTU $16, R4, R5
|
||||
BNE R5, xor_8_check
|
||||
xor_16:
|
||||
MOVW (R2), R6
|
||||
MOVW 4(R2), R7
|
||||
MOVW 8(R2), R8
|
||||
MOVW 12(R2), R9
|
||||
MOVW (R3), R10
|
||||
MOVW 4(R3), R11
|
||||
MOVW 8(R3), R12
|
||||
MOVW 12(R3), R13
|
||||
XOR R6, R10
|
||||
XOR R7, R11
|
||||
XOR R8, R12
|
||||
XOR R9, R13
|
||||
MOVW R10, (R1)
|
||||
MOVW R11, 4(R1)
|
||||
MOVW R12, 8(R1)
|
||||
MOVW R13, 12(R1)
|
||||
ADD $16, R2
|
||||
ADD $16, R3
|
||||
ADD $16, R1
|
||||
SUB $16, R4
|
||||
BEQ R0, R4, end
|
||||
|
||||
xor_8_check:
|
||||
SGTU $8, R4, R5
|
||||
BNE R5, xor_4_check
|
||||
xor_8:
|
||||
MOVW (R2), R6
|
||||
MOVW 4(R2), R7
|
||||
MOVW (R3), R8
|
||||
MOVW 4(R3), R9
|
||||
XOR R6, R8
|
||||
XOR R7, R9
|
||||
MOVW R8, (R1)
|
||||
MOVW R9, 4(R1)
|
||||
ADD $8, R1
|
||||
ADD $8, R2
|
||||
ADD $8, R3
|
||||
SUB $8, R4
|
||||
BEQ R0, R4, end
|
||||
|
||||
xor_4_check:
|
||||
SGTU $4, R4, R5
|
||||
BNE R5, xor_2_check
|
||||
xor_4:
|
||||
MOVW (R2), R6
|
||||
MOVW (R3), R7
|
||||
XOR R6, R7
|
||||
MOVW R7, (R1)
|
||||
ADD $4, R2
|
||||
ADD $4, R3
|
||||
ADD $4, R1
|
||||
SUB $4, R4
|
||||
BEQ R0, R4, end
|
||||
|
||||
xor_2_check:
|
||||
SGTU $2, R4, R5
|
||||
BNE R5, xor_1
|
||||
xor_2:
|
||||
MOVH (R2), R6
|
||||
MOVH (R3), R7
|
||||
XOR R6, R7
|
||||
MOVH R7, (R1)
|
||||
ADD $2, R2
|
||||
ADD $2, R3
|
||||
ADD $2, R1
|
||||
SUB $2, R4
|
||||
BEQ R0, R4, end
|
||||
|
||||
xor_1:
|
||||
MOVB (R2), R6
|
||||
MOVB (R3), R7
|
||||
XOR R6, R7
|
||||
MOVB R7, (R1)
|
||||
|
||||
end:
|
||||
RET
|
||||
@@ -91,7 +91,15 @@ func Decode(data []byte) (p *Block, rest []byte) {
|
||||
// the byte array, we'll accept the start string without it.
|
||||
rest = data
|
||||
|
||||
endTrailerIndex := 0
|
||||
for {
|
||||
// If we've already tried parsing a block, skip past the END we already
|
||||
// saw.
|
||||
if endTrailerIndex < 0 || endTrailerIndex > len(rest) {
|
||||
return nil, data
|
||||
}
|
||||
rest = rest[endTrailerIndex:]
|
||||
|
||||
// Find the first END line, and then find the last BEGIN line before
|
||||
// the end line. This lets us skip any repeated BEGIN lines that don't
|
||||
// have a matching END.
|
||||
@@ -99,10 +107,10 @@ func Decode(data []byte) (p *Block, rest []byte) {
|
||||
if endIndex < 0 {
|
||||
return nil, data
|
||||
}
|
||||
endTrailerIndex := endIndex + len(pemEnd)
|
||||
endTrailerIndex = endIndex + len(pemEnd)
|
||||
beginIndex := bytes.LastIndex(rest[:endIndex], pemStart[1:])
|
||||
if beginIndex < 0 || beginIndex > 0 && rest[beginIndex-1] != '\n' {
|
||||
return nil, data
|
||||
if beginIndex < 0 || (beginIndex > 0 && rest[beginIndex-1] != '\n') {
|
||||
continue
|
||||
}
|
||||
rest = rest[beginIndex+len(pemStart)-1:]
|
||||
endIndex -= beginIndex + len(pemStart) - 1
|
||||
@@ -111,11 +119,11 @@ func Decode(data []byte) (p *Block, rest []byte) {
|
||||
var typeLine []byte
|
||||
var consumed int
|
||||
typeLine, rest, consumed = getLine(rest)
|
||||
endIndex -= consumed
|
||||
endTrailerIndex -= consumed
|
||||
if !bytes.HasSuffix(typeLine, pemEndOfLine) {
|
||||
continue
|
||||
}
|
||||
endIndex -= consumed
|
||||
endTrailerIndex -= consumed
|
||||
typeLine = typeLine[0 : len(typeLine)-len(pemEndOfLine)]
|
||||
|
||||
p = &Block{
|
||||
|
||||
@@ -639,3 +639,104 @@ func TestBadEncode(t *testing.T) {
|
||||
}
|
||||
|
||||
func testingKey(s string) string { return strings.ReplaceAll(s, "TESTING KEY", "PRIVATE KEY") }
|
||||
|
||||
func TestDecodeStrangeCases(t *testing.T) {
|
||||
sentinelType := "TEST BLOCK"
|
||||
sentinelBytes := []byte("hello")
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
pem string
|
||||
}{
|
||||
{
|
||||
name: "invalid section (not base64)",
|
||||
pem: `-----BEGIN COMMENT-----
|
||||
foo foo foo
|
||||
-----END COMMENT-----
|
||||
-----BEGIN TEST BLOCK-----
|
||||
aGVsbG8=
|
||||
-----END TEST BLOCK-----`,
|
||||
},
|
||||
{
|
||||
name: "leading garbage on block",
|
||||
pem: `foo foo foo-----BEGIN CERTIFICATE-----
|
||||
MCowBQYDK2VwAyEApVjJeLW5MoP6uR3+OeITokM+rBDng6dgl1vvhcy+wws=
|
||||
-----END PUBLIC KEY-----
|
||||
-----BEGIN TEST BLOCK-----
|
||||
aGVsbG8=
|
||||
-----END TEST BLOCK-----`,
|
||||
},
|
||||
{
|
||||
name: "leading garbage",
|
||||
pem: `foo foo foo
|
||||
-----BEGIN TEST BLOCK-----
|
||||
aGVsbG8=
|
||||
-----END TEST BLOCK-----`,
|
||||
},
|
||||
{
|
||||
name: "leading partial block",
|
||||
pem: `foo foo foo
|
||||
-----END COMMENT-----
|
||||
-----BEGIN TEST BLOCK-----
|
||||
aGVsbG8=
|
||||
-----END TEST BLOCK-----`,
|
||||
},
|
||||
{
|
||||
name: "multiple BEGIN",
|
||||
pem: `-----BEGIN TEST BLOCK-----
|
||||
-----BEGIN TEST BLOCK-----
|
||||
-----BEGIN TEST BLOCK-----
|
||||
aGVsbG8=
|
||||
-----END TEST BLOCK-----`,
|
||||
},
|
||||
{
|
||||
name: "multiple END",
|
||||
pem: `-----BEGIN TEST BLOCK-----
|
||||
aGVsbG8=
|
||||
-----END TEST BLOCK-----
|
||||
-----END TEST BLOCK-----
|
||||
-----END TEST BLOCK-----`,
|
||||
},
|
||||
{
|
||||
name: "leading malformed BEGIN",
|
||||
pem: `-----BEGIN PUBLIC KEY
|
||||
aGVsbG8=
|
||||
-----END PUBLIC KEY-----
|
||||
-----BEGIN TEST BLOCK-----
|
||||
aGVsbG8=
|
||||
-----END TEST BLOCK-----`,
|
||||
},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
block, _ := Decode([]byte(tc.pem))
|
||||
if block == nil {
|
||||
t.Fatal("expected valid block")
|
||||
}
|
||||
if block.Type != sentinelType {
|
||||
t.Fatalf("unexpected block returned, got type %q, want type %q", block.Type, sentinelType)
|
||||
}
|
||||
if !bytes.Equal(block.Bytes, sentinelBytes) {
|
||||
t.Fatalf("unexpected block content, got %x, want %x", block.Bytes, sentinelBytes)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestJustEnd(t *testing.T) {
|
||||
pemData := `
|
||||
-----END PUBLIC KEY-----`
|
||||
|
||||
block, _ := Decode([]byte(pemData))
|
||||
if block != nil {
|
||||
t.Fatal("unexpected block")
|
||||
}
|
||||
}
|
||||
|
||||
func FuzzDecode(f *testing.F) {
|
||||
f.Fuzz(func(t *testing.T, data []byte) {
|
||||
Decode(data)
|
||||
})
|
||||
}
|
||||
|
||||
func TestMissingEndTrailer(t *testing.T) {
|
||||
Decode([]byte{0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x42, 0x45, 0x47, 0x49, 0x4e, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0xa, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x45, 0x4e, 0x44, 0x20})
|
||||
}
|
||||
|
||||
@@ -204,7 +204,7 @@ func Deleteat(dirfd syscall.Handle, name string, options uint32) error {
|
||||
var h syscall.Handle
|
||||
err := NtOpenFile(
|
||||
&h,
|
||||
SYNCHRONIZE|DELETE,
|
||||
SYNCHRONIZE|FILE_READ_ATTRIBUTES|DELETE,
|
||||
objAttrs,
|
||||
&IO_STATUS_BLOCK{},
|
||||
FILE_SHARE_DELETE|FILE_SHARE_READ|FILE_SHARE_WRITE,
|
||||
@@ -215,14 +215,22 @@ func Deleteat(dirfd syscall.Handle, name string, options uint32) error {
|
||||
}
|
||||
defer syscall.CloseHandle(h)
|
||||
|
||||
const (
|
||||
FileDispositionInformation = 13
|
||||
FileDispositionInformationEx = 64
|
||||
)
|
||||
if TestDeleteatFallback {
|
||||
return deleteatFallback(h)
|
||||
}
|
||||
|
||||
const FileDispositionInformationEx = 64
|
||||
|
||||
// First, attempt to delete the file using POSIX semantics
|
||||
// (which permit a file to be deleted while it is still open).
|
||||
// This matches the behavior of DeleteFileW.
|
||||
//
|
||||
// The following call uses features available on different Windows versions:
|
||||
// - FILE_DISPOSITION_INFORMATION_EX: Windows 10, version 1607 (aka RS1)
|
||||
// - FILE_DISPOSITION_POSIX_SEMANTICS: Windows 10, version 1607 (aka RS1)
|
||||
// - FILE_DISPOSITION_IGNORE_READONLY_ATTRIBUTE: Windows 10, version 1809 (aka RS5)
|
||||
//
|
||||
// Also, some file systems, like FAT32, don't support POSIX semantics.
|
||||
err = NtSetInformationFile(
|
||||
h,
|
||||
&IO_STATUS_BLOCK{},
|
||||
@@ -241,28 +249,57 @@ func Deleteat(dirfd syscall.Handle, name string, options uint32) error {
|
||||
switch err {
|
||||
case nil:
|
||||
return nil
|
||||
case STATUS_CANNOT_DELETE, STATUS_DIRECTORY_NOT_EMPTY:
|
||||
case STATUS_INVALID_INFO_CLASS, // the operating system doesn't support FileDispositionInformationEx
|
||||
STATUS_INVALID_PARAMETER, // the operating system doesn't support one of the flags
|
||||
STATUS_NOT_SUPPORTED: // the file system doesn't support FILE_DISPOSITION_INFORMATION_EX or one of the flags
|
||||
return deleteatFallback(h)
|
||||
default:
|
||||
return err.(NTStatus).Errno()
|
||||
}
|
||||
}
|
||||
|
||||
// If the prior deletion failed, the filesystem either doesn't support
|
||||
// POSIX semantics (for example, FAT), or hasn't implemented
|
||||
// FILE_DISPOSITION_INFORMATION_EX.
|
||||
//
|
||||
// Try again.
|
||||
err = NtSetInformationFile(
|
||||
// TestDeleteatFallback should only be used for testing purposes.
|
||||
// When set, [Deleteat] uses the fallback path unconditionally.
|
||||
var TestDeleteatFallback bool
|
||||
|
||||
// deleteatFallback is a deleteat implementation that strives
|
||||
// for compatibility with older Windows versions and file systems
|
||||
// over performance.
|
||||
func deleteatFallback(h syscall.Handle) error {
|
||||
var data syscall.ByHandleFileInformation
|
||||
if err := syscall.GetFileInformationByHandle(h, &data); err == nil && data.FileAttributes&syscall.FILE_ATTRIBUTE_READONLY != 0 {
|
||||
// Remove read-only attribute. Reopen the file, as it was previously open without FILE_WRITE_ATTRIBUTES access
|
||||
// in order to maximize compatibility in the happy path.
|
||||
wh, err := ReOpenFile(h,
|
||||
FILE_WRITE_ATTRIBUTES,
|
||||
FILE_SHARE_READ|FILE_SHARE_WRITE|FILE_SHARE_DELETE,
|
||||
syscall.FILE_FLAG_OPEN_REPARSE_POINT|syscall.FILE_FLAG_BACKUP_SEMANTICS,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = SetFileInformationByHandle(
|
||||
wh,
|
||||
FileBasicInfo,
|
||||
unsafe.Pointer(&FILE_BASIC_INFO{
|
||||
FileAttributes: data.FileAttributes &^ FILE_ATTRIBUTE_READONLY,
|
||||
}),
|
||||
uint32(unsafe.Sizeof(FILE_BASIC_INFO{})),
|
||||
)
|
||||
syscall.CloseHandle(wh)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return SetFileInformationByHandle(
|
||||
h,
|
||||
&IO_STATUS_BLOCK{},
|
||||
unsafe.Pointer(&FILE_DISPOSITION_INFORMATION{
|
||||
FileDispositionInfo,
|
||||
unsafe.Pointer(&FILE_DISPOSITION_INFO{
|
||||
DeleteFile: true,
|
||||
}),
|
||||
uint32(unsafe.Sizeof(FILE_DISPOSITION_INFORMATION{})),
|
||||
FileDispositionInformation,
|
||||
uint32(unsafe.Sizeof(FILE_DISPOSITION_INFO{})),
|
||||
)
|
||||
if st, ok := err.(NTStatus); ok {
|
||||
return st.Errno()
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func Renameat(olddirfd syscall.Handle, oldpath string, newdirfd syscall.Handle, newpath string) error {
|
||||
|
||||
@@ -19,6 +19,7 @@ const (
|
||||
FileBasicInfo = 0 // FILE_BASIC_INFO
|
||||
FileStandardInfo = 1 // FILE_STANDARD_INFO
|
||||
FileNameInfo = 2 // FILE_NAME_INFO
|
||||
FileDispositionInfo = 4 // FILE_DISPOSITION_INFO
|
||||
FileStreamInfo = 7 // FILE_STREAM_INFO
|
||||
FileCompressionInfo = 8 // FILE_COMPRESSION_INFO
|
||||
FileAttributeTagInfo = 9 // FILE_ATTRIBUTE_TAG_INFO
|
||||
|
||||
@@ -529,6 +529,8 @@ const (
|
||||
//sys GetOverlappedResult(handle syscall.Handle, overlapped *syscall.Overlapped, done *uint32, wait bool) (err error)
|
||||
//sys CreateNamedPipe(name *uint16, flags uint32, pipeMode uint32, maxInstances uint32, outSize uint32, inSize uint32, defaultTimeout uint32, sa *syscall.SecurityAttributes) (handle syscall.Handle, err error) [failretval==syscall.InvalidHandle] = CreateNamedPipeW
|
||||
|
||||
//sys ReOpenFile(filehandle syscall.Handle, desiredAccess uint32, shareMode uint32, flagAndAttributes uint32) (handle syscall.Handle, err error)
|
||||
|
||||
// NTStatus corresponds with NTSTATUS, error values returned by ntdll.dll and
|
||||
// other native functions.
|
||||
type NTStatus uint32
|
||||
@@ -554,6 +556,9 @@ const (
|
||||
STATUS_NOT_A_DIRECTORY NTStatus = 0xC0000103
|
||||
STATUS_CANNOT_DELETE NTStatus = 0xC0000121
|
||||
STATUS_REPARSE_POINT_ENCOUNTERED NTStatus = 0xC000050B
|
||||
STATUS_NOT_SUPPORTED NTStatus = 0xC00000BB
|
||||
STATUS_INVALID_PARAMETER NTStatus = 0xC000000D
|
||||
STATUS_INVALID_INFO_CLASS NTStatus = 0xC0000003
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@@ -199,6 +199,11 @@ const (
|
||||
FILE_OPEN_FOR_FREE_SPACE_QUERY = 0x00800000
|
||||
)
|
||||
|
||||
// https://learn.microsoft.com/en-us/windows/win32/api/winbase/ns-winbase-file_disposition_info
|
||||
type FILE_DISPOSITION_INFO struct {
|
||||
DeleteFile bool
|
||||
}
|
||||
|
||||
// https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/ntddk/ns-ntddk-_file_disposition_information
|
||||
type FILE_DISPOSITION_INFORMATION struct {
|
||||
DeleteFile bool
|
||||
|
||||
@@ -84,6 +84,7 @@ var (
|
||||
procModule32NextW = modkernel32.NewProc("Module32NextW")
|
||||
procMoveFileExW = modkernel32.NewProc("MoveFileExW")
|
||||
procMultiByteToWideChar = modkernel32.NewProc("MultiByteToWideChar")
|
||||
procReOpenFile = modkernel32.NewProc("ReOpenFile")
|
||||
procRtlLookupFunctionEntry = modkernel32.NewProc("RtlLookupFunctionEntry")
|
||||
procRtlVirtualUnwind = modkernel32.NewProc("RtlVirtualUnwind")
|
||||
procSetFileInformationByHandle = modkernel32.NewProc("SetFileInformationByHandle")
|
||||
@@ -430,6 +431,15 @@ func MultiByteToWideChar(codePage uint32, dwFlags uint32, str *byte, nstr int32,
|
||||
return
|
||||
}
|
||||
|
||||
func ReOpenFile(filehandle syscall.Handle, desiredAccess uint32, shareMode uint32, flagAndAttributes uint32) (handle syscall.Handle, err error) {
|
||||
r0, _, e1 := syscall.Syscall6(procReOpenFile.Addr(), 4, uintptr(filehandle), uintptr(desiredAccess), uintptr(shareMode), uintptr(flagAndAttributes), 0, 0)
|
||||
handle = syscall.Handle(r0)
|
||||
if handle == 0 {
|
||||
err = errnoErr(e1)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func RtlLookupFunctionEntry(pc uintptr, baseAddress *uintptr, table unsafe.Pointer) (ret *RUNTIME_FUNCTION) {
|
||||
r0, _, _ := syscall.Syscall(procRtlLookupFunctionEntry.Addr(), 3, uintptr(pc), uintptr(unsafe.Pointer(baseAddress)), uintptr(table))
|
||||
ret = (*RUNTIME_FUNCTION)(unsafe.Pointer(r0))
|
||||
|
||||
@@ -673,13 +673,13 @@ func parseHost(host string) (string, error) {
|
||||
|
||||
// Per RFC 3986, only a host identified by a valid
|
||||
// IPv6 address can be enclosed by square brackets.
|
||||
// This excludes any IPv4 or IPv4-mapped addresses.
|
||||
// This excludes any IPv4, but notably not IPv4-mapped addresses.
|
||||
addr, err := netip.ParseAddr(unescapedHostname)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("invalid host: %w", err)
|
||||
}
|
||||
if addr.Is4() || addr.Is4In6() {
|
||||
return "", errors.New("invalid IPv6 host")
|
||||
if addr.Is4() {
|
||||
return "", errors.New("invalid IP-literal")
|
||||
}
|
||||
return "[" + unescapedHostname + "]" + unescapedColonPort, nil
|
||||
} else if i := strings.LastIndex(host, ":"); i != -1 {
|
||||
|
||||
@@ -726,7 +726,7 @@ var parseRequestURLTests = []struct {
|
||||
{"https://[2001:db8::1]/path", true}, // compressed IPv6 address with path
|
||||
{"https://[fe80::1%25eth0]/path?query=1", true}, // link-local with zone, path, and query
|
||||
|
||||
{"https://[::ffff:192.0.2.1]", false},
|
||||
{"https://[::ffff:192.0.2.1]", true},
|
||||
{"https://[:1] ", false},
|
||||
{"https://[1:2:3:4:5:6:7:8:9]", false},
|
||||
{"https://[1::1::1]", false},
|
||||
@@ -1672,16 +1672,17 @@ func TestParseErrors(t *testing.T) {
|
||||
{"cache_object:foo/bar", true},
|
||||
{"cache_object/:foo/bar", false},
|
||||
|
||||
{"http://[192.168.0.1]/", true}, // IPv4 in brackets
|
||||
{"http://[192.168.0.1]:8080/", true}, // IPv4 in brackets with port
|
||||
{"http://[::ffff:192.168.0.1]/", true}, // IPv4-mapped IPv6 in brackets
|
||||
{"http://[::ffff:192.168.0.1]:8080/", true}, // IPv4-mapped IPv6 in brackets with port
|
||||
{"http://[::ffff:c0a8:1]/", true}, // IPv4-mapped IPv6 in brackets (hex)
|
||||
{"http://[not-an-ip]/", true}, // invalid IP string in brackets
|
||||
{"http://[fe80::1%foo]/", true}, // invalid zone format in brackets
|
||||
{"http://[fe80::1", true}, // missing closing bracket
|
||||
{"http://fe80::1]/", true}, // missing opening bracket
|
||||
{"http://[test.com]/", true}, // domain name in brackets
|
||||
{"http://[192.168.0.1]/", true}, // IPv4 in brackets
|
||||
{"http://[192.168.0.1]:8080/", true}, // IPv4 in brackets with port
|
||||
{"http://[::ffff:192.168.0.1]/", false}, // IPv4-mapped IPv6 in brackets
|
||||
{"http://[::ffff:192.168.0.1000]/", true}, // Out of range IPv4-mapped IPv6 in brackets
|
||||
{"http://[::ffff:192.168.0.1]:8080/", false}, // IPv4-mapped IPv6 in brackets with port
|
||||
{"http://[::ffff:c0a8:1]/", false}, // IPv4-mapped IPv6 in brackets (hex)
|
||||
{"http://[not-an-ip]/", true}, // invalid IP string in brackets
|
||||
{"http://[fe80::1%foo]/", true}, // invalid zone format in brackets
|
||||
{"http://[fe80::1", true}, // missing closing bracket
|
||||
{"http://fe80::1]/", true}, // missing opening bracket
|
||||
{"http://[test.com]/", true}, // domain name in brackets
|
||||
}
|
||||
for _, tt := range tests {
|
||||
u, err := Parse(tt.in)
|
||||
|
||||
@@ -236,6 +236,23 @@ func TestRemoveAllLongPathRelative(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoveAllFallback(t *testing.T) {
|
||||
windows.TestDeleteatFallback = true
|
||||
t.Cleanup(func() { windows.TestDeleteatFallback = false })
|
||||
|
||||
dir := t.TempDir()
|
||||
if err := os.WriteFile(filepath.Join(dir, "file1"), []byte{}, 0700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(dir, "file2"), []byte{}, 0400); err != nil { // read-only file
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := os.RemoveAll(dir); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func testLongPathAbs(t *testing.T, target string) {
|
||||
t.Helper()
|
||||
testWalkFn := func(path string, info os.FileInfo, err error) error {
|
||||
|
||||
@@ -22,10 +22,17 @@ const (
|
||||
// On AMD64, virtual addresses are 48-bit (or 57-bit) sign-extended.
|
||||
// Other archs are 48-bit zero-extended.
|
||||
//
|
||||
// We use one extra bit to placate systems which simulate amd64 binaries on
|
||||
// an arm64 host. Allocated arm64 addresses could be as high as 1<<48-1,
|
||||
// which would be invalid if we assumed 48-bit sign-extended addresses.
|
||||
// See issue 69255.
|
||||
// (Note that this does not help the other way around, simluating arm64
|
||||
// on amd64, but we don't have that problem at the moment.)
|
||||
//
|
||||
// On s390x, virtual addresses are 64-bit. There's not much we
|
||||
// can do about this, so we just hope that the kernel doesn't
|
||||
// get to really high addresses and panic if it does.
|
||||
defaultAddrBits = 48
|
||||
defaultAddrBits = 48 + 1
|
||||
|
||||
// On AIX, 64-bit addresses are split into 36-bit segment number and 28-bit
|
||||
// offset in segment. Segment numbers in the range 0x0A0000000-0x0AFFFFFFF(LSA)
|
||||
|
||||
35
test/fixedbugs/issue76008.go
Normal file
35
test/fixedbugs/issue76008.go
Normal file
@@ -0,0 +1,35 @@
|
||||
// run
|
||||
|
||||
// Copyright 2025 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package main
|
||||
|
||||
import "runtime"
|
||||
|
||||
func main() {
|
||||
shouldPanic(func() {
|
||||
g = any(func() {}) == any(func() {})
|
||||
})
|
||||
shouldPanic(func() {
|
||||
g = any(map[int]int{}) == any(map[int]int{})
|
||||
})
|
||||
shouldPanic(func() {
|
||||
g = any([]int{}) == any([]int{})
|
||||
})
|
||||
}
|
||||
|
||||
var g bool
|
||||
|
||||
func shouldPanic(f func()) {
|
||||
defer func() {
|
||||
err := recover()
|
||||
if err == nil {
|
||||
_, _, line, _ := runtime.Caller(2)
|
||||
println("did not panic at line", line+1)
|
||||
}
|
||||
}()
|
||||
|
||||
f()
|
||||
}
|
||||
Reference in New Issue
Block a user